mirror of
https://github.com/BTMuli/TeyvatGuide.git
synced 2026-05-16 04:36:46 +08:00
♻️ 调整目录结构
This commit is contained in:
202
src/components/app/t-gamenav.vue
Normal file
202
src/components/app/t-gamenav.vue
Normal file
@@ -0,0 +1,202 @@
|
||||
<template>
|
||||
<div class="tgn-container">
|
||||
<div v-for="navItem in nav" :key="navItem.id" class="tgn-nav" @click="toNav(navItem)">
|
||||
<img alt="navIcon" :src="navItem.icon" />
|
||||
<span>{{ navItem.name }}</span>
|
||||
</div>
|
||||
<div v-if="props.modelValue === 2 && hasNav" class="tgn-nav">
|
||||
<v-btn size="25" @click="tryGetCode" title="查看兑换码" icon="mdi-code-tags-check"></v-btn>
|
||||
</div>
|
||||
<ToLivecode v-model="showOverlay" :data="codeData" v-model:actId="actId" />
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { emit } from "@tauri-apps/api/event";
|
||||
import { computed, onMounted, ref, watch } from "vue";
|
||||
|
||||
import Mys from "../../plugins/Mys/index.js";
|
||||
import { useAppStore } from "../../store/modules/app.js";
|
||||
import TGClient from "../../utils/TGClient.js";
|
||||
import TGLogger from "../../utils/TGLogger.js";
|
||||
import { createPost } from "../../utils/TGWindow.js";
|
||||
import OtherApi from "../../web/request/otherReq.js";
|
||||
import showDialog from "../func/dialog.js";
|
||||
import showSnackbar from "../func/snackbar.js";
|
||||
|
||||
import ToLivecode from "./to-livecode.vue";
|
||||
|
||||
interface TGameNavProps {
|
||||
modelValue: number;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<TGameNavProps>(), {
|
||||
modelValue: 2,
|
||||
});
|
||||
|
||||
const appStore = useAppStore();
|
||||
const nav = ref<TGApp.BBS.Navigator.Navigator[]>([]);
|
||||
const codeData = ref<TGApp.BBS.Navigator.CodeData[]>([]);
|
||||
const showOverlay = ref<boolean>(false);
|
||||
const actId = ref<string>();
|
||||
|
||||
const hasNav = computed<boolean>(() => {
|
||||
if (props.modelValue !== 2) return false;
|
||||
return nav.value.find((item) => item.name === "前瞻直播") !== undefined;
|
||||
});
|
||||
|
||||
onMounted(async () => await loadNav());
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
async () => await loadNav(),
|
||||
);
|
||||
|
||||
async function loadNav(): Promise<void> {
|
||||
nav.value = await Mys.ApiHub.homeNew(props.modelValue);
|
||||
}
|
||||
|
||||
async function tryGetCode(): Promise<void> {
|
||||
if (props.modelValue !== 2) return;
|
||||
const navFind = nav.value.find((item) => item.name === "前瞻直播");
|
||||
if (!navFind) return;
|
||||
const actIdFind = new URL(navFind.app_path).searchParams.get("act_id");
|
||||
if (!actIdFind) {
|
||||
showSnackbar.warn("未找到活动ID");
|
||||
return;
|
||||
}
|
||||
actId.value = actIdFind;
|
||||
const res = await OtherApi.code(actIdFind);
|
||||
if (!Array.isArray(res)) {
|
||||
showSnackbar.warn(`[${res.retcode}] ${res.message}`);
|
||||
return;
|
||||
}
|
||||
codeData.value = res;
|
||||
showSnackbar.success("获取兑换码成功");
|
||||
await TGLogger.Info(JSON.stringify(res));
|
||||
showOverlay.value = true;
|
||||
}
|
||||
|
||||
async function toNav(item: TGApp.BBS.Navigator.Navigator): Promise<void> {
|
||||
if (!appStore.isLogin) {
|
||||
showSnackbar.warn("请先登录");
|
||||
return;
|
||||
}
|
||||
await TGLogger.Info(`[TGameNav][toNav] 打开网页活动 ${item.name}`);
|
||||
await TGLogger.Info(`[TGameNav}][toNav] ${item.app_path}`);
|
||||
const link = new URL(item.app_path);
|
||||
const mysList = [
|
||||
"https://act.mihoyo.com",
|
||||
"https://webstatic.mihoyo.com",
|
||||
"https://bbs.mihoyo.com",
|
||||
"https://qaa.miyoushe.com",
|
||||
"https://mhyurl.cn",
|
||||
];
|
||||
if (link.protocol != "https:") {
|
||||
await toBBS(link);
|
||||
return;
|
||||
}
|
||||
// 如果不在上面的域名里面,就直接打开
|
||||
if (!mysList.includes(link.origin)) {
|
||||
window.open(item.app_path);
|
||||
return;
|
||||
}
|
||||
if (item.name === "签到福利") {
|
||||
await TGClient.open("web_act_thin", item.app_path);
|
||||
return;
|
||||
}
|
||||
const modeCheck = await showDialog.check("是否采用宽屏模式打开?", "取消则采用竖屏模式打开");
|
||||
if (modeCheck === undefined) {
|
||||
showSnackbar.cancel("已取消打开");
|
||||
return;
|
||||
}
|
||||
if (!modeCheck) await TGClient.open("web_act_thin", item.app_path);
|
||||
else await TGClient.open("web_act", item.app_path);
|
||||
}
|
||||
|
||||
// 处理 protocol
|
||||
async function toBBS(link: URL): Promise<void> {
|
||||
if (link.protocol == "mihoyobbs:") {
|
||||
if (link.pathname.startsWith("//article")) {
|
||||
const postId = link.pathname.split("/").pop();
|
||||
await createPost(<string>postId);
|
||||
return;
|
||||
}
|
||||
if (link.pathname.startsWith("//forum")) {
|
||||
const forumId = link.pathname.split("/").pop();
|
||||
const localPath = getLocalPath(forumId);
|
||||
if (localPath === "") {
|
||||
showSnackbar.warn(`不支持的链接:${link.href}`);
|
||||
return;
|
||||
}
|
||||
await emit("active_deep_link", `router?path=${localPath}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
showSnackbar.warn(`不支持的链接:${link.href}`);
|
||||
}
|
||||
|
||||
function getLocalPath(forum?: string): string {
|
||||
if (!forum) return "";
|
||||
const forumLocalMap: Record<string, string> = {
|
||||
"31": "/news/3", // 崩坏2官方
|
||||
"6": "/news/1", // 崩坏3官方
|
||||
"28": "/news/2", // 原神官方
|
||||
"33": "/news/4", // 未定官方
|
||||
"58": "/news/8", // 绝区零官方
|
||||
"36": "/news/5", // 大别野公告
|
||||
};
|
||||
if (forumLocalMap[forum]) return forumLocalMap[forum];
|
||||
const ysForums = ["26", "43", "29", "49", "50"];
|
||||
const srForums = ["52", "61", "56", "62"];
|
||||
const bh3Forums = ["1", "14", "4", "41"];
|
||||
const bh2Forums = ["30", "51", "40"];
|
||||
const wdForums = ["37", "60", "42", "38"];
|
||||
const zzzForums = ["57", "59", "64", "65"];
|
||||
const dbyForums = ["54", "35", "34", "39", "47", "48", "55", "36"];
|
||||
if (ysForums.includes(forum)) return `/posts/forum/2/${forum}`;
|
||||
if (srForums.includes(forum)) return `/posts/forum/6/${forum}`;
|
||||
if (bh3Forums.includes(forum)) return `/posts/forum/1/${forum}`;
|
||||
if (bh2Forums.includes(forum)) return `/posts/forum/3/${forum}`;
|
||||
if (wdForums.includes(forum)) return `/posts/forum/4/${forum}`;
|
||||
if (zzzForums.includes(forum)) return `/posts/forum/8/${forum}`;
|
||||
if (dbyForums.includes(forum)) return `/posts/forum/5/${forum}`;
|
||||
return "";
|
||||
}
|
||||
</script>
|
||||
<style lang="css" scoped>
|
||||
.tgn-container {
|
||||
display: flex;
|
||||
padding: 5px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.tgn-nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 5px;
|
||||
border-radius: 5px;
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
backdrop-filter: blur(20px);
|
||||
background: var(--common-shadow-t-4);
|
||||
box-shadow: 0 0 5px var(--common-shadow-4);
|
||||
color: var(--tgc-white-1);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tgn-nav img {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
}
|
||||
|
||||
.tgn-nav span {
|
||||
display: none;
|
||||
color: var(--common-text-title);
|
||||
font-family: var(--font-title);
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.tgn-nav:hover span {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
204
src/components/app/t-item-box.vue
Normal file
204
src/components/app/t-item-box.vue
Normal file
@@ -0,0 +1,204 @@
|
||||
<template>
|
||||
<div class="tib-box">
|
||||
<div class="tib-bg">
|
||||
<slot name="bg">
|
||||
<img :src="props.modelValue.bg" alt="bg" />
|
||||
</slot>
|
||||
</div>
|
||||
<div class="tib-icon">
|
||||
<slot name="icon">
|
||||
<img :src="props.modelValue.icon" alt="icon" />
|
||||
</slot>
|
||||
</div>
|
||||
<div class="tib-cover">
|
||||
<div class="tib-lt">
|
||||
<img :src="props.modelValue.lt" alt="lt" />
|
||||
</div>
|
||||
<div v-show="props.modelValue.rt" class="tib-rt">
|
||||
{{ props.modelValue.rt }}
|
||||
</div>
|
||||
<div class="tib-inner">
|
||||
<slot name="inner-icon">
|
||||
<img
|
||||
v-show="props.modelValue.innerIcon"
|
||||
:src="props.modelValue.innerIcon"
|
||||
alt="inner-icon"
|
||||
/>
|
||||
</slot>
|
||||
<slot name="inner-text">
|
||||
<span :title="props.modelValue.innerText">{{ props.modelValue.innerText }}</span>
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="props.modelValue.display === 'outer'" class="tib-outer">
|
||||
<slot name="outer-text">
|
||||
<span>{{ props.modelValue.outerText }}</span>
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
export interface TItemBoxData {
|
||||
bg: string;
|
||||
icon: string;
|
||||
size: string;
|
||||
height: string;
|
||||
display: "inner" | "outer";
|
||||
clickable: boolean;
|
||||
lt: string;
|
||||
ltSize: string;
|
||||
rt?: string;
|
||||
rtSize?: string;
|
||||
innerHeight?: number;
|
||||
innerIcon?: string;
|
||||
innerText: string;
|
||||
outerHeight?: number;
|
||||
outerText?: string;
|
||||
innerBlur?: string;
|
||||
}
|
||||
|
||||
interface TItemBoxProps {
|
||||
modelValue: TItemBoxData;
|
||||
}
|
||||
|
||||
const props = defineProps<TItemBoxProps>();
|
||||
const size = props.modelValue.size;
|
||||
const height = props.modelValue.height;
|
||||
const cursor = props.modelValue.clickable ? "pointer" : "default";
|
||||
const sizeLt = props.modelValue.ltSize;
|
||||
const sizeRt = props.modelValue.rtSize;
|
||||
const sizeInner = `${props.modelValue.innerHeight ?? 0}px`;
|
||||
const fontSizeInner = props.modelValue.innerHeight ? `${props.modelValue.innerHeight / 2}px` : "0";
|
||||
const sizeOuter = `${props.modelValue.outerHeight ?? 0}px`;
|
||||
const fontSizeOuter = props.modelValue.outerHeight ? `${props.modelValue.outerHeight / 2}px` : "0";
|
||||
const innerBlur = props.modelValue.innerBlur ?? "0";
|
||||
</script>
|
||||
<style lang="css" scoped>
|
||||
.tib-box {
|
||||
position: relative;
|
||||
width: v-bind(size);
|
||||
height: v-bind(height);
|
||||
cursor: v-bind(cursor);
|
||||
}
|
||||
|
||||
.tib-bg {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
overflow: hidden;
|
||||
width: v-bind(size);
|
||||
height: v-bind(size);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.tib-bg img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.tib-icon {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
width: v-bind(size);
|
||||
height: v-bind(size);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.tib-icon img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.tib-cover {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
width: v-bind(size);
|
||||
height: v-bind(size);
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.tib-lt {
|
||||
position: absolute;
|
||||
top: 3%;
|
||||
left: 3%;
|
||||
display: flex;
|
||||
width: v-bind(sizeLt);
|
||||
height: v-bind(sizeLt);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.tib-lt img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.tib-rt {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
width: v-bind(sizeRt);
|
||||
height: v-bind(sizeRt);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgb(0 0 0 / 40%);
|
||||
border-bottom-left-radius: 5px;
|
||||
border-top-right-radius: 5px;
|
||||
color: var(--tgc-white-1);
|
||||
font-family: var(--font-title);
|
||||
}
|
||||
|
||||
.tib-inner {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: v-bind(sizeInner);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
-webkit-backdrop-filter: blur(v-bind(innerBlur));
|
||||
backdrop-filter: blur(v-bind(innerBlur));
|
||||
background: rgb(20 20 20 / 40%);
|
||||
border-bottom-left-radius: 5px;
|
||||
border-bottom-right-radius: 5px;
|
||||
color: var(--tgc-white-1);
|
||||
font-family: var(--font-title);
|
||||
font-size: v-bind(fontSizeInner);
|
||||
}
|
||||
|
||||
.tib-inner img {
|
||||
width: v-bind(sizeInner);
|
||||
height: v-bind(sizeInner);
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.tib-inner span {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.tib-outer {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: v-bind(sizeOuter);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--common-text-title);
|
||||
font-size: v-bind(fontSizeOuter);
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
101
src/components/app/t-overlay.vue
Normal file
101
src/components/app/t-overlay.vue
Normal file
@@ -0,0 +1,101 @@
|
||||
<template>
|
||||
<transition enter-from-class="tolo-enter-from" name="tolo">
|
||||
<div v-if="showTolo" class="tolo-box" @click.self.prevent="toClick">
|
||||
<transition enter-from-class="toli-enter-from" name="toli">
|
||||
<slot v-if="showToli" />
|
||||
</transition>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
// vue
|
||||
import { ref, watch } from "vue";
|
||||
|
||||
interface TolProps {
|
||||
modelValue: boolean;
|
||||
toClick?: () => void;
|
||||
blurVal: string;
|
||||
hide?: true;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<TolProps>(), {
|
||||
modelValue: false,
|
||||
blurVal: "20px",
|
||||
});
|
||||
|
||||
const showTolo = ref(!props.hide);
|
||||
const showToli = ref(!props.hide);
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
() => {
|
||||
if (props.modelValue) {
|
||||
showTolo.value = true;
|
||||
showToli.value = true;
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
showToli.value = false;
|
||||
}, 100);
|
||||
setTimeout(() => {
|
||||
showTolo.value = false;
|
||||
}, 300);
|
||||
}
|
||||
},
|
||||
);
|
||||
</script>
|
||||
<style lang="css" scoped>
|
||||
.tolo-enter-active,
|
||||
.tolo-leave-active,
|
||||
.toli-enter-active {
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.toli-leave-active {
|
||||
transition: all 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
.tolo-enter-from,
|
||||
.toli-enter-from {
|
||||
opacity: 0;
|
||||
transform: scale(1.5);
|
||||
}
|
||||
|
||||
.tolo-enter-to,
|
||||
.toli-enter-to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
.tolo-leave-from {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.tolo-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.toli-leave-from {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
.toli-leave-to {
|
||||
opacity: 0;
|
||||
transform: scale(0);
|
||||
}
|
||||
|
||||
.tolo-box {
|
||||
position: fixed;
|
||||
z-index: 100;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
-webkit-backdrop-filter: blur(v-bind(blurVal));
|
||||
backdrop-filter: blur(v-bind(blurVal));
|
||||
background: rgb(0 0 0 / 50%);
|
||||
}
|
||||
</style>
|
||||
486
src/components/app/t-postcard.vue
Normal file
486
src/components/app/t-postcard.vue
Normal file
@@ -0,0 +1,486 @@
|
||||
<template>
|
||||
<div v-if="card" :id="`post-card-${card.postId}`" class="tpc-card">
|
||||
<div class="tpc-top">
|
||||
<div class="tpc-cover" @click="createPost(card)">
|
||||
<img :src="localCover" alt="cover" v-if="localCover" />
|
||||
<v-progress-circular color="primary" :indeterminate="true" v-else-if="card.cover !== ''" />
|
||||
<img src="/source/UI/defaultCover.webp" alt="cover" v-else />
|
||||
<div v-if="isAct" class="tpc-act">
|
||||
<div class="tpc-status">{{ card.status?.status }}</div>
|
||||
<div class="tpc-time">
|
||||
<v-icon>mdi-clock-time-four-outline</v-icon>
|
||||
<span>{{ card.subtitle }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tpc-title" :title="card.title" @click="shareCard">{{ card.title }}</div>
|
||||
</div>
|
||||
<div class="tpc-mid" v-if="card.user">
|
||||
<TpAvatar :data="card.user" position="left" />
|
||||
</div>
|
||||
<div class="tpc-bottom" v-if="card.data">
|
||||
<div class="tpc-tags">
|
||||
<div v-for="topic in card.topics" :key="topic.id" class="tpc-tag" @click="toTopic(topic)">
|
||||
<v-icon size="10">mdi-tag</v-icon>
|
||||
<span>{{ topic.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tpc-data">
|
||||
<div class="tpc-info-item" :title="`浏览数:${card.data.view}`">
|
||||
<v-icon>mdi-eye</v-icon>
|
||||
<span>{{ card.data.view }}</span>
|
||||
</div>
|
||||
<div class="tpc-info-item" :title="`收藏数:${card.data.mark}`">
|
||||
<v-icon>mdi-star</v-icon>
|
||||
<span>{{ card.data.mark }}</span>
|
||||
</div>
|
||||
<div class="tpc-info-item" :title="`回复数:${card.data.reply}`">
|
||||
<v-icon>mdi-comment</v-icon>
|
||||
<span>{{ card.data.reply }}</span>
|
||||
</div>
|
||||
<div class="tpc-info-item" :title="`点赞数:${card.data.like}`">
|
||||
<v-icon>mdi-thumb-up</v-icon>
|
||||
<span>{{ card.data.like }}</span>
|
||||
</div>
|
||||
<div class="tpc-info-item" :title="`转发数:${card.data.forward}`">
|
||||
<v-icon>mdi-share-variant</v-icon>
|
||||
<span>{{ card.data.forward }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="tpc-forum"
|
||||
v-if="card.forum && card.forum.name !== ''"
|
||||
:title="`频道: ${card.forum.name}`"
|
||||
@click="toForum(card.forum)"
|
||||
>
|
||||
<img :src="card.forum.icon" :alt="card.forum.name" />
|
||||
<span>{{ card.forum.name }}</span>
|
||||
</div>
|
||||
<v-checkbox-btn
|
||||
v-if="props.selectMode"
|
||||
class="tpc-select"
|
||||
@click="emits('onSelected', props.modelValue.post.post_id)"
|
||||
data-html2canvas-ignore
|
||||
/>
|
||||
<div class="tpc-info-id" v-else>{{ props.modelValue.post.post_id }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { emit } from "@tauri-apps/api/event";
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from "vue";
|
||||
|
||||
import { generateShareImg, saveImgLocal } from "../../utils/TGShare.js";
|
||||
import { createPost } from "../../utils/TGWindow.js";
|
||||
import TpAvatar from "../viewPost/tp-avatar.vue";
|
||||
|
||||
interface TPostCardProps {
|
||||
modelValue: TGApp.Plugins.Mys.Post.FullData;
|
||||
selectMode?: boolean;
|
||||
}
|
||||
|
||||
interface TPostCardEmits {
|
||||
(e: "onSelected", value: string): void;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<TPostCardProps>(), {
|
||||
selectMode: false,
|
||||
});
|
||||
const emits = defineEmits<TPostCardEmits>();
|
||||
const isAct = ref<boolean>(false);
|
||||
const card = ref<TGApp.Plugins.Mys.News.RenderCard>();
|
||||
const localCover = ref<string>();
|
||||
|
||||
const cardBg = computed<string>(() => {
|
||||
if (card.value && card.value.status) return card.value.status.colorCss;
|
||||
return "none";
|
||||
});
|
||||
|
||||
onMounted(async () => await reload(props.modelValue));
|
||||
|
||||
watch(() => props.modelValue, reload);
|
||||
|
||||
async function reload(data: TGApp.Plugins.Mys.Post.FullData): Promise<void> {
|
||||
if (localCover.value) {
|
||||
URL.revokeObjectURL(localCover.value);
|
||||
localCover.value = undefined;
|
||||
}
|
||||
card.value = getPostCard(data);
|
||||
if (card.value && card.value.cover !== "") {
|
||||
localCover.value = await saveImgLocal(card.value.cover);
|
||||
}
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
if (localCover.value) {
|
||||
URL.revokeObjectURL(localCover.value);
|
||||
localCover.value = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @description 活动状态
|
||||
* @since Alpha v0.2.1
|
||||
* @enum {TGApp.Plugins.Mys.News.RenderStatus}
|
||||
* @property {TGApp.Plugins.Mys.News.RenderStatus} STARTED 进行中
|
||||
* @property {TGApp.Plugins.Mys.News.RenderStatus} FINISHED 已结束
|
||||
* @property {TGApp.Plugins.Mys.News.RenderStatus} SELECTION 评选中
|
||||
* @return EnumStatus
|
||||
*/
|
||||
const EnumStatus = {
|
||||
STARTED: {
|
||||
status: "进行中",
|
||||
colorCss: "var(--tgc-od-green)",
|
||||
},
|
||||
FINISHED: {
|
||||
status: "已结束",
|
||||
colorCss: "var(--tgc-od-white)",
|
||||
},
|
||||
SELECTION: {
|
||||
status: "评选中",
|
||||
colorCss: "var(--tgc-od-orange)",
|
||||
},
|
||||
UNKNOWN: {
|
||||
status: "未知",
|
||||
colorCss: "var(--tgc-od-red)",
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* @description 获取活动状态
|
||||
* @since Alpha
|
||||
* @param {number} status 活动状态码
|
||||
* @returns {string}
|
||||
*/
|
||||
function getActivityStatus(status: number): TGApp.Plugins.Mys.News.RenderStatus {
|
||||
switch (status) {
|
||||
case 1:
|
||||
return EnumStatus.STARTED;
|
||||
case 2:
|
||||
return EnumStatus.SELECTION;
|
||||
case 3:
|
||||
return EnumStatus.FINISHED;
|
||||
default:
|
||||
return EnumStatus.UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
function getPostCover(item: TGApp.Plugins.Mys.Post.FullData): string {
|
||||
let cover;
|
||||
if (item.cover) {
|
||||
cover = item.cover.url;
|
||||
} else if (item.post.cover) {
|
||||
cover = item.post.cover;
|
||||
} else if (item.post.images.length > 0) {
|
||||
cover = item.post.images[0];
|
||||
}
|
||||
if (cover === undefined) return "";
|
||||
if (cover.endsWith(".gif")) return cover;
|
||||
return `${cover}?x-oss-process=image/format,png`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 获取公共属性
|
||||
* @since Beta v0.6.1
|
||||
* @param {TGApp.Plugins.Mys.Post.FullData} item 咨讯列表项
|
||||
* @returns {TGApp.Plugins.Mys.News.RenderCard} 渲染用咨讯列表项
|
||||
*/
|
||||
function getCommonCard(item: TGApp.Plugins.Mys.Post.FullData): TGApp.Plugins.Mys.News.RenderCard {
|
||||
let forum: TGApp.Plugins.Mys.News.RenderForum | null = null;
|
||||
let data: TGApp.Plugins.Mys.News.RenderData | null = null;
|
||||
if (item.forum !== null) {
|
||||
forum = {
|
||||
name: item.forum.name,
|
||||
icon: item.forum.icon,
|
||||
id: item.forum.id,
|
||||
};
|
||||
}
|
||||
if (item.stat !== null) {
|
||||
data = {
|
||||
mark: item.stat.bookmark_num,
|
||||
forward: item.stat.forward_num,
|
||||
like: item.stat.like_num,
|
||||
reply: item.stat.reply_num,
|
||||
view: item.stat.view_num,
|
||||
};
|
||||
}
|
||||
return {
|
||||
title: item.post.subject,
|
||||
cover: getPostCover(item),
|
||||
postId: Number(item.post.post_id),
|
||||
subtitle: item.post.post_id,
|
||||
user: item.user,
|
||||
forum: forum,
|
||||
data: data,
|
||||
topics: item.topics,
|
||||
};
|
||||
}
|
||||
|
||||
function getPostCard(item: TGApp.Plugins.Mys.Post.FullData): TGApp.Plugins.Mys.News.RenderCard {
|
||||
const commonCard = getCommonCard(item);
|
||||
if (
|
||||
item.news_meta !== undefined &&
|
||||
item.news_meta !== null &&
|
||||
item.news_meta.start_at_sec !== "0"
|
||||
) {
|
||||
isAct.value = true;
|
||||
const startTime = new Date(Number(item.news_meta.start_at_sec) * 1000).toLocaleDateString();
|
||||
const endTime = new Date(Number(item.news_meta.end_at_sec) * 1000).toLocaleDateString();
|
||||
const statusInfo = getActivityStatus(item.news_meta.activity_status);
|
||||
commonCard.subtitle = `${startTime} - ${endTime}`;
|
||||
commonCard.status = statusInfo;
|
||||
}
|
||||
return commonCard;
|
||||
}
|
||||
|
||||
async function shareCard(): Promise<void> {
|
||||
if (!card.value) return;
|
||||
const dom = <HTMLDivElement>document.querySelector(`#post-card-${card.value.postId}`);
|
||||
const fileName = `PostCard_${card.value.postId}`;
|
||||
await generateShareImg(fileName, dom, 2.5);
|
||||
}
|
||||
|
||||
async function toTopic(topic: TGApp.Plugins.Mys.Topic.Info): Promise<void> {
|
||||
const gid = props.modelValue.post.game_id;
|
||||
await emit("active_deep_link", `router?path=/posts/topic/${gid}/${topic.id}`);
|
||||
}
|
||||
|
||||
async function toForum(forum: TGApp.Plugins.Mys.News.RenderForum): Promise<void> {
|
||||
const gid = props.modelValue.post.game_id;
|
||||
await emit("active_deep_link", `router?path=/posts/forum/${gid}/${forum.id}`);
|
||||
}
|
||||
</script>
|
||||
<style lang="css" scoped>
|
||||
.tpc-card {
|
||||
position: relative;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border: 1px solid var(--common-shadow-1);
|
||||
border-radius: 5px;
|
||||
background: var(--box-bg-1);
|
||||
box-shadow: 2px 2px 5px var(--common-shadow-2);
|
||||
row-gap: 10px;
|
||||
}
|
||||
|
||||
.tpc-top {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
row-gap: 5px;
|
||||
}
|
||||
|
||||
.tpc-cover {
|
||||
position: relative;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
aspect-ratio: 36 / 13;
|
||||
background: var(--common-shadow-2);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tpc-cover img {
|
||||
width: 100%;
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
transition: all 0.3s linear;
|
||||
}
|
||||
|
||||
.tpc-mid {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.tpc-bottom {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
padding: 5px 10px;
|
||||
row-gap: 5px;
|
||||
}
|
||||
|
||||
.tpc-title {
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
padding: 5px 10px;
|
||||
cursor: pointer;
|
||||
font-family: var(--font-title);
|
||||
font-size: 18px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tpc-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
color: var(--box-text-5);
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
gap: 5px;
|
||||
|
||||
:hover {
|
||||
color: var(--box-text-3);
|
||||
}
|
||||
}
|
||||
|
||||
.tpc-tag {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 3px;
|
||||
border: 1px solid var(--common-shadow-1);
|
||||
border-radius: 5px;
|
||||
background: var(--box-bg-2);
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.tpc-forum {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
padding: 5px;
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
backdrop-filter: blur(20px);
|
||||
background: var(--common-shadow-2);
|
||||
border-bottom-left-radius: 5px;
|
||||
box-shadow: 0 0 10px var(--tgc-dark-1);
|
||||
color: var(--tgc-white-1);
|
||||
cursor: pointer;
|
||||
text-shadow: 0 0 5px var(--tgc-dark-1);
|
||||
}
|
||||
|
||||
.tpc-select {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
backdrop-filter: blur(20px);
|
||||
background: var(--box-bg-2);
|
||||
border-bottom-right-radius: 4px;
|
||||
box-shadow: 0 0 10px var(--tgc-dark-1);
|
||||
color: var(--box-text-5);
|
||||
}
|
||||
|
||||
.tpc-forum img {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.tpc-cover img:hover {
|
||||
transform: scale(1.1);
|
||||
transition: all 0.3s linear;
|
||||
}
|
||||
|
||||
.tpc-data {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
padding: 5px;
|
||||
column-gap: 10px;
|
||||
}
|
||||
|
||||
.tpc-info-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
color: var(--box-text-7);
|
||||
font-size: 12px;
|
||||
gap: 5px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.tpc-act {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
backdrop-filter: blur(20px);
|
||||
background: rgb(0 0 0/50%);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.tpc-status {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
padding: 5px 30px 5px 5px;
|
||||
background-color: v-bind(cardBg);
|
||||
clip-path: polygon(0 0, calc(100% - 15px) 0, 100% 50%, calc(100% - 15px) 100%, 0 100%);
|
||||
color: var(--tgc-white-1);
|
||||
|
||||
&::after {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgb(255 255 255/40%);
|
||||
clip-path: polygon(
|
||||
calc(100% - 25px) 0,
|
||||
100% 0,
|
||||
100% 100%,
|
||||
calc(100% - 25px) 100%,
|
||||
calc(100% - 10px) 50%
|
||||
);
|
||||
content: "";
|
||||
}
|
||||
}
|
||||
|
||||
.tpc-time {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
margin: 5px;
|
||||
color: var(--tgc-white-1);
|
||||
gap: 5px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.tpc-info-id {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 5px;
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
backdrop-filter: blur(20px);
|
||||
background: var(--common-shadow-1);
|
||||
border-bottom-right-radius: 5px;
|
||||
border-top-left-radius: 5px;
|
||||
box-shadow: 2px 2px 5px var(--tgc-dark-1);
|
||||
color: var(--tgc-white-1);
|
||||
font-size: 12px;
|
||||
text-shadow: 0 0 5px var(--tgc-dark-1);
|
||||
}
|
||||
</style>
|
||||
41
src/components/app/t-subline.vue
Normal file
41
src/components/app/t-subline.vue
Normal file
@@ -0,0 +1,41 @@
|
||||
<template>
|
||||
<div class="tsl-box">
|
||||
<img src="../../assets/icons/arrow-right.svg" alt="right" />
|
||||
<slot>{{ props.title }}</slot>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
const props = defineProps<{
|
||||
title?: string;
|
||||
}>();
|
||||
</script>
|
||||
<style lang="css" scoped>
|
||||
.tsl-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 5px;
|
||||
border-radius: 5px;
|
||||
margin: 5px 0;
|
||||
background: var(--box-bg-3);
|
||||
color: var(--box-text-4);
|
||||
font-family: var(--font-title);
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.tsl-box :first-child {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
padding: 2px;
|
||||
border-radius: 5px;
|
||||
filter: invert(22%) sepia(7%) saturate(1241%) hue-rotate(182deg) brightness(95%) contrast(99%);
|
||||
}
|
||||
|
||||
.dark .tsl-box {
|
||||
background: #2c313a;
|
||||
color: #faf7e8;
|
||||
}
|
||||
|
||||
.dark .tsl-box :first-child {
|
||||
filter: none;
|
||||
}
|
||||
</style>
|
||||
136
src/components/app/to-livecode.vue
Normal file
136
src/components/app/to-livecode.vue
Normal file
@@ -0,0 +1,136 @@
|
||||
<template>
|
||||
<TOverlay
|
||||
v-model="visible"
|
||||
:hide="true"
|
||||
:to-click="onCancel"
|
||||
blur-val="20px"
|
||||
class="tolc-overlay"
|
||||
>
|
||||
<div class="tolc-box">
|
||||
<div class="tolc-title">
|
||||
<span>兑换码</span>
|
||||
<v-icon
|
||||
size="18px"
|
||||
title="share"
|
||||
@click="shareImg()"
|
||||
icon="mdi-share-variant"
|
||||
variant="outlined"
|
||||
data-html2canvas-ignore
|
||||
/>
|
||||
</div>
|
||||
<div class="tolc-info">ActID:{{ props.actId }}</div>
|
||||
<v-list-item v-for="(item, index) in props.data" :key="index">
|
||||
<template #title>
|
||||
{{ item.code === "" ? "暂无兑换码" : item.code }}
|
||||
</template>
|
||||
<template #subtitle>
|
||||
<div v-html="item.title"></div>
|
||||
<span title="开放时间">{{ timestampToDate(Number(item.to_get_time) * 1000) }}</span>
|
||||
</template>
|
||||
<template #prepend>
|
||||
<img
|
||||
:src="item.img === '' ? '/source/UI/empty.webp' : item.img"
|
||||
alt="icon"
|
||||
class="tolc-icon"
|
||||
/>
|
||||
</template>
|
||||
<template #append>
|
||||
<v-btn
|
||||
size="small"
|
||||
:disabled="item.code === ''"
|
||||
@click="copy(item.code)"
|
||||
icon="mdi-content-copy"
|
||||
variant="outlined"
|
||||
class="tolc-btn"
|
||||
data-html2canvas-ignore
|
||||
></v-btn>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</div>
|
||||
</TOverlay>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
|
||||
import { generateShareImg } from "../../utils/TGShare.js";
|
||||
import { timestampToDate } from "../../utils/toolFunc.js";
|
||||
import showSnackbar from "../func/snackbar.js";
|
||||
|
||||
import TOverlay from "./t-overlay.vue";
|
||||
|
||||
interface ToLiveCodeProps {
|
||||
data: TGApp.BBS.Navigator.CodeData[];
|
||||
actId: string | undefined;
|
||||
modelValue: boolean;
|
||||
}
|
||||
|
||||
type ToLiveCodeEmits = (e: "update:modelValue", value: boolean) => void;
|
||||
|
||||
const props = withDefaults(defineProps<ToLiveCodeProps>(), { modelValue: false });
|
||||
const emits = defineEmits<ToLiveCodeEmits>();
|
||||
|
||||
const visible = computed<boolean>({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => {
|
||||
emits("update:modelValue", value);
|
||||
},
|
||||
});
|
||||
|
||||
function onCancel(): void {
|
||||
visible.value = false;
|
||||
}
|
||||
|
||||
function copy(code: string): void {
|
||||
navigator.clipboard.writeText(code);
|
||||
showSnackbar.success("已复制到剪贴板");
|
||||
}
|
||||
|
||||
async function shareImg(): Promise<void> {
|
||||
const element = <HTMLElement>document.querySelector(".tolc-box");
|
||||
const fileName = `LiveCode_${props.actId}_${new Date().getTime()}`;
|
||||
await generateShareImg(fileName, element, 2);
|
||||
}
|
||||
</script>
|
||||
<style lang="css" scoped>
|
||||
.tolc-overlay {
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.tolc-box {
|
||||
position: relative;
|
||||
width: 340px;
|
||||
padding: 10px;
|
||||
border: 1px solid var(--common-shadow-2);
|
||||
border-radius: 5px;
|
||||
background: var(--app-page-bg);
|
||||
}
|
||||
|
||||
.tolc-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--common-text-title);
|
||||
column-gap: 5px;
|
||||
font-family: var(--font-title);
|
||||
font-size: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.tolc-info {
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
right: 6px;
|
||||
bottom: 2px;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.tolc-icon {
|
||||
width: 40px;
|
||||
margin-right: 10px;
|
||||
aspect-ratio: 1;
|
||||
}
|
||||
|
||||
.tolc-btn {
|
||||
margin-left: 10px;
|
||||
}
|
||||
</style>
|
||||
244
src/components/app/to-namecard.vue
Normal file
244
src/components/app/to-namecard.vue
Normal file
@@ -0,0 +1,244 @@
|
||||
<template>
|
||||
<TOverlay v-model="visible" hide :to-click="onCancel" blur-val="20px">
|
||||
<div v-if="props.data" class="ton-container">
|
||||
<slot name="left"></slot>
|
||||
<div class="ton-box">
|
||||
<img alt="bg" class="ton-bg" v-if="props.data" :src="props.data.profile" />
|
||||
<div class="ton-content">
|
||||
<span>{{ props.data.name }}</span>
|
||||
<span>{{ parseNamecard(props.data.desc) }}</span>
|
||||
<span>获取途径:{{ props.data.source }}</span>
|
||||
</div>
|
||||
<div class="ton-type">{{ getType }}</div>
|
||||
<v-btn
|
||||
class="ton-share"
|
||||
@click="shareNamecard"
|
||||
variant="outlined"
|
||||
:loading="loading"
|
||||
data-html2canvas-ignore
|
||||
>
|
||||
<v-icon>mdi-share-variant</v-icon>
|
||||
<span>分享</span>
|
||||
</v-btn>
|
||||
</div>
|
||||
<slot name="right"></slot>
|
||||
</div>
|
||||
</TOverlay>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from "vue";
|
||||
|
||||
import { generateShareImg } from "../../utils/TGShare.js";
|
||||
|
||||
import TOverlay from "./t-overlay.vue";
|
||||
|
||||
interface ToNamecardProps {
|
||||
modelValue: boolean;
|
||||
data?: TGApp.App.NameCard.Item;
|
||||
}
|
||||
|
||||
enum ToNamecardTypeEnum {
|
||||
other = 0,
|
||||
achievement = 1,
|
||||
role = 2,
|
||||
record = 3,
|
||||
activity = 4,
|
||||
unknown = 5,
|
||||
}
|
||||
|
||||
type ToNamecardTypeMap = {
|
||||
[key in ToNamecardTypeEnum]: string;
|
||||
};
|
||||
|
||||
const typeMap: ToNamecardTypeMap = {
|
||||
[ToNamecardTypeEnum.other]: "其他",
|
||||
[ToNamecardTypeEnum.achievement]: "成就",
|
||||
[ToNamecardTypeEnum.role]: "角色",
|
||||
[ToNamecardTypeEnum.record]: "纪行",
|
||||
[ToNamecardTypeEnum.activity]: "活动",
|
||||
[ToNamecardTypeEnum.unknown]: "未知",
|
||||
};
|
||||
|
||||
type ToNamecardEmits = (e: "update:modelValue", value: boolean) => void;
|
||||
|
||||
const props = defineProps<ToNamecardProps>();
|
||||
|
||||
const emits = defineEmits<ToNamecardEmits>();
|
||||
|
||||
const loading = ref<boolean>(false);
|
||||
|
||||
const getType = computed(() => {
|
||||
if (!props.data) return typeMap[ToNamecardTypeEnum.unknown];
|
||||
switch (props.data.type) {
|
||||
case ToNamecardTypeEnum.achievement:
|
||||
return typeMap[ToNamecardTypeEnum.achievement];
|
||||
case ToNamecardTypeEnum.role:
|
||||
return typeMap[ToNamecardTypeEnum.role];
|
||||
case ToNamecardTypeEnum.record:
|
||||
return typeMap[ToNamecardTypeEnum.record];
|
||||
case ToNamecardTypeEnum.activity:
|
||||
return typeMap[ToNamecardTypeEnum.activity];
|
||||
default:
|
||||
return typeMap[ToNamecardTypeEnum.other];
|
||||
}
|
||||
});
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => {
|
||||
emits("update:modelValue", value);
|
||||
},
|
||||
});
|
||||
|
||||
function onCancel() {
|
||||
visible.value = false;
|
||||
}
|
||||
|
||||
function parseNamecard(desc: string): string {
|
||||
let array = [];
|
||||
if (desc.startsWith("名片纹饰。「") && desc.endsWith("」")) {
|
||||
array.push("名片纹饰。");
|
||||
const reg = /「.+?」/g;
|
||||
const match = desc.match(reg);
|
||||
if (match !== null) {
|
||||
for (const item of match) {
|
||||
if (item.length <= 34) {
|
||||
array.push(item);
|
||||
} else {
|
||||
array.push("「");
|
||||
array.push(...parseDesc(item.slice(1, -1), true));
|
||||
const maxLength = Math.max(...array.map((item) => item.length));
|
||||
array.push(" ".repeat(maxLength - 4) + "」");
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
array.push("名片纹饰。");
|
||||
const content = desc.slice(5);
|
||||
if (content.length <= 32) {
|
||||
array.push(content);
|
||||
} else {
|
||||
array.push(...parseDesc(content));
|
||||
}
|
||||
}
|
||||
const res = array.join("\n");
|
||||
if (!res.endsWith("\n")) return res + "\n";
|
||||
return res;
|
||||
}
|
||||
|
||||
function parseDesc(desc: string, inQuote: boolean = false): string[] {
|
||||
let res = desc.replace(/。/g, "。\n");
|
||||
res = res.replace(/;/g, ";\n");
|
||||
if (props?.data?.index !== 187) {
|
||||
res = res.replace(/:/g, ":\n");
|
||||
res = res.replace(/?/g, "?\n");
|
||||
} else {
|
||||
res = res.replace("时候,", "时候,\n");
|
||||
res = res.replace("。\n」", "。」");
|
||||
}
|
||||
if (!desc.includes("!」")) {
|
||||
res = res.replace(/!/g, "!\n");
|
||||
}
|
||||
res = res.replace(/…/g, "…\n");
|
||||
const match = res.split("\n");
|
||||
let array: string[] = [];
|
||||
for (const item of match) {
|
||||
if (item.length > 0 && item.length <= 32) {
|
||||
array.push(item);
|
||||
} else {
|
||||
const match2 = item.replace(/,/g, ",\n").split("\n");
|
||||
for (const item2 of match2) {
|
||||
if (item2.length > 0) array.push(item2);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (inQuote) array = array.map((item) => ` ${item}`);
|
||||
return array;
|
||||
}
|
||||
|
||||
async function shareNamecard(): Promise<void> {
|
||||
const namecardBox = <HTMLElement>document.querySelector(".ton-box");
|
||||
const fileName = `【${getType.value}名片】-${props.data?.name}`;
|
||||
loading.value = true;
|
||||
await generateShareImg(fileName, namecardBox);
|
||||
loading.value = false;
|
||||
}
|
||||
</script>
|
||||
<style lang="css" scoped>
|
||||
.ton-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
column-gap: 10px;
|
||||
}
|
||||
|
||||
.ton-box {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
width: 800px;
|
||||
height: 400px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.ton-bg {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.ton-type {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
padding: 0 5px;
|
||||
border: 1px solid var(--tgc-white-1);
|
||||
border-radius: 5px;
|
||||
color: var(--tgc-white-1);
|
||||
}
|
||||
|
||||
.ton-content {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-end;
|
||||
padding: 10px;
|
||||
backdrop-filter: blur(5px);
|
||||
background: rgb(0 0 0 / 25%);
|
||||
color: var(--tgc-white-1);
|
||||
}
|
||||
|
||||
.dark .ton-content {
|
||||
background: rgb(0 0 0/ 50%);
|
||||
}
|
||||
|
||||
.ton-content :first-child {
|
||||
font-family: var(--font-title);
|
||||
font-size: 20px;
|
||||
text-shadow: 0 0 5px rgb(0 0 0/80%);
|
||||
}
|
||||
|
||||
.ton-content :nth-child(2) {
|
||||
border-bottom: 1px dotted var(--tgc-white-1);
|
||||
text-shadow: 0 0 2px rgb(0 0 0/80%);
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.ton-content :last-child {
|
||||
opacity: 0.8;
|
||||
text-shadow: 0 0 2px black;
|
||||
}
|
||||
|
||||
.ton-share {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
bottom: 10px;
|
||||
border: 1px solid var(--tgc-white-1);
|
||||
border-radius: 5px;
|
||||
color: var(--tgc-white-1);
|
||||
}
|
||||
</style>
|
||||
49
src/components/app/top-namecard.vue
Normal file
49
src/components/app/top-namecard.vue
Normal file
@@ -0,0 +1,49 @@
|
||||
<template>
|
||||
<v-list class="top-nc-box" @click="toNameCard(props.data)">
|
||||
<v-list-item :title="props.data.name">
|
||||
<template #subtitle>
|
||||
<span :title="props.data.desc">{{ props.data.desc }}</span>
|
||||
</template>
|
||||
<template #prepend>
|
||||
<v-img width="80px" style="margin-right: 10px" :src="props.data.icon" />
|
||||
</template>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { computed } from "vue";
|
||||
|
||||
interface TopNameCardProps {
|
||||
data: TGApp.App.NameCard.Item;
|
||||
}
|
||||
|
||||
interface TopNameCardEmits {
|
||||
(e: "selected", data: TGApp.App.NameCard.Item): void;
|
||||
}
|
||||
|
||||
const props = defineProps<TopNameCardProps>();
|
||||
const emit = defineEmits<TopNameCardEmits>();
|
||||
|
||||
const bgImage = computed<string>(() => {
|
||||
if (props.data.name === "原神·印象") return "none;";
|
||||
return `url("${props.data.bg}")`;
|
||||
});
|
||||
|
||||
function toNameCard(item: TGApp.App.NameCard.Item) {
|
||||
emit("selected", item);
|
||||
}
|
||||
</script>
|
||||
<style lang="css" scoped>
|
||||
.top-nc-box {
|
||||
width: 100%;
|
||||
height: 80px;
|
||||
border: 1px solid var(--common-shadow-2);
|
||||
border-radius: 10px 50px 50px 10px;
|
||||
background-color: var(--box-bg-1);
|
||||
background-image: v-bind(bgImage);
|
||||
background-position: right;
|
||||
background-repeat: no-repeat;
|
||||
cursor: pointer;
|
||||
font-family: var(--font-title);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user