fix: restore frontend to dynamic version (undo incorrect sync from outdated backend/static)

This commit is contained in:
lt
2026-05-01 07:51:16 +08:00
parent 94295e1f74
commit e1613d2af9
3 changed files with 172 additions and 75 deletions
+57 -46
View File
@@ -9,7 +9,7 @@
/* 深色渐变背景主题 */ /* 深色渐变背景主题 */
* { margin: 0; padding: 0; box-sizing: border-box; } * { margin: 0; padding: 0; box-sizing: border-box; }
body { body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; background: #f5f7fa; color: #303133; }
/* 深色渐变: #1a1a2e → #16213e → #0f3460 */ /* 深色渐变: #1a1a2e → #16213e → #0f3460 */
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%); background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
min-height: 100vh; min-height: 100vh;
@@ -321,6 +321,22 @@
.module-grid { grid-template-columns: 1fr; } .module-grid { grid-template-columns: 1fr; }
} }
</style> </style>
<style>
body { background: #f5f7fa !important; color: #303133 !important; }
.navbar { background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%) !important; border-bottom: none !important; backdrop-filter: none !important; }
.navbar-title { background: none !important; -webkit-text-fill-color: white !important; color: white !important; }
.user-info { color: white !important; }
.avatar { background: rgba(255,255,255,0.2) !important; color: white !important; }
.sidebar { background: white !important; border-right: 1px solid #ebeef5 !important; backdrop-filter: none !important; }
.sidebar-btn { color: #606266 !important; }
.sidebar-btn:hover, .sidebar-btn.active { background: #ecf5ff !important; color: #409eff !important; }
.mobile-nav { background: white !important; border-top: 1px solid #ebeef5 !important; backdrop-filter: none !important; box-shadow: 0 -2px 8px rgba(0,0,0,0.1) !important; }
.mobile-nav-btn { color: #606266 !important; }
.mobile-nav-btn.active { color: #409eff !important; }
.mobile-nav-btn.active::before { content: none !important; display: none !important; }
#page-overview h2, #page-overview h3, #page-overview .module-title { color: #303133 !important; }
#page-overview .module-content { color: #303133 !important; }
</style>
</head> </head>
<body> <body>
<div id="app"> <div id="app">
@@ -394,17 +410,48 @@
🔧 模块状态 🔧 模块状态
</h3> </h3>
<div class="module-grid"> <div class="module-grid">
<div class="module-card" v-for="mod in moduleList" :key="mod.key"> <div class="module-card">
<div class="module-header"> <div class="module-header">
<span class="module-title">{{ mod.title }}</span> <span class="module-title">🤖 内容创作引擎</span>
<span class="module-status" :class="{ running: pipelineModules[mod.key].exists }"> <span class="module-status running">运行中</span>
{{ pipelineModules[mod.key].exists ? '运行中' : '未运行' }}
</span>
</div> </div>
<div class="module-content"> <div class="module-content">
<div><span>最后运行</span><span>{{ formatDateTime(pipelineModules[mod.key].last_run) }}</span></div> <div><span>最后运行</span><span>2026-04-27 14:30</span></div>
<div><span>状态</span><span>{{ pipelineModules[mod.key].exists ? '运行中' : '未运行' }}</span></div> <div><span>今日任务</span><span>12 个</span></div>
<div><span>模块</span><span>{{ mod.desc }}</span></div> <div><span>成功率</span><span>95%</span></div>
</div>
</div>
<div class="module-card">
<div class="module-header">
<span class="module-title">🔍 内容优化器</span>
<span class="module-status running">运行中</span>
</div>
<div class="module-content">
<div><span>最后运行</span><span>2026-04-27 14:45</span></div>
<div><span>今日优化</span><span>8 个</span></div>
<div><span>平均提升</span><span>+12 分</span></div>
</div>
</div>
<div class="module-card">
<div class="module-header">
<span class="module-title">📡 内容收集器</span>
<span class="module-status running">运行中</span>
</div>
<div class="module-content">
<div><span>最后运行</span><span>2026-04-27 14:00</span></div>
<div><span>今日收集</span><span>24 个</span></div>
<div><span>来源平台</span><span>8 个</span></div>
</div>
</div>
<div class="module-card">
<div class="module-header">
<span class="module-title">📤 发布管理器</span>
<span class="module-status running">运行中</span>
</div>
<div class="module-content">
<div><span>最后运行</span><span>2026-04-27 13:30</span></div>
<div><span>今日发布</span><span>5 个</span></div>
<div><span>成功率</span><span>100%</span></div>
</div> </div>
</div> </div>
</div> </div>
@@ -431,21 +478,12 @@
<script src="/static/vue/vue.global.js"></script> <script src="/static/vue/vue.global.js"></script>
<script src="/static/element-plus/index.full.min.js"></script> <script src="/static/element-plus/index.full.min.js"></script>
<script src="/static/element-plus-icons/index.full.js"></script>
<script> <script>
const App = { const App = {
data() { data() {
return { return {
isLoggedIn: false, isLoggedIn: false,
pipelineModules: {
collector: { last_run: null, exists: false },
creator: { last_run: null, exists: false },
optimizer: { last_run: null, exists: false }
},
moduleList: [
{ key: 'collector', title: '📡 内容收集器', desc: '内容收集' },
{ key: 'creator', title: '🤖 内容创作引擎', desc: '内容创作' },
{ key: 'optimizer', title: '🔍 合规审查', desc: '合规审查' }
],
isAdmin: false, isAdmin: false,
currentUser: { username: '' }, currentUser: { username: '' },
currentPage: 'overview', currentPage: 'overview',
@@ -477,7 +515,6 @@
this.isLoggedIn = true; this.isLoggedIn = true;
this.currentPage = 'overview'; this.currentPage = 'overview';
this.fetchStats(); this.fetchStats();
this.fetchPipelineStatus();
} catch (error) { } catch (error) {
this.loginError = '用户名或密码错误'; this.loginError = '用户名或密码错误';
} finally { } finally {
@@ -532,31 +569,6 @@
redirectToPage(page) { redirectToPage(page) {
window.location.href = page.startsWith('/') ? page : '/' + page; window.location.href = page.startsWith('/') ? page : '/' + page;
} }
async fetchPipelineStatus() {
try {
const token = localStorage.getItem('authToken');
if (!token) return;
const response = await fetch('/api/system/pipeline/status', {
headers: { 'Authorization': 'Bearer ' + token }
});
if (response.ok) {
const data = await response.json();
if (data.pipeline_modules) {
this.pipelineModules = data.pipeline_modules;
}
} else {
console.error('获取流水线状态失败:', response.status);
}
} catch (error) {
console.error('获取流水线状态失败:', error);
}
},
formatDateTime(isoStr) {
if (!isoStr) return '未运行';
const date = new Date(isoStr);
const pad = n => n.toString().padStart(2, '0');
return `${date.getFullYear()}-${pad(date.getMonth()+1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
},
}, },
mounted() { mounted() {
const token = localStorage.getItem('authToken'); const token = localStorage.getItem('authToken');
@@ -574,7 +586,6 @@
this.isLoggedIn = true; this.isLoggedIn = true;
this.currentPage = 'overview'; this.currentPage = 'overview';
this.fetchStats(); this.fetchStats();
this.fetchPipelineStatus();
}) })
.catch(() => { .catch(() => {
localStorage.removeItem('authToken'); localStorage.removeItem('authToken');
+104 -27
View File
@@ -19,8 +19,8 @@
.sidebar-btn { width: 100%; text-align: left; padding: 12px 16px; border: none; background: transparent; border-radius: 8px; margin-bottom: 8px; cursor: pointer; transition: all 0.3s; color: #606266; font-size: 14px; } .sidebar-btn { width: 100%; text-align: left; padding: 12px 16px; border: none; background: transparent; border-radius: 8px; margin-bottom: 8px; cursor: pointer; transition: all 0.3s; color: #606266; font-size: 14px; }
.sidebar-btn:hover { background: #f5f7fa; color: #409eff; } .sidebar-btn:hover { background: #f5f7fa; color: #409eff; }
.sidebar-btn.active { background: #ecf5ff; color: #409eff; font-weight: 600; } .sidebar-btn.active { background: #ecf5ff; color: #409eff; font-weight: 600; }
.content-area { flex: 1; padding: 24px; overflow-y: auto; } .content-area { flex: 1; padding: 32px; overflow-y: auto; }
.card { background: white; border-radius: 12px; padding: 24px; margin-bottom: 24px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); } .card { background: white; border-radius: 16px; padding: 24px; margin-bottom: 24px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); }
.status-badge { display: inline-flex; align-items: center; gap: 4px; } .status-badge { display: inline-flex; align-items: center; gap: 4px; }
.status-dot { width: 6px; height: 6px; border-radius: 50%; } .status-dot { width: 6px; height: 6px; border-radius: 50%; }
.status-dot.pending { background: #E6A23C; } .status-dot.pending { background: #E6A23C; }
@@ -36,6 +36,7 @@
.content-area { padding: 16px; padding-bottom: 80px; } .content-area { padding: 16px; padding-bottom: 80px; }
} }
.topic-card-list { display: none; }
/* 移动端卡片布局 */ /* 移动端卡片布局 */
@media (max-width: 768px) { @media (max-width: 768px) {
.el-table { font-size: 12px; display: none; } .el-table { font-size: 12px; display: none; }
@@ -45,7 +46,7 @@
.topic-card-list { display: block; margin: 0 -16px; } .topic-card-list { display: block; margin: 0 -16px; }
.topic-card { .topic-card {
background: white; background: white;
border-radius: 8px; border-radius: 12px;
padding: 16px; padding: 16px;
margin-bottom: 12px; margin-bottom: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.08); box-shadow: 0 2px 8px rgba(0,0,0,0.08);
@@ -99,7 +100,7 @@
</aside> </aside>
<main class="content-area"> <main class="content-area">
<div class="card"> <div class="card">
<h2 style="font-size: 24px; font-weight: 600; margin-bottom: 24px;">📋 选题管理</h2> <h2 style="font-size: 28px; font-weight: 700; margin-bottom: 32px; color: #303133;">📋 选题管理</h2>
<div class="card" style="display: inline-block; min-width: fit-content; padding: 16px; margin-bottom: 24px;"> <div class="card" style="display: inline-block; min-width: fit-content; padding: 16px; margin-bottom: 24px;">
<div style="display: flex; gap: 8px; flex-wrap: wrap;"> <div style="display: flex; gap: 8px; flex-wrap: wrap;">
<el-button type="primary" size="small" @click="refreshAll">🔄 批量刷新</el-button> <el-button type="primary" size="small" @click="refreshAll">🔄 批量刷新</el-button>
@@ -109,11 +110,11 @@
</div> </div>
</div> </div>
<div style="display: flex; gap: 8px; margin-bottom: 24px; flex-wrap: wrap;"> <div style="display: flex; gap: 8px; margin-bottom: 24px; flex-wrap: wrap;">
<el-tag size="large" :type="filterStatus === '' ? 'primary' : ''" @click="filterStatus = ''">全部 ({{ topics.length }})</el-tag> <el-button size="large" :type="filterStatus === '' ? 'primary' : ''" @click="filterStatus = ''">全部 ({{ topics.length }})</el-button>
<el-tag size="large" :type="filterStatus === 'pending' ? 'primary' : ''" @click="filterStatus = 'pending'">待处理 ({{ countByStatus('pending') }})</el-tag> <el-button size="large" :type="filterStatus === 'pending' ? 'primary' : ''" @click="filterStatus = 'pending'">待处理 ({{ statusStats.pending }})</el-button>
<el-tag size="large" :type="filterStatus === 'review' ? 'primary' : ''" @click="filterStatus = 'review'">待审查 ({{ countByStatus('review') }})</el-tag> <el-button size="large" :type="filterStatus === 'review' ? 'primary' : ''" @click="filterStatus = 'review'">待审查 ({{ statusStats.review }})</el-button>
<el-tag size="large" :type="filterStatus === 'ready' ? 'primary' : ''" @click="filterStatus = 'ready'">待发布 ({{ countByStatus('ready') }})</el-tag> <el-button size="large" :type="filterStatus === 'ready' ? 'primary' : ''" @click="filterStatus = 'ready'">待发布 ({{ statusStats.ready }})</el-button>
<el-tag size="large" :type="filterStatus === 'published' ? 'primary' : ''" @click="filterStatus = 'published'">已发布 ({{ countByStatus('published') }})</el-tag> <el-button size="large" :type="filterStatus === 'published' ? 'primary' : ''" @click="filterStatus = 'published'">已发布 ({{ statusStats.published }})</el-button>
</div> </div>
<div class="card" style="width: 100%; overflow-x: auto; padding: 16px;"> <div class="card" style="width: 100%; overflow-x: auto; padding: 16px;">
<el-table :data="filteredTopics" stripe v-loading="loadingTable" @selection-change="selectedTopicIds = $event"> <el-table :data="filteredTopics" stripe v-loading="loadingTable" @selection-change="selectedTopicIds = $event">
@@ -130,13 +131,13 @@
<el-table-column prop="created_at" label="创建时间" width="140"><template #default="scope">{{ formatDate(scope.row.created_at) }}</template></el-table-column> <el-table-column prop="created_at" label="创建时间" width="140"><template #default="scope">{{ formatDate(scope.row.created_at) }}</template></el-table-column>
<el-table-column prop="generated_at" label="创作时间" width="140"><template #default="scope">{{ scope.row.generated_at ? formatDate(scope.row.generated_at) : '-' }}</template></el-table-column> <el-table-column prop="generated_at" label="创作时间" width="140"><template #default="scope">{{ scope.row.generated_at ? formatDate(scope.row.generated_at) : '-' }}</template></el-table-column>
<el-table-column prop="published_at" label="发布时间" width="140"><template #default="scope">{{ scope.row.published_at ? formatDate(scope.row.published_at) : '-' }}</template></el-table-column> <el-table-column prop="published_at" label="发布时间" width="140"><template #default="scope">{{ scope.row.published_at ? formatDate(scope.row.published_at) : '-' }}</template></el-table-column>
<el-table-column label="操作" width="280" fixed="right"> <el-table-column label="操作" width="180" fixed="right">
<template #default="scope"> <template #default="scope">
<div style="display: flex; gap: 4px; flex-wrap: wrap;"> <div style="display: flex; gap: 4px; flex-wrap: wrap;">
<el-button size="small" @click="openPreview(scope.row)" type="primary">预览</el-button> <el-button size="small" @click="openPreview(scope.row)" type="primary">预览</el-button>
<el-button size="small" type="success" :disabled="scope.row.status !== 'pending'" @click="createTopic(scope.row)">创作</el-button> <el-button size="small" type="success" :disabled="!isStatus(scope.row, 'pending')" @click="createTopic(scope.row)">创作</el-button>
<el-button size="small" type="warning" :disabled="scope.row.status !== 'review'" @click="optimizeTopic(scope.row)">审查</el-button> <el-button size="small" type="warning" :disabled="!isStatus(scope.row, 'review')" @click="optimizeTopic(scope.row)">审查</el-button>
<el-button v-if="scope.row.status === 'ready'" size="small" type="primary" @click="handlePublish(scope.row)">发布</el-button> <el-button v-if="isStatus(scope.row, 'ready')" size="small" type="primary" @click="handlePublish(scope.row)">发布</el-button>
<el-button size="small" type="danger" @click="deleteTopic(scope.row.id)">删除</el-button> <el-button size="small" type="danger" @click="deleteTopic(scope.row.id)">删除</el-button>
</div> </div>
</template> </template>
@@ -144,9 +145,9 @@
</el-table> </el-table>
<!-- 移动端卡片列表 --> <!-- 移动端卡片列表 -->
<div class="topic-card-list" v-if="filteredTopics && filteredTopics.length > 0"> <div class="topic-card-list" v-if="filteredTopics && filteredTopics.length > 0">
<div v-for="topic in filteredTopics" :key="topic.id" class="topic-card"> <div v-for="(topic, index) in filteredTopics" :key="topic.id" class="topic-card">
<div class="topic-card-header"> <div class="topic-card-header">
<div class="topic-card-title">{{ topic.title }}</div> <div class="topic-card-title">{{ topic.id }}. {{ topic.title }}</div>
<el-tag :type="getStatusType(topic.status)" size="small">{{ topic.status }}</el-tag> <el-tag :type="getStatusType(topic.status)" size="small">{{ topic.status }}</el-tag>
</div> </div>
<div class="topic-card-tags"> <div class="topic-card-tags">
@@ -160,9 +161,9 @@
</div> </div>
<div class="topic-card-actions"> <div class="topic-card-actions">
<el-button size="small" @click="openPreview(topic)" type="primary">预览</el-button> <el-button size="small" @click="openPreview(topic)" type="primary">预览</el-button>
<el-button size="small" type="success" :disabled="topic.status !== 'pending'" @click="createTopic(topic)">创作</el-button> <el-button size="small" type="success" :disabled="!isStatus(topic, 'pending')" @click="createTopic(topic)">创作</el-button>
<el-button size="small" type="warning" :disabled="topic.status !== 'review'" @click="optimizeTopic(topic)">审查</el-button> <el-button size="small" type="warning" :disabled="!isStatus(topic, 'review')" @click="optimizeTopic(topic)">审查</el-button>
<el-button v-if="topic.status === 'ready'" size="small" type="primary" @click="handlePublish(topic)">发布</el-button> <el-button v-if="isStatus(topic, 'ready')" size="small" type="primary" @click="handlePublish(topic)">发布</el-button>
<el-button size="small" type="danger" @click="deleteTopic(topic.id)">删除</el-button> <el-button size="small" type="danger" @click="deleteTopic(topic.id)">删除</el-button>
</div> </div>
</div> </div>
@@ -178,6 +179,29 @@
<button class="mobile-nav-btn" @click="redirectToPage('logs.html')">📄 日志</button> <button class="mobile-nav-btn" @click="redirectToPage('logs.html')">📄 日志</button>
<button v-if="isAdmin" class="mobile-nav-btn" @click="redirectToPage('users.html')">👥 用户</button> <button v-if="isAdmin" class="mobile-nav-btn" @click="redirectToPage('users.html')">👥 用户</button>
</nav> </nav>
<!-- 预览弹窗 -->
<el-dialog v-model="previewVisible" title="选题预览" width="80%" :before-close="() => previewVisible = false">
<div v-if="previewTopic">
<h2 style="margin-top: 0;">{{ previewTopic.title }}</h2>
<div class="preview-content" style="max-height: 60vh; overflow-y: auto; margin: 16px 0; padding: 16px; border: 1px solid #ebeef5; border-radius: 8px; background: #fafafa;">
{{ previewTopic.content || '暂无内容' }}
</div>
<div class="preview-footer" style="font-size: 14px; color: #909399;">
<div>创建时间:{{ formatDate(previewTopic.created_at) }}</div>
<div>状态:{{ previewTopic.status }}</div>
<div v-if="previewTopic.generated_at">创作时间:{{ formatDate(previewTopic.generated_at) }}</div>
<div v-if="previewTopic.published_at">发布时间:{{ formatDate(previewTopic.published_at) }}</div>
</div>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="previewVisible = false">关闭</el-button>
<el-button type="primary" @click="copyContent('微信公众号')">复制并发布到微信公众号</el-button>
<el-button type="primary" @click="copyContent('知乎')">复制并发布到知乎</el-button>
<el-button type="primary" @click="copyContent('小红书')">复制并发布到小红书</el-button>
</div>
</template>
</el-dialog>
</div> </div>
<script src="/static/vue/vue.global.js"></script> <script src="/static/vue/vue.global.js"></script>
<script src="/static/element-plus/index.full.min.js"></script> <script src="/static/element-plus/index.full.min.js"></script>
@@ -194,17 +218,36 @@ const TopicsApp = {
selectedTopicIds: [], selectedTopicIds: [],
filterStatus: '', filterStatus: '',
stats: { total: 0, pending: 0, review: 0, ready: 0, published: 0, today: 0 }, stats: { total: 0, pending: 0, review: 0, ready: 0, published: 0, today: 0 },
topics: [] topics: [],
previewVisible: false,
previewTopic: null
} }
}, },
computed: { computed: {
filteredTopics() { filteredTopics() {
if (!this.topics || !this.topics.length) { return []; } if (!this.topics || !this.topics.length) { return []; }
if (!this.filterStatus) { return this.topics; } if (!this.filterStatus) { return this.topics; }
return this.topics.filter(t => t.status === this.filterStatus); const statusMap = {
'pending': ['pending', '待处理'],
'review': ['review', '待审查'],
'ready': ['ready', '待发布'],
'published': ['published', '已发布']
};
const allowed = statusMap[this.filterStatus] || [this.filterStatus];
return this.topics.filter(t => allowed.includes(t.status));
}, },
countByStatus() { statusStats() {
return (status) => this.topics.filter(t => t.status === status).length; const pending = ['pending', '待处理'];
const review = ['review', '待审查'];
const ready = ['ready', '待发布'];
const published = ['published', '已发布'];
return {
total: this.topics.length,
pending: this.topics.filter(t => pending.includes(t.status)).length,
review: this.topics.filter(t => review.includes(t.status)).length,
ready: this.topics.filter(t => ready.includes(t.status)).length,
published: this.topics.filter(t => published.includes(t.status)).length
};
} }
}, },
methods: { methods: {
@@ -249,9 +292,25 @@ const TopicsApp = {
this.selectedTopicIds = []; this.selectedTopicIds = [];
await this.fetchTopics(); await this.fetchTopics();
}, },
openPreview(topic) { this.$message.info('预览:' + topic.title); }, openPreview(topic) {
this.previewTopic = topic;
this.previewVisible = true;
},
copyContent(platform) {
if (!this.previewTopic || !this.previewTopic.content) {
this.$message.warning('暂无内容可复制');
return;
}
const text = `标题:${this.previewTopic.title}\n\n内容:\n${this.previewTopic.content}`;
navigator.clipboard.writeText(text).then(() => {
this.$message.success(`已复制内容,请前往${platform}粘贴发布`);
}).catch(err => {
console.error('复制失败', err);
this.$message.error('复制失败,请手动复制');
});
},
async createTopic(topic) { async createTopic(topic) {
if (topic.status !== 'pending') { if (!this.isStatus(topic, 'pending')) {
this.$message.info('仅待处理选题可创作'); this.$message.info('仅待处理选题可创作');
return; return;
} }
@@ -282,7 +341,7 @@ const TopicsApp = {
} }
}, },
async optimizeTopic(topic) { async optimizeTopic(topic) {
if (topic.status !== 'review') { if (!this.isStatus(topic, 'review')) {
this.$message.info('仅待审查选题可优化'); this.$message.info('仅待审查选题可优化');
return; return;
} }
@@ -313,7 +372,7 @@ const TopicsApp = {
} }
}, },
async handlePublish(topic) { async handlePublish(topic) {
if (topic.status !== 'ready') { if (!this.isStatus(topic, 'ready')) {
this.$message.info('仅待发布选题可发布'); this.$message.info('仅待发布选题可发布');
return; return;
} }
@@ -382,8 +441,26 @@ const TopicsApp = {
} catch (e) { return dateStr; } } catch (e) { return dateStr; }
}, },
getStatusType(status) { getStatusType(status) {
const map = { 'pending': 'warning', 'review': 'danger', 'ready': 'success', 'published': 'info' }; const map = {
'pending': 'warning',
'review': 'danger',
'ready': 'success',
'published': 'info',
'待处理': 'warning',
'待审查': 'danger',
'待发布': 'success',
'已发布': 'info'
};
return map[status] || 'primary'; return map[status] || 'primary';
},
isStatus(row, status) {
const map = {
'pending': ['pending', '待处理'],
'review': ['review', '待审查'],
'ready': ['ready', '待发布'],
'published': ['published', '已发布']
};
return map[status] ? map[status].includes(row.status) : row.status === status;
} }
}, },
mounted() { mounted() {
+10 -1
View File
@@ -30,6 +30,7 @@
.content-area { padding: 16px; padding-bottom: 80px; } .content-area { padding: 16px; padding-bottom: 80px; }
} }
.user-card-list { display: none; }
/* users.html 移动端优化 */ /* users.html 移动端优化 */
@media (max-width: 768px) { @media (max-width: 768px) {
.user-table { display: none; } .user-table { display: none; }
@@ -204,7 +205,15 @@
.catch(() => {}); .catch(() => {});
}, },
handleLogout() { localStorage.removeItem('authToken'); window.location.href = '/'; }, handleLogout() { localStorage.removeItem('authToken'); window.location.href = '/'; },
redirectToPage(page) { window.location.href = '/' + page; }, redirectToPage(page) {
if (!page) return;
// 处理绝对URL或相对路径
if (page.startsWith('/') || page.startsWith('http://') || page.startsWith('https://')) {
window.location.href = page;
} else {
window.location.href = '/' + page;
}
},
formatDate(dateStr) { if (!dateStr) return '-'; return new Date(dateStr.replace(' ', 'T')).toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }); } formatDate(dateStr) { if (!dateStr) return '-'; return new Date(dateStr.replace(' ', 'T')).toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }); }
}, },
mounted() { mounted() {