频道筛选:text_message 表增加 (channel_id, created_at) 复合索引,主页增加频道筛选输入框,服务端筛选+防抖重载,版本号 backend 1.1.0 / frontend 0.2.0

This commit is contained in:
2026-08-07 19:10:49 +08:00
parent 1c9f94dcf8
commit d919ea2c53
4 changed files with 41 additions and 9 deletions
+3 -3
View File
@@ -439,7 +439,7 @@ type TextMessageRecord struct {
Text *string `gorm:"column:text"` Text *string `gorm:"column:text"`
PayloadHex *string `gorm:"column:payload_hex"` PayloadHex *string `gorm:"column:payload_hex"`
Topic string `gorm:"column:topic;not null"` Topic string `gorm:"column:topic;not null"`
ChannelID *string `gorm:"column:channel_id"` ChannelID *string `gorm:"column:channel_id;index:idx_text_message_channel_id_created_at,priority:1"`
GatewayID *string `gorm:"column:gateway_id"` GatewayID *string `gorm:"column:gateway_id"`
PacketID *int64 `gorm:"column:packet_id;index:idx_text_message_packet_id"` PacketID *int64 `gorm:"column:packet_id;index:idx_text_message_packet_id"`
PacketTo *string `gorm:"column:packet_to"` PacketTo *string `gorm:"column:packet_to"`
@@ -458,7 +458,7 @@ type TextMessageRecord struct {
MQTTRemoteHost *string `gorm:"column:mqtt_remote_host"` MQTTRemoteHost *string `gorm:"column:mqtt_remote_host"`
MQTTRemotePort *string `gorm:"column:mqtt_remote_port"` MQTTRemotePort *string `gorm:"column:mqtt_remote_port"`
ContentJSON string `gorm:"column:content_json;not null"` ContentJSON string `gorm:"column:content_json;not null"`
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;index:idx_text_message_from_num_created_at,priority:2;index:idx_text_message_created_at"` CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;index:idx_text_message_from_num_created_at,priority:2;index:idx_text_message_created_at;index:idx_text_message_channel_id_created_at,priority:2"`
} }
func (TextMessageRecord) TableName() string { func (TextMessageRecord) TableName() string {
@@ -707,7 +707,7 @@ func (s *Store) migrate() error {
model any model any
indexes []string indexes []string
}{ }{
{label: "text_message", model: &TextMessageRecord{}, indexes: []string{"idx_text_message_from_num_created_at", "idx_text_message_created_at", "idx_text_message_packet_id"}}, {label: "text_message", model: &TextMessageRecord{}, indexes: []string{"idx_text_message_from_num_created_at", "idx_text_message_created_at", "idx_text_message_packet_id", "idx_text_message_channel_id_created_at"}},
{label: "bot_direct_messages", model: &BotDirectMessageRecord{}, indexes: []string{"idx_bot_dm_bot_peer", "idx_bot_dm_bot_created_at"}}, {label: "bot_direct_messages", model: &BotDirectMessageRecord{}, indexes: []string{"idx_bot_dm_bot_peer", "idx_bot_dm_bot_created_at"}},
{label: "llm_message_queue", model: &LLMMessageQueueRecord{}, indexes: []string{"idx_llm_queue_bot_created"}}, {label: "llm_message_queue", model: &LLMMessageQueueRecord{}, indexes: []string{"idx_llm_queue_bot_created"}},
} { } {
+1 -1
View File
@@ -84,7 +84,7 @@ func NewRouter(cfg configpkg.WebConfig, consoleLog bool, store *storepkg.Store,
return r return r
} }
const BackendVersion = "1.0.0" const BackendVersion = "1.1.0"
var CommitVersion = "dev" var CommitVersion = "dev"
+36 -4
View File
@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, ref } from 'vue' import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { adminLogout, createNodeBlockingRule, deleteNode, deleteTextMessage, getAdminMe, getHealth, getMapReportViewport, getNodeInfo, getPositions, getTextMessages, purgeNode } from './api' import { adminLogout, createNodeBlockingRule, deleteNode, deleteTextMessage, getAdminMe, getHealth, getMapReportViewport, getNodeInfo, getPositions, getTextMessages, purgeNode } from './api'
import AdminBlockingManagement from './components/AdminBlockingManagement.vue' import AdminBlockingManagement from './components/AdminBlockingManagement.vue'
import AdminBot from './components/AdminBot.vue' import AdminBot from './components/AdminBot.vue'
@@ -69,6 +69,7 @@ const mapReportTotal = ref(0)
const mapSources = ref<PublicMapTileSource[]>([fallbackMapSource]) const mapSources = ref<PublicMapTileSource[]>([fallbackMapSource])
const mapSource = ref<PublicMapTileSource>(fallbackMapSource) const mapSource = ref<PublicMapTileSource>(fallbackMapSource)
const nodeFilter = ref('') const nodeFilter = ref('')
const channelFilter = ref('')
const pendingDeleteAction = ref<PendingDeleteAction | null>(null) const pendingDeleteAction = ref<PendingDeleteAction | null>(null)
type DeletableTextMessage = TextMessage & { mergedCount?: number; mergedMessages?: TextMessage[] } type DeletableTextMessage = TextMessage & { mergedCount?: number; mergedMessages?: TextMessage[] }
type NodeActionRequest = { nodeId: string; nodeNum: number | null; message?: DeletableTextMessage } type NodeActionRequest = { nodeId: string; nodeNum: number | null; message?: DeletableTextMessage }
@@ -95,6 +96,7 @@ const nodesById = computed<NodeInfoById>(() => {
}) })
const normalizedNodeFilter = computed(() => nodeFilter.value.trim().toLowerCase()) const normalizedNodeFilter = computed(() => nodeFilter.value.trim().toLowerCase())
const normalizedChannelFilter = computed(() => channelFilter.value.trim())
function nodeMatchesFilterByInfo(node: NodeInfo | null | undefined, keyword: string): boolean { function nodeMatchesFilterByInfo(node: NodeInfo | null | undefined, keyword: string): boolean {
if (!keyword) { if (!keyword) {
@@ -298,7 +300,7 @@ function clearSelectedNode() {
} }
async function loadInitialChatMessages() { async function loadInitialChatMessages() {
const response = await getTextMessages(chatPageSize, 0) const response = await getTextMessages(chatPageSize, 0, normalizedChannelFilter.value ? { channelId: normalizedChannelFilter.value } : '')
messages.value = toChronological(response.items) messages.value = toChronological(response.items)
chatHasMore.value = response.items.length === chatPageSize chatHasMore.value = response.items.length === chatPageSize
chatInitialized.value = true chatInitialized.value = true
@@ -311,7 +313,7 @@ async function loadOlderMessages() {
chatLoadingOlder.value = true chatLoadingOlder.value = true
try { try {
const response = await getTextMessages(chatPageSize, messages.value.length) const response = await getTextMessages(chatPageSize, messages.value.length, normalizedChannelFilter.value ? { channelId: normalizedChannelFilter.value } : '')
messages.value = mergeMessages(messages.value, toChronological(response.items)) messages.value = mergeMessages(messages.value, toChronological(response.items))
chatHasMore.value = response.items.length === chatPageSize chatHasMore.value = response.items.length === chatPageSize
} catch (err) { } catch (err) {
@@ -322,10 +324,23 @@ async function loadOlderMessages() {
} }
async function pollLatestMessages() { async function pollLatestMessages() {
const response = await getTextMessages(chatPageSize, 0) const response = await getTextMessages(chatPageSize, 0, normalizedChannelFilter.value ? { channelId: normalizedChannelFilter.value } : '')
messages.value = mergeMessages(messages.value, toChronological(response.items)) messages.value = mergeMessages(messages.value, toChronological(response.items))
} }
let channelFilterTimer: number | undefined
watch(normalizedChannelFilter, () => {
if (channelFilterTimer !== undefined) {
window.clearTimeout(channelFilterTimer)
}
channelFilterTimer = window.setTimeout(() => {
messages.value = []
chatHasMore.value = true
chatInitialized.value = false
loadInitialChatMessages()
}, 400)
})
async function loadNodePage(page: number, showLoading = true) { async function loadNodePage(page: number, showLoading = true) {
if (showLoading) { if (showLoading) {
nodePageLoading.value = true nodePageLoading.value = true
@@ -676,6 +691,9 @@ onBeforeUnmount(() => {
if (mapBoundsTimer !== undefined) { if (mapBoundsTimer !== undefined) {
window.clearTimeout(mapBoundsTimer) window.clearTimeout(mapBoundsTimer)
} }
if (channelFilterTimer !== undefined) {
window.clearTimeout(channelFilterTimer)
}
}) })
</script> </script>
@@ -739,6 +757,20 @@ onBeforeUnmount(() => {
@click="nodeFilter = ''" @click="nodeFilter = ''"
>清除</button> >清除</button>
</div> </div>
<div class="topbar-filter">
<input
type="search"
class="topbar-filter-input"
v-model="channelFilter"
placeholder="筛选频道"
/>
<button
v-if="normalizedChannelFilter"
type="button"
class="topbar-filter-clear"
@click="channelFilter = ''"
>清除</button>
</div>
<a class="topbar-link" href="/signed">签到列表</a> <a class="topbar-link" href="/signed">签到列表</a>
<a class="topbar-link" href="/help">使用帮助</a> <a class="topbar-link" href="/help">使用帮助</a>
<a class="topbar-link" href="/admin">管理</a> <a class="topbar-link" href="/admin">管理</a>
+1 -1
View File
@@ -1 +1 @@
export const FRONTEND_VERSION = '0.1.2' export const FRONTEND_VERSION = '0.2.0'