重构帖子解析逻辑,增加新类型解析

*PostID:69886846,69915487
This commit is contained in:
BTMuli
2025-10-24 22:12:14 +08:00
parent 9020214d23
commit 33d9ba5c4d
9 changed files with 489 additions and 16 deletions

View File

@@ -112,7 +112,10 @@
}" }"
v-else v-else
> >
{{ props.modelValue.post.post_id }} <span>{{ props.modelValue.post.post_id }}</span>
<template v-if="isDevEnv">
<span>[{{ props.modelValue.post.view_type }}]</span>
</template>
</div> </div>
</div> </div>
</template> </template>
@@ -154,7 +157,8 @@ export type RenderCard = {
topics: Array<TGApp.BBS.Post.Topic>; topics: Array<TGApp.BBS.Post.Topic>;
reasons: Array<TGApp.BBS.Post.RecommendTags>; reasons: Array<TGApp.BBS.Post.RecommendTags>;
}; };
// @ts-expect-error The import.meta meta-property is not allowed in files which will build into CommonJS output.
const isDevEnv = import.meta.env.MODE === "development";
const stats: Readonly<Array<RenderStatus>> = [ const stats: Readonly<Array<RenderStatus>> = [
{ stat: 0, label: "未知", color: "var(--tgc-od-red)" }, { stat: 0, label: "未知", color: "var(--tgc-od-red)" },
{ stat: 1, label: "进行中", color: "var(--tgc-od-green)" }, { stat: 1, label: "进行中", color: "var(--tgc-od-green)" },

View File

@@ -7,6 +7,8 @@
/> />
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import TpUgcLevel from "@comp/viewPost/tp-ugc-level.vue";
import TpUgcTag from "@comp/viewPost/tp-ugc-tag.vue";
import type { Component } from "vue"; import type { Component } from "vue";
import TpBackupText from "./tp-backupText.vue"; import TpBackupText from "./tp-backupText.vue";
@@ -72,6 +74,8 @@ function getParsedData(data: SctPostDataArr): SctPostDataArr {
if (check < parsedText.length && check !== 0) { if (check < parsedText.length && check !== 0) {
res.push(...child); res.push(...child);
child = []; child = [];
} else if (check === 0 && child.length > 0) {
res.push(...child);
} }
} }
if (res.length === 0 && child.length > 0) res.push(...child); if (res.length === 0 && child.length > 0) res.push(...child);
@@ -95,8 +99,9 @@ function getTpName(tp: TGApp.BBS.SctPost.Base): Component {
if (tp.children) return TpTexts; if (tp.children) return TpTexts;
if (typeof tp.insert === "undefined") return TpUnknown; if (typeof tp.insert === "undefined") return TpUnknown;
if (typeof tp.insert === "string") return TpText; if (typeof tp.insert === "string") return TpText;
// game_user_info属于backup_text的一种必须放在backup_text判断的前面 // 判断特殊backup_text
if ("game_user_info" in tp.insert) return TpUid; if ("game_user_info" in tp.insert) return TpUid;
if ("ugc_master_tag" in tp.insert) return TpUgcTag;
if ("backup_text" in tp.insert) { if ("backup_text" in tp.insert) {
if (tp.insert.backup_text === "[游戏卡片]" && "reception_card" in tp.insert) return TpGameCard; if (tp.insert.backup_text === "[游戏卡片]" && "reception_card" in tp.insert) return TpGameCard;
if (tp.insert.backup_text === "[自定义表情]" && "custom_emoticon" in tp.insert) { if (tp.insert.backup_text === "[自定义表情]" && "custom_emoticon" in tp.insert) {
@@ -106,6 +111,7 @@ function getTpName(tp: TGApp.BBS.SctPost.Base): Component {
} }
if ("divider" in tp.insert) return TpDivider; if ("divider" in tp.insert) return TpDivider;
if ("image" in tp.insert) return TpImage; if ("image" in tp.insert) return TpImage;
if ("level" in tp.insert) return TpUgcLevel;
if ("link_card" in tp.insert) return TpLinkCard; if ("link_card" in tp.insert) return TpLinkCard;
if ("mention" in tp.insert) return TpMention; if ("mention" in tp.insert) return TpMention;
if ("video" in tp.insert) return TpVideo; if ("video" in tp.insert) return TpVideo;

View File

@@ -0,0 +1,139 @@
<!-- UGC关卡组件 TODO:UI调整-->
<template>
<div class="tul-card-box" @click="console.log(props.data)">
<TMiImg class="tul-cover" :src="props.data.insert.level.cover.url" alt="cover" />
<div class="tul-content">
<div class="tul-top">
<div class="tul-title">{{ props.data.insert.level.level_name }}</div>
<div class="tul-sub">
<div class="tul-info-item" title="游戏类型">
<v-icon size="16" color="yellow">mdi-gamepad-variant</v-icon>
<span>{{ props.data.insert.level.play_type }}</span>
</div>
<div class="tul-info-item" title="游玩人数">
<v-icon size="16" color="blue">mdi-account-multiple</v-icon>
<span>{{ props.data.insert.level.show_limit_play_num_str }}</span>
</div>
</div>
<div class="tul-attach" v-if="props.data.insert.level.level_attachment">
<span></span>
<span>{{ props.data.insert.level.level_attachment.content }}</span>
</div>
</div>
<div class="tul-bottom">
<div class="tul-info">
<div class="tul-info-item" title="热度">
<v-icon size="16" color="orange">mdi-fire</v-icon>
<span>{{ props.data.insert.level.hot_score }}</span>
</div>
<div class="tul-info-item" title="点赞率">
<v-icon size="16" color="pink">mdi-thumb-up</v-icon>
<span>{{ props.data.insert.level.good_rate }}</span>
</div>
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import TMiImg from "@comp/app/t-mi-img.vue";
type TpUgcLevel = { insert: { level: TGApp.BBS.UGC.Level } };
type TpUgcLevelProps = { data: TpUgcLevel };
const props = defineProps<TpUgcLevelProps>();
</script>
<style lang="scss" scoped>
.tul-card-box {
display: flex;
width: 100%;
box-sizing: border-box;
padding: 8px;
border: 1px solid var(--common-shadow-1);
border-radius: 4px;
margin-bottom: 8px;
background: var(--box-bg-1);
column-gap: 8px;
}
.tul-cover {
max-width: 50%;
max-height: 180px;
border-radius: 4px;
cursor: pointer;
transition: all 0.5s;
&:hover {
scale: 0.9;
}
}
.tul-content {
display: flex;
width: 100%;
flex-direction: column;
align-items: center;
justify-content: space-between;
font-family: var(--font-title);
}
.tul-top {
display: flex;
width: 100%;
flex-direction: column;
align-items: flex-start;
justify-content: center;
}
.tul-title {
color: var(--tgc-od-red);
font-family: var(--font-title);
font-size: 20px;
}
.tul-sub {
display: flex;
width: 100%;
align-items: center;
justify-content: flex-start;
color: var(--tgc-od-white);
column-gap: 16px;
font-size: 16px;
}
.tul-attach {
margin-top: 8px;
color: var(--common-text-secondary);
font-size: 16px;
span {
color: var(--tgc-od-blue);
&:first-child {
font-family: Genshin, sans-serif;
font-size: 20px;
line-height: 14px;
vertical-align: top;
}
}
}
.tul-bottom {
display: flex;
width: 100%;
align-items: center;
justify-content: flex-end;
}
.tul-info {
display: flex;
gap: 16px;
}
.tul-info-item {
display: flex;
align-items: center;
font-size: 14px;
gap: 4px;
}
</style>

View File

@@ -0,0 +1,52 @@
<!-- ugc_master_tag backup_text -->
<template>
<span class="tut-box">
{{ tagName }}
</span>
</template>
<script lang="ts" setup>
import useAppStore from "@store/app.js";
import { str2Color } from "@utils/toolFunc.js";
import { storeToRefs } from "pinia";
import { computed } from "vue";
type TpUgcTag = {
insert: {
backup_text: string;
ugc_master_tag: {
id: number;
name: string;
is_available: boolean;
};
};
};
type TpUgcTagProps = { data: TpUgcTag };
const props = defineProps<TpUgcTagProps>();
const { theme } = storeToRefs(useAppStore());
const tagName = computed<string>(() => props.data.insert.ugc_master_tag.name);
const isDarkMode = computed<boolean>(() => theme.value === "dark");
const tagColor = computed<string>(() => tag2Color(tagName.value, isDarkMode.value));
const bgColor = computed<string>(() => `rgba(${tagColor.value.slice(4, -1)}, 0.18)`);
function tag2Color(str: string, isDarkMode: boolean = false): string {
const adjust = isDarkMode ? 80 : -40;
return str2Color(str, adjust);
}
</script>
<style lang="scss" scoped>
.tut-box {
display: inline-flex;
width: fit-content;
align-items: center;
justify-content: center;
padding: 0 6px;
border-radius: 12px;
margin-right: 4px;
background: v-bind(bgColor); /* stylelint-disable-line value-keyword-case */
color: v-bind(tagColor); /* stylelint-disable-line value-keyword-case */
font-family: var(--font-title);
font-size: 12px;
gap: 2px;
}
</style>

View File

@@ -25,3 +25,15 @@ export const AvatarExtResTypeEnum: typeof TGApp.BBS.User.AvatarExtResType = {
GIF: 3, GIF: 3,
PNG: 4, PNG: 4,
}; };
/**
* @description 帖子ViewType
* @since Beta v0.8.4
* @enum PostViewTypeEnum
*/
export const PostViewTypeEnum: typeof TGApp.BBS.Post.PostViewType = {
NORMAL: 1,
PIC: 2,
VOD: 5,
UGC: 7,
};

View File

@@ -151,7 +151,7 @@ declare namespace TGApp.BBS.Post {
* @property {string} release_time_type 发布时间类型 * @property {string} release_time_type 发布时间类型
* @property {number} future_release_time 未来发布时间 * @property {number} future_release_time 未来发布时间
* @property {ExternalLink} external_link 外部链接信息 * @property {ExternalLink} external_link 外部链接信息
* @property {unknown} post_full_extra_info 帖子完整额外信息,可能为 null * @property {PostExtraFull} post_full_extra_info 帖子完整额外信息,可能为 null
* @property {unknown} post_attachment_info 帖子附件信息,可能为 null * @property {unknown} post_attachment_info 帖子附件信息,可能为 null
* @property {unknown} feed_attachment_info 动态附件信息,可能为 null * @property {unknown} feed_attachment_info 动态附件信息,可能为 null
*/ */
@@ -191,7 +191,7 @@ declare namespace TGApp.BBS.Post {
release_time_type: string; release_time_type: string;
future_release_time: number; future_release_time: number;
external_link: ExternalLink; external_link: ExternalLink;
post_full_extra_info: unknown | null; post_full_extra_info: PostExtraFull | null;
post_attachment_info: unknown | null; post_attachment_info: unknown | null;
feed_attachment_info: unknown | null; feed_attachment_info: unknown | null;
}; };
@@ -243,7 +243,7 @@ declare namespace TGApp.BBS.Post {
* @property {string} subject 帖子标题 * @property {string} subject 帖子标题
* @property {string} content 帖子内容,为 html 格式 * @property {string} content 帖子内容,为 html 格式
* @property {string} cover 封面图 URL可能为 "" * @property {string} cover 封面图 URL可能为 ""
* @property {number} view_type 浏览类型 * @property {ViewTypeEnum} view_type 浏览类型
* @property {number} created_at 发帖时间 * @property {number} created_at 发帖时间
* @property {Array<string>} images 图片列表,可能为空 * @property {Array<string>} images 图片列表,可能为空
* @property {PostStat} post_status 帖子状态 * @property {PostStat} post_status 帖子状态
@@ -286,7 +286,7 @@ declare namespace TGApp.BBS.Post {
subject: string; subject: string;
content: string; content: string;
cover: string; cover: string;
view_type: number; view_type: ViewTypeEnum | number;
created_at: number; created_at: number;
images: Array<string>; images: Array<string>;
post_status: PostStat; post_status: PostStat;
@@ -322,6 +322,31 @@ declare namespace TGApp.BBS.Post {
aigc_meta: PostAigcMeta | null; aigc_meta: PostAigcMeta | null;
}; };
/**
* @description 浏览类型枚举
* @since Beta v0.8.4
* @const PostViewType
* @todo 待确定是否有其他类型
* @property {number} NORMAL 正常帖子
* @property {number} PIC 图片帖子如同人图COS
* @property {number} VOD 含视频帖子
* @property {number} UGC 千星奇域 // TODO: 待确定是否有其他分类
*/
const PostViewType = <const>{
NORMAL: 1,
PIC: 2,
VOD: 5,
UGC: 7,
};
/**
* @description 浏览类型枚举类型
* @since Beta v0.8.4
* @enum {number}
* @type PostViewTypeEnum
*/
type ViewTypeEnum = (typeof PostViewType)[keyof typeof PostViewType];
/** /**
* @description 帖子状态 * @description 帖子状态
* @since Beta v0.7.2 * @since Beta v0.7.2
@@ -664,4 +689,28 @@ declare namespace TGApp.BBS.Post {
* @property {string} external_link_title 外部链接标题 * @property {string} external_link_title 外部链接标题
*/ */
type ExternalLink = { external_link: string; external_link_title: string }; type ExternalLink = { external_link: string; external_link_title: string };
/**
* @description 帖子完整额外信息
* @since Beta v0.8.4
* @interface PostExtraFull
* @property {PostExtraUgcFull} ugc_master_post_extra UGCMaster 额外信息
*/
type PostExtraFull = { ugc_master_post_extra: PostExtraUgcFull };
/**
* @description UGC所有者完整额外信息
* @since Beta v0.8.4
* @interface PostExtraUgcFull
* @property {TGApp.BBS.UGC.Character} game_character 游戏角色信息
* @property {boolean} user_is_use_game_info 用户是否使用游戏信息
* @property {string} ugc_master_post_type UGC 主帖子类型
* @property {Array<TGApp.BBS.UGC.Level>} level_list 等级列表
*/
type PostExtraUgcFull = {
game_character: TGApp.BBS.UGC.Character;
user_is_use_game_info: boolean;
ugc_master_post_type: string;
level_list: Array<TGApp.BBS.UGC.Level>;
};
} }

View File

@@ -1,7 +1,7 @@
/** /**
* @file types/BBS/SctPost.d.ts * @file types/BBS/SctPost.d.ts
* @description 结构化帖子类型声明文件 * @description 结构化帖子类型声明文件
* @since Beta v0.7.2 * @since Beta v0.8.4
*/ */
declare namespace TGApp.BBS.SctPost { declare namespace TGApp.BBS.SctPost {
@@ -31,14 +31,59 @@ declare namespace TGApp.BBS.SctPost {
type Empty = { insert: never; attributes?: never }; type Empty = { insert: never; attributes?: never };
/** /**
* @description 帖子结构化数据-其他类型 * @description 帖子结构化数据-viewType为2
* @since Beta v0.6.7 * @since Beta v0.8.4
* @property {string} describe - 描述 * @property {string} describe - 描述
* @property {Array<string>} imgs - 图片链接 * @property {Array<string>} imgs - 图片链接
* @property {Array<string>} link_card_ids - 关联卡片ID * @property {Array<string>} link_card_ids - 关联卡片ID
* @return Other * @return Pic
*/ */
type Other = { describe: string; imgs: Array<string>; link_card_ids?: Array<string> } & { type Pic = { describe: string; imgs: Array<string>; link_card_ids?: Array<string> } & {
[key: string]: unknown; [key: string]: unknown;
}; };
/**
* @description 帖子结构化数据-viewType为7
* @since Beta v0.8.4
* @description 下面详细结构参见相关组件
* @property {Array<Base>} text - 文字内容
* @property {Array<UgcImage>} images - 图片内容
* @property {Array<UgcVod>} vods - 视频内容
* @property {Array<UgcLevel>} levels - 等级内容
*/
type Ugc = {
text: Array<Base>;
images: Array<UgcImage>;
vods: Array<UgcVod>;
levels: Array<UgcLevel>;
};
/**
* @description Ugc结构下的图片内容
* @since Beta v0.8.4
* @interface UgcImage
* @property {number} image_id - 图片ID
* @property {string} image_url - 图片链接
* @property {TGApp.BBS.Post.Image} image - 图片信息
*/
type UgcImage = { image_id: number; image_url: string; image: TGApp.BBS.Post.Image };
/**
* @description Ugc结构下的视频内容
* @since Beta v0.8.4
* @interface UgcVod
* @property {string} vod_id - 视频ID
* @property {TGApp.BBS.Post.Vod} vod - 视频信息
*/
type UgcVod = { vod_id: string; vod: TGApp.BBS.Post.Vod };
/**
* @description Ugc结构下的关卡内容
* @since Beta v0.8.4
* @interface UgcLevel
* @property {string} level_id - 关卡ID
* @property {string} region - 关卡服务器
* @property {TGApp.BBS.UGC.Level} level - 关卡信息
*/
type UgcLevel = { level_id: string; region: string; level: TGApp.BBS.UGC.Level };
} }

133
src/types/BBS/UGC.d.ts vendored Normal file
View File

@@ -0,0 +1,133 @@
/**
* @file types/BBS/UGC.d.ts
* @description 千星奇域类型定义文件
* @since Beta v0.8.4
*/
declare namespace TGApp.BBS.UGC {
/**
* @description UGC 游戏角色信息
* @since Beta v0.8.4
* @interface Character
* @property {string} region 游戏区服
* @property {string} game_uid 游戏 UID
* @property {string} nickname 游戏昵称
* @property {string} user_label 用户标签
* @property {string} region_name 区服名称
*/
type Character = {
region: string;
game_uid: string;
nickname: string;
user_label: string;
region_name: string;
};
/**
* @description 关卡信息
* @since Beta v0.8.4
* @interface Level
* @property {string} level_id 关卡 ID
* @property {string} region 区域
* @property {string} level_name 关卡名称
* @property {Cover} cover 关卡封面
* @property {string} desc 关卡描述
* @property {number} limit_play_num_min 最小游玩人数
* @property {number} limit_play_num_max 最大游玩人数
* @property {string} play_type 游玩类型
* @property {string} good_rate 好评率
* @property {string} hot_score 热度分数
* @property {string} creator_uid 创建者 UID
* @property {InteractInfo} interact_info 交互信息
* @property {LevelAttachment} level_attachment 关卡附件
* @property {UserPlayInfo} user_play_info 用户游玩信息
* @property {Extra} extra 额外信息
* @property {boolean} level_info_has_released 关卡信息是否已发布
* @property {string} level_source_type 关卡来源类型
* @property {string} data_box 数据盒
* @property {string} show_limit_play_num_str 显示的游玩人数限制字符串
* @property {string} level_intro 关卡介绍
*/
type Level = {
level_id: string;
region: string;
level_name: string;
cover: Cover;
desc: string;
limit_play_num_min: number;
limit_play_num_max: number;
play_type: string;
good_rate: string;
hot_score: string;
creator_uid: string;
interact_info: InteractInfo;
level_attachment: LevelAttachment | null;
user_play_info: UserPlayInfo;
extra: Extra;
level_info_has_released: boolean;
level_source_type: string;
data_box: string;
show_limit_play_num_str: string;
level_intro: string;
};
/**
* @description 关卡封面
* @since Beta v0.8.4
* @interface Cover
* @property {string} url 封面链接
*/
type Cover = { url: string };
/**
* @description 用户交互信息
* @since Beta v0.8.4
* @interface InteractInfo
* @property {boolean} has_fav 是否已收藏
*/
type InteractInfo = { has_fav: boolean };
/**
* @description 关卡附件
* @since Beta v0.8.4
* @interface LevelAttachment
* @property {string} type 信息类型
* @property {string} content 信息内容
*/
type LevelAttachment = { type: string; content: string };
/**
* @description 游玩信息
* @since Beta v0.8.4
* @interface UserPlayInfo
* @property {boolean} has_played 是否已游玩
* @property {string} played_time 游玩时间
* @property {number} played_count 游玩次数
*/
type UserPlayInfo = { has_played: boolean; played_time: string; played_count: number };
/**
* @description 额外信息
* @since Beta v0.8.4
* @interface Extra
* @property {Array<PlayLink>} play_link 游玩链接
* @property {boolean} friends_played 好友游玩过
* @property {Array<unknown>} friends_played_list 好友游玩列表 // TODO: 类型待确定
* @property {string} first_online_time 首次上线时间
*/
type Extra = {
play_link: Array<PlayLink>;
friends_played: boolean;
friends_played_list: Array<Character>;
first_online_time: string;
};
/**
* @description 游玩链接
* @since Beta v0.8.4
* @interface PlayLink
* @property {string} link_content 链接内容
* @property {string} link_type 链接类型
*/
type PlayLink = { link_content: string; link_type: string };
}

View File

@@ -133,6 +133,7 @@ import VpBtnCollect from "@comp/viewPost/vp-btn-collect.vue";
import VpBtnReply from "@comp/viewPost/vp-btn-reply.vue"; import VpBtnReply from "@comp/viewPost/vp-btn-reply.vue";
import VpOverlayCollection from "@comp/viewPost/vp-overlay-collection.vue"; import VpOverlayCollection from "@comp/viewPost/vp-overlay-collection.vue";
import VpOverlayUser from "@comp/viewPost/vp-overlay-user.vue"; import VpOverlayUser from "@comp/viewPost/vp-overlay-user.vue";
import { PostViewTypeEnum } from "@enum/bbs.js";
import apiHubReq from "@req/apiHubReq.js"; import apiHubReq from "@req/apiHubReq.js";
import postReq from "@req/postReq.js"; import postReq from "@req/postReq.js";
import useAppStore from "@store/app.js"; import useAppStore from "@store/app.js";
@@ -272,13 +273,25 @@ function getRepublishAuthorization(type: number): string {
async function getRenderPost( async function getRenderPost(
data: TGApp.BBS.Post.FullData, data: TGApp.BBS.Post.FullData,
): Promise<Array<TGApp.BBS.SctPost.Base>> { ): Promise<Array<TGApp.BBS.SctPost.Base>> {
if (
data.post.view_type === PostViewTypeEnum.NORMAL ||
data.post.view_type === PostViewTypeEnum.VOD
) {
return JSON.parse(data.post.structured_content);
}
if (data.post.view_type === PostViewTypeEnum.PIC) {
return await parsePostPic(data);
}
if (data.post.view_type === PostViewTypeEnum.UGC) {
return parsePostUgc(data.post);
}
const postContent = data.post.content; const postContent = data.post.content;
let jsonParse: string; let jsonParse: string;
if (postContent.startsWith("<")) { if (postContent.startsWith("<")) {
jsonParse = data.post.structured_content; jsonParse = data.post.structured_content;
} else { } else {
try { try {
jsonParse = await parseContent(data); jsonParse = JSON.stringify(await parsePostPic(data));
} catch (e) { } catch (e) {
if (e instanceof SyntaxError) { if (e instanceof SyntaxError) {
await TGLogger.Warn(`[t-post][${postId}] ${e.name}: ${e.message}`); await TGLogger.Warn(`[t-post][${postId}] ${e.name}: ${e.message}`);
@@ -291,9 +304,11 @@ async function getRenderPost(
return res; return res;
} }
async function parseContent(fullData: TGApp.BBS.Post.FullData): Promise<string> { async function parsePostPic(
fullData: TGApp.BBS.Post.FullData,
): Promise<Array<TGApp.BBS.SctPost.Base>> {
const content = fullData.post.content; const content = fullData.post.content;
const data: TGApp.BBS.SctPost.Other = JSON.parse(content); const data: TGApp.BBS.SctPost.Pic = JSON.parse(content);
const result: TGApp.BBS.SctPost.Base[] = []; const result: TGApp.BBS.SctPost.Base[] = [];
for (const key of Object.keys(data)) { for (const key of Object.keys(data)) {
switch (key) { switch (key) {
@@ -325,7 +340,25 @@ async function parseContent(fullData: TGApp.BBS.Post.FullData): Promise<string>
break; break;
} }
} }
return JSON.stringify(result); return result;
}
function parsePostUgc(post: TGApp.BBS.Post.Post): Array<TGApp.BBS.SctPost.Base> {
const data: TGApp.BBS.SctPost.Ugc = JSON.parse(post.structured_content);
const result: Array<TGApp.BBS.SctPost.Base> = [];
for (const text of data.text) {
result.push(text);
}
for (const image of data.images) {
result.push({ insert: { image: image.image } });
}
for (const vod of data.vods) {
result.push({ insert: { vod: vod.vod } });
}
for (const level of data.levels) {
result.push({ insert: { level: level.level } });
}
return result;
} }
async function createPostJson(postId: number): Promise<void> { async function createPostJson(postId: number): Promise<void> {