iter(g07): 新增 timeline 时间线组件
This commit is contained in:
@@ -29,6 +29,7 @@ export { stepper } from './stepper.js';
|
||||
export { rate } from './rate.js';
|
||||
export { empty } from './empty.js';
|
||||
export { divider } from './divider.js';
|
||||
export { timeline } from './timeline.js';
|
||||
export { modal } from './modal.js';
|
||||
export { drawer } from './drawer.js';
|
||||
export { form, formItem } from './form.js';
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { h } from '@ui-arch/frame';
|
||||
import type { TimelineItem, TimelineProps } from './types.js';
|
||||
|
||||
/*** 单个时间线节点:绝对定位的语义色圆点 + 标题 / 时间 / 正文,pending 节点为虚线空心圈 ***/
|
||||
function node(item: TimelineItem, pending: boolean): HTMLElement {
|
||||
const dot = h('span', { class: `ua-tl-dot ua-tl-dot-${pending ? 'pending' : item.variant ?? 'default'}` });
|
||||
const head = h('div', { class: 'ua-tl-head' },
|
||||
h('span', { class: 'ua-tl-title' }, item.title),
|
||||
item.time ? h('span', { class: 'ua-tl-time' }, item.time) : null);
|
||||
const body = item.content ? h('p', { class: 'ua-tl-content' }, item.content) : null;
|
||||
return h('li', { class: `ua-tl-item${pending ? ' is-pending' : ''}` }, dot, h('div', { class: 'ua-tl-body' }, head, body));
|
||||
}
|
||||
|
||||
/*** 时间线:纵向事件流,mode 决定轴线在左还是在右;pending 为真时末尾补一个进行中节点 ***/
|
||||
export function timeline(props: TimelineProps): HTMLElement {
|
||||
const rows: TimelineItem[] = [...props.items]; // 复制入参,避免改动调用方数组
|
||||
if (props.pending) rows.push({ title: '进行中…' }); // 尾部虚节点表示流程尚未结束
|
||||
const items = rows.map((row, i) => node(row, !!props.pending && i === rows.length - 1));
|
||||
return h('ol', { class: `ua-tl ua-tl-${props.mode ?? 'left'}` }, ...items);
|
||||
}
|
||||
@@ -348,4 +348,19 @@ export interface FormProps {
|
||||
onReset?: () => void; // 重置回调
|
||||
}
|
||||
|
||||
/*** 时间线条目 ***/
|
||||
export interface TimelineItem {
|
||||
title: string; // 条目标题(事件名)
|
||||
content?: string; // 条目正文补充;缺省不渲染
|
||||
time?: string; // 时间文案(如 '09:30' / '10-23');缺省不渲染
|
||||
variant?: 'default' | 'success' | 'warning' | 'danger'; // 节点语义色;缺省 default
|
||||
}
|
||||
|
||||
/*** 时间线入参 ***/
|
||||
export interface TimelineProps {
|
||||
items: TimelineItem[]; // 条目列表,按时间顺序自上而下
|
||||
mode?: 'left' | 'right'; // 轴线与节点所在侧;缺省 left(内容在右)
|
||||
pending?: boolean; // 末尾追加「进行中」虚线节点,表示流程未结束;缺省 false
|
||||
}
|
||||
|
||||
export type { ColumnDef, RowKey }; // 再导出常用内核类型,方便调用方
|
||||
|
||||
@@ -47,7 +47,8 @@
|
||||
"form(props, items):表单容器,布局 + 聚合校验,全部通过才触发 onSubmit",
|
||||
"formItem(props, control):表单字段,label + 必填红星 + 规则校验 + 错误区,返回 { el, validate } 句柄",
|
||||
"dataTable(props):数据表格,粘性表头 / 行选择(全选三态)/ 排序 / 筛选 / 分页 / 空状态;loading 为真时数据行换骨架行且筛选/翻页禁用,内核为 table-core",
|
||||
"image(props):图片,缩略图点击 / 回车开遮罩大图预览(Esc 或点击遮罩关闭),width 控制缩略宽"
|
||||
"image(props):图片,缩略图点击 / 回车开遮罩大图预览(Esc 或点击遮罩关闭),width 控制缩略宽",
|
||||
"timeline(props):时间线,items 按序渲染纵向事件流(标题 / 时间 / 正文 + 语义色节点),mode 控制轴线左右,pending 追加进行中节点"
|
||||
],
|
||||
"keyTypes": [
|
||||
"ButtonProps",
|
||||
@@ -84,7 +85,9 @@
|
||||
"SliderProps",
|
||||
"TagProps",
|
||||
"DrawerProps",
|
||||
"AvatarProps"
|
||||
"AvatarProps",
|
||||
"TimelineItem",
|
||||
"TimelineProps"
|
||||
]
|
||||
},
|
||||
"usage": "const checked = signal(false); checkbox({ label: '同意', checked }); const open = signal(false); modal({ title: '确认', open }, '内容')",
|
||||
@@ -1450,6 +1453,40 @@
|
||||
"需要独立分页器联动时传 page 信号:const page = signal(1),dataTable({ page }) 与 pagination({ page }) 共用",
|
||||
"需要批量操作时传 selected 信号:内部勾选实时写回,外部置空或赋值即同步整表(跨页保留,按 rowKey 存)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "timeline",
|
||||
"title": "时间线 Timeline",
|
||||
"demo": "sec-timeline",
|
||||
"props": [
|
||||
{
|
||||
"n": "items",
|
||||
"t": "TimelineItem[]",
|
||||
"r": 1,
|
||||
"d": "条目列表,按时间顺序自上而下;每项含 title(必填)、content、time、variant"
|
||||
},
|
||||
{
|
||||
"n": "mode",
|
||||
"t": "'left' | 'right'",
|
||||
"d": "轴线与节点所在侧;缺省 left(内容在右),right 时内容左对齐轴线居右",
|
||||
"df": "left"
|
||||
},
|
||||
{
|
||||
"n": "pending",
|
||||
"t": "boolean",
|
||||
"d": "末尾追加虚线「进行中…」节点,表示流程尚未结束;缺省 false",
|
||||
"df": "false"
|
||||
}
|
||||
],
|
||||
"example": "timeline({ items: [{ title: '提交审批', time: '09:12', variant: 'success' }, { title: '财务复核', time: '10:30', content: '等待张工处理' }], pending: true })",
|
||||
"steps": [
|
||||
"事件流按时间顺序排(组件不做排序):较早的条目放前面,最新的放最后",
|
||||
"variant 只承载语义:success 完成、warning 待处理、danger 失败、default 普通记录(节点色走 --ua-* 令牌,随暗色主题变化)",
|
||||
"time 是纯展示文案,格式自由('09:12' / '10-23 18:00'),组件不做日期解析",
|
||||
"流程未结束时传 pending: true,末尾自动补一个虚线脉冲节点,完成后置回 false 即消失",
|
||||
"需要轴线居右(如右侧栏时间流)时传 mode: 'right',条目与节点整体镜像",
|
||||
"content 为纯文本;需要按钮等交互内容时改用 accordion / card 组合"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1647,3 +1647,110 @@ button.ua-step-dot:hover {
|
||||
font-size: var(--ua-font-size-md);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* —— 时间线:纵向轴线 + 语义色节点,mode=right 时轴线居右 —— */
|
||||
.ua-tl {
|
||||
position: relative;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
.ua-tl::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 5px;
|
||||
top: 6px;
|
||||
bottom: 0;
|
||||
width: 1px;
|
||||
background: var(--ua-color-border);
|
||||
}
|
||||
.ua-tl-right::before {
|
||||
left: auto;
|
||||
right: 5px;
|
||||
}
|
||||
.ua-tl-item {
|
||||
position: relative;
|
||||
padding: 0 0 var(--ua-space-3) 22px;
|
||||
animation: ua-tl-in 0.18s ease-out both;
|
||||
}
|
||||
.ua-tl-item:last-child {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
.ua-tl-right .ua-tl-item {
|
||||
padding: 0 22px var(--ua-space-3) 0;
|
||||
text-align: right;
|
||||
}
|
||||
.ua-tl-right .ua-tl-item:last-child {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
.ua-tl-dot {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 4px;
|
||||
width: 11px;
|
||||
height: 11px;
|
||||
border-radius: 50%;
|
||||
background: var(--ua-color-text-muted);
|
||||
box-shadow: 0 0 0 3px color-mix(in srgb, var(--ua-color-accent) 10%, transparent);
|
||||
transition: transform 0.15s;
|
||||
}
|
||||
.ua-tl-right .ua-tl-dot {
|
||||
left: auto;
|
||||
right: 0;
|
||||
}
|
||||
.ua-tl-item:hover .ua-tl-dot {
|
||||
transform: scale(1.15);
|
||||
}
|
||||
.ua-tl-dot-default { background: var(--ua-color-accent); }
|
||||
.ua-tl-dot-success { background: var(--ua-color-success); }
|
||||
.ua-tl-dot-warning { background: var(--ua-color-warning); }
|
||||
.ua-tl-dot-danger { background: var(--ua-color-danger); }
|
||||
.ua-tl-dot-pending {
|
||||
background: var(--ua-color-bg);
|
||||
border: 1px dashed var(--ua-color-accent);
|
||||
animation: ua-tl-pulse 1.6s ease-in-out infinite;
|
||||
}
|
||||
.ua-tl-head {
|
||||
display: flex;
|
||||
gap: var(--ua-space-2);
|
||||
align-items: baseline;
|
||||
}
|
||||
.ua-tl-right .ua-tl-head {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.ua-tl-title {
|
||||
color: var(--ua-color-text);
|
||||
font-size: var(--ua-font-size-md);
|
||||
font-weight: 600;
|
||||
}
|
||||
.ua-tl-item.is-pending .ua-tl-title {
|
||||
color: var(--ua-color-text-muted);
|
||||
font-weight: 500;
|
||||
}
|
||||
.ua-tl-time {
|
||||
color: var(--ua-color-text-muted);
|
||||
font-size: var(--ua-font-size-sm);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.ua-tl-content {
|
||||
margin: 2px 0 0;
|
||||
color: var(--ua-color-text-muted);
|
||||
font-size: var(--ua-font-size-sm);
|
||||
line-height: 1.6;
|
||||
}
|
||||
@keyframes ua-tl-in {
|
||||
from { opacity: 0; transform: translateY(-4px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
@keyframes ua-tl-pulse {
|
||||
0%, 100% { box-shadow: 0 0 0 2px color-mix(in srgb, var(--ua-color-accent) 18%, transparent); }
|
||||
50% { box-shadow: 0 0 0 5px color-mix(in srgb, var(--ua-color-accent) 4%, transparent); }
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.ua-tl-item,
|
||||
.ua-tl-dot-pending,
|
||||
.ua-tl-dot {
|
||||
animation: none !important;
|
||||
transition: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user