基于 Laravel + Vue 构建 API 驱动的前后端分离应用系列(三十九) —— 管理后台前端动作审核列表页面功能实现

上一篇教程中,我们完成了管理后台前端路由定义,并通过 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,如果没有待审核记录,页面显示如下:

如果有待审核记录,就会显示出动作审核列表:

点击每条记录右侧的下拉箭头就可以看到对应的数据明细:

点击通过/拒绝,则会执行通过或拒绝审核动作,操作成功会弹出提示:

学院君 has written 1269 articles

Laravel学院院长,终身学习者

积分:177167 等级:P12 职业:手艺人 城市:杭州

0 条回复

登录后才能进行评论,立即登录?