├── min-program ├── miniprogram │ ├── components │ │ └── line │ │ │ ├── line.json │ │ │ ├── line.wxml │ │ │ ├── line.js │ │ │ └── line.wxss │ ├── doc │ │ └── line.EAP │ ├── images │ │ └── star.png │ ├── pages │ │ └── index │ │ │ ├── index.json │ │ │ ├── index.wxml │ │ │ ├── index.wxss │ │ │ └── index.js │ ├── sitemap.json │ ├── package.json │ ├── app.json │ ├── package-lock.json │ ├── app.wxss │ ├── app.js │ └── style │ │ └── guide.wxss ├── cloudfunctions │ ├── echo │ │ ├── config.json │ │ ├── index.js │ │ └── package.json │ ├── login │ │ ├── config.json │ │ ├── package.json │ │ └── index.js │ ├── callback │ │ ├── config.json │ │ ├── package.json │ │ └── index.js │ └── openapi │ │ ├── package.json │ │ ├── config.json │ │ └── index.js ├── project.config.json └── README.md ├── view-pic ├── public │ ├── favicon.ico │ └── index.html ├── babel.config.js ├── server │ ├── index.js │ ├── router │ │ ├── picListRoute.js │ │ └── index.js │ ├── controller │ │ └── picListCtl.js │ ├── model │ │ └── picList.js │ └── config │ │ └── connect.js ├── vue.config.js ├── src │ ├── App.vue │ ├── router │ │ └── index.js │ ├── api │ │ └── picListSevice.js │ ├── main.js │ ├── custom │ │ └── layer │ │ │ ├── index.js │ │ │ └── layer.vue │ ├── page │ │ └── view.vue │ └── static │ │ └── reset.css ├── .gitignore ├── README.md └── package.json ├── scroll-demo ├── public │ ├── favicon.ico │ └── index.html ├── babel.config.js ├── src │ ├── main.js │ ├── directive │ │ └── vIscroll.js │ ├── assets │ │ └── reset.css │ ├── App.vue │ └── components │ │ └── tabList.vue ├── .gitignore ├── package.json └── README.md ├── video-player ├── babel.config.js ├── public │ ├── favicon.ico │ └── index.html ├── src │ ├── static │ │ ├── media │ │ │ └── taru.mp4 │ │ ├── fonts │ │ │ ├── iconfont.eot │ │ │ ├── iconfont.ttf │ │ │ ├── iconfont.woff │ │ │ ├── iconfont.woff2 │ │ │ ├── iconfont.json │ │ │ ├── iconfont.css │ │ │ ├── iconfont.svg │ │ │ ├── iconfont.js │ │ │ ├── demo_index.html │ │ │ └── demo.css │ │ └── images │ │ │ └── poster.jpg │ ├── main.js │ ├── router │ │ └── index.js │ └── App.vue ├── .gitignore ├── package.json └── README.md ├── web-cache ├── cache-web │ ├── babel.config.js │ ├── public │ │ ├── favicon.ico │ │ └── index.html │ ├── src │ │ ├── assets │ │ │ ├── loading.gif │ │ │ ├── cache-img.png │ │ │ └── reset.css │ │ ├── main.js │ │ └── App.vue │ ├── .gitignore │ ├── README.md │ ├── vue.config.js │ └── package.json ├── cache-node │ ├── app │ │ ├── public │ │ │ └── cache │ │ │ │ ├── favicon.ico │ │ │ │ ├── static │ │ │ │ ├── img │ │ │ │ │ └── cache-img.6efc6c0d.png │ │ │ │ ├── css │ │ │ │ │ └── app.e5f657b7.css │ │ │ │ └── js │ │ │ │ │ ├── app.86a4b0ce.js │ │ │ │ │ └── app.86a4b0ce.js.map │ │ │ │ └── index.html │ │ ├── index.js │ │ └── api.js │ ├── .gitignore │ └── package.json └── .vscode │ └── launch.json ├── README.md └── audio ├── js ├── main.js └── record.js ├── index.html └── README.md /min-program/miniprogram/components/line/line.json: -------------------------------------------------------------------------------- 1 | { 2 | "component": true 3 | } -------------------------------------------------------------------------------- /min-program/miniprogram/components/line/line.wxml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /min-program/cloudfunctions/echo/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "permissions": { 3 | "openapi": [] 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /min-program/cloudfunctions/login/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "permissions": { 3 | "openapi": [] 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /view-pic/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yuelinghunyu/blog-demo/HEAD/view-pic/public/favicon.ico -------------------------------------------------------------------------------- /scroll-demo/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yuelinghunyu/blog-demo/HEAD/scroll-demo/public/favicon.ico -------------------------------------------------------------------------------- /view-pic/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /scroll-demo/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /video-player/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /video-player/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yuelinghunyu/blog-demo/HEAD/video-player/public/favicon.ico -------------------------------------------------------------------------------- /web-cache/cache-web/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /min-program/miniprogram/doc/line.EAP: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yuelinghunyu/blog-demo/HEAD/min-program/miniprogram/doc/line.EAP -------------------------------------------------------------------------------- /min-program/miniprogram/images/star.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yuelinghunyu/blog-demo/HEAD/min-program/miniprogram/images/star.png -------------------------------------------------------------------------------- /video-player/src/static/media/taru.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yuelinghunyu/blog-demo/HEAD/video-player/src/static/media/taru.mp4 -------------------------------------------------------------------------------- /web-cache/cache-web/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yuelinghunyu/blog-demo/HEAD/web-cache/cache-web/public/favicon.ico -------------------------------------------------------------------------------- /video-player/src/static/fonts/iconfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yuelinghunyu/blog-demo/HEAD/video-player/src/static/fonts/iconfont.eot -------------------------------------------------------------------------------- /video-player/src/static/fonts/iconfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yuelinghunyu/blog-demo/HEAD/video-player/src/static/fonts/iconfont.ttf -------------------------------------------------------------------------------- /video-player/src/static/images/poster.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yuelinghunyu/blog-demo/HEAD/video-player/src/static/images/poster.jpg -------------------------------------------------------------------------------- /web-cache/cache-web/src/assets/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yuelinghunyu/blog-demo/HEAD/web-cache/cache-web/src/assets/loading.gif -------------------------------------------------------------------------------- /video-player/src/static/fonts/iconfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yuelinghunyu/blog-demo/HEAD/video-player/src/static/fonts/iconfont.woff -------------------------------------------------------------------------------- /video-player/src/static/fonts/iconfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yuelinghunyu/blog-demo/HEAD/video-player/src/static/fonts/iconfont.woff2 -------------------------------------------------------------------------------- /web-cache/cache-web/src/assets/cache-img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yuelinghunyu/blog-demo/HEAD/web-cache/cache-web/src/assets/cache-img.png -------------------------------------------------------------------------------- /min-program/cloudfunctions/callback/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "permissions": { 3 | "openapi": [ 4 | "customerServiceMessage.send" 5 | ] 6 | } 7 | } -------------------------------------------------------------------------------- /web-cache/cache-node/app/public/cache/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yuelinghunyu/blog-demo/HEAD/web-cache/cache-node/app/public/cache/favicon.ico -------------------------------------------------------------------------------- /min-program/miniprogram/pages/index/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "usingComponents": { 3 | "custom-line": "../../components/line/line" 4 | }, 5 | "disableScroll": true 6 | } -------------------------------------------------------------------------------- /view-pic/server/index.js: -------------------------------------------------------------------------------- 1 | const Koa = require("koa") 2 | const app = new Koa() 3 | 4 | const routing = require('./router') 5 | routing(app) 6 | app.listen(3000, () => { console.log("服务正常启动") }) -------------------------------------------------------------------------------- /web-cache/cache-node/app/public/cache/static/img/cache-img.6efc6c0d.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yuelinghunyu/blog-demo/HEAD/web-cache/cache-node/app/public/cache/static/img/cache-img.6efc6c0d.png -------------------------------------------------------------------------------- /min-program/miniprogram/sitemap.json: -------------------------------------------------------------------------------- 1 | { 2 | "desc": "关于本文件的更多信息,请参考文档 https://developers.weixin.qq.com/miniprogram/dev/framework/sitemap.html", 3 | "rules": [{ 4 | "action": "allow", 5 | "page": "*" 6 | }] 7 | } -------------------------------------------------------------------------------- /web-cache/cache-web/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | import "@/assets/reset.css" 4 | 5 | Vue.config.productionTip = false 6 | 7 | new Vue({ 8 | render: h => h(App), 9 | }).$mount('#app') 10 | -------------------------------------------------------------------------------- /view-pic/vue.config.js: -------------------------------------------------------------------------------- 1 | const config = { 2 | devServer: { 3 | proxy: { 4 | '/view': { 5 | target: 'http://localhost:3000', 6 | ws: true, 7 | changeOrigin: true 8 | }, 9 | } 10 | } 11 | } 12 | 13 | module.exports = config -------------------------------------------------------------------------------- /min-program/cloudfunctions/echo/index.js: -------------------------------------------------------------------------------- 1 | const cloud = require('wx-server-sdk') 2 | 3 | exports.main = async (event, context) => { 4 | // event.userInfo 是已废弃的保留字段,在此不做展示 5 | // 获取 OPENID 等微信上下文请使用 cloud.getWXContext() 6 | delete event.userInfo 7 | return event 8 | } 9 | -------------------------------------------------------------------------------- /view-pic/server/router/picListRoute.js: -------------------------------------------------------------------------------- 1 | const Router = require("koa-router") 2 | const router = new Router({ prefix: "/view" }) 3 | const { getAllPicList } = require("../controller/picListCtl") 4 | 5 | 6 | router.get("/:page/:limit", getAllPicList) 7 | 8 | module.exports = router -------------------------------------------------------------------------------- /video-player/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | import router from './router' 4 | import "./static/fonts/iconfont.css" 5 | 6 | Vue.config.productionTip = false 7 | 8 | 9 | new Vue({ 10 | render: h => h(App), 11 | router 12 | }).$mount('#app') 13 | -------------------------------------------------------------------------------- /view-pic/src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | 18 | -------------------------------------------------------------------------------- /scroll-demo/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | import "./assets/reset.css" 4 | // 加载scroll指令 5 | import VIscroll from './directive/vIscroll' 6 | Vue.use(VIscroll) 7 | Vue.config.productionTip = false 8 | 9 | new Vue({ 10 | render: h => h(App), 11 | }).$mount('#app') 12 | -------------------------------------------------------------------------------- /scroll-demo/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw? 22 | -------------------------------------------------------------------------------- /video-player/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw? 22 | -------------------------------------------------------------------------------- /view-pic/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw? 22 | -------------------------------------------------------------------------------- /view-pic/server/router/index.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs") 2 | const readFile = (app) => { 3 | fs.readdirSync(__dirname).forEach(file => { 4 | if (file === 'index.js') return 5 | const route = require(`./${file}`) 6 | app.use(route.routes()) 7 | .use(route.allowedMethods()) 8 | }) 9 | } 10 | 11 | module.exports = readFile -------------------------------------------------------------------------------- /web-cache/cache-node/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw? 22 | -------------------------------------------------------------------------------- /web-cache/cache-web/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw? 22 | -------------------------------------------------------------------------------- /min-program/miniprogram/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "miniprogram", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "app.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "jsplumb": "^2.12.11" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /min-program/cloudfunctions/echo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "echo", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "wx-server-sdk": "latest" 13 | } 14 | } -------------------------------------------------------------------------------- /min-program/cloudfunctions/callback/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "callback", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "wx-server-sdk": "latest" 13 | } 14 | } -------------------------------------------------------------------------------- /min-program/cloudfunctions/login/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "login", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "wx-server-sdk": "latest" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /min-program/cloudfunctions/openapi/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "openapi", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "wx-server-sdk": "latest" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /min-program/miniprogram/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "pages": [ 3 | "pages/index/index" 4 | ], 5 | "window": { 6 | "backgroundColor": "#F6F6F6", 7 | "backgroundTextStyle": "light", 8 | "navigationBarBackgroundColor": "#000", 9 | "navigationBarTitleText": "one", 10 | "navigationBarTextStyle": "white" 11 | }, 12 | "sitemapLocation": "sitemap.json" 13 | } -------------------------------------------------------------------------------- /view-pic/src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue" 2 | import VueRouter from 'vue-router' 3 | Vue.use(VueRouter) 4 | 5 | const View = () => import("../page/view") 6 | 7 | const routes = [ 8 | { 9 | path: "/", 10 | redirect: "/view" 11 | }, 12 | { 13 | path: "/view", 14 | component: View 15 | } 16 | ] 17 | const router = new VueRouter({ routes }) 18 | 19 | export default router -------------------------------------------------------------------------------- /video-player/src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue" 2 | import VueRouter from 'vue-router' 3 | Vue.use(VueRouter) 4 | 5 | const Video = () => import("../components/video/video") 6 | 7 | const routes = [ 8 | { 9 | path: "/", redirect: "/video" 10 | }, 11 | { 12 | path: "/video", component: Video 13 | } 14 | ] 15 | 16 | const router = new VueRouter({ routes }) 17 | 18 | export default router -------------------------------------------------------------------------------- /view-pic/src/api/picListSevice.js: -------------------------------------------------------------------------------- 1 | import axios from "axios" 2 | 3 | class PicLiseSevice { 4 | getPicList (param) { 5 | const page = param.page || 1 6 | const limit = param.limit || 10 7 | const url = `http://172.20.10.7:8080/view/${page}/${limit}` 8 | // const url = `http://10.5.113.90:8080/view/${page}/${limit}` 9 | return axios.get(url) 10 | } 11 | } 12 | 13 | export default new PicLiseSevice() -------------------------------------------------------------------------------- /web-cache/cache-node/app/index.js: -------------------------------------------------------------------------------- 1 | const Koa = require("koa") 2 | const app = new Koa() 3 | const path = require("path") 4 | const serve = require("koa-static") 5 | 6 | const route = require("./api") 7 | const public = serve(path.join(__dirname) + '/public/') 8 | 9 | app.use(public) 10 | app.use(route.routes()) 11 | .use(route.allowedMethods()) 12 | 13 | app.listen(3000, () => { 14 | console.log("缓存服务启动成功") 15 | }) -------------------------------------------------------------------------------- /view-pic/README.md: -------------------------------------------------------------------------------- 1 | # view-pic 2 | 3 | ## Project setup 4 | ``` 5 | yarn install 6 | ``` 7 | 8 | ### Compiles and hot-reloads for development 9 | ``` 10 | yarn serve 11 | ``` 12 | 13 | ### Compiles and minifies for production 14 | ``` 15 | yarn build 16 | ``` 17 | 18 | ### Lints and fixes files 19 | ``` 20 | yarn lint 21 | ``` 22 | 23 | ### Customize configuration 24 | See [Configuration Reference](https://cli.vuejs.org/config/). 25 | -------------------------------------------------------------------------------- /view-pic/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | import router from "./router" 4 | import "mso-flex/mso-flex.js" 5 | import VueTouch from "vue-touch" 6 | import Layer from './custom/layer' 7 | import "./static/reset.css" 8 | 9 | Vue.config.productionTip = false 10 | Vue.use(VueTouch, { name: 'v-touch' }) 11 | Vue.use(Layer) 12 | new Vue({ 13 | render: h => h(App), 14 | router 15 | }).$mount('#app') 16 | -------------------------------------------------------------------------------- /web-cache/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // 使用 IntelliSense 了解相关属性。 3 | // 悬停以查看现有属性的描述。 4 | // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [{ 7 | "type": "node", 8 | "request": "launch", 9 | "name": "启动程序", 10 | "skipFiles": [ 11 | "/**" 12 | ], 13 | "program": "${workspaceFolder}\\cache-node\\app\\index.js" 14 | }] 15 | } -------------------------------------------------------------------------------- /web-cache/cache-web/README.md: -------------------------------------------------------------------------------- 1 | # cache-web 2 | 3 | ## Project setup 4 | ``` 5 | yarn install 6 | ``` 7 | 8 | ### Compiles and hot-reloads for development 9 | ``` 10 | yarn serve 11 | ``` 12 | 13 | ### Compiles and minifies for production 14 | ``` 15 | yarn build 16 | ``` 17 | 18 | ### Lints and fixes files 19 | ``` 20 | yarn lint 21 | ``` 22 | 23 | ### Customize configuration 24 | See [Configuration Reference](https://cli.vuejs.org/config/). 25 | -------------------------------------------------------------------------------- /min-program/miniprogram/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "miniprogram", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "jsplumb": { 8 | "version": "2.12.11", 9 | "resolved": "https://registry.npmjs.org/jsplumb/-/jsplumb-2.12.11.tgz", 10 | "integrity": "sha512-xECEU4HpqkhISrMjTAgWd3RzBK6xOua2KeSe8KkLN56moRYzNX9BaNG65rcH3NQkmUwGVJdpeVyDsX8eERTdeg==" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /web-cache/cache-web/vue.config.js: -------------------------------------------------------------------------------- 1 | 2 | const path = require("path") 3 | const config = { 4 | publicPath: process.env.NODE_ENV === 'production' ? "/cache/" : "", 5 | outputDir: path.resolve(__dirname, "../cache-node/app/public/cache"), 6 | assetsDir: "static", 7 | devServer: { 8 | proxy: { 9 | "/cache": { 10 | target: "http://localhost:3000", 11 | changeOrigin: true 12 | } 13 | } 14 | } 15 | } 16 | 17 | module.exports = config -------------------------------------------------------------------------------- /web-cache/cache-node/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cache-node", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "server": "nodemon app/index.js" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "koa": "^2.11.0", 14 | "koa-static": "^5.0.0" 15 | }, 16 | "devDependencies": { 17 | "koa-router": "^8.0.8", 18 | "nodemon": "^2.0.3" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /video-player/src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 13 | 14 | 24 | -------------------------------------------------------------------------------- /min-program/cloudfunctions/openapi/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "permissions": { 3 | "openapi": [ 4 | "wxacode.get", 5 | "subscribeMessage.send", 6 | "subscribeMessage.addTemplate", 7 | "templateMessage.send", 8 | "templateMessage.addTemplate", 9 | "templateMessage.deleteTemplate", 10 | "templateMessage.getTemplateList", 11 | "templateMessage.getTemplateLibraryById", 12 | "templateMessage.getTemplateLibraryList" 13 | ] 14 | } 15 | } -------------------------------------------------------------------------------- /min-program/miniprogram/app.wxss: -------------------------------------------------------------------------------- 1 | /**app.wxss**/ 2 | .container { 3 | display: flex; 4 | flex-direction: column; 5 | align-items: center; 6 | box-sizing: border-box; 7 | } 8 | 9 | button { 10 | background: initial; 11 | } 12 | 13 | button:focus{ 14 | outline: 0; 15 | } 16 | 17 | button::after{ 18 | border: none; 19 | } 20 | 21 | 22 | page { 23 | background: #f6f6f6; 24 | display: flex; 25 | flex-direction: column; 26 | justify-content: flex-start; 27 | } 28 | -------------------------------------------------------------------------------- /min-program/miniprogram/app.js: -------------------------------------------------------------------------------- 1 | //app.js 2 | App({ 3 | onLaunch: function () { 4 | 5 | if (!wx.cloud) { 6 | console.error('请使用 2.2.3 或以上的基础库以使用云能力') 7 | } else { 8 | wx.cloud.init({ 9 | // env 参数说明: 10 | // env 参数决定接下来小程序发起的云开发调用(wx.cloud.xxx)会默认请求到哪个云环境的资源 11 | // 此处请填入环境 ID, 环境 ID 可打开云控制台查看 12 | // 如不填则使用默认环境(第一个创建的环境) 13 | // env: 'my-env-id', 14 | traceUser: true, 15 | }) 16 | } 17 | 18 | this.globalData = {} 19 | } 20 | }) 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 一、前端webrtc基础 —— 录音篇 2 | 利用webrtc进行声音录制,转成pcm文件、wav文件,canvas画音域图,文件播放 3 | 掘金地址:[前端webrtc基础 —— 录音篇 ](https://juejin.im/post/5d8b2c21e51d45781d5e4b74) 4 | ## 二、自定义H5 video 播放器 5 | 基于HTML5标签实现的一个自定义视频播放器。其中实现了播放暂停、进度拖拽、音量控制及全屏等功能 6 | 掘金地址:[自定义H5 video 播放器](https://juejin.im/post/5daef8b6e51d4524e60e0f6a) 7 | ## 三、vue中利用iscroll.js解决pc端滚动问题 8 | 利用vue指令封装iscroll.js,解决pc浏览器滚动和切换居中问题 9 | 掘金地址:[vue中利用iscroll.js解决pc端滚动问题](https://juejin.im/post/5e4506dc51882549417fbdd5) 10 | ## 四、小程序实践 —— 精简版前端连线题 11 | 纯js实现画线条 12 | 掘金地址:[小程序实践 —— 精简版前端连线题](https://juejin.im/post/5e741dd151882549087dc386) -------------------------------------------------------------------------------- /min-program/cloudfunctions/callback/index.js: -------------------------------------------------------------------------------- 1 | const cloud = require('wx-server-sdk') 2 | 3 | cloud.init({ 4 | // API 调用都保持和云函数当前所在环境一致 5 | env: cloud.DYNAMIC_CURRENT_ENV 6 | }) 7 | 8 | // 云函数入口函数 9 | exports.main = async (event, context) => { 10 | 11 | console.log(event) 12 | 13 | const { OPENID } = cloud.getWXContext() 14 | 15 | const result = await cloud.openapi.customerServiceMessage.send({ 16 | touser: OPENID, 17 | msgtype: 'text', 18 | text: { 19 | content: '收到:' + event.Content, 20 | } 21 | }) 22 | 23 | console.log(result) 24 | 25 | return result 26 | } 27 | -------------------------------------------------------------------------------- /video-player/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | video-player 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /view-pic/server/controller/picListCtl.js: -------------------------------------------------------------------------------- 1 | const PicList = require("../model/picList") 2 | 3 | class PicListCtl { 4 | async getAllPicList (ctx) { 5 | const { page, limit } = ctx.params 6 | const data = await PicList.findAndCountAll({ 7 | limit: Number(limit), 8 | offset: Number((page - 1) * limit), 9 | attributes: ['id', 'file_path'] 10 | }) 11 | const list = data.rows 12 | const total = data.count 13 | ctx.body = { 14 | code: 200, 15 | msg: "success", 16 | content: { 17 | list: list, 18 | total: total 19 | } 20 | } 21 | } 22 | } 23 | 24 | module.exports = new PicListCtl() -------------------------------------------------------------------------------- /view-pic/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | <%= htmlWebpackPlugin.options.title %> 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /view-pic/server/model/picList.js: -------------------------------------------------------------------------------- 1 | const Sequelize = require("sequelize") 2 | const sequelize = require("../config/connect") 3 | 4 | const Model = Sequelize.Model 5 | class PicList extends Model { } 6 | 7 | PicList.init({ 8 | id: { 9 | type: Sequelize.STRING(40), // varchar(40) 10 | primaryKey: true, 11 | allowNull: false, // 不允许为null 12 | comment: "图片id", 13 | }, 14 | file_path: { 15 | type: Sequelize.STRING(255), 16 | allowNull: false, // 不允许为null 17 | comment: "图片访问路径" 18 | } 19 | }, { 20 | sequelize, 21 | freezeTableName: true, 22 | modelName: 'pic_list', // 数据库表名 23 | timestamps: false 24 | }) 25 | PicList.sync() 26 | module.exports = PicList -------------------------------------------------------------------------------- /scroll-demo/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /view-pic/src/custom/layer/index.js: -------------------------------------------------------------------------------- 1 | import Layer from './layer' 2 | 3 | const layer = {} 4 | 5 | layer.install = (vue) => { 6 | let layerComponent = null 7 | const layerInstance = vue.extend(Layer) 8 | const instance = () => { 9 | layerComponent = new layerInstance() 10 | const layerEl = layerComponent.$mount().$el 11 | document.getElementById("app").appendChild(layerEl) 12 | } 13 | vue.prototype.$layer = { 14 | show (option) { 15 | instance() 16 | Object.assign(layerComponent, option) 17 | }, 18 | hide () { 19 | if (layerComponent) { 20 | const layerEl = layerComponent.$mount().$el 21 | document.getElementById("app").removeChild(layerEl) 22 | } 23 | } 24 | } 25 | } 26 | 27 | export default layer -------------------------------------------------------------------------------- /web-cache/cache-node/app/public/cache/index.html: -------------------------------------------------------------------------------- 1 | cache-web
-------------------------------------------------------------------------------- /min-program/cloudfunctions/login/index.js: -------------------------------------------------------------------------------- 1 | // 云函数模板 2 | // 部署:在 cloud-functions/login 文件夹右击选择 “上传并部署” 3 | 4 | const cloud = require('wx-server-sdk') 5 | 6 | // 初始化 cloud 7 | cloud.init({ 8 | // API 调用都保持和云函数当前所在环境一致 9 | env: cloud.DYNAMIC_CURRENT_ENV 10 | }) 11 | 12 | /** 13 | * 这个示例将经自动鉴权过的小程序用户 openid 返回给小程序端 14 | * 15 | * event 参数包含小程序端调用传入的 data 16 | * 17 | */ 18 | exports.main = (event, context) => { 19 | console.log(event) 20 | console.log(context) 21 | 22 | // 可执行其他自定义逻辑 23 | // console.log 的内容可以在云开发云函数调用日志查看 24 | 25 | // 获取 WX Context (微信调用上下文),包括 OPENID、APPID、及 UNIONID(需满足 UNIONID 获取条件)等信息 26 | const wxContext = cloud.getWXContext() 27 | 28 | return { 29 | event, 30 | openid: wxContext.OPENID, 31 | appid: wxContext.APPID, 32 | unionid: wxContext.UNIONID, 33 | env: wxContext.ENV, 34 | } 35 | } 36 | 37 | -------------------------------------------------------------------------------- /web-cache/cache-web/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 11 | 12 | <%= htmlWebpackPlugin.options.title %> 13 | 14 | 15 | 16 | 20 |
21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /view-pic/server/config/connect.js: -------------------------------------------------------------------------------- 1 | const Sequelize = require("sequelize") 2 | const env = process.env.NODE_ENV // 'production' 3 | 4 | const common_config = { 5 | dialect: 'mysql', 6 | pool: { 7 | max: 5, 8 | min: 0, 9 | idle: 10000 10 | } 11 | } 12 | const dev_config = { 13 | database: "view_pic", 14 | username: "root", 15 | password: "root", 16 | host: "localhost", 17 | port: 3306, 18 | } 19 | const pro_config = {} 20 | 21 | let config = env === 'production' ? Object.assign({}, common_config, pro_config) : Object.assign({}, common_config, dev_config) 22 | 23 | console.log(config) 24 | const sequelize = new Sequelize( 25 | config.database, // database 26 | config.username, // username 27 | config.password, // password 28 | { 29 | host: config.host, 30 | dialect: config.dialect, 31 | pool: config.pool 32 | } 33 | ) 34 | 35 | module.exports = sequelize -------------------------------------------------------------------------------- /web-cache/cache-web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cache-web", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint" 9 | }, 10 | "dependencies": { 11 | "core-js": "^3.6.5", 12 | "vue": "^2.6.11" 13 | }, 14 | "devDependencies": { 15 | "@vue/cli-plugin-babel": "~4.3.0", 16 | "@vue/cli-plugin-eslint": "~4.3.0", 17 | "@vue/cli-service": "~4.3.0", 18 | "axios": "^0.19.2", 19 | "babel-eslint": "^10.1.0", 20 | "eslint": "^6.7.2", 21 | "eslint-plugin-vue": "^6.2.2", 22 | "vue-template-compiler": "^2.6.11" 23 | }, 24 | "eslintConfig": { 25 | "root": true, 26 | "env": { 27 | "node": true 28 | }, 29 | "extends": [ 30 | "plugin:vue/essential", 31 | "eslint:recommended" 32 | ], 33 | "parserOptions": { 34 | "parser": "babel-eslint" 35 | }, 36 | "rules": {} 37 | }, 38 | "browserslist": [ 39 | "> 1%", 40 | "last 2 versions", 41 | "not dead" 42 | ] 43 | } 44 | -------------------------------------------------------------------------------- /video-player/src/static/fonts/iconfont.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "1468000", 3 | "name": "video", 4 | "font_family": "iconfont", 5 | "css_prefix_text": "icon-", 6 | "description": "", 7 | "glyphs": [ 8 | { 9 | "icon_id": "807275", 10 | "name": "全屏", 11 | "font_class": "quanping", 12 | "unicode": "e660", 13 | "unicode_decimal": 58976 14 | }, 15 | { 16 | "icon_id": "1111441", 17 | "name": "播放", 18 | "font_class": "bofang", 19 | "unicode": "e63a", 20 | "unicode_decimal": 58938 21 | }, 22 | { 23 | "icon_id": "1458367", 24 | "name": "暂停", 25 | "font_class": "zanting", 26 | "unicode": "e60a", 27 | "unicode_decimal": 58890 28 | }, 29 | { 30 | "icon_id": "9459583", 31 | "name": "声音", 32 | "font_class": "shengyin", 33 | "unicode": "e617", 34 | "unicode_decimal": 58903 35 | }, 36 | { 37 | "icon_id": "11372697", 38 | "name": "声音关闭", 39 | "font_class": "shengyinguanbi", 40 | "unicode": "e8b8", 41 | "unicode_decimal": 59576 42 | } 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /scroll-demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "scroll-demo", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint" 9 | }, 10 | "dependencies": { 11 | "core-js": "^3.6.4", 12 | "vue": "^2.6.11" 13 | }, 14 | "devDependencies": { 15 | "@vue/cli-plugin-babel": "~4.2.0", 16 | "@vue/cli-plugin-eslint": "~4.2.0", 17 | "@vue/cli-service": "~4.2.0", 18 | "babel-eslint": "^10.0.3", 19 | "eslint": "^6.7.2", 20 | "eslint-plugin-vue": "^6.1.2", 21 | "iscroll": "^5.2.0", 22 | "vue-template-compiler": "^2.6.11" 23 | }, 24 | "eslintConfig": { 25 | "root": true, 26 | "env": { 27 | "node": true 28 | }, 29 | "extends": [ 30 | "plugin:vue/essential", 31 | "eslint:recommended" 32 | ], 33 | "parserOptions": { 34 | "parser": "babel-eslint" 35 | }, 36 | "rules": { 37 | "no-console": "off", 38 | "no-debugger": "off" 39 | } 40 | }, 41 | "browserslist": [ 42 | "> 1%", 43 | "last 2 versions" 44 | ] 45 | } -------------------------------------------------------------------------------- /video-player/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "video-player", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint" 9 | }, 10 | "dependencies": { 11 | "core-js": "^3.3.2", 12 | "vue": "^2.6.10" 13 | }, 14 | "devDependencies": { 15 | "@vue/cli-plugin-babel": "^4.0.0", 16 | "@vue/cli-plugin-eslint": "^4.0.0", 17 | "@vue/cli-service": "^4.0.0", 18 | "babel-eslint": "^10.0.3", 19 | "eslint": "^5.16.0", 20 | "eslint-plugin-vue": "^5.0.0", 21 | "vue-router": "^3.1.3", 22 | "vue-template-compiler": "^2.6.10" 23 | }, 24 | "eslintConfig": { 25 | "root": true, 26 | "env": { 27 | "node": true 28 | }, 29 | "extends": [ 30 | "plugin:vue/essential", 31 | "eslint:recommended" 32 | ], 33 | "rules": {}, 34 | "parserOptions": { 35 | "parser": "babel-eslint" 36 | } 37 | }, 38 | "postcss": { 39 | "plugins": { 40 | "autoprefixer": {} 41 | } 42 | }, 43 | "browserslist": [ 44 | "> 1%", 45 | "last 2 versions" 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /web-cache/cache-node/app/public/cache/static/css/app.e5f657b7.css: -------------------------------------------------------------------------------- 1 | #app[data-v-7e314f6e]{font-family:Avenir,Helvetica,Arial,sans-serif;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;text-align:center;color:#2c3e50;margin-top:60px}h3[data-v-7e314f6e]{margin-top:50px;font-size:30px}p[data-v-7e314f6e]{margin-top:50px}span[data-v-7e314f6e]{font-size:30px;color:#368fff}blockquote,body,button,caption,dd,div,dl,dt,fieldset,figure,form,h1,h2,h3,h4,h5,h6,hr,html,input,legend,li,menu,ol,p,pre,table,td,textarea,th,ul{margin:0;padding:0}address,article,aside,details,figcaption,figure,footer,header,hgroup,menu,nav,section{display:block}table{border-collapse:collapse;border-spacing:0}caption,th{text-align:left;font-weight:400}abbr,body,fieldset,html,iframe,img{border:0}address,cite,dfn,em,i,var{font-style:normal}[hidefocus],summary{outline:0}li{list-style:none}h1,h2,h3,h4,h5,h6,small{font-size:100%}sub,sup{font-size:83%}code,kbd,pre,samp{font-family:inherit}q:after,q:before{content:none}textarea{overflow:auto;resize:none}label,summary{cursor:default}a,button{cursor:pointer}b,em,h1,h2,h3,h4,h5,h6,strong{font-weight:700}a,a:hover,del,ins,s,u{text-decoration:none}body,button,input,keygen,legend,select,textarea{font:12px/1.14 arial,\5b8b\4f53;color:#333;outline:0}body{background:#fff}a,a:hover{color:#333} -------------------------------------------------------------------------------- /min-program/project.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "miniprogramRoot": "miniprogram/", 3 | "cloudfunctionRoot": "cloudfunctions/", 4 | "setting": { 5 | "urlCheck": true, 6 | "es6": true, 7 | "enhance": true, 8 | "postcss": true, 9 | "minified": true, 10 | "newFeature": true, 11 | "coverView": true, 12 | "autoAudits": false, 13 | "showShadowRootInWxmlPanel": true, 14 | "scopeDataCheck": false, 15 | "checkInvalidKey": true, 16 | "checkSiteMap": true, 17 | "uploadWithSourceMap": true, 18 | "babelSetting": { 19 | "ignore": [], 20 | "disablePlugins": [], 21 | "outputPath": "" 22 | }, 23 | "nodeModules": true 24 | }, 25 | "appid": "wxa6598360ddb43ca9", 26 | "projectname": "difficult", 27 | "libVersion": "2.8.1", 28 | "simulatorType": "wechat", 29 | "simulatorPluginLibVersion": {}, 30 | "cloudfunctionTemplateRoot": "cloudfunctionTemplate", 31 | "condition": { 32 | "search": { 33 | "current": -1, 34 | "list": [] 35 | }, 36 | "conversation": { 37 | "current": -1, 38 | "list": [] 39 | }, 40 | "plugin": { 41 | "current": -1, 42 | "list": [] 43 | }, 44 | "game": { 45 | "list": [] 46 | }, 47 | "miniprogram": { 48 | "current": 0, 49 | "list": [ 50 | { 51 | "id": -1, 52 | "name": "db guide", 53 | "pathName": "pages/databaseGuide/databaseGuide" 54 | } 55 | ] 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /audio/js/main.js: -------------------------------------------------------------------------------- 1 | const record = document.getElementById("record") 2 | const stop = document.getElementById("stop") 3 | const play = document.getElementById("play") 4 | const audio = document.getElementById("audio") 5 | const downWav = document.getElementById("downWav") 6 | const downPcm = document.getElementById("downPcm") 7 | const canvas = document.getElementById("canvas") 8 | const ctx = canvas.getContext("2d") 9 | canvas.width = 600 10 | canvas.height = 200 11 | let recorder = null 12 | 13 | // 录制 14 | record.addEventListener("click", () => { 15 | if(recorder !== null) recorder.close() 16 | Recorder.get((rec) => { 17 | recorder = rec 18 | recorder.start() 19 | }) 20 | }) 21 | 22 | // 停止 23 | stop.addEventListener("click", () => { 24 | if(recorder === null) return alert("请先录音") 25 | recorder.stop() 26 | }) 27 | 28 | // 播放 29 | play.addEventListener("click", () => { 30 | if(recorder === null) return alert("请先录音") 31 | recorder.play(audio, ctx) 32 | recorder.draw(ctx) 33 | }) 34 | 35 | // 下载 wav 36 | downWav.addEventListener("click", () => { 37 | if(recorder === null) return alert("请先录音") 38 | const src = recorder.wavSrc() 39 | downWav.setAttribute("href", src) 40 | }) 41 | 42 | // 下载 pcm 43 | downPcm.addEventListener("click", () => { 44 | if(recorder === null) return alert("请先录音") 45 | const src = recorder.pcmSrc() 46 | downPcm.setAttribute("href", src) 47 | }) -------------------------------------------------------------------------------- /min-program/miniprogram/pages/index/index.wxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 排序连线 4 | 5 | 6 | 7 | 8 | {{index + 1}} : {{item.text}} 9 | 10 | 11 | 12 | 13 | 14 | {{index + 1}} : {{item.text}} 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 点击左边 "黑点" 拖动至右边 "黑点区域" 进行连线 23 | -------------------------------------------------------------------------------- /web-cache/cache-web/src/App.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 47 | 48 | 69 | -------------------------------------------------------------------------------- /view-pic/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "view-pic", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "server": "nodemon server/index.js", 8 | "build": "vue-cli-service build", 9 | "lint": "vue-cli-service lint" 10 | }, 11 | "dependencies": { 12 | "core-js": "^3.6.4", 13 | "vue": "^2.6.11" 14 | }, 15 | "devDependencies": { 16 | "@vue/cli-plugin-babel": "~4.2.0", 17 | "@vue/cli-plugin-eslint": "~4.2.0", 18 | "@vue/cli-service": "~4.2.0", 19 | "axios": "^0.19.2", 20 | "babel-eslint": "^10.0.3", 21 | "eslint": "^6.7.2", 22 | "eslint-plugin-vue": "^6.1.2", 23 | "koa": "^2.11.0", 24 | "koa-router": "^8.0.8", 25 | "mso-flex": "^1.0.2", 26 | "mysql2": "^2.1.0", 27 | "nodemon": "^2.0.2", 28 | "sequelize": "^5.21.5", 29 | "swiper": "^5.3.6", 30 | "vue-awesome-swiper": "^4.0.4", 31 | "vue-router": "^3.1.6", 32 | "vue-template-compiler": "^2.6.11", 33 | "vue-touch": "^2.0.0-beta.4" 34 | }, 35 | "eslintConfig": { 36 | "root": true, 37 | "env": { 38 | "node": true 39 | }, 40 | "extends": [ 41 | "plugin:vue/essential", 42 | "eslint:recommended" 43 | ], 44 | "parserOptions": { 45 | "parser": "babel-eslint" 46 | }, 47 | "rules": { 48 | "no-console": "off", 49 | "no-debugger": "off" 50 | } 51 | }, 52 | "browserslist": [ 53 | "> 1%", 54 | "last 2 versions" 55 | ] 56 | } 57 | -------------------------------------------------------------------------------- /min-program/miniprogram/pages/index/index.wxss: -------------------------------------------------------------------------------- 1 | .title { 2 | color: #625963; 3 | line-height: 100rpx; 4 | } 5 | 6 | .content { 7 | width: 100%; 8 | display: flex; 9 | flex-direction: row; 10 | justify-content: space-between; 11 | margin-top: 80rpx; 12 | position: relative; 13 | box-sizing: border-box; 14 | border: 1rpx solid #b57fba; 15 | } 16 | 17 | .custom-line { 18 | height: 5rpx; 19 | position: absolute; 20 | transform-origin: 0 0; 21 | } 22 | 23 | .left, 24 | .right { 25 | width: 30%; 26 | display: flex; 27 | flex-direction: column; 28 | } 29 | 30 | .left-item, 31 | .right-item { 32 | display: flex; 33 | font-size: 30rpx; 34 | line-height: 100rpx; 35 | position: relative; 36 | } 37 | 38 | .left-item-icon, 39 | .right-item-icon { 40 | position: absolute; 41 | width: 30rpx; 42 | height: 30rpx; 43 | border-radius: 50%; 44 | background-color: #625963; 45 | top: 35rpx; 46 | } 47 | 48 | .left-item-icon { 49 | right: -30rpx; 50 | } 51 | 52 | .right-item-icon { 53 | left: -30rpx; 54 | } 55 | 56 | .left-item, 57 | .left-item-text { 58 | justify-content: flex-end; 59 | } 60 | 61 | .left-item { 62 | padding-right: 5%; 63 | } 64 | 65 | .right-item, 66 | .right-item-text { 67 | justify-content: flex-start; 68 | } 69 | 70 | .right-item { 71 | padding-left: 5%; 72 | } 73 | 74 | .btn-container { 75 | margin: 80rpx 0; 76 | width: 100%; 77 | display: flex; 78 | flex-direction: row; 79 | align-items: center; 80 | justify-content: space-evenly; 81 | } 82 | 83 | button { 84 | width: 200rpx; 85 | height: 80rpx; 86 | font-size: 36rpx; 87 | line-height: 80rpx; 88 | } 89 | 90 | .text-container { 91 | font-size: 30rpx; 92 | color: #b57fba; 93 | font-weight: bold; 94 | } -------------------------------------------------------------------------------- /scroll-demo/src/directive/vIscroll.js: -------------------------------------------------------------------------------- 1 | const IScroll = require('iscroll') 2 | const VIScroll = { 3 | install: function (Vue, options) { 4 | Vue.directive('iscroll', { 5 | inserted: function (el, binding, vnode) { 6 | let callBack 7 | let iscrollOptions = options 8 | const option = binding.value && binding.value.option 9 | const func = binding.value && binding.value.instance 10 | // 判断输入参数 11 | const optionType = option ? [].toString.call(option) : undefined 12 | const funcType = func ? [].toString.call(func) : undefined 13 | // 兼容 google 浏览器拖动 14 | el.addEventListener('touchmove', function (e) { 15 | e.preventDefault() 16 | }) 17 | // 将参数配置到new IScroll(el, iscrollOptions)中 18 | if (optionType === '[object Object]') { 19 | iscrollOptions = option 20 | } 21 | if (funcType === '[object Function]') { 22 | callBack = func 23 | } 24 | // 使用vnode绑定iscroll是为了让iscroll对象能够夸状态传递,避免iscroll重复建立 25 | // 这里面跟官方网站 const myScroll = new IScroll('#wrapper',option) 初始化一样 26 | vnode.scroll = new IScroll(el, iscrollOptions) 27 | // 如果指令传递函数进来,把iscroll实例传递出去 28 | if (callBack) callBack(vnode.scroll) 29 | }, 30 | componentUpdated: function (el, binding, vnode, oldVnode) { 31 | // 将scroll绑定到新的vnode上,避免多次绑定 32 | vnode.scroll = oldVnode.scroll 33 | // 使用 settimeout 让refresh跳到事件流结尾,保证refresh时数据已经更新完毕 34 | setTimeout(() => { 35 | vnode.scroll.refresh() 36 | }, 0) 37 | }, 38 | unbind: function (el, binding, vnode, oldVnode) { 39 | // 解除绑定时要把iscroll销毁 40 | vnode.scroll = oldVnode.scroll 41 | vnode.scroll.destroy() 42 | vnode.scroll = null 43 | } 44 | }) 45 | } 46 | } 47 | module.exports = VIScroll -------------------------------------------------------------------------------- /scroll-demo/src/assets/reset.css: -------------------------------------------------------------------------------- 1 | /* http://meyerweb.com/eric/tools/css/reset/ 2 | v2.0 | 20110126 3 | License: none (public domain) 4 | */ 5 | 6 | html, 7 | body, 8 | div, 9 | span, 10 | applet, 11 | object, 12 | iframe, 13 | h1, 14 | h2, 15 | h3, 16 | h4, 17 | h5, 18 | h6, 19 | p, 20 | blockquote, 21 | pre, 22 | a, 23 | abbr, 24 | acronym, 25 | address, 26 | big, 27 | cite, 28 | code, 29 | del, 30 | dfn, 31 | em, 32 | img, 33 | ins, 34 | kbd, 35 | q, 36 | s, 37 | samp, 38 | small, 39 | strike, 40 | strong, 41 | sub, 42 | sup, 43 | tt, 44 | var, 45 | b, 46 | u, 47 | i, 48 | center, 49 | dl, 50 | dt, 51 | dd, 52 | ol, 53 | ul, 54 | li, 55 | fieldset, 56 | form, 57 | label, 58 | legend, 59 | table, 60 | caption, 61 | tbody, 62 | tfoot, 63 | thead, 64 | tr, 65 | th, 66 | td, 67 | article, 68 | aside, 69 | canvas, 70 | details, 71 | embed, 72 | figure, 73 | figcaption, 74 | footer, 75 | header, 76 | hgroup, 77 | menu, 78 | nav, 79 | output, 80 | ruby, 81 | section, 82 | summary, 83 | time, 84 | mark, 85 | audio, 86 | video { 87 | margin: 0; 88 | padding: 0; 89 | border: 0; 90 | font-size: 100%; 91 | font: inherit; 92 | vertical-align: baseline; 93 | } 94 | 95 | /* HTML5 display-role reset for older browsers */ 96 | article, 97 | aside, 98 | details, 99 | figcaption, 100 | figure, 101 | footer, 102 | header, 103 | hgroup, 104 | menu, 105 | nav, 106 | section { 107 | display: block; 108 | } 109 | 110 | body { 111 | line-height: 1; 112 | } 113 | 114 | ol, 115 | ul { 116 | list-style: none; 117 | } 118 | 119 | blockquote, 120 | q { 121 | quotes: none; 122 | } 123 | 124 | blockquote:before, 125 | blockquote:after, 126 | q:before, 127 | q:after { 128 | content: ''; 129 | content: none; 130 | } 131 | 132 | table { 133 | border-collapse: collapse; 134 | border-spacing: 0; 135 | } -------------------------------------------------------------------------------- /audio/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 音频录制和展示 8 | 44 | 45 | 46 |
音频录制和展示
47 |
50 |
53 | 54 | 55 | 56 | 下载pcm 57 | 下载wav 58 |
59 |
62 | 63 | 64 | 65 | 66 |
67 |
68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /min-program/miniprogram/components/line/line.js: -------------------------------------------------------------------------------- 1 | Component({ 2 | properties: { 3 | // 这里定义了line组件属性,属性值可以在组件使用时指定 4 | startX: { 5 | type: Number, 6 | value: 0, 7 | }, 8 | startY: { 9 | type: Number, 10 | value: 0, 11 | }, 12 | endX: { 13 | type: Number, 14 | value: 0, 15 | }, 16 | endY: { 17 | type: Number, 18 | value: 0, 19 | } 20 | }, 21 | // 实时监听从主页传来的坐标,相当于vue中watch 22 | observers: { 23 | 'startX, startY, endX, endY': function (startX, startY, endX, endY) { 24 | // 计算线条角度和长度 25 | let { angle, line } = this.getAngle(startX, startY, endX, endY) 26 | this.setData({ 27 | angle: angle, 28 | line: line 29 | }) 30 | // 这里必须在组件都加载成功后调用此方法,不然主页绑定不了子组件的事件 31 | if (this.data.ready) this.initStartPostion() 32 | } 33 | }, 34 | data: { 35 | angle: 0, 36 | line: 0, 37 | ready: false 38 | }, 39 | lifetimes: { 40 | // 这是小程序的生命周期 41 | ready: function () { // 初始节点完成初始调用 42 | this.setData({ 43 | ready: true 44 | }) 45 | // 这里是小程序的主动发射事件,相当于vue emit发射事件,初始化调用一次,然后在observers变化时调用 46 | this.initStartPostion() 47 | } 48 | }, 49 | methods: { 50 | // 算角度 51 | getAngle: function (startX, startY, endX, endY) { 52 | let angle = 0 53 | let line = 0 54 | angle = this.getTanDeg(startX, endX, startY, endY) 55 | console.log("angle:" + angle) 56 | line = Math.round(Math.sqrt(Math.pow((endX - startX), 2) + Math.pow((endY - startY), 2))) 57 | return { angle, line } 58 | }, 59 | // 三角函数算角度 60 | getTanDeg: function (startX, endX, startY, endY) { 61 | let disY = endY - startY 62 | let disX = endX - startX 63 | let result = Math.atan2(disY, disX) * (180 / Math.PI) 64 | return Math.round(result) 65 | }, 66 | // 向主页发射方法,用于主页实时渲染 67 | initStartPostion: function () { 68 | this.triggerEvent("calcPostionLine", { angle: this.data.angle, line: this.data.line }) 69 | } 70 | }, 71 | }) -------------------------------------------------------------------------------- /web-cache/cache-node/app/api.js: -------------------------------------------------------------------------------- 1 | 2 | const Router = require("koa-router") 3 | const router = new Router({ prefix: "/cache" }) 4 | let etag = 1 5 | let day = "2020-05-08 00:00:00:000" 6 | const returnResult = () => { 7 | return new Promise((resolve, reject) => { 8 | const body = { 9 | code: 200, 10 | msg: "success", 11 | content: "我是服务端数据sss" 12 | } 13 | return resolve(body) 14 | }).catch(error => { 15 | console.log(error) 16 | }) 17 | } 18 | 19 | const setCache = (ctx) => { 20 | // 强制缓存 21 | // Cache-Control 22 | // ctx.response.set("Cache-Control", "max-age=100;public") 23 | // expires 24 | // let now = new Date().getTime() 25 | // now += 3600 * 1000 26 | // ctx.response.set("Expires", new Date(now).toGMTString()) 27 | // 协商缓存 28 | // etag / if-none-match 29 | // ctx.response.set("Etag", etag) 30 | // last-modified / if-modified-since 31 | 32 | ctx.response.set('Last-Modified', new Date(day).toGMTString()) 33 | } 34 | 35 | const results = async (ctx) => { 36 | const result = await returnResult() 37 | console.log("我是服务请求") 38 | ctx.body = result 39 | } 40 | 41 | const resultc = (ctx) => { 42 | ctx.status = 304 43 | console.log("我是协商缓存") 44 | return 45 | } 46 | class CacheCtl { 47 | async getCacheData (ctx) { 48 | // const match = ctx.request.get("If-None-Match") 49 | setCache(ctx) 50 | results(ctx) 51 | // const since = ctx.request.get('If-Modify-Since') 52 | // if (match == etag) { 53 | // ctx.status = 304 54 | // console.log("我是协商缓存") 55 | // return 56 | // } else { 57 | // const result = await returnResult() 58 | // console.log("我是服务请求") 59 | // ctx.body = result 60 | // } 61 | // console.log(since) 62 | // if (since) { 63 | // let sincetime = new Date(since).getTime() 64 | // if (sincetime < day) { 65 | // // 设置缓存方法 66 | // results(ctx) 67 | // } else { 68 | // resultc(ctx) 69 | // } 70 | // } else { 71 | // results(ctx) 72 | // } 73 | } 74 | } 75 | const cacheInstance = new CacheCtl() 76 | 77 | router.get("/cache", cacheInstance.getCacheData) 78 | 79 | module.exports = router 80 | -------------------------------------------------------------------------------- /web-cache/cache-web/src/assets/reset.css: -------------------------------------------------------------------------------- 1 | /* reset */ 2 | html, 3 | body, 4 | h1, 5 | h2, 6 | h3, 7 | h4, 8 | h5, 9 | h6, 10 | div, 11 | dl, 12 | dt, 13 | dd, 14 | ul, 15 | ol, 16 | li, 17 | p, 18 | blockquote, 19 | pre, 20 | hr, 21 | figure, 22 | table, 23 | caption, 24 | th, 25 | td, 26 | form, 27 | fieldset, 28 | legend, 29 | input, 30 | button, 31 | textarea, 32 | menu { 33 | margin: 0; 34 | padding: 0; 35 | } 36 | 37 | header, 38 | footer, 39 | section, 40 | article, 41 | aside, 42 | nav, 43 | hgroup, 44 | address, 45 | figure, 46 | figcaption, 47 | menu, 48 | details { 49 | display: block; 50 | } 51 | 52 | table { 53 | border-collapse: collapse; 54 | border-spacing: 0; 55 | } 56 | 57 | caption, 58 | th { 59 | text-align: left; 60 | font-weight: normal; 61 | } 62 | 63 | html, 64 | body, 65 | fieldset, 66 | img, 67 | iframe, 68 | abbr { 69 | border: 0; 70 | } 71 | 72 | i, 73 | cite, 74 | em, 75 | var, 76 | address, 77 | dfn { 78 | font-style: normal; 79 | } 80 | 81 | [hidefocus], 82 | summary { 83 | outline: 0; 84 | } 85 | 86 | li { 87 | list-style: none; 88 | } 89 | 90 | h1, 91 | h2, 92 | h3, 93 | h4, 94 | h5, 95 | h6, 96 | small { 97 | font-size: 100%; 98 | } 99 | 100 | sup, 101 | sub { 102 | font-size: 83%; 103 | } 104 | 105 | pre, 106 | code, 107 | kbd, 108 | samp { 109 | font-family: inherit; 110 | } 111 | 112 | q:before, 113 | q:after { 114 | content: none; 115 | } 116 | 117 | textarea { 118 | overflow: auto; 119 | resize: none; 120 | } 121 | 122 | label, 123 | summary { 124 | cursor: default; 125 | } 126 | 127 | a, 128 | button { 129 | cursor: pointer; 130 | } 131 | 132 | h1, 133 | h2, 134 | h3, 135 | h4, 136 | h5, 137 | h6, 138 | em, 139 | strong, 140 | b { 141 | font-weight: bold; 142 | } 143 | 144 | del, 145 | ins, 146 | u, 147 | s, 148 | a, 149 | a:hover { 150 | text-decoration: none; 151 | } 152 | 153 | body, 154 | textarea, 155 | input, 156 | button, 157 | select, 158 | keygen, 159 | legend { 160 | font: 12px/1.14 arial, \5b8b\4f53; 161 | color: #333; 162 | outline: 0; 163 | } 164 | 165 | body { 166 | background: #fff; 167 | } 168 | 169 | a, 170 | a:hover { 171 | color: #333; 172 | } -------------------------------------------------------------------------------- /min-program/cloudfunctions/openapi/index.js: -------------------------------------------------------------------------------- 1 | // 云函数入口文件 2 | const cloud = require('wx-server-sdk') 3 | 4 | cloud.init() 5 | 6 | // 云函数入口函数 7 | exports.main = async (event, context) => { 8 | console.log(event) 9 | switch (event.action) { 10 | case 'requestSubscribeMessage': { 11 | return requestSubscribeMessage(event) 12 | } 13 | case 'sendSubscribeMessage': { 14 | return sendSubscribeMessage(event) 15 | } 16 | case 'getWXACode': { 17 | return getWXACode(event) 18 | } 19 | case 'getOpenData': { 20 | return getOpenData(event) 21 | } 22 | default: { 23 | return 24 | } 25 | } 26 | } 27 | 28 | async function requestSubscribeMessage(event) { 29 | // 此处为模板 ID,开发者需要到小程序管理后台 - 订阅消息 - 公共模板库中添加模板, 30 | // 然后在我的模板中找到对应模板的 ID,填入此处 31 | return '请到管理后台申请模板 ID 然后在此替换' // 如 'N_J6F05_bjhqd6zh2h1LHJ9TAv9IpkCiAJEpSw0PrmQ' 32 | } 33 | 34 | async function sendSubscribeMessage(event) { 35 | const { OPENID } = cloud.getWXContext() 36 | 37 | const { templateId } = event 38 | 39 | const sendResult = await cloud.openapi.subscribeMessage.send({ 40 | touser: OPENID, 41 | templateId, 42 | miniprogram_state: 'developer', 43 | page: 'pages/openapi/openapi', 44 | // 此处字段应修改为所申请模板所要求的字段 45 | data: { 46 | thing1: { 47 | value: '咖啡', 48 | }, 49 | time3: { 50 | value: '2020-01-01 00:00', 51 | }, 52 | } 53 | }) 54 | 55 | return sendResult 56 | } 57 | 58 | async function getWXACode(event) { 59 | 60 | // 此处将获取永久有效的小程序码,并将其保存在云文件存储中,最后返回云文件 ID 给前端使用 61 | 62 | const wxacodeResult = await cloud.openapi.wxacode.get({ 63 | path: 'pages/openapi/openapi', 64 | }) 65 | 66 | const fileExtensionMatches = wxacodeResult.contentType.match(/\/([^\/]+)/) 67 | const fileExtension = (fileExtensionMatches && fileExtensionMatches[1]) || 'jpg' 68 | 69 | const uploadResult = await cloud.uploadFile({ 70 | // 云文件路径,此处为演示采用一个固定名称 71 | cloudPath: `wxacode_default_openapi_page.${fileExtension}`, 72 | // 要上传的文件内容可直接传入图片 Buffer 73 | fileContent: wxacodeResult.buffer, 74 | }) 75 | 76 | if (!uploadResult.fileID) { 77 | throw new Error(`upload failed with empty fileID and storage server status code ${uploadResult.statusCode}`) 78 | } 79 | 80 | return uploadResult.fileID 81 | } 82 | 83 | async function getOpenData(event) { 84 | return cloud.getOpenData({ 85 | list: event.openData.list, 86 | }) 87 | } 88 | -------------------------------------------------------------------------------- /view-pic/src/page/view.vue: -------------------------------------------------------------------------------- 1 | 22 | 56 | 100 | -------------------------------------------------------------------------------- /view-pic/src/static/reset.css: -------------------------------------------------------------------------------- 1 | /* reset */ 2 | html, 3 | body, 4 | h1, 5 | h2, 6 | h3, 7 | h4, 8 | h5, 9 | h6, 10 | div, 11 | dl, 12 | dt, 13 | dd, 14 | ul, 15 | ol, 16 | li, 17 | p, 18 | blockquote, 19 | pre, 20 | hr, 21 | figure, 22 | table, 23 | caption, 24 | th, 25 | td, 26 | form, 27 | fieldset, 28 | legend, 29 | input, 30 | button, 31 | textarea, 32 | menu { 33 | margin: 0; 34 | padding: 0; 35 | } 36 | 37 | html, 38 | body { 39 | height: 100%; 40 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 41 | -webkit-transform-style: preserve-3d; 42 | /*设置内嵌的元素在 3D 空间如何呈现:保留 3D*/ 43 | -webkit-backface-visibility: hidden; 44 | /*(设置进行转换的元素的背面在面对用户时是否可见:隐藏)*/ 45 | -webkit-touch-callout: none; 46 | background: rgba(244, 247, 249, 1); 47 | position: relative; 48 | } 49 | 50 | html, 51 | body, 52 | form, 53 | fieldset, 54 | p, 55 | div, 56 | h1, 57 | h2, 58 | h3, 59 | h4, 60 | h5, 61 | h6 { 62 | -webkit-text-size-adjust: none; 63 | } 64 | 65 | header, 66 | footer, 67 | section, 68 | article, 69 | aside, 70 | nav, 71 | hgroup, 72 | address, 73 | figure, 74 | figcaption, 75 | menu, 76 | details { 77 | display: block; 78 | } 79 | 80 | table { 81 | border-collapse: collapse; 82 | border-spacing: 0; 83 | } 84 | 85 | caption, 86 | th { 87 | text-align: left; 88 | font-weight: normal; 89 | } 90 | 91 | html, 92 | body, 93 | fieldset, 94 | img, 95 | iframe, 96 | abbr { 97 | border: 0; 98 | } 99 | 100 | i, 101 | cite, 102 | em, 103 | var, 104 | address, 105 | dfn { 106 | font-style: normal; 107 | } 108 | 109 | [hidefocus], 110 | summary { 111 | outline: 0; 112 | } 113 | 114 | li { 115 | list-style: none; 116 | } 117 | 118 | h1, 119 | h2, 120 | h3, 121 | h4, 122 | h5, 123 | h6, 124 | small { 125 | font-size: 100%; 126 | } 127 | 128 | sup, 129 | sub { 130 | font-size: 83%; 131 | } 132 | 133 | pre, 134 | code, 135 | kbd, 136 | samp { 137 | font-family: inherit; 138 | } 139 | 140 | q:before, 141 | q:after { 142 | content: none; 143 | } 144 | 145 | textarea { 146 | overflow: auto; 147 | resize: none; 148 | } 149 | 150 | label, 151 | summary { 152 | cursor: default; 153 | } 154 | 155 | a, 156 | button { 157 | cursor: pointer; 158 | } 159 | 160 | h1, 161 | h2, 162 | h3, 163 | h4, 164 | h5, 165 | h6, 166 | em, 167 | strong, 168 | b { 169 | font-weight: bold; 170 | } 171 | 172 | del, 173 | ins, 174 | u, 175 | s, 176 | a, 177 | a:hover { 178 | text-decoration: none; 179 | } 180 | 181 | body, 182 | textarea, 183 | input, 184 | button, 185 | select, 186 | keygen, 187 | legend { 188 | font: 12px/1.14 arial, \5b8b\4f53; 189 | color: #333; 190 | outline: 0; 191 | } 192 | 193 | a, 194 | a:hover { 195 | color: #333; 196 | } -------------------------------------------------------------------------------- /video-player/src/static/fonts/iconfont.css: -------------------------------------------------------------------------------- 1 | @font-face {font-family: "iconfont"; 2 | src: url('iconfont.eot?t=1571664681339'); /* IE9 */ 3 | src: url('iconfont.eot?t=1571664681339#iefix') format('embedded-opentype'), /* IE6-IE8 */ 4 | url('data:application/x-font-woff2;charset=utf-8;base64,d09GMgABAAAAAAVEAAsAAAAAClwAAAT2AAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHEIGVgCDSAqHRIZEATYCJAMYCw4ABCAFhG0HYRsDCVGUTk6a7KcmTza0+tw5G4aBgQNwxKMAeAAgARMoAEA8PO3369yZeQ+VBkk0SVdPhEgySYROSGKlUzKhyO7mzf9q2vZbl4+88emS2lW1qXuX+7QyE0U3EqGxMQK6BuEYfj6vfH/G4WB3M1MJt4WS1Lit+NM6aR2W4KDUjWzYX3N0R4eqmbYtOM9iIYnhf+acFsBQ0gFSoDG2LSpYuRUrVkz7AM9wYQLvJ9BqmKCxun96AX2FoA1sOuHMhL7LpsSwQ7OxDjm3ZDQWcGpOs9ML8KD/fPyD2eiDpMogPOjweM+Bze/hdb5pYBiSMVyeB7eLyJgBCnEf6rvTMnMzWlo/v6FtG2jXLCnfw+YtZ1w3/uNSCaHQLv7LIyuSUBE1oK6dqbv5iu+hbJE/+jlaFNQvo0VCvYEigt96RAXqEvcZADEH4oVAm6Er4x9pknswvfQOiqJp+ROSCMShRJTEVCklDUNaQhDyJgC8XjG4UkLXpaUkqWwGwOtkHRWRmobN2BO2ICRtVLTEYkJVsQWarZ7tN8gqlMZiQkzOjkQkDuajUY4ixuMWaUwYNrDvWZHIIEFXwYY1TjSkyfK6BJu2XtmI7NIG0oEZY4az6xsG5ccP/JCMxYpOK5UTsrPlYyHAx+uTvFbti4ZLW4YIOkGI5EZOHDJYX2daiYejONJINRMje7ijcSTFuohhxOuE5Ohmhbt7e2nmZHHj/rAGDnNTPTlJIDacqIKWnmzIqwShJoDWq45E3jmW1/CmmL8jW5W3SJwo6CsPJvKisX95gnuq5/Cp/+EPdJwCKk6z4f+U8uzwEe9maeVFdortodOLdrVSCgpSaqXV3iUhDVnpWim103bRzJC2nQ/W2h88sK8VLQ8eKCp7uzpT9leOPu9BnvOjjbtk0bC9CVOTrEHWrNaSqkG1NBhNV83bNjxw3PH7t+OYiFTNcVba7eTdjmvssbhjNSNGpmk1f01olfOePhQ5qEIYPH9CgM3b6w7RvjEBWOgfU3fB3rm/E0z4raX/3bcUem17tn70uQ56g/r90+r2+6Z3mDiH9ebvaDWkQ8e2w56WTDBGfHTNSu1UobQrrlHkQtyMTd3nZWT1WRwH6B6f1mXWmTK2mfPxCPbi9TVGfrCYd4z1oXQ5zzt5geedl5jszbtR6eI9vDAr7A/PEnjBYoYMDX/NHMOeqzyefDSEj0EZAID/J/Npe78L71EWhryKXA8xbwDKY/oH5I/0fu7uZ3P/9FqfdfvvIY/npa/vSGOONmw/lSM7AfVSy89Ue/WmNNVXKdjr8lbJ0lSGkq9G7UAPMwmtYogGhXr9lmUYSN1IaDZgQ9JkCLJmI8RCnYFKm3lNzdQFaDXt4OI2fVItUbzAlEeE0O0Zkk6fkHV7JRbqD1QG/alrpEcLWh1F8JptxoIP+6Zo5ugQqyLcjkIqOfOh0XyGrPDN1KnUuVeYZjxJVEnxFg8wxLSME7KSaXlOCU2jgOzDw9D3IxKn0QLtXHLzPF6WZRr2QZIdBTC0IoVMOeQgLBWCs0VCVF2cG/q+/wxiCj5TGjP1I34FpTJ864RKoqQgHujCVFPb0pkpMZqcMhRBb42RALFPHuSLuQgRh++1gGw5iZsjE1sm0zyaVifVbwiW8gZoJTyrDhuyIwdyIhdyQ/NlYYYxD1l3VERNzdMrMaeoLGT9XBfVZMXDqaFhiaIWBwAAAA==') format('woff2'), 5 | url('iconfont.woff?t=1571664681339') format('woff'), 6 | url('iconfont.ttf?t=1571664681339') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+ */ 7 | url('iconfont.svg?t=1571664681339#iconfont') format('svg'); /* iOS 4.1- */ 8 | } 9 | 10 | .iconfont { 11 | font-family: "iconfont" !important; 12 | font-size: 16px; 13 | font-style: normal; 14 | -webkit-font-smoothing: antialiased; 15 | -moz-osx-font-smoothing: grayscale; 16 | } 17 | 18 | .icon-quanping:before { 19 | content: "\e660"; 20 | } 21 | 22 | .icon-bofang:before { 23 | content: "\e63a"; 24 | } 25 | 26 | .icon-zanting:before { 27 | content: "\e60a"; 28 | } 29 | 30 | .icon-shengyin:before { 31 | content: "\e617"; 32 | } 33 | 34 | .icon-shengyinguanbi:before { 35 | content: "\e8b8"; 36 | } 37 | 38 | -------------------------------------------------------------------------------- /min-program/miniprogram/style/guide.wxss: -------------------------------------------------------------------------------- 1 | page { 2 | background: #f6f6f6; 3 | display: flex; 4 | flex-direction: column; 5 | justify-content: flex-start; 6 | } 7 | 8 | .list { 9 | margin-top: 40rpx; 10 | height: auto; 11 | width: 100%; 12 | background: #fff; 13 | padding: 0 40rpx; 14 | border: 1px solid rgba(0, 0, 0, 0.1); 15 | border-left: none; 16 | border-right: none; 17 | transition: all 300ms ease; 18 | display: flex; 19 | flex-direction: column; 20 | align-items: stretch; 21 | box-sizing: border-box; 22 | } 23 | 24 | .list-item { 25 | width: 100%; 26 | padding: 0; 27 | line-height: 104rpx; 28 | font-size: 34rpx; 29 | color: #007aff; 30 | border-top: 1px solid rgba(0, 0, 0, 0.1); 31 | display: flex; 32 | flex-direction: row; 33 | align-content: center; 34 | justify-content: space-between; 35 | box-sizing: border-box; 36 | } 37 | 38 | .list-item:first-child { 39 | border-top: none; 40 | } 41 | 42 | .list-item image { 43 | max-width: 100%; 44 | max-height: 20vh; 45 | margin: 20rpx 0; 46 | } 47 | 48 | .request-text { 49 | color: #222; 50 | padding: 20rpx 0; 51 | font-size: 24rpx; 52 | line-height: 36rpx; 53 | word-break: break-all; 54 | } 55 | 56 | .guide { 57 | width: 100%; 58 | padding: 40rpx; 59 | box-sizing: border-box; 60 | display: flex; 61 | flex-direction: column; 62 | } 63 | 64 | .guide .headline { 65 | font-size: 34rpx; 66 | font-weight: bold; 67 | color: #555; 68 | line-height: 40rpx; 69 | } 70 | 71 | .guide .p { 72 | margin-top: 20rpx; 73 | font-size: 28rpx; 74 | line-height: 36rpx; 75 | color: #666; 76 | } 77 | 78 | .guide .code { 79 | margin-top: 20rpx; 80 | font-size: 28rpx; 81 | line-height: 36rpx; 82 | color: #666; 83 | background: white; 84 | white-space: pre; 85 | } 86 | 87 | .guide .code-dark { 88 | margin-top: 20rpx; 89 | background: rgba(0, 0, 0, 0.8); 90 | padding: 20rpx; 91 | font-size: 28rpx; 92 | line-height: 36rpx; 93 | border-radius: 6rpx; 94 | color: #fff; 95 | white-space: pre 96 | } 97 | 98 | .guide image { 99 | max-width: 100%; 100 | } 101 | 102 | .guide .image1 { 103 | margin-top: 20rpx; 104 | max-width: 100%; 105 | width: 356px; 106 | height: 47px; 107 | } 108 | 109 | .guide .image2 { 110 | margin-top: 20rpx; 111 | width: 264px; 112 | height: 100px; 113 | } 114 | 115 | .guide .flat-image { 116 | height: 100px; 117 | } 118 | 119 | .guide .code-image { 120 | max-width: 100%; 121 | } 122 | 123 | .guide .copyBtn { 124 | width: 180rpx; 125 | font-size: 20rpx; 126 | margin-top: 16rpx; 127 | margin-left: 0; 128 | } 129 | 130 | .guide .nav { 131 | margin-top: 50rpx; 132 | display: flex; 133 | flex-direction: row; 134 | align-content: space-between; 135 | } 136 | 137 | .guide .nav .prev { 138 | margin-left: unset; 139 | } 140 | 141 | .guide .nav .next { 142 | margin-right: unset; 143 | } 144 | 145 | -------------------------------------------------------------------------------- /scroll-demo/src/App.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 80 | 113 | -------------------------------------------------------------------------------- /scroll-demo/src/components/tabList.vue: -------------------------------------------------------------------------------- 1 | 42 | 107 | -------------------------------------------------------------------------------- /web-cache/cache-node/app/public/cache/static/js/app.86a4b0ce.js: -------------------------------------------------------------------------------- 1 | (function(A){function e(e){for(var t,i,a=e[0],c=e[1],u=e[2],g=0,s=[];g输入采样频率一致 12 | ### 二、实践场景 13 |     下面实现一个demo,通过google浏览器打开电脑麦克风,利用webrtc相关api录音,然后转换成pcm、wav格式,并且用audio标签进行播放,用cavans画出音域图,大致流程如下: 14 | 15 | ![](https://user-gold-cdn.xitu.io/2019/9/25/16d68497a9a88ac8?w=937&h=390&f=png&s=21720) 16 | ### 三、实现步骤 17 | #### 1、获取麦克风权限 18 | 这里使用的是 navigator.getUserMedia 方法,当然如果只是用谷歌浏览器,可以不用兼容处理,主要结构代码如下 19 | ``` 20 | navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia 21 | navigator.getUserMedia({ 22 | audio: true // 这里面有video 和 audio 两个参数,视频选择video 23 | }, (stream) => { 24 | 25 | }, (error) => { 26 | console.log(error) 27 | }) 28 | ``` 29 | #### 2、pcm数据获取 30 | 下面就用 window.AudioContext进行解析麦克风信息,重点用到createMediaStreamSource、createScriptProcessor、onaudioprocess三个方法,具体结构代码如下 31 | ``` 32 | 33 | let audioContext = window.AudioContext || window.webkitAudioContext 34 | const context = new audioContext() 35 | 36 | 37 | let audioInput = context.createMediaStreamSource(stream) 38 | 39 | 40 | let recorder = context.createScriptProcessor(config.bufferSize, config.channelCount, config.channelCount) // 这里config是自定义,后面会附带源码 41 | 42 | 43 | recorder.onaudioprocess = (e) => { 44 | audioData.input(e.inputBuffer.getChannelData(0)) 45 | } 46 | ``` 47 | 但是在获取的过程中要有个触发点,比如说本实践的demo最终效果图如下: 48 | 49 | ![](https://user-gold-cdn.xitu.io/2019/9/26/16d6af1270e60541?w=1089&h=655&f=gif&s=310976) 50 | 所以在录音的过程中,通过点击gif图中的录制按钮,通过点击事件(onclick)触发下面的两行代码,如果不是点击的时候(也可以是其他事件)触发该代码,onaudioprocess方法将接收不到你在打开麦克风权限后所录得音源信息 51 | ``` 52 | audioInput.connect(recorder) //声音源链接过滤处理器 53 | recorder.connect(context.destination) //过滤处理器链接扬声器 54 | ``` 55 | 链接完成后,createScriptProcessor的onaudioprocess方法可以持续不断的返回采样数据,这些数据范围在[-1,1]之间,类型是Float32。现在要做的就是将它们收集起来,将它转成pcm文件数据。 56 | #### 3、audioData定义 57 | 首先定义个 audioData 对象,用来处理数据,整体结构如下,具体见下面源码: 58 | ``` 59 | let audioData = { 60 | size: 0, //录音文件长度 61 | buffer: [], //录音缓存 62 | inputSampleRate: context.sampleRate, //输入采样率 63 | inputSampleBits: 16, //输入采样数位 8, 16 64 | outputSampleRate: config.sampleRate, //输出采样率 65 | oututSampleBits: config.sampleBits, //输出采样数位 8, 16 66 | input: function(data) { // 实时存储录音的数据 67 | }, 68 | getRawData: function() { //合并压缩 69 | }, 70 | covertWav: function() { // 转换成wav文件数据 71 | }, 72 | getFullWavData: function() { // 用blob生成文件 73 | }, 74 | closeContext: function(){ //关闭AudioContext否则录音多次会报错 75 | }, 76 | reshapeWavData: function(sampleBits, offset, iBytes, oData) { // 8位采样数位 77 | }, 78 | getWavBuffer: function() { // 用于绘图wav格式的buffer数据 79 | }, 80 | getPcmBuffer: function() { // pcm buffer 数据 81 | } 82 | } 83 | ``` 84 | 根据上面的gif图: 85 | 86 | >a、第一步点击录制会执行章节 ***1、获取麦克风权限*** 和 ***2、pcm数据获取*** 对应流程, 87 | 然后onaudioprocess方法中调用audioData对象input方法,用来存储buffer数据; 88 | 89 | >b、点击“下载pcm”标签,会依次执行audioData对象getRawData、getPcmBuffer方法,但是下载的是txt文件,并非是pcm文件,由于不知道如何在js环境将txt文件转成pcm文件,所以本人在将txt文件下载下来后直接手动修改了拓展名,当然此修改后的文件是可以播放的,操作流程如下 90 | 91 | ![](https://user-gold-cdn.xitu.io/2019/9/26/16d6b8bf5cdaad97?w=1908&h=895&f=gif&s=1430101) 92 | [pcm文件在线播放链接](https://bj.openstorage.cn/v1/iflyad/landing/pcm_player/),因为本demo是8位的采样位数,所以选择的时候注意一下 93 | #### 4、pcm转wav 94 | pcm是没有头信息的,只要增加44个字节的头信息即可转换成wav,头信息都是固定的,直接用即可,借用网上千篇一律的代码片段 95 | ``` 96 | let writeString = function (str) { 97 | for (var i = 0; i < str.length; i++) { 98 | data.setUint8(offset + i, str.charCodeAt(i)) 99 | } 100 | } 101 | // 资源交换文件标识符 102 | writeString('RIFF'); offset += 4 103 | // 下个地址开始到文件尾总字节数,即文件大小-8 104 | data.setUint32(offset, 36 + dataLength, true); offset += 4 105 | // WAV文件标志 106 | writeString('WAVE'); offset += 4 107 | // 波形格式标志 108 | writeString('fmt '); offset += 4 109 | // 过滤字节,一般为 0x10 = 16 110 | data.setUint32(offset, 16, true); offset += 4 111 | // 格式类别 (PCM形式采样数据) 112 | data.setUint16(offset, 1, true); offset += 2 113 | // 通道数 114 | data.setUint16(offset, config.channelCount, true); offset += 2 115 | // 采样率,每秒样本数,表示每个通道的播放速度 116 | data.setUint32(offset, sampleRate, true); offset += 4 117 | // 波形数据传输率 (每秒平均字节数) 单声道×每秒数据位数×每样本数据位/8 118 | data.setUint32(offset, config.channelCount * sampleRate * (sampleBits / 8), true); offset += 4 119 | // 快数据调整数 采样一次占用字节数 单声道×每样本的数据位数/8 120 | data.setUint16(offset, config.channelCount * (sampleBits / 8), true); offset += 2 121 | // 每样本数据位数 122 | data.setUint16(offset, sampleBits, true); offset += 2 123 | // 数据标识符 124 | writeString('data'); offset += 4 125 | // 采样数据总数,即数据总大小-44 126 | data.setUint32(offset, dataLength, true); offset += 4 127 | // 写入采样数据 128 | data = this.reshapeWavData(sampleBits, offset, bytes, data) 129 | ``` 130 | #### 5、数据转音域图 131 | 转成音域图重点用到AudioContext中的createAnalyser方法,它可以将音波分解,具体步骤如下: 132 | ``` 133 | window.audioBufferSouceNode = context.createBufferSource() //创建声源对象 134 | audioBufferSouceNode.buffer = buffer /声源buffer文件流 135 | gainNode = context.createGain() //创建音量控制器 136 | gainNode.gain.value = 2 137 | audioBufferSouceNode.connect(gainNode) //声源链接音量控制器 138 | let analyser = context.createAnalyser() //创建分析器 139 | analyser.fftSize = 256 140 | gainNode.connect(analyser) //音量控制器链接分析器 141 | analyser.connect(context.destination) //分析器链接扬声器 142 | ``` 143 | 然后拿 analyser.frequencyBinCount 数据用canvas进行绘制,主要代码如下: 144 | ``` 145 | let drawing = function() { 146 | let array = new Uint8Array(analyser.frequencyBinCount) 147 | analyser.getByteFrequencyData(array) 148 | ctx.clearRect(0, 0, 600, 200) 149 | for(let i = 0; i < array.length; i++) { 150 | let _height = array[i] 151 | if(!top[i] || (_height > top[i])) {//帽头落下 152 | top[i] = _height 153 | } else { 154 | top[i] -= 1 155 | } 156 | ctx.fillRect(i * 20, 200 - _height, 4, _height) 157 | ctx.fillRect(i * 20, 200 - top[i] -6.6, 4, 3.3)//绘制帽头 158 | ctx.fillStyle = gradient 159 | } 160 | requestAnimationFrame(drawing) 161 | } 162 | ``` 163 | ### 四、源码地址 164 | 源码github地址:[audio](https://github.com/yuelinghunyu/blog-demo/tree/master/audio) 165 | 166 | 167 | 168 | 169 | -------------------------------------------------------------------------------- /video-player/src/static/fonts/iconfont.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | Created by iconfont 9 | 10 | 11 | 12 | 13 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /video-player/src/static/fonts/iconfont.js: -------------------------------------------------------------------------------- 1 | !function(a){var t,c='',e=(t=document.getElementsByTagName("script"))[t.length-1].getAttribute("data-injectcss");if(e&&!a.__iconfont__svg__cssinject__){a.__iconfont__svg__cssinject__=!0;try{document.write("")}catch(t){console&&console.log(t)}}!function(t){if(document.addEventListener)if(~["complete","loaded","interactive"].indexOf(document.readyState))setTimeout(t,0);else{var e=function(){document.removeEventListener("DOMContentLoaded",e,!1),t()};document.addEventListener("DOMContentLoaded",e,!1)}else document.attachEvent&&(n=t,o=a.document,i=!1,(l=function(){try{o.documentElement.doScroll("left")}catch(t){return void setTimeout(l,50)}c()})(),o.onreadystatechange=function(){"complete"==o.readyState&&(o.onreadystatechange=null,c())});function c(){i||(i=!0,n())}var n,o,i,l}(function(){var t,e;(t=document.createElement("div")).innerHTML=c,c=null,(e=t.getElementsByTagName("svg")[0])&&(e.setAttribute("aria-hidden","true"),e.style.position="absolute",e.style.width=0,e.style.height=0,e.style.overflow="hidden",function(t,e){e.firstChild?function(t,e){e.parentNode.insertBefore(t,e)}(t,e.firstChild):e.appendChild(t)}(e,document.body))})}(window); -------------------------------------------------------------------------------- /min-program/miniprogram/pages/index/index.js: -------------------------------------------------------------------------------- 1 | //index.js 2 | const app = getApp() 3 | 4 | Page({ 5 | data: { 6 | leftList: [], 7 | rightList: [], 8 | leftBorder: {}, 9 | rightBorder: {}, 10 | stardHeight: 0, 11 | positionList: [], 12 | parentTop: 0, 13 | parentLeft: 0, 14 | startFlag: false, 15 | currentId: "", 16 | currentX: 0, 17 | currentY: 0, 18 | rightLeft: 0 19 | }, 20 | // 页面生命周期初始化 21 | onLoad: function () { 22 | wx.showLoading({ 23 | title: '加载中', 24 | }) 25 | // 页面创建时执行 26 | const db = wx.cloud.database({ 27 | env: 'difficult-ojjvv' 28 | }) 29 | db.collection('line').get().then(res => { 30 | wx.hideLoading() 31 | const listArray = res.data[0] 32 | const leftList = listArray.left_list 33 | const rightList = listArray.right_list 34 | this.setData({ 35 | leftList: leftList, 36 | rightList: rightList 37 | }) 38 | this.initPosition() 39 | this.initBorders() 40 | }) 41 | }, 42 | // 获取题面黑点的中心点坐标 43 | initPosition: function () { 44 | const that = this 45 | const query = wx.createSelectorQuery() 46 | query.select("#content").boundingClientRect(function (res) { 47 | that.setData({ 48 | parentTop: res.top, 49 | parentLeft: res.left 50 | }) 51 | }) 52 | query.selectAll(".left-item-icon").boundingClientRect(function (nodeList) { 53 | nodeList.forEach(function (node, index) { 54 | const itemX = "leftList[" + index + "].x" 55 | const itemY = "leftList[" + index + "].y" 56 | that.setData({ 57 | [itemX]: node.left - that.data.parentLeft + node.width / 2, 58 | [itemY]: node.top - that.data.parentTop + node.height / 2 59 | }) 60 | }) 61 | }) 62 | query.selectAll(".right-item-icon").boundingClientRect(function (nodeList) { 63 | nodeList.forEach(function (node, index) { 64 | const itemX = "rightList[" + index + "].x" 65 | const itemY = "rightList[" + index + "].y" 66 | that.setData({ 67 | [itemX]: node.left + node.width / 2, 68 | [itemY]: node.top - that.data.parentTop + node.height / 2 69 | }) 70 | }) 71 | }) 72 | query.select(".right-item-icon").boundingClientRect(function (res) { 73 | that.setData({ 74 | rightLeft: res.left 75 | }) 76 | }) 77 | query.exec() 78 | }, 79 | // 获取坐标允许的经过的边界 80 | initBorders: function () { 81 | const that = this 82 | const query = wx.createSelectorQuery() 83 | query.select(".left").boundingClientRect(function (res) { 84 | console.log(res) 85 | that.setData({ 86 | leftBorder: { 87 | startX: res.left, 88 | startY: 0, 89 | endX: res.right, 90 | endY: res.height 91 | } 92 | }) 93 | }) 94 | query.select(".right").boundingClientRect(function (res) { 95 | console.log(res) 96 | that.setData({ 97 | rightBorder: { 98 | startX: that.data.rightLeft, 99 | startY: 0, 100 | endX: res.right, 101 | endY: res.height 102 | } 103 | }) 104 | }) 105 | query.select(".left-item").boundingClientRect(function (res) { 106 | console.log(res) 107 | that.setData({ 108 | stardHeight: res.height 109 | }) 110 | }) 111 | query.exec() 112 | }, 113 | sortList: function () { 114 | wx.showLoading({ 115 | title: '排序中', 116 | }) 117 | this.data.leftList.sort(this.randomSort) 118 | this.data.rightList.sort(this.randomSort) 119 | this.setData({ 120 | leftList: this.data.leftList, 121 | rightList: this.data.rightList 122 | }) 123 | wx.hideLoading() 124 | this.initPosition() 125 | this.initBorders() 126 | this.resetData() 127 | }, 128 | randomSort: function () { 129 | return Math.random() > 0.5 ? -1 : 1 130 | }, 131 | onCalcPostionLine: function (e) { 132 | let { angle, line } = e.detail 133 | const id = this.data.currentId 134 | const updateItemIndex = this.data.positionList.findIndex(function (leftItem) { 135 | return leftItem.id === id 136 | }) 137 | if (updateItemIndex !== -1) { 138 | const upWidth = "positionList[" + updateItemIndex + "].width" 139 | const upRotate = "positionList[" + updateItemIndex + "].rotate" 140 | this.setData({ 141 | [upWidth]: line, 142 | [upRotate]: angle, 143 | }) 144 | } else { 145 | console.log("流程不对") 146 | } 147 | }, 148 | lineStart: function (e) { 149 | console.log("连线开始", e) 150 | this.setData({ 151 | startFlag: true 152 | }) 153 | const id = e.currentTarget.dataset.id 154 | console.log(this.data.leftList) 155 | const currentItem = this.data.leftList.find(function (leftItem) { 156 | return leftItem.id === id 157 | }) 158 | const existPositionIndex = this.data.positionList.findIndex(function (postion) { 159 | return postion.id === id 160 | }) 161 | let originList = this.data.positionList 162 | if (existPositionIndex !== -1) originList.splice(existPositionIndex, 1) 163 | const newPosition = { 164 | id: id, 165 | startX: currentItem.x, 166 | startY: currentItem.y, 167 | endX: currentItem.x, 168 | endY: currentItem.y, 169 | width: 0, 170 | rotate: 0 171 | } 172 | originList.push(newPosition) 173 | this.setData({ 174 | positionList: originList, 175 | currentId: id 176 | }) 177 | }, 178 | lineMove: function (e) { 179 | const moveX = e.touches[0].pageX - this.data.parentLeft 180 | const moveY = e.touches[0].pageY - this.data.parentTop 181 | if (this.data.startFlag) { 182 | const id = this.data.currentId 183 | const updateItemIndex = this.data.positionList.findIndex(function (leftItem) { 184 | return leftItem.id === id 185 | }) 186 | if (updateItemIndex !== -1) { 187 | const updateEndX = "positionList[" + updateItemIndex + "].endX" 188 | const updateEndY = "positionList[" + updateItemIndex + "].endY" 189 | this.setData({ 190 | [updateEndX]: moveX, 191 | [updateEndY]: moveY, 192 | }) 193 | this.setData({ 194 | currentX: moveX, 195 | currentY: moveY 196 | }) 197 | } else { 198 | console.log("流程不对") 199 | } 200 | } 201 | }, 202 | lineEnd: function () { 203 | this.setData({ 204 | startFlag: false 205 | }) 206 | const currentIndex = this.testBorder(this.data.currentX, this.data.currentY, 'right') 207 | const id = this.data.currentId 208 | const currentPositionList = this.data.positionList 209 | const updateItemIndex = currentPositionList.findIndex(function (leftItem) { 210 | return leftItem.id === id 211 | }) 212 | if (currentIndex !== -1) { 213 | const currentItem = this.data.rightList[currentIndex] // 坐标 214 | if (updateItemIndex !== -1) { 215 | const updateEndX = "positionList[" + updateItemIndex + "].endX" 216 | const updateEndY = "positionList[" + updateItemIndex + "].endY" 217 | this.setData({ 218 | [updateEndX]: currentItem.x, 219 | [updateEndY]: currentItem.y, 220 | }) 221 | this.setData({ 222 | currentX: 0, 223 | currentY: 0 224 | }) 225 | } else { 226 | console.log("流程不对") 227 | } 228 | } else { 229 | currentPositionList.splice(updateItemIndex, 1) 230 | this.setData({ 231 | positionList: currentPositionList 232 | }) 233 | } 234 | console.log(this.data.positionList) 235 | console.log("连线结束") 236 | }, 237 | testBorder: function (x, y, direction) { 238 | if (direction === "left") { // 左边区域 239 | if (x >= this.data.leftBorder.startX && x <= this.data.leftBorder.endX && y >= this.data.leftBorder.startY && y <= this.data.leftBorder.endY) { // 边界中 240 | const index = Math.floor(y / this.data.stardHeight) 241 | return index 242 | } else { 243 | return -1 244 | } 245 | } 246 | if (direction === "right") { // 右边区域 247 | if (x >= this.data.rightBorder.startX && x <= this.data.rightBorder.endX && y >= this.data.rightBorder.startY && y <= this.data.rightBorder.endY) { // 边界中 248 | const index = Math.floor(y / this.data.stardHeight) 249 | return index 250 | } else { 251 | return -1 252 | } 253 | } 254 | }, 255 | resetData: function () { 256 | this.setData({ 257 | positionList: [] 258 | }) 259 | } 260 | }) 261 | -------------------------------------------------------------------------------- /view-pic/src/custom/layer/layer.vue: -------------------------------------------------------------------------------- 1 | 51 | 230 | -------------------------------------------------------------------------------- /audio/js/record.js: -------------------------------------------------------------------------------- 1 | ((window) => { 2 | window.URL = window.URL || window.webkitURL 3 | navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia 4 | window.audioBufferSouceNode = null 5 | 6 | let Recorder = function(stream, config){ 7 | let audioContext = window.AudioContext || window.webkitAudioContext 8 | const context = new audioContext() 9 | 10 | config = config || {} 11 | config.channelCount = 1 12 | config.numberOfInputChannels = config.channelCount 13 | config.numberOfOutputChannels = config.channelCount 14 | config.sampleBits = config.sampleBits || 16 15 | config.sampleRate = config.sampleRate || 8000 16 | config.bufferSize = 4096 //创建缓存,用来缓存声音 17 | 18 | let audioInput = context.createMediaStreamSource(stream) //将声音输入这个对像 19 | let volume = context.createGain() //设置音量节点 20 | audioInput.connect(volume) 21 | 22 | // 创建声音的缓存节点,createScriptProcessor方法的第二个和第三个参数指的是输入和输出都是声道数 23 | let recorder = context.createScriptProcessor(config.bufferSize, config.channelCount, config.channelCount) 24 | 25 | //用来储存读出的麦克风数据,和压缩这些数据,将这些数据转换为WAV文件的格式 26 | let audioData = { 27 | size: 0, //录音文件长度 28 | buffer: [], //录音缓存 29 | inputSampleRate: context.sampleRate, //输入采样率 30 | inputSampleBits: 16, //输入采样数位 8, 16 31 | outputSampleRate: config.sampleRate, //输出采样率 32 | oututSampleBits: config.sampleBits, //输出采样数位 8, 16 33 | input: function(data) { // 实时存储录音的数据 34 | this.buffer.push(new Float32Array(data)) //Float32Array 35 | this.size += data.length 36 | }, 37 | getRawData: function() { //合并压缩 38 | //合并 39 | let data = new Float32Array(this.size) 40 | let offset = 0 41 | for(let i = 0; i < this.buffer.length; i++) { 42 | data.set(this.buffer[i], offset) 43 | offset += this.buffer[i].length 44 | } 45 | // 压缩 46 | let getRawDataion = parseInt(this.inputSampleRate / this.outputSampleRate) 47 | let length = data.length / getRawDataion 48 | let result = new Float32Array(length) 49 | let index = 0, j = 0 50 | while (index < length) { 51 | result[index] = data[j] 52 | j += getRawDataion 53 | index++ 54 | } 55 | return result 56 | }, 57 | covertWav: function() { // 转换成wav文件数据 58 | let sampleRate = Math.min(this.inputSampleRate, this.outputSampleRate) 59 | let sampleBits = Math.min(this.inputSampleBits, this.oututSampleBits) 60 | let bytes = this.getRawData() 61 | let dataLength = bytes.length * (sampleBits / 8) 62 | let buffer = new ArrayBuffer(44 + dataLength) 63 | let data = new DataView(buffer) 64 | let offset = 0 65 | let writeString = function (str) { 66 | for (var i = 0; i < str.length; i++) { 67 | data.setUint8(offset + i, str.charCodeAt(i)) 68 | } 69 | } 70 | // 资源交换文件标识符 71 | writeString('RIFF'); offset += 4 72 | // 下个地址开始到文件尾总字节数,即文件大小-8 73 | data.setUint32(offset, 36 + dataLength, true); offset += 4 74 | // WAV文件标志 75 | writeString('WAVE'); offset += 4 76 | // 波形格式标志 77 | writeString('fmt '); offset += 4 78 | // 过滤字节,一般为 0x10 = 16 79 | data.setUint32(offset, 16, true); offset += 4 80 | // 格式类别 (PCM形式采样数据) 81 | data.setUint16(offset, 1, true); offset += 2 82 | // 通道数 83 | data.setUint16(offset, config.channelCount, true); offset += 2 84 | // 采样率,每秒样本数,表示每个通道的播放速度 85 | data.setUint32(offset, sampleRate, true); offset += 4 86 | // 波形数据传输率 (每秒平均字节数) 单声道×每秒数据位数×每样本数据位/8 87 | data.setUint32(offset, config.channelCount * sampleRate * (sampleBits / 8), true); offset += 4 88 | // 快数据调整数 采样一次占用字节数 单声道×每样本的数据位数/8 89 | data.setUint16(offset, config.channelCount * (sampleBits / 8), true); offset += 2 90 | // 每样本数据位数 91 | data.setUint16(offset, sampleBits, true); offset += 2 92 | // 数据标识符 93 | writeString('data'); offset += 4 94 | // 采样数据总数,即数据总大小-44 95 | data.setUint32(offset, dataLength, true); offset += 4 96 | // 写入采样数据 97 | data = this.reshapeWavData(sampleBits, offset, bytes, data) 98 | return data 99 | }, 100 | getFullWavData: function() { // 用blob生成文件 101 | const data = this.covertWav() 102 | return new Blob([data], { type: 'audio/wav' }) 103 | }, 104 | closeContext: function(){ //关闭AudioContext否则录音多次会报错 105 | context.close() 106 | }, 107 | reshapeWavData: function(sampleBits, offset, iBytes, oData) { // 8位采样数位 108 | if (sampleBits === 8) { 109 | for (let i = 0; i < iBytes.length; i++, offset++) { 110 | let s = Math.max(-1, Math.min(1, iBytes[i])) 111 | let val = s < 0 ? s * 0x8000 : s * 0x7FFF 112 | val = parseInt(255 / (65535 / (val + 32768))) 113 | oData.setInt8(offset, val, true) 114 | } 115 | } else { 116 | for (let i = 0; i < iBytes.length; i++, offset += 2) { 117 | let s = Math.max(-1, Math.min(1, iBytes[i])) 118 | oData.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true) 119 | } 120 | } 121 | return oData 122 | }, 123 | getWavBuffer: function() { // 用于绘图wav格式的buffer数据 124 | const data = this.covertWav() 125 | return data.buffer 126 | }, 127 | getPcmBuffer: function() { // pcm buffer 数据 128 | let bytes = this.getRawData(), 129 | offset = 0, 130 | sampleBits = this.oututSampleBits, 131 | dataLength = bytes.length * (sampleBits / 8), 132 | buffer = new ArrayBuffer(dataLength), 133 | data = new DataView(buffer); 134 | for (var i = 0; i < bytes.length; i++, offset += 2) { 135 | var s = Math.max(-1, Math.min(1, bytes[i])); 136 | // 16位直接乘就行了 137 | data.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true); 138 | } 139 | return new Blob([data]) 140 | } 141 | } 142 | 143 | // 开始录音 144 | this.start = () => { 145 | audioInput.connect(recorder) 146 | recorder.connect(context.destination) 147 | } 148 | // 获取音频文件 149 | this.getBlob = () => { 150 | this.stop() 151 | return audioData.getFullWavData() 152 | } 153 | this.getBuffer = () => { 154 | this.stop() 155 | return audioData.getPcmBuffer() 156 | } 157 | // 播放 158 | this.play = (audio, ctx) => { 159 | audio.src = window.URL.createObjectURL(this.getBlob()) 160 | audio.addEventListener("play", () => { 161 | this.draw(ctx) 162 | }) 163 | } 164 | // wav文件资源 165 | this.wavSrc = () => { 166 | return window.URL.createObjectURL(this.getBlob()) 167 | } 168 | // pcm 文件 169 | this.pcmSrc = () => { 170 | this.stop() 171 | return window.URL.createObjectURL(this.getBuffer()) 172 | } 173 | // 停止 174 | this.stop = () => { 175 | recorder.disconnect() 176 | } 177 | this.close=function(){ 178 | audioData.closeContext() 179 | } 180 | // 音频绘制 181 | this.draw = function(ctx) { 182 | const arraybuffer = audioData.getWavBuffer() 183 | context.decodeAudioData(arraybuffer, (buffer) => { 184 | if(window.audioBufferSouceNode!=null) { 185 | window.audioBufferSouceNode.stop() 186 | } 187 | window.audioBufferSouceNode = context.createBufferSource() 188 | audioBufferSouceNode.buffer = buffer 189 | gainNode = context.createGain() 190 | gainNode.gain.value = 2 191 | audioBufferSouceNode.connect(gainNode) 192 | let analyser = context.createAnalyser() 193 | analyser.fftSize = 256 194 | gainNode.connect(analyser) 195 | analyser.connect(context.destination) 196 | audioBufferSouceNode.start(0) 197 | 198 | let top = new Uint8Array(analyser.frequencyBinCount) 199 | let gradient = ctx.createLinearGradient(0, 0, 4, 200) 200 | gradient.addColorStop(1, 'pink') 201 | gradient.addColorStop(0.5, 'blue') 202 | gradient.addColorStop(0, 'red') 203 | let drawing = function() { 204 | let array = new Uint8Array(analyser.frequencyBinCount) 205 | analyser.getByteFrequencyData(array) 206 | ctx.clearRect(0, 0, 600, 200) 207 | for(let i = 0; i < array.length; i++) { 208 | let _height = array[i] 209 | if(!top[i] || (_height > top[i])) {//帽头落下 210 | top[i] = _height 211 | } else { 212 | top[i] -= 1 213 | } 214 | ctx.fillRect(i * 20, 200 - _height, 4, _height) 215 | ctx.fillRect(i * 20, 200 - top[i] -6.6, 4, 3.3)//绘制帽头 216 | ctx.fillStyle = gradient 217 | } 218 | requestAnimationFrame(drawing) 219 | } 220 | drawing() 221 | }) 222 | } 223 | // 音频采集 224 | recorder.onaudioprocess = (e) => { 225 | audioData.input(e.inputBuffer.getChannelData(0)) 226 | } 227 | } 228 | // 获取麦克风 229 | Recorder.get = (callback, config) => { 230 | if(callback) { 231 | if(navigator.getUserMedia) { 232 | navigator.getUserMedia({ 233 | audio: true 234 | }, (stream) => { 235 | const rec = new Recorder(stream, config) 236 | callback(rec) 237 | }, (error) => { 238 | console.log(error) 239 | }) 240 | } else { 241 | alert("麦克风获取失败") 242 | } 243 | } 244 | } 245 | window.Recorder = Recorder 246 | })(window) -------------------------------------------------------------------------------- /min-program/README.md: -------------------------------------------------------------------------------- 1 | ### 前言 2 | --- 3 | 前段时间一直查阅关于前端划一条直线的实现方案,网上给的答案大概归于两种: 4 | * 根据区域中两个坐标点,在其之间由点的集合形成直线 5 | * 第二种由svg代替点的集合 6 | 7 | 前端还有许多插件可以实现连线,比如 [jsplumb 中文教程](https://wdd.js.org/jsplumb-chinese-tutorial/#/) ;今天还是手动实现下类似于上面两种方案的连线,实现的场景微信小程序。 8 | 9 | ### 规划 10 | --- 11 | 首先你会小程序,回想一下这几年一直都在叠加业务代码,不免有种后背发凉的感觉:“**小程序不会!!!**”,所以巴拉巴拉的看了下小程序官方文档,把能用到的地方都重点看了下,下面开干。 12 | 13 | #### 1、设计场景 14 | 15 | 根据本人从事教育行业遇到的试题操作 —— **连线题**,如图所示场景实现的效果图,在本文的结尾会有小程序二维码奉上 16 | 17 | ![](https://user-gold-cdn.xitu.io/2020/3/20/170f5ea267617a58?w=1481&h=749&f=gif&s=234637) 18 | **
古诗词连线示例图
** 19 | 20 | #### 2、方案设计 21 | 根据图例在小程序中实现左边诗句和右边诗句的连线,具体实现方案如下: 22 | 1. 整个实例分成首页和线条组件两部分,线条的信息通过数组保存起来 23 | 2. 线条组件本质是一个div标签在实时的变化长度和角度 24 | 3. 线条组件接收起始点坐标,利用三角函数算出角度和div的长度,实时改变其位置 25 | 26 | ### 实现 27 | --- 28 | 下面开始一步步实现前端代码,这里面为了方便小伙伴们节省精力,以下的代码都是主要架构业务逻辑,具体实现结尾会附上源码。 29 | 30 | 首先**主页**和**线条组件**关系如图所示: 31 | 32 | ![](https://user-gold-cdn.xitu.io/2020/3/23/171075f94d68b537?w=500&h=250&f=png&s=22761) 33 | **
古诗词连线组件图
** 34 | 35 | #### 1、线条组件 36 | 线条组件代码量不多,下面粘贴的代码都附加注释,方便阅读 37 | ``` 38 | Component({ 39 | properties: { 40 | // 这里定义了line组件属性,属性值可以在组件使用时指定 41 | startX: { 42 | type: Number, 43 | value: 0, 44 | }, 45 | startY: { 46 | type: Number, 47 | value: 0, 48 | }, 49 | endX: { 50 | type: Number, 51 | value: 0, 52 | }, 53 | endY: { 54 | type: Number, 55 | value: 0, 56 | } 57 | }, 58 | // 实时监听从主页传来的坐标,相当于vue中watch 59 | observers: { 60 | 'startX, startY, endX, endY': function (startX, startY, endX, endY) { 61 | // 计算线条角度和长度 62 | let { angle, line } = this.getAngle(startX, startY, endX, endY) 63 | this.setData({ 64 | angle: angle, 65 | line: line 66 | }) 67 | // 这里必须在组件都加载成功后调用此方法,不然主页绑定不了子组件的事件 68 | if (this.data.ready) this.initStartPostion() 69 | } 70 | }, 71 | data: { 72 | angle: 0, 73 | line: 0, 74 | ready: false 75 | }, 76 | lifetimes: { 77 | // 这是小程序的生命周期 78 | ready: function () { // 初始节点完成初始调用 79 | this.setData({ 80 | ready: true 81 | }) 82 | // 这里是小程序的主动发射事件,相当于vue emit发射事件,初始化调用一次,然后在observers变化时调用 83 | this.initStartPostion() 84 | } 85 | }, 86 | methods: { 87 | // 算角度 88 | getAngle: function (startX, startY, endX, endY) { 89 | let angle = 0 90 | let line = 0 91 | angle = this.getTanDeg(startX, endX, startY, endY) 92 | console.log("angle:" + angle) 93 | line = Math.round(Math.sqrt(Math.pow((endX - startX), 2) + Math.pow((endY - startY), 2))) 94 | return { angle, line } 95 | }, 96 | // 三角函数算角度 97 | getTanDeg: function (startX, endX, startY, endY) { 98 | let disY = endY - startY 99 | let disX = endX - startX 100 | let result = Math.atan2(disY, disX) * (180 / Math.PI) 101 | return Math.round(result) 102 | }, 103 | // 向主页发射方法,用于主页实时渲染 104 | initStartPostion: function () { 105 | this.triggerEvent("calcPostionLine", { angle: this.data.angle, line: this.data.line }) 106 | } 107 | }, 108 | }) 109 | ``` 110 | 上面的 triggerEvent 绑定的事件calcPostionLine 用于主页接收事件: 111 | ``` 112 | 113 | 114 | ...... 115 | 129 | 130 | ...... 131 | 132 | ``` 133 | #### 2、主页 134 | 主页主要是初始化渲染左右诗词,获取诗词的起点和终点,然后绑定touchstart、touchmove、touchend事件,根据这些事件更新子组件line的props值,经此循环,实现动态的划线 135 | 136 | * ##### 获取诗词的起始坐标 137 | ``` 138 | // 页面生命周期初始化 139 | onLoad: function () { 140 | ...... 141 | this.initPosition() 142 | this.initBorders() 143 | ...... 144 | }, 145 | // 获取题面左右黑点的中心点坐标 146 | initPosition: function () { 147 | const query = wx.createSelectorQuery() 148 | query.select("#content").boundingClientRect(function (res) { 149 | ....... 150 | }) 151 | query.selectAll(".left-item-icon").boundingClientRect(function (nodeList) { 152 | nodeList.forEach(function (node, index) { 153 | ...... 154 | }) 155 | }) 156 | query.selectAll(".right-item-icon").boundingClientRect(function (nodeList) { 157 | nodeList.forEach(function (node, index) { 158 | ...... 159 | }) 160 | }) 161 | query.select(".right-item-icon").boundingClientRect(function (res) { 162 | ...... 163 | }) 164 | query.exec() 165 | }, 166 | // 获取坐标允许的经过的边界 167 | initBorders: function () { 168 | const query = wx.createSelectorQuery() 169 | query.select(".left").boundingClientRect(function (res) { 170 | ...... 171 | }) 172 | query.select(".right").boundingClientRect(function (res) { 173 | ...... 174 | }) 175 | query.select(".left-item").boundingClientRect(function (res) { 176 | ...... 177 | }) 178 | query.exec() 179 | }, 180 | ``` 181 | 上面是小程序获取页面节点的方式,小程序没有dom的概念,这些方法可以获取相应节点的偏移值、尺寸等属性值 182 | * ##### 根据手势获取拖动坐标 183 | 页面的线条是通过维护数组positionList字段,下面的手势就是更新数组对应的线条的起始点坐标 184 | ``` 185 | lineStart: function (e) { 186 | console.log("连线开始", e) 187 | this.setData({ 188 | startFlag: true 189 | }) 190 | const id = e.currentTarget.dataset.id 191 | console.log(this.data.leftList) 192 | const currentItem = this.data.leftList.find(function (leftItem) { 193 | return leftItem.id === id 194 | }) 195 | const existPositionIndex = this.data.positionList.findIndex(function (postion) { 196 | return postion.id === id 197 | }) 198 | let originList = this.data.positionList 199 | 200 | if (existPositionIndex !== -1) originList.splice(existPositionIndex, 1) 201 | const newPosition = { 202 | id: id, 203 | startX: currentItem.x, 204 | startY: currentItem.y, 205 | endX: currentItem.x, 206 | endY: currentItem.y, 207 | width: 0, 208 | rotate: 0 209 | } 210 | originList.push(newPosition) 211 | this.setData({ 212 | positionList: originList, 213 | currentId: id 214 | }) 215 | }, 216 | lineMove: function (e) { 217 | const moveX = e.touches[0].pageX - this.data.parentLeft 218 | const moveY = e.touches[0].pageY - this.data.parentTop 219 | if (this.data.startFlag) { 220 | const id = this.data.currentId 221 | const updateItemIndex = this.data.positionList.findIndex(function (leftItem) { 222 | return leftItem.id === id 223 | }) 224 | 225 | if (updateItemIndex !== -1) { 226 | const updateEndX = "positionList[" + updateItemIndex + "].endX" 227 | const updateEndY = "positionList[" + updateItemIndex + "].endY" 228 | this.setData({ 229 | [updateEndX]: moveX, 230 | [updateEndY]: moveY, 231 | }) 232 | this.setData({ 233 | currentX: moveX, 234 | currentY: moveY 235 | }) 236 | } else { 237 | console.log("流程不对") 238 | } 239 | } 240 | }, 241 | lineEnd: function () { 242 | this.setData({ 243 | startFlag: false 244 | }) 245 | 246 | const currentIndex = this.testBorder(this.data.currentX, this.data.currentY, 'right') 247 | const id = this.data.currentId 248 | const currentPositionList = this.data.positionList 249 | const updateItemIndex = currentPositionList.findIndex(function (leftItem) { 250 | return leftItem.id === id 251 | }) 252 | if (currentIndex !== -1) { 253 | 254 | const currentItem = this.data.rightList[currentIndex] // 坐标 255 | if (updateItemIndex !== -1) { 256 | const updateEndX = "positionList[" + updateItemIndex + "].endX" 257 | const updateEndY = "positionList[" + updateItemIndex + "].endY" 258 | this.setData({ 259 | [updateEndX]: currentItem.x, 260 | [updateEndY]: currentItem.y, 261 | }) 262 | this.setData({ 263 | currentX: 0, 264 | currentY: 0 265 | }) 266 | } else { 267 | console.log("流程不对") 268 | } 269 | } else { 270 | currentPositionList.splice(updateItemIndex, 1) 271 | this.setData({ 272 | positionList: currentPositionList 273 | }) 274 | } 275 | console.log(this.data.positionList) 276 | console.log("连线结束") 277 | }, 278 | ``` 279 | 这里面更新数据的方式有点特别: 280 | ``` 281 | const updateEndX = "positionList[" + updateItemIndex + "].endX" 282 | const updateEndY = "positionList[" + updateItemIndex + "].endY" 283 | this.setData({ 284 | [updateEndX]: moveX, 285 | [updateEndY]: moveY, 286 | }) 287 | ``` 288 | 有没有发现有点像symbol对象更新对象的方式 289 | * ##### 边界判断 290 | 边界判断是将终点坐标在右侧诗词区域内都视为连线成功区域,此方法返回终点坐标在右侧诗词区域第几个下标 291 | ``` 292 | testBorder: function (x, y, direction) { 293 | if (direction === "left") { // 左边区域 294 | if (x >= this.data.leftBorder.startX && x <= this.data.leftBorder.endX && y >= this.data.leftBorder.startY && y <= this.data.leftBorder.endY) { // 边界中 295 | const index = Math.floor(y / this.data.stardHeight) 296 | return index 297 | } else { 298 | return -1 299 | } 300 | } 301 | if (direction === "right") { // 右边区域 302 | if (x >= this.data.rightBorder.startX && x <= this.data.rightBorder.endX && y >= this.data.rightBorder.startY && y <= this.data.rightBorder.endY) { // 边界中 303 | const index = Math.floor(y / this.data.stardHeight) 304 | return index 305 | } else { 306 | return -1 307 | } 308 | } 309 | }, 310 | ``` 311 | 312 | ### 结语 313 | ___ 314 | * 上面代码量也不多,感兴趣可以在微信开发工具查看效果,重点是实现连线的思想是否可取,这种方式如果改变线条的样式怎么办?我也试着改了下代码(可能看不清,锯齿状的线条): 315 | 316 | ![](https://user-gold-cdn.xitu.io/2020/3/24/1710c4ec668c113d?w=1481&h=749&f=gif&s=183572) 317 | **
锯齿线条
** 318 | * 可以继续在此拓展左右都可以连线,长按删除连线等 319 | ### 源码 320 | ___ 321 | 源码地址:[小程序实践 —— 精简版前端连线题](https://github.com/yuelinghunyu/blog-demo/tree/master/min-program) 322 | 在线预览: 323 | ![](https://user-gold-cdn.xitu.io/2020/3/24/1710c579ecf2aa86?w=258&h=294&f=jpeg&s=18714) -------------------------------------------------------------------------------- /scroll-demo/README.md: -------------------------------------------------------------------------------- 1 | ### 一、场景 2 | 项目中经常遇到区域超出部分会出现滚动条,滚动条在pc端可以通过鼠标滚轮控制上下,在移动端可以通过鼠标拖动页面进行滚动,这两种场景都是符合用户习惯,然而这种滚动条一般都是竖【vertical】型滚动条,如果pc端出现横向滚动条【horizontal】,在不做处理的情况下,你只能用鼠标拖动横向滚动条按钮展示滚动区域,而且为了美观,一般滚动条会进行样式编写或者隐藏,那么横向区域默认情况下就没法滚动。 3 | ### 二、描述 4 | 现为了解决pc端滚动区域能像移动端一样,能够通过鼠标拖动滚动区域直接进行滚动,如图所示 5 | 6 | ![](https://user-gold-cdn.xitu.io/2020/2/13/1703dc165ec9694d?w=1438&h=805&f=gif&s=629013) 7 |
滚动联动示例图
8 | 9 | 滚动实例用到知识点如下: 10 | 11 | + 采用 vue-cli3 + iscroll.js 组合实现; 12 | + 使用 vue 自定义指令实现 iscroll 实例化和参数配置; 13 | + 上下滚动区域联动,自行实现横向滚动条居中显示和使用 scrollIntoView 的差别 14 | ### 三、自定义指令 v-iscroll 15 | #### 1、新建指令文件 16 | 这里使用 vue 自定义指令初始化 iscroll 实例,在 vue-cli3 项目目录下新建 vIscroll.js ,文件代码如下: 17 | ``` 18 | const IScroll = require('iscroll') 19 | const VIScroll = { 20 | install: function (Vue, options) { 21 | Vue.directive('iscroll', { 22 | inserted: function (el, binding, vnode) { 23 | let callBack 24 | let iscrollOptions = options 25 | const option = binding.value && binding.value.option 26 | const func = binding.value && binding.value.instance 27 | // 判断输入参数 28 | const optionType = option ? [].toString.call(option) : undefined 29 | const funcType = func ? [].toString.call(func) : undefined 30 | // 兼容 google 浏览器拖动 31 | el.addEventListener('touchmove', function (e) { 32 | e.preventDefault() 33 | }) 34 | // 将参数配置到new IScroll(el, iscrollOptions)中 35 | if (optionType === '[object Object]') { 36 | iscrollOptions = option 37 | } 38 | if (funcType === '[object Function]') { 39 | callBack = func 40 | } 41 | // 使用vnode绑定iscroll是为了让iscroll对象能够夸状态传递,避免iscroll重复建立 42 | // 这里面跟官方网站 const myScroll = new IScroll('#wrapper',option) 初始化一样 43 | vnode.scroll = new IScroll(el, iscrollOptions) 44 | // 如果指令传递函数进来,把iscroll实例传递出去 45 | if (callBack) callBack(vnode.scroll) 46 | }, 47 | componentUpdated: function (el, binding, vnode, oldVnode) { 48 | // 将scroll绑定到新的vnode上,避免多次绑定 49 | vnode.scroll = oldVnode.scroll 50 | // 使用 settimeout 让refresh跳到事件流结尾,保证refresh时数据已经更新完毕 51 | setTimeout(() => { 52 | vnode.scroll.refresh() 53 | }, 0) 54 | }, 55 | unbind: function (el, binding, vnode, oldVnode) { 56 | // 解除绑定时要把iscroll销毁 57 | vnode.scroll = oldVnode.scroll 58 | vnode.scroll.destroy() 59 | vnode.scroll = null 60 | } 61 | }) 62 | } 63 | } 64 | module.exports = VIScroll 65 | ``` 66 | 这里附上 [**iscroll.js 5**](http://caibaojian.com/iscroll-5/) 官方文档地址,[**iscroll npm**](https://www.npmjs.com/package/iscroll) 包地址,相关属性和方法自行查看。 67 | #### 2、加载引用指令 68 | 首先在 main.js 中加载指令: 69 | ``` 70 | import Vue from 'vue' 71 | import App from './App.vue' 72 | import "./assets/reset.css" 73 | // 加载scroll指令 74 | import VIscroll from './directive/vIscroll' 75 | Vue.use(VIscroll) 76 | Vue.config.productionTip = false 77 | 78 | new Vue({ 79 | render: h => h(App), 80 | }).$mount('#app') 81 | 82 | ``` 83 | 使用指令,摘自部分代码如下: 84 | 85 | ``` 86 | 127 | 172 | 175 | ``` 176 | 上述代码中 v-iscroll 指令传入两个字段参数: 177 | + option:配置iscroll参数,这里面注意scrollX,scrollY两个属性,代表的是横向还是竖向滚动; 178 | + instance:回调方法的调用, **vIscroll.js ** 中执行回调方法,通过该组件方法 getIscroll() 获取到 iscroll 的实例。 179 | #### 3、上下滚动区域联动 180 | 实际上面的代码可以解决上面场景的问题,现在实现上下区域联动,通过选中横向滚动条某个按钮,使其变成选中状态,然后竖向滚动条对应的项跳到首位,如图所以: 181 | 182 | 183 | 184 | ![](https://user-gold-cdn.xitu.io/2020/2/13/1703e5b384ef4f6f?w=944&h=394&f=png&s=20489) 185 | 186 |
联动示例图
187 | 188 | ##### 3-1、联动实现方法 189 | 点击按钮的方法: 190 | ``` 191 | tabEvent (item, currentIndex) { 192 | this.currentId = item.id 193 | this.currentIndex = currentIndex 194 | 195 | ... 196 | 197 | this.$emit("switchTab", this.currentId, this.currentIndex) 198 | }, 199 | ``` 200 | 竖向组件代码部分如下,并对 switchTab() 代码进行详细注释: 201 | ``` 202 | 230 | 231 | 272 | 276 | 277 | ``` 278 | 这里面用到的都是 iscroll 插件自带的属性和方法进行滚动边界的判断和滚动,比用 JavaScript 方法方便的多,而且用了iscroll作为滚动容器,已经在 vIscroll.js 禁用了相关浏览器默认事件。 279 | ##### 3-2、居中显示 280 | 这里 JavaScript 有个 **scrollIntoView()** 方法,[官方文档链接](https://developer.mozilla.org/zh-CN/docs/Web/API/Element/scrollIntoView),这个方法让当前的元素滚动到浏览器窗口的可视区域内。关键缺点是,如果横向滚动和竖向滚动都同时用到这个方法,只能保证一个滚动区域有效,另一个会不滚动。 281 | 使用 scrollIntoView() 方法配置如下: 282 | ``` 283 | this.$refs.tabItem[this.currentIndex].scrollIntoView({ 284 | behavior: "smooth", 285 | inline: "center", 286 | block: 'nearest' 287 | }) 288 | ``` 289 | 这里再横向滚动区域添加了一对左右按钮,实现切换功能,如图所示: 290 | 291 | ![](https://user-gold-cdn.xitu.io/2020/2/13/1703e7aebe13ca54?w=682&h=168&f=png&s=6247) 292 | 293 |
切换按钮示例图
294 | 295 | 切换按钮事件方法就是通过改变上一个、下一个按钮下标,调用[单击按钮](#jump)方法,实现切换功能,切换事件方法逻辑如下: 296 | ``` 297 | tabBtnEvent (direction) { 298 | const max = this.$refs.tabItem.length 299 | if (direction === 'left' && this.currentIndex > 0) { 300 | this.currentIndex-- 301 | } 302 | if (direction === 'right' && this.currentIndex < max - 1) { 303 | this.currentIndex++ 304 | } 305 | 306 | this.tabEvent(this.$refs.tabItem[this.currentIndex], this.currentIndex) 307 | }, 308 | ``` 309 | 下面对单击按钮事件添加居中逻辑,详细代码和解析图如下,可以对比查看: 310 | 311 | ![](https://user-gold-cdn.xitu.io/2020/2/13/1703ea766b7c4cbc?w=569&h=315&f=png&s=34723) 312 |
居中计算图
313 | 314 | ``` 315 | tabEvent (item, currentIndex) { 316 | this.currentId = item.id 317 | this.currentIndex = currentIndex 318 | // 获取滚动容器的长度的一半,即中间点 319 | const scrollContainerHalfWidth = this.$refs.scrollContainer.offsetWidth / 2 320 | // 获取单个item的一半长度 321 | const tabItemHalfWidth = this.$refs.tabItem[currentIndex].offsetWidth / 2 322 | // 求取插值,就是开始到中间开始位置的距离 323 | const halfDistance = scrollContainerHalfWidth - tabItemHalfWidth 324 | // 求取当前item的相对总长度的偏移量 325 | const currentItemOffsetLeft = this.$refs.tabItem[currentIndex].offsetLeft 326 | // scroll 移动到中间的值 327 | const x = halfDistance - currentItemOffsetLeft 328 | this.myScroll.scrollTo(x, 0) 329 | this.$emit("switchTab", this.currentId, this.currentIndex) 330 | }, 331 | ``` 332 | ### 4、总结 333 | 1、整个实例用的都是iscroll插件相关属性实现的滚动,避免同时使用JavaScript方法造成的代码混乱; 334 | 2、利用自定义指令的方式有效的避免了传统实例化iscroll带来的代码冗余,使其方便简洁; 335 | 3、本实例滚动选项都是字符串,如果出现图片的情况,合理使用iscroll.refresh(); 方法,合适时期重新计算滚动区域,避免滚动边界受限; 336 | 4、附上代码 GitHub 地址 [vue中利用iscroll.js解决pc端滚动问题](https://github.com/yuelinghunyu/blog-demo/tree/master/scroll-demo) 337 | -------------------------------------------------------------------------------- /video-player/src/static/fonts/demo_index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IconFont Demo 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 |

19 | 29 |
30 |
31 |
    32 | 33 |
  • 34 | 35 |
    全屏
    36 |
    &#xe660;
    37 |
  • 38 | 39 |
  • 40 | 41 |
    播放
    42 |
    &#xe63a;
    43 |
  • 44 | 45 |
  • 46 | 47 |
    暂停
    48 |
    &#xe60a;
    49 |
  • 50 | 51 |
  • 52 | 53 |
    声音
    54 |
    &#xe617;
    55 |
  • 56 | 57 |
  • 58 | 59 |
    声音关闭
    60 |
    &#xe8b8;
    61 |
  • 62 | 63 |
64 |
65 |

Unicode 引用

66 |
67 | 68 |

Unicode 是字体在网页端最原始的应用方式,特点是:

69 |
    70 |
  • 兼容性最好,支持 IE6+,及所有现代浏览器。
  • 71 |
  • 支持按字体的方式去动态调整图标大小,颜色等等。
  • 72 |
  • 但是因为是字体,所以不支持多色。只能使用平台里单色的图标,就算项目里有多色图标也会自动去色。
  • 73 |
74 |
75 |

注意:新版 iconfont 支持多色图标,这些多色图标在 Unicode 模式下将不能使用,如果有需求建议使用symbol 的引用方式

76 |
77 |

Unicode 使用步骤如下:

78 |

第一步:拷贝项目下面生成的 @font-face

79 |
@font-face {
 81 |   font-family: 'iconfont';
 82 |   src: url('iconfont.eot');
 83 |   src: url('iconfont.eot?#iefix') format('embedded-opentype'),
 84 |       url('iconfont.woff2') format('woff2'),
 85 |       url('iconfont.woff') format('woff'),
 86 |       url('iconfont.ttf') format('truetype'),
 87 |       url('iconfont.svg#iconfont') format('svg');
 88 | }
 89 | 
90 |

第二步:定义使用 iconfont 的样式

91 |
.iconfont {
 93 |   font-family: "iconfont" !important;
 94 |   font-size: 16px;
 95 |   font-style: normal;
 96 |   -webkit-font-smoothing: antialiased;
 97 |   -moz-osx-font-smoothing: grayscale;
 98 | }
 99 | 
100 |

第三步:挑选相应图标并获取字体编码,应用于页面

101 |
102 | <span class="iconfont">&#x33;</span>
104 | 
105 |
106 |

"iconfont" 是你项目下的 font-family。可以通过编辑项目查看,默认是 "iconfont"。

107 |
108 |
109 |
110 |
111 |
    112 | 113 |
  • 114 | 115 |
    116 | 全屏 117 |
    118 |
    .icon-quanping 119 |
    120 |
  • 121 | 122 |
  • 123 | 124 |
    125 | 播放 126 |
    127 |
    .icon-bofang 128 |
    129 |
  • 130 | 131 |
  • 132 | 133 |
    134 | 暂停 135 |
    136 |
    .icon-zanting 137 |
    138 |
  • 139 | 140 |
  • 141 | 142 |
    143 | 声音 144 |
    145 |
    .icon-shengyin 146 |
    147 |
  • 148 | 149 |
  • 150 | 151 |
    152 | 声音关闭 153 |
    154 |
    .icon-shengyinguanbi 155 |
    156 |
  • 157 | 158 |
159 |
160 |

font-class 引用

161 |
162 | 163 |

font-class 是 Unicode 使用方式的一种变种,主要是解决 Unicode 书写不直观,语意不明确的问题。

164 |

与 Unicode 使用方式相比,具有如下特点:

165 |
    166 |
  • 兼容性良好,支持 IE8+,及所有现代浏览器。
  • 167 |
  • 相比于 Unicode 语意明确,书写更直观。可以很容易分辨这个 icon 是什么。
  • 168 |
  • 因为使用 class 来定义图标,所以当要替换图标时,只需要修改 class 里面的 Unicode 引用。
  • 169 |
  • 不过因为本质上还是使用的字体,所以多色图标还是不支持的。
  • 170 |
171 |

使用步骤如下:

172 |

第一步:引入项目下面生成的 fontclass 代码:

173 |
<link rel="stylesheet" href="./iconfont.css">
174 | 
175 |

第二步:挑选相应图标并获取类名,应用于页面:

176 |
<span class="iconfont icon-xxx"></span>
177 | 
178 |
179 |

" 180 | iconfont" 是你项目下的 font-family。可以通过编辑项目查看,默认是 "iconfont"。

181 |
182 |
183 |
184 |
185 |
    186 | 187 |
  • 188 | 191 |
    全屏
    192 |
    #icon-quanping
    193 |
  • 194 | 195 |
  • 196 | 199 |
    播放
    200 |
    #icon-bofang
    201 |
  • 202 | 203 |
  • 204 | 207 |
    暂停
    208 |
    #icon-zanting
    209 |
  • 210 | 211 |
  • 212 | 215 |
    声音
    216 |
    #icon-shengyin
    217 |
  • 218 | 219 |
  • 220 | 223 |
    声音关闭
    224 |
    #icon-shengyinguanbi
    225 |
  • 226 | 227 |
228 |
229 |

Symbol 引用

230 |
231 | 232 |

这是一种全新的使用方式,应该说这才是未来的主流,也是平台目前推荐的用法。相关介绍可以参考这篇文章 233 | 这种用法其实是做了一个 SVG 的集合,与另外两种相比具有如下特点:

234 |
    235 |
  • 支持多色图标了,不再受单色限制。
  • 236 |
  • 通过一些技巧,支持像字体那样,通过 font-size, color 来调整样式。
  • 237 |
  • 兼容性较差,支持 IE9+,及现代浏览器。
  • 238 |
  • 浏览器渲染 SVG 的性能一般,还不如 png。
  • 239 |
240 |

使用步骤如下:

241 |

第一步:引入项目下面生成的 symbol 代码:

242 |
<script src="./iconfont.js"></script>
243 | 
244 |

第二步:加入通用 CSS 代码(引入一次就行):

245 |
<style>
246 | .icon {
247 |   width: 1em;
248 |   height: 1em;
249 |   vertical-align: -0.15em;
250 |   fill: currentColor;
251 |   overflow: hidden;
252 | }
253 | </style>
254 | 
255 |

第三步:挑选相应图标并获取类名,应用于页面:

256 |
<svg class="icon" aria-hidden="true">
257 |   <use xlink:href="#icon-xxx"></use>
258 | </svg>
259 | 
260 |
261 |
262 | 263 |
264 |
265 | 284 | 285 | 286 | -------------------------------------------------------------------------------- /video-player/src/static/fonts/demo.css: -------------------------------------------------------------------------------- 1 | /* Logo 字体 */ 2 | @font-face { 3 | font-family: "iconfont logo"; 4 | src: url('https://at.alicdn.com/t/font_985780_km7mi63cihi.eot?t=1545807318834'); 5 | src: url('https://at.alicdn.com/t/font_985780_km7mi63cihi.eot?t=1545807318834#iefix') format('embedded-opentype'), 6 | url('https://at.alicdn.com/t/font_985780_km7mi63cihi.woff?t=1545807318834') format('woff'), 7 | url('https://at.alicdn.com/t/font_985780_km7mi63cihi.ttf?t=1545807318834') format('truetype'), 8 | url('https://at.alicdn.com/t/font_985780_km7mi63cihi.svg?t=1545807318834#iconfont') format('svg'); 9 | } 10 | 11 | .logo { 12 | font-family: "iconfont logo"; 13 | font-size: 160px; 14 | font-style: normal; 15 | -webkit-font-smoothing: antialiased; 16 | -moz-osx-font-smoothing: grayscale; 17 | } 18 | 19 | /* tabs */ 20 | .nav-tabs { 21 | position: relative; 22 | } 23 | 24 | .nav-tabs .nav-more { 25 | position: absolute; 26 | right: 0; 27 | bottom: 0; 28 | height: 42px; 29 | line-height: 42px; 30 | color: #666; 31 | } 32 | 33 | #tabs { 34 | border-bottom: 1px solid #eee; 35 | } 36 | 37 | #tabs li { 38 | cursor: pointer; 39 | width: 100px; 40 | height: 40px; 41 | line-height: 40px; 42 | text-align: center; 43 | font-size: 16px; 44 | border-bottom: 2px solid transparent; 45 | position: relative; 46 | z-index: 1; 47 | margin-bottom: -1px; 48 | color: #666; 49 | } 50 | 51 | 52 | #tabs .active { 53 | border-bottom-color: #f00; 54 | color: #222; 55 | } 56 | 57 | .tab-container .content { 58 | display: none; 59 | } 60 | 61 | /* 页面布局 */ 62 | .main { 63 | padding: 30px 100px; 64 | width: 960px; 65 | margin: 0 auto; 66 | } 67 | 68 | .main .logo { 69 | color: #333; 70 | text-align: left; 71 | margin-bottom: 30px; 72 | line-height: 1; 73 | height: 110px; 74 | margin-top: -50px; 75 | overflow: hidden; 76 | *zoom: 1; 77 | } 78 | 79 | .main .logo a { 80 | font-size: 160px; 81 | color: #333; 82 | } 83 | 84 | .helps { 85 | margin-top: 40px; 86 | } 87 | 88 | .helps pre { 89 | padding: 20px; 90 | margin: 10px 0; 91 | border: solid 1px #e7e1cd; 92 | background-color: #fffdef; 93 | overflow: auto; 94 | } 95 | 96 | .icon_lists { 97 | width: 100% !important; 98 | overflow: hidden; 99 | *zoom: 1; 100 | } 101 | 102 | .icon_lists li { 103 | width: 100px; 104 | margin-bottom: 10px; 105 | margin-right: 20px; 106 | text-align: center; 107 | list-style: none !important; 108 | cursor: default; 109 | } 110 | 111 | .icon_lists li .code-name { 112 | line-height: 1.2; 113 | } 114 | 115 | .icon_lists .icon { 116 | display: block; 117 | height: 100px; 118 | line-height: 100px; 119 | font-size: 42px; 120 | margin: 10px auto; 121 | color: #333; 122 | -webkit-transition: font-size 0.25s linear, width 0.25s linear; 123 | -moz-transition: font-size 0.25s linear, width 0.25s linear; 124 | transition: font-size 0.25s linear, width 0.25s linear; 125 | } 126 | 127 | .icon_lists .icon:hover { 128 | font-size: 100px; 129 | } 130 | 131 | .icon_lists .svg-icon { 132 | /* 通过设置 font-size 来改变图标大小 */ 133 | width: 1em; 134 | /* 图标和文字相邻时,垂直对齐 */ 135 | vertical-align: -0.15em; 136 | /* 通过设置 color 来改变 SVG 的颜色/fill */ 137 | fill: currentColor; 138 | /* path 和 stroke 溢出 viewBox 部分在 IE 下会显示 139 | normalize.css 中也包含这行 */ 140 | overflow: hidden; 141 | } 142 | 143 | .icon_lists li .name, 144 | .icon_lists li .code-name { 145 | color: #666; 146 | } 147 | 148 | /* markdown 样式 */ 149 | .markdown { 150 | color: #666; 151 | font-size: 14px; 152 | line-height: 1.8; 153 | } 154 | 155 | .highlight { 156 | line-height: 1.5; 157 | } 158 | 159 | .markdown img { 160 | vertical-align: middle; 161 | max-width: 100%; 162 | } 163 | 164 | .markdown h1 { 165 | color: #404040; 166 | font-weight: 500; 167 | line-height: 40px; 168 | margin-bottom: 24px; 169 | } 170 | 171 | .markdown h2, 172 | .markdown h3, 173 | .markdown h4, 174 | .markdown h5, 175 | .markdown h6 { 176 | color: #404040; 177 | margin: 1.6em 0 0.6em 0; 178 | font-weight: 500; 179 | clear: both; 180 | } 181 | 182 | .markdown h1 { 183 | font-size: 28px; 184 | } 185 | 186 | .markdown h2 { 187 | font-size: 22px; 188 | } 189 | 190 | .markdown h3 { 191 | font-size: 16px; 192 | } 193 | 194 | .markdown h4 { 195 | font-size: 14px; 196 | } 197 | 198 | .markdown h5 { 199 | font-size: 12px; 200 | } 201 | 202 | .markdown h6 { 203 | font-size: 12px; 204 | } 205 | 206 | .markdown hr { 207 | height: 1px; 208 | border: 0; 209 | background: #e9e9e9; 210 | margin: 16px 0; 211 | clear: both; 212 | } 213 | 214 | .markdown p { 215 | margin: 1em 0; 216 | } 217 | 218 | .markdown>p, 219 | .markdown>blockquote, 220 | .markdown>.highlight, 221 | .markdown>ol, 222 | .markdown>ul { 223 | width: 80%; 224 | } 225 | 226 | .markdown ul>li { 227 | list-style: circle; 228 | } 229 | 230 | .markdown>ul li, 231 | .markdown blockquote ul>li { 232 | margin-left: 20px; 233 | padding-left: 4px; 234 | } 235 | 236 | .markdown>ul li p, 237 | .markdown>ol li p { 238 | margin: 0.6em 0; 239 | } 240 | 241 | .markdown ol>li { 242 | list-style: decimal; 243 | } 244 | 245 | .markdown>ol li, 246 | .markdown blockquote ol>li { 247 | margin-left: 20px; 248 | padding-left: 4px; 249 | } 250 | 251 | .markdown code { 252 | margin: 0 3px; 253 | padding: 0 5px; 254 | background: #eee; 255 | border-radius: 3px; 256 | } 257 | 258 | .markdown strong, 259 | .markdown b { 260 | font-weight: 600; 261 | } 262 | 263 | .markdown>table { 264 | border-collapse: collapse; 265 | border-spacing: 0px; 266 | empty-cells: show; 267 | border: 1px solid #e9e9e9; 268 | width: 95%; 269 | margin-bottom: 24px; 270 | } 271 | 272 | .markdown>table th { 273 | white-space: nowrap; 274 | color: #333; 275 | font-weight: 600; 276 | } 277 | 278 | .markdown>table th, 279 | .markdown>table td { 280 | border: 1px solid #e9e9e9; 281 | padding: 8px 16px; 282 | text-align: left; 283 | } 284 | 285 | .markdown>table th { 286 | background: #F7F7F7; 287 | } 288 | 289 | .markdown blockquote { 290 | font-size: 90%; 291 | color: #999; 292 | border-left: 4px solid #e9e9e9; 293 | padding-left: 0.8em; 294 | margin: 1em 0; 295 | } 296 | 297 | .markdown blockquote p { 298 | margin: 0; 299 | } 300 | 301 | .markdown .anchor { 302 | opacity: 0; 303 | transition: opacity 0.3s ease; 304 | margin-left: 8px; 305 | } 306 | 307 | .markdown .waiting { 308 | color: #ccc; 309 | } 310 | 311 | .markdown h1:hover .anchor, 312 | .markdown h2:hover .anchor, 313 | .markdown h3:hover .anchor, 314 | .markdown h4:hover .anchor, 315 | .markdown h5:hover .anchor, 316 | .markdown h6:hover .anchor { 317 | opacity: 1; 318 | display: inline-block; 319 | } 320 | 321 | .markdown>br, 322 | .markdown>p>br { 323 | clear: both; 324 | } 325 | 326 | 327 | .hljs { 328 | display: block; 329 | background: white; 330 | padding: 0.5em; 331 | color: #333333; 332 | overflow-x: auto; 333 | } 334 | 335 | .hljs-comment, 336 | .hljs-meta { 337 | color: #969896; 338 | } 339 | 340 | .hljs-string, 341 | .hljs-variable, 342 | .hljs-template-variable, 343 | .hljs-strong, 344 | .hljs-emphasis, 345 | .hljs-quote { 346 | color: #df5000; 347 | } 348 | 349 | .hljs-keyword, 350 | .hljs-selector-tag, 351 | .hljs-type { 352 | color: #a71d5d; 353 | } 354 | 355 | .hljs-literal, 356 | .hljs-symbol, 357 | .hljs-bullet, 358 | .hljs-attribute { 359 | color: #0086b3; 360 | } 361 | 362 | .hljs-section, 363 | .hljs-name { 364 | color: #63a35c; 365 | } 366 | 367 | .hljs-tag { 368 | color: #333333; 369 | } 370 | 371 | .hljs-title, 372 | .hljs-attr, 373 | .hljs-selector-id, 374 | .hljs-selector-class, 375 | .hljs-selector-attr, 376 | .hljs-selector-pseudo { 377 | color: #795da3; 378 | } 379 | 380 | .hljs-addition { 381 | color: #55a532; 382 | background-color: #eaffea; 383 | } 384 | 385 | .hljs-deletion { 386 | color: #bd2c00; 387 | background-color: #ffecec; 388 | } 389 | 390 | .hljs-link { 391 | text-decoration: underline; 392 | } 393 | 394 | /* 代码高亮 */ 395 | /* PrismJS 1.15.0 396 | https://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascript */ 397 | /** 398 | * prism.js default theme for JavaScript, CSS and HTML 399 | * Based on dabblet (http://dabblet.com) 400 | * @author Lea Verou 401 | */ 402 | code[class*="language-"], 403 | pre[class*="language-"] { 404 | color: black; 405 | background: none; 406 | text-shadow: 0 1px white; 407 | font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; 408 | text-align: left; 409 | white-space: pre; 410 | word-spacing: normal; 411 | word-break: normal; 412 | word-wrap: normal; 413 | line-height: 1.5; 414 | 415 | -moz-tab-size: 4; 416 | -o-tab-size: 4; 417 | tab-size: 4; 418 | 419 | -webkit-hyphens: none; 420 | -moz-hyphens: none; 421 | -ms-hyphens: none; 422 | hyphens: none; 423 | } 424 | 425 | pre[class*="language-"]::-moz-selection, 426 | pre[class*="language-"] ::-moz-selection, 427 | code[class*="language-"]::-moz-selection, 428 | code[class*="language-"] ::-moz-selection { 429 | text-shadow: none; 430 | background: #b3d4fc; 431 | } 432 | 433 | pre[class*="language-"]::selection, 434 | pre[class*="language-"] ::selection, 435 | code[class*="language-"]::selection, 436 | code[class*="language-"] ::selection { 437 | text-shadow: none; 438 | background: #b3d4fc; 439 | } 440 | 441 | @media print { 442 | 443 | code[class*="language-"], 444 | pre[class*="language-"] { 445 | text-shadow: none; 446 | } 447 | } 448 | 449 | /* Code blocks */ 450 | pre[class*="language-"] { 451 | padding: 1em; 452 | margin: .5em 0; 453 | overflow: auto; 454 | } 455 | 456 | :not(pre)>code[class*="language-"], 457 | pre[class*="language-"] { 458 | background: #f5f2f0; 459 | } 460 | 461 | /* Inline code */ 462 | :not(pre)>code[class*="language-"] { 463 | padding: .1em; 464 | border-radius: .3em; 465 | white-space: normal; 466 | } 467 | 468 | .token.comment, 469 | .token.prolog, 470 | .token.doctype, 471 | .token.cdata { 472 | color: slategray; 473 | } 474 | 475 | .token.punctuation { 476 | color: #999; 477 | } 478 | 479 | .namespace { 480 | opacity: .7; 481 | } 482 | 483 | .token.property, 484 | .token.tag, 485 | .token.boolean, 486 | .token.number, 487 | .token.constant, 488 | .token.symbol, 489 | .token.deleted { 490 | color: #905; 491 | } 492 | 493 | .token.selector, 494 | .token.attr-name, 495 | .token.string, 496 | .token.char, 497 | .token.builtin, 498 | .token.inserted { 499 | color: #690; 500 | } 501 | 502 | .token.operator, 503 | .token.entity, 504 | .token.url, 505 | .language-css .token.string, 506 | .style .token.string { 507 | color: #9a6e3a; 508 | background: hsla(0, 0%, 100%, .5); 509 | } 510 | 511 | .token.atrule, 512 | .token.attr-value, 513 | .token.keyword { 514 | color: #07a; 515 | } 516 | 517 | .token.function, 518 | .token.class-name { 519 | color: #DD4A68; 520 | } 521 | 522 | .token.regex, 523 | .token.important, 524 | .token.variable { 525 | color: #e90; 526 | } 527 | 528 | .token.important, 529 | .token.bold { 530 | font-weight: bold; 531 | } 532 | 533 | .token.italic { 534 | font-style: italic; 535 | } 536 | 537 | .token.entity { 538 | cursor: help; 539 | } 540 | -------------------------------------------------------------------------------- /min-program/miniprogram/components/line/line.wxss: -------------------------------------------------------------------------------- 1 | .line-container { 2 | width: 100%; 3 | height: 100%; 4 | background-image: url(data:image/jpeg;base64,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); 5 | background-size: contain; 6 | } -------------------------------------------------------------------------------- /video-player/README.md: -------------------------------------------------------------------------------- 1 | 工作中用到video标签做视频播放器,一开始用 video.js 插件代替,如果只用这个插件进行简单的播放视频,是不是有点浪费,而且这个插件用 webpack 打包后 vendor.js 会很大,所以本文实现一个基于HTML5标签video的自定义视频播放器。其中实现了播放暂停、进度拖拽、音量控制及全屏等功能 2 | ### 一、效果预览 3 | 4 | ![](https://user-gold-cdn.xitu.io/2019/10/23/16df636daef1377d?w=628&h=375&f=gif&s=4106556) 5 | 6 | ### 二、逻辑简介 7 | 1. dom元素和css样式编写; 8 | 2. 简单的播放和暂停; 9 | 3. 时间进度显示; 10 | 4. 控制栏视频进度条拖拽; 11 | 5. 声音进度条控制; 12 | 6. 全屏和退出全屏基础实现。 13 | 7. video自带属性和方法 14 | 15 | | 属性或者方法 | 解释 | 16 | | :------| :------ | 17 | | currentTime | 当前视频播放的时间,单位是s | 18 | | duration | 当前视频播放总时长,单位是s | 19 | | volume | 声音,最大值为1 | 20 | | paused | 暂停状态 | 21 | | ended | 结束状态 | 22 | | play() | 播放视频 | 23 | | pause() | 暂停视频播放 | 24 | | loadedmetadata() | 视频加载获取数据,这里获取duration | 25 | | timeupdate() | 视频变化事件,这里获取实时的currentTime | 26 | | ended() | 视频播放结束事件 | 27 | | volumechange() | 视频声音事件 | 28 | 29 | ### 三、步骤实现 30 | #### 1、静态页面编写 31 | 本实例采用vue脚手架,具体dom元素实现如下,并添加相关注释: 32 | ``` 33 |
34 | 35 | 42 | 43 | 44 | 45 |
46 | 47 |
48 |
52 | 56 | 60 |
61 |
62 | 63 |
64 | 65 |
66 |
67 | 68 | 69 |
70 |
71 |
72 | 73 |
74 | "00:00"/"00:00" 75 |
76 | 77 | 78 |
79 |
80 | ``` 81 | 与之对应css和完整的代码会附加在文章的结尾。后面相关逻辑会依据上面的dom结构进行事件绑定和拓展。 82 | #### 2、播放和暂停 83 | 播放或者暂停有两种场景,第一种通过点击“播放”或者“暂停”按钮控制播放或者暂停;第二种是点击视频区域控制播放或者暂停。 84 | 第一种实现通过点击“播放”按钮,此按钮的样式变成“暂停”,所以修改上面注释区域**播放或者暂停按钮**元素 85 | ``` 86 | 87 | 92 | 93 | 98 | 99 | ``` 100 | 并且对应的js如下: 101 | ``` 102 | data() { 103 | return { 104 | videoState: { 105 | play: false, //播放状态 106 | playState: false, // 记录播放状态 107 | }, 108 | videoDom: null, // video 109 | } 110 | }, 111 | mounted() { 112 | // 初始化相关元数据 113 | this.videoDom = this.$refs["custom-video"] 114 | }, 115 | methods: { 116 | play(flag) { // 播放按钮事件 117 | if(flag) this.videoState.playState = true 118 | this.videoState.play = true 119 | this.videoDom.play() 120 | }, 121 | pause(flag) { // 暂停按钮事件 122 | if(flag) this.videoState.playState = false 123 | this.videoDom.pause() 124 | this.videoState.play = false 125 | }, 126 | } 127 | ``` 128 | 点击“播放”按钮调用play()方法,videoState.play值变化引起按钮样式变成“暂停”,并调用video标签自带的播放方法,反之就是“暂停”。 129 | 第二种点击屏幕播放或者暂停是通过监听video click事件,代码如下: 130 | ``` 131 | mounted() { 132 | // 初始化相关元数据 133 | this.videoDom = this.$refs["custom-video"] 134 | this.initMedaData() 135 | }, 136 | methods: { 137 | initMedaData() { // 初始化video相关事件 138 | this.videoDom.addEventListener("click", () => { // 点击视频区域可以进行播放或者暂停 139 | if(this.videoDom.paused || this.videoDom.ended) { 140 | if(this.videoDom.ended) { 141 | this.videoDom.currentTime = 0 142 | } 143 | this.play('btn') //调用下面play的方法 144 | } else { 145 | this.pause('btn') //调用下面pause的方法 146 | } 147 | }) 148 | }, 149 | play(flag) { // 播放按钮事件 150 | if(flag) this.videoState.playState = true 151 | this.videoState.play = true 152 | this.videoDom.play() 153 | }, 154 | pause(flag) { // 暂停按钮事件 155 | if(flag) this.videoState.playState = false 156 | this.videoDom.pause() 157 | this.videoState.play = false 158 | }, 159 | } 160 | ``` 161 | #### 3、时间显示 162 | 时间就是图片右下角“00:00 / 00:29”,它是当前的播放时间比上视频总时长,video自带属性currentTime和duration,这两个字段获取的值单位都是s,所以要进行格式转换,先修改对应的dom结构: 163 | ``` 164 | 165 |
166 | {{currentTime ? currentTime : "00:00"}} 167 | / 168 | {{duration ? duration : "00:00"}} 169 |
170 | ``` 171 | 相关js代码如下: 172 | ``` 173 | data() { 174 | return { 175 | duration: 0, // 视频总时长 176 | currentTime: 0, // 视频当前播放时长 177 | } 178 | }, 179 | mounted() { 180 | // 初始化相关元数据 181 | this.videoDom = this.$refs["custom-video"] 182 | this.initMedaData() 183 | }, 184 | methods: { 185 | initMedaData() { // 初始化video相关事件 186 | this.videoDom.addEventListener('loadedmetadata', () => { // 获取视频总时长 187 | this.duration = this.timeTranslate(this.videoDom.duration) 188 | }) 189 | }, 190 | this.videoDom.addEventListener("timeupdate", () => { // 监听视频播放过程中的时间 191 | this.currentTime = this.timeTranslate(this.videoDom.currentTime) 192 | }), 193 | timeTranslate(t) { // 时间转化 194 | let m = Math.floor(t / 60) 195 | m < 10 && (m = '0' + m) 196 | return m + ":" + (t % 60 / 100 ).toFixed(2).slice(-2) 197 | }, 198 | } 199 | ``` 200 | #### 4、播放进度显示 201 | 1. 播放进度随着播放慢慢边长,播放进度条由三部分组成: 202 | 203 | ![](https://user-gold-cdn.xitu.io/2019/10/23/16df67a4a82bf27f?w=673&h=120&f=png&s=59696) 204 | video自带的timeupdate的方法会实时的监听播放状态,通过实时的获取currentTime和duration的比值,这个比例值就是图中inside占整个outside的比重。代码如下: 205 | ``` 206 | data() { 207 | return { 208 | videoDom: null, // video 209 | videoProOut: null, // 视频总进度条 210 | videoPro: null, // 视频进度条 211 | videoPoi: null, // 视频进度点 212 | } 213 | }, 214 | mounted() { 215 | // 初始化相关元数据 216 | this.videoDom = this.$refs["custom-video"] 217 | this.videoProOut = this.$refs['custom-video_control-bg-outside'] 218 | this.videoPro = this.$refs['custom-video_control-bg-inside'] 219 | this.videoPoi = this.$refs['custom-video_control-bg-inside-point'] 220 | this.initMedaData() 221 | }, 222 | methods: { 223 | initMedaData() { // 初始化video相关事件 224 | this.videoDom.addEventListener('loadedmetadata', () => { // 获取视频总时长 225 | this.duration = this.timeTranslate(this.videoDom.duration) 226 | }) 227 | }, 228 | this.videoDom.addEventListener("timeupdate", () => { // 监听视频播放过程中的时间 229 | const percentage = 100 * this.videoDom.currentTime / this.videoDom.duration 230 | this.videoPro.style.width = percentage + '%' 231 | this.videoPoi.style.left = percentage - 1 + '%' 232 | }) 233 | } 234 | ``` 235 | 2. 播放进度可以点击、拖动,这两步操作会用到mousedown、mousemove、mouseup事件,对应三个方法handlePrograssDown、handlePrograssMove、handlePrograssUp,这三个方法挂载到dom节点如下: 236 | ``` 237 | 238 |
244 | 245 |
246 | ``` 247 | 拖拽或者点击进度条首先计算进度条的起点水平距离,所以需要计算该点的偏移量,封装偏移量getOffset方法如下(ps:貌似是zepto源码片段), 248 | ``` 249 | getOffset(node, offset) { // 获取当前屏幕下进度条的左偏移量和又偏移量 250 | if(!offset) { 251 | offset = {} 252 | offset.left = 0 253 | offset.top = 0 254 | } 255 | if(node === document.body || node === null) { 256 | return offset 257 | } 258 | offset.top += node.offsetTop 259 | offset.left += node.offsetLeft 260 | return this.getOffset(node.offsetParent, offset) 261 | }, 262 | ``` 263 | 点击逻辑如下: 264 | ``` 265 | handlePrograssDown(ev) { // 监听点击进度条事件,方便获取初始点击的位置 266 | // 视频暂停 267 | this.videoState.downState = true //按下鼠标标志 268 | this.pause() // 视频暂时停止 269 | this.videoState.distance = ev.clientX - this.videoState.leftInit //记录点击的离起点的距离 270 | 这里的leftInit就是通过getOffset方法获取的进度条起点偏移量 271 | }, 272 | ``` 273 | 松开鼠标,通过记录的距离算出当前的currentTime,然后从此点进行视频播放或者暂停,逻辑如下: 274 | ``` 275 | handlePrograssUp() { //松开鼠标,播放当前进度条视频 276 | this.videoState.downState = false 277 | // 计算点击此处的currentTime 278 | this.videoDom.currentTime = this.videoState.distance / this.processWidth * this.videoDom.duration 279 | // 页面回显的currentTime数据 280 | this.currentTime = this.timeTranslate(this.videoDom.currentTime) 281 | // 这个是判断当前视频是在播放状态进行点击还是在暂停状态进行点击的 282 | if(this.videoState.playState) { 283 | this.play() 284 | } 285 | }, 286 | ``` 287 | 上面的this.videoState.playState这个状态是通过按钮或者视频区域点击进行判定的,具体在上面play(flag)和pause(flag)方法中。 288 | 289 | 3. 拖拽方法如下: 290 | ``` 291 | handlePrograssMove(ev) { // 监听移动进度条事件,同步播放相关事件 292 | if(!this.videoState.downState) return //如果没有通过鼠标点击起点,则直接不进行下面计算 293 | let disX = ev.clientX - this.videoState.leftInit 294 | // 进行边界判断 295 | if(disX > this.processWidth) { 296 | disX = this.processWidth 297 | } 298 | if(disX < 0) { 299 | disX = 0 300 | } 301 | this.videoState.distance = disX 302 | // 计算当前的currentTime 303 | this.videoDom.currentTime = this.videoState.distance / this.processWidth * this.videoDom.duration 304 | }, 305 | ``` 306 | 播放的进度条重点是通过点击或者拖动的位置计算当前的视频的时间点,将此值赋予video标签,这里就是 307 | >this.videoDom.currentTime = 表达式 308 | 309 | 这样timeupdate方法会被触发: 310 | ``` 311 | this.videoDom.addEventListener("timeupdate", () => { // 监听视频播放过程中的时间 312 | const percentage = 100 * this.videoDom.currentTime / this.videoDom.duration 313 | 314 | this.videoPro.style.width = percentage + '%' 315 | 316 | this.videoPoi.style.left = percentage - 1 + '%' 317 | this.currentTime = this.timeTranslate(this.videoDom.currentTime) 318 | }) 319 | ``` 320 | #### 5、声音控制 321 | 声音与视频的进度条是类似的,只不过声音的进度条是计算竖直方向的,声音相关属性volume,值的范围 0 ~ 1,监听声音的方法是volumechange。声音的样式图片样式如下: 322 | 323 | ![](https://user-gold-cdn.xitu.io/2019/10/23/16df6a0750f3ab13?w=104&h=193&f=png&s=18415) 324 | 相关方法如下: 325 | ``` 326 | // 监听声音的方法,通过此方法进行进度条渲染 327 | this.videoDom.addEventListener("volumechange", () => { 328 | const percentage = this.videoDom.volume * 100 329 | this.voicePro.style.height = percentage + '%' 330 | this.voicePoi.style.bottom = percentage + '%' 331 | }) 332 | 333 | // 声音控制的三个方法 334 | handleVolPrograssDown(ev) { // 监听声音点击事件 335 | this.voiceState.topInit = this.getOffset(this.voiceProOut).top 336 | this.volProcessHeight = this.voiceProOut.clientHeight 337 | this.voiceState.downState = true //按下鼠标标志 338 | this.voiceState.distance = ev.clientY - this.voiceState.topInit 339 | }, 340 | handleVolPrograssMove(ev) { // 监听声音进度条移动事件 341 | if(!this.voiceState.downState) return 342 | let disY = this.voiceState.topInit + this.volProcessHeight - ev.clientY 343 | if(disY > this.volProcessHeight - 2) { 344 | disY = this.volProcessHeight - 2 345 | } 346 | if(disY < 0) { 347 | disY = 0 348 | } 349 | this.voiceState.distance = disY 350 | this.videoDom.volume = this.voiceState.distance / this.volProcessHeight 351 | this.videoOption.volume = Math.round(this.videoDom.volume * 100) 352 | }, 353 | handleVolPrograssUp() { // 监听声音鼠标离开事件 354 | this.voiceState.downState = false //按下鼠标标志 355 | this.videoDom.volume = this.voiceState.distance / this.volProcessHeight 356 | this.videoOption.volume = Math.round(this.videoDom.volume * 100) 357 | }, 358 | ``` 359 | #### 6、全屏和退出全屏控制 360 | 全屏方法,这里面进行了兼容处理: 361 | ``` 362 | fullScreen() { 363 | let ele = document.documentElement 364 | if (ele .requestFullscreen) { 365 | ele .requestFullscreen() 366 | } else if (ele .mozRequestFullScreen) { 367 | ele .mozRequestFullScreen() 368 | } else if (ele .webkitRequestFullScreen) { 369 | ele .webkitRequestFullScreen() 370 | } 371 | // 对应的video标签大小100% 372 | this.$refs['custom-video_container'].style.width = "100%" 373 | this.$refs['custom-video_container'].style.height = "100%" 374 | }, 375 | ``` 376 | 退出全屏方法: 377 | ``` 378 | exitFullscreen() { 379 | let de = document 380 | if (de.exitFullscreen) { 381 | de.exitFullscreen(); 382 | } else if (de.mozCancelFullScreen) { 383 | de.mozCancelFullScreen(); 384 | } else if (de.webkitCancelFullScreen) { 385 | de.webkitCancelFullScreen(); 386 | } 387 | // 返回初始化值 388 | this.$refs['custom-video_container'].style.width = "500px" 389 | this.$refs['custom-video_container'].style.height = "300px" 390 | } 391 | ``` 392 | 注意:这里进度条和退出全屏事件都没有对键盘对应健进行处理,只是单纯的鼠标点击事件 393 | #### 7、控制栏隐藏和展示 394 | 控制栏在暂停的时候显示,在播放的时候,只要鼠标在视频播放器中,也会显示,离开后几秒后消失,这里用到vue过渡动画: 395 | ``` 396 | 397 | name="fade" 398 | > 399 |
403 | 404 |
405 |
406 | ``` 407 | 对应的css文件 408 | ``` 409 | /* 控制栏隐藏动画 */ 410 | .fade-enter-active { 411 | transition: all .3s ease; 412 | } 413 | .fade-leave-active { 414 | transition: all .8s cubic-bezier(1.0, 0.5, 0.8, 1.0); 415 | } 416 | .fade-enter, .fade-leave-to { 417 | transform: translateY(50px); 418 | opacity: 0; 419 | } 420 | ``` 421 | 控制栏的消失或者展示是通过绑定最外层dom元素的mouseover、mouseleave事件进行逻辑控制, 422 | 这里用mouseleave,而不是用mouseout,如果使用mouseout事件,在经过控制栏时会出现闪烁。具体事件源码如下: 423 | ``` 424 | handleControls(ev, flag) { // 监听离开或者进入视频区域隐藏或者展示控制栏 425 | switch (flag) { 426 | case 'start': 427 | this.videoState.hideControl = false 428 | break; 429 | case 'end': 430 | this.videoState.hideControl = true 431 | break; 432 | default: 433 | break; 434 | } 435 | }, 436 | ``` 437 | 通过控制this.videoState.hideControl状态显示隐藏活隐藏控制栏。 438 | ### 四、源码 439 | 源码地址:[vue-player](https://github.com/yuelinghunyu/blog-demo/tree/master/video-player) 440 | -------------------------------------------------------------------------------- /web-cache/cache-node/app/public/cache/static/js/app.86a4b0ce.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["webpack:///webpack/bootstrap","webpack:///./src/assets/cache-img.png","webpack:///./src/App.vue?6ec8","webpack:///./src/App.vue?cdbc","webpack:///src/App.vue","webpack:///./src/App.vue?1160","webpack:///./src/App.vue?bff9","webpack:///./src/main.js","webpack:///./src/assets/loading.gif"],"names":["webpackJsonpCallback","data","moduleId","chunkId","chunkIds","moreModules","executeModules","i","resolves","length","Object","prototype","hasOwnProperty","call","installedChunks","push","modules","parentJsonpFunction","shift","deferredModules","apply","checkDeferredModules","result","deferredModule","fulfilled","j","depId","splice","__webpack_require__","s","installedModules","exports","module","l","m","c","d","name","getter","o","defineProperty","enumerable","get","r","Symbol","toStringTag","value","t","mode","__esModule","ns","create","key","bind","n","object","property","p","jsonpArray","window","oldJsonpFunction","slice","_vm","this","_h","$createElement","_c","_self","attrs","logo","_v","loading","_s","text","staticRenderFns","a","headers","isLoading","created","getServerContent","methods","component","Vue","config","productionTip","render","h","App","$mount"],"mappings":"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","file":"static/js/app.86a4b0ce.js","sourcesContent":[" \t// install a JSONP callback for chunk loading\n \tfunction webpackJsonpCallback(data) {\n \t\tvar chunkIds = data[0];\n \t\tvar moreModules = data[1];\n \t\tvar executeModules = data[2];\n\n \t\t// add \"moreModules\" to the modules object,\n \t\t// then flag all \"chunkIds\" as loaded and fire callback\n \t\tvar moduleId, chunkId, i = 0, resolves = [];\n \t\tfor(;i < chunkIds.length; i++) {\n \t\t\tchunkId = chunkIds[i];\n \t\t\tif(Object.prototype.hasOwnProperty.call(installedChunks, chunkId) && installedChunks[chunkId]) {\n \t\t\t\tresolves.push(installedChunks[chunkId][0]);\n \t\t\t}\n \t\t\tinstalledChunks[chunkId] = 0;\n \t\t}\n \t\tfor(moduleId in moreModules) {\n \t\t\tif(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {\n \t\t\t\tmodules[moduleId] = moreModules[moduleId];\n \t\t\t}\n \t\t}\n \t\tif(parentJsonpFunction) parentJsonpFunction(data);\n\n \t\twhile(resolves.length) {\n \t\t\tresolves.shift()();\n \t\t}\n\n \t\t// add entry modules from loaded chunk to deferred list\n \t\tdeferredModules.push.apply(deferredModules, executeModules || []);\n\n \t\t// run deferred modules when all chunks ready\n \t\treturn checkDeferredModules();\n \t};\n \tfunction checkDeferredModules() {\n \t\tvar result;\n \t\tfor(var i = 0; i < deferredModules.length; i++) {\n \t\t\tvar deferredModule = deferredModules[i];\n \t\t\tvar fulfilled = true;\n \t\t\tfor(var j = 1; j < deferredModule.length; j++) {\n \t\t\t\tvar depId = deferredModule[j];\n \t\t\t\tif(installedChunks[depId] !== 0) fulfilled = false;\n \t\t\t}\n \t\t\tif(fulfilled) {\n \t\t\t\tdeferredModules.splice(i--, 1);\n \t\t\t\tresult = __webpack_require__(__webpack_require__.s = deferredModule[0]);\n \t\t\t}\n \t\t}\n\n \t\treturn result;\n \t}\n\n \t// The module cache\n \tvar installedModules = {};\n\n \t// object to store loaded and loading chunks\n \t// undefined = chunk not loaded, null = chunk preloaded/prefetched\n \t// Promise = chunk loading, 0 = chunk loaded\n \tvar installedChunks = {\n \t\t\"app\": 0\n \t};\n\n \tvar deferredModules = [];\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId]) {\n \t\t\treturn installedModules[moduleId].exports;\n \t\t}\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\ti: moduleId,\n \t\t\tl: false,\n \t\t\texports: {}\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.l = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// define getter function for harmony exports\n \t__webpack_require__.d = function(exports, name, getter) {\n \t\tif(!__webpack_require__.o(exports, name)) {\n \t\t\tObject.defineProperty(exports, name, { enumerable: true, get: getter });\n \t\t}\n \t};\n\n \t// define __esModule on exports\n \t__webpack_require__.r = function(exports) {\n \t\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n \t\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n \t\t}\n \t\tObject.defineProperty(exports, '__esModule', { value: true });\n \t};\n\n \t// create a fake namespace object\n \t// mode & 1: value is a module id, require it\n \t// mode & 2: merge all properties of value into the ns\n \t// mode & 4: return value when already ns object\n \t// mode & 8|1: behave like require\n \t__webpack_require__.t = function(value, mode) {\n \t\tif(mode & 1) value = __webpack_require__(value);\n \t\tif(mode & 8) return value;\n \t\tif((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;\n \t\tvar ns = Object.create(null);\n \t\t__webpack_require__.r(ns);\n \t\tObject.defineProperty(ns, 'default', { enumerable: true, value: value });\n \t\tif(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));\n \t\treturn ns;\n \t};\n\n \t// getDefaultExport function for compatibility with non-harmony modules\n \t__webpack_require__.n = function(module) {\n \t\tvar getter = module && module.__esModule ?\n \t\t\tfunction getDefault() { return module['default']; } :\n \t\t\tfunction getModuleExports() { return module; };\n \t\t__webpack_require__.d(getter, 'a', getter);\n \t\treturn getter;\n \t};\n\n \t// Object.prototype.hasOwnProperty.call\n \t__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"/cache/\";\n\n \tvar jsonpArray = window[\"webpackJsonp\"] = window[\"webpackJsonp\"] || [];\n \tvar oldJsonpFunction = jsonpArray.push.bind(jsonpArray);\n \tjsonpArray.push = webpackJsonpCallback;\n \tjsonpArray = jsonpArray.slice();\n \tfor(var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);\n \tvar parentJsonpFunction = oldJsonpFunction;\n\n\n \t// add entry module to deferred list\n \tdeferredModules.push([0,\"chunk-vendors\"]);\n \t// run deferred modules when ready\n \treturn checkDeferredModules();\n","module.exports = __webpack_public_path__ + \"static/img/cache-img.6efc6c0d.png\";","import mod from \"-!../node_modules/mini-css-extract-plugin/dist/loader.js??ref--6-oneOf-1-0!../node_modules/css-loader/dist/cjs.js??ref--6-oneOf-1-1!../node_modules/vue-loader/lib/loaders/stylePostLoader.js!../node_modules/postcss-loader/src/index.js??ref--6-oneOf-1-2!../node_modules/cache-loader/dist/cjs.js??ref--0-0!../node_modules/vue-loader/lib/index.js??vue-loader-options!./App.vue?vue&type=style&index=0&id=7e314f6e&scoped=true&lang=css&\"; export default mod; export * from \"-!../node_modules/mini-css-extract-plugin/dist/loader.js??ref--6-oneOf-1-0!../node_modules/css-loader/dist/cjs.js??ref--6-oneOf-1-1!../node_modules/vue-loader/lib/loaders/stylePostLoader.js!../node_modules/postcss-loader/src/index.js??ref--6-oneOf-1-2!../node_modules/cache-loader/dist/cjs.js??ref--0-0!../node_modules/vue-loader/lib/index.js??vue-loader-options!./App.vue?vue&type=style&index=0&id=7e314f6e&scoped=true&lang=css&\"","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{attrs:{\"id\":\"app\"}},[_c('img',{attrs:{\"alt\":\"Vue logo\",\"src\":_vm.logo}}),_c('h3',[_vm._v(\"前端请求后端服务器数据\")]),_c('p',[(_vm.isLoading)?_c('img',{attrs:{\"src\":_vm.loading}}):_c('span',[_vm._v(_vm._s(_vm.text))])])])}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","\n\n\n\n\n","import mod from \"-!../node_modules/cache-loader/dist/cjs.js??ref--12-0!../node_modules/thread-loader/dist/cjs.js!../node_modules/babel-loader/lib/index.js!../node_modules/cache-loader/dist/cjs.js??ref--0-0!../node_modules/vue-loader/lib/index.js??vue-loader-options!./App.vue?vue&type=script&lang=js&\"; export default mod; export * from \"-!../node_modules/cache-loader/dist/cjs.js??ref--12-0!../node_modules/thread-loader/dist/cjs.js!../node_modules/babel-loader/lib/index.js!../node_modules/cache-loader/dist/cjs.js??ref--0-0!../node_modules/vue-loader/lib/index.js??vue-loader-options!./App.vue?vue&type=script&lang=js&\"","import { render, staticRenderFns } from \"./App.vue?vue&type=template&id=7e314f6e&scoped=true&\"\nimport script from \"./App.vue?vue&type=script&lang=js&\"\nexport * from \"./App.vue?vue&type=script&lang=js&\"\nimport style0 from \"./App.vue?vue&type=style&index=0&id=7e314f6e&scoped=true&lang=css&\"\n\n\n/* normalize component */\nimport normalizer from \"!../node_modules/vue-loader/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n \"7e314f6e\",\n null\n \n)\n\nexport default component.exports","import Vue from 'vue'\nimport App from './App.vue'\nimport \"@/assets/reset.css\"\n\nVue.config.productionTip = false\n\nnew Vue({\n render: h => h(App),\n}).$mount('#app')\n","module.exports = \"data:image/gif;base64,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\""],"sourceRoot":""} --------------------------------------------------------------------------------