vue.js高仿饿了么(前期整理)

时间:2022-12-17 12:02:04

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高仿饿了么(前期整理)

什么是vue.js

它是一个轻量级MVVM框架,主要用于数据驱动+组件化的前端开发。它具有以下特点(数据驱动和组件化为核心思想):

1、轻量级(大小只有20k+)

2、简洁(容易上手,学习曲线平稳)

3、快速

4、组件化:扩展HTML元素,封装可重用的代码。设计的原则为(1)页面上每个独立的可视/可交互区域视为一个组件;(2)每个组件对应一个工程目录,组件所需要的各种资源在这个目录下就近维护;(3)页面不过是组件的容器,组件可以嵌套*组合,形成完整的页面。

vue.js高仿饿了么(前期整理)

5、数据驱动:DOM是数据的一种自然映射,看下面两张图进行理解:

vue.js高仿饿了么(前期整理)         vue.js高仿饿了么(前期整理)

结合上面两张图,再看看下面一张图,进一步理解vue.js实现双向数据绑定的原理(又称为数据响应原理,即数据【model】改变驱动视图【view】自动更新):

vue.js高仿饿了么(前期整理)

6、模块友好

Vue-cli介绍

这是一款Vue的脚手架工具,脚手架的含义就是编写好基础的代码。vue-cli帮助我们编写好了目录结构、本地调试、代码部署、热加载和单元测试。

github地址:https://github.com/vuejs/vue-cli

安装Vue-cli

基本教程戳这里

【注意】

初次使用,千万不要启动ESLint语法规范。

webpack打包

webpack就是将各种资源打包,然后输出js、图片、css静态文件。

vue.js高仿饿了么(前期整理)

新姿势

设备像素比(DPR)

图标字体的制作

打开https://icomoon.io/ => 点击右上角icomoon app => 点击左上角import icons =>选择做好的svg文件 => 点击左上角untitled set选择上图标 => 点击右下角generate fonts =>再点击右下角download即可下载(左上角preferences是修改下载文件夹的名称)。

使用图标字体

设计好项目目录,将fonts文件夹下的文件复制粘贴进项目中,将style.css复制粘贴进stylus目录中,并改名为icon.styl,同时将里面的内容改为stylus语法

vue.js高仿饿了么(前期整理)

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

vue.js高仿饿了么(前期整理)

组件拆分部分(一) 启动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>

vue.js高仿饿了么(前期整理)

组件拆分部分(二) 

由于需要用到stylus语法,因此事先得安装stylus 和 stylus-loader的相关依赖包

vue.js高仿饿了么(前期整理)

步骤一:修改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>

vue.js高仿饿了么(前期整理)

在局域网内,通过手机访问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的规则

假如在创建项目中出现如下情况

vue.js高仿饿了么(前期整理)

那么就打开.eslintrc.js文件,将其规则忽略掉(设置为0)

vue.js高仿饿了么(前期整理)

清除掉令人抓狂的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

vue.js高仿饿了么(前期整理)

步骤二:配置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文件夹中

vue.js高仿饿了么(前期整理)

步骤二:编写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>

详情弹层页(二)

sticky footers布局相关知识

继续编写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

vue.js高仿饿了么(前期整理)

步骤二:编写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>

