├── 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 |
2 |
3 |
4 |
5 |
6 |
7 |
12 |
--------------------------------------------------------------------------------
/examples/app/components/Base/Icon.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
15 |
16 |
19 |
--------------------------------------------------------------------------------
/src/components/TabCon.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
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 |
2 |
9 | sfafaf
10 | {{ y }}
11 |
12 |
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 |
6 |
{{ title }}
7 |
8 |
9 |
10 |
11 |
12 |
13 |
38 |
--------------------------------------------------------------------------------
/examples/app/components/Base/Loading.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
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 |
2 |
3 |
4 |
5 |
6 |
13 |
14 |
15 |
16 |
17 |
18 |
33 |
--------------------------------------------------------------------------------
/src/components/ScrollView.vue:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
12 |
13 |
45 |
--------------------------------------------------------------------------------
/src/components/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
vue-app-effect MV {{ index + 1 }}
9 |
10 |
播放: {{ index + 1 }}.99 万
11 |
12 |
13 |
14 |
15 |
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 |
2 |
3 |
4 |
当前是{{ name }}详情页{{ id }}
5 |
6 |
7 | Singer详情页{{ id + 1 }}
8 |
9 |
10 | MV详情页{{ id + 1 }}
11 |
12 |
13 |
14 |
15 |
16 |
17 |
52 |
--------------------------------------------------------------------------------
/src/components/PageView.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
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 |
2 |
7 |
8 |
9 | 专辑共 {{total}} 首
10 |
11 |
12 | 全部播放
13 |
14 |
15 |
16 |
17 |
18 |
41 |
42 |
75 |
--------------------------------------------------------------------------------
/examples/app/pages/RankDetail/tabPosition.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 | 排行榜共 {{total}} 首
10 |
11 |
12 | 全部播放
13 |
14 |
15 |
16 |
17 |
18 |
41 |
42 |
75 |
--------------------------------------------------------------------------------
/examples/app/components/Lists/AlbumList.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
{{item.album_name}}
7 |
{{item.pub_time}}
8 |
9 |
10 |
11 |
12 |
13 |
36 |
37 |
72 |
--------------------------------------------------------------------------------
/examples/app/pages/Recomment/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
{{item.mv_title}}
8 |
9 |
{{item.play_str}}
10 |
11 |
12 |
13 |
14 |
15 |
38 |
39 |
83 |
--------------------------------------------------------------------------------
/src/components/PageScrollView.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
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 |
2 |
3 |
4 |
5 | 单曲 {{totals.song_total}}
6 |
7 |
8 | 专辑 {{totals.album_total}}
9 |
10 |
11 | MV {{totals.mv_total}}
12 |
13 |
14 |
15 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
{{item.listen_count}} 万
9 |
10 |
11 |
12 |
13 |
14 |
50 |
51 |
109 |
--------------------------------------------------------------------------------
/examples/app/pages/Rank/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | {{item.top_title}}
10 |
11 |
12 |
13 | {{key+1}} {{value.song_name}} - {{value.singer_name}}
14 |
15 |
16 |
17 |
20 |
21 |
22 |
23 |
24 |
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 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 | ```
85 | 在 tabbar 路由的子路由的容器下使用 `` 进行导航页面的路由缓存。
86 |
87 | ```vue
88 |
89 |
90 |
91 |
92 |
93 |
94 |
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 |
2 |
3 |
4 |
{{index+1}}
5 |
6 |
7 |
8 |
{{item.singers | nameArrgs }} · {{item.album_name}}
9 |
专辑: {{item.album_name }}
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
86 |
87 |
150 |
--------------------------------------------------------------------------------
/examples/app/pages/RankDetail/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
{{topInfo.pts}}
13 |
{{topInfo.listen_str}} 粉丝
14 |
{{topInfo.update_time}} 更新
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
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 |
2 |
3 |
21 |
22 |
23 |
24 |
25 |
81 |
82 |
146 |
--------------------------------------------------------------------------------
/examples/app/components/Base/Progress.vue:
--------------------------------------------------------------------------------
1 |
2 |
16 |
17 |
18 |
92 |
93 |
136 |
--------------------------------------------------------------------------------
/examples/app/components/Lists/PlayList.vue:
--------------------------------------------------------------------------------
1 |
2 |
32 |
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 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 | ```
78 | Used under the container for subpaths of tabbar routing `` cache for navigation pages.
79 |
80 | ```vue
81 |
82 |
83 |
84 |
85 |
86 |
87 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
粉丝: {{singerInfo.singer_fance}}
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
181 |
182 |
254 |
--------------------------------------------------------------------------------
/examples/app/pages/MvPlayer/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
15 |
16 |
17 |
18 |
{{mvInfo.name}}
19 |
{{mvInfo.play_str}}
20 |
21 |
大部分人还爱看
22 |
23 |
24 |
25 |
26 |
27 |
28 | {{item.mv_name}}
29 |
30 |
{{item.play_str}}
31 |
32 |
33 |
34 |
37 |
38 |
39 |
40 |
41 |
42 |
125 |
250 |
--------------------------------------------------------------------------------
/examples/app/pages/Singer/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | {{item.name}}
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | {{item.name}}
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | {{item.name}}
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | {{item.name}}
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
{{item.singer_name}}
50 |
51 |
52 |
53 |
54 |
什么都木有!
55 |
请切换类目继续浏览
56 |
57 |
58 |
59 |
60 |
61 |
62 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
{{playSongName}}
11 |
12 |
13 |
{{playSongSinger}}
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | {{item.text}}
33 |
34 |
35 |
36 |
37 |
38 |
{{playCurrentTime}}
39 |
48 |
{{playInterval}}
49 |
50 |
84 |
85 |
86 |
87 |
88 |
89 |
列表循环 [{{playerList.length}}]
90 |
单曲循环 [{{playerList.length}}]
91 |
随机播放 [{{playerList.length}}]
92 |
93 |
清空
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
367 |
368 |
371 |
--------------------------------------------------------------------------------