后端: - ORDER BY 白名单(sanitizeOrder)堵住 ?order= SQL 注入,补回归测试 - 登录限速(每 IP 10 次失败/10 分钟 429)、TLS/反代下 Secure cookie、NewAPI 构造器 - Delete/setTags/MergeTags/DeleteTag 包事务;Archive 去 500 篇上限 - 列表接口裁剪:不传 content_md,长文 content_html 截 600,新增 content_len;health 探 DB 前端: - EditorView 路由复用串写修复(RouterView :key + sync watch 回写原文章) - v-html 出口统一过 DOMPurify(sanitizeHtml),stripTags 改 DOMParser - 列表竞态防护(Home/Tag/Posts 请求序号)、TagView 分页修复 - 侧栏接口 30s 缓存去重;one:unauthorized 监听器泄漏修复 - 删 styles.css 498 行重复块;移除 tailwind/marked/vue-tsc 死依赖;CommandPalette a11y 语义
68 lines
2.2 KiB
JavaScript
68 lines
2.2 KiB
JavaScript
import DOMPurify from 'dompurify'
|
|
|
|
// 后端 goldmark 已转义原始 HTML,这里再过一道 DOMPurify 作纵深防御,
|
|
// 所有 v-html 出口必须经过它。
|
|
export function sanitizeHtml(html) {
|
|
return DOMPurify.sanitize(html || '')
|
|
}
|
|
|
|
// 用 Intl.DateTimeFormat — locale 感知,未来要 i18n 只换 locale 即可
|
|
const longDateFmt = new Intl.DateTimeFormat('zh-CN', {
|
|
year: 'numeric',
|
|
month: 'long',
|
|
day: 'numeric'
|
|
})
|
|
const shortDateFmt = new Intl.DateTimeFormat('sv-SE') // YYYY-MM-DD
|
|
|
|
export function formatDate(iso) {
|
|
if (!iso) return ''
|
|
const d = new Date(iso)
|
|
if (Number.isNaN(d.getTime())) return iso
|
|
return longDateFmt.format(d)
|
|
}
|
|
|
|
export function formatDateShort(iso) {
|
|
if (!iso) return ''
|
|
const d = new Date(iso)
|
|
if (Number.isNaN(d.getTime())) return iso
|
|
return shortDateFmt.format(d)
|
|
}
|
|
|
|
// 时间线上的相对时间:三天内用「几小时前」,同年省略年份
|
|
export function relativeDate(iso) {
|
|
if (!iso) return ''
|
|
const d = new Date(iso)
|
|
if (Number.isNaN(d.getTime())) return iso
|
|
const diff = Date.now() - d.getTime()
|
|
const hour = 3600 * 1000
|
|
if (diff < hour) return `${Math.max(1, Math.floor(diff / 60000))} 分钟前`
|
|
if (diff < 24 * hour) return `${Math.floor(diff / hour)} 小时前`
|
|
if (diff < 3 * 24 * hour) return `${Math.floor(diff / (24 * hour))} 天前`
|
|
const sameYear = d.getFullYear() === new Date().getFullYear()
|
|
return sameYear
|
|
? `${d.getMonth() + 1} 月 ${d.getDate()} 日`
|
|
: `${d.getFullYear()}/${d.getMonth() + 1}/${d.getDate()}`
|
|
}
|
|
|
|
export function stripTags(html) {
|
|
// 不用 div.innerHTML:那会真正解析并触发 <img onerror> 之类的事件
|
|
return new DOMParser().parseFromString(html || '', 'text/html').body.textContent || ''
|
|
}
|
|
|
|
// 短文在列表里没有标题,用正文首句当索引
|
|
export function displayTitle(post) {
|
|
if (post.kind === 'short') return ''
|
|
return post.title || '无题'
|
|
}
|
|
|
|
export function excerpt(post, limit = 60) {
|
|
if (post.summary) return post.summary
|
|
const text = stripTags(post.content_html || '')
|
|
return text.length > limit ? text.slice(0, limit) + '…' : text
|
|
}
|
|
|
|
export function minutesLabel(post) {
|
|
if (post.kind === 'short') return '短文'
|
|
return `${post.reading_minutes || 1} 分钟`
|
|
}
|