一、项目介绍

本项目是书城项目,该项目共分为以下功能模块:

  1. 首页
    • banner图
    • 获取热门图书
  2. 列表页
    • 展示列表
    • 删除和收藏
    • 点击图书进入详情页
  3. 收藏
    • 列表
    • 删除收藏
  4. 新增
    • 新增图书

二、技术方案

  • 前端:vue + vue-router + less + vue-cli + axios + vue-awesome-swiper
  • 后端:express

三、项目结构以及git仓库

QQ图片20190801204817.png

四、入口文件及功能组件

1. main.js

  1. import Vue from 'vue'
  2. import App from './App.vue'
  3. import router from './router'
  4. import store from './store'
  5. import VueAwesomeSwiper from 'vue-awesome-swiper'
  6. import 'swiper/dist/css/swiper.css'
  7. Vue.config.productionTip = false
  8. Vue.use(VueAwesomeSwiper)
  9. new Vue({
  10. router,
  11. store,
  12. render: h => h(App)
  13. }).$mount('#app')

2. App.vue

  1. <template>
  2. <div id="app">
  3. <Tab></Tab>
  4. <router-view/>
  5. </div>
  6. </template>
  7. <script>
  8. // App.vue 这个组件不会销毁
  9. import Tab from '@/components/base/Tab.vue'
  10. export default {
  11. name: 'App',
  12. components: {
  13. Tab
  14. }
  15. }
  16. </script>
  17. <style>
  18. /*如果是公共样式,最好写在 App.vue 中*/
  19. * {
  20. margin: 0;
  21. padding: 0;
  22. }
  23. ul li {
  24. list-style: none;
  25. }
  26. a {
  27. text-decoration: none;
  28. }
  29. .content {
  30. position: absolute;
  31. top: 40px;
  32. width: 100%;
  33. bottom: 50px;
  34. }
  35. </style>

3. router.js

  1. import Vue from 'vue'
  2. import Router from 'vue-router'
  3. import Home from './views/Home.vue'
  4. import Collect from './views/Collect.vue'
  5. import Add from './views/Add.vue'
  6. Vue.use(Router)
  7. export default new Router({
  8. routes: [
  9. {
  10. path: '/',
  11. name: 'home',
  12. component: Home,
  13. alias: '/home'
  14. },
  15. {
  16. path: '/list',
  17. component: () => import('./views/List.vue')
  18. },
  19. {
  20. name: 'detail',
  21. path: '/detail/:id',
  22. component: () => import('./views/Detail.vue')
  23. },
  24. {
  25. path: '/collect',
  26. component: Collect
  27. },
  28. {
  29. name: 'add',
  30. path: '/add',
  31. component: Add
  32. }
  33. ]
  34. })

4. Tab.vue 组件

  1. <template>
  2. <div class="footer">
  3. <router-link to="/home">
  4. <i class="iconfont icon-home"></i>
  5. <span>首页</span>
  6. </router-link>
  7. <router-link to="/list">
  8. <i class="iconfont icon-chazhaobiaodanliebiao"></i>
  9. <span>列表</span>
  10. </router-link>
  11. <router-link to="/collect">
  12. <i class="iconfont icon-shoucang"></i>
  13. <span>收藏</span>
  14. </router-link>
  15. <router-link to="/add">
  16. <i class="iconfont icon-tianjia-xue"></i>
  17. <span>添加</span>
  18. </router-link>
  19. </div>
  20. </template>
  21. <style scoped lang="less">
  22. .footer {
  23. position: fixed;
  24. width: 100%;
  25. bottom: 0;
  26. display: flex;
  27. border-top: 1px solid #ccc;
  28. background: #fff;
  29. z-index: 100;
  30. a {
  31. display: flex;
  32. flex: 1; /* 每个a标签占相同的比例 */
  33. flex-direction: column;
  34. font-size: 18px;
  35. align-items: center;
  36. color: yellowgreen;
  37. }
  38. .router-link-active {
  39. color: red;
  40. }
  41. }
  42. </style>

