1329 lines
30 KiB
Vue
1329 lines
30 KiB
Vue
<template>
|
||
<div class="page-console">
|
||
<!-- 顶部头部栏:标题 + 自动投递设置 + 快速统计 + 设置按钮 -->
|
||
<div class="console-header">
|
||
<div class="header-left">
|
||
<div class="header-title-section">
|
||
<h2 class="page-title">控制台</h2>
|
||
<!-- 自动投递设置摘要 -->
|
||
<div class="delivery-settings-summary">
|
||
<div class="summary-item">
|
||
<span class="summary-label">自动投递:</span>
|
||
<span :class="['summary-value', deliveryConfig.autoDelivery ? 'enabled' : 'disabled']">
|
||
{{ deliveryConfig.autoDelivery ? '已开启' : '已关闭' }}
|
||
</span>
|
||
</div>
|
||
<div class="summary-item">
|
||
<span class="summary-label">投递时间:</span>
|
||
<span class="summary-value">{{ deliveryConfig.startTime || '09:00' }} - {{ deliveryConfig.endTime ||
|
||
'18:00' }}</span>
|
||
</div>
|
||
<div class="summary-item">
|
||
<span class="summary-label">仅工作日:</span>
|
||
<span class="summary-value">{{ deliveryConfig.workdaysOnly ? '是' : '否' }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="header-right">
|
||
<!-- 快速统计 -->
|
||
<div class="quick-stats">
|
||
<div class="quick-stat-item">
|
||
<span class="stat-label">今日投递</span>
|
||
<span class="stat-value">{{ deliveryStats.todayCount || 0 }}</span>
|
||
</div>
|
||
<div class="quick-stat-item">
|
||
<span class="stat-label">今日找工作</span>
|
||
<span class="stat-value">{{ todayJobSearchCount || 0 }}</span>
|
||
</div>
|
||
<div class="quick-stat-item">
|
||
<span class="stat-label">今日沟通</span>
|
||
<span class="stat-value">{{ todayChatCount || 0 }}</span>
|
||
</div>
|
||
<div class="quick-stat-item highlight">
|
||
<span class="stat-label">执行中任务</span>
|
||
<span class="stat-value">{{ currentActivity ? 1 : 0 }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 设置按钮 -->
|
||
<button class="btn-settings" @click="toggleSettings">
|
||
<span class="settings-icon">⚙️</span>
|
||
<span class="settings-text">设置</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 设置弹窗 -->
|
||
<div v-if="showSettings" class="settings-modal" @click.self="toggleSettings">
|
||
<div class="settings-modal-content">
|
||
<div class="modal-header">
|
||
<h3 class="modal-title">自动投递设置</h3>
|
||
<button class="btn-close" @click="toggleSettings">×</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<DeliverySettings :config="deliveryConfig" @update-config="handleUpdateConfig" />
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button class="btn btn-secondary" @click="toggleSettings">取消</button>
|
||
<button class="btn btn-primary" @click="handleSaveConfig">保存</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 主要内容区域 -->
|
||
<div class="console-content">
|
||
<!-- 左侧:系统状态和任务信息 -->
|
||
<div class="content-left">
|
||
<!-- 系统状态卡片 -->
|
||
<div class="status-card">
|
||
<div class="card-header">
|
||
<h3 class="card-title">系统状态</h3>
|
||
</div>
|
||
<div class="status-grid">
|
||
<!-- 用户登录状态 -->
|
||
<div class="status-item">
|
||
<div class="status-label">用户登录</div>
|
||
<div class="status-value">
|
||
<span :class="['status-badge', isLoggedIn ? 'status-success' : 'status-error']">
|
||
{{ isLoggedIn ? '已登录' : '未登录' }}
|
||
</span>
|
||
</div>
|
||
<div v-if="isLoggedIn && userName" class="status-detail">{{ userName }}</div>
|
||
<div v-if="isLoggedIn && remainingDays !== null" class="status-detail">
|
||
剩余: <span :class="remainingDays <= 3 ? 'text-warning' : ''">{{ remainingDays }}天</span>
|
||
</div>
|
||
<div class="status-actions">
|
||
<button class="btn-action" @click="handleReloadBrowser" title="重新加载浏览器页面">
|
||
🔄 重新加载
|
||
</button>
|
||
<button class="btn-action" @click="handleShowBrowser" title="显示浏览器窗口">
|
||
🖥️ 显示浏览器
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 平台登录状态 -->
|
||
<div class="status-item">
|
||
<div class="status-label">平台登录</div>
|
||
<div class="status-detail">{{ displayPlatform }}</div>
|
||
<div class="status-detail">
|
||
<span :class="['status-badge', isPlatformLoggedIn ? 'status-success' : 'status-warning']">
|
||
{{ isPlatformLoggedIn ? '已登录' : '未登录' }}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 设备信息 -->
|
||
<div class="status-item">
|
||
<div class="status-label">设备信息</div>
|
||
<div class="status-detail">SN: {{ snCode || '-' }}</div>
|
||
<div class="status-detail">ID: {{ deviceId || '-' }}</div>
|
||
</div>
|
||
|
||
<!-- 系统资源 -->
|
||
<div class="status-item">
|
||
<div class="status-label">系统资源</div>
|
||
<div class="status-detail">运行: {{ uptime }}</div>
|
||
<div class="status-detail">CPU: {{ cpuUsage }}</div>
|
||
<div class="status-detail">内存: {{ memUsage }}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 任务信息卡片 -->
|
||
<div class="task-card">
|
||
<div class="card-header">
|
||
<h3 class="card-title">任务信息</h3>
|
||
</div>
|
||
|
||
<!-- 设备工作状态(新方案:直接显示服务端格式化好的文本) -->
|
||
<div v-if="displayText" class="device-work-status">
|
||
<div class="status-header">
|
||
<span class="status-label">设备工作状态</span>
|
||
</div>
|
||
<div class="status-content">
|
||
<div class="status-text">{{ displayText }}</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 当前活动(兼容显示) -->
|
||
<div v-if="currentActivity" class="current-activity">
|
||
<div class="task-header">
|
||
<span class="task-label">当前活动</span>
|
||
<span :class="['status-badge', 'status-info']">{{ currentActivity.status === 'running' ? '执行中' :
|
||
currentActivity.status === 'completed' ? '已完成' : '未知' }}</span>
|
||
</div>
|
||
<div class="task-content">
|
||
<div class="task-name">{{ currentActivity.description || currentActivity.name || '-' }}</div>
|
||
<div v-if="currentActivity.progress !== null && currentActivity.progress !== undefined"
|
||
class="task-progress">
|
||
<div class="progress-bar">
|
||
<div class="progress-fill" :style="{ width: currentActivity.progress + '%' }"></div>
|
||
</div>
|
||
<span class="progress-text">{{ currentActivity.progress }}%</span>
|
||
</div>
|
||
<div v-if="currentActivity.currentStep" class="task-step">{{ currentActivity.currentStep }}</div>
|
||
</div>
|
||
</div>
|
||
<div v-else-if="!displayText" class="no-task">暂无执行中的活动</div>
|
||
|
||
<!-- 待执行队列 -->
|
||
<div v-if="pendingQueue" class="pending-queue">
|
||
<div class="task-header">
|
||
<span class="task-label">待执行队列</span>
|
||
<span class="task-count">{{ pendingQueue.totalCount || 0 }}个</span>
|
||
</div>
|
||
<div class="queue-content">
|
||
<div class="queue-count">待执行: {{ pendingQueue.totalCount || 0 }}个任务</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 下次任务执行时间(使用服务端格式化好的文本) -->
|
||
<div v-if="nextExecuteTimeText" class="next-task-time">
|
||
<div class="task-header">
|
||
<span class="task-label">下次执行时间</span>
|
||
</div>
|
||
<div class="time-content">{{ nextExecuteTimeText }}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 右侧:二维码和详细统计 -->
|
||
<div class="content-right">
|
||
<!-- 二维码卡片 -->
|
||
<div class="qr-card">
|
||
<div class="card-header">
|
||
<h3 class="card-title">登录二维码</h3>
|
||
<button v-if="!isPlatformLoggedIn" class="btn-qr-refresh" @click="handleGetQrCode">
|
||
获取二维码
|
||
</button>
|
||
</div>
|
||
<div class="qr-content">
|
||
<div v-if="!qrCodeUrl" class="qr-placeholder">
|
||
<p>点击"获取二维码"按钮获取二维码</p>
|
||
</div>
|
||
<div v-else class="qr-display">
|
||
<img :src="qrCodeUrl" alt="登录二维码" class="qr-image">
|
||
<div class="qr-info">
|
||
<p>请使用微信扫描二维码登录</p>
|
||
<p v-if="qrCodeCountdownActive && qrCodeCountdown > 0" class="qr-countdown">
|
||
{{ qrCodeCountdown }}秒后自动刷新
|
||
</p>
|
||
<p v-if="qrCodeExpired" class="qr-expired">
|
||
二维码已过期,请重新获取
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 近7天投递趋势图表 -->
|
||
<div class="trend-chart-card">
|
||
<div class="card-header">
|
||
<h3 class="card-title">近7天投递趋势</h3>
|
||
</div>
|
||
<div class="chart-content">
|
||
<DeliveryTrendChart :data="trendData" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
import DeliverySettings from '../components/DeliverySettings.vue';
|
||
import DeliveryTrendChart from '../components/DeliveryTrendChart.vue';
|
||
import { mapState, mapActions } from 'vuex';
|
||
import applyRecordsAPI from '../api/apply_records.js';
|
||
import taskMixin from '../mixins/taskMixin.js';
|
||
import eventListenerMixin from '../mixins/eventListenerMixin.js';
|
||
import systemInfoMixin from '../mixins/systemInfoMixin.js';
|
||
import qrCodeMixin from '../mixins/qrCodeMixin.js';
|
||
import mqttMixin from '../mixins/mqttMixin.js';
|
||
import logMixin from '../mixins/logMixin.js';
|
||
let glTaskStats = null
|
||
export default {
|
||
name: 'ConsolePage',
|
||
mixins: [taskMixin, eventListenerMixin, systemInfoMixin, qrCodeMixin, mqttMixin, logMixin],
|
||
components: {
|
||
DeliverySettings,
|
||
DeliveryTrendChart
|
||
},
|
||
data() {
|
||
return {
|
||
showSettings: false,
|
||
trendData: []
|
||
};
|
||
},
|
||
computed: {
|
||
...mapState('auth', ['isLoggedIn', 'userName', 'remainingDays', 'snCode', 'platformType']),
|
||
...mapState('task', ['displayText', 'nextExecuteTimeText', 'currentActivity', 'pendingQueue', 'deviceStatus', 'taskStats']),
|
||
...mapState('delivery', ['deliveryStats', 'deliveryConfig']),
|
||
...mapState('platform', ['isPlatformLoggedIn']),
|
||
...mapState('qrCode', ['qrCodeUrl', 'qrCodeCountdownActive', 'qrCodeCountdown', 'qrCodeExpired']),
|
||
...mapState('system', ['deviceId', 'uptime', 'cpuUsage', 'memUsage']),
|
||
|
||
// 今日找工作数量(今日完成的任务数)
|
||
todayJobSearchCount() {
|
||
return this.taskStats ? this.taskStats.todayCount : 0;
|
||
},
|
||
|
||
// 今日沟通数量(暂时使用 0,后续可以添加沟通统计接口)
|
||
todayChatCount() {
|
||
return 0; // TODO: 后续添加沟通统计接口
|
||
},
|
||
|
||
// 直接从 auth.platformType 转换显示名称,避免数据冗余
|
||
displayPlatform() {
|
||
if (this.platformType) {
|
||
const platformNames = {
|
||
'boss': 'BOSS直聘',
|
||
'liepin': '猎聘',
|
||
'zhilian': '智联招聘',
|
||
'1': 'BOSS直聘'
|
||
};
|
||
return platformNames[this.platformType] || this.platformType;
|
||
}
|
||
return '-';
|
||
},
|
||
},
|
||
watch: {
|
||
|
||
},
|
||
mounted() {
|
||
// 加载趋势数据
|
||
this.loadTrendData();
|
||
// 加载投递配置
|
||
this.loadDeliveryConfig();
|
||
// 加载投递统计
|
||
this.loadDeliveryStats();
|
||
// 加载任务统计
|
||
|
||
|
||
this.autoLoadTaskStats()
|
||
|
||
|
||
// 确保事件监听器已设置(通过 eventListenerMixin)
|
||
if (this.setupEventListeners) {
|
||
this.setupEventListeners();
|
||
}
|
||
|
||
// 主动查询MQTT连接状态,确保UI显示正确
|
||
this.$nextTick(async () => {
|
||
if (this.checkMQTTStatus) {
|
||
await this.checkMQTTStatus();
|
||
console.log('[ConsolePage] MQTT状态已查询');
|
||
}
|
||
});
|
||
|
||
// 如果已有二维码,启动自动刷新
|
||
if (this.qrCodeUrl && !this.isPlatformLoggedIn) {
|
||
this.startQrCodeAutoRefresh();
|
||
}
|
||
|
||
// 添加调试信息
|
||
this.$nextTick(() => {
|
||
console.log('[ConsolePage] 页面已挂载,设备工作状态:', {
|
||
displayText: this.displayText,
|
||
nextExecuteTimeText: this.nextExecuteTimeText,
|
||
currentActivity: this.currentActivity,
|
||
pendingQueue: this.pendingQueue,
|
||
deviceStatus: this.deviceStatus,
|
||
isLoggedIn: this.isLoggedIn,
|
||
snCode: this.snCode,
|
||
storeState: this.$store ? this.$store.state.task : null
|
||
});
|
||
});
|
||
},
|
||
beforeUnmount() {
|
||
// 组件销毁时清理二维码定时器
|
||
if (this.stopQrCodeAutoRefresh) {
|
||
this.stopQrCodeAutoRefresh();
|
||
}
|
||
},
|
||
methods: {
|
||
...mapActions('delivery', ['updateDeliveryConfig', 'saveDeliveryConfig']),
|
||
formatTaskTime(time) {
|
||
if (!time) return '-';
|
||
try {
|
||
const date = new Date(time);
|
||
const now = new Date();
|
||
const diff = date.getTime() - now.getTime();
|
||
|
||
if (diff < 0) {
|
||
return '已过期';
|
||
}
|
||
|
||
const minutes = Math.floor(diff / (1000 * 60));
|
||
const hours = Math.floor(minutes / 60);
|
||
const days = Math.floor(hours / 24);
|
||
|
||
if (days > 0) {
|
||
return `${days}天后 (${date.toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' })})`;
|
||
} else if (hours > 0) {
|
||
return `${hours}小时后 (${date.toLocaleString('zh-CN', { hour: '2-digit', minute: '2-digit' })})`;
|
||
} else if (minutes > 0) {
|
||
return `${minutes}分钟后 (${date.toLocaleString('zh-CN', { hour: '2-digit', minute: '2-digit' })})`;
|
||
} else {
|
||
return '即将执行';
|
||
}
|
||
} catch (error) {
|
||
return time;
|
||
}
|
||
},
|
||
toggleSettings() {
|
||
this.showSettings = !this.showSettings;
|
||
},
|
||
async loadTrendData() {
|
||
try {
|
||
// 获取 snCode 用于请求
|
||
const snCode = this.$store?.state?.auth?.snCode;
|
||
const result = await applyRecordsAPI.getTrendData(snCode);
|
||
|
||
// 检查响应格式:code === 0 表示成功
|
||
if (result && result.code === 0 && result.data) {
|
||
// 如果后端返回的是数组格式
|
||
if (Array.isArray(result.data)) {
|
||
this.trendData = result.data;
|
||
} else if (result.data.trendData) {
|
||
this.trendData = result.data.trendData;
|
||
} else {
|
||
// 如果没有数据,生成7天的空数据
|
||
this.generateEmptyTrendData();
|
||
}
|
||
} else {
|
||
this.generateEmptyTrendData();
|
||
}
|
||
} catch (error) {
|
||
console.error('加载趋势数据失败:', error);
|
||
this.generateEmptyTrendData();
|
||
}
|
||
},
|
||
generateEmptyTrendData() {
|
||
const today = new Date();
|
||
this.trendData = Array.from({ length: 7 }, (_, i) => {
|
||
const date = new Date(today);
|
||
date.setDate(date.getDate() - (6 - i));
|
||
return {
|
||
date: this.formatDate(date),
|
||
value: 0
|
||
};
|
||
});
|
||
},
|
||
formatDate(date) {
|
||
const month = date.getMonth() + 1;
|
||
const day = date.getDate();
|
||
return `${month}/${day}`;
|
||
},
|
||
handleUpdateConfig({ key, value }) {
|
||
this.updateDeliveryConfig({ key, value });
|
||
},
|
||
async handleSaveConfig() {
|
||
try {
|
||
const result = await this.saveDeliveryConfig();
|
||
if (result && result.success) {
|
||
this.showSettings = false;
|
||
this.addLog('success', '投递配置已保存');
|
||
} else {
|
||
this.addLog('error', result?.error || '保存配置失败');
|
||
}
|
||
} catch (error) {
|
||
console.error('保存配置失败:', error);
|
||
this.addLog('error', `保存配置失败: ${error.message}`);
|
||
}
|
||
},
|
||
async loadDeliveryConfig() {
|
||
try {
|
||
await this.$store.dispatch('delivery/loadDeliveryConfig');
|
||
} catch (error) {
|
||
console.error('加载投递配置失败:', error);
|
||
}
|
||
},
|
||
async loadDeliveryStats() {
|
||
try {
|
||
await this.$store.dispatch('delivery/loadDeliveryStats');
|
||
} catch (error) {
|
||
console.error('加载投递统计失败:', error);
|
||
}
|
||
},
|
||
async loadTaskStats() {
|
||
try {
|
||
await this.$store.dispatch('task/loadTaskStats');
|
||
} catch (error) {
|
||
console.error('加载任务统计失败:', error);
|
||
}
|
||
},
|
||
async autoLoadTaskStats() {
|
||
|
||
|
||
this.loadTaskStats()
|
||
|
||
// 60秒调用一下保持是最新的
|
||
if (!glTaskStats) {
|
||
glTaskStats = setInterval(() => {
|
||
this.loadTaskStats()
|
||
}, 60 * 1000)
|
||
}
|
||
|
||
},
|
||
async handleGetQrCode() {
|
||
const success = await this.getQrCode();
|
||
if (success) {
|
||
// 获取成功后启动自动刷新
|
||
this.startQrCodeAutoRefresh();
|
||
}
|
||
},
|
||
async handleReloadBrowser() {
|
||
try {
|
||
const result = await window.electronAPI.invoke('browser-window:reload');
|
||
if (result && result.success) {
|
||
this.addLog('success', '浏览器页面已重新加载');
|
||
} else {
|
||
this.addLog('error', result?.error || '重新加载失败');
|
||
}
|
||
} catch (error) {
|
||
console.error('重新加载浏览器失败:', error);
|
||
this.addLog('error', `重新加载失败: ${error.message}`);
|
||
}
|
||
},
|
||
async handleShowBrowser() {
|
||
try {
|
||
const result = await window.electronAPI.invoke('browser-window:show');
|
||
if (result && result.success) {
|
||
this.addLog('success', '浏览器窗口已显示');
|
||
} else {
|
||
this.addLog('error', result?.error || '显示浏览器失败');
|
||
}
|
||
} catch (error) {
|
||
console.error('显示浏览器失败:', error);
|
||
this.addLog('error', `显示浏览器失败: ${error.message}`);
|
||
}
|
||
}
|
||
},
|
||
beforeDestroy() {
|
||
if (glTaskStats) {
|
||
clearInterval(glTaskStats)
|
||
glTaskStats = null
|
||
}
|
||
}
|
||
};
|
||
</script>
|
||
|
||
<style lang="less" scoped>
|
||
.page-console {
|
||
padding: 20px;
|
||
height: 100%;
|
||
overflow-y: auto;
|
||
background: #f5f5f5;
|
||
}
|
||
|
||
/* 头部栏 */
|
||
.console-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
background: #fff;
|
||
padding: 15px 20px;
|
||
border-radius: 8px;
|
||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.header-left {
|
||
flex: 1;
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.header-title-section {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 20px;
|
||
flex-wrap: wrap;
|
||
|
||
.page-title {
|
||
margin: 0;
|
||
font-size: 24px;
|
||
font-weight: 600;
|
||
color: #333;
|
||
white-space: nowrap;
|
||
}
|
||
}
|
||
|
||
/* 自动投递设置摘要 */
|
||
.delivery-settings-summary {
|
||
display: flex;
|
||
gap: 20px;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
|
||
.summary-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
font-size: 13px;
|
||
|
||
.summary-label {
|
||
color: #666;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.summary-value {
|
||
font-weight: 500;
|
||
white-space: nowrap;
|
||
|
||
&.enabled {
|
||
color: #4CAF50;
|
||
}
|
||
|
||
&.disabled {
|
||
color: #999;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.header-right {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 20px;
|
||
}
|
||
|
||
.quick-stats {
|
||
display: flex;
|
||
gap: 20px;
|
||
align-items: center;
|
||
}
|
||
|
||
.quick-stat-item {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
padding: 8px 15px;
|
||
background: #f9f9f9;
|
||
border-radius: 6px;
|
||
min-width: 60px;
|
||
|
||
&.highlight {
|
||
background: #e8f5e9;
|
||
}
|
||
|
||
.stat-label {
|
||
font-size: 12px;
|
||
color: #666;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.stat-value {
|
||
font-size: 20px;
|
||
font-weight: 600;
|
||
color: #4CAF50;
|
||
}
|
||
}
|
||
|
||
.btn-settings {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 10px 20px;
|
||
background: #4CAF50;
|
||
color: #fff;
|
||
border: none;
|
||
border-radius: 6px;
|
||
font-size: 14px;
|
||
cursor: pointer;
|
||
transition: all 0.3s;
|
||
|
||
&:hover {
|
||
background: #45a049;
|
||
}
|
||
|
||
.settings-icon {
|
||
font-size: 16px;
|
||
}
|
||
}
|
||
|
||
/* 设置弹窗 */
|
||
.settings-modal {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 1000;
|
||
animation: fadeIn 0.3s ease;
|
||
}
|
||
|
||
@keyframes fadeIn {
|
||
from {
|
||
opacity: 0;
|
||
}
|
||
|
||
to {
|
||
opacity: 1;
|
||
}
|
||
}
|
||
|
||
.settings-modal-content {
|
||
background: #fff;
|
||
border-radius: 8px;
|
||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
||
width: 90%;
|
||
max-width: 600px;
|
||
max-height: 90vh;
|
||
overflow-y: auto;
|
||
animation: slideUp 0.3s ease;
|
||
}
|
||
|
||
@keyframes slideUp {
|
||
from {
|
||
transform: translateY(20px);
|
||
opacity: 0;
|
||
}
|
||
|
||
to {
|
||
transform: translateY(0);
|
||
opacity: 1;
|
||
}
|
||
}
|
||
|
||
.modal-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 20px;
|
||
border-bottom: 1px solid #f0f0f0;
|
||
|
||
.modal-title {
|
||
margin: 0;
|
||
font-size: 20px;
|
||
font-weight: 600;
|
||
color: #333;
|
||
}
|
||
|
||
.btn-close {
|
||
background: none;
|
||
border: none;
|
||
font-size: 28px;
|
||
color: #999;
|
||
cursor: pointer;
|
||
padding: 0;
|
||
width: 32px;
|
||
height: 32px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border-radius: 4px;
|
||
transition: all 0.3s;
|
||
|
||
&:hover {
|
||
background: #f5f5f5;
|
||
color: #333;
|
||
}
|
||
}
|
||
}
|
||
|
||
.modal-body {
|
||
padding: 20px;
|
||
|
||
.settings-section {
|
||
margin-bottom: 0;
|
||
|
||
.page-title {
|
||
display: none; // 隐藏重复的标题,因为弹窗头部已经有了
|
||
}
|
||
|
||
.settings-form-horizontal {
|
||
border: none;
|
||
box-shadow: none;
|
||
padding: 0;
|
||
}
|
||
|
||
.form-row {
|
||
flex-direction: column;
|
||
align-items: stretch;
|
||
gap: 15px;
|
||
}
|
||
|
||
.form-item {
|
||
justify-content: space-between;
|
||
width: 100%;
|
||
|
||
.form-label {
|
||
min-width: 120px;
|
||
}
|
||
|
||
.form-input {
|
||
flex: 1;
|
||
max-width: 300px;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.modal-footer {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
gap: 12px;
|
||
padding: 15px 20px;
|
||
border-top: 1px solid #f0f0f0;
|
||
|
||
.btn {
|
||
padding: 10px 24px;
|
||
border: none;
|
||
border-radius: 6px;
|
||
font-size: 14px;
|
||
cursor: pointer;
|
||
transition: all 0.3s;
|
||
|
||
&.btn-secondary {
|
||
background: #f5f5f5;
|
||
color: #666;
|
||
|
||
&:hover {
|
||
background: #e0e0e0;
|
||
}
|
||
}
|
||
|
||
&.btn-primary {
|
||
background: #4CAF50;
|
||
color: #fff;
|
||
|
||
&:hover {
|
||
background: #45a049;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/* 主要内容区域 */
|
||
.console-content {
|
||
display: grid;
|
||
grid-template-columns: 2fr 1fr;
|
||
gap: 20px;
|
||
align-items: start;
|
||
}
|
||
|
||
.content-left {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 20px;
|
||
}
|
||
|
||
.content-right {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 20px;
|
||
}
|
||
|
||
/* 卡片通用样式 */
|
||
.status-card,
|
||
.task-card,
|
||
.qr-card,
|
||
.stats-card {
|
||
background: #fff;
|
||
border-radius: 8px;
|
||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||
overflow: hidden;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.card-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 15px 20px;
|
||
border-bottom: 1px solid #f0f0f0;
|
||
|
||
.card-title {
|
||
margin: 0;
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: #333;
|
||
}
|
||
}
|
||
|
||
/* 系统状态 */
|
||
.status-card {
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.status-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, 1fr);
|
||
gap: 15px;
|
||
padding: 20px;
|
||
flex: 1;
|
||
}
|
||
|
||
.status-item {
|
||
.status-label {
|
||
font-size: 13px;
|
||
color: #666;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.status-value {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: #333;
|
||
margin-bottom: 6px;
|
||
}
|
||
|
||
.status-detail {
|
||
font-size: 12px;
|
||
color: #999;
|
||
margin-top: 4px;
|
||
}
|
||
|
||
.status-actions {
|
||
display: flex;
|
||
gap: 8px;
|
||
margin-top: 8px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.btn-action {
|
||
padding: 4px 10px;
|
||
font-size: 11px;
|
||
background: #f5f5f5;
|
||
color: #666;
|
||
border: 1px solid #ddd;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
white-space: nowrap;
|
||
|
||
&:hover {
|
||
background: #e0e0e0;
|
||
border-color: #ccc;
|
||
color: #333;
|
||
}
|
||
|
||
&:active {
|
||
transform: scale(0.98);
|
||
}
|
||
}
|
||
}
|
||
|
||
.status-badge {
|
||
display: inline-block;
|
||
padding: 4px 10px;
|
||
border-radius: 12px;
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
|
||
&.status-success {
|
||
background: #e8f5e9;
|
||
color: #4caf50;
|
||
}
|
||
|
||
&.status-error {
|
||
background: #ffebee;
|
||
color: #f44336;
|
||
}
|
||
|
||
&.status-warning {
|
||
background: #fff3e0;
|
||
color: #ff9800;
|
||
}
|
||
|
||
&.status-info {
|
||
background: #e3f2fd;
|
||
color: #2196f3;
|
||
}
|
||
}
|
||
|
||
.text-warning {
|
||
color: #ff9800;
|
||
}
|
||
|
||
/* 任务信息 */
|
||
.task-card {
|
||
.card-body {
|
||
padding: 20px;
|
||
}
|
||
|
||
.card-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 15px;
|
||
|
||
.mqtt-topic-info {
|
||
font-size: 12px;
|
||
color: #666;
|
||
|
||
.topic-label {
|
||
margin-right: 5px;
|
||
}
|
||
|
||
.topic-value {
|
||
color: #2196f3;
|
||
font-family: monospace;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.current-task,
|
||
.pending-tasks,
|
||
.next-task-time,
|
||
.device-work-status,
|
||
.current-activity,
|
||
.pending-queue {
|
||
padding: 15px 20px;
|
||
border-bottom: 1px solid #f0f0f0;
|
||
|
||
&:last-child {
|
||
border-bottom: none;
|
||
}
|
||
}
|
||
|
||
.device-work-status {
|
||
.status-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 12px;
|
||
|
||
.status-label {
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: #333;
|
||
}
|
||
}
|
||
|
||
.status-content {
|
||
.status-text {
|
||
font-size: 14px;
|
||
color: #666;
|
||
line-height: 1.6;
|
||
word-break: break-word;
|
||
}
|
||
}
|
||
}
|
||
|
||
.current-activity {
|
||
.task-content {
|
||
.task-name {
|
||
font-size: 15px;
|
||
font-weight: 500;
|
||
color: #333;
|
||
margin-bottom: 10px;
|
||
}
|
||
}
|
||
}
|
||
|
||
.pending-queue {
|
||
.queue-content {
|
||
.queue-count {
|
||
font-size: 14px;
|
||
color: #666;
|
||
}
|
||
}
|
||
}
|
||
|
||
.next-task-time {
|
||
.time-content {
|
||
color: #666;
|
||
font-size: 14px;
|
||
}
|
||
}
|
||
|
||
.task-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 12px;
|
||
|
||
.task-label {
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: #333;
|
||
}
|
||
|
||
.task-count {
|
||
font-size: 12px;
|
||
color: #999;
|
||
}
|
||
}
|
||
|
||
.task-content {
|
||
.task-name {
|
||
font-size: 15px;
|
||
font-weight: 500;
|
||
color: #333;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.task-progress {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
margin-bottom: 8px;
|
||
|
||
.progress-bar {
|
||
flex: 1;
|
||
height: 6px;
|
||
background: #e0e0e0;
|
||
border-radius: 3px;
|
||
overflow: hidden;
|
||
|
||
.progress-fill {
|
||
height: 100%;
|
||
background: linear-gradient(90deg, #4caf50, #8bc34a);
|
||
transition: width 0.3s ease;
|
||
}
|
||
}
|
||
|
||
.progress-text {
|
||
font-size: 12px;
|
||
color: #666;
|
||
min-width: 35px;
|
||
}
|
||
}
|
||
|
||
.task-step {
|
||
font-size: 12px;
|
||
color: #666;
|
||
}
|
||
}
|
||
|
||
.pending-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
}
|
||
|
||
.pending-item {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 8px 12px;
|
||
background: #f9f9f9;
|
||
border-radius: 4px;
|
||
|
||
.pending-name {
|
||
font-size: 13px;
|
||
color: #333;
|
||
}
|
||
}
|
||
|
||
.no-task {
|
||
text-align: center;
|
||
padding: 20px;
|
||
color: #999;
|
||
font-size: 13px;
|
||
}
|
||
|
||
/* 二维码卡片 */
|
||
.qr-card {
|
||
display: flex;
|
||
flex-direction: column;
|
||
min-height: 0;
|
||
/* 允许flex子元素收缩 */
|
||
|
||
.btn-qr-refresh {
|
||
padding: 6px 12px;
|
||
background: #4CAF50;
|
||
color: #fff;
|
||
border: none;
|
||
border-radius: 4px;
|
||
font-size: 12px;
|
||
cursor: pointer;
|
||
|
||
&:hover {
|
||
background: #45a049;
|
||
}
|
||
}
|
||
}
|
||
|
||
.qr-content {
|
||
padding: 20px;
|
||
text-align: center;
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: center;
|
||
align-items: center;
|
||
}
|
||
|
||
.qr-placeholder {
|
||
padding: 40px 20px;
|
||
color: #999;
|
||
font-size: 13px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
flex: 1;
|
||
}
|
||
|
||
/* 趋势图表卡片 */
|
||
.trend-chart-card {
|
||
.chart-content {
|
||
padding: 20px;
|
||
}
|
||
}
|
||
|
||
.qr-display {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
flex: 1;
|
||
|
||
.qr-image {
|
||
max-width: 100%;
|
||
width: 200px;
|
||
height: 200px;
|
||
object-fit: contain;
|
||
border-radius: 8px;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.qr-info {
|
||
margin-top: 12px;
|
||
font-size: 12px;
|
||
color: #666;
|
||
text-align: center;
|
||
|
||
p {
|
||
margin: 4px 0;
|
||
}
|
||
|
||
.qr-countdown {
|
||
color: #2196f3;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.qr-expired {
|
||
color: #f44336;
|
||
font-weight: 600;
|
||
}
|
||
}
|
||
}
|
||
|
||
/* 统计卡片 */
|
||
.stats-list {
|
||
padding: 20px;
|
||
}
|
||
|
||
.stat-row {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 12px 0;
|
||
border-bottom: 1px solid #f0f0f0;
|
||
|
||
&:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
&.highlight {
|
||
background: #f9f9f9;
|
||
margin: 0 -20px;
|
||
padding: 12px 20px;
|
||
border-radius: 0;
|
||
}
|
||
|
||
.stat-row-label {
|
||
font-size: 14px;
|
||
color: #666;
|
||
}
|
||
|
||
.stat-row-value {
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
color: #4CAF50;
|
||
}
|
||
}
|
||
|
||
/* 响应式设计 */
|
||
@media (max-width: 1200px) {
|
||
.console-content {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.status-grid {
|
||
grid-template-columns: repeat(2, 1fr);
|
||
}
|
||
|
||
.console-header {
|
||
flex-direction: column;
|
||
align-items: flex-start;
|
||
gap: 15px;
|
||
}
|
||
|
||
.header-right {
|
||
width: 100%;
|
||
justify-content: space-between;
|
||
}
|
||
|
||
.delivery-settings-summary {
|
||
width: 100%;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.console-header {
|
||
padding: 12px 15px;
|
||
}
|
||
|
||
.header-left {
|
||
width: 100%;
|
||
|
||
.page-title {
|
||
font-size: 20px;
|
||
}
|
||
}
|
||
|
||
.delivery-settings-summary {
|
||
flex-direction: column;
|
||
align-items: flex-start;
|
||
gap: 8px;
|
||
}
|
||
|
||
.quick-stats {
|
||
flex-wrap: wrap;
|
||
gap: 10px;
|
||
}
|
||
|
||
.header-right {
|
||
flex-direction: column;
|
||
gap: 15px;
|
||
width: 100%;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.console-header {
|
||
flex-direction: column;
|
||
gap: 15px;
|
||
align-items: stretch;
|
||
}
|
||
|
||
.header-right {
|
||
flex-direction: column;
|
||
gap: 15px;
|
||
}
|
||
|
||
.quick-stats {
|
||
justify-content: space-around;
|
||
width: 100%;
|
||
}
|
||
|
||
.status-grid {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
}
|
||
</style>
|