refactor: remove standalone chat and log pages

This commit is contained in:
ngfchl
2026-07-07 14:26:50 +08:00
parent 290f5f0dbe
commit 09b4024a70
5 changed files with 340 additions and 28 deletions
+4 -26
View File
@@ -21,7 +21,7 @@ const lotteries = ref<LotteryItem[]>([]);
const lotteryParticipants = ref<Record<string, any>[]>([]); const lotteryParticipants = ref<Record<string, any>[]>([]);
const shopForm = reactive({ item_key: '', name: '', cost: 0, desc: '', enabled: true }); const shopForm = reactive({ item_key: '', name: '', cost: 0, desc: '', enabled: true });
const lotteryForm = reactive({ id: 0, title: '', prize: '', description: '', condition_type: 'all', condition_value: 0, draw_type: 'people', end_type: 'people', end_value: 3, max_participants: 3, draw_at: '', status: 'draft' }); const lotteryForm = reactive({ id: 0, title: '', prize: '', description: '', condition_type: 'all', condition_value: 0, draw_type: 'people', end_type: 'people', end_value: 3, max_participants: 3, draw_at: '', status: 'draft' });
const activePage = ref<'dashboard' | 'chat' | 'logs' | 'shop' | 'lottery' | 'bans'>('dashboard'); const activePage = ref<'dashboard' | 'shop' | 'lottery' | 'bans'>('dashboard');
const activeTab = reactive({ shop: 'items', lottery: 'events' }); const activeTab = reactive({ shop: 'items', lottery: 'events' });
const toast = ref(''); const toast = ref('');
const confirmState = reactive({ open: false, title: '', message: '', confirmText: '确认', danger: false, resolver: null as null | ((value: boolean) => void) }); const confirmState = reactive({ open: false, title: '', message: '', confirmText: '确认', danger: false, resolver: null as null | ((value: boolean) => void) });
@@ -42,10 +42,10 @@ const chatCount = computed(() => chatLogs.value.length);
const selectedQuery = computed(() => selectedChatId.value ? `?chat_id=${selectedChatId.value}` : ''); const selectedQuery = computed(() => selectedChatId.value ? `?chat_id=${selectedChatId.value}` : '');
const pageTitle = computed(() => ({ const pageTitle = computed(() => ({
dashboard: '群组安全总览', chat: '聊天记录', logs: '运行日志', shop: '商城管理', lottery: '抽奖管理', bans: '封禁记录' dashboard: '群组安全总览', shop: '商城管理', lottery: '抽奖管理', bans: '封禁记录'
}[activePage.value])); }[activePage.value]));
const pageDesc = computed(() => ({ const pageDesc = computed(() => ({
dashboard: '实时审计聊天、封禁、日志和运营活动', chat: '群消息实时审计与手动处置', logs: '服务运行状态与错误排查', shop: '商品 CRUD 与兑换交易记录', lottery: '抽奖项目、开奖条件与参与记录', bans: '封禁审计与手动解封' dashboard: '实时审计聊天、封禁、日志和运营活动', shop: '商品 CRUD 与兑换交易记录', lottery: '抽奖项目、开奖条件与参与记录', bans: '封禁审计与手动解封'
}[activePage.value])); }[activePage.value]));
function goPage(page: typeof activePage.value) { function goPage(page: typeof activePage.value) {
activePage.value = page; activePage.value = page;
@@ -383,8 +383,6 @@ onBeforeUnmount(() => {
</div> </div>
<nav class="side-nav"> <nav class="side-nav">
<button :class="{active: activePage === 'dashboard'}" @click="goPage('dashboard')"><span>总览</span><em>Dashboard</em></button> <button :class="{active: activePage === 'dashboard'}" @click="goPage('dashboard')"><span>总览</span><em>Dashboard</em></button>
<button :class="{active: activePage === 'chat'}" @click="goPage('chat')"><span>聊天审计</span><em>Messages</em></button>
<button :class="{active: activePage === 'logs'}" @click="goPage('logs')"><span>运行日志</span><em>Logs</em></button>
<button :class="{active: activePage === 'shop'}" @click="goPage('shop')"><span>商城管理</span><em>Shop</em></button> <button :class="{active: activePage === 'shop'}" @click="goPage('shop')"><span>商城管理</span><em>Shop</em></button>
<button :class="{active: activePage === 'lottery'}" @click="goPage('lottery')"><span>抽奖管理</span><em>Lottery</em></button> <button :class="{active: activePage === 'lottery'}" @click="goPage('lottery')"><span>抽奖管理</span><em>Lottery</em></button>
<button :class="{active: activePage === 'bans'}" @click="goPage('bans')"><span>封禁记录</span><em>Bans</em></button> <button :class="{active: activePage === 'bans'}" @click="goPage('bans')"><span>封禁记录</span><em>Bans</em></button>
@@ -469,26 +467,6 @@ onBeforeUnmount(() => {
</SCard> </SCard>
</section> </section>
<section v-show="activePage === 'chat'" class="tab-page chat-page">
<SCard class="panel chat-panel page-panel" title="聊天记录" :description="`${chatCount} 条`">
<div class="scroll-area chat-scroll">
<div v-if="!chatLogs.length" class="empty">等待消息...</div>
<article v-for="(item, i) in chatLogs" :key="item.id || i" :class="chatItemClass(item)">
<div class="chat-head"><div class="chat-user-line"><STag size="xs" :color="tagColor(item)" variant="soft">{{ chatStatusText(item) }}</STag><span class="chat-user">@{{ item.username || item.first_name || 'unknown' }}</span><span class="chat-uid">{{ item.user_id }}</span></div><div class="chat-meta-line"><span class="chat-id">#{{ item.msg_id || '-' }}</span><span class="chat-time">{{ formatTime(item.time) }}</span></div></div>
<div class="chat-text">{{ item.text || '(空消息)' }}</div>
<div class="chat-foot"><STag v-if="item.spam" class="reason-tag" size="xs" color="destructive" variant="soft" :title="item.reason || item.spam_reason">{{ item.reason || item.spam_reason }}</STag><span v-else /><div class="chat-actions"><STag v-if="item.deleted" size="xs" color="secondary" variant="soft">已删 · {{ formatTime(item.deleted_at) }}</STag><template v-else><SButton size="xs" variant="link" color="destructive" :disabled="item._deleting" @click="deleteChatMessage(item, false)">删除</SButton><SButton v-if="item.user_id" size="xs" variant="link" color="warning" :disabled="item._deleting" @click="deleteChatMessage(item, true)">删封</SButton></template></div></div>
</article>
</div>
</SCard>
</section>
<section v-show="activePage === 'logs'" class="tab-page log-page">
<SCard class="panel log-panel page-panel" title="运行日志" :description="`${logs.length} 条`">
<template #extra><div class="panel-extra"><SButton size="xs" variant="outline" @click="copyLogs">复制</SButton><SButton size="xs" variant="outline" @click="loadLogs">刷新</SButton></div></template>
<div ref="logScroll" class="log-window"><div v-if="!logs.length" class="empty dark">等待日志...</div><div v-for="(item, i) in logs" :key="i" class="log-line" :class="item.level" :title="compactLog(item.message)"><span class="log-time">{{ formatTime(item.time) }}</span><span class="log-level">{{ item.level }}</span><span class="log-msg">{{ compactLog(item.message) }}</span></div></div>
</SCard>
</section>
<section v-show="activePage === 'shop'" class="tab-page manager-layout"> <section v-show="activePage === 'shop'" class="tab-page manager-layout">
<aside class="editor-panel"><div class="section-title"><span>商品编辑</span><small>{{ shopForm.item_key ? '编辑中' : '新建' }}</small></div><label>商品编号<SInput v-model="shopForm.item_key" placeholder="如 1 / vip_card" /></label><label>商品名称<SInput v-model="shopForm.name" placeholder="补签卡" /></label><label>积分价格<SInput v-model="shopForm.cost" placeholder="50" /></label><label>说明<SInput v-model="shopForm.desc" placeholder="展示在 /shop 内" /></label><label class="switch-line"><input v-model="shopForm.enabled" type="checkbox" /> 上架销售</label><div class="editor-actions"><SButton size="xs" color="primary" @click="saveShopItem">保存商品</SButton><SButton size="xs" variant="outline" @click="Object.assign(shopForm, { item_key: '', name: '', cost: 0, desc: '', enabled: true })">新建</SButton></div></aside> <aside class="editor-panel"><div class="section-title"><span>商品编辑</span><small>{{ shopForm.item_key ? '编辑中' : '新建' }}</small></div><label>商品编号<SInput v-model="shopForm.item_key" placeholder="如 1 / vip_card" /></label><label>商品名称<SInput v-model="shopForm.name" placeholder="补签卡" /></label><label>积分价格<SInput v-model="shopForm.cost" placeholder="50" /></label><label>说明<SInput v-model="shopForm.desc" placeholder="展示在 /shop 内" /></label><label class="switch-line"><input v-model="shopForm.enabled" type="checkbox" /> 上架销售</label><div class="editor-actions"><SButton size="xs" color="primary" @click="saveShopItem">保存商品</SButton><SButton size="xs" variant="outline" @click="Object.assign(shopForm, { item_key: '', name: '', cost: 0, desc: '', enabled: true })">新建</SButton></div></aside>
<section class="data-panel"><div class="tabbar"><button :class="{active: activeTab.shop === 'items'}" @click="activeTab.shop='items'">商品列表</button><button :class="{active: activeTab.shop === 'tx'}" @click="activeTab.shop='tx'; loadShopTransactions()">交易记录</button></div><div v-if="activeTab.shop === 'items'" class="data-table"><div class="table-head"><span>编号/商品</span><span>价格</span><span>状态</span><span>操作</span></div><div v-for="item in shopItems" :key="item.item_key" class="table-row"><span><b>{{ item.item_key }}</b><em>{{ item.name }}</em><small>{{ item.desc || '无说明' }}</small></span><span>{{ item.cost }} 分</span><span><STag size="xs" :color="item.enabled ? 'success' : 'secondary'" variant="soft">{{ item.enabled ? '上架' : '下架' }}</STag></span><span class="row-actions"><SButton size="xs" variant="outline" @click="editShopItem(item)">编辑</SButton><SButton size="xs" variant="soft" color="destructive" @click="deleteShopItem(item)">删除</SButton></span></div></div><div v-else class="data-table tx-table"><div class="table-head"><span>用户</span><span>商品</span><span>积分</span><span>时间</span></div><div v-for="tx in shopTransactions" :key="tx.id" class="table-row"><span>@{{ tx.username || tx.user_id }}</span><span>{{ tx.item_name }}</span><span>{{ tx.cost }} </span><span>{{ formatTime(tx.created_at) }}</span></div></div></section> <section class="data-panel"><div class="tabbar"><button :class="{active: activeTab.shop === 'items'}" @click="activeTab.shop='items'">商品列表</button><button :class="{active: activeTab.shop === 'tx'}" @click="activeTab.shop='tx'; loadShopTransactions()">交易记录</button></div><div v-if="activeTab.shop === 'items'" class="data-table"><div class="table-head"><span>编号/商品</span><span>价格</span><span>状态</span><span>操作</span></div><div v-for="item in shopItems" :key="item.item_key" class="table-row"><span><b>{{ item.item_key }}</b><em>{{ item.name }}</em><small>{{ item.desc || '无说明' }}</small></span><span>{{ item.cost }} 分</span><span><STag size="xs" :color="item.enabled ? 'success' : 'secondary'" variant="soft">{{ item.enabled ? '上架' : '下架' }}</STag></span><span class="row-actions"><SButton size="xs" variant="outline" @click="editShopItem(item)">编辑</SButton><SButton size="xs" variant="soft" color="destructive" @click="deleteShopItem(item)">删除</SButton></span></div></div><div v-else class="data-table tx-table"><div class="table-head"><span>用户</span><span>商品</span><span>积分</span><span>时间</span></div><div v-for="tx in shopTransactions" :key="tx.id" class="table-row"><span>@{{ tx.username || tx.user_id }}</span><span>{{ tx.item_name }}</span><span>{{ tx.cost }} </span><span>{{ formatTime(tx.created_at) }}</span></div></div></section>
@@ -503,7 +481,7 @@ onBeforeUnmount(() => {
<SCard class="ban-panel page-panel" title="封禁记录" :description="`${bans.length} 条`"><template #extra><SButton size="xs" variant="outline" @click="loadBans">刷新</SButton></template><div class="ban-list"><div v-if="!bans.length" class="empty">暂无封禁记录</div><div v-for="(item, i) in bans" :key="item.id || i" class="ban-item"><div class="ban-main"><b>@{{ item.username || item.first_name || item.user_id }}</b><span>{{ item.reason || '-' }}</span></div><div class="ban-meta"><span>{{ formatTime(item.time) }}</span><SButton v-if="item.auto_unban" size="xs" variant="outline" color="success" :disabled="item._loading" @click="unban(item)">解封</SButton><STag v-else size="xs" color="secondary" variant="soft">已处理</STag></div></div></div></SCard> <SCard class="ban-panel page-panel" title="封禁记录" :description="`${bans.length} 条`"><template #extra><SButton size="xs" variant="outline" @click="loadBans">刷新</SButton></template><div class="ban-list"><div v-if="!bans.length" class="empty">暂无封禁记录</div><div v-for="(item, i) in bans" :key="item.id || i" class="ban-item"><div class="ban-main"><b>@{{ item.username || item.first_name || item.user_id }}</b><span>{{ item.reason || '-' }}</span></div><div class="ban-meta"><span>{{ formatTime(item.time) }}</span><SButton v-if="item.auto_unban" size="xs" variant="outline" color="success" :disabled="item._loading" @click="unban(item)">解封</SButton><STag v-else size="xs" color="secondary" variant="soft">已处理</STag></div></div></div></SCard>
</section> </section>
<form v-if="activePage === 'chat'" class="send-box floating-chat-send" @submit.prevent="sendMessage"> <form v-if="activePage === 'dashboard'" class="send-box floating-chat-send" @submit.prevent="sendMessage">
<SInput v-model="newMessage" placeholder="发送消息到 TG 群" /> <SInput v-model="newMessage" placeholder="发送消息到 TG 群" />
<SButton type="submit" color="primary" :disabled="sending || !newMessage.trim()">发送</SButton> <SButton type="submit" color="primary" :disabled="sending || !newMessage.trim()">发送</SButton>
</form> </form>
+5
View File
@@ -214,3 +214,8 @@ body { background: #f3f6fb; }
.admin-shell .send-box:not(.floating-chat-send) { position: static; } .admin-shell .send-box:not(.floating-chat-send) { position: static; }
@media (max-width: 1100px) { .admin-shell .floating-chat-send { left: 16px; right: 16px; } .tab-page { min-height: auto; } } @media (max-width: 1100px) { .admin-shell .floating-chat-send { left: 16px; right: 16px; } .tab-page { min-height: auto; } }
@media (max-width: 520px) { .admin-shell .floating-chat-send { left: 8px; right: 8px; bottom: 8px; } .chat-page .chat-panel { height: calc(100vh - 270px); min-height: 420px; } } @media (max-width: 520px) { .admin-shell .floating-chat-send { left: 8px; right: 8px; bottom: 8px; } .chat-page .chat-panel { height: calc(100vh - 270px); min-height: 420px; } }
/* Remove standalone chat/log pages: composer belongs to dashboard */
.admin-shell .floating-chat-send { left: 284px; right: 20px; bottom: 18px; }
.main-grid { padding-bottom: 72px; }
@media (max-width: 1100px) { .admin-shell .floating-chat-send { left: 16px; right: 16px; } }
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -5,8 +5,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" type="image/svg+xml" href="/static/icon.svg" /> <link rel="icon" type="image/svg+xml" href="/static/icon.svg" />
<title>TG Spam Guard</title> <title>TG Spam Guard</title>
<script type="module" crossorigin src="/static/assets/index-BLyluaUp.js"></script> <script type="module" crossorigin src="/static/assets/index-D_B5Zn2J.js"></script>
<link rel="stylesheet" crossorigin href="/static/assets/index-DEuQ3oiJ.css"> <link rel="stylesheet" crossorigin href="/static/assets/index-Djy9tMxw.css">
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>