5. MyHeader.vue 组件

  1. <template>
  2. <div class="header">
  3. <slot></slot>
  4. <i class="iconfont icon-fanhui" v-if="back" @click="goBack"></i>
  5. </div>
  6. </template>
  7. <script>
  8. export default {
  9. data () {
  10. return {}
  11. },
  12. props: ['back'],
  13. methods: {
  14. goBack () {
  15. this.$router.go(-1)
  16. }
  17. }
  18. }
  19. </script>
  20. <style scoped>
  21. .header {
  22. position: fixed;
  23. width: 100%;
  24. background: #afd9ee;
  25. text-align: center;
  26. height: 40px;
  27. line-height: 40px;
  28. font-size: 20px;
  29. z-index: 100;
  30. }
  31. .icon-fanhui {
  32. position: absolute;
  33. left: 10px;
  34. }
  35. </style>

5. Swiper.vue 组件

  1. <template>
  2. <swiper :options="swiperOption">
  3. <!---->
  4. <swiper-slide v-for="(item, index) in sliders" :key="index">
  5. <img :src="item" alt="">
  6. </swiper-slide>
  7. <div class="swiper-pagination" slot="pagination"></div>
  8. </swiper>
  9. </template>
  10. <script>
  11. import { swiper, swiperSlide } from 'vue-awesome-swiper'
  12. export default {
  13. data () {
  14. return {
  15. swiperOption: {
  16. autoplay: 3000,
  17. pagination: '.swiper-pagination',
  18. loop: true
  19. }
  20. }
  21. },
  22. props: ['sliders'],
  23. components: {
  24. swiper,
  25. swiperSlide
  26. }
  27. }
  28. </script>
  29. <style scoped>
  30. img {
  31. width: 100%;
  32. }
  33. </style>

五、页面组件

  • 首页截图

1.png

1. Home.vue

  1. <template>
  2. <div>
  3. <MyHeader>首页</MyHeader>
  4. <div class="content">
  5. <!--内容区域-->
  6. <Swiper :sliders="sliders"></Swiper>
  7. <div class="container">
  8. <h2>热门图书</h2>
  9. <ul>
  10. <li v-for="(item, index) in hotBooks" :key="index">
  11. <img :src="item.bookCover" alt="">
  12. <b>{{item.bookName}}</b>
  13. </li>
  14. </ul>
  15. </div>
  16. </div>
  17. </div>
  18. </template>
  19. <script>
  20. // @ is an alias to /src
  21. import MyHeader from '@/components/base/MyHeader.vue'
  22. import Swiper from '@/components/base/Swiper.vue'
  23. import { getSlider, getHot } from '../api/home'
  24. export default {
  25. name: 'home',
  26. data () {
  27. return {
  28. hotBooks: [],
  29. sliders: []
  30. }
  31. },
  32. async created () {
  33. this.sliders = await getSlider()
  34. this.hotBooks = await getHot()
  35. // this.slide()
  36. // this.getHotBook()
  37. },
  38. methods: {
  39. async slide () {
  40. this.sliders = await getSlider()
  41. },
  42. async getHotBook () {
  43. this.hotBooks = await getHot()
  44. }
  45. },
  46. components: {
  47. MyHeader,
  48. Swiper
  49. }
  50. }
  51. </script>
  52. <style scoped lang="less">
  53. .container {
  54. box-sizing: border-box;
  55. overflow-x: hidden;
  56. h2 {
  57. padding-left: 30px;
  58. }
  59. ul li {
  60. float: left;
  61. margin: 20px 0;
  62. width: 50%;
  63. img {
  64. display: block;
  65. }
  66. b {
  67. display: block;
  68. padding-left: 20px;
  69. }
  70. }
  71. }
  72. </style>

2. List.vue 列表页

