feat: add frontend
This commit is contained in:
Binary file not shown.
31
frontend/.editorconfig
Normal file
31
frontend/.editorconfig
Normal file
@@ -0,0 +1,31 @@
|
||||
# EditorConfig is awesome: https://editorconfig.org
|
||||
|
||||
# top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
# Unix-style newlines with a newline ending every file
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
|
||||
# Matches multiple files with brace expansion notation
|
||||
# Set default charset
|
||||
[*.{js,vue,json}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
|
||||
# 4 space indentation
|
||||
[*.py]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
# Tab indentation (no size specified)
|
||||
[Makefile]
|
||||
indent_style = tab
|
||||
|
||||
# Matches the exact files either package.json or .travis.yml
|
||||
[{package.json,.travis.yml}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
@@ -1,10 +0,0 @@
|
||||
gen:
|
||||
model:
|
||||
dsn: __DSN
|
||||
source: postgres
|
||||
schema: public
|
||||
path: ./database
|
||||
ignores: [] # ignore tables
|
||||
types:
|
||||
kube_pods: # table name
|
||||
labels: functl/pkg/pg.JsonMap # column type
|
||||
8
frontend/.gitignore
vendored
8
frontend/.gitignore
vendored
@@ -8,17 +8,23 @@ pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
coverage
|
||||
*.local
|
||||
|
||||
/cypress/videos/
|
||||
/cypress/screenshots/
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
*.tsbuildinfo
|
||||
|
||||
3
frontend/.vscode/extensions.json
vendored
Normal file
3
frontend/.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"recommendations": ["Vue.volar"]
|
||||
}
|
||||
@@ -1,5 +1,29 @@
|
||||
# Vue 3 + TypeScript + Vite
|
||||
# b
|
||||
|
||||
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
||||
This template should help get you started developing with Vue 3 in Vite.
|
||||
|
||||
Learn more about the recommended Project Setup and IDE Support in the [Vue Docs TypeScript Guide](https://vuejs.org/guide/typescript/overview.html#project-setup).
|
||||
## Recommended IDE Setup
|
||||
|
||||
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
|
||||
|
||||
## Customize configuration
|
||||
|
||||
See [Vite Configuration Reference](https://vitejs.dev/config/).
|
||||
|
||||
## Project Setup
|
||||
|
||||
```sh
|
||||
npm install
|
||||
```
|
||||
|
||||
### Compile and Hot-Reload for Development
|
||||
|
||||
```sh
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Compile and Minify for Production
|
||||
|
||||
```sh
|
||||
npm run build
|
||||
```
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
<!doctype html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>TGShow</title>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>TG Show</title>
|
||||
</head>
|
||||
<body class="bg-slate-300">
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
8
frontend/jsconfig.json
Normal file
8
frontend/jsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
772
frontend/package-lock.json
generated
772
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,30 +1,26 @@
|
||||
{
|
||||
"name": "tg-show",
|
||||
"private": true,
|
||||
"name": "b",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --host 0.0.0.0",
|
||||
"build": "vue-tsc -b && vite build",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@headlessui/vue": "^1.7.23",
|
||||
"@heroicons/vue": "^2.1.5",
|
||||
"axios": "^1.7.7",
|
||||
"vue": "^3.4.37",
|
||||
"vue-router": "^4.4.5"
|
||||
"pinia": "^2.1.7",
|
||||
"vue": "^3.4.29",
|
||||
"vue-router": "^4.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.1.2",
|
||||
"@vitejs/plugin-vue": "^5.0.5",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"nodemon": "^3.1.6",
|
||||
"postcss": "^8.4.47",
|
||||
"tailwindcss": "^3.4.12",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"typescript": "^5.5.3",
|
||||
"vite": "^5.4.1",
|
||||
"vue-tsc": "^2.0.29"
|
||||
"vite": "^5.3.1"
|
||||
}
|
||||
}
|
||||
|
||||
BIN
frontend/public/favicon.ico
Normal file
BIN
frontend/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 1.5 KiB |
3
frontend/src/.gitignore
vendored
3
frontend/src/.gitignore
vendored
@@ -1,3 +0,0 @@
|
||||
|
||||
*.js
|
||||
*.vue.js
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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
|
||||
<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="item.current ? 'page' : undefined"
|
||||
>{{ item.name }}</RouterLink
|
||||
>
|
||||
]" :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>
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
200
frontend/src/fixtures/channels.js
Normal file
200
frontend/src/fixtures/channels.js
Normal 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": "ahhhhfs|A姐分享",
|
||||
"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
|
||||
}
|
||||
]
|
||||
4
frontend/src/fixtures/index.js
Normal file
4
frontend/src/fixtures/index.js
Normal file
@@ -0,0 +1,4 @@
|
||||
import channels from "./channels"
|
||||
import messages from "./messages"
|
||||
|
||||
export default { channels, messages }
|
||||
122
frontend/src/fixtures/messages.js
Normal file
122
frontend/src/fixtures/messages.js
Normal 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
13
frontend/src/main.js
Normal 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')
|
||||
@@ -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')
|
||||
25
frontend/src/router/index.js
Normal file
25
frontend/src/router/index.js
Normal 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
|
||||
15
frontend/src/services/channels.js
Normal file
15
frontend/src/services/channels.js
Normal 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;
|
||||
}
|
||||
22
frontend/src/services/http.js
Normal file
22
frontend/src/services/http.js
Normal 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 };
|
||||
|
||||
38
frontend/src/services/messages.js
Normal file
38
frontend/src/services/messages.js
Normal 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);
|
||||
}
|
||||
12
frontend/src/stores/counter.js
Normal file
12
frontend/src/stores/counter.js
Normal 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 }
|
||||
})
|
||||
17
frontend/src/stores/navigation.js
Normal file
17
frontend/src/stores/navigation.js
Normal 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 }
|
||||
})
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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" />
|
||||
<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>
|
||||
|
||||
31
frontend/src/views/FavoriteMessages.vue
Normal file
31
frontend/src/views/FavoriteMessages.vue
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
|
||||
<template>
|
||||
<h1>Tag: {{ route.params.tag }}</h1>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useRoute } from 'vue-router';
|
||||
const route = useRoute();
|
||||
</script>
|
||||
@@ -1,4 +0,0 @@
|
||||
|
||||
<template>
|
||||
<h1>Tags</h1>
|
||||
</template>
|
||||
13
frontend/src/vite-env.d.ts
vendored
13
frontend/src/vite-env.d.ts
vendored
@@ -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
|
||||
@@ -1,24 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "preserve",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": "./",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./src/*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"src/**/*.d.ts",
|
||||
"src/**/*.tsx",
|
||||
"src/**/*.vue"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2023"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
{"root":["./src/data.ts","./src/main.ts","./src/types.ts","./src/vite-env.d.ts","./src/App.vue","./src/components/ListItem.vue","./src/components/MediaDocument.vue","./src/components/MediaGrid.vue","./src/components/MediaItem.vue","./src/components/MediaPhoto.vue","./src/components/Navigation.vue","./src/views/ChannelMessages.vue","./src/views/FavoritesMessages.vue","./src/views/Home.vue","./src/views/Tag.vue","./src/views/Tags.vue"],"version":"5.6.2"}
|
||||
@@ -1,19 +1,19 @@
|
||||
import { fileURLToPath, URL } from 'node:url'
|
||||
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import { defineConfig } from 'vite'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
optimizeDeps: {
|
||||
include: ['linked-dep'],
|
||||
plugins: [
|
||||
vue(),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||
}
|
||||
},
|
||||
build: {
|
||||
commonjsOptions: {
|
||||
include: [],
|
||||
},
|
||||
},
|
||||
plugins: [vue()],
|
||||
server: {
|
||||
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: "http://127.0.0.1:3000/", //目标域名
|
||||
@@ -2,35 +2,21 @@ package internal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"exporter/database/telegram_resource/public/model"
|
||||
"exporter/database/telegram_resource/public/table"
|
||||
"exporter/frontend/dist"
|
||||
|
||||
. "github.com/go-jet/jet/v2/postgres"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v2/middleware/favicon"
|
||||
"github.com/gofiber/fiber/v2/middleware/filesystem"
|
||||
"github.com/gofiber/fiber/v2/middleware/recover"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type Pagination struct {
|
||||
Page int64 `json:"page"`
|
||||
Limit int64 `json:"limit"`
|
||||
Offset int64 `json:"-"`
|
||||
}
|
||||
|
||||
func (p *Pagination) Format() {
|
||||
if p.Limit == 0 {
|
||||
p.Limit = 10
|
||||
}
|
||||
|
||||
if p.Page == 0 {
|
||||
p.Offset = 0
|
||||
} else {
|
||||
p.Offset = (p.Page - 1) * p.Limit
|
||||
}
|
||||
}
|
||||
|
||||
func ServeCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "serve",
|
||||
@@ -54,11 +40,24 @@ func serveCmd(cmd *cobra.Command, args []string) error {
|
||||
app := fiber.New()
|
||||
|
||||
app.Static("/medias", "/share/telegram/outputs")
|
||||
app.Static("/", "/public")
|
||||
|
||||
app.Use(favicon.New(favicon.Config{
|
||||
Data: dist.Favicon,
|
||||
}))
|
||||
|
||||
app.Use("/assets", filesystem.New(filesystem.Config{
|
||||
Root: http.FS(dist.StaticDist),
|
||||
PathPrefix: "assets",
|
||||
}))
|
||||
|
||||
// Initialize default config
|
||||
app.Use(recover.New())
|
||||
|
||||
app.Get("/", func(c *fiber.Ctx) error {
|
||||
c.Context().SetContentType("text/html")
|
||||
return c.SendString(dist.IndexPage)
|
||||
})
|
||||
|
||||
group := app.Group("/api")
|
||||
|
||||
// get channel list
|
||||
@@ -101,21 +100,20 @@ func serveCmd(cmd *cobra.Command, args []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
var p Pagination
|
||||
if err := c.QueryParser(&p); err != nil {
|
||||
return err
|
||||
}
|
||||
p.Format()
|
||||
offsetPk := c.QueryInt("offset", 0)
|
||||
|
||||
var messages []model.ChannelMessages
|
||||
|
||||
tbl := table.ChannelMessages
|
||||
cond := tbl.ChannelID.EQ(Int64(int64(channelID)))
|
||||
if offsetPk > 0 {
|
||||
cond = cond.AND(tbl.ID.LT(Int64(int64(offsetPk))))
|
||||
}
|
||||
err = tbl.
|
||||
SELECT(tbl.AllColumns).
|
||||
WHERE(tbl.ChannelID.EQ(Int64(int64(channelID)))).
|
||||
WHERE(cond).
|
||||
LIMIT(5).
|
||||
ORDER_BY(tbl.ID.DESC()).
|
||||
LIMIT(p.Limit).
|
||||
OFFSET(p.Offset).
|
||||
QueryContext(c.Context(), db, &messages)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -126,25 +124,24 @@ func serveCmd(cmd *cobra.Command, args []string) error {
|
||||
|
||||
// favorite messages
|
||||
group.Get("/favorites", func(c *fiber.Ctx) error {
|
||||
var p Pagination
|
||||
if err := c.QueryParser(&p); err != nil {
|
||||
return err
|
||||
offsetPk := c.QueryInt("offset", 0)
|
||||
|
||||
tbl := table.ChannelMessages
|
||||
cond := tbl.ChannelID.EQ(Int64(int64(channelID))).AND(
|
||||
tbl.Like.EQ(Bool(true)),
|
||||
)
|
||||
|
||||
if offsetPk > 0 {
|
||||
cond = cond.AND(tbl.ID.LT(Int64(int64(offsetPk))))
|
||||
}
|
||||
p.Format()
|
||||
|
||||
var messages []model.ChannelMessages
|
||||
|
||||
tbl := table.ChannelMessages
|
||||
err = tbl.
|
||||
SELECT(tbl.AllColumns).
|
||||
WHERE(
|
||||
tbl.ChannelID.EQ(Int64(int64(channelID))).AND(
|
||||
tbl.Like.EQ(Bool(true)),
|
||||
),
|
||||
).
|
||||
WHERE(cond).
|
||||
LIMIT(5).
|
||||
ORDER_BY(tbl.ID.DESC()).
|
||||
LIMIT(p.Limit).
|
||||
OFFSET(p.Offset).
|
||||
QueryContext(c.Context(), db, &messages)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
Reference in New Issue
Block a user