sync from local backup

This commit is contained in:
chunyu
2026-08-05 23:59:22 +08:00
commit a59f46b894
409 changed files with 166027 additions and 0 deletions
+117
View File
@@ -0,0 +1,117 @@
# CSS 暗色模式覆盖添加计划
## 目标
为 6 个工具页面 CSS 文件添加 `[data-theme="dark"]` 暗色模式覆盖样式。
## 文件清单与分析
### 1. TimestampTool/TimestampTool.css
- **前缀**: `.timestamp-`, `.ts-`
- **主色**: `#8b5cf6` (紫色渐变 #8b5cf6 → #6366f1 → #3b82f6)
- **需要覆盖的类**:
- `.timestamp-page`
- `.timestamp-panel`
- `.ts-panel-header`
- `.ts-panel-title`
- `.ts-panel-dot`
- `.ts-back-btn`
- `.timestamp-timezone-bar`
- `.ts-timezone-label`
- `.ts-result-item`, `.ts-result-label`, `.ts-result-value`
- `.ts-result-copy`
- `.ts-badge-seconds`, `.ts-badge-milliseconds`
- `.ts-empty-hint-icon`
- `.ts-action-clear` (危险操作按钮)
- **说明:用户已提供完整内容,直接追加即可。
### 2. RegexTool/RegexTool.css
- **前缀**: `.regex-`
- **主色**: `#f97316` (橙色渐变 #f97316 → #ea580c → #dc2626)
- **需要覆盖的类**:
- `.regex-page`
- `.regex-panel`
- `.regex-panel-header`
- `.regex-panel-title`
- `.regex-panel-dot`
- `.regex-back-btn`
- `.regex-action-primary` (主按钮 - 保持渐变)
- `.regex-action-danger` (危险按钮)
- `.regex-flag-bar`
- `.regex-preset-btn`
- `.regex-match-count`
- `.regex-result-item`, `.regex-result-index`, `.regex-result-text`, `.regex-result-pos`
- `.regex-highlight-section`, `.regex-highlight-header`, `.regex-highlight-title`
- `.regex-highlight-text` (深色代码背景 #1e1e2e - 保持不变)
- `.regex-highlight-mark`
- `.regex-tip-card`
- `.regex-tip-symbol`, `.regex-tip-desc`
### 3. TextDiff/TextDiff.css
- **前缀**: `.diff-`
- **主色**: `#667eea` (共享 ToolDetail 基础)
- **需要覆盖的类**:
- `.diff-stats-bar`
- `.diff-result-container` (已经是深色背景,保持不变)
- `.diff-line` 行样式保持
- `.diff-line-number`
- `.diff-line-prefix`
- `.diff-line-content`
### 4. EncodingConverter/EncodingConverter.css
- **前缀**: `.ec-`
- **主色**: `#f43f5e` (粉色渐变 #f43f5e → #ec4899 → #fb7185)
- **注意**: 依赖 `.tool-detail-page .tool-detail-hero` (ToolDetail.css 已处理)
- **需要覆盖的类**:
- `.encoding-converter-page .tool-detail-hero` (hero背景)
- `.ec-encoding-selectors`
- `.ec-swap-encoding-btn`
- `.ec-convert-btn` (主按钮,保持渐变)
- `.ec-detected-tag`
### 5. CharsetConverter/CharsetConverter.css
- **前缀**: `.cc-` (charset converter)
- **主色**: `#1677ff` (蓝色)
- **注意**: 无 hero 渐变,但使用 `#1677ff`
- **需要覆盖的类**:
- `.charset-converter-page`
- `.cc-encoding-selectors`
- `.cc-panel`
- `.cc-textarea` (浅色输入背景,但暗色模式用深色)
- `.cc-panel-footer`
- `.cc-output` (浅色输出背景)
- `.cc-swap-encoding-btn`
- `.cc-convert-btn`
### 6. ColorConverter/ColorConverter.css
- **前缀**: `.cc-` (color converter), `.color-converter-`
- **主色**: `#6366f1` (靛蓝色渐变 #6366f1 → #8b5cf6 → #a855f7)
- **注意**: `cc-` 前缀与 CharsetConverter 相同,但使用不同的主色
- **需要覆盖的类**:
- `.color-converter-page`
- `.cc-panel`
- `.cc-panel-header`
- `.cc-panel-title`
- `.cc-panel-dot`
- `.cc-back-btn`
- `.cc-preview-block`
- `.cc-native-picker`
- `.cc-css-input`
- `.cc-format-input`
- `.cc-format-copy`
- `.cc-swap-btn`
- `.cc-section-divider`
- 注意:`.color-converter-toolbar`, `.color-converter-container` 等容器
## 通用规则
1. 页面背景: `#0f172a`
2. 面板背景: `#1e293b`
3. 边框: `#334155`
4. 面板头部: `rgba(主色, 0.08)`
5. 标题/文字: 标题 #f1f5f9, 正文 #94a3b8
6. 返回按钮: 颜色 #94a3b8, 边框 #334155, hover 变主题色
7. textarea 深色代码背景 #1e1e2e 保持不变
8. 危险按钮: 颜色 #f87171, hover 背景 rgba(239,68,68,0.1)
9. 在 `@media` 响应式块之后添加暗色模式
## 编辑方式
每个文件在末尾添加 `/* ===== 暗色模式 =====` 开始的暗色模式覆盖块。
+148
View File
@@ -0,0 +1,148 @@
# 每日学习计时上报功能
## 目标
在课程学习页面(CourseLearn)实现后台静默学习计时,每 5 分钟调用 `tasks.track` 上报分钟数,页面关闭/离线时再次上报剩余时长,驱动"每日学习"和"学习达人"任务自动完成。
## 现状分析
- **后端已就绪**:`TaskTrackAPIView` + `TaskDefinition` 种子数据(每日学习 5min / 学习达人 30min)
- **前端已定义未调用**:`api_request.login.tasks.track()` 已定义但从未使用
- **CourseLearn.tsx 无计时**:当前无任何计时相关代码
- **后端 count 单位 = 分钟**:target_count=5 即 5 分钟
## 设计方案
### 计时逻辑
- 进入课程学习页面即开始计时(页面打开即计时)
- 切到后台也继续计算(不检测活跃时间)
- 不显示计时器(后台静默)
### 上报策略
- **每 5 分钟上报一次**:count=5
- **页面关闭/离线前上报剩余时长**:监听 `beforeunload` + `visibilitychange`(隐藏时)
- **首次进入立即上报 1 分钟**:保证用户停留即有意义,后续每 5 分钟补报
### 数据存储(防丢失)
- 用 `localStorage` 记录当前课程累计已上报分钟数
- key:`learn_time_{courseId}`,value:已上报分钟数
- 页面卸载时同步保存,重新进入时恢复
### 防重复
- 已上报的分钟数不重复上报(通过 localStorage 追踪)
- 页面卸载时把未上报的剩余分钟数一次性补报
## 实现文件
### 1. 新建 `src/hooks/useLearnTimer.ts`(计时 Hook)
```
功能:
- 接收 courseId,返回空(纯副作用)
- 进入时读取 localStorage 恢复已上报时长
- 每 1 分钟 setInterval 累加本地时长
- 每满 5 分钟调用 api_request.login.tasks.track({ action_type: 'learn', count: 5 })
- 上报成功后更新 localStorage 中的已上报值
- beforeunload / visibilitychange(hidden) 时:
- 上报剩余未报分钟数(count = 剩余分钟)
- 使用 navigator.sendBeacon 或同步 XHR 保证可靠性
```
### 2. 修改 `src/pages/CourseLearn/CourseLearn.tsx`
```
- 导入 useLearnTimer
- 在组件内调用 useLearnTimer(courseData?.id)
- 无需新增 UI 元素(后台静默)
```
## 关键代码结构
### useLearnTimer Hook
```typescript
export const useLearnTimer = (courseId: number | undefined) => {
const [reportedMinutes, setReportedMinutes] = useState(0);
const [currentMinutes, setCurrentMinutes] = useState(0);
useEffect(() => {
if (!courseId) return;
// 从 localStorage 恢复
const saved = localStorage.getItem(`learn_time_${courseId}`);
const savedReported = saved ? parseInt(saved, 10) : 0;
setReportedMinutes(savedReported);
setCurrentMinutes(savedReported);
const interval = setInterval(() => {
setCurrentMinutes(prev => {
const next = prev + 1;
const newReported = reportedMinutes + Math.floor((next - reportedMinutes) / 5) * 5;
// 每满 5 分钟上报
const batchesReady = Math.floor((next - reportedMinutes) / 5);
if (batchesReady >= 1) {
const toReport = batchesReady * 5;
trackLearnTime(toReport);
setReportedMinutes(reportedMinutes + toReport);
localStorage.setItem(`learn_time_${courseId}`, String(reportedMinutes + toReport));
}
return next;
});
}, 60000); // 每分钟检查
// 页面卸载上报剩余
const handleUnload = () => {
// 上报剩余分钟(不足 5 分钟的部分也上报)
const remaining = currentMinutes - reportedMinutes;
if (remaining > 0) {
trackLearnTime(remaining);
localStorage.removeItem(`learn_time_${courseId}`);
}
};
window.addEventListener('beforeunload', handleUnload);
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') handleUnload();
});
return () => {
clearInterval(interval);
window.removeEventListener('beforeunload', handleUnload);
};
}, [courseId]);
};
```
### trackLearnTime 辅助函数
```typescript
const trackLearnTime = async (minutes: number) => {
try {
await api_request.login.tasks.track({
action_type: 'learn',
count: minutes,
});
} catch {
// 静默失败,不阻塞用户
}
};
```
## 上报时序示例
| 时间 | 动作 | 累计学习 | 上报 | 后端进度 |
|------|------|----------|------|----------|
| 00:00 | 进入页面 | 0 | - | 0/5 |
| 01:00 | 第 1 分钟 | 1 | - | - |
| 05:00 | 满 5 分钟 | 5 | count=5 | 5/5 ✅ |
| 10:00 | 满 10 分钟 | 10 | count=5 | 10/5(每日学习已完成,累计到学习达人) |
| 15:00 | 关闭页面 | 12 | count=2(剩余) | 12/5 |
## 验证步骤
1. 打开任意课程学习页面
2. 等待 5 分钟,检查 TaskCenter 页面"每日学习"进度是否更新
3. 关闭页面,检查是否有补报请求(Network 面板)
4. 切换语言后确认功能不受影响
5. 未登录用户:计时不上报(或游客模式本地存储,登录后合并上报)
## 假设与决策
- **计时从页面打开开始**:用户选择,无需检测活跃时间
- **每 5 分钟上报一次**:用户选择,平衡实时性与请求频率
- **关闭页面用 beforeunload + visibilitychange**:双重保障,防止离线丢失
- **后台静默不显示 UI**:用户选择
- **action_type='learn'**:匹配种子数据中每日学习/学习达人的 action_type
+210
View File
@@ -0,0 +1,210 @@
# 日期计算器移动端适配修复计划
## 问题描述
日期计算器工具在移动端无法正常使用。当前CSS虽有部分移动端样式,但存在布局错乱、元素溢出、交互困难等问题。
## 当前状态分析
### 现有移动端样式(`@media (max-width: 768px)`)
- `.dc-panels` 已设置为 `grid-template-columns: 1fr`(单列)
- `.dc-actions` 已设置为 `grid-template-columns: 1fr`(单列)
- `.dc-toolbar` 已设置为 `flex-direction: column`
### 存在的移动端问题
1. **Hero区域过高**:200px最小高度 + 40px padding,在移动端占用过多空间
2. **结果项横向溢出**:`dc-result-item` 使用 `flex` 布局,label + value + copy按钮在小屏幕上可能溢出
3. **时区选择器宽度问题**:`min-width: 220px` 在小屏幕上可能超出容器
4. **输入框字体过大**:15px字体在移动端可能导致输入困难
5. **按钮间距不足**:操作按钮在移动端可能难以点击
6. **DatePicker组件**:带 `showTime` 的日期选择器在移动端体验不佳
## 实现方案
### 1. CSS移动端样式优化
**文件**:`DateCalculator.css`
**修改内容**:
#### 1.1 Hero区域压缩
```css
@media (max-width: 768px) {
.date-calculator-hero {
min-height: 120px;
padding: 24px 16px;
}
.date-calculator-hero-icon {
width: 48px;
height: 48px;
font-size: 22px;
margin-bottom: 12px;
}
.date-calculator-hero-title {
font-size: 20px;
letter-spacing: 1px;
}
}
```
#### 1.2 时区栏适配
```css
@media (max-width: 768px) {
.dc-timezone-bar {
flex-wrap: wrap;
padding: 8px 12px;
gap: 8px;
}
.dc-timezone-select {
min-width: 0;
flex: 1;
}
}
```
#### 1.3 结果项垂直布局
```css
@media (max-width: 768px) {
.dc-result-item {
flex-direction: column;
align-items: stretch;
gap: 8px;
padding: 12px;
}
.dc-result-label {
min-width: 0;
font-size: 13px;
}
.dc-result-value {
margin: 0;
width: 100%;
font-size: 14px;
word-break: break-all;
padding: 8px;
background: #f1f5f9;
border-radius: 6px;
}
.dc-result-copy {
align-self: flex-end;
width: 36px;
height: 36px;
}
}
```
#### 1.4 输入区域优化
```css
@media (max-width: 768px) {
.dc-input-area {
font-size: 16px; /* 防止iOS缩放 */
padding: 12px;
}
}
```
#### 1.5 按钮优化
```css
@media (max-width: 768px) {
.dc-action-btn {
height: 52px;
font-size: 16px;
}
.dc-actions {
gap: 12px;
}
}
```
#### 1.6 面板间距优化
```css
@media (max-width: 768px) {
.dc-panels {
gap: 12px;
margin-bottom: 16px;
}
.dc-panel-body {
padding: 12px;
gap: 12px;
}
}
```
### 2. 组件逻辑优化
**文件**:`DateCalculator.tsx`
**修改内容**:
#### 2.1 添加移动端检测
```tsx
const [isMobile, setIsMobile] = useState(false);
useEffect(() => {
const checkMobile = () => {
setIsMobile(window.innerWidth <= 768);
};
checkMobile();
window.addEventListener('resize', checkMobile);
return () => window.removeEventListener('resize', checkMobile);
}, []);
```
#### 2.2 DatePicker移动端优化
在移动端使用更简单的日期选择器,不使用 `showTime`:
```tsx
<DatePicker
showTime={!isMobile}
value={dateInput}
onChange={(val) => setDateInput(val)}
placeholder={t("dateCalculator.datePickPlaceholder")}
format={isMobile ? "YYYY-MM-DD" : "YYYY-MM-DD HH:mm:ss"}
style={{ width: "100%" }}
size="large"
/>
```
### 3. 涉及文件
| 文件 | 修改类型 | 说明 |
|------|----------|------|
| `DateCalculator.css` | 修改 | 优化移动端布局样式 |
| `DateCalculator.tsx` | 修改 | 添加移动端检测,优化DatePicker |
## 验证步骤
1. **Chrome DevTools移动端模拟**
- 打开 DevTools → Toggle device toolbar
- 选择 iPhone SE / iPhone 12 Pro / Pixel 5 等设备
- 验证布局是否正常
2. **真机测试**
- 在手机浏览器中打开 `/utility/date-calculator`
- 测试以下功能:
- 时间戳转日期输入和结果显示
- 日期转时间戳选择日期
- 复制功能
- 获取当前时间
- 清除功能
- 时区切换
3. **交互测试**
- 输入框是否容易点击和输入
- 按钮是否容易点击
- 结果项是否完整显示
- 日期选择器是否正常工作
## 预期效果
- Hero区域高度减少约40%
- 结果项在移动端垂直排列,更易阅读
- 输入框字体16px,防止iOS自动缩放
- 按钮高度52px,更易点击
- 整体布局紧凑,无水平溢出
+109
View File
@@ -0,0 +1,109 @@
# 邮箱验证双重验证实现计划
## 需求总结
在找回密码功能中,用户可能使用虚假邮箱(如 `test@example.com`)导致邮件被退回。需要在发送前验证邮箱真实性,避免无效发送。
## 当前状态分析
### 前端(ForgotPassword.tsx)
- 仅验证邮箱是否为空
- 无格式验证
- 无"必须使用真实邮箱"提示
### 后端(user/views/user.py)
- `ForgotPasswordSendCodeAPIView` 仅验证邮箱是否为空
- 直接调用 `FUser.objects.filter(email=to_email).first()` 查询用户
- 若用户不存在,返回 `USER_NOT_FOUND` 错误
- 无 MX 记录验证
## 实现方案
### 1. 前端验证
**文件**:`src/pages/ForgotPassword/ForgotPassword.tsx`
**修改内容**:
- 增加邮箱格式验证(使用正则表达式)
- 发送前弹出确认提示:"请确保使用真实邮箱,验证码将发送至该邮箱"
- 前端验证不通过时不发送请求
```tsx
const handleSendCode = async () => {
if (!email) {
message.warning(t('forgotPassword.warnEmailRequired'));
return;
}
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
message.warning(t('forgotPassword.warnEmailFormat'));
return;
}
// 继续发送...
};
```
**新增翻译文本**:
- `warnEmailFormat`: "邮箱格式不正确"
- `confirmSend`: "验证码将发送至该邮箱,请确认是真实邮箱"
### 2. 后端 MX 记录验证
**文件**:`user/views/user.py`
**修改内容**:
- 在 `ForgotPasswordSendCodeAPIView.post()` 中增加 MX 记录验证
- 使用 Python `socket` 或 `dns.resolver` 库查询 MX 记录
- 若 MX 记录不存在,直接返回错误
```python
import socket
def validate_email_mx(email):
"""验证邮箱域名是否有MX记录"""
try:
domain = email.split('@')[1]
mx_records = socket.getaddrinfo(domain, None, socket.AF_INET, socket.SOCK_STREAM)
return len(mx_records) > 0
except (socket.gaierror, IndexError):
return False
```
**注意**:QQ Mail 对无效域名会退信,MX 验证可以在发送前拦截。
### 3. 涉及文件
| 文件 | 修改类型 | 说明 |
|------|----------|------|
| `src/pages/ForgotPassword/ForgotPassword.tsx` | 修改 | 增加格式验证和提示 |
| `user/views/user.py` | 修改 | 增加 MX 记录验证 |
| `src/locales/zh.json` | 修改 | 添加错误提示文本 |
## 验证步骤
1. **前端格式验证**
- 输入 `test`(无@符号)→ 提示格式错误
- 输入 `test@example.com` → 继续验证
- 输入 `test@nonexistent-domain-xyz.com` → 后端MX验证拦截
2. **后端 MX 验证**
- 输入 `test@example.com` → MX 不存在,返回错误
- 输入 `test@qq.com` → MX 存在,继续发送
- 输入 `test@gmail.com` → MX 存在,继续发送
3. **端到端测试**
- 使用真实邮箱 `test@qq.com` 测试完整流程
- 确认收到验证码邮件
## 备选方案
如果 MX 验证过于严格(可能误杀某些特殊域名),可以考虑:
- 仅在前端提示,不拦截
- 或添加白名单机制
- 或使用第三方邮箱验证 API
## 注意事项
- MX 验证需要 DNS 查询,可能增加约 100ms 延迟
- 建议结合缓存避免重复查询
- 仅对找回密码等关键场景启用
@@ -0,0 +1,139 @@
# 找回密码邮件发送问题修复计划
## 问题描述
用户在找回密码页面输入邮箱后,页面显示发送成功,但实际未收到验证码邮件。
## 根因分析
### 当前代码流程
1. 用户输入邮箱 → 前端调用 `/api/forgot_password_send_code/`
2. 后端 `ForgotPasswordSendCodeAPIView` 生成验证码并保存到缓存
3. 调用 `submit_task(send_reset_password_email_task, to_email, code)` 发送邮件
4. 前端显示成功
### 发现的问题
#### 1. Celery Worker 可能未运行
- **问题**:`submit_task` 函数先尝试 `apply_async`(异步),如果成功则直接返回
- **风险**:如果 Redis 可达但无 worker 运行,任务会永远留在队列中不被执行
- **验证**:本地测试显示 `apply_async` 返回 PENDING,说明任务已入队但可能无 worker 消费
- **fallback 机制**:`submit_task` 只在 `apply_async` 抛异常或超时时才降级为同步 `apply`
#### 2. QQ Mail 发送限制
- **问题**:QQ Mail 对同一地址发送频率有限制
- **风险**:短时间内多次发送可能被 QQ Mail 服务器静默丢弃
- **特征**:SMTP 连接成功,但邮件未投递
#### 3. 邮件进入垃圾箱
- **问题**:验证码邮件可能被收件方归类为垃圾邮件
- **风险**:用户未检查垃圾邮件箱
## 修复方案
### 修复1:确保邮件同步发送(解决 Celery 不可用问题)
**文件**:`utils/safe_task.py`
**修改**:将 `submit_task` 改为**默认同步发送**,失败时降级为异步
**理由**:
- 验证码邮件需要立即发送,不能依赖 Celery worker 的可用性
- 同步发送确保邮件立即投递
- 如果同步失败,再尝试异步作为 fallback
```python
def submit_task(task, *args, **kwargs):
"""发送任务,优先同步执行,失败时降级为异步"""
try:
# 优先同步执行,确保立即发送
return task.apply(*args, **kwargs)
except Exception as exc:
logger.warning('sync send failed, fallback to async: %s', exc)
try:
return task.apply_async(args, kwargs, kwargs.get('_timeout', SUBMIT_TIMEOUT))
except Exception as exc2:
logger.error('async fallback also failed: %s', exc2)
return None
```
### 修复2:增加邮件发送状态反馈
**文件**:`user/views/user.py` → `ForgotPasswordSendCodeAPIView`
**修改**:
- 在 `submit_task` 调用后检查返回结果
- 如果同步发送失败,返回更明确的错误信息
- 添加日志记录发送状态
```python
result = submit_task(send_reset_password_email_task, to_email, code)
if result is None:
logger.warning(f'Reset password email failed to send: {to_email}')
# 不暴露内部错误给用户,但记录日志
```
### 修复3:前端增加提示
**文件**:`src/pages/ForgotPassword/ForgotPassword.tsx`
**修改**:
- 发送成功后提示用户检查垃圾邮件箱
- 如果发送失败,提示用户稍后重试
```tsx
if (res?.code === 10009) {
message.success(t('forgotPassword.successCodeSent'));
message.info(t('forgotPassword.checkSpam')); // 新增:提示检查垃圾邮件
// ...
}
```
### 修复4:QQ Mail 发送频率优化(可选)
**文件**:`utils/safe_task.py`
**修改**:
- 添加发送间隔限制,避免被 QQ Mail 拦截
- 对同一邮箱,短时间内不重复发送
## 涉及文件
| 文件 | 修改类型 | 说明 |
|------|----------|------|
| `utils/safe_task.py` | 修改 | 改为同步优先发送 |
| `user/views/user.py` | 修改 | 增加发送状态日志 |
| `src/pages/ForgotPassword/ForgotPassword.tsx` | 修改 | 增加垃圾邮件提示 |
| `src/locales/zh.json` | 修改 | 添加提示文本 |
## 验证步骤
1. **本地测试**
- 启动 Django 开发服务器
- 访问找回密码页面
- 输入已注册邮箱
- 检查是否收到邮件
- 检查 Django 日志是否有发送记录
2. **Celery 未运行场景**
- 确保本地无 Celery worker 运行
- 测试邮件发送是否正常
3. **同一邮箱多次发送**
- 连续发送3次验证码
- 检查是否每次都能收到
- 检查是否有频率限制
4. **垃圾邮件检查**
- 检查 QQ Mail 垃圾邮件箱
- 确认邮件是否被归类为垃圾邮件
## 风险评估
- 同步发送会增加请求响应时间(约1-2秒),但验证码邮件场景下可接受
- 如果 SMTP 服务器响应慢,可能导致请求超时
- 需要监控邮件发送失败率
## 备选方案
如果同步发送影响性能,可以考虑:
1. 使用数据库记录发送状态
2. 前端轮询检查邮件是否发送成功
3. 提供"重新发送"按钮
@@ -0,0 +1,80 @@
# 修复分享功能并添加 tasks.track 调用
## 问题分析
1. **分享功能不工作**:当前只是复制链接到剪贴板,没有调用 `tasks.track` 上报分享行为
2. **tasks.track 从未被调用**:API 方法已在 `request.ts` 中定义,但整个项目中没有任何地方调用
## 任务类型(TaskCenter 中定义)
| action_type | 说明 | 调用位置 |
|-------------|------|----------|
| `share` | 分享 | ArticleDetail.tsx, UserHome.tsx |
| `post` | 发表评论 | ArticleDetail.tsx |
| `browse` | 浏览文章 | ArticleDetail.tsx |
| `learn` | 完成学习 | CourseLearn.tsx |
| `profile` | 完善资料 | Profile.tsx |
| `checkin` | 每日签到 | TaskCenter.tsx |
## 实现方案
### 1. 创建 tasks.track 工具函数
**文件**: `src/utils/taskTrack.ts`
封装 `tasks.track` 调用,统一处理错误(静默处理,不影响用户体验):
```typescript
import { api_request } from "@/utils/request";
export const trackTask = async (action_type: string, count: number = 1) => {
try {
await api_request.login.tasks.track({ action_type, count });
} catch {
// 静默处理,不影响用户体验
}
};
```
### 2. 修改分享按钮添加 track 调用
**文件**: `src/pages/ArticleDetail/ArticleDetail.tsx`
- `handleShare` 中添加 `trackTask('share')`
**文件**: `src/pages/UserHome/UserHome.tsx`
- 分享按钮 onClick 中添加 `trackTask('share')`
### 3. 添加评论发表 track 调用
**文件**: `src/pages/ArticleDetail/ArticleDetail.tsx`
- `handleComment` 成功后添加 `trackTask('post')`
- `handleReply` 成功后添加 `trackTask('post')`
### 4. 添加文章浏览 track 调用
**文件**: `src/pages/ArticleDetail/ArticleDetail.tsx`
- 文章加载成功后添加 `trackTask('browse')`
### 5. 添加学习完成 track 调用
**文件**: `src/pages/CourseLearn/CourseLearn.tsx`
- 章节标记完成成功后添加 `trackTask('learn')`
### 6. 添加资料完善 track 调用
**文件**: `src/pages/Profile/Profile.tsx`
- 资料保存成功后添加 `trackTask('profile')`
## 验证步骤
1. 点击分享按钮,检查是否复制链接并调用 track API
2. 发表评论,检查是否调用 track API
3. 浏览文章,检查是否调用 track API
4. 完成学习章节,检查是否调用 track API
5. 完善个人资料,检查是否调用 track API
+81
View File
@@ -0,0 +1,81 @@
# 历史记录按天去重功能实现计划
## 需求总结
当用户在**同一天内**重复点击同一个实用工具或API界面时,不创建新的历史记录,而是**更新已有记录的浏览时间**。
## 当前状态分析
### 前端(useRecordHistory.ts)
- 使用 `useRef` 实现2秒防抖
- **问题**:只在同一组件实例内有效,组件卸载后状态丢失,无法跨导航去重
### 后端(history/serializers.py)
- `BrowsingHistoryCreateSerializer.create()` 已有去重逻辑
- 查询一小时内是否有相同 `user + link` 的记录
- 如果有,调用 `existing.save()` 更新 `viewed_at`(因为 `auto_now=True`)
- **问题**:时间窗口是1小时,不是按天
## 实现方案
### 1. 前端修改:`src/hooks/useRecordHistory.ts`
**目标**:使用 localStorage 实现跨组件/跨导航的按天去重
**修改内容**:
- 添加 `getDailyKey(type, link)` 函数,生成格式为 `history:{type}:{link}:{YYYY-MM-DD}` 的 key
- 添加 `isRecordedToday(type, link)` 函数,检查 localStorage 中是否已存在当天记录
- 添加 `markAsRecorded(type, link)` 函数,在 localStorage 中标记已记录
- 在 `useEffect` 中,先检查 `isRecordedToday`,如果已记录则不发送请求
- 发送请求成功后,调用 `markAsRecorded` 标记
**localStorage 设计**:
- Key: `history:{type}:{link}:{YYYY-MM-DD}`
- Value: 时间戳或简单标记 `"1"`
- 示例: `history:tool:/utility/qrcode-generator:2026-07-28`
### 2. 后端修改:`history/serializers.py`
**目标**:将去重逻辑从"一小时内"改为"当天内"
**修改内容**:
- 修改 `BrowsingHistoryCreateSerializer.create()` 方法
- 将 `one_hour_ago = timezone.now() - timedelta(hours=1)` 改为当天开始时间
- 使用 `viewed_at__date=timezone.now().date()` 匹配当天记录
- 查询条件:`user=user, link=link, viewed_at__date=today`
- 如果找到当天记录,调用 `existing.save()` 更新 `viewed_at`
- 如果没有,创建新记录
### 3. 不需要修改的文件
- `history/models.py` - 模型定义无需修改
- `history/views.py` - 视图逻辑无需修改,serializer 已处理去重
- `history/urls.py` - URL 配置无需修改
## 涉及文件
| 文件 | 修改类型 | 说明 |
|------|----------|------|
| `src/hooks/useRecordHistory.ts` | 修改 | 添加 localStorage 按天去重逻辑 |
| `chunyu_project/history/serializers.py` | 修改 | 将去重时间窗口从1小时改为当天 |
## 验证步骤
1. **同一天内重复点击测试**
- 打开同一个工具/API 页面3次
- 检查历史记录页面,应只有1条记录
- 检查 localStorage,应有对应的 key
2. **跨天测试**
- 第一天点击工具A,记录历史
- 第二天再次点击工具A
- 检查历史记录页面,应有2条记录(分别在不同日期)
3. **不同工具测试**
- 同一天内点击工具A、工具B、工具C
- 检查历史记录页面,应有3条记录
4. **localStorage 清理测试**
- 确认不会无限增长(每天自动覆盖旧日期的 key)
## 风险评估
- localStorage 被用户清除时,去重会失效,但后端仍有兜底逻辑
- 后端修改不影响现有API接口,只是调整了去重时间窗口
+57
View File
@@ -0,0 +1,57 @@
# 邀请好友功能修复方案
## 问题分析
邀请好友页面(`/wallet/invite`)当前显示邀请码和链接,但当用户通过邀请链接注册时,系统未调用 `tasks.track` API 记录邀请行为,导致邀请任务无法完成和领取奖励。
---
## 当前状态
| 项目 | 状态 |
|------|------|
| 邀请页面 | `src/pages/Wallet/Invite.tsx` |
| 邀请 API | `api_request.invite.getCode()` / `getStats()` / `getRecords()` |
| 任务追踪 API | `api_request.tasks.track({ action_type, count })` |
| 注册流程 | `user_login_or_register` 未传递 `invite_code` |
---
## 问题根因
1. **注册时未传递 invite_code** - 新用户通过邀请链接注册时,前端未将邀请码传递给注册 API
2. **未调用 tasks.track** - 注册成功后未记录邀请行为到任务系统
---
## 实现方案
### 修改文件
| 文件 | 修改内容 |
|------|----------|
| `authSlice.tsx` | 登录/注册时传递 `invite_code` |
| `Invite.tsx` | 注册成功后调用 `tasks.track` |
### 详细步骤
#### 1. 从 URL 提取 invite_code
当用户通过邀请链接(如 `https://chunyu.dev/invite/CY123456` 或带 `?invite=CY123456`)访问时,从 URL 中提取邀请码并存储到 localStorage。
#### 2. 注册时传递 invite_code
在 `user_login_or_register` 请求中附加 `invite_code` 参数。
#### 3. 注册成功后调用 tasks.track
注册成功后,调用 `api_request.tasks.track({ action_type: 'invite', count: 1 })` 记录邀请行为。
---
## 验证步骤
1. 访问 `/invite/CY123456` 确认邀请码被正确提取
2. 使用新账号注册,确认注册请求包含 `invite_code`
3. 注册成功后确认 `tasks.track` 被调用
4. 在任务中心确认邀请任务进度更新
+187
View File
@@ -0,0 +1,187 @@
# 编程学习模块教学内容与资料下载实现计划
## 需求总结
为编程学习模块(Python基础)添加三部分教学内容:
1. **文章教学** - Markdown格式的章节内容
2. **视频学习** - 章节视频播放
3. **资料下载** - 新增功能,支持下载课程资料(代码、PDF等)
## 当前状态分析
### 现有架构
- **Course** → **Chapter** → **ChapterContent**(Markdown内容)
- 章节已有 `video_url` 字段,但**缺少资料下载功能**
- `CourseLearn.tsx` 已有视频和文档标签页,但无下载入口
### 需要新增的内容
- 5个Python基础章节的完整Markdown教学内容
- 视频链接(使用免费教学视频)
- **资料下载功能**(新模型 + 前端UI)
## 实现方案
### 1. 新增资料下载模型
**文件**:`learn/models.py`
**新增模型** `CourseMaterial`:
```python
class CourseMaterial(models.Model):
chapter = models.ForeignKey(Chapter, on_delete=models.CASCADE, related_name='materials', verbose_name='章节')
title = models.CharField(max_length=200, verbose_name='资料名称')
file = models.FileField(upload_to='learn/materials/%Y/%m/', verbose_name='文件')
file_type = models.CharField(max_length=20, choices=[('pdf', 'PDF'), ('code', '代码'), ('other', '其他')], default='other')
file_size = models.PositiveIntegerField(default=0, verbose_name='文件大小(字节)')
download_count = models.PositiveIntegerField(default=0, verbose_name='下载次数')
sort_order = models.IntegerField(default=0)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ['sort_order']
verbose_name = '课程资料'
verbose_name_plural = '课程资料'
```
### 2. 新增资料下载API
**文件**:`learn/views.py`
**新增视图**:
- `MaterialListView` - 获取章节资料列表
- `MaterialDownloadView` - 文件下载接口(增加下载次数统计)
**文件**:`learn/serializers.py`
**新增序列化器**:
- `MaterialSerializer` - 资料列表序列化
### 3. 前端资料下载UI
**文件**:`CourseLearn.tsx`
**修改内容**:
- 新增"资料"标签页,显示当前章节的可下载资料
- 资料卡片展示:文件名、类型图标、大小、下载次数
- 点击下载按钮触发文件下载
- 支持PDF、代码文件、压缩包等类型图标
### 4. Python基础教学内容
**文件**:`learn/management/commands/seed_python_content.py`
**新增种子命令**,为Python课程填充5个章节内容:
#### 章节1:Python 简介与安装
- **文章内容**:Python历史、特点、安装步骤、第一个程序
- **视频**:B站Python入门视频链接
- **资料**:Python安装包链接、练习题
#### 章节2:变量与数据类型
- **文章内容**:变量命名、数字、字符串、列表、字典、元组
- **视频**:数据类型详解视频
- **资料**:练习题代码、速查表PDF
#### 章节3:条件判断与循环
- **文章内容**:if/else、for循环、while循环、break/continue
- **视频**:流程控制教学视频
- **资料**:编程练习题、示例代码
#### 章节4:函数与模块
- **文章内容**:函数定义、参数、返回值、模块导入、标准库
- **视频**:函数与模块视频
- **资料**:常用模块速查表、练习代码
#### 章节5:面向对象编程
- **文章内容**:类与对象、继承、多态、封装、魔术方法
- **视频**:OOP教学视频
- **资料**:完整示例代码、OOP设计模式PDF
### 5. 种子数据更新
**文件**:`seed_courses.py`
在创建章节时,同时创建示例 `ChapterContent` 内容(Markdown格式)。
## 涉及文件
### 后端(Django)
| 文件 | 修改类型 | 说明 |
|------|----------|------|
| `learn/models.py` | 修改 | 新增 `CourseMaterial` 模型 |
| `learn/serializers.py` | 修改 | 新增 `MaterialSerializer` |
| `learn/views.py` | 修改 | 新增资料列表和下载接口 |
| `learn/urls.py` | 修改 | 新增资料相关路由 |
| `learn/admin.py` | 修改 | 注册 `CourseMaterial` 模型 |
| `learn/management/commands/seed_python_content.py` | 新增 | Python基础内容种子命令 |
### 前端(React)
| 文件 | 修改类型 | 说明 |
|------|----------|------|
| `CourseLearn.tsx` | 修改 | 新增资料下载标签页和UI |
| `CourseLearn.css` | 修改 | 资料卡片样式 |
| `api/request.ts` | 修改 | 新增资料API调用 |
| `locales/zh.json` | 修改 | 新增资料相关文本 |
## 资料下载功能设计
### 数据库流程
```
Chapter → CourseMaterial(1:N)
用户点击下载 → API查询文件 → 文件流返回 → download_count + 1
```
### 前端展示
```
[视频] [文档] [资料] ← 标签页
┌─────────────────────────┐
│ 📄 Python基础速查表.pdf │
│ 📝 1.2 MB | 128次下载 │
│ [ 立即下载 ] │
├─────────────────────────┤
│ 💻 示例代码.zip │
│ 📝 856 KB | 89次下载 │
│ [ 立即下载 ] │
└─────────────────────────┘
```
### 支持的文件类型
| 类型 | 扩展名 | 图标 |
|------|--------|------|
| PDF | .pdf | 📄 |
| 代码 | .py, .js, .html, .css | 💻 |
| 压缩包 | .zip, .rar, .7z | 📦 |
| 文档 | .doc, .docx, .md | 📝 |
| 其他 | * | 📎 |
## 验证步骤
1. **模型迁移**
```bash
python manage.py makemigrations learn
python manage.py migrate
```
2. **种子数据**
```bash
python manage.py seed_python_content
```
3. **API测试**
- GET /api/learn/materials/?chapter_id=1 - 获取资料列表
- GET /api/learn/materials/1/download/ - 下载文件
4. **前端测试**
- 进入Python课程 → 选择章节 → 点击"资料"标签
- 显示资料列表 → 点击下载按钮 → 文件下载成功
## 风险评估
- 文件上传需要配置 `MEDIA_ROOT` 和 `MEDIA_URL`
- 大文件下载可能需要流式传输
- 下载权限控制(是否需要登录)
- 文件大小限制和存储配额
## 后续优化
- 资料上传进度条
- 批量下载(打包为zip)
- 资料搜索功能
- 用户上传资料审核
@@ -0,0 +1,69 @@
# 时间戳工具移动端适配方案
## 问题分析
时间戳工具(`/utility/timestamp`)当前仅有一个桌面端组件 `TimestampTool.tsx`,通过 CSS Media Query 适配移动端。存在的问题:
### 当前移动端问题
1. **DatePicker 组件不友好** - antd 的 `DatePicker` + `showTime` 在移动端弹窗体验差,日期选择不便
2. **双栏布局空间利用率低** - 两列面板在窄屏上内容拥挤
3. **时区 Select 下拉框难用** - 7 个选项的下拉选择器在移动端操作困难
4. **结果项溢出** - 复制按钮和长文本在小屏上布局错乱
5. **触控目标偏小** - 按钮和输入框未针对触摸优化
## 当前状态
| 项目 | 状态 |
|------|------|
| 桌面端组件 | `TimestampTool.tsx` (已有) |
| 移动端组件 | ❌ 不存在 |
| 路由配置 | App.tsx 未区分移动/桌面端 |
| 移动端样式 | 仅依赖 CSS Media Query |
## 实现方案
### 方案:创建独立移动端组件
遵循项目已有模式(如 `RegexToolMobile`、`JsonFormatterMobile`):
#### 新增文件
| 文件 | 说明 |
|------|------|
| `src/pages/TimestampTool/TimestampToolMobile.tsx` | 移动端专用组件 |
| `src/pages/TimestampTool/TimestampToolMobile.css` | 移动端专用样式 |
#### 修改文件
| 文件 | 修改内容 |
|------|----------|
| `src/App.tsx` | 添加 `TimestampToolMobile` 的懒加载和路由条件渲染 |
### 移动端组件设计要点
1. **单栏流式布局** - 上下排列,充分利用宽度
2. **原生日期选择** - 使用 `<input type="datetime-local">` 替代 antd DatePicker,移动端自动唤起原生选择器
3. **时区快速切换** - 使用按钮组/分段控制器替代 Select 下拉
4. **大号触控按钮** - 所有按钮最小 44px 高度
5. **结果卡片化** - 每个结果独立卡片,易于阅读和操作
6. **固定底部操作栏** - 获取当前时间、清空等常用操作固定在底部
### 核心功能保留
- 时间戳 → 日期转换(自动识别秒/毫秒)
- 日期 → 时间戳转换
- 时区切换
- 复制结果
- 获取当前时间
- 清空输入
## 验证步骤
1. 桌面端访问 `/utility/timestamp` 显示原组件
2. 移动端(≤768px)访问 `/utility/timestamp` 显示新移动端组件
3. 测试时间戳输入转换功能正常
4. 测试日期选择功能正常(原生日期选择器)
5. 测试时区切换功能正常
6. 测试复制功能正常
7. 测试暗色模式切换正常