Commit c4cfb1ff by 周田

init

parents
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
{
"recommendations": ["Vue.volar"]
}
# TimeLine 甘特时间轴组件
基于 Vue 3 + TypeScript + Element Plus 的深色主题甘特时间轴弹窗组件,支持多任务多段拖拽编辑、时间范围约束和脏数据检测。
## 功能
- 深色主题甘特图,多任务多段时间条
- 拖拽段左右边缘缩放、整体平移
- 鼠标滚轮缩放视口,底部缩略导航条平移
- 每分钟磁吸对齐,自适应刻度标签
- 段重叠检测与徽标提示
- 内嵌编辑弹窗:精确输入时间、支持 2/3 段拆分
- 每任务可配置 `limit` 约束范围,超出区域显示斜线遮罩并阻止拖拽
- `isDirty` 检测:confirm 时输出各任务相对初始值是否有改动
- 所有时间戳均以 **Unix 毫秒(ms)** 为单位输入输出
## 安装依赖
```bash
pnpm install
```
## 开发 / 构建
```bash
pnpm dev # 启动开发服务器 (localhost:5173)
pnpm build # 生产构建
```
## 用法
```vue
<script setup lang="ts">
import { ref } from 'vue'
import TimeLine from './components/TimeLine.vue'
import { type TimeTask, type TaskResult } from './components/timeline-types'
const visible = ref(false)
const tasks: TimeTask[] = [
{
id: 'task-1',
name: '任务 A',
color: '#60A5FA',
colorEnd: '#2563EB',
segments: [
{ startTime: 1778839200000, endTime: 1778925600000 },
],
// 可选:限制可编辑范围(Unix ms)
limit: { startTime: 1778774400000, endTime: 1778947200000 },
},
]
function onConfirm(result: Record<string, TaskResult>) {
for (const [id, r] of Object.entries(result)) {
console.log(id, r.segments, r.isDirty)
}
}
</script>
<template>
<button @click="visible = true">打开时间轴</button>
<TimeLine
v-model="visible"
:tasks="tasks"
@confirm="onConfirm"
/>
</template>
```
## Props
| Prop | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `v-model` | `boolean` | `false` | 控制弹窗显示/隐藏 |
| `tasks` | `TimeTask[]` | 必填 | 任务数组,时间戳均为 Unix ms |
| `trackDirty` | `boolean` | `true` | 是否在 confirm 结果中输出 `isDirty` |
| `enforceLimit` | `boolean` | `true` | 是否启用每任务的 `limit` 范围约束 |
## 事件
| 事件 | 载荷类型 | 触发时机 |
|------|---------|---------|
| `confirm` | `Record<string, TaskResult>` | 点击外层弹窗「确认」按钮 |
## 类型定义
```typescript
/** 时间原点:2026-05-15 00:00:00 本地时间 */
export const ORIGIN: number
export interface TimeSegment {
startTime: number // Unix ms
endTime: number // Unix ms
}
export interface TimeTask {
id: string
name: string
color: string // 渐变起始色(CSS 颜色值)
colorEnd: string // 渐变结束色
segments: TimeSegment[]
limit?: TimeSegment // 可选:该任务可编辑的时间范围约束(Unix ms)
}
export interface TaskResult {
segments: [number, number][] // 各段 [startMs, endMs]
isDirty: boolean // 相对于打开时是否有改动
}
```
## 关闭功能
两个可选功能均默认开启,可通过 prop 单独关闭:
```vue
<!-- 关闭 isDirty 检测:confirm 结果中 isDirty 始终为 false -->
<TimeLine v-model="visible" :tasks="tasks" :track-dirty="false" @confirm="onConfirm" />
<!-- 关闭 limit 约束:忽略任务的 limit 字段,斜线遮罩不显示,拖拽不受范围限制 -->
<TimeLine v-model="visible" :tasks="tasks" :enforce-limit="false" @confirm="onConfirm" />
<!-- 同时关闭两者 -->
<TimeLine v-model="visible" :tasks="tasks" :track-dirty="false" :enforce-limit="false" @confirm="onConfirm" />
```
> **注意**:这两个 prop 类型为 `boolean`,Vue 3 存在 boolean casting 行为——缺省时会被转成 `false` 而非 `undefined`,因此组件内部通过 `withDefaults` 将默认值显式设为 `true`。直接写 `:track-dirty` / `:enforce-limit`(不带值)等价于传 `true`,行为与缺省一致。
## limit 约束说明
为任务设置 `limit` 字段后(且 `enforceLimit``true`):
- 超出约束范围的区域显示斜线遮罩,视觉上标记为禁区
- 拖拽边缘或整体移动不可超出 `limit` 边界
- 内嵌编辑弹窗顶部显示当前任务的约束范围
```typescript
// 任务 A 只能在 5/15 00:00 ~ 5/17 00:00 内编辑
{
id: 'task-1',
limit: {
startTime: new Date(2026, 4, 15).getTime(), // 5/15 00:00
endTime: new Date(2026, 4, 17).getTime(), // 5/17 00:00
},
}
```
## isDirty 说明
`trackDirty`(默认 `true`)开启时,每次弹窗打开会对各任务的当前 segments 取快照。点击「确认」时,与快照比对并在 `TaskResult.isDirty` 中返回结果:
- 拖拽、编辑过任务段 → `isDirty: true`
- 未做任何改动直接确认 → `isDirty: false`
- 点击「取消」关闭弹窗时,所有修改丢弃,不触发 confirm
## 技术栈
- Vue 3.5 + `<script setup>` + TypeScript
- Element Plus 2.9(ElDialog)
- Vite 8 + vue-tsc
{
"version": "2.11",
"children": [
{
"type": "text",
"id": "xFdfi",
"x": 0,
"y": 4,
"name": "l1",
"fill": "#60A5FA",
"content": "01 默认状态 — 完整组件视图",
"fontFamily": "Inter",
"fontSize": 15,
"fontWeight": "600"
},
{
"type": "text",
"id": "Z1CGB",
"x": 0,
"y": 804,
"name": "l2",
"fill": "#60A5FA",
"content": "02 拖动交互 — 端点拖拽调整范围",
"fontFamily": "Inter",
"fontSize": 15,
"fontWeight": "600"
},
{
"type": "text",
"id": "JXYyT",
"x": 750,
"y": 804,
"name": "l3",
"fill": "#34D399",
"content": "03 拆分功能 — 条目多段化选取",
"fontFamily": "Inter",
"fontSize": 15,
"fontWeight": "600"
},
{
"type": "text",
"id": "GDOdb",
"x": 0,
"y": 1244,
"name": "l4",
"fill": "#EF4444",
"content": "04 重叠检测 — 红色高亮预警",
"fontFamily": "Inter",
"fontSize": 15,
"fontWeight": "600"
},
{
"type": "text",
"id": "BXKxM",
"x": 750,
"y": 1244,
"name": "l5",
"fill": "#A78BFA",
"content": "05 时间精确输入 — 弹窗表单",
"fontFamily": "Inter",
"fontSize": 15,
"fontWeight": "600"
},
{
"type": "frame",
"id": "MINh3",
"x": 0,
"y": 30,
"name": "01 默认状态",
"width": 1440,
"height": 740,
"fill": "#0F1629",
"layout": "none",
"children": [
{
"type": "frame",
"id": "ii9NY",
"x": 0,
"y": 0,
"name": "header",
"width": 1440,
"height": 56,
"fill": "#1A2240",
"layout": "none",
"children": [
{
"type": "text",
"id": "iqVDm",
"x": 24,
"y": 16,
"name": "hTitle",
"fill": "#F1F5F9",
"content": "时间轴范围选择器",
"fontFamily": "Inter",
"fontSize": 20,
"fontWeight": "700"
},
{
"type": "text",
"id": "q3ePss",
"x": 252,
"y": 20,
"name": "hSub",
"fill": "#64748B",
"content": "可拖拽时间条 · 支持拆分 · 重叠检测 · 精确到秒 · 3天视图",
"fontFamily": "Inter",
"fontSize": 13,
"fontWeight": "normal"
},
{
"type": "frame",
"id": "Jx5th",
"x": 1296,
"y": 14,
"name": "btn-legend",
"width": 64,
"height": 28,
"fill": "#2D3B5E",
"cornerRadius": 6,
"layout": "none",
"children": [
{
"type": "text",
"id": "ZX8sg",
"x": 14,
"y": 7,
"name": "hBtn1T",
"fill": "#94A3B8",
"content": "图例",
"fontFamily": "Inter",
"fontSize": 12,
"fontWeight": "normal"
}
]
},
{
"type": "frame",
"id": "D8cXna",
"x": 1368,
"y": 14,
"name": "btn-settings",
"width": 56,
"height": 28,
"fill": "#3B82F6",
"cornerRadius": 6,
"layout": "none",
"children": [
{
"type": "text",
"id": "zpKVy",
"x": 14,
"y": 7,
"name": "hBtn2T",
"fill": "#FFFFFF",
"content": "设置",
"fontFamily": "Inter",
"fontSize": 12,
"fontWeight": "normal"
}
]
}
]
},
{
"type": "rectangle",
"id": "SO9nX",
"x": 0,
"y": 56,
"name": "hdr-divider",
"fill": "#2D3B5E",
"width": 1440,
"height": 1
},
{
"type": "rectangle",
"id": "pj80J",
"x": 172,
"y": 57,
"name": "ruler-bg",
"fill": "#0C1525",
"width": 1244,
"height": 39
},
{
"type": "rectangle",
"id": "jOusS",
"x": 172,
"y": 57,
"name": "label-sep",
"fill": "#2D3B5E",
"width": 1,
"height": 455
},
{
"type": "rectangle",
"id": "Vzg0j",
"x": 0,
"y": 96,
"name": "row-bg-1",
"fill": "#1E2845",
"width": 1440,
"height": 104
},
{
"type": "rectangle",
"id": "WIOfm",
"x": 0,
"y": 200,
"name": "row-bg-2",
"fill": "#1A2240",
"width": 1440,
"height": 104
},
{
"type": "rectangle",
"id": "mdOBs",
"x": 0,
"y": 304,
"name": "row-bg-3",
"fill": "#1E2845",
"width": 1440,
"height": 104
},
{
"type": "rectangle",
"id": "di41u",
"x": 0,
"y": 408,
"name": "row-bg-4",
"fill": "#1A2240",
"width": 1440,
"height": 104
},
{
"type": "rectangle",
"id": "p6We0q",
"x": 0,
"y": 200,
"name": "row-div-1",
"fill": "#2D3B5E",
"width": 1440,
"height": 1
},
{
"type": "rectangle",
"id": "jnwvQ",
"x": 0,
"y": 304,
"name": "row-div-2",
"fill": "#2D3B5E",
"width": 1440,
"height": 1
},
{
"type": "rectangle",
"id": "fXGOc",
"x": 0,
"y": 408,
"name": "row-div-3",
"fill": "#2D3B5E",
"width": 1440,
"height": 1
},
{
"type": "rectangle",
"id": "DVk0e",
"x": 0,
"y": 512,
"name": "legend-bg",
"fill": "#0A1020",
"width": 1440,
"height": 228
},
{
"type": "rectangle",
"id": "f1s7q",
"x": 0,
"y": 512,
"name": "legend-div",
"fill": "#2D3B5E",
"width": 1440,
"height": 1
},
{
"type": "text",
"id": "Yz7vG",
"x": 157,
"y": 66,
"name": "rt0",
"fill": "#94A3B8",
"content": "5/15\n00:00",
"fontFamily": "Inter",
"fontSize": 10,
"fontWeight": "normal"
},
{
"type": "text",
"id": "B0NeIo",
"x": 357,
"y": 66,
"name": "rt4",
"fill": "#3D4F6B",
"content": "12:00",
"fontFamily": "Inter",
"fontSize": 10,
"fontWeight": "normal"
},
{
"type": "text",
"id": "D1wA0j",
"x": 557,
"y": 66,
"name": "rt8",
"fill": "#94A3B8",
"content": "5/16\n00:00",
"fontFamily": "Inter",
"fontSize": 10,
"fontWeight": "normal"
},
{
"type": "text",
"id": "rdsH9",
"x": 757,
"y": 66,
"name": "rt12",
"fill": "#3D4F6B",
"content": "12:00",
"fontFamily": "Inter",
"fontSize": 10,
"fontWeight": "normal"
},
{
"type": "text",
"id": "mizWk",
"x": 957,
"y": 66,
"name": "rt16",
"fill": "#94A3B8",
"content": "5/17\n00:00",
"fontFamily": "Inter",
"fontSize": 10,
"fontWeight": "normal"
},
{
"type": "text",
"id": "Tsyw3",
"x": 1157,
"y": 66,
"name": "rt20",
"fill": "#3D4F6B",
"content": "12:00",
"fontFamily": "Inter",
"fontSize": 10,
"fontWeight": "normal"
},
{
"type": "text",
"id": "qcVe3",
"x": 1357,
"y": 66,
"name": "rt24",
"fill": "#94A3B8",
"content": "5/18\n00:00",
"fontFamily": "Inter",
"fontSize": 10,
"fontWeight": "normal"
},
{
"type": "rectangle",
"id": "OPsnn",
"x": 372,
"y": 57,
"name": "tk4",
"fill": "#2D3B5E",
"width": 1,
"height": 39
},
{
"type": "rectangle",
"id": "YEFPj",
"x": 572,
"y": 57,
"name": "tk8",
"fill": "#2D3B5E",
"width": 1,
"height": 39
},
{
"type": "rectangle",
"id": "VIb6S",
"x": 572,
"y": 57,
"name": "dayLine2",
"fill": "#243050",
"width": 1,
"height": 455
},
{
"type": "rectangle",
"id": "dXNqu",
"x": 972,
"y": 57,
"name": "tk16",
"fill": "#2D3B5E",
"width": 1,
"height": 39
},
{
"type": "rectangle",
"id": "P3mos",
"x": 1172,
"y": 57,
"name": "tk20",
"fill": "#2D3B5E",
"width": 1,
"height": 39
},
{
"type": "text",
"id": "gip0Z",
"x": 14,
"y": 132,
"name": "rn1",
"fill": "#CBD5E1",
"content": "任务 A",
"fontFamily": "Inter",
"fontSize": 14,
"fontWeight": "600"
},
{
"type": "text",
"id": "xMpRd",
"x": 14,
"y": 152,
"name": "ri1",
"fill": "#475569",
"content": "id: 001",
"fontFamily": "Inter",
"fontSize": 11,
"fontWeight": "normal"
},
{
"type": "text",
"id": "ED18Q",
"x": 14,
"y": 236,
"name": "rn2",
"fill": "#CBD5E1",
"content": "任务 B",
"fontFamily": "Inter",
"fontSize": 14,
"fontWeight": "600"
},
{
"type": "text",
"id": "BtsZ0",
"x": 14,
"y": 256,
"name": "ri2",
"fill": "#475569",
"content": "id: 002",
"fontFamily": "Inter",
"fontSize": 11,
"fontWeight": "normal"
},
{
"type": "text",
"id": "T01Rts",
"x": 14,
"y": 340,
"name": "rn3",
"fill": "#CBD5E1",
"content": "任务 C",
"fontFamily": "Inter",
"fontSize": 14,
"fontWeight": "600"
},
{
"type": "text",
"id": "t93hz",
"x": 14,
"y": 360,
"name": "ri3",
"fill": "#475569",
"content": "id: 003",
"fontFamily": "Inter",
"fontSize": 11,
"fontWeight": "normal"
},
{
"type": "text",
"id": "yZV57",
"x": 14,
"y": 444,
"name": "rn4",
"fill": "#CBD5E1",
"content": "任务 D",
"fontFamily": "Inter",
"fontSize": 14,
"fontWeight": "600"
},
{
"type": "text",
"id": "xpZzh",
"x": 14,
"y": 464,
"name": "ri4",
"fill": "#475569",
"content": "id: 004",
"fontFamily": "Inter",
"fontSize": 11,
"fontWeight": "normal"
},
{
"type": "rectangle",
"cornerRadius": 14,
"id": "H5HhW",
"x": 472,
"y": 134,
"name": "bar-A",
"fill": {
"type": "gradient",
"gradientType": "linear",
"enabled": true,
"rotation": 90,
"size": {
"height": 1
},
"colors": [
{
"color": "#60A5FA",
"position": 0
},
{
"color": "#2563EB",
"position": 1
}
]
},
"width": 400,
"height": 28
},
{
"type": "ellipse",
"id": "JPJ5Y",
"x": 465,
"y": 141,
"name": "handle-A-L",
"fill": "#FFFFFF",
"width": 14,
"height": 14,
"stroke": {
"thickness": 2,
"fill": "#3B82F6"
}
},
{
"type": "ellipse",
"id": "BfxZc",
"x": 865,
"y": 141,
"name": "handle-A-R",
"fill": "#FFFFFF",
"width": 14,
"height": 14,
"stroke": {
"thickness": 2,
"fill": "#3B82F6"
}
},
{
"type": "text",
"id": "pVQjQ",
"x": 456,
"y": 168,
"name": "tA1",
"fill": "#60A5FA",
"content": "5/15 18:00",
"fontFamily": "Inter",
"fontSize": 10,
"fontWeight": "normal"
},
{
"type": "text",
"id": "MfxGC",
"x": 856,
"y": 168,
"name": "tA2",
"fill": "#60A5FA",
"content": "5/16 18:00",
"fontFamily": "Inter",
"fontSize": 10,
"fontWeight": "normal"
},
{
"type": "rectangle",
"cornerRadius": 14,
"id": "DH2qx",
"x": 672,
"y": 238,
"name": "bar-B",
"fill": {
"type": "gradient",
"gradientType": "linear",
"enabled": true,
"rotation": 90,
"size": {
"height": 1
},
"colors": [
{
"color": "#34D399",
"position": 0
},
{
"color": "#059669",
"position": 1
}
]
},
"width": 400,
"height": 28
},
{
"type": "ellipse",
"id": "NzV6z",
"x": 665,
"y": 245,
"name": "handle-B-L",
"fill": "#FFFFFF",
"width": 14,
"height": 14,
"stroke": {
"thickness": 2,
"fill": "#10B981"
}
},
{
"type": "ellipse",
"id": "OntqV",
"x": 1065,
"y": 245,
"name": "handle-B-R",
"fill": "#FFFFFF",
"width": 14,
"height": 14,
"stroke": {
"thickness": 2,
"fill": "#10B981"
}
},
{
"type": "text",
"id": "ZgC91",
"x": 656,
"y": 272,
"name": "tB1",
"fill": "#34D399",
"content": "5/16 06:00",
"fontFamily": "Inter",
"fontSize": 10,
"fontWeight": "normal"
},
{
"type": "text",
"id": "t3uAcI",
"x": 1056,
"y": 272,
"name": "tB2",
"fill": "#34D399",
"content": "5/17 06:00",
"fontFamily": "Inter",
"fontSize": 10,
"fontWeight": "normal"
},
{
"type": "rectangle",
"cornerRadius": 14,
"id": "s1kxW5",
"x": 922,
"y": 342,
"name": "bar-C",
"fill": {
"type": "gradient",
"gradientType": "linear",
"enabled": true,
"rotation": 90,
"size": {
"height": 1
},
"colors": [
{
"color": "#FCD34D",
"position": 0
},
{
"color": "#D97706",
"position": 1
}
]
},
"width": 350,
"height": 28
},
{
"type": "ellipse",
"id": "rww3Y",
"x": 915,
"y": 349,
"name": "handle-C-L",
"fill": "#FFFFFF",
"width": 14,
"height": 14,
"stroke": {
"thickness": 2,
"fill": "#F59E0B"
}
},
{
"type": "ellipse",
"id": "O7ZQQH",
"x": 1265,
"y": 349,
"name": "handle-C-R",
"fill": "#FFFFFF",
"width": 14,
"height": 14,
"stroke": {
"thickness": 2,
"fill": "#F59E0B"
}
},
{
"type": "text",
"id": "J7uCx",
"x": 906,
"y": 376,
"name": "tC1",
"fill": "#FCD34D",
"content": "5/16 21:00",
"fontFamily": "Inter",
"fontSize": 10,
"fontWeight": "normal"
},
{
"type": "text",
"id": "KU7IH",
"x": 1256,
"y": 376,
"name": "tC2",
"fill": "#FCD34D",
"content": "5/17 18:00",
"fontFamily": "Inter",
"fontSize": 10,
"fontWeight": "normal"
},
{
"type": "rectangle",
"cornerRadius": 14,
"id": "vtcXk",
"x": 322,
"y": 446,
"name": "bar-D",
"fill": {
"type": "gradient",
"gradientType": "linear",
"enabled": true,
"rotation": 90,
"size": {
"height": 1
},
"colors": [
{
"color": "#A78BFA",
"position": 0
},
{
"color": "#7C3AED",
"position": 1
}
]
},
"width": 300,
"height": 28
},
{
"type": "ellipse",
"id": "B1IcWm",
"x": 315,
"y": 453,
"name": "handle-D-L",
"fill": "#FFFFFF",
"width": 14,
"height": 14,
"stroke": {
"thickness": 2,
"fill": "#8B5CF6"
}
},
{
"type": "ellipse",
"id": "oRmXF",
"x": 615,
"y": 453,
"name": "handle-D-R",
"fill": "#FFFFFF",
"width": 14,
"height": 14,
"stroke": {
"thickness": 2,
"fill": "#8B5CF6"
}
},
{
"type": "text",
"id": "FhVuC",
"x": 306,
"y": 480,
"name": "tD1",
"fill": "#A78BFA",
"content": "5/15 09:00",
"fontFamily": "Inter",
"fontSize": 10,
"fontWeight": "normal"
},
{
"type": "text",
"id": "Buw5h",
"x": 606,
"y": 480,
"name": "tD2",
"fill": "#A78BFA",
"content": "5/16 03:00",
"fontFamily": "Inter",
"fontSize": 10,
"fontWeight": "normal"
},
{
"type": "text",
"id": "th3Tc",
"x": 24,
"y": 525,
"name": "legTitle",
"fill": "#94A3B8",
"content": "图例说明",
"fontFamily": "Inter",
"fontSize": 13,
"fontWeight": "600"
},
{
"type": "ellipse",
"id": "x2jTS",
"x": 24,
"y": 558,
"name": "legH",
"fill": "#FFFFFF",
"width": 14,
"height": 14,
"stroke": {
"thickness": 2,
"fill": "#3B82F6"
}
},
{
"type": "text",
"id": "tEhFq",
"x": 46,
"y": 560,
"name": "legHT",
"fill": "#94A3B8",
"content": "拖动把手 — 拖拽两端白色圆圈,可向左/右延伸或缩短时间范围",
"fontFamily": "Inter",
"fontSize": 12,
"fontWeight": "normal"
},
{
"type": "rectangle",
"cornerRadius": 7,
"id": "A0vhjZ",
"x": 24,
"y": 588,
"name": "legSBar1",
"fill": "#3B82F6",
"width": 60,
"height": 14
},
{
"type": "rectangle",
"id": "ko9q9",
"x": 53,
"y": 582,
"name": "legSLine",
"fill": "#FFD700",
"width": 2,
"height": 26
},
{
"type": "text",
"id": "qzJZN",
"x": 96,
"y": 590,
"name": "legST",
"fill": "#94A3B8",
"content": "拆分按钮 — 悬停时显示黄色分割线,点击将条目拆为独立的多段",
"fontFamily": "Inter",
"fontSize": 12,
"fontWeight": "normal"
},
{
"type": "rectangle",
"cornerRadius": 7,
"id": "n7DvX",
"x": 24,
"y": 618,
"name": "legOA",
"fill": "#3B82F6",
"width": 56,
"height": 14
},
{
"type": "rectangle",
"cornerRadius": 3,
"id": "Pewph",
"x": 44,
"y": 622,
"name": "legOB",
"fill": "#10B981",
"width": 56,
"height": 6
},
{
"type": "rectangle",
"cornerRadius": 4,
"id": "j9CsoS",
"x": 44,
"y": 618,
"name": "legOR",
"opacity": 0.85,
"fill": "#EF4444",
"width": 36,
"height": 14
},
{
"type": "text",
"id": "fdZbm",
"x": 116,
"y": 620,
"name": "legOT",
"fill": "#94A3B8",
"content": "重叠区域 — 两条时间段发生时间交叉时,交叉区域以红色高亮警告",
"fontFamily": "Inter",
"fontSize": 12,
"fontWeight": "normal"
},
{
"type": "frame",
"id": "l1toWO",
"x": 24,
"y": 654,
"name": "legInF",
"width": 84,
"height": 22,
"fill": "#1E2845",
"cornerRadius": 4,
"stroke": {
"thickness": 1,
"fill": "#3B82F6"
},
"layout": "none",
"children": [
{
"type": "text",
"id": "dEb0s",
"x": 0,
"y": 0,
"name": "legInT",
"fill": "#60A5FA",
"content": "14:00 ▾",
"fontFamily": "Inter",
"fontSize": 11,
"fontWeight": "normal"
}
]
},
{
"type": "text",
"id": "uLwds",
"x": 120,
"y": 657,
"name": "legIT",
"fill": "#94A3B8",
"content": "时间输入框 — 点击时间标签,弹出精确输入弹窗,支持键盘直接键入",
"fontFamily": "Inter",
"fontSize": 12,
"fontWeight": "normal"
},
{
"type": "rectangle",
"id": "j0CDu",
"x": 671,
"y": 130,
"name": "split-hint",
"opacity": 0.9,
"fill": "#FFD700",
"width": 2,
"height": 36
},
{
"type": "frame",
"id": "d61H2",
"x": 649,
"y": 112,
"name": "split-tooltip",
"width": 48,
"height": 18,
"fill": "#1E293B",
"cornerRadius": 4,
"stroke": {
"thickness": 1,
"fill": "#FFD700"
},
"layout": "none",
"children": [
{
"type": "text",
"id": "ET8VU",
"x": 0,
"y": 0,
"name": "splitHintLT",
"fill": "#FFD700",
"content": "✂ 拆分",
"fontFamily": "Inter",
"fontSize": 10,
"fontWeight": "normal"
}
]
},
{
"type": "rectangle",
"id": "d3t1g",
"x": 972,
"y": 57,
"name": "dayLine3",
"fill": "#243050",
"width": 1,
"height": 455
}
]
},
{
"type": "frame",
"id": "Qf9Xr",
"x": 0,
"y": 830,
"name": "02 拖动交互",
"width": 700,
"height": 380,
"fill": "#0F1629",
"layout": "none",
"children": [
{
"type": "frame",
"id": "j1OJE",
"x": 0,
"y": 0,
"name": "header",
"width": 700,
"height": 48,
"fill": "#1A2240",
"layout": "none",
"children": [
{
"type": "text",
"id": "zSElg",
"x": 20,
"y": 14,
"name": "s2ht",
"fill": "#F1F5F9",
"content": "② 拖动交互",
"fontFamily": "Inter",
"fontSize": 16,
"fontWeight": "700"
},
{
"type": "text",
"id": "aDBtw",
"x": 142,
"y": 17,
"name": "s2hst",
"fill": "#64748B",
"content": "— 拖拽端点把手,实时调整时间范围",
"fontFamily": "Inter",
"fontSize": 12,
"fontWeight": "normal"
}
]
},
{
"type": "rectangle",
"id": "L53al",
"x": 0,
"y": 68,
"name": "s2rowBg",
"fill": "#1E2845",
"width": 700,
"height": 100
},
{
"type": "rectangle",
"id": "ZaeqW",
"x": 120,
"y": 48,
"name": "s2rulBg",
"fill": "#0C1525",
"width": 580,
"height": 20
},
{
"type": "rectangle",
"id": "hs9tp",
"x": 120,
"y": 48,
"name": "s2lbSep",
"fill": "#2D3B5E",
"width": 1,
"height": 120
},
{
"type": "text",
"id": "eElAn",
"x": 14,
"y": 100,
"name": "s2lbName",
"fill": "#CBD5E1",
"content": "任务 A",
"fontFamily": "Inter",
"fontSize": 13,
"fontWeight": "600"
},
{
"type": "text",
"id": "JPWao",
"x": 14,
"y": 118,
"name": "s2lbId",
"fill": "#475569",
"content": "id: 001",
"fontFamily": "Inter",
"fontSize": 11,
"fontWeight": "normal"
},
{
"type": "text",
"id": "gOEps",
"x": 114,
"y": 52,
"name": "s2rt0",
"fill": "#475569",
"content": "0",
"fontFamily": "Inter",
"fontSize": 10,
"fontWeight": "normal"
},
{
"type": "text",
"id": "b0oTa3",
"x": 246,
"y": 52,
"name": "s2rt6",
"fill": "#475569",
"content": "06",
"fontFamily": "Inter",
"fontSize": 10,
"fontWeight": "normal"
},
{
"type": "text",
"id": "q8rJgr",
"x": 378,
"y": 52,
"name": "s2rt12",
"fill": "#94A3B8",
"content": "12",
"fontFamily": "Inter",
"fontSize": 10,
"fontWeight": "normal"
},
{
"type": "text",
"id": "BB93C",
"x": 422,
"y": 52,
"name": "s2rt14",
"fill": "#EF4444",
"content": "14",
"fontFamily": "Inter",
"fontSize": 10,
"fontWeight": "normal"
},
{
"type": "text",
"id": "Xsd4D",
"x": 464,
"y": 52,
"name": "s2rt16",
"fill": "#60A5FA",
"content": "16",
"fontFamily": "Inter",
"fontSize": 10,
"fontWeight": "normal"
},
{
"type": "text",
"id": "Z4RcKI",
"x": 641,
"y": 52,
"name": "s2rt24",
"fill": "#475569",
"content": "24",
"fontFamily": "Inter",
"fontSize": 10,
"fontWeight": "normal"
},
{
"type": "rectangle",
"id": "wuFuT",
"x": 428,
"y": 48,
"name": "s2tk14",
"opacity": 0.5,
"fill": "#EF4444",
"width": 1,
"height": 120
},
{
"type": "text",
"id": "SVphW",
"x": 410,
"y": 172,
"name": "s2oldLabel",
"opacity": 0.8,
"fill": "#EF4444",
"content": "原始\n14:00",
"fontFamily": "Inter",
"fontSize": 10,
"fontWeight": "normal"
},
{
"type": "rectangle",
"cornerRadius": 14,
"id": "QXMJl",
"x": 252,
"y": 96,
"name": "drag-bar",
"fill": {
"type": "gradient",
"gradientType": "linear",
"enabled": true,
"rotation": 90,
"size": {
"height": 1
},
"colors": [
{
"color": "#60A5FA",
"position": 0
},
{
"color": "#2563EB",
"position": 1
}
]
},
"width": 220,
"height": 28
},
{
"type": "ellipse",
"id": "tAxUV",
"x": 245,
"y": 103,
"name": "s2hl",
"fill": "#FFFFFF",
"width": 14,
"height": 14,
"stroke": {
"thickness": 2,
"fill": "#3B82F6"
}
},
{
"type": "ellipse",
"id": "H6WVx",
"x": 461,
"y": 103,
"name": "s2hr",
"fill": "#3B82F6",
"width": 14,
"height": 14,
"stroke": {
"thickness": 2.5,
"fill": "#FFFFFF"
}
},
{
"type": "ellipse",
"id": "U41Y8",
"x": 455,
"y": 97,
"name": "s2hrGlow",
"opacity": 0.2,
"fill": "#3B82F6",
"width": 26,
"height": 26
},
{
"type": "text",
"id": "mEE7N",
"x": 240,
"y": 130,
"name": "s2tStart",
"fill": "#60A5FA",
"content": "06:00",
"fontFamily": "Inter",
"fontSize": 11,
"fontWeight": "normal"
},
{
"type": "text",
"id": "nAFX9",
"x": 460,
"y": 130,
"name": "s2tEnd",
"fill": "#FFFFFF",
"content": "16:00",
"fontFamily": "Inter",
"fontSize": 11,
"fontWeight": "600"
},
{
"type": "frame",
"id": "XnST8",
"x": 444,
"y": 68,
"name": "time-tooltip",
"width": 62,
"height": 26,
"fill": "#1E293B",
"cornerRadius": 6,
"stroke": {
"thickness": 1.5,
"fill": "#60A5FA"
},
"layout": "none",
"children": [
{
"type": "text",
"id": "C87hcH",
"x": 10,
"y": 6,
"name": "s2tipT",
"fill": "#60A5FA",
"content": "16:00",
"fontFamily": "Inter",
"fontSize": 12,
"fontWeight": "700"
}
]
},
{
"type": "rectangle",
"id": "nGZC0",
"x": 473,
"y": 92,
"name": "s2tipArrow",
"rotation": 45,
"fill": "#1E293B",
"width": 8,
"height": 8,
"stroke": {
"thickness": 1.5,
"fill": "#60A5FA"
}
},
{
"type": "rectangle",
"id": "WYiAC",
"x": 0,
"y": 210,
"name": "s2ann",
"fill": "#111827",
"width": 700,
"height": 170
},
{
"type": "rectangle",
"id": "li3HM",
"x": 0,
"y": 210,
"name": "s2annDiv",
"fill": "#2D3B5E",
"width": 700,
"height": 1
},
{
"type": "text",
"id": "KEtHR",
"x": 24,
"y": 224,
"name": "s2ann1",
"fill": "#94A3B8",
"content": "① 将鼠标悬停在时间条两端的白色把手上,光标变为左右拖拽形状",
"fontFamily": "Inter",
"fontSize": 12,
"fontWeight": "normal"
},
{
"type": "text",
"id": "S6HWm",
"x": 24,
"y": 252,
"name": "s2ann2",
"fill": "#94A3B8",
"content": "② 按住鼠标左键并拖拽,时间条实时延伸/缩短,上方动态提示框显示当前时间",
"fontFamily": "Inter",
"fontSize": 12,
"fontWeight": "normal"
},
{
"type": "text",
"id": "E7yO5",
"x": 24,
"y": 280,
"name": "s2ann3",
"fill": "#94A3B8",
"content": "③ 红色虚线标记原始位置(14:00 → 拖拽至 16:00),松开鼠标后完成调整",
"fontFamily": "Inter",
"fontSize": 12,
"fontWeight": "normal"
},
{
"type": "text",
"id": "AIZOo",
"x": 24,
"y": 308,
"name": "s2ann4",
"fill": "#94A3B8",
"content": "④ 时间精度可配置:默认 30min,可改为 15min / 5min / 1min",
"fontFamily": "Inter",
"fontSize": 12,
"fontWeight": "normal"
},
{
"type": "text",
"id": "uwAzX",
"x": 464,
"y": 96,
"name": "s2cursorIco",
"fill": "#FFFFFF",
"content": "⟺",
"fontFamily": "Inter",
"fontSize": 14,
"fontWeight": "700"
}
]
},
{
"type": "frame",
"id": "ROsLM",
"x": 750,
"y": 830,
"name": "03 拆分功能",
"width": 700,
"height": 380,
"fill": "#0F1629",
"layout": "none",
"children": [
{
"type": "frame",
"id": "eyXAB",
"x": 0,
"y": 0,
"name": "header",
"width": 700,
"height": 48,
"fill": "#1A2240",
"layout": "none",
"children": [
{
"type": "text",
"id": "iUp8Z",
"x": 20,
"y": 14,
"name": "s3ht",
"fill": "#F1F5F9",
"content": "③ 拆分功能",
"fontFamily": "Inter",
"fontSize": 16,
"fontWeight": "700"
},
{
"type": "text",
"id": "r1ux0n",
"x": 148,
"y": 17,
"name": "s3hst",
"fill": "#64748B",
"content": "— 将单条拆为多个独立段,分别调整",
"fontFamily": "Inter",
"fontSize": 12,
"fontWeight": "normal"
}
]
},
{
"type": "rectangle",
"id": "k2Srp",
"x": 0,
"y": 190,
"name": "s3secDiv",
"fill": "#2D3B5E",
"width": 700,
"height": 1
},
{
"type": "text",
"id": "S64it",
"x": 8,
"y": 52,
"name": "s3secLbl1",
"fill": "#F59E0B",
"content": "拆分前",
"fontFamily": "Inter",
"fontSize": 11,
"fontWeight": "600"
},
{
"type": "text",
"id": "Agkw7",
"x": 8,
"y": 196,
"name": "s3secLbl2",
"fill": "#34D399",
"content": "拆分后",
"fontFamily": "Inter",
"fontSize": 11,
"fontWeight": "600"
},
{
"type": "rectangle",
"id": "X2VK0",
"x": 0,
"y": 68,
"name": "s3rb1",
"fill": "#1E2845",
"width": 700,
"height": 112
},
{
"type": "rectangle",
"id": "H4vFEb",
"x": 0,
"y": 192,
"name": "s3rb2",
"fill": "#192038",
"width": 700,
"height": 112
},
{
"type": "rectangle",
"id": "t5QG8i",
"x": 120,
"y": 48,
"name": "s3lbSep",
"fill": "#2D3B5E",
"width": 1,
"height": 260
},
{
"type": "rectangle",
"id": "tKvk8",
"x": 120,
"y": 48,
"name": "s3rulBg",
"fill": "#0C1525",
"width": 580,
"height": 20
},
{
"type": "text",
"id": "cxCNB",
"x": 114,
"y": 52,
"name": "s3rt0",
"fill": "#475569",
"content": "0",
"fontFamily": "Inter",
"fontSize": 10,
"fontWeight": "normal"
},
{
"type": "text",
"id": "rV8Aq",
"x": 246,
"y": 52,
"name": "s3rt6",
"fill": "#475569",
"content": "06",
"fontFamily": "Inter",
"fontSize": 10,
"fontWeight": "normal"
},
{
"type": "text",
"id": "g1IKh",
"x": 358,
"y": 52,
"name": "s3rt11",
"fill": "#475569",
"content": "11",
"fontFamily": "Inter",
"fontSize": 10,
"fontWeight": "normal"
},
{
"type": "text",
"id": "DRM7J",
"x": 380,
"y": 52,
"name": "s3rt12",
"fill": "#F59E0B",
"content": "12",
"fontFamily": "Inter",
"fontSize": 10,
"fontWeight": "normal"
},
{
"type": "text",
"id": "C6hKM",
"x": 402,
"y": 52,
"name": "s3rt13",
"fill": "#475569",
"content": "13",
"fontFamily": "Inter",
"fontSize": 10,
"fontWeight": "normal"
},
{
"type": "text",
"id": "ggvxh",
"x": 510,
"y": 52,
"name": "s3rt18",
"fill": "#475569",
"content": "18",
"fontFamily": "Inter",
"fontSize": 10,
"fontWeight": "normal"
},
{
"type": "rectangle",
"cornerRadius": 14,
"id": "dyn46",
"x": 252,
"y": 108,
"name": "full-bar",
"fill": {
"type": "gradient",
"gradientType": "linear",
"enabled": true,
"rotation": 90,
"size": {
"height": 1
},
"colors": [
{
"color": "#FCD34D",
"position": 0
},
{
"color": "#D97706",
"position": 1
}
]
},
"width": 264,
"height": 28
},
{
"type": "ellipse",
"id": "R4hk2J",
"x": 245,
"y": 115,
"name": "s3hlF1",
"fill": "#FFFFFF",
"width": 14,
"height": 14,
"stroke": {
"thickness": 2,
"fill": "#F59E0B"
}
},
{
"type": "ellipse",
"id": "rjLUm",
"x": 509,
"y": 115,
"name": "s3hlF2",
"fill": "#FFFFFF",
"width": 14,
"height": 14,
"stroke": {
"thickness": 2,
"fill": "#F59E0B"
}
},
{
"type": "text",
"id": "WIkGL",
"x": 240,
"y": 142,
"name": "s3tF1",
"fill": "#FCD34D",
"content": "06:00",
"fontFamily": "Inter",
"fontSize": 11,
"fontWeight": "normal"
},
{
"type": "text",
"id": "mXIPF",
"x": 503,
"y": 142,
"name": "s3tF2",
"fill": "#FCD34D",
"content": "18:00",
"fontFamily": "Inter",
"fontSize": 11,
"fontWeight": "normal"
},
{
"type": "rectangle",
"id": "SvgAn",
"x": 384,
"y": 96,
"name": "split-line",
"fill": "#FFD700",
"width": 2,
"height": 52
},
{
"type": "frame",
"id": "q2gh9",
"x": 360,
"y": 72,
"name": "split-tooltip",
"width": 52,
"height": 22,
"fill": "#1E2845",
"cornerRadius": 4,
"stroke": {
"thickness": 1.5,
"fill": "#FFD700"
},
"layout": "none",
"children": [
{
"type": "text",
"id": "tYJph",
"x": 7,
"y": 5,
"name": "s3splitTipT",
"fill": "#FFD700",
"content": "✂ 拆分",
"fontFamily": "Inter",
"fontSize": 11,
"fontWeight": "600"
}
]
},
{
"type": "rectangle",
"cornerRadius": 14,
"id": "qSPhA",
"x": 252,
"y": 216,
"name": "seg-1",
"fill": {
"type": "gradient",
"gradientType": "linear",
"enabled": true,
"rotation": 90,
"size": {
"height": 1
},
"colors": [
{
"color": "#FCD34D",
"position": 0
},
{
"color": "#D97706",
"position": 1
}
]
},
"width": 110,
"height": 28
},
{
"type": "rectangle",
"cornerRadius": 14,
"id": "HVt1Y",
"x": 406,
"y": 216,
"name": "seg-2",
"fill": {
"type": "gradient",
"gradientType": "linear",
"enabled": true,
"rotation": 90,
"size": {
"height": 1
},
"colors": [
{
"color": "#FCD34D",
"position": 0
},
{
"color": "#D97706",
"position": 1
}
]
},
"width": 110,
"height": 28
},
{
"type": "rectangle",
"cornerRadius": 4,
"id": "WbJB7",
"x": 362,
"y": 220,
"name": "gap-area",
"fill": "#111827",
"width": 44,
"height": 20
},
{
"type": "text",
"id": "B56Vrw",
"x": 360,
"y": 221,
"name": "s3gapT",
"fill": "#475569",
"content": "· · ·",
"fontFamily": "Inter",
"fontSize": 10,
"fontWeight": "normal"
},
{
"type": "ellipse",
"id": "ZaNFE",
"x": 245,
"y": 223,
"name": "s3hS1L",
"fill": "#FFFFFF",
"width": 14,
"height": 14,
"stroke": {
"thickness": 2,
"fill": "#F59E0B"
}
},
{
"type": "ellipse",
"id": "qsbKp",
"x": 355,
"y": 223,
"name": "s3hS1R",
"fill": "#FFFFFF",
"width": 14,
"height": 14,
"stroke": {
"thickness": 2,
"fill": "#F59E0B"
}
},
{
"type": "ellipse",
"id": "M9nZWU",
"x": 399,
"y": 223,
"name": "s3hS2L",
"fill": "#FFFFFF",
"width": 14,
"height": 14,
"stroke": {
"thickness": 2,
"fill": "#F59E0B"
}
},
{
"type": "ellipse",
"id": "A7JJSO",
"x": 509,
"y": 223,
"name": "s3hS2R",
"fill": "#FFFFFF",
"width": 14,
"height": 14,
"stroke": {
"thickness": 2,
"fill": "#F59E0B"
}
},
{
"type": "text",
"id": "zq3jw",
"x": 240,
"y": 250,
"name": "s3ts1",
"fill": "#FCD34D",
"content": "06:00",
"fontFamily": "Inter",
"fontSize": 11,
"fontWeight": "normal"
},
{
"type": "text",
"id": "jz9eB",
"x": 350,
"y": 250,
"name": "s3te1",
"fill": "#FCD34D",
"content": "11:00",
"fontFamily": "Inter",
"fontSize": 11,
"fontWeight": "normal"
},
{
"type": "text",
"id": "LjALr",
"x": 392,
"y": 250,
"name": "s3ts2",
"fill": "#FCD34D",
"content": "13:00",
"fontFamily": "Inter",
"fontSize": 11,
"fontWeight": "normal"
},
{
"type": "text",
"id": "T30s7",
"x": 503,
"y": 250,
"name": "s3te2",
"fill": "#FCD34D",
"content": "18:00",
"fontFamily": "Inter",
"fontSize": 11,
"fontWeight": "normal"
},
{
"type": "frame",
"id": "bEttq",
"x": 284,
"y": 208,
"name": "seg-badge-1",
"width": 44,
"height": 16,
"fill": "#3B82F6",
"cornerRadius": 8,
"layout": "none",
"children": [
{
"type": "text",
"id": "GVLSj",
"x": 10,
"y": 3,
"name": "s3badge1T",
"fill": "#FFFFFF",
"content": "段 1",
"fontFamily": "Inter",
"fontSize": 10,
"fontWeight": "normal"
}
]
},
{
"type": "frame",
"id": "dVz0W",
"x": 434,
"y": 208,
"name": "seg-badge-2",
"width": 44,
"height": 16,
"fill": "#10B981",
"cornerRadius": 8,
"layout": "none",
"children": [
{
"type": "text",
"id": "vtrlr",
"x": 10,
"y": 3,
"name": "s3badge2T",
"fill": "#FFFFFF",
"content": "段 2",
"fontFamily": "Inter",
"fontSize": 10,
"fontWeight": "normal"
}
]
},
{
"type": "rectangle",
"id": "BYzCa",
"x": 0,
"y": 312,
"name": "s3ann",
"fill": "#111827",
"width": 700,
"height": 68
},
{
"type": "rectangle",
"id": "b3QsS",
"x": 0,
"y": 312,
"name": "s3annDiv",
"fill": "#2D3B5E",
"width": 700,
"height": 1
},
{
"type": "text",
"id": "vuf9p",
"x": 24,
"y": 322,
"name": "s3ann1",
"fill": "#94A3B8",
"content": "• 悬停在时间条上,中间出现黄色分割线;点击即拆分为两段",
"fontFamily": "Inter",
"fontSize": 12,
"fontWeight": "normal"
},
{
"type": "text",
"id": "XJDsu",
"x": 24,
"y": 345,
"name": "s3ann2",
"fill": "#94A3B8",
"content": "• 每段可独立拖拽把手,互不干扰;段与段之间的空隙为未选区域",
"fontFamily": "Inter",
"fontSize": 12,
"fontWeight": "normal"
}
]
},
{
"type": "frame",
"id": "HCC4e",
"x": 0,
"y": 1270,
"name": "04 重叠检测",
"width": 700,
"height": 380,
"fill": "#0F1629",
"layout": "none",
"children": [
{
"type": "frame",
"id": "F3vKkJ",
"x": 0,
"y": 0,
"name": "header",
"width": 700,
"height": 48,
"fill": "#1A2240",
"layout": "none",
"children": [
{
"type": "text",
"id": "euOvg",
"x": 20,
"y": 14,
"name": "s4ht",
"fill": "#F1F5F9",
"content": "④ 重叠检测",
"fontFamily": "Inter",
"fontSize": 16,
"fontWeight": "700"
},
{
"type": "text",
"id": "pAqjs",
"x": 148,
"y": 17,
"name": "s4hst",
"fill": "#64748B",
"content": "— 时间交叉区域红色高亮,实时提示",
"fontFamily": "Inter",
"fontSize": 12,
"fontWeight": "normal"
}
]
},
{
"type": "rectangle",
"id": "X0P7pW",
"x": 0,
"y": 68,
"name": "s4rb1",
"fill": "#1E2845",
"width": 700,
"height": 88
},
{
"type": "rectangle",
"id": "fHaNq",
"x": 0,
"y": 156,
"name": "s4rb2",
"fill": "#1A2240",
"width": 700,
"height": 88
},
{
"type": "rectangle",
"id": "sfJlf",
"x": 0,
"y": 156,
"name": "s4rbDiv",
"fill": "#2D3B5E",
"width": 700,
"height": 1
},
{
"type": "rectangle",
"id": "L7ROO",
"x": 120,
"y": 48,
"name": "s4rulBg",
"fill": "#0C1525",
"width": 580,
"height": 20
},
{
"type": "rectangle",
"id": "ucRrK",
"x": 120,
"y": 48,
"name": "s4lbSep",
"fill": "#2D3B5E",
"width": 1,
"height": 200
},
{
"type": "text",
"id": "Bd7P3",
"x": 14,
"y": 100,
"name": "s4lb1",
"fill": "#CBD5E1",
"content": "任务 A",
"fontFamily": "Inter",
"fontSize": 13,
"fontWeight": "600"
},
{
"type": "text",
"id": "EbAUQ",
"x": 14,
"y": 118,
"name": "s4lb1id",
"fill": "#475569",
"content": "id: 001",
"fontFamily": "Inter",
"fontSize": 11,
"fontWeight": "normal"
},
{
"type": "text",
"id": "KCDGE",
"x": 14,
"y": 184,
"name": "s4lb2",
"fill": "#CBD5E1",
"content": "任务 B",
"fontFamily": "Inter",
"fontSize": 14,
"fontWeight": "600"
},
{
"type": "text",
"id": "U3UleB",
"x": 14,
"y": 202,
"name": "s4lb2id",
"fill": "#475569",
"content": "id: 002",
"fontFamily": "Inter",
"fontSize": 11,
"fontWeight": "normal"
},
{
"type": "text",
"id": "akUTy",
"x": 114,
"y": 52,
"name": "s4rt0",
"fill": "#475569",
"content": "0",
"fontFamily": "Inter",
"fontSize": 10,
"fontWeight": "normal"
},
{
"type": "text",
"id": "I6LXoW",
"x": 246,
"y": 52,
"name": "s4rt6",
"fill": "#475569",
"content": "06",
"fontFamily": "Inter",
"fontSize": 10,
"fontWeight": "normal"
},
{
"type": "text",
"id": "NUC5P",
"x": 334,
"y": 52,
"name": "s4rt10",
"fill": "#EF4444",
"content": "10",
"fontFamily": "Inter",
"fontSize": 10,
"fontWeight": "normal"
},
{
"type": "text",
"id": "HMKw2",
"x": 422,
"y": 52,
"name": "s4rt14",
"fill": "#EF4444",
"content": "14",
"fontFamily": "Inter",
"fontSize": 10,
"fontWeight": "normal"
},
{
"type": "text",
"id": "DdCg1",
"x": 510,
"y": 52,
"name": "s4rt18",
"fill": "#475569",
"content": "18",
"fontFamily": "Inter",
"fontSize": 10,
"fontWeight": "normal"
},
{
"type": "rectangle",
"cornerRadius": 14,
"id": "t1d2s",
"x": 252,
"y": 96,
"name": "bar-A",
"fill": {
"type": "gradient",
"gradientType": "linear",
"enabled": true,
"rotation": 90,
"size": {
"height": 1
},
"colors": [
{
"color": "#60A5FA",
"position": 0
},
{
"color": "#2563EB",
"position": 1
}
]
},
"width": 176,
"height": 28
},
{
"type": "rectangle",
"cornerRadius": 14,
"id": "w6deVi",
"x": 340,
"y": 184,
"name": "bar-B",
"fill": {
"type": "gradient",
"gradientType": "linear",
"enabled": true,
"rotation": 90,
"size": {
"height": 1
},
"colors": [
{
"color": "#34D399",
"position": 0
},
{
"color": "#059669",
"position": 1
}
]
},
"width": 176,
"height": 28
},
{
"type": "rectangle",
"id": "SRNs4",
"x": 340,
"y": 92,
"name": "overlap-zone",
"opacity": 0.2,
"fill": "#EF4444",
"width": 88,
"height": 128
},
{
"type": "rectangle",
"id": "mafdu",
"x": 340,
"y": 92,
"name": "s4ovBorderL",
"opacity": 0.8,
"fill": "#EF4444",
"width": 2,
"height": 128
},
{
"type": "rectangle",
"id": "rvPJC",
"x": 426,
"y": 92,
"name": "s4ovBorderR",
"opacity": 0.8,
"fill": "#EF4444",
"width": 2,
"height": 128
},
{
"type": "text",
"id": "x3SjiK",
"x": 336,
"y": 55,
"name": "s4ovTop",
"fill": "#EF4444",
"content": "⬇",
"fontFamily": "Inter",
"fontSize": 10,
"fontWeight": "normal"
},
{
"type": "text",
"id": "sLqTG",
"x": 418,
"y": 55,
"name": "s4ovBot",
"fill": "#EF4444",
"content": "⬇",
"fontFamily": "Inter",
"fontSize": 10,
"fontWeight": "normal"
},
{
"type": "ellipse",
"id": "rqQKP",
"x": 245,
"y": 103,
"name": "s4hlA1",
"fill": "#FFFFFF",
"width": 14,
"height": 14,
"stroke": {
"thickness": 2,
"fill": "#3B82F6"
}
},
{
"type": "ellipse",
"id": "sw1Hw",
"x": 421,
"y": 103,
"name": "s4hlA2",
"fill": "#FFFFFF",
"width": 14,
"height": 14,
"stroke": {
"thickness": 2,
"fill": "#3B82F6"
}
},
{
"type": "ellipse",
"id": "T46tYd",
"x": 333,
"y": 191,
"name": "s4hlB1",
"fill": "#FFFFFF",
"width": 14,
"height": 14,
"stroke": {
"thickness": 2,
"fill": "#10B981"
}
},
{
"type": "ellipse",
"id": "al4Lw",
"x": 509,
"y": 191,
"name": "s4hlB2",
"fill": "#FFFFFF",
"width": 14,
"height": 14,
"stroke": {
"thickness": 2,
"fill": "#10B981"
}
},
{
"type": "text",
"id": "diJ2T",
"x": 240,
"y": 130,
"name": "s4tA1",
"fill": "#60A5FA",
"content": "06:00",
"fontFamily": "Inter",
"fontSize": 11,
"fontWeight": "normal"
},
{
"type": "text",
"id": "MhXA2",
"x": 416,
"y": 130,
"name": "s4tA2",
"fill": "#60A5FA",
"content": "14:00",
"fontFamily": "Inter",
"fontSize": 11,
"fontWeight": "normal"
},
{
"type": "text",
"id": "lQ9ye",
"x": 328,
"y": 218,
"name": "s4tB1",
"fill": "#34D399",
"content": "10:00",
"fontFamily": "Inter",
"fontSize": 11,
"fontWeight": "normal"
},
{
"type": "text",
"id": "VswgH",
"x": 504,
"y": 218,
"name": "s4tB2",
"fill": "#34D399",
"content": "18:00",
"fontFamily": "Inter",
"fontSize": 11,
"fontWeight": "normal"
},
{
"type": "frame",
"id": "eohaA",
"x": 344,
"y": 165,
"name": "warning-badge",
"width": 80,
"height": 20,
"fill": "#EF4444",
"cornerRadius": 10,
"layout": "none",
"children": [
{
"type": "text",
"id": "frmmC",
"x": 8,
"y": 4,
"name": "s4warnT",
"fill": "#FFFFFF",
"content": "⚠ 重叠 4h",
"fontFamily": "Inter",
"fontSize": 11,
"fontWeight": "600"
}
]
},
{
"type": "frame",
"id": "FzZxD",
"x": 310,
"y": 240,
"name": "warning-tip",
"width": 216,
"height": 22,
"fill": "#1E293B",
"cornerRadius": 4,
"stroke": {
"thickness": 1,
"fill": "#EF4444"
},
"layout": "none",
"children": [
{
"type": "text",
"id": "mhPJJ",
"x": 10,
"y": 5,
"name": "s4warnTipT",
"fill": "#EF4444",
"content": "10:00 — 14:00 时间段重叠 (4小时)",
"fontFamily": "Inter",
"fontSize": 11,
"fontWeight": "normal"
}
]
},
{
"type": "rectangle",
"id": "YZ23F",
"x": 0,
"y": 265,
"name": "s4ann",
"fill": "#111827",
"width": 700,
"height": 115
},
{
"type": "rectangle",
"id": "fMbYz",
"x": 0,
"y": 265,
"name": "s4annDiv",
"fill": "#2D3B5E",
"width": 700,
"height": 1
},
{
"type": "text",
"id": "URLSd",
"x": 24,
"y": 278,
"name": "s4ann1",
"fill": "#94A3B8",
"content": "• 重叠区域以半透明红色背景 + 红色边框实时高亮显示",
"fontFamily": "Inter",
"fontSize": 12,
"fontWeight": "normal"
},
{
"type": "text",
"id": "XJolK",
"x": 24,
"y": 302,
"name": "s4ann2",
"fill": "#94A3B8",
"content": "• 重叠带上方显示 ⚠ 红色徽章,提示重叠总时长",
"fontFamily": "Inter",
"fontSize": 12,
"fontWeight": "normal"
},
{
"type": "text",
"id": "tRf9v",
"x": 24,
"y": 326,
"name": "s4ann3",
"fill": "#94A3B8",
"content": "• 拖拽任意一条条目把手后,检测实时刷新;可配置是否允许重叠",
"fontFamily": "Inter",
"fontSize": 12,
"fontWeight": "normal"
}
]
},
{
"type": "frame",
"id": "sicM3",
"x": 750,
"y": 1270,
"name": "05 时间输入",
"width": 580,
"height": 560,
"fill": "#0F1629",
"layout": "none",
"children": [
{
"type": "frame",
"id": "rGTMZ",
"x": 0,
"y": 0,
"name": "header",
"width": 580,
"height": 48,
"fill": "#1A2240",
"layout": "none",
"children": [
{
"type": "text",
"id": "KceiO",
"x": 20,
"y": 14,
"name": "s5ht",
"fill": "#F1F5F9",
"content": "⑤ 时间精确输入",
"fontFamily": "Inter",
"fontSize": 16,
"fontWeight": "700"
},
{
"type": "text",
"id": "DYc52",
"x": 178,
"y": 17,
"name": "s5hst",
"fill": "#64748B",
"content": "— 点击时间标签弹出输入弹窗",
"fontFamily": "Inter",
"fontSize": 12,
"fontWeight": "normal"
}
]
},
{
"type": "rectangle",
"id": "r1Uo2",
"x": 0,
"y": 48,
"name": "s5bgLayer",
"fill": "#0A1020",
"width": 580,
"height": 392
},
{
"type": "rectangle",
"id": "sNF7F",
"x": 0,
"y": 68,
"name": "s5ctxBar",
"fill": "#1E2845",
"width": 580,
"height": 64
},
{
"type": "text",
"id": "eZU8h",
"x": 14,
"y": 88,
"name": "s5ctxLbl",
"fill": "#CBD5E1",
"content": "任务 A",
"fontFamily": "Inter",
"fontSize": 12,
"fontWeight": "normal"
},
{
"type": "rectangle",
"cornerRadius": 12,
"id": "J7b93S",
"x": 130,
"y": 84,
"name": "s5ctxBar2",
"fill": {
"type": "gradient",
"gradientType": "linear",
"enabled": true,
"rotation": 90,
"size": {
"height": 1
},
"colors": [
{
"color": "#60A5FA",
"position": 0
},
{
"color": "#2563EB",
"position": 1
}
]
},
"width": 220,
"height": 24
},
{
"type": "frame",
"id": "t8Kzcy",
"x": 120,
"y": 102,
"name": "click-time-label",
"width": 54,
"height": 18,
"fill": "#1E2845",
"cornerRadius": 4,
"stroke": {
"thickness": 1.5,
"fill": "#3B82F6"
},
"layout": "none",
"children": [
{
"type": "text",
"id": "kYdfF",
"x": 8,
"y": 3,
"name": "s5ctxT1T",
"fill": "#60A5FA",
"content": "06:00 ▾",
"fontFamily": "Inter",
"fontSize": 10,
"fontWeight": "normal"
}
]
},
{
"type": "text",
"id": "ZYfTY",
"x": 179,
"y": 122,
"name": "s5clickArrow",
"fill": "#3B82F6",
"content": "↑ 点击此处",
"fontFamily": "Inter",
"fontSize": 10,
"fontWeight": "normal"
},
{
"type": "frame",
"id": "X6zJw",
"x": 30,
"y": 148,
"name": "input-dialog",
"width": 520,
"height": 340,
"fill": "#1A2240",
"cornerRadius": 12,
"stroke": {
"thickness": 1.5,
"fill": "#3B82F6"
},
"effect": {
"type": "shadow",
"shadowType": "outer",
"color": "#000000AA",
"offset": {
"x": 0,
"y": 8
},
"blur": 32
},
"layout": "none",
"children": [
{
"type": "text",
"id": "f77HZ",
"x": 24,
"y": 24,
"name": "s5dlgTitle",
"fill": "#F1F5F9",
"content": "编辑时间范围",
"fontFamily": "Inter",
"fontSize": 16,
"fontWeight": "700"
},
{
"type": "text",
"id": "N7l6d",
"x": 24,
"y": 48,
"name": "s5dlgSubT",
"fill": "#64748B",
"content": "任务 A · id: 001",
"fontFamily": "Inter",
"fontSize": 12,
"fontWeight": "normal"
},
{
"type": "rectangle",
"id": "P4or6",
"x": 0,
"y": 72,
"name": "s5dlgDiv1",
"fill": "#2D3B5E",
"width": 520,
"height": 1
},
{
"type": "text",
"id": "YP6bX",
"x": 24,
"y": 90,
"name": "s5startLbl",
"fill": "#94A3B8",
"content": "开始时间",
"fontFamily": "Inter",
"fontSize": 12,
"fontWeight": "normal"
},
{
"type": "frame",
"id": "jVzhf",
"x": 24,
"y": 110,
"name": "start-field",
"width": 232,
"height": 60,
"fill": "#0F1629",
"cornerRadius": 8,
"stroke": {
"thickness": 2,
"fill": "#3B82F6"
},
"layout": "none",
"children": [
{
"type": "text",
"id": "nbNxq",
"x": 16,
"y": 30,
"name": "s5startVal",
"fill": "#60A5FA",
"content": "06 : 00 : 00",
"fontFamily": "Inter",
"fontSize": 16,
"fontWeight": "700"
},
{
"type": "text",
"id": "fK1LJ",
"x": 12,
"y": 8,
"name": "s5startDate",
"fill": "#6B7280",
"content": "2026-05-15",
"fontFamily": "Inter",
"fontSize": 10,
"fontWeight": "normal"
}
]
},
{
"type": "text",
"id": "RZIlg",
"x": 268,
"y": 90,
"name": "s5endLbl",
"fill": "#94A3B8",
"content": "结束时间",
"fontFamily": "Inter",
"fontSize": 12,
"fontWeight": "normal"
},
{
"type": "frame",
"id": "rQPpI",
"x": 268,
"y": 110,
"name": "end-field",
"width": 232,
"height": 60,
"fill": "#0F1629",
"cornerRadius": 8,
"stroke": {
"thickness": 1,
"fill": "#2D3B5E"
},
"layout": "none",
"children": [
{
"type": "text",
"id": "StRoo",
"x": 16,
"y": 30,
"name": "s5endVal",
"fill": "#94A3B8",
"content": "14 : 00 : 00",
"fontFamily": "Inter",
"fontSize": 16,
"fontWeight": "700"
},
{
"type": "text",
"id": "fz1Oc",
"x": 12,
"y": 8,
"name": "s5endDate",
"fill": "#6B7280",
"content": "2026-05-15",
"fontFamily": "Inter",
"fontSize": 10,
"fontWeight": "normal"
}
]
},
{
"type": "text",
"id": "S69Sf",
"x": 24,
"y": 188,
"name": "s5durLbl",
"fill": "#94A3B8",
"content": "持续时长",
"fontFamily": "Inter",
"fontSize": 12,
"fontWeight": "normal"
},
{
"type": "text",
"id": "JmoDg",
"x": 120,
"y": 188,
"name": "s5durVal",
"fill": "#34D399",
"content": "8 小时 0 分钟 0 秒",
"fontFamily": "Inter",
"fontSize": 12,
"fontWeight": "600"
},
{
"type": "text",
"id": "itC7T",
"x": 24,
"y": 214,
"name": "s5splitLbl",
"fill": "#94A3B8",
"content": "拆分方式",
"fontFamily": "Inter",
"fontSize": 12,
"fontWeight": "normal"
},
{
"type": "frame",
"id": "iVVDX",
"x": 24,
"y": 234,
"name": "opt-none",
"width": 96,
"height": 28,
"fill": "#3B82F6",
"cornerRadius": 14,
"layout": "none",
"children": [
{
"type": "text",
"id": "Zf2wZ",
"x": 22,
"y": 7,
"name": "s5opt1T",
"fill": "#FFFFFF",
"content": "不拆分",
"fontFamily": "Inter",
"fontSize": 12,
"fontWeight": "normal"
}
]
},
{
"type": "frame",
"id": "dwuzG",
"x": 128,
"y": 234,
"name": "opt-2",
"width": 96,
"height": 28,
"fill": "#1E2845",
"cornerRadius": 14,
"stroke": {
"thickness": 1,
"fill": "#2D3B5E"
},
"layout": "none",
"children": [
{
"type": "text",
"id": "e3lZn",
"x": 14,
"y": 7,
"name": "s5opt2T",
"fill": "#94A3B8",
"content": "均分 2 段",
"fontFamily": "Inter",
"fontSize": 12,
"fontWeight": "normal"
}
]
},
{
"type": "frame",
"id": "FGdoT",
"x": 232,
"y": 234,
"name": "opt-3",
"width": 96,
"height": 28,
"fill": "#1E2845",
"cornerRadius": 14,
"stroke": {
"thickness": 1,
"fill": "#2D3B5E"
},
"layout": "none",
"children": [
{
"type": "text",
"id": "TOMrU",
"x": 14,
"y": 7,
"name": "s5opt3T",
"fill": "#94A3B8",
"content": "均分 3 段",
"fontFamily": "Inter",
"fontSize": 12,
"fontWeight": "normal"
}
]
},
{
"type": "rectangle",
"id": "pgLuN",
"x": 0,
"y": 278,
"name": "s5dlgDiv2",
"fill": "#2D3B5E",
"width": 520,
"height": 1
},
{
"type": "frame",
"id": "RT7IT",
"x": 336,
"y": 290,
"name": "cancel-btn",
"width": 80,
"height": 36,
"fill": "#1E2845",
"cornerRadius": 8,
"stroke": {
"thickness": 1,
"fill": "#2D3B5E"
},
"layout": "none",
"children": [
{
"type": "text",
"id": "vEssi",
"x": 22,
"y": 10,
"name": "s5cancelT",
"fill": "#94A3B8",
"content": "取消",
"fontFamily": "Inter",
"fontSize": 14,
"fontWeight": "normal"
}
]
},
{
"type": "frame",
"id": "D7rycT",
"x": 424,
"y": 290,
"name": "confirm-btn",
"width": 80,
"height": 36,
"fill": "#3B82F6",
"cornerRadius": 8,
"layout": "none",
"children": [
{
"type": "text",
"id": "CrCZG",
"x": 22,
"y": 10,
"name": "s5confirmT",
"fill": "#FFFFFF",
"content": "确认",
"fontFamily": "Inter",
"fontSize": 14,
"fontWeight": "600"
}
]
}
]
},
{
"type": "rectangle",
"id": "C4Mnf",
"x": 0,
"y": 496,
"name": "s5ann",
"fill": "#111827",
"width": 580,
"height": 64
},
{
"type": "rectangle",
"id": "b8bRlH",
"x": 0,
"y": 496,
"name": "s5annDiv",
"fill": "#2D3B5E",
"width": 580,
"height": 1
},
{
"type": "text",
"id": "KtVJ2",
"x": 24,
"y": 508,
"name": "s5ann1",
"fill": "#94A3B8",
"content": "• 点击时间条上的开始/结束时间标签,弹出此对话框",
"fontFamily": "Inter",
"fontSize": 12,
"fontWeight": "normal"
},
{
"type": "text",
"id": "rIsib",
"x": 24,
"y": 530,
"name": "s5ann2",
"fill": "#94A3B8",
"content": "• 支持精确到秒的时间输入;日期跨越多天时自动识别跨天",
"fontFamily": "Inter",
"fontSize": 12,
"fontWeight": "normal"
}
]
}
]
}
\ No newline at end of file
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>time-line</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
{
"name": "time-line",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vue-tsc -b && vite build",
"preview": "vite preview"
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.2",
"element-plus": "2.9.1",
"vue": "^3.5.34"
},
"devDependencies": {
"@types/node": "^24.12.3",
"@vitejs/plugin-vue": "^6.0.6",
"@vue/tsconfig": "^0.9.1",
"typescript": "~6.0.2",
"vite": "^8.0.12",
"vue-tsc": "^3.2.8"
}
}
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
dependencies:
'@element-plus/icons-vue':
specifier: ^2.3.2
version: 2.3.2(vue@3.5.34(typescript@6.0.3))
element-plus:
specifier: 2.9.1
version: 2.9.1(vue@3.5.34(typescript@6.0.3))
vue:
specifier: ^3.5.34
version: 3.5.34(typescript@6.0.3)
devDependencies:
'@types/node':
specifier: ^24.12.3
version: 24.12.4
'@vitejs/plugin-vue':
specifier: ^6.0.6
version: 6.0.7(vite@8.0.13(@types/node@24.12.4))(vue@3.5.34(typescript@6.0.3))
'@vue/tsconfig':
specifier: ^0.9.1
version: 0.9.1(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3))
typescript:
specifier: ~6.0.2
version: 6.0.3
vite:
specifier: ^8.0.12
version: 8.0.13(@types/node@24.12.4)
vue-tsc:
specifier: ^3.2.8
version: 3.2.9(typescript@6.0.3)
packages:
'@babel/helper-string-parser@7.27.1':
resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==}
engines: {node: '>=6.9.0'}
'@babel/helper-validator-identifier@7.28.5':
resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==}
engines: {node: '>=6.9.0'}
'@babel/parser@7.29.3':
resolution: {integrity: sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==}
engines: {node: '>=6.0.0'}
hasBin: true
'@babel/types@7.29.0':
resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==}
engines: {node: '>=6.9.0'}
'@ctrl/tinycolor@3.6.1':
resolution: {integrity: sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==}
engines: {node: '>=10'}
'@element-plus/icons-vue@2.3.2':
resolution: {integrity: sha512-OzIuTaIfC8QXEPmJvB4Y4kw34rSXdCJzxcD1kFStBvr8bK6X1zQAYDo0CNMjojnfTqRQCJ0I7prlErcoRiET2A==}
peerDependencies:
vue: ^3.2.0
'@emnapi/core@1.10.0':
resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==}
'@emnapi/runtime@1.10.0':
resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==}
'@emnapi/wasi-threads@1.2.1':
resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==}
'@floating-ui/core@1.7.5':
resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==}
'@floating-ui/dom@1.7.6':
resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==}
'@floating-ui/utils@0.2.11':
resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==}
'@jridgewell/sourcemap-codec@1.5.5':
resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
'@napi-rs/wasm-runtime@1.1.4':
resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==}
peerDependencies:
'@emnapi/core': ^1.7.1
'@emnapi/runtime': ^1.7.1
'@oxc-project/types@0.130.0':
resolution: {integrity: sha512-ibD2usx9JRu7f5pu2tMKMI4cpA4NgXJQoYRP4pQ7Pxmn1l6k/53qWtQWZayhYy3X4QZkt90Ot+mJEaeXouio6Q==}
'@rolldown/binding-android-arm64@1.0.1':
resolution: {integrity: sha512-fJI3I0r3C3Oj/zdBCpaCmBRZYf07xpaq4yCfDDoSFm+beWNzbIl26puW8RraUdugoJw/95zerNOn6jasAhzSmg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [android]
'@rolldown/binding-darwin-arm64@1.0.1':
resolution: {integrity: sha512-cKnAhWEsV7TPcA/5EAteDp6KcJZBQ2G+BqE7zayMMi7kMvwRsbv7WT9aOnn0WNl4SKEIf43vjS31iUPu80nzXg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [darwin]
'@rolldown/binding-darwin-x64@1.0.1':
resolution: {integrity: sha512-YKrVwQjIRBPo+5G/u03wGjbdy4q7pyzCe93DK9VJ7zkVmeg8LJ7GbgsiHWdR4xSoe4CAXRD7Bcjgbtr64bkXNg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [darwin]
'@rolldown/binding-freebsd-x64@1.0.1':
resolution: {integrity: sha512-z/oBsREo46SsFqBwYtFe0kpJeBijAT48O/WXLI4suiCLBkr03RTtTJMCzSdDd2znlh8VJizL09XVkQgk8IZonw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [freebsd]
'@rolldown/binding-linux-arm-gnueabihf@1.0.1':
resolution: {integrity: sha512-ik8q7GM11zxvYxFc2PeDcT6TBvhCQMaUxfph/M5l9sKuTs/Sjg3L+Byw0F7w0ZVLBZmx30P+gG0ECzzN+MFcmQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm]
os: [linux]
'@rolldown/binding-linux-arm64-gnu@1.0.1':
resolution: {integrity: sha512-QoSx2EkyrrdZ6kcyE8stqZ62t0Yra8Fs5ia9lOxJrh6TMQJK7gQKmscdTHf7pOXKREKrVwOtJcQG3qVSfc866A==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@rolldown/binding-linux-arm64-musl@1.0.1':
resolution: {integrity: sha512-uwNwFpwKeNiZawfAWBgg0VIztPTV3ihhh1vV334h9ivnNLorxnQMU6Fz8wG1Zb4Qh9LC1/MkcyT3YlDXG3Rsgg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [musl]
'@rolldown/binding-linux-ppc64-gnu@1.0.1':
resolution: {integrity: sha512-zY1bul7OWr7DFBiJ++wofXvnr8B45ce3QsQUhKrIhXsygAh7bTkwyeM1bi1a2g5C/yC/N8TZyGDEoMfm/l9mpg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@rolldown/binding-linux-s390x-gnu@1.0.1':
resolution: {integrity: sha512-0frlsT/f4Ft6I7SMESTKnF3cZsdicQn1dCMkF/jT9wDLE+gGoiQfv1nmT9e+s7s/fekvvy6tZM2jHvI2tkbJDQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@rolldown/binding-linux-x64-gnu@1.0.1':
resolution: {integrity: sha512-XABVmGp9Tg0WspTVvwduTc4fpqy6JnAUrSQe6OuyqD/03nI7r0O9OWUkMIwFrjKAIqolvqoA4ZrJppgwE0Gxmw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [glibc]
'@rolldown/binding-linux-x64-musl@1.0.1':
resolution: {integrity: sha512-bV4fzswuzVcKD90o/VM6QqKxnxlDq0g2BISDLNVmxrnhpv1DDbyPhCIjYfvzYLV+MvkKKnQt2Q6AO86SEBULUQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [musl]
'@rolldown/binding-openharmony-arm64@1.0.1':
resolution: {integrity: sha512-/Mh0Zhq3OP7fVs0kcQHZP6lZEthMGTaSf8UBQYSFEZDWGXXlEC+nJ6EqenaK2t4LBXMe3A+K/G2BVXXdtOr4PQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [openharmony]
'@rolldown/binding-wasm32-wasi@1.0.1':
resolution: {integrity: sha512-+1xc9X45l8ufsBAm6Gjvx2qDRIY9lTVt0cgWNcJ+1gdhXvkbxePA60yRTwSTuXL09CMhyJmjpV7E3NoyxbqFQQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [wasm32]
'@rolldown/binding-win32-arm64-msvc@1.0.1':
resolution: {integrity: sha512-1D+UqZdfnuR+Jy1GgMJwi85bD40H21uNmOPRWQhw4oRSuolZ/B5rixZ45DK2KXOTCvmVCecauWgEhbw8bI7tOw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [win32]
'@rolldown/binding-win32-x64-msvc@1.0.1':
resolution: {integrity: sha512-INAycaWuhlOK3wk4mRHGsdgwYWmd9cChdPdE9bwWmy6rn9VqVNYNFGhOdXrofXUxwHIncSiPNb8tNm8knDVIeQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [win32]
'@rolldown/pluginutils@1.0.1':
resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==}
'@sxzz/popperjs-es@2.11.8':
resolution: {integrity: sha512-wOwESXvvED3S8xBmcPWHs2dUuzrE4XiZeFu7e1hROIJkm02a49N120pmOXxY33sBb6hArItm5W5tcg1cBtV+HQ==}
'@tybys/wasm-util@0.10.2':
resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==}
'@types/lodash-es@4.17.12':
resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==}
'@types/lodash@4.17.24':
resolution: {integrity: sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==}
'@types/node@24.12.4':
resolution: {integrity: sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA==}
'@types/web-bluetooth@0.0.16':
resolution: {integrity: sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ==}
'@vitejs/plugin-vue@6.0.7':
resolution: {integrity: sha512-km+p+XdSz9Sxm5rqUbqcSfZYaAniKxWBj1KURl+Jr7UaPvvX7BmaWMdP69I5rrFDeQGyxAG7NXdc57vz+snhWg==}
engines: {node: ^20.19.0 || >=22.12.0}
peerDependencies:
vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0
vue: ^3.2.25
'@volar/language-core@2.4.28':
resolution: {integrity: sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==}
'@volar/source-map@2.4.28':
resolution: {integrity: sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==}
'@volar/typescript@2.4.28':
resolution: {integrity: sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==}
'@vue/compiler-core@3.5.34':
resolution: {integrity: sha512-s9cLyK5mLcvZ4Agva5QgRsQyLKvts9WbU9DB6NqiZkkGEdwmcEiylj5Jbwkp680drF/NNCV8OlAJSe+yMLxaJw==}
'@vue/compiler-dom@3.5.34':
resolution: {integrity: sha512-EbF/T++k0e2MMZlJsBhzK8Sgwt0HcIPOhzn1CTB/lv6sQcyk+OWf8YeiLxZp3ro7MbbLcAfAJ6sEvjFWuNgUCw==}
'@vue/compiler-sfc@3.5.34':
resolution: {integrity: sha512-D/ihr6uZeIt6r+pVZf46RWT1fAsLFMbUP7k8G1VkiiWexriED9GrX3echHd4Abbt17zjlfiFJ8z7a3BxZOPNjg==}
'@vue/compiler-ssr@3.5.34':
resolution: {integrity: sha512-cDtTHKibkThKGHH1SP+WdccquNRYQDFH6rRjQCqT9G2ltFAfoR5pUftpab/z+aM5mW9HLLVQW7hfKKQe/1GBeQ==}
'@vue/language-core@3.2.9':
resolution: {integrity: sha512-ie0ojt/0fU/GfIogh+zgHbaYRPlt9S+cLOxcWwF7nTSFh897BVgnFKL2byT4kpp1mlqYWZ2psGwSniyE2xsxYw==}
'@vue/reactivity@3.5.34':
resolution: {integrity: sha512-y9XDjCEuBp+98k+UL5dbYkh57AHU4o6cxZedOPXw3bmrZZYLQsVHguGurq7hVrPCSrQtrnz1f9dssyFr+dMXfQ==}
'@vue/runtime-core@3.5.34':
resolution: {integrity: sha512-mKeBYvu8tcMSLhypAHBmriUFfWXKTCF/23Z4jiCoYK3UtWepkliViNLuR90V9XOyD62mUxs9p1jsrpK3CCGIzw==}
'@vue/runtime-dom@3.5.34':
resolution: {integrity: sha512-e8kZzERmCwUnBRVsgSQlAfrfU2rGoy0FFKPBXSlfEjc/O3KfA7QP0t1/2ZylrbchjmIKB4dPTd07A6WPr0eOrg==}
'@vue/server-renderer@3.5.34':
resolution: {integrity: sha512-nHxmJoTrKsmrkbILRhkC9gY1G3moZbJTqCzDd7DOOzG5KH9oeJ0Unqrff5f9v0pW//jES05ZkJcNtfE8JjOIew==}
peerDependencies:
vue: 3.5.34
'@vue/shared@3.5.34':
resolution: {integrity: sha512-24uqU4OIiX29ryC3MeWid/Xf2fa2EFRUVLb77nRhk+UrTVrh/XiGtFAFmJBAtBRbjwNdsPRP+jj/OL27Eg1NDA==}
'@vue/tsconfig@0.9.1':
resolution: {integrity: sha512-buvjm+9NzLCJL29KY1j1991YYJ5e6275OiK+G4jtmfIb+z4POywbdm0wXusT9adVWqe0xqg70TbI7+mRx4uU9w==}
peerDependencies:
typescript: '>= 5.8'
vue: ^3.4.0
peerDependenciesMeta:
typescript:
optional: true
vue:
optional: true
'@vueuse/core@9.13.0':
resolution: {integrity: sha512-pujnclbeHWxxPRqXWmdkKV5OX4Wk4YeK7wusHqRwU0Q7EFusHoqNA/aPhB6KCh9hEqJkLAJo7bb0Lh9b+OIVzw==}
'@vueuse/metadata@9.13.0':
resolution: {integrity: sha512-gdU7TKNAUVlXXLbaF+ZCfte8BjRJQWPCa2J55+7/h+yDtzw3vOoGQDRXzI6pyKyo6bXFT5/QoPE4hAknExjRLQ==}
'@vueuse/shared@9.13.0':
resolution: {integrity: sha512-UrnhU+Cnufu4S6JLCPZnkWh0WwZGUp72ktOF2DFptMlOs3TOdVv8xJN53zhHGARmVOsz5KqOls09+J1NR6sBKw==}
alien-signals@3.2.1:
resolution: {integrity: sha512-I8FjmltrfnDFoZedi5CG8DghVYNhzb/Ijluz7tCSJH0xpd0484Kowhbb1XDYOxfJpU1p5wnM2X54dA+IfGyD1g==}
async-validator@4.2.5:
resolution: {integrity: sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==}
csstype@3.2.3:
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
dayjs@1.11.20:
resolution: {integrity: sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==}
detect-libc@2.1.2:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'}
element-plus@2.9.1:
resolution: {integrity: sha512-9Agqf/jt4Ugk7EZ6C5LME71sgkvauPCsnvJN12Xid2XVobjufxMGpRE4L7pS4luJMOmFAH3J0NgYEGZT5r+NDg==}
peerDependencies:
vue: ^3.2.0
entities@7.0.1:
resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==}
engines: {node: '>=0.12'}
escape-html@1.0.3:
resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==}
estree-walker@2.0.2:
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
fdir@6.5.0:
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
engines: {node: '>=12.0.0'}
peerDependencies:
picomatch: ^3 || ^4
peerDependenciesMeta:
picomatch:
optional: true
fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
lightningcss-android-arm64@1.32.0:
resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [android]
lightningcss-darwin-arm64@1.32.0:
resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [darwin]
lightningcss-darwin-x64@1.32.0:
resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [darwin]
lightningcss-freebsd-x64@1.32.0:
resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [freebsd]
lightningcss-linux-arm-gnueabihf@1.32.0:
resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==}
engines: {node: '>= 12.0.0'}
cpu: [arm]
os: [linux]
lightningcss-linux-arm64-gnu@1.32.0:
resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
libc: [glibc]
lightningcss-linux-arm64-musl@1.32.0:
resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
libc: [musl]
lightningcss-linux-x64-gnu@1.32.0:
resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
libc: [glibc]
lightningcss-linux-x64-musl@1.32.0:
resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
libc: [musl]
lightningcss-win32-arm64-msvc@1.32.0:
resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [win32]
lightningcss-win32-x64-msvc@1.32.0:
resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [win32]
lightningcss@1.32.0:
resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==}
engines: {node: '>= 12.0.0'}
lodash-es@4.18.1:
resolution: {integrity: sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==}
lodash-unified@1.0.3:
resolution: {integrity: sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==}
peerDependencies:
'@types/lodash-es': '*'
lodash: '*'
lodash-es: '*'
lodash@4.18.1:
resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==}
magic-string@0.30.21:
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
memoize-one@6.0.0:
resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==}
muggle-string@0.4.1:
resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==}
nanoid@3.3.12:
resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
normalize-wheel-es@1.2.0:
resolution: {integrity: sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==}
path-browserify@1.0.1:
resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==}
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
picomatch@4.0.4:
resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==}
engines: {node: '>=12'}
postcss@8.5.14:
resolution: {integrity: sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==}
engines: {node: ^10 || ^12 || >=14}
rolldown@1.0.1:
resolution: {integrity: sha512-X0KQHljNnEkWNqqiz9zJrGunh1B0HgOxLXvnFpCOcadzcy5qohZ3tqMEUg00vncoRovXuK3ZqCT9KnnKzoInFQ==}
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
source-map-js@1.2.1:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'}
tinyglobby@0.2.16:
resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==}
engines: {node: '>=12.0.0'}
tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
typescript@6.0.3:
resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==}
engines: {node: '>=14.17'}
hasBin: true
undici-types@7.16.0:
resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==}
vite@8.0.13:
resolution: {integrity: sha512-MFtjBYgzmSxmgA4RAfjIyXWpGe1oALnjgUTzzV7QLx/TKxCzjtMH6Fd9/eVK+5Fg1qNoz5VAwsmMs/NofrmJvw==}
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
peerDependencies:
'@types/node': ^20.19.0 || >=22.12.0
'@vitejs/devtools': ^0.1.18
esbuild: ^0.27.0 || ^0.28.0
jiti: '>=1.21.0'
less: ^4.0.0
sass: ^1.70.0
sass-embedded: ^1.70.0
stylus: '>=0.54.8'
sugarss: ^5.0.0
terser: ^5.16.0
tsx: ^4.8.1
yaml: ^2.4.2
peerDependenciesMeta:
'@types/node':
optional: true
'@vitejs/devtools':
optional: true
esbuild:
optional: true
jiti:
optional: true
less:
optional: true
sass:
optional: true
sass-embedded:
optional: true
stylus:
optional: true
sugarss:
optional: true
terser:
optional: true
tsx:
optional: true
yaml:
optional: true
vscode-uri@3.1.0:
resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==}
vue-demi@0.14.10:
resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==}
engines: {node: '>=12'}
hasBin: true
peerDependencies:
'@vue/composition-api': ^1.0.0-rc.1
vue: ^3.0.0-0 || ^2.6.0
peerDependenciesMeta:
'@vue/composition-api':
optional: true
vue-tsc@3.2.9:
resolution: {integrity: sha512-qm8/nbo+9eZc1SCndm9wT+gq23pM+wRIdHY0wjm83B3lIginHTwcdrLUyTrKjDWXbMVNjKegNrnymhpdqnCL3A==}
hasBin: true
peerDependencies:
typescript: '>=5.0.0'
vue@3.5.34:
resolution: {integrity: sha512-WdLBG9gm02OgJIG9axd5Hpx0TFLdzVgfG2evFFu8Rur5O/IoGc5cMjnjh3tPL6GnRGsYvUhBSKVPYVcxRKpMCA==}
peerDependencies:
typescript: '*'
peerDependenciesMeta:
typescript:
optional: true
snapshots:
'@babel/helper-string-parser@7.27.1': {}
'@babel/helper-validator-identifier@7.28.5': {}
'@babel/parser@7.29.3':
dependencies:
'@babel/types': 7.29.0
'@babel/types@7.29.0':
dependencies:
'@babel/helper-string-parser': 7.27.1
'@babel/helper-validator-identifier': 7.28.5
'@ctrl/tinycolor@3.6.1': {}
'@element-plus/icons-vue@2.3.2(vue@3.5.34(typescript@6.0.3))':
dependencies:
vue: 3.5.34(typescript@6.0.3)
'@emnapi/core@1.10.0':
dependencies:
'@emnapi/wasi-threads': 1.2.1
tslib: 2.8.1
optional: true
'@emnapi/runtime@1.10.0':
dependencies:
tslib: 2.8.1
optional: true
'@emnapi/wasi-threads@1.2.1':
dependencies:
tslib: 2.8.1
optional: true
'@floating-ui/core@1.7.5':
dependencies:
'@floating-ui/utils': 0.2.11
'@floating-ui/dom@1.7.6':
dependencies:
'@floating-ui/core': 1.7.5
'@floating-ui/utils': 0.2.11
'@floating-ui/utils@0.2.11': {}
'@jridgewell/sourcemap-codec@1.5.5': {}
'@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)':
dependencies:
'@emnapi/core': 1.10.0
'@emnapi/runtime': 1.10.0
'@tybys/wasm-util': 0.10.2
optional: true
'@oxc-project/types@0.130.0': {}
'@rolldown/binding-android-arm64@1.0.1':
optional: true
'@rolldown/binding-darwin-arm64@1.0.1':
optional: true
'@rolldown/binding-darwin-x64@1.0.1':
optional: true
'@rolldown/binding-freebsd-x64@1.0.1':
optional: true
'@rolldown/binding-linux-arm-gnueabihf@1.0.1':
optional: true
'@rolldown/binding-linux-arm64-gnu@1.0.1':
optional: true
'@rolldown/binding-linux-arm64-musl@1.0.1':
optional: true
'@rolldown/binding-linux-ppc64-gnu@1.0.1':
optional: true
'@rolldown/binding-linux-s390x-gnu@1.0.1':
optional: true
'@rolldown/binding-linux-x64-gnu@1.0.1':
optional: true
'@rolldown/binding-linux-x64-musl@1.0.1':
optional: true
'@rolldown/binding-openharmony-arm64@1.0.1':
optional: true
'@rolldown/binding-wasm32-wasi@1.0.1':
dependencies:
'@emnapi/core': 1.10.0
'@emnapi/runtime': 1.10.0
'@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)
optional: true
'@rolldown/binding-win32-arm64-msvc@1.0.1':
optional: true
'@rolldown/binding-win32-x64-msvc@1.0.1':
optional: true
'@rolldown/pluginutils@1.0.1': {}
'@sxzz/popperjs-es@2.11.8': {}
'@tybys/wasm-util@0.10.2':
dependencies:
tslib: 2.8.1
optional: true
'@types/lodash-es@4.17.12':
dependencies:
'@types/lodash': 4.17.24
'@types/lodash@4.17.24': {}
'@types/node@24.12.4':
dependencies:
undici-types: 7.16.0
'@types/web-bluetooth@0.0.16': {}
'@vitejs/plugin-vue@6.0.7(vite@8.0.13(@types/node@24.12.4))(vue@3.5.34(typescript@6.0.3))':
dependencies:
'@rolldown/pluginutils': 1.0.1
vite: 8.0.13(@types/node@24.12.4)
vue: 3.5.34(typescript@6.0.3)
'@volar/language-core@2.4.28':
dependencies:
'@volar/source-map': 2.4.28
'@volar/source-map@2.4.28': {}
'@volar/typescript@2.4.28':
dependencies:
'@volar/language-core': 2.4.28
path-browserify: 1.0.1
vscode-uri: 3.1.0
'@vue/compiler-core@3.5.34':
dependencies:
'@babel/parser': 7.29.3
'@vue/shared': 3.5.34
entities: 7.0.1
estree-walker: 2.0.2
source-map-js: 1.2.1
'@vue/compiler-dom@3.5.34':
dependencies:
'@vue/compiler-core': 3.5.34
'@vue/shared': 3.5.34
'@vue/compiler-sfc@3.5.34':
dependencies:
'@babel/parser': 7.29.3
'@vue/compiler-core': 3.5.34
'@vue/compiler-dom': 3.5.34
'@vue/compiler-ssr': 3.5.34
'@vue/shared': 3.5.34
estree-walker: 2.0.2
magic-string: 0.30.21
postcss: 8.5.14
source-map-js: 1.2.1
'@vue/compiler-ssr@3.5.34':
dependencies:
'@vue/compiler-dom': 3.5.34
'@vue/shared': 3.5.34
'@vue/language-core@3.2.9':
dependencies:
'@volar/language-core': 2.4.28
'@vue/compiler-dom': 3.5.34
'@vue/shared': 3.5.34
alien-signals: 3.2.1
muggle-string: 0.4.1
path-browserify: 1.0.1
picomatch: 4.0.4
'@vue/reactivity@3.5.34':
dependencies:
'@vue/shared': 3.5.34
'@vue/runtime-core@3.5.34':
dependencies:
'@vue/reactivity': 3.5.34
'@vue/shared': 3.5.34
'@vue/runtime-dom@3.5.34':
dependencies:
'@vue/reactivity': 3.5.34
'@vue/runtime-core': 3.5.34
'@vue/shared': 3.5.34
csstype: 3.2.3
'@vue/server-renderer@3.5.34(vue@3.5.34(typescript@6.0.3))':
dependencies:
'@vue/compiler-ssr': 3.5.34
'@vue/shared': 3.5.34
vue: 3.5.34(typescript@6.0.3)
'@vue/shared@3.5.34': {}
'@vue/tsconfig@0.9.1(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3))':
optionalDependencies:
typescript: 6.0.3
vue: 3.5.34(typescript@6.0.3)
'@vueuse/core@9.13.0(vue@3.5.34(typescript@6.0.3))':
dependencies:
'@types/web-bluetooth': 0.0.16
'@vueuse/metadata': 9.13.0
'@vueuse/shared': 9.13.0(vue@3.5.34(typescript@6.0.3))
vue-demi: 0.14.10(vue@3.5.34(typescript@6.0.3))
transitivePeerDependencies:
- '@vue/composition-api'
- vue
'@vueuse/metadata@9.13.0': {}
'@vueuse/shared@9.13.0(vue@3.5.34(typescript@6.0.3))':
dependencies:
vue-demi: 0.14.10(vue@3.5.34(typescript@6.0.3))
transitivePeerDependencies:
- '@vue/composition-api'
- vue
alien-signals@3.2.1: {}
async-validator@4.2.5: {}
csstype@3.2.3: {}
dayjs@1.11.20: {}
detect-libc@2.1.2: {}
element-plus@2.9.1(vue@3.5.34(typescript@6.0.3)):
dependencies:
'@ctrl/tinycolor': 3.6.1
'@element-plus/icons-vue': 2.3.2(vue@3.5.34(typescript@6.0.3))
'@floating-ui/dom': 1.7.6
'@popperjs/core': '@sxzz/popperjs-es@2.11.8'
'@types/lodash': 4.17.24
'@types/lodash-es': 4.17.12
'@vueuse/core': 9.13.0(vue@3.5.34(typescript@6.0.3))
async-validator: 4.2.5
dayjs: 1.11.20
escape-html: 1.0.3
lodash: 4.18.1
lodash-es: 4.18.1
lodash-unified: 1.0.3(@types/lodash-es@4.17.12)(lodash-es@4.18.1)(lodash@4.18.1)
memoize-one: 6.0.0
normalize-wheel-es: 1.2.0
vue: 3.5.34(typescript@6.0.3)
transitivePeerDependencies:
- '@vue/composition-api'
entities@7.0.1: {}
escape-html@1.0.3: {}
estree-walker@2.0.2: {}
fdir@6.5.0(picomatch@4.0.4):
optionalDependencies:
picomatch: 4.0.4
fsevents@2.3.3:
optional: true
lightningcss-android-arm64@1.32.0:
optional: true
lightningcss-darwin-arm64@1.32.0:
optional: true
lightningcss-darwin-x64@1.32.0:
optional: true
lightningcss-freebsd-x64@1.32.0:
optional: true
lightningcss-linux-arm-gnueabihf@1.32.0:
optional: true
lightningcss-linux-arm64-gnu@1.32.0:
optional: true
lightningcss-linux-arm64-musl@1.32.0:
optional: true
lightningcss-linux-x64-gnu@1.32.0:
optional: true
lightningcss-linux-x64-musl@1.32.0:
optional: true
lightningcss-win32-arm64-msvc@1.32.0:
optional: true
lightningcss-win32-x64-msvc@1.32.0:
optional: true
lightningcss@1.32.0:
dependencies:
detect-libc: 2.1.2
optionalDependencies:
lightningcss-android-arm64: 1.32.0
lightningcss-darwin-arm64: 1.32.0
lightningcss-darwin-x64: 1.32.0
lightningcss-freebsd-x64: 1.32.0
lightningcss-linux-arm-gnueabihf: 1.32.0
lightningcss-linux-arm64-gnu: 1.32.0
lightningcss-linux-arm64-musl: 1.32.0
lightningcss-linux-x64-gnu: 1.32.0
lightningcss-linux-x64-musl: 1.32.0
lightningcss-win32-arm64-msvc: 1.32.0
lightningcss-win32-x64-msvc: 1.32.0
lodash-es@4.18.1: {}
lodash-unified@1.0.3(@types/lodash-es@4.17.12)(lodash-es@4.18.1)(lodash@4.18.1):
dependencies:
'@types/lodash-es': 4.17.12
lodash: 4.18.1
lodash-es: 4.18.1
lodash@4.18.1: {}
magic-string@0.30.21:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
memoize-one@6.0.0: {}
muggle-string@0.4.1: {}
nanoid@3.3.12: {}
normalize-wheel-es@1.2.0: {}
path-browserify@1.0.1: {}
picocolors@1.1.1: {}
picomatch@4.0.4: {}
postcss@8.5.14:
dependencies:
nanoid: 3.3.12
picocolors: 1.1.1
source-map-js: 1.2.1
rolldown@1.0.1:
dependencies:
'@oxc-project/types': 0.130.0
'@rolldown/pluginutils': 1.0.1
optionalDependencies:
'@rolldown/binding-android-arm64': 1.0.1
'@rolldown/binding-darwin-arm64': 1.0.1
'@rolldown/binding-darwin-x64': 1.0.1
'@rolldown/binding-freebsd-x64': 1.0.1
'@rolldown/binding-linux-arm-gnueabihf': 1.0.1
'@rolldown/binding-linux-arm64-gnu': 1.0.1
'@rolldown/binding-linux-arm64-musl': 1.0.1
'@rolldown/binding-linux-ppc64-gnu': 1.0.1
'@rolldown/binding-linux-s390x-gnu': 1.0.1
'@rolldown/binding-linux-x64-gnu': 1.0.1
'@rolldown/binding-linux-x64-musl': 1.0.1
'@rolldown/binding-openharmony-arm64': 1.0.1
'@rolldown/binding-wasm32-wasi': 1.0.1
'@rolldown/binding-win32-arm64-msvc': 1.0.1
'@rolldown/binding-win32-x64-msvc': 1.0.1
source-map-js@1.2.1: {}
tinyglobby@0.2.16:
dependencies:
fdir: 6.5.0(picomatch@4.0.4)
picomatch: 4.0.4
tslib@2.8.1:
optional: true
typescript@6.0.3: {}
undici-types@7.16.0: {}
vite@8.0.13(@types/node@24.12.4):
dependencies:
lightningcss: 1.32.0
picomatch: 4.0.4
postcss: 8.5.14
rolldown: 1.0.1
tinyglobby: 0.2.16
optionalDependencies:
'@types/node': 24.12.4
fsevents: 2.3.3
vscode-uri@3.1.0: {}
vue-demi@0.14.10(vue@3.5.34(typescript@6.0.3)):
dependencies:
vue: 3.5.34(typescript@6.0.3)
vue-tsc@3.2.9(typescript@6.0.3):
dependencies:
'@volar/typescript': 2.4.28
'@vue/language-core': 3.2.9
typescript: 6.0.3
vue@3.5.34(typescript@6.0.3):
dependencies:
'@vue/compiler-dom': 3.5.34
'@vue/compiler-sfc': 3.5.34
'@vue/runtime-dom': 3.5.34
'@vue/server-renderer': 3.5.34(vue@3.5.34(typescript@6.0.3))
'@vue/shared': 3.5.34
optionalDependencies:
typescript: 6.0.3
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="46" fill="none" viewBox="0 0 48 46"><path fill="#863bff" d="M25.946 44.938c-.664.845-2.021.375-2.021-.698V33.937a2.26 2.26 0 0 0-2.262-2.262H10.287c-.92 0-1.456-1.04-.92-1.788l7.48-10.471c1.07-1.497 0-3.578-1.842-3.578H1.237c-.92 0-1.456-1.04-.92-1.788L10.013.474c.214-.297.556-.474.92-.474h28.894c.92 0 1.456 1.04.92 1.788l-7.48 10.471c-1.07 1.498 0 3.579 1.842 3.579h11.377c.943 0 1.473 1.088.89 1.83L25.947 44.94z" style="fill:#863bff;fill:color(display-p3 .5252 .23 1);fill-opacity:1"/><mask id="a" width="48" height="46" x="0" y="0" maskUnits="userSpaceOnUse" style="mask-type:alpha"><path fill="#000" d="M25.842 44.938c-.664.844-2.021.375-2.021-.698V33.937a2.26 2.26 0 0 0-2.262-2.262H10.183c-.92 0-1.456-1.04-.92-1.788l7.48-10.471c1.07-1.498 0-3.579-1.842-3.579H1.133c-.92 0-1.456-1.04-.92-1.787L9.91.473c.214-.297.556-.474.92-.474h28.894c.92 0 1.456 1.04.92 1.788l-7.48 10.471c-1.07 1.498 0 3.578 1.842 3.578h11.377c.943 0 1.473 1.088.89 1.832L25.843 44.94z" style="fill:#000;fill-opacity:1"/></mask><g mask="url(#a)"><g filter="url(#b)"><ellipse cx="5.508" cy="14.704" fill="#ede6ff" rx="5.508" ry="14.704" style="fill:#ede6ff;fill:color(display-p3 .9275 .9033 1);fill-opacity:1" transform="matrix(.00324 1 1 -.00324 -4.47 31.516)"/></g><g filter="url(#c)"><ellipse cx="10.399" cy="29.851" fill="#ede6ff" rx="10.399" ry="29.851" style="fill:#ede6ff;fill:color(display-p3 .9275 .9033 1);fill-opacity:1" transform="matrix(.00324 1 1 -.00324 -39.328 7.883)"/></g><g filter="url(#d)"><ellipse cx="5.508" cy="30.487" fill="#7e14ff" rx="5.508" ry="30.487" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.814 -25.913 -14.639)scale(1 -1)"/></g><g filter="url(#e)"><ellipse cx="5.508" cy="30.599" fill="#7e14ff" rx="5.508" ry="30.599" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.814 -32.644 -3.334)scale(1 -1)"/></g><g filter="url(#f)"><ellipse cx="5.508" cy="30.599" fill="#7e14ff" rx="5.508" ry="30.599" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="matrix(.00324 1 1 -.00324 -34.34 30.47)"/></g><g filter="url(#g)"><ellipse cx="14.072" cy="22.078" fill="#ede6ff" rx="14.072" ry="22.078" style="fill:#ede6ff;fill:color(display-p3 .9275 .9033 1);fill-opacity:1" transform="rotate(93.35 24.506 48.493)scale(-1 1)"/></g><g filter="url(#h)"><ellipse cx="3.47" cy="21.501" fill="#7e14ff" rx="3.47" ry="21.501" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.009 28.708 47.59)scale(-1 1)"/></g><g filter="url(#i)"><ellipse cx="3.47" cy="21.501" fill="#7e14ff" rx="3.47" ry="21.501" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.009 28.708 47.59)scale(-1 1)"/></g><g filter="url(#j)"><ellipse cx=".387" cy="8.972" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(39.51 .387 8.972)"/></g><g filter="url(#k)"><ellipse cx="47.523" cy="-6.092" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 47.523 -6.092)"/></g><g filter="url(#l)"><ellipse cx="41.412" cy="6.333" fill="#47bfff" rx="5.971" ry="9.665" style="fill:#47bfff;fill:color(display-p3 .2799 .748 1);fill-opacity:1" transform="rotate(37.892 41.412 6.333)"/></g><g filter="url(#m)"><ellipse cx="-1.879" cy="38.332" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 -1.88 38.332)"/></g><g filter="url(#n)"><ellipse cx="-1.879" cy="38.332" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 -1.88 38.332)"/></g><g filter="url(#o)"><ellipse cx="35.651" cy="29.907" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 35.651 29.907)"/></g><g filter="url(#p)"><ellipse cx="38.418" cy="32.4" fill="#47bfff" rx="5.971" ry="15.297" style="fill:#47bfff;fill:color(display-p3 .2799 .748 1);fill-opacity:1" transform="rotate(37.892 38.418 32.4)"/></g></g><defs><filter id="b" width="60.045" height="41.654" x="-19.77" y="16.149" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="7.659"/></filter><filter id="c" width="90.34" height="51.437" x="-54.613" y="-7.533" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="7.659"/></filter><filter id="d" width="79.355" height="29.4" x="-49.64" y="2.03" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="e" width="79.579" height="29.4" x="-45.045" y="20.029" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="f" width="79.579" height="29.4" x="-43.513" y="21.178" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="g" width="74.749" height="58.852" x="15.756" y="-17.901" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="7.659"/></filter><filter id="h" width="61.377" height="25.362" x="23.548" y="2.284" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="i" width="61.377" height="25.362" x="23.548" y="2.284" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="j" width="56.045" height="63.649" x="-27.636" y="-22.853" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="k" width="54.814" height="64.646" x="20.116" y="-38.415" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="l" width="33.541" height="35.313" x="24.641" y="-11.323" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="m" width="54.814" height="64.646" x="-29.286" y="6.009" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="n" width="54.814" height="64.646" x="-29.286" y="6.009" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="o" width="54.814" height="64.646" x="8.244" y="-2.416" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="p" width="39.409" height="43.623" x="18.713" y="10.588" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter></defs></svg>
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg">
<symbol id="bluesky-icon" viewBox="0 0 16 17">
<g clip-path="url(#bluesky-clip)"><path fill="#08060d" d="M7.75 7.735c-.693-1.348-2.58-3.86-4.334-5.097-1.68-1.187-2.32-.981-2.74-.79C.188 2.065.1 2.812.1 3.251s.241 3.602.398 4.13c.52 1.744 2.367 2.333 4.07 2.145-2.495.37-4.71 1.278-1.805 4.512 3.196 3.309 4.38-.71 4.987-2.746.608 2.036 1.307 5.91 4.93 2.746 2.72-2.746.747-4.143-1.747-4.512 1.702.189 3.55-.4 4.07-2.145.156-.528.397-3.691.397-4.13s-.088-1.186-.575-1.406c-.42-.19-1.06-.395-2.741.79-1.755 1.24-3.64 3.752-4.334 5.099"/></g>
<defs><clipPath id="bluesky-clip"><path fill="#fff" d="M.1.85h15.3v15.3H.1z"/></clipPath></defs>
</symbol>
<symbol id="discord-icon" viewBox="0 0 20 19">
<path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/>
</symbol>
<symbol id="documentation-icon" viewBox="0 0 21 20">
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/>
</symbol>
<symbol id="github-icon" viewBox="0 0 19 19">
<path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/>
</symbol>
<symbol id="social-icon" viewBox="0 0 20 20">
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/>
</symbol>
<symbol id="x-icon" viewBox="0 0 19 19">
<path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/>
</symbol>
</svg>
<script setup lang="ts">
import { ref } from 'vue'
import TimeLine from './components/TimeLine.vue'
import { defaultTasks, type TaskResult } from './components/timeline-types'
const visible = ref(false)
const result = ref<Record<string, TaskResult> | null>(null)
// 示例数据:实际使用时由业务方传入
const tasks = defaultTasks()
function onConfirm(r: Record<string, TaskResult>) {
result.value = r
console.log('[TimeLine confirm]', r)
}
</script>
<template>
<div class="app-page">
<button class="open-btn" @click="visible = true">打开时间轴</button>
<div v-if="result" class="result-panel">
<div class="result-panel__header">
<span>📤 最新输出结果</span>
<span class="result-panel__hint">(每次点击「确认」更新)</span>
</div>
<pre class="result-panel__body">{{ JSON.stringify(result, null, 2) }}</pre>
</div>
</div>
<TimeLine v-model="visible" :tasks="tasks" @confirm="onConfirm" />
</template>
<style scoped>
.app-page {
min-height: 100vh;
background: #0A0F1E;
display: flex;
flex-direction: column;
align-items: flex-start;
padding: 40px;
gap: 24px;
}
.open-btn {
padding: 10px 28px;
background: linear-gradient(135deg, #3B82F6, #1D4ED8);
color: #fff;
border: none;
border-radius: 8px;
font-size: 15px;
cursor: pointer;
letter-spacing: 0.5px;
transition: opacity 0.15s;
}
.open-btn:hover { opacity: 0.85; }
.result-panel {
background: #0D1424;
border: 1px solid #1E3A5F;
border-radius: 8px;
padding: 12px 20px 16px;
max-width: 700px;
width: 100%;
}
.result-panel__header {
font-size: 12px;
color: #64748B;
margin-bottom: 8px;
display: flex;
align-items: center;
gap: 8px;
}
.result-panel__hint { color: #475569; }
.result-panel__body {
margin: 0;
font-family: 'JetBrains Mono', 'Fira Code', monospace;
font-size: 12px;
line-height: 1.6;
color: #34D399;
white-space: pre-wrap;
word-break: break-all;
}
</style>
<svg xmlns="http://www.w3.org/2000/svg" width="77" height="47" fill="none" aria-labelledby="vite-logo-title" viewBox="0 0 77 47"><title id="vite-logo-title">Vite</title><style>.parenthesis{fill:#000}@media (prefers-color-scheme:dark){.parenthesis{fill:#fff}}</style><path fill="#9135ff" d="M40.151 45.71c-.663.844-2.02.374-2.02-.699V34.708a2.26 2.26 0 0 0-2.262-2.262H24.493c-.92 0-1.457-1.04-.92-1.788l7.479-10.471c1.07-1.498 0-3.578-1.842-3.578H15.443c-.92 0-1.456-1.04-.92-1.788l9.696-13.576c.213-.297.556-.474.92-.474h28.894c.92 0 1.456 1.04.92 1.788l-7.48 10.472c-1.07 1.497 0 3.578 1.842 3.578h11.376c.944 0 1.474 1.087.89 1.83L40.153 45.712z"/><mask id="a" width="48" height="47" x="14" y="0" maskUnits="userSpaceOnUse" style="mask-type:alpha"><path fill="#000" d="M40.047 45.71c-.663.843-2.02.374-2.02-.699V34.708a2.26 2.26 0 0 0-2.262-2.262H24.389c-.92 0-1.457-1.04-.92-1.788l7.479-10.472c1.07-1.497 0-3.578-1.842-3.578H15.34c-.92 0-1.456-1.04-.92-1.788l9.696-13.575c.213-.297.556-.474.92-.474H53.93c.92 0 1.456 1.04.92 1.788L47.37 13.03c-1.07 1.498 0 3.578 1.842 3.578h11.376c.944 0 1.474 1.088.89 1.831L40.049 45.712z"/></mask><g mask="url(#a)"><g filter="url(#b)"><ellipse cx="5.508" cy="14.704" fill="#eee6ff" rx="5.508" ry="14.704" transform="rotate(269.814 20.96 11.29)scale(-1 1)"/></g><g filter="url(#c)"><ellipse cx="10.399" cy="29.851" fill="#eee6ff" rx="10.399" ry="29.851" transform="rotate(89.814 -16.902 -8.275)scale(1 -1)"/></g><g filter="url(#d)"><ellipse cx="5.508" cy="30.487" fill="#8900ff" rx="5.508" ry="30.487" transform="rotate(89.814 -19.197 -7.127)scale(1 -1)"/></g><g filter="url(#e)"><ellipse cx="5.508" cy="30.599" fill="#8900ff" rx="5.508" ry="30.599" transform="rotate(89.814 -25.928 4.177)scale(1 -1)"/></g><g filter="url(#f)"><ellipse cx="5.508" cy="30.599" fill="#8900ff" rx="5.508" ry="30.599" transform="rotate(89.814 -25.738 5.52)scale(1 -1)"/></g><g filter="url(#g)"><ellipse cx="14.072" cy="22.078" fill="#eee6ff" rx="14.072" ry="22.078" transform="rotate(93.35 31.245 55.578)scale(-1 1)"/></g><g filter="url(#h)"><ellipse cx="3.47" cy="21.501" fill="#8900ff" rx="3.47" ry="21.501" transform="rotate(89.009 35.419 55.202)scale(-1 1)"/></g><g filter="url(#i)"><ellipse cx="3.47" cy="21.501" fill="#8900ff" rx="3.47" ry="21.501" transform="rotate(89.009 35.419 55.202)scale(-1 1)"/></g><g filter="url(#j)"><ellipse cx="14.592" cy="9.743" fill="#8900ff" rx="4.407" ry="29.108" transform="rotate(39.51 14.592 9.743)"/></g><g filter="url(#k)"><ellipse cx="61.728" cy="-5.321" fill="#8900ff" rx="4.407" ry="29.108" transform="rotate(37.892 61.728 -5.32)"/></g><g filter="url(#l)"><ellipse cx="55.618" cy="7.104" fill="#00c2ff" rx="5.971" ry="9.665" transform="rotate(37.892 55.618 7.104)"/></g><g filter="url(#m)"><ellipse cx="12.326" cy="39.103" fill="#8900ff" rx="4.407" ry="29.108" transform="rotate(37.892 12.326 39.103)"/></g><g filter="url(#n)"><ellipse cx="12.326" cy="39.103" fill="#8900ff" rx="4.407" ry="29.108" transform="rotate(37.892 12.326 39.103)"/></g><g filter="url(#o)"><ellipse cx="49.857" cy="30.678" fill="#8900ff" rx="4.407" ry="29.108" transform="rotate(37.892 49.857 30.678)"/></g><g filter="url(#p)"><ellipse cx="52.623" cy="33.171" fill="#00c2ff" rx="5.971" ry="15.297" transform="rotate(37.892 52.623 33.17)"/></g></g><path d="M6.919 0c-9.198 13.166-9.252 33.575 0 46.789h6.215c-9.25-13.214-9.196-33.623 0-46.789zm62.424 0h-6.215c9.198 13.166 9.252 33.575 0 46.789h6.215c9.25-13.214 9.196-33.623 0-46.789" class="parenthesis"/><defs><filter id="b" width="60.045" height="41.654" x="-5.564" y="16.92" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="7.659"/></filter><filter id="c" width="90.34" height="51.437" x="-40.407" y="-6.762" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="7.659"/></filter><filter id="d" width="79.355" height="29.4" x="-35.435" y="2.801" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="e" width="79.579" height="29.4" x="-30.84" y="20.8" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="f" width="79.579" height="29.4" x="-29.307" y="21.949" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="g" width="74.749" height="58.852" x="29.961" y="-17.13" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="7.659"/></filter><filter id="h" width="61.377" height="25.362" x="37.754" y="3.055" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="i" width="61.377" height="25.362" x="37.754" y="3.055" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="j" width="56.045" height="63.649" x="-13.43" y="-22.082" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="k" width="54.814" height="64.646" x="34.321" y="-37.644" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="l" width="33.541" height="35.313" x="38.847" y="-10.552" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="m" width="54.814" height="64.646" x="-15.081" y="6.78" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="n" width="54.814" height="64.646" x="-15.081" y="6.78" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="o" width="54.814" height="64.646" x="22.45" y="-1.645" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="p" width="39.409" height="43.623" x="32.919" y="11.36" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter></defs></svg>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
\ No newline at end of file
<script setup lang="ts">
import { ref } from 'vue'
import viteLogo from '../assets/vite.svg'
import heroImg from '../assets/hero.png'
import vueLogo from '../assets/vue.svg'
const count = ref(0)
</script>
<template>
<section id="center">
<div class="hero">
<img :src="heroImg" class="base" width="170" height="179" alt="" />
<img :src="vueLogo" class="framework" alt="Vue logo" />
<img :src="viteLogo" class="vite" alt="Vite logo" />
</div>
<div>
<h1>Get started</h1>
<p>Edit <code>src/App.vue</code> and save to test <code>HMR</code></p>
</div>
<button type="button" class="counter" @click="count++">
Count is {{ count }}
</button>
</section>
<div class="ticks"></div>
<section id="next-steps">
<div id="docs">
<svg class="icon" role="presentation" aria-hidden="true">
<use href="/icons.svg#documentation-icon"></use>
</svg>
<h2>Documentation</h2>
<p>Your questions, answered</p>
<ul>
<li>
<a href="https://vite.dev/" target="_blank">
<img class="logo" :src="viteLogo" alt="" />
Explore Vite
</a>
</li>
<li>
<a href="https://vuejs.org/" target="_blank">
<img class="button-icon" :src="vueLogo" alt="" />
Learn more
</a>
</li>
</ul>
</div>
<div id="social">
<svg class="icon" role="presentation" aria-hidden="true">
<use href="/icons.svg#social-icon"></use>
</svg>
<h2>Connect with us</h2>
<p>Join the Vite community</p>
<ul>
<li>
<a href="https://github.com/vitejs/vite" target="_blank">
<svg class="button-icon" role="presentation" aria-hidden="true">
<use href="/icons.svg#github-icon"></use>
</svg>
GitHub
</a>
</li>
<li>
<a href="https://chat.vite.dev/" target="_blank">
<svg class="button-icon" role="presentation" aria-hidden="true">
<use href="/icons.svg#discord-icon"></use>
</svg>
Discord
</a>
</li>
<li>
<a href="https://x.com/vite_js" target="_blank">
<svg class="button-icon" role="presentation" aria-hidden="true">
<use href="/icons.svg#x-icon"></use>
</svg>
X.com
</a>
</li>
<li>
<a href="https://bsky.app/profile/vite.dev" target="_blank">
<svg class="button-icon" role="presentation" aria-hidden="true">
<use href="/icons.svg#bluesky-icon"></use>
</svg>
Bluesky
</a>
</li>
</ul>
</div>
</section>
<div class="ticks"></div>
<section id="spacer"></section>
</template>
<script setup lang="ts">
import { ref, computed, watch, nextTick, onMounted, onUnmounted } from 'vue'
import { ElDialog, ElButton, ElInput, ElRadioGroup, ElRadioButton } from 'element-plus'
import { type TimeTask, type TaskResult, ORIGIN } from './timeline-types'
// ────────────────────────── Props / Emits ──────────────────
// 注:trackDirty / enforceLimit 必须用 withDefaults 显式设为 true,
// 否则 Vue 的 boolean casting 会将未传入的 ?:boolean prop 转成 false
const props = withDefaults(defineProps<{
/** v-model — 控制整个弹窗的显示/隐藏 */
modelValue?: boolean
/** 必传:任务数组(segments.startTime/endTime 均为 Unix ms 时间戳)*/
tasks: TimeTask[]
/** 是否在 confirm 结果中追踪 isDirty(默认 true)*/
trackDirty?: boolean
/** 是否启用每任务 limit 时间范围约束(默认 true)*/
enforceLimit?: boolean
}>(), {
trackDirty: true,
enforceLimit: true,
})
/**
* update:modelValue — v-model 双向绑定可见性
* confirm — 点击外层「确认」时触发,携带每个任务的最终段区间和 isDirty
* 格式: { [id]: { segments: [[startMs, endMs], ...], isDirty: boolean } }
* 时间格式: Unix 毫秒时间戳(number)
*/
const emit = defineEmits<{
'update:modelValue': [value: boolean]
confirm: [result: Record<string, TaskResult>]
}>()
/** 本地可见状态,双向绑定到父组件 v-model */
const localVisible = computed({
get: () => props.modelValue ?? false,
set: (v) => emit('update:modelValue', v),
})
// ────────────────────────── 常量 ──────────────────────────
const LABEL_W = 172 // 左侧任务标签列宽度(px)
const MIN_BAR_SECONDS = 900 // 时间段最小宽度:15 分钟(防止拖拽至零长)
const TOTAL_RANGE = 7 * 86400 // 可导航的总时间范围:7 天(秒)
/** Unix ms 时间戳 → 内部秒(距 ORIGIN 的秒数)*/
const msToSec = (ms: number) => Math.round((ms - ORIGIN) / 1000)
// ────────────────────────── 响应式状态 ─────────────────────
/** 时间轴容器 DOM 引用,用于测量实际宽度 */
const containerRef = ref<HTMLElement | null>(null)
/** 容器宽度缓存(px),随窗口 resize 实时更新 */
const containerWidth = ref(1440)
/** 工作副本:内部均为秒(距 ORIGIN 的秒数),弹窗打开时从 props.tasks(ms)转换而来 */
const tasks = ref<TimeTask[]>(props.tasks.map(t => ({
...t,
segments: t.segments.map(s => ({ startTime: msToSec(s.startTime), endTime: msToSec(s.endTime) })),
})))
/** 弹窗打开时的初始秒数快照,用于 isDirty 比较(key=task.id,值为内部秒)*/
const originalSegments = ref<Record<string, [number, number][]>>({})
// 每次弹窗打开时从 props 重新初始化(取消则丢弃本次修改)
watch(localVisible, (v) => {
if (v) {
tasks.value = props.tasks.map(t => ({
...t,
segments: t.segments.map(s => ({ startTime: msToSec(s.startTime), endTime: msToSec(s.endTime) })),
}))
// 在转换完成后,从 tasks.value(已是秒)取快照——无需二次 ms/sec 转换
if (props.trackDirty !== false) {
const orig: Record<string, [number, number][]> = {}
for (const t of tasks.value) {
orig[t.id] = t.segments.map(s => [s.startTime, s.endTime])
}
originalSegments.value = orig
}
nextTick(() => updateWidth())
}
})
// 根据传入数据的时间范围推算初始视口,无需外部传入
const _initMin = props.tasks.reduce((m, t) => t.segments.reduce((m2, s) => Math.min(m2, msToSec(s.startTime)), m), Infinity)
const _initMax = props.tasks.reduce((m, t) => t.segments.reduce((m2, s) => Math.max(m2, msToSec(s.endTime)), m), -Infinity)
const _boundsMin = _initMin === Infinity ? 0 : _initMin
const _boundsMax = _initMax === -Infinity ? TOTAL_RANGE : _initMax
// 两侧各留 3% 视觉余量,避免首尾拖拽柄被裁切(最小 900 秒)
const _pad = Math.max(900, Math.round((_boundsMax - _boundsMin) * 0.03))
const _viewMin = _boundsMin - _pad // 可滚动范围左边界(秒)
const _viewMax = _boundsMax + _pad // 可滚动范围右边界(秒)
/** 当前视口起始秒(对应时间轴左侧可见位置)*/
const viewStart = ref(_viewMin)
/** 当前视口结束秒(对应时间轴右侧可见位置)*/
const viewEnd = ref(_viewMax)
// ── 编辑弹窗状态 ──
/** 编辑弹窗是否可见 */
const dialogVisible = ref(false)
/** 当前正在编辑的任务对象 */
const editingTask = ref<TimeTask | null>(null)
/** 当前正在编辑的段索引 */
const editingSegIdx = ref(0)
/** 编辑框:开始时间字符串(YYYY-MM-DD HH:mm:ss)*/
const editStart = ref('')
/** 编辑框:结束时间字符串(YYYY-MM-DD HH:mm:ss)*/
const editEnd = ref('')
/** 拆分模式:none=不拆分,2=拆分2段,3=拆分3段 */
const splitMode = ref<'none' | '2' | '3'>('none')
/** 弹窗校验错误提示文本 */
const dialogError = ref('')
/** 拆分模式下每个子段的时间输入 */
type SplitSeg = { start: string; end: string }
const splitSegs = ref<SplitSeg[]>([])
/** 日期格式正则 YYYY-MM-DD HH:mm:ss */
const DATE_RE = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/
/** 将"距 ORIGIN 的秒数"格式化为 YYYY-MM-DD HH:mm:ss 字符串 */
function secToStr(s: number): string {
const d = secToDate(s)
const p = (n: number) => String(n).padStart(2, '0')
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`
}
/** 将 YYYY-MM-DD HH:mm:ss 字符串解析为"距 ORIGIN 的秒数",格式非法或日期无效时返回 null */
function strToSec(str: string): number | null {
if (!DATE_RE.test(str)) return null
const d = new Date(str.replace(' ', 'T'))
return isNaN(d.getTime()) ? null : dateToSec(d)
}
// ── 拖拽状态 ──
/** 当前拖拽目标:任务索引、段索引、操作边(左/右/整体移动)*/
type DragTarget = { taskIdx: number; segIdx: number; edge: 'left' | 'right' | 'move' }
const dragging = ref<DragTarget | null>(null)
/** 拖拽起始鼠标 X(px)*/
const dragStartX = ref(0)
/** 拖拽起始时段 startTime(秒)*/
const dragStartSec = ref(0)
/** 拖拽起始时段 endTime(秒)*/
const dragStartSecEnd = ref(0)
// ── 背景平移状态 ──
/** 是否正在平移视口 */
const isPanning = ref(false)
/** 平移起始鼠标 X */
const panStartX = ref(0)
/** 平移起始 viewStart(秒)*/
const panStartViewStart = ref(0)
// ── 导航滑块拖拽状态 ──
/** 是否正在拖拽底部导航缩略滑块 */
const isScrollThumbDrag = ref(false)
/** 滑块拖拽起始鼠标 X */
const thumbDragStartX = ref(0)
/** 滑块拖拽起始 viewStart(秒)*/
const thumbDragStartViewStart = ref(0)
// ────────────────────────── 计算属性 ────────────────────────
/** 当前视口时长(秒)*/
const totalSeconds = computed(() => viewEnd.value - viewStart.value)
/** 时间轴区域宽度 = 容器宽度 - 左侧标签列宽(px)*/
const timelineWidth = computed(() => containerWidth.value - LABEL_W)
/** 底部导航滑块左偏移(px):当前视口在全局范围内的位置比例 */
const thumbLeft = computed(() => ((viewStart.value - _viewMin) / (_viewMax - _viewMin)) * timelineWidth.value)
/** 底部导航滑块宽度(px):当前视口占全局范围的比例,最小 40px */
const thumbWidth = computed(() => Math.max(40, (totalSeconds.value / (_viewMax - _viewMin)) * timelineWidth.value))
/** 可拖拽/编辑的时间边界:取所有初始段的并集(防止拖出数据范围)*/
const dataBounds = computed(() => {
let min = Infinity, max = -Infinity
for (const t of props.tasks) {
for (const s of t.segments) {
const sMin = msToSec(s.startTime), sMax = msToSec(s.endTime)
if (sMin < min) min = sMin
if (sMax > max) max = sMax
}
}
return {
min: min === Infinity ? 0 : min,
max: max === -Infinity ? TOTAL_RANGE : max,
}
})
/** 各任务的 limit 约束,转换为内部秒格式(enforceLimit !== false 且 task.limit 存在时有效)*/
const taskLimits = computed<Record<string, [number, number]>>(() => {
if (props.enforceLimit === false) return {}
return Object.fromEntries(
props.tasks
.filter(t => t.limit)
.map(t => [t.id, [msToSec(t.limit!.startTime), msToSec(t.limit!.endTime)] as [number, number]])
)
})
/** 获取指定任务的有效时间边界:有 limit 则用 limit,否则用全局 dataBounds */
function getTaskBounds(taskIdx: number): { min: number; max: number } {
const task = tasks.value[taskIdx]
const limit = taskLimits.value[task.id]
if (limit) return { min: limit[0], max: limit[1] }
return dataBounds.value
}
/** 当前正在编辑任务的 limit 约束(秒),无约束时为 null */
const currentTaskLimitSec = computed<[number, number] | null>(() => {
if (!editingTask.value) return null
return taskLimits.value[editingTask.value.id] ?? null
})
/** 将"距 ORIGIN 的秒数"转换为时间轴 X 坐标(px)*/
function secondToX(sec: number): number {
return ((sec - viewStart.value) / totalSeconds.value) * timelineWidth.value
}
/** 将秒数对齐到最近的 step 整数倍(默认 60 秒 = 1 分钟),实现磁吸效果 */
function snapSecond(s: number, step = 60): number {
return Math.round(s / step) * step
}
/**
* 刻度线列表:根据当前像素密度自动选择合适的刻度间隔,避免标签重叠。
* 档位:15min / 30min / 1h / 2h / 3h / 6h / 12h / 1d / 2d
* 策略:每两条相邻刻度之间至少保留 MIN_TICK_PX 像素宽度。
*/
const rulerTicks = computed(() => {
// 刻度间隔候选档位(秒),由小到大
const STEPS = [900, 1800, 3600, 7200, 3 * 3600, 6 * 3600, 12 * 3600, 86400, 2 * 86400]
const MIN_TICK_PX = 72 // 相邻刻度最小像素间距(保留标签宽度余量)
const pxPerSec = timelineWidth.value / totalSeconds.value
const minStepSec = MIN_TICK_PX / pxPerSec
// 选最小的、但仍能保证间距的档位
const step = STEPS.find(s => s >= minStepSec) ?? STEPS[STEPS.length - 1]
const ticks: { label: string; x: number; major: boolean }[] = []
const start = Math.ceil(viewStart.value / step) * step
for (let s = start; s <= viewEnd.value; s += step) {
const date = new Date(ORIGIN + s * 1000)
const isMidnight = s % (24 * 3600) === 0
const h = pad(date.getHours())
const m = pad(date.getMinutes())
// 小于 1 小时的档位显示分钟,否则只显示小时
const timeStr = step < 3600 ? `${h}:${m}` : `${h}:00`
const label = isMidnight
? `${date.getMonth() + 1}/${date.getDate()}\n${timeStr}`
: timeStr
ticks.push({ label, x: secondToX(s), major: isMidnight })
}
return ticks
})
function pad(n: number) { return String(n).padStart(2, '0') }
/** 将"距 ORIGIN 的秒数"格式化为 "M/D HH:MM" 用于时间段标签显示 */
function formatSecond(s: number): string {
const d = new Date(ORIGIN + s * 1000)
return `${d.getMonth() + 1}/${d.getDate()} ${pad(d.getHours())}:${pad(d.getMinutes())}`
}
/** 将"距 ORIGIN 的秒数"转换为 Date 对象 */
function secToDate(s: number): Date {
return new Date(ORIGIN + s * 1000)
}
/** 将 Date 对象转换为"距 ORIGIN 的秒数"(四舍五入到整秒)*/
function dateToSec(d: Date): number {
return Math.round((d.getTime() - ORIGIN) / 1000)
}
/** 将秒数时长格式化为 "X小时Y分钟Z秒" 的可读字符串 */
function formatDuration(secs: number): string {
const h = Math.floor(secs / 3600)
const m = Math.floor((secs % 3600) / 60)
const s = secs % 60
return `${h}小时${m}分钟${s}秒`
}
/** 将重叠时长格式化为简短标签,如 "2h30m"(用于重叠区域徽标)*/
function formatOverlap(secs: number): string {
const h = Math.floor(secs / 3600)
const m = Math.floor((secs % 3600) / 60)
if (h > 0) return `${h}h${m > 0 ? m + 'm' : ''}`
return `${m}m`
}
/** 重叠检测:遍历所有任务对的所有段组合,收集有重叠的区间 */
const overlaps = computed(() => {
const result: { taskA: number; taskB: number; start: number; end: number }[] = []
for (let i = 0; i < tasks.value.length; i++) {
for (let j = i + 1; j < tasks.value.length; j++) {
for (const sa of tasks.value[i].segments) {
for (const sb of tasks.value[j].segments) {
const start = Math.max(sa.startTime, sb.startTime)
const end = Math.min(sa.endTime, sb.endTime)
if (end > start) {
result.push({ taskA: i, taskB: j, start, end })
}
}
}
}
}
return result
})
// ────────────────────────── 拖拽事件 ────────────────────────
/** 拖拽拖柄(左/右边缘)按下:记录初始状态,开始调整段宽度 */
function onHandleMousedown(e: MouseEvent, taskIdx: number, segIdx: number, edge: 'left' | 'right') {
e.preventDefault()
e.stopPropagation()
const seg = tasks.value[taskIdx].segments[segIdx]
dragging.value = { taskIdx, segIdx, edge }
dragStartX.value = e.clientX
dragStartSec.value = seg.startTime
dragStartSecEnd.value = seg.endTime
}
/** 拖拽时间段主体按下:记录初始状态,开始整体平移段 */
function onBarMousedown(e: MouseEvent, taskIdx: number, segIdx: number) {
e.preventDefault()
e.stopPropagation()
const seg = tasks.value[taskIdx].segments[segIdx]
dragging.value = { taskIdx, segIdx, edge: 'move' }
dragStartX.value = e.clientX
dragStartSec.value = seg.startTime
dragStartSecEnd.value = seg.endTime
}
/** 全局 mousemove:统一处理滚动条拖拽、背景平移、段拖拽三种模式 */
function onMousemove(e: MouseEvent) {
// 模式一:底部导航滑块拖拽
if (isScrollThumbDrag.value) {
const dx = e.clientX - thumbDragStartX.value
const dur = totalSeconds.value
let newStart = thumbDragStartViewStart.value + (dx / timelineWidth.value) * (_viewMax - _viewMin)
newStart = Math.max(_viewMin, Math.min(_viewMax - dur, newStart))
viewStart.value = newStart
viewEnd.value = newStart + dur
return
}
// 模式二:背景平移(按住空白区域左右滑动)
if (isPanning.value) {
const dx = e.clientX - panStartX.value
const dur = totalSeconds.value
let newStart = panStartViewStart.value - (dx / timelineWidth.value) * dur
newStart = Math.max(_viewMin, Math.min(_viewMax - dur, newStart))
viewStart.value = newStart
viewEnd.value = newStart + dur
return
}
if (!dragging.value) return
const dx = e.clientX - dragStartX.value
const dSec = (dx / timelineWidth.value) * totalSeconds.value
const seg = tasks.value[dragging.value.taskIdx].segments[dragging.value.segIdx]
const bounds = getTaskBounds(dragging.value.taskIdx)
if (dragging.value.edge === 'left') {
const newStart = snapSecond(dragStartSec.value + dSec)
seg.startTime = Math.max(bounds.min, Math.min(newStart, seg.endTime - MIN_BAR_SECONDS))
} else if (dragging.value.edge === 'right') {
const newEnd = snapSecond(dragStartSecEnd.value + dSec)
seg.endTime = Math.min(bounds.max, Math.max(newEnd, seg.startTime + MIN_BAR_SECONDS))
} else {
const dur = dragStartSecEnd.value - dragStartSec.value
const raw = snapSecond(dragStartSec.value + dSec)
const newStart = Math.max(bounds.min, Math.min(bounds.max - dur, raw))
seg.startTime = newStart
seg.endTime = newStart + dur
}
}
/** 全局 mouseup:清除所有拖拽/平移状态 */
function onMouseup() {
dragging.value = null
isPanning.value = false
isScrollThumbDrag.value = false
}
// ────────────────────────── 视口平移 ────────────────────────
/** 主区域 mousedown:排除交互元素后启动背景平移 */
function onMainMousedown(e: MouseEvent) {
const target = e.target as HTMLElement
if (target.closest('.tl-bar, .tl-handle, .tl-time-label, .tl-label-cell, .tl-nav-scrollbar')) return
e.preventDefault()
isPanning.value = true
panStartX.value = e.clientX
panStartViewStart.value = viewStart.value
}
/** 底部导航滑块 mousedown:启动滑块拖拽 */
function onScrollThumbMousedown(e: MouseEvent) {
e.preventDefault()
e.stopPropagation()
isScrollThumbDrag.value = true
thumbDragStartX.value = e.clientX
thumbDragStartViewStart.value = viewStart.value
}
/** 点击导航轨道(非滑块区域):将视口中心跳转到点击位置 */
function onScrollTrackClick(e: MouseEvent) {
// 点击的是滑块本身时不触发(避免与拖拽冲突)
if ((e.target as HTMLElement).classList.contains('tl-nav-thumb')) return
const track = e.currentTarget as HTMLElement
const rect = track.getBoundingClientRect()
const x = e.clientX - rect.left
const clickSec = _viewMin + (x / timelineWidth.value) * (_viewMax - _viewMin)
const dur = totalSeconds.value
let newStart = clickSec - dur / 2
newStart = Math.max(_viewMin, Math.min(_viewMax - dur, newStart))
viewStart.value = newStart
viewEnd.value = newStart + dur
}
// ────────────────────────── 编辑弹窗 ────────────────────────
/** 打开段编辑弹窗:将当前段时间填入输入框,重置拆分状态 */
function openEditDialog(taskIdx: number, segIdx: number) {
editingTask.value = tasks.value[taskIdx]
editingSegIdx.value = segIdx
const seg = tasks.value[taskIdx].segments[segIdx]
editStart.value = secToStr(seg.startTime)
editEnd.value = secToStr(seg.endTime)
splitMode.value = 'none'
splitSegs.value = []
dialogError.value = ''
dialogVisible.value = true
}
/** 监听拆分模式切换:自动将当前段均分为 2 或 3 段并填入各子段输入框 */
watch(splitMode, (mode) => {
if (mode === 'none') return
const start = strToSec(editStart.value)
const end = strToSec(editEnd.value)
if (start === null || end === null) return
const count = mode === '2' ? 2 : 3
const dur = Math.round((end - start) / count)
splitSegs.value = Array.from({ length: count }, (_, i) => {
const s = start + dur * i
const e = i === count - 1 ? end : start + dur * (i + 1)
return { start: secToStr(s), end: secToStr(e) }
})
})
/** 编辑弹窗「确认」:校验输入,更新段数据(拆分模式下插入新段),关闭弹窗 */
function onConfirm() {
if (!editingTask.value) return
const seg = editingTask.value.segments[editingSegIdx.value]
const taskIdx = tasks.value.indexOf(editingTask.value)
if (taskIdx < 0) return
const bounds = getTaskBounds(taskIdx)
dialogError.value = ''
if (splitMode.value !== 'none') {
for (let i = 0; i < splitSegs.value.length; i++) {
const s = splitSegs.value[i]
if (!DATE_RE.test(s.start) || !DATE_RE.test(s.end)) {
dialogError.value = `段 ${i + 1} 格式错误,请使用 YYYY-MM-DD HH:mm:ss`; return
}
}
const parsed = splitSegs.value.map(s => ({
start: strToSec(s.start)!,
end: strToSec(s.end)!,
}))
for (let i = 0; i < parsed.length; i++) {
if (isNaN(parsed[i].start) || isNaN(parsed[i].end)) {
dialogError.value = `段 ${i + 1} 时间无效`; return
}
if (parsed[i].end <= parsed[i].start) {
dialogError.value = `段 ${i + 1} 的结束时间必须晚于开始时间`; return
}
if (parsed[i].start < bounds.min) {
dialogError.value = `段 ${i + 1} 的开始时间不能早于 ${formatSecond(bounds.min)}`; return
}
if (parsed[i].end > bounds.max) {
dialogError.value = `段 ${i + 1} 的结束时间不能晚于 ${formatSecond(bounds.max)}`; return
}
}
seg.startTime = parsed[0].start
seg.endTime = parsed[0].end
for (let i = 1; i < parsed.length; i++) {
tasks.value[taskIdx].segments.splice(editingSegIdx.value + i, 0, {
startTime: parsed[i].start, endTime: parsed[i].end,
})
}
} else {
if (!DATE_RE.test(editStart.value) || !DATE_RE.test(editEnd.value)) {
dialogError.value = '时间格式错误,请使用 YYYY-MM-DD HH:mm:ss'; return
}
const newStart = strToSec(editStart.value)!
const newEnd = strToSec(editEnd.value)!
if (isNaN(newStart) || isNaN(newEnd)) { dialogError.value = '时间无效,请检查日期是否存在'; return }
if (newEnd <= newStart) { dialogError.value = '结束时间必须晚于开始时间'; return }
if (newStart < bounds.min) { dialogError.value = `开始时间不能早于 ${formatSecond(bounds.min)}`; return }
if (newEnd > bounds.max) { dialogError.value = `结束时间不能晚于 ${formatSecond(bounds.max)}`; return }
seg.startTime = newStart
seg.endTime = newEnd
}
dialogVisible.value = false
}
/** 外层弹窗「确认」— 输出所有任务的最终段区间并关闭弹窗 */
function onOuterConfirm() {
const result: Record<string, TaskResult> = {}
for (const task of tasks.value) {
const currentSegs: [number, number][] = task.segments.map(s => [
ORIGIN + s.startTime * 1000,
ORIGIN + s.endTime * 1000,
])
let isDirty = false
if (props.trackDirty !== false) {
// 直接比较内部秒数(origSegs 也是秒,无需再做 ms 转换)
const origSegs = originalSegments.value[task.id] ?? []
if (origSegs.length !== task.segments.length) {
isDirty = true
} else {
isDirty = task.segments.some(
(s, i) => s.startTime !== origSegs[i][0] || s.endTime !== origSegs[i][1]
)
}
}
result[task.id] = { segments: currentSegs, isDirty }
}
emit('confirm', result)
localVisible.value = false
}
// ────────────────────────── 缩放 ────────────────────────────
const ZOOM_FACTOR = 1.2 // 按钮每次缩放比例
const MIN_ZOOM_DUR = 3600 // 最小视口:1 小时(秒)
/**
* 鼠标滚轮缩放:以光标所在时间点为中心放大/缩小视口。
* 使用 deltaY 实际值做比例缩放,避免固定档位滚动过快。
* 普通鼠标一格 ≈ 100 单位 → 约 8% 变化;触控板连续滚动更平滑。
*/
function onWheel(e: WheelEvent) {
const rect = containerRef.value?.getBoundingClientRect()
if (!rect) return
const xInTimeline = e.clientX - rect.left - LABEL_W
const ratio = Math.max(0, Math.min(1, xInTimeline / timelineWidth.value))
const mouseSec = viewStart.value + ratio * totalSeconds.value
// 每单位 deltaY 对应 0.08% 缩放,普通鼠标一格 ~8%,触控板极平滑
const factor = Math.pow(1.0008, e.deltaY)
const newDur = Math.max(MIN_ZOOM_DUR, Math.min(_viewMax - _viewMin, totalSeconds.value * factor))
let newStart = mouseSec - ratio * newDur
newStart = Math.max(_viewMin, Math.min(_viewMax - newDur, newStart))
viewStart.value = newStart
viewEnd.value = newStart + newDur
}
/**
* 按钮缩放:以视口中心为轴,direction=-1 放大,direction=1 缩小。
*/
function onZoom(direction: 1 | -1) {
const center = (viewStart.value + viewEnd.value) / 2
const factor = direction > 0 ? ZOOM_FACTOR : 1 / ZOOM_FACTOR
const newDur = Math.max(MIN_ZOOM_DUR, Math.min(_viewMax - _viewMin, totalSeconds.value * factor))
let newStart = center - newDur / 2
newStart = Math.max(_viewMin, Math.min(_viewMax - newDur, newStart))
viewStart.value = newStart
viewEnd.value = newStart + newDur
}
// ────────────────────────── 生命周期 ────────────────────────
/** 更新容器宽度缓存(resize 时调用)*/
function updateWidth() {
if (containerRef.value) containerWidth.value = containerRef.value.offsetWidth
}
onMounted(() => {
updateWidth()
window.addEventListener('mousemove', onMousemove)
window.addEventListener('mouseup', onMouseup)
window.addEventListener('resize', updateWidth)
})
onUnmounted(() => {
window.removeEventListener('mousemove', onMousemove)
window.removeEventListener('mouseup', onMouseup)
window.removeEventListener('resize', updateWidth)
})
</script>
<template>
<ElDialog
v-model="localVisible"
title="时间轴范围选择器"
width="92%"
class="tl-outer-dialog"
append-to-body
>
<div class="tl-root">
<!-- ── Main area ── -->
<div
ref="containerRef"
class="tl-main"
:class="{ 'tl-main--panning': isPanning }"
@mousedown="onMainMousedown"
@wheel.prevent="onWheel"
>
<div class="tl-scroll-inner">
<!-- ── Ruler ── -->
<div class="tl-ruler-row">
<div class="tl-label-cell" />
<div class="tl-ruler">
<template v-for="tick in rulerTicks" :key="tick.x">
<div
class="tl-tick"
:class="tick.major ? 'tl-tick--major' : 'tl-tick--minor'"
:style="{ left: tick.x + 'px' }"
>
<span class="tl-tick-label" v-html="tick.label.replace('\n', '<br>')" />
<div class="tl-tick-line" />
</div>
</template>
</div>
</div>
<!-- ── Task Rows ── -->
<div
v-for="(task, taskIdx) in tasks"
:key="task.id"
class="tl-row"
:class="taskIdx % 2 === 0 ? 'tl-row--odd' : 'tl-row--even'"
>
<!-- Row divider -->
<div v-if="taskIdx > 0" class="tl-row-divider" />
<!-- Label -->
<div class="tl-label-cell">
<span class="tl-task-name">{{ task.name }}</span>
<span class="tl-task-id">id: {{ task.id }}</span>
</div>
<!-- Segments area -->
<div class="tl-segments" :style="{ width: timelineWidth + 'px' }">
<!-- Day grid lines -->
<template v-for="tick in rulerTicks" :key="'grid-' + tick.x">
<div
v-if="tick.major"
class="tl-grid-line"
:style="{ left: tick.x + 'px' }"
/>
</template>
<!-- Overlap regions -->
<template v-for="ov in overlaps" :key="`ov-${ov.taskA}-${ov.taskB}-${ov.start}`">
<template v-if="ov.taskA === taskIdx || ov.taskB === taskIdx">
<div
class="tl-overlap-region"
:style="{
left: secondToX(ov.start) + 'px',
width: (secondToX(ov.end) - secondToX(ov.start)) + 'px',
}"
>
<span class="tl-overlap-badge">
{{ formatOverlap(ov.end - ov.start) }}
</span>
</div>
</template>
</template>
<!-- Limit masks(超出 limit 范围的禁止编辑遮罩)-->
<template v-if="taskLimits[task.id]">
<!-- 左侧禁区(limit 开始之前)-->
<div
class="tl-limit-mask"
:style="{
left: '0',
width: Math.max(0, secondToX(taskLimits[task.id][0])) + 'px',
}"
/>
<!-- 右侧禁区(limit 结束之后)-->
<div
class="tl-limit-mask"
:style="{
left: secondToX(taskLimits[task.id][1]) + 'px',
right: '0',
width: 'auto',
}"
/>
</template>
<!-- Bar segments -->
<template v-for="(seg, segIdx) in task.segments" :key="segIdx">
<div
class="tl-bar"
:style="{
left: secondToX(seg.startTime) + 'px',
width: (secondToX(seg.endTime) - secondToX(seg.startTime)) + 'px',
background: `linear-gradient(180deg, ${task.color} 0%, ${task.colorEnd} 100%)`,
}"
@mousedown="onBarMousedown($event, taskIdx, segIdx)"
/>
<!-- Left handle -->
<div
class="tl-handle tl-handle--left"
:style="{
left: (secondToX(seg.startTime) - 7) + 'px',
borderColor: task.color,
}"
@mousedown.stop="onHandleMousedown($event, taskIdx, segIdx, 'left')"
/>
<!-- Right handle -->
<div
class="tl-handle tl-handle--right"
:style="{
left: (secondToX(seg.endTime) - 7) + 'px',
borderColor: task.color,
}"
@mousedown.stop="onHandleMousedown($event, taskIdx, segIdx, 'right')"
/>
<!-- Time labels -->
<span
class="tl-time-label tl-time-label--left"
:style="{
left: secondToX(seg.startTime) + 'px',
color: task.color,
}"
@click.stop="openEditDialog(taskIdx, segIdx)"
>{{ formatSecond(seg.startTime) }}</span>
<span
class="tl-time-label tl-time-label--right"
:style="{
left: secondToX(seg.endTime) + 'px',
color: task.color,
}"
@click.stop="openEditDialog(taskIdx, segIdx)"
>{{ formatSecond(seg.endTime) }}</span>
</template>
</div>
</div>
</div><!-- /.tl-scroll-inner -->
</div>
<!-- ── Time navigator scrollbar ── -->
<div class="tl-nav-scrollbar">
<div class="tl-nav-spacer">
<div class="tl-zoom-btns">
<button class="tl-zoom-btn" title="缩小" @click="onZoom(1)"></button>
<button class="tl-zoom-btn" title="放大" @click="onZoom(-1)">+</button>
</div>
</div>
<div class="tl-nav-track" @click="onScrollTrackClick">
<div
class="tl-nav-thumb"
:style="{ left: thumbLeft + 'px', width: thumbWidth + 'px' }"
@mousedown="onScrollThumbMousedown"
/>
</div>
</div>
<!-- ── Edit Dialog ── -->
<ElDialog
v-model="dialogVisible"
title="编辑时间范围"
width="640px"
class="tl-dialog"
>
<template v-if="editingTask">
<div class="tl-dialog-meta">
{{ editingTask.name }} · id: {{ editingTask.id }}
</div>
<!-- 拆分方式(置顶) -->
<div class="tl-dialog-split">
<label>拆分方式</label>
<ElRadioGroup v-model="splitMode" size="small">
<ElRadioButton value="none">不拆分</ElRadioButton>
<ElRadioButton value="2">拆分 2 段</ElRadioButton>
<ElRadioButton value="3">拆分 3 段</ElRadioButton>
</ElRadioGroup>
</div>
<!-- 不拆分:两个文本输入框 -->
<template v-if="splitMode === 'none'">
<div class="tl-dialog-row">
<div class="tl-dialog-field">
<label>开始时间</label>
<ElInput v-model="editStart" placeholder="YYYY-MM-DD HH:mm:ss" class="tl-dt-input" />
</div>
<div class="tl-dialog-field">
<label>结束时间</label>
<ElInput v-model="editEnd" placeholder="YYYY-MM-DD HH:mm:ss" class="tl-dt-input" />
</div>
</div>
</template>
<!-- 拆分模式:每段两个输入框 -->
<template v-else>
<div class="tl-split-segs">
<div
v-for="(s, i) in splitSegs"
:key="i"
class="tl-split-seg-row"
>
<div class="tl-split-seg-tag">{{ i + 1 }}</div>
<ElInput v-model="s.start" placeholder="YYYY-MM-DD HH:mm:ss" class="tl-dt-input" />
<span class="tl-seg-sep"></span>
<ElInput v-model="s.end" placeholder="YYYY-MM-DD HH:mm:ss" class="tl-dt-input" />
</div>
</div>
</template>
<div class="tl-dialog-duration" v-if="editingTask.segments[editingSegIdx]">
总持续时长:<span class="tl-duration-val">{{
formatDuration(editingTask.segments[editingSegIdx].endTime - editingTask.segments[editingSegIdx].startTime)
}}</span>
</div>
<div class="tl-dialog-bounds" v-if="currentTaskLimitSec">
限制范围:{{ formatSecond(currentTaskLimitSec[0]) }} ~ {{ formatSecond(currentTaskLimitSec[1]) }}
</div>
<div class="tl-dialog-bounds" v-else>
可用范围:{{ formatSecond(dataBounds.min) }} ~ {{ formatSecond(dataBounds.max) }}
</div>
<div class="tl-dialog-error" v-if="dialogError">{{ dialogError }}</div>
</template>
<template #footer>
<ElButton @click="dialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="onConfirm">确认</ElButton>
</template>
</ElDialog>
</div><!-- /.tl-root -->
<template #footer>
<ElButton @click="localVisible = false">取消</ElButton>
<ElButton type="primary" @click="onOuterConfirm">确认</ElButton>
</template>
</ElDialog>
</template>
<style scoped>
/* ── Root ── */
.tl-root {
font-family: 'Inter', 'PingFang SC', sans-serif;
background: #0F1629;
color: #CBD5E1;
min-width: 800px;
position: relative;
user-select: none;
display: flex;
flex-direction: column;
flex: 1; /* 作为 .el-dialog__body flex 容器的子项,撑满可用高度 */
min-height: 0; /* 允许 flex 压缩,height:100% 在 flex 子项中无法解析 */
}
/* ── Main ── */
.tl-main {
position: relative;
overflow-x: hidden;
overflow-y: auto;
flex: 1;
min-height: 0; /* flex 容器内必须,否则 overflow 不生效 */
}
/* Pan cursor on background */
.tl-scroll-inner { cursor: grab; }
.tl-main--panning .tl-scroll-inner { cursor: grabbing; }
/* But keep bar/handle/label cursors */
.tl-main--panning .tl-bar,
.tl-main--panning .tl-handle { cursor: inherit; }
.tl-scroll-inner {
min-width: 900px;
position: relative;
}
/* ── Time navigator scrollbar ── */
.tl-nav-scrollbar {
display: flex;
align-items: center;
background: #0A1020;
border-top: 1px solid #1A2240;
height: 22px;
padding: 6px 8px 6px 0;
user-select: none;
}
.tl-nav-spacer {
width: 172px;
min-width: 172px;
flex-shrink: 0;
border-right: 1px solid #2D3B5E;
display: flex;
align-items: center;
justify-content: flex-end;
padding-right: 6px;
gap: 4px;
}
.tl-zoom-btns {
display: flex;
gap: 4px;
}
.tl-zoom-btn {
width: 22px;
height: 18px;
background: #1A2A4A;
border: 1px solid #2D3B5E;
border-radius: 4px;
color: #7A9CC6;
font-size: 14px;
line-height: 1;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
transition: background 0.15s, color 0.15s;
}
.tl-zoom-btn:hover { background: #253650; color: #A8C4E0; }
.tl-zoom-btn:active { background: #2D4A6A; }
.tl-nav-track {
flex: 1;
height: 6px;
background: #1A2240;
border-radius: 3px;
position: relative;
cursor: pointer;
margin: 0 4px;
}
.tl-nav-thumb {
position: absolute;
top: 0;
height: 100%;
background: #3D5A80;
border-radius: 3px;
cursor: grab;
transition: background 0.15s;
min-width: 40px;
}
.tl-nav-thumb:hover { background: #507DAE; }
.tl-nav-thumb:active { cursor: grabbing; background: #6196C6; }
/* ── 竖向滚动条(与横向导航条同色系)── */
.tl-main::-webkit-scrollbar {
width: 6px;
}
.tl-main::-webkit-scrollbar-track {
background: #1A2240;
border-radius: 3px;
}
.tl-main::-webkit-scrollbar-thumb {
background: #3D5A80;
border-radius: 3px;
}
.tl-main::-webkit-scrollbar-thumb:hover {
background: #507DAE;
}
.tl-main::-webkit-scrollbar-thumb:active {
background: #6196C6;
}
/* ── Ruler ── */
.tl-ruler-row {
display: flex;
background: #0C1525;
height: 39px;
position: sticky;
top: 0;
z-index: 10; /* 标尺行固定在滚动区顶部,高于任务行 */
position: relative;
}
.tl-label-cell {
width: 172px;
min-width: 172px;
display: flex;
flex-direction: column;
justify-content: center;
padding: 0 14px;
}
.tl-ruler {
flex: 1;
position: relative;
overflow: hidden;
}
.tl-tick {
position: absolute;
transform: translateX(-50%);
display: flex;
flex-direction: column;
align-items: center;
top: 0;
}
.tl-tick-label {
font-size: 10px;
line-height: 1.3;
text-align: center;
display: block;
}
.tl-tick--major .tl-tick-label { color: #94A3B8; }
.tl-tick--minor .tl-tick-label { color: #3D4F6B; }
.tl-tick-line {
width: 1px;
height: 6px;
background: #2D3B5E;
margin-top: 2px;
}
/* ── Row ── */
.tl-row {
display: flex;
height: 104px;
position: relative;
}
.tl-row--odd { background: #1E2845; }
.tl-row--even { background: #1A2240; }
.tl-row-divider {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 1px;
background: #2D3B5E;
z-index: 2;
}
.tl-task-name {
font-size: 14px;
font-weight: 600;
color: #CBD5E1;
}
.tl-task-id {
font-size: 11px;
color: #475569;
margin-top: 4px;
}
/* ── Label cell sticky ── */
.tl-label-cell {
position: sticky;
left: 0;
z-index: 3;
border-right: 1px solid #2D3B5E;
}
.tl-ruler-row .tl-label-cell { background: #0C1525; }
.tl-row--odd .tl-label-cell { background: #1E2845; }
.tl-row--even .tl-label-cell { background: #1A2240; }
/* ── Segments area ── */
.tl-segments {
position: relative;
height: 100%;
overflow: visible;
z-index: 1; /* 形成独立层叠上下文,使内部元素低于标签列(z-index: 3)*/
}
/* ── Grid line ── */
.tl-grid-line {
position: absolute;
top: 0;
bottom: 0;
width: 1px;
background: #243050;
pointer-events: none;
}
/* ── Bar ── */
.tl-bar {
position: absolute;
top: 50%;
transform: translateY(-50%);
height: 28px;
border-radius: 14px;
cursor: grab;
z-index: 2;
transition: filter 0.15s;
}
.tl-bar:active {
cursor: grabbing;
}
.tl-bar:hover {
filter: brightness(1.1);
}
/* ── Handles ── */
.tl-handle {
position: absolute;
top: 50%;
transform: translateY(-50%);
width: 14px;
height: 14px;
border-radius: 50%;
background: #fff;
border: 2px solid;
cursor: ew-resize;
z-index: 5;
}
/* ── Time labels ── */
.tl-time-label {
position: absolute;
top: calc(50% + 18px);
font-size: 10px;
white-space: nowrap;
cursor: pointer;
}
.tl-time-label--left { transform: translateX(-50%); }
.tl-time-label--right { transform: translateX(-50%); }
.tl-time-label:hover {
text-decoration: underline;
}
/* ── Overlap region ── */
.tl-overlap-region {
position: absolute;
top: calc(50% - 14px);
height: 28px;
background: rgba(239, 68, 68, 0.35);
border-left: 1px solid rgba(239, 68, 68, 0.7);
border-right: 1px solid rgba(239, 68, 68, 0.7);
z-index: 4;
pointer-events: none;
}
.tl-limit-mask {
position: absolute;
top: 0; bottom: 0;
background: repeating-linear-gradient(
-45deg,
rgba(0, 0, 0, 0.32),
rgba(0, 0, 0, 0.32) 4px,
transparent 4px,
transparent 8px
);
pointer-events: none;
z-index: 2;
}
.tl-overlap-badge {
position: absolute;
top: -20px;
left: 50%;
transform: translateX(-50%);
background: #EF4444;
border-radius: 4px;
font-size: 10px;
color: #fff;
padding: 1px 5px;
white-space: nowrap;
pointer-events: none;
}
/* ── Dialog ── */
:deep(.tl-dialog) {
--el-dialog-bg-color: #1A2240;
--el-dialog-title-font-size: 15px;
--el-dialog-border-radius: 8px;
--el-color-primary: #3B82F6;
--el-text-color-primary: #CBD5E1;
--el-text-color-regular: #94A3B8;
--el-border-color: #2D3B5E;
--el-fill-color-blank: #0F1629;
--el-input-text-color: #CBD5E1;
--el-input-bg-color: #0C1525;
--el-input-border-color: #2D3B5E;
--el-input-focus-border-color: #3B82F6;
--el-button-bg-color: #1E2845;
--el-button-border-color: #2D3B5E;
--el-button-text-color: #94A3B8;
}
.tl-dialog-meta {
font-size: 12px;
color: #475569;
margin-bottom: 16px;
}
.tl-dialog-row {
display: flex;
gap: 16px;
}
.tl-dialog-field {
flex: 1;
display: flex;
flex-direction: column;
gap: 6px;
}
.tl-dialog-field label {
font-size: 12px;
color: #94A3B8;
}
/* 暗色输入框 */
:deep(.tl-dt-input .el-input__wrapper) {
background: #0C1525;
box-shadow: 0 0 0 1px #2D3B5E inset;
}
:deep(.tl-dt-input .el-input__wrapper:hover) {
box-shadow: 0 0 0 1px #3B82F6 inset;
}
:deep(.tl-dt-input.el-input--focused .el-input__wrapper),
:deep(.tl-dt-input .el-input__wrapper.is-focus) {
box-shadow: 0 0 0 1px #3B82F6 inset;
}
:deep(.tl-dt-input .el-input__inner) {
color: #CBD5E1;
background: transparent;
font-size: 13px;
font-family: 'JetBrains Mono', 'Menlo', monospace;
letter-spacing: 0.3px;
}
:deep(.tl-dt-input .el-input__inner::placeholder) {
color: #344060;
}
.tl-dt-input {
flex: 1;
}
.tl-seg-sep {
color: #64748B;
font-size: 13px;
align-self: center;
flex-shrink: 0;
}
.tl-dialog-duration {
margin-top: 16px;
font-size: 13px;
color: #94A3B8;
}
.tl-duration-val {
color: #34D399;
font-weight: 600;
}
.tl-dialog-bounds {
margin-top: 8px;
font-size: 12px;
color: #64748B;
}
.tl-dialog-error {
margin-top: 6px;
font-size: 12px;
color: #EF4444;
}
.tl-dialog-split {
margin-bottom: 16px;
display: flex;
align-items: center;
gap: 12px;
}
.tl-dialog-split label {
font-size: 12px;
color: #94A3B8;
white-space: nowrap;
}
/* ── Split segments (split mode) ── */
.tl-split-segs {
margin-top: 6px;
}
.tl-split-seg-row {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 12px 0;
border-top: 1px solid #1A2845;
}
.tl-split-seg-row:first-child {
border-top: none;
padding-top: 6px;
}
.tl-split-seg-tag {
font-size: 11px;
font-weight: 600;
color: #60A5FA;
background: rgba(59, 130, 246, 0.12);
border-radius: 4px;
padding: 3px 8px;
white-space: nowrap;
align-self: center;
flex-shrink: 0;
min-width: 46px;
text-align: center;
}
:deep(.tl-dialog .el-radio-button__inner) {
background: #1E2845;
border-color: #2D3B5E;
color: #94A3B8;
}
:deep(.tl-dialog .el-radio-button__original-radio:checked + .el-radio-button__inner) {
background: #3B82F6;
border-color: #3B82F6;
color: #fff;
box-shadow: none;
}
</style>
<!-- Global dialog dark theme overrides (dialog renders inside component, no append-to-body) -->
<style>
/* ── 外层弹窗:全局样式(append-to-body teleport 到 body,scoped 不可达)── */
.tl-outer-dialog.el-dialog {
max-height: 90vh;
margin: 5vh auto; /* 覆盖 Element Plus 默认 margin-top: 15vh,5+90+5=100vh 不超出 */
display: flex;
flex-direction: column;
overflow: hidden;
}
.tl-outer-dialog.el-dialog .el-dialog__body {
flex: 1;
min-height: 0;
overflow: hidden;
padding: 0;
display: flex; /* 让子元素 .tl-root 能用 flex: 1 解析高度 */
flex-direction: column;
}
.tl-dialog.el-dialog {
background: #1A2240;
border: 1px solid #2D3B5E;
border-radius: 8px;
}
.tl-dialog .el-dialog__title {
color: #CBD5E1;
font-size: 15px;
font-weight: 600;
}
.tl-dialog .el-dialog__headerbtn .el-icon {
color: #475569;
}
.tl-dialog .el-dialog__headerbtn:hover .el-icon {
color: #94A3B8;
}
.tl-dialog .el-dialog__body {
color: #94A3B8;
padding: 20px 20px 10px;
}
.tl-dialog .el-dialog__footer {
border-top: 1px solid #2D3B5E;
padding: 12px 20px;
}
.tl-dialog .el-button {
background: #1E2845;
border-color: #2D3B5E;
color: #94A3B8;
}
.tl-dialog .el-button:hover {
background: #2D3B5E;
border-color: #3B82F6;
color: #CBD5E1;
}
.tl-dialog .el-button--primary {
background: #3B82F6 !important;
border-color: #3B82F6 !important;
color: #fff !important;
}
.tl-dialog .el-button--primary:hover {
background: #60A5FA !important;
border-color: #60A5FA !important;
}
.tl-dialog .el-radio-button__inner {
background: #1E2845;
border-color: #2D3B5E;
color: #94A3B8;
}
.tl-dialog .el-radio-button .el-radio-button__original-radio:checked + .el-radio-button__inner {
background: #3B82F6;
border-color: #3B82F6;
color: #fff;
box-shadow: none;
}
.tl-dialog .el-overlay {
background: rgba(0, 0, 0, 0.6);
}
</style>
/** 时间原点:2026-05-15 00:00:00 本地时间(内部秒偏移量以此为基准)*/
export const ORIGIN = new Date(2026, 4, 15).getTime()
export interface TimeSegment {
startTime: number // Unix ms 时间戳
endTime: number // Unix ms 时间戳
}
export interface TimeTask {
id: string
name: string
color: string
colorEnd: string
segments: TimeSegment[]
/**
* 可选:该任务可编辑的时间范围约束(Unix ms 时间戳)。
* 启用 enforceLimit 后,拖拽和编辑均不可超出此范围。
*/
limit?: TimeSegment
}
/** 外层确认时每个任务的输出结构 */
export interface TaskResult {
/** 各段最终时间区间(Unix ms 时间戳对)*/
segments: [number, number][]
/** 相对于传入时是否发生改动(trackDirty !== false 时有效)*/
isDirty: boolean
}
export function defaultTasks(): TimeTask[] {
// d(h, m, s) → seconds from 2026-05-15 00:00:00
// d(h, m, s) → Unix ms 时间戳(从 2026-05-15 00:00 偏移)
const d = (h: number, m = 0, s = 0) => ORIGIN + (h * 3600 + m * 60 + s) * 1000
return [
// ── 单段:跨天,与 B 有重叠 / 限制在 5/15~5/17 内编辑 ──
{
id: '001', name: '任务 A',
color: '#60A5FA', colorEnd: '#2563EB',
segments: [{ startTime: d(18), endTime: d(42) }], // 5/15 18:00 ~ 5/16 18:00
limit: { startTime: d(18), endTime: d(42) }, // 限制:5/15 00:00 ~ 5/17 00:00
},
// ── 单段:与 A、C 均重叠 / 限制在 5/16 内 ──────────────
{
id: '002', name: '任务 B',
color: '#34D399', colorEnd: '#059669',
segments: [{ startTime: d(30), endTime: d(54) }], // 5/16 06:00 ~ 5/17 06:00
limit: { startTime: d(24), endTime: d(72) }, // 限制:5/16 00:00 ~ 5/18 00:00
},
// ── 单段:与 B 末段重叠 ──────────────────────────────────
{
id: '003', name: '任务 C',
color: '#FCD34D', colorEnd: '#D97706',
segments: [{ startTime: d(45), endTime: d(66) }], // 5/16 21:00 ~ 5/17 18:00
},
// ── 单段:较短,仅在第一天 / 限制在 5/15 内 ────────────
{
id: '004', name: '任务 D',
color: '#A78BFA', colorEnd: '#7C3AED',
segments: [{ startTime: d(9), endTime: d(27) }], // 5/15 09:00 ~ 5/16 03:00
limit: { startTime: d(0), endTime: d(36) }, // 限制:5/15 00:00 ~ 5/16 12:00
},
// ── 多段(三段不连续)────────────────────────────────────
{
id: '005', name: '任务 E(多段)',
color: '#F472B6', colorEnd: '#BE185D',
segments: [
{ startTime: d(6), endTime: d(14) }, // 5/15 06:00 ~ 14:00
{ startTime: d(36), endTime: d(48) }, // 5/16 12:00 ~ 5/17 00:00
{ startTime: d(72), endTime: d(84) }, // 5/18 00:00 ~ 12:00
],
},
// ── 多段(两段)+ 短段测试 ────────────────────────────────
{
id: '006', name: '任务 F(短段)',
color: '#FB923C', colorEnd: '#C2410C',
segments: [
{ startTime: d(24 + 6, 30), endTime: d(24 + 8) }, // 5/16 06:30 ~ 08:00(1.5h)
{ startTime: d(24 * 3), endTime: d(24 * 3 + 14) }, // 5/18 00:00 ~ 14:00
],
},
// ── 超长段:跨三天 ───────────────────────────────────────
{
id: '007', name: '任务 G(长跨度)',
color: '#22D3EE', colorEnd: '#0E7490',
segments: [{ startTime: d(48), endTime: d(120) }], // 5/17 00:00 ~ 5/20 00:00
},
// ── 末段:第 6~7 天 ──────────────────────────────────────
{
id: '008', name: '任务 H(末段)',
color: '#86EFAC', colorEnd: '#15803D',
segments: [{ startTime: d(24 * 5 + 12), endTime: d(24 * 6 + 6) }], // 5/20 12:00 ~ 5/21 06:00
},
// ── 多段密集,前两段与 E 有重叠 ─────────────────────────
{
id: '009', name: '任务 I(密集)',
color: '#C084FC', colorEnd: '#7E22CE',
segments: [
{ startTime: d(4), endTime: d(10) }, // 5/15 04:00 ~ 10:00 (与E段1重叠)
{ startTime: d(38), endTime: d(50) }, // 5/16 14:00 ~ 5/17 02:00
{ startTime: d(24 * 4 + 8), endTime: d(24 * 4 + 20) }, // 5/19 08:00 ~ 20:00
],
},
// ── 仅夜间段 ────────────────────────────────────────────
{
id: '010', name: '任务 J(夜间)',
color: '#FDA4AF', colorEnd: '#9F1239',
segments: [
{ startTime: d(22), endTime: d(24 + 4) }, // 5/15 22:00 ~ 5/16 04:00
{ startTime: d(24 * 2 + 22), endTime: d(24 * 3 + 4) }, // 5/17 22:00 ~ 5/18 04:00
{ startTime: d(24 * 4 + 22), endTime: d(24 * 5 + 4) }, // 5/19 22:00 ~ 5/20 04:00
],
},
]
}
import { createApp } from 'vue'
import './style.css'
import 'element-plus/es/components/dialog/style/css'
import 'element-plus/es/components/button/style/css'
import 'element-plus/es/components/radio-group/style/css'
import 'element-plus/es/components/radio-button/style/css'
import 'element-plus/es/components/input/style/css'
import App from './App.vue'
const app = createApp(App)
app.mount('#app')
:root {
--text: #6b6375;
--text-h: #08060d;
--bg: #fff;
--border: #e5e4e7;
--code-bg: #f4f3ec;
--accent: #aa3bff;
--accent-bg: rgba(170, 59, 255, 0.1);
--accent-border: rgba(170, 59, 255, 0.5);
--social-bg: rgba(244, 243, 236, 0.5);
--shadow:
rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px;
--sans: system-ui, 'Segoe UI', Roboto, sans-serif;
--heading: system-ui, 'Segoe UI', Roboto, sans-serif;
--mono: ui-monospace, Consolas, monospace;
font: 18px/145% var(--sans);
letter-spacing: 0.18px;
color-scheme: light dark;
color: var(--text);
background: var(--bg);
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
@media (max-width: 1024px) {
font-size: 16px;
}
}
@media (prefers-color-scheme: dark) {
:root {
--text: #9ca3af;
--text-h: #f3f4f6;
--bg: #16171d;
--border: #2e303a;
--code-bg: #1f2028;
--accent: #c084fc;
--accent-bg: rgba(192, 132, 252, 0.15);
--accent-border: rgba(192, 132, 252, 0.5);
--social-bg: rgba(47, 48, 58, 0.5);
--shadow:
rgba(0, 0, 0, 0.4) 0 10px 15px -3px, rgba(0, 0, 0, 0.25) 0 4px 6px -2px;
}
#social .button-icon {
filter: invert(1) brightness(2);
}
}
body {
margin: 0;
}
h1,
h2 {
font-family: var(--heading);
font-weight: 500;
color: var(--text-h);
}
h1 {
font-size: 56px;
letter-spacing: -1.68px;
margin: 32px 0;
@media (max-width: 1024px) {
font-size: 36px;
margin: 20px 0;
}
}
h2 {
font-size: 24px;
line-height: 118%;
letter-spacing: -0.24px;
margin: 0 0 8px;
@media (max-width: 1024px) {
font-size: 20px;
}
}
p {
margin: 0;
}
code,
.counter {
font-family: var(--mono);
display: inline-flex;
border-radius: 4px;
color: var(--text-h);
}
code {
font-size: 15px;
line-height: 135%;
padding: 4px 8px;
background: var(--code-bg);
}
.counter {
font-size: 16px;
padding: 5px 10px;
border-radius: 5px;
color: var(--accent);
background: var(--accent-bg);
border: 2px solid transparent;
transition: border-color 0.3s;
margin-bottom: 24px;
&:hover {
border-color: var(--accent-border);
}
&:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
}
.hero {
position: relative;
.base,
.framework,
.vite {
inset-inline: 0;
margin: 0 auto;
}
.base {
width: 170px;
position: relative;
z-index: 0;
}
.framework,
.vite {
position: absolute;
}
.framework {
z-index: 1;
top: 34px;
height: 28px;
transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg)
scale(1.4);
}
.vite {
z-index: 0;
top: 107px;
height: 26px;
width: auto;
transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg)
scale(0.8);
}
}
#app {
width: 1126px;
max-width: 100%;
margin: 0 auto;
text-align: center;
border-inline: 1px solid var(--border);
min-height: 100svh;
display: flex;
flex-direction: column;
box-sizing: border-box;
}
#center {
display: flex;
flex-direction: column;
gap: 25px;
place-content: center;
place-items: center;
flex-grow: 1;
@media (max-width: 1024px) {
padding: 32px 20px 24px;
gap: 18px;
}
}
#next-steps {
display: flex;
border-top: 1px solid var(--border);
text-align: left;
& > div {
flex: 1 1 0;
padding: 32px;
@media (max-width: 1024px) {
padding: 24px 20px;
}
}
.icon {
margin-bottom: 16px;
width: 22px;
height: 22px;
}
@media (max-width: 1024px) {
flex-direction: column;
text-align: center;
}
}
#docs {
border-right: 1px solid var(--border);
@media (max-width: 1024px) {
border-right: none;
border-bottom: 1px solid var(--border);
}
}
#next-steps ul {
list-style: none;
padding: 0;
display: flex;
gap: 8px;
margin: 32px 0 0;
.logo {
height: 18px;
}
a {
color: var(--text-h);
font-size: 16px;
border-radius: 6px;
background: var(--social-bg);
display: flex;
padding: 6px 12px;
align-items: center;
gap: 8px;
text-decoration: none;
transition: box-shadow 0.3s;
&:hover {
box-shadow: var(--shadow);
}
.button-icon {
height: 18px;
width: 18px;
}
}
@media (max-width: 1024px) {
margin-top: 20px;
flex-wrap: wrap;
justify-content: center;
li {
flex: 1 1 calc(50% - 8px);
}
a {
width: 100%;
justify-content: center;
box-sizing: border-box;
}
}
}
#spacer {
height: 88px;
border-top: 1px solid var(--border);
@media (max-width: 1024px) {
height: 48px;
}
}
.ticks {
position: relative;
width: 100%;
&::before,
&::after {
content: '';
position: absolute;
top: -4.5px;
border: 5px solid transparent;
}
&::before {
left: 0;
border-left-color: var(--border);
}
&::after {
right: 0;
border-right-color: var(--border);
}
}
/* ── TimeLine 外层弹窗样式(需全局穿透 ElDialog) ── */
.tl-outer-dialog {
background: #0A0F1E !important;
border: 1px solid #1E3A5F !important;
border-radius: 12px !important;
}
.tl-outer-dialog .el-dialog__header {
background: #0F1629;
border-bottom: 1px solid #1E3A5F;
border-radius: 12px 12px 0 0;
padding: 16px 24px;
}
.tl-outer-dialog .el-dialog__title {
color: #E2E8F0;
font-size: 15px;
font-weight: 600;
letter-spacing: 0.3px;
}
.tl-outer-dialog .el-dialog__headerbtn .el-dialog__close {
color: #64748B;
}
.tl-outer-dialog .el-dialog__headerbtn:hover .el-dialog__close {
color: #94A3B8;
}
.tl-outer-dialog .el-dialog__body {
padding: 0;
overflow: hidden;
}
.tl-outer-dialog .el-dialog__footer {
background: #0F1629;
border-top: 1px solid #1E3A5F;
border-radius: 0 0 12px 12px;
padding: 12px 20px;
display: flex;
justify-content: flex-end;
gap: 10px;
}
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"types": ["vite/client"],
/* Linting */
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
}
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "es2023",
"lib": ["ES2023"],
"module": "esnext",
"types": ["node"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true
},
"include": ["vite.config.ts"]
}
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vite.dev/config/
export default defineConfig({
plugins: [vue()],
})
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment