Files
blog/src/components/article/template/ArticleAbout.vue
2025-07-22 12:57:39 +08:00

164 lines
4.4 KiB
Vue

<template>
<div class="article-about">
<div v-if="article">
<markdown-view class="content" :content="article.data" />
<p
class="update-at gray"
v-text="'最后编辑时间:' + Time.toPassedDateTime(article.updatedAt || article.createdAt)"
></p>
</div>
<div class="spectrum" @click="toggleBGM" v-popup:text="'点击暂停/播放'">
<canvas ref="canvas" :width="canvasWidth" height="80"></canvas>
</div>
<audio ref="player" autoplay>
<source src="@/assets/media/fragile.mp3" type="audio/mpeg" />
</audio>
<p class="survival-time light-gray" v-text="survivalTime"></p>
<comment
v-if="SettingMapper.is(SettingKey.ENABLE_COMMENT) && article && article.showComment"
:bizType="CommentBizType.ARTICLE"
:bizId="1"
:titleStickyOffset="49"
:canComment="article.canComment"
/>
</div>
</template>
<script lang="ts" setup>
import { ArticleView, CommentBizType, MarkdownView, SettingKey, SettingMapper, Time } from "timi-web";
import ArticleAPI from "@/api/ArticleAPI";
import { Comment } from "timi-tdesign-pc";
// 文章
const article = ref<ArticleView<any>>();
onMounted(async () => article.value = await ArticleAPI.view(1));
// 运行时间
const survivalTime = ref<string>("网站已运行 ---- 年 --- 天 -- 小时 -- 分钟 -- 秒");
const survivalTimer = ref();
onMounted(async () => {
const begin = new Date("2017/10/9 22:32:52");
clearInterval(survivalTimer.value);
survivalTimer.value = setInterval(() => {
const r = Time.between(begin);
survivalTime.value = `网站已运行 ${r.y}${r.d}${r.h} 小时 ${r.m.toString().padStart(2, "0")} 分钟 ${r.s.toString().padStart(2, "0")}`;
}, 1000);
});
onBeforeUnmount(() => clearInterval(survivalTimer.value));
// 音乐
const player = ref<HTMLAudioElement>();
const canvas = ref<HTMLCanvasElement>();
const canvasWidth = ref(480);
const spectrumRender = ref<number>();
const toggleBGM = () => {
if (player.value) {
if (player.value.paused) {
player.value.play();
} else {
player.value.pause();
}
}
};
const drawSpectrum = () => {
if (!canvas.value || !player.value) {
return;
}
let ctx: any = new AudioContext();
const analyser = ctx.createAnalyser();
const audioSrc = ctx.createMediaElementSource(<HTMLMediaElement>player.value);
audioSrc.connect(analyser);
analyser.connect(ctx.destination);
const cWidth = canvas.value.width,
meterSize = 7,
cHeight = canvas.value.height - 2,
meterWidth = 6,
capHeight = 2,
meterNum = 2400 / meterSize,
capYPositionArray = [];
ctx = canvas.value.getContext("2d");
// 渐变
const gradient = ctx.createLinearGradient(0, 0, 0, 100);
gradient.addColorStop(1, "#A67D7B");
gradient.addColorStop(0.5, "#A67D7B");
gradient.addColorStop(0, "#A67D7B");
const capStyle = "#A67D7B";
// 动画
(function renderFrame() {
const array = new Uint8Array(analyser.frequencyBinCount);
analyser.getByteFrequencyData(array);
const step = Math.round(array.length / meterNum);
ctx.clearRect(0, 0, cWidth, cHeight);
for (let i = 0; i < meterNum; i++) {
const value = array[i * step];
if (capYPositionArray.length < Math.round(meterNum)) {
capYPositionArray.push(value);
}
ctx.fillStyle = capStyle;
if (value < capYPositionArray[i]) {
ctx.fillRect(4 + i * meterSize, cHeight - (--capYPositionArray[i] / 3.2), meterWidth, capHeight);
} else {
ctx.fillRect(4 + i * meterSize, cHeight - value / 3.2, meterWidth, capHeight);
capYPositionArray[i] = value;
}
ctx.fillStyle = gradient;
ctx.fillRect(4 + i * meterSize, cHeight - value / 3.2 + capHeight, meterWidth, cHeight);
}
spectrumRender.value = requestAnimationFrame(renderFrame);
})();
};
onMounted(async () => {
if (player.value) {
player.value.volume = .2;
player.value.addEventListener("loadeddata", () => {
if (player.value && 2 <= player.value.readyState) {
// TODO: 成功 play 才允许绘制
drawSpectrum();
}
});
}
});
onBeforeUnmount(() => {
if (spectrumRender.value) {
cancelAnimationFrame(spectrumRender.value);
}
});
</script>
<style lang="less" scoped>
.article-about {
.content {
width: calc(100% - 2rem);
padding: .5rem 1rem;
}
.update-at {
text-align: right;
padding-right: 1rem;
}
.spectrum {
width: 20rem;
height: 320px;
margin: 0 auto;
display: flex;
background: url("@/assets/img/nagiasu.png") bottom no-repeat;
flex-direction: column;
justify-content: end;
background-size: 100%;
}
.survival-time {
height: 16px;
font-size: 13px;
text-align: center;
}
}
</style>