Files
workspace/code/g3soft-libs/packages/ui/playground/main.ts
T
2026-10-16 17:30:17 +08:00

743 lines
29 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 type { G3TableColumn, TableRow } 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: 'input', inputType: 'number' },
},
{
dataIndex: 'status',
title: '状态',
width: 110,
editor: {
type: 'select',
options: [
{ label: '启用', value: 1 },
{ label: '停用', value: 0 },
],
// 按行禁用:enabled=0 的那一行状态不许改(fms 的 editor.disabled 语义)
disabled: (row: { enabled?: number }) => row.enabled === 0,
},
},
{ dataIndex: 'enabled', title: '启用', width: 80, editor: { type: 'checkbox' } },
{
dataIndex: 'remark',
title: '备注',
editor: { type: 'input' },
},
{ 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: 'input' } },
{ dataIndex: 'value', title: '数值', width: 140, align: 'right', editor: { type: 'input', inputType: 'number' } },
]
const bigTableData = Array.from({ length: 5000 }, (_, i) => ({
id: i + 1,
name: `记录 ${i + 1}`,
value: (i + 1) * 7,
}))
/**
* 纯展示表(列上**都没有** `editor`)。
*
* ⚠️ fms-vue 的 fms-table 没有「不可改灰底」这套视觉(那是旧版 G3Table 的自有能力),
* 所以本表与其它表的差别只剩「没有编辑器」。
*/
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:固定列 + 受控当前行 ---- */
/**
* 固定列的宿主:列宽总和顶出视口,制造必须横向滚动的场景。
*
* 配置只有列上的 `fixed`,但有三条硬约束:
* · `fixed: 'left'` 的列排在最前面、`'right'` 的排在最后面;
* · 固定列必须写 `width`(左固定列的吸附位移由「它左侧固定列宽度累加」算出);
* · 总列宽要超过容器宽,否则固定与不固定看不出区别。
*/
const fixedTableColumns = [
{ dataIndex: 'code', title: '编号(左固定)', width: 200, fixed: 'left' },
{
dataIndex: 'name',
title: '名称',
width: 200,
fixed: 'left',
editor: { type: 'input' },
},
// 填充列:把总宽顶出视口,否则固定列与普通列长得一模一样
...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) },
{ 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)
/**
* 日期列(**只读展示**)。
*
* ⚠️ fms-vue 的 fms-table 没有 date 编辑器,所以本组件也没有 —— `editor.type`
* 只有 input / select / checkbox。这里保留两列纯粹是让「含日期字符串的列」
* 在真实密度下正常渲染,不再挂编辑器。
*/
const dateTableColumns = ref<G3TableColumn<TableRow>[]>([
{ dataIndex: 'bookingNo', title: '订舱号', width: 120 },
{ dataIndex: 'etd', title: '开航日', width: 150 },
{ dataIndex: 'docClose', title: '截单日', width: 180 },
])
const dateTableData = ref<TableRow[]>([
{ id: 1, bookingNo: 'BK001', etd: '2026-03-15', docClose: '2026-03-12 18:00' },
// 空值行:既验 placeholder,也是「选完日期写回哪一格」的落点
{ id: 2, bookingNo: 'BK002', etd: '', docClose: '' },
{ id: 3, bookingNo: 'BK003', etd: '2026-04-01', docClose: '2026-03-28 12:30' },
])
/**
* 键盘行为的宿主:三列都是**单行**编辑器。
*
* 表格编辑器没有 `textarea`(见 `src/table/types.ts` 里 `TableEditor.type` 的说明:
* 行高放不下多行正文),所以这里不存在「多行 Enter 换行」那种分支 ——
* Enter 一律是「确认 + 下移」,上下键一律归表格导航。
*/
const keysTableColumns = ref<G3TableColumn<TableRow>[]>([
{ dataIndex: 'code', title: '编号', width: 130, editor: { type: 'input' } },
{ dataIndex: 'qty', title: '数量', width: 110, align: 'right', editor: { type: 'input', inputType: 'number' } },
{ dataIndex: 'note', title: '备注', width: 260, editor: { type: 'input' } },
])
const keysTableData = ref<TableRow[]>([
{ id: 1, code: 'K-001', qty: 12, note: '备注一' },
{ id: 2, code: 'K-002', qty: 34, note: '' },
{ id: 3, code: 'K-003', qty: 56, note: '' },
])
return {
rtl,
toggleRtl,
tableColumns,
tableData,
tableSelectedKeys,
bigTableColumns,
bigTableData,
plainColumns,
plainData,
fixedTableColumns,
fixedTableData,
fixedActiveKey,
dateTableColumns,
dateTableData,
keysTableColumns,
keysTableData,
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 行):纵向虚拟滚动应当开启;容器限高以产生滚动。 -->
<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"
/>
</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"
@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>
<!-- 日期列(只读展示):fms 没有 date 编辑器,见 dateTableColumns 的说明。 -->
<div data-testid="table-date-wrap" style="height: 220px; margin-top: 16px">
<G3Table
data-testid="table-date"
:columns="dateTableColumns"
:data-source="dateTableData"
:seq="false"
/>
</div>
<!-- 键盘行为的宿主:Enter 下移 / 左右键到边界切格 / 上下键切行。
同样单独一张表(理由同上)。三列都是单行编辑器 —— 表格没有 textarea。 -->
<div data-testid="table-keys-wrap" style="height: 260px; margin-top: 16px">
<G3Table
data-testid="table-keys"
:columns="keysTableColumns"
:data-source="keysTableData"
:seq="false"
/>
</div>
</section>
</div>
`,
}
createApp(App).mount('#app')