diff --git a/app/Domains/Real/Repositories/OrderRepository.php b/app/Domains/Real/Repositories/OrderRepository.php index 7d5d31d5..50c1c4f9 100644 --- a/app/Domains/Real/Repositories/OrderRepository.php +++ b/app/Domains/Real/Repositories/OrderRepository.php @@ -2,6 +2,7 @@ namespace App\Domains\Real\Repositories; +use Carbon\Carbon; use App\Core\Repository; use App\Models\Real\Order as Model; diff --git a/frontend/src/components/table/assist.js b/frontend/src/components/table/assist.js new file mode 100644 index 00000000..b6bd5500 --- /dev/null +++ b/frontend/src/components/table/assist.js @@ -0,0 +1,329 @@ +import Vue from 'vue'; +const isServer = Vue.prototype.$isServer; +// 判断参数是否是其中之一 +export function oneOf(value, validList) { + for (let i = 0; i < validList.length; i++) { + if (value === validList[i]) { + return true; + } + } + return false; +} + +export function camelcaseToHyphen(str) { + return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase(); +} + +// For Modal scrollBar hidden +let cached; +export function getScrollBarSize(fresh) { + if (isServer) return 0; + if (fresh || cached === undefined) { + const inner = document.createElement('div'); + inner.style.width = '100%'; + inner.style.height = '200px'; + + const outer = document.createElement('div'); + const outerStyle = outer.style; + + outerStyle.position = 'absolute'; + outerStyle.top = 0; + outerStyle.left = 0; + outerStyle.pointerEvents = 'none'; + outerStyle.visibility = 'hidden'; + outerStyle.width = '200px'; + outerStyle.height = '150px'; + outerStyle.overflow = 'hidden'; + + outer.appendChild(inner); + + document.body.appendChild(outer); + + const widthContained = inner.offsetWidth; + outer.style.overflow = 'scroll'; + let widthScroll = inner.offsetWidth; + + if (widthContained === widthScroll) { + widthScroll = outer.clientWidth; + } + + document.body.removeChild(outer); + + cached = widthContained - widthScroll; + } + return cached; +} + +// watch DOM change +export const MutationObserver = isServer ? false : window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver || false; + +const SPECIAL_CHARS_REGEXP = /([\:\-\_]+(.))/g; +const MOZ_HACK_REGEXP = /^moz([A-Z])/; + +function camelCase(name) { + return name.replace(SPECIAL_CHARS_REGEXP, function(_, separator, letter, offset) { + return offset ? letter.toUpperCase() : letter; + }).replace(MOZ_HACK_REGEXP, 'Moz$1'); +} +// getStyle +export function getStyle(element, styleName) { + if (!element || !styleName) return null; + styleName = camelCase(styleName); + if (styleName === 'float') { + styleName = 'cssFloat'; + } + try { + const computed = document.defaultView.getComputedStyle(element, ''); + return element.style[styleName] || computed ? computed[styleName] : null; + } catch (e) { + return element.style[styleName]; + } +} + +// firstUpperCase +function firstUpperCase(str) { + return str.toString()[0].toUpperCase() + str.toString().slice(1); +} +export { firstUpperCase }; + +// Warn +export function warnProp(component, prop, correctType, wrongType) { + correctType = firstUpperCase(correctType); + wrongType = firstUpperCase(wrongType); + console.error(`[iView warn]: Invalid prop: type check failed for prop ${prop}. Expected ${correctType}, got ${wrongType}. (found in component: ${component})`); // eslint-disable-line +} + +function typeOf(obj) { + const toString = Object.prototype.toString; + const map = { + '[object Boolean]': 'boolean', + '[object Number]': 'number', + '[object String]': 'string', + '[object Function]': 'function', + '[object Array]': 'array', + '[object Date]': 'date', + '[object RegExp]': 'regExp', + '[object Undefined]': 'undefined', + '[object Null]': 'null', + '[object Object]': 'object' + }; + return map[toString.call(obj)]; +} + +// deepCopy +function deepCopy(data) { + const t = typeOf(data); + let o; + + if (t === 'array') { + o = []; + } else if (t === 'object') { + o = {}; + } else { + return data; + } + + if (t === 'array') { + for (let i = 0; i < data.length; i++) { + o.push(deepCopy(data[i])); + } + } else if (t === 'object') { + for (let i in data) { + o[i] = deepCopy(data[i]); + } + } + return o; +} + +export { deepCopy }; + +// scrollTop animation +export function scrollTop(el, from = 0, to, duration = 500, endCallback) { + if (!window.requestAnimationFrame) { + window.requestAnimationFrame = ( + window.webkitRequestAnimationFrame || + window.mozRequestAnimationFrame || + window.msRequestAnimationFrame || + function(callback) { + return window.setTimeout(callback, 1000 / 60); + } + ); + } + const difference = Math.abs(from - to); + const step = Math.ceil(difference / duration * 50); + + function scroll(start, end, step) { + if (start === end) { + endCallback && endCallback(); + return; + } + + let d = (start + step > end) ? end : start + step; + if (start > end) { + d = (start - step < end) ? end : start - step; + } + + if (el === window) { + window.scrollTo(d, d); + } else { + el.scrollTop = d; + } + window.requestAnimationFrame(() => scroll(d, end, step)); + } + scroll(from, to, step); +} + +// Find components upward +function findComponentUpward(context, componentName, componentNames) { + if (typeof componentName === 'string') { + componentNames = [componentName]; + } else { + componentNames = componentName; + } + + let parent = context.$parent; + let name = parent.$options.name; + while (parent && (!name || componentNames.indexOf(name) < 0)) { + parent = parent.$parent; + if (parent) name = parent.$options.name; + } + return parent; +} +export { findComponentUpward }; + +// Find component downward +export function findComponentDownward(context, componentName) { + const childrens = context.$children; + let children = null; + + if (childrens.length) { + for (const child of childrens) { + const name = child.$options.name; + if (name === componentName) { + children = child; + break; + } else { + children = findComponentDownward(child, componentName); + if (children) break; + } + } + } + return children; +} + +// Find components downward +export function findComponentsDownward(context, componentName) { + return context.$children.reduce((components, child) => { + if (child.$options.name === componentName) components.push(child); + const foundChilds = findComponentsDownward(child, componentName); + return components.concat(foundChilds); + }, []); +} + +// Find components upward +export function findComponentsUpward(context, componentName) { + let parents = []; + const parent = context.$parent; + if (parent) { + if (parent.$options.name === componentName) parents.push(parent); + return parents.concat(findComponentsUpward(parent, componentName)); + } else { + return []; + } +} + +// Find brothers components +export function findBrothersComponents(context, componentName, exceptMe = true) { + let res = context.$parent.$children.filter(item => { + return item.$options.name === componentName; + }); + let index = res.findIndex(item => item._uid === context._uid); + if (exceptMe) res.splice(index, 1); + return res; +} + +/* istanbul ignore next */ +const trim = function(string) { + return (string || '').replace(/^[\s\uFEFF]+|[\s\uFEFF]+$/g, ''); +}; + +/* istanbul ignore next */ +export function hasClass(el, cls) { + if (!el || !cls) return false; + if (cls.indexOf(' ') !== -1) throw new Error('className should not contain space.'); + if (el.classList) { + return el.classList.contains(cls); + } else { + return (' ' + el.className + ' ').indexOf(' ' + cls + ' ') > -1; + } +} + +/* istanbul ignore next */ +export function addClass(el, cls) { + if (!el) return; + let curClass = el.className; + const classes = (cls || '').split(' '); + + for (let i = 0, j = classes.length; i < j; i++) { + const clsName = classes[i]; + if (!clsName) continue; + + if (el.classList) { + el.classList.add(clsName); + } else { + if (!hasClass(el, clsName)) { + curClass += ' ' + clsName; + } + } + } + if (!el.classList) { + el.className = curClass; + } +} + +/* istanbul ignore next */ +export function removeClass(el, cls) { + if (!el || !cls) return; + const classes = cls.split(' '); + let curClass = ' ' + el.className + ' '; + + for (let i = 0, j = classes.length; i < j; i++) { + const clsName = classes[i]; + if (!clsName) continue; + + if (el.classList) { + el.classList.remove(clsName); + } else { + if (hasClass(el, clsName)) { + curClass = curClass.replace(' ' + clsName + ' ', ' '); + } + } + } + if (!el.classList) { + el.className = trim(curClass); + } +} + +export const dimensionMap = { + xs: '480px', + sm: '768px', + md: '992px', + lg: '1200px', + xl: '1600px' +}; + +export function setMatchMedia() { + if (typeof window !== 'undefined') { + const matchMediaPolyfill = mediaQuery => { + return { + media: mediaQuery, + matches: false, + on() {}, + off() {} + }; + }; + window.matchMedia = window.matchMedia || matchMediaPolyfill; + } +} + +export const sharpMatcherRegx = /#([^#]+)$/; diff --git a/frontend/src/components/table/cell.vue b/frontend/src/components/table/cell.vue new file mode 100644 index 00000000..f30ad940 --- /dev/null +++ b/frontend/src/components/table/cell.vue @@ -0,0 +1,137 @@ + + diff --git a/frontend/src/components/table/csv.js b/frontend/src/components/table/csv.js new file mode 100644 index 00000000..b0605415 --- /dev/null +++ b/frontend/src/components/table/csv.js @@ -0,0 +1,59 @@ +/* + inspired by https://www.npmjs.com/package/react-csv-downloader + now removed from Github +*/ + +const newLine = '\r\n'; +const appendLine = (content, row, { separator, quoted }) => { + const line = row.map(data => { + if (!quoted) return data; + // quote data + data = typeof data === 'string' ? data.replace(/"/g, '"') : data; + return `"${data}"`; + }); + content.push(line.join(separator)); +}; + +const defaults = { + separator: ',', + quoted: false +}; + +export default function csv(columns, datas, options, noHeader = false) { + options = Object.assign({}, defaults, options); + let columnOrder; + const content = []; + const column = []; + + if (columns) { + columnOrder = columns.map(v => { + if (typeof v === 'string') return v; + if (!noHeader) { + column.push(typeof v.title !== 'undefined' ? v.title : v.key); + } + return v.key; + }); + if (column.length > 0) appendLine(content, column, options); + } else { + columnOrder = []; + datas.forEach(v => { + if (!Array.isArray(v)) { + columnOrder = columnOrder.concat(Object.keys(v)); + } + }); + if (columnOrder.length > 0) { + columnOrder = columnOrder.filter((value, index, self) => self.indexOf(value) === index); + if (!noHeader) appendLine(content, columnOrder, options); + } + } + + if (Array.isArray(datas)) { + datas.forEach(row => { + if (!Array.isArray(row)) { + row = columnOrder.map(k => (typeof row[k] !== 'undefined' ? row[k] : '')); + } + appendLine(content, row, options); + }); + } + return content.join(newLine); +} diff --git a/frontend/src/components/table/dom.js b/frontend/src/components/table/dom.js new file mode 100644 index 00000000..26a228e0 --- /dev/null +++ b/frontend/src/components/table/dom.js @@ -0,0 +1,36 @@ +import Vue from 'vue'; +const isServer = Vue.prototype.$isServer; + +/* istanbul ignore next */ +export const on = (function() { + if (!isServer && document.addEventListener) { + return function(element, event, handler) { + if (element && event && handler) { + element.addEventListener(event, handler, false); + } + }; + } else { + return function(element, event, handler) { + if (element && event && handler) { + element.attachEvent('on' + event, handler); + } + }; + } +})(); + +/* istanbul ignore next */ +export const off = (function() { + if (!isServer && document.removeEventListener) { + return function(element, event, handler) { + if (element && event) { + element.removeEventListener(event, handler, false); + } + }; + } else { + return function(element, event, handler) { + if (element && event) { + element.detachEvent('on' + event, handler); + } + }; + } +})(); diff --git a/frontend/src/components/table/expand.js b/frontend/src/components/table/expand.js new file mode 100644 index 00000000..2c855946 --- /dev/null +++ b/frontend/src/components/table/expand.js @@ -0,0 +1,21 @@ +export default { + name: 'TableExpand', + functional: true, + props: { + row: Object, + render: Function, + index: Number, + column: { + type: Object, + default: null + } + }, + render: (h, ctx) => { + const params = { + row: ctx.props.row, + index: ctx.props.index + }; + if (ctx.props.column) params.column = ctx.props.column; + return ctx.props.render(h, params); + } +}; diff --git a/frontend/src/components/table/export-csv.js b/frontend/src/components/table/export-csv.js new file mode 100644 index 00000000..c76c726f --- /dev/null +++ b/frontend/src/components/table/export-csv.js @@ -0,0 +1,76 @@ +function has(browser) { + const ua = navigator.userAgent; + if (browser === 'ie') { + const isIE = ua.indexOf('compatible') > -1 && ua.indexOf('MSIE') > -1; + if (isIE) { + const reIE = new RegExp('MSIE (\\d+\\.\\d+);'); + reIE.test(ua); + return parseFloat(RegExp['$1']); + } else { + return false; + } + } else { + return ua.indexOf(browser) > -1; + } +} + +const csv = { + _isIE11() { + let iev = 0; + const ieold = (/MSIE (\d+\.\d+);/.test(navigator.userAgent)); + const trident = !!navigator.userAgent.match(/Trident\/7.0/); + const rv = navigator.userAgent.indexOf('rv:11.0'); + + if (ieold) { + iev = Number(RegExp.$1); + } + if (navigator.appVersion.indexOf('MSIE 10') !== -1) { + iev = 10; + } + if (trident && rv !== -1) { + iev = 11; + } + + return iev === 11; + }, + + _isEdge() { + return /Edge/.test(navigator.userAgent); + }, + + _getDownloadUrl(text) { + const BOM = '\uFEFF'; + // Add BOM to text for open in excel correctly + if (window.Blob && window.URL && window.URL.createObjectURL) { + const csvData = new Blob([BOM + text], { type: 'text/csv' }); + return URL.createObjectURL(csvData); + } else { + return 'data:attachment/csv;charset=utf-8,' + BOM + encodeURIComponent(text); + } + }, + + download(filename, text) { + if (has('ie') && has('ie') < 10) { + // has module unable identify ie11 and Edge + const oWin = window.top.open('about:blank', '_blank'); + oWin.document.charset = 'utf-8'; + oWin.document.write(text); + oWin.document.close(); + oWin.document.execCommand('SaveAs', filename); + oWin.close(); + } else if (has('ie') === 10 || this._isIE11() || this._isEdge()) { + const BOM = '\uFEFF'; + const csvData = new Blob([BOM + text], { type: 'text/csv' }); + navigator.msSaveBlob(csvData, filename); + } else { + const link = document.createElement('a'); + link.download = filename; + link.href = this._getDownloadUrl(text); + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + } + } +}; + +export default csv; diff --git a/frontend/src/components/table/header.js b/frontend/src/components/table/header.js new file mode 100644 index 00000000..0c31c6ac --- /dev/null +++ b/frontend/src/components/table/header.js @@ -0,0 +1,16 @@ +export default { + name: 'TableRenderHeader', + functional: true, + props: { + render: Function, + column: Object, + index: Number + }, + render: (h, ctx) => { + const params = { + column: ctx.props.column, + index: ctx.props.index + }; + return ctx.props.render(h, params); + } +}; diff --git a/frontend/src/components/table/index.js b/frontend/src/components/table/index.js new file mode 100644 index 00000000..087b4c9e --- /dev/null +++ b/frontend/src/components/table/index.js @@ -0,0 +1,2 @@ +import Table from './table.vue'; +export default Table; diff --git a/frontend/src/components/table/mixin.js b/frontend/src/components/table/mixin.js new file mode 100644 index 00000000..ecafae2d --- /dev/null +++ b/frontend/src/components/table/mixin.js @@ -0,0 +1,31 @@ +export default { + methods: { + alignCls(column, row = {}) { + let cellClassName = ''; + if (row.cellClassName && column.key && row.cellClassName[column.key]) { + cellClassName = row.cellClassName[column.key]; + } + return [ + { + [`${cellClassName}`]: cellClassName, // cell className + [`${column.className}`]: column.className, // column className + [`${this.prefixCls}-column-${column.align}`]: column.align, + [`${this.prefixCls}-hidden`]: (this.fixed === 'left' && column.fixed !== 'left') || (this.fixed === 'right' && column.fixed !== 'right') || (!this.fixed && column.fixed && (column.fixed === 'left' || column.fixed === 'right')) + } + ]; + }, + isPopperShow(column) { + return column.filters && ((!this.fixed && !column.fixed) || (this.fixed === 'left' && column.fixed === 'left') || (this.fixed === 'right' && column.fixed === 'right')); + }, + setCellWidth(column) { + let width = ''; + if (column.width) { + width = column.width; + } else if (this.columnsWidth[column._index]) { + width = this.columnsWidth[column._index].width; + } + if (width === '0') width = ''; + return width; + } + } +}; diff --git a/frontend/src/components/table/table-body.vue b/frontend/src/components/table/table-body.vue new file mode 100644 index 00000000..6dc5ad85 --- /dev/null +++ b/frontend/src/components/table/table-body.vue @@ -0,0 +1,245 @@ + + + + + diff --git a/frontend/src/components/table/table-head.vue b/frontend/src/components/table/table-head.vue new file mode 100644 index 00000000..e5877e0c --- /dev/null +++ b/frontend/src/components/table/table-head.vue @@ -0,0 +1,265 @@ + + diff --git a/frontend/src/components/table/table-tr.vue b/frontend/src/components/table/table-tr.vue new file mode 100644 index 00000000..8d07bfec --- /dev/null +++ b/frontend/src/components/table/table-tr.vue @@ -0,0 +1,35 @@ + + diff --git a/frontend/src/components/table/table.vue b/frontend/src/components/table/table.vue new file mode 100644 index 00000000..f4d0394b --- /dev/null +++ b/frontend/src/components/table/table.vue @@ -0,0 +1,1081 @@ + + diff --git a/frontend/src/components/table/util.js b/frontend/src/components/table/util.js new file mode 100644 index 00000000..c45d51a7 --- /dev/null +++ b/frontend/src/components/table/util.js @@ -0,0 +1,93 @@ +import { deepCopy } from './assist'; + +const convertColumnOrder = (columns, fixedType) => { + let list = []; + let other = []; + columns.forEach((col) => { + if (col.fixed && col.fixed === fixedType) { + list.push(col); + } else { + other.push(col); + } + }); + return list.concat(other); +}; + +export { convertColumnOrder }; + +// set forTableHead to true when convertToRows, false in normal cases like table.vue +const getAllColumns = (cols, forTableHead = false) => { + const columns = deepCopy(cols); + const result = []; + columns.forEach((column) => { + if (column.children) { + if (forTableHead) result.push(column); + result.push.apply(result, getAllColumns(column.children, forTableHead)); + } else { + result.push(column); + } + }); + return result; +}; + +export { getAllColumns }; + +const convertToRows = (columns, fixedType = false) => { + const originColumns = fixedType ? fixedType === 'left' ? deepCopy(convertColumnOrder(columns, 'left')) : deepCopy(convertColumnOrder(columns, 'right')) : deepCopy(columns); + let maxLevel = 1; + const traverse = (column, parent) => { + if (parent) { + column.level = parent.level + 1; + if (maxLevel < column.level) { + maxLevel = column.level; + } + } + if (column.children) { + let colSpan = 0; + column.children.forEach((subColumn) => { + traverse(subColumn, column); + colSpan += subColumn.colSpan; + }); + column.colSpan = colSpan; + } else { + column.colSpan = 1; + } + }; + + originColumns.forEach((column) => { + column.level = 1; + traverse(column); + }); + + const rows = []; + for (let i = 0; i < maxLevel; i++) { + rows.push([]); + } + + const allColumns = getAllColumns(originColumns, true); + + allColumns.forEach((column) => { + if (!column.children) { + column.rowSpan = maxLevel - column.level + 1; + } else { + column.rowSpan = 1; + } + rows[column.level - 1].push(column); + }); + + return rows; +}; + +export { convertToRows }; + +const getRandomStr = function(len = 32) { + const $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890'; + const maxPos = $chars.length; + let str = ''; + for (let i = 0; i < len; i++) { + str += $chars.charAt(Math.floor(Math.random() * maxPos)); + } + return str; +}; + +export { getRandomStr }; diff --git a/frontend/src/components/vue-bigdata-table/components/button.vue b/frontend/src/components/vue-bigdata-table/components/button.vue new file mode 100644 index 00000000..a4a473c3 --- /dev/null +++ b/frontend/src/components/vue-bigdata-table/components/button.vue @@ -0,0 +1,81 @@ + + + diff --git a/frontend/src/components/vue-bigdata-table/components/input-render.js b/frontend/src/components/vue-bigdata-table/components/input-render.js new file mode 100644 index 00000000..6f0f28cc --- /dev/null +++ b/frontend/src/components/vue-bigdata-table/components/input-render.js @@ -0,0 +1,59 @@ +import Input from './input.vue'; +import Button from './button.vue'; +export default (h, { row, col, value, beforeSave, initRowIndex }, table) => { + return h('div', { + 'class': 'edit-item-con' + }, [ + h(Input, { + 'class': 'edit-item-input', + props: { + value: value + }, + on: { + input(res) { + table.editContent = res; + } + } + }), + h('div', { + 'class': 'edit-item-btn-con' + }, [ + h(Button, { + 'class': 'edit-btn', + props: { + type: 'confirm' + }, + on: { + click() { + if (beforeSave({ row, col, value, initRowIndex })) { + table.$emit('on-success-save', { + row: row, + col: col, + value: table.editContent, + initRowIndex: initRowIndex + }); + } else { + table.$emit('on-fail-save', { + row: row, + col: col, + value: table.editContent, + initRowIndex: initRowIndex + }); + } + } + } + }), + h(Button, { + 'class': 'edit-btn', + props: { + type: 'cancel' + }, + on: { + click() { + table.$emit('on-cancel-edit'); + } + } + }) + ]) + ]); +}; diff --git a/frontend/src/components/vue-bigdata-table/components/input.vue b/frontend/src/components/vue-bigdata-table/components/input.vue new file mode 100644 index 00000000..7b68ab32 --- /dev/null +++ b/frontend/src/components/vue-bigdata-table/components/input.vue @@ -0,0 +1,46 @@ + + + diff --git a/frontend/src/components/vue-bigdata-table/components/item-table.vue b/frontend/src/components/vue-bigdata-table/components/item-table.vue new file mode 100755 index 00000000..ce8911c9 --- /dev/null +++ b/frontend/src/components/vue-bigdata-table/components/item-table.vue @@ -0,0 +1,279 @@ + + diff --git a/frontend/src/components/vue-bigdata-table/components/renderDom.js b/frontend/src/components/vue-bigdata-table/components/renderDom.js new file mode 100644 index 00000000..75649b7a --- /dev/null +++ b/frontend/src/components/vue-bigdata-table/components/renderDom.js @@ -0,0 +1,11 @@ +export default { + name: 'RenderCell', + functional: true, + props: { + render: Function, + backValue: [Number, Object] + }, + render: (h, ctx) => { + return ctx.props.render(h, ctx.props.backValue, ctx.parent); + } +}; diff --git a/frontend/src/components/vue-bigdata-table/components/sort-button.vue b/frontend/src/components/vue-bigdata-table/components/sort-button.vue new file mode 100644 index 00000000..a4dc8fc5 --- /dev/null +++ b/frontend/src/components/vue-bigdata-table/components/sort-button.vue @@ -0,0 +1,51 @@ + + diff --git a/frontend/src/components/vue-bigdata-table/index.js b/frontend/src/components/vue-bigdata-table/index.js new file mode 100644 index 00000000..25dd4ff8 --- /dev/null +++ b/frontend/src/components/vue-bigdata-table/index.js @@ -0,0 +1,2 @@ +import bigdataTable from './vue-bigdata-table.vue'; +export default bigdataTable; diff --git a/frontend/src/components/vue-bigdata-table/mixins/data-handle.js b/frontend/src/components/vue-bigdata-table/mixins/data-handle.js new file mode 100644 index 00000000..2efdde3d --- /dev/null +++ b/frontend/src/components/vue-bigdata-table/mixins/data-handle.js @@ -0,0 +1,217 @@ +import ItemTable from '../components/item-table.vue'; +import { iteratorByTimes, getHeaderWords } from '../util'; +export default { + data() { + return { + times0: 0, // 当前是第几轮 + times1: 0, + times2: -1, + table1Data: [], + table2Data: [], + table3Data: [], + currentIndex: 0, // 当前展示的表格是第几个 + itemNum: 0, // 一块数据显示的数据条数 + timer: null, + scrollLeft: 0, + insideTableData: [], + initTableData: [] // 初始表格数据,用于恢复搜索和筛选, + }; + }, + computed: { + cellNum() { // 表格列数 + return this.columnsHandled.length; + }, + columnsHandled() { + let columns = [...this.columns]; + if (this.colNum > this.columns.length) { + let colLength = this.colNum - this.columns.length; + let headerWordsArr = getHeaderWords(colLength); + iteratorByTimes(colLength, (i) => { + columns.push({ + title: headerWordsArr[i] + }); + }); + } + if (this.showIndex) { + columns.unshift({ + title: 'No', + align: 'center', + width: this.indexWidthInside + }); + } + return columns; + } + }, + methods: { + getComputedTableDataIndex(colIndex) { + return this.showIndex ? (colIndex - 1) : colIndex; + }, + handleScroll(e) { + let ele = e.srcElement || e.target; + let { scrollTop, scrollLeft } = ele; + this.scrollLeft = scrollLeft; + // let direction = (scrollTop - this.scrollTop) > 0 ? 1 : ((scrollTop - this.scrollTop) < 0 ? -1 : 0); // 1 => down -1 => up 0 => stop + this.currentIndex = parseInt((scrollTop % (this.moduleHeight * 3)) / this.moduleHeight); + this.scrollTop = scrollTop; + this.$nextTick(() => { + this.setTopPlace(); + }); + }, + setTableData() { + let count1 = this.times0 * this.itemNum * 3; + this.table1Data = this.insideTableData.slice(count1, count1 + this.itemNum); + let count2 = this.times1 * this.itemNum * 3; + this.table2Data = this.insideTableData.slice(count2 + this.itemNum, count2 + this.itemNum * 2); + let count3 = this.times2 * this.itemNum * 3; + this.table3Data = this.insideTableData.slice(count3 + this.itemNum * 2, count3 + this.itemNum * 3); + }, + getTables(h) { + let table1 = this.getItemTable(h, this.table1Data, 1); + let table2 = this.getItemTable(h, this.table2Data, 2); + let table3 = this.getItemTable(h, this.table3Data, 3); + if (this.currentIndex === 0) { + return [table1, table2, table3]; + } else if (this.currentIndex === 1) { + return [table2, table3, table1]; + } else { + return [table3, table1, table2]; + } + }, + renderTable(h) { + return h('div', { + style: this.tableWidthStyles + }, this.getTables(h)); + }, + getItemTable(h, data, index) { + return h(ItemTable, { + props: { + times: this['times' + (index - 1)], + tableIndex: index, + itemData: data, + itemNum: this.itemNum, + rowStyles: this.rowStyles, + widthArr: this.colWidthArr, + columns: this.columnsHandled, + showIndex: this.showIndex, + indexRender: this.indexRender, + stripe: this.stripe, + fixedCol: this.fixedCol, + currentScrollToRowIndex: this.currentScrollToRowIndex, + canEdit: this.canEdit, + edittingTd: this.edittingTd, + startEditType: this.startEditType, + showFixedBoxShadow: this.showFixedBoxShadow, + editCellRender: this.editCellRender, + beforeSave: this.beforeSave, + canSelectText: this.canSelectText, + startSelect: this.startSelect, + endSelect: this.endSelect, + disabledHover: this.disabledHover, + highlightRow: this.highlightRow, + highlightRowIndex: this.highlightRowIndex, + indexRenderParams: this.indexRenderParams + }, + on: { + 'on-click-tr': (index, initRowIndex) => { + if (this.highlightRow) this.highlightRowIndex = index; + this.$emit('on-click-tr', index, initRowIndex); + }, + 'on-click-td': (params) => { + this.$emit('on-click-td', params); + }, + 'on-edit-cell': (row, col) => { + // this.edittingTd = `${row}-${col}`; + this._editCell(row, col, false); + }, + 'on-success-save': ({ row, col, value, initRowIndex, oldValue }) => { + let data = [...this.value]; + data[initRowIndex][col] = value; + this.$emit('input', data); + this.$emit('on-success-save', { row, col, value, initRowIndex, oldValue }); + this.edittingTd = ''; + }, + 'on-fail-save': ({ row, col, value, initRowIndex }) => { + this.$emit('on-fail-save', { row, col, value, initRowIndex }); + }, + 'on-cancel-edit': () => { + this.edittingTd = ''; + }, + 'on-paste': (data) => { + if (!this.paste) return; + let value = [...this.value]; + let rowLength = data.length; + let startSelect = this.startSelect; + let endSelect = this.endSelect; + let startRow = startSelect.row; + let startCol = startSelect.col; + let endRow = endSelect.row; + let endCol = endSelect.col; + let selectRow = endRow - startRow + 1; + let selectCol = endCol - startCol + 1; + // let lastColLength = value[0].length - startCol; + // let lastRowLength = value.length - startRow; + if (rowLength === 0) return; + let colLength = data[0].length; + if (colLength === 0) return; + // 使用复制的数据替换原数据 + for (let r = 0; r < rowLength && r < selectRow; r++) { + for (let c = 0; c < colLength && c < selectCol; c++) { + let valueRow = startRow + r; + let valueCol = startCol + c; + if (typeof value[valueRow][valueCol] === 'object') { + value[valueRow][valueCol].value = data[r][c]; + } else { + value[valueRow][valueCol] = data[r][c]; + } + } + } + // for (let r = startRow; r < selectRow; r++) { + // for (let c = startCol; c < selectCol; c++) { + // // + // } + // } + this.$emit('input', value); + this.$emit('on-paste', data); + this._tableResize(); + } + }, + key: 'table-item-key' + index, + ref: 'itemTable' + index, + attrs: { + 'data-index': index + } + }); + }, + _scrollToIndexRow(index) { + index = parseInt(index); + if (isNaN(index) || index >= this.insideTableData.length || index < 0) return; + let scrollTop = index * this.itemRowHeight; + this.$refs.outer.scrollTop = scrollTop; + this.currentScrollToRowIndex = index; + clearTimeout(this.timer); + this.timer = setTimeout(() => { + this.currentScrollToRowIndex = -1; + }, 1800); + }, + // 给表格数据添加行号,用于排序后正确修改数据 + setInitIndex(tableData) { + return tableData.map((item, i) => { + let row = item; + row.initRowIndex = i; + return row; + }); + }, + // 获取指定行的初始行号 + _getInitRowIndexByIndex(index) { + return this.insideTableData[index].initRowIndex; + }, + _getIndexByInitRowIndex(index) { + let i = -1; + let len = this.insideTableData.length; + while (++i < len) { + let row = this.insideTableData[i]; + if (row.initRowIndex === index) return i; + } + } + } +}; diff --git a/frontend/src/components/vue-bigdata-table/mixins/edit.js b/frontend/src/components/vue-bigdata-table/mixins/edit.js new file mode 100644 index 00000000..0ba5d304 --- /dev/null +++ b/frontend/src/components/vue-bigdata-table/mixins/edit.js @@ -0,0 +1,90 @@ +import { findNodeUpper } from '../util'; +export default { + data() { + return { + edittingTd: '', // 正在编辑的单元格的行号和列号拼接的字符串 `${row}-${col}` + editContent: '', // 用来保存编辑的内容 + selectCellsStart: {}, // 编辑模式下可选中多行多列,此用来保存其实单元格行列号 + selectCellsEnd: {}, + selectTotalColStartIndex: -1, // 选取整列起始序列号 + selectTotalColEndIndex: -1 + }; + }, + computed: { + startSelect() { + return { + row: Math.min(this.selectCellsStart.row, this.selectCellsEnd.row), + col: Math.min(this.selectCellsStart.col, this.selectCellsEnd.col) + }; + }, + endSelect() { + return { + row: Math.max(this.selectCellsStart.row, this.selectCellsEnd.row), + col: Math.max(this.selectCellsStart.col, this.selectCellsEnd.col) + }; + } + }, + watch: { + selectable() { + this.selectCellsStart = { + row: -1, + col: -1 + }; + this.selectCellsEnd = { + row: -1, + col: -1 + }; + } + }, + methods: { + _editCell(row, col, scrollToView = true) { + if (!this.canEdit || row < 0 || row > this.insideTableData.length || col < 0 || col > this.columns.length || this.edittingTd === `${row}-${col}`) return; + if (scrollToView && parseInt(this.edittingTd.split('-')[0]) !== row) this.scrollToRow(row); + this.edittingTd = `${row}-${col}`; + }, + handleMousedownOnTable(e) { + if (e.button !== 0 || (!this.paste && !this.selectable)) return; + let currentTd = e.target.tagName === 'TD' ? e.target : findNodeUpper(e.target, 'td'); + this.selectCellsStart = { + row: currentTd.getAttribute('data-row'), + col: currentTd.getAttribute('data-col') + }; + this.selectCellsEnd = { + row: currentTd.getAttribute('data-row'), + col: currentTd.getAttribute('data-col') + }; + this.canSelectText = false; + document.addEventListener('mousemove', this.handleMoveOnTable); + document.addEventListener('mouseup', this.handleUpOnTable); + }, + handleMoveOnTable(e) { + if (!(e.target.tagName === 'TD' || findNodeUpper(e.target, 'td'))) return; + let currentTd = e.target.tagName === 'TD' ? e.target : findNodeUpper(e.target, 'td'); + this.selectCellsEnd = { + row: currentTd.getAttribute('data-row'), + col: currentTd.getAttribute('data-col') + }; + }, + handleUpOnTable(e) { + if (!this.paste && !this.selectable) return; + this.canSelectText = true; + this.handleMoveOnTable(e); + document.removeEventListener('mousemove', this.handleMoveOnTable); + document.removeEventListener('mouseup', this.handleUpOnTable); + this.$emit('on-select-cells', { + start: { + row: this.startSelect.row, + col: this.startSelect.col + }, + end: { + row: this.endSelect.row, + col: this.endSelect.col + } + }); + }, + _selectCell(row, col) { + this.selectCellsStart = { row, col }; + this.selectCellsEnd = { row, col }; + } + } +}; diff --git a/frontend/src/components/vue-bigdata-table/mixins/empty-table.js b/frontend/src/components/vue-bigdata-table/mixins/empty-table.js new file mode 100644 index 00000000..221f7cd1 --- /dev/null +++ b/frontend/src/components/vue-bigdata-table/mixins/empty-table.js @@ -0,0 +1,31 @@ +export default { + methods: { + _createEmptyData() { + // this.$nextTick(() => { + let rowNum = this.rowNum; + let colNum = this.colNum; + if (this.rowNum && this.colNum) { + console.log(this.value.length, this.rowNum, this.colNum); + let valueRowNum = this.value.length; + let valueColNum = this.value[0] ? this.value[0].length : 0; + let totalRowNum = valueRowNum + rowNum; + let totalColNum = valueColNum + colNum; + let value = [...this.value]; + console.log(totalRowNum, valueRowNum); + for (let r = valueRowNum; r < totalRowNum; r++) { + value.push([]); + for (let c = valueColNum; c < totalColNum; c++) { + value[r].push(''); + } + } + // this. + console.log(value); + this.$emit('input', value); + // this.$nextTick(() => { + // this._tableResize(); + // }) + } + // }); + } + } +}; diff --git a/frontend/src/components/vue-bigdata-table/mixins/filter.js b/frontend/src/components/vue-bigdata-table/mixins/filter.js new file mode 100644 index 00000000..bcb2c13f --- /dev/null +++ b/frontend/src/components/vue-bigdata-table/mixins/filter.js @@ -0,0 +1,14 @@ +import { hasOneOf } from '../util'; +export default { + methods: { + _filter(col, queryArr) { + let value = [...this.value]; + this.insideTableData = value.filter(item => hasOneOf(item[col], queryArr)); + this._tableResize(); + }, + _cancelFilter() { + this.insideTableData = [...this.value]; + this._tableResize(); + } + } +}; diff --git a/frontend/src/components/vue-bigdata-table/mixins/header-move.js b/frontend/src/components/vue-bigdata-table/mixins/header-move.js new file mode 100644 index 00000000..cb03dc63 --- /dev/null +++ b/frontend/src/components/vue-bigdata-table/mixins/header-move.js @@ -0,0 +1,65 @@ +import { findNodeUpper } from '../util'; +export default { + data() { + return { + isOnCellEdge: false, // 鼠标是否在表头的两个单元格之间的边框上 + canResizeCell: false, + initCellX: 0, // 用于计算鼠标移动的距离 + scrollLeft: 0, + colIndex: 0, // 在表头上移动时鼠标所在列的序号, + atLeftGivenArea: false, // 是否在表头单元格指定区域(距左侧) + atRightGivenArea: false // 是否在表头单元格指定区域(距右侧) + }; + }, + methods: { + handleMousemove(e) { + const target = e.srcElement || e.target; + let cell = target.tagName.toUpperCase() === 'TH' ? target : findNodeUpper(target, 'th'); + let cellDomRect = cell.getBoundingClientRect(); + let atLeft = (e.pageX - cellDomRect.left) < (cellDomRect.width / 2); + let atLeftGivenArea = (cellDomRect.left + this.atLeftCellPosi) >= e.pageX; + let atRightGivenArea = (cellDomRect.right - e.pageX) <= this.atRightCellPosi; + let cellIndex = parseInt(cell.getAttribute('data-index')); // 当前单元格的序号 + if (atLeft && cellIndex !== 0) { + this.isOnCellEdge = (e.pageX - cellDomRect.left) <= 1 && cellIndex - 1 !== this.fixedCol; + } else if (!atLeft && cellIndex !== this.cellNum - 1) { + this.isOnCellEdge = (cellDomRect.right - e.pageX) <= 1 && cellIndex !== this.fixedCol; + } + e.atRightGivenArea = atRightGivenArea; + e.atLeftGivenArea = atLeftGivenArea; + this.atRightGivenArea = atRightGivenArea; + this.atLeftGivenArea = atLeftGivenArea; + let index = 0; // 调整表格列宽的左侧的表格的序列 + e.colIndex = cellIndex; + this.colIndex = cellIndex; + this.$emit('on-moving-on-header', e); + if (this.canResizeCell) { + if (atLeft) { + index = cellIndex - 1; + } else { + index = cellIndex; + } + if (index === this.fixedCol) return; + let widthLeft = this.widthArr[index] + e.pageX - this.initCellX; + let widthRight = this.widthArr[index + 1] + this.initCellX - e.pageX; + this.widthArr.splice(index, 2, widthLeft, widthRight); + this.initCellX = e.pageX; + } + }, + handleMousedown(e) { + e.colIndex = this.cellIndex; + this.$emit('on-mousedown-on-header', e); + if (this.isOnCellEdge) { + this.canResizeCell = true; + this.initCellX = e.pageX; + } + }, + canNotMove(e) { + this.canResizeCell = false; + e.colIndex = this.colIndex; + e.atLeftGivenArea = this.atLeftGivenArea; + e.atRightGivenArea = this.atRightGivenArea; + this.$emit('on-mouseup-on-header', e); + } + } +}; diff --git a/frontend/src/components/vue-bigdata-table/mixins/index.js b/frontend/src/components/vue-bigdata-table/mixins/index.js new file mode 100644 index 00000000..426f0de6 --- /dev/null +++ b/frontend/src/components/vue-bigdata-table/mixins/index.js @@ -0,0 +1,9 @@ +import headerMove from './header-move'; +import styleComputed from './style-compute'; +import dataHandle from './data-handle'; +import edit from './edit'; +import emptyTable from './empty-table'; +import sort from './sort'; +import filter from './filter'; + +export default [headerMove, styleComputed, dataHandle, edit, emptyTable, sort, filter]; diff --git a/frontend/src/components/vue-bigdata-table/mixins/sort.js b/frontend/src/components/vue-bigdata-table/mixins/sort.js new file mode 100644 index 00000000..d30daa30 --- /dev/null +++ b/frontend/src/components/vue-bigdata-table/mixins/sort.js @@ -0,0 +1,40 @@ +import { sortArr, sortDesArr } from '../util'; +export default { + data() { + return { + sortedByColIndex: -1, + sortedType: '' + }; + }, + methods: { + showSortBtn(colIndex) { + const sortable = this.sortable ? true : this.sortIndex !== undefined; + return (sortable && !(this.showIndex && colIndex === 0) && (typeof this.sortIndex === 'number' ? colIndex <= this.sortIndex : this.sortIndex.indexOf(colIndex) >= 0)) || this.columnsHandled[colIndex].sortable; + }, + handleSort(colIndex, sortType) { + this.sortedByColIndex = colIndex; + this.sortedType = sortType; + let valueArr = [...this.value]; + colIndex = this.showIndex ? colIndex - 1 : colIndex; + if (sortType === 'up') { + sortArr(valueArr, colIndex); + } else { + sortDesArr(valueArr, colIndex); + } + this.insideTableData = [...valueArr]; + }, + handleCancelSort() { + this.sortedByColIndex = -1; + this.sortedType = ''; + this.insideTableData = [...this.value]; + }, + initSort() { + if (this.defaultSort) { + const colIndex = parseInt(Object.keys(this.defaultSort)[0]); + if (!(colIndex || colIndex === 0)) return; + const sortType = this.defaultSort[colIndex]; + this.handleSort(colIndex, sortType); + } + } + } +}; diff --git a/frontend/src/components/vue-bigdata-table/mixins/style-compute.js b/frontend/src/components/vue-bigdata-table/mixins/style-compute.js new file mode 100644 index 00000000..f7958a1b --- /dev/null +++ b/frontend/src/components/vue-bigdata-table/mixins/style-compute.js @@ -0,0 +1,202 @@ +import { getScrollbarWidth } from '../util'; +export default { + data() { + return { + wrapperHeight: 0, + scrollTop: 0, + moduleHeight: 0, // 三个tr块中的一块的高度 + topPlaceholderHeight: 0, // 顶部占位容器高度 + tableWidth: 0, + widthArr: [], // 用于给数据表格传递列宽 + totalRowHeight: 0, // 如果全量渲染应该是多高,用于计算占位 + currentScrollToRowIndex: -1, // 当前跳转到的行号,用于做闪烁提示 + canSelectText: true, // 用于控制是否可选中表格文字 + indexWidthInside: 0, + outerWidth: 0, // 外面容器宽度 + oldTableWidth: 0, // 旧的表格宽度,用于重新计算列宽 + highlightRowIndex: -1, // 高亮行号 + updateID: 0 + }; + }, + computed: { + fixedColCom() { + return this.showIndex ? (this.fixedCol + 1) : this.fixedCol; + }, + wrapperClasses() { + return [ + this.prefix, + this.fixed ? `${this.prefix}-fixed` : '' + ]; + }, + headerStyle() { + return { + height: this.headerHeight + 'px', + transform: 'translateX(0)' + }; + }, + showFixedBoxShadow() { + return this.scrollLeft !== 0; + }, + tableWidthStyles() { + return { width: this.tableWidth + 'px' }; + }, + rowStyles() { + return this.rowHeight !== undefined ? { height: `${this.rowHeight}px` } : {}; + }, + placeholderHeight() { + return this.totalRowHeight - this.moduleHeight * 3; // 占位容器的总高度(上 + 下) + }, + bottomPlaceholderHeight() { + return (this.placeholderHeight - this.topPlaceholderHeight) < 0 ? 0 : this.placeholderHeight - this.topPlaceholderHeight; + }, + itemRowHeight() { + return this.rowHeight === undefined ? 48 : this.rowHeight; + }, + colWidthArr() { + let len = this.cellNum; + let colWidthArr = []; + if (this.fixedWrapperWidth) { + let width = this.outerWidth; + let num = this.cellNum; + if (this.showIndex) { + colWidthArr.push(this.indexWidth); + width -= this.indexWidth; + num -= 1; + } + let i = -1; + let itemColWidth = width / num; + while (++i < num) { + colWidthArr.push(itemColWidth); + } + } else { + let i = 0; + let hasWidthCellCount = 0; // 统计设置了width的列的数量,从而为没有设置width的列分配宽度 + let noWidthCellIndexArr = []; // 没有设置宽度的列的序列 + let hasWidthCellTotalWidth = 0; // 设置了width的列一共多宽 + while (i < len) { + if (this.columnsHandled[i].width) { + hasWidthCellCount++; + hasWidthCellTotalWidth += this.columnsHandled[i].width; + colWidthArr.push(this.columnsHandled[i].width); + } else { + noWidthCellIndexArr.push(i); + colWidthArr.push(0); + } + i++; + } + let noWidthCellWidth = (this.tableWidth - hasWidthCellTotalWidth) / (len - hasWidthCellCount); + let w = 0; + let indexArrLen = noWidthCellIndexArr.length; + while (w < indexArrLen) { + colWidthArr[noWidthCellIndexArr[w]] = noWidthCellWidth; + w++; + } + // this.widthArr = colWidthArr; + } + return colWidthArr; + }, + cursorOnHeader() { + return this.headerTrStyle.cursor ? this.headerTrStyle.cursor : ((this.isOnCellEdge || this.canResizeCell) ? 'col-resize' : 'default'); + } + }, + watch: { + highlightRow() { + this._clearCurrentRow(); + } + }, + methods: { + _tableResize() { + this.$nextTick(() => { + this.updateHeight(); + this.setComputedProps(); + let scrollBarWidth = this.totalRowHeight > this.wrapperHeight ? getScrollbarWidth() : 0; + this.outerWidth = this.$refs.outer.offsetWidth - 2 - scrollBarWidth; + let width = this.colWidth * this.columns.length + (this.showIndex ? this.indexWidthInside : 0); + // this.tableWidth = width > this.outerWidth ? width : this.outerWidth; + this.tableWidth = this.fixedWrapperWidth ? this.outerWidth : (width > this.outerWidth ? width : this.outerWidth); + if (width < this.outerWidth) this._setColWidthArr(); + this.widthArr = this.colWidthArr; + }); + }, + updateHeight() { + this.$nextTick(() => { + let wrapperHeight = this.$refs.outer.offsetHeight; + this.itemNum = Math.ceil((wrapperHeight - this.headerHeight) / this.itemRowHeight) + this.appendNum; + this.moduleHeight = this.itemNum * this.itemRowHeight; + this.wrapperHeight = wrapperHeight; + this.setTopPlace(); + }); + }, + setComputedProps() { + const len = this.insideTableData.length; + this.totalRowHeight = len * this.itemRowHeight; + }, + setIndexWidth(len) { + let width = 70; + if (len <= 99) { + width = 50; + } else if (len > 99 && len <= 1000) { + width = 60; + } else if (len > 1000 && len <= 10000) { + width = 70; + } else if (len > 10000 && len <= 100000) { + width = 90; + } else { + width = 100; + } + return width; + }, + setTopPlace() { + let scrollTop = this.scrollTop; + let t0 = 0; + let t1 = 0; + let t2 = 0; + if (scrollTop > this.moduleHeight) { + switch (this.currentIndex) { + case 0: + t0 = parseInt(scrollTop / (this.moduleHeight * 3)); + t1 = t2 = t0; + break; + case 1: + t1 = parseInt((scrollTop - this.moduleHeight) / (this.moduleHeight * 3)); + t0 = t1 + 1; + t2 = t1; + break; + case 2: + t2 = parseInt((scrollTop - this.moduleHeight * 2) / (this.moduleHeight * 3)); + t0 = t1 = t2 + 1; + } + } + this.times0 = t0; + this.times1 = t1; + this.times2 = t2; + this.topPlaceholderHeight = parseInt(scrollTop / this.moduleHeight) * this.moduleHeight; + this.setTableData(); + }, + _initMountedHandle() { + if (this.indexWidth === undefined) this.indexWidthInside = this.setIndexWidth(this.insideTableData.length); + else this.indexWidthInside = this.indexWidth; + this.oldTableWidth = this.colWidthArr.reduce((sum, b) => { + return sum + b; + }, 0); + this.widthArr = this.colWidthArr; + if ((this.colWidth * this.columns.length + (this.showIndex ? this.indexWidthInside : 0)) < this.outerWidth) this._setColWidthArr(); + }, + _setColWidthArr() { + let widthArr = this.widthArr.map(width => { + return width / this.oldTableWidth * this.tableWidth; + }); + this.oldTableWidth = this.tableWidth; + this.widthArr = widthArr; + }, + _clearCurrentRow() { + this.highlightRowIndex = -1; + }, + refreshHeader() { + this.updateID++; + }, + _setHighlightRow(row) { + this.highlightRowIndex = row; + } + } +}; diff --git a/frontend/src/components/vue-bigdata-table/util/index.js b/frontend/src/components/vue-bigdata-table/util/index.js new file mode 100644 index 00000000..d235b557 --- /dev/null +++ b/frontend/src/components/vue-bigdata-table/util/index.js @@ -0,0 +1,136 @@ +export const findNodeUpper = (ele, tag) => { + if (ele.parentNode) { + if (ele.parentNode.tagName === tag.toUpperCase()) { + return ele.parentNode; + } else { + if (ele.parentNode) return findNodeUpper(ele.parentNode, tag); + else return false; + } + } +}; + +export const getScrollbarWidth = () => { + let oP = document.createElement('p'); + let styles = { + width: '100px', + height: '100px', + overflowY: 'scroll' + }; + for (let i in styles) { + oP.style[i] = styles[i]; + } + document.body.appendChild(oP); + let scrollbarWidth = oP.offsetWidth - oP.clientWidth; + oP.remove(); + return scrollbarWidth; +}; + +export const createNewArray = (length, content = undefined) => { + let i = -1; + let arr = []; + while (++i < length) { + let con = Array.isArray(content) ? content[i] : content; + arr.push(con); + } + return arr; +}; + +export const iteratorByTimes = (times, fn) => { + let i = -1; + while (++i < times) { + fn(i); + } +}; + +export const getHeaderWords = (length) => { + let wordsArr = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z']; + let headerArr = []; + if (length <= 26) { + headerArr = wordsArr.slice(0, length); + } else { + headerArr = [...wordsArr]; + let num = length - 26; + let firstWordIndex = 0; + let secondWordIndex = 0; + let i = -1; + while (++i < num) { + firstWordIndex = Math.floor(i / 26); + secondWordIndex = i % 26; + let sumWord = `${wordsArr[firstWordIndex]}${wordsArr[secondWordIndex]}`; + headerArr.push(sumWord); + } + } + return headerArr; +}; + +// 获取数组中第一个不为空的值 +export const getFirstNotNullValue = (array, index) => { + if (!(array && array.length)) return false; + let r = -1; + let rowLength = array.length; + while (++r < rowLength) { + let item = array[r][index]; + if (item || item === 0) return item; + } + return false; +}; + +export const sortArr = (arr, index) => { + const isChineseReg = new RegExp('[\\u4E00-\\u9FFF]+', 'g'); + if (arr.length <= 1) return; + const firstNotNullValue = getFirstNotNullValue(arr, index); + if (!firstNotNullValue && firstNotNullValue !== 0) return; + if (!isChineseReg.test(firstNotNullValue)) { + if (isNaN(Number(firstNotNullValue))) { + // 非中文非数值 + arr.sort(); + } else { + // 数值型 + arr.sort((a, b) => { + return a[index] - b[index]; + }); + } + } else { + arr.sort((a, b) => { + return a[index].localeCompare(b[index], 'zh'); + }); + } +}; + +// 倒序 +export const sortDesArr = (arr, index) => { + const isChineseReg = new RegExp('[\\u4E00-\\u9FFF]+', 'g'); + if (arr.length <= 1) return; + const firstNotNullValue = getFirstNotNullValue(arr, index); + if (!firstNotNullValue && firstNotNullValue !== 0) return; + if (!isChineseReg.test(firstNotNullValue)) { + if (isNaN(Number(firstNotNullValue))) { + // 非中文非数值 + arr.sort().reverse(); + } else { + // 数值型 + arr.sort((a, b) => { + return b[index] - a[index]; + }); + } + } else { + arr.sort((a, b) => { + return b[index].localeCompare(a[index], 'zh'); + }); + } +}; + +export const hasOneOf = (str, targetArr) => { + let len = targetArr.length; + let i = -1; + while (++i < len) { + if (str.indexOf(targetArr[i]) >= 0) { + return true; + } + } + return false; +}; + +export const oneOf = (ele, targetArr) => { + return targetArr.indexOf(ele) > -1; +}; diff --git a/frontend/src/components/vue-bigdata-table/vue-bigdata-table.less b/frontend/src/components/vue-bigdata-table/vue-bigdata-table.less new file mode 100755 index 00000000..4dbe6a15 --- /dev/null +++ b/frontend/src/components/vue-bigdata-table/vue-bigdata-table.less @@ -0,0 +1,311 @@ +@prefix: ~"vue-bigdata-table"; +@select-border-color: #3695FE; + +@keyframes scroll-tip { + 0% { + background: #fff; + } + + 50% { + background: #d0e8ff; + } +} + +.@{prefix} { + width: 100%; + box-sizing: border-box; + + * { + font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "微软雅黑", Arial, sans-serif; + color: #495060; + font-size: 12px; + font-weight: 400; + } + + &-outer { + width: 100%; + height: 100%; + overflow: auto; + border: 1px solid #e9eaec; + box-sizing: border-box; + position: relative; + + .@{prefix}-header-wrapper { + box-sizing: border-box; + z-index: 70; + + &.header-wrapper-fixed { + position: -webkit-sticky; + position: sticky; + } + + table { + table-layout: fixed; + height: 100%; + + tr th { + border-right: 1px solid #e9eaec; + border-bottom: 1px solid #e9eaec; + background: #fff; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + word-break: break-all; + + // &:last-child{ + // border-right: none; + // } + .@{prefix}-header-inside-wrapper { + box-sizing: border-box; + padding: 0 8px; + } + } + } + } + } + + &-fixed-header { + position: -webkit-sticky; + position: sticky; + transform: translateX(0); + left: 0; + z-index: 110; + transition: box-shadow .2s ease; + + &.box-shadow { + box-shadow: 2px 0 6px -2px rgba(0, 0, 0, .2); + transition: box-shadow .2s ease; + } + } + + &-wrapper { + width: 100%; + border-bottom: none; + + .@{prefix}-content { + width: 100%; + height: auto; + // &.noselect-text{ + // -webkit-touch-callout: none; + // -webkit-user-select: none; + // -khtml-user-select: none; + // -moz-user-select: none; + // -ms-user-select: none; + // user-select: none; + // } + } + + &:nth-child(2) { + border-top: 1px solid #e9eaec; + } + + &:nth-child(4) { + border-bottom: 1px solid #e9eaec; + } + + .@{prefix}-data-table { + &.@{prefix}-content-table { + + left: 0; + top: 0; + } + + border-bottom: none; + border-top: none; + table-layout: fixed; + + tr { + background: #fff; + + &.scroll-to-row-tip { + animation: scroll-tip .6s 3; + } + + td { + min-width: 0; + height: 48px; + box-sizing: border-box; + overflow: hidden; + text-overflow: ellipsis; + vertical-align: middle; + border-bottom: 1px solid #e9eaec; + border-right: 1px solid #e9eaec; + + border-left: 1px solid transparent; + border-top: 1px solid transparent; // 表格选中 + + .@{prefix}-cell { + box-sizing: border-box; + padding: 0 18px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + word-break: break-all; + } + + // &:last-child{ + // border-right: none; + // } + .edit-item-con { + width: 100%; + text-align: left; + padding: 0 6px; + box-sizing: border-box; + + .edit-item { + &-input { + width: ~"calc(100% - 50px)"; + float: left; + } + + &-btn-con { + float: left; + + .edit-btn { + width: 20px; + margin: 7px 4px 0 0; + } + } + } + } + + &.start-select-cell { + border-left: 1px solid @select-border-color; + border-top: 1px solid @select-border-color; + } + + &.end-select-cell { + border-right: 1px solid @select-border-color; + border-bottom: 1px solid @select-border-color; + } + + &.right-top-select-cell { + border-right: 1px solid @select-border-color; + border-top: 1px solid @select-border-color; + } + + &.left-bottom-select-cell { + border-left: 1px solid @select-border-color; + border-bottom: 1px solid @select-border-color; + } + + &.top-center-select-cell { + border-top: 1px solid @select-border-color; + } + + &.bottom-center-select-cell { + border-bottom: 1px solid @select-border-color; + } + + &.left-center-select-cell { + border-left: 1px solid @select-border-color; + } + + &.right-center-select-cell { + border-right: 1px solid @select-border-color; + } + } + + &.stripe-gray { + background: #f8f8f9; + } + + &.highlight-row { + background: #ebf7ff; + } + } + + &-left { + text-align: left; + } + + &-center { + text-align: center; + } + + &-right { + text-align: right; + } + } + } + + &-fixed { + .@{prefix}-header-wrapper { + top: 0; + left: 0; + box-sizing: border-box; + } + } + + &-fixed-table { + position: -webkit-sticky; + position: sticky; + left: 0; + z-index: 60; + transition: box-shadow .2s ease; + + &.box-shadow { + box-shadow: 2px 0 6px -2px rgba(0, 0, 0, .2); + transition: box-shadow .2s ease; + } + + td { + border-right: 1px solid #e9eaec; + // &:last-child{ + // border-right: 1px solid #e9eaec !important; + // } + } + } + + &-item-table { + position: relative; + } + + .sort-button { + &-wrapper { + display: inline-block; + position: relative; + width: 10px; + height: 11px; + transform: translateY(1px); + } + + &-item { + position: absolute; + display: inline-block; + width: 0; + height: 0; + border: 4px solid transparent; + margin: 0; + padding: 0; + cursor: pointer; + transition: border-color .2s ease; + + &-up { + top: -4px; + border-bottom: 4px solid #bbbec4; + + &:hover { + border-bottom: 4px solid #495060; + } + + &-active { + border-bottom: 4px solid #2d8cf0; + } + } + + &-down { + bottom: -4px; + border-top: 4px solid #bbbec4; + + &:hover { + border-top: 4px solid #495060; + } + + &-active { + border-top: 4px solid #2d8cf0; + } + } + } + } +} diff --git a/frontend/src/components/vue-bigdata-table/vue-bigdata-table.vue b/frontend/src/components/vue-bigdata-table/vue-bigdata-table.vue new file mode 100644 index 00000000..65bd8c1e --- /dev/null +++ b/frontend/src/components/vue-bigdata-table/vue-bigdata-table.vue @@ -0,0 +1,441 @@ + + + + diff --git a/frontend/src/views/virtual/orders/cards.vue b/frontend/src/views/virtual/orders/cards.vue index ec2db8ba..65aa8957 100644 --- a/frontend/src/views/virtual/orders/cards.vue +++ b/frontend/src/views/virtual/orders/cards.vue @@ -127,15 +127,14 @@ -
+ :height="449" + > @@ -148,3 +147,14 @@ + + + diff --git a/frontend/src/views/virtual/orders/js/cards.js b/frontend/src/views/virtual/orders/js/cards.js index ce8f62d9..74499809 100644 --- a/frontend/src/views/virtual/orders/js/cards.js +++ b/frontend/src/views/virtual/orders/js/cards.js @@ -1,4 +1,7 @@ export default { + components: { + BTable: resolve => require(['components/table'], resolve) + }, props: { show: { type: Boolean, @@ -20,13 +23,17 @@ export default { show: true }, params: { - company_id: "", - package_id: "", - carrier_operator: "", - time: [], - used: 0, - sim: "" + company_id: '', + package_id: '', + carrier_operator: '', + time: [ + this.moment().subtract('2', 'months').startOf('month').format('YYYY-MM-DD'), + this.moment().subtract('2', 'months').endOf('month').format('YYYY-MM-DD') + ], + used: '', + sim: '' }, + showCards: [], orderColumns: [ { width: 60, @@ -96,52 +103,52 @@ export default { { title: "订单编号", key: "sn", - width: 200 + width: 165 }, { title: "企业名称", key: "company_name", - width: 280 + width: 210 }, { title: "运营商", key: "carrier_operator_name", - width: 90 + width: 75 }, { title: "套餐名称", key: "package_name", - width: 180 + width: 150 }, { title: "支付方式", key: "pay_channel_name", - width: 100 + width: 90 }, { title: "订单卡量", key: "counts", - width: 100 + width: 90 }, { title: "订单金额", key: "total_price", - width: 120 + width: 100 }, { title: "订单时间", key: "order_at", - width: 170 + width: 150 }, { title: "所需卡量", key: "", - width: 100 + width: 90 } ], - cardColumns: [ + cardColumns2: [ { width: 60, align: "center", @@ -201,6 +208,11 @@ export default { }); } }, + { + title: "序号", + key: "_index", + width: 140 + }, { title: "SIM", key: "sim", @@ -231,6 +243,87 @@ export default { key: "package_name", width: 200 } + ], + + cardColumns: [ + { + width: 60, + align: 'center', + render: (h) => { + let value = false; + let indeterminate = false; + + let select = this.selected.find(item => { + return item.id === this.order_id; + }); + + value = !!select; + + indeterminate = + select && + select.cards && + select.cards.length !== this.cards.length; + + return h("Checkbox", { + props: { + indeterminate: value && indeterminate, + value: value + }, + on: { + input: value => { + console.log(value); + } + } + }); + }, + cellRender: (h, context) => { + let value = !!context.value; + + return h("Checkbox", { + props: { + value: value + }, + on: { + input: value => { + console.log(value); + } + } + }); + } + }, + { + title: "SIM", + width: 135, + align: 'center' + }, + { + title: "状态", + width: 100, + align: 'center', + cellRender: (h, context) => { + return h( + "Tag", { + props: { + color: context.value ? "error" : "primary" + } + }, + context.value ? "已使用" : "未使用" + ); + } + }, + { + title: "数量", + width: 80, + align: 'center' + }, + { + title: "VD企业", + width: 210 + }, + { + title: "VD套餐", + width: 150 + } ] }; }, @@ -297,6 +390,17 @@ export default { this.$store .dispatch("getCards", params) .then(cards => { + this.showCards = cards.map(item => { + return [ + false, + item.sim, + item.virtual_order_id ? 1 : 0, + item.counts, + item.virtual ? item.virtual.company_name : '', + item.virtual ? item.virtual.package_name : '' + ]; + }); + this.cardLoading = false; resolve(cards); }) @@ -316,12 +420,13 @@ export default { }, resetSearch() { for (let k in this.params) { - if (k === "time") { - this.params[k] = []; - } else if (k === "used") { - this.params[k] = 0; + if (k === 'time') { + this.params[k] = [ + this.moment().subtract('2', 'months').startOf('month').format('YYYY-MM-DD'), + this.moment().subtract('2', 'months').endOf('month').format('YYYY-MM-DD') + ]; } else { - this.params[k] = ""; + this.params[k] = ''; } } this.index(1); @@ -335,15 +440,20 @@ export default { cards: cards }; - this.cards = this.cards.map(item => { - item._checked = true; + this.showCards = this.showCards.map(item => { + item[0] = true; return item; }); - this.$store.commit("PUSH_REAL_ORDER_SELECTED", obj); + this.$store.commit('PUSH_REAL_ORDER_SELECTED', obj); }); } else { - this.$store.commit("REMOVE_REAL_ORDER_SELECTED", row.id); + this.showCards = this.showCards.map(item => { + item[0] = false; + return item; + }); + + this.$store.commit('REMOVE_REAL_ORDER_SELECTED', row.id); } }, handleSelectCards(t, a, b) { diff --git a/frontend/src/views/virtual/orders/js/edit.js b/frontend/src/views/virtual/orders/js/edit.js index 93ea231c..b6c60bdf 100644 --- a/frontend/src/views/virtual/orders/js/edit.js +++ b/frontend/src/views/virtual/orders/js/edit.js @@ -144,6 +144,9 @@ export default { this.params[k] = ''; } + this.params.unit_price = 0; + this.params.counts = 0; + this.params.area = []; this.my_show = false; }, handleChange(type) { diff --git a/frontend/vue.config.js b/frontend/vue.config.js index 09efd1a2..f840ae7e 100644 --- a/frontend/vue.config.js +++ b/frontend/vue.config.js @@ -10,6 +10,7 @@ module.exports = { lintOnSave: true, chainWebpack: config => { config.resolve.alias + .set('node_modules', resolve('node_modules')) .set('@', resolve('src')) // key,value自行定义,比如.set('@@', resolve('src/components')) .set('src', resolve('src')) .set('views', resolve('src/views')) diff --git a/tests/SchemaTest.php b/tests/SchemaTest.php index 80ab7075..464b7e70 100644 --- a/tests/SchemaTest.php +++ b/tests/SchemaTest.php @@ -1,13 +1,20 @@ truncate(); -Schema::create('real_order_cards_table_partition', function (Blueprint $table) { +DB::table('virtual_order_cards_partition')->truncate(); + +dd(); + +Schema::dropIfExists('real_order_cards_partition'); + +Schema::create('real_order_cards_partition', function (Blueprint $table) { $table->increments('id')->comment('关联表ID'); $table->tinyInteger('type')->unsigned()->default(0)->comment('订单类型(1:套餐续费 2:续费包 3:加油包 4:可选包 5:附加包)'); $table->bigInteger('sim')->unsigned()->default(0)->comment('sim号');