@adonis0123/weekly-report 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-skill.json +46 -0
- package/README.md +63 -0
- package/SKILL.md +174 -0
- package/install-skill.js +315 -0
- package/package.json +35 -0
- package/references/WEEKLY_REPORT_FORMAT.md +116 -0
- package/src/__init__.py +3 -0
- package/src/config_manager.py +171 -0
- package/src/date_utils.py +272 -0
- package/src/git_analyzer.py +342 -0
- package/src/report_generator.py +257 -0
- package/src/storage.py +491 -0
- package/uninstall-skill.js +191 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# 周报格式规范
|
|
2
|
+
|
|
3
|
+
## 角色定义
|
|
4
|
+
|
|
5
|
+
你是一位专业的技术周报总结专家,擅长从 Git commit 记录或文字描述中提炼关键工作内容,生成简洁、结构化的周报摘要。
|
|
6
|
+
|
|
7
|
+
## 任务目标
|
|
8
|
+
|
|
9
|
+
将一周的工作内容(Git commit 记录截图或文字描述)提炼为结构化的周报总结。
|
|
10
|
+
|
|
11
|
+
## 输入来源
|
|
12
|
+
|
|
13
|
+
- **文字输入**:直接提供的工作内容描述
|
|
14
|
+
- **Git 提交**:一周的 Git commit 提交记录
|
|
15
|
+
|
|
16
|
+
## 输出格式
|
|
17
|
+
|
|
18
|
+
使用 Markdown 格式,按项目分组,采用层级列表结构:
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
项目名称
|
|
22
|
+
- 主要工作点(必须,10 字以内,最长不超过 20 字)
|
|
23
|
+
- 补充说明(可选)
|
|
24
|
+
- 更详细的补充(可选)
|
|
25
|
+
|
|
26
|
+
其他
|
|
27
|
+
- 不属于特定项目的工作内容
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## 总结原则
|
|
31
|
+
|
|
32
|
+
### 必须遵守
|
|
33
|
+
|
|
34
|
+
1. **事实导向**:只总结实际完成的工作,不添加虚构或修饰性内容
|
|
35
|
+
2. **简洁精炼**:主要工作点控制在 10 字以内,绝对不超过 20 字
|
|
36
|
+
3. **重点突出**:只保留重要内容,过滤掉琐碎的修改
|
|
37
|
+
4. **按项目分组**:相同项目的工作归类到一起
|
|
38
|
+
5. **层级清晰**:用缩进表示内容的从属关系
|
|
39
|
+
|
|
40
|
+
### 过滤规则
|
|
41
|
+
|
|
42
|
+
以下类型的 commit 通常不需要单独列出:
|
|
43
|
+
|
|
44
|
+
- 纯格式化/代码风格调整(除非是大规模重构)
|
|
45
|
+
- 简单的 typo 修复
|
|
46
|
+
- 依赖版本小幅更新(除非涉及重大升级)
|
|
47
|
+
- 重复性的相似提交(合并为一条)
|
|
48
|
+
- Merge commit
|
|
49
|
+
|
|
50
|
+
### 合并规则
|
|
51
|
+
|
|
52
|
+
- 相关联的多个小改动合并为一个主题
|
|
53
|
+
- 同一功能的多次迭代合并为一条记录
|
|
54
|
+
- 问题排查和解决归为一条
|
|
55
|
+
|
|
56
|
+
## 输出示例
|
|
57
|
+
|
|
58
|
+
### 示例 1
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
project-frontend
|
|
62
|
+
- 构建工具升级改造及问题排查
|
|
63
|
+
- 核心功能开发流程跟进
|
|
64
|
+
- 异常处理优化
|
|
65
|
+
- 方案合理性优化
|
|
66
|
+
- 问题边界定义
|
|
67
|
+
- 对话头像渲染
|
|
68
|
+
- 脚本国际化检查优化
|
|
69
|
+
|
|
70
|
+
其他
|
|
71
|
+
- 新版国际化方案讨论
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### 示例 2
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
project-backend
|
|
78
|
+
- 自定义类型化流式消息渲染
|
|
79
|
+
- 异常状态调试优化
|
|
80
|
+
- 支持 debug 模式
|
|
81
|
+
- 原始消息结构查看
|
|
82
|
+
- device id 修复及有效期处理
|
|
83
|
+
- 断线重连流程梳理
|
|
84
|
+
|
|
85
|
+
其他
|
|
86
|
+
- 全局弹窗缺陷修复
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### 示例 3
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
project-frontend
|
|
93
|
+
- 前后端分离方案跟进
|
|
94
|
+
- 拆分服务(web/admin/server)
|
|
95
|
+
- 统一转发代理消除跨域
|
|
96
|
+
- 活动页需求设计
|
|
97
|
+
- 支持免发版管理
|
|
98
|
+
- SDK 方案调通
|
|
99
|
+
- SEO 预渲染方案
|
|
100
|
+
|
|
101
|
+
project-backend
|
|
102
|
+
- 断线重连方案实现
|
|
103
|
+
- 自动重试失败后提示刷新
|
|
104
|
+
- 依赖库升级尝试
|
|
105
|
+
|
|
106
|
+
其他
|
|
107
|
+
- 多媒体资源运营管理方案讨论
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## 注意事项
|
|
111
|
+
|
|
112
|
+
1. 如果输入是图片,请仔细识别 commit 信息,按时间和项目归类
|
|
113
|
+
2. commit message 中的技术术语可以保留,但要确保表述通顺
|
|
114
|
+
3. 如有多个项目,按工作量或重要性排序
|
|
115
|
+
4. "其他"分类用于放置不属于特定项目的工作内容
|
|
116
|
+
5. 如果某周工作内容较少,如实反映即可,不需要刻意扩充
|
package/src/__init__.py
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""配置管理模块
|
|
2
|
+
|
|
3
|
+
管理周报工具的配置,包括仓库列表等。
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# 默认配置
|
|
12
|
+
DEFAULT_CONFIG: Dict[str, Any] = {
|
|
13
|
+
"repos": [],
|
|
14
|
+
"default_author": "auto",
|
|
15
|
+
"output_format": "markdown",
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def get_config_path(base_dir: Optional[Path] = None) -> Path:
|
|
20
|
+
"""获取配置文件路径
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
base_dir: 基础目录,默认为 ~/.weekly-reports
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
配置文件路径
|
|
27
|
+
"""
|
|
28
|
+
if base_dir is None:
|
|
29
|
+
base_dir = Path.home() / ".weekly-reports"
|
|
30
|
+
|
|
31
|
+
return base_dir / "config.json"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def load_config(config_path: Optional[Path] = None) -> Dict[str, Any]:
|
|
35
|
+
"""加载配置文件
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
config_path: 配置文件路径,默认为 ~/.weekly-reports/config.json
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
配置字典
|
|
42
|
+
"""
|
|
43
|
+
if config_path is None:
|
|
44
|
+
config_path = get_config_path()
|
|
45
|
+
|
|
46
|
+
if not config_path.exists():
|
|
47
|
+
return DEFAULT_CONFIG.copy()
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
with open(config_path, "r", encoding="utf-8") as f:
|
|
51
|
+
config = json.load(f)
|
|
52
|
+
# 合并默认配置,确保所有字段都存在
|
|
53
|
+
return {**DEFAULT_CONFIG, **config}
|
|
54
|
+
except (json.JSONDecodeError, OSError):
|
|
55
|
+
return DEFAULT_CONFIG.copy()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def save_config(
|
|
59
|
+
config: Dict[str, Any],
|
|
60
|
+
config_path: Optional[Path] = None,
|
|
61
|
+
) -> None:
|
|
62
|
+
"""保存配置文件
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
config: 配置字典
|
|
66
|
+
config_path: 配置文件路径
|
|
67
|
+
"""
|
|
68
|
+
if config_path is None:
|
|
69
|
+
config_path = get_config_path()
|
|
70
|
+
|
|
71
|
+
# 确保父目录存在
|
|
72
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
73
|
+
|
|
74
|
+
with open(config_path, "w", encoding="utf-8") as f:
|
|
75
|
+
json.dump(config, f, indent=2, ensure_ascii=False)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def add_repo(
|
|
79
|
+
config: Dict[str, Any],
|
|
80
|
+
name: str,
|
|
81
|
+
path: str,
|
|
82
|
+
) -> Dict[str, Any]:
|
|
83
|
+
"""添加仓库到配置
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
config: 配置字典
|
|
87
|
+
name: 仓库名称
|
|
88
|
+
path: 仓库路径
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
更新后的配置
|
|
92
|
+
"""
|
|
93
|
+
repos = config.get("repos", [])
|
|
94
|
+
|
|
95
|
+
# 检查是否已存在
|
|
96
|
+
for repo in repos:
|
|
97
|
+
if repo["name"] == name:
|
|
98
|
+
# 更新路径
|
|
99
|
+
repo["path"] = path
|
|
100
|
+
return config
|
|
101
|
+
|
|
102
|
+
# 添加新仓库
|
|
103
|
+
repos.append({"name": name, "path": path})
|
|
104
|
+
config["repos"] = repos
|
|
105
|
+
|
|
106
|
+
return config
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def remove_repo(config: Dict[str, Any], name: str) -> Dict[str, Any]:
|
|
110
|
+
"""从配置中移除仓库
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
config: 配置字典
|
|
114
|
+
name: 仓库名称
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
更新后的配置
|
|
118
|
+
"""
|
|
119
|
+
repos = config.get("repos", [])
|
|
120
|
+
config["repos"] = [r for r in repos if r["name"] != name]
|
|
121
|
+
return config
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def get_repos(config: Dict[str, Any]) -> List[Dict[str, str]]:
|
|
125
|
+
"""获取仓库列表
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
config: 配置字典
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
仓库列表
|
|
132
|
+
"""
|
|
133
|
+
return config.get("repos", [])
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def validate_repo(path: Path) -> Tuple[bool, Optional[str]]:
|
|
137
|
+
"""验证仓库路径是否有效
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
path: 仓库路径
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
(is_valid, error_message)
|
|
144
|
+
"""
|
|
145
|
+
if isinstance(path, str):
|
|
146
|
+
path = Path(path)
|
|
147
|
+
|
|
148
|
+
if not path.exists():
|
|
149
|
+
return False, f"路径不存在: {path}"
|
|
150
|
+
|
|
151
|
+
if not path.is_dir():
|
|
152
|
+
return False, f"路径不是目录: {path}"
|
|
153
|
+
|
|
154
|
+
git_dir = path / ".git"
|
|
155
|
+
if not git_dir.exists():
|
|
156
|
+
return False, f"不是有效的 Git 仓库: {path}"
|
|
157
|
+
|
|
158
|
+
return True, None
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def get_repo_paths(config: Dict[str, Any]) -> List[Path]:
|
|
162
|
+
"""获取所有仓库路径
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
config: 配置字典
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
仓库路径列表
|
|
169
|
+
"""
|
|
170
|
+
repos = get_repos(config)
|
|
171
|
+
return [Path(r["path"]) for r in repos]
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
"""日期处理工具模块
|
|
2
|
+
|
|
3
|
+
提供周报日期范围计算和验证功能。
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from datetime import date, datetime, timedelta, timezone
|
|
7
|
+
from typing import Optional, Tuple
|
|
8
|
+
|
|
9
|
+
from dateutil.relativedelta import relativedelta
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# 中国时区(东八区)
|
|
13
|
+
CHINA_TZ = timezone(timedelta(hours=8))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def get_today_china() -> date:
|
|
17
|
+
"""获取中国时区(东八区)的今天日期
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
中国时区的今天日期
|
|
21
|
+
"""
|
|
22
|
+
return datetime.now(CHINA_TZ).date()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_week_range(offset: int = 0) -> Tuple[date, date]:
|
|
26
|
+
"""获取指定周的日期范围(周一到周日)
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
offset: 周偏移量,0 表示本周,-1 表示上周,以此类推
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
(start_date, end_date): 周一和周日的日期元组
|
|
33
|
+
如果周日在未来,则结束日期为今天
|
|
34
|
+
"""
|
|
35
|
+
today = get_today_china()
|
|
36
|
+
|
|
37
|
+
# 计算本周一
|
|
38
|
+
days_since_monday = today.weekday()
|
|
39
|
+
current_monday = today - timedelta(days=days_since_monday)
|
|
40
|
+
|
|
41
|
+
# 应用偏移量
|
|
42
|
+
target_monday = current_monday + timedelta(weeks=offset)
|
|
43
|
+
target_sunday = target_monday + timedelta(days=6)
|
|
44
|
+
|
|
45
|
+
# 结束日期不能超过今天
|
|
46
|
+
end_date = min(target_sunday, today)
|
|
47
|
+
|
|
48
|
+
return target_monday, end_date
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def validate_date_range(
|
|
52
|
+
start: date, end: date
|
|
53
|
+
) -> Tuple[bool, Optional[str]]:
|
|
54
|
+
"""验证日期范围是否有效
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
start: 开始日期
|
|
58
|
+
end: 结束日期
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
(is_valid, error_message): 验证结果和错误信息
|
|
62
|
+
"""
|
|
63
|
+
today = get_today_china()
|
|
64
|
+
|
|
65
|
+
# 检查开始日期是否晚于结束日期
|
|
66
|
+
if start > end:
|
|
67
|
+
return False, "开始日期不能晚于结束日期"
|
|
68
|
+
|
|
69
|
+
# 检查是否选择了未来日期
|
|
70
|
+
if end > today:
|
|
71
|
+
return False, "不能选择未来日期"
|
|
72
|
+
|
|
73
|
+
# 检查开始日期是否是周一
|
|
74
|
+
if start.weekday() != 0:
|
|
75
|
+
return False, "开始日期必须是周一"
|
|
76
|
+
|
|
77
|
+
return True, None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def is_valid_week(start: date, end: date) -> bool:
|
|
81
|
+
"""检查是否为有效的周一到周日
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
start: 开始日期
|
|
85
|
+
end: 结束日期
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
是否为有效的完整周
|
|
89
|
+
"""
|
|
90
|
+
# 开始日期必须是周一
|
|
91
|
+
if start.weekday() != 0:
|
|
92
|
+
return False
|
|
93
|
+
|
|
94
|
+
# 结束日期必须是周日
|
|
95
|
+
if end.weekday() != 6:
|
|
96
|
+
return False
|
|
97
|
+
|
|
98
|
+
# 间隔必须是 6 天(同一周)
|
|
99
|
+
if (end - start).days != 6:
|
|
100
|
+
return False
|
|
101
|
+
|
|
102
|
+
return True
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def get_week_number(d: date) -> int:
|
|
106
|
+
"""获取日期所在的周数
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
d: 日期
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
ISO 周数 (1-53)
|
|
113
|
+
"""
|
|
114
|
+
return d.isocalendar()[1]
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def format_date_range(start: date, end: date) -> str:
|
|
118
|
+
"""格式化日期范围为字符串
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
start: 开始日期
|
|
122
|
+
end: 结束日期
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
格式化的日期范围字符串
|
|
126
|
+
"""
|
|
127
|
+
return f"{start.isoformat()} ~ {end.isoformat()}"
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def get_available_weeks(count: int = 5) -> list:
|
|
131
|
+
"""获取可选择的周列表
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
count: 返回的周数量
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
周列表,每项包含 (offset, start_date, end_date, label)
|
|
138
|
+
"""
|
|
139
|
+
weeks = []
|
|
140
|
+
for i in range(count):
|
|
141
|
+
offset = -i
|
|
142
|
+
start, end = get_week_range(offset)
|
|
143
|
+
|
|
144
|
+
if i == 0:
|
|
145
|
+
label = "本周"
|
|
146
|
+
elif i == 1:
|
|
147
|
+
label = "上周"
|
|
148
|
+
else:
|
|
149
|
+
label = f"{i} 周前"
|
|
150
|
+
|
|
151
|
+
weeks.append({
|
|
152
|
+
"offset": offset,
|
|
153
|
+
"start": start,
|
|
154
|
+
"end": end,
|
|
155
|
+
"label": label,
|
|
156
|
+
"display": f"{label} ({format_date_range(start, end)})",
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
return weeks
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
# ==================== 时间段报告相关函数 ====================
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def get_half_year_range() -> Tuple[date, date]:
|
|
166
|
+
"""获取前半年的日期范围
|
|
167
|
+
|
|
168
|
+
从当前时间往前推 6 个自然月。
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
(start_date, end_date): 从 6 个月前到今天的日期元组
|
|
172
|
+
"""
|
|
173
|
+
today = get_today_china()
|
|
174
|
+
# 使用 relativedelta 计算 6 个月前的日期
|
|
175
|
+
start_date = today - relativedelta(months=6)
|
|
176
|
+
return start_date, today
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def validate_custom_date_range(start: date, end: date) -> Tuple[bool, Optional[str]]:
|
|
180
|
+
"""验证自定义日期范围(无周一限制)
|
|
181
|
+
|
|
182
|
+
用于时间段报告的日期验证,允许任意日期作为起始日。
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
start: 开始日期
|
|
186
|
+
end: 结束日期
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
(is_valid, error_message): 验证结果和错误信息
|
|
190
|
+
"""
|
|
191
|
+
today = get_today_china()
|
|
192
|
+
|
|
193
|
+
# 检查开始日期是否晚于结束日期
|
|
194
|
+
if start > end:
|
|
195
|
+
return False, "开始日期不能晚于结束日期"
|
|
196
|
+
|
|
197
|
+
# 检查是否选择了未来日期
|
|
198
|
+
if end > today:
|
|
199
|
+
return False, "不能选择未来日期"
|
|
200
|
+
|
|
201
|
+
return True, None
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def format_date_for_filename(start: date, end: date) -> str:
|
|
205
|
+
"""生成用于文件名的日期范围字符串
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
start: 开始日期
|
|
209
|
+
end: 结束日期
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
格式为 "YYYY-MM-DD_to_YYYY-MM-DD"
|
|
213
|
+
"""
|
|
214
|
+
return f"{start.isoformat()}_to_{end.isoformat()}"
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def format_period_title(start: date, end: date) -> str:
|
|
218
|
+
"""生成时间段报告的标题
|
|
219
|
+
|
|
220
|
+
Args:
|
|
221
|
+
start: 开始日期
|
|
222
|
+
end: 结束日期
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
格式为 "YYYY-MM-DD ~ YYYY-MM-DD"
|
|
226
|
+
"""
|
|
227
|
+
return format_date_range(start, end)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def get_available_time_ranges() -> list:
|
|
231
|
+
"""获取可选择的时间范围列表(包含周报和时间段报告选项)
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
时间范围列表,每项包含 type, start, end, label, display
|
|
235
|
+
type 可选值: "week" 或 "period"
|
|
236
|
+
"""
|
|
237
|
+
ranges = []
|
|
238
|
+
|
|
239
|
+
# 添加本周和上周(保持原有逻辑)
|
|
240
|
+
weeks = get_available_weeks(count=2)
|
|
241
|
+
for week in weeks:
|
|
242
|
+
ranges.append({
|
|
243
|
+
"type": "week",
|
|
244
|
+
"offset": week["offset"],
|
|
245
|
+
"start": week["start"],
|
|
246
|
+
"end": week["end"],
|
|
247
|
+
"label": week["label"],
|
|
248
|
+
"display": week["display"],
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
# 添加前半年选项
|
|
252
|
+
half_year_start, half_year_end = get_half_year_range()
|
|
253
|
+
ranges.append({
|
|
254
|
+
"type": "period",
|
|
255
|
+
"period_name": "前半年",
|
|
256
|
+
"start": half_year_start,
|
|
257
|
+
"end": half_year_end,
|
|
258
|
+
"label": "前半年",
|
|
259
|
+
"display": f"前半年 ({format_date_range(half_year_start, half_year_end)})",
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
# 添加自定义时间段选项
|
|
263
|
+
ranges.append({
|
|
264
|
+
"type": "period",
|
|
265
|
+
"period_name": "custom",
|
|
266
|
+
"start": None, # 用户输入
|
|
267
|
+
"end": None, # 用户输入(今天)
|
|
268
|
+
"label": "自定义时间段",
|
|
269
|
+
"display": "自定义时间段(输入起始日期,截止到今天)",
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
return ranges
|