Initial commit of akmon project
This commit is contained in:
847
pages/mall/admin/index.uvue
Normal file
847
pages/mall/admin/index.uvue
Normal file
@@ -0,0 +1,847 @@
|
||||
<!-- 后台管理端首页 - UTS Android 兼容 -->
|
||||
<template>
|
||||
<view class="admin-container">
|
||||
<!-- 头部导航 -->
|
||||
<view class="header">
|
||||
<view class="header-left">
|
||||
<text class="app-title">商城管理后台</text>
|
||||
<text class="welcome-text">欢迎回来,{{ adminInfo.nickname }}</text>
|
||||
</view>
|
||||
<view class="header-right">
|
||||
<text class="notification-btn" @click="goToNotifications">🔔</text>
|
||||
<text class="profile-btn" @click="goToProfile">👤</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 核心指标概览 -->
|
||||
<view class="metrics-section">
|
||||
<text class="section-title">核心指标</text>
|
||||
<view class="metrics-grid">
|
||||
<view class="metric-card">
|
||||
<text class="metric-value">¥{{ platformStats.total_gmv }}</text>
|
||||
<text class="metric-label">总GMV</text>
|
||||
<text class="metric-change positive">+{{ platformStats.gmv_growth }}%</text>
|
||||
</view>
|
||||
<view class="metric-card">
|
||||
<text class="metric-value">{{ platformStats.total_orders }}</text>
|
||||
<text class="metric-label">总订单数</text>
|
||||
<text class="metric-change positive">+{{ platformStats.order_growth }}%</text>
|
||||
</view>
|
||||
<view class="metric-card">
|
||||
<text class="metric-value">{{ platformStats.total_users }}</text>
|
||||
<text class="metric-label">注册用户</text>
|
||||
<text class="metric-change positive">+{{ platformStats.user_growth }}%</text>
|
||||
</view>
|
||||
<view class="metric-card">
|
||||
<text class="metric-value">{{ platformStats.total_merchants }}</text>
|
||||
<text class="metric-label">入驻商家</text>
|
||||
<text class="metric-change positive">+{{ platformStats.merchant_growth }}%</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 今日数据 -->
|
||||
<view class="today-section">
|
||||
<text class="section-title">今日数据</text>
|
||||
<view class="today-grid">
|
||||
<view class="today-item">
|
||||
<text class="today-value">¥{{ todayStats.sales }}</text>
|
||||
<text class="today-label">销售额</text>
|
||||
</view>
|
||||
<view class="today-item">
|
||||
<text class="today-value">{{ todayStats.orders }}</text>
|
||||
<text class="today-label">订单数</text>
|
||||
</view>
|
||||
<view class="today-item">
|
||||
<text class="today-value">{{ todayStats.new_users }}</text>
|
||||
<text class="today-label">新增用户</text>
|
||||
</view>
|
||||
<view class="today-item">
|
||||
<text class="today-value">{{ todayStats.active_users }}</text>
|
||||
<text class="today-label">活跃用户</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 待处理事项 -->
|
||||
<view class="pending-section">
|
||||
<text class="section-title">待处理事项</text>
|
||||
<view class="pending-list">
|
||||
<view class="pending-item urgent" @click="goToMerchantReview">
|
||||
<text class="pending-icon">🏪</text>
|
||||
<view class="pending-content">
|
||||
<text class="pending-title">商家入驻审核</text>
|
||||
<text class="pending-subtitle">{{ pendingCounts.merchant_review }}个商家待审核</text>
|
||||
</view>
|
||||
<text class="pending-count">{{ pendingCounts.merchant_review }}</text>
|
||||
</view>
|
||||
|
||||
<view class="pending-item" @click="goToProductReview">
|
||||
<text class="pending-icon">📦</text>
|
||||
<view class="pending-content">
|
||||
<text class="pending-title">商品审核</text>
|
||||
<text class="pending-subtitle">{{ pendingCounts.product_review }}个商品待审核</text>
|
||||
</view>
|
||||
<text class="pending-count">{{ pendingCounts.product_review }}</text>
|
||||
</view>
|
||||
|
||||
<view class="pending-item" @click="goToRefundReview">
|
||||
<text class="pending-icon">💰</text>
|
||||
<view class="pending-content">
|
||||
<text class="pending-title">退款处理</text>
|
||||
<text class="pending-subtitle">{{ pendingCounts.refund_review }}个退款申请</text>
|
||||
</view>
|
||||
<text class="pending-count">{{ pendingCounts.refund_review }}</text>
|
||||
</view>
|
||||
|
||||
<view class="pending-item" @click="goToComplaints">
|
||||
<text class="pending-icon">⚠️</text>
|
||||
<view class="pending-content">
|
||||
<text class="pending-title">投诉处理</text>
|
||||
<text class="pending-subtitle">{{ pendingCounts.complaints }}个投诉待处理</text>
|
||||
</view>
|
||||
<text class="pending-count">{{ pendingCounts.complaints }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 实时监控 -->
|
||||
<view class="monitor-section">
|
||||
<text class="section-title">实时监控</text>
|
||||
<view class="monitor-grid">
|
||||
<view class="monitor-card">
|
||||
<text class="monitor-title">在线用户</text>
|
||||
<text class="monitor-value">{{ realTimeStats.online_users }}</text>
|
||||
<text class="monitor-unit">人</text>
|
||||
</view>
|
||||
<view class="monitor-card">
|
||||
<text class="monitor-title">活跃配送员</text>
|
||||
<text class="monitor-value">{{ realTimeStats.active_drivers }}</text>
|
||||
<text class="monitor-unit">人</text>
|
||||
</view>
|
||||
<view class="monitor-card">
|
||||
<text class="monitor-title">配送中订单</text>
|
||||
<text class="monitor-value">{{ realTimeStats.delivering_orders }}</text>
|
||||
<text class="monitor-unit">单</text>
|
||||
</view>
|
||||
<view class="monitor-card">
|
||||
<text class="monitor-title">系统负载</text>
|
||||
<text class="monitor-value">{{ realTimeStats.system_load }}</text>
|
||||
<text class="monitor-unit">%</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 快捷管理功能 -->
|
||||
<view class="shortcuts-section">
|
||||
<text class="section-title">快捷管理</text>
|
||||
<view class="shortcuts-grid">
|
||||
<view class="shortcut-item" @click="goToUserManagement">
|
||||
<text class="shortcut-icon">👥</text>
|
||||
<text class="shortcut-text">用户管理</text>
|
||||
</view>
|
||||
<view class="shortcut-item" @click="goToMerchantManagement">
|
||||
<text class="shortcut-icon">🏪</text>
|
||||
<text class="shortcut-text">商家管理</text>
|
||||
</view>
|
||||
<view class="shortcut-item" @click="goToProductManagement">
|
||||
<text class="shortcut-icon">📦</text>
|
||||
<text class="shortcut-text">商品管理</text>
|
||||
</view>
|
||||
<view class="shortcut-item" @click="goToOrderManagement">
|
||||
<text class="shortcut-icon">📋</text>
|
||||
<text class="shortcut-text">订单管理</text>
|
||||
</view>
|
||||
<view class="shortcut-item" @click="goToCouponManagement">
|
||||
<text class="shortcut-icon">🎫</text>
|
||||
<text class="shortcut-text">优惠券管理</text>
|
||||
</view>
|
||||
<view class="shortcut-item" @click="goToDeliveryManagement">
|
||||
<text class="shortcut-icon">🚚</text>
|
||||
<text class="shortcut-text">配送管理</text>
|
||||
</view>
|
||||
<view class="shortcut-item" @click="goToFinanceManagement">
|
||||
<text class="shortcut-icon">💳</text>
|
||||
<text class="shortcut-text">财务管理</text>
|
||||
</view>
|
||||
<view class="shortcut-item" @click="goToSystemSettings">
|
||||
<text class="shortcut-icon">⚙️</text>
|
||||
<text class="shortcut-text">系统设置</text>
|
||||
</view>
|
||||
<view class="shortcut-item" @click="goToAdminUserSubscriptions">
|
||||
<text class="shortcut-icon">📑</text>
|
||||
<text class="shortcut-text">用户订阅</text>
|
||||
</view>
|
||||
<view class="shortcut-item" @click="goToSubscriptionPlans">
|
||||
<text class="shortcut-icon">🧾</text>
|
||||
<text class="shortcut-text">订阅方案</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 最新动态 -->
|
||||
<view class="activities-section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">最新动态</text>
|
||||
<text class="section-more" @click="goToActivityLog">查看全部</text>
|
||||
</view>
|
||||
<view class="activities-list">
|
||||
<view v-for="activity in recentActivities" :key="activity.id" class="activity-item">
|
||||
<text class="activity-icon">{{ getActivityIcon(activity.type) }}</text>
|
||||
<view class="activity-content">
|
||||
<text class="activity-text">{{ activity.description }}</text>
|
||||
<text class="activity-time">{{ formatTime(activity.created_at) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="uts">
|
||||
type AdminInfoType = {
|
||||
id: string
|
||||
nickname: string
|
||||
role: string
|
||||
}
|
||||
|
||||
type PlatformStatsType = {
|
||||
total_gmv: string
|
||||
gmv_growth: number
|
||||
total_orders: number
|
||||
order_growth: number
|
||||
total_users: number
|
||||
user_growth: number
|
||||
total_merchants: number
|
||||
merchant_growth: number
|
||||
}
|
||||
|
||||
type TodayStatsType = {
|
||||
sales: string
|
||||
orders: number
|
||||
new_users: number
|
||||
active_users: number
|
||||
}
|
||||
|
||||
type PendingCountsType = {
|
||||
merchant_review: number
|
||||
product_review: number
|
||||
refund_review: number
|
||||
complaints: number
|
||||
}
|
||||
|
||||
type RealTimeStatsType = {
|
||||
online_users: number
|
||||
active_drivers: number
|
||||
delivering_orders: number
|
||||
system_load: number
|
||||
}
|
||||
|
||||
type ActivityType = {
|
||||
id: string
|
||||
type: string
|
||||
description: string
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
adminInfo: {
|
||||
id: '',
|
||||
nickname: '管理员',
|
||||
role: 'admin'
|
||||
} as AdminInfoType,
|
||||
|
||||
platformStats: {
|
||||
total_gmv: '0.00',
|
||||
gmv_growth: 0,
|
||||
total_orders: 0,
|
||||
order_growth: 0,
|
||||
total_users: 0,
|
||||
user_growth: 0,
|
||||
total_merchants: 0,
|
||||
merchant_growth: 0
|
||||
} as PlatformStatsType,
|
||||
|
||||
todayStats: {
|
||||
sales: '0.00',
|
||||
orders: 0,
|
||||
new_users: 0,
|
||||
active_users: 0
|
||||
} as TodayStatsType,
|
||||
|
||||
pendingCounts: {
|
||||
merchant_review: 0,
|
||||
product_review: 0,
|
||||
refund_review: 0,
|
||||
complaints: 0
|
||||
} as PendingCountsType,
|
||||
|
||||
realTimeStats: {
|
||||
online_users: 0,
|
||||
active_drivers: 0,
|
||||
delivering_orders: 0,
|
||||
system_load: 0
|
||||
} as RealTimeStatsType,
|
||||
|
||||
recentActivities: [] as Array<ActivityType>
|
||||
}
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.loadAdminInfo()
|
||||
this.loadPlatformStats()
|
||||
this.loadTodayStats()
|
||||
this.loadPendingCounts()
|
||||
this.loadRealTimeStats()
|
||||
this.loadRecentActivities()
|
||||
},
|
||||
|
||||
onShow() {
|
||||
// 页面显示时刷新实时数据
|
||||
this.refreshRealTimeData()
|
||||
},
|
||||
|
||||
methods: {
|
||||
// 加载管理员信息
|
||||
loadAdminInfo() {
|
||||
// TODO: 调用API获取管理员信息
|
||||
this.adminInfo.nickname = '系统管理员'
|
||||
},
|
||||
|
||||
// 加载平台统计
|
||||
loadPlatformStats() {
|
||||
// TODO: 调用API获取平台统计数据
|
||||
this.platformStats = {
|
||||
total_gmv: '12,580,000.00',
|
||||
gmv_growth: 15.6,
|
||||
total_orders: 125800,
|
||||
order_growth: 12.3,
|
||||
total_users: 45600,
|
||||
user_growth: 8.9,
|
||||
total_merchants: 2560,
|
||||
merchant_growth: 5.2
|
||||
}
|
||||
},
|
||||
|
||||
// 加载今日统计
|
||||
loadTodayStats() {
|
||||
// TODO: 调用API获取今日数据
|
||||
this.todayStats = {
|
||||
sales: '156,800.00',
|
||||
orders: 1568,
|
||||
new_users: 89,
|
||||
active_users: 3456
|
||||
}
|
||||
},
|
||||
|
||||
// 加载待处理数量
|
||||
loadPendingCounts() {
|
||||
// TODO: 调用API获取待处理数量
|
||||
this.pendingCounts = {
|
||||
merchant_review: 12,
|
||||
product_review: 45,
|
||||
refund_review: 8,
|
||||
complaints: 3
|
||||
}
|
||||
},
|
||||
|
||||
// 加载实时统计
|
||||
loadRealTimeStats() {
|
||||
// TODO: 调用API获取实时数据
|
||||
this.realTimeStats = {
|
||||
online_users: 2345,
|
||||
active_drivers: 156,
|
||||
delivering_orders: 234,
|
||||
system_load: 68
|
||||
}
|
||||
},
|
||||
|
||||
// 加载最新动态
|
||||
loadRecentActivities() {
|
||||
// TODO: 调用API获取最新动态
|
||||
this.recentActivities = [
|
||||
{
|
||||
id: '1',
|
||||
type: 'user_register',
|
||||
description: '新用户注册:张三',
|
||||
created_at: '2025-01-08T15:30:00Z'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
type: 'merchant_apply',
|
||||
description: '商家申请入驻:华强北电子商城',
|
||||
created_at: '2025-01-08T15:25:00Z'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
type: 'order_created',
|
||||
description: '新订单创建:订单号 M202501081567',
|
||||
created_at: '2025-01-08T15:20:00Z'
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
// 刷新实时数据
|
||||
refreshRealTimeData() {
|
||||
this.loadRealTimeStats()
|
||||
this.loadPendingCounts()
|
||||
this.loadRecentActivities()
|
||||
},
|
||||
|
||||
// 获取活动图标
|
||||
getActivityIcon(type: string): string {
|
||||
switch (type) {
|
||||
case 'user_register': return '👤'
|
||||
case 'merchant_apply': return '🏪'
|
||||
case 'order_created': return '📋'
|
||||
case 'product_review': return '📦'
|
||||
case 'refund_request': return '💰'
|
||||
case 'complaint': return '⚠️'
|
||||
default: return '📝'
|
||||
}
|
||||
},
|
||||
|
||||
// 格式化时间
|
||||
formatTime(timeStr: string): string {
|
||||
const date = new Date(timeStr)
|
||||
const now = new Date()
|
||||
const diff = now.getTime() - date.getTime()
|
||||
const minutes = Math.floor(diff / (1000 * 60))
|
||||
|
||||
if (minutes < 60) {
|
||||
return `${minutes}分钟前`
|
||||
} else if (minutes < 1440) {
|
||||
return `${Math.floor(minutes / 60)}小时前`
|
||||
} else {
|
||||
return `${Math.floor(minutes / 1440)}天前`
|
||||
}
|
||||
},
|
||||
|
||||
// 导航方法
|
||||
goToNotifications() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/mall/admin/notifications'
|
||||
})
|
||||
},
|
||||
|
||||
goToProfile() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/mall/admin/profile'
|
||||
})
|
||||
},
|
||||
|
||||
goToMerchantReview() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/mall/admin/merchant-review'
|
||||
})
|
||||
},
|
||||
|
||||
goToProductReview() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/mall/admin/product-review'
|
||||
})
|
||||
},
|
||||
|
||||
goToRefundReview() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/mall/admin/refund-review'
|
||||
})
|
||||
},
|
||||
|
||||
goToComplaints() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/mall/admin/complaints'
|
||||
})
|
||||
},
|
||||
|
||||
goToUserManagement() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/mall/admin/user-management'
|
||||
})
|
||||
},
|
||||
|
||||
goToMerchantManagement() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/mall/admin/merchant-management'
|
||||
})
|
||||
},
|
||||
|
||||
goToProductManagement() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/mall/admin/product-management'
|
||||
})
|
||||
},
|
||||
|
||||
goToOrderManagement() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/mall/admin/order-management'
|
||||
})
|
||||
},
|
||||
|
||||
goToCouponManagement() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/mall/admin/coupon-management'
|
||||
})
|
||||
},
|
||||
|
||||
goToDeliveryManagement() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/mall/admin/delivery-management'
|
||||
})
|
||||
},
|
||||
|
||||
goToFinanceManagement() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/mall/admin/finance-management'
|
||||
})
|
||||
},
|
||||
|
||||
goToSystemSettings() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/mall/admin/system-settings'
|
||||
})
|
||||
},
|
||||
|
||||
goToActivityLog() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/mall/admin/activity-log'
|
||||
})
|
||||
},
|
||||
|
||||
goToSubscriptionPlans() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/mall/admin/subscription/plan-management'
|
||||
})
|
||||
},
|
||||
|
||||
goToAdminUserSubscriptions() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/mall/admin/subscription/user-subscriptions'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.admin-container {
|
||||
background-color: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
padding-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
padding: 40rpx 30rpx 30rpx;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.app-title {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.welcome-text {
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.notification-btn,
|
||||
.profile-btn {
|
||||
font-size: 32rpx;
|
||||
color: #fff;
|
||||
margin-left: 30rpx;
|
||||
}
|
||||
|
||||
.metrics-section {
|
||||
background-color: #fff;
|
||||
margin: 20rpx;
|
||||
padding: 30rpx;
|
||||
border-radius: 16rpx;
|
||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.metrics-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.metric-card {
|
||||
width: 48%;
|
||||
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||
padding: 30rpx 20rpx;
|
||||
border-radius: 12rpx;
|
||||
margin-bottom: 20rpx;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.metric-label {
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.metric-change {
|
||||
font-size: 20rpx;
|
||||
position: absolute;
|
||||
top: 20rpx;
|
||||
right: 20rpx;
|
||||
}
|
||||
|
||||
.positive {
|
||||
color: #4CAF50;
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
padding: 4rpx 8rpx;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.today-section {
|
||||
background-color: #fff;
|
||||
margin: 20rpx;
|
||||
padding: 30rpx;
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
|
||||
.today-grid {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.today-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.today-value {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
color: #2196F3;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.today-label {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.pending-section {
|
||||
background-color: #fff;
|
||||
margin: 20rpx;
|
||||
padding: 30rpx;
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
|
||||
.pending-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.pending-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 20rpx;
|
||||
border-radius: 12rpx;
|
||||
margin-bottom: 15rpx;
|
||||
border: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.pending-item.urgent {
|
||||
border-color: #FF5722;
|
||||
background-color: #FFF3E0;
|
||||
}
|
||||
|
||||
.pending-icon {
|
||||
font-size: 32rpx;
|
||||
margin-right: 20rpx;
|
||||
width: 40rpx;
|
||||
}
|
||||
|
||||
.pending-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.pending-title {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
margin-bottom: 6rpx;
|
||||
}
|
||||
|
||||
.pending-subtitle {
|
||||
font-size: 22rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.pending-count {
|
||||
font-size: 28rpx;
|
||||
color: #FF5722;
|
||||
font-weight: bold;
|
||||
background-color: #FFEBEE;
|
||||
padding: 8rpx 16rpx;
|
||||
border-radius: 20rpx;
|
||||
}
|
||||
|
||||
.monitor-section {
|
||||
background-color: #fff;
|
||||
margin: 20rpx;
|
||||
padding: 30rpx;
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
|
||||
.monitor-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.monitor-card {
|
||||
width: 48%;
|
||||
background: linear-gradient(135deg, #84fab0 0%, #8fd3f4 100%);
|
||||
padding: 30rpx 20rpx;
|
||||
border-radius: 12rpx;
|
||||
margin-bottom: 20rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.monitor-title {
|
||||
font-size: 24rpx;
|
||||
color: #333;
|
||||
margin-bottom: 15rpx;
|
||||
}
|
||||
|
||||
.monitor-value {
|
||||
font-size: 48rpx;
|
||||
font-weight: bold;
|
||||
color: #2E7D32;
|
||||
margin-bottom: 5rpx;
|
||||
}
|
||||
|
||||
.monitor-unit {
|
||||
font-size: 20rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.shortcuts-section {
|
||||
background-color: #fff;
|
||||
margin: 20rpx;
|
||||
padding: 30rpx;
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
|
||||
.shortcuts-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.shortcut-item {
|
||||
width: 22%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.shortcut-icon {
|
||||
font-size: 48rpx;
|
||||
margin-bottom: 15rpx;
|
||||
}
|
||||
|
||||
.shortcut-text {
|
||||
font-size: 22rpx;
|
||||
color: #333;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.activities-section {
|
||||
background-color: #fff;
|
||||
margin: 20rpx;
|
||||
padding: 30rpx;
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.section-more {
|
||||
font-size: 24rpx;
|
||||
color: #2196F3;
|
||||
}
|
||||
|
||||
.activities-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.activity-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 15rpx 0;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.activity-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.activity-icon {
|
||||
font-size: 28rpx;
|
||||
margin-right: 20rpx;
|
||||
width: 40rpx;
|
||||
}
|
||||
|
||||
.activity-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.activity-text {
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
margin-bottom: 5rpx;
|
||||
}
|
||||
|
||||
.activity-time {
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
}
|
||||
</style>
|
||||
873
pages/mall/admin/profile.uvue
Normal file
873
pages/mall/admin/profile.uvue
Normal file
@@ -0,0 +1,873 @@
|
||||
<!-- 管理端 - 个人中心 -->
|
||||
<template>
|
||||
<view class="admin-profile">
|
||||
<!-- 管理员信息头部 -->
|
||||
<view class="profile-header">
|
||||
<image :src="adminInfo.avatar_url || '/static/default-avatar.png'" class="admin-avatar" @click="editProfile" />
|
||||
<view class="admin-info">
|
||||
<text class="admin-name">{{ adminInfo.nickname || adminInfo.phone }}</text>
|
||||
<text class="admin-role">{{ getAdminRole() }}</text>
|
||||
<view class="admin-stats">
|
||||
<text class="stat-item">在线时长: {{ onlineHours }}h</text>
|
||||
<text class="stat-item">权限等级: {{ adminLevel }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="settings-icon" @click="goToSettings">⚙️</view>
|
||||
</view>
|
||||
|
||||
<!-- 系统概览 -->
|
||||
<view class="system-overview">
|
||||
<view class="section-title">系统概览</view>
|
||||
<view class="overview-grid">
|
||||
<view class="overview-card" @click="goToUsers">
|
||||
<text class="card-icon">👥</text>
|
||||
<text class="card-value">{{ systemStats.users }}</text>
|
||||
<text class="card-label">用户总数</text>
|
||||
<text class="card-change" :class="{ positive: systemStats.userGrowth > 0 }">
|
||||
{{ systemStats.userGrowth > 0 ? '+' : '' }}{{ systemStats.userGrowth }}%
|
||||
</text>
|
||||
</view>
|
||||
<view class="overview-card" @click="goToOrders">
|
||||
<text class="card-icon">📋</text>
|
||||
<text class="card-value">{{ systemStats.orders }}</text>
|
||||
<text class="card-label">订单总数</text>
|
||||
<text class="card-change" :class="{ positive: systemStats.orderGrowth > 0 }">
|
||||
{{ systemStats.orderGrowth > 0 ? '+' : '' }}{{ systemStats.orderGrowth }}%
|
||||
</text>
|
||||
</view>
|
||||
<view class="overview-card" @click="goToMerchants">
|
||||
<text class="card-icon">🏪</text>
|
||||
<text class="card-value">{{ systemStats.merchants }}</text>
|
||||
<text class="card-label">商家总数</text>
|
||||
<text class="card-change" :class="{ positive: systemStats.merchantGrowth > 0 }">
|
||||
{{ systemStats.merchantGrowth > 0 ? '+' : '' }}{{ systemStats.merchantGrowth }}%
|
||||
</text>
|
||||
</view>
|
||||
<view class="overview-card" @click="goToRevenue">
|
||||
<text class="card-icon">💰</text>
|
||||
<text class="card-value">¥{{ systemStats.revenue }}</text>
|
||||
<text class="card-label">总营收</text>
|
||||
<text class="card-change" :class="{ positive: systemStats.revenueGrowth > 0 }">
|
||||
{{ systemStats.revenueGrowth > 0 ? '+' : '' }}{{ systemStats.revenueGrowth }}%
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 待处理事项 -->
|
||||
<view class="pending-tasks">
|
||||
<view class="section-title">待处理事项</view>
|
||||
<view class="task-list">
|
||||
<view class="task-item urgent" @click="goToAudit('merchant')">
|
||||
<text class="task-icon">🏪</text>
|
||||
<view class="task-info">
|
||||
<text class="task-title">商家审核</text>
|
||||
<text class="task-desc">{{ pendingTasks.merchantAudit }}个商家待审核</text>
|
||||
</view>
|
||||
<text class="task-badge urgent">{{ pendingTasks.merchantAudit }}</text>
|
||||
</view>
|
||||
<view class="task-item" @click="goToComplaints">
|
||||
<text class="task-icon">📢</text>
|
||||
<view class="task-info">
|
||||
<text class="task-title">投诉处理</text>
|
||||
<text class="task-desc">{{ pendingTasks.complaints }}个投诉待处理</text>
|
||||
</view>
|
||||
<text class="task-badge">{{ pendingTasks.complaints }}</text>
|
||||
</view>
|
||||
<view class="task-item" @click="goToRefunds">
|
||||
<text class="task-icon">↩️</text>
|
||||
<view class="task-info">
|
||||
<text class="task-title">退款审核</text>
|
||||
<text class="task-desc">{{ pendingTasks.refunds }}个退款待审核</text>
|
||||
</view>
|
||||
<text class="task-badge">{{ pendingTasks.refunds }}</text>
|
||||
</view>
|
||||
<view class="task-item" @click="goToReports">
|
||||
<text class="task-icon">⚠️</text>
|
||||
<view class="task-info">
|
||||
<text class="task-title">举报处理</text>
|
||||
<text class="task-desc">{{ pendingTasks.reports }}个举报待处理</text>
|
||||
</view>
|
||||
<text class="task-badge">{{ pendingTasks.reports }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 今日数据 -->
|
||||
<view class="today-data">
|
||||
<view class="section-title">今日数据</view>
|
||||
<view class="data-grid">
|
||||
<view class="data-card">
|
||||
<text class="data-value">{{ todayData.newUsers }}</text>
|
||||
<text class="data-label">新增用户</text>
|
||||
</view>
|
||||
<view class="data-card">
|
||||
<text class="data-value">{{ todayData.newOrders }}</text>
|
||||
<text class="data-label">新增订单</text>
|
||||
</view>
|
||||
<view class="data-card">
|
||||
<text class="data-value">¥{{ todayData.revenue }}</text>
|
||||
<text class="data-label">平台收入</text>
|
||||
</view>
|
||||
<view class="data-card">
|
||||
<text class="data-value">{{ todayData.activeUsers }}</text>
|
||||
<text class="data-label">活跃用户</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 系统健康状态 -->
|
||||
<view class="system-health">
|
||||
<view class="section-header">
|
||||
<text class="section-title">系统健康状态</text>
|
||||
<text class="view-more" @click="goToMonitor">详细监控 ></text>
|
||||
</view>
|
||||
<view class="health-grid">
|
||||
<view class="health-item">
|
||||
<text class="health-label">服务器状态</text>
|
||||
<view class="health-status">
|
||||
<view class="status-dot" :class="{ online: systemHealth.server }"></view>
|
||||
<text class="status-text">{{ systemHealth.server ? '正常' : '异常' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="health-item">
|
||||
<text class="health-label">数据库状态</text>
|
||||
<view class="health-status">
|
||||
<view class="status-dot" :class="{ online: systemHealth.database }"></view>
|
||||
<text class="status-text">{{ systemHealth.database ? '正常' : '异常' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="health-item">
|
||||
<text class="health-label">缓存状态</text>
|
||||
<view class="health-status">
|
||||
<view class="status-dot" :class="{ online: systemHealth.cache }"></view>
|
||||
<text class="status-text">{{ systemHealth.cache ? '正常' : '异常' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="health-item">
|
||||
<text class="health-label">支付服务</text>
|
||||
<view class="health-status">
|
||||
<view class="status-dot" :class="{ online: systemHealth.payment }"></view>
|
||||
<text class="status-text">{{ systemHealth.payment ? '正常' : '异常' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 最近操作记录 -->
|
||||
<view class="recent-operations">
|
||||
<view class="section-header">
|
||||
<text class="section-title">最近操作</text>
|
||||
<text class="view-all" @click="goToOperationLogs">查看全部 ></text>
|
||||
</view>
|
||||
<view v-if="recentOperations.length > 0" class="operation-list">
|
||||
<view v-for="operation in recentOperations" :key="operation.id" class="operation-item">
|
||||
<view class="operation-info">
|
||||
<text class="operation-title">{{ operation.title }}</text>
|
||||
<text class="operation-desc">{{ operation.description }}</text>
|
||||
</view>
|
||||
<text class="operation-time">{{ formatTime(operation.created_at) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else class="no-data">
|
||||
<text class="no-data-text">暂无操作记录</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 快捷功能 -->
|
||||
<view class="quick-functions">
|
||||
<view class="section-title">快捷功能</view>
|
||||
<view class="function-grid">
|
||||
<view class="function-item" @click="goToUserManagement">
|
||||
<text class="function-icon">👥</text>
|
||||
<text class="function-label">用户管理</text>
|
||||
</view>
|
||||
<view class="function-item" @click="goToMerchantManagement">
|
||||
<text class="function-icon">🏪</text>
|
||||
<text class="function-label">商家管理</text>
|
||||
</view>
|
||||
<view class="function-item" @click="goToProductManagement">
|
||||
<text class="function-icon">📦</text>
|
||||
<text class="function-label">商品管理</text>
|
||||
</view>
|
||||
<view class="function-item" @click="goToOrderManagement">
|
||||
<text class="function-icon">📋</text>
|
||||
<text class="function-label">订单管理</text>
|
||||
</view>
|
||||
<view class="function-item" @click="goToFinanceManagement">
|
||||
<text class="function-icon">💰</text>
|
||||
<text class="function-label">财务管理</text>
|
||||
</view>
|
||||
<view class="function-item" @click="goToSystemSettings">
|
||||
<text class="function-icon">⚙️</text>
|
||||
<text class="function-label">系统设置</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 功能菜单 -->
|
||||
<view class="function-menu">
|
||||
<view class="menu-group">
|
||||
<view class="menu-item" @click="goToReports">
|
||||
<text class="menu-icon">📊</text>
|
||||
<text class="menu-label">数据报表</text>
|
||||
<text class="menu-arrow">></text>
|
||||
</view>
|
||||
<view class="menu-item" @click="goToAnnouncements">
|
||||
<text class="menu-icon">📢</text>
|
||||
<text class="menu-label">公告管理</text>
|
||||
<text class="menu-arrow">></text>
|
||||
</view>
|
||||
<view class="menu-item" @click="goToPermissions">
|
||||
<text class="menu-icon">🔐</text>
|
||||
<text class="menu-label">权限管理</text>
|
||||
<text class="menu-arrow">></text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="menu-group">
|
||||
<view class="menu-item" @click="goToHelp">
|
||||
<text class="menu-icon">❓</text>
|
||||
<text class="menu-label">帮助中心</text>
|
||||
<text class="menu-arrow">></text>
|
||||
</view>
|
||||
<view class="menu-item" @click="goToFeedback">
|
||||
<text class="menu-icon">💬</text>
|
||||
<text class="menu-label">意见反馈</text>
|
||||
<text class="menu-arrow">></text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import type { UserType, ApiResponseType } from '@/types/mall-types'
|
||||
|
||||
// 响应式数据
|
||||
const adminInfo = ref({
|
||||
id: '',
|
||||
phone: '',
|
||||
nickname: '系统管理员',
|
||||
avatar_url: ''
|
||||
} as UserType)
|
||||
|
||||
const onlineHours = ref(8.5)
|
||||
const adminLevel = ref('超级管理员')
|
||||
|
||||
const systemStats = ref({
|
||||
users: '12,568',
|
||||
userGrowth: 12.5,
|
||||
orders: '8,932',
|
||||
orderGrowth: 8.3,
|
||||
merchants: '1,234',
|
||||
merchantGrowth: 5.2,
|
||||
revenue: '1,256,789',
|
||||
revenueGrowth: 15.8
|
||||
})
|
||||
|
||||
const pendingTasks = ref({
|
||||
merchantAudit: 8,
|
||||
complaints: 15,
|
||||
refunds: 12,
|
||||
reports: 6
|
||||
})
|
||||
|
||||
const todayData = ref({
|
||||
newUsers: 156,
|
||||
newOrders: 289,
|
||||
revenue: '25,680',
|
||||
activeUsers: 3456
|
||||
})
|
||||
|
||||
const systemHealth = ref({
|
||||
server: true,
|
||||
database: true,
|
||||
cache: true,
|
||||
payment: true
|
||||
})
|
||||
|
||||
const recentOperations = ref([
|
||||
{
|
||||
id: 'op001',
|
||||
title: '商家审核通过',
|
||||
description: '审核通过商家"时尚服饰专营店"',
|
||||
created_at: '2024-12-01 14:30:00'
|
||||
},
|
||||
{
|
||||
id: 'op002',
|
||||
title: '处理用户投诉',
|
||||
description: '处理订单#ORD20241201001投诉',
|
||||
created_at: '2024-12-01 13:45:00'
|
||||
},
|
||||
{
|
||||
id: 'op003',
|
||||
title: '系统配置更新',
|
||||
description: '更新了支付配置参数',
|
||||
created_at: '2024-12-01 12:20:00'
|
||||
}
|
||||
])
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
loadAdminInfo()
|
||||
loadSystemStats()
|
||||
loadPendingTasks()
|
||||
})
|
||||
|
||||
// 方法
|
||||
function loadAdminInfo() {
|
||||
// 模拟加载管理员信息
|
||||
adminInfo.value = {
|
||||
id: 'admin001',
|
||||
phone: '13900000000',
|
||||
email: 'admin@mall.com',
|
||||
nickname: '超级管理员',
|
||||
avatar_url: '/static/admin-avatar.png',
|
||||
gender: 0,
|
||||
user_type: 99,
|
||||
status: 1,
|
||||
created_at: '2024-01-01'
|
||||
}
|
||||
}
|
||||
|
||||
function loadSystemStats() {
|
||||
// 模拟加载系统统计
|
||||
// 实际应用中从API获取
|
||||
}
|
||||
|
||||
function loadPendingTasks() {
|
||||
// 模拟加载待处理任务
|
||||
// 实际应用中从API获取
|
||||
}
|
||||
|
||||
function getAdminRole(): string {
|
||||
const roleMap = {
|
||||
99: '超级管理员',
|
||||
98: '系统管理员',
|
||||
97: '运营管理员'
|
||||
}
|
||||
return roleMap[adminInfo.value.user_type] || '管理员'
|
||||
}
|
||||
|
||||
function formatTime(dateStr: string): string {
|
||||
const date = new Date(dateStr)
|
||||
const now = new Date()
|
||||
const diff = now.getTime() - date.getTime()
|
||||
const hours = Math.floor(diff / (1000 * 60 * 60))
|
||||
|
||||
if (hours < 1) {
|
||||
return '刚刚'
|
||||
} else if (hours < 24) {
|
||||
return `${hours}小时前`
|
||||
} else {
|
||||
return `${Math.floor(hours / 24)}天前`
|
||||
}
|
||||
}
|
||||
|
||||
// 导航方法
|
||||
function editProfile() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/mall/admin/profile-edit'
|
||||
})
|
||||
}
|
||||
|
||||
function goToSettings() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/mall/admin/settings'
|
||||
})
|
||||
}
|
||||
|
||||
function goToUsers() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/mall/admin/users'
|
||||
})
|
||||
}
|
||||
|
||||
function goToOrders() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/mall/admin/orders'
|
||||
})
|
||||
}
|
||||
|
||||
function goToMerchants() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/mall/admin/merchants'
|
||||
})
|
||||
}
|
||||
|
||||
function goToRevenue() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/mall/admin/revenue'
|
||||
})
|
||||
}
|
||||
|
||||
function goToAudit(type: string) {
|
||||
uni.navigateTo({
|
||||
url: `/pages/mall/admin/audit?type=${type}`
|
||||
})
|
||||
}
|
||||
|
||||
function goToComplaints() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/mall/admin/complaints'
|
||||
})
|
||||
}
|
||||
|
||||
function goToRefunds() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/mall/admin/refunds'
|
||||
})
|
||||
}
|
||||
|
||||
function goToReports() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/mall/admin/reports'
|
||||
})
|
||||
}
|
||||
|
||||
function goToMonitor() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/mall/admin/monitor'
|
||||
})
|
||||
}
|
||||
|
||||
function goToOperationLogs() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/mall/admin/operation-logs'
|
||||
})
|
||||
}
|
||||
|
||||
function goToUserManagement() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/mall/admin/user-management'
|
||||
})
|
||||
}
|
||||
|
||||
function goToMerchantManagement() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/mall/admin/merchant-management'
|
||||
})
|
||||
}
|
||||
|
||||
function goToProductManagement() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/mall/admin/product-management'
|
||||
})
|
||||
}
|
||||
|
||||
function goToOrderManagement() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/mall/admin/order-management'
|
||||
})
|
||||
}
|
||||
|
||||
function goToFinanceManagement() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/mall/admin/finance-management'
|
||||
})
|
||||
}
|
||||
|
||||
function goToSystemSettings() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/mall/admin/system-settings'
|
||||
})
|
||||
}
|
||||
|
||||
function goToAnnouncements() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/mall/admin/announcements'
|
||||
})
|
||||
}
|
||||
|
||||
function goToPermissions() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/mall/admin/permissions'
|
||||
})
|
||||
}
|
||||
|
||||
function goToHelp() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/mall/common/help'
|
||||
})
|
||||
}
|
||||
|
||||
function goToFeedback() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/mall/common/feedback'
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.admin-profile {
|
||||
padding: 0 0 120rpx 0;
|
||||
background-color: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.profile-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 40rpx 30rpx;
|
||||
background: linear-gradient(135deg, #a29bfe 0%, #6c5ce7 100%);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.admin-avatar {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
border-radius: 60rpx;
|
||||
margin-right: 30rpx;
|
||||
border: 4rpx solid rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.admin-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.admin-name {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.admin-role {
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
margin-bottom: 15rpx;
|
||||
}
|
||||
|
||||
.admin-stats {
|
||||
display: flex;
|
||||
gap: 30rpx;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
font-size: 22rpx;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
.settings-icon {
|
||||
font-size: 36rpx;
|
||||
color: white;
|
||||
padding: 10rpx;
|
||||
}
|
||||
|
||||
.system-overview, .pending-tasks, .today-data, .system-health, .recent-operations, .quick-functions, .function-menu {
|
||||
margin: 20rpx 30rpx;
|
||||
background: white;
|
||||
border-radius: 20rpx;
|
||||
padding: 30rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.view-all, .view-more {
|
||||
font-size: 24rpx;
|
||||
color: #a29bfe;
|
||||
}
|
||||
|
||||
.overview-grid {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.overview-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
min-width: 150rpx;
|
||||
padding: 25rpx 15rpx;
|
||||
background: linear-gradient(135deg, #f8f9ff 0%, #e8ecff 100%);
|
||||
border-radius: 20rpx;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
font-size: 48rpx;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.card-value {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 5rpx;
|
||||
}
|
||||
|
||||
.card-label {
|
||||
font-size: 22rpx;
|
||||
color: #666;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.card-change {
|
||||
font-size: 20rpx;
|
||||
color: #ff6b6b;
|
||||
}
|
||||
|
||||
.card-change.positive {
|
||||
color: #00b894;
|
||||
}
|
||||
|
||||
.task-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.task-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 20rpx;
|
||||
background: #f8f9ff;
|
||||
border-radius: 15rpx;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.task-item.urgent {
|
||||
border-left: 6rpx solid #ff6b6b;
|
||||
}
|
||||
|
||||
.task-icon {
|
||||
font-size: 36rpx;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.task-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.task-title {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
margin-bottom: 5rpx;
|
||||
}
|
||||
|
||||
.task-desc {
|
||||
font-size: 22rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.task-badge {
|
||||
background: #a29bfe;
|
||||
color: white;
|
||||
font-size: 20rpx;
|
||||
padding: 6rpx 12rpx;
|
||||
border-radius: 15rpx;
|
||||
min-width: 30rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.task-badge.urgent {
|
||||
background: #ff6b6b;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% { transform: scale(1); }
|
||||
50% { transform: scale(1.1); }
|
||||
100% { transform: scale(1); }
|
||||
}
|
||||
|
||||
.data-grid {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.data-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
min-width: 150rpx;
|
||||
padding: 20rpx;
|
||||
background: #f8f9ff;
|
||||
border-radius: 15rpx;
|
||||
}
|
||||
|
||||
.data-value {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
color: #a29bfe;
|
||||
margin-bottom: 5rpx;
|
||||
}
|
||||
|
||||
.data-label {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.health-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.health-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20rpx;
|
||||
background: #f8f9ff;
|
||||
border-radius: 15rpx;
|
||||
}
|
||||
|
||||
.health-label {
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.health-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10rpx;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 16rpx;
|
||||
height: 16rpx;
|
||||
border-radius: 50%;
|
||||
background: #ff6b6b;
|
||||
}
|
||||
|
||||
.status-dot.online {
|
||||
background: #00b894;
|
||||
}
|
||||
|
||||
.status-text {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.operation-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.operation-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
padding: 20rpx;
|
||||
background: #f8f9ff;
|
||||
border-radius: 15rpx;
|
||||
}
|
||||
|
||||
.operation-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.operation-title {
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
margin-bottom: 5rpx;
|
||||
}
|
||||
|
||||
.operation-desc {
|
||||
font-size: 22rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.operation-time {
|
||||
font-size: 20rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.function-grid {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.function-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
min-width: 140rpx;
|
||||
padding: 25rpx 15rpx;
|
||||
background: #f8f9ff;
|
||||
border-radius: 15rpx;
|
||||
}
|
||||
|
||||
.function-icon {
|
||||
font-size: 48rpx;
|
||||
margin-bottom: 15rpx;
|
||||
}
|
||||
|
||||
.function-label {
|
||||
font-size: 24rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.menu-group {
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.menu-group:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 25rpx 0;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.menu-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.menu-icon {
|
||||
font-size: 36rpx;
|
||||
width: 60rpx;
|
||||
margin-right: 25rpx;
|
||||
}
|
||||
|
||||
.menu-label {
|
||||
flex: 1;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.menu-arrow {
|
||||
font-size: 24rpx;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.no-data {
|
||||
text-align: center;
|
||||
padding: 60rpx 0;
|
||||
}
|
||||
|
||||
.no-data-text {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
</style>
|
||||
416
pages/mall/admin/subscription/plan-management.uvue
Normal file
416
pages/mall/admin/subscription/plan-management.uvue
Normal file
@@ -0,0 +1,416 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<!-- 顶部操作栏 -->
|
||||
<view class="toolbar">
|
||||
<text class="title">订阅方案管理</text>
|
||||
<button class="add-btn" @click="openCreate">新增方案</button>
|
||||
</view>
|
||||
|
||||
<!-- 列表 -->
|
||||
<view v-if="!loading && plans.length > 0" class="list">
|
||||
<view class="row header">
|
||||
<text class="cell code">编码</text>
|
||||
<text class="cell name">名称</text>
|
||||
<text class="cell price">价格</text>
|
||||
<text class="cell period">周期</text>
|
||||
<text class="cell active">启用</text>
|
||||
<text class="cell sort">排序</text>
|
||||
<text class="cell actions">操作</text>
|
||||
</view>
|
||||
<list-view>
|
||||
<list-item v-for="item in plans" :key="item['id']">
|
||||
<view class="row">
|
||||
<text class="cell code">{{ item['plan_code'] }}</text>
|
||||
<text class="cell name">{{ item['name'] }}</text>
|
||||
<text class="cell price">{{ formatPrice(item['price']) }} {{ item['currency'] || 'CNY' }}</text>
|
||||
<text class="cell period">{{ item['billing_period'] === 'yearly' ? '年付' : '月付' }}</text>
|
||||
<view class="cell active">
|
||||
<switch :checked="!!item['is_active']" @change="onActiveChange(item, $event)" />
|
||||
</view>
|
||||
<view class="cell sort">
|
||||
<text>{{ item['sort_order'] ?? '-' }}</text>
|
||||
</view>
|
||||
<view class="cell actions">
|
||||
<button size="mini" @click="openEdit(item)">编辑</button>
|
||||
<button size="mini" type="warn" class="ml8" @click="confirmDelete(item)">删除</button>
|
||||
</view>
|
||||
</view>
|
||||
</list-item>
|
||||
</list-view>
|
||||
</view>
|
||||
|
||||
<view v-if="!loading && plans.length === 0" class="empty">
|
||||
<text>暂无订阅方案</text>
|
||||
</view>
|
||||
|
||||
<view v-if="loading" class="loading"><text>加载中...</text></view>
|
||||
|
||||
<!-- 编辑弹层(纯样式实现,兼容 uni-app-x) -->
|
||||
<view v-if="editVisible" class="overlay" @click.self="closeEdit">
|
||||
<view class="sheet">
|
||||
<text class="sheet-title">{{ editMode === 'create' ? '新增方案' : '编辑方案' }}</text>
|
||||
|
||||
<scroll-view scroll-y="true" class="form">
|
||||
<view class="form-item">
|
||||
<text class="label">编码</text>
|
||||
<input class="input" type="text" placeholder="例如: PRO_M"
|
||||
:value="editForm.plan_code || ''"
|
||||
@input="(e:any)=>editForm.plan_code=e.detail.value" />
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="label">名称</text>
|
||||
<input class="input" type="text" placeholder="例如: 专业版(月付)"
|
||||
:value="editForm.name || ''"
|
||||
@input="(e:any)=>editForm.name=e.detail.value" />
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="label">描述</text>
|
||||
<textarea class="textarea" placeholder="简要说明"
|
||||
:value="editForm.description || ''"
|
||||
@input="(e:any)=>editForm.description=e.detail.value" />
|
||||
</view>
|
||||
<view class="form-row">
|
||||
<view class="form-item half">
|
||||
<text class="label">价格</text>
|
||||
<input class="input" type="number" placeholder="例如: 99"
|
||||
:value="String(editForm.price ?? '')"
|
||||
@input="(e:any)=>editForm.price=toNumber(e.detail.value)" />
|
||||
</view>
|
||||
<view class="form-item half">
|
||||
<text class="label">币种</text>
|
||||
<input class="input" type="text" placeholder="CNY / USD"
|
||||
:value="editForm.currency || 'CNY'"
|
||||
@input="(e:any)=>editForm.currency=e.detail.value" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="form-row">
|
||||
<view class="form-item half">
|
||||
<text class="label">周期</text>
|
||||
<picker mode="selector" :range="periodOptions" :value="periodIndex"
|
||||
@change="onPeriodPick">
|
||||
<view class="picker-value">{{ editForm.billing_period === 'yearly' ? '年付' : '月付' }}</view>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="form-item half">
|
||||
<text class="label">试用天数</text>
|
||||
<input class="input" type="number" placeholder="例如: 14(可留空)"
|
||||
:value="editForm.trial_days != null ? String(editForm.trial_days) : ''"
|
||||
@input="(e:any)=>editForm.trial_days=toOptionalNumber(e.detail.value)" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="form-row">
|
||||
<view class="form-item half">
|
||||
<text class="label">排序</text>
|
||||
<input class="input" type="number" placeholder="越小越靠前"
|
||||
:value="String(editForm.sort_order ?? '')"
|
||||
@input="(e:any)=>editForm.sort_order=toOptionalNumber(e.detail.value)" />
|
||||
</view>
|
||||
<view class="form-item half switch-item">
|
||||
<text class="label">启用</text>
|
||||
<switch :checked="!!editForm.is_active" @change="(e:any)=>editForm.is_active=e.detail.value===true" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<text class="label">功能点(每行一个)</text>
|
||||
<textarea class="textarea" placeholder="示例:\n- 支持X\n- 提供Y"
|
||||
:value="featuresText"
|
||||
@input="(e:any)=>featuresText=e.detail.value" />
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<view class="sheet-actions">
|
||||
<button class="cancel" @click="closeEdit">取消</button>
|
||||
<button class="save" @click="savePlan">保存</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="uts">
|
||||
import supa from '@/components/supadb/aksupainstance.uts'
|
||||
|
||||
type UJ = UTSJSONObject
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
loading: false as boolean,
|
||||
plans: [] as Array<UJ>,
|
||||
// 编辑弹层
|
||||
editVisible: false as boolean,
|
||||
editMode: 'create' as 'create' | 'edit',
|
||||
editId: '' as string,
|
||||
editForm: {
|
||||
plan_code: '',
|
||||
name: '',
|
||||
description: '',
|
||||
price: 0,
|
||||
currency: 'CNY',
|
||||
billing_period: 'monthly',
|
||||
trial_days: null,
|
||||
is_active: true,
|
||||
sort_order: 0,
|
||||
features: null
|
||||
} as UJ,
|
||||
featuresText: '' as string,
|
||||
periodOptions: ['monthly','yearly'] as Array<string>,
|
||||
periodIndex: 0 as number
|
||||
}
|
||||
},
|
||||
|
||||
onShow() {
|
||||
this.loadPlans()
|
||||
},
|
||||
|
||||
methods: {
|
||||
async loadPlans() {
|
||||
try {
|
||||
this.loading = true
|
||||
const res = await supa
|
||||
.from('ml_subscription_plans')
|
||||
.select('*', {})
|
||||
.order('sort_order', { ascending: true })
|
||||
.execute()
|
||||
this.plans = Array.isArray(res.data) ? (res.data as Array<UJ>) : []
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '加载失败', icon: 'none' })
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
formatPrice(v: any): string {
|
||||
const n = Number(v)
|
||||
if (isNaN(n)) return String(v ?? '')
|
||||
return n.toFixed(2)
|
||||
},
|
||||
|
||||
toNumber(v: any): number {
|
||||
const n = Number(v)
|
||||
return isNaN(n) ? 0 : n
|
||||
},
|
||||
|
||||
toOptionalNumber(v: any): number | null {
|
||||
if (v == null || String(v).trim() === '') return null
|
||||
const n = Number(v)
|
||||
return isNaN(n) ? null : n
|
||||
},
|
||||
|
||||
openCreate() {
|
||||
this.editMode = 'create'
|
||||
this.editId = ''
|
||||
this.editForm = {
|
||||
plan_code: '',
|
||||
name: '',
|
||||
description: '',
|
||||
price: 0,
|
||||
currency: 'CNY',
|
||||
billing_period: 'monthly',
|
||||
trial_days: null,
|
||||
is_active: true,
|
||||
sort_order: 0,
|
||||
features: null
|
||||
} as UJ
|
||||
this.featuresText = ''
|
||||
this.periodIndex = 0
|
||||
this.editVisible = true
|
||||
},
|
||||
|
||||
openEdit(item: UJ) {
|
||||
this.editMode = 'edit'
|
||||
this.editId = (item['id'] ?? '') as string
|
||||
// 拷贝主要字段
|
||||
this.editForm = {
|
||||
plan_code: item['plan_code'] ?? '',
|
||||
name: item['name'] ?? '',
|
||||
description: item['description'] ?? '',
|
||||
price: item['price'] ?? 0,
|
||||
currency: item['currency'] ?? 'CNY',
|
||||
billing_period: item['billing_period'] ?? 'monthly',
|
||||
trial_days: item['trial_days'] ?? null,
|
||||
is_active: !!item['is_active'],
|
||||
sort_order: item['sort_order'] ?? 0,
|
||||
features: item['features'] ?? null
|
||||
} as UJ
|
||||
this.featuresText = this.featuresJsonToText(this.editForm['features'])
|
||||
this.periodIndex = this.editForm['billing_period'] === 'yearly' ? 1 : 0
|
||||
this.editVisible = true
|
||||
},
|
||||
|
||||
closeEdit() {
|
||||
this.editVisible = false
|
||||
},
|
||||
|
||||
onPeriodPick(e: any) {
|
||||
const idx = Number(e.detail.value)
|
||||
this.periodIndex = isNaN(idx) ? 0 : idx
|
||||
this.editForm['billing_period'] = this.periodOptions[this.periodIndex]
|
||||
},
|
||||
|
||||
featuresTextToJson(text: string): UJ | null {
|
||||
// 将每行转换为 { f1: '...', f2: '...' }
|
||||
if (text == null || text.trim() === '') return null
|
||||
const lines = text.split(/\r?\n/).map(s => s.trim()).filter(s => s.length > 0)
|
||||
const obj: UJ = {} as any
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const key = 'f' + (i + 1)
|
||||
;(obj as any)[key] = lines[i]
|
||||
}
|
||||
return obj
|
||||
},
|
||||
|
||||
featuresJsonToText(j: any): string {
|
||||
if (j == null) return ''
|
||||
const out: Array<string> = []
|
||||
try {
|
||||
const keys = Object.keys(j as any)
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const k = keys[i]
|
||||
const v = (j as any)[k]
|
||||
out.push(typeof v === 'string' ? v : JSON.stringify(v))
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
return out.join('\n')
|
||||
},
|
||||
|
||||
async savePlan() {
|
||||
// 校验
|
||||
if (!this.editForm['plan_code'] || !this.editForm['name']) {
|
||||
uni.showToast({ title: '编码和名称必填', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (this.editForm['price'] == null || Number(this.editForm['price']) < 0) {
|
||||
uni.showToast({ title: '价格需为非负数', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
// features 处理
|
||||
this.editForm['features'] = this.featuresTextToJson(this.featuresText)
|
||||
|
||||
try {
|
||||
if (this.editMode === 'create') {
|
||||
const res = await supa
|
||||
.from('ml_subscription_plans')
|
||||
.insert([this.editForm])
|
||||
.execute()
|
||||
if (res.status >= 200 && res.status < 300) {
|
||||
uni.showToast({ title: '创建成功', icon: 'success' })
|
||||
this.closeEdit()
|
||||
this.loadPlans()
|
||||
} else {
|
||||
uni.showToast({ title: '创建失败', icon: 'none' })
|
||||
}
|
||||
} else {
|
||||
const res = await supa
|
||||
.from('ml_subscription_plans')
|
||||
.update(this.editForm)
|
||||
.eq('id', this.editId)
|
||||
.execute()
|
||||
if (res.status >= 200 && res.status < 300) {
|
||||
uni.showToast({ title: '保存成功', icon: 'success' })
|
||||
this.closeEdit()
|
||||
this.loadPlans()
|
||||
} else {
|
||||
uni.showToast({ title: '保存失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '请求失败', icon: 'none' })
|
||||
}
|
||||
},
|
||||
|
||||
confirmDelete(item: UJ) {
|
||||
const id = (item['id'] ?? '') as string
|
||||
if (!id) return
|
||||
uni.showModal({
|
||||
title: '删除确认',
|
||||
content: `确定删除方案「${item['name'] ?? ''}」吗?`,
|
||||
success: (r:any) => {
|
||||
if (r.confirm) this.deletePlan(id)
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
async deletePlan(id: string) {
|
||||
try {
|
||||
const res = await supa
|
||||
.from('ml_subscription_plans')
|
||||
.delete()
|
||||
.eq('id', id)
|
||||
.execute()
|
||||
if (res.status >= 200 && res.status < 300) {
|
||||
uni.showToast({ title: '已删除', icon: 'success' })
|
||||
this.loadPlans()
|
||||
} else {
|
||||
uni.showToast({ title: '删除失败', icon: 'none' })
|
||||
}
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '请求失败', icon: 'none' })
|
||||
}
|
||||
},
|
||||
|
||||
async onActiveChange(item: UJ, evt: any) {
|
||||
const id = (item['id'] ?? '') as string
|
||||
const newVal = evt?.detail?.value === true
|
||||
try {
|
||||
const res = await supa
|
||||
.from('ml_subscription_plans')
|
||||
.update({ is_active: newVal })
|
||||
.eq('id', id)
|
||||
.execute()
|
||||
if (!(res.status >= 200 && res.status < 300)) {
|
||||
uni.showToast({ title: '更新失败', icon: 'none' })
|
||||
// 回滚 UI
|
||||
item['is_active'] = !newVal
|
||||
}
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '请求失败', icon: 'none' })
|
||||
item['is_active'] = !newVal
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page { padding: 12px; }
|
||||
.toolbar { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; }
|
||||
.title { font-size: 18px; font-weight: 600; }
|
||||
.add-btn { background: #3cc51f; color: #fff; border-radius: 6px; padding: 8px 12px; }
|
||||
|
||||
.list { background: #fff; border-radius: 10px; overflow: hidden; }
|
||||
.row { display: flex; align-items: center; padding: 10px; }
|
||||
.row.header { background: #fafafa; font-weight: 600; color: #555; }
|
||||
.cell { padding: 0 6px; }
|
||||
.code { width: 18%; }
|
||||
.name { width: 22%; }
|
||||
.price { width: 18%; }
|
||||
.period { width: 12%; }
|
||||
.active { width: 12%; display: flex; align-items: center; }
|
||||
.sort { width: 8%; }
|
||||
.actions { width: 20%; display: flex; justify-content: flex-end; }
|
||||
.ml8 { margin-left: 8px; }
|
||||
|
||||
.empty, .loading { padding: 24px; text-align: center; color: #888; }
|
||||
|
||||
/* 弹层 */
|
||||
.overlay { position: fixed; left: 0; right: 0; top: 0; bottom: 0; background: rgba(0,0,0,0.45); display: flex; align-items: flex-end; }
|
||||
.sheet { background: #fff; width: 100%; max-height: 80%; border-top-left-radius: 12px; border-top-right-radius: 12px; padding: 12px; }
|
||||
.sheet-title { font-size: 16px; font-weight: 600; margin-bottom: 8px; }
|
||||
.form { max-height: 60vh; }
|
||||
.form-item { margin-bottom: 12px; }
|
||||
.form-row { display: flex; gap: 10px; }
|
||||
.half { flex: 1; }
|
||||
.label { font-size: 13px; color: #666; margin-bottom: 6px; display: block; }
|
||||
.input { background: #f6f6f6; border-radius: 6px; padding: 8px; }
|
||||
.textarea { background: #f6f6f6; border-radius: 6px; padding: 8px; min-height: 80px; }
|
||||
.picker-value { background: #f6f6f6; border-radius: 6px; padding: 8px; color: #333; }
|
||||
.switch-item { display: flex; align-items: center; justify-content: space-between; }
|
||||
.sheet-actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 10px; }
|
||||
.cancel { background: #f0f0f0; color: #333; border-radius: 6px; padding: 8px 12px; }
|
||||
.save { background: #3cc51f; color: #fff; border-radius: 6px; padding: 8px 12px; }
|
||||
</style>
|
||||
328
pages/mall/admin/subscription/user-subscriptions.uvue
Normal file
328
pages/mall/admin/subscription/user-subscriptions.uvue
Normal file
@@ -0,0 +1,328 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<view class="toolbar">
|
||||
<text class="title">用户订阅管理</text>
|
||||
</view>
|
||||
|
||||
<!-- 筛选器 -->
|
||||
<view class="filters">
|
||||
<view class="filter-item">
|
||||
<text class="label">用户ID</text>
|
||||
<input class="input" placeholder="输入用户ID(支持部分匹配)"
|
||||
:value="filters.userId"
|
||||
@input="(e:any)=>filters.userId=e.detail.value" />
|
||||
</view>
|
||||
<view class="filter-item">
|
||||
<text class="label">方案</text>
|
||||
<picker mode="selector" :range="planNames" :value="planIndex" @change="onPlanPick">
|
||||
<view class="picker-value">{{ planIndex===0 ? '全部' : planNames[planIndex] }}</view>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="filter-item">
|
||||
<text class="label">状态</text>
|
||||
<picker mode="selector" :range="statusOptions" :value="statusIndex" @change="onStatusPick">
|
||||
<view class="picker-value">{{ statusOptions[statusIndex] }}</view>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="filter-actions">
|
||||
<button class="btn" @click="search">查询</button>
|
||||
<button class="btn ghost" @click="reset">重置</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 列表头 -->
|
||||
<view class="table">
|
||||
<view class="row header">
|
||||
<text class="cell user">用户</text>
|
||||
<text class="cell plan">方案</text>
|
||||
<text class="cell status">状态</text>
|
||||
<text class="cell period">周期</text>
|
||||
<text class="cell renew">自动续订</text>
|
||||
<text class="cell cancel">终止到期</text>
|
||||
<text class="cell actions">操作</text>
|
||||
</view>
|
||||
<list-view>
|
||||
<list-item v-for="item in rows" :key="item['id']">
|
||||
<view class="row">
|
||||
<view class="cell user">
|
||||
<text class="mono">{{ shortUser(item['user_id']) }}</text>
|
||||
</view>
|
||||
<text class="cell plan">{{ (item['plan'] && item['plan']['name']) ? item['plan']['name'] : '—' }}</text>
|
||||
<text class="cell status">{{ mapStatus(item['status']) }}</text>
|
||||
<view class="cell period">
|
||||
<text class="mono">{{ fmtDate(item['start_date']) }} → {{ fmtDate(item['end_date']) || '—' }}</text>
|
||||
<text class="sub">下次扣费:{{ fmtDate(item['next_billing_date']) || '—' }}</text>
|
||||
</view>
|
||||
<view class="cell renew">
|
||||
<switch :checked="!!item['auto_renew']" @change="onToggleRenew(item, $event)" />
|
||||
</view>
|
||||
<view class="cell cancel">
|
||||
<switch :checked="!!item['cancel_at_period_end']" @change="onToggleCancelAtEnd(item, $event)" />
|
||||
</view>
|
||||
<view class="cell actions">
|
||||
<button size="mini" @click="openSetStatus(item)">设置状态</button>
|
||||
<button size="mini" type="warn" class="ml8" @click="terminateNow(item)">立即终止</button>
|
||||
</view>
|
||||
</view>
|
||||
</list-item>
|
||||
</list-view>
|
||||
</view>
|
||||
|
||||
<view v-if="!loading && rows.length === 0" class="empty">暂无数据</view>
|
||||
<view v-if="loading" class="loading">加载中...</view>
|
||||
|
||||
<!-- 状态选择 ActionSheet -->
|
||||
<view v-if="statusSheet.visible" class="overlay" @click.self="closeStatusSheet">
|
||||
<view class="sheet">
|
||||
<text class="sheet-title">设置订阅状态</text>
|
||||
<view class="sheet-list">
|
||||
<button v-for="s in statusUpdateList" :key="s" class="sheet-item" @click="applyStatus(s)">{{ mapStatus(s) }}</button>
|
||||
</view>
|
||||
<button class="sheet-cancel" @click="closeStatusSheet">取消</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="uts">
|
||||
import supa from '@/components/supadb/aksupainstance.uts'
|
||||
import { SUBSCRIPTION_STATUS } from '@/types/mall-types.uts'
|
||||
|
||||
type UJ = UTSJSONObject
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
loading: false as boolean,
|
||||
rows: [] as Array<UJ>,
|
||||
plans: [] as Array<UJ>,
|
||||
|
||||
// filters
|
||||
filters: {
|
||||
userId: '' as string,
|
||||
planId: '' as string,
|
||||
status: '' as string // '' means all
|
||||
},
|
||||
planNames: ['全部'] as Array<string>,
|
||||
planIds: [''] as Array<string>,
|
||||
planIndex: 0 as number,
|
||||
statusOptions: ['全部','试用','生效','待付款','已取消','已过期'] as Array<string>,
|
||||
statusValues: ['','trial','active','past_due','canceled','expired'] as Array<string>,
|
||||
statusIndex: 0 as number,
|
||||
|
||||
// status sheet
|
||||
statusSheet: {
|
||||
visible: false as boolean,
|
||||
id: '' as string,
|
||||
current: '' as string
|
||||
},
|
||||
statusUpdateList: ['trial','active','past_due','canceled','expired'] as Array<string>
|
||||
}
|
||||
},
|
||||
|
||||
onShow() {
|
||||
this.loadPlans()
|
||||
this.search()
|
||||
},
|
||||
|
||||
methods: {
|
||||
async loadPlans() {
|
||||
try {
|
||||
const res = await supa.from('ml_subscription_plans').select('id,name', {}).order('sort_order', { ascending: true }).execute()
|
||||
const arr = Array.isArray(res.data) ? (res.data as Array<UJ>) : []
|
||||
this.plans = arr
|
||||
this.planNames = ['全部']
|
||||
this.planIds = ['']
|
||||
for (let i=0;i<arr.length;i++) {
|
||||
this.planNames.push((arr[i]['name'] ?? '') as string)
|
||||
this.planIds.push((arr[i]['id'] ?? '') as string)
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
},
|
||||
|
||||
onPlanPick(e:any) {
|
||||
const idx = Number(e.detail.value)
|
||||
this.planIndex = isNaN(idx) ? 0 : idx
|
||||
this.filters.planId = this.planIds[this.planIndex]
|
||||
},
|
||||
onStatusPick(e:any) {
|
||||
const idx = Number(e.detail.value)
|
||||
this.statusIndex = isNaN(idx) ? 0 : idx
|
||||
this.filters.status = this.statusValues[this.statusIndex]
|
||||
},
|
||||
|
||||
async search() {
|
||||
try {
|
||||
this.loading = true
|
||||
let q = supa
|
||||
.from('ml_user_subscriptions')
|
||||
.select('*, plan:ml_subscription_plans(*)', {})
|
||||
.order('created_at', { ascending: false })
|
||||
|
||||
if (this.filters.userId && this.filters.userId.trim() !== '') {
|
||||
q = q.ilike('user_id', `%${this.filters.userId.trim()}%`)
|
||||
}
|
||||
if (this.filters.planId && this.filters.planId !== '') {
|
||||
q = q.eq('plan_id', this.filters.planId)
|
||||
}
|
||||
if (this.filters.status && this.filters.status !== '') {
|
||||
q = q.eq('status', this.filters.status)
|
||||
}
|
||||
|
||||
const res = await q.execute()
|
||||
this.rows = Array.isArray(res.data) ? (res.data as Array<UJ>) : []
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '查询失败', icon: 'none' })
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
reset() {
|
||||
this.filters.userId = ''
|
||||
this.planIndex = 0
|
||||
this.statusIndex = 0
|
||||
this.filters.planId = ''
|
||||
this.filters.status = ''
|
||||
this.search()
|
||||
},
|
||||
|
||||
shortUser(id:any): string { const s = String(id ?? ''); return s.length>10 ? s.slice(0,6)+'…'+s.slice(-4) : s },
|
||||
fmtDate(s:any): string { if(!s) return ''; try { return new Date(String(s)).toISOString().slice(0,10) } catch { return '' } },
|
||||
mapStatus(s:any): string {
|
||||
const v = String(s ?? '')
|
||||
switch (v) {
|
||||
case 'trial': return '试用'
|
||||
case 'active': return '生效'
|
||||
case 'past_due': return '待付款'
|
||||
case 'canceled': return '已取消'
|
||||
case 'expired': return '已过期'
|
||||
default: return '未知'
|
||||
}
|
||||
},
|
||||
|
||||
async onToggleRenew(item: UJ, evt:any) {
|
||||
const id = (item['id'] ?? '') as string
|
||||
const newVal = evt?.detail?.value === true
|
||||
try {
|
||||
const res = await supa.from('ml_user_subscriptions').update({ auto_renew: newVal }).eq('id', id).execute()
|
||||
if (!(res.status>=200 && res.status<300)) {
|
||||
item['auto_renew'] = !newVal
|
||||
uni.showToast({ title: '更新失败', icon: 'none' })
|
||||
}
|
||||
} catch (e) {
|
||||
item['auto_renew'] = !newVal
|
||||
uni.showToast({ title: '请求失败', icon: 'none' })
|
||||
}
|
||||
},
|
||||
|
||||
async onToggleCancelAtEnd(item: UJ, evt:any) {
|
||||
const id = (item['id'] ?? '') as string
|
||||
const newVal = evt?.detail?.value === true
|
||||
try {
|
||||
const res = await supa.from('ml_user_subscriptions').update({ cancel_at_period_end: newVal }).eq('id', id).execute()
|
||||
if (!(res.status>=200 && res.status<300)) {
|
||||
item['cancel_at_period_end'] = !newVal
|
||||
uni.showToast({ title: '更新失败', icon: 'none' })
|
||||
}
|
||||
} catch (e) {
|
||||
item['cancel_at_period_end'] = !newVal
|
||||
uni.showToast({ title: '请求失败', icon: 'none' })
|
||||
}
|
||||
},
|
||||
|
||||
openSetStatus(item: UJ) {
|
||||
this.statusSheet.visible = true
|
||||
this.statusSheet.id = (item['id'] ?? '') as string
|
||||
this.statusSheet.current = (item['status'] ?? '') as string
|
||||
},
|
||||
closeStatusSheet() { this.statusSheet.visible = false },
|
||||
|
||||
async applyStatus(s: string) {
|
||||
const id = this.statusSheet.id
|
||||
try {
|
||||
const res = await supa.from('ml_user_subscriptions').update({ status: s }).eq('id', id).execute()
|
||||
if (res.status>=200 && res.status<300) {
|
||||
uni.showToast({ title: '已更新', icon: 'success' })
|
||||
this.closeStatusSheet()
|
||||
this.search()
|
||||
} else {
|
||||
uni.showToast({ title: '更新失败', icon: 'none' })
|
||||
}
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '请求失败', icon: 'none' })
|
||||
}
|
||||
},
|
||||
|
||||
terminateNow(item: UJ) {
|
||||
const id = (item['id'] ?? '') as string
|
||||
if (!id) return
|
||||
uni.showModal({
|
||||
title: '确认终止',
|
||||
content: '将立即终止该订阅,并设置状态为已取消,是否继续?',
|
||||
success: async (r:any) => {
|
||||
if (r.confirm) {
|
||||
try {
|
||||
const now = new Date().toISOString()
|
||||
const res = await supa
|
||||
.from('ml_user_subscriptions')
|
||||
.update({ status: 'canceled', end_date: now, auto_renew: false })
|
||||
.eq('id', id)
|
||||
.execute()
|
||||
if (res.status>=200 && res.status<300) {
|
||||
uni.showToast({ title: '已终止', icon: 'success' })
|
||||
this.search()
|
||||
} else {
|
||||
uni.showToast({ title: '操作失败', icon: 'none' })
|
||||
}
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '请求失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page { padding: 12px; }
|
||||
.toolbar { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; }
|
||||
.title { font-size: 18px; font-weight: 600; }
|
||||
|
||||
.filters { background: #fff; border-radius: 10px; padding: 10px; display: grid; grid-template-columns: 1fr 1fr 1fr auto; gap: 10px; margin-bottom: 10px; }
|
||||
.filter-item {}
|
||||
.label { font-size: 12px; color: #666; margin-bottom: 4px; display: block; }
|
||||
.input { background: #f6f6f6; border-radius: 6px; padding: 8px; }
|
||||
.picker-value { background: #f6f6f6; border-radius: 6px; padding: 8px; color: #333; }
|
||||
.filter-actions { display: flex; gap: 8px; align-items: end; }
|
||||
.btn { background: #3cc51f; color: #fff; border-radius: 6px; padding: 8px 12px; }
|
||||
.btn.ghost { background: #f0f0f0; color: #333; }
|
||||
|
||||
.table { background: #fff; border-radius: 10px; overflow: hidden; }
|
||||
.row { display: flex; align-items: center; padding: 10px; }
|
||||
.row.header { background: #fafafa; font-weight: 600; color: #555; }
|
||||
.cell { padding: 0 6px; }
|
||||
.user { width: 18%; }
|
||||
.plan { width: 18%; }
|
||||
.status { width: 12%; }
|
||||
.period { width: 26%; display: flex; flex-direction: column; }
|
||||
.renew { width: 12%; display: flex; align-items: center; }
|
||||
.cancel { width: 12%; display: flex; align-items: center; }
|
||||
.actions { width: 22%; display: flex; justify-content: flex-end; }
|
||||
.ml8 { margin-left: 8px; }
|
||||
.mono { font-family: monospace; }
|
||||
.sub { color: #888; font-size: 12px; }
|
||||
.empty, .loading { padding: 24px; text-align: center; color: #888; }
|
||||
|
||||
/* ActionSheet */
|
||||
.overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.45); display: flex; align-items: flex-end; }
|
||||
.sheet { background: #fff; width: 100%; border-top-left-radius: 12px; border-top-right-radius: 12px; padding: 12px; }
|
||||
.sheet-title { font-size: 16px; font-weight: 600; margin-bottom: 8px; }
|
||||
.sheet-list { display: flex; flex-direction: column; gap: 8px; }
|
||||
.sheet-item { background: #f6f6f6; color: #333; border-radius: 8px; padding: 10px; text-align: center; }
|
||||
.sheet-cancel { background: #eee; color: #333; border-radius: 8px; padding: 10px; margin-top: 8px; }
|
||||
</style>
|
||||
908
pages/mall/admin/user-detail.uvue
Normal file
908
pages/mall/admin/user-detail.uvue
Normal file
@@ -0,0 +1,908 @@
|
||||
<!-- 管理端 - 用户详情页 -->
|
||||
<template>
|
||||
<view class="user-detail-page">
|
||||
<!-- 用户基本信息 -->
|
||||
<view class="user-profile">
|
||||
<view class="profile-header">
|
||||
<image :src="user.avatar_url || '/static/default-avatar.png'" class="user-avatar" />
|
||||
<view class="user-basic">
|
||||
<text class="user-name">{{ user.nickname || user.phone }}</text>
|
||||
<text class="user-id">ID: {{ user.id }}</text>
|
||||
<view class="user-status">
|
||||
<text class="status-badge" :class="{ active: user.status === 1, inactive: user.status === 0 }">
|
||||
{{ user.status === 1 ? '正常' : '冻结' }}
|
||||
</text>
|
||||
<text class="user-type">{{ getUserTypeText() }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="action-menu" @click="showActionMenu">⋮</view>
|
||||
</view>
|
||||
|
||||
<view class="profile-details">
|
||||
<view class="detail-item">
|
||||
<text class="detail-label">手机号码</text>
|
||||
<text class="detail-value">{{ user.phone }}</text>
|
||||
</view>
|
||||
<view class="detail-item">
|
||||
<text class="detail-label">邮箱地址</text>
|
||||
<text class="detail-value">{{ user.email || '未设置' }}</text>
|
||||
</view>
|
||||
<view class="detail-item">
|
||||
<text class="detail-label">性别</text>
|
||||
<text class="detail-value">{{ getGenderText() }}</text>
|
||||
</view>
|
||||
<view class="detail-item">
|
||||
<text class="detail-label">注册时间</text>
|
||||
<text class="detail-value">{{ formatTime(user.created_at) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 用户统计 -->
|
||||
<view class="user-stats">
|
||||
<view class="section-title">用户统计</view>
|
||||
<view class="stats-grid">
|
||||
<view class="stat-item">
|
||||
<text class="stat-value">{{ userStats.total_orders }}</text>
|
||||
<text class="stat-label">总订单数</text>
|
||||
</view>
|
||||
<view class="stat-item">
|
||||
<text class="stat-value">¥{{ userStats.total_amount }}</text>
|
||||
<text class="stat-label">消费总额</text>
|
||||
</view>
|
||||
<view class="stat-item">
|
||||
<text class="stat-value">{{ userStats.total_reviews }}</text>
|
||||
<text class="stat-label">评价数量</text>
|
||||
</view>
|
||||
<view class="stat-item">
|
||||
<text class="stat-value">{{ userStats.avg_rating.toFixed(1) }}</text>
|
||||
<text class="stat-label">平均评分</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 最近订单 -->
|
||||
<view class="recent-orders">
|
||||
<view class="section-header">
|
||||
<text class="section-title">最近订单</text>
|
||||
<text class="view-all" @click="viewAllOrders">查看全部</text>
|
||||
</view>
|
||||
|
||||
<view v-if="recentOrders.length === 0" class="empty-orders">
|
||||
<text class="empty-text">暂无订单记录</text>
|
||||
</view>
|
||||
|
||||
<view v-for="order in recentOrders" :key="order.id" class="order-item" @click="viewOrderDetail(order)">
|
||||
<view class="order-header">
|
||||
<text class="order-no">{{ order.order_no }}</text>
|
||||
<text class="order-status" :class="getOrderStatusClass(order.status)">{{ getOrderStatusText(order.status) }}</text>
|
||||
</view>
|
||||
<view class="order-info">
|
||||
<text class="order-amount">¥{{ order.actual_amount }}</text>
|
||||
<text class="order-time">{{ formatTime(order.created_at) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 用户行为记录 -->
|
||||
<view class="user-activities">
|
||||
<view class="section-header">
|
||||
<text class="section-title">行为记录</text>
|
||||
<text class="view-all" @click="viewAllActivities">查看全部</text>
|
||||
</view>
|
||||
|
||||
<view v-for="activity in userActivities" :key="activity.id" class="activity-item">
|
||||
<view class="activity-icon" :class="activity.type">{{ getActivityIcon(activity.type) }}</view>
|
||||
<view class="activity-content">
|
||||
<text class="activity-desc">{{ activity.description }}</text>
|
||||
<text class="activity-time">{{ formatTime(activity.created_at) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 风险评估 -->
|
||||
<view class="risk-assessment">
|
||||
<view class="section-title">风险评估</view>
|
||||
<view class="risk-score">
|
||||
<view class="score-circle" :class="getRiskLevel()">
|
||||
<text class="score-value">{{ riskData.score }}</text>
|
||||
<text class="score-max">/100</text>
|
||||
</view>
|
||||
<view class="risk-info">
|
||||
<text class="risk-level">{{ getRiskLevelText() }}</text>
|
||||
<text class="risk-desc">{{ getRiskDescription() }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="risk-factors">
|
||||
<view v-for="factor in riskData.factors" :key="factor.type" class="factor-item">
|
||||
<text class="factor-label">{{ factor.label }}</text>
|
||||
<view class="factor-bar">
|
||||
<view class="factor-fill" :style="{ width: factor.value + '%' }" :class="factor.level"></view>
|
||||
</view>
|
||||
<text class="factor-value">{{ factor.value }}%</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 操作记录 -->
|
||||
<view class="admin-logs">
|
||||
<view class="section-header">
|
||||
<text class="section-title">操作记录</text>
|
||||
<text class="add-log" @click="addAdminLog">添加记录</text>
|
||||
</view>
|
||||
|
||||
<view v-for="log in adminLogs" :key="log.id" class="log-item">
|
||||
<view class="log-content">
|
||||
<text class="log-action">{{ log.action }}</text>
|
||||
<text class="log-reason">{{ log.reason }}</text>
|
||||
</view>
|
||||
<view class="log-meta">
|
||||
<text class="log-admin">{{ log.admin_name }}</text>
|
||||
<text class="log-time">{{ formatTime(log.created_at) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 操作菜单弹窗 -->
|
||||
<view v-if="showMenu" class="action-modal" @click="hideActionMenu">
|
||||
<view class="action-list" @click.stop>
|
||||
<view class="action-item" @click="toggleUserStatus">
|
||||
{{ user.status === 1 ? '冻结用户' : '解冻用户' }}
|
||||
</view>
|
||||
<view class="action-item" @click="resetPassword">重置密码</view>
|
||||
<view class="action-item" @click="sendMessage">发送消息</view>
|
||||
<view class="action-item danger" @click="deleteUser">删除用户</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { UserType, OrderType } from '@/types/mall-types.uts'
|
||||
|
||||
type UserStatsType = {
|
||||
total_orders: number
|
||||
total_amount: number
|
||||
total_reviews: number
|
||||
avg_rating: number
|
||||
}
|
||||
|
||||
type UserActivityType = {
|
||||
id: string
|
||||
type: string
|
||||
description: string
|
||||
created_at: string
|
||||
}
|
||||
|
||||
type RiskFactorType = {
|
||||
type: string
|
||||
label: string
|
||||
value: number
|
||||
level: string
|
||||
}
|
||||
|
||||
type RiskDataType = {
|
||||
score: number
|
||||
level: string
|
||||
factors: Array<RiskFactorType>
|
||||
}
|
||||
|
||||
type AdminLogType = {
|
||||
id: string
|
||||
action: string
|
||||
reason: string
|
||||
admin_name: string
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
user: {
|
||||
id: '',
|
||||
phone: '',
|
||||
email: '',
|
||||
nickname: '',
|
||||
avatar_url: '',
|
||||
gender: 0,
|
||||
user_type: 0,
|
||||
status: 0,
|
||||
created_at: ''
|
||||
} as UserType,
|
||||
userStats: {
|
||||
total_orders: 0,
|
||||
total_amount: 0,
|
||||
total_reviews: 0,
|
||||
avg_rating: 0
|
||||
} as UserStatsType,
|
||||
recentOrders: [] as Array<OrderType>,
|
||||
userActivities: [] as Array<UserActivityType>,
|
||||
riskData: {
|
||||
score: 0,
|
||||
level: '',
|
||||
factors: []
|
||||
} as RiskDataType,
|
||||
adminLogs: [] as Array<AdminLogType>,
|
||||
showMenu: false
|
||||
}
|
||||
},
|
||||
onLoad(options: any) {
|
||||
const userId = options.userId as string
|
||||
if (userId) {
|
||||
this.loadUserDetail(userId)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
loadUserDetail(userId: string) {
|
||||
// 模拟加载用户详情数据
|
||||
this.user = {
|
||||
id: userId,
|
||||
phone: '13800138000',
|
||||
email: 'user@example.com',
|
||||
nickname: '张三',
|
||||
avatar_url: '/static/avatar1.jpg',
|
||||
gender: 1,
|
||||
user_type: 1,
|
||||
status: 1,
|
||||
created_at: '2023-06-15T10:30:00'
|
||||
}
|
||||
|
||||
this.userStats = {
|
||||
total_orders: 23,
|
||||
total_amount: 5680.50,
|
||||
total_reviews: 18,
|
||||
avg_rating: 4.3
|
||||
}
|
||||
|
||||
this.recentOrders = [
|
||||
{
|
||||
id: 'order_001',
|
||||
order_no: 'ORD202401150001',
|
||||
user_id: userId,
|
||||
merchant_id: 'merchant_001',
|
||||
status: 4,
|
||||
total_amount: 299.98,
|
||||
discount_amount: 30.00,
|
||||
delivery_fee: 8.00,
|
||||
actual_amount: 277.98,
|
||||
payment_method: 1,
|
||||
payment_status: 1,
|
||||
delivery_address: {},
|
||||
created_at: '2024-01-15T14:30:00'
|
||||
}
|
||||
]
|
||||
|
||||
this.userActivities = [
|
||||
{
|
||||
id: 'activity_001',
|
||||
type: 'login',
|
||||
description: '用户登录系统',
|
||||
created_at: '2024-01-15T09:30:00'
|
||||
},
|
||||
{
|
||||
id: 'activity_002',
|
||||
type: 'order',
|
||||
description: '创建订单 ORD202401150001',
|
||||
created_at: '2024-01-15T14:30:00'
|
||||
},
|
||||
{
|
||||
id: 'activity_003',
|
||||
type: 'review',
|
||||
description: '对商品进行评价',
|
||||
created_at: '2024-01-14T16:20:00'
|
||||
}
|
||||
]
|
||||
|
||||
this.riskData = {
|
||||
score: 25,
|
||||
level: 'low',
|
||||
factors: [
|
||||
{ type: 'refund', label: '退款率', value: 15, level: 'low' },
|
||||
{ type: 'complaint', label: '投诉率', value: 5, level: 'low' },
|
||||
{ type: 'chargeback', label: '拒付率', value: 0, level: 'low' },
|
||||
{ type: 'fraud', label: '欺诈风险', value: 10, level: 'low' }
|
||||
]
|
||||
}
|
||||
|
||||
this.adminLogs = [
|
||||
{
|
||||
id: 'log_001',
|
||||
action: '账户验证',
|
||||
reason: '用户实名认证通过',
|
||||
admin_name: '管理员小李',
|
||||
created_at: '2024-01-10T11:00:00'
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
getUserTypeText(): string {
|
||||
const types = ['普通用户', '普通用户', '商家用户', '配送员', '管理员']
|
||||
return types[this.user.user_type] || '未知'
|
||||
},
|
||||
|
||||
getGenderText(): string {
|
||||
const genders = ['未知', '男', '女']
|
||||
return genders[this.user.gender] || '未知'
|
||||
},
|
||||
|
||||
getOrderStatusText(status: number): string {
|
||||
const statusTexts = ['异常', '待支付', '待发货', '待收货', '已完成', '已取消']
|
||||
return statusTexts[status] || '未知'
|
||||
},
|
||||
|
||||
getOrderStatusClass(status: number): string {
|
||||
const statusClasses = ['error', 'pending', 'processing', 'shipping', 'completed', 'cancelled']
|
||||
return statusClasses[status] || 'error'
|
||||
},
|
||||
|
||||
getActivityIcon(type: string): string {
|
||||
const icons: Record<string, string> = {
|
||||
login: '🔐',
|
||||
order: '🛍️',
|
||||
review: '⭐',
|
||||
refund: '💰',
|
||||
complaint: '⚠️'
|
||||
}
|
||||
return icons[type] || '📝'
|
||||
},
|
||||
|
||||
getRiskLevel(): string {
|
||||
if (this.riskData.score < 30) return 'low'
|
||||
if (this.riskData.score < 70) return 'medium'
|
||||
return 'high'
|
||||
},
|
||||
|
||||
getRiskLevelText(): string {
|
||||
const level = this.getRiskLevel()
|
||||
const levelTexts: Record<string, string> = {
|
||||
low: '低风险',
|
||||
medium: '中风险',
|
||||
high: '高风险'
|
||||
}
|
||||
return levelTexts[level] || '未知'
|
||||
},
|
||||
|
||||
getRiskDescription(): string {
|
||||
const level = this.getRiskLevel()
|
||||
const descriptions: Record<string, string> = {
|
||||
low: '用户行为正常,风险较低',
|
||||
medium: '存在一定风险,需要关注',
|
||||
high: '高风险用户,建议重点监控'
|
||||
}
|
||||
return descriptions[level] || ''
|
||||
},
|
||||
|
||||
formatTime(timeStr: string): string {
|
||||
return timeStr.replace('T', ' ').split('.')[0]
|
||||
},
|
||||
|
||||
showActionMenu() {
|
||||
this.showMenu = true
|
||||
},
|
||||
|
||||
hideActionMenu() {
|
||||
this.showMenu = false
|
||||
},
|
||||
|
||||
toggleUserStatus() {
|
||||
const action = this.user.status === 1 ? '冻结' : '解冻'
|
||||
uni.showModal({
|
||||
title: `${action}用户`,
|
||||
content: `确定要${action}用户 ${this.user.nickname} 吗?`,
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
this.user.status = this.user.status === 1 ? 0 : 1
|
||||
this.hideActionMenu()
|
||||
uni.showToast({
|
||||
title: `${action}成功`,
|
||||
icon: 'success'
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
resetPassword() {
|
||||
uni.showModal({
|
||||
title: '重置密码',
|
||||
content: '确定要重置用户密码吗?新密码将发送到用户手机。',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
this.hideActionMenu()
|
||||
uni.showToast({
|
||||
title: '密码重置成功',
|
||||
icon: 'success'
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
sendMessage() {
|
||||
this.hideActionMenu()
|
||||
uni.navigateTo({
|
||||
url: `/pages/mall/admin/send-message?userId=${this.user.id}`
|
||||
})
|
||||
},
|
||||
|
||||
deleteUser() {
|
||||
uni.showModal({
|
||||
title: '删除用户',
|
||||
content: '删除用户将无法恢复,确定要删除吗?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
this.hideActionMenu()
|
||||
uni.showToast({
|
||||
title: '用户已删除',
|
||||
icon: 'success'
|
||||
})
|
||||
setTimeout(() => {
|
||||
uni.navigateBack()
|
||||
}, 1500)
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
viewAllOrders() {
|
||||
uni.navigateTo({
|
||||
url: `/pages/mall/admin/user-orders?userId=${this.user.id}`
|
||||
})
|
||||
},
|
||||
|
||||
viewOrderDetail(order: OrderType) {
|
||||
uni.navigateTo({
|
||||
url: `/pages/mall/admin/order-detail?orderId=${order.id}`
|
||||
})
|
||||
},
|
||||
|
||||
viewAllActivities() {
|
||||
uni.navigateTo({
|
||||
url: `/pages/mall/admin/user-activities?userId=${this.user.id}`
|
||||
})
|
||||
},
|
||||
|
||||
addAdminLog() {
|
||||
uni.navigateTo({
|
||||
url: `/pages/mall/admin/add-log?userId=${this.user.id}`
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.user-detail-page {
|
||||
background-color: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.user-profile, .user-stats, .recent-orders, .user-activities, .risk-assessment, .admin-logs {
|
||||
background-color: #fff;
|
||||
margin-bottom: 20rpx;
|
||||
padding: 30rpx;
|
||||
}
|
||||
|
||||
.profile-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
border-radius: 60rpx;
|
||||
margin-right: 25rpx;
|
||||
}
|
||||
|
||||
.user-basic {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.user-id {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.user-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15rpx;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
padding: 8rpx 16rpx;
|
||||
border-radius: 12rpx;
|
||||
font-size: 22rpx;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.status-badge.active {
|
||||
background-color: #4caf50;
|
||||
}
|
||||
|
||||
.status-badge.inactive {
|
||||
background-color: #ff4444;
|
||||
}
|
||||
|
||||
.user-type {
|
||||
font-size: 22rpx;
|
||||
color: #666;
|
||||
background-color: #f0f0f0;
|
||||
padding: 8rpx 16rpx;
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
|
||||
.action-menu {
|
||||
font-size: 32rpx;
|
||||
color: #666;
|
||||
padding: 10rpx;
|
||||
}
|
||||
|
||||
.profile-details {
|
||||
border-top: 1rpx solid #f5f5f5;
|
||||
padding-top: 25rpx;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 15rpx 0;
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
width: 150rpx;
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
flex: 1;
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 25rpx;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 25rpx;
|
||||
}
|
||||
|
||||
.view-all, .add-log {
|
||||
font-size: 24rpx;
|
||||
color: #007aff;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 30rpx 0;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 10rpx;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 22rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.empty-orders {
|
||||
text-align: center;
|
||||
padding: 60rpx 0;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.order-item, .log-item {
|
||||
padding: 25rpx 0;
|
||||
border-bottom: 1rpx solid #f5f5f5;
|
||||
}
|
||||
|
||||
.order-item:last-child, .log-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.order-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.order-no {
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.order-status {
|
||||
font-size: 22rpx;
|
||||
padding: 6rpx 12rpx;
|
||||
border-radius: 10rpx;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.order-status.pending {
|
||||
background-color: #ffa726;
|
||||
}
|
||||
|
||||
.order-status.processing {
|
||||
background-color: #2196f3;
|
||||
}
|
||||
|
||||
.order-status.shipping {
|
||||
background-color: #9c27b0;
|
||||
}
|
||||
|
||||
.order-status.completed {
|
||||
background-color: #4caf50;
|
||||
}
|
||||
|
||||
.order-status.cancelled {
|
||||
background-color: #999;
|
||||
}
|
||||
|
||||
.order-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.order-amount {
|
||||
font-size: 24rpx;
|
||||
color: #ff4444;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.order-time {
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.activity-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
padding: 20rpx 0;
|
||||
border-bottom: 1rpx solid #f5f5f5;
|
||||
}
|
||||
|
||||
.activity-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.activity-icon {
|
||||
font-size: 28rpx;
|
||||
margin-right: 15rpx;
|
||||
margin-top: 5rpx;
|
||||
}
|
||||
|
||||
.activity-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.activity-desc {
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
margin-bottom: 5rpx;
|
||||
}
|
||||
|
||||
.activity-time {
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.risk-score {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.score-circle {
|
||||
width: 140rpx;
|
||||
height: 140rpx;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 30rpx;
|
||||
border: 6rpx solid;
|
||||
}
|
||||
|
||||
.score-circle.low {
|
||||
border-color: #4caf50;
|
||||
background-color: #e8f5e8;
|
||||
}
|
||||
|
||||
.score-circle.medium {
|
||||
border-color: #ffa726;
|
||||
background-color: #fff8e1;
|
||||
}
|
||||
|
||||
.score-circle.high {
|
||||
border-color: #ff4444;
|
||||
background-color: #ffebee;
|
||||
}
|
||||
|
||||
.score-value {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.score-max {
|
||||
font-size: 20rpx;
|
||||
color: #666;
|
||||
margin-top: -5rpx;
|
||||
}
|
||||
|
||||
.risk-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.risk-level {
|
||||
font-size: 28rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.risk-desc {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.risk-factors {
|
||||
margin-top: 30rpx;
|
||||
}
|
||||
|
||||
.factor-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.factor-label {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
width: 120rpx;
|
||||
}
|
||||
|
||||
.factor-bar {
|
||||
flex: 1;
|
||||
height: 12rpx;
|
||||
background-color: #f0f0f0;
|
||||
border-radius: 6rpx;
|
||||
margin: 0 20rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.factor-fill {
|
||||
height: 100%;
|
||||
border-radius: 6rpx;
|
||||
}
|
||||
|
||||
.factor-fill.low {
|
||||
background-color: #4caf50;
|
||||
}
|
||||
|
||||
.factor-fill.medium {
|
||||
background-color: #ffa726;
|
||||
}
|
||||
|
||||
.factor-fill.high {
|
||||
background-color: #ff4444;
|
||||
}
|
||||
|
||||
.factor-value {
|
||||
font-size: 22rpx;
|
||||
color: #333;
|
||||
width: 60rpx;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.log-content {
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.log-action {
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
margin-bottom: 5rpx;
|
||||
}
|
||||
|
||||
.log-reason {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.log-meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.log-admin {
|
||||
font-size: 22rpx;
|
||||
color: #007aff;
|
||||
}
|
||||
|
||||
.log-time {
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.action-modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.action-list {
|
||||
background-color: #fff;
|
||||
border-radius: 20rpx;
|
||||
padding: 30rpx 0;
|
||||
margin: 0 60rpx;
|
||||
max-width: 500rpx;
|
||||
}
|
||||
|
||||
.action-item {
|
||||
padding: 25rpx 40rpx;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
text-align: center;
|
||||
border-bottom: 1rpx solid #f5f5f5;
|
||||
}
|
||||
|
||||
.action-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.action-item.danger {
|
||||
color: #ff4444;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user