Files
akmon/pages/mall/nfc/admin/index.uvue
2026-01-20 08:04:15 +08:00

838 lines
20 KiB
Plaintext

<template>
<view class="admin-dashboard">
<!-- 顶部统计卡片 -->
<view class="stats-cards">
<view class="stat-card">
<view class="stat-icon users">
<image src="/static/icons/users.png" />
</view>
<view class="stat-info">
<text class="stat-value">{{ statsData.totalUsers }}</text>
<text class="stat-label">总用户数</text>
<text class="stat-change positive">+{{ statsData.userGrowth }}%</text>
</view>
</view>
<view class="stat-card">
<view class="stat-icon transactions">
<image src="/static/icons/transaction.png" />
</view>
<view class="stat-info">
<text class="stat-value">{{ statsData.todayTransactions }}</text>
<text class="stat-label">今日交易</text>
<text class="stat-change" :class="{ 'positive': statsData.transactionGrowth > 0 }">
{{ statsData.transactionGrowth > 0 ? '+' : '' }}{{ statsData.transactionGrowth }}%
</text>
</view>
</view>
<view class="stat-card">
<view class="stat-icon revenue">
<image src="/static/icons/revenue.png" />
</view>
<view class="stat-info">
<text class="stat-value">¥{{ statsData.todayRevenue }}</text>
<text class="stat-label">今日营收</text>
<text class="stat-change positive">+{{ statsData.revenueGrowth }}%</text>
</view>
</view>
<view class="stat-card">
<view class="stat-icon devices">
<image src="/static/icons/devices.png" />
</view>
<view class="stat-info">
<text class="stat-value">{{ statsData.onlineDevices }}/{{ statsData.totalDevices }}</text>
<text class="stat-label">在线设备</text>
<text class="stat-change" :class="{ 'negative': deviceOfflineRate > 10 }">
{{ (100 - deviceOfflineRate).toFixed(1) }}%
</text>
</view>
</view>
</view>
<!-- 系统状态监控 -->
<view class="system-status">
<view class="section-header">
<text class="section-title">系统状态</text>
<text class="last-update">更新时间: {{ formatTime(lastUpdateTime) }}</text>
</view>
<view class="status-grid">
<view class="status-item" v-for="service in systemServices" :key="service.name">
<view class="service-info">
<view class="service-status" :class="service.status">
<view class="status-dot"></view>
</view>
<text class="service-name">{{ service.name }}</text>
</view>
<view class="service-metrics">
<text class="metric-item">CPU: {{ service.cpu }}%</text>
<text class="metric-item">内存: {{ service.memory }}%</text>
<text class="metric-item">响应: {{ service.responseTime }}ms</text>
</view>
</view>
</view>
</view>
<!-- 实时告警 -->
<view class="alerts-section" v-if="alerts.length > 0">
<view class="section-header">
<text class="section-title">实时告警</text>
<text class="alert-count">{{ alerts.length }}条未处理</text>
</view>
<view class="alerts-list">
<view class="alert-item"
v-for="alert in alerts"
:key="alert.id"
:class="alert.level"
@click="handleAlert(alert)">
<view class="alert-icon">
<image :src="getAlertIcon(alert.level)" />
</view>
<view class="alert-content">
<text class="alert-title">{{ alert.title }}</text>
<text class="alert-desc">{{ alert.description }}</text>
<text class="alert-time">{{ formatTime(alert.time) }}</text>
</view>
<view class="alert-actions">
<button class="action-btn" @click.stop="resolveAlert(alert.id)">处理</button>
</view>
</view>
</view>
</view>
<!-- 快速操作 -->
<view class="quick-operations">
<view class="section-title">快速操作</view>
<view class="operations-grid">
<view class="operation-item" @click="goToUserManagement">
<image class="operation-icon" src="/static/icons/user-manage.png" />
<text class="operation-text">用户管理</text>
</view>
<view class="operation-item" @click="goToCardManagement">
<image class="operation-icon" src="/static/icons/card-manage.png" />
<text class="operation-text">卡片管理</text>
</view>
<view class="operation-item" @click="goToFinancialManagement">
<image class="operation-icon" src="/static/icons/finance.png" />
<text class="operation-text">财务管理</text>
</view>
<view class="operation-item" @click="goToDeviceManagement">
<image class="operation-icon" src="/static/icons/device-manage.png" />
<text class="operation-text">设备管理</text>
</view>
<view class="operation-item" @click="goToReports">
<image class="operation-icon" src="/static/icons/reports.png" />
<text class="operation-text">数据报表</text>
</view>
<view class="operation-item" @click="goToSystemConfig">
<image class="operation-icon" src="/static/icons/settings.png" />
<text class="operation-text">系统配置</text>
</view>
<view class="operation-item" @click="goToSecurityMonitor">
<image class="operation-icon" src="/static/icons/security.png" />
<text class="operation-text">安全监控</text>
</view>
<view class="operation-item emergency" @click="goToEmergencyControl">
<image class="operation-icon" src="/static/icons/emergency.png" />
<text class="operation-text">应急控制</text>
</view>
</view>
</view>
<!-- 数据概览图表 -->
<view class="charts-section">
<view class="section-title">数据概览</view>
<view class="chart-container">
<view class="chart-header">
<text class="chart-title">交易趋势</text>
<view class="chart-filters">
<text class="filter-item"
:class="{ 'active': chartPeriod === 'day' }"
@click="changeChartPeriod('day')">今日</text>
<text class="filter-item"
:class="{ 'active': chartPeriod === 'week' }"
@click="changeChartPeriod('week')">本周</text>
<text class="filter-item"
:class="{ 'active': chartPeriod === 'month' }"
@click="changeChartPeriod('month')">本月</text>
</view>
</view>
<canvas canvas-id="transactionChart" class="chart-canvas"></canvas>
</view>
<view class="mini-charts">
<view class="mini-chart">
<text class="mini-chart-title">消费分布</text>
<canvas canvas-id="consumptionChart" class="mini-chart-canvas"></canvas>
</view>
<view class="mini-chart">
<text class="mini-chart-title">设备状态</text>
<canvas canvas-id="deviceChart" class="mini-chart-canvas"></canvas>
</view>
</view>
</view>
<!-- 系统信息 -->
<view class="system-info">
<view class="section-title">系统信息</view>
<view class="info-grid">
<view class="info-item">
<text class="info-label">系统版本</text>
<text class="info-value">v2.1.0</text>
</view>
<view class="info-item">
<text class="info-label">数据库</text>
<text class="info-value">PostgreSQL 14.2</text>
</view>
<view class="info-item">
<text class="info-label">在线时长</text>
<text class="info-value">{{ systemUptime }}</text>
</view>
<view class="info-item">
<text class="info-label">存储使用</text>
<text class="info-value">{{ storageUsage }}%</text>
</view>
</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
statsData: {
totalUsers: 1247,
userGrowth: 12.5,
todayTransactions: 3652,
transactionGrowth: 8.3,
todayRevenue: '45,680',
revenueGrowth: 15.2,
onlineDevices: 285,
totalDevices: 310
},
systemServices: [
{
name: '核心服务',
status: 'healthy',
cpu: 35,
memory: 68,
responseTime: 120
},
{
name: 'NFC服务',
status: 'healthy',
cpu: 28,
memory: 45,
responseTime: 95
},
{
name: '支付服务',
status: 'warning',
cpu: 78,
memory: 82,
responseTime: 350
},
{
name: '门禁服务',
status: 'healthy',
cpu: 22,
memory: 38,
responseTime: 85
}
],
alerts: [
{
id: 1,
level: 'warning',
title: '支付服务响应缓慢',
description: '支付服务平均响应时间超过300ms',
time: new Date(Date.now() - 300000)
},
{
id: 2,
level: 'error',
title: '设备离线',
description: '饭堂POS机#003失去连接',
time: new Date(Date.now() - 600000)
}
],
lastUpdateTime: new Date(),
chartPeriod: 'day',
systemUptime: '15天8小时',
storageUsage: 67
}
},
computed: {
deviceOfflineRate() {
return ((this.statsData.totalDevices - this.statsData.onlineDevices) / this.statsData.totalDevices * 100)
}
},
onLoad() {
this.initDashboard()
},
onPullDownRefresh() {
this.refreshDashboard()
},
methods: {
initDashboard() {
this.loadStatsData()
this.loadSystemStatus()
this.loadAlerts()
this.initCharts()
this.startRealTimeUpdate()
},
loadStatsData() {
uni.request({
url: '/api/v1/admin/dashboard-stats',
success: (res) => {
this.statsData = res.data
}
})
},
loadSystemStatus() {
uni.request({
url: '/api/v1/admin/system-status',
success: (res) => {
this.systemServices = res.data
}
})
},
loadAlerts() {
uni.request({
url: '/api/v1/admin/alerts',
success: (res) => {
this.alerts = res.data
}
})
},
refreshDashboard() {
Promise.all([
this.loadStatsData(),
this.loadSystemStatus(),
this.loadAlerts()
]).then(() => {
this.lastUpdateTime = new Date()
uni.stopPullDownRefresh()
})
},
startRealTimeUpdate() {
// 每30秒更新一次数据
setInterval(() => {
this.loadStatsData()
this.loadSystemStatus()
this.lastUpdateTime = new Date()
}, 30000)
},
initCharts() {
// 初始化图表
this.drawTransactionChart()
this.drawConsumptionChart()
this.drawDeviceChart()
},
drawTransactionChart() {
// 绘制交易趋势图
const ctx = uni.createCanvasContext('transactionChart', this)
// 这里实现具体的图表绘制逻辑
ctx.draw()
},
drawConsumptionChart() {
// 绘制消费分布图
const ctx = uni.createCanvasContext('consumptionChart', this)
// 实现饼图或柱状图
ctx.draw()
},
drawDeviceChart() {
// 绘制设备状态图
const ctx = uni.createCanvasContext('deviceChart', this)
// 实现状态分布图
ctx.draw()
},
changeChartPeriod(period) {
this.chartPeriod = period
this.drawTransactionChart()
},
handleAlert(alert) {
uni.navigateTo({
url: `/pages/mall/nfc/admin/alert-detail?id=${alert.id}`
})
},
resolveAlert(alertId) {
uni.request({
url: '/api/v1/admin/resolve-alert',
method: 'POST',
data: { alertId },
success: () => {
this.alerts = this.alerts.filter(alert => alert.id !== alertId)
uni.showToast({
title: '告警已处理',
icon: 'success'
})
}
})
},
getAlertIcon(level) {
const icons = {
info: '/static/icons/info.png',
warning: '/static/icons/warning.png',
error: '/static/icons/error.png',
critical: '/static/icons/critical.png'
}
return icons[level] || icons.info
},
// 导航方法
goToUserManagement() {
uni.navigateTo({ url: '/pages/mall/nfc/admin/user-management' })
},
goToCardManagement() {
uni.navigateTo({ url: '/pages/mall/nfc/admin/card-management' })
},
goToFinancialManagement() {
uni.navigateTo({ url: '/pages/mall/nfc/admin/financial-management' })
},
goToDeviceManagement() {
uni.navigateTo({ url: '/pages/mall/nfc/admin/device-management' })
},
goToReports() {
uni.navigateTo({ url: '/pages/mall/nfc/admin/data-reports' })
},
goToSystemConfig() {
uni.navigateTo({ url: '/pages/mall/nfc/admin/system-config' })
},
goToSecurityMonitor() {
uni.navigateTo({ url: '/pages/mall/nfc/admin/security-monitor' })
},
goToEmergencyControl() {
uni.showModal({
title: '应急控制',
content: '即将进入应急控制模式,请确认您有相应权限',
success: (res) => {
if (res.confirm) {
uni.navigateTo({ url: '/pages/mall/nfc/admin/emergency-control' })
}
}
})
},
formatTime(time) {
const now = new Date()
const diff = now - time
const minutes = Math.floor(diff / 60000)
if (minutes < 1) return '刚刚'
if (minutes < 60) return `${minutes}分钟前`
const hours = Math.floor(minutes / 60)
if (hours < 24) return `${hours}小时前`
return time.toLocaleDateString()
}
}
}
</script>
<style>
.admin-dashboard {
padding: 20rpx;
background-color: #f8f9fa;
min-height: 100vh;
}
.stats-cards {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20rpx;
margin-bottom: 30rpx;
}
.stat-card {
background: white;
border-radius: 16rpx;
padding: 30rpx;
display: flex;
align-items: center;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
}
.stat-icon {
width: 80rpx;
height: 80rpx;
border-radius: 16rpx;
display: flex;
align-items: center;
justify-content: center;
margin-right: 24rpx;
}
.stat-icon.users {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.stat-icon.transactions {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
}
.stat-icon.revenue {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
}
.stat-icon.devices {
background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
}
.stat-icon image {
width: 40rpx;
height: 40rpx;
}
.stat-info {
flex: 1;
display: flex;
flex-direction: column;
}
.stat-value {
font-size: 36rpx;
font-weight: bold;
color: #333;
margin-bottom: 8rpx;
}
.stat-label {
font-size: 24rpx;
color: #666;
margin-bottom: 8rpx;
}
.stat-change {
font-size: 20rpx;
padding: 4rpx 8rpx;
border-radius: 8rpx;
align-self: flex-start;
}
.stat-change.positive {
background: #d4edda;
color: #155724;
}
.stat-change.negative {
background: #f8d7da;
color: #721c24;
}
.system-status, .alerts-section, .quick-operations,
.charts-section, .system-info {
background: white;
border-radius: 16rpx;
padding: 30rpx;
margin-bottom: 30rpx;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
}
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
}
.last-update {
font-size: 20rpx;
color: #999;
}
.alert-count {
font-size: 24rpx;
color: #dc3545;
background: #f8d7da;
padding: 8rpx 16rpx;
border-radius: 12rpx;
}
.status-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20rpx;
}
.status-item {
padding: 20rpx;
border: 1rpx solid #eee;
border-radius: 12rpx;
}
.service-info {
display: flex;
align-items: center;
margin-bottom: 16rpx;
}
.service-status {
width: 16rpx;
height: 16rpx;
border-radius: 50%;
margin-right: 12rpx;
position: relative;
}
.service-status.healthy {
background: #28a745;
}
.service-status.warning {
background: #ffc107;
}
.service-status.error {
background: #dc3545;
}
.status-dot {
width: 100%;
height: 100%;
border-radius: 50%;
animation: pulse 2s infinite;
}
@keyframes pulse {
0% {
transform: scale(1);
opacity: 1;
}
50% {
transform: scale(1.2);
opacity: 0.7;
}
100% {
transform: scale(1);
opacity: 1;
}
}
.service-name {
font-size: 24rpx;
color: #333;
font-weight: bold;
}
.service-metrics {
display: flex;
flex-direction: column;
gap: 8rpx;
}
.metric-item {
font-size: 20rpx;
color: #666;
}
.alerts-list {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.alert-item {
display: flex;
align-items: center;
padding: 20rpx;
border-radius: 12rpx;
border-left: 6rpx solid;
}
.alert-item.warning {
background: #fff3cd;
border-left-color: #ffc107;
}
.alert-item.error {
background: #f8d7da;
border-left-color: #dc3545;
}
.alert-icon {
width: 40rpx;
height: 40rpx;
margin-right: 20rpx;
}
.alert-icon image {
width: 100%;
height: 100%;
}
.alert-content {
flex: 1;
display: flex;
flex-direction: column;
}
.alert-title {
font-size: 28rpx;
color: #333;
font-weight: bold;
margin-bottom: 8rpx;
}
.alert-desc {
font-size: 24rpx;
color: #666;
margin-bottom: 8rpx;
}
.alert-time {
font-size: 20rpx;
color: #999;
}
.alert-actions {
margin-left: 20rpx;
}
.action-btn {
background: #007AFF;
color: white;
border: none;
border-radius: 8rpx;
padding: 12rpx 20rpx;
font-size: 24rpx;
}
.operations-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20rpx;
}
.operation-item {
display: flex;
flex-direction: column;
align-items: center;
padding: 30rpx 20rpx;
border: 1rpx solid #eee;
border-radius: 12rpx;
transition: all 0.3s ease;
}
.operation-item:hover {
border-color: #007AFF;
background: #f8f9ff;
}
.operation-item.emergency {
border-color: #dc3545;
background: #fff5f5;
}
.operation-icon {
width: 48rpx;
height: 48rpx;
margin-bottom: 16rpx;
}
.operation-text {
font-size: 24rpx;
color: #333;
text-align: center;
}
.chart-container {
margin-bottom: 30rpx;
}
.chart-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
}
.chart-title {
font-size: 28rpx;
color: #333;
font-weight: bold;
}
.chart-filters {
display: flex;
gap: 20rpx;
}
.filter-item {
font-size: 24rpx;
color: #666;
padding: 8rpx 16rpx;
border-radius: 8rpx;
border: 1rpx solid #ddd;
}
.filter-item.active {
background: #007AFF;
color: white;
border-color: #007AFF;
}
.chart-canvas {
width: 100%;
height: 400rpx;
}
.mini-charts {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20rpx;
}
.mini-chart {
text-align: center;
}
.mini-chart-title {
font-size: 24rpx;
color: #333;
margin-bottom: 16rpx;
display: block;
}
.mini-chart-canvas {
width: 100%;
height: 200rpx;
}
.info-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20rpx;
}
.info-item {
display: flex;
flex-direction: column;
align-items: center;
padding: 20rpx;
border: 1rpx solid #eee;
border-radius: 12rpx;
}
.info-label {
font-size: 24rpx;
color: #666;
margin-bottom: 8rpx;
}
.info-value {
font-size: 28rpx;
color: #333;
font-weight: bold;
}
</style>