2.png

  1. <template>
  2. <div class="wrapper">
  3. <MyHeader :back="true">列表页</MyHeader>
  4. <div class="content">
  5. <ul class="container">
  6. <router-link v-for="(book, index) in allBooks"
  7. :key="index"
  8. :to="{name: 'detail', params: {id: book.bookId}}" tag="li">
  9. <img :src="book.bookCover" alt="">
  10. <div class="right">
  11. <h3>{{book.bookName}}</h3>
  12. <p>{{book.bookInfo}}</p>
  13. <p class="rice">{{book.bookPrice}}</p>
  14. <button class="btn" @click.stop="remove(book.bookId)">删除</button>
  15. <button class="btn" @click.stop="collect(book)">收藏</button>
  16. </div>
  17. </router-link>
  18. </ul>
  19. </div>
  20. </div>
  21. </template>
  22. <script>
  23. import MyHeader from '@/components/base/MyHeader.vue'
  24. import { getAll, deleteBook, collectBook } from '../api/list'
  25. export default {
  26. name: 'List',
  27. data () {
  28. return {
  29. allBooks: []
  30. }
  31. },
  32. created () {
  33. this.getAllBooks()
  34. },
  35. methods: {
  36. async getAllBooks () {
  37. this.allBooks = await getAll()
  38. },
  39. async remove (id) {
  40. await deleteBook(id)
  41. this.getAllBooks()
  42. },
  43. async collect (data) {
  44. await collectBook(data)
  45. }
  46. },
  47. components: {
  48. MyHeader
  49. }
  50. }
  51. </script>
  52. <style scoped lang="less">
  53. .container {
  54. margin-bottom: 50px;
  55. li {
  56. padding: 10px;
  57. font-size: 16px;
  58. img {
  59. width: 160px;
  60. }
  61. .right {
  62. padding-top: 30px;
  63. width: 180px;
  64. float: right;
  65. }
  66. .price {
  67. color: red;
  68. font-size: 30px;
  69. }
  70. .btn {
  71. width: 60px;
  72. height: 30px;
  73. background: red;
  74. color: #fff;
  75. font-size: 18px;
  76. border: none;
  77. border-radius: 5px;
  78. &:nth-of-type(1) {
  79. margin-right: 5px;
  80. }
  81. }
  82. }
  83. }
  84. </style>

3. 详情页 Detail.vue

3.png

  1. <template>
  2. <div>
  3. <MyHeader :back="true"></MyHeader>
  4. <div class="content container">
  5. <ul>
  6. <li>
  7. <label>书名</label>
  8. <input type="text" v-model="book.bookName">
  9. </li>
  10. <li>
  11. <label>信息</label>
  12. <input type="text" v-model="book.bookInfo">
  13. </li>
  14. <li>
  15. <label>价格</label>
  16. <input type="text" v-model="book.bookPrice">
  17. </li>
  18. </ul>
  19. <button @click="updateBook">确认修改</button>
  20. </div>
  21. </div>
  22. </template>
  23. <script>
  24. import MyHeader from '../components/base/MyHeader.vue'
  25. import { getOne, update } from '../api/detail'
  26. export default {
  27. components: {
  28. MyHeader
  29. },
  30. name: 'Detail',
  31. data () {
  32. return {
  33. book: {}
  34. }
  35. },
  36. created () {
  37. let { id } = this.$route.params
  38. this.getBook(id)
  39. },
  40. methods: {
  41. async getBook (id) {
  42. this.book = await getOne(id)
  43. this.book.bookId = id
  44. },
  45. async updateBook () {
  46. await update(this.book)
  47. this.$router.go(-1)
  48. }
  49. }
  50. }
  51. </script>
  52. <style scoped lang="less">
  53. .container {
  54. width: 100%;
  55. padding: 20px;
  56. position: fixed;
  57. top: 40px;
  58. left: 0;
  59. right: 0;
  60. bottom: 0;
  61. height: 100%;
  62. background: #fff;
  63. z-index: 101;
  64. li {
  65. height: 100px;
  66. label {
  67. display: block;
  68. font-size: 25px;
  69. font-weight: bold;
  70. margin-bottom: 10px;
  71. }
  72. input {
  73. display: block;
  74. width: 300px;
  75. height: 40px;
  76. padding-left: 10px;
  77. margin-left: 5px;
  78. }
  79. }
  80. button {
  81. display: block;
  82. width: 100px;
  83. height: 40px;
  84. text-align: center;
  85. line-height: 40px;
  86. background: red;
  87. color: #fff;
  88. font-size: 20px;
  89. border-radius: 4px;
  90. border: none;
  91. }
  92. }
  93. </style>

