1、熟悉項目開發流程
需求分析——>腳手架工具——>數據mock——>架構設計——>代碼編寫——>自測——>編譯打包。
2、熟悉代碼規范
從架構設計、組件抽象、模塊拆分,到代碼風格統一、CSS代碼規范和JavaScript變量命名規范,以標准寫代碼,開發出擴展性、通用性強的優質代碼。
3、掌握Vue.js在實戰中應用
4、學會使用Vue.js完整地開發移動端App
5、學會工程化開發、組件化開發和模塊化開發的方式
6、酷炫的交互設計
所用到的技術
后端:vue-resource(ajax通信)
前端:vue-router(官方插件管理路由)、localstorage、flex布局、css sticky footer布局、html、css、es6、Vue-cli(腳手架,用來搭建基本代碼框架)、vue(熱門前端框架)
其他:webpack(構建工具)、eslint(代碼風格檢查工具)
第三方庫:better-scroll
Vue.js出現的歷史原因也是前端的發展趨勢
1、舊瀏覽器逐漸淘汰,移動端需求增加;
2、前端交互越來越多,功能越來越復雜;
3、架構從傳統后台MVC向REST API+前端MV*(MVC、MVP、MVVM)遷移。
MVVM框架具有的優點
1、針對具有復雜交互邏輯的前端應用;
2、提供基礎的架構抽象;
3、通過Ajax數據持久化,保證前端用戶體驗。
下面這張圖充分說明了什么是MVVM框架,也說明vue.js雙向數據綁定的基本原理:
什么是vue.js
它是一個輕量級MVVM框架,主要用於數據驅動+組件化的前端開發。它具有以下特點(數據驅動和組件化為核心思想):
1、輕量級(大小只有20k+)
2、簡潔(容易上手,學習曲線平穩)
3、快速
4、組件化:擴展HTML元素,封裝可重用的代碼。設計的原則為(1)頁面上每個獨立的可視/可交互區域視為一個組件;(2)每個組件對應一個工程目錄,組件所需要的各種資源在這個目錄下就近維護;(3)頁面不過是組件的容器,組件可以嵌套自由組合,形成完整的頁面。

5、數據驅動:DOM是數據的一種自然映射,看下面兩張圖進行理解:

結合上面兩張圖,再看看下面一張圖,進一步理解vue.js實現雙向數據綁定的原理(又稱為數據響應原理,即數據【model】改變驅動視圖【view】自動更新):

6、模塊友好
Vue-cli介紹
這是一款Vue的腳手架工具,腳手架的含義就是編寫好基礎的代碼。vue-cli幫助我們編寫好了目錄結構、本地調試、代碼部署、熱加載和單元測試。
github地址:https://github.com/vuejs/vue-cli
安裝Vue-cli
基本教程戳這里
【注意】
初次使用,千萬不要啟動ESLint語法規范。
webpack打包
webpack就是將各種資源打包,然后輸出js、圖片、css靜態文件。

新姿勢
設備像素比(DPR)
圖標字體的制作
打開https://icomoon.io/ => 點擊右上角icomoon app => 點擊左上角import icons =>選擇做好的svg文件 => 點擊左上角untitled set選擇上圖標 => 點擊右下角generate fonts =>再點擊右下角download即可下載(左上角preferences是修改下載文件夾的名稱)。
使用圖標字體
設計好項目目錄,將fonts文件夾下的文件復制粘貼進項目中,將style.css復制粘貼進stylus目錄中,並改名為icon.styl,同時將里面的內容改為stylus語法

mock數據
作為前端經常需要模擬后台數據,我們稱之為mock,通常的方式為自己搭建一個服務器或者創建一個json文件,返回我們想要的數據。
在這一步有個坑,就是vue-cli更新了,很多配置都發生了變更。在這里有很好的解決方法。下面貼上webpack.dev.conf.js更改后的代碼
'use strict' const utils = require('./utils') const webpack = require('webpack') const config = require('../config') const merge = require('webpack-merge') const path = require('path') const baseWebpackConfig = require('./webpack.base.conf') const CopyWebpackPlugin = require('copy-webpack-plugin') const HtmlWebpackPlugin = require('html-webpack-plugin') const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin') const portfinder = require('portfinder') //首先 const express = require('express') const app = express() var appData = require('../data.json') var seller = appData.seller var goods = appData.goods var ratings = appData.ratings var apiRoutes = express.Router() app.use('/api', apiRoutes) const HOST = process.env.HOST const PORT = process.env.PORT && Number(process.env.PORT) const devWebpackConfig = merge(baseWebpackConfig, { module: { rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, usePostCSS: true }) }, // cheap-module-eval-source-map is faster for development devtool: config.dev.devtool, // these devServer options should be customized in /config/index.js devServer: { clientLogLevel: 'warning', historyApiFallback: { rewrites: [{ from: /.*/, to: path.join(config.dev.assetsPublicPath, 'index.html') }, ], }, hot: true, contentBase: false, // since we use CopyWebpackPlugin. compress: true, host: HOST || config.dev.host, port: PORT || config.dev.port, open: config.dev.autoOpenBrowser, overlay: config.dev.errorOverlay ? { warnings: false, errors: true } : false, publicPath: config.dev.assetsPublicPath, proxy: config.dev.proxyTable, quiet: true, // necessary for FriendlyErrorsPlugin watchOptions: { poll: config.dev.poll, }, //找到devServer,添加 before(app) { app.get('/api/seller', (req, res) => { res.json({ // 這里是你的json內容 errno: 0, data: seller }) }), app.get('/api/goods', (req, res) => { res.json({ // 這里是你的json內容 errno: 0, data: goods }) }), app.get('/api/ratings', (req, res) => { res.json({ // 這里是你的json內容 errno: 0, data: ratings }) }) } }, plugins: [ new webpack.DefinePlugin({ 'process.env': require('../config/dev.env') }), new webpack.HotModuleReplacementPlugin(), new webpack.NamedModulesPlugin(), // HMR shows correct file names in console on update. new webpack.NoEmitOnErrorsPlugin(), // https://github.com/ampedandwired/html-webpack-plugin new HtmlWebpackPlugin({ filename: 'index.html', template: 'index.html', inject: true }), // copy custom static assets new CopyWebpackPlugin([{ from: path.resolve(__dirname, '../static'), to: config.dev.assetsSubDirectory, ignore: ['.*'] }]) ] }) module.exports = new Promise((resolve, reject) => { portfinder.basePort = process.env.PORT || config.dev.port portfinder.getPort((err, port) => { if (err) { reject(err) } else { // publish the new Port, necessary for e2e tests process.env.PORT = port // add port to devServer config devWebpackConfig.devServer.port = port // Add FriendlyErrorsPlugin devWebpackConfig.plugins.push(new FriendlyErrorsPlugin({ compilationSuccessInfo: { messages: [`Your application is running here: http://${devWebpackConfig.devServer.host}:${port}`], }, onErrors: config.dev.notifyOnErrors ? utils.createNotifierCallback() : undefined })) resolve(devWebpackConfig) } }) })
沒有啟動ESlint語法檢查的webpack.dev.conf.js的代碼
'use strict' const utils = require('./utils') const webpack = require('webpack') const config = require('../config') const merge = require('webpack-merge') const path = require('path') const baseWebpackConfig = require('./webpack.base.conf') const CopyWebpackPlugin = require('copy-webpack-plugin') const HtmlWebpackPlugin = require('html-webpack-plugin') const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin') const portfinder = require('portfinder') //首先 const express = require('express') const app = express() var appData = require('../data.json') var seller = appData.seller var goods = appData.goods var ratings = appData.ratings var apiRoutes = express.Router() app.use('/api', apiRoutes) const HOST = process.env.HOST const PORT = process.env.PORT && Number(process.env.PORT) const devWebpackConfig = merge(baseWebpackConfig, { module: { rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, usePostCSS: true }) }, // cheap-module-eval-source-map is faster for development devtool: config.dev.devtool, // these devServer options should be customized in /config/index.js devServer: { clientLogLevel: 'warning', historyApiFallback: { rewrites: [ { from: /.*/, to: path.posix.join(config.dev.assetsPublicPath, 'index.html') }, ], }, hot: true, contentBase: false, // since we use CopyWebpackPlugin. compress: true, host: HOST || config.dev.host, port: PORT || config.dev.port, open: config.dev.autoOpenBrowser, overlay: config.dev.errorOverlay ? { warnings: false, errors: true } : false, publicPath: config.dev.assetsPublicPath, proxy: config.dev.proxyTable, quiet: true, // necessary for FriendlyErrorsPlugin watchOptions: { poll: config.dev.poll, }, //找到devServer,添加 before(app) { app.get('/api/seller', (req, res) => { res.json({ // 這里是你的json內容 errno: 0, data: seller }) }), app.get('/api/goods', (req, res) => { res.json({ // 這里是你的json內容 errno: 0, data: goods }) }), app.get('/api/ratings', (req, res) => { res.json({ // 這里是你的json內容 errno: 0, data: ratings }) }) } }, plugins: [ new webpack.DefinePlugin({ 'process.env': require('../config/dev.env') }), new webpack.HotModuleReplacementPlugin(), new webpack.NamedModulesPlugin(), // HMR shows correct file names in console on update. new webpack.NoEmitOnErrorsPlugin(), // https://github.com/ampedandwired/html-webpack-plugin new HtmlWebpackPlugin({ filename: 'index.html', template: 'index.html', inject: true }), // copy custom static assets new CopyWebpackPlugin([ { from: path.resolve(__dirname, '../static'), to: config.dev.assetsSubDirectory, ignore: ['.*'] } ]) ] }) module.exports = new Promise((resolve, reject) => { portfinder.basePort = process.env.PORT || config.dev.port portfinder.getPort((err, port) => { if (err) { reject(err) } else { // publish the new Port, necessary for e2e tests process.env.PORT = port // add port to devServer config devWebpackConfig.devServer.port = port // Add FriendlyErrorsPlugin devWebpackConfig.plugins.push(new FriendlyErrorsPlugin({ compilationSuccessInfo: { messages: [`Your application is running here: http://${devWebpackConfig.devServer.host}:${port}`], }, onErrors: config.dev.notifyOnErrors ? utils.createNotifierCallback() : undefined })) resolve(devWebpackConfig) } }) })
為了更好的查看調取api回來的數據,建議安裝Google的jsonview插件。然后在瀏覽器查看是否成功返回數據,測試:http://localhost:8080/api/goods
此時在項目中導入了一個data.json文件

組件拆分部分(一) 啟動ESlint語法時,對各種格式的檢查到了駭人的地步,多個;和換個行都能報warning!並且js文件的最后還要求換行!注釋還要遵守規范!!
步驟一:導入static文件reset.css
步驟二:刪除默認組件HelloWorld.vue和assets文件夾,修改router中的index.js
import Vue from 'vue' import Router from 'vue-router' import header from '@/components/header/header' Vue.use(Router) export default new Router({ routes: [ { path: '/', name: 'header', component: header } ] })
步驟三:分別配置App.vue和main.js
App.vue <template> <div id="app"> <v-header></v-header> <div class="tab"> 我是導航區塊 </div> <div class="conpent"> 我是內容區塊 </div> </div> </template> <script> import header from './components/header/header.vue' export default { name: 'app', components: { 'v-header': header } } </script> <style scoped> </style>
// The Vue build version to load with the `import` command // (runtime-only or standalone) has been set in webpack.base.conf with an alias. import Vue from 'vue' import App from './App' import router from './router' Vue.config.productionTip = false /* eslint-disable no-new */ new Vue({ el: '#app', router, template: '<App/>', components: { App } })
步驟四:增加一個header.vue文件
<template> <div class="header"> 我是header </div> </template> <script> export default { name: 'v-header' } </script> <style scoped> </style>

組件拆分部分(二)
由於需要用到stylus語法,因此事先得安裝stylus 和 stylus-loader的相關依賴包

