管理后台前端动作审核列表页面功能实现
在上一篇教程中,我们完成了管理后台前端路由定义,并通过 Vue Router 的路由元信息功能实现了在前端路由中进行权限判断及请求拦截。在这篇教程中我们就要来真正为动作审核列表编写管理后台页面,对列表数据进行渲染并实现对每个审核动作的通过和拒绝操作。
第一步:为后台布局文件 Admin.vue 添加页面头部和导航组件
在开始之前,首先需要填充 resources/assets/js/layouts/Admin.vue
组件,和前台布局文件 Layout.vue
一样,需要在该组件中引入公共的页面头部、导航组件、其他辅助组件和子页面组件。
为此,首先为管理后台创建一个 resources/assets/js/components/admin/AdminHeader.vue
组件用于渲染管理后台头部,该组件用于在页面顶部渲染应用 Logo 和用户登录等元素(在应用前台这些元素是在导航组件中渲染的):
<style lang="scss">
@import '~@/abstracts/_variables.scss';
header {
background-color: #FFFFFF;
height: 75px;
z-index: 9999;
position: absolute;
top: 0;
left: 0;
right: 0;
img.logo {
margin: auto;
margin-top: 22.5px;
margin-bottom: 22.5px;
display: block;
}
img.hamburger {
float: right;
margin-right: 18px;
margin-top: 30px;
cursor: pointer;
}
img.avatar {
float: right;
margin-right: 20px;
width: 40px;
height: 40px;
border-radius: 20px;
margin-top: 18px;
}
&:after {
content: "";
display: table;
clear: both;
}
}
/* Small only */
@media screen and (max-width: 39.9375em) {
nav.top-navigation {
span.login {
display: none;
}
img.hamburger {
margin-top: 26px;
}
}
}
/* Medium only */
@media screen and (min-width: 40em) and (max-width: 63.9375em) {
}
/* Large only */
@media screen and (min-width: 64em) and (max-width: 74.9375em) {
}
</style>
<template>
<header class="admin-header">
<div class="grid-x">
<div class="large-4 medium-4 small-4 cell">
</div>
<div class="large-4 medium-4 small-4 cell">
<router-link :to="{ name: 'cafes'}">
<img src="/storage/img/logo.svg" class="logo"/>
</router-link>
</div>
<div class="large-4 medium-4 small-4 cell">
<img class="hamburger" src="/storage/img/hamburger.svg" v-on:click="setShowPopOut()"/>
<img class="avatar" v-if="user !== '' && userLoadStatus === 2" :src="user.avatar"
v-show="userLoadStatus === 2"/>
<span class="login" v-if="user === ''" v-on:click="login()">登录</span>
</div>
</div>
</header>
</template>
<script>
export default {
computed: {
userLoadStatus() {
return this.$store.getters.getUserLoadStatus();
},
user() {
return this.$store.getters.getUser;
}
},
methods: {
setShowPopOut() {
this.$store.dispatch('toggleShowPopOut', {showPopOut: true});
}
}
}
</script>
然后创建管理后台导航组件 resources/assets/js/components/admin/Navigation.vue
用于渲染类似页面面包屑导航之类的元素:
<style lang="scss">
@import '~@/abstracts/_variables.scss';
nav.admin-navigation{
div.admin-link{
font-size: 16px;
font-weight: bold;
font-family: "Lato", sans-serif;
text-transform: uppercase;
padding-top: 15px;
padding-bottom: 15px;
a{
color: black;
&.router-link-active{
color: $secondary-color;
}
}
}
}
</style>
<template>
<nav class="admin-navigation">
<div class="admin-link">
<router-link :to="{ name: 'admin-actions' }">
动作审核列表
</router-link>
</div>
</nav>
</template>
<script>
export default {
}
</script>
最后,打开布局文件 resources/assets/js/layouts/Admin.vue
,编写代码如下:
<style lang="scss">
@import '~@/abstracts/_variables.scss';
div#admin-layout {
div#page-container {
margin-top: 75px;
}
}
</style>
<template>
<div id="admin-layout">
<admin-header></admin-header>
<success-notification></success-notification>
<error-notification></error-notification>
<div class="grid-container" id="page-container">
<div class="grid-x grid-padding-x">
<div class="large-3 medium-4 cell">
<navigation></navigation>
</div>
<div class="large-9 medium-8 cell">
<router-view></router-view>
</div>
</div>
</div>
<pop-out></pop-out>
</div>
</template>
<script>
import SuccessNotification from '../components/global/SuccessNotification.vue';
import ErrorNotification from '../components/global/ErrorNotification.vue';
import AdminHeader from '../components/admin/AdminHeader.vue';
import Navigation from '../components/admin/Navigation.vue';
import PopOut from '../components/global/PopOut.vue';
export default {
components: {
SuccessNotification,
ErrorNotification,
AdminHeader,
Navigation,
PopOut
},
created() {
this.$store.dispatch('loadBrewMethods');
},
computed: {
user() {
return this.$store.getters.getUser;
}
}
}
</script>
在这个布局文件中,我们引入了刚刚创建的后台头部组件、导航组件,以及和前台共用消息通知组件、滑出菜单组件,子页面组件还是通过内置的 router-view
引入,下面我们就要来创建用于渲染动作审核列表的子页面组件。
第二步:为动作审核子页面定义前端路由、API 调用和 Vuex 模块
接下来我们在前端路由文件 resources/assets/js/routes.js
中定义动作审核子页面路由,改写之前的 admin
路由定义如下:
{
path: '/admin',
name: 'admin',
component: Vue.component('Admin', require('./layouts/Admin.vue')),
beforeEnter: requireAuth,
meta: {
permission: 'owner'
},
children: [
{
path: 'actions',
name: 'admin-actions',
component: Vue.component('AdminActions', require('./pages/admin/Actions.vue')),
meta: {
permission: 'owner'
}
},
{
path: '_=_',
redirect: '/'
}
]
}
创建一个空的子页面组件 resources/assets/js/pages/admin/Actions.vue
,具体代码稍后编写,我们先创建一个新的 API 调用文件 resources/assets/js/api/admin/actions.js
,并在其中定义对动作审核相关后台 API 的请求:
/*
Imports the Roast API URL from the config.
*/
import {ROAST_CONFIG} from '../../config.js';
export default {
/*
GET /api/v1/admin/actions
*/
getActions: function () {
return axios.get(ROAST_CONFIG.API_URL + '/admin/actions');
},
/*
PUT /admin/v1/admin/actions/{action}/approve
*/
putApproveAction: function (id) {
return axios.put(ROAST_CONFIG.API_URL + '/admin/actions/' + id + '/approve');
},
/*
PUT /admin/v1/admin/actions/{action}/deny
*/
putDenyAction: function (id) {
return axios.put(ROAST_CONFIG.API_URL + '/admin/actions/' + id + '/deny');
}
}
然后为审核动作创建一个新的 Vuex 模块 resources/assets/js/modules/admin/actions.js
,编写模块代码如下:
/*
|-------------------------------------------------------------------------------
| VUEX modules/admin/actions.js
|-------------------------------------------------------------------------------
| The Vuex data store for the admin actions
*/
import ActionsAPI from '../../api/admin/actions.js';
export const actions = {
/*
Defines the state being monitored for the module.
*/
state: {
actions: [],
actionsLoadStatus: 0,
actionApproveStatus: 0,
actionDeniedStatus: 0
},
actions: {
loadAdminActions({commit}) {
commit('setActionsLoadStatus', 1);
ActionsAPI.getActions()
.then(function (response) {
commit('setActions', response.data);
commit('setActionsLoadStatus', 2);
})
.catch(function () {
commit('setActions', []);
commit('setActionsLoadStatus', 3);
});
},
approveAction({commit, state, dispatch}, data) {
commit('setActionApproveStatus', 1);
ActionsAPI.putApproveAction(data.id)
.then(function (response) {
commit('setActionApproveStatus', 2);
dispatch('loadAdminActions');
})
.catch(function () {
commit('setActionApproveStatus', 3);
});
},
denyAction({commit, state, dispatch}, data) {
commit('setActionDeniedStatus', 1);
ActionsAPI.putDenyAction(data.id)
.then(function (response) {
commit('setActionDeniedStatus', 2);
dispatch('loadAdminActions');
})
.catch(function () {
commit('setActionDeniedStatus', 3);
});
}
},
mutations: {
setActionsLoadStatus(state, status) {
state.actionsLoadStatus = status;
},
setActions(state, actions) {
state.actions = actions;
},
setActionApproveStatus(state, status) {
state.actionApproveStatus = status;
},
setActionDeniedStatus(state, status) {
state.actionDeniedStatus = status;
}
},
getters: {
getActions(state) {
return state.actions;
},
getActionsLoadStatus(state) {
return state.actionsLoadStatus;
},
getActionApproveStatus(state) {
return state.actionApproveStatus;
},
getActionDeniedStatus(state) {
return state.actionDeniedStatus;
}
}
};
第三步:实现动作审核列表子页面组件
最后,我们编写动作审核列表子页面组件 resources/assets/js/pages/admin/Actions.vue
代码如下:
<style lang="scss">
@import '~@/abstracts/_variables.scss';
div#admin-actions {
div.actions-header {
font-family: "Lato", sans-serif;
border-bottom: 1px solid black;
font-weight: bold;
padding-bottom: 10px;
}
div.no-actions-available {
text-align: center;
font-family: "Lato", sans-serif;
font-size: 20px;
padding-top: 20px;
padding-bottom: 20px;
}
}
</style>
<template>
<div id="admin-actions">
<div class="grid-container">
<div class="grid-x">
<div class="large-12 medium-12 cell">
<h3 class="page-header">Actions</h3>
</div>
</div>
</div>
<div class="grid-container">
<div class="grid-x actions-header">
<div class="large-3 medium-3 cell">
公司
</div>
<div class="large-3 medium-3 cell">
咖啡店
</div>
<div class="large-3 medium-3 cell">
类型
</div>
<div class="large-3 medium-3 cell">
操作
</div>
</div>
<action v-for="action in actions"
:key="action.id"
:action="action">
</action>
<div class="large-12 medium-12 cell no-actions-available" v-show="actions.length == 0">
All outstanding actions have been processed!
</div>
</div>
</div>
</template>
<script>
import Action from '../../components/admin/actions/Action.vue';
export default {
components: {
Action
},
created() {
this.$store.dispatch('loadAdminActions');
},
computed: {
actions() {
return this.$store.getters.getActions;
}
}
}
</script>
这个组件主要用于渲染待审核动作列表,对于每个具体的待审核动作,通过创建一个新的 resources/assets/js/components/admin/actions/Action.vue
组件来渲染:
<style lang="scss">
@import '~@/abstracts/_variables.scss';
div.action {
font-family: "Lato", sans-serif;
border-bottom: 1px solid black;
padding-bottom: 15px;
padding-top: 15px;
span.approve-action {
font-weight: bold;
cursor: pointer;
display: inline-block;
margin-right: 20px;
}
span.deny-action {
color: $secondary-color;
font-weight: bold;
cursor: pointer;
display: inline-block;
}
img.more-info {
cursor: pointer;
float: right;
margin-top: 10px;
margin-right: 10px;
}
}
</style>
<template>
<div class="action">
<div class="grid-x">
<div class="large-3 medium-3 cell">
{{ action.company != null ? action.company.name : '' }}
</div>
<div class="large-3 medium-3 cell">
{{ action.cafe != null ? action.cafe.location_name : '' }}
</div>
<div class="large-3 medium-3 cell">
{{ type }}
</div>
<div class="large-3 medium-3 cell">
<span class="approve-action" v-on:click="approveAction()">通过</span>
<span class="deny-action" v-on:click="denyAction()">拒绝</span>
<span v-on:click="showDetails = !showDetails">
<img src="/storage/img/more-info-closed.svg" class="more-info" v-show="!showDetails"/>
<img src="/storage/img/more-info-open.svg" class="more-info" v-show="showDetails"/>
</span>
</div>
</div>
<div class="grid-x" v-show="showDetails">
<div class="large-12 medium-12 cell">
<action-cafe-added v-if="action.type == 'cafe-added'" :action="action"></action-cafe-added>
<action-cafe-edited v-if="action.type == 'cafe-updated'" :action="action"></action-cafe-edited>
<action-cafe-deleted v-if="action.type == 'cafe-deleted'" :action="action"></action-cafe-deleted>
</div>
</div>
</div>
</template>
<script>
import ActionCafeAdded from './ActionCafeAdded.vue';
import ActionCafeEdited from './ActionCafeEdited.vue';
import ActionCafeDeleted from './ActionCafeDeleted.vue';
import {EventBus} from '../../../event-bus.js';
export default {
props: ['action'],
components: {
ActionCafeAdded,
ActionCafeEdited,
ActionCafeDeleted
},
data() {
return {
showDetails: false
}
},
computed: {
type() {
switch (this.action.type) {
case 'cafe-added':
return '添加咖啡店';
break;
case 'cafe-updated':
return '更新咖啡店';
break;
case 'cafe-deleted':
return '删除咖啡店';
break;
}
},
actionApproveStatus() {
return this.$store.getters.getActionApproveStatus;
},
actionDeniedStatus() {
return this.$store.getters.getActionDeniedStatus;
}
},
watch: {
'actionApprovedStatus': function () {
if (this.actionApproveStatus == 2) {
EventBus.$emit('show-success', {
notification: 'Action approved successfully!'
});
}
},
'actionDeniedStatus': function () {
if (this.actionDeniedStatus == 2) {
EventBus.$emit('show-success', {
notification: 'Action denied successfully!'
});
}
}
},
methods: {
approveAction() {
this.$store.dispatch('approveAction', {
id: this.action.id
});
},
denyAction() {
this.$store.dispatch('denyAction', {
id: this.action.id
});
}
}
}
</script>
在这个组件中,主要渲染每个待审核动作的具体字段信息以及通过、拒绝按钮及对应点击触发方法,然后还有一个显示操作明细数据的组件,我们根据条件渲染来显示它们,比如新增咖啡店显示新增咖啡店数据,修改咖啡店显示修改咖啡店数据,删除咖啡店显示删除咖啡店数据,这些数据的显示我们也分割到不同的子组件中去实现了,分别是新增数据显示组件 resources/assets/js/components/admin/actions/ActionCafeAdded.vue
:
<style lang="scss">
@import '~@/abstracts/_variables.scss';
</style>
<template>
<div class="action-cafe-added action-cafe-detail">
<div class="grid-x grid-padding-x">
<div class="large-6 medium-6 cell">
<div class="grid-x">
<div class="large-12 medium-12 small-12 cell">
<label>公司名称</label>
<span class="action-content">{{ content.company_name }}</span>
</div>
</div>
<div class="grid-x">
<div class="large-12 medium-12 small-12 cell">
<label>网站</label>
<span class="action-content">{{ content.website }}</span>
</div>
</div>
<div class="grid-x">
<div class="large-12 medium-12 small-12 cell">
<label>位置名称</label>
<span class="action-content">{{ content.location_name }}</span>
</div>
</div>
<div class="grid-x">
<div class="large-12 medium-12 small-12 cell">
<label>街道地址</label>
<span class="action-content">{{ content.address }}</span>
</div>
</div>
<div class="grid-x">
<div class="large-12 medium-12 small-12 cell">
<label>城市</label>
<span class="action-content">{{ content.city }}</span>
</div>
</div>
<div class="grid-x">
<div class="large-12 medium-12 small-12 cell">
<label>省份</label>
<span class="action-content">{{ content.state }}</span>
</div>
</div>
<div class="grid-x">
<div class="large-12 medium-12 small-12 cell">
<label>邮编</label>
<span class="action-content">{{ content.zip }}</span>
</div>
</div>
<div class="grid-x">
<div class="large-12 medium-12 small-12 cell">
<label>冲泡方法</label>
<div class="brew-method option" v-for="method in actionBrewMethods">
<div class="option-container">
<img v-bind:src="method.icon" class="option-icon"/> <span class="option-name">{{ method.method }}</span>
</div>
</div>
</div>
</div>
<div class="grid-x" v-if="content.tea == 1">
<div class="large-12 medium-12 small-12 cell">
<label>茶包</label>
<div class="drink-option option">
<div class="option-container">
<img v-bind:src="'/storage/img/tea-bag.svg'" class="option-icon"/> <span
class="option-name">茶包</span>
</div>
</div>
</div>
</div>
<div class="grid-x" v-if="content.matcha == 1">
<div class="large-12 medium-12 small-12 cell">
<label>抹茶</label>
<div class="drink-option option">
<div class="option-container">
<img v-bind:src="'/storage/img/matcha-latte.svg'" class="option-icon"/> <span
class="option-name">抹茶</span>
</div>
</div>
</div>
</div>
</div>
<div class="large-12 medium-12 cell">
<span class="action-information">Cafe Added by {{ action.by.name }} on {{ action.created_at }}</span>
</div>
</div>
</div>
</template>
<script>
export default {
props: ['action'],
data() {
return {
content: ''
}
},
created() {
this.content = JSON.parse(this.action.content);
},
computed: {
brewMethods() {
return this.$store.getters.getBrewMethods;
},
actionBrewMethods() {
let actionBrewMethods = [];
let contentBrewMethods = JSON.parse(this.content.brew_methods);
for (let i = 0; i < contentBrewMethods.length; i++) {
for (let k = 0; k < this.brewMethods.length; k++) {
if (parseInt(contentBrewMethods[i]) === parseInt(this.brewMethods[k].id)) {
actionBrewMethods.push(this.brewMethods[k]);
}
}
}
return actionBrewMethods;
}
}
}
</script>
编辑数据显示组件 resources/assets/js/components/admin/actions/ActionCafeEdited.vue
:
<style lang="scss">
div.action-cafe-edited {
span.change {
color: red;
}
}
</style>
<template>
<div class="action-cafe-edited action-cafe-detail">
<div class="grid-x grid-padding-x">
<div class="large-6 medium-6 cell">
<div class="grid-x">
<div class="large-12 medium-12 small-12 cell">
<span class="action-detail-header">当前数据</span>
</div>
</div>
<div class="grid-x">
<div class="large-12 medium-12 small-12 cell">
<label>公司名称</label>
<span class="action-content">{{ action.company.name }}</span>
</div>
</div>
<div class="grid-x">
<div class="large-12 medium-12 small-12 cell">
<label>网站</label>
<span class="action-content">{{ action.company.website }}</span>
</div>
</div>
<div class="grid-x">
<div class="large-12 medium-12 small-12 cell">
<label>位置名称</label>
<span class="action-content">{{ action.cafe.location_name }}</span>
</div>
</div>
<div class="grid-x">
<div class="large-12 medium-12 small-12 cell">
<label>街道地址</label>
<span class="action-content">{{ action.cafe.address }}</span>
</div>
</div>
<div class="grid-x">
<div class="large-12 medium-12 small-12 cell">
<label>城市</label>
<span class="action-content">{{ action.cafe.city }}</span>
</div>
</div>
<div class="grid-x">
<div class="large-12 medium-12 small-12 cell">
<label>省份</label>
<span class="action-content">{{ action.cafe.state }}</span>
</div>
</div>
<div class="grid-x">
<div class="large-12 medium-12 small-12 cell">
<label>邮编</label>
<span class="action-content">{{ action.cafe.zip }}</span>
</div>
</div>
<div class="grid-x">
<div class="large-12 medium-12 small-12 cell">
<label>冲泡方法</label>
<div class="brew-method option" v-for="method in action.cafe.brew_methods">
<div class="option-container">
<img v-bind:src="method.icon" class="option-icon"/> <span class="option-name">{{ method.method }}</span>
</div>
</div>
</div>
</div>
<div class="grid-x" v-if="action.cafe.tea === 1">
<div class="large-12 medium-12 small-12 cell">
<label>茶包</label>
<div class="drink-option option">
<div class="option-container">
<img v-bind:src="'/storage/img/tea-bag.svg'" class="option-icon"/> <span
class="option-name">茶包</span>
</div>
</div>
</div>
</div>
<div class="grid-x" v-if="action.cafe.matcha === 1">
<div class="large-12 medium-12 small-12 cell">
<label>抹茶</label>
<div class="drink-option option">
<div class="option-container">
<img v-bind:src="'/storage/img/matcha-latte.svg'" class="option-icon"/> <span
class="option-name">抹茶</span>
</div>
</div>
</div>
</div>
</div>
<div class="large-6 medium-6 cell">
<div class="grid-x">
<div class="large-12 medium-12 small-12 cell">
<span class="action-detail-header">更新后数据</span>
</div>
</div>
<div class="grid-x">
<div class="large-12 medium-12 small-12 cell">
<label>公司名称</label>
<span class="action-content"
v-bind:class="{'change': content.after.company_name !== action.company.name }">{{ content.after.company_name }}</span>
</div>
</div>
<div class="grid-x">
<div class="large-12 medium-12 small-12 cell">
<label>网站</label>
<span class="action-content"
v-bind:class="{'change': content.after.website !== action.company.website }">{{ content.after.website }}</span>
</div>
</div>
<div class="grid-x">
<div class="large-12 medium-12 small-12 cell">
<label>位置名称</label>
<span class="action-content"
v-bind:class="{'change': content.after.location_name !== action.cafe.location_name }">{{ content.after.location_name }}</span>
</div>
</div>
<div class="grid-x">
<div class="large-12 medium-12 small-12 cell">
<label>街道地址</label>
<span class="action-content"
v-bind:class="{'change': content.after.address !== action.cafe.address }">{{ content.after.address }}</span>
</div>
</div>
<div class="grid-x">
<div class="large-12 medium-12 small-12 cell">
<label>城市</label>
<span class="action-content"
v-bind:class="{'change': content.after.city !== action.cafe.city }">{{ content.after.city }}</span>
</div>
</div>
<div class="grid-x">
<div class="large-12 medium-12 small-12 cell">
<label>省份</label>
<span class="action-content"
v-bind:class="{'change': content.after.state !== action.cafe.state }">{{ content.after.state }}</span>
</div>
</div>
<div class="grid-x">
<div class="large-12 medium-12 small-12 cell">
<label>邮编</label>
<span class="action-content" v-bind:class="{'change': content.after.zip !== action.cafe.zip }">{{ content.after.zip }}</span>
</div>
</div>
<div class="grid-x">
<div class="large-12 medium-12 small-12 cell">
<label>冲泡方法</label>
<div class="brew-method option" v-for="method in actionBrewMethods">
<div class="option-container">
<img v-bind:src="method.icon" class="option-icon"/> <span class="option-name">{{ method.method }}</span>
</div>
</div>
</div>
</div>
<div class="grid-x" v-if="content.after.tea === 1">
<div class="large-12 medium-12 small-12 cell">
<label>茶包</label>
<div class="drink-option option">
<div class="option-container">
<img v-bind:src="'/storage/img/icons/tea-bag.svg'" class="option-icon"/> <span
class="option-name">茶包</span>
</div>
</div>
</div>
</div>
<div class="grid-x" v-if="content.after.matcha === 1">
<div class="large-12 medium-12 small-12 cell">
<label>抹茶</label>
<div class="drink-option option">
<div class="option-container">
<img v-bind:src="'/storage/img/icons/matcha-latte.svg'" class="option-icon"/> <span
class="option-name">抹茶</span>
</div>
</div>
</div>
</div>
</div>
<div class="large-12 medium-12 cell">
<span class="action-information">Cafe Updated by {{ action.by.name }} on {{ action.created_at }}</span>
</div>
</div>
</div>
</template>
<script>
export default {
props: ['action'],
data() {
return {
content: ''
}
},
created() {
this.content = JSON.parse(this.action.content);
},
computed: {
brewMethods() {
return this.$store.getters.getBrewMethods;
},
actionBrewMethods() {
let actionBrewMethods = [];
let contentBrewMethods = JSON.parse(this.content.after.brew_methods);
for (let i = 0; i < contentBrewMethods.length; i++) {
for (let k = 0; k < this.brewMethods.length; k++) {
if (parseInt(contentBrewMethods[i]) === parseInt(this.brewMethods[k].id)) {
actionBrewMethods.push(this.brewMethods[k]);
}
}
}
return actionBrewMethods;
}
}
}
</script>
和删除数据显示组件 resources/assets/js/components/admin/actions/ActionCafeDeleted.vue
:
<style>
</style>
<template>
<div class="action-cafe-deleted action-cafe-detail">
<div class="grid-x grid-padding-x">
<div class="large-6 medium-6 cell">
<div class="grid-x">
<div class="large-12 medium-12 small-12 cell">
<label>公司名称</label>
<span class="action-content">{{ action.cafe.company_name }}</span>
</div>
</div>
<div class="grid-x">
<div class="large-12 medium-12 small-12 cell">
<label>网站</label>
<span class="action-content">{{ action.cafe.website }}</span>
</div>
</div>
<div class="grid-x">
<div class="large-12 medium-12 small-12 cell">
<label>位置名称</label>
<span class="action-content">{{ action.cafe.location_name }}</span>
</div>
</div>
<div class="grid-x">
<div class="large-12 medium-12 small-12 cell">
<label>街道地址</label>
<span class="action-content">{{ action.cafe.address }}</span>
</div>
</div>
<div class="grid-x">
<div class="large-12 medium-12 small-12 cell">
<label>城市</label>
<span class="action-content">{{ action.cafe.city }}</span>
</div>
</div>
<div class="grid-x">
<div class="large-12 medium-12 small-12 cell">
<label>省份</label>
<span class="action-content">{{ action.cafe.state }}</span>
</div>
</div>
<div class="grid-x">
<div class="large-12 medium-12 small-12 cell">
<label>邮编</label>
<span class="action-content">{{ action.cafe.zip }}</span>
</div>
</div>
<div class="grid-x">
<div class="large-12 medium-12 small-12 cell">
<label>冲泡方法</label>
<div class="brew-method option" v-for="method in action.cafe.brew_methods">
<div class="option-container">
<img v-bind:src="method.icon" class="option-icon"/> <span class="option-name">{{ method.method }}</span>
</div>
</div>
</div>
</div>
<div class="grid-x" v-if="action.cafe.tea === 1">
<div class="large-12 medium-12 small-12 cell">
<label>茶包</label>
<div class="drink-option option">
<div class="option-container">
<img v-bind:src="'/storage/img/tea-bag.svg'" class="option-icon"/> <span
class="option-name">茶包</span>
</div>
</div>
</div>
</div>
<div class="grid-x" v-if="action.cafe.matcha === 1">
<div class="large-12 medium-12 small-12 cell">
<label>抹茶</label>
<div class="drink-option option">
<div class="option-container">
<img v-bind:src="'/storage/img/matcha-latte.svg'" class="option-icon"/> <span
class="option-name">抹茶</span>
</div>
</div>
</div>
</div>
</div>
<div class="large-12 medium-12 cell">
<span class="action-information">Cafe Deleted by {{ action.by.name }} on {{ action.created_at }}</span>
</div>
</div>
</div>
</template>
<script>
export default {
props: ['action'],
data() {
return {
content: ''
}
},
created() {
this.content = JSON.parse(this.action.content);
}
}
</script>
创建完这些组件后,在 Action.vue
中引入它们。至此,我们的动作审核子页面组件全部代码就编写完成了,接下来,只需要将对应的 Vuex 模块引入就大功告成了。
第四步:动态引入动作审核 Vuex 模块
你可能已经注意到我们在创建完 resources/assets/js/modules/admin/actions.js
模块后,并没有像在之前用户前台代码里所做的那样立刻在 resources/assets/js/store.js
引入它们,这是因为只有管理员才能登录到管理后台,对于游客、普通用户和商家用户而言,根本无需加载这些资源,我们甚至可以在用户需要这些 Vuex 模块的时候加载它们,不需要的时候卸载它们(比如退出登录),这也是 Vuex 模块的强大之处,接下来,我们将通过动态加载的方式来引入 Actions
Vuex 模块。
首先,回到后台布局文件 resources/assets/js/layouts/Admin.vue
,在 created
钩子中编写代码如下:
// 引入后台 Vuex 模块
import {actions} from '../modules/admin/actions.js';
created() {
this.$store.dispatch('loadBrewMethods');
if (!this.$store._modules.get(['admin'])) {
this.$store.registerModule('admin', {});
}
if (!this.$store._modules.get(['admin', 'actions'])) {
this.$store.registerModule(['admin', 'actions'], actions);
}
},
在这段代码中,我们首先查看 admin
模块是否已经注册,如果没有注册话则注册它,它是其他后台模块的父模块,然后我们检查在 admin
父模块中是否存在 actions
子模块,如果没有的话,则将其注册到 admin
模块中,这也就是我们的动作审核 Vuex 模块,这个是按照模块路径来依次注册的,registerModule
方法用于动态注册 Vuex 模块,该方法第一个参数是一个字符串或数组,第二个参数是要引入的 Vuex 模块。这段代码整体的意思就是用户进入管理后台后才会引入动作审核 Vuex 模块(admin.actions
)。
接下来,打开前台布局文件 resources/assets/js/layouts/Layout.vue
,我们要在这个组件中卸载所有 admin.*
Vuex 模块,这样,当管理员退出的时候,就可以从系统中卸载 admin
相关模块,还是在 created
钩子中新增一段卸载 Vuex 模块代码如下:
created() {
this.$store.dispatch('loadCafes');
this.$store.dispatch('loadUser');
this.$store.dispatch('loadBrewMethods');
this.$store.dispatch('loadCities');
if (this.$store._modules.get(['admin'])) {
this.$store.unregisterModule('admin', {});
}
},
我们会先判断 $store
模块中是否存在 admin
父模块,如果存在,则通过 unregisterModule
方法将其卸载,这样也会连带将 admin
模块下的所有子模块一起卸载。
至此,我们的管理后台动作审核列表前端 UI 功能开发工作算是全部完成了,在演示功能之前,还要做少许优化,打开滑出菜单组件 resources/assets/js/components/global/PopOut.vue
,加入管理后台入口(只有管理员可见):
<div class="side-bar-link" v-if="user != '' && userLoadStatus === 2 && user.permission >= 1">
<router-link :to="{ name: 'admin'}" v-on:click.native="hideNav()">
管理后台
</router-link>
</div>
然后为新增的管理后台头部组件添加一些样式代码,在 resources/assets/sass/components/admin
目录下创建 _page-header.scss
,编写 Sass 代码如下:
h3.page-header {
font-weight: bold;
font-family: "Lato", sans-serif;
margin-bottom: 30px;
font-size: 20px;
a {
color: black;
}
}
最后在 resources/assets/sass/app.scss
中引入上述 Sass 文件:
@import "components/admin/page-header";
接下来,就可以演示管理后台动作审核列表功能了,运行 npm run dev
重新编译前端资源,打开 Roast 应用首页并登录,此时由于用户还是普通用户,没有管理员权限,所以点击右上角滑出菜单是看不到「管理后台」的入口链接的,我们去数据库修改下 users
表对应用户的 permission
字段值为 2
,然后刷新页面就可以看到这个入口了:
接下来我们进入管理后台,点击「审核列表」进入动作审核页面 http://roast.test/#/admin/actions
,如果没有待审核记录,页面显示如下:
如果有待审核记录,就会显示出动作审核列表:
点击每条记录右侧的下拉箭头就可以看到对应的数据明细:
点击通过/拒绝,则会执行通过或拒绝审核动作,操作成功会弹出提示:
No Comments