Files
iboard/src/pages/task/date.js
T
jif2.zhang 06e9a3e694 fix(任务): 编号改为 #序号展示、详情页完整编号可复制、修复详情改列报「看板列不存在」
- 列表/看板任务编号徽标改为 `#序号`(如 #18), 移除不再需要的 codePrefix 透传
- 详情页标题下新增完整任务编号胶囊 (项目编号-序号, 如 ADMIN-18), 点按复制:
  Clipboard API 优先, 非安全上下文/旧 WebView 回退 execCommand, 并提示已复制
- 修复详情页「看板列」切换报错: 0.2.1 本地库列 id 为 uuid 字符串,
  ActionSheet 的 key 恒为字符串, 旧代码 Number(key) 得到 NaN ->
  update 校验失败抛「看板列不存在」。改为字符串比对 (currentCol/勾选标记同步)
- 详情页保存后先就地替换该行再回源刷新
2026-10-27 10:06:12 +08:00

115 lines
3.9 KiB
JavaScript

// 任务模块前端: 自然日边界 + 保留选项可见性
// 与后端 server/task-domain/date.js 保持同一口径, 改动需同步
export const TZ = 'Asia/Shanghai'
export const RETENTION_LABEL = {
never: '永不显示',
today: '当天',
'3d': '3 天',
week: '本周',
month: '本月',
forever: '永久显示',
}
export const RETENTION_OPTIONS = ['never', 'today', '3d', 'week', 'month', 'forever']
export const STATUS_LABEL = { todo: '未开始', doing: '执行中', done: '完成' }
export const STATUS_OPTIONS = ['todo', 'doing', 'done']
// 任务编号 (T-TK-编号): 服务端分配 `Project.code` + 项目内自增 `Task.number`
// - 列表 / 看板: 只显示 `#序号` (短, 不占卡片宽度)
// - 详情页: 显示完整编号 `项目编号-序号`, 点按复制
// - 离线新建、上行定号前的任务 number 为空 -> 两处都不出编号 (不做假编号)
/** 列表 / 看板的短编号: '#12'; 未定号返回 null */
export function taskShortId(task) {
const n = task?.number
return n === null || n === undefined || n === '' ? null : `#${n}`
}
/** 完整任务编号: 'IB-12'; 未定号返回 null */
export function taskFullId(task, code) {
const n = task?.number
if (n === null || n === undefined || n === '') return null
return code ? `${code}-${n}` : `#${n}`
}
const FMT = new Intl.DateTimeFormat('en-CA', {
timeZone: TZ, year: 'numeric', month: '2-digit', day: '2-digit',
})
export function toDateStr(d = new Date()) {
return FMT.format(d instanceof Date ? d : new Date(d))
}
function toUTC(s) {
const [y, m, d] = String(s).split('-').map(Number)
return new Date(Date.UTC(y, (m || 1) - 1, d || 1))
}
function fromUTC(dt) {
return dt.toISOString().slice(0, 10)
}
export function addDays(s, n) {
const d = toUTC(s)
d.setUTCDate(d.getUTCDate() + n)
return fromUTC(d)
}
/** 周一为一周第一天 */
export function startOfWeek(s) {
const dow = (toUTC(s).getUTCDay() + 6) % 7
return addDays(s, -dow)
}
export function startOfMonth(s) {
return String(s).slice(0, 8) + '01'
}
/** 保留选项 -> 窗口起点 ("YYYY-MM-DD") 或 特殊标记 */
export function retentionStart(option, now = new Date()) {
const today = toDateStr(now)
switch (option) {
case 'never': return { mode: 'never' }
case 'forever': return { mode: 'all' }
case 'today': return { mode: 'since', date: today }
case '3d': return { mode: 'since', date: addDays(today, -2) }
case 'week': return { mode: 'since', date: startOfWeek(today) }
case 'month': return { mode: 'since', date: startOfMonth(today) }
default: return { mode: 'all' }
}
}
/** 单条任务在某保留选项下是否可见 */
export function isVisibleByRetention(task, option, now = new Date()) {
if (task.status !== 'done') return true
const win = retentionStart(option, now)
if (win.mode === 'never') return false
if (win.mode === 'all') return true
if (!task.doneAt) return false
return toDateStr(task.doneAt) >= win.date
}
/** 按任务所属列的保留选项过滤任务 */
export function filterVisible(tasks, columns, now = new Date()) {
const map = new Map((columns || []).map((c) => [c.id, c.retention || 'forever']))
return (tasks || []).filter((t) => isVisibleByRetention(t, map.get(t.columnId) || 'forever', now))
}
/** 距离下一个自然日/周/月边界的毫秒数, 用于定时重算 (T-EX-18) */
export function msToNextBoundary(option, now = new Date()) {
if (!['today', '3d', 'week', 'month'].includes(option)) return null
const today = toDateStr(now)
let next
if (option === 'today' || option === '3d') next = addDays(today, 1)
else if (option === 'week') next = addDays(startOfWeek(today), 7)
else {
const [y, m] = today.split('-').map(Number)
const nm = new Date(Date.UTC(y, m, 1))
next = nm.toISOString().slice(0, 10)
}
// 本地零点转北京时间零点: 用 Date 构造 "next 00:00 +08:00"
const target = new Date(next + 'T00:00:00+08:00').getTime()
return Math.max(1000, target - now.getTime())
}