2026新年第一篇:uni-app + AI = 3分钟实现数据大屏

大家好,我是忒可君!

3分钟讲明白,uni-app + AI 实现数据大屏开发。

一、前端UI界面实现

uni-app vue3

因为页面是由三段式构成,所以AI友好性大大增加,直接复制可以使用,以后叫我 AICV工程师。

结构如下:

AI出来的代码直接复制,demo如下:

html 复制代码
<template>
    <view class="dashboard-container">
        <!-- 顶部标题栏 -->
        <view class="header">
            <view class="title-container">
                <text class="title">数据大屏</text>
                <text class="subtitle">实时监控与数据分析平台</text>
            </view>
            <view class="time-info">
                <text class="time">{{ currentTime }}</text>
                <text class="date">{{ currentDate }}</text>
            </view>
        </view>

        <!-- 统计数据栏 -->
        <view class="stats-bar">
            <view class="stat-card" :class="stats.totalEquipment > 0 ? 'active' : ''">
                <view class="stat-icon">🏭</view>
                <view class="stat-info">
                    <text class="stat-value">{{ stats.totalEquipment || 0 }}</text>
                    <text class="stat-label">设备总数</text>
                </view>
            </view>
            
            <view class="stat-card" :class="stats.runningEquipment > 0 ? 'active' : ''">
                <view class="stat-icon">✅</view>
                <view class="stat-info">
                    <text class="stat-value">{{ stats.runningEquipment || 0 }}</text>
                    <text class="stat-label">运行中</text>
                </view>
            </view>
            
            <view class="stat-card" :class="stats.activeAlarms > 0 ? 'warning' : ''">
                <view class="stat-icon">⚠️</view>
                <view class="stat-info">
                    <text class="stat-value">{{ stats.activeAlarms || 0 }}</text>
                    <text class="stat-label">未处理警报</text>
                </view>
            </view>
            
            <view class="stat-card" :class="stats.todayProduction > 0 ? 'active' : ''">
                <view class="stat-icon">📊</view>
                <view class="stat-info">
                    <text class="stat-value">{{ stats.todayProduction || 0 }}</text>
                    <text class="stat-label">今日产量</text>
                </view>
            </view>
        </view>

        <!-- 主要内容区域 -->
        <scroll-view class="main-content" scroll-y>
            <view class="content-grid">
                <!-- 左侧:设备总览 -->
                <view class="grid-column" style="flex: 2;">
                    <view class="section-card">
                        <view class="section-header">
                            <text class="section-title">设备总览</text>
                            <text class="section-subtitle">实时状态监控</text>
                        </view>
                        <view class="equipment-grid">
                            <EquipmentCard 
                                v-for="equipment in equipmentList" 
                                :key="equipment.id"
                                :equipment="equipment"
                                @click="viewEquipmentDetail(equipment.id)"
                            />
                        </view>
                    </view>
                </view>

                <!-- 右侧:警报和产能 -->
                <view class="grid-column" style="flex: 1;">
                    <!-- 警报信息 -->
                    <view class="section-card alarm-section">
                        <view class="section-header">
                            <text class="section-title">警报信息</text>
                            <text class="section-subtitle">未处理:{{ activeAlarms.length }}</text>
                        </view>
                        <AlarmList :alarms="activeAlarms" @resolve="resolveAlarm" />
                    </view>

                    <!-- 产能信息 -->
                    <view class="section-card production-section">
                        <view class="section-header">
                            <text class="section-title">产能信息</text>
                            <text class="section-subtitle">今日生产数据</text>
                        </view>
                        <ProductionChart :data="productionData" />
                    </view>
                </view>
            </view>
        </scroll-view>

        <!-- 底部状态栏 -->
        <view class="footer">
            <view class="status-indicators">
                <view class="status-item">
                    <view class="status-dot normal"></view>
                    <text class="status-text">正常</text>
                </view>
                <view class="status-item">
                    <view class="status-dot warning"></view>
                    <text class="status-text">警告</text>
                </view>
                <view class="status-item">
                    <view class="status-dot error"></view>
                    <text class="status-text">故障</text>
                </view>
                <view class="status-item">
                    <view class="status-dot offline"></view>
                    <text class="status-text">离线</text>
                </view>
            </view>
            <text class="update-time">最后更新:{{ lastUpdateTime }}</text>
        </view>
    </view>
