【Vue】Ego商城项目跟做

技术栈

Vue全家桶:Vue + VueRouter + Vuex + Axios +ElementUI

依赖安装

网络请求:npm install --save axios --no-fund

Element:vue add element

后端相关依赖:npm install --save express cors mysql --no-fund

token:npm install --save jsonwebtoken --no-fund

对象转换:npm install querystring --no-fund

文件上传:npm install --save multer --no-fund

富文本编辑器:npm install wangeditor --save --no-fund

快捷运行方案

npm install -g concurrently --no-fund

npm install -g nodemon --no-fund

项目框架搭建

1.使用vue create xxx命令创建项目

2.使用VS Code打开项目

端口号修改:修改vue.config.js文件,增加如下代码

添加elementUI

1.终端输入vue add element添加element

添加完毕后,项目新增element文件

2.运行项目,查看项目是否可正常运行

注:因为版本问题,可能运行时会报错,以及element组件无法根据使用自动追加对应的库,所以这里element.js文件我改了,如上图所示

数据库准备

sql 复制代码
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for category
-- ----------------------------
DROP TABLE IF EXISTS `category`;
CREATE TABLE `category`  (
  `id` int(11) NOT NULL,
  `cid` int(11) NULL DEFAULT NULL,
  `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `pid` int(11) NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of category
-- ----------------------------
INSERT INTO `category` VALUES (1, 1001, '家用电器', 1);
INSERT INTO `category` VALUES (2, 1002, '手机/运营商/数码', 1);
INSERT INTO `category` VALUES (3, 1003, '电脑/办公', 1);
INSERT INTO `category` VALUES (4, 1004, '家具/家居', 1);
INSERT INTO `category` VALUES (5, 10001, '电视', 1001);
INSERT INTO `category` VALUES (6, 10001, '手机通讯', 1002);
INSERT INTO `category` VALUES (7, 10001, '电脑整机', 1003);
INSERT INTO `category` VALUES (8, 10001, '厨具', 1004);
INSERT INTO `category` VALUES (9, 10002, '空调', 1001);
INSERT INTO `category` VALUES (10, 10002, '运营商', 1002);
INSERT INTO `category` VALUES (11, 10002, '电脑配件', 1003);
INSERT INTO `category` VALUES (12, 10002, '家纺', 1004);
INSERT INTO `category` VALUES (13, 10003, '洗衣机', 1001);
INSERT INTO `category` VALUES (14, 10003, '摄影', 1002);
INSERT INTO `category` VALUES (15, 10003, '外设产品', 1003);
INSERT INTO `category` VALUES (16, 10003, '灯具', 1004);
INSERT INTO `category` VALUES (17, 10004, '冰箱', 1001);
INSERT INTO `category` VALUES (18, 10004, '摄像', 1002);
INSERT INTO `category` VALUES (19, 10004, '游戏设备', 1003);
INSERT INTO `category` VALUES (20, 10004, '家具', 1004);
INSERT INTO `category` VALUES (21, 100001, '超薄电视', 10001);
INSERT INTO `category` VALUES (22, 100002, '全面屏电视', 10001);

-- ----------------------------
-- Table structure for project
-- ----------------------------
DROP TABLE IF EXISTS `project`;
CREATE TABLE `project`  (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `title` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '标题',
  `image` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '图片路径',
  `sellPoint` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `price` int(10) NULL DEFAULT NULL COMMENT '价格',
  `cid` int(11) NULL DEFAULT NULL,
  `num` int(11) NULL DEFAULT NULL,
  `barcode` tinyint(4) NULL DEFAULT NULL,
  `status` tinyint(4) NULL DEFAULT NULL,
  `created` datetime(0) NULL DEFAULT NULL,
  `updated` datetime(0) NULL DEFAULT NULL,
  `descs` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 31 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of project
-- ----------------------------
INSERT INTO `project` VALUES (1, '隐形的守护', NULL, '互动游戏', 128, 1, 4, NULL, 1, NULL, NULL, '这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!');
INSERT INTO `project` VALUES (2, '饥饿游戏', NULL, '影视作品', 58, 1, 5, NULL, 1, NULL, NULL, NULL);
INSERT INTO `project` VALUES (3, '家族4', NULL, 'slg游戏', 18, NULL, 123, NULL, 1, NULL, NULL, '这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!');
INSERT INTO `project` VALUES (4, '家族1', NULL, 'slg游戏', 12, NULL, 123, NULL, 1, NULL, NULL, '这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!');
INSERT INTO `project` VALUES (5, '家族2', NULL, 'slg游戏', 14, NULL, 22, NULL, 1, NULL, NULL, '这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!');
INSERT INTO `project` VALUES (6, '家族3', NULL, 'slg游戏', 18, NULL, 42, NULL, 1, NULL, NULL, '这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!');
INSERT INTO `project` VALUES (7, '三国群英传1', NULL, 'slg游戏', 12, NULL, 421, NULL, 1, NULL, NULL, '光荣只会卖情怀');
INSERT INTO `project` VALUES (9, '三国群英传3', NULL, 'slg游戏', 12, NULL, 421, NULL, 1, NULL, NULL, '光荣只会卖情怀');
INSERT INTO `project` VALUES (10, '三国群英传4', NULL, 'slg游戏', 12, NULL, 421, NULL, 1, NULL, NULL, '光荣只会卖情怀');
INSERT INTO `project` VALUES (11, '三国群英传5', NULL, 'slg游戏', 12, NULL, 421, NULL, 1, NULL, NULL, '光荣只会卖情怀');
INSERT INTO `project` VALUES (12, '三国群英传6', NULL, 'slg游戏', 12, NULL, 421, NULL, 1, NULL, NULL, '光荣只会卖情怀');
INSERT INTO `project` VALUES (13, '三国群英传7', NULL, 'slg游戏', 12, NULL, 421, NULL, 1, NULL, NULL, '光荣只会卖情怀');
INSERT INTO `project` VALUES (14, '三国群英传8', NULL, 'slg游戏', 12, NULL, 421, NULL, 1, NULL, NULL, '毫无亮点,走网游路线了');
INSERT INTO `project` VALUES (15, '三国志1', NULL, 'slg游戏', 32, NULL, 32, NULL, 1, NULL, NULL, '光荣只会卖情怀');
INSERT INTO `project` VALUES (16, '三国志2', NULL, 'slg游戏', 32, NULL, 32, NULL, 1, NULL, NULL, '光荣只会卖情怀');
INSERT INTO `project` VALUES (17, '三国志3', NULL, 'slg游戏', 32, NULL, 32, NULL, 1, NULL, NULL, '光荣只会卖情怀');
INSERT INTO `project` VALUES (18, '三国志4', NULL, 'slg游戏', 32, NULL, 32, NULL, 1, NULL, NULL, '光荣只会卖情怀');
INSERT INTO `project` VALUES (19, '三国志5', NULL, 'slg游戏', 32, NULL, 32, NULL, 1, NULL, NULL, '光荣只会卖情怀');
INSERT INTO `project` VALUES (20, '三国志6', NULL, 'slg游戏', 32, NULL, 32, NULL, 1, NULL, NULL, '光荣只会卖情怀');
INSERT INTO `project` VALUES (21, '三国志7', NULL, 'slg游戏', 32, NULL, 32, NULL, 1, NULL, NULL, '光荣只会卖情怀');
INSERT INTO `project` VALUES (22, '三国志8', NULL, 'slg游戏', 32, NULL, 32, NULL, 1, NULL, NULL, '光荣只会卖情怀');
INSERT INTO `project` VALUES (23, '三国志9', NULL, 'slg游戏', 32, NULL, 32, NULL, 1, NULL, NULL, '光荣只会卖情怀');
INSERT INTO `project` VALUES (24, '三国志10', NULL, 'slg游戏', 32, NULL, 32, NULL, 1, NULL, NULL, '光荣只会卖情怀');
INSERT INTO `project` VALUES (25, '三国志11', NULL, 'slg游戏', 32, NULL, 32, NULL, 1, NULL, NULL, '光荣只会卖情怀');
INSERT INTO `project` VALUES (26, '三国志12', NULL, 'slg游戏', 32, NULL, 32, NULL, 1, NULL, NULL, '光荣只会卖情怀');
INSERT INTO `project` VALUES (27, '三国志13', NULL, 'slg游戏', 32, NULL, 32, NULL, 1, NULL, NULL, '光荣只会卖情怀');
INSERT INTO `project` VALUES (28, '三国无双1', 'http://localhost:3000\\1732370097961-1.jpg', '任务强度高', 58, 10002, 500, NULL, 1, NULL, NULL, '<p>方式的监护人突然<strike>让我如很高的发挥</strike><font color=\"#f9963b\">地方规划人都热</font>帖该问题公司的归属感和无人</p>');
INSERT INTO `project` VALUES (30, '三国无双4', 'http://localhost:3000\\1732702595899-1.jpg', '情怀', 58, 10003, 12, NULL, 1, NULL, NULL, '<p>是的话<font color=\"#c24f4a\">标点符号的分</font>工</p>');

-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user`  (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `password` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `email` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 11 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES (7, 'cy', '123456', '123456');
INSERT INTO `user` VALUES (8, 'admin', '123456', 'admin@123.com');
INSERT INTO `user` VALUES (9, 'admin1', '123456', 'admin@123.com');
INSERT INTO `user` VALUES (10, 'admin2', '123456', 'admin@123.com');

SET FOREIGN_KEY_CHECKS = 1;

项目开发

1.准备页签栏

src/views文件夹内创建main/ADCategory.vuemain/ParamsView.vuemain/ProductView.vueLayoutView.vue文件

修改router/index.js文件,增加对应路由跳转配置

javascript 复制代码
import Vue from 'vue'
import VueRouter from 'vue-router'
import HomeView from '../views/HomeView.vue'
import Layout from '../views/LayoutView.vue'

Vue.use(VueRouter)

const routes = [
  {
    path: '/',
    name: 'Layout',
    component: Layout,
    children:[
      {
        path: '',
        name: 'home',
        component: HomeView
      },
      {
        path:"product",
        name:"Product",
        component:() => import("../views/main/ProductView.vue")
      },
      {
        path:"params",
        name:"Params",
        component:() => import("../views/main/ParamsView.vue")
      },
      {
        path:"ad",
        name:"ADCategory",
        component:() => import("../views/main/ADCategory.vue")
      }
    ]
  },
]
const router = new VueRouter({
  routes,
  // 去除路径上的#号
  mode:"history"
})
export default router

2.登录拦截

router文件夹下新建permission.js文件,编写拦截代码

javascript 复制代码
import router from "./index"
import store from "../store"

// 访问路径前判断权限
router.beforeEach((to,from,next) =>{
    if(to.meta.isLogin){
        // 暂未实现token
        let token = false;
        console.log(token);
        if(token){
            next();
        }else{
            next({
                name:"Login"
            })
        }
    }else{
        next();
    }
})

修改router/index.js文件,给需要登录才能访问的路由设置isLogin参数

javascript 复制代码
import Vue from 'vue'
import VueRouter from 'vue-router'
import HomeView from '../views/HomeView.vue'
import Layout from '../views/LayoutView.vue'

Vue.use(VueRouter)

const routes = [
  {
    path: '/',
    name: 'Layout',
    component: Layout,
    children:[
      {
        path: '',
        name: 'home',
        component: HomeView,
        meta:{
          isLogin:true
        }
      },
      {
        path:"product",
        name:"Product",
        component:() => import("../views/main/ProductView.vue"),
        meta:{
          isLogin:true
        }
      },
      {
        path:"params",
        name:"Params",
        component:() => import("../views/main/ParamsView.vue"),
        meta:{
          isLogin:true
        }
      },
      {
        path:"ad",
        name:"ADCategory",
        component:() => import("../views/main/ADCategory.vue"),
        meta:{
          isLogin:true
        }
      }
    ]
  },
  {
    path:'/login',
    name:'Login',
    component:() => import("../views/LoginView.vue")
  }
]
const router = new VueRouter({
  routes,
  // 去除路径上的#号
  mode:"history"
})
export default router

修改main.js文件,导入permission.js

javascript 复制代码
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import './plugins/element.js'
// 导入permission.js文件
import './router/permission'

Vue.config.productionTip = false

new Vue({
  router,
  store,
  render: h => h(App)
}).$mount('#app')

编写LoginView.vue文件

vue 复制代码
<template>
  <div>
    登录注册
  </div>
</template>

<script>
export default {
}
</script>

<style>
</style>

运行效果

3.编写登录注册页

编写LoginView.vue文件

vue 复制代码
<template>
    <div class="login">
        <el-card class="box-card">
            <div slot="header" class="clearfix">
                <span>Ego商城后台管理系统</span>
            </div>
            <div>
                <el-tabs v-model="currentIndex" stretch type="border-card" @tab-click="handleTabClick">
                    <el-tab-pane label="登录" name="login">
                        <el-form :model="loginForm" status-icon ref="loginForm" :rules="rules">
                            <el-form-item label="用户名:" label-width="80px" prop="username">
                                <el-input type="text" v-model="loginForm.username" />
                            </el-form-item>
                            <el-form-item label="密码:" label-width="80px" prop="password">
                                <el-input type="password" v-model="loginForm.password" />
                            </el-form-item>
                            <el-form-item >
                                <el-button type="primary" @click="submitForm('loginForm')">登录</el-button>
                            </el-form-item>
                        </el-form>
                    </el-tab-pane>
                       <el-tab-pane label="注册" name="register">
                            <el-form :model="registerForm" status-icon ref="registerForm" :rules="rules">
                                <el-form-item label="用户名:" label-width="80px" prop="username">
                                    <el-input type="text" v-model="registerForm.username" />
                                </el-form-item>
                                <el-form-item label="邮箱:" label-width="80px" prop="email">
                                    <el-input type="text" v-model="registerForm.email" />
                                </el-form-item>
                                <el-form-item label="密码:" label-width="80px" prop="password">
                                    <el-input type="password" v-model="registerForm.password" />
                                </el-form-item>
                                <el-form-item label="确认密码:" label-width="80px" prop="configurePassword">
                                    <el-input type="password" v-model="registerForm.configurePassword" />
                                </el-form-item>
                                <el-form-item >
                                    <el-button type="primary" @click="submitForm('registerForm')">注册</el-button>
                                </el-form-item>
                            </el-form>
                        </el-tab-pane>
                </el-tabs>
            </div>
        </el-card>
    </div>
  
</template>

<script>
export default {
    data(){
        // 验证规则
        var validateUsername = (rule,value,callback) =>{
            if(value === ""){
                callback(new Error("请输入用户名"));
            }else if(value.length < 6){
                callback(new Error("长度不足6位"));
            }else{
                callback();
            }
        }
        var validatePassword = (rule,value,callback) =>{
            if(value === ""){
                callback(new Error("请输入密码"));
            }else{
                callback();
            }
        }
        var validateConfigurePassword = (rule,value,callback) =>{
            if(value === ""){
                callback(new Error("请输入密码"));
            }else if(value !== this.registerForm.password){
                callback(new Error("两次密码不一致"));
            }
            else{
                callback();
            }
        }
        return{
            currentIndex:"login",
            loginForm:{
                username:"",
                password:""
            },
            registerForm:{
                username:"",
                password:"",
                email:"",
                configurePassword:""
            },
            activeTab:"login",
            rules:{
                username:[
                    {
                        validator:validateUsername,
                        trigger:'blur'
                    }
                ],
                password:[
                    {
                        validator:validatePassword,
                        trigger:'blur'
                    }
                ],
                configurePassword:[
                    {
                        validator:validateConfigurePassword,
                        trigger:'blur'
                    }
                ]
            }
        }
    },
    methods:{
        submitForm( formName ){
            this.$refs[formName].validate((valid) =>{
                if(valid){
                    // 根据当前所在tabs页签的name的值去执行对应页签的逻辑
                    if(this.activeTab === "login"){
                        // 提交注册表单逻辑
                        console.log(this.loginForm);
                    }else if(this.activeTab === "register"){
                        // 提交注册表单逻辑
                        console.log(this.registerForm);
                    }else{
                        return;
                    }
                }
            })
        },
        // 获取当前所在tabs页签的name
        handleTabClick(tab){
            this.activeTab = tab.name;
        }
    }
}
</script>

<style scoped lang="less">
.login{
    width:1200px;
    margin: 0 auto;
    .box-card{
        width: 500px;
        margin: 100px auto;
    }
}
</style>

运行效果

4.nodeJS后台实现

在项目目录下新建server目录(与src目录同级),并新建index.jsrouter.jsconfig.js文件

编写index.js,实现服务器配置

javascript 复制代码
const express = require("express");
const app = express();
const cors = require("cors");
const bodyParser = require("body-parser");
const router = require("./router")

app.use(cors());
app.use(bodyParser.urlencoded({
    extended:true
}))
app.use("/api",router);
// 监听端口号
app.listen(3000,() =>{
    console.log(3000)
})

编写config.js,配置数据库链接配置

javascript 复制代码
const mysql = require("mysql");
const client = mysql.createConnection({
    host:"localhost",
    user:"root",
    password:"123456",
    database:"ego_shop_one"
})
const sqlClient = (sql,arr,callback) =>{
    client.query(sql,arr,(error,result)=>{
        if(error){
            console.log(error);
            return;
        }
        callback(result);
    })
}

module.exports = sqlClient;

编写router.js,编写服务器数据操作逻辑

javascript 复制代码
const express = require("express");
const router = express.Router();
const sqlClient = require("./config")
const jwt = require("jsonwebtoken")

/**
 * 注册
 */
router.post("/register",(req,res) =>{
    const {username,password,email} = req.body;
    const sql = "INSERT INTO user VALUES(null,?,?,?)";
    const arr = [username,password,email];
    sqlClient(sql,arr,result=>{
        if(result.affectedRows > 0){
            res.send({
                status:200,
                msg:"注册成功"
            })
        }else{
            res.send({
                status:401,
                msg:"注册失败"
            })
        }
    })
})

/**
 * 登录
 */
router.post("/login",(req,res) =>{
    const {username,password} = req.body;
    const sql = "SELECT * FROM user WHERE username = ? and password = ?";
    const arr = [username,password];
    sqlClient(sql,arr,result =>{
        if(result.length > 0){
            let token = jwt.sign({
                username,
                id:result[0].id
            },'soomekeys')
            res.send({
                status:200,
                token,
                username
            })
        }else{
            res.send({
                status:401,
                msg:"登录失败"
            })
        }
    })
})
module.exports = router;

修改package.json文件,增加快捷运行脚本"dev": "concurrently \"npm run serve\" \"nodemon server/index.js\""

json 复制代码
{
  "name": "my_shopyigou",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build",
    "lint": "vue-cli-service lint",
    "dev": "concurrently \"npm run serve\" \"nodemon server/index.js\"" 
  },
    ......
}

运行npm run dev,可同步运行服务器和客户端;使用postman测试后端接口

5.前后端登录注册对接

在src目录下新建api目录,新建api/base.jsapi/index.js文件

编写api/index.js,实现请求跳转

javascript 复制代码
import axios from "../utils/request"
import base from "./base"

const api = {
    /**
     * 注册
     */
    register(params){
        return axios.post(base.baseUrl + base.register,params)
    },
    /**
     * 登录
     */
    login(params){
        return axios.post(base.baseUrl + base.login,params)
    }
}
export default api;

编写api/base.js

javascript 复制代码
const base = {
    baseUrl:"http://localhost:3000",
    register:"/api/register",
    login:"/api/login"
}
export default base;

修改router/permission.js

javascript 复制代码
import router from "./index"
import store from "../store"

// 访问路径前判断权限
router.beforeEach((to,from,next) =>{
    if(to.meta.isLogin){
        // 获取token
        let token = store.state.login.user.token;
        console.log(token);
        if(token){
            next();
        }else{
            next({
                name:"Login"
            })
        }
    }else{
        next();
    }
})

新建store/modules/login.js文件

javascript 复制代码
export default{
    namespaced:true,
    state:{
        user:{
            username:"",
            token:""
        }
    },mutations:{
        setUser(state,user){
            state.user = user;
        }
    }
}

修改store/index.js文件

javascript 复制代码
import Vue from 'vue'
import Vuex from 'vuex'
import login from './modules/login'

Vue.use(Vuex)

export default new Vuex.Store({
  modules: {
    login
  }
})

在src目录下新建utils目录,新建utils/request.jsutils/init.js文件

编写utils/request.js文件

javascript 复制代码
import axios from 'axios'
import qs from "querystring"
import store from "../store"

/**
 * 处理失败的方法
 */
const errorHandle = (status,info) =>{
    switch(status){
        case 400:
            console.log("语义有误,当前请求无法被服务器理解。");
            break;
        case 401:
            // token:令牌
            console.log("服务器认证失败!");
            break;
        case 403:
            console.log("服务器已经理解请求,但是拒绝执行它!");
            break;
        case 404:
            console.log("请检查网络请求地址!");
            break;      
        case 500:
            console.log("服务器遇到了一个未曾预料的状态,导致它无法完成对请求的处理");
            break;  
        case 502:
            console.log("作为网关或者代理工作的服务器尝试执行请求时,从上游服务器接收到的请求异常");
            break;
        default:
            console.log(info);
            break;
    }
}

/**
 * 创建axios实例对象
 */
const instance = axios.create({
    // 公共配置
    timeout:5000
})

/**
 * 请求拦截
 */
instance.interceptors.request.use(
    (config) => {
        if(config.method === "post"){
            config.data = qs.stringify(config.data)
        }
        return config
    },
    (error) => {
        return Promise.reject(error);
    } 
);
/**
 * 响应拦截
 */
instance.interceptors.response.use(
    //完成了
    (response) => {
        return response.status === 200 ? Promise.resolve(response) : Promise.reject(response);
    },
    (error) => {
        const {response} = error;
        errorHandle(response.status,response.info);
    }
);

export default instance

编写utils/init.js文件

javascript 复制代码
import store from "../store"
if(localStorage.getItem("ego")){
    store.commit("login/setUser",JSON.parse(localStorage.getItem("ego")))
}

修改main.js文件,导入使用./utils/init

javascript 复制代码
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import './plugins/element.js'
import './router/permission'
import './utils/init'

Vue.config.productionTip = false

new Vue({
  router,
  store,
  render: h => h(App)
}).$mount('#app')

修改LoginView.vue代码,添加实际登录注册请求逻辑

vue 复制代码
<script>
import { mapMutations } from "vuex";
import api from "../api"
export default {
    ......
    methods:{
        ...mapMutations("login",["setUser"]),
        submitForm( formName ){
            this.$refs[formName].validate((valid) =>{
                if(valid){
                    if(this.activeTab === "login"){
                        // 提交登录表单逻辑
                        api.login(this.loginForm).then(res =>{
                            // console.log(res.data);
                            if(res.data.status === 200){
                                this.setUser(res.data);
                                localStorage.setItem("ego",JSON.stringify(res.data));
                                this.$router.push('/')
                            }else{
                                const h = this.$createElement;
                                this.$notify({
                                    title:"登录失败",
                                    message:h("i","用户名密码错误")
                                })
                            }
                            
                        })
                    }else if(this.activeTab === "register"){
                        // 提交注册表单逻辑
                        // console.log(this.registerForm);
                        api.register(this.registerForm).then(res =>{
                            if(res.data.status === 200){
                                const h = this.$createElement;
                                this.$notify({
                                    title:"注册成功",
                                    message:h("i","请前往登录页面登录")
                                });
                            }else{
                                const h = this.$createElement;
                                this.$notify({
                                    title:"注册失败",
                                    message:h("i","请重新注册")
                                })
                            }
                        })
                    }else{
                        return;
                    }
                }
            })
        },
        handleTabClick(tab){
            this.activeTab = tab.name;
        }
    }
}
</script>

运行效果

登录后,会自动跳转至首页且本地缓存中增加token

注册

暂未实现重复用户验证

6.导航栏实现

components目录下新建HeaderNav.vue,编写导航栏组件

vue 复制代码
<template>
  <el-menu :default-active="active" mode="horizontal"
     background-color="#545c64"
     text-color="#fff"
     active-text-color="#ffd04b"
     router >
    <el-menu-item index="/">首页</el-menu-item>
    <el-menu-item index="/product">商品管理</el-menu-item>
    <el-menu-item index="/params">规格参数</el-menu-item>
    <el-menu-item index="/ad">广告管理</el-menu-item>
    <el-menu-item class="user">
        <span class="user-name">{{ user.username }}</span>
        <el-button @click="logoutHandle">退出</el-button>
    </el-menu-item>
  </el-menu>
</template>

<script>
import { mapState,mapMutations } from 'vuex'
export default {
    data(){
        return {
            active:"/"
        }
    },
    computed:{
        ...mapState("login",["user"])
    },
    methods:{
        ...mapMutations("login",["setUser"]),
        logoutHandle(){
            this.setUser({})
            localStorage.removeItem('ego')
            this.$router.push('/login')
        }
    }
}
</script>

<style lang="less" scoped>
.user{
    float: right !important;
    margin-right: 20px !important;
    line-height: 60px !important;
    .user-name{
        color:#fff;
        margin-right: 10px;
        font-size: 15px;
        border: 1px solid #fff;
        border-radius: 50%;
        width:40px;
        height: 40px;
        display: inline-block;
        line-height: 40px;
        overflow: hidden;
    }
}
.set-lang{
    float: right !important;
}
.el-dropdown-link{
    color:#fff;
}

</style>

修改LayoutView.vue文件,引入使用导航栏

vue 复制代码
<template>
    <div>
        <header-nav></header-nav>
        <router-view></router-view>
    </div>
</template>

<script>
import HeaderNav from "@/components/HeaderNav.vue"
export default {
    components:{
        HeaderNav
    }
}
</script>

<style>
</style>

运行效果

*.公共样式导入

在src/assets目录下新建css目录,编写common.css文件

css 复制代码
body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,form,fieldset,label
{
    margin: 0;
    padding: 0;
}
body{text-align: center;background: #f1f1f1;}
li{list-style: none;}
a{text-decoration: none;}
input,button,img{border: none;}
.active{color: #409eff;}

修改main.js文件,导入使用公共样式

javascript 复制代码
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import './plugins/element.js'
import './router/permission'
import './utils/init'
import './assets/css/common.css'
Vue.config.productionTip = false

new Vue({
  router,
  store,
  render: h => h(App)
}).$mount('#app')

7.首页页面实现

在src/view/main目录下,新建HomePage目录,用于存放所有首页视图页面,将HomeView.vue移动至该目录下

修改src/router目录下的index.js文件,更新HomeView的路径

javascript 复制代码
import Vue from 'vue'
import VueRouter from 'vue-router'
import HomeView from '../views/main/HomePage/HomeView.vue'
import Layout from '../views/LayoutView.vue'
......

7.1.左导航栏

在HomePage目录下新建HomeNavView.vue文件,编写左导航栏组件

vue 复制代码
<template>
  <div class="index-warp">
    <div class="index-left">
        <div class="index-left-block">
            <h2>全部产品</h2>
            <div v-for="(product,index) in productList" :key="index">
                <h3>{{ product.title }}</h3>
                <ul>
                    <li v-for="(item,index) in product.list" :key="index">
                        <a :href="item.url">{{ item.name }}</a>
                        <span class="hot-tag" v-if="item.hot">HOT</span>
                    </li>
                </ul>
            </div>
        </div>
    </div>
  </div>
</template>

<script>
export default {
    data(){
        return{
            productList:[
                {
                    title:"手机应用类",
                    list:[
                        {
                            id:1,
                            name:"baidu",
                            url:"www.baidu.com",
                            hot:true
                        },
                        {
                            id:2,
                            name:"baidu2",
                            url:"www.baidu.com",
                            hot:false
                        },
                        {
                            id:3,
                            name:"baidu3",
                            url:"www.baidu.com",
                            hot:false
                        }
                    ]
                },
                {
                    title:"PC产品类",
                    list:[
                        {
                            id:1,
                            name:"华为",
                            url:"www.baidu.com",
                            hot:true
                        },
                        {
                            id:2,
                            name:"mac",
                            url:"www.baidu.com",
                            hot:false
                        },
                        {
                            id:3,
                            name:"微软",
                            url:"www.baidu.com",
                            hot:false
                        }
                    ]
                }
            ]
        }
    }
}
</script>

<style scoped lang="less">

.index-wrap{
    width: 1200px;
    margin:0 auto;
    overflow: hidden;
}

.index-left{
    float: left;
    width: 300px;
    text-align: left;
}
.index-right{
    float:left;
    width:900px;
}
.index-left-block{
    margin: 15px;
    background: #fff;
    box-shadow: 0 0 1px #ddd;
}

.index-left-block .hr{
    margin-bottom: 20px;
    height: 1px;
    width: 100%;
    background: #ddd;
}
.index-left-block h2{
    background: #4fc08d;
    color: #fff;
    padding: 10px 15px;
    margin-bottom: 20px;
}
.index-left-block h3{
    padding: 0 15px 5px 15px;
    font-weight: bold;
    color: #222;
}

.index-left-block ul{
    padding: 10px 15px;
}

.index-left-block li{
    padding:5px;
    a{
        color:#222;
    }
}
.index-board-list{
    overflow:hidden;
    margin-top: 15px;
}
.index-board-item{
    float: left;
    width: 400px;
    background: #fff;
    box-shadow: 0 0 1px #ddd;
    padding: 20px;
    margin-right: 20px;
    margin-bottom: 20px;
}

.index-board-item-inner{
    min-height: 125px;
    padding-left: 120px;
}
/**
.index-board-openproduct .index-board-item-inner{
    background: url(../assets/images/1.png) no-repeat;
}
.index-board-logo .index-board-item-inner{
    background: url(../assets/images/2.jpeg) no-repeat;
}
.index-board-golife .index-board-item-inner{
    background: url(../assets/images/1.png) no-repeat;
}
.index-board-heigh .index-board-item-inner{
    background: url(../assets/images/2.jpeg) no-repeat;
}
 */
.index-board-item h2{
    font-size: 18px;
    font-weight: bold;
    color: #000;
    margin-bottom: 15px;
}

.line-last{
    margin-right: 0;
}

.index-board-button{
    margin-top: 20px;
}
.lastest-news{
    min-height: 350px;
}

.hot-tag{
    background: red;
    color: #fff;
}
.new-item{
    display: inline-block;
    width: 230px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.swiperimg{
    width: 100%;
    height: 350px;
}
.swiper-size{
    margin-block: 15px;
}
.button{
    background: #4fc08d;
    color:#fff;
    display: inline-block;
    padding: 10px 20px;
    cursor: pointer;
}
</style>

修改HomeView.vue文件,导入使用左导航栏组件

vue 复制代码
<template>
  <div class="home">
    <home-nav></home-nav>
  </div>
</template>

<script>
import HomeNav from "./HomeNavView.vue"
export default {
  name: 'HomeView',
  components: {
    HomeNav
  }
}
</script>

运行效果

7.2.轮播图

安装轮播图插件:cnpm install swiper@5.x vue-awesome-swiper@3.1.3 --save --no-fund

注:使用新版本可以使用npm,但要下载使用旧版本,需使用cnpm

修改main.js文件,引入使用轮播插件

javascript 复制代码
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import './plugins/element.js'
import './router/permission'
import './utils/init'
import './assets/css/common.css'
// 引入轮播图插件
import VueAwesomeSwiper from 'vue-awesome-swiper'
import 'swiper/css/swiper.css'
// 使用VueAwesomeSwiper
Vue.use(VueAwesomeSwiper)
Vue.config.productionTip = false

new Vue({
  router,
  store,
  render: h => h(App)
}).$mount('#app')

在src/assets目录下新建slideShow目录,存放需要轮播的图片

在HomePage目录下新建HomeSwiper.vue文件,实现轮播组件

vue 复制代码
<template>
  <div class="swiper-size">
    <swiper :options="swiperOption">
        <swiper-slide v-for="(imgSrc,index) in bannerImg" :key="index">
            <img class="swiperimg" :src="imgSrc" alt=""/>
        </swiper-slide>
        <!-- 轮播图下方轮播节点 -->
        <div class="swiper-pagination" slot="pagination"></div>
        <!-- 左右切换按钮 -->
        <div class="swiper-button-prev" slot="button-prev"></div>
        <div class="swiper-button-next" slot="button-next"></div>
    </swiper>
  </div>
</template>

<script>
export default {
    data(){
        return{
            swiperOption:{
                pagination:{
                    el:".swiper-pagination",
                },
                autoplay:true,
                navigation:{
                    nextEl:".swiper-button-next",
                    prevEl:".swiper-button-prev"
                },
                loop:true
            },
            bannerImg:[
                require("@/assets/slideShow/1.png"),
                require("@/assets/slideShow/2.jpg"),
                require("@/assets/slideShow/3.jpg"),
                require("@/assets/slideShow/4.jpg"),
            ]
        }
    }
}
</script>

<style scoped lang="less">
.swiper-size{
    margin-top: 15px;
}
.swiperimg{
    width: 100%;
    height: 450px;
}
</style>

修改HomeView.vue文件,引入轮播组件

vue 复制代码
<template>
  <div class="index-wrap">
    <div class="index-left">
      <home-nav></home-nav>
    </div>
    <div class="index-right">
      <home-swiper></home-swiper>
    </div>
  </div>
</template>

<script>
import HomeNav from "./HomeNavView.vue"
import HomeSwiper from "./HomeSwiper.vue"
export default {
  name: 'HomeView',
  components: {
    HomeNav,HomeSwiper
  }
}
</script>
<style lang="less" scoped>
.index-wrap{
    width: 1500px;
    margin:0 auto;
    overflow: hidden;
}

.index-left{
    float: left;
    width: 300px;
    text-align: left;
}
.index-right{
    float:left;
    width:1200px;
}
</style>

运行效果

7.2.1.封装组件

在components目录下,新建SwiperView.vue文件,用做通用轮播图组件

编辑SwiperView.vue文件,跟HomeSwiper.vue文件内容差不多,只是图片信息改用props接收

vue 复制代码
<template>
  <div class="swiper-size">
    <swiper :options="swiperOption">
        <swiper-slide v-for="(imgSrc,index) in bannerSwiperImg" :key="index">
            <img class="swiper-img" :src="imgSrc" alt=""/>
        </swiper-slide>
        <!-- 轮播图下方轮播节点 -->
        <div class="swiper-pagination" slot="pagination"></div>
        <!-- 左右切换按钮 -->
        <div class="swiper-button-prev" slot="button-prev"></div>
        <div class="swiper-button-next" slot="button-next"></div>
    </swiper>
  </div>
</template>

<script>
export default {
    data(){
        return {
            // 轮播图选项
            swiperOption:{
                pagination:{
                    el:".swiper-pagination"
                },
                // 是否自动轮播
                autoplay:true,
                // 切换组件
                navigation:{
                    nextEl:".swiper-button-next",
                    prevEl:".swiper-button-prev"
                },
                // 轮播进度
                loop:true
            }
        }
    },
    props:{
        bannerSwiperImg:{
            type:Array,
            default(){
                // 没有传入数据,则默认使用一下图片
                return [
                    require("@/assets/slideShow/1.png"),
                ]
            }
        }
    },

}
</script>

<style scoped>
.swiper-size{
    margin-top: 15px;
}
.swiper-img{
    width: 100%;
    height: 450px;
}
</style>

修改HomeView.vue文件,改为使用通用组件

vue 复制代码
<template>
  <div class="index-wrap">
    <div class="index-left">
      <home-nav-view />
    </div>
    <div class="index-right">
      <swiper-view :bannerSwiperImg="bannerSwiperImg"/>
    </div>
  </div>
</template>

<script>
import SwiperView from '@/components/SwiperView.vue'
import HomeNavView from './HomeNavView.vue'

export default {
  name: 'HomeView',
  data(){
    return {
      bannerSwiperImg:[
                require("@/assets/slideShow/2.jpg"),
                require("@/assets/slideShow/1.png"),
                require("@/assets/slideShow/4.jpg"),
                require("@/assets/slideShow/3.jpg"),
      ]
    }
  },
  components: {
    HomeNavView,
    SwiperView
  },
  
}
</script>

<style scoped>
.index-wrap{
    width: 1500px;
    margin:0 auto;
    overflow: hidden;
}

.index-left{
    float: left;
    width: 300px;
    text-align: left;
}
.index-right{
    float:left;
    width:1200px;
}
</style>

运行效果

7.3.信息列表

在HomePage目录下新建HomeProductList.vue文件

vue 复制代码
<template>
  <div class="index-board-list">
    <div class="index-board-item" 
        v-for="(item,index) in buyData" 
        :key="index"
        :class="['index-board-' + item.url,{'line-last':index%2 !== 0}]">
        <div class="index-board-item-inner">
            <h2>{{ item.title }}</h2>
            <p>{{ item.desc }}</p>
            <div class="index-board-button">
                <router-link  to="/details" class="button">立即购买</router-link>
            </div>
        </div>
        
    </div>
  </div>
</template>

<script>
export default {
    data(){
        return{
            buyData:[
                {
                    title:"开放产品",
                    desc:"开放产品描述",
                    url:"openproduct"
                },
                {
                    title:"品牌营销",
                    desc:"品牌营销描述",
                    url:"logo"
                },
                {
                    title:"电子产品",
                    desc:"电子产品描述",
                    url:"golife"
                },
                {
                    title:"酒水产品",
                    desc:"酒水产品描述",
                    url:"heigh"
                },
            ]
        }
    }
}
</script>

<style scoped lang="less">
.index-board-list{
    overflow:hidden;
    margin-top: 15px;
}
.index-board-item{
    float: left;
    width: 550px;
    background: #fff;
    box-shadow: 0 0 1px #ddd;
    padding: 20px;
    margin-right: 20px;
    margin-bottom: 20px;
}

.index-board-item-inner{
    min-height: 125px;
    padding-left: 120px;
}

.index-board-openproduct .index-board-item-inner{
    background: url(@/assets/images/1.jpg) no-repeat;
    background-size: 120px 120px;
}
.index-board-logo .index-board-item-inner{
    background: url(@/assets/images/2.jpeg) no-repeat;
    background-size: 120px 120px;
}
.index-board-golife .index-board-item-inner{
    background: url(@/assets/images/3.jpg) no-repeat;
    background-size: 120px 120px;
}
.index-board-heigh .index-board-item-inner{
    background: url(@/assets/images/4.jpg) no-repeat;
    background-size: 120px 120px;
}

.index-board-item h2{
    font-size: 18px;
    font-weight: bold;
    color: #000;
    margin-bottom: 15px;
}

.line-last{
    margin-right: 0;
}

.index-board-button{
    margin-top: 20px;
}
.lastest-news{
    min-height: 350px;
}
.new-item{
    display: inline-block;
    width: 230px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.button{
    background: #4fc08d;
    color:#fff;
    display: inline-block;
    padding: 10px 20px;
    cursor: pointer;
}
</style>

百度找几张图标,放在src/assets/images目录下

修改HomeView.vue文件,添加使用HomeProductList组件

vue 复制代码
<template>
  <div class="index-wrap">
    <div class="index-left">
      <home-nav></home-nav>
    </div>
    <div class="index-right">
      <home-swiper></home-swiper>
      <home-product-list></home-product-list>
    </div>
  </div>
</template>

<script>
import HomeNav from "./HomeNavView.vue"
import HomeSwiper from "./HomeSwiper.vue"
import HomeProductList from "./HomeProductList.vue"
export default {
  name: 'HomeView',
  components: {
    HomeNav,HomeSwiper,HomeProductList
  }
}
</script>
<style lang="less" scoped>
.index-wrap{
    width: 1500px;
    margin:0 auto;
    overflow: hidden;
}

.index-left{
    float: left;
    width: 300px;
    text-align: left;
}
.index-right{
    float:left;
    width:1200px;
}
</style>

运行效果

7.4.详情页实现

在src/views/main/HomePage目录下新建sub目录,用于存放子页面文件

新建DetailsView.vue,编写详情页信息

vue 复制代码
<template>
  <div class="detail-wrap">
    <div class="detail-left">
      <div class="product-board">
        <img src="@/assets/images/1.jpg" alt="">
        <ul>
          <router-link 
            tag="li" 
            active-class="active"
            v-for="(item,index) in detailsNav" 
            :key="index" 
            :to="'/details/' + item.id">
            {{ item.title }}
          </router-link>
        </ul>
      </div>
    </div>
    <div class="detail-right">
      <router-view></router-view>
    </div>
  </div>
</template>

<script>
export default {
    data(){
      return {
        detailsNav:[
          {
            title:"开放产品",
            id:"openproduct"
          },
          {
            title:"品牌营销",
            id:"logo"
          },
          {
            title:"电子产品",
            id:"golife"
          },
          {
            title:"酒水产品",
            id:"heigh"
          },
        ]
      }
    }
}
</script>

<style >
.detail-wrap{
  width: 1200px;
  margin: 0 auto;
  overflow: hidden;
  padding-top: 20px;
}
.detail-left {
  float: left;
  width: 200px;
  text-align: center;
}
.detail-right{
  float: left;
  width: 980px;
  margin-left: 20px;
}
.product-board{
  background: #fff;
  padding: 20px 0;
}
.product-board img{
  width: 120px;
  height: 120px;
}
.product-board ul{
  margin-top: 20px;
}
.product-board li{
  text-align: left;
  padding: 10px 15px;
  cursor: pointer;
}
.product-board li.active,
.product-board li:hover{
  background: #4fc08d;
  color:#fff;
}
.product-board li a{
  display: block;
}
/* 下方为右边区域子页面样式 */
.sales-board{
  background: #fff;
}
.sales-board-intro h2{
  font-size: 20px;
  padding: 20px;
}
.sales-board-intro p{
  background: #f7fcff;
  padding: 10px 20px;
  color: #999;
  line-height: 1.8;
}
.sales-board-form{
  padding: 10px 20px;
  font-size: 14px;
}
.sales-board-line{
  clear: both;
  padding-bottom: 20px
}
.sales-board-line-left{
  display: inline-block;
  margin-right: 10px;
}
.sales-board-line-right {
  display: inline-block;
  width: 25%;
}
.sales-board-des {
  border-top: 20px solid #fff;
  background: #fff;
  padding: 15px 20px;
}
.sales-board-des p {
  line-height: 1.6;
}
.sales-board-des h2{
  font-size: 20px;
  padding-bottom: 15px;
}
.sales-board-des h3{
  font-size: 18px;
  font-weight: bold;
  padding: 20px 0 10px 0;
}
.sales-board-des li{
  padding: 5px 0;
}
.sales-board-table{
  width:100%;
  margin-top: 20px;
}
.sales-board-table th{
  border: 1px solid #4fc08d;
  color:#fff;
}
.sales-board-table td{
  border: 1px solid #f0f2f5;
  padding:15px;
}
</style>

新建Openproduct.vueGolife.vueHeigh.vueLogo.vue四个模块文件

编辑Openproduct.vue

vue 复制代码
<template>
<div class="open">
  <div class="sales-board">
    <div class="sales-board-intro">
        <h2>开放产品</h2>
        <p>
            中国和拉美虽然相距遥远,但共同的梦想和追求将双方紧紧联系在一起。<br>在遥远的南美国家哥伦比亚,一场与中国的 "地铁之约" 正在精彩上演。<br>两年来,哥伦比亚首都波哥大地铁一号线项目建设热火朝天,50 名当地青年学员更是不远万里,先后来到中国西安进行专业培训。
            <br>这不仅仅是一次学习之旅,更是中拉基建合作的生动写照。<br>中国与哥伦比亚携手,共同为城市交通注入新活力。从规划到建设,每一个环节都凝聚着双方的智慧和努力。
        </p>
        <div class="sales-board-form">
            <div class="sales-board-line-left">购买数量:</div>
            <div class="sales-board-line-right">
                <!-- 购买数量组件 -->
                <Counter :counterObj="counterObj"></Counter>
            </div>
        </div>
        <div class="sales-board-form">
            <div class="sales-board-line-left">产品类型:</div>
            <!-- 产品类型选择组件 -->
            <Types :selecterData="selecterData"/>
        </div>
        <div class="sales-board-form">
            <div class="sales-board-line-left">有效时间:</div>
            <!-- 有效时间选择组件 -->
            <Timer :timerData="timerData"/>
        </div>
    </div>
    <div class="sales-board-des">
        <h2>产品说明</h2>
        <p>这是产品说明...这是产品说明......这是产品说明...这是产品说明......这是产品说明...这是产品说明......</p>
        <h2>用户行为指标</h2>
        <ul>
            <li>用户行为指标...用户行为指标...用户行为指标...用户行为指标...</li>
            <li>用户行为指标1</li>
            <li>用户行为指标2</li>
            <li>用户行为指标3</li>
            <li>用户行为指标4</li>
        </ul>

        <h3>浏览网站方式</h3>
        <ul>
            <li>浏览网站方式1</li>
            <li>浏览网站方式2</li>
            <li>浏览网站方式3</li>
            <li>浏览网站方式4</li>
            <li>浏览网站方式5</li>
        </ul>
    </div>
  </div>
</div>
</template>

<script>
import Counter from '@/components/HomePage/counter'
import Types from '@/components/HomePage/types'
import Timer from '@/components/HomePage/timer'
export default {
    data(){
        return {
            counterObj:{
                min:1,
                max:20
            },
            selecterData:[
                {
                    value:"经典型",
                    id:1
                },
                {
                    value:"加强型",
                    id:2
                },
                {
                    value:"至尊豪华型",
                    id:3
                },
            ],
            timerData:[
                {
                    value:'一个月',
                    id:1
                },
                {
                    value:'三个月',
                    id:3
                },
                {
                    value:'六个月',
                    id:6
                },
                {
                    value:'一年',
                    id:12
                },
            ]
        }
    },
    components:{
        Counter,Types,Timer
    }
}
</script>

<style scoped>
.open{
    text-align: left;
}
.buy-dialog-title{
    font-size: 16px;
    font-weight: bold;
}
.buy-dialog-btn{
    margin-top: 20px;
}
.buy-dialog-table{
    width:100%;
    margin-bottom: 20px;
}
.buy-dialog-table td,
.buy-dialog-table th{
    border: 1px solid #e3e3e3;
    text-align: center;
    padding: 5px 0;
}
.buy-dialog-table th{
    background: #4fc08d;
    color: #fff;
    border: 1px solid #4fc08d;
}
.button{
    background: #4fc08d;
    color: #fff;
    display: inline-block;
    padding: 10px 20px;
    cursor: pointer;
}
</style>

修改router/index.js文件,增加详情页路由

javascript 复制代码
import Vue from 'vue'
import VueRouter from 'vue-router'
import HomeView from '../views/main/HomePage/HomeView.vue'
import Layout from '../views/LayoutView.vue'

Vue.use(VueRouter)

const routes = [
  {
    path: '/',
    name: 'Layout',
    component: Layout,
    children:[
      {
        path: '',
        name: 'home',
        component: HomeView,
        meta:{
          isLogin:true
        }
      },
      {
        path:"product",
        name:"Product",
        component:() => import("../views/main/ProductPage"),
        meta:{
          isLogin:true
        }
      },
      {
        path:"params",
        name:"Params",
        component:() => import("../views/main/ParamsView.vue"),
        meta:{
          isLogin:true
        }
      },
      {
        path:"ad",
        name:"ADCategory",
        component:() => import("../views/main/ADCategory.vue"),
        meta:{
          isLogin:true
        }
      },
      {
        path:"details",
        name:"Details",
        component:() => import("../views/main/HomePage/sub/DetailsView.vue"),
        children:[
          {
            path:"openproduct",
            name:"OpenProduct",
            component:() => import("../views/main/HomePage/sub/Openproduct.vue")
          },
          {
            path:"golife",
            name:"Golife",
            component:() => import("../views/main/HomePage/sub/Golife.vue")
          },
          {
            path:"logo",
            name:"Logo",
            component:() => import("../views/main/HomePage/sub/Logo.vue")
          },
          {
            path:"heigh",
            name:"Heigh",
            component:() => import("../views/main/HomePage/sub/Heigh.vue")
          },
        ],
        meta:{
          isLogin:true
        }
      }
    ]
  },
  {
    path:'/login',
    name:'Login',
    component:() => import("../views/LoginView.vue")
  }
]

const router = new VueRouter({
  routes,
  // 去除路径上的#号
  mode:"history"
})

export default router

修改HomeProductList.vue文件,增加具体跳转路由信息

vue 复制代码
<template>
  <div class="index-board-list">
    <div class="index-board-item" 
        v-for="(item,index) in buyData" 
        :key="index"
        :class="['index-board-' + item.url,{'line-last':index%2 !== 0}]">
        <div class="index-board-item-inner">
            <h2>{{ item.title }}</h2>
            <p>{{ item.desc }}</p>
            <div class="index-board-button">
                <!-- 拼接具体跳转位置 -->
                <router-link  :to="'/details/' + item.url" class="button">立即购买</router-link>
            </div>
        </div>
    </div>
  </div>
</template>
7.3.1.购买数量组件

在src/components目录下新建HomePage目录,新建counter.vue文件实现购买数量组件

vue 复制代码
<template>
  <div class="counter-component">
    <div  class="counter-btn" @click="minHandle">-</div>
    <div class="counter-show">
        <input type="text" v-model="counter" @keyup="innerHeight">
    </div>
    <div  class="counter-btn" @click="addHandle">+</div>
  </div>
</template>

<script>
export default {
    data(){
        return {
            counter : 1
        }
    },
    props:{
        counterObj:{
            type:Object,
            default(){
                return{ 
                    min:1,
                    max:1
                }
            }
        }
    },
    methods:{
        minHandle(){
            if(this.counter <= this.counterObj.min){
                return;
            }
            this.counter--;
        },
        addHandle(){
            if(this.counter >= this.counterObj.max){
                return;
            }
            this.counter++;
        },
        innerHeight(){
            var fix;
            if(typeof this.counter === "string"){
                fix = Number(this.counter.replace(/\D/g,""));
            }else{
                // 如果用户输入的是字符串,则将最小值赋值给fix
                fix = this.counterObj.min;
            }
            if(fix <= this.counterObj.min){
                fix = this.counterObj.min;
            }
            if(fix > this.counterObj.max){
                fix = this.counterObj.max;
            }
            this.counter = fix;
        }
    }
}
</script>

<style scoped>
.counter-component{
    position: relative;
    display: inline-block;
    overflow: hidden;
    vertical-align: middle;
}
.counter-show{
    float: left;
    width: 50px;
}
.counter-show input{
    width: 50px;
    border: none;
    text-align: center;
    border-top: 1px solid #e3e3e3;
    border-bottom: 1px solid #e3e3e3;
    height: 23px;
    line-height: 23px;
}
.counter-btn{
    border: 1px solid #e3e3e3;
    float: left;
    height: 25px;
    line-height: 25px;
    width: 25px;
    text-align: center;
    cursor: pointer;
}
.counter-btn:hover{
    border-color: #4fc08d;
    background: #4fc08d;
    color: #fff;
}
</style>
7.4.2.产品类型选择组件

在src/components/HomePage目录下,新建types.vue文件实现产品类型选择组件

vue 复制代码
<template>
  <div class="selection-component">
    <div class="selection-show" @click="showListHendle"> 
        <span>{{ selecterData[currentIndex].value }}</span>
        <div class="arrow"></div>
    </div>
    <div class="selection-list" v-show="isOpen">
        <ul>
            <li v-for="(item,index) in selecterData" 
                :key="index"
                @click="selectHandle(index)">
                {{ item.value }}
            </li>
        </ul>
    </div>
  </div>
</template>

<script>
export default {
    data(){
        return{
            isOpen:false,
            currentIndex:0,
        }
    },
    props:{
        selecterData:{
            type:Array,
            default(){
                return [{value:"test",id:1}];
            }
        }
    },
    methods:{
        showListHendle(){
            this.isOpen = !this.isOpen;
        },
        selectHandle(index){
            this.currentIndex = index;
            this.isOpen = false;
        }
    }
}
</script>

<style scoped>
.selection-component{
    position: relative;
    display: inline-block;
}
.selection-show{
    border: 1px solid #e3e3e3;
    padding: 0 20px 0 10px;
    display: inline-block;
    position: relative;
    cursor: pointer;
    height: 25px;
    line-height: 25px;
    border-radius: 3px;
    background: #fff;
}
.selection-show .arrow{
    display: inline-block;
    border-left: 4px solid transparent;
    border-right: 4px solid transparent;
    border-top: 5px solid transparent;
    width: 0;
    height: 0;
    margin-top: -1px;
    margin-left: 6px;
    margin-right: -14px;
    vertical-align: middle;
}
.selection-list{
    display: inline-block;
    position: absolute;
    left: 0;
    top: 25px;
    width: 100%;
    background: #fff;
    border-top: 1px solid #e3e3e3;
    border-bottom: 1px solid #e3e3e3;
    z-index: 5;
}
.selection-list li{
    padding: 5px 15px 5px 10px;
    border-left: 1px solid #e3e3e3;
    border-right: 1px solid #e3e3e3;
    cursor: pointer;
    background: #fff;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.selection-list li:hover{
    background: #e3e3e3;
}
</style>
7.4.3.有效时间选择组件

在src/components/HomePage目录下,新建timer.vue文件实现产品类型选择组件

vue 复制代码
<template>
    <div class="chooser-component">
        <ul class="chooser-list">
            <li @click="timerHandle(index)" 
                :class="{active:nowIndex === index}" v-for="(item,index) in timerData" :key="index">{{ item.value }}</li>
        </ul>
    </div>
</template>

<script>
export default {
    data(){
        return {
            nowIndex:0,
        }
    },
    props:{
        timerData:{
            type:Array,
            default(){
                return [{value:"test",id:1}];
            }
        }
    },
    methods:{
        timerHandle(index){
            this.nowIndex = index
        }
    }
}
</script>

<style scoped>
.chooser-component{
    position: relative;
    display: inline-block;
}
.chooser-list li{
    display: inline-block;
    border:1px solid #e3e3e3;
    height: 25px;
    line-height: 25px;
    padding: 0 8px;
    margin-right: 5px;
    border-radius: 3px;
    text-align: center;
    cursor: pointer;
}
.chooser-list li.active{
    border-color: #4fc08d;
    background: #4fc08d;
    color: #fff;
}
</style>

另外三个模块暂时忽略

运行效果

8.商品管理页实现

src/views/main目录下新建ProductPage目录,用于存放商品管理页面的相关组件

ProductPage目录下,新建index.vue文件,作为商品管理页主入口

vue 复制代码
<template>
  <div class="product">
      商品管理
  </div>
</template>

<script>
export default {
}
</script>

<style>
.product{
  width: 1200px;
  margin: 0 auto;
  margin-top: 20px;
}
</style>

修改product的路由配置

vue 复制代码
	{
        path:'product',
        name:'Product',
        component:() => import('../views/main/ProductPage'),
        meta:{
          isLogin:true
        }
      },

8.1.服务器数据接口

修改server/router.js文件,增加商品管理相关操作接口

java 复制代码
const url = require("url")
const fs = require("fs")
const multer = require("multer")

/**
 * 商品查询
 */
router.get("/backend/item/selectTbItemAllByPage",(req,res)=>{
    // 分页
    const page = url.parse(req.url,true).query.page || 1;
    const sql = "select * from project order by id desc limit 10 offset " + (page - 1) * 10;
    sqlClient(sql,null,result=>{
        if(result.length > 0){
            res.send({
                status:200,
                result
            })
        }else{
            res.send({
                status:401,
                msg:"暂无数据"
            })
        }
    })
})
/**
 * 商品总数
 */
router.get("/total",(req,res)=>{
    const sql = "SELECT COUNT(*) AS count FROM project where id";
    sqlClient(sql,null,result =>{
        if(result.length > 0){
            res.send({
                status:200,
                result
            })
        }else{
            res.send({
                status:500,
                msg:"暂无更多数据"
            })
        }
    })
})

/**
 * 模糊查询
 */
router.get("/search",(req,res)=>{
    const search = url.parse(req.url,true).query.search;
    const sql = "SELECT * FROM project WHERE concat(`title`,`sellPoint`,`descs`) LIKE '%" + search + "%'";
    sqlClient(sql,null,result =>{
        if(result.length > 0){
            res.send({ 
                status:200,
                result
            })
        }else{
            res.send({
                status:500,
                msg:"暂无数据"
            })
        }
    })
})

/**
 * 类目选择
 */
router.get("/backend/itemCategory/selectItemCategoryByParentId",(req,res)=>{
    const id = url.parse(req.url,true).query.id || 1;
    const sql = "SELECT cid,pid,name FROM category WHERE pid = ?";
    const arr = [id]
    sqlClient(sql,arr,result =>{
        if(result.length > 0){
            res.send({
                status:200,
                result
            })
        }else{
            res.send({
                status:500,
                msg:"暂无数据"
            })
        }
    })
})

/**
 * 上传图片
 */
var storage = multer.diskStorage({
    destination:function(req,file,cb){
        cb(null,'./upload/')
    },
    filename:function(req,file,cb){
        cb(null,Date.now() + "-" + file.originalname)
    }
})

var createFolder = function (folder) {
    try {
        fs.accessSync(folder)
    } catch (e) {
        fs.mkdirSync(folder)
    }
}

var uploadFolder = './upload/';
createFolder(uploadFolder);
var upload = multer({storage : storage});

router.post('/upload',upload.single('file'),function(req,res,next){
    var file = req.file;
    console.log('文件类型:%s',file.mimetype);
    console.log('原文件名:%s',Buffer.from(file.originalname, "latin1").toString("utf8"));
    console.log('文件大小:%s',file.size);
    console.log('文件保存路径:%s',Buffer.from(file.path, "latin1").toString("utf8"));
    res.json({
        res_code:'0',
        name:Buffer.from(file.originalname, "latin1").toString("utf8"),
        url:Buffer.from(file.path, "latin1").toString("utf8")
        });
})

/**
 * 添加商品
 */
router.get("/backend/item/insertTbItem",(req,res)=>{
    const cid = url.parse(req.url,true).query.cid || "";
    const title = url.parse(req.url,true).query.title || "";
    const sellPoint = url.parse(req.url,true).query.sellPoint || "";
    const price = url.parse(req.url,true).query.price || "";
    const num = url.parse(req.url,true).query.num || "";
    const image = url.parse(req.url,true).query.image || "";
    const desc = url.parse(req.url,true).query.desc || "";
    const sql = "INSERT INTO project(`title`,`image`,`sellPoint`,`price`,`cid`,`num`,`status`,`descs`) VALUES(?,?,?,?,?,?,1,?)"
    const arr = [title,image,sellPoint,price,cid,num,desc];
    sqlClient(sql,arr,result=>{
        if(result.affectedRows > 0){
            res.send({
                status:200,
                msg:"添加成功"
            })
        }else{
            res.send({
                status:500,
                msg:"添加失败"
            })
        }
    })
})

/**
 * 商品删除
 */
router.get("/backend/item/deleteItemById",(req,res) =>{
    const id = url.parse(req.url,true).query.id;
    const sql = "DELETE FROM project WHERE id = ?";
    const arr = [id];
    sqlClient(sql,arr,result =>{
        if(result.affectedRows > 0){
            res.send({
                status:200,
                msg:"删除成功"
            })
        }else{
            res.send({
                status:500,
                msg:"删除失败"
            })
        }
    })
})

/**
 * 预更新
 */
router.get("/backend/item/preUpdateItem",(req,res) =>{
    const id = url.parse(req.url,true).query.id;
    const sql = "SELECT * FROM project WHERE id = ?";
    sqlClient(sql,[id],result =>{
        if(result.length > 0){
            res.send({
                status:200,
                result
            })
        }else{
            res.send({
                status:500,
                msg:"预更新失败"
            })
        }
    })
})


/**
 * 编辑商品信息
 */
router.get("/backend/item/updateTbItem",(req,res) =>{
    const cid = url.parse(req.url,true).query.cid || "";
    const title = url.parse(req.url,true).query.title || "";
    const sellPoint = url.parse(req.url,true).query.sellPoint || "";
    const price = url.parse(req.url,true).query.price || "";
    const num = url.parse(req.url,true).query.num || "";
    const image = url.parse(req.url,true).query.image || "";
    const desc = url.parse(req.url,true).query.desc || "";
    const id = url.parse(req.url,true).query.id;
    const sql = "UPDATE project set title=?,sellPoint=?,cid=?,price=?,descs=?,image=?,num=? WHERE id=?";
    const arr = [title,sellPoint,cid,price,desc,image,num,id]
    sqlClient(sql,arr,(result) =>{
        if(result.affectedRows > 0){
            res.send({
                status:200,
                msg:"修改成功"
            })
        }else{
            res.send({
                status:500,
                msg:"修改失败"
            })
        }
    })
})

修改server/router.js文件,增加静态文件位置

javascript 复制代码
const express = require("express");
const app = express();
const cors = require("cors");
const bodyParser = require("body-parser");
const router = require("./router")

app.use(cors());
app.use(bodyParser.urlencoded({
    extended:true
}))

app.use("/api",router);
app.use(express.static("upload"))
// 监听端口号
app.listen(3000,() =>{
    console.log(3000)
})

src/api/base.js文件中增加对应信息

vue 复制代码
const base = {
    baseUrl:"http://localhost:3000",
    register:"/api/register",	// 注册
    login:"/api/login",	// 登录
    selectTbItemAllByPage:"/api/backend/item/selectTbItemAllByPage", // 商品列表
    total:"/api/total", // 商品总数
    search:"/api/search",     // 模糊查询
    selectItemCategoryByParentId:"/api/backend/itemCategory/selectItemCategoryByParentId",  // 类目选择
    insertTbItem:"/api/backend/item/insertTbItem",  // 商品添加
    deleteItemById:"/api/backend/item/deleteItemById", //商品删除
    preUpdateItem:"/api/backend/item/preUpdateItem",      // 预更新
    updateTbItem:"/api/backend/item/updateTbItem",    // 修改商品
}
export default base;

修改src/api/index.js文件,增加商品查询api

javascript 复制代码
	/**
     * 商品列表
     */
    selectTbItemAllByPage(params){
        return axios.get(base.baseUrl + base.selectTbItemAllByPage,{
            params
        })
    },
     /**
     * 商品数量
     */
    total(){
        return axios.get(base.baseUrl + base.total)
    },
    /**
     * 模糊查询
     */
    search(params){
        return axios.get(base.baseUrl + base.search,{
            params
        })
    },
    /**
     * 类目选择
     */
    selectItemCategoryByParentId(params){
        return axios.get(base.baseUrl + base.selectItemCategoryByParentId,{
            params
        })
    },
    /**
     * 添加商品
     */
    insertTbItem(params){
        return axios.get(base.baseUrl + base.insertTbItem,{
            params
        })
    },
    /**
     * 商品删除
     */
    deleteItemById(params){
        return axios.get(base.baseUrl + base.deleteItemById,{
            params
        })
    },

    /**
     * 预更新
     */
    preUpdateItem(params){
        return axios.get(base.baseUrl + base.preUpdateItem,{
            params
        })
    },
    /**
     * 修改商品
     */
    updateTbItem(params){
        return axios.get(base.baseUrl + base.updateTbItem,{
            params
        })
    }

8.2.eventbus工具类实现

src/utils目录下新建eventbut.js文件,实现EventBus,方便组件之间的数据传递

javascript 复制代码
import Vue from "vue"

const EventBus = new Vue();
Object.defineProperties(Vue.prototype,{
    $bus:{
        get(){
            return EventBus;
        }
    }
})

修改main.js文件,【这里顺便加api的引用,方便后面调用api】

vue 复制代码
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import './plugins/element.js'
import './router/permission'
import './utils/init'
import './assets/css/common.css'
// 引入轮播图插件
import VueAwesomeSwiper from 'vue-awesome-swiper'
import 'swiper/css/swiper.css'
// 引入api
import api from "./api"
// 引入eventbus
import "./utils/eventbus"

Vue.prototype.$api = api
// 使用VueAwesomeSwiper
Vue.use(VueAwesomeSwiper)
Vue.config.productionTip = false

new Vue({
  router,
  store,
  render: h => h(App)
}).$mount('#app')

8.3.商品列表信息

src/views/main/ProductPage目录下,新建ProductList.vue文件,实现商品列表展示

vue 复制代码
<template>
  <div>
    <el-table :data="tableData">
        <el-table-column show-overflow-tooltip prop="id" label="产品id" width="100"></el-table-column>
        <el-table-column show-overflow-tooltip prop="title" label="产品名称" width="150"></el-table-column>
        <el-table-column show-overflow-tooltip prop="image" label="产品图片" width="100"></el-table-column>
        <el-table-column show-overflow-tooltip prop="sellPoint" label="产品卖点" width="300"></el-table-column>
        <el-table-column show-overflow-tooltip prop="price" label="产品价格" width="100"></el-table-column>
        <el-table-column show-overflow-tooltip prop="num" label="产品数量" width="100"></el-table-column>
        <el-table-column show-overflow-tooltip prop="descs" label="产品描述">
            <template v-slot="scope">
                <div v-html="scope.row.descs"></div>
            </template>
        </el-table-column>
        <el-table-column label="操作">
            <template v-slot="scope">
                <el-button size="mini" @click="handleEdit(scope.$index, scope.row)">编辑</el-button>
                <el-button size="mini" type="danger" @click="handleDelete(scope.$index, scope.row)">删除</el-button>
            </template>
        </el-table-column>
    </el-table>
  </div>
</template>

<script>
export default {
    data(){
        return {
            tableData:[]
        }
    },
    mounted(){
        this.http(1);
        // 接收分页组件传递的数据
        this.$bus.$on("page",page =>{
            this.http(page)
        })
        // 接收搜索框组件传递的数据
        this.$bus.$on("searchData",data=>{
            this.tableData = data
        })
        this.$bus.$on("refresh",flag =>{
            this.http(1)
        })
    },
    methods:{
        http(page){
            this.$api.selectTbItemAllByPage({page}).then(res =>{
                // 获取对应页的商品数据
                if(res.data.status === 200){
                    this.tableData =res.data.result
                }
            })
        },
        // 编辑按钮
        handleEdit(index,row){
            this.$bus.$emit("onEditorEvent",row);
        },
        // 删除按钮
        handleDelete(index,row){
            this.$confirm("此操作会永久删除该数据,是否继续?","提示",{
                confirmButtonText:"确定",
                cancelButtonText:"取消",
                type:"warning",
            }).then(() =>{
                this.$api.deleteItemById({ id:row.id }).then(res =>{
                    if(res.data.status === 200){
                        this.$message({
                            type:"success",
                            message:"删除成功"
                        })
                        this.http(1);
                    }else{
                        this.$message({
                            type:"error",
                            message:"删除失败"
                        })
                    }
                })
            }).catch(() => {
                this.$message({
                    type:"info",
                    message:"已取消删除"
                })
            })
        },
    }
}
</script>

<style scoped>

</style>

修改index.vue文件,增加使用列表组件

vue 复制代码
<template>
    <div class="product">
        <ProductList/>
    </div>
  </template>
  
  <script>
  import ProductList from './ProductList.vue'
  export default {
    components:{
      ProductList
    }
  }
  </script>
  
  <style>
  .product{
    width: 1200px;
    margin: 0 auto;
    margin-top: 20px;
  }
  </style>

运行效果

8.4.搜索框

ProductPage目录下新建ProductHeader.vue文件

vue 复制代码
<template>
  <div class="head">
    <el-form ref="searchForm" :model="search" @submit.native.prevent>
        <el-form-item>
            <el-input v-model="search.content" @keyup.enter.native="onSubmitSearch"/>
        </el-form-item>
        <el-form-item>
            <el-button type="primary" @click="onSubmitSearch">查询</el-button>
        </el-form-item>
        <el-form-item>
            <el-button type="primary" @click="addFormHandle">添加</el-button>
        </el-form-item>
    </el-form>
  </div>
</template>

<script>
export default {
    data(){
        return {
            search:{
                content:""
            }
        }
    },
    methods:{
        onSubmitSearch(){
            // 查询
            this.$api.search({
                search:this.search.content
            }).then(res =>{
                // 将查询到的数据传递给接收对象
                this.$bus.$emit('searchData',res.data.result)
            }).catch(error =>{
                console.log(error);
            })
        },
        addFormHandle(){
            // 打开添加商品窗口
            this.$bus.$emit("onAddEvent",true)
        }
    }
}
</script>

<style scoped lang="less">
.el-form{
    overflow: hidden;
    clear: both;
    .el-form-item{
        float: left;
        margin-right: 10px;
        .el-input{
            width:1030px;
        }
    }
}
.head{
    margin-top: 20px;
    width: 100%;
}
</style>

修改index.vue文件,增加使用搜索框组件

vue 复制代码
<template>
    <div class="product">
        <ProductHeader/>
        <ProductList/>
    </div>
  </template>
  
  <script>
  import ProductList from './ProductList.vue'
  import ProductHeader from './ProductHeader.vue'
  export default {
    components:{
      ProductList,ProductHeader
    }
  }
  </script>
  
  <style>
  .product{
    width: 1200px;
    margin: 0 auto;
    margin-top: 20px;
  }
  </style>

运行效果

8.5.分页

ProductPage目录下新建ProductPagination.vue文件

vue 复制代码
<template>
    <!-- 分页 -->
  <div class="pagination-container">
    <el-pagination
        layout="prev,pager,next,jumper"
        :current-page.sync="currentPage"
        @current-change="hangleCurrentChange"
        :total="total" 
        @size-change="handleSizechange"
    >
    </el-pagination>
  </div>
</template>

<script>
export default {
    data(){
        return {
            currentPage:1,
            total:0
        }
    },
    methods:{
        handleSizechange(){
            
        },
        hangleCurrentChange(val){
            this.$bus.$emit("page",val)
        }
    },
    mounted(){
        this.$api.total().then(res =>{
            if(res.data.status === 200){
                this.total = res.data.result[0]["count"]
            }
        })
    }
}
</script>

<style scoped>
.pagination-container{
    margin-top : 20px;
}
</style>

修改index.vue文件,增加使用分页组件

vue 复制代码
<template>
  <div class="product">
    <product-header></product-header>
    <product-list/>
    <product-pagination/>
  </div>
</template>

<script>
import ProductHeader from './ProductHeader.vue'
import ProductList from './ProductList'
import ProductPagination from './ProductPagination.vue'
export default {
  components:{
    ProductList,ProductPagination,ProductHeader
  }
}
</script>

<style>
.product{
  width: 1200px;
  margin: 0 auto;
  margin-top: 20px;
}
</style>

运行效果

8.6.添加商品窗口

ProductPage目录下新建ProductAdd.vuePriductUpload.vueProductTree.vueProductWangEditor.vue文件

编写ProductAdd.vue文件

vue 复制代码
<template>
    <el-dialog  
        title="添加产品"
        :visible.sync="diaologAddVisible"
        width="60%"
        :before-close="handleClose"
    >
        <el-form 
            label-width="80px"
            :model="addForm"
            ref="addForm"    
        >
            <el-form-item label="商品类目:">
                <span class="location tree">{{ treeData.name }}</span>
                <el-button type="primary" class="location" @click="dialogCategoryHandle">类目选择</el-button>
                <el-dialog
                    width="50%"
                    append-to-body
                    title="类目选择"
                    :visible.sync="dialogCategoryVisible"
                >
                    <product-tree @onTree="getTreeData"></product-tree>
                    <span slot="footer" class="dialog-footer">
                        <el-button type="primary" @click="dialogCategoryClose">确认</el-button>
                    </span>
                </el-dialog>
            </el-form-item>
            <el-form-item label="商品名称:">
                <el-input v-model="addForm.name"></el-input>
            </el-form-item>
            <el-form-item label="商品卖点:">
                <el-input v-model="addForm.sellPoint"></el-input>
            </el-form-item>
            <el-form-item label="商品价格:">
                <el-input v-model="addForm.price"></el-input>
            </el-form-item>
            <el-form-item label="商品数量:">
                <el-input v-model="addForm.num"></el-input>
            </el-form-item>
            <el-form-item label="商品图片:">
                <img class="upload-img" :src="uploadData.url" alt="">
                <el-button type="primary" class="location" 
                    @click="dialogUploadHandle">上传图片</el-button>
                
                <el-dialog
                    width="50%"
                    append-to-body
                    title="图片上传"
                    :visible.sync="dialogUploadVisible"
                >
                    <priduct-upload @onUpload="getOnUpload"></priduct-upload>
                    <span slot="footer" class="dialog-footer">
                        <el-button type="primary" @click="dialogUploadClose">确认</el-button>
                    </span>
                </el-dialog>
                
            </el-form-item>
            <el-form-item label="商品描述:">
                <ProductWangEditor @onEditor="getEditor"></ProductWangEditor>
            </el-form-item>
        </el-form>
        <span slot="footer" class="dialog-footer">
            <el-button @click="addFormClose">取消</el-button>
            <el-button type="primary" @click="addProductHandle">确认</el-button>
        </span>
    </el-dialog>
</template>

<script>
import PriductUpload from './PriductUpload.vue';
import ProductTree from './ProductTree.vue';
import ProductWangEditor from './ProductWangEditor.vue';
export default{
    components:{ProductTree,PriductUpload,ProductWangEditor},
    data(){
        return {
            diaologAddVisible : false,
            dialogCategoryVisible:false,
            dialogUploadVisible:false,
            addForm:{
                name:"",
                sellPoint:"",
                price:"",
                num:"",
            },
            treeData:{},    // 类目选择    
            uploadData:{},  // 存储图片
            editorData:"",
        }
    },
    mounted(){
        this.$bus.$on("onAddEvent",flag =>{
            this.diaologAddVisible = flag
        })
    },
    methods:{
        handleClose(done){
            this.$confirm("确认关闭")
                .then(_ =>{
                    
                    done();
                })
                .catch(_ =>{});
        },
        addFormClose(){
            this.diaologAddVisible = false
        },
        dialogCategoryHandle(){
            this.dialogCategoryVisible = true
        },
        dialogCategoryClose(){
            this.dialogCategoryVisible = false
        },
        dialogUploadHandle(){
            this.dialogUploadVisible = true
        },
        dialogUploadClose(){
            this.dialogUploadVisible = false
        },
        // 读取类目选择数据
        getTreeData(data){
            // console.log(data);
            this.treeData = data;
        },
        // 读取图片地址
        getOnUpload(data){
            if(data.url){
                data.url = data.url.replace("upload","http://localhost:3000")
            }
            this.uploadData = data;
        },
        //接收富文本数据
        getEditor(data){
            this.editorData = data;
        },
        // 添加产品
        addProductHandle(){
            this.$api.insertTbItem({
                cid:this.treeData.cid,
                title:this.addForm.name,
                sellPoint:this.addForm.sellPoint,
                price:this.addForm.price,
                num:this.addForm.num,
                desc:this.editorData,
                image:this.uploadData.url
            }).then(res =>{
                if(res.data.status === 200){
                    this.diaologAddVisible = false;
                    this.$bus.$emit("refresh",true)
                }
            }).catch(error=>{
                console.log(error);
            })
        }
    }
}
</script>

<style scoped>
.location{
    float: left;
}
.tree{
    margin-right: 20px;
}
.upload-img{
    width: 300px;
    float: left;
    margin-right: 20px;
}
</style>

修改index.vue文件,增加使用添加商品组件

vue 复制代码
<template>
  <div class="product">
    <product-header></product-header>
    <product-list/>
    <product-pagination/>
    <product-add></product-add>
  </div>
</template>

<script>
import ProductHeader from './ProductHeader.vue'
import ProductList from './ProductList'
import ProductPagination from './ProductPagination.vue'
import ProductAdd from './ProductAdd'
export default {
  components:{
    ProductList,ProductPagination,ProductHeader,ProductAdd
  }
}
</script>

<style>
.product{
  width: 1200px;
  margin: 0 auto;
  margin-top: 20px;
}
</style>
8.6.1.图片上传组件

编写PriductUpload.vue文件

vue 复制代码
<template>
    <!-- 图片上传一定给的是单独的地址,而且跨域是后台解决的 -->
  <el-upload
    class="upload-demo"
    ref="upload"
    action="http://localhost:3000/api/upload"
    :on-remove="handleRemove"
    :file-list="fileList"
    :auto-upload="false"
    :on-success="handleSuccess"
  >
  <el-button slot="trigger" size="small" type="primary">选取文件</el-button>
  <el-button 
    style="margin-left: 10px;" 
    size="small" 
    type="success" 
    @click="submitUpload"
    >
    上传到服务器
    </el-button>
    <div slot="tip" class="el-upload__tip">目前仅支持单张图片上传</div>
  </el-upload>
</template>

<script>
export default {
    data(){
        return {
            fileList:[]
        }
    },
    methods:{
        handleRemove(file, fileList) {
            return this.$confirm(`确定移除 ${ file.name }?`);
        },
        handleSuccess(response){
            // 上传成功后返回的数据response
            this.$emit("onUpload",response)
        },
        submitUpload(){
            this.$refs.upload.submit();
        }
    }
}
</script>

<style>

</style>
8.6.2.类目选择组件

编写ProductTree.vue文件

vue 复制代码
<template>
  <el-tree
    :props="defaultProps"
    :load="loadNode"
    lazy
    @node-click="handleNodeClick"
  >
  </el-tree>
</template>

<script>
export default {
    data() {
      return {
        data: [],
        defaultProps: {
          children: 'children',
          label: 'name'
        }
      };
    },
    mounted(){
      
    },
    methods: {
      handleNodeClick(data) {
        this.$emit("onTree",data)
      },
      loadNode(node, resolve){
        // 第一层数据
        if(node.level ===0){
          this.$api.selectItemCategoryByParentId().then(res =>{
            if(res.data.status === 200){
              return resolve(res.data.result)
            }else{
              return resolve([])
            }
          })
        }
        // 后续展开的数据
        if(node.level >= 1){
          this.$api.selectItemCategoryByParentId({
            id:node.data.cid
          }).then(res =>{
            if(res.data.status === 200){
              return resolve(res.data.result)
            }else{
              return resolve([])
            }
          }).catch(error =>{
            return resolve([])
          })
        }
      }
    }
}
</script>

<style>

</style>
8.6.3.富文本编辑器组件

编写ProductWangEditor.vue文件

vue 复制代码
<template>
  <div ref="editorWang"
    style="text-align: left;"
  >
  </div>
</template>

<script>
import wangEditor from 'wangeditor'
export default {
    data(){
        return {
            editor:null,    // editor对象
            editorData:''   // 承载编辑器数据
        }
    },
    props:{
        currentEditorData:{
            type:String,
            default:""
        }
    },
    watch:{
        currentEditorData(newVal, oldVal){
            this.editor.txt.html(newVal);
        }
    },
    mounted(){
        this.editor = new wangEditor(this.$refs.editorWang);
        // 配置 onchange 回调函数,将数据同步到vue中
        this.editor.config.onchange = (newHtml) =>{
            this.editorData = newHtml;
            this.$emit("onEditor",this.editorData);
        }
        // 自定义菜单配置
        this.editor.config.menus = [
            'head',         // 标题
            'bold',         // 粗体
            'fontSize',     // 字号
            'fontName',     // 字体
            'italic',       // 斜体
            'underline',    // 下划线
            'strikeThrough',// 删除线
            'foreColor',    // 文字颜色
            'backColor',    // 背景颜色
            'link',         // 插入链接
            'list',         // 列表
            'justify',      // 对齐方式
            'quote',        // 引用
            'emoticon',     // 标签
            'image',        // 插入图片
            'table',        // 表格
            'code',         // 插入代码
            'undo',         // 撤销
            'redo',         // 重复
        ];
        

        this.editor.create();   // 创建编辑器
    },
    beforeDestroy(){
        // 调用销毁API对当前编辑器实例进行销毁
        this.editor.destroy();
        this.editor = null;
    }

}
</script>

<style>

</style>

运行效果

8.7.编辑商品窗口

ProductPage目录下新建ProductEditor.vue文件

vue 复制代码
<template>
    <el-dialog  
        title="编辑产品"
        :visible.sync="diaologEditorVisible"
        width="60%"
        :before-close="handleClose"
    >
        <el-form 
            label-width="80px"
            :model="editorForm"
            ref="editorForm"    
        >
            <el-form-item label="商品类目:">
                <span class="location tree">{{ treeData.name }}</span>
                <el-button type="primary" class="location" @click="dialogCategoryHandle">类目选择</el-button>
                <el-dialog
                    width="50%"
                    append-to-body
                    title="类目选择"
                    :visible.sync="dialogCategoryVisible"
                >
                    <product-tree @onTree="getTreeData"></product-tree>
                    <span slot="footer" class="dialog-footer">
                        <el-button type="primary" @click="dialogCategoryClose">确认</el-button>
                    </span>
                </el-dialog>
            </el-form-item>
            <el-form-item label="商品名称:">
                <el-input v-model="editorForm.name"></el-input>
            </el-form-item>
            <el-form-item label="商品卖点:">
                <el-input v-model="editorForm.sellPoint"></el-input>
            </el-form-item>
            <el-form-item label="商品价格:">
                <el-input v-model="editorForm.price"></el-input>
            </el-form-item>
            <el-form-item label="商品数量:">
                <el-input v-model="editorForm.num"></el-input>
            </el-form-item>
            <el-form-item label="商品图片:">
                <img class="upload-img" :src="uploadData.url" alt="">
                <el-button type="primary" class="location" 
                    @click="dialogUploadHandle">上传图片</el-button>
                
                <el-dialog
                    width="50%"
                    append-to-body
                    title="图片上传"
                    :visible.sync="dialogUploadVisible"
                >
                    <priduct-upload @onUpload="getOnUpload"></priduct-upload>
                    <span slot="footer" class="dialog-footer">
                        <el-button type="primary" @click="dialogUploadClose">确认</el-button>
                    </span>
                </el-dialog>
                
            </el-form-item>
            <el-form-item label="商品描述:">
                <ProductWangEditor @onEditor="getEditor" :currentEditorData="editorData"></ProductWangEditor>
            </el-form-item>
        </el-form>
        <span slot="footer" class="dialog-footer">
            <el-button @click="editorFormClose">取消</el-button>
            <el-button type="primary" @click="editorProductHandle">确认</el-button>
        </span>
    </el-dialog>
</template>

<script>
import PriductUpload from './PriductUpload.vue';
import ProductTree from './ProductTree.vue';
import ProductWangEditor from './ProductWangEditor.vue';
export default{
    components:{ProductTree,PriductUpload,ProductWangEditor},
    data(){
        return {
            diaologEditorVisible : false,
            dialogCategoryVisible:false,
            dialogUploadVisible:false,
            editorForm:{
                name:"",
                sellPoint:"",
                price:"",
                num:"",
            },
            treeData:{
                cid:"",
                name:""
            },    // 类目选择    
            uploadData:{
                url:""
            },  // 存储图片
            editorData:"",
            currentData:{}
        }
    },
    mounted(){
        this.$bus.$on("onEditorEvent",row =>{
            this.diaologEditorVisible = true;
            this.currentData = row;
            // 获取更新数据
            this.$api.preUpdateItem({id:row.id}).then((res)=>{
                // 赋值
                this.treeData.name = res.data.result[0].cid;
                this.treeData.cid = res.data.result[0].cid;
                this.editorForm.name = res.data.result[0].title
                this.editorForm.sellPoint = res.data.result[0].sellPoint
                this.editorForm.price = res.data.result[0].price
                this.editorForm.num = res.data.result[0].num
                this.uploadData.url = res.data.result[0].image
                this.editorData = res.data.result[0].descs
            })
        })
        
    },
    methods:{
        handleClose(done){
            this.$confirm("确认关闭")
                .then(_ =>{
                    done();
                })
                .catch(_ =>{});
        },
        editorFormClose(){
            this.diaologEditorVisible = false
        },
        dialogCategoryHandle(){
            this.dialogCategoryVisible = true
        },
        dialogCategoryClose(){
            this.dialogCategoryVisible = false
        },
        dialogUploadHandle(){
            this.dialogUploadVisible = true
        },
        dialogUploadClose(){
            this.dialogUploadVisible = false
        },
        // 读取类目选择数据
        getTreeData(data){
            // console.log(data);
            this.treeData = data;
        },
        // 读取图片地址
        getOnUpload(data){
            if(data.url){
                data.url = data.url.replace("upload","http://localhost:3000")
            }
            this.uploadData = data;
        },
        //接收富文本数据
        getEditor(data){
            this.editorData = data;
        },
        // 编辑产品
        editorProductHandle(){
            this.$api.updateTbItem({
                id:this.currentData.id,
                cid:this.treeData.cid,
                title:this.editorForm.name,
                sellPoint:this.editorForm.sellPoint,
                price:this.editorForm.price,
                num:this.editorForm.num,
                desc:this.editorData,
                image:this.uploadData.url
            }).then(res =>{
                if(res.data.status === 200){
                    this.diaologEditorVisible = false;
                    this.$bus.$emit("refresh",true)
                }
            }).catch(error=>{
                console.log(error);
            })
        }
    }
}
</script>

<style scoped>
.location{
    float: left;
}
.tree{
    margin-right: 20px;
}
.upload-img{
    width: 300px;
    float: left;
    margin-right: 20px;
}
</style>

修改index.vue文件,增加使用修改商品组件

vue 复制代码
<template>
  <div class="product">
    <product-header></product-header>
    <product-list/>
    <product-pagination/>
    <product-add></product-add>
    <ProductEditor/>
  </div>
</template>

<script>
import ProductHeader from './ProductHeader.vue'
import ProductList from './ProductList'
import ProductPagination from './ProductPagination.vue'
import ProductAdd from './ProductAdd'
import ProductEditor from './ProductEditor.vue'
export default {
  components:{
    ProductList,ProductPagination,ProductHeader,ProductAdd,ProductEditor
  }
}
</script>

<style>
.product{
  width: 1200px;
  margin: 0 auto;
  margin-top: 20px;
}
</style>

运行效果

报错处理

1.找不到element模块

报错截图

处理方法

修改element.js文件,注释掉lang和locale的引用

javascript 复制代码
import Vue from 'vue'
import element from 'element-ui'
//导入组件相关样式
import 'element-ui/lib/theme-chalk/index.css'
// import lang from 'element-ui/lib/locale/lang/'
// import locale from 'element-ui/lib/locale'

// locale.use(lang)

Vue.use(element)

2.找不到less-loader模块

报错截图

处理方法

使用npm install less-loader less --save-dev安装模块

相关推荐
江湖有缘1 分钟前
Docker部署music-tag-web音乐标签编辑器
前端·docker·编辑器
hzb666662 分钟前
unictf2026
开发语言·javascript·安全·web安全·php
恋猫de小郭1 小时前
Flutter Zero 是什么?它的出现有什么意义?为什么你需要了解下?
android·前端·flutter
崔庆才丨静觅8 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60618 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了9 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅9 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅9 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅9 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment9 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端