feat: add frontend

This commit is contained in:
Rogee
2024-09-20 23:24:50 +08:00
parent 098b5ac8bc
commit 5aecfe6379
49 changed files with 861 additions and 1117 deletions

View File

@@ -1,3 +0,0 @@
*.js
*.vue.js

View File

@@ -1,11 +1,15 @@
<script setup lang="ts">
import Navigation from './components/Navigation.vue';
</script>
<template>
<Navigation />
<Navigation :links="stNav.links" :setActive="stNav.setActive" />
<main class="p-4 mt-16 md:max-w-screen-md mx-auto">
<RouterView />
</main>
</template>
<script setup>
import Navigation from '@/components/Navigation.vue';
import { useNavigationStore } from "@/stores/navigation";
const stNav = useNavigationStore();
</script>

View File

@@ -1,19 +1,20 @@
<template>
<div class="bg-slate-100 border rounded mb-4 p-2 md:mb-8 md:p-4">
<div
v-if="item.Content.length > 0"
class="text-wrap font-sans"
v-html="processedContent"
></div>
<div v-if="item.Content.length > 0" class="text-wrap font-sans" v-html="processedContent"></div>
<MediaGrid :medias="item.Media" :channel="item.ChannelID" />
<div v-if="item.Media.length > 0" class="mt-2 md:mt-4">
<div class="medias grid grid-cols-3 gap-2 md:gap-4">
<template v-for="media in item.Media" :key="media.id">
<MediaItem :media="media" :channel="item.ChannelID" />
</template>
</div>
</div>
</div>
</template>
<script lang="ts">
import { PostItem } from "@/types";
import { defineComponent, computed, PropType } from "vue";
import MediaGrid from "./MediaGrid.vue";
<script>
import { computed, defineComponent } from "vue";
import MediaItem from "./MediaItem.vue";
function nl2br(str, is_xhtml) {
var breakTag = is_xhtml || typeof is_xhtml === "undefined" ? "<br />" : "<br>";
@@ -22,19 +23,16 @@ function nl2br(str, is_xhtml) {
export default defineComponent({
components: {
MediaGrid,
MediaItem,
},
name: "ListItem",
props: {
item: {
type: Object ,
required: true,
},
item: { type: Object, required: true },
},
setup(props) {
const processedContent = computed(() => {
let content = props.item.Content.trim();
return nl2br(content,false);
return nl2br(content, false);
});
return {

View File

@@ -1,9 +1,6 @@
<template>
<div
v-if="hideVideo"
@click="hideVideo = false"
class="bg-gray-500 min-h-full cursor-pointer flex justify-center align-center"
>
<div v-if="hideVideo" @click="hideVideo = false"
class="bg-gray-500 min-h-full cursor-pointer flex justify-center align-center">
<PlayIcon class="text-gray-400 w-1/2" />
</div>
<video v-else controls class="min-w-full min-h-full">
@@ -11,10 +8,9 @@
</video>
</template>
<script lang="ts">
import { Media } from "@/types";
<script>
import { PlayIcon } from "@heroicons/vue/24/outline";
import { defineComponent, PropType, ref } from "vue";
import { defineComponent, ref } from "vue";
export default defineComponent({
components: {
@@ -23,10 +19,7 @@ export default defineComponent({
name: "MediaDocument",
props: {
channel: { required: true },
media: {
type: Object as PropType<Media>,
required: true,
},
media: { type: Object, required: true },
},
setup(props) {
const videoSrc = () => {

View File

@@ -1,45 +0,0 @@
<template>
<div v-if="medias.length > 0" class="mt-2 md:mt-4">
<template v-if="medias.length == 1">
<template v-for="media in medias" :key="media.id">
<MediaItem :media="media" :channel="channel" />
</template>
</template>
<template v-else-if="medias.length % 2 == 0 && medias.length % 3 != 0">
<div class="medias grid grid-cols-2 gap-2 md:gap-4">
<template v-for="media in medias" :key="media.id">
<MediaItem :media="media" :channel="channel" />
</template>
</div>
</template>
<template v-else>
<div class="medias grid grid-cols-3 gap-2 md:gap-4">
<template v-for="media in medias" :key="media.id">
<MediaItem :media="media" :channel="channel" />
</template>
</div>
</template>
</div>
</template>
<script lang="ts">
import { Media } from "@/types";
import { defineComponent, PropType } from "vue";
import MediaItem from "./MediaItem.vue";
export default defineComponent({
components: {
MediaItem,
},
name: "MediaGrid",
props: {
channel: { required: true },
medias: {
type: Object as PropType<Media[]>,
required: true,
},
},
});
</script>

View File

@@ -1,19 +1,13 @@
<template>
<div
class="max-w-full overflow-hidden hide-scrollbar rounded-sm hover:shadow-lg aspect-3/4 bg-gray-500 min-h-full cursor-pointer flex justify-center align-center"
>
class="aspect-3/4 max-w-full overflow-hidden hide-scrollbar rounded-sm hover:shadow-lg bg-gray-500 min-h-full cursor-pointer flex justify-center align-center">
<MediaPhoto v-if="media.photo?.length > 0" :media="media" :channel="channel" />
<MediaDocument
v-else-if="typeof media.document != undefined"
:media="media"
:channel="channel"
/>
<MediaDocument v-else-if="typeof media.document != undefined" :media="media" :channel="channel" />
</div>
</template>
<script lang="ts">
import { Media } from "@/types";
import { defineComponent, PropType } from "vue";
<script>
import { defineComponent } from "vue";
import MediaDocument from "./MediaDocument.vue";
import MediaPhoto from "./MediaPhoto.vue";
@@ -25,10 +19,7 @@ export default defineComponent({
name: "MediaItem",
props: {
channel: { required: true },
media: {
type: Object as PropType<Media>,
required: true,
},
media: { type: Object, required: true },
},
});
</script>

View File

@@ -1,28 +1,18 @@
<template>
<img
:src="photoSrc()"
class="max-w-full h-full max-h-full"
loading="lazy"
decoding="async"
@click="openPreview"
/>
<img :src="photoSrc()" class="max-w-full h-full max-h-full" loading="lazy" decoding="async" @click="openPreview" />
<div v-if="isPreviewVisible" class="modal" @click="closePreview">
<img :src="photoSrc()" alt="Preview" class="preview-image" />
</div>
</template>
<script lang="ts">
import { Media } from "@/types";
import { defineComponent, nextTick, PropType, ref } from "vue";
<script>
import { defineComponent, nextTick, ref } from "vue";
export default defineComponent({
name: "MediaPhoto",
props: {
channel: { required: true },
media: {
type: Object as PropType<Media>,
required: true,
},
media: { type: Object, required: true },
},
setup(props) {
const photoSrc = () => {

View File

@@ -1,22 +1,14 @@
<!-- This example requires Tailwind CSS v2.0+ -->
<template>
<Disclosure as="nav" class="bg-gray-800 fixed w-full top-0" v-slot="{ open }">
<Disclosure as="nav" class="bg-gray-800 fixed w-full top-0">
<nav class="md:max-w-screen-md mx-auto">
<div class="flex items-center justify-between h-16 px-4">
<div class="flex space-x-4">
<RouterLink
v-for="item in navigation"
:key="item.name"
:to="item.href"
:class="[
item.current
? 'bg-gray-900 text-white'
: 'text-gray-300 hover:bg-gray-700 hover:text-white',
'px-3 py-2 rounded-md text-sm font-medium cursor-pointer',
]"
:aria-current="item.current ? 'page' : undefined"
>{{ item.name }}</RouterLink
>
<RouterLink v-for="nav in links" :key="nav.name" :to="nav.href" @click="setActive(nav.name)" :class="[
nav.current
? 'bg-gray-900 text-white'
: 'text-gray-300 hover:bg-gray-700 hover:text-white',
'px-3 py-2 rounded-md text-sm font-medium cursor-pointer',
]" :aria-current="nav.current ? 'page' : undefined">{{ nav.name }}</RouterLink>
</div>
</div>
</nav>
@@ -24,22 +16,13 @@
</template>
<script>
import { Disclosure } from "@headlessui/vue";
import { RouterLink } from "vue-router";
import { defineComponent } from "vue";
const navigation = [
{ name: "Home", href: "/", current: false },
{ name: "Favorites", href: "/favorites", current: false },
];
export default {
components: {
RouterLink,
Disclosure,
export default defineComponent({
name: "Navigation",
props: {
links: { type: Array, required: true },
setActive: { type: Function, required: true },
},
setup() {
return {
navigation,
};
},
};
})
</script>

View File

@@ -1,37 +0,0 @@
export default [
{
"photo": "https://picsum.photos/300/300",
"msg_id": 26701,
"asset_id": 6330272214070443931
},
{
"photo": "https://picsum.photos/300/300",
"msg_id": 26702,
"asset_id": 6330272214070443932
},
{
"photo": "https://picsum.photos/300/300",
"msg_id": 26703,
"asset_id": 6330272214070443933
},
{
"photo": "https://picsum.photos/300/300",
"msg_id": 26704,
"asset_id": 6330272214070443934
},
{
"msg_id": 26705,
"asset_id": 6330272213614202883,
"document": {
"Ext": ".mp4",
"Size": 2235348,
"Video": {
"Width": 576,
"Height": 1280,
"Duration": 25.147
},
"Filename": "",
"MimeType": "video/mp4"
}
}
]

View File

@@ -0,0 +1,200 @@
export default [
{
"ID": 17,
"UUID": 1811965783,
"Username": "zhudayigebaipiao",
"Title": "𝖥𝗋𝖾𝖾 𝖢𝗁𝖺𝗇𝗇𝖾𝗅🐸",
"CreatedAt": "2024-09-12T10:26:09.874+08:00",
"UpdatedAt": "2024-09-12T10:26:09.874+08:00",
"Offset": 501,
"MinID": 501,
"ExportMedia": false
},
{
"ID": 3,
"UUID": 1674292882,
"Username": "Aliyun_4K_Movies",
"Title": "阿里云盘4K影视",
"CreatedAt": "2024-09-02T18:59:18.782+08:00",
"UpdatedAt": "2024-09-02T18:59:18.782+08:00",
"Offset": 27022,
"MinID": 27022,
"ExportMedia": false
},
{
"ID": 19,
"UUID": 1855542885,
"Username": "meinvshouji",
"Title": "美女收集器👗",
"CreatedAt": "2024-09-13T08:52:07.72+08:00",
"UpdatedAt": "2024-09-13T08:52:07.72+08:00",
"Offset": 1339,
"MinID": 1339,
"ExportMedia": true
},
{
"ID": 8,
"UUID": 1117480027,
"Username": "ruyoblog",
"Title": "如有乐享",
"CreatedAt": "2024-09-02T19:46:24.462+08:00",
"UpdatedAt": "2024-09-02T19:46:24.462+08:00",
"Offset": 4306,
"MinID": 4306,
"ExportMedia": false
},
{
"ID": 4,
"UUID": 1651435712,
"Username": "abskoop",
"Title": "ahhhhfsA姐分享",
"CreatedAt": "2024-09-02T19:10:15.728+08:00",
"UpdatedAt": "2024-09-02T19:10:15.728+08:00",
"Offset": 8628,
"MinID": 8628,
"ExportMedia": false
},
{
"ID": 5,
"UUID": 1469109660,
"Username": "shareAliyun",
"Title": "阿里云盘发布频道",
"CreatedAt": "2024-09-02T19:13:23.865+08:00",
"UpdatedAt": "2024-09-02T19:13:23.865+08:00",
"Offset": 66151,
"MinID": 66151,
"ExportMedia": false
},
{
"ID": 10,
"UUID": 2081161978,
"Username": "hackerNewsSummary007",
"Title": "Hacker News 中文摘要",
"CreatedAt": "2024-09-02T23:07:22.335+08:00",
"UpdatedAt": "2024-09-02T23:07:22.335+08:00",
"Offset": 1522,
"MinID": 1522,
"ExportMedia": false
},
{
"ID": 15,
"UUID": 1361351430,
"Username": "chiguamaopao",
"Title": "吃瓜冒泡吧",
"CreatedAt": "2024-09-12T10:14:06.746+08:00",
"UpdatedAt": "2024-09-12T10:14:06.746+08:00",
"Offset": 11634,
"MinID": 11634,
"ExportMedia": true
},
{
"ID": 11,
"UUID": 1166415755,
"Username": "buliang00",
"Title": "不良林",
"CreatedAt": "2024-09-02T23:09:25.094+08:00",
"UpdatedAt": "2024-09-02T23:09:25.094+08:00",
"Offset": 192,
"MinID": 192,
"ExportMedia": false
},
{
"ID": 18,
"UUID": 1443355998,
"Username": "biubiubiuchat",
"Title": "小岛电波",
"CreatedAt": "2024-09-12T10:39:38.504+08:00",
"UpdatedAt": "2024-09-12T10:39:38.504+08:00",
"Offset": 948,
"MinID": 948,
"ExportMedia": true
},
{
"ID": 20,
"UUID": 1723117448,
"Username": "yunying23",
"Title": "自媒体运营秘籍",
"CreatedAt": "2024-09-13T09:16:22.942+08:00",
"UpdatedAt": "2024-09-13T09:16:22.942+08:00",
"Offset": 3937,
"MinID": 3937,
"ExportMedia": true
},
{
"ID": 14,
"UUID": 2041671763,
"Username": "cgblz",
"Title": "吃瓜爆料站(苹果看不了👉🏾@cgbl8)",
"CreatedAt": "2024-09-03T00:30:15.126+08:00",
"UpdatedAt": "2024-09-03T00:30:15.126+08:00",
"Offset": 26805,
"MinID": 26805,
"ExportMedia": true
},
{
"ID": 16,
"UUID": 1851814415,
"Username": "plmmyyds",
"Title": "妹子即正义😘",
"CreatedAt": "2024-09-12T10:18:39.159+08:00",
"UpdatedAt": "2024-09-12T10:18:39.159+08:00",
"Offset": 9519,
"MinID": 9519,
"ExportMedia": true
},
{
"ID": 13,
"UUID": 2023304596,
"Username": "",
"Title": "爆料瓜田",
"CreatedAt": "2024-09-02T23:34:26.949+08:00",
"UpdatedAt": "2024-09-02T23:34:26.949+08:00",
"Offset": 2874,
"MinID": 2874,
"ExportMedia": true
},
{
"ID": 6,
"UUID": 1604423588,
"Username": "meizitu3",
"Title": "朱颜别镜 | 妹子图 | 美女图",
"CreatedAt": "2024-09-02T19:16:04.812+08:00",
"UpdatedAt": "2024-09-02T19:16:04.812+08:00",
"Offset": 7177,
"MinID": 7177,
"ExportMedia": true
},
{
"ID": 12,
"UUID": 1341930464,
"Username": "woshadiao",
"Title": "每日沙雕墙",
"CreatedAt": "2024-09-02T23:18:11.31+08:00",
"UpdatedAt": "2024-09-02T23:18:11.31+08:00",
"Offset": 163088,
"MinID": 163088,
"ExportMedia": true
},
{
"ID": 7,
"UUID": 1320622866,
"Username": "DNSPODT",
"Title": "LoopDNS资讯播报",
"CreatedAt": "2024-09-02T19:34:57.663+08:00",
"UpdatedAt": "2024-09-02T19:34:57.663+08:00",
"Offset": 5507,
"MinID": 5507,
"ExportMedia": false
},
{
"ID": 2,
"UUID": 1762530683,
"Username": "yunpanshare",
"Title": "网盘资源收藏(夸克)",
"CreatedAt": "2024-09-02T18:33:38.418+08:00",
"UpdatedAt": "2024-09-02T18:33:38.418+08:00",
"Offset": 71495,
"MinID": 71495,
"ExportMedia": false
}
]

View File

@@ -0,0 +1,4 @@
import channels from "./channels"
import messages from "./messages"
export default { channels, messages }

View File

@@ -0,0 +1,122 @@
export default [
{
"ID": 197357,
"ChannelID": 2023304596,
"UUID": 2903,
"Content": "东湾 溜冰 千万不要别人的男朋友,你老公知道的下场有多么的恐怖 #打女人",
"Media": "[{\"msg_id\": 2903, \"asset_id\": 6282933363250565939, \"document\": {\"Ext\": \".mp4\", \"Size\": 6459613, \"Video\": {\"Width\": 720, \"Height\": 1280, \"Duration\": 21.134}, \"Filename\": \"IMG_5451.MP4\", \"MimeType\": \"video/mp4\"}}]",
"PublishedAt": "2024-09-20T18:01:06+08:00",
"CreatedAt": "2024-09-20T18:06:37.863+08:00",
"GroupID": 0,
"Published": false,
"Like": false
},
{
"ID": 197316,
"ChannelID": 2023304596,
"UUID": 2902,
"Content": "#斑斑园区 #校长 #枫哥 #辉哥 #火枪\n\n推监狱生涯人情冷暖 \n\n老板校长枫哥 ,辉哥,克拉克班班园区,刚开始出事的时候还说会管,工资也不发,饭也不管了,这边也出不去,也无法回到国内,微信支付宝都被司法冻结,工资不发就算了,现在我们只想吃一口饱饭,能够正常的活下去,没有别的需求.\n\n联系公司的领导消息都是已读不回过中秋节都是饿着肚子真的让人寒心这么大的老板几个人都管不起了吗格局就这么小每天都是饿着肚子睡觉上厕所都是半个月上一次上多了怕饿我们也不奢求什么了只求吃一口饱饭\n\n公司领导校长枫哥辉哥盘口主管火枪\n\nPS:吃不饱了 ?好可怜\n\n------------------------------------\n⚡ 查看: 凤凰娱乐已在爆料瓜田上押70000U保证金 🙏 点击\n\n群主担保 000999c.com 放心娱乐\n\n☎ 免费投稿爆料: @TT9533",
"Media": "[{\"photo\": \"6294249687703731203.jpg\", \"msg_id\": 2902, \"asset_id\": 6294249687703731203}]",
"PublishedAt": "2024-09-20T17:01:51+08:00",
"CreatedAt": "2024-09-20T17:03:11.268+08:00",
"GroupID": 0,
"Published": false,
"Like": false
},
{
"ID": 196959,
"ChannelID": 2023304596,
"UUID": 2901,
"Content": "#凤凰娱乐 ❗️ 巨额出款无忧❗️❗️\n东南亚最大线上博彩平台❤ ❤️❤️❤️❤️\n\n\n\n⚡ 查看: 凤凰娱乐已在爆料瓜田上押70000U保证金 🙏 点击\n\n凤凰娱乐大会员再创新高\n\n逆天之举4月3号会员本金2600赢走58万 🙏\n史无前例国内江苏某行业老板30天赢走568万 🙏\n赌神附体6月7号PG麻将大爆91万提款 🙏\n再创新高8月14号某盘口老板单笔提款163万 🙏\n怒杀狗庄8月15号某盘口管理怒提203万 🙏\n天降好运8月18号天降彩金2888爆赢78万 🙏\n\n❤全球性顶级博彩盘口支持USDT存出款、微信、支付宝、银行卡以及多种电子钱包存取款。东南亚国家地区通通不限ip  U存U取无须实名绑卡 \n\n\n巨额出款稳定 U存提款 每日提款不限额度800/1000万+随便提 欢迎各位大佬休闲娱乐❗️\n\n客服专员 @VIP360 ❤️\n代理专员 @Lafei (拉菲)\n注册网址 000999c.com",
"Media": "[{\"photo\": \"6210809064531805472.jpg\", \"msg_id\": 2901, \"asset_id\": 6210809064531805472}]",
"PublishedAt": "2024-09-20T16:29:06+08:00",
"CreatedAt": "2024-09-20T17:01:43.656+08:00",
"GroupID": 0,
"Published": false,
"Like": false
},
{
"ID": 196958,
"ChannelID": 2023304596,
"UUID": 2898,
"Content": "石家庄 正定当街 #抓小三",
"Media": "[{\"msg_id\": 2898, \"asset_id\": 6287331963812450638, \"document\": {\"Ext\": \".mp4\", \"Size\": 37600341, \"Video\": {\"Width\": 464, \"Height\": 848, \"Duration\": 99.354}, \"Filename\": \"IMG_5669.MP4\", \"MimeType\": \"video/mp4\"}}, {\"msg_id\": 2899, \"asset_id\": 6287331963812450639, \"document\": {\"Ext\": \".mp4\", \"Size\": 6529727, \"Video\": {\"Width\": 720, \"Height\": 1280, \"Duration\": 18.5}, \"Filename\": \"IMG_5670.MP4\", \"MimeType\": \"video/mp4\"}}, {\"msg_id\": 2900, \"asset_id\": 6287331963812450640, \"document\": {\"Ext\": \".mp4\", \"Size\": 2397718, \"Video\": {\"Width\": 720, \"Height\": 1280, \"Duration\": 6.234}, \"Filename\": \"IMG_5671.MP4\", \"MimeType\": \"video/mp4\"}}]",
"PublishedAt": "2024-09-20T16:01:06+08:00",
"CreatedAt": "2024-09-20T17:01:33.203+08:00",
"GroupID": 13814554131228860,
"Published": false,
"Like": false
},
{
"ID": 196957,
"ChannelID": 2023304596,
"UUID": 2897,
"Content": "🇰🇭柬埔寨旅游部长和监察部长互换职位\n\n9月20日上午柬埔寨国会召开特别会议批准柬埔寨旅游部长宋速甘和监察部长何哈互换职位即何哈出任旅游部长宋速甘出任监察部长。\n\n宋速甘图左出任旅游部长何哈图右出任监察部长\n\n此次会议由国会主席昆素达丽主持106名国会议员全部投票支持原旅游部长宋速甘改任监察部长、原监察部长何哈改任旅游部长。\n\n对此洪马耐表示部长职位的调整旨在更好地适应政府的需要确保各项工作的顺利进行。\n\n据了解宋速甘是已故副总理宋安之子其岳父是原工业部部长占比塞其哥哥宋卜提武是洪森女婿。\n\n何哈是洪森夫人洪文拉妮的侄女婿。",
"Media": "[{\"photo\": \"6298423987893551139.jpg\", \"msg_id\": 2897, \"asset_id\": 6298423987893551139}]",
"PublishedAt": "2024-09-20T15:33:06+08:00",
"CreatedAt": "2024-09-20T16:59:46.494+08:00",
"GroupID": 0,
"Published": false,
"Like": false
},
{
"ID": 196956,
"ChannelID": 2023304596,
"UUID": 2895,
"Content": "#群友投稿 \n\n#我要匿名投诉华泰11楼B区24号办公室。这逼阴险歹毒对上边领导殷勤奉承对下诋毁挤兑。通过各种蛇形走位鸭形走位爬到组长的位置管理一团糟。\n\n调戏已经有男朋友的泰国女同事和越南女同事搞得对方上班不自在。没办法他故意把组内的女同事调到自己身边完全无视人家有男朋友这个事实。对组里的同事说话直接就是能干就干不能干就滚。自己上班就是经常睡觉但是我们愣着电脑屏幕都不行。自己唯一拿得出手的活儿就是操纵别人搞对方心理有一点小问题就要搞你。我们管理平台运营本来就是要有点是非判断。\n\n现在只要他看不顺眼主播就是工资全扣白干走人。主播辛辛苦苦一个月一分工资拿不到或者就是十天半个月的扣工资。以前没有权利的时候就已经不是人了这权利到手还不张扬跋扈公泄私愤被打压的主播就在圈子里报平台黑料因为他一个人搞得我们的工作越来越难做。\n\n平时对我们各种施压一幅小人得志的嘴脸。平台让这样的人上位早晚要完蛋。这个人工作名字叫长生但是我们私下里都叫他CS畜生。\n\n------------------------------------\n⚡ 查看: 凤凰娱乐已在爆料瓜田上押70000U保证金 🙏 点击\n\n群主担保 000999c.com 放心娱乐\n\n☎ 免费投稿爆料: @TT9533",
"Media": "[{\"photo\": \"6291948676154768953.jpg\", \"msg_id\": 2895, \"asset_id\": 6291948676154768953}, {\"photo\": \"6291948676154768954.jpg\", \"msg_id\": 2896, \"asset_id\": 6291948676154768954}]",
"PublishedAt": "2024-09-20T15:02:07+08:00",
"CreatedAt": "2024-09-20T16:59:45.405+08:00",
"GroupID": 13814525816373596,
"Published": false,
"Like": false
},
{
"ID": 196955,
"ChannelID": 2023304596,
"UUID": 2892,
"Content": "【 湖南省 财政厅 厅长 刘文杰 意外 去世 】2024年9月19日上午湖南省财政厅党组书记、厅长刘文杰意外去世。此事或涉及刑事案件。9月19日下午包括刘文杰的同事在内的多位知情人士告知了记者这一消息。",
"Media": "[{\"photo\": \"6296265990165611162.jpg\", \"msg_id\": 2892, \"asset_id\": 6296265990165611162}, {\"photo\": \"6296265990165611163.jpg\", \"msg_id\": 2893, \"asset_id\": 6296265990165611163}, {\"photo\": \"6296265990165611164.jpg\", \"msg_id\": 2894, \"asset_id\": 6296265990165611164}]",
"PublishedAt": "2024-09-20T14:55:06+08:00",
"CreatedAt": "2024-09-20T16:59:44.085+08:00",
"GroupID": 13814522448560924,
"Published": false,
"Like": false
},
{
"ID": 196954,
"ChannelID": 2023304596,
"UUID": 2889,
"Content": "#每日眼力考试\n\n屌大的都看的到看到的评论区发个6不然都是小屌子\n\n#抖音 #快手 #直播 #露点 #网红",
"Media": "[{\"msg_id\": 2889, \"asset_id\": 4976560077386285696, \"document\": {\"Ext\": \".mp4\", \"Size\": 2732376, \"Video\": {\"Width\": 576, \"Height\": 1280, \"Duration\": 6}, \"Filename\": \"VID_20230702_172252_533.mp4\", \"MimeType\": \"video/mp4\"}}]",
"PublishedAt": "2024-09-20T14:29:06+08:00",
"CreatedAt": "2024-09-20T16:59:43.625+08:00",
"GroupID": 0,
"Published": false,
"Like": false
},
{
"ID": 196953,
"ChannelID": 2023304596,
"UUID": 2888,
"Content": "前两天从厦门驾驶快艇 #偷渡 到 #台湾 的哥们。",
"Media": "[{\"msg_id\": 2888, \"asset_id\": 6287331963812450416, \"document\": {\"Ext\": \".mp4\", \"Size\": 17103391, \"Video\": {\"Width\": 848, \"Height\": 496, \"Duration\": 82.802222222222}, \"Filename\": \"IMG_5556.MP4\", \"MimeType\": \"video/mp4\"}}]",
"PublishedAt": "2024-09-20T14:01:06+08:00",
"CreatedAt": "2024-09-20T16:59:40.273+08:00",
"GroupID": 0,
"Published": false,
"Like": false
},
{
"ID": 196952,
"ChannelID": 2023304596,
"UUID": 2887,
"Content": "#群友投稿 妈的昨晚上去珍珠收了一下尾款遭遇抢劫\n\n大意了坐了出租车想着也不远走到半路这狗司机不讲武德我来了先来了句你好我没搭理又给我来了句kuya我特么就想着不对劲了回了句yes妈的直接给我锁车门了拉到个贫民窟给我洗劫了二部手机六万p现金让我雨中凌乱我恳求他给我😞退我一部手机该说不说给了我一部手机和50p现金让我自己想办法离开那里.\n\n没办法了我特么走路3公里走到shore找了个地方手机充电找了个朋友帮忙一下所以以后各位多打出租车让这些出租车司机吃饱喝足只有这样了才会源源不断的抢劫最后提醒一下出租车的车牌NIK90136\n\n已经报警了说安排查出租车公司但是希望不大车能找回来人和钱肯定是找不到了\n\nPS白色出租车 \n\n------------------------------------\n⚡ 查看: 凤凰娱乐已在爆料瓜田上押70000U保证金 🙏 点击\n\n群主担保 000999c.com 放心娱乐\n\n☎ 免费投稿爆料: @TT9533",
"Media": "[{\"photo\": \"6003642163117802028.jpg\", \"msg_id\": 2887, \"asset_id\": 6003642163117802028}]",
"PublishedAt": "2024-09-20T13:02:06+08:00",
"CreatedAt": "2024-09-20T16:59:02.3+08:00",
"GroupID": 0,
"Published": false,
"Like": false
}
]

13
frontend/src/main.js Normal file
View File

@@ -0,0 +1,13 @@
import './assets/main.css'
import { createPinia } from 'pinia'
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
const app = createApp(App)
app.use(createPinia())
app.use(router)
app.mount('#app')

View File

@@ -1,25 +0,0 @@
import { createApp } from 'vue'
import App from './App.vue'
import './style.css'
import { createRouter, createWebHistory } from 'vue-router'
import ChannelMessages from './views/ChannelMessages.vue'
import FavoritesMessages from './views/FavoritesMessages.vue'
import Home from './views/Home.vue'
const routes = [
{ path: '/', component: Home, name: 'home' },
{ path: '/favorites', component: FavoritesMessages, name: 'favorites' },
// { path: '/channels', component: Channel, name: 'channels' },
{ path: '/channels/:channel/messages', component: ChannelMessages, name: 'channel-messages' },
]
const router = createRouter({
history: createWebHistory(),
routes,
})
createApp(App)
.use(router)
.mount('#app')

View File

@@ -0,0 +1,25 @@
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'home',
component: () => import('@/views/Home.vue'),
},
{
path: '/favorites',
name: 'favorites',
component: () => import('@/views/FavoriteMessages.vue'),
},
{
path: '/channels/:channel/messages',
name: 'channel-messages',
component: () => import('@/views/ChannelMessages.vue'),
},
]
})
export default router

View File

@@ -0,0 +1,15 @@
import { http } from './http';
export async function getChannels() {
// return mock('channels')
const resp = await http.get('/channels');
return resp.data;
}
export async function getChannel(channelId) {
// return mock('channels', (data) => data[0])
const resp = await http.get(`/channels/${channelId}`);
return resp.data;
}

View File

@@ -0,0 +1,22 @@
import fixtures from '@/fixtures/index.js';
import axios from 'axios';
const http = axios.create({
baseURL: '/api',
headers: {
"Access-Control-Allow-Origin": "*",
}
});
const mock = async (fixture, process) => {
let data = fixtures[fixture]
if (typeof process === 'function') {
data = process(data);
}
console.log('mock', fixture, data);
return data
}
export { http, mock };

View File

@@ -0,0 +1,38 @@
import { http } from './http';
function processResponseMessage(data) {
// let copyData = JSON.parse(JSON.stringify(data));
return data.map((item) => {
console.log(typeof item.Media)
let media = item.Media.replace(/"asset_id":\s(\d+)/g, (match, p1, p2, p3, offset, string) => {
return `"asset_id": "${p1}"`
})
item.Media = JSON.parse(media).filter((item) => {
return Object.keys(item).length > 0;
});
return item
});
}
export async function toggleFavorite(messageId) {
// return mock('messages', processResponseMessage)
const resp = await http.patch('/messages/' + messageId + '/favorite');
return resp.data;
}
export async function getChannelMessages(channelId, params) {
// return mock('messages', processResponseMessage)
const resp = await http.get(`/channels/${channelId}/messages`, { params });
return processResponseMessage(resp.data);
}
export async function getFavoriteMessages(params) {
// return mock('messages', processResponseMessage)
const resp = await http.get(`/favorites`, { params });
return processResponseMessage(resp.data);
}

View File

@@ -0,0 +1,12 @@
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
function increment() {
count.value++
}
return { count, doubleCount, increment }
})

View File

@@ -0,0 +1,17 @@
import { defineStore } from 'pinia';
import { ref } from 'vue';
export const useNavigationStore = defineStore('navigation', () => {
const links = ref([
{ name: "Home", href: "/", current: true },
{ name: "Favorites", href: "/favorites", current: false },
]);
function setActive(name) {
links.value.forEach(link => {
link.current = link.name === name;
});
}
return { links, setActive }
})

View File

@@ -1,37 +0,0 @@
export interface Channel {
UUID: number;
Username: string;
Title: string;
CreatedAt: Date;
UpdatedAt: Date;
Offset: number;
MinID: number;
ExportMedia: boolean;
}
export interface PostItem {
id: number;
content: string;
medias: Array<Media>;
}
export interface Media {
photo?: string;
msg_id: number;
asset_id: number;
document?: Document;
}
export interface Document {
Ext: string;
Size: number;
Video: Video;
Filename: string;
MimeType: string;
}
export interface Video {
Width: number;
Height: number;
Duration: number;
}

View File

@@ -1,37 +1,40 @@
<template>
<h1 class="mb-4 font-semibold text-xl">{{ channel.Title }}</h1>
<ListItem v-for="item in items" :key="item.id" :item="item" />
<h1 class="mb-4 font-semibold text-xl">{{ channel.Title }}</h1>
<div v-if="messages.length == 0">Empty...</div>
<template v-else>
<ListItem v-for="message in messages" :key="message.ID" :item="message" />
<div class="flex items-center justify-center">
<button class="px-4 py-2 hover:bg-slate-100 rounded border text-xl font-semibold w-full"
@click="loadMore">LoadMore</button>
</div>
</template>
</template>
<script setup>
import { useRoute } from "vue-router";
import ListItem from "@/components/ListItem.vue";
import { getChannel } from "@/services/channels";
import { getChannelMessages } from "@/services/messages";
import { onMounted, ref } from "vue";
import ListItem from "../components/ListItem.vue";
import axios from "axios";
import { useRoute } from "vue-router";
const route = useRoute();
const channel = ref({});
const items = ref([]);
const messages = ref([]);
onMounted(() => {
axios.get(`/channels/${route.params.channel}`).then((resp) => {
channel.value = resp.data;
});
const loadMore = async () => {
const items = await getChannelMessages(route.params.channel, { offset: messages.value[messages.value.length - 1].ID });
messages.value.push(...items);
}
axios.get(`/channels/${route.params.channel}/messages`).then((resp) => {
let data = resp.data;
data.map((item) => {
let media = item.Media.replace(/"asset_id":\s(\d+)/g, (match, p1, p2, p3, offset, string) => {
return `"asset_id": "${p1}"`
})
onMounted(async () => {
// get channel info
channel.value = await getChannel(route.params.channel);
console.log("channel", channel.value);
item.Media = JSON.parse(media).filter((item) => {
return Object.keys(item).length > 0;
});
console.log(item);
});
items.value = data;
});
// get channel messages
messages.value = await getChannelMessages(route.params.channel);
console.log("messages", messages.value);
});
</script>

View File

@@ -0,0 +1,31 @@
<template>
<h1 class="mb-4 font-semibold text-xl">Favorites</h1>
<div v-if="messages.length == 0">Empty...</div>
<template v-else>
<ListItem v-for="message in messages" :key="message.ID" :item="message" />
<div class="flex items-center justify-center">
<button class="px-4 py-2 hover:bg-slate-100 rounded border text-xl font-semibold w-full"
@click="loadMore">LoadMore</button>
</div>
</template>
</template>
<script setup>
import ListItem from "@/components/ListItem.vue";
import { getFavoriteMessages } from "@/services/messages";
import { onMounted, ref } from "vue";
const messages = ref([]);
const loadMore = async () => {
const items = await getChannelMessages(route.params.channel, { offset: messages.value[messages.value.length - 1].ID });
messages.value.push(...items);
}
onMounted(async () => {
messages.value = await getFavoriteMessages();
});
</script>

View File

@@ -1,29 +0,0 @@
<template>
<h1 class="mb-4 font-semibold text-xl">Favorites</h1>
<ListItem v-for="item in items" :key="item.id" :item="item" />
<div v-if="items.length==0">Empty...</div>
</template>
<script setup>
import { useRoute } from 'vue-router';
import { onMounted, ref } from 'vue';
import ListItem from '../components/ListItem.vue';
import axios from 'axios';
const route = useRoute();
const items = ref([])
onMounted(() => {
axios.get('/favorites').then(resp => {
let data = resp.data
data.map(item => {
item.Media = JSON.parse(item.Media).filter(item => {
return Object.keys(item).length > 0
})
})
items.value = data
})
})
</script>

View File

@@ -1,27 +1,21 @@
<template>
<h1>Home</h1>
<div class="grid grid-cols-2 gap-4">
<RouterLink
:to="`/channels/${item.UUID}/messages`"
v-for="item in items"
:key="item.UUID"
class="border p-4 hover:shadow-lg hover:bg-slate-100 bg-slate-100 rounded"
>
<h2 class="font-semibold text-lg">{{ item.Title }}</h2>
<span>{{ item.Username }}</span>
<RouterLink :to="`/channels/${item.UUID}/messages`" v-for="item in items" :key="item.UUID"
class="border p-4 hover:shadow-lg hover:bg-slate-100 bg-slate-100 rounded">
<h2 class="mb-2 font-semibold text-lg">{{ item.Title }}</h2>
<small class="text-gray-500">{{ item.Username }}</small>
</RouterLink>
</div>
</template>
<script setup lang="ts">
import { Channel } from "@/types";
<script setup>
import { getChannels } from "@/services/channels";
import { onMounted, ref } from "vue";
import axios from "axios";
const items = ref<Channel[]>([]);
const items = ref([]);
onMounted(() => {
axios.get('/channels').then((resp) => {
items.value = resp.data;
});
onMounted(async () => {
items.value = await getChannels();
});
</script>

View File

@@ -1,9 +0,0 @@
<template>
<h1>Tag: {{ route.params.tag }}</h1>
</template>
<script setup>
import { useRoute } from 'vue-router';
const route = useRoute();
</script>

View File

@@ -1,4 +0,0 @@
<template>
<h1>Tags</h1>
</template>

View File

@@ -1,13 +0,0 @@
/// <reference types="vite/client" />
declare module '*.vue' {
import { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}
// Remove the relative module declaration