步驟一:修改App.vue文件
<template>
<div id="app">
<v-header></v-header>
<div class="tab">
<div class="tab-item">商品</div>
<div class="tab-item">評論</div>
<div class="tab-item">商家</div>
</div>
<div class="conpent">
我是內容區塊
</div>
</div>
</template>
<script>
import header from './components/header/header.vue'
export default {
name: 'app',
components: {
'v-header': header
}
}
</script>
<style scoped lang="stylus" rel="stylesheet/stylus">
#app
.tab
display: flex
width: 100%
height: 40px
line-height: 40px
.tab-item
flex: 1
text-align: center
</style>
配置路由規則
配置路由規則沒什么難度,去官方網站瞧一瞧就行了,這里有個linkActiveClass屬性,挺有用的。下面稍微看一看配置過程
步驟一:創建一個goods.vue文件,其他兩個ratings.vue和seller.vue類似
<template> <div> 我是goods </div> </template> <script> export default { name: 'v-goods' } </script> <style scoped> </style>
步驟二:在index.js文件中配置路由規則
import Vue from 'vue' import Router from 'vue-router' import goods from '../components/goods/goods' import ratings from '../components/ratings/ratings' import seller from '../components/seller/seller' Vue.use(Router) export default new Router({ // 改變路由激活時的class名稱 linkActiveClass: 'active', routes: [ { path: '/', component: goods }, { path: '/goods', component: goods }, { path: '/ratings', component: ratings }, { path: '/seller', component: seller } ] })
另一種路由規則配置
import Vue from 'vue' import Router from 'vue-router' import header from '@/components/header/header' import goods from '@/components/goods/goods' import ratings from '@/components/ratings/ratings' import seller from '@/components/seller/seller' Vue.use(Router) export default new Router({ // 改變路由激活時的class名稱 linkActiveClass: 'active', routes: [ { path: '/', name: 'header', component: header }, { path: '/goods', name: 'goods', component: goods }, { path: '/ratings', name: 'ratings', component: ratings }, { path: '/seller', naem: 'seller', component: seller } ] })
步驟三:配置根組件App.vue
<template>
<div>
<v-header></v-header>
<div class="tab">
<div class="tab-item">
<router-link to="/goods">商品</router-link>
</div>
<div class="tab-item">
<router-link to="/ratings">評價</router-link>
</div>
<div class="tab-item">
<router-link to="/seller">商家</router-link>
</div>
</div>
<router-view></router-view>
</div>
</template>
<script>
import header from './components/header/header.vue'
export default {
name: 'app',
components: {
'v-header': header
}
}
</script>
<style scoped lang="stylus" rel="stylesheet/stylus">
.tab
display: flex
width: 100%
height: 40px
line-height: 40px
.tab-item
flex: 1
text-align: center
& > a
display:block
font-size:16px
color:rgb(77,85,93)
&.active
color:rgb(240,20,20)
</style>

在局域網內,通過手機訪問webapp項目
命令行:ipconfig查詢本機IP => 用IP代替localhost => 將地址復制到草料二維碼的官方網站生成二維碼 => 用微信掃一掃(假如微信掃不了的話,下載一個二維碼掃描來掃描)
【注意】
需要解決ip地址無法訪問vue項目的問題
1、將config文件夾中的index.js文件修改
'use strict' // Template version: 1.3.1 // see http://vuejs-templates.github.io/webpack for documentation. const path = require('path') module.exports = { dev: { // Paths assetsSubDirectory: 'static', assetsPublicPath: '/', proxyTable: {}, // Various Dev Server settings host: '0.0.0.0', // can be overwritten by process.env.HOST port: 8080, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined autoOpenBrowser: false, errorOverlay: true, notifyOnErrors: true, poll: false, // https://webpack.js.org/configuration/dev-server/#devserver-watchoptions- /** * Source Maps */ // https://webpack.js.org/configuration/devtool/#development devtool: 'cheap-module-eval-source-map', // If you have problems debugging vue-files in devtools, // set this to false - it *may* help // https://vue-loader.vuejs.org/en/options.html#cachebusting cacheBusting: true, cssSourceMap: true }, build: { // Template for index.html index: path.resolve(__dirname, '../dist/index.html'), // Paths assetsRoot: path.resolve(__dirname, '../dist'), assetsSubDirectory: 'static', assetsPublicPath: '/', /** * Source Maps */ productionSourceMap: true, // https://webpack.js.org/configuration/devtool/#production devtool: '#source-map', // Gzip off by default as many popular static hosts such as // Surge or Netlify already gzip all static assets for you. // Before setting to `true`, make sure to: // npm install --save-dev compression-webpack-plugin productionGzip: false, productionGzipExtensions: ['js', 'css'], // Run the build command with an extra argument to // View the bundle analyzer report after build finishes: // `npm run build --report` // Set to `true` or `false` to always turn it on or off bundleAnalyzerReport: process.env.npm_config_report } }
2、重啟服務
由於DPR引發的1px問題以及解決方式
步驟一:創建一個mixin.styl文件,作為邊框的公共樣式
border-1px($color)
position: relative
&:after
display: block
position: absolute
left: 0
bottom: 0
width: 100%
border-top: 1px solid $color
content: ' '
步驟二:創建一個base.styl文件,解決1px問題
@media (-webkit-min-device-pixel-ratio: 1.5),(min-device-pixel-ratio: 1.5)
.border-1px
&::after
-webkit-transform: scaleY(0.7)
transform: scaleY(0.7)
@media (-webkit-min-device-pixel-ratio: 2),(min-device-pixel-ratio: 2)
.border-1px
&::after
-webkit-transform: scaleY(0.5)
transform: scaleY(0.5)
步驟三:創建一個index.styl文件,統一導入styl文件
@import "./mixin"
@import "./icon"
@import "./base"
步驟四:修改icon.styl文件,需要更改url路徑,不然會報錯
@font-face font-family: 'sell-icon' src: url('../fonts/sell-icon.eot?2430tu') src: url('../fonts/sell-icon.eot?2430tu#iefix') format('embedded-opentype'), url('../fonts/sell-icon.ttf?2430tu') format('truetype'), url('../fonts/sell-icon.woff?2430tu') format('woff'), url('../fonts/sell-icon.svg?2430tu#sell-icon') format('svg') font-weight: normal font-style: normal [class^="icon-"], [class*=" icon-"] /* use !important to prevent issues with browser extensions that change fonts */ font-family: 'sell-icon' !important speak: none font-style: normal font-weight: normal font-variant: normal text-transform: none line-height: 1 /* Better Font Rendering =========== */ -webkit-font-smoothing: antialiased -moz-osx-font-smoothing: grayscale .icon-add_circle:before content: "\e900" .icon-arrow_lift:before content: "\e901" .icon-check_circle:before content: "\e902" .icon-close:before content: "\e903" .icon-favorite:before content: "\e904" .icon-keyboard_arrow_right:before content: "\e905" .icon-remove_circle_outline:before content: "\e906" .icon-shopping_cart:before content: "\e907" .icon-thumb_down:before content: "\e908" .icon-thumb_up:before content: "\e909" .icon-office:before content: "\e90a"
步驟五:配置router文件夾下的index.js文件
import Vue from 'vue' import Router from 'vue-router' import header from '@/components/header/header' import goods from '@/components/goods/goods' import ratings from '@/components/ratings/ratings' import seller from '@/components/seller/seller' import '@/common/stylus/index.styl' Vue.use(Router) export default new Router({ // 改變路由激活時的class名稱 linkActiveClass: 'active', routes: [ { path: '/', name: 'header', component: header }, { path: '/goods', name: 'goods', component: goods }, { path: '/ratings', name: 'ratings', component: ratings }, { path: '/seller', naem: 'seller', component: seller } ] })
步驟六:在App.vue使用寫好的styl樣式
<template> <div> <v-header></v-header> <div class="tab border-1px"> <div class="tab-item"> <router-link to="/goods">商品</router-link> </div> <div class="tab-item"> <router-link to="/ratings">評價</router-link> </div> <div class="tab-item"> <router-link to="/seller">商家</router-link> </div> </div> <router-view></router-view> </div> </template> <script> import header from './components/header/header.vue' export default { name: 'app', components: { 'v-header': header } } </script> <style scoped lang="stylus" rel="stylesheet/stylus"> @import "./common/stylus/mixin.styl"; .tab display: flex width: 100% height: 40px line-height: 40px /*border-bottom: 1px solid rgba(7, 17, 27, 0.1)*/ border-1px(rgba(7, 17, 27, 0.1)) .tab-item flex: 1 text-align: center & > a display:block font-size:16px color:rgb(77,85,93) &.active color:rgb(240,20,20) </style>
編輯.eslintrc.js的規則
假如在創建項目中出現如下情況

那么就打開.eslintrc.js文件,將其規則忽略掉(設置為0)

清除掉令人抓狂的eslintrc語法規則!
'use strict' const path = require('path') const utils = require('./utils') const config = require('../config') const vueLoaderConfig = require('./vue-loader.conf') function resolve (dir) { return path.join(__dirname, '..', dir) } //const createLintingRule = () => ({ ////test: /\.(js|vue)$/, ////loader: 'eslint-loader', ////enforce: 'pre', ////include: [resolve('src'), resolve('test')], ////options: { //// formatter: require('eslint-friendly-formatter'), //// emitWarning: !config.dev.showEslintErrorsInOverlay ////} //}) module.exports = { context: path.resolve(__dirname, '../'), entry: { app: './src/main.js' }, output: { path: config.build.assetsRoot, filename: '[name].js', publicPath: process.env.NODE_ENV === 'production' ? config.build.assetsPublicPath : config.dev.assetsPublicPath }, resolve: { extensions: ['.js', '.vue', '.json'], alias: { 'vue$': 'vue/dist/vue.esm.js', '@': resolve('src'), } }, module: { rules: [ // ...(config.dev.useEslint ? [createLintingRule()] : []), { test: /\.vue$/, loader: 'vue-loader', options: vueLoaderConfig }, { test: /\.js$/, loader: 'babel-loader', include: [resolve('src'), resolve('test'), resolve('node_modules/webpack-dev-server/client')] }, { test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, loader: 'url-loader', options: { limit: 10000, name: utils.assetsPath('img/[name].[hash:7].[ext]') } }, { test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/, loader: 'url-loader', options: { limit: 10000, name: utils.assetsPath('media/[name].[hash:7].[ext]') } }, { test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, loader: 'url-loader', options: { limit: 10000, name: utils.assetsPath('fonts/[name].[hash:7].[ext]') } } ] }, node: { // prevent webpack from injecting useless setImmediate polyfill because Vue // source contains it (although only uses it if it's native). setImmediate: false, // prevent webpack from injecting mocks to Node native modules // that does not make sense for the client dgram: 'empty', fs: 'empty', net: 'empty', tls: 'empty', child_process: 'empty' } }
使用vue-resource
步驟一:安裝cnpm install vue-resource --save

步驟二:配置router文件夾中的index.js文件
import Vue from 'vue' import Router from 'vue-router' import Resource from 'vue-resource' import header from '@/components/header/header' import goods from '@/components/goods/goods' import ratings from '@/components/ratings/ratings' import seller from '@/components/seller/seller' import '@/common/stylus/index.styl' Vue.use(Router) Vue.use(Resource) export default new Router({ // 改變路由激活時的class名稱 linkActiveClass: 'active', routes: [ { path: '/', name: 'header', component: header }, { path: '/goods', name: 'goods', component: goods }, { path: '/ratings', name: 'ratings', component: ratings }, { path: '/seller', naem: 'seller', component: seller } ] })
步驟三:配置App.vue文件
<template>
<div>
<v-header></v-header>
<div class="tab border-1px">
<div class="tab-item">
<router-link to="/goods">商品</router-link>
</div>
<div class="tab-item">
<router-link to="/ratings">評價</router-link>
</div>
<div class="tab-item">
<router-link to="/seller">商家</router-link>
</div>
</div>
<router-view></router-view>
</div>
</template>
<script>
import header from './components/header/header.vue';
const ERR_OK = 0;
export default {
name: 'app',
data() {
return {
seller: {}
}
},
created() {
this.$http.get('api/seller').then((response) => {
response = response.body;
if(response.error === ERR_OK){
this.seller = response.data;
}
console.log(response.data);
})
},
components: {
'v-header': header
}
}
</script>
<style scoped lang="stylus" rel="stylesheet/stylus">
@import "./common/stylus/mixin.styl";
.tab
display: flex
width: 100%
height: 40px
line-height: 40px
/*border-bottom: 1px solid rgba(7, 17, 27, 0.1)*/
border-1px(rgba(7, 17, 27, 0.1))
.tab-item
flex: 1
text-align: center
& > a
display:block
font-size:16px
color:rgb(77,85,93)
&.active
color:rgb(240,20,20)
</style>
切記
導入stylus樣式表必須在style作如下的定義
<style scoped lang="stylus" rel="stylesheet/stylus"></style>
外部組件(一)
步驟一:修改App.vue,將seller對象的數據傳遞給組件header.vue
<template>
<div>
<v-header :seller="seller"></v-header>
<div class="tab border-1px">
<div class="tab-item">
<router-link to="/goods">商品</router-link>
</div>
<div class="tab-item">
<router-link to="/ratings">評價</router-link>
</div>
<div class="tab-item">
<router-link to="/seller">商家</router-link>
</div>
</div>
<router-view></router-view>
</div>
</template>
<script>
import header from './components/header/header.vue';
const ERR_OK = 0;
export default {
name: 'app',
data() {
return {
seller: {}
}
},
created() {
this.$http.get('api/seller').then((response) => {
response = response.body;
if(response.error === ERR_OK){
this.seller = response.data;
}
console.log(response.data);
this.seller = response.data;
})
},
components: {
'v-header': header
}
}
</script>
<style scoped lang="stylus" rel="stylesheet/stylus">
@import "./common/stylus/mixin.styl";
.tab
display: flex
width: 100%
height: 40px
line-height: 40px
/*border-bottom: 1px solid rgba(7, 17, 27, 0.1)*/
border-1px(rgba(7, 17, 27, 0.1))
.tab-item
flex: 1
text-align: center
& > a
display:block
font-size:16px
color:rgb(77,85,93)
&.active
color:rgb(240,20,20)
</style>
步驟二:編寫header.vue組件
<template>
<div class="header">
<div class="content-wrapper">
<div class="avatar">
<img width="64" height="64" :src="seller.avatar">
</div>
<div class="content">
<div class="title">
<span class="brand"></span>
<span class="name">{{seller.name}}</span>
</div>
<div class="description">
{{seller.description}}/{{seller.deliveryTime}}分鍾送達
</div>
<div v-if="seller.supports" class="support">
<span class="icon"></span>
<span class="text">{{seller.supports[0].description}}</span>
</div>
</div>
</div>
<div class="bulletin-wrapper"></div>
</div>
</template>
<script>
export default {
name: 'v-header',
props: {
seller: {
type: Object
}
}
}
</script>
<style scoped lang="stylus" rel="stylesheet/stylus">
.header
color: #fff
background: #000
.content-wrapper
padding: 24px 12px 18px 24px
.avatar
display: inline-block
</style>
外部組件(二)
步驟一:將所需要的圖片導入到header文件夾中

步驟二:編寫mixin.styl文件,目的是讓程序在不同設備下顯示不同的圖片大小
border-1px($color) position: relative &:after display: block position: absolute left: 0 bottom: 0 width: 100% border-top: 1px solid $color content: ' ' bg-image($url) background-image: url($url + "@2x.png") @media (-webkit-min-device-pixel-ratio: 3),(min-device-pixel-ratio: 3) background-image: url($url + "@3x.png")
步驟三:編寫base.styl文件,目的是統一頁面字體
body, html line-height: 1 font-weight: 200 font-family: 'PingFang SC', 'STHeitiSC-Light', 'Helvetica-Light', arial, sans-serif @media (-webkit-min-device-pixel-ratio: 1.5),(min-device-pixel-ratio: 1.5) .border-1px &::after -webkit-transform: scaleY(0.7) transform: scaleY(0.7) @media (-webkit-min-device-pixel-ratio: 2),(min-device-pixel-ratio: 2) .border-1px &::after -webkit-transform: scaleY(0.5) transform: scaleY(0.5)
步驟四:在header.vue文件中增加樣式
<template>
<div class="header">
<div class="content-wrapper">
<div class="avatar">
<img width="64" height="64" :src="seller.avatar">
</div>
<div class="content">
<div class="title">
<span class="brand"></span>
<span class="name">{{seller.name}}</span>
</div>
<div class="description">
{{seller.description}}/{{seller.deliveryTime}}分鍾送達
</div>
<div v-if="seller.supports" class="support">
<span class="icon"></span>
<span class="text">{{seller.supports[0].description}}</span>
</div>
</div>
</div>
<div class="bulletin-wrapper"></div>
</div>
</template>
<script>
export default {
name: 'v-header',
props: {
seller: {
type: Object
}
}
}
</script>
<style scoped lang="stylus" rel="stylesheet/stylus">
@import "../../common/stylus/mixin";
.header
color: #fff
background: #000
.content-wrapper
padding: 24px 12px 18px 24px
font-size: 0
.avatar
display: inline-block
.content
display: inline-block
margin-left: 16px
font-size: 14px
.title
margin: 2px 0 8px 0
.brand
display: inline-block
vertical-align: top
width: 30px
height:18px
bg-image("brand")
background-size: 30px 18px
background-repeat: no-repeat
.name
margin-left: 6px
font-size: 16px
line-height: 18px
font-weight: bold
</style>
外部組件(三)
繼續編寫header.vue文件,當然相關的圖片也要導入進來。我們可以通過修改<span class="icon" :class="classMap[seller.supports[0].type]"></span>的數值來決定要顯示的圖片,對內容顯示操作也是如此:
<span class="text">{{seller.supports[0].description}}</span>
<template>
<div class="header">
<div class="content-wrapper">
<div class="avatar">
<img width="64" height="64" :src="seller.avatar">
</div>
<div class="content">
<div class="title">
<span class="brand"></span>
<span class="name">{{seller.name}}</span>
</div>
<div class="description">
{{seller.description}}/{{seller.deliveryTime}}分鍾送達
</div>
<div v-if="seller.supports" class="support">
<span class="icon" :class="classMap[seller.supports[0].type]"></span>
<span class="text">{{seller.supports[0].description}}</span>
</div>
</div>
</div>
<div class="bulletin-wrapper"></div>
</div>
</template>
<script>
export default {
name: 'v-header',
props: {
seller: {
type: Object
}
},
created() {
this.classMap = ['decrease','discount','special','invoice','guarantee'];
}
}
</script>
<style scoped lang="stylus" rel="stylesheet/stylus">
@import "../../common/stylus/mixin";
.header
color: #fff
background: #000
.content-wrapper
padding: 24px 12px 18px 24px
font-size: 0
.avatar
display: inline-block
vertical-align: top
img
border-radius: 2px
.content
display: inline-block
margin-left: 16px
.title
margin: 2px 0 8px 0
.brand
display: inline-block
vertical-align: top
width: 30px
height:18px
bg-image("brand")
background-size: 30px 18px
background-repeat: no-repeat
.name
margin-left: 6px
font-size: 16px
line-height: 18px
font-weight: bold
.description
margin-bottom: 10px
line-height: 12px
font-size: 12px
.support
.icon
display: inline-block
vertical-align: top
height: 12px
width: 12px
margin-right: 4px
background-size: 12px 12px
background-repeat: no-repeat
&.decrease
bg-image('decrease_1')
&.discount
bg-image('discount_1')
&.guarantee
bg-image('guarantee_1')
&.invoice
bg-image('invoice_1')
&.special
bg-image('special_1')
.text
font-size: 10px
line-height: 12px
</style>
外部組件(四)
繼續編寫header.vue文件
<template>
<div class="header">
<div class="content-wrapper">
<div class="avatar">
<img width="64" height="64" :src="seller.avatar">
</div>
<div class="content">
<div class="title">
<span class="brand"></span>
<span class="name">{{seller.name}}</span>
</div>
<div class="description">
{{seller.description}}/{{seller.deliveryTime}}分鍾送達
</div>
<div v-if="seller.supports" class="support">
<span class="icon" :class="classMap[seller.supports[0].type]"></span>
<span class="text">{{seller.supports[0].description}}</span>
</div>
</div>
<div class="support-count" v-if="seller.supports">
<span class="count">{{seller.supports.length}}</span>
<i class="icon-keyboard_arrow_right"></i>
</div>
</div>
<div class="bulletin-wrapper"></div>
</div>
</template>
<script>
export default {
name: 'v-header',
props: {
seller: {
type: Object
}
},
created() {
this.classMap = ['decrease','discount','special','invoice','guarantee'];
}
}
</script>
<style scoped lang="stylus" rel="stylesheet/stylus">
@import "../../common/stylus/mixin";
.header
color: #fff
background: #000
.content-wrapper
position: relative
padding: 24px 12px 18px 24px
font-size: 0
.avatar
display: inline-block
vertical-align: top
img
border-radius: 2px
.content
display: inline-block
margin-left: 16px
.title
margin: 2px 0 8px 0
.brand
display: inline-block
vertical-align: top
width: 30px
height:18px
bg-image("brand")
background-size: 30px 18px
background-repeat: no-repeat
.name
margin-left: 6px
font-size: 16px
line-height: 18px
font-weight: bold
.description
margin-bottom: 10px
line-height: 12px
font-size: 12px
.support
.icon
display: inline-block
vertical-align: top
height: 12px
width: 12px
margin-right: 4px
background-size: 12px 12px
background-repeat: no-repeat
&.decrease
bg-image('decrease_1')
&.discount
bg-image('discount_1')
&.guarantee
bg-image('guarantee_1')
&.invoice
bg-image('invoice_1')
&.special
bg-image('special_1')
.text
font-size: 10px
line-height: 12px
.support-count
position: absolute
right: 12px
bottom: 14px
padding: 0 8px
height: 24px
line-height: 24px
border-radius: 14px
background: rgba(0, 0, 0, 0.2)
text-align: center
.count
vertical-align: top
font-size: 10px
.icon-keyboard_arrow_right
margin-left: 2px
line-height: 24px
font-size: 10px
</style>
外部組件(五)
繼續編寫header.vue文件,增加公告部分
<template>
<div class="header">
<div class="content-wrapper">
<div class="avatar">
<img width="64" height="64" :src="seller.avatar">
</div>
<div class="content">
<div class="title">
<span class="brand"></span>
<span class="name">{{seller.name}}</span>
</div>
<div class="description">
{{seller.description}}/{{seller.deliveryTime}}分鍾送達
</div>
<div v-if="seller.supports" class="support">
<span class="icon" :class="classMap[seller.supports[0].type]"></span>
<span class="text">{{seller.supports[0].description}}</span>
</div>
</div>
<div class="support-count" v-if="seller.supports">
<span class="count">{{seller.supports.length}}</span>
<i class="icon-keyboard_arrow_right"></i>
</div>
</div>
<div class="bulletin-wrapper">
<span class="bulletin-title"></span>
<span class="bulletin-text">{{seller.bulletin}}</span>
<i class="icon-keyboard_arrow_right"></i>
</div>
</div>
</template>
<script>
export default {
name: 'v-header',
props: {
seller: {
type: Object
}
},
created() {
this.classMap = ['decrease','discount','special','invoice','guarantee'];
}
}
</script>
<style scoped lang="stylus" rel="stylesheet/stylus">
@import "../../common/stylus/mixin";
.header
color: #fff
background: #000
.content-wrapper
position: relative
padding: 24px 12px 18px 24px
font-size: 0
.avatar
display: inline-block
vertical-align: top
img
border-radius: 2px
.content
display: inline-block
margin-left: 16px
.title
margin: 2px 0 8px 0
.brand
display: inline-block
vertical-align: top
width: 30px
height:18px
bg-image("brand")
background-size: 30px 18px
background-repeat: no-repeat
.name
margin-left: 6px
font-size: 16px
line-height: 18px
font-weight: bold
.description
margin-bottom: 10px
line-height: 12px
font-size: 12px
.support
.icon
display: inline-block
vertical-align: top
height: 12px
width: 12px
margin-right: 4px
background-size: 12px 12px
background-repeat: no-repeat
&.decrease
bg-image('decrease_1')
&.discount
bg-image('discount_1')
&.guarantee
bg-image('guarantee_1')
&.invoice
bg-image('invoice_1')
&.special
bg-image('special_1')
.text
font-size: 10px
line-height: 12px
.support-count
position: absolute
right: 12px
bottom: 14px
padding: 0 8px
height: 24px
line-height: 24px
border-radius: 14px
background: rgba(0, 0, 0, 0.2)
text-align: center
.count
vertical-align: top
font-size: 10px
.icon-keyboard_arrow_right
margin-left: 2px
line-height: 24px
font-size: 10px
.bulletin-wrapper
position: relative
height: 28px
line-height: 28px
padding: 0 22px 0 12px
white-space: nowrap
overflow: hidden
text-overflow: ellipsis
background: rgba(7, 17, 27, 0.2)
.bulletin-title
display: inline-block
vertical-align: top
margin-top: 8px
width: 22px
height: 12px
bg-image('bulletin')
background-size: 22px 12px
background-repeat: no-repeat
.bulletin-text
vertical-align: top
margin: 0 4px
font-size: 10px
.icon-keyboard_arrow_right
position: absolute
font-size: 10px
right: 12px
top: 8px
</style>
外部組件(六)
繼續編寫header.vue文件,增加頂部蒙層效果
<template>
<div class="header">
<div class="content-wrapper">
<div class="avatar">
<img width="64" height="64" :src="seller.avatar">
</div>
<div class="content">
<div class="title">
<span class="brand"></span>
<span class="name">{{seller.name}}</span>
</div>
<div class="description">
{{seller.description}}/{{seller.deliveryTime}}分鍾送達
</div>
<div v-if="seller.supports" class="support">
<span class="icon" :class="classMap[seller.supports[0].type]"></span>
<span class="text">{{seller.supports[0].description}}</span>
</div>
</div>
<div class="support-count" v-if="seller.supports">
<span class="count">{{seller.supports.length}}</span>
<i class="icon-keyboard_arrow_right"></i>
</div>
</div>
<div class="bulletin-wrapper">
<span class="bulletin-title"></span>
<span class="bulletin-text">{{seller.bulletin}}</span>
<i class="icon-keyboard_arrow_right"></i>
</div>
<div class="background">
<img :src="seller.avatar" width="100%" height="100%">
</div>
</div>
</template>
<script>
export default {
name: 'v-header',
props: {
seller: {
type: Object
}
},
created() {
this.classMap = ['decrease','discount','special','invoice','guarantee'];
}
}
</script>
<style scoped lang="stylus" rel="stylesheet/stylus">
@import "../../common/stylus/mixin";
.header
position: relative
overflow: hidden
color: #fff
background: rgba(7,17,27,0.5)
.content-wrapper
position: relative
padding: 24px 12px 18px 24px
font-size: 0
.avatar
display: inline-block
vertical-align: top
img
border-radius: 2px
.content
display: inline-block
margin-left: 16px
.title
margin: 2px 0 8px 0
.brand
display: inline-block
vertical-align: top
width: 30px
height:18px
bg-image("brand")
background-size: 30px 18px
background-repeat: no-repeat
.name
margin-left: 6px
font-size: 16px
line-height: 18px
font-weight: bold
.description
margin-bottom: 10px
line-height: 12px
font-size: 12px
.support
.icon
display: inline-block
vertical-align: top
height: 12px
width: 12px
margin-right: 4px
background-size: 12px 12px
background-repeat: no-repeat
&.decrease
bg-image('decrease_1')
&.discount
bg-image('discount_1')
&.guarantee
bg-image('guarantee_1')
&.invoice
bg-image('invoice_1')
&.special
bg-image('special_1')
.text
font-size: 10px
line-height: 12px
.support-count
position: absolute
right: 12px
bottom: 14px
padding: 0 8px
height: 24px
line-height: 24px
border-radius: 14px
background: rgba(0, 0, 0, 0.2)
text-align: center
.count
vertical-align: top
font-size: 10px
.icon-keyboard_arrow_right
margin-left: 2px
line-height: 24px
font-size: 10px
.bulletin-wrapper
position: relative
height: 28px
line-height: 28px
padding: 0 22px 0 12px
white-space: nowrap
overflow: hidden
text-overflow: ellipsis
background: rgba(7, 17, 27, 0.2)
.bulletin-title
display: inline-block
vertical-align: top
margin-top: 8px
width: 22px
height: 12px
bg-image('bulletin')
background-size: 22px 12px
background-repeat: no-repeat
.bulletin-text
vertical-align: top
margin: 0 4px
font-size: 10px
.icon-keyboard_arrow_right
position: absolute
font-size: 10px
right: 12px
top: 8px
.background
position: absolute
top: 0
left: 0
width: 100%
height: 100%
z-index: -1
/*增加模糊度*/
filter: blur(5px)
</style>
詳情彈層頁(一)
繼續編寫header.vue文件,增加詳情彈層效果
<template>
<div class="header">
<div class="content-wrapper">
<div class="avatar">
<img width="64" height="64" :src="seller.avatar">
</div>
<div class="content">
<div class="title">
<span class="brand"></span>
<span class="name">{{seller.name}}</span>
</div>
<div class="description">
{{seller.description}}/{{seller.deliveryTime}}分鍾送達
</div>
<div v-if="seller.supports" class="support">
<span class="icon" :class="classMap[seller.supports[0].type]"></span>
<span class="text">{{seller.supports[0].description}}</span>
</div>
</div>
<div class="support-count" v-if="seller.supports" @click="showDetail">
<span class="count">{{seller.supports.length}}</span>
<i class="icon-keyboard_arrow_right"></i>
</div>
</div>
<div class="bulletin-wrapper" @click="showDetail">
<span class="bulletin-title"></span>
<span class="bulletin-text">{{seller.bulletin}}</span>
<i class="icon-keyboard_arrow_right"></i>
</div>
<div class="background">
<img :src="seller.avatar" width="100%" height="100%">
</div>
<div class="detail" v-show="detailShow"></div>
</div>
</template>
<script>
export default {
name: 'v-header',
props: {
seller: {
type: Object
}
},
data() {
return {
detailShow: false
};
},
methods: {
showDetail() {
this.detailShow = true;
}
},
created() {
this.classMap = ['decrease','discount','special','invoice','guarantee'];
}
}
</script>
<style scoped lang="stylus" rel="stylesheet/stylus">
@import "../../common/stylus/mixin";
.header
position: relative
overflow: hidden
color: #fff
background: rgba(7,17,27,0.5)
.content-wrapper
position: relative
padding: 24px 12px 18px 24px
font-size: 0
.avatar
display: inline-block
vertical-align: top
img
border-radius: 2px
.content
display: inline-block
margin-left: 16px
.title
margin: 2px 0 8px 0
.brand
display: inline-block
vertical-align: top
width: 30px
height:18px
bg-image("brand")
background-size: 30px 18px
background-repeat: no-repeat
.name
margin-left: 6px
font-size: 16px
line-height: 18px
font-weight: bold
.description
margin-bottom: 10px
line-height: 12px
font-size: 12px
.support
.icon
display: inline-block
vertical-align: top
height: 12px
width: 12px
margin-right: 4px
background-size: 12px 12px
background-repeat: no-repeat
&.decrease
bg-image('decrease_1')
&.discount
bg-image('discount_1')
&.guarantee
bg-image('guarantee_1')
&.invoice
bg-image('invoice_1')
&.special
bg-image('special_1')
.text
font-size: 10px
line-height: 12px
.support-count
position: absolute
right: 12px
bottom: 14px
padding: 0 8px
height: 24px
line-height: 24px
border-radius: 14px
background: rgba(0, 0, 0, 0.2)
text-align: center
.count
vertical-align: top
font-size: 10px
.icon-keyboard_arrow_right
margin-left: 2px
line-height: 24px
font-size: 10px
.bulletin-wrapper
position: relative
height: 28px
line-height: 28px
padding: 0 22px 0 12px
white-space: nowrap
overflow: hidden
text-overflow: ellipsis
background: rgba(7, 17, 27, 0.2)
.bulletin-title
display: inline-block
vertical-align: top
margin-top: 8px
width: 22px
height: 12px
bg-image('bulletin')
background-size: 22px 12px
background-repeat: no-repeat
.bulletin-text
vertical-align: top
margin: 0 4px
font-size: 10px
.icon-keyboard_arrow_right
position: absolute
font-size: 10px
right: 12px
top: 8px
.background
position: absolute
top: 0
left: 0
width: 100%
height: 100%
z-index: -1
/*增加模糊度*/
filter: blur(5px)
.detail
position: fixed
top: 0
left: 0
z-index: 100
height: 100%
width: 100%
overflow: auto
backdrop-filter: blur(10px)
opacity: 1
background: rgba(7, 17, 27, 0.8)
</style>
詳情彈層頁(二)
繼續編寫header.vue文件,設置彈窗關閉按鈕,利用sticky footers知識
<template>
<div class="header">
<div class="content-wrapper">
<div class="avatar">
<img width="64" height="64" :src="seller.avatar">
</div>
<div class="content">
<div class="title">
<span class="brand"></span>
<span class="name">{{seller.name}}</span>
</div>
<div class="description">
{{seller.description}}/{{seller.deliveryTime}}分鍾送達
</div>
<div v-if="seller.supports" class="support">
<span class="icon" :class="classMap[seller.supports[0].type]"></span>
<span class="text">{{seller.supports[0].description}}</span>
</div>
</div>
<div class="support-count" v-if="seller.supports" @click="showDetail">
<span class="count">{{seller.supports.length}}</span>
<i class="icon-keyboard_arrow_right"></i>
</div>
</div>
<div class="bulletin-wrapper" @click="showDetail">
<span class="bulletin-title"></span>
<span class="bulletin-text">{{seller.bulletin}}</span>
<i class="icon-keyboard_arrow_right"></i>
</div>
<div class="background">
<img :src="seller.avatar" width="100%" height="100%">
</div>
<div class="detail" v-show="detailShow">
<div class="detail-wrapper clearfix">
<div class="detail-main"></div>
</div>
<div class="detail-close">
<i class="icon-close"></i>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'v-header',
props: {
seller: {
type: Object
}
},
data() {
return {
detailShow: false
};
},
methods: {
showDetail() {
this.detailShow = true;
}
},
created() {
this.classMap = ['decrease','discount','special','invoice','guarantee'];
}
}
</script>
<style scoped lang="stylus" rel="stylesheet/stylus">
@import "../../common/stylus/mixin";
.header
position: relative
overflow: hidden
color: #fff
background: rgba(7,17,27,0.5)
.content-wrapper
position: relative
padding: 24px 12px 18px 24px
font-size: 0
.avatar
display: inline-block
vertical-align: top
img
border-radius: 2px
.content
display: inline-block
margin-left: 16px
.title
margin: 2px 0 8px 0
.brand
display: inline-block
vertical-align: top
width: 30px
height:18px
bg-image("brand")
background-size: 30px 18px
background-repeat: no-repeat
.name
margin-left: 6px
font-size: 16px
line-height: 18px
font-weight: bold
.description
margin-bottom: 10px
line-height: 12px
font-size: 12px
.support
.icon
display: inline-block
vertical-align: top
height: 12px
width: 12px
margin-right: 4px
background-size: 12px 12px
background-repeat: no-repeat
&.decrease
bg-image('decrease_1')
&.discount
bg-image('discount_1')
&.guarantee
bg-image('guarantee_1')
&.invoice
bg-image('invoice_1')
&.special
bg-image('special_1')
.text
font-size: 10px
line-height: 12px
.support-count
position: absolute
right: 12px
bottom: 14px
padding: 0 8px
height: 24px
line-height: 24px
border-radius: 14px
background: rgba(0, 0, 0, 0.2)
text-align: center
.count
vertical-align: top
font-size: 10px
.icon-keyboard_arrow_right
margin-left: 2px
line-height: 24px
font-size: 10px
.bulletin-wrapper
position: relative
height: 28px
line-height: 28px
padding: 0 22px 0 12px
white-space: nowrap
overflow: hidden
text-overflow: ellipsis
background: rgba(7, 17, 27, 0.2)
.bulletin-title
display: inline-block
vertical-align: top
margin-top: 8px
width: 22px
height: 12px
bg-image('bulletin')
background-size: 22px 12px
background-repeat: no-repeat
.bulletin-text
vertical-align: top
margin: 0 4px
font-size: 10px
.icon-keyboard_arrow_right
position: absolute
font-size: 10px
right: 12px
top: 8px
.background
position: absolute
top: 0
left: 0
width: 100%
height: 100%
z-index: -1
/*增加模糊度*/
filter: blur(5px)
.detail
position: fixed
top: 0
left: 0
z-index: 100
height: 100%
width: 100%
overflow: auto
background: rgba(7, 17, 27, 0.8)
.detail-wrapper
min-height: 100%
width: 100%
.detail-main
margin-top: 64px
padding-bottom: 64px
.detail-close
position: relative
width: 32px
height: 32px
margin: -64px auto 0 auto
clear: both
font-size: 32px
</style>
編寫base.styl文件,設置清除浮動樣式
body, html line-height: 1 font-weight: 200 font-family: 'PingFang SC', 'STHeitiSC-Light', 'Helvetica-Light', arial, sans-serif .clearfix display: inline-block &:after display: block content: "." height: 0 line-height: 0 clear: both visibility: hidden @media (-webkit-min-device-pixel-ratio: 1.5),(min-device-pixel-ratio: 1.5) .border-1px &::after -webkit-transform: scaleY(0.7) transform: scaleY(0.7) @media (-webkit-min-device-pixel-ratio: 2),(min-device-pixel-ratio: 2) .border-1px &::after -webkit-transform: scaleY(0.5) transform: scaleY(0.5)
詳情彈層頁(三、四)
步驟一:新建一個通用的組件star.vue
<template>
<div class="star" :class="starType">
<span v-for="itemClass in itemClasses" :class="itemClass" class="star-item"></span>
</div>
</template>
<script>
const LENGTH = 5;
const CLS_ON = 'on';
const CLS_HALF = 'half';
const CLS_OFF = 'off';
export default {
props: {
size: {
type: Number
},
score: {
type: Number
}
},
computed: {
starType() {
return 'star-' + this.size;
},
itemClasses() {
let result = [];
let score = Math.floor(this.score * 2) / 2;
let hasDecimal = score % 1 !== 0;
let integer = Math.floor(score);
for (let i = 0; i < integer; i++) {
result.push(CLS_ON);
}
if (hasDecimal) {
result.push(CLS_HALF);
}
while (result.length < LENGTH) {
result.push(CLS_OFF);
}
return result;
}
}
};
</script>
<style lang="stylus" rel="stylesheet/stylus">
@import "../../common/stylus/mixin.styl"
.star
font-size: 0
.star-item
display: inline-block
background-repeat: no-repeat
&.star-48
.star-item
width: 20px
height: 20px
margin-right: 22px
background-size: 20px 20px
&:last-child
margin-right: 0
&.on
bg-image('star48_on')
&.half
bg-image('star48_half')
&.off
bg-image('star48_off')
&.star-36
.star-item
width: 15px
height: 15px
margin-right: 6px
background-size: 15px 15px
&:last-child
margin-right: 0
&.on
bg-image('star36_on')
&.half
bg-image('star36_half')
&.off
bg-image('star36_off')
&.star-24
.star-item
width: 10px
height: 10px
margin-right: 3px
background-size: 10px 10px
&:last-child
margin-right: 0
&.on
bg-image('star24_on')
&.half
bg-image('star24_half')
&.off
bg-image('star24_off')
</style>
步驟二:繼續編寫header.vue文件
<template>
<div class="header">
<div class="content-wrapper">
<div class="avatar">
<img width="64" height="64" :src="seller.avatar">
</div>
<div class="content">
<div class="title">
<span class="brand"></span>
<span class="name">{{seller.name}}</span>
</div>
<div class="description">
{{seller.description}}/{{seller.deliveryTime}}分鍾送達
</div>
<div v-if="seller.supports" class="support">
<span class="icon" :class="classMap[seller.supports[0].type]"></span>
<span class="text">{{seller.supports[0].description}}</span>
</div>
</div>
<div class="support-count" v-if="seller.supports" @click="showDetail">
<span class="count">{{seller.supports.length}}</span>
<i class="icon-keyboard_arrow_right"></i>
</div>
</div>
<div class="bulletin-wrapper" @click="showDetail">
<span class="bulletin-title"></span>
<span class="bulletin-text">{{seller.bulletin}}</span>
<i class="icon-keyboard_arrow_right"></i>
</div>
<div class="background">
<img :src="seller.avatar" width="100%" height="100%">
</div>
<div class="detail" v-show="detailShow">
<div class="detail-wrapper clearfix">
<div class="detail-main">
<h1 class="name">{{seller.name}}</h1>
<div class="star-wrapper">
<star :size="36" :score="seller.score"></star>
</div>
</div>
</div>
<div class="detail-close">
<i class="icon-close"></i>
</div>
</div>
</div>
</template>
<script>
import star from '../../components/star/star';
export default {
name: 'v-header',
props: {
seller: {
type: Object
}
},
data() {
return {
detailShow: false
};
},
methods: {
showDetail() {
this.detailShow = true;
}
},
created() {
this.classMap = ['decrease','discount','special','invoice','guarantee'];
},
components: {
star
}
}
</script>
<style scoped lang="stylus" rel="stylesheet/stylus">
@import "../../common/stylus/mixin";
.header
position: relative
overflow: hidden
color: #fff
background: rgba(7,17,27,0.5)
.content-wrapper
position: relative
padding: 24px 12px 18px 24px
font-size: 0
.avatar
display: inline-block
vertical-align: top
img
border-radius: 2px
.content
display: inline-block
margin-left: 16px
.title
margin: 2px 0 8px 0
.brand
display: inline-block
vertical-align: top
width: 30px
height:18px
bg-image("brand")
background-size: 30px 18px
background-repeat: no-repeat
.name
margin-left: 6px
font-size: 16px
line-height: 18px
font-weight: bold
.description
margin-bottom: 10px
line-height: 12px
font-size: 12px
.support
.icon
display: inline-block
vertical-align: top
height: 12px
width: 12px
margin-right: 4px
background-size: 12px 12px
background-repeat: no-repeat
&.decrease
bg-image('decrease_1')
&.discount
bg-image('discount_1')
&.guarantee
bg-image('guarantee_1')
&.invoice
bg-image('invoice_1')
&.special
bg-image('special_1')
.text
font-size: 10px
line-height: 12px
.support-count
position: absolute
right: 12px
bottom: 14px
padding: 0 8px
height: 24px
line-height: 24px
border-radius: 14px
background: rgba(0, 0, 0, 0.2)
text-align: center
.count
vertical-align: top
font-size: 10px
.icon-keyboard_arrow_right
margin-left: 2px
line-height: 24px
font-size: 10px
.bulletin-wrapper
position: relative
height: 28px
line-height: 28px
padding: 0 22px 0 12px
white-space: nowrap
overflow: hidden
text-overflow: ellipsis
background: rgba(7, 17, 27, 0.2)
.bulletin-title
display: inline-block
vertical-align: top
margin-top: 8px
width: 22px
height: 12px
bg-image('bulletin')
background-size: 22px 12px
background-repeat: no-repeat
.bulletin-text
vertical-align: top
margin: 0 4px
font-size: 10px
.icon-keyboard_arrow_right
position: absolute
font-size: 10px
right: 12px
top: 8px
.background
position: absolute
top: 0
left: 0
width: 100%
height: 100%
z-index: -1
/*增加模糊度*/
filter: blur(5px)
.detail
position: fixed
top: 0
left: 0
z-index: 100
height: 100%
width: 100%
overflow: auto
background: rgba(7, 17, 27, 0.8)
.detail-wrapper
min-height: 100%
width: 100%
.detail-main
margin-top: 64px
padding-bottom: 64px
.name
line-height: 16px
text-align: center
font-size: 16px
font-weight: 700
.star-wrapper
margin-top: 18px
padding: 2px 0
text-align: center
.detail-close
position: relative
width: 32px
height: 32px
margin: -64px auto 0 auto
clear: both
font-size: 32px
</style>
詳情彈層頁(五)
繼續編寫header.vue文件,增加響應式的水平線條
<template>
<div class="header">
<div class="content-wrapper">
<div class="avatar">
<img width="64" height="64" :src="seller.avatar">
</div>
<div class="content">
<div class="title">
<span class="brand"></span>
<span class="name">{{seller.name}}</span>
</div>
<div class="description">
{{seller.description}}/{{seller.deliveryTime}}分鍾送達
</div>
<div v-if="seller.supports" class="support">
<span class="icon" :class="classMap[seller.supports[0].type]"></span>
<span class="text">{{seller.supports[0].description}}</span>
</div>
</div>
<div class="support-count" v-if="seller.supports" @click="showDetail">
<span class="count">{{seller.supports.length}}</span>
<i class="icon-keyboard_arrow_right"></i>
</div>
</div>
<div class="bulletin-wrapper" @click="showDetail">
<span class="bulletin-title"></span>
<span class="bulletin-text">{{seller.bulletin}}</span>
<i class="icon-keyboard_arrow_right"></i>
</div>
<div class="background">
<img :src="seller.avatar" width="100%" height="100%">
</div>
<div class="detail" v-show="detailShow">
<div class="detail-wrapper clearfix">
<div class="detail-main">
<h1 class="name">{{seller.name}}</h1>
<div class="star-wrapper">
<star :size="36" :score="seller.score"></star>
</div>
<div class="title">
<div class="line"></div>
<div class="text">優惠信息</div>
<div class="line"></div>
</div>
</div>
</div>
<div class="detail-close">
<i class="icon-close"></i>
</div>
</div>
</div>
</template>
<script>
import star from '../../components/star/star';
export default {
name: 'v-header',
props: {
seller: {
type: Object
}
},
data() {
return {
detailShow: false
};
},
methods: {
showDetail() {
this.detailShow = true;
}
},
created() {
this.classMap = ['decrease','discount','special','invoice','guarantee'];
},
components: {
star
}
}
</script>
<style scoped lang="stylus" rel="stylesheet/stylus">
@import "../../common/stylus/mixin";
.header
position: relative
overflow: hidden
color: #fff
background: rgba(7,17,27,0.5)
.content-wrapper
position: relative
padding: 24px 12px 18px 24px
font-size: 0
.avatar
display: inline-block
vertical-align: top
img
border-radius: 2px
.content
display: inline-block
margin-left: 16px
.title
margin: 2px 0 8px 0
.brand
display: inline-block
vertical-align: top
width: 30px
height:18px
bg-image("brand")
background-size: 30px 18px
background-repeat: no-repeat
.name
margin-left: 6px
font-size: 16px
line-height: 18px
font-weight: bold
.description
margin-bottom: 10px
line-height: 12px
font-size: 12px
.support
.icon
display: inline-block
vertical-align: top
height: 12px
width: 12px
margin-right: 4px
background-size: 12px 12px
background-repeat: no-repeat
&.decrease
bg-image('decrease_1')
&.discount
bg-image('discount_1')
&.guarantee
bg-image('guarantee_1')
&.invoice
bg-image('invoice_1')
&.special
bg-image('special_1')
.text
font-size: 10px
line-height: 12px
.support-count
position: absolute
right: 12px
bottom: 14px
padding: 0 8px
height: 24px
line-height: 24px
border-radius: 14px
background: rgba(0, 0, 0, 0.2)
text-align: center
.count
vertical-align: top
font-size: 10px
.icon-keyboard_arrow_right
margin-left: 2px
line-height: 24px
font-size: 10px
.bulletin-wrapper
position: relative
height: 28px
line-height: 28px
padding: 0 22px 0 12px
white-space: nowrap
overflow: hidden
text-overflow: ellipsis
background: rgba(7, 17, 27, 0.2)
.bulletin-title
display: inline-block
vertical-align: top
margin-top: 8px
width: 22px
height: 12px
bg-image('bulletin')
background-size: 22px 12px
background-repeat: no-repeat
.bulletin-text
vertical-align: top
margin: 0 4px
font-size: 10px
.icon-keyboard_arrow_right
position: absolute
font-size: 10px
right: 12px
top: 8px
.background
position: absolute
top: 0
left: 0
width: 100%
height: 100%
z-index: -1
/*增加模糊度*/
filter: blur(5px)
.detail
position: fixed
top: 0
left: 0
z-index: 100
height: 100%
width: 100%
overflow: auto
background: rgba(7, 17, 27, 0.8)
.detail-wrapper
min-height: 100%
width: 100%
.detail-main
margin-top: 64px
padding-bottom: 64px
.name
line-height: 16px
text-align: center
font-size: 16px
font-weight: 700
.star-wrapper
margin-top: 18px
padding: 2px 0
text-align: center
.title
display: flex
width: 80%
margin: 28px auto 24px auto
.line
flex: 1
position: relative
top: -6px
border-bottom: 1px solid rgba(255, 255, 255, 0.2)
.text
padding: 0 12px
font-weight: 700
font-size: 14px
.detail-close
position: relative
width: 32px
height: 32px
margin: -64px auto 0 auto
clear: both
font-size: 32px
</style>
食品組件布局(一)
創建goods.vue組件,首先編寫食品左側內容
<template>
<div class="goods">
<div class="menu-wrapper">
<ul>
<li v-for="item in goods" class="menu-item">
<span class="text border-1px">
<span v-show="item.type>0" class="icon" :class="classMap[item.type]"></span>
{{item.name}}
</span>
</li>
</ul>
</div>
<div class="foods-wrapper">
</div>
</div>
</template>
<script>
const ERR_OK = 0;
export default {
name: 'v-goods',
props: {
seller: {
type: Object
}
},
data() {
return {
goods: []
};
},
created() {
this.classMap = ['decrease','discount','special','invoice','guarantee'];
this.$http.get('api/goods').then((response) => {
response = response.body;
if(response.error === ERR_OK){
this.goods = response.data;
}
console.log(response.data);
this.goods = response.data;
})
}
}
</script>
<style scoped lang="stylus" rel="stylesheet/stylus">
@import "../../common/stylus/mixin";
.goods
display: flex
position: absolute
top: 174px
bottom: 46px
width: 100%
overflow: hidden
.menu-wrapper
flex: 0 0 80px
width: 80px
background: #f3f5f7
.menu-item
display: table
height: 54px
width: 56px
padding: 0 12px
line-height: 14px
.icon
display: inline-block
vertical-align: top
width: 12px
height: 12px
margin-right: 2px
background-size: 12px 12px
background-repeat: no-repeat
&.decrease
bg-image('decrease_3')
&.discount
bg-image('discount_3')
&.guarantee
bg-image('guarantee_3')
&.invoice
bg-image('invoice_3')
&.special
bg-image('special_3')
.text
display: table-cell
width: 56px
vertical-align: middle
border-1px(rgba(7,17,27,0.1))
font-size: 12px
.foods-wrapper
flex: 1
</style>
食品組件布局(二)
編寫goods.vue組件,增加右側內容
<template>
<div class="goods">
<div class="menu-wrapper">
<ul>
<li v-for="item in goods" class="menu-item">
<span class="text border-1px">
<span v-show="item.type>0" class="icon" :class="classMap[item.type]"></span> {{item.name}}
</span>
</li>
</ul>
</div>
<div class="foods-wrapper">
<ul>
<li v-for="item in goods" class="food-list">
<h1 class="title">{{item.name}}</h1>
<ul>
<li v-for="food in item.foods" class="food-item">
<div class="icon">
<img :src="food.icon" />
</div>
<div class="content">
<h2 class="name">{{food.name}}</h2>
<p class="desc">{{food.description}}</p>
<div class="extra">
<span>月售{{food.sellCount}}份</span>
<span>好評率{{food.rating}}%</span>
</div>
<div class="price">
<span>¥{{food.price}}</span>
<span v-show="food.oldPrice">¥{{food.oldPrice}}</span>
</div>
</div>
</li>
</ul>
</li>
</ul>
</div>
</div>
</template>
<script>
const ERR_OK = 0;
export default {
name: 'v-goods',
props: {
seller: {
type: Object
}
},
data() {
return {
goods: []
};
},
created() {
this.classMap = ['decrease','discount','special','invoice','guarantee'];
this.$http.get('api/goods').then((response) => {
response = response.body;
if(response.error === ERR_OK){
this.goods = response.data;
}
console.log(response.data);
this.goods = response.data;
})
}
}
</script>
<style scoped lang="stylus" rel="stylesheet/stylus">
@import "../../common/stylus/mixin";
.goods
display: flex
position: absolute
top: 174px
bottom: 46px
width: 100%
overflow: hidden
.menu-wrapper
flex: 0 0 80px
width: 80px
background: #f3f5f7
.menu-item
display: table
height: 54px
width: 56px
padding: 0 12px
line-height: 14px
.icon
display: inline-block
vertical-align: top
width: 12px
height: 12px
margin-right: 2px
background-size: 12px 12px
background-repeat: no-repeat
&.decrease
bg-image('decrease_3')
&.discount
bg-image('discount_3')
&.guarantee
bg-image('guarantee_3')
&.invoice
bg-image('invoice_3')
&.special
bg-image('special_3')
.text
display: table-cell
width: 56px
vertical-align: middle
border-1px(rgba(7,17,27,0.1))
font-size: 12px
.foods-wrapper
flex: 1
</style>
食品組件布局(三)
繼續編寫goods.vue組件,增加樣式
<template>
<div class="goods">
<div class="menu-wrapper">
<ul>
<li v-for="item in goods" class="menu-item">
<span class="text border-1px">
<span v-show="item.type>0" class="icon" :class="classMap[item.type]"></span> {{item.name}}
</span>
</li>
</ul>
</div>
<div class="foods-wrapper">
<ul>
<li v-for="item in goods" class="food-list">
<h1 class="title">{{item.name}}</h1>
<ul>
<li v-for="food in item.foods" class="food-item border-1px">
<div class="icon">
<img :src="food.icon" width="57" height="57" />
</div>
<div class="content">
<h2 class="name">{{food.name}}</h2>
<p class="desc">{{food.description}}</p>
<div class="extra">
<span>月售{{food.sellCount}}份</span>
<span>好評率{{food.rating}}%</span>
</div>
<div class="price">
<span>¥{{food.price}}</span>
<span v-show="food.oldPrice">¥{{food.oldPrice}}</span>
</div>
</div>
</li>
</ul>
</li>
</ul>
</div>
</div>
</template>
<script>
const ERR_OK = 0;
export default {
name: 'v-goods',
props: {
seller: {
type: Object
}
},
data() {
return {
goods: []
};
},
created() {
this.classMap = ['decrease','discount','special','invoice','guarantee'];
this.$http.get('api/goods').then((response) => {
response = response.body;
if(response.error === ERR_OK){
this.goods = response.data;
}
console.log(response.data);
this.goods = response.data;
})
}
}
</script>
<style scoped lang="stylus" rel="stylesheet/stylus">
@import "../../common/stylus/mixin";
.goods
display: flex
position: absolute
top: 174px
bottom: 46px
width: 100%
overflow: hidden
.menu-wrapper
flex: 0 0 80px
width: 80px
background: #f3f5f7
.menu-item
display: table
height: 54px
width: 56px
padding: 0 12px
line-height: 14px
.icon
display: inline-block
vertical-align: top
width: 12px
height: 12px
margin-right: 2px
background-size: 12px 12px
background-repeat: no-repeat
&.decrease
bg-image('decrease_3')
&.discount
bg-image('discount_3')
&.guarantee
bg-image('guarantee_3')
&.invoice
bg-image('invoice_3')
&.special
bg-image('special_3')
.text
display: table-cell
width: 56px
vertical-align: middle
border-1px(rgba(7,17,27,0.1))
font-size: 12px
.foods-wrapper
flex: 1
.title
padding-left: 14px
height: 26px
line-height: 26px
border-left: 2px solid #d9dde1
font-size: 12px
color: rgb(147,153,159)
background: #f3f5f7
.food-item
display: flex
margin: 18px
padding-bottom: 18px
border-1px(rgba(7,17,27,0.1))
&:last-child
border-none()
margin-bottom: 0
.icon
flex: 0 0 57px
margin-right: 10px
.content
flex: 1
.name
margin: 2px 0 8px 0
height: 14px
line-height: 14px
font-size: 14px
color: rgb(7,17,27)
</style>
食品組件布局(四)
繼續編寫goods.vue組件,完成樣式部分
<template>
<div class="goods">
<div class="menu-wrapper">
<ul>
<li v-for="item in goods" class="menu-item">
<span class="text border-1px">
<span v-show="item.type>0" class="icon" :class="classMap[item.type]"></span> {{item.name}}
</span>
</li>
</ul>
</div>
<div class="foods-wrapper">
<ul>
<li v-for="item in goods" class="food-list">
<h1 class="title">{{item.name}}</h1>
<ul>
<li v-for="food in item.foods" class="food-item border-1px">
<div class="icon">
<img :src="food.icon" width="57" height="57" />
</div>
<div class="content">
<h2 class="name">{{food.name}}</h2>
<p class="desc">{{food.description}}</p>
<div class="extra">
<span class="count">月售{{food.sellCount}}份</span>
<span>好評率{{food.rating}}%</span>
</div>
<div class="price">
<span class="now">¥{{food.price}}</span>
<span class="old" v-show="food.oldPrice">¥{{food.oldPrice}}</span>
</div>
</div>
</li>
</ul>
</li>
</ul>
</div>
</div>
</template>
<script>
const ERR_OK = 0;
export default {
name: 'v-goods',
props: {
seller: {
type: Object
}
},
data() {
return {
goods: []
};
},
created() {
this.classMap = ['decrease','discount','special','invoice','guarantee'];
this.$http.get('api/goods').then((response) => {
response = response.body;
if(response.error === ERR_OK){
this.goods = response.data;
}
console.log(response.data);
this.goods = response.data;
})
}
}
</script>
<style scoped lang="stylus" rel="stylesheet/stylus">
@import "../../common/stylus/mixin";
.goods
display: flex
position: absolute
top: 174px
bottom: 46px
width: 100%
overflow: hidden
.menu-wrapper
flex: 0 0 80px
width: 80px
background: #f3f5f7
.menu-item
display: table
height: 54px
width: 56px
padding: 0 12px
line-height: 14px
.icon
display: inline-block
vertical-align: top
width: 12px
height: 12px
margin-right: 2px
background-size: 12px 12px
background-repeat: no-repeat
&.decrease
bg-image('decrease_3')
&.discount
bg-image('discount_3')
&.guarantee
bg-image('guarantee_3')
&.invoice
bg-image('invoice_3')
&.special
bg-image('special_3')
.text
display: table-cell
width: 56px
vertical-align: middle
border-1px(rgba(7,17,27,0.1))
font-size: 12px
.foods-wrapper
flex: 1
.title
padding-left: 14px
height: 26px
line-height: 26px
border-left: 2px solid #d9dde1
font-size: 12px
color: rgb(147,153,159)
background: #f3f5f7
.food-item
display: flex
margin: 18px
padding-bottom: 18px
border-1px(rgba(7,17,27,0.1))
&:last-child
border-none()
margin-bottom: 0
.icon
flex: 0 0 57px
margin-right: 10px
.content
flex: 1
.name
margin: 2px 0 8px 0
height: 14px
line-height: 14px
font-size: 14px
color: rgb(7,17,27)
.desc, .extra
line-height: 10px
font-size: 10px
color: rgb(147,153,159)
.desc
margin-bottom: 8px
.extra
&.count
margin-right: 12px
.price
font-weight: 700
line-height: 24px
.now
margin-right: 8px
font-size: 14px
color: rgb(240,20,20)
.old
text-decoration: line-through
font-size: 10px
color: rgb(147,153,159)
</style>
使用better-scroll(一)
插件地址:https://github.com/ustbhuangyi/better-scroll
步驟一:下載安裝cnpm install better-scroll --save

步驟二:編寫goods.vue組件,讓頁面滾動起來
<template>
<div class="goods">
<div class="menu-wrapper" ref="menuWrapper">
<ul>
<li v-for="item in goods" class="menu-item">
<span class="text border-1px">
<span v-show="item.type>0" class="icon" :class="classMap[item.type]"></span> {{item.name}}
</span>
</li>
</ul>
</div>
<div class="foods-wrapper" ref="foodsWrapper">
<ul>
<li v-for="item in goods" class="food-list">
<h1 class="title">{{item.name}}</h1>
<ul>
<li v-for="food in item.foods" class="food-item border-1px">
<div class="icon">
<img :src="food.icon" width="57" height="57" />
</div>
<div class="content">
<h2 class="name">{{food.name}}</h2>
<p class="desc">{{food.description}}</p>
<div class="extra">
<span class="count">月售{{food.sellCount}}份</span>
<span>好評率{{food.rating}}%</span>
</div>
<div class="price">
<span class="now">¥{{food.price}}</span>
<span class="old" v-show="food.oldPrice">¥{{food.oldPrice}}</span>
</div>
</div>
</li>
</ul>
</li>
</ul>
</div>
</div>
</template>
<script>
import BScroll from 'better-scroll'
const ERR_OK = 0;
export default {
name: 'v-goods',
props: {
seller: {
type: Object
}
},
data() {
return {
goods: []
};
},
created() {
this.classMap = ['decrease','discount','special','invoice','guarantee'];
this.$http.get('api/goods').then((response) => {
response = response.body;
if(response.error === ERR_OK){
this.goods = response.data;
}
this.goods = response.data;
this.$nextTick(() => {
this._initScroll();
});
});
},
methods: {
_initScroll() {
this.menuScroll = new BScroll(this.$refs.menuWrapper,{});
this.foodsScroll = new BScroll(this.$refs.foodsWrapper,{});
}
}
}
</script>
<style scoped lang="stylus" rel="stylesheet/stylus">
@import "../../common/stylus/mixin";
.goods
display: flex
position: absolute
top: 174px
bottom: 46px
width: 100%
overflow: hidden
.menu-wrapper
flex: 0 0 80px
width: 80px
background: #f3f5f7
.menu-item
display: table
height: 54px
width: 56px
padding: 0 12px
line-height: 14px
.icon
display: inline-block
vertical-align: top
width: 12px
height: 12px
margin-right: 2px
background-size: 12px 12px
background-repeat: no-repeat
&.decrease
bg-image('decrease_3')
&.discount
bg-image('discount_3')
&.guarantee
bg-image('guarantee_3')
&.invoice
bg-image('invoice_3')
&.special
bg-image('special_3')
.text
display: table-cell
width: 56px
vertical-align: middle
border-1px(rgba(7,17,27,0.1))
font-size: 12px
.foods-wrapper
flex: 1
.title
padding-left: 14px
height: 26px
line-height: 26px
border-left: 2px solid #d9dde1
font-size: 12px
color: rgb(147,153,159)
background: #f3f5f7
.food-item
display: flex
margin: 18px
padding-bottom: 18px
border-1px(rgba(7,17,27,0.1))
&:last-child
border-none()
margin-bottom: 0
.icon
flex: 0 0 57px
margin-right: 10px
.content
flex: 1
.name
margin: 2px 0 8px 0
height: 14px
line-height: 14px
font-size: 14px
color: rgb(7,17,27)
.desc, .extra
line-height: 10px
font-size: 10px
color: rgb(147,153,159)
.desc
margin-bottom: 8px
line-height: 14px
.extra
.count
margin-right: 12px
.price
font-weight: 700
line-height: 24px
.now
margin-right: 8px
font-size: 14px
color: rgb(240,20,20)
.old
text-decoration: line-through
font-size: 10px
color: rgb(147,153,159)
</style>
使用better-scroll(二)
編寫goods.vue組件,監測右側頁面高度
<template>
<div class="goods">
<div class="menu-wrapper" ref="menuWrapper">
<ul>
<li v-for="item in goods" class="menu-item">
<span class="text border-1px">
<span v-show="item.type>0" class="icon" :class="classMap[item.type]"></span> {{item.name}}
</span>
</li>
</ul>
</div>
<div class="foods-wrapper" ref="foodsWrapper">
<ul>
<li v-for="item in goods" class="food-list food-list-hook">
<h1 class="title">{{item.name}}</h1>
<ul>
<li v-for="food in item.foods" class="food-item border-1px">
<div class="icon">
<img :src="food.icon" width="57" height="57" />
</div>
<div class="content">
<h2 class="name">{{food.name}}</h2>
<p class="desc">{{food.description}}</p>
<div class="extra">
<span class="count">月售{{food.sellCount}}份</span>
<span>好評率{{food.rating}}%</span>
</div>
<div class="price">
<span class="now">¥{{food.price}}</span>
<span class="old" v-show="food.oldPrice">¥{{food.oldPrice}}</span>
</div>
</div>
</li>
</ul>
</li>
</ul>
</div>
</div>
</template>
<script>
import BScroll from 'better-scroll'
const ERR_OK = 0;
export default {
name: 'v-goods',
props: {
seller: {
type: Object
}
},
data() {
return {
goods: [],
listHeight: [],
scrollY: 0
};
},
computed: {
currentIndex() {
for(let i=0;i<this.listHeight.length;i++){
let height1 = this.listHeight[i];
let height2 = this.listHeight[i+1];
if (!height2 || (this.scrollY > height1 && this.scrollY <height2)){
return i;
}
}
return 0;
}
},
created() {
this.classMap = ['decrease','discount','special','invoice','guarantee'];
this.$http.get('api/goods').then((response) => {
response = response.body;
if(response.error === ERR_OK){
this.goods = response.data;
}
this.goods = response.data;
this.$nextTick(() => {
this._initScroll();
this._calculateHeight();
});
});
},
methods: {
_initScroll() {
this.menuScroll = new BScroll(this.$refs.menuWrapper,{});
this.foodsScroll = new BScroll(this.$refs.foodsWrapper,{
probeType: 3
});
this.foodsScroll.on("scroll",(pos) => {
this.scrollY = Math.abs(Math.round(pos.y));
});
},
_calculateHeight() {
let foodList = this.$refs.foodsWrapper.getElementsByClassName('food-list-hook');
let height = 0;
this.listHeight.push(height);
for(let i=0;i<foodList.length;i++){
let item = foodList[i];
height += item.clientHeight;
this.listHeight.push(height);
}
}
}
}
</script>
<style scoped lang="stylus" rel="stylesheet/stylus">
@import "../../common/stylus/mixin";
.goods
display: flex
position: absolute
top: 174px
bottom: 46px
width: 100%
overflow: hidden
.menu-wrapper
flex: 0 0 80px
width: 80px
background: #f3f5f7
.menu-item
display: table
height: 54px
width: 56px
padding: 0 12px
line-height: 14px
.icon
display: inline-block
vertical-align: top
width: 12px
height: 12px
margin-right: 2px
background-size: 12px 12px
background-repeat: no-repeat
&.decrease
bg-image('decrease_3')
&.discount
bg-image('discount_3')
&.guarantee
bg-image('guarantee_3')
&.invoice
bg-image('invoice_3')
&.special
bg-image('special_3')
.text
display: table-cell
width: 56px
vertical-align: middle
border-1px(rgba(7,17,27,0.1))
font-size: 12px
.foods-wrapper
flex: 1
.title
padding-left: 14px
height: 26px
line-height: 26px
border-left: 2px solid #d9dde1
font-size: 12px
color: rgb(147,153,159)
background: #f3f5f7
.food-item
display: flex
margin: 18px
padding-bottom: 18px
border-1px(rgba(7,17,27,0.1))
&:last-child
border-none()
margin-bottom: 0
.icon
flex: 0 0 57px
margin-right: 10px
.content
flex: 1
.name
margin: 2px 0 8px 0
height: 14px
line-height: 14px
font-size: 14px
color: rgb(7,17,27)
.desc, .extra
line-height: 10px
font-size: 10px
color: rgb(147,153,159)
.desc
margin-bottom: 8px
line-height: 14px
.extra
.count
margin-right: 12px
.price
font-weight: 700
line-height: 24px
.now
margin-right: 8px
font-size: 14px
color: rgb(240,20,20)
.old
text-decoration: line-through
font-size: 10px
color: rgb(147,153,159)
</style>
使用better-scroll(三)
編寫goods.vue組件,實現右側滾動左邊實時發生變化
<template>
<div class="goods">
<div class="menu-wrapper" ref="menuWrapper">
<ul>
<li v-for="(item,index) in goods" class="menu-item" :class="{'current':currentIndex===index}">
<span class="text border-1px">
<span v-show="item.type>0" class="icon" :class="classMap[item.type]"></span> {{item.name}}
</span>
</li>
</ul>
</div>
<div class="foods-wrapper" ref="foodsWrapper">
<ul>
<li v-for="item in goods" class="food-list food-list-hook">
<h1 class="title">{{item.name}}</h1>
<ul>
<li v-for="food in item.foods" class="food-item border-1px">
<div class="icon">
<img :src="food.icon" width="57" height="57" />
</div>
<div class="content">
<h2 class="name">{{food.name}}</h2>
<p class="desc">{{food.description}}</p>
<div class="extra">
<span class="count">月售{{food.sellCount}}份</span>
<span>好評率{{food.rating}}%</span>
</div>
<div class="price">
<span class="now">¥{{food.price}}</span>
<span class="old" v-show="food.oldPrice">¥{{food.oldPrice}}</span>
</div>
</div>
</li>
</ul>
</li>
</ul>
</div>
</div>
</template>
<script>
import BScroll from 'better-scroll'
const ERR_OK = 0;
export default {
name: 'v-goods',
props: {
seller: {
type: Object
}
},
data() {
return {
goods: [],
listHeight: [],
scrollY: 0
};
},
computed: {
currentIndex() {
for(let i=0;i<this.listHeight.length;i++){
let height1 = this.listHeight[i];
let height2 = this.listHeight[i+1];
if (!height2 || (this.scrollY >= height1 && this.scrollY < height2)){
return i;
}
}
return 0;
}
},
created() {
this.classMap = ['decrease','discount','special','invoice','guarantee'];
this.$http.get('api/goods').then((response) => {
response = response.body;
if(response.error === ERR_OK){
this.goods = response.data;
}
this.goods = response.data;
this.$nextTick(() => {
this._initScroll();
this._calculateHeight();
});
});
},
methods: {
_initScroll() {
this.menuScroll = new BScroll(this.$refs.menuWrapper,{});
this.foodsScroll = new BScroll(this.$refs.foodsWrapper,{
probeType: 3
});
this.foodsScroll.on("scroll",(pos) => {
this.scrollY = Math.abs(Math.round(pos.y));
});
},
_calculateHeight() {
let foodList = this.$refs.foodsWrapper.getElementsByClassName('food-list-hook');
let height = 0;
this.listHeight.push(height);
for(let i=0;i<foodList.length;i++){
let item = foodList[i];
height += item.clientHeight;
this.listHeight.push(height);
}
}
}
}
</script>
<style scoped lang="stylus" rel="stylesheet/stylus">
@import "../../common/stylus/mixin";
.goods
display: flex
position: absolute
top: 174px
bottom: 46px
width: 100%
overflow: hidden
.menu-wrapper
flex: 0 0 80px
width: 80px
background: #f3f5f7
.menu-item
display: table
height: 54px
width: 56px
padding: 0 12px
line-height: 14px
&.current
position: relative
z-index: 10
margin-top: -1px
background: #ccc
font-weight: 700
.text
border-none()
.icon
display: inline-block
vertical-align: top
width: 12px
height: 12px
margin-right: 2px
background-size: 12px 12px
background-repeat: no-repeat
&.decrease
bg-image('decrease_3')
&.discount
bg-image('discount_3')
&.guarantee
bg-image('guarantee_3')
&.invoice
bg-image('invoice_3')
&.special
bg-image('special_3')
.text
display: table-cell
width: 56px
vertical-align: middle
border-1px(rgba(7,17,27,0.1))
font-size: 12px
.foods-wrapper
flex: 1
.title
padding-left: 14px
height: 26px
line-height: 26px
border-left: 2px solid #d9dde1
font-size: 12px
color: rgb(147,153,159)
background: #f3f5f7
.food-item
display: flex
margin: 18px
padding-bottom: 18px
border-1px(rgba(7,17,27,0.1))
&:last-child
border-none()
margin-bottom: 0
.icon
flex: 0 0 57px
margin-right: 10px
.content
flex: 1
.name
margin: 2px 0 8px 0
height: 14px
line-height: 14px
font-size: 14px
color: rgb(7,17,27)
.desc, .extra
line-height: 10px
font-size: 10px
color: rgb(147,153,159)
.desc
margin-bottom: 8px
line-height: 14px
.extra
.count
margin-right: 12px
.price
font-weight: 700
line-height: 24px
.now
margin-right: 8px
font-size: 14px
color: rgb(240,20,20)
.old
text-decoration: line-through
font-size: 10px
color: rgb(147,153,159)
</style>
使用better-scroll(四)
編寫goods.vue組價,完全實現左右聯動
<template>
<div class="goods">
<div class="menu-wrapper" ref="menuWrapper">
<ul>
<li v-for="(item,index) in goods" class="menu-item" :class="{'current':currentIndex===index}" @click="selectMenu(index,$event)">
<span class="text border-1px">
<span v-show="item.type>0" class="icon" :class="classMap[item.type]"></span> {{item.name}}
</span>
</li>
</ul>
</div>
<div class="foods-wrapper" ref="foodsWrapper">
<ul>
<li v-for="item in goods" class="food-list food-list-hook">
<h1 class="title">{{item.name}}</h1>
<ul>
<li v-for="food in item.foods" class="food-item border-1px">
<div class="icon">
<img :src="food.icon" width="57" height="57" />
</div>
<div class="content">
<h2 class="name">{{food.name}}</h2>
<p class="desc">{{food.description}}</p>
<div class="extra">
<span class="count">月售{{food.sellCount}}份</span>
<span>好評率{{food.rating}}%</span>
</div>
<div class="price">
<span class="now">¥{{food.price}}</span>
<span class="old" v-show="food.oldPrice">¥{{food.oldPrice}}</span>
</div>
</div>
</li>
</ul>
</li>
</ul>
</div>
</div>
</template>
<script>
import BScroll from 'better-scroll'
const ERR_OK = 0;
export default {
name: 'v-goods',
props: {
seller: {
type: Object
}
},
data() {
return {
goods: [],
listHeight: [],
scrollY: 0
};
},
computed: {
currentIndex() {
for(let i=0;i<this.listHeight.length;i++){
let height1 = this.listHeight[i];
let height2 = this.listHeight[i+1];
if (!height2 || (this.scrollY >= height1 && this.scrollY < height2)){
return i;
}
}
return 0;
}
},
created() {
this.classMap = ['decrease','discount','special','invoice','guarantee'];
this.$http.get('api/goods').then((response) => {
response = response.body;
if(response.error === ERR_OK){
this.goods = response.data;
}
this.goods = response.data;
this.$nextTick(() => {
this._initScroll();
this._calculateHeight();
});
});
},
methods: {
selectMenu(index,event){
if(!event._constructed){
return;
}
let foodList = this.$refs.foodsWrapper.getElementsByClassName('food-list-hook');
let el = foodList[index];
this.foodsScroll.scrollToElement(el,300);
console.log(index);
},
_initScroll() {
this.menuScroll = new BScroll(this.$refs.menuWrapper,{
click: true
});
this.foodsScroll = new BScroll(this.$refs.foodsWrapper,{
click: true,
probeType: 3
});
this.foodsScroll.on("scroll",(pos) => {
this.scrollY = Math.abs(Math.round(pos.y));
});
},
_calculateHeight() {
let foodList = this.$refs.foodsWrapper.getElementsByClassName('food-list-hook');
let height = 0;
this.listHeight.push(height);
for(let i=0;i<foodList.length;i++){
let item = foodList[i];
height += item.clientHeight;
this.listHeight.push(height);
}
}
}
}
</script>
<style scoped lang="stylus" rel="stylesheet/stylus">
@import "../../common/stylus/mixin";
.goods
display: flex
position: absolute
top: 174px
bottom: 46px
width: 100%
overflow: hidden
.menu-wrapper
flex: 0 0 80px
width: 80px
background: #f3f5f7
.menu-item
display: table
height: 54px
width: 56px
padding: 0 12px
line-height: 14px
&.current
position: relative
z-index: 10
margin-top: -1px
background: #ccc
font-weight: 700
.text
border-none()
.icon
display: inline-block
vertical-align: top
width: 12px
height: 12px
margin-right: 2px
background-size: 12px 12px
background-repeat: no-repeat
&.decrease
bg-image('decrease_3')
&.discount
bg-image('discount_3')
&.guarantee
bg-image('guarantee_3')
&.invoice
bg-image('invoice_3')
&.special
bg-image('special_3')
.text
display: table-cell
width: 56px
vertical-align: middle
border-1px(rgba(7,17,27,0.1))
font-size: 12px
.foods-wrapper
flex: 1
.title
padding-left: 14px
height: 26px
line-height: 26px
border-left: 2px solid #d9dde1
font-size: 12px
color: rgb(147,153,159)
background: #f3f5f7
.food-item
display: flex
margin: 18px
padding-bottom: 18px
border-1px(rgba(7,17,27,0.1))
&:last-child
border-none()
margin-bottom: 0
.icon
flex: 0 0 57px
margin-right: 10px
.content
flex: 1
.name
margin: 2px 0 8px 0
height: 14px
line-height: 14px
font-size: 14px
color: rgb(7,17,27)
.desc, .extra
line-height: 10px
font-size: 10px
color: rgb(147,153,159)
.desc
margin-bottom: 8px
line-height: 14px
.extra
.count
margin-right: 12px
.price
font-weight: 700
line-height: 24px
.now
margin-right: 8px
font-size: 14px
color: rgb(240,20,20)
.old
text-decoration: line-through
font-size: 10px
color: rgb(147,153,159)
</style>
購物車組件(一)
步驟一:創建一個shopcart.vue組件,編寫好基礎的樣式
<template>
<div class="shopcart">
<div class="content">
<div class="content-left">
<div class="logo-wrapper">
<div class="logo">
<span class="icon-shopping_cart"></span>
</div>
</div>
<div class="price"></div>
<div class="desc"></div>
</div>
<div class="content-right"></div>
</div>
</div>
</template>
<script>
export default {
name: 'v-goods'
}
</script>
<style scoped lang="stylus" rel="stylesheet/stylus">
.shopcart
position: fixed
left: 0
bottom: 0
z-index: 50
width: 100%
height: 48px
background: #000
</style>
步驟二:配置好goods.vue組件
<template>
<div class="goods">
<div class="menu-wrapper" ref="menuWrapper">
<ul>
<li v-for="(item,index) in goods" class="menu-item" :class="{'current':currentIndex===index}" @click="selectMenu(index,$event)">
<span class="text border-1px">
<span v-show="item.type>0" class="icon" :class="classMap[item.type]"></span> {{item.name}}
</span>
</li>
</ul>
</div>
<div class="foods-wrapper" ref="foodsWrapper">
<ul>
<li v-for="item in goods" class="food-list food-list-hook">
<h1 class="title">{{item.name}}</h1>
<ul>
<li v-for="food in item.foods" class="food-item border-1px">
<div class="icon">
<img :src="food.icon" width="57" height="57" />
</div>
<div class="content">
<h2 class="name">{{food.name}}</h2>
<p class="desc">{{food.description}}</p>
<div class="extra">
<span class="count">月售{{food.sellCount}}份</span>
<span>好評率{{food.rating}}%</span>
</div>
<div class="price">
<span class="now">¥{{food.price}}</span>
<span class="old" v-show="food.oldPrice">¥{{food.oldPrice}}</span>
</div>
</div>
</li>
</ul>
</li>
</ul>
</div>
<shopcart></shopcart>
</div>
</template>
<script>
import BScroll from 'better-scroll';
import shopcart from '../../components/shopcart/shopcart';
const ERR_OK = 0;
export default {
name: 'v-goods',
props: {
seller: {
type: Object
}
},
data() {
return {
goods: [],
listHeight: [],
scrollY: 0
};
},
computed: {
currentIndex() {
for(let i=0;i<this.listHeight.length;i++){
let height1 = this.listHeight[i];
let height2 = this.listHeight[i+1];
if (!height2 || (this.scrollY >= height1 && this.scrollY < height2)){
return i;
}
}
return 0;
}
},
created() {
this.classMap = ['decrease','discount','special','invoice','guarantee'];
this.$http.get('api/goods').then((response) => {
response = response.body;
if(response.error === ERR_OK){
this.goods = response.data;
}
this.goods = response.data;
this.$nextTick(() => {
this._initScroll();
this._calculateHeight();
});
});
},
methods: {
selectMenu(index,event){
if(!event._constructed){
return;
}
let foodList = this.$refs.foodsWrapper.getElementsByClassName('food-list-hook');
let el = foodList[index];
this.foodsScroll.scrollToElement(el,300);
console.log(index);
},
_initScroll() {
this.menuScroll = new BScroll(this.$refs.menuWrapper,{
click: true
});
this.foodsScroll = new BScroll(this.$refs.foodsWrapper,{
click: true,
probeType: 3
});
this.foodsScroll.on("scroll",(pos) => {
this.scrollY = Math.abs(Math.round(pos.y));
});
},
_calculateHeight() {
let foodList = this.$refs.foodsWrapper.getElementsByClassName('food-list-hook');
let height = 0;
this.listHeight.push(height);
for(let i=0;i<foodList.length;i++){
let item = foodList[i];
height += item.clientHeight;
this.listHeight.push(height);
}
}
},
components: {
shopcart
}
}
</script>
<style scoped lang="stylus" rel="stylesheet/stylus">
@import "../../common/stylus/mixin";
.goods
display: flex
position: absolute
top: 174px
bottom: 46px
width: 100%
overflow: hidden
.menu-wrapper
flex: 0 0 80px
width: 80px
background: #f3f5f7
.menu-item
display: table
height: 54px
width: 56px
padding: 0 12px
line-height: 14px
&.current
position: relative
z-index: 10
margin-top: -1px
background: #ccc
font-weight: 700
.text
border-none()
.icon
display: inline-block
vertical-align: top
width: 12px
height: 12px
margin-right: 2px
background-size: 12px 12px
background-repeat: no-repeat
&.decrease
bg-image('decrease_3')
&.discount
bg-image('discount_3')
&.guarantee
bg-image('guarantee_3')
&.invoice
bg-image('invoice_3')
&.special
bg-image('special_3')
.text
display: table-cell
width: 56px
vertical-align: middle
border-1px(rgba(7,17,27,0.1))
font-size: 12px
.foods-wrapper
flex: 1
.title
padding-left: 14px
height: 26px
line-height: 26px
border-left: 2px solid #d9dde1
font-size: 12px
color: rgb(147,153,159)
background: #f3f5f7
.food-item
display: flex
margin: 18px
padding-bottom: 18px
border-1px(rgba(7,17,27,0.1))
&:last-child
border-none()
margin-bottom: 0
.icon
flex: 0 0 57px
margin-right: 10px
.content
flex: 1
.name
margin: 2px 0 8px 0
height: 14px
line-height: 14px
font-size: 14px
color: rgb(7,17,27)
.desc, .extra
line-height: 10px
font-size: 10px
color: rgb(147,153,159)
.desc
margin-bottom: 8px
line-height: 14px
.extra
.count
margin-right: 12px
.price
font-weight: 700
line-height: 24px
.now
margin-right: 8px
font-size: 14px
color: rgb(240,20,20)
.old
text-decoration: line-through
font-size: 10px
color: rgb(147,153,159)
</style>
購物車組件(二)
繼續編寫shopcart.vue組件,增加樣式
<template>
<div class="shopcart">
<div class="content">
<div class="content-left">
<div class="logo-wrapper">
<div class="logo">
<span class="icon-shopping_cart"></span>
</div>
</div>
<div class="price"></div>
<div class="desc"></div>
</div>
<div class="content-right"></div>
</div>
</div>
</template>
<script>
export default {
name: 'v-goods'
}
</script>
<style scoped lang="stylus" rel="stylesheet/stylus">
.shopcart
position: fixed
left: 0
bottom: 0
z-index: 50
width: 100%
height: 48px
.content
display: flex
background: #141d27
font-size: 0
.content-left
flex: 1
.logo-wrapper
display: inline-block
position: relative
top:-10px
margin: 0 12px
padding: 6px
width: 56px
height: 56px
vertical-align: top
box-sizing: border-box
border-radius: 50%
background: #141d27
.logo
width: 100%
height: 100%
border-radius: 50%
text-align: center
background: #2b343c
.icon-shopping_cart
font-size: 24px
line-height: 44px
color: #80858a
.price
display: inline-block
vertical-align: top
margin-top: 12px
line-height: 24px
padding-right: 12px
box-sizing: border-box
border-right: 1px solid rgba(255,255,255,0.1)
font-size: 16px
font-weight: 700
.desc
display: inline-block
.content-right
flex: 0 0 105px
width: 105px
</style>
購物車組件(三)
步驟一:修改App.vue根組價,傳遞值
<template>
<div>
<v-header :seller="seller"></v-header>
<div class="tab border-1px">
<div class="tab-item">
<router-link to="/goods">商品</router-link>
</div>
<div class="tab-item">
<router-link to="/ratings">評價</router-link>
</div>
<div class="tab-item">
<router-link to="/seller">商家</router-link>
</div>
</div>
<router-view :seller="seller"></router-view>
</div>
</template>
<script>
import header from './components/header/header.vue';
const ERR_OK = 0;
export default {
name: 'app',
data() {
return {
seller: {}
}
},
created() {
this.$http.get('api/seller').then((response) => {
response = response.body;
if(response.error === ERR_OK){
this.seller = response.data;
}
this.seller = response.data;
})
},
components: {
'v-header': header
}
}
</script>
<style scoped lang="stylus" rel="stylesheet/stylus">
@import "./common/stylus/mixin.styl";
.tab
display: flex
width: 100%
height: 40px
line-height: 40px
/*border-bottom: 1px solid rgba(7, 17, 27, 0.1)*/
border-1px(rgba(7, 17, 27, 0.1))
.tab-item
flex: 1
text-align: center
& > a
display:block
font-size:16px
color:rgb(77,85,93)
&.active
color:rgb(240,20,20)
</style>
步驟二:修改goods.vue組件,傳遞值
<template>
<div class="goods">
<div class="menu-wrapper" ref="menuWrapper">
<ul>
<li v-for="(item,index) in goods" class="menu-item" :class="{'current':currentIndex===index}" @click="selectMenu(index,$event)">
<span class="text border-1px">
<span v-show="item.type>0" class="icon" :class="classMap[item.type]"></span> {{item.name}}
</span>
</li>
</ul>
</div>
<div class="foods-wrapper" ref="foodsWrapper">
<ul>
<li v-for="item in goods" class="food-list food-list-hook">
<h1 class="title">{{item.name}}</h1>
<ul>
<li v-for="food in item.foods" class="food-item border-1px">
<div class="icon">
<img :src="food.icon" width="57" height="57" />
</div>
<div class="content">
<h2 class="name">{{food.name}}</h2>
<p class="desc">{{food.description}}</p>
<div class="extra">
<span class="count">月售{{food.sellCount}}份</span>
<span>好評率{{food.rating}}%</span>
</div>
<div class="price">
<span class="now">¥{{food.price}}</span>
<span class="old" v-show="food.oldPrice">¥{{food.oldPrice}}</span>
</div>
</div>
</li>
</ul>
</li>
</ul>
</div>
<shopcart :delivery-price="seller.deliveryPrice" :min-price="seller.minPrice"></shopcart>
</div>
</template>
<script>
import BScroll from 'better-scroll';
import shopcart from '../../components/shopcart/shopcart';
const ERR_OK = 0;
export default {
name: 'v-goods',
props: {
seller: {
type: Object
}
},
data() {
return {
goods: [],
listHeight: [],
scrollY: 0
};
},
computed: {
currentIndex() {
for(let i=0;i<this.listHeight.length;i++){
let height1 = this.listHeight[i];
let height2 = this.listHeight[i+1];
if (!height2 || (this.scrollY >= height1 && this.scrollY < height2)){
return i;
}
}
return 0;
}
},
created() {
this.classMap = ['decrease','discount','special','invoice','guarantee'];
this.$http.get('api/goods').then((response) => {
response = response.body;
if(response.error === ERR_OK){
this.goods = response.data;
}
this.goods = response.data;
this.$nextTick(() => {
this._initScroll();
this._calculateHeight();
});
});
},
methods: {
selectMenu(index,event){
if(!event._constructed){
return;
}
let foodList = this.$refs.foodsWrapper.getElementsByClassName('food-list-hook');
let el = foodList[index];
this.foodsScroll.scrollToElement(el,300);
console.log(index);
},
_initScroll() {
this.menuScroll = new BScroll(this.$refs.menuWrapper,{
click: true
});
this.foodsScroll = new BScroll(this.$refs.foodsWrapper,{
click: true,
probeType: 3
});
this.foodsScroll.on("scroll",(pos) => {
this.scrollY = Math.abs(Math.round(pos.y));
});
},
_calculateHeight() {
let foodList = this.$refs.foodsWrapper.getElementsByClassName('food-list-hook');
let height = 0;
this.listHeight.push(height);
for(let i=0;i<foodList.length;i++){
let item = foodList[i];
height += item.clientHeight;
this.listHeight.push(height);
}
}
},
components: {
shopcart
}
}
</script>
<style scoped lang="stylus" rel="stylesheet/stylus">
@import "../../common/stylus/mixin";
.goods
display: flex
position: absolute
top: 174px
bottom: 46px
width: 100%
overflow: hidden
.menu-wrapper
flex: 0 0 80px
width: 80px
background: #f3f5f7
.menu-item
display: table
height: 54px
width: 56px
padding: 0 12px
line-height: 14px
&.current
position: relative
z-index: 10
margin-top: -1px
background: #ccc
font-weight: 700
.text
border-none()
.icon
display: inline-block
vertical-align: top
width: 12px
height: 12px
margin-right: 2px
background-size: 12px 12px
background-repeat: no-repeat
&.decrease
bg-image('decrease_3')
&.discount
bg-image('discount_3')
&.guarantee
bg-image('guarantee_3')
&.invoice
bg-image('invoice_3')
&.special
bg-image('special_3')
.text
display: table-cell
width: 56px
vertical-align: middle
border-1px(rgba(7,17,27,0.1))
font-size: 12px
.foods-wrapper
flex: 1
.title
padding-left: 14px
height: 26px
line-height: 26px
border-left: 2px solid #d9dde1
font-size: 12px
color: rgb(147,153,159)
background: #f3f5f7
.food-item
display: flex
margin: 18px
padding-bottom: 18px
border-1px(rgba(7,17,27,0.1))
&:last-child
border-none()
margin-bottom: 0
.icon
flex: 0 0 57px
margin-right: 10px
.content
flex: 1
.name
margin: 2px 0 8px 0
height: 14px
line-height: 14px
font-size: 14px
color: rgb(7,17,27)
.desc, .extra
line-height: 10px
font-size: 10px
color: rgb(147,153,159)
.desc
margin-bottom: 8px
line-height: 14px
.extra
.count
margin-right: 12px
.price
font-weight: 700
line-height: 24px
.now
margin-right: 8px
font-size: 14px
color: rgb(240,20,20)
.old
text-decoration: line-through
font-size: 10px
color: rgb(147,153,159)
</style>
步驟三:編寫shopcart.vue組件,接收值並增加樣式
<template>
<div class="shopcart">
<div class="content">
<div class="content-left">
<div class="logo-wrapper">
<div class="logo">
<span class="icon-shopping_cart"></span>
</div>
</div>
<div class="price">0元</div>
<div class="desc">另需配送費¥{{deliveryPrice}}元</div>
</div>
<div class="content-right"></div>
</div>
</div>
</template>
<script>
export default {
name: 'v-shopcart',
props: {
deliveryPrice: {
type: Number,
default: 0
},
minPrice: {
type: Number,
default: 0
}
}
}
</script>
<style scoped lang="stylus" rel="stylesheet/stylus">
.shopcart
position: fixed
left: 0
bottom: 0
z-index: 50
width: 100%
height: 48px
.content
display: flex
background: #141d27
font-size: 0
.content-left
flex: 1
.logo-wrapper
display: inline-block
position: relative
top:-10px
margin: 0 12px
padding: 6px
width: 56px
height: 56px
vertical-align: top
box-sizing: border-box
border-radius: 50%
background: #141d27
.logo
width: 100%
height: 100%
border-radius: 50%
text-align: center
background: #2b343c
.icon-shopping_cart
font-size: 24px
line-height: 44px
color: #80858a
.price
display: inline-block
vertical-align: top
margin-top: 12px
line-height: 24px
padding-right: 12px
box-sizing: border-box
border-right: 1px solid rgba(255,255,255,0.1)
font-size: 16px
font-weight: 700
color: rgba(255,255,255,0.4)
.desc
display: inline-block
vertical-align: top
line-height: 24px
margin: 12px 0 0 12px
font-size: 10px
color: rgba(255,255,255,0.4)
.content-right
flex: 0 0 105px
width: 105px
</style>
購物車組件(四)
繼續編寫shopcart.vue組件,為商品價格計算做前期編碼
<template>
<div class="shopcart">
<div class="content">
<div class="content-left">
<div class="logo-wrapper">
<div class="logo">
<span class="icon-shopping_cart"></span>
</div>
</div>
<div class="price">¥{{totalPrice}}</div>
<div class="desc">另需配送費¥{{deliveryPrice}}元</div>
</div>
<div class="content-right">
<div class="pay">
¥{{minPrice}}元起送
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'v-shopcart',
props: {
selectFoods: {
type: Array,
default() {
return [
{
price:10,
count:1
}
];
}
},
deliveryPrice: {
type: Number,
default: 0
},
minPrice: {
type: Number,
default: 0
}
},
computed: {
totalPrice() {
let total = 0;
this.selectFoods.forEach((food) => {
total += food.price * food.count;
});
return total;
},
totalCount() {
let count = 0;
this.selectFoods.forEach((food) => {
count += food.count;
});
return count;
}
}
}
</script>
<style scoped lang="stylus" rel="stylesheet/stylus">
.shopcart
position: fixed
left: 0
bottom: 0
z-index: 50
width: 100%
height: 48px
.content
display: flex
background: #141d27
font-size: 0
.content-left
flex: 1
.logo-wrapper
display: inline-block
position: relative
top:-10px
margin: 0 12px
padding: 6px
width: 56px
height: 56px
vertical-align: top
box-sizing: border-box
border-radius: 50%
background: #141d27
.logo
width: 100%
height: 100%
border-radius: 50%
text-align: center
background: #2b343c
.icon-shopping_cart
font-size: 24px
line-height: 44px
color: #80858a
.price
display: inline-block
vertical-align: top
margin-top: 12px
line-height: 24px
padding-right: 12px
box-sizing: border-box
border-right: 1px solid rgba(255,255,255,0.1)
font-size: 16px
font-weight: 700
color: rgba(255,255,255,0.4)
.desc
display: inline-block
vertical-align: top
line-height: 24px
margin: 12px 0 0 12px
font-size: 10px
color: rgba(255,255,255,0.4)
.content-right
flex: 0 0 105px
width: 105px
.pay
height: 48px
line-height: 48px
text-align: center
font-size: 12px
color: rgba(255,255,255,0.4)
font-weight: 700
background: #2b333b
</style>
購物車組件(五)
繼續編寫shopcart.vue組件,實現樣式動態改變
<template>
<div class="shopcart">
<div class="content">
<div class="content-left">
<div class="logo-wrapper">
<div class="logo" :class="{'highlight':totalCount>0}">
<span class="icon-shopping_cart" :class="{'highlight':totalCount>0}"></span>
</div>
<div class="num" v-show="totalCount>0">{{totalCount}}</div>
</div>
<div class="price" :class="{'highlight':totalPrice>0}">¥{{totalPrice}}</div>
<div class="desc">另需配送費¥{{deliveryPrice}}元</div>
</div>
<div class="content-right">
<div class="pay">
¥{{minPrice}}元起送
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'v-shopcart',
props: {
selectFoods: {
type: Array,
default() {
return [];
}
},
deliveryPrice: {
type: Number,
default: 0
},
minPrice: {
type: Number,
default: 0
}
},
computed: {
totalPrice() {
let total = 0;
this.selectFoods.forEach((food) => {
total += food.price * food.count;
});
return total;
},
totalCount() {
let count = 0;
this.selectFoods.forEach((food) => {
count += food.count;
});
return count;
}
}
}
</script>
<style scoped lang="stylus" rel="stylesheet/stylus">
.shopcart
position: fixed
left: 0
bottom: 0
z-index: 50
width: 100%
height: 48px
.content
display: flex
background: #141d27
font-size: 0
.content-left
flex: 1
.logo-wrapper
display: inline-block
position: relative
top:-10px
margin: 0 12px
padding: 6px
width: 56px
height: 56px
vertical-align: top
box-sizing: border-box
border-radius: 50%
background: #141d27
.logo
width: 100%
height: 100%
border-radius: 50%
text-align: center
background: #2b343c
&.highlight
background: rgb(0,160,220)
.icon-shopping_cart
font-size: 24px
line-height: 44px
color: #80858a
&.highlight
color: #fff
.num
position: absolute
top: 0px
right: 0px
width: 24px
height: 16px
line-height: 16px
text-align: center
border-radius: 16px
font-size: 9px
font-weight: 700
color: #fff
background: rgb(240,20,20)
box-shadow: 0 4px 8px 0 rgba(0,0,0,0.4)
.price
display: inline-block
vertical-align: top
margin-top: 12px
line-height: 24px
padding-right: 12px
box-sizing: border-box
border-right: 1px solid rgba(255,255,255,0.1)
font-size: 16px
font-weight: 700
color: rgba(255,255,255,0.4)
&.highlight
color: #fff
.desc
display: inline-block
vertical-align: top
line-height: 24px
margin: 12px 0 0 12px
font-size: 10px
color: rgba(255,255,255,0.4)
.content-right
flex: 0 0 105px
width: 105px
.pay
height: 48px
line-height: 48px
text-align: center
font-size: 12px
color: rgba(255,255,255,0.4)
font-weight: 700
background: #2b333b
</style>
購物車組件(六)
繼續編寫shopcart.vue組件,基本完成全部效果
<template>
<div class="shopcart">
<div class="content">
<div class="content-left">
<div class="logo-wrapper">
<div class="logo" :class="{'highlight':totalCount>0}">
<span class="icon-shopping_cart" :class="{'highlight':totalCount>0}"></span>
</div>
<div class="num" v-show="totalCount>0">{{totalCount}}</div>
</div>
<div class="price" :class="{'highlight':totalPrice>0}">¥{{totalPrice}}</div>
<div class="desc">另需配送費¥{{deliveryPrice}}元</div>
</div>
<div class="content-right">
<div class="pay" :class="payClass">
{{payDesc}}
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'v-shopcart',
props: {
selectFoods: {
type: Array,
default() {
return [
{
price: 10,
count: 5
}
];
}
},
deliveryPrice: {
type: Number,
default: 0
},
minPrice: {
type: Number,
default: 0
}
},
computed: {
totalPrice() {
let total = 0;
this.selectFoods.forEach((food) => {
total += food.price * food.count;
});
return total;
},
totalCount() {
let count = 0;
this.selectFoods.forEach((food) => {
count += food.count;
});
return count;
},
payDesc() {
if(this.totalPrice === 0){
return `¥${this.minPrice}元起送`;
}else if(this.totalPrice<this.minPrice){
let diff = this.minPrice - this.totalPrice;
return `還差¥${diff}元起送`;
}else{
return '去結算';
}
},
payClass() {
if(this.totalPrice < this.minPrice){
return 'not-enough';
}else{
return 'enough';
}
}
}
}
</script>
<style scoped lang="stylus" rel="stylesheet/stylus">
.shopcart
position: fixed
left: 0
bottom: 0
z-index: 50
width: 100%
height: 48px
.content
display: flex
background: #141d27
font-size: 0
.content-left
flex: 1
.logo-wrapper
display: inline-block
position: relative
top:-10px
margin: 0 12px
padding: 6px
width: 56px
height: 56px
vertical-align: top
box-sizing: border-box
border-radius: 50%
background: #141d27
.logo
width: 100%
height: 100%
border-radius: 50%
text-align: center
background: #2b343c
&.highlight
background: rgb(0,160,220)
.icon-shopping_cart
font-size: 24px
line-height: 44px
color: #80858a
&.highlight
color: #fff
.num
position: absolute
top: 0px
right: 0px
width: 24px
height: 16px
line-height: 16px
text-align: center
border-radius: 16px
font-size: 9px
font-weight: 700
color: #fff
background: rgb(240,20,20)
box-shadow: 0 4px 8px 0 rgba(0,0,0,0.4)
.price
display: inline-block
vertical-align: top
margin-top: 12px
line-height: 24px
padding-right: 12px
box-sizing: border-box
border-right: 1px solid rgba(255,255,255,0.1)
font-size: 16px
font-weight: 700
color: rgba(255,255,255,0.4)
&.highlight
color: #fff
.desc
display: inline-block
vertical-align: top
line-height: 24px
margin: 12px 0 0 12px
font-size: 10px
color: rgba(255,255,255,0.4)
.content-right
flex: 0 0 105px
width: 105px
.pay
height: 48px
line-height: 48px
text-align: center
font-size: 12px
color: rgba(255,255,255,0.4)
font-weight: 700
background: #2b333b
&.not-enough
background: #2b333b
&.enough
background: #00b43c
color: #fff
</style>
cartcontrol組件(一)
步驟一:新建cartcontrol.vue組件,作為購物車的添加按鈕
<template>
<div class="cartcontrol">
<div class="cart-decrease icon-remove_circle_outline" v-show="food.count>0"></div>
<div class="cart-count" v-show="food.count>0">{{food.count}}</div>
<div class="cart-add icon-add_circle" @click="addCart"></div>
</div>
</template>
<script>
export default {
name: 'v-cartcontrol',
props: {
food: {
type: Object
}
},
created() {
console.log(this.food);
},
methods: {
addCart() {
}
}
}
</script>
<style scoped lang="stylus" rel="stylesheet/stylus">
.cartcontrol
font-size: 0
.cart-decrease, .cart-add
font-size: 24px
line-height: 24px
padding: 6px
color: rgb(0,160,220)
display: inline-block
.cart-count
display: inline-block
.cart-add
display: inline-block
</style>
步驟二:在goods.vue組件中調用cartcontrol.vue組件
<template>
<div class="goods">
<div class="menu-wrapper" ref="menuWrapper">
<ul>
<li v-for="(item,index) in goods" class="menu-item" :class="{'current':currentIndex===index}" @click="selectMenu(index,$event)">
<span class="text border-1px">
<span v-show="item.type>0" class="icon" :class="classMap[item.type]"></span> {{item.name}}
</span>
</li>
</ul>
</div>
<div class="foods-wrapper" ref="foodsWrapper">
<ul>
<li v-for="item in goods" class="food-list food-list-hook">
<h1 class="title">{{item.name}}</h1>
<ul>
<li v-for="food in item.foods" class="food-item border-1px">
<div class="icon">
<img :src="food.icon" width="57" height="57" />
</div>
<div class="content">
<h2 class="name">{{food.name}}</h2>
<p class="desc">{{food.description}}</p>
<div class="extra">
<span class="count">月售{{food.sellCount}}份</span>
<span>好評率{{food.rating}}%</span>
</div>
<div class="price">
<span class="now">¥{{food.price}}</span>
<span class="old" v-show="food.oldPrice">¥{{food.oldPrice}}</span>
</div>
<div class="cartcontrol-wrapper">
<cartcontrol :food="food"></cartcontrol>
</div>
</div>
</li>
</ul>
</li>
</ul>
</div>
<shopcart :delivery-price="seller.deliveryPrice" :min-price="seller.minPrice"></shopcart>
</div>
</template>
<script>
import BScroll from 'better-scroll';
import shopcart from '../../components/shopcart/shopcart';
import cartcontrol from '../../components/cartcontrol/cartcontrol';
const ERR_OK = 0;
export default {
name: 'v-goods',
props: {
seller: {
type: Object
}
},
data() {
return {
goods: [],
listHeight: [],
scrollY: 0
};
},
computed: {
currentIndex() {
for(let i=0;i<this.listHeight.length;i++){
let height1 = this.listHeight[i];
let height2 = this.listHeight[i+1];
if (!height2 || (this.scrollY >= height1 && this.scrollY < height2)){
return i;
}
}
return 0;
}
},
created() {
this.classMap = ['decrease','discount','special','invoice','guarantee'];
this.$http.get('api/goods').then((response) => {
response = response.body;
if(response.error === ERR_OK){
this.goods = response.data;
}
this.goods = response.data;
this.$nextTick(() => {
this._initScroll();
this._calculateHeight();
});
});
},
methods: {
selectMenu(index,event){
if(!event._constructed){
return;
}
let foodList = this.$refs.foodsWrapper.getElementsByClassName('food-list-hook');
let el = foodList[index];
this.foodsScroll.scrollToElement(el,300);
console.log(index);
},
_initScroll() {
this.menuScroll = new BScroll(this.$refs.menuWrapper,{
click: true
});
this.foodsScroll = new BScroll(this.$refs.foodsWrapper,{
click: true,
probeType: 3
});
this.foodsScroll.on("scroll",(pos) => {
this.scrollY = Math.abs(Math.round(pos.y));
});
},
_calculateHeight() {
let foodList = this.$refs.foodsWrapper.getElementsByClassName('food-list-hook');
let height = 0;
this.listHeight.push(height);
for(let i=0;i<foodList.length;i++){
let item = foodList[i];
height += item.clientHeight;
this.listHeight.push(height);
}
}
},
components: {
shopcart,
cartcontrol
}
}
</script>
<style scoped lang="stylus" rel="stylesheet/stylus">
@import "../../common/stylus/mixin";
.goods
display: flex
position: absolute
top: 174px
bottom: 46px
width: 100%
overflow: hidden
.menu-wrapper
flex: 0 0 80px
width: 80px
background: #f3f5f7
.menu-item
display: table
height: 54px
width: 56px
padding: 0 12px
line-height: 14px
&.current
position: relative
z-index: 10
margin-top: -1px
background: #ccc
font-weight: 700
.text
border-none()
.icon
display: inline-block
vertical-align: top
width: 12px
height: 12px
margin-right: 2px
background-size: 12px 12px
background-repeat: no-repeat
&.decrease
bg-image('decrease_3')
&.discount
bg-image('discount_3')
&.guarantee
bg-image('guarantee_3')
&.invoice
bg-image('invoice_3')
&.special
bg-image('special_3')
.text
display: table-cell
width: 56px
vertical-align: middle
border-1px(rgba(7,17,27,0.1))
font-size: 12px
.foods-wrapper
flex: 1
.title
padding-left: 14px
height: 26px
line-height: 26px
border-left: 2px solid #d9dde1
font-size: 12px
color: rgb(147,153,159)
background: #f3f5f7
.food-item
display: flex
margin: 18px
padding-bottom: 18px
border-1px(rgba(7,17,27,0.1))
&:last-child
border-none()
margin-bottom: 0
.icon
flex: 0 0 57px
margin-right: 10px
.content
flex: 1
.name
margin: 2px 0 8px 0
height: 14px
line-height: 14px
font-size: 14px
color: rgb(7,17,27)
.desc, .extra
line-height: 10px
font-size: 10px
color: rgb(147,153,159)
.desc
margin-bottom: 8px
line-height: 14px
.extra
.count
margin-right: 12px
.price
font-weight: 700
line-height: 24px
.now
margin-right: 8px
font-size: 14px
color: rgb(240,20,20)
.old
text-decoration: line-through
font-size: 10px
color: rgb(147,153,159)
.cartcontrol-wrapper
position: absolute
right: 0
bottom: 12px
</style>
cartcontrol組件(二)
繼續編寫cartcontrol.vue組件,進一步實現購物車按鈕效果
<template>
<div class="cartcontrol">
<div class="cart-decrease icon-remove_circle_outline" v-show="food.count>0" @click="decreaseCart"></div>
<div class="cart-count" v-show="food.count>0">{{food.count}}</div>
<div class="cart-add icon-add_circle" @click="addCart"></div>
</div>
</template>
<script>
import Vue from 'vue';
export default {
name: 'v-cartcontrol',
props: {
food: {
type: Object
}
},
methods: {
addCart(event) {
if (!event._constructed) {
return;
}
if (!this.food.count) {
Vue.set(this.food, 'count', 1);
} else {
this.food.count++;
}
},
decreaseCart(event) {
if (!event._constructed) {
return;
}
if (this.food.count) {
this.food.count--;
}
}
}
}
</script>
<style scoped lang="stylus" rel="stylesheet/stylus">
.cartcontrol
font-size: 0
.cart-decrease, .cart-add
font-size: 24px
line-height: 24px
padding: 6px
color: rgb(0,160,220)
display: inline-block
.cart-count
display: inline-block
vertical-align: top
width: 12px
padding-top: 6px
line-height: 24px
text-align: center
font-size: 10px
color: rgb(147,153,159)
.cart-add
display: inline-block
</style>
cartcontrol組件(三)
步驟一:編寫cartcontrol.vue組件,增加食品增加和減少動畫效果
<template>
<div class="cartcontrol">
<transition name="move">
<div class="cart-decrease" v-show="food.count>0" @click="decreaseCart">
<span class="inner icon-remove_circle_outline"></span>
</div>
</transition>
<div class="cart-count" v-show="food.count>0">{{food.count}}</div>
<div class="cart-add icon-add_circle" @click="addCart"></div>
</div>
</template>
<script>
import Vue from 'vue';
export default {
name: 'v-cartcontrol',
props: {
food: {
type: Object
}
},
methods: {
addCart(event) {
if (!event._constructed) {
return;
}
if (!this.food.count) {
Vue.set(this.food, 'count', 1);
} else {
this.food.count++;
}
},
decreaseCart(event) {
if (!event._constructed) {
return;
}
if (this.food.count) {
this.food.count--;
}
}
}
}
</script>
<style scoped lang="stylus" rel="stylesheet/stylus">
.cartcontrol
font-size: 0
.cart-decrease
padding: 6px
color: rgb(0,160,220)
display: inline-block
opacity: 1
transform: translate3d(0, 0, 0)
.inner
display: inline-block
line-height: 24px
font-size: 24px
color: rgb(0, 160, 220)
transition: all 0.4s linear
transform: rotate(0)
&.move-enter-active, &.move-leave-active
transition: all 0.4s linear
&.move-enter, &.move-leave-active
opacity: 0
transform: translate3d(24px, 0, 0)
.inner
transform: rotate(180deg)
.cart-count
display: inline-block
vertical-align: top
width: 12px
padding-top: 6px
line-height: 24px
text-align: center
font-size: 10px
color: rgb(147,153,159)
.cart-add
display: inline-block
padding: 6px
line-height: 24px
font-size: 24px
color: rgb(0, 160, 220)
</style>
步驟二:編寫goods.vue組件,將按鈕和購物車區實現聯動效果
<template>
<div class="goods">
<div class="menu-wrapper" ref="menuWrapper">
<ul>
<li v-for="(item,index) in goods" class="menu-item" :class="{'current':currentIndex===index}" @click="selectMenu(index,$event)">
<span class="text border-1px">
<span v-show="item.type>0" class="icon" :class="classMap[item.type]"></span> {{item.name}}
</span>
</li>
</ul>
</div>
<div class="foods-wrapper" ref="foodsWrapper">
<ul>
<li v-for="item in goods" class="food-list food-list-hook">
<h1 class="title">{{item.name}}</h1>
<ul>
<li v-for="food in item.foods" class="food-item border-1px">
<div class="icon">
<img :src="food.icon" width="57" height="57" />
</div>
<div class="content">
<h2 class="name">{{food.name}}</h2>
<p class="desc">{{food.description}}</p>
<div class="extra">
<span class="count">月售{{food.sellCount}}份</span>
<span>好評率{{food.rating}}%</span>
</div>
<div class="price">
<span class="now">¥{{food.price}}</span>
<span class="old" v-show="food.oldPrice">¥{{food.oldPrice}}</span>
</div>
<div class="cartcontrol-wrapper">
<cartcontrol :food="food"></cartcontrol>
</div>
</div>
</li>
</ul>
</li>
</ul>
</div>
<shopcart :select-foods="selectFoods" :delivery-price="seller.deliveryPrice" :min-price="seller.minPrice"></shopcart>
</div>
</template>
<script>
import BScroll from 'better-scroll';
import shopcart from '../../components/shopcart/shopcart';
import cartcontrol from '../../components/cartcontrol/cartcontrol';
const ERR_OK = 0;
export default {
name: 'v-goods',
props: {
seller: {
type: Object
}
},
data() {
return {
goods: [],
listHeight: [],
scrollY: 0
};
},
computed: {
currentIndex() {
for (let i = 0; i < this.listHeight.length; i++) {
let height1 = this.listHeight[i];
let height2 = this.listHeight[i + 1];
if (!height2 || (this.scrollY >= height1 && this.scrollY < height2)) {
return i;
}
}
return 0;
},
selectFoods() {
let foods = [];
this.goods.forEach((good) => {
good.foods.forEach((food) => {
if (food.count) {
foods.push(food);
}
});
});
return foods;
}
},
created() {
this.classMap = ['decrease', 'discount', 'special', 'invoice', 'guarantee'];
this.$http.get('api/goods').then((response) => {
response = response.body;
if (response.error === ERR_OK) {
this.goods = response.data;
}
this.goods = response.data;
this.$nextTick(() => {
this._initScroll();
this._calculateHeight();
});
});
},
methods: {
selectMenu(index, event) {
if (!event._constructed) {
return;
}
let foodList = this.$refs.foodsWrapper.getElementsByClassName('food-list-hook');
let el = foodList[index];
this.foodsScroll.scrollToElement(el, 300);
console.log(index);
},
_initScroll() {
this.menuScroll = new BScroll(this.$refs.menuWrapper, {
click: true
});
this.foodsScroll = new BScroll(this.$refs.foodsWrapper, {
click: true,
probeType: 3
});
this.foodsScroll.on("scroll", (pos) => {
this.scrollY = Math.abs(Math.round(pos.y));
});
},
_calculateHeight() {
let foodList = this.$refs.foodsWrapper.getElementsByClassName('food-list-hook');
let height = 0;
this.listHeight.push(height);
for (let i = 0; i < foodList.length; i++) {
let item = foodList[i];
height += item.clientHeight;
this.listHeight.push(height);
}
}
},
components: {
shopcart,
cartcontrol
}
}
</script>
<style scoped lang="stylus" rel="stylesheet/stylus">
@import "../../common/stylus/mixin";
.goods
display: flex
position: absolute
top: 174px
bottom: 46px
width: 100%
overflow: hidden
.menu-wrapper
flex: 0 0 80px
width: 80px
background: #f3f5f7
.menu-item
display: table
height: 54px
width: 56px
padding: 0 12px
line-height: 14px
&.current
position: relative
z-index: 10
margin-top: -1px
background: #ccc
font-weight: 700
.text
border-none()
.icon
display: inline-block
vertical-align: top
width: 12px
height: 12px
margin-right: 2px
background-size: 12px 12px
background-repeat: no-repeat
&.decrease
bg-image('decrease_3')
&.discount
bg-image('discount_3')
&.guarantee
bg-image('guarantee_3')
&.invoice
bg-image('invoice_3')
&.special
bg-image('special_3')
.text
display: table-cell
width: 56px
vertical-align: middle
border-1px(rgba(7,17,27,0.1))
font-size: 12px
.foods-wrapper
flex: 1
.title
padding-left: 14px
height: 26px
line-height: 26px
border-left: 2px solid #d9dde1
font-size: 12px
color: rgb(147,153,159)
background: #f3f5f7
.food-item
display: flex
margin: 18px
padding-bottom: 18px
border-1px(rgba(7,17,27,0.1))
&:last-child
border-none()
margin-bottom: 0
.icon
flex: 0 0 57px
margin-right: 10px
.content
flex: 1
.name
margin: 2px 0 8px 0
height: 14px
line-height: 14px
font-size: 14px
color: rgb(7,17,27)
.desc, .extra
line-height: 10px
font-size: 10px
color: rgb(147,153,159)
.desc
margin-bottom: 8px
line-height: 14px
.extra
.count
margin-right: 12px
.price
font-weight: 700
line-height: 24px
.now
margin-right: 8px
font-size: 14px
color: rgb(240,20,20)
.old
text-decoration: line-through
font-size: 10px
color: rgb(147,153,159)
.cartcontrol-wrapper
position: absolute
right: 0
bottom: 12px
</style>
購物車小球動畫實現(一)
【注意】無法實現
步驟一:編寫goods.vue組件
<template>
<div class="goods">
<div class="menu-wrapper" ref="menuWrapper">
<ul>
<li v-for="(item,index) in goods" class="menu-item" :class="{'current':currentIndex===index}" @click="selectMenu(index,$event)">
<span class="text border-1px">
<span v-show="item.type>0" class="icon" :class="classMap[item.type]"></span> {{item.name}}
</span>
</li>
</ul>
</div>
<div class="foods-wrapper" ref="foodsWrapper">
<ul>
<li v-for="item in goods" class="food-list food-list-hook">
<h1 class="title">{{item.name}}</h1>
<ul>
<li v-for="food in item.foods" class="food-item border-1px">
<div class="icon">
<img :src="food.icon" width="57" height="57" />
</div>
<div class="content">
<h2 class="name">{{food.name}}</h2>
<p class="desc">{{food.description}}</p>
<div class="extra">
<span class="count">月售{{food.sellCount}}份</span>
<span>好評率{{food.rating}}%</span>
</div>
<div class="price">
<span class="now">¥{{food.price}}</span>
<span class="old" v-show="food.oldPrice">¥{{food.oldPrice}}</span>
</div>
<div class="cartcontrol-wrapper">
<cartcontrol :food="food"></cartcontrol>
</div>
</div>
</li>
</ul>
</li>
</ul>
</div>
<shopcart ref:shopcart :select-foods="selectFoods" :delivery-price="seller.deliveryPrice" :min-price="seller.minPrice"></shopcart>
</div>
</template>
<script>
import BScroll from 'better-scroll';
import shopcart from '../../components/shopcart/shopcart';
import cartcontrol from '../../components/cartcontrol/cartcontrol';
const ERR_OK = 0;
export default {
name: 'v-goods',
props: {
seller: {
type: Object
}
},
data() {
return {
goods: [],
listHeight: [],
scrollY: 0
};
},
computed: {
currentIndex() {
for (let i = 0; i < this.listHeight.length; i++) {
let height1 = this.listHeight[i];
let height2 = this.listHeight[i + 1];
if (!height2 || (this.scrollY >= height1 && this.scrollY < height2)) {
return i;
}
}
return 0;
},
selectFoods() {
let foods = [];
this.goods.forEach((good) => {
good.foods.forEach((food) => {
if (food.count) {
foods.push(food);
}
});
});
return foods;
}
},
created() {
this.classMap = ['decrease', 'discount', 'special', 'invoice', 'guarantee'];
this.$http.get('api/goods').then((response) => {
response = response.body;
if (response.error === ERR_OK) {
this.goods = response.data;
}
this.goods = response.data;
this.$nextTick(() => {
this._initScroll();
this._calculateHeight();
});
});
},
methods: {
selectMenu(index, event) {
if (!event._constructed) {
return;
}
let foodList = this.$refs.foodsWrapper.getElementsByClassName('food-list-hook');
let el = foodList[index];
this.foodsScroll.scrollToElement(el, 300);
console.log(index);
},
_drop(target) {
//體驗優化,異步執行下落動畫
this.$nextTick(() => {
this.$refs.shopcart.drop(target);
});
},
_initScroll() {
this.menuScroll = new BScroll(this.$refs.menuWrapper, {
click: true
});
this.foodsScroll = new BScroll(this.$refs.foodsWrapper, {
click: true,
probeType: 3
});
this.foodsScroll.on("scroll", (pos) => {
this.scrollY = Math.abs(Math.round(pos.y));
});
},
_calculateHeight() {
let foodList = this.$refs.foodsWrapper.getElementsByClassName('food-list-hook');
let height = 0;
this.listHeight.push(height);
for (let i = 0; i < foodList.length; i++) {
let item = foodList[i];
height += item.clientHeight;
this.listHeight.push(height);
}
}
},
components: {
shopcart,
cartcontrol
},
events: {
'cart.add'(target){
this._drop(target);
}
}
}
</script>
<style scoped lang="stylus" rel="stylesheet/stylus">
@import "../../common/stylus/mixin";
.goods
display: flex
position: absolute
top: 174px
bottom: 46px
width: 100%
overflow: hidden
.menu-wrapper
flex: 0 0 80px
width: 80px
background: #f3f5f7
.menu-item
display: table
height: 54px
width: 56px
padding: 0 12px
line-height: 14px
&.current
position: relative
z-index: 10
margin-top: -1px
background: #ccc
font-weight: 700
.text
border-none()
.icon
display: inline-block
vertical-align: top
width: 12px
height: 12px
margin-right: 2px
background-size: 12px 12px
background-repeat: no-repeat
&.decrease
bg-image('decrease_3')
&.discount
bg-image('discount_3')
&.guarantee
bg-image('guarantee_3')
&.invoice
bg-image('invoice_3')
&.special
bg-image('special_3')
.text
display: table-cell
width: 56px
vertical-align: middle
border-1px(rgba(7,17,27,0.1))
font-size: 12px
.foods-wrapper
flex: 1
.title
padding-left: 14px
height: 26px
line-height: 26px
border-left: 2px solid #d9dde1
font-size: 12px
color: rgb(147,153,159)
background: #f3f5f7
.food-item
display: flex
margin: 18px
padding-bottom: 18px
border-1px(rgba(7,17,27,0.1))
&:last-child
border-none()
margin-bottom: 0
.icon
flex: 0 0 57px
margin-right: 10px
.content
flex: 1
.name
margin: 2px 0 8px 0
height: 14px
line-height: 14px
font-size: 14px
color: rgb(7,17,27)
.desc, .extra
line-height: 10px
font-size: 10px
color: rgb(147,153,159)
.desc
margin-bottom: 8px
line-height: 14px
.extra
.count
margin-right: 12px
.price
font-weight: 700
line-height: 24px
.now
margin-right: 8px
font-size: 14px
color: rgb(240,20,20)
.old
text-decoration: line-through
font-size: 10px
color: rgb(147,153,159)
.cartcontrol-wrapper
position: absolute
right: 0
bottom: 12px
</style>
步驟二:編寫carcontrol.vue組件
<template>
<div class="cartcontrol">
<transition name="move">
<div class="cart-decrease" v-show="food.count>0" @click.stop.prevent="decreaseCart">
<span class="inner icon-remove_circle_outline"></span>
</div>
</transition>
<div class="cart-count" v-show="food.count>0">{{food.count}}</div>
<div class="cart-add icon-add_circle" @click.stop.prevent="addCart"></div>
</div>
</template>
<script>
import Vue from 'vue';
export default {
props: {
food: {
type: Object
}
},
methods: {
addCart(event) {
if (!event._constructed) {
return;
}
if (!this.food.count) {
Vue.set(this.food, 'count', 1);
} else {
this.food.count++;
}
this.$dispatch('cart.add',event.target);
},
decreaseCart(event) {
if (!event._constructed) {
return;
}
if (this.food.count) {
this.food.count--;
}
}
}
};
</script>
<style lang="stylus" rel="stylesheet/stylus">
.cartcontrol
font-size: 0
.cart-decrease
display: inline-block
padding: 6px
opacity: 1
transform: translate3d(0, 0, 0)
.inner
display: inline-block
line-height: 24px
font-size: 24px
color: rgb(0, 160, 220)
transition: all 0.4s linear
transform: rotate(0)
&.move-enter-active, &.move-leave-active
transition: all 0.4s linear
&.move-enter, &.move-leave-active
opacity: 0
transform: translate3d(24px, 0, 0)
.inner
transform: rotate(180deg)
.cart-count
display: inline-block
vertical-align: top
width: 12px
padding-top: 6px
line-height: 24px
text-align: center
font-size: 10px
color: rgb(147, 153, 159)
.cart-add
display: inline-block
padding: 6px
line-height: 24px
font-size: 24px
color: rgb(0, 160, 220)
</style>
步驟三:編寫shoucart.vue組件
<template>
<div class="shopcart">
<div class="content">
<div class="content-left">
<div class="logo-wrapper">
<div class="logo" :class="{'highlight':totalCount>0}">
<span class="icon-shopping_cart" :class="{'highlight':totalCount>0}"></span>
</div>
<div class="num" v-show="totalCount>0">{{totalCount}}</div>
</div>
<div class="price" :class="{'highlight':totalPrice>0}">¥{{totalPrice}}</div>
<div class="desc">另需配送費¥{{deliveryPrice}}元</div>
</div>
<div class="content-right">
<div class="pay" :class="payClass">
{{payDesc}}
</div>
</div>
</div>
<div class="ball-container">
<div v-for="ball in balls">
<transition name="drop">
<div class="ball" v-show="ball.show">
<div class="inner inner-hook"></div>
</div>
</transition>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'v-shopcart',
props: {
selectFoods: {
type: Array,
default () {
return [{
price: 10,
count: 5
}];
}
},
deliveryPrice: {
type: Number,
default: 0
},
minPrice: {
type: Number,
default: 0
}
},
data() {
return {
balls: [{
show: false
},
{
show: false
},
{
show: false
},
{
show: false
},
{
show: false
}
],
dropBall: []
};
},
computed: {
totalPrice() {
let total = 0;
this.selectFoods.forEach((food) => {
total += food.price * food.count;
});
return total;
},
totalCount() {
let count = 0;
this.selectFoods.forEach((food) => {
count += food.count;
});
return count;
},
payDesc() {
if (this.totalPrice === 0) {
return `¥${this.minPrice}元起送`;
} else if (this.totalPrice < this.minPrice) {
let diff = this.minPrice - this.totalPrice;
return `還差¥${diff}元起送`;
} else {
return '去結算';
}
},
payClass() {
if (this.totalPrice < this.minPrice) {
return 'not-enough';
} else {
return 'enough';
}
}
},
methods: {
drop(el) {
for (let i = 0; i < this.balls.length; i++) {
let ball = this.balls[i];
if (!ball.show) {
ball.show = true;
ball.el = el;
this.dropBall.push(ball);
return;
}
}
}
},
transitions: {
drop: {
beforeDrop(el) {
let count = this.balls.length;
while (count--) {
let ball = this.balls[count];
if (ball.show) {
let rect = ball.el.getBoundingClientRect();
let x = rect.left - 32;
let y = -(window.innerHeight - rect.top - 22);
el.style.display = '';
el.style.webkitTransform = `translate3d(0,${y}px,0)`;
el.style.transform = `translate3d(0,${y}px,0)`;
let inner = el.getElementsByClassName('inner-hook')[0];
inner.style.webkitTransform = `translate3d(${x}px,0,0)`;
inner.style.transform = `translate3d(${x}px,0,0)`;
}
}
},
enter(el) {
/* eslint-disable no-unused-vars */
let rf = el.offsetHeight;
this.$nextTick(() => {
el.style.webkitTransform = 'translate3d(0,0,0)';
el.style.transform = 'translate3d(0,0,0)';
let inner = el.getElementsByClassName('inner-hook')[0];
inner.style.webkitTransform = 'translate3d(0,0,0)';
inner.style.transform = 'translate3d(0,0,0)';
el.addEventListener('transitionend', done);
});
},
afterEnter(el) {
let ball = this.dropBalls.shift();
if (ball) {
ball.show = false;
el.style.display = 'none';
}
}
}
}
}
</script>
<style scoped lang="stylus" rel="stylesheet/stylus">
.shopcart
position: fixed
left: 0
bottom: 0
z-index: 50
width: 100%
height: 48px
.content
display: flex
background: #141d27
font-size: 0
.content-left
flex: 1
.logo-wrapper
display: inline-block
position: relative
top:-10px
margin: 0 12px
padding: 6px
width: 56px
height: 56px
vertical-align: top
box-sizing: border-box
border-radius: 50%
background: #141d27
.logo
width: 100%
height: 100%
border-radius: 50%
text-align: center
background: #2b343c
&.highlight
background: rgb(0,160,220)
.icon-shopping_cart
font-size: 24px
line-height: 44px
color: #80858a
&.highlight
color: #fff
.num
position: absolute
top: 0px
right: 0px
width: 24px
height: 16px
line-height: 16px
text-align: center
border-radius: 16px
font-size: 9px
font-weight: 700
color: #fff
background: rgb(240,20,20)
box-shadow: 0 4px 8px 0 rgba(0,0,0,0.4)
.price
display: inline-block
vertical-align: top
margin-top: 12px
line-height: 24px
padding-right: 12px
box-sizing: border-box
border-right: 1px solid rgba(255,255,255,0.1)
font-size: 16px
font-weight: 700
color: rgba(255,255,255,0.4)
&.highlight
color: #fff
.desc
display: inline-block
vertical-align: top
line-height: 24px
margin: 12px 0 0 12px
font-size: 10px
color: rgba(255,255,255,0.4)
.content-right
flex: 0 0 105px
width: 105px
.pay
height: 48px
line-height: 48px
text-align: center
font-size: 12px
color: rgba(255,255,255,0.4)
font-weight: 700
background: #2b333b
&.not-enough
background: #2b333b
&.enough
background: #00b43c
color: #fff
.ball-container
.ball
position: fixed
left: 32px
bottom: 22px
z-index: 200
transition: all 0.4s cubic-bezier(0.49, -0.29, 0.75, 0.41)
.inner
width: 16px
height: 16px
border-radius: 50%
background: rgb(0, 160, 220)
transition: all 0.4s linear
</style>
購物車詳情頁(一、二)
編寫shopcart.vue組件,實現初步效果
<template>
<div class="shopcart">
<div class="content" @click="toggleList">
<div class="content-left">
<div class="logo-wrapper">
<div class="logo" :class="{'highlight':totalCount>0}">
<span class="icon-shopping_cart" :class="{'highlight':totalCount>0}"></span>
</div>
<div class="num" v-show="totalCount>0">{{totalCount}}</div>
</div>
<div class="price" :class="{'highlight':totalPrice>0}">¥{{totalPrice}}</div>
<div class="desc">另需配送費¥{{deliveryPrice}}元</div>
</div>
<div class="content-right">
<div class="pay" :class="payClass">
{{payDesc}}
</div>
</div>
</div>
<div class="ball-container">
<div v-for="ball in balls">
<transition name="drop">
<div class="ball" v-show="ball.show">
<div class="inner inner-hook"></div>
</div>
</transition>
</div>
</div>
<transition name="fold">
<div class="shopcart-list" v-show="listShow">
<div class="list-header">
<h1 class="title">購物車</h1>
<span class="empty">清空</span>
</div>
<div class="list-content">
<ul>
<li class="food" v-for="food in selectFoods">
<span class="name">{{food.name}}</span>
<div class="price">
<span>¥{{food.price*food.count}}</span>
</div>
<div class="cartcontrol-wrapper">
<cartcontrol :food="food"></cartcontrol>
</div>
</li>
</ul>
</div>
</div>
</transition>
</div>
</template>
<script>
import cartcontrol from '../../components/cartcontrol/cartcontrol';
export default {
name: 'v-shopcart',
props: {
selectFoods: {
type: Array,
default () {
return [{
price: 10,
count: 5
}];
}
},
deliveryPrice: {
type: Number,
default: 0
},
minPrice: {
type: Number,
default: 0
}
},
data() {
return {
balls: [{
show: false
},
{
show: false
},
{
show: false
},
{
show: false
},
{
show: false
}
],
dropBall: [],
fold: true
};
},
computed: {
totalPrice() {
let total = 0;
this.selectFoods.forEach((food) => {
total += food.price * food.count;
});
return total;
},
totalCount() {
let count = 0;
this.selectFoods.forEach((food) => {
count += food.count;
});
return count;
},
payDesc() {
if (this.totalPrice === 0) {
return `¥${this.minPrice}元起送`;
} else if (this.totalPrice < this.minPrice) {
let diff = this.minPrice - this.totalPrice;
return `還差¥${diff}元起送`;
} else {
return '去結算';
}
},
payClass() {
if (this.totalPrice < this.minPrice) {
return 'not-enough';
} else {
return 'enough';
}
},
listShow() {
if (!this.totalCount) {
this.fold = true;
return false;
}
let show = !this.fold;
return show;
}
},
methods: {
drop(el) {
for (let i = 0; i < this.balls.length; i++) {
let ball = this.balls[i];
if (!ball.show) {
ball.show = true;
ball.el = el;
this.dropBall.push(ball);
return;
}
}
},
toggleList() {
if (!this.totalCount) {
return;
}
this.fold = !this.fold;
}
},
transitions: {
drop: {
beforeDrop(el) {
let count = this.balls.length;
while (count--) {
let ball = this.balls[count];
if (ball.show) {
let rect = ball.el.getBoundingClientRect();
let x = rect.left - 32;
let y = -(window.innerHeight - rect.top - 22);
el.style.display = '';
el.style.webkitTransform = `translate3d(0,${y}px,0)`;
el.style.transform = `translate3d(0,${y}px,0)`;
let inner = el.getElementsByClassName('inner-hook')[0];
inner.style.webkitTransform = `translate3d(${x}px,0,0)`;
inner.style.transform = `translate3d(${x}px,0,0)`;
}
}
},
enter(el) {
/* eslint-disable no-unused-vars */
let rf = el.offsetHeight;
this.$nextTick(() => {
el.style.webkitTransform = 'translate3d(0,0,0)';
el.style.transform = 'translate3d(0,0,0)';
let inner = el.getElementsByClassName('inner-hook')[0];
inner.style.webkitTransform = 'translate3d(0,0,0)';
inner.style.transform = 'translate3d(0,0,0)';
el.addEventListener('transitionend', done);
});
},
afterEnter(el) {
let ball = this.dropBalls.shift();
if (ball) {
ball.show = false;
el.style.display = 'none';
}
}
}
},
components: {
cartcontrol
}
}
</script>
<style scoped lang="stylus" rel="stylesheet/stylus">
@import "../../common/stylus/mixin";
.shopcart
position: fixed
left: 0
bottom: 0
z-index: 50
width: 100%
height: 48px
.content
display: flex
background: #141d27
font-size: 0
.content-left
flex: 1
.logo-wrapper
display: inline-block
position: relative
top:-10px
margin: 0 12px
padding: 6px
width: 56px
height: 56px
vertical-align: top
box-sizing: border-box
border-radius: 50%
background: #141d27
.logo
width: 100%
height: 100%
border-radius: 50%
text-align: center
background: #2b343c
&.highlight
background: rgb(0,160,220)
.icon-shopping_cart
font-size: 24px
line-height: 44px
color: #80858a
&.highlight
color: #fff
.num
position: absolute
top: 0px
right: 0px
width: 24px
height: 16px
line-height: 16px
text-align: center
border-radius: 16px
font-size: 9px
font-weight: 700
color: #fff
background: rgb(240,20,20)
box-shadow: 0 4px 8px 0 rgba(0,0,0,0.4)
.price
display: inline-block
vertical-align: top
margin-top: 12px
line-height: 24px
padding-right: 12px
box-sizing: border-box
border-right: 1px solid rgba(255,255,255,0.1)
font-size: 16px
font-weight: 700
color: rgba(255,255,255,0.4)
&.highlight
color: #fff
.desc
display: inline-block
vertical-align: top
line-height: 24px
margin: 12px 0 0 12px
font-size: 10px
color: rgba(255,255,255,0.4)
.content-right
flex: 0 0 105px
width: 105px
.pay
height: 48px
line-height: 48px
text-align: center
font-size: 12px
color: rgba(255,255,255,0.4)
font-weight: 700
background: #2b333b
&.not-enough
background: #2b333b
&.enough
background: #00b43c
color: #fff
.ball-container
.ball
position: fixed
left: 32px
bottom: 22px
z-index: 200
transition: all 0.4s cubic-bezier(0.49, -0.29, 0.75, 0.41)
.inner
width: 16px
height: 16px
border-radius: 50%
background: rgb(0, 160, 220)
transition: all 0.4s linear
.shopcart-list
position: absolute
left: 0
top: 0
z-index: -1
width: 100%
transform: translate3d(0, -100%, 0)
&.fold-enter-active, &.fold-leave-active
transition: all 0.5s
&.fold-enter, &.fold-leave-active
transform: translate3d(0, 0, 0)
.list-header
height: 40px
line-height: 40px
padding: 0 18px
background: #f3f5f7
border-bottom: 1px solid rgba(7, 17, 27, 0.1)
.title
float: left
font-size: 14px
color: rgb(7, 17, 27)
.empty
float: right
font-size: 12px
color: rgb(0, 160, 220)
.list-content
padding: 0 18px
max-height: 217px
overflow: hidden
background: #fff
.food
position: relative
padding: 12px 0
box-sizing: border-box
border-1px(rgba(7, 17, 27, 0.1))
.name
line-height: 24px
font-size: 14px
color: rgb(7, 17, 27)
.price
position: absolute
right: 90px
bottom: 12px
line-height: 24px
font-size: 14px
font-weight: 700
color: rgb(240, 20, 20)
.cartcontrol-wrapper
position: absolute
right: 0
bottom: 6px
</style>
購物車詳情頁(三)
繼續編寫shopcart.vue組件,實現增加和減少功能
<template>
<div class="shopcart">
<div class="content" @click="toggleList">
<div class="content-left">
<div class="logo-wrapper">
<div class="logo" :class="{'highlight':totalCount>0}">
<span class="icon-shopping_cart" :class="{'highlight':totalCount>0}"></span>
</div>
<div class="num" v-show="totalCount>0">{{totalCount}}</div>
</div>
<div class="price" :class="{'highlight':totalPrice>0}">¥{{totalPrice}}</div>
<div class="desc">另需配送費¥{{deliveryPrice}}元</div>
</div>
<div class="content-right">
<div class="pay" :class="payClass">
{{payDesc}}
</div>
</div>
</div>
<div class="ball-container">
<div v-for="ball in balls">
<transition name="drop">
<div class="ball" v-show="ball.show">
<div class="inner inner-hook"></div>
</div>
</transition>
</div>
</div>
<transition name="fold">
<div class="shopcart-list" v-show="listShow">
<div class="list-header">
<h1 class="title">購物車</h1>
<span class="empty">清空</span>
</div>
<div class="list-content" ref="listContent">
<ul>
<li class="food" v-for="food in selectFoods">
<span class="name">{{food.name}}</span>
<div class="price">
<span>¥{{food.price*food.count}}</span>
</div>
<div class="cartcontrol-wrapper">
<cartcontrol :food="food"></cartcontrol>
</div>
</li>
</ul>
</div>
</div>
</transition>
</div>
</template>
<script>
import BScroll from 'better-scroll';
import cartcontrol from '../../components/cartcontrol/cartcontrol';
export default {
name: 'v-shopcart',
props: {
selectFoods: {
type: Array,
default () {
return [{
price: 10,
count: 5
}];
}
},
deliveryPrice: {
type: Number,
default: 0
},
minPrice: {
type: Number,
default: 0
}
},
data() {
return {
balls: [{
show: false
},
{
show: false
},
{
show: false
},
{
show: false
},
{
show: false
}
],
dropBall: [],
fold: true
};
},
computed: {
totalPrice() {
let total = 0;
this.selectFoods.forEach((food) => {
total += food.price * food.count;
});
return total;
},
totalCount() {
let count = 0;
this.selectFoods.forEach((food) => {
count += food.count;
});
return count;
},
payDesc() {
if (this.totalPrice === 0) {
return `¥${this.minPrice}元起送`;
} else if (this.totalPrice < this.minPrice) {
let diff = this.minPrice - this.totalPrice;
return `還差¥${diff}元起送`;
} else {
return '去結算';
}
},
payClass() {
if (this.totalPrice < this.minPrice) {
return 'not-enough';
} else {
return 'enough';
}
},
listShow() {
if (!this.totalCount) {
this.fold = true;
return false;
}
let show = !this.fold;
if (show) {
this.$nextTick(() => {
if (!this.scroll) {
this.scroll = new BScroll(this.$refs.listContent, {
click: true
});
} else {
this.scroll.refresh();
}
});
}
return show;
}
},
methods: {
drop(el) {
for (let i = 0; i < this.balls.length; i++) {
let ball = this.balls[i];
if (!ball.show) {
ball.show = true;
ball.el = el;
this.dropBall.push(ball);
return;
}
}
},
toggleList() {
if (!this.totalCount) {
return;
}
this.fold = !this.fold;
}
},
transitions: {
drop: {
beforeDrop(el) {
let count = this.balls.length;
while (count--) {
let ball = this.balls[count];
if (ball.show) {
let rect = ball.el.getBoundingClientRect();
let x = rect.left - 32;
let y = -(window.innerHeight - rect.top - 22);
el.style.display = '';
el.style.webkitTransform = `translate3d(0,${y}px,0)`;
el.style.transform = `translate3d(0,${y}px,0)`;
let inner = el.getElementsByClassName('inner-hook')[0];
inner.style.webkitTransform = `translate3d(${x}px,0,0)`;
inner.style.transform = `translate3d(${x}px,0,0)`;
}
}
},
enter(el) {
/* eslint-disable no-unused-vars */
let rf = el.offsetHeight;
this.$nextTick(() => {
el.style.webkitTransform = 'translate3d(0,0,0)';
el.style.transform = 'translate3d(0,0,0)';
let inner = el.getElementsByClassName('inner-hook')[0];
inner.style.webkitTransform = 'translate3d(0,0,0)';
inner.style.transform = 'translate3d(0,0,0)';
el.addEventListener('transitionend', done);
});
},
afterEnter(el) {
let ball = this.dropBalls.shift();
if (ball) {
ball.show = false;
el.style.display = 'none';
}
}
}
},
components: {
cartcontrol
}
}
</script>
<style scoped lang="stylus" rel="stylesheet/stylus">
@import "../../common/stylus/mixin";
.shopcart
position: fixed
left: 0
bottom: 0
z-index: 50
width: 100%
height: 48px
.content
display: flex
background: #141d27
font-size: 0
.content-left
flex: 1
.logo-wrapper
display: inline-block
position: relative
top:-10px
margin: 0 12px
padding: 6px
width: 56px
height: 56px
vertical-align: top
box-sizing: border-box
border-radius: 50%
background: #141d27
.logo
width: 100%
height: 100%
border-radius: 50%
text-align: center
background: #2b343c
&.highlight
background: rgb(0,160,220)
.icon-shopping_cart
font-size: 24px
line-height: 44px
color: #80858a
&.highlight
color: #fff
.num
position: absolute
top: 0px
right: 0px
width: 24px
height: 16px
line-height: 16px
text-align: center
border-radius: 16px
font-size: 9px
font-weight: 700
color: #fff
background: rgb(240,20,20)
box-shadow: 0 4px 8px 0 rgba(0,0,0,0.4)
.price
display: inline-block
vertical-align: top
margin-top: 12px
line-height: 24px
padding-right: 12px
box-sizing: border-box
border-right: 1px solid rgba(255,255,255,0.1)
font-size: 16px
font-weight: 700
color: rgba(255,255,255,0.4)
&.highlight
color: #fff
.desc
display: inline-block
vertical-align: top
line-height: 24px
margin: 12px 0 0 12px
font-size: 10px
color: rgba(255,255,255,0.4)
.content-right
flex: 0 0 105px
width: 105px
.pay
height: 48px
line-height: 48px
text-align: center
font-size: 12px
color: rgba(255,255,255,0.4)
font-weight: 700
background: #2b333b
&.not-enough
background: #2b333b
&.enough
background: #00b43c
color: #fff
.ball-container
.ball
position: fixed
left: 32px
bottom: 22px
z-index: 200
transition: all 0.4s cubic-bezier(0.49, -0.29, 0.75, 0.41)
.inner
width: 16px
height: 16px
border-radius: 50%
background: rgb(0, 160, 220)
transition: all 0.4s linear
.shopcart-list
position: absolute
left: 0
top: 0
z-index: -1
width: 100%
transform: translate3d(0, -100%, 0)
&.fold-enter-active, &.fold-leave-active
transition: all 0.5s
&.fold-enter, &.fold-leave-active
transform: translate3d(0, 0, 0)
.list-header
height: 40px
line-height: 40px
padding: 0 18px
background: #f3f5f7
border-bottom: 1px solid rgba(7, 17, 27, 0.1)
.title
float: left
font-size: 14px
color: rgb(7, 17, 27)
.empty
float: right
font-size: 12px
color: rgb(0, 160, 220)
.list-content
padding: 0 18px
max-height: 217px
overflow: hidden
background: #fff
.food
position: relative
padding: 12px 0
box-sizing: border-box
border-1px(rgba(7, 17, 27, 0.1))
.name
line-height: 24px
font-size: 14px
color: rgb(7, 17, 27)
.price
position: absolute
right: 90px
bottom: 12px
line-height: 24px
font-size: 14px
font-weight: 700
color: rgb(240, 20, 20)
.cartcontrol-wrapper
position: absolute
right: 0
bottom: 6px
</style>
購物車詳情頁(四)
繼續編寫shopcart.vue組件,實現蒙層效果
<template>
<div class="shopcart">
<div class="content" @click="toggleList">
<div class="content-left">
<div class="logo-wrapper">
<div class="logo" :class="{'highlight':totalCount>0}">
<span class="icon-shopping_cart" :class="{'highlight':totalCount>0}"></span>
</div>
<div class="num" v-show="totalCount>0">{{totalCount}}</div>
</div>
<div class="price" :class="{'highlight':totalPrice>0}">¥{{totalPrice}}</div>
<div class="desc">另需配送費¥{{deliveryPrice}}元</div>
</div>
<!--@click.stop.prevent阻止冒泡行為-->
<div class="content-right" @click.stop.prevent="pay">
<div class="pay" :class="payClass">
{{payDesc}}
</div>
</div>
</div>
<div class="ball-container">
<div v-for="ball in balls">
<transition name="drop">
<div class="ball" v-show="ball.show">
<div class="inner inner-hook"></div>
</div>
</transition>
</div>
</div>
<transition name="fold">
<div class="shopcart-list" v-show="listShow">
<div class="list-header">
<h1 class="title">購物車</h1>
<span class="empty" @click="empty">清空</span>
</div>
<div class="list-content" ref="listContent">
<ul>
<li class="food" v-for="food in selectFoods">
<span class="name">{{food.name}}</span>
<div class="price">
<span>¥{{food.price*food.count}}</span>
</div>
<div class="cartcontrol-wrapper">
<cartcontrol :food="food"></cartcontrol>
</div>
</li>
</ul>
</div>
</div>
</transition>
<transition name="fade">
<div class="list-mask" @click="hideList" v-show="listShow"></div>
</transition>
</div>
</template>
<script>
import BScroll from 'better-scroll';
import cartcontrol from '../../components/cartcontrol/cartcontrol';
export default {
name: 'v-shopcart',
props: {
selectFoods: {
type: Array,
default () {
return [{
price: 10,
count: 5
}];
}
},
deliveryPrice: {
type: Number,
default: 0
},
minPrice: {
type: Number,
default: 0
}
},
data() {
return {
balls: [{
show: false
},
{
show: false
},
{
show: false
},
{
show: false
},
{
show: false
}
],
dropBall: [],
fold: true
};
},
computed: {
totalPrice() {
let total = 0;
this.selectFoods.forEach((food) => {
total += food.price * food.count;
});
return total;
},
totalCount() {
let count = 0;
this.selectFoods.forEach((food) => {
count += food.count;
});
return count;
},
payDesc() {
if (this.totalPrice === 0) {
return `¥${this.minPrice}元起送`;
} else if (this.totalPrice < this.minPrice) {
let diff = this.minPrice - this.totalPrice;
return `還差¥${diff}元起送`;
} else {
return '去結算';
}
},
payClass() {
if (this.totalPrice < this.minPrice) {
return 'not-enough';
} else {
return 'enough';
}
},
listShow() {
if (!this.totalCount) {
this.fold = true;
return false;
}
let show = !this.fold;
if (show) {
this.$nextTick(() => {
if (!this.scroll) {
this.scroll = new BScroll(this.$refs.listContent, {
click: true
});
} else {
this.scroll.refresh();
}
});
}
return show;
}
},
methods: {
drop(el) {
for (let i = 0; i < this.balls.length; i++) {
let ball = this.balls[i];
if (!ball.show) {
ball.show = true;
ball.el = el;
this.dropBall.push(ball);
return;
}
}
},
toggleList() {
if (!this.totalCount) {
return;
}
this.fold = !this.fold;
},
hideList() {
this.fold = true;
},
empty() {
this.selectFoods.forEach((food) => {
food.count = 0;
});
},
pay() {
if (this.totalPrice < this.minPrice) {
return;
}
window.alert(`支付${this.totalPrice}元`);
}
},
transitions: {
drop: {
beforeDrop(el) {
let count = this.balls.length;
while (count--) {
let ball = this.balls[count];
if (ball.show) {
let rect = ball.el.getBoundingClientRect();
let x = rect.left - 32;
let y = -(window.innerHeight - rect.top - 22);
el.style.display = '';
el.style.webkitTransform = `translate3d(0,${y}px,0)`;
el.style.transform = `translate3d(0,${y}px,0)`;
let inner = el.getElementsByClassName('inner-hook')[0];
inner.style.webkitTransform = `translate3d(${x}px,0,0)`;
inner.style.transform = `translate3d(${x}px,0,0)`;
}
}
},
enter(el) {
/* eslint-disable no-unused-vars */
let rf = el.offsetHeight;
this.$nextTick(() => {
el.style.webkitTransform = 'translate3d(0,0,0)';
el.style.transform = 'translate3d(0,0,0)';
let inner = el.getElementsByClassName('inner-hook')[0];
inner.style.webkitTransform = 'translate3d(0,0,0)';
inner.style.transform = 'translate3d(0,0,0)';
el.addEventListener('transitionend', done);
});
},
afterEnter(el) {
let ball = this.dropBalls.shift();
if (ball) {
ball.show = false;
el.style.display = 'none';
}
}
}
},
components: {
cartcontrol
}
}
</script>
<style scoped lang="stylus" rel="stylesheet/stylus">
@import "../../common/stylus/mixin";
.shopcart
position: fixed
left: 0
bottom: 0
z-index: 50
width: 100%
height: 48px
.content
display: flex
background: #141d27
font-size: 0
.content-left
flex: 1
.logo-wrapper
display: inline-block
position: relative
top:-10px
margin: 0 12px
padding: 6px
width: 56px
height: 56px
vertical-align: top
box-sizing: border-box
border-radius: 50%
background: #141d27
.logo
width: 100%
height: 100%
border-radius: 50%
text-align: center
background: #2b343c
&.highlight
background: rgb(0,160,220)
.icon-shopping_cart
font-size: 24px
line-height: 44px
color: #80858a
&.highlight
color: #fff
.num
position: absolute
top: 0px
right: 0px
width: 24px
height: 16px
line-height: 16px
text-align: center
border-radius: 16px
font-size: 9px
font-weight: 700
color: #fff
background: rgb(240,20,20)
box-shadow: 0 4px 8px 0 rgba(0,0,0,0.4)
.price
display: inline-block
vertical-align: top
margin-top: 12px
line-height: 24px
padding-right: 12px
box-sizing: border-box
border-right: 1px solid rgba(255,255,255,0.1)
font-size: 16px
font-weight: 700
color: rgba(255,255,255,0.4)
&.highlight
color: #fff
.desc
display: inline-block
vertical-align: top
line-height: 24px
margin: 12px 0 0 12px
font-size: 10px
color: rgba(255,255,255,0.4)
.content-right
flex: 0 0 105px
width: 105px
.pay
height: 48px
line-height: 48px
text-align: center
font-size: 12px
color: rgba(255,255,255,0.4)
font-weight: 700
background: #2b333b
&.not-enough
background: #2b333b
&.enough
background: #00b43c
color: #fff
.ball-container
.ball
position: fixed
left: 32px
bottom: 22px
z-index: 200
transition: all 0.4s cubic-bezier(0.49, -0.29, 0.75, 0.41)
.inner
width: 16px
height: 16px
border-radius: 50%
background: rgb(0, 160, 220)
transition: all 0.4s linear
.shopcart-list
position: absolute
left: 0
top: 0
z-index: 50
width: 100%
transform: translate3d(0, -100%, 0)
&.fold-enter-active, &.fold-leave-active
transition: all 0.5s
&.fold-enter, &.fold-leave-active
transform: translate3d(0, 0, 0)
.list-header
height: 40px
line-height: 40px
padding: 0 18px
background: #f3f5f7
border-bottom: 1px solid rgba(7, 17, 27, 0.1)
.title
float: left
font-size: 14px
color: rgb(7, 17, 27)
.empty
float: right
font-size: 12px
color: rgb(0, 160, 220)
.list-content
padding: 0 18px
max-height: 217px
overflow: hidden
background: #fff
.food
position: relative
padding: 12px 0
box-sizing: border-box
border-1px(rgba(7, 17, 27, 0.1))
.name
line-height: 24px
font-size: 14px
color: rgb(7, 17, 27)
.price
position: absolute
right: 90px
bottom: 12px
line-height: 24px
font-size: 14px
font-weight: 700
color: rgb(240, 20, 20)
.cartcontrol-wrapper
position: absolute
right: 0
bottom: 6px
.list-mask
position: fixed
top: 0
left: 0
width: 100%
height: 100%
z-index: 40
backdrop-filter: blur(10px)
opacity: 1
background: rgba(7, 17, 27, 0.6)
&.fade-enter-active, &.fade-leave-active
transition: all 0.5s
&.fade-enter, &.fade-leave-active
opacity: 0
background: rgba(7, 17, 27, 0)
</style>
商品詳情頁實現(一)
步驟一:編寫goods.vue組件
<template>
<div>
<div class="goods">
<div class="menu-wrapper" ref="menuWrapper">
<ul>
<li v-for="(item,index) in goods" class="menu-item" :class="{'current':currentIndex===index}" @click="selectMenu(index,$event)">
<span class="text border-1px">
<span v-show="item.type>0" class="icon" :class="classMap[item.type]"></span> {{item.name}}
</span>
</li>
</ul>
</div>
<div class="foods-wrapper" ref="foodsWrapper">
<ul>
<li v-for="item in goods" class="food-list food-list-hook">
<h1 class="title">{{item.name}}</h1>
<ul>
<li @click="selectFood(food,$event)" v-for="food in item.foods" class="food-item border-1px">
<div class="icon">
<img :src="food.icon" width="57" height="57" />
</div>
<div class="content">
<h2 class="name">{{food.name}}</h2>
<p class="desc">{{food.description}}</p>
<div class="extra">
<span class="count">月售{{food.sellCount}}份</span>
<span>好評率{{food.rating}}%</span>
</div>
<div class="price">
<span class="now">¥{{food.price}}</span>
<span class="old" v-show="food.oldPrice">¥{{food.oldPrice}}</span>
</div>
<div class="cartcontrol-wrapper">
<cartcontrol :food="food"></cartcontrol>
</div>
</div>
</li>
</ul>
</li>
</ul>
</div>
<shopcart ref:shopcart :select-foods="selectFoods" :delivery-price="seller.deliveryPrice" :min-price="seller.minPrice"></shopcart>
</div>
<food :food="selectedFood"></food>
</div>
</template>
<script>
import BScroll from 'better-scroll';
import shopcart from '../../components/shopcart/shopcart';
import cartcontrol from '../../components/cartcontrol/cartcontrol';
import food from '../../components/food/food';
const ERR_OK = 0;
export default {
name: 'v-goods',
props: {
seller: {
type: Object
}
},
data() {
return {
goods: [],
listHeight: [],
scrollY: 0,
selectedFood: {}
};
},
computed: {
currentIndex() {
for (let i = 0; i < this.listHeight.length; i++) {
let height1 = this.listHeight[i];
let height2 = this.listHeight[i + 1];
if (!height2 || (this.scrollY >= height1 && this.scrollY < height2)) {
return i;
}
}
return 0;
},
selectFoods() {
let foods = [];
this.goods.forEach((good) => {
good.foods.forEach((food) => {
if (food.count) {
foods.push(food);
}
});
});
return foods;
}
},
created() {
this.classMap = ['decrease', 'discount', 'special', 'invoice', 'guarantee'];
this.$http.get('api/goods').then((response) => {
response = response.body;
if (response.error === ERR_OK) {
this.goods = response.data;
}
this.goods = response.data;
this.$nextTick(() => {
this._initScroll();
this._calculateHeight();
});
});
},
methods: {
selectMenu(index, event) {
if (!event._constructed) {
return;
}
let foodList = this.$refs.foodsWrapper.getElementsByClassName('food-list-hook');
let el = foodList[index];
this.foodsScroll.scrollToElement(el, 300);
console.log(index);
},
selectFood(food, event) {
if (!event._constructed) {
return;
}
this.selectedFood = food;
},
_drop(target) {
//體驗優化,異步執行下落動畫
this.$nextTick(() => {
this.$refs.shopcart.drop(target);
});
},
_initScroll() {
this.menuScroll = new BScroll(this.$refs.menuWrapper, {
click: true
});
this.foodsScroll = new BScroll(this.$refs.foodsWrapper, {
click: true,
probeType: 3
});
this.foodsScroll.on("scroll", (pos) => {
this.scrollY = Math.abs(Math.round(pos.y));
});
},
_calculateHeight() {
let foodList = this.$refs.foodsWrapper.getElementsByClassName('food-list-hook');
let height = 0;
this.listHeight.push(height);
for (let i = 0; i < foodList.length; i++) {
let item = foodList[i];
height += item.clientHeight;
this.listHeight.push(height);
}
}
},
components: {
shopcart,
cartcontrol,
food
},
events: {
'cart.add' (target) {
this._drop(target);
}
}
}
</script>
<style scoped lang="stylus" rel="stylesheet/stylus">
@import "../../common/stylus/mixin";
.goods
display: flex
position: absolute
top: 174px
bottom: 46px
width: 100%
overflow: hidden
.menu-wrapper
flex: 0 0 80px
width: 80px
background: #f3f5f7
.menu-item
display: table
height: 54px
width: 56px
padding: 0 12px
line-height: 14px
&.current
position: relative
z-index: 10
margin-top: -1px
background: #ccc
font-weight: 700
.text
border-none()
.icon
display: inline-block
vertical-align: top
width: 12px
height: 12px
margin-right: 2px
background-size: 12px 12px
background-repeat: no-repeat
&.decrease
bg-image('decrease_3')
&.discount
bg-image('discount_3')
&.guarantee
bg-image('guarantee_3')
&.invoice
bg-image('invoice_3')
&.special
bg-image('special_3')
.text
display: table-cell
width: 56px
vertical-align: middle
border-1px(rgba(7,17,27,0.1))
font-size: 12px
.foods-wrapper
flex: 1
.title
padding-left: 14px
height: 26px
line-height: 26px
border-left: 2px solid #d9dde1
font-size: 12px
color: rgb(147,153,159)
background: #f3f5f7
.food-item
display: flex
margin: 18px
padding-bottom: 18px
border-1px(rgba(7,17,27,0.1))
&:last-child
border-none()
margin-bottom: 0
.icon
flex: 0 0 57px
margin-right: 10px
.content
flex: 1
.name
margin: 2px 0 8px 0
height: 14px
line-height: 14px
font-size: 14px
color: rgb(7,17,27)
.desc, .extra
line-height: 10px
font-size: 10px
color: rgb(147,153,159)
.desc
margin-bottom: 8px
line-height: 14px
.extra
.count
margin-right: 12px
.price
font-weight: 700
line-height: 24px
.now
margin-right: 8px
font-size: 14px
color: rgb(240,20,20)
.old
text-decoration: line-through
font-size: 10px
color: rgb(147,153,159)
.cartcontrol-wrapper
position: absolute
right: 0
bottom: 12px
</style>
步驟二:新建food.vuez組件
<template> <div v-show="showFlag" class="food"></div> </template> <script> export default { name: 'v-food', props: { food: { type: Object } }, data() { return { showFlag: false }; } } </script> <style scoped lang="stylus" rel="stylesheet/stylus"> .food position: fixed left: 0 top: 0 bottom: 48px </style>
商品詳情實現(二)
步驟一:編寫goods.vue組件
<template>
<div>
<div class="goods">
<div class="menu-wrapper" ref="menuWrapper">
<ul>
<li v-for="(item,index) in goods" class="menu-item" :class="{'current':currentIndex===index}" @click="selectMenu(index,$event)">
<span class="text border-1px">
<span v-show="item.type>0" class="icon" :class="classMap[item.type]"></span> {{item.name}}
</span>
</li>
</ul>
</div>
<div class="foods-wrapper" ref="foodsWrapper">
<ul>
<li v-for="item in goods" class="food-list food-list-hook">
<h1 class="title">{{item.name}}</h1>
<ul>
<li @click="selectFood(food,$event)" v-for="food in item.foods" class="food-item border-1px">
<div class="icon">
<img :src="food.icon" width="57" height="57" />
</div>
<div class="content">
<h2 class="name">{{food.name}}</h2>
<p class="desc">{{food.description}}</p>
<div class="extra">
<span class="count">月售{{food.sellCount}}份</span>
<span>好評率{{food.rating}}%</span>
</div>
<div class="price">
<span class="now">¥{{food.price}}</span>
<span class="old" v-show="food.oldPrice">¥{{food.oldPrice}}</span>
</div>
<div class="cartcontrol-wrapper">
<cartcontrol :food="food"></cartcontrol>
</div>
</div>
</li>
</ul>
</li>
</ul>
</div>
<shopcart ref:shopcart :select-foods="selectFoods" :delivery-price="seller.deliveryPrice" :min-price="seller.minPrice"></shopcart>
</div>
<food :food="selectedFood" ref="food"></food>
</div>
</template>
<script>
import BScroll from 'better-scroll';
import shopcart from '../../components/shopcart/shopcart';
import cartcontrol from '../../components/cartcontrol/cartcontrol';
import food from '../../components/food/food';
const ERR_OK = 0;
export default {
name: 'v-goods',
props: {
seller: {
type: Object
}
},
data() {
return {
goods: [],
listHeight: [],
scrollY: 0,
selectedFood: {}
};
},
computed: {
currentIndex() {
for (let i = 0; i < this.listHeight.length; i++) {
let height1 = this.listHeight[i];
let height2 = this.listHeight[i + 1];
if (!height2 || (this.scrollY >= height1 && this.scrollY < height2)) {
return i;
}
}
return 0;
},
selectFoods() {
let foods = [];
this.goods.forEach((good) => {
good.foods.forEach((food) => {
if (food.count) {
foods.push(food);
}
});
});
return foods;
}
},
created() {
this.classMap = ['decrease', 'discount', 'special', 'invoice', 'guarantee'];
this.$http.get('api/goods').then((response) => {
response = response.body;
if (response.error === ERR_OK) {
this.goods = response.data;
}
this.goods = response.data;
this.$nextTick(() => {
this._initScroll();
this._calculateHeight();
});
});
},
methods: {
selectMenu(index, event) {
if (!event._constructed) {
return;
}
let foodList = this.$refs.foodsWrapper.getElementsByClassName('food-list-hook');
let el = foodList[index];
this.foodsScroll.scrollToElement(el, 300);
console.log(index);
},
selectFood(food, event) {
if (!event._constructed) {
return;
}
this.selectedFood = food;
this.$refs.food.show();
},
_drop(target) {
//體驗優化,異步執行下落動畫
this.$nextTick(() => {
this.$refs.shopcart.drop(target);
});
},
_initScroll() {
this.menuScroll = new BScroll(this.$refs.menuWrapper, {
click: true
});
this.foodsScroll = new BScroll(this.$refs.foodsWrapper, {
click: true,
probeType: 3
});
this.foodsScroll.on("scroll", (pos) => {
this.scrollY = Math.abs(Math.round(pos.y));
});
},
_calculateHeight() {
let foodList = this.$refs.foodsWrapper.getElementsByClassName('food-list-hook');
let height = 0;
this.listHeight.push(height);
for (let i = 0; i < foodList.length; i++) {
let item = foodList[i];
height += item.clientHeight;
this.listHeight.push(height);
}
}
},
components: {
shopcart,
cartcontrol,
food
},
events: {
'cart.add' (target) {
this._drop(target);
}
}
}
</script>
<style scoped lang="stylus" rel="stylesheet/stylus">
@import "../../common/stylus/mixin";
.goods
display: flex
position: absolute
top: 174px
bottom: 46px
width: 100%
overflow: hidden
.menu-wrapper
flex: 0 0 80px
width: 80px
background: #f3f5f7
.menu-item
display: table
height: 54px
width: 56px
padding: 0 12px
line-height: 14px
&.current
position: relative
z-index: 10
margin-top: -1px
background: #ccc
font-weight: 700
.text
border-none()
.icon
display: inline-block
vertical-align: top
width: 12px
height: 12px
margin-right: 2px
background-size: 12px 12px
background-repeat: no-repeat
&.decrease
bg-image('decrease_3')
&.discount
bg-image('discount_3')
&.guarantee
bg-image('guarantee_3')
&.invoice
bg-image('invoice_3')
&.special
bg-image('special_3')
.text
display: table-cell
width: 56px
vertical-align: middle
border-1px(rgba(7,17,27,0.1))
font-size: 12px
.foods-wrapper
flex: 1
.title
padding-left: 14px
height: 26px
line-height: 26px
border-left: 2px solid #d9dde1
font-size: 12px
color: rgb(147,153,159)
background: #f3f5f7
.food-item
display: flex
margin: 18px
padding-bottom: 18px
border-1px(rgba(7,17,27,0.1))
&:last-child
border-none()
margin-bottom: 0
.icon
flex: 0 0 57px
margin-right: 10px
.content
flex: 1
.name
margin: 2px 0 8px 0
height: 14px
line-height: 14px
font-size: 14px
color: rgb(7,17,27)
.desc, .extra
line-height: 10px
font-size: 10px
color: rgb(147,153,159)
.desc
margin-bottom: 8px
line-height: 14px
.extra
.count
margin-right: 12px
.price
font-weight: 700
line-height: 24px
.now
margin-right: 8px
font-size: 14px
color: rgb(240,20,20)
.old
text-decoration: line-through
font-size: 10px
color: rgb(147,153,159)
.cartcontrol-wrapper
position: absolute
right: 0
bottom: 12px
</style>
步驟二:編寫food.vue組件,實現切換動畫效果
<template>
<transition name="move">
<div v-show="showFlag" class="food">
<div class="food-content">
<div class="image-header">
<img :src="food.image" />
</div>
</div>
</div>
</transition>
</template>
<script>
export default {
name: 'v-food',
props: {
food: {
type: Object
}
},
data() {
return {
showFlag: false
};
},
methods: {
show() {
this.showFlag = true;
}
}
}
</script>
<style scoped lang="stylus" rel="stylesheet/stylus">
.food
position: fixed
left: 0
top: 0
bottom: 48px
z-index: 30px
width: 100%
background: #fff
transform: translate3d(0, 0, 0)
&.move-enter-active, &.move-leave-active
transition: all 0.2s linear
&.move-enter, &.move-leave-active
transform: translate3d(100%, 0, 0)
</style>
商品詳情實現(三)
編寫food.vue組件,實現后退效果以及部分內容展示
<template>
<transition name="move">
<div v-show="showFlag" class="food">
<div class="food-content">
<div class="image-header">
<img :src="food.image" />
<div class="back" @click="hide">
<i class="icon-arrow_lift"></i>
</div>
</div>
<div class="content">
<h1 class="title">{{food.name}}</h1>
<div class="detail">
<span class="sell-count">月售{{food.sellCount}}份</span>
<span class="rating">好評率{{food.rating}}</span>
</div>
<div class="price">
<span class="now">¥{{food.price}}</span>
<span class="old" v-show="food.oldPrice">¥{{food.oldPrice}}</span>
</div>
</div>
</div>
</div>
</transition>
</template>
<script>
export default {
name: 'v-food',
props: {
food: {
type: Object
}
},
data() {
return {
showFlag: false
};
},
methods: {
show() {
this.showFlag = true;
},
hide() {
this.showFlag = false;
}
}
}
</script>
<style scoped lang="stylus" rel="stylesheet/stylus">
.food
position: fixed
left: 0
top: 0
bottom: 48px
z-index: 30px
width: 100%
background: #fff
transform: translate3d(0, 0, 0)
&.move-enter-active, &.move-leave-active
transition: all 0.2s linear
&.move-enter, &.move-leave-active
transform: translate3d(100%, 0, 0)
.image-header
position: relative
width: 100%
height: 0
padding-top: 100%
img
position: absolute
top: 0
left: 0
width: 100%
height: 100%
.back
position: absolute
top: 10px
left: 0
.icon-arrow_lift
display: block
padding: 10px
font-size: 20px
color: #fff
</style>
商品詳情實現(四)
編寫food.vue組件,增加cartcontrol.vue組件
<template>
<transition name="move">
<div v-show="showFlag" class="food" ref="food">
<div class="food-content">
<div class="image-header">
<img :src="food.image" />
<div class="back" @click="hide">
<i class="icon-arrow_lift"></i>
</div>
</div>
<div class="content">
<h1 class="title">{{food.name}}</h1>
<div class="detail">
<span class="sell-count">月售{{food.sellCount}}份</span>
<span class="rating">好評率{{food.rating}}%</span>
</div>
<div class="price">
<span class="now">¥{{food.price}}</span>
<span class="old" v-show="food.oldPrice">¥{{food.oldPrice}}</span>
</div>
</div>
<div class="cartcontrol-wrapper">
<cartcontrol :food="food"></cartcontrol>
</div>
</div>
</div>
</transition>
</template>
<script>
import BScroll from 'better-scroll';
import cartcontrol from '../../components/cartcontrol/cartcontrol';
export default {
name: 'v-food',
props: {
food: {
type: Object
}
},
data() {
return {
showFlag: false
};
},
methods: {
show() {
this.showFlag = true;
this.$nextTick(() => {
if (!this.scroll) {
this.scroll = new BScroll(this.$refs.food, {
click: true
});
} else {
this.scroll.refresh();
}
});
},
hide() {
this.showFlag = false;
}
},
components: {
cartcontrol
}
}
</script>
<style scoped lang="stylus" rel="stylesheet/stylus">
.food
position: fixed
left: 0
top: 0
bottom: 48px
z-index: 30px
width: 100%
background: #fff
transform: translate3d(0, 0, 0)
&.move-enter-active, &.move-leave-active
transition: all 0.2s linear
&.move-enter, &.move-leave-active
transform: translate3d(100%, 0, 0)
.image-header
position: relative
width: 100%
height: 0
padding-top: 100%
img
position: absolute
top: 0
left: 0
width: 100%
height: 100%
.back
position: absolute
top: 10px
left: 0
.icon-arrow_lift
display: block
padding: 10px
font-size: 20px
color: #fff
.content
padding: 18px
.title
line-height: 14px
margin-bottom: 8px
font-size: 14px
font-weight: 700
color: rgb(7, 17, 27)
.detail
margin-bottom: 18px
line-height: 10px
height: 10px
font-size: 0
.sell-count, .rating
font-size: 10px
color: rgb(147, 153, 159)
.sell-count
margin-right: 12px
.price
font-weight: 700
line-height: 24px
.now
margin-right: 8px
font-size: 14px
color: rgb(240, 20, 20)
.old
text-decoration: line-through
font-size: 10px
color: rgb(147, 153, 159)
</style>
商品詳情實現(五)
步驟一:編寫food.vue組件,實現添加購物車按鈕(動畫效果依然無法實現)
<template>
<transition name="move">
<div v-show="showFlag" class="food" ref="food">
<div class="food-content">
<div class="image-header">
<img :src="food.image" />
<div class="back" @click="hide">
<i class="icon-arrow_lift"></i>
</div>
</div>
<div class="content">
<h1 class="title">{{food.name}}</h1>
<div class="detail">
<span class="sell-count">月售{{food.sellCount}}份</span>
<span class="rating">好評率{{food.rating}}%</span>
</div>
<div class="price">
<span class="now">¥{{food.price}}</span>
<span class="old" v-show="food.oldPrice">¥{{food.oldPrice}}</span>
</div>
</div>
<div class="cartcontrol-wrapper">
<cartcontrol :food="food"></cartcontrol>
</div>
<transition name="fade">
<div @click.stop.prevent="addFirst" class="buy" v-show="!food.count || food.count===0">
加入購物車
</div>
</transition>
</div>
</div>
</transition>
</template>
<script>
import BScroll from 'better-scroll';
import Vue from 'vue';
import cartcontrol from '../../components/cartcontrol/cartcontrol';
export default {
name: 'v-food',
props: {
food: {
type: Object
}
},
data() {
return {
showFlag: false
};
},
methods: {
show() {
this.showFlag = true;
this.$nextTick(() => {
if (!this.scroll) {
this.scroll = new BScroll(this.$refs.food, {
click: true
});
} else {
this.scroll.refresh();
}
});
},
hide() {
this.showFlag = false;
},
addFirst(event) {
if(!event._constructed) {
return;
}
console.log(event.target);
this.$emit('add', event.target);
Vue.set(this.food, 'count', 1);
}
},
components: {
cartcontrol
}
}
</script>
<style scoped lang="stylus" rel="stylesheet/stylus">
.food
position: fixed
left: 0
top: 0
bottom: 48px
z-index: 30px
width: 100%
background: #fff
transform: translate3d(0, 0, 0)
&.move-enter-active, &.move-leave-active
transition: all 0.2s linear
&.move-enter, &.move-leave-active
transform: translate3d(100%, 0, 0)
.image-header
position: relative
width: 100%
height: 0
padding-top: 100%
img
position: absolute
top: 0
left: 0
width: 100%
height: 100%
.back
position: absolute
top: 10px
left: 0
.icon-arrow_lift
display: block
padding: 10px
font-size: 20px
color: #fff
.content
padding: 18px
.title
line-height: 14px
margin-bottom: 8px
font-size: 14px
font-weight: 700
color: rgb(7, 17, 27)
.detail
margin-bottom: 18px
line-height: 10px
height: 10px
font-size: 0
.sell-count, .rating
font-size: 10px
color: rgb(147, 153, 159)
.sell-count
margin-right: 12px
.price
font-weight: 700
line-height: 24px
.now
margin-right: 8px
font-size: 14px
color: rgb(240, 20, 20)
.old
text-decoration: line-through
font-size: 10px
color: rgb(147, 153, 159)
.cartcontrol-wrapper
position: absolute
right: 12px
bottom: 12px
.buy
position: absolute
right: 18px
bottom: 18px
z-index: 10
height: 24px
line-height: 24px
padding: 0 12px
box-sizing: border-box
border-radius: 12px
font-size: 10px
color: #fff
background: rgb(0, 160, 220)
opacity: 1
&.fade-enter-active, &.fade-leave-active
transition: all 0.2s
&.fade-enter, &.fade-leave-active
opacity: 0
z-index: -1
</style>
步驟二:編寫cartcontrol.vue組件,阻止點擊事件冒泡行為
<template>
<div class="cartcontrol">
<transition name="move">
<div class="cart-decrease" v-show="food.count>0" @click.stop.prevent="decreaseCart">
<span class="inner icon-remove_circle_outline"></span>
</div>
</transition>
<div class="cart-count" v-show="food.count>0">{{food.count}}</div>
<div class="cart-add icon-add_circle" @click.stop.prevent="addCart"></div>
</div>
</template>
<script>
import Vue from 'vue';
export default {
name: 'v-cartcontrol',
props: {
food: {
type: Object
}
},
methods: {
addCart(event) {
if (!event._constructed) {
return;
}
if (!this.food.count) {
Vue.set(this.food, 'count', 1);
} else {
this.food.count++;
}
},
decreaseCart(event) {
if (!event._constructed) {
return;
}
if (this.food.count) {
this.food.count--;
}
}
}
}
</script>
<style scoped lang="stylus" rel="stylesheet/stylus">
.cartcontrol
font-size: 0
.cart-decrease
padding: 6px
color: rgb(0,160,220)
display: inline-block
opacity: 1
transform: translate3d(0, 0, 0)
.inner
display: inline-block
line-height: 24px
font-size: 24px
color: rgb(0, 160, 220)
transition: all 0.4s linear
transform: rotate(0)
&.move-enter-active, &.move-leave-active
transition: all 0.4s linear
&.move-enter, &.move-leave-active
opacity: 0
transform: translate3d(24px, 0, 0)
.inner
transform: rotate(180deg)
.cart-count
display: inline-block
vertical-align: top
width: 12px
padding-top: 6px
line-height: 24px
text-align: center
font-size: 10px
color: rgb(147,153,159)
.cart-add
display: inline-block
padding: 6px
line-height: 24px
font-size: 24px
color: rgb(0, 160, 220)
</style>
split組件實現
步驟一:新建split.vue組件
<template> <div class="split"></div> </template> <script> export default {}; </script> <style lang="stylus" rel="stylesheet/stylus"> .split width: 100% height: 16px border-top: 1px solid rgba(7, 17, 27, 0.1) border-bottom: 1px solid rgba(7, 17, 27, 0.1) /*background: #f3f5f7*/ background: #ccc </style>
步驟二:編寫food.vue組件,繼續添加內容和添加split.vue組件
<template>
<transition name="move">
<div v-show="showFlag" class="food" ref="food">
<div class="food-content">
<div class="image-header">
<img :src="food.image" />
<div class="back" @click="hide">
<i class="icon-arrow_lift"></i>
</div>
</div>
<div class="content">
<h1 class="title">{{food.name}}</h1>
<div class="detail">
<span class="sell-count">月售{{food.sellCount}}份</span>
<span class="rating">好評率{{food.rating}}%</span>
</div>
<div class="price">
<span class="now">¥{{food.price}}</span>
<span class="old" v-show="food.oldPrice">¥{{food.oldPrice}}</span>
</div>
<div class="cartcontrol-wrapper">
<cartcontrol :food="food"></cartcontrol>
</div>
<transition name="fade">
<div @click.stop.prevent="addFirst" class="buy" v-show="!food.count || food.count===0">
加入購物車
</div>
</transition>
</div>
<split></split>
<div class="info" v-show="food.info">
<h1 class="title">商品信息</h1>
<p class="text">{{food.info}}</p>
</div>
</div>
</div>
</transition>
</template>
<script>
import BScroll from 'better-scroll';
import Vue from 'vue';
import cartcontrol from '../../components/cartcontrol/cartcontrol';
import split from '../../components/split/split';
export default {
name: 'v-food',
props: {
food: {
type: Object
}
},
data() {
return {
showFlag: false
};
},
methods: {
show() {
this.showFlag = true;
this.$nextTick(() => {
if (!this.scroll) {
this.scroll = new BScroll(this.$refs.food, {
click: true
});
} else {
this.scroll.refresh();
}
});
},
hide() {
this.showFlag = false;
},
addFirst(event) {
if(!event._constructed) {
return;
}
this.$emit('add', event.target);
Vue.set(this.food, 'count', 1);
}
},
components: {
cartcontrol,
split
}
}
</script>
<style scoped lang="stylus" rel="stylesheet/stylus">
.food
position: fixed
left: 0
top: 0
bottom: 48px
z-index: 30px
width: 100%
background: #fff
transform: translate3d(0, 0, 0)
&.move-enter-active, &.move-leave-active
transition: all 0.2s linear
&.move-enter, &.move-leave-active
transform: translate3d(100%, 0, 0)
.image-header
position: relative
width: 100%
height: 0
padding-top: 100%
img
position: absolute
top: 0
left: 0
width: 100%
height: 100%
.back
position: absolute
top: 10px
left: 0
.icon-arrow_lift
display: block
padding: 10px
font-size: 20px
color: #fff
.content
position: relative
padding: 18px
.title
line-height: 14px
margin-bottom: 8px
font-size: 14px
font-weight: 700
color: rgb(7, 17, 27)
.detail
margin-bottom: 18px
line-height: 10px
height: 10px
font-size: 0
.sell-count, .rating
font-size: 10px
color: rgb(147, 153, 159)
.sell-count
margin-right: 12px
.price
font-weight: 700
line-height: 24px
.now
margin-right: 8px
font-size: 14px
color: rgb(240, 20, 20)
.old
text-decoration: line-through
font-size: 10px
color: rgb(147, 153, 159)
.cartcontrol-wrapper
position: absolute
right: 12px
bottom: 12px
.buy
position: absolute
right: 18px
bottom: 18px
z-index: 10
height: 24px
line-height: 24px
padding: 0 12px
box-sizing: border-box
border-radius: 12px
font-size: 10px
color: #fff
background: rgb(0, 160, 220)
opacity: 1
&.fade-enter-active, &.fade-leave-active
transition: all 0.2s
&.fade-enter, &.fade-leave-active
opacity: 0
z-index: -1
.info
padding: 18px
.title
line-height: 14px
margin-bottom: 6px
font-size: 14px
font-weight: 700
color: rgb(7, 17, 27)
.text
line-height: 24px
padding: 0 8px
font-size: 12px
color: rgb(77, 85, 93)
</style>
ratingselect組件(一)
步驟一:新建ratingselect.vue組件
<template>
<div class="ratingselect">
<div class="rating-type">
<span>{{desc.all}}</span>
<span>{{desc.positive}}</span>
<span>{{desc.negative}}</span>
</div>
<div class="switch">
<span class="icon-check_circle"></span>
<span class="text">只看有內容的評價</span>
</div>
</div>
</template>
<script>
const POSITIVE = 0;
const NEGATIVE = 1;
const ALL = 2;
export default {
props: {
ratings: {
type: Array,
default () {
return [];
}
},
selectType: {
type: Number,
default: ALL
},
onlyContent: {
type: Boolean,
default: false
},
desc: {
type: Object,
default () {
return {
all: '全部',
positive: '滿意',
negative: '不滿意'
};
}
}
}
};
</script>
<style lang="stylus" rel="stylesheet/stylus">
</style>
步驟二:編寫food.vue組件,將ratingselect.vue組件加載
<template>
<transition name="move">
<div v-show="showFlag" class="food" ref="food">
<div class="food-content">
<div class="image-header">
<img :src="food.image" />
<div class="back" @click="hide">
<i class="icon-arrow_lift"></i>
</div>
</div>
<div class="content">
<h1 class="title">{{food.name}}</h1>
<div class="detail">
<span class="sell-count">月售{{food.sellCount}}份</span>
<span class="rating">好評率{{food.rating}}%</span>
</div>
<div class="price">
<span class="now">¥{{food.price}}</span>
<span class="old" v-show="food.oldPrice">¥{{food.oldPrice}}</span>
</div>
<div class="cartcontrol-wrapper">
<cartcontrol :food="food"></cartcontrol>
</div>
<transition name="fade">
<div @click.stop.prevent="addFirst" class="buy" v-show="!food.count || food.count===0">
加入購物車
</div>
</transition>
</div>
<split v-show="food.info"></split>
<div class="info" v-show="food.info">
<h1 class="title">商品信息</h1>
<p class="text">{{food.info}}</p>
</div>
<split></split>
<div class="rating">
<h1 class="title">商品評價</h1>
<ratingselect></ratingselect>
</div>
</div>
</div>
</transition>
</template>
<script>
import BScroll from 'better-scroll';
import Vue from 'vue';
import cartcontrol from '../../components/cartcontrol/cartcontrol';
import split from '../../components/split/split';
import ratingselect from '../../components/ratingselect/ratingselect';
export default {
name: 'v-food',
props: {
food: {
type: Object
}
},
data() {
return {
showFlag: false
};
},
methods: {
show() {
this.showFlag = true;
this.$nextTick(() => {
if (!this.scroll) {
this.scroll = new BScroll(this.$refs.food, {
click: true
});
} else {
this.scroll.refresh();
}
});
},
hide() {
this.showFlag = false;
},
addFirst(event) {
if(!event._constructed) {
return;
}
this.$emit('add', event.target);
Vue.set(this.food, 'count', 1);
}
},
components: {
cartcontrol,
split,
ratingselect
}
}
</script>
<style scoped lang="stylus" rel="stylesheet/stylus">
.food
position: fixed
left: 0
top: 0
bottom: 48px
z-index: 30px
width: 100%
background: #fff
transform: translate3d(0, 0, 0)
&.move-enter-active, &.move-leave-active
transition: all 0.2s linear
&.move-enter, &.move-leave-active
transform: translate3d(100%, 0, 0)
.image-header
position: relative
width: 100%
height: 0
padding-top: 100%
img
position: absolute
top: 0
left: 0
width: 100%
height: 100%
.back
position: absolute
top: 10px
left: 0
.icon-arrow_lift
display: block
padding: 10px
font-size: 20px
color: #fff
.content
position: relative
padding: 18px
.title
line-height: 14px
margin-bottom: 8px
font-size: 14px
font-weight: 700
color: rgb(7, 17, 27)
.detail
margin-bottom: 18px
line-height: 10px
height: 10px
font-size: 0
.sell-count, .rating
font-size: 10px
color: rgb(147, 153, 159)
.sell-count
margin-right: 12px
.price
font-weight: 700
line-height: 24px
.now
margin-right: 8px
font-size: 14px
color: rgb(240, 20, 20)
.old
text-decoration: line-through
font-size: 10px
color: rgb(147, 153, 159)
.cartcontrol-wrapper
position: absolute
right: 12px
bottom: 12px
.buy
position: absolute
right: 18px
bottom: 18px
z-index: 10
height: 24px
line-height: 24px
padding: 0 12px
box-sizing: border-box
border-radius: 12px
font-size: 10px
color: #fff
background: rgb(0, 160, 220)
opacity: 1
&.fade-enter-active, &.fade-leave-active
transition: all 0.2s
&.fade-enter, &.fade-leave-active
opacity: 0
z-index: -1
.info
padding: 18px
.title
line-height: 14px
margin-bottom: 6px
font-size: 14px
font-weight: 700
color: rgb(7, 17, 27)
.text
line-height: 24px
padding: 0 8px
font-size: 12px
color: rgb(77, 85, 93)
</style>
ratingselect組件(二)
編寫food.vue組件,實現部分ratingselect.vue效果
<template>
<transition name="move">
<div v-show="showFlag" class="food" ref="food">
<div class="food-content">
<div class="image-header">
<img :src="food.image" />
<div class="back" @click="hide">
<i class="icon-arrow_lift"></i>
</div>
</div>
<div class="content">
<h1 class="title">{{food.name}}</h1>
<div class="detail">
<span class="sell-count">月售{{food.sellCount}}份</span>
<span class="rating">好評率{{food.rating}}%</span>
</div>
<div class="price">
<span class="now">¥{{food.price}}</span>
<span class="old" v-show="food.oldPrice">¥{{food.oldPrice}}</span>
</div>
<div class="cartcontrol-wrapper">
<cartcontrol :food="food"></cartcontrol>
</div>
<transition name="fade">
<div @click.stop.prevent="addFirst" class="buy" v-show="!food.count || food.count===0">
加入購物車
</div>
</transition>
</div>
<split v-show="food.info"></split>
<div class="info" v-show="food.info">
<h1 class="title">商品信息</h1>
<p class="text">{{food.info}}</p>
</div>
<split></split>
<div class="rating">
<h1 class="title">商品評價</h1>
<ratingselect :select-type="selectType" :only-content="onlyContent" :desc="desc" :ratings="food.ratings"></ratingselect>
</div>
</div>
</div>
</transition>
</template>
<script>
import BScroll from 'better-scroll';
import Vue from 'vue';
import cartcontrol from '../../components/cartcontrol/cartcontrol';
import split from '../../components/split/split';
import ratingselect from '../../components/ratingselect/ratingselect';
const POSITIVE = 0;
const NEGATIVE = 1;
const ALL = 2;
export default {
name: 'v-food',
props: {
food: {
type: Object
}
},
data() {
return {
showFlag: false,
selectType: ALL,
onlyContent: true,
desc: {
all: '全部',
positive: '推薦',
negative: '吐槽'
}
};
},
methods: {
show() {
this.showFlag = true;
this.selectType = ALL;
this.onlyContent = true;
this.$nextTick(() => {
if (!this.scroll) {
this.scroll = new BScroll(this.$refs.food, {
click: true
});
} else {
this.scroll.refresh();
}
});
},
hide() {
this.showFlag = false;
},
addFirst(event) {
if (!event._constructed) {
return;
}
this.$emit('add', event.target);
Vue.set(this.food, 'count', 1);
}
},
components: {
cartcontrol,
split,
ratingselect
}
}
</script>
<style scoped lang="stylus" rel="stylesheet/stylus">
.food
position: fixed
left: 0
top: 0
bottom: 48px
z-index: 30px
width: 100%
background: #fff
transform: translate3d(0, 0, 0)
&.move-enter-active, &.move-leave-active
transition: all 0.2s linear
&.move-enter, &.move-leave-active
transform: translate3d(100%, 0, 0)
.image-header
position: relative
width: 100%
height: 0
padding-top: 100%
img
position: absolute
top: 0
left: 0
width: 100%
height: 100%
.back
position: absolute
top: 10px
left: 0
.icon-arrow_lift
display: block
padding: 10px
font-size: 20px
color: #fff
.content
position: relative
padding: 18px
.title
line-height: 14px
margin-bottom: 8px
font-size: 14px
font-weight: 700
color: rgb(7, 17, 27)
.detail
margin-bottom: 18px
line-height: 10px
height: 10px
font-size: 0
.sell-count, .rating
font-size: 10px
color: rgb(147, 153, 159)
.sell-count
margin-right: 12px
.price
font-weight: 700
line-height: 24px
.now
margin-right: 8px
font-size: 14px
color: rgb(240, 20, 20)
.old
text-decoration: line-through
font-size: 10px
color: rgb(147, 153, 159)
.cartcontrol-wrapper
position: absolute
right: 12px
bottom: 12px
.buy
position: absolute
right: 18px
bottom: 18px
z-index: 10
height: 24px
line-height: 24px
padding: 0 12px
box-sizing: border-box
border-radius: 12px
font-size: 10px
color: #fff
background: rgb(0, 160, 220)
opacity: 1
&.fade-enter-active, &.fade-leave-active
transition: all 0.2s
&.fade-enter, &.fade-leave-active
opacity: 0
z-index: -1
.info
padding: 18px
.title
line-height: 14px
margin-bottom: 6px
font-size: 14px
font-weight: 700
color: rgb(7, 17, 27)
.text
line-height: 24px
padding: 0 8px
font-size: 12px
color: rgb(77, 85, 93)
.rating
padding-top: 18px
.title
line-height: 14px
margin-left: 18px
font-size: 14px
color: rgb(7, 17, 27)
</style>
ratingselect組件(三)
繼續編寫ratingselect.vue組件,實現部分樣式和動態切換
<template>
<div class="ratingselect">
<div class="rating-type border-1px">
<span class="block positive" :class="{'active':selectType===2}">
{{desc.all}}
<span class="count">47</span>
</span>
<span class="block positive" :class="{'active':selectType===0}">
{{desc.positive}}
<span class="count">40</span>
</span>
<span class="block negative" :class="{'active':selectType===1}">
{{desc.negative}}
<span class="count">10</span>
</span>
</div>
<div class="switch">
<span class="icon-check_circle"></span>
<span class="text">只看有內容的評價</span>
</div>
</div>
</template>
<script>
const POSITIVE = 0;
const NEGATIVE = 1;
const ALL = 2;
export default {
props: {
ratings: {
type: Array,
default () {
return [];
}
},
selectType: {
type: Number,
default: ALL
},
onlyContent: {
type: Boolean,
default: false
},
desc: {
type: Object,
default () {
return {
all: '全部',
positive: '滿意',
negative: '不滿意'
};
}
}
}
};
</script>
<style lang="stylus" rel="stylesheet/stylus">
.ratingselect
.rating-type
padding: 18px 0
margin: 0 18px
border-1px(rgba(7, 17, 27, 0.1))
font-size: 0
.block
display: inline-block
padding: 8px 12px
margin-right: 8px
line-height: 16px
border-radius: 1px
font-size: 12px
color: rgb(77, 85, 93)
&.active
color: #fff
.count
margin-left: 2px
font-size: 8px
&.positive
background: rgba(0, 160, 220, 0.2)
&.active
background: rgb(0, 160, 220)
&.negative
background: rgba(77, 85, 93, 0.2)
&.active
background: rgb(77, 85, 93)
</style>
ratingselect組件(四)
繼續編寫ratingselect.vue組件,進一步實現樣式
<template>
<div class="ratingselect">
<div class="rating-type border-1px">
<span class="block positive" :class="{'active':selectType===2}">
{{desc.all}}
<span class="count">47</span>
</span>
<span class="block positive" :class="{'active':selectType===0}">
{{desc.positive}}
<span class="count">40</span>
</span>
<span class="block negative" :class="{'active':selectType===1}">
{{desc.negative}}
<span class="count">10</span>
</span>
</div>
<div class="switch" :class="{'on':onlyContent}">
<span class="icon-check_circle"></span>
<span class="text">只看有內容的評價</span>
</div>
</div>
</template>
<script>
const POSITIVE = 0;
const NEGATIVE = 1;
const ALL = 2;
export default {
props: {
ratings: {
type: Array,
default () {
return [];
}
},
selectType: {
type: Number,
default: ALL
},
onlyContent: {
type: Boolean,
default: false
},
desc: {
type: Object,
default () {
return {
all: '全部',
positive: '滿意',
negative: '不滿意'
};
}
}
}
};
</script>
<style lang="stylus" rel="stylesheet/stylus">
.ratingselect
.rating-type
padding: 18px 0
margin: 0 18px
border-1px(rgba(7, 17, 27, 0.1))
font-size: 0
.block
display: inline-block
padding: 8px 12px
margin-right: 8px
line-height: 16px
border-radius: 1px
font-size: 12px
color: rgb(77, 85, 93)
&.active
color: #fff
.count
margin-left: 2px
font-size: 8px
&.positive
background: rgba(0, 160, 220, 0.2)
&.active
background: rgb(0, 160, 220)
&.negative
background: rgba(77, 85, 93, 0.2)
&.active
background: rgb(77, 85, 93)
.switch
padding: 12px 18px
line-height: 24px
border-bottom: 1px solid rgba(7, 17, 27, 0.1)
color: rgb(147, 153, 159)
font-size: 0
&.on
.icon-check_circle
color: #00c850
.icon-check_circle
display: inline-block
vertical-align: top
margin-right: 4px
font-size: 24px
.text
display: inline-block
vertical-align: top
font-size: 12px
</style>
ratingselect組件(五)
繼續編寫ratingselect.vue組件,完成全部切換效果(點擊效果無效)
<template>
<div class="ratingselect">
<div class="rating-type border-1px">
<span @click="select(2,$event)" class="block positive" :class="{'active':selectType===2}">{{desc.all}}<span
class="count">{{ratings.length}}</span></span>
<span @click="select(0,$event)" class="block positive" :class="{'active':selectType===0}">{{desc.positive}}<span
class="count">{{positives.length}}</span></span>
<span @click="select(1,$event)" class="block negative" :class="{'active':selectType===1}">{{desc.negative}}<span
class="count">{{negatives.length}}</span></span>
</div>
<div @click="toggleContent" class="switch" :class="{'on':onlyContent}">
<span class="icon-check_circle"></span>
<span class="text">只看有內容的評價</span>
</div>
</div>
</template>
<script>
const POSITIVE = 0;
const NEGATIVE = 1;
const ALL = 2;
export default {
props: {
ratings: {
type: Array,
default() {
return [];
}
},
selectType: {
type: Number,
default: ALL
},
onlyContent: {
type: Boolean,
default: false
},
desc: {
type: Object,
default() {
return {
all: '全部',
positive: '滿意',
negative: '不滿意'
};
}
}
},
computed: {
positives() {
return this.ratings.filter((rating) => {
return rating.rateType === POSITIVE;
});
},
negatives() {
return this.ratings.filter((rating) => {
return rating.rateType === NEGATIVE;
});
}
},
methods: {
select(type, event) {
if (!event._constructed) {
return;
}
this.$emit('select', type);
},
toggleContent(event) {
if (!event._constructed) {
return;
}
this.$emit('toggle');
}
}
};
</script>
<style lang="stylus" rel="stylesheet/stylus">
@import "../../common/stylus/mixin.styl"
.ratingselect
.rating-type
padding: 18px 0
margin: 0 18px
border-1px(rgba(7, 17, 27, 0.1))
font-size: 0
.block
display: inline-block
padding: 8px 12px
margin-right: 8px
line-height: 16px
border-radius: 1px
font-size: 12px
color: rgb(77, 85, 93)
&.active
color: #fff
.count
margin-left: 2px
font-size: 8px
&.positive
background: rgba(0, 160, 220, 0.2)
&.active
background: rgb(0, 160, 220)
&.negative
background: rgba(77, 85, 93, 0.2)
&.active
background: rgb(77, 85, 93)
.switch
padding: 12px 18px
line-height: 24px
border-bottom: 1px solid rgba(7, 17, 27, 0.1)
color: rgb(147, 153, 159)
font-size: 0
&.on
.icon-check_circle
color: #00c850
.icon-check_circle
display: inline-block
vertical-align: top
margin-right: 4px
font-size: 24px
.text
display: inline-block
vertical-align: top
font-size: 12px
</style>
評價列表(一)
編寫好food.vue組件的評價結構
<template>
<transition name="move">
<div v-show="showFlag" class="food" ref="food">
<div class="food-content">
<div class="image-header">
<img :src="food.image" />
<div class="back" @click="hide">
<i class="icon-arrow_lift"></i>
</div>
</div>
<div class="content">
<h1 class="title">{{food.name}}</h1>
<div class="detail">
<span class="sell-count">月售{{food.sellCount}}份</span>
<span class="rating">好評率{{food.rating}}%</span>
</div>
<div class="price">
<span class="now">¥{{food.price}}</span>
<span class="old" v-show="food.oldPrice">¥{{food.oldPrice}}</span>
</div>
<div class="cartcontrol-wrapper">
<cartcontrol :food="food"></cartcontrol>
</div>
<transition name="fade">
<div @click.stop.prevent="addFirst" class="buy" v-show="!food.count || food.count===0">
加入購物車
</div>
</transition>
</div>
<split v-show="food.info"></split>
<div class="info" v-show="food.info">
<h1 class="title">商品信息</h1>
<p class="text">{{food.info}}</p>
</div>
<split></split>
<div class="rating">
<h1 class="title">商品評價</h1>
<ratingselect :select-type="selectType" :only-content="onlyContent" :desc="desc" :ratings="food.ratings"></ratingselect>
<div class="rating-wrapper">
<ul v-show="food.ratings && food.ratings.length">
<li v-for="rating in food.ratings" class="rating-item">
<div class="user">
<span class="name">{{rating.username}}</span>
<img class="avatar" width="12" height="12" :src="rating.avatar" />
</div>
<div class="time">{{rating.rateTime}}</div>
<p class="text">
<span :class="{'icon-thumb_up':rating.rateType===0,'icon-thumb_down':rating.rateType===1}"></span> {{rating.text}}
</p>
</li>
</ul>
<div class="no-rating" v-show="!food.ratings || !food.ratings.length"></div>
</div>
</div>
</div>
</div>
</transition>
</template>
<script>
import BScroll from 'better-scroll';
import Vue from 'vue';
import cartcontrol from '../../components/cartcontrol/cartcontrol';
import split from '../../components/split/split';
import ratingselect from '../../components/ratingselect/ratingselect';
const POSITIVE = 0;
const NEGATIVE = 1;
const ALL = 2;
export default {
name: 'v-food',
props: {
food: {
type: Object
}
},
data() {
return {
showFlag: false,
selectType: ALL,
onlyContent: true,
desc: {
all: '全部',
positive: '推薦',
negative: '吐槽'
}
};
},
methods: {
show() {
this.showFlag = true;
this.selectType = ALL;
this.onlyContent = true;
this.$nextTick(() => {
if (!this.scroll) {
this.scroll = new BScroll(this.$refs.food, {
click: true
});
} else {
this.scroll.refresh();
}
});
},
hide() {
this.showFlag = false;
},
addFirst(event) {
if (!event._constructed) {
return;
}
this.$emit('add', event.target);
Vue.set(this.food, 'count', 1);
}
},
components: {
cartcontrol,
split,
ratingselect
}
}
</script>
<style scoped lang="stylus" rel="stylesheet/stylus">
.food
position: fixed
left: 0
top: 0
bottom: 48px
z-index: 30px
width: 100%
background: #fff
transform: translate3d(0, 0, 0)
&.move-enter-active, &.move-leave-active
transition: all 0.2s linear
&.move-enter, &.move-leave-active
transform: translate3d(100%, 0, 0)
.image-header
position: relative
width: 100%
height: 0
padding-top: 100%
img
position: absolute
top: 0
left: 0
width: 100%
height: 100%
.back
position: absolute
top: 10px
left: 0
.icon-arrow_lift
display: block
padding: 10px
font-size: 20px
color: #fff
.content
position: relative
padding: 18px
.title
line-height: 14px
margin-bottom: 8px
font-size: 14px
font-weight: 700
color: rgb(7, 17, 27)
.detail
margin-bottom: 18px
line-height: 10px
height: 10px
font-size: 0
.sell-count, .rating
font-size: 10px
color: rgb(147, 153, 159)
.sell-count
margin-right: 12px
.price
font-weight: 700
line-height: 24px
.now
margin-right: 8px
font-size: 14px
color: rgb(240, 20, 20)
.old
text-decoration: line-through
font-size: 10px
color: rgb(147, 153, 159)
.cartcontrol-wrapper
position: absolute
right: 12px
bottom: 12px
.buy
position: absolute
right: 18px
bottom: 18px
z-index: 10
height: 24px
line-height: 24px
padding: 0 12px
box-sizing: border-box
border-radius: 12px
font-size: 10px
color: #fff
background: rgb(0, 160, 220)
opacity: 1
&.fade-enter-active, &.fade-leave-active
transition: all 0.2s
&.fade-enter, &.fade-leave-active
opacity: 0
z-index: -1
.info
padding: 18px
.title
line-height: 14px
margin-bottom: 6px
font-size: 14px
font-weight: 700
color: rgb(7, 17, 27)
.text
line-height: 24px
padding: 0 8px
font-size: 12px
color: rgb(77, 85, 93)
.rating
padding-top: 18px
.title
line-height: 14px
margin-left: 18px
font-size: 14px
color: rgb(7, 17, 27)
</style>
評論列表(二)
繼續編寫food.vue組件的評論區樣式
<template> <transition name="move"> <div v-show="showFlag" class="food" ref="food"> <div class="food-content"> <div class="image-header"> <img :src="food.image" /> <div class="back" @click="hide"> <i class="icon-arrow_lift"></i> </div> </div> <div class="content"> <h1 class="title">{{food.name}}</h1> <div class="detail"> <span class="sell-count">月售{{food.sellCount}}份</span> <span class="rating">好評率{{food.rating}}%</span> </div> <div class="price"> <span class="now">¥{{food.price}}</span> <span class="old" v-show="food.oldPrice">¥{{food.oldPrice}}</span> </div> <div class="cartcontrol-wrapper"> <cartcontrol :food="food"></cartcontrol> </div> <transition name="fade"> <div @click.stop.prevent="addFirst" class="buy" v-show="!food.count || food.count===0"> 加入購物車 </div> </transition> </div> <split v-show="food.info"></split> <div class="info" v-show="food.info"> <h1 class="title">商品信息</h1> <p class="text">{{food.info}}</p> </div> <split></split> <div class="rating"> <h1 class="title">商品評價</h1> <ratingselect :select-type="selectType" :only-content="onlyContent" :desc="desc" :ratings="food.ratings"></ratingselect> <div class="rating-wrapper"> <ul v-show="food.ratings && food.ratings.length"> <li v-for="rating in food.ratings" class="rating-item border-1px"> <div class="user"> <span class="name">{{rating.username}}</span> <img class="avatar" width="12" height="12" :src="rating.avatar" /> </div> <div class="time">{{rating.rateTime}}</div> <p class="text"> <span :class="{'icon-thumb_up':rating.rateType===0,'icon-thumb_down':rating.rateType===1}"></span> {{rating.text}} </p> </li> </ul> <div class="no-rating" v-show="!food.ratings || !food.ratings.length"></div> </div> </div> </div> </div> </transition> </template> <script> import BScroll from 'better-scroll'; import Vue from 'vue'; import cartcontrol from '../../components/cartcontrol/cartcontrol'; import split from '../../components/split/split'; import ratingselect from '../../components/ratingselect/ratingselect'; const POSITIVE = 0; const NEGATIVE = 1; const ALL = 2; export default { name: 'v-food', props: { food: { type: Object } }, data() { return { showFlag: false, selectType: ALL, onlyContent: true, desc: { all: '全部', positive: '推薦', negative: '吐槽' } }; }, methods: { show() { this.showFlag = true; this.selectType = ALL; this.onlyContent = true; this.$nextTick(() => { if (!this.scroll) { this.scroll = new BScroll(this.$refs.food, { click: true }); } else { this.scroll.refresh(); } }); }, hide() { this.showFlag = false; }, addFirst(event) { if (!event._constructed) { return; } this.$emit('add', event.target); Vue.set(this.food, 'count', 1); } }, components: { cartcontrol, split, ratingselect } } </script> <style scoped lang="stylus" rel="stylesheet/stylus"> @import "../../common/stylus/mixin.styl" .food position: fixed left: 0 top: 0 bottom: 48px z-index: 30px width: 100% background: #fff transform: translate3d(0, 0, 0) &.move-enter-active, &.move-leave-active transition: all 0.2s linear &.move-enter, &.move-leave-active transform: translate3d(100%, 0, 0) .image-header position: relative width: 100% height: 0 padding-top: 100% img position: absolute top: 0 left: 0 width: 100% height: 100% .back position: absolute top: 10px left: 0 .icon-arrow_lift display: block padding: 10px font-size: 20px color: #fff .content position: relative padding: 18px .title line-height: 14px margin-bottom: 8px font-size: 14px font-weight: 700 color: rgb(7, 17, 27) .detail margin-bottom: 18px line-height: 10px height: 10px font-size: 0 .sell-count, .rating font-size: 10px color: rgb(147, 153, 159) .sell-count margin-right: 12px .price font-weight: 700 line-height: 24px .now margin-right: 8px font-size: 14px color: rgb(240, 20, 20) .old text-decoration: line-through font-size: 10px color: rgb(147, 153, 159) .cartcontrol-wrapper position: absolute right: 12px bottom: 12px .buy position: absolute right: 18px bottom: 18px z-index: 10 height: 24px line-height: 24px padding: 0 12px box-sizing: border-box border-radius: 12px font-size: 10px color: #fff background: rgb(0, 160, 220) opacity: 1 &.fade-enter-active, &.fade-leave-active transition: all 0.2s &.fade-enter, &.fade-leave-active opacity: 0 z-index: -1 .info padding: 18px .title line-height: 14px margin-bottom: 6px font-size: 14px font-weight: 700 color: rgb(7, 17, 27) .text line-height: 24px padding: 0 8px font-size: 12px color: rgb(77, 85, 93) .rating padding-top: 18px .title line-height: 14px margin-left: 18px font-size: 14px color: rgb(7, 17, 27) .rating-wrapper padding: 0 18px .rating-item position: relative padding: 16px 0 border-1px(rgba(7, 17, 27, 0.1)) .user position: absolute right: 0 top: 16px line-height: 12px font-size: 0 .name display: inline-block margin-right: 6px vertical-align: top font-size: 10px color: rgb(147, 153, 159) .avatar border-radius: 50% .time margin-bottom: 6px line-height: 12px font-size: 10px color: rgb(147, 153, 159) .text line-height: 16px font-size: 12px color: rgb(7, 17, 27) .icon-thumb_up, .icon-thumb_down margin-right: 4px line-height: 16px font-size: 12px .icon-thumb_up color: rgb(0, 160, 220) .icon-thumb_down color: rgb(147, 153, 159) .no-rating padding: 16px 0 font-size: 12px color: rgb(147, 153, 159) </style>
評論列表(三)
繼續編寫food.vue組件,實現評價切換效果(失敗)
<template> <transition name="move"> <div v-show="showFlag" class="food" ref="food"> <div class="food-content"> <div class="image-header"> <img :src="food.image" /> <div class="back" @click="hide"> <i class="icon-arrow_lift"></i> </div> </div> <div class="content"> <h1 class="title">{{food.name}}</h1> <div class="detail"> <span class="sell-count">月售{{food.sellCount}}份</span> <span class="rating">好評率{{food.rating}}%</span> </div> <div class="price"> <span class="now">¥{{food.price}}</span> <span class="old" v-show="food.oldPrice">¥{{food.oldPrice}}</span> </div> <div class="cartcontrol-wrapper"> <cartcontrol :food="food"></cartcontrol> </div> <transition name="fade"> <div @click.stop.prevent="addFirst" class="buy" v-show="!food.count || food.count===0"> 加入購物車 </div> </transition> </div> <split v-show="food.info"></split> <div class="info" v-show="food.info"> <h1 class="title">商品信息</h1> <p class="text">{{food.info}}</p> </div> <split></split> <div class="rating"> <h1 class="title">商品評價</h1> <ratingselect :select-type="selectType" :only-content="onlyContent" :desc="desc" :ratings="food.ratings"></ratingselect> <div class="rating-wrapper"> <ul v-show="food.ratings && food.ratings.length"> <li v-show="needShow(rating.rateType,rating.text)" v-for="rating in food.ratings" class="rating-item border-1px"> <div class="user"> <span class="name">{{rating.username}}</span> <img class="avatar" width="12" height="12" :src="rating.avatar" /> </div> <div class="time">{{rating.rateTime}}</div> <p class="text"> <span :class="{'icon-thumb_up':rating.rateType===0,'icon-thumb_down':rating.rateType===1}"></span> {{rating.text}} </p> </li> </ul> <div class="no-rating" v-show="!food.ratings || !food.ratings.length"></div> </div> </div> </div> </div> </transition> </template> <script> import BScroll from 'better-scroll'; import Vue from 'vue'; import cartcontrol from '../../components/cartcontrol/cartcontrol'; import split from '../../components/split/split'; import ratingselect from '../../components/ratingselect/ratingselect'; const POSITIVE = 0; const NEGATIVE = 1; const ALL = 2; export default { name: 'v-food', props: { food: { type: Object } }, data() { return { showFlag: false, selectType: ALL, onlyContent: true, desc: { all: '全部', positive: '推薦', negative: '吐槽' } }; }, methods: { show() { this.showFlag = true; this.selectType = ALL; this.onlyContent = true; this.$nextTick(() => { if (!this.scroll) { this.scroll = new BScroll(this.$refs.food, { click: true }); } else { this.scroll.refresh(); } }); }, hide() { this.showFlag = false; }, addFirst(event) { if (!event._constructed) { return; } this.$emit('add', event.target); Vue.set(this.food, 'count', 1); }, needShow(type, text) { if (this.onlyContent && !text) { return false; } if (this.selectType === ALL) { return true; } else { return type === this.selectType; } } }, events: { 'ratingtype.select'(type) { this.selectType = type; this.$nextTick(() => { this.scroll.refresh(); }); }, 'content.toggle'(onlyContent) { this.onlyContent = onlyContent; this.$nextTick(() => { this.scroll.refresh(); }); } }, components: { cartcontrol, split, ratingselect } } </script> <style scoped lang="stylus" rel="stylesheet/stylus"> @import "../../common/stylus/mixin.styl" .food position: fixed left: 0 top: 0 bottom: 48px z-index: 30px width: 100% background: #fff transform: translate3d(0, 0, 0) &.move-enter-active, &.move-leave-active transition: all 0.2s linear &.move-enter, &.move-leave-active transform: translate3d(100%, 0, 0) .image-header position: relative width: 100% height: 0 padding-top: 100% img position: absolute top: 0 left: 0 width: 100% height: 100% .back position: absolute top: 10px left: 0 .icon-arrow_lift display: block padding: 10px font-size: 20px color: #fff .content position: relative padding: 18px .title line-height: 14px margin-bottom: 8px font-size: 14px font-weight: 700 color: rgb(7, 17, 27) .detail margin-bottom: 18px line-height: 10px height: 10px font-size: 0 .sell-count, .rating font-size: 10px color: rgb(147, 153, 159) .sell-count margin-right: 12px .price font-weight: 700 line-height: 24px .now margin-right: 8px font-size: 14px color: rgb(240, 20, 20) .old text-decoration: line-through font-size: 10px color: rgb(147, 153, 159) .cartcontrol-wrapper position: absolute right: 12px bottom: 12px .buy position: absolute right: 18px bottom: 18px z-index: 10 height: 24px line-height: 24px padding: 0 12px box-sizing: border-box border-radius: 12px font-size: 10px color: #fff background: rgb(0, 160, 220) opacity: 1 &.fade-enter-active, &.fade-leave-active transition: all 0.2s &.fade-enter, &.fade-leave-active opacity: 0 z-index: -1 .info padding: 18px .title line-height: 14px margin-bottom: 6px font-size: 14px font-weight: 700 color: rgb(7, 17, 27) .text line-height: 24px padding: 0 8px font-size: 12px color: rgb(77, 85, 93) .rating padding-top: 18px .title line-height: 14px margin-left: 18px font-size: 14px color: rgb(7, 17, 27) .rating-wrapper padding: 0 18px .rating-item position: relative padding: 16px 0 border-1px(rgba(7, 17, 27, 0.1)) .user position: absolute right: 0 top: 16px line-height: 12px font-size: 0 .name display: inline-block margin-right: 6px vertical-align: top font-size: 10px color: rgb(147, 153, 159) .avatar border-radius: 50% .time margin-bottom: 6px line-height: 12px font-size: 10px color: rgb(147, 153, 159) .text line-height: 16px font-size: 12px color: rgb(7, 17, 27) .icon-thumb_up, .icon-thumb_down margin-right: 4px line-height: 16px font-size: 12px .icon-thumb_up color: rgb(0, 160, 220) .icon-thumb_down color: rgb(147, 153, 159) .no-rating padding: 16px 0 font-size: 12px color: rgb(147, 153, 159) </style>
評論列表(四、五、六)
步驟一:編寫food.vue組件,創建一個時間過濾器
<template> <transition name="move"> <div v-show="showFlag" class="food" ref="food"> <div class="food-content"> <div class="image-header"> <img :src="food.image" /> <div class="back" @click="hide"> <i class="icon-arrow_lift"></i> </div> </div> <div class="content"> <h1 class="title">{{food.name}}</h1> <div class="detail"> <span class="sell-count">月售{{food.sellCount}}份</span> <span class="rating">好評率{{food.rating}}%</span> </div> <div class="price"> <span class="now">¥{{food.price}}</span> <span class="old" v-show="food.oldPrice">¥{{food.oldPrice}}</span> </div> <div class="cartcontrol-wrapper"> <cartcontrol :food="food"></cartcontrol> </div> <transition name="fade"> <div @click.stop.prevent="addFirst" class="buy" v-show="!food.count || food.count===0"> 加入購物車 </div> </transition> </div> <split v-show="food.info"></split> <div class="info" v-show="food.info"> <h1 class="title">商品信息</h1> <p class="text">{{food.info}}</p> </div> <split></split> <div class="rating"> <h1 class="title">商品評價</h1> <ratingselect :select-type="selectType" :only-content="onlyContent" :desc="desc" :ratings="food.ratings"></ratingselect> <div class="rating-wrapper"> <ul v-show="food.ratings && food.ratings.length"> <li v-show="needShow(rating.rateType,rating.text)" v-for="rating in food.ratings" class="rating-item border-1px"> <div class="user"> <span class="name">{{rating.username}}</span> <img class="avatar" width="12" height="12" :src="rating.avatar" /> </div> <div class="time">{{rating.rateTime | formatDate}}</div> <p class="text"> <span :class="{'icon-thumb_up':rating.rateType===0,'icon-thumb_down':rating.rateType===1}"></span> {{rating.text}} </p> </li> </ul> <div class="no-rating" v-show="!food.ratings || !food.ratings.length">暫無評價</div> </div> </div> </div> </div> </transition> </template> <script> import BScroll from 'better-scroll'; import Vue from 'vue'; import {formatDate} from '../../common/js/date'; import cartcontrol from '../../components/cartcontrol/cartcontrol'; import split from '../../components/split/split'; import ratingselect from '../../components/ratingselect/ratingselect'; const POSITIVE = 0; const NEGATIVE = 1; const ALL = 2; export default { name: 'v-food', props: { food: { type: Object } }, data() { return { showFlag: false, selectType: ALL, onlyContent: true, desc: { all: '全部', positive: '推薦', negative: '吐槽' } }; }, methods: { show() { this.showFlag = true; this.selectType = ALL; this.onlyContent = true; this.$nextTick(() => { if (!this.scroll) { this.scroll = new BScroll(this.$refs.food, { click: true }); } else { this.scroll.refresh(); } }); }, hide() { this.showFlag = false; }, addFirst(event) { if (!event._constructed) { return; } this.$emit('add', event.target); Vue.set(this.food, 'count', 1); }, needShow(type, text) { if (this.onlyContent && !text) { return false; } if (this.selectType === ALL) { return true; } else { return type === this.selectType; } } }, events: { 'ratingtype.select'(type) { this.selectType = type; this.$nextTick(() => { this.scroll.refresh(); }); }, 'content.toggle'(onlyContent) { this.onlyContent = onlyContent; this.$nextTick(() => { this.scroll.refresh(); }); } }, filters: { formatDate(time) { let date = new Date(time); return formatDate(date, 'yyyy-MM-dd hh:mm'); } }, components: { cartcontrol, split, ratingselect } } </script> <style scoped lang="stylus" rel="stylesheet/stylus"> @import "../../common/stylus/mixin.styl" .food position: fixed left: 0 top: 0 bottom: 48px z-index: 30px width: 100% background: #fff transform: translate3d(0, 0, 0) &.move-enter-active, &.move-leave-active transition: all 0.2s linear &.move-enter, &.move-leave-active transform: translate3d(100%, 0, 0) .image-header position: relative width: 100% height: 0 padding-top: 100% img position: absolute top: 0 left: 0 width: 100% height: 100% .back position: absolute top: 10px left: 0 .icon-arrow_lift display: block padding: 10px font-size: 20px color: #fff .content position: relative padding: 18px .title line-height: 14px margin-bottom: 8px font-size: 14px font-weight: 700 color: rgb(7, 17, 27) .detail margin-bottom: 18px line-height: 10px height: 10px font-size: 0 .sell-count, .rating font-size: 10px color: rgb(147, 153, 159) .sell-count margin-right: 12px .price font-weight: 700 line-height: 24px .now margin-right: 8px font-size: 14px color: rgb(240, 20, 20) .old text-decoration: line-through font-size: 10px color: rgb(147, 153, 159) .cartcontrol-wrapper position: absolute right: 12px bottom: 12px .buy position: absolute right: 18px bottom: 18px z-index: 10 height: 24px line-height: 24px padding: 0 12px box-sizing: border-box border-radius: 12px font-size: 10px color: #fff background: rgb(0, 160, 220) opacity: 1 &.fade-enter-active, &.fade-leave-active transition: all 0.2s &.fade-enter, &.fade-leave-active opacity: 0 z-index: -1 .info padding: 18px .title line-height: 14px margin-bottom: 6px font-size: 14px font-weight: 700 color: rgb(7, 17, 27) .text line-height: 24px padding: 0 8px font-size: 12px color: rgb(77, 85, 93) .rating padding-top: 18px .title line-height: 14px margin-left: 18px font-size: 14px color: rgb(7, 17, 27) .rating-wrapper padding: 0 18px .rating-item position: relative padding: 16px 0 border-1px(rgba(7, 17, 27, 0.1)) .user position: absolute right: 0 top: 16px line-height: 12px font-size: 0 .name display: inline-block margin-right: 6px vertical-align: top font-size: 10px color: rgb(147, 153, 159) .avatar border-radius: 50% .time margin-bottom: 6px line-height: 12px font-size: 10px color: rgb(147, 153, 159) .text line-height: 16px font-size: 12px color: rgb(7, 17, 27) .icon-thumb_up, .icon-thumb_down margin-right: 4px line-height: 16px font-size: 12px .icon-thumb_up color: rgb(0, 160, 220) .icon-thumb_down color: rgb(147, 153, 159) .no-rating padding: 16px 0 font-size: 12px color: rgb(147, 153, 159) </style>
步驟二:編寫公共的date.js文件,實現時間過濾的效果
export function formatDate(date, fmt) {
if (/(y+)/.test(fmt)) {
fmt = fmt.replace(RegExp.$1, (date.getFullYear() + '').substr(4 - RegExp.$1.length));
}
let o = {
'M+': date.getMonth() + 1,
'd+': date.getDate(),
'h+': date.getHours(),
'm+': date.getMinutes(),
's+': date.getSeconds()
};
for (let k in o) {
if (new RegExp(`(${k})`).test(fmt)) {
let str = o[k] + '';
fmt = fmt.replace(RegExp.$1, (RegExp.$1.length === 1) ? str : padLeftZero(str));
}
}
return fmt;
};
function padLeftZero(str) {
return ('00' + str).substr(str.length);
}
rating組件開發(一)
編寫ratings.vue組件,完成左邊部分樣式
<template> <div class="ratings"> <div class="ratings-content"> <div class="overview"> <div class="overview-left"> <h1 class="score">{{seller.score}}</h1> <div class="title">綜合評分</div> <div class="rank">高於周邊商家{{seller.rankRate}}%</div> </div> <div class="overview-right"></div> </div> </div> </div> </template> <script> export default { name: 'v-ratings', props: { seller: { type: Object } } } </script> <style scoped lang="stylus" rel="stylesheet/stylus"> .ratings position: absolute top: 174px bottom: 0 left: 0 width: 100% overflow: hidden .overview display: flex padding: 18px 0 .overview-left flex: 0 0 137px padding: 6px 0 width: 137px border-right: 1px solid rgba(7, 17, 27, 0.1) text-align: center @media only screen and (max-width: 320px) flex: 0 0 120px width: 120px .score margin-bottom: 6px line-height: 28px font-size: 24px color: rgb(255, 153, 0) .title margin-bottom: 8px line-height: 12px font-size: 12px color: rgb(7, 17, 27) .rank line-height: 10px font-size: 10px color: rgb(147, 153, 159) .overview-right flex: 1 padding: 6px 0 6px 24px </style>
rating組件開發(二)
繼續編寫ratings.vue組件,完成右邊部分樣式
<template> <div class="ratings"> <div class="ratings-content"> <div class="overview"> <div class="overview-left"> <h1 class="score">{{seller.score}}</h1> <div class="title">綜合評分</div> <div class="rank">高於周邊商家{{seller.rankRate}}%</div> </div> <div class="overview-right"> <div class="score-wrapper"> <span class="title">服務態度</span> <star :size="36" :score="seller.serviceScore"></star> <span class="score">{{seller.serviceScore}}</span> </div> <div class="score-wrapper"> <span class="title">商品評分</span> <star :size="36" :score="seller.foodScore"></star> <span class="score">{{seller.foodScore}}</span> </div> <div class="delivery-wrapper"> <span class="title">送達時間</span> <span class="delivery">{{seller.deliveryTime}}分鍾</span> </div> </div> </div> </div> </div> </template> <script> import star from '../../components/star/star'; export default { name: 'v-ratings', props: { seller: { type: Object } }, components: { star } } </script> <style scoped lang="stylus" rel="stylesheet/stylus"> .ratings position: absolute top: 174px bottom: 0 left: 0 width: 100% overflow: hidden .overview display: flex padding: 18px 0 .overview-left flex: 0 0 137px padding: 6px 0 width: 137px border-right: 1px solid rgba(7, 17, 27, 0.1) text-align: center @media only screen and (max-width: 320px) flex: 0 0 120px width: 120px .score margin-bottom: 6px line-height: 28px font-size: 24px color: rgb(255, 153, 0) .title margin-bottom: 8px line-height: 12px font-size: 12px color: rgb(7, 17, 27) .rank line-height: 10px font-size: 10px color: rgb(147, 153, 159) .overview-right flex: 1 padding: 6px 0 6px 24px @media only screen and (max-width: 320px) padding-left: 6px .score-wrapper margin-bottom: 8px font-size: 0 .title display: inline-block line-height: 18px vertical-align: top font-size: 12px color: rgb(7, 17, 27) .star display: inline-block margin: 0 12px vertical-align: top .score display: inline-block line-height: 18px vertical-align: top font-size: 12px color: rgb(255, 153, 0) .delivery-wrapper font-size: 0 .title line-height: 18px font-size: 12px color: rgb(7, 17, 27) .delivery margin-left: 12px font-size: 12px color: rgb(147, 153, 159) </style>
rating組件開發(三)
繼續編寫ratings.vue組件,利用media實現響應式功能
<template> <div class="ratings"> <div class="ratings-content"> <div class="overview"> <div class="overview-left"> <h1 class="score">{{seller.score}}</h1> <div class="title">綜合評分</div> <div class="rank">高於周邊商家{{seller.rankRate}}%</div> </div> <div class="overview-right"> <div class="score-wrapper"> <span class="title">服務態度</span> <star :size="36" :score="seller.serviceScore"></star> <span class="score">{{seller.serviceScore}}</span> </div> <div class="score-wrapper"> <span class="title">商品評分</span> <star :size="36" :score="seller.foodScore"></star> <span class="score">{{seller.foodScore}}</span> </div> <div class="delivery-wrapper"> <span class="title">送達時間</span> <span class="delivery">{{seller.deliveryTime}}分鍾</span> </div> </div> </div> </div> </div> </template> <script> import star from '../../components/star/star'; export default { name: 'v-ratings', props: { seller: { type: Object } }, components: { star } } </script> <style scoped lang="stylus" rel="stylesheet/stylus"> .ratings position: absolute top: 174px bottom: 0 left: 0 width: 100% overflow: hidden .overview display: flex padding: 18px 0 .overview-left flex: 0 0 137px padding: 6px 0 width: 137px border-right: 1px solid rgba(7, 17, 27, 0.1) text-align: center @media only screen and (max-width: 320px) flex: 0 0 120px width: 120px .score margin-bottom: 6px line-height: 28px font-size: 24px color: rgb(255, 153, 0) .title margin-bottom: 8px line-height: 12px font-size: 12px color: rgb(7, 17, 27) .rank line-height: 10px font-size: 10px color: rgb(147, 153, 159) .overview-right flex: 1 padding: 6px 0 6px 24px @media only screen and (max-width: 320px) padding-left: 6px .score-wrapper margin-bottom: 8px font-size: 0 .title display: inline-block line-height: 18px vertical-align: top font-size: 12px color: rgb(7, 17, 27) .star display: inline-block margin: 0 12px vertical-align: top .score display: inline-block line-height: 18px vertical-align: top font-size: 12px color: rgb(255, 153, 0) .delivery-wrapper font-size: 0 .title line-height: 18px font-size: 12px color: rgb(7, 17, 27) .delivery margin-left: 12px font-size: 12px color: rgb(147, 153, 159) </style>
rating組件開發(四)
繼續編寫ratings.vue組件,實現評論區內容展示
<template> <div class="ratings"> <div class="ratings-content"> <div class="overview"> <div class="overview-left"> <h1 class="score">{{seller.score}}</h1> <div class="title">綜合評分</div> <div class="rank">高於周邊商家{{seller.rankRate}}%</div> </div> <div class="overview-right"> <div class="score-wrapper"> <span class="title">服務態度</span> <star :size="36" :score="seller.serviceScore"></star> <span class="score">{{seller.serviceScore}}</span> </div> <div class="score-wrapper"> <span class="title">商品評分</span> <star :size="36" :score="seller.foodScore"></star> <span class="score">{{seller.foodScore}}</span> </div> <div class="delivery-wrapper"> <span class="title">送達時間</span> <span class="delivery">{{seller.deliveryTime}}分鍾</span> </div> </div> </div> <split></split> <ratingselect :selectType="selectType" :onlyContent="onlyContent" :ratings="ratings"></ratingselect> <div class="rating-wrapper"> <ul> <li v-for="rating in ratings" class="rating-item"> <div class="avatar"> <img width="28" height="28" :src="rating.avatar"> </div> <div class="content"> <h1 class="name">{{rating.username}}</h1> <div class="star-wrapper"> <star :size="24" :score="rating.score"></star> <span class="delivery" v-show="rating.deliveryTime">{{rating.deliveryTime}}</span> </div> <p class="text">{{rating.text}}</p> <div class="recommend" v-show="rating.recommend && rating.recommend.length"> <span class="icon-thumb_up"></span> <span class="item" v-for="item in rating.recommend">{{item}}</span> </div> <div class="time"> {{rating.rateTime}} </div> </div> </li> </ul> </div> </div> </div> </template> <script> import star from '../../components/star/star'; import split from '../../components/split/split'; import ratingselect from '../../components/ratingselect/ratingselect'; const ALL = 2; const ERR_OK = 0; export default { name: 'v-ratings', props: { seller: { type: Object } }, data() { return { ratings: [], selectType: ALL, onlyContent: true }; }, created() { this.$http.get('/api/ratings').then((response) => { response = response.body; if (response.errno === ERR_OK) { this.ratings = response.data; } }); }, components: { star, split, ratingselect } } </script> <style scoped lang="stylus" rel="stylesheet/stylus"> .ratings position: absolute top: 174px bottom: 0 left: 0 width: 100% overflow: hidden .overview display: flex padding: 18px 0 .overview-left flex: 0 0 137px padding: 6px 0 width: 137px border-right: 1px solid rgba(7, 17, 27, 0.1) text-align: center @media only screen and (max-width: 320px) flex: 0 0 120px width: 120px .score margin-bottom: 6px line-height: 28px font-size: 24px color: rgb(255, 153, 0) .title margin-bottom: 8px line-height: 12px font-size: 12px color: rgb(7, 17, 27) .rank line-height: 10px font-size: 10px color: rgb(147, 153, 159) .overview-right flex: 1 padding: 6px 0 6px 24px @media only screen and (max-width: 320px) padding-left: 6px .score-wrapper margin-bottom: 8px font-size: 0 .title display: inline-block line-height: 18px vertical-align: top font-size: 12px color: rgb(7, 17, 27) .star display: inline-block margin: 0 12px vertical-align: top .score display: inline-block line-height: 18px vertical-align: top font-size: 12px color: rgb(255, 153, 0) .delivery-wrapper font-size: 0 .title line-height: 18px font-size: 12px color: rgb(7, 17, 27) .delivery margin-left: 12px font-size: 12px color: rgb(147, 153, 159) </style>
rating組件開發(五)
繼續編寫ratings.vue組件,實現評論區滾動功能和相關樣式
<template> <div class="ratings" ref="ratings"> <div class="ratings-content"> <div class="overview"> <div class="overview-left"> <h1 class="score">{{seller.score}}</h1> <div class="title">綜合評分</div> <div class="rank">高於周邊商家{{seller.rankRate}}%</div> </div> <div class="overview-right"> <div class="score-wrapper"> <span class="title">服務態度</span> <star :size="36" :score="seller.serviceScore"></star> <span class="score">{{seller.serviceScore}}</span> </div> <div class="score-wrapper"> <span class="title">商品評分</span> <star :size="36" :score="seller.foodScore"></star> <span class="score">{{seller.foodScore}}</span> </div> <div class="delivery-wrapper"> <span class="title">送達時間</span> <span class="delivery">{{seller.deliveryTime}}分鍾</span> </div> </div> </div> <split></split> <ratingselect :selectType="selectType" :onlyContent="onlyContent" :ratings="ratings"></ratingselect> <div class="rating-wrapper"> <ul> <li v-for="rating in ratings" class="rating-item"> <div class="avatar"> <img width="28" height="28" :src="rating.avatar"> </div> <div class="content"> <h1 class="name">{{rating.username}}</h1> <div class="star-wrapper"> <star :size="24" :score="rating.score"></star> <span class="delivery" v-show="rating.deliveryTime">{{rating.deliveryTime}}</span> </div> <p class="text">{{rating.text}}</p> <div class="recommend" v-show="rating.recommend && rating.recommend.length"> <span class="icon-thumb_up"></span> <span class="item" v-for="item in rating.recommend">{{item}}</span> </div> <div class="time"> {{rating.rateTime | formatDate}} </div> </div> </li> </ul> </div> </div> </div> </template> <script> import BScroll from 'better-scroll'; import star from '../../components/star/star'; import split from '../../components/split/split'; import ratingselect from '../../components/ratingselect/ratingselect'; import { formatDate } from '../../common/js/date'; const ALL = 2; const ERR_OK = 0; export default { name: 'v-ratings', props: { seller: { type: Object } }, data() { return { ratings: [], selectType: ALL, onlyContent: true }; }, created() { this.$http.get('/api/ratings').then((response) => { response = response.body; if (response.errno === ERR_OK) { this.ratings = response.data; this.$nextTick(() => { this.scroll = new BScroll(this.$refs.ratings, { click: true }); }); } }); }, filters: { formatDate(time) { let date = new Date(time); return formatDate(date, 'yyyy-MM-dd hh:mm'); } }, components: { star, split, ratingselect } } </script> <style scoped lang="stylus" rel="stylesheet/stylus"> @import "../../common/stylus/mixin.styl" .ratings position: absolute top: 174px bottom: 0 left: 0 width: 100% overflow: hidden .overview display: flex padding: 18px 0 .overview-left flex: 0 0 137px padding: 6px 0 width: 137px border-right: 1px solid rgba(7, 17, 27, 0.1) text-align: center @media only screen and (max-width: 320px) flex: 0 0 120px width: 120px .score margin-bottom: 6px line-height: 28px font-size: 24px color: rgb(255, 153, 0) .title margin-bottom: 8px line-height: 12px font-size: 12px color: rgb(7, 17, 27) .rank line-height: 10px font-size: 10px color: rgb(147, 153, 159) .overview-right flex: 1 padding: 6px 0 6px 24px @media only screen and (max-width: 320px) padding-left: 6px .score-wrapper margin-bottom: 8px font-size: 0 .title display: inline-block line-height: 18px vertical-align: top font-size: 12px color: rgb(7, 17, 27) .star display: inline-block margin: 0 12px vertical-align: top .score display: inline-block line-height: 18px vertical-align: top font-size: 12px color: rgb(255, 153, 0) .delivery-wrapper font-size: 0 .title line-height: 18px font-size: 12px color: rgb(7, 17, 27) .delivery margin-left: 12px font-size: 12px color: rgb(147, 153, 159) .rating-wrapper padding: 0 18px .rating-item display: flex padding: 18px 0 border-1px(rgba(7, 17, 27, 0.1)) .avatar flex: 0 0 28px width: 28px margin-right: 12px img border-radius: 50% .content position: relative flex: 1 .name margin-bottom: 4px line-height: 12px font-size: 10px color: rgb(7, 17, 27) .star-wrapper margin-bottom: 6px font-size: 0 .star display: inline-block margin-right: 6px vertical-align: top .delivery display: inline-block vertical-align: top line-height: 12px font-size: 10px color: rgb(147, 153, 159) .text margin-bottom: 8px line-height: 18px color: rgb(7, 17, 27) font-size: 12px .recommend line-height: 16px font-size: 0 .icon-thumb_up, .item display: inline-block margin: 0 8px 4px 0 font-size: 9px .icon-thumb_up color: rgb(0, 160, 220) .item padding: 0 6px border: 1px solid rgba(7, 17, 27, 0.1) border-radius: 1px color: rgb(147, 153, 159) background: #fff .time position: absolute top: 0 right: 0 line-height: 12px font-size: 10px color: rgb(147, 153, 159) </style>
rating組件開發(六)
完善ratings.vue組件切換“滿意、不滿意”的效果(失敗)
<template> <div class="ratings" ref="ratings"> <div class="ratings-content"> <div class="overview"> <div class="overview-left"> <h1 class="score">{{seller.score}}</h1> <div class="title">綜合評分</div> <div class="rank">高於周邊商家{{seller.rankRate}}%</div> </div> <div class="overview-right"> <div class="score-wrapper"> <span class="title">服務態度</span> <star :size="36" :score="seller.serviceScore"></star> <span class="score">{{seller.serviceScore}}</span> </div> <div class="score-wrapper"> <span class="title">商品評分</span> <star :size="36" :score="seller.foodScore"></star> <span class="score">{{seller.foodScore}}</span> </div> <div class="delivery-wrapper"> <span class="title">送達時間</span> <span class="delivery">{{seller.deliveryTime}}分鍾</span> </div> </div> </div> <split></split> <ratingselect :selectType="selectType" :onlyContent="onlyContent" :ratings="ratings"></ratingselect> <div class="rating-wrapper"> <ul> <li v-for="rating in ratings" v-show="needShow(rating.rateType, rating.text)" class="rating-item"> <div class="avatar"> <img width="28" height="28" :src="rating.avatar"> </div> <div class="content"> <h1 class="name">{{rating.username}}</h1> <div class="star-wrapper"> <star :size="24" :score="rating.score"></star> <span class="delivery" v-show="rating.deliveryTime">{{rating.deliveryTime}}</span> </div> <p class="text">{{rating.text}}</p> <div class="recommend" v-show="rating.recommend && rating.recommend.length"> <span class="icon-thumb_up"></span> <span class="item" v-for="item in rating.recommend">{{item}}</span> </div> <div class="time"> {{rating.rateTime | formatDate}} </div> </div> </li> </ul> </div> </div> </div> </template> <script> import BScroll from 'better-scroll'; import star from '../../components/star/star'; import split from '../../components/split/split'; import ratingselect from '../../components/ratingselect/ratingselect'; import { formatDate } from '../../common/js/date'; const ALL = 2; const ERR_OK = 0; export default { name: 'v-ratings', props: { seller: { type: Object } }, data() { return { ratings: [], selectType: ALL, onlyContent: true }; }, created() { this.$http.get('/api/ratings').then((response) => { response = response.body; if (response.errno === ERR_OK) { this.ratings = response.data; this.$nextTick(() => { this.scroll = new BScroll(this.$refs.ratings, { click: true }); }); } }); }, events: { 'ratingtype.select'(type) { this.selectType = type; this.$nextTick(() => { this.scroll.refresh(); }); }, 'content.toggle'(onlyContent) { this.onlyContent = onlyContent; this.$nextTick(() => { this.scroll.refresh(); }); } }, filters: { formatDate(time) { let date = new Date(time); return formatDate(date, 'yyyy-MM-dd hh:mm'); } }, methods: { needShow(type, text) { if (this.onlyContent && !text) { return false; } if (this.selectType === ALL) { return true; } else { return type === this.selectType; } } }, components: { star, split, ratingselect } } </script> <style scoped lang="stylus" rel="stylesheet/stylus"> @import "../../common/stylus/mixin.styl" .ratings position: absolute top: 174px bottom: 0 left: 0 width: 100% overflow: hidden .overview display: flex padding: 18px 0 .overview-left flex: 0 0 137px padding: 6px 0 width: 137px border-right: 1px solid rgba(7, 17, 27, 0.1) text-align: center @media only screen and (max-width: 320px) flex: 0 0 120px width: 120px .score margin-bottom: 6px line-height: 28px font-size: 24px color: rgb(255, 153, 0) .title margin-bottom: 8px line-height: 12px font-size: 12px color: rgb(7, 17, 27) .rank line-height: 10px font-size: 10px color: rgb(147, 153, 159) .overview-right flex: 1 padding: 6px 0 6px 24px @media only screen and (max-width: 320px) padding-left: 6px .score-wrapper margin-bottom: 8px font-size: 0 .title display: inline-block line-height: 18px vertical-align: top font-size: 12px color: rgb(7, 17, 27) .star display: inline-block margin: 0 12px vertical-align: top .score display: inline-block line-height: 18px vertical-align: top font-size: 12px color: rgb(255, 153, 0) .delivery-wrapper font-size: 0 .title line-height: 18px font-size: 12px color: rgb(7, 17, 27) .delivery margin-left: 12px font-size: 12px color: rgb(147, 153, 159) .rating-wrapper padding: 0 18px .rating-item display: flex padding: 18px 0 border-1px(rgba(7, 17, 27, 0.1)) .avatar flex: 0 0 28px width: 28px margin-right: 12px img border-radius: 50% .content position: relative flex: 1 .name margin-bottom: 4px line-height: 12px font-size: 10px color: rgb(7, 17, 27) .star-wrapper margin-bottom: 6px font-size: 0 .star display: inline-block margin-right: 6px vertical-align: top .delivery display: inline-block vertical-align: top line-height: 12px font-size: 10px color: rgb(147, 153, 159) .text margin-bottom: 8px line-height: 18px color: rgb(7, 17, 27) font-size: 12px .recommend line-height: 16px font-size: 0 .icon-thumb_up, .item display: inline-block margin: 0 8px 4px 0 font-size: 9px .icon-thumb_up color: rgb(0, 160, 220) .item padding: 0 6px border: 1px solid rgba(7, 17, 27, 0.1) border-radius: 1px color: rgb(147, 153, 159) background: #fff .time position: absolute top: 0 right: 0 line-height: 12px font-size: 10px color: rgb(147, 153, 159) </style>
seller組件開發(一)
編寫seller.vue組件,建立初步的頭部結構
<template> <div class="seller"> <div class="seller-content"> <div class="overview"> <h1 class="title">{{seller.name}}</h1> <div class="desc"> <star :size="36" :score="seller.score"></star> <span class="text">({{seller.ratingCount}})</span> <span class="text">月售{{seller.sellCount}}單</span> </div> <ul class="remark"> <li class="block"> <h2>起送價</h2> <div class="content"> <span class="stress">{{seller.minPrice}}元</span> </div> </li> <li class="block"> <h2>商家配送</h2> <div class="content"> <span class="stress">{{seller.deliveryPrice}}元</span> </div> </li> <li class="block"> <h2>平均配送時間</h2> <div class="content"> <span class="stress">{{seller.deliveryTime}}分鍾</span> </div> </li> </ul> </div> </div> </div> </template> <script> import star from '../../components/star/star'; export default { name: 'v-seller', props: { seller: { type: Object } }, components: { star } } </script> <style scoped> </style>
seller組件開發(二)
編寫seller.vue組件,建立初步的頭部結構樣式
<template> <div class="seller"> <div class="seller-content"> <div class="overview"> <h1 class="title">{{seller.name}}</h1> <div class="desc border-1px"> <star :size="36" :score="seller.score"></star> <span class="text">({{seller.ratingCount}})</span> <span class="text">月售{{seller.sellCount}}單</span> </div> <ul class="remark"> <li class="block"> <h2>起送價</h2> <div class="content"> <span class="stress">{{seller.minPrice}}元</span> </div> </li> <li class="block"> <h2>商家配送</h2> <div class="content"> <span class="stress">{{seller.deliveryPrice}}元</span> </div> </li> <li class="block"> <h2>平均配送時間</h2> <div class="content"> <span class="stress">{{seller.deliveryTime}}分鍾</span> </div> </li> </ul> </div> </div> </div> </template> <script> import star from '../../components/star/star'; export default { name: 'v-seller', props: { seller: { type: Object } }, components: { star } } </script> <style lang="stylus" rel="stylesheet/stylus"> @import "../../common/stylus/mixin.styl" .seller position: absolute top: 174px bottom: 0 left: 0 width: 100% overflow: hidden .overview position: relative padding: 18px .title margin-bottom: 8px line-height: 14px color: rgb(7, 17, 27) font-size: 14px .desc padding-bottom: 18px border-1px(rgba(7, 17, 27, 0.1)) font-size: 0 .star display: inline-block margin-right: 8px vertical-align: top .text display: inline-block margin-right: 12px line-height: 18px vertical-align: top font-size: 10px color: rgb(77, 85, 93) .remark display: flex padding-top: 18px .block flex: 1 text-align: center border-right: 1px solid rgba(7, 17, 27, 0.1) &:last-child border: none h2 margin-bottom: 4px line-height: 10px font-size: 10px color: rgb(147, 153, 159) .content line-height: 24px font-size: 10px color: rgb(7, 17, 27) .stress font-size: 24px </style>
seller組件開發(三)
編寫seller.vue組件,公告與活動初次開發
<template> <div class="seller"> <div class="seller-content"> <div class="overview"> <h1 class="title">{{seller.name}}</h1> <div class="desc border-1px"> <star :size="36" :score="seller.score"></star> <span class="text">({{seller.ratingCount}})</span> <span class="text">月售{{seller.sellCount}}單</span> </div> <ul class="remark"> <li class="block"> <h2>起送價</h2> <div class="content"> <span class="stress">{{seller.minPrice}}</span>元 </div> </li> <li class="block"> <h2>商家配送</h2> <div class="content"> <span class="stress">{{seller.deliveryPrice}}</span>元 </div> </li> <li class="block"> <h2>平均配送時間</h2> <div class="content"> <span class="stress">{{seller.deliveryTime}}</span>分鍾 </div> </li> </ul> </div> <split></split> <div class="bulletin"> <h1 class="title">公告與活動</h1> <div class="content-wrapper border-1px"> <p class="content">{{seller.bulletin}}</p> </div> <ul v-if="seller.supports" class="supports"> <li class="support-item border-1px" v-for="(item,index) in seller.supports"> <span class="icon" :class="classMap[seller.supports[index].type]"></span> <span class="text">{{seller.supports[index].description}}</span> </li> </ul> </div> </div> </div> </template> <script> import star from '../../components/star/star'; import split from '../../components/split/split'; export default { name: 'v-seller', props: { seller: { type: Object } }, components: { star, split }, created() { this.classMap = ['decrease', 'discount', 'special', 'invoice', 'guarantee']; } } </script> <style lang="stylus" rel="stylesheet/stylus"> @import "../../common/stylus/mixin.styl" .seller position: absolute top: 174px bottom: 0 left: 0 width: 100% overflow: hidden .overview position: relative padding: 18px .title margin-bottom: 8px line-height: 14px color: rgb(7, 17, 27) font-size: 14px font-weight: 700 .desc padding-bottom: 18px border-1px(rgba(7, 17, 27, 0.1)) font-size: 0 .star display: inline-block margin-right: 8px vertical-align: top .text display: inline-block margin-right: 12px line-height: 18px vertical-align: top font-size: 10px color: rgb(77, 85, 93) .remark display: flex padding-top: 18px .block flex: 1 text-align: center border-right: 1px solid rgba(7, 17, 27, 0.1) &:last-child border: none h2 margin-bottom: 4px line-height: 10px font-size: 10px color: rgb(147, 153, 159) .content line-height: 24px font-size: 10px color: rgb(7, 17, 27) .stress font-size: 24px .bulletin padding: 18px 18px 0 18px .title margin-bottom: 8px line-height: 14px color: rgb(7, 17, 27) font-size: 14px .content-wrapper padding: 0 12px 16px 12px border-1px(rgba(7, 17, 27, 0.1)) .content line-height: 24px font-size: 12px color: rgb(240, 20, 20) </style>
seller組件開發(四)
編寫seller.vue組件,公告與活動二次開發,並且實現滾動功能
<template> <div class="seller" ref="seller"> <div class="seller-content"> <div class="overview"> <h1 class="title">{{seller.name}}</h1> <div class="desc border-1px"> <star :size="36" :score="seller.score"></star> <span class="text">({{seller.ratingCount}})</span> <span class="text">月售{{seller.sellCount}}單</span> </div> <ul class="remark"> <li class="block"> <h2>起送價</h2> <div class="content"> <span class="stress">{{seller.minPrice}}</span>元 </div> </li> <li class="block"> <h2>商家配送</h2> <div class="content"> <span class="stress">{{seller.deliveryPrice}}</span>元 </div> </li> <li class="block"> <h2>平均配送時間</h2> <div class="content"> <span class="stress">{{seller.deliveryTime}}</span>分鍾 </div> </li> </ul> </div> <split></split> <div class="bulletin"> <h1 class="title">公告與活動</h1> <div class="content-wrapper border-1px"> <p class="content">{{seller.bulletin}}</p> </div> <ul v-if="seller.supports" class="supports"> <li class="support-item border-1px" v-for="(item,index) in seller.supports"> <span class="icon" :class="classMap[seller.supports[index].type]"></span> <span class="text">{{seller.supports[index].description}}</span> </li> </ul> </div> </div> </div> </template> <script> import BScroll from 'better-scroll'; import star from '../../components/star/star'; import split from '../../components/split/split'; export default { name: 'v-seller', props: { seller: { type: Object } }, components: { star, split }, watch: { 'seller' () { this.$nextTick(() => { this._initScroll(); }); } }, methods: { _initScroll() { if (!this.scroll) { this.scroll = new BScroll(this.$refs.seller, { click: true }); } else { this.scroll.refresh(); } } }, created() { this.classMap = ['decrease', 'discount', 'special', 'invoice', 'guarantee']; } } </script> <style lang="stylus" rel="stylesheet/stylus"> @import "../../common/stylus/mixin.styl" .seller position: absolute top: 174px bottom: 0 left: 0 width: 100% overflow: hidden .overview position: relative padding: 18px .title margin-bottom: 8px line-height: 14px color: rgb(7, 17, 27) font-size: 14px font-weight: 700 .desc padding-bottom: 18px border-1px(rgba(7, 17, 27, 0.1)) font-size: 0 .star display: inline-block margin-right: 8px vertical-align: top .text display: inline-block margin-right: 12px line-height: 18px vertical-align: top font-size: 10px color: rgb(77, 85, 93) .remark display: flex padding-top: 18px .block flex: 1 text-align: center border-right: 1px solid rgba(7, 17, 27, 0.1) &:last-child border: none h2 margin-bottom: 4px line-height: 10px font-size: 10px color: rgb(147, 153, 159) .content line-height: 24px font-size: 10px color: rgb(7, 17, 27) .stress font-size: 24px .bulletin padding: 18px 18px 0 18px .title margin-bottom: 8px line-height: 14px color: rgb(7, 17, 27) font-size: 14px .content-wrapper padding: 0 12px 16px 12px border-1px(rgba(7, 17, 27, 0.1)) .content line-height: 24px font-size: 12px color: rgb(240, 20, 20) .supports .support-item padding: 16px 12px border-1px(rgba(7, 17, 27, 0.1)) font-size: 0 &:last-child border-none() .icon display: inline-block width: 16px height: 16px vertical-align: top margin-right: 6px background-size: 16px 16px background-repeat: no-repeat &.decrease bg-image('decrease_4') &.discount bg-image('discount_4') &.guarantee bg-image('guarantee_4') &.invoice bg-image('invoice_4') &.special bg-image('special_4') .text line-height: 16px font-size: 12px color: rgb(7, 17, 27) </style>
seller組件開發——商家實景圖(五)
編寫seller.vue組件,實現商家實景圖效果
<template> <div class="seller" ref="seller"> <div class="seller-content"> <div class="overview"> <h1 class="title">{{seller.name}}</h1> <div class="desc border-1px"> <star :size="36" :score="seller.score"></star> <span class="text">({{seller.ratingCount}})</span> <span class="text">月售{{seller.sellCount}}單</span> </div> <ul class="remark"> <li class="block"> <h2>起送價</h2> <div class="content"> <span class="stress">{{seller.minPrice}}</span>元 </div> </li> <li class="block"> <h2>商家配送</h2> <div class="content"> <span class="stress">{{seller.deliveryPrice}}</span>元 </div> </li> <li class="block"> <h2>平均配送時間</h2> <div class="content"> <span class="stress">{{seller.deliveryTime}}</span>分鍾 </div> </li> </ul> </div> <split></split> <div class="bulletin"> <h1 class="title">公告與活動</h1> <div class="content-wrapper border-1px"> <p class="content">{{seller.bulletin}}</p> </div> <ul v-if="seller.supports" class="supports"> <li class="support-item border-1px" v-for="(item,index) in seller.supports"> <span class="icon" :class="classMap[seller.supports[index].type]"></span> <span class="text">{{seller.supports[index].description}}</span> </li> </ul> </div> <split></split> <div class="pics"> <h1 class="title">商家實景</h1> <div class="pic-wrapper" ref="picWrapper"> <ul class="pic-list" ref="picList"> <li class="pic-item" v-for="pic in seller.pics"> <img :src="pic" width="120" height="90"> </li> </ul> </div> </div> </div> </div> </template> <script> import BScroll from 'better-scroll'; import star from '../../components/star/star'; import split from '../../components/split/split'; export default { name: 'v-seller', props: { seller: { type: Object } }, created() { this.classMap = ['decrease', 'discount', 'special', 'invoice', 'guarantee']; }, watch: { 'seller' () { this.$nextTick(() => { this._initScroll(); this._initPics(); }); } }, mounted() { this.$nextTick(() => { this._initScroll(); this._initPics(); }); }, methods: { _initScroll() { if (!this.scroll) { this.scroll = new BScroll(this.$refs.seller, { click: true }); } else { this.scroll.refresh(); } }, _initPics() { if (this.seller.pics) { let picWidth = 120; let margin = 6; let width = (picWidth + margin) * this.seller.pics.length - margin; this.$refs.picList.style.width = width + 'px'; this.$nextTick(() => { if (!this.picScroll) { this.picScroll = new BScroll(this.$refs.picWrapper, { scrollX: true, eventPassthrough: 'vertical' }); } else { this.picScroll.refresh(); } }); } } }, components: { star, split } } </script> <style lang="stylus" rel="stylesheet/stylus"> @import "../../common/stylus/mixin.styl" .seller position: absolute top: 174px bottom: 0 left: 0 width: 100% overflow: hidden .overview position: relative padding: 18px .title margin-bottom: 8px line-height: 14px color: rgb(7, 17, 27) font-size: 14px font-weight: 700 .desc padding-bottom: 18px border-1px(rgba(7, 17, 27, 0.1)) font-size: 0 .star display: inline-block margin-right: 8px vertical-align: top .text display: inline-block margin-right: 12px line-height: 18px vertical-align: top font-size: 10px color: rgb(77, 85, 93) .remark display: flex padding-top: 18px .block flex: 1 text-align: center border-right: 1px solid rgba(7, 17, 27, 0.1) &:last-child border: none h2 margin-bottom: 4px line-height: 10px font-size: 10px color: rgb(147, 153, 159) .content line-height: 24px font-size: 10px color: rgb(7, 17, 27) .stress font-size: 24px .bulletin padding: 18px 18px 0 18px .title margin-bottom: 8px line-height: 14px color: rgb(7, 17, 27) font-size: 14px .content-wrapper padding: 0 12px 16px 12px border-1px(rgba(7, 17, 27, 0.1)) .content line-height: 24px font-size: 12px color: rgb(240, 20, 20) .supports .support-item padding: 16px 12px border-1px(rgba(7, 17, 27, 0.1)) font-size: 0 &:last-child border-none() .icon display: inline-block width: 16px height: 16px vertical-align: top margin-right: 6px background-size: 16px 16px background-repeat: no-repeat &.decrease bg-image('decrease_4') &.discount bg-image('discount_4') &.guarantee bg-image('guarantee_4') &.invoice bg-image('invoice_4') &.special bg-image('special_4') .text line-height: 16px font-size: 12px color: rgb(7, 17, 27) .pics padding: 18px .title margin-bottom: 12px line-height: 14px color: rgb(7, 17, 27) font-size: 14px .pic-wrapper width: 100% overflow: hidden white-space: nowrap .pic-list font-size: 0 .pic-item display: inline-block margin-right: 6px width: 120px height: 90px &:last-child margin: 0 </style>
seller組件開發——商家信息(六)
編寫seller.vue組件,實現商家信息部分
<template> <div class="seller" ref="seller"> <div class="seller-content"> <div class="overview"> <h1 class="title">{{seller.name}}</h1> <div class="desc border-1px"> <star :size="36" :score="seller.score"></star> <span class="text">({{seller.ratingCount}})</span> <span class="text">月售{{seller.sellCount}}單</span> </div> <ul class="remark"> <li class="block"> <h2>起送價</h2> <div class="content"> <span class="stress">{{seller.minPrice}}</span>元 </div> </li> <li class="block"> <h2>商家配送</h2> <div class="content"> <span class="stress">{{seller.deliveryPrice}}</span>元 </div> </li> <li class="block"> <h2>平均配送時間</h2> <div class="content"> <span class="stress">{{seller.deliveryTime}}</span>分鍾 </div> </li> </ul> </div> <split></split> <div class="bulletin"> <h1 class="title">公告與活動</h1> <div class="content-wrapper border-1px"> <p class="content">{{seller.bulletin}}</p> </div> <ul v-if="seller.supports" class="supports"> <li class="support-item border-1px" v-for="(item,index) in seller.supports"> <span class="icon" :class="classMap[seller.supports[index].type]"></span> <span class="text">{{seller.supports[index].description}}</span> </li> </ul> </div> <split></split> <div class="pics"> <h1 class="title">商家實景</h1> <div class="pic-wrapper" ref="picWrapper"> <ul class="pic-list" ref="picList"> <li class="pic-item" v-for="pic in seller.pics"> <img :src="pic" width="120" height="90"> </li> </ul> </div> </div> <split></split> <div class="info"> <h1 class="title border-1px">商家信息</h1> <ul> <li class="info-item" v-for="info in seller.infos">{{info}}</li> </ul> </div> </div> </div> </template> <script> import BScroll from 'better-scroll'; import star from '../../components/star/star'; import split from '../../components/split/split'; export default { name: 'v-seller', props: { seller: { type: Object } }, created() { this.classMap = ['decrease', 'discount', 'special', 'invoice', 'guarantee']; }, watch: { 'seller' () { this.$nextTick(() => { this._initScroll(); this._initPics(); }); } }, mounted() { this.$nextTick(() => { this._initScroll(); this._initPics(); }); }, methods: { _initScroll() { if (!this.scroll) { this.scroll = new BScroll(this.$refs.seller, { click: true }); } else { this.scroll.refresh(); } }, _initPics() { if (this.seller.pics) { let picWidth = 120; let margin = 6; let width = (picWidth + margin) * this.seller.pics.length - margin; this.$refs.picList.style.width = width + 'px'; this.$nextTick(() => { if (!this.picScroll) { this.picScroll = new BScroll(this.$refs.picWrapper, { scrollX: true, eventPassthrough: 'vertical' }); } else { this.picScroll.refresh(); } }); } } }, components: { star, split } } </script> <style lang="stylus" rel="stylesheet/stylus"> @import "../../common/stylus/mixin.styl" .seller position: absolute top: 174px bottom: 0 left: 0 width: 100% overflow: hidden .overview position: relative padding: 18px .title margin-bottom: 8px line-height: 14px color: rgb(7, 17, 27) font-size: 14px font-weight: 700 .desc padding-bottom: 18px border-1px(rgba(7, 17, 27, 0.1)) font-size: 0 .star display: inline-block margin-right: 8px vertical-align: top .text display: inline-block margin-right: 12px line-height: 18px vertical-align: top font-size: 10px color: rgb(77, 85, 93) .remark display: flex padding-top: 18px .block flex: 1 text-align: center border-right: 1px solid rgba(7, 17, 27, 0.1) &:last-child border: none h2 margin-bottom: 4px line-height: 10px font-size: 10px color: rgb(147, 153, 159) .content line-height: 24px font-size: 10px color: rgb(7, 17, 27) .stress font-size: 24px .bulletin padding: 18px 18px 0 18px .title margin-bottom: 8px line-height: 14px color: rgb(7, 17, 27) font-size: 14px .content-wrapper padding: 0 12px 16px 12px border-1px(rgba(7, 17, 27, 0.1)) .content line-height: 24px font-size: 12px color: rgb(240, 20, 20) .supports .support-item padding: 16px 12px border-1px(rgba(7, 17, 27, 0.1)) font-size: 0 &:last-child border-none() .icon display: inline-block width: 16px height: 16px vertical-align: top margin-right: 6px background-size: 16px 16px background-repeat: no-repeat &.decrease bg-image('decrease_4') &.discount bg-image('discount_4') &.guarantee bg-image('guarantee_4') &.invoice bg-image('invoice_4') &.special bg-image('special_4') .text line-height: 16px font-size: 12px color: rgb(7, 17, 27) .pics padding: 18px .title margin-bottom: 12px line-height: 14px color: rgb(7, 17, 27) font-size: 14px .pic-wrapper width: 100% overflow: hidden white-space: nowrap .pic-list font-size: 0 .pic-item display: inline-block margin-right: 6px width: 120px height: 90px &:last-child margin: 0 .info padding: 18px 18px 0 18px color: rgb(7, 17, 27) .title padding-bottom: 12px line-height: 14px border-1px(rgba(7, 17, 27, 0.1)) font-size: 14px .info-item padding: 16px 12px line-height: 16px border-1px(rgba(7, 17, 27, 0.1)) font-size: 12px &:last-child border-none() </style>
seller組件開發——收藏商家(一)
編寫seller.vue組件,實現商家收藏基本效果
<template> <div class="seller" ref="seller"> <div class="seller-content"> <div class="overview"> <h1 class="title">{{seller.name}}</h1> <div class="desc border-1px"> <star :size="36" :score="seller.score"></star> <span class="text">({{seller.ratingCount}})</span> <span class="text">月售{{seller.sellCount}}單</span> </div> <ul class="remark"> <li class="block"> <h2>起送價</h2> <div class="content"> <span class="stress">{{seller.minPrice}}</span>元 </div> </li> <li class="block"> <h2>商家配送</h2> <div class="content"> <span class="stress">{{seller.deliveryPrice}}</span>元 </div> </li> <li class="block"> <h2>平均配送時間</h2> <div class="content"> <span class="stress">{{seller.deliveryTime}}</span>分鍾 </div> </li> </ul> <div class="favorite" @click="toggleFavorite"> <span class="icon-favorite" :class="{'active':favorite}"></span> <span class="text">{{favoriteText}}</span> </div> </div> <split></split> <div class="bulletin"> <h1 class="title">公告與活動</h1> <div class="content-wrapper border-1px"> <p class="content">{{seller.bulletin}}</p> </div> <ul v-if="seller.supports" class="supports"> <li class="support-item border-1px" v-for="(item,index) in seller.supports"> <span class="icon" :class="classMap[seller.supports[index].type]"></span> <span class="text">{{seller.supports[index].description}}</span> </li> </ul> </div> <split></split> <div class="pics"> <h1 class="title">商家實景</h1> <div class="pic-wrapper" ref="picWrapper"> <ul class="pic-list" ref="picList"> <li class="pic-item" v-for="pic in seller.pics"> <img :src="pic" width="120" height="90"> </li> </ul> </div> </div> <split></split> <div class="info"> <h1 class="title border-1px">商家信息</h1> <ul> <li class="info-item" v-for="info in seller.infos">{{info}}</li> </ul> </div> </div> </div> </template> <script> import BScroll from 'better-scroll'; import star from '../../components/star/star'; import split from '../../components/split/split'; export default { name: 'v-seller', props: { seller: { type: Object } }, data() { return { favorite: false }; }, computed: { favoriteText() { return this.favorite ? '已收藏' : '收藏'; } }, created() { this.classMap = ['decrease', 'discount', 'special', 'invoice', 'guarantee']; }, watch: { 'seller' () { this.$nextTick(() => { this._initScroll(); this._initPics(); }); } }, mounted() { this.$nextTick(() => { this._initScroll(); this._initPics(); }); }, methods: { toggleFavorite(event) { if (!event._constructed) { return; } this.favorite = !this.favorite; }, _initScroll() { if (!this.scroll) { this.scroll = new BScroll(this.$refs.seller, { click: true }); } else { this.scroll.refresh(); } }, _initPics() { if (this.seller.pics) { let picWidth = 120; let margin = 6; let width = (picWidth + margin) * this.seller.pics.length - margin; this.$refs.picList.style.width = width + 'px'; this.$nextTick(() => { if (!this.picScroll) { this.picScroll = new BScroll(this.$refs.picWrapper, { scrollX: true, eventPassthrough: 'vertical' }); } else { this.picScroll.refresh(); } }); } } }, components: { star, split } } </script> <style lang="stylus" rel="stylesheet/stylus"> @import "../../common/stylus/mixin.styl" .seller position: absolute top: 174px bottom: 0 left: 0 width: 100% overflow: hidden .overview position: relative padding: 18px .title margin-bottom: 8px line-height: 14px color: rgb(7, 17, 27) font-size: 14px font-weight: 700 .desc padding-bottom: 18px border-1px(rgba(7, 17, 27, 0.1)) font-size: 0 .star display: inline-block margin-right: 8px vertical-align: top .text display: inline-block margin-right: 12px line-height: 18px vertical-align: top font-size: 10px color: rgb(77, 85, 93) .remark display: flex padding-top: 18px .block flex: 1 text-align: center border-right: 1px solid rgba(7, 17, 27, 0.1) &:last-child border: none h2 margin-bottom: 4px line-height: 10px font-size: 10px color: rgb(147, 153, 159) .content line-height: 24px font-size: 10px color: rgb(7, 17, 27) .stress font-size: 24px .favorite position: absolute width: 50px right: 11px top: 18px text-align: center .icon-favorite display: block margin-bottom: 4px line-height: 24px font-size: 24px color: #d4d6d9 &.active color: rgb(240, 20, 20) .text line-height: 10px font-size: 10px color: rgb(77, 85, 93) .bulletin padding: 18px 18px 0 18px .title margin-bottom: 8px line-height: 14px color: rgb(7, 17, 27) font-size: 14px .content-wrapper padding: 0 12px 16px 12px border-1px(rgba(7, 17, 27, 0.1)) .content line-height: 24px font-size: 12px color: rgb(240, 20, 20) .supports .support-item padding: 16px 12px border-1px(rgba(7, 17, 27, 0.1)) font-size: 0 &:last-child border-none() .icon display: inline-block width: 16px height: 16px vertical-align: top margin-right: 6px background-size: 16px 16px background-repeat: no-repeat &.decrease bg-image('decrease_4') &.discount bg-image('discount_4') &.guarantee bg-image('guarantee_4') &.invoice bg-image('invoice_4') &.special bg-image('special_4') .text line-height: 16px font-size: 12px color: rgb(7, 17, 27) .pics padding: 18px .title margin-bottom: 12px line-height: 14px color: rgb(7, 17, 27) font-size: 14px .pic-wrapper width: 100% overflow: hidden white-space: nowrap .pic-list font-size: 0 .pic-item display: inline-block margin-right: 6px width: 120px height: 90px &:last-child margin: 0 .info padding: 18px 18px 0 18px color: rgb(7, 17, 27) .title padding-bottom: 12px line-height: 14px border-1px(rgba(7, 17, 27, 0.1)) font-size: 14px .info-item padding: 16px 12px line-height: 16px border-1px(rgba(7, 17, 27, 0.1)) font-size: 12px &:last-child border-none() </style>
seller組件開發——收藏商家(二)
步驟一:編寫App.vue根組件,為傳遞商家ID值做前期准備
<template> <div> <v-header :seller="seller"></v-header> <div class="tab border-1px"> <div class="tab-item"> <router-link to="/goods">商品</router-link> </div> <div class="tab-item"> <router-link to="/ratings">評價</router-link> </div> <div class="tab-item"> <router-link to="/seller">商家</router-link> </div> </div> <router-view :seller="seller"></router-view> </div> </template> <script> import header from './components/header/header.vue'; import {urlParse} from './common/js/util'; const ERR_OK = 0; export default { name: 'app', data() { return { seller: { id: (() => { let queryParam = urlParse(); return queryParam.id; })() } }; }, created() { this.$http.get('api/seller?id=' + this.seller.id).then((response) => { response = response.body; if(response.error === ERR_OK){ this.seller = Object.assign({}, this.seller, response.data); } this.seller = response.data; }) }, components: { 'v-header': header } } </script> <style scoped lang="stylus" rel="stylesheet/stylus"> @import "./common/stylus/mixin.styl"; .tab display: flex width: 100% height: 40px line-height: 40px /*border-bottom: 1px solid rgba(7, 17, 27, 0.1)*/ border-1px(rgba(7, 17, 27, 0.1)) .tab-item flex: 1 text-align: center & > a display:block font-size:16px color:rgb(77,85,93) &.active color:rgb(240,20,20) </style>
步驟二:新建util.js文件,實現查詢的效果
/** * 解析url參數 * @example ?id=12345&a=b * @return Object {id:12345,a:b} */ export function urlParse() { let url = window.location.search; let obj = {}; let reg = /[?&][^?&]+=[^?&]+/g; let arr = url.match(reg); // ['?id=12345', '&a=b'] if (arr) { arr.forEach((item) => { let tempArr = item.substring(1).split('='); let key = decodeURIComponent(tempArr[0]); let val = decodeURIComponent(tempArr[1]); obj[key] = val; }); } return obj; };
seller組件開發——收藏商家(三)
步驟一:新建store.js文件,實現存儲狀態的功能
export function saveToLocal(id, key, value) {
let seller = window.localStorage.__seller__;
if (!seller) {
seller = {};
seller[id] = {};
} else {
seller = JSON.parse(seller);
if (!seller[id]) {
seller[id] = {};
}
}
seller[id][key] = value;
window.localStorage.__seller__ = JSON.stringify(seller);
};
export function loadFromLocal(id, key, def) {
let seller = window.localStorage.__seller__;
if (!seller) {
return def;
}
seller = JSON.parse(seller)[id];
if (!seller) {
return def;
}
let ret = seller[key];
return ret || def;
};
步驟二:編寫seller.vue組件,將數據傳入store.js中
<template> <div class="seller" ref="seller"> <div class="seller-content"> <div class="overview"> <h1 class="title">{{seller.name}}</h1> <div class="desc border-1px"> <star :size="36" :score="seller.score"></star> <span class="text">({{seller.ratingCount}})</span> <span class="text">月售{{seller.sellCount}}單</span> </div> <ul class="remark"> <li class="block"> <h2>起送價</h2> <div class="content"> <span class="stress">{{seller.minPrice}}</span>元 </div> </li> <li class="block"> <h2>商家配送</h2> <div class="content"> <span class="stress">{{seller.deliveryPrice}}</span>元 </div> </li> <li class="block"> <h2>平均配送時間</h2> <div class="content"> <span class="stress">{{seller.deliveryTime}}</span>分鍾 </div> </li> </ul> <div class="favorite" @click="toggleFavorite"> <span class="icon-favorite" :class="{'active':favorite}"></span> <span class="text">{{favoriteText}}</span> </div> </div> <split></split> <div class="bulletin"> <h1 class="title">公告與活動</h1> <div class="content-wrapper border-1px"> <p class="content">{{seller.bulletin}}</p> </div> <ul v-if="seller.supports" class="supports"> <li class="support-item border-1px" v-for="(item,index) in seller.supports"> <span class="icon" :class="classMap[seller.supports[index].type]"></span> <span class="text">{{seller.supports[index].description}}</span> </li> </ul> </div> <split></split> <div class="pics"> <h1 class="title">商家實景</h1> <div class="pic-wrapper" ref="picWrapper"> <ul class="pic-list" ref="picList"> <li class="pic-item" v-for="pic in seller.pics"> <img :src="pic" width="120" height="90"> </li> </ul> </div> </div> <split></split> <div class="info"> <h1 class="title border-1px">商家信息</h1> <ul> <li class="info-item" v-for="info in seller.infos">{{info}}</li> </ul> </div> </div> </div> </template> <script> import BScroll from 'better-scroll'; import star from '../../components/star/star'; import split from '../../components/split/split'; import {saveToLocal, loadFromLocal} from '../../common/js/store'; export default { name: 'v-seller', props: { seller: { type: Object } }, data() { return { favorite: (() => { return loadFromLocal(this.seller.id, 'favorite', false); })() }; }, computed: { favoriteText() { return this.favorite ? '已收藏' : '收藏'; } }, created() { this.classMap = ['decrease', 'discount', 'special', 'invoice', 'guarantee']; }, watch: { 'seller' () { this.$nextTick(() => { this._initScroll(); this._initPics(); }); } }, mounted() { this.$nextTick(() => { this._initScroll(); this._initPics(); }); }, methods: { toggleFavorite(event) { if (!event._constructed) { return; } this.favorite = !this.favorite; saveToLocal(this.seller.id, 'favorite', this.favorite); }, _initScroll() { if (!this.scroll) { this.scroll = new BScroll(this.$refs.seller, { click: true }); } else { this.scroll.refresh(); } }, _initPics() { if (this.seller.pics) { let picWidth = 120; let margin = 6; let width = (picWidth + margin) * this.seller.pics.length - margin; this.$refs.picList.style.width = width + 'px'; this.$nextTick(() => { if (!this.picScroll) { this.picScroll = new BScroll(this.$refs.picWrapper, { scrollX: true, eventPassthrough: 'vertical' }); } else { this.picScroll.refresh(); } }); } } }, components: { star, split } } </script> <style lang="stylus" rel="stylesheet/stylus"> @import "../../common/stylus/mixin.styl" .seller position: absolute top: 174px bottom: 0 left: 0 width: 100% overflow: hidden .overview position: relative padding: 18px .title margin-bottom: 8px line-height: 14px color: rgb(7, 17, 27) font-size: 14px font-weight: 700 .desc padding-bottom: 18px border-1px(rgba(7, 17, 27, 0.1)) font-size: 0 .star display: inline-block margin-right: 8px vertical-align: top .text display: inline-block margin-right: 12px line-height: 18px vertical-align: top font-size: 10px color: rgb(77, 85, 93) .remark display: flex padding-top: 18px .block flex: 1 text-align: center border-right: 1px solid rgba(7, 17, 27, 0.1) &:last-child border: none h2 margin-bottom: 4px line-height: 10px font-size: 10px color: rgb(147, 153, 159) .content line-height: 24px font-size: 10px color: rgb(7, 17, 27) .stress font-size: 24px .favorite position: absolute width: 50px right: 11px top: 18px text-align: center .icon-favorite display: block margin-bottom: 4px line-height: 24px font-size: 24px color: #d4d6d9 &.active color: rgb(240, 20, 20) .text line-height: 10px font-size: 10px color: rgb(77, 85, 93) .bulletin padding: 18px 18px 0 18px .title margin-bottom: 8px line-height: 14px color: rgb(7, 17, 27) font-size: 14px .content-wrapper padding: 0 12px 16px 12px border-1px(rgba(7, 17, 27, 0.1)) .content line-height: 24px font-size: 12px color: rgb(240, 20, 20) .supports .support-item padding: 16px 12px border-1px(rgba(7, 17, 27, 0.1)) font-size: 0 &:last-child border-none() .icon display: inline-block width: 16px height: 16px vertical-align: top margin-right: 6px background-size: 16px 16px background-repeat: no-repeat &.decrease bg-image('decrease_4') &.discount bg-image('discount_4') &.guarantee bg-image('guarantee_4') &.invoice bg-image('invoice_4') &.special bg-image('special_4') .text line-height: 16px font-size: 12px color: rgb(7, 17, 27) .pics padding: 18px .title margin-bottom: 12px line-height: 14px color: rgb(7, 17, 27) font-size: 14px .pic-wrapper width: 100% overflow: hidden white-space: nowrap .pic-list font-size: 0 .pic-item display: inline-block margin-right: 6px width: 120px height: 90px &:last-child margin: 0 .info padding: 18px 18px 0 18px color: rgb(7, 17, 27) .title padding-bottom: 12px line-height: 14px border-1px(rgba(7, 17, 27, 0.1)) font-size: 14px .info-item padding: 16px 12px line-height: 16px border-1px(rgba(7, 17, 27, 0.1)) font-size: 12px &:last-child border-none() </style>
體驗優化
編寫App.vue組件實現狀態的保留,原理是將當前的狀態保存在內存中,使用的keep-alive標簽。
<template> <div> <v-header :seller="seller"></v-header> <div class="tab border-1px"> <div class="tab-item"> <router-link to="/goods">商品</router-link> </div> <div class="tab-item"> <router-link to="/ratings">評價</router-link> </div> <div class="tab-item"> <router-link to="/seller">商家</router-link> </div> </div> <keep-alive> <router-view :seller="seller"></router-view> </keep-alive> </div> </template> <script> import header from './components/header/header.vue'; import {urlParse} from './common/js/util'; const ERR_OK = 0; export default { name: 'app', data() { return { seller: { id: (() => { let queryParam = urlParse(); return queryParam.id; })() } }; }, created() { this.$http.get('api/seller?id=' + this.seller.id).then((response) => { response = response.body; if(response.error === ERR_OK){ this.seller = Object.assign({}, this.seller, response.data); } this.seller = response.data; }) }, components: { 'v-header': header } } </script> <style scoped lang="stylus" rel="stylesheet/stylus"> @import "./common/stylus/mixin.styl"; .tab display: flex width: 100% height: 40px line-height: 40px /*border-bottom: 1px solid rgba(7, 17, 27, 0.1)*/ border-1px(rgba(7, 17, 27, 0.1)) .tab-item flex: 1 text-align: center & > a display:block font-size:16px color:rgb(77,85,93) &.active color:rgb(240,20,20) </style>
