stk-table-vue 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -23,6 +23,7 @@ Support Vue3 and Vue2.7
23
23
 
24
24
  ## Documentation
25
25
  ### [Stk Table Vue Official](https://ja-plus.github.io/stk-table-vue/)
26
+ ### [Stk Table React Official](https://ja-plus.github.io/stk-table-react/)
26
27
 
27
28
 
28
29
  ## Repo:
@@ -6,7 +6,7 @@
6
6
  * homepage: https://ja-plus.github.io/stk-table-vue/
7
7
  * license: MIT
8
8
  */
9
- import { t as StkTable_default } from "./StkTable-QUd4TJCF.js";
9
+ import { t as StkTable_default } from "./StkTable-BK4VNsvG.js";
10
10
  import { createElementBlock, createElementVNode, createVNode, defineComponent, h, nextTick, normalizeClass, normalizeStyle, onMounted, onUnmounted, openBlock, reactive, ref, withModifiers } from "vue";
11
11
  //#region src/StkTable/custom-cells/FilterCell/Dropdown/index.vue?vue&type=script&setup=true&lang.ts
12
12
  var DROPDOWN_DEFAULT_WIDTH = 300;
@@ -347,12 +347,12 @@ function useAreaSelection(props, emits, tableContainerRef, dataSourceCopy, table
347
347
  const KEY_ESCAPE = "Escape";
348
348
  const KEY_ESC = "Esc";
349
349
  const KEY_C = "c";
350
- const CELL_RANGE_SELECTED = "cell-range-selected";
351
- const CELL_RANGE_TOP = "cell-range-t";
352
- const CELL_RANGE_BOTTOM = "cell-range-b";
353
- const CELL_RANGE_LEFT = "cell-range-l";
354
- const CELL_RANGE_RIGHT = "cell-range-r";
355
- const ROW_RANGE_SELECTED = "row-range-selected";
350
+ const ATTR_CELL_SELECTED = "data-cs-s";
351
+ const ATTR_CELL_TOP = "data-cs-t";
352
+ const ATTR_CELL_BOTTOM = "data-cs-b";
353
+ const ATTR_CELL_LEFT = "data-cs-l";
354
+ const ATTR_CELL_RIGHT = "data-cs-r";
355
+ const ATTR_ROW_SELECTED = "data-rs-s";
356
356
  const selectionRanges = ref([]);
357
357
  const isSelecting = ref(false);
358
358
  /** start cell */
@@ -478,10 +478,17 @@ function useAreaSelection(props, emits, tableContainerRef, dataSourceCopy, table
478
478
  if (!container) return;
479
479
  const cellHighlight = highlightCellEnabled.value;
480
480
  const rowHighlight = highlightRowEnabled.value;
481
- const oldSelectedCells = container.querySelectorAll(`.${CELL_RANGE_SELECTED}`);
482
- for (let i = 0; i < oldSelectedCells.length; i++) oldSelectedCells[i].classList.remove(CELL_RANGE_SELECTED, CELL_RANGE_TOP, CELL_RANGE_BOTTOM, CELL_RANGE_LEFT, CELL_RANGE_RIGHT);
483
- const oldSelectedRows = container.querySelectorAll(`.${ROW_RANGE_SELECTED}`);
484
- for (let i = 0; i < oldSelectedRows.length; i++) oldSelectedRows[i].classList.remove(ROW_RANGE_SELECTED);
481
+ const oldSelectedCells = container.querySelectorAll(`[${ATTR_CELL_SELECTED}]`);
482
+ for (let i = 0; i < oldSelectedCells.length; i++) {
483
+ const el = oldSelectedCells[i];
484
+ el.removeAttribute(ATTR_CELL_SELECTED);
485
+ el.removeAttribute(ATTR_CELL_TOP);
486
+ el.removeAttribute(ATTR_CELL_BOTTOM);
487
+ el.removeAttribute(ATTR_CELL_LEFT);
488
+ el.removeAttribute(ATTR_CELL_RIGHT);
489
+ }
490
+ const oldSelectedRows = container.querySelectorAll(`[${ATTR_ROW_SELECTED}]`);
491
+ for (let i = 0; i < oldSelectedRows.length; i++) oldSelectedRows[i].removeAttribute(ATTR_ROW_SELECTED);
485
492
  recomputeSelectedCellKeys();
486
493
  const ranges = selectionRanges.value;
487
494
  if (!ranges.length) return;
@@ -491,7 +498,7 @@ function useAreaSelection(props, emits, tableContainerRef, dataSourceCopy, table
491
498
  const { minRow, maxRow } = normalizeRange(range);
492
499
  for (let r = minRow; r <= maxRow; r++) {
493
500
  const tr = tbody.querySelector(`tr[data-row-i="${r}"]`);
494
- if (tr) tr.classList.add(ROW_RANGE_SELECTED);
501
+ if (tr) tr.setAttribute(ATTR_ROW_SELECTED, "");
495
502
  }
496
503
  }
497
504
  if (cellHighlight) {
@@ -521,12 +528,14 @@ function useAreaSelection(props, emits, tableContainerRef, dataSourceCopy, table
521
528
  if (!row || !cols[colIndex]) continue;
522
529
  const ck = cellKeyGen(row, cols[colIndex]);
523
530
  if (!selectedCellKeys.has(ck)) continue;
524
- td.classList.add(CELL_RANGE_SELECTED);
531
+ td.setAttribute(ATTR_CELL_SELECTED, "");
525
532
  if (rowIndex >= lrMinRow && rowIndex <= lrMaxRow && colIndex >= lrMinCol && colIndex <= lrMaxCol) {
526
- if (rowIndex === lrMinRow) td.classList.add(CELL_RANGE_TOP);
527
- if (rowIndex === lrMaxRow) td.classList.add(CELL_RANGE_BOTTOM);
528
- if (colIndex === lrMinCol) td.classList.add(CELL_RANGE_LEFT);
529
- if (colIndex === lrMaxCol) td.classList.add(CELL_RANGE_RIGHT);
533
+ const effEndRow = rowIndex + (parseInt(td.getAttribute("rowspan") || "1", 10) || 1) - 1;
534
+ const effEndCol = colIndex + (parseInt(td.getAttribute("colspan") || "1", 10) || 1) - 1;
535
+ if (rowIndex === lrMinRow) td.setAttribute(ATTR_CELL_TOP, "");
536
+ if (effEndRow === lrMaxRow) td.setAttribute(ATTR_CELL_BOTTOM, "");
537
+ if (colIndex === lrMinCol) td.setAttribute(ATTR_CELL_LEFT, "");
538
+ if (effEndCol === lrMaxCol) td.setAttribute(ATTR_CELL_RIGHT, "");
530
539
  }
531
540
  }
532
541
  }
@@ -642,6 +651,92 @@ function useAreaSelection(props, emits, tableContainerRef, dataSourceCopy, table
642
651
  if (!colKey) return -1;
643
652
  return colKeyToIndexMap.value.get(colKey) ?? -1;
644
653
  }
654
+ /** 获取指定单元格的合并信息 [rowspan, colspan],无合并返回 [1, 1] */
655
+ function getMergeSpan(rowIndex, colIndex) {
656
+ const data = dataSourceCopy.value;
657
+ const cols = tableHeaderLast.value;
658
+ const row = data[rowIndex];
659
+ const col = cols[colIndex];
660
+ if (!row || !col || !col.mergeCells) return [1, 1];
661
+ const { rowspan = 1, colspan = 1 } = col.mergeCells({
662
+ row,
663
+ col,
664
+ rowIndex,
665
+ colIndex
666
+ }) || {};
667
+ return [rowspan || 1, colspan || 1];
668
+ }
669
+ /**
670
+ * 扩展选区范围,使其完整覆盖边界上部分选中的合并单元格
671
+ * en: Expand selection range to fully cover merged cells that are partially selected at boundaries
672
+ */
673
+ function expandRangeToCoverMergedCells(range) {
674
+ const { minRow, maxRow, minCol, maxCol } = normalizeRange(range);
675
+ const data = dataSourceCopy.value;
676
+ const cols = tableHeaderLast.value;
677
+ const rowCount = data.length;
678
+ const colCount = cols.length;
679
+ const mergeColIndices = [];
680
+ for (let c = 0; c < colCount; c++) {
681
+ var _cols$c;
682
+ if ((_cols$c = cols[c]) === null || _cols$c === void 0 ? void 0 : _cols$c.mergeCells) mergeColIndices.push(c);
683
+ }
684
+ if (!mergeColIndices.length) return range;
685
+ let [eMinRow, eMaxRow, eMinCol, eMaxCol] = [
686
+ minRow,
687
+ maxRow,
688
+ minCol,
689
+ maxCol
690
+ ];
691
+ let changed = true;
692
+ let guard = 0;
693
+ while (changed && guard++ < 100) {
694
+ changed = false;
695
+ for (const c of mergeColIndices) {
696
+ if (c < eMinCol || c > eMaxCol) continue;
697
+ const [rs] = getMergeSpan(eMaxRow, c);
698
+ if (rs > 1 && eMaxRow + rs - 1 < rowCount && eMaxRow + rs - 1 > eMaxRow) {
699
+ eMaxRow = eMaxRow + rs - 1;
700
+ changed = true;
701
+ }
702
+ }
703
+ for (let r = eMinRow; r <= eMaxRow; r++) {
704
+ const [, cs] = getMergeSpan(r, eMaxCol);
705
+ if (cs > 1 && eMaxCol + cs - 1 < colCount && eMaxCol + cs - 1 > eMaxCol) {
706
+ eMaxCol = eMaxCol + cs - 1;
707
+ changed = true;
708
+ }
709
+ }
710
+ for (const c of mergeColIndices) {
711
+ if (c < eMinCol || c > eMaxCol) continue;
712
+ for (let r = eMinRow - 1; r >= 0 && r > eMinRow - 500; r--) {
713
+ const [rs] = getMergeSpan(r, c);
714
+ if (rs <= 1) continue;
715
+ if (r + rs - 1 >= eMinRow) {
716
+ if (r < eMinRow) {
717
+ eMinRow = r;
718
+ changed = true;
719
+ }
720
+ } else break;
721
+ }
722
+ }
723
+ for (let r = eMinRow; r <= eMaxRow; r++) for (let c = eMinCol - 1; c >= 0 && c > eMinCol - 500; c--) {
724
+ const [, cs] = getMergeSpan(r, c);
725
+ if (cs <= 1) continue;
726
+ if (c + cs - 1 >= eMinCol) {
727
+ if (c < eMinCol) {
728
+ eMinCol = c;
729
+ changed = true;
730
+ }
731
+ } else break;
732
+ }
733
+ }
734
+ if (eMinRow === minRow && eMaxRow === maxRow && eMinCol === minCol && eMaxCol === maxCol) return range;
735
+ const { begin, end } = range.index;
736
+ const newBeginRow = begin.row < end.row || begin.row === end.row ? eMinRow : eMaxRow;
737
+ const newEndRow = begin.row < end.row || begin.row === end.row ? eMaxRow : eMinRow;
738
+ return makeRange(newBeginRow, begin.col <= end.col ? eMinCol : eMaxCol, newEndRow, begin.col <= end.col ? eMaxCol : eMinCol);
739
+ }
645
740
  /** 获取列的左边距和宽度
646
741
  * @param colIndex 列的绝对索引
647
742
  * @returns [left, width]
@@ -720,10 +815,10 @@ function useAreaSelection(props, emits, tableContainerRef, dataSourceCopy, table
720
815
  const colIndex = getColIndexByKey(getClosestColKey(e.target));
721
816
  if (rowIndex < 0 || colIndex < 0) return;
722
817
  const ctrlKey = e.ctrlKey || e.metaKey;
723
- const range = makeRange(rowIndex, colIndex, rowIndex, colIndex);
818
+ const range = expandRangeToCoverMergedCells(makeRange(rowIndex, colIndex, rowIndex, colIndex));
724
819
  if (e.shiftKey && anchorCell && shiftEnabled.value) {
725
820
  const ranges = selectionRanges.value.slice();
726
- const shiftRange = makeRange(anchorCell.rowIndex, anchorCell.colIndex, rowIndex, colIndex);
821
+ const shiftRange = expandRangeToCoverMergedCells(makeRange(anchorCell.rowIndex, anchorCell.colIndex, rowIndex, colIndex));
727
822
  if (ranges.length) ranges[ranges.length - 1] = shiftRange;
728
823
  else ranges.push(shiftRange);
729
824
  selectionRanges.value = ranges;
@@ -762,7 +857,7 @@ function useAreaSelection(props, emits, tableContainerRef, dataSourceCopy, table
762
857
  /** 更新最后一个选区的终点(拖拽过程中) */
763
858
  function updateSelectionEnd(endRowIndex, endColIndex) {
764
859
  if (!anchorCell) return;
765
- const newRange = makeRange(anchorCell.rowIndex, anchorCell.colIndex, endRowIndex, endColIndex);
860
+ const newRange = expandRangeToCoverMergedCells(makeRange(anchorCell.rowIndex, anchorCell.colIndex, endRowIndex, endColIndex));
766
861
  const ranges = [...selectionRanges.value];
767
862
  if (ranges.length > 0) ranges[ranges.length - 1] = newRange;
768
863
  else ranges.push(newRange);
@@ -825,6 +920,15 @@ function useAreaSelection(props, emits, tableContainerRef, dataSourceCopy, table
825
920
  stopAutoScroll();
826
921
  document.removeEventListener("mousemove", onDocumentMouseMove);
827
922
  document.removeEventListener("mouseup", onDocumentMouseUp);
923
+ const ranges = selectionRanges.value;
924
+ if (ranges.length) {
925
+ const expanded = expandRangeToCoverMergedCells(ranges[ranges.length - 1]);
926
+ if (expanded !== ranges[ranges.length - 1]) {
927
+ const newRanges = [...ranges];
928
+ newRanges[newRanges.length - 1] = expanded;
929
+ selectionRanges.value = newRanges;
930
+ }
931
+ }
828
932
  emitSelectionChange();
829
933
  }
830
934
  function emitSelectionChange() {
@@ -0,0 +1,55 @@
1
+ import { FormatNumberOptions } from '../utils/formatNumber';
2
+
3
+ /** createChangeCell 配置选项 */
4
+ export interface CreateChangeCellOptions extends FormatNumberOptions {
5
+ /** 涨跌颜色是否反转:false=A股(涨红跌绿,默认),true=国际(涨绿跌红) */
6
+ colorReverse?: boolean;
7
+ /** 是否显示涨跌箭头 ▲/▼,默认 false */
8
+ arrow?: boolean;
9
+ /** 自定义上涨颜色(覆盖默认配色,传入后不受 colorReverse 影响) */
10
+ riseColor?: string;
11
+ /** 自定义下跌颜色 */
12
+ fallColor?: string;
13
+ /** 自定义平盘颜色 */
14
+ flatColor?: string;
15
+ }
16
+ /**
17
+ * 涨跌单元格工厂函数
18
+ *
19
+ * 在 {@link formatNumber} 的基础上叠加涨跌染色与箭头。染色按 `cellValue` 正负判断,
20
+ * 颜色方向可通过 `colorReverse` 切换(A股 / 国际),也可用 `riseColor` 等自定义。
21
+ * 数字对齐请在列配置上设置 `align: 'right'`。
22
+ *
23
+ * @param options 格式化 + 染色/箭头选项
24
+ * @returns `ChangeCell` 组件构造函数,使用时需调用一次:`customCell: ChangeCell()`
25
+ *
26
+ * @example
27
+ * ```ts
28
+ * const { ChangeCell } = createChangeCell({ decimals: 2, showSign: true, arrow: true });
29
+ * const ChangeCellComp = ChangeCell();
30
+ * const columns = [
31
+ * { title: '涨跌额', dataIndex: 'change', align: 'right', customCell: ChangeCellComp },
32
+ * ];
33
+ * ```
34
+ */
35
+ export declare function createChangeCell(options?: CreateChangeCellOptions): {
36
+ ChangeCell: () => import('vue').Raw<import('vue').DefineComponent<{
37
+ row: any;
38
+ col: import('../..').StkTableColumn<any>;
39
+ cellValue: any;
40
+ rowIndex: number;
41
+ colIndex: number;
42
+ expanded?: import('../..').StkTableColumn<any> | undefined;
43
+ treeExpanded?: boolean | undefined;
44
+ }, () => import('vue').VNode<import('vue').RendererNode, import('vue').RendererElement, {
45
+ [key: string]: any;
46
+ }>, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<{
47
+ row: any;
48
+ col: import('../..').StkTableColumn<any>;
49
+ cellValue: any;
50
+ rowIndex: number;
51
+ colIndex: number;
52
+ expanded?: import('../..').StkTableColumn<any> | undefined;
53
+ treeExpanded?: boolean | undefined;
54
+ }> & Readonly<{}>, {}, {}, {}, {}, string, import('vue').ComponentProvideOptions, true, {}, any>>;
55
+ };
@@ -0,0 +1,2 @@
1
+ export { createChangeCell } from './createChangeCell';
2
+ export type { CreateChangeCellOptions } from './createChangeCell';
@@ -0,0 +1,44 @@
1
+ import { FormatNumberOptions } from '../utils/formatNumber';
2
+
3
+ /** createNumberCell 配置选项(等同于格式化选项) */
4
+ export interface CreateNumberCellOptions extends FormatNumberOptions {
5
+ }
6
+ /**
7
+ * 数字格式化单元格工厂函数
8
+ *
9
+ * 纯展示型单元格:将 `cellValue` 经 {@link formatNumber} 格式化为文本。
10
+ * 数字对齐请在列配置上设置 `align: 'right'`。
11
+ *
12
+ * @param options 格式化选项
13
+ * @returns `NumberCell` 组件构造函数,使用时需调用一次:`customCell: NumberCell()`
14
+ *
15
+ * @example
16
+ * ```ts
17
+ * const { NumberCell } = createNumberCell({ decimals: 2 });
18
+ * const NumberCellComp = NumberCell();
19
+ * const columns = [
20
+ * { title: '现价', dataIndex: 'price', align: 'right', customCell: NumberCellComp },
21
+ * ];
22
+ * ```
23
+ */
24
+ export declare function createNumberCell(options?: CreateNumberCellOptions): {
25
+ NumberCell: () => import('vue').Raw<import('vue').DefineComponent<{
26
+ row: any;
27
+ col: import('../..').StkTableColumn<any>;
28
+ cellValue: any;
29
+ rowIndex: number;
30
+ colIndex: number;
31
+ expanded?: import('../..').StkTableColumn<any> | undefined;
32
+ treeExpanded?: boolean | undefined;
33
+ }, () => import('vue').VNode<import('vue').RendererNode, import('vue').RendererElement, {
34
+ [key: string]: any;
35
+ }>, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<{
36
+ row: any;
37
+ col: import('../..').StkTableColumn<any>;
38
+ cellValue: any;
39
+ rowIndex: number;
40
+ colIndex: number;
41
+ expanded?: import('../..').StkTableColumn<any> | undefined;
42
+ treeExpanded?: boolean | undefined;
43
+ }> & Readonly<{}>, {}, {}, {}, {}, string, import('vue').ComponentProvideOptions, true, {}, any>>;
44
+ };
@@ -0,0 +1,2 @@
1
+ export { createNumberCell } from './createNumberCell';
2
+ export type { CreateNumberCellOptions } from './createNumberCell';
@@ -0,0 +1,35 @@
1
+ /**
2
+ * 数字格式化选项
3
+ *
4
+ * NumberCell / ChangeCell 共享的格式化配置。
5
+ */
6
+ export interface FormatNumberOptions {
7
+ /** 小数位数(四舍五入)。不传则不强制处理小数位 */
8
+ decimals?: number;
9
+ /** 是否使用千分位分隔符,默认 true */
10
+ thousands?: boolean;
11
+ /** 前缀,如 '¥'、'$' */
12
+ prefix?: string;
13
+ /** 后缀,如 '元'、'股' */
14
+ suffix?: string;
15
+ /** 正数是否强制显示 '+' 号,默认 false(负数始终带 '-') */
16
+ showSign?: boolean;
17
+ /** 百分比模式:值 ×100 并追加 '%',默认 false。与 abbr 互斥 */
18
+ percent?: boolean;
19
+ /** 单位缩放:'cn' = 万/亿,'en' = K/M/B。不传则不缩放。与 percent 互斥 */
20
+ abbr?: 'cn' | 'en';
21
+ /** 缩放后保留的小数位,默认 2 */
22
+ abbrDecimals?: number;
23
+ /** 空值(null/undefined/'')或非数字时的占位符,默认 '--' */
24
+ placeholder?: string;
25
+ }
26
+ /**
27
+ * 数字格式化纯函数(NumberCell / ChangeCell 共享内核)。
28
+ *
29
+ * 处理管线:空值判断 → 取符号 → 百分比 → 单位缩放 → 小数位 → 千分位 → 拼装。
30
+ *
31
+ * @param value 原始值(数字或可转为数字的字符串)
32
+ * @param options 格式化选项
33
+ * @returns 格式化后的字符串;空值或非数字返回 placeholder
34
+ */
35
+ export declare function formatNumber(value: unknown, options?: FormatNumberOptions): string;
@@ -1,7 +1,7 @@
1
1
  export { useAreaSelection } from './features/index';
2
2
  export { registerFeature } from './registerFeature';
3
3
  export { default as StkTable } from './StkTable.vue';
4
- export type { Order, SortConfig, SortOption, SortState, StkTableColumn } from './types/index';
4
+ export type { AreaSelectionRange, Order, SortConfig, SortOption, SortState, StkTableColumn } from './types/index';
5
5
  export { binarySearch, insertToOrderedArray, strCompare, tableSort } from './utils';
6
6
  export { createFilterCell } from './custom-cells/FilterCell/index';
7
7
  export type { CreateFilterCellOption, FilterStatus } from './custom-cells/FilterCell/index';
@@ -9,3 +9,9 @@ export { createEditableCell } from './custom-cells/EditableCell/index';
9
9
  export type { CreateEditableCellOptions } from './custom-cells/EditableCell/index';
10
10
  export { createCheckboxCell } from './custom-cells/CheckboxCell/index';
11
11
  export type { createCheckboxCellOptions } from './custom-cells/CheckboxCell/index';
12
+ export { createNumberCell } from './custom-cells/NumberCell/index';
13
+ export type { CreateNumberCellOptions } from './custom-cells/NumberCell/index';
14
+ export { createChangeCell } from './custom-cells/ChangeCell/index';
15
+ export type { CreateChangeCellOptions } from './custom-cells/ChangeCell/index';
16
+ export { formatNumber } from './custom-cells/utils/formatNumber';
17
+ export type { FormatNumberOptions } from './custom-cells/utils/formatNumber';
@@ -6,7 +6,7 @@
6
6
  * homepage: https://ja-plus.github.io/stk-table-vue/
7
7
  * license: MIT
8
8
  */
9
- import { a as insertToOrderedArray, i as binarySearch, n as registerFeature, o as strCompare, r as useAreaSelection, s as tableSort, t as StkTable_default } from "./StkTable-QUd4TJCF.js";
9
+ import { a as insertToOrderedArray, i as binarySearch, n as registerFeature, o as strCompare, r as useAreaSelection, s as tableSort, t as StkTable_default } from "./StkTable-BK4VNsvG.js";
10
10
  import { Fragment, computed, createApp, createBlock, createElementBlock, createElementVNode, createTextVNode, defineComponent, getCurrentInstance, h, markRaw, nextTick, normalizeClass, openBlock, ref, renderSlot, resolveDynamicComponent, toDisplayString, watch, withModifiers } from "vue";
11
11
  //#region src/StkTable/custom-cells/FilterCell/Dropdown/index.ts
12
12
  var DropdownIns = null;
@@ -15,7 +15,7 @@ async function getDropdownIns() {
15
15
  const div = document.createElement("div");
16
16
  div.classList.add("stk-filter-dropdown-wrapper");
17
17
  document.body.appendChild(div);
18
- DropdownIns = createApp(await import("./Dropdown-Clc70S1z.js").then((module) => module.default)).mount(div);
18
+ DropdownIns = createApp(await import("./Dropdown-wEI6qpCx.js").then((module) => module.default)).mount(div);
19
19
  }
20
20
  return DropdownIns;
21
21
  }
@@ -478,4 +478,168 @@ function createCheckboxCell(options) {
478
478
  };
479
479
  }
480
480
  //#endregion
481
- export { StkTable_default as StkTable, binarySearch, createCheckboxCell, createEditableCell, createFilterCell, insertToOrderedArray, registerFeature, strCompare, tableSort, useAreaSelection };
481
+ //#region src/StkTable/custom-cells/utils/formatNumber.ts
482
+ /** 缩放单位阈值表(从大到小匹配) */
483
+ var ABBR_UNITS = {
484
+ cn: [[1e8, "亿"], [1e4, "万"]],
485
+ en: [
486
+ [1e9, "B"],
487
+ [1e6, "M"],
488
+ [1e3, "K"]
489
+ ]
490
+ };
491
+ /**
492
+ * 给数字字符串的整数部分添加千分位分隔符。
493
+ * @param numStr 不含符号的正数字符串(可含小数部分)
494
+ *
495
+ * 用手动分组代替先读正则 `\B(?=(\d{3})+(?!\d))`:
496
+ * 后者在长数字上开销随位数非线性增长,且 split('.') 会额外分配数组。
497
+ */
498
+ function addThousandsSeparator(numStr) {
499
+ const dotIndex = numStr.indexOf(".");
500
+ const intPart = dotIndex === -1 ? numStr : numStr.slice(0, dotIndex);
501
+ const len = intPart.length;
502
+ if (len <= 3) return numStr;
503
+ const firstGroupLen = len % 3 || 3;
504
+ let withSep = intPart.slice(0, firstGroupLen);
505
+ for (let i = firstGroupLen; i < len; i += 3) withSep += "," + intPart.slice(i, i + 3);
506
+ return dotIndex === -1 ? withSep : withSep + numStr.slice(dotIndex);
507
+ }
508
+ /**
509
+ * 数字格式化纯函数(NumberCell / ChangeCell 共享内核)。
510
+ *
511
+ * 处理管线:空值判断 → 取符号 → 百分比 → 单位缩放 → 小数位 → 千分位 → 拼装。
512
+ *
513
+ * @param value 原始值(数字或可转为数字的字符串)
514
+ * @param options 格式化选项
515
+ * @returns 格式化后的字符串;空值或非数字返回 placeholder
516
+ */
517
+ function formatNumber(value, options = {}) {
518
+ const { decimals, thousands = true, prefix = "", suffix = "", showSign = false, percent = false, abbr, abbrDecimals, placeholder = "--" } = options;
519
+ if (value === null || value === void 0 || value === "") return placeholder;
520
+ const num = typeof value === "number" ? value : Number(value);
521
+ if (Number.isNaN(num)) return placeholder;
522
+ const sign = num < 0 ? "-" : showSign && num > 0 ? "+" : "";
523
+ let work = Math.abs(num);
524
+ if (percent) work = work * 100;
525
+ let unit = "";
526
+ if (abbr && !percent) {
527
+ const units = ABBR_UNITS[abbr];
528
+ for (let i = 0; i < units.length; i++) {
529
+ const threshold = units[i][0];
530
+ if (work >= threshold) {
531
+ work = work / threshold;
532
+ unit = units[i][1];
533
+ break;
534
+ }
535
+ }
536
+ }
537
+ let fractionDigits;
538
+ if (unit) fractionDigits = abbrDecimals ?? decimals ?? 2;
539
+ else fractionDigits = decimals ?? null;
540
+ let numStr = fractionDigits == null ? String(work) : work.toFixed(fractionDigits);
541
+ if (thousands) numStr = addThousandsSeparator(numStr);
542
+ return `${prefix}${sign}${numStr}${unit}${percent ? "%" : ""}${suffix}`;
543
+ }
544
+ //#endregion
545
+ //#region src/StkTable/custom-cells/NumberCell/createNumberCell.ts
546
+ /**
547
+ * 数字格式化单元格工厂函数
548
+ *
549
+ * 纯展示型单元格:将 `cellValue` 经 {@link formatNumber} 格式化为文本。
550
+ * 数字对齐请在列配置上设置 `align: 'right'`。
551
+ *
552
+ * @param options 格式化选项
553
+ * @returns `NumberCell` 组件构造函数,使用时需调用一次:`customCell: NumberCell()`
554
+ *
555
+ * @example
556
+ * ```ts
557
+ * const { NumberCell } = createNumberCell({ decimals: 2 });
558
+ * const NumberCellComp = NumberCell();
559
+ * const columns = [
560
+ * { title: '现价', dataIndex: 'price', align: 'right', customCell: NumberCellComp },
561
+ * ];
562
+ * ```
563
+ */
564
+ function createNumberCell(options) {
565
+ /** 单元格组件 - 用于 customCell */
566
+ function NumberCell() {
567
+ return markRaw(defineComponent({
568
+ props: [
569
+ "row",
570
+ "col",
571
+ "cellValue",
572
+ "rowIndex",
573
+ "colIndex",
574
+ "expanded",
575
+ "treeExpanded"
576
+ ],
577
+ setup(props) {
578
+ return () => h("span", { class: "stk-number-cell" }, formatNumber(props.cellValue, options));
579
+ }
580
+ }));
581
+ }
582
+ return { NumberCell };
583
+ }
584
+ //#endregion
585
+ //#region src/StkTable/custom-cells/ChangeCell/createChangeCell.ts
586
+ /** 根据原始值判断涨跌方向 */
587
+ function resolveDir(rawValue) {
588
+ const num = rawValue === "" || rawValue == null ? NaN : Number(rawValue);
589
+ if (Number.isNaN(num) || num === 0) return "flat";
590
+ return num > 0 ? "rise" : "fall";
591
+ }
592
+ /**
593
+ * 涨跌单元格工厂函数
594
+ *
595
+ * 在 {@link formatNumber} 的基础上叠加涨跌染色与箭头。染色按 `cellValue` 正负判断,
596
+ * 颜色方向可通过 `colorReverse` 切换(A股 / 国际),也可用 `riseColor` 等自定义。
597
+ * 数字对齐请在列配置上设置 `align: 'right'`。
598
+ *
599
+ * @param options 格式化 + 染色/箭头选项
600
+ * @returns `ChangeCell` 组件构造函数,使用时需调用一次:`customCell: ChangeCell()`
601
+ *
602
+ * @example
603
+ * ```ts
604
+ * const { ChangeCell } = createChangeCell({ decimals: 2, showSign: true, arrow: true });
605
+ * const ChangeCellComp = ChangeCell();
606
+ * const columns = [
607
+ * { title: '涨跌额', dataIndex: 'change', align: 'right', customCell: ChangeCellComp },
608
+ * ];
609
+ * ```
610
+ */
611
+ function createChangeCell(options = {}) {
612
+ const { colorReverse = false, arrow = false, riseColor, fallColor, flatColor } = options;
613
+ /** 单元格组件 - 用于 customCell */
614
+ function ChangeCell() {
615
+ return markRaw(defineComponent({
616
+ props: [
617
+ "row",
618
+ "col",
619
+ "cellValue",
620
+ "rowIndex",
621
+ "colIndex",
622
+ "expanded",
623
+ "treeExpanded"
624
+ ],
625
+ setup(props) {
626
+ return () => {
627
+ const raw = props.cellValue;
628
+ const dir = resolveDir(raw);
629
+ let colorClass = "stk-change-cell--flat";
630
+ if (dir === "rise") colorClass = colorReverse ? "stk-change-cell--green" : "stk-change-cell--red";
631
+ else if (dir === "fall") colorClass = colorReverse ? "stk-change-cell--red" : "stk-change-cell--green";
632
+ const customColor = dir === "rise" ? riseColor : dir === "fall" ? fallColor : flatColor;
633
+ const arrowChar = arrow && dir !== "flat" ? dir === "rise" ? "▲" : "▼" : "";
634
+ return h("span", {
635
+ class: ["stk-change-cell", colorClass],
636
+ style: customColor ? { color: customColor } : void 0
637
+ }, [arrowChar ? h("span", { class: "stk-change-cell__arrow" }, arrowChar) : null, formatNumber(raw, options)]);
638
+ };
639
+ }
640
+ }));
641
+ }
642
+ return { ChangeCell };
643
+ }
644
+ //#endregion
645
+ export { StkTable_default as StkTable, binarySearch, createChangeCell, createCheckboxCell, createEditableCell, createFilterCell, createNumberCell, formatNumber, insertToOrderedArray, registerFeature, strCompare, tableSort, useAreaSelection };
package/lib/style.css CHANGED
@@ -139,6 +139,32 @@
139
139
  cursor:pointer;
140
140
  vertical-align:middle;
141
141
  }
142
+ .stk-change-cell{
143
+ --stk-change-red:#e02020;
144
+ --stk-change-green:#00a870;
145
+ --stk-change-flat:#666;
146
+ display:inline-flex;
147
+ align-items:center;
148
+ gap:2px;
149
+ }
150
+ .stk-change-cell--red{
151
+ color:var(--stk-change-red);
152
+ }
153
+ .stk-change-cell--green{
154
+ color:var(--stk-change-green);
155
+ }
156
+ .stk-change-cell--flat{
157
+ color:var(--stk-change-flat);
158
+ }
159
+ .stk-change-cell__arrow{
160
+ font-size:0.85em;
161
+ line-height:1;
162
+ }
163
+ .stk-table.dark .stk-change-cell{
164
+ --stk-change-red:#ff5c5c;
165
+ --stk-change-green:#26c281;
166
+ --stk-change-flat:#aaa;
167
+ }
142
168
  @keyframes stk-table-dim{
143
169
  from{
144
170
  background-color:var(--highlight-color);
@@ -226,7 +252,7 @@
226
252
  --rs-bc:#4b8ef0;
227
253
  color:#d1d1e0;
228
254
  }
229
- .stk-table.dark .cell-range-selected{
255
+ .stk-table.dark td[data-cs-s]{
230
256
  background-blend-mode:normal;
231
257
  }
232
258
  .stk-table.scrollbar-on{
@@ -255,7 +281,7 @@
255
281
  pointer-events:none;
256
282
  }
257
283
  .stk-table.bordered{
258
- border-left:1px solid var(--border-color);
284
+ border-left:var(--border-width) solid var(--border-color);
259
285
  background-image:var(--bg-border-top), var(--bg-border-right), var(--bg-border-bottom);
260
286
  }
261
287
  .stk-table.bordered th,
@@ -286,7 +312,7 @@
286
312
  .stk-table.stripe.vt-on .stk-tbody-main tr:nth-child(odd){
287
313
  background-color:var(--stripe-bgc);
288
314
  }
289
- .stk-table.stripe.area-selection .stk-tbody-main tr.row-range-selected{
315
+ .stk-table.stripe.area-selection .stk-tbody-main tr[data-rs-s]{
290
316
  background-color:var(--rs-bgc);
291
317
  }
292
318
  .stk-table.stripe.row-hover .stk-tbody-main tr:hover{
@@ -301,7 +327,7 @@
301
327
  .stk-table.row-active .stk-tbody-main tr.active{
302
328
  background-color:var(--tr-active-bgc);
303
329
  }
304
- .stk-table .stk-tbody-main tr.row-range-selected{
330
+ .stk-table .stk-tbody-main tr[data-rs-s]{
305
331
  background-color:var(--rs-bgc);
306
332
  }
307
333
  .stk-table .stk-tbody-main tr.row-range-start td{
@@ -495,22 +521,26 @@
495
521
  .stk-table .fixed-cell{
496
522
  background-color:inherit;
497
523
  }
498
- .stk-table .cell-range-selected{
524
+ .stk-table td[data-cs-s]{
499
525
  background-blend-mode:multiply;
500
526
  background-color:var(--cs-bgc);
501
527
  box-shadow:inset 0 0 0 0 var(--cs-bc), inset 0 0 0 0 var(--cs-bc), inset 0 0 0 0 var(--cs-bc), inset 0 0 0 0 var(--cs-bc);
502
528
  box-shadow:inset 0 var(--cs-t, 0) 0 0 var(--cs-bc), inset 0 var(--cs-b, 0) 0 0 var(--cs-bc), inset var(--cs-l, 0) 0 0 0 var(--cs-bc), inset var(--cs-r, 0) 0 0 0 var(--cs-bc);
503
529
  }
504
- .stk-table .cell-range-t{
530
+ .stk-table td[data-cs-t]{
505
531
  --cs-t:2px;
506
532
  }
507
- .stk-table .cell-range-b{
533
+ .stk-table td[data-cs-s].cell-hover,
534
+ .stk-table td[data-cs-s].cell-active{
535
+ background-color:var(--cs-bgc);
536
+ }
537
+ .stk-table td[data-cs-b]{
508
538
  --cs-b:-2px;
509
539
  }
510
- .stk-table .cell-range-l{
540
+ .stk-table td[data-cs-l]{
511
541
  --cs-l:2px;
512
542
  }
513
- .stk-table .cell-range-r{
543
+ .stk-table td[data-cs-r]{
514
544
  --cs-r:-2px;
515
545
  }
516
546
  .stk-table .highlight-cell{