|
|
@@ -0,0 +1,1145 @@
|
|
|
+<template>
|
|
|
+ <view class="page-admin">
|
|
|
+ <!-- ===== Login Screen ===== -->
|
|
|
+ <view v-if="!authenticated" class="login-screen">
|
|
|
+ <view class="login-card">
|
|
|
+ <text class="login-title">管理后台</text>
|
|
|
+ <text class="login-subtitle">数字能量学系统</text>
|
|
|
+ <view class="login-form">
|
|
|
+ <view class="login-field">
|
|
|
+ <text class="field-label">用户名</text>
|
|
|
+ <input
|
|
|
+ class="field-input"
|
|
|
+ v-model="loginForm.username"
|
|
|
+ placeholder="请输入管理员账号"
|
|
|
+ placeholder-class="placeholder-style"
|
|
|
+ @confirm="handleLogin"
|
|
|
+ />
|
|
|
+ </view>
|
|
|
+ <view class="login-field">
|
|
|
+ <text class="field-label">密码</text>
|
|
|
+ <input
|
|
|
+ class="field-input"
|
|
|
+ type="password"
|
|
|
+ v-model="loginForm.password"
|
|
|
+ placeholder="请输入密码"
|
|
|
+ placeholder-class="placeholder-style"
|
|
|
+ @confirm="handleLogin"
|
|
|
+ />
|
|
|
+ </view>
|
|
|
+ <view class="login-error" v-if="loginError">
|
|
|
+ <text class="login-error-text">{{ loginError }}</text>
|
|
|
+ </view>
|
|
|
+ <button
|
|
|
+ class="login-btn"
|
|
|
+ :disabled="loggingIn"
|
|
|
+ @click="handleLogin"
|
|
|
+ >
|
|
|
+ <text v-if="loggingIn">登录中...</text>
|
|
|
+ <text v-else>登 录</text>
|
|
|
+ </button>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- ===== Main Admin Dashboard ===== -->
|
|
|
+ <view v-else class="dashboard">
|
|
|
+ <!-- Refresh indicator -->
|
|
|
+ <view v-if="loading" class="loading-state">
|
|
|
+ <text class="loading-text">加载中...</text>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <template v-else>
|
|
|
+ <!-- ===== Stats Grid ===== -->
|
|
|
+ <view class="stats-grid">
|
|
|
+ <view class="stat-card">
|
|
|
+ <text class="stat-value accent-blue">{{ stats.totalUsers }}</text>
|
|
|
+ <text class="stat-label">总用户数</text>
|
|
|
+ </view>
|
|
|
+ <view class="stat-card">
|
|
|
+ <text class="stat-value accent-green">{{ stats.vipUsers }}</text>
|
|
|
+ <text class="stat-label">VIP 用户</text>
|
|
|
+ </view>
|
|
|
+ <view class="stat-card">
|
|
|
+ <text class="stat-value accent-amber">{{ stats.totalOrders }}</text>
|
|
|
+ <text class="stat-label">总订单</text>
|
|
|
+ </view>
|
|
|
+ <view class="stat-card">
|
|
|
+ <text class="stat-value accent-rose">{{ formatMoney(stats.totalRevenue) }}</text>
|
|
|
+ <text class="stat-label">总收入 (¥)</text>
|
|
|
+ </view>
|
|
|
+ <view class="stat-card">
|
|
|
+ <text class="stat-value accent-blue">{{ stats.totalCharts }}</text>
|
|
|
+ <text class="stat-label">命盘总数</text>
|
|
|
+ </view>
|
|
|
+ <view class="stat-card">
|
|
|
+ <text class="stat-value accent-green">{{ stats.totalCommissions }}</text>
|
|
|
+ <text class="stat-label">佣金笔数</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- ===== Recent Orders Preview ===== -->
|
|
|
+ <view class="section-card" v-if="stats.recentOrders && stats.recentOrders.length > 0">
|
|
|
+ <view class="section-header">
|
|
|
+ <text class="section-title">最近订单</text>
|
|
|
+ </view>
|
|
|
+ <view
|
|
|
+ v-for="(order, idx) in stats.recentOrders.slice(0, 5)"
|
|
|
+ :key="idx"
|
|
|
+ class="mini-row"
|
|
|
+ >
|
|
|
+ <view class="mini-left">
|
|
|
+ <text class="mini-label">#{{ order.id }}</text>
|
|
|
+ <text class="mini-desc">{{ order.productType || '--' }}</text>
|
|
|
+ </view>
|
|
|
+ <view class="mini-right">
|
|
|
+ <text class="mini-value accent-amber">¥{{ formatMoney(order.totalFee) }}</text>
|
|
|
+ <text class="mini-status" :class="'status-' + order.status">
|
|
|
+ {{ orderStatusLabel(order.status) }}
|
|
|
+ </text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- ===== Tab Navigation ===== -->
|
|
|
+ <view class="tab-bar">
|
|
|
+ <view
|
|
|
+ v-for="tab in tabs"
|
|
|
+ :key="tab.key"
|
|
|
+ class="tab-item"
|
|
|
+ :class="{ 'tab-active': currentTab === tab.key }"
|
|
|
+ @click="currentTab = tab.key"
|
|
|
+ >
|
|
|
+ <text class="tab-text">{{ tab.label }}</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- ===== Users Tab ===== -->
|
|
|
+ <view v-show="currentTab === 'users'" class="tab-content">
|
|
|
+ <view v-if="users.length === 0" class="empty-state">
|
|
|
+ <text class="empty-text">暂无用户数据</text>
|
|
|
+ </view>
|
|
|
+ <view v-for="(user, idx) in users" :key="user.id || idx" class="data-card">
|
|
|
+ <view class="data-row">
|
|
|
+ <view class="data-left">
|
|
|
+ <view class="data-info">
|
|
|
+ <text class="data-primary">
|
|
|
+ {{ user.nickname || '微信用户' }}
|
|
|
+ <text v-if="user.isAdmin" class="badge-admin">管理员</text>
|
|
|
+ </text>
|
|
|
+ <text class="data-meta">ID: {{ user.id }} | 注册: {{ formatDate(user.createdAt) }}</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ <view class="data-right">
|
|
|
+ <text
|
|
|
+ class="tag-vip"
|
|
|
+ :class="{ 'tag-vip-active': isVipActive(user) }"
|
|
|
+ >
|
|
|
+ {{ isVipActive(user) ? 'VIP' : '普通' }}
|
|
|
+ </text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ <view class="action-row">
|
|
|
+ <button
|
|
|
+ v-if="!isVipActive(user)"
|
|
|
+ class="btn btn-sm btn-green"
|
|
|
+ :disabled="actionLoading.userId === user.id"
|
|
|
+ @click="handleUpgrade(user.id)"
|
|
|
+ >
|
|
|
+ {{ actionLoading.userId === user.id && actionLoading.type === 'upgrade' ? '...' : '升级VIP' }}
|
|
|
+ </button>
|
|
|
+ <button
|
|
|
+ v-if="isVipActive(user)"
|
|
|
+ class="btn btn-sm btn-red-outline"
|
|
|
+ :disabled="actionLoading.userId === user.id"
|
|
|
+ @click="handleDowngrade(user.id)"
|
|
|
+ >
|
|
|
+ {{ actionLoading.userId === user.id && actionLoading.type === 'downgrade' ? '...' : '取消VIP' }}
|
|
|
+ </button>
|
|
|
+ <button
|
|
|
+ class="btn btn-sm"
|
|
|
+ :class="user.isAdmin ? 'btn-amber-outline' : 'btn-blue-outline'"
|
|
|
+ :disabled="actionLoading.userId === user.id"
|
|
|
+ @click="handleToggleAdmin(user)"
|
|
|
+ >
|
|
|
+ {{
|
|
|
+ actionLoading.userId === user.id && actionLoading.type === 'set-admin'
|
|
|
+ ? '...'
|
|
|
+ : (user.isAdmin ? '取消管理' : '设为管理')
|
|
|
+ }}
|
|
|
+ </button>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- ===== Orders Tab ===== -->
|
|
|
+ <view v-show="currentTab === 'orders'" class="tab-content">
|
|
|
+ <view v-if="orders.length === 0" class="empty-state">
|
|
|
+ <text class="empty-text">暂无订单数据</text>
|
|
|
+ </view>
|
|
|
+ <view v-for="(order, idx) in orders" :key="order.id || idx" class="data-card">
|
|
|
+ <view class="data-row">
|
|
|
+ <view class="data-left">
|
|
|
+ <view class="data-info">
|
|
|
+ <text class="data-primary">订单 #{{ order.id }}</text>
|
|
|
+ <text class="data-meta">
|
|
|
+ 用户ID: {{ order.userId }} | {{ order.productType || '--' }}
|
|
|
+ </text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ <view class="data-right">
|
|
|
+ <text class="data-price">¥{{ formatMoney(order.totalFee) }}</text>
|
|
|
+ <text class="tag-status" :class="'tag-status-' + order.status">
|
|
|
+ {{ orderStatusLabel(order.status) }}
|
|
|
+ </text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ <view class="data-extras" v-if="order.paidAt || order.createdAt">
|
|
|
+ <text class="data-meta" v-if="order.paidAt">支付时间: {{ formatDate(order.paidAt) }}</text>
|
|
|
+ <text class="data-meta" v-else>创建时间: {{ formatDate(order.createdAt) }}</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- ===== Commissions Tab ===== -->
|
|
|
+ <view v-show="currentTab === 'commissions'" class="tab-content">
|
|
|
+ <view v-if="commissions.length === 0" class="empty-state">
|
|
|
+ <text class="empty-text">暂无佣金数据</text>
|
|
|
+ </view>
|
|
|
+ <view v-for="(comm, idx) in commissions" :key="comm.id || idx" class="data-card">
|
|
|
+ <view class="data-row">
|
|
|
+ <view class="data-left">
|
|
|
+ <view class="data-info">
|
|
|
+ <text class="data-primary">用户 #{{ comm.userId }}</text>
|
|
|
+ <text class="data-meta">
|
|
|
+ 级别: {{ comm.level === 1 ? '一级推广' : '二级推广' }}
|
|
|
+ <text v-if="comm.remark"> | {{ comm.remark }}</text>
|
|
|
+ </text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ <view class="data-right">
|
|
|
+ <text class="data-price accent-green">+¥{{ formatMoney(comm.amount) }}</text>
|
|
|
+ <text class="tag-status" :class="'tag-status-' + comm.status">
|
|
|
+ {{ commissionStatusLabel(comm.status) }}
|
|
|
+ </text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ <view class="data-extras">
|
|
|
+ <text class="data-meta">{{ formatDate(comm.createdAt) }}</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- ===== Withdraws Tab ===== -->
|
|
|
+ <view v-show="currentTab === 'withdraws'" class="tab-content">
|
|
|
+ <view v-if="withdraws.length === 0" class="empty-state">
|
|
|
+ <text class="empty-text">暂无提现申请</text>
|
|
|
+ </view>
|
|
|
+ <view v-for="(wd, idx) in withdraws" :key="wd.id || idx" class="data-card">
|
|
|
+ <view class="data-row">
|
|
|
+ <view class="data-left">
|
|
|
+ <view class="data-info">
|
|
|
+ <text class="data-primary">提现 #{{ wd.id }}</text>
|
|
|
+ <text class="data-meta">用户ID: {{ wd.userId }}</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ <view class="data-right">
|
|
|
+ <text class="data-price">¥{{ formatMoney(wd.amount) }}</text>
|
|
|
+ <text class="tag-status" :class="'tag-status-' + wd.status">
|
|
|
+ {{ withdrawStatusLabel(wd.status) }}
|
|
|
+ </text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ <view class="data-extras">
|
|
|
+ <text class="data-meta">{{ formatDate(wd.createdAt) }}</text>
|
|
|
+ </view>
|
|
|
+ <view class="action-row" v-if="wd.status === 'pending'">
|
|
|
+ <button
|
|
|
+ class="btn btn-sm btn-green"
|
|
|
+ :disabled="actionLoading.withdrawId === wd.id"
|
|
|
+ @click="handleApproveWithdraw(wd.id)"
|
|
|
+ >
|
|
|
+ {{ actionLoading.withdrawId === wd.id && actionLoading.type === 'approve' ? '...' : '批准' }}
|
|
|
+ </button>
|
|
|
+ <button
|
|
|
+ class="btn btn-sm btn-red-outline"
|
|
|
+ :disabled="actionLoading.withdrawId === wd.id"
|
|
|
+ @click="handleRejectWithdraw(wd.id)"
|
|
|
+ >
|
|
|
+ {{ actionLoading.withdrawId === wd.id && actionLoading.type === 'reject' ? '...' : '拒绝' }}
|
|
|
+ </button>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- ===== Config Tab ===== -->
|
|
|
+ <view v-show="currentTab === 'config'" class="tab-content">
|
|
|
+ <view v-if="configs.length === 0" class="empty-state">
|
|
|
+ <text class="empty-text">暂无系统配置</text>
|
|
|
+ </view>
|
|
|
+ <view v-for="(cfg, idx) in configs" :key="cfg.id || idx" class="data-card">
|
|
|
+ <view class="config-row">
|
|
|
+ <view class="config-header">
|
|
|
+ <text class="data-primary">{{ cfg.configKey }}</text>
|
|
|
+ <button
|
|
|
+ class="btn btn-xxs btn-amber-outline"
|
|
|
+ :disabled="configResetting === cfg.configKey"
|
|
|
+ @click="handleResetConfig(cfg.configKey)"
|
|
|
+ >
|
|
|
+ {{ configResetting === cfg.configKey ? '...' : '重置' }}
|
|
|
+ </button>
|
|
|
+ </view>
|
|
|
+ <view class="config-value-row">
|
|
|
+ <input
|
|
|
+ class="config-input"
|
|
|
+ v-model="configEdits[cfg.configKey]"
|
|
|
+ :placeholder="cfg.configValue || '输入值'"
|
|
|
+ placeholder-class="placeholder-style"
|
|
|
+ />
|
|
|
+ <button
|
|
|
+ class="btn btn-xxs btn-green"
|
|
|
+ :disabled="configSaving === cfg.configKey || configEdits[cfg.configKey] === cfg.configValue"
|
|
|
+ @click="handleSaveConfig(cfg.configKey)"
|
|
|
+ >
|
|
|
+ {{ configSaving === cfg.configKey ? '...' : '保存' }}
|
|
|
+ </button>
|
|
|
+ </view>
|
|
|
+ <text class="data-meta config-desc" v-if="cfg.description">{{ cfg.description }}</text>
|
|
|
+ <text class="data-meta config-desc" v-if="cfg.defaultValue">默认值: {{ cfg.defaultValue }}</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </template>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- ===== Admin logout hint ===== -->
|
|
|
+ <view v-if="authenticated" class="logout-bar" @click="handleLogout">
|
|
|
+ <text class="logout-text">退出登录</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup>
|
|
|
+import { ref, reactive, computed, onMounted } from 'vue'
|
|
|
+import { onPullDownRefresh } from '@dcloudio/uni-app'
|
|
|
+import { adminApi } from '@/utils/api'
|
|
|
+
|
|
|
+// ─── Auth State ───
|
|
|
+const authenticated = ref(!!uni.getStorageSync('adminToken'))
|
|
|
+const loginForm = reactive({ username: '', password: '' })
|
|
|
+const loggingIn = ref(false)
|
|
|
+const loginError = ref('')
|
|
|
+
|
|
|
+// ─── Data State ───
|
|
|
+const loading = ref(true)
|
|
|
+const stats = reactive({
|
|
|
+ totalUsers: 0,
|
|
|
+ vipUsers: 0,
|
|
|
+ totalOrders: 0,
|
|
|
+ totalRevenue: 0,
|
|
|
+ totalCharts: 0,
|
|
|
+ totalCommissions: 0,
|
|
|
+ recentOrders: []
|
|
|
+})
|
|
|
+const users = ref([])
|
|
|
+const orders = ref([])
|
|
|
+const commissions = ref([])
|
|
|
+const withdraws = ref([])
|
|
|
+const configs = ref([])
|
|
|
+
|
|
|
+// ─── UI State ───
|
|
|
+const currentTab = ref('users')
|
|
|
+const actionLoading = reactive({ userId: null, withdrawId: null, type: null })
|
|
|
+const configResetting = ref(null)
|
|
|
+const configSaving = ref(null)
|
|
|
+const configEdits = reactive({})
|
|
|
+
|
|
|
+const tabs = [
|
|
|
+ { key: 'users', label: '用户管理' },
|
|
|
+ { key: 'orders', label: '订单管理' },
|
|
|
+ { key: 'commissions', label: '佣金记录' },
|
|
|
+ { key: 'withdraws', label: '提现审核' },
|
|
|
+ { key: 'config', label: '系统配置' }
|
|
|
+]
|
|
|
+
|
|
|
+// ─── Helpers ───
|
|
|
+function formatMoney(cents) {
|
|
|
+ if (cents == null) return '0.00'
|
|
|
+ return (cents / 100).toFixed(2)
|
|
|
+}
|
|
|
+
|
|
|
+function formatDate(dateStr) {
|
|
|
+ if (!dateStr) return '--'
|
|
|
+ const d = new Date(dateStr)
|
|
|
+ if (isNaN(d.getTime())) return '--'
|
|
|
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
|
|
|
+}
|
|
|
+
|
|
|
+function isVipActive(user) {
|
|
|
+ if (!user.vipEndTime) return false
|
|
|
+ return new Date(user.vipEndTime) > new Date()
|
|
|
+}
|
|
|
+
|
|
|
+function orderStatusLabel(status) {
|
|
|
+ const map = { paid: '已支付', unpaid: '未支付', cancelled: '已取消', refunded: '已退款', pending: '处理中' }
|
|
|
+ return map[status] || status || '未知'
|
|
|
+}
|
|
|
+
|
|
|
+function commissionStatusLabel(status) {
|
|
|
+ const map = { settled: '已结算', pending: '处理中', withdrawn: '已提现', available: '可提现' }
|
|
|
+ return map[status] || status || '未知'
|
|
|
+}
|
|
|
+
|
|
|
+function withdrawStatusLabel(status) {
|
|
|
+ const map = { pending: '待审核', approved: '已批准', rejected: '已拒绝', completed: '已完成' }
|
|
|
+ return map[status] || status || '未知'
|
|
|
+}
|
|
|
+
|
|
|
+// ─── Auth ───
|
|
|
+async function handleLogin() {
|
|
|
+ if (!loginForm.username || !loginForm.password) {
|
|
|
+ loginError.value = '请输入用户名和密码'
|
|
|
+ return
|
|
|
+ }
|
|
|
+ loginError.value = ''
|
|
|
+ loggingIn.value = true
|
|
|
+ try {
|
|
|
+ const result = await adminApi.login(loginForm.username, loginForm.password)
|
|
|
+ uni.setStorageSync('adminToken', result.token)
|
|
|
+ authenticated.value = true
|
|
|
+ await fetchAllData()
|
|
|
+ } catch (e) {
|
|
|
+ loginError.value = e.message || '登录失败,请检查账号密码'
|
|
|
+ } finally {
|
|
|
+ loggingIn.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function handleLogout() {
|
|
|
+ uni.showModal({
|
|
|
+ title: '退出登录',
|
|
|
+ content: '确定要退出管理后台吗?',
|
|
|
+ success: (res) => {
|
|
|
+ if (res.confirm) {
|
|
|
+ uni.removeStorageSync('adminToken')
|
|
|
+ authenticated.value = false
|
|
|
+ loginForm.username = ''
|
|
|
+ loginForm.password = ''
|
|
|
+ }
|
|
|
+ }
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+// ─── Data Fetching ───
|
|
|
+async function fetchAllData() {
|
|
|
+ loading.value = true
|
|
|
+ try {
|
|
|
+ const [
|
|
|
+ statsData,
|
|
|
+ usersData,
|
|
|
+ ordersData,
|
|
|
+ commissionsData,
|
|
|
+ withdrawsData,
|
|
|
+ configsData
|
|
|
+ ] = await Promise.all([
|
|
|
+ adminApi.stats(),
|
|
|
+ adminApi.users(),
|
|
|
+ adminApi.orders(),
|
|
|
+ adminApi.commissions(),
|
|
|
+ adminApi.withdraws(),
|
|
|
+ adminApi.configList()
|
|
|
+ ])
|
|
|
+ if (statsData) {
|
|
|
+ stats.totalUsers = statsData.totalUsers || 0
|
|
|
+ stats.vipUsers = statsData.vipUsers || 0
|
|
|
+ stats.totalOrders = statsData.totalOrders || 0
|
|
|
+ stats.totalRevenue = statsData.totalRevenue || 0
|
|
|
+ stats.totalCharts = statsData.totalCharts || 0
|
|
|
+ stats.totalCommissions = statsData.totalCommissions || 0
|
|
|
+ stats.recentOrders = statsData.recentOrders || []
|
|
|
+ }
|
|
|
+ users.value = usersData || []
|
|
|
+ orders.value = ordersData || []
|
|
|
+ commissions.value = commissionsData || []
|
|
|
+ withdraws.value = withdrawsData || []
|
|
|
+ configs.value = configsData || []
|
|
|
+ // Init config edits
|
|
|
+ if (configsData) {
|
|
|
+ configsData.forEach(cfg => {
|
|
|
+ configEdits[cfg.configKey] = cfg.configValue || ''
|
|
|
+ })
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ if (e.code === 1001) {
|
|
|
+ // Token expired — redirect to login
|
|
|
+ uni.removeStorageSync('adminToken')
|
|
|
+ authenticated.value = false
|
|
|
+ }
|
|
|
+ uni.showToast({ title: e.message || '数据加载失败', icon: 'none' })
|
|
|
+ } finally {
|
|
|
+ loading.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+onMounted(async () => {
|
|
|
+ if (authenticated.value) {
|
|
|
+ await fetchAllData()
|
|
|
+ } else {
|
|
|
+ loading.value = false
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+onPullDownRefresh(async () => {
|
|
|
+ if (authenticated.value) {
|
|
|
+ await fetchAllData()
|
|
|
+ }
|
|
|
+ uni.stopPullDownRefresh()
|
|
|
+})
|
|
|
+
|
|
|
+// ─── User Actions ───
|
|
|
+async function handleUpgrade(userId) {
|
|
|
+ actionLoading.userId = userId
|
|
|
+ actionLoading.type = 'upgrade'
|
|
|
+ try {
|
|
|
+ await adminApi.upgradeUser(userId)
|
|
|
+ uni.showToast({ title: '已升级为VIP', icon: 'success' })
|
|
|
+ await fetchAllData()
|
|
|
+ } catch (e) {
|
|
|
+ uni.showToast({ title: e.message || '操作失败', icon: 'none' })
|
|
|
+ } finally {
|
|
|
+ actionLoading.userId = null
|
|
|
+ actionLoading.type = null
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function handleDowngrade(userId) {
|
|
|
+ uni.showModal({
|
|
|
+ title: '取消VIP',
|
|
|
+ content: '确定要取消该用户的VIP资格吗?',
|
|
|
+ success: async (res) => {
|
|
|
+ if (!res.confirm) return
|
|
|
+ actionLoading.userId = userId
|
|
|
+ actionLoading.type = 'downgrade'
|
|
|
+ try {
|
|
|
+ await adminApi.downgradeUser(userId)
|
|
|
+ uni.showToast({ title: '已取消VIP', icon: 'success' })
|
|
|
+ await fetchAllData()
|
|
|
+ } catch (e) {
|
|
|
+ uni.showToast({ title: e.message || '操作失败', icon: 'none' })
|
|
|
+ } finally {
|
|
|
+ actionLoading.userId = null
|
|
|
+ actionLoading.type = null
|
|
|
+ }
|
|
|
+ }
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+async function handleToggleAdmin(user) {
|
|
|
+ const newIsAdmin = !user.isAdmin
|
|
|
+ actionLoading.userId = user.id
|
|
|
+ actionLoading.type = 'set-admin'
|
|
|
+ try {
|
|
|
+ await adminApi.setAdmin(user.id, newIsAdmin)
|
|
|
+ uni.showToast({ title: newIsAdmin ? '已设为管理员' : '已取消管理员', icon: 'success' })
|
|
|
+ await fetchAllData()
|
|
|
+ } catch (e) {
|
|
|
+ uni.showToast({ title: e.message || '操作失败', icon: 'none' })
|
|
|
+ } finally {
|
|
|
+ actionLoading.userId = null
|
|
|
+ actionLoading.type = null
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// ─── Withdraw Actions ───
|
|
|
+async function handleApproveWithdraw(id) {
|
|
|
+ actionLoading.withdrawId = id
|
|
|
+ actionLoading.type = 'approve'
|
|
|
+ try {
|
|
|
+ await adminApi.approveWithdraw(id)
|
|
|
+ uni.showToast({ title: '提现已批准', icon: 'success' })
|
|
|
+ await fetchAllData()
|
|
|
+ } catch (e) {
|
|
|
+ uni.showToast({ title: e.message || '操作失败', icon: 'none' })
|
|
|
+ } finally {
|
|
|
+ actionLoading.withdrawId = null
|
|
|
+ actionLoading.type = null
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function handleRejectWithdraw(id) {
|
|
|
+ uni.showModal({
|
|
|
+ title: '拒绝提现',
|
|
|
+ content: '确定要拒绝该提现申请吗?',
|
|
|
+ success: async (res) => {
|
|
|
+ if (!res.confirm) return
|
|
|
+ actionLoading.withdrawId = id
|
|
|
+ actionLoading.type = 'reject'
|
|
|
+ try {
|
|
|
+ await adminApi.rejectWithdraw(id)
|
|
|
+ uni.showToast({ title: '提现已拒绝', icon: 'success' })
|
|
|
+ await fetchAllData()
|
|
|
+ } catch (e) {
|
|
|
+ uni.showToast({ title: e.message || '操作失败', icon: 'none' })
|
|
|
+ } finally {
|
|
|
+ actionLoading.withdrawId = null
|
|
|
+ actionLoading.type = null
|
|
|
+ }
|
|
|
+ }
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+// ─── Config Actions ───
|
|
|
+async function handleSaveConfig(configKey) {
|
|
|
+ const newValue = configEdits[configKey]
|
|
|
+ if (newValue == null) return
|
|
|
+ configSaving.value = configKey
|
|
|
+ try {
|
|
|
+ await adminApi.configUpdate({ key: configKey, value: newValue })
|
|
|
+ uni.showToast({ title: '配置已更新', icon: 'success' })
|
|
|
+ await fetchAllData()
|
|
|
+ } catch (e) {
|
|
|
+ uni.showToast({ title: e.message || '保存失败', icon: 'none' })
|
|
|
+ } finally {
|
|
|
+ configSaving.value = null
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function handleResetConfig(configKey) {
|
|
|
+ uni.showModal({
|
|
|
+ title: '重置配置',
|
|
|
+ content: `确定要将 "${configKey}" 重置为默认值吗?`,
|
|
|
+ success: async (res) => {
|
|
|
+ if (!res.confirm) return
|
|
|
+ configResetting.value = configKey
|
|
|
+ try {
|
|
|
+ await adminApi.configReset(configKey)
|
|
|
+ uni.showToast({ title: '配置已重置', icon: 'success' })
|
|
|
+ await fetchAllData()
|
|
|
+ } catch (e) {
|
|
|
+ uni.showToast({ title: e.message || '重置失败', icon: 'none' })
|
|
|
+ } finally {
|
|
|
+ configResetting.value = null
|
|
|
+ }
|
|
|
+ }
|
|
|
+ })
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped lang="scss">
|
|
|
+.page-admin {
|
|
|
+ min-height: 100vh;
|
|
|
+ background: linear-gradient(180deg, #0c0a1a 0%, #1a1232 100%);
|
|
|
+ padding: 16px;
|
|
|
+ padding-bottom: 40px;
|
|
|
+}
|
|
|
+
|
|
|
+// ─── Placeholder ───
|
|
|
+.placeholder-style {
|
|
|
+ color: rgba(255, 255, 255, 0.25);
|
|
|
+}
|
|
|
+
|
|
|
+// ─── Login Screen ───
|
|
|
+.login-screen {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ min-height: 90vh;
|
|
|
+ padding: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.login-card {
|
|
|
+ width: 100%;
|
|
|
+ max-width: 340px;
|
|
|
+ background: rgba(255, 255, 255, 0.03);
|
|
|
+ border: 1px solid rgba(255, 255, 255, 0.06);
|
|
|
+ border-radius: 20px;
|
|
|
+ padding: 32px 24px;
|
|
|
+}
|
|
|
+
|
|
|
+.login-title {
|
|
|
+ display: block;
|
|
|
+ font-size: 26px;
|
|
|
+ font-weight: 700;
|
|
|
+ color: rgba(255, 255, 255, 0.95);
|
|
|
+ text-align: center;
|
|
|
+ margin-bottom: 6px;
|
|
|
+}
|
|
|
+
|
|
|
+.login-subtitle {
|
|
|
+ display: block;
|
|
|
+ font-size: 13px;
|
|
|
+ color: rgba(255, 255, 255, 0.4);
|
|
|
+ text-align: center;
|
|
|
+ margin-bottom: 32px;
|
|
|
+}
|
|
|
+
|
|
|
+.login-form {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.login-field {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 6px;
|
|
|
+}
|
|
|
+
|
|
|
+.field-label {
|
|
|
+ font-size: 13px;
|
|
|
+ font-weight: 500;
|
|
|
+ color: rgba(255, 255, 255, 0.6);
|
|
|
+}
|
|
|
+
|
|
|
+.field-input {
|
|
|
+ height: 44px;
|
|
|
+ padding: 0 14px;
|
|
|
+ background: rgba(255, 255, 255, 0.04);
|
|
|
+ border: 1px solid rgba(255, 255, 255, 0.08);
|
|
|
+ border-radius: 10px;
|
|
|
+ font-size: 15px;
|
|
|
+ color: rgba(255, 255, 255, 0.9);
|
|
|
+ box-sizing: border-box;
|
|
|
+}
|
|
|
+
|
|
|
+.login-error {
|
|
|
+ padding: 8px 12px;
|
|
|
+ background: rgba(239, 68, 68, 0.1);
|
|
|
+ border-radius: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.login-error-text {
|
|
|
+ font-size: 13px;
|
|
|
+ color: #ef4444;
|
|
|
+}
|
|
|
+
|
|
|
+.login-btn {
|
|
|
+ height: 46px;
|
|
|
+ margin-top: 8px;
|
|
|
+ background: linear-gradient(135deg, #3b82f6, #2563eb);
|
|
|
+ border: none;
|
|
|
+ border-radius: 12px;
|
|
|
+ font-size: 16px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #fff;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ line-height: 1;
|
|
|
+}
|
|
|
+
|
|
|
+.login-btn[disabled] {
|
|
|
+ opacity: 0.5;
|
|
|
+}
|
|
|
+
|
|
|
+// ─── Loading ───
|
|
|
+.loading-state {
|
|
|
+ padding: 60px 0;
|
|
|
+ text-align: center;
|
|
|
+}
|
|
|
+
|
|
|
+.loading-text {
|
|
|
+ font-size: 14px;
|
|
|
+ color: rgba(255, 255, 255, 0.4);
|
|
|
+}
|
|
|
+
|
|
|
+// ─── Stats Grid ───
|
|
|
+.stats-grid {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: 1fr 1fr 1fr;
|
|
|
+ gap: 8px;
|
|
|
+ margin-bottom: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.stat-card {
|
|
|
+ background: rgba(255, 255, 255, 0.02);
|
|
|
+ border: 1px solid rgba(255, 255, 255, 0.04);
|
|
|
+ border-radius: 14px;
|
|
|
+ padding: 14px 10px;
|
|
|
+ text-align: center;
|
|
|
+}
|
|
|
+
|
|
|
+.stat-value {
|
|
|
+ display: block;
|
|
|
+ font-size: 22px;
|
|
|
+ font-weight: 700;
|
|
|
+ margin-bottom: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+.stat-label {
|
|
|
+ display: block;
|
|
|
+ font-size: 11px;
|
|
|
+ color: rgba(255, 255, 255, 0.45);
|
|
|
+}
|
|
|
+
|
|
|
+.accent-blue { color: #3b82f6; }
|
|
|
+.accent-green { color: #10b981; }
|
|
|
+.accent-amber { color: #f59e0b; }
|
|
|
+.accent-rose { color: #f472b6; }
|
|
|
+
|
|
|
+// ─── Section Card ───
|
|
|
+.section-card {
|
|
|
+ background: rgba(255, 255, 255, 0.02);
|
|
|
+ border: 1px solid rgba(255, 255, 255, 0.04);
|
|
|
+ border-radius: 16px;
|
|
|
+ padding: 14px;
|
|
|
+ margin-bottom: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.section-header {
|
|
|
+ margin-bottom: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.section-title {
|
|
|
+ font-size: 15px;
|
|
|
+ font-weight: 700;
|
|
|
+ color: rgba(255, 255, 255, 0.85);
|
|
|
+}
|
|
|
+
|
|
|
+.mini-row {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ padding: 8px 0;
|
|
|
+ border-bottom: 1px solid rgba(255, 255, 255, 0.04);
|
|
|
+}
|
|
|
+
|
|
|
+.mini-row:last-child {
|
|
|
+ border-bottom: none;
|
|
|
+}
|
|
|
+
|
|
|
+.mini-left {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 2px;
|
|
|
+}
|
|
|
+
|
|
|
+.mini-label {
|
|
|
+ font-size: 13px;
|
|
|
+ font-weight: 500;
|
|
|
+ color: rgba(255, 255, 255, 0.8);
|
|
|
+}
|
|
|
+
|
|
|
+.mini-desc {
|
|
|
+ font-size: 11px;
|
|
|
+ color: rgba(255, 255, 255, 0.4);
|
|
|
+}
|
|
|
+
|
|
|
+.mini-right {
|
|
|
+ text-align: right;
|
|
|
+}
|
|
|
+
|
|
|
+.mini-value {
|
|
|
+ display: block;
|
|
|
+ font-size: 14px;
|
|
|
+ font-weight: 600;
|
|
|
+}
|
|
|
+
|
|
|
+.mini-status {
|
|
|
+ font-size: 10px;
|
|
|
+ padding: 2px 8px;
|
|
|
+ border-radius: 4px;
|
|
|
+ display: inline-block;
|
|
|
+ margin-top: 2px;
|
|
|
+}
|
|
|
+
|
|
|
+// ─── Tab Bar ───
|
|
|
+.tab-bar {
|
|
|
+ display: flex;
|
|
|
+ gap: 6px;
|
|
|
+ margin-bottom: 14px;
|
|
|
+ overflow-x: auto;
|
|
|
+ white-space: nowrap;
|
|
|
+ -webkit-overflow-scrolling: touch;
|
|
|
+ padding-bottom: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+.tab-item {
|
|
|
+ flex-shrink: 0;
|
|
|
+ padding: 8px 16px;
|
|
|
+ border-radius: 10px;
|
|
|
+ background: rgba(255, 255, 255, 0.02);
|
|
|
+ border: 1px solid rgba(255, 255, 255, 0.04);
|
|
|
+}
|
|
|
+
|
|
|
+.tab-active {
|
|
|
+ background: rgba(59, 130, 246, 0.12);
|
|
|
+ border-color: rgba(59, 130, 246, 0.25);
|
|
|
+}
|
|
|
+
|
|
|
+.tab-text {
|
|
|
+ font-size: 13px;
|
|
|
+ font-weight: 500;
|
|
|
+ color: rgba(255, 255, 255, 0.5);
|
|
|
+}
|
|
|
+
|
|
|
+.tab-active .tab-text {
|
|
|
+ color: #3b82f6;
|
|
|
+}
|
|
|
+
|
|
|
+// ─── Tab Content ───
|
|
|
+.tab-content {
|
|
|
+ margin-bottom: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+// ─── Data Card ───
|
|
|
+.data-card {
|
|
|
+ background: rgba(255, 255, 255, 0.02);
|
|
|
+ border: 1px solid rgba(255, 255, 255, 0.04);
|
|
|
+ border-radius: 14px;
|
|
|
+ padding: 14px;
|
|
|
+ margin-bottom: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.data-row {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: flex-start;
|
|
|
+}
|
|
|
+
|
|
|
+.data-left {
|
|
|
+ flex: 1;
|
|
|
+ min-width: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.data-info {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 3px;
|
|
|
+}
|
|
|
+
|
|
|
+.data-primary {
|
|
|
+ font-size: 14px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: rgba(255, 255, 255, 0.85);
|
|
|
+}
|
|
|
+
|
|
|
+.data-meta {
|
|
|
+ font-size: 11px;
|
|
|
+ color: rgba(255, 255, 255, 0.4);
|
|
|
+}
|
|
|
+
|
|
|
+.data-right {
|
|
|
+ text-align: right;
|
|
|
+ flex-shrink: 0;
|
|
|
+ margin-left: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.data-price {
|
|
|
+ display: block;
|
|
|
+ font-size: 15px;
|
|
|
+ font-weight: 700;
|
|
|
+ color: rgba(255, 255, 255, 0.85);
|
|
|
+ margin-bottom: 3px;
|
|
|
+}
|
|
|
+
|
|
|
+.data-extras {
|
|
|
+ margin-top: 6px;
|
|
|
+ padding-top: 6px;
|
|
|
+ border-top: 1px solid rgba(255, 255, 255, 0.04);
|
|
|
+}
|
|
|
+
|
|
|
+// ─── Tags ───
|
|
|
+.tag-vip {
|
|
|
+ font-size: 10px;
|
|
|
+ font-weight: 600;
|
|
|
+ padding: 2px 10px;
|
|
|
+ border-radius: 999px;
|
|
|
+ background: rgba(107, 114, 128, 0.2);
|
|
|
+ color: rgba(255, 255, 255, 0.4);
|
|
|
+}
|
|
|
+
|
|
|
+.tag-vip-active {
|
|
|
+ background: rgba(16, 185, 129, 0.15);
|
|
|
+ color: #10b981;
|
|
|
+}
|
|
|
+
|
|
|
+.tag-status {
|
|
|
+ font-size: 10px;
|
|
|
+ font-weight: 500;
|
|
|
+ padding: 2px 8px;
|
|
|
+ border-radius: 4px;
|
|
|
+ display: inline-block;
|
|
|
+}
|
|
|
+
|
|
|
+.tag-status-paid,
|
|
|
+.tag-status-settled,
|
|
|
+.tag-status-approved,
|
|
|
+.tag-status-completed {
|
|
|
+ background: rgba(16, 185, 129, 0.12);
|
|
|
+ color: #10b981;
|
|
|
+}
|
|
|
+
|
|
|
+.tag-status-unpaid,
|
|
|
+.tag-status-pending {
|
|
|
+ background: rgba(245, 158, 11, 0.12);
|
|
|
+ color: #f59e0b;
|
|
|
+}
|
|
|
+
|
|
|
+.tag-status-cancelled,
|
|
|
+.tag-status-rejected {
|
|
|
+ background: rgba(239, 68, 68, 0.12);
|
|
|
+ color: #ef4444;
|
|
|
+}
|
|
|
+
|
|
|
+.tag-status-refunded {
|
|
|
+ background: rgba(107, 114, 128, 0.12);
|
|
|
+ color: #9ca3af;
|
|
|
+}
|
|
|
+
|
|
|
+.tag-status-available {
|
|
|
+ background: rgba(59, 130, 246, 0.12);
|
|
|
+ color: #3b82f6;
|
|
|
+}
|
|
|
+
|
|
|
+.tag-status-withdrawn {
|
|
|
+ background: rgba(107, 114, 128, 0.12);
|
|
|
+ color: #9ca3af;
|
|
|
+}
|
|
|
+
|
|
|
+.badge-admin {
|
|
|
+ font-size: 9px;
|
|
|
+ font-weight: 600;
|
|
|
+ background: rgba(59, 130, 246, 0.15);
|
|
|
+ color: #3b82f6;
|
|
|
+ padding: 1px 6px;
|
|
|
+ border-radius: 4px;
|
|
|
+ margin-left: 6px;
|
|
|
+ vertical-align: middle;
|
|
|
+}
|
|
|
+
|
|
|
+// ─── Action Buttons ───
|
|
|
+.action-row {
|
|
|
+ display: flex;
|
|
|
+ gap: 8px;
|
|
|
+ margin-top: 10px;
|
|
|
+ padding-top: 10px;
|
|
|
+ border-top: 1px solid rgba(255, 255, 255, 0.04);
|
|
|
+}
|
|
|
+
|
|
|
+.btn {
|
|
|
+ border: none;
|
|
|
+ border-radius: 8px;
|
|
|
+ font-size: 12px;
|
|
|
+ font-weight: 600;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ line-height: 1;
|
|
|
+ height: auto;
|
|
|
+ padding: 8px 14px;
|
|
|
+}
|
|
|
+
|
|
|
+.btn-sm {
|
|
|
+ font-size: 11px;
|
|
|
+ padding: 6px 12px;
|
|
|
+ height: 30px;
|
|
|
+}
|
|
|
+
|
|
|
+.btn-xxs {
|
|
|
+ font-size: 10px;
|
|
|
+ padding: 4px 10px;
|
|
|
+ height: 26px;
|
|
|
+}
|
|
|
+
|
|
|
+.btn-green {
|
|
|
+ background: #10b981;
|
|
|
+ color: #fff;
|
|
|
+}
|
|
|
+
|
|
|
+.btn-green[disabled] {
|
|
|
+ opacity: 0.5;
|
|
|
+}
|
|
|
+
|
|
|
+.btn-red-outline {
|
|
|
+ background: transparent;
|
|
|
+ border: 1px solid #ef4444;
|
|
|
+ color: #ef4444;
|
|
|
+}
|
|
|
+
|
|
|
+.btn-red-outline[disabled] {
|
|
|
+ opacity: 0.5;
|
|
|
+}
|
|
|
+
|
|
|
+.btn-blue-outline {
|
|
|
+ background: transparent;
|
|
|
+ border: 1px solid rgba(59, 130, 246, 0.4);
|
|
|
+ color: #3b82f6;
|
|
|
+}
|
|
|
+
|
|
|
+.btn-blue-outline[disabled] {
|
|
|
+ opacity: 0.5;
|
|
|
+}
|
|
|
+
|
|
|
+.btn-amber-outline {
|
|
|
+ background: transparent;
|
|
|
+ border: 1px solid rgba(245, 158, 11, 0.4);
|
|
|
+ color: #f59e0b;
|
|
|
+}
|
|
|
+
|
|
|
+.btn-amber-outline[disabled] {
|
|
|
+ opacity: 0.5;
|
|
|
+}
|
|
|
+
|
|
|
+// ─── Config ───
|
|
|
+.config-row {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.config-header {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+
|
|
|
+.config-value-row {
|
|
|
+ display: flex;
|
|
|
+ gap: 8px;
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+
|
|
|
+.config-input {
|
|
|
+ flex: 1;
|
|
|
+ height: 34px;
|
|
|
+ padding: 0 10px;
|
|
|
+ background: rgba(255, 255, 255, 0.04);
|
|
|
+ border: 1px solid rgba(255, 255, 255, 0.08);
|
|
|
+ border-radius: 8px;
|
|
|
+ font-size: 13px;
|
|
|
+ color: rgba(255, 255, 255, 0.9);
|
|
|
+ box-sizing: border-box;
|
|
|
+ min-width: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.config-desc {
|
|
|
+ font-size: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+// ─── Empty State ───
|
|
|
+.empty-state {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ padding: 60px 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.empty-text {
|
|
|
+ font-size: 13px;
|
|
|
+ color: rgba(255, 255, 255, 0.35);
|
|
|
+}
|
|
|
+
|
|
|
+// ─── Logout ───
|
|
|
+.logout-bar {
|
|
|
+ margin-top: 8px;
|
|
|
+ padding: 12px 0;
|
|
|
+ text-align: center;
|
|
|
+}
|
|
|
+
|
|
|
+.logout-text {
|
|
|
+ font-size: 12px;
|
|
|
+ color: rgba(255, 255, 255, 0.3);
|
|
|
+ text-decoration: underline;
|
|
|
+}
|
|
|
+</style>
|