4. Collect.vue 收藏夹

4.png

  1. <template>
  2. <div>
  3. <MyHeader :back="true">收藏页</MyHeader>
  4. <div class="content">
  5. <ul class="container">
  6. <li v-for="(book, index) in allBooks" :key="index">
  7. <img :src="book.bookCover" alt="">
  8. <div class="right">
  9. <h3>
  10. {{book.bookName}}
  11. </h3>
  12. <p>
  13. {{book.bookInfo}}
  14. </p>
  15. <p class="price">{{book.bookPrice}}</p>
  16. <button class="btn" @click="remove(book.bookId)">删除</button>
  17. </div>
  18. </li>
  19. </ul>
  20. </div>
  21. </div>
  22. </template>
  23. <script>
  24. import MyHeader from '../components/base/MyHeader.vue'
  25. import { getCollect, rmCollect } from '../api/collect'
  26. export default {
  27. name: 'Collect',
  28. data () {
  29. return {
  30. allBooks: []
  31. }
  32. },
  33. created () {
  34. this.getCollect()
  35. },
  36. methods: {
  37. async getCollect () {
  38. this.allBooks = await getCollect()
  39. },
  40. async remove (id) {
  41. await rmCollect(id)
  42. this.getCollect()
  43. }
  44. },
  45. components: {
  46. MyHeader
  47. }
  48. }
  49. </script>
  50. <style scoped lang="less">
  51. .container {
  52. margin-bottom: 50px;
  53. li {
  54. padding: 10px;
  55. font-size: 16px;
  56. img {
  57. width: 160px;
  58. }
  59. .right {
  60. float: right;
  61. width: 180px;
  62. }
  63. .price {
  64. color: red;
  65. font-size: 30px;
  66. }
  67. .btn {
  68. width: 60px;
  69. height: 30px;
  70. background: red;
  71. color: #fff;
  72. border: none;
  73. border-radius: 5px;
  74. }
  75. }
  76. }
  77. </style>

5. Add.vue 新增页面

5.png

  1. <template>
  2. <div>
  3. <MyHeader>添加页</MyHeader>
  4. <div class="content container">
  5. <ul>
  6. <li>
  7. <label>书名</label>
  8. <input type="text" v-model="book.bookName">
  9. </li>
  10. <li>
  11. <label>信息</label>
  12. <input type="text" v-model="book.bookInfo">
  13. </li>
  14. <li>
  15. <label>价格</label>
  16. <input type="text" v-model="book.bookPrice">
  17. </li>
  18. <li>
  19. <label>封面</label>
  20. <input type="text" v-model="book.bookCover">
  21. </li>
  22. </ul>
  23. <button @click="add" class="btn">新增</button>
  24. </div>
  25. </div>
  26. </template>
  27. <script>
  28. import MyHeader from '../components/base/MyHeader.vue'
  29. import { addBook } from '../api/add.js'
  30. export default {
  31. name: 'Add',
  32. data () {
  33. return {
  34. book: {}
  35. }
  36. },
  37. methods: {
  38. async add () {
  39. await addBook(this.book)
  40. this.$router.push('/list')
  41. }
  42. },
  43. components: {
  44. MyHeader
  45. }
  46. }
  47. </script>
  48. <style scoped lang="less">
  49. .container {
  50. width: 100%;
  51. padding: 20px;
  52. position: fixed;
  53. top: 40px;
  54. left: 0;
  55. right: 0;
  56. bottom: 50px;
  57. height: 100%;
  58. z-index: 10;
  59. li {
  60. height: 100px;
  61. label {
  62. display: block;
  63. font-size: 25px;
  64. font-weight: bold;
  65. margin-bottom: 10px;
  66. }
  67. input {
  68. display: block;
  69. width: 300px;
  70. height: 40px;
  71. padding-left: 10px;
  72. margin-left: 5px;
  73. }
  74. }
  75. button {
  76. width: 100px;
  77. height: 40px;
  78. display: block;
  79. text-align: center;
  80. line-height: 40px;
  81. background: red;
  82. color: #fff;
  83. font-size: 20px;
  84. border: none;
  85. border-radius: 5px;
  86. }
  87. }
  88. </style>