vue.js高仿饿了么(前期整理)的更多相关文章

  1. Vue&period;js高仿饿了么WebApp

    介绍 学习Vue.js也有一阵子了,为了加深对Vue的理解及运用,做了一个小项目.这是一个高仿饿了么外卖WebApp,现已完成商品预览.商品详情.商家预览.添加购物.查看评论等功能. 部分截图 项目预 ...

  2. 【饿了么】—— Vue2&period;0高仿饿了么核心模块&amp&semi;移动端Web App项目爬坑(三)

    前言:接着上一篇项目总结,这一篇是学习过程记录的最后一篇,这里会梳理:评论组件.商家组件.优化.打包.相关资料链接.项目github地址:https://github.com/66Web/ljq_el ...

  3. 基于vue2&plus;nuxt构建的高仿饿了么(2018版)

    前言 高仿饿了么,以nuxt作为vue的服务端渲染,适合刚接触或者准备上vue ssr的同学参考和学习 项目地址如遇网络不佳,请移步国内镜像加速节点 效果演示 查看demo请戳这里(请用chrome手 ...

  4. 用vue2 &plus;vue-router2 &plus; es6 &plus;webpack 高仿饿了么app(干货满满)

    #高仿饿了么app商家详情 (vue2 +vue-router2 + es6 +webpack )   ##demo [demo 地址](http://liangxiaojuan.github.io/ ...

  5. Vuejs 高仿饿了么外卖APP 百度云视频教程下载

    Vuejs 高仿饿了么外卖APP 百度云视频教程下载 链接:https://pan.baidu.com/s/1KPbKog0qJqXI-2ztQ19o7w 提取码: 关注公众号[GitHubCN]回复 ...

  6. 使用Vue&period;js制作仿Metronic高级表格(一)静态设计

    Metronic高级表格是Metonic框架中自行实现的表格,其底层是Datatables.本教程将主要使用Vue实现交互部分,使用Bootstrap做样式库.jQuery做部分用户交互(弹窗). 使 ...

  7. 【Vue&period;js】高仿饿了么外卖App(一)

    1.架构从传统的MVC向REST API+前端MV*迁移 参考链接: http://blog.csdn.net/broadview2006/article/details/8615055 http:/ ...

  8. 慕课网,vue高仿饿了吗ASP源码视频笔记

    1.源码笔记 我的源码+笔记(很重要):http://pan.baidu.com/s/1geI4i2Z 感谢麦子学院项目相关视频 2.参考资料 Vue.js官网(https://vuejs.org.c ...

  9. VUE高仿饿了么app开发思维导图

    来自互联网 文章来源:刘俊涛的博客 地址:http://www.cnblogs.com/lovebing

随机推荐

  1. 高仿QQ顶部控件之IOS SegmentView

    经常会看到QQ上面有一个 消息和电话 的顶部面板,这个空间是IOS7的分段控制,android中没有这个控件,今天在威哥的微信公众号中成功gank到这个自定义控件的实现,下面跟着尝试一波. 首先是定义 ...

  2. case when语句后的表达式

    SQL中Case When语句的语法如下 Simple CASE expression: CASE input_expression WHEN when_expression THEN result_ ...

  3. 弹窗插件layer

    layer的插件的地址:http://layer.layui.com/简单使用: layer.open({ type: , //page层 area: ['500px', '300px'], titl ...

  4. java用正则表达式获取domain

    在工作中经常用到获取url的来源和域名的黑白名单功能.前段时间写了一个获取url中域名的方法.但是在测试过程中发现有些小问题. /** * 根据URL获取domain * @param url * @ ...

  5. 原生ajax写的上拉加载

    上拉加载的思路 1 上拉加载是要把屏幕拉到最底部的时候触发ajax事件请求数据 2.所有要获取屏幕的高度 文档的高度 和滚动的高度 下面的代码是已经做好了兼容的可以直接拿来用 Javascript: ...

  6. 通过 Git 上传代码到 GitHub 必要操作详解

    目录 Git 介绍 起步 下载 配置 准备 在 GitHub 上创建 SSH Key 提交 修改代码 更多信息 Git 欢迎来到 Git 的学习. 介绍 首先先了解一下 Git. Git,是一个开源的 ...

  7. 【MySQL】解决You can&&num;39&semi;t specify target table &&num;39&semi;user&lowbar;cut&lowbar;record&lowbar;0413&&num;39&semi; for update in FROM clause

    问题 You can't specify target table 'user_cut_record_0413' for update in FROM clause 原因 待更新/删除的数据集与查询的 ...

  8. 自学Linux Shell9&period;2-基于Red Hat系统工具包存在两种方式之一:RPM包

    点击返回 自学Linux命令行与Shell脚本之路 9.2-基于Red Hat系统工具包存在两种方式之一:RPM包 本节主要介绍基于Red Had的系统(测试系统centos) 1. 工具包存在两种方 ...

  9. 报错解决——ctypes&period;ArgumentError&colon; argument 1&colon;………&period;&period; &colon; wrong type

    运行 python darknet.py 结果报错如下: Traceback (most recent call last): File “darknet.py”, line 136, in net ...

  10. 永久有效的 webstorm license server 20180808

    下载地址  https://download.jetbrains.com/webstorm/WebStorm-2018.3.2.exe 2018年10月26日,最近老是过期,搞了一个1年有效的代码,是 ...