一、项目介绍
本项目是书城项目,该项目共分为以下功能模块:
- 首页
- banner图
- 获取热门图书
- 列表页
- 展示列表
- 删除和收藏
- 点击图书进入详情页
- 收藏
- 列表
- 删除收藏
- 新增
- 新增图书
二、技术方案
- 前端:vue + vue-router + less + vue-cli + axios + vue-awesome-swiper
- 后端:express
三、项目结构以及git仓库
四、入口文件及功能组件
1. main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import VueAwesomeSwiper from 'vue-awesome-swiper'
import 'swiper/dist/css/swiper.css'
Vue.config.productionTip = false
Vue.use(VueAwesomeSwiper)
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
2. App.vue
<template>
<div id="app">
<Tab></Tab>
<router-view/>
</div>
</template>
<script>
// App.vue 这个组件不会销毁
import Tab from '@/components/base/Tab.vue'
export default {
name: 'App',
components: {
Tab
}
}
</script>
<style>
/*如果是公共样式,最好写在 App.vue 中*/
* {
margin: 0;
padding: 0;
}
ul li {
list-style: none;
}
a {
text-decoration: none;
}
.content {
position: absolute;
top: 40px;
width: 100%;
bottom: 50px;
}
</style>
3. router.js
import Vue from 'vue'
import Router from 'vue-router'
import Home from './views/Home.vue'
import Collect from './views/Collect.vue'
import Add from './views/Add.vue'
Vue.use(Router)
export default new Router({
routes: [
{
path: '/',
name: 'home',
component: Home,
alias: '/home'
},
{
path: '/list',
component: () => import('./views/List.vue')
},
{
name: 'detail',
path: '/detail/:id',
component: () => import('./views/Detail.vue')
},
{
path: '/collect',
component: Collect
},
{
name: 'add',
path: '/add',
component: Add
}
]
})
4. Tab.vue 组件
<template>
<div class="footer">
<router-link to="/home">
<i class="iconfont icon-home"></i>
<span>首页</span>
</router-link>
<router-link to="/list">
<i class="iconfont icon-chazhaobiaodanliebiao"></i>
<span>列表</span>
</router-link>
<router-link to="/collect">
<i class="iconfont icon-shoucang"></i>
<span>收藏</span>
</router-link>
<router-link to="/add">
<i class="iconfont icon-tianjia-xue"></i>
<span>添加</span>
</router-link>
</div>
</template>
<style scoped lang="less">
.footer {
position: fixed;
width: 100%;
bottom: 0;
display: flex;
border-top: 1px solid #ccc;
background: #fff;
z-index: 100;
a {
display: flex;
flex: 1; /* 每个a标签占相同的比例 */
flex-direction: column;
font-size: 18px;
align-items: center;
color: yellowgreen;
}
.router-link-active {
color: red;
}
}
</style>
5. MyHeader.vue 组件
<template>
<div class="header">
<slot></slot>
<i class="iconfont icon-fanhui" v-if="back" @click="goBack"></i>
</div>
</template>
<script>
export default {
data () {
return {}
},
props: ['back'],
methods: {
goBack () {
this.$router.go(-1)
}
}
}
</script>
<style scoped>
.header {
position: fixed;
width: 100%;
background: #afd9ee;
text-align: center;
height: 40px;
line-height: 40px;
font-size: 20px;
z-index: 100;
}
.icon-fanhui {
position: absolute;
left: 10px;
}
</style>
5. Swiper.vue 组件
<template>
<swiper :options="swiperOption">
<!---->
<swiper-slide v-for="(item, index) in sliders" :key="index">
<img :src="item" alt="">
</swiper-slide>
<div class="swiper-pagination" slot="pagination"></div>
</swiper>
</template>
<script>
import { swiper, swiperSlide } from 'vue-awesome-swiper'
export default {
data () {
return {
swiperOption: {
autoplay: 3000,
pagination: '.swiper-pagination',
loop: true
}
}
},
props: ['sliders'],
components: {
swiper,
swiperSlide
}
}
</script>
<style scoped>
img {
width: 100%;
}
</style>
五、页面组件
- 首页截图
1. Home.vue
<template>
<div>
<MyHeader>首页</MyHeader>
<div class="content">
<!--内容区域-->
<Swiper :sliders="sliders"></Swiper>
<div class="container">
<h2>热门图书</h2>
<ul>
<li v-for="(item, index) in hotBooks" :key="index">
<img :src="item.bookCover" alt="">
<b>{{item.bookName}}</b>
</li>
</ul>
</div>
</div>
</div>
</template>
<script>
// @ is an alias to /src
import MyHeader from '@/components/base/MyHeader.vue'
import Swiper from '@/components/base/Swiper.vue'
import { getSlider, getHot } from '../api/home'
export default {
name: 'home',
data () {
return {
hotBooks: [],
sliders: []
}
},
async created () {
this.sliders = await getSlider()
this.hotBooks = await getHot()
// this.slide()
// this.getHotBook()
},
methods: {
async slide () {
this.sliders = await getSlider()
},
async getHotBook () {
this.hotBooks = await getHot()
}
},
components: {
MyHeader,
Swiper
}
}
</script>
<style scoped lang="less">
.container {
box-sizing: border-box;
overflow-x: hidden;
h2 {
padding-left: 30px;
}
ul li {
float: left;
margin: 20px 0;
width: 50%;
img {
display: block;
}
b {
display: block;
padding-left: 20px;
}
}
}
</style>
2. List.vue 列表页
<template>
<div class="wrapper">
<MyHeader :back="true">列表页</MyHeader>
<div class="content">
<ul class="container">
<router-link v-for="(book, index) in allBooks"
:key="index"
:to="{name: 'detail', params: {id: book.bookId}}" tag="li">
<img :src="book.bookCover" alt="">
<div class="right">
<h3>{{book.bookName}}</h3>
<p>{{book.bookInfo}}</p>
<p class="rice">{{book.bookPrice}}</p>
<button class="btn" @click.stop="remove(book.bookId)">删除</button>
<button class="btn" @click.stop="collect(book)">收藏</button>
</div>
</router-link>
</ul>
</div>
</div>
</template>
<script>
import MyHeader from '@/components/base/MyHeader.vue'
import { getAll, deleteBook, collectBook } from '../api/list'
export default {
name: 'List',
data () {
return {
allBooks: []
}
},
created () {
this.getAllBooks()
},
methods: {
async getAllBooks () {
this.allBooks = await getAll()
},
async remove (id) {
await deleteBook(id)
this.getAllBooks()
},
async collect (data) {
await collectBook(data)
}
},
components: {
MyHeader
}
}
</script>
<style scoped lang="less">
.container {
margin-bottom: 50px;
li {
padding: 10px;
font-size: 16px;
img {
width: 160px;
}
.right {
padding-top: 30px;
width: 180px;
float: right;
}
.price {
color: red;
font-size: 30px;
}
.btn {
width: 60px;
height: 30px;
background: red;
color: #fff;
font-size: 18px;
border: none;
border-radius: 5px;
&:nth-of-type(1) {
margin-right: 5px;
}
}
}
}
</style>
3. 详情页 Detail.vue
<template>
<div>
<MyHeader :back="true"></MyHeader>
<div class="content container">
<ul>
<li>
<label>书名</label>
<input type="text" v-model="book.bookName">
</li>
<li>
<label>信息</label>
<input type="text" v-model="book.bookInfo">
</li>
<li>
<label>价格</label>
<input type="text" v-model="book.bookPrice">
</li>
</ul>
<button @click="updateBook">确认修改</button>
</div>
</div>
</template>
<script>
import MyHeader from '../components/base/MyHeader.vue'
import { getOne, update } from '../api/detail'
export default {
components: {
MyHeader
},
name: 'Detail',
data () {
return {
book: {}
}
},
created () {
let { id } = this.$route.params
this.getBook(id)
},
methods: {
async getBook (id) {
this.book = await getOne(id)
this.book.bookId = id
},
async updateBook () {
await update(this.book)
this.$router.go(-1)
}
}
}
</script>
<style scoped lang="less">
.container {
width: 100%;
padding: 20px;
position: fixed;
top: 40px;
left: 0;
right: 0;
bottom: 0;
height: 100%;
background: #fff;
z-index: 101;
li {
height: 100px;
label {
display: block;
font-size: 25px;
font-weight: bold;
margin-bottom: 10px;
}
input {
display: block;
width: 300px;
height: 40px;
padding-left: 10px;
margin-left: 5px;
}
}
button {
display: block;
width: 100px;
height: 40px;
text-align: center;
line-height: 40px;
background: red;
color: #fff;
font-size: 20px;
border-radius: 4px;
border: none;
}
}
</style>
4. Collect.vue 收藏夹
<template>
<div>
<MyHeader :back="true">收藏页</MyHeader>
<div class="content">
<ul class="container">
<li v-for="(book, index) in allBooks" :key="index">
<img :src="book.bookCover" alt="">
<div class="right">
<h3>
{{book.bookName}}
</h3>
<p>
{{book.bookInfo}}
</p>
<p class="price">{{book.bookPrice}}</p>
<button class="btn" @click="remove(book.bookId)">删除</button>
</div>
</li>
</ul>
</div>
</div>
</template>
<script>
import MyHeader from '../components/base/MyHeader.vue'
import { getCollect, rmCollect } from '../api/collect'
export default {
name: 'Collect',
data () {
return {
allBooks: []
}
},
created () {
this.getCollect()
},
methods: {
async getCollect () {
this.allBooks = await getCollect()
},
async remove (id) {
await rmCollect(id)
this.getCollect()
}
},
components: {
MyHeader
}
}
</script>
<style scoped lang="less">
.container {
margin-bottom: 50px;
li {
padding: 10px;
font-size: 16px;
img {
width: 160px;
}
.right {
float: right;
width: 180px;
}
.price {
color: red;
font-size: 30px;
}
.btn {
width: 60px;
height: 30px;
background: red;
color: #fff;
border: none;
border-radius: 5px;
}
}
}
</style>
5. Add.vue 新增页面
<template>
<div>
<MyHeader>添加页</MyHeader>
<div class="content container">
<ul>
<li>
<label>书名</label>
<input type="text" v-model="book.bookName">
</li>
<li>
<label>信息</label>
<input type="text" v-model="book.bookInfo">
</li>
<li>
<label>价格</label>
<input type="text" v-model="book.bookPrice">
</li>
<li>
<label>封面</label>
<input type="text" v-model="book.bookCover">
</li>
</ul>
<button @click="add" class="btn">新增</button>
</div>
</div>
</template>
<script>
import MyHeader from '../components/base/MyHeader.vue'
import { addBook } from '../api/add.js'
export default {
name: 'Add',
data () {
return {
book: {}
}
},
methods: {
async add () {
await addBook(this.book)
this.$router.push('/list')
}
},
components: {
MyHeader
}
}
</script>
<style scoped lang="less">
.container {
width: 100%;
padding: 20px;
position: fixed;
top: 40px;
left: 0;
right: 0;
bottom: 50px;
height: 100%;
z-index: 10;
li {
height: 100px;
label {
display: block;
font-size: 25px;
font-weight: bold;
margin-bottom: 10px;
}
input {
display: block;
width: 300px;
height: 40px;
padding-left: 10px;
margin-left: 5px;
}
}
button {
width: 100px;
height: 40px;
display: block;
text-align: center;
line-height: 40px;
background: red;
color: #fff;
font-size: 20px;
border: none;
border-radius: 5px;
}
}
</style>
六、服务端代码
let express = require('express');
let bodyParser = require('body-parser');
let fs = require('fs');
let slides = require('./database/sliders');
let bookData = './database/book.json';
let collectData = './database/collect.json';
let jdb = (dir) => JSON.parse(fs.readFileSync(dir, 'utf8'));
let app = express();
app.use(express.static(__dirname + '/static'));
app.use(bodyParser.json());
// 首页
app.get('/api/sliders', (req, res) => {
res.send(slides);
});
// 获取热门图书
app.get('/api/hot', (req, res) => {
// 从数组中最后四个
let con = jdb(bookData, 'utf8');
let data = con.slice(-4);
res.send(data);
});
// 获取所有图书
app.get('/api/books', (req, res) => {
let con = jdb(bookData);
res.send(con);
});
app.get('/api/collect', (req, res) => {
let con = jdb(collectData);
res.send(con)
});
// 删除书
app.get('/api/delete', (req, res) => {
let { id } = req.query;
let con = jdb(bookData);
con = con.filter(item => +item.bookId !== +id);
fs.writeFileSync(bookData, JSON.stringify(con), 'utf8');
res.send({
code: 0,
data: null,
msg: 'ok'
});
});
// 获取指定的id的图书
app.get('/api/getOne', (req, res) => {
let { id } = req.query;
let con = jdb(bookData);
let byId = con.find(item => +item.bookId === +id);
if (byId) {
res.send(byId)
} else {
res.send({
code: 1,
data: null,
msg: 'id不存在'
})
}
});
// 修改图书信息
app.post('/api/update', (req, res) => {
let { bookId }= req.body;
let con = jdb(bookData);
let index = con.findIndex(item => +item.bookId === +bookId);
con[index] = req.body;
console.log(con[index]);
fs.writeFileSync(bookData, JSON.stringify(con), 'utf8');
res.send({
code: 0,
data: null,
msg: 'ok'
});
});
// 新增
app.post('/api/add', (req, res) => {
let con = jdb(bookData);
let data = req.body;
data.bookId = con.length ? +con[con.length - 1].bookId + 1 : 1;
con.push(data);
fs.writeFileSync(bookData, JSON.stringify(con), 'utf8');
res.send({
code: 0,
data: null,
msg: 'ok'
})
});
// 收藏
app.post('/api/collect', (req, res) => {
let con = jdb(collectData);
let data = req.body;
con.push(data);
fs.writeFileSync(collectData, JSON.stringify(con), 'utf8');
res.send({
code: 0,
data: null,
msg: 'ok'
})
});
app.get('/api/rmCollect', (req, res) => {
let con = jdb(collectData);
let { id } = req.query;
con = con.filter(item => +item.bookId !== +id);
fs.writeFileSync(collectData, JSON.stringify(con), 'utf8');
res.send({
code: 0,
data: null,
msg: 'ok'
})
});
app.listen(8090, () => console.log('port 8000 is on'));
七、vue.config.js
module.exports = {
outputDir: '../book-server/static',
devServer: {
open: true,
proxy: {
'/api': {
target: 'http://localhost:8090',
changeOrigin: true,
secure: false
}
}
}
};