六、服务端代码

  1. let express = require('express');
  2. let bodyParser = require('body-parser');
  3. let fs = require('fs');
  4. let slides = require('./database/sliders');
  5. let bookData = './database/book.json';
  6. let collectData = './database/collect.json';
  7. let jdb = (dir) => JSON.parse(fs.readFileSync(dir, 'utf8'));
  8. let app = express();
  9. app.use(express.static(__dirname + '/static'));
  10. app.use(bodyParser.json());
  11. // 首页
  12. app.get('/api/sliders', (req, res) => {
  13. res.send(slides);
  14. });
  15. // 获取热门图书
  16. app.get('/api/hot', (req, res) => {
  17. // 从数组中最后四个
  18. let con = jdb(bookData, 'utf8');
  19. let data = con.slice(-4);
  20. res.send(data);
  21. });
  22. // 获取所有图书
  23. app.get('/api/books', (req, res) => {
  24. let con = jdb(bookData);
  25. res.send(con);
  26. });
  27. app.get('/api/collect', (req, res) => {
  28. let con = jdb(collectData);
  29. res.send(con)
  30. });
  31. // 删除书
  32. app.get('/api/delete', (req, res) => {
  33. let { id } = req.query;
  34. let con = jdb(bookData);
  35. con = con.filter(item => +item.bookId !== +id);
  36. fs.writeFileSync(bookData, JSON.stringify(con), 'utf8');
  37. res.send({
  38. code: 0,
  39. data: null,
  40. msg: 'ok'
  41. });
  42. });
  43. // 获取指定的id的图书
  44. app.get('/api/getOne', (req, res) => {
  45. let { id } = req.query;
  46. let con = jdb(bookData);
  47. let byId = con.find(item => +item.bookId === +id);
  48. if (byId) {
  49. res.send(byId)
  50. } else {
  51. res.send({
  52. code: 1,
  53. data: null,
  54. msg: 'id不存在'
  55. })
  56. }
  57. });
  58. // 修改图书信息
  59. app.post('/api/update', (req, res) => {
  60. let { bookId }= req.body;
  61. let con = jdb(bookData);
  62. let index = con.findIndex(item => +item.bookId === +bookId);
  63. con[index] = req.body;
  64. console.log(con[index]);
  65. fs.writeFileSync(bookData, JSON.stringify(con), 'utf8');
  66. res.send({
  67. code: 0,
  68. data: null,
  69. msg: 'ok'
  70. });
  71. });
  72. // 新增
  73. app.post('/api/add', (req, res) => {
  74. let con = jdb(bookData);
  75. let data = req.body;
  76. data.bookId = con.length ? +con[con.length - 1].bookId + 1 : 1;
  77. con.push(data);
  78. fs.writeFileSync(bookData, JSON.stringify(con), 'utf8');
  79. res.send({
  80. code: 0,
  81. data: null,
  82. msg: 'ok'
  83. })
  84. });
  85. // 收藏
  86. app.post('/api/collect', (req, res) => {
  87. let con = jdb(collectData);
  88. let data = req.body;
  89. con.push(data);
  90. fs.writeFileSync(collectData, JSON.stringify(con), 'utf8');
  91. res.send({
  92. code: 0,
  93. data: null,
  94. msg: 'ok'
  95. })
  96. });
  97. app.get('/api/rmCollect', (req, res) => {
  98. let con = jdb(collectData);
  99. let { id } = req.query;
  100. con = con.filter(item => +item.bookId !== +id);
  101. fs.writeFileSync(collectData, JSON.stringify(con), 'utf8');
  102. res.send({
  103. code: 0,
  104. data: null,
  105. msg: 'ok'
  106. })
  107. });
  108. app.listen(8090, () => console.log('port 8000 is on'));

七、vue.config.js

  1. module.exports = {
  2. outputDir: '../book-server/static',
  3. devServer: {
  4. open: true,
  5. proxy: {
  6. '/api': {
  7. target: 'http://localhost:8090',
  8. changeOrigin: true,
  9. secure: false
  10. }
  11. }
  12. }
  13. };