forked from kevin/meshtastic_mqtt_server
统一 Admin 管理后台样式:全部页面改为 100% 全宽,表格长文本列自动换行去除横向滚动,LLM 队列/API 配置页重做为全局 panel 风格,前端 v0.3.2
This commit is contained in:
@@ -458,36 +458,36 @@ onMounted(() => {
|
|||||||
<p v-if="wordError" class="error">{{ wordError }}</p>
|
<p v-if="wordError" class="error">{{ wordError }}</p>
|
||||||
<p v-if="wordMessage" class="success">{{ wordMessage }}</p>
|
<p v-if="wordMessage" class="success">{{ wordMessage }}</p>
|
||||||
|
|
||||||
<div class="node-table-wrap">
|
<div class="node-table-wrap table-fit-wrap">
|
||||||
<table class="node-table">
|
<table class="node-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>ID</th>
|
<th class="cell-nowrap">ID</th>
|
||||||
<th>违禁词</th>
|
<th>违禁词</th>
|
||||||
<th>匹配类型</th>
|
<th class="cell-nowrap">匹配类型</th>
|
||||||
<th>区分大小写</th>
|
<th class="cell-nowrap">区分大小写</th>
|
||||||
<th>原因</th>
|
<th>原因</th>
|
||||||
<th>启用</th>
|
<th class="cell-nowrap">启用</th>
|
||||||
<th>创建时间</th>
|
<th class="cell-nowrap">创建时间</th>
|
||||||
<th>更新时间</th>
|
<th class="cell-nowrap">更新时间</th>
|
||||||
<th>操作</th>
|
<th class="cell-nowrap">操作</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="rule in wordRules" :key="rule.id">
|
<tr v-for="rule in wordRules" :key="rule.id">
|
||||||
<td>{{ rule.id }}</td>
|
<td class="cell-nowrap">{{ rule.id }}</td>
|
||||||
<td><input v-model="wordEdits[rule.id].word" class="admin-table-input" /></td>
|
<td><input v-model="wordEdits[rule.id].word" class="admin-table-input" /></td>
|
||||||
<td>
|
<td>
|
||||||
<select v-model="wordEdits[rule.id].match_type" class="admin-table-input">
|
<select v-model="wordEdits[rule.id].match_type" class="admin-table-input">
|
||||||
<option value="contains">包含</option>
|
<option value="contains">包含</option>
|
||||||
</select>
|
</select>
|
||||||
</td>
|
</td>
|
||||||
<td><input v-model="wordEdits[rule.id].case_sensitive" type="checkbox" /></td>
|
<td class="cell-nowrap"><input v-model="wordEdits[rule.id].case_sensitive" type="checkbox" /></td>
|
||||||
<td><input v-model="wordEdits[rule.id].reason" class="admin-table-input" /></td>
|
<td><input v-model="wordEdits[rule.id].reason" class="admin-table-input" /></td>
|
||||||
<td><input v-model="wordEdits[rule.id].enabled" type="checkbox" /></td>
|
<td class="cell-nowrap"><input v-model="wordEdits[rule.id].enabled" type="checkbox" /></td>
|
||||||
<td>{{ formatTime(rule.created_at) }}</td>
|
<td class="cell-nowrap">{{ formatTime(rule.created_at) }}</td>
|
||||||
<td>{{ formatTime(rule.updated_at) }}</td>
|
<td class="cell-nowrap">{{ formatTime(rule.updated_at) }}</td>
|
||||||
<td>
|
<td class="cell-nowrap">
|
||||||
<button class="admin-button" :disabled="wordLoading" @click="saveWordRule(rule)">保存</button>
|
<button class="admin-button" :disabled="wordLoading" @click="saveWordRule(rule)">保存</button>
|
||||||
<button class="admin-button" :disabled="wordLoading" @click="removeWordRule(rule)">删除</button>
|
<button class="admin-button" :disabled="wordLoading" @click="removeWordRule(rule)">删除</button>
|
||||||
</td>
|
</td>
|
||||||
@@ -533,30 +533,30 @@ onMounted(() => {
|
|||||||
<p v-if="nodeError" class="error">{{ nodeError }}</p>
|
<p v-if="nodeError" class="error">{{ nodeError }}</p>
|
||||||
<p v-if="nodeMessage" class="success">{{ nodeMessage }}</p>
|
<p v-if="nodeMessage" class="success">{{ nodeMessage }}</p>
|
||||||
|
|
||||||
<div class="node-table-wrap">
|
<div class="node-table-wrap table-fit-wrap">
|
||||||
<table class="node-table">
|
<table class="node-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>ID</th>
|
<th class="cell-nowrap">ID</th>
|
||||||
<th>节点 ID</th>
|
<th>节点 ID</th>
|
||||||
<th>数字 ID</th>
|
<th class="cell-nowrap">数字 ID</th>
|
||||||
<th>原因</th>
|
<th>原因</th>
|
||||||
<th>启用</th>
|
<th class="cell-nowrap">启用</th>
|
||||||
<th>创建时间</th>
|
<th class="cell-nowrap">创建时间</th>
|
||||||
<th>更新时间</th>
|
<th class="cell-nowrap">更新时间</th>
|
||||||
<th>操作</th>
|
<th class="cell-nowrap">操作</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="rule in nodeRules" :key="rule.id">
|
<tr v-for="rule in nodeRules" :key="rule.id">
|
||||||
<td>{{ rule.id }}</td>
|
<td class="cell-nowrap">{{ rule.id }}</td>
|
||||||
<td><input v-model="nodeEdits[rule.id].node_id" class="admin-table-input" /></td>
|
<td><input v-model="nodeEdits[rule.id].node_id" class="admin-table-input" /></td>
|
||||||
<td><input v-model="nodeEdits[rule.id].node_num" class="admin-table-input" placeholder="可选" /></td>
|
<td><input v-model="nodeEdits[rule.id].node_num" class="admin-table-input" placeholder="可选" /></td>
|
||||||
<td><input v-model="nodeEdits[rule.id].reason" class="admin-table-input" /></td>
|
<td><input v-model="nodeEdits[rule.id].reason" class="admin-table-input" /></td>
|
||||||
<td><input v-model="nodeEdits[rule.id].enabled" type="checkbox" /></td>
|
<td class="cell-nowrap"><input v-model="nodeEdits[rule.id].enabled" type="checkbox" /></td>
|
||||||
<td>{{ formatTime(rule.created_at) }}</td>
|
<td class="cell-nowrap">{{ formatTime(rule.created_at) }}</td>
|
||||||
<td>{{ formatTime(rule.updated_at) }}</td>
|
<td class="cell-nowrap">{{ formatTime(rule.updated_at) }}</td>
|
||||||
<td>
|
<td class="cell-nowrap">
|
||||||
<button class="admin-button" :disabled="nodeLoading" @click="saveNodeRule(rule)">保存</button>
|
<button class="admin-button" :disabled="nodeLoading" @click="saveNodeRule(rule)">保存</button>
|
||||||
<button class="admin-button" :disabled="nodeLoading" @click="removeNodeRule(rule)">删除</button>
|
<button class="admin-button" :disabled="nodeLoading" @click="removeNodeRule(rule)">删除</button>
|
||||||
</td>
|
</td>
|
||||||
@@ -599,28 +599,28 @@ onMounted(() => {
|
|||||||
<p v-if="ipError" class="error">{{ ipError }}</p>
|
<p v-if="ipError" class="error">{{ ipError }}</p>
|
||||||
<p v-if="ipMessage" class="success">{{ ipMessage }}</p>
|
<p v-if="ipMessage" class="success">{{ ipMessage }}</p>
|
||||||
|
|
||||||
<div class="node-table-wrap">
|
<div class="node-table-wrap table-fit-wrap">
|
||||||
<table class="node-table">
|
<table class="node-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>ID</th>
|
<th class="cell-nowrap">ID</th>
|
||||||
<th>IP/CIDR</th>
|
<th>IP/CIDR</th>
|
||||||
<th>原因</th>
|
<th>原因</th>
|
||||||
<th>启用</th>
|
<th class="cell-nowrap">启用</th>
|
||||||
<th>创建时间</th>
|
<th class="cell-nowrap">创建时间</th>
|
||||||
<th>更新时间</th>
|
<th class="cell-nowrap">更新时间</th>
|
||||||
<th>操作</th>
|
<th class="cell-nowrap">操作</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="rule in ipRules" :key="rule.id">
|
<tr v-for="rule in ipRules" :key="rule.id">
|
||||||
<td>{{ rule.id }}</td>
|
<td class="cell-nowrap">{{ rule.id }}</td>
|
||||||
<td><input v-model="ipEdits[rule.id].ip_value" class="admin-table-input" /></td>
|
<td><input v-model="ipEdits[rule.id].ip_value" class="admin-table-input" /></td>
|
||||||
<td><input v-model="ipEdits[rule.id].reason" class="admin-table-input" /></td>
|
<td><input v-model="ipEdits[rule.id].reason" class="admin-table-input" /></td>
|
||||||
<td><input v-model="ipEdits[rule.id].enabled" type="checkbox" /></td>
|
<td class="cell-nowrap"><input v-model="ipEdits[rule.id].enabled" type="checkbox" /></td>
|
||||||
<td>{{ formatTime(rule.created_at) }}</td>
|
<td class="cell-nowrap">{{ formatTime(rule.created_at) }}</td>
|
||||||
<td>{{ formatTime(rule.updated_at) }}</td>
|
<td class="cell-nowrap">{{ formatTime(rule.updated_at) }}</td>
|
||||||
<td>
|
<td class="cell-nowrap">
|
||||||
<button class="admin-button" :disabled="ipLoading" @click="saveIPRule(rule)">保存</button>
|
<button class="admin-button" :disabled="ipLoading" @click="saveIPRule(rule)">保存</button>
|
||||||
<button class="admin-button" :disabled="ipLoading" @click="removeIPRule(rule)">删除</button>
|
<button class="admin-button" :disabled="ipLoading" @click="removeIPRule(rule)">删除</button>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -360,8 +360,9 @@ onBeforeUnmount(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="panel direct-page">
|
<section class="admin-dashboard">
|
||||||
<div class="direct-header">
|
<div class="panel">
|
||||||
|
<div class="panel-header direct-header">
|
||||||
<div>
|
<div>
|
||||||
<p class="eyebrow">Direct Bot Chat</p>
|
<p class="eyebrow">Direct Bot Chat</p>
|
||||||
<h2>
|
<h2>
|
||||||
@@ -393,7 +394,9 @@ onBeforeUnmount(() => {
|
|||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel direct-chat-panel">
|
||||||
<div class="direct-layout">
|
<div class="direct-layout">
|
||||||
<aside class="conversation-list">
|
<aside class="conversation-list">
|
||||||
<p v-if="conversations.length === 0" class="empty-state">还没有私聊会话。等设备发来消息或在上方“新建私聊”开启。</p>
|
<p v-if="conversations.length === 0" class="empty-state">还没有私聊会话。等设备发来消息或在上方“新建私聊”开启。</p>
|
||||||
@@ -442,11 +445,13 @@ onBeforeUnmount(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.direct-page { display: grid; gap: 12px; padding: 16px; }
|
.direct-chat-panel { padding: 16px; }
|
||||||
|
.direct-bot-picker { padding: 16px; }
|
||||||
.direct-header, .direct-actions, .send-actions { display: flex; align-items: center; justify-content: space-between; gap: 10px; flex-wrap: wrap; }
|
.direct-header, .direct-actions, .send-actions { display: flex; align-items: center; justify-content: space-between; gap: 10px; flex-wrap: wrap; }
|
||||||
.direct-bot-picker { display: grid; grid-template-columns: repeat(2, minmax(200px, 1fr)); gap: 12px; }
|
.direct-bot-picker { display: grid; grid-template-columns: repeat(2, minmax(200px, 1fr)); gap: 12px; }
|
||||||
.direct-hint { color: #475569; font-size: 12px; margin: 0 0 8px; }
|
.direct-hint { color: #475569; font-size: 12px; margin: 0 0 8px; }
|
||||||
|
|||||||
@@ -253,7 +253,7 @@ onBeforeUnmount(() => {
|
|||||||
</div>
|
</div>
|
||||||
<span class="badge">{{ status?.clients?.length ?? 0 }}</span>
|
<span class="badge">{{ status?.clients?.length ?? 0 }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="node-table-wrap">
|
<div class="node-table-wrap table-fit-wrap">
|
||||||
<table class="node-table">
|
<table class="node-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -261,9 +261,9 @@ onBeforeUnmount(() => {
|
|||||||
<th class="sortable" @click="toggleClientSort('username')">Username <span class="sort-indicator">{{ sortIndicator('username') }}</span></th>
|
<th class="sortable" @click="toggleClientSort('username')">Username <span class="sort-indicator">{{ sortIndicator('username') }}</span></th>
|
||||||
<th class="sortable" @click="toggleClientSort('listener')">Listener <span class="sort-indicator">{{ sortIndicator('listener') }}</span></th>
|
<th class="sortable" @click="toggleClientSort('listener')">Listener <span class="sort-indicator">{{ sortIndicator('listener') }}</span></th>
|
||||||
<th class="sortable" @click="toggleClientSort('remote_addr')">Remote Addr <span class="sort-indicator">{{ sortIndicator('remote_addr') }}</span></th>
|
<th class="sortable" @click="toggleClientSort('remote_addr')">Remote Addr <span class="sort-indicator">{{ sortIndicator('remote_addr') }}</span></th>
|
||||||
<th class="sortable" @click="toggleClientSort('packets_in')">客户端→服务器 <span class="sort-indicator">{{ sortIndicator('packets_in') }}</span></th>
|
<th class="sortable cell-nowrap" @click="toggleClientSort('packets_in')">客户端→服务器 <span class="sort-indicator">{{ sortIndicator('packets_in') }}</span></th>
|
||||||
<th class="sortable" @click="toggleClientSort('packets_out')">服务器→客户端 <span class="sort-indicator">{{ sortIndicator('packets_out') }}</span></th>
|
<th class="sortable cell-nowrap" @click="toggleClientSort('packets_out')">服务器→客户端 <span class="sort-indicator">{{ sortIndicator('packets_out') }}</span></th>
|
||||||
<th>操作</th>
|
<th class="cell-nowrap">操作</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -272,9 +272,9 @@ onBeforeUnmount(() => {
|
|||||||
<td>{{ client.username || '-' }}</td>
|
<td>{{ client.username || '-' }}</td>
|
||||||
<td>{{ client.listener || '-' }}</td>
|
<td>{{ client.listener || '-' }}</td>
|
||||||
<td>{{ client.remote_addr || '-' }}</td>
|
<td>{{ client.remote_addr || '-' }}</td>
|
||||||
<td>{{ client.packets_in ?? 0 }}</td>
|
<td class="cell-nowrap">{{ client.packets_in ?? 0 }}</td>
|
||||||
<td>{{ client.packets_out ?? 0 }}</td>
|
<td class="cell-nowrap">{{ client.packets_out ?? 0 }}</td>
|
||||||
<td>
|
<td class="cell-nowrap">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="client-danger-action"
|
class="client-danger-action"
|
||||||
|
|||||||
@@ -135,11 +135,11 @@ onMounted(refreshItems)
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p v-if="error" class="error">{{ error }}</p>
|
<p v-if="error" class="error">{{ error }}</p>
|
||||||
<div class="node-table-wrap">
|
<div class="node-table-wrap table-fit-wrap">
|
||||||
<table class="node-table">
|
<table class="node-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th style="width: 40px;">
|
<th style="width: 40px;" class="cell-nowrap">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
:checked="allSelected"
|
:checked="allSelected"
|
||||||
@@ -148,10 +148,10 @@ onMounted(refreshItems)
|
|||||||
@change="toggleAll(($event.target as HTMLInputElement).checked)"
|
@change="toggleAll(($event.target as HTMLInputElement).checked)"
|
||||||
/>
|
/>
|
||||||
</th>
|
</th>
|
||||||
<th>时间</th>
|
<th class="cell-nowrap">时间</th>
|
||||||
<th>Topic</th>
|
<th>Topic</th>
|
||||||
<th>Error</th>
|
<th>Error</th>
|
||||||
<th>Payload Len</th>
|
<th class="cell-nowrap">Payload Len</th>
|
||||||
<th>Client ID</th>
|
<th>Client ID</th>
|
||||||
<th>Username</th>
|
<th>Username</th>
|
||||||
<th>Listener</th>
|
<th>Listener</th>
|
||||||
@@ -162,17 +162,17 @@ onMounted(refreshItems)
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="item in items" :key="item.id">
|
<tr v-for="item in items" :key="item.id">
|
||||||
<td>
|
<td class="cell-nowrap">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
:checked="selectedIds.has(item.id)"
|
:checked="selectedIds.has(item.id)"
|
||||||
@change="toggleRow(item.id, ($event.target as HTMLInputElement).checked)"
|
@change="toggleRow(item.id, ($event.target as HTMLInputElement).checked)"
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ formatTime(item.created_at) }}</td>
|
<td class="cell-nowrap">{{ formatTime(item.created_at) }}</td>
|
||||||
<td>{{ item.topic || '-' }}</td>
|
<td>{{ item.topic || '-' }}</td>
|
||||||
<td>{{ item.error || '-' }}</td>
|
<td>{{ item.error || '-' }}</td>
|
||||||
<td>{{ item.payload_len }}</td>
|
<td class="cell-nowrap">{{ item.payload_len }}</td>
|
||||||
<td>{{ item.mqtt_client_id || '-' }}</td>
|
<td>{{ item.mqtt_client_id || '-' }}</td>
|
||||||
<td>{{ item.mqtt_username || '-' }}</td>
|
<td>{{ item.mqtt_username || '-' }}</td>
|
||||||
<td>{{ item.mqtt_listener || '-' }}</td>
|
<td>{{ item.mqtt_listener || '-' }}</td>
|
||||||
|
|||||||
@@ -14,13 +14,6 @@ const offset = ref(0)
|
|||||||
const selectedBotId = ref<number | ''>('')
|
const selectedBotId = ref<number | ''>('')
|
||||||
const includeDeleted = ref(false)
|
const includeDeleted = ref(false)
|
||||||
|
|
||||||
const statusColors: Record<string, string> = {
|
|
||||||
pending: 'background: linear-gradient(135deg, #fef3c7 0%, #fde68a33 100%);',
|
|
||||||
processing: 'background: linear-gradient(135deg, #dbeafe 0%, #bfdbfe33 100%);',
|
|
||||||
processed: 'background: linear-gradient(135deg, #dcfce7 0%, #bbf7d033 100%);',
|
|
||||||
error: 'background: linear-gradient(135deg, #fee2e2 0%, #fecaca33 100%);',
|
|
||||||
}
|
|
||||||
|
|
||||||
const statusLabels: Record<string, string> = {
|
const statusLabels: Record<string, string> = {
|
||||||
pending: '待处理',
|
pending: '待处理',
|
||||||
processing: '处理中',
|
processing: '处理中',
|
||||||
@@ -178,12 +171,15 @@ onMounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="admin-llm">
|
<section class="admin-dashboard">
|
||||||
<h2>LLM 消息队列</h2>
|
<div class="panel admin-status-panel">
|
||||||
|
<div class="panel-header">
|
||||||
<div class="admin-llm-section">
|
<div>
|
||||||
<h3>机器人 LLM 设置</h3>
|
<p class="eyebrow">LLM</p>
|
||||||
<p class="section-desc">每个机器人可以独立启用或禁用 LLM 消息队列。启用后,该机器人收到的私聊消息将被加入队列。</p>
|
<h2>机器人 LLM 设置</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="llm-section-desc">每个机器人可以独立启用或禁用 LLM 消息队列。启用后,该机器人收到的私聊消息将被加入队列。</p>
|
||||||
|
|
||||||
<div class="bot-settings-grid">
|
<div class="bot-settings-grid">
|
||||||
<div v-for="bot in botNodes" :key="bot.id" class="bot-settings-card">
|
<div v-for="bot in botNodes" :key="bot.id" class="bot-settings-card">
|
||||||
@@ -216,74 +212,78 @@ onMounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="admin-llm-toolbar">
|
<div class="panel admin-status-panel">
|
||||||
<div class="admin-llm-filter">
|
<div class="panel-header">
|
||||||
<label>选择机器人:</label>
|
<div>
|
||||||
|
<p class="eyebrow">Queue</p>
|
||||||
|
<h2>LLM 消息队列</h2>
|
||||||
|
</div>
|
||||||
|
<div class="admin-actions">
|
||||||
|
<button class="admin-button" @click="() => loadMessages()" :disabled="loading">{{ loading ? '刷新中...' : '刷新' }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="llm-toolbar">
|
||||||
|
<label class="llm-filter">
|
||||||
|
<span>选择机器人</span>
|
||||||
<select v-model="selectedBotId" @change="loadMessages(true)">
|
<select v-model="selectedBotId" @change="loadMessages(true)">
|
||||||
<option :value="''">全部</option>
|
<option :value="''">全部</option>
|
||||||
<option v-for="bot in botNodes" :key="bot.id" :value="bot.id">
|
<option v-for="bot in botNodes" :key="bot.id" :value="bot.id">
|
||||||
{{ bot.long_name }} ({{ bot.node_id }})
|
{{ bot.long_name }} ({{ bot.node_id }})
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</label>
|
||||||
|
|
||||||
<div class="admin-llm-filter">
|
<label class="llm-filter llm-filter-inline">
|
||||||
<label>
|
|
||||||
<input type="checkbox" v-model="includeDeleted" @change="loadMessages(true)" />
|
<input type="checkbox" v-model="includeDeleted" @change="loadMessages(true)" />
|
||||||
包含已删除
|
包含已删除
|
||||||
</label>
|
</label>
|
||||||
</div>
|
|
||||||
|
|
||||||
<button class="admin-button admin-button-danger" @click="handleDeleteAllByBot" :disabled="!selectedBotId">
|
<button class="admin-button danger" @click="handleDeleteAllByBot" :disabled="!selectedBotId">
|
||||||
删除该机器人所有消息
|
删除该机器人所有消息
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button class="admin-button" @click="() => loadMessages()">刷新</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p v-if="error" class="error">{{ error }}</p>
|
<p v-if="error" class="error">{{ error }}</p>
|
||||||
|
|
||||||
<div class="admin-llm-stats">
|
<div class="llm-stats">
|
||||||
共 {{ total }} 条消息,当前显示 {{ displayFrom }} - {{ displayTo }}
|
共 {{ total }} 条消息,当前显示 {{ displayFrom }} - {{ displayTo }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="loading" class="admin-loading">
|
<div v-if="loading" class="empty">加载中...</div>
|
||||||
<div style="margin-bottom: 1rem;">🔄</div>
|
|
||||||
加载中...
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else class="admin-llm-table-wrapper">
|
<div v-else class="node-table-wrap table-fit-wrap">
|
||||||
<table class="admin-llm-table">
|
<table class="node-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>ID</th>
|
<th class="cell-nowrap">ID</th>
|
||||||
<th>机器人</th>
|
<th>机器人</th>
|
||||||
<th>类型</th>
|
<th class="cell-nowrap">类型</th>
|
||||||
<th>状态</th>
|
<th class="cell-nowrap">状态</th>
|
||||||
<th>来自节点</th>
|
<th>来自节点</th>
|
||||||
<th>频道</th>
|
<th>频道</th>
|
||||||
<th>消息内容</th>
|
<th>消息内容</th>
|
||||||
<th>接收时间</th>
|
<th class="cell-nowrap">接收时间</th>
|
||||||
<th>处理时间</th>
|
<th class="cell-nowrap">处理时间</th>
|
||||||
<th>操作</th>
|
<th class="cell-nowrap">操作</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="msg in messages" :key="msg.id" :style="statusColors[msg.status]">
|
<tr v-for="msg in messages" :key="msg.id">
|
||||||
<td>{{ msg.id }}</td>
|
<td class="cell-nowrap">{{ msg.id }}</td>
|
||||||
<td class="bot-name-cell">
|
<td>
|
||||||
<div class="bot-info">
|
<div class="bot-info">
|
||||||
<div class="bot-long-name">{{ getBotName(msg.bot_id) }}</div>
|
<div class="bot-long-name">{{ getBotName(msg.bot_id) }}</div>
|
||||||
<div class="bot-node-id" v-if="msg.bot_id !== 0">{{ msg.bot_node_id }}</div>
|
<div class="bot-node-id" v-if="msg.bot_id !== 0">{{ msg.bot_node_id }}</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td class="cell-nowrap">
|
||||||
<span class="type-badge" :class="getMessageType(msg) === '频道' ? 'channel' : 'direct'">
|
<span class="type-badge" :class="getMessageType(msg) === '频道' ? 'channel' : 'direct'">
|
||||||
{{ getMessageType(msg) }}
|
{{ getMessageType(msg) }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td class="cell-nowrap">
|
||||||
<span class="status-badge" :style="statusColors[msg.status]">
|
<span class="status-badge" :class="`status-${msg.status}`">
|
||||||
{{ statusLabels[msg.status] || msg.status }}
|
{{ statusLabels[msg.status] || msg.status }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
@@ -295,12 +295,12 @@ onMounted(() => {
|
|||||||
</td>
|
</td>
|
||||||
<td class="channel-cell">{{ msg.channel_id || '-' }}</td>
|
<td class="channel-cell">{{ msg.channel_id || '-' }}</td>
|
||||||
<td class="message-text">{{ msg.text }}</td>
|
<td class="message-text">{{ msg.text }}</td>
|
||||||
<td>{{ formatTime(msg.received_at) }}</td>
|
<td class="cell-nowrap">{{ formatTime(msg.received_at) }}</td>
|
||||||
<td>{{ formatTime(msg.processed_at) }}</td>
|
<td class="cell-nowrap">{{ formatTime(msg.processed_at) }}</td>
|
||||||
<td>
|
<td class="cell-nowrap">
|
||||||
<button
|
<button
|
||||||
v-if="!msg.deleted_at"
|
v-if="!msg.deleted_at"
|
||||||
class="admin-button admin-button-small admin-button-danger"
|
class="admin-button danger"
|
||||||
@click="handleDeleteMessage(msg.id)"
|
@click="handleDeleteMessage(msg.id)"
|
||||||
>
|
>
|
||||||
删除
|
删除
|
||||||
@@ -308,516 +308,249 @@ onMounted(() => {
|
|||||||
<span v-else class="muted-tag">已删除</span>
|
<span v-else class="muted-tag">已删除</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-if="messages.length === 0">
|
|
||||||
<td colspan="10" class="empty-state">暂无消息</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
<div v-if="messages.length === 0" class="empty">暂无消息</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="admin-llm-pagination">
|
<div class="pagination">
|
||||||
<button class="admin-button admin-button-small" @click="goPrev" :disabled="!hasPrev">上一页</button>
|
<button @click="goPrev" :disabled="!hasPrev">上一页</button>
|
||||||
<span class="pagination-info">第 {{ Math.floor(offset / limit) + 1 }} 页</span>
|
<span>第 {{ Math.floor(offset / limit) + 1 }} 页</span>
|
||||||
<button class="admin-button admin-button-small" @click="goNext" :disabled="!hasMore">下一页</button>
|
<button @click="goNext" :disabled="!hasMore">下一页</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.admin-llm {
|
.llm-section-desc {
|
||||||
padding: 1.5rem;
|
margin: 0;
|
||||||
max-width: 100%;
|
padding: 12px 16px 0;
|
||||||
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
|
color: var(--color-muted);
|
||||||
min-height: 100vh;
|
font-size: 13px;
|
||||||
}
|
line-height: 1.6;
|
||||||
|
|
||||||
.admin-llm h2 {
|
|
||||||
margin: 0 0 2rem;
|
|
||||||
font-size: 1.75rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #1e293b;
|
|
||||||
letter-spacing: -0.02em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-llm-section {
|
|
||||||
background: white;
|
|
||||||
padding: 1.75rem;
|
|
||||||
border-radius: 16px;
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05), 0 4px 6px rgba(0, 0, 0, 0.03);
|
|
||||||
border: 1px solid #e2e8f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-llm-section h3 {
|
|
||||||
margin: 0 0 0.75rem;
|
|
||||||
font-size: 1.25rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #334155;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-desc {
|
|
||||||
margin: 0 0 1.5rem;
|
|
||||||
color: #64748b;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.bot-settings-grid {
|
.bot-settings-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||||
gap: 1.25rem;
|
gap: 12px;
|
||||||
|
padding: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bot-settings-card {
|
.bot-settings-card {
|
||||||
background: linear-gradient(135deg, #fafbfc 0%, #f8fafc 100%);
|
border: 1px solid var(--color-border);
|
||||||
padding: 1.25rem;
|
border-radius: var(--radius-md);
|
||||||
border-radius: 12px;
|
padding: 14px;
|
||||||
border: 1px solid #e2e8f0;
|
background: var(--color-surface-soft);
|
||||||
transition: all 0.2s ease;
|
transition: border-color 0.16s ease, box-shadow 0.16s ease, transform 0.16s ease;
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bot-settings-card::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
height: 3px;
|
|
||||||
background: linear-gradient(90deg, #3b82f6, #8b5cf6);
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 0.2s ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.bot-settings-card:hover {
|
.bot-settings-card:hover {
|
||||||
transform: translateY(-2px);
|
border-color: var(--color-primary);
|
||||||
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.08);
|
box-shadow: var(--shadow-sm);
|
||||||
border-color: #cbd5e1;
|
transform: translateY(-1px);
|
||||||
}
|
|
||||||
|
|
||||||
.bot-settings-card:hover::before {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.bot-header {
|
.bot-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.35rem;
|
gap: 6px;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 10px;
|
||||||
padding-bottom: 1rem;
|
padding-bottom: 10px;
|
||||||
border-bottom: 1px solid #e2e8f0;
|
border-bottom: 1px solid var(--color-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.bot-header strong {
|
.bot-header strong {
|
||||||
font-size: 1.05rem;
|
color: var(--color-heading);
|
||||||
color: #1e293b;
|
font-size: 15px;
|
||||||
font-weight: 600;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.bot-node-id {
|
.bot-node-id {
|
||||||
font-size: 0.8rem;
|
|
||||||
color: #64748b;
|
|
||||||
font-family: 'SF Mono', 'Monaco', 'Inconsolata', monospace;
|
|
||||||
background: #f1f5f9;
|
|
||||||
padding: 0.2rem 0.5rem;
|
|
||||||
border-radius: 4px;
|
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
padding: 2px 8px;
|
||||||
|
color: var(--color-muted);
|
||||||
|
background: var(--color-surface-muted);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bot-settings {
|
.bot-settings {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.75rem;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.setting-item {
|
.setting-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.75rem;
|
gap: 8px;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
padding: 6px 8px;
|
||||||
|
color: var(--color-text);
|
||||||
|
font-size: 13px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 0.9rem;
|
transition: background-color 0.16s ease;
|
||||||
color: #475569;
|
|
||||||
padding: 0.5rem;
|
|
||||||
border-radius: 8px;
|
|
||||||
transition: background-color 0.15s ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.setting-item:hover {
|
.setting-item:hover {
|
||||||
background: #f1f5f9;
|
background: var(--color-primary-soft);
|
||||||
}
|
}
|
||||||
|
|
||||||
.setting-item input[type='checkbox'] {
|
.setting-item input[type='checkbox'] {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
width: 18px;
|
width: 16px;
|
||||||
height: 18px;
|
height: 16px;
|
||||||
accent-color: #3b82f6;
|
accent-color: var(--color-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.saving-indicator {
|
.saving-indicator {
|
||||||
margin-top: 0.75rem;
|
margin-top: 8px;
|
||||||
font-size: 0.8rem;
|
color: var(--color-primary);
|
||||||
color: #3b82f6;
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.llm-toolbar {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: flex-end;
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.saving-indicator::before {
|
|
||||||
content: '';
|
|
||||||
width: 12px;
|
|
||||||
height: 12px;
|
|
||||||
border: 2px solid #3b82f6;
|
|
||||||
border-top-color: transparent;
|
|
||||||
border-radius: 50%;
|
|
||||||
animation: spin 0.8s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
to {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-llm-toolbar {
|
|
||||||
display: flex;
|
|
||||||
gap: 1rem;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
padding: 1.25rem;
|
gap: 12px;
|
||||||
background: white;
|
padding: 14px 16px;
|
||||||
border-radius: 12px;
|
border-bottom: 1px solid var(--color-border);
|
||||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
|
||||||
border: 1px solid #e2e8f0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-llm-filter {
|
.llm-filter {
|
||||||
|
display: grid;
|
||||||
|
gap: 6px;
|
||||||
|
color: var(--color-muted);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.llm-filter select {
|
||||||
|
border: 1px solid var(--color-border-strong);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
padding: 9px 12px;
|
||||||
|
min-width: 220px;
|
||||||
|
color: var(--color-heading);
|
||||||
|
background: var(--color-surface);
|
||||||
|
outline: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color 0.16s ease, box-shadow 0.16s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.llm-filter select:focus {
|
||||||
|
border-color: var(--color-primary);
|
||||||
|
box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-primary) 20%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.llm-filter-inline {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.75rem;
|
gap: 8px;
|
||||||
}
|
padding-bottom: 9px;
|
||||||
|
color: var(--color-text);
|
||||||
.admin-llm-filter label {
|
font-size: 13px;
|
||||||
font-size: 0.9rem;
|
|
||||||
color: #64748b;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-llm-filter select {
|
|
||||||
padding: 0.6rem 1rem;
|
|
||||||
border: 1px solid #cbd5e1;
|
|
||||||
border-radius: 8px;
|
|
||||||
min-width: 220px;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
color: #334155;
|
|
||||||
background: white;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.15s ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-llm-filter select:hover {
|
.llm-filter-inline input[type='checkbox'] {
|
||||||
border-color: #94a3b8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-llm-filter select:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: #3b82f6;
|
|
||||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-llm-filter input[type='checkbox'] {
|
|
||||||
width: 16px;
|
width: 16px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
accent-color: #3b82f6;
|
accent-color: var(--color-primary);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-llm-stats {
|
.llm-stats {
|
||||||
padding: 1rem 1.25rem;
|
padding: 12px 16px;
|
||||||
background: linear-gradient(135deg, #eff6ff 0%, #dbeafe 100%);
|
border-bottom: 1px solid var(--color-border);
|
||||||
border-radius: 10px;
|
color: var(--color-muted);
|
||||||
margin-bottom: 1.25rem;
|
font-size: 13px;
|
||||||
font-size: 0.9rem;
|
}
|
||||||
color: #1e40af;
|
|
||||||
font-weight: 500;
|
.type-badge,
|
||||||
display: flex;
|
.status-badge {
|
||||||
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
border-radius: 999px;
|
||||||
border: 1px solid #bfdbfe;
|
padding: 4px 10px;
|
||||||
}
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
.admin-loading {
|
white-space: nowrap;
|
||||||
padding: 3rem;
|
|
||||||
text-align: center;
|
|
||||||
color: #64748b;
|
|
||||||
background: white;
|
|
||||||
border-radius: 12px;
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.type-badge {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 0.35rem 0.75rem;
|
|
||||||
border-radius: 20px;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
font-weight: 600;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.02em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.type-badge.channel {
|
.type-badge.channel {
|
||||||
background: linear-gradient(135deg, #e0e7ff 0%, #c7d2fe 100%);
|
color: color-mix(in srgb, var(--color-primary) 78%, var(--color-heading));
|
||||||
color: #4338ca;
|
background: var(--color-primary-soft);
|
||||||
border: 1px solid #a5b4fc;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.type-badge.direct {
|
.type-badge.direct {
|
||||||
background: linear-gradient(135deg, #fce7f3 0%, #fbcfe8 100%);
|
color: color-mix(in srgb, var(--color-accent) 78%, var(--color-heading));
|
||||||
color: #be185d;
|
background: color-mix(in srgb, var(--color-accent) 14%, white);
|
||||||
border: 1px solid #f9a8d4;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.channel-cell {
|
.status-badge.status-pending {
|
||||||
font-family: 'SF Mono', 'Monaco', 'Inconsolata', monospace;
|
color: color-mix(in srgb, var(--color-warning) 72%, var(--color-heading));
|
||||||
font-size: 0.8rem;
|
background: var(--color-warning-soft);
|
||||||
color: #64748b;
|
|
||||||
max-width: 120px;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
background: #f8fafc;
|
|
||||||
padding: 0.25rem 0.5rem;
|
|
||||||
border-radius: 4px;
|
|
||||||
border: 1px solid #e2e8f0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.bot-name-cell {
|
.status-badge.status-processing {
|
||||||
min-width: 160px;
|
color: color-mix(in srgb, var(--color-primary) 78%, var(--color-heading));
|
||||||
|
background: var(--color-primary-soft);
|
||||||
}
|
}
|
||||||
|
|
||||||
.bot-info {
|
.status-badge.status-processed {
|
||||||
display: flex;
|
color: color-mix(in srgb, var(--color-success) 70%, var(--color-heading));
|
||||||
flex-direction: column;
|
background: var(--color-success-soft);
|
||||||
gap: 0.35rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.bot-long-name {
|
.status-badge.status-error {
|
||||||
font-weight: 600;
|
color: color-mix(in srgb, var(--color-danger) 76%, var(--color-heading));
|
||||||
color: #1e293b;
|
background: var(--color-danger-soft);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bot-info,
|
||||||
.node-info {
|
.node-info {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.35rem;
|
gap: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bot-long-name,
|
||||||
.node-name {
|
.node-name {
|
||||||
font-weight: 600;
|
color: var(--color-heading);
|
||||||
color: #1e293b;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.node-id {
|
.node-id {
|
||||||
font-size: 0.8rem;
|
|
||||||
color: #64748b;
|
|
||||||
font-family: 'SF Mono', 'Monaco', 'Inconsolata', monospace;
|
|
||||||
background: #f1f5f9;
|
|
||||||
padding: 0.2rem 0.5rem;
|
|
||||||
border-radius: 4px;
|
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
padding: 1px 6px;
|
||||||
|
color: var(--color-muted);
|
||||||
|
background: var(--color-surface-muted);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 11px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-llm-table-wrapper {
|
.channel-cell {
|
||||||
overflow-x: auto;
|
color: var(--color-muted);
|
||||||
background: white;
|
font-family: var(--font-mono);
|
||||||
border-radius: 12px;
|
font-size: 12px;
|
||||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
|
||||||
border: 1px solid #e2e8f0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-llm-table {
|
.message-text {
|
||||||
width: 100%;
|
line-height: 1.5;
|
||||||
border-collapse: collapse;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
min-width: 1000px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-llm-table th,
|
|
||||||
.admin-llm-table td {
|
|
||||||
padding: 1rem;
|
|
||||||
text-align: left;
|
|
||||||
border-bottom: 1px solid #f1f5f9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-llm-table th {
|
|
||||||
background: linear-gradient(180deg, #f8fafc 0%, #f1f5f9 100%);
|
|
||||||
font-weight: 600;
|
|
||||||
color: #475569;
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.03em;
|
|
||||||
border-bottom: 2px solid #e2e8f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-llm-table tbody tr {
|
|
||||||
transition: background-color 0.15s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-llm-table tbody tr:hover {
|
|
||||||
background: #f8fafc;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-badge {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 0.4rem 0.8rem;
|
|
||||||
border-radius: 20px;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
font-weight: 600;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.02em;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.muted-tag {
|
.muted-tag {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 0.25rem 0.6rem;
|
border-radius: var(--radius-sm);
|
||||||
border-radius: 6px;
|
padding: 2px 8px;
|
||||||
font-size: 0.75rem;
|
color: var(--color-muted);
|
||||||
color: #94a3b8;
|
background: var(--color-surface-muted);
|
||||||
background: #f1f5f9;
|
font-size: 12px;
|
||||||
}
|
|
||||||
|
|
||||||
.status-badge[style*='#fff3cd'] {
|
|
||||||
background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%) !important;
|
|
||||||
color: #92400e;
|
|
||||||
border-color: #fcd34d;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-badge[style*='#cfe2ff'] {
|
|
||||||
background: linear-gradient(135deg, #dbeafe 0%, #bfdbfe 100%) !important;
|
|
||||||
color: #1e40af;
|
|
||||||
border-color: #93c5fd;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-badge[style*='#d1e7dd'] {
|
|
||||||
background: linear-gradient(135deg, #dcfce7 0%, #bbf7d0 100%) !important;
|
|
||||||
color: #166534;
|
|
||||||
border-color: #86efac;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-badge[style*='#f8d7da'] {
|
|
||||||
background: linear-gradient(135deg, #fee2e2 0%, #fecaca 100%) !important;
|
|
||||||
color: #991b1b;
|
|
||||||
border-color: #fca5a5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-text {
|
|
||||||
max-width: 450px;
|
|
||||||
word-break: break-word;
|
|
||||||
line-height: 1.5;
|
|
||||||
color: #334155;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-state {
|
|
||||||
text-align: center;
|
|
||||||
padding: 4rem 2rem !important;
|
|
||||||
color: #94a3b8;
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-state::before {
|
|
||||||
content: '📭';
|
|
||||||
display: block;
|
|
||||||
font-size: 3rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-llm-pagination {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 1rem;
|
|
||||||
margin-top: 1.5rem;
|
|
||||||
padding: 1.25rem;
|
|
||||||
background: white;
|
|
||||||
border-radius: 12px;
|
|
||||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
|
||||||
border: 1px solid #e2e8f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pagination-info {
|
|
||||||
color: #64748b;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
font-weight: 500;
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
background: #f8fafc;
|
|
||||||
border-radius: 8px;
|
|
||||||
min-width: 80px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error {
|
|
||||||
color: #991b1b;
|
|
||||||
padding: 1rem 1.25rem;
|
|
||||||
background: linear-gradient(135deg, #fee2e2 0%, #fecaca 100%);
|
|
||||||
border-radius: 10px;
|
|
||||||
margin-bottom: 1.25rem;
|
|
||||||
border: 1px solid #fca5a5;
|
|
||||||
font-weight: 500;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error::before {
|
|
||||||
content: '⚠️';
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-button {
|
|
||||||
padding: 0.6rem 1.25rem;
|
|
||||||
border: none;
|
|
||||||
border-radius: 8px;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
font-weight: 500;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.15s ease;
|
|
||||||
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
|
|
||||||
color: white;
|
|
||||||
box-shadow: 0 2px 4px rgba(59, 130, 246, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-button:hover:not(:disabled) {
|
|
||||||
transform: translateY(-1px);
|
|
||||||
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-button:active:not(:disabled) {
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-button-small {
|
|
||||||
padding: 0.4rem 0.75rem;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-button-danger {
|
|
||||||
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
|
|
||||||
box-shadow: 0 2px 4px rgba(239, 68, 68, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-button-danger:hover:not(:disabled) {
|
|
||||||
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
button:disabled {
|
|
||||||
opacity: 0.5;
|
|
||||||
cursor: not-allowed;
|
|
||||||
transform: none !important;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -338,9 +338,8 @@ onMounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="admin-llm-api">
|
<section class="admin-dashboard">
|
||||||
<h2>LLM API 配置管理</h2>
|
<div class="panel">
|
||||||
|
|
||||||
<div class="ai-status-bar">
|
<div class="ai-status-bar">
|
||||||
<span class="status-indicator" :class="{ running: aiStatus.running, stopped: !aiStatus.running }"></span>
|
<span class="status-indicator" :class="{ running: aiStatus.running, stopped: !aiStatus.running }"></span>
|
||||||
<span class="status-text">
|
<span class="status-text">
|
||||||
@@ -348,7 +347,7 @@ onMounted(() => {
|
|||||||
<template v-else>{{ aiStatus.message || 'AI 服务未运行' }}</template>
|
<template v-else>{{ aiStatus.message || 'AI 服务未运行' }}</template>
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
class="admin-button admin-button-small"
|
class="admin-button"
|
||||||
:disabled="restarting"
|
:disabled="restarting"
|
||||||
@click="handleRestartAI"
|
@click="handleRestartAI"
|
||||||
>
|
>
|
||||||
@@ -358,18 +357,21 @@ onMounted(() => {
|
|||||||
|
|
||||||
<p v-if="error" class="error">{{ error }}</p>
|
<p v-if="error" class="error">{{ error }}</p>
|
||||||
<p v-if="success" :class="showWarning ? 'warning' : 'success'">{{ success }}</p>
|
<p v-if="success" :class="showWarning ? 'warning' : 'success'">{{ success }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- LLM Provider 列表 -->
|
<!-- LLM Provider 列表 -->
|
||||||
<div class="admin-section">
|
<div class="panel">
|
||||||
<div class="section-header">
|
<div class="panel-header">
|
||||||
<div>
|
<div>
|
||||||
<h3>AI 提供商配置</h3>
|
<p class="eyebrow">Providers</p>
|
||||||
<p class="section-desc">管理多个 LLM API 提供商配置,支持不同模型和服务。</p>
|
<h2>AI 提供商配置</h2>
|
||||||
</div>
|
</div>
|
||||||
<button class="admin-button" @click="openCreateProvider">+ 添加配置</button>
|
<button class="admin-button" @click="openCreateProvider">+ 添加配置</button>
|
||||||
</div>
|
</div>
|
||||||
|
<p class="section-desc">管理多个 LLM API 提供商配置,支持不同模型和服务。</p>
|
||||||
|
|
||||||
<div v-if="loading" class="admin-loading">加载中...</div>
|
<div class="section-body">
|
||||||
|
<div v-if="loading" class="empty">加载中...</div>
|
||||||
|
|
||||||
<div v-else class="provider-grid">
|
<div v-else class="provider-grid">
|
||||||
<div v-for="provider in providers" :key="provider.name" class="provider-card">
|
<div v-for="provider in providers" :key="provider.name" class="provider-card">
|
||||||
@@ -381,8 +383,8 @@ onMounted(() => {
|
|||||||
<strong>{{ provider.name }}</strong>
|
<strong>{{ provider.name }}</strong>
|
||||||
</div>
|
</div>
|
||||||
<div class="provider-actions">
|
<div class="provider-actions">
|
||||||
<button class="admin-button admin-button-small" @click="openEditProvider(provider)">编辑</button>
|
<button class="admin-button" @click="openEditProvider(provider)">编辑</button>
|
||||||
<button class="admin-button admin-button-small admin-button-danger" @click="confirmDeleteProvider(provider.name)">删除</button>
|
<button class="admin-button danger" @click="confirmDeleteProvider(provider.name)">删除</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="provider-details">
|
<div class="provider-details">
|
||||||
@@ -409,22 +411,23 @@ onMounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="providers.length === 0" class="empty-state">
|
<div v-if="providers.length === 0" class="empty">暂无配置,点击上方按钮添加第一个 AI 提供商配置。</div>
|
||||||
<p>暂无配置,点击上方按钮添加第一个 AI 提供商配置。</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Tool Router 配置 -->
|
<!-- Tool Router 配置 -->
|
||||||
<div class="admin-section">
|
<div class="panel">
|
||||||
<div class="section-header">
|
<div class="panel-header">
|
||||||
<div>
|
<div>
|
||||||
<h3>工具路由配置</h3>
|
<p class="eyebrow">Tool Router</p>
|
||||||
<p class="section-desc">配置 LLM 工具调用的路由设置,用于实现函数调用功能。</p>
|
<h2>工具路由配置</h2>
|
||||||
</div>
|
</div>
|
||||||
<button v-if="!editingToolRouter" class="admin-button" @click="openEditToolRouter">编辑配置</button>
|
<button v-if="!editingToolRouter" class="admin-button" @click="openEditToolRouter">编辑配置</button>
|
||||||
</div>
|
</div>
|
||||||
|
<p class="section-desc">配置 LLM 工具调用的路由设置,用于实现函数调用功能。</p>
|
||||||
|
|
||||||
|
<div class="section-body">
|
||||||
<div v-if="editingToolRouter" class="tool-router-form">
|
<div v-if="editingToolRouter" class="tool-router-form">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>
|
<label>
|
||||||
@@ -462,7 +465,7 @@ onMounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
<button class="admin-button admin-button-secondary" @click="closeToolRouterForm">取消</button>
|
<button class="admin-button ghost" @click="closeToolRouterForm">取消</button>
|
||||||
<button class="admin-button" @click="saveToolRouter">保存</button>
|
<button class="admin-button" @click="saveToolRouter">保存</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -493,21 +496,22 @@ onMounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else class="empty-state">
|
<div v-else class="empty">暂无工具路由配置,点击上方按钮进行配置。</div>
|
||||||
<p>暂无工具路由配置,点击上方按钮进行配置。</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Topic Config 配置 - 话题选择配置 -->
|
<!-- Topic Config 配置 - 话题选择配置 -->
|
||||||
<div class="admin-section">
|
<div class="panel">
|
||||||
<div class="section-header">
|
<div class="panel-header">
|
||||||
<div>
|
<div>
|
||||||
<h3>话题选择配置</h3>
|
<p class="eyebrow">Topic Filter</p>
|
||||||
<p class="section-desc">当工具路由未命中任何工具时,由话题选择判断是否回复。命中(输出 REPLY)才进入主回复,否则丢弃不回复。</p>
|
<h2>话题选择配置</h2>
|
||||||
</div>
|
</div>
|
||||||
<button v-if="!editingTopicConfig" class="admin-button" @click="openEditTopicConfig">编辑配置</button>
|
<button v-if="!editingTopicConfig" class="admin-button" @click="openEditTopicConfig">编辑配置</button>
|
||||||
</div>
|
</div>
|
||||||
|
<p class="section-desc">当工具路由未命中任何工具时,由话题选择判断是否回复。命中(输出 REPLY)才进入主回复,否则丢弃不回复。</p>
|
||||||
|
|
||||||
|
<div class="section-body">
|
||||||
<div v-if="editingTopicConfig" class="tool-router-form">
|
<div v-if="editingTopicConfig" class="tool-router-form">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>
|
<label>
|
||||||
@@ -546,7 +550,7 @@ onMounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
<button class="admin-button admin-button-secondary" @click="closeTopicConfigForm">取消</button>
|
<button class="admin-button ghost" @click="closeTopicConfigForm">取消</button>
|
||||||
<button class="admin-button" @click="saveTopicConfig">保存</button>
|
<button class="admin-button" @click="saveTopicConfig">保存</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -577,21 +581,22 @@ onMounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else class="empty-state">
|
<div v-else class="empty">暂无话题选择配置,点击上方按钮进行配置。</div>
|
||||||
<p>暂无话题选择配置,点击上方按钮进行配置。</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Primary AI Config 配置 - 主 AI 回复配置 -->
|
<!-- Primary AI Config 配置 - 主 AI 回复配置 -->
|
||||||
<div class="admin-section">
|
<div class="panel">
|
||||||
<div class="section-header">
|
<div class="panel-header">
|
||||||
<div>
|
<div>
|
||||||
<h3>主 AI 回复配置</h3>
|
<p class="eyebrow">Primary AI</p>
|
||||||
<p class="section-desc">配置机器人自动回复消息的核心 AI 设置。</p>
|
<h2>主 AI 回复配置</h2>
|
||||||
</div>
|
</div>
|
||||||
<button v-if="!editingPrimaryConfig" class="admin-button" @click="openEditPrimaryConfig">编辑配置</button>
|
<button v-if="!editingPrimaryConfig" class="admin-button" @click="openEditPrimaryConfig">编辑配置</button>
|
||||||
</div>
|
</div>
|
||||||
|
<p class="section-desc">配置机器人自动回复消息的核心 AI 设置。</p>
|
||||||
|
|
||||||
|
<div class="section-body">
|
||||||
<div v-if="editingPrimaryConfig" class="tool-router-form">
|
<div v-if="editingPrimaryConfig" class="tool-router-form">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>
|
<label>
|
||||||
@@ -637,7 +642,7 @@ onMounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
<button class="admin-button admin-button-secondary" @click="closePrimaryConfigForm">取消</button>
|
<button class="admin-button ghost" @click="closePrimaryConfigForm">取消</button>
|
||||||
<button class="admin-button" @click="savePrimaryConfig">保存</button>
|
<button class="admin-button" @click="savePrimaryConfig">保存</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -672,8 +677,7 @@ onMounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else class="empty-state">
|
<div v-else class="empty">暂无主 AI 回复配置,点击上方按钮进行配置。</div>
|
||||||
<p>暂无主 AI 回复配置,点击上方按钮进行配置。</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -731,40 +735,20 @@ onMounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button class="admin-button admin-button-secondary" @click="closeProviderForm">取消</button>
|
<button class="admin-button ghost" @click="closeProviderForm">取消</button>
|
||||||
<button class="admin-button" @click="saveProvider">保存</button>
|
<button class="admin-button" @click="saveProvider">保存</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.admin-llm-api {
|
|
||||||
padding: 1.5rem;
|
|
||||||
max-width: 100%;
|
|
||||||
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
|
|
||||||
min-height: 100vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-llm-api h2 {
|
|
||||||
margin: 0 0 1rem;
|
|
||||||
font-size: 1.75rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #1e293b;
|
|
||||||
letter-spacing: -0.02em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ai-status-bar {
|
.ai-status-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.75rem;
|
gap: 12px;
|
||||||
padding: 0.75rem 1.25rem;
|
padding: 14px 16px;
|
||||||
background: white;
|
|
||||||
border-radius: 12px;
|
|
||||||
border: 1px solid #e2e8f0;
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-indicator {
|
.status-indicator {
|
||||||
@@ -775,110 +759,94 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.status-indicator.running {
|
.status-indicator.running {
|
||||||
background: #22c55e;
|
background: var(--color-success);
|
||||||
box-shadow: 0 0 6px rgba(34, 197, 94, 0.5);
|
box-shadow: 0 0 6px color-mix(in srgb, var(--color-success) 60%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-indicator.stopped {
|
.status-indicator.stopped {
|
||||||
background: #ef4444;
|
background: var(--color-danger);
|
||||||
box-shadow: 0 0 6px rgba(239, 68, 68, 0.4);
|
box-shadow: 0 0 6px color-mix(in srgb, var(--color-danger) 50%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-text {
|
.status-text {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
font-size: 0.9rem;
|
color: var(--color-text);
|
||||||
color: #475569;
|
font-size: 14px;
|
||||||
font-weight: 500;
|
font-weight: 700;
|
||||||
}
|
|
||||||
|
|
||||||
.admin-section {
|
|
||||||
background: white;
|
|
||||||
padding: 1.75rem;
|
|
||||||
border-radius: 16px;
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05), 0 4px 6px rgba(0, 0, 0, 0.03);
|
|
||||||
border: 1px solid #e2e8f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: flex-start;
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
padding-bottom: 1rem;
|
|
||||||
border-bottom: 1px solid #e2e8f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-header h3 {
|
|
||||||
margin: 0 0 0.5rem;
|
|
||||||
font-size: 1.25rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #334155;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-desc {
|
.section-desc {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: #64748b;
|
padding: 12px 16px 0;
|
||||||
font-size: 0.9rem;
|
color: var(--color-muted);
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-body {
|
||||||
|
padding: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.provider-grid {
|
.provider-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(380px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
|
||||||
gap: 1.25rem;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.provider-card {
|
.provider-card {
|
||||||
background: linear-gradient(135deg, #fafbfc 0%, #f8fafc 100%);
|
border: 1px solid var(--color-border);
|
||||||
border: 1px solid #e2e8f0;
|
border-radius: var(--radius-md);
|
||||||
border-radius: 12px;
|
padding: 14px;
|
||||||
padding: 1.25rem;
|
background: var(--color-surface-soft);
|
||||||
transition: all 0.2s ease;
|
transition: border-color 0.16s ease, box-shadow 0.16s ease, transform 0.16s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.provider-card:hover {
|
.provider-card:hover {
|
||||||
transform: translateY(-2px);
|
border-color: var(--color-primary);
|
||||||
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.08);
|
box-shadow: var(--shadow-sm);
|
||||||
border-color: #cbd5e1;
|
transform: translateY(-1px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.provider-header {
|
.provider-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 1rem;
|
flex-wrap: wrap;
|
||||||
padding-bottom: 0.75rem;
|
gap: 8px;
|
||||||
border-bottom: 1px solid #e2e8f0;
|
margin-bottom: 10px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.provider-name {
|
.provider-name {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.75rem;
|
gap: 8px;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.provider-name strong {
|
.provider-name strong {
|
||||||
font-size: 1.05rem;
|
color: var(--color-heading);
|
||||||
color: #1e293b;
|
font-size: 15px;
|
||||||
font-weight: 600;
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
|
|
||||||
.provider-actions {
|
.provider-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.5rem;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.provider-details {
|
.provider-details {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.5rem;
|
gap: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-row {
|
.detail-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: baseline;
|
||||||
gap: 1rem;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-row.full-width {
|
.detail-row.full-width {
|
||||||
@@ -887,70 +855,68 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.detail-label {
|
.detail-label {
|
||||||
font-size: 0.85rem;
|
flex-shrink: 0;
|
||||||
color: #64748b;
|
color: var(--color-muted);
|
||||||
font-weight: 500;
|
font-size: 12px;
|
||||||
white-space: nowrap;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-value {
|
.detail-value {
|
||||||
font-size: 0.9rem;
|
color: var(--color-heading);
|
||||||
color: #334155;
|
font-size: 13px;
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-value.masked {
|
.detail-value.masked {
|
||||||
font-family: monospace;
|
font-family: var(--font-mono);
|
||||||
letter-spacing: 0.1em;
|
letter-spacing: 0.1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-badge {
|
.status-badge {
|
||||||
display: inline-block;
|
display: inline-flex;
|
||||||
padding: 0.25rem 0.6rem;
|
align-items: center;
|
||||||
border-radius: 20px;
|
border-radius: 999px;
|
||||||
font-size: 0.75rem;
|
padding: 4px 10px;
|
||||||
font-weight: 600;
|
font-size: 12px;
|
||||||
text-transform: uppercase;
|
font-weight: 700;
|
||||||
letter-spacing: 0.02em;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-badge.active {
|
.status-badge.active {
|
||||||
background: linear-gradient(135deg, #dcfce7 0%, #bbf7d0 100%);
|
color: color-mix(in srgb, var(--color-success) 70%, var(--color-heading));
|
||||||
color: #166534;
|
background: var(--color-success-soft);
|
||||||
border: 1px solid #86efac;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-badge.inactive {
|
.status-badge.inactive {
|
||||||
background: linear-gradient(135deg, #fee2e2 0%, #fecaca 100%);
|
color: color-mix(in srgb, var(--color-danger) 76%, var(--color-heading));
|
||||||
color: #991b1b;
|
background: var(--color-danger-soft);
|
||||||
border: 1px solid #fca5a5;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tool-router-form,
|
.tool-router-form,
|
||||||
.tool-router-display {
|
.tool-router-display {
|
||||||
padding: 1.25rem;
|
border: 1px solid var(--color-border);
|
||||||
background: #f8fafc;
|
border-radius: var(--radius-md);
|
||||||
border-radius: 12px;
|
padding: 16px;
|
||||||
border: 1px solid #e2e8f0;
|
background: var(--color-surface-soft);
|
||||||
}
|
}
|
||||||
|
|
||||||
.router-status {
|
.router-status {
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 12px;
|
||||||
padding-bottom: 1rem;
|
padding-bottom: 12px;
|
||||||
border-bottom: 1px solid #e2e8f0;
|
border-bottom: 1px solid var(--color-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.router-details {
|
.router-details {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
gap: 1rem;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.router-details .detail-row {
|
.router-details .detail-row {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
gap: 0.25rem;
|
gap: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.router-details .detail-row .detail-value {
|
.router-details .detail-row .detail-value {
|
||||||
@@ -959,64 +925,64 @@ onMounted(() => {
|
|||||||
|
|
||||||
.system-prompt {
|
.system-prompt {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
background: white;
|
border: 1px solid var(--color-border);
|
||||||
padding: 0.75rem;
|
border-radius: var(--radius-sm);
|
||||||
border-radius: 8px;
|
padding: 10px 12px;
|
||||||
border: 1px solid #e2e8f0;
|
color: var(--color-text);
|
||||||
font-size: 0.85rem;
|
background: var(--color-surface);
|
||||||
|
font-size: 13px;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
margin: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-group {
|
.form-group {
|
||||||
margin-bottom: 1.25rem;
|
margin-bottom: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-group label {
|
.form-group label {
|
||||||
display: block;
|
display: block;
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 6px;
|
||||||
font-weight: 500;
|
color: var(--color-text);
|
||||||
color: #334155;
|
font-size: 13px;
|
||||||
font-size: 0.9rem;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-group label input[type='checkbox'] {
|
.form-group label input[type='checkbox'] {
|
||||||
margin-right: 0.5rem;
|
margin-right: 8px;
|
||||||
width: auto;
|
width: auto;
|
||||||
|
accent-color: var(--color-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-row {
|
.form-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
gap: 1rem;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-input,
|
.form-input,
|
||||||
.form-textarea,
|
.form-textarea {
|
||||||
.form-select {
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 0.75rem 1rem;
|
|
||||||
border: 1px solid #cbd5e1;
|
|
||||||
border-radius: 8px;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
color: #334155;
|
|
||||||
background: white;
|
|
||||||
transition: all 0.15s ease;
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
border: 1px solid var(--color-border-strong);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
padding: 9px 12px;
|
||||||
|
color: var(--color-heading);
|
||||||
|
background: var(--color-surface);
|
||||||
|
font-size: 14px;
|
||||||
|
outline: none;
|
||||||
|
transition: border-color 0.16s ease, box-shadow 0.16s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-input:focus,
|
.form-input:focus,
|
||||||
.form-textarea:focus,
|
.form-textarea:focus {
|
||||||
.form-select:focus {
|
border-color: var(--color-primary);
|
||||||
outline: none;
|
box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-primary) 20%, transparent);
|
||||||
border-color: #3b82f6;
|
|
||||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-input:disabled {
|
.form-input:disabled {
|
||||||
background: #f1f5f9;
|
background: var(--color-surface-muted);
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1027,197 +993,102 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.form-hint {
|
.form-hint {
|
||||||
margin: 0.5rem 0 0;
|
margin: 6px 0 0;
|
||||||
font-size: 0.8rem;
|
color: var(--color-muted);
|
||||||
color: #64748b;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-actions {
|
.form-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
gap: 0.75rem;
|
gap: 10px;
|
||||||
margin-top: 1rem;
|
margin-top: 12px;
|
||||||
padding-top: 1rem;
|
padding-top: 12px;
|
||||||
border-top: 1px solid #e2e8f0;
|
border-top: 1px solid var(--color-border);
|
||||||
}
|
|
||||||
|
|
||||||
.error {
|
|
||||||
color: #991b1b;
|
|
||||||
padding: 1rem 1.25rem;
|
|
||||||
background: linear-gradient(135deg, #fee2e2 0%, #fecaca 100%);
|
|
||||||
border-radius: 10px;
|
|
||||||
margin-bottom: 1.25rem;
|
|
||||||
border: 1px solid #fca5a5;
|
|
||||||
font-weight: 500;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.success {
|
|
||||||
color: #166534;
|
|
||||||
padding: 1rem 1.25rem;
|
|
||||||
background: linear-gradient(135deg, #dcfce7 0%, #bbf7d0 100%);
|
|
||||||
border-radius: 10px;
|
|
||||||
margin-bottom: 1.25rem;
|
|
||||||
border: 1px solid #86efac;
|
|
||||||
font-weight: 500;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.warning {
|
.warning {
|
||||||
color: #92400e;
|
margin: 0 16px 12px;
|
||||||
padding: 1rem 1.25rem;
|
border: 1px solid color-mix(in srgb, var(--color-warning) 36%, white);
|
||||||
background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
|
border-radius: var(--radius-md);
|
||||||
border-radius: 10px;
|
padding: 10px 12px;
|
||||||
margin-bottom: 1.25rem;
|
color: color-mix(in srgb, var(--color-warning) 72%, var(--color-heading));
|
||||||
border: 1px solid #fcd34d;
|
background: var(--color-warning-soft);
|
||||||
font-weight: 500;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-loading {
|
|
||||||
padding: 3rem;
|
|
||||||
text-align: center;
|
|
||||||
color: #64748b;
|
|
||||||
background: #f8fafc;
|
|
||||||
border-radius: 12px;
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-state {
|
|
||||||
padding: 3rem 2rem;
|
|
||||||
text-align: center;
|
|
||||||
color: #64748b;
|
|
||||||
background: #f8fafc;
|
|
||||||
border-radius: 12px;
|
|
||||||
border: 2px dashed #e2e8f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-state p {
|
|
||||||
margin: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 弹窗样式 */
|
/* 弹窗样式 */
|
||||||
.modal-overlay {
|
.modal-overlay {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
inset: 0;
|
||||||
left: 0;
|
z-index: 1000;
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background: rgba(0, 0, 0, 0.5);
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
z-index: 1000;
|
padding: 16px;
|
||||||
padding: 1rem;
|
background: rgba(36, 41, 39, 0.45);
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-content {
|
.modal-content {
|
||||||
background: white;
|
|
||||||
border-radius: 16px;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 550px;
|
max-width: 550px;
|
||||||
max-height: 90vh;
|
max-height: 90vh;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
background: var(--color-surface);
|
||||||
|
box-shadow: var(--shadow-floating);
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-header {
|
.modal-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 1.5rem 1.5rem 1rem;
|
padding: 14px 16px;
|
||||||
border-bottom: 1px solid #e2e8f0;
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
background: color-mix(in srgb, var(--color-surface) 92%, var(--color-bg));
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-header h3 {
|
.modal-header h3 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 1.25rem;
|
color: var(--color-heading);
|
||||||
font-weight: 600;
|
font-size: 16px;
|
||||||
color: #1e293b;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-close {
|
.modal-close {
|
||||||
background: none;
|
|
||||||
border: none;
|
border: none;
|
||||||
font-size: 1.5rem;
|
border-radius: var(--radius-sm);
|
||||||
cursor: pointer;
|
padding: 4px 8px;
|
||||||
color: #64748b;
|
color: var(--color-muted);
|
||||||
padding: 0.25rem 0.5rem;
|
background: none;
|
||||||
|
font-size: 20px;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
border-radius: 6px;
|
cursor: pointer;
|
||||||
transition: all 0.15s ease;
|
transition: background-color 0.16s ease, color 0.16s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-close:hover {
|
.modal-close:hover {
|
||||||
background: #f1f5f9;
|
color: var(--color-heading);
|
||||||
color: #1e293b;
|
background: var(--color-surface-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-body {
|
.modal-body {
|
||||||
padding: 1.5rem;
|
padding: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-footer {
|
.modal-footer {
|
||||||
padding: 1rem 1.5rem 1.5rem;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
gap: 0.75rem;
|
gap: 10px;
|
||||||
|
padding: 12px 16px 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-button {
|
@media (max-width: 640px) {
|
||||||
padding: 0.6rem 1.25rem;
|
.form-row {
|
||||||
border: none;
|
grid-template-columns: 1fr;
|
||||||
border-radius: 8px;
|
}
|
||||||
font-size: 0.9rem;
|
|
||||||
font-weight: 500;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.15s ease;
|
|
||||||
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
|
|
||||||
color: white;
|
|
||||||
box-shadow: 0 2px 4px rgba(59, 130, 246, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-button:hover:not(:disabled) {
|
.provider-grid {
|
||||||
transform: translateY(-1px);
|
grid-template-columns: 1fr;
|
||||||
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.admin-button:active:not(:disabled) {
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-button-small {
|
|
||||||
padding: 0.4rem 0.75rem;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-button-secondary {
|
|
||||||
background: linear-gradient(135deg, #64748b 0%, #475569 100%);
|
|
||||||
box-shadow: 0 2px 4px rgba(100, 116, 139, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-button-secondary:hover:not(:disabled) {
|
|
||||||
box-shadow: 0 4px 12px rgba(100, 116, 139, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-button-danger {
|
|
||||||
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
|
|
||||||
box-shadow: 0 2px 4px rgba(239, 68, 68, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-button-danger:hover:not(:disabled) {
|
|
||||||
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
button:disabled {
|
|
||||||
opacity: 0.5;
|
|
||||||
cursor: not-allowed;
|
|
||||||
transform: none !important;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -49,13 +49,13 @@ onMounted(refreshLogs)
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p v-if="error" class="error">{{ error }}</p>
|
<p v-if="error" class="error">{{ error }}</p>
|
||||||
<div class="node-table-wrap">
|
<div class="node-table-wrap table-fit-wrap">
|
||||||
<table class="node-table">
|
<table class="node-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>时间</th>
|
<th class="cell-nowrap">时间</th>
|
||||||
<th>用户名</th>
|
<th>用户名</th>
|
||||||
<th>结果</th>
|
<th class="cell-nowrap">结果</th>
|
||||||
<th>原因</th>
|
<th>原因</th>
|
||||||
<th>Remote Addr</th>
|
<th>Remote Addr</th>
|
||||||
<th>Remote Host</th>
|
<th>Remote Host</th>
|
||||||
@@ -64,9 +64,9 @@ onMounted(refreshLogs)
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="log in logs" :key="log.id">
|
<tr v-for="log in logs" :key="log.id">
|
||||||
<td>{{ formatTime(log.created_at) }}</td>
|
<td class="cell-nowrap">{{ formatTime(log.created_at) }}</td>
|
||||||
<td>{{ log.username || '-' }}</td>
|
<td>{{ log.username || '-' }}</td>
|
||||||
<td>
|
<td class="cell-nowrap">
|
||||||
<span class="log-badge" :class="log.success ? 'log-success' : 'log-failure'">
|
<span class="log-badge" :class="log.success ? 'log-success' : 'log-failure'">
|
||||||
{{ log.success ? '成功' : '失败' }}
|
{{ log.success ? '成功' : '失败' }}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -572,8 +572,7 @@ onBeforeUnmount(() => {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.mqtt-forward-page {
|
.mqtt-forward-page {
|
||||||
width: min(1440px, 100%);
|
width: 100%;
|
||||||
margin: 0 auto;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
|
|||||||
@@ -212,41 +212,41 @@ onMounted(() => refreshSigns())
|
|||||||
<p v-if="error" class="error">{{ error }}</p>
|
<p v-if="error" class="error">{{ error }}</p>
|
||||||
<p v-if="message" class="success">{{ message }}</p>
|
<p v-if="message" class="success">{{ message }}</p>
|
||||||
|
|
||||||
<div class="node-table-wrap">
|
<div class="node-table-wrap table-fit-wrap">
|
||||||
<table class="node-table">
|
<table class="node-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>ID</th>
|
<th class="cell-nowrap">ID</th>
|
||||||
<th>节点 ID</th>
|
<th class="cell-nowrap">节点 ID</th>
|
||||||
<th>Long Name</th>
|
<th>Long Name</th>
|
||||||
<th>Short Name</th>
|
<th class="cell-nowrap">Short Name</th>
|
||||||
<th>签到文本</th>
|
<th>签到文本</th>
|
||||||
<th>签到时间</th>
|
<th class="cell-nowrap">签到时间</th>
|
||||||
<th>操作</th>
|
<th class="cell-nowrap">操作</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="record in records" :key="record.id">
|
<tr v-for="record in records" :key="record.id">
|
||||||
<template v-if="edits[record.id]">
|
<template v-if="edits[record.id]">
|
||||||
<td>{{ record.id }}</td>
|
<td class="cell-nowrap">{{ record.id }}</td>
|
||||||
<td><input v-model="edits[record.id].node_id" class="admin-table-input" /></td>
|
<td><input v-model="edits[record.id].node_id" class="admin-table-input" /></td>
|
||||||
<td><input v-model="edits[record.id].long_name" class="admin-table-input" /></td>
|
<td><input v-model="edits[record.id].long_name" class="admin-table-input" /></td>
|
||||||
<td><input v-model="edits[record.id].short_name" class="admin-table-input" /></td>
|
<td><input v-model="edits[record.id].short_name" class="admin-table-input" /></td>
|
||||||
<td><input v-model="edits[record.id].sign_text" class="admin-table-input" /></td>
|
<td><input v-model="edits[record.id].sign_text" class="admin-table-input" /></td>
|
||||||
<td><input v-model="edits[record.id].sign_time" class="admin-table-input" type="datetime-local" /></td>
|
<td><input v-model="edits[record.id].sign_time" class="admin-table-input" type="datetime-local" /></td>
|
||||||
<td>
|
<td class="cell-nowrap">
|
||||||
<button class="admin-button" :disabled="loading" @click="saveSign(record)">保存</button>
|
<button class="admin-button" :disabled="loading" @click="saveSign(record)">保存</button>
|
||||||
<button class="admin-button" :disabled="loading" @click="cancelEdit(record.id)">取消</button>
|
<button class="admin-button" :disabled="loading" @click="cancelEdit(record.id)">取消</button>
|
||||||
</td>
|
</td>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<td>{{ record.id }}</td>
|
<td class="cell-nowrap">{{ record.id }}</td>
|
||||||
<td>{{ record.node_id }}</td>
|
<td class="cell-nowrap">{{ record.node_id }}</td>
|
||||||
<td>{{ record.long_name || '-' }}</td>
|
<td>{{ record.long_name || '-' }}</td>
|
||||||
<td>{{ record.short_name || '-' }}</td>
|
<td class="cell-nowrap">{{ record.short_name || '-' }}</td>
|
||||||
<td>{{ record.sign_text }}</td>
|
<td>{{ record.sign_text }}</td>
|
||||||
<td>{{ formatTime(record.sign_time) }}</td>
|
<td class="cell-nowrap">{{ formatTime(record.sign_time) }}</td>
|
||||||
<td>
|
<td class="cell-nowrap">
|
||||||
<button class="admin-button" :disabled="loading" @click="startEdit(record)">编辑</button>
|
<button class="admin-button" :disabled="loading" @click="startEdit(record)">编辑</button>
|
||||||
<button class="admin-button danger" :disabled="loading" @click="removeSign(record)">删除</button>
|
<button class="admin-button danger" :disabled="loading" @click="removeSign(record)">删除</button>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -119,26 +119,26 @@ onMounted(refreshUsers)
|
|||||||
<p v-if="userError" class="error">{{ userError }}</p>
|
<p v-if="userError" class="error">{{ userError }}</p>
|
||||||
<p v-if="userMessage" class="success">{{ userMessage }}</p>
|
<p v-if="userMessage" class="success">{{ userMessage }}</p>
|
||||||
|
|
||||||
<div class="node-table-wrap">
|
<div class="node-table-wrap table-fit-wrap">
|
||||||
<table class="node-table">
|
<table class="node-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>ID</th>
|
<th class="cell-nowrap">ID</th>
|
||||||
<th>用户名</th>
|
<th>用户名</th>
|
||||||
<th>角色</th>
|
<th class="cell-nowrap">角色</th>
|
||||||
<th>创建时间</th>
|
<th class="cell-nowrap">创建时间</th>
|
||||||
<th>更新时间</th>
|
<th class="cell-nowrap">更新时间</th>
|
||||||
<th>新密码</th>
|
<th>新密码</th>
|
||||||
<th>操作</th>
|
<th class="cell-nowrap">操作</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="managedUser in users" :key="managedUser.id">
|
<tr v-for="managedUser in users" :key="managedUser.id">
|
||||||
<td>{{ managedUser.id }}</td>
|
<td class="cell-nowrap">{{ managedUser.id }}</td>
|
||||||
<td>{{ managedUser.username }} <span v-if="managedUser.username === props.user.username" class="badge">当前</span></td>
|
<td>{{ managedUser.username }} <span v-if="managedUser.username === props.user.username" class="badge">当前</span></td>
|
||||||
<td>{{ managedUser.role }}</td>
|
<td class="cell-nowrap">{{ managedUser.role }}</td>
|
||||||
<td>{{ formatTime(managedUser.created_at) }}</td>
|
<td class="cell-nowrap">{{ formatTime(managedUser.created_at) }}</td>
|
||||||
<td>{{ formatTime(managedUser.updated_at) }}</td>
|
<td class="cell-nowrap">{{ formatTime(managedUser.updated_at) }}</td>
|
||||||
<td>
|
<td>
|
||||||
<input
|
<input
|
||||||
v-model="passwordEdits[managedUser.id]"
|
v-model="passwordEdits[managedUser.id]"
|
||||||
@@ -148,7 +148,7 @@ onMounted(refreshUsers)
|
|||||||
placeholder="输入新密码"
|
placeholder="输入新密码"
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td class="cell-nowrap">
|
||||||
<button class="admin-button" :disabled="passwordSaving[managedUser.id]" @click="updatePassword(managedUser)">
|
<button class="admin-button" :disabled="passwordSaving[managedUser.id]" @click="updatePassword(managedUser)">
|
||||||
{{ passwordSaving[managedUser.id] ? '保存中...' : '修改密码' }}
|
{{ passwordSaving[managedUser.id] ? '保存中...' : '修改密码' }}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -1225,6 +1225,25 @@ h3 {
|
|||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* === 管理页表格:长文本列换行完整显示,短列保持单行 === */
|
||||||
|
.table-fit-wrap {
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-fit-wrap .node-table th,
|
||||||
|
.table-fit-wrap .node-table td {
|
||||||
|
white-space: normal;
|
||||||
|
word-break: break-all;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-fit-wrap .node-table th.cell-nowrap,
|
||||||
|
.table-fit-wrap .node-table td.cell-nowrap {
|
||||||
|
white-space: nowrap;
|
||||||
|
word-break: normal;
|
||||||
|
}
|
||||||
|
|
||||||
.pubkey-cell {
|
.pubkey-cell {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -1259,10 +1278,13 @@ h3 {
|
|||||||
color: var(--color-muted);
|
color: var(--color-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-login,
|
|
||||||
.admin-dashboard,
|
.admin-dashboard,
|
||||||
.admin-session-card {
|
.admin-session-card {
|
||||||
max-width: 1100px;
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-login {
|
||||||
|
max-width: 480px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
@@ -1374,6 +1396,12 @@ h3 {
|
|||||||
padding: 8px 10px;
|
padding: 8px 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.table-fit-wrap .admin-table-input {
|
||||||
|
min-width: 0;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
.success {
|
.success {
|
||||||
margin: 0 16px 12px;
|
margin: 0 16px 12px;
|
||||||
border: 1px solid color-mix(in srgb, var(--color-success) 34%, white);
|
border: 1px solid color-mix(in srgb, var(--color-success) 34%, white);
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
export const FRONTEND_VERSION = '0.3.1'
|
export const FRONTEND_VERSION = '0.3.2'
|
||||||
Reference in New Issue
Block a user