-
Notifications
You must be signed in to change notification settings - Fork 215
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
8 changed files
with
315 additions
and
10 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,16 +1,212 @@ | ||
<template> | ||
<div class="flex flex-col h-full"> | ||
<Teleport defer to="#title"> | ||
<h1 class="text-[28px] leading-[34px] text-slate-12 font-bold">合集下载</h1> | ||
<h1 class="text-[28px] leading-[34px] text-slate-12 font-bold">合集下载 <span class="text-sm text-slate-10">合集数据来自于已缓存文章</span> | ||
</h1> | ||
</Teleport> | ||
<div class="px-6 flex-1 overflow-scroll"> | ||
<p>~~ 敬请期待 ~~</p> | ||
<div class="flex flex-1 overflow-hidden"> | ||
|
||
<ul class="flex flex-col h-full w-fit overflow-y-scroll divide-y"> | ||
<li v-for="accountInfo in cachedAccountInfos" :key="accountInfo.fakeid" class="relative px-4 pr-16 py-4" | ||
:class="{'bg-slate-3': selectedAccount?.fakeid === accountInfo.fakeid}" | ||
@click="toggleSelectedAccount(accountInfo)"> | ||
<p>公众号: | ||
<span v-if="accountInfo.nickname" class="text-xl font-medium">{{ accountInfo.nickname }}</span> | ||
</p> | ||
<p>ID: <span class="font-mono">{{ accountInfo.fakeid }}</span></p> | ||
<UBadge variant="subtle" color="red" class="absolute top-4 right-2"> | ||
<Loader v-if="!accountInfo.albums" :size="28" class="animate-spin text-slate-500"/> | ||
<span v-else>{{ accountInfo.albums.length }}</span> | ||
</UBadge> | ||
</li> | ||
</ul> | ||
<ul v-if="selectedAccount" class="flex flex-col h-full w-72 overflow-y-scroll divide-y"> | ||
<li v-for="(album, index) in selectedAccount.albums" :key="album.id" class="p-4" | ||
:class="{'bg-slate-3': selectedAlbum?.id === album.id}" @click="toggleSelectedAlbum(album)"> | ||
{{ index + 1 }}. {{ album.title }} | ||
</li> | ||
</ul> | ||
<main class="flex-1 h-full overflow-y-scroll bg-[#ededed]" v-if="selectedAccount && selectedAlbum"> | ||
<div v-if="albumLoading" class="flex justify-center items-center mt-5"> | ||
<Loader :size="28" class="animate-spin text-slate-500"/> | ||
</div> | ||
<div v-else-if="albumBaseInfo" class="max-w-2xl mx-auto bg-white"> | ||
<!-- banner --> | ||
<div class="px-5 pt-7 pb-14 banner"> | ||
<h2 class="text-2xl text-white font-bold"># {{ albumBaseInfo.title }}</h2> | ||
</div> | ||
<div class="rounded-xl -mt-4 relative z-50 bg-white px-4 py-6"> | ||
<!-- 头部信息 --> | ||
<div class="pb-10"> | ||
<p class="flex items-center space-x-2 mb-2"> | ||
<img class="size-5" :src="albumBaseInfo.brand_icon" alt=""> | ||
<span>{{ albumBaseInfo.nickname }}</span> | ||
</p> | ||
<p class="text-sm text-slate-10"> | ||
<span>{{ albumBaseInfo.article_count }}篇内容</span> | ||
<span v-if="albumBaseInfo.description"> · {{ albumBaseInfo.description }}</span> | ||
</p> | ||
<div class="flex mt-8 space-x-2 w-fit" @click="toggleReverse"> | ||
<ArrowUpNarrowWide v-if="isReverse" /> | ||
<ArrowDownNarrowWide v-else /> | ||
<span>{{isReverse ? '倒序' : '正序'}}</span> | ||
</div> | ||
</div> | ||
<!-- 文章列表 --> | ||
<ul class="divide-y"> | ||
<li class="flex justify-between items-center py-5 px-1" v-for="article in albumArticles" | ||
:key="article.key"> | ||
<div class="flex-1"> | ||
<h3 class="text-lg mb-2"> | ||
<span v-if="article.pos_num">{{ article.pos_num }}. </span> | ||
<span>{{ article.title }}</span> | ||
</h3> | ||
<time class="text-sm text-slate-10">{{ article.create_time }}</time> | ||
</div> | ||
<img class="size-16 ml-4 flex-shrink-0" :src="article.cover_img_1_1" alt=""> | ||
</li> | ||
</ul> | ||
<div v-element-visibility="onElementVisibility"></div> | ||
<p v-if="articleLoading" class="flex justify-center items-center mt-2 py-2"> | ||
<Loader :size="28" class="animate-spin text-slate-500"/> | ||
</p> | ||
<p v-else-if="noMoreData" class="text-center mt-2 py-2 text-slate-400">已全部加载完毕</p> | ||
</div> | ||
</div> | ||
</main> | ||
</div> | ||
</div> | ||
</template> | ||
<script setup lang="ts"> | ||
import {getAllInfo, type Info} from "~/store/info"; | ||
import {getArticleCache} from "~/store/article"; | ||
import type {AppMsgAlbumInfo} from "~/types/types"; | ||
import {Loader} from "lucide-vue-next"; | ||
import type {AppMsgAlbumResult, ArticleItem, BaseInfo} from "~/types/album"; | ||
import { ArrowDownNarrowWide, ArrowUpNarrowWide } from 'lucide-vue-next'; | ||
import {vElementVisibility} from "@vueuse/components" | ||
useHead({ | ||
title: '合集下载 | 微信公众号文章导出' | ||
}); | ||
interface AccountInfo extends Info { | ||
albums?: AppMsgAlbumInfo[] | ||
} | ||
// 已缓存的公众号信息 | ||
const cachedAccountInfos: AccountInfo[] = reactive(await getAllInfo()) | ||
cachedAccountInfos.forEach(async accountInfo => { | ||
accountInfo.albums = await getAllAlbums(accountInfo.fakeid) | ||
}) | ||
const selectedAccount = ref<AccountInfo | null>(null) | ||
async function toggleSelectedAccount(info: AccountInfo) { | ||
if (info.fakeid !== selectedAccount.value?.fakeid) { | ||
selectedAccount.value = info | ||
} | ||
} | ||
const selectedAlbum = ref<AppMsgAlbumInfo | null>(null) | ||
function toggleSelectedAlbum(album: AppMsgAlbumInfo) { | ||
if (album.id !== selectedAlbum.value?.id) { | ||
selectedAlbum.value = album | ||
isReverse.value = false | ||
getFirstPageAlbumData().catch(() => {}) | ||
} | ||
} | ||
async function getAllAlbums(fakeid: string) { | ||
const articles = await getArticleCache(fakeid, Date.now()) | ||
const albums: AppMsgAlbumInfo[] = [] | ||
articles.flatMap(article => article.appmsg_album_infos).forEach(album => { | ||
if (!albums.some(a => a.id === album.id)) { | ||
albums.push(album) | ||
} | ||
}) | ||
return albums | ||
} | ||
const albumArticles: ArticleItem[] = reactive([]) | ||
const albumBaseInfo = ref<BaseInfo | null>(null) | ||
const isReverse = ref(false) | ||
const albumLoading = ref(false) | ||
const articleLoading = ref(false) | ||
async function getFirstPageAlbumData() { | ||
albumLoading.value = true | ||
albumArticles.length = 0 | ||
const data = await $fetch<AppMsgAlbumResult>('/api/appmsgalbum', { | ||
method: 'GET', | ||
query: { | ||
fakeid: selectedAccount.value!.fakeid, | ||
album_id: selectedAlbum.value!.id, | ||
is_reverse: isReverse.value ? '1' : '0', | ||
} | ||
}) | ||
albumLoading.value = false | ||
if (data.base_resp.ret === 0) { | ||
albumBaseInfo.value = data.getalbum_resp.base_info | ||
albumArticles.push(...data.getalbum_resp.article_list) | ||
noMoreData.value = data.getalbum_resp.continue_flag === '0' | ||
} | ||
} | ||
function toggleReverse() { | ||
isReverse.value = !isReverse.value | ||
albumArticles.length = 0 | ||
} | ||
async function loadMoreData() { | ||
articleLoading.value = true | ||
const lastArticle = albumArticles[albumArticles.length - 1] | ||
const data = await $fetch<AppMsgAlbumResult>('/api/appmsgalbum', { | ||
method: 'GET', | ||
query: { | ||
fakeid: selectedAccount.value!.fakeid, | ||
album_id: selectedAlbum.value!.id, | ||
is_reverse: isReverse.value ? '1' : '0', | ||
begin_msgid: lastArticle?.msgid, | ||
begin_itemidx: lastArticle?.itemidx, | ||
} | ||
}) | ||
articleLoading.value = false | ||
if (data.base_resp.ret === 0) { | ||
albumArticles.push(...data.getalbum_resp.article_list) | ||
noMoreData.value = data.getalbum_resp.continue_flag === '0' | ||
} | ||
} | ||
const noMoreData = ref(false) | ||
// 判断是否触底 | ||
const bottomElementIsVisible = ref(false) | ||
function onElementVisibility(visible: boolean) { | ||
bottomElementIsVisible.value = visible | ||
if (visible && !noMoreData.value) { | ||
loadMoreData() | ||
} | ||
} | ||
</script> | ||
<style scoped> | ||
.banner { | ||
background: linear-gradient(rgb(9, 9, 9), rgb(35, 35, 35)); | ||
} | ||
</style> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
/** | ||
* 获取合集数据接口 | ||
*/ | ||
|
||
import {proxyMpRequest} from "~/server/utils"; | ||
|
||
interface AppMsgAlbumQuery { | ||
fakeid: string | ||
album_id: string | ||
is_reverse?: string | ||
count?: number | ||
begin_msgid?: string | ||
begin_itemidx?: string | ||
} | ||
|
||
export default defineEventHandler(async (event) => { | ||
const query = getQuery<AppMsgAlbumQuery>(event) | ||
const fakeid = query.fakeid | ||
const album_id = query.album_id | ||
const isReverse = query.is_reverse || '0' | ||
const begin_msgid = query.begin_msgid | ||
const begin_itemidx = query.begin_itemidx | ||
const count: number = query.count || 10 | ||
|
||
|
||
const params: Record<string, string | number | undefined> = { | ||
action: 'getalbum', | ||
__biz: fakeid, | ||
album_id: album_id, | ||
begin_msgid: begin_msgid, | ||
begin_itemidx: begin_itemidx, | ||
count: count, | ||
is_reverse: isReverse, | ||
f: "json", | ||
} | ||
|
||
return proxyMpRequest({ | ||
event: event, | ||
method: 'GET', | ||
endpoint: 'https://mp.weixin.qq.com/mp/appmsgalbum', | ||
query: params, | ||
parseJson: true, | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
import type {RGB} from "~/types/types"; | ||
|
||
interface BaseResp { | ||
exportkey_token: string | ||
ret: number | ||
} | ||
type BooleanString = "0" | "1" | ||
|
||
export interface BaseInfo { | ||
article_count: string | ||
brand_icon: string | ||
cover: string | ||
cover_ban: BooleanString | ||
description: string | ||
fee: string | ||
is_first_screen: BooleanString | ||
is_numbered: BooleanString | ||
is_paid: BooleanString | ||
is_reverse: BooleanString | ||
isupdating: BooleanString | ||
needpay: BooleanString | ||
nickname: string | ||
public_tag_content_num: string | ||
public_tag_link: string | ||
share_brand_icon: string | ||
subtype: string | ||
title: string | ||
type: string | ||
update_frequence: Record<string, string> | ||
username: string | ||
} | ||
|
||
export interface ArticleItem { | ||
cover_img_1_1: string | ||
cover_theme_color: RGB | ||
create_time: string | ||
is_pay_subscribe: BooleanString | ||
is_read: BooleanString | ||
item_show_type: string | ||
itemidx: string | ||
key: string | ||
msgid: string | ||
pos_num?: string | ||
title: string | ||
tts_is_ban: string | ||
url: string | ||
user_read_status: string | ||
} | ||
|
||
export interface GetAlbumResp { | ||
article_list: ArticleItem[] | ||
base_info: BaseInfo | ||
continue_flag: BooleanString | ||
is_pay_subscribe: BooleanString | ||
reverse_continue_flag: BooleanString | ||
} | ||
|
||
export interface AppMsgAlbumResult { | ||
base_resp: BaseResp | ||
getalbum_resp: GetAlbumResp | ||
} |