技术栈
Vue全家桶:Vue + VueRouter + Vuex + Axios +ElementUI
依赖安装
网络请求:npm install --save axios --no-fund
Element:vue add element
后端相关依赖:npm install --save express cors mysql --no-fund
token:npm install --save jsonwebtoken --no-fund
对象转换:npm install querystring --no-fund
文件上传:npm install --save multer --no-fund
富文本编辑器:npm install wangeditor --save --no-fund
快捷运行方案
npm install -g concurrently --no-fund
npm install -g nodemon --no-fund
项目框架搭建
1.使用vue create xxx
命令创建项目
2.使用VS Code打开项目
端口号修改:修改vue.config.js文件,增加如下代码
添加elementUI
1.终端输入vue add element
添加element
添加完毕后,项目新增element文件
2.运行项目,查看项目是否可正常运行
注:因为版本问题,可能运行时会报错,以及element组件无法根据使用自动追加对应的库,所以这里element.js文件我改了,如上图所示
数据库准备
sql
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for category
-- ----------------------------
DROP TABLE IF EXISTS `category`;
CREATE TABLE `category` (
`id` int(11) NOT NULL,
`cid` int(11) NULL DEFAULT NULL,
`name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`pid` int(11) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of category
-- ----------------------------
INSERT INTO `category` VALUES (1, 1001, '家用电器', 1);
INSERT INTO `category` VALUES (2, 1002, '手机/运营商/数码', 1);
INSERT INTO `category` VALUES (3, 1003, '电脑/办公', 1);
INSERT INTO `category` VALUES (4, 1004, '家具/家居', 1);
INSERT INTO `category` VALUES (5, 10001, '电视', 1001);
INSERT INTO `category` VALUES (6, 10001, '手机通讯', 1002);
INSERT INTO `category` VALUES (7, 10001, '电脑整机', 1003);
INSERT INTO `category` VALUES (8, 10001, '厨具', 1004);
INSERT INTO `category` VALUES (9, 10002, '空调', 1001);
INSERT INTO `category` VALUES (10, 10002, '运营商', 1002);
INSERT INTO `category` VALUES (11, 10002, '电脑配件', 1003);
INSERT INTO `category` VALUES (12, 10002, '家纺', 1004);
INSERT INTO `category` VALUES (13, 10003, '洗衣机', 1001);
INSERT INTO `category` VALUES (14, 10003, '摄影', 1002);
INSERT INTO `category` VALUES (15, 10003, '外设产品', 1003);
INSERT INTO `category` VALUES (16, 10003, '灯具', 1004);
INSERT INTO `category` VALUES (17, 10004, '冰箱', 1001);
INSERT INTO `category` VALUES (18, 10004, '摄像', 1002);
INSERT INTO `category` VALUES (19, 10004, '游戏设备', 1003);
INSERT INTO `category` VALUES (20, 10004, '家具', 1004);
INSERT INTO `category` VALUES (21, 100001, '超薄电视', 10001);
INSERT INTO `category` VALUES (22, 100002, '全面屏电视', 10001);
-- ----------------------------
-- Table structure for project
-- ----------------------------
DROP TABLE IF EXISTS `project`;
CREATE TABLE `project` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`title` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '标题',
`image` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '图片路径',
`sellPoint` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`price` int(10) NULL DEFAULT NULL COMMENT '价格',
`cid` int(11) NULL DEFAULT NULL,
`num` int(11) NULL DEFAULT NULL,
`barcode` tinyint(4) NULL DEFAULT NULL,
`status` tinyint(4) NULL DEFAULT NULL,
`created` datetime(0) NULL DEFAULT NULL,
`updated` datetime(0) NULL DEFAULT NULL,
`descs` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 31 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of project
-- ----------------------------
INSERT INTO `project` VALUES (1, '隐形的守护', NULL, '互动游戏', 128, 1, 4, NULL, 1, NULL, NULL, '这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!');
INSERT INTO `project` VALUES (2, '饥饿游戏', NULL, '影视作品', 58, 1, 5, NULL, 1, NULL, NULL, NULL);
INSERT INTO `project` VALUES (3, '家族4', NULL, 'slg游戏', 18, NULL, 123, NULL, 1, NULL, NULL, '这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!');
INSERT INTO `project` VALUES (4, '家族1', NULL, 'slg游戏', 12, NULL, 123, NULL, 1, NULL, NULL, '这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!');
INSERT INTO `project` VALUES (5, '家族2', NULL, 'slg游戏', 14, NULL, 22, NULL, 1, NULL, NULL, '这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!');
INSERT INTO `project` VALUES (6, '家族3', NULL, 'slg游戏', 18, NULL, 42, NULL, 1, NULL, NULL, '这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!这是描述!!!!');
INSERT INTO `project` VALUES (7, '三国群英传1', NULL, 'slg游戏', 12, NULL, 421, NULL, 1, NULL, NULL, '光荣只会卖情怀');
INSERT INTO `project` VALUES (9, '三国群英传3', NULL, 'slg游戏', 12, NULL, 421, NULL, 1, NULL, NULL, '光荣只会卖情怀');
INSERT INTO `project` VALUES (10, '三国群英传4', NULL, 'slg游戏', 12, NULL, 421, NULL, 1, NULL, NULL, '光荣只会卖情怀');
INSERT INTO `project` VALUES (11, '三国群英传5', NULL, 'slg游戏', 12, NULL, 421, NULL, 1, NULL, NULL, '光荣只会卖情怀');
INSERT INTO `project` VALUES (12, '三国群英传6', NULL, 'slg游戏', 12, NULL, 421, NULL, 1, NULL, NULL, '光荣只会卖情怀');
INSERT INTO `project` VALUES (13, '三国群英传7', NULL, 'slg游戏', 12, NULL, 421, NULL, 1, NULL, NULL, '光荣只会卖情怀');
INSERT INTO `project` VALUES (14, '三国群英传8', NULL, 'slg游戏', 12, NULL, 421, NULL, 1, NULL, NULL, '毫无亮点,走网游路线了');
INSERT INTO `project` VALUES (15, '三国志1', NULL, 'slg游戏', 32, NULL, 32, NULL, 1, NULL, NULL, '光荣只会卖情怀');
INSERT INTO `project` VALUES (16, '三国志2', NULL, 'slg游戏', 32, NULL, 32, NULL, 1, NULL, NULL, '光荣只会卖情怀');
INSERT INTO `project` VALUES (17, '三国志3', NULL, 'slg游戏', 32, NULL, 32, NULL, 1, NULL, NULL, '光荣只会卖情怀');
INSERT INTO `project` VALUES (18, '三国志4', NULL, 'slg游戏', 32, NULL, 32, NULL, 1, NULL, NULL, '光荣只会卖情怀');
INSERT INTO `project` VALUES (19, '三国志5', NULL, 'slg游戏', 32, NULL, 32, NULL, 1, NULL, NULL, '光荣只会卖情怀');
INSERT INTO `project` VALUES (20, '三国志6', NULL, 'slg游戏', 32, NULL, 32, NULL, 1, NULL, NULL, '光荣只会卖情怀');
INSERT INTO `project` VALUES (21, '三国志7', NULL, 'slg游戏', 32, NULL, 32, NULL, 1, NULL, NULL, '光荣只会卖情怀');
INSERT INTO `project` VALUES (22, '三国志8', NULL, 'slg游戏', 32, NULL, 32, NULL, 1, NULL, NULL, '光荣只会卖情怀');
INSERT INTO `project` VALUES (23, '三国志9', NULL, 'slg游戏', 32, NULL, 32, NULL, 1, NULL, NULL, '光荣只会卖情怀');
INSERT INTO `project` VALUES (24, '三国志10', NULL, 'slg游戏', 32, NULL, 32, NULL, 1, NULL, NULL, '光荣只会卖情怀');
INSERT INTO `project` VALUES (25, '三国志11', NULL, 'slg游戏', 32, NULL, 32, NULL, 1, NULL, NULL, '光荣只会卖情怀');
INSERT INTO `project` VALUES (26, '三国志12', NULL, 'slg游戏', 32, NULL, 32, NULL, 1, NULL, NULL, '光荣只会卖情怀');
INSERT INTO `project` VALUES (27, '三国志13', NULL, 'slg游戏', 32, NULL, 32, NULL, 1, NULL, NULL, '光荣只会卖情怀');
INSERT INTO `project` VALUES (28, '三国无双1', 'http://localhost:3000\\1732370097961-1.jpg', '任务强度高', 58, 10002, 500, NULL, 1, NULL, NULL, '<p>方式的监护人突然<strike>让我如很高的发挥</strike><font color=\"#f9963b\">地方规划人都热</font>帖该问题公司的归属感和无人</p>');
INSERT INTO `project` VALUES (30, '三国无双4', 'http://localhost:3000\\1732702595899-1.jpg', '情怀', 58, 10003, 12, NULL, 1, NULL, NULL, '<p>是的话<font color=\"#c24f4a\">标点符号的分</font>工</p>');
-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`password` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`email` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 11 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES (7, 'cy', '123456', '123456');
INSERT INTO `user` VALUES (8, 'admin', '123456', 'admin@123.com');
INSERT INTO `user` VALUES (9, 'admin1', '123456', 'admin@123.com');
INSERT INTO `user` VALUES (10, 'admin2', '123456', 'admin@123.com');
SET FOREIGN_KEY_CHECKS = 1;
项目开发
1.准备页签栏
在src/views
文件夹内创建main/ADCategory.vue
、main/ParamsView.vue
、main/ProductView.vue
、LayoutView.vue
文件
修改router/index.js
文件,增加对应路由跳转配置
javascript
import Vue from 'vue'
import VueRouter from 'vue-router'
import HomeView from '../views/HomeView.vue'
import Layout from '../views/LayoutView.vue'
Vue.use(VueRouter)
const routes = [
{
path: '/',
name: 'Layout',
component: Layout,
children:[
{
path: '',
name: 'home',
component: HomeView
},
{
path:"product",
name:"Product",
component:() => import("../views/main/ProductView.vue")
},
{
path:"params",
name:"Params",
component:() => import("../views/main/ParamsView.vue")
},
{
path:"ad",
name:"ADCategory",
component:() => import("../views/main/ADCategory.vue")
}
]
},
]
const router = new VueRouter({
routes,
// 去除路径上的#号
mode:"history"
})
export default router
2.登录拦截
在router
文件夹下新建permission.js文件,编写拦截代码
javascript
import router from "./index"
import store from "../store"
// 访问路径前判断权限
router.beforeEach((to,from,next) =>{
if(to.meta.isLogin){
// 暂未实现token
let token = false;
console.log(token);
if(token){
next();
}else{
next({
name:"Login"
})
}
}else{
next();
}
})
修改router/index.js
文件,给需要登录才能访问的路由设置isLogin参数
javascript
import Vue from 'vue'
import VueRouter from 'vue-router'
import HomeView from '../views/HomeView.vue'
import Layout from '../views/LayoutView.vue'
Vue.use(VueRouter)
const routes = [
{
path: '/',
name: 'Layout',
component: Layout,
children:[
{
path: '',
name: 'home',
component: HomeView,
meta:{
isLogin:true
}
},
{
path:"product",
name:"Product",
component:() => import("../views/main/ProductView.vue"),
meta:{
isLogin:true
}
},
{
path:"params",
name:"Params",
component:() => import("../views/main/ParamsView.vue"),
meta:{
isLogin:true
}
},
{
path:"ad",
name:"ADCategory",
component:() => import("../views/main/ADCategory.vue"),
meta:{
isLogin:true
}
}
]
},
{
path:'/login',
name:'Login',
component:() => import("../views/LoginView.vue")
}
]
const router = new VueRouter({
routes,
// 去除路径上的#号
mode:"history"
})
export default router
修改main.js
文件,导入permission.js
javascript
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import './plugins/element.js'
// 导入permission.js文件
import './router/permission'
Vue.config.productionTip = false
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
编写LoginView.vue
文件
vue
<template>
<div>
登录注册
</div>
</template>
<script>
export default {
}
</script>
<style>
</style>
运行效果
3.编写登录注册页
编写LoginView.vue
文件
vue
<template>
<div class="login">
<el-card class="box-card">
<div slot="header" class="clearfix">
<span>Ego商城后台管理系统</span>
</div>
<div>
<el-tabs v-model="currentIndex" stretch type="border-card" @tab-click="handleTabClick">
<el-tab-pane label="登录" name="login">
<el-form :model="loginForm" status-icon ref="loginForm" :rules="rules">
<el-form-item label="用户名:" label-width="80px" prop="username">
<el-input type="text" v-model="loginForm.username" />
</el-form-item>
<el-form-item label="密码:" label-width="80px" prop="password">
<el-input type="password" v-model="loginForm.password" />
</el-form-item>
<el-form-item >
<el-button type="primary" @click="submitForm('loginForm')">登录</el-button>
</el-form-item>
</el-form>
</el-tab-pane>
<el-tab-pane label="注册" name="register">
<el-form :model="registerForm" status-icon ref="registerForm" :rules="rules">
<el-form-item label="用户名:" label-width="80px" prop="username">
<el-input type="text" v-model="registerForm.username" />
</el-form-item>
<el-form-item label="邮箱:" label-width="80px" prop="email">
<el-input type="text" v-model="registerForm.email" />
</el-form-item>
<el-form-item label="密码:" label-width="80px" prop="password">
<el-input type="password" v-model="registerForm.password" />
</el-form-item>
<el-form-item label="确认密码:" label-width="80px" prop="configurePassword">
<el-input type="password" v-model="registerForm.configurePassword" />
</el-form-item>
<el-form-item >
<el-button type="primary" @click="submitForm('registerForm')">注册</el-button>
</el-form-item>
</el-form>
</el-tab-pane>
</el-tabs>
</div>
</el-card>
</div>
</template>
<script>
export default {
data(){
// 验证规则
var validateUsername = (rule,value,callback) =>{
if(value === ""){
callback(new Error("请输入用户名"));
}else if(value.length < 6){
callback(new Error("长度不足6位"));
}else{
callback();
}
}
var validatePassword = (rule,value,callback) =>{
if(value === ""){
callback(new Error("请输入密码"));
}else{
callback();
}
}
var validateConfigurePassword = (rule,value,callback) =>{
if(value === ""){
callback(new Error("请输入密码"));
}else if(value !== this.registerForm.password){
callback(new Error("两次密码不一致"));
}
else{
callback();
}
}
return{
currentIndex:"login",
loginForm:{
username:"",
password:""
},
registerForm:{
username:"",
password:"",
email:"",
configurePassword:""
},
activeTab:"login",
rules:{
username:[
{
validator:validateUsername,
trigger:'blur'
}
],
password:[
{
validator:validatePassword,
trigger:'blur'
}
],
configurePassword:[
{
validator:validateConfigurePassword,
trigger:'blur'
}
]
}
}
},
methods:{
submitForm( formName ){
this.$refs[formName].validate((valid) =>{
if(valid){
// 根据当前所在tabs页签的name的值去执行对应页签的逻辑
if(this.activeTab === "login"){
// 提交注册表单逻辑
console.log(this.loginForm);
}else if(this.activeTab === "register"){
// 提交注册表单逻辑
console.log(this.registerForm);
}else{
return;
}
}
})
},
// 获取当前所在tabs页签的name
handleTabClick(tab){
this.activeTab = tab.name;
}
}
}
</script>
<style scoped lang="less">
.login{
width:1200px;
margin: 0 auto;
.box-card{
width: 500px;
margin: 100px auto;
}
}
</style>
运行效果
4.nodeJS后台实现
在项目目录下新建server
目录(与src目录同级),并新建index.js
、router.js
、config.js
文件
编写index.js
,实现服务器配置
javascript
const express = require("express");
const app = express();
const cors = require("cors");
const bodyParser = require("body-parser");
const router = require("./router")
app.use(cors());
app.use(bodyParser.urlencoded({
extended:true
}))
app.use("/api",router);
// 监听端口号
app.listen(3000,() =>{
console.log(3000)
})
编写config.js
,配置数据库链接配置
javascript
const mysql = require("mysql");
const client = mysql.createConnection({
host:"localhost",
user:"root",
password:"123456",
database:"ego_shop_one"
})
const sqlClient = (sql,arr,callback) =>{
client.query(sql,arr,(error,result)=>{
if(error){
console.log(error);
return;
}
callback(result);
})
}
module.exports = sqlClient;
编写router.js
,编写服务器数据操作逻辑
javascript
const express = require("express");
const router = express.Router();
const sqlClient = require("./config")
const jwt = require("jsonwebtoken")
/**
* 注册
*/
router.post("/register",(req,res) =>{
const {username,password,email} = req.body;
const sql = "INSERT INTO user VALUES(null,?,?,?)";
const arr = [username,password,email];
sqlClient(sql,arr,result=>{
if(result.affectedRows > 0){
res.send({
status:200,
msg:"注册成功"
})
}else{
res.send({
status:401,
msg:"注册失败"
})
}
})
})
/**
* 登录
*/
router.post("/login",(req,res) =>{
const {username,password} = req.body;
const sql = "SELECT * FROM user WHERE username = ? and password = ?";
const arr = [username,password];
sqlClient(sql,arr,result =>{
if(result.length > 0){
let token = jwt.sign({
username,
id:result[0].id
},'soomekeys')
res.send({
status:200,
token,
username
})
}else{
res.send({
status:401,
msg:"登录失败"
})
}
})
})
module.exports = router;
修改package.json
文件,增加快捷运行脚本"dev": "concurrently \"npm run serve\" \"nodemon server/index.js\""
json
{
"name": "my_shopyigou",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint",
"dev": "concurrently \"npm run serve\" \"nodemon server/index.js\""
},
......
}
运行npm run dev
,可同步运行服务器和客户端;使用postman测试后端接口
5.前后端登录注册对接
在src目录下新建api目录,新建api/base.js
、api/index.js
文件
编写api/index.js
,实现请求跳转
javascript
import axios from "../utils/request"
import base from "./base"
const api = {
/**
* 注册
*/
register(params){
return axios.post(base.baseUrl + base.register,params)
},
/**
* 登录
*/
login(params){
return axios.post(base.baseUrl + base.login,params)
}
}
export default api;
编写api/base.js
javascript
const base = {
baseUrl:"http://localhost:3000",
register:"/api/register",
login:"/api/login"
}
export default base;
修改router/permission.js
javascript
import router from "./index"
import store from "../store"
// 访问路径前判断权限
router.beforeEach((to,from,next) =>{
if(to.meta.isLogin){
// 获取token
let token = store.state.login.user.token;
console.log(token);
if(token){
next();
}else{
next({
name:"Login"
})
}
}else{
next();
}
})
新建store/modules/login.js
文件
javascript
export default{
namespaced:true,
state:{
user:{
username:"",
token:""
}
},mutations:{
setUser(state,user){
state.user = user;
}
}
}
修改store/index.js
文件
javascript
import Vue from 'vue'
import Vuex from 'vuex'
import login from './modules/login'
Vue.use(Vuex)
export default new Vuex.Store({
modules: {
login
}
})
在src目录下新建utils目录,新建utils/request.js
、utils/init.js
文件
编写utils/request.js
文件
javascript
import axios from 'axios'
import qs from "querystring"
import store from "../store"
/**
* 处理失败的方法
*/
const errorHandle = (status,info) =>{
switch(status){
case 400:
console.log("语义有误,当前请求无法被服务器理解。");
break;
case 401:
// token:令牌
console.log("服务器认证失败!");
break;
case 403:
console.log("服务器已经理解请求,但是拒绝执行它!");
break;
case 404:
console.log("请检查网络请求地址!");
break;
case 500:
console.log("服务器遇到了一个未曾预料的状态,导致它无法完成对请求的处理");
break;
case 502:
console.log("作为网关或者代理工作的服务器尝试执行请求时,从上游服务器接收到的请求异常");
break;
default:
console.log(info);
break;
}
}
/**
* 创建axios实例对象
*/
const instance = axios.create({
// 公共配置
timeout:5000
})
/**
* 请求拦截
*/
instance.interceptors.request.use(
(config) => {
if(config.method === "post"){
config.data = qs.stringify(config.data)
}
return config
},
(error) => {
return Promise.reject(error);
}
);
/**
* 响应拦截
*/
instance.interceptors.response.use(
//完成了
(response) => {
return response.status === 200 ? Promise.resolve(response) : Promise.reject(response);
},
(error) => {
const {response} = error;
errorHandle(response.status,response.info);
}
);
export default instance
编写utils/init.js
文件
javascript
import store from "../store"
if(localStorage.getItem("ego")){
store.commit("login/setUser",JSON.parse(localStorage.getItem("ego")))
}
修改main.js
文件,导入使用./utils/init
javascript
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import './plugins/element.js'
import './router/permission'
import './utils/init'
Vue.config.productionTip = false
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
修改LoginView.vue代码,添加实际登录注册请求逻辑
vue
<script>
import { mapMutations } from "vuex";
import api from "../api"
export default {
......
methods:{
...mapMutations("login",["setUser"]),
submitForm( formName ){
this.$refs[formName].validate((valid) =>{
if(valid){
if(this.activeTab === "login"){
// 提交登录表单逻辑
api.login(this.loginForm).then(res =>{
// console.log(res.data);
if(res.data.status === 200){
this.setUser(res.data);
localStorage.setItem("ego",JSON.stringify(res.data));
this.$router.push('/')
}else{
const h = this.$createElement;
this.$notify({
title:"登录失败",
message:h("i","用户名密码错误")
})
}
})
}else if(this.activeTab === "register"){
// 提交注册表单逻辑
// console.log(this.registerForm);
api.register(this.registerForm).then(res =>{
if(res.data.status === 200){
const h = this.$createElement;
this.$notify({
title:"注册成功",
message:h("i","请前往登录页面登录")
});
}else{
const h = this.$createElement;
this.$notify({
title:"注册失败",
message:h("i","请重新注册")
})
}
})
}else{
return;
}
}
})
},
handleTabClick(tab){
this.activeTab = tab.name;
}
}
}
</script>
运行效果
登录后,会自动跳转至首页且本地缓存中增加token
注册
暂未实现重复用户验证
6.导航栏实现
components目录下新建HeaderNav.vue
,编写导航栏组件
vue
<template>
<el-menu :default-active="active" mode="horizontal"
background-color="#545c64"
text-color="#fff"
active-text-color="#ffd04b"
router >
<el-menu-item index="/">首页</el-menu-item>
<el-menu-item index="/product">商品管理</el-menu-item>
<el-menu-item index="/params">规格参数</el-menu-item>
<el-menu-item index="/ad">广告管理</el-menu-item>
<el-menu-item class="user">
<span class="user-name">{{ user.username }}</span>
<el-button @click="logoutHandle">退出</el-button>
</el-menu-item>
</el-menu>
</template>
<script>
import { mapState,mapMutations } from 'vuex'
export default {
data(){
return {
active:"/"
}
},
computed:{
...mapState("login",["user"])
},
methods:{
...mapMutations("login",["setUser"]),
logoutHandle(){
this.setUser({})
localStorage.removeItem('ego')
this.$router.push('/login')
}
}
}
</script>
<style lang="less" scoped>
.user{
float: right !important;
margin-right: 20px !important;
line-height: 60px !important;
.user-name{
color:#fff;
margin-right: 10px;
font-size: 15px;
border: 1px solid #fff;
border-radius: 50%;
width:40px;
height: 40px;
display: inline-block;
line-height: 40px;
overflow: hidden;
}
}
.set-lang{
float: right !important;
}
.el-dropdown-link{
color:#fff;
}
</style>
修改LayoutView.vue
文件,引入使用导航栏
vue
<template>
<div>
<header-nav></header-nav>
<router-view></router-view>
</div>
</template>
<script>
import HeaderNav from "@/components/HeaderNav.vue"
export default {
components:{
HeaderNav
}
}
</script>
<style>
</style>
运行效果
*.公共样式导入
在src/assets目录下新建css目录,编写common.css
文件
css
body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,form,fieldset,label
{
margin: 0;
padding: 0;
}
body{text-align: center;background: #f1f1f1;}
li{list-style: none;}
a{text-decoration: none;}
input,button,img{border: none;}
.active{color: #409eff;}
修改main.js
文件,导入使用公共样式
javascript
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import './plugins/element.js'
import './router/permission'
import './utils/init'
import './assets/css/common.css'
Vue.config.productionTip = false
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
7.首页页面实现
在src/view/main目录下,新建HomePage目录,用于存放所有首页视图页面,将HomeView.vue
移动至该目录下
修改src/router目录下的index.js文件,更新HomeView的路径
javascript
import Vue from 'vue'
import VueRouter from 'vue-router'
import HomeView from '../views/main/HomePage/HomeView.vue'
import Layout from '../views/LayoutView.vue'
......
7.1.左导航栏
在HomePage目录下新建HomeNavView.vue
文件,编写左导航栏组件
vue
<template>
<div class="index-warp">
<div class="index-left">
<div class="index-left-block">
<h2>全部产品</h2>
<div v-for="(product,index) in productList" :key="index">
<h3>{{ product.title }}</h3>
<ul>
<li v-for="(item,index) in product.list" :key="index">
<a :href="item.url">{{ item.name }}</a>
<span class="hot-tag" v-if="item.hot">HOT</span>
</li>
</ul>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
data(){
return{
productList:[
{
title:"手机应用类",
list:[
{
id:1,
name:"baidu",
url:"www.baidu.com",
hot:true
},
{
id:2,
name:"baidu2",
url:"www.baidu.com",
hot:false
},
{
id:3,
name:"baidu3",
url:"www.baidu.com",
hot:false
}
]
},
{
title:"PC产品类",
list:[
{
id:1,
name:"华为",
url:"www.baidu.com",
hot:true
},
{
id:2,
name:"mac",
url:"www.baidu.com",
hot:false
},
{
id:3,
name:"微软",
url:"www.baidu.com",
hot:false
}
]
}
]
}
}
}
</script>
<style scoped lang="less">
.index-wrap{
width: 1200px;
margin:0 auto;
overflow: hidden;
}
.index-left{
float: left;
width: 300px;
text-align: left;
}
.index-right{
float:left;
width:900px;
}
.index-left-block{
margin: 15px;
background: #fff;
box-shadow: 0 0 1px #ddd;
}
.index-left-block .hr{
margin-bottom: 20px;
height: 1px;
width: 100%;
background: #ddd;
}
.index-left-block h2{
background: #4fc08d;
color: #fff;
padding: 10px 15px;
margin-bottom: 20px;
}
.index-left-block h3{
padding: 0 15px 5px 15px;
font-weight: bold;
color: #222;
}
.index-left-block ul{
padding: 10px 15px;
}
.index-left-block li{
padding:5px;
a{
color:#222;
}
}
.index-board-list{
overflow:hidden;
margin-top: 15px;
}
.index-board-item{
float: left;
width: 400px;
background: #fff;
box-shadow: 0 0 1px #ddd;
padding: 20px;
margin-right: 20px;
margin-bottom: 20px;
}
.index-board-item-inner{
min-height: 125px;
padding-left: 120px;
}
/**
.index-board-openproduct .index-board-item-inner{
background: url(../assets/images/1.png) no-repeat;
}
.index-board-logo .index-board-item-inner{
background: url(../assets/images/2.jpeg) no-repeat;
}
.index-board-golife .index-board-item-inner{
background: url(../assets/images/1.png) no-repeat;
}
.index-board-heigh .index-board-item-inner{
background: url(../assets/images/2.jpeg) no-repeat;
}
*/
.index-board-item h2{
font-size: 18px;
font-weight: bold;
color: #000;
margin-bottom: 15px;
}
.line-last{
margin-right: 0;
}
.index-board-button{
margin-top: 20px;
}
.lastest-news{
min-height: 350px;
}
.hot-tag{
background: red;
color: #fff;
}
.new-item{
display: inline-block;
width: 230px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.swiperimg{
width: 100%;
height: 350px;
}
.swiper-size{
margin-block: 15px;
}
.button{
background: #4fc08d;
color:#fff;
display: inline-block;
padding: 10px 20px;
cursor: pointer;
}
</style>
修改HomeView.vue
文件,导入使用左导航栏组件
vue
<template>
<div class="home">
<home-nav></home-nav>
</div>
</template>
<script>
import HomeNav from "./HomeNavView.vue"
export default {
name: 'HomeView',
components: {
HomeNav
}
}
</script>
运行效果
7.2.轮播图
安装轮播图插件:cnpm install swiper@5.x vue-awesome-swiper@3.1.3 --save --no-fund
注:使用新版本可以使用npm,但要下载使用旧版本,需使用cnpm
修改main.js
文件,引入使用轮播插件
javascript
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import './plugins/element.js'
import './router/permission'
import './utils/init'
import './assets/css/common.css'
// 引入轮播图插件
import VueAwesomeSwiper from 'vue-awesome-swiper'
import 'swiper/css/swiper.css'
// 使用VueAwesomeSwiper
Vue.use(VueAwesomeSwiper)
Vue.config.productionTip = false
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
在src/assets目录下新建slideShow目录,存放需要轮播的图片
在HomePage目录下新建HomeSwiper.vue
文件,实现轮播组件
vue
<template>
<div class="swiper-size">
<swiper :options="swiperOption">
<swiper-slide v-for="(imgSrc,index) in bannerImg" :key="index">
<img class="swiperimg" :src="imgSrc" alt=""/>
</swiper-slide>
<!-- 轮播图下方轮播节点 -->
<div class="swiper-pagination" slot="pagination"></div>
<!-- 左右切换按钮 -->
<div class="swiper-button-prev" slot="button-prev"></div>
<div class="swiper-button-next" slot="button-next"></div>
</swiper>
</div>
</template>
<script>
export default {
data(){
return{
swiperOption:{
pagination:{
el:".swiper-pagination",
},
autoplay:true,
navigation:{
nextEl:".swiper-button-next",
prevEl:".swiper-button-prev"
},
loop:true
},
bannerImg:[
require("@/assets/slideShow/1.png"),
require("@/assets/slideShow/2.jpg"),
require("@/assets/slideShow/3.jpg"),
require("@/assets/slideShow/4.jpg"),
]
}
}
}
</script>
<style scoped lang="less">
.swiper-size{
margin-top: 15px;
}
.swiperimg{
width: 100%;
height: 450px;
}
</style>
修改HomeView.vue
文件,引入轮播组件
vue
<template>
<div class="index-wrap">
<div class="index-left">
<home-nav></home-nav>
</div>
<div class="index-right">
<home-swiper></home-swiper>
</div>
</div>
</template>
<script>
import HomeNav from "./HomeNavView.vue"
import HomeSwiper from "./HomeSwiper.vue"
export default {
name: 'HomeView',
components: {
HomeNav,HomeSwiper
}
}
</script>
<style lang="less" scoped>
.index-wrap{
width: 1500px;
margin:0 auto;
overflow: hidden;
}
.index-left{
float: left;
width: 300px;
text-align: left;
}
.index-right{
float:left;
width:1200px;
}
</style>
运行效果
7.2.1.封装组件
在components目录下,新建SwiperView.vue
文件,用做通用轮播图组件
编辑SwiperView.vue
文件,跟HomeSwiper.vue
文件内容差不多,只是图片信息改用props接收
vue
<template>
<div class="swiper-size">
<swiper :options="swiperOption">
<swiper-slide v-for="(imgSrc,index) in bannerSwiperImg" :key="index">
<img class="swiper-img" :src="imgSrc" alt=""/>
</swiper-slide>
<!-- 轮播图下方轮播节点 -->
<div class="swiper-pagination" slot="pagination"></div>
<!-- 左右切换按钮 -->
<div class="swiper-button-prev" slot="button-prev"></div>
<div class="swiper-button-next" slot="button-next"></div>
</swiper>
</div>
</template>
<script>
export default {
data(){
return {
// 轮播图选项
swiperOption:{
pagination:{
el:".swiper-pagination"
},
// 是否自动轮播
autoplay:true,
// 切换组件
navigation:{
nextEl:".swiper-button-next",
prevEl:".swiper-button-prev"
},
// 轮播进度
loop:true
}
}
},
props:{
bannerSwiperImg:{
type:Array,
default(){
// 没有传入数据,则默认使用一下图片
return [
require("@/assets/slideShow/1.png"),
]
}
}
},
}
</script>
<style scoped>
.swiper-size{
margin-top: 15px;
}
.swiper-img{
width: 100%;
height: 450px;
}
</style>
修改HomeView.vue
文件,改为使用通用组件
vue
<template>
<div class="index-wrap">
<div class="index-left">
<home-nav-view />
</div>
<div class="index-right">
<swiper-view :bannerSwiperImg="bannerSwiperImg"/>
</div>
</div>
</template>
<script>
import SwiperView from '@/components/SwiperView.vue'
import HomeNavView from './HomeNavView.vue'
export default {
name: 'HomeView',
data(){
return {
bannerSwiperImg:[
require("@/assets/slideShow/2.jpg"),
require("@/assets/slideShow/1.png"),
require("@/assets/slideShow/4.jpg"),
require("@/assets/slideShow/3.jpg"),
]
}
},
components: {
HomeNavView,
SwiperView
},
}
</script>
<style scoped>
.index-wrap{
width: 1500px;
margin:0 auto;
overflow: hidden;
}
.index-left{
float: left;
width: 300px;
text-align: left;
}
.index-right{
float:left;
width:1200px;
}
</style>
运行效果
7.3.信息列表
在HomePage目录下新建HomeProductList.vue
文件
vue
<template>
<div class="index-board-list">
<div class="index-board-item"
v-for="(item,index) in buyData"
:key="index"
:class="['index-board-' + item.url,{'line-last':index%2 !== 0}]">
<div class="index-board-item-inner">
<h2>{{ item.title }}</h2>
<p>{{ item.desc }}</p>
<div class="index-board-button">
<router-link to="/details" class="button">立即购买</router-link>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
data(){
return{
buyData:[
{
title:"开放产品",
desc:"开放产品描述",
url:"openproduct"
},
{
title:"品牌营销",
desc:"品牌营销描述",
url:"logo"
},
{
title:"电子产品",
desc:"电子产品描述",
url:"golife"
},
{
title:"酒水产品",
desc:"酒水产品描述",
url:"heigh"
},
]
}
}
}
</script>
<style scoped lang="less">
.index-board-list{
overflow:hidden;
margin-top: 15px;
}
.index-board-item{
float: left;
width: 550px;
background: #fff;
box-shadow: 0 0 1px #ddd;
padding: 20px;
margin-right: 20px;
margin-bottom: 20px;
}
.index-board-item-inner{
min-height: 125px;
padding-left: 120px;
}
.index-board-openproduct .index-board-item-inner{
background: url(@/assets/images/1.jpg) no-repeat;
background-size: 120px 120px;
}
.index-board-logo .index-board-item-inner{
background: url(@/assets/images/2.jpeg) no-repeat;
background-size: 120px 120px;
}
.index-board-golife .index-board-item-inner{
background: url(@/assets/images/3.jpg) no-repeat;
background-size: 120px 120px;
}
.index-board-heigh .index-board-item-inner{
background: url(@/assets/images/4.jpg) no-repeat;
background-size: 120px 120px;
}
.index-board-item h2{
font-size: 18px;
font-weight: bold;
color: #000;
margin-bottom: 15px;
}
.line-last{
margin-right: 0;
}
.index-board-button{
margin-top: 20px;
}
.lastest-news{
min-height: 350px;
}
.new-item{
display: inline-block;
width: 230px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.button{
background: #4fc08d;
color:#fff;
display: inline-block;
padding: 10px 20px;
cursor: pointer;
}
</style>
百度找几张图标,放在src/assets/images目录下
修改HomeView.vue
文件,添加使用HomeProductList组件
vue
<template>
<div class="index-wrap">
<div class="index-left">
<home-nav></home-nav>
</div>
<div class="index-right">
<home-swiper></home-swiper>
<home-product-list></home-product-list>
</div>
</div>
</template>
<script>
import HomeNav from "./HomeNavView.vue"
import HomeSwiper from "./HomeSwiper.vue"
import HomeProductList from "./HomeProductList.vue"
export default {
name: 'HomeView',
components: {
HomeNav,HomeSwiper,HomeProductList
}
}
</script>
<style lang="less" scoped>
.index-wrap{
width: 1500px;
margin:0 auto;
overflow: hidden;
}
.index-left{
float: left;
width: 300px;
text-align: left;
}
.index-right{
float:left;
width:1200px;
}
</style>
运行效果
7.4.详情页实现
在src/views/main/HomePage目录下新建sub目录,用于存放子页面文件
新建DetailsView.vue
,编写详情页信息
vue
<template>
<div class="detail-wrap">
<div class="detail-left">
<div class="product-board">
<img src="@/assets/images/1.jpg" alt="">
<ul>
<router-link
tag="li"
active-class="active"
v-for="(item,index) in detailsNav"
:key="index"
:to="'/details/' + item.id">
{{ item.title }}
</router-link>
</ul>
</div>
</div>
<div class="detail-right">
<router-view></router-view>
</div>
</div>
</template>
<script>
export default {
data(){
return {
detailsNav:[
{
title:"开放产品",
id:"openproduct"
},
{
title:"品牌营销",
id:"logo"
},
{
title:"电子产品",
id:"golife"
},
{
title:"酒水产品",
id:"heigh"
},
]
}
}
}
</script>
<style >
.detail-wrap{
width: 1200px;
margin: 0 auto;
overflow: hidden;
padding-top: 20px;
}
.detail-left {
float: left;
width: 200px;
text-align: center;
}
.detail-right{
float: left;
width: 980px;
margin-left: 20px;
}
.product-board{
background: #fff;
padding: 20px 0;
}
.product-board img{
width: 120px;
height: 120px;
}
.product-board ul{
margin-top: 20px;
}
.product-board li{
text-align: left;
padding: 10px 15px;
cursor: pointer;
}
.product-board li.active,
.product-board li:hover{
background: #4fc08d;
color:#fff;
}
.product-board li a{
display: block;
}
/* 下方为右边区域子页面样式 */
.sales-board{
background: #fff;
}
.sales-board-intro h2{
font-size: 20px;
padding: 20px;
}
.sales-board-intro p{
background: #f7fcff;
padding: 10px 20px;
color: #999;
line-height: 1.8;
}
.sales-board-form{
padding: 10px 20px;
font-size: 14px;
}
.sales-board-line{
clear: both;
padding-bottom: 20px
}
.sales-board-line-left{
display: inline-block;
margin-right: 10px;
}
.sales-board-line-right {
display: inline-block;
width: 25%;
}
.sales-board-des {
border-top: 20px solid #fff;
background: #fff;
padding: 15px 20px;
}
.sales-board-des p {
line-height: 1.6;
}
.sales-board-des h2{
font-size: 20px;
padding-bottom: 15px;
}
.sales-board-des h3{
font-size: 18px;
font-weight: bold;
padding: 20px 0 10px 0;
}
.sales-board-des li{
padding: 5px 0;
}
.sales-board-table{
width:100%;
margin-top: 20px;
}
.sales-board-table th{
border: 1px solid #4fc08d;
color:#fff;
}
.sales-board-table td{
border: 1px solid #f0f2f5;
padding:15px;
}
</style>
新建Openproduct.vue
、Golife.vue
、Heigh.vue
、Logo.vue
四个模块文件
编辑Openproduct.vue
vue
<template>
<div class="open">
<div class="sales-board">
<div class="sales-board-intro">
<h2>开放产品</h2>
<p>
中国和拉美虽然相距遥远,但共同的梦想和追求将双方紧紧联系在一起。<br>在遥远的南美国家哥伦比亚,一场与中国的 "地铁之约" 正在精彩上演。<br>两年来,哥伦比亚首都波哥大地铁一号线项目建设热火朝天,50 名当地青年学员更是不远万里,先后来到中国西安进行专业培训。
<br>这不仅仅是一次学习之旅,更是中拉基建合作的生动写照。<br>中国与哥伦比亚携手,共同为城市交通注入新活力。从规划到建设,每一个环节都凝聚着双方的智慧和努力。
</p>
<div class="sales-board-form">
<div class="sales-board-line-left">购买数量:</div>
<div class="sales-board-line-right">
<!-- 购买数量组件 -->
<Counter :counterObj="counterObj"></Counter>
</div>
</div>
<div class="sales-board-form">
<div class="sales-board-line-left">产品类型:</div>
<!-- 产品类型选择组件 -->
<Types :selecterData="selecterData"/>
</div>
<div class="sales-board-form">
<div class="sales-board-line-left">有效时间:</div>
<!-- 有效时间选择组件 -->
<Timer :timerData="timerData"/>
</div>
</div>
<div class="sales-board-des">
<h2>产品说明</h2>
<p>这是产品说明...这是产品说明......这是产品说明...这是产品说明......这是产品说明...这是产品说明......</p>
<h2>用户行为指标</h2>
<ul>
<li>用户行为指标...用户行为指标...用户行为指标...用户行为指标...</li>
<li>用户行为指标1</li>
<li>用户行为指标2</li>
<li>用户行为指标3</li>
<li>用户行为指标4</li>
</ul>
<h3>浏览网站方式</h3>
<ul>
<li>浏览网站方式1</li>
<li>浏览网站方式2</li>
<li>浏览网站方式3</li>
<li>浏览网站方式4</li>
<li>浏览网站方式5</li>
</ul>
</div>
</div>
</div>
</template>
<script>
import Counter from '@/components/HomePage/counter'
import Types from '@/components/HomePage/types'
import Timer from '@/components/HomePage/timer'
export default {
data(){
return {
counterObj:{
min:1,
max:20
},
selecterData:[
{
value:"经典型",
id:1
},
{
value:"加强型",
id:2
},
{
value:"至尊豪华型",
id:3
},
],
timerData:[
{
value:'一个月',
id:1
},
{
value:'三个月',
id:3
},
{
value:'六个月',
id:6
},
{
value:'一年',
id:12
},
]
}
},
components:{
Counter,Types,Timer
}
}
</script>
<style scoped>
.open{
text-align: left;
}
.buy-dialog-title{
font-size: 16px;
font-weight: bold;
}
.buy-dialog-btn{
margin-top: 20px;
}
.buy-dialog-table{
width:100%;
margin-bottom: 20px;
}
.buy-dialog-table td,
.buy-dialog-table th{
border: 1px solid #e3e3e3;
text-align: center;
padding: 5px 0;
}
.buy-dialog-table th{
background: #4fc08d;
color: #fff;
border: 1px solid #4fc08d;
}
.button{
background: #4fc08d;
color: #fff;
display: inline-block;
padding: 10px 20px;
cursor: pointer;
}
</style>
修改router/index.js文件,增加详情页路由
javascript
import Vue from 'vue'
import VueRouter from 'vue-router'
import HomeView from '../views/main/HomePage/HomeView.vue'
import Layout from '../views/LayoutView.vue'
Vue.use(VueRouter)
const routes = [
{
path: '/',
name: 'Layout',
component: Layout,
children:[
{
path: '',
name: 'home',
component: HomeView,
meta:{
isLogin:true
}
},
{
path:"product",
name:"Product",
component:() => import("../views/main/ProductPage"),
meta:{
isLogin:true
}
},
{
path:"params",
name:"Params",
component:() => import("../views/main/ParamsView.vue"),
meta:{
isLogin:true
}
},
{
path:"ad",
name:"ADCategory",
component:() => import("../views/main/ADCategory.vue"),
meta:{
isLogin:true
}
},
{
path:"details",
name:"Details",
component:() => import("../views/main/HomePage/sub/DetailsView.vue"),
children:[
{
path:"openproduct",
name:"OpenProduct",
component:() => import("../views/main/HomePage/sub/Openproduct.vue")
},
{
path:"golife",
name:"Golife",
component:() => import("../views/main/HomePage/sub/Golife.vue")
},
{
path:"logo",
name:"Logo",
component:() => import("../views/main/HomePage/sub/Logo.vue")
},
{
path:"heigh",
name:"Heigh",
component:() => import("../views/main/HomePage/sub/Heigh.vue")
},
],
meta:{
isLogin:true
}
}
]
},
{
path:'/login',
name:'Login',
component:() => import("../views/LoginView.vue")
}
]
const router = new VueRouter({
routes,
// 去除路径上的#号
mode:"history"
})
export default router
修改HomeProductList.vue
文件,增加具体跳转路由信息
vue
<template>
<div class="index-board-list">
<div class="index-board-item"
v-for="(item,index) in buyData"
:key="index"
:class="['index-board-' + item.url,{'line-last':index%2 !== 0}]">
<div class="index-board-item-inner">
<h2>{{ item.title }}</h2>
<p>{{ item.desc }}</p>
<div class="index-board-button">
<!-- 拼接具体跳转位置 -->
<router-link :to="'/details/' + item.url" class="button">立即购买</router-link>
</div>
</div>
</div>
</div>
</template>
7.3.1.购买数量组件
在src/components目录下新建HomePage目录,新建counter.vue
文件实现购买数量组件
vue
<template>
<div class="counter-component">
<div class="counter-btn" @click="minHandle">-</div>
<div class="counter-show">
<input type="text" v-model="counter" @keyup="innerHeight">
</div>
<div class="counter-btn" @click="addHandle">+</div>
</div>
</template>
<script>
export default {
data(){
return {
counter : 1
}
},
props:{
counterObj:{
type:Object,
default(){
return{
min:1,
max:1
}
}
}
},
methods:{
minHandle(){
if(this.counter <= this.counterObj.min){
return;
}
this.counter--;
},
addHandle(){
if(this.counter >= this.counterObj.max){
return;
}
this.counter++;
},
innerHeight(){
var fix;
if(typeof this.counter === "string"){
fix = Number(this.counter.replace(/\D/g,""));
}else{
// 如果用户输入的是字符串,则将最小值赋值给fix
fix = this.counterObj.min;
}
if(fix <= this.counterObj.min){
fix = this.counterObj.min;
}
if(fix > this.counterObj.max){
fix = this.counterObj.max;
}
this.counter = fix;
}
}
}
</script>
<style scoped>
.counter-component{
position: relative;
display: inline-block;
overflow: hidden;
vertical-align: middle;
}
.counter-show{
float: left;
width: 50px;
}
.counter-show input{
width: 50px;
border: none;
text-align: center;
border-top: 1px solid #e3e3e3;
border-bottom: 1px solid #e3e3e3;
height: 23px;
line-height: 23px;
}
.counter-btn{
border: 1px solid #e3e3e3;
float: left;
height: 25px;
line-height: 25px;
width: 25px;
text-align: center;
cursor: pointer;
}
.counter-btn:hover{
border-color: #4fc08d;
background: #4fc08d;
color: #fff;
}
</style>
7.4.2.产品类型选择组件
在src/components/HomePage目录下,新建types.vue
文件实现产品类型选择组件
vue
<template>
<div class="selection-component">
<div class="selection-show" @click="showListHendle">
<span>{{ selecterData[currentIndex].value }}</span>
<div class="arrow"></div>
</div>
<div class="selection-list" v-show="isOpen">
<ul>
<li v-for="(item,index) in selecterData"
:key="index"
@click="selectHandle(index)">
{{ item.value }}
</li>
</ul>
</div>
</div>
</template>
<script>
export default {
data(){
return{
isOpen:false,
currentIndex:0,
}
},
props:{
selecterData:{
type:Array,
default(){
return [{value:"test",id:1}];
}
}
},
methods:{
showListHendle(){
this.isOpen = !this.isOpen;
},
selectHandle(index){
this.currentIndex = index;
this.isOpen = false;
}
}
}
</script>
<style scoped>
.selection-component{
position: relative;
display: inline-block;
}
.selection-show{
border: 1px solid #e3e3e3;
padding: 0 20px 0 10px;
display: inline-block;
position: relative;
cursor: pointer;
height: 25px;
line-height: 25px;
border-radius: 3px;
background: #fff;
}
.selection-show .arrow{
display: inline-block;
border-left: 4px solid transparent;
border-right: 4px solid transparent;
border-top: 5px solid transparent;
width: 0;
height: 0;
margin-top: -1px;
margin-left: 6px;
margin-right: -14px;
vertical-align: middle;
}
.selection-list{
display: inline-block;
position: absolute;
left: 0;
top: 25px;
width: 100%;
background: #fff;
border-top: 1px solid #e3e3e3;
border-bottom: 1px solid #e3e3e3;
z-index: 5;
}
.selection-list li{
padding: 5px 15px 5px 10px;
border-left: 1px solid #e3e3e3;
border-right: 1px solid #e3e3e3;
cursor: pointer;
background: #fff;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.selection-list li:hover{
background: #e3e3e3;
}
</style>
7.4.3.有效时间选择组件
在src/components/HomePage目录下,新建timer.vue
文件实现产品类型选择组件
vue
<template>
<div class="chooser-component">
<ul class="chooser-list">
<li @click="timerHandle(index)"
:class="{active:nowIndex === index}" v-for="(item,index) in timerData" :key="index">{{ item.value }}</li>
</ul>
</div>
</template>
<script>
export default {
data(){
return {
nowIndex:0,
}
},
props:{
timerData:{
type:Array,
default(){
return [{value:"test",id:1}];
}
}
},
methods:{
timerHandle(index){
this.nowIndex = index
}
}
}
</script>
<style scoped>
.chooser-component{
position: relative;
display: inline-block;
}
.chooser-list li{
display: inline-block;
border:1px solid #e3e3e3;
height: 25px;
line-height: 25px;
padding: 0 8px;
margin-right: 5px;
border-radius: 3px;
text-align: center;
cursor: pointer;
}
.chooser-list li.active{
border-color: #4fc08d;
background: #4fc08d;
color: #fff;
}
</style>
另外三个模块暂时忽略
运行效果
8.商品管理页实现
src/views/main目录下新建ProductPage目录,用于存放商品管理页面的相关组件
ProductPage目录下,新建index.vue
文件,作为商品管理页主入口
vue
<template>
<div class="product">
商品管理
</div>
</template>
<script>
export default {
}
</script>
<style>
.product{
width: 1200px;
margin: 0 auto;
margin-top: 20px;
}
</style>
修改product的路由配置
vue
{
path:'product',
name:'Product',
component:() => import('../views/main/ProductPage'),
meta:{
isLogin:true
}
},
8.1.服务器数据接口
修改server/router.js
文件,增加商品管理相关操作接口
java
const url = require("url")
const fs = require("fs")
const multer = require("multer")
/**
* 商品查询
*/
router.get("/backend/item/selectTbItemAllByPage",(req,res)=>{
// 分页
const page = url.parse(req.url,true).query.page || 1;
const sql = "select * from project order by id desc limit 10 offset " + (page - 1) * 10;
sqlClient(sql,null,result=>{
if(result.length > 0){
res.send({
status:200,
result
})
}else{
res.send({
status:401,
msg:"暂无数据"
})
}
})
})
/**
* 商品总数
*/
router.get("/total",(req,res)=>{
const sql = "SELECT COUNT(*) AS count FROM project where id";
sqlClient(sql,null,result =>{
if(result.length > 0){
res.send({
status:200,
result
})
}else{
res.send({
status:500,
msg:"暂无更多数据"
})
}
})
})
/**
* 模糊查询
*/
router.get("/search",(req,res)=>{
const search = url.parse(req.url,true).query.search;
const sql = "SELECT * FROM project WHERE concat(`title`,`sellPoint`,`descs`) LIKE '%" + search + "%'";
sqlClient(sql,null,result =>{
if(result.length > 0){
res.send({
status:200,
result
})
}else{
res.send({
status:500,
msg:"暂无数据"
})
}
})
})
/**
* 类目选择
*/
router.get("/backend/itemCategory/selectItemCategoryByParentId",(req,res)=>{
const id = url.parse(req.url,true).query.id || 1;
const sql = "SELECT cid,pid,name FROM category WHERE pid = ?";
const arr = [id]
sqlClient(sql,arr,result =>{
if(result.length > 0){
res.send({
status:200,
result
})
}else{
res.send({
status:500,
msg:"暂无数据"
})
}
})
})
/**
* 上传图片
*/
var storage = multer.diskStorage({
destination:function(req,file,cb){
cb(null,'./upload/')
},
filename:function(req,file,cb){
cb(null,Date.now() + "-" + file.originalname)
}
})
var createFolder = function (folder) {
try {
fs.accessSync(folder)
} catch (e) {
fs.mkdirSync(folder)
}
}
var uploadFolder = './upload/';
createFolder(uploadFolder);
var upload = multer({storage : storage});
router.post('/upload',upload.single('file'),function(req,res,next){
var file = req.file;
console.log('文件类型:%s',file.mimetype);
console.log('原文件名:%s',Buffer.from(file.originalname, "latin1").toString("utf8"));
console.log('文件大小:%s',file.size);
console.log('文件保存路径:%s',Buffer.from(file.path, "latin1").toString("utf8"));
res.json({
res_code:'0',
name:Buffer.from(file.originalname, "latin1").toString("utf8"),
url:Buffer.from(file.path, "latin1").toString("utf8")
});
})
/**
* 添加商品
*/
router.get("/backend/item/insertTbItem",(req,res)=>{
const cid = url.parse(req.url,true).query.cid || "";
const title = url.parse(req.url,true).query.title || "";
const sellPoint = url.parse(req.url,true).query.sellPoint || "";
const price = url.parse(req.url,true).query.price || "";
const num = url.parse(req.url,true).query.num || "";
const image = url.parse(req.url,true).query.image || "";
const desc = url.parse(req.url,true).query.desc || "";
const sql = "INSERT INTO project(`title`,`image`,`sellPoint`,`price`,`cid`,`num`,`status`,`descs`) VALUES(?,?,?,?,?,?,1,?)"
const arr = [title,image,sellPoint,price,cid,num,desc];
sqlClient(sql,arr,result=>{
if(result.affectedRows > 0){
res.send({
status:200,
msg:"添加成功"
})
}else{
res.send({
status:500,
msg:"添加失败"
})
}
})
})
/**
* 商品删除
*/
router.get("/backend/item/deleteItemById",(req,res) =>{
const id = url.parse(req.url,true).query.id;
const sql = "DELETE FROM project WHERE id = ?";
const arr = [id];
sqlClient(sql,arr,result =>{
if(result.affectedRows > 0){
res.send({
status:200,
msg:"删除成功"
})
}else{
res.send({
status:500,
msg:"删除失败"
})
}
})
})
/**
* 预更新
*/
router.get("/backend/item/preUpdateItem",(req,res) =>{
const id = url.parse(req.url,true).query.id;
const sql = "SELECT * FROM project WHERE id = ?";
sqlClient(sql,[id],result =>{
if(result.length > 0){
res.send({
status:200,
result
})
}else{
res.send({
status:500,
msg:"预更新失败"
})
}
})
})
/**
* 编辑商品信息
*/
router.get("/backend/item/updateTbItem",(req,res) =>{
const cid = url.parse(req.url,true).query.cid || "";
const title = url.parse(req.url,true).query.title || "";
const sellPoint = url.parse(req.url,true).query.sellPoint || "";
const price = url.parse(req.url,true).query.price || "";
const num = url.parse(req.url,true).query.num || "";
const image = url.parse(req.url,true).query.image || "";
const desc = url.parse(req.url,true).query.desc || "";
const id = url.parse(req.url,true).query.id;
const sql = "UPDATE project set title=?,sellPoint=?,cid=?,price=?,descs=?,image=?,num=? WHERE id=?";
const arr = [title,sellPoint,cid,price,desc,image,num,id]
sqlClient(sql,arr,(result) =>{
if(result.affectedRows > 0){
res.send({
status:200,
msg:"修改成功"
})
}else{
res.send({
status:500,
msg:"修改失败"
})
}
})
})
修改server/router.js
文件,增加静态文件位置
javascript
const express = require("express");
const app = express();
const cors = require("cors");
const bodyParser = require("body-parser");
const router = require("./router")
app.use(cors());
app.use(bodyParser.urlencoded({
extended:true
}))
app.use("/api",router);
app.use(express.static("upload"))
// 监听端口号
app.listen(3000,() =>{
console.log(3000)
})
在src/api/base.js
文件中增加对应信息
vue
const base = {
baseUrl:"http://localhost:3000",
register:"/api/register", // 注册
login:"/api/login", // 登录
selectTbItemAllByPage:"/api/backend/item/selectTbItemAllByPage", // 商品列表
total:"/api/total", // 商品总数
search:"/api/search", // 模糊查询
selectItemCategoryByParentId:"/api/backend/itemCategory/selectItemCategoryByParentId", // 类目选择
insertTbItem:"/api/backend/item/insertTbItem", // 商品添加
deleteItemById:"/api/backend/item/deleteItemById", //商品删除
preUpdateItem:"/api/backend/item/preUpdateItem", // 预更新
updateTbItem:"/api/backend/item/updateTbItem", // 修改商品
}
export default base;
修改src/api/index.js
文件,增加商品查询api
javascript
/**
* 商品列表
*/
selectTbItemAllByPage(params){
return axios.get(base.baseUrl + base.selectTbItemAllByPage,{
params
})
},
/**
* 商品数量
*/
total(){
return axios.get(base.baseUrl + base.total)
},
/**
* 模糊查询
*/
search(params){
return axios.get(base.baseUrl + base.search,{
params
})
},
/**
* 类目选择
*/
selectItemCategoryByParentId(params){
return axios.get(base.baseUrl + base.selectItemCategoryByParentId,{
params
})
},
/**
* 添加商品
*/
insertTbItem(params){
return axios.get(base.baseUrl + base.insertTbItem,{
params
})
},
/**
* 商品删除
*/
deleteItemById(params){
return axios.get(base.baseUrl + base.deleteItemById,{
params
})
},
/**
* 预更新
*/
preUpdateItem(params){
return axios.get(base.baseUrl + base.preUpdateItem,{
params
})
},
/**
* 修改商品
*/
updateTbItem(params){
return axios.get(base.baseUrl + base.updateTbItem,{
params
})
}
8.2.eventbus工具类实现
src/utils目录下新建eventbut.js
文件,实现EventBus,方便组件之间的数据传递
javascript
import Vue from "vue"
const EventBus = new Vue();
Object.defineProperties(Vue.prototype,{
$bus:{
get(){
return EventBus;
}
}
})
修改main.js
文件,【这里顺便加api的引用,方便后面调用api】
vue
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import './plugins/element.js'
import './router/permission'
import './utils/init'
import './assets/css/common.css'
// 引入轮播图插件
import VueAwesomeSwiper from 'vue-awesome-swiper'
import 'swiper/css/swiper.css'
// 引入api
import api from "./api"
// 引入eventbus
import "./utils/eventbus"
Vue.prototype.$api = api
// 使用VueAwesomeSwiper
Vue.use(VueAwesomeSwiper)
Vue.config.productionTip = false
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
8.3.商品列表信息
src/views/main/ProductPage目录下,新建ProductList.vue
文件,实现商品列表展示
vue
<template>
<div>
<el-table :data="tableData">
<el-table-column show-overflow-tooltip prop="id" label="产品id" width="100"></el-table-column>
<el-table-column show-overflow-tooltip prop="title" label="产品名称" width="150"></el-table-column>
<el-table-column show-overflow-tooltip prop="image" label="产品图片" width="100"></el-table-column>
<el-table-column show-overflow-tooltip prop="sellPoint" label="产品卖点" width="300"></el-table-column>
<el-table-column show-overflow-tooltip prop="price" label="产品价格" width="100"></el-table-column>
<el-table-column show-overflow-tooltip prop="num" label="产品数量" width="100"></el-table-column>
<el-table-column show-overflow-tooltip prop="descs" label="产品描述">
<template v-slot="scope">
<div v-html="scope.row.descs"></div>
</template>
</el-table-column>
<el-table-column label="操作">
<template v-slot="scope">
<el-button size="mini" @click="handleEdit(scope.$index, scope.row)">编辑</el-button>
<el-button size="mini" type="danger" @click="handleDelete(scope.$index, scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
</template>
<script>
export default {
data(){
return {
tableData:[]
}
},
mounted(){
this.http(1);
// 接收分页组件传递的数据
this.$bus.$on("page",page =>{
this.http(page)
})
// 接收搜索框组件传递的数据
this.$bus.$on("searchData",data=>{
this.tableData = data
})
this.$bus.$on("refresh",flag =>{
this.http(1)
})
},
methods:{
http(page){
this.$api.selectTbItemAllByPage({page}).then(res =>{
// 获取对应页的商品数据
if(res.data.status === 200){
this.tableData =res.data.result
}
})
},
// 编辑按钮
handleEdit(index,row){
this.$bus.$emit("onEditorEvent",row);
},
// 删除按钮
handleDelete(index,row){
this.$confirm("此操作会永久删除该数据,是否继续?","提示",{
confirmButtonText:"确定",
cancelButtonText:"取消",
type:"warning",
}).then(() =>{
this.$api.deleteItemById({ id:row.id }).then(res =>{
if(res.data.status === 200){
this.$message({
type:"success",
message:"删除成功"
})
this.http(1);
}else{
this.$message({
type:"error",
message:"删除失败"
})
}
})
}).catch(() => {
this.$message({
type:"info",
message:"已取消删除"
})
})
},
}
}
</script>
<style scoped>
</style>
修改index.vue文件,增加使用列表组件
vue
<template>
<div class="product">
<ProductList/>
</div>
</template>
<script>
import ProductList from './ProductList.vue'
export default {
components:{
ProductList
}
}
</script>
<style>
.product{
width: 1200px;
margin: 0 auto;
margin-top: 20px;
}
</style>
运行效果
8.4.搜索框
ProductPage目录下新建ProductHeader.vue
文件
vue
<template>
<div class="head">
<el-form ref="searchForm" :model="search" @submit.native.prevent>
<el-form-item>
<el-input v-model="search.content" @keyup.enter.native="onSubmitSearch"/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onSubmitSearch">查询</el-button>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="addFormHandle">添加</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script>
export default {
data(){
return {
search:{
content:""
}
}
},
methods:{
onSubmitSearch(){
// 查询
this.$api.search({
search:this.search.content
}).then(res =>{
// 将查询到的数据传递给接收对象
this.$bus.$emit('searchData',res.data.result)
}).catch(error =>{
console.log(error);
})
},
addFormHandle(){
// 打开添加商品窗口
this.$bus.$emit("onAddEvent",true)
}
}
}
</script>
<style scoped lang="less">
.el-form{
overflow: hidden;
clear: both;
.el-form-item{
float: left;
margin-right: 10px;
.el-input{
width:1030px;
}
}
}
.head{
margin-top: 20px;
width: 100%;
}
</style>
修改index.vue文件,增加使用搜索框组件
vue
<template>
<div class="product">
<ProductHeader/>
<ProductList/>
</div>
</template>
<script>
import ProductList from './ProductList.vue'
import ProductHeader from './ProductHeader.vue'
export default {
components:{
ProductList,ProductHeader
}
}
</script>
<style>
.product{
width: 1200px;
margin: 0 auto;
margin-top: 20px;
}
</style>
运行效果
8.5.分页
ProductPage目录下新建ProductPagination.vue
文件
vue
<template>
<!-- 分页 -->
<div class="pagination-container">
<el-pagination
layout="prev,pager,next,jumper"
:current-page.sync="currentPage"
@current-change="hangleCurrentChange"
:total="total"
@size-change="handleSizechange"
>
</el-pagination>
</div>
</template>
<script>
export default {
data(){
return {
currentPage:1,
total:0
}
},
methods:{
handleSizechange(){
},
hangleCurrentChange(val){
this.$bus.$emit("page",val)
}
},
mounted(){
this.$api.total().then(res =>{
if(res.data.status === 200){
this.total = res.data.result[0]["count"]
}
})
}
}
</script>
<style scoped>
.pagination-container{
margin-top : 20px;
}
</style>
修改index.vue文件,增加使用分页组件
vue
<template>
<div class="product">
<product-header></product-header>
<product-list/>
<product-pagination/>
</div>
</template>
<script>
import ProductHeader from './ProductHeader.vue'
import ProductList from './ProductList'
import ProductPagination from './ProductPagination.vue'
export default {
components:{
ProductList,ProductPagination,ProductHeader
}
}
</script>
<style>
.product{
width: 1200px;
margin: 0 auto;
margin-top: 20px;
}
</style>
运行效果
8.6.添加商品窗口
ProductPage目录下新建ProductAdd.vue
、PriductUpload.vue
、ProductTree.vue
、ProductWangEditor.vue
文件
编写ProductAdd.vue
文件
vue
<template>
<el-dialog
title="添加产品"
:visible.sync="diaologAddVisible"
width="60%"
:before-close="handleClose"
>
<el-form
label-width="80px"
:model="addForm"
ref="addForm"
>
<el-form-item label="商品类目:">
<span class="location tree">{{ treeData.name }}</span>
<el-button type="primary" class="location" @click="dialogCategoryHandle">类目选择</el-button>
<el-dialog
width="50%"
append-to-body
title="类目选择"
:visible.sync="dialogCategoryVisible"
>
<product-tree @onTree="getTreeData"></product-tree>
<span slot="footer" class="dialog-footer">
<el-button type="primary" @click="dialogCategoryClose">确认</el-button>
</span>
</el-dialog>
</el-form-item>
<el-form-item label="商品名称:">
<el-input v-model="addForm.name"></el-input>
</el-form-item>
<el-form-item label="商品卖点:">
<el-input v-model="addForm.sellPoint"></el-input>
</el-form-item>
<el-form-item label="商品价格:">
<el-input v-model="addForm.price"></el-input>
</el-form-item>
<el-form-item label="商品数量:">
<el-input v-model="addForm.num"></el-input>
</el-form-item>
<el-form-item label="商品图片:">
<img class="upload-img" :src="uploadData.url" alt="">
<el-button type="primary" class="location"
@click="dialogUploadHandle">上传图片</el-button>
<el-dialog
width="50%"
append-to-body
title="图片上传"
:visible.sync="dialogUploadVisible"
>
<priduct-upload @onUpload="getOnUpload"></priduct-upload>
<span slot="footer" class="dialog-footer">
<el-button type="primary" @click="dialogUploadClose">确认</el-button>
</span>
</el-dialog>
</el-form-item>
<el-form-item label="商品描述:">
<ProductWangEditor @onEditor="getEditor"></ProductWangEditor>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="addFormClose">取消</el-button>
<el-button type="primary" @click="addProductHandle">确认</el-button>
</span>
</el-dialog>
</template>
<script>
import PriductUpload from './PriductUpload.vue';
import ProductTree from './ProductTree.vue';
import ProductWangEditor from './ProductWangEditor.vue';
export default{
components:{ProductTree,PriductUpload,ProductWangEditor},
data(){
return {
diaologAddVisible : false,
dialogCategoryVisible:false,
dialogUploadVisible:false,
addForm:{
name:"",
sellPoint:"",
price:"",
num:"",
},
treeData:{}, // 类目选择
uploadData:{}, // 存储图片
editorData:"",
}
},
mounted(){
this.$bus.$on("onAddEvent",flag =>{
this.diaologAddVisible = flag
})
},
methods:{
handleClose(done){
this.$confirm("确认关闭")
.then(_ =>{
done();
})
.catch(_ =>{});
},
addFormClose(){
this.diaologAddVisible = false
},
dialogCategoryHandle(){
this.dialogCategoryVisible = true
},
dialogCategoryClose(){
this.dialogCategoryVisible = false
},
dialogUploadHandle(){
this.dialogUploadVisible = true
},
dialogUploadClose(){
this.dialogUploadVisible = false
},
// 读取类目选择数据
getTreeData(data){
// console.log(data);
this.treeData = data;
},
// 读取图片地址
getOnUpload(data){
if(data.url){
data.url = data.url.replace("upload","http://localhost:3000")
}
this.uploadData = data;
},
//接收富文本数据
getEditor(data){
this.editorData = data;
},
// 添加产品
addProductHandle(){
this.$api.insertTbItem({
cid:this.treeData.cid,
title:this.addForm.name,
sellPoint:this.addForm.sellPoint,
price:this.addForm.price,
num:this.addForm.num,
desc:this.editorData,
image:this.uploadData.url
}).then(res =>{
if(res.data.status === 200){
this.diaologAddVisible = false;
this.$bus.$emit("refresh",true)
}
}).catch(error=>{
console.log(error);
})
}
}
}
</script>
<style scoped>
.location{
float: left;
}
.tree{
margin-right: 20px;
}
.upload-img{
width: 300px;
float: left;
margin-right: 20px;
}
</style>
修改index.vue文件,增加使用添加商品组件
vue
<template>
<div class="product">
<product-header></product-header>
<product-list/>
<product-pagination/>
<product-add></product-add>
</div>
</template>
<script>
import ProductHeader from './ProductHeader.vue'
import ProductList from './ProductList'
import ProductPagination from './ProductPagination.vue'
import ProductAdd from './ProductAdd'
export default {
components:{
ProductList,ProductPagination,ProductHeader,ProductAdd
}
}
</script>
<style>
.product{
width: 1200px;
margin: 0 auto;
margin-top: 20px;
}
</style>
8.6.1.图片上传组件
编写PriductUpload.vue
文件
vue
<template>
<!-- 图片上传一定给的是单独的地址,而且跨域是后台解决的 -->
<el-upload
class="upload-demo"
ref="upload"
action="http://localhost:3000/api/upload"
:on-remove="handleRemove"
:file-list="fileList"
:auto-upload="false"
:on-success="handleSuccess"
>
<el-button slot="trigger" size="small" type="primary">选取文件</el-button>
<el-button
style="margin-left: 10px;"
size="small"
type="success"
@click="submitUpload"
>
上传到服务器
</el-button>
<div slot="tip" class="el-upload__tip">目前仅支持单张图片上传</div>
</el-upload>
</template>
<script>
export default {
data(){
return {
fileList:[]
}
},
methods:{
handleRemove(file, fileList) {
return this.$confirm(`确定移除 ${ file.name }?`);
},
handleSuccess(response){
// 上传成功后返回的数据response
this.$emit("onUpload",response)
},
submitUpload(){
this.$refs.upload.submit();
}
}
}
</script>
<style>
</style>
8.6.2.类目选择组件
编写ProductTree.vue
文件
vue
<template>
<el-tree
:props="defaultProps"
:load="loadNode"
lazy
@node-click="handleNodeClick"
>
</el-tree>
</template>
<script>
export default {
data() {
return {
data: [],
defaultProps: {
children: 'children',
label: 'name'
}
};
},
mounted(){
},
methods: {
handleNodeClick(data) {
this.$emit("onTree",data)
},
loadNode(node, resolve){
// 第一层数据
if(node.level ===0){
this.$api.selectItemCategoryByParentId().then(res =>{
if(res.data.status === 200){
return resolve(res.data.result)
}else{
return resolve([])
}
})
}
// 后续展开的数据
if(node.level >= 1){
this.$api.selectItemCategoryByParentId({
id:node.data.cid
}).then(res =>{
if(res.data.status === 200){
return resolve(res.data.result)
}else{
return resolve([])
}
}).catch(error =>{
return resolve([])
})
}
}
}
}
</script>
<style>
</style>
8.6.3.富文本编辑器组件
编写ProductWangEditor.vue
文件
vue
<template>
<div ref="editorWang"
style="text-align: left;"
>
</div>
</template>
<script>
import wangEditor from 'wangeditor'
export default {
data(){
return {
editor:null, // editor对象
editorData:'' // 承载编辑器数据
}
},
props:{
currentEditorData:{
type:String,
default:""
}
},
watch:{
currentEditorData(newVal, oldVal){
this.editor.txt.html(newVal);
}
},
mounted(){
this.editor = new wangEditor(this.$refs.editorWang);
// 配置 onchange 回调函数,将数据同步到vue中
this.editor.config.onchange = (newHtml) =>{
this.editorData = newHtml;
this.$emit("onEditor",this.editorData);
}
// 自定义菜单配置
this.editor.config.menus = [
'head', // 标题
'bold', // 粗体
'fontSize', // 字号
'fontName', // 字体
'italic', // 斜体
'underline', // 下划线
'strikeThrough',// 删除线
'foreColor', // 文字颜色
'backColor', // 背景颜色
'link', // 插入链接
'list', // 列表
'justify', // 对齐方式
'quote', // 引用
'emoticon', // 标签
'image', // 插入图片
'table', // 表格
'code', // 插入代码
'undo', // 撤销
'redo', // 重复
];
this.editor.create(); // 创建编辑器
},
beforeDestroy(){
// 调用销毁API对当前编辑器实例进行销毁
this.editor.destroy();
this.editor = null;
}
}
</script>
<style>
</style>
运行效果
8.7.编辑商品窗口
ProductPage目录下新建ProductEditor.vue
文件
vue
<template>
<el-dialog
title="编辑产品"
:visible.sync="diaologEditorVisible"
width="60%"
:before-close="handleClose"
>
<el-form
label-width="80px"
:model="editorForm"
ref="editorForm"
>
<el-form-item label="商品类目:">
<span class="location tree">{{ treeData.name }}</span>
<el-button type="primary" class="location" @click="dialogCategoryHandle">类目选择</el-button>
<el-dialog
width="50%"
append-to-body
title="类目选择"
:visible.sync="dialogCategoryVisible"
>
<product-tree @onTree="getTreeData"></product-tree>
<span slot="footer" class="dialog-footer">
<el-button type="primary" @click="dialogCategoryClose">确认</el-button>
</span>
</el-dialog>
</el-form-item>
<el-form-item label="商品名称:">
<el-input v-model="editorForm.name"></el-input>
</el-form-item>
<el-form-item label="商品卖点:">
<el-input v-model="editorForm.sellPoint"></el-input>
</el-form-item>
<el-form-item label="商品价格:">
<el-input v-model="editorForm.price"></el-input>
</el-form-item>
<el-form-item label="商品数量:">
<el-input v-model="editorForm.num"></el-input>
</el-form-item>
<el-form-item label="商品图片:">
<img class="upload-img" :src="uploadData.url" alt="">
<el-button type="primary" class="location"
@click="dialogUploadHandle">上传图片</el-button>
<el-dialog
width="50%"
append-to-body
title="图片上传"
:visible.sync="dialogUploadVisible"
>
<priduct-upload @onUpload="getOnUpload"></priduct-upload>
<span slot="footer" class="dialog-footer">
<el-button type="primary" @click="dialogUploadClose">确认</el-button>
</span>
</el-dialog>
</el-form-item>
<el-form-item label="商品描述:">
<ProductWangEditor @onEditor="getEditor" :currentEditorData="editorData"></ProductWangEditor>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="editorFormClose">取消</el-button>
<el-button type="primary" @click="editorProductHandle">确认</el-button>
</span>
</el-dialog>
</template>
<script>
import PriductUpload from './PriductUpload.vue';
import ProductTree from './ProductTree.vue';
import ProductWangEditor from './ProductWangEditor.vue';
export default{
components:{ProductTree,PriductUpload,ProductWangEditor},
data(){
return {
diaologEditorVisible : false,
dialogCategoryVisible:false,
dialogUploadVisible:false,
editorForm:{
name:"",
sellPoint:"",
price:"",
num:"",
},
treeData:{
cid:"",
name:""
}, // 类目选择
uploadData:{
url:""
}, // 存储图片
editorData:"",
currentData:{}
}
},
mounted(){
this.$bus.$on("onEditorEvent",row =>{
this.diaologEditorVisible = true;
this.currentData = row;
// 获取更新数据
this.$api.preUpdateItem({id:row.id}).then((res)=>{
// 赋值
this.treeData.name = res.data.result[0].cid;
this.treeData.cid = res.data.result[0].cid;
this.editorForm.name = res.data.result[0].title
this.editorForm.sellPoint = res.data.result[0].sellPoint
this.editorForm.price = res.data.result[0].price
this.editorForm.num = res.data.result[0].num
this.uploadData.url = res.data.result[0].image
this.editorData = res.data.result[0].descs
})
})
},
methods:{
handleClose(done){
this.$confirm("确认关闭")
.then(_ =>{
done();
})
.catch(_ =>{});
},
editorFormClose(){
this.diaologEditorVisible = false
},
dialogCategoryHandle(){
this.dialogCategoryVisible = true
},
dialogCategoryClose(){
this.dialogCategoryVisible = false
},
dialogUploadHandle(){
this.dialogUploadVisible = true
},
dialogUploadClose(){
this.dialogUploadVisible = false
},
// 读取类目选择数据
getTreeData(data){
// console.log(data);
this.treeData = data;
},
// 读取图片地址
getOnUpload(data){
if(data.url){
data.url = data.url.replace("upload","http://localhost:3000")
}
this.uploadData = data;
},
//接收富文本数据
getEditor(data){
this.editorData = data;
},
// 编辑产品
editorProductHandle(){
this.$api.updateTbItem({
id:this.currentData.id,
cid:this.treeData.cid,
title:this.editorForm.name,
sellPoint:this.editorForm.sellPoint,
price:this.editorForm.price,
num:this.editorForm.num,
desc:this.editorData,
image:this.uploadData.url
}).then(res =>{
if(res.data.status === 200){
this.diaologEditorVisible = false;
this.$bus.$emit("refresh",true)
}
}).catch(error=>{
console.log(error);
})
}
}
}
</script>
<style scoped>
.location{
float: left;
}
.tree{
margin-right: 20px;
}
.upload-img{
width: 300px;
float: left;
margin-right: 20px;
}
</style>
修改index.vue文件,增加使用修改商品组件
vue
<template>
<div class="product">
<product-header></product-header>
<product-list/>
<product-pagination/>
<product-add></product-add>
<ProductEditor/>
</div>
</template>
<script>
import ProductHeader from './ProductHeader.vue'
import ProductList from './ProductList'
import ProductPagination from './ProductPagination.vue'
import ProductAdd from './ProductAdd'
import ProductEditor from './ProductEditor.vue'
export default {
components:{
ProductList,ProductPagination,ProductHeader,ProductAdd,ProductEditor
}
}
</script>
<style>
.product{
width: 1200px;
margin: 0 auto;
margin-top: 20px;
}
</style>
运行效果
报错处理
1.找不到element模块
报错截图
处理方法
修改element.js文件,注释掉lang和locale的引用
javascript
import Vue from 'vue'
import element from 'element-ui'
//导入组件相关样式
import 'element-ui/lib/theme-chalk/index.css'
// import lang from 'element-ui/lib/locale/lang/'
// import locale from 'element-ui/lib/locale'
// locale.use(lang)
Vue.use(element)
2.找不到less-loader模块
报错截图
处理方法
使用
npm install less-loader less --save-dev
安装模块