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 @@
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 @@
-
- -
-
-
-
-
-
-
- -
-
-
-
- -
-
-
-
-
- -
-
-
-
-
-