diff --git a/packages/ccui/ui/shared/hooks/use-namespace.ts b/packages/ccui/ui/shared/hooks/use-namespace.ts
index 33391d46..2e20e447 100644
--- a/packages/ccui/ui/shared/hooks/use-namespace.ts
+++ b/packages/ccui/ui/shared/hooks/use-namespace.ts
@@ -3,6 +3,7 @@ export interface UseNamespace {
e: (el: string) => string
m: (mo: string) => string
em: (el: string, mo: string) => string
+ is: (name: string) => string
}
function createBem(
@@ -35,10 +36,12 @@ export function useNamespace(block: string, needDot = false): UseNamespace {
modifier ? createBem(namespace, '', modifier) : ''
const em = (element: string, modifier: string) =>
element && modifier ? createBem(namespace, element, modifier) : ''
+ const is = (name: string) => `is-${name}`
return {
b,
e,
m,
em,
+ is,
}
}
diff --git a/packages/ccui/ui/timeline/index.ts b/packages/ccui/ui/timeline/index.ts
new file mode 100644
index 00000000..3b2ae153
--- /dev/null
+++ b/packages/ccui/ui/timeline/index.ts
@@ -0,0 +1,19 @@
+import type { App } from 'vue'
+import Timeline from './src/timeline'
+import TimelineItem from './src/timeline-item'
+
+Timeline.install = function (app: App) {
+ app.component(Timeline.name!, Timeline)
+ app.component(TimelineItem.name!, TimelineItem)
+}
+
+export { Timeline, TimelineItem }
+
+export default {
+ title: 'Timeline 时间线',
+ category: '数据展示',
+ status: '100%',
+ install(app: App) {
+ Timeline.install(app)
+ },
+}
diff --git a/packages/ccui/ui/timeline/src/timeline-item.tsx b/packages/ccui/ui/timeline/src/timeline-item.tsx
new file mode 100644
index 00000000..de4028f6
--- /dev/null
+++ b/packages/ccui/ui/timeline/src/timeline-item.tsx
@@ -0,0 +1,103 @@
+import type { TimelineItemProps } from './timeline-types'
+import { computed, defineComponent, h } from 'vue'
+import { useNamespace } from '../../shared/hooks/use-namespace'
+import { timelineItemProps } from './timeline-types'
+
+export default defineComponent({
+ name: 'CTimelineItem',
+ props: timelineItemProps,
+ emits: [],
+ setup(props: TimelineItemProps, { slots }) {
+ const ns = useNamespace('timeline-item')
+
+ // 计算节点的样式类名
+ const nodeClasses = computed(() => {
+ return [
+ ns.e('node'),
+ ns.em('node', props.size),
+ props.type && ns.em('node', props.type),
+ props.hollow && ns.is('hollow'),
+ ].filter(Boolean)
+ })
+
+ // 计算时间戳的样式类名
+ const timestampClasses = computed(() => {
+ return [
+ ns.e('timestamp'),
+ ns.is(props.placement),
+ ]
+ })
+
+ // 渲染图标
+ const renderIcon = () => {
+ if (props.icon) {
+ if (typeof props.icon === 'string') {
+ return
+ }
+ else {
+ // 如果是组件,使用 h 函数渲染
+ return h(props.icon, { class: ns.e('icon') })
+ }
+ }
+ return null
+ }
+
+ // 渲染节点
+ const renderNode = () => {
+ if (slots.dot) {
+ return (
+
+ {slots.dot()}
+
+ )
+ }
+
+ return (
+
+ {renderIcon()}
+
+ )
+ }
+
+ // 渲染时间戳
+ const renderTimestamp = () => {
+ if (props.hideTimestamp)
+ return null
+
+ return (
+
+ {props.timestamp}
+
+ )
+ }
+
+ return () => {
+ return (
+
+ {/* 连接线 */}
+
+
+ {/* 节点 */}
+ {renderNode()}
+
+ {/* 内容区域 */}
+
+ {/* 顶部时间戳 */}
+ {props.placement === 'top' && renderTimestamp()}
+
+ {/* 内容 */}
+
+ {slots.default && slots.default()}
+
+
+ {/* 底部时间戳 */}
+ {props.placement === 'bottom' && renderTimestamp()}
+
+
+ )
+ }
+ },
+})
diff --git a/packages/ccui/ui/timeline/src/timeline-types.ts b/packages/ccui/ui/timeline/src/timeline-types.ts
new file mode 100644
index 00000000..b170587c
--- /dev/null
+++ b/packages/ccui/ui/timeline/src/timeline-types.ts
@@ -0,0 +1,81 @@
+import type { Component, ExtractPropTypes, PropType } from 'vue'
+
+// Timeline 主组件的 props
+export const timelineProps = {
+ // 暂时没有特殊的 props,主要通过插槽传递 TimelineItem
+} as const
+
+export type TimelineProps = ExtractPropTypes
+
+export type TimelineItemType = 'primary' | 'success' | 'warning' | 'danger' | 'info' | ''
+
+// TimelineItem 组件的 props
+export const timelineItemProps = {
+ /**
+ * 时间戳内容
+ */
+ timestamp: {
+ type: String,
+ default: '',
+ },
+ /**
+ * 是否隐藏时间戳
+ */
+ hideTimestamp: {
+ type: Boolean,
+ default: false,
+ },
+ /**
+ * 是否垂直居中
+ */
+ center: {
+ type: Boolean,
+ default: false,
+ },
+ /**
+ * 时间戳位置
+ */
+ placement: {
+ type: String as PropType<'top' | 'bottom'>,
+ default: 'bottom',
+ validator: (value: string) => ['top', 'bottom'].includes(value),
+ },
+ /**
+ * 节点类型
+ */
+ type: {
+ type: String as PropType,
+ default: '',
+ validator: (value: string) => ['primary', 'success', 'warning', 'danger', 'info', ''].includes(value),
+ },
+ /**
+ * 节点颜色
+ */
+ color: {
+ type: String,
+ default: '',
+ },
+ /**
+ * 节点尺寸
+ */
+ size: {
+ type: String as PropType<'normal' | 'large'>,
+ default: 'normal',
+ validator: (value: string) => ['normal', 'large'].includes(value),
+ },
+ /**
+ * 自定义图标
+ */
+ icon: {
+ type: [String, Object] as PropType,
+ },
+ /**
+ * 是否空心点
+ */
+ hollow: {
+ type: Boolean,
+ default: false,
+ },
+} as const
+
+export type TimelineItemProps = ExtractPropTypes
diff --git a/packages/ccui/ui/timeline/src/timeline.scss b/packages/ccui/ui/timeline/src/timeline.scss
new file mode 100644
index 00000000..4f2ab091
--- /dev/null
+++ b/packages/ccui/ui/timeline/src/timeline.scss
@@ -0,0 +1,187 @@
+@use '../../style-var/index.scss' as *;
+
+.ccui-timeline {
+ margin: 0;
+ font-size: $ccui-font-size-lg;
+ list-style: none;
+ padding: 0;
+
+ // 最后一个时间线项目不显示尾部连接线
+ .ccui-timeline-item:last-child {
+ .ccui-timeline-item__tail {
+ display: none;
+ }
+ }
+
+ // 垂直居中的时间线项目
+ .ccui-timeline-item.ccui-timeline-item__center {
+ display: flex;
+ align-items: center;
+ min-height: 60px;
+
+ .ccui-timeline-item__wrapper {
+ width: 100%;
+ }
+
+ .ccui-timeline-item__tail {
+ top: 6px;
+ height: calc(100% + 14px);
+ }
+
+ .ccui-timeline-item__node {
+ top: 50%;
+ transform: translateY(-50%);
+ }
+
+ .ccui-timeline-item__dot {
+ top: 50%;
+ transform: translateY(-50%);
+ }
+ }
+
+ // 第一个垂直居中项目的特殊处理
+ .ccui-timeline-item.ccui-timeline-item__center:first-child {
+ .ccui-timeline-item__tail {
+ top: 50%;
+ height: calc(50% + 20px);
+ }
+ }
+
+ // 最后一个垂直居中项目的特殊处理
+ .ccui-timeline-item.ccui-timeline-item__center:last-child {
+ .ccui-timeline-item__tail {
+ display: block;
+ height: 50%;
+ top: 6px;
+ }
+ }
+}
+
+.ccui-timeline-item {
+ position: relative;
+ padding-bottom: 20px;
+ list-style: none;
+
+ // 确保隐藏所有列表标记
+ &::marker {
+ display: none;
+ }
+
+ &::before {
+ display: none;
+ }
+
+ // 内容包装器
+ &__wrapper {
+ position: relative;
+ padding-left: 28px;
+ top: -3px;
+ }
+
+ // 连接线
+ &__tail {
+ position: absolute;
+ left: 4px;
+ top: 0;
+ height: calc(100% + 20px);
+ border-left: 2px solid $ccui-dividing-line;
+ }
+
+ // 图标样式
+ &__icon {
+ color: $ccui-light-text;
+ font-size: $ccui-font-size-sm;
+ }
+
+ // 节点
+ &__node {
+ position: absolute;
+ background-color: $ccui-dividing-line;
+ border-color: $ccui-dividing-line;
+ border-radius: 50%;
+ box-sizing: border-box;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
+ // 普通尺寸
+ &--normal {
+ left: -1px;
+ top: 0;
+ width: 12px;
+ height: 12px;
+ }
+
+ // 大尺寸
+ &--large {
+ left: -2px;
+ top: 0;
+ width: 14px;
+ height: 14px;
+ }
+
+ // 空心样式
+ &.is-hollow {
+ background: $ccui-base-bg;
+ border-style: solid;
+ border-width: 2px;
+ }
+
+ // 不同类型的颜色
+ &--primary {
+ background-color: $ccui-primary;
+ border-color: $ccui-primary;
+ }
+
+ &--success {
+ background-color: $ccui-success;
+ border-color: $ccui-success;
+ }
+
+ &--warning {
+ background-color: $ccui-warning;
+ border-color: $ccui-warning;
+ }
+
+ &--danger {
+ background-color: $ccui-danger;
+ border-color: $ccui-danger;
+ }
+
+ &--info {
+ background-color: $ccui-info;
+ border-color: $ccui-info;
+ }
+ }
+
+ // 自定义节点
+ &__dot {
+ position: absolute;
+ top: 0;
+ left: -6px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ }
+
+ // 内容区域
+ &__content {
+ color: $ccui-text;
+ }
+
+ // 时间戳
+ &__timestamp {
+ color: $ccui-aide-text;
+ line-height: $ccui-line-height-base;
+ font-size: $ccui-font-size-sm;
+
+ &.is-top {
+ margin-bottom: 8px;
+ padding-top: 4px;
+ }
+
+ &.is-bottom {
+ margin-top: 8px;
+ }
+ }
+}
diff --git a/packages/ccui/ui/timeline/src/timeline.tsx b/packages/ccui/ui/timeline/src/timeline.tsx
new file mode 100644
index 00000000..37eb969b
--- /dev/null
+++ b/packages/ccui/ui/timeline/src/timeline.tsx
@@ -0,0 +1,22 @@
+import type { TimelineProps } from './timeline-types'
+import { defineComponent } from 'vue'
+import { useNamespace } from '../../shared/hooks/use-namespace'
+import { timelineProps } from './timeline-types'
+import './timeline.scss'
+
+export default defineComponent({
+ name: 'CTimeline',
+ props: timelineProps,
+ emits: [],
+ setup(props: TimelineProps, { slots }) {
+ const ns = useNamespace('timeline')
+
+ return () => {
+ return (
+
+ {slots.default && slots.default()}
+
+ )
+ }
+ },
+})
diff --git a/packages/ccui/ui/timeline/test/timeline.test.ts b/packages/ccui/ui/timeline/test/timeline.test.ts
new file mode 100644
index 00000000..78db997e
--- /dev/null
+++ b/packages/ccui/ui/timeline/test/timeline.test.ts
@@ -0,0 +1,197 @@
+import { mount } from '@vue/test-utils'
+import { describe, expect, it } from 'vitest'
+import { h } from 'vue'
+import { Timeline, TimelineItem } from '../index'
+
+describe('timeline', () => {
+ it('should render', () => {
+ const wrapper = mount(Timeline)
+ expect(wrapper.exists()).toBe(true)
+ expect(wrapper.classes()).toContain('ccui-timeline')
+ })
+
+ it('should render timeline items', () => {
+ const wrapper = mount(Timeline, {
+ slots: {
+ default: () => [
+ h(TimelineItem, { timestamp: '2018/4/12' }, () => 'Test content 1'),
+ h(TimelineItem, { timestamp: '2018/4/3' }, () => 'Test content 2'),
+ ],
+ },
+ })
+
+ expect(wrapper.element.tagName).toBe('UL')
+ expect(wrapper.findAll('.ccui-timeline-item')).toHaveLength(2)
+ expect(wrapper.findAll('.ccui-timeline-item__timestamp')[0].text()).toBe('2018/4/12')
+ expect(wrapper.findAll('.ccui-timeline-item__timestamp')[1].text()).toBe('2018/4/3')
+ })
+})
+
+describe('timelineItem', () => {
+ it('should render with timestamp', () => {
+ const wrapper = mount(TimelineItem, {
+ props: {
+ timestamp: '2018/4/12',
+ },
+ slots: {
+ default: 'Test content',
+ },
+ })
+
+ expect(wrapper.exists()).toBe(true)
+ expect(wrapper.classes()).toContain('ccui-timeline-item')
+ expect(wrapper.find('.ccui-timeline-item__timestamp').text()).toBe('2018/4/12')
+ expect(wrapper.find('.ccui-timeline-item__content').text()).toBe('Test content')
+ })
+
+ it('should hide timestamp when hideTimestamp is true', () => {
+ const wrapper = mount(TimelineItem, {
+ props: {
+ timestamp: '2018/4/12',
+ hideTimestamp: true,
+ },
+ slots: {
+ default: 'Test content',
+ },
+ })
+
+ expect(wrapper.find('.ccui-timeline-item__timestamp').exists()).toBe(false)
+ })
+
+ it('should render with different types', () => {
+ const wrapper = mount(TimelineItem, {
+ props: {
+ timestamp: '2018/4/12',
+ type: 'primary',
+ },
+ slots: {
+ default: 'Test content',
+ },
+ })
+
+ expect(wrapper.find('.ccui-timeline-item__node--primary').exists()).toBe(true)
+ })
+
+ it('should render with custom color', () => {
+ const wrapper = mount(TimelineItem, {
+ props: {
+ timestamp: '2018/4/12',
+ color: '#ff0000',
+ },
+ slots: {
+ default: 'Test content',
+ },
+ })
+
+ const node = wrapper.find('.ccui-timeline-item__node')
+ expect(node.attributes('style')).toContain('background-color: rgb(255, 0, 0)')
+ })
+
+ it('should render with large size', () => {
+ const wrapper = mount(TimelineItem, {
+ props: {
+ timestamp: '2018/4/12',
+ size: 'large',
+ },
+ slots: {
+ default: 'Test content',
+ },
+ })
+
+ expect(wrapper.find('.ccui-timeline-item__node--large').exists()).toBe(true)
+ })
+
+ it('should render with hollow style', () => {
+ const wrapper = mount(TimelineItem, {
+ props: {
+ timestamp: '2018/4/12',
+ hollow: true,
+ },
+ slots: {
+ default: 'Test content',
+ },
+ })
+
+ expect(wrapper.find('.ccui-timeline-item__node.is-hollow').exists()).toBe(true)
+ })
+
+ it('should render timestamp at top when placement is top', () => {
+ const wrapper = mount(TimelineItem, {
+ props: {
+ timestamp: '2018/4/12',
+ placement: 'top',
+ },
+ slots: {
+ default: 'Test content',
+ },
+ })
+
+ expect(wrapper.find('.ccui-timeline-item__timestamp.is-top').exists()).toBe(true)
+ })
+
+ it('should render with center alignment', () => {
+ const wrapper = mount(TimelineItem, {
+ props: {
+ timestamp: '2018/4/12',
+ center: true,
+ },
+ slots: {
+ default: 'Test content',
+ },
+ })
+
+ expect(wrapper.classes()).toContain('ccui-timeline-item__center')
+ })
+
+ it('should render with string icon', () => {
+ const wrapper = mount(TimelineItem, {
+ props: {
+ timestamp: '2018/4/12',
+ icon: 'icon-class-name',
+ },
+ slots: {
+ default: 'Test content',
+ },
+ })
+
+ expect(wrapper.find('.ccui-timeline-item__icon').exists()).toBe(true)
+ expect(wrapper.find('.icon-class-name').exists()).toBe(true)
+ })
+
+ it('should render with component icon', () => {
+ const IconComponent = {
+ name: 'TestIcon',
+ render() {
+ return h('span', { class: 'test-icon' }, 'Icon')
+ },
+ }
+
+ const wrapper = mount(TimelineItem, {
+ props: {
+ timestamp: '2018/4/12',
+ icon: IconComponent,
+ },
+ slots: {
+ default: 'Test content',
+ },
+ })
+
+ expect(wrapper.find('.ccui-timeline-item__icon').exists()).toBe(true)
+ expect(wrapper.find('.test-icon').exists()).toBe(true)
+ })
+
+ it('should render with custom dot slot', () => {
+ const wrapper = mount(TimelineItem, {
+ props: {
+ timestamp: '2018/4/12',
+ },
+ slots: {
+ default: 'Test content',
+ dot: 'Custom
',
+ },
+ })
+
+ expect(wrapper.find('.ccui-timeline-item__dot').exists()).toBe(true)
+ expect(wrapper.find('.custom-dot').exists()).toBe(true)
+ })
+})
diff --git a/packages/docs/components/divider/index.md b/packages/docs/components/divider/index.md
index 088052e0..88c21799 100644
--- a/packages/docs/components/divider/index.md
+++ b/packages/docs/components/divider/index.md
@@ -82,7 +82,7 @@ export default defineComponent({
| color | string | -- | 设置分隔线的颜色 |
| direction | [DirectionType](#directiontype) | horizontal | 设置分隔线方向 |
| border-style | [BorderStyleType](#borderstyletype) | solid | 设置分隔符样式 |
-| content-position | [ContentPositionType](#Contentpositiontype) | center | 设置分隔线文案的位置 |
+| content-position | [ContentPositionType](#contentpositiontype) | center | 设置分隔线文案的位置 |
| content-color | string | -- | 设置分隔线文案的颜色 |
| content-background-color | string | -- | 设置分隔线文案的背景颜色 |
diff --git a/packages/docs/components/timeline/index.md b/packages/docs/components/timeline/index.md
new file mode 100644
index 00000000..06ca9169
--- /dev/null
+++ b/packages/docs/components/timeline/index.md
@@ -0,0 +1,263 @@
+# Timeline 时间线
+
+可视化地呈现时间流信息。
+
+## 何时使用
+
+- 当有一系列信息需按时间排列时
+- 需要有一条时间轴进行视觉上的串联时
+
+## 基本用法
+
+Timeline 可拆分成多个按照时间戳排列的活动,时间戳是其区分于其他控件的重要特征。
+
+:::demo
+
+```vue
+
+
+
+
+
+
+ 更新 Github 模板
+
+
+ 更新 Github 模板
+
+
+ 更新 Github 模板
+
+
+
+
+
+
+```
+
+:::
+
+## 自定义节点样式
+
+可根据实际场景自定义节点尺寸、颜色,或直接使用图标。
+
+:::demo
+
+```vue
+
+
+
+
+
+
+ 支持使用图标
+
+
+ 支持自定义颜色
+
+
+ 支持自定义尺寸
+
+
+ 支持空心点
+
+
+
+
+
+
+```
+
+:::
+
+## 自定义时间戳
+
+当内容在垂直方向上过高时,可将时间戳置于内容之上。
+
+:::demo
+
+```vue
+
+
+
+
+
+
+
+
更新 Github 模板
+
Tom 在 2025/09/09 20:46 提交了更新
+
+
+
+
+
更新 Github 模板
+
Tom 在 2025/09/08 20:46 提交了更新
+
+
+
+
+
更新 Github 模板
+
Tom 在 2025/09/07 20:46 提交了更新
+
+
+
+
+
+
+
+```
+
+:::
+
+## 垂直居中
+
+垂直居中样式的 Timeline-Item。
+
+:::demo
+
+```vue
+
+
+
+
+
+
+
+
更新 Github 模板
+
Tom 在 2025/09/09 20:46 提交了更新
+
+
+
+
+
更新 Github 模板
+
Tom 在 2025/09/08 20:46 提交了更新
+
+
+
+
+
+
+
+```
+
+:::
+
+## 自定义节点
+
+可以通过插槽自定义节点。
+
+:::demo
+
+```vue
+
+
+
+
+
+
+
+
+ ✓
+
+
+ 自定义节点内容
+
+
+ 普通节点
+
+
+
+
+
+
+```
+
+:::
+
+## API
+
+### Timeline Slots
+
+| 插槽名 | 说明 | 子标签 |
+| ---- | ---- | ---- |
+| default | timeline 组件的自定义默认内容 | Timeline-Item |
+
+### Timeline-Item Props
+
+| 参数 | 说明 | 类型 | 可选值 | 默认值 |
+| ---- | -------- | ---- | ---- | ---- |
+| timestamp | 时间戳 | string | — | '' |
+| hide-timestamp | 是否隐藏时间戳 | boolean | — | false |
+| center | 是否垂直居中 | boolean | — | false |
+| placement | 时间戳位置 | string | top/bottom | bottom |
+| type | 节点类型 | string | [TimelineItemType](#timelineitemtype) | — |
+| color | 节点颜色 | string | — | — |
+| size | 节点尺寸 | string | normal/large | normal |
+| icon | 自定义图标 | string/Component | — | — |
+| hollow | 是否空心点 | boolean | — | false |
+
+### Timeline-Item Events
+
+| 事件名 | 说明 | 回调参数 |
+| ---- | ---- | ---- |
+| — | — | — |
+
+### Timeline-Item Slots
+
+| 插槽名 | 说明 |
+| ---- | ---- |
+| default | 自定义内容 |
+| dot | 自定义节点 |
+
+## Timeline类型定义
+
+### TimelineItemType
+
+```ts
+export type TimelineItemType = 'primary' | 'success' | 'warning' | 'danger' | 'info' | ''
+```