vue.js高仿饿了么(前期整理)
1、熟悉项目开发流程
需求分析——>脚手架工具——>数据mock——>架构设计——>代码编写——>自测——>编译打包。
2、熟悉代码规范
从架构设计、组件抽象、模块拆分,到代码风格统一、CSS代码规范和JavaScript变量命名规范,以标准写代码,开发出扩展性、通用性强的优质代码。
3、掌握Vue.js在实战中应用
4、学会使用Vue.js完整地开发移动端App
5、学会工程化开发、组件化开发和模块化开发的方式
6、酷炫的交互设计
所用到的技术
后端:vue-resource(ajax通信)
前端:vue-router(官方插件管理路由)、localstorage、flex布局、css sticky footer布局、html、css、es6、Vue-cli(脚手架,用来搭建基本代码框架)、vue(热门前端框架)
其他:webpack(构建工具)、eslint(代码风格检查工具)
第三方库:better-scroll
Vue.js出现的历史原因也是前端的发展趋势
1、旧浏览器逐渐淘汰,移动端需求增加;
2、前端交互越来越多,功能越来越复杂;
3、架构从传统后台MVC向REST API+前端MV*(MVC、MVP、MVVM)迁移。
MVVM框架具有的优点
1、针对具有复杂交互逻辑的前端应用;
2、提供基础的架构抽象;
3、通过Ajax数据持久化,保证前端用户体验。
下面这张图充分说明了什么是MVVM框架,也说明vue.js双向数据绑定的基本原理:
什么是vue.js
它是一个轻量级MVVM框架,主要用于数据驱动+组件化的前端开发。它具有以下特点(数据驱动和组件化为核心思想):
1、轻量级(大小只有20k+)
2、简洁(容易上手,学习曲线平稳)
3、快速
4、组件化:扩展HTML元素,封装可重用的代码。设计的原则为(1)页面上每个独立的可视/可交互区域视为一个组件;(2)每个组件对应一个工程目录,组件所需要的各种资源在这个目录下就近维护;(3)页面不过是组件的容器,组件可以嵌套自由组合,形成完整的页面。
5、数据驱动:DOM是数据的一种自然映射,看下面两张图进行理解:
结合上面两张图,再看看下面一张图,进一步理解vue.js实现双向数据绑定的原理(又称为数据响应原理,即数据【model】改变驱动视图【view】自动更新):
6、模块友好
Vue-cli介绍
这是一款Vue的脚手架工具,脚手架的含义就是编写好基础的代码。vue-cli帮助我们编写好了目录结构、本地调试、代码部署、热加载和单元测试。
github地址:https://github.com/vuejs/vue-cli
安装Vue-cli
基本教程戳这里
【注意】
初次使用,千万不要启动ESLint语法规范。
webpack打包
webpack就是将各种资源打包,然后输出js、图片、css静态文件。
新姿势
设备像素比(DPR)
图标字体的制作
打开https://icomoon.io/ => 点击右上角icomoon app => 点击左上角import icons =>选择做好的svg文件 => 点击左上角untitled set选择上图标 => 点击右下角generate fonts =>再点击右下角download即可下载(左上角preferences是修改下载文件夹的名称)。
使用图标字体
设计好项目目录,将fonts文件夹下的文件复制粘贴进项目中,将style.css复制粘贴进stylus目录中,并改名为icon.styl,同时将里面的内容改为stylus语法
mock数据
作为前端经常需要模拟后台数据,我们称之为mock,通常的方式为自己搭建一个服务器或者创建一个json文件,返回我们想要的数据。
在这一步有个坑,就是vue-cli更新了,很多配置都发生了变更。在这里有很好的解决方法。下面贴上webpack.dev.conf.js更改后的代码
'use strict' const utils = require('./utils') const webpack = require('webpack') const config = require('../config') const merge = require('webpack-merge') const path = require('path') const baseWebpackConfig = require('./webpack.base.conf') const CopyWebpackPlugin = require('copy-webpack-plugin') const HtmlWebpackPlugin = require('html-webpack-plugin') const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin') const portfinder = require('portfinder') //首先 const express = require('express') const app = express() var appData = require('../data.json') var seller = appData.seller var goods = appData.goods var ratings = appData.ratings var apiRoutes = express.Router() app.use('/api', apiRoutes) const HOST = process.env.HOST const PORT = process.env.PORT && Number(process.env.PORT) const devWebpackConfig = merge(baseWebpackConfig, { module: { rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, usePostCSS: true }) }, // cheap-module-eval-source-map is faster for development devtool: config.dev.devtool, // these devServer options should be customized in /config/index.js devServer: { clientLogLevel: 'warning', historyApiFallback: { rewrites: [{ from: /.*/, to: path.join(config.dev.assetsPublicPath, 'index.html') }, ], }, hot: true, contentBase: false, // since we use CopyWebpackPlugin. compress: true, host: HOST || config.dev.host, port: PORT || config.dev.port, open: config.dev.autoOpenBrowser, overlay: config.dev.errorOverlay ? { warnings: false, errors: true } : false, publicPath: config.dev.assetsPublicPath, proxy: config.dev.proxyTable, quiet: true, // necessary for FriendlyErrorsPlugin watchOptions: { poll: config.dev.poll, }, //找到devServer,添加 before(app) { app.get('/api/seller', (req, res) => { res.json({ // 这里是你的json内容 errno: 0, data: seller }) }), app.get('/api/goods', (req, res) => { res.json({ // 这里是你的json内容 errno: 0, data: goods }) }), app.get('/api/ratings', (req, res) => { res.json({ // 这里是你的json内容 errno: 0, data: ratings }) }) } }, plugins: [ new webpack.DefinePlugin({ 'process.env': require('../config/dev.env') }), new webpack.HotModuleReplacementPlugin(), new webpack.NamedModulesPlugin(), // HMR shows correct file names in console on update. new webpack.NoEmitOnErrorsPlugin(), // https://github.com/ampedandwired/html-webpack-plugin new HtmlWebpackPlugin({ filename: 'index.html', template: 'index.html', inject: true }), // copy custom static assets new CopyWebpackPlugin([{ from: path.resolve(__dirname, '../static'), to: config.dev.assetsSubDirectory, ignore: ['.*'] }]) ] }) module.exports = new Promise((resolve, reject) => { portfinder.basePort = process.env.PORT || config.dev.port portfinder.getPort((err, port) => { if (err) { reject(err) } else { // publish the new Port, necessary for e2e tests process.env.PORT = port // add port to devServer config devWebpackConfig.devServer.port = port // Add FriendlyErrorsPlugin devWebpackConfig.plugins.push(new FriendlyErrorsPlugin({ compilationSuccessInfo: { messages: [`Your application is running here: http://${devWebpackConfig.devServer.host}:${port}`], }, onErrors: config.dev.notifyOnErrors ? utils.createNotifierCallback() : undefined })) resolve(devWebpackConfig) } }) })
没有启动ESlint语法检查的webpack.dev.conf.js的代码
'use strict' const utils = require('./utils') const webpack = require('webpack') const config = require('../config') const merge = require('webpack-merge') const path = require('path') const baseWebpackConfig = require('./webpack.base.conf') const CopyWebpackPlugin = require('copy-webpack-plugin') const HtmlWebpackPlugin = require('html-webpack-plugin') const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin') const portfinder = require('portfinder') //首先 const express = require('express') const app = express() var appData = require('../data.json') var seller = appData.seller var goods = appData.goods var ratings = appData.ratings var apiRoutes = express.Router() app.use('/api', apiRoutes) const HOST = process.env.HOST const PORT = process.env.PORT && Number(process.env.PORT) const devWebpackConfig = merge(baseWebpackConfig, { module: { rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, usePostCSS: true }) }, // cheap-module-eval-source-map is faster for development devtool: config.dev.devtool, // these devServer options should be customized in /config/index.js devServer: { clientLogLevel: 'warning', historyApiFallback: { rewrites: [ { from: /.*/, to: path.posix.join(config.dev.assetsPublicPath, 'index.html') }, ], }, hot: true, contentBase: false, // since we use CopyWebpackPlugin. compress: true, host: HOST || config.dev.host, port: PORT || config.dev.port, open: config.dev.autoOpenBrowser, overlay: config.dev.errorOverlay ? { warnings: false, errors: true } : false, publicPath: config.dev.assetsPublicPath, proxy: config.dev.proxyTable, quiet: true, // necessary for FriendlyErrorsPlugin watchOptions: { poll: config.dev.poll, }, //找到devServer,添加 before(app) { app.get('/api/seller', (req, res) => { res.json({ // 这里是你的json内容 errno: 0, data: seller }) }), app.get('/api/goods', (req, res) => { res.json({ // 这里是你的json内容 errno: 0, data: goods }) }), app.get('/api/ratings', (req, res) => { res.json({ // 这里是你的json内容 errno: 0, data: ratings }) }) } }, plugins: [ new webpack.DefinePlugin({ 'process.env': require('../config/dev.env') }), new webpack.HotModuleReplacementPlugin(), new webpack.NamedModulesPlugin(), // HMR shows correct file names in console on update. new webpack.NoEmitOnErrorsPlugin(), // https://github.com/ampedandwired/html-webpack-plugin new HtmlWebpackPlugin({ filename: 'index.html', template: 'index.html', inject: true }), // copy custom static assets new CopyWebpackPlugin([ { from: path.resolve(__dirname, '../static'), to: config.dev.assetsSubDirectory, ignore: ['.*'] } ]) ] }) module.exports = new Promise((resolve, reject) => { portfinder.basePort = process.env.PORT || config.dev.port portfinder.getPort((err, port) => { if (err) { reject(err) } else { // publish the new Port, necessary for e2e tests process.env.PORT = port // add port to devServer config devWebpackConfig.devServer.port = port // Add FriendlyErrorsPlugin devWebpackConfig.plugins.push(new FriendlyErrorsPlugin({ compilationSuccessInfo: { messages: [`Your application is running here: http://${devWebpackConfig.devServer.host}:${port}`], }, onErrors: config.dev.notifyOnErrors ? utils.createNotifierCallback() : undefined })) resolve(devWebpackConfig) } }) })
为了更好的查看调取api回来的数据,建议安装Google的jsonview插件。然后在浏览器查看是否成功返回数据,测试:http://localhost:8080/api/goods
此时在项目中导入了一个data.json文件
组件拆分部分(一) 启动ESlint语法时,对各种格式的检查到了骇人的地步,多个;和换个行都能报warning!并且js文件的最后还要求换行!注释还要遵守规范!!
步骤一:导入static文件reset.css
步骤二:删除默认组件HelloWorld.vue和assets文件夹,修改router中的index.js
import Vue from 'vue' import Router from 'vue-router' import header from '@/components/header/header' Vue.use(Router) export default new Router({ routes: [ { path: '/', name: 'header', component: header } ] })
步骤三:分别配置App.vue和main.js
App.vue <template> <div id="app"> <v-header></v-header> <div class="tab"> 我是导航区块 </div> <div class="conpent"> 我是内容区块 </div> </div> </template> <script> import header from './components/header/header.vue' export default { name: 'app', components: { 'v-header': header } } </script> <style scoped> </style>
// The Vue build version to load with the `import` command // (runtime-only or standalone) has been set in webpack.base.conf with an alias. import Vue from 'vue' import App from './App' import router from './router' Vue.config.productionTip = false /* eslint-disable no-new */ new Vue({ el: '#app', router, template: '<App/>', components: { App } })
步骤四:增加一个header.vue文件
<template> <div class="header"> 我是header </div> </template> <script> export default { name: 'v-header' } </script> <style scoped> </style>
组件拆分部分(二)
由于需要用到stylus语法,因此事先得安装stylus 和 stylus-loader的相关依赖包
步骤一:修改App.vue文件
<template> <div id="app"> <v-header></v-header> <div class="tab"> <div class="tab-item">商品</div> <div class="tab-item">评论</div> <div class="tab-item">商家</div> </div> <div class="conpent"> 我是内容区块 </div> </div> </template> <script> import header from './components/header/header.vue' export default { name: 'app', components: { 'v-header': header } } </script> <style scoped lang="stylus" rel="stylesheet/stylus"> #app .tab display: flex width: 100% height: 40px line-height: 40px .tab-item flex: 1 text-align: center </style>
配置路由规则
配置路由规则没什么难度,去官方网站瞧一瞧就行了,这里有个linkActiveClass属性,挺有用的。下面稍微看一看配置过程
步骤一:创建一个goods.vue文件,其他两个ratings.vue和seller.vue类似
<template> <div> 我是goods </div> </template> <script> export default { name: 'v-goods' } </script> <style scoped> </style>
步骤二:在index.js文件中配置路由规则
import Vue from 'vue' import Router from 'vue-router' import goods from '../components/goods/goods' import ratings from '../components/ratings/ratings' import seller from '../components/seller/seller' Vue.use(Router) export default new Router({ // 改变路由激活时的class名称 linkActiveClass: 'active', routes: [ { path: '/', component: goods }, { path: '/goods', component: goods }, { path: '/ratings', component: ratings }, { path: '/seller', component: seller } ] })
另一种路由规则配置
import Vue from 'vue' import Router from 'vue-router' import header from '@/components/header/header' import goods from '@/components/goods/goods' import ratings from '@/components/ratings/ratings' import seller from '@/components/seller/seller' Vue.use(Router) export default new Router({ // 改变路由激活时的class名称 linkActiveClass: 'active', routes: [ { path: '/', name: 'header', component: header }, { path: '/goods', name: 'goods', component: goods }, { path: '/ratings', name: 'ratings', component: ratings }, { path: '/seller', naem: 'seller', component: seller } ] })
步骤三:配置根组件App.vue
<template> <div> <v-header></v-header> <div class="tab"> <div class="tab-item"> <router-link to="/goods">商品</router-link> </div> <div class="tab-item"> <router-link to="/ratings">评价</router-link> </div> <div class="tab-item"> <router-link to="/seller">商家</router-link> </div> </div> <router-view></router-view> </div> </template> <script> import header from './components/header/header.vue' export default { name: 'app', components: { 'v-header': header } } </script> <style scoped lang="stylus" rel="stylesheet/stylus"> .tab display: flex width: 100% height: 40px line-height: 40px .tab-item flex: 1 text-align: center & > a display:block font-size:16px color:rgb(77,85,93) &.active color:rgb(240,20,20) </style>
在局域网内,通过手机访问webapp项目
命令行:ipconfig查询本机IP => 用IP代替localhost => 将地址复制到草料二维码的官方网站生成二维码 => 用微信扫一扫(假如微信扫不了的话,下载一个二维码扫描来扫描)
【注意】
需要解决ip地址无法访问vue项目的问题
1、将config文件夹中的index.js文件修改
'use strict' // Template version: 1.3.1 // see http://vuejs-templates.github.io/webpack for documentation. const path = require('path') module.exports = { dev: { // Paths assetsSubDirectory: 'static', assetsPublicPath: '/', proxyTable: {}, // Various Dev Server settings host: '0.0.0.0', // can be overwritten by process.env.HOST port: 8080, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined autoOpenBrowser: false, errorOverlay: true, notifyOnErrors: true, poll: false, // https://webpack.js.org/configuration/dev-server/#devserver-watchoptions- /** * Source Maps */ // https://webpack.js.org/configuration/devtool/#development devtool: 'cheap-module-eval-source-map', // If you have problems debugging vue-files in devtools, // set this to false - it *may* help // https://vue-loader.vuejs.org/en/options.html#cachebusting cacheBusting: true, cssSourceMap: true }, build: { // Template for index.html index: path.resolve(__dirname, '../dist/index.html'), // Paths assetsRoot: path.resolve(__dirname, '../dist'), assetsSubDirectory: 'static', assetsPublicPath: '/', /** * Source Maps */ productionSourceMap: true, // https://webpack.js.org/configuration/devtool/#production devtool: '#source-map', // Gzip off by default as many popular static hosts such as // Surge or Netlify already gzip all static assets for you. // Before setting to `true`, make sure to: // npm install --save-dev compression-webpack-plugin productionGzip: false, productionGzipExtensions: ['js', 'css'], // Run the build command with an extra argument to // View the bundle analyzer report after build finishes: // `npm run build --report` // Set to `true` or `false` to always turn it on or off bundleAnalyzerReport: process.env.npm_config_report } }
2、重启服务
由于DPR引发的1px问题以及解决方式
步骤一:创建一个mixin.styl文件,作为边框的公共样式
border-1px($color)
position: relative
&:after
display: block
position: absolute
left: 0
bottom: 0
width: 100%
border-top: 1px solid $color
content: ' '
步骤二:创建一个base.styl文件,解决1px问题
@media (-webkit-min-device-pixel-ratio: 1.5),(min-device-pixel-ratio: 1.5)
.border-1px
&::after
-webkit-transform: scaleY(0.7)
transform: scaleY(0.7)
@media (-webkit-min-device-pixel-ratio: 2),(min-device-pixel-ratio: 2)
.border-1px
&::after
-webkit-transform: scaleY(0.5)
transform: scaleY(0.5)
步骤三:创建一个index.styl文件,统一导入styl文件
@import "./mixin"
@import "./icon"
@import "./base"
步骤四:修改icon.styl文件,需要更改url路径,不然会报错
@font-face font-family: 'sell-icon' src: url('../fonts/sell-icon.eot?2430tu') src: url('../fonts/sell-icon.eot?2430tu#iefix') format('embedded-opentype'), url('../fonts/sell-icon.ttf?2430tu') format('truetype'), url('../fonts/sell-icon.woff?2430tu') format('woff'), url('../fonts/sell-icon.svg?2430tu#sell-icon') format('svg') font-weight: normal font-style: normal [class^="icon-"], [class*=" icon-"] /* use !important to prevent issues with browser extensions that change fonts */ font-family: 'sell-icon' !important speak: none font-style: normal font-weight: normal font-variant: normal text-transform: none line-height: 1 /* Better Font Rendering =========== */ -webkit-font-smoothing: antialiased -moz-osx-font-smoothing: grayscale .icon-add_circle:before content: "\e900" .icon-arrow_lift:before content: "\e901" .icon-check_circle:before content: "\e902" .icon-close:before content: "\e903" .icon-favorite:before content: "\e904" .icon-keyboard_arrow_right:before content: "\e905" .icon-remove_circle_outline:before content: "\e906" .icon-shopping_cart:before content: "\e907" .icon-thumb_down:before content: "\e908" .icon-thumb_up:before content: "\e909" .icon-office:before content: "\e90a"
步骤五:配置router文件夹下的index.js文件
import Vue from 'vue' import Router from 'vue-router' import header from '@/components/header/header' import goods from '@/components/goods/goods' import ratings from '@/components/ratings/ratings' import seller from '@/components/seller/seller' import '@/common/stylus/index.styl' Vue.use(Router) export default new Router({ // 改变路由激活时的class名称 linkActiveClass: 'active', routes: [ { path: '/', name: 'header', component: header }, { path: '/goods', name: 'goods', component: goods }, { path: '/ratings', name: 'ratings', component: ratings }, { path: '/seller', naem: 'seller', component: seller } ] })
步骤六:在App.vue使用写好的styl样式
<template> <div> <v-header></v-header> <div class="tab border-1px"> <div class="tab-item"> <router-link to="/goods">商品</router-link> </div> <div class="tab-item"> <router-link to="/ratings">评价</router-link> </div> <div class="tab-item"> <router-link to="/seller">商家</router-link> </div> </div> <router-view></router-view> </div> </template> <script> import header from './components/header/header.vue' export default { name: 'app', components: { 'v-header': header } } </script> <style scoped lang="stylus" rel="stylesheet/stylus"> @import "./common/stylus/mixin.styl"; .tab display: flex width: 100% height: 40px line-height: 40px /*border-bottom: 1px solid rgba(7, 17, 27, 0.1)*/ border-1px(rgba(7, 17, 27, 0.1)) .tab-item flex: 1 text-align: center & > a display:block font-size:16px color:rgb(77,85,93) &.active color:rgb(240,20,20) </style>
编辑.eslintrc.js的规则
假如在创建项目中出现如下情况
那么就打开.eslintrc.js文件,将其规则忽略掉(设置为0)
清除掉令人抓狂的eslintrc语法规则!
'use strict' const path = require('path') const utils = require('./utils') const config = require('../config') const vueLoaderConfig = require('./vue-loader.conf') function resolve (dir) { return path.join(__dirname, '..', dir) } //const createLintingRule = () => ({ ////test: /\.(js|vue)$/, ////loader: 'eslint-loader', ////enforce: 'pre', ////include: [resolve('src'), resolve('test')], ////options: { //// formatter: require('eslint-friendly-formatter'), //// emitWarning: !config.dev.showEslintErrorsInOverlay ////} //}) module.exports = { context: path.resolve(__dirname, '../'), entry: { app: './src/main.js' }, output: { path: config.build.assetsRoot, filename: '[name].js', publicPath: process.env.NODE_ENV === 'production' ? config.build.assetsPublicPath : config.dev.assetsPublicPath }, resolve: { extensions: ['.js', '.vue', '.json'], alias: { 'vue$': 'vue/dist/vue.esm.js', '@': resolve('src'), } }, module: { rules: [ // ...(config.dev.useEslint ? [createLintingRule()] : []), { test: /\.vue$/, loader: 'vue-loader', options: vueLoaderConfig }, { test: /\.js$/, loader: 'babel-loader', include: [resolve('src'), resolve('test'), resolve('node_modules/webpack-dev-server/client')] }, { test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, loader: 'url-loader', options: { limit: 10000, name: utils.assetsPath('img/[name].[hash:7].[ext]') } }, { test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/, loader: 'url-loader', options: { limit: 10000, name: utils.assetsPath('media/[name].[hash:7].[ext]') } }, { test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, loader: 'url-loader', options: { limit: 10000, name: utils.assetsPath('fonts/[name].[hash:7].[ext]') } } ] }, node: { // prevent webpack from injecting useless setImmediate polyfill because Vue // source contains it (although only uses it if it's native). setImmediate: false, // prevent webpack from injecting mocks to Node native modules // that does not make sense for the client dgram: 'empty', fs: 'empty', net: 'empty', tls: 'empty', child_process: 'empty' } }
使用vue-resource
步骤一:安装cnpm install vue-resource --save
步骤二:配置router文件夹中的index.js文件
import Vue from 'vue' import Router from 'vue-router' import Resource from 'vue-resource' import header from '@/components/header/header' import goods from '@/components/goods/goods' import ratings from '@/components/ratings/ratings' import seller from '@/components/seller/seller' import '@/common/stylus/index.styl' Vue.use(Router) Vue.use(Resource) export default new Router({ // 改变路由激活时的class名称 linkActiveClass: 'active', routes: [ { path: '/', name: 'header', component: header }, { path: '/goods', name: 'goods', component: goods }, { path: '/ratings', name: 'ratings', component: ratings }, { path: '/seller', naem: 'seller', component: seller } ] })
步骤三:配置App.vue文件
<template> <div> <v-header></v-header> <div class="tab border-1px"> <div class="tab-item"> <router-link to="/goods">商品</router-link> </div> <div class="tab-item"> <router-link to="/ratings">评价</router-link> </div> <div class="tab-item"> <router-link to="/seller">商家</router-link> </div> </div> <router-view></router-view> </div> </template> <script> import header from './components/header/header.vue'; const ERR_OK = 0; export default { name: 'app', data() { return { seller: {} } }, created() { this.$http.get('api/seller').then((response) => { response = response.body; if(response.error === ERR_OK){ this.seller = response.data; } console.log(response.data); }) }, components: { 'v-header': header } } </script> <style scoped lang="stylus" rel="stylesheet/stylus"> @import "./common/stylus/mixin.styl"; .tab display: flex width: 100% height: 40px line-height: 40px /*border-bottom: 1px solid rgba(7, 17, 27, 0.1)*/ border-1px(rgba(7, 17, 27, 0.1)) .tab-item flex: 1 text-align: center & > a display:block font-size:16px color:rgb(77,85,93) &.active color:rgb(240,20,20) </style>
切记
导入stylus样式表必须在style作如下的定义
<style scoped lang="stylus" rel="stylesheet/stylus"></style>
外部组件(一)
步骤一:修改App.vue,将seller对象的数据传递给组件header.vue
<template> <div> <v-header :seller="seller"></v-header> <div class="tab border-1px"> <div class="tab-item"> <router-link to="/goods">商品</router-link> </div> <div class="tab-item"> <router-link to="/ratings">评价</router-link> </div> <div class="tab-item"> <router-link to="/seller">商家</router-link> </div> </div> <router-view></router-view> </div> </template> <script> import header from './components/header/header.vue'; const ERR_OK = 0; export default { name: 'app', data() { return { seller: {} } }, created() { this.$http.get('api/seller').then((response) => { response = response.body; if(response.error === ERR_OK){ this.seller = response.data; } console.log(response.data); this.seller = response.data; }) }, components: { 'v-header': header } } </script> <style scoped lang="stylus" rel="stylesheet/stylus"> @import "./common/stylus/mixin.styl"; .tab display: flex width: 100% height: 40px line-height: 40px /*border-bottom: 1px solid rgba(7, 17, 27, 0.1)*/ border-1px(rgba(7, 17, 27, 0.1)) .tab-item flex: 1 text-align: center & > a display:block font-size:16px color:rgb(77,85,93) &.active color:rgb(240,20,20) </style>
步骤二:编写header.vue组件
<template> <div class="header"> <div class="content-wrapper"> <div class="avatar"> <img width="64" height="64" :src="seller.avatar"> </div> <div class="content"> <div class="title"> <span class="brand"></span> <span class="name">{{seller.name}}</span> </div> <div class="description"> {{seller.description}}/{{seller.deliveryTime}}分钟送达 </div> <div v-if="seller.supports" class="support"> <span class="icon"></span> <span class="text">{{seller.supports[0].description}}</span> </div> </div> </div> <div class="bulletin-wrapper"></div> </div> </template> <script> export default { name: 'v-header', props: { seller: { type: Object } } } </script> <style scoped lang="stylus" rel="stylesheet/stylus"> .header color: #fff background: #000 .content-wrapper padding: 24px 12px 18px 24px .avatar display: inline-block </style>
外部组件(二)
步骤一:将所需要的图片导入到header文件夹中
步骤二:编写mixin.styl文件,目的是让程序在不同设备下显示不同的图片大小
border-1px($color) position: relative &:after display: block position: absolute left: 0 bottom: 0 width: 100% border-top: 1px solid $color content: ' ' bg-image($url) background-image: url($url + "@2x.png") @media (-webkit-min-device-pixel-ratio: 3),(min-device-pixel-ratio: 3) background-image: url($url + "@3x.png")
步骤三:编写base.styl文件,目的是统一页面字体
body, html line-height: 1 font-weight: 200 font-family: 'PingFang SC', 'STHeitiSC-Light', 'Helvetica-Light', arial, sans-serif @media (-webkit-min-device-pixel-ratio: 1.5),(min-device-pixel-ratio: 1.5) .border-1px &::after -webkit-transform: scaleY(0.7) transform: scaleY(0.7) @media (-webkit-min-device-pixel-ratio: 2),(min-device-pixel-ratio: 2) .border-1px &::after -webkit-transform: scaleY(0.5) transform: scaleY(0.5)
步骤四:在header.vue文件中增加样式
<template> <div class="header"> <div class="content-wrapper"> <div class="avatar"> <img width="64" height="64" :src="seller.avatar"> </div> <div class="content"> <div class="title"> <span class="brand"></span> <span class="name">{{seller.name}}</span> </div> <div class="description"> {{seller.description}}/{{seller.deliveryTime}}分钟送达 </div> <div v-if="seller.supports" class="support"> <span class="icon"></span> <span class="text">{{seller.supports[0].description}}</span> </div> </div> </div> <div class="bulletin-wrapper"></div> </div> </template> <script> export default { name: 'v-header', props: { seller: { type: Object } } } </script> <style scoped lang="stylus" rel="stylesheet/stylus"> @import "../../common/stylus/mixin"; .header color: #fff background: #000 .content-wrapper padding: 24px 12px 18px 24px font-size: 0 .avatar display: inline-block .content display: inline-block margin-left: 16px font-size: 14px .title margin: 2px 0 8px 0 .brand display: inline-block vertical-align: top width: 30px height:18px bg-image("brand") background-size: 30px 18px background-repeat: no-repeat .name margin-left: 6px font-size: 16px line-height: 18px font-weight: bold </style>
外部组件(三)
继续编写header.vue文件,当然相关的图片也要导入进来。我们可以通过修改<span class="icon" :class="classMap[seller.supports[0].type]"></span>的数值来决定要显示的图片,对内容显示操作也是如此:
<span class="text">{{seller.supports[0].description}}</span>
<template> <div class="header"> <div class="content-wrapper"> <div class="avatar"> <img width="64" height="64" :src="seller.avatar"> </div> <div class="content"> <div class="title"> <span class="brand"></span> <span class="name">{{seller.name}}</span> </div> <div class="description"> {{seller.description}}/{{seller.deliveryTime}}分钟送达 </div> <div v-if="seller.supports" class="support"> <span class="icon" :class="classMap[seller.supports[0].type]"></span> <span class="text">{{seller.supports[0].description}}</span> </div> </div> </div> <div class="bulletin-wrapper"></div> </div> </template> <script> export default { name: 'v-header', props: { seller: { type: Object } }, created() { this.classMap = ['decrease','discount','special','invoice','guarantee']; } } </script> <style scoped lang="stylus" rel="stylesheet/stylus"> @import "../../common/stylus/mixin"; .header color: #fff background: #000 .content-wrapper padding: 24px 12px 18px 24px font-size: 0 .avatar display: inline-block vertical-align: top img border-radius: 2px .content display: inline-block margin-left: 16px .title margin: 2px 0 8px 0 .brand display: inline-block vertical-align: top width: 30px height:18px bg-image("brand") background-size: 30px 18px background-repeat: no-repeat .name margin-left: 6px font-size: 16px line-height: 18px font-weight: bold .description margin-bottom: 10px line-height: 12px font-size: 12px .support .icon display: inline-block vertical-align: top height: 12px width: 12px margin-right: 4px background-size: 12px 12px background-repeat: no-repeat &.decrease bg-image('decrease_1') &.discount bg-image('discount_1') &.guarantee bg-image('guarantee_1') &.invoice bg-image('invoice_1') &.special bg-image('special_1') .text font-size: 10px line-height: 12px </style>
外部组件(四)
继续编写header.vue文件
<template> <div class="header"> <div class="content-wrapper"> <div class="avatar"> <img width="64" height="64" :src="seller.avatar"> </div> <div class="content"> <div class="title"> <span class="brand"></span> <span class="name">{{seller.name}}</span> </div> <div class="description"> {{seller.description}}/{{seller.deliveryTime}}分钟送达 </div> <div v-if="seller.supports" class="support"> <span class="icon" :class="classMap[seller.supports[0].type]"></span> <span class="text">{{seller.supports[0].description}}</span> </div> </div> <div class="support-count" v-if="seller.supports"> <span class="count">{{seller.supports.length}}</span> <i class="icon-keyboard_arrow_right"></i> </div> </div> <div class="bulletin-wrapper"></div> </div> </template> <script> export default { name: 'v-header', props: { seller: { type: Object } }, created() { this.classMap = ['decrease','discount','special','invoice','guarantee']; } } </script> <style scoped lang="stylus" rel="stylesheet/stylus"> @import "../../common/stylus/mixin"; .header color: #fff background: #000 .content-wrapper position: relative padding: 24px 12px 18px 24px font-size: 0 .avatar display: inline-block vertical-align: top img border-radius: 2px .content display: inline-block margin-left: 16px .title margin: 2px 0 8px 0 .brand display: inline-block vertical-align: top width: 30px height:18px bg-image("brand") background-size: 30px 18px background-repeat: no-repeat .name margin-left: 6px font-size: 16px line-height: 18px font-weight: bold .description margin-bottom: 10px line-height: 12px font-size: 12px .support .icon display: inline-block vertical-align: top height: 12px width: 12px margin-right: 4px background-size: 12px 12px background-repeat: no-repeat &.decrease bg-image('decrease_1') &.discount bg-image('discount_1') &.guarantee bg-image('guarantee_1') &.invoice bg-image('invoice_1') &.special bg-image('special_1') .text font-size: 10px line-height: 12px .support-count position: absolute right: 12px bottom: 14px padding: 0 8px height: 24px line-height: 24px border-radius: 14px background: rgba(0, 0, 0, 0.2) text-align: center .count vertical-align: top font-size: 10px .icon-keyboard_arrow_right margin-left: 2px line-height: 24px font-size: 10px </style>
外部组件(五)
继续编写header.vue文件,增加公告部分
<template> <div class="header"> <div class="content-wrapper"> <div class="avatar"> <img width="64" height="64" :src="seller.avatar"> </div> <div class="content"> <div class="title"> <span class="brand"></span> <span class="name">{{seller.name}}</span> </div> <div class="description"> {{seller.description}}/{{seller.deliveryTime}}分钟送达 </div> <div v-if="seller.supports" class="support"> <span class="icon" :class="classMap[seller.supports[0].type]"></span> <span class="text">{{seller.supports[0].description}}</span> </div> </div> <div class="support-count" v-if="seller.supports"> <span class="count">{{seller.supports.length}}</span> <i class="icon-keyboard_arrow_right"></i> </div> </div> <div class="bulletin-wrapper"> <span class="bulletin-title"></span> <span class="bulletin-text">{{seller.bulletin}}</span> <i class="icon-keyboard_arrow_right"></i> </div> </div> </template> <script> export default { name: 'v-header', props: { seller: { type: Object } }, created() { this.classMap = ['decrease','discount','special','invoice','guarantee']; } } </script> <style scoped lang="stylus" rel="stylesheet/stylus"> @import "../../common/stylus/mixin"; .header color: #fff background: #000 .content-wrapper position: relative padding: 24px 12px 18px 24px font-size: 0 .avatar display: inline-block vertical-align: top img border-radius: 2px .content display: inline-block margin-left: 16px .title margin: 2px 0 8px 0 .brand display: inline-block vertical-align: top width: 30px height:18px bg-image("brand") background-size: 30px 18px background-repeat: no-repeat .name margin-left: 6px font-size: 16px line-height: 18px font-weight: bold .description margin-bottom: 10px line-height: 12px font-size: 12px .support .icon display: inline-block vertical-align: top height: 12px width: 12px margin-right: 4px background-size: 12px 12px background-repeat: no-repeat &.decrease bg-image('decrease_1') &.discount bg-image('discount_1') &.guarantee bg-image('guarantee_1') &.invoice bg-image('invoice_1') &.special bg-image('special_1') .text font-size: 10px line-height: 12px .support-count position: absolute right: 12px bottom: 14px padding: 0 8px height: 24px line-height: 24px border-radius: 14px background: rgba(0, 0, 0, 0.2) text-align: center .count vertical-align: top font-size: 10px .icon-keyboard_arrow_right margin-left: 2px line-height: 24px font-size: 10px .bulletin-wrapper position: relative height: 28px line-height: 28px padding: 0 22px 0 12px white-space: nowrap overflow: hidden text-overflow: ellipsis background: rgba(7, 17, 27, 0.2) .bulletin-title display: inline-block vertical-align: top margin-top: 8px width: 22px height: 12px bg-image('bulletin') background-size: 22px 12px background-repeat: no-repeat .bulletin-text vertical-align: top margin: 0 4px font-size: 10px .icon-keyboard_arrow_right position: absolute font-size: 10px right: 12px top: 8px </style>
外部组件(六)
继续编写header.vue文件,增加顶部蒙层效果
<template> <div class="header"> <div class="content-wrapper"> <div class="avatar"> <img width="64" height="64" :src="seller.avatar"> </div> <div class="content"> <div class="title"> <span class="brand"></span> <span class="name">{{seller.name}}</span> </div> <div class="description"> {{seller.description}}/{{seller.deliveryTime}}分钟送达 </div> <div v-if="seller.supports" class="support"> <span class="icon" :class="classMap[seller.supports[0].type]"></span> <span class="text">{{seller.supports[0].description}}</span> </div> </div> <div class="support-count" v-if="seller.supports"> <span class="count">{{seller.supports.length}}</span> <i class="icon-keyboard_arrow_right"></i> </div> </div> <div class="bulletin-wrapper"> <span class="bulletin-title"></span> <span class="bulletin-text">{{seller.bulletin}}</span> <i class="icon-keyboard_arrow_right"></i> </div> <div class="background"> <img :src="seller.avatar" width="100%" height="100%"> </div> </div> </template> <script> export default { name: 'v-header', props: { seller: { type: Object } }, created() { this.classMap = ['decrease','discount','special','invoice','guarantee']; } } </script> <style scoped lang="stylus" rel="stylesheet/stylus"> @import "../../common/stylus/mixin"; .header position: relative overflow: hidden color: #fff background: rgba(7,17,27,0.5) .content-wrapper position: relative padding: 24px 12px 18px 24px font-size: 0 .avatar display: inline-block vertical-align: top img border-radius: 2px .content display: inline-block margin-left: 16px .title margin: 2px 0 8px 0 .brand display: inline-block vertical-align: top width: 30px height:18px bg-image("brand") background-size: 30px 18px background-repeat: no-repeat .name margin-left: 6px font-size: 16px line-height: 18px font-weight: bold .description margin-bottom: 10px line-height: 12px font-size: 12px .support .icon display: inline-block vertical-align: top height: 12px width: 12px margin-right: 4px background-size: 12px 12px background-repeat: no-repeat &.decrease bg-image('decrease_1') &.discount bg-image('discount_1') &.guarantee bg-image('guarantee_1') &.invoice bg-image('invoice_1') &.special bg-image('special_1') .text font-size: 10px line-height: 12px .support-count position: absolute right: 12px bottom: 14px padding: 0 8px height: 24px line-height: 24px border-radius: 14px background: rgba(0, 0, 0, 0.2) text-align: center .count vertical-align: top font-size: 10px .icon-keyboard_arrow_right margin-left: 2px line-height: 24px font-size: 10px .bulletin-wrapper position: relative height: 28px line-height: 28px padding: 0 22px 0 12px white-space: nowrap overflow: hidden text-overflow: ellipsis background: rgba(7, 17, 27, 0.2) .bulletin-title display: inline-block vertical-align: top margin-top: 8px width: 22px height: 12px bg-image('bulletin') background-size: 22px 12px background-repeat: no-repeat .bulletin-text vertical-align: top margin: 0 4px font-size: 10px .icon-keyboard_arrow_right position: absolute font-size: 10px right: 12px top: 8px .background position: absolute top: 0 left: 0 width: 100% height: 100% z-index: -1 /*增加模糊度*/ filter: blur(5px) </style>
详情弹层页(一)
继续编写header.vue文件,增加详情弹层效果
<template> <div class="header"> <div class="content-wrapper"> <div class="avatar"> <img width="64" height="64" :src="seller.avatar"> </div> <div class="content"> <div class="title"> <span class="brand"></span> <span class="name">{{seller.name}}</span> </div> <div class="description"> {{seller.description}}/{{seller.deliveryTime}}分钟送达 </div> <div v-if="seller.supports" class="support"> <span class="icon" :class="classMap[seller.supports[0].type]"></span> <span class="text">{{seller.supports[0].description}}</span> </div> </div> <div class="support-count" v-if="seller.supports" @click="showDetail"> <span class="count">{{seller.supports.length}}</span> <i class="icon-keyboard_arrow_right"></i> </div> </div> <div class="bulletin-wrapper" @click="showDetail"> <span class="bulletin-title"></span> <span class="bulletin-text">{{seller.bulletin}}</span> <i class="icon-keyboard_arrow_right"></i> </div> <div class="background"> <img :src="seller.avatar" width="100%" height="100%"> </div> <div class="detail" v-show="detailShow"></div> </div> </template> <script> export default { name: 'v-header', props: { seller: { type: Object } }, data() { return { detailShow: false }; }, methods: { showDetail() { this.detailShow = true; } }, created() { this.classMap = ['decrease','discount','special','invoice','guarantee']; } } </script> <style scoped lang="stylus" rel="stylesheet/stylus"> @import "../../common/stylus/mixin"; .header position: relative overflow: hidden color: #fff background: rgba(7,17,27,0.5) .content-wrapper position: relative padding: 24px 12px 18px 24px font-size: 0 .avatar display: inline-block vertical-align: top img border-radius: 2px .content display: inline-block margin-left: 16px .title margin: 2px 0 8px 0 .brand display: inline-block vertical-align: top width: 30px height:18px bg-image("brand") background-size: 30px 18px background-repeat: no-repeat .name margin-left: 6px font-size: 16px line-height: 18px font-weight: bold .description margin-bottom: 10px line-height: 12px font-size: 12px .support .icon display: inline-block vertical-align: top height: 12px width: 12px margin-right: 4px background-size: 12px 12px background-repeat: no-repeat &.decrease bg-image('decrease_1') &.discount bg-image('discount_1') &.guarantee bg-image('guarantee_1') &.invoice bg-image('invoice_1') &.special bg-image('special_1') .text font-size: 10px line-height: 12px .support-count position: absolute right: 12px bottom: 14px padding: 0 8px height: 24px line-height: 24px border-radius: 14px background: rgba(0, 0, 0, 0.2) text-align: center .count vertical-align: top font-size: 10px .icon-keyboard_arrow_right margin-left: 2px line-height: 24px font-size: 10px .bulletin-wrapper position: relative height: 28px line-height: 28px padding: 0 22px 0 12px white-space: nowrap overflow: hidden text-overflow: ellipsis background: rgba(7, 17, 27, 0.2) .bulletin-title display: inline-block vertical-align: top margin-top: 8px width: 22px height: 12px bg-image('bulletin') background-size: 22px 12px background-repeat: no-repeat .bulletin-text vertical-align: top margin: 0 4px font-size: 10px .icon-keyboard_arrow_right position: absolute font-size: 10px right: 12px top: 8px .background position: absolute top: 0 left: 0 width: 100% height: 100% z-index: -1 /*增加模糊度*/ filter: blur(5px) .detail position: fixed top: 0 left: 0 z-index: 100 height: 100% width: 100% overflow: auto backdrop-filter: blur(10px) opacity: 1 background: rgba(7, 17, 27, 0.8) </style>
详情弹层页(二)
继续编写header.vue文件,设置弹窗关闭按钮,利用sticky footers知识
<template> <div class="header"> <div class="content-wrapper"> <div class="avatar"> <img width="64" height="64" :src="seller.avatar"> </div> <div class="content"> <div class="title"> <span class="brand"></span> <span class="name">{{seller.name}}</span> </div> <div class="description"> {{seller.description}}/{{seller.deliveryTime}}分钟送达 </div> <div v-if="seller.supports" class="support"> <span class="icon" :class="classMap[seller.supports[0].type]"></span> <span class="text">{{seller.supports[0].description}}</span> </div> </div> <div class="support-count" v-if="seller.supports" @click="showDetail"> <span class="count">{{seller.supports.length}}</span> <i class="icon-keyboard_arrow_right"></i> </div> </div> <div class="bulletin-wrapper" @click="showDetail"> <span class="bulletin-title"></span> <span class="bulletin-text">{{seller.bulletin}}</span> <i class="icon-keyboard_arrow_right"></i> </div> <div class="background"> <img :src="seller.avatar" width="100%" height="100%"> </div> <div class="detail" v-show="detailShow"> <div class="detail-wrapper clearfix"> <div class="detail-main"></div> </div> <div class="detail-close"> <i class="icon-close"></i> </div> </div> </div> </template> <script> export default { name: 'v-header', props: { seller: { type: Object } }, data() { return { detailShow: false }; }, methods: { showDetail() { this.detailShow = true; } }, created() { this.classMap = ['decrease','discount','special','invoice','guarantee']; } } </script> <style scoped lang="stylus" rel="stylesheet/stylus"> @import "../../common/stylus/mixin"; .header position: relative overflow: hidden color: #fff background: rgba(7,17,27,0.5) .content-wrapper position: relative padding: 24px 12px 18px 24px font-size: 0 .avatar display: inline-block vertical-align: top img border-radius: 2px .content display: inline-block margin-left: 16px .title margin: 2px 0 8px 0 .brand display: inline-block vertical-align: top width: 30px height:18px bg-image("brand") background-size: 30px 18px background-repeat: no-repeat .name margin-left: 6px font-size: 16px line-height: 18px font-weight: bold .description margin-bottom: 10px line-height: 12px font-size: 12px .support .icon display: inline-block vertical-align: top height: 12px width: 12px margin-right: 4px background-size: 12px 12px background-repeat: no-repeat &.decrease bg-image('decrease_1') &.discount bg-image('discount_1') &.guarantee bg-image('guarantee_1') &.invoice bg-image('invoice_1') &.special bg-image('special_1') .text font-size: 10px line-height: 12px .support-count position: absolute right: 12px bottom: 14px padding: 0 8px height: 24px line-height: 24px border-radius: 14px background: rgba(0, 0, 0, 0.2) text-align: center .count vertical-align: top font-size: 10px .icon-keyboard_arrow_right margin-left: 2px line-height: 24px font-size: 10px .bulletin-wrapper position: relative height: 28px line-height: 28px padding: 0 22px 0 12px white-space: nowrap overflow: hidden text-overflow: ellipsis background: rgba(7, 17, 27, 0.2) .bulletin-title display: inline-block vertical-align: top margin-top: 8px width: 22px height: 12px bg-image('bulletin') background-size: 22px 12px background-repeat: no-repeat .bulletin-text vertical-align: top margin: 0 4px font-size: 10px .icon-keyboard_arrow_right position: absolute font-size: 10px right: 12px top: 8px .background position: absolute top: 0 left: 0 width: 100% height: 100% z-index: -1 /*增加模糊度*/ filter: blur(5px) .detail position: fixed top: 0 left: 0 z-index: 100 height: 100% width: 100% overflow: auto background: rgba(7, 17, 27, 0.8) .detail-wrapper min-height: 100% width: 100% .detail-main margin-top: 64px padding-bottom: 64px .detail-close position: relative width: 32px height: 32px margin: -64px auto 0 auto clear: both font-size: 32px </style>
编写base.styl文件,设置清除浮动样式
body, html line-height: 1 font-weight: 200 font-family: 'PingFang SC', 'STHeitiSC-Light', 'Helvetica-Light', arial, sans-serif .clearfix display: inline-block &:after display: block content: "." height: 0 line-height: 0 clear: both visibility: hidden @media (-webkit-min-device-pixel-ratio: 1.5),(min-device-pixel-ratio: 1.5) .border-1px &::after -webkit-transform: scaleY(0.7) transform: scaleY(0.7) @media (-webkit-min-device-pixel-ratio: 2),(min-device-pixel-ratio: 2) .border-1px &::after -webkit-transform: scaleY(0.5) transform: scaleY(0.5)
详情弹层页(三、四)
步骤一:新建一个通用的组件star.vue
<template> <div class="star" :class="starType"> <span v-for="itemClass in itemClasses" :class="itemClass" class="star-item"></span> </div> </template> <script> const LENGTH = 5; const CLS_ON = 'on'; const CLS_HALF = 'half'; const CLS_OFF = 'off'; export default { props: { size: { type: Number }, score: { type: Number } }, computed: { starType() { return 'star-' + this.size; }, itemClasses() { let result = []; let score = Math.floor(this.score * 2) / 2; let hasDecimal = score % 1 !== 0; let integer = Math.floor(score); for (let i = 0; i < integer; i++) { result.push(CLS_ON); } if (hasDecimal) { result.push(CLS_HALF); } while (result.length < LENGTH) { result.push(CLS_OFF); } return result; } } }; </script> <style lang="stylus" rel="stylesheet/stylus"> @import "../../common/stylus/mixin.styl" .star font-size: 0 .star-item display: inline-block background-repeat: no-repeat &.star-48 .star-item width: 20px height: 20px margin-right: 22px background-size: 20px 20px &:last-child margin-right: 0 &.on bg-image('star48_on') &.half bg-image('star48_half') &.off bg-image('star48_off') &.star-36 .star-item width: 15px height: 15px margin-right: 6px background-size: 15px 15px &:last-child margin-right: 0 &.on bg-image('star36_on') &.half bg-image('star36_half') &.off bg-image('star36_off') &.star-24 .star-item width: 10px height: 10px margin-right: 3px background-size: 10px 10px &:last-child margin-right: 0 &.on bg-image('star24_on') &.half bg-image('star24_half') &.off bg-image('star24_off') </style>
步骤二:继续编写header.vue文件
<template> <div class="header"> <div class="content-wrapper"> <div class="avatar"> <img width="64" height="64" :src="seller.avatar"> </div> <div class="content"> <div class="title"> <span class="brand"></span> <span class="name">{{seller.name}}</span> </div> <div class="description"> {{seller.description}}/{{seller.deliveryTime}}分钟送达 </div> <div v-if="seller.supports" class="support"> <span class="icon" :class="classMap[seller.supports[0].type]"></span> <span class="text">{{seller.supports[0].description}}</span> </div> </div> <div class="support-count" v-if="seller.supports" @click="showDetail"> <span class="count">{{seller.supports.length}}</span> <i class="icon-keyboard_arrow_right"></i> </div> </div> <div class="bulletin-wrapper" @click="showDetail"> <span class="bulletin-title"></span> <span class="bulletin-text">{{seller.bulletin}}</span> <i class="icon-keyboard_arrow_right"></i> </div> <div class="background"> <img :src="seller.avatar" width="100%" height="100%"> </div> <div class="detail" v-show="detailShow"> <div class="detail-wrapper clearfix"> <div class="detail-main"> <h1 class="name">{{seller.name}}</h1> <div class="star-wrapper"> <star :size="36" :score="seller.score"></star> </div> </div> </div> <div class="detail-close"> <i class="icon-close"></i> </div> </div> </div> </template> <script> import star from '../../components/star/star'; export default { name: 'v-header', props: { seller: { type: Object } }, data() { return { detailShow: false }; }, methods: { showDetail() { this.detailShow = true; } }, created() { this.classMap = ['decrease','discount','special','invoice','guarantee']; }, components: { star } } </script> <style scoped lang="stylus" rel="stylesheet/stylus"> @import "../../common/stylus/mixin"; .header position: relative overflow: hidden color: #fff background: rgba(7,17,27,0.5) .content-wrapper position: relative padding: 24px 12px 18px 24px font-size: 0 .avatar display: inline-block vertical-align: top img border-radius: 2px .content display: inline-block margin-left: 16px .title margin: 2px 0 8px 0 .brand display: inline-block vertical-align: top width: 30px height:18px bg-image("brand") background-size: 30px 18px background-repeat: no-repeat .name margin-left: 6px font-size: 16px line-height: 18px font-weight: bold .description margin-bottom: 10px line-height: 12px font-size: 12px .support .icon display: inline-block vertical-align: top height: 12px width: 12px margin-right: 4px background-size: 12px 12px background-repeat: no-repeat &.decrease bg-image('decrease_1') &.discount bg-image('discount_1') &.guarantee bg-image('guarantee_1') &.invoice bg-image('invoice_1') &.special bg-image('special_1') .text font-size: 10px line-height: 12px .support-count position: absolute right: 12px bottom: 14px padding: 0 8px height: 24px line-height: 24px border-radius: 14px background: rgba(0, 0, 0, 0.2) text-align: center .count vertical-align: top font-size: 10px .icon-keyboard_arrow_right margin-left: 2px line-height: 24px font-size: 10px .bulletin-wrapper position: relative height: 28px line-height: 28px padding: 0 22px 0 12px white-space: nowrap overflow: hidden text-overflow: ellipsis background: rgba(7, 17, 27, 0.2) .bulletin-title display: inline-block vertical-align: top margin-top: 8px width: 22px height: 12px bg-image('bulletin') background-size: 22px 12px background-repeat: no-repeat .bulletin-text vertical-align: top margin: 0 4px font-size: 10px .icon-keyboard_arrow_right position: absolute font-size: 10px right: 12px top: 8px .background position: absolute top: 0 left: 0 width: 100% height: 100% z-index: -1 /*增加模糊度*/ filter: blur(5px) .detail position: fixed top: 0 left: 0 z-index: 100 height: 100% width: 100% overflow: auto background: rgba(7, 17, 27, 0.8) .detail-wrapper min-height: 100% width: 100% .detail-main margin-top: 64px padding-bottom: 64px .name line-height: 16px text-align: center font-size: 16px font-weight: 700 .star-wrapper margin-top: 18px padding: 2px 0 text-align: center .detail-close position: relative width: 32px height: 32px margin: -64px auto 0 auto clear: both font-size: 32px </style>
详情弹层页(五)
继续编写header.vue文件,增加响应式的水平线条
<template> <div class="header"> <div class="content-wrapper"> <div class="avatar"> <img width="64" height="64" :src="seller.avatar"> </div> <div class="content"> <div class="title"> <span class="brand"></span> <span class="name">{{seller.name}}</span> </div> <div class="description"> {{seller.description}}/{{seller.deliveryTime}}分钟送达 </div> <div v-if="seller.supports" class="support"> <span class="icon" :class="classMap[seller.supports[0].type]"></span> <span class="text">{{seller.supports[0].description}}</span> </div> </div> <div class="support-count" v-if="seller.supports" @click="showDetail"> <span class="count">{{seller.supports.length}}</span> <i class="icon-keyboard_arrow_right"></i> </div> </div> <div class="bulletin-wrapper" @click="showDetail"> <span class="bulletin-title"></span> <span class="bulletin-text">{{seller.bulletin}}</span> <i class="icon-keyboard_arrow_right"></i> </div> <div class="background"> <img :src="seller.avatar" width="100%" height="100%"> </div> <div class="detail" v-show="detailShow"> <div class="detail-wrapper clearfix"> <div class="detail-main"> <h1 class="name">{{seller.name}}</h1> <div class="star-wrapper"> <star :size="36" :score="seller.score"></star> </div> <div class="title"> <div class="line"></div> <div class="text">优惠信息</div> <div class="line"></div> </div> </div> </div> <div class="detail-close"> <i class="icon-close"></i> </div> </div> </div> </template> <script> import star from '../../components/star/star'; export default { name: 'v-header', props: { seller: { type: Object } }, data() { return { detailShow: false }; }, methods: { showDetail() { this.detailShow = true; } }, created() { this.classMap = ['decrease','discount','special','invoice','guarantee']; }, components: { star } } </script> <style scoped lang="stylus" rel="stylesheet/stylus"> @import "../../common/stylus/mixin"; .header position: relative overflow: hidden color: #fff background: rgba(7,17,27,0.5) .content-wrapper position: relative padding: 24px 12px 18px 24px font-size: 0 .avatar display: inline-block vertical-align: top img border-radius: 2px .content display: inline-block margin-left: 16px .title margin: 2px 0 8px 0 .brand display: inline-block vertical-align: top width: 30px height:18px bg-image("brand") background-size: 30px 18px background-repeat: no-repeat .name margin-left: 6px font-size: 16px line-height: 18px font-weight: bold .description margin-bottom: 10px line-height: 12px font-size: 12px .support .icon display: inline-block vertical-align: top height: 12px width: 12px margin-right: 4px background-size: 12px 12px background-repeat: no-repeat &.decrease bg-image('decrease_1') &.discount bg-image('discount_1') &.guarantee bg-image('guarantee_1') &.invoice bg-image('invoice_1') &.special bg-image('special_1') .text font-size: 10px line-height: 12px .support-count position: absolute right: 12px bottom: 14px padding: 0 8px height: 24px line-height: 24px border-radius: 14px background: rgba(0, 0, 0, 0.2) text-align: center .count vertical-align: top font-size: 10px .icon-keyboard_arrow_right margin-left: 2px line-height: 24px font-size: 10px .bulletin-wrapper position: relative height: 28px line-height: 28px padding: 0 22px 0 12px white-space: nowrap overflow: hidden text-overflow: ellipsis background: rgba(7, 17, 27, 0.2) .bulletin-title display: inline-block vertical-align: top margin-top: 8px width: 22px height: 12px bg-image('bulletin') background-size: 22px 12px background-repeat: no-repeat .bulletin-text vertical-align: top margin: 0 4px font-size: 10px .icon-keyboard_arrow_right position: absolute font-size: 10px right: 12px top: 8px .background position: absolute top: 0 left: 0 width: 100% height: 100% z-index: -1 /*增加模糊度*/ filter: blur(5px) .detail position: fixed top: 0 left: 0 z-index: 100 height: 100% width: 100% overflow: auto background: rgba(7, 17, 27, 0.8) .detail-wrapper min-height: 100% width: 100% .detail-main margin-top: 64px padding-bottom: 64px .name line-height: 16px text-align: center font-size: 16px font-weight: 700 .star-wrapper margin-top: 18px padding: 2px 0 text-align: center .title display: flex width: 80% margin: 28px auto 24px auto .line flex: 1 position: relative top: -6px border-bottom: 1px solid rgba(255, 255, 255, 0.2) .text padding: 0 12px font-weight: 700 font-size: 14px .detail-close position: relative width: 32px height: 32px margin: -64px auto 0 auto clear: both font-size: 32px </style>
食品组件布局(一)
创建goods.vue组件,首先编写食品左侧内容
<template> <div class="goods"> <div class="menu-wrapper"> <ul> <li v-for="item in goods" class="menu-item"> <span class="text border-1px"> <span v-show="item.type>0" class="icon" :class="classMap[item.type]"></span> {{item.name}} </span> </li> </ul> </div> <div class="foods-wrapper"> </div> </div> </template> <script> const ERR_OK = 0; export default { name: 'v-goods', props: { seller: { type: Object } }, data() { return { goods: [] }; }, created() { this.classMap = ['decrease','discount','special','invoice','guarantee']; this.$http.get('api/goods').then((response) => { response = response.body; if(response.error === ERR_OK){ this.goods = response.data; } console.log(response.data); this.goods = response.data; }) } } </script> <style scoped lang="stylus" rel="stylesheet/stylus"> @import "../../common/stylus/mixin"; .goods display: flex position: absolute top: 174px bottom: 46px width: 100% overflow: hidden .menu-wrapper flex: 0 0 80px width: 80px background: #f3f5f7 .menu-item display: table height: 54px width: 56px padding: 0 12px line-height: 14px .icon display: inline-block vertical-align: top width: 12px height: 12px margin-right: 2px background-size: 12px 12px background-repeat: no-repeat &.decrease bg-image('decrease_3') &.discount bg-image('discount_3') &.guarantee bg-image('guarantee_3') &.invoice bg-image('invoice_3') &.special bg-image('special_3') .text display: table-cell width: 56px vertical-align: middle border-1px(rgba(7,17,27,0.1)) font-size: 12px .foods-wrapper flex: 1 </style>
食品组件布局(二)
编写goods.vue组件,增加右侧内容
<template> <div class="goods"> <div class="menu-wrapper"> <ul> <li v-for="item in goods" class="menu-item"> <span class="text border-1px"> <span v-show="item.type>0" class="icon" :class="classMap[item.type]"></span> {{item.name}} </span> </li> </ul> </div> <div class="foods-wrapper"> <ul> <li v-for="item in goods" class="food-list"> <h1 class="title">{{item.name}}</h1> <ul> <li v-for="food in item.foods" class="food-item"> <div class="icon"> <img :src="food.icon" /> </div> <div class="content"> <h2 class="name">{{food.name}}</h2> <p class="desc">{{food.description}}</p> <div class="extra"> <span>月售{{food.sellCount}}份</span> <span>好评率{{food.rating}}%</span> </div> <div class="price"> <span>¥{{food.price}}</span> <span v-show="food.oldPrice">¥{{food.oldPrice}}</span> </div> </div> </li> </ul> </li> </ul> </div> </div> </template> <script> const ERR_OK = 0; export default { name: 'v-goods', props: { seller: { type: Object } }, data() { return { goods: [] }; }, created() { this.classMap = ['decrease','discount','special','invoice','guarantee']; this.$http.get('api/goods').then((response) => { response = response.body; if(response.error === ERR_OK){ this.goods = response.data; } console.log(response.data); this.goods = response.data; }) } } </script> <style scoped lang="stylus" rel="stylesheet/stylus"> @import "../../common/stylus/mixin"; .goods display: flex position: absolute top: 174px bottom: 46px width: 100% overflow: hidden .menu-wrapper flex: 0 0 80px width: 80px background: #f3f5f7 .menu-item display: table height: 54px width: 56px padding: 0 12px line-height: 14px .icon display: inline-block vertical-align: top width: 12px height: 12px margin-right: 2px background-size: 12px 12px background-repeat: no-repeat &.decrease bg-image('decrease_3') &.discount bg-image('discount_3') &.guarantee bg-image('guarantee_3') &.invoice bg-image('invoice_3') &.special bg-image('special_3') .text display: table-cell width: 56px vertical-align: middle border-1px(rgba(7,17,27,0.1)) font-size: 12px .foods-wrapper flex: 1 </style>
食品组件布局(三)
继续编写goods.vue组件,增加样式
<template> <div class="goods"> <div class="menu-wrapper"> <ul> <li v-for="item in goods" class="menu-item"> <span class="text border-1px"> <span v-show="item.type>0" class="icon" :class="classMap[item.type]"></span> {{item.name}} </span> </li> </ul> </div> <div class="foods-wrapper"> <ul> <li v-for="item in goods" class="food-list"> <h1 class="title">{{item.name}}</h1> <ul> <li v-for="food in item.foods" class="food-item border-1px"> <div class="icon"> <img :src="food.icon" width="57" height="57" /> </div> <div class="content"> <h2 class="name">{{food.name}}</h2> <p class="desc">{{food.description}}</p> <div class="extra"> <span>月售{{food.sellCount}}份</span> <span>好评率{{food.rating}}%</span> </div> <div class="price"> <span>¥{{food.price}}</span> <span v-show="food.oldPrice">¥{{food.oldPrice}}</span> </div> </div> </li> </ul> </li> </ul> </div> </div> </template> <script> const ERR_OK = 0; export default { name: 'v-goods', props: { seller: { type: Object } }, data() { return { goods: [] }; }, created() { this.classMap = ['decrease','discount','special','invoice','guarantee']; this.$http.get('api/goods').then((response) => { response = response.body; if(response.error === ERR_OK){ this.goods = response.data; } console.log(response.data); this.goods = response.data; }) } } </script> <style scoped lang="stylus" rel="stylesheet/stylus"> @import "../../common/stylus/mixin"; .goods display: flex position: absolute top: 174px bottom: 46px width: 100% overflow: hidden .menu-wrapper flex: 0 0 80px width: 80px background: #f3f5f7 .menu-item display: table height: 54px width: 56px padding: 0 12px line-height: 14px .icon display: inline-block vertical-align: top width: 12px height: 12px margin-right: 2px background-size: 12px 12px background-repeat: no-repeat &.decrease bg-image('decrease_3') &.discount bg-image('discount_3') &.guarantee bg-image('guarantee_3') &.invoice bg-image('invoice_3') &.special bg-image('special_3') .text display: table-cell width: 56px vertical-align: middle border-1px(rgba(7,17,27,0.1)) font-size: 12px .foods-wrapper flex: 1 .title padding-left: 14px height: 26px line-height: 26px border-left: 2px solid #d9dde1 font-size: 12px color: rgb(147,153,159) background: #f3f5f7 .food-item display: flex margin: 18px padding-bottom: 18px border-1px(rgba(7,17,27,0.1)) &:last-child border-none() margin-bottom: 0 .icon flex: 0 0 57px margin-right: 10px .content flex: 1 .name margin: 2px 0 8px 0 height: 14px line-height: 14px font-size: 14px color: rgb(7,17,27) </style>
食品组件布局(四)
继续编写goods.vue组件,完成样式部分
<template> <div class="goods"> <div class="menu-wrapper"> <ul> <li v-for="item in goods" class="menu-item"> <span class="text border-1px"> <span v-show="item.type>0" class="icon" :class="classMap[item.type]"></span> {{item.name}} </span> </li> </ul> </div> <div class="foods-wrapper"> <ul> <li v-for="item in goods" class="food-list"> <h1 class="title">{{item.name}}</h1> <ul> <li v-for="food in item.foods" class="food-item border-1px"> <div class="icon"> <img :src="food.icon" width="57" height="57" /> </div> <div class="content"> <h2 class="name">{{food.name}}</h2> <p class="desc">{{food.description}}</p> <div class="extra"> <span class="count">月售{{food.sellCount}}份</span> <span>好评率{{food.rating}}%</span> </div> <div class="price"> <span class="now">¥{{food.price}}</span> <span class="old" v-show="food.oldPrice">¥{{food.oldPrice}}</span> </div> </div> </li> </ul> </li> </ul> </div> </div> </template> <script> const ERR_OK = 0; export default { name: 'v-goods', props: { seller: { type: Object } }, data() { return { goods: [] }; }, created() { this.classMap = ['decrease','discount','special','invoice','guarantee']; this.$http.get('api/goods').then((response) => { response = response.body; if(response.error === ERR_OK){ this.goods = response.data; } console.log(response.data); this.goods = response.data; }) } } </script> <style scoped lang="stylus" rel="stylesheet/stylus"> @import "../../common/stylus/mixin"; .goods display: flex position: absolute top: 174px bottom: 46px width: 100% overflow: hidden .menu-wrapper flex: 0 0 80px width: 80px background: #f3f5f7 .menu-item display: table height: 54px width: 56px padding: 0 12px line-height: 14px .icon display: inline-block vertical-align: top width: 12px height: 12px margin-right: 2px background-size: 12px 12px background-repeat: no-repeat &.decrease bg-image('decrease_3') &.discount bg-image('discount_3') &.guarantee bg-image('guarantee_3') &.invoice bg-image('invoice_3') &.special bg-image('special_3') .text display: table-cell width: 56px vertical-align: middle border-1px(rgba(7,17,27,0.1)) font-size: 12px .foods-wrapper flex: 1 .title padding-left: 14px height: 26px line-height: 26px border-left: 2px solid #d9dde1 font-size: 12px color: rgb(147,153,159) background: #f3f5f7 .food-item display: flex margin: 18px padding-bottom: 18px border-1px(rgba(7,17,27,0.1)) &:last-child border-none() margin-bottom: 0 .icon flex: 0 0 57px margin-right: 10px .content flex: 1 .name margin: 2px 0 8px 0 height: 14px line-height: 14px font-size: 14px color: rgb(7,17,27) .desc, .extra line-height: 10px font-size: 10px color: rgb(147,153,159) .desc margin-bottom: 8px .extra &.count margin-right: 12px .price font-weight: 700 line-height: 24px .now margin-right: 8px font-size: 14px color: rgb(240,20,20) .old text-decoration: line-through font-size: 10px color: rgb(147,153,159) </style>
使用better-scroll(一)
插件地址:https://github.com/ustbhuangyi/better-scroll
步骤一:下载安装cnpm install better-scroll --save
步骤二:编写goods.vue组件,让页面滚动起来
<template> <div class="goods"> <div class="menu-wrapper" ref="menuWrapper"> <ul> <li v-for="item in goods" class="menu-item"> <span class="text border-1px"> <span v-show="item.type>0" class="icon" :class="classMap[item.type]"></span> {{item.name}} </span> </li> </ul> </div> <div class="foods-wrapper" ref="foodsWrapper"> <ul> <li v-for="item in goods" class="food-list"> <h1 class="title">{{item.name}}</h1> <ul> <li v-for="food in item.foods" class="food-item border-1px"> <div class="icon"> <img :src="food.icon" width="57" height="57" /> </div> <div class="content"> <h2 class="name">{{food.name}}</h2> <p class="desc">{{food.description}}</p> <div class="extra"> <span class="count">月售{{food.sellCount}}份</span> <span>好评率{{food.rating}}%</span> </div> <div class="price"> <span class="now">¥{{food.price}}</span> <span class="old" v-show="food.oldPrice">¥{{food.oldPrice}}</span> </div> </div> </li> </ul> </li> </ul> </div> </div> </template> <script> import BScroll from 'better-scroll' const ERR_OK = 0; export default { name: 'v-goods', props: { seller: { type: Object } }, data() { return { goods: [] }; }, created() { this.classMap = ['decrease','discount','special','invoice','guarantee']; this.$http.get('api/goods').then((response) => { response = response.body; if(response.error === ERR_OK){ this.goods = response.data; } this.goods = response.data; this.$nextTick(() => { this._initScroll(); }); }); }, methods: { _initScroll() { this.menuScroll = new BScroll(this.$refs.menuWrapper,{}); this.foodsScroll = new BScroll(this.$refs.foodsWrapper,{}); } } } </script> <style scoped lang="stylus" rel="stylesheet/stylus"> @import "../../common/stylus/mixin"; .goods display: flex position: absolute top: 174px bottom: 46px width: 100% overflow: hidden .menu-wrapper flex: 0 0 80px width: 80px background: #f3f5f7 .menu-item display: table height: 54px width: 56px padding: 0 12px line-height: 14px .icon display: inline-block vertical-align: top width: 12px height: 12px margin-right: 2px background-size: 12px 12px background-repeat: no-repeat &.decrease bg-image('decrease_3') &.discount bg-image('discount_3') &.guarantee bg-image('guarantee_3') &.invoice bg-image('invoice_3') &.special bg-image('special_3') .text display: table-cell width: 56px vertical-align: middle border-1px(rgba(7,17,27,0.1)) font-size: 12px .foods-wrapper flex: 1 .title padding-left: 14px height: 26px line-height: 26px border-left: 2px solid #d9dde1 font-size: 12px color: rgb(147,153,159) background: #f3f5f7 .food-item display: flex margin: 18px padding-bottom: 18px border-1px(rgba(7,17,27,0.1)) &:last-child border-none() margin-bottom: 0 .icon flex: 0 0 57px margin-right: 10px .content flex: 1 .name margin: 2px 0 8px 0 height: 14px line-height: 14px font-size: 14px color: rgb(7,17,27) .desc, .extra line-height: 10px font-size: 10px color: rgb(147,153,159) .desc margin-bottom: 8px line-height: 14px .extra .count margin-right: 12px .price font-weight: 700 line-height: 24px .now margin-right: 8px font-size: 14px color: rgb(240,20,20) .old text-decoration: line-through font-size: 10px color: rgb(147,153,159) </style>
使用better-scroll(二)
编写goods.vue组件,监测右侧页面高度
<template> <div class="goods"> <div class="menu-wrapper" ref="menuWrapper"> <ul> <li v-for="item in goods" class="menu-item"> <span class="text border-1px"> <span v-show="item.type>0" class="icon" :class="classMap[item.type]"></span> {{item.name}} </span> </li> </ul> </div> <div class="foods-wrapper" ref="foodsWrapper"> <ul> <li v-for="item in goods" class="food-list food-list-hook"> <h1 class="title">{{item.name}}</h1> <ul> <li v-for="food in item.foods" class="food-item border-1px"> <div class="icon"> <img :src="food.icon" width="57" height="57" /> </div> <div class="content"> <h2 class="name">{{food.name}}</h2> <p class="desc">{{food.description}}</p> <div class="extra"> <span class="count">月售{{food.sellCount}}份</span> <span>好评率{{food.rating}}%</span> </div> <div class="price"> <span class="now">¥{{food.price}}</span> <span class="old" v-show="food.oldPrice">¥{{food.oldPrice}}</span> </div> </div> </li> </ul> </li> </ul> </div> </div> </template> <script> import BScroll from 'better-scroll' const ERR_OK = 0; export default { name: 'v-goods', props: { seller: { type: Object } }, data() { return { goods: [], listHeight: [], scrollY: 0 }; }, computed: { currentIndex() { for(let i=0;i<this.listHeight.length;i++){ let height1 = this.listHeight[i]; let height2 = this.listHeight[i+1]; if (!height2 || (this.scrollY > height1 && this.scrollY <height2)){ return i; } } return 0; } }, created() { this.classMap = ['decrease','discount','special','invoice','guarantee']; this.$http.get('api/goods').then((response) => { response = response.body; if(response.error === ERR_OK){ this.goods = response.data; } this.goods = response.data; this.$nextTick(() => { this._initScroll(); this._calculateHeight(); }); }); }, methods: { _initScroll() { this.menuScroll = new BScroll(this.$refs.menuWrapper,{}); this.foodsScroll = new BScroll(this.$refs.foodsWrapper,{ probeType: 3 }); this.foodsScroll.on("scroll",(pos) => { this.scrollY = Math.abs(Math.round(pos.y)); }); }, _calculateHeight() { let foodList = this.$refs.foodsWrapper.getElementsByClassName('food-list-hook'); let height = 0; this.listHeight.push(height); for(let i=0;i<foodList.length;i++){ let item = foodList[i]; height += item.clientHeight; this.listHeight.push(height); } } } } </script> <style scoped lang="stylus" rel="stylesheet/stylus"> @import "../../common/stylus/mixin"; .goods display: flex position: absolute top: 174px bottom: 46px width: 100% overflow: hidden .menu-wrapper flex: 0 0 80px width: 80px background: #f3f5f7 .menu-item display: table height: 54px width: 56px padding: 0 12px line-height: 14px .icon display: inline-block vertical-align: top width: 12px height: 12px margin-right: 2px background-size: 12px 12px background-repeat: no-repeat &.decrease bg-image('decrease_3') &.discount bg-image('discount_3') &.guarantee bg-image('guarantee_3') &.invoice bg-image('invoice_3') &.special bg-image('special_3') .text display: table-cell width: 56px vertical-align: middle border-1px(rgba(7,17,27,0.1)) font-size: 12px .foods-wrapper flex: 1 .title padding-left: 14px height: 26px line-height: 26px border-left: 2px solid #d9dde1 font-size: 12px color: rgb(147,153,159) background: #f3f5f7 .food-item display: flex margin: 18px padding-bottom: 18px border-1px(rgba(7,17,27,0.1)) &:last-child border-none() margin-bottom: 0 .icon flex: 0 0 57px margin-right: 10px .content flex: 1 .name margin: 2px 0 8px 0 height: 14px line-height: 14px font-size: 14px color: rgb(7,17,27) .desc, .extra line-height: 10px font-size: 10px color: rgb(147,153,159) .desc margin-bottom: 8px line-height: 14px .extra .count margin-right: 12px .price font-weight: 700 line-height: 24px .now margin-right: 8px font-size: 14px color: rgb(240,20,20) .old text-decoration: line-through font-size: 10px color: rgb(147,153,159) </style>
使用better-scroll(三)
编写goods.vue组件,实现右侧滚动左边实时发生变化
<template> <div class="goods"> <div class="menu-wrapper" ref="menuWrapper"> <ul> <li v-for="(item,index) in goods" class="menu-item" :class="{'current':currentIndex===index}"> <span class="text border-1px"> <span v-show="item.type>0" class="icon" :class="classMap[item.type]"></span> {{item.name}} </span> </li> </ul> </div> <div class="foods-wrapper" ref="foodsWrapper"> <ul> <li v-for="item in goods" class="food-list food-list-hook"> <h1 class="title">{{item.name}}</h1> <ul> <li v-for="food in item.foods" class="food-item border-1px"> <div class="icon"> <img :src="food.icon" width="57" height="57" /> </div> <div class="content"> <h2 class="name">{{food.name}}</h2> <p class="desc">{{food.description}}</p> <div class="extra"> <span class="count">月售{{food.sellCount}}份</span> <span>好评率{{food.rating}}%</span> </div> <div class="price"> <span class="now">¥{{food.price}}</span> <span class="old" v-show="food.oldPrice">¥{{food.oldPrice}}</span> </div> </div> </li> </ul> </li> </ul> </div> </div> </template> <script> import BScroll from 'better-scroll' const ERR_OK = 0; export default { name: 'v-goods', props: { seller: { type: Object } }, data() { return { goods: [], listHeight: [], scrollY: 0 }; }, computed: { currentIndex() { for(let i=0;i<this.listHeight.length;i++){ let height1 = this.listHeight[i]; let height2 = this.listHeight[i+1]; if (!height2 || (this.scrollY >= height1 && this.scrollY < height2)){ return i; } } return 0; } }, created() { this.classMap = ['decrease','discount','special','invoice','guarantee']; this.$http.get('api/goods').then((response) => { response = response.body; if(response.error === ERR_OK){ this.goods = response.data; } this.goods = response.data; this.$nextTick(() => { this._initScroll(); this._calculateHeight(); }); }); }, methods: { _initScroll() { this.menuScroll = new BScroll(this.$refs.menuWrapper,{}); this.foodsScroll = new BScroll(this.$refs.foodsWrapper,{ probeType: 3 }); this.foodsScroll.on("scroll",(pos) => { this.scrollY = Math.abs(Math.round(pos.y)); }); }, _calculateHeight() { let foodList = this.$refs.foodsWrapper.getElementsByClassName('food-list-hook'); let height = 0; this.listHeight.push(height); for(let i=0;i<foodList.length;i++){ let item = foodList[i]; height += item.clientHeight; this.listHeight.push(height); } } } } </script> <style scoped lang="stylus" rel="stylesheet/stylus"> @import "../../common/stylus/mixin"; .goods display: flex position: absolute top: 174px bottom: 46px width: 100% overflow: hidden .menu-wrapper flex: 0 0 80px width: 80px background: #f3f5f7 .menu-item display: table height: 54px width: 56px padding: 0 12px line-height: 14px &.current position: relative z-index: 10 margin-top: -1px background: #ccc font-weight: 700 .text border-none() .icon display: inline-block vertical-align: top width: 12px height: 12px margin-right: 2px background-size: 12px 12px background-repeat: no-repeat &.decrease bg-image('decrease_3') &.discount bg-image('discount_3') &.guarantee bg-image('guarantee_3') &.invoice bg-image('invoice_3') &.special bg-image('special_3') .text display: table-cell width: 56px vertical-align: middle border-1px(rgba(7,17,27,0.1)) font-size: 12px .foods-wrapper flex: 1 .title padding-left: 14px height: 26px line-height: 26px border-left: 2px solid #d9dde1 font-size: 12px color: rgb(147,153,159) background: #f3f5f7 .food-item display: flex margin: 18px padding-bottom: 18px border-1px(rgba(7,17,27,0.1)) &:last-child border-none() margin-bottom: 0 .icon flex: 0 0 57px margin-right: 10px .content flex: 1 .name margin: 2px 0 8px 0 height: 14px line-height: 14px font-size: 14px color: rgb(7,17,27) .desc, .extra line-height: 10px font-size: 10px color: rgb(147,153,159) .desc margin-bottom: 8px line-height: 14px .extra .count margin-right: 12px .price font-weight: 700 line-height: 24px .now margin-right: 8px font-size: 14px color: rgb(240,20,20) .old text-decoration: line-through font-size: 10px color: rgb(147,153,159) </style>
使用better-scroll(四)
编写goods.vue组价,完全实现左右联动
<template> <div class="goods"> <div class="menu-wrapper" ref="menuWrapper"> <ul> <li v-for="(item,index) in goods" class="menu-item" :class="{'current':currentIndex===index}" @click="selectMenu(index,$event)"> <span class="text border-1px"> <span v-show="item.type>0" class="icon" :class="classMap[item.type]"></span> {{item.name}} </span> </li> </ul> </div> <div class="foods-wrapper" ref="foodsWrapper"> <ul> <li v-for="item in goods" class="food-list food-list-hook"> <h1 class="title">{{item.name}}</h1> <ul> <li v-for="food in item.foods" class="food-item border-1px"> <div class="icon"> <img :src="food.icon" width="57" height="57" /> </div> <div class="content"> <h2 class="name">{{food.name}}</h2> <p class="desc">{{food.description}}</p> <div class="extra"> <span class="count">月售{{food.sellCount}}份</span> <span>好评率{{food.rating}}%</span> </div> <div class="price"> <span class="now">¥{{food.price}}</span> <span class="old" v-show="food.oldPrice">¥{{food.oldPrice}}</span> </div> </div> </li> </ul> </li> </ul> </div> </div> </template> <script> import BScroll from 'better-scroll' const ERR_OK = 0; export default { name: 'v-goods', props: { seller: { type: Object } }, data() { return { goods: [], listHeight: [], scrollY: 0 }; }, computed: { currentIndex() { for(let i=0;i<this.listHeight.length;i++){ let height1 = this.listHeight[i]; let height2 = this.listHeight[i+1]; if (!height2 || (this.scrollY >= height1 && this.scrollY < height2)){ return i; } } return 0; } }, created() { this.classMap = ['decrease','discount','special','invoice','guarantee']; this.$http.get('api/goods').then((response) => { response = response.body; if(response.error === ERR_OK){ this.goods = response.data; } this.goods = response.data; this.$nextTick(() => { this._initScroll(); this._calculateHeight(); }); }); }, methods: { selectMenu(index,event){ if(!event._constructed){ return; } let foodList = this.$refs.foodsWrapper.getElementsByClassName('food-list-hook'); let el = foodList[index]; this.foodsScroll.scrollToElement(el,300); console.log(index); }, _initScroll() { this.menuScroll = new BScroll(this.$refs.menuWrapper,{ click: true }); this.foodsScroll = new BScroll(this.$refs.foodsWrapper,{ click: true, probeType: 3 }); this.foodsScroll.on("scroll",(pos) => { this.scrollY = Math.abs(Math.round(pos.y)); }); }, _calculateHeight() { let foodList = this.$refs.foodsWrapper.getElementsByClassName('food-list-hook'); let height = 0; this.listHeight.push(height); for(let i=0;i<foodList.length;i++){ let item = foodList[i]; height += item.clientHeight; this.listHeight.push(height); } } } } </script> <style scoped lang="stylus" rel="stylesheet/stylus"> @import "../../common/stylus/mixin"; .goods display: flex position: absolute top: 174px bottom: 46px width: 100% overflow: hidden .menu-wrapper flex: 0 0 80px width: 80px background: #f3f5f7 .menu-item display: table height: 54px width: 56px padding: 0 12px line-height: 14px &.current position: relative z-index: 10 margin-top: -1px background: #ccc font-weight: 700 .text border-none() .icon display: inline-block vertical-align: top width: 12px height: 12px margin-right: 2px background-size: 12px 12px background-repeat: no-repeat &.decrease bg-image('decrease_3') &.discount bg-image('discount_3') &.guarantee bg-image('guarantee_3') &.invoice bg-image('invoice_3') &.special bg-image('special_3') .text display: table-cell width: 56px vertical-align: middle border-1px(rgba(7,17,27,0.1)) font-size: 12px .foods-wrapper flex: 1 .title padding-left: 14px height: 26px line-height: 26px border-left: 2px solid #d9dde1 font-size: 12px color: rgb(147,153,159) background: #f3f5f7 .food-item display: flex margin: 18px padding-bottom: 18px border-1px(rgba(7,17,27,0.1)) &:last-child border-none() margin-bottom: 0 .icon flex: 0 0 57px margin-right: 10px .content flex: 1 .name margin: 2px 0 8px 0 height: 14px line-height: 14px font-size: 14px color: rgb(7,17,27) .desc, .extra line-height: 10px font-size: 10px color: rgb(147,153,159) .desc margin-bottom: 8px line-height: 14px .extra .count margin-right: 12px .price font-weight: 700 line-height: 24px .now margin-right: 8px font-size: 14px color: rgb(240,20,20) .old text-decoration: line-through font-size: 10px color: rgb(147,153,159) </style>
购物车组件(一)
步骤一:创建一个shopcart.vue组件,编写好基础的样式
<template> <div class="shopcart"> <div class="content"> <div class="content-left"> <div class="logo-wrapper"> <div class="logo"> <span class="icon-shopping_cart"></span> </div> </div> <div class="price"></div> <div class="desc"></div> </div> <div class="content-right"></div> </div> </div> </template> <script> export default { name: 'v-goods' } </script> <style scoped lang="stylus" rel="stylesheet/stylus"> .shopcart position: fixed left: 0 bottom: 0 z-index: 50 width: 100% height: 48px background: #000 </style>
步骤二:配置好goods.vue组件
<template> <div class="goods"> <div class="menu-wrapper" ref="menuWrapper"> <ul> <li v-for="(item,index) in goods" class="menu-item" :class="{'current':currentIndex===index}" @click="selectMenu(index,$event)"> <span class="text border-1px"> <span v-show="item.type>0" class="icon" :class="classMap[item.type]"></span> {{item.name}} </span> </li> </ul> </div> <div class="foods-wrapper" ref="foodsWrapper"> <ul> <li v-for="item in goods" class="food-list food-list-hook"> <h1 class="title">{{item.name}}</h1> <ul> <li v-for="food in item.foods" class="food-item border-1px"> <div class="icon"> <img :src="food.icon" width="57" height="57" /> </div> <div class="content"> <h2 class="name">{{food.name}}</h2> <p class="desc">{{food.description}}</p> <div class="extra"> <span class="count">月售{{food.sellCount}}份</span> <span>好评率{{food.rating}}%</span> </div> <div class="price"> <span class="now">¥{{food.price}}</span> <span class="old" v-show="food.oldPrice">¥{{food.oldPrice}}</span> </div> </div> </li> </ul> </li> </ul> </div> <shopcart></shopcart> </div> </template> <script> import BScroll from 'better-scroll'; import shopcart from '../../components/shopcart/shopcart'; const ERR_OK = 0; export default { name: 'v-goods', props: { seller: { type: Object } }, data() { return { goods: [], listHeight: [], scrollY: 0 }; }, computed: { currentIndex() { for(let i=0;i<this.listHeight.length;i++){ let height1 = this.listHeight[i]; let height2 = this.listHeight[i+1]; if (!height2 || (this.scrollY >= height1 && this.scrollY < height2)){ return i; } } return 0; } }, created() { this.classMap = ['decrease','discount','special','invoice','guarantee']; this.$http.get('api/goods').then((response) => { response = response.body; if(response.error === ERR_OK){ this.goods = response.data; } this.goods = response.data; this.$nextTick(() => { this._initScroll(); this._calculateHeight(); }); }); }, methods: { selectMenu(index,event){ if(!event._constructed){ return; } let foodList = this.$refs.foodsWrapper.getElementsByClassName('food-list-hook'); let el = foodList[index]; this.foodsScroll.scrollToElement(el,300); console.log(index); }, _initScroll() { this.menuScroll = new BScroll(this.$refs.menuWrapper,{ click: true }); this.foodsScroll = new BScroll(this.$refs.foodsWrapper,{ click: true, probeType: 3 }); this.foodsScroll.on("scroll",(pos) => { this.scrollY = Math.abs(Math.round(pos.y)); }); }, _calculateHeight() { let foodList = this.$refs.foodsWrapper.getElementsByClassName('food-list-hook'); let height = 0; this.listHeight.push(height); for(let i=0;i<foodList.length;i++){ let item = foodList[i]; height += item.clientHeight; this.listHeight.push(height); } } }, components: { shopcart } } </script> <style scoped lang="stylus" rel="stylesheet/stylus"> @import "../../common/stylus/mixin"; .goods display: flex position: absolute top: 174px bottom: 46px width: 100% overflow: hidden .menu-wrapper flex: 0 0 80px width: 80px background: #f3f5f7 .menu-item display: table height: 54px width: 56px padding: 0 12px line-height: 14px &.current position: relative z-index: 10 margin-top: -1px background: #ccc font-weight: 700 .text border-none() .icon display: inline-block vertical-align: top width: 12px height: 12px margin-right: 2px background-size: 12px 12px background-repeat: no-repeat &.decrease bg-image('decrease_3') &.discount bg-image('discount_3') &.guarantee bg-image('guarantee_3') &.invoice bg-image('invoice_3') &.special bg-image('special_3') .text display: table-cell width: 56px vertical-align: middle border-1px(rgba(7,17,27,0.1)) font-size: 12px .foods-wrapper flex: 1 .title padding-left: 14px height: 26px line-height: 26px border-left: 2px solid #d9dde1 font-size: 12px color: rgb(147,153,159) background: #f3f5f7 .food-item display: flex margin: 18px padding-bottom: 18px border-1px(rgba(7,17,27,0.1)) &:last-child border-none() margin-bottom: 0 .icon flex: 0 0 57px margin-right: 10px .content flex: 1 .name margin: 2px 0 8px 0 height: 14px line-height: 14px font-size: 14px color: rgb(7,17,27) .desc, .extra line-height: 10px font-size: 10px color: rgb(147,153,159) .desc margin-bottom: 8px line-height: 14px .extra .count margin-right: 12px .price font-weight: 700 line-height: 24px .now margin-right: 8px font-size: 14px color: rgb(240,20,20) .old text-decoration: line-through font-size: 10px color: rgb(147,153,159) </style>
购物车组件(二)
继续编写shopcart.vue组件,增加样式
<template> <div class="shopcart"> <div class="content"> <div class="content-left"> <div class="logo-wrapper"> <div class="logo"> <span class="icon-shopping_cart"></span> </div> </div> <div class="price"></div> <div class="desc"></div> </div> <div class="content-right"></div> </div> </div> </template> <script> export default { name: 'v-goods' } </script> <style scoped lang="stylus" rel="stylesheet/stylus"> .shopcart position: fixed left: 0 bottom: 0 z-index: 50 width: 100% height: 48px .content display: flex background: #141d27 font-size: 0 .content-left flex: 1 .logo-wrapper display: inline-block position: relative top:-10px margin: 0 12px padding: 6px width: 56px height: 56px vertical-align: top box-sizing: border-box border-radius: 50% background: #141d27 .logo width: 100% height: 100% border-radius: 50% text-align: center background: #2b343c .icon-shopping_cart font-size: 24px line-height: 44px color: #80858a .price display: inline-block vertical-align: top margin-top: 12px line-height: 24px padding-right: 12px box-sizing: border-box border-right: 1px solid rgba(255,255,255,0.1) font-size: 16px font-weight: 700 .desc display: inline-block .content-right flex: 0 0 105px width: 105px </style>
购物车组件(三)
步骤一:修改App.vue根组价,传递值
<template> <div> <v-header :seller="seller"></v-header> <div class="tab border-1px"> <div class="tab-item"> <router-link to="/goods">商品</router-link> </div> <div class="tab-item"> <router-link to="/ratings">评价</router-link> </div> <div class="tab-item"> <router-link to="/seller">商家</router-link> </div> </div> <router-view :seller="seller"></router-view> </div> </template> <script> import header from './components/header/header.vue'; const ERR_OK = 0; export default { name: 'app', data() { return { seller: {} } }, created() { this.$http.get('api/seller').then((response) => { response = response.body; if(response.error === ERR_OK){ this.seller = response.data; } this.seller = response.data; }) }, components: { 'v-header': header } } </script> <style scoped lang="stylus" rel="stylesheet/stylus"> @import "./common/stylus/mixin.styl"; .tab display: flex width: 100% height: 40px line-height: 40px /*border-bottom: 1px solid rgba(7, 17, 27, 0.1)*/ border-1px(rgba(7, 17, 27, 0.1)) .tab-item flex: 1 text-align: center & > a display:block font-size:16px color:rgb(77,85,93) &.active color:rgb(240,20,20) </style>
步骤二:修改goods.vue组件,传递值
<template> <div class="goods"> <div class="menu-wrapper" ref="menuWrapper"> <ul> <li v-for="(item,index) in goods" class="menu-item" :class="{'current':currentIndex===index}" @click="selectMenu(index,$event)"> <span class="text border-1px"> <span v-show="item.type>0" class="icon" :class="classMap[item.type]"></span> {{item.name}} </span> </li> </ul> </div> <div class="foods-wrapper" ref="foodsWrapper"> <ul> <li v-for="item in goods" class="food-list food-list-hook"> <h1 class="title">{{item.name}}</h1> <ul> <li v-for="food in item.foods" class="food-item border-1px"> <div class="icon"> <img :src="food.icon" width="57" height="57" /> </div> <div class="content"> <h2 class="name">{{food.name}}</h2> <p class="desc">{{food.description}}</p> <div class="extra"> <span class="count">月售{{food.sellCount}}份</span> <span>好评率{{food.rating}}%</span> </div> <div class="price"> <span class="now">¥{{food.price}}</span> <span class="old" v-show="food.oldPrice">¥{{food.oldPrice}}</span> </div> </div> </li> </ul> </li> </ul> </div> <shopcart :delivery-price="seller.deliveryPrice" :min-price="seller.minPrice"></shopcart> </div> </template> <script> import BScroll from 'better-scroll'; import shopcart from '../../components/shopcart/shopcart'; const ERR_OK = 0; export default { name: 'v-goods', props: { seller: { type: Object } }, data() { return { goods: [], listHeight: [], scrollY: 0 }; }, computed: { currentIndex() { for(let i=0;i<this.listHeight.length;i++){ let height1 = this.listHeight[i]; let height2 = this.listHeight[i+1]; if (!height2 || (this.scrollY >= height1 && this.scrollY < height2)){ return i; } } return 0; } }, created() { this.classMap = ['decrease','discount','special','invoice','guarantee']; this.$http.get('api/goods').then((response) => { response = response.body; if(response.error === ERR_OK){ this.goods = response.data; } this.goods = response.data; this.$nextTick(() => { this._initScroll(); this._calculateHeight(); }); }); }, methods: { selectMenu(index,event){ if(!event._constructed){ return; } let foodList = this.$refs.foodsWrapper.getElementsByClassName('food-list-hook'); let el = foodList[index]; this.foodsScroll.scrollToElement(el,300); console.log(index); }, _initScroll() { this.menuScroll = new BScroll(this.$refs.menuWrapper,{ click: true }); this.foodsScroll = new BScroll(this.$refs.foodsWrapper,{ click: true, probeType: 3 }); this.foodsScroll.on("scroll",(pos) => { this.scrollY = Math.abs(Math.round(pos.y)); }); }, _calculateHeight() { let foodList = this.$refs.foodsWrapper.getElementsByClassName('food-list-hook'); let height = 0; this.listHeight.push(height); for(let i=0;i<foodList.length;i++){ let item = foodList[i]; height += item.clientHeight; this.listHeight.push(height); } } }, components: { shopcart } } </script> <style scoped lang="stylus" rel="stylesheet/stylus"> @import "../../common/stylus/mixin"; .goods display: flex position: absolute top: 174px bottom: 46px width: 100% overflow: hidden .menu-wrapper flex: 0 0 80px width: 80px background: #f3f5f7 .menu-item display: table height: 54px width: 56px padding: 0 12px line-height: 14px &.current position: relative z-index: 10 margin-top: -1px background: #ccc font-weight: 700 .text border-none() .icon display: inline-block vertical-align: top width: 12px height: 12px margin-right: 2px background-size: 12px 12px background-repeat: no-repeat &.decrease bg-image('decrease_3') &.discount bg-image('discount_3') &.guarantee bg-image('guarantee_3') &.invoice bg-image('invoice_3') &.special bg-image('special_3') .text display: table-cell width: 56px vertical-align: middle border-1px(rgba(7,17,27,0.1)) font-size: 12px .foods-wrapper flex: 1 .title padding-left: 14px height: 26px line-height: 26px border-left: 2px solid #d9dde1 font-size: 12px color: rgb(147,153,159) background: #f3f5f7 .food-item display: flex margin: 18px padding-bottom: 18px border-1px(rgba(7,17,27,0.1)) &:last-child border-none() margin-bottom: 0 .icon flex: 0 0 57px margin-right: 10px .content flex: 1 .name margin: 2px 0 8px 0 height: 14px line-height: 14px font-size: 14px color: rgb(7,17,27) .desc, .extra line-height: 10px font-size: 10px color: rgb(147,153,159) .desc margin-bottom: 8px line-height: 14px .extra .count margin-right: 12px .price font-weight: 700 line-height: 24px .now margin-right: 8px font-size: 14px color: rgb(240,20,20) .old text-decoration: line-through font-size: 10px color: rgb(147,153,159) </style>
步骤三:编写shopcart.vue组件,接收值并增加样式
<template> <div class="shopcart"> <div class="content"> <div class="content-left"> <div class="logo-wrapper"> <div class="logo"> <span class="icon-shopping_cart"></span> </div> </div> <div class="price">0元</div> <div class="desc">另需配送费¥{{deliveryPrice}}元</div> </div> <div class="content-right"></div> </div> </div> </template> <script> export default { name: 'v-shopcart', props: { deliveryPrice: { type: Number, default: 0 }, minPrice: { type: Number, default: 0 } } } </script> <style scoped lang="stylus" rel="stylesheet/stylus"> .shopcart position: fixed left: 0 bottom: 0 z-index: 50 width: 100% height: 48px .content display: flex background: #141d27 font-size: 0 .content-left flex: 1 .logo-wrapper display: inline-block position: relative top:-10px margin: 0 12px padding: 6px width: 56px height: 56px vertical-align: top box-sizing: border-box border-radius: 50% background: #141d27 .logo width: 100% height: 100% border-radius: 50% text-align: center background: #2b343c .icon-shopping_cart font-size: 24px line-height: 44px color: #80858a .price display: inline-block vertical-align: top margin-top: 12px line-height: 24px padding-right: 12px box-sizing: border-box border-right: 1px solid rgba(255,255,255,0.1) font-size: 16px font-weight: 700 color: rgba(255,255,255,0.4) .desc display: inline-block vertical-align: top line-height: 24px margin: 12px 0 0 12px font-size: 10px color: rgba(255,255,255,0.4) .content-right flex: 0 0 105px width: 105px </style>
购物车组件(四)
继续编写shopcart.vue组件,为商品价格计算做前期编码
<template> <div class="shopcart"> <div class="content"> <div class="content-left"> <div class="logo-wrapper"> <div class="logo"> <span class="icon-shopping_cart"></span> </div> </div> <div class="price">¥{{totalPrice}}</div> <div class="desc">另需配送费¥{{deliveryPrice}}元</div> </div> <div class="content-right"> <div class="pay"> ¥{{minPrice}}元起送 </div> </div> </div> </div> </template> <script> export default { name: 'v-shopcart', props: { selectFoods: { type: Array, default() { return [ { price:10, count:1 } ]; } }, deliveryPrice: { type: Number, default: 0 }, minPrice: { type: Number, default: 0 } }, computed: { totalPrice() { let total = 0; this.selectFoods.forEach((food) => { total += food.price * food.count; }); return total; }, totalCount() { let count = 0; this.selectFoods.forEach((food) => { count += food.count; }); return count; } } } </script> <style scoped lang="stylus" rel="stylesheet/stylus"> .shopcart position: fixed left: 0 bottom: 0 z-index: 50 width: 100% height: 48px .content display: flex background: #141d27 font-size: 0 .content-left flex: 1 .logo-wrapper display: inline-block position: relative top:-10px margin: 0 12px padding: 6px width: 56px height: 56px vertical-align: top box-sizing: border-box border-radius: 50% background: #141d27 .logo width: 100% height: 100% border-radius: 50% text-align: center background: #2b343c .icon-shopping_cart font-size: 24px line-height: 44px color: #80858a .price display: inline-block vertical-align: top margin-top: 12px line-height: 24px padding-right: 12px box-sizing: border-box border-right: 1px solid rgba(255,255,255,0.1) font-size: 16px font-weight: 700 color: rgba(255,255,255,0.4) .desc display: inline-block vertical-align: top line-height: 24px margin: 12px 0 0 12px font-size: 10px color: rgba(255,255,255,0.4) .content-right flex: 0 0 105px width: 105px .pay height: 48px line-height: 48px text-align: center font-size: 12px color: rgba(255,255,255,0.4) font-weight: 700 background: #2b333b </style>
购物车组件(五)
继续编写shopcart.vue组件,实现样式动态改变
<template> <div class="shopcart"> <div class="content"> <div class="content-left"> <div class="logo-wrapper"> <div class="logo" :class="{'highlight':totalCount>0}"> <span class="icon-shopping_cart" :class="{'highlight':totalCount>0}"></span> </div> <div class="num" v-show="totalCount>0">{{totalCount}}</div> </div> <div class="price" :class="{'highlight':totalPrice>0}">¥{{totalPrice}}</div> <div class="desc">另需配送费¥{{deliveryPrice}}元</div> </div> <div class="content-right"> <div class="pay"> ¥{{minPrice}}元起送 </div> </div> </div> </div> </template> <script> export default { name: 'v-shopcart', props: { selectFoods: { type: Array, default() { return []; } }, deliveryPrice: { type: Number, default: 0 }, minPrice: { type: Number, default: 0 } }, computed: { totalPrice() { let total = 0; this.selectFoods.forEach((food) => { total += food.price * food.count; }); return total; }, totalCount() { let count = 0; this.selectFoods.forEach((food) => { count += food.count; }); return count; } } } </script> <style scoped lang="stylus" rel="stylesheet/stylus"> .shopcart position: fixed left: 0 bottom: 0 z-index: 50 width: 100% height: 48px .content display: flex background: #141d27 font-size: 0 .content-left flex: 1 .logo-wrapper display: inline-block position: relative top:-10px margin: 0 12px padding: 6px width: 56px height: 56px vertical-align: top box-sizing: border-box border-radius: 50% background: #141d27 .logo width: 100% height: 100% border-radius: 50% text-align: center background: #2b343c &.highlight background: rgb(0,160,220) .icon-shopping_cart font-size: 24px line-height: 44px color: #80858a &.highlight color: #fff .num position: absolute top: 0px right: 0px width: 24px height: 16px line-height: 16px text-align: center border-radius: 16px font-size: 9px font-weight: 700 color: #fff background: rgb(240,20,20) box-shadow: 0 4px 8px 0 rgba(0,0,0,0.4) .price display: inline-block vertical-align: top margin-top: 12px line-height: 24px padding-right: 12px box-sizing: border-box border-right: 1px solid rgba(255,255,255,0.1) font-size: 16px font-weight: 700 color: rgba(255,255,255,0.4) &.highlight color: #fff .desc display: inline-block vertical-align: top line-height: 24px margin: 12px 0 0 12px font-size: 10px color: rgba(255,255,255,0.4) .content-right flex: 0 0 105px width: 105px .pay height: 48px line-height: 48px text-align: center font-size: 12px color: rgba(255,255,255,0.4) font-weight: 700 background: #2b333b </style>
购物车组件(六)
继续编写shopcart.vue组件,基本完成全部效果
<template> <div class="shopcart"> <div class="content"> <div class="content-left"> <div class="logo-wrapper"> <div class="logo" :class="{'highlight':totalCount>0}"> <span class="icon-shopping_cart" :class="{'highlight':totalCount>0}"></span> </div> <div class="num" v-show="totalCount>0">{{totalCount}}</div> </div> <div class="price" :class="{'highlight':totalPrice>0}">¥{{totalPrice}}</div> <div class="desc">另需配送费¥{{deliveryPrice}}元</div> </div> <div class="content-right"> <div class="pay" :class="payClass"> {{payDesc}} </div> </div> </div> </div> </template> <script> export default { name: 'v-shopcart', props: { selectFoods: { type: Array, default() { return [ { price: 10, count: 5 } ]; } }, deliveryPrice: { type: Number, default: 0 }, minPrice: { type: Number, default: 0 } }, computed: { totalPrice() { let total = 0; this.selectFoods.forEach((food) => { total += food.price * food.count; }); return total; }, totalCount() { let count = 0; this.selectFoods.forEach((food) => { count += food.count; }); return count; }, payDesc() { if(this.totalPrice === 0){ return `¥${this.minPrice}元起送`; }else if(this.totalPrice<this.minPrice){ let diff = this.minPrice - this.totalPrice; return `还差¥${diff}元起送`; }else{ return '去结算'; } }, payClass() { if(this.totalPrice < this.minPrice){ return 'not-enough'; }else{ return 'enough'; } } } } </script> <style scoped lang="stylus" rel="stylesheet/stylus"> .shopcart position: fixed left: 0 bottom: 0 z-index: 50 width: 100% height: 48px .content display: flex background: #141d27 font-size: 0 .content-left flex: 1 .logo-wrapper display: inline-block position: relative top:-10px margin: 0 12px padding: 6px width: 56px height: 56px vertical-align: top box-sizing: border-box border-radius: 50% background: #141d27 .logo width: 100% height: 100% border-radius: 50% text-align: center background: #2b343c &.highlight background: rgb(0,160,220) .icon-shopping_cart font-size: 24px line-height: 44px color: #80858a &.highlight color: #fff .num position: absolute top: 0px right: 0px width: 24px height: 16px line-height: 16px text-align: center border-radius: 16px font-size: 9px font-weight: 700 color: #fff background: rgb(240,20,20) box-shadow: 0 4px 8px 0 rgba(0,0,0,0.4) .price display: inline-block vertical-align: top margin-top: 12px line-height: 24px padding-right: 12px box-sizing: border-box border-right: 1px solid rgba(255,255,255,0.1) font-size: 16px font-weight: 700 color: rgba(255,255,255,0.4) &.highlight color: #fff .desc display: inline-block vertical-align: top line-height: 24px margin: 12px 0 0 12px font-size: 10px color: rgba(255,255,255,0.4) .content-right flex: 0 0 105px width: 105px .pay height: 48px line-height: 48px text-align: center font-size: 12px color: rgba(255,255,255,0.4) font-weight: 700 background: #2b333b &.not-enough background: #2b333b &.enough background: #00b43c color: #fff </style>
cartcontrol组件(一)
步骤一:新建cartcontrol.vue组件,作为购物车的添加按钮
<template> <div class="cartcontrol"> <div class="cart-decrease icon-remove_circle_outline" v-show="food.count>0"></div> <div class="cart-count" v-show="food.count>0">{{food.count}}</div> <div class="cart-add icon-add_circle" @click="addCart"></div> </div> </template> <script> export default { name: 'v-cartcontrol', props: { food: { type: Object } }, created() { console.log(this.food); }, methods: { addCart() { } } } </script> <style scoped lang="stylus" rel="stylesheet/stylus"> .cartcontrol font-size: 0 .cart-decrease, .cart-add font-size: 24px line-height: 24px padding: 6px color: rgb(0,160,220) display: inline-block .cart-count display: inline-block .cart-add display: inline-block </style>
步骤二:在goods.vue组件中调用cartcontrol.vue组件
<template> <div class="goods"> <div class="menu-wrapper" ref="menuWrapper"> <ul> <li v-for="(item,index) in goods" class="menu-item" :class="{'current':currentIndex===index}" @click="selectMenu(index,$event)"> <span class="text border-1px"> <span v-show="item.type>0" class="icon" :class="classMap[item.type]"></span> {{item.name}} </span> </li> </ul> </div> <div class="foods-wrapper" ref="foodsWrapper"> <ul> <li v-for="item in goods" class="food-list food-list-hook"> <h1 class="title">{{item.name}}</h1> <ul> <li v-for="food in item.foods" class="food-item border-1px"> <div class="icon"> <img :src="food.icon" width="57" height="57" /> </div> <div class="content"> <h2 class="name">{{food.name}}</h2> <p class="desc">{{food.description}}</p> <div class="extra"> <span class="count">月售{{food.sellCount}}份</span> <span>好评率{{food.rating}}%</span> </div> <div class="price"> <span class="now">¥{{food.price}}</span> <span class="old" v-show="food.oldPrice">¥{{food.oldPrice}}</span> </div> <div class="cartcontrol-wrapper"> <cartcontrol :food="food"></cartcontrol> </div> </div> </li> </ul> </li> </ul> </div> <shopcart :delivery-price="seller.deliveryPrice" :min-price="seller.minPrice"></shopcart> </div> </template> <script> import BScroll from 'better-scroll'; import shopcart from '../../components/shopcart/shopcart'; import cartcontrol from '../../components/cartcontrol/cartcontrol'; const ERR_OK = 0; export default { name: 'v-goods', props: { seller: { type: Object } }, data() { return { goods: [], listHeight: [], scrollY: 0 }; }, computed: { currentIndex() { for(let i=0;i<this.listHeight.length;i++){ let height1 = this.listHeight[i]; let height2 = this.listHeight[i+1]; if (!height2 || (this.scrollY >= height1 && this.scrollY < height2)){ return i; } } return 0; } }, created() { this.classMap = ['decrease','discount','special','invoice','guarantee']; this.$http.get('api/goods').then((response) => { response = response.body; if(response.error === ERR_OK){ this.goods = response.data; } this.goods = response.data; this.$nextTick(() => { this._initScroll(); this._calculateHeight(); }); }); }, methods: { selectMenu(index,event){ if(!event._constructed){ return; } let foodList = this.$refs.foodsWrapper.getElementsByClassName('food-list-hook'); let el = foodList[index]; this.foodsScroll.scrollToElement(el,300); console.log(index); }, _initScroll() { this.menuScroll = new BScroll(this.$refs.menuWrapper,{ click: true }); this.foodsScroll = new BScroll(this.$refs.foodsWrapper,{ click: true, probeType: 3 }); this.foodsScroll.on("scroll",(pos) => { this.scrollY = Math.abs(Math.round(pos.y)); }); }, _calculateHeight() { let foodList = this.$refs.foodsWrapper.getElementsByClassName('food-list-hook'); let height = 0; this.listHeight.push(height); for(let i=0;i<foodList.length;i++){ let item = foodList[i]; height += item.clientHeight; this.listHeight.push(height); } } }, components: { shopcart, cartcontrol } } </script> <style scoped lang="stylus" rel="stylesheet/stylus"> @import "../../common/stylus/mixin"; .goods display: flex position: absolute top: 174px bottom: 46px width: 100% overflow: hidden .menu-wrapper flex: 0 0 80px width: 80px background: #f3f5f7 .menu-item display: table height: 54px width: 56px padding: 0 12px line-height: 14px &.current position: relative z-index: 10 margin-top: -1px background: #ccc font-weight: 700 .text border-none() .icon display: inline-block vertical-align: top width: 12px height: 12px margin-right: 2px background-size: 12px 12px background-repeat: no-repeat &.decrease bg-image('decrease_3') &.discount bg-image('discount_3') &.guarantee bg-image('guarantee_3') &.invoice bg-image('invoice_3') &.special bg-image('special_3') .text display: table-cell width: 56px vertical-align: middle border-1px(rgba(7,17,27,0.1)) font-size: 12px .foods-wrapper flex: 1 .title padding-left: 14px height: 26px line-height: 26px border-left: 2px solid #d9dde1 font-size: 12px color: rgb(147,153,159) background: #f3f5f7 .food-item display: flex margin: 18px padding-bottom: 18px border-1px(rgba(7,17,27,0.1)) &:last-child border-none() margin-bottom: 0 .icon flex: 0 0 57px margin-right: 10px .content flex: 1 .name margin: 2px 0 8px 0 height: 14px line-height: 14px font-size: 14px color: rgb(7,17,27) .desc, .extra line-height: 10px font-size: 10px color: rgb(147,153,159) .desc margin-bottom: 8px line-height: 14px .extra .count margin-right: 12px .price font-weight: 700 line-height: 24px .now margin-right: 8px font-size: 14px color: rgb(240,20,20) .old text-decoration: line-through font-size: 10px color: rgb(147,153,159) .cartcontrol-wrapper position: absolute right: 0 bottom: 12px </style>
cartcontrol组件(二)
继续编写cartcontrol.vue组件,进一步实现购物车按钮效果
<template> <div class="cartcontrol"> <div class="cart-decrease icon-remove_circle_outline" v-show="food.count>0" @click="decreaseCart"></div> <div class="cart-count" v-show="food.count>0">{{food.count}}</div> <div class="cart-add icon-add_circle" @click="addCart"></div> </div> </template> <script> import Vue from 'vue'; export default { name: 'v-cartcontrol', props: { food: { type: Object } }, methods: { addCart(event) { if (!event._constructed) { return; } if (!this.food.count) { Vue.set(this.food, 'count', 1); } else { this.food.count++; } }, decreaseCart(event) { if (!event._constructed) { return; } if (this.food.count) { this.food.count--; } } } } </script> <style scoped lang="stylus" rel="stylesheet/stylus"> .cartcontrol font-size: 0 .cart-decrease, .cart-add font-size: 24px line-height: 24px padding: 6px color: rgb(0,160,220) display: inline-block .cart-count display: inline-block vertical-align: top width: 12px padding-top: 6px line-height: 24px text-align: center font-size: 10px color: rgb(147,153,159) .cart-add display: inline-block </style>
cartcontrol组件(三)
步骤一:编写cartcontrol.vue组件,增加食品增加和减少动画效果
<template> <div class="cartcontrol"> <transition name="move"> <div class="cart-decrease" v-show="food.count>0" @click="decreaseCart"> <span class="inner icon-remove_circle_outline"></span> </div> </transition> <div class="cart-count" v-show="food.count>0">{{food.count}}</div> <div class="cart-add icon-add_circle" @click="addCart"></div> </div> </template> <script> import Vue from 'vue'; export default { name: 'v-cartcontrol', props: { food: { type: Object } }, methods: { addCart(event) { if (!event._constructed) { return; } if (!this.food.count) { Vue.set(this.food, 'count', 1); } else { this.food.count++; } }, decreaseCart(event) { if (!event._constructed) { return; } if (this.food.count) { this.food.count--; } } } } </script> <style scoped lang="stylus" rel="stylesheet/stylus"> .cartcontrol font-size: 0 .cart-decrease padding: 6px color: rgb(0,160,220) display: inline-block opacity: 1 transform: translate3d(0, 0, 0) .inner display: inline-block line-height: 24px font-size: 24px color: rgb(0, 160, 220) transition: all 0.4s linear transform: rotate(0) &.move-enter-active, &.move-leave-active transition: all 0.4s linear &.move-enter, &.move-leave-active opacity: 0 transform: translate3d(24px, 0, 0) .inner transform: rotate(180deg) .cart-count display: inline-block vertical-align: top width: 12px padding-top: 6px line-height: 24px text-align: center font-size: 10px color: rgb(147,153,159) .cart-add display: inline-block padding: 6px line-height: 24px font-size: 24px color: rgb(0, 160, 220) </style>
步骤二:编写goods.vue组件,将按钮和购物车区实现联动效果
<template> <div class="goods"> <div class="menu-wrapper" ref="menuWrapper"> <ul> <li v-for="(item,index) in goods" class="menu-item" :class="{'current':currentIndex===index}" @click="selectMenu(index,$event)"> <span class="text border-1px"> <span v-show="item.type>0" class="icon" :class="classMap[item.type]"></span> {{item.name}} </span> </li> </ul> </div> <div class="foods-wrapper" ref="foodsWrapper"> <ul> <li v-for="item in goods" class="food-list food-list-hook"> <h1 class="title">{{item.name}}</h1> <ul> <li v-for="food in item.foods" class="food-item border-1px"> <div class="icon"> <img :src="food.icon" width="57" height="57" /> </div> <div class="content"> <h2 class="name">{{food.name}}</h2> <p class="desc">{{food.description}}</p> <div class="extra"> <span class="count">月售{{food.sellCount}}份</span> <span>好评率{{food.rating}}%</span> </div> <div class="price"> <span class="now">¥{{food.price}}</span> <span class="old" v-show="food.oldPrice">¥{{food.oldPrice}}</span> </div> <div class="cartcontrol-wrapper"> <cartcontrol :food="food"></cartcontrol> </div> </div> </li> </ul> </li> </ul> </div> <shopcart :select-foods="selectFoods" :delivery-price="seller.deliveryPrice" :min-price="seller.minPrice"></shopcart> </div> </template> <script> import BScroll from 'better-scroll'; import shopcart from '../../components/shopcart/shopcart'; import cartcontrol from '../../components/cartcontrol/cartcontrol'; const ERR_OK = 0; export default { name: 'v-goods', props: { seller: { type: Object } }, data() { return { goods: [], listHeight: [], scrollY: 0 }; }, computed: { currentIndex() { for (let i = 0; i < this.listHeight.length; i++) { let height1 = this.listHeight[i]; let height2 = this.listHeight[i + 1]; if (!height2 || (this.scrollY >= height1 && this.scrollY < height2)) { return i; } } return 0; }, selectFoods() { let foods = []; this.goods.forEach((good) => { good.foods.forEach((food) => { if (food.count) { foods.push(food); } }); }); return foods; } }, created() { this.classMap = ['decrease', 'discount', 'special', 'invoice', 'guarantee']; this.$http.get('api/goods').then((response) => { response = response.body; if (response.error === ERR_OK) { this.goods = response.data; } this.goods = response.data; this.$nextTick(() => { this._initScroll(); this._calculateHeight(); }); }); }, methods: { selectMenu(index, event) { if (!event._constructed) { return; } let foodList = this.$refs.foodsWrapper.getElementsByClassName('food-list-hook'); let el = foodList[index]; this.foodsScroll.scrollToElement(el, 300); console.log(index); }, _initScroll() { this.menuScroll = new BScroll(this.$refs.menuWrapper, { click: true }); this.foodsScroll = new BScroll(this.$refs.foodsWrapper, { click: true, probeType: 3 }); this.foodsScroll.on("scroll", (pos) => { this.scrollY = Math.abs(Math.round(pos.y)); }); }, _calculateHeight() { let foodList = this.$refs.foodsWrapper.getElementsByClassName('food-list-hook'); let height = 0; this.listHeight.push(height); for (let i = 0; i < foodList.length; i++) { let item = foodList[i]; height += item.clientHeight; this.listHeight.push(height); } } }, components: { shopcart, cartcontrol } } </script> <style scoped lang="stylus" rel="stylesheet/stylus"> @import "../../common/stylus/mixin"; .goods display: flex position: absolute top: 174px bottom: 46px width: 100% overflow: hidden .menu-wrapper flex: 0 0 80px width: 80px background: #f3f5f7 .menu-item display: table height: 54px width: 56px padding: 0 12px line-height: 14px &.current position: relative z-index: 10 margin-top: -1px background: #ccc font-weight: 700 .text border-none() .icon display: inline-block vertical-align: top width: 12px height: 12px margin-right: 2px background-size: 12px 12px background-repeat: no-repeat &.decrease bg-image('decrease_3') &.discount bg-image('discount_3') &.guarantee bg-image('guarantee_3') &.invoice bg-image('invoice_3') &.special bg-image('special_3') .text display: table-cell width: 56px vertical-align: middle border-1px(rgba(7,17,27,0.1)) font-size: 12px .foods-wrapper flex: 1 .title padding-left: 14px height: 26px line-height: 26px border-left: 2px solid #d9dde1 font-size: 12px color: rgb(147,153,159) background: #f3f5f7 .food-item display: flex margin: 18px padding-bottom: 18px border-1px(rgba(7,17,27,0.1)) &:last-child border-none() margin-bottom: 0 .icon flex: 0 0 57px margin-right: 10px .content flex: 1 .name margin: 2px 0 8px 0 height: 14px line-height: 14px font-size: 14px color: rgb(7,17,27) .desc, .extra line-height: 10px font-size: 10px color: rgb(147,153,159) .desc margin-bottom: 8px line-height: 14px .extra .count margin-right: 12px .price font-weight: 700 line-height: 24px .now margin-right: 8px font-size: 14px color: rgb(240,20,20) .old text-decoration: line-through font-size: 10px color: rgb(147,153,159) .cartcontrol-wrapper position: absolute right: 0 bottom: 12px </style>
购物车小球动画实现(一)
【注意】无法实现
步骤一:编写goods.vue组件
<template> <div class="goods"> <div class="menu-wrapper" ref="menuWrapper"> <ul> <li v-for="(item,index) in goods" class="menu-item" :class="{'current':currentIndex===index}" @click="selectMenu(index,$event)"> <span class="text border-1px"> <span v-show="item.type>0" class="icon" :class="classMap[item.type]"></span> {{item.name}} </span> </li> </ul> </div> <div class="foods-wrapper" ref="foodsWrapper"> <ul> <li v-for="item in goods" class="food-list food-list-hook"> <h1 class="title">{{item.name}}</h1> <ul> <li v-for="food in item.foods" class="food-item border-1px"> <div class="icon"> <img :src="food.icon" width="57" height="57" /> </div> <div class="content"> <h2 class="name">{{food.name}}</h2> <p class="desc">{{food.description}}</p> <div class="extra"> <span class="count">月售{{food.sellCount}}份</span> <span>好评率{{food.rating}}%</span> </div> <div class="price"> <span class="now">¥{{food.price}}</span> <span class="old" v-show="food.oldPrice">¥{{food.oldPrice}}</span> </div> <div class="cartcontrol-wrapper"> <cartcontrol :food="food"></cartcontrol> </div> </div> </li> </ul> </li> </ul> </div> <shopcart ref:shopcart :select-foods="selectFoods" :delivery-price="seller.deliveryPrice" :min-price="seller.minPrice"></shopcart> </div> </template> <script> import BScroll from 'better-scroll'; import shopcart from '../../components/shopcart/shopcart'; import cartcontrol from '../../components/cartcontrol/cartcontrol'; const ERR_OK = 0; export default { name: 'v-goods', props: { seller: { type: Object } }, data() { return { goods: [], listHeight: [], scrollY: 0 }; }, computed: { currentIndex() { for (let i = 0; i < this.listHeight.length; i++) { let height1 = this.listHeight[i]; let height2 = this.listHeight[i + 1]; if (!height2 || (this.scrollY >= height1 && this.scrollY < height2)) { return i; } } return 0; }, selectFoods() { let foods = []; this.goods.forEach((good) => { good.foods.forEach((food) => { if (food.count) { foods.push(food); } }); }); return foods; } }, created() { this.classMap = ['decrease', 'discount', 'special', 'invoice', 'guarantee']; this.$http.get('api/goods').then((response) => { response = response.body; if (response.error === ERR_OK) { this.goods = response.data; } this.goods = response.data; this.$nextTick(() => { this._initScroll(); this._calculateHeight(); }); }); }, methods: { selectMenu(index, event) { if (!event._constructed) { return; } let foodList = this.$refs.foodsWrapper.getElementsByClassName('food-list-hook'); let el = foodList[index]; this.foodsScroll.scrollToElement(el, 300); console.log(index); }, _drop(target) { //体验优化,异步执行下落动画 this.$nextTick(() => { this.$refs.shopcart.drop(target); }); }, _initScroll() { this.menuScroll = new BScroll(this.$refs.menuWrapper, { click: true }); this.foodsScroll = new BScroll(this.$refs.foodsWrapper, { click: true, probeType: 3 }); this.foodsScroll.on("scroll", (pos) => { this.scrollY = Math.abs(Math.round(pos.y)); }); }, _calculateHeight() { let foodList = this.$refs.foodsWrapper.getElementsByClassName('food-list-hook'); let height = 0; this.listHeight.push(height); for (let i = 0; i < foodList.length; i++) { let item = foodList[i]; height += item.clientHeight; this.listHeight.push(height); } } }, components: { shopcart, cartcontrol }, events: { 'cart.add'(target){ this._drop(target); } } } </script> <style scoped lang="stylus" rel="stylesheet/stylus"> @import "../../common/stylus/mixin"; .goods display: flex position: absolute top: 174px bottom: 46px width: 100% overflow: hidden .menu-wrapper flex: 0 0 80px width: 80px background: #f3f5f7 .menu-item display: table height: 54px width: 56px padding: 0 12px line-height: 14px &.current position: relative z-index: 10 margin-top: -1px background: #ccc font-weight: 700 .text border-none() .icon display: inline-block vertical-align: top width: 12px height: 12px margin-right: 2px background-size: 12px 12px background-repeat: no-repeat &.decrease bg-image('decrease_3') &.discount bg-image('discount_3') &.guarantee bg-image('guarantee_3') &.invoice bg-image('invoice_3') &.special bg-image('special_3') .text display: table-cell width: 56px vertical-align: middle border-1px(rgba(7,17,27,0.1)) font-size: 12px .foods-wrapper flex: 1 .title padding-left: 14px height: 26px line-height: 26px border-left: 2px solid #d9dde1 font-size: 12px color: rgb(147,153,159) background: #f3f5f7 .food-item display: flex margin: 18px padding-bottom: 18px border-1px(rgba(7,17,27,0.1)) &:last-child border-none() margin-bottom: 0 .icon flex: 0 0 57px margin-right: 10px .content flex: 1 .name margin: 2px 0 8px 0 height: 14px line-height: 14px font-size: 14px color: rgb(7,17,27) .desc, .extra line-height: 10px font-size: 10px color: rgb(147,153,159) .desc margin-bottom: 8px line-height: 14px .extra .count margin-right: 12px .price font-weight: 700 line-height: 24px .now margin-right: 8px font-size: 14px color: rgb(240,20,20) .old text-decoration: line-through font-size: 10px color: rgb(147,153,159) .cartcontrol-wrapper position: absolute right: 0 bottom: 12px </style>
步骤二:编写carcontrol.vue组件
<template> <div class="cartcontrol"> <transition name="move"> <div class="cart-decrease" v-show="food.count>0" @click.stop.prevent="decreaseCart"> <span class="inner icon-remove_circle_outline"></span> </div> </transition> <div class="cart-count" v-show="food.count>0">{{food.count}}</div> <div class="cart-add icon-add_circle" @click.stop.prevent="addCart"></div> </div> </template> <script> import Vue from 'vue'; export default { props: { food: { type: Object } }, methods: { addCart(event) { if (!event._constructed) { return; } if (!this.food.count) { Vue.set(this.food, 'count', 1); } else { this.food.count++; } this.$dispatch('cart.add',event.target); }, decreaseCart(event) { if (!event._constructed) { return; } if (this.food.count) { this.food.count--; } } } }; </script> <style lang="stylus" rel="stylesheet/stylus"> .cartcontrol font-size: 0 .cart-decrease display: inline-block padding: 6px opacity: 1 transform: translate3d(0, 0, 0) .inner display: inline-block line-height: 24px font-size: 24px color: rgb(0, 160, 220) transition: all 0.4s linear transform: rotate(0) &.move-enter-active, &.move-leave-active transition: all 0.4s linear &.move-enter, &.move-leave-active opacity: 0 transform: translate3d(24px, 0, 0) .inner transform: rotate(180deg) .cart-count display: inline-block vertical-align: top width: 12px padding-top: 6px line-height: 24px text-align: center font-size: 10px color: rgb(147, 153, 159) .cart-add display: inline-block padding: 6px line-height: 24px font-size: 24px color: rgb(0, 160, 220) </style>
步骤三:编写shoucart.vue组件
<template> <div class="shopcart"> <div class="content"> <div class="content-left"> <div class="logo-wrapper"> <div class="logo" :class="{'highlight':totalCount>0}"> <span class="icon-shopping_cart" :class="{'highlight':totalCount>0}"></span> </div> <div class="num" v-show="totalCount>0">{{totalCount}}</div> </div> <div class="price" :class="{'highlight':totalPrice>0}">¥{{totalPrice}}</div> <div class="desc">另需配送费¥{{deliveryPrice}}元</div> </div> <div class="content-right"> <div class="pay" :class="payClass"> {{payDesc}} </div> </div> </div> <div class="ball-container"> <div v-for="ball in balls"> <transition name="drop"> <div class="ball" v-show="ball.show"> <div class="inner inner-hook"></div> </div> </transition> </div> </div> </div> </template> <script> export default { name: 'v-shopcart', props: { selectFoods: { type: Array, default () { return [{ price: 10, count: 5 }]; } }, deliveryPrice: { type: Number, default: 0 }, minPrice: { type: Number, default: 0 } }, data() { return { balls: [{ show: false }, { show: false }, { show: false }, { show: false }, { show: false } ], dropBall: [] }; }, computed: { totalPrice() { let total = 0; this.selectFoods.forEach((food) => { total += food.price * food.count; }); return total; }, totalCount() { let count = 0; this.selectFoods.forEach((food) => { count += food.count; }); return count; }, payDesc() { if (this.totalPrice === 0) { return `¥${this.minPrice}元起送`; } else if (this.totalPrice < this.minPrice) { let diff = this.minPrice - this.totalPrice; return `还差¥${diff}元起送`; } else { return '去结算'; } }, payClass() { if (this.totalPrice < this.minPrice) { return 'not-enough'; } else { return 'enough'; } } }, methods: { drop(el) { for (let i = 0; i < this.balls.length; i++) { let ball = this.balls[i]; if (!ball.show) { ball.show = true; ball.el = el; this.dropBall.push(ball); return; } } } }, transitions: { drop: { beforeDrop(el) { let count = this.balls.length; while (count--) { let ball = this.balls[count]; if (ball.show) { let rect = ball.el.getBoundingClientRect(); let x = rect.left - 32; let y = -(window.innerHeight - rect.top - 22); el.style.display = ''; el.style.webkitTransform = `translate3d(0,${y}px,0)`; el.style.transform = `translate3d(0,${y}px,0)`; let inner = el.getElementsByClassName('inner-hook')[0]; inner.style.webkitTransform = `translate3d(${x}px,0,0)`; inner.style.transform = `translate3d(${x}px,0,0)`; } } }, enter(el) { /* eslint-disable no-unused-vars */ let rf = el.offsetHeight; this.$nextTick(() => { el.style.webkitTransform = 'translate3d(0,0,0)'; el.style.transform = 'translate3d(0,0,0)'; let inner = el.getElementsByClassName('inner-hook')[0]; inner.style.webkitTransform = 'translate3d(0,0,0)'; inner.style.transform = 'translate3d(0,0,0)'; el.addEventListener('transitionend', done); }); }, afterEnter(el) { let ball = this.dropBalls.shift(); if (ball) { ball.show = false; el.style.display = 'none'; } } } } } </script> <style scoped lang="stylus" rel="stylesheet/stylus"> .shopcart position: fixed left: 0 bottom: 0 z-index: 50 width: 100% height: 48px .content display: flex background: #141d27 font-size: 0 .content-left flex: 1 .logo-wrapper display: inline-block position: relative top:-10px margin: 0 12px padding: 6px width: 56px height: 56px vertical-align: top box-sizing: border-box border-radius: 50% background: #141d27 .logo width: 100% height: 100% border-radius: 50% text-align: center background: #2b343c &.highlight background: rgb(0,160,220) .icon-shopping_cart font-size: 24px line-height: 44px color: #80858a &.highlight color: #fff .num position: absolute top: 0px right: 0px width: 24px height: 16px line-height: 16px text-align: center border-radius: 16px font-size: 9px font-weight: 700 color: #fff background: rgb(240,20,20) box-shadow: 0 4px 8px 0 rgba(0,0,0,0.4) .price display: inline-block vertical-align: top margin-top: 12px line-height: 24px padding-right: 12px box-sizing: border-box border-right: 1px solid rgba(255,255,255,0.1) font-size: 16px font-weight: 700 color: rgba(255,255,255,0.4) &.highlight color: #fff .desc display: inline-block vertical-align: top line-height: 24px margin: 12px 0 0 12px font-size: 10px color: rgba(255,255,255,0.4) .content-right flex: 0 0 105px width: 105px .pay height: 48px line-height: 48px text-align: center font-size: 12px color: rgba(255,255,255,0.4) font-weight: 700 background: #2b333b &.not-enough background: #2b333b &.enough background: #00b43c color: #fff .ball-container .ball position: fixed left: 32px bottom: 22px z-index: 200 transition: all 0.4s cubic-bezier(0.49, -0.29, 0.75, 0.41) .inner width: 16px height: 16px border-radius: 50% background: rgb(0, 160, 220) transition: all 0.4s linear </style>
购物车详情页(一、二)
编写shopcart.vue组件,实现初步效果
<template> <div class="shopcart"> <div class="content" @click="toggleList"> <div class="content-left"> <div class="logo-wrapper"> <div class="logo" :class="{'highlight':totalCount>0}"> <span class="icon-shopping_cart" :class="{'highlight':totalCount>0}"></span> </div> <div class="num" v-show="totalCount>0">{{totalCount}}</div> </div> <div class="price" :class="{'highlight':totalPrice>0}">¥{{totalPrice}}</div> <div class="desc">另需配送费¥{{deliveryPrice}}元</div> </div> <div class="content-right"> <div class="pay" :class="payClass"> {{payDesc}} </div> </div> </div> <div class="ball-container"> <div v-for="ball in balls"> <transition name="drop"> <div class="ball" v-show="ball.show"> <div class="inner inner-hook"></div> </div> </transition> </div> </div> <transition name="fold"> <div class="shopcart-list" v-show="listShow"> <div class="list-header"> <h1 class="title">购物车</h1> <span class="empty">清空</span> </div> <div class="list-content"> <ul> <li class="food" v-for="food in selectFoods"> <span class="name">{{food.name}}</span> <div class="price"> <span>¥{{food.price*food.count}}</span> </div> <div class="cartcontrol-wrapper"> <cartcontrol :food="food"></cartcontrol> </div> </li> </ul> </div> </div> </transition> </div> </template> <script> import cartcontrol from '../../components/cartcontrol/cartcontrol'; export default { name: 'v-shopcart', props: { selectFoods: { type: Array, default () { return [{ price: 10, count: 5 }]; } }, deliveryPrice: { type: Number, default: 0 }, minPrice: { type: Number, default: 0 } }, data() { return { balls: [{ show: false }, { show: false }, { show: false }, { show: false }, { show: false } ], dropBall: [], fold: true }; }, computed: { totalPrice() { let total = 0; this.selectFoods.forEach((food) => { total += food.price * food.count; }); return total; }, totalCount() { let count = 0; this.selectFoods.forEach((food) => { count += food.count; }); return count; }, payDesc() { if (this.totalPrice === 0) { return `¥${this.minPrice}元起送`; } else if (this.totalPrice < this.minPrice) { let diff = this.minPrice - this.totalPrice; return `还差¥${diff}元起送`; } else { return '去结算'; } }, payClass() { if (this.totalPrice < this.minPrice) { return 'not-enough'; } else { return 'enough'; } }, listShow() { if (!this.totalCount) { this.fold = true; return false; } let show = !this.fold; return show; } }, methods: { drop(el) { for (let i = 0; i < this.balls.length; i++) { let ball = this.balls[i]; if (!ball.show) { ball.show = true; ball.el = el; this.dropBall.push(ball); return; } } }, toggleList() { if (!this.totalCount) { return; } this.fold = !this.fold; } }, transitions: { drop: { beforeDrop(el) { let count = this.balls.length; while (count--) { let ball = this.balls[count]; if (ball.show) { let rect = ball.el.getBoundingClientRect(); let x = rect.left - 32; let y = -(window.innerHeight - rect.top - 22); el.style.display = ''; el.style.webkitTransform = `translate3d(0,${y}px,0)`; el.style.transform = `translate3d(0,${y}px,0)`; let inner = el.getElementsByClassName('inner-hook')[0]; inner.style.webkitTransform = `translate3d(${x}px,0,0)`; inner.style.transform = `translate3d(${x}px,0,0)`; } } }, enter(el) { /* eslint-disable no-unused-vars */ let rf = el.offsetHeight; this.$nextTick(() => { el.style.webkitTransform = 'translate3d(0,0,0)'; el.style.transform = 'translate3d(0,0,0)'; let inner = el.getElementsByClassName('inner-hook')[0]; inner.style.webkitTransform = 'translate3d(0,0,0)'; inner.style.transform = 'translate3d(0,0,0)'; el.addEventListener('transitionend', done); }); }, afterEnter(el) { let ball = this.dropBalls.shift(); if (ball) { ball.show = false; el.style.display = 'none'; } } } }, components: { cartcontrol } } </script> <style scoped lang="stylus" rel="stylesheet/stylus"> @import "../../common/stylus/mixin"; .shopcart position: fixed left: 0 bottom: 0 z-index: 50 width: 100% height: 48px .content display: flex background: #141d27 font-size: 0 .content-left flex: 1 .logo-wrapper display: inline-block position: relative top:-10px margin: 0 12px padding: 6px width: 56px height: 56px vertical-align: top box-sizing: border-box border-radius: 50% background: #141d27 .logo width: 100% height: 100% border-radius: 50% text-align: center background: #2b343c &.highlight background: rgb(0,160,220) .icon-shopping_cart font-size: 24px line-height: 44px color: #80858a &.highlight color: #fff .num position: absolute top: 0px right: 0px width: 24px height: 16px line-height: 16px text-align: center border-radius: 16px font-size: 9px font-weight: 700 color: #fff background: rgb(240,20,20) box-shadow: 0 4px 8px 0 rgba(0,0,0,0.4) .price display: inline-block vertical-align: top margin-top: 12px line-height: 24px padding-right: 12px box-sizing: border-box border-right: 1px solid rgba(255,255,255,0.1) font-size: 16px font-weight: 700 color: rgba(255,255,255,0.4) &.highlight color: #fff .desc display: inline-block vertical-align: top line-height: 24px margin: 12px 0 0 12px font-size: 10px color: rgba(255,255,255,0.4) .content-right flex: 0 0 105px width: 105px .pay height: 48px line-height: 48px text-align: center font-size: 12px color: rgba(255,255,255,0.4) font-weight: 700 background: #2b333b &.not-enough background: #2b333b &.enough background: #00b43c color: #fff .ball-container .ball position: fixed left: 32px bottom: 22px z-index: 200 transition: all 0.4s cubic-bezier(0.49, -0.29, 0.75, 0.41) .inner width: 16px height: 16px border-radius: 50% background: rgb(0, 160, 220) transition: all 0.4s linear .shopcart-list position: absolute left: 0 top: 0 z-index: -1 width: 100% transform: translate3d(0, -100%, 0) &.fold-enter-active, &.fold-leave-active transition: all 0.5s &.fold-enter, &.fold-leave-active transform: translate3d(0, 0, 0) .list-header height: 40px line-height: 40px padding: 0 18px background: #f3f5f7 border-bottom: 1px solid rgba(7, 17, 27, 0.1) .title float: left font-size: 14px color: rgb(7, 17, 27) .empty float: right font-size: 12px color: rgb(0, 160, 220) .list-content padding: 0 18px max-height: 217px overflow: hidden background: #fff .food position: relative padding: 12px 0 box-sizing: border-box border-1px(rgba(7, 17, 27, 0.1)) .name line-height: 24px font-size: 14px color: rgb(7, 17, 27) .price position: absolute right: 90px bottom: 12px line-height: 24px font-size: 14px font-weight: 700 color: rgb(240, 20, 20) .cartcontrol-wrapper position: absolute right: 0 bottom: 6px </style>
购物车详情页(三)
继续编写shopcart.vue组件,实现增加和减少功能
<template> <div class="shopcart"> <div class="content" @click="toggleList"> <div class="content-left"> <div class="logo-wrapper"> <div class="logo" :class="{'highlight':totalCount>0}"> <span class="icon-shopping_cart" :class="{'highlight':totalCount>0}"></span> </div> <div class="num" v-show="totalCount>0">{{totalCount}}</div> </div> <div class="price" :class="{'highlight':totalPrice>0}">¥{{totalPrice}}</div> <div class="desc">另需配送费¥{{deliveryPrice}}元</div> </div> <div class="content-right"> <div class="pay" :class="payClass"> {{payDesc}} </div> </div> </div> <div class="ball-container"> <div v-for="ball in balls"> <transition name="drop"> <div class="ball" v-show="ball.show"> <div class="inner inner-hook"></div> </div> </transition> </div> </div> <transition name="fold"> <div class="shopcart-list" v-show="listShow"> <div class="list-header"> <h1 class="title">购物车</h1> <span class="empty">清空</span> </div> <div class="list-content" ref="listContent"> <ul> <li class="food" v-for="food in selectFoods"> <span class="name">{{food.name}}</span> <div class="price"> <span>¥{{food.price*food.count}}</span> </div> <div class="cartcontrol-wrapper"> <cartcontrol :food="food"></cartcontrol> </div> </li> </ul> </div> </div> </transition> </div> </template> <script> import BScroll from 'better-scroll'; import cartcontrol from '../../components/cartcontrol/cartcontrol'; export default { name: 'v-shopcart', props: { selectFoods: { type: Array, default () { return [{ price: 10, count: 5 }]; } }, deliveryPrice: { type: Number, default: 0 }, minPrice: { type: Number, default: 0 } }, data() { return { balls: [{ show: false }, { show: false }, { show: false }, { show: false }, { show: false } ], dropBall: [], fold: true }; }, computed: { totalPrice() { let total = 0; this.selectFoods.forEach((food) => { total += food.price * food.count; }); return total; }, totalCount() { let count = 0; this.selectFoods.forEach((food) => { count += food.count; }); return count; }, payDesc() { if (this.totalPrice === 0) { return `¥${this.minPrice}元起送`; } else if (this.totalPrice < this.minPrice) { let diff = this.minPrice - this.totalPrice; return `还差¥${diff}元起送`; } else { return '去结算'; } }, payClass() { if (this.totalPrice < this.minPrice) { return 'not-enough'; } else { return 'enough'; } }, listShow() { if (!this.totalCount) { this.fold = true; return false; } let show = !this.fold; if (show) { this.$nextTick(() => { if (!this.scroll) { this.scroll = new BScroll(this.$refs.listContent, { click: true }); } else { this.scroll.refresh(); } }); } return show; } }, methods: { drop(el) { for (let i = 0; i < this.balls.length; i++) { let ball = this.balls[i]; if (!ball.show) { ball.show = true; ball.el = el; this.dropBall.push(ball); return; } } }, toggleList() { if (!this.totalCount) { return; } this.fold = !this.fold; } }, transitions: { drop: { beforeDrop(el) { let count = this.balls.length; while (count--) { let ball = this.balls[count]; if (ball.show) { let rect = ball.el.getBoundingClientRect(); let x = rect.left - 32; let y = -(window.innerHeight - rect.top - 22); el.style.display = ''; el.style.webkitTransform = `translate3d(0,${y}px,0)`; el.style.transform = `translate3d(0,${y}px,0)`; let inner = el.getElementsByClassName('inner-hook')[0]; inner.style.webkitTransform = `translate3d(${x}px,0,0)`; inner.style.transform = `translate3d(${x}px,0,0)`; } } }, enter(el) { /* eslint-disable no-unused-vars */ let rf = el.offsetHeight; this.$nextTick(() => { el.style.webkitTransform = 'translate3d(0,0,0)'; el.style.transform = 'translate3d(0,0,0)'; let inner = el.getElementsByClassName('inner-hook')[0]; inner.style.webkitTransform = 'translate3d(0,0,0)'; inner.style.transform = 'translate3d(0,0,0)'; el.addEventListener('transitionend', done); }); }, afterEnter(el) { let ball = this.dropBalls.shift(); if (ball) { ball.show = false; el.style.display = 'none'; } } } }, components: { cartcontrol } } </script> <style scoped lang="stylus" rel="stylesheet/stylus"> @import "../../common/stylus/mixin"; .shopcart position: fixed left: 0 bottom: 0 z-index: 50 width: 100% height: 48px .content display: flex background: #141d27 font-size: 0 .content-left flex: 1 .logo-wrapper display: inline-block position: relative top:-10px margin: 0 12px padding: 6px width: 56px height: 56px vertical-align: top box-sizing: border-box border-radius: 50% background: #141d27 .logo width: 100% height: 100% border-radius: 50% text-align: center background: #2b343c &.highlight background: rgb(0,160,220) .icon-shopping_cart font-size: 24px line-height: 44px color: #80858a &.highlight color: #fff .num position: absolute top: 0px right: 0px width: 24px height: 16px line-height: 16px text-align: center border-radius: 16px font-size: 9px font-weight: 700 color: #fff background: rgb(240,20,20) box-shadow: 0 4px 8px 0 rgba(0,0,0,0.4) .price display: inline-block vertical-align: top margin-top: 12px line-height: 24px padding-right: 12px box-sizing: border-box border-right: 1px solid rgba(255,255,255,0.1) font-size: 16px font-weight: 700 color: rgba(255,255,255,0.4) &.highlight color: #fff .desc display: inline-block vertical-align: top line-height: 24px margin: 12px 0 0 12px font-size: 10px color: rgba(255,255,255,0.4) .content-right flex: 0 0 105px width: 105px .pay height: 48px line-height: 48px text-align: center font-size: 12px color: rgba(255,255,255,0.4) font-weight: 700 background: #2b333b &.not-enough background: #2b333b &.enough background: #00b43c color: #fff .ball-container .ball position: fixed left: 32px bottom: 22px z-index: 200 transition: all 0.4s cubic-bezier(0.49, -0.29, 0.75, 0.41) .inner width: 16px height: 16px border-radius: 50% background: rgb(0, 160, 220) transition: all 0.4s linear .shopcart-list position: absolute left: 0 top: 0 z-index: -1 width: 100% transform: translate3d(0, -100%, 0) &.fold-enter-active, &.fold-leave-active transition: all 0.5s &.fold-enter, &.fold-leave-active transform: translate3d(0, 0, 0) .list-header height: 40px line-height: 40px padding: 0 18px background: #f3f5f7 border-bottom: 1px solid rgba(7, 17, 27, 0.1) .title float: left font-size: 14px color: rgb(7, 17, 27) .empty float: right font-size: 12px color: rgb(0, 160, 220) .list-content padding: 0 18px max-height: 217px overflow: hidden background: #fff .food position: relative padding: 12px 0 box-sizing: border-box border-1px(rgba(7, 17, 27, 0.1)) .name line-height: 24px font-size: 14px color: rgb(7, 17, 27) .price position: absolute right: 90px bottom: 12px line-height: 24px font-size: 14px font-weight: 700 color: rgb(240, 20, 20) .cartcontrol-wrapper position: absolute right: 0 bottom: 6px </style>
购物车详情页(四)
继续编写shopcart.vue组件,实现蒙层效果
<template> <div class="shopcart"> <div class="content" @click="toggleList"> <div class="content-left"> <div class="logo-wrapper"> <div class="logo" :class="{'highlight':totalCount>0}"> <span class="icon-shopping_cart" :class="{'highlight':totalCount>0}"></span> </div> <div class="num" v-show="totalCount>0">{{totalCount}}</div> </div> <div class="price" :class="{'highlight':totalPrice>0}">¥{{totalPrice}}</div> <div class="desc">另需配送费¥{{deliveryPrice}}元</div> </div> <!--@click.stop.prevent阻止冒泡行为--> <div class="content-right" @click.stop.prevent="pay"> <div class="pay" :class="payClass"> {{payDesc}} </div> </div> </div> <div class="ball-container"> <div v-for="ball in balls"> <transition name="drop"> <div class="ball" v-show="ball.show"> <div class="inner inner-hook"></div> </div> </transition> </div> </div> <transition name="fold"> <div class="shopcart-list" v-show="listShow"> <div class="list-header"> <h1 class="title">购物车</h1> <span class="empty" @click="empty">清空</span> </div> <div class="list-content" ref="listContent"> <ul> <li class="food" v-for="food in selectFoods"> <span class="name">{{food.name}}</span> <div class="price"> <span>¥{{food.price*food.count}}</span> </div> <div class="cartcontrol-wrapper"> <cartcontrol :food="food"></cartcontrol> </div> </li> </ul> </div> </div> </transition> <transition name="fade"> <div class="list-mask" @click="hideList" v-show="listShow"></div> </transition> </div> </template> <script> import BScroll from 'better-scroll'; import cartcontrol from '../../components/cartcontrol/cartcontrol'; export default { name: 'v-shopcart', props: { selectFoods: { type: Array, default () { return [{ price: 10, count: 5 }]; } }, deliveryPrice: { type: Number, default: 0 }, minPrice: { type: Number, default: 0 } }, data() { return { balls: [{ show: false }, { show: false }, { show: false }, { show: false }, { show: false } ], dropBall: [], fold: true }; }, computed: { totalPrice() { let total = 0; this.selectFoods.forEach((food) => { total += food.price * food.count; }); return total; }, totalCount() { let count = 0; this.selectFoods.forEach((food) => { count += food.count; }); return count; }, payDesc() { if (this.totalPrice === 0) { return `¥${this.minPrice}元起送`; } else if (this.totalPrice < this.minPrice) { let diff = this.minPrice - this.totalPrice; return `还差¥${diff}元起送`; } else { return '去结算'; } }, payClass() { if (this.totalPrice < this.minPrice) { return 'not-enough'; } else { return 'enough'; } }, listShow() { if (!this.totalCount) { this.fold = true; return false; } let show = !this.fold; if (show) { this.$nextTick(() => { if (!this.scroll) { this.scroll = new BScroll(this.$refs.listContent, { click: true }); } else { this.scroll.refresh(); } }); } return show; } }, methods: { drop(el) { for (let i = 0; i < this.balls.length; i++) { let ball = this.balls[i]; if (!ball.show) { ball.show = true; ball.el = el; this.dropBall.push(ball); return; } } }, toggleList() { if (!this.totalCount) { return; } this.fold = !this.fold; }, hideList() { this.fold = true; }, empty() { this.selectFoods.forEach((food) => { food.count = 0; }); }, pay() { if (this.totalPrice < this.minPrice) { return; } window.alert(`支付${this.totalPrice}元`); } }, transitions: { drop: { beforeDrop(el) { let count = this.balls.length; while (count--) { let ball = this.balls[count]; if (ball.show) { let rect = ball.el.getBoundingClientRect(); let x = rect.left - 32; let y = -(window.innerHeight - rect.top - 22); el.style.display = ''; el.style.webkitTransform = `translate3d(0,${y}px,0)`; el.style.transform = `translate3d(0,${y}px,0)`; let inner = el.getElementsByClassName('inner-hook')[0]; inner.style.webkitTransform = `translate3d(${x}px,0,0)`; inner.style.transform = `translate3d(${x}px,0,0)`; } } }, enter(el) { /* eslint-disable no-unused-vars */ let rf = el.offsetHeight; this.$nextTick(() => { el.style.webkitTransform = 'translate3d(0,0,0)'; el.style.transform = 'translate3d(0,0,0)'; let inner = el.getElementsByClassName('inner-hook')[0]; inner.style.webkitTransform = 'translate3d(0,0,0)'; inner.style.transform = 'translate3d(0,0,0)'; el.addEventListener('transitionend', done); }); }, afterEnter(el) { let ball = this.dropBalls.shift(); if (ball) { ball.show = false; el.style.display = 'none'; } } } }, components: { cartcontrol } } </script> <style scoped lang="stylus" rel="stylesheet/stylus"> @import "../../common/stylus/mixin"; .shopcart position: fixed left: 0 bottom: 0 z-index: 50 width: 100% height: 48px .content display: flex background: #141d27 font-size: 0 .content-left flex: 1 .logo-wrapper display: inline-block position: relative top:-10px margin: 0 12px padding: 6px width: 56px height: 56px vertical-align: top box-sizing: border-box border-radius: 50% background: #141d27 .logo width: 100% height: 100% border-radius: 50% text-align: center background: #2b343c &.highlight background: rgb(0,160,220) .icon-shopping_cart font-size: 24px line-height: 44px color: #80858a &.highlight color: #fff .num position: absolute top: 0px right: 0px width: 24px height: 16px line-height: 16px text-align: center border-radius: 16px font-size: 9px font-weight: 700 color: #fff background: rgb(240,20,20) box-shadow: 0 4px 8px 0 rgba(0,0,0,0.4) .price display: inline-block vertical-align: top margin-top: 12px line-height: 24px padding-right: 12px box-sizing: border-box border-right: 1px solid rgba(255,255,255,0.1) font-size: 16px font-weight: 700 color: rgba(255,255,255,0.4) &.highlight color: #fff .desc display: inline-block vertical-align: top line-height: 24px margin: 12px 0 0 12px font-size: 10px color: rgba(255,255,255,0.4) .content-right flex: 0 0 105px width: 105px .pay height: 48px line-height: 48px text-align: center font-size: 12px color: rgba(255,255,255,0.4) font-weight: 700 background: #2b333b &.not-enough background: #2b333b &.enough background: #00b43c color: #fff .ball-container .ball position: fixed left: 32px bottom: 22px z-index: 200 transition: all 0.4s cubic-bezier(0.49, -0.29, 0.75, 0.41) .inner width: 16px height: 16px border-radius: 50% background: rgb(0, 160, 220) transition: all 0.4s linear .shopcart-list position: absolute left: 0 top: 0 z-index: 50 width: 100% transform: translate3d(0, -100%, 0) &.fold-enter-active, &.fold-leave-active transition: all 0.5s &.fold-enter, &.fold-leave-active transform: translate3d(0, 0, 0) .list-header height: 40px line-height: 40px padding: 0 18px background: #f3f5f7 border-bottom: 1px solid rgba(7, 17, 27, 0.1) .title float: left font-size: 14px color: rgb(7, 17, 27) .empty float: right font-size: 12px color: rgb(0, 160, 220) .list-content padding: 0 18px max-height: 217px overflow: hidden background: #fff .food position: relative padding: 12px 0 box-sizing: border-box border-1px(rgba(7, 17, 27, 0.1)) .name line-height: 24px font-size: 14px color: rgb(7, 17, 27) .price position: absolute right: 90px bottom: 12px line-height: 24px font-size: 14px font-weight: 700 color: rgb(240, 20, 20) .cartcontrol-wrapper position: absolute right: 0 bottom: 6px .list-mask position: fixed top: 0 left: 0 width: 100% height: 100% z-index: 40 backdrop-filter: blur(10px) opacity: 1 background: rgba(7, 17, 27, 0.6) &.fade-enter-active, &.fade-leave-active transition: all 0.5s &.fade-enter, &.fade-leave-active opacity: 0 background: rgba(7, 17, 27, 0) </style>
商品详情页实现(一)
步骤一:编写goods.vue组件
<template> <div> <div class="goods"> <div class="menu-wrapper" ref="menuWrapper"> <ul> <li v-for="(item,index) in goods" class="menu-item" :class="{'current':currentIndex===index}" @click="selectMenu(index,$event)"> <span class="text border-1px"> <span v-show="item.type>0" class="icon" :class="classMap[item.type]"></span> {{item.name}} </span> </li> </ul> </div> <div class="foods-wrapper" ref="foodsWrapper"> <ul> <li v-for="item in goods" class="food-list food-list-hook"> <h1 class="title">{{item.name}}</h1> <ul> <li @click="selectFood(food,$event)" v-for="food in item.foods" class="food-item border-1px"> <div class="icon"> <img :src="food.icon" width="57" height="57" /> </div> <div class="content"> <h2 class="name">{{food.name}}</h2> <p class="desc">{{food.description}}</p> <div class="extra"> <span class="count">月售{{food.sellCount}}份</span> <span>好评率{{food.rating}}%</span> </div> <div class="price"> <span class="now">¥{{food.price}}</span> <span class="old" v-show="food.oldPrice">¥{{food.oldPrice}}</span> </div> <div class="cartcontrol-wrapper"> <cartcontrol :food="food"></cartcontrol> </div> </div> </li> </ul> </li> </ul> </div> <shopcart ref:shopcart :select-foods="selectFoods" :delivery-price="seller.deliveryPrice" :min-price="seller.minPrice"></shopcart> </div> <food :food="selectedFood"></food> </div> </template> <script> import BScroll from 'better-scroll'; import shopcart from '../../components/shopcart/shopcart'; import cartcontrol from '../../components/cartcontrol/cartcontrol'; import food from '../../components/food/food'; const ERR_OK = 0; export default { name: 'v-goods', props: { seller: { type: Object } }, data() { return { goods: [], listHeight: [], scrollY: 0, selectedFood: {} }; }, computed: { currentIndex() { for (let i = 0; i < this.listHeight.length; i++) { let height1 = this.listHeight[i]; let height2 = this.listHeight[i + 1]; if (!height2 || (this.scrollY >= height1 && this.scrollY < height2)) { return i; } } return 0; }, selectFoods() { let foods = []; this.goods.forEach((good) => { good.foods.forEach((food) => { if (food.count) { foods.push(food); } }); }); return foods; } }, created() { this.classMap = ['decrease', 'discount', 'special', 'invoice', 'guarantee']; this.$http.get('api/goods').then((response) => { response = response.body; if (response.error === ERR_OK) { this.goods = response.data; } this.goods = response.data; this.$nextTick(() => { this._initScroll(); this._calculateHeight(); }); }); }, methods: { selectMenu(index, event) { if (!event._constructed) { return; } let foodList = this.$refs.foodsWrapper.getElementsByClassName('food-list-hook'); let el = foodList[index]; this.foodsScroll.scrollToElement(el, 300); console.log(index); }, selectFood(food, event) { if (!event._constructed) { return; } this.selectedFood = food; }, _drop(target) { //体验优化,异步执行下落动画 this.$nextTick(() => { this.$refs.shopcart.drop(target); }); }, _initScroll() { this.menuScroll = new BScroll(this.$refs.menuWrapper, { click: true }); this.foodsScroll = new BScroll(this.$refs.foodsWrapper, { click: true, probeType: 3 }); this.foodsScroll.on("scroll", (pos) => { this.scrollY = Math.abs(Math.round(pos.y)); }); }, _calculateHeight() { let foodList = this.$refs.foodsWrapper.getElementsByClassName('food-list-hook'); let height = 0; this.listHeight.push(height); for (let i = 0; i < foodList.length; i++) { let item = foodList[i]; height += item.clientHeight; this.listHeight.push(height); } } }, components: { shopcart, cartcontrol, food }, events: { 'cart.add' (target) { this._drop(target); } } } </script> <style scoped lang="stylus" rel="stylesheet/stylus"> @import "../../common/stylus/mixin"; .goods display: flex position: absolute top: 174px bottom: 46px width: 100% overflow: hidden .menu-wrapper flex: 0 0 80px width: 80px background: #f3f5f7 .menu-item display: table height: 54px width: 56px padding: 0 12px line-height: 14px &.current position: relative z-index: 10 margin-top: -1px background: #ccc font-weight: 700 .text border-none() .icon display: inline-block vertical-align: top width: 12px height: 12px margin-right: 2px background-size: 12px 12px background-repeat: no-repeat &.decrease bg-image('decrease_3') &.discount bg-image('discount_3') &.guarantee bg-image('guarantee_3') &.invoice bg-image('invoice_3') &.special bg-image('special_3') .text display: table-cell width: 56px vertical-align: middle border-1px(rgba(7,17,27,0.1)) font-size: 12px .foods-wrapper flex: 1 .title padding-left: 14px height: 26px line-height: 26px border-left: 2px solid #d9dde1 font-size: 12px color: rgb(147,153,159) background: #f3f5f7 .food-item display: flex margin: 18px padding-bottom: 18px border-1px(rgba(7,17,27,0.1)) &:last-child border-none() margin-bottom: 0 .icon flex: 0 0 57px margin-right: 10px .content flex: 1 .name margin: 2px 0 8px 0 height: 14px line-height: 14px font-size: 14px color: rgb(7,17,27) .desc, .extra line-height: 10px font-size: 10px color: rgb(147,153,159) .desc margin-bottom: 8px line-height: 14px .extra .count margin-right: 12px .price font-weight: 700 line-height: 24px .now margin-right: 8px font-size: 14px color: rgb(240,20,20) .old text-decoration: line-through font-size: 10px color: rgb(147,153,159) .cartcontrol-wrapper position: absolute right: 0 bottom: 12px </style>
步骤二:新建food.vuez组件
<template> <div v-show="showFlag" class="food"></div> </template> <script> export default { name: 'v-food', props: { food: { type: Object } }, data() { return { showFlag: false }; } } </script> <style scoped lang="stylus" rel="stylesheet/stylus"> .food position: fixed left: 0 top: 0 bottom: 48px </style>
商品详情实现(二)
步骤一:编写goods.vue组件
<template> <div> <div class="goods"> <div class="menu-wrapper" ref="menuWrapper"> <ul> <li v-for="(item,index) in goods" class="menu-item" :class="{'current':currentIndex===index}" @click="selectMenu(index,$event)"> <span class="text border-1px"> <span v-show="item.type>0" class="icon" :class="classMap[item.type]"></span> {{item.name}} </span> </li> </ul> </div> <div class="foods-wrapper" ref="foodsWrapper"> <ul> <li v-for="item in goods" class="food-list food-list-hook"> <h1 class="title">{{item.name}}</h1> <ul> <li @click="selectFood(food,$event)" v-for="food in item.foods" class="food-item border-1px"> <div class="icon"> <img :src="food.icon" width="57" height="57" /> </div> <div class="content"> <h2 class="name">{{food.name}}</h2> <p class="desc">{{food.description}}</p> <div class="extra"> <span class="count">月售{{food.sellCount}}份</span> <span>好评率{{food.rating}}%</span> </div> <div class="price"> <span class="now">¥{{food.price}}</span> <span class="old" v-show="food.oldPrice">¥{{food.oldPrice}}</span> </div> <div class="cartcontrol-wrapper"> <cartcontrol :food="food"></cartcontrol> </div> </div> </li> </ul> </li> </ul> </div> <shopcart ref:shopcart :select-foods="selectFoods" :delivery-price="seller.deliveryPrice" :min-price="seller.minPrice"></shopcart> </div> <food :food="selectedFood" ref="food"></food> </div> </template> <script> import BScroll from 'better-scroll'; import shopcart from '../../components/shopcart/shopcart'; import cartcontrol from '../../components/cartcontrol/cartcontrol'; import food from '../../components/food/food'; const ERR_OK = 0; export default { name: 'v-goods', props: { seller: { type: Object } }, data() { return { goods: [], listHeight: [], scrollY: 0, selectedFood: {} }; }, computed: { currentIndex() { for (let i = 0; i < this.listHeight.length; i++) { let height1 = this.listHeight[i]; let height2 = this.listHeight[i + 1]; if (!height2 || (this.scrollY >= height1 && this.scrollY < height2)) { return i; } } return 0; }, selectFoods() { let foods = []; this.goods.forEach((good) => { good.foods.forEach((food) => { if (food.count) { foods.push(food); } }); }); return foods; } }, created() { this.classMap = ['decrease', 'discount', 'special', 'invoice', 'guarantee']; this.$http.get('api/goods').then((response) => { response = response.body; if (response.error === ERR_OK) { this.goods = response.data; } this.goods = response.data; this.$nextTick(() => { this._initScroll(); this._calculateHeight(); }); }); }, methods: { selectMenu(index, event) { if (!event._constructed) { return; } let foodList = this.$refs.foodsWrapper.getElementsByClassName('food-list-hook'); let el = foodList[index]; this.foodsScroll.scrollToElement(el, 300); console.log(index); }, selectFood(food, event) { if (!event._constructed) { return; } this.selectedFood = food; this.$refs.food.show(); }, _drop(target) { //体验优化,异步执行下落动画 this.$nextTick(() => { this.$refs.shopcart.drop(target); }); }, _initScroll() { this.menuScroll = new BScroll(this.$refs.menuWrapper, { click: true }); this.foodsScroll = new BScroll(this.$refs.foodsWrapper, { click: true, probeType: 3 }); this.foodsScroll.on("scroll", (pos) => { this.scrollY = Math.abs(Math.round(pos.y)); }); }, _calculateHeight() { let foodList = this.$refs.foodsWrapper.getElementsByClassName('food-list-hook'); let height = 0; this.listHeight.push(height); for (let i = 0; i < foodList.length; i++) { let item = foodList[i]; height += item.clientHeight; this.listHeight.push(height); } } }, components: { shopcart, cartcontrol, food }, events: { 'cart.add' (target) { this._drop(target); } } } </script> <style scoped lang="stylus" rel="stylesheet/stylus"> @import "../../common/stylus/mixin"; .goods display: flex position: absolute top: 174px bottom: 46px width: 100% overflow: hidden .menu-wrapper flex: 0 0 80px width: 80px background: #f3f5f7 .menu-item display: table height: 54px width: 56px padding: 0 12px line-height: 14px &.current position: relative z-index: 10 margin-top: -1px background: #ccc font-weight: 700 .text border-none() .icon display: inline-block vertical-align: top width: 12px height: 12px margin-right: 2px background-size: 12px 12px background-repeat: no-repeat &.decrease bg-image('decrease_3') &.discount bg-image('discount_3') &.guarantee bg-image('guarantee_3') &.invoice bg-image('invoice_3') &.special bg-image('special_3') .text display: table-cell width: 56px vertical-align: middle border-1px(rgba(7,17,27,0.1)) font-size: 12px .foods-wrapper flex: 1 .title padding-left: 14px height: 26px line-height: 26px border-left: 2px solid #d9dde1 font-size: 12px color: rgb(147,153,159) background: #f3f5f7 .food-item display: flex margin: 18px padding-bottom: 18px border-1px(rgba(7,17,27,0.1)) &:last-child border-none() margin-bottom: 0 .icon flex: 0 0 57px margin-right: 10px .content flex: 1 .name margin: 2px 0 8px 0 height: 14px line-height: 14px font-size: 14px color: rgb(7,17,27) .desc, .extra line-height: 10px font-size: 10px color: rgb(147,153,159) .desc margin-bottom: 8px line-height: 14px .extra .count margin-right: 12px .price font-weight: 700 line-height: 24px .now margin-right: 8px font-size: 14px color: rgb(240,20,20) .old text-decoration: line-through font-size: 10px color: rgb(147,153,159) .cartcontrol-wrapper position: absolute right: 0 bottom: 12px </style>
步骤二:编写food.vue组件,实现切换动画效果
<template> <transition name="move"> <div v-show="showFlag" class="food"> <div class="food-content"> <div class="image-header"> <img :src="food.image" /> </div> </div> </div> </transition> </template> <script> export default { name: 'v-food', props: { food: { type: Object } }, data() { return { showFlag: false }; }, methods: { show() { this.showFlag = true; } } } </script> <style scoped lang="stylus" rel="stylesheet/stylus"> .food position: fixed left: 0 top: 0 bottom: 48px z-index: 30px width: 100% background: #fff transform: translate3d(0, 0, 0) &.move-enter-active, &.move-leave-active transition: all 0.2s linear &.move-enter, &.move-leave-active transform: translate3d(100%, 0, 0) </style>
商品详情实现(三)
编写food.vue组件,实现后退效果以及部分内容展示
<template> <transition name="move"> <div v-show="showFlag" class="food"> <div class="food-content"> <div class="image-header"> <img :src="food.image" /> <div class="back" @click="hide"> <i class="icon-arrow_lift"></i> </div> </div> <div class="content"> <h1 class="title">{{food.name}}</h1> <div class="detail"> <span class="sell-count">月售{{food.sellCount}}份</span> <span class="rating">好评率{{food.rating}}</span> </div> <div class="price"> <span class="now">¥{{food.price}}</span> <span class="old" v-show="food.oldPrice">¥{{food.oldPrice}}</span> </div> </div> </div> </div> </transition> </template> <script> export default { name: 'v-food', props: { food: { type: Object } }, data() { return { showFlag: false }; }, methods: { show() { this.showFlag = true; }, hide() { this.showFlag = false; } } } </script> <style scoped lang="stylus" rel="stylesheet/stylus"> .food position: fixed left: 0 top: 0 bottom: 48px z-index: 30px width: 100% background: #fff transform: translate3d(0, 0, 0) &.move-enter-active, &.move-leave-active transition: all 0.2s linear &.move-enter, &.move-leave-active transform: translate3d(100%, 0, 0) .image-header position: relative width: 100% height: 0 padding-top: 100% img position: absolute top: 0 left: 0 width: 100% height: 100% .back position: absolute top: 10px left: 0 .icon-arrow_lift display: block padding: 10px font-size: 20px color: #fff </style>
商品详情实现(四)
编写food.vue组件,增加cartcontrol.vue组件
<template> <transition name="move"> <div v-show="showFlag" class="food" ref="food"> <div class="food-content"> <div class="image-header"> <img :src="food.image" /> <div class="back" @click="hide"> <i class="icon-arrow_lift"></i> </div> </div> <div class="content"> <h1 class="title">{{food.name}}</h1> <div class="detail"> <span class="sell-count">月售{{food.sellCount}}份</span> <span class="rating">好评率{{food.rating}}%</span> </div> <div class="price"> <span class="now">¥{{food.price}}</span> <span class="old" v-show="food.oldPrice">¥{{food.oldPrice}}</span> </div> </div> <div class="cartcontrol-wrapper"> <cartcontrol :food="food"></cartcontrol> </div> </div> </div> </transition> </template> <script> import BScroll from 'better-scroll'; import cartcontrol from '../../components/cartcontrol/cartcontrol'; export default { name: 'v-food', props: { food: { type: Object } }, data() { return { showFlag: false }; }, methods: { show() { this.showFlag = true; this.$nextTick(() => { if (!this.scroll) { this.scroll = new BScroll(this.$refs.food, { click: true }); } else { this.scroll.refresh(); } }); }, hide() { this.showFlag = false; } }, components: { cartcontrol } } </script> <style scoped lang="stylus" rel="stylesheet/stylus"> .food position: fixed left: 0 top: 0 bottom: 48px z-index: 30px width: 100% background: #fff transform: translate3d(0, 0, 0) &.move-enter-active, &.move-leave-active transition: all 0.2s linear &.move-enter, &.move-leave-active transform: translate3d(100%, 0, 0) .image-header position: relative width: 100% height: 0 padding-top: 100% img position: absolute top: 0 left: 0 width: 100% height: 100% .back position: absolute top: 10px left: 0 .icon-arrow_lift display: block padding: 10px font-size: 20px color: #fff .content padding: 18px .title line-height: 14px margin-bottom: 8px font-size: 14px font-weight: 700 color: rgb(7, 17, 27) .detail margin-bottom: 18px line-height: 10px height: 10px font-size: 0 .sell-count, .rating font-size: 10px color: rgb(147, 153, 159) .sell-count margin-right: 12px .price font-weight: 700 line-height: 24px .now margin-right: 8px font-size: 14px color: rgb(240, 20, 20) .old text-decoration: line-through font-size: 10px color: rgb(147, 153, 159) </style>
商品详情实现(五)
步骤一:编写food.vue组件,实现添加购物车按钮(动画效果依然无法实现)
<template> <transition name="move"> <div v-show="showFlag" class="food" ref="food"> <div class="food-content"> <div class="image-header"> <img :src="food.image" /> <div class="back" @click="hide"> <i class="icon-arrow_lift"></i> </div> </div> <div class="content"> <h1 class="title">{{food.name}}</h1> <div class="detail"> <span class="sell-count">月售{{food.sellCount}}份</span> <span class="rating">好评率{{food.rating}}%</span> </div> <div class="price"> <span class="now">¥{{food.price}}</span> <span class="old" v-show="food.oldPrice">¥{{food.oldPrice}}</span> </div> </div> <div class="cartcontrol-wrapper"> <cartcontrol :food="food"></cartcontrol> </div> <transition name="fade"> <div @click.stop.prevent="addFirst" class="buy" v-show="!food.count || food.count===0"> 加入购物车 </div> </transition> </div> </div> </transition> </template> <script> import BScroll from 'better-scroll'; import Vue from 'vue'; import cartcontrol from '../../components/cartcontrol/cartcontrol'; export default { name: 'v-food', props: { food: { type: Object } }, data() { return { showFlag: false }; }, methods: { show() { this.showFlag = true; this.$nextTick(() => { if (!this.scroll) { this.scroll = new BScroll(this.$refs.food, { click: true }); } else { this.scroll.refresh(); } }); }, hide() { this.showFlag = false; }, addFirst(event) { if(!event._constructed) { return; } console.log(event.target); this.$emit('add', event.target); Vue.set(this.food, 'count', 1); } }, components: { cartcontrol } } </script> <style scoped lang="stylus" rel="stylesheet/stylus"> .food position: fixed left: 0 top: 0 bottom: 48px z-index: 30px width: 100% background: #fff transform: translate3d(0, 0, 0) &.move-enter-active, &.move-leave-active transition: all 0.2s linear &.move-enter, &.move-leave-active transform: translate3d(100%, 0, 0) .image-header position: relative width: 100% height: 0 padding-top: 100% img position: absolute top: 0 left: 0 width: 100% height: 100% .back position: absolute top: 10px left: 0 .icon-arrow_lift display: block padding: 10px font-size: 20px color: #fff .content padding: 18px .title line-height: 14px margin-bottom: 8px font-size: 14px font-weight: 700 color: rgb(7, 17, 27) .detail margin-bottom: 18px line-height: 10px height: 10px font-size: 0 .sell-count, .rating font-size: 10px color: rgb(147, 153, 159) .sell-count margin-right: 12px .price font-weight: 700 line-height: 24px .now margin-right: 8px font-size: 14px color: rgb(240, 20, 20) .old text-decoration: line-through font-size: 10px color: rgb(147, 153, 159) .cartcontrol-wrapper position: absolute right: 12px bottom: 12px .buy position: absolute right: 18px bottom: 18px z-index: 10 height: 24px line-height: 24px padding: 0 12px box-sizing: border-box border-radius: 12px font-size: 10px color: #fff background: rgb(0, 160, 220) opacity: 1 &.fade-enter-active, &.fade-leave-active transition: all 0.2s &.fade-enter, &.fade-leave-active opacity: 0 z-index: -1 </style>
步骤二:编写cartcontrol.vue组件,阻止点击事件冒泡行为
<template> <div class="cartcontrol"> <transition name="move"> <div class="cart-decrease" v-show="food.count>0" @click.stop.prevent="decreaseCart"> <span class="inner icon-remove_circle_outline"></span> </div> </transition> <div class="cart-count" v-show="food.count>0">{{food.count}}</div> <div class="cart-add icon-add_circle" @click.stop.prevent="addCart"></div> </div> </template> <script> import Vue from 'vue'; export default { name: 'v-cartcontrol', props: { food: { type: Object } }, methods: { addCart(event) { if (!event._constructed) { return; } if (!this.food.count) { Vue.set(this.food, 'count', 1); } else { this.food.count++; } }, decreaseCart(event) { if (!event._constructed) { return; } if (this.food.count) { this.food.count--; } } } } </script> <style scoped lang="stylus" rel="stylesheet/stylus"> .cartcontrol font-size: 0 .cart-decrease padding: 6px color: rgb(0,160,220) display: inline-block opacity: 1 transform: translate3d(0, 0, 0) .inner display: inline-block line-height: 24px font-size: 24px color: rgb(0, 160, 220) transition: all 0.4s linear transform: rotate(0) &.move-enter-active, &.move-leave-active transition: all 0.4s linear &.move-enter, &.move-leave-active opacity: 0 transform: translate3d(24px, 0, 0) .inner transform: rotate(180deg) .cart-count display: inline-block vertical-align: top width: 12px padding-top: 6px line-height: 24px text-align: center font-size: 10px color: rgb(147,153,159) .cart-add display: inline-block padding: 6px line-height: 24px font-size: 24px color: rgb(0, 160, 220) </style>
split组件实现
步骤一:新建split.vue组件
<template> <div class="split"></div> </template> <script> export default {}; </script> <style lang="stylus" rel="stylesheet/stylus"> .split width: 100% height: 16px border-top: 1px solid rgba(7, 17, 27, 0.1) border-bottom: 1px solid rgba(7, 17, 27, 0.1) /*background: #f3f5f7*/ background: #ccc </style>
步骤二:编写food.vue组件,继续添加内容和添加split.vue组件
<template> <transition name="move"> <div v-show="showFlag" class="food" ref="food"> <div class="food-content"> <div class="image-header"> <img :src="food.image" /> <div class="back" @click="hide"> <i class="icon-arrow_lift"></i> </div> </div> <div class="content"> <h1 class="title">{{food.name}}</h1> <div class="detail"> <span class="sell-count">月售{{food.sellCount}}份</span> <span class="rating">好评率{{food.rating}}%</span> </div> <div class="price"> <span class="now">¥{{food.price}}</span> <span class="old" v-show="food.oldPrice">¥{{food.oldPrice}}</span> </div> <div class="cartcontrol-wrapper"> <cartcontrol :food="food"></cartcontrol> </div> <transition name="fade"> <div @click.stop.prevent="addFirst" class="buy" v-show="!food.count || food.count===0"> 加入购物车 </div> </transition> </div> <split></split> <div class="info" v-show="food.info"> <h1 class="title">商品信息</h1> <p class="text">{{food.info}}</p> </div> </div> </div> </transition> </template> <script> import BScroll from 'better-scroll'; import Vue from 'vue'; import cartcontrol from '../../components/cartcontrol/cartcontrol'; import split from '../../components/split/split'; export default { name: 'v-food', props: { food: { type: Object } }, data() { return { showFlag: false }; }, methods: { show() { this.showFlag = true; this.$nextTick(() => { if (!this.scroll) { this.scroll = new BScroll(this.$refs.food, { click: true }); } else { this.scroll.refresh(); } }); }, hide() { this.showFlag = false; }, addFirst(event) { if(!event._constructed) { return; } this.$emit('add', event.target); Vue.set(this.food, 'count', 1); } }, components: { cartcontrol, split } } </script> <style scoped lang="stylus" rel="stylesheet/stylus"> .food position: fixed left: 0 top: 0 bottom: 48px z-index: 30px width: 100% background: #fff transform: translate3d(0, 0, 0) &.move-enter-active, &.move-leave-active transition: all 0.2s linear &.move-enter, &.move-leave-active transform: translate3d(100%, 0, 0) .image-header position: relative width: 100% height: 0 padding-top: 100% img position: absolute top: 0 left: 0 width: 100% height: 100% .back position: absolute top: 10px left: 0 .icon-arrow_lift display: block padding: 10px font-size: 20px color: #fff .content position: relative padding: 18px .title line-height: 14px margin-bottom: 8px font-size: 14px font-weight: 700 color: rgb(7, 17, 27) .detail margin-bottom: 18px line-height: 10px height: 10px font-size: 0 .sell-count, .rating font-size: 10px color: rgb(147, 153, 159) .sell-count margin-right: 12px .price font-weight: 700 line-height: 24px .now margin-right: 8px font-size: 14px color: rgb(240, 20, 20) .old text-decoration: line-through font-size: 10px color: rgb(147, 153, 159) .cartcontrol-wrapper position: absolute right: 12px bottom: 12px .buy position: absolute right: 18px bottom: 18px z-index: 10 height: 24px line-height: 24px padding: 0 12px box-sizing: border-box border-radius: 12px font-size: 10px color: #fff background: rgb(0, 160, 220) opacity: 1 &.fade-enter-active, &.fade-leave-active transition: all 0.2s &.fade-enter, &.fade-leave-active opacity: 0 z-index: -1 .info padding: 18px .title line-height: 14px margin-bottom: 6px font-size: 14px font-weight: 700 color: rgb(7, 17, 27) .text line-height: 24px padding: 0 8px font-size: 12px color: rgb(77, 85, 93) </style>
ratingselect组件(一)
步骤一:新建ratingselect.vue组件
<template> <div class="ratingselect"> <div class="rating-type"> <span>{{desc.all}}</span> <span>{{desc.positive}}</span> <span>{{desc.negative}}</span> </div> <div class="switch"> <span class="icon-check_circle"></span> <span class="text">只看有内容的评价</span> </div> </div> </template> <script> const POSITIVE = 0; const NEGATIVE = 1; const ALL = 2; export default { props: { ratings: { type: Array, default () { return []; } }, selectType: { type: Number, default: ALL }, onlyContent: { type: Boolean, default: false }, desc: { type: Object, default () { return { all: '全部', positive: '满意', negative: '不满意' }; } } } }; </script> <style lang="stylus" rel="stylesheet/stylus"> </style>
步骤二:编写food.vue组件,将ratingselect.vue组件加载
<template> <transition name="move"> <div v-show="showFlag" class="food" ref="food"> <div class="food-content"> <div class="image-header"> <img :src="food.image" /> <div class="back" @click="hide"> <i class="icon-arrow_lift"></i> </div> </div> <div class="content"> <h1 class="title">{{food.name}}</h1> <div class="detail"> <span class="sell-count">月售{{food.sellCount}}份</span> <span class="rating">好评率{{food.rating}}%</span> </div> <div class="price"> <span class="now">¥{{food.price}}</span> <span class="old" v-show="food.oldPrice">¥{{food.oldPrice}}</span> </div> <div class="cartcontrol-wrapper"> <cartcontrol :food="food"></cartcontrol> </div> <transition name="fade"> <div @click.stop.prevent="addFirst" class="buy" v-show="!food.count || food.count===0"> 加入购物车 </div> </transition> </div> <split v-show="food.info"></split> <div class="info" v-show="food.info"> <h1 class="title">商品信息</h1> <p class="text">{{food.info}}</p> </div> <split></split> <div class="rating"> <h1 class="title">商品评价</h1> <ratingselect></ratingselect> </div> </div> </div> </transition> </template> <script> import BScroll from 'better-scroll'; import Vue from 'vue'; import cartcontrol from '../../components/cartcontrol/cartcontrol'; import split from '../../components/split/split'; import ratingselect from '../../components/ratingselect/ratingselect'; export default { name: 'v-food', props: { food: { type: Object } }, data() { return { showFlag: false }; }, methods: { show() { this.showFlag = true; this.$nextTick(() => { if (!this.scroll) { this.scroll = new BScroll(this.$refs.food, { click: true }); } else { this.scroll.refresh(); } }); }, hide() { this.showFlag = false; }, addFirst(event) { if(!event._constructed) { return; } this.$emit('add', event.target); Vue.set(this.food, 'count', 1); } }, components: { cartcontrol, split, ratingselect } } </script> <style scoped lang="stylus" rel="stylesheet/stylus"> .food position: fixed left: 0 top: 0 bottom: 48px z-index: 30px width: 100% background: #fff transform: translate3d(0, 0, 0) &.move-enter-active, &.move-leave-active transition: all 0.2s linear &.move-enter, &.move-leave-active transform: translate3d(100%, 0, 0) .image-header position: relative width: 100% height: 0 padding-top: 100% img position: absolute top: 0 left: 0 width: 100% height: 100% .back position: absolute top: 10px left: 0 .icon-arrow_lift display: block padding: 10px font-size: 20px color: #fff .content position: relative padding: 18px .title line-height: 14px margin-bottom: 8px font-size: 14px font-weight: 700 color: rgb(7, 17, 27) .detail margin-bottom: 18px line-height: 10px height: 10px font-size: 0 .sell-count, .rating font-size: 10px color: rgb(147, 153, 159) .sell-count margin-right: 12px .price font-weight: 700 line-height: 24px .now margin-right: 8px font-size: 14px color: rgb(240, 20, 20) .old text-decoration: line-through font-size: 10px color: rgb(147, 153, 159) .cartcontrol-wrapper position: absolute right: 12px bottom: 12px .buy position: absolute right: 18px bottom: 18px z-index: 10 height: 24px line-height: 24px padding: 0 12px box-sizing: border-box border-radius: 12px font-size: 10px color: #fff background: rgb(0, 160, 220) opacity: 1 &.fade-enter-active, &.fade-leave-active transition: all 0.2s &.fade-enter, &.fade-leave-active opacity: 0 z-index: -1 .info padding: 18px .title line-height: 14px margin-bottom: 6px font-size: 14px font-weight: 700 color: rgb(7, 17, 27) .text line-height: 24px padding: 0 8px font-size: 12px color: rgb(77, 85, 93) </style>
ratingselect组件(二)
编写food.vue组件,实现部分ratingselect.vue效果
<template> <transition name="move"> <div v-show="showFlag" class="food" ref="food"> <div class="food-content"> <div class="image-header"> <img :src="food.image" /> <div class="back" @click="hide"> <i class="icon-arrow_lift"></i> </div> </div> <div class="content"> <h1 class="title">{{food.name}}</h1> <div class="detail"> <span class="sell-count">月售{{food.sellCount}}份</span> <span class="rating">好评率{{food.rating}}%</span> </div> <div class="price"> <span class="now">¥{{food.price}}</span> <span class="old" v-show="food.oldPrice">¥{{food.oldPrice}}</span> </div> <div class="cartcontrol-wrapper"> <cartcontrol :food="food"></cartcontrol> </div> <transition name="fade"> <div @click.stop.prevent="addFirst" class="buy" v-show="!food.count || food.count===0"> 加入购物车 </div> </transition> </div> <split v-show="food.info"></split> <div class="info" v-show="food.info"> <h1 class="title">商品信息</h1> <p class="text">{{food.info}}</p> </div> <split></split> <div class="rating"> <h1 class="title">商品评价</h1> <ratingselect :select-type="selectType" :only-content="onlyContent" :desc="desc" :ratings="food.ratings"></ratingselect> </div> </div> </div> </transition> </template> <script> import BScroll from 'better-scroll'; import Vue from 'vue'; import cartcontrol from '../../components/cartcontrol/cartcontrol'; import split from '../../components/split/split'; import ratingselect from '../../components/ratingselect/ratingselect'; const POSITIVE = 0; const NEGATIVE = 1; const ALL = 2; export default { name: 'v-food', props: { food: { type: Object } }, data() { return { showFlag: false, selectType: ALL, onlyContent: true, desc: { all: '全部', positive: '推荐', negative: '吐槽' } }; }, methods: { show() { this.showFlag = true; this.selectType = ALL; this.onlyContent = true; this.$nextTick(() => { if (!this.scroll) { this.scroll = new BScroll(this.$refs.food, { click: true }); } else { this.scroll.refresh(); } }); }, hide() { this.showFlag = false; }, addFirst(event) { if (!event._constructed) { return; } this.$emit('add', event.target); Vue.set(this.food, 'count', 1); } }, components: { cartcontrol, split, ratingselect } } </script> <style scoped lang="stylus" rel="stylesheet/stylus"> .food position: fixed left: 0 top: 0 bottom: 48px z-index: 30px width: 100% background: #fff transform: translate3d(0, 0, 0) &.move-enter-active, &.move-leave-active transition: all 0.2s linear &.move-enter, &.move-leave-active transform: translate3d(100%, 0, 0) .image-header position: relative width: 100% height: 0 padding-top: 100% img position: absolute top: 0 left: 0 width: 100% height: 100% .back position: absolute top: 10px left: 0 .icon-arrow_lift display: block padding: 10px font-size: 20px color: #fff .content position: relative padding: 18px .title line-height: 14px margin-bottom: 8px font-size: 14px font-weight: 700 color: rgb(7, 17, 27) .detail margin-bottom: 18px line-height: 10px height: 10px font-size: 0 .sell-count, .rating font-size: 10px color: rgb(147, 153, 159) .sell-count margin-right: 12px .price font-weight: 700 line-height: 24px .now margin-right: 8px font-size: 14px color: rgb(240, 20, 20) .old text-decoration: line-through font-size: 10px color: rgb(147, 153, 159) .cartcontrol-wrapper position: absolute right: 12px bottom: 12px .buy position: absolute right: 18px bottom: 18px z-index: 10 height: 24px line-height: 24px padding: 0 12px box-sizing: border-box border-radius: 12px font-size: 10px color: #fff background: rgb(0, 160, 220) opacity: 1 &.fade-enter-active, &.fade-leave-active transition: all 0.2s &.fade-enter, &.fade-leave-active opacity: 0 z-index: -1 .info padding: 18px .title line-height: 14px margin-bottom: 6px font-size: 14px font-weight: 700 color: rgb(7, 17, 27) .text line-height: 24px padding: 0 8px font-size: 12px color: rgb(77, 85, 93) .rating padding-top: 18px .title line-height: 14px margin-left: 18px font-size: 14px color: rgb(7, 17, 27) </style>
ratingselect组件(三)
继续编写ratingselect.vue组件,实现部分样式和动态切换
<template> <div class="ratingselect"> <div class="rating-type border-1px"> <span class="block positive" :class="{'active':selectType===2}"> {{desc.all}} <span class="count">47</span> </span> <span class="block positive" :class="{'active':selectType===0}"> {{desc.positive}} <span class="count">40</span> </span> <span class="block negative" :class="{'active':selectType===1}"> {{desc.negative}} <span class="count">10</span> </span> </div> <div class="switch"> <span class="icon-check_circle"></span> <span class="text">只看有内容的评价</span> </div> </div> </template> <script> const POSITIVE = 0; const NEGATIVE = 1; const ALL = 2; export default { props: { ratings: { type: Array, default () { return []; } }, selectType: { type: Number, default: ALL }, onlyContent: { type: Boolean, default: false }, desc: { type: Object, default () { return { all: '全部', positive: '满意', negative: '不满意' }; } } } }; </script> <style lang="stylus" rel="stylesheet/stylus"> .ratingselect .rating-type padding: 18px 0 margin: 0 18px border-1px(rgba(7, 17, 27, 0.1)) font-size: 0 .block display: inline-block padding: 8px 12px margin-right: 8px line-height: 16px border-radius: 1px font-size: 12px color: rgb(77, 85, 93) &.active color: #fff .count margin-left: 2px font-size: 8px &.positive background: rgba(0, 160, 220, 0.2) &.active background: rgb(0, 160, 220) &.negative background: rgba(77, 85, 93, 0.2) &.active background: rgb(77, 85, 93) </style>
ratingselect组件(四)
继续编写ratingselect.vue组件,进一步实现样式
<template> <div class="ratingselect"> <div class="rating-type border-1px"> <span class="block positive" :class="{'active':selectType===2}"> {{desc.all}} <span class="count">47</span> </span> <span class="block positive" :class="{'active':selectType===0}"> {{desc.positive}} <span class="count">40</span> </span> <span class="block negative" :class="{'active':selectType===1}"> {{desc.negative}} <span class="count">10</span> </span> </div> <div class="switch" :class="{'on':onlyContent}"> <span class="icon-check_circle"></span> <span class="text">只看有内容的评价</span> </div> </div> </template> <script> const POSITIVE = 0; const NEGATIVE = 1; const ALL = 2; export default { props: { ratings: { type: Array, default () { return []; } }, selectType: { type: Number, default: ALL }, onlyContent: { type: Boolean, default: false }, desc: { type: Object, default () { return { all: '全部', positive: '满意', negative: '不满意' }; } } } }; </script> <style lang="stylus" rel="stylesheet/stylus"> .ratingselect .rating-type padding: 18px 0 margin: 0 18px border-1px(rgba(7, 17, 27, 0.1)) font-size: 0 .block display: inline-block padding: 8px 12px margin-right: 8px line-height: 16px border-radius: 1px font-size: 12px color: rgb(77, 85, 93) &.active color: #fff .count margin-left: 2px font-size: 8px &.positive background: rgba(0, 160, 220, 0.2) &.active background: rgb(0, 160, 220) &.negative background: rgba(77, 85, 93, 0.2) &.active background: rgb(77, 85, 93) .switch padding: 12px 18px line-height: 24px border-bottom: 1px solid rgba(7, 17, 27, 0.1) color: rgb(147, 153, 159) font-size: 0 &.on .icon-check_circle color: #00c850 .icon-check_circle display: inline-block vertical-align: top margin-right: 4px font-size: 24px .text display: inline-block vertical-align: top font-size: 12px </style>
ratingselect组件(五)
继续编写ratingselect.vue组件,完成全部切换效果(点击效果无效)
<template> <div class="ratingselect"> <div class="rating-type border-1px"> <span @click="select(2,$event)" class="block positive" :class="{'active':selectType===2}">{{desc.all}}<span class="count">{{ratings.length}}</span></span> <span @click="select(0,$event)" class="block positive" :class="{'active':selectType===0}">{{desc.positive}}<span class="count">{{positives.length}}</span></span> <span @click="select(1,$event)" class="block negative" :class="{'active':selectType===1}">{{desc.negative}}<span class="count">{{negatives.length}}</span></span> </div> <div @click="toggleContent" class="switch" :class="{'on':onlyContent}"> <span class="icon-check_circle"></span> <span class="text">只看有内容的评价</span> </div> </div> </template> <script> const POSITIVE = 0; const NEGATIVE = 1; const ALL = 2; export default { props: { ratings: { type: Array, default() { return []; } }, selectType: { type: Number, default: ALL }, onlyContent: { type: Boolean, default: false }, desc: { type: Object, default() { return { all: '全部', positive: '满意', negative: '不满意' }; } } }, computed: { positives() { return this.ratings.filter((rating) => { return rating.rateType === POSITIVE; }); }, negatives() { return this.ratings.filter((rating) => { return rating.rateType === NEGATIVE; }); } }, methods: { select(type, event) { if (!event._constructed) { return; } this.$emit('select', type); }, toggleContent(event) { if (!event._constructed) { return; } this.$emit('toggle'); } } }; </script> <style lang="stylus" rel="stylesheet/stylus"> @import "../../common/stylus/mixin.styl" .ratingselect .rating-type padding: 18px 0 margin: 0 18px border-1px(rgba(7, 17, 27, 0.1)) font-size: 0 .block display: inline-block padding: 8px 12px margin-right: 8px line-height: 16px border-radius: 1px font-size: 12px color: rgb(77, 85, 93) &.active color: #fff .count margin-left: 2px font-size: 8px &.positive background: rgba(0, 160, 220, 0.2) &.active background: rgb(0, 160, 220) &.negative background: rgba(77, 85, 93, 0.2) &.active background: rgb(77, 85, 93) .switch padding: 12px 18px line-height: 24px border-bottom: 1px solid rgba(7, 17, 27, 0.1) color: rgb(147, 153, 159) font-size: 0 &.on .icon-check_circle color: #00c850 .icon-check_circle display: inline-block vertical-align: top margin-right: 4px font-size: 24px .text display: inline-block vertical-align: top font-size: 12px </style>
评价列表(一)
编写好food.vue组件的评价结构
<template> <transition name="move"> <div v-show="showFlag" class="food" ref="food"> <div class="food-content"> <div class="image-header"> <img :src="food.image" /> <div class="back" @click="hide"> <i class="icon-arrow_lift"></i> </div> </div> <div class="content"> <h1 class="title">{{food.name}}</h1> <div class="detail"> <span class="sell-count">月售{{food.sellCount}}份</span> <span class="rating">好评率{{food.rating}}%</span> </div> <div class="price"> <span class="now">¥{{food.price}}</span> <span class="old" v-show="food.oldPrice">¥{{food.oldPrice}}</span> </div> <div class="cartcontrol-wrapper"> <cartcontrol :food="food"></cartcontrol> </div> <transition name="fade"> <div @click.stop.prevent="addFirst" class="buy" v-show="!food.count || food.count===0"> 加入购物车 </div> </transition> </div> <split v-show="food.info"></split> <div class="info" v-show="food.info"> <h1 class="title">商品信息</h1> <p class="text">{{food.info}}</p> </div> <split></split> <div class="rating"> <h1 class="title">商品评价</h1> <ratingselect :select-type="selectType" :only-content="onlyContent" :desc="desc" :ratings="food.ratings"></ratingselect> <div class="rating-wrapper"> <ul v-show="food.ratings && food.ratings.length"> <li v-for="rating in food.ratings" class="rating-item"> <div class="user"> <span class="name">{{rating.username}}</span> <img class="avatar" width="12" height="12" :src="rating.avatar" /> </div> <div class="time">{{rating.rateTime}}</div> <p class="text"> <span :class="{'icon-thumb_up':rating.rateType===0,'icon-thumb_down':rating.rateType===1}"></span> {{rating.text}} </p> </li> </ul> <div class="no-rating" v-show="!food.ratings || !food.ratings.length"></div> </div> </div> </div> </div> </transition> </template> <script> import BScroll from 'better-scroll'; import Vue from 'vue'; import cartcontrol from '../../components/cartcontrol/cartcontrol'; import split from '../../components/split/split'; import ratingselect from '../../components/ratingselect/ratingselect'; const POSITIVE = 0; const NEGATIVE = 1; const ALL = 2; export default { name: 'v-food', props: { food: { type: Object } }, data() { return { showFlag: false, selectType: ALL, onlyContent: true, desc: { all: '全部', positive: '推荐', negative: '吐槽' } }; }, methods: { show() { this.showFlag = true; this.selectType = ALL; this.onlyContent = true; this.$nextTick(() => { if (!this.scroll) { this.scroll = new BScroll(this.$refs.food, { click: true }); } else { this.scroll.refresh(); } }); }, hide() { this.showFlag = false; }, addFirst(event) { if (!event._constructed) { return; } this.$emit('add', event.target); Vue.set(this.food, 'count', 1); } }, components: { cartcontrol, split, ratingselect } } </script> <style scoped lang="stylus" rel="stylesheet/stylus"> .food position: fixed left: 0 top: 0 bottom: 48px z-index: 30px width: 100% background: #fff transform: translate3d(0, 0, 0) &.move-enter-active, &.move-leave-active transition: all 0.2s linear &.move-enter, &.move-leave-active transform: translate3d(100%, 0, 0) .image-header position: relative width: 100% height: 0 padding-top: 100% img position: absolute top: 0 left: 0 width: 100% height: 100% .back position: absolute top: 10px left: 0 .icon-arrow_lift display: block padding: 10px font-size: 20px color: #fff .content position: relative padding: 18px .title line-height: 14px margin-bottom: 8px font-size: 14px font-weight: 700 color: rgb(7, 17, 27) .detail margin-bottom: 18px line-height: 10px height: 10px font-size: 0 .sell-count, .rating font-size: 10px color: rgb(147, 153, 159) .sell-count margin-right: 12px .price font-weight: 700 line-height: 24px .now margin-right: 8px font-size: 14px color: rgb(240, 20, 20) .old text-decoration: line-through font-size: 10px color: rgb(147, 153, 159) .cartcontrol-wrapper position: absolute right: 12px bottom: 12px .buy position: absolute right: 18px bottom: 18px z-index: 10 height: 24px line-height: 24px padding: 0 12px box-sizing: border-box border-radius: 12px font-size: 10px color: #fff background: rgb(0, 160, 220) opacity: 1 &.fade-enter-active, &.fade-leave-active transition: all 0.2s &.fade-enter, &.fade-leave-active opacity: 0 z-index: -1 .info padding: 18px .title line-height: 14px margin-bottom: 6px font-size: 14px font-weight: 700 color: rgb(7, 17, 27) .text line-height: 24px padding: 0 8px font-size: 12px color: rgb(77, 85, 93) .rating padding-top: 18px .title line-height: 14px margin-left: 18px font-size: 14px color: rgb(7, 17, 27) </style>
评论列表(二)
继续编写food.vue组件的评论区样式
<template> <transition name="move"> <div v-show="showFlag" class="food" ref="food"> <div class="food-content"> <div class="image-header"> <img :src="food.image" /> <div class="back" @click="hide"> <i class="icon-arrow_lift"></i> </div> </div> <div class="content"> <h1 class="title">{{food.name}}</h1> <div class="detail"> <span class="sell-count">月售{{food.sellCount}}份</span> <span class="rating">好评率{{food.rating}}%</span> </div> <div class="price"> <span class="now">¥{{food.price}}</span> <span class="old" v-show="food.oldPrice">¥{{food.oldPrice}}</span> </div> <div class="cartcontrol-wrapper"> <cartcontrol :food="food"></cartcontrol> </div> <transition name="fade"> <div @click.stop.prevent="addFirst" class="buy" v-show="!food.count || food.count===0"> 加入购物车 </div> </transition> </div> <split v-show="food.info"></split> <div class="info" v-show="food.info"> <h1 class="title">商品信息</h1> <p class="text">{{food.info}}</p> </div> <split></split> <div class="rating"> <h1 class="title">商品评价</h1> <ratingselect :select-type="selectType" :only-content="onlyContent" :desc="desc" :ratings="food.ratings"></ratingselect> <div class="rating-wrapper"> <ul v-show="food.ratings && food.ratings.length"> <li v-for="rating in food.ratings" class="rating-item border-1px"> <div class="user"> <span class="name">{{rating.username}}</span> <img class="avatar" width="12" height="12" :src="rating.avatar" /> </div> <div class="time">{{rating.rateTime}}</div> <p class="text"> <span :class="{'icon-thumb_up':rating.rateType===0,'icon-thumb_down':rating.rateType===1}"></span> {{rating.text}} </p> </li> </ul> <div class="no-rating" v-show="!food.ratings || !food.ratings.length"></div> </div> </div> </div> </div> </transition> </template> <script> import BScroll from 'better-scroll'; import Vue from 'vue'; import cartcontrol from '../../components/cartcontrol/cartcontrol'; import split from '../../components/split/split'; import ratingselect from '../../components/ratingselect/ratingselect'; const POSITIVE = 0; const NEGATIVE = 1; const ALL = 2; export default { name: 'v-food', props: { food: { type: Object } }, data() { return { showFlag: false, selectType: ALL, onlyContent: true, desc: { all: '全部', positive: '推荐', negative: '吐槽' } }; }, methods: { show() { this.showFlag = true; this.selectType = ALL; this.onlyContent = true; this.$nextTick(() => { if (!this.scroll) { this.scroll = new BScroll(this.$refs.food, { click: true }); } else { this.scroll.refresh(); } }); }, hide() { this.showFlag = false; }, addFirst(event) { if (!event._constructed) { return; } this.$emit('add', event.target); Vue.set(this.food, 'count', 1); } }, components: { cartcontrol, split, ratingselect } } </script> <style scoped lang="stylus" rel="stylesheet/stylus"> @import "../../common/stylus/mixin.styl" .food position: fixed left: 0 top: 0 bottom: 48px z-index: 30px width: 100% background: #fff transform: translate3d(0, 0, 0) &.move-enter-active, &.move-leave-active transition: all 0.2s linear &.move-enter, &.move-leave-active transform: translate3d(100%, 0, 0) .image-header position: relative width: 100% height: 0 padding-top: 100% img position: absolute top: 0 left: 0 width: 100% height: 100% .back position: absolute top: 10px left: 0 .icon-arrow_lift display: block padding: 10px font-size: 20px color: #fff .content position: relative padding: 18px .title line-height: 14px margin-bottom: 8px font-size: 14px font-weight: 700 color: rgb(7, 17, 27) .detail margin-bottom: 18px line-height: 10px height: 10px font-size: 0 .sell-count, .rating font-size: 10px color: rgb(147, 153, 159) .sell-count margin-right: 12px .price font-weight: 700 line-height: 24px .now margin-right: 8px font-size: 14px color: rgb(240, 20, 20) .old text-decoration: line-through font-size: 10px color: rgb(147, 153, 159) .cartcontrol-wrapper position: absolute right: 12px bottom: 12px .buy position: absolute right: 18px bottom: 18px z-index: 10 height: 24px line-height: 24px padding: 0 12px box-sizing: border-box border-radius: 12px font-size: 10px color: #fff background: rgb(0, 160, 220) opacity: 1 &.fade-enter-active, &.fade-leave-active transition: all 0.2s &.fade-enter, &.fade-leave-active opacity: 0 z-index: -1 .info padding: 18px .title line-height: 14px margin-bottom: 6px font-size: 14px font-weight: 700 color: rgb(7, 17, 27) .text line-height: 24px padding: 0 8px font-size: 12px color: rgb(77, 85, 93) .rating padding-top: 18px .title line-height: 14px margin-left: 18px font-size: 14px color: rgb(7, 17, 27) .rating-wrapper padding: 0 18px .rating-item position: relative padding: 16px 0 border-1px(rgba(7, 17, 27, 0.1)) .user position: absolute right: 0 top: 16px line-height: 12px font-size: 0 .name display: inline-block margin-right: 6px vertical-align: top font-size: 10px color: rgb(147, 153, 159) .avatar border-radius: 50% .time margin-bottom: 6px line-height: 12px font-size: 10px color: rgb(147, 153, 159) .text line-height: 16px font-size: 12px color: rgb(7, 17, 27) .icon-thumb_up, .icon-thumb_down margin-right: 4px line-height: 16px font-size: 12px .icon-thumb_up color: rgb(0, 160, 220) .icon-thumb_down color: rgb(147, 153, 159) .no-rating padding: 16px 0 font-size: 12px color: rgb(147, 153, 159) </style>
评论列表(三)
继续编写food.vue组件,实现评价切换效果(失败)
<template> <transition name="move"> <div v-show="showFlag" class="food" ref="food"> <div class="food-content"> <div class="image-header"> <img :src="food.image" /> <div class="back" @click="hide"> <i class="icon-arrow_lift"></i> </div> </div> <div class="content"> <h1 class="title">{{food.name}}</h1> <div class="detail"> <span class="sell-count">月售{{food.sellCount}}份</span> <span class="rating">好评率{{food.rating}}%</span> </div> <div class="price"> <span class="now">¥{{food.price}}</span> <span class="old" v-show="food.oldPrice">¥{{food.oldPrice}}</span> </div> <div class="cartcontrol-wrapper"> <cartcontrol :food="food"></cartcontrol> </div> <transition name="fade"> <div @click.stop.prevent="addFirst" class="buy" v-show="!food.count || food.count===0"> 加入购物车 </div> </transition> </div> <split v-show="food.info"></split> <div class="info" v-show="food.info"> <h1 class="title">商品信息</h1> <p class="text">{{food.info}}</p> </div> <split></split> <div class="rating"> <h1 class="title">商品评价</h1> <ratingselect :select-type="selectType" :only-content="onlyContent" :desc="desc" :ratings="food.ratings"></ratingselect> <div class="rating-wrapper"> <ul v-show="food.ratings && food.ratings.length"> <li v-show="needShow(rating.rateType,rating.text)" v-for="rating in food.ratings" class="rating-item border-1px"> <div class="user"> <span class="name">{{rating.username}}</span> <img class="avatar" width="12" height="12" :src="rating.avatar" /> </div> <div class="time">{{rating.rateTime}}</div> <p class="text"> <span :class="{'icon-thumb_up':rating.rateType===0,'icon-thumb_down':rating.rateType===1}"></span> {{rating.text}} </p> </li> </ul> <div class="no-rating" v-show="!food.ratings || !food.ratings.length"></div> </div> </div> </div> </div> </transition> </template> <script> import BScroll from 'better-scroll'; import Vue from 'vue'; import cartcontrol from '../../components/cartcontrol/cartcontrol'; import split from '../../components/split/split'; import ratingselect from '../../components/ratingselect/ratingselect'; const POSITIVE = 0; const NEGATIVE = 1; const ALL = 2; export default { name: 'v-food', props: { food: { type: Object } }, data() { return { showFlag: false, selectType: ALL, onlyContent: true, desc: { all: '全部', positive: '推荐', negative: '吐槽' } }; }, methods: { show() { this.showFlag = true; this.selectType = ALL; this.onlyContent = true; this.$nextTick(() => { if (!this.scroll) { this.scroll = new BScroll(this.$refs.food, { click: true }); } else { this.scroll.refresh(); } }); }, hide() { this.showFlag = false; }, addFirst(event) { if (!event._constructed) { return; } this.$emit('add', event.target); Vue.set(this.food, 'count', 1); }, needShow(type, text) { if (this.onlyContent && !text) { return false; } if (this.selectType === ALL) { return true; } else { return type === this.selectType; } } }, events: { 'ratingtype.select'(type) { this.selectType = type; this.$nextTick(() => { this.scroll.refresh(); }); }, 'content.toggle'(onlyContent) { this.onlyContent = onlyContent; this.$nextTick(() => { this.scroll.refresh(); }); } }, components: { cartcontrol, split, ratingselect } } </script> <style scoped lang="stylus" rel="stylesheet/stylus"> @import "../../common/stylus/mixin.styl" .food position: fixed left: 0 top: 0 bottom: 48px z-index: 30px width: 100% background: #fff transform: translate3d(0, 0, 0) &.move-enter-active, &.move-leave-active transition: all 0.2s linear &.move-enter, &.move-leave-active transform: translate3d(100%, 0, 0) .image-header position: relative width: 100% height: 0 padding-top: 100% img position: absolute top: 0 left: 0 width: 100% height: 100% .back position: absolute top: 10px left: 0 .icon-arrow_lift display: block padding: 10px font-size: 20px color: #fff .content position: relative padding: 18px .title line-height: 14px margin-bottom: 8px font-size: 14px font-weight: 700 color: rgb(7, 17, 27) .detail margin-bottom: 18px line-height: 10px height: 10px font-size: 0 .sell-count, .rating font-size: 10px color: rgb(147, 153, 159) .sell-count margin-right: 12px .price font-weight: 700 line-height: 24px .now margin-right: 8px font-size: 14px color: rgb(240, 20, 20) .old text-decoration: line-through font-size: 10px color: rgb(147, 153, 159) .cartcontrol-wrapper position: absolute right: 12px bottom: 12px .buy position: absolute right: 18px bottom: 18px z-index: 10 height: 24px line-height: 24px padding: 0 12px box-sizing: border-box border-radius: 12px font-size: 10px color: #fff background: rgb(0, 160, 220) opacity: 1 &.fade-enter-active, &.fade-leave-active transition: all 0.2s &.fade-enter, &.fade-leave-active opacity: 0 z-index: -1 .info padding: 18px .title line-height: 14px margin-bottom: 6px font-size: 14px font-weight: 700 color: rgb(7, 17, 27) .text line-height: 24px padding: 0 8px font-size: 12px color: rgb(77, 85, 93) .rating padding-top: 18px .title line-height: 14px margin-left: 18px font-size: 14px color: rgb(7, 17, 27) .rating-wrapper padding: 0 18px .rating-item position: relative padding: 16px 0 border-1px(rgba(7, 17, 27, 0.1)) .user position: absolute right: 0 top: 16px line-height: 12px font-size: 0 .name display: inline-block margin-right: 6px vertical-align: top font-size: 10px color: rgb(147, 153, 159) .avatar border-radius: 50% .time margin-bottom: 6px line-height: 12px font-size: 10px color: rgb(147, 153, 159) .text line-height: 16px font-size: 12px color: rgb(7, 17, 27) .icon-thumb_up, .icon-thumb_down margin-right: 4px line-height: 16px font-size: 12px .icon-thumb_up color: rgb(0, 160, 220) .icon-thumb_down color: rgb(147, 153, 159) .no-rating padding: 16px 0 font-size: 12px color: rgb(147, 153, 159) </style>
评论列表(四、五、六)
步骤一:编写food.vue组件,创建一个时间过滤器
<template> <transition name="move"> <div v-show="showFlag" class="food" ref="food"> <div class="food-content"> <div class="image-header"> <img :src="food.image" /> <div class="back" @click="hide"> <i class="icon-arrow_lift"></i> </div> </div> <div class="content"> <h1 class="title">{{food.name}}</h1> <div class="detail"> <span class="sell-count">月售{{food.sellCount}}份</span> <span class="rating">好评率{{food.rating}}%</span> </div> <div class="price"> <span class="now">¥{{food.price}}</span> <span class="old" v-show="food.oldPrice">¥{{food.oldPrice}}</span> </div> <div class="cartcontrol-wrapper"> <cartcontrol :food="food"></cartcontrol> </div> <transition name="fade"> <div @click.stop.prevent="addFirst" class="buy" v-show="!food.count || food.count===0"> 加入购物车 </div> </transition> </div> <split v-show="food.info"></split> <div class="info" v-show="food.info"> <h1 class="title">商品信息</h1> <p class="text">{{food.info}}</p> </div> <split></split> <div class="rating"> <h1 class="title">商品评价</h1> <ratingselect :select-type="selectType" :only-content="onlyContent" :desc="desc" :ratings="food.ratings"></ratingselect> <div class="rating-wrapper"> <ul v-show="food.ratings && food.ratings.length"> <li v-show="needShow(rating.rateType,rating.text)" v-for="rating in food.ratings" class="rating-item border-1px"> <div class="user"> <span class="name">{{rating.username}}</span> <img class="avatar" width="12" height="12" :src="rating.avatar" /> </div> <div class="time">{{rating.rateTime | formatDate}}</div> <p class="text"> <span :class="{'icon-thumb_up':rating.rateType===0,'icon-thumb_down':rating.rateType===1}"></span> {{rating.text}} </p> </li> </ul> <div class="no-rating" v-show="!food.ratings || !food.ratings.length">暂无评价</div> </div> </div> </div> </div> </transition> </template> <script> import BScroll from 'better-scroll'; import Vue from 'vue'; import {formatDate} from '../../common/js/date'; import cartcontrol from '../../components/cartcontrol/cartcontrol'; import split from '../../components/split/split'; import ratingselect from '../../components/ratingselect/ratingselect'; const POSITIVE = 0; const NEGATIVE = 1; const ALL = 2; export default { name: 'v-food', props: { food: { type: Object } }, data() { return { showFlag: false, selectType: ALL, onlyContent: true, desc: { all: '全部', positive: '推荐', negative: '吐槽' } }; }, methods: { show() { this.showFlag = true; this.selectType = ALL; this.onlyContent = true; this.$nextTick(() => { if (!this.scroll) { this.scroll = new BScroll(this.$refs.food, { click: true }); } else { this.scroll.refresh(); } }); }, hide() { this.showFlag = false; }, addFirst(event) { if (!event._constructed) { return; } this.$emit('add', event.target); Vue.set(this.food, 'count', 1); }, needShow(type, text) { if (this.onlyContent && !text) { return false; } if (this.selectType === ALL) { return true; } else { return type === this.selectType; } } }, events: { 'ratingtype.select'(type) { this.selectType = type; this.$nextTick(() => { this.scroll.refresh(); }); }, 'content.toggle'(onlyContent) { this.onlyContent = onlyContent; this.$nextTick(() => { this.scroll.refresh(); }); } }, filters: { formatDate(time) { let date = new Date(time); return formatDate(date, 'yyyy-MM-dd hh:mm'); } }, components: { cartcontrol, split, ratingselect } } </script> <style scoped lang="stylus" rel="stylesheet/stylus"> @import "../../common/stylus/mixin.styl" .food position: fixed left: 0 top: 0 bottom: 48px z-index: 30px width: 100% background: #fff transform: translate3d(0, 0, 0) &.move-enter-active, &.move-leave-active transition: all 0.2s linear &.move-enter, &.move-leave-active transform: translate3d(100%, 0, 0) .image-header position: relative width: 100% height: 0 padding-top: 100% img position: absolute top: 0 left: 0 width: 100% height: 100% .back position: absolute top: 10px left: 0 .icon-arrow_lift display: block padding: 10px font-size: 20px color: #fff .content position: relative padding: 18px .title line-height: 14px margin-bottom: 8px font-size: 14px font-weight: 700 color: rgb(7, 17, 27) .detail margin-bottom: 18px line-height: 10px height: 10px font-size: 0 .sell-count, .rating font-size: 10px color: rgb(147, 153, 159) .sell-count margin-right: 12px .price font-weight: 700 line-height: 24px .now margin-right: 8px font-size: 14px color: rgb(240, 20, 20) .old text-decoration: line-through font-size: 10px color: rgb(147, 153, 159) .cartcontrol-wrapper position: absolute right: 12px bottom: 12px .buy position: absolute right: 18px bottom: 18px z-index: 10 height: 24px line-height: 24px padding: 0 12px box-sizing: border-box border-radius: 12px font-size: 10px color: #fff background: rgb(0, 160, 220) opacity: 1 &.fade-enter-active, &.fade-leave-active transition: all 0.2s &.fade-enter, &.fade-leave-active opacity: 0 z-index: -1 .info padding: 18px .title line-height: 14px margin-bottom: 6px font-size: 14px font-weight: 700 color: rgb(7, 17, 27) .text line-height: 24px padding: 0 8px font-size: 12px color: rgb(77, 85, 93) .rating padding-top: 18px .title line-height: 14px margin-left: 18px font-size: 14px color: rgb(7, 17, 27) .rating-wrapper padding: 0 18px .rating-item position: relative padding: 16px 0 border-1px(rgba(7, 17, 27, 0.1)) .user position: absolute right: 0 top: 16px line-height: 12px font-size: 0 .name display: inline-block margin-right: 6px vertical-align: top font-size: 10px color: rgb(147, 153, 159) .avatar border-radius: 50% .time margin-bottom: 6px line-height: 12px font-size: 10px color: rgb(147, 153, 159) .text line-height: 16px font-size: 12px color: rgb(7, 17, 27) .icon-thumb_up, .icon-thumb_down margin-right: 4px line-height: 16px font-size: 12px .icon-thumb_up color: rgb(0, 160, 220) .icon-thumb_down color: rgb(147, 153, 159) .no-rating padding: 16px 0 font-size: 12px color: rgb(147, 153, 159) </style>
步骤二:编写公共的date.js文件,实现时间过滤的效果
export function formatDate(date, fmt) {
if (/(y+)/.test(fmt)) {
fmt = fmt.replace(RegExp.$1, (date.getFullYear() + '').substr(4 - RegExp.$1.length));
}
let o = {
'M+': date.getMonth() + 1,
'd+': date.getDate(),
'h+': date.getHours(),
'm+': date.getMinutes(),
's+': date.getSeconds()
};
for (let k in o) {
if (new RegExp(`(${k})`).test(fmt)) {
let str = o[k] + '';
fmt = fmt.replace(RegExp.$1, (RegExp.$1.length === 1) ? str : padLeftZero(str));
}
}
return fmt;
};
function padLeftZero(str) {
return ('00' + str).substr(str.length);
}
rating组件开发(一)
编写ratings.vue组件,完成左边部分样式
<template> <div class="ratings"> <div class="ratings-content"> <div class="overview"> <div class="overview-left"> <h1 class="score">{{seller.score}}</h1> <div class="title">综合评分</div> <div class="rank">高于周边商家{{seller.rankRate}}%</div> </div> <div class="overview-right"></div> </div> </div> </div> </template> <script> export default { name: 'v-ratings', props: { seller: { type: Object } } } </script> <style scoped lang="stylus" rel="stylesheet/stylus"> .ratings position: absolute top: 174px bottom: 0 left: 0 width: 100% overflow: hidden .overview display: flex padding: 18px 0 .overview-left flex: 0 0 137px padding: 6px 0 width: 137px border-right: 1px solid rgba(7, 17, 27, 0.1) text-align: center @media only screen and (max-width: 320px) flex: 0 0 120px width: 120px .score margin-bottom: 6px line-height: 28px font-size: 24px color: rgb(255, 153, 0) .title margin-bottom: 8px line-height: 12px font-size: 12px color: rgb(7, 17, 27) .rank line-height: 10px font-size: 10px color: rgb(147, 153, 159) .overview-right flex: 1 padding: 6px 0 6px 24px </style>
rating组件开发(二)
继续编写ratings.vue组件,完成右边部分样式
<template> <div class="ratings"> <div class="ratings-content"> <div class="overview"> <div class="overview-left"> <h1 class="score">{{seller.score}}</h1> <div class="title">综合评分</div> <div class="rank">高于周边商家{{seller.rankRate}}%</div> </div> <div class="overview-right"> <div class="score-wrapper"> <span class="title">服务态度</span> <star :size="36" :score="seller.serviceScore"></star> <span class="score">{{seller.serviceScore}}</span> </div> <div class="score-wrapper"> <span class="title">商品评分</span> <star :size="36" :score="seller.foodScore"></star> <span class="score">{{seller.foodScore}}</span> </div> <div class="delivery-wrapper"> <span class="title">送达时间</span> <span class="delivery">{{seller.deliveryTime}}分钟</span> </div> </div> </div> </div> </div> </template> <script> import star from '../../components/star/star'; export default { name: 'v-ratings', props: { seller: { type: Object } }, components: { star } } </script> <style scoped lang="stylus" rel="stylesheet/stylus"> .ratings position: absolute top: 174px bottom: 0 left: 0 width: 100% overflow: hidden .overview display: flex padding: 18px 0 .overview-left flex: 0 0 137px padding: 6px 0 width: 137px border-right: 1px solid rgba(7, 17, 27, 0.1) text-align: center @media only screen and (max-width: 320px) flex: 0 0 120px width: 120px .score margin-bottom: 6px line-height: 28px font-size: 24px color: rgb(255, 153, 0) .title margin-bottom: 8px line-height: 12px font-size: 12px color: rgb(7, 17, 27) .rank line-height: 10px font-size: 10px color: rgb(147, 153, 159) .overview-right flex: 1 padding: 6px 0 6px 24px @media only screen and (max-width: 320px) padding-left: 6px .score-wrapper margin-bottom: 8px font-size: 0 .title display: inline-block line-height: 18px vertical-align: top font-size: 12px color: rgb(7, 17, 27) .star display: inline-block margin: 0 12px vertical-align: top .score display: inline-block line-height: 18px vertical-align: top font-size: 12px color: rgb(255, 153, 0) .delivery-wrapper font-size: 0 .title line-height: 18px font-size: 12px color: rgb(7, 17, 27) .delivery margin-left: 12px font-size: 12px color: rgb(147, 153, 159) </style>
rating组件开发(三)
继续编写ratings.vue组件,利用media实现响应式功能
<template> <div class="ratings"> <div class="ratings-content"> <div class="overview"> <div class="overview-left"> <h1 class="score">{{seller.score}}</h1> <div class="title">综合评分</div> <div class="rank">高于周边商家{{seller.rankRate}}%</div> </div> <div class="overview-right"> <div class="score-wrapper"> <span class="title">服务态度</span> <star :size="36" :score="seller.serviceScore"></star> <span class="score">{{seller.serviceScore}}</span> </div> <div class="score-wrapper"> <span class="title">商品评分</span> <star :size="36" :score="seller.foodScore"></star> <span class="score">{{seller.foodScore}}</span> </div> <div class="delivery-wrapper"> <span class="title">送达时间</span> <span class="delivery">{{seller.deliveryTime}}分钟</span> </div> </div> </div> </div> </div> </template> <script> import star from '../../components/star/star'; export default { name: 'v-ratings', props: { seller: { type: Object } }, components: { star } } </script> <style scoped lang="stylus" rel="stylesheet/stylus"> .ratings position: absolute top: 174px bottom: 0 left: 0 width: 100% overflow: hidden .overview display: flex padding: 18px 0 .overview-left flex: 0 0 137px padding: 6px 0 width: 137px border-right: 1px solid rgba(7, 17, 27, 0.1) text-align: center @media only screen and (max-width: 320px) flex: 0 0 120px width: 120px .score margin-bottom: 6px line-height: 28px font-size: 24px color: rgb(255, 153, 0) .title margin-bottom: 8px line-height: 12px font-size: 12px color: rgb(7, 17, 27) .rank line-height: 10px font-size: 10px color: rgb(147, 153, 159) .overview-right flex: 1 padding: 6px 0 6px 24px @media only screen and (max-width: 320px) padding-left: 6px .score-wrapper margin-bottom: 8px font-size: 0 .title display: inline-block line-height: 18px vertical-align: top font-size: 12px color: rgb(7, 17, 27) .star display: inline-block margin: 0 12px vertical-align: top .score display: inline-block line-height: 18px vertical-align: top font-size: 12px color: rgb(255, 153, 0) .delivery-wrapper font-size: 0 .title line-height: 18px font-size: 12px color: rgb(7, 17, 27) .delivery margin-left: 12px font-size: 12px color: rgb(147, 153, 159) </style>
rating组件开发(四)
继续编写ratings.vue组件,实现评论区内容展示
<template> <div class="ratings"> <div class="ratings-content"> <div class="overview"> <div class="overview-left"> <h1 class="score">{{seller.score}}</h1> <div class="title">综合评分</div> <div class="rank">高于周边商家{{seller.rankRate}}%</div> </div> <div class="overview-right"> <div class="score-wrapper"> <span class="title">服务态度</span> <star :size="36" :score="seller.serviceScore"></star> <span class="score">{{seller.serviceScore}}</span> </div> <div class="score-wrapper"> <span class="title">商品评分</span> <star :size="36" :score="seller.foodScore"></star> <span class="score">{{seller.foodScore}}</span> </div> <div class="delivery-wrapper"> <span class="title">送达时间</span> <span class="delivery">{{seller.deliveryTime}}分钟</span> </div> </div> </div> <split></split> <ratingselect :selectType="selectType" :onlyContent="onlyContent" :ratings="ratings"></ratingselect> <div class="rating-wrapper"> <ul> <li v-for="rating in ratings" class="rating-item"> <div class="avatar"> <img width="28" height="28" :src="rating.avatar"> </div> <div class="content"> <h1 class="name">{{rating.username}}</h1> <div class="star-wrapper"> <star :size="24" :score="rating.score"></star> <span class="delivery" v-show="rating.deliveryTime">{{rating.deliveryTime}}</span> </div> <p class="text">{{rating.text}}</p> <div class="recommend" v-show="rating.recommend && rating.recommend.length"> <span class="icon-thumb_up"></span> <span class="item" v-for="item in rating.recommend">{{item}}</span> </div> <div class="time"> {{rating.rateTime}} </div> </div> </li> </ul> </div> </div> </div> </template> <script> import star from '../../components/star/star'; import split from '../../components/split/split'; import ratingselect from '../../components/ratingselect/ratingselect'; const ALL = 2; const ERR_OK = 0; export default { name: 'v-ratings', props: { seller: { type: Object } }, data() { return { ratings: [], selectType: ALL, onlyContent: true }; }, created() { this.$http.get('/api/ratings').then((response) => { response = response.body; if (response.errno === ERR_OK) { this.ratings = response.data; } }); }, components: { star, split, ratingselect } } </script> <style scoped lang="stylus" rel="stylesheet/stylus"> .ratings position: absolute top: 174px bottom: 0 left: 0 width: 100% overflow: hidden .overview display: flex padding: 18px 0 .overview-left flex: 0 0 137px padding: 6px 0 width: 137px border-right: 1px solid rgba(7, 17, 27, 0.1) text-align: center @media only screen and (max-width: 320px) flex: 0 0 120px width: 120px .score margin-bottom: 6px line-height: 28px font-size: 24px color: rgb(255, 153, 0) .title margin-bottom: 8px line-height: 12px font-size: 12px color: rgb(7, 17, 27) .rank line-height: 10px font-size: 10px color: rgb(147, 153, 159) .overview-right flex: 1 padding: 6px 0 6px 24px @media only screen and (max-width: 320px) padding-left: 6px .score-wrapper margin-bottom: 8px font-size: 0 .title display: inline-block line-height: 18px vertical-align: top font-size: 12px color: rgb(7, 17, 27) .star display: inline-block margin: 0 12px vertical-align: top .score display: inline-block line-height: 18px vertical-align: top font-size: 12px color: rgb(255, 153, 0) .delivery-wrapper font-size: 0 .title line-height: 18px font-size: 12px color: rgb(7, 17, 27) .delivery margin-left: 12px font-size: 12px color: rgb(147, 153, 159) </style>
rating组件开发(五)
继续编写ratings.vue组件,实现评论区滚动功能和相关样式
<template> <div class="ratings" ref="ratings"> <div class="ratings-content"> <div class="overview"> <div class="overview-left"> <h1 class="score">{{seller.score}}</h1> <div class="title">综合评分</div> <div class="rank">高于周边商家{{seller.rankRate}}%</div> </div> <div class="overview-right"> <div class="score-wrapper"> <span class="title">服务态度</span> <star :size="36" :score="seller.serviceScore"></star> <span class="score">{{seller.serviceScore}}</span> </div> <div class="score-wrapper"> <span class="title">商品评分</span> <star :size="36" :score="seller.foodScore"></star> <span class="score">{{seller.foodScore}}</span> </div> <div class="delivery-wrapper"> <span class="title">送达时间</span> <span class="delivery">{{seller.deliveryTime}}分钟</span> </div> </div> </div> <split></split> <ratingselect :selectType="selectType" :onlyContent="onlyContent" :ratings="ratings"></ratingselect> <div class="rating-wrapper"> <ul> <li v-for="rating in ratings" class="rating-item"> <div class="avatar"> <img width="28" height="28" :src="rating.avatar"> </div> <div class="content"> <h1 class="name">{{rating.username}}</h1> <div class="star-wrapper"> <star :size="24" :score="rating.score"></star> <span class="delivery" v-show="rating.deliveryTime">{{rating.deliveryTime}}</span> </div> <p class="text">{{rating.text}}</p> <div class="recommend" v-show="rating.recommend && rating.recommend.length"> <span class="icon-thumb_up"></span> <span class="item" v-for="item in rating.recommend">{{item}}</span> </div> <div class="time"> {{rating.rateTime | formatDate}} </div> </div> </li> </ul> </div> </div> </div> </template> <script> import BScroll from 'better-scroll'; import star from '../../components/star/star'; import split from '../../components/split/split'; import ratingselect from '../../components/ratingselect/ratingselect'; import { formatDate } from '../../common/js/date'; const ALL = 2; const ERR_OK = 0; export default { name: 'v-ratings', props: { seller: { type: Object } }, data() { return { ratings: [], selectType: ALL, onlyContent: true }; }, created() { this.$http.get('/api/ratings').then((response) => { response = response.body; if (response.errno === ERR_OK) { this.ratings = response.data; this.$nextTick(() => { this.scroll = new BScroll(this.$refs.ratings, { click: true }); }); } }); }, filters: { formatDate(time) { let date = new Date(time); return formatDate(date, 'yyyy-MM-dd hh:mm'); } }, components: { star, split, ratingselect } } </script> <style scoped lang="stylus" rel="stylesheet/stylus"> @import "../../common/stylus/mixin.styl" .ratings position: absolute top: 174px bottom: 0 left: 0 width: 100% overflow: hidden .overview display: flex padding: 18px 0 .overview-left flex: 0 0 137px padding: 6px 0 width: 137px border-right: 1px solid rgba(7, 17, 27, 0.1) text-align: center @media only screen and (max-width: 320px) flex: 0 0 120px width: 120px .score margin-bottom: 6px line-height: 28px font-size: 24px color: rgb(255, 153, 0) .title margin-bottom: 8px line-height: 12px font-size: 12px color: rgb(7, 17, 27) .rank line-height: 10px font-size: 10px color: rgb(147, 153, 159) .overview-right flex: 1 padding: 6px 0 6px 24px @media only screen and (max-width: 320px) padding-left: 6px .score-wrapper margin-bottom: 8px font-size: 0 .title display: inline-block line-height: 18px vertical-align: top font-size: 12px color: rgb(7, 17, 27) .star display: inline-block margin: 0 12px vertical-align: top .score display: inline-block line-height: 18px vertical-align: top font-size: 12px color: rgb(255, 153, 0) .delivery-wrapper font-size: 0 .title line-height: 18px font-size: 12px color: rgb(7, 17, 27) .delivery margin-left: 12px font-size: 12px color: rgb(147, 153, 159) .rating-wrapper padding: 0 18px .rating-item display: flex padding: 18px 0 border-1px(rgba(7, 17, 27, 0.1)) .avatar flex: 0 0 28px width: 28px margin-right: 12px img border-radius: 50% .content position: relative flex: 1 .name margin-bottom: 4px line-height: 12px font-size: 10px color: rgb(7, 17, 27) .star-wrapper margin-bottom: 6px font-size: 0 .star display: inline-block margin-right: 6px vertical-align: top .delivery display: inline-block vertical-align: top line-height: 12px font-size: 10px color: rgb(147, 153, 159) .text margin-bottom: 8px line-height: 18px color: rgb(7, 17, 27) font-size: 12px .recommend line-height: 16px font-size: 0 .icon-thumb_up, .item display: inline-block margin: 0 8px 4px 0 font-size: 9px .icon-thumb_up color: rgb(0, 160, 220) .item padding: 0 6px border: 1px solid rgba(7, 17, 27, 0.1) border-radius: 1px color: rgb(147, 153, 159) background: #fff .time position: absolute top: 0 right: 0 line-height: 12px font-size: 10px color: rgb(147, 153, 159) </style>
rating组件开发(六)
完善ratings.vue组件切换“满意、不满意”的效果(失败)
<template> <div class="ratings" ref="ratings"> <div class="ratings-content"> <div class="overview"> <div class="overview-left"> <h1 class="score">{{seller.score}}</h1> <div class="title">综合评分</div> <div class="rank">高于周边商家{{seller.rankRate}}%</div> </div> <div class="overview-right"> <div class="score-wrapper"> <span class="title">服务态度</span> <star :size="36" :score="seller.serviceScore"></star> <span class="score">{{seller.serviceScore}}</span> </div> <div class="score-wrapper"> <span class="title">商品评分</span> <star :size="36" :score="seller.foodScore"></star> <span class="score">{{seller.foodScore}}</span> </div> <div class="delivery-wrapper"> <span class="title">送达时间</span> <span class="delivery">{{seller.deliveryTime}}分钟</span> </div> </div> </div> <split></split> <ratingselect :selectType="selectType" :onlyContent="onlyContent" :ratings="ratings"></ratingselect> <div class="rating-wrapper"> <ul> <li v-for="rating in ratings" v-show="needShow(rating.rateType, rating.text)" class="rating-item"> <div class="avatar"> <img width="28" height="28" :src="rating.avatar"> </div> <div class="content"> <h1 class="name">{{rating.username}}</h1> <div class="star-wrapper"> <star :size="24" :score="rating.score"></star> <span class="delivery" v-show="rating.deliveryTime">{{rating.deliveryTime}}</span> </div> <p class="text">{{rating.text}}</p> <div class="recommend" v-show="rating.recommend && rating.recommend.length"> <span class="icon-thumb_up"></span> <span class="item" v-for="item in rating.recommend">{{item}}</span> </div> <div class="time"> {{rating.rateTime | formatDate}} </div> </div> </li> </ul> </div> </div> </div> </template> <script> import BScroll from 'better-scroll'; import star from '../../components/star/star'; import split from '../../components/split/split'; import ratingselect from '../../components/ratingselect/ratingselect'; import { formatDate } from '../../common/js/date'; const ALL = 2; const ERR_OK = 0; export default { name: 'v-ratings', props: { seller: { type: Object } }, data() { return { ratings: [], selectType: ALL, onlyContent: true }; }, created() { this.$http.get('/api/ratings').then((response) => { response = response.body; if (response.errno === ERR_OK) { this.ratings = response.data; this.$nextTick(() => { this.scroll = new BScroll(this.$refs.ratings, { click: true }); }); } }); }, events: { 'ratingtype.select'(type) { this.selectType = type; this.$nextTick(() => { this.scroll.refresh(); }); }, 'content.toggle'(onlyContent) { this.onlyContent = onlyContent; this.$nextTick(() => { this.scroll.refresh(); }); } }, filters: { formatDate(time) { let date = new Date(time); return formatDate(date, 'yyyy-MM-dd hh:mm'); } }, methods: { needShow(type, text) { if (this.onlyContent && !text) { return false; } if (this.selectType === ALL) { return true; } else { return type === this.selectType; } } }, components: { star, split, ratingselect } } </script> <style scoped lang="stylus" rel="stylesheet/stylus"> @import "../../common/stylus/mixin.styl" .ratings position: absolute top: 174px bottom: 0 left: 0 width: 100% overflow: hidden .overview display: flex padding: 18px 0 .overview-left flex: 0 0 137px padding: 6px 0 width: 137px border-right: 1px solid rgba(7, 17, 27, 0.1) text-align: center @media only screen and (max-width: 320px) flex: 0 0 120px width: 120px .score margin-bottom: 6px line-height: 28px font-size: 24px color: rgb(255, 153, 0) .title margin-bottom: 8px line-height: 12px font-size: 12px color: rgb(7, 17, 27) .rank line-height: 10px font-size: 10px color: rgb(147, 153, 159) .overview-right flex: 1 padding: 6px 0 6px 24px @media only screen and (max-width: 320px) padding-left: 6px .score-wrapper margin-bottom: 8px font-size: 0 .title display: inline-block line-height: 18px vertical-align: top font-size: 12px color: rgb(7, 17, 27) .star display: inline-block margin: 0 12px vertical-align: top .score display: inline-block line-height: 18px vertical-align: top font-size: 12px color: rgb(255, 153, 0) .delivery-wrapper font-size: 0 .title line-height: 18px font-size: 12px color: rgb(7, 17, 27) .delivery margin-left: 12px font-size: 12px color: rgb(147, 153, 159) .rating-wrapper padding: 0 18px .rating-item display: flex padding: 18px 0 border-1px(rgba(7, 17, 27, 0.1)) .avatar flex: 0 0 28px width: 28px margin-right: 12px img border-radius: 50% .content position: relative flex: 1 .name margin-bottom: 4px line-height: 12px font-size: 10px color: rgb(7, 17, 27) .star-wrapper margin-bottom: 6px font-size: 0 .star display: inline-block margin-right: 6px vertical-align: top .delivery display: inline-block vertical-align: top line-height: 12px font-size: 10px color: rgb(147, 153, 159) .text margin-bottom: 8px line-height: 18px color: rgb(7, 17, 27) font-size: 12px .recommend line-height: 16px font-size: 0 .icon-thumb_up, .item display: inline-block margin: 0 8px 4px 0 font-size: 9px .icon-thumb_up color: rgb(0, 160, 220) .item padding: 0 6px border: 1px solid rgba(7, 17, 27, 0.1) border-radius: 1px color: rgb(147, 153, 159) background: #fff .time position: absolute top: 0 right: 0 line-height: 12px font-size: 10px color: rgb(147, 153, 159) </style>
seller组件开发(一)
编写seller.vue组件,建立初步的头部结构
<template> <div class="seller"> <div class="seller-content"> <div class="overview"> <h1 class="title">{{seller.name}}</h1> <div class="desc"> <star :size="36" :score="seller.score"></star> <span class="text">({{seller.ratingCount}})</span> <span class="text">月售{{seller.sellCount}}单</span> </div> <ul class="remark"> <li class="block"> <h2>起送价</h2> <div class="content"> <span class="stress">{{seller.minPrice}}元</span> </div> </li> <li class="block"> <h2>商家配送</h2> <div class="content"> <span class="stress">{{seller.deliveryPrice}}元</span> </div> </li> <li class="block"> <h2>平均配送时间</h2> <div class="content"> <span class="stress">{{seller.deliveryTime}}分钟</span> </div> </li> </ul> </div> </div> </div> </template> <script> import star from '../../components/star/star'; export default { name: 'v-seller', props: { seller: { type: Object } }, components: { star } } </script> <style scoped> </style>
seller组件开发(二)
编写seller.vue组件,建立初步的头部结构样式
<template> <div class="seller"> <div class="seller-content"> <div class="overview"> <h1 class="title">{{seller.name}}</h1> <div class="desc border-1px"> <star :size="36" :score="seller.score"></star> <span class="text">({{seller.ratingCount}})</span> <span class="text">月售{{seller.sellCount}}单</span> </div> <ul class="remark"> <li class="block"> <h2>起送价</h2> <div class="content"> <span class="stress">{{seller.minPrice}}元</span> </div> </li> <li class="block"> <h2>商家配送</h2> <div class="content"> <span class="stress">{{seller.deliveryPrice}}元</span> </div> </li> <li class="block"> <h2>平均配送时间</h2> <div class="content"> <span class="stress">{{seller.deliveryTime}}分钟</span> </div> </li> </ul> </div> </div> </div> </template> <script> import star from '../../components/star/star'; export default { name: 'v-seller', props: { seller: { type: Object } }, components: { star } } </script> <style lang="stylus" rel="stylesheet/stylus"> @import "../../common/stylus/mixin.styl" .seller position: absolute top: 174px bottom: 0 left: 0 width: 100% overflow: hidden .overview position: relative padding: 18px .title margin-bottom: 8px line-height: 14px color: rgb(7, 17, 27) font-size: 14px .desc padding-bottom: 18px border-1px(rgba(7, 17, 27, 0.1)) font-size: 0 .star display: inline-block margin-right: 8px vertical-align: top .text display: inline-block margin-right: 12px line-height: 18px vertical-align: top font-size: 10px color: rgb(77, 85, 93) .remark display: flex padding-top: 18px .block flex: 1 text-align: center border-right: 1px solid rgba(7, 17, 27, 0.1) &:last-child border: none h2 margin-bottom: 4px line-height: 10px font-size: 10px color: rgb(147, 153, 159) .content line-height: 24px font-size: 10px color: rgb(7, 17, 27) .stress font-size: 24px </style>
seller组件开发(三)
编写seller.vue组件,公告与活动初次开发
<template> <div class="seller"> <div class="seller-content"> <div class="overview"> <h1 class="title">{{seller.name}}</h1> <div class="desc border-1px"> <star :size="36" :score="seller.score"></star> <span class="text">({{seller.ratingCount}})</span> <span class="text">月售{{seller.sellCount}}单</span> </div> <ul class="remark"> <li class="block"> <h2>起送价</h2> <div class="content"> <span class="stress">{{seller.minPrice}}</span>元 </div> </li> <li class="block"> <h2>商家配送</h2> <div class="content"> <span class="stress">{{seller.deliveryPrice}}</span>元 </div> </li> <li class="block"> <h2>平均配送时间</h2> <div class="content"> <span class="stress">{{seller.deliveryTime}}</span>分钟 </div> </li> </ul> </div> <split></split> <div class="bulletin"> <h1 class="title">公告与活动</h1> <div class="content-wrapper border-1px"> <p class="content">{{seller.bulletin}}</p> </div> <ul v-if="seller.supports" class="supports"> <li class="support-item border-1px" v-for="(item,index) in seller.supports"> <span class="icon" :class="classMap[seller.supports[index].type]"></span> <span class="text">{{seller.supports[index].description}}</span> </li> </ul> </div> </div> </div> </template> <script> import star from '../../components/star/star'; import split from '../../components/split/split'; export default { name: 'v-seller', props: { seller: { type: Object } }, components: { star, split }, created() { this.classMap = ['decrease', 'discount', 'special', 'invoice', 'guarantee']; } } </script> <style lang="stylus" rel="stylesheet/stylus"> @import "../../common/stylus/mixin.styl" .seller position: absolute top: 174px bottom: 0 left: 0 width: 100% overflow: hidden .overview position: relative padding: 18px .title margin-bottom: 8px line-height: 14px color: rgb(7, 17, 27) font-size: 14px font-weight: 700 .desc padding-bottom: 18px border-1px(rgba(7, 17, 27, 0.1)) font-size: 0 .star display: inline-block margin-right: 8px vertical-align: top .text display: inline-block margin-right: 12px line-height: 18px vertical-align: top font-size: 10px color: rgb(77, 85, 93) .remark display: flex padding-top: 18px .block flex: 1 text-align: center border-right: 1px solid rgba(7, 17, 27, 0.1) &:last-child border: none h2 margin-bottom: 4px line-height: 10px font-size: 10px color: rgb(147, 153, 159) .content line-height: 24px font-size: 10px color: rgb(7, 17, 27) .stress font-size: 24px .bulletin padding: 18px 18px 0 18px .title margin-bottom: 8px line-height: 14px color: rgb(7, 17, 27) font-size: 14px .content-wrapper padding: 0 12px 16px 12px border-1px(rgba(7, 17, 27, 0.1)) .content line-height: 24px font-size: 12px color: rgb(240, 20, 20) </style>
seller组件开发(四)
编写seller.vue组件,公告与活动二次开发,并且实现滚动功能
<template> <div class="seller" ref="seller"> <div class="seller-content"> <div class="overview"> <h1 class="title">{{seller.name}}</h1> <div class="desc border-1px"> <star :size="36" :score="seller.score"></star> <span class="text">({{seller.ratingCount}})</span> <span class="text">月售{{seller.sellCount}}单</span> </div> <ul class="remark"> <li class="block"> <h2>起送价</h2> <div class="content"> <span class="stress">{{seller.minPrice}}</span>元 </div> </li> <li class="block"> <h2>商家配送</h2> <div class="content"> <span class="stress">{{seller.deliveryPrice}}</span>元 </div> </li> <li class="block"> <h2>平均配送时间</h2> <div class="content"> <span class="stress">{{seller.deliveryTime}}</span>分钟 </div> </li> </ul> </div> <split></split> <div class="bulletin"> <h1 class="title">公告与活动</h1> <div class="content-wrapper border-1px"> <p class="content">{{seller.bulletin}}</p> </div> <ul v-if="seller.supports" class="supports"> <li class="support-item border-1px" v-for="(item,index) in seller.supports"> <span class="icon" :class="classMap[seller.supports[index].type]"></span> <span class="text">{{seller.supports[index].description}}</span> </li> </ul> </div> </div> </div> </template> <script> import BScroll from 'better-scroll'; import star from '../../components/star/star'; import split from '../../components/split/split'; export default { name: 'v-seller', props: { seller: { type: Object } }, components: { star, split }, watch: { 'seller' () { this.$nextTick(() => { this._initScroll(); }); } }, methods: { _initScroll() { if (!this.scroll) { this.scroll = new BScroll(this.$refs.seller, { click: true }); } else { this.scroll.refresh(); } } }, created() { this.classMap = ['decrease', 'discount', 'special', 'invoice', 'guarantee']; } } </script> <style lang="stylus" rel="stylesheet/stylus"> @import "../../common/stylus/mixin.styl" .seller position: absolute top: 174px bottom: 0 left: 0 width: 100% overflow: hidden .overview position: relative padding: 18px .title margin-bottom: 8px line-height: 14px color: rgb(7, 17, 27) font-size: 14px font-weight: 700 .desc padding-bottom: 18px border-1px(rgba(7, 17, 27, 0.1)) font-size: 0 .star display: inline-block margin-right: 8px vertical-align: top .text display: inline-block margin-right: 12px line-height: 18px vertical-align: top font-size: 10px color: rgb(77, 85, 93) .remark display: flex padding-top: 18px .block flex: 1 text-align: center border-right: 1px solid rgba(7, 17, 27, 0.1) &:last-child border: none h2 margin-bottom: 4px line-height: 10px font-size: 10px color: rgb(147, 153, 159) .content line-height: 24px font-size: 10px color: rgb(7, 17, 27) .stress font-size: 24px .bulletin padding: 18px 18px 0 18px .title margin-bottom: 8px line-height: 14px color: rgb(7, 17, 27) font-size: 14px .content-wrapper padding: 0 12px 16px 12px border-1px(rgba(7, 17, 27, 0.1)) .content line-height: 24px font-size: 12px color: rgb(240, 20, 20) .supports .support-item padding: 16px 12px border-1px(rgba(7, 17, 27, 0.1)) font-size: 0 &:last-child border-none() .icon display: inline-block width: 16px height: 16px vertical-align: top margin-right: 6px background-size: 16px 16px background-repeat: no-repeat &.decrease bg-image('decrease_4') &.discount bg-image('discount_4') &.guarantee bg-image('guarantee_4') &.invoice bg-image('invoice_4') &.special bg-image('special_4') .text line-height: 16px font-size: 12px color: rgb(7, 17, 27) </style>
seller组件开发——商家实景图(五)
编写seller.vue组件,实现商家实景图效果
<template> <div class="seller" ref="seller"> <div class="seller-content"> <div class="overview"> <h1 class="title">{{seller.name}}</h1> <div class="desc border-1px"> <star :size="36" :score="seller.score"></star> <span class="text">({{seller.ratingCount}})</span> <span class="text">月售{{seller.sellCount}}单</span> </div> <ul class="remark"> <li class="block"> <h2>起送价</h2> <div class="content"> <span class="stress">{{seller.minPrice}}</span>元 </div> </li> <li class="block"> <h2>商家配送</h2> <div class="content"> <span class="stress">{{seller.deliveryPrice}}</span>元 </div> </li> <li class="block"> <h2>平均配送时间</h2> <div class="content"> <span class="stress">{{seller.deliveryTime}}</span>分钟 </div> </li> </ul> </div> <split></split> <div class="bulletin"> <h1 class="title">公告与活动</h1> <div class="content-wrapper border-1px"> <p class="content">{{seller.bulletin}}</p> </div> <ul v-if="seller.supports" class="supports"> <li class="support-item border-1px" v-for="(item,index) in seller.supports"> <span class="icon" :class="classMap[seller.supports[index].type]"></span> <span class="text">{{seller.supports[index].description}}</span> </li> </ul> </div> <split></split> <div class="pics"> <h1 class="title">商家实景</h1> <div class="pic-wrapper" ref="picWrapper"> <ul class="pic-list" ref="picList"> <li class="pic-item" v-for="pic in seller.pics"> <img :src="pic" width="120" height="90"> </li> </ul> </div> </div> </div> </div> </template> <script> import BScroll from 'better-scroll'; import star from '../../components/star/star'; import split from '../../components/split/split'; export default { name: 'v-seller', props: { seller: { type: Object } }, created() { this.classMap = ['decrease', 'discount', 'special', 'invoice', 'guarantee']; }, watch: { 'seller' () { this.$nextTick(() => { this._initScroll(); this._initPics(); }); } }, mounted() { this.$nextTick(() => { this._initScroll(); this._initPics(); }); }, methods: { _initScroll() { if (!this.scroll) { this.scroll = new BScroll(this.$refs.seller, { click: true }); } else { this.scroll.refresh(); } }, _initPics() { if (this.seller.pics) { let picWidth = 120; let margin = 6; let width = (picWidth + margin) * this.seller.pics.length - margin; this.$refs.picList.style.width = width + 'px'; this.$nextTick(() => { if (!this.picScroll) { this.picScroll = new BScroll(this.$refs.picWrapper, { scrollX: true, eventPassthrough: 'vertical' }); } else { this.picScroll.refresh(); } }); } } }, components: { star, split } } </script> <style lang="stylus" rel="stylesheet/stylus"> @import "../../common/stylus/mixin.styl" .seller position: absolute top: 174px bottom: 0 left: 0 width: 100% overflow: hidden .overview position: relative padding: 18px .title margin-bottom: 8px line-height: 14px color: rgb(7, 17, 27) font-size: 14px font-weight: 700 .desc padding-bottom: 18px border-1px(rgba(7, 17, 27, 0.1)) font-size: 0 .star display: inline-block margin-right: 8px vertical-align: top .text display: inline-block margin-right: 12px line-height: 18px vertical-align: top font-size: 10px color: rgb(77, 85, 93) .remark display: flex padding-top: 18px .block flex: 1 text-align: center border-right: 1px solid rgba(7, 17, 27, 0.1) &:last-child border: none h2 margin-bottom: 4px line-height: 10px font-size: 10px color: rgb(147, 153, 159) .content line-height: 24px font-size: 10px color: rgb(7, 17, 27) .stress font-size: 24px .bulletin padding: 18px 18px 0 18px .title margin-bottom: 8px line-height: 14px color: rgb(7, 17, 27) font-size: 14px .content-wrapper padding: 0 12px 16px 12px border-1px(rgba(7, 17, 27, 0.1)) .content line-height: 24px font-size: 12px color: rgb(240, 20, 20) .supports .support-item padding: 16px 12px border-1px(rgba(7, 17, 27, 0.1)) font-size: 0 &:last-child border-none() .icon display: inline-block width: 16px height: 16px vertical-align: top margin-right: 6px background-size: 16px 16px background-repeat: no-repeat &.decrease bg-image('decrease_4') &.discount bg-image('discount_4') &.guarantee bg-image('guarantee_4') &.invoice bg-image('invoice_4') &.special bg-image('special_4') .text line-height: 16px font-size: 12px color: rgb(7, 17, 27) .pics padding: 18px .title margin-bottom: 12px line-height: 14px color: rgb(7, 17, 27) font-size: 14px .pic-wrapper width: 100% overflow: hidden white-space: nowrap .pic-list font-size: 0 .pic-item display: inline-block margin-right: 6px width: 120px height: 90px &:last-child margin: 0 </style>
seller组件开发——商家信息(六)
编写seller.vue组件,实现商家信息部分
<template> <div class="seller" ref="seller"> <div class="seller-content"> <div class="overview"> <h1 class="title">{{seller.name}}</h1> <div class="desc border-1px"> <star :size="36" :score="seller.score"></star> <span class="text">({{seller.ratingCount}})</span> <span class="text">月售{{seller.sellCount}}单</span> </div> <ul class="remark"> <li class="block"> <h2>起送价</h2> <div class="content"> <span class="stress">{{seller.minPrice}}</span>元 </div> </li> <li class="block"> <h2>商家配送</h2> <div class="content"> <span class="stress">{{seller.deliveryPrice}}</span>元 </div> </li> <li class="block"> <h2>平均配送时间</h2> <div class="content"> <span class="stress">{{seller.deliveryTime}}</span>分钟 </div> </li> </ul> </div> <split></split> <div class="bulletin"> <h1 class="title">公告与活动</h1> <div class="content-wrapper border-1px"> <p class="content">{{seller.bulletin}}</p> </div> <ul v-if="seller.supports" class="supports"> <li class="support-item border-1px" v-for="(item,index) in seller.supports"> <span class="icon" :class="classMap[seller.supports[index].type]"></span> <span class="text">{{seller.supports[index].description}}</span> </li> </ul> </div> <split></split> <div class="pics"> <h1 class="title">商家实景</h1> <div class="pic-wrapper" ref="picWrapper"> <ul class="pic-list" ref="picList"> <li class="pic-item" v-for="pic in seller.pics"> <img :src="pic" width="120" height="90"> </li> </ul> </div> </div> <split></split> <div class="info"> <h1 class="title border-1px">商家信息</h1> <ul> <li class="info-item" v-for="info in seller.infos">{{info}}</li> </ul> </div> </div> </div> </template> <script> import BScroll from 'better-scroll'; import star from '../../components/star/star'; import split from '../../components/split/split'; export default { name: 'v-seller', props: { seller: { type: Object } }, created() { this.classMap = ['decrease', 'discount', 'special', 'invoice', 'guarantee']; }, watch: { 'seller' () { this.$nextTick(() => { this._initScroll(); this._initPics(); }); } }, mounted() { this.$nextTick(() => { this._initScroll(); this._initPics(); }); }, methods: { _initScroll() { if (!this.scroll) { this.scroll = new BScroll(this.$refs.seller, { click: true }); } else { this.scroll.refresh(); } }, _initPics() { if (this.seller.pics) { let picWidth = 120; let margin = 6; let width = (picWidth + margin) * this.seller.pics.length - margin; this.$refs.picList.style.width = width + 'px'; this.$nextTick(() => { if (!this.picScroll) { this.picScroll = new BScroll(this.$refs.picWrapper, { scrollX: true, eventPassthrough: 'vertical' }); } else { this.picScroll.refresh(); } }); } } }, components: { star, split } } </script> <style lang="stylus" rel="stylesheet/stylus"> @import "../../common/stylus/mixin.styl" .seller position: absolute top: 174px bottom: 0 left: 0 width: 100% overflow: hidden .overview position: relative padding: 18px .title margin-bottom: 8px line-height: 14px color: rgb(7, 17, 27) font-size: 14px font-weight: 700 .desc padding-bottom: 18px border-1px(rgba(7, 17, 27, 0.1)) font-size: 0 .star display: inline-block margin-right: 8px vertical-align: top .text display: inline-block margin-right: 12px line-height: 18px vertical-align: top font-size: 10px color: rgb(77, 85, 93) .remark display: flex padding-top: 18px .block flex: 1 text-align: center border-right: 1px solid rgba(7, 17, 27, 0.1) &:last-child border: none h2 margin-bottom: 4px line-height: 10px font-size: 10px color: rgb(147, 153, 159) .content line-height: 24px font-size: 10px color: rgb(7, 17, 27) .stress font-size: 24px .bulletin padding: 18px 18px 0 18px .title margin-bottom: 8px line-height: 14px color: rgb(7, 17, 27) font-size: 14px .content-wrapper padding: 0 12px 16px 12px border-1px(rgba(7, 17, 27, 0.1)) .content line-height: 24px font-size: 12px color: rgb(240, 20, 20) .supports .support-item padding: 16px 12px border-1px(rgba(7, 17, 27, 0.1)) font-size: 0 &:last-child border-none() .icon display: inline-block width: 16px height: 16px vertical-align: top margin-right: 6px background-size: 16px 16px background-repeat: no-repeat &.decrease bg-image('decrease_4') &.discount bg-image('discount_4') &.guarantee bg-image('guarantee_4') &.invoice bg-image('invoice_4') &.special bg-image('special_4') .text line-height: 16px font-size: 12px color: rgb(7, 17, 27) .pics padding: 18px .title margin-bottom: 12px line-height: 14px color: rgb(7, 17, 27) font-size: 14px .pic-wrapper width: 100% overflow: hidden white-space: nowrap .pic-list font-size: 0 .pic-item display: inline-block margin-right: 6px width: 120px height: 90px &:last-child margin: 0 .info padding: 18px 18px 0 18px color: rgb(7, 17, 27) .title padding-bottom: 12px line-height: 14px border-1px(rgba(7, 17, 27, 0.1)) font-size: 14px .info-item padding: 16px 12px line-height: 16px border-1px(rgba(7, 17, 27, 0.1)) font-size: 12px &:last-child border-none() </style>
seller组件开发——收藏商家(一)
编写seller.vue组件,实现商家收藏基本效果
<template> <div class="seller" ref="seller"> <div class="seller-content"> <div class="overview"> <h1 class="title">{{seller.name}}</h1> <div class="desc border-1px"> <star :size="36" :score="seller.score"></star> <span class="text">({{seller.ratingCount}})</span> <span class="text">月售{{seller.sellCount}}单</span> </div> <ul class="remark"> <li class="block"> <h2>起送价</h2> <div class="content"> <span class="stress">{{seller.minPrice}}</span>元 </div> </li> <li class="block"> <h2>商家配送</h2> <div class="content"> <span class="stress">{{seller.deliveryPrice}}</span>元 </div> </li> <li class="block"> <h2>平均配送时间</h2> <div class="content"> <span class="stress">{{seller.deliveryTime}}</span>分钟 </div> </li> </ul> <div class="favorite" @click="toggleFavorite"> <span class="icon-favorite" :class="{'active':favorite}"></span> <span class="text">{{favoriteText}}</span> </div> </div> <split></split> <div class="bulletin"> <h1 class="title">公告与活动</h1> <div class="content-wrapper border-1px"> <p class="content">{{seller.bulletin}}</p> </div> <ul v-if="seller.supports" class="supports"> <li class="support-item border-1px" v-for="(item,index) in seller.supports"> <span class="icon" :class="classMap[seller.supports[index].type]"></span> <span class="text">{{seller.supports[index].description}}</span> </li> </ul> </div> <split></split> <div class="pics"> <h1 class="title">商家实景</h1> <div class="pic-wrapper" ref="picWrapper"> <ul class="pic-list" ref="picList"> <li class="pic-item" v-for="pic in seller.pics"> <img :src="pic" width="120" height="90"> </li> </ul> </div> </div> <split></split> <div class="info"> <h1 class="title border-1px">商家信息</h1> <ul> <li class="info-item" v-for="info in seller.infos">{{info}}</li> </ul> </div> </div> </div> </template> <script> import BScroll from 'better-scroll'; import star from '../../components/star/star'; import split from '../../components/split/split'; export default { name: 'v-seller', props: { seller: { type: Object } }, data() { return { favorite: false }; }, computed: { favoriteText() { return this.favorite ? '已收藏' : '收藏'; } }, created() { this.classMap = ['decrease', 'discount', 'special', 'invoice', 'guarantee']; }, watch: { 'seller' () { this.$nextTick(() => { this._initScroll(); this._initPics(); }); } }, mounted() { this.$nextTick(() => { this._initScroll(); this._initPics(); }); }, methods: { toggleFavorite(event) { if (!event._constructed) { return; } this.favorite = !this.favorite; }, _initScroll() { if (!this.scroll) { this.scroll = new BScroll(this.$refs.seller, { click: true }); } else { this.scroll.refresh(); } }, _initPics() { if (this.seller.pics) { let picWidth = 120; let margin = 6; let width = (picWidth + margin) * this.seller.pics.length - margin; this.$refs.picList.style.width = width + 'px'; this.$nextTick(() => { if (!this.picScroll) { this.picScroll = new BScroll(this.$refs.picWrapper, { scrollX: true, eventPassthrough: 'vertical' }); } else { this.picScroll.refresh(); } }); } } }, components: { star, split } } </script> <style lang="stylus" rel="stylesheet/stylus"> @import "../../common/stylus/mixin.styl" .seller position: absolute top: 174px bottom: 0 left: 0 width: 100% overflow: hidden .overview position: relative padding: 18px .title margin-bottom: 8px line-height: 14px color: rgb(7, 17, 27) font-size: 14px font-weight: 700 .desc padding-bottom: 18px border-1px(rgba(7, 17, 27, 0.1)) font-size: 0 .star display: inline-block margin-right: 8px vertical-align: top .text display: inline-block margin-right: 12px line-height: 18px vertical-align: top font-size: 10px color: rgb(77, 85, 93) .remark display: flex padding-top: 18px .block flex: 1 text-align: center border-right: 1px solid rgba(7, 17, 27, 0.1) &:last-child border: none h2 margin-bottom: 4px line-height: 10px font-size: 10px color: rgb(147, 153, 159) .content line-height: 24px font-size: 10px color: rgb(7, 17, 27) .stress font-size: 24px .favorite position: absolute width: 50px right: 11px top: 18px text-align: center .icon-favorite display: block margin-bottom: 4px line-height: 24px font-size: 24px color: #d4d6d9 &.active color: rgb(240, 20, 20) .text line-height: 10px font-size: 10px color: rgb(77, 85, 93) .bulletin padding: 18px 18px 0 18px .title margin-bottom: 8px line-height: 14px color: rgb(7, 17, 27) font-size: 14px .content-wrapper padding: 0 12px 16px 12px border-1px(rgba(7, 17, 27, 0.1)) .content line-height: 24px font-size: 12px color: rgb(240, 20, 20) .supports .support-item padding: 16px 12px border-1px(rgba(7, 17, 27, 0.1)) font-size: 0 &:last-child border-none() .icon display: inline-block width: 16px height: 16px vertical-align: top margin-right: 6px background-size: 16px 16px background-repeat: no-repeat &.decrease bg-image('decrease_4') &.discount bg-image('discount_4') &.guarantee bg-image('guarantee_4') &.invoice bg-image('invoice_4') &.special bg-image('special_4') .text line-height: 16px font-size: 12px color: rgb(7, 17, 27) .pics padding: 18px .title margin-bottom: 12px line-height: 14px color: rgb(7, 17, 27) font-size: 14px .pic-wrapper width: 100% overflow: hidden white-space: nowrap .pic-list font-size: 0 .pic-item display: inline-block margin-right: 6px width: 120px height: 90px &:last-child margin: 0 .info padding: 18px 18px 0 18px color: rgb(7, 17, 27) .title padding-bottom: 12px line-height: 14px border-1px(rgba(7, 17, 27, 0.1)) font-size: 14px .info-item padding: 16px 12px line-height: 16px border-1px(rgba(7, 17, 27, 0.1)) font-size: 12px &:last-child border-none() </style>
seller组件开发——收藏商家(二)
步骤一:编写App.vue根组件,为传递商家ID值做前期准备
<template> <div> <v-header :seller="seller"></v-header> <div class="tab border-1px"> <div class="tab-item"> <router-link to="/goods">商品</router-link> </div> <div class="tab-item"> <router-link to="/ratings">评价</router-link> </div> <div class="tab-item"> <router-link to="/seller">商家</router-link> </div> </div> <router-view :seller="seller"></router-view> </div> </template> <script> import header from './components/header/header.vue'; import {urlParse} from './common/js/util'; const ERR_OK = 0; export default { name: 'app', data() { return { seller: { id: (() => { let queryParam = urlParse(); return queryParam.id; })() } }; }, created() { this.$http.get('api/seller?id=' + this.seller.id).then((response) => { response = response.body; if(response.error === ERR_OK){ this.seller = Object.assign({}, this.seller, response.data); } this.seller = response.data; }) }, components: { 'v-header': header } } </script> <style scoped lang="stylus" rel="stylesheet/stylus"> @import "./common/stylus/mixin.styl"; .tab display: flex width: 100% height: 40px line-height: 40px /*border-bottom: 1px solid rgba(7, 17, 27, 0.1)*/ border-1px(rgba(7, 17, 27, 0.1)) .tab-item flex: 1 text-align: center & > a display:block font-size:16px color:rgb(77,85,93) &.active color:rgb(240,20,20) </style>
步骤二:新建util.js文件,实现查询的效果
/** * 解析url参数 * @example ?id=12345&a=b * @return Object {id:12345,a:b} */ export function urlParse() { let url = window.location.search; let obj = {}; let reg = /[?&][^?&]+=[^?&]+/g; let arr = url.match(reg); // ['?id=12345', '&a=b'] if (arr) { arr.forEach((item) => { let tempArr = item.substring(1).split('='); let key = decodeURIComponent(tempArr[0]); let val = decodeURIComponent(tempArr[1]); obj[key] = val; }); } return obj; };
seller组件开发——收藏商家(三)
步骤一:新建store.js文件,实现存储状态的功能
export function saveToLocal(id, key, value) {
let seller = window.localStorage.__seller__;
if (!seller) {
seller = {};
seller[id] = {};
} else {
seller = JSON.parse(seller);
if (!seller[id]) {
seller[id] = {};
}
}
seller[id][key] = value;
window.localStorage.__seller__ = JSON.stringify(seller);
};
export function loadFromLocal(id, key, def) {
let seller = window.localStorage.__seller__;
if (!seller) {
return def;
}
seller = JSON.parse(seller)[id];
if (!seller) {
return def;
}
let ret = seller[key];
return ret || def;
};
步骤二:编写seller.vue组件,将数据传入store.js中
<template> <div class="seller" ref="seller"> <div class="seller-content"> <div class="overview"> <h1 class="title">{{seller.name}}</h1> <div class="desc border-1px"> <star :size="36" :score="seller.score"></star> <span class="text">({{seller.ratingCount}})</span> <span class="text">月售{{seller.sellCount}}单</span> </div> <ul class="remark"> <li class="block"> <h2>起送价</h2> <div class="content"> <span class="stress">{{seller.minPrice}}</span>元 </div> </li> <li class="block"> <h2>商家配送</h2> <div class="content"> <span class="stress">{{seller.deliveryPrice}}</span>元 </div> </li> <li class="block"> <h2>平均配送时间</h2> <div class="content"> <span class="stress">{{seller.deliveryTime}}</span>分钟 </div> </li> </ul> <div class="favorite" @click="toggleFavorite"> <span class="icon-favorite" :class="{'active':favorite}"></span> <span class="text">{{favoriteText}}</span> </div> </div> <split></split> <div class="bulletin"> <h1 class="title">公告与活动</h1> <div class="content-wrapper border-1px"> <p class="content">{{seller.bulletin}}</p> </div> <ul v-if="seller.supports" class="supports"> <li class="support-item border-1px" v-for="(item,index) in seller.supports"> <span class="icon" :class="classMap[seller.supports[index].type]"></span> <span class="text">{{seller.supports[index].description}}</span> </li> </ul> </div> <split></split> <div class="pics"> <h1 class="title">商家实景</h1> <div class="pic-wrapper" ref="picWrapper"> <ul class="pic-list" ref="picList"> <li class="pic-item" v-for="pic in seller.pics"> <img :src="pic" width="120" height="90"> </li> </ul> </div> </div> <split></split> <div class="info"> <h1 class="title border-1px">商家信息</h1> <ul> <li class="info-item" v-for="info in seller.infos">{{info}}</li> </ul> </div> </div> </div> </template> <script> import BScroll from 'better-scroll'; import star from '../../components/star/star'; import split from '../../components/split/split'; import {saveToLocal, loadFromLocal} from '../../common/js/store'; export default { name: 'v-seller', props: { seller: { type: Object } }, data() { return { favorite: (() => { return loadFromLocal(this.seller.id, 'favorite', false); })() }; }, computed: { favoriteText() { return this.favorite ? '已收藏' : '收藏'; } }, created() { this.classMap = ['decrease', 'discount', 'special', 'invoice', 'guarantee']; }, watch: { 'seller' () { this.$nextTick(() => { this._initScroll(); this._initPics(); }); } }, mounted() { this.$nextTick(() => { this._initScroll(); this._initPics(); }); }, methods: { toggleFavorite(event) { if (!event._constructed) { return; } this.favorite = !this.favorite; saveToLocal(this.seller.id, 'favorite', this.favorite); }, _initScroll() { if (!this.scroll) { this.scroll = new BScroll(this.$refs.seller, { click: true }); } else { this.scroll.refresh(); } }, _initPics() { if (this.seller.pics) { let picWidth = 120; let margin = 6; let width = (picWidth + margin) * this.seller.pics.length - margin; this.$refs.picList.style.width = width + 'px'; this.$nextTick(() => { if (!this.picScroll) { this.picScroll = new BScroll(this.$refs.picWrapper, { scrollX: true, eventPassthrough: 'vertical' }); } else { this.picScroll.refresh(); } }); } } }, components: { star, split } } </script> <style lang="stylus" rel="stylesheet/stylus"> @import "../../common/stylus/mixin.styl" .seller position: absolute top: 174px bottom: 0 left: 0 width: 100% overflow: hidden .overview position: relative padding: 18px .title margin-bottom: 8px line-height: 14px color: rgb(7, 17, 27) font-size: 14px font-weight: 700 .desc padding-bottom: 18px border-1px(rgba(7, 17, 27, 0.1)) font-size: 0 .star display: inline-block margin-right: 8px vertical-align: top .text display: inline-block margin-right: 12px line-height: 18px vertical-align: top font-size: 10px color: rgb(77, 85, 93) .remark display: flex padding-top: 18px .block flex: 1 text-align: center border-right: 1px solid rgba(7, 17, 27, 0.1) &:last-child border: none h2 margin-bottom: 4px line-height: 10px font-size: 10px color: rgb(147, 153, 159) .content line-height: 24px font-size: 10px color: rgb(7, 17, 27) .stress font-size: 24px .favorite position: absolute width: 50px right: 11px top: 18px text-align: center .icon-favorite display: block margin-bottom: 4px line-height: 24px font-size: 24px color: #d4d6d9 &.active color: rgb(240, 20, 20) .text line-height: 10px font-size: 10px color: rgb(77, 85, 93) .bulletin padding: 18px 18px 0 18px .title margin-bottom: 8px line-height: 14px color: rgb(7, 17, 27) font-size: 14px .content-wrapper padding: 0 12px 16px 12px border-1px(rgba(7, 17, 27, 0.1)) .content line-height: 24px font-size: 12px color: rgb(240, 20, 20) .supports .support-item padding: 16px 12px border-1px(rgba(7, 17, 27, 0.1)) font-size: 0 &:last-child border-none() .icon display: inline-block width: 16px height: 16px vertical-align: top margin-right: 6px background-size: 16px 16px background-repeat: no-repeat &.decrease bg-image('decrease_4') &.discount bg-image('discount_4') &.guarantee bg-image('guarantee_4') &.invoice bg-image('invoice_4') &.special bg-image('special_4') .text line-height: 16px font-size: 12px color: rgb(7, 17, 27) .pics padding: 18px .title margin-bottom: 12px line-height: 14px color: rgb(7, 17, 27) font-size: 14px .pic-wrapper width: 100% overflow: hidden white-space: nowrap .pic-list font-size: 0 .pic-item display: inline-block margin-right: 6px width: 120px height: 90px &:last-child margin: 0 .info padding: 18px 18px 0 18px color: rgb(7, 17, 27) .title padding-bottom: 12px line-height: 14px border-1px(rgba(7, 17, 27, 0.1)) font-size: 14px .info-item padding: 16px 12px line-height: 16px border-1px(rgba(7, 17, 27, 0.1)) font-size: 12px &:last-child border-none() </style>
体验优化
编写App.vue组件实现状态的保留,原理是将当前的状态保存在内存中,使用的keep-alive标签。
<template> <div> <v-header :seller="seller"></v-header> <div class="tab border-1px"> <div class="tab-item"> <router-link to="/goods">商品</router-link> </div> <div class="tab-item"> <router-link to="/ratings">评价</router-link> </div> <div class="tab-item"> <router-link to="/seller">商家</router-link> </div> </div> <keep-alive> <router-view :seller="seller"></router-view> </keep-alive> </div> </template> <script> import header from './components/header/header.vue'; import {urlParse} from './common/js/util'; const ERR_OK = 0; export default { name: 'app', data() { return { seller: { id: (() => { let queryParam = urlParse(); return queryParam.id; })() } }; }, created() { this.$http.get('api/seller?id=' + this.seller.id).then((response) => { response = response.body; if(response.error === ERR_OK){ this.seller = Object.assign({}, this.seller, response.data); } this.seller = response.data; }) }, components: { 'v-header': header } } </script> <style scoped lang="stylus" rel="stylesheet/stylus"> @import "./common/stylus/mixin.styl"; .tab display: flex width: 100% height: 40px line-height: 40px /*border-bottom: 1px solid rgba(7, 17, 27, 0.1)*/ border-1px(rgba(7, 17, 27, 0.1)) .tab-item flex: 1 text-align: center & > a display:block font-size:16px color:rgb(77,85,93) &.active color:rgb(240,20,20) </style>