</template>

<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import EquipmentCard from '@/components/EquipmentCard.vue'
import AlarmList from '@/components/AlarmList.vue'
import ProductionChart from '@/components/ProductionChart.vue'

// 导入API - 使用正确的相对路径
import { 
    equipmentApi, 
    alarmApi, 
    productionApi, 
    dashboardApi 
} from '../../utils/api.js'


// 页面变量定义
const currentTime = ref('')
const currentDate = ref('')
const equipmentList = ref([])
const activeAlarms = ref([])
const productionData = ref([])
const stats = ref({
    totalEquipment: 0,
    runningEquipment: 0,
    activeAlarms: 0,
    todayProduction: 0,
    todayQualified: 0,
    qualificationRate: 0
})
const lastUpdateTime = ref('')
let updateTimer = null

// 更新时间函数
const updateDateTime = () => {
    const now = new Date()
    currentTime.value = now.toLocaleTimeString('zh-CN', { 
        hour12: false,
        hour: '2-digit',
        minute: '2-digit',
        second: '2-digit'
    })
    currentDate.value = now.toLocaleDateString('zh-CN', {
        year: 'numeric',
        month: '2-digit',
        day: '2-digit',
        weekday: 'long'
    })
    lastUpdateTime.value = currentTime.value
}

// 加载设备数据
const loadEquipmentData = async () => {
    try {
        console.log('开始加载设备数据...')
        const response = await equipmentApi.getAll()
        console.log('设备数据响应:', response)
        
        if (response && response.success) {
            equipmentList.value = response.data || []
            // 计算运行中的设备数量
            stats.value.runningEquipment = equipmentList.value.filter(
                eq => eq.status === 'running' || eq.status === 'normal'
            ).length
            console.log('设备数据加载成功,数量:', equipmentList.value.length)
        } else {
            console.error('设备数据加载失败:', response?.error)
            // 使用模拟数据作为fallback
            useMockData()
        }
    } catch (error) {
        console.error('加载设备数据失败:', error)
        useMockData()
    }
}

// 加载警报数据
const loadAlarmData = async () => {
    try {
        console.log('开始加载警报数据...')
        const response = await alarmApi.getAlarms(0)
        console.log('警报数据响应:', response)
        
        if (response && response.success) {
            activeAlarms.value = response.data || []
            console.log('警报数据加载成功,数量:', activeAlarms.value.length)
        } else {
            console.error('警报数据加载失败:', response?.error)
        }
    } catch (error) {
        console.error('加载警报数据失败:', error)
    }
}

// 加载产能数据
const loadProductionData = async () => {
    try {
        console.log('开始加载产能数据...')
        const response = await productionApi.getTodayProduction()
        console.log('产能数据响应:', response)
        
        if (response && response.success) {
            productionData.value = response.data || []
            console.log('产能数据加载成功,数量:', productionData.value.length)
        } else {
            console.error('产能数据加载失败:', response?.error)
        }
    } catch (error) {
        console.error('加载产能数据失败:', error)
    }
}

// 加载统计数据
const loadStats = async () => {
    try {
        console.log('开始加载统计数据...')
        const response = await dashboardApi.getStats()
        console.log('统计数据响应:', response)
        
        if (response && response.success) {
            // 合并统计数据
            stats.value = { ...stats.value, ...response.data }
            console.log('统计数据加载成功:', stats.value)
        } else {
            console.error('统计数据加载失败:', response?.error)
        }
    } catch (error) {
        console.error('加载统计数据失败:', error)
    }
}

