- 管理后台 adminT 文案库、结算与代理端页面、表单校验 - 玩家端 vue-i18n 补全首页/公告/串关与 ms 文案 - Element Plus ms 语言包与共享 locale 工具
596 lines
22 KiB
Vue
596 lines
22 KiB
Vue
<script setup lang="ts">
|
||
import { ref, onMounted } from 'vue';
|
||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||
import { useAdminLocale } from '../composables/useAdminLocale';
|
||
import { resolveFormError } from '../i18n/form-validation';
|
||
import api from '../api';
|
||
|
||
const { t, localeTag } = useAdminLocale();
|
||
import {
|
||
emptyPlayerCreateForm,
|
||
emptyPlayerEditForm,
|
||
editFormFromDetail,
|
||
buildCreatePlayerPayload,
|
||
type PlayerRow,
|
||
type PlayerDetail,
|
||
type PlayerCreateForm,
|
||
type PlayerEditForm,
|
||
} from './user-form';
|
||
import {
|
||
formatAmount,
|
||
formatAmountFull,
|
||
shouldCompactAmount as shouldCompact,
|
||
} from '../utils/format-amount';
|
||
|
||
const users = ref<PlayerRow[]>([]);
|
||
const total = ref(0);
|
||
const page = ref(1);
|
||
const pageSize = ref(10);
|
||
const keyword = ref('');
|
||
const filterStatus = ref('');
|
||
const filterParentId = ref('');
|
||
|
||
const agentOptions = ref<{ id: string; username: string }[]>([]);
|
||
|
||
const createVisible = ref(false);
|
||
const editVisible = ref(false);
|
||
const detailVisible = ref(false);
|
||
const depositVisible = ref(false);
|
||
const createLoading = ref(false);
|
||
const editLoading = ref(false);
|
||
const depositLoading = ref(false);
|
||
|
||
const createForm = ref<PlayerCreateForm>(emptyPlayerCreateForm());
|
||
const editForm = ref<PlayerEditForm>(emptyPlayerEditForm());
|
||
const detail = ref<PlayerDetail | null>(null);
|
||
const editingId = ref('');
|
||
|
||
const depositForm = ref({ userId: '', amount: 100, remark: '' });
|
||
|
||
onMounted(() => {
|
||
loadAgentOptions();
|
||
load();
|
||
});
|
||
|
||
async function loadAgentOptions() {
|
||
const { data } = await api.get('/admin/agents/options');
|
||
agentOptions.value = data.data;
|
||
}
|
||
|
||
async function load() {
|
||
const { data } = await api.get('/admin/users', {
|
||
params: {
|
||
page: page.value,
|
||
pageSize: pageSize.value,
|
||
keyword: keyword.value || undefined,
|
||
status: filterStatus.value || undefined,
|
||
parentId: filterParentId.value || undefined,
|
||
},
|
||
});
|
||
users.value = data.data.items;
|
||
total.value = data.data.total;
|
||
}
|
||
|
||
function onPageChange(p: number) {
|
||
page.value = p;
|
||
load();
|
||
}
|
||
|
||
function onSizeChange(size: number) {
|
||
pageSize.value = size;
|
||
page.value = 1;
|
||
load();
|
||
}
|
||
|
||
function openCreate() {
|
||
createForm.value = emptyPlayerCreateForm();
|
||
createVisible.value = true;
|
||
}
|
||
|
||
function parentLabel(row: PlayerRow) {
|
||
return row.parentUsername ?? t('common.platform_direct');
|
||
}
|
||
|
||
async function openDetail(id: string) {
|
||
const { data } = await api.get(`/admin/users/${id}`);
|
||
detail.value = data.data as PlayerDetail;
|
||
detailVisible.value = true;
|
||
}
|
||
|
||
async function openEdit(id: string) {
|
||
const { data } = await api.get(`/admin/users/${id}`);
|
||
const d = data.data as PlayerDetail;
|
||
editingId.value = id;
|
||
editForm.value = editFormFromDetail(d);
|
||
editVisible.value = true;
|
||
}
|
||
|
||
function openDeposit(row: PlayerRow) {
|
||
depositForm.value = { userId: row.id, amount: 100, remark: t('user.deposit_remark_default') };
|
||
depositVisible.value = true;
|
||
}
|
||
|
||
async function submitCreate() {
|
||
let payload: ReturnType<typeof buildCreatePlayerPayload>;
|
||
try {
|
||
payload = buildCreatePlayerPayload(createForm.value);
|
||
} catch (e) {
|
||
ElMessage.warning(resolveFormError(e, t));
|
||
return;
|
||
}
|
||
createLoading.value = true;
|
||
try {
|
||
await api.post('/admin/users', payload);
|
||
ElMessage.success(t('msg.player_created'));
|
||
createVisible.value = false;
|
||
load();
|
||
} catch (e: unknown) {
|
||
const err = e as { response?: { data?: { error?: string } } };
|
||
ElMessage.error(err.response?.data?.error ?? t('msg.create_failed'));
|
||
} finally {
|
||
createLoading.value = false;
|
||
}
|
||
}
|
||
|
||
async function toggleFreeze(row: PlayerRow) {
|
||
const freeze = row.status === 'ACTIVE';
|
||
const action = freeze ? t('common.freeze') : t('common.unfreeze');
|
||
try {
|
||
await ElMessageBox.confirm(
|
||
t('msg.freeze_confirm_body', {
|
||
action,
|
||
name: row.username,
|
||
extra: freeze ? t('msg.freeze_extra') : '',
|
||
}),
|
||
t('msg.freeze_confirm_title', { action }),
|
||
{ type: 'warning', confirmButtonText: action, cancelButtonText: t('common.cancel') },
|
||
);
|
||
} catch {
|
||
return;
|
||
}
|
||
try {
|
||
await api.put(`/admin/users/${row.id}`, {
|
||
status: freeze ? 'SUSPENDED' : 'ACTIVE',
|
||
});
|
||
ElMessage.success(t('msg.freeze_done', { action }));
|
||
load();
|
||
} catch (e: unknown) {
|
||
const err = e as { response?: { data?: { error?: string } } };
|
||
ElMessage.error(err.response?.data?.error ?? t('msg.freeze_failed', { action }));
|
||
}
|
||
}
|
||
|
||
async function submitEdit() {
|
||
editLoading.value = true;
|
||
try {
|
||
await api.put(`/admin/users/${editingId.value}`, {
|
||
parentId: editForm.value.parentId || '',
|
||
phone: editForm.value.phone.trim() || undefined,
|
||
email: editForm.value.email.trim() || undefined,
|
||
});
|
||
ElMessage.success(t('msg.saved'));
|
||
editVisible.value = false;
|
||
load();
|
||
} catch (e: unknown) {
|
||
const err = e as { response?: { data?: { error?: string } } };
|
||
ElMessage.error(err.response?.data?.error ?? t('msg.save_failed'));
|
||
} finally {
|
||
editLoading.value = false;
|
||
}
|
||
}
|
||
|
||
async function submitDeposit() {
|
||
if (depositForm.value.amount <= 0) {
|
||
ElMessage.warning(t('msg.amount_gt_zero'));
|
||
return;
|
||
}
|
||
depositLoading.value = true;
|
||
try {
|
||
await api.post('/admin/wallet/deposit', {
|
||
userId: depositForm.value.userId,
|
||
amount: depositForm.value.amount,
|
||
remark: depositForm.value.remark,
|
||
requestId: `dep-${depositForm.value.userId}-${Date.now()}`,
|
||
});
|
||
ElMessage.success(t('msg.topup_ok'));
|
||
depositVisible.value = false;
|
||
load();
|
||
} catch (e: unknown) {
|
||
const err = e as { response?: { data?: { error?: string } } };
|
||
ElMessage.error(err.response?.data?.error ?? t('msg.topup_failed'));
|
||
} finally {
|
||
depositLoading.value = false;
|
||
}
|
||
}
|
||
|
||
function formatTime(v: string) {
|
||
if (!v) return '—';
|
||
return new Date(v).toLocaleString(localeTag.value);
|
||
}
|
||
|
||
function formatLastLogin(v: string | null) {
|
||
if (!v) return t('common.never_login');
|
||
const d = new Date(v);
|
||
const now = new Date();
|
||
const sameDay =
|
||
d.getFullYear() === now.getFullYear() &&
|
||
d.getMonth() === now.getMonth() &&
|
||
d.getDate() === now.getDate();
|
||
if (sameDay) {
|
||
return d.toLocaleTimeString(localeTag.value, { hour: '2-digit', minute: '2-digit' });
|
||
}
|
||
return d.toLocaleString(localeTag.value, {
|
||
month: '2-digit',
|
||
day: '2-digit',
|
||
hour: '2-digit',
|
||
minute: '2-digit',
|
||
});
|
||
}
|
||
|
||
function statusTagType(s: string) {
|
||
return s === 'ACTIVE' ? 'success' : 'warning';
|
||
}
|
||
|
||
function statusLabel(s: string) {
|
||
const key = `user.status.${s}`;
|
||
const v = t(key);
|
||
return v !== key ? v : s;
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<div class="admin-list-page users-page">
|
||
<div class="page-header">
|
||
<div>
|
||
<h2 class="page-title">{{ t('page.users.title') }}</h2>
|
||
<span class="page-desc">{{ t('page.users.desc') }}</span>
|
||
</div>
|
||
<el-button type="primary" @click="openCreate">{{ t('user.create_btn') }}</el-button>
|
||
</div>
|
||
|
||
<el-card class="filter-card" shadow="never">
|
||
<el-form inline>
|
||
<el-form-item :label="t('common.keyword')">
|
||
<el-input
|
||
v-model="keyword"
|
||
:placeholder="t('user.filter.username_ph')"
|
||
clearable
|
||
style="width: 160px"
|
||
@keyup.enter="load"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item :label="t('user.filter.agent')">
|
||
<el-select
|
||
v-model="filterParentId"
|
||
:placeholder="t('user.filter.agent_ph')"
|
||
clearable
|
||
style="width: 180px"
|
||
>
|
||
<el-option
|
||
v-for="a in agentOptions"
|
||
:key="a.id"
|
||
:label="a.username"
|
||
:value="a.id"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item :label="t('common.status')">
|
||
<el-select v-model="filterStatus" :placeholder="t('common.all')" clearable style="width: 120px">
|
||
<el-option :label="t('user.status.ACTIVE')" value="ACTIVE" />
|
||
<el-option :label="t('user.status.SUSPENDED')" value="SUSPENDED" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item>
|
||
<el-button type="primary" @click="load">{{ t('common.search') }}</el-button>
|
||
</el-form-item>
|
||
</el-form>
|
||
</el-card>
|
||
|
||
<el-card class="data-card" shadow="never">
|
||
<div class="table-wrap">
|
||
<el-table :data="users" stripe>
|
||
<el-table-column prop="id" label="ID" width="72" />
|
||
<el-table-column prop="username" :label="t('user.col.username')" min-width="120" />
|
||
<el-table-column :label="t('common.status')" width="88">
|
||
<template #default="{ row }">
|
||
<el-tag :type="statusTagType(row.status)" size="small">
|
||
{{ statusLabel(row.status) }}
|
||
</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column :label="t('user.col.agent')" min-width="120">
|
||
<template #default="{ row }">{{ parentLabel(row) }}</template>
|
||
</el-table-column>
|
||
<el-table-column :label="t('user.col.balance')" min-width="128" align="right">
|
||
<template #default="{ row }">
|
||
<el-tooltip
|
||
:content="`${formatAmountFull(row.availableBalance)} / ${formatAmountFull(row.frozenBalance)}`"
|
||
placement="top"
|
||
>
|
||
<span class="amount-compact">
|
||
{{ formatAmount(row.availableBalance) }} / {{ formatAmount(row.frozenBalance) }}
|
||
</span>
|
||
</el-tooltip>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="betCount" :label="t('user.col.bets')" width="64" align="center" />
|
||
<el-table-column :label="t('user.col.stake_payout')" min-width="108" align="right">
|
||
<template #default="{ row }">
|
||
<span class="amount-compact">
|
||
{{ formatAmount(row.totalStake) }} / {{ formatAmount(row.totalReturn) }}
|
||
</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column :label="t('user.col.last_login')" width="108">
|
||
<template #default="{ row }">
|
||
<el-tooltip v-if="row.lastLoginAt" :content="formatTime(row.lastLoginAt)" placement="top">
|
||
<span>{{ formatLastLogin(row.lastLoginAt) }}</span>
|
||
</el-tooltip>
|
||
<span v-else class="text-muted">{{ t('common.never_login') }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column :label="t('user.col.created')" width="108">
|
||
<template #default="{ row }">
|
||
<el-tooltip :content="formatTime(row.createdAt)" placement="top">
|
||
<span>{{ formatLastLogin(row.createdAt) }}</span>
|
||
</el-tooltip>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column :label="t('common.actions')" width="300" fixed="right" align="center">
|
||
<template #default="{ row }">
|
||
<el-button size="small" link type="primary" @click="openDetail(row.id)">{{ t('common.detail') }}</el-button>
|
||
<el-button size="small" link type="primary" @click="openEdit(row.id)">{{ t('common.edit') }}</el-button>
|
||
<el-button size="small" link type="primary" @click="openDeposit(row)">{{ t('common.topup') }}</el-button>
|
||
<el-button
|
||
v-if="row.status === 'ACTIVE'"
|
||
size="small"
|
||
link
|
||
type="warning"
|
||
@click="toggleFreeze(row)"
|
||
>
|
||
{{ t('common.freeze') }}
|
||
</el-button>
|
||
<el-button
|
||
v-else
|
||
size="small"
|
||
link
|
||
type="primary"
|
||
@click="toggleFreeze(row)"
|
||
>
|
||
{{ t('common.unfreeze') }}
|
||
</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</div>
|
||
<div class="pager">
|
||
<el-pagination
|
||
v-model:current-page="page"
|
||
v-model:page-size="pageSize"
|
||
:total="total"
|
||
:page-sizes="[10, 20, 50, 100]"
|
||
layout="total, sizes, prev, pager, next"
|
||
background
|
||
@current-change="onPageChange"
|
||
@size-change="onSizeChange"
|
||
/>
|
||
</div>
|
||
</el-card>
|
||
|
||
<el-dialog v-model="createVisible" :title="t('user.dialog.create')" width="520px" destroy-on-close>
|
||
<el-form label-width="100px">
|
||
<el-form-item :label="t('user.col.username')" required>
|
||
<el-input v-model="createForm.username" :placeholder="t('user.ph.username_unique')" />
|
||
</el-form-item>
|
||
<el-form-item :label="t('user.field.password')" required>
|
||
<el-input v-model="createForm.password" type="text" autocomplete="off" />
|
||
</el-form-item>
|
||
<el-form-item :label="t('user.field.confirm_password')" required>
|
||
<el-input v-model="createForm.confirmPassword" type="text" autocomplete="off" />
|
||
</el-form-item>
|
||
<el-form-item :label="t('user.filter.agent')">
|
||
<el-select
|
||
v-model="createForm.parentId"
|
||
:placeholder="t('user.ph.no_agent')"
|
||
clearable
|
||
style="width: 100%"
|
||
>
|
||
<el-option
|
||
v-for="a in agentOptions"
|
||
:key="a.id"
|
||
:label="`${a.username} (#${a.id})`"
|
||
:value="a.id"
|
||
/>
|
||
</el-select>
|
||
<div class="field-hint">{{ t('user.hint.no_agent') }}</div>
|
||
</el-form-item>
|
||
<el-form-item :label="t('user.field.phone')">
|
||
<el-input v-model="createForm.phone" :placeholder="t('common.optional')" />
|
||
</el-form-item>
|
||
<el-form-item :label="t('user.field.email')">
|
||
<el-input v-model="createForm.email" :placeholder="t('common.optional')" />
|
||
</el-form-item>
|
||
<el-form-item :label="t('user.field.initial_balance')">
|
||
<el-input-number v-model="createForm.initialDeposit" :min="0" :step="100" style="width: 100%" />
|
||
<div class="field-hint">{{ t('user.hint.initial_balance') }}</div>
|
||
</el-form-item>
|
||
<el-form-item :label="t('user.field.deposit_remark')">
|
||
<el-input v-model="createForm.remark" :placeholder="t('user.ph.remark_initial')" />
|
||
</el-form-item>
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button @click="createVisible = false">{{ t('common.cancel') }}</el-button>
|
||
<el-button type="primary" :loading="createLoading" @click="submitCreate">{{ t('user.btn.create') }}</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<el-dialog v-model="editVisible" :title="t('user.dialog.edit')" width="560px" destroy-on-close>
|
||
<el-form label-width="100px">
|
||
<el-form-item :label="t('user.field.player_id')">
|
||
<el-input :model-value="editForm.id" disabled />
|
||
</el-form-item>
|
||
<el-form-item :label="t('user.col.username')">
|
||
<el-input :model-value="editForm.username" disabled />
|
||
</el-form-item>
|
||
<el-form-item :label="t('user.field.account_status')">
|
||
<el-tag :type="statusTagType(editForm.status)" size="small">
|
||
{{ statusLabel(editForm.status) }}
|
||
</el-tag>
|
||
<span class="field-hint inline-hint">{{ t('user.hint.freeze_in_list') }}</span>
|
||
</el-form-item>
|
||
<el-form-item :label="t('user.filter.agent')">
|
||
<el-select
|
||
v-model="editForm.parentId"
|
||
:placeholder="t('user.ph.no_agent')"
|
||
clearable
|
||
style="width: 100%"
|
||
>
|
||
<el-option
|
||
v-for="a in agentOptions"
|
||
:key="a.id"
|
||
:label="`${a.username} (#${a.id})`"
|
||
:value="a.id"
|
||
/>
|
||
</el-select>
|
||
<div class="field-hint">{{ t('user.hint.agent_change') }}</div>
|
||
</el-form-item>
|
||
<el-form-item :label="t('user.field.available')">
|
||
<el-input :model-value="formatAmount(editForm.availableBalance)" disabled />
|
||
</el-form-item>
|
||
<el-form-item :label="t('user.field.frozen_balance')">
|
||
<el-input :model-value="formatAmount(editForm.frozenBalance)" disabled />
|
||
</el-form-item>
|
||
<el-form-item :label="t('user.field.bets_summary')">
|
||
<el-input
|
||
:model-value="t('user.bets_edit_value', { n: editForm.betCount, stake: formatAmount(editForm.totalStake) })"
|
||
disabled
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item :label="t('user.field.total_payout')">
|
||
<el-input :model-value="formatAmount(editForm.totalReturn)" disabled />
|
||
</el-form-item>
|
||
<el-form-item :label="t('user.col.last_login')">
|
||
<el-input
|
||
:model-value="editForm.lastLoginAt ? formatTime(editForm.lastLoginAt) : t('common.never_login')"
|
||
disabled
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item :label="t('user.field.login_fail')">
|
||
<el-input :model-value="t('user.login_fail_value', { n: editForm.loginFailCount })" disabled />
|
||
</el-form-item>
|
||
<el-form-item :label="t('user.col.created')">
|
||
<el-input :model-value="formatTime(editForm.createdAt)" disabled />
|
||
</el-form-item>
|
||
<el-divider />
|
||
<el-form-item :label="t('user.field.phone')">
|
||
<el-input v-model="editForm.phone" :placeholder="t('common.optional')" />
|
||
</el-form-item>
|
||
<el-form-item :label="t('user.field.email')">
|
||
<el-input v-model="editForm.email" :placeholder="t('common.optional')" />
|
||
</el-form-item>
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button @click="editVisible = false">{{ t('common.cancel') }}</el-button>
|
||
<el-button type="primary" :loading="editLoading" @click="submitEdit">{{ t('user.btn.save_profile') }}</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<el-dialog v-model="depositVisible" :title="t('user.dialog.deposit')" width="400px" destroy-on-close>
|
||
<el-form label-width="80px">
|
||
<el-form-item :label="t('user.field.player_id')">
|
||
<el-input :model-value="depositForm.userId" disabled />
|
||
</el-form-item>
|
||
<el-form-item :label="t('user.field.amount')">
|
||
<el-input-number v-model="depositForm.amount" :min="0.01" :step="10" style="width: 100%" />
|
||
</el-form-item>
|
||
<el-form-item :label="t('user.field.remark')">
|
||
<el-input v-model="depositForm.remark" />
|
||
</el-form-item>
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button @click="depositVisible = false">{{ t('common.cancel') }}</el-button>
|
||
<el-button type="primary" :loading="depositLoading" @click="submitDeposit">{{ t('user.btn.confirm_deposit') }}</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<el-dialog v-model="detailVisible" :title="t('user.dialog.detail')" width="560px" destroy-on-close>
|
||
<template v-if="detail">
|
||
<el-descriptions :column="2" border size="small">
|
||
<el-descriptions-item :label="t('common.col_id')">{{ detail.id }}</el-descriptions-item>
|
||
<el-descriptions-item :label="t('user.col.username')">{{ detail.username }}</el-descriptions-item>
|
||
<el-descriptions-item :label="t('common.status')">
|
||
<el-tag :type="statusTagType(detail.status)" size="small">
|
||
{{ statusLabel(detail.status) }}
|
||
</el-tag>
|
||
</el-descriptions-item>
|
||
<el-descriptions-item :label="t('user.col.agent')">
|
||
{{ detail.parentUsername ?? t('common.platform_direct') }}
|
||
</el-descriptions-item>
|
||
<el-descriptions-item :label="t('user.field.available')">
|
||
{{ formatAmount(detail.availableBalance) }}
|
||
<span v-if="shouldCompact(detail.availableBalance)" class="amount-full-hint">
|
||
({{ formatAmountFull(detail.availableBalance) }})
|
||
</span>
|
||
</el-descriptions-item>
|
||
<el-descriptions-item :label="t('user.field.frozen_balance')">
|
||
{{ formatAmount(detail.frozenBalance) }}
|
||
<span v-if="shouldCompact(detail.frozenBalance)" class="amount-full-hint">
|
||
({{ formatAmountFull(detail.frozenBalance) }})
|
||
</span>
|
||
</el-descriptions-item>
|
||
<el-descriptions-item :label="t('user.field.phone')">{{ detail.phone ?? '—' }}</el-descriptions-item>
|
||
<el-descriptions-item :label="t('user.field.email')">{{ detail.email ?? '—' }}</el-descriptions-item>
|
||
<el-descriptions-item :label="t('user.field.bet_count')">{{ detail.betCount }}</el-descriptions-item>
|
||
<el-descriptions-item :label="t('user.field.total_stake')">{{ formatAmount(detail.totalStake) }}</el-descriptions-item>
|
||
<el-descriptions-item :label="t('user.field.total_payout')">{{ formatAmount(detail.totalReturn) }}</el-descriptions-item>
|
||
<el-descriptions-item :label="t('user.col.last_login')">
|
||
{{ detail.lastLoginAt ? formatTime(detail.lastLoginAt) : t('common.never_login') }}
|
||
</el-descriptions-item>
|
||
<el-descriptions-item :label="t('user.field.login_fail')">{{ t('user.login_fail_value', { n: detail.loginFailCount }) }}</el-descriptions-item>
|
||
<el-descriptions-item :label="t('user.field.registered_at')" :span="2">
|
||
{{ formatTime(detail.createdAt) }}
|
||
</el-descriptions-item>
|
||
</el-descriptions>
|
||
</template>
|
||
</el-dialog>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.page-header {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
justify-content: space-between;
|
||
margin-bottom: 20px;
|
||
}
|
||
.page-title { font-size: 20px; font-weight: 700; color: #e0e0e0; margin: 0 0 4px; }
|
||
.page-desc { font-size: 13px; color: #666; }
|
||
.filter-card { margin-bottom: 16px; border-radius: 12px; }
|
||
.data-card { border-radius: 12px; }
|
||
.pager { margin-top: 16px; display: flex; justify-content: flex-end; }
|
||
.field-hint { font-size: 12px; color: #888; margin-top: 4px; }
|
||
.inline-hint { margin-top: 0; margin-left: 10px; display: inline-block; }
|
||
.amount-compact { white-space: nowrap; font-variant-numeric: tabular-nums; cursor: default; }
|
||
.amount-full-hint { font-size: 11px; color: #666; margin-left: 4px; }
|
||
.text-muted { color: #666; font-size: 12px; }
|
||
</style>
|
||
|
||
<style>
|
||
/* 玩家列表「冻结」:橙黄底白字 */
|
||
.users-page .el-button.is-link.el-button--warning {
|
||
color: #ffffff !important;
|
||
background: linear-gradient(165deg, #e8a84a 0%, #c47a18 42%, #9a5c10 100%) !important;
|
||
border: 1px solid rgba(232, 168, 74, 0.45) !important;
|
||
border-radius: 6px !important;
|
||
padding: 5px 11px !important;
|
||
box-shadow: 0 1px 0 rgba(255, 255, 255, 0.12) inset, 0 1px 6px rgba(0, 0, 0, 0.35) !important;
|
||
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2);
|
||
}
|
||
.users-page .el-button.is-link.el-button--warning:hover,
|
||
.users-page .el-button.is-link.el-button--warning:focus {
|
||
color: #ffffff !important;
|
||
background: linear-gradient(165deg, #f0bc62 0%, #d48a28 42%, #a86814 100%) !important;
|
||
border-color: rgba(240, 188, 98, 0.55) !important;
|
||
}
|
||
</style>
|