├── .github └── workflows │ └── deploy.yml ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── LICENSE ├── README.md ├── commitlint.config.js ├── package.json ├── packages ├── vue3-scroll-seamless-docs │ ├── docs │ │ ├── .vitepress │ │ │ ├── components │ │ │ │ ├── echarts.min.js │ │ │ │ ├── example01.vue │ │ │ │ ├── example02.vue │ │ │ │ ├── example03.vue │ │ │ │ └── example04.vue │ │ │ ├── config.ts │ │ │ └── theme │ │ │ │ └── index.ts │ │ ├── guide │ │ │ ├── 01-basic.md │ │ │ ├── 02-direction-bottom.md │ │ │ ├── 03-direction-right.md │ │ │ ├── 04-step.md │ │ │ ├── 05-hoverStop.md │ │ │ ├── 06-singleStop.md │ │ │ ├── 07-singleStopTime.md │ │ │ ├── 08-echart.md │ │ │ ├── 09-array-property-update.md │ │ │ ├── 10-array-length-update.md │ │ │ ├── 11-daynamicData.md │ │ │ ├── a-switch.md │ │ │ ├── events.md │ │ │ ├── index.md │ │ │ ├── issuesSolution.md │ │ │ ├── notice.md │ │ │ ├── properties.md │ │ │ └── usage.md │ │ └── index.md │ ├── package.json │ └── shims.vue.d.ts └── vue3-scroll-seamless │ ├── .eslintignore │ ├── .eslintrc.cjs │ ├── dist │ ├── vue3-scroll-seamless.iife.js │ ├── vue3-scroll-seamless.mjs │ └── vue3-scroll-seamless.umd.js │ ├── index.html │ ├── package.json │ ├── src │ ├── _test_ │ │ └── seamlessScroll.spec.ts │ ├── components │ │ └── seamlessScroll.vue │ ├── demo.vue │ ├── entry.ts │ ├── index.ts │ ├── shims.vue.d.ts │ └── utils │ │ └── index.ts │ ├── tsconfig.json │ └── vite.config.ts ├── pnpm-lock.yaml ├── pnpm-workspace.yaml └── scripts └── preinstall.js /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | deploy: 10 | strategy: 11 | matrix: 12 | node-version: [16] 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v3 16 | - uses: pnpm/action-setup@v2.2.2 17 | with: 18 | version: 7 19 | - name: Use Node.js ${{ matrix.node-version }} 20 | uses: actions/setup-node@v3 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | cache: 'pnpm' 24 | - name: Install dependencies 25 | run: pnpm install --no-frozen-lockfile 26 | - name: Run Build 27 | run: cd packages/vue3-scroll-seamless-docs&&pnpm docs:build 28 | - name: Deploy 29 | uses: peaceiris/actions-gh-pages@v3 30 | with: 31 | github_token: ${{ secrets.GITHUB_TOKEN }} 32 | publish_dir: packages/vue3-scroll-seamless-docs/docs/.vitepress/dist 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/node_modules/ 2 | **/coverage/ 3 | **/.pnpm-debug 4 | package-lock.json 5 | .DS_Store 6 | .spress 7 | .vercel 8 | .idea 9 | /test 10 | *.zip -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | echo ====代码提交验证==== 5 | npx --no -- commitlint --edit "$1" 6 | 7 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | echo ====代码静态检查==== 5 | cd packages/vue3-scroll-seamless 6 | pnpm run lint -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022-present xiaofulzm 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 |

vue3-scroll-seamless

2 | 3 |

4 | 基于vue3简单的无缝滚动 5 | 6 |

