feat: optimize admin operate modules

This commit is contained in:
hy 2026-04-18 17:20:23 +08:00
parent 2ef56428dc
commit 1462c296af
10 changed files with 368 additions and 127 deletions

View File

@ -13,6 +13,7 @@ import { useAccessStore } from '@vben/stores';
import { message } from 'antdv-next'; import { message } from 'antdv-next';
import { getDevApiBaseURL } from '#/config/dev-api-switch';
import { useAuthStore } from '#/store'; import { useAuthStore } from '#/store';
const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD); const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD);
@ -48,6 +49,10 @@ function createRequestClient(baseURL: string, options?: RequestClientOptions) {
// 请求头处理 // 请求头处理
client.addRequestInterceptor({ client.addRequestInterceptor({
fulfilled: async (config) => { fulfilled: async (config) => {
const devBaseURL = getDevApiBaseURL(String(config.url ?? ''));
if (devBaseURL) {
config.baseURL = devBaseURL;
}
const accessStore = useAccessStore(); const accessStore = useAccessStore();
const accessToken = accessStore.accessToken; const accessToken = accessStore.accessToken;
if (accessToken) { if (accessToken) {

View File

@ -0,0 +1,66 @@
<script lang="ts" setup>
import { ref } from 'vue';
import { Select, message } from 'antdv-next';
import {
DEV_API_OPTIONS,
getDevApiTargetLabel,
getStoredDevApiTarget,
isDevApiSwitchEnabled,
normalizeDevApiTarget,
type DevApiTarget,
setStoredDevApiTarget,
} from '#/config/dev-api-switch';
defineOptions({
name: 'DevApiSwitch',
});
const enabled = isDevApiSwitchEnabled();
const currentTarget = ref<DevApiTarget>(getStoredDevApiTarget());
function handleChange(value: string) {
const nextTarget = normalizeDevApiTarget(value);
if (nextTarget === currentTarget.value) {
return;
}
currentTarget.value = nextTarget;
setStoredDevApiTarget(nextTarget);
message.success(`已切换到${getDevApiTargetLabel(nextTarget)},后续请求将使用对应环境`);
}
</script>
<template>
<div v-if="enabled" class="dev-api-switch">
<span class="dev-api-switch__label">API</span>
<Select
:value="currentTarget"
:options="DEV_API_OPTIONS"
class="dev-api-switch__select"
option-label-prop="label"
size="small"
@change="handleChange"
/>
</div>
</template>
<style scoped>
.dev-api-switch {
align-items: center;
display: flex;
gap: 8px;
height: 100%;
}
.dev-api-switch__label {
color: var(--vben-layout-header-text-color, rgb(15 23 42 / 0.72));
font-size: 12px;
font-weight: 600;
letter-spacing: 0.08em;
}
.dev-api-switch__select {
width: 128px;
}
</style>

View File

@ -0,0 +1,79 @@
export const DEV_API_SWITCH_STORAGE_KEY = '__chatapp3_admin_dev_api_target__';
export const DEV_API_TARGETS = {
internal: 'internal',
prod: 'prod',
} as const;
export type DevApiTarget =
(typeof DEV_API_TARGETS)[keyof typeof DEV_API_TARGETS];
export const DEV_API_OPTIONS: Array<{
label: string;
value: DevApiTarget;
}> = [
{
label: '内网 API',
value: DEV_API_TARGETS.internal,
},
{
label: '线上 API',
value: DEV_API_TARGETS.prod,
},
];
const DEV_API_PREFIX_MAP: Record<DevApiTarget, string> = {
[DEV_API_TARGETS.internal]: '/__dev_api_internal__',
[DEV_API_TARGETS.prod]: '/__dev_api_prod__',
};
function canUseStorage() {
return typeof window !== 'undefined' && typeof window.localStorage !== 'undefined';
}
export function isDevApiSwitchEnabled() {
return import.meta.env.DEV;
}
export function normalizeDevApiTarget(value: null | string | undefined): DevApiTarget {
return value === DEV_API_TARGETS.prod
? DEV_API_TARGETS.prod
: DEV_API_TARGETS.internal;
}
export function getStoredDevApiTarget(): DevApiTarget {
if (!isDevApiSwitchEnabled() || !canUseStorage()) {
return DEV_API_TARGETS.internal;
}
return normalizeDevApiTarget(
window.localStorage.getItem(DEV_API_SWITCH_STORAGE_KEY),
);
}
export function setStoredDevApiTarget(target: DevApiTarget) {
if (!isDevApiSwitchEnabled() || !canUseStorage()) {
return;
}
window.localStorage.setItem(
DEV_API_SWITCH_STORAGE_KEY,
normalizeDevApiTarget(target),
);
}
export function getDevApiTargetLabel(target: DevApiTarget) {
return (
DEV_API_OPTIONS.find((item) => item.value === target)?.label ||
'内网 API'
);
}
export function getDevApiBaseURL(url?: string) {
if (!isDevApiSwitchEnabled()) {
return null;
}
const normalizedUrl = String(url || '');
if (/^https?:\/\//.test(normalizedUrl)) {
return null;
}
return DEV_API_PREFIX_MAP[getStoredDevApiTarget()];
}

View File

@ -17,8 +17,10 @@ import { useAccessStore, useUserStore } from '@vben/stores';
import { $t } from '#/locales'; import { $t } from '#/locales';
import { useAuthStore } from '#/store'; import { useAuthStore } from '#/store';
import DevApiSwitch from '#/components/dev-api-switch.vue';
import LoginForm from '#/views/_core/authentication/login.vue'; import LoginForm from '#/views/_core/authentication/login.vue';
const isDev = import.meta.env.DEV;
const notifications = ref<NotificationItem[]>([ const notifications = ref<NotificationItem[]>([
{ {
id: 1, id: 1,
@ -141,6 +143,9 @@ watch(
<template> <template>
<BasicLayout @clear-preferences-and-logout="handleLogout"> <BasicLayout @clear-preferences-and-logout="handleLogout">
<template v-if="isDev" #header-right-55>
<DevApiSwitch />
</template>
<template #user-dropdown> <template #user-dropdown>
<UserDropdown <UserDropdown
:avatar :avatar

View File

@ -53,72 +53,18 @@ const routes: RouteRecordRaw[] = [
component: () => import('#/views/operate/ludo-game.vue'), component: () => import('#/views/operate/ludo-game.vue'),
meta: { title: 'Ludo飞行棋' }, meta: { title: 'Ludo飞行棋' },
}, },
{
name: 'SetTop',
path: 'set-top-room',
component: () => import('#/views/operate/set-top-room.vue'),
meta: { title: '置顶房间' },
},
{
name: 'SetHot',
path: 'set-hot-room',
component: () => import('#/views/operate/set-hot-room.vue'),
meta: { title: '热门房间' },
},
{ {
name: 'RefundDeductTagLog', name: 'RefundDeductTagLog',
path: 'refund-deduct-tag-log', path: 'refund-deduct-tag-log',
component: () => import('#/views/operate/refund-deduct-tag-log.vue'), component: () => import('#/views/operate/refund-deduct-tag-log.vue'),
meta: { title: '退款扣除目标' }, meta: { title: '退款扣除目标' },
}, },
{
name: 'PayCountry',
path: 'pay/country',
component: () => import('#/views/operate/pay-country.vue'),
meta: { title: '开通支付国家' },
},
{
name: 'RegionRelation',
path: 'region/relation',
component: () => import('#/views/operate/region-relation.vue'),
meta: { title: '支付区域关系' },
},
{
name: 'PayApplication',
path: 'pay/application',
component: () => import('#/views/operate/pay-application.vue'),
meta: { title: '应用管理' },
},
{
name: 'PayFactory',
path: 'pay/factory',
component: () => import('#/views/operate/pay-factory.vue'),
meta: { title: '支付厂商' },
},
{
name: 'PayChannel',
path: 'pay/channel',
component: () => import('#/views/operate/pay-channel.vue'),
meta: { title: '支付渠道' },
},
{
name: 'OpenPayCountry',
path: 'open_pay/country',
component: () => import('#/views/operate/pay-country.vue'),
meta: { title: '开通支付国家' },
},
{ {
name: 'OperateCnfLottery', name: 'OperateCnfLottery',
path: 'cnf/lottery', path: 'cnf/lottery',
component: () => import('#/views/operate/lottery-wheel-config.vue'), component: () => import('#/views/operate/lottery-wheel-config.vue'),
meta: { title: '抽奖品配置' }, meta: { title: '抽奖品配置' },
}, },
{
name: 'OperateBusinessDevelopment',
path: 'room/business-development',
component: () => import('#/views/operate/business-development.vue'),
meta: { title: 'BD列表' },
},
{ {
name: 'OperateAdministrator', name: 'OperateAdministrator',
path: 'administrator', path: 'administrator',
@ -215,30 +161,12 @@ const routes: RouteRecordRaw[] = [
component: () => import('#/views/operate/teen-patti.vue'), component: () => import('#/views/operate/teen-patti.vue'),
meta: { title: '炸金花' }, meta: { title: '炸金花' },
}, },
{
name: 'OperateContributionBalance',
path: 'contribution/balance',
component: () => import('#/views/operate/contribution-balance.vue'),
meta: { title: '房间贡献余额' },
},
{ {
name: 'OperatePush', name: 'OperatePush',
path: 'push', path: 'push',
component: () => import('#/views/operate/push.vue'), component: () => import('#/views/operate/push.vue'),
meta: { title: '消息推送' }, meta: { title: '消息推送' },
}, },
{
name: 'OperateOnlineRoom',
path: 'online/room',
component: () => import('#/views/operate/online-room.vue'),
meta: { title: '在线房间' },
},
{
name: 'OperateRoomProfile',
path: 'profile/room',
component: () => import('#/views/operate/room-profile.vue'),
meta: { title: '房间资料信息' },
},
{ {
name: 'OperateSysCountry', name: 'OperateSysCountry',
path: 'sys/country', path: 'sys/country',
@ -293,12 +221,6 @@ const routes: RouteRecordRaw[] = [
component: () => import('#/views/operate/game-egg.vue'), component: () => import('#/views/operate/game-egg.vue'),
meta: { title: '砸金蛋' }, meta: { title: '砸金蛋' },
}, },
{
name: 'ActivityRoomContribution',
path: '/activity/room/contribution',
component: () => import('#/views/operate/room-contribution.vue'),
meta: { title: '房间支持活动' },
},
{ {
name: 'ActivityHallFame', name: 'ActivityHallFame',
path: '/activity/hall-fame', path: '/activity/hall-fame',

View File

@ -0,0 +1,53 @@
import type { RouteRecordRaw } from 'vue-router';
const routes: RouteRecordRaw[] = [
{
meta: {
icon: 'lucide:credit-card',
order: 50.1,
title: '支付管理',
},
name: 'PaymentManager',
path: '/payment/manager',
redirect: '/payment/manager/country',
children: [
{
name: 'PayCountry',
path: 'country',
alias: '/operate/manager/pay/country',
component: () => import('#/views/operate/pay-country.vue'),
meta: { title: '开通支付国家' },
},
{
name: 'RegionRelation',
path: 'region/relation',
alias: '/operate/manager/region/relation',
component: () => import('#/views/operate/region-relation.vue'),
meta: { title: '支付区域关系' },
},
{
name: 'PayApplication',
path: 'application',
alias: '/operate/manager/pay/application',
component: () => import('#/views/operate/pay-application.vue'),
meta: { title: '应用管理' },
},
{
name: 'PayFactory',
path: 'factory',
alias: '/operate/manager/pay/factory',
component: () => import('#/views/operate/pay-factory.vue'),
meta: { title: '支付厂商' },
},
{
name: 'PayChannel',
path: 'channel',
alias: '/operate/manager/pay/channel',
component: () => import('#/views/operate/pay-channel.vue'),
meta: { title: '支付渠道' },
},
],
},
];
export default routes;

View File

@ -0,0 +1,67 @@
import type { RouteRecordRaw } from 'vue-router';
const routes: RouteRecordRaw[] = [
{
meta: {
icon: 'lucide:house',
order: 50.2,
title: '房间管理',
},
name: 'RoomManager',
path: '/room/manager',
redirect: '/room/manager/set-top-room',
children: [
{
name: 'SetTop',
path: 'set-top-room',
alias: '/operate/manager/set-top-room',
component: () => import('#/views/operate/set-top-room.vue'),
meta: { title: '置顶房间' },
},
{
name: 'SetHot',
path: 'set-hot-room',
alias: '/operate/manager/set-hot-room',
component: () => import('#/views/operate/set-hot-room.vue'),
meta: { title: '热门房间' },
},
{
name: 'OperateBusinessDevelopment',
path: 'business-development',
alias: '/operate/manager/room/business-development',
component: () => import('#/views/operate/business-development.vue'),
meta: { title: 'BD列表' },
},
{
name: 'OperateContributionBalance',
path: 'contribution/balance',
alias: '/operate/manager/contribution/balance',
component: () => import('#/views/operate/contribution-balance.vue'),
meta: { title: '房间贡献余额' },
},
{
name: 'OperateOnlineRoom',
path: 'online/room',
alias: '/operate/manager/online/room',
component: () => import('#/views/operate/online-room.vue'),
meta: { title: '在线房间' },
},
{
name: 'OperateRoomProfile',
path: 'profile/room',
alias: '/operate/manager/profile/room',
component: () => import('#/views/operate/room-profile.vue'),
meta: { title: '房间资料信息' },
},
{
name: 'ActivityRoomContribution',
path: 'activity/room/contribution',
alias: '/activity/room/contribution',
component: () => import('#/views/operate/room-contribution.vue'),
meta: { title: '房间支持活动' },
},
],
},
];
export default routes;

View File

@ -14,11 +14,12 @@ import {
FormItem, FormItem,
Input, Input,
Select, Select,
SelectOption,
Space, Space,
message, message,
} from 'antdv-next'; } from 'antdv-next';
import TeamCountrySelect from '#/views/team/components/team-country-select.vue';
const props = defineProps<{ const props = defineProps<{
open: boolean; open: boolean;
userId: number | string; userId: number | string;
@ -31,26 +32,58 @@ const emit = defineEmits<{
const loading = ref(false); const loading = ref(false);
const birthday = ref(''); const birthday = ref('');
const form = reactive<Record<string, any>>({
bornDay: '', const genderOptions = [
bornMonth: '', { label: '女', value: 0 },
bornYear: '', { label: '男', value: 1 },
countryName: '', ];
id: '',
userNickname: '', function createForm() {
userSex: undefined, return {
}); bornDay: '',
bornMonth: '',
bornYear: '',
countryCode: '',
countryId: '',
countryName: '',
id: '',
userNickname: '',
userSex: undefined as number | undefined,
};
}
const form = reactive<Record<string, any>>(createForm());
function normalizeUserSex(value: number | string | undefined) {
if (value === '' || value === null || value === undefined) {
return undefined;
}
const nextValue = Number(value);
return Number.isNaN(nextValue) ? undefined : nextValue;
}
function handleCountryChange(
_value: number | string,
row: Record<string, any> | null,
) {
form.countryId = row?.id || '';
form.countryCode = row?.alphaTwo || row?.alphaThree || '';
form.countryName = row?.aliasName || row?.countryName || '';
}
watch( watch(
() => [props.open, props.userId], () => [props.open, props.userId],
async ([open, userId]) => { async ([open, userId]) => {
if (!open || !userId) { if (!open || !userId) {
Object.assign(form, createForm());
birthday.value = '';
return; return;
} }
loading.value = true; loading.value = true;
try { try {
const data = await getUserBaseInfo(String(userId)); const data = await getUserBaseInfo(String(userId));
Object.assign(form, data || {}); Object.assign(form, createForm(), data || {});
form.userSex = normalizeUserSex(data?.userSex);
birthday.value = birthday.value =
data?.bornYear && data?.bornMonth && data?.bornDay data?.bornYear && data?.bornMonth && data?.bornDay
? `${String(data.bornYear)}-${String(data.bornMonth).padStart(2, '0')}-${String(data.bornDay).padStart(2, '0')}` ? `${String(data.bornYear)}-${String(data.bornMonth).padStart(2, '0')}-${String(data.bornDay).padStart(2, '0')}`
@ -120,10 +153,13 @@ async function handleSubmit() {
<Input v-model:value="form.userNickname" :disabled="loading" /> <Input v-model:value="form.userNickname" :disabled="loading" />
</FormItem> </FormItem>
<FormItem label="性别"> <FormItem label="性别">
<Select option-label-prop="children" v-model:value="form.userSex" :disabled="loading"> <Select
<SelectOption :value="0"></SelectOption> v-model:value="form.userSex"
<SelectOption :value="1"></SelectOption> :disabled="loading"
</Select> :options="genderOptions"
option-label-prop="label"
placeholder="请选择性别"
/>
</FormItem> </FormItem>
<FormItem label="出生日期"> <FormItem label="出生日期">
<DatePicker <DatePicker
@ -134,7 +170,11 @@ async function handleSubmit() {
/> />
</FormItem> </FormItem>
<FormItem label="国家"> <FormItem label="国家">
<Input v-model:value="form.countryName" disabled /> <TeamCountrySelect
v-model:value="form.countryId"
:disabled="loading"
@change="handleCountryChange"
/>
</FormItem> </FormItem>
</Form> </Form>

View File

@ -6,14 +6,12 @@ import {
updateBdLead, updateBdLead,
} from '#/api/legacy/team'; } from '#/api/legacy/team';
import AccountInput from '#/components/account-input.vue'; import AccountInput from '#/components/account-input.vue';
import { regionConfigTable } from '#/api/legacy/system'; import SystemRegionSelect from '#/views/operate/components/system-region-select.vue';
import { import {
Form, Form,
FormItem, FormItem,
Modal, Modal,
Select,
SelectOption,
Input, Input,
message, message,
} from 'antdv-next'; } from 'antdv-next';
@ -30,8 +28,6 @@ const emit = defineEmits<{
}>(); }>();
const saving = ref(false); const saving = ref(false);
const regionLoading = ref(false);
const regionOptions = ref<Array<Record<string, any>>>([]);
const form = reactive({ const form = reactive({
contact: '', contact: '',
@ -52,28 +48,13 @@ watch(
} }
form.id = String(props.row?.id || ''); form.id = String(props.row?.id || '');
form.userId = String(props.row?.userId || props.row?.userProfile?.id || ''); form.userId = String(props.row?.userId || props.row?.userProfile?.id || '');
form.sysOrigin = props.sysOrigin || props.row?.sysOrigin || 'LIKEI'; form.sysOrigin = props.row?.sysOrigin || props.sysOrigin || 'LIKEI';
form.contact = props.row?.contact || ''; form.contact = props.row?.contact || '';
form.regionId = String(props.row?.regionId || ''); form.regionId = String(props.row?.regionId || '');
await loadRegions(form.sysOrigin);
}, },
{ immediate: true }, { immediate: true },
); );
async function loadRegions(sysOrigin: string) {
if (!sysOrigin) {
regionOptions.value = [];
return;
}
regionLoading.value = true;
try {
const result = await regionConfigTable({ sysOrigin });
regionOptions.value = result || [];
} finally {
regionLoading.value = false;
}
}
async function handleSubmit() { async function handleSubmit() {
if (!isUpdate.value && !String(form.userId || '').trim()) { if (!isUpdate.value && !String(form.userId || '').trim()) {
message.warning('请输入BD Leader 用户ID'); message.warning('请输入BD Leader 用户ID');
@ -124,20 +105,11 @@ async function handleSubmit() {
/> />
</FormItem> </FormItem>
<FormItem label="区域"> <FormItem label="区域">
<Select option-label-prop="label" <SystemRegionSelect
v-model:value="form.regionId" v-model:value="form.regionId"
:loading="regionLoading"
placeholder="请选择区域" placeholder="请选择区域"
> :sys-origin="form.sysOrigin"
<SelectOption />
v-for="item in regionOptions"
:key="item.id"
:label="item.regionName"
:value="String(item.id)"
>
{{ item.regionName }}
</SelectOption>
</Select>
</FormItem> </FormItem>
<FormItem label="联系方式"> <FormItem label="联系方式">
<Input v-model:value="form.contact" placeholder="联系方式" /> <Input v-model:value="form.contact" placeholder="联系方式" />

View File

@ -1,11 +1,43 @@
import { defineConfig } from '@vben/vite-config'; import { defineConfig } from '@vben/vite-config';
const DEV_CONSOLE_PREFIX = '/console';
const DEV_API_INTERNAL_PREFIX = '/__dev_api_internal__';
const DEV_API_PROD_PREFIX = '/__dev_api_prod__';
const DEV_INTERNAL_CONSOLE_TARGET = 'http://127.0.0.1:2700';
const DEV_INTERNAL_GOLANG_TARGET = 'http://127.0.0.1:2900';
const DEV_PROD_TARGET = 'https://yumi-admin.haiyihy.com';
function createPrefixRegExp(prefix: string) {
return new RegExp(`^${prefix.replaceAll('/', '\\/')}`);
}
export default defineConfig(async () => { export default defineConfig(async () => {
return { return {
application: {}, application: {},
vite: { vite: {
server: { server: {
proxy: { proxy: {
[`${DEV_API_INTERNAL_PREFIX}/go`]: {
changeOrigin: true,
rewrite: (path) =>
path.replace(createPrefixRegExp(`${DEV_API_INTERNAL_PREFIX}/go`), ''),
target: DEV_INTERNAL_GOLANG_TARGET,
ws: true,
},
[DEV_API_INTERNAL_PREFIX]: {
changeOrigin: true,
rewrite: (path) =>
path.replace(createPrefixRegExp(DEV_API_INTERNAL_PREFIX), DEV_CONSOLE_PREFIX),
target: DEV_INTERNAL_CONSOLE_TARGET,
ws: true,
},
[DEV_API_PROD_PREFIX]: {
changeOrigin: true,
rewrite: (path) =>
path.replace(createPrefixRegExp(DEV_API_PROD_PREFIX), DEV_CONSOLE_PREFIX),
target: DEV_PROD_TARGET,
ws: true,
},
'/console/go': { '/console/go': {
changeOrigin: true, changeOrigin: true,
rewrite: (path) => path.replace(/^\/console\/go/, ''), rewrite: (path) => path.replace(/^\/console\/go/, ''),