Initial project
This commit is contained in:
92
src/components/LoginMenu.vue
Normal file
92
src/components/LoginMenu.vue
Normal file
@ -0,0 +1,92 @@
|
||||
<template>
|
||||
<div class="login-menu">
|
||||
<t-dropdown
|
||||
v-if="userStore.isLogged() && userStore.loginUser.user"
|
||||
class="logged"
|
||||
trigger="click"
|
||||
v-popup:config="popupUserStore.config"
|
||||
@mouseenter="popupUserStore.user.value = userStore.loginUser.user"
|
||||
>
|
||||
<t-button class="button" variant="text">
|
||||
<template #icon>
|
||||
<t-avatar
|
||||
class="icon"
|
||||
:class="(<any>ImageType)[userStore.loginUser.user.profile.avatarType]"
|
||||
:image="UserAPI.getAvatarURL(userStore.loginUser.user.profile)"
|
||||
size="small"
|
||||
shape="round"
|
||||
/>
|
||||
</template>
|
||||
<span v-text="userStore.loginUser.user.name"></span>
|
||||
<template #suffix>
|
||||
<icon name="ARROW_1_S" />
|
||||
</template>
|
||||
</t-button>
|
||||
<t-dropdown-menu>
|
||||
<t-dropdown-item @click="toUserSpace">
|
||||
<template #prefixIcon>
|
||||
<icon name="USER" />
|
||||
</template>
|
||||
<span>个人中心</span>
|
||||
</t-dropdown-item>
|
||||
<t-dropdown-item @click="userStore.logout()">
|
||||
<template #prefixIcon>
|
||||
<icon />
|
||||
</template>
|
||||
<span>退出登录</span>
|
||||
</t-dropdown-item>
|
||||
</t-dropdown-menu>
|
||||
</t-dropdown>
|
||||
<t-button
|
||||
v-else
|
||||
variant="text"
|
||||
@click="visitableAuth = true"
|
||||
>
|
||||
<template #icon>
|
||||
<icon class="icon" name="USER" />
|
||||
</template>
|
||||
<span>登录</span>
|
||||
</t-button>
|
||||
</div>
|
||||
<t-dialog
|
||||
v-model:visible="visitableAuth"
|
||||
attach="body"
|
||||
:close-btn="false"
|
||||
:header="false"
|
||||
:footer="false"
|
||||
>
|
||||
<authorize-form
|
||||
v-if="visitableAuth"
|
||||
@login-success="visitableAuth = false"
|
||||
@register-success="visitableAuth = false"
|
||||
/>
|
||||
</t-dialog>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { Icon, ImageType, SettingKey, SettingMapper, UserAPI, userStore } from "timi-web";
|
||||
import { AuthorizeForm, popupUserStore } from "timi-tdesign-pc";
|
||||
|
||||
const visitableAuth = ref<boolean>(false);
|
||||
|
||||
const toUserSpace = () => {
|
||||
const loginUserId = userStore.loginUser.token?.id;
|
||||
if (loginUserId) {
|
||||
window.open(`${SettingMapper.getDomainLink(SettingKey.DOMAIN_SPACE)}/${loginUserId}`, "_blank");
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.login-menu {
|
||||
|
||||
.icon {
|
||||
margin-right: .5rem;
|
||||
}
|
||||
|
||||
.logged {
|
||||
|
||||
&.button {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
59
src/components/article/ArticleList.vue
Normal file
59
src/components/article/ArticleList.vue
Normal file
@ -0,0 +1,59 @@
|
||||
<template>
|
||||
<div class="article-list">
|
||||
<article
|
||||
class="article"
|
||||
v-for="item in list"
|
||||
:key="item.id"
|
||||
>
|
||||
<header class="header">
|
||||
<h4 class="title" >
|
||||
<router-link class="link black" :to="`/aid${item.id}.html`">
|
||||
<span v-text="item.title"></span>
|
||||
</router-link>
|
||||
</h4>
|
||||
</header>
|
||||
<section class="digest gray" v-text="item.digest"></section>
|
||||
</article>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { Article } from "@/types/Article";
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
list: Article<any>[]
|
||||
}>(), {
|
||||
list: () => []
|
||||
});
|
||||
const { list } = toRefs(props);
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.article-list {
|
||||
padding: 1rem;
|
||||
|
||||
.article {
|
||||
margin-bottom: 1rem;
|
||||
|
||||
&:hover {
|
||||
background: #EEE;
|
||||
}
|
||||
|
||||
.header {
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
padding: .25rem 1rem;
|
||||
border-left: 3px solid var(--td-brand-color);
|
||||
}
|
||||
}
|
||||
|
||||
.digest {
|
||||
height: 3rem;
|
||||
font-size: 13px;
|
||||
margin-top: .25rem;
|
||||
padding-left: calc(1rem + 3px);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
164
src/components/article/template/ArticleAbout.vue
Normal file
164
src/components/article/template/ArticleAbout.vue
Normal file
@ -0,0 +1,164 @@
|
||||
<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 { CommentBizType, MarkdownView, SettingKey, SettingMapper, Time } from "timi-web";
|
||||
import ArticleAPI from "@/api/ArticleAPI";
|
||||
import { ArticleView } from "@/types/Article";
|
||||
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>
|
||||
346
src/components/article/template/ArticleMusic.vue
Normal file
346
src/components/article/template/ArticleMusic.vue
Normal file
@ -0,0 +1,346 @@
|
||||
<template>
|
||||
<div v-if="article" class="article-music">
|
||||
<p class="time" v-text="articleTime" @click="isCreatedAt = !isCreatedAt"></p>
|
||||
<!-- 光盘 -->
|
||||
<div class="disk-box" v-if="article.extendData">
|
||||
<div class="cover">
|
||||
<img :src="coverUrl" alt="专辑封面" />
|
||||
</div>
|
||||
<!-- SVG 绘制扇形文字 -->
|
||||
<div class="disk">
|
||||
<svg viewBox="0 0 100 100">
|
||||
<path d="M 0,50 a 50,50 0 1,1 0,1 z" id="circle" />
|
||||
<text fill="#F4F4F4">
|
||||
<textPath xlink:href="#circle">{{ article.extendData.title }}</textPath>
|
||||
</text>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<markdown-view class="description" :content="article.data" />
|
||||
<div class="content selectable" v-if="article.extendData">
|
||||
<section class="list">
|
||||
<h4 class="title">曲目:</h4>
|
||||
<ul class="items">
|
||||
<li class="item" v-for="(item, i) in article.extendData.list" :key="i" :data-i="`${i + 1}.`">
|
||||
<div class="value">
|
||||
<span v-text="item.title"></span>
|
||||
<icon v-if="item.audio" class="icon cur-pointer" name="MUSIC" @click="doActiveMedia(i)" />
|
||||
<icon v-if="item.video" class="icon cur-pointer" name="VIDEO" @click="doActiveMedia(i)" />
|
||||
</div>
|
||||
<div v-if="activeMediaIndex === i" class="media">
|
||||
<audio v-if="item.audio" :src="Toolkit.toResURL(item.audio)" controls></audio>
|
||||
<video v-if="item.video" :src="Toolkit.toResURL(item.video)" controls></video>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
<section class="info">
|
||||
<h4 class="title">信息:</h4>
|
||||
<ul class="items">
|
||||
<li class="item" v-for="(item, i) in article.extendData.info" :key="i">
|
||||
<span class="label diselect" v-text="`${item.key}:`"></span>
|
||||
<span class="gray" v-text="item.value"></span>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
<comment
|
||||
v-if="SettingMapper.is(SettingKey.ENABLE_COMMENT) && article && article.showComment"
|
||||
:bizType="CommentBizType.ARTICLE"
|
||||
:bizId="article.id!"
|
||||
:titleStickyOffset="80"
|
||||
:canComment="article.canComment"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ArticleAttachType, ArticleMusicExtendData, ArticleView } from "@/types/Article.ts";
|
||||
import { CommentBizType, CommonAPI, Icon, MarkdownView, SettingKey, SettingMapper, Time, Toolkit } from "timi-web";
|
||||
import { Comment } from "timi-tdesign-pc";
|
||||
|
||||
const props = defineProps<{
|
||||
article?: ArticleView<ArticleMusicExtendData>,
|
||||
}>();
|
||||
const { article } = toRefs(props);
|
||||
|
||||
const isCreatedAt = ref<boolean>(false);
|
||||
const articleTime = computed(() => {
|
||||
if (article.value) {
|
||||
if (isCreatedAt.value || !article.value.updatedAt) {
|
||||
return "发布于 " + Time.toPassedDateTime(article.value.createdAt);
|
||||
} else {
|
||||
return "编辑于 " + Time.toPassedDateTime(article.value.updatedAt);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 专辑封面
|
||||
const coverUrl = computed(() => {
|
||||
if (article.value && article.value.attachmentList) {
|
||||
for (let i = 0; i < article.value.attachmentList.length; i++) {
|
||||
const item = article.value.attachmentList[i];
|
||||
const attachType = (<any>ArticleAttachType)[item.attachType!];
|
||||
if (attachType === ArticleAttachType.COVER) {
|
||||
return CommonAPI.getAttachmentReadAPI(item.mongoId);
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
})
|
||||
|
||||
// 展开可播放媒体
|
||||
const activeMediaIndex = ref();
|
||||
function doActiveMedia(index: number) {
|
||||
if (index === activeMediaIndex.value) {
|
||||
activeMediaIndex.value = undefined;
|
||||
} else {
|
||||
activeMediaIndex.value = index;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.article-music {
|
||||
|
||||
.time {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.disk-box {
|
||||
width: 628px;
|
||||
height: 430px;
|
||||
margin: 12px auto;
|
||||
display: flex;
|
||||
position: relative;
|
||||
align-items: center;
|
||||
|
||||
.cover {
|
||||
left: 3rem;
|
||||
width: 456px;
|
||||
height: 428px;
|
||||
z-index: 2;
|
||||
padding: 2px 0 0 2px;
|
||||
position: absolute;
|
||||
transition: .3s;
|
||||
|
||||
/* 蒙板 */
|
||||
&::after {
|
||||
content: '';
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 458px;
|
||||
height: 430px;
|
||||
z-index: 3;
|
||||
position: absolute;
|
||||
background: url('~@/assets/img/disk.png') no-repeat;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: var(--tui-border);
|
||||
box-shadow: var(--tui-shadow);
|
||||
}
|
||||
}
|
||||
|
||||
.disk {
|
||||
top: 50%;
|
||||
right: 3rem;
|
||||
color: #F4F4F4;
|
||||
width: 16rem;
|
||||
height: 16rem;
|
||||
border: 3px solid #888;
|
||||
padding: 40px;
|
||||
z-index: 1;
|
||||
position: absolute;
|
||||
font-size: .5rem;
|
||||
transform: rotateZ(50deg);
|
||||
transition: .3s;
|
||||
margin-top: calc(-43px - 8rem);
|
||||
background: #212121;
|
||||
border-radius: 50%;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
margin: -23px 0 0 -23px;
|
||||
border: 8px solid #BBB;
|
||||
position: absolute;
|
||||
background: #F4F4F4;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
svg {
|
||||
display: block;
|
||||
overflow: visible;
|
||||
|
||||
path {
|
||||
fill: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:active {
|
||||
|
||||
.cover {
|
||||
left: 0;
|
||||
transition: 520ms var(--tui-bezier);
|
||||
}
|
||||
|
||||
.disk {
|
||||
right: 0;
|
||||
transform: rotateZ(120deg);
|
||||
transition: 520ms var(--tui-bezier);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.description {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
padding: 12px 20px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
|
||||
.list {
|
||||
flex: 10;
|
||||
border: 1px solid #CDDEF0;
|
||||
min-width: 440px;
|
||||
background: #FFF;
|
||||
margin-right: 1.5rem;
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
padding: .5rem 1rem;
|
||||
background: #CDDEF0;
|
||||
}
|
||||
|
||||
.items {
|
||||
margin: 0;
|
||||
position: relative;
|
||||
list-style: none;
|
||||
padding-left: 0;
|
||||
|
||||
.item {
|
||||
padding: 4px 0;
|
||||
display: flex;
|
||||
line-height: 1.5;
|
||||
flex-direction: column;
|
||||
|
||||
&::before {
|
||||
content: attr(data-i);
|
||||
color: #525870;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
position: absolute;
|
||||
text-align: right;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
&:nth-child(2n) {
|
||||
background: #F4F4F4;
|
||||
}
|
||||
|
||||
.value {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-indent: 2.5em;
|
||||
|
||||
.icon {
|
||||
margin-left: .5rem;
|
||||
|
||||
&:hover {
|
||||
fill: #FF7A9B;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.media {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
audio,
|
||||
video {
|
||||
width: calc(100% - 5em);
|
||||
margin: 4px auto;
|
||||
border: var(--tui-border);
|
||||
box-shadow: var(--tui-shadow);
|
||||
}
|
||||
|
||||
audio {
|
||||
height: 2.5rem;
|
||||
background: #F1F3F4;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.info {
|
||||
color: #666;
|
||||
flex: 2;
|
||||
min-width: 220px;
|
||||
text-align: left;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 24px;
|
||||
|
||||
.title {
|
||||
margin: .5rem 0 0 0;
|
||||
}
|
||||
|
||||
.items {
|
||||
padding: 4px;
|
||||
font-size: 12px;
|
||||
margin-top: 0;
|
||||
|
||||
.item {
|
||||
padding: 6px 0;
|
||||
line-height: 1.5;
|
||||
border-bottom: 1px dashed #777;
|
||||
|
||||
.label {
|
||||
color: #525870;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@media screen and (max-width: 680px) {
|
||||
.article-music {
|
||||
|
||||
.disk-box {
|
||||
width: calc(628px * .5);
|
||||
height: calc(430px * .5);
|
||||
|
||||
.cover {
|
||||
left: 3rem;
|
||||
width: calc(456px * .5);
|
||||
height: calc(428px * .5);
|
||||
|
||||
&::after {
|
||||
width: calc(458px * .5);
|
||||
height: calc(430px * .5);
|
||||
}
|
||||
}
|
||||
|
||||
.disk {
|
||||
width: calc(16rem * .4);
|
||||
height: calc(16rem * .4);
|
||||
margin-top: calc(-43px - 8rem * .4);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
52
src/components/article/template/ArticlePublic.vue
Normal file
52
src/components/article/template/ArticlePublic.vue
Normal file
@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<div v-if="article" class="article-public">
|
||||
<div class="time" v-text="articleTime" @click="isCreatedAt = !isCreatedAt"></div>
|
||||
<markdown-view class="content" :content="article.data" />
|
||||
<comment
|
||||
v-if="SettingMapper.is(SettingKey.ENABLE_COMMENT) && article.showComment"
|
||||
:bizType="CommentBizType.ARTICLE"
|
||||
:bizId="article.id!"
|
||||
:titleStickyOffset="80"
|
||||
:canComment="article.canComment"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { CommentBizType, MarkdownView, SettingKey, SettingMapper, Time } from "timi-web";
|
||||
import { ArticleView } from "@/types/Article";
|
||||
import { Comment } from "timi-tdesign-pc";
|
||||
|
||||
const props = defineProps<{
|
||||
article?: ArticleView<any>,
|
||||
}>();
|
||||
const { article } = toRefs(props);
|
||||
|
||||
const isCreatedAt = ref<boolean>(false);
|
||||
|
||||
const articleTime = computed(() => {
|
||||
if (article.value) {
|
||||
if (isCreatedAt.value || !article.value.updatedAt) {
|
||||
return "发布于 " + Time.toPassedDateTime(article.value.createdAt);
|
||||
} else {
|
||||
return "编辑于 " + Time.toPassedDateTime(article.value.updatedAt);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.article-public {
|
||||
|
||||
.time {
|
||||
text-align: center;
|
||||
margin: 1rem 0 2rem 0;
|
||||
}
|
||||
|
||||
.content {
|
||||
width: calc(100% - 2rem);
|
||||
padding: .5rem 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
168
src/components/article/template/ArticleSoftware.vue
Normal file
168
src/components/article/template/ArticleSoftware.vue
Normal file
@ -0,0 +1,168 @@
|
||||
<template>
|
||||
<div v-if="article" class="article-software">
|
||||
<p class="time" v-text="articleTime" @click="isCreatedAt = !isCreatedAt"></p>
|
||||
<div class="header">
|
||||
<img class="logo" :src="coverUrl" alt="程序封面" />
|
||||
<div v-if="article.extendData" class="info">
|
||||
<div>
|
||||
<span>官网:</span>
|
||||
<a :href="article.extendData.url" v-text="article.extendData.url"></a>
|
||||
</div>
|
||||
<div>
|
||||
<span>版本:</span>
|
||||
<span class="selectable" v-text="article.extendData.version"></span>
|
||||
</div>
|
||||
<div>
|
||||
<span>文件格式:</span>
|
||||
<span class="selectable" v-text="article.extendData.format"></span>
|
||||
</div>
|
||||
<div class="runtimes">
|
||||
<span>运行环境:</span>
|
||||
<span
|
||||
class="runtime"
|
||||
v-for="item in article.extendData.runtime"
|
||||
v-text="`${item} `"
|
||||
:key="item"
|
||||
></span>
|
||||
</div>
|
||||
<div>
|
||||
<span>大小:</span>
|
||||
<span v-text="IOSize.format(article.extendData.size)"></span>
|
||||
</div>
|
||||
<div class="downloads">
|
||||
<span>下载:</span>
|
||||
<a
|
||||
class="download"
|
||||
v-for="item in article.extendData.downloads"
|
||||
:href="Toolkit.toResURL(item.value)"
|
||||
:key="item.value"
|
||||
v-text="item.type"
|
||||
target="_blank"
|
||||
></a>
|
||||
</div>
|
||||
<div>
|
||||
<span>解压密码:</span>
|
||||
<span class="selectable" v-text="article.extendData.password"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<markdown-view class="content" :content="article.data" />
|
||||
</div>
|
||||
<comment
|
||||
v-if="SettingMapper.is(SettingKey.ENABLE_COMMENT) && article && article.showComment"
|
||||
:bizType="CommentBizType.ARTICLE"
|
||||
:bizId="article.id!"
|
||||
:titleStickyOffset="80"
|
||||
:canComment="article.canComment"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ArticleAttachType, ArticleSoftwareExtendData, ArticleView } from "@/types/Article.ts";
|
||||
import { CommentBizType, CommonAPI, IOSize, MarkdownView, SettingKey, SettingMapper, Time, Toolkit } from "timi-web";
|
||||
import { Comment } from "timi-tdesign-pc";
|
||||
|
||||
const props = defineProps<{
|
||||
article?: ArticleView<ArticleSoftwareExtendData>,
|
||||
}>();
|
||||
const { article } = toRefs(props);
|
||||
|
||||
const isCreatedAt = ref<boolean>(false);
|
||||
const articleTime = computed(() => {
|
||||
if (article.value) {
|
||||
if (isCreatedAt.value || !article.value.updatedAt) {
|
||||
return "发布于 " + Time.toPassedDateTime(article.value.createdAt);
|
||||
} else {
|
||||
return "编辑于 " + Time.toPassedDateTime(article.value.updatedAt);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const coverUrl = computed(() => {
|
||||
if (article.value && article.value.attachmentList) {
|
||||
for (let i = 0; i < article.value.attachmentList.length; i++) {
|
||||
const item = article.value.attachmentList[i];
|
||||
const attachType = (<any>ArticleAttachType)[item.attachType!];
|
||||
if (attachType === ArticleAttachType.COVER) {
|
||||
return CommonAPI.getAttachmentReadAPI(item.mongoId);
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.article-software {
|
||||
|
||||
.time {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
padding: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
|
||||
.logo {
|
||||
width: 140px;
|
||||
height: 140px;
|
||||
transition: 520ms var(--tui-bezier);
|
||||
}
|
||||
|
||||
.info {
|
||||
font-size: 13px;
|
||||
border-left: 2px solid var(--tui-light-gray);
|
||||
margin-left: 1rem;
|
||||
padding-left: 1rem;
|
||||
|
||||
> div {
|
||||
margin-bottom: 4px;
|
||||
|
||||
> span:first-child {
|
||||
color: #666;
|
||||
padding: 2px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.downloads {
|
||||
|
||||
.download {
|
||||
border: 1px solid #FF7A9B;
|
||||
padding: 1px 4px;
|
||||
background: transparent;
|
||||
margin-right: 6px;
|
||||
|
||||
&:hover {
|
||||
color: #FFF;
|
||||
background: #FF7A9B;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: #FFAAC0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
width: calc(100% - 2rem);
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 650px) {
|
||||
|
||||
.article-software {
|
||||
|
||||
.header {
|
||||
|
||||
.logo {
|
||||
width: 70px;
|
||||
height: 70px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user