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('
标题
这是加粗、斜体、上标、下标的正文。
')
// RTL 开关:直接翻 。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((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(null)
/**
* 日期列(**只读展示**)。
*
* ⚠️ fms-vue 的 fms-table 没有 date 编辑器,所以本组件也没有 —— `editor.type`
* 只有 input / select / checkbox。这里保留两列纯粹是让「含日期字符串的列」
* 在真实密度下正常渲染,不再挂编辑器。
*/
const dateTableColumns = ref[]>([
{ dataIndex: 'bookingNo', title: '订舱号', width: 120 },
{ dataIndex: 'etd', title: '开航日', width: 150 },
{ dataIndex: 'docClose', title: '截单日', width: 180 },
])
const dateTableData = ref([
{ 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[]>([
{ 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([
{ 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: `
dir: {{ rtl ? 'rtl' : 'ltr' }}
基础:Button / Tag / Spin / Segmented / Space
主要
次要
描边
幽灵
危险
危险描边
链接
小
大
加载中
禁用
左
中
右
第一
第二
第三
上
中
下
default
secondary
outline
success
danger
ghost
可关闭
间距测试
SliderCaptcha 滑块验证码
{{ captchaPassed ? 'passed' : 'idle' }}
Tabs / Tree / Pagination / Splitter / Scrollbar / TimeSelect
标签一的内容
标签二的内容
不可见
左侧标签页内容
第二个面板
右侧标签页内容
第二个面板
Dropdown / Tooltip / Popover(首两个挂载即展开,用于层级回归)
下拉菜单
编辑
复制
删除
悬停提示
初始展开 A
浮层 A
初始展开 B
浮层 B
Modal / Drawer / Message / Notification
打开 Modal
右侧 Drawer
左侧 Drawer
上方 Drawer
下方 Drawer
Modal.confirm
Message 默认
Message 左上
Message 右下
Notification
第一段内容。
第 {{ i }} 段内容,用于验证缩放时内容区是否内部滚动。
抽屉内容
Table 表格
(fixedActiveKey = String(row.id))"
/>
`,
}
createApp(App).mount('#app')