| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148 |
- <template>
- <view class="page-admin" :class="themeClass">
- <!-- ===== 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'
- import { useTheme } from '@/composables/useThemeStyle'
- const { themeClass } = useTheme()
- // ─── 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: 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>
|