diff --git a/Readme.md b/Readme.md
index 47e6795..5511ccf 100644
--- a/Readme.md
+++ b/Readme.md
@@ -13,7 +13,7 @@
- @bd/core 针对`web components`的核心封装库, 以数据驱动, 可以更方便的开发 wc 组件
-### 开发进度 && 计划 (30/54)
+### 开发进度 && 计划 (31/54)
- [x] `wc-card` 卡片组件
- [x] `wc-space` 间隔组件
@@ -59,7 +59,7 @@
- [ ] `wc-loading` 加载组件
- [x] `wc-tabs` 选项卡组件
- [x] `wc-steps` 步骤条组件
-- [ ] `wc-timeline` 时间线组件
+- [x] `wc-timeline` 时间线组件
- [ ] `wc-layout` 布局组件
- [ ] `wc-tag` 标签组件
- [ ] `wc-tooltip` 文字提示组件
diff --git a/src/card/index.js b/src/card/index.js
index 67afc03..725057f 100644
--- a/src/card/index.js
+++ b/src/card/index.js
@@ -61,7 +61,7 @@ class Card extends Component {
render() {
return html`
`
diff --git a/src/timeline/index.js b/src/timeline/index.js
new file mode 100644
index 0000000..75c1de7
--- /dev/null
+++ b/src/timeline/index.js
@@ -0,0 +1,147 @@
+/**
+ * {}
+ * @author yutent
+ * @date 2023/04/18 09:38:01
+ */
+
+import { css, html, Component, bind, styleMap, classMap } from '@bd/core'
+import '../icon/index.js'
+
+function pad(n) {
+ return n < 10 ? '0' + n : n
+}
+
+function getTimeStamp(t) {
+ let y = t.getFullYear()
+ let m = pad(t.getMonth() + 1)
+ let d = pad(t.getDate())
+ let h = pad(t.getHours())
+ let i = pad(t.getMinutes())
+ return `${y}/${m}/${d} ${h}:${i}`
+}
+
+const DEFAULT_TIME = getTimeStamp(new Date())
+
+class Timeline extends Component {
+ static styles = [
+ css`
+ :host {
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ }
+ `
+ ]
+ created() {
+ bind(this.root, 'slotchange', ev => {
+ let children = ev.target.assignedNodes()
+
+ children.forEach((it, i) => {
+ it.removeAttribute('last')
+ })
+
+ children.at(-1).setAttribute('last', '')
+ })
+ }
+}
+
+class Item extends Component {
+ static props = {
+ time: {
+ type: String,
+ default: DEFAULT_TIME,
+ observer(v) {
+ if (isFinite(v)) {
+ this.time = getTimeStamp(new Date(+v))
+ }
+ }
+ },
+ content: '',
+ icon: { type: String, default: null },
+ color: ''
+ }
+
+ static styles = css`
+ :host {
+ flex: 1;
+ display: flex;
+ font-size: 14px;
+ }
+ .container {
+ display: flex;
+ width: 100%;
+ margin-bottom: 32px;
+ color: var(--color-dark-1);
+ }
+
+ .header {
+ position: relative;
+ display: flex;
+ justify-content: center;
+ width: 26px;
+ height: 100%;
+ min-height: 64px;
+ user-select: none;
+
+ &::after {
+ position: absolute;
+ left: 12px;
+ top: 20px;
+ width: 2px;
+ height: calc(100% + 12px); // -20 + 32
+ background: var(--color-grey-1);
+ content: '';
+ }
+ }
+ :host([last]) {
+ .header:after {
+ display: none;
+ }
+ }
+
+ .dot {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 20px;
+ height: 20px;
+ border-radius: 50%;
+ background: var(--color-grey-1);
+ color: #fff;
+ --size: 14px;
+ }
+ .group {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ margin-left: 12px;
+ }
+ .time {
+ margin-bottom: 4px;
+ line-height: 1.5;
+ font-size: 12px;
+ color: var(--color-grey-3);
+ user-select: none;
+ font-family: Raleway, Arial, Helvetica, sans-serif;
+ }
+ `
+
+ render() {
+ return html`
+
+ `
+ }
+}
+
+Timeline.reg('timeline')
+Item.reg('timeline-item')