feat: sync admin local updates

This commit is contained in:
hy 2026-04-21 20:42:21 +08:00
parent edb43e2532
commit 44531d62bb
16 changed files with 1621 additions and 533 deletions

View File

@ -241,9 +241,9 @@ export async function removeSpecialId(data: Record<string, any>) {
return requestClient.post('/user-special-id/remove', data);
}
export async function listSpecialIdLatestLog(account: number | string) {
export async function listSpecialIdLatestLog(userId: number | string) {
return requestClient.get<LegacySpecialIdLogItem[]>('/user-special-id/latest-log', {
params: { account },
params: { userId },
});
}

View File

@ -0,0 +1,28 @@
import { requestClient } from '#/api/request';
export async function pageBaishunGames(params: Record<string, any>) {
return requestClient.get<Record<string, any>>('/go/operate/baishun-game/page', {
params,
});
}
export async function saveBaishunGame(data: Record<string, any>) {
return requestClient.post<Record<string, any>>('/go/operate/baishun-game/save', data);
}
export async function deleteBaishunGame(id: number | string, sysOrigin: string) {
return requestClient.delete<Record<string, any>>('/go/operate/baishun-game', {
params: { id, sysOrigin },
});
}
export async function importBaishunCatalog(data: Record<string, any>) {
return requestClient.post<Record<string, any>>(
'/go/operate/baishun-game/import-catalog',
data,
);
}
export async function syncBaishunGames(data: Record<string, any>) {
return requestClient.post<Record<string, any>>('/go/operate/baishun-game/sync', data);
}

View File

@ -4,6 +4,7 @@ import { requestClient } from '#/api/request';
const SPECIFIED_GIFT_WEEKLY_RANK_BASE =
'/go/resident-activity/specified-gift-weekly-rank';
const SIGN_IN_REWARD_BASE = '/go/resident-activity/sign-in-reward';
export async function getResidentInviteConfig(sysOrigin: string) {
return requestClient.get<Record<string, any>>('/resident-activity/invite/config', {
@ -54,6 +55,22 @@ export async function pageResidentRegisterRewardDailyRecords(
);
}
export async function getResidentSignInRewardConfig(sysOrigin: string) {
return requestClient.get<Record<string, any>>(`${SIGN_IN_REWARD_BASE}/config`, {
params: { sysOrigin },
});
}
export async function saveResidentSignInRewardConfig(data: Record<string, any>) {
return requestClient.post(`${SIGN_IN_REWARD_BASE}/config/save`, data);
}
export async function pageResidentSignInRewardRecords(params: Record<string, any>) {
return requestClient.get<Record<string, any>>(`${SIGN_IN_REWARD_BASE}/record/page`, {
params,
});
}
export async function pageSpecifiedGiftWeeklyRankConfigs(params: Record<string, any>) {
return requestClient.get<LegacyPageResult<Record<string, any>>>(
`${SPECIFIED_GIFT_WEEKLY_RANK_BASE}/config/page`,

View File

@ -16,9 +16,7 @@ const externalRouteFiles = import.meta.glob('./external/**/*.ts', {
const accessRouteFiles = Object.fromEntries(
Object.entries(dynamicRouteFiles).filter(
([path]) =>
!path.endsWith('/dashboard.ts') &&
!path.endsWith('/resident-activity.ts'),
([path]) => !path.endsWith('/dashboard.ts'),
),
);

View File

@ -203,6 +203,12 @@ const routes: RouteRecordRaw[] = [
component: () => import('#/views/operate/game-config.vue'),
meta: { title: '游戏列表' },
},
{
name: 'OperateBaishunGameManage',
path: 'baishun-game-manage',
component: () => import('#/views/operate/baishun-game-manage.vue'),
meta: { title: '百顺游戏管理' },
},
{
name: 'OperateMikeTypeList',
path: 'mike/type/list',

View File

@ -9,8 +9,14 @@ const routes: RouteRecordRaw[] = [
},
name: 'ResidentActivityManager',
path: '/resident-activity/manager',
redirect: '/resident-activity/manager/invite/config',
redirect: '/resident-activity/manager/sign-in-reward/config',
children: [
{
name: 'ResidentSignInRewardConfig',
path: 'sign-in-reward/config',
component: () => import('#/views/resident-activity/sign-in-reward-config.vue'),
meta: { title: '签到奖励配置' },
},
{
name: 'ResidentRegisterRewardConfig',
path: 'register-reward/config',

View File

@ -2,6 +2,8 @@
import { computed, reactive, ref, watch } from 'vue';
import { saveOrUpdateSpecialId } from '#/api/legacy/app-system';
import { getUserBaseInfoBySysOriginAccount } from '#/api/legacy/user';
import AccountInput from '#/components/account-input.vue';
import {
Button,
@ -23,7 +25,7 @@ const props = defineProps<{
const emit = defineEmits<{
close: [];
success: [];
success: [payload: { account: string; sysOrigin: string; userId: string }];
}>();
const loading = ref(false);
@ -37,6 +39,7 @@ const form = reactive<Record<string, any>>({
remark: '',
sysOrigin: '',
userId: '',
userSearch: '',
});
const title = computed(() => (props.initialRecord?.id ? '修改' : '添加'));
@ -50,6 +53,7 @@ function syncForm() {
form.remark = record.remark ?? '';
form.sysOrigin = record.sysOrigin ?? props.sysOrigin;
form.userId = record.userId ?? '';
form.userSearch = record.userId ?? '';
}
watch(
@ -64,7 +68,7 @@ watch(
);
function validateForm() {
if (!String(form.userId || '').trim()) {
if (!String(form.userSearch || '').trim()) {
message.warning('请输入用户ID');
return false;
}
@ -79,21 +83,79 @@ function validateForm() {
return true;
}
function parseEncodedValue(value: unknown) {
const raw = String(value ?? '');
if (!raw) {
return {
accountType: '',
content: '',
sysOrigin: '',
};
}
const match = raw.match(/^\*(.*)\*$/);
if (!match) {
return {
accountType: '',
content: raw,
sysOrigin: '',
};
}
const body = match[1] || '';
return {
accountType: body.match(/accountType:([^,]*)/)?.[1] || '',
content: body.match(/content:(.*)$/)?.[1] || '',
sysOrigin: body.match(/sysOrigin:([^,]*)/)?.[1] || '',
};
}
async function resolveUserId() {
const parsed = parseEncodedValue(form.userSearch);
const content = String(parsed.content || '').trim();
if (!content) {
message.warning('请输入用户ID');
return '';
}
if (!parsed.accountType || parsed.accountType === 'LONG') {
return content.replace(/[^\d]/g, '');
}
const sysOrigin = parsed.sysOrigin || form.sysOrigin || props.sysOrigin;
if (!sysOrigin) {
message.warning('请选择系统');
return '';
}
const result = await getUserBaseInfoBySysOriginAccount(sysOrigin, content);
const userId = result?.id || result?.userId;
if (!userId) {
message.warning('未找到用户');
return '';
}
return String(userId);
}
async function handleSubmit() {
if (!validateForm()) {
return;
}
const resolvedUserId = await resolveUserId();
if (!resolvedUserId) {
return;
}
const payload = {
account: String(form.account || '').trim(),
sysOrigin: form.sysOrigin || props.sysOrigin,
userId: resolvedUserId,
};
loading.value = true;
try {
await saveOrUpdateSpecialId({
...form,
account: String(form.account || '').trim(),
account: payload.account,
remark: String(form.remark || '').trim(),
sysOrigin: form.sysOrigin || props.sysOrigin,
userId: String(form.userId || '').trim(),
sysOrigin: payload.sysOrigin,
userId: payload.userId,
});
message.success('操作成功');
emit('success');
emit('success', payload);
emit('close');
} finally {
loading.value = false;
@ -114,9 +176,10 @@ async function handleSubmit() {
<div class="readonly-value">{{ sysOrigin }}</div>
</FormItem>
<FormItem label="用户">
<Input
v-model:value="form.userId"
<AccountInput
v-model:value="form.userSearch"
:disabled="loading"
:sys-origin="form.sysOrigin || props.sysOrigin"
placeholder="请输入用户ID"
/>
</FormItem>

View File

@ -7,7 +7,7 @@ import { formatDate } from '#/views/system/shared';
import { Drawer, Empty, Spin } from 'antdv-next';
const props = defineProps<{
account: number | string;
userId: number | string;
open: boolean;
}>();
@ -19,14 +19,14 @@ const loading = ref(false);
const list = ref<Array<Record<string, any>>>([]);
watch(
() => ({ account: props.account, open: props.open }),
async ({ account, open }) => {
if (!open || !account) {
() => ({ open: props.open, userId: props.userId }),
async ({ open, userId }) => {
if (!open || !userId) {
return;
}
loading.value = true;
try {
list.value = await listSpecialIdLatestLog(account as number | string);
list.value = await listSpecialIdLatestLog(userId as number | string);
} finally {
loading.value = false;
}
@ -55,7 +55,7 @@ watch(
<div class="time">{{ formatDate(item.createTime) }}</div>
<div class="desc">{{ item.optUserNickname || '-' }}{{ item.eventDesc || '-' }}</div>
<div class="sub">
用户ID {{ item.userBaseInfo?.id || '-' }} / {{ item.userBaseInfo?.userNickname || '-' }}
用户ID {{ item.userBaseInfo?.id || item.userId || '-' }} / {{ item.userBaseInfo?.userNickname || '-' }}
</div>
</div>
</div>

View File

@ -46,6 +46,12 @@ import SpecialIdLogDrawer from './components/special-id-log-drawer.vue';
defineOptions({ name: 'AppSystemSpecialIdManager' });
type SpecialIdFormSuccessPayload = {
account: string;
sysOrigin: string;
userId: string;
};
const router = useRouter();
const accessStore = useAccessStore();
@ -122,33 +128,44 @@ if (!showSettingTab.value && showLogTab.value) {
activeTab.value = 'logs';
}
function getEffectiveSpecialQuery() {
const account = String(settingQuery.account || '').trim();
const userId = String(settingQuery.userId || '').trim();
function getEffectiveSpecialQuery(
query: Partial<typeof settingQuery> = {},
) {
const account = String(query.account ?? settingQuery.account ?? '').trim();
const userId = String(query.userId ?? settingQuery.userId ?? '').trim();
return {
...settingQuery,
account: account.length === 0 && userId.length === 0 ? 'aaaaa' : account,
...query,
account,
userId,
};
}
function getUserInfo(record: LegacySpecialIdFlowItem) {
function getUserInfo(record: Record<string, any>) {
return record.userBaseInfo || {};
}
function getSpecialInfo(record: LegacySpecialIdFlowItem) {
function getSpecialInfo(record: Record<string, any>) {
return record.userSpecialId || {};
}
function openUserDetails(record: LegacySpecialIdFlowItem) {
const userId = getUserInfo(record).id;
function getLogUserId(record: Record<string, any>) {
return getUserInfo(record).id || record.userId || '';
}
function openUserDetails(record: Record<string, any>) {
const userId = getLogUserId(record);
if (!userId) {
return;
}
router.push(`/common/user/deatils/${userId}`);
}
async function loadSettingData(reset = false, append = false) {
async function loadSettingData(
reset = false,
append = false,
query: Partial<typeof settingQuery> = {},
) {
if (reset) {
settingQuery.lastId = '';
list.value = [];
@ -159,7 +176,7 @@ async function loadSettingData(reset = false, append = false) {
loading.value = true;
}
try {
const result = await specialIdFlow(getEffectiveSpecialQuery());
const result = await specialIdFlow(getEffectiveSpecialQuery(query));
const current = result || [];
notData.value = current.length <= 0;
if (!notData.value) {
@ -197,6 +214,13 @@ function openCreate() {
formOpen.value = true;
}
function handleCreateSuccess(payload: SpecialIdFormSuccessPayload) {
settingQuery.account = payload.account;
settingQuery.sysOrigin = payload.sysOrigin;
settingQuery.userId = payload.userId;
loadSettingData(true, false, payload);
}
function openLatestLog(record: LegacySpecialIdFlowItem) {
selectedRow.value = record;
latestLogOpen.value = true;
@ -496,7 +520,7 @@ loadLogData(true);
>
<div class="user-copy">
<span class="user-name">{{ record.userBaseInfo?.userNickname || '-' }}</span>
<span class="user-sub">ID {{ record.userBaseInfo?.id || '-' }}</span>
<span class="user-sub">ID {{ getLogUserId(record as any) || '-' }}</span>
</div>
</button>
</template>
@ -524,11 +548,11 @@ loadLogData(true);
:open="formOpen"
:sys-origin="settingQuery.sysOrigin"
@close="formOpen = false"
@success="loadSettingData(true, false)"
@success="handleCreateSuccess"
/>
<SpecialIdLogDrawer
:account="getSpecialInfo(selectedRow || {}).userId || ''"
:user-id="getSpecialInfo(selectedRow || {}).userId || ''"
:open="latestLogOpen"
@close="latestLogOpen = false"
/>

View File

@ -0,0 +1,470 @@
<script lang="ts" setup>
import { computed, reactive, ref, watch } from 'vue';
import { Page } from '@vben/common-ui';
import { useAccessStore } from '@vben/stores';
import {
deleteBaishunGame,
importBaishunCatalog,
pageBaishunGames,
saveBaishunGame,
syncBaishunGames,
} from '#/api/legacy/baishun-game';
import { getAllowedSysOrigins } from '#/views/system/shared';
import {
Button,
Card,
Form,
FormItem,
Image,
Input,
InputNumber,
Modal,
Pagination,
Select,
Space,
Table,
Tag,
message,
} from 'antdv-next';
import {
GAME_CATEGORY_OPTIONS,
GAME_CLIENT_ORIGIN_OPTIONS,
} from './constants';
defineOptions({ name: 'OperateBaishunGameManage' });
function createForm(sysOrigin = '') {
return {
amounts: '',
category: 'OTHER',
clientOrigin: 'COMMON',
cover: '',
fullScreen: true as any,
gameCode: '',
gameId: '',
gameMode: 3 as any,
gsp: '',
height: 0,
id: '',
launchMode: 'H5_REMOTE',
name: '',
orientation: undefined as any,
packageUrl: '',
packageVersion: '',
previewUrl: '',
regions: '',
safeHeight: 0,
showcase: false as any,
sort: 0,
sysOrigin,
vendorGameId: undefined as any,
width: 0,
currencyIcon: '',
};
}
const accessStore = useAccessStore();
const sysOriginOptions = computed(() => {
const options = getAllowedSysOrigins(accessStore.accessCodes || []);
return options.length > 0 ? options : getAllowedSysOrigins([]);
});
const booleanOptions = [
{ label: '是', value: true as any },
{ label: '否', value: false as any },
];
const showcaseOptions = [
{ label: '全部', value: '' as any },
{ label: '上架', value: 'true' as any },
{ label: '下架', value: 'false' as any },
];
const clientOriginOptions = GAME_CLIENT_ORIGIN_OPTIONS.map((item) => ({
label: item.name,
value: item.value as any,
}));
const categoryOptions = GAME_CATEGORY_OPTIONS.map((item) => ({
label: item.name,
value: item.value as any,
}));
const launchModeOptions = [
{ label: 'H5_REMOTE', value: 'H5_REMOTE' },
{ label: 'H5_ZIP_LOCAL', value: 'H5_ZIP_LOCAL' },
];
const gameModeOptions = [
{ label: '1', value: 1 as any },
{ label: '2 半屏', value: 2 as any },
{ label: '3 默认', value: 3 as any },
];
const orientationOptions = [
{ label: '0', value: 0 as any },
{ label: '1', value: 1 as any },
{ label: '2', value: 2 as any },
];
const query = reactive<Record<string, any>>({
cursor: 1,
keyword: '',
limit: 20,
showcase: '',
sysOrigin: '',
});
const loading = ref(false);
const saving = ref(false);
const importing = ref(false);
const syncing = ref(false);
const total = ref(0);
const modalOpen = ref(false);
const list = ref<Array<Record<string, any>>>([]);
const form = reactive<Record<string, any>>(createForm());
const columns = [
{ dataIndex: 'cover', key: 'cover', title: '封面', width: 90 },
{ dataIndex: 'name', key: 'name', title: '名称', width: 180 },
{ dataIndex: 'gameId', key: 'gameId', title: '内部ID', width: 150 },
{ dataIndex: 'vendorGameId', key: 'vendorGameId', title: '百顺ID', width: 120 },
{ dataIndex: 'showcase', key: 'showcase', title: '状态', width: 100 },
{ dataIndex: 'clientOrigin', key: 'clientOrigin', title: '客户端', width: 100 },
{ dataIndex: 'gameMode', key: 'gameMode', title: 'Game Mode', width: 110 },
{ dataIndex: 'launchMode', key: 'launchMode', title: '启动模式', width: 130 },
{ dataIndex: 'safeHeight', key: 'safeHeight', title: '安全区', width: 110 },
{ dataIndex: 'sort', key: 'sort', title: '排序', width: 90 },
{ dataIndex: 'updateTime', key: 'updateTime', title: '更新时间', width: 180 },
{ dataIndex: 'actions', key: 'actions', title: '操作', width: 160 },
];
watch(
sysOriginOptions,
(options) => {
if (!query.sysOrigin) {
query.sysOrigin = String(options[0]?.value || '');
}
},
{ immediate: true },
);
watch(
() => query.sysOrigin,
(value) => {
if (value) {
void loadData(true);
}
},
{ immediate: true },
);
function resetForm() {
Object.assign(form, createForm(query.sysOrigin));
}
function openCreate() {
resetForm();
modalOpen.value = true;
}
function openEdit(record: Record<string, any>) {
Object.assign(form, createForm(query.sysOrigin), {
...record,
id: String(record.id || ''),
vendorGameId: Number(record.vendorGameId || 0) || undefined,
});
modalOpen.value = true;
}
async function loadData(reset = false) {
if (!query.sysOrigin) {
return;
}
if (reset) {
query.cursor = 1;
}
loading.value = true;
try {
const result = await pageBaishunGames({ ...query });
list.value = Array.isArray(result.records) ? result.records : [];
total.value = Number(result.total || 0);
} finally {
loading.value = false;
}
}
function handleSearch() {
void loadData(true);
}
function handlePageChange(page: number, pageSize: number) {
query.cursor = page;
query.limit = pageSize;
void loadData();
}
async function submitForm() {
if (!form.sysOrigin || !form.name || !form.gameId || !form.vendorGameId) {
message.warning('请补全系统、名称、内部ID和百顺ID');
return;
}
saving.value = true;
try {
await saveBaishunGame({
...form,
id: Number(form.id || 0),
vendorGameId: Number(form.vendorGameId || 0),
});
message.success('保存成功');
modalOpen.value = false;
await loadData(true);
} finally {
saving.value = false;
}
}
function handleDelete(record: Record<string, any>) {
Modal.confirm({
title: '确认删除这个游戏配置吗?',
async onOk() {
await deleteBaishunGame(record.id, record.sysOrigin);
message.success('删除成功');
await loadData(true);
},
});
}
async function handleImportCatalog() {
if (!query.sysOrigin) {
return;
}
importing.value = true;
try {
const result = await importBaishunCatalog({ sysOrigin: query.sysOrigin });
message.success(`导入完成:新增 ${Number(result.imported || 0)},已存在 ${Number(result.existing || 0)}`);
await loadData(true);
} finally {
importing.value = false;
}
}
async function handleSync() {
if (!query.sysOrigin) {
return;
}
syncing.value = true;
try {
const result = await syncBaishunGames({ sysOrigin: query.sysOrigin });
message.success(
`同步完成:目录新增 ${Number(result.inserted || 0)},更新 ${Number(result.updated || 0)},导入 ${Number(result.imported || 0)}`,
);
await loadData(true);
} finally {
syncing.value = false;
}
}
</script>
<template>
<Page title="百顺游戏管理">
<Card>
<div class="toolbar">
<SysOriginSelect
v-model:value="query.sysOrigin"
style="width: 140px"
:options="sysOriginOptions"
/>
<Select
v-model:value="query.showcase"
style="width: 120px"
:options="showcaseOptions"
@change="handleSearch"
/>
<Input
v-model:value="query.keyword"
placeholder="搜索名称 / 内部ID / 百顺ID"
style="width: 240px"
@pressEnter="handleSearch"
/>
<Button type="primary" :loading="loading" @click="handleSearch">
搜索
</Button>
<Button :loading="importing" @click="handleImportCatalog">
导入本地目录
</Button>
<Button :loading="syncing" @click="handleSync">
同步百顺目录
</Button>
<Button type="primary" @click="openCreate">
新增
</Button>
</div>
<Table
:columns="columns"
:data-source="list"
:loading="loading"
:pagination="false"
row-key="id"
:scroll="{ x: 1600 }"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'cover'">
<Image v-if="record.cover" :src="record.cover" class="cover" />
<span v-else>-</span>
</template>
<template v-else-if="column.key === 'showcase'">
<Tag :color="record.showcase ? 'success' : 'default'">
{{ record.showcase ? '上架' : '下架' }}
</Tag>
</template>
<template v-else-if="column.key === 'actions'">
<Space>
<Button size="small" type="link" @click="openEdit(record)">
编辑
</Button>
<Button danger size="small" type="link" @click="handleDelete(record)">
删除
</Button>
</Space>
</template>
</template>
</Table>
<div v-if="total > 0" class="pager">
<Pagination
:current="query.cursor"
:page-size="query.limit"
:total="total"
show-size-changer
@change="handlePageChange"
@showSizeChange="handlePageChange"
/>
</div>
</Card>
<Modal
:confirm-loading="saving"
:open="modalOpen"
destroy-on-close
:title="form.id ? '编辑百顺游戏' : '新增百顺游戏'"
width="860px"
@cancel="modalOpen = false"
@ok="submitForm"
>
<Form layout="vertical">
<div class="form-grid">
<FormItem label="系统">
<SysOriginSelect v-model:value="form.sysOrigin" :options="sysOriginOptions" />
</FormItem>
<FormItem label="名称">
<Input v-model:value="form.name" placeholder="请输入游戏名称" />
</FormItem>
<FormItem label="内部ID">
<Input v-model:value="form.gameId" placeholder="例如 bs_1100" />
</FormItem>
<FormItem label="百顺ID">
<InputNumber v-model:value="form.vendorGameId" :min="1" style="width: 100%" />
</FormItem>
<FormItem label="编号">
<Input v-model:value="form.gameCode" placeholder="默认取百顺ID" />
</FormItem>
<FormItem label="分类">
<Select v-model:value="form.category" :options="categoryOptions" />
</FormItem>
<FormItem label="客户端">
<Select v-model:value="form.clientOrigin" :options="clientOriginOptions" />
</FormItem>
<FormItem label="状态">
<Select v-model:value="form.showcase" :options="booleanOptions" />
</FormItem>
<FormItem label="是否全屏">
<Select v-model:value="form.fullScreen" :options="booleanOptions" />
</FormItem>
<FormItem label="Game Mode">
<Select v-model:value="form.gameMode" :options="gameModeOptions" />
</FormItem>
<FormItem label="启动模式">
<Select v-model:value="form.launchMode" :options="launchModeOptions" />
</FormItem>
<FormItem label="排序">
<InputNumber v-model:value="form.sort" :min="0" style="width: 100%" />
</FormItem>
<FormItem label="安全区高度">
<InputNumber v-model:value="form.safeHeight" :min="0" style="width: 100%" />
</FormItem>
<FormItem label="Orientation">
<Select
v-model:value="form.orientation"
:options="orientationOptions"
allow-clear
/>
</FormItem>
<FormItem label="宽">
<InputNumber v-model:value="form.width" :min="0" style="width: 100%" />
</FormItem>
<FormItem label="高">
<InputNumber v-model:value="form.height" :min="0" style="width: 100%" />
</FormItem>
<FormItem label="GSP">
<Input v-model:value="form.gsp" placeholder="默认取目录或系统配置" />
</FormItem>
<FormItem label="金额">
<Input v-model:value="form.amounts" placeholder="可选" />
</FormItem>
<FormItem label="区域">
<Input v-model:value="form.regions" placeholder="多个区域用逗号分隔" />
</FormItem>
<FormItem label="货币图标">
<Input v-model:value="form.currencyIcon" placeholder="可选" />
</FormItem>
</div>
<FormItem label="封面">
<Input v-model:value="form.cover" placeholder="请输入封面 URL" />
</FormItem>
<FormItem label="预览地址">
<Input v-model:value="form.previewUrl" placeholder="请输入预览地址" />
</FormItem>
<FormItem label="包地址">
<Input v-model:value="form.packageUrl" placeholder="请输入包地址" />
</FormItem>
<FormItem label="包版本">
<Input v-model:value="form.packageVersion" placeholder="请输入包版本" />
</FormItem>
</Form>
</Modal>
</Page>
</template>
<style scoped>
.toolbar {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-bottom: 16px;
}
.pager {
display: flex;
justify-content: flex-end;
margin-top: 16px;
}
.cover {
width: 50px;
height: 50px;
object-fit: cover;
border-radius: 6px;
}
.form-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0 16px;
}
@media (max-width: 900px) {
.form-grid {
grid-template-columns: minmax(0, 1fr);
}
}
</style>

View File

@ -302,6 +302,10 @@ function openFruitConfig() {
router.push('/operate/manager/game/fruit/task-config');
}
function openBaishunManager() {
router.push('/operate/manager/baishun-game-manage');
}
void loadData(true);
</script>
@ -329,6 +333,9 @@ void loadData(true);
<Button type="primary" @click="openCreate">
新增
</Button>
<Button @click="openBaishunManager">
百顺游戏管理
</Button>
<Button @click="openFruitConfig">
摩天轮任务配置
</Button>

View File

@ -48,6 +48,7 @@ const query = reactive<Record<string, any>>({
const form = reactive<Record<string, any>>({
aliasName: '',
countryName: '',
countryCodeAliasesInput: '',
id: '',
nationalFlag: '',
open: false,
@ -59,6 +60,7 @@ const form = reactive<Record<string, any>>({
const columns: any[] = [
{ dataIndex: 'countryName', key: 'countryName', title: '国家名称', width: 200 },
{ dataIndex: 'aliasName', key: 'aliasName', title: '别名', width: 160 },
{ dataIndex: 'countryCodeAliases', key: 'countryCodeAliases', title: '国家码集合', width: 220 },
{ dataIndex: 'nationalFlag', key: 'nationalFlag', title: '国旗', width: 120 },
{ dataIndex: 'open', key: 'open', title: '开放状态', width: 120 },
{ dataIndex: 'phonePrefix', key: 'phonePrefix', title: '手机号码前缀', width: 140 },
@ -94,6 +96,9 @@ function handlePageChange(page: number, pageSize: number) {
function openEdit(record: Record<string, any>) {
form.aliasName = record.aliasName || '';
form.countryName = record.countryName || '';
form.countryCodeAliasesInput = Array.isArray(record.countryCodeAliases)
? record.countryCodeAliases.join(',')
: '';
form.id = record.id || '';
form.nationalFlag = record.nationalFlag || '';
form.open = record.open ?? false;
@ -144,7 +149,15 @@ async function saveForm() {
}
submitLoading.value = true;
try {
await updateSysCountryCode({ ...form });
const payload: Record<string, any> = {
...form,
countryCodeAliases: String(form.countryCodeAliasesInput || '')
.split(',')
.map((item) => item.trim().toUpperCase())
.filter(Boolean),
};
delete payload.countryCodeAliasesInput;
await updateSysCountryCode(payload);
message.success('保存成功');
formOpen.value = false;
await loadData();
@ -198,6 +211,9 @@ loadData(true);
<template v-else-if="column.key === 'nationalFlag'">
<Image :preview="false" :src="record.nationalFlag" class="flag" />
</template>
<template v-else-if="column.key === 'countryCodeAliases'">
{{ Array.isArray(record.countryCodeAliases) ? record.countryCodeAliases.join(', ') : '-' }}
</template>
<template v-else-if="column.key === 'open'">
{{ record.open ? '已开放' : '未开放' }}
</template>
@ -282,6 +298,9 @@ loadData(true);
<FormItem label="别名">
<Input v-model:value="form.aliasName" />
</FormItem>
<FormItem label="国家码集合">
<Input v-model:value="form.countryCodeAliasesInput" placeholder="例如SA,KSA" />
</FormItem>
<FormItem label="手机号前缀">
<Input v-model:value="form.phonePrefix" />
</FormItem>

View File

@ -0,0 +1,574 @@
<script lang="ts" setup>
import { computed, reactive, ref, watch } from 'vue';
import { Page } from '@vben/common-ui';
import { useAccessStore } from '@vben/stores';
import {
getResidentSignInRewardConfig,
pageResidentSignInRewardRecords,
saveResidentSignInRewardConfig,
} from '#/api/legacy/resident-activity';
import ActivityResourceGroupSelectDrawer from '#/views/props/components/activity-resource-group-select-drawer.vue';
import UserProfileLink from '#/views/operate/components/user-profile-link.vue';
import { getAllowedSysOrigins } from '#/views/system/shared';
import {
Button,
Card,
DatePicker,
Input,
Modal,
Pagination,
Select,
Space,
Spin,
Switch,
Table,
Tag,
message,
} from 'antdv-next';
defineOptions({ name: 'ResidentSignInRewardConfigPage' });
function createDay(dayIndex: number) {
return {
dayIndex,
rewardGroupId: null as null | number | string,
rewardGroupName: '',
};
}
function createEmptyForm(sysOrigin = '') {
return {
configured: false,
cycleDays: 7,
enabled: false,
id: 0,
sysOrigin,
timezone: 'Asia/Riyadh',
updateTime: '',
days: Array.from({ length: 7 }, (_, index) => createDay(index + 1)),
};
}
const accessStore = useAccessStore();
const timezoneOptions = [
{ label: '沙特阿拉伯 (Asia/Riyadh)', value: 'Asia/Riyadh' },
{ label: '阿联酋 (Asia/Dubai)', value: 'Asia/Dubai' },
{ label: 'UTC', value: 'UTC' },
{ label: '中国 (Asia/Shanghai)', value: 'Asia/Shanghai' },
{ label: '巴西 (America/Sao_Paulo)', value: 'America/Sao_Paulo' },
];
const sysOriginOptions = computed(() => {
const options = getAllowedSysOrigins(accessStore.accessCodes || []);
return options.length > 0 ? options : getAllowedSysOrigins([]);
});
const loading = ref(false);
const saving = ref(false);
const recordLoading = ref(false);
const recordTotal = ref(0);
const groupPickerOpen = ref(false);
const pickerDayIndex = ref<number>(1);
const recordList = ref<Array<Record<string, any>>>([]);
const dayColumns = [
{ dataIndex: 'dayIndex', key: 'dayIndex', title: '签到日', width: 120 },
{ dataIndex: 'rewardGroup', key: 'rewardGroup', title: '奖励组', width: 420 },
];
const recordColumns = [
{ dataIndex: 'user', key: 'user', title: '签到用户', width: 280 },
{ dataIndex: 'claimDate', key: 'claimDate', title: '签到日期', width: 120 },
{ dataIndex: 'dayIndex', key: 'dayIndex', title: '循环日', width: 100 },
{ dataIndex: 'streakCount', key: 'streakCount', title: '连续天数', width: 100 },
{ dataIndex: 'rewardPayload', key: 'rewardPayload', title: '奖励内容', width: 220 },
{ dataIndex: 'status', key: 'status', title: '状态', width: 120 },
{ dataIndex: 'createTime', key: 'createTime', title: '签到时间', width: 180 },
{ dataIndex: 'errorMessage', key: 'errorMessage', title: '备注 / 失败原因', width: 260 },
];
const form = reactive<Record<string, any>>(createEmptyForm());
const recordQuery = reactive<Record<string, any>>({
claimDate: '',
cursor: 1,
limit: 20,
status: '',
userId: '',
});
const recordSummary = reactive<Record<string, any>>({
date: '',
signCount: 0,
successCount: 0,
timezone: '',
});
const statusMeta = computed(() => {
if (!form.configured) {
return {
color: 'processing',
description: '当前未配置app 调签到奖励接口时会返回不可用。',
text: '未配置',
};
}
if (form.enabled) {
return {
color: 'success',
description: '当前已启用,用户每天签到一次,按 7 天配置循环领奖。',
text: '已启用',
};
}
return {
color: 'default',
description: '当前已关闭app 可读配置但不可签到领奖。',
text: '已关闭',
};
});
function normalizeDays(days: Array<Record<string, any>> | undefined) {
const dayMap = new Map<number, Record<string, any>>();
for (const item of days || []) {
const dayIndex = Number(item.dayIndex || 0);
if (dayIndex >= 1 && dayIndex <= 7) {
dayMap.set(dayIndex, {
dayIndex,
rewardGroupId: item.rewardGroupId ?? null,
rewardGroupName: String(item.rewardGroupName || ''),
});
}
}
return Array.from({ length: 7 }, (_, index) => {
const dayIndex = index + 1;
return dayMap.get(dayIndex) || createDay(dayIndex);
});
}
function applyConfig(result: Record<string, any> | null | undefined) {
const value = result ?? {};
const next = createEmptyForm(
String(value.sysOrigin || form.sysOrigin || sysOriginOptions.value[0]?.value || ''),
);
Object.assign(form, next, {
configured: Boolean(value.configured),
cycleDays: Number(value.cycleDays || 7),
enabled: Boolean(value.enabled),
id: Number(value.id || 0),
sysOrigin: String(value.sysOrigin || next.sysOrigin || ''),
timezone: String(value.timezone || 'Asia/Riyadh'),
updateTime: String(value.updateTime || ''),
});
form.days = normalizeDays(value.days);
}
function syncRecordSummary(result: Record<string, any> | null | undefined) {
const value = result ?? {};
recordSummary.date = String(value.date || recordSummary.date || '');
recordSummary.signCount = Number(value.signCount || 0);
recordSummary.successCount = Number(value.successCount || 0);
recordSummary.timezone = String(value.timezone || recordSummary.timezone || '');
}
async function loadConfig() {
if (!form.sysOrigin) {
return;
}
loading.value = true;
try {
const result = await getResidentSignInRewardConfig(form.sysOrigin);
applyConfig(result);
} finally {
loading.value = false;
}
}
async function loadRecords(reset = false) {
if (!form.sysOrigin) {
recordList.value = [];
recordTotal.value = 0;
return;
}
if (reset) {
recordQuery.cursor = 1;
}
recordLoading.value = true;
try {
const result = await pageResidentSignInRewardRecords({
...recordQuery,
sysOrigin: form.sysOrigin,
});
recordList.value = Array.isArray(result.records) ? result.records : [];
recordTotal.value = Number(result.total || 0);
syncRecordSummary(result);
if (!recordQuery.claimDate && result.date) {
recordQuery.claimDate = String(result.date);
}
} finally {
recordLoading.value = false;
}
}
async function loadData() {
await Promise.all([loadConfig(), loadRecords(true)]);
}
watch(
sysOriginOptions,
(options) => {
if (!form.sysOrigin) {
form.sysOrigin = String(options[0]?.value || '');
}
},
{ immediate: true },
);
watch(
() => form.sysOrigin,
(value) => {
if (value) {
void loadData();
}
},
{ immediate: true },
);
function openRewardPicker(dayIndex: number) {
pickerDayIndex.value = dayIndex;
groupPickerOpen.value = true;
}
function clearRewardGroup(dayIndex: number) {
const target = form.days.find((item: Record<string, any>) => item.dayIndex === dayIndex);
if (!target) {
return;
}
target.rewardGroupId = null;
target.rewardGroupName = '';
}
function handleRewardGroupSelect(row: Record<string, any>) {
const target = form.days.find(
(item: Record<string, any>) => item.dayIndex === pickerDayIndex.value,
);
if (!target) {
return;
}
target.rewardGroupId = row.id;
target.rewardGroupName = row.name;
groupPickerOpen.value = false;
}
async function submitForm() {
saving.value = true;
try {
await saveResidentSignInRewardConfig({
enabled: Boolean(form.enabled),
sysOrigin: form.sysOrigin,
timezone: String(form.timezone || 'Asia/Riyadh').trim(),
days: (form.days || []).map((item: Record<string, any>) => ({
dayIndex: Number(item.dayIndex || 0),
rewardGroupId: item.rewardGroupId ?? null,
rewardGroupName: String(item.rewardGroupName || ''),
})),
});
message.success('保存成功');
await loadConfig();
} finally {
saving.value = false;
}
}
function handleSave() {
Modal.confirm({
async onOk() {
await submitForm();
},
title: '确认保存当前系统的签到奖励配置?',
content: '保存后 app 会立即读取新的 7 天循环签到奖励配置。',
});
}
function recordStatusColor(status: string) {
switch (String(status || '').toUpperCase()) {
case 'SUCCESS':
return 'success';
case 'FAILED':
return 'error';
default:
return 'processing';
}
}
function handleRecordSearch() {
void loadRecords(true);
}
function handleRecordPageChange(page: number, pageSize: number) {
recordQuery.cursor = page;
recordQuery.limit = pageSize;
void loadRecords();
}
</script>
<template>
<Page auto-content-height>
<Spin :spinning="loading">
<Card :bordered="false" title="签到奖励配置">
<Space class="toolbar" wrap>
<Select
v-model:value="form.sysOrigin"
:options="sysOriginOptions"
option-filter-prop="label"
option-label-prop="label"
placeholder="请选择系统"
show-search
style="width: 180px"
/>
<Tag :color="statusMeta.color">
{{ statusMeta.text }}
</Tag>
<span class="status-desc">{{ statusMeta.description }}</span>
</Space>
<div class="section">
<div class="field-label">启用签到奖励</div>
<Switch v-model:checked="form.enabled" />
</div>
<div class="section">
<div class="field-label">签到时区</div>
<Select
v-model:value="form.timezone"
:disabled="saving"
:options="timezoneOptions"
option-filter-prop="label"
option-label-prop="label"
placeholder="请选择时区"
show-search
style="width: 260px"
/>
<span class="field-desc">默认沙特阿拉伯时区按所选时区切自然日</span>
</div>
<div class="section">
<div class="field-label">循环规则</div>
<span class="field-desc">固定 7 天循环用户连续签到满 7 天后从第 1 天奖励重新开始</span>
</div>
<Table
:columns="dayColumns"
:data-source="form.days"
:pagination="false"
row-key="dayIndex"
:scroll="{ x: 760 }"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'dayIndex'">
{{ record.dayIndex }}
</template>
<template v-else-if="column.key === 'rewardGroup'">
<Space wrap>
<Button :disabled="!form.enabled" @click="openRewardPicker(record.dayIndex)">
{{ record.rewardGroupId ? '更换奖励组' : '选择奖励组' }}
</Button>
<Button
v-if="record.rewardGroupId"
:disabled="!form.enabled"
danger
type="link"
@click="clearRewardGroup(record.dayIndex)"
>
清空
</Button>
<span v-if="record.rewardGroupId" class="reward-group">
{{ record.rewardGroupName || '未命名奖励组' }} (ID: {{ record.rewardGroupId }})
</span>
<span v-else class="reward-group empty">未选择奖励组</span>
</Space>
</template>
</template>
</Table>
<div class="tips">
<div>启用后 7 天都必须配置奖励组</div>
<div>签到状态按用户连续签到计算中断一天后下次从第 1 天开始</div>
<div>奖励组明细由 app 侧接口实时读取后台这里保存的是奖励组引用</div>
</div>
<Space class="actions" wrap>
<Button :loading="saving" type="primary" @click="handleSave">
保存配置
</Button>
<span v-if="form.updateTime" class="field-desc">最后更新时间{{ form.updateTime }}</span>
</Space>
</Card>
<Card :bordered="false" class="record-card" title="签到记录">
<div class="toolbar">
<Space wrap>
<DatePicker
v-model:value="recordQuery.claimDate"
placeholder="签到日期"
style="width: 160px"
value-format="YYYY-MM-DD"
/>
<Input
v-model:value="recordQuery.userId"
allow-clear
placeholder="用户ID"
style="width: 160px"
/>
<Select
v-model:value="recordQuery.status"
allow-clear
:options="[
{ label: '成功', value: 'SUCCESS' },
{ label: '失败', value: 'FAILED' },
{ label: '处理中', value: 'PENDING' },
]"
placeholder="状态"
style="width: 140px"
/>
<Button :loading="recordLoading" type="primary" @click="handleRecordSearch">
查询
</Button>
</Space>
</div>
<div class="summary">
<Tag color="processing">
统计日期{{ recordSummary.date || '-' }}
</Tag>
<Tag color="purple">
时区{{ recordSummary.timezone || '-' }}
</Tag>
<Tag color="orange">
当日签到数{{ recordSummary.signCount }}
</Tag>
<Tag color="success">
发奖成功数{{ recordSummary.successCount }}
</Tag>
</div>
<Table
:columns="recordColumns"
:data-source="recordList"
:loading="recordLoading"
:pagination="false"
row-key="id"
:scroll="{ x: 1400 }"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'user'">
<UserProfileLink
:profile="{
account: record.account,
countryCode: record.countryCode,
countryName: record.countryName,
id: record.userId,
userAvatar: record.userAvatar,
userNickname: record.userNickname,
}"
/>
</template>
<template v-else-if="column.key === 'dayIndex'">
{{ record.dayIndex || 0 }}
</template>
<template v-else-if="column.key === 'rewardPayload'">
<div>奖励组{{ record.rewardGroupName || record.rewardGroupId || '-' }}</div>
</template>
<template v-else-if="column.key === 'status'">
<Tag :color="recordStatusColor(record.status)">
{{ record.status || '-' }}
</Tag>
</template>
<template v-else-if="column.key === 'errorMessage'">
{{ record.errorMessage || '-' }}
</template>
</template>
</Table>
<div v-if="recordTotal > 0" class="pager">
<Pagination
:current="recordQuery.cursor"
:page-size="recordQuery.limit"
:total="recordTotal"
show-size-changer
@change="handleRecordPageChange"
@showSizeChange="handleRecordPageChange"
/>
</div>
</Card>
<ActivityResourceGroupSelectDrawer
:open="groupPickerOpen"
:sys-origin="form.sysOrigin"
@close="groupPickerOpen = false"
@select="handleRewardGroupSelect"
/>
</Spin>
</Page>
</template>
<style scoped>
.toolbar {
margin-bottom: 16px;
}
.status-desc {
color: rgb(100 116 139);
}
.section {
align-items: center;
display: flex;
gap: 16px;
margin-bottom: 20px;
}
.field-label {
color: rgb(15 23 42);
flex: 0 0 120px;
font-weight: 600;
}
.field-desc {
color: rgb(100 116 139);
}
.reward-group {
color: rgb(15 23 42);
}
.reward-group.empty {
color: rgb(148 163 184);
}
.tips {
background: rgb(248 250 252);
border-radius: 8px;
color: rgb(71 85 105);
line-height: 1.8;
margin-top: 16px;
padding: 16px;
}
.actions {
margin-top: 20px;
}
.record-card {
margin-top: 16px;
}
.summary {
margin-bottom: 16px;
}
.pager {
display: flex;
justify-content: flex-end;
margin-top: 16px;
}
</style>

View File

@ -0,0 +1,375 @@
INSERT INTO sys_menu (
id,
parent_id,
menu_name,
path,
menu_type,
icon,
create_user,
update_user,
sort,
status,
router,
alias
)
SELECT
1700000251,
0,
'常驻活动',
'/resident-activity/manager',
1,
'example',
'0',
'0',
195,
0,
NULL,
'ResidentActivityManager'
WHERE NOT EXISTS (
SELECT 1 FROM sys_menu WHERE alias = 'ResidentActivityManager'
);
UPDATE sys_menu
SET
parent_id = 0,
menu_name = '常驻活动',
path = '/resident-activity/manager',
menu_type = 1,
icon = 'example',
create_user = '0',
update_user = '0',
sort = 195,
status = 0,
router = NULL
WHERE alias = 'ResidentActivityManager';
INSERT INTO sys_menu (
id,
parent_id,
menu_name,
path,
menu_type,
icon,
create_user,
update_user,
sort,
status,
router,
alias
)
SELECT
1700000252,
(SELECT id FROM sys_menu WHERE alias = 'ResidentActivityManager' LIMIT 1),
'签到奖励配置',
'resident-activity/sign-in-reward-config/index',
2,
'form',
'0',
'0',
1,
0,
'sign-in-reward/config',
'ResidentSignInRewardConfig'
WHERE NOT EXISTS (
SELECT 1 FROM sys_menu WHERE alias = 'ResidentSignInRewardConfig'
);
UPDATE sys_menu
SET
parent_id = (
SELECT resident_menu.id
FROM (
SELECT id
FROM sys_menu
WHERE alias = 'ResidentActivityManager'
LIMIT 1
) AS resident_menu
),
menu_name = '签到奖励配置',
path = 'resident-activity/sign-in-reward-config/index',
menu_type = 2,
icon = 'form',
create_user = '0',
update_user = '0',
sort = 1,
status = 0,
router = 'sign-in-reward/config'
WHERE alias = 'ResidentSignInRewardConfig';
INSERT INTO sys_menu (
id,
parent_id,
menu_name,
path,
menu_type,
icon,
create_user,
update_user,
sort,
status,
router,
alias
)
SELECT
1700000253,
(SELECT id FROM sys_menu WHERE alias = 'ResidentActivityManager' LIMIT 1),
'注册奖励配置',
'resident-activity/register-reward-config/index',
2,
'form',
'0',
'0',
2,
0,
'register-reward/config',
'ResidentRegisterRewardConfig'
WHERE NOT EXISTS (
SELECT 1 FROM sys_menu WHERE alias = 'ResidentRegisterRewardConfig'
);
UPDATE sys_menu
SET
parent_id = (
SELECT resident_menu.id
FROM (
SELECT id
FROM sys_menu
WHERE alias = 'ResidentActivityManager'
LIMIT 1
) AS resident_menu
),
menu_name = '注册奖励配置',
path = 'resident-activity/register-reward-config/index',
menu_type = 2,
icon = 'form',
create_user = '0',
update_user = '0',
sort = 2,
status = 0,
router = 'register-reward/config'
WHERE alias = 'ResidentRegisterRewardConfig';
INSERT INTO sys_menu (
id,
parent_id,
menu_name,
path,
menu_type,
icon,
create_user,
update_user,
sort,
status,
router,
alias
)
SELECT
1700000254,
(SELECT id FROM sys_menu WHERE alias = 'ResidentActivityManager' LIMIT 1),
'邀请活动配置',
'resident-activity/invite-config/index',
2,
'form',
'0',
'0',
3,
0,
'invite/config',
'ResidentInviteConfig'
WHERE NOT EXISTS (
SELECT 1 FROM sys_menu WHERE alias = 'ResidentInviteConfig'
);
UPDATE sys_menu
SET
parent_id = (
SELECT resident_menu.id
FROM (
SELECT id
FROM sys_menu
WHERE alias = 'ResidentActivityManager'
LIMIT 1
) AS resident_menu
),
menu_name = '邀请活动配置',
path = 'resident-activity/invite-config/index',
menu_type = 2,
icon = 'form',
create_user = '0',
update_user = '0',
sort = 3,
status = 0,
router = 'invite/config'
WHERE alias = 'ResidentInviteConfig';
INSERT INTO sys_menu (
id,
parent_id,
menu_name,
path,
menu_type,
icon,
create_user,
update_user,
sort,
status,
router,
alias
)
SELECT
1700000255,
(SELECT id FROM sys_menu WHERE alias = 'ResidentActivityManager' LIMIT 1),
'邀请活动列表',
'resident-activity/invite-list/index',
2,
'form',
'0',
'0',
4,
0,
'invite/list',
'ResidentInviteList'
WHERE NOT EXISTS (
SELECT 1 FROM sys_menu WHERE alias = 'ResidentInviteList'
);
UPDATE sys_menu
SET
parent_id = (
SELECT resident_menu.id
FROM (
SELECT id
FROM sys_menu
WHERE alias = 'ResidentActivityManager'
LIMIT 1
) AS resident_menu
),
menu_name = '邀请活动列表',
path = 'resident-activity/invite-list/index',
menu_type = 2,
icon = 'form',
create_user = '0',
update_user = '0',
sort = 4,
status = 0,
router = 'invite/list'
WHERE alias = 'ResidentInviteList';
INSERT INTO sys_menu (
id,
parent_id,
menu_name,
path,
menu_type,
icon,
create_user,
update_user,
sort,
status,
router,
alias
)
SELECT
1700000256,
(SELECT id FROM sys_menu WHERE alias = 'ResidentActivityManager' LIMIT 1),
'指定礼物周榜',
'resident-activity/week-star/index',
2,
'form',
'0',
'0',
5,
0,
'specified-gift-weekly-rank',
'ResidentSpecifiedGiftWeeklyRank'
WHERE NOT EXISTS (
SELECT 1 FROM sys_menu WHERE alias = 'ResidentSpecifiedGiftWeeklyRank'
);
UPDATE sys_menu
SET
parent_id = (
SELECT resident_menu.id
FROM (
SELECT id
FROM sys_menu
WHERE alias = 'ResidentActivityManager'
LIMIT 1
) AS resident_menu
),
menu_name = '指定礼物周榜',
path = 'resident-activity/week-star/index',
menu_type = 2,
icon = 'form',
create_user = '0',
update_user = '0',
sort = 5,
status = 0,
router = 'specified-gift-weekly-rank'
WHERE alias = 'ResidentSpecifiedGiftWeeklyRank';
INSERT INTO sys_menu (
id,
parent_id,
menu_name,
path,
menu_type,
icon,
create_user,
update_user,
sort,
status,
router,
alias
)
SELECT
1700000257,
1700000057,
'百顺游戏管理',
'sys/game-config/baishun/index',
2,
'form',
'0',
'0',
133,
0,
'baishun-game-manage',
'OperateBaishunGameManage'
WHERE NOT EXISTS (
SELECT 1 FROM sys_menu WHERE alias = 'OperateBaishunGameManage'
);
UPDATE sys_menu
SET
parent_id = 1700000057,
menu_name = '百顺游戏管理',
path = 'sys/game-config/baishun/index',
menu_type = 2,
icon = 'form',
create_user = '0',
update_user = '0',
sort = 133,
status = 0,
router = 'baishun-game-manage'
WHERE alias = 'OperateBaishunGameManage';
INSERT INTO sys_role_menu (role_id, menu_id)
SELECT DISTINCT
parent_role.role_id,
target_menu.id
FROM sys_role_menu AS parent_role
JOIN sys_menu AS target_menu
ON target_menu.alias IN (
'ResidentActivityManager',
'ResidentSignInRewardConfig',
'ResidentRegisterRewardConfig',
'ResidentInviteConfig',
'ResidentInviteList',
'ResidentSpecifiedGiftWeeklyRank',
'OperateBaishunGameManage'
)
LEFT JOIN sys_role_menu AS existing_role_menu
ON existing_role_menu.role_id = parent_role.role_id
AND existing_role_menu.menu_id = target_menu.id
WHERE parent_role.menu_id = 1700000057
AND existing_role_menu.id IS NULL;

View File

@ -1,209 +0,0 @@
# webconsole UI 重构清单
## 页面文案约束
- 页面内禁止出现“重构、迁移、兼容、旧系统、承接页、占位、开发中”等与使用者无关的描述
- 页面只保留业务信息、操作说明、状态提示
- 本规则适用于当前页面与后续所有开发
## 统计结果
- 基础路由5
- 业务路由144
- 合计路由页149
## 2026-03-31 筛选下拉审计
- 审计范围:`Frontend/apps/src/router/routes/modules` 对应的全部路由页,以及 [重构清单.md](/Users/zuozuo/Desktop/HY/AppPlatform/Frontend/重构清单.md) 中登记的 `149` 个页面范围
- 路由组件实扫:`147` 个实际 `.vue` 路由页
- 初始高风险页:`56`
- 本轮批量修复:`50` 个路由页首轮收敛,剩余 `9` 个手工补齐
- 当前残留:`0` 个“筛选区仍使用 `SelectOption` 且可能出现无文案回显”的路由页
- 当前新增脚本:[fix-route-filter-selects.mjs](/Users/zuozuo/Desktop/HY/AppPlatform/Frontend/apps/scripts/fix-route-filter-selects.mjs#L1)
- 本轮重点补齐:`用户银行流水` 的“类型/事件”以及运营管理、系统管理、团队管理、动态管理、App 系统管理里剩余的筛选下拉文案回显
- 本轮追加补齐:`用户银行卡提现现金申请``用户银行卡提现钻石申请` 的“卡片类型 / 类型 / 状态”,以及路由页里剩余的“类型”类下拉回显
## 基础路由5项
- [x] 登录页 | 路由: `/login`
- [x] 404页 | 路由: `/404`
- [x] 401页 | 路由: `/401`
- [x] 首页 | 路由: `/dashboard`
- [x] 用户详情 | 路由: `/common/user/deatils/:id`
## 团队管理8项
- [x] 团队政策 | 路由: `/team/team-policy`
- [x] 团队钻石政策 | 路由: `/team/team-diamond-policy`
- [x] 团队列表 | 路由: `/team/team-list`
- [x] 成员列表 | 路由: `/team/team/member`
- [x] 账单列表 | 路由: `/team/team/bill`
- [x] 成员工作 | 路由: `/team/team/member-work`
- [x] 成员审核日志 | 路由: `/team/team/application-process`
- [x] BD Lead | 路由: `/team/team/bd-lead`
## 动态管理4项
- [x] 标签列表 | 路由: `/dynamic/manager/dynamic/tag`
- [x] 权重配置 | 路由: `/dynamic/manager/dynamic/popular/config`
- [x] 用户动态 | 路由: `/dynamic/manager/user/dynamic/list`
- [x] 黑名单 | 路由: `/dynamic/manager/dynamic/blacklist`
## 家族管理2项
- [x] 家族配置 | 路由: `/family/manager/family_config`
- [x] 家族列表 | 路由: `/family/manager/family_list`
## 宠物管理4项
- [x] 宠物喂养记录 | 路由: `/pet/manager/feeding`
- [x] 豆子账户 | 路由: `/pet/manager/beans`
- [x] 豆子流水 | 路由: `/pet/manager/beans/running-water`
- [x] 宠物池 | 路由: `/pet/manager/pool`
## 道具管理5项
- [x] 道具赠送 | 路由: `/props/manager/props/send_tool`
- [x] 资源配置 | 路由: `/props/manager/props/config`
- [x] 道具资源组配置 | 路由: `/props/manager/props/props_source_group`
- [x] 活动道具规则配置 | 路由: `/props/manager/props/props-activity-rule-config`
- [x] 道具商店 | 路由: `/props/manager/props/props_store`
## 工具管理5项
- [x] 请求黑名单 | 路由: `/app/tools/request-blacklist`
- [x] 金币分析 | 路由: `/app/tools/gold-analyze`
- [x] Redis缓存管理 | 路由: `/redis`
- [x] 文件上传 | 路由: `/upload/file`
- [x] Socket测试 | 路由: `/socket`
## App系统管理13项
- [x] 登陆日志 | 路由: `/app/sys/mamange/login/logger`
- [x] 房间黑名单 | 路由: `/app/sys/mamange/log/blacklist`
- [x] 请求日志 | 路由: `/app/sys/mamange/log/api-request-log`
- [x] 靓号管理 A | 路由: `/app/sys/mamange/userSpecialId`
- [x] 靓号管理 B | 路由: `/app/sys/mamange/userSpecialId`
- [x] 文档管理 | 路由: `/app/sys/mamange/external/app_doc_interface`
- [x] IM通讯 | 路由: `/app/sys/mamange/external/im`
- [x] 版本管理 | 路由: `/app/sys/mamange/version`
- [x] 参数配置管理 | 路由: `/app/sys/mamange/enum/config`
- [x] 参数配置 | 路由: `/app/sys/mamange/enum/setting`
- [x] 内购产品配置V2 | 路由: `/app/sys/mamange/config/product/v2`
- [x] 封禁设备 | 路由: `/app/sys/mamange/archive/device`
- [x] 表情管理 | 路由: `/app/sys/mamange/emoji/config`
## 系统管理5项
- [x] 区域配置 | 路由: `/sys/manager/region/config`
- [x] 用户管理 | 路由: `/sys/manager/user`
- [x] 角色管理 | 路由: `/sys/manager/roles`
- [x] 菜单管理 | 路由: `/sys/manager/menus`
- [x] 资源管理 | 路由: `/sys/manager/resource`
## 统计管理1项
- [x] 数据大屏 | 路由: `/statistics/manager/datav`
## 运营管理82项
- [x] 代理活动 | 路由: `/operate/manager/agentActivity`
- [x] 金币余额 | 路由: `/operate/manager/gold-balance`
- [x] 自动发放工资凭据 | 路由: `/auto-salary-pay-record`
- [x] 用户银行卡提现现金申请 | 路由: `/withdraw-money-apply-page`
- [x] 用户银行卡提现钻石申请 | 路由: `/withdraw-diamond-apply-page`
- [x] 用户银行金币兑换申请 | 路由: `/user-bank-withdraw-gold-apply`
- [x] 用户银行卡钻石兑换金币申请 | 路由: `/user-bank-diamond-withdraw-gold-apply`
- [x] 用户银行账户 | 路由: `/user-bank-balance`
- [x] 用户银行流水 | 路由: `/user-running-water`
- [x] 客服列表 | 路由: `/customer-service`
- [x] CP申请 | 路由: `/operate/manager/cp`
- [x] PK记录 | 路由: `/game/pk`
- [x] 邀请用户日志 | 路由: `/operate/manager/invite-user-log`
- [x] 活动配置 | 路由: `/operate/manager/activity/cnf`
- [x] 活动模版 | 路由: `/operate/manager/activity/template`
- [x] 用户友谊卡配置 | 路由: `/sys/friendship-card`
- [x] 邀请用户配置 | 路由: `/sys/invite-user-rule`
- [x] 用户观看广告配置 | 路由: `/sys/user-advertising`
- [x] Ludo飞行棋 | 路由: `/game-ludo`
- [x] 置顶房间 | 路由: `/operate/manager/set-top-room`
- [x] 热门房间 | 路由: `/operate/manager/set-hot-room`
- [x] 退款扣除目标 | 路由: `/operate/manager/refund-deduct-tag-log`
- [x] 开通支付国家 A | 路由: `/operate/manager/pay/country`
- [x] 支付区域关系 | 路由: `/operate/manager/region/relation`
- [x] 应用管理 | 路由: `/operate/manager/pay/application`
- [x] 支付厂商 | 路由: `/operate/manager/pay/factory`
- [x] 支付渠道 | 路由: `/operate/manager/pay/channel`
- [x] 开通支付国家 B | 路由: `/operate/manager/open_pay/country`
- [x] 抽奖品配置 | 路由: `/operate/manager/cnf/lottery`
- [x] 货运代理 | 路由: `/operate/manager/user_freight`
- [x] BD列表 | 路由: `/operate/manager/room/business-development`
- [x] 邀请用户奖励记录 | 路由: `/operate/manager/user/invite-reward`
- [x] IM账号管理 | 路由: `/operate/manager/im/account/manager`
- [x] 用户签到 | 路由: `/operate/manager/check/in/log`
- [x] 用户列表 A与 B 合并为同页双标签) | 路由: `/operate/manager/user`
- [x] 用户列表 B与 A 合并为同页双标签) | 路由: `/operate/manager/user`
- [x] 用户最新设备列表 | 路由: `/operate/manager/user/user-device`
- [x] APP管理员 | 路由: `/operate/manager/administrator`
- [x] 订单补偿 | 路由: `/operate/manager/compensate`
- [x] 小工具 | 路由: `/operate/manager/tools/other`
- [x] 异常订单 | 路由: `/operate/manager/abnormal/log`
- [x] 货运代理流水 | 路由: `/operate/manager/user_freight_info_list`
- [x] 高产用户列表 | 路由: `/operate/manager/quality_users_list`
- [x] 金币收支记录 | 路由: `/operate/manager/running/water`
- [x] 内部操作金币 | 路由: `/operate/manager/inside-operation-gold`
- [x] 游戏券收支记录 | 路由: `/game/coupon/running/water`
- [x] 积分收支记录 | 路由: `/operate/manager/integral/stream`
- [x] 订单详情 | 路由: `/operate/manager/order/details`
- [x] 退款记录 | 路由: `/operate/manager/reimburse`
- [x] 礼物管理 | 路由: `/operate/manager/gift`
- [x] 抽奖概率配置 | 路由: `/game/lottery-config`
- [x] 徽章配置管理 | 路由: `/operate/manager/badge`
- [x] 靓号池管理 | 路由: `/operate/manager/beautiful-number`
- [x] 礼物赠送记录 | 路由: `/operate/manager/gift/history`
- [x] 房间贡献余额 | 路由: `/operate/manager/contribution/balance`
- [x] 消息推送 | 路由: `/operate/manager/push`
- [x] 在线房间 | 路由: `/operate/manager/online/room`
- [x] 房间资料信息 | 路由: `/operate/manager/profile/room`
- [x] 系统banner管理 | 路由: `/operate/manager/sys/banner`
- [x] 麦位类型管理 | 路由: `/operate/manager/mike/type`
- [x] 游戏列表 | 路由: `/operate/manager/game-config`
- [x] App启动页 | 路由: `/operate/manager/sys/start/page`
- [x] 系统公告管理 | 路由: `/operate/manager/notice/message`
- [x] 国家管理 | 路由: `/operate/manager/sys/country`
- [x] 用户道具流水 | 路由: `/running/water/user/props`
- [x] 用户钻石余额列表 | 路由: `/user/diamond-balance`
- [x] 用户钻石流水列表 | 路由: `/user/diamond-run-water`
- [x] 购买麦位类型流水 | 路由: `/operate/manager/mike/type/list`
- [x] 红包发送列表 | 路由: `/game/red/packet/record`
- [x] 用户活动领奖记录 | 路由: `/user/user-activity-receive`
- [x] 幸运礼物规则配置 | 路由: `/game/lucky-gift/rule-config/config`
- [x] 幸运礼物规格配置 | 路由: `/game/lucky-gift/standard-config/config`
- [x] 幸运礼物送礼记录 | 路由: `/game/lucky-gift/give-record`
- [x] LuckyBox抽奖记录 | 路由: `/game/lucky-box`
- [x] 摩天轮游戏每日数据 | 路由: `/game/fruit/log/fruit-days-log`
- [x] 摩天轮任务配置 | 路由: `/operate/manager/game/fruit/task-config`
- [x] 砸金蛋记录 | 路由: `/game/egg`
- [x] 炸金花 | 路由: `/game/teen-patti`
- [x] 用户申请靓号记录 | 路由: `/user/beautiful-number-apply`
- [x] 自定义名人堂 | 路由: `/activity/hall-fame`
- [x] 喇叭列表 | 路由: `/game/trumpet`
- [x] 房间支持活动 | 路由: `/activity/room/contribution`
## 审批管理15项
- [x] 头像审核 | 路由: `/approval/manager/user/avatatphoto/approval`
- [x] 昵称审核 | 路由: `/approval/manager/user/nickname/approval`
- [x] 照片墙审核 | 路由: `/approval/manager/user/photowall/approval`
- [x] 房间封面审批 | 路由: `/approval/manager/room/cover/approval`
- [x] 用户个性签名审批 | 路由: `/approval/manager/user/profile_desc/approval`
- [x] 房间通知审批 | 路由: `/approval/manager/room/notice/approval`
- [x] 房间名称审批 | 路由: `/approval/manager/room/nickanme/approval`
- [x] 房间主题审批 | 路由: `/approval/manager/room/theme/approval`
- [x] 举报管理 | 路由: `/approval/manager/report`
- [x] 意见反馈 | 路由: `/approval/manager/feedback`
- [x] 违规历史记录 | 路由: `/approval/manager/not_pass_history/table`
- [x] 家族头像审批 | 路由: `/approval/manager/family/approval`
- [x] 动态内容审批 | 路由: `/approval/manager/dynamic/content/approval`
- [x] 动态举报审批 | 路由: `/approval/manager/dynamic/report`
- [x] 用户银行卡审批 | 路由: `/approval/manager/user/bank-card/approval`

View File

@ -1,290 +0,0 @@
# webconsole UI 重构进度
## 当前总览
- 承载项目:`Frontend/`
- 来源系统:`项目交接文件/webconsole_前端后管系统`
- 启动日期2026-03-30
- 当前阶段:全部 149 个路由已完成;运营管理与统计管理均已按旧项目真实路由、接口和交互完成对齐
- 设计基线:`Frontend/apps``vue-vben-admin 5.7` 风格
- 重构范围:仅 UI 层
- 总路由页149
- 基础路由5
- 业务路由144
- 已完成149
- 进行中0
- 未开始0
- 最近更新2026-03-31
## 完成标准
- 页面布局、筛选区、工具栏、表格、分页、详情抽屉、编辑弹窗统一到 `Frontend/apps` 风格
- 仅调整 UI 与交互呈现,不改接口字段、不改业务逻辑、不改权限点
- 保持来源系统的 fullPath、菜单权限、接口调用兼容
- 一个路由页只有在其直接依赖的抽屉 / 弹窗 / 编辑页样式同步完成后,才可记为“完成”
## 模块进度
| 模块 | 路由数 | 已完成 | 状态 | 备注 |
| --- | ---: | ---: | --- | --- |
| 基础路由 | 5 | 5 | 已完成 | 已完成旧路径兼容与 Frontend 风格承接 |
| 团队管理 | 8 | 8 | 已完成 | 已对齐团队政策、团队钻石政策、团队列表、账单列表、成员列表、成员工作、成员审核日志、BD Lead |
| 动态管理 | 4 | 4 | 已完成 | 已对齐标签、权重、用户动态、黑名单 |
| 家族管理 | 2 | 2 | 已完成 | 已对齐家族配置三标签页、家族列表、成员弹层与解散/移除操作 |
| 宠物管理 | 4 | 4 | 已完成 | 已对齐喂养记录、豆子账户、豆子流水、宠物池 |
| 道具管理 | 5 | 5 | 已完成 | 已对齐赠送、资源配置、资源组、活动规则、商店 |
| 工具管理 | 5 | 5 | 已完成 | 已对齐请求黑名单、金币分析、Redis缓存管理、文件上传、Socket测试 |
| App系统管理 | 13 | 13 | 已完成 | 已对齐登陆日志、房间黑名单、请求日志、靓号管理、文档管理、IM通讯、版本管理、参数配置管理、参数配置、内购产品配置V2、封禁设备、表情管理 |
| 系统管理 | 5 | 5 | 已完成 | 已对齐区域配置、用户、角色、菜单、资源的真实接口与操作 |
| 统计管理 | 1 | 1 | 已完成 | 已对齐数据大屏的购买用户滚动列表、今日付费预览、Likei 在线统计与双击全屏交互 |
| 运营管理 | 82 | 82 | 已完成 | 已对齐银行相关、客服/邀请/广告/友谊卡、CP申请、PK记录、房间运营、支付配置、应用配置、工具与奖励记录、活动配置与活动模版、抽奖品配置、用户列表、礼物管理、抽奖概率配置、徽章配置管理、房间资料信息、砸金蛋以及管理员/国家/麦位/在线房间、货运代理、订单与礼物记录、IM账号、金币/游戏券流水、靓号池管理、游戏列表、房间贡献余额、消息推送、红包发送、幸运礼物、LuckyBox、炸金花、自定义名人堂、BD列表、摩天轮每日数据与任务配置旧项目重复 `/operate/manager/user` 已按同页双标签收口 |
| 审批管理 | 15 | 15 | 已完成 | 已按旧项目真实接口、筛选项、批量审批与弹层动作对齐 |
## 建议重构顺序
1. 基础路由
2. 审批管理
3. 系统管理 + App系统管理 + 工具管理
4. 团队管理 + 动态管理 + 家族管理 + 宠物管理 + 道具管理
5. 运营管理
## 当前风险
- 存在重复完整路由:
- `/app/sys/mamange/userSpecialId`
- `/operate/manager/user` 已按同页双标签承载两个旧页面
- 存在历史拼写路由,迁移阶段暂不修正:
- `/app/sys/mamange`
- `/approval/manager/user/avatatphoto/approval`
- `/approval/manager/room/nickanme/approval`
- `/common/user/deatils/:id`
- 存在绝对路径业务页,不属于父模块前缀下的相对路由,迁移时必须按真实 fullPath 保持兼容
## 更新规则
- 每次完成一个或多个路由页后,必须同步更新:
- “当前总览”中的已完成 / 进行中 / 未开始数量
- “模块进度”中的对应模块完成数与状态
- “更新日志”中的本次完成内容
## 更新日志
### 2026-03-30
- 已将重构承载项目切换到 `Frontend/`
- 已在 `Frontend/` 初始化设计上下文、重构清单和进度文档
- 已添加基础路由兼容模块 `apps/src/router/routes/modules/legacy-core.ts`
- 已完成登录页 `/login` 的 Frontend 登录页收敛,关闭多余演示入口
- 已确认 `/401` `/404` 复用 Frontend 异常页完成承接
- 已完成旧首页 `/dashboard` 的 Frontend 承接页 `apps/src/views/legacy/dashboard/index.vue`
- 已完成旧用户详情 `/common/user/deatils/:id` 的详情页 UI 重构
- 已清理重复 `/dashboard` 路由定义,并将默认首页切换到 `/dashboard`
- 已新增审批路由模块 `apps/src/router/routes/modules/approval.ts`
- 已将 `Frontend/apps` 请求层切换到旧项目 `/console` 地址、旧响应结构与旧鉴权头
- 已将审批管理 15 个路由页改为旧项目真实接口、真实筛选参数与真实批量审批逻辑
- 已补齐审批页依赖的编辑用户、账号处理、反馈处理弹层
- 已将旧用户详情 `/common/user/deatils/:id` 改为真实接口数据页
- 已将登录页改为用户名 / 密码表单,去除额外登录字段,保持旧项目校验规则与跳转方式
- 已将登录链路调整为先 `/account/login` 获取 token再由路由守卫按旧顺序拉取 `/account/info`、按钮权限与菜单
- 已补充登录初始化失败时的清理与回登录页逻辑,避免 `info` 请求异常后残留无效登录态
- 已新增系统管理路由模块 `apps/src/router/routes/modules/system.ts`
- 已完成区域配置 `/sys/manager/region/config` 的区域列表与辅助配置双标签页
- 已完成用户管理 `/sys/manager/user` 的查询、添加、修改、状态切换与重置密码
- 已完成角色管理 `/sys/manager/roles` 的查询、添加、修改、删除与菜单授权
- 已完成菜单管理 `/sys/manager/menus` 的菜单树、节点增改删与资源绑定
- 已完成资源管理 `/sys/manager/resource` 的查询与刷新资源
- 已新增 App系统管理路由模块 `apps/src/router/routes/modules/app-system.ts`
- 已完成登陆日志 `/app/sys/mamange/login/logger` 的查询、分页、违规记录、账号处理记录与账号处理
- 已完成房间黑名单 `/app/sys/mamange/log/blacklist` 的系统筛选、按 `lastId` 加载更多与房间详情查看
- 已完成请求日志 `/app/sys/mamange/log/api-request-log` 的查询筛选与列表展示
- 已完成重复路由 `/app/sys/mamange/userSpecialId` 的靓号管理页,对齐管理列表、操作日志、添加、改号、改有效期、自定义属性与删除
- 已完成文档管理 `/app/sys/mamange/external/app_doc_interface` 与 IM通讯 `/app/sys/mamange/external/im` 的外链页接入
- 已完成版本管理 `/app/sys/mamange/version` 的系统筛选、平台渠道切换、版本增改删、服务强更与多语言更新描述
- 已完成参数配置管理 `/app/sys/mamange/enum/config` 的系统筛选、分组筛选、增改删与数据类型校验
- 已完成参数配置 `/app/sys/mamange/enum/setting` 的分组切换、布尔开关编辑、普通参数编辑与拖拽排序保存
- 已完成内购产品配置V2 `/app/sys/mamange/config/product/v2` 的系统筛选、平台/区域筛选与新增修改
- 已完成封禁设备 `/app/sys/mamange/archive/device` 的查询、分页与解封操作
- 已完成表情管理 `/app/sys/mamange/emoji/config` 的分组/资源双标签、上下架切换、OSS上传与新增修改
- 已新增工具管理路由模块 `apps/src/router/routes/modules/tools.ts`
- 已完成请求黑名单 `/app/tools/request-blacklist` 的黑名单查询、新增与删除
- 已完成金币分析 `/app/tools/gold-analyze` 的时间区间、系统来源、用户维度图表分析
- 已完成 Redis缓存管理 `/redis` 的缓存查询入口与结果展示
- 已完成文件上传 `/upload/file` 的拖拽上传、多文件上传、原文件名切换、按文件类型分桶与链接复制
- 已完成 Socket测试 `/socket` 的广播订阅、点对点订阅、消息发送、仓库写入与连接状态展示
- 已新增动态管理路由模块 `apps/src/router/routes/modules/dynamic.ts`
- 已完成标签列表 `/dynamic/manager/dynamic/tag` 的系统/状态/语言筛选、标签新增修改与背景图上传
- 已完成权重配置 `/dynamic/manager/dynamic/popular/config` 的热门权重、等级限制、数量限制与金币配置修改
- 已完成用户动态 `/dynamic/manager/user/dynamic/list` 的系统/置顶/时间/用户筛选、批量删除、置顶编辑、取消置顶、账号处理与用户详情跳转
- 已完成黑名单 `/dynamic/manager/dynamic/blacklist` 的系统/用户筛选、加入黑名单、删除与用户详情跳转
- 已新增团队管理路由模块 `apps/src/router/routes/modules/team.ts`
- 已完成成员审核日志 `/team/team/application-process` 的系统/类型/原因/申请人筛选与按 `lastId` 加载更多
- 已完成 BD Lead `/team/team/bd-lead` 的系统/用户筛选、分页、创建、编辑、删除与绑定子级BD
- 已完成成员工作 `/team/team/member-work` 的系统/账单/代理/成员/时间筛选、按 `lastId` 加载更多、礼物流水查询与详情抽屉
- 已补齐成员工作依赖的礼物流水弹层与礼物详情抽屉
- 已补齐团队模块通用账号输入组件,对齐旧项目 `account-input` 的账号编码筛选逻辑
- 已将成员审核日志、BD Lead、成员工作切换为旧项目账号筛选参数格式
- 已完成成员列表 `/team/team/member` 的系统/角色/代理/成员/时间筛选、批量删除、添加成员、更换团队、修改备注与重置目标
- 已补齐成员列表依赖的添加成员、更换团队、备注编辑、目标重置弹层
- 已完成团队列表 `/team/team-list` 的系统/区域/状态/代理/成员数量/时间筛选、按 `lastId` 加载更多、批量删除与权限判断
- 已补齐团队列表依赖的创建代理、编辑资料、备注、联系方式、资料详情、发送通知、通知记录、团队申请、绑定BD弹层
- 已补齐团队列表所需的国家选择、团队通知、团队详情与申请记录真实接口
- 已完成账单列表 `/team/team/bill` 的系统/区域/账单状态/代理/账单ID/时间筛选、按 `lastId` 加载更多与导出条件抽屉
- 已补齐账单详情抽屉与账单导出下载逻辑,保持旧项目 `/team/bill/list``/team/bill/excel` 接口口径
- 当前 UI 重构完成数53 / 149
### 2026-03-31
- 已完成团队政策 `/team/team-policy` 的系统/区域/国家筛选、当前发布政策查看、历史政策列表、创建政策、保存、发布、删除与道具奖励编辑
- 已完成团队钻石政策 `/team/team-diamond-policy` 的系统/区域筛选、当前发布政策查看、历史钻石政策列表、创建政策、保存、发布、删除与道具奖励编辑
- 已补齐团队政策依赖的礼物、徽章、道具、支付国家与历史政策真实接口封装
- 已新增团队政策共享奖励编辑抽屉,对齐旧项目的资源类型、数量/天数、描述、拖拽排序与保存口径
- 已新增宠物管理路由模块 `apps/src/router/routes/modules/pet.ts`
- 已完成宠物喂养记录 `/pet/manager/feeding` 的系统/用户/宠物筛选、分页与用户详情跳转
- 已完成豆子账户 `/pet/manager/beans` 的用户筛选、余额列表、赠送/扣除弹窗与流水弹窗
- 已完成豆子流水 `/pet/manager/beans/running-water` 的系统/类型/来源/用户/时间筛选,并支持余额页内嵌复用
- 已完成宠物池 `/pet/manager/pool` 的系统/上架筛选、上下架切换、新增编辑、阶段配置、解锁条件、奖励组选择与资源上传
- 已补齐宠物池依赖的奖励展示、奖励组选择抽屉与豆子操作弹窗,保持旧项目 `/pet_pool/*``/user-bean-balance/*``/props/activity/reward/group/page` 接口口径
- 当前 UI 重构完成数59 / 149
- 已新增运营管理路由模块 `apps/src/router/routes/modules/operate.ts`
- 已完成金币余额 `/operate/manager/gold-balance` 的系统筛选、用户筛选、金币余额列表与按 `lastId` 加载更多
- 已完成自动发放工资凭据 `/auto-salary-pay-record` 的系统筛选、代理/成员筛选、分页列表、详情、政策详情与备注抽屉
- 已完成用户银行账户 `/user-bank-balance` 的系统/用户筛选、余额列表、创建账户、发送金额、扣除金额、转账、导入与单用户流水查看
- 已完成用户银行流水 `/user-running-water` 的系统/类型/事件/用户/时间筛选、总额统计与按 `lastId` 加载更多
- 已完成用户银行卡提现现金申请 `/withdraw-money-apply-page` 与用户银行卡提现钻石申请 `/withdraw-diamond-apply-page` 的系统/卡类型/用户/状态/时间筛选、详情、审核与导出条件抽屉
- 已完成用户银行金币兑换申请 `/user-bank-withdraw-gold-apply` 与用户银行卡钻石兑换金币申请 `/user-bank-diamond-withdraw-gold-apply` 的系统/提交用户/接收用户/接收方式/时间筛选与按 `lastId` 加载更多
- 已补齐运营管理首批页面依赖的用户银行、提现、兑换金币、自动工资凭据真实接口封装与共享弹层
- 已新增道具管理路由模块 `apps/src/router/routes/modules/props.ts`
- 已完成道具赠送 `/props/manager/props/send_tool` 的道具、徽章、道具券三类赠送表单,对齐旧项目权限点、资源加载与无效短账号反馈
- 已完成资源配置 `/props/manager/props/config` 的系统/类型/状态筛选、上下架切换、新增修改、销售报表与销售概览
- 已完成道具资源组配置 `/props/manager/props/props_source_group` 的系统/状态筛选、资源组编辑、奖励项排序与上下架切换
- 已完成活动道具规则配置 `/props/manager/props/props-activity-rule-config` 的系统筛选、规则新增修改删除、资源组关联与特殊规则 JSON 配置
- 已完成道具商店 `/props/manager/props/props_store` 的系统/类型/状态/付费类型筛选、商品新增修改与上下架切换
- 已补齐道具管理依赖的资源上传、销量统计、首充奖励、游戏配置与商店真实接口封装,保持旧项目 `/props/source/record/*``/props/activity/*``/props/store/*``/count/operation/active-index/*` 接口口径
- 当前 UI 重构完成数72 / 149
- 已完成客服列表 `/customer-service` 的系统/区域筛选、客服列表、新增、修改、删除与区域/时间段配置
- 已完成邀请用户日志 `/operate/manager/invite-user-log` 的系统/国家/邀请用户/被邀请用户/时间筛选与分页
- 已完成用户友谊卡配置 `/sys/friendship-card` 的系统筛选、分页列表、新增修改、图片上传、上架状态与卡片金额配置
- 已完成邀请用户配置 `/sys/invite-user-rule` 的 Lotfun 邀请有效天数与佣金比例配置
- 已完成用户观看广告配置 `/sys/user-advertising` 的 Aswat 广告开关、每日上限、间隔秒数与金币奖励配置
- 已补齐以上页面依赖的邀请用户、客服、友谊卡、邀请规则、广告配置真实接口封装
- 当前 UI 重构完成数77 / 149
- 已完成 Ludo飞行棋 `/game-ludo` 的运行中/历史记录切换、房间搜索、参与用户弹窗、房间详情与强制解散操作
- 已完成置顶房间 `/operate/manager/set-top-room` 的系统/区域/房间筛选、创建修改、权重配置、有效期与房间详情
- 已完成热门房间 `/operate/manager/set-hot-room` 的系统/房间筛选、创建修改、有效期与房间详情
- 已完成退款扣除目标 `/operate/manager/refund-deduct-tag-log` 的系统/主播/退款人筛选与分页展示
- 已补齐房间搜索、系统区域选择、Ludo、置顶/热门房间真实接口封装与复用弹层
- 当前 UI 重构完成数81 / 149
- 已完成 CP申请 `/operate/manager/cp` 的状态、发送用户、接收用户筛选与分页展示
- 已完成 PK记录 `/game/pk` 的房间PK/团队PK双标签、系统筛选、房间搜索、加载更多与房间详情
- 已完成邀请用户奖励记录 `/operate/manager/user/invite-reward` 的用户/时间筛选与分页展示
- 已完成用户签到 `/operate/manager/check/in/log` 的用户/时间筛选、签到奖励展示与分页
- 已完成用户钻石余额列表 `/user/diamond-balance` 的用户筛选、余额分页与用户资料跳转
- 已完成用户钻石流水列表 `/user/diamond-run-water` 的系统/用户/类型/来源筛选与分页展示
- 已完成用户活动领奖记录 `/user/user-activity-receive` 的用户/时间筛选与分页展示
- 已完成喇叭列表 `/game/trumpet` 的系统/状态/用户筛选、按 `lastId` 加载更多、删除与账号处理
- 已补齐 CP、PK、邀请奖励、签到、钻石余额/流水、活动领奖、喇叭列表真实接口封装与房间PK子面板
- 当前 UI 重构完成数89 / 149
- 已完成开通支付国家 `/operate/manager/pay/country``/operate/manager/open_pay/country` 的系统筛选、分页列表、批量新增国家、汇率/币种/排序更新、上下架切换与国家关联渠道维护
- 已完成支付区域关系 `/operate/manager/region/relation` 的系统筛选、区域关系分页、新增、修改与支付国家关联
- 已完成支付厂商 `/operate/manager/pay/factory` 的分页列表、图标上传、新增修改与关联渠道配置
- 已完成支付渠道 `/operate/manager/pay/channel` 的分页列表、图标上传、新增修改与渠道类型管理
- 已补齐支付配置组依赖的支付国家、区域关系、厂商、渠道、国家渠道明细真实接口封装与共享弹层
- 当前 UI 重构完成数94 / 149
- 已完成应用管理 `/operate/manager/pay/application` 的应用卡片列表、支付链接复制、应用新增修改,以及商品管理 / 商品管理V2 双标签配置
- 已完成用户最新设备列表 `/operate/manager/user/user-device` 的系统/用户/设备号/时间筛选、分页展示、设备号复制与用户详情跳转
- 已完成订单补偿 `/operate/manager/compensate` 的订单补偿表单、糖果补偿表单与确认提交流程
- 已完成小工具 `/operate/manager/tools/other` 的用户账户调换、API参数签名、Alias Method分析、令牌解析、Redis清Key、用户房间区域变动与注销用户恢复
- 已完成异常订单 `/operate/manager/abnormal/log` 的系统/状态/平台/时间筛选、异常订单分页、单据复制与状态变更
- 已补齐以上页面依赖的支付应用、支付商品、设备列表、订单异常、补偿订单与工具类真实接口封装
- 当前 UI 重构完成数99 / 149
- 已完成 APP管理员 `/operate/manager/administrator` 的管理员列表、状态切换、角色分配、权限分配、移除管理,以及权限资源列表的增改删
- 已完成 国家管理 `/operate/manager/sys/country` 的国家列表、国家简称筛选、图标上传、置顶与启用状态切换、编辑保存
- 已完成 麦位类型管理 `/operate/manager/mike/type` 的系统/收费/状态筛选、麦位图与图标上传、新增修改与删除
- 已完成 购买麦位类型流水 `/operate/manager/mike/type/list` 的系统/用户/麦位类型/到期类型筛选与分页展示
- 已完成 在线房间 `/operate/manager/online/room` 的系统/区域/房间筛选、在线房间卡片列表、在线成员抽屉、账号处理与房间详情
- 已完成 房间支持活动 `/activity/room/contribution` 的房间账号筛选、分页列表、房间详情与用户资料跳转
- 已补齐以上页面依赖的管理员、国家、麦位类型、在线房间、房间支持活动真实接口封装与共享弹层
- 当前 UI 重构完成数105 / 149
- 已完成 代理活动 `/operate/manager/agentActivity` 的周/月类型筛选、区域筛选与分页列表展示
- 已完成 用户道具流水 `/running/water/user/props` 的来源搜索、道具ID、购买人/接收人、时间筛选与分页展示
- 已完成 用户申请靓号记录 `/user/beautiful-number-apply` 的系统/用户筛选、同意/驳回处理与分页列表
- 已完成 高产用户列表 `/operate/manager/quality_users_list` 的总预览、每月预览、备注、本月充值明细与每月明细弹层
- 已完成 积分收支记录 `/operate/manager/integral/stream` 的用户/时间/来源筛选、30天提示与分页展示
- 已新增统计管理路由模块 `apps/src/router/routes/modules/statistics.ts`
- 已完成 数据大屏 `/statistics/manager/datav` 的深色大屏布局、购买用户滚动列表、今日付费预览、Likei 在线统计与双击全屏交互
- 已补齐数据大屏依赖的 `/datav/*` 与今日付费统计真实接口封装,并复用旧项目头图资源 `office_efficiency_header_bg.png`
- 当前 UI 重构完成数149 / 149
- 已完成 活动配置 `/operate/manager/activity/cnf` 的活动图片列表、新增修改、图片上传、删除与活动链接复制
- 已完成 系统banner管理 `/operate/manager/sys/banner` 的系统/平台/状态/区域/国家筛选、三图上传、新增修改与内容跳转配置
- 已完成 App启动页 `/operate/manager/sys/start/page` 的系统/状态/类型筛选、启动页列表、新增修改、图片上传与删除
- 已完成 系统公告管理 `/operate/manager/notice/message` 的系统/状态/发布时间筛选、公告新增修改、封面上传、发布与上下架切换
- 已补齐以上页面依赖的代理活动、活动图片、启动页、系统公告、Banner、靓号申请、高产用户、积分流水、用户道具流水真实接口封装与共享弹层
- 当前 UI 重构完成数114 / 149
- 已完成 货运代理 `/operate/manager/user_freight` 的系统/显示/经销商/状态/用户筛选、账户列表、状态切换、发货、扣款、流水与授权卖家管理
- 已完成 IM账号管理 `/operate/manager/im/account/manager` 的用户搜索、账号新增、删除与重置密码
- 已完成 货运代理流水 `/operate/manager/user_freight_info_list` 的系统/发送用户/接收用户/来源/时间筛选、分页与导出
- 已完成 金币收支记录 `/operate/manager/running/water` 的多系统筛选、来源搜索、用户/跟踪ID/记录ID/时间筛选与按 context 加载更多
- 已完成 内部操作金币 `/operate/manager/inside-operation-gold` 的用户收支 / 货运流水双标签承接
- 已完成 游戏券收支记录 `/game/coupon/running/water` 的多系统筛选、平台/类型/来源/用户/时间筛选与分页
- 已完成 订单详情 `/operate/manager/order/details` 的多系统筛选、支付平台/状态/订单类型/用户/订单号/地区/时间筛选、统计汇总与详情抽屉
- 已完成 退款记录 `/operate/manager/reimburse` 的多系统筛选、来源平台/用户/时间筛选、退款列表与详情抽屉
- 已完成 礼物赠送记录 `/operate/manager/gift/history` 的系统/发送用户/接收用户/礼物ID/时间筛选、总价值统计、接收数量统计、详情抽屉与房间详情跳转
- 已完成 靓号池管理 `/operate/manager/beautiful-number` 的状态/靓号/分组筛选、列表、新增、修改与删除
- 已完成 游戏列表 `/operate/manager/game-config` 的系统/游戏源/状态筛选、房间游戏列表、新增修改、封面上传、删除与摩天轮数据配置跳转
- 已补齐以上页面依赖的货运代理、货运流水、IM账号、游戏券流水、金币流水、订单明细、礼物记录、靓号池与游戏列表真实接口封装与详情抽屉
- 当前 UI 重构完成数125 / 149
- 已完成 活动配置 `/operate/manager/activity/cnf` 的活动列表、活动新增修改、活动复制、排行榜查看、奖品发送记录与活动链接复制,并按原项目 `template-cnf` 结构接回真实活动接口
- 已完成 活动模版 `/operate/manager/activity/template` 的模版列表、名称编辑、模版复制,以及模版 `indexTree` JSON 编辑保存
- 已完成 抽奖品配置 `/operate/manager/cnf/lottery` 的按系统加载/保存完整转盘 `body` 配置结构
- 已完成 用户列表 `/operate/manager/user` 的旧 `user-table``user-table-role` 双页面同路由收口,保留双标签页、用户资料、账号处理、认证信息、重置密码与金币/钻石/游戏券操作
- 已完成 礼物管理 `/operate/manager/gift` 的礼物配置、周星礼物配置、CP组合赠送礼物三标签页以及礼物新增修改、区域/规格/特效/资源上传与上下架切换
- 已完成 抽奖概率配置 `/game/lottery-config` 的分组列表与完整奖励组 JSON 编辑保存
- 已完成 徽章配置管理 `/operate/manager/badge` 的规则、赠送/收回三标签页,以及徽章规则新增修改、资源上传、赠送与收回操作
- 已完成 房间资料信息 `/operate/manager/profile/room` 的房间资料列表、成员/访客/日志抽屉、房间编辑、审批与黑名单/权限操作
- 已完成 砸金蛋 `/game/egg` 的碎片兑换、抽奖记录、配置三标签页,以及抽取率、奖池和奖励配置维护
- 已完成 运营管理收尾路由接入,修正 `/operate/manager/activity/cnf` 历史错配,并补齐 `/operate/manager/activity/template``/operate/manager/cnf/lottery``/operate/manager/gift``/operate/manager/badge``/operate/manager/profile/room``/game/lottery-config``/game/egg`
- 当前 UI 重构完成数148 / 149
### 2026-03-31
- 已新增家族管理路由模块 `apps/src/router/routes/modules/family.ts`
- 已完成 家族配置 `/family/manager/family_config` 的家族等级配置、创建家族规则、创建每周奖励规则三标签页承接
- 已完成 家族配置依赖的等级列表、创建规则、奖励规则真实接口封装,以及头像框/徽章/礼物选项和背景图上传
- 已完成 家族列表 `/family/manager/family_list` 的系统/族长/家族账号/时间筛选、家族列表分页、成员列表弹层、解散家族与移除成员操作
- 已补齐家族列表与家族成员真实接口封装,保持旧项目筛选参数与动作接口一致
- 当前 UI 重构完成数127 / 149
- 已完成 房间贡献余额 `/operate/manager/contribution/balance` 的总预览/时间段预览双标签、房间搜索、贡献余额展示与房间详情抽屉
- 已完成 消息推送 `/operate/manager/push` 的新建Push、Push日志、推送任务三标签页以及任务新增修改删除
- 已完成 自定义名人堂 `/activity/hall-fame` 的名人堂列表、多语言文案、增改删与语言描述维护
- 已完成 红包发送列表 `/game/red/packet/record` 的系统/类型/房间/用户/时间筛选、按 `lastId` 加载更多与详情抽屉
- 已完成 幸运礼物规则配置 `/game/lucky-gift/rule-config/config` 的系统切换与大中小奖区间规则保存
- 已完成 幸运礼物送礼记录 `/game/lucky-gift/give-record` 的系统/数量/礼物/用户/房间/时间筛选、统计汇总与分页展示
- 已完成 LuckyBox抽奖记录 `/game/lucky-box` 的系统/用户/时间筛选、奖池/盈亏摘要、比例调整与详情抽屉
- 已完成 炸金花 `/game/teen-patti` 的记录/配置双标签、开奖记录、下注用户抽屉与配置保存
- 已完成 BD列表 `/operate/manager/room/business-development` 的 BD Lead 页面承接,沿用当前已对齐的系统/用户筛选、创建、编辑、删除与子级BD绑定逻辑
- 已完成 幸运礼物规格配置 `/game/lucky-gift/standard-config/config` 的规格列表、增改删与 1/7/77/177/777 概率配置抽屉
- 已完成 摩天轮游戏每日数据 `/game/fruit/log/fruit-days-log` 的每日数据/用户数据双标签、服务端排序、汇总信息与用户筛选
- 已完成 摩天轮任务配置 `/operate/manager/game/fruit/task-config` 的赏金任务列表、任务配置抽屉、奖项图片加载、资源组选择与资源组配置入口
- 已核对发现此前记录为完成的 活动配置 `/operate/manager/activity/cnf` 实际仍指向旧项目不对应页面,已从完成统计回退,待按原项目 `views/activity/template-cnf/index` 重新重构
- 当前 UI 重构完成数138 / 149
## 单次更新模板
### YYYY-MM-DD
- 完成模块:
- 完成路由:
- 累计完成:
- 当前进行中:
- 风险 / 阻塞: