| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147 |
- <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">
- @import "@/styles/theme.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: var(--color-text-tertiary);
- 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: var(--color-text-secondary);
- }
- .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: var(--color-text-tertiary);
- }
- // ─── 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: var(--color-text-tertiary);
- }
- .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: var(--color-text-tertiary);
- }
- .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: var(--color-text-tertiary);
- }
- .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: var(--color-text-tertiary);
- }
- .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: var(--color-text-tertiary);
- text-decoration: underline;
- }
- </style>
|