从 main 同步站内信手动发送、媒体库选择、列表子路由重构等 API/Admin 改动;玩家端仅合并 ADMIN_CUSTOM 富文本、消息预览与充值截图压缩逻辑,保留 theme-2 样式。
369 lines
13 KiB
Vue
369 lines
13 KiB
Vue
<script setup lang="ts">
|
|
import { ref, onMounted } from 'vue';
|
|
import { useRouter } from 'vue-router';
|
|
import { ElMessage, ElMessageBox } from 'element-plus';
|
|
import { useAdminLocale } from '../../composables/useAdminLocale';
|
|
import { usePermissions } from '../../composables/usePermissions';
|
|
import { AdminPerm } from '../../constants/permissions';
|
|
import api from '../../api';
|
|
import { clearStaffSession } from '../../stores/auth';
|
|
import AdminSubNav from '../../components/AdminSubNav.vue';
|
|
import RatePercentInput from '../../components/RatePercentInput.vue';
|
|
import { percentToDecimalRate, decimalRateToPercent } from '../../utils/rate-percent';
|
|
|
|
defineOptions({ name: 'AdminGlobalSettingsView' });
|
|
|
|
const { t } = useAdminLocale();
|
|
const router = useRouter();
|
|
const { hasPermission } = usePermissions();
|
|
const canManageSettings = hasPermission(AdminPerm.settings);
|
|
|
|
const playerSettings = ref({ allowPasswordChange: true, allowUsernameChange: false });
|
|
const bettingLimits = ref({
|
|
minStake: 1,
|
|
maxStakeSingle: 50000,
|
|
maxStakeParlay: 20000,
|
|
maxPayoutSingle: 500000,
|
|
maxPayoutParlay: 1000000,
|
|
dailyStakeLimit: 200000,
|
|
});
|
|
const hierarchySettings = ref({ maxAgentLevel: 0 });
|
|
const platformDirectRate = ref(0);
|
|
const adminInviteRate = ref(0);
|
|
const resetAllowed = ref(false);
|
|
const resetConfirmPhrase = ref('');
|
|
|
|
const settingsSaving = ref(false);
|
|
const limitsSaving = ref(false);
|
|
const hierarchySaving = ref(false);
|
|
const platformDirectSaving = ref(false);
|
|
const resetLoading = ref(false);
|
|
const loading = ref(false);
|
|
|
|
async function loadSettings() {
|
|
loading.value = true;
|
|
try {
|
|
const { data } = await api.get('/admin/users/page-init');
|
|
const payload = data.data as {
|
|
playerSettings?: typeof playerSettings.value;
|
|
bettingLimits?: typeof bettingLimits.value;
|
|
hierarchySettings?: { maxAgentLevel: number };
|
|
platformDirect?: { platformDirectRate?: number | string; adminInviteRate?: number | string };
|
|
};
|
|
if (payload.playerSettings) playerSettings.value = payload.playerSettings;
|
|
if (payload.bettingLimits) bettingLimits.value = payload.bettingLimits;
|
|
if (payload.hierarchySettings) {
|
|
hierarchySettings.value = {
|
|
maxAgentLevel: payload.hierarchySettings.maxAgentLevel ?? 0,
|
|
};
|
|
}
|
|
if (payload.platformDirect) {
|
|
platformDirectRate.value = decimalRateToPercent(payload.platformDirect.platformDirectRate ?? 0);
|
|
adminInviteRate.value = decimalRateToPercent(
|
|
payload.platformDirect.adminInviteRate ?? payload.platformDirect.platformDirectRate ?? 0,
|
|
);
|
|
}
|
|
} catch {
|
|
/* keep defaults */
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
}
|
|
|
|
async function loadResetDatabaseStatus() {
|
|
try {
|
|
const { data } = await api.get('/admin/system/reset-database');
|
|
resetAllowed.value = !!data.data?.allowed;
|
|
} catch {
|
|
resetAllowed.value = false;
|
|
}
|
|
}
|
|
|
|
async function savePlayerSettings() {
|
|
settingsSaving.value = true;
|
|
try {
|
|
const { data } = await api.put('/admin/users/settings/account', playerSettings.value);
|
|
playerSettings.value = data.data;
|
|
ElMessage.success(t('msg.saved'));
|
|
} catch (e: unknown) {
|
|
const err = e as { response?: { data?: { error?: string } } };
|
|
ElMessage.error(err.response?.data?.error ?? t('msg.save_failed'));
|
|
} finally {
|
|
settingsSaving.value = false;
|
|
}
|
|
}
|
|
|
|
async function saveHierarchySettings() {
|
|
hierarchySaving.value = true;
|
|
try {
|
|
const { data } = await api.put('/admin/agents/settings/hierarchy', hierarchySettings.value);
|
|
hierarchySettings.value = { maxAgentLevel: data.data?.maxAgentLevel ?? hierarchySettings.value.maxAgentLevel };
|
|
ElMessage.success(t('msg.saved'));
|
|
} catch (e: unknown) {
|
|
const err = e as { response?: { data?: { error?: string } } };
|
|
ElMessage.error(err.response?.data?.error ?? t('msg.save_failed'));
|
|
} finally {
|
|
hierarchySaving.value = false;
|
|
}
|
|
}
|
|
|
|
async function savePlatformDirectSettings() {
|
|
platformDirectSaving.value = true;
|
|
try {
|
|
const { data } = await api.put('/admin/settings/cashback/platform-direct', {
|
|
platformDirectRate: percentToDecimalRate(platformDirectRate.value),
|
|
adminInviteRate: percentToDecimalRate(adminInviteRate.value),
|
|
});
|
|
const payload = data.data as { platformDirectRate?: number | string; adminInviteRate?: number | string };
|
|
platformDirectRate.value = decimalRateToPercent(payload?.platformDirectRate ?? 0);
|
|
adminInviteRate.value = decimalRateToPercent(payload?.adminInviteRate ?? payload?.platformDirectRate ?? 0);
|
|
ElMessage.success(t('msg.saved'));
|
|
} catch (e: unknown) {
|
|
const err = e as { response?: { data?: { error?: string } } };
|
|
ElMessage.error(err.response?.data?.error ?? t('msg.save_failed'));
|
|
} finally {
|
|
platformDirectSaving.value = false;
|
|
}
|
|
}
|
|
|
|
async function saveBettingLimits() {
|
|
limitsSaving.value = true;
|
|
try {
|
|
const { data } = await api.put('/admin/settings/betting-limits', bettingLimits.value);
|
|
bettingLimits.value = data.data;
|
|
ElMessage.success(t('msg.saved'));
|
|
} catch (e: unknown) {
|
|
const err = e as { response?: { data?: { error?: string } } };
|
|
ElMessage.error(err.response?.data?.error ?? t('msg.save_failed'));
|
|
} finally {
|
|
limitsSaving.value = false;
|
|
}
|
|
}
|
|
|
|
async function resetDatabase() {
|
|
if (resetConfirmPhrase.value !== 'RESET') {
|
|
ElMessage.warning(t('user.reset_database_confirm_label'));
|
|
return;
|
|
}
|
|
try {
|
|
await ElMessageBox.confirm(t('user.reset_database_hint'), t('user.reset_database'), {
|
|
type: 'warning',
|
|
confirmButtonText: t('user.reset_database_btn'),
|
|
cancelButtonText: t('common.cancel'),
|
|
});
|
|
} catch {
|
|
return;
|
|
}
|
|
resetLoading.value = true;
|
|
try {
|
|
const { data } = await api.post('/admin/system/reset-database', { confirmPhrase: 'RESET' });
|
|
const accounts: string[] = data.data?.demoAccounts ?? [];
|
|
ElMessage.success({
|
|
message: `${t('user.reset_database_success')}\n${t('user.reset_database_accounts')}: ${accounts.join(' · ')}`,
|
|
duration: 8000,
|
|
});
|
|
clearStaffSession();
|
|
await router.push('/login');
|
|
} catch (e: unknown) {
|
|
const err = e as { response?: { data?: { error?: string } } };
|
|
ElMessage.error(err.response?.data?.error ?? t('msg.save_failed'));
|
|
} finally {
|
|
resetLoading.value = false;
|
|
}
|
|
}
|
|
|
|
onMounted(() => {
|
|
if (!canManageSettings) {
|
|
void router.replace('/users');
|
|
return;
|
|
}
|
|
void Promise.all([loadSettings(), loadResetDatabaseStatus()]);
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<div v-loading="loading" class="admin-list-page global-settings-page">
|
|
<AdminSubNav :title="t('user.page_settings')" />
|
|
|
|
<section class="global-settings-panel">
|
|
<div class="list-settings-block">
|
|
<p class="list-settings-title">{{ t('user.global_settings') }}</p>
|
|
<el-form inline size="small" class="settings-form">
|
|
<el-form-item :label="t('user.field.allow_password_change')">
|
|
<el-switch v-model="playerSettings.allowPasswordChange" :loading="settingsSaving" @change="savePlayerSettings" />
|
|
</el-form-item>
|
|
<el-form-item :label="t('user.field.allow_username_change')">
|
|
<el-switch v-model="playerSettings.allowUsernameChange" :loading="settingsSaving" @change="savePlayerSettings" />
|
|
</el-form-item>
|
|
</el-form>
|
|
</div>
|
|
|
|
<div class="list-settings-block">
|
|
<p class="list-settings-title">{{ t('agent.hierarchy.settings_title') }}</p>
|
|
<p class="list-settings-hint">{{ t('agent.hierarchy.settings_hint') }}</p>
|
|
<el-form inline size="small" class="settings-form">
|
|
<el-form-item :label="t('agent.hierarchy.max_level')">
|
|
<el-input-number
|
|
v-model="hierarchySettings.maxAgentLevel"
|
|
:min="0"
|
|
:step="1"
|
|
controls-position="right"
|
|
:disabled="hierarchySaving"
|
|
/>
|
|
</el-form-item>
|
|
<el-form-item>
|
|
<el-button type="primary" :loading="hierarchySaving" @click="saveHierarchySettings">
|
|
{{ t('common.save') }}
|
|
</el-button>
|
|
</el-form-item>
|
|
</el-form>
|
|
</div>
|
|
|
|
<div class="list-settings-block">
|
|
<p class="list-settings-title">{{ t('cashback.settings_title') }}</p>
|
|
<el-form inline size="small" class="settings-form">
|
|
<el-form-item :label="t('cashback.platform_direct_default_rate')">
|
|
<RatePercentInput v-model="platformDirectRate" />
|
|
</el-form-item>
|
|
<p class="list-settings-hint block-hint">{{ t('cashback.platform_direct_default_hint') }}</p>
|
|
<el-form-item :label="t('cashback.admin_invite_default_rate')">
|
|
<RatePercentInput v-model="adminInviteRate" />
|
|
</el-form-item>
|
|
<p class="list-settings-hint block-hint">{{ t('cashback.admin_invite_default_hint') }}</p>
|
|
<el-form-item>
|
|
<el-button type="primary" :loading="platformDirectSaving" @click="savePlatformDirectSettings">
|
|
{{ t('common.save') }}
|
|
</el-button>
|
|
</el-form-item>
|
|
</el-form>
|
|
</div>
|
|
|
|
<div class="list-settings-block">
|
|
<p class="list-settings-title">{{ t('user.betting_limits') }}</p>
|
|
<el-form inline size="small" class="settings-form limits-form">
|
|
<el-form-item :label="t('user.limit.min_stake')">
|
|
<el-input-number v-model="bettingLimits.minStake" :min="0" :step="1" controls-position="right" />
|
|
</el-form-item>
|
|
<el-form-item :label="t('user.limit.max_stake_single')">
|
|
<el-input-number v-model="bettingLimits.maxStakeSingle" :min="0" :step="100" controls-position="right" />
|
|
</el-form-item>
|
|
<el-form-item :label="t('user.limit.max_stake_parlay')">
|
|
<el-input-number v-model="bettingLimits.maxStakeParlay" :min="0" :step="100" controls-position="right" />
|
|
</el-form-item>
|
|
<el-form-item :label="t('user.limit.max_payout_single')">
|
|
<el-input-number v-model="bettingLimits.maxPayoutSingle" :min="0" :step="1000" controls-position="right" />
|
|
</el-form-item>
|
|
<el-form-item :label="t('user.limit.max_payout_parlay')">
|
|
<el-input-number v-model="bettingLimits.maxPayoutParlay" :min="0" :step="1000" controls-position="right" />
|
|
</el-form-item>
|
|
<el-form-item :label="t('user.limit.daily_stake')">
|
|
<el-input-number v-model="bettingLimits.dailyStakeLimit" :min="0" :step="1000" controls-position="right" />
|
|
</el-form-item>
|
|
<el-form-item>
|
|
<el-button type="primary" :loading="limitsSaving" @click="saveBettingLimits">
|
|
{{ t('common.save') }}
|
|
</el-button>
|
|
</el-form-item>
|
|
</el-form>
|
|
</div>
|
|
|
|
<div class="list-settings-block list-settings-block--danger">
|
|
<p class="list-settings-title">{{ t('user.reset_database') }}</p>
|
|
<p class="list-settings-hint">{{ t('user.reset_database_hint') }}</p>
|
|
<el-alert
|
|
v-if="!resetAllowed"
|
|
type="warning"
|
|
:closable="false"
|
|
show-icon
|
|
class="reset-db-alert"
|
|
:title="t('user.reset_database_disabled_prod')"
|
|
/>
|
|
<el-form inline size="small" class="settings-form reset-db-form">
|
|
<el-form-item :label="t('user.reset_database_confirm_label')">
|
|
<el-input
|
|
v-model="resetConfirmPhrase"
|
|
:placeholder="t('user.reset_database_confirm_ph')"
|
|
style="width: 160px"
|
|
:disabled="!resetAllowed"
|
|
autocomplete="off"
|
|
/>
|
|
</el-form-item>
|
|
<el-form-item>
|
|
<el-button
|
|
type="danger"
|
|
plain
|
|
:loading="resetLoading"
|
|
:disabled="!resetAllowed || resetConfirmPhrase !== 'RESET'"
|
|
@click="resetDatabase"
|
|
>
|
|
{{ t('user.reset_database_btn') }}
|
|
</el-button>
|
|
</el-form-item>
|
|
</el-form>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.global-settings-page {
|
|
display: flex;
|
|
flex-direction: column;
|
|
height: 100%;
|
|
min-height: 0;
|
|
}
|
|
|
|
.global-settings-page :deep(.admin-subnav) {
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.global-settings-panel {
|
|
flex: 1;
|
|
min-height: 0;
|
|
overflow: auto;
|
|
padding: 12px 14px 16px;
|
|
border: 1px solid var(--border);
|
|
border-radius: 10px;
|
|
background: var(--bg-card);
|
|
}
|
|
|
|
.list-settings-block + .list-settings-block {
|
|
margin-top: 12px;
|
|
padding-top: 12px;
|
|
border-top: 1px solid var(--border-soft);
|
|
}
|
|
|
|
.list-settings-title {
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
color: var(--text);
|
|
margin: 0 0 8px;
|
|
}
|
|
|
|
.list-settings-hint {
|
|
font-size: 12px;
|
|
color: var(--text-muted);
|
|
margin: 0 0 10px;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.block-hint {
|
|
width: 100%;
|
|
margin-top: -4px;
|
|
}
|
|
|
|
.list-settings-block--danger {
|
|
border-top: 1px dashed var(--danger-border);
|
|
}
|
|
|
|
.reset-db-alert {
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.limits-form {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 4px 12px;
|
|
}
|
|
</style>
|