virtualizedtableforantd4空白bug修复

在业务开发过程中,使用了antd的table,为渲染大量的数据并进行性能调优,使用了virtualizedtableforantd4组件,具体如何使用参考https://cnpmjs.org/package/vi...
【virtualizedtableforantd4空白bug修复】因为页面需要动态改变table的高度,所以scroll的y使用了动态计算
virtualizedtableforantd4空白bug修复
文章图片

在渲染table的时候,出现了大量的空白,如下图所示。
virtualizedtableforantd4空白bug修复
文章图片

于是去看了下源码,问题出现在scroll.y使用了字符串计算,设置为字符串的时候,
ctx.y会获取parentElement.offsetHeight,而因为渲染问题,所获取的值为0,
最终所获得的tail为默认的10(tail就是j,overscanRowCount默认为5),所以出现了空白现象。

else if (typeof scroll_y === "string") { /* a string, like "calc(100vh - 300px)" */ if (ctx.debug) console.warn("AntD.Table.scroll.y: ".concat(scroll_y, ", it may cause performance problems.")); ctx._raw_y = scroll_y; ctx._y = ctx.wrap_inst.current.parentElement.offsetHeight; }

virtualizedtableforantd4空白bug修复
文章图片

知道了原因后,就好解决了,既然你获取不到offsetHeight,那么我根据你用户传的string,我去帮你动态计算出真实结果,问题就迎刃而解了。
else if (typeof scroll_y === "string") { /* a string, like "calc(100vh - 300px)" */ if (ctx.debug) console.warn("AntD.Table.scroll.y: ".concat(scroll_y, ", it may cause performance problems.")); ctx._raw_y = scroll_y; ctx._y = transferStringToNumberSize(scroll_y); }

