♻️ 调整目录结构

This commit is contained in:
目棃
2024-11-19 14:45:29 +08:00
parent e1f85d1d92
commit 3fef8467f4
121 changed files with 532 additions and 554 deletions

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>