Files
autoAiWorkSys/app/views/ConsolePage.vue
张成 e17d5610f5 1
2025-12-22 16:26:59 +08:00

1329 lines
30 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>