transferStringToNumberSize函数如下:
function transferStringToNumberSize(param) { let regx = { px: /^[0-9.]+(vh|px|vw)?$/,//"300","300px","50vh","10vw", calc: /^calc\([0-9.]+(vh|px|vw)?( (\-|\+) [0-9.]+(vh|px|vw)?)*\)$/,//"calc({px} +/- {px})" }function getTrueHeight(value) { let parts = value.match(/([0-9.]+)(vh|vw|px)?/); let q = Number(parts[1]); let end = parts[2]; let result = 0; switch (end) { case "vh": result = window.innerHeight * (q / 100); break; case "vw": result = window.innerWidth * (q / 100); break; case "px": default: result = q; break; } return result; }let resultHeight = 0, separator = " ", matches = [], lastOpr = "+"; if (regx.px.test(param)) { resultHeight = getTrueHeight(param); } else if (regx.calc.test(param)) { matches = param.match(/^calc\((.*)\)$/); matches = matches[1].split(separator); for (let i = 0; i < matches.length; i++) { if (matches[i] == "+") { lastOpr = "+"; } else if (matches[i] == "-") { lastOpr = "-"; } else { if (lastOpr == "+") { resultHeight += getTrueHeight(matches[i]); } else if (lastOpr == "-") { resultHeight -= getTrueHeight(matches[i]); } } } } else { resultHeight = window.innerHeight; }return resultHeight; }

完整解决方案:
1.找到node_modules里的virtualizedtableforantd4
2.将vt.js整体替换成如下内容
virtualizedtableforantd4空白bug修复
文章图片

function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; }function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; }function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }function _objectWithoutProperties(source, excluded) { if (source == null) return {}; var target = _objectWithoutPropertiesLoose(source, excluded); var key, i; if (Object.getOwnPropertySymbols) { var sourceSymbolKeys = Object.getOwnPropertySymbols(source); for (i = 0; i < sourceSymbolKeys.length; i++) { key = sourceSymbolKeys[i]; if (excluded.indexOf(key) >= 0) continue; if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue; target[key] = source[key]; } } return target; }function _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; }/* The MIT License (MIT)Copyright (c) 2019 https://github.com/wubostc/Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. */ import React, { useRef, useState, useCallback, useContext, useEffect, useMemo, useImperativeHandle } from "react"; /** * THE EVENTS OF SCROLLING. */var SCROLLEVT_NULL = 0 << 0; var SCROLLEVT_INIT = 1 << 0; var SCROLLEVT_RECOMPUTE = 1 << 1; var SCROLLEVT_NATIVE = 1 << 3; var SCROLLEVT_BY_HOOK = 1 << 6; // any events will be `SCROLLEVT_BY_HOOK` if the `ctx.f_top ===TOP_CONTINUE`.var TOP_CONTINUE = 0; var TOP_DONE = 1; /** * `INIT` -> `LOADED` -> `RUNNING` */var e_VT_STATE; (function (e_VT_STATE) { e_VT_STATE[e_VT_STATE["INIT"] = 1] = "INIT"; e_VT_STATE[e_VT_STATE["LOADED"] = 2] = "LOADED"; e_VT_STATE[e_VT_STATE["RUNNING"] = 4] = "RUNNING"; })(e_VT_STATE || (e_VT_STATE = {})); function default_context() { return { vt_state: e_VT_STATE.INIT, possible_hight_per_tr: -1, computed_h: 0, re_computed: 0, row_height: [], row_count: 0, prev_row_count: 0, _offset_top: 0 | 0, _offset_head: 0 | 0, _offset_tail: 0 | 1, WH: 0, top: 0, left: 0, evt: SCROLLEVT_NULL, end: false, final_top: 0, f_final_top: TOP_DONE, update_count: 0 }; } /* overload __DIAGNOSIS__. */function helper_diagnosis(ctx) { if (ctx.hasOwnProperty("CLICK~__DIAGNOSIS__")) return; Object.defineProperty(ctx, "CLICK~__DIAGNOSIS__", { get: function get() { console.debug("OoOoOoO DIAGNOSIS OoOoOoO"); var expect_height = 0; for (var i = 0; i < ctx.row_count; ++i) { expect_height += ctx.row_height[i]; }var color, explain; if (expect_height > ctx.computed_h) { color = "color:rgb(15, 179, 9)"; // greenexplain = "lower than expected"; } else if (expect_height < ctx.computed_h) { color = "color:rgb(202, 61, 81)"; // redexplain = "higher than expected"; } else { color = "color:rgba(0, 0, 0, 0.85)"; explain = "normal"; }console.debug("%c%d(%d)(".concat(explain, ")"), color, expect_height, ctx.computed_h - expect_height); console.debug("OoOoOoOoOoOoOOoOoOoOoOoOo"); }, configurable: false, enumerable: false }); }function log_debug(ctx, msg) { if (ctx.debug) { var ts = new Date().getTime(); console.debug("%c[".concat(ctx.id, "][").concat(ts, "][").concat(msg, "] vt"), "color:#a00", ctx); } } // the factory function returns a SimEvent.function make_evt(ne) { var target = ne.target; return { target: { scrollTop: target.scrollTop, scrollLeft: target.scrollLeft }, end: target.scrollHeight - target.clientHeight === Math.round(target.scrollTop), flag: SCROLLEVT_NATIVE }; } /** * Default Implementation Layer. *//** AntD.TableComponent.table */var TableImpl = React.forwardRef(function TableImpl(props, ref) { return React.createElement("table", Object.assign({ ref: ref }, props)); }); /** AntD.TableComponent.body.wrapper */function WrapperImpl(props) { return React.createElement("tbody", Object.assign({}, props)); } /** AntD.TableComponent.body.row */var RowImpl = React.forwardRef(function RowImpl(props, ref) { return React.createElement("tr", Object.assign({ ref: ref }, props)); }); /** * O(n) * returns offset: [head, tail, top] */function scroll_with_offset(ctx, top, scroll_y) { var row_height = ctx.row_height, row_count = ctx.row_count, default_h = ctx.possible_hight_per_tr, overscanRowCount = ctx.overscanRowCount; var overscan = overscanRowCount; if (typeof scroll_y === "number") { ctx._raw_y = scroll_y; ctx._y = ctx._raw_y; } else if (typeof scroll_y === "string") { /* a string, like "calc(100vh - 300px)" */ if (ctx.debug) console.warn("AntD.Table.scroll.y: ".concat(scroll_y, ", it may cause performance problems.")); ctx._raw_y = scroll_y; ctx._y = transferStringToNumberSize(scroll_y); } else { if (ctx.debug) console.warn("AntD.Table.scroll.y: ".concat(scroll_y, ", it may cause performance problems.")); console.info("VT will not works well, did you forget to set `scroll.y`?"); ctx._raw_y = null; ctx._y = ctx.wrap_inst.current.parentElement.offsetHeight; }console.assert(ctx._y >= 0); // to calc `_top` with `row_height` and `overscan`.var _top = 0, i = 0, j = 0; // the height to render.var torender_h = 0; // scroll to the bottom of the table.if (top === -1 && row_count > 0) { i = row_count; while (i > 0 && torender_h < ctx._y) { torender_h += row_height[--i]; }return [0 | i, 0 | row_count, 0 | ctx.computed_h - torender_h]; }for (; i < row_count && _top <= top; ++i) { _top += row_height[i] || default_h; }while (i > 0 && overscan--) { _top -= row_height[--i]; }j = i; for (; j < row_count && torender_h < ctx._y; ++j) { torender_h += row_height[j] || default_h; }j += overscanRowCount * 2; if (j > row_count) j = row_count; // returns [head, tail, top].return [0 | i, 0 | j, 0 | _top]; } // set the variables for offset top/head/tail.function set_offset(ctx, top, head, tail) { ctx._offset_top = 0 | top; ctx._offset_head = 0 | head; ctx._offset_tail = 0 | tail; }function set_scroll(ctx, top, left, evt, end) { ctx.top = top; ctx.left = left; ctx.evt = evt; ctx.end = end; }function update_wrap_style(ctx, h) { if (ctx.WH === h) return; ctx.WH = h; var s = ctx.wrap_inst.current.style; s.height = h ? (s.maxHeight = h + 'px', s.maxHeight) : (s.maxHeight = 'unset', s.maxHeight); } // scrolls the parent element to specified location.function scroll_to(ctx, top, left) { if (!ctx.wrap_inst.current) return; var ele = ctx.wrap_inst.current.parentElement; /** ie */ele.scrollTop = top; ele.scrollLeft = left; }function _repainting(ctx, ms) { var fn = function fn() { log_debug(ctx, "REPAINTING"); if (ctx.vt_state === e_VT_STATE.RUNNING && ctx.wrap_inst.current) { // output to the buffer update_wrap_style(ctx, ctx.computed_h); } // free this handle manually.ctx.HND_PAINT = 0; }; return ms < 0 ? window.requestAnimationFrame(fn) : window.setTimeout(fn, ms); } // a wrapper function for `_repainting`.function repainting(ctx) { if (ctx.HND_PAINT > 0) return; ctx.HND_PAINT = _repainting(ctx, -1); }function srs_expand(ctx, len, prev_len, fill_value) { var slen = len - prev_len; var shadow_rows = new Array(slen).fill(fill_value); ctx.row_height = ctx.row_height.concat(shadow_rows); ctx.computed_h += slen * fill_value; }function srs_shrink(ctx, len, prev_len) { if (len === 0) { ctx.computed_h = 0; return; }var rows = ctx.row_height; var h2shrink = 0; for (var i = len; i < prev_len; ++i) { h2shrink += rows[i]; }ctx.computed_h -= h2shrink; }function set_tr_cnt(ctx, n) { ctx.re_computed = n - ctx.row_count; ctx.prev_row_count = ctx.row_count; ctx.row_count = n; }function VTable(props, ref) { var style = props.style, context = props.context, rest = _objectWithoutProperties(props, ["style", "context"]); // force update this vt.var force = useState(0); /*********** DOM ************/var wrap_inst = useMemo(function () { return React.createRef(); }, []); /*********** context ************/var ctx = useContext(context); useMemo(function () { Object.assign(ctx, default_context()); if (ctx.wrap_inst && ctx.wrap_inst.current) { ctx.wrap_inst.current.parentElement.onscroll = null; }ctx.wrap_inst = wrap_inst; ctx.top = ctx.initTop; helper_diagnosis(ctx); }, []); /*********** scroll event ************/var event_queue = useRef([]).current; var HND_RAF = useRef(0); // handle of requestAnimationFrame/* eslint-disable prefer-const */var RAF_update_self; /*********** scroll hook ************/var scroll_hook = useCallback(function (e) { if (ctx.vt_state !== e_VT_STATE.RUNNING) return; if (e) { event_queue.push(e); if (ctx.f_final_top === TOP_CONTINUE) { e.flag = SCROLLEVT_BY_HOOK; return RAF_update_self(0); } }if (event_queue.length) { if (HND_RAF.current) cancelAnimationFrame(HND_RAF.current); // requestAnimationFrame, ie >= 10HND_RAF.current = requestAnimationFrame(RAF_update_self); } }, []); var scroll_hook_native = useCallback(function (e) { scroll_hook(make_evt(e)); }, []); /* requestAnimationFrame callback */RAF_update_self = useCallback(function (timestamp) { if (ctx.vt_state !== e_VT_STATE.RUNNING) return; var evq = event_queue; var e; // consume the `evq` first.if (evq.length) { e = evq.shift(); } else { return; }var etop = e.target.scrollTop; var eleft = e.target.scrollLeft; var flag = e.flag; if (ctx.debug) { console.debug("[".concat(ctx.id, "][SCROLL] top: %d, left: %d"), etop, eleft); } // checks every tr's height, which will take some time...var offset = scroll_with_offset(ctx, ctx.f_final_top === TOP_CONTINUE ? ctx.final_top : etop, ctx.scroll.y); var head = offset[0]; var tail = offset[1]; var top = offset[2]; var prev_head = ctx._offset_head; var prev_tail = ctx._offset_tail; var prev_top = ctx._offset_top; var end; switch (flag) { case SCROLLEVT_INIT: log_debug(ctx, "SCROLLEVT_INIT"); end = false; break; case SCROLLEVT_BY_HOOK: log_debug(ctx, "SCROLLEVT_BY_HOOK"); if (head === prev_head && tail === prev_tail && top === prev_top) { ctx.f_final_top = TOP_DONE; if (ctx.final_top === -1) etop = ctx.computed_h - ctx._y; end = true; } else { if (ctx.final_top === -1) etop = top; end = false; }break; case SCROLLEVT_RECOMPUTE: log_debug(ctx, "SCROLLEVT_RECOMPUTE"); if (head === prev_head && tail === prev_tail && top === prev_top) { HND_RAF.current = 0; if (event_queue.length) scroll_hook(null); // consume the next.return; }end = false; break; case SCROLLEVT_NATIVE: log_debug(ctx, "SCROLLEVT_NATIVE"); HND_RAF.current = 0; if (ctx.onScroll) { ctx.onScroll({ top: etop, left: eleft, isEnd: e.end }); }if (head === prev_head && tail === prev_tail && top === prev_top) { return; }end = e.end; break; }set_offset(ctx, top, head, tail); set_scroll(ctx, etop, eleft, flag, end); force[1](++ctx.update_count); }, []); // expose to the parent components you are using.useImperativeHandle(ref, function () { return { // `y === -1` indicates you need to scroll to the bottom of the table. scrollTo: function scrollTo(y) { ctx.f_final_top = TOP_CONTINUE; ctx.final_top = y; scroll_hook({ target: { scrollTop: y, scrollLeft: -1 }, flag: SCROLLEVT_BY_HOOK }); } }; }, []); useEffect(function () { ctx.wrap_inst.current.parentElement.onscroll = scroll_hook_native; }, [wrap_inst]); // update DOM style.useEffect(function () { switch (ctx.evt) { case SCROLLEVT_BY_HOOK: scroll_to(ctx, ctx.top, ctx.left); break; case SCROLLEVT_INIT: case SCROLLEVT_RECOMPUTE: scroll_to(ctx, ctx.top, ctx.left); if (event_queue.length) RAF_update_self(0); // consume the next.break; } }, [force[0] /* for performance. */ ]); useEffect(function () { switch (ctx.vt_state) { case e_VT_STATE.INIT: // init vt without the rows. break; case e_VT_STATE.LOADED: // changed by VTRow only. ctx.vt_state = e_VT_STATE.RUNNING; // force update.scroll_hook({ target: { scrollTop: ctx.top, scrollLeft: 0 }, flag: SCROLLEVT_INIT }); break; case e_VT_STATE.RUNNING: if (ctx.re_computed !== 0) { // rerender ctx.re_computed = 0; scroll_hook({ target: { scrollTop: ctx.top, scrollLeft: ctx.left }, flag: SCROLLEVT_RECOMPUTE }); }break; } }); style.position = "relative"; style.top = ctx._offset_top; var width = style.width, rest_style = _objectWithoutProperties(style, ["width"]); var wrap_style = useMemo(function () { return { width: width, position: "relative", transform: "matrix(1, 0, 0, 1, 0, 0)" }; }, [width]); var Table = ctx.components.table; return React.createElement("div", { ref: wrap_inst, style: wrap_style }, React.createElement(context.Provider, { value: _objectSpread({}, ctx) }, React.createElement(Table, Object.assign({}, rest, { style: rest_style })))); }function VWrapper(props) { var c = props.children, ctx = props.ctx, restProps = _objectWithoutProperties(props, ["children", "ctx"]); var measureRow = c[0]; var rows = c[1]; var Wrapper = ctx.components.body.wrapper; // reference https://github.com/react-component/table/blob/master/src/Body/index.tsx#L6var len = Array.isArray(rows) ? rows.length : 0; var head = ctx._offset_head, tail = ctx._offset_tail; var trs; switch (ctx.vt_state) { case e_VT_STATE.INIT: if (len >= 0) { console.assert(head === 0); console.assert(tail === 1); trs = Array.isArray(rows) ? rows.slice(head, tail) : rows; ctx.re_computed = len; ctx.prev_row_count = len; ctx.row_count = len; }break; case e_VT_STATE.RUNNING: { if (tail > len) { var offset = tail - len; tail -= offset; head -= offset; if (head < 0) head = 0; if (tail < 0) tail = 0; // update the `head` and `tail`.set_offset(ctx, ctx._offset_top /* NOTE: invalided param, just to fill for this param */ , head, tail); }if (ctx.row_count !== len) { set_tr_cnt(ctx, len); }len = ctx.row_count; var prev_len = ctx.prev_row_count; /* shadow-rows rendering phase. */if (len < prev_len) { srs_shrink(ctx, len, prev_len); } else if (len > prev_len) { var row_h = ctx.row_height; if (len - row_h.length > 0) { srs_expand(ctx, len, row_h.length, ctx.possible_hight_per_tr); } else { // calculate the total height quickly. row_h.fill(ctx.possible_hight_per_tr, prev_len, len); ctx.computed_h += ctx.possible_hight_per_tr * (len - prev_len); } }trs = len ? rows.slice(head, tail) : rows; ctx.prev_row_count = ctx.row_count; } break; case e_VT_STATE.LOADED: console.assert(false); break; }return React.createElement(Wrapper, Object.assign({}, restProps), measureRow, trs); }function VTRow(props) { var inst = React.createRef(); var context = props.context, rest = _objectWithoutProperties(props, ["context"]); var ctx = context; var children = props.children; var Row = ctx.components.body.row; if (!Array.isArray(children)) { // reference https://github.com/react-component/table/blob/master/src/Body/ExpandedRow.tsx#L55 return React.createElement(Row, Object.assign({}, rest), children); }var index = children[0].props.index; var last_index = useRef(children[0].props.index); useEffect(function () { if (ctx.vt_state === e_VT_STATE.RUNNING) { // apply_h(ctx, index, inst.current.offsetHeight, "dom"); repainting(ctx); } else { console.assert(ctx.vt_state === e_VT_STATE.INIT); ctx.vt_state = e_VT_STATE.LOADED; ctx.possible_hight_per_tr = inst.current.offsetHeight; srs_expand(ctx, ctx.row_count, 0, ctx.possible_hight_per_tr); // create a timeout task._repainting(ctx, 16); }return function () { return repainting(ctx); }; }, []); useEffect(function () { var h = inst.current.offsetHeight; var curr_h = ctx.row_height[index]; var last_h = ctx.row_height[last_index.current]; ctx.computed_h -= curr_h; ctx.computed_h += last_h; ctx.computed_h += h - last_h; ctx.row_height[index] = h; repainting(ctx); }); return React.createElement(Row, Object.assign({}, rest, { ref: inst })); }export function _set_components(ctx, components) { var table = components.table, body = components.body, header = components.header; ctx.components.body = _objectSpread(_objectSpread({}, ctx.components.body), body); if (body && body.cell) { ctx._vtcomponents.body.cell = body.cell; }if (header) { ctx.components.header = header; ctx._vtcomponents.header = header; }if (table) { ctx.components.table = table; } } export function init(fnOpts, deps) { var ctx = useRef(React.createContext({})).current; var ctx_value = https://www.it610.com/article/useContext(ctx); var default_ref = useRef(); useMemo(function () { return Object.assign(ctx_value, { id: +new Date(), initTop: 0, overscanRowCount: 5, debug: false, ref: default_ref }, fnOpts()); }, deps); useMemo(function () { var VTable2 = React.forwardRef(VTable); // set the virtual layer.ctx_value._vtcomponents = { table: function table(props) { return React.createElement(VTable2, Object.assign({}, props, { context: ctx, ref: ctx_value.ref })); }, body: { wrapper: function wrapper(props) { return React.createElement(ctx.Consumer, null, function () /* value */ { return React.createElement(VWrapper, Object.assign({}, props, { ctx: ctx_value })); }); }, row: function row(props) { return React.createElement(VTRow, Object.assign({}, props, { context: ctx_value })); } } }; // set the default implementation layer.ctx_value.components = {}; _set_components(ctx_value, { table: TableImpl, body: { wrapper: WrapperImpl, row: RowImpl } }); // start -> `INIT`ctx_value.vt_state = e_VT_STATE.INIT; }, []); return ctx_value; }function transferStringToNumberSize(param) { let regx = { px: /^[0-9.]+(vh|px|vw)?$/,//"300","300px","50vh","10vw", calc: /^calc\([0-9.]+(vh|px|vw)?( (\-|\+) [0-9.]+(vh|px|vw)?)*\)$/,//"calc({px} +/- {px})" }function getTrueHeight(value) { let parts = value.match(/([0-9.]+)(vh|vw|px)?/); let q = Number(parts[1]); let end = parts[2]; let result = 0; switch (end) { case "vh": result = window.innerHeight * (q / 100); break; case "vw": result = window.innerWidth * (q / 100); break; case "px": default: result = q; break; } return result; }let resultHeight = 0, separator = " ", matches = [], lastOpr = "+"; if (regx.px.test(param)) { resultHeight = getTrueHeight(param); } else if (regx.calc.test(param)) { matches = param.match(/^calc\((.*)\)$/); matches = matches[1].split(separator); for (let i = 0; i < matches.length; i++) { if (matches[i] == "+") { lastOpr = "+"; } else if (matches[i] == "-") { lastOpr = "-"; } else { if (lastOpr == "+") { resultHeight += getTrueHeight(matches[i]); } else if (lastOpr == "-") { resultHeight -= getTrueHeight(matches[i]); } } } } else { resultHeight = window.innerHeight; }return resultHeight; }

作者:点墨
版权:本文版权归作者所有
转载:欢迎转载,但未经作者同意,必须保留此段声明;必须在文章中给出原文连接;否则必究法律责任

    推荐阅读