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

533 lines
20 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 {
G3Attachment,
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,
G3TabPane,
G3Tabs,
G3Tag,
G3TimeSelect,
G3Tooltip,
G3Tree,
} from '@g3soft/ui'
import type { AttachmentCategory, AttachmentItem } 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: {
G3Attachment,
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,
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)),
})
}
/* ---- Attachment 附件管理器 ---- */
const attachmentFiles = ref<AttachmentItem[]>([
{ id: 1, name: '2026年度框架合同.pdf', size: 512_000, categoryId: 'contract' },
{ id: 2, name: '供应商报价明细.xlsx', size: 86_000, categoryId: 'invoice' },
{ id: 3, name: '现场照片.jpg', size: 1_240_000, categoryId: 'photo' },
{ id: 4, name: '验收纪要.docx', size: 24_000, categoryId: 'contract' },
{ id: 5, name: '进度汇报.pptx', size: 640_000, categoryId: 'photo' },
{ id: 6, name: '现场记录.zip', size: 8_192_000, categoryId: 'invoice' },
])
const attachmentCategories: AttachmentCategory[] = [
{ id: 'contract', name: '合同文件' },
{ id: 'invoice', name: '发票凭证' },
{ id: 'photo', name: '现场照片' },
]
return {
attachmentFiles,
attachmentCategories,
rtl,
toggleRtl,
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-attachment">
<h2>Attachment 附件管理器</h2>
<!-- 根节点是单元素 div,data-testid 直接落在组件上即可 -->
<G3Attachment
data-testid="attachment"
:files="attachmentFiles"
:categories="attachmentCategories"
:height="420"
/>
<!-- 窄实例:分类栏 200 + 分割条 10 之后列表只剩 ~210px,
压到一行的极限(工具栏必须裁切 / 省略,而不是长高)-->
<div style="width: 420px; margin-top: 12px">
<G3Attachment
data-testid="attachment-narrow"
:files="attachmentFiles"
:categories="attachmentCategories"
:height="360"
/>
</div>
</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>
</div>
`,
}
createApp(App).mount('#app')