├── .eslintignore ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── babel.config.js ├── deploy_docs.sh ├── docs ├── .vuepress │ └── config.js ├── README.md ├── api │ ├── README.md │ ├── events.md │ ├── methods.md │ └── slots.md ├── guide │ ├── README.md │ ├── bing-images.md │ ├── getting-started.md │ ├── how-to-click.md │ └── oops.md └── zh │ ├── README.md │ ├── api │ ├── README.md │ ├── events.md │ ├── methods.md │ └── slots.md │ └── guide │ ├── README.md │ ├── bing-images.md │ ├── getting-started.md │ ├── how-to-click.md │ └── oops.md ├── lib └── vue-tinder.js ├── package.json ├── public ├── favicon.ico ├── images │ ├── down-txt.png │ ├── help.png │ ├── like-txt.png │ ├── like.png │ ├── nope-txt.png │ ├── nope.png │ ├── rewind-txt.png │ ├── rewind.png │ ├── super-like.png │ └── super-txt.png ├── index.html └── preview.gif ├── release.sh ├── rollup.config.js ├── src ├── App.vue ├── assets │ ├── help.png │ ├── history.png │ ├── like-txt.png │ ├── like.png │ ├── nope-txt.png │ ├── nope.png │ ├── super-like.png │ └── super-txt.png ├── bing.js ├── components │ ├── index.js │ └── vue-tinder │ │ ├── Tinder.vue │ │ ├── TinderCard.vue │ │ ├── open-methods.js │ │ ├── queue-handle.js │ │ ├── status.js │ │ ├── touch-event.js │ │ └── transition-event.js └── main.js ├── vue.config.js └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | lib/* -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | 4 | # local env files 5 | .env.local 6 | .env.*.local 7 | 8 | # Log files 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | 13 | # Editor directories and files 14 | .idea 15 | .vscode 16 | *.suo 17 | *.ntvs* 18 | *.njsproj 19 | *.sln 20 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: 2 | node_js 3 | 4 | node_js: 5 | - "10" 6 | 7 | install: 8 | - yarn global add rollup 9 | - yarn 10 | 11 | script: 12 | - yarn build 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 单泠浩 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | [![Travis (.org)](https://img.shields.io/travis/shanlh/vue-tinder/master)](https://travis-ci.org/github/shanlh/vue-tinder) 4 | [![npm bundle size](https://img.shields.io/bundlephobia/minzip/vue-tinder)](https://www.npmjs.com/package/vue-tinder) 5 | [![npm](https://img.shields.io/npm/dm/vue-tinder)](https://www.npmjs.com/package/vue-tinder) 6 | [![NPM](https://img.shields.io/npm/v/vue-tinder.svg)](https://www.npmjs.com/package/vue-tinder) 7 | [![NPM](https://img.shields.io/npm/l/vue-tinder)](https://www.npmjs.com/package/vue-tinder) 8 | 9 | `VueTinder` is a Vue component that helps you quickly implement the main features of similar apps like [Tinder](https://tinder.com), [TanTan](https://tantanapp.com/), etc. 10 | 11 | ## Links 12 | 13 | * 📘Documentation: [https://shanlh.github.io/vue-tinder](https://shanlh.github.io/vue-tinder) 14 | * 👉[Play with vue-tinder online](https://codesandbox.io/embed/vue-tinder-preview-by7qi) 15 | 16 | ## Features 17 | 18 | - Made With ❤️, strict detail requirements, under the slow motion, can better see the ease of transition animation, no matter how fast and complicated the operation does not have to worry about the problem. 19 | - Full Functioning, in addition to the original left, right, and up/down-sliding, a new fallback function has been added, which also supports multiple rollbacks at the same time. 20 | - Rich Configuration, adjustable sliding, spacing parameters and CSS units for a more flexible and adaptable layout. 21 | - Simple and lightweight (~5KB after Gzip compression) 22 | 23 | ![](https://raw.githubusercontent.com/shanlh/vue-tinder/master/public/preview.gif) 24 | 25 | ## TODO 26 | 27 | VueTinder is still under development, and here are some of the issues that are currently known: 28 | 29 | - The mobile end may accidentally touch VueTinder when the finger slides back. 30 | 31 | You are welcome to contribute to the development of VueTinder. 32 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [['@vue/app', { useBuiltIns: false }]] 3 | } 4 | -------------------------------------------------------------------------------- /deploy_docs.sh: -------------------------------------------------------------------------------- 1 | # 确保脚本抛出遇到的错误 2 | set -e 3 | 4 | # 生成静态文件 5 | yarn docs:build 6 | 7 | # 进入生成的文件夹 8 | cd docs/.vuepress/dist 9 | 10 | # 如果是发布到自定义域名 11 | # echo 'www.example.com' > CNAME 12 | 13 | git init 14 | git add -A 15 | git commit -m 'deploy' 16 | 17 | # 如果发布到 https://.github.io 18 | # git push -f git@github.com:/.github.io.git master 19 | 20 | # 如果发布到 https://.github.io/ 21 | git push -f git@github.com:shanlh/vue-tinder.git master:gh-pages 22 | 23 | cd - -------------------------------------------------------------------------------- /docs/.vuepress/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | base: '/vue-tinder/', 3 | locales: { 4 | // 键名是该语言所属的子路径 5 | // 作为特例,默认语言可以使用 '/' 作为其路径。 6 | '/': { 7 | lang: 'en-US', // 将会被设置为 的 lang 属性 8 | title: 'VueTinder', 9 | description: 'Vue-powered Static Site Generator' 10 | }, 11 | '/zh/': { 12 | lang: 'zh-CN', 13 | title: 'VueTinder', 14 | description: 'Vue 驱动的静态网站生成器' 15 | } 16 | }, 17 | themeConfig: { 18 | repo: 'shanlh/vue-tinder', 19 | smoothScroll: true, 20 | locales: { 21 | '/': { 22 | selectText: 'Languages', 23 | label: 'English', 24 | // ariaLabel: 'Languages', 25 | editLinkText: 'Edit this page on GitHub', 26 | // serviceWorker: { 27 | // updatePopup: { 28 | // message: "New content is available.", 29 | // buttonText: "Refresh" 30 | // } 31 | // }, 32 | // algolia: {}, 33 | nav: [ 34 | { text: 'Guide', link: '/guide/', ariaLabel: 'Guide' }, 35 | { text: 'API', link: '/api/' } 36 | ], 37 | sidebar: { 38 | '/guide/': [ 39 | { 40 | title: 'Guide', 41 | collapsable: false, 42 | children: [ 43 | ['/guide/', 'Introduction'], 44 | ['/guide/getting-started', 'Getting Started'], 45 | ['/guide/bing-images', 'Start With Bing Images'], 46 | ['/guide/how-to-click', 'How To Click?'], 47 | ['/guide/oops', 'Oops!'] 48 | ] 49 | } 50 | ], 51 | '/api/': [ 52 | { 53 | title: 'API', 54 | collapsable: false, 55 | children: [ 56 | ['/api/', 'Props'], 57 | ['/api/events', 'Events'], 58 | ['/api/methods', 'Methods'], 59 | ['/api/slots', 'Slots'] 60 | ] 61 | } 62 | ] 63 | } 64 | }, 65 | '/zh/': { 66 | selectText: '选择语言', 67 | label: '简体中文', 68 | editLinkText: '在 GitHub 上编辑此页', 69 | nav: [ 70 | { text: '指南', link: '/zh/guide/' }, 71 | { text: 'API', link: '/zh/api/' } 72 | ], 73 | sidebar: { 74 | '/zh/guide/': [ 75 | { 76 | title: '指南', 77 | collapsable: false, 78 | children: [ 79 | ['/zh/guide/', '介绍'], 80 | ['/zh/guide/getting-started', '快速起步'], 81 | ['/zh/guide/bing-images', '从 Bing Images 开始'], 82 | ['/zh/guide/how-to-click', '如何点击?'], 83 | ['/zh/guide/oops', 'Oops!'] 84 | ] 85 | } 86 | ], 87 | '/zh/api/': [ 88 | { 89 | title: 'API', 90 | collapsable: false, 91 | children: [ 92 | ['/zh/api/', '属性'], 93 | ['/zh/api/events', '事件'], 94 | ['/zh/api/methods', '方法'], 95 | ['/zh/api/slots', '插槽'] 96 | ] 97 | } 98 | ] 99 | } 100 | } 101 | } 102 | }, 103 | plugins: [ 104 | [ 105 | '@vuepress/google-analytics', 106 | { 107 | ga: 'UA-150557393-1' 108 | } 109 | ] 110 | ] 111 | } 112 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | home: true 3 | # heroImage: /hero.png 4 | heroText: VueTinder 5 | tagline: Have your own Tinder. 6 | actionText: Getting Started → 7 | actionLink: /guide/ 8 | features: 9 | - title: Made With ❤️ 10 | details: Strict detail requirements, under the slow motion, can better see the ease of transition animation, no matter how fast and complicated the operation does not have to worry about the problem. 11 | - title: Full Functioning 12 | details: In addition to the original left, right, and up/down-sliding, a new fallback function has been added, which also supports multiple rollbacks at the same time. 13 | - title: Rich Configuration 14 | details: Adjustable sliding, spacing parameters and CSS units for a more flexible and adaptable layout. 15 | footer: MIT Licensed | Copyright © 2017-present JohnnyDan 16 | --- -------------------------------------------------------------------------------- /docs/api/README.md: -------------------------------------------------------------------------------- 1 | ## queue.sync 2 | 3 | * Type: `Array` 4 | * Default: `[]` 5 | 6 | A queue for storing cards. 7 | 8 | ::: warning 9 | Please don't remove the `.sync` modifier of `queue`, `VueTinder` will modify `queue` according to your sliding operation. [Why do this?](https://vuejs.org/v2/guide/components-custom-events.html#sync-Modifier) 10 | ::: 11 | 12 | ## keyName 13 | 14 | * Type: `String` 15 | * Default: `key` 16 | 17 | ```html 18 | 19 | ... 20 | 21 | ``` 22 | 23 | ```js 24 | export default { 25 | data: { 26 | queue: [{ 27 | // key: 1 (If you do not set the key-name, the 'key' will be used by default. Please ensure that it exists and is independent) 28 | id: 1 29 | }, { 30 | id: 2 31 | }] 32 | } 33 | } 34 | ``` 35 | 36 | ::: warning 37 | Since `vue-tinder` uses `transition-group`, each child of `` must have **independent key**, the animation can work normally, so it corresponds to `queue` The `key-name` also needs to exist and be independent, and `VueTinder` will work properly. 38 | ::: 39 | 40 | The removed card's `key-name` will be passed as a parameter in the custom event `submit`, which you may need to use in the callback to remove the card. 41 | 42 | ## scaleStep 43 | 44 | * Type: `Number` 45 | * Default: `0.05` 46 | 47 | The step size between cards. The default value of 0.05 means that the scale of the second card is 0.95, and so on, the third is 0.9. 48 | 49 | ## offsetY 50 | 51 | * Type: `Number` 52 | * Default: `0` 53 | 54 | Card spacing. If it is greater than 0, the cards will be stacked down at a distance corresponding to px. Conversely, if it is less than 0, the cards will be stacked up. To modify the css unit, modify the offsetUnit. 55 | 56 | ::: warning 57 | If there is a requirement for browser compatibility, please use it as appropriate because the internal calculation uses `calc` 58 | ::: 59 | 60 | ## offsetUnit 61 | 62 | * Type: `String` 63 | * Default: `px` 64 | 65 | The css unit of card spacing. 66 | 67 | ## allowSuper 68 | 69 | * Type: `Boolean` 70 | * Default: `true` 71 | 72 | Whether to allow a response to a slip event. When sliding up in the open state, `slot="super"` will change its transparency. When the finger leaves the screen, it will consider whether the upsliding position meets the conditions for removing the card, and it is only possible to customize in the open state. The type of the value `super` is obtained in the `event: submit`. 73 | 74 | ## superThreshold 75 | 76 | * Type: `Number` 77 | * Default: `0.5` 78 | 79 | When moving up until it disappears, the moving distance is proportional to the height of the card. By default, the 1/2 height is in accordance with the removal condition. 80 | 81 | ## allowDown 82 | 83 | * Type: `Boolean` 84 | * Default: `false` 85 | 86 | Support to move down. 87 | 88 | ## downThreshold 89 | 90 | * Type: `Number` 91 | * Default: `0.5` 92 | 93 | Similar to superThreshold. 94 | 95 | ## pointerThreshold 96 | 97 | * Type: `Number` 98 | * Default: `0.5` 99 | 100 | When moving horizontally until it disappears, the moving distance is proportional to the "half width" of the card. Because it is the ratio of half the width of the card, the default of 0.5 is equivalent to 1/4 (0.5*0.5) card width. 101 | 102 | ## sync 103 | 104 | * Type: `Boolean` 105 | * Default: `false` 106 | 107 | Do you need to wait for the card to disappear completely after performing the next operation. 108 | 109 | ## max 110 | 111 | * Type: `Number` 112 | * Default: `3` 113 | 114 | The maximum number of renderings, in order to ensure performance, try to set the value smaller. `VueTinder` In order to achieve the effect of the preliminary card fade-in, an additional one will be rendered on the basis of max. -------------------------------------------------------------------------------- /docs/api/events.md: -------------------------------------------------------------------------------- 1 | # Events 2 | 3 | ## submit 4 | 5 | * Return: `{type, key, item}` 6 | 7 | The callback function after removing the card will be triggered by `VueTinder` via `$emit`. If you need to process the card after removing it, you need to listen on the component, such as: 8 | 9 | ```html 10 | 11 | ... 12 | 13 | ``` 14 | 15 | ```js 16 | export default { 17 | ... 18 | methods: { 19 | onSubmit(choice) { 20 | // choice.type => result,'like': swipe right, 'nope': swipe left, 'super': swipe up, 'down': swipe down 21 | // choice.key => The keyName of the removed card 22 | // choice.item => Child object in queue 23 | } 24 | } 25 | } 26 | ``` -------------------------------------------------------------------------------- /docs/api/methods.md: -------------------------------------------------------------------------------- 1 | # Methods 2 | 3 | ## decide 4 | 5 | * Argument: `type` 6 | * Type: `String` 7 | * Available: `'like'|'nope'|'super'|'down'` 8 | 9 | ```html 10 | 11 | ... 12 | 13 | ``` 14 | 15 | ```js {5,8,11,14} 16 | export default { 17 | ... 18 | methods: { 19 | like() { // Swipe right 20 | this.$refs['tinder'].decide('like') 21 | }, 22 | nope() { // Swipe left 23 | this.$refs['tinder'].decide('nope') 24 | }, 25 | superLike() { // Swipe up 26 | this.$refs['tinder'].decide('super') 27 | }, 28 | down() { // Swipe down 29 | this.$refs['tinder'].decide('down') 30 | } 31 | } 32 | } 33 | ``` 34 | 35 | ## rewind 36 | 37 | * Argument: `rewindList` 38 | * Type: `Array` 39 | 40 | As long as you are guaranteed to pass in an array, you can rewind one or more as needed. 41 | 42 | ```html 43 | 44 | ... 45 | 46 | ``` 47 | 48 | ```js {5} 49 | export default { 50 | ... 51 | methods: { 52 | rewind() { 53 | this.$refs['tinder'].rewind([{id: 1}, {id: 2}]) 54 | } 55 | } 56 | } 57 | ``` 58 | -------------------------------------------------------------------------------- /docs/api/slots.md: -------------------------------------------------------------------------------- 1 | # Slots 2 | 3 | ## Default slot 4 | 5 | It will be inserted indirectly into each sub-component `TinderCard` of `VueTinder`, which is the content inside the "card". 6 | 7 | ```html 8 | 9 | 10 | ... 11 | 12 | ``` 13 | 14 | ## Scoped Slots 15 | 16 | * Argument: `{ index,data }` 17 | 18 | Reusable slot for obtaining data from subcomponents, used to customize the contents of the card, `index` is the index value of the card, `data` is each object in `queue`, and the property value is obtained as follows: 19 | 20 | ```html 21 | 22 | 27 | 28 | 35 | 36 | ``` 37 | 38 | * See also: [Destructuring-Slot-Props](https://vuejs.org/v2/guide/components-slots.html#Destructuring-Slot-Props) 39 | 40 | ## like 41 | 42 | The slot that will gradually become opaque during the right sliding process, the opacity is automatically controlled by `VueTinder`, and only the slot content and style need to be customized. 43 | 44 | The response range is controlled by `pointerThreshold` (default 0.5). 45 | 46 | ```html 47 | 48 | 51 | 52 | 53 | 54 | ``` 55 | 56 | ## nope 57 | 58 | The slots that are gradually opaque during the left-sliding process, and the rest of the features are as above. 59 | 60 | ## super 61 | 62 | The slot that gradually becomes opaque during the sliding process, except for the response range is different from like/nope, the other features are as above. 63 | 64 | The response range is controlled by `super-threshold` (default 0.5). 65 | 66 | ## down 67 | 68 | Similar to super. 69 | 70 | The response range is controlled by `down-threshold` (default 0.5). 71 | 72 | ## rewind 73 | 74 | The slot that disappears and then disappears when the `rewind` operation is executed. -------------------------------------------------------------------------------- /docs/guide/README.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | `VueTinder` is a Vue component that helps you quickly implement the main features of similar apps like [Tinder](https://tinder.com), [TanTan](https://tantanapp.com/), etc. 4 | 5 | ## Features 6 | 7 | - Made With ❤️, strict detail requirements, under the slow motion, can better see the ease of transition animation, no matter how fast and complicated the operation does not have to worry about the problem. 8 | - Full Functioning, in addition to the original left, right, and up-sliding, a new fallback function has been added, which also supports multiple rollbacks at the same time. 9 | - Rich Configuration, adjustable sliding, spacing parameters and CSS units for a more flexible and adaptable layout. 10 | - Simple and lightweight (~5KB after Gzip compression) 11 | 12 | [Preview](https://codesandbox.io/embed/vue-tinder-preview-by7qi) 13 | 14 | ![](https://raw.githubusercontent.com/shanlh/vue-tinder/master/public/preview.gif) 15 | 16 | ## TODO 17 | 18 | VueTinder is still under development, and here are some of the issues that are currently known: 19 | 20 | - The mobile end may accidentally touch VueTinder when the finger slides back. 21 | 22 | You are welcome to contribute to the development of VueTinder. -------------------------------------------------------------------------------- /docs/guide/bing-images.md: -------------------------------------------------------------------------------- 1 | # Start With Bing Images 2 | 3 | > The case code in the tutorial will be written using ES2015. 4 | 5 | ::: tip 6 | This tutorial uses [BING Images](https://bing.com/) 7 | ::: 8 | 9 | As the first step in understanding this plugin, we will be doing a Bing Images that you can choose whether you like it or not. Let's take a look at the results. 10 | 11 | 12 | 13 | First, we need to write a template with the following content (the code that is not important is omitted): 14 | 15 | ``` html 16 | 17 |
23 | 24 | 25 | 26 | 27 | ``` 28 | 29 | In the template, we bind `queue.sync` to the `VueTinder` as the data source. This queue is looped inside `VueTinder` and listens for the component's `submit` event using a method called `onSubmit`. The `offset-y` attribute is configured so that the card is separated from the card by `10px`. If you use `rem` for mobile adaptation, you can modify the css unit of the card spacing by modifying `offsetUnit`. 30 | 31 | ::: warning 32 | Please don't remove the `.sync` modifier of `queue`, `VueTinder` will modify `queue` according to your sliding operation. [Why do this?](https://vuejs.org/v2/guide/components-custom-events.html#sync-Modifier) 33 | ::: 34 | 35 | We also set the property `keyName` for `VueTinder` so that `VueTinder` knows how to set `key` for the internal `v-for`. The default value of this property is `key`. If it happens to be the same as yours, you don't have to set it. 36 | 37 | It is worth mentioning that you also need to specify a width and height for VueTinder, such as: 38 | 39 | ``` css 40 | .vue-tinder { 41 | width: 335px; 42 | height: 447px; 43 | } 44 | ``` 45 | 46 | This is important for `VueTinder`, which requires height for internal touch events in order to achieve moving animations for different starting positions. 47 | 48 | The next step is to get the data and accept the result of the operation. When the result is returned, you can judge whether you need to append the data according to the actual situation. The mock method used in the example is `only for the schematic` ,`Do not` copy directly. You can modify it according to the actual situation: 49 | 50 | ```js 51 | import source from '@/where/source' // Such as: [ {id: 'AdelieBreeding_ZH-CN1750945258'} , ... ] 52 | 53 | export default { 54 | data: () => ({ 55 | queue: [], 56 | offset: 0 57 | }), 58 | created() { 59 | this.mock() 60 | }, 61 | methods: { 62 | mock(count = 5) { 63 | const list = source.slice(this.offset, count) 64 | this.offset += count 65 | this.queue = this.queue.concat(list) 66 | }, 67 | onSubmit(type, key, item) { 68 | // type: result,'like': swipe right, 'nope': swipe left, 'super': swipe up 69 | // key: The keyName of the removed card 70 | // item: Child object in queue 71 | if (this.queue.length < 3) { 72 | this.mock() 73 | } 74 | } 75 | } 76 | } 77 | ``` 78 | 79 | Now, you have completed a Bing Images that you can choose with `VueTinder`. What else? Please see the next section: [How To Click?](/guide/how-to-click) → -------------------------------------------------------------------------------- /docs/guide/getting-started.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | ## Installation 4 | 5 | ### NPM / Yarn 6 | It is recommended to install this way when building large applications. 7 | 8 | ``` bash 9 | npm install vue-tinder --save 10 | # or 11 | yarn add vue-tinder 12 | ``` 13 | 14 | ### Direct download / CDN 15 | 16 | [https://unpkg.com/vue-tinder/dist/vue-tinder.js](https://unpkg.com/vue-tinder/dist/vue-tinder.js) 17 | 18 | Unpkg.com provides an NPM-based CDN link, and the link above will always point to the latest version released by NPM. You can also specify the version number or Tag like [https://unpkg.com/vue-tinder@2.0.0/dist/vue-tinder.js](https://unpkg.com/vue-tinder@2.0.0/dist/vue-tinder.js). 19 | 20 | After the plugin is introduced using the `script` tag, the VueTinder component is automatically registered to the global and can be used directly during development. 21 | 22 | ```html 23 | 24 | 25 | ``` 26 | 27 | ## Reference plugin 28 | 29 | ### Component format 30 | 31 | You can refer to it directly as a custom component: 32 | 33 | ``` vue 34 | 37 | 38 | 47 | ``` 48 | 49 | ### Plugin format 50 | 51 | If you need to register `VueTinder` globally in your project, you can register it with the global method `Vue.use()` provided by Vue.js: 52 | 53 | ``` js 54 | // main.js or index.js 55 | import VueTinder from 'vue-tinder' 56 | 57 | Vue.use(VueTinder) 58 | ``` 59 | 60 | As with `script`, the plugin format also registers the `VueTinder` component as a global component, eliminating the need to re-register with the `components` attribute in your own components. 61 | 62 | ## Build development version 63 | 64 | If you want to use the latest development version, you have to clone directly from GitHub and build a `VueTinder` yourself. 65 | 66 | ``` bash 67 | git clone https://github.com/shanlh/vue-tinder.git node_modules/vue-tinder 68 | cd node_modules/vue-tinder 69 | yarn 70 | yarn build 71 | ``` -------------------------------------------------------------------------------- /docs/guide/how-to-click.md: -------------------------------------------------------------------------------- 1 | # How To Click? 2 | 3 | In the previous section, we can only decide whether to like it by sliding. Sometimes the user may not want to slide. The `VueTinder` also thinks about this. I have prepared some methods for you to perform operations for the user. The examples are as follows: 4 | 5 | 6 | 7 | First, we need to add a click button: 8 | 9 | ``` html 10 | ... 11 |
12 | 13 | 14 | 15 |
16 | ``` 17 | 18 | You may have noticed that we added `ref` to `VueTinder`. Yes, we need to call `VueTinder` to execute the operation via `ref`. Let's see what to do: 19 | 20 | ``` js 21 | export default { 22 | ... 23 | methods: { 24 | decide (choice) { 25 | this.$refs.tinder.decide(choice) 26 | } 27 | } 28 | } 29 | ``` 30 | 31 | Very simple, you only need to call the `decide` method, passing in the corresponding operation, such as: nope, like, super. 32 | 33 | But what if the user wants to return because of a mistake? Please see the next section: [Oops!](/guide/oops) -------------------------------------------------------------------------------- /docs/guide/oops.md: -------------------------------------------------------------------------------- 1 | # Oops! 2 | 3 | In the previous section, we can choose whether we like it by clicking, but what if the user wants to return because of a mistake? `VueTinder` provides you with a new method: `rewind`, you can see the effect first: 4 | 5 | 6 | 7 | First, we need to add a button that can be clicked on the Rewind operation. To make the experience better and make the source of the card more understandable, we can add an indicator `(optional)` for the Rewind operation, and `VueTinder` will be appropriate. When it is displayed or hidden, the template code is as follows: 8 | 9 | ``` html 10 | 11 | ... 12 | ... 13 | 14 | 15 | 16 | 17 | ``` 18 | 19 | Then write the core code for this example: 20 | 21 | ``` js 22 | export default { 23 | data: () => ({ 24 | ... 25 | history: [] 26 | }), 27 | ... 28 | methods: { 29 | decide(choice) { 30 | if(choice === 'rewind') { 31 | if(this.history.length) { 32 | this.$refs.tinder.rewind([this.history.pop()]) 33 | } 34 | return 35 | } 36 | this.$refs.tinder.decide(choice) 37 | }, 38 | onSubmit({item}) { 39 | ... 40 | this.history.push(item) 41 | } 42 | } 43 | } 44 | ``` 45 | 46 | In the example, we use `history` to store the `item` of the executed operation, and you can store the history according to your preferences. Note that `VueTinder` can be more than `rewind` at the same time, so the incoming must be an array. If you only need `rewind`, you need to wrap it with `[]`. 47 | 48 | Having said that, we have already finished the basics. You can understand the more advanced configuration by looking at the [API](/api). -------------------------------------------------------------------------------- /docs/zh/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | home: true 3 | heroText: VueTinder 4 | tagline: 创造属于你的 Tinder 5 | actionText: 快速上手 → 6 | actionLink: /zh/guide/ 7 | features: 8 | - title: Made with ❤️ 9 | details: 严苛的细节要求,在慢镜头下,更能看出过渡动画的从容,无论多快速复杂的操作都不用担心出问题。 10 | - title: 功能齐全 11 | details: 除原有的左、右、上、下滑外,还新增了回退功能,更支持同时回退多个。 12 | - title: 配置丰富 13 | details: 可调滑动、间距、缩放比例参数及 CSS 单位,以实现更灵活,更具适配性的布局。 14 | footer: MIT Licensed | Copyright © 2017-present JohnnyDan 15 | --- -------------------------------------------------------------------------------- /docs/zh/api/README.md: -------------------------------------------------------------------------------- 1 | ## queue.sync 2 | 3 | * 类型:`Array` 4 | * 默认值:`[]` 5 | 6 | 用于存放卡片的队列。 7 | 8 | ::: warning 9 | 请不要去除 `queue` 的 `.sync` 修饰符,`VueTinder` 会根据你的滑动操作来对 `queue` 进行修改。[为什么这么做?](https://cn.vuejs.org/v2/guide/components-custom-events.html#sync-%E4%BF%AE%E9%A5%B0%E7%AC%A6) 10 | ::: 11 | 12 | ## keyName 13 | 14 | * 类型:`String` 15 | * 默认值:`key` 16 | 17 | ```html 18 | 19 | ... 20 | 21 | ``` 22 | 23 | ```js 24 | export default { 25 | data: { 26 | queue: [{ 27 | // key: 1 (若不设置key-name,会默认使用key,请确保存在且独立) 28 | id: 1 29 | }, { 30 | id: 2 31 | }] 32 | } 33 | } 34 | ``` 35 | 36 | ::: warning 37 | 由于 `vue-tinder` 内使用了 `transition-group`,每个 `` 的子节点必须有 **独立的 key** ,动画才能正常工作,所以在 `queue` 中所对应的 `key-name` 也需要存在且独立,`VueTinder` 才能正常工作。 38 | ::: 39 | 40 | 被移除卡片的 `key-name` 会在自定义事件 `submit` 中作为参数传递,你在移除卡片的回调中可能会需要用到。 41 | 42 | ## scaleStep 43 | 44 | * 类型:`Number` 45 | * 默认值:`0.05` 46 | 47 | 卡片间缩放步长。默认值为 0.05 即表明:第二个卡片的 scale 大小为 0.95,依此类推第三个为 0.9。 48 | 49 | ## offsetY 50 | 51 | * 类型:`Number` 52 | * 默认值:`0` 53 | 54 | 卡片间距。如果大于 0,卡片会以对应 px 的距离向下堆叠。相反的,如果小于 0,则卡片会往上堆叠。如需修改 css 单位,请修改 offsetUnit。 55 | 56 | ::: warning 57 | 如果对浏览器兼容性有要求,请酌情使用,因为内部计算使用了 `calc` 58 | ::: 59 | 60 | ## offsetUnit 61 | 62 | * 类型:`String` 63 | * 默认值:`px` 64 | 65 | 卡片间距的 CSS 单位。 66 | 67 | ## allowSuper 68 | 69 | * 类型:`Boolean` 70 | * 默认值:`true` 71 | 72 | 是否允许响应上滑事件。在开启状态下上滑时,`slot="super"` 会有透明度变化,手指离开屏幕时会考虑上滑位置是否符合移除卡片的条件,且只有在开启状态下才有可能在 `事件:submit` 中获取到值为 `super` 的 type。 73 | 74 | ## superThreshold 75 | 76 | * 类型:`Number` 77 | * 默认值:`0.5` 78 | 79 | 向上移动直至消失时,移动距离占卡片高度的比例,默认移动 1/2 高度便符合移出条件。 80 | 81 | ## allowDown 82 | 83 | * 类型:`Boolean` 84 | * 默认值:`false` 85 | 86 | 是否允许响应下滑事件。 87 | 88 | ## downThreshold 89 | 90 | * 类型:`Number` 91 | * 默认值:`0.5` 92 | 93 | 类似 superThreshold。 94 | 95 | ## pointerThreshold 96 | 97 | * 类型:`Number` 98 | * 默认值:`0.5` 99 | 100 | 横向移动直至消失时,移动距离占卡片 "一半宽度" 的比例,因为是占卡片一半宽度的比例,所以默认 0.5 便相当于 1/4(0.5*0.5)卡片宽度。 101 | 102 | ## sync 103 | 104 | * 类型:`Boolean` 105 | * 默认值:`false` 106 | 107 | 执行下次操作是否需要等卡片完全消失。 108 | 109 | ## max 110 | 111 | * 类型:`Number` 112 | * 默认值:`3` 113 | 114 | 最大渲染个数,为了保证性能,尽量把值设得小些。`VueTinder` 为了实现预备卡片渐入的效果,会在 max 的基础上额外渲染一个。 -------------------------------------------------------------------------------- /docs/zh/api/events.md: -------------------------------------------------------------------------------- 1 | # 事件 2 | 3 | ## submit 4 | 5 | * 返回值:`{type, key, item}` 6 | 7 | 移除卡片后的回调函数,会由 `VueTinder` 通过 `$emit` 来触发,如需在移除卡片后做处理,需要在组件上监听,如: 8 | 9 | ```html 10 | 11 | ... 12 | 13 | ``` 14 | 15 | ```js 16 | export default { 17 | ... 18 | methods: { 19 | onSubmit(choice) { 20 | // choice.type: 结果,'like':右滑, 'nope':左滑, 'super':上滑,'down':下滑 21 | // choice.key: 被移除卡片的 keyName 22 | // choice.item: queue 中的子对象 23 | } 24 | } 25 | } 26 | ``` -------------------------------------------------------------------------------- /docs/zh/api/methods.md: -------------------------------------------------------------------------------- 1 | # 方法 2 | 3 | ## decide 4 | 5 | * 参数: `type` 6 | * 类型:`String` 7 | * 可选值:`'like'|'nope'|'super'|'down'` 8 | 9 | ```html 10 | 11 | ... 12 | 13 | ``` 14 | 15 | ```js {5,8,11,14} 16 | export default { 17 | ... 18 | methods: { 19 | like() { // 右滑 20 | this.$refs['tinder'].decide('like') 21 | }, 22 | nope() { // 左滑 23 | this.$refs['tinder'].decide('nope') 24 | }, 25 | superLike() { // 上滑 26 | this.$refs['tinder'].decide('super') 27 | }, 28 | down() { // 下滑 29 | this.$refs['tinder'].decide('down') 30 | } 31 | } 32 | } 33 | ``` 34 | 35 | ## rewind 36 | 37 | * 参数: `rewindList` 38 | * 类型: `Array` 39 | 40 | 只要保证传入的是数组,可以根据需要 rewind 一个或多个。 41 | 42 | ```html 43 | 44 | ... 45 | 46 | ``` 47 | 48 | ```js {5} 49 | export default { 50 | ... 51 | methods: { 52 | rewind() { 53 | this.$refs['tinder'].rewind([{id: 1}, {id: 2}]) 54 | } 55 | } 56 | } 57 | ``` 58 | -------------------------------------------------------------------------------- /docs/zh/api/slots.md: -------------------------------------------------------------------------------- 1 | # 插槽 2 | 3 | ## 默认插槽 4 | 5 | 会间接插入到 `VueTinder` 的每个子组件 `TinderCard` 中,即 “卡片” 内的内容。 6 | 7 | ```html 8 | 9 | 10 | ... 11 | 12 | ``` 13 | 14 | ## 作用域插槽 15 | 16 | * 参数:`{ index,data }` 17 | 18 | 可从子组件获取数据的可复用的插槽,用于自定义卡片的内容,`index` 为卡片的索引值,`data` 是 `queue` 中每个对象,属性值获取方法如下: 19 | 20 | ```html 21 | 22 | 27 | 28 | 35 | 36 | ``` 37 | 38 | * 参考: [作用域插槽](https://cn.vuejs.org/v2/guide/components-slots.html#%E4%BD%9C%E7%94%A8%E5%9F%9F%E6%8F%92%E6%A7%BD) 39 | 40 | ## like 41 | 42 | 右滑过程中会逐渐不透明的插槽,不透明度已由 `VueTinder` 自动控制,仅需自定义插槽内容及样式即可。 43 | 44 | 响应范围由 `pointerThreshold` 控制(默认0.5)。 45 | 46 | ```html 47 | 48 | 51 | 52 | 53 | 54 | ``` 55 | 56 | ## nope 57 | 58 | 左滑过程中会逐渐不透明的插槽,其余特性如上。 59 | 60 | ## super 61 | 62 | 上滑过程中会逐渐不透明的插槽,除响应范围与 like/nope 不同外,其余特性如上。 63 | 64 | 响应范围由 `super-threshold` 控制(默认 0.5 )。 65 | 66 | ## down 67 | 68 | 类似 super。 69 | 70 | 响应范围由 `down-threshold` 控制(默认 0.5 )。 71 | 72 | ## rewind 73 | 74 | 执行 `rewind` 操作时会短暂显示然后消失的插槽。 -------------------------------------------------------------------------------- /docs/zh/guide/README.md: -------------------------------------------------------------------------------- 1 | # 介绍 2 | 3 | `VueTinder` 是一款能帮助你快速实现 [Tinder](https://tinder.com) 、 [探探](https://tantanapp.com/) 等类似 APP 主要功能的 Vue 组件,[使用文档](https://shanlh.github.io/vue-tinder)。 4 | 5 | ## 特性 6 | 7 | - Made with ❤️,严苛的细节要求,在慢镜头下,更能看出过渡动画的从容,无论多快速复杂的操作都不用担心出问题 8 | - 功能齐全,除原有的左、右、上滑外,还新增了回退功能,更支持同时回退多个 9 | - 配置丰富,可调滑动、间距参数及 CSS 单位,以实现更灵活,更具适配性的布局。 10 | - 简单轻量 ( Gzip 压缩后约 5KB ) 11 | 12 | [在线预览](https://codesandbox.io/embed/vue-tinder-preview-by7qi) 13 | 14 | ![](https://raw.githubusercontent.com/shanlh/vue-tinder/master/public/preview.gif) 15 | 16 | ## TODO 17 | 18 | VueTinder 仍然处于开发中,这里有一些目前已知存在的问题: 19 | 20 | - 移动端在手指侧滑返回时可能会误触到 VueTinder 21 | 22 | 欢迎你为 VueTinder 的开发作出贡献。 -------------------------------------------------------------------------------- /docs/zh/guide/bing-images.md: -------------------------------------------------------------------------------- 1 | # 从 Bing Images 开始 2 | 3 | > 教程中的案例代码将使用 ES2015 来编写。 4 | 5 | ::: tip 6 | 本教程图片素材使用 [BING 美图](https://bing.com/) 7 | ::: 8 | 9 | 作为了解这款插件的第一步,我们将会做一个能选择是否喜欢的 Bing Images,先来看看结果吧,滑动卡片试试: 10 | 11 | 12 | 13 | 首先,我们需要编写模板,内容大概如下(已省略不重要的代码): 14 | 15 | ``` html 16 | 17 |
23 | 24 | 25 | 26 | 27 | ``` 28 | 29 | 在模板中,我们为 `VueTinder` 绑定了 `queue.sync` 作为数据源,在 `VueTinder` 内部会循环这个队列,并使用一个叫做 `onSubmit` 的方法监听组件的 `submit` 事件,我们还配置了 `offset-y` 属性,使卡片与卡片之间间隔了 `10px`,如果你使用 `rem` 来做移动端适配,可以通过修改 `offsetUnit` 来修改卡片间距的 css 单位。 30 | 31 | ::: warning 32 | 请不要去除 `queue` 的 `.sync` 修饰符,`VueTinder` 会根据你的滑动操作来对 `queue` 进行修改。[为什么这么做?](https://cn.vuejs.org/v2/guide/components-custom-events.html#sync-%E4%BF%AE%E9%A5%B0%E7%AC%A6) 33 | ::: 34 | 35 | 我们还为 `VueTinder` 设置了属性 `keyName`,这样 `VueTinder` 才知道如何为内部 `v-for` 设置 `key`,该属性默认值为 `key`,如果正好与你的一样则不必设置。 36 | 37 | 值得一提的是,你还需要为 `VueTinder` 规定一个宽高,如: 38 | 39 | ``` css 40 | .vue-tinder { 41 | width: 335px; 42 | height: 447px; 43 | } 44 | ``` 45 | 46 | 这对 `VueTinder` 很重要,内部触摸事件需要用到高度,为了实现不同起始位置对应的移动动画。 47 | 48 | 接下来是获取数据、接受操作结果,当返回结果后可以根据实际情况判断是否需要追加数据,示例所用的 mock 方法`仅作示意`,`请勿直接复制`,你可根据实际情况进行修改: 49 | 50 | ```js 51 | import source from '@/where/source' // 如: [ {id: 'AdelieBreeding_ZH-CN1750945258'} , ... ] 52 | 53 | export default { 54 | data: () => ({ 55 | queue: [], 56 | offset: 0 57 | }), 58 | created() { 59 | this.mock() 60 | }, 61 | methods: { 62 | mock(count = 5) { 63 | const list = source.slice(this.offset, count) 64 | this.offset += count 65 | this.queue = this.queue.concat(list) 66 | }, 67 | onSubmit(type, key, item) { 68 | // type: 结果,'like':右滑, 'nope':左滑, 'super':上滑 69 | // key: 被移除卡片的 keyName 70 | // item: queue 中的子对象 71 | if (this.queue.length < 3) { 72 | this.mock() 73 | } 74 | } 75 | } 76 | } 77 | ``` 78 | 79 | 现在,你已经用 `VueTinder` 完成了一个可以选择是否喜欢的 Bing Images,还差点什么?请看下节:[如何点击?](/zh/guide/how-to-click) → -------------------------------------------------------------------------------- /docs/zh/guide/getting-started.md: -------------------------------------------------------------------------------- 1 | # 快速起步 2 | 3 | ## 安装 4 | 5 | ### NPM / Yarn 6 | 推荐在构建大型应用的时候使用这种方式进行安装。 7 | 8 | ``` bash 9 | npm install vue-tinder --save 10 | # or 11 | yarn add vue-tinder 12 | ``` 13 | 14 | ### 直接下载 / CDN 15 | 16 | [https://unpkg.com/vue-tinder/dist/vue-tinder.js](https://unpkg.com/vue-tinder/dist/vue-tinder.js) 17 | 18 | Unpkg.com 提供了基于 NPM 的 CDN 链接,上面的链接会一直指向在 NPM 发布的最新版本。你也可以像 [https://unpkg.com/vue-tinder@2.0.0/dist/vue-tinder.js](https://unpkg.com/vue-tinder@2.0.0/dist/vue-tinder.js) 这样指定 版本号 或者 Tag。 19 | 20 | 在使用 `script` 标签引入此插件后,VueTinder 组件会被自动注册到全局,开发时直接进行使用即可。 21 | 22 | ```html 23 | 24 | 25 | ``` 26 | 27 | ## 引用插件 28 | 29 | ### 组件形式 30 | 31 | 你可以直接将它当做一个自定义组件进行引用: 32 | 33 | ``` vue 34 | 37 | 38 | 47 | ``` 48 | 49 | ### 插件形式 50 | 51 | 如果你需要在项目中全局注册 `VueTinder`,那么可以采用 Vue.js 提供的全局方法 `Vue.use()` 对此插件进行注册: 52 | 53 | ``` js 54 | // main.js or index.js 55 | import VueTinder from 'vue-tinder' 56 | 57 | Vue.use(VueTinder) 58 | ``` 59 | 60 | 和 `script` 引入方式一样,使用插件形式也会将 `VueTinder` 组件注册为全局组件,在你自己的组件中就无需再使用 `components` 属性重复注册了。 61 | 62 | ## 构建开发版 63 | 64 | 如果你想使用最新的开发版,就得从 GitHub 上直接 clone,然后自己 build 一个 `VueTinder`。 65 | 66 | ``` bash 67 | git clone https://github.com/shanlh/vue-tinder.git node_modules/vue-tinder 68 | cd node_modules/vue-tinder 69 | yarn 70 | yarn build 71 | ``` -------------------------------------------------------------------------------- /docs/zh/guide/how-to-click.md: -------------------------------------------------------------------------------- 1 | # 如何点击? 2 | 3 | 在上一节中,我们只能通过滑动来决定是否喜欢,有些时候用户可能并不想滑动,`VueTinder` 也想到了这点,为你准备了一些方法来为用户执行操作,示例如下: 4 | 5 | 6 | 7 | 首先,我们需要添加点击按钮: 8 | 9 | ``` html 10 | ... 11 |
12 | 13 | 14 | 15 |
16 | ``` 17 | 18 | 你可能已经注意到我们为 `VueTinder` 加上了 `ref`,是的,我们需要通过 `ref` 来调用 `VueTinder` 提供的方法来执行操作,接下来看看怎么做吧: 19 | 20 | ``` js 21 | export default { 22 | ... 23 | methods: { 24 | decide (choice) { 25 | this.$refs.tinder.decide(choice) 26 | } 27 | } 28 | } 29 | ``` 30 | 31 | 很简单吧,你只需要调用 `decide` 方法,传入对应的操作,如:nope、like、super 即可。 32 | 33 | 可是如果用户因为误操作想要返回该怎么办呢?请看下节:[Oops!](/zh/guide/oops) -------------------------------------------------------------------------------- /docs/zh/guide/oops.md: -------------------------------------------------------------------------------- 1 | # Oops! 2 | 3 | 在上一节中,我们可以通过点击来选择是否喜欢了,但如果用户因为误操作想要返回该怎么办呢?`VueTinder` 为你提供了新方法:`rewind`,可以先来看下效果: 4 | 5 | 6 | 7 | 首先,我们需要添加一个可以点击进行 Rewind 操作的按钮,为了体验更好,让这张卡片的来源更明白,我们可以为 Rewind 操作增加一个指示器`(可选)`,`VueTinder` 会在合适的时候对其进行显示或隐藏,模版代码如下: 8 | 9 | ``` html 10 | 11 | ... 12 | ... 13 | 14 | 15 | 16 | 17 | ``` 18 | 19 | 然后编写本例的核心代码: 20 | 21 | ``` js 22 | export default { 23 | data: () => ({ 24 | ... 25 | history: [] 26 | }), 27 | ... 28 | methods: { 29 | decide(choice) { 30 | if(choice === 'rewind') { 31 | if(this.history.length) { 32 | this.$refs.tinder.rewind([this.history.pop()]) 33 | } 34 | return 35 | } 36 | this.$refs.tinder.decide(choice) 37 | }, 38 | onSubmit({item}) { 39 | ... 40 | this.history.push(item) 41 | } 42 | } 43 | } 44 | ``` 45 | 46 | 示例中,我们用 `history` 来存放被执行操作的 `item`,你可以根据喜好来存放历史。需要注意的是:`VueTinder` 可以同时 `rewind` 多个,所以传入的必须是个数组,如果你只需要 `rewind` 一个,也需要用 `[]` 将其包裹。 47 | 48 | 讲到这里,我们已经基本介绍完了,更高级的配置你可以通过查看 [API](/zh/api) 来了解。 -------------------------------------------------------------------------------- /lib/vue-tinder.js: -------------------------------------------------------------------------------- 1 | !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self)["vue-tinder"]=e()}(this,function(){"use strict";function e(t){return{status:t?3:0,touchId:null,start:{},move:{},startPoint:1,result:null}}var u=0,s=1,c=2,a=3,l=4,t={name:"TinderCard",props:{tinderMounted:{type:Boolean,default:!1},index:{type:Number,required:!0},ready:{type:Boolean,default:!1},state:{type:Object,required:!0},ratio:{type:Number,default:0},rewind:{type:[Number,Boolean],default:!1},scaleStep:{type:Number,required:!0},offsetY:{type:Number,required:!0},offsetUnit:{type:String,required:!0}},data:function(){return{inited:!1,scopedRewind:!1,willDestory:!1}},computed:{curScale:function(){return this.scaleStep*this.index},isCur:function(){return 0===this.index},style:function(){return this.inited?this.state.status===s?this.movingStyle:this.normalStyle:{}},normalStyle:function(){return this.isCur?{opacity:1,transform:"translate3d(0,0,0) rotate(0) scale3d(1,1,1)",transition:"all 500ms cubic-bezier(0.175, 0.885, 0.32, 1.275), z-index 0s"}:{opacity:this.ready?0:1,transform:this.getTransform(),transition:"all 500ms cubic-bezier(0.175, 0.885, 0.32, 1.275) ".concat(this.scopedRewind?80*this.scopedRewind:0,"ms, z-index 0s")}},movingStyle:function(){var t,e,n,i,s={transition:"none"};return this.isCur?(n=(e=this.state).start,t=e.move,i=e.startPoint,e=t.x-n.x||0,n=t.y-n.y||0,i=10*this.ratio*i,s.transform="translate3d(".concat(e,"px,").concat(n,"px,0) rotate(").concat(i,"deg)")):(1<(i=Math.abs(this.ratio))&&(i=1),this.ready&&(s.opacity=+i),s.transform=this.getTransform(i)),s}},watch:{index:function(t,e){tt.length)&&(e=t.length);for(var n=0,i=new Array(e);nthis.hideIndex?(e=this.hideIndex,this.hideIndex+=1+n):e=this.hideIndex++:e=this.hideIndex+n-t,this.lastHideIndex=e},getTransform:function(t){var e,n,i=1-this.scaleStep*t,s=0;return this.offsetY&&(e=this.offsetY<0,n=t*Math.abs(this.offsetY),t=(1-i)/2*100,e&&(n*=-1,t*=-1),s="calc(".concat(t,"% + ").concat(n).concat(this.offsetUnit,")")),"translate3d(0,".concat(s,",0) scale3d(").concat(i,",").concat(i,",1)")}}},{data:function(){return{rewindKeys:[]}},methods:{decide:function(t){this.state.touchId||this.status!==u||(this.state.start={x:0,y:0},this.state.move={x:"super"===t||"down"===t?0:"like"===t?1:-1,y:"super"===t?-1:"down"===t?1:0},this.state.startPoint=1,this.shiftCard(t))},rewind:function(t){var e,n=this.keyName,i=function(t,e){var n;if("undefined"==typeof Symbol||null==t[Symbol.iterator]){if(Array.isArray(t)||(n=y(t))||e&&t&&"number"==typeof t.length){n&&(t=n);var i=0,e=function(){};return{s:e,n:function(){return i>=t.length?{done:!0}:{done:!1,value:t[i++]}},e:function(t){throw t},f:e}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var s,a=!0,r=!1;return{s:function(){n=t[Symbol.iterator]()},n:function(){var t=n.next();return a=t.done,t},e:function(t){r=!0,s=t},f:function(){try{a||null==n.return||n.return()}finally{if(r)throw s}}}}(t);try{for(i.s();!(s=i.n()).done;){var s=s.value;this.rewindKeys.push(s[n]+"")}}catch(t){i.e(t)}finally{i.f()}(e=this.queue).unshift.apply(e,f(t))},shiftCard:function(t){this.state.status=c,this.state.result=t;var e=this.queue.shift();this.$emit("update:queue",this.queue),this.submitDecide(t,e)},submitDecide:function(t,e){this.$emit("submit",{type:t,key:e[this.keyName],item:e})}}}],components:{TinderCard:n({render:t,staticRenderFns:[]},function(t){t&&t("data-v-1776a5a8_0",{source:"\n.tinder-card[data-v-1776a5a8] {\n position: absolute;\n width: 100%;\n height: 100%;\n overflow: hidden;\n background: #fefefe;\n border-radius: 10px;\n box-shadow: 0 2px 10px rgba(0, 0, 0, 0.15);\n}\n.tinder-rewind-leave-active[data-v-1776a5a8] {\n transition: all 0.5s ease;\n}\n.tinder-rewind-leave-to[data-v-1776a5a8] {\n opacity: 0;\n}\n",map:void 0,media:void 0})},h,"data-v-1776a5a8",!1,void 0,!1,i,void 0,void 0)},props:{allowSuper:{type:Boolean,default:!0},allowDown:{type:Boolean,default:!1},queue:{type:Array,default:function(){return[]}},keyName:{type:String,default:"key"},pointerThreshold:{type:Number,default:.5},superThreshold:{type:Number,default:.5},downThreshold:{type:Number,default:.5},sync:{type:Boolean,default:!1},max:{type:Number,default:3},scaleStep:{type:Number,default:.05},offsetY:{type:Number,default:0},offsetUnit:{type:String,default:"px"}},data:function(){return{size:{top:0,width:0,height:0},state:e(),list:[],tinderMounted:!1}},computed:{status:function(){return this.state.status},ratio:function(){if(this.size.width){var t=this.state,e=t.start,n=(t.move.x-e.x||0)/(.5*this.size.width);return n}return 0},pointerOpacity:function(){return this.ratio/this.pointerThreshold},disY:function(){return this.allowSuper||this.allowDown?this.state.move.y-this.state.start.y:0},superOpacity:function(){if(!this.allowSuper)return 0;var t=this.disY/(-this.superThreshold*this.size.height);return Math.abs(this.pointerOpacity) 1%", 78 | "last 2 versions" 79 | ] 80 | } 81 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shanlh/vue-tinder/b3887b49ac71cf0e94c16b03ed1d434d50c5dd8d/public/favicon.ico -------------------------------------------------------------------------------- /public/images/down-txt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shanlh/vue-tinder/b3887b49ac71cf0e94c16b03ed1d434d50c5dd8d/public/images/down-txt.png -------------------------------------------------------------------------------- /public/images/help.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shanlh/vue-tinder/b3887b49ac71cf0e94c16b03ed1d434d50c5dd8d/public/images/help.png -------------------------------------------------------------------------------- /public/images/like-txt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shanlh/vue-tinder/b3887b49ac71cf0e94c16b03ed1d434d50c5dd8d/public/images/like-txt.png -------------------------------------------------------------------------------- /public/images/like.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shanlh/vue-tinder/b3887b49ac71cf0e94c16b03ed1d434d50c5dd8d/public/images/like.png -------------------------------------------------------------------------------- /public/images/nope-txt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shanlh/vue-tinder/b3887b49ac71cf0e94c16b03ed1d434d50c5dd8d/public/images/nope-txt.png -------------------------------------------------------------------------------- /public/images/nope.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shanlh/vue-tinder/b3887b49ac71cf0e94c16b03ed1d434d50c5dd8d/public/images/nope.png -------------------------------------------------------------------------------- /public/images/rewind-txt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shanlh/vue-tinder/b3887b49ac71cf0e94c16b03ed1d434d50c5dd8d/public/images/rewind-txt.png -------------------------------------------------------------------------------- /public/images/rewind.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shanlh/vue-tinder/b3887b49ac71cf0e94c16b03ed1d434d50c5dd8d/public/images/rewind.png -------------------------------------------------------------------------------- /public/images/super-like.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shanlh/vue-tinder/b3887b49ac71cf0e94c16b03ed1d434d50c5dd8d/public/images/super-like.png -------------------------------------------------------------------------------- /public/images/super-txt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shanlh/vue-tinder/b3887b49ac71cf0e94c16b03ed1d434d50c5dd8d/public/images/super-txt.png -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | vue-tinder 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /public/preview.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shanlh/vue-tinder/b3887b49ac71cf0e94c16b03ed1d434d50c5dd8d/public/preview.gif -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | set -e 2 | echo "Enter release version: " 3 | read VERSION 4 | 5 | read -p "Releasing $VERSION - are you sure? (y/n)" -n 1 -r 6 | echo # (optional) move to a new line 7 | if [[ $REPLY =~ ^[Yy]$ ]] 8 | then 9 | echo "Releasing $VERSION ..." 10 | 11 | # build 12 | yarn build 13 | 14 | # commit 15 | git add -A 16 | git commit -m "Build for $VERSION" 17 | npm version $VERSION -m "Upgrade to $VERSION" 18 | 19 | # publish 20 | npm publish 21 | echo "Publish $VERSION successfully!" 22 | 23 | # push 24 | git push origin refs/tags/v$VERSION 25 | git push 26 | 27 | echo "Done!" 28 | fi -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import resolve from 'rollup-plugin-node-resolve' 2 | import commonjs from 'rollup-plugin-commonjs' 3 | import vue from 'rollup-plugin-vue' 4 | import babel from 'rollup-plugin-babel' 5 | import { uglify } from 'rollup-plugin-uglify' 6 | 7 | export default { 8 | input: 'src/components/index.js', 9 | output: { 10 | name: 'vue-tinder', 11 | file: 'lib/vue-tinder.js', 12 | format: 'umd' 13 | }, 14 | plugins: [ 15 | resolve(), 16 | commonjs({ sourceMap: false }), 17 | vue({ needMap: false }), 18 | babel({ 19 | runtimeHelpers: true, 20 | extensions: ['.js', '.jsx', '.es6', '.es', '.mjs', '.vue'] 21 | }), 22 | uglify() 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 100 | 101 | 233 | -------------------------------------------------------------------------------- /src/assets/help.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shanlh/vue-tinder/b3887b49ac71cf0e94c16b03ed1d434d50c5dd8d/src/assets/help.png -------------------------------------------------------------------------------- /src/assets/history.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shanlh/vue-tinder/b3887b49ac71cf0e94c16b03ed1d434d50c5dd8d/src/assets/history.png -------------------------------------------------------------------------------- /src/assets/like-txt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shanlh/vue-tinder/b3887b49ac71cf0e94c16b03ed1d434d50c5dd8d/src/assets/like-txt.png -------------------------------------------------------------------------------- /src/assets/like.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shanlh/vue-tinder/b3887b49ac71cf0e94c16b03ed1d434d50c5dd8d/src/assets/like.png -------------------------------------------------------------------------------- /src/assets/nope-txt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shanlh/vue-tinder/b3887b49ac71cf0e94c16b03ed1d434d50c5dd8d/src/assets/nope-txt.png -------------------------------------------------------------------------------- /src/assets/nope.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shanlh/vue-tinder/b3887b49ac71cf0e94c16b03ed1d434d50c5dd8d/src/assets/nope.png -------------------------------------------------------------------------------- /src/assets/super-like.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shanlh/vue-tinder/b3887b49ac71cf0e94c16b03ed1d434d50c5dd8d/src/assets/super-like.png -------------------------------------------------------------------------------- /src/assets/super-txt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shanlh/vue-tinder/b3887b49ac71cf0e94c16b03ed1d434d50c5dd8d/src/assets/super-txt.png -------------------------------------------------------------------------------- /src/bing.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | 'AdelieBreeding_ZH-CN1750945258', 3 | 'BarcolanaTrieste_ZH-CN5745744257', 4 | 'RedRocksArches_ZH-CN5664546697', 5 | 'NationalDay70_ZH-CN1636316274', 6 | 'LofotenSurfing_ZH-CN5901239545', 7 | 'UgandaGorilla_ZH-CN5826117482', 8 | 'FeatherSerpent_ZH-CN5706017355', 9 | 'VancouverFall_ZH-CN9824386829', 10 | 'WallofPeace_ZH-CN5582031878', 11 | 'SanSebastianFilm_ZH-CN5506786379', 12 | 'CommonLoon_ZH-CN5437917206', 13 | 'SunbeamsForest_ZH-CN5358008117', 14 | 'StokePero_ZH-CN5293082939', 15 | 'Wachsenburg_ZH-CN5224299503', 16 | 'SurfboardRow_ZH-CN5154549470', 17 | 'ToothWalkingSeahorse_ZH-CN5089043566', 18 | 'midmoon_ZH-CN4973736313', 19 | 'MilkyWayCanyonlands_ZH-CN2363274510', 20 | 'DaintreeRiver_ZH-CN2284362798', 21 | 'TsavoGerenuk_ZH-CN2231549718', 22 | 'ArroyoGrande_ZH-CN2178202888', 23 | 'SouthernYellow_ZH-CN2055825919', 24 | 'MountFanjing_ZH-CN1999613800', 25 | 'ElMorro_ZH-CN1911346184', 26 | 'Tegallalang_ZH-CN1855493751', 27 | 'AutumnTreesNewEngland_ZH-CN1766405773', 28 | 'SquirrelHeather_ZH-CN1683129884', 29 | 'RamsauWimbachklamm_ZH-CN1602837695', 30 | 'Castelbouc_ZH-CN1475157551', 31 | 'Slackers_ZH-CN1408656264', 32 | 'AsburyParkNJ_ZH-CN1353073841', 33 | 'HardeeCoFair_ZH-CN8647295545', 34 | 'CorsiniGardens_ZH-CN8547012221', 35 | 'Krakatoa_ZH-CN8471800710', 36 | 'ParrotsIndia_ZH-CN8386276023', 37 | 'WinnatsPass_ZH-CN8251326840', 38 | 'AugustBears_ZH-CN8159736622', 39 | 'FarmlandLandscape_ZH-CN8021236701', 40 | 'DubaiFountain_ZH-CN7944507087', 41 | 'MaraRiverCrossing_ZH-CN6598585392', 42 | 'FinlandCamping_ZH-CN6418764403', 43 | 'Feringasee_ZH-CN6335425001', 44 | 'MagdalenCave_ZH-CN6279630125', 45 | 'DrinkingNectar_ZH-CN6196689688', 46 | 'GoldRushYukon_ZH-CN6132080652', 47 | 'SmogenSweden_ZH-CN0457682922', 48 | 'HornedAnole_ZH-CN0388959247', 49 | 'MartianSouthPole_ZH-CN0324422893', 50 | 'AmboseliHerd_ZH-CN0249135007', 51 | 'TRNPThunderstorm_ZH-CN0178957327', 52 | 'TrianaBridge_ZH-CN0107319931', 53 | 'KluaneAspen_ZH-CN0028056280', 54 | 'LinyantiLeopard_ZH-CN9934758728', 55 | 'qixi_ZH-CN3534017617', 56 | 'WhiteStorksNest_ZH-CN9809680903', 57 | 'ApostleIslands_ZH-CN9543695883', 58 | 'SwiftFox_ZH-CN9413097062', 59 | 'UhuRLP_ZH-CN5421658032', 60 | 'CrummockWater_ZH-CN9317792500', 61 | 'LavaFlows_ZH-CN4235925500', 62 | 'TreeTower_ZH-CN4181961177', 63 | 'TortoiseMigration_ZH-CN4128473636', 64 | 'TrilliumLake_ZH-CN4079462365', 65 | 'PuffinSkomer_ZH-CN4039641381', 66 | 'CahuitaNP_ZH-CN3985565209', 67 | 'ElkFallsBridge_ZH-CN3921681387', 68 | 'CathedralMountBuffalo_ZH-CN4341947983', 69 | 'MeerkatMob_ZH-CN3788674757', 70 | 'Skywalk_ZH-CN3725661090', 71 | 'SardiniaHawkMoth_ZH-CN3672906054', 72 | 'BuckinghamSummer_ZH-CN3519250117', 73 | 'MiquelonPanorama_ZH-CN3614818937', 74 | 'GodsGarden_ZH-CN3317703606', 75 | 'LeatherbackTT_ZH-CN5495532728', 76 | 'Narrenmuehle_ZH-CN5582540867', 77 | 'VulpesVulpes_ZH-CN5650159325', 78 | 'Ushitukiiwa_ZH-CN5710944706', 79 | 'WaterperryGardens_ZH-CN5767279278', 80 | 'CradleMountain_ZH-CN5817437189', 81 | 'NightofNights_ZH-CN5872572560', 82 | 'IndiaLitSpace_ZH-CN5941074986', 83 | 'JaguarPantanal_ZH-CN6062516404', 84 | 'ChefchaouenMorocco_ZH-CN6127993429', 85 | 'WesternArcticHerd_ZH-CN6254887608', 86 | 'SommerCalviCorsica_ZH-CN6313433064', 87 | 'PeelCastle_ZH-CN6366204379', 88 | 'Transfagarasan_ZH-CN5760731327', 89 | 'BailysBeads_ZH-CN5728297739', 90 | 'HKreuni_ZH-CN5683726370', 91 | 'RedAnthiasCoralMayotte_ZH-CN5646370533', 92 | 'BurrowingOwlet_ZH-CN5583013899', 93 | 'Montreux_ZH-CN5485205583', 94 | 'RootBridge_ZH-CN5173953292', 95 | 'GlastonburyTor_ZH-CN4673691420', 96 | 'SutherlandFalls_ZH-CN4602884079', 97 | 'PhilippinesFirefly_ZH-CN4519927697', 98 | 'Gnomesville_ZH-CN4402652527', 99 | 'ManausBasin_ZH-CN4303809335', 100 | 'HawksbillCrag_ZH-CN4429681235', 101 | 'CommonSundewVosges_ZH-CN0507660055', 102 | 'CherryLaurelMaze_ZH-CN9887470516', 103 | 'HelixPomatia_ZH-CN9785223494', 104 | 'AlaskaEagle_ZH-CN9957205086', 105 | 'PantheraLeoDad_ZH-CN9580668524', 106 | 'SaskFlowers_ZH-CN9497517721', 107 | 'TreeFrog_ZH-CN9016355758', 108 | 'SainteVictoireCezanneBirthday_ZH-CN8216109812', 109 | 'RioGrande_ZH-CN8091224199', 110 | 'FujiSakura_ZH-CN8005792871', 111 | 'PontadaPiedade_ZH-CN7717691454', 112 | 'OntWarbler_ZH-CN7999782156', 113 | 'Biorocks_ZH-CN7851264095', 114 | 'dragonboat_ZH-CN0697680986', 115 | 'MulberryArtificialHarbour_ZH-CN3973249802', 116 | 'PeruvianRainforest_ZH-CN4066508593', 117 | 'VastPalmGrove_ZH-CN4145018538', 118 | 'HeligolandSealPup_ZH-CN4217382978', 119 | 'BassRock_ZH-CN4418828352', 120 | 'HighTrestleTrail_ZH-CN4499525731', 121 | 'ZumwaltPrairie_ZH-CN4572430876', 122 | 'Manhattanhenge_ZH-CN4659585143', 123 | 'BlumenwieseNRW_ZH-CN4774429225', 124 | 'NFLDfog_ZH-CN4846953507' 125 | ] 126 | -------------------------------------------------------------------------------- /src/components/index.js: -------------------------------------------------------------------------------- 1 | import Tinder from './vue-tinder/Tinder.vue' 2 | 3 | Tinder.install = Vue => { 4 | Vue.component('vue-tinder', Tinder) 5 | } 6 | 7 | export default Tinder 8 | 9 | if (typeof window !== 'undefined' && window.Vue) { 10 | window.Vue.component('tinder', Tinder) 11 | } 12 | -------------------------------------------------------------------------------- /src/components/vue-tinder/Tinder.vue: -------------------------------------------------------------------------------- 1 | 79 | 80 | 266 | 267 | 298 | -------------------------------------------------------------------------------- /src/components/vue-tinder/TinderCard.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 194 | 195 | 214 | -------------------------------------------------------------------------------- /src/components/vue-tinder/open-methods.js: -------------------------------------------------------------------------------- 1 | import { STATUS } from './status' 2 | 3 | export default { 4 | data: () => ({ 5 | rewindKeys: [] 6 | }), 7 | methods: { 8 | /** 9 | * 点击按钮做选择 10 | * @param {String} type like:喜欢,nope:不喜欢,super:超喜欢,down:向下 11 | */ 12 | decide(type) { 13 | if (this.state.touchId || this.status !== STATUS.NORMAL) { 14 | return 15 | } 16 | this.state.start = { x: 0, y: 0 } 17 | this.state.move = { 18 | x: type === 'super' || type === 'down' ? 0 : type === 'like' ? 1 : -1, 19 | y: type === 'super' ? -1 : type === 'down' ? 1 : 0 20 | } 21 | this.state.startPoint = 1 22 | this.shiftCard(type) 23 | }, 24 | /** 25 | * 恢复一个列表 26 | * @param {Array} list 27 | */ 28 | rewind(list) { 29 | const keyName = this.keyName 30 | // TODO: 其实可以换个地方把 id 放置进去,目前这么做主要是为了后期可以配置 rewind 的来源位置 31 | for (const item of list) { 32 | this.rewindKeys.push(item[keyName] + '') // 避免数字类型的 id 引起后续判断不匹配 33 | } 34 | this.queue.unshift(...list) 35 | }, 36 | /***************** 以下方法不对外开放,对 queue 操作请用以上的函数 *****************/ 37 | /** 38 | * 把卡片移除 39 | * @param {String} type 移除方式,like:喜欢,nope:不喜欢,super:超喜欢,down:向下 40 | */ 41 | shiftCard(type) { 42 | this.state.status = STATUS.LEAVING 43 | this.state.result = type 44 | const item = this.queue.shift() 45 | this.$emit('update:queue', this.queue) 46 | this.submitDecide(type, item) 47 | }, 48 | /** 49 | * 提交选择 50 | * @param {Boolean} type 类型,like:喜欢,nope:不喜欢,super:超喜欢,down:向下 51 | * @param {String} key 当前卡片的key 52 | * @param {Object} item 卡片对象 53 | */ 54 | submitDecide(type, item) { 55 | this.$emit('submit', { type, key: item[this.keyName], item }) 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/components/vue-tinder/queue-handle.js: -------------------------------------------------------------------------------- 1 | import { STATUS } from './status' 2 | 3 | const difference = (array, exclude) => { 4 | const result = [] 5 | for (let i = 0; i < array.length; i++) { 6 | if (exclude.indexOf(array[i]) > -1) { 7 | break 8 | } 9 | result.push(array[i]) 10 | } 11 | return result 12 | } 13 | 14 | export default { 15 | data: () => ({ 16 | /** 17 | * 正在被移除的 key,目前只管插入,没做移除后的删除处理 18 | */ 19 | leavingKeys: [], 20 | /** 21 | * 本次 rewind 的数量,每次 diff 更新,会在动画移除时有用,用于决定 card 隐藏后的目标状态 22 | */ 23 | onceRewindCount: 0 24 | }), 25 | methods: { 26 | /** 27 | * 需要区分数组变化是增加还是减少 28 | * @param {Array} list 29 | * @param {Array} old 30 | */ 31 | diff(list, old) { 32 | // 新增或 rewind 33 | const keyName = this.keyName 34 | const add = difference(list, old) 35 | let onceRewindCount = 0 36 | if (add.length) { 37 | for (let i = 0; i < add.length; i++) { 38 | const item = this.queue[i] 39 | if (item[keyName] && add[i] === item[keyName]) { 40 | onceRewindCount++ 41 | const id = item[keyName] 42 | const newVueTinderkey = id + Math.random() 43 | if ( 44 | this.leavingKeys.indexOf(item.$vtKey) > -1 || 45 | this.leavingKeys.indexOf(id) > -1 || 46 | this.rewindKeys.indexOf(item.$vtKey) > -1 || 47 | this.rewindKeys.indexOf(id) > -1 48 | ) { 49 | // 已经移除过再出现,为了避免 dom 被重用中断了之前的消失动画,需要给一个新的 key 50 | item.$vtKey = newVueTinderkey 51 | // 因为在 beforeEnter 中,存入 rewindKeys 中的是 data-id, 52 | // 而 data-id 以 $vtKey 为更高优先级,如果直接将之前移除过的对象重新 rewind, 53 | // 则有很大的可能是本身存在 $vtKey 属性的,所以单单 indexOf 其 id 是不一定能找到的 54 | // 所以还需要查找 $vtKey,并为了保险起见,也需要赋值一个新的 $vtKey 55 | const rewindIndex = Math.max( 56 | this.rewindKeys.indexOf(item.$vtKey), 57 | this.rewindKeys.indexOf(id) 58 | ) 59 | if (rewindIndex > -1) { 60 | this.rewindKeys[rewindIndex] = newVueTinderkey 61 | this.state.status = STATUS.REWINDING 62 | } 63 | } 64 | } else { 65 | break 66 | } 67 | } 68 | } 69 | this.onceRewindCount = onceRewindCount 70 | 71 | // 移除 72 | const remove = difference(old, list) 73 | if (remove.length) { 74 | // 这边只考虑了移除一个的情况,手动移除头部的情况不负责,应该避免手动操作队列,除了向后追加 75 | this.leavingKeys.push(this.list[0].$vtKey || this.list[0][keyName]) 76 | for (let i = this.max + 1; i < this.max + 1 + remove.length; i++) { 77 | const item = this.list[i] 78 | if (item) { 79 | if ( 80 | this.leavingKeys.indexOf(item[keyName]) > -1 || 81 | // 被隐藏,但即将出现的 item,需要创建 $vtKey,避免出现正在隐藏的情况(与刚出来的 key 冲突) 82 | this.hidingKeys.indexOf(item[keyName]) > -1 83 | ) { 84 | item.$vtKey = item[keyName] + Math.random() 85 | } 86 | } 87 | } 88 | } 89 | 90 | this.list = this.queue.slice(0) 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/components/vue-tinder/status.js: -------------------------------------------------------------------------------- 1 | export const initStatus = revert => ({ 2 | status: revert ? 3 : 0, 3 | touchId: null, 4 | start: {}, 5 | move: {}, 6 | startPoint: 1, // 手指落在卡片的上半部分(1),下半部分(-1) 7 | result: null 8 | }) 9 | 10 | export const STATUS = { 11 | NORMAL: 0, 12 | MOVING: 1, 13 | LEAVING: 2, 14 | REVERT: 3, 15 | REWINDING: 4 16 | } 17 | -------------------------------------------------------------------------------- /src/components/vue-tinder/touch-event.js: -------------------------------------------------------------------------------- 1 | import { STATUS, initStatus } from './status' 2 | 3 | export default { 4 | methods: { 5 | /** 6 | * 开始移动 7 | * @param {Object} e 触摸/鼠标事件 8 | */ 9 | start(e) { 10 | const state = this.state 11 | if ( 12 | state.touchId !== null || 13 | this.status === STATUS.LEAVING || 14 | this.status === STATUS.REVERT || 15 | this.status === STATUS.REWINDING 16 | ) { 17 | return 18 | } 19 | let pageX, pageY 20 | if (e.type === 'touchstart') { 21 | pageX = e.changedTouches[0].pageX 22 | pageY = e.changedTouches[0].pageY 23 | // TODO: iOS侧滑返回区域,不应该继续,这个区域还需要调整,有必要的话还要区分下iOS/Android 24 | // if (pageX < ?) { 25 | // return 26 | // } 27 | } else { 28 | pageX = e.clientX 29 | pageY = e.clientY 30 | } 31 | // 判断触摸起始位置在卡片的上部还是下部 32 | const top = this.size.top 33 | const height = this.size.height 34 | const centerY = top + height / 2 35 | const startPoint = pageY > centerY ? -1 : 1 36 | // 初始化 37 | this.state = { 38 | status: STATUS.MOVING, 39 | touchId: 40 | e.type === 'touchstart' ? e.changedTouches[0].identifier : 'mouse', 41 | start: { 42 | x: pageX, 43 | y: pageY 44 | }, 45 | move: Object.create(null), 46 | startPoint, 47 | result: null 48 | } 49 | }, 50 | /** 51 | * 移动卡片 52 | * @param {Object} e 触摸/鼠标事件 53 | */ 54 | move(e) { 55 | e.preventDefault() 56 | const state = this.state 57 | if ( 58 | state.touchId === null || 59 | this.status === STATUS.LEAVING || 60 | this.status === STATUS.REVERT || 61 | this.status === STATUS.REWINDING || 62 | (e.type === 'touchmove' && 63 | state.touchId !== e.changedTouches[0].identifier) 64 | ) { 65 | return 66 | } 67 | let pageX, pageY 68 | if (e.type === 'touchmove') { 69 | pageX = e.changedTouches[0].pageX 70 | pageY = e.changedTouches[0].pageY 71 | } else { 72 | pageX = e.clientX 73 | pageY = e.clientY 74 | } 75 | state.move = { 76 | x: pageX, 77 | y: pageY 78 | } 79 | }, 80 | /** 81 | * 移动结束,分析行为 82 | * @param {Object} e 触摸/鼠标事件 83 | */ 84 | end(e) { 85 | if ( 86 | e.type === 'touchstart' && 87 | this.state.touchId !== e.changedTouches[0].identifier 88 | ) { 89 | return 90 | } 91 | if ( 92 | this.status === STATUS.LEAVING || 93 | this.status === STATUS.REVERT || 94 | this.status === STATUS.REWINDING 95 | ) { 96 | return 97 | } 98 | if ( 99 | Math.abs(this.pointerOpacity) >= 1 || 100 | this.superOpacity >= 1 || 101 | this.downOpacity >= 1 102 | ) { 103 | const result = 104 | this.superOpacity >= 1 105 | ? 'super' 106 | : this.downOpacity >= 1 107 | ? 'down' 108 | : this.pointerOpacity > 0 109 | ? 'like' 110 | : 'nope' 111 | this.shiftCard(result) 112 | } else if (this.status === STATUS.MOVING) { 113 | // 操作取消,回归原位,回归原位后 status 会通过 TinderCard 通知 Tinder 将 status 重置为 0 114 | this.state = initStatus('reverted') 115 | } 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/components/vue-tinder/transition-event.js: -------------------------------------------------------------------------------- 1 | import { STATUS } from './status' 2 | 3 | export default { 4 | data: () => ({ 5 | /** 6 | * 已移除的 card 数量: 7 | * 每个移除的 z-index 会从基础的 100 提升到 10000 左右, 8 | * 以避免与当前显示 card 的 z-index 重叠 9 | */ 10 | leavedCount: 0, 11 | /** 12 | * 被隐藏 card 的 z-index 与最新被设置的 index,从 50 开始变化 13 | */ 14 | hideIndex: 50, 15 | lastHideIndex: 50, 16 | /** 17 | * 存储因为 z-index 大于 max 而隐藏过的 key: 18 | * 用于 diff 函数中的 remove 部分,避免在隐藏过程中又有 card 被执行移除操作, 19 | * 而 key 没有改变,导致 dom 被重用,动画过渡不自然的问题 20 | */ 21 | hidingKeys: [] 22 | }), 23 | methods: { 24 | beforeEnter(el) { 25 | const beforeIndex = el.dataset.index - 0 + 1 26 | el.style.opacity = 0 27 | el.style.transform = this.getTransform(beforeIndex) 28 | if (this.rewindKeys.indexOf(el.dataset.id) > -1) { 29 | // 这里与 leave 函数中,卡片被移除后目的地的计算方式相同 30 | let x = -1 // 从左边 rewind 31 | x += this.size.width * (x < 0 ? -0.5 : 0.5) 32 | const ratio = x / (this.size.width * 0.5) 33 | const rotate = (ratio / (0.8 / 0.5)) * 15 * 1 34 | el.style.transform = `translate3d(${x}px, 0, 0) rotate(${rotate}deg)` 35 | } 36 | el.style.transition = 'all 0s' 37 | }, 38 | /** 39 | * 当前卡片正在离开 40 | * 当只用 JavaScript 过渡的时候, 在 enter 和 leave 中,回调函数 done 是必须的 。 41 | * 否则,它们会被同步调用,过渡会立即完成。 42 | * @param {element} el 当前卡片 43 | * @param {Function} done 回调函数 44 | */ 45 | leave(el, done) { 46 | const state = this.state 47 | const { start, move, startPoint } = state 48 | let x = move.x - start.x || 0 49 | let y = move.y - start.y || 0 50 | if (state.result === 'super') { 51 | y -= this.size.width 52 | } else if (state.result === 'down') { 53 | y += this.size.width 54 | } else { 55 | x += this.size.width * (x < 0 ? -0.5 : 0.5) 56 | y *= x / (move.x - start.x) 57 | } 58 | const ratio = x / (this.size.width * 0.5) // 不能直接使用 this.ratio,因为 x、y 被微调过 59 | const rotate = (ratio / (0.8 / 0.5)) * 15 * startPoint 60 | let duration = 61 | state.touchId === null || 62 | state.result === 'super' || 63 | state.result === 'down' 64 | ? 800 65 | : 300 66 | el.style.opacity = 0 67 | el.style['pointer-events'] = 'none' 68 | if (this.leavingKeys.indexOf(el.dataset.id) > -1) { 69 | // 操作移除 70 | el.className += ` ${state.result}` 71 | el.style.transform = `translate3d(${x}px,${y}px,0) rotate(${rotate}deg)` 72 | // 保证出队列的 z-index 正确(先出的在上) 73 | el.style.zIndex = 1000000 - this.leavedCount++ 74 | } else { 75 | // 因执行 rewind 操作后,index 大于 max 而需被隐藏 76 | this.hidingKeys.push(el.dataset.id) 77 | duration = 500 78 | const index = 79 | Math.min(this.max, this.onceRewindCount) + (el.dataset.index - 0) 80 | el.style.transform = this.getTransform(index) 81 | el.style.zIndex = this.getHideIndex(el.dataset.index - 0) 82 | } 83 | el.style.transition = `all ${duration}ms ${ 84 | duration === 500 ? 'cubic-bezier(0.175, 0.885, 0.32, 1.275)' : 'ease' 85 | },z-index 0s` 86 | el.addEventListener('transitionend', e => { 87 | if (e.propertyName === 'transform') { 88 | // 重置 hideIndex,避免与主要卡片的层级冲突 89 | if (this.lastHideIndex === el.style.zIndex - 0) { 90 | this.lastHideIndex = 50 91 | this.hideIndex = 50 92 | } 93 | if ( 94 | this.sync && 95 | (this.status === STATUS.NORMAL || this.status === STATUS.LEAVING) 96 | ) { 97 | this.resetStatus() 98 | } 99 | done() 100 | } 101 | }) 102 | if ( 103 | !this.sync && 104 | el.dataset.index - 0 === 0 && 105 | this.status !== STATUS.REWINDING 106 | ) { 107 | this.resetStatus() 108 | } 109 | }, 110 | /** 111 | * 根据不同情况(同时 rewind 多个、逐个 rewind) 112 | * 给大于 max 而隐藏的 card 一个正确的 z-index 113 | * 将最后一个当前的 index 储存起来,在 leave 中重置,避免与主要卡片的层级冲突 114 | * @param {Number} index 115 | */ 116 | getHideIndex(index) { 117 | const max = this.max 118 | let cur 119 | if (index === max) { 120 | if (this.lastHideIndex > this.hideIndex) { 121 | // 说明之前有过层级更高的,到这边需要重新把 hidexIndex 恢复到最上一级的 index 以避免后续冲突 122 | cur = this.hideIndex 123 | this.hideIndex += 1 + max 124 | } else { 125 | cur = this.hideIndex++ 126 | } 127 | } else { 128 | cur = this.hideIndex + max - index 129 | } 130 | this.lastHideIndex = cur 131 | return cur 132 | }, 133 | getTransform(index) { 134 | const scale = 1 - this.scaleStep * index 135 | let translateY = 0 136 | if (this.offsetY) { 137 | const inverse = this.offsetY < 0 138 | const offsetY = Math.abs(this.offsetY) 139 | let y = index * offsetY 140 | let offsetScale = ((1 - scale) / 2) * 100 141 | if (inverse) { 142 | y *= -1 143 | offsetScale *= -1 144 | } 145 | translateY = `calc(${offsetScale}% + ${y}${this.offsetUnit})` 146 | } 147 | return `translate3d(0,${translateY},0) scale3d(${scale},${scale},1)` 148 | } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | 4 | Vue.config.productionTip = false 5 | 6 | new Vue({ 7 | render: h => h(App) 8 | }).$mount('#app') 9 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | 3 | function resolve(dir) { 4 | return path.join(__dirname, dir) 5 | } 6 | 7 | module.exports = { 8 | configureWebpack: { 9 | resolve: { 10 | alias: { 11 | img: resolve('public/images') 12 | } 13 | }, 14 | output: { 15 | libraryExport: 'default' 16 | } 17 | }, 18 | css: { 19 | extract: false 20 | } 21 | } 22 | --------------------------------------------------------------------------------