This commit is contained in:
oneao committed 2026-10-12 22:25:41 +08:00
1 parent ae3ca3cfa1
commit 7bd5f1dde9
45 files changed
+3414 -229

No files matched your search

@@ -1,20 +1,29 @@
<script setup lang="ts">
import { ref } from 'vue'
import { Pencil, Trash2 } from '@lucide/vue'
import { Pencil, Trash2, Copy, Archive, Download } from '@lucide/vue'
import { G3Message, G3Table } from '@g3soft/ui'
import type { G3TableColumn, TableRow } from '@g3soft/ui'
// 操作列:列上加 actions 即可,渲染成一排小按钮。
// - type: 'more' 的项会收进「更多」下拉(这里没配,全部内联显示)
// - confirm 给字符串或对象时先弹确认框,确认后才执行 onClick
// - 操作列建议显式写 resizable: false(宽度由内容决定,拖了不落库)
// - type: 'danger' | 'delete' → 危险色
// - visible / disabled 可传 boolean 或 (row) => boolean
// - confirm:字符串=标题,对象=完整配置;确认后才执行 onClick
// - type: 'more' 的项收进「更多」下拉(它的 items 是子项)
//
// 操作列建议显式写 resizable: false —— 宽度由内容决定,拖了不会落库,列一重建就丢。
const data = ref<TableRow[]>([
{ id: 1, name: '框架合同', amount: 120000, status: 'draft' },
{ id: 2, name: '采购订单', amount: 86000, status: 'active' },
{ id: 3, name: '验收单', amount: 43000, status: 'archived' },
])
const columns: G3TableColumn<TableRow>[] = [
{ dataIndex: 'name', title: '名称', width: 180 },
{ dataIndex: 'amount', title: '金额', width: 120, align: 'right' },
{
dataIndex: 'op',
title: '操作',
width: 160,
width: 220,
resizable: false,
actions: [
{
@@ -26,22 +35,49 @@ const columns: G3TableColumn<TableRow>[] = [
label: '删除',
icon: Trash2,
type: 'danger',
// 已归档的单据不允许删除
disabled: (row) => row.status === 'archived',
confirm: { title: '确认删除', description: '删除后不可恢复,确定继续吗?' },
onClick: (row) => G3Message.success(`已删除:${row.name}`),
},
{
type: 'more',
label: '更多',
items: [
{
key: 'copy',
label: '复制',
icon: Copy,
onClick: (row) => G3Message.info(`复制:${row.name}`),
},
{
key: 'archive',
label: '归档',
icon: Archive,
// 已归档的不再显示该项(visible 传函数)
visible: (row) => row.status !== 'archived',
onClick: (row) => G3Message.success(`已归档:${row.name}`),
},
{
key: 'export',
label: '导出',
icon: Download,
onClick: (row) => G3Message.info(`导出:${row.name}`),
},
],
},
],
},
]
const data = ref<TableRow[]>([
{ id: 1, name: '框架合同', amount: 120000 },
{ id: 2, name: '采购订单', amount: 86000 },
{ id: 3, name: '验收单', amount: 43000 },
])
</script>
<template>
<div style="height: 260px">
<G3Table :columns="columns" :data-source="data" />
<div>
<p style="margin: 0 0 8px; color: #999; font-size: 12px">
「删除」带二次确认,且对已归档行禁用;「更多」里「归档」对已归档行隐藏。
</p>
<div style="height: 260px">
<G3Table :columns="columns" :data-source="data" />
</div>
</div>
</template>
@@ -3,14 +3,16 @@ import { ref } from 'vue'
import { G3Table } from '@g3soft/ui'
import type { G3TableColumn, TableRow } from '@g3soft/ui'
// 列定义:dataIndex 指向行数据的字段
// 最小用法:给 columns 与 dataSource 两份数据即可。
// - seq 默认开启,会在最前列注入序号列
// - bordered 默认开启,边框由底座(stk-table-vue)绘制
// - 没写 width 的列会自动摊掉剩余宽度
const columns: G3TableColumn<TableRow>[] = [
{ dataIndex: 'name', title: '名称', width: 180 },
{ dataIndex: 'amount', title: '金额', width: 120, align: 'right' },
{ dataIndex: 'owner', title: '负责人' },
]
// seq 默认开启(最前面的序号列),bordered 默认 true
const data = ref<TableRow[]>([
{ id: 1, name: '框架合同', amount: 120000, owner: '张三' },
{ id: 2, name: '采购订单', amount: 86000, owner: '李四' },
@@ -3,17 +3,21 @@ import { ref } from 'vue'
import { G3Table } from '@g3soft/ui'
import type { G3TableColumn, TableRow } from '@g3soft/ui'
// 行内编辑:列上加 editor 即可。
// 单击单元格进入编辑;Enter 确认、Esc 取消。值变化通过 change 事件抛出。
// 行内编辑:列上加 editor 即可。单击单元格进入编辑,Enter 确认、Esc 取消。
//
// 编辑器类型:text / number / textarea / select / checkbox
// checkbox 约定 1 = 选中、0 = 未选中(与后端 tinyint 对齐)。
// 编辑器类型:
// text / number / password / textarea → 用 G3Input 渲染
// select → 用 G3Select 渲染;options 可传数组,也可传「接收当前行的函数」
// checkbox → 点击即切换,无需先激活整行;约定 1 = 选中、0 = 未选中(对齐后端 tinyint)
//
// 关键行为:输入值先写进内部草稿,**失焦才落库并抛出 change**。
// 这样每敲一个字符不会触发父级重渲染(否则光标会丢、输入法会断)。
const columns: G3TableColumn<TableRow>[] = [
{ dataIndex: 'name', title: '名称', width: 200, editor: { type: 'text' } },
{
dataIndex: 'status',
title: '状态',
width: 140,
title: '状态(select)',
width: 150,
editor: {
type: 'select',
options: [
@@ -23,25 +27,53 @@ const columns: G3TableColumn<TableRow>[] = [
},
},
{ dataIndex: 'enabled', title: '启用', width: 80, editor: { type: 'checkbox' } },
{ dataIndex: 'qty', title: '数量', width: 120, align: 'right', editor: { type: 'number' } },
{ dataIndex: 'qty', title: '数量(number)', width: 140, align: 'right', editor: { type: 'number' } },
{
dataIndex: 'remark',
title: '备注(textarea)',
width: 180,
editor: { type: 'textarea' },
},
{
dataIndex: 'owner',
title: '负责人(options 依赖本行)',
width: 200,
// options 传函数时会收到当前行 —— 按「状态」给出不同候选人。
// 注意这类列在非编辑态会把值解析成 label 展示。
editor: {
type: 'select',
options: (row: TableRow) =>
row.status === 1
? [
{ label: '张三', value: '张三' },
{ label: '李四', value: '李四' },
]
: [{ label: '王五', value: '王五' }],
},
},
]
const data = ref<TableRow[]>([
{ id: 1, name: '框架合同', status: 1, enabled: 1, qty: 12 },
{ id: 2, name: '采购订单', status: 0, enabled: 0, qty: 45 },
{ id: 1, name: '框架合同', status: 1, enabled: 1, qty: 12, remark: '年度框架', owner: '张三' },
{ id: 2, name: '采购订单', status: 0, enabled: 0, qty: 45, remark: '', owner: '王五' },
])
const lastChange = ref('')
function onChange(row: TableRow, col: G3TableColumn<TableRow>, value: unknown) {
lastChange.value = `${row.name} 的「${col.title}」改为 ${value}`
// 只有值**真的变化**才会走到这里(组件内部按编辑前的快照去重),
// 所以「点进去又点出来」不会产生无意义的 change。
lastChange.value = `「${row.name}」的「${col.title}」→ ${value}`
}
</script>
<template>
<div>
<p style="margin: 0 0 8px; color: #666; font-size: 13px">
最近一次修改:{{ lastChange || '(点击单元格试试)' }}
最近一次 change:{{ lastChange || '(点击单元格试试)' }}
</p>
<p style="margin: 0 0 8px; color: #999; font-size: 12px">
单击进入编辑 · Enter 确认 · Esc 取消 · Tab / 方向键可换格
</p>
<div style="height: 240px">
<G3Table :columns="columns" :data-source="data" :seq="false" @change="onChange" />
@@ -3,21 +3,22 @@ import { ref } from 'vue'
import { G3Table } from '@g3soft/ui'
import type { G3TableColumn, TableRow } from '@g3soft/ui'
// 列宽 / 列序拖拽与「列定义回写」:
// 两者都要求用 v-model:columns 绑定,否则组件拖完无处回写(松手回弹),
// 组件检测到没绑定会直接关掉这两个功能。
// 列宽 / 列序拖拽要求用 v-model:columns 绑定列定义。
// 组件会检测父级有没有绑 update:columns —— 没绑就**直接关掉**这两个功能,
// 因为拖完无处回写(松手回弹)。这也是最容易踩的一条。
const columns = ref<G3TableColumn<TableRow>[]>([
{ dataIndex: 'name', title: '名称(可拖宽)', width: 200 },
{ dataIndex: 'amount', title: '金额', width: 140, align: 'right' },
{ dataIndex: 'owner', title: '负责人', width: 140 },
// 操作列声明 resizable: false 后不可拖宽
// 操作列显式声明 resizable: false —— 它的宽度由内容决定,拖了不会落库、列一重建就丢。
// 也可以在 colResizable 里用 disabled(col) 按列禁用。
{ dataIndex: 'op', title: '操作', width: 100, resizable: false },
])
// 行拖拽排序:开启后显示拖拽手柄,拖完通过 row-order-change 回传新顺序
const dragColumns = ref<G3TableColumn<TableRow>[]>([
{ dataIndex: 'name', title: '名称' },
{ dataIndex: 'amount', title: '金额', align: 'right' },
// 按列禁用拖宽的另一种写法:整列标成 noResize。
const columnsByConfig = ref<G3TableColumn<TableRow>[]>([
{ dataIndex: 'name', title: '名称', width: 180 },
{ dataIndex: 'amount', title: '金额(禁用拖宽)', width: 160, align: 'right' },
])
const data = ref<TableRow[]>([
@@ -26,27 +27,60 @@ const data = ref<TableRow[]>([
{ id: 3, name: '验收单', amount: 43000, owner: '王五', op: '—' },
])
// ── 行拖拽排序 ──
// row-draggable 开启后最左侧出现拖拽手柄;拖完通过 row-order-change 回传新顺序。
// 注意回传的是 (rows, sourceIndex, targetIndex) —— rows 是拖拽后的完整数据,直接用即可。
const dragColumns = ref<G3TableColumn<TableRow>[]>([
{ dataIndex: 'name', title: '步骤' },
{ dataIndex: 'amount', title: '耗时(分钟)', align: 'right' },
])
const dragData = ref<TableRow[]>([
{ id: 1, name: '第一步', amount: 100 },
{ id: 2, name: '第二步', amount: 200 },
{ id: 3, name: '第三步', amount: 300 },
{ id: 1, name: '第一步:准备', amount: 100 },
{ id: 2, name: '第二步:执行', amount: 200 },
{ id: 3, name: '第三步:验收', amount: 300 },
])
const orderText = ref('')
function onRowOrderChange(rows: TableRow[]) {
function onRowOrderChange(rows: TableRow[], sourceIndex: number, targetIndex: number) {
dragData.value = rows
orderText.value = rows.map((row) => row.name).join(' → ')
orderText.value = `${sourceIndex} → ${targetIndex}:${rows.map((r) => r.name).join(' → ')}`
}
// 当前列宽(演示 v-model:columns 的回写确实发生了)
const widths = ref('')
function onColumnsUpdate(next: G3TableColumn<TableRow>[]) {
columns.value = next
widths.value = next
.filter((c) => c.dataIndex !== 'op')
.map((c) => `${c.title}=${c.width ?? 'auto'}`)
.join(' · ')
}
</script>
<template>
<div>
<p style="margin: 0 0 8px; color: #666; font-size: 13px">
拖动表头分隔线改列宽、拖表头文字改列序。
<p style="margin: 0 0 8px; color: #999; font-size: 12px">
拖动表头分隔线改列宽、拖动表头文字改列序。「操作」列设了 resizable: false,拖不动。
</p>
<div style="height: 240px">
<G3Table v-model:columns="columns" :data-source="data" :row-selection="null" />
<p style="margin: 0 0 8px; color: #666; font-size: 13px">
回写后的列宽:{{ widths || '(拖一下试试)' }}
</p>
<div style="height: 220px">
<G3Table :columns="columns" :data-source="data" @update:columns="onColumnsUpdate" />
</div>
<p style="margin: 16px 0 8px; color: #999; font-size: 12px">
按列禁用拖宽:用 colResizable 的 disabled(col) 回调。
</p>
<div style="height: 180px">
<G3Table
v-model:columns="columnsByConfig"
:data-source="data"
:seq="false"
:col-resizable="{ disabled: (col) => col.dataIndex === 'amount' }"
/>
</div>
<p style="margin: 16px 0 8px; color: #666; font-size: 13px">
@@ -4,31 +4,37 @@ import { G3Table } from '@g3soft/ui'
import type { G3TableColumn, TableRow } from '@g3soft/ui'
// 多选:传 rowSelection 即在最前注入选择列。
// getCheckboxProps 可禁用个别行(禁用行不参与「全选」的分母)。
// - selectedRowKeys 用 v-model 式双向绑定,勾选变化会回写
// - getCheckboxProps 可禁用个别行;**禁用行不计入「全选」的分母**
// (所以下面表格里表头全选只会选中 2 行)
const columns: G3TableColumn<TableRow>[] = [
{ dataIndex: 'name', title: '名称', width: 180 },
{ dataIndex: 'amount', title: '金额', width: 120, align: 'right' },
{ dataIndex: 'owner', title: '负责人', width: 120 },
]
const data: TableRow[] = [
{ id: 1, name: '框架合同', amount: 120000 },
{ id: 2, name: '采购订单', amount: 86000, locked: true },
{ id: 3, name: '验收单', amount: 43000 },
{ id: 1, name: '框架合同', amount: 120000, owner: '张三' },
{ id: 2, name: '采购订单', amount: 86000, owner: '李四', locked: true },
{ id: 3, name: '验收单', amount: 43000, owner: '王五' },
]
const selectedRowKeys = ref<Array<string | number>>([])
const selectedText = ref('')
const selectedNames = ref<string[]>([])
function onSelectionChange(keys: Array<string | number>, rows: TableRow[]) {
selectedRowKeys.value = keys
selectedText.value = rows.map((row) => row.name).join('、')
selectedNames.value = rows.map((row) => String(row.name))
}
</script>
<template>
<div>
<p style="margin: 0 0 8px; color: #666; font-size: 13px">
已选:{{ selectedText || '(无)' }}
已选 {{ selectedRowKeys.length }} 项:{{ selectedNames.join('、') || '(无)' }}
</p>
<p style="margin: 0 0 8px; color: #999; font-size: 12px">
「采购订单」被 getCheckboxProps 禁用,不参与全选。
</p>
<div style="height: 260px">
<G3Table
@@ -3,18 +3,41 @@ import { ref } from 'vue'
import { G3Table } from '@g3soft/ui'
import type { G3TableColumn, TableRow } from '@g3soft/ui'
// 排序:列上加 sorter: true 即可点表头排序(组件内排,不请求接口)。
// sortType 决定按数字还是字符串比较,默认按字符串。
// 排序:列上写 sorter: true 即可点表头排序(组件内排序,不请求接口)。
// - sortType 决定按数字还是字符串比较,默认字符串;金额这类数值列要显式写 'number'
// - 需要自定义规则时 sorter 也可以传函数(见「名称长度」列)
const columns: G3TableColumn<TableRow>[] = [
{ dataIndex: 'name', title: '名称', width: 180 },
{ dataIndex: 'amount', title: '金额', width: 140, align: 'right', sorter: true, sortType: 'number' },
{
dataIndex: 'amount',
title: '金额(数字排序)',
width: 160,
align: 'right',
sorter: true,
sortType: 'number',
},
{ dataIndex: 'qty', title: '数量', width: 120, align: 'right', sorter: true, sortType: 'number' },
{
// dataIndex 必须指向一个**真实存在的字段**:库按 row[dataIndex] 渲染单元格,
// 复用 'name' 会让这一列显示名称本身(而不是长度),并且与「名称」列重复。
dataIndex: 'nameLength',
title: '名称长度(自定义)',
width: 160,
align: 'right',
// 自定义排序接收的是**整个数组**与排序状态,返回排好的新数组
// (不是 Array.prototype.sort 的 (a, b) 比较器——写成 (a, b) 会静默不生效)
sorter: (rows: TableRow[], { order }: { order: 'asc' | 'desc' | null }) => {
if (!order) return rows
const sign = order === 'asc' ? 1 : -1
return [...rows].sort((a, b) => (Number(a.nameLength) - Number(b.nameLength)) * sign)
},
},
]
const data = ref<TableRow[]>([
{ id: 1, name: '框架合同', amount: 120000, qty: 12 },
{ id: 2, name: '采购订单', amount: 86000, qty: 45 },
{ id: 3, name: '验收单', amount: 43000, qty: 7 },
{ id: 1, name: '框架合同', nameLength: 4, amount: 120000, qty: 12 },
{ id: 2, name: '采购订单补充协议', nameLength: 8, amount: 86000, qty: 45 },
{ id: 3, name: '验收单', nameLength: 3, amount: 43000, qty: 7 },
])
</script>
@@ -3,13 +3,14 @@ import { computed, ref } from 'vue'
import { G3Table } from '@g3soft/ui'
import type { G3TableColumn, TableRow } from '@g3soft/ui'
// 性能门控:行数超过 100 才开启纵向虚拟滚动。
// 组件内部按 dataSource.length 自动判断,业务不需要手动切换 virtual。
// 性能门控:行数 > 100 才开启纵向虚拟滚动,组件内部按 dataSource.length 自动判断,
// 业务不需要手动切 virtual(也**不能**强制打开到阈值以下)。
// 拖动下面的滑杆可以直观看到阈值前后的差异。
const rowCount = ref(5000)
const virtualColumns: G3TableColumn<TableRow>[] = [
{ dataIndex: 'id', title: 'ID', width: 90 },
{ dataIndex: 'name', title: '名称', width: 200 },
{ dataIndex: 'value', title: '数值', width: 140, align: 'right' },
{ dataIndex: 'value', title: '数值', width: 140, align: 'right', sorter: true, sortType: 'number' },
]
const virtualData = computed(() =>
@@ -24,9 +25,11 @@ const virtualData = computed(() =>
<template>
<div>
<p style="margin: 0 0 8px; color: #666; font-size: 13px">
当前 {{ rowCount }} 行 → {{ rowCount > 100 ? '已开启' : '未开启(行数不足 100 时全量渲染)' }}纵向虚拟滚动
当前 {{ rowCount }} 行 →
{{ rowCount > 100 ? '已开启' : '未开启(行数不足 100 时全量渲染)' }}纵向虚拟滚动
</p>
<div style="height: 320px">
<input v-model.number="rowCount" type="range" min="10" max="5000" step="10" />
<div style="height: 320px; margin-top: 8px">
<G3Table :columns="virtualColumns" :data-source="virtualData" :seq="false" />
</div>
</div>