Files
workspace/code/g3soft-libs/packages/ui/playground/main.ts
T
2026-10-13 22:32:50 +08:00

707 lines
28 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { createApp, reactive, ref } from 'vue'
import { Plus, Search, Settings } from '@lucide/vue'
import {
G3Button,
G3ButtonGroup,
G3ButtonGroupSeparator,
G3Checkbox,
G3Col,
G3ColorPicker,
G3DatePicker,
G3Drawer,
G3Dropdown,
G3DropdownItem,
G3Form,
G3FormGroup,
G3FormItem,
G3Input,
G3Message,
G3Modal,
G3Notification,
G3Pagination,
G3Popover,
G3Radio,
G3RadioGroup,
G3RangePicker,
G3RichText,
G3Row,
G3Scrollbar,
G3Segmented,
G3Select,
G3SliderCaptcha,
G3Space,
G3Spin,
G3Splitter,
G3SplitterPanel,
G3Switch,
G3Table,
G3TabPane,
G3Tabs,
G3Tag,
G3TimeSelect,
G3Tooltip,
G3Tree,
} from '@g3soft/ui'
import '@g3soft/tokens'
/**
* playground —— 组件在真实浏览器里的落脚点。
*
* 约定:所有 E2E 与人工手测都通过 `data-testid` 找元素,
* 不要依赖文本或层级结构(文案会改、结构会调,testid 保持稳定)。
*/
const ROLE_OPTIONS = [
{ label: '管理员', value: 1 },
{ label: '会员', value: 2 },
{ label: '访客', value: 3 },
]
const TREE_DATA = [
{
key: '1',
title: '前端',
children: [
{ key: '1-1', title: 'Vue', children: [{ key: '1-1-1', title: '组合式 API' }] },
{ key: '1-2', title: 'React' },
],
},
{ key: '2', title: '后端', children: [{ key: '2-1', title: 'Node.js' }] },
]
const LONG_TEXT = Array.from({ length: 40 }, (_, index) => `第 ${index + 1} 行内容`).join('\n')
const App = {
components: {
G3Button,
G3ButtonGroup,
G3ButtonGroupSeparator,
G3Checkbox,
G3Col,
G3ColorPicker,
G3DatePicker,
G3Drawer,
G3Dropdown,
G3DropdownItem,
G3Form,
G3FormGroup,
G3FormItem,
G3Input,
G3Modal,
G3Pagination,
G3Popover,
G3Radio,
G3RadioGroup,
G3RangePicker,
G3RichText,
G3Row,
G3Scrollbar,
G3Segmented,
G3Select,
G3SliderCaptcha,
G3Space,
G3Spin,
G3Splitter,
G3SplitterPanel,
G3Switch,
G3Table,
G3TabPane,
G3Tabs,
G3Tag,
G3TimeSelect,
G3Tooltip,
G3Tree,
Plus,
Search,
Settings,
},
setup() {
const model = reactive({
name: '张三',
intro: '这是多行文本',
role: 1 as number | undefined,
city: [] as number[],
agree: true,
level: 2,
gender: 'male',
date: '2026-10-08 09:30:00',
range: ['2026-10-01', '2026-10-08'] as string[],
})
const tab = ref('a')
// 选项是中文原始值,初始值必须对上,否则没有选中项
const seg = ref('日')
const time = ref(new Date(2026, 9, 8, 9, 30, 0))
const modalOpen = ref(false)
const modalConfirmOpen = ref(false)
const drawerOpen = ref(false)
// 四个方向都要能验:上下面板的宽度必须撑满视口(曾因用 flex 对齐而缩到内容宽)
const drawerPlacement = ref<'left' | 'right' | 'top' | 'bottom'>('right')
// 挂载即展开的浮层:验证「初始 open」也能拿到 z-index
const popoverAOpen = ref(true)
const popoverBOpen = ref(true)
// 颜色选择器:给 RTL 用例验「取色区/色相条不镜像」用
const color = ref('#165dff')
// 滑块验证码:给 E2E 验真实指针拖拽、键盘与 RTL 起步边
const captchaPassed = ref(false)
const captchaDisabled = ref(false)
// 富文本编辑器:给样式核对用
const richHtml = ref('<h2>标题</h2><p>这是<b>加粗</b>、<i>斜体</i>、<sup>上标</sup>、<sub>下标</sub>的正文。</p><ul><li>列表项一</li><li>列表项二</li></ul>')
// RTL 开关:直接翻 <html dir>。CSS 侧靠 [dir='rtl'] 选择器,JS 侧靠 useDirection
// 观察 documentElement,两边都跟得上。也支持 ?dir=rtl 直接以 RTL 进入,
// 便于 E2E 与人工核对(不依赖点击顺序)。
const rtl = ref(new URLSearchParams(location.search).get('dir') === 'rtl')
function applyRtl() {
document.documentElement.setAttribute('dir', rtl.value ? 'rtl' : 'ltr')
}
function toggleRtl() {
rtl.value = !rtl.value
applyRtl()
}
applyRtl()
// 扁平树:两个节点都不是父节点,用来验「叶子不该留展开箭头占位」
const flatTreeData = [
{ key: 'flat-1', title: '节点一(无子节点)' },
{ key: 'flat-2', title: '节点二(无子节点)' },
]
const treeMenuItems = [
{ key: 'add', label: '新增子节点' },
{ key: 'rename', label: '重命名' },
{ key: 'delete', label: '删除', danger: true },
]
function toast(placement?: string) {
G3Message.success(`操作成功`, placement ? { placement: placement as never } : undefined)
}
function notify() {
G3Notification.info({ title: '定时任务完成', description: '本次共同步 128 条数据。' })
}
function openDrawer(placement: 'left' | 'right' | 'top' | 'bottom') {
drawerPlacement.value = placement
drawerOpen.value = true
}
function openCommandConfirm() {
G3Modal.confirm({
title: '删除确认',
content: '确定要删除这条记录吗?',
onConfirm: () => new Promise<void>((resolve) => setTimeout(resolve, 600)),
})
}
/* ---- Table 表格 ---- */
// 小数据量:用于验证「行数 ≤ 100 时关闭虚拟滚动」的门控
// 注意必须是 ref:模板里 v-model:columns 要能回写,否则拖列宽无处落库(松手回弹)
const tableColumns = ref([
{ dataIndex: 'name', title: '名称', width: 180 },
{
dataIndex: 'amount',
title: '金额',
width: 120,
align: 'right',
editor: {
type: 'number',
// 金额必须 > 0:让「校验态全表可见」在 E2E 里有可断言的 error 数据
validate: (v: unknown) => Number(v) > 0 || '金额必须大于 0',
},
},
{
dataIndex: 'status',
title: '状态',
width: 110,
editor: {
type: 'select',
options: [
{ label: '启用', value: 1 },
{ label: '停用', value: 0 },
],
// 按行禁用:enabled=0 的那一行状态不许改 —— 用来锁「动态灰底」这条视觉
// (e2e 断言第 2 行灰、第 1 行不灰,且都**不需要先进入编辑行**)
disabled: (row: { enabled?: number }) => row.enabled === 0,
},
},
{ dataIndex: 'enabled', title: '启用', width: 80, editor: { type: 'checkbox' } },
{
dataIndex: 'remark',
title: '备注',
editor: {
type: 'text',
// 备注非空:提供 warning 级校验(金额是 error,这里是 warning,两级都有样例)
validateWarning: (v: unknown) => String(v ?? '').length > 0 || '建议填写备注',
},
},
{ dataIndex: 'op', title: '操作', width: 140, resizable: false, actions: [
{ label: '编辑', onClick: () => {} },
{ label: '删除', type: 'danger', confirm: '确认删除该行?', onClick: () => {} },
] },
])
const tableData = ref([
{ id: 1, name: '合同 A', amount: 12000, status: 1, enabled: 1, remark: '已归档' },
{ id: 2, name: '合同 B', amount: 0, status: 0, enabled: 0, remark: '' },
{ id: 3, name: '合同 C', amount: 43000, status: 1, enabled: 1, remark: '待复核' },
])
const tableSelectedKeys = ref([])
// 大数据量:用于验证虚拟滚动真的开启
// 后两列带 editor:「编辑行被虚拟滚动滚出窗口 → 编辑器卸载 → 焦点掉到 body」这条
// 路径只能在这种「行数多到会虚拟化」的表上复现(见 e2e/table.spec.ts 的键盘跳格用例)
const bigTableColumns = [
{ dataIndex: 'id', title: 'ID', width: 80 },
{ dataIndex: 'name', title: '名称', width: 200, editor: { type: 'text' } },
{ dataIndex: 'value', title: '数值', width: 140, align: 'right', editor: { type: 'number' } },
]
const bigTableData = Array.from({ length: 5000 }, (_, i) => ({
id: i + 1,
name: `记录 ${i + 1}`,
value: (i + 1) * 7,
}))
/**
* 纯展示表(列上**都没有** `editor`)—— 「不可改灰底」门控的宿主。
*
* 灰底的语义是「这一格现在改不了」,只有**旁边存在可改的格子**时才是信息。
* 这类表若逐格铺灰,整张表看上去就是「禁用状态」(文档里 6/7 张 demo 正是纯展示表)。
* 判据在 G3Table 的 editableColumnsExist / shouldMarkReadonly,见那边注释。
*/
const plainColumns = [
{ dataIndex: 'code', title: '编号', width: 120 },
{ dataIndex: 'name', title: '名称', width: 200 },
{ dataIndex: 'remark', title: '备注', width: 200 },
]
const plainData = ref([
{ code: 'A-001', name: '纯展示行', remark: '这一列也不可编辑' },
{ code: 'A-002', name: '纯展示行 2', remark: '这一列也不可编辑' },
])
/* ---- Table:固定列 + 校验态(几何回归用例的宿主)---- */
/**
* 为什么单独造一份数据:
* 「校验未通过的格子刚好在固定列上」这个组合出过一次**只有真浏览器才量得出**的
* 位移 —— 校验态样式给 td 的 `position: relative` 覆盖了库的固定列定位,
* 内联的 `left: 200px` 于是从「sticky 吸附偏移」变成普通相对位移,
* 整格被推开「前面所有左固定列宽度之和」。happy-dom 没有布局引擎,锁不住这类问题。
*
* 两个刻意之处:
* · 第 2 行名称为空 → 固定列上必然出现一个 error 格;
* · 列宽总和(1680px)超过视口宽度 → 必须横向滚动,固定列才有「固定」可言。
*/
const fixedTableColumns = [
{ dataIndex: 'code', title: '编号(左固定)', width: 200, fixed: 'left' },
{
dataIndex: 'name',
title: '名称(校验列)',
width: 200,
fixed: 'left',
editor: {
type: 'text',
validate: (v: unknown) => String(v ?? '').length > 0 || '名称不能为空',
},
},
// 填充列:把总宽顶出视口,否则固定列与普通列长得一模一样
...Array.from({ length: 6 }, (_, i) => ({
dataIndex: `extra${i}`,
title: `填充列 ${i + 1}`,
width: 180,
})),
{ dataIndex: 'remark', title: '备注(右固定)', width: 200, fixed: 'right' },
]
const fixedRowFillers = (row: number) =>
Object.fromEntries(Array.from({ length: 6 }, (_, k) => [`extra${k}`, `填充值 ${k + 1}-${row}`]))
const fixedTableData = [
{ id: 1, code: 'A-001', name: '正常行', remark: '已归档', ...fixedRowFillers(1) },
// 空名称 → 这一格必然处于 error 态
{ id: 2, code: 'A-002', name: '', remark: '待复核', ...fixedRowFillers(2) },
]
/**
* 当前行按 fms 那套写法交给调用方管(`:active-row-key` + `@row-click`)。
*
* 这张表专门用来锁「受控调用方点格进入编辑 → 当前行也要高亮」——
* 曾经因为浏览器在「单元格 DOM 被替换」时不派发 click,`row-click` 永不触发,
* 调用方这个唯一写入者收不到消息,整行不高亮(见 G3Table 的 handleAreaSelectionChange)。
*/
const fixedActiveKey = ref<string | null>(null)
return {
rtl,
toggleRtl,
tableColumns,
tableData,
tableSelectedKeys,
bigTableColumns,
bigTableData,
plainColumns,
plainData,
fixedTableColumns,
fixedTableData,
fixedActiveKey,
model,
tab,
seg,
time,
modalOpen,
modalConfirmOpen,
drawerOpen,
drawerPlacement,
openDrawer,
popoverAOpen,
popoverBOpen,
color,
captchaPassed,
captchaDisabled,
richHtml,
flatTreeData,
treeMenuItems,
roleOptions: ROLE_OPTIONS,
treeData: TREE_DATA,
longText: LONG_TEXT,
toast,
notify,
openCommandConfirm,
}
},
template: `
<div class="page">
<div class="row" style="padding: 8px 0">
<G3Button size="small" data-testid="toggle-rtl" @click="toggleRtl">
dir: {{ rtl ? 'rtl' : 'ltr' }}
</G3Button>
</div>
<section id="s-basic">
<h2>基础:Button / Tag / Spin / Segmented / Space</h2>
<div class="row">
<G3Button data-testid="btn-primary">主要</G3Button>
<G3Button type="secondary">次要</G3Button>
<G3Button type="outline">描边</G3Button>
<G3Button type="ghost">幽灵</G3Button>
<G3Button type="danger">危险</G3Button>
<G3Button type="danger-outline">危险描边</G3Button>
<G3Button type="link">链接</G3Button>
<G3Button type="icon" aria-label="新增"><Plus /></G3Button>
<G3Button size="small">小</G3Button>
<G3Button size="large">大</G3Button>
<G3Button loading>加载中</G3Button>
<G3Button disabled>禁用</G3Button>
</div>
<div class="row">
<G3ButtonGroup data-testid="btn-group">
<G3Button type="outline">左</G3Button>
<G3ButtonGroupSeparator />
<G3Button type="outline">中</G3Button>
<G3ButtonGroupSeparator />
<G3Button type="outline">右</G3Button>
</G3ButtonGroup>
<!-- 无分隔线的连续按钮组:相邻边框重叠,hover 时被压住的那条必须可见 -->
<G3ButtonGroup data-testid="btn-group-plain">
<G3Button type="outline">第一</G3Button>
<G3Button type="outline">第二</G3Button>
<G3Button type="outline">第三</G3Button>
</G3ButtonGroup>
<G3ButtonGroup orientation="vertical" data-testid="btn-group-vertical">
<G3Button type="outline">上</G3Button>
<G3Button type="outline">中</G3Button>
<G3Button type="outline">下</G3Button>
</G3ButtonGroup>
<G3Tag variant="default">default</G3Tag>
<G3Tag variant="secondary">secondary</G3Tag>
<G3Tag variant="outline">outline</G3Tag>
<G3Tag variant="success">success</G3Tag>
<G3Tag variant="danger">danger</G3Tag>
<G3Tag variant="ghost">ghost</G3Tag>
<G3Tag variant="success" closable data-testid="tag-closable">可关闭</G3Tag>
<G3Spin data-testid="spin" />
</div>
<div class="row">
<G3Segmented v-model="seg" :options="['日', '周', '月']" data-testid="segmented" />
<G3Space :size="8"><G3Tag>间距</G3Tag><G3Tag>测试</G3Tag></G3Space>
</div>
</section>
<section id="s-input">
<h2>Input(含 textarea 与拖拽手柄)</h2>
<div class="row">
<G3Input v-model="model.name" data-testid="input-basic" placeholder="默认" style="width: 180px" />
<G3Input v-model="model.name" type="password" data-testid="input-password" style="width: 180px" />
<G3Input v-model="model.level" type="number" data-testid="input-number" style="width: 120px" />
<G3Input v-model="model.name" clearable data-testid="input-clear" style="width: 180px" />
<G3Input v-model="model.name" show-count :maxlength="20" style="width: 180px" />
<G3Input v-model="model.name" status="error" style="width: 160px" />
<G3Input v-model="model.name" disabled style="width: 160px" />
<G3Input v-model="model.name" placeholder="带前缀">
<template #prefix><Search /></template>
</G3Input>
</div>
<div class="row">
<G3Input v-model="model.intro" type="textarea" :rows="3" data-testid="textarea-basic" style="width: 300px" />
<G3Input
v-model="model.intro"
type="textarea"
:rows="3"
show-count
:maxlength="100"
data-testid="textarea-count"
style="width: 300px"
/>
</div>
</section>
<section id="s-color-picker">
<h2>ColorPicker</h2>
<div class="row">
<!-- 注意:不给 G3ColorPicker 挂 data-testid —— 它的根是 G3Popover(多根:
触发器 + Teleport),多根组件不做 attr 透传,testid 会被 Vue 丢弃。
E2E 用 .g3-color-picker 类名定位。 -->
<G3ColorPicker v-model="color" />
<G3ColorPicker v-model="color" :presets="false" />
<G3ColorPicker v-model="color" size="small" />
<G3ColorPicker v-model="color" disabled />
</div>
</section>
<section id="s-form">
<h2>Form / Select / Date / Switch / Radio / Checkbox</h2>
<G3Form :model="model" :label-width="90" data-testid="form">
<G3FormItem name="name" label="名称"><G3Input v-model="model.name" /></G3FormItem>
<G3FormItem name="role" label="角色">
<G3Select v-model="model.role" :options="roleOptions" data-testid="select-basic" placeholder="请选择" />
</G3FormItem>
<G3FormItem name="city" label="城市">
<G3Select
v-model="model.city"
:options="roleOptions"
multiple
data-testid="select-multiple"
placeholder="多选"
/>
</G3FormItem>
<G3FormItem name="date" label="日期时间">
<G3DatePicker v-model="model.date" format="YYYY-MM-DD HH:mm:ss" data-testid="date-picker" />
</G3FormItem>
<G3FormItem name="range" label="日期区间">
<G3RangePicker v-model="model.range" format="YYYY-MM-DD" data-testid="range-picker" />
</G3FormItem>
<G3FormItem name="intro" label="简介">
<G3Input v-model="model.intro" type="textarea" :rows="3" />
</G3FormItem>
<G3FormItem name="agree" label="开关"><G3Switch v-model="model.agree" data-testid="switch" /></G3FormItem>
<G3FormItem name="gender" label="单选">
<G3RadioGroup v-model="model.gender">
<G3Radio value="male">男</G3Radio>
<G3Radio value="female">女</G3Radio>
</G3RadioGroup>
</G3FormItem>
<G3FormItem name="level" label="复选">
<G3Checkbox :model-value="true">已选中</G3Checkbox>
<G3Checkbox :model-value="false">未选中</G3Checkbox>
<G3Checkbox :model-value="false" disabled>禁用</G3Checkbox>
</G3FormItem>
</G3Form>
<G3FormGroup title="分组(可折叠)" style="margin-top: 12px" data-testid="form-group">
<G3FormItem name="name" label="组内字段"><G3Input v-model="model.name" /></G3FormItem>
</G3FormGroup>
</section>
<section id="s-captcha">
<h2>SliderCaptcha 滑块验证码</h2>
<div class="row" style="align-items: flex-start">
<div style="width: 320px">
<G3SliderCaptcha v-model="captchaPassed" data-testid="slider-captcha" />
<div data-testid="slider-captcha-state" style="margin-top: 8px; font-size: 12px">
{{ captchaPassed ? 'passed' : 'idle' }}
</div>
</div>
<div style="width: 320px">
<G3SliderCaptcha v-model="captchaDisabled" disabled data-testid="slider-captcha-disabled" />
</div>
</div>
</section>
<section id="s-data">
<h2>Tabs / Tree / Pagination / Splitter / Scrollbar / TimeSelect</h2>
<div class="row" style="align-items: flex-start">
<div style="width: 380px">
<!-- data-testid 挂在包裹层:G3Tabs / G3Tree 是多根或 Teleport 根节点,
直接挂在组件上会触发 Vue 的 attrs 继承警告(E2E 仍可用它定位) -->
<div data-testid="tabs">
<G3Tabs v-model="tab">
<G3TabPane tab-key="a" label="标签一">标签一的内容</G3TabPane>
<G3TabPane tab-key="b" label="标签二">标签二的内容</G3TabPane>
<G3TabPane tab-key="c" label="禁用" disabled>不可见</G3TabPane>
</G3Tabs>
</div>
<G3Tabs v-model="tab" placement="left" type="line">
<G3TabPane tab-key="a" label="左侧一">左侧标签页内容</G3TabPane>
<G3TabPane tab-key="b" label="左侧二">第二个面板</G3TabPane>
</G3Tabs>
<G3Tabs v-model="tab" placement="right" type="line">
<G3TabPane tab-key="a" label="右侧一">右侧标签页内容</G3TabPane>
<G3TabPane tab-key="b" label="右侧二">第二个面板</G3TabPane>
</G3Tabs>
</div>
<div style="width: 280px">
<div data-testid="tree">
<G3Tree :tree-data="treeData" checkable default-expand-all block-node />
</div>
<!-- 右键菜单:两个节点都是叶子,验证叶子保留(隐藏的)箭头占位列 -->
<G3Tree :tree-data="flatTreeData" :context-menu="treeMenuItems" />
<!-- 拖拽排序:长按 400ms 才武装,避免手抖 -->
<G3Tree :tree-data="treeData" draggable default-expand-all />
</div>
<div style="width: 240px">
<G3TimeSelect v-model="time" data-testid="time-select" />
</div>
</div>
<div style="margin-top: 12px">
<G3Pagination :total="126" show-total show-size-changer show-quick-jumper data-testid="pagination" />
</div>
<div style="margin-top: 12px; height: 120px; display: flex; gap: 12px">
<G3Splitter style="height: 120px; flex: 1" data-testid="splitter">
<G3SplitterPanel :default-size="'40%'"><div class="box">左</div></G3SplitterPanel>
<G3SplitterPanel><div class="box">右</div></G3SplitterPanel>
</G3Splitter>
<!-- auto-hide=never:滚动条常驻,E2E 才能稳定点到轨道 -->
<G3Scrollbar
style="height: 120px; width: 220px; border: 1px solid #e5e7eb"
auto-hide="never"
data-testid="scrollbar"
>
<pre style="margin: 0; padding: 8px">{{ longText }}</pre>
</G3Scrollbar>
</div>
</section>
<section id="s-overlay">
<h2>Dropdown / Tooltip / Popover(首两个挂载即展开,用于层级回归)</h2>
<div class="row">
<G3Dropdown data-testid="dropdown">
<template #trigger><G3Button type="outline">下拉菜单</G3Button></template>
<G3DropdownItem item-key="edit">编辑</G3DropdownItem>
<G3DropdownItem item-key="copy">复制</G3DropdownItem>
<G3DropdownItem divider />
<G3DropdownItem item-key="del" danger>删除</G3DropdownItem>
</G3Dropdown>
<G3Tooltip content="这是一段提示文案">
<G3Button type="outline" data-testid="tooltip-trigger">悬停提示</G3Button>
</G3Tooltip>
<G3Popover v-model:open="popoverAOpen" trigger="manual" placement="bottom-start" data-testid="popover-a">
<template #trigger><G3Button type="outline">初始展开 A</G3Button></template>
<template #content><div style="width: 120px">浮层 A</div></template>
</G3Popover>
<G3Popover v-model:open="popoverBOpen" trigger="manual" placement="bottom-start" data-testid="popover-b">
<template #trigger><G3Button type="outline">初始展开 B</G3Button></template>
<template #content><div style="width: 120px">浮层 B</div></template>
</G3Popover>
</div>
</section>
<section id="s-richtext">
<h2>RichText 富文本编辑器</h2>
<G3RichText v-model="richHtml" data-testid="rich-text" placeholder="请输入正文…" />
</section>
<section id="s-feedback">
<h2>Modal / Drawer / Message / Notification</h2>
<div class="row">
<G3Button data-testid="open-modal" @click="modalOpen = true">打开 Modal</G3Button>
<G3Button data-testid="open-drawer" @click="openDrawer('right')">右侧 Drawer</G3Button>
<G3Button data-testid="open-drawer-left" @click="openDrawer('left')">左侧 Drawer</G3Button>
<G3Button data-testid="open-drawer-top" @click="openDrawer('top')">上方 Drawer</G3Button>
<G3Button data-testid="open-drawer-bottom" @click="openDrawer('bottom')">下方 Drawer</G3Button>
<G3Button data-testid="open-confirm" @click="openCommandConfirm">Modal.confirm</G3Button>
<G3Button data-testid="toast-default" @click="toast()">Message 默认</G3Button>
<G3Button data-testid="toast-top-left" @click="toast('top-left')">Message 左上</G3Button>
<G3Button data-testid="toast-bottom-right" @click="toast('bottom-right')">Message 右下</G3Button>
<G3Button data-testid="notify" @click="notify">Notification</G3Button>
</div>
<G3Modal v-model:open="modalOpen" title="对话框标题" data-testid="modal">
<p>第一段内容。</p>
<G3Select v-model="model.role" :options="roleOptions" data-testid="modal-select" placeholder="弹窗内选择器" />
<p v-for="i in 20" :key="i">第 {{ i }} 段内容,用于验证缩放时内容区是否内部滚动。</p>
</G3Modal>
<G3Drawer v-model:open="drawerOpen" :placement="drawerPlacement" title="抽屉标题" data-testid="drawer">
<p>抽屉内容</p>
<G3Select v-model="model.role" :options="roleOptions" data-testid="drawer-select" placeholder="抽屉内选择" />
</G3Drawer>
</section>
<section id="s-table">
<h2>Table 表格</h2>
<!-- 小数据量(3 行):行数 ≤ 100,纵向虚拟滚动应当关闭 -->
<div data-testid="table-wrap" style="height: 320px">
<G3Table
data-testid="table"
v-model:columns="tableColumns"
:data-source="tableData"
:row-selection="{ selectedRowKeys: tableSelectedKeys, onChange: (keys) => (tableSelectedKeys = keys) }"
/>
</div>
<!-- 大数据量(5000 行):纵向虚拟滚动应当开启;容器限高以产生滚动。
顺带开斑马纹:纵向虚拟时库改取 nth-child(odd)(DOM 首行≈偶数数据行),
冻结列的条纹叠色必须跟着同一套 nth-child 走(见 _overrides.scss 的 stripe 段)。 -->
<div data-testid="table-big-wrap" style="height: 360px; margin-top: 16px">
<G3Table
data-testid="table-big"
:columns="bigTableColumns"
:data-source="bigTableData"
:seq="false"
:row-height="30"
stripe
/>
</div>
<!-- 窄容器:验证列宽铺满与省略号 -->
<div data-testid="table-narrow-wrap" style="width: 480px; height: 240px; margin-top: 16px">
<G3Table
data-testid="table-narrow"
:columns="tableColumns"
:data-source="tableData"
:seq="false"
/>
</div>
<!-- 固定列 + 校验态 + 受控当前行:几何与「点格进编辑要高亮」的回归宿主 -->
<div data-testid="table-fixed-wrap" style="height: 240px; margin-top: 16px">
<G3Table
data-testid="table-fixed"
:columns="fixedTableColumns"
:data-source="fixedTableData"
:active-row-key="fixedActiveKey"
fixed-col-shadow
@row-click="(_, row) => (fixedActiveKey = String(row.id))"
/>
</div>
<!-- 纯展示表(列上都没有 editor):不该铺「不可改」灰底 -->
<div data-testid="table-plain-wrap" style="height: 200px; margin-top: 16px">
<G3Table data-testid="table-plain" :columns="plainColumns" :data-source="plainData" />
</div>
<!-- 关闭编辑(editable: false),但列上声明了 editor:同样不该铺灰底 -->
<div data-testid="table-noedit-wrap" style="height: 200px; margin-top: 16px">
<G3Table
data-testid="table-noedit"
:columns="tableColumns"
:data-source="tableData"
:editable="false"
/>
</div>
<!-- 斑马纹(stripe):固定列(序号列)也要跟着上条纹。
用纯展示列(没有 editor)当宿主,避免「不可改灰底」盖住条纹干扰断言。 -->
<div data-testid="table-stripe-wrap" style="height: 200px; margin-top: 16px">
<G3Table data-testid="table-stripe" :columns="plainColumns" :data-source="plainData" stripe />
</div>
</section>
</div>
`,
}
createApp(App).mount('#app')