7 | 8 | 9 | 10 | ## Features 11 | 12 | * 初始配置支持 13 | * 兼容多种平台 14 | * 多技术栈版本支持 15 | 16 | ## Documentation 17 | 要查看 [实时示例](https://xiaofulzm.github.io/vue3-scroll-seamless/guide/01-basic.html) 和文档,请访问 [vue3-scroll-seamless-docs](https://xiaofulzm.github.io/vue3-scroll-seamless/)。 18 | 19 | ## Cares 20 | 原作者库以及js版本,可以切换到这里[chenxuan0000](https://github.com/chenxuan0000/vue-seamless-scroll), 本人只是重构了vue3版本 21 | 22 | ## Contribution 23 | 欢迎大家提出建议和优化,期待你的`Pull Request`。 24 | 25 | ## License 26 | [ MIT License ](LICENSE) 27 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = {extends: ['@commitlint/config-conventional']} 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue3-scroll-seamless", 3 | "version": "1.0.7", 4 | "description": "A simple, Seamless scrolling for Vue3.js", 5 | "homepage": "https://xiaofulzm.github.io/vue3-scroll-seamless/", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://xiaofulzm.github.io/vue3-scroll-seamless/" 9 | }, 10 | "main": "index.js", 11 | "scripts": { 12 | "preinstall": "node ./scripts/preinstall.js", 13 | "prepare": "husky install" 14 | }, 15 | "keywords": [ 16 | "vue", 17 | "vuejs", 18 | "ui", 19 | "components", 20 | "Seamless", 21 | "scroll" 22 | ], 23 | "author": "luxiaofu", 24 | "license": "ISC", 25 | "dependencies": { 26 | "@commitlint/cli": "17.0.2", 27 | "@commitlint/config-conventional": "17.0.2", 28 | "husky": "^8.0.1", 29 | "vite": "^3.1.4", 30 | "vitepress": "1.0.0-alpha.21", 31 | "vue": "^3.2.40" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/vue3-scroll-seamless-docs/docs/.vitepress/components/example01.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 54 | 87 | -------------------------------------------------------------------------------- /packages/vue3-scroll-seamless-docs/docs/.vitepress/components/example02.vue: -------------------------------------------------------------------------------- 1 | 65 | 66 | 84 | 113 | -------------------------------------------------------------------------------- /packages/vue3-scroll-seamless-docs/docs/.vitepress/components/example03.vue: -------------------------------------------------------------------------------- 1 | 54 | 55 | 74 | 116 | -------------------------------------------------------------------------------- /packages/vue3-scroll-seamless-docs/docs/.vitepress/components/example04.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 44 | 94 | -------------------------------------------------------------------------------- /packages/vue3-scroll-seamless-docs/docs/.vitepress/config.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | const config = { 4 | base:'/vue3-scroll-seamless/', 5 | title:'vue3-scroll-seamless', 6 | themeConfig:{ 7 | siteTitle: 'vue3-scroll-seamless', 8 | nav: [ 9 | { text: '指南', link: '/guide/' }, 10 | { text: '更新日志', link: '/configs' }, 11 | { 12 | text: '其他版本', 13 | items: [ 14 | { text: 'Vue2版本', link: 'https://github.com/chenxuan0000/vue-seamless-scroll' }, 15 | { text: 'JavaScript版本', link: 'https://github.com/chenxuan0000/seamless-scroll' }, 16 | ] 17 | }, 18 | ], 19 | socialLinks: [ 20 | { icon: 'github', link: 'https://github.com/xiaofulzm/vue3-scroll-seamless' } 21 | ], 22 | sidebar: {// 侧边栏 23 | '/guide/': [ 24 | { 25 | text: '指南', 26 | items: [ 27 | { text: '安装', link: '/guide/' }, 28 | { text: '使用', link: '/guide/usage' }, 29 | { text: '组件配置', link: '/guide/properties' }, 30 | { text: '回调事件', link: '/guide/events' }, 31 | { text: '注意项', link: '/guide/notice' }, 32 | { text: '常见Issues', link: '/guide/issuesSolution' } 33 | ] 34 | }, 35 | { 36 | text: '示例', 37 | items: [ 38 | { text: '01 - 默认配置', link: '/guide/01-basic' }, 39 | { text: '02 - 向下滚动', link: '/guide/02-direction-bottom' }, 40 | { text: '03 - 向右滚动', link: '/guide/03-direction-right' }, 41 | { text: '04 - 滚动速度', link: '/guide/04-step' }, 42 | { text: '05 - 静止鼠标悬停停止', link: '/guide/05-hoverStop' }, 43 | { text: '06 - 单步停顿', link: '/guide/06-singleStop' }, 44 | { text: '07 - 单行停顿时间', link: '/guide/07-singleStopTime' }, 45 | // { text: '08 - switch控制切换', link: '/guide/08-switch' }, 46 | { text: '08 - echart图表无缝滚动', link: '/guide/08-echart' }, 47 | { text: '09 - 复杂结构数组属性更新问题', link: '/guide/09-array-property-update' }, 48 | { text: '10 - 滚动列表动态追加数据', link: '/guide/10-array-length-update' }, 49 | { text: '11 - 固定高度动态数据', link: '/guide/11-daynamicData' } 50 | 51 | ] 52 | } 53 | ], 54 | 55 | }, 56 | footer: { 57 | message: '欢迎给出一些意见和优化,期待你的 Pull Request.', 58 | copyright: '如有建议和疑问请前往issues.' 59 | } 60 | } 61 | } 62 | 63 | export default config; -------------------------------------------------------------------------------- /packages/vue3-scroll-seamless-docs/docs/.vitepress/theme/index.ts: -------------------------------------------------------------------------------- 1 | 2 | // 导入scroll 组件 3 | import Example01from from "../components/example01.vue"; 4 | import Example02 from "../components/example02.vue"; 5 | import Example03 from "../components/example03.vue"; 6 | import Example04 from "../components/example04.vue"; 7 | 8 | 9 | import {vue3ScrollSeamless} from "vue3-scroll-seamless"; 10 | 11 | 12 | import DefaultTheme from 'vitepress/theme' 13 | 14 | export default { 15 | ...DefaultTheme, 16 | enhanceApp({ app }) { 17 | // 注册一个全局组件 18 | app.component('Example01from',Example01from) 19 | app.component('Example02',Example02) 20 | app.component('Example04',Example04) 21 | app.component('vue3ScrollSeamless',vue3ScrollSeamless) 22 | } 23 | } -------------------------------------------------------------------------------- /packages/vue3-scroll-seamless-docs/docs/guide/01-basic.md: -------------------------------------------------------------------------------- 1 | # 01-默认配置 2 | 3 | 4 | 5 | ```vue 6 | 37 | 38 | 53 | 77 | 78 | ``` 79 | -------------------------------------------------------------------------------- /packages/vue3-scroll-seamless-docs/docs/guide/02-direction-bottom.md: -------------------------------------------------------------------------------- 1 | # 02 - 向下滚动 2 | 3 | 4 | 5 | ```vue 6 | 37 | 38 | 53 | 77 | 78 | ``` 79 | -------------------------------------------------------------------------------- /packages/vue3-scroll-seamless-docs/docs/guide/03-direction-right.md: -------------------------------------------------------------------------------- 1 | # 03 - 向右滚动 2 | 3 | 4 | ```vue 5 | 15 | 16 | 31 | 61 | 62 | ``` 63 | -------------------------------------------------------------------------------- /packages/vue3-scroll-seamless-docs/docs/guide/04-step.md: -------------------------------------------------------------------------------- 1 | # 04 - 滚动速度 2 | 3 | 4 | 5 | ```vue 6 | 37 | 38 | 53 | 77 | 78 | ``` 79 | -------------------------------------------------------------------------------- /packages/vue3-scroll-seamless-docs/docs/guide/05-hoverStop.md: -------------------------------------------------------------------------------- 1 | # 05 - 静止鼠标悬停停止 2 | 3 | 4 | 5 | ```vue 6 | 37 | 38 | 53 | 77 | 78 | ``` 79 | -------------------------------------------------------------------------------- /packages/vue3-scroll-seamless-docs/docs/guide/06-singleStop.md: -------------------------------------------------------------------------------- 1 | # 06 - 单步停顿 2 | 3 | 4 | 5 | ```vue 6 | 37 | 38 | 53 | 77 | 78 | ``` 79 | -------------------------------------------------------------------------------- /packages/vue3-scroll-seamless-docs/docs/guide/07-singleStopTime.md: -------------------------------------------------------------------------------- 1 | # 07 - 单行停顿时间 2 | 3 | 4 | 5 | 6 | ```vue 7 | 39 | 40 | 55 | 79 | 80 | ``` 81 | -------------------------------------------------------------------------------- /packages/vue3-scroll-seamless-docs/docs/guide/08-echart.md: -------------------------------------------------------------------------------- 1 | # 08 - echart图表无缝滚动 2 | 3 | 4 | ```vue 5 | 62 | 63 | 78 | 107 | 108 | ``` 109 | -------------------------------------------------------------------------------- /packages/vue3-scroll-seamless-docs/docs/guide/09-array-property-update.md: -------------------------------------------------------------------------------- 1 | # 09 - 复杂结构数组属性更新问题 2 | 3 | 4 | 5 | ```vue 6 | 52 | 53 | 72 | 111 | 112 | ``` 113 | -------------------------------------------------------------------------------- /packages/vue3-scroll-seamless-docs/docs/guide/10-array-length-update.md: -------------------------------------------------------------------------------- 1 | 10 - 滚动列表动态追加数据 2 | 3 | # 09 - 复杂结构数组属性更新问题 4 | 5 | 6 | 7 | ```vue 8 | 55 | 56 | 75 | 114 | 115 | ``` 116 | -------------------------------------------------------------------------------- /packages/vue3-scroll-seamless-docs/docs/guide/11-daynamicData.md: -------------------------------------------------------------------------------- 1 | 11 - 动态数据 2 | 3 | # 01 - 动态数据 4 | 5 | 6 | 7 | ```vue 8 | 36 | 37 | 51 | 101 | 102 | 103 | ``` 104 | -------------------------------------------------------------------------------- /packages/vue3-scroll-seamless-docs/docs/guide/a-switch.md: -------------------------------------------------------------------------------- 1 | # 08 - switch控制切换 2 | 3 | 4 | 5 | ```vue 6 | 38 | 39 | 54 | 78 | 79 | ``` 80 | -------------------------------------------------------------------------------- /packages/vue3-scroll-seamless-docs/docs/guide/events.md: -------------------------------------------------------------------------------- 1 | # 回调事件 2 | 3 | ```vue 4 | 9 | 10 | 26 | ``` -------------------------------------------------------------------------------- /packages/vue3-scroll-seamless-docs/docs/guide/index.md: -------------------------------------------------------------------------------- 1 | # 安装 2 | ## NPM 3 | ``` 4 | npm install vue3-scroll-seamless --save 5 | ``` 6 | ## PNPM 7 | ``` 8 | pnpm install vue3-scroll-seamless --save 9 | ``` 10 | ## Yarn 11 | ``` 12 | yarn add vue3-seamless-scroll 13 | ``` 14 | -------------------------------------------------------------------------------- /packages/vue3-scroll-seamless-docs/docs/guide/issuesSolution.md: -------------------------------------------------------------------------------- 1 | # 常见Issues 2 | 3 | 1、**`事件无法被复制的问题`** 4 | 5 | 组件本身没有对copy的html做一个节点的深度事件复制(类似jq的clone(true)) 6 | 7 | 解决方法 8 | 9 | ::: tip 10 | 事件代理,给父元素绑定对应事件,在需要的子元素上进行事件补获。(推荐) 11 | ::: 12 | ::: danger 13 | 简单的直接原生js进行`addEventListener`,存在异步数据无法绑定上问题。(不推荐) 14 | ::: -------------------------------------------------------------------------------- /packages/vue3-scroll-seamless-docs/docs/guide/notice.md: -------------------------------------------------------------------------------- 1 | # 注意项 2 | ::: tip 3 | - 1.最外层容器需要手动设置`width、height、overflow:hidden` 4 | 5 | - 2.左右的无缝滚动需要给主内容区域(即默认slot插槽提供)设定合适的`css width`属性(否则无法正确计算实际宽度)。 6 | 也可以通过给他设置为`display:flex;`无需设置`css width`属性 7 | 8 | - 3.step值不建议太小,不然会有卡顿效果(如果设置了单步滚动,step需是单步大小的约数,否则无法保证单步滚动结束的位置是否准确。~~~~~,比如单步设置的30,step不能为4) 9 | ::: 10 | ::: danger 11 | 目前涉及到switch控制切换的功能暂未实现 12 | ::: 13 | -------------------------------------------------------------------------------- /packages/vue3-scroll-seamless-docs/docs/guide/properties.md: -------------------------------------------------------------------------------- 1 | # 组件配置 2 | ## dataList 3 | - type: `Array` 4 | - required: `true` 5 | ::: tip 6 | 无缝滚动list数据 , 组件内部只关注data数组的length。 7 | ::: 8 | ## classOption 9 | ### step 10 | - type: `Number` 11 | - required: `false` 12 | - default:`1` 13 | ::: tip 14 | 数值越大速度滚动越快。 15 | 16 | step 值不建议太小,不然会有卡顿效果(如果设置了单步滚动,step 需是单步大小的约数,否则无法保证单步滚动结束的位置是否准确。~~~~~,比如单步设置的 30,step 不能为 4) 17 | ::: 18 | 19 | ### limitMoveNum 20 | 21 | - type: `Number` 22 | - required: `false` 23 | - default:`5` 24 | ::: tip 25 | 开启无缝滚动的数据量。 26 | ::: 27 | 28 | ### hoverStop 29 | 30 | - type: `Boolean` 31 | - required: `false` 32 | - default:`false` 33 | ::: tip 34 | 是否启用鼠标 hover 控制。 35 | ::: 36 | 37 | ### direction 38 | 39 | - type: `Number` 40 | - required: `false` 41 | - default:`1` 42 | ::: tip 43 | 方向: 0 往下 1 往上 2 向左 3 向右。 44 | ::: 45 | 46 | ### openTouch 47 | 48 | 53 | ::: danger 54 | 暂不支持 55 | ::: 56 | 57 | ### singleHeight 58 | 59 | - type: `Number` 60 | - required: `false` 61 | - default:`0` 62 | ::: tip 63 | 单步运动停止的高度(默认值 0 是无缝不停止的滚动),direction 为 0|1 时生效。 64 | ::: 65 | 66 | ### singleWidth 67 | 68 | - type: `Number` 69 | - required: `false` 70 | - default:`0` 71 | ::: tip 72 | 单步运动停止的宽度(默认值 0 是无缝不停止的滚动),direction 为 2|3 时生效 73 | ::: 74 | 75 | ### waitTime 76 | 77 | - type: `Number` 78 | - required: `false` 79 | - default:`1000` 80 | ::: tip 81 | 单步停止等待时间(默认值 1000ms)。 82 | ::: 83 | 84 | ### switchOffset 85 | ::: danger 86 | 暂不支持 87 | ::: 88 | 93 | 94 | ### autoPlay 95 | ::: danger 96 | 暂未支持 97 | ::: 98 | 99 | 104 | 105 | ### switchSingleStep 106 | ::: danger 107 | 暂未支持 108 | ::: 109 | 114 | 115 | ### switchDelay 116 | 117 | - type: `Number` 118 | - required: `false` 119 | - default:`400` 120 | ::: tip 121 | 单步切换的动画时间(ms)。 122 | ::: 123 | 124 | ### switchDisabledClass 125 | ::: danger 126 | 暂未支持 127 | ::: 128 | 133 | 134 | ### isSingleRemUnit 135 | 136 | - type: `Boolean` 137 | - required: `false` 138 | - default:`false` 139 | ::: tip 140 | singleHeight and singleWidth 是否开启 rem 度量。 141 | ::: 142 | 143 | ### navigation 144 | ::: danger 145 | 暂未支持 146 | ::: 147 | 152 | -------------------------------------------------------------------------------- /packages/vue3-scroll-seamless-docs/docs/guide/usage.md: -------------------------------------------------------------------------------- 1 | # 使用 2 | ## 组件注册与使用 3 | 4 | ```vue 5 | 6 | 7 | 35 | 36 | 51 | 75 | ``` 76 | 77 | ## 全局注册 78 | ```js 79 | // **main.js** 80 | import {vue3ScrollSeamless} from "vue3-scroll-seamless"; 81 | createApp(App).component('vue3ScrollSeamless',vue3ScrollSeamless).mount('#app') 82 | ``` -------------------------------------------------------------------------------- /packages/vue3-scroll-seamless-docs/docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: home 3 | 4 | hero: 5 | name: vue3-scroll-seamless 6 | tagline: 一个简单的基于vue3.js的无缝滚动 7 | actions: 8 | - theme: brand 9 | text: 开始 10 | link: /guide/ 11 | - theme: alt 12 | text: 在 GitHub 上查看 13 | link: https://github.com/xiaofulzm/vue3-scroll-seamless 14 | 15 | features: 16 | - title: "多样化配置支持" 17 | details: 目前支持上下左右无缝滚动,单步滚动。 18 | - title: 兼容多平台 19 | details: IE12+、Firefox、Chrome、Safari、iOS、Android。 20 | - title: 多技术栈版本支持 21 | details: 目前有Vue3、Vue2、JavaScript版本。 22 | --- 23 | 24 | -------------------------------------------------------------------------------- /packages/vue3-scroll-seamless-docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue3-scroll-seamless-docs", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "docs:dev": "vitepress dev docs ", 8 | "docs:build": "vitepress build docs", 9 | "docs:serve": "vitepress serve docs" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "ISC", 14 | "dependencies": { 15 | "vitepress": "1.0.0-alpha.21", 16 | "vue3-scroll-seamless":"../vue3-scroll-seamless" 17 | }, 18 | "devDependencies": { 19 | "@types/node": "^18.11.9" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/vue3-scroll-seamless-docs/shims.vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.vue" { 2 | import { DefineComponent } from "vue"; 3 | const component: DefineComponent<{}, {}, any>; 4 | export default component; 5 | } 6 | -------------------------------------------------------------------------------- /packages/vue3-scroll-seamless/.eslintignore: -------------------------------------------------------------------------------- 1 | *.sh 2 | node_modules 3 | lib 4 | coverage 5 | *.md 6 | *.scss 7 | *.woff 8 | *.ttf 9 | src/index.ts 10 | dist -------------------------------------------------------------------------------- /packages/vue3-scroll-seamless/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | browser: true, 5 | es2020: true, 6 | node: true, 7 | jest: true 8 | }, 9 | globals: { 10 | ga: true, 11 | chrome: true, 12 | __DEV__: true 13 | }, 14 | // 解析 .vue 文件 15 | parser: 'vue-eslint-parser', 16 | extends: [ 17 | 'plugin:json/recommended', 18 | 'plugin:vue/vue3-essential', 19 | 'eslint:recommended', 20 | '@vue/prettier' 21 | ], 22 | plugins: ['@typescript-eslint'], 23 | parserOptions: { 24 | parser: '@typescript-eslint/parser' // 解析 .ts 文件 25 | }, 26 | rules: { 27 | 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 28 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 29 | 'prettier/prettier': 'error', 30 | 'vue/multi-word-component-names': 'off', 31 | } 32 | } -------------------------------------------------------------------------------- /packages/vue3-scroll-seamless/dist/vue3-scroll-seamless.iife.js: -------------------------------------------------------------------------------- 1 | var vue3ScrollSeamless = function(exports, vue) { 2 | "use strict"; 3 | function animationFrame() { 4 | window.cancelAnimationFrame = function() { 5 | return window.cancelAnimationFrame || window.webkitCancelAnimationFrame || window.mozCancelAnimationFrame || window.oCancelAnimationFrame || window.msCancelAnimationFrame || function(id) { 6 | return window.clearTimeout(id); 7 | }; 8 | }(); 9 | window.requestAnimationFrame = function() { 10 | return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame || function(callback) { 11 | return window.setTimeout(callback, 1e3 / 60); 12 | }; 13 | }(); 14 | } 15 | function arrayEqual(arr1, arr2) { 16 | if (arr1 === arr2) 17 | return true; 18 | if (arr1.length !== arr2.length) 19 | return false; 20 | for (var i = 0; i < arr1.length; ++i) { 21 | if (arr1[i] !== arr2[i]) 22 | return false; 23 | } 24 | return true; 25 | } 26 | const _hoisted_1 = ["innerHTML"]; 27 | const _sfc_main = /* @__PURE__ */ vue.defineComponent({ 28 | __name: "seamlessScroll", 29 | props: { 30 | dataList: { 31 | type: Array, 32 | default: [] 33 | }, 34 | classOptions: { 35 | type: Object, 36 | default: {} 37 | } 38 | }, 39 | emits: ["ScrollEnd"], 40 | setup(__props, { emit: __emit }) { 41 | animationFrame(); 42 | const slotList = vue.ref(null); 43 | const wrap = vue.ref(null); 44 | const realBox = vue.ref(null); 45 | let copyHtml = vue.ref(), initData = vue.reactive({ 46 | xPos: 0, 47 | yPos: 0, 48 | delay: 0, 49 | ease: "ease-in", 50 | height: 0, 51 | width: 0, 52 | realBoxWidth: 0, 53 | realBoxHeight: 0, 54 | isHover: false, 55 | reqFrame: null, 56 | singleWaitTime: null 57 | }); 58 | const defaultOption = { 59 | step: 1, 60 | limitMoveNum: 5, 61 | hoverStop: true, 62 | direction: 1, 63 | openTouch: true, 64 | singleHeight: 0, 65 | singleWidth: 0, 66 | waitTime: 1e3, 67 | switchOffset: 30, 68 | autoPlay: true, 69 | navigation: false, 70 | switchSingleStep: 134, 71 | switchDelay: 400, 72 | switchDisabledClass: "disabled", 73 | isSingleRemUnit: false 74 | }; 75 | const emit = __emit; 76 | const Props = __props; 77 | vue.onBeforeMount(() => { 78 | initData.ease = "ease-in"; 79 | initData.isHover = false; 80 | initData.reqFrame = null; 81 | initData.singleWaitTime = null; 82 | }); 83 | vue.onMounted(() => { 84 | _initMove(); 85 | }); 86 | vue.onBeforeUnmount(() => { 87 | _cancle(); 88 | clearTimeout(initData.singleWaitTime); 89 | }); 90 | const options = vue.computed(() => { 91 | return { ...defaultOption, ...Props.classOptions }; 92 | }); 93 | const isHorizontal = vue.computed(() => options.value.direction > 1).value; 94 | const float = vue.computed(() => { 95 | let isFloat; 96 | if (isHorizontal) { 97 | isFloat = { float: "left", overflow: "hidden" }; 98 | } else { 99 | isFloat = { overflow: "hidden" }; 100 | } 101 | return isFloat; 102 | }); 103 | const pos = vue.computed(() => { 104 | return { 105 | transform: `translate(${initData.xPos}px,${initData.yPos}px)`, 106 | transition: `all ${initData.ease} ${initData.delay}ms`, 107 | overflow: "hidden" 108 | }; 109 | }); 110 | const navigation = vue.computed(() => options.value.navigation).value; 111 | const autoPlay = vue.computed(() => { 112 | if (navigation) 113 | return false; 114 | return options.value.autoPlay; 115 | }); 116 | const scrollSwitch = vue.computed( 117 | () => Props.dataList.length >= options.value.limitMoveNum 118 | ); 119 | const hoverStopSwitch = vue.computed( 120 | () => options.value.hoverStop && autoPlay.value && scrollSwitch.value 121 | ); 122 | const baseFontSize = vue.computed( 123 | () => options.value.isSingleRemUnit ? parseInt(window.getComputedStyle(document.documentElement, null).fontSize) : 1 124 | ).value; 125 | const realSingleStopWidth = vue.computed( 126 | () => options.value.singleWidth * baseFontSize 127 | ).value; 128 | const realSingleStopHeight = vue.computed( 129 | () => options.value.singleHeight * baseFontSize 130 | ).value; 131 | const gap = vue.ref(1); 132 | const step = vue.computed(() => { 133 | let singleStep; 134 | let step2 = options.value.step; 135 | if (isHorizontal) { 136 | singleStep = realSingleStopWidth; 137 | } else { 138 | singleStep = realSingleStopHeight; 139 | } 140 | if (singleStep > 0 && singleStep % step2 > 0) { 141 | console.error( 142 | "\u5982\u679C\u8BBE\u7F6E\u4E86\u5355\u6B65\u6EDA\u52A8,step\u9700\u662F\u5355\u6B65\u5927\u5C0F\u7684\u7EA6\u6570,\u5426\u5219\u65E0\u6CD5\u4FDD\u8BC1\u5355\u6B65\u6EDA\u52A8\u7ED3\u675F\u7684\u4F4D\u7F6E\u662F\u5426\u51C6\u786E~~~~~" 143 | ); 144 | } 145 | return step2; 146 | }).value; 147 | vue.watch( 148 | () => Props.dataList, 149 | (newValue, oldValue) => { 150 | _dataWarm(newValue); 151 | if (!arrayEqual(newValue, oldValue)) { 152 | reset(); 153 | } 154 | } 155 | ); 156 | vue.watch(autoPlay, (newBol) => { 157 | if (newBol) { 158 | reset(); 159 | } else { 160 | _stopMove(); 161 | } 162 | }); 163 | function reset() { 164 | _cancle(); 165 | _initMove(); 166 | } 167 | function changeEnter() { 168 | if (hoverStopSwitch.value) 169 | _stopMove(); 170 | } 171 | function changeLeave() { 172 | if (hoverStopSwitch.value) 173 | _startMove(); 174 | } 175 | function _dataWarm(data) { 176 | if (data.length > 100) { 177 | console.warn( 178 | `\u6570\u636E\u8FBE\u5230\u4E86${data.length}\u6761\u6709\u70B9\u591A\u54E6~,\u53EF\u80FD\u4F1A\u9020\u6210\u90E8\u5206\u8001\u65E7\u6D4F\u89C8\u5668\u5361\u987F\u3002` 179 | ); 180 | } 181 | } 182 | async function _initMove() { 183 | await vue.nextTick(); 184 | const { switchDelay } = options.value; 185 | _dataWarm(Props.dataList); 186 | copyHtml.value = ""; 187 | if (isHorizontal) { 188 | initData.height = wrap.value.offsetHeight; 189 | initData.width = wrap.value.offsetWidth; 190 | let slotListWidth = slotList.value.offsetWidth; 191 | if (autoPlay.value) { 192 | slotListWidth = slotListWidth * 2 + 1; 193 | } 194 | realBox.value.style.width = slotListWidth + "px"; 195 | initData.realBoxWidth = slotListWidth; 196 | } 197 | if (autoPlay.value) { 198 | initData.ease = "ease-in"; 199 | initData.delay = 0; 200 | } else { 201 | initData.ease = "linear"; 202 | initData.delay = switchDelay; 203 | return; 204 | } 205 | if (scrollSwitch.value) { 206 | let initTimer = null; 207 | copyHtml.value = slotList.value.innerHTML; 208 | gap.value = Math.ceil(realBox.value.offsetHeight / copyHtml.value.clientHeight); 209 | if (initTimer) 210 | clearTimeout(initTimer); 211 | initTimer = setTimeout(() => { 212 | initData.realBoxHeight = realBox.value.offsetHeight; 213 | _move(); 214 | }, 0); 215 | } else { 216 | _cancle(); 217 | initData.xPos = 0; 218 | initData.yPos = 0; 219 | } 220 | } 221 | function _move() { 222 | if (initData.isHover) 223 | return; 224 | _cancle(); 225 | initData.reqFrame = requestAnimationFrame(function() { 226 | const h = initData.realBoxHeight / 2; 227 | const w = initData.realBoxWidth / 2; 228 | let { direction, waitTime } = options.value; 229 | if (direction === 1) { 230 | if (Math.abs(initData.yPos) >= h) { 231 | emit("ScrollEnd"); 232 | initData.yPos = 0; 233 | } 234 | initData.yPos -= step; 235 | } else if (direction === 0) { 236 | if (initData.yPos >= 0) { 237 | emit("ScrollEnd"); 238 | initData.yPos = h * -1; 239 | } 240 | initData.yPos += step; 241 | } else if (direction === 2) { 242 | if (Math.abs(initData.xPos) >= w) { 243 | emit("ScrollEnd"); 244 | initData.xPos = 0; 245 | } 246 | initData.xPos -= step; 247 | } else if (direction === 3) { 248 | if (initData.xPos >= 0) { 249 | emit("ScrollEnd"); 250 | initData.xPos = w * -1; 251 | } 252 | initData.xPos += step; 253 | } 254 | if (initData.singleWaitTime) 255 | clearTimeout(initData.singleWaitTime); 256 | if (realSingleStopHeight) { 257 | if (Math.abs(initData.yPos) % realSingleStopHeight < step) { 258 | initData.singleWaitTime = setTimeout(() => { 259 | _move(); 260 | }, waitTime); 261 | } else { 262 | _move(); 263 | } 264 | } else if (realSingleStopWidth) { 265 | if (Math.abs(initData.xPos) % realSingleStopWidth < step) { 266 | initData.singleWaitTime = setTimeout(() => { 267 | _move(); 268 | }, waitTime); 269 | } else { 270 | _move(); 271 | } 272 | } else { 273 | _move(); 274 | } 275 | }); 276 | } 277 | function _cancle() { 278 | cancelAnimationFrame(initData.reqFrame || null); 279 | } 280 | function _stopMove() { 281 | initData.isHover = true; 282 | if (initData.singleWaitTime) 283 | clearTimeout(initData.singleWaitTime); 284 | _cancle(); 285 | } 286 | function _startMove() { 287 | initData.isHover = false; 288 | _move(); 289 | } 290 | return (_ctx, _cache) => { 291 | return vue.openBlock(), vue.createElementBlock("div", { 292 | ref_key: "wrap", 293 | ref: wrap 294 | }, [ 295 | vue.createElementVNode("div", { 296 | onMouseenter: changeEnter, 297 | onMouseleave: changeLeave, 298 | ref_key: "realBox", 299 | ref: realBox, 300 | style: vue.normalizeStyle(pos.value) 301 | }, [ 302 | vue.createElementVNode("div", { 303 | style: vue.normalizeStyle(float.value), 304 | ref_key: "slotList", 305 | ref: slotList 306 | }, [ 307 | vue.renderSlot(_ctx.$slots, "default") 308 | ], 4), 309 | (vue.openBlock(true), vue.createElementBlock(vue.Fragment, null, vue.renderList(gap.value, (i) => { 310 | return vue.openBlock(), vue.createElementBlock("div", { 311 | style: vue.normalizeStyle(float.value), 312 | innerHTML: vue.unref(copyHtml), 313 | key: i 314 | }, null, 12, _hoisted_1); 315 | }), 128)) 316 | ], 36) 317 | ], 512); 318 | }; 319 | } 320 | }); 321 | const entry = { 322 | install(app) { 323 | app.component(_sfc_main.name, _sfc_main); 324 | } 325 | }; 326 | exports.default = entry; 327 | exports.vue3ScrollSeamless = _sfc_main; 328 | Object.defineProperties(exports, { __esModule: { value: true }, [Symbol.toStringTag]: { value: "Module" } }); 329 | return exports; 330 | }({}, Vue); 331 | -------------------------------------------------------------------------------- /packages/vue3-scroll-seamless/dist/vue3-scroll-seamless.mjs: -------------------------------------------------------------------------------- 1 | import { defineComponent, ref, reactive, onBeforeMount, onMounted, onBeforeUnmount, computed, watch, nextTick, openBlock, createElementBlock, createElementVNode, normalizeStyle, renderSlot, Fragment, renderList, unref } from "vue"; 2 | function animationFrame() { 3 | window.cancelAnimationFrame = function() { 4 | return window.cancelAnimationFrame || window.webkitCancelAnimationFrame || window.mozCancelAnimationFrame || window.oCancelAnimationFrame || window.msCancelAnimationFrame || function(id) { 5 | return window.clearTimeout(id); 6 | }; 7 | }(); 8 | window.requestAnimationFrame = function() { 9 | return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame || function(callback) { 10 | return window.setTimeout(callback, 1e3 / 60); 11 | }; 12 | }(); 13 | } 14 | function arrayEqual(arr1, arr2) { 15 | if (arr1 === arr2) 16 | return true; 17 | if (arr1.length !== arr2.length) 18 | return false; 19 | for (var i = 0; i < arr1.length; ++i) { 20 | if (arr1[i] !== arr2[i]) 21 | return false; 22 | } 23 | return true; 24 | } 25 | const _hoisted_1 = ["innerHTML"]; 26 | const _sfc_main = /* @__PURE__ */ defineComponent({ 27 | __name: "seamlessScroll", 28 | props: { 29 | dataList: { 30 | type: Array, 31 | default: [] 32 | }, 33 | classOptions: { 34 | type: Object, 35 | default: {} 36 | } 37 | }, 38 | emits: ["ScrollEnd"], 39 | setup(__props, { emit: __emit }) { 40 | animationFrame(); 41 | const slotList = ref(null); 42 | const wrap = ref(null); 43 | const realBox = ref(null); 44 | let copyHtml = ref(), initData = reactive({ 45 | xPos: 0, 46 | yPos: 0, 47 | delay: 0, 48 | ease: "ease-in", 49 | height: 0, 50 | width: 0, 51 | realBoxWidth: 0, 52 | realBoxHeight: 0, 53 | isHover: false, 54 | reqFrame: null, 55 | singleWaitTime: null 56 | }); 57 | const defaultOption = { 58 | step: 1, 59 | limitMoveNum: 5, 60 | hoverStop: true, 61 | direction: 1, 62 | openTouch: true, 63 | singleHeight: 0, 64 | singleWidth: 0, 65 | waitTime: 1e3, 66 | switchOffset: 30, 67 | autoPlay: true, 68 | navigation: false, 69 | switchSingleStep: 134, 70 | switchDelay: 400, 71 | switchDisabledClass: "disabled", 72 | isSingleRemUnit: false 73 | }; 74 | const emit = __emit; 75 | const Props = __props; 76 | onBeforeMount(() => { 77 | initData.ease = "ease-in"; 78 | initData.isHover = false; 79 | initData.reqFrame = null; 80 | initData.singleWaitTime = null; 81 | }); 82 | onMounted(() => { 83 | _initMove(); 84 | }); 85 | onBeforeUnmount(() => { 86 | _cancle(); 87 | clearTimeout(initData.singleWaitTime); 88 | }); 89 | const options = computed(() => { 90 | return { ...defaultOption, ...Props.classOptions }; 91 | }); 92 | const isHorizontal = computed(() => options.value.direction > 1).value; 93 | const float = computed(() => { 94 | let isFloat; 95 | if (isHorizontal) { 96 | isFloat = { float: "left", overflow: "hidden" }; 97 | } else { 98 | isFloat = { overflow: "hidden" }; 99 | } 100 | return isFloat; 101 | }); 102 | const pos = computed(() => { 103 | return { 104 | transform: `translate(${initData.xPos}px,${initData.yPos}px)`, 105 | transition: `all ${initData.ease} ${initData.delay}ms`, 106 | overflow: "hidden" 107 | }; 108 | }); 109 | const navigation = computed(() => options.value.navigation).value; 110 | const autoPlay = computed(() => { 111 | if (navigation) 112 | return false; 113 | return options.value.autoPlay; 114 | }); 115 | const scrollSwitch = computed( 116 | () => Props.dataList.length >= options.value.limitMoveNum 117 | ); 118 | const hoverStopSwitch = computed( 119 | () => options.value.hoverStop && autoPlay.value && scrollSwitch.value 120 | ); 121 | const baseFontSize = computed( 122 | () => options.value.isSingleRemUnit ? parseInt(window.getComputedStyle(document.documentElement, null).fontSize) : 1 123 | ).value; 124 | const realSingleStopWidth = computed( 125 | () => options.value.singleWidth * baseFontSize 126 | ).value; 127 | const realSingleStopHeight = computed( 128 | () => options.value.singleHeight * baseFontSize 129 | ).value; 130 | const gap = ref(1); 131 | const step = computed(() => { 132 | let singleStep; 133 | let step2 = options.value.step; 134 | if (isHorizontal) { 135 | singleStep = realSingleStopWidth; 136 | } else { 137 | singleStep = realSingleStopHeight; 138 | } 139 | if (singleStep > 0 && singleStep % step2 > 0) { 140 | console.error( 141 | "\u5982\u679C\u8BBE\u7F6E\u4E86\u5355\u6B65\u6EDA\u52A8,step\u9700\u662F\u5355\u6B65\u5927\u5C0F\u7684\u7EA6\u6570,\u5426\u5219\u65E0\u6CD5\u4FDD\u8BC1\u5355\u6B65\u6EDA\u52A8\u7ED3\u675F\u7684\u4F4D\u7F6E\u662F\u5426\u51C6\u786E~~~~~" 142 | ); 143 | } 144 | return step2; 145 | }).value; 146 | watch( 147 | () => Props.dataList, 148 | (newValue, oldValue) => { 149 | _dataWarm(newValue); 150 | if (!arrayEqual(newValue, oldValue)) { 151 | reset(); 152 | } 153 | } 154 | ); 155 | watch(autoPlay, (newBol) => { 156 | if (newBol) { 157 | reset(); 158 | } else { 159 | _stopMove(); 160 | } 161 | }); 162 | function reset() { 163 | _cancle(); 164 | _initMove(); 165 | } 166 | function changeEnter() { 167 | if (hoverStopSwitch.value) 168 | _stopMove(); 169 | } 170 | function changeLeave() { 171 | if (hoverStopSwitch.value) 172 | _startMove(); 173 | } 174 | function _dataWarm(data) { 175 | if (data.length > 100) { 176 | console.warn( 177 | `\u6570\u636E\u8FBE\u5230\u4E86${data.length}\u6761\u6709\u70B9\u591A\u54E6~,\u53EF\u80FD\u4F1A\u9020\u6210\u90E8\u5206\u8001\u65E7\u6D4F\u89C8\u5668\u5361\u987F\u3002` 178 | ); 179 | } 180 | } 181 | async function _initMove() { 182 | await nextTick(); 183 | const { switchDelay } = options.value; 184 | _dataWarm(Props.dataList); 185 | copyHtml.value = ""; 186 | if (isHorizontal) { 187 | initData.height = wrap.value.offsetHeight; 188 | initData.width = wrap.value.offsetWidth; 189 | let slotListWidth = slotList.value.offsetWidth; 190 | if (autoPlay.value) { 191 | slotListWidth = slotListWidth * 2 + 1; 192 | } 193 | realBox.value.style.width = slotListWidth + "px"; 194 | initData.realBoxWidth = slotListWidth; 195 | } 196 | if (autoPlay.value) { 197 | initData.ease = "ease-in"; 198 | initData.delay = 0; 199 | } else { 200 | initData.ease = "linear"; 201 | initData.delay = switchDelay; 202 | return; 203 | } 204 | if (scrollSwitch.value) { 205 | let initTimer = null; 206 | copyHtml.value = slotList.value.innerHTML; 207 | gap.value = Math.ceil(wrap.value.offsetHeight / slotList.value.offsetHeight) 208 | if (initTimer) 209 | clearTimeout(initTimer); 210 | initTimer = setTimeout(() => { 211 | initData.realBoxHeight = realBox.value.offsetHeight; 212 | _move(); 213 | }, 0); 214 | } else { 215 | _cancle(); 216 | initData.xPos = 0; 217 | initData.yPos = 0; 218 | } 219 | } 220 | function _move() { 221 | if (initData.isHover) 222 | return; 223 | _cancle(); 224 | initData.reqFrame = requestAnimationFrame(function() { 225 | const h = initData.realBoxHeight / 2; 226 | const w = initData.realBoxWidth / 2; 227 | let { direction, waitTime } = options.value; 228 | if (direction === 1) { 229 | if (Math.abs(initData.yPos) >= h) { 230 | emit("ScrollEnd"); 231 | initData.yPos = 0; 232 | } 233 | initData.yPos -= step; 234 | } else if (direction === 0) { 235 | if (initData.yPos >= 0) { 236 | emit("ScrollEnd"); 237 | initData.yPos = h * -1; 238 | } 239 | initData.yPos += step; 240 | } else if (direction === 2) { 241 | if (Math.abs(initData.xPos) >= w) { 242 | emit("ScrollEnd"); 243 | initData.xPos = 0; 244 | } 245 | initData.xPos -= step; 246 | } else if (direction === 3) { 247 | if (initData.xPos >= 0) { 248 | emit("ScrollEnd"); 249 | initData.xPos = w * -1; 250 | } 251 | initData.xPos += step; 252 | } 253 | if (initData.singleWaitTime) 254 | clearTimeout(initData.singleWaitTime); 255 | if (realSingleStopHeight) { 256 | if (Math.abs(initData.yPos) % realSingleStopHeight < step) { 257 | initData.singleWaitTime = setTimeout(() => { 258 | _move(); 259 | }, waitTime); 260 | } else { 261 | _move(); 262 | } 263 | } else if (realSingleStopWidth) { 264 | if (Math.abs(initData.xPos) % realSingleStopWidth < step) { 265 | initData.singleWaitTime = setTimeout(() => { 266 | _move(); 267 | }, waitTime); 268 | } else { 269 | _move(); 270 | } 271 | } else { 272 | _move(); 273 | } 274 | }); 275 | } 276 | function _cancle() { 277 | cancelAnimationFrame(initData.reqFrame || null); 278 | } 279 | function _stopMove() { 280 | initData.isHover = true; 281 | if (initData.singleWaitTime) 282 | clearTimeout(initData.singleWaitTime); 283 | _cancle(); 284 | } 285 | function _startMove() { 286 | initData.isHover = false; 287 | _move(); 288 | } 289 | return (_ctx, _cache) => { 290 | return openBlock(), createElementBlock("div", { 291 | ref_key: "wrap", 292 | ref: wrap 293 | }, [ 294 | createElementVNode("div", { 295 | onMouseenter: changeEnter, 296 | onMouseleave: changeLeave, 297 | ref_key: "realBox", 298 | ref: realBox, 299 | style: normalizeStyle(pos.value) 300 | }, [ 301 | createElementVNode("div", { 302 | style: normalizeStyle(float.value), 303 | ref_key: "slotList", 304 | ref: slotList 305 | }, [ 306 | renderSlot(_ctx.$slots, "default") 307 | ], 4), 308 | (openBlock(true), createElementBlock(Fragment, null, renderList(gap.value, (i) => { 309 | return openBlock(), createElementBlock("div", { 310 | style: normalizeStyle(float.value), 311 | innerHTML: unref(copyHtml), 312 | key: i 313 | }, null, 12, _hoisted_1); 314 | }), 128)) 315 | ], 36) 316 | ], 512); 317 | }; 318 | } 319 | }); 320 | const entry = { 321 | install(app) { 322 | app.component(_sfc_main.name, _sfc_main); 323 | } 324 | }; 325 | export { 326 | entry as default, 327 | _sfc_main as vue3ScrollSeamless 328 | }; 329 | -------------------------------------------------------------------------------- /packages/vue3-scroll-seamless/dist/vue3-scroll-seamless.umd.js: -------------------------------------------------------------------------------- 1 | (function(global, factory) { 2 | typeof exports === "object" && typeof module !== "undefined" ? factory(exports, require("vue")) : typeof define === "function" && define.amd ? define(["exports", "vue"], factory) : (global = typeof globalThis !== "undefined" ? globalThis : global || self, factory(global.vue3ScrollSeamless = {}, global.Vue)); 3 | })(this, function(exports2, vue) { 4 | "use strict"; 5 | function animationFrame() { 6 | window.cancelAnimationFrame = function() { 7 | return window.cancelAnimationFrame || window.webkitCancelAnimationFrame || window.mozCancelAnimationFrame || window.oCancelAnimationFrame || window.msCancelAnimationFrame || function(id) { 8 | return window.clearTimeout(id); 9 | }; 10 | }(); 11 | window.requestAnimationFrame = function() { 12 | return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame || function(callback) { 13 | return window.setTimeout(callback, 1e3 / 60); 14 | }; 15 | }(); 16 | } 17 | function arrayEqual(arr1, arr2) { 18 | if (arr1 === arr2) 19 | return true; 20 | if (arr1.length !== arr2.length) 21 | return false; 22 | for (var i = 0; i < arr1.length; ++i) { 23 | if (arr1[i] !== arr2[i]) 24 | return false; 25 | } 26 | return true; 27 | } 28 | const _hoisted_1 = ["innerHTML"]; 29 | const _sfc_main = /* @__PURE__ */ vue.defineComponent({ 30 | __name: "seamlessScroll", 31 | props: { 32 | dataList: { 33 | type: Array, 34 | default: [] 35 | }, 36 | classOptions: { 37 | type: Object, 38 | default: {} 39 | } 40 | }, 41 | emits: ["ScrollEnd"], 42 | setup(__props, { emit: __emit }) { 43 | animationFrame(); 44 | const slotList = vue.ref(null); 45 | const wrap = vue.ref(null); 46 | const realBox = vue.ref(null); 47 | let copyHtml = vue.ref(), initData = vue.reactive({ 48 | xPos: 0, 49 | yPos: 0, 50 | delay: 0, 51 | ease: "ease-in", 52 | height: 0, 53 | width: 0, 54 | realBoxWidth: 0, 55 | realBoxHeight: 0, 56 | isHover: false, 57 | reqFrame: null, 58 | singleWaitTime: null 59 | }); 60 | const defaultOption = { 61 | step: 1, 62 | limitMoveNum: 5, 63 | hoverStop: true, 64 | direction: 1, 65 | openTouch: true, 66 | singleHeight: 0, 67 | singleWidth: 0, 68 | waitTime: 1e3, 69 | switchOffset: 30, 70 | autoPlay: true, 71 | navigation: false, 72 | switchSingleStep: 134, 73 | switchDelay: 400, 74 | switchDisabledClass: "disabled", 75 | isSingleRemUnit: false 76 | }; 77 | const emit = __emit; 78 | const Props = __props; 79 | vue.onBeforeMount(() => { 80 | initData.ease = "ease-in"; 81 | initData.isHover = false; 82 | initData.reqFrame = null; 83 | initData.singleWaitTime = null; 84 | }); 85 | vue.onMounted(() => { 86 | _initMove(); 87 | }); 88 | vue.onBeforeUnmount(() => { 89 | _cancle(); 90 | clearTimeout(initData.singleWaitTime); 91 | }); 92 | const options = vue.computed(() => { 93 | return { ...defaultOption, ...Props.classOptions }; 94 | }); 95 | const isHorizontal = vue.computed(() => options.value.direction > 1).value; 96 | const float = vue.computed(() => { 97 | let isFloat; 98 | if (isHorizontal) { 99 | isFloat = { float: "left", overflow: "hidden" }; 100 | } else { 101 | isFloat = { overflow: "hidden" }; 102 | } 103 | return isFloat; 104 | }); 105 | const pos = vue.computed(() => { 106 | return { 107 | transform: `translate(${initData.xPos}px,${initData.yPos}px)`, 108 | transition: `all ${initData.ease} ${initData.delay}ms`, 109 | overflow: "hidden" 110 | }; 111 | }); 112 | const navigation = vue.computed(() => options.value.navigation).value; 113 | const autoPlay = vue.computed(() => { 114 | if (navigation) 115 | return false; 116 | return options.value.autoPlay; 117 | }); 118 | const scrollSwitch = vue.computed( 119 | () => Props.dataList.length >= options.value.limitMoveNum 120 | ); 121 | const hoverStopSwitch = vue.computed( 122 | () => options.value.hoverStop && autoPlay.value && scrollSwitch.value 123 | ); 124 | const baseFontSize = vue.computed( 125 | () => options.value.isSingleRemUnit ? parseInt(window.getComputedStyle(document.documentElement, null).fontSize) : 1 126 | ).value; 127 | const realSingleStopWidth = vue.computed( 128 | () => options.value.singleWidth * baseFontSize 129 | ).value; 130 | const realSingleStopHeight = vue.computed( 131 | () => options.value.singleHeight * baseFontSize 132 | ).value; 133 | const gap = vue.ref(1); 134 | const step = vue.computed(() => { 135 | let singleStep; 136 | let step2 = options.value.step; 137 | if (isHorizontal) { 138 | singleStep = realSingleStopWidth; 139 | } else { 140 | singleStep = realSingleStopHeight; 141 | } 142 | if (singleStep > 0 && singleStep % step2 > 0) { 143 | console.error( 144 | "\u5982\u679C\u8BBE\u7F6E\u4E86\u5355\u6B65\u6EDA\u52A8,step\u9700\u662F\u5355\u6B65\u5927\u5C0F\u7684\u7EA6\u6570,\u5426\u5219\u65E0\u6CD5\u4FDD\u8BC1\u5355\u6B65\u6EDA\u52A8\u7ED3\u675F\u7684\u4F4D\u7F6E\u662F\u5426\u51C6\u786E~~~~~" 145 | ); 146 | } 147 | return step2; 148 | }).value; 149 | vue.watch( 150 | () => Props.dataList, 151 | (newValue, oldValue) => { 152 | _dataWarm(newValue); 153 | if (!arrayEqual(newValue, oldValue)) { 154 | reset(); 155 | } 156 | } 157 | ); 158 | vue.watch(autoPlay, (newBol) => { 159 | if (newBol) { 160 | reset(); 161 | } else { 162 | _stopMove(); 163 | } 164 | }); 165 | function reset() { 166 | _cancle(); 167 | _initMove(); 168 | } 169 | function changeEnter() { 170 | if (hoverStopSwitch.value) 171 | _stopMove(); 172 | } 173 | function changeLeave() { 174 | if (hoverStopSwitch.value) 175 | _startMove(); 176 | } 177 | function _dataWarm(data) { 178 | if (data.length > 100) { 179 | console.warn( 180 | `\u6570\u636E\u8FBE\u5230\u4E86${data.length}\u6761\u6709\u70B9\u591A\u54E6~,\u53EF\u80FD\u4F1A\u9020\u6210\u90E8\u5206\u8001\u65E7\u6D4F\u89C8\u5668\u5361\u987F\u3002` 181 | ); 182 | } 183 | } 184 | async function _initMove() { 185 | await vue.nextTick(); 186 | const { switchDelay } = options.value; 187 | _dataWarm(Props.dataList); 188 | copyHtml.value = ""; 189 | if (isHorizontal) { 190 | initData.height = wrap.value.offsetHeight; 191 | initData.width = wrap.value.offsetWidth; 192 | let slotListWidth = slotList.value.offsetWidth; 193 | if (autoPlay.value) { 194 | slotListWidth = slotListWidth * 2 + 1; 195 | } 196 | realBox.value.style.width = slotListWidth + "px"; 197 | initData.realBoxWidth = slotListWidth; 198 | } 199 | if (autoPlay.value) { 200 | initData.ease = "ease-in"; 201 | initData.delay = 0; 202 | } else { 203 | initData.ease = "linear"; 204 | initData.delay = switchDelay; 205 | return; 206 | } 207 | if (scrollSwitch.value) { 208 | let initTimer = null; 209 | copyHtml.value = slotList.value.innerHTML; 210 | gap.value = Math.ceil(realBox.value.offsetHeight / copyHtml.value.clientHeight); 211 | if (initTimer) 212 | clearTimeout(initTimer); 213 | initTimer = setTimeout(() => { 214 | initData.realBoxHeight = realBox.value.offsetHeight; 215 | _move(); 216 | }, 0); 217 | } else { 218 | _cancle(); 219 | initData.xPos = 0; 220 | initData.yPos = 0; 221 | } 222 | } 223 | function _move() { 224 | if (initData.isHover) 225 | return; 226 | _cancle(); 227 | initData.reqFrame = requestAnimationFrame(function() { 228 | const h = initData.realBoxHeight / 2; 229 | const w = initData.realBoxWidth / 2; 230 | let { direction, waitTime } = options.value; 231 | if (direction === 1) { 232 | if (Math.abs(initData.yPos) >= h) { 233 | emit("ScrollEnd"); 234 | initData.yPos = 0; 235 | } 236 | initData.yPos -= step; 237 | } else if (direction === 0) { 238 | if (initData.yPos >= 0) { 239 | emit("ScrollEnd"); 240 | initData.yPos = h * -1; 241 | } 242 | initData.yPos += step; 243 | } else if (direction === 2) { 244 | if (Math.abs(initData.xPos) >= w) { 245 | emit("ScrollEnd"); 246 | initData.xPos = 0; 247 | } 248 | initData.xPos -= step; 249 | } else if (direction === 3) { 250 | if (initData.xPos >= 0) { 251 | emit("ScrollEnd"); 252 | initData.xPos = w * -1; 253 | } 254 | initData.xPos += step; 255 | } 256 | if (initData.singleWaitTime) 257 | clearTimeout(initData.singleWaitTime); 258 | if (realSingleStopHeight) { 259 | if (Math.abs(initData.yPos) % realSingleStopHeight < step) { 260 | initData.singleWaitTime = setTimeout(() => { 261 | _move(); 262 | }, waitTime); 263 | } else { 264 | _move(); 265 | } 266 | } else if (realSingleStopWidth) { 267 | if (Math.abs(initData.xPos) % realSingleStopWidth < step) { 268 | initData.singleWaitTime = setTimeout(() => { 269 | _move(); 270 | }, waitTime); 271 | } else { 272 | _move(); 273 | } 274 | } else { 275 | _move(); 276 | } 277 | }); 278 | } 279 | function _cancle() { 280 | cancelAnimationFrame(initData.reqFrame || null); 281 | } 282 | function _stopMove() { 283 | initData.isHover = true; 284 | if (initData.singleWaitTime) 285 | clearTimeout(initData.singleWaitTime); 286 | _cancle(); 287 | } 288 | function _startMove() { 289 | initData.isHover = false; 290 | _move(); 291 | } 292 | return (_ctx, _cache) => { 293 | return vue.openBlock(), vue.createElementBlock("div", { 294 | ref_key: "wrap", 295 | ref: wrap 296 | }, [ 297 | vue.createElementVNode("div", { 298 | onMouseenter: changeEnter, 299 | onMouseleave: changeLeave, 300 | ref_key: "realBox", 301 | ref: realBox, 302 | style: vue.normalizeStyle(pos.value) 303 | }, [ 304 | vue.createElementVNode("div", { 305 | style: vue.normalizeStyle(float.value), 306 | ref_key: "slotList", 307 | ref: slotList 308 | }, [ 309 | vue.renderSlot(_ctx.$slots, "default") 310 | ], 4), 311 | (vue.openBlock(true), vue.createElementBlock(vue.Fragment, null, vue.renderList(gap.value, (i) => { 312 | return vue.openBlock(), vue.createElementBlock("div", { 313 | style: vue.normalizeStyle(float.value), 314 | innerHTML: vue.unref(copyHtml), 315 | key: i 316 | }, null, 12, _hoisted_1); 317 | }), 128)) 318 | ], 36) 319 | ], 512); 320 | }; 321 | } 322 | }); 323 | const entry = { 324 | install(app) { 325 | app.component(_sfc_main.name, _sfc_main); 326 | } 327 | }; 328 | exports2.default = entry; 329 | exports2.vue3ScrollSeamless = _sfc_main; 330 | Object.defineProperties(exports2, { __esModule: { value: true }, [Symbol.toStringTag]: { value: "Module" } }); 331 | }); 332 | -------------------------------------------------------------------------------- /packages/vue3-scroll-seamless/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /packages/vue3-scroll-seamless/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue3-scroll-seamless", 3 | "version": "1.0.7", 4 | "description": "", 5 | "main": "dist/vue3-scroll-seamless.mjs", 6 | "homepage": "https://xiaofulzm.github.io/vue3-scroll-seamless/", 7 | "files": [ 8 | "dist" 9 | ], 10 | "exports":{ 11 | ".":{ 12 | "import":"./dist/vue3-scroll-seamless.mjs", 13 | "require": "./dist/vue3-scroll-seamless.iifejs", 14 | "types": "./dist/vue3-scroll-seamless.umdjs" 15 | } 16 | }, 17 | "scripts": { 18 | "dev": "vite", 19 | "build": "vite build", 20 | "test": "vitest", 21 | "lint": "eslint --fix --ext .ts,.vue src", 22 | "format": "prettier --write \"src/**/*.ts\" \"src/**/*.vue\"" 23 | }, 24 | "keywords": [ 25 | "vue3", 26 | "vuejs", 27 | "ui", 28 | "components", 29 | "Seamless", 30 | "scroll" 31 | ], 32 | "author": "", 33 | "license": "MIT", 34 | "devDependencies": { 35 | "@typescript-eslint/eslint-plugin": "^5.36.2", 36 | "@typescript-eslint/parser": "^5.36.2", 37 | "@vitejs/plugin-vue": "^3.1.0", 38 | "@vue/eslint-config-prettier": "^7.0.0", 39 | "@vue/test-utils": "2.0.2", 40 | "babel-eslint": "^10.1.0", 41 | "eslint": "^8.23.0", 42 | "eslint-formatter-pretty": "^4.1.0", 43 | "eslint-plugin-json": "^3.1.0", 44 | "eslint-plugin-prettier": "^4.2.1", 45 | "eslint-plugin-vue": "^9.4.0", 46 | "happy-dom": "6.0.4", 47 | "prettier": "^2.7.1", 48 | "vitest": "0.21.1" 49 | }, 50 | "dependencies": { 51 | "typescript": "^4.8.4" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /packages/vue3-scroll-seamless/src/_test_/seamlessScroll.spec.ts: -------------------------------------------------------------------------------- 1 | import vue3ScrollSeamless from "../components/seamlessScroll.vue"; 2 | 3 | import { shallowMount } from "@vue/test-utils"; 4 | import { describe, expect, test } from "vitest"; 5 | 6 | //测试分组 7 | // 测试分组 8 | describe("无缝滚动组件配置参数测试", () => { 9 | test("测试limitMoveNum(开启无缝滚动的数据量)配置是否生效!", async () => { 10 | const wrapper = shallowMount(vue3ScrollSeamless, { 11 | props: { 12 | dataList: [1, 2, 3, 4, 5], 13 | classOptions: { 14 | limitMoveNum: 6, 15 | }, 16 | }, 17 | }); 18 | 19 | // 断言 20 | expect(wrapper.vm.scrollSwitch).toBe(false); 21 | await wrapper.setProps({ dataList: [1, 2, 3, 4, 5, 6, 7] }); 22 | expect(wrapper.vm.scrollSwitch).toBe(true); 23 | }); 24 | 25 | test("测试hoverStop(是否启用鼠标hover控制)配置是否生效!", () => { 26 | const wrapper = shallowMount(vue3ScrollSeamless, { 27 | props: { 28 | dataList: [1, 2, 3, 4, 5, 6, 7], 29 | }, 30 | }); 31 | expect(wrapper.vm.initData.isHover).toBe(false); 32 | const main = wrapper.find({ ref: "realBox" }); 33 | main.trigger("mouseenter"); 34 | expect(wrapper.vm.initData.isHover).toBe(true); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /packages/vue3-scroll-seamless/src/components/seamlessScroll.vue: -------------------------------------------------------------------------------- 1 | 347 | 348 | 361 | 362 | -------------------------------------------------------------------------------- /packages/vue3-scroll-seamless/src/demo.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 37 | 70 | -------------------------------------------------------------------------------- /packages/vue3-scroll-seamless/src/entry.ts: -------------------------------------------------------------------------------- 1 | import vue3ScrollSeamless from "./components/seamlessScroll.vue"; 2 | 3 | import { App } from "vue"; 4 | 5 | // console.log(vue3ScrollSeamless); 6 | 7 | export { vue3ScrollSeamless }; 8 | 9 | export default { 10 | install(app: App) { 11 | app.component(vue3ScrollSeamless.name, vue3ScrollSeamless); 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /packages/vue3-scroll-seamless/src/index.ts: -------------------------------------------------------------------------------- 1 | import { h,createApp } from "vue"; 2 | import Demo from "./demo.vue"; 3 | 4 | createApp({ 5 | render: () => h(Demo), 6 | }).mount("#app"); 7 | -------------------------------------------------------------------------------- /packages/vue3-scroll-seamless/src/shims.vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.vue" { 2 | import { DefineComponent } from "vue"; 3 | const component: DefineComponent<{}, {}, any>; 4 | export default component; 5 | } 6 | -------------------------------------------------------------------------------- /packages/vue3-scroll-seamless/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @desc AnimationFrame简单兼容 3 | */ 4 | 5 | export function animationFrame() { 6 | window.cancelAnimationFrame = (function () { 7 | // @ts-ignore 8 | return ( 9 | window.cancelAnimationFrame || 10 | window.webkitCancelAnimationFrame || 11 | window.mozCancelAnimationFrame || 12 | window.oCancelAnimationFrame || 13 | window.msCancelAnimationFrame || 14 | function (id) { 15 | return window.clearTimeout(id); 16 | } 17 | ); 18 | })(); 19 | window.requestAnimationFrame = (function () { 20 | // @ts-ignore 21 | return ( 22 | window.requestAnimationFrame || 23 | window.webkitRequestAnimationFrame || 24 | window.mozRequestAnimationFrame || 25 | window.oRequestAnimationFrame || 26 | window.msRequestAnimationFrame || 27 | function (callback) { 28 | return window.setTimeout(callback, 1000 / 60); 29 | } 30 | ); 31 | })(); 32 | } 33 | 34 | /** 35 | * @desc 判断数组是否相等 36 | * @param {arr1,arr2} 37 | * @return {Boolean} 38 | */ 39 | export function arrayEqual(arr1: unknown[], arr2: unknown[]): boolean { 40 | if (arr1 === arr2) return true; 41 | if (arr1.length !== arr2.length) return false; 42 | for (var i = 0; i < arr1.length; ++i) { 43 | if (arr1[i] !== arr2[i]) return false; 44 | } 45 | return true; 46 | } 47 | -------------------------------------------------------------------------------- /packages/vue3-scroll-seamless/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, /* 生成相关的 '.d.ts' 文件。 */ 4 | "declarationDir": "./dist/types", /* '.d.ts' 文件输出目录 */ 5 | "lib": [ "dom", "es5", "es2015.promise" ,"es2015", "es2017"], 6 | "jsx": "preserve", 7 | "isolatedModules": true, 8 | "noImplicitThis": true 9 | }, 10 | "include": [ 11 | "./src/**/*.*" 12 | ], 13 | "exclude": [ 14 | "node_modules" 15 | ], 16 | "esModuleInterop": true, 17 | "allowSyntheticDefaultImports": "true" 18 | } -------------------------------------------------------------------------------- /packages/vue3-scroll-seamless/vite.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { defineConfig } from "vite"; 3 | import vue from "@vitejs/plugin-vue"; 4 | 5 | 6 | const rollupOptions = { 7 | external: ["vue", "vue-router"], 8 | output: { 9 | globals: { 10 | vue: "Vue", 11 | }, 12 | }, 13 | }; 14 | 15 | export default defineConfig({ 16 | plugins: [vue()], 17 | build: { 18 | rollupOptions, 19 | minify:false, 20 | lib: { 21 | entry: "./src/entry.ts", 22 | name: "vue3ScrollSeamless", 23 | fileName: "vue3-scroll-seamless", 24 | // 导出模块格式 25 | formats: ["es", "umd","iife"], 26 | }, 27 | }, 28 | test: { 29 | globals: true, 30 | environment: 'happy-dom', 31 | } 32 | }); -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | # all packages in subdirs of packages/ and components/ 3 | - 'packages/**' -------------------------------------------------------------------------------- /scripts/preinstall.js: -------------------------------------------------------------------------------- 1 | if (!/pnpm/.test(process.env.npm_execpath || '')) { 2 | // console.log('不懂问然叔') 3 | console.warn( 4 | `\u001b[33mThis repository requires using pnpm as the package manager ` + 5 | ` for scripts to work properly.\u001b[39m\n` 6 | ) 7 | process.exit(1) 8 | } --------------------------------------------------------------------------------