feat(theme-2): sync inbox/announcements/presence/deposit from main
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import api from '../api';
|
||||
import { useAdminLocale } from '../composables/useAdminLocale';
|
||||
import { useAuthStore } from '../stores/auth';
|
||||
import AdminTableEmpty from '../components/AdminTableEmpty.vue';
|
||||
import AdminTableWrap from '../components/AdminTableWrap.vue';
|
||||
|
||||
@@ -14,6 +15,7 @@ interface StaffRow {
|
||||
roleName: string | null;
|
||||
lastLoginAt: string | null;
|
||||
createdAt: string;
|
||||
visibleMenus: string | null;
|
||||
}
|
||||
|
||||
interface RoleOption {
|
||||
@@ -23,22 +25,46 @@ interface RoleOption {
|
||||
}
|
||||
|
||||
const { t, localeTag } = useAdminLocale();
|
||||
const authStore = useAuthStore();
|
||||
const currentUserId = computed(() => authStore.user.value?.id);
|
||||
|
||||
const loading = ref(false);
|
||||
const rows = ref<StaffRow[]>([]);
|
||||
const total = ref(0);
|
||||
const page = ref(1);
|
||||
const pageSize = ref(20);
|
||||
const pageSize = ref(10);
|
||||
const keyword = ref('');
|
||||
const roles = ref<RoleOption[]>([]);
|
||||
|
||||
const menuOptions = [
|
||||
{ key: 'dashboard', label: 'nav.dashboard' },
|
||||
{ key: 'matches', label: 'nav.matches' },
|
||||
{ key: 'users', label: 'nav.agents_players' },
|
||||
{ key: 'finance-logs', label: 'nav.finance_logs' },
|
||||
{ key: 'deposit', label: 'nav.deposit_manage' },
|
||||
{ key: 'cashback', label: 'nav.cashback' },
|
||||
{ key: 'bets', label: 'nav.bets' },
|
||||
{ key: 'contents', label: 'nav.contents' },
|
||||
{ key: 'media', label: 'nav.media' },
|
||||
{ key: 'audit', label: 'nav.audit' },
|
||||
{ key: 'staff', label: 'nav.staff' },
|
||||
{ key: 'smoke-tests', label: 'nav.smoke_tests' },
|
||||
];
|
||||
|
||||
const ROLE_DEFAULT_MENUS: Record<string, string[]> = {
|
||||
SUPER_ADMIN: ['dashboard', 'matches', 'users', 'finance-logs', 'deposit', 'cashback', 'bets', 'contents', 'media', 'audit', 'staff', 'smoke-tests'],
|
||||
MATCH_ADMIN: ['matches', 'bets', 'contents', 'media'],
|
||||
FINANCE_ADMIN: ['dashboard', 'users', 'finance-logs', 'deposit', 'cashback', 'bets'],
|
||||
SUPPORT: ['users', 'bets', 'contents', 'media'],
|
||||
};
|
||||
|
||||
const createVisible = ref(false);
|
||||
const createLoading = ref(false);
|
||||
const createForm = ref({ username: '', password: '', confirmPassword: '', roleCode: 'MATCH_ADMIN' });
|
||||
const createForm = ref({ username: '', password: '', confirmPassword: '', roleCode: 'MATCH_ADMIN', checkedMenus: [] as string[] });
|
||||
|
||||
const editVisible = ref(false);
|
||||
const editLoading = ref(false);
|
||||
const editForm = ref({ id: '', username: '', status: 'ACTIVE', roleCode: 'MATCH_ADMIN', password: '' });
|
||||
const editForm = ref({ id: '', username: '', status: 'ACTIVE', roleCode: 'MATCH_ADMIN', password: '', checkedMenus: [] as string[] });
|
||||
|
||||
const roleLabel = computed(() => {
|
||||
const map: Record<string, string> = {
|
||||
@@ -80,8 +106,18 @@ async function load() {
|
||||
}
|
||||
}
|
||||
|
||||
function onCreateRoleChange(newRole: string) {
|
||||
createForm.value.checkedMenus = [...(ROLE_DEFAULT_MENUS[newRole] || [])];
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
createForm.value = { username: '', password: '', confirmPassword: '', roleCode: 'MATCH_ADMIN' };
|
||||
createForm.value = {
|
||||
username: '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
roleCode: 'MATCH_ADMIN',
|
||||
checkedMenus: [...ROLE_DEFAULT_MENUS['MATCH_ADMIN']],
|
||||
};
|
||||
createVisible.value = true;
|
||||
}
|
||||
|
||||
@@ -105,6 +141,7 @@ async function submitCreate() {
|
||||
username: f.username.trim(),
|
||||
password: f.password,
|
||||
roleCode: f.roleCode,
|
||||
visibleMenus: f.checkedMenus.join(','),
|
||||
});
|
||||
ElMessage.success(t('msg.saved'));
|
||||
createVisible.value = false;
|
||||
@@ -117,6 +154,10 @@ async function submitCreate() {
|
||||
}
|
||||
}
|
||||
|
||||
function onEditRoleChange(newRole: string) {
|
||||
editForm.value.checkedMenus = [...(ROLE_DEFAULT_MENUS[newRole] || [])];
|
||||
}
|
||||
|
||||
function openEdit(row: StaffRow) {
|
||||
editForm.value = {
|
||||
id: row.id,
|
||||
@@ -124,6 +165,9 @@ function openEdit(row: StaffRow) {
|
||||
status: row.status,
|
||||
roleCode: row.role ?? 'MATCH_ADMIN',
|
||||
password: '',
|
||||
checkedMenus: row.visibleMenus
|
||||
? row.visibleMenus.split(',').filter(Boolean)
|
||||
: [...(ROLE_DEFAULT_MENUS[row.role ?? 'MATCH_ADMIN'] || [])],
|
||||
};
|
||||
editVisible.value = true;
|
||||
}
|
||||
@@ -139,6 +183,7 @@ async function submitEdit() {
|
||||
const payload: Record<string, string> = {
|
||||
status: f.status,
|
||||
roleCode: f.roleCode,
|
||||
visibleMenus: f.checkedMenus.join(','),
|
||||
};
|
||||
if (f.password.trim()) payload.password = f.password.trim();
|
||||
const { data } = await api.patch(`/admin/staff/${f.id}`, payload);
|
||||
@@ -157,6 +202,49 @@ async function submitEdit() {
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleFreeze(row: StaffRow, targetStatus: string) {
|
||||
const actionText = targetStatus === 'SUSPENDED' ? t('common.freeze') : t('common.unfreeze');
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要${actionText}管理员「${row.username}」吗?`,
|
||||
t('msg.freeze_confirm_title', { action: actionText }),
|
||||
{ type: 'warning' },
|
||||
);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await api.patch(`/admin/staff/${row.id}`, { status: targetStatus });
|
||||
ElMessage.success(t('msg.saved'));
|
||||
await load();
|
||||
} catch (e: unknown) {
|
||||
const err = e as { response?: { data?: { error?: string } } };
|
||||
ElMessage.error(err.response?.data?.error ?? t('msg.save_failed'));
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteStaff(row: StaffRow) {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要删除管理员「${row.username}」吗?此操作无法撤销。`,
|
||||
t('common.delete'),
|
||||
{ type: 'warning' },
|
||||
);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await api.delete(`/admin/staff/${row.id}`);
|
||||
ElMessage.success(t('msg.saved'));
|
||||
await load();
|
||||
} catch (e: unknown) {
|
||||
const err = e as { response?: { data?: { error?: string } } };
|
||||
ElMessage.error(err.response?.data?.error ?? t('msg.save_failed'));
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await loadRoles();
|
||||
await load();
|
||||
@@ -191,19 +279,61 @@ onMounted(async () => {
|
||||
<template #empty>
|
||||
<AdminTableEmpty />
|
||||
</template>
|
||||
<el-table-column type="index" :index="(i: number) => (page - 1) * pageSize + i + 1" :label="t('common.seq')" width="70" align="center" />
|
||||
<el-table-column prop="username" :label="t('login.username')" min-width="120" />
|
||||
<el-table-column :label="t('staff.col.role')" min-width="140">
|
||||
<template #default="{ row }">{{ roleLabel(row.role) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('common.status')" width="100">
|
||||
<template #default="{ row }">{{ row.status }}</template>
|
||||
<el-table-column :label="t('common.status')" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
size="small"
|
||||
:type="row.status === 'ACTIVE' ? 'success' : row.status === 'SUSPENDED' ? 'warning' : 'danger'"
|
||||
effect="dark"
|
||||
>
|
||||
{{ t(`user.status.${row.status}`) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('staff.col.last_login')" min-width="160">
|
||||
<template #default="{ row }">{{ formatTime(row.lastLoginAt) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('common.actions')" width="100" align="center">
|
||||
<el-table-column :label="t('common.actions')" width="220" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" size="small" @click="openEdit(row)">{{ t('common.edit') }}</el-button>
|
||||
<el-button link type="primary" size="small" @click="openEdit(row)">
|
||||
{{ t('common.edit') }}
|
||||
</el-button>
|
||||
|
||||
<el-button
|
||||
v-if="row.status === 'ACTIVE'"
|
||||
link
|
||||
type="warning"
|
||||
size="small"
|
||||
:disabled="row.id === currentUserId"
|
||||
@click="toggleFreeze(row, 'SUSPENDED')"
|
||||
>
|
||||
{{ t('common.freeze') }}
|
||||
</el-button>
|
||||
<el-button
|
||||
v-else
|
||||
link
|
||||
type="success"
|
||||
size="small"
|
||||
:disabled="row.id === currentUserId"
|
||||
@click="toggleFreeze(row, 'ACTIVE')"
|
||||
>
|
||||
{{ t('common.unfreeze') }}
|
||||
</el-button>
|
||||
|
||||
<el-button
|
||||
link
|
||||
type="danger"
|
||||
size="small"
|
||||
:disabled="row.id === currentUserId"
|
||||
@click="deleteStaff(row)"
|
||||
>
|
||||
{{ t('common.delete') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -229,7 +359,7 @@ onMounted(async () => {
|
||||
<el-input v-model="createForm.username" autocomplete="off" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('staff.col.role')" required>
|
||||
<el-select v-model="createForm.roleCode" style="width: 100%">
|
||||
<el-select v-model="createForm.roleCode" style="width: 100%" @change="onCreateRoleChange">
|
||||
<el-option
|
||||
v-for="r in roles.filter((x) => x.code !== 'SUPER_ADMIN')"
|
||||
:key="r.code"
|
||||
@@ -238,6 +368,19 @@ onMounted(async () => {
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('staff.field.visible_menus')">
|
||||
<el-checkbox-group v-model="createForm.checkedMenus">
|
||||
<div class="menu-grid">
|
||||
<el-checkbox
|
||||
v-for="item in menuOptions"
|
||||
:key="item.key"
|
||||
:label="item.key"
|
||||
>
|
||||
{{ t(item.label) }}
|
||||
</el-checkbox>
|
||||
</div>
|
||||
</el-checkbox-group>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('login.password')" required>
|
||||
<el-input v-model="createForm.password" type="password" autocomplete="new-password" />
|
||||
</el-form-item>
|
||||
@@ -257,7 +400,7 @@ onMounted(async () => {
|
||||
<el-input :model-value="editForm.username" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('staff.col.role')">
|
||||
<el-select v-model="editForm.roleCode" style="width: 100%" :disabled="editForm.roleCode === 'SUPER_ADMIN'">
|
||||
<el-select v-model="editForm.roleCode" style="width: 100%" :disabled="editForm.roleCode === 'SUPER_ADMIN'" @change="onEditRoleChange">
|
||||
<el-option
|
||||
v-for="r in roles"
|
||||
:key="r.code"
|
||||
@@ -266,11 +409,24 @@ onMounted(async () => {
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('staff.field.visible_menus')">
|
||||
<el-checkbox-group v-model="editForm.checkedMenus">
|
||||
<div class="menu-grid">
|
||||
<el-checkbox
|
||||
v-for="item in menuOptions"
|
||||
:key="item.key"
|
||||
:label="item.key"
|
||||
>
|
||||
{{ t(item.label) }}
|
||||
</el-checkbox>
|
||||
</div>
|
||||
</el-checkbox-group>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('common.status')">
|
||||
<el-select v-model="editForm.status" style="width: 100%">
|
||||
<el-option label="ACTIVE" value="ACTIVE" />
|
||||
<el-option label="SUSPENDED" value="SUSPENDED" />
|
||||
<el-option label="DISABLED" value="DISABLED" />
|
||||
<el-option :label="t('user.status.ACTIVE')" value="ACTIVE" />
|
||||
<el-option :label="t('user.status.SUSPENDED')" value="SUSPENDED" />
|
||||
<el-option :label="t('user.status.DISABLED')" value="DISABLED" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('user.field.reset_password')">
|
||||
@@ -291,4 +447,10 @@ onMounted(async () => {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.menu-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 4px 12px;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user