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:
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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 />
|
||||
|
||||
Reference in New Issue
Block a user