1
0
mirror of https://github.com/sendevia/website.git synced 2026-03-06 07:40:50 +08:00

feat(Article): remove useTimeFormatter and reimplement custom time formatting

This commit is contained in:
2026-01-05 14:56:57 +08:00
parent d6a6a46327
commit d3d7216b86
2 changed files with 76 additions and 199 deletions

View File

@@ -1,122 +0,0 @@
import { computed } from "vue";
/**
* 时间格式化组合式函数
* 统一处理客户端时间格式化
*/
export function useTimeFormatter() {
/**
* 将时间字符串或时间戳转换为客户端本地时间
*/
const toLocalTime = (time: string | number | Date): Date => {
if (!time) return new Date(0);
if (typeof time === "string") {
// 处理 ISO 格式时间字符串
return new Date(time);
} else if (typeof time === "number") {
// 时间戳
return new Date(time);
} else {
// Date 对象
return time;
}
};
/**
* 格式化日期
* @param time 时间字符串、时间戳或 Date 对象
* @param format 格式类型:'chinese' 或 'iso'
*/
const formatDate = (time: string | number | Date, format: "chinese" | "iso" = "chinese"): string => {
if (!time) return "";
const date = toLocalTime(time);
if (isNaN(date.getTime())) return "";
const year = date.getFullYear();
const month = date.getMonth() + 1;
const day = date.getDate();
const hours = date.getHours();
const minutes = date.getMinutes();
const seconds = date.getSeconds();
if (format === "iso") {
return `${year}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")} ${String(hours).padStart(
2,
"0"
)}:${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
}
return `${year}${month}${day}${hours}${minutes}`;
};
/**
* 计算相对时间(多久以前)
* @param time 过去的时间
* @param now 当前时间,默认为客户端当前时间
*/
const formatTimeAgo = (time: string | number | Date, now: number = Date.now()): string => {
if (!time) return "刚刚";
const pastTime = toLocalTime(time).getTime();
if (isNaN(pastTime)) return "刚刚";
const diffMs = now - pastTime;
if (diffMs <= 0) return "刚刚";
const seconds = Math.floor(diffMs / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);
const months = Math.floor(days / 30);
const years = Math.floor(days / 365);
const timeUnits = [
{ value: years, unit: "年前" },
{ value: months, unit: "个月前" },
{ value: days, unit: "天前" },
{ value: hours, unit: "小时前" },
{ value: minutes, unit: "分钟前" },
{ value: seconds, unit: "秒前" },
];
for (const { value, unit } of timeUnits) {
if (value > 0) return `${value}${unit}`;
}
return "刚刚";
};
/**
* 创建时间格式化计算属性
*/
const useFormattedTime = (
timeSource: () => string | number | Date | undefined | null,
options: {
format?: "chinese" | "iso";
isTimeAgo?: boolean;
now?: number;
} = {}
) => {
const { format = "chinese", isTimeAgo = false, now = Date.now() } = options;
return computed(() => {
const time = timeSource();
if (!time) return "";
if (isTimeAgo) {
return formatTimeAgo(time, now);
} else {
return formatDate(time, format);
}
});
};
return {
toLocalTime,
formatDate,
formatTimeAgo,
useFormattedTime,
};
}

View File

@@ -1,98 +1,105 @@
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount, computed } from "vue";
import { useClipboard } from "@vueuse/core";
import { useClipboard, useTimestamp, useDateFormat } from "@vueuse/core";
import { useGlobalData } from "../composables/useGlobalData";
import { usePostStore } from "../stores/posts";
import { useTimeFormatter } from "../composables/useTimeFormatter";
import { isClient } from "../utils/env";
const showImageViewer = ref(false);
const currentImageIndex = ref(0);
const articleImages = ref<string[]>([]);
const imageOriginPosition = ref({ x: 0, y: 0, width: 0, height: 0 });
const postStore = usePostStore();
const { page, frontmatter } = useGlobalData();
const { copy: copyToClipboard, copied: isCopied } = useClipboard();
const { formatDate, formatTimeAgo, useFormattedTime } = useTimeFormatter();
// 计算当前文章 ID
// 获取当前时间戳
const now = useTimestamp({ interval: 30000 });
// 计算发布时间和最后修改时间
const publishTime = computed(() => frontmatter.value?.date);
const lastUpdatedTime = computed(() => page.value?.lastUpdated);
const formattedPublishDate = computed(() =>
publishTime.value ? useDateFormat(publishTime.value, "YYYY年M月D日").value : ""
);
const lastUpdatedRawTime = computed(() =>
lastUpdatedTime.value ? useDateFormat(lastUpdatedTime.value, "YYYY-MM-DD HH:mm:ss").value : ""
);
// 格式化相对时间函数
function formatTimeAgo(date: string | number | Date) {
const diff = new Date(date).getTime() - now.value; // 计算差值
const absDiff = Math.abs(diff);
// 定义时间单位阈值 (从大到小)
const units = [
{ max: Infinity, val: 31536000000, name: "year" }, // 年
{ max: 31536000000, val: 2592000000, name: "month" }, // 月
{ max: 2592000000, val: 86400000, name: "day" }, // 天
{ max: 86400000, val: 3600000, name: "hour" }, // 时
{ max: 3600000, val: 60000, name: "minute" }, // 分
] as const;
// 使用 Intl.RelativeTimeFormat 返回对应语言的格式
const rtf = new Intl.RelativeTimeFormat("zh-CN", { numeric: "auto" });
for (const { val, name } of units) {
if (absDiff >= val || (name === "minute" && absDiff >= 60000)) {
// 超过该单位的基准值,则使用该单位
// 例如:差值是 2小时 (7200000ms),匹配到 hour (3600000ms)
return rtf.format(Math.round(diff / val), name);
}
}
return "刚刚";
}
// 最终显示的最后修改时间
const formattedLastUpdated = computed(() => {
const uDate = lastUpdatedTime.value ? new Date(lastUpdatedTime.value) : null;
const pDate = publishTime.value ? new Date(publishTime.value) : null;
if (!uDate) return "";
// 如果没有发布时间或发布时间与修改时间极其接近1分钟内显示绝对日期
if (!pDate || Math.abs(uDate.getTime() - pDate.getTime()) < 60000) {
return useDateFormat(uDate, "YYYY年M月D日").value;
}
// 否则显示相对时间 (依赖 now.value会自动更新)
return `${formatTimeAgo(uDate)}编辑`;
});
// 计算文章ID
const articleId = computed(() => {
const relativePath = page.value?.relativePath;
if (!relativePath) return "";
const path = relativePath.replace(/\.md$/, "");
const lookupUrl = path.startsWith("/") ? path : `/${path}`;
const post = postStore.getPostByUrl(lookupUrl);
return post?.id || "";
});
// 生成短链
const shortLink = computed(() => {
if (!articleId.value) return "";
return `/p/${articleId.value}`;
});
// 复制短链到剪贴板
const copyShortLink = async () => {
if (!shortLink.value) return;
const fullUrl = `${window.location.origin}${shortLink.value}`;
await copyToClipboard(fullUrl);
await copyToClipboard(`${window.location.origin}${shortLink.value}`);
};
// 获取发布时间
const publishTime = computed(() => {
return frontmatter.value?.date;
});
// 获取最后修改时间
const lastUpdatedTime = computed(() => {
const val = page.value?.lastUpdated;
if (!val) return undefined;
return val;
});
// 格式化发布日期
const formattedPublishDate = useFormattedTime(() => publishTime.value, { format: "chinese" });
// 原始最后修改时间本地ISO格式
const lastUpdatedRawTime = useFormattedTime(() => lastUpdatedTime.value, { format: "iso" });
// 格式化最后修改时间(显示文本)
const formattedLastUpdated = computed(() => {
const publishTimeValue = publishTime.value;
const lastUpdateTimeValue = lastUpdatedTime.value;
if (!lastUpdateTimeValue) return "";
// 判定是否为发布即最后修改
let isSameTime = false;
if (publishTimeValue && lastUpdateTimeValue) {
const publishDate = new Date(publishTimeValue);
const lastUpdateDate = new Date(lastUpdateTimeValue);
if (!isNaN(publishDate.getTime()) && !isNaN(lastUpdateDate.getTime())) {
isSameTime = Math.abs(lastUpdateDate.getTime() - publishDate.getTime()) < 60000;
}
}
if (isSameTime || !publishTimeValue) {
return formatDate(lastUpdateTimeValue, "chinese");
} else {
return `${formatTimeAgo(lastUpdateTimeValue)}编辑`;
}
});
// 图片查看器相关逻辑
const showImageViewer = ref(false);
const currentImageIndex = ref(0);
const articleImages = ref<string[]>([]);
const imageOriginPosition = ref({ x: 0, y: 0, width: 0, height: 0 });
// 打开图片查看器
function openImageViewer(index: number, event: MouseEvent) {
const contentElement = document.querySelector("#article-content");
if (contentElement) {
const images = Array.from(contentElement.querySelectorAll("img"))
.map((img) => img.src)
.filter((src) => src && !src.includes("data:"));
articleImages.value = images;
currentImageIndex.value = index;
const target = event.target as HTMLElement;
const rect = target.getBoundingClientRect();
imageOriginPosition.value = {
@@ -101,26 +108,26 @@ function openImageViewer(index: number, event: MouseEvent) {
width: rect.width,
height: rect.height,
};
showImageViewer.value = true;
}
}
// 关闭图片查看器
function closeImageViewer() {
showImageViewer.value = false;
}
// 更新当前图片索引
function updateCurrentImageIndex(index: number) {
currentImageIndex.value = index;
}
// 复制锚点链接函数
function copyAnchorLink(this: HTMLElement) {
const anchor = this as HTMLAnchorElement;
const href = anchor.getAttribute("href");
const fullUrl = `${window.location.origin}${window.location.pathname}${href}`;
copyToClipboard(fullUrl);
const label = anchor.querySelector("span.visually-hidden") as HTMLSpanElement;
if (isCopied) {
const originalText = label.textContent;
@@ -131,6 +138,7 @@ function copyAnchorLink(this: HTMLElement) {
}
}
// 自定义无序列表样式函数
function ulCustomBullets() {
const listItems = document.querySelectorAll("ul li") as NodeListOf<HTMLElement>;
listItems.forEach((li, index) => {
@@ -138,12 +146,12 @@ function ulCustomBullets() {
const computedStyle = window.getComputedStyle(li);
const lineHeight = parseFloat(computedStyle.lineHeight);
const bulletTop = lineHeight / 2 - 8;
li.style.setProperty("--random-rotation", `${stableRotation}deg`);
li.style.setProperty("--bullet-top", `${bulletTop}px`);
});
}
// 有序列表数字对齐函数
function olCountAttributes() {
const orderedLists = document.querySelectorAll("ol") as NodeListOf<HTMLElement>;
orderedLists.forEach((ol) => {
@@ -151,12 +159,9 @@ function olCountAttributes() {
const startAttr = ol.getAttribute("start");
const startValue = startAttr ? parseInt(startAttr, 10) : 1;
const effectiveCount = liCount + (startValue - 1);
ol.removeAttribute("data-count-range");
const digitCount = Math.max(1, Math.floor(Math.log10(effectiveCount)) + 1);
const paddingValue = 24 + (digitCount - 1) * 10;
ol.style.setProperty("padding-inline-start", `${paddingValue}px`);
});
}
@@ -167,7 +172,6 @@ if (isClient()) {
anchors.forEach((anchor) => {
anchor.addEventListener("click", copyAnchorLink);
});
function setupImageClickListeners() {
const contentElement = document.querySelector("#article-content");
if (contentElement) {
@@ -177,28 +181,19 @@ if (isClient()) {
});
}
}
ulCustomBullets();
olCountAttributes();
setupImageClickListeners();
window.addEventListener("resize", ulCustomBullets);
const observer = new MutationObserver(() => {
ulCustomBullets();
olCountAttributes();
setupImageClickListeners();
});
const contentElement = document.querySelector("#article-content");
if (contentElement) {
observer.observe(contentElement, {
childList: true,
subtree: true,
characterData: true,
});
observer.observe(contentElement, { childList: true, subtree: true, characterData: true });
}
onBeforeUnmount(() => {
observer.disconnect();
window.removeEventListener("resize", ulCustomBullets);
@@ -229,7 +224,11 @@ if (isClient()) {
</MaterialButton>
<div class="post-info">
<p class="date-publish" v-if="formattedPublishDate">发布于 {{ formattedPublishDate }}</p>
<p class="date-update" :title="lastUpdatedRawTime">{{ formattedLastUpdated }}</p>
<ClientOnly>
<p class="date-update" :title="lastUpdatedRawTime" v-if="formattedLastUpdated">
{{ formattedLastUpdated }}
</p>
</ClientOnly>
<p class="id" v-if="articleId">文章ID {{ articleId }}</p>
</div>
<PageIndicator />