docs: 精简 README, 补充 Ubuntu 安装与 systemd 部署说明
- 重写 README 为精简版, 包含项目结构/Ubuntu 从零安装/启动关闭使用说明 - vite.config.js 添加 preview.allowedHosts 允许 f.zikai.wang 域名访问 - package-lock.json 随 npm 版本差异更新
This commit is contained in:
219
README.md
219
README.md
@@ -1,103 +1,146 @@
|
||||
# timeTable2 - 纯前端 ICS 日历整理器
|
||||
# timeTableFix - 纯前端 ICS 日历整理器
|
||||
|
||||
浏览器内解析、去重、编辑 ICS 日历文件,全程无需 Python 或后端服务。拖入 `.ics` → 整理去重 → 编辑 → 下载。
|
||||
浏览器内解析、去重、编辑 ICS 日历文件,全程无需后端服务。拖入 `.ics` -> 整理去重 -> 编辑 -> 下载。
|
||||
|
||||
## 功能
|
||||
|
||||
- 📥 **拖入即用**:拖拽或选择 `.ics` 文件,浏览器内直接解析
|
||||
- 🔁 **智能去重**:自动检测重复模式,将 61 个扁平事件合并为 5 个 RRULE 重复事件 + 3 个独立事件
|
||||
- ✏️ **可视化编辑**:编辑标题/地点/时间/重复规则,点击网格切换排除日期
|
||||
- ✂️ **拆分事件**:把一个重复事件按日期拆成前后两段(如后半学期换教室)
|
||||
- ➕ **增删事件**:新增单次/重复事件,删除任意事件
|
||||
- ↶ **撤销重做**:快照式 undo/redo,最多 50 步
|
||||
- ⬇ **导出 ICS**:编辑完成下载整理后的 `.ics`,可直接导入日历应用
|
||||
|
||||
## 快速开始
|
||||
|
||||
```bash
|
||||
# 安装依赖(使用 npmmirror 镜像加速,已配在 .npmrc)
|
||||
npm install
|
||||
|
||||
# 启动开发服务器
|
||||
npm run dev
|
||||
|
||||
# 构建生产版本
|
||||
npm run build
|
||||
|
||||
# 运行单元测试
|
||||
npm test
|
||||
```
|
||||
|
||||
浏览器打开 `http://localhost:5173`,拖入 `.ics` 文件即可。
|
||||
|
||||
## 使用流程
|
||||
|
||||
1. **导入**:拖入 `.ics` 文件(或点击选择),左侧显示所有原始事件
|
||||
2. **去重**:点击顶栏「🔁 整理去重」,自动合并重复事件为 RRULE + EXDATE
|
||||
3. **编辑**:点击左侧事件,右侧展开编辑表单
|
||||
- 修改标题/地点/描述/时间
|
||||
- 调整重复规则(频率/间隔/星期/重复到)
|
||||
- 点击 occurrence 网格切换排除日期(绿色=包含,红色删除线=排除)
|
||||
4. **拆分**(可选):点击「✂ 拆分此日程」,选日期,把重复事件拆成前后两段
|
||||
5. **新增/删除**:点「➕ 新增日程」或「删除此日程」
|
||||
6. **撤销**:「↶ 撤销」/「↷ 重做」最多 50 步
|
||||
7. **下载**:点「⬇ 下载 ICS」,导出整理后的 `.ics`(有未保存更改时会提示确认)
|
||||
- 📥 拖入即用:拖拽或选择 `.ics` 文件,浏览器内直接解析
|
||||
- 🔁 智能去重:自动检测重复模式,将扁平事件合并为 RRULE 重复事件 + EXDATE
|
||||
- ✏️ 可视化编辑:编辑标题/地点/时间/重复规则,点击网格切换排除日期
|
||||
- ✂️ 拆分事件:把一个重复事件按日期拆成前后两段
|
||||
- ➕ 增删事件:新增单次/重复事件,删除任意事件
|
||||
- ↶ 撤销重做:快照式 undo/redo,最多 50 步
|
||||
- ⬇️ 导出 ICS:下载整理后的 `.ics`,可直接导入日历应用
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
timeTable2/
|
||||
├── index.html Vite 入口
|
||||
timeTableFix/
|
||||
├── index.html Vite 入口
|
||||
├── package.json
|
||||
├── .npmrc npmmirror 镜像加速
|
||||
├── vite.config.js
|
||||
├── vite.config.js 构建配置(含 preview.allowedHosts)
|
||||
├── vitest.config.js
|
||||
├── .npmrc npmmirror 镜像加速
|
||||
├── src/
|
||||
│ ├── main.js 应用挂载
|
||||
│ ├── App.vue 布局装配
|
||||
│ ├── main.js 应用挂载
|
||||
│ ├── App.vue 布局装配
|
||||
│ ├── composables/
|
||||
│ │ └── useCalendar.js 中心 store + 快照 undo/redo
|
||||
│ │ └── useCalendar.js 中心 store + 快照 undo/redo
|
||||
│ ├── lib/
|
||||
│ │ ├── weekday.js 星期常量与工具(统一 getDay 索引)
|
||||
│ │ ├── ical-io.js ICS 文本 <-> plain model(ical.js 封装)
|
||||
│ │ ├── recur.js RRULE 网格展开
|
||||
│ │ └── organize.js GCD 去重算法(移植自 Python 版)
|
||||
│ │ ├── weekday.js 星期常量与工具
|
||||
│ │ ├── date.js 日期工具
|
||||
│ │ ├── ical-io.js ICS 文本 <-> plain model
|
||||
│ │ ├── recur.js RRULE 网格展开
|
||||
│ │ ├── organize.js GCD 去重算法
|
||||
│ │ ├── event-factory.js 事件构造工厂
|
||||
│ │ └── split.js 事件拆分逻辑
|
||||
│ └── components/
|
||||
│ ├── HeaderBar.vue 顶栏:整理/撤销/重做/导入/下载
|
||||
│ ├── DropZone.vue 拖拽/选择文件
|
||||
│ ├── EventList.vue 侧栏事件列表
|
||||
│ ├── EventDetail.vue 编辑表单 + 拆分
|
||||
│ └── OccurrenceGrid.vue 排除日期网格
|
||||
└── test/ Vitest 单元测试(34 个)
|
||||
├── golden-org.ics 测试用例(61 事件的真实课表)
|
||||
├── weekday.test.js
|
||||
├── ical-io.test.js
|
||||
├── recur.test.js
|
||||
└── organize.test.js
|
||||
│ ├── HeaderBar.vue 顶栏:整理/撤销/重做/导入/下载
|
||||
│ ├── DropZone.vue 拖拽/选择文件
|
||||
│ ├── EventList.vue 侧栏事件列表
|
||||
│ ├── EventDetail.vue 编辑表单 + 拆分
|
||||
│ └── OccurrenceGrid.vue 排除日期网格
|
||||
├── test/ Vitest 单元测试
|
||||
│ ├── golden-org.ics 测试用例(61 事件的真实课表)
|
||||
│ ├── weekday.test.js
|
||||
│ ├── ical-io.test.js
|
||||
│ ├── recur.test.js
|
||||
│ └── organize.test.js
|
||||
└── docs/ 设计文档
|
||||
```
|
||||
|
||||
### 三个核心 lib 模块
|
||||
## Ubuntu 从零安装
|
||||
|
||||
| 文件 | 作用 |
|
||||
|------|------|
|
||||
| `ical-io.js` | ICS 文本解析为普通对象、序列化回 ICS 文本(ical.js 封装) |
|
||||
| `organize.js` | GCD 间隔检测 + 系列分组 + EXDATE 生成(移植自 `ical_organizer.py`) |
|
||||
| `recur.js` | 从 RRULE + EXDATE 按需展开 occurrence 网格 |
|
||||
### 1. 安装 Node.js 18+
|
||||
|
||||
## 算法
|
||||
```bash
|
||||
curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash -
|
||||
sudo apt-get install -y nodejs
|
||||
node --version # 确认 v18.x
|
||||
```
|
||||
|
||||
去重算法从 `timeTable/ical_organizer.py` 1:1 移植:
|
||||
### 2. 克隆并安装依赖
|
||||
|
||||
1. 按 (标题 + 地点 + 开始时间 + 时长 + 星期) 将事件分组
|
||||
2. 计算组内相邻日期间隔的 **GCD**,识别:
|
||||
- GCD = 7 → `FREQ=WEEKLY`(每周,含 `BYDAY`)
|
||||
- GCD = 1 → `FREQ=DAILY`(每天)
|
||||
- GCD = 14 → `FREQ=WEEKLY;INTERVAL=2`(每两周)
|
||||
3. 构建期望网格,网格上缺失的日期写入 `EXDATE`(如假期)
|
||||
4. 不符合规律的组(跳过次数 ≥ 事件数)保留为独立事件
|
||||
5. 合并后保留首个事件的原始 UID(避免重新导入冲突)
|
||||
```bash
|
||||
git clone https://git.zikai.wang/zikai/timeTableFix.git
|
||||
cd timeTableFix
|
||||
npm install # 已配 npmmirror 镜像加速
|
||||
```
|
||||
|
||||
**验证**:对 `org.ics`(61 事件)整理后输出 8 个事件(5 重复 + 3 独立),与 Python 版 `ical_organizer.py --verify` 结果一致。下载的 ICS 用 Python `recurring-ical-events` 展开,61 个 occurrence 与原始文件完全匹配。
|
||||
### 3. 构建生产版本
|
||||
|
||||
```bash
|
||||
npm run build # 产物输出到 dist/
|
||||
npm test # (可选)运行单元测试
|
||||
```
|
||||
|
||||
### 4. 部署为系统服务(systemd)
|
||||
|
||||
创建服务文件 `/etc/systemd/system/timetable-fix.service`:
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=timeTableFix ICS Calendar Organizer
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
WorkingDirectory=/path/to/timeTableFix
|
||||
ExecStart=/path/to/timeTableFix/node_modules/.bin/vite preview --host 0.0.0.0 --port 8888
|
||||
Restart=on-failure
|
||||
RestartSec=3
|
||||
Environment=NODE_ENV=production
|
||||
StandardOutput=append:/var/log/timetable-preview.log
|
||||
StandardError=append:/var/log/timetable-preview.log
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
> 将上述 `WorkingDirectory` 和 `ExecStart` 中的路径替换为实际的 `timeTableFix` 绝对路径。
|
||||
|
||||
加载并启动:
|
||||
|
||||
```bash
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable --now timetable-fix
|
||||
```
|
||||
|
||||
### 5. 放行防火墙端口
|
||||
|
||||
```bash
|
||||
sudo ufw allow 8888/tcp
|
||||
```
|
||||
|
||||
浏览器访问 `http://<服务器IP>:8888` 即可。若通过域名访问(如 `f.zikai.wang`),需在 `vite.config.js` 的 `preview.allowedHosts` 中添加该域名。
|
||||
|
||||
## 使用(启动 / 关闭)
|
||||
|
||||
服务由 systemd 管理,开机自启、崩溃自动恢复。
|
||||
|
||||
```bash
|
||||
# 启动
|
||||
sudo systemctl start timetable-fix
|
||||
|
||||
# 关闭
|
||||
sudo systemctl stop timetable-fix
|
||||
|
||||
# 重启
|
||||
sudo systemctl restart timetable-fix
|
||||
|
||||
# 查看状态
|
||||
sudo systemctl status timetable-fix
|
||||
|
||||
# 查看日志
|
||||
tail -f /var/log/timetable-preview.log
|
||||
```
|
||||
|
||||
开发模式(无需 systemd,前台运行):
|
||||
|
||||
```bash
|
||||
cd timeTableFix
|
||||
npm run dev # http://localhost:5173
|
||||
```
|
||||
|
||||
## 技术栈
|
||||
|
||||
@@ -105,21 +148,7 @@ timeTable2/
|
||||
|----|------|
|
||||
| 框架 | Vue 3.5(SFC,`<script setup>`) |
|
||||
| 构建 | Vite 6 |
|
||||
| ICS 库 | ical.js 2.x(仅用于文本↔对象转换) |
|
||||
| 状态管理 | composable 单例 store(无 Pinia) |
|
||||
| ICS 库 | ical.js 2.x |
|
||||
| 状态管理 | composable 单例 store |
|
||||
| 测试 | Vitest |
|
||||
| 包源 | npmmirror.com(中国镜像加速) |
|
||||
|
||||
## 与 timeTable 的关系
|
||||
|
||||
`timeTable`(Python 版)使用 Flask + icalendar 库,需创建 venv 并启动本地服务器。
|
||||
`timeTable2` 是纯前端等价实现:零安装、双击即用、算法行为一致。
|
||||
|
||||
| 对比 | timeTable (Python) | timeTable2 (前端) |
|
||||
|------|--------------------|--------------------|
|
||||
| 运行方式 | venv + Flask 服务器 | 浏览器直接打开 |
|
||||
| 依赖 | icalendar, flask, recurring-ical-events | ical.js |
|
||||
| 整理器 | `ical_organizer.py` 命令行 | 网页内「整理去重」按钮 |
|
||||
| 编辑器 | `ical_editor.py` + `editor.html` | Vue3 组件 |
|
||||
| 验证 | `recurring_ical_events` 展开 | 网页内 occurrence 网格 |
|
||||
| 导出 | 保存到磁盘 / 下载 | 下载 `.ics` |
|
||||
|
||||
39
package-lock.json
generated
39
package-lock.json
generated
@@ -603,9 +603,6 @@
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -620,9 +617,6 @@
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -637,9 +631,6 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -654,9 +645,6 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -671,9 +659,6 @@
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -688,9 +673,6 @@
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -705,9 +687,6 @@
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -722,9 +701,6 @@
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -739,9 +715,6 @@
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -756,9 +729,6 @@
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -773,9 +743,6 @@
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -790,9 +757,6 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -807,9 +771,6 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
||||
@@ -4,4 +4,7 @@ import vue from '@vitejs/plugin-vue'
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
base: './',
|
||||
preview: {
|
||||
allowedHosts: ['f.zikai.wang'],
|
||||
},
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user