【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文件我改了,如上图所示

数据库准备

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文件,增加对应路由跳转配置

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文件,编写拦截代码

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参数

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

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文件

<template>
  <div>
    登录注册
  </div>
</template>

<script>
export default {
}
</script>

<style>
</style>

运行效果
在这里插入图片描述

3.编写登录注册页

编写LoginView.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,实现服务器配置

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,配置数据库链接配置

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,编写服务器数据操作逻辑

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\""

{
  "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,实现请求跳转

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

const base = {
    baseUrl:"http://localhost:3000",
    register:"/api/register",
    login:"/api/login"
}
export default base;

修改router/permission.js

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文件

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

修改store/index.js文件

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文件

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文件

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

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

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代码,添加实际登录注册请求逻辑

<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,编写导航栏组件

<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文件,引入使用导航栏

<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文件

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文件,导入使用公共样式

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的路径

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文件,编写左导航栏组件

<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文件,导入使用左导航栏组件

<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文件,引入使用轮播插件

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文件,实现轮播组件

<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文件,引入轮播组件

<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接收

<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文件,改为使用通用组件

<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文件

<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组件

<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,编写详情页信息

<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

<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文件,增加详情页路由

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文件,增加具体跳转路由信息

<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文件实现购买数量组件

<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文件实现产品类型选择组件

<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文件实现产品类型选择组件

<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文件,作为商品管理页主入口

<template>
  <div class="product">
      商品管理
  </div>
</template>

<script>
export default {
}
</script>

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

修改product的路由配置

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

8.1.服务器数据接口

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

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文件,增加静态文件位置

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文件中增加对应信息

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

	/**
     * 商品列表
     */
    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,方便组件之间的数据传递

import Vue from "vue"

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

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

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文件,实现商品列表展示

<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文件,增加使用列表组件

<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文件

<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文件,增加使用搜索框组件

<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文件

<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文件,增加使用分页组件

<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文件

<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文件,增加使用添加商品组件

<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文件

<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文件

<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文件

<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文件

<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文件,增加使用修改商品组件

<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的引用

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安装模块

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/924248.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

ALSA(4) --- CPU DAI实践

CPU_DAI实践 物理拓扑图 上图可知&#xff0c;从dma过来数据&#xff0c;会保存在DAI的一个FIFO队列中&#xff0c;数据是并行过来的各个通道数据&#xff0c;经过shift移位寄存器&#xff0c;再经过P2S并行转串行&#xff0c;再经过DAVC音量控制输出到GPIO端口 音频数据接口…

【开篇】.NET开源 ORM 框架 SqlSugar 系列

01. 前言 ☘️ 1.1 什么是ORM? 对象-关系映射&#xff08;Object-Relational Mapping&#xff0c;简称ORM&#xff09;&#xff0c;面向对象的开发方法是当今企业级应用开发环境中的主流开发方法&#xff0c;关系数据库是企业级应用环境中永久存放数据的主流数据存储系统。对…

EtherCAT Coe对象创建与通信

目录 前言使用SSC工具生成XML填充读写函数测试 前言 EtherCAT协议栈生成参考https://blog.csdn.net/qq_42039294/article/details/144061669 本文默认大家有EtherCAT基础的移植经验 使用SSC工具生成XML 首先确保COE是开启的 打开表格&#xff0c;编辑内容如下 更多的数据类…

Axure农业农村数据可视化大屏模板分享

在当今信息技术飞速发展的时代&#xff0c;数据可视化已成为各行各业提升管理效率、优化决策过程的重要手段。Axure作为一款强大的原型设计工具&#xff0c;凭借其高度的自定义能力和丰富的交互设计功能&#xff0c;在农业农村数据可视化领域展现出强大的潜力。本文将详细介绍A…

conda、pip同时安装包引起混乱问题剖析

一句话总结&#xff1a; 安装版本不一致时会有两个.dist-info文件夹&#xff08;举例&#xff1a;scapy-2.6.1.dist-info和scapy-2.4.3.dist-info&#xff09;&#xff0c;conda list和pip list依靠这两个文件夹进行包的识别&#xff08;疑似pip list识别老版本&#xff0c;co…

vue实现滚动条滑动到底部分页调取后端接口加载数据

一、案例效果 二、前提条件 接口返回数据 三、案例代码 子组件 const $emit defineEmits([cloneItem, updateList]);const props defineProps({rightList: {type: Array,},chartTableData: {type: Array as () > ChartListType[],},deleteChartInfo: {type: Object,}…

redis 底层数据结构

概述 Redis 6 和 Redis 7 之间对比&#xff1a; Redis6 和 Redis7 最大的区别就在于 Redis7 已经用 listpack 替代了 ziplist. 以下是基于 Redis 7基础分析。 RedisObject Redis是⼀个<k,v>型的数据库&#xff0c;其中key通常都是string类型的字符串对象&#xff0c;⽽…

arm rk3588 onnx转rknn

一、环境部署&#xff1a; https://github.com/airockchip/rknn_model_zoo/tree/main/examples/yolo11 从该网址下载yolo11的模型。支持80种类型检测 二、下载模型 进入examples/yolo11/model文件夹&#xff0c;执行 ./download_model.sh 如图&#xff1a; 三、模型转换…

Flutter 3.24.5安装配置——2024年11月26日

目录 1️⃣前置安装使用环境配置步骤安装Flutter SDK安装Android SDK修改文件默认安装位置&#xff08;.gradle, AVD&#xff09;开始项目 2️⃣执行结果&#x1fab2;Bug找不到**.jar文件 &#x1f517;参考链接 1️⃣前置安装 使用环境 Windows 11IDEA 2024.2.3Flutter 3.2…

Pytest-Bdd-Playwright 系列教程(13):钩子(hooks)

Pytest-Bdd-Playwright 系列教程&#xff08;13&#xff09;&#xff1a;钩子&#xff08;hooks&#xff09; 前言一、什么是钩子&#xff1f;二、Pytest-Bdd 提供的钩子一览三、钩子用法详解1. pytest_bdd_before_scenario2. pytest_bdd_after_scenario3. pytest_bdd_before_s…

23种设计模式-生成器(Builder)设计模式

文章目录 一.什么是生成器设计模式&#xff1f;二.生成器模式的特点三.生成器模式的结构四.生成器模式的优缺点五.生成器模式的 C 实现六.生成器模式的 Java 实现七.代码解析八. 总结 类图&#xff1a; 生成器设计模式类图 一.什么是生成器设计模式&#xff1f; 生成器模式&am…

HCIP——堆叠技术实验配置

目录 一、堆叠的理论知识 二、堆叠技术实验配置 三、总结 一、堆叠的理论知识 1.1堆叠概述&#xff1a; 是指将两台交换机通过堆叠线缆连接在一起&#xff0c;从逻辑上变成一台交换设备&#xff0c;作为一个整体参与数据的转发。 1.2堆叠的基本概念 堆叠系统中所有的单台…

Python - 函数(四)

函数&#xff1a;在编写程序的过程中&#xff0c;有某一功能代码块出现多次&#xff0c; 但是为了提高编写的效率以及代码的重用&#xff0c;所以把具有独立功能的代码块组织为一个小模块&#xff0c;这就是函数 ‌Python中的函数‌是一组被命名的可执行代码&#xff0c;用于完…

豆包MarsCode算法题:三数之和问题

问题描述 思路分析 1. 排序数组 目的: 将数组 arr 按升序排序&#xff0c;这样可以方便地使用双指针找到满足条件的三元组&#xff0c;同时避免重复的三元组被重复计算。优势: 数组有序后&#xff0c;处理两个数和 target - arr[i] 的问题可以通过双指针快速找到所有可能的组…

使用guzzlehttp异步多进程实现爬虫业务

Python和PHP核心技术共享平台 背景 小哥近来在通过动态代理池爬取一些公司需要的大文件pdf规格书的处理。遇到的难点&#xff0c;如何保证服务器CPU、连接数等正常情况下&#xff0c;多进程、异步快速处理这些业务并且保证准确。下面小哥就给看官唠嗑一下&#xff0c;我使用gu…

Chrome和edge浏览器如何为任何网站强制暗模式

前言 因为我的编辑器是黑色&#xff0c;可能是看的时间长了比较喜欢这种颜色了&#xff0c;感觉白色有些刺眼。尤其是看文章时&#xff0c;两边的空白纯白色&#xff0c;所以强迫症搜素设置了谷歌浏览器和edge如何设置成黑色。 Chrome和edge浏览器如何为任何网站强制暗模式 前…

STM32-- keil使用 -设备选择

keil-arm 在project--》manager--》pack installer&#xff0c;更新芯片包&#xff0c; 有些这里不全面&#xff0c;可以在官网下载包进行安装。 比如stm8系列在这里是没有的&#xff0c;因为他的内核是哈弗架构。还有51单片机要在keil c51里面找 keil5中找不到或没有对应的…

K8s内存溢出问题剖析:排查与解决方案

文章目录 一、背景二、排查方案&#xff1a;1. 可能是数据量超出了限制的大小&#xff0c;检查数据目录大小2. 查看是否是内存溢出2.1 排查数据量&#xff08;查看数据目录大小是否超过limit限制&#xff09;2.2 查看pod详情发现问题 三、解决过程 一、背景 做redis压测过程中…

在 Mac ARM 架构(例如 M1 或 M2 芯片)上安装 Node.js

文章目录 方法一&#xff1a;使用 Homebrew 安装 Node.js方法二&#xff1a;使用 Node Version Manager (NVM) 安装 Node.js方法三&#xff1a;从 Node.js 官方网站下载安装包注意事项 在 Mac ARM 架构&#xff08;例如 M1 或 M2 芯片&#xff09;上安装 Node.js 可以通过几种不…

pycharm2021.1汉化失败 “chinese (simplified) language pack“ was not installed

汉化报错&#xff1a;pycharm plugin “chinese (simplified) language pack” was not installed : Invalid filename returned by a server 翻译&#xff1a;pycharm 插件“中文&#xff08;简体&#xff09;语言包”未安装&#xff1a;服务器返回的文件名无效 解决&#…