├── examples ├── app │ ├── app.css │ ├── assets │ │ ├── images │ │ │ ├── mv.png │ │ │ ├── album.png │ │ │ └── singer.png │ │ └── css │ │ │ ├── variable.styl │ │ │ ├── reset.css │ │ │ └── common.styl │ ├── pages │ │ ├── Player │ │ │ └── index.vue │ │ ├── Song │ │ │ └── index.vue │ │ ├── Movie │ │ │ └── index.vue │ │ ├── MovieDetail │ │ │ └── index.vue │ │ ├── AlbumDetail │ │ │ ├── tabPosition.vue │ │ │ └── index.vue │ │ ├── RankDetail │ │ │ ├── tabPosition.vue │ │ │ └── index.vue │ │ ├── Recomment │ │ │ └── index.vue │ │ ├── SingerDetail │ │ │ ├── tabPosition.vue │ │ │ └── index.vue │ │ ├── Rank │ │ │ └── index.vue │ │ ├── MvPlayer │ │ │ └── index.vue │ │ ├── Singer │ │ │ └── index.vue │ │ └── SongPlayer │ │ │ ├── index.styl │ │ │ └── index.vue │ ├── components │ │ ├── Base │ │ │ ├── Icon.vue │ │ │ ├── Loading.vue │ │ │ ├── Beating.vue │ │ │ └── Progress.vue │ │ └── Lists │ │ │ ├── AlbumList.vue │ │ │ ├── MvList.vue │ │ │ ├── MenuList.vue │ │ │ ├── SongList.vue │ │ │ └── PlayList.vue │ ├── store │ │ ├── getters.js │ │ ├── state.js │ │ ├── index.js │ │ ├── types.js │ │ ├── actions.js │ │ └── mutations.js │ ├── service │ │ ├── axios.js │ │ ├── host.js │ │ └── api.js │ ├── app.js │ └── app.json ├── static │ └── .gitkeep ├── config │ ├── prod.env.js │ ├── dev.env.js │ └── index.js ├── index.html └── build │ ├── vue-loader.conf.js │ ├── build.js │ ├── check-versions.js │ ├── webpack.base.conf.js │ ├── utils.js │ ├── webpack.dev.conf.js │ └── webpack.prod.conf.js ├── .eslintignore ├── .editorconfig ├── .gitignore ├── src ├── components │ ├── TabCon.vue │ ├── Navigation.vue │ ├── TabBar.vue │ ├── ScrollView.vue │ ├── App.vue │ ├── PageView.vue │ └── PageScrollView.vue ├── effect-core │ ├── index.js │ ├── install.js │ ├── vnode-cache.js │ └── direction.js ├── util.js ├── router.js ├── index.js └── base.css ├── .postcssrc.js ├── .babelrc ├── demo └── examples │ ├── index.html │ └── static │ ├── js │ └── manifest.c671b31a6294c60d458b.js │ └── css │ └── app.2d0aa6749630fc98cd589e832010c3a5.css ├── .eslintrc.js ├── LICENSE ├── package.json ├── README.md └── README_EN.md /examples/app/app.css: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /examples/static/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /build/ 2 | /config/ 3 | /dist/ 4 | /*.js 5 | -------------------------------------------------------------------------------- /examples/config/prod.env.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | module.exports = { 3 | NODE_ENV: '"production"' 4 | } 5 | -------------------------------------------------------------------------------- /examples/app/assets/images/mv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JooZh/vue-app-effect/HEAD/examples/app/assets/images/mv.png -------------------------------------------------------------------------------- /examples/app/assets/images/album.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JooZh/vue-app-effect/HEAD/examples/app/assets/images/album.png -------------------------------------------------------------------------------- /examples/app/assets/images/singer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JooZh/vue-app-effect/HEAD/examples/app/assets/images/singer.png -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /examples/config/dev.env.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const merge = require('webpack-merge') 3 | const prodEnv = require('./prod.env') 4 | 5 | module.exports = merge(prodEnv, { 6 | NODE_ENV: '"development"' 7 | }) 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | npm-debug.log* 4 | yarn-debug.log* 5 | yarn-error.log* 6 | 7 | # Editor directories and files 8 | .idea 9 | .vscode 10 | *.suo 11 | *.ntvs* 12 | *.njsproj 13 | *.sln 14 | -------------------------------------------------------------------------------- /examples/app/pages/Player/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /examples/app/components/Base/Icon.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 15 | 16 | 19 | -------------------------------------------------------------------------------- /src/components/TabCon.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 14 | -------------------------------------------------------------------------------- /.postcssrc.js: -------------------------------------------------------------------------------- 1 | // https://github.com/michael-ciniawsky/postcss-load-config 2 | 3 | module.exports = { 4 | "plugins": { 5 | "postcss-import": {}, 6 | "postcss-url": {}, 7 | // to edit target browsers: use "browserslist" field in package.json 8 | "autoprefixer": {} 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /examples/app/assets/css/variable.styl: -------------------------------------------------------------------------------- 1 | 2 | $bg-home = rgba(255,235,59,1) 3 | $font-home = rgba(37,37,37,1) 4 | 5 | $bgColor = #252525 6 | $themeColor = #ffcd32 7 | $defaultColor =#ccc 8 | $boderColor = #444 9 | 10 | $fontXS = 12px 11 | $fontS = 14px 12 | $fontM = 16px 13 | $fontXM = 18px 14 | $fontL = 20px 15 | $fontXL = 22px 16 | $fontXXL = 24px 17 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "modules": false 5 | }], 6 | "stage-2" 7 | ], 8 | "comments": false, 9 | "env": { 10 | "test": { 11 | "presets": [ 12 | "env", 13 | "stage-2" 14 | ], 15 | "plugins": [ 16 | "istanbul", 17 | "transform-vue-jsx", 18 | "transform-runtime" 19 | ] 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/app/store/getters.js: -------------------------------------------------------------------------------- 1 | export const playerState = state => state.playerState; 2 | export const playSong = state => state.playSong; 3 | export const playStatus = state => state.playStatus; 4 | export const playerList = state => state.playerList; 5 | export const playSongindex = state => state.playSongindex; 6 | export const backType = state => state.backType; 7 | export const playMvData = state => state.playMvData; 8 | -------------------------------------------------------------------------------- /examples/app/service/axios.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | 3 | function sendRequest(options){ 4 | return new Promise((resolve,reject)=>{ 5 | axios.get(options.path, { 6 | params:options.params 7 | }).then(res=>{ 8 | if(res.data.status === 0){ 9 | resolve(res.data.data) 10 | } else { 11 | reject() 12 | } 13 | }) 14 | }) 15 | } 16 | 17 | export default sendRequest 18 | -------------------------------------------------------------------------------- /examples/app/store/state.js: -------------------------------------------------------------------------------- 1 | // 维护的数据 2 | 3 | export default { 4 | // 播放器返回类型 ios 左滑 和点击 5 | backType: "ios", 6 | // 播放器状态 7 | playerState: false, 8 | // 播放列表 9 | playerList: [], 10 | // 当前播放歌曲信息 11 | playSong: {}, 12 | // 当前播放的歌曲列表位置 13 | playSongindex: 0, 14 | // 当前播放列表状态 1列表循环,2单曲循环,3随机播放 15 | playListState: 0, 16 | // 当前播放状态 17 | playStatus: false, 18 | // 播放的mv的数据 19 | playMvData: {} 20 | }; 21 | -------------------------------------------------------------------------------- /src/effect-core/index.js: -------------------------------------------------------------------------------- 1 | import install from './install.js' 2 | import direction from './direction.js' 3 | 4 | export default { 5 | install: (Vue, { router, tabbar, common='' } = {}) => { 6 | // 判断参数的完整性 7 | if (!router || !tabbar) { 8 | console.error('vue-app-effect need options: router, tabbar') 9 | return 10 | } 11 | // 数据传递 12 | const bus = new Vue() 13 | // 执行 install 14 | install(Vue,bus,tabbar) 15 | // 执行 router 监听事件 16 | direction(router,bus,tabbar,common) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/util.js: -------------------------------------------------------------------------------- 1 | // production 2 | // import config from "../../../src/app.json"; 3 | 4 | // function getComponent(name){ 5 | // if(name){ 6 | // return require(`../../../src/${name}`).default 7 | // } else { 8 | // return null 9 | // } 10 | // } 11 | 12 | // develpment 13 | import config from "../examples/app/app.json"; 14 | 15 | function getComponent(name){ 16 | if(name){ 17 | return require(`../examples/app/${name}`).default 18 | } else { 19 | return null 20 | } 21 | } 22 | 23 | export { 24 | config, 25 | getComponent 26 | } 27 | -------------------------------------------------------------------------------- /demo/examples/index.html: -------------------------------------------------------------------------------- 1 | vue-app-effect demo
-------------------------------------------------------------------------------- /examples/app/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import Vuex from "vuex"; 3 | import state from "./state"; 4 | import actions from "./actions"; 5 | import mutations from "./mutations"; 6 | import * as getters from "./getters"; 7 | 8 | // 引入 vuex 的 logger 在输出属性用的 9 | import createLogger from "vuex/dist/logger"; 10 | 11 | Vue.use(Vuex); 12 | 13 | const debug = process.env.NODE_ENV !== "production"; 14 | 15 | export default new Vuex.Store({ 16 | state, 17 | actions, 18 | mutations, 19 | getters, 20 | strict: debug, 21 | plugins: debug ? [createLogger()] : [] 22 | }); 23 | -------------------------------------------------------------------------------- /examples/app/store/types.js: -------------------------------------------------------------------------------- 1 | export const togglePlayer = "togglePlayer"; 2 | export const playAll = "playAll"; 3 | export const playOne = "playOne"; 4 | export const playAdd = "playAdd"; 5 | export const playSome = "playSome"; 6 | export const delOne = "delOne"; 7 | export const next = "next"; 8 | export const prev = "prev"; 9 | export const play = "play"; 10 | export const pause = "pause"; 11 | export const repeat = "repeat"; 12 | export const random = "random"; 13 | export const playBackType = "playBackType"; 14 | export const clearList = "clearList"; 15 | export const playMv = "playMv"; 16 | -------------------------------------------------------------------------------- /examples/app/service/host.js: -------------------------------------------------------------------------------- 1 | const apiHost = { 2 | dev: { 3 | API_HOST: 'http://localhost:9090/music/api' 4 | }, 5 | test:{ 6 | API_HOST: 'http://localhost:9090/music/api' 7 | }, 8 | prod: { 9 | API_HOST: 'https://joozh.cn/music/api' 10 | } 11 | }; 12 | 13 | const getApiHost = () => { 14 | let url = window.location.origin; 15 | if (url.indexOf('9090') > -1) { 16 | return apiHost.test; 17 | } else if (url.indexOf('joozh.cn') > -1) { 18 | return apiHost.prod; 19 | } else { 20 | return apiHost.dev; 21 | } 22 | }; 23 | export default getApiHost; 24 | -------------------------------------------------------------------------------- /examples/app/assets/css/reset.css: -------------------------------------------------------------------------------- 1 | @import url("https://cdn.bootcss.com/normalize/8.0.0/normalize.min.css"); 2 | @import url("https://cdn.bootcss.com/font-awesome/4.7.0/css/font-awesome.min.css"); 3 | @import url("https://cdn.bootcss.com/animate.css/3.5.2/animate.min.css"); 4 | 5 | html,body{ 6 | height: 100%; 7 | background: #252525 8 | } 9 | #app { 10 | font-family: "Avenir", Helvetica, Arial, sans-serif; 11 | -webkit-font-smoothing: antialiased; 12 | -moz-osx-font-smoothing: grayscale; 13 | font-size: 12px; 14 | color: #999; 15 | } 16 | a { 17 | text-decoration: none; 18 | } 19 | 20 | -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | vue-app-effect demo 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/build/vue-loader.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const utils = require('./utils') 3 | const config = require('../config') 4 | const isProduction = process.env.NODE_ENV === 'production' 5 | const sourceMapEnabled = isProduction 6 | ? config.build.productionSourceMap 7 | : config.dev.cssSourceMap 8 | 9 | module.exports = { 10 | loaders: utils.cssLoaders({ 11 | sourceMap: sourceMapEnabled, 12 | extract: isProduction 13 | }), 14 | cssSourceMap: sourceMapEnabled, 15 | cacheBusting: config.dev.cacheBusting, 16 | transformToRequire: { 17 | video: ['src', 'poster'], 18 | source: 'src', 19 | img: 'src', 20 | image: 'xlink:href' 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/app/pages/Song/index.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 32 | 33 | 39 | -------------------------------------------------------------------------------- /examples/app/app.js: -------------------------------------------------------------------------------- 1 | // The Vue build version to load with the `import` command 2 | // (runtime-only or standalone) has been set in webpack.base.conf with an alias. 3 | 4 | import store from "./store/index"; 5 | import CreateEffectApp from "../../src/index"; 6 | 7 | import 'iview/dist/styles/iview.css' 8 | import "./assets/css/reset.css"; 9 | import "./assets/css/common.styl"; 10 | 11 | import lazyload from 'vue-lazyload'; 12 | import { Icon } from 'iview' 13 | // import Icon from './components/Base/Icon' 14 | 15 | CreateEffectApp({ 16 | // 使用vuex 17 | store, 18 | // 添加使用的插件 19 | plugins: [ 20 | lazyload 21 | ], 22 | // 添加使用的第三方组件 23 | components:[ 24 | ['Icon', Icon] 25 | ], 26 | // 使用vue的自定义配置 27 | config: { 28 | productionTip:false 29 | } 30 | }); 31 | -------------------------------------------------------------------------------- /examples/app/store/actions.js: -------------------------------------------------------------------------------- 1 | import * as types from "./types"; 2 | 3 | export default { 4 | togglePlayer({ commit }) { 5 | commit(types.togglePlayer); 6 | }, 7 | next({ commit }) { 8 | commit(types.next); 9 | }, 10 | prev({ commit }) { 11 | commit(types.prev); 12 | }, 13 | play({ commit }) { 14 | commit(types.play); 15 | }, 16 | pause({ commit }) { 17 | commit(types.pause); 18 | }, 19 | random({ commit }) { 20 | commit(types.random); 21 | }, 22 | repeat({ commit }) { 23 | commit(types.repeat); 24 | }, 25 | playBackType({ commit }, payload) { 26 | commit(types.playBackType, payload); 27 | }, 28 | clearList({ commit }) { 29 | commit(types.clearList); 30 | }, 31 | playMv({ commit }) { 32 | commit(types.playMv); 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /demo/examples/static/js/manifest.c671b31a6294c60d458b.js: -------------------------------------------------------------------------------- 1 | !function(r){var n=window.webpackJsonp;window.webpackJsonp=function(e,u,c){for(var f,i,p,a=0,l=[];a 2 |
3 |
4 |
5 |
6 |
{{ title }}
7 |
8 | 9 |
10 |
11 | 12 | 13 | 38 | -------------------------------------------------------------------------------- /examples/app/components/Base/Loading.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 13 | 14 | 43 | -------------------------------------------------------------------------------- /src/router.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import Router from "vue-router"; 3 | import {config, getComponent} from './util.js' 4 | 5 | let routes = []; 6 | 7 | // 创建tab页面子路由列表 8 | let tabBar = []; 9 | config.tabBar.list.forEach(item => { 10 | tabBar.push({ 11 | path: `/${item.path}`, 12 | name: `/${item.path}`, 13 | component: getComponent(item.path) 14 | }); 15 | }); 16 | 17 | // 创建tab页面路由 18 | let tabRouter = { 19 | path: "/", 20 | component: require(`./components/TabCon`).default, 21 | redirect: tabBar[0].path, 22 | children: tabBar 23 | }; 24 | 25 | // 添加tab页面路由到路由配置列表 26 | routes.push(tabRouter); 27 | 28 | // 添加其他页面路由 29 | let pages = config.pages.concat([config.commonPage]); 30 | pages.forEach(path => { 31 | routes.push({ 32 | path: `/${path}`, 33 | name: `/${path}`, 34 | component: getComponent(path) 35 | }); 36 | }); 37 | 38 | Vue.use(Router); 39 | 40 | export default new Router({ 41 | routes: routes 42 | }); 43 | -------------------------------------------------------------------------------- /src/components/TabBar.vue: -------------------------------------------------------------------------------- 1 | 18 | 33 | -------------------------------------------------------------------------------- /src/components/ScrollView.vue: -------------------------------------------------------------------------------- 1 | 13 | 45 | -------------------------------------------------------------------------------- /src/components/App.vue: -------------------------------------------------------------------------------- 1 | 12 | 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /examples/app/pages/Movie/index.vue: -------------------------------------------------------------------------------- 1 | 16 | 44 | -------------------------------------------------------------------------------- /examples/build/build.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | require('./check-versions')() 3 | 4 | process.env.NODE_ENV = 'production' 5 | 6 | const ora = require('ora') 7 | const rm = require('rimraf') 8 | const path = require('path') 9 | const chalk = require('chalk') 10 | const webpack = require('webpack') 11 | const config = require('../config') 12 | const webpackConfig = require('./webpack.prod.conf') 13 | 14 | const spinner = ora('building for production...') 15 | spinner.start() 16 | 17 | rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => { 18 | if (err) throw err 19 | webpack(webpackConfig, (err, stats) => { 20 | spinner.stop() 21 | if (err) throw err 22 | process.stdout.write(stats.toString({ 23 | colors: true, 24 | modules: false, 25 | children: false, // If you are using ts-loader, setting this to true will make TypeScript errors show up during build. 26 | chunks: false, 27 | chunkModules: false 28 | }) + '\n\n') 29 | 30 | if (stats.hasErrors()) { 31 | console.log(chalk.red(' Build failed with errors.\n')) 32 | process.exit(1) 33 | } 34 | 35 | console.log(chalk.cyan(' Build complete.\n')) 36 | console.log(chalk.yellow( 37 | ' Tip: built files are meant to be served over an HTTP server.\n' + 38 | ' Opening index.html over file:// won\'t work.\n' 39 | )) 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /examples/app/pages/MovieDetail/index.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 52 | -------------------------------------------------------------------------------- /src/components/PageView.vue: -------------------------------------------------------------------------------- 1 | 19 | 55 | -------------------------------------------------------------------------------- /examples/app/assets/css/common.styl: -------------------------------------------------------------------------------- 1 | @import './variable.styl' 2 | // 1px边框 3 | .border-half-top 4 | position relative 5 | &:before 6 | position absolute 7 | content ' ' 8 | height 1px 9 | background $boderColor 10 | transform scaleY(0.5) 11 | width 100% 12 | top 0 13 | left 0 14 | right 0 15 | z-index: 10 16 | .border-half-bottom 17 | position relative 18 | &:after 19 | position absolute 20 | content ' ' 21 | height 1px 22 | background $boderColor 23 | transform scaleY(0.5) 24 | width 100% 25 | bottom 0 26 | left 0 27 | right 0 28 | z-index: 10 29 | .border-half-left 30 | position relative 31 | &::after 32 | position absolute 33 | content ' ' 34 | width: 1px 35 | background $boderColor 36 | transform scaleX(0.5) 37 | height 100% 38 | bottom 0 39 | left 0 40 | top 0 41 | z-index: 10 42 | .border-half-right 43 | position relative 44 | &::before 45 | position absolute 46 | content ' ' 47 | width: 1px 48 | background $boderColor 49 | transform scaleX(0.5) 50 | height 100% 51 | bottom 0 52 | top 0 53 | right 0 54 | z-index: 10 55 | // 单行文字省略号 56 | .text-line 57 | position relative 58 | .pix 59 | position absolute 60 | left 0 61 | top 0 62 | right 0 63 | bottom 0 64 | overflow hidden 65 | text-overflow ellipsis 66 | white-space nowrap 67 | word-wrap normal 68 | -------------------------------------------------------------------------------- /examples/build/check-versions.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const chalk = require('chalk') 3 | const semver = require('semver') 4 | const packageConfig = require('../../package.json') 5 | const shell = require('shelljs') 6 | 7 | function exec (cmd) { 8 | return require('child_process').execSync(cmd).toString().trim() 9 | } 10 | 11 | const versionRequirements = [ 12 | { 13 | name: 'node', 14 | currentVersion: semver.clean(process.version), 15 | versionRequirement: packageConfig.engines.node 16 | } 17 | ] 18 | 19 | if (shell.which('npm')) { 20 | versionRequirements.push({ 21 | name: 'npm', 22 | currentVersion: exec('npm --version'), 23 | versionRequirement: packageConfig.engines.npm 24 | }) 25 | } 26 | 27 | module.exports = function () { 28 | const warnings = [] 29 | 30 | for (let i = 0; i < versionRequirements.length; i++) { 31 | const mod = versionRequirements[i] 32 | 33 | if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) { 34 | warnings.push(mod.name + ': ' + 35 | chalk.red(mod.currentVersion) + ' should be ' + 36 | chalk.green(mod.versionRequirement) 37 | ) 38 | } 39 | } 40 | 41 | if (warnings.length) { 42 | console.log('') 43 | console.log(chalk.yellow('To use this template, you must update following to modules:')) 44 | console.log() 45 | 46 | for (let i = 0; i < warnings.length; i++) { 47 | const warning = warnings[i] 48 | console.log(' ' + warning) 49 | } 50 | 51 | console.log() 52 | process.exit(1) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /examples/app/pages/AlbumDetail/tabPosition.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 41 | 42 | 75 | -------------------------------------------------------------------------------- /examples/app/pages/RankDetail/tabPosition.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 41 | 42 | 75 | -------------------------------------------------------------------------------- /examples/app/components/Lists/AlbumList.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 36 | 37 | 72 | -------------------------------------------------------------------------------- /examples/app/pages/Recomment/index.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 50 | 51 | 77 | -------------------------------------------------------------------------------- /src/effect-core/install.js: -------------------------------------------------------------------------------- 1 | import VnodeCache from './vnode-cache.js' 2 | function install (Vue,bus,tabbar){ 3 | // vnode-cache 组件 4 | Vue.component('vnode-cache', VnodeCache(bus, tabbar)) 5 | 6 | Vue.prototype.$vueAppEffect = { 7 | on: (event, callback) => { 8 | bus.$on(event, callback) 9 | }, 10 | back: ()=>{ 11 | window.$VueAppEffect.paths.pop() 12 | vm.$router.options.routes.pop(); 13 | window.vm.$router.replace({ 14 | name: window.$VueAppEffect.paths.concat([]).pop() 15 | }) 16 | }, 17 | backTo: (options)=>{ 18 | // 19 | }, 20 | next: (options)=>{ 21 | let routePath = options.path 22 | routePath = routePath.indexOf('/') !== 0 ? `/${routePath}` : routePath; 23 | // 判断路由是否存在 24 | let find = window.vm.$router.options.routes.findIndex(item => { 25 | return item.path === routePath 26 | }) 27 | // 不存在 添加一个新路由 28 | if (find === -1) { 29 | // 找出匹配的重复使用组件 30 | let routeName= routePath.split('/') 31 | routeName.pop() 32 | routeName = routeName.join('/'); 33 | 34 | let route = window.vm.$router.options.routes.find(item => { 35 | return item.path === routeName 36 | }) 37 | if(!route){ 38 | throw Error(routeName+' is not defined'); 39 | } 40 | let newRoute = [{ 41 | path: routePath, 42 | name: routePath, 43 | component: {extends: route.component} 44 | }] 45 | 46 | window.vm.$router.options.routes.push(newRoute[0]) 47 | window.vm.$router.addRoutes(newRoute) 48 | } 49 | // 然后跳转 50 | window.vm.$router.replace({ 51 | name: routePath, 52 | params: options.params 53 | }) 54 | } 55 | } 56 | } 57 | 58 | export default install 59 | -------------------------------------------------------------------------------- /examples/app/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "basePath":"/app/", 3 | "pages":[ 4 | "pages/MovieDetail/index", 5 | "pages/SingerDetail/index", 6 | "pages/MvPlayer/index", 7 | "pages/RankDetail/index", 8 | "pages/AlbumDetail/index" 9 | ], 10 | "customTabBarComponent": "pages/SingerDetail/index", 11 | "commonPage":"pages/SongPlayer/index", 12 | "commonComponent":"pages/SongPlayer/index", 13 | "tabBar":{ 14 | "barHeight":"", 15 | "backgroundColor":"", 16 | "position":"", 17 | "list":[ 18 | { 19 | "text":"推荐", 20 | "path":"pages/Recomment/index", 21 | "iconFont":"", 22 | "iconFontActive":"", 23 | "iconImagePath":"", 24 | "iconImageActivePath":"" 25 | },{ 26 | "text":"视频", 27 | "path":"pages/Movie/index", 28 | "iconFont":"", 29 | "iconFontActive":"", 30 | "iconImagePath":"", 31 | "iconImageActivePath":"" 32 | },{ 33 | "text":"排行", 34 | "path":"pages/Rank/index", 35 | "iconFont":"", 36 | "iconFontActive":"", 37 | "iconImagePath":"", 38 | "iconImageActivePath":"" 39 | },{ 40 | "text":"歌手", 41 | "path":"pages/Singer/index", 42 | "iconFont":"", 43 | "iconFontActive":"", 44 | "iconImagePath":"", 45 | "iconImageActivePath":"" 46 | },{ 47 | "text":"歌单", 48 | "path":"pages/Song/index", 49 | "iconFont":"", 50 | "iconFontActive":"", 51 | "iconImagePath":"", 52 | "iconImageActivePath":"" 53 | } 54 | ] 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /examples/app/components/Lists/MvList.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 38 | 39 | 83 | -------------------------------------------------------------------------------- /src/components/PageScrollView.vue: -------------------------------------------------------------------------------- 1 | 38 | 78 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | // 2 | import Vue from "vue"; 3 | import FastClick from "fastclick"; 4 | import router from "./router"; 5 | import VnodeCache from './effect-core/index' 6 | import VueAppScroller from 'vue-app-scroller' 7 | 8 | import App from "./components/App"; 9 | import PageView from "./components/PageView"; 10 | import PageScrollView from "./components/PageScrollView"; 11 | import TabBar from "./components/TabBar"; 12 | import ScrollView from "./components/ScrollView"; 13 | 14 | import { config } from './util.js' 15 | 16 | // 加载必须的样式文件 17 | require("./base.css"); 18 | 19 | function CreateEffectApp (options){ 20 | // 引入 移动端点击延迟 21 | FastClick.attach(document.body); 22 | 23 | // 安装内置布局组件 24 | Vue.component("PageView", PageView); 25 | Vue.component("PageScrollView", PageScrollView); 26 | Vue.component("ScrollView", ScrollView); 27 | Vue.component("TabBar", TabBar); 28 | 29 | // 使用必须的插件 30 | // 滚动插件 31 | Vue.use(VueAppScroller); 32 | 33 | // 效果插件 34 | Vue.use(VnodeCache, { 35 | router, 36 | tabbar: config.tabBar.list.map(item => `/${item.path}`), 37 | common: "/" + config.commonPage 38 | }); 39 | 40 | // 使用第三方组件 41 | if(options.components.length){ 42 | options.components.map(component =>{ 43 | if(Array.isArray(component)){ 44 | Vue.component(...component); 45 | } else { 46 | throw new Error('component maybe for ["componentName", component]') 47 | } 48 | }) 49 | } 50 | 51 | // 使用第三方插件 52 | if(options.plugins.length){ 53 | options.plugins.map(plugin=>{ 54 | if(Array.isArray(plugin)){ 55 | Vue.use(...plugin); 56 | } else { 57 | Vue.use(plugin); 58 | } 59 | }) 60 | } 61 | 62 | // 使用自定义配置 63 | if(Object.keys(options.config).length){ 64 | Object.assign(Vue.config, options.config) 65 | } 66 | 67 | // 初始化 68 | let newVueConfig = { 69 | router, 70 | render: h => h(App) 71 | } 72 | 73 | // 如果使用 stroe 74 | if(options && options.store){ 75 | newVueConfig.store = options.store; 76 | } 77 | 78 | // 需要在window上挂载一个 vm 79 | window.vm = new Vue(newVueConfig).$mount("#app"); 80 | 81 | } 82 | 83 | export default CreateEffectApp 84 | -------------------------------------------------------------------------------- /examples/app/pages/SingerDetail/tabPosition.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 59 | 96 | -------------------------------------------------------------------------------- /examples/app/service/api.js: -------------------------------------------------------------------------------- 1 | import sendRequest from './axios.js' 2 | import getApiHost from './host'; 3 | const { API_HOST } = getApiHost(); 4 | 5 | export const recommendNewAlbum = params =>{ 6 | return sendRequest({ 7 | path: API_HOST + '/recommend_new_album', 8 | params: params 9 | }) 10 | } 11 | 12 | // mv相关 13 | export const recommendMvList = params =>{ 14 | return sendRequest({ 15 | path: API_HOST + '/mv_list', 16 | params: params 17 | }) 18 | } 19 | export const recommendMvDetailAll = params =>{ 20 | return sendRequest({ 21 | path: API_HOST + '/mv_detail_all', 22 | params: params 23 | }) 24 | } 25 | 26 | // 歌手相关 27 | export const singerList = params =>{ 28 | return sendRequest({ 29 | path: API_HOST + '/singer_list', 30 | params: params 31 | }) 32 | } 33 | export const singerAlbum = params =>{ 34 | return sendRequest({ 35 | path: API_HOST + '/singer_album', 36 | params: params 37 | }) 38 | } 39 | export const singerSong = params =>{ 40 | return sendRequest({ 41 | path: API_HOST + '/singer_song', 42 | params: params 43 | }) 44 | } 45 | export const singerMv = params =>{ 46 | return sendRequest({ 47 | path: API_HOST + '/singer_mv', 48 | params: params 49 | }) 50 | } 51 | export const singerAttention = params =>{ 52 | return sendRequest({ 53 | path: API_HOST + '/singer_attention', 54 | params: params 55 | }) 56 | } 57 | 58 | 59 | // 排行榜相关 60 | export const topList = params =>{ 61 | return sendRequest({ 62 | path: API_HOST + '/top_list', 63 | params: params 64 | }) 65 | } 66 | export const topDetail = params =>{ 67 | return sendRequest({ 68 | path: API_HOST + '/top_detail', 69 | params: params 70 | }) 71 | } 72 | 73 | // 歌曲相关 74 | export const songPlay = params =>{ 75 | return sendRequest({ 76 | path: API_HOST + '/song_play', 77 | params: params 78 | }) 79 | } 80 | export const songLyric = params =>{ 81 | return sendRequest({ 82 | path: API_HOST + '/song_lyric', 83 | params: params 84 | }) 85 | } 86 | export const songDetail = params =>{ 87 | return sendRequest({ 88 | path: API_HOST + '/song_detail', 89 | params: params 90 | }) 91 | } 92 | 93 | // 歌单相关 94 | export const AlbumDetail = params =>{ 95 | return sendRequest({ 96 | path: API_HOST + '/album_detail', 97 | params: params 98 | }) 99 | } 100 | export const AlbumList = params =>{ 101 | return sendRequest({ 102 | path: API_HOST + '/album_list', 103 | params: params 104 | }) 105 | } 106 | -------------------------------------------------------------------------------- /examples/config/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | // Template version: 1.3.1 3 | // see http://vuejs-templates.github.io/webpack for documentation. 4 | 5 | const path = require('path') 6 | const ip = require('ip') 7 | 8 | module.exports = { 9 | dev: { 10 | 11 | // Paths 12 | assetsSubDirectory: 'static', 13 | assetsPublicPath: '/', 14 | proxyTable: {}, 15 | 16 | // Various Dev Server settings 17 | // host: ip.address(), // can be overwritten by process.env.HOST 18 | host: 'localhost', 19 | port: 9090, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined 20 | autoOpenBrowser: false, 21 | errorOverlay: true, 22 | notifyOnErrors: true, 23 | poll: false, // https://webpack.js.org/configuration/dev-server/#devserver-watchoptions- 24 | 25 | // Use Eslint Loader? 26 | // If true, your code will be linted during bundling and 27 | // linting errors and warnings will be shown in the console. 28 | useEslint: false, 29 | // If true, eslint errors and warnings will also be shown in the error overlay 30 | // in the browser. 31 | showEslintErrorsInOverlay: false, 32 | 33 | /** 34 | * Source Maps 35 | */ 36 | 37 | // https://webpack.js.org/configuration/devtool/#development 38 | devtool: 'cheap-module-eval-source-map', 39 | 40 | // If you have problems debugging vue-files in devtools, 41 | // set this to false - it *may* help 42 | // https://vue-loader.vuejs.org/en/options.html#cachebusting 43 | cacheBusting: true, 44 | 45 | cssSourceMap: false 46 | }, 47 | 48 | build: { 49 | // Template for index.html 50 | index: path.resolve(__dirname, '../../demo/examples/index.html'), 51 | 52 | // Paths 53 | assetsRoot: path.resolve(__dirname, '../../demo/examples'), 54 | assetsSubDirectory: 'static', 55 | assetsPublicPath: './', 56 | 57 | /** 58 | * Source Maps 59 | */ 60 | 61 | productionSourceMap: false, 62 | // https://webpack.js.org/configuration/devtool/#production 63 | devtool: '#source-map', 64 | 65 | // Gzip off by default as many popular static hosts such as 66 | // Surge or Netlify already gzip all static assets for you. 67 | // Before setting to `true`, make sure to: 68 | // npm install --save-dev compression-webpack-plugin 69 | productionGzip: false, 70 | productionGzipExtensions: ['js', 'css'], 71 | 72 | // Run the build command with an extra argument to 73 | // View the bundle analyzer report after build finishes: 74 | // `npm run build --report` 75 | // Set to `true` or `false` to always turn it on or off 76 | bundleAnalyzerReport: process.env.npm_config_report 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /examples/app/components/Base/Beating.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 24 | 25 | 86 | -------------------------------------------------------------------------------- /examples/build/webpack.base.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const path = require('path') 3 | const utils = require('./utils') 4 | const config = require('../config') 5 | const vueLoaderConfig = require('./vue-loader.conf') 6 | 7 | function resolve (dir) { 8 | return path.join(__dirname, '..', dir) 9 | } 10 | 11 | const createLintingRule = () => ({ 12 | test: /\.(js|vue)$/, 13 | loader: 'eslint-loader', 14 | enforce: 'pre', 15 | include: [resolve('src'), resolve('test')], 16 | options: { 17 | formatter: require('eslint-friendly-formatter'), 18 | emitWarning: !config.dev.showEslintErrorsInOverlay 19 | } 20 | }) 21 | 22 | module.exports = { 23 | context: path.resolve(__dirname, '../'), 24 | entry: { 25 | app: './app/app.js' 26 | }, 27 | output: { 28 | path: config.build.assetsRoot, 29 | filename: '[name].js', 30 | publicPath: process.env.NODE_ENV === 'production' 31 | ? config.build.assetsPublicPath 32 | : config.dev.assetsPublicPath 33 | }, 34 | resolve: { 35 | extensions: ['.js', '.vue', '.json'], 36 | alias: { 37 | 'vue$': 'vue/dist/vue.esm.js', 38 | '@': resolve('app'), 39 | // 'app': resolve('app') 40 | } 41 | }, 42 | module: { 43 | rules: [ 44 | ...(config.dev.useEslint ? [createLintingRule()] : []), 45 | { 46 | test: /\.vue$/, 47 | loader: 'vue-loader', 48 | options: vueLoaderConfig 49 | }, 50 | { 51 | test: /\.js$/, 52 | loader: 'babel-loader', 53 | include: [resolve('src'), resolve('test'), resolve('node_modules/webpack-dev-server/client')] 54 | }, 55 | { 56 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, 57 | loader: 'url-loader', 58 | options: { 59 | limit: 10000, 60 | name: utils.assetsPath('img/[name].[hash:7].[ext]') 61 | } 62 | }, 63 | { 64 | test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/, 65 | loader: 'url-loader', 66 | options: { 67 | limit: 10000, 68 | name: utils.assetsPath('media/[name].[hash:7].[ext]') 69 | } 70 | }, 71 | { 72 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, 73 | loader: 'url-loader', 74 | options: { 75 | limit: 10000, 76 | name: utils.assetsPath('fonts/[name].[hash:7].[ext]') 77 | } 78 | } 79 | ] 80 | }, 81 | node: { 82 | // prevent webpack from injecting useless setImmediate polyfill because Vue 83 | // source contains it (although only uses it if it's native). 84 | setImmediate: false, 85 | // prevent webpack from injecting mocks to Node native modules 86 | // that does not make sense for the client 87 | dgram: 'empty', 88 | fs: 'empty', 89 | net: 'empty', 90 | tls: 'empty', 91 | child_process: 'empty' 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /examples/app/components/Lists/MenuList.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 50 | 51 | 109 | -------------------------------------------------------------------------------- /examples/app/pages/Rank/index.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 57 | 58 | 105 | 106 | -------------------------------------------------------------------------------- /examples/build/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const path = require('path') 3 | const config = require('../config') 4 | const ExtractTextPlugin = require('extract-text-webpack-plugin') 5 | const packageConfig = require('../../package.json') 6 | 7 | exports.assetsPath = function (_path) { 8 | const assetsSubDirectory = process.env.NODE_ENV === 'production' 9 | ? config.build.assetsSubDirectory 10 | : config.dev.assetsSubDirectory 11 | 12 | return path.posix.join(assetsSubDirectory, _path) 13 | } 14 | 15 | exports.cssLoaders = function (options) { 16 | options = options || {} 17 | 18 | const cssLoader = { 19 | loader: 'css-loader', 20 | options: { 21 | sourceMap: options.sourceMap 22 | } 23 | } 24 | 25 | const postcssLoader = { 26 | loader: 'postcss-loader', 27 | options: { 28 | sourceMap: options.sourceMap 29 | } 30 | } 31 | 32 | // generate loader string to be used with extract text plugin 33 | function generateLoaders (loader, loaderOptions) { 34 | const loaders = options.usePostCSS ? [cssLoader, postcssLoader] : [cssLoader] 35 | 36 | if (loader) { 37 | loaders.push({ 38 | loader: loader + '-loader', 39 | options: Object.assign({}, loaderOptions, { 40 | sourceMap: options.sourceMap 41 | }) 42 | }) 43 | } 44 | 45 | // Extract CSS when that option is specified 46 | // (which is the case during production build) 47 | if (options.extract) { 48 | return ExtractTextPlugin.extract({ 49 | use: loaders, 50 | fallback: 'vue-style-loader' 51 | }) 52 | } else { 53 | return ['vue-style-loader'].concat(loaders) 54 | } 55 | } 56 | 57 | // https://vue-loader.vuejs.org/en/configurations/extract-css.html 58 | return { 59 | css: generateLoaders(), 60 | postcss: generateLoaders(), 61 | less: generateLoaders('less'), 62 | sass: generateLoaders('sass', { indentedSyntax: true }), 63 | scss: generateLoaders('sass'), 64 | stylus: generateLoaders('stylus'), 65 | styl: generateLoaders('stylus') 66 | } 67 | } 68 | 69 | // Generate loaders for standalone style files (outside of .vue) 70 | exports.styleLoaders = function (options) { 71 | const output = [] 72 | const loaders = exports.cssLoaders(options) 73 | 74 | for (const extension in loaders) { 75 | const loader = loaders[extension] 76 | output.push({ 77 | test: new RegExp('\\.' + extension + '$'), 78 | use: loader 79 | }) 80 | } 81 | 82 | return output 83 | } 84 | 85 | exports.createNotifierCallback = () => { 86 | const notifier = require('node-notifier') 87 | 88 | return (severity, errors) => { 89 | if (severity !== 'error') return 90 | 91 | const error = errors[0] 92 | const filename = error.file && error.file.split('!').pop() 93 | 94 | notifier.notify({ 95 | title: packageConfig.name, 96 | message: severity + ': ' + error.name, 97 | subtitle: filename || '', 98 | icon: path.join(__dirname, 'logo.png') 99 | }) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/effect-core/vnode-cache.js: -------------------------------------------------------------------------------- 1 | export default (bus, tabbar) => { 2 | return { 3 | name: 'vnode-cache', 4 | abstract: true, 5 | props: {}, 6 | data: () => ({ 7 | routerLen: 0, 8 | route: {}, 9 | to: {}, 10 | from: {}, 11 | tabBar: tabbar, 12 | // direction: '', 13 | paths: [] 14 | }), 15 | computed: {}, 16 | watch: { 17 | route (to, from) { 18 | this.to = to 19 | this.from = from 20 | let findTo = this.tabBar.findIndex(item => item === this.$route.fullPath) 21 | if (findTo === -1) { 22 | this.paths.push(to.fullPath) 23 | this.paths = [...new Set(this.paths)] 24 | } 25 | // console.log(window.sessionStorage) 26 | // console.log('路径:', this.paths) 27 | // console.log('路由', this.$router.options.routes) 28 | } 29 | }, 30 | created () { 31 | this.cache = {} 32 | this.routerLen = this.$router.options.routes.length 33 | this.route = this.$route 34 | this.to = this.$route 35 | // 监听返回事件 36 | bus.$on('reverse', () => { 37 | // this.direction = 'reverse' 38 | this.reverse() 39 | }) 40 | // 监听前进事件 41 | // bus.$on('forward', () => { 42 | // this.direction = 'forward' 43 | // }) 44 | }, 45 | destroyed () { 46 | for (const key in this.cache) { 47 | const vnode = this.cache[key] 48 | vnode && vnode.componentInstance.$destroy() 49 | } 50 | }, 51 | methods: { 52 | reverse () { 53 | let beforePath = this.paths.pop() 54 | 55 | let routes = this.$router.options.routes 56 | // 查询是不是导航路由 57 | let isTabBar = this.tabBar.findIndex(item => item === this.$route.fullPath) 58 | // 查询当前路由在路由列表中的位置 59 | let routerIndex = routes.findIndex(item => item.path === beforePath) 60 | // 当不是导航路由,并且不是默认配置路由 61 | if (isTabBar === -1 && routerIndex >= this.routerLen) { 62 | // 清除对应历史记录 63 | delete window.$VueAppEffect[beforePath] 64 | window.$VueAppEffect.count -= 1 65 | } 66 | // 当不是导航的时候 删除上一个缓存 67 | let key = isTabBar === -1 ? this.$route.fullPath : '' 68 | if (this.cache[key]) { 69 | this.cache[beforePath].componentInstance.$destroy() 70 | delete this.cache[beforePath] 71 | } 72 | // console.log('删除:', this.cache) 73 | } 74 | }, 75 | render () { 76 | // 保存路由 77 | this.route = this.$route 78 | // 得到 vnode 79 | const vnode = this.$slots.default ? this.$slots.default[0] : null 80 | // 如果 vnode 存在 81 | if (vnode) { 82 | // vnode.key = vnode.key || (vnode.isComment ? 'comment' : vnode.tag) 83 | let findTo = this.tabBar.findIndex(item => item === this.$route.fullPath) 84 | let key = findTo === -1 ? this.$route.fullPath : '/tab-bar' 85 | // 判断是否缓存过了 86 | if (this.cache[key]) { 87 | vnode.componentInstance = this.cache[key].componentInstance 88 | // console.log('激活', this.cache) 89 | } else { 90 | this.cache[key] = vnode 91 | // console.log('新增', this.cache) 92 | } 93 | 94 | vnode.data.keepAlive = true 95 | } 96 | return vnode 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/effect-core/direction.js: -------------------------------------------------------------------------------- 1 | function deriection(router,bus,tabbar,common){ 2 | // 添加一个新的战队列存放 3 | router.$task = []; 4 | const tabBar = router.options.routes[0].children 5 | 6 | console.log(tabBar) 7 | 8 | let isPush = false 9 | let endTime = Date.now() 10 | let methods = ['push', 'go', 'replace', 'forward', 'back'] 11 | document.addEventListener('touchend', () => { 12 | endTime = Date.now() 13 | }) 14 | methods.forEach(key => { 15 | let method = router[key].bind(router) 16 | router[key] = function (...args) { 17 | isPush = true 18 | method.apply(null, args) 19 | } 20 | }) 21 | 22 | router.beforeEach((to, from, next)=>{ 23 | // 如果是外链直接跳转 24 | if (/\/http/.test(to.path)) { 25 | window.location.href = to.path 26 | return 27 | } 28 | // 否则保存待跳转的路由 29 | router.$task.push(router.history.pending) 30 | 31 | console.log(router) 32 | 33 | // 不是外链的情况下 34 | // 得到来去的路由序列编号 35 | let toIndex = Number(window.$VueAppEffect[to.path]) 36 | let fromIndex = Number(window.$VueAppEffect[from.path]) 37 | fromIndex = fromIndex ? fromIndex : 0 38 | // 进入新路由 判断是否为 tabBar 39 | let toIsTabBar = tabbar.findIndex(item => item.path === to.path) 40 | // 当前路由 判断是否为 tabBar 41 | // let formIsTabBar = tabbar.findIndex(item => item === from.path) 42 | // 不是进入 tabBar 路由 -------------------------- 43 | if (toIsTabBar === -1) { 44 | // 层级大于0 即非导航层级 45 | if (toIndex > 0) { 46 | // 判断是不是返回 47 | if (toIndex > fromIndex) { // 不是返回 48 | bus.$emit('forward', { 49 | type:'forward', 50 | isTabBar:false, 51 | transitionName:'vue-app-effect-in' 52 | }) 53 | window.$VueAppEffect.paths.push(to.path) 54 | } else { // 是返回 55 | // 判断是否是ios左滑返回 56 | if (!isPush && (Date.now() - endTime) < 377) { 57 | bus.$emit('reverse', { 58 | type:'', 59 | isTabBar:false, 60 | transitionName:'vue-app-effect-out' 61 | }) 62 | } else { 63 | bus.$emit('reverse', { 64 | type:'reverse', 65 | isTabBar:false, 66 | transitionName:'vue-app-effect-out' 67 | }) 68 | } 69 | } 70 | // 是返回 71 | } else { 72 | let count = ++ window.$VueAppEffect.count 73 | window.$VueAppEffect.count = count 74 | window.$VueAppEffect[to.path] = count 75 | bus.$emit('forward', { 76 | type:'forward', 77 | isTabBar:false, 78 | transitionName:'vue-app-effect-in' 79 | }) 80 | window.$VueAppEffect.paths.push(to.path) 81 | } 82 | // 是进入 tabbar 路由 --------------------------------------- 83 | } else { 84 | window.$VueAppEffect.paths.pop() 85 | // 判断是否是ios左滑返回 86 | if (!isPush && (Date.now() - endTime) < 377) { 87 | bus.$emit('reverse', { 88 | type:'', 89 | isTabBar:true, 90 | transitionName:'vue-app-effect-out' 91 | }) 92 | } else { 93 | bus.$emit('reverse', { 94 | type:'reverse', 95 | isTabBar:true, 96 | transitionName:'vue-app-effect-out' 97 | }) 98 | } 99 | window.$VueAppEffect.paths.push(to.path) 100 | } 101 | next() 102 | }) 103 | 104 | router.afterEach(function () { 105 | isPush = false 106 | }) 107 | } 108 | 109 | export default deriection 110 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-app-effect", 3 | "version": "1.0.4", 4 | "description": "A template that simulates app page switching and caching effects", 5 | "private": false, 6 | "scripts": { 7 | "dev": "cd examples && webpack-dev-server --inline --progress --config build/webpack.dev.conf.js", 8 | "start": "npm run dev", 9 | "build": "npm run build:examples", 10 | "build:examples": "cd examples && node build/build.js" 11 | }, 12 | "main": "src/index.js", 13 | "module": "src/index.js", 14 | "keywords": [ 15 | "app", 16 | "effect", 17 | "navigation", 18 | "vue", 19 | "vuex", 20 | "vue-router", 21 | "vue-app-effect" 22 | ], 23 | "files": [ 24 | "demo", 25 | "src", 26 | "examples" 27 | ], 28 | "author": "joozh ", 29 | "license": "MIT", 30 | "bugs": { 31 | "url": "https://github.com/JooZh/vue-app-effect/issues" 32 | }, 33 | "homepage": "https://github.com/JooZh/vue-app-effect#readme", 34 | "repository": { 35 | "type": "git", 36 | "url": "git+https://github.com/JooZh/vue-app-effect.git" 37 | }, 38 | "dependencies": {}, 39 | "devDependencies": { 40 | "autoprefixer": "^7.1.2", 41 | "axios": "^0.18.0", 42 | "babel-core": "^6.22.1", 43 | "babel-helper-vue-jsx-merge-props": "^2.0.3", 44 | "babel-loader": "^7.1.1", 45 | "babel-plugin-import": "^1.8.0", 46 | "babel-plugin-syntax-jsx": "^6.18.0", 47 | "babel-plugin-transform-runtime": "^6.22.0", 48 | "babel-plugin-transform-vue-jsx": "^3.5.0", 49 | "babel-preset-env": "^1.3.2", 50 | "babel-preset-stage-2": "^6.22.0", 51 | "chalk": "^2.0.1", 52 | "copy-webpack-plugin": "^4.0.1", 53 | "css-loader": "^0.28.0", 54 | "extract-text-webpack-plugin": "^3.0.0", 55 | "fastclick": "^1.0.6", 56 | "file-loader": "^1.1.4", 57 | "filesize": "^3.5.10", 58 | "friendly-errors-webpack-plugin": "^1.6.1", 59 | "html-webpack-plugin": "^2.30.1", 60 | "ip": "^1.1.5", 61 | "iview": "^3.5.4", 62 | "music-api-for-qq": "0.0.5", 63 | "node-notifier": "^5.1.2", 64 | "optimize-css-assets-webpack-plugin": "^3.2.0", 65 | "ora": "^1.2.0", 66 | "portfinder": "^1.0.13", 67 | "postcss-import": "^11.0.0", 68 | "postcss-loader": "^2.0.8", 69 | "postcss-url": "^7.2.1", 70 | "rimraf": "^2.6.0", 71 | "rollup": "^0.41.6", 72 | "rollup-plugin-babel": "^2.7.1", 73 | "rollup-plugin-progress": "^0.2.1", 74 | "semver": "^5.3.0", 75 | "shelljs": "^0.7.6", 76 | "stylus": "^0.54.5", 77 | "stylus-loader": "^3.0.2", 78 | "uglify-es": "^3.0.26", 79 | "uglify-js": "^3.0.26", 80 | "uglifyjs-webpack-plugin": "^1.1.1", 81 | "url-loader": "^0.5.8", 82 | "vue": "^2.5.2", 83 | "vue-app-scroller": "^1.0.5", 84 | "vue-awesome-swiper": "^3.1.3", 85 | "vue-axios": "^2.1.3", 86 | "vue-lazyload": "^1.3.3", 87 | "vue-loader": "^13.3.0", 88 | "vue-router": "^3.0.1", 89 | "vue-style-loader": "^3.0.1", 90 | "vue-template-compiler": "^2.5.2", 91 | "vuex": "^3.1.2", 92 | "webpack": "^3.6.0", 93 | "webpack-bundle-analyzer": "^2.9.0", 94 | "webpack-dev-server": "^2.9.1", 95 | "webpack-merge": "^4.1.0" 96 | }, 97 | "engines": { 98 | "node": ">= 6.0.0", 99 | "npm": ">= 3.0.0" 100 | }, 101 | "browserslist": [ 102 | "> 1%", 103 | "last 2 versions", 104 | "not ie <= 8" 105 | ], 106 | "directories": { 107 | "doc": "docs", 108 | "example": "examples" 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /examples/build/webpack.dev.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const utils = require('./utils') 3 | const webpack = require('webpack') 4 | const config = require('../config') 5 | const merge = require('webpack-merge') 6 | const path = require('path') 7 | const baseWebpackConfig = require('./webpack.base.conf') 8 | const CopyWebpackPlugin = require('copy-webpack-plugin') 9 | const HtmlWebpackPlugin = require('html-webpack-plugin') 10 | const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin') 11 | const portfinder = require('portfinder') 12 | const musicApi = require("music-api-for-qq"); 13 | 14 | const HOST = process.env.HOST 15 | const PORT = process.env.PORT && Number(process.env.PORT) 16 | 17 | const devWebpackConfig = merge(baseWebpackConfig, { 18 | module: { 19 | rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, usePostCSS: true }) 20 | }, 21 | // cheap-module-eval-source-map is faster for development 22 | devtool: config.dev.devtool, 23 | 24 | // these devServer options should be customized in /config/index.js 25 | devServer: { 26 | clientLogLevel: 'warning', 27 | historyApiFallback: { 28 | rewrites: [ 29 | { from: /.*/, to: path.posix.join(config.dev.assetsPublicPath, 'index.html') }, 30 | ], 31 | }, 32 | hot: true, 33 | contentBase: false, // since we use CopyWebpackPlugin. 34 | compress: true, 35 | host: HOST || config.dev.host, 36 | port: PORT || config.dev.port, 37 | open: config.dev.autoOpenBrowser, 38 | overlay: config.dev.errorOverlay 39 | ? { warnings: false, errors: true } 40 | : false, 41 | publicPath: config.dev.assetsPublicPath, 42 | proxy: config.dev.proxyTable, 43 | quiet: true, // necessary for FriendlyErrorsPlugin 44 | watchOptions: { 45 | poll: config.dev.poll, 46 | }, 47 | before: function(app) { 48 | app.use('/music',musicApi.router('/api')) 49 | } 50 | }, 51 | plugins: [ 52 | new webpack.DefinePlugin({ 53 | 'process.env': require('../config/dev.env') 54 | }), 55 | new webpack.HotModuleReplacementPlugin(), 56 | new webpack.NamedModulesPlugin(), // HMR shows correct file names in console on update. 57 | new webpack.NoEmitOnErrorsPlugin(), 58 | // https://github.com/ampedandwired/html-webpack-plugin 59 | new HtmlWebpackPlugin({ 60 | filename: 'index.html', 61 | template: 'index.html', 62 | inject: true 63 | }), 64 | // copy custom static assets 65 | new CopyWebpackPlugin([ 66 | { 67 | from: path.resolve(__dirname, '../static'), 68 | to: config.dev.assetsSubDirectory, 69 | ignore: ['.*'] 70 | } 71 | ]) 72 | ] 73 | }) 74 | 75 | module.exports = new Promise((resolve, reject) => { 76 | portfinder.basePort = process.env.PORT || config.dev.port 77 | portfinder.getPort((err, port) => { 78 | if (err) { 79 | reject(err) 80 | } else { 81 | // publish the new Port, necessary for e2e tests 82 | process.env.PORT = port 83 | // add port to devServer config 84 | devWebpackConfig.devServer.port = port 85 | 86 | // Add FriendlyErrorsPlugin 87 | devWebpackConfig.plugins.push(new FriendlyErrorsPlugin({ 88 | compilationSuccessInfo: { 89 | messages: [`Your application is running here: http://${devWebpackConfig.devServer.host}:${port}`], 90 | }, 91 | onErrors: config.dev.notifyOnErrors 92 | ? utils.createNotifierCallback() 93 | : undefined 94 | })) 95 | 96 | resolve(devWebpackConfig) 97 | } 98 | }) 99 | }) 100 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue-app-effect 2 | 实现模拟原生app页面切换效果和缓存效果的前端设计方案, 行为上模拟了app的操作模式,前进刷新,后退缓存,保存数据和页面状态。并且可以始终保存tab菜单页面的内容不会被路由切换清除。 3 | 4 | ## 使用前提 5 | 需要 vue 2.x , vue-router 2.x 6 | 7 | 库只是一个核心处理文件,页面结构和配置还需要参考 examples 文件夹中的结果进行搭建 8 | 9 | 注意:每个路由下的组件根节点都需要采用绝对定位的方式,不采用的话切换会有一定问题。 10 | 11 | 推荐:每个路由页面采用 [vue-app-scroller](https://github.com/JooZh/vue-app-scroller) 滚动插件来处理页面的滚动。可以不用自行处理路由切换后滚动条位置的问题。 12 | 13 | 也可以使用 `-webkit-overflow-scrolling:touch` 对固定容器进行溢出滚动的方式,使用这个需要自行处理滚动条位置问题, 14 | 15 | ## 在线演示 16 | 17 | [Demo演示示例](https://joozh.github.io/vue-app-effect/examples) 18 | 19 | ## 安装使用 20 | 21 | ```bash 22 | $ npm install vue-app-effect -S 23 | ``` 24 | 25 | ### 参数配置 options 26 | 27 | ```js 28 | import Vue from 'vue' 29 | import router from './router' 30 | import VnodeCache from 'vue-app-effect' 31 | // 参数配置 32 | Vue.use(VnodeCache, { 33 | router, // 必须 34 | tabbar: ['/bar1', '/bar2', '/bar3', '/bar4'], // 必须: 35 | common: '/common_route' 36 | }) 37 | ``` 38 | 参数 必须:[ tabbar ] 导航菜单的路由名称建议带 / 。 39 | 40 | 参数 可选:[ common ] 可以添加一个公共路由,这个路由可以在任何地方都能打开。 41 | 42 | ### 监听事件 event 43 | 实例化 `vue-app-effect` 使用 `this.$vueAppEffect.on()` 进行事件监听。 44 | 45 | 每个一级路由下面都需要进行事件监听来处理前进和后退的结果。 46 | ```js 47 | data () { 48 | return { 49 | Direction:{ 50 | type: '', 51 | isTabBar: true, 52 | transitionName: '' 53 | } 54 | } 55 | }, 56 | created () { 57 | // 监听前进事件 58 | this.$vueAppEffect.on('forward', (direction) => { 59 | this.Direction = direction 60 | // direction = {type:'forward',isTabBar:false,transitionName: ''} 61 | }) 62 | // 监听返回事件 63 | this.$vueAppEffect.on('reverse', (direction) => { 64 | this.Direction = direction 65 | // direction = {type:'reverse',isTabBar:false,transitionName: ''} 66 | }) 67 | } 68 | ``` 69 | ### 数据缓存 70 | 71 | 实例化 `vue-app-effect` 之后可以使用 `` 进行非导航菜单缓存管理,类似于 `` 72 | 73 | ```vue 74 | 84 | ``` 85 | 在 tabbar 路由的子路由的容器下使用 `` 进行导航页面的路由缓存。 86 | 87 | ```vue 88 | 95 | ``` 96 | 97 | 具体配置参考 [示例文件](https://github.com/JooZh/vue-app-effect/tree/master/examples) 98 | 99 | ## 动态路由实现多组件独立复用 100 | 101 | 在有些情况下需要不同路由打开同一个组件,而且在同一个组件中打开自己,这种情况下可以采用动态注册路由的方式 102 | 103 | 路由部分,将被需要复用的组件先读取,然后保存在可全局调用的地方。 104 | 105 | ```js 106 | import Router from 'vue-router' 107 | // 需要被复用的组件 108 | import Detail from '@/components/Detail/index' 109 | // 每个动态注册的路由重复使用的页面组件。 110 | Vue.prototype.repeatComponents = { 111 | Detail 112 | } 113 | ``` 114 | 实例化 `vue-app-effect` 后会得到一个 next 方法 使用 `this.$vueAppEffect.next()` 进行复用组件调用的跳转 115 | 116 | ```js 117 | methods:{ 118 | goDetail (index, name) { 119 | this.$vueAppEffect.next({ 120 | vm:this, // 必传,当前的 this 121 | path:`/movie/${index}`, // 必传,跳转的路由 122 | component:this.repeatComponents.Detail, // 跳转到的组件,可以是保存的复用组件 123 | params:{ id: index, name: name } // 传递的参数 124 | }) 125 | } 126 | } 127 | 128 | ``` 129 | 130 | 实例化 `vue-app-effect` 后会得到一个 back 方法 使用 `this.$vueAppEffect.back()` 进行 回退操作,一般存在于头部 131 | 132 | ```js 133 | methods: { 134 | back () { 135 | this.$vueAppEffect.back(this) // 只需要传入 this 136 | } 137 | } 138 | 139 | ``` 140 | -------------------------------------------------------------------------------- /examples/app/store/mutations.js: -------------------------------------------------------------------------------- 1 | import * as types from "./types"; 2 | 3 | function find(list,id){ 4 | return list.findIndex(item => item.song_id === id); 5 | } 6 | export default { 7 | // 路由方向 8 | [types.playBackType](state, payload) { 9 | state.backType = payload; 10 | }, 11 | 12 | // 切换播放器状态 13 | [types.togglePlayer](state) { 14 | // 当无播放列表的时候 15 | if (state.playerList.length === 0) { 16 | state.playStatus = false; 17 | } 18 | state.playerState = !state.playerState; 19 | }, 20 | 21 | // 非播放列表点击全部播放 22 | [types.playAll](state, list) { 23 | // 数组深拷贝防止播放列表的歌曲和当前列表相同被导致删除被修改 24 | let listArr = []; 25 | list.forEach(item => { 26 | listArr.push(item); 27 | }); 28 | state.playerList = listArr; 29 | state.playSong = listArr[0]; 30 | state.playerState = !state.playerState; 31 | state.playSongindex = 0; 32 | state.playStatus = true 33 | }, 34 | 35 | // 非播放列表点击播放单首 36 | [types.playOne](state, song) { 37 | // 传过来的是song对象进行对象深拷贝 38 | let index = find(state.playerList, song.song_id) 39 | if(index === -1){ 40 | let newSong = JSON.parse(JSON.stringify(song)); 41 | state.playerList.unshift(newSong); 42 | state.playSongindex = 0; 43 | state.playSong = state.playerList[0]; 44 | } else { 45 | state.playSong = state.playerList[index]; 46 | state.playSongindex = index; 47 | } 48 | }, 49 | 50 | // 清空播放列表 51 | [types.clearList](state) { 52 | state.playerList = []; 53 | state.playSongindex = 0; 54 | }, 55 | 56 | // 播放列表点击播放 57 | [types.playSome](state, index) { 58 | state.playSong = state.playerList[index]; 59 | state.playSongindex = index; 60 | }, 61 | 62 | // 点击添加歌曲 63 | [types.playAdd](state, song) { 64 | let newSong = JSON.parse(JSON.stringify(song)); 65 | if(find(state.playerList, song.song_id) === -1){ 66 | state.playerList.unshift(newSong); 67 | } 68 | }, 69 | 70 | // 删除一条歌曲 71 | [types.delOne](state, index) { 72 | if (index === state.playSongindex) { 73 | state.playSong = state.playerList[index + 1]; 74 | } else if (index < state.playSongindex) { 75 | state.playSongindex -= 1; 76 | } 77 | state.playerList.splice(index, 1); 78 | }, 79 | 80 | // 点击下一曲 81 | [types.next](state) { 82 | let long = state.playerList.length - 1; 83 | if (state.playSongindex !== long) { 84 | state.playSongindex += 1; 85 | } else { 86 | state.playSongindex = 0; 87 | } 88 | state.playSong = {}; 89 | state.playSong = state.playerList[state.playSongindex]; 90 | }, 91 | 92 | // 点击上一曲 93 | [types.prev](state) { 94 | if (state.playSongindex !== 0) { 95 | state.playSongindex -= 1; 96 | } else { 97 | state.playSongindex = state.playerList.length - 1; 98 | } 99 | state.playSong = {}; 100 | state.playSong = state.playerList[state.playSongindex]; 101 | }, 102 | 103 | // 重复播放 104 | [types.repeat](state) { 105 | let red = state.playSong; 106 | state.playSong = {}; 107 | state.playSong = red; 108 | }, 109 | 110 | // 随机播放 111 | [types.random](state) { 112 | let random = Math.floor(Math.random() * state.playerList.length); 113 | console.log(random); 114 | state.playSongindex = random; 115 | state.playSong = {}; 116 | state.playSong = state.playerList[random]; 117 | }, 118 | 119 | // 点击播放 120 | [types.play](state) { 121 | state.playStatus = true; 122 | }, 123 | 124 | // 点击暂停 125 | [types.pause](state) { 126 | state.playStatus = false; 127 | }, 128 | 129 | // mv的数据 130 | [types.playMv](state, mvObj) { 131 | state.playMvData = mvObj; 132 | } 133 | }; 134 | -------------------------------------------------------------------------------- /examples/app/components/Lists/SongList.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 86 | 87 | 150 | -------------------------------------------------------------------------------- /examples/app/pages/RankDetail/index.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 82 | 83 | 135 | -------------------------------------------------------------------------------- /src/base.css: -------------------------------------------------------------------------------- 1 | 2 | /*** 切换效果css ***/ 3 | .vue-app-effect-out-enter-active, 4 | .vue-app-effect-out-leave-active, 5 | .vue-app-effect-in-enter-active, 6 | .vue-app-effect-in-leave-active { 7 | will-change: transform; 8 | transition: all 400ms cubic-bezier(.55,0,.1,1); 9 | bottom: 0; 10 | top: 0; 11 | position: absolute; 12 | backface-visibility: hidden; 13 | perspective: 1000; 14 | } 15 | .vue-app-effect-out-enter { 16 | opacity: 0; 17 | transform: translate3d(-70%, 0, 0); 18 | } 19 | .vue-app-effect-out-leave-active { 20 | opacity: 0 ; 21 | transform: translate3d(70%, 0, 0); 22 | } 23 | .vue-app-effect-in-enter { 24 | opacity: 0; 25 | transform: translate3d(70%, 0, 0); 26 | } 27 | .vue-app-effect-in-leave-active { 28 | opacity: 0; 29 | transform: translate3d(-70%, 0, 0); 30 | } 31 | 32 | /*** 布局css ***/ 33 | #vue-app-effect { 34 | width: 100%; 35 | height: 100%; 36 | position: relative; 37 | overflow: hidden; 38 | } 39 | #vue-app-effect__page-view { 40 | width: 100%; 41 | position: absolute; 42 | left: 0; 43 | right: 0; 44 | top: 0; 45 | bottom: 50px; 46 | z-index: 5; 47 | } 48 | 49 | /*导航路由容器*/ 50 | #vue-app-effect__tab-router-view { 51 | width: 100%; 52 | height: 100%; 53 | } 54 | #vue-app-effect__tab-router-view .bd-view { 55 | position: absolute; 56 | top: 40px; 57 | left: 0; 58 | right: 0; 59 | bottom: 0; 60 | overflow: hidden; 61 | } 62 | #vue-app-effect__tab-router-view .bd-view.bd-view-full { 63 | top: 0; 64 | } 65 | #vue-app-effect__tab-router-view .bd-view .container { 66 | position: relative; 67 | } 68 | 69 | /*子路由容器*/ 70 | #vue-app-effect__sub-router-view { 71 | position: relative; 72 | width: 100%; 73 | height: calc(100% + 50px); 74 | background: #252525; 75 | z-index: 12; 76 | } 77 | #vue-app-effect__sub-router-view .bd-view { 78 | position: absolute; 79 | top: 40px; 80 | left: 0; 81 | right: 0; 82 | bottom: 0; 83 | overflow: hidden; 84 | } 85 | #vue-app-effect__sub-router-view .bd-view.bd-view-full { 86 | top: 0; 87 | } 88 | #vue-app-effect__sub-router-view .bd-view .container { 89 | position: relative; 90 | } 91 | 92 | /*导航栏*/ 93 | #vue-app-effect__tab-bar { 94 | height: 50px; 95 | background: #252525; 96 | position: fixed; 97 | bottom: 0; 98 | left: 0; 99 | right: 0; 100 | z-index: 4; 101 | } 102 | #vue-app-effect__tab-bar .nav { 103 | display: flex; 104 | width: 100%; 105 | height: 100%; 106 | } 107 | #vue-app-effect__tab-bar .nav .bar { 108 | flex: 1; 109 | display: flex; 110 | justify-content: center; 111 | align-items: center; 112 | color: #ccc; 113 | text-align: center; 114 | } 115 | #vue-app-effect__tab-bar .nav .bar.router-link-active { 116 | color: #ffcd32; 117 | } 118 | #vue-app-effect__tab-bar .nav .bar .icon { 119 | font-size: 20px; 120 | } 121 | #vue-app-effect__tab-bar .nav .bar .text { 122 | font-size: 10px; 123 | line-height: 18px; 124 | } 125 | 126 | /*顶部导航栏样式*/ 127 | .hd-view { 128 | display: flex; 129 | height: 40px; 130 | font-size: 16px; 131 | line-height: 40px; 132 | text-align: center; 133 | color: #ffcd32; 134 | width: 100%; 135 | position: fixed !important; 136 | top: 0; 137 | left: 0; 138 | right: 0; 139 | z-index: 9; 140 | transition: background 0.4s; 141 | } 142 | .hd-view.bg { 143 | transition: background 0.4s; 144 | background: #252525; 145 | } 146 | .hd-view .title { 147 | flex: 1; 148 | text-align: center; 149 | } 150 | .hd-view .back-btn { 151 | flex: 40px 0 0; 152 | font-size: 28px; 153 | text-align: center; 154 | } 155 | .hd-view .back-btn.hide { 156 | visibility: hidden; 157 | } 158 | .hd-view .show-cd { 159 | flex: 40px 0 0; 160 | font-size: 20px; 161 | text-align: center; 162 | display: flex; 163 | justify-content: center; 164 | align-items: center; 165 | } 166 | .hd-view .show-cd .pi { 167 | width: 30px; 168 | height: 30px; 169 | border-radius: 50%; 170 | border: 1px solid #ffcd32; 171 | display: flex; 172 | justify-content: center; 173 | align-items: center; 174 | } 175 | -------------------------------------------------------------------------------- /examples/app/pages/AlbumDetail/index.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 81 | 82 | 146 | -------------------------------------------------------------------------------- /examples/app/components/Base/Progress.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 92 | 93 | 136 | -------------------------------------------------------------------------------- /examples/app/components/Lists/PlayList.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 70 | 71 | 160 | -------------------------------------------------------------------------------- /README_EN.md: -------------------------------------------------------------------------------- 1 | # vue-app-effect 2 | The front-end design scheme is designed to simulate the switching effect and caching effect of the native App page. The operation mode of the App is simulated on the behavior, and the data and page state are saved by forward refreshing and back caching. And you can always save the content of the TAB menu page without being cleared by the router switch. 3 | 4 | [Englist Document](https://github.com/JooZh/vue-app-effect/blob/master/README_EN.md) 5 | 6 | ## Premise 7 | Need vue 2.x , vue-router 2.x 8 | 9 | The library is only a core file, and the page structure and configuration need to be built against the results in the examples folder 10 | 11 | **Note**: the component root node under each routing needs to be positioned in an absolute manner, and switching without this will be problematic. 12 | 13 | **Recommendation**: each routing page takes [better-scroll](https://github.com/ustbhuangyi/better-scroll) the scroll plug-in handles the scrolling of the page. You don't have to deal with the scroll bar location after routing switching. 14 | 15 | You can use it `-webkit-overflow-scrolling:touch` see the demo example for a way to do overflow scrolling on a stationary container, which requires you to handle the scroll bar location on your own 16 | 17 | ## Demo 18 | 19 | [Simple Demo](https://joozh.github.io/vue-app-effect/) 20 | 21 | [Full Music App Demo](https://joozh.cn/music/) 22 | 23 | ## Install 24 | 25 | ```bash 26 | $ npm install vue-app-effect -S 27 | ``` 28 | 29 | ### Options 30 | 31 | ```js 32 | import Vue from 'vue' 33 | import router from './router' 34 | import VnodeCache from 'vue-app-effect' 35 | // Parameter configuration 36 | Vue.use(VnodeCache, { 37 | router, // necessary 38 | tabbar: ['/bar1', '/bar2', '/bar3', '/bar4'], // necessary 39 | common: '/common_route' // optional 40 | }) 41 | ``` 42 | options necessary:[ tabbar ] The route name of the navigation menu advice with / . 43 | 44 | options optional:[ common ] you can add a public route that can be opened anywhere. 45 | 46 | ### Event 47 | Instantiation `vue-app-effect` use `this.$direction.on()` event monitoring 48 | 49 | ```js 50 | created () { 51 | // listen forward 52 | this.$direction.on('forward', (direction) => { 53 | console.log(direction) //{type:'forward',isTab:false} 54 | }) 55 | // listen reverse 56 | this.$direction.on('reverse', (direction) => { 57 | console.log(direction) // {type:'reverse',isTab:false} 58 | }) 59 | // type value are 'forward', 'reverse', '' 60 | } 61 | ``` 62 | ### `` 63 | 64 | Instantiation `vue-app-effect` and then you can use `` non-navigational menu cache management,similar to the `` 65 | 66 | ```vue 67 | 77 | ``` 78 | Used under the container for subpaths of tabbar routing `` cache for navigation pages. 79 | 80 | ```vue 81 | 88 | ``` 89 | 90 | Specific configuration reference [Sample files](https://github.com/JooZh/vue-app-effect/tree/master/examples) 91 | 92 | ## Dynamic routing realizes multicomponent independent reuse 93 | 94 | In cases where you need different routes to open the same component and open yourself in the same component, you can dynamically register the routing 95 | 96 | router.js 97 | 98 | ```js 99 | import Router from 'vue-router' 100 | // Components that need to be inherited 101 | import Detail from '@/components/Detail/index' 102 | // Mount to prototype 103 | Router.prototype.extends = { 104 | Detail 105 | } 106 | ``` 107 | with methods 108 | 109 | ```js 110 | methods:{ 111 | goDetail (index, name) { 112 | // new router 113 | let newPath = `/movie/${index}` 114 | let newRoute = [{ 115 | path: newPath, 116 | name: newPath, 117 | component: {extends: this.$router.extends.Detail} 118 | }] 119 | // To determine if a routing exists or does not exist add a new routing 120 | let find = this.$router.options.routes.findIndex(item => item.path === newPath) 121 | if (find === -1) { 122 | this.$router.options.routes.push(newRoute[0]) 123 | this.$router.addRoutes(newRoute) 124 | } 125 | // There is a direct jump to routing 126 | this.$router.replace({ 127 | name: newPath, 128 | params: { id: index, name: name } 129 | }) 130 | } 131 | } 132 | 133 | ``` 134 | The back button use a special method 135 | 136 | ```js 137 | methods: { 138 | back () { 139 | window.NavStorage.paths.pop() 140 | let newNavStorage = window.NavStorage.paths.concat([]) 141 | let path = newNavStorage.pop() 142 | this.$router.replace({ 143 | name: path 144 | }) 145 | } 146 | } 147 | 148 | ``` -------------------------------------------------------------------------------- /examples/build/webpack.prod.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const path = require('path') 3 | const utils = require('./utils') 4 | const webpack = require('webpack') 5 | const config = require('../config') 6 | const merge = require('webpack-merge') 7 | const baseWebpackConfig = require('./webpack.base.conf') 8 | const CopyWebpackPlugin = require('copy-webpack-plugin') 9 | const HtmlWebpackPlugin = require('html-webpack-plugin') 10 | const ExtractTextPlugin = require('extract-text-webpack-plugin') 11 | const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin') 12 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin') 13 | 14 | const env = require('../config/prod.env') 15 | 16 | const webpackConfig = merge(baseWebpackConfig, { 17 | module: { 18 | rules: utils.styleLoaders({ 19 | sourceMap: config.build.productionSourceMap, 20 | extract: false, 21 | usePostCSS: true 22 | }) 23 | }, 24 | devtool: config.build.productionSourceMap ? config.build.devtool : false, 25 | output: { 26 | path: config.build.assetsRoot, 27 | filename: utils.assetsPath('js/[name].[chunkhash].js'), 28 | chunkFilename: utils.assetsPath('js/[id].[chunkhash].js') 29 | }, 30 | plugins: [ 31 | // http://vuejs.github.io/vue-loader/en/workflow/production.html 32 | new webpack.DefinePlugin({ 33 | 'process.env': env 34 | }), 35 | new UglifyJsPlugin({ 36 | uglifyOptions: { 37 | compress: { 38 | warnings: false 39 | } 40 | }, 41 | sourceMap: config.build.productionSourceMap, 42 | parallel: true 43 | }), 44 | // extract css into its own file 45 | new ExtractTextPlugin({ 46 | filename: utils.assetsPath('css/[name].[contenthash].css'), 47 | // Setting the following option to `false` will not extract CSS from codesplit chunks. 48 | // Their CSS will instead be inserted dynamically with style-loader when the codesplit chunk has been loaded by webpack. 49 | // It's currently set to `true` because we are seeing that sourcemaps are included in the codesplit bundle as well when it's `false`, 50 | // increasing file size: https://github.com/vuejs-templates/webpack/issues/1110 51 | allChunks: true, 52 | }), 53 | // Compress extracted CSS. We are using this plugin so that possible 54 | // duplicated CSS from different components can be deduped. 55 | new OptimizeCSSPlugin({ 56 | cssProcessorOptions: config.build.productionSourceMap 57 | ? { safe: true, map: { inline: false } } 58 | : { safe: true } 59 | }), 60 | // generate dist index.html with correct asset hash for caching. 61 | // you can customize output by editing /index.html 62 | // see https://github.com/ampedandwired/html-webpack-plugin 63 | new HtmlWebpackPlugin({ 64 | filename: config.build.index, 65 | template: 'index.html', 66 | inject: true, 67 | minify: { 68 | removeComments: true, 69 | collapseWhitespace: true, 70 | removeAttributeQuotes: true 71 | // more options: 72 | // https://github.com/kangax/html-minifier#options-quick-reference 73 | }, 74 | // necessary to consistently work with multiple chunks via CommonsChunkPlugin 75 | chunksSortMode: 'dependency' 76 | }), 77 | // keep module.id stable when vendor modules does not change 78 | new webpack.HashedModuleIdsPlugin(), 79 | // enable scope hoisting 80 | new webpack.optimize.ModuleConcatenationPlugin(), 81 | // split vendor js into its own file 82 | new webpack.optimize.CommonsChunkPlugin({ 83 | name: 'vendor', 84 | minChunks (module) { 85 | // any required modules inside node_modules are extracted to vendor 86 | return ( 87 | module.resource && 88 | /\.js$/.test(module.resource) && 89 | module.resource.indexOf( 90 | path.join(__dirname, '../node_modules') 91 | ) === 0 92 | ) 93 | } 94 | }), 95 | // extract webpack runtime and module manifest to its own file in order to 96 | // prevent vendor hash from being updated whenever app bundle is updated 97 | new webpack.optimize.CommonsChunkPlugin({ 98 | name: 'manifest', 99 | minChunks: Infinity 100 | }), 101 | // This instance extracts shared chunks from code splitted chunks and bundles them 102 | // in a separate chunk, similar to the vendor chunk 103 | // see: https://webpack.js.org/plugins/commons-chunk-plugin/#extra-async-commons-chunk 104 | new webpack.optimize.CommonsChunkPlugin({ 105 | name: 'app', 106 | async: 'vendor-async', 107 | children: true, 108 | minChunks: 3 109 | }), 110 | 111 | // copy custom static assets 112 | new CopyWebpackPlugin([ 113 | { 114 | from: path.resolve(__dirname, '../static'), 115 | to: config.build.assetsSubDirectory, 116 | ignore: ['.*'] 117 | } 118 | ]) 119 | ] 120 | }) 121 | 122 | if (config.build.productionGzip) { 123 | const CompressionWebpackPlugin = require('compression-webpack-plugin') 124 | 125 | webpackConfig.plugins.push( 126 | new CompressionWebpackPlugin({ 127 | asset: '[path].gz[query]', 128 | algorithm: 'gzip', 129 | test: new RegExp( 130 | '\\.(' + 131 | config.build.productionGzipExtensions.join('|') + 132 | ')$' 133 | ), 134 | threshold: 10240, 135 | minRatio: 0.8 136 | }) 137 | ) 138 | } 139 | 140 | if (config.build.bundleAnalyzerReport) { 141 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin 142 | webpackConfig.plugins.push(new BundleAnalyzerPlugin()) 143 | } 144 | 145 | module.exports = webpackConfig 146 | -------------------------------------------------------------------------------- /demo/examples/static/css/app.2d0aa6749630fc98cd589e832010c3a5.css: -------------------------------------------------------------------------------- 1 | #app{width:100%;height:100%}#app #page-view{width:100%;position:absolute;left:0;right:0;top:0;bottom:50px;z-index:5}#app #page-view #tab-router-view{width:100%;height:100%}#app #page-view #tab-router-view .bd-view{position:absolute;top:40px;left:0;right:0;bottom:0;overflow:hidden}#app #page-view #tab-router-view .bd-view.bd-view-full{top:0}#app #page-view #tab-router-view .bd-view .container{position:relative}#app #page-view #sub-router-view{position:relative;width:100%;height:calc(100% + 50px);background:#252525;z-index:12}#app #page-view #sub-router-view .bd-view{position:absolute;top:40px;left:0;right:0;bottom:0;overflow:hidden}#app #page-view #sub-router-view .bd-view.bd-view-full{top:0}#app #page-view #sub-router-view .bd-view .container{position:relative}li[data-v-8391861e],ul[data-v-8391861e]{margin:0;padding:0;border:0;list-style:none}.wrap[data-v-8391861e]{width:40px;height:40px;position:relative;display:-ms-flexbox;display:flex;-ms-flex-pack:center;justify-content:center;-ms-flex-align:center;align-items:center}.wrap .music[data-v-8391861e]{display:inline-block;vertical-align:baseline;width:25px;height:25px}.wrap .music li[data-v-8391861e]{background-color:#ffcd32;margin-left:2px;float:left;width:2px;height:25px}.wrap .music.start .m1[data-v-8391861e]{animation:.8s .1s living-data-v-8391861e linear infinite backwards normal;animation-delay:-1.1s}.wrap .music.start .m2[data-v-8391861e]{animation:.8s .3s living-data-v-8391861e linear infinite backwards normal;animation-delay:-1.3s}.wrap .music.start .m3[data-v-8391861e]{animation:.8s .6s living-data-v-8391861e linear infinite backwards normal;animation-delay:-1.6s}.wrap .music.stop[data-v-8391861e]{position:relative;vertical-align:bottom}.wrap .music.stop .m1[data-v-8391861e]{height:18px;position:relative;top:7px}.wrap .music.stop .m2[data-v-8391861e]{height:23px;position:relative;top:2px}.wrap .music.stop .m3[data-v-8391861e]{height:13px;position:relative;top:12px}@keyframes living-data-v-8391861e{0%{transform:scaleY(1);transform-origin:0 25px}50%{transform:scaleY(.3);transform-origin:0 25px}to{transform:scaleY(1);transform-origin:0 25px}}.play{width:250px;height:250px;border:20px solid #454545;border-radius:50%}.play:before{content:" ";display:block;width:100%;height:100%;background:#222;border-radius:50%}.lists{margin:0 10px 20px;padding-top:20px}.lists .content{font-size:22px;background:#444;height:500px;text-align:center;border-radius:5px;line-height:500px;margin-bottom:20px;margin:0 10px 20px}.lists .info{font-size:16px;display:-ms-flexbox;display:flex}.lists .info p.info-button{-ms-flex:1;flex:1;font-size:16px;background:#444;color:#ccc;display:inline-block;line-height:50px;border-radius:5px;margin:0 10px 20px;padding:0 20px}.mvlist{color:hsla(0,0%,100%,.5);display:-ms-flexbox;display:flex;padding:5px;-ms-flex-wrap:wrap;flex-wrap:wrap}.mvlist .list{-ms-flex:50% 0 0px;flex:50% 0 0}.mvlist .list .detail{padding:5px;position:relative}.mvlist .list .detail .img{width:100%}.mvlist .list .detail .title-box{height:30px;position:relative;line-height:30px}.mvlist .list .detail .title{position:absolute;left:0;right:0;font-size:14px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;word-wrap:normal}.mvlist .list .detail .date{font-size:12px}.mvlist .list .detail .date i{font-size:20px;position:relative;top:-1px}.num{position:absolute;left:10px;top:10px}.vue-app-scroller__container{width:100%;height:100%;position:absolute;top:0;left:0;bottom:0;right:0;overflow:hidden}.vue-app-scroller__content{width:100%}.vue-app-scroller__content .refresh--content{width:100%;height:44px;margin-top:-44px;text-align:center;font-size:14px;color:#aaa;display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center}.vue-app-scroller__content .refresh--content .refresh--content__icons{width:18px;height:18px}.vue-app-scroller__content .refresh--content .refresh--content__icons .icon__arrow{width:16px;height:16px;transform:translateZ(0) rotate(0deg);transition:transform .2s linear}.vue-app-scroller__content .refresh--content.active .refresh--content__icons .icon__arrow{transform:translateZ(0) rotate(180deg)}.vue-app-scroller__content .refresh--content .refresh--content__icons .icon__spinner{width:18px;height:18px}.vue-app-scroller__content .refresh--content .refresh--content__texts{margin-left:5px}.vue-app-scroller__content .refresh--content .refresh--content__texts .refresh__text{color:#69717d}.vue-app-scroller__content .loading--content{width:100%;height:40px;text-align:center;font-size:14px;line-height:40px;color:#aaa;position:relative;display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center}.vue-app-scroller__content .loading--content .spinner-holder{width:18px;height:18px}.vue-app-scroller__content .loading--content .spinner-holder .icon__spinner{width:18px;height:18px;display:block}.vue-app-scroller__content .loading--content .no-data-text{position:absolute;left:0;top:0;width:100%;height:100%;z-index:1}.spinner{fill:#aaa;stroke:#aaa}.hd-view{display:-ms-flexbox;display:flex;height:40px;font-size:16px;line-height:40px;text-align:center;color:#ffcd32;width:100%;position:fixed!important;top:0;left:0;right:0;z-index:9}.hd-view,.hd-view.bg{transition:background .4s}.hd-view.bg{background:#252525}.hd-view .title{-ms-flex:1;flex:1;text-align:center}.hd-view .back-btn{-ms-flex:40px 0 0px;flex:40px 0 0;font-size:28px;text-align:center}.hd-view .back-btn.hide{visibility:hidden}.hd-view .show-cd{-ms-flex:40px 0 0px;flex:40px 0 0;font-size:20px;text-align:center}.hd-view .show-cd,.hd-view .show-cd .pi{display:-ms-flexbox;display:flex;-ms-flex-pack:center;justify-content:center;-ms-flex-align:center;align-items:center}.hd-view .show-cd .pi{width:30px;height:30px;border-radius:50%;border:1px solid #ffcd32}#tab-bar{height:50px;background:#252525;position:fixed;bottom:0;left:0;right:0;z-index:4}#tab-bar .nav{display:-ms-flexbox;display:flex;width:100%;height:100%}#tab-bar .nav .bar{-ms-flex:1;flex:1;display:-ms-flexbox;display:flex;-ms-flex-pack:center;justify-content:center;-ms-flex-align:center;align-items:center;color:#ccc;text-align:center}#tab-bar .nav .bar.router-link-active{color:#ffcd32}#tab-bar .nav .bar .icon{font-size:23px}#tab-bar .nav .bar .text{font-size:10px} -------------------------------------------------------------------------------- /examples/app/pages/SingerDetail/index.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 181 | 182 | 254 | -------------------------------------------------------------------------------- /examples/app/pages/MvPlayer/index.vue: -------------------------------------------------------------------------------- 1 | 42 | 125 | 250 | -------------------------------------------------------------------------------- /examples/app/pages/Singer/index.vue: -------------------------------------------------------------------------------- 1 | 63 | 64 | 178 | 179 | 256 | -------------------------------------------------------------------------------- /examples/app/pages/SongPlayer/index.styl: -------------------------------------------------------------------------------- 1 | @import '../../assets/css/common'; 2 | 3 | #song-player 4 | .player 5 | position fixed 6 | top 0 7 | left 0 8 | bottom 0 9 | right 0 10 | background #232332 11 | overflow hidden 12 | color #fff 13 | z-index 12 14 | .play-bg 15 | position absolute 16 | top 0 17 | left 0 18 | bottom 0 19 | right 0 20 | width: 100% 21 | height: 100% 22 | background: #353535; 23 | -webkit-filter: blur(30px) 24 | filter: blur(30px) 25 | opacity: 0.5 26 | z-index -1 27 | .background 28 | width: 300%; 29 | transform: translate3d(-50%, -30%, 0) 30 | .play-title 31 | height 44px; 32 | display flex 33 | font-size $fontXM 34 | text-align center 35 | color #fff 36 | width 100% 37 | position relative 38 | z-index 10 39 | &:after 40 | background rgba(255,255,255,0.1) 41 | .title 42 | flex 1 43 | text-align center 44 | .song-name 45 | margin-top 8px 46 | font-size 16px 47 | line-height 20px 48 | height 20px 49 | position relative 50 | .singer-name 51 | font-size 10px 52 | height 15px 53 | color rgba(255,255,255,0.7) 54 | position relative 55 | .pix 56 | position absolute 57 | top: 0 58 | left 0 59 | right 0 60 | bottom: 0 61 | overflow hidden 62 | text-overflow: ellipsis; 63 | white-space: nowrap; 64 | word-wrap: normal; 65 | 66 | .back-btn 67 | flex 45px 0 0 68 | font-size 28px 69 | text-align center 70 | .show-cd 71 | flex 45px 0 0 72 | font-size 26px 73 | text-align center 74 | position relative 75 | .play-center 76 | width: 100%; 77 | position: absolute; 78 | bottom: 100px; 79 | left: 0; 80 | right: 0; 81 | top: 0; 82 | display: flex; 83 | justify-content: center; 84 | align-items: center; 85 | .toggleShow{ 86 | z-index: 6; 87 | opacity: 1; 88 | transition: opacity 0.3s ease-in 89 | } 90 | .toggleHide{ 91 | z-index: 1; 92 | opacity: 0; 93 | transition: opacity 0.3s ease-in 94 | } 95 | .circle 96 | width: 300px; 97 | height: 300px; 98 | border-radius: 50%; 99 | border:10px solid rgba(255, 255, 255, 0.2); 100 | justify-content: center; 101 | align-items: center; 102 | overflow: hidden; 103 | position: relative; 104 | display: flex; 105 | &:before 106 | display: block; 107 | content: " "; 108 | width: 100%; 109 | height: 100%; 110 | border:50px solid rgba(33, 33, 33, 1); 111 | border-radius: 50%; 112 | position: absolute; 113 | top 0 114 | left 0; 115 | bottom 0; 116 | right 0; 117 | z-index: 1 118 | .cd 119 | width: 195px; 120 | height: 195px; 121 | border-radius: 50%; 122 | position: relative; 123 | z-index: 2; 124 | background: rgba(255, 255, 255, 0.4); 125 | animation: rotate 20s linear infinite; 126 | &.paused 127 | animation-play-state:paused 128 | .lyric 129 | transition: all 0.5 easing 130 | position absolute 131 | top: 44px 132 | left 10px 133 | right 10px 134 | bottom: 41px 135 | overflow hidden 136 | text-align: center 137 | &.center 138 | justify-content center 139 | align-items: center 140 | display flex 141 | .list 142 | list-style none 143 | line-height: 28px 144 | font-size: 14px 145 | color rgba(255,255,255,0.5) 146 | &.current 147 | color rgba(255,255,255,1) 148 | .play-bottom 149 | color: rgba(255,255,255,0.8) 150 | width: 100%; 151 | height: 100px; 152 | position: absolute; 153 | bottom: 0; 154 | left: 0; 155 | .play-progress 156 | display: flex; 157 | margin-bottom: 20px; 158 | font-size: 10px; 159 | text-align: center; 160 | height: 15px; 161 | line-height:15px; 162 | font-size 12px 163 | .this-time 164 | flex: 0 0 70px; 165 | .progress-box 166 | flex: 1; 167 | display: flex; 168 | justify-content: center; 169 | align-items: center; 170 | .progress 171 | width: 100%; 172 | margin: 0; 173 | .all-time 174 | flex: 0 0 70px; 175 | .play-buttons 176 | display: flex; 177 | .button 178 | vertical-align: middle 179 | text-align: center; 180 | flex: 1; 181 | display: flex; 182 | justify-content: center; 183 | align-items: center; 184 | font-size: 23px; 185 | .loops 186 | font-size: 30px 187 | i 188 | font-weight: 700 !important; 189 | .cltr 190 | font-size: 35px 191 | height: 50px; 192 | position relative 193 | top: -2px 194 | .play-list 195 | height: 100%; 196 | width: 100% 197 | position: absolute; 198 | bottom: 0; 199 | z-index: 20; 200 | transform: translate3d(0,100%,0) 201 | transition: all 0.3s cubic-bezier(0.455, 0.03, 0.515, 0.955); 202 | &.list-show 203 | transition: all 0.3s cubic-bezier(0.455, 0.03, 0.515, 0.955); 204 | transform: translate3d(0,0,0) 205 | .play-list-bg 206 | opacity 0.5 207 | .play-list-bg 208 | height 100% 209 | width: 100% 210 | position: absolute; 211 | bottom: 0; 212 | background: rgba(0,0,0,0.4); 213 | opacity 0 214 | z-index 2 215 | .play-list-container 216 | height 75% 217 | width: 100% 218 | position: absolute; 219 | bottom: 0; 220 | background: rgba(0,0,0,0.8); 221 | z-index 3 222 | 223 | .play-list-nav 224 | display: flex; 225 | height: 50px; 226 | position relative 227 | font-size: 14px 228 | .nav-state 229 | flex: 0 0 120px; 230 | display: flex; 231 | justify-content: center; 232 | align-items: center; 233 | .nav-center 234 | flex: 1 235 | .nav-clear 236 | flex: 0 0 60px; 237 | display: flex; 238 | justify-content: center; 239 | align-items: center; 240 | .icon 241 | font-size: 15px 242 | .play-list-scroll 243 | position: absolute; 244 | top: 50px; 245 | left: 0; 246 | right: 0; 247 | bottom: 50px; 248 | overflow hidden 249 | .close 250 | position: absolute; 251 | bottom: 0; 252 | left: 0; 253 | font-size: 30px 254 | height: 50px; 255 | width: 100%; 256 | text-align: center; 257 | line-height: 50px; 258 | 259 | @media screen and (max-width:480px) 260 | #song-player .player .play-center 261 | .circle 262 | width: 300px; 263 | height: 300px; 264 | &:before 265 | border-width 45px 266 | .cd 267 | width: 182px; 268 | height: 182px; 269 | @media screen and (max-width:375px) 270 | #song-player .player .play-center 271 | .circle 272 | width: 270px; 273 | height: 270px; 274 | &:before 275 | border-width 40px 276 | .cd 277 | width: 167px; 278 | height: 167px; 279 | @media screen and (max-width:320px) 280 | #song-player .player .play-center 281 | .circle 282 | width: 250px; 283 | height: 250px; 284 | &:before 285 | border-width 35px 286 | .cd 287 | width: 158px; 288 | height: 158px; 289 | 290 | @keyframes rotate{ 291 | 0% { transform: rotate(0); } 292 | 100% { transform: rotate(360deg); } 293 | } 294 | 295 | -------------------------------------------------------------------------------- /examples/app/pages/SongPlayer/index.vue: -------------------------------------------------------------------------------- 1 | 105 | 106 | 367 | 368 | 371 | --------------------------------------------------------------------------------