Initial commit of akmon project
This commit is contained in:
837
pages/mall/nfc/admin/index.uvue
Normal file
837
pages/mall/nfc/admin/index.uvue
Normal file
@@ -0,0 +1,837 @@
|
||||
<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>
|
||||
1251
pages/mall/nfc/librarian/index.uvue
Normal file
1251
pages/mall/nfc/librarian/index.uvue
Normal file
File diff suppressed because it is too large
Load Diff
857
pages/mall/nfc/merchant/pos-cashier.uvue
Normal file
857
pages/mall/nfc/merchant/pos-cashier.uvue
Normal file
@@ -0,0 +1,857 @@
|
||||
<template>
|
||||
<view class="pos-cashier">
|
||||
<!-- 顶部状态栏 -->
|
||||
<view class="status-bar">
|
||||
<view class="terminal-info">
|
||||
<text class="terminal-id">终端: {{ terminalInfo.id }}</text>
|
||||
<text class="merchant-name">{{ terminalInfo.merchantName }}</text>
|
||||
</view>
|
||||
<view class="status-indicators">
|
||||
<view class="status-item" :class="{ 'online': nfcStatus }">
|
||||
<text class="status-text">NFC</text>
|
||||
</view>
|
||||
<view class="status-item" :class="{ 'online': networkStatus }">
|
||||
<text class="status-text">网络</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 商品清单 -->
|
||||
<view class="order-list">
|
||||
<view class="list-header">
|
||||
<text class="header-title">商品清单</text>
|
||||
<text class="item-count">{{ orderItems.length }}项</text>
|
||||
</view>
|
||||
<scroll-view scroll-y="true" class="items-scroll">
|
||||
<view class="order-item" v-for="(item, index) in orderItems" :key="index">
|
||||
<view class="item-info">
|
||||
<text class="item-name">{{ item.name }}</text>
|
||||
<text class="item-spec" v-if="item.spec">{{ item.spec }}</text>
|
||||
</view>
|
||||
<view class="item-quantity">
|
||||
<button class="qty-btn" @click="decreaseQuantity(index)">-</button>
|
||||
<text class="qty-text">{{ item.quantity }}</text>
|
||||
<button class="qty-btn" @click="increaseQuantity(index)">+</button>
|
||||
</view>
|
||||
<view class="item-price">
|
||||
<text class="unit-price">¥{{ item.price.toFixed(2) }}</text>
|
||||
<text class="total-price">¥{{ (item.price * item.quantity).toFixed(2) }}</text>
|
||||
</view>
|
||||
<button class="remove-btn" @click="removeItem(index)">删除</button>
|
||||
</view>
|
||||
<view class="empty-state" v-if="orderItems.length === 0">
|
||||
<image class="empty-icon" src="/static/icons/empty-cart.png" />
|
||||
<text class="empty-text">暂无商品,请扫码或手动添加</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<!-- 商品操作区 -->
|
||||
<view class="product-actions">
|
||||
<button class="action-btn scan-btn" @click="scanProduct">
|
||||
<image class="btn-icon" src="/static/icons/scan.png" />
|
||||
<text class="btn-text">扫码添加</text>
|
||||
</button>
|
||||
<button class="action-btn search-btn" @click="searchProduct">
|
||||
<image class="btn-icon" src="/static/icons/search.png" />
|
||||
<text class="btn-text">搜索商品</text>
|
||||
</button>
|
||||
<button class="action-btn manual-btn" @click="manualAdd">
|
||||
<image class="btn-icon" src="/static/icons/add.png" />
|
||||
<text class="btn-text">手动添加</text>
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<!-- 结算区域 -->
|
||||
<view class="checkout-section">
|
||||
<view class="total-info">
|
||||
<view class="total-items">
|
||||
<text class="total-label">共{{ totalQuantity }}件商品</text>
|
||||
</view>
|
||||
<view class="total-amount">
|
||||
<text class="amount-label">总计:</text>
|
||||
<text class="amount-value">¥{{ totalAmount.toFixed(2) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="payment-buttons">
|
||||
<button class="payment-btn nfc-btn"
|
||||
@click="processNFCPayment"
|
||||
:disabled="orderItems.length === 0">
|
||||
<image class="payment-icon" src="/static/icons/nfc.png" />
|
||||
<text class="payment-text">NFC支付</text>
|
||||
</button>
|
||||
<button class="payment-btn qr-btn"
|
||||
@click="processQRPayment"
|
||||
:disabled="orderItems.length === 0">
|
||||
<image class="payment-icon" src="/static/icons/qr.png" />
|
||||
<text class="payment-text">扫码支付</text>
|
||||
</button>
|
||||
<button class="payment-btn cash-btn"
|
||||
@click="processCashPayment"
|
||||
:disabled="orderItems.length === 0">
|
||||
<image class="payment-icon" src="/static/icons/cash.png" />
|
||||
<text class="payment-text">现金支付</text>
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<view class="quick-actions-row">
|
||||
<button class="quick-btn" @click="clearOrder">清空订单</button>
|
||||
<button class="quick-btn" @click="saveOrder">保存订单</button>
|
||||
<button class="quick-btn" @click="printReceipt" :disabled="!lastTransaction">打印小票</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- NFC支付等待界面 -->
|
||||
<view class="nfc-waiting" v-if="isWaitingNFC">
|
||||
<view class="waiting-content">
|
||||
<view class="nfc-animation">
|
||||
<image class="nfc-icon" src="/static/icons/nfc-large.png" />
|
||||
<view class="ripple"></view>
|
||||
<view class="ripple ripple-2"></view>
|
||||
</view>
|
||||
<text class="waiting-text">请顾客将校园卡或手机靠近读卡器</text>
|
||||
<text class="amount-display">支付金额: ¥{{ totalAmount.toFixed(2) }}</text>
|
||||
<button class="cancel-btn" @click="cancelNFCPayment">取消支付</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 支付成功界面 -->
|
||||
<view class="payment-success" v-if="showPaymentSuccess">
|
||||
<view class="success-content">
|
||||
<image class="success-icon" src="/static/icons/success.png" />
|
||||
<text class="success-text">支付成功</text>
|
||||
<text class="success-amount">¥{{ lastTransaction.amount }}</text>
|
||||
<view class="success-details">
|
||||
<text class="detail-item">订单号: {{ lastTransaction.orderNo }}</text>
|
||||
<text class="detail-item">支付方式: {{ lastTransaction.paymentMethod }}</text>
|
||||
<text class="detail-item">交易时间: {{ formatTime(lastTransaction.time) }}</text>
|
||||
</view>
|
||||
<button class="continue-btn" @click="continueOrder">继续收银</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
terminalInfo: {
|
||||
id: 'POS001',
|
||||
merchantName: '第一饭堂'
|
||||
},
|
||||
nfcStatus: true,
|
||||
networkStatus: true,
|
||||
orderItems: [
|
||||
{
|
||||
name: '红烧肉',
|
||||
spec: '份',
|
||||
price: 12.00,
|
||||
quantity: 1
|
||||
},
|
||||
{
|
||||
name: '米饭',
|
||||
spec: '份',
|
||||
price: 3.50,
|
||||
quantity: 2
|
||||
}
|
||||
],
|
||||
isWaitingNFC: false,
|
||||
showPaymentSuccess: false,
|
||||
lastTransaction: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
totalQuantity() {
|
||||
return this.orderItems.reduce((sum, item) => sum + item.quantity, 0)
|
||||
},
|
||||
totalAmount() {
|
||||
return this.orderItems.reduce((sum, item) => sum + (item.price * item.quantity), 0)
|
||||
}
|
||||
},
|
||||
onLoad() {
|
||||
this.initPOSSystem()
|
||||
},
|
||||
methods: {
|
||||
initPOSSystem() {
|
||||
// 初始化POS系统
|
||||
this.checkNFCStatus()
|
||||
this.checkNetworkStatus()
|
||||
},
|
||||
checkNFCStatus() {
|
||||
// 检查NFC状态
|
||||
// 这里可以调用硬件API检查NFC读卡器状态
|
||||
setTimeout(() => {
|
||||
this.nfcStatus = true
|
||||
}, 1000)
|
||||
},
|
||||
checkNetworkStatus() {
|
||||
// 检查网络状态
|
||||
uni.getNetworkType({
|
||||
success: (res) => {
|
||||
this.networkStatus = res.networkType !== 'none'
|
||||
}
|
||||
})
|
||||
},
|
||||
scanProduct() {
|
||||
// 扫码添加商品
|
||||
uni.scanCode({
|
||||
success: (res) => {
|
||||
this.addProductByCode(res.result)
|
||||
},
|
||||
fail: () => {
|
||||
uni.showToast({
|
||||
title: '扫码失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
searchProduct() {
|
||||
// 搜索商品
|
||||
uni.navigateTo({
|
||||
url: '/pages/mall/nfc/merchant/product-search'
|
||||
})
|
||||
},
|
||||
manualAdd() {
|
||||
// 手动添加商品
|
||||
uni.showModal({
|
||||
title: '添加商品',
|
||||
editable: true,
|
||||
placeholderText: '输入商品名称',
|
||||
success: (res) => {
|
||||
if (res.confirm && res.content) {
|
||||
this.addProductManually(res.content)
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
addProductByCode(code) {
|
||||
// 根据条码添加商品
|
||||
uni.request({
|
||||
url: '/api/v1/merchant/product-by-code',
|
||||
data: { code },
|
||||
success: (res) => {
|
||||
this.addItemToOrder(res.data)
|
||||
},
|
||||
fail: () => {
|
||||
uni.showToast({
|
||||
title: '商品不存在',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
addProductManually(name) {
|
||||
// 手动添加商品(需要输入价格)
|
||||
uni.showModal({
|
||||
title: '输入价格',
|
||||
editable: true,
|
||||
placeholderText: '0.00',
|
||||
success: (res) => {
|
||||
if (res.confirm && res.content) {
|
||||
const price = parseFloat(res.content)
|
||||
if (price > 0) {
|
||||
this.addItemToOrder({
|
||||
name,
|
||||
price,
|
||||
spec: '份'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
addItemToOrder(product) {
|
||||
const existingIndex = this.orderItems.findIndex(item => item.name === product.name)
|
||||
if (existingIndex >= 0) {
|
||||
this.orderItems[existingIndex].quantity++
|
||||
} else {
|
||||
this.orderItems.push({
|
||||
...product,
|
||||
quantity: 1
|
||||
})
|
||||
}
|
||||
},
|
||||
increaseQuantity(index) {
|
||||
this.orderItems[index].quantity++
|
||||
},
|
||||
decreaseQuantity(index) {
|
||||
if (this.orderItems[index].quantity > 1) {
|
||||
this.orderItems[index].quantity--
|
||||
}
|
||||
},
|
||||
removeItem(index) {
|
||||
this.orderItems.splice(index, 1)
|
||||
},
|
||||
processNFCPayment() {
|
||||
if (this.totalAmount <= 0) return
|
||||
|
||||
this.isWaitingNFC = true
|
||||
this.startNFCListening()
|
||||
},
|
||||
startNFCListening() {
|
||||
// 模拟NFC支付处理
|
||||
setTimeout(() => {
|
||||
const success = Math.random() > 0.1 // 90%成功率
|
||||
|
||||
if (success) {
|
||||
this.completePayment('NFC支付')
|
||||
} else {
|
||||
this.isWaitingNFC = false
|
||||
uni.showToast({
|
||||
title: '支付失败,请重试',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
}, 3000)
|
||||
},
|
||||
cancelNFCPayment() {
|
||||
this.isWaitingNFC = false
|
||||
},
|
||||
processQRPayment() {
|
||||
// 二维码支付
|
||||
uni.showModal({
|
||||
title: '扫码支付',
|
||||
content: '请顾客使用手机扫描支付二维码',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
this.completePayment('扫码支付')
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
processCashPayment() {
|
||||
// 现金支付
|
||||
uni.showModal({
|
||||
title: '现金支付',
|
||||
content: `收款金额: ¥${this.totalAmount.toFixed(2)}`,
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
this.completePayment('现金支付')
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
completePayment(paymentMethod) {
|
||||
this.isWaitingNFC = false
|
||||
|
||||
// 生成交易记录
|
||||
this.lastTransaction = {
|
||||
orderNo: this.generateOrderNo(),
|
||||
amount: this.totalAmount.toFixed(2),
|
||||
paymentMethod,
|
||||
time: new Date(),
|
||||
items: [...this.orderItems]
|
||||
}
|
||||
|
||||
// 保存交易到服务器
|
||||
this.saveTransaction()
|
||||
|
||||
// 显示支付成功界面
|
||||
this.showPaymentSuccess = true
|
||||
|
||||
// 3秒后自动继续
|
||||
setTimeout(() => {
|
||||
this.continueOrder()
|
||||
}, 3000)
|
||||
},
|
||||
saveTransaction() {
|
||||
uni.request({
|
||||
url: '/api/v1/merchant/save-transaction',
|
||||
method: 'POST',
|
||||
data: this.lastTransaction,
|
||||
success: (res) => {
|
||||
console.log('交易保存成功')
|
||||
}
|
||||
})
|
||||
},
|
||||
generateOrderNo() {
|
||||
return 'ORD' + Date.now().toString()
|
||||
},
|
||||
continueOrder() {
|
||||
this.showPaymentSuccess = false
|
||||
this.clearOrder()
|
||||
},
|
||||
clearOrder() {
|
||||
this.orderItems = []
|
||||
},
|
||||
saveOrder() {
|
||||
// 保存订单(暂存)
|
||||
const orderData = {
|
||||
items: this.orderItems,
|
||||
totalAmount: this.totalAmount,
|
||||
timestamp: new Date()
|
||||
}
|
||||
|
||||
uni.setStorage({
|
||||
key: 'savedOrder',
|
||||
data: orderData,
|
||||
success: () => {
|
||||
uni.showToast({
|
||||
title: '订单已保存',
|
||||
icon: 'success'
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
printReceipt() {
|
||||
if (!this.lastTransaction) return
|
||||
|
||||
// 打印小票
|
||||
uni.showToast({
|
||||
title: '正在打印小票...',
|
||||
icon: 'loading'
|
||||
})
|
||||
|
||||
// 这里可以调用打印机API
|
||||
setTimeout(() => {
|
||||
uni.showToast({
|
||||
title: '小票打印完成',
|
||||
icon: 'success'
|
||||
})
|
||||
}, 2000)
|
||||
},
|
||||
formatTime(time) {
|
||||
return time.toLocaleString()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.pos-cashier {
|
||||
height: 100vh;
|
||||
background-color: #f8f9fa;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.status-bar {
|
||||
background: white;
|
||||
padding: 20rpx 30rpx;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-bottom: 1rpx solid #eee;
|
||||
}
|
||||
|
||||
.terminal-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.terminal-id {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.merchant-name {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.status-indicators {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.status-item {
|
||||
padding: 8rpx 16rpx;
|
||||
border-radius: 8rpx;
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
.status-item.online {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.status-text {
|
||||
font-size: 20rpx;
|
||||
}
|
||||
|
||||
.order-list {
|
||||
flex: 1;
|
||||
background: white;
|
||||
margin: 20rpx;
|
||||
border-radius: 16rpx;
|
||||
padding: 30rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.list-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.item-count {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.items-scroll {
|
||||
flex: 1;
|
||||
max-height: 500rpx;
|
||||
}
|
||||
|
||||
.order-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 20rpx 0;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.order-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.item-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.item-name {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.item-spec {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.item-quantity {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0 20rpx;
|
||||
}
|
||||
|
||||
.qty-btn {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
border: 1rpx solid #ddd;
|
||||
background: white;
|
||||
font-size: 24rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.qty-text {
|
||||
margin: 0 20rpx;
|
||||
font-size: 24rpx;
|
||||
min-width: 40rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.item-price {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.unit-price {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.total-price {
|
||||
font-size: 28rpx;
|
||||
color: #fd7e14;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.remove-btn {
|
||||
padding: 12rpx 20rpx;
|
||||
background: #ff3b30;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8rpx;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 100rpx 0;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
margin-bottom: 30rpx;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.product-actions {
|
||||
display: flex;
|
||||
padding: 0 20rpx;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
flex: 1;
|
||||
background: white;
|
||||
border: 1rpx solid #ddd;
|
||||
border-radius: 12rpx;
|
||||
padding: 20rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.btn-text {
|
||||
font-size: 24rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.checkout-section {
|
||||
background: white;
|
||||
margin: 20rpx;
|
||||
border-radius: 16rpx;
|
||||
padding: 30rpx;
|
||||
}
|
||||
|
||||
.total-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 30rpx;
|
||||
padding-bottom: 20rpx;
|
||||
border-bottom: 2rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.total-items {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.total-amount {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.amount-label {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
|
||||
.amount-value {
|
||||
font-size: 48rpx;
|
||||
color: #fd7e14;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.payment-buttons {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.payment-btn {
|
||||
flex: 1;
|
||||
padding: 24rpx;
|
||||
border-radius: 12rpx;
|
||||
border: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.nfc-btn {
|
||||
background: #007AFF;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.qr-btn {
|
||||
background: #28a745;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.cash-btn {
|
||||
background: #ffc107;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.payment-btn:disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.payment-icon {
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.payment-text {
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.quick-actions-row {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.quick-btn {
|
||||
flex: 1;
|
||||
padding: 16rpx;
|
||||
background: #f8f9fa;
|
||||
border: 1rpx solid #ddd;
|
||||
border-radius: 8rpx;
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.nfc-waiting, .payment-success {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.waiting-content, .success-content {
|
||||
background: white;
|
||||
border-radius: 20rpx;
|
||||
padding: 60rpx;
|
||||
text-align: center;
|
||||
width: 600rpx;
|
||||
}
|
||||
|
||||
.nfc-animation {
|
||||
position: relative;
|
||||
width: 150rpx;
|
||||
height: 150rpx;
|
||||
margin: 0 auto 40rpx;
|
||||
}
|
||||
|
||||
.nfc-icon {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.ripple {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
border: 4rpx solid #007AFF;
|
||||
border-radius: 50%;
|
||||
animation: ripple 2s infinite;
|
||||
}
|
||||
|
||||
.ripple-2 {
|
||||
animation-delay: 1s;
|
||||
}
|
||||
|
||||
@keyframes ripple {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: scale(1.5);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.waiting-text {
|
||||
font-size: 32rpx;
|
||||
color: #333;
|
||||
margin-bottom: 20rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.amount-display {
|
||||
font-size: 48rpx;
|
||||
color: #fd7e14;
|
||||
font-weight: bold;
|
||||
margin-bottom: 40rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.cancel-btn {
|
||||
background: #ff3b30;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 12rpx;
|
||||
padding: 20rpx 40rpx;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.success-icon {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.success-text {
|
||||
font-size: 36rpx;
|
||||
color: #28a745;
|
||||
font-weight: bold;
|
||||
margin-bottom: 20rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.success-amount {
|
||||
font-size: 48rpx;
|
||||
color: #fd7e14;
|
||||
font-weight: bold;
|
||||
margin-bottom: 30rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.success-details {
|
||||
text-align: left;
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
margin-bottom: 12rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.continue-btn {
|
||||
background: #007AFF;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 12rpx;
|
||||
padding: 20rpx 40rpx;
|
||||
font-size: 28rpx;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
635
pages/mall/nfc/parent/index.uvue
Normal file
635
pages/mall/nfc/parent/index.uvue
Normal file
@@ -0,0 +1,635 @@
|
||||
<template>
|
||||
<view class="parent-home">
|
||||
<!-- 家长信息头部 -->
|
||||
<view class="parent-header">
|
||||
<view class="parent-info">
|
||||
<image class="avatar" :src="parentInfo.avatar || '/static/default-avatar.png'" />
|
||||
<view class="info">
|
||||
<text class="name">{{ parentInfo.name }}</text>
|
||||
<text class="role">家长</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="notification-btn" @click="goToNotifications">
|
||||
<image class="bell-icon" src="/static/icons/bell.png" />
|
||||
<view class="badge" v-if="unreadCount > 0">{{ unreadCount }}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 孩子选择器 -->
|
||||
<view class="children-selector">
|
||||
<scroll-view scroll-x="true" class="children-scroll">
|
||||
<view class="child-item"
|
||||
v-for="child in children"
|
||||
:key="child.id"
|
||||
:class="{ 'active': selectedChild.id === child.id }"
|
||||
@click="selectChild(child)">
|
||||
<image class="child-avatar" :src="child.avatar || '/static/default-avatar.png'" />
|
||||
<text class="child-name">{{ child.name }}</text>
|
||||
<view class="child-balance">余额: ¥{{ child.balance }}</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<!-- 今日概况 -->
|
||||
<view class="today-overview">
|
||||
<view class="section-title">今日概况</view>
|
||||
<view class="overview-cards">
|
||||
<view class="overview-card">
|
||||
<text class="card-value">¥{{ todayData.consumption }}</text>
|
||||
<text class="card-label">今日消费</text>
|
||||
<text class="card-change" :class="{ 'increase': todayData.change > 0 }">
|
||||
{{ todayData.change > 0 ? '+' : '' }}{{ todayData.change }}%
|
||||
</text>
|
||||
</view>
|
||||
<view class="overview-card">
|
||||
<text class="card-value">{{ todayData.meals }}</text>
|
||||
<text class="card-label">用餐次数</text>
|
||||
</view>
|
||||
<view class="overview-card">
|
||||
<text class="card-value">{{ todayData.accessCount }}</text>
|
||||
<text class="card-label">进出次数</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 快速操作 -->
|
||||
<view class="quick-actions">
|
||||
<view class="action-item" @click="goToRecharge">
|
||||
<image class="action-icon" src="/static/icons/recharge.png" />
|
||||
<text class="action-text">代充值</text>
|
||||
</view>
|
||||
<view class="action-item" @click="goToSpendingLimit">
|
||||
<image class="action-icon" src="/static/icons/limit.png" />
|
||||
<text class="action-text">消费限额</text>
|
||||
</view>
|
||||
<view class="action-item" @click="goToNutritionReport">
|
||||
<image class="action-icon" src="/static/icons/nutrition.png" />
|
||||
<text class="action-text">营养报告</text>
|
||||
</view>
|
||||
<view class="action-item" @click="goToAccessAlerts">
|
||||
<image class="action-icon" src="/static/icons/location.png" />
|
||||
<text class="action-text">位置提醒</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 最近消费 -->
|
||||
<view class="recent-consumption">
|
||||
<view class="section-header">
|
||||
<text class="section-title">最近消费</text>
|
||||
<text class="view-more" @click="goToConsumptionMonitor">查看更多</text>
|
||||
</view>
|
||||
<view class="consumption-list">
|
||||
<view class="consumption-item" v-for="item in recentConsumption" :key="item.id">
|
||||
<view class="consumption-info">
|
||||
<text class="merchant">{{ item.merchant }}</text>
|
||||
<text class="time">{{ formatTime(item.time) }}</text>
|
||||
</view>
|
||||
<view class="consumption-amount">
|
||||
<text class="amount">-¥{{ item.amount.toFixed(2) }}</text>
|
||||
<text class="balance">余额: ¥{{ item.balanceAfter }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 营养健康 -->
|
||||
<view class="nutrition-health">
|
||||
<view class="section-title">营养健康</view>
|
||||
<view class="nutrition-summary">
|
||||
<view class="nutrition-chart">
|
||||
<canvas canvas-id="nutritionChart" class="chart"></canvas>
|
||||
</view>
|
||||
<view class="nutrition-details">
|
||||
<view class="nutrition-item">
|
||||
<text class="nutrition-label">今日卡路里</text>
|
||||
<text class="nutrition-value">{{ nutritionData.calories }}kcal</text>
|
||||
</view>
|
||||
<view class="nutrition-item">
|
||||
<text class="nutrition-label">营养均衡</text>
|
||||
<text class="nutrition-value">{{ nutritionData.balance }}%</text>
|
||||
</view>
|
||||
<view class="nutrition-item">
|
||||
<text class="nutrition-label">健康评分</text>
|
||||
<text class="nutrition-value">{{ nutritionData.score }}分</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 安全提醒 -->
|
||||
<view class="safety-alerts" v-if="safetyAlerts.length > 0">
|
||||
<view class="section-title">安全提醒</view>
|
||||
<view class="alert-item" v-for="alert in safetyAlerts" :key="alert.id">
|
||||
<image class="alert-icon" :src="alert.icon" />
|
||||
<view class="alert-content">
|
||||
<text class="alert-title">{{ alert.title }}</text>
|
||||
<text class="alert-time">{{ formatTime(alert.time) }}</text>
|
||||
</view>
|
||||
<view class="alert-status" :class="alert.type">{{ alert.status }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
parentInfo: {
|
||||
name: '王女士',
|
||||
avatar: ''
|
||||
},
|
||||
unreadCount: 3,
|
||||
children: [
|
||||
{
|
||||
id: 1,
|
||||
name: '王小明',
|
||||
avatar: '',
|
||||
balance: 156.80,
|
||||
class: '三年级2班'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '王小红',
|
||||
avatar: '',
|
||||
balance: 98.50,
|
||||
class: '一年级1班'
|
||||
}
|
||||
],
|
||||
selectedChild: {},
|
||||
todayData: {
|
||||
consumption: '25.50',
|
||||
change: -12.5,
|
||||
meals: 3,
|
||||
accessCount: 8
|
||||
},
|
||||
recentConsumption: [
|
||||
{
|
||||
id: 1,
|
||||
merchant: '第一饭堂',
|
||||
amount: 12.50,
|
||||
balanceAfter: 144.30,
|
||||
time: new Date(Date.now() - 1800000)
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
merchant: '校园便利店',
|
||||
amount: 8.80,
|
||||
balanceAfter: 156.80,
|
||||
time: new Date(Date.now() - 3600000)
|
||||
}
|
||||
],
|
||||
nutritionData: {
|
||||
calories: 1650,
|
||||
balance: 88,
|
||||
score: 85
|
||||
},
|
||||
safetyAlerts: [
|
||||
{
|
||||
id: 1,
|
||||
title: '孩子已安全到校',
|
||||
time: new Date(Date.now() - 7200000),
|
||||
status: '正常',
|
||||
type: 'normal',
|
||||
icon: '/static/icons/check.png'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
onLoad() {
|
||||
this.selectedChild = this.children[0] || {}
|
||||
this.loadData()
|
||||
},
|
||||
onPullDownRefresh() {
|
||||
this.refreshData()
|
||||
},
|
||||
methods: {
|
||||
loadData() {
|
||||
this.loadTodayData()
|
||||
this.loadRecentConsumption()
|
||||
this.loadNutritionData()
|
||||
this.loadSafetyAlerts()
|
||||
},
|
||||
refreshData() {
|
||||
Promise.all([
|
||||
this.loadData()
|
||||
]).then(() => {
|
||||
uni.stopPullDownRefresh()
|
||||
})
|
||||
},
|
||||
selectChild(child) {
|
||||
this.selectedChild = child
|
||||
this.loadData() // 重新加载选中孩子的数据
|
||||
},
|
||||
loadTodayData() {
|
||||
// 加载今日数据
|
||||
uni.request({
|
||||
url: '/api/v1/parent/today-stats',
|
||||
data: { childId: this.selectedChild.id },
|
||||
success: (res) => {
|
||||
this.todayData = res.data
|
||||
}
|
||||
})
|
||||
},
|
||||
loadRecentConsumption() {
|
||||
// 加载最近消费
|
||||
uni.request({
|
||||
url: '/api/v1/parent/recent-consumption',
|
||||
data: { childId: this.selectedChild.id },
|
||||
success: (res) => {
|
||||
this.recentConsumption = res.data
|
||||
}
|
||||
})
|
||||
},
|
||||
loadNutritionData() {
|
||||
// 加载营养数据
|
||||
uni.request({
|
||||
url: '/api/v1/parent/nutrition',
|
||||
data: { childId: this.selectedChild.id },
|
||||
success: (res) => {
|
||||
this.nutritionData = res.data
|
||||
}
|
||||
})
|
||||
},
|
||||
loadSafetyAlerts() {
|
||||
// 加载安全提醒
|
||||
uni.request({
|
||||
url: '/api/v1/parent/safety-alerts',
|
||||
data: { childId: this.selectedChild.id },
|
||||
success: (res) => {
|
||||
this.safetyAlerts = res.data
|
||||
}
|
||||
})
|
||||
},
|
||||
goToNotifications() {
|
||||
uni.navigateTo({ url: '/pages/mall/nfc/parent/notifications' })
|
||||
},
|
||||
goToRecharge() {
|
||||
uni.navigateTo({
|
||||
url: `/pages/mall/nfc/parent/recharge-for-child?childId=${this.selectedChild.id}`
|
||||
})
|
||||
},
|
||||
goToSpendingLimit() {
|
||||
uni.navigateTo({
|
||||
url: `/pages/mall/nfc/parent/spending-limit?childId=${this.selectedChild.id}`
|
||||
})
|
||||
},
|
||||
goToNutritionReport() {
|
||||
uni.navigateTo({
|
||||
url: `/pages/mall/nfc/parent/nutrition-report?childId=${this.selectedChild.id}`
|
||||
})
|
||||
},
|
||||
goToAccessAlerts() {
|
||||
uni.navigateTo({ url: '/pages/mall/nfc/parent/access-alerts' })
|
||||
},
|
||||
goToConsumptionMonitor() {
|
||||
uni.navigateTo({ url: '/pages/mall/nfc/parent/consumption-monitor' })
|
||||
},
|
||||
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>
|
||||
.parent-home {
|
||||
padding: 20rpx;
|
||||
background-color: #f8f9fa;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.parent-header {
|
||||
background: linear-gradient(135deg, #FF6B35 0%, #F7931E 100%);
|
||||
border-radius: 16rpx;
|
||||
padding: 30rpx;
|
||||
margin-bottom: 20rpx;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.parent-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
border-radius: 40rpx;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.name {
|
||||
color: white;
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.role {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.notification-btn {
|
||||
position: relative;
|
||||
padding: 16rpx;
|
||||
}
|
||||
|
||||
.bell-icon {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
}
|
||||
|
||||
.badge {
|
||||
position: absolute;
|
||||
top: 8rpx;
|
||||
right: 8rpx;
|
||||
background: #ff3b30;
|
||||
color: white;
|
||||
font-size: 20rpx;
|
||||
padding: 4rpx 8rpx;
|
||||
border-radius: 12rpx;
|
||||
min-width: 24rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.children-selector {
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.children-scroll {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.child-item {
|
||||
display: inline-block;
|
||||
background: white;
|
||||
border-radius: 16rpx;
|
||||
padding: 20rpx;
|
||||
margin-right: 16rpx;
|
||||
text-align: center;
|
||||
min-width: 140rpx;
|
||||
border: 2rpx solid transparent;
|
||||
}
|
||||
|
||||
.child-item.active {
|
||||
border-color: #FF6B35;
|
||||
}
|
||||
|
||||
.child-avatar {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
border-radius: 30rpx;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.child-name {
|
||||
display: block;
|
||||
font-size: 24rpx;
|
||||
color: #333;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.child-balance {
|
||||
font-size: 20rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.today-overview, .quick-actions, .recent-consumption,
|
||||
.nutrition-health, .safety-alerts {
|
||||
background: white;
|
||||
border-radius: 16rpx;
|
||||
padding: 30rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.view-more {
|
||||
font-size: 24rpx;
|
||||
color: #FF6B35;
|
||||
}
|
||||
|
||||
.overview-cards {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.overview-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.card-value {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
color: #FF6B35;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.card-label {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
margin-bottom: 4rpx;
|
||||
}
|
||||
|
||||
.card-change {
|
||||
font-size: 20rpx;
|
||||
color: #28a745;
|
||||
}
|
||||
|
||||
.card-change.increase {
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
.quick-actions {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.action-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.action-icon {
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.action-text {
|
||||
font-size: 24rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.consumption-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20rpx 0;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.consumption-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.consumption-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.merchant {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.time {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.consumption-amount {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.amount {
|
||||
font-size: 28rpx;
|
||||
color: #ff3b30;
|
||||
font-weight: bold;
|
||||
margin-bottom: 4rpx;
|
||||
}
|
||||
|
||||
.balance {
|
||||
font-size: 20rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.nutrition-summary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.nutrition-chart {
|
||||
width: 200rpx;
|
||||
height: 200rpx;
|
||||
margin-right: 30rpx;
|
||||
}
|
||||
|
||||
.chart {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.nutrition-details {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.nutrition-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.nutrition-label {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.nutrition-value {
|
||||
font-size: 24rpx;
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.alert-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 20rpx 0;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.alert-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.alert-icon {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.alert-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.alert-title {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.alert-time {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.alert-status {
|
||||
font-size: 24rpx;
|
||||
padding: 8rpx 16rpx;
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
|
||||
.alert-status.normal {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.alert-status.warning {
|
||||
background: #fff3cd;
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
.alert-status.danger {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
</style>
|
||||
1100
pages/mall/nfc/security/index.uvue
Normal file
1100
pages/mall/nfc/security/index.uvue
Normal file
File diff suppressed because it is too large
Load Diff
373
pages/mall/nfc/student/index.uvue
Normal file
373
pages/mall/nfc/student/index.uvue
Normal file
@@ -0,0 +1,373 @@
|
||||
<template>
|
||||
<view class="student-home">
|
||||
<!-- 顶部用户信息卡片 -->
|
||||
<view class="user-card">
|
||||
<view class="user-info">
|
||||
<image class="avatar" :src="userInfo.avatar || '/static/default-avatar.png'" />
|
||||
<view class="info">
|
||||
<text class="name">{{ userInfo.name }}</text>
|
||||
<text class="student-id">学号: {{ userInfo.studentId }}</text>
|
||||
<text class="class">{{ userInfo.class }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="balance-info" @click="goToBalance">
|
||||
<text class="balance-label">余额</text>
|
||||
<text class="balance-amount">¥{{ userInfo.balance }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 快速操作区 -->
|
||||
<view class="quick-actions">
|
||||
<view class="action-item" @click="goToNFCPay">
|
||||
<image class="action-icon" src="/static/icons/nfc-pay.png" />
|
||||
<text class="action-text">NFC支付</text>
|
||||
</view>
|
||||
<view class="action-item" @click="goToQRCode">
|
||||
<image class="action-icon" src="/static/icons/qr-code.png" />
|
||||
<text class="action-text">付款码</text>
|
||||
</view>
|
||||
<view class="action-item" @click="goToRecharge">
|
||||
<image class="action-icon" src="/static/icons/recharge.png" />
|
||||
<text class="action-text">充值</text>
|
||||
</view>
|
||||
<view class="action-item" @click="goToTransactions">
|
||||
<image class="action-icon" src="/static/icons/history.png" />
|
||||
<text class="action-text">消费记录</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 校园服务 -->
|
||||
<view class="campus-services">
|
||||
<view class="section-title">校园服务</view>
|
||||
<view class="service-grid">
|
||||
<view class="service-item" @click="goToCanteen">
|
||||
<image class="service-icon" src="/static/icons/restaurant.png" />
|
||||
<text class="service-name">饭堂消费</text>
|
||||
</view>
|
||||
<view class="service-item" @click="goToShop">
|
||||
<image class="service-icon" src="/static/icons/shop.png" />
|
||||
<text class="service-name">小卖部</text>
|
||||
</view>
|
||||
<view class="service-item" @click="goToLibrary">
|
||||
<image class="service-icon" src="/static/icons/library.png" />
|
||||
<text class="service-name">图书借阅</text>
|
||||
</view>
|
||||
<view class="service-item" @click="goToAccessLog">
|
||||
<image class="service-icon" src="/static/icons/access.png" />
|
||||
<text class="service-name">进出记录</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 今日数据 -->
|
||||
<view class="today-data">
|
||||
<view class="section-title">今日数据</view>
|
||||
<view class="data-cards">
|
||||
<view class="data-card">
|
||||
<text class="data-value">{{ todayData.consumption }}</text>
|
||||
<text class="data-label">今日消费</text>
|
||||
</view>
|
||||
<view class="data-card">
|
||||
<text class="data-value">{{ todayData.transactions }}</text>
|
||||
<text class="data-label">交易次数</text>
|
||||
</view>
|
||||
<view class="data-card">
|
||||
<text class="data-value">{{ todayData.access }}</text>
|
||||
<text class="data-label">进出次数</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 营养分析 -->
|
||||
<view class="nutrition-section" @click="goToNutrition">
|
||||
<view class="section-title">营养分析</view>
|
||||
<view class="nutrition-summary">
|
||||
<view class="nutrition-item">
|
||||
<text class="nutrition-label">今日卡路里</text>
|
||||
<text class="nutrition-value">{{ nutritionData.calories }}kcal</text>
|
||||
</view>
|
||||
<view class="nutrition-item">
|
||||
<text class="nutrition-label">营养均衡度</text>
|
||||
<text class="nutrition-value">{{ nutritionData.balance }}%</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
userInfo: {
|
||||
name: '张三',
|
||||
studentId: '2024001',
|
||||
class: '计算机科学与技术1班',
|
||||
avatar: '',
|
||||
balance: 156.80
|
||||
},
|
||||
todayData: {
|
||||
consumption: '25.50',
|
||||
transactions: 3,
|
||||
access: 8
|
||||
},
|
||||
nutritionData: {
|
||||
calories: 1850,
|
||||
balance: 85
|
||||
}
|
||||
}
|
||||
},
|
||||
onLoad() {
|
||||
this.loadUserInfo()
|
||||
this.loadTodayData()
|
||||
this.loadNutritionData()
|
||||
},
|
||||
onPullDownRefresh() {
|
||||
this.refreshData()
|
||||
},
|
||||
methods: {
|
||||
loadUserInfo() {
|
||||
// 加载用户信息
|
||||
uni.request({
|
||||
url: '/api/v1/student/profile',
|
||||
success: (res) => {
|
||||
this.userInfo = res.data
|
||||
}
|
||||
})
|
||||
},
|
||||
loadTodayData() {
|
||||
// 加载今日数据
|
||||
uni.request({
|
||||
url: '/api/v1/student/today-stats',
|
||||
success: (res) => {
|
||||
this.todayData = res.data
|
||||
}
|
||||
})
|
||||
},
|
||||
loadNutritionData() {
|
||||
// 加载营养数据
|
||||
uni.request({
|
||||
url: '/api/v1/student/nutrition',
|
||||
success: (res) => {
|
||||
this.nutritionData = res.data
|
||||
}
|
||||
})
|
||||
},
|
||||
refreshData() {
|
||||
Promise.all([
|
||||
this.loadUserInfo(),
|
||||
this.loadTodayData(),
|
||||
this.loadNutritionData()
|
||||
]).then(() => {
|
||||
uni.stopPullDownRefresh()
|
||||
})
|
||||
},
|
||||
goToBalance() {
|
||||
uni.navigateTo({ url: '/pages/mall/nfc/student/balance' })
|
||||
},
|
||||
goToNFCPay() {
|
||||
uni.navigateTo({ url: '/pages/mall/nfc/student/nfc-pay' })
|
||||
},
|
||||
goToQRCode() {
|
||||
uni.navigateTo({ url: '/pages/mall/nfc/student/qr-code' })
|
||||
},
|
||||
goToRecharge() {
|
||||
uni.navigateTo({ url: '/pages/mall/nfc/student/recharge' })
|
||||
},
|
||||
goToTransactions() {
|
||||
uni.navigateTo({ url: '/pages/mall/nfc/student/transactions' })
|
||||
},
|
||||
goToCanteen() {
|
||||
uni.navigateTo({ url: '/pages/mall/nfc/student/canteen' })
|
||||
},
|
||||
goToShop() {
|
||||
uni.navigateTo({ url: '/pages/mall/nfc/student/shop' })
|
||||
},
|
||||
goToLibrary() {
|
||||
uni.navigateTo({ url: '/pages/mall/nfc/student/library' })
|
||||
},
|
||||
goToAccessLog() {
|
||||
uni.navigateTo({ url: '/pages/mall/nfc/student/access-log' })
|
||||
},
|
||||
goToNutrition() {
|
||||
uni.navigateTo({ url: '/pages/mall/nfc/student/nutrition' })
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.student-home {
|
||||
padding: 20rpx;
|
||||
background-color: #f8f9fa;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.user-card {
|
||||
background: linear-gradient(135deg, #007AFF 0%, #5AC8FA 100%);
|
||||
border-radius: 16rpx;
|
||||
padding: 30rpx;
|
||||
margin-bottom: 20rpx;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
border-radius: 40rpx;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.name {
|
||||
color: white;
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.student-id, .class {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
font-size: 24rpx;
|
||||
margin-bottom: 4rpx;
|
||||
}
|
||||
|
||||
.balance-info {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.balance-label {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
font-size: 24rpx;
|
||||
display: block;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.balance-amount {
|
||||
color: white;
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.quick-actions {
|
||||
display: flex;
|
||||
background: white;
|
||||
border-radius: 16rpx;
|
||||
padding: 30rpx;
|
||||
margin-bottom: 20rpx;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.action-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.action-icon {
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.action-text {
|
||||
font-size: 24rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.campus-services, .today-data, .nutrition-section {
|
||||
background: white;
|
||||
border-radius: 16rpx;
|
||||
padding: 30rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.service-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.service-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.service-icon {
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.service-name {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.data-cards {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.data-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.data-value {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
color: #007AFF;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.data-label {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.nutrition-summary {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.nutrition-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.nutrition-label {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.nutrition-value {
|
||||
font-size: 28rpx;
|
||||
font-weight: bold;
|
||||
color: #28a745;
|
||||
}
|
||||
</style>
|
||||
452
pages/mall/nfc/student/nfc-pay.uvue
Normal file
452
pages/mall/nfc/student/nfc-pay.uvue
Normal file
@@ -0,0 +1,452 @@
|
||||
<template>
|
||||
<view class="nfc-pay">
|
||||
<!-- NFC状态指示 -->
|
||||
<view class="nfc-status" :class="{ 'active': nfcStatus === 'ready' }">
|
||||
<image class="nfc-icon" src="/static/icons/nfc-large.png" />
|
||||
<text class="status-text">{{ statusText }}</text>
|
||||
<text class="status-desc">{{ statusDesc }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 支付信息 -->
|
||||
<view class="payment-info" v-if="paymentAmount > 0">
|
||||
<view class="amount-display">
|
||||
<text class="currency">¥</text>
|
||||
<text class="amount">{{ paymentAmount.toFixed(2) }}</text>
|
||||
</view>
|
||||
<text class="merchant-name">{{ merchantName }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<view class="action-buttons">
|
||||
<button class="btn-primary" @click="enableNFC" v-if="nfcStatus === 'disabled'">
|
||||
启用NFC
|
||||
</button>
|
||||
<button class="btn-secondary" @click="switchToQR" v-if="nfcStatus === 'ready'">
|
||||
切换到扫码支付
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<!-- 余额显示 -->
|
||||
<view class="balance-display">
|
||||
<text class="balance-label">当前余额</text>
|
||||
<text class="balance-amount">¥{{ userBalance.toFixed(2) }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 最近交易 -->
|
||||
<view class="recent-transactions" v-if="recentTransactions.length > 0">
|
||||
<view class="section-title">最近交易</view>
|
||||
<view class="transaction-item" v-for="item in recentTransactions" :key="item.id">
|
||||
<view class="transaction-info">
|
||||
<text class="merchant">{{ item.merchant }}</text>
|
||||
<text class="time">{{ formatTime(item.time) }}</text>
|
||||
</view>
|
||||
<text class="amount">-¥{{ item.amount.toFixed(2) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- NFC支付动画 -->
|
||||
<view class="nfc-animation" v-if="isPaymentProcessing">
|
||||
<view class="ripple"></view>
|
||||
<view class="ripple ripple-2"></view>
|
||||
<view class="ripple ripple-3"></view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
nfcStatus: 'checking', // checking, disabled, ready, processing
|
||||
paymentAmount: 0,
|
||||
merchantName: '',
|
||||
userBalance: 156.80,
|
||||
isPaymentProcessing: false,
|
||||
recentTransactions: [
|
||||
{
|
||||
id: 1,
|
||||
merchant: '第一饭堂',
|
||||
amount: 12.50,
|
||||
time: new Date(Date.now() - 3600000)
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
merchant: '校园便利店',
|
||||
amount: 8.80,
|
||||
time: new Date(Date.now() - 7200000)
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
statusText() {
|
||||
const statusMap = {
|
||||
checking: '检查NFC状态...',
|
||||
disabled: 'NFC未启用',
|
||||
ready: '请将手机靠近读卡器',
|
||||
processing: '支付处理中...'
|
||||
}
|
||||
return statusMap[this.nfcStatus] || ''
|
||||
},
|
||||
statusDesc() {
|
||||
const descMap = {
|
||||
checking: '正在检测设备NFC功能',
|
||||
disabled: '请在设置中启用NFC功能',
|
||||
ready: '保持手机靠近直到支付完成',
|
||||
processing: '请勿移动手机'
|
||||
}
|
||||
return descMap[this.nfcStatus] || ''
|
||||
}
|
||||
},
|
||||
onLoad(options) {
|
||||
this.paymentAmount = parseFloat(options.amount || 0)
|
||||
this.merchantName = options.merchant || ''
|
||||
this.checkNFCStatus()
|
||||
},
|
||||
onShow() {
|
||||
this.initNFCPayment()
|
||||
},
|
||||
onHide() {
|
||||
this.stopNFCPayment()
|
||||
},
|
||||
methods: {
|
||||
checkNFCStatus() {
|
||||
// 检查NFC状态
|
||||
// #ifdef APP-PLUS
|
||||
plus.nfc.isAvailable((available) => {
|
||||
if (available) {
|
||||
plus.nfc.isEnabled((enabled) => {
|
||||
this.nfcStatus = enabled ? 'ready' : 'disabled'
|
||||
})
|
||||
} else {
|
||||
this.nfcStatus = 'disabled'
|
||||
uni.showToast({
|
||||
title: '设备不支持NFC',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
})
|
||||
// #endif
|
||||
|
||||
// #ifndef APP-PLUS
|
||||
// Web环境模拟
|
||||
setTimeout(() => {
|
||||
this.nfcStatus = 'ready'
|
||||
}, 1000)
|
||||
// #endif
|
||||
},
|
||||
enableNFC() {
|
||||
// #ifdef APP-PLUS
|
||||
plus.nfc.enable(() => {
|
||||
this.nfcStatus = 'ready'
|
||||
this.initNFCPayment()
|
||||
}, (error) => {
|
||||
uni.showToast({
|
||||
title: '启用NFC失败',
|
||||
icon: 'none'
|
||||
})
|
||||
})
|
||||
// #endif
|
||||
},
|
||||
initNFCPayment() {
|
||||
if (this.nfcStatus === 'ready') {
|
||||
// 初始化NFC支付监听
|
||||
this.startNFCListening()
|
||||
}
|
||||
},
|
||||
startNFCListening() {
|
||||
// #ifdef APP-PLUS
|
||||
plus.nfc.addEventListener('tag', this.handleNFCTag)
|
||||
// #endif
|
||||
},
|
||||
stopNFCPayment() {
|
||||
// #ifdef APP-PLUS
|
||||
plus.nfc.removeEventListener('tag', this.handleNFCTag)
|
||||
// #endif
|
||||
},
|
||||
handleNFCTag(tag) {
|
||||
if (this.paymentAmount <= 0) {
|
||||
uni.showToast({
|
||||
title: '无效的支付金额',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
this.processPayment(tag)
|
||||
},
|
||||
processPayment(tag) {
|
||||
this.isPaymentProcessing = true
|
||||
this.nfcStatus = 'processing'
|
||||
|
||||
// 模拟支付处理
|
||||
setTimeout(() => {
|
||||
const success = Math.random() > 0.1 // 90% 成功率
|
||||
|
||||
if (success) {
|
||||
this.userBalance -= this.paymentAmount
|
||||
this.addRecentTransaction()
|
||||
|
||||
uni.showToast({
|
||||
title: '支付成功',
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
uni.navigateBack()
|
||||
}, 1500)
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: '支付失败,请重试',
|
||||
icon: 'none'
|
||||
})
|
||||
this.nfcStatus = 'ready'
|
||||
}
|
||||
|
||||
this.isPaymentProcessing = false
|
||||
}, 2000)
|
||||
},
|
||||
addRecentTransaction() {
|
||||
this.recentTransactions.unshift({
|
||||
id: Date.now(),
|
||||
merchant: this.merchantName || '校园商户',
|
||||
amount: this.paymentAmount,
|
||||
time: new Date()
|
||||
})
|
||||
|
||||
if (this.recentTransactions.length > 5) {
|
||||
this.recentTransactions.pop()
|
||||
}
|
||||
},
|
||||
switchToQR() {
|
||||
uni.navigateTo({
|
||||
url: `/pages/mall/nfc/student/qr-code?amount=${this.paymentAmount}&merchant=${this.merchantName}`
|
||||
})
|
||||
},
|
||||
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>
|
||||
.nfc-pay {
|
||||
padding: 40rpx;
|
||||
background-color: #f8f9fa;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.nfc-status {
|
||||
background: white;
|
||||
border-radius: 20rpx;
|
||||
padding: 60rpx 40rpx;
|
||||
margin-bottom: 40rpx;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.nfc-status.active {
|
||||
background: linear-gradient(135deg, #007AFF 0%, #5AC8FA 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.nfc-icon {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.status-text {
|
||||
display: block;
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.status-desc {
|
||||
display: block;
|
||||
font-size: 24rpx;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.payment-info {
|
||||
background: white;
|
||||
border-radius: 16rpx;
|
||||
padding: 40rpx;
|
||||
margin-bottom: 30rpx;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.amount-display {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: baseline;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.currency {
|
||||
font-size: 36rpx;
|
||||
color: #333;
|
||||
margin-right: 8rpx;
|
||||
}
|
||||
|
||||
.amount {
|
||||
font-size: 72rpx;
|
||||
font-weight: bold;
|
||||
color: #007AFF;
|
||||
}
|
||||
|
||||
.merchant-name {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
width: 100%;
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.btn-primary, .btn-secondary {
|
||||
width: 100%;
|
||||
padding: 24rpx;
|
||||
border-radius: 12rpx;
|
||||
font-size: 32rpx;
|
||||
border: none;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #007AFF;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: white;
|
||||
color: #007AFF;
|
||||
border: 2rpx solid #007AFF;
|
||||
}
|
||||
|
||||
.balance-display {
|
||||
background: white;
|
||||
border-radius: 16rpx;
|
||||
padding: 30rpx;
|
||||
margin-bottom: 30rpx;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.balance-label {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.balance-amount {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #28a745;
|
||||
}
|
||||
|
||||
.recent-transactions {
|
||||
background: white;
|
||||
border-radius: 16rpx;
|
||||
padding: 30rpx;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.transaction-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20rpx 0;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.transaction-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.transaction-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.merchant {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.time {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.transaction-item .amount {
|
||||
font-size: 28rpx;
|
||||
color: #ff3b30;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.nfc-animation {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 200rpx;
|
||||
height: 200rpx;
|
||||
}
|
||||
|
||||
.ripple {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
border: 4rpx solid #007AFF;
|
||||
animation: ripple 2s infinite;
|
||||
}
|
||||
|
||||
.ripple-2 {
|
||||
animation-delay: 0.5s;
|
||||
}
|
||||
|
||||
.ripple-3 {
|
||||
animation-delay: 1s;
|
||||
}
|
||||
|
||||
@keyframes ripple {
|
||||
0% {
|
||||
transform: scale(0.3);
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
912
pages/mall/nfc/teacher/index.uvue
Normal file
912
pages/mall/nfc/teacher/index.uvue
Normal file
@@ -0,0 +1,912 @@
|
||||
<template>
|
||||
<view class="teacher-workspace">
|
||||
<!-- 教师信息头部 -->
|
||||
<view class="teacher-header">
|
||||
<view class="teacher-info">
|
||||
<image class="avatar" :src="teacherInfo.avatar || '/static/default-avatar.png'" />
|
||||
<view class="info">
|
||||
<text class="name">{{ teacherInfo.name }}</text>
|
||||
<text class="department">{{ teacherInfo.department }}</text>
|
||||
<text class="title">{{ teacherInfo.title }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="header-actions">
|
||||
<view class="attendance-btn" @click="quickAttendance">
|
||||
<image class="btn-icon" src="/static/icons/attendance.png" />
|
||||
<text class="btn-text">打卡</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 今日课程安排 -->
|
||||
<view class="today-schedule">
|
||||
<view class="section-header">
|
||||
<text class="section-title">今日课程</text>
|
||||
<text class="date-info">{{ formatDate(new Date()) }}</text>
|
||||
</view>
|
||||
|
||||
<scroll-view scroll-x="true" class="schedule-scroll">
|
||||
<view class="schedule-item"
|
||||
v-for="course in todayCourses"
|
||||
:key="course.id"
|
||||
:class="{ 'current': course.isCurrent, 'completed': course.isCompleted }">
|
||||
<view class="time-info">
|
||||
<text class="time-period">{{ course.startTime }}-{{ course.endTime }}</text>
|
||||
<text class="course-status">{{ getCourseStatus(course) }}</text>
|
||||
</view>
|
||||
<view class="course-info">
|
||||
<text class="course-name">{{ course.name }}</text>
|
||||
<text class="course-location">{{ course.location }}</text>
|
||||
<text class="student-count">{{ course.studentCount }}人</text>
|
||||
</view>
|
||||
<button class="course-action"
|
||||
v-if="course.isCurrent"
|
||||
@click="manageCourse(course)">
|
||||
管理课堂
|
||||
</button>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<!-- 班级管理概览 -->
|
||||
<view class="class-overview">
|
||||
<view class="section-title">班级概览</view>
|
||||
<view class="class-stats">
|
||||
<view class="stat-card">
|
||||
<text class="stat-value">{{ classStats.totalStudents }}</text>
|
||||
<text class="stat-label">管理学生</text>
|
||||
</view>
|
||||
<view class="stat-card">
|
||||
<text class="stat-value">{{ classStats.presentToday }}</text>
|
||||
<text class="stat-label">今日出勤</text>
|
||||
</view>
|
||||
<view class="stat-card">
|
||||
<text class="stat-value">{{ classStats.avgConsumption }}</text>
|
||||
<text class="stat-label">日均消费</text>
|
||||
</view>
|
||||
<view class="stat-card">
|
||||
<text class="stat-value">{{ classStats.pendingApprovals }}</text>
|
||||
<text class="stat-label">待审批</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 快速功能 -->
|
||||
<view class="quick-functions">
|
||||
<view class="section-title">快速功能</view>
|
||||
<view class="function-grid">
|
||||
<view class="function-item" @click="goToStudentManagement">
|
||||
<image class="function-icon" src="/static/icons/students.png" />
|
||||
<text class="function-text">学生管理</text>
|
||||
<view class="function-badge" v-if="newStudentNotifications > 0">
|
||||
{{ newStudentNotifications }}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="function-item" @click="goToAttendance">
|
||||
<image class="function-icon" src="/static/icons/attendance.png" />
|
||||
<text class="function-text">考勤管理</text>
|
||||
</view>
|
||||
|
||||
<view class="function-item" @click="goToClassConsumption">
|
||||
<image class="function-icon" src="/static/icons/consumption.png" />
|
||||
<text class="function-text">班级消费</text>
|
||||
</view>
|
||||
|
||||
<view class="function-item" @click="goToVisitorApproval">
|
||||
<image class="function-icon" src="/static/icons/visitor.png" />
|
||||
<text class="function-text">访客审批</text>
|
||||
<view class="function-badge" v-if="pendingVisitors > 0">
|
||||
{{ pendingVisitors }}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="function-item" @click="goToLibraryManagement">
|
||||
<image class="function-icon" src="/static/icons/library.png" />
|
||||
<text class="function-text">图书管理</text>
|
||||
</view>
|
||||
|
||||
<view class="function-item" @click="goToReports">
|
||||
<image class="function-icon" src="/static/icons/report.png" />
|
||||
<text class="function-text">数据报告</text>
|
||||
</view>
|
||||
|
||||
<view class="function-item emergency" @click="goToEmergencyUnlock">
|
||||
<image class="function-icon" src="/static/icons/emergency.png" />
|
||||
<text class="function-text">紧急开门</text>
|
||||
</view>
|
||||
|
||||
<view class="function-item" @click="goToNotifications">
|
||||
<image class="function-icon" src="/static/icons/notifications.png" />
|
||||
<text class="function-text">消息通知</text>
|
||||
<view class="function-badge" v-if="unreadNotifications > 0">
|
||||
{{ unreadNotifications }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 学生动态 -->
|
||||
<view class="student-activities">
|
||||
<view class="section-header">
|
||||
<text class="section-title">学生动态</text>
|
||||
<text class="view-more" @click="goToStudentManagement">查看更多</text>
|
||||
</view>
|
||||
|
||||
<view class="activities-list">
|
||||
<view class="activity-item" v-for="activity in recentActivities" :key="activity.id">
|
||||
<view class="activity-avatar">
|
||||
<image :src="activity.studentAvatar || '/static/default-avatar.png'" />
|
||||
</view>
|
||||
<view class="activity-content">
|
||||
<view class="activity-header">
|
||||
<text class="student-name">{{ activity.studentName }}</text>
|
||||
<text class="activity-time">{{ formatTime(activity.time) }}</text>
|
||||
</view>
|
||||
<text class="activity-desc">{{ activity.description }}</text>
|
||||
<view class="activity-details" v-if="activity.details">
|
||||
<text class="detail-item" v-for="detail in activity.details" :key="detail">
|
||||
{{ detail }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="activity-status" :class="activity.status">
|
||||
{{ getActivityStatusText(activity.status) }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 门禁权限管理 -->
|
||||
<view class="access-management">
|
||||
<view class="section-title">门禁权限</view>
|
||||
<view class="access-summary">
|
||||
<view class="permission-item">
|
||||
<text class="permission-name">教师办公室</text>
|
||||
<text class="permission-status active">有权限</text>
|
||||
</view>
|
||||
<view class="permission-item">
|
||||
<text class="permission-name">实验室A101</text>
|
||||
<text class="permission-status active">有权限</text>
|
||||
</view>
|
||||
<view class="permission-item">
|
||||
<text class="permission-name">会议室301</text>
|
||||
<text class="permission-status pending">申请中</text>
|
||||
</view>
|
||||
<view class="permission-item">
|
||||
<text class="permission-name">图书馆管理区</text>
|
||||
<text class="permission-status inactive">无权限</text>
|
||||
</view>
|
||||
</view>
|
||||
<button class="apply-permission-btn" @click="applyForPermission">
|
||||
申请新权限
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<!-- 消息中心 -->
|
||||
<view class="message-center" v-if="importantMessages.length > 0">
|
||||
<view class="section-title">重要通知</view>
|
||||
<view class="message-list">
|
||||
<view class="message-item"
|
||||
v-for="message in importantMessages"
|
||||
:key="message.id"
|
||||
@click="viewMessage(message)">
|
||||
<view class="message-icon" :class="message.type">
|
||||
<image :src="getMessageIcon(message.type)" />
|
||||
</view>
|
||||
<view class="message-content">
|
||||
<text class="message-title">{{ message.title }}</text>
|
||||
<text class="message-preview">{{ message.preview }}</text>
|
||||
</view>
|
||||
<view class="message-time">
|
||||
{{ formatTime(message.time) }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
teacherInfo: {
|
||||
name: '李老师',
|
||||
department: '计算机科学学院',
|
||||
title: '副教授',
|
||||
avatar: ''
|
||||
},
|
||||
todayCourses: [
|
||||
{
|
||||
id: 1,
|
||||
name: '数据结构',
|
||||
startTime: '08:00',
|
||||
endTime: '09:40',
|
||||
location: 'A101',
|
||||
studentCount: 45,
|
||||
isCompleted: true,
|
||||
isCurrent: false
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '算法设计',
|
||||
startTime: '10:00',
|
||||
endTime: '11:40',
|
||||
location: 'A102',
|
||||
studentCount: 38,
|
||||
isCompleted: false,
|
||||
isCurrent: true
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: '软件工程',
|
||||
startTime: '14:00',
|
||||
endTime: '15:40',
|
||||
location: 'A103',
|
||||
studentCount: 42,
|
||||
isCompleted: false,
|
||||
isCurrent: false
|
||||
}
|
||||
],
|
||||
classStats: {
|
||||
totalStudents: 125,
|
||||
presentToday: 118,
|
||||
avgConsumption: '25.6',
|
||||
pendingApprovals: 3
|
||||
},
|
||||
newStudentNotifications: 2,
|
||||
pendingVisitors: 1,
|
||||
unreadNotifications: 5,
|
||||
recentActivities: [
|
||||
{
|
||||
id: 1,
|
||||
studentName: '张小明',
|
||||
studentAvatar: '',
|
||||
description: '在第一饭堂消费',
|
||||
details: ['消费金额: ¥12.50', '余额: ¥98.30'],
|
||||
time: new Date(Date.now() - 1800000),
|
||||
status: 'normal'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
studentName: '王小红',
|
||||
studentAvatar: '',
|
||||
description: '申请图书续借',
|
||||
details: ['《数据结构与算法》', '申请续借7天'],
|
||||
time: new Date(Date.now() - 3600000),
|
||||
status: 'pending'
|
||||
}
|
||||
],
|
||||
importantMessages: [
|
||||
{
|
||||
id: 1,
|
||||
type: 'urgent',
|
||||
title: '紧急通知',
|
||||
preview: '关于加强校园疫情防控的通知...',
|
||||
time: new Date(Date.now() - 7200000)
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
onLoad() {
|
||||
this.loadTeacherData()
|
||||
},
|
||||
onPullDownRefresh() {
|
||||
this.refreshData()
|
||||
},
|
||||
methods: {
|
||||
loadTeacherData() {
|
||||
// 加载教师相关数据
|
||||
this.loadTodayCourses()
|
||||
this.loadClassStats()
|
||||
this.loadRecentActivities()
|
||||
this.loadMessages()
|
||||
},
|
||||
refreshData() {
|
||||
Promise.all([
|
||||
this.loadTeacherData()
|
||||
]).then(() => {
|
||||
uni.stopPullDownRefresh()
|
||||
})
|
||||
},
|
||||
loadTodayCourses() {
|
||||
uni.request({
|
||||
url: '/api/v1/teacher/today-courses',
|
||||
success: (res) => {
|
||||
this.todayCourses = res.data
|
||||
}
|
||||
})
|
||||
},
|
||||
loadClassStats() {
|
||||
uni.request({
|
||||
url: '/api/v1/teacher/class-stats',
|
||||
success: (res) => {
|
||||
this.classStats = res.data
|
||||
}
|
||||
})
|
||||
},
|
||||
loadRecentActivities() {
|
||||
uni.request({
|
||||
url: '/api/v1/teacher/student-activities',
|
||||
success: (res) => {
|
||||
this.recentActivities = res.data
|
||||
}
|
||||
})
|
||||
},
|
||||
loadMessages() {
|
||||
uni.request({
|
||||
url: '/api/v1/teacher/messages',
|
||||
success: (res) => {
|
||||
this.importantMessages = res.data
|
||||
}
|
||||
})
|
||||
},
|
||||
quickAttendance() {
|
||||
// 快速打卡
|
||||
uni.showLoading({ title: '打卡中...' })
|
||||
|
||||
uni.request({
|
||||
url: '/api/v1/teacher/attendance',
|
||||
method: 'POST',
|
||||
success: () => {
|
||||
uni.hideLoading()
|
||||
uni.showToast({
|
||||
title: '打卡成功',
|
||||
icon: 'success'
|
||||
})
|
||||
},
|
||||
fail: () => {
|
||||
uni.hideLoading()
|
||||
uni.showToast({
|
||||
title: '打卡失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
manageCourse(course) {
|
||||
uni.navigateTo({
|
||||
url: `/pages/mall/nfc/teacher/course-management?courseId=${course.id}`
|
||||
})
|
||||
},
|
||||
getCourseStatus(course) {
|
||||
if (course.isCompleted) return '已结束'
|
||||
if (course.isCurrent) return '进行中'
|
||||
return '未开始'
|
||||
},
|
||||
getActivityStatusText(status) {
|
||||
const statusMap = {
|
||||
normal: '正常',
|
||||
pending: '待处理',
|
||||
warning: '注意',
|
||||
error: '异常'
|
||||
}
|
||||
return statusMap[status] || '未知'
|
||||
},
|
||||
getMessageIcon(type) {
|
||||
const icons = {
|
||||
urgent: '/static/icons/urgent.png',
|
||||
info: '/static/icons/info.png',
|
||||
warning: '/static/icons/warning.png'
|
||||
}
|
||||
return icons[type] || icons.info
|
||||
},
|
||||
// 导航方法
|
||||
goToStudentManagement() {
|
||||
uni.navigateTo({ url: '/pages/mall/nfc/teacher/student-management' })
|
||||
},
|
||||
goToAttendance() {
|
||||
uni.navigateTo({ url: '/pages/mall/nfc/teacher/attendance' })
|
||||
},
|
||||
goToClassConsumption() {
|
||||
uni.navigateTo({ url: '/pages/mall/nfc/teacher/class-consumption' })
|
||||
},
|
||||
goToVisitorApproval() {
|
||||
uni.navigateTo({ url: '/pages/mall/nfc/teacher/visitor-approval' })
|
||||
},
|
||||
goToLibraryManagement() {
|
||||
uni.navigateTo({ url: '/pages/mall/nfc/teacher/library-management' })
|
||||
},
|
||||
goToReports() {
|
||||
uni.navigateTo({ url: '/pages/mall/nfc/teacher/reports' })
|
||||
},
|
||||
goToEmergencyUnlock() {
|
||||
uni.showModal({
|
||||
title: '紧急开门',
|
||||
content: '确认要进行紧急开门操作吗?此操作将被记录。',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
uni.navigateTo({ url: '/pages/mall/nfc/teacher/emergency-unlock' })
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
goToNotifications() {
|
||||
uni.navigateTo({ url: '/pages/mall/nfc/teacher/notifications' })
|
||||
},
|
||||
applyForPermission() {
|
||||
uni.navigateTo({ url: '/pages/mall/nfc/teacher/permission-application' })
|
||||
},
|
||||
viewMessage(message) {
|
||||
uni.navigateTo({
|
||||
url: `/pages/mall/nfc/teacher/message-detail?id=${message.id}`
|
||||
})
|
||||
},
|
||||
formatDate(date) {
|
||||
return date.toLocaleDateString('zh-CN', {
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
weekday: 'long'
|
||||
})
|
||||
},
|
||||
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>
|
||||
.teacher-workspace {
|
||||
padding: 20rpx;
|
||||
background-color: #f8f9fa;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.teacher-header {
|
||||
background: linear-gradient(135deg, #28a745 0%, #20c997 100%);
|
||||
border-radius: 16rpx;
|
||||
padding: 30rpx;
|
||||
margin-bottom: 30rpx;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.teacher-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
border-radius: 40rpx;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.name {
|
||||
color: white;
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.department, .title {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
font-size: 24rpx;
|
||||
margin-bottom: 4rpx;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.attendance-btn {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 12rpx;
|
||||
padding: 16rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.btn-text {
|
||||
color: white;
|
||||
font-size: 20rpx;
|
||||
}
|
||||
|
||||
.today-schedule, .class-overview, .quick-functions,
|
||||
.student-activities, .access-management, .message-center {
|
||||
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;
|
||||
}
|
||||
|
||||
.date-info {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.view-more {
|
||||
font-size: 24rpx;
|
||||
color: #28a745;
|
||||
}
|
||||
|
||||
.schedule-scroll {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.schedule-item {
|
||||
display: inline-block;
|
||||
background: #f8f9fa;
|
||||
border-radius: 12rpx;
|
||||
padding: 20rpx;
|
||||
margin-right: 20rpx;
|
||||
min-width: 280rpx;
|
||||
border: 2rpx solid transparent;
|
||||
}
|
||||
|
||||
.schedule-item.current {
|
||||
background: #e7f3ff;
|
||||
border-color: #007AFF;
|
||||
}
|
||||
|
||||
.schedule-item.completed {
|
||||
background: #e6f7e6;
|
||||
border-color: #28a745;
|
||||
}
|
||||
|
||||
.time-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.time-period {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.course-status {
|
||||
font-size: 20rpx;
|
||||
padding: 4rpx 8rpx;
|
||||
border-radius: 8rpx;
|
||||
background: #28a745;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.course-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.course-name {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.course-location, .student-count {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
margin-bottom: 4rpx;
|
||||
}
|
||||
|
||||
.course-action {
|
||||
background: #28a745;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8rpx;
|
||||
padding: 12rpx;
|
||||
font-size: 24rpx;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.class-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
text-align: center;
|
||||
padding: 20rpx;
|
||||
border: 1rpx solid #eee;
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
color: #28a745;
|
||||
margin-bottom: 8rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.function-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.function-item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 30rpx 20rpx;
|
||||
border: 1rpx solid #eee;
|
||||
border-radius: 12rpx;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.function-item:hover {
|
||||
border-color: #28a745;
|
||||
background: #f8fff8;
|
||||
}
|
||||
|
||||
.function-item.emergency {
|
||||
border-color: #dc3545;
|
||||
background: #fff5f5;
|
||||
}
|
||||
|
||||
.function-icon {
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.function-text {
|
||||
font-size: 24rpx;
|
||||
color: #333;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.function-badge {
|
||||
position: absolute;
|
||||
top: 16rpx;
|
||||
right: 16rpx;
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
font-size: 20rpx;
|
||||
padding: 4rpx 8rpx;
|
||||
border-radius: 12rpx;
|
||||
min-width: 24rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.activities-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.activity-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
padding: 20rpx;
|
||||
border: 1rpx solid #eee;
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
|
||||
.activity-avatar {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.activity-avatar image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 30rpx;
|
||||
}
|
||||
|
||||
.activity-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.activity-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.student-name {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.activity-time {
|
||||
font-size: 20rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.activity-desc {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.activity-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4rpx;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
font-size: 20rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.activity-status {
|
||||
padding: 8rpx 16rpx;
|
||||
border-radius: 12rpx;
|
||||
font-size: 20rpx;
|
||||
margin-left: 20rpx;
|
||||
}
|
||||
|
||||
.activity-status.normal {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.activity-status.pending {
|
||||
background: #fff3cd;
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
.activity-status.warning {
|
||||
background: #ffeaa7;
|
||||
color: #6c5ce7;
|
||||
}
|
||||
|
||||
.access-summary {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16rpx;
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.permission-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16rpx 0;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.permission-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.permission-name {
|
||||
font-size: 24rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.permission-status {
|
||||
font-size: 20rpx;
|
||||
padding: 6rpx 12rpx;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.permission-status.active {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.permission-status.pending {
|
||||
background: #fff3cd;
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
.permission-status.inactive {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.apply-permission-btn {
|
||||
background: #28a745;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 12rpx;
|
||||
padding: 20rpx;
|
||||
font-size: 28rpx;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.message-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.message-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 20rpx;
|
||||
border: 1rpx solid #eee;
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
|
||||
.message-icon {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
margin-right: 20rpx;
|
||||
border-radius: 8rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.message-icon.urgent {
|
||||
background: #fff5f5;
|
||||
}
|
||||
|
||||
.message-icon image {
|
||||
width: 24rpx;
|
||||
height: 24rpx;
|
||||
}
|
||||
|
||||
.message-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.message-title {
|
||||
font-size: 24rpx;
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.message-preview {
|
||||
font-size: 22rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.message-time {
|
||||
font-size: 20rpx;
|
||||
color: #999;
|
||||
margin-left: 20rpx;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user