Initial commit of akmon project

This commit is contained in:
2026-01-20 08:04:15 +08:00
commit 77a2bab985
1309 changed files with 343305 additions and 0 deletions

View 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>

File diff suppressed because it is too large Load Diff

View 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>

View 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>

File diff suppressed because it is too large Load Diff

View 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>

View 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>

View 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>