├── demo └── demo.gif ├── public ├── favicon.ico └── index.html ├── src ├── assets │ └── logo.png ├── store │ └── index.js ├── router │ └── index.js ├── main.js ├── components │ ├── tag-linkage-time │ │ └── index.vue │ ├── tag-linkage-addr │ │ └── index.vue │ ├── tag-linkage-date │ │ └── index.vue │ └── tag-linkage-base │ │ ├── js │ │ └── MTween.js │ │ └── index.vue └── App.vue ├── babel.config.js ├── .editorconfig ├── .gitignore ├── README.md └── package.json /demo/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chilliness/vue-linkage/HEAD/demo/demo.gif -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chilliness/vue-linkage/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chilliness/vue-linkage/HEAD/src/assets/logo.png -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{js,jsx,ts,tsx,vue}] 2 | indent_style = space 3 | indent_size = 2 4 | trim_trailing_whitespace = true 5 | insert_final_newline = true 6 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import { createStore } from "vuex"; 2 | 3 | export default createStore({ 4 | state: {}, 5 | mutations: {}, 6 | actions: {}, 7 | modules: {}, 8 | }); 9 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHashHistory } from "vue-router"; 2 | 3 | const routes = []; 4 | 5 | const router = createRouter({ 6 | history: createWebHashHistory(), 7 | routes, 8 | }); 9 | 10 | export default router; 11 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from "vue"; 2 | import App from "./App.vue"; 3 | import TagLinkageBase from "@/components/tag-linkage-base"; 4 | 5 | let app = createApp(App); 6 | 7 | app.component("tag-linkage-base", TagLinkageBase); 8 | 9 | app.mount("#app"); 10 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 多级联动 14 | 15 | 16 | 19 |
20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 多级联动 2 | 3 | > 版本 4 | 5 | |链接|链接|链接| 6 | |:------:|:------:|:------:| 7 | |[vue版](https://github.com/chilliness/vue-linkage)|[angular版](https://github.com/chilliness/ngx-linkage)|[react版](https://github.com/chilliness/react-linkage)| 8 | 9 | > 亮点 10 | 11 | ``` bash 12 | 1、原生JavaScript实现,无须引入第三方 13 | 14 | 2、支持初始化数据,满足初始化需求 15 | 16 | 3、支持自定义数据,想要几列,由你做主 17 | 18 | 4、解决苹果手机橡皮筋效果带来的负面影响 19 | 20 | 5、已实现『时间、日期、省市区』联动组件,方便直接使用和个性化定制参考 21 | 22 | 6、提供『初始化、取消、确定、滚动结束』事件回调,并返回带有字段说明的对象 23 | 24 | 7、其他亮点,不再一一罗列,请自行发掘 25 | ``` 26 | 27 | > 效果 28 | 29 | ![最终效果](/demo/demo.gif) 30 | 31 | > 注意事项 32 | 33 | ``` bash 34 | 1、CSS代码使用Scss编写 35 | 36 | 2、本组件是针对移动端开发的,但PC端亦可使用 37 | 38 | 3、所有数据和组件均在src/components文件夹下,以文件夹划分 39 | 40 | 4、基础组件为tag-linkage-base,可根据此组件个性化定制符合需求的组件 41 | 42 | 5、如需使用本项目提供的『时间、日期、省市区』联动组件,请参考本项目代码 43 | 44 | 6、个性化定制时,请在『事件回调内容flag为true』时再做处理,不包含初始化事件 45 | 46 | 7、初始化事件回调『只发生在有初始化且初始化成功的前提下』,并返回带有字段说明的对象 47 | 48 | 8、本组件列数是根据list传入的二维数组长度决定的,请务必保证『二维数组list的合法性』 49 | 50 | 9、由于『显示内容和获取结果』是取子项的val属性,请务必保证『二维数组list每一项的子项为带有val属性的对象』 51 | ``` 52 | 53 | ## Project setup 54 | ``` 55 | npm install 56 | ``` 57 | 58 | ### Compiles and hot-reloads for development 59 | ``` 60 | npm run serve 61 | ``` 62 | 63 | ### Compiles and minifies for production 64 | ``` 65 | npm run build 66 | ``` 67 | 68 | ### Lints and fixes files 69 | ``` 70 | npm run lint 71 | ``` 72 | -------------------------------------------------------------------------------- /src/components/tag-linkage-time/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 58 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-linkage", 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": "^3.0.0", 13 | "vue-router": "^4.0.0-0", 14 | "vuex": "^4.0.0-0" 15 | }, 16 | "devDependencies": { 17 | "@vue/cli-plugin-babel": "~4.5.11", 18 | "@vue/cli-plugin-eslint": "~4.5.11", 19 | "@vue/cli-plugin-router": "~4.5.11", 20 | "@vue/cli-plugin-vuex": "~4.5.11", 21 | "@vue/cli-service": "~4.5.11", 22 | "@vue/compiler-sfc": "^3.0.0", 23 | "@vue/eslint-config-standard": "^5.1.2", 24 | "babel-eslint": "^10.1.0", 25 | "eslint": "^6.7.2", 26 | "eslint-plugin-import": "^2.20.2", 27 | "eslint-plugin-node": "^11.1.0", 28 | "eslint-plugin-promise": "^4.2.1", 29 | "eslint-plugin-standard": "^4.0.0", 30 | "eslint-plugin-vue": "^7.0.0", 31 | "sass": "^1.26.5", 32 | "sass-loader": "^8.0.2" 33 | }, 34 | "eslintConfig": { 35 | "root": true, 36 | "env": { 37 | "node": true 38 | }, 39 | "extends": [ 40 | "plugin:vue/vue3-essential", 41 | "@vue/standard" 42 | ], 43 | "parserOptions": { 44 | "parser": "babel-eslint" 45 | }, 46 | "rules": { 47 | "dot-notation": 0, 48 | "prefer-const": 0, 49 | "comma-dangle": 0, 50 | "vue/no-setup-props-destructure": 0, 51 | "semi": [ 52 | "error", 53 | "always" 54 | ], 55 | "quotes": [ 56 | "error", 57 | "double" 58 | ], 59 | "space-before-function-paren": [ 60 | "error", 61 | "never" 62 | ] 63 | } 64 | }, 65 | "browserslist": [ 66 | "> 1%", 67 | "last 2 versions", 68 | "not dead" 69 | ] 70 | } 71 | -------------------------------------------------------------------------------- /src/components/tag-linkage-addr/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 85 | -------------------------------------------------------------------------------- /src/components/tag-linkage-date/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 95 | -------------------------------------------------------------------------------- /src/components/tag-linkage-base/js/MTween.js: -------------------------------------------------------------------------------- 1 | let Tween = { 2 | linear(t, b, c, d) { 3 | return (c * t) / d + b; 4 | }, 5 | easeIn(t, b, c, d) { 6 | return c * (t /= d) * t + b; 7 | }, 8 | easeOut(t, b, c, d) { 9 | return -c * (t /= d) * (t - 2) + b; 10 | }, 11 | easeBoth(t, b, c, d) { 12 | if ((t /= d / 2) < 1) { 13 | return (c / 2) * t * t + b; 14 | } 15 | return (-c / 2) * (--t * (t - 2) - 1) + b; 16 | }, 17 | easeInStrong(t, b, c, d) { 18 | return c * (t /= d) * t * t * t + b; 19 | }, 20 | easeOutStrong(t, b, c, d) { 21 | return -c * ((t = t / d - 1) * t * t * t - 1) + b; 22 | }, 23 | easeBothStrong(t, b, c, d) { 24 | if ((t /= d / 2) < 1) { 25 | return (c / 2) * t * t * t * t + b; 26 | } 27 | return (-c / 2) * ((t -= 2) * t * t * t - 2) + b; 28 | }, 29 | elasticIn(t, b, c, d, a, p) { 30 | let s; 31 | if (t === 0) { 32 | return b; 33 | } 34 | if ((t /= d) === 1) { 35 | return b + c; 36 | } 37 | if (!p) { 38 | p = d * 0.3; 39 | } 40 | if (!a || a < Math.abs(c)) { 41 | a = c; 42 | s = p / 4; 43 | } else { 44 | s = (p / (2 * Math.PI)) * Math.asin(c / a); 45 | } 46 | return ( 47 | -( 48 | a * 49 | Math.pow(2, 10 * (t -= 1)) * 50 | Math.sin(((t * d - s) * (2 * Math.PI)) / p) 51 | ) + b 52 | ); 53 | }, 54 | elasticOut(t, b, c, d, a, p) { 55 | let s; 56 | if (t === 0) { 57 | return b; 58 | } 59 | if ((t /= d) === 1) { 60 | return b + c; 61 | } 62 | if (!p) { 63 | p = d * 0.3; 64 | } 65 | if (!a || a < Math.abs(c)) { 66 | a = c; 67 | s = p / 4; 68 | } else { 69 | s = (p / (2 * Math.PI)) * Math.asin(c / a); 70 | } 71 | return ( 72 | a * Math.pow(2, -10 * t) * Math.sin(((t * d - s) * (2 * Math.PI)) / p) + 73 | c + 74 | b 75 | ); 76 | }, 77 | elasticBoth(t, b, c, d, a, p) { 78 | let s; 79 | if (t === 0) { 80 | return b; 81 | } 82 | if ((t /= d / 2) === 2) { 83 | return b + c; 84 | } 85 | if (!p) { 86 | p = d * (0.3 * 1.5); 87 | } 88 | if (!a || a < Math.abs(c)) { 89 | a = c; 90 | s = p / 4; 91 | } else { 92 | s = (p / (2 * Math.PI)) * Math.asin(c / a); 93 | } 94 | if (t < 1) { 95 | return ( 96 | -0.5 * 97 | (a * 98 | Math.pow(2, 10 * (t -= 1)) * 99 | Math.sin(((t * d - s) * (2 * Math.PI)) / p)) + 100 | b 101 | ); 102 | } 103 | return ( 104 | a * 105 | Math.pow(2, -10 * (t -= 1)) * 106 | Math.sin(((t * d - s) * (2 * Math.PI)) / p) * 107 | 0.5 + 108 | c + 109 | b 110 | ); 111 | }, 112 | backIn(t, b, c, d, s) { 113 | if (typeof s === "undefined") { 114 | s = 1.70158; 115 | } 116 | return c * (t /= d) * t * ((s + 1) * t - s) + b; 117 | }, 118 | backOut(t, b, c, d, s) { 119 | if (typeof s === "undefined") { 120 | s = 2.70158; // 回缩的距离 121 | } 122 | return c * ((t = t / d - 1) * t * ((s + 1) * t + s) + 1) + b; 123 | }, 124 | backBoth(t, b, c, d, s) { 125 | if (typeof s === "undefined") { 126 | s = 1.70158; 127 | } 128 | if ((t /= d / 2) < 1) { 129 | return (c / 2) * (t * t * (((s *= 1.525) + 1) * t - s)) + b; 130 | } 131 | return (c / 2) * ((t -= 2) * t * (((s *= 1.525) + 1) * t + s) + 2) + b; 132 | }, 133 | bounceIn(t, b, c, d) { 134 | return c - Tween["bounceOut"](d - t, 0, c, d) + b; 135 | }, 136 | bounceOut(t, b, c, d) { 137 | if ((t /= d) < 1 / 2.75) { 138 | return c * (7.5625 * t * t) + b; 139 | } else if (t < 2 / 2.75) { 140 | return c * (7.5625 * (t -= 1.5 / 2.75) * t + 0.75) + b; 141 | } else if (t < 2.5 / 2.75) { 142 | return c * (7.5625 * (t -= 2.25 / 2.75) * t + 0.9375) + b; 143 | } 144 | return c * (7.5625 * (t -= 2.625 / 2.75) * t + 0.984375) + b; 145 | }, 146 | bounceBoth(t, b, c, d) { 147 | if (t < d / 2) { 148 | return Tween["bounceIn"](t * 2, 0, c, d) * 0.5 + b; 149 | } 150 | return Tween["bounceOut"](t * 2 - d, 0, c, d) * 0.5 + c * 0.5 + b; 151 | } 152 | }; 153 | 154 | export function cssTransform(el, attr, val) { 155 | if (!el.transform) { 156 | el.transform = {}; 157 | } 158 | if (typeof val === "undefined") { 159 | if (typeof el.transform[attr] === "undefined") { 160 | switch (attr) { 161 | case "scale": 162 | case "scaleX": 163 | case "scaleY": 164 | el.transform[attr] = 100; 165 | break; 166 | default: 167 | el.transform[attr] = 0; 168 | } 169 | } 170 | return el.transform[attr]; 171 | } else { 172 | let transformVal = ""; 173 | el.transform[attr] = Number(val); 174 | for (let s in el.transform) { 175 | switch (s) { 176 | case "rotate": 177 | case "rotateX": 178 | case "rotateY": 179 | case "rotateZ": 180 | case "skewX": 181 | case "skewY": 182 | transformVal += " " + s + "(" + el.transform[s] + "deg)"; 183 | break; 184 | case "translateX": 185 | case "translateY": 186 | case "translateZ": 187 | transformVal += " " + s + "(" + el.transform[s] + "px)"; 188 | break; 189 | case "scale": 190 | case "scaleX": 191 | case "scaleY": 192 | transformVal += " " + s + "(" + el.transform[s] / 100 + ")"; 193 | break; 194 | } 195 | } 196 | el.style.WebkitTransform = el.style.transform = transformVal; 197 | } 198 | } 199 | 200 | export function css(el, attr, val) { 201 | if ( 202 | [ 203 | "rotate", 204 | "rotateX", 205 | "rotateY", 206 | "rotateZ", 207 | "scale", 208 | "scaleX", 209 | "scaleY", 210 | "skewX", 211 | "skewY", 212 | "translateX", 213 | "translateY", 214 | "translateZ" 215 | ].includes(attr) 216 | ) { 217 | return cssTransform(el, attr, val); 218 | } 219 | if (arguments.length === 2) { 220 | let val = getComputedStyle(el)[attr]; 221 | if (attr === "opacity") { 222 | val = Math.round(val * 100); 223 | } 224 | return parseFloat(val); 225 | } 226 | if (attr === "opacity") { 227 | el.style.opacity = val / 100; 228 | } else { 229 | el.style[attr] = val + "px"; 230 | } 231 | } 232 | 233 | export function MTween(init) { 234 | let [t, b, c, d] = [0, {}, {}, init.time / 20]; 235 | for (let s in init.target) { 236 | b[s] = css(init.el, s); 237 | c[s] = init.target[s] - b[s]; 238 | } 239 | clearInterval(init.el.timer); 240 | init.el.timer = setInterval(() => { 241 | t++; 242 | if (t > d) { 243 | clearInterval(init.el.timer); 244 | init.callBack && init.callBack.call(init.el); 245 | } else { 246 | init.callIn && init.callIn.call(init.el); 247 | for (let s in b) { 248 | let val = Tween[init.type](t, b[s], c[s], d).toFixed(2); 249 | css(init.el, s, val); 250 | } 251 | } 252 | }, 20); 253 | } 254 | 255 | export default MTween; 256 | -------------------------------------------------------------------------------- /src/components/tag-linkage-base/index.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 211 | 212 | 252 | 253 | 323 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 51 | 52 | 89 | 90 | 146 | --------------------------------------------------------------------------------