diff --git a/resources/assets/js/app.js b/resources/assets/js/app.js index 34434b0b2..fa54f5051 100644 --- a/resources/assets/js/app.js +++ b/resources/assets/js/app.js @@ -40,6 +40,7 @@ import TableAction from './components/TableAction.vue' import QuickEdit from './components/QuickEdit.vue' import UserAvatar from './components/UserAvatar.vue' import ImgView from './components/ImgView.vue' +import Scrollbar from './components/Scrollbar' Vue.component('PageTitle', PageTitle); Vue.component('Loading', Loading); @@ -49,6 +50,7 @@ Vue.component('TableAction', TableAction); Vue.component('QuickEdit', QuickEdit); Vue.component('UserAvatar', UserAvatar); Vue.component('ImgView', ImgView); +Vue.component('Scrollbar', Scrollbar); import { Avatar, diff --git a/resources/assets/js/components/RightBottom.vue b/resources/assets/js/components/RightBottom.vue index ded349394..9d78ef336 100644 --- a/resources/assets/js/components/RightBottom.vue +++ b/resources/assets/js/components/RightBottom.vue @@ -29,7 +29,9 @@
{{$L('离最新版本只有一步之遥了!重新启动应用即可完成更新。')}}
- + + +
diff --git a/resources/assets/js/components/Scrollbar/index.js b/resources/assets/js/components/Scrollbar/index.js new file mode 100644 index 000000000..19c5a7ce5 --- /dev/null +++ b/resources/assets/js/components/Scrollbar/index.js @@ -0,0 +1,419 @@ +import {supportsTouch, toInt} from "./lib/util"; +import * as CSS from './lib/css'; + +export default { + name: 'Scrollbar', + props: { + tag: { + type: String, + default: 'div' + }, + className: { + type: String, + default: '' + }, + enableX: { + type: Boolean, + default: false + }, + enableY: { + type: Boolean, + default: true + }, + hideBar: { + type: Boolean, + default: false + }, + minSize: { + type: Number, + default: 20 + }, + }, + data() { + return { + isReady: false, + + scrollingX: false, + scrollingY: false, + + moveingX: false, + moveingY: false, + + containerWidth: null, + containerHeight: null, + + contentWidth: null, + contentHeight: null, + contentOverflow: { + x: null, + y: null, + }, + + thumbYHeight: null, + thumbYTop: null, + thumbXWidth: null, + thumbXLeft: null, + + lastScrollTop: 0, + lastScrollLeft: 0, + + timeouts: {}, + } + }, + computed: { + containerClass() { + const classList = ['scrollbar-container']; + if (supportsTouch) { + classList.push('scrollbar-touch') + } else { + classList.push('scrollbar-desktop') + } + if (this.contentWidth > this.containerWidth && this.contentOverflow.x !== 'hidden' && this.enableX) { + classList.push('scrollbar-active-x') + } + if (this.contentHeight > this.containerHeight && this.contentOverflow.y !== 'hidden' && this.enableY) { + classList.push('scrollbar-active-y') + } + if (this.scrollingX) { + classList.push('scrollbar-scrolling-x') + } + if (this.scrollingY) { + classList.push('scrollbar-scrolling-y') + } + if (this.moveingX) { + classList.push('scrollbar-moveing-x') + } + if (this.moveingY) { + classList.push('scrollbar-moveing-y') + } + if (this.hideBar || !this.isReady) { + classList.push('scrollbar-hidebar') + } + return classList + }, + contentClass({className, enableX, enableY}) { + const classList = ['scrollbar-content']; + if (className) { + classList.push(className) + } + if (!enableX) { + classList.push('scrollbar-disable-x') + } + if (!enableY) { + classList.push('scrollbar-disable-y') + } + return classList + } + }, + mounted() { + this.$nextTick(() => { + this.updateBase() + }); + }, + updated() { + this.$nextTick(() => { + this.updateGeometry(false); + }); + }, + methods: { + /** + * 滚动区域信息 + * @returns {{scale: number, scrollY: *, scrollE: number}} + */ + scrollInfo() { + const scroller = $A(this.$refs.content); + const wInnerH = Math.round(scroller.innerHeight()); + const wScrollY = scroller.scrollTop(); + const bScrollH = this.$refs.content.scrollHeight; + return { + scale: wScrollY / (bScrollH - wInnerH), //已滚动比例 + scrollY: wScrollY, //滚动的距离 + scrollE: bScrollH - wInnerH - wScrollY, //与底部距离 + } + }, + + /** + * 滚动区域元素 + * @returns {Vue | Element | (Vue | Element)[]} + */ + scrollElement() { + return this.$refs.content; + }, + + /** + * 从滚动区域获取指定元素 + * @param el + * @returns {*} + */ + querySelector(el) { + return this.$refs.content && this.$refs.content.querySelector(el) + }, + + /** + * 更新基础信息 + */ + updateBase() { + if (supportsTouch) { + return; + } + const containerStyles = CSS.get(this.$refs.container); + const contentStyles = CSS.get(this.$refs.content); + + CSS.set(this.$refs.trackX, { + left: toInt(containerStyles.paddingLeft) + toInt(contentStyles.marginLeft), + right: toInt(containerStyles.paddingRight) + toInt(contentStyles.marginRight), + bottom: toInt(containerStyles.paddingBottom) + toInt(contentStyles.marginBottom), + }); + CSS.set(this.$refs.trackY, { + top: toInt(containerStyles.paddingTop) + toInt(contentStyles.marginTop), + bottom: toInt(containerStyles.paddingBottom) + toInt(contentStyles.marginBottom), + right: toInt(containerStyles.paddingRight) + toInt(contentStyles.marginRight), + }); + + this.contentOverflow = { + x: contentStyles.overflowX, + y: contentStyles.overflowY, + } + }, + + /** + * 更新滚动条 + * @param scrolling 是否正在滚动 + */ + updateGeometry(scrolling) { + if (supportsTouch) { + return; + } + + const element = this.$refs.content; + if (!element) { + return; + } + + const scrollTop = Math.floor(element.scrollTop); + const rect = element.getBoundingClientRect(); + + this.containerWidth = Math.round(rect.width); + this.containerHeight = Math.round(rect.height); + this.contentWidth = element.scrollWidth; + this.contentHeight = element.scrollHeight; + + this.thumbXWidth = Math.max(toInt((this.containerWidth * this.containerWidth) / this.contentWidth), this.minSize); + this.thumbXLeft = toInt((element.scrollLeft * (this.containerWidth - this.thumbXWidth)) / (this.contentWidth - this.containerWidth)); + this.thumbYHeight = Math.max(toInt((this.containerHeight * this.containerHeight) / this.contentHeight), this.minSize); + this.thumbYTop = toInt((scrollTop * (this.containerHeight - this.thumbYHeight)) / (this.contentHeight - this.containerHeight)); + + CSS.set(this.$refs.thumbX, { + left: this.thumbXLeft, + width: this.thumbXWidth, + }); + CSS.set(this.$refs.thumbY, { + top: this.thumbYTop, + height: this.thumbYHeight, + }); + + if (scrolling) { + this.scrollingX = this.lastScrollLeft !== element.scrollLeft; + this.scrollingY = this.lastScrollTop !== element.scrollTop; + + this.lastScrollTop = element.scrollTop; + this.lastScrollLeft = element.scrollLeft; + + this.timeouts['scroll'] && clearTimeout(this.timeouts['scroll']); + this.timeouts['scroll'] = setTimeout(() => { + this.scrollingX = false; + this.scrollingY = false; + }, 1000) + } + }, + + /** + * 鼠标移入事件(单次) + */ + onContainerMouseMove() { + setTimeout(() => { + if (this.isReady) { + return + } + this.updateGeometry(true); + this.isReady = true + }, 300) + }, + + /** + * 滚动区域滚动事件 + * @param e + */ + onContentScroll(e) { + this.updateGeometry(true); + this.$emit('on-scroll', e); + this.isReady = true + }, + + /** + * 内容区域鼠标进入事件 + */ + onContentMouseenter() { + this.updateBase(); + this.updateGeometry(false); + }, + + /** + * 轨道区域(X)鼠标按下事件 + * @param e + */ + onTrackXMouseDown(e) { + if (supportsTouch) { + return; + } + const element = this.$refs.content; + const rect = this.$refs.trackX.getBoundingClientRect(); + + const positionLeft = e.pageX - window.scrollX - rect.left; + const direction = positionLeft > this.thumbXLeft ? 1 : -1; + + element.scrollLeft += direction * this.containerWidth; + this.updateGeometry(true); + + e.stopPropagation(); + }, + + /** + * 轨道区域(Y)鼠标按下事件 + * @param e + */ + onTrackYMouseDown(e) { + if (supportsTouch) { + return; + } + const element = this.$refs.content; + const rect = this.$refs.trackY.getBoundingClientRect(); + + const positionTop = e.pageY - window.scrollY - rect.top; + const direction = positionTop > this.thumbYTop ? 1 : -1; + + element.scrollTop += direction * this.containerHeight; + this.updateGeometry(true); + + e.stopPropagation(); + }, + + /** + * 滚动条(X)鼠标按下事件 + * @param e + */ + onThumbXMouseDown(e) { + if (supportsTouch) { + return; + } + const element = this.$refs.content; + const rect = element.getBoundingClientRect(); + const scrollLeft = element.scrollLeft; + const pageX = e.pageX - window.scrollX; + + const mouseMoveHandler = (e) => { + const diff = e.pageX - pageX; + element.scrollLeft = scrollLeft + diff * this.contentWidth / rect.width; + }; + + const mouseUpHandler = () => { + this.timeouts['moveX'] = setTimeout(() => { + this.moveingX = false; + }, 100); + document.removeEventListener('mousemove', mouseMoveHandler); + document.removeEventListener('mouseup', mouseUpHandler); + }; + this.moveingX = true; + this.timeouts['moveX'] && clearTimeout(this.timeouts['moveX']); + + document.addEventListener('mousemove', mouseMoveHandler); + document.addEventListener('mouseup', mouseUpHandler); + + e.preventDefault(); + e.stopPropagation(); + }, + + /** + * 滚动条(Y)鼠标按下事件 + * @param e + */ + onThumbYMouseDown(e) { + if (supportsTouch) { + return; + } + const element = this.$refs.content; + const rect = element.getBoundingClientRect(); + const scrollTop = element.scrollTop; + const pageY = e.pageY - window.scrollY; + + const mouseMoveHandler = (e) => { + const diff = e.pageY - pageY; + element.scrollTop = scrollTop + diff * this.contentHeight / rect.height; + }; + + const mouseUpHandler = () => { + this.timeouts['moveY'] = setTimeout(() => { + this.moveingY = false; + }, 100); + document.removeEventListener('mousemove', mouseMoveHandler); + document.removeEventListener('mouseup', mouseUpHandler); + }; + this.moveingY = true; + this.timeouts['moveY'] && clearTimeout(this.timeouts['moveY']); + + document.addEventListener('mousemove', mouseMoveHandler); + document.addEventListener('mouseup', mouseUpHandler); + + e.preventDefault(); + e.stopPropagation(); + } + }, + render(h) { + return h('div', { + ref: 'container', + class: this.containerClass, + on: { + '~mousemove': this.onContainerMouseMove, + } + }, [ + h(this.tag, { + ref: 'content', + class: this.contentClass, + on: { + scroll: this.onContentScroll, + mouseenter: this.onContentMouseenter, + } + }, this.$slots.default), + h('div', { + ref: 'trackX', + class: 'scrollbar-track-x', + on: { + mousedown: this.onTrackXMouseDown + } + }, [ + h('div', { + ref: 'thumbX', + class: 'scrollbar-thumb-x', + on: { + mousedown: this.onThumbXMouseDown + } + }), + ]), + h('div', { + ref: 'trackY', + class: 'scrollbar-track-y', + on: { + mousedown: this.onTrackYMouseDown + } + }, [ + h('div', { + ref: 'thumbY', + class: 'scrollbar-thumb-y', + on: { + mousedown: this.onThumbYMouseDown + } + }) + ]), + ]) + } +} diff --git a/resources/assets/js/components/Scrollbar/lib/css.js b/resources/assets/js/components/Scrollbar/lib/css.js new file mode 100644 index 000000000..f3a40019b --- /dev/null +++ b/resources/assets/js/components/Scrollbar/lib/css.js @@ -0,0 +1,19 @@ +export function get(element) { + if (element) { + return getComputedStyle(element); + } + return {}; +} + +export function set(element, obj) { + if (element) { + for (const key in obj) { + let val = obj[key]; + if (typeof val === 'number') { + val = `${val}px`; + } + element.style[key] = val; + } + } + return element; +} diff --git a/resources/assets/js/components/Scrollbar/lib/util.js b/resources/assets/js/components/Scrollbar/lib/util.js new file mode 100644 index 000000000..5cc55f13f --- /dev/null +++ b/resources/assets/js/components/Scrollbar/lib/util.js @@ -0,0 +1,9 @@ +export function toInt(x) { + return parseInt(x, 10) || 0; +} + +export const supportsTouch = typeof window !== 'undefined' && + ('ontouchstart' in window || + ('maxTouchPoints' in window.navigator && + window.navigator.maxTouchPoints > 0) || + (window.DocumentTouch && document instanceof window.DocumentTouch)); diff --git a/resources/assets/js/components/Scrollbar/style.scss b/resources/assets/js/components/Scrollbar/style.scss new file mode 100644 index 000000000..7591e980c --- /dev/null +++ b/resources/assets/js/components/Scrollbar/style.scss @@ -0,0 +1,156 @@ +@import 'perfect-scrollbar/css/perfect-scrollbar.css'; + +.scrollbar-container { + flex: 1; + height: 100%; + position: relative; + overflow: hidden; + + /* + * 触摸设备隐藏自定义滚动条 + */ + &.scrollbar-touch { + .scrollbar-track-x, + .scrollbar-track-y { + display: none; + } + } + + /* + * 桌面设备隐藏系统滚动条 + */ + &.scrollbar-desktop, + &.scrollbar-hidebar { + .scrollbar-content { + &::-webkit-scrollbar { + display: none; + width: 0; + height: 0; + } + } + } + + /* + * 隐藏滚动条 + */ + &.scrollbar-hidebar { + .scrollbar-track-x, + .scrollbar-track-y { + opacity: 0 !important; + } + } + + /* + * 滚动条轨道样式 + */ + .scrollbar-track-x, + .scrollbar-track-y { + position: absolute; + z-index: 101; + display: block; + visibility: hidden; + opacity: 0; + transition: background-color .2s linear, opacity .2s linear; + } + + .scrollbar-track-x { + left: 0; + right: 0; + bottom: 0; + height: 15px; + } + + .scrollbar-track-y { + top: 0; + right: 0; + bottom: 0; + width: 15px; + } + + &.scrollbar-active-x .scrollbar-track-x, + &.scrollbar-active-y .scrollbar-track-y { + visibility: visible; + background-color: transparent; + } + + &:hover > .scrollbar-track-x, + &:hover > .scrollbar-track-y, + &.scrollbar-scrolling-x .scrollbar-track-x, + &.scrollbar-scrolling-y .scrollbar-track-y { + opacity: 0.6; + } + + .scrollbar-track-x:hover, + .scrollbar-track-y:hover, + .scrollbar-track-x:focus, + .scrollbar-track-y:focus, + &.scrollbar-moveing-x .scrollbar-track-x, + &.scrollbar-moveing-y .scrollbar-track-y { + background-color: #eee; + opacity: 0.9; + } + + /* + * 滚动条样式 + */ + .scrollbar-thumb-x, + .scrollbar-thumb-y { + position: absolute; + z-index: 102; + background-color: #aaa; + border-radius: 6px; + transform: translateZ(0); + } + + .scrollbar-thumb-x { + transition: background-color .2s linear, height .2s ease-in-out; + height: 6px; + bottom: 2px; + } + + .scrollbar-thumb-y { + transition: background-color .2s linear, width .2s ease-in-out; + width: 6px; + right: 2px; + } + + .scrollbar-track-x:hover > .scrollbar-thumb-x, + .scrollbar-track-x:focus > .scrollbar-thumb-x, + &.scrollbar-moveing-x .scrollbar-thumb-x { + background-color: #999; + height: 11px; + } + + .scrollbar-track-y:hover > .scrollbar-thumb-y, + .scrollbar-track-y:focus > .scrollbar-thumb-y, + &.scrollbar-moveing-y .scrollbar-thumb-y { + background-color: #999; + width: 11px; + } + + /* + * 内容区域样式 + */ + .scrollbar-content { + height: 100%; + overflow: auto; + -webkit-overflow-scrolling: touch; + + &.scrollbar-disable-x { + overflow-x: hidden; + } + + &.scrollbar-disable-y { + overflow-y: hidden; + } + } +} + +/* + * 隐藏系统滚动条 + */ +.scrollbar-hidden { + &::-webkit-scrollbar { + display: none; + } +} diff --git a/resources/assets/js/components/ScrollerY.vue b/resources/assets/js/components/ScrollerY.vue deleted file mode 100644 index a67742008..000000000 --- a/resources/assets/js/components/ScrollerY.vue +++ /dev/null @@ -1,152 +0,0 @@ - - - diff --git a/resources/assets/js/pages/manage.vue b/resources/assets/js/pages/manage.vue index 2c061e8d0..ba2736e24 100644 --- a/resources/assets/js/pages/manage.vue +++ b/resources/assets/js/pages/manage.vue @@ -100,29 +100,33 @@ -
+
userid === userId).map(({id}) => { diff --git a/resources/assets/js/pages/manage/components/ChatInput/emoji.vue b/resources/assets/js/pages/manage/components/ChatInput/emoji.vue index 11456f1ba..c79bb1f57 100644 --- a/resources/assets/js/pages/manage/components/ChatInput/emoji.vue +++ b/resources/assets/js/pages/manage/components/ChatInput/emoji.vue @@ -14,12 +14,14 @@ @click="emojiNavActive=item.type" v-html="item.content">
- + + + -
+ -
+ diff --git a/resources/assets/js/pages/manage/messenger.vue b/resources/assets/js/pages/manage/messenger.vue index c9657d0da..da235145c 100644 --- a/resources/assets/js/pages/manage/messenger.vue +++ b/resources/assets/js/pages/manage/messenger.vue @@ -45,16 +45,13 @@
{{$L('未开启通知权限')}}
- -