// 使用模拟数据(当API不可用时)
const useMockData = () => {
    console.log('使用模拟数据...')
    // 模拟设备数据
    equipmentList.value = [
       
    ]
    
    // 模拟警报数据
    activeAlarms.value = [
        {
            id: 1,
            equipment_id: 2,
            alarm_type: '高温警报',
            severity: 'high',
            message: '温度超过安全阈值',
            alarm_time: '2024-01-25 14:30:00',
            resolved: 0
        },
        {
            id: 2,
            equipment_id: 4,
            alarm_type: '气压不足',
            severity: 'low',
            message: '气压低于工作标准',
            alarm_time: '2024-01-25 09:45:00',
            resolved: 0
        }
    ]
    
    // 模拟产能数据
    productionData.value = [
        
    ]
    
    // 模拟统计数据
    stats.value = {
        totalEquipment: 7,
        runningEquipment: 5,
        activeAlarms: 2,
        todayProduction: 920,
        todayQualified: 897,
        qualificationRate: 97.5
    }
}

// 处理警报
const resolveAlarm = async (alarmId) => {
    try {
        console.log('处理警报:', alarmId)
        const response = await alarmApi.resolveAlarm(alarmId)
        
        if (response && response.success) {
            // 重新加载警报数据
            loadAlarmData()
            loadStats()
            uni.showToast({
                title: '警报已处理',
                icon: 'success'
            })
        } else {
            uni.showToast({
                title: '处理失败: ' + (response?.error || '未知错误'),
                icon: 'error'
            })
        }
    } catch (error) {
        console.error('处理警报失败:', error)
        uni.showToast({
            title: '网络请求失败',
            icon: 'error'
        })
    }
}

// 查看设备详情
const viewEquipmentDetail = (equipmentId) => {
    uni.navigateTo({
        url: `/pages/detail/detail?id=${equipmentId}`
    })
}

// 初始化数据加载
const loadAllData = async () => {
    updateDateTime()
    await Promise.all([
        loadEquipmentData(),
        loadAlarmData(),
        loadProductionData(),
        loadStats()
    ])
}

// 生命周期钩子
onMounted(() => {
    console.log('页面加载完成,开始初始化数据...')
    loadAllData()
    
    // 每30秒更新一次数据
    updateTimer = setInterval(loadAllData, 30000)
    
    // 每秒更新时间
    setInterval(updateDateTime, 1000)
})

onUnmounted(() => {
    if (updateTimer) {
        clearInterval(updateTimer)
    }
})
</script>

<style scoped>
.dashboard-container {
    display: flex;
    flex-direction: column;
    height: 100vh;
    background: linear-gradient(135deg, #1a237e 0%, #283593 100%);
    color: #ffffff;
}

/* 头部样式 */
.header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 20px 30px;
    background: rgba(0, 0, 0, 0.2);
    border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}

.title-container {
    display: flex;
    flex-direction: column;
}

.title {
    font-size: 24px;
    font-weight: bold;
    color: #ffffff;
}

.subtitle {
    font-size: 14px;
    color: rgba(255, 255, 255, 0.7);
    margin-top: 5px;
}

.time-info {
    display: flex;
    flex-direction: column;
    align-items: flex-end;
}

.time {
    font-size: 20px;
    font-weight: bold;
    font-family: 'Courier New', monospace;
}

.date {
    font-size: 14px;
    color: rgba(255, 255, 255, 0.7);
    margin-top: 5px;
}

/* 统计数据栏 */
.stats-bar {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: 15px;
    padding: 20px 30px;
}

.stat-card {
    display: flex;
    align-items: center;
    padding: 20px;
    background: rgba(255, 255, 255, 0.1);
    border-radius: 10px;
    backdrop-filter: blur(10px);
    border: 1px solid rgba(255, 255, 255, 0.1);
    transition: all 0.3s ease;
}

.stat-card.active {
    background: rgba(76, 175, 80, 0.2);
    border-color: rgba(76, 175, 80, 0.3);
}

.stat-card.warning {
    background: rgba(255, 193, 7, 0.2);
    border-color: rgba(255, 193, 7, 0.3);
}

.stat-icon {
    font-size: 32px;
    margin-right: 15px;
}

.stat-info {
    display: flex;
    flex-direction: column;
}

.stat-value {
    font-size: 28px;
    font-weight: bold;
    line-height: 1;
}

.stat-label {
    font-size: 12px;
    color: rgba(255, 255, 255, 0.7);
    margin-top: 5px;
}

/* 主要内容区域 */
.main-content {
    flex: 1;
    padding: 0 30px 20px;
}

.content-grid {
    display: flex;
    gap: 20px;
    height: 100%;
}

.grid-column {
    display: flex;
    flex-direction: column;
    gap: 20px;
}

/* 区域卡片样式 */
.section-card {
    background: rgba(255, 255, 255, 0.05);
    border-radius: 15px;
    padding: 20px;
    border: 1px solid rgba(255, 255, 255, 0.1);
    backdrop-filter: blur(10px);
}

.section-header {
    margin-bottom: 20px;
    padding-bottom: 15px;
    border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}

.section-title {
    display: block;
    font-size: 18px;
    font-weight: bold;
    margin-bottom: 5px;
}

.section-subtitle {
    font-size: 12px;
    color: rgba(255, 255, 255, 0.7);
}

/* 设备网格 */
.equipment-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
    gap: 15px;
}

/* 底部样式 */
.footer {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 15px 30px;
    background: rgba(0, 0, 0, 0.3);
    border-top: 1px solid rgba(255, 255, 255, 0.1);
}

.status-indicators {
    display: flex;
    gap: 20px;
}

.status-item {
    display: flex;
    align-items: center;
    gap: 8px;
}

.status-dot {
    width: 10px;
    height: 10px;
    border-radius: 50%;
}

.status-dot.normal {
    background: #4CAF50;
    box-shadow: 0 0 10px #4CAF50;
}

.status-dot.warning {
    background: #FFC107;
    box-shadow: 0 0 10px #FFC107;
}

.status-dot.error {
    background: #F44336;
    box-shadow: 0 0 10px #F44336;
}

.status-dot.offline {
    background: #9E9E9E;
    box-shadow: 0 0 10px #9E9E9E;
}

.update-time {
    font-size: 12px;
    color: rgba(255, 255, 255, 0.7);
}

/* 响应式设计 */
@media (max-width: 768px) {
    .content-grid {
        flex-direction: column;
    }
    
    .stats-bar {
        grid-template-columns: repeat(2, 1fr);
    }
    
    .equipment-grid {
        grid-template-columns: 1fr;
    }
}
</style>

二、node.js实现后端功能

目录demo如下:

在cmd中启动:

  1. npm install :安装项目依赖(根据 package.json 文件)

  2. node server.js :运行 Node.js 服务器

复制代码
npm start(启动后端服务)

三、选择适配自己的AI功能

我个人推荐,deepseek。对会写代码的人更加友好,不会乱七叭嗦的。

四、问题汇总

1、跨域:自己搜一搜

2、该项目适合本地,云端可以使用uni-cloud,会更加简单。

3、有参考的效果图,可以给AI直接生成代码。如果只有想法可以给元宝、豆包生成效果图或者可运行的demo,截图生成uni-app三段式代码即可。

相关推荐
泰勒疯狂展开17 小时前
Vue3研学-标签ref属性与TS接口泛型
前端·javascript·vue.js
小二·17 小时前
前端 DevOps 完全指南:从 Docker 容器化到 GitHub Actions 自动化部署(Vue 3 + Vite)
前端·docker·devops
行走的陀螺仪17 小时前
UniApp 横向可滚动 Tab 组件开发详解
uni-app·封装组件·tabs·自定义封装组件·可滚动组件tab
Komorebi゛17 小时前
【CSS】线性流动边框样式
前端·css·css3
2501_9159184117 小时前
介绍如何在电脑上查看 iPhone 和 iPad 的完整设备信息
android·ios·小程序·uni-app·电脑·iphone·ipad
我不吃饼干18 小时前
手写 Vue 模板编译(生成篇)
前端·vue.js
s小布丁18 小时前
vue2纯前端使用Docxtemplater生成word报告,包含echart图表,表格
前端
2501_9160088918 小时前
没有 Mac 如何在 Windows 上创建 iOS 应用描述文件
android·macos·ios·小程序·uni-app·iphone·webview
web小白成长日记1 天前
企业级 Vue3 + Element Plus 主题定制架构:从“能用”到“好用”的进阶之路
前端·架构