fix: sync admin menu access and bd management

This commit is contained in:
hy 2026-04-21 20:11:22 +08:00
parent 246697871c
commit 8f2b5ad3dd
13 changed files with 1158 additions and 14 deletions

View File

@ -69,10 +69,19 @@ function convertMenuNodes(menus: LegacyMenu[]): RouteRecordStringComponent[] {
});
}
/**
*
*/
export async function getLegacyMenusApi() {
return (await requestClient.get<LegacyMenu[]>('/account/menus')) || [];
}
/**
*
*/
export async function getAllMenusApi() {
const result = await requestClient.get<LegacyMenu[]>('/account/menus');
const result = await getLegacyMenusApi();
return convertMenuNodes(result || []);
}
export type { LegacyMenu };

View File

@ -193,6 +193,48 @@ export async function pageBdLead(params: Record<string, any>) {
);
}
export async function pageBdTable(params: Record<string, any>) {
return requestClient.get<LegacyPageResult<Record<string, any>>>(
'/team/bd/page',
{
params,
},
);
}
export async function pageBdMemberTable(params: Record<string, any>) {
return requestClient.get<LegacyPageResult<Record<string, any>>>(
'/team/bd/member/page',
{
params,
},
);
}
export async function deleteBd(id: number | string) {
return requestClient.get('/team/bd/del', {
params: { id },
});
}
export async function deleteBdMember(id: number | string) {
return requestClient.get('/team/bd/member/del', {
params: { id },
});
}
export async function addBd(data: Record<string, any>) {
return requestClient.post('/team/bd/add', data);
}
export async function updateBd(data: Record<string, any>) {
return requestClient.post('/team/bd/update', data);
}
export async function changeBdLeader(data: Record<string, any>) {
return requestClient.post('/team/bd/change-bd-leader', data);
}
export async function deleteBdLead(id: number | string) {
return requestClient.get('/team/bd/leader/del', {
params: { id },

View File

@ -8,6 +8,7 @@ import { defineOverridesPreferences } from '@vben/preferences';
export const overridesPreferences = defineOverridesPreferences({
// overrides
app: {
accessMode: 'frontend',
defaultHomePath: '/workspace',
name: '数据平台',
},

View File

@ -2,18 +2,169 @@ import type {
ComponentRecordType,
GenerateMenuAndRoutesOptions,
} from '@vben/types';
import type { RouteRecordRaw } from 'vue-router';
import { generateAccessible } from '@vben/access';
import { preferences } from '@vben/preferences';
import { message } from 'antdv-next';
import { getAllMenusApi } from '#/api';
import { getAllMenusApi, getLegacyMenusApi, type LegacyMenu } from '#/api';
import { BasicLayout, IFrameView } from '#/layouts';
import { $t } from '#/locales';
const forbiddenComponent = () => import('#/views/_core/fallback/forbidden.vue');
const ROUTE_MENU_OVERRIDES: Record<
string,
{
aliases?: string[];
routers?: string[];
titles?: string[];
}
> = {
BdLead: { aliases: ['BdLead'], routers: ['team/bd-lead'] },
DynamicTagList: { aliases: ['DynamicTag'], routers: ['dynamic/tag'] },
DynamicUserList: { aliases: ['UserDynamicList'], routers: ['user/dynamic/list'] },
OperateBusinessDevelopment: {
aliases: ['BusinessDevelopment'],
routers: ['room/business-development'],
titles: ['BD列表'],
},
PropsResourceConfig: { aliases: ['PropsConfig'], routers: ['props/config'] },
PropsSourceGroup: {
aliases: ['PropsActivityRewardCnf'],
routers: ['props/props_source_group'],
},
SystemMenusManager: { aliases: ['MenusManager'], routers: ['menus'] },
SystemResourceManager: { aliases: ['ResourceManager'], routers: ['resource'] },
SystemRoleManager: { aliases: ['RolesManager'], routers: ['roles'] },
SystemUserManager: { aliases: ['UserManager'], routers: ['user'] },
TeamBillList: { aliases: ['TeamBill'], routers: ['team/bill'] },
TeamMemberList: { aliases: ['TeamMember'], routers: ['team/member'] },
UserDiamondBalance: {
aliases: ['GameDoubleLayerFruit'],
routers: ['/user/diamond-balance'],
},
UserDiamondRunWater: {
aliases: ['GameDoubleLayerFruit'],
routers: ['/user/diamond-run-water'],
},
};
function normalizeRoutePath(path?: string | null) {
return String(path || '')
.trim()
.replace(/^\/+/, '')
.replace(/\/+$/, '');
}
function collectRouteAliases(route: RouteRecordRaw) {
const values: string[] = [];
if (route.name) {
values.push(String(route.name));
}
const override = ROUTE_MENU_OVERRIDES[String(route.name || '')];
if (override?.aliases) {
values.push(...override.aliases);
}
return new Set(values.filter(Boolean));
}
function collectRoutePaths(route: RouteRecordRaw) {
const values = [normalizeRoutePath(route.path)];
const aliases = Array.isArray(route.alias)
? route.alias
: route.alias
? [route.alias]
: [];
values.push(...aliases.map((item) => normalizeRoutePath(String(item))));
const override = ROUTE_MENU_OVERRIDES[String(route.name || '')];
if (override?.routers) {
values.push(...override.routers.map((item) => normalizeRoutePath(item)));
}
return new Set(values.filter(Boolean));
}
function collectRouteTitles(route: RouteRecordRaw) {
const values = [String(route.meta?.title || '').trim()];
const override = ROUTE_MENU_OVERRIDES[String(route.name || '')];
if (override?.titles) {
values.push(...override.titles.map((item) => item.trim()));
}
return new Set(values.filter(Boolean));
}
function buildAuthorizedMenuMatcher(menus: LegacyMenu[]) {
const aliasSet = new Set<string>();
const routerSet = new Set<string>();
const titleSet = new Set<string>();
const queue = [...menus];
while (queue.length > 0) {
const item = queue.shift();
if (!item) {
continue;
}
if (item.alias) {
aliasSet.add(String(item.alias));
}
if (item.router) {
routerSet.add(normalizeRoutePath(item.router));
}
if (item.menuType === 2 && item.menuName) {
titleSet.add(String(item.menuName).trim());
}
if (Array.isArray(item.childrens) && item.childrens.length > 0) {
queue.push(...item.childrens);
}
}
return (route: RouteRecordRaw) => {
for (const alias of collectRouteAliases(route)) {
if (aliasSet.has(alias)) {
return true;
}
}
for (const path of collectRoutePaths(route)) {
if (routerSet.has(path)) {
return true;
}
}
for (const title of collectRouteTitles(route)) {
if (titleSet.has(title)) {
return true;
}
}
return false;
};
}
function filterRoutesByAuthorizedMenus(
routes: RouteRecordRaw[],
isAuthorized: (route: RouteRecordRaw) => boolean,
): RouteRecordRaw[] {
return routes.flatMap((route) => {
const nextRoute: RouteRecordRaw = { ...route };
const children = route.children
? filterRoutesByAuthorizedMenus(route.children, isAuthorized)
: [];
if (route.meta?.ignoreAccess || route.meta?.hideInMenu) {
nextRoute.children = children;
return [nextRoute];
}
if (children.length > 0) {
nextRoute.children = children;
return [nextRoute];
}
delete nextRoute.children;
return isAuthorized(route) ? [nextRoute] : [];
});
}
async function generateAccess(options: GenerateMenuAndRoutesOptions) {
const pageMap: ComponentRecordType = import.meta.glob('../views/**/*.vue');
@ -22,8 +173,21 @@ async function generateAccess(options: GenerateMenuAndRoutesOptions) {
IFrameView,
};
let routes = options.routes || [];
if (preferences.app.accessMode === 'frontend') {
message.loading({
content: `${$t('common.loadingMenu')}...`,
duration: 1.5,
});
const legacyMenus = await getLegacyMenusApi();
const isAuthorized = buildAuthorizedMenuMatcher(legacyMenus);
routes = filterRoutesByAuthorizedMenus(routes, isAuthorized);
}
return await generateAccessible(preferences.app.accessMode, {
...options,
routes,
fetchMenuListAsync: async () => {
message.loading({
content: `${$t('common.loadingMenu')}...`,

View File

@ -3,6 +3,7 @@ import type { RouteRecordRaw } from 'vue-router';
import { mergeRouteModules, traverseTreeValues } from '@vben/utils';
import { coreRoutes, fallbackNotFoundRoute } from './core';
import dashboardRoutes from './modules/dashboard';
const dynamicRouteFiles = import.meta.glob('./modules/**/*.ts', {
eager: true,
@ -13,8 +14,14 @@ const externalRouteFiles = import.meta.glob('./external/**/*.ts', {
});
// const staticRouteFiles = import.meta.glob('./static/**/*.ts', { eager: true });
const accessRouteFiles = Object.fromEntries(
Object.entries(dynamicRouteFiles).filter(
([path]) => !path.endsWith('/dashboard.ts'),
),
);
/** 动态路由 */
const dynamicRoutes: RouteRecordRaw[] = mergeRouteModules(dynamicRouteFiles);
const dynamicRoutes: RouteRecordRaw[] = mergeRouteModules(accessRouteFiles);
/** 外部路由列表访问这些页面可以不需要Layout可能用于内嵌在别的系统(不会显示在菜单中) */
const externalRoutes: RouteRecordRaw[] = mergeRouteModules(externalRouteFiles);
@ -25,6 +32,7 @@ const staticRoutes: RouteRecordRaw[] = [];
* */
const routes: RouteRecordRaw[] = [
...coreRoutes,
...dashboardRoutes,
...externalRoutes,
fallbackNotFoundRoute,
];

View File

@ -25,13 +25,6 @@ const routes: RouteRecordRaw[] = [
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',

View File

@ -52,6 +52,13 @@ const routes: RouteRecordRaw[] = [
component: () => import('#/views/team/application-process-list.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: 'BdLead',
path: 'team/bd-lead',

View File

@ -1,9 +1,444 @@
<script lang="ts" setup>
import TeamBdLead from '#/views/team/bd-lead.vue';
import { computed, reactive, ref } from 'vue';
import { useRouter } from 'vue-router';
import { Page } from '@vben/common-ui';
import { useAccessStore, useUserStore } from '@vben/stores';
import {
deleteBd,
listMembers,
pageBdTable,
} from '#/api/legacy/team';
import AccountInput from '#/components/account-input.vue';
import SysOriginLabel from '#/components/sys-origin-label.vue';
import SysOriginSelect from '#/components/sys-origin-select.vue';
import TeamCreateDrawer from '#/views/team/components/team-create-drawer.vue';
import {
formatDate,
getAllowedSysOrigins,
} from '#/views/system/shared';
import {
Button,
Card,
Input,
Modal,
Pagination,
Select,
Space,
Table,
message,
} from 'antdv-next';
import BdBindLeaderModal from './components/bd-bind-leader-modal.vue';
import BdBindTeamModal from './components/bd-bind-team-modal.vue';
import BdFormModal from './components/bd-form-modal.vue';
import BdMemberDrawer from './components/bd-member-drawer.vue';
import SystemRegionSelect from './components/system-region-select.vue';
defineOptions({ name: 'OperateBusinessDevelopment' });
const router = useRouter();
const accessStore = useAccessStore();
const userStore = useUserStore();
const sysOriginOptions = computed(() => {
const options = getAllowedSysOrigins(accessStore.accessCodes || []);
return options.length > 0 ? options : getAllowedSysOrigins([]);
});
const loading = ref(false);
const membersLoading = ref(false);
const total = ref(0);
const list = ref<Array<Record<string, any>>>([]);
const members = ref<Array<Record<string, any>>>([]);
const formOpen = ref(false);
const bindLeaderOpen = ref(false);
const bindTeamOpen = ref(false);
const memberOpen = ref(false);
const createTeamOpen = ref(false);
const activeRow = ref<null | Record<string, any>>(null);
const query = reactive({
bdLeadUserId: '',
createUser: '',
cursor: 1,
limit: 20,
memberQuantityRange: '',
region: '',
sysOrigin: '',
userId: '',
});
const columns = [
{ dataIndex: 'sysOrigin', key: 'sysOrigin', title: '系统', width: 100 },
{ dataIndex: 'userProfile', key: 'userProfile', title: '用户', width: 240 },
{ dataIndex: 'regionName', key: 'regionName', title: '区域', width: 120 },
{ dataIndex: 'agentCount', key: 'agentCount', title: '团队数量', width: 110 },
{ dataIndex: 'bdLeadUserProfile', key: 'bdLeadUserProfile', title: 'BD Leader', width: 240 },
{ dataIndex: 'contact', key: 'contact', title: '联系方式', width: 180 },
{ dataIndex: 'createUserName', key: 'createUserName', title: '创建人', width: 140 },
{ dataIndex: 'updateUserName', key: 'updateUserName', title: '修改人', width: 140 },
{ dataIndex: 'createTime', key: 'createTime', title: '创建时间', width: 180 },
{ dataIndex: 'actions', key: 'actions', title: '操作', width: 280 },
];
function hasPermission(code: string) {
const codes = accessStore.accessCodes || [];
return codes.length === 0 || codes.includes(code);
}
const canQueryAll = computed(() => hasPermission('bd:list:query:all'));
const canQuerySelf = computed(() => hasPermission('bd:list:query:self'));
const canQuery = computed(() => canQueryAll.value || canQuerySelf.value);
const canCreate = computed(() => hasPermission('bd:list:add'));
const canEdit = computed(() => hasPermission('bd:list:edit'));
const canDelete = computed(() => hasPermission('bd:list:del'));
const canAddMember = computed(() => hasPermission('bd:list:add:member'));
const canBindMember = computed(() => hasPermission('bd:list:add:bind'));
const showOperationCol = computed(
() =>
canEdit.value ||
canDelete.value ||
canAddMember.value ||
canBindMember.value,
);
function getUserText(profile?: Record<string, any>) {
if (!profile) {
return '-';
}
const nickname = profile.userNickname || profile.nickname || '-';
const account = profile.actualAccount || profile.account || profile.id;
return account ? `${nickname} / ${account}` : nickname;
}
function openUserDetails(userId?: number | string) {
if (!userId) {
return;
}
router.push(`/common/user/deatils/${userId}`);
}
async function loadMembers() {
if (!canQueryAll.value) {
members.value = [];
return;
}
membersLoading.value = true;
try {
members.value = await listMembers();
} finally {
membersLoading.value = false;
}
}
async function loadData(reset = false) {
if (!canQuery.value) {
list.value = [];
total.value = 0;
return;
}
if (reset) {
query.cursor = 1;
}
loading.value = true;
try {
const result = await pageBdTable({ ...query });
list.value = result.records || [];
total.value = result.total || 0;
} finally {
loading.value = false;
}
}
function handleSearch() {
loadData(true);
}
function handlePageChange(page: number, pageSize: number) {
query.cursor = page;
query.limit = pageSize;
loadData();
}
function openCreate() {
activeRow.value = {
sysOrigin: query.sysOrigin,
};
formOpen.value = true;
}
function openEdit(record: Record<string, any>) {
activeRow.value = {
...record,
sysOrigin: record.sysOrigin || query.sysOrigin,
};
formOpen.value = true;
}
function openBindLeader(record: Record<string, any>) {
activeRow.value = { ...record };
bindLeaderOpen.value = true;
}
function openBindTeam(record: Record<string, any>) {
activeRow.value = { ...record };
bindTeamOpen.value = true;
}
function openCreateTeam(record: Record<string, any>) {
activeRow.value = { ...record };
createTeamOpen.value = true;
}
function openMembers(record: Record<string, any>) {
activeRow.value = { ...record };
memberOpen.value = true;
}
function handleDelete(record: Record<string, any>) {
Modal.confirm({
title: '您确定要删除吗(将无法恢复, 解散当前团队成员)?',
async onOk() {
await deleteBd(record.id);
message.success('删除成功');
await loadData(true);
},
});
}
query.sysOrigin = String(sysOriginOptions.value[0]?.value || '');
if (canQuerySelf.value && !canQueryAll.value) {
query.createUser = String(userStore.userInfo?.userId || '');
}
loadMembers();
loadData(true);
</script>
<template>
<TeamBdLead />
<Page title="BD列表">
<Card>
<div v-if="canQuery" class="toolbar">
<Space wrap>
<SysOriginSelect
v-model:value="query.sysOrigin"
:options="sysOriginOptions"
style="width: 140px"
@change="handleSearch"
/>
<AccountInput
v-model:value="query.userId"
:sys-origin="query.sysOrigin"
placeholder="BD用户ID"
style="width: 220px"
/>
<AccountInput
v-model:value="query.bdLeadUserId"
:sys-origin="query.sysOrigin"
placeholder="BD Lead用户ID"
style="width: 220px"
/>
<SystemRegionSelect
v-model:value="query.region"
:sys-origin="query.sysOrigin"
clearable
placeholder="区域"
style="width: 160px"
@change="handleSearch"
/>
<Input
v-model:value="query.memberQuantityRange"
placeholder="开始~结束成员数量"
style="width: 180px"
/>
<Select
v-if="canQueryAll"
v-model:value="query.createUser"
:loading="membersLoading"
allow-clear
:field-names="{ label: 'nickname', value: 'id' }"
:options="members"
placeholder="后台成员"
show-search
style="width: 160px"
@change="handleSearch"
/>
<Button :loading="loading" type="primary" @click="handleSearch">
搜索
</Button>
<Button v-if="canCreate" @click="openCreate">新增BD</Button>
</Space>
</div>
<div v-else class="permission-tip">
抱歉您无权查看请联系管理员开通查看权限bd:list:query:all / bd:list:query:self
</div>
<Table
v-if="canQuery"
:columns="columns"
:data-source="list"
:loading="loading"
:pagination="false"
row-key="id"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'sysOrigin'">
<SysOriginLabel :value="record.sysOrigin" />
</template>
<template v-else-if="column.key === 'userProfile'">
<Button
v-if="record.userProfile?.id"
type="link"
@click="openUserDetails(record.userProfile?.id)"
>
{{ getUserText(record.userProfile) }}
</Button>
<span v-else>{{ getUserText(record.userProfile) }}</span>
</template>
<template v-else-if="column.key === 'agentCount'">
<Button
v-if="Number(record.agentCount || 0) > 0"
size="small"
type="link"
@click="openMembers(record)"
>
{{ record.agentCount }}
</Button>
<span v-else>0</span>
</template>
<template v-else-if="column.key === 'bdLeadUserProfile'">
<Button
v-if="record.bdLeadUserProfile?.id"
type="link"
@click="openUserDetails(record.bdLeadUserProfile?.id)"
>
{{ getUserText(record.bdLeadUserProfile) }}
</Button>
<span v-else>{{ getUserText(record.bdLeadUserProfile) }}</span>
</template>
<template v-else-if="column.key === 'contact'">
{{ canEdit ? record.contact || '-' : '-' }}
</template>
<template v-else-if="column.key === 'createTime'">
{{ formatDate(record.createTime) }}
</template>
<template v-else-if="column.key === 'actions' && showOperationCol">
<Space wrap>
<Button
v-if="canAddMember"
size="small"
type="link"
@click="openCreateTeam(record)"
>
新增代理
</Button>
<Button
v-if="canBindMember"
size="small"
type="link"
@click="openBindTeam(record)"
>
绑定代理
</Button>
<Button
v-if="canEdit"
size="small"
type="link"
@click="openEdit(record)"
>
编辑
</Button>
<Button
v-if="canEdit"
size="small"
type="link"
@click="openBindLeader(record)"
>
换绑BD Leader
</Button>
<Button
v-if="canDelete"
danger
size="small"
type="link"
@click="handleDelete(record)"
>
删除
</Button>
</Space>
</template>
</template>
</Table>
<div v-if="canQuery" class="pagination">
<Pagination
:current="query.cursor"
:page-size="query.limit"
:total="total"
show-size-changer
@change="handlePageChange"
@showSizeChange="handlePageChange"
/>
</div>
</Card>
<BdFormModal
:open="formOpen"
:row="activeRow"
:sys-origin="query.sysOrigin"
@close="formOpen = false"
@success="loadData(true)"
/>
<BdBindLeaderModal
:open="bindLeaderOpen"
:row="activeRow"
@close="bindLeaderOpen = false"
@success="loadData(true)"
/>
<BdBindTeamModal
:open="bindTeamOpen"
:row="activeRow"
@close="bindTeamOpen = false"
@success="loadData(true)"
/>
<BdMemberDrawer
:open="memberOpen"
:row="activeRow"
@close="memberOpen = false"
/>
<TeamCreateDrawer
:default-bd-user-id="String(activeRow?.userProfile?.id || activeRow?.userId || '')"
:default-region="String(activeRow?.region || '')"
:lock-bind-bd="true"
:open="createTeamOpen"
:sys-origin="String(activeRow?.sysOrigin || query.sysOrigin || '')"
@close="createTeamOpen = false"
@success="loadData(true)"
/>
</Page>
</template>
<style scoped>
.toolbar {
margin-bottom: 16px;
}
.pagination {
display: flex;
justify-content: flex-end;
margin-top: 16px;
}
.permission-tip {
color: #64748b;
padding: 24px 0;
text-align: center;
}
</style>

View File

@ -0,0 +1,71 @@
<script lang="ts" setup>
import { reactive, ref, watch } from 'vue';
import { changeBdLeader } from '#/api/legacy/team';
import { Form, FormItem, Input, Modal, message } from 'antdv-next';
const props = defineProps<{
open: boolean;
row: null | Record<string, any>;
}>();
const emit = defineEmits<{
close: [];
success: [];
}>();
const saving = ref(false);
const form = reactive({
bdId: '',
newAccount: '',
});
watch(
() => props.open,
(open) => {
if (!open) {
return;
}
form.bdId = String(props.row?.id || '');
form.newAccount = '';
},
{ immediate: true },
);
async function handleSubmit() {
if (!form.newAccount) {
message.warning('请输入BD Leader账号');
return;
}
saving.value = true;
try {
await changeBdLeader({ ...form });
message.success('保存成功');
emit('success');
emit('close');
} finally {
saving.value = false;
}
}
</script>
<template>
<Modal
:confirm-loading="saving"
:open="open"
destroy-on-close
title="换绑BD Leader"
@cancel="emit('close')"
@ok="handleSubmit"
>
<Form layout="vertical">
<FormItem label="BD Leader">
<Input
v-model:value="form.newAccount"
placeholder="请输入BD Leader账号"
/>
</FormItem>
</Form>
</Modal>
</template>

View File

@ -0,0 +1,77 @@
<script lang="ts" setup>
import { reactive, ref, watch } from 'vue';
import { teamBindBd } from '#/api/legacy/team';
import AccountInput from '#/components/account-input.vue';
import { Form, FormItem, Modal, message } from 'antdv-next';
const props = defineProps<{
open: boolean;
row: null | Record<string, any>;
}>();
const emit = defineEmits<{
close: [];
success: [];
}>();
const saving = ref(false);
const sysOrigin = ref('');
const form = reactive({
bdUserId: '',
ownUserId: '',
});
watch(
() => props.open,
(open) => {
if (!open) {
return;
}
sysOrigin.value = String(
props.row?.userProfile?.originSys || props.row?.sysOrigin || '',
);
form.bdUserId = String(props.row?.userProfile?.id || props.row?.userId || '');
form.ownUserId = '';
},
{ immediate: true },
);
async function handleSubmit() {
if (!form.ownUserId) {
message.warning('请输入代理账号');
return;
}
saving.value = true;
try {
await teamBindBd({ ...form });
message.success('保存成功');
emit('success');
emit('close');
} finally {
saving.value = false;
}
}
</script>
<template>
<Modal
:confirm-loading="saving"
:open="open"
destroy-on-close
title="绑定代理"
@cancel="emit('close')"
@ok="handleSubmit"
>
<Form layout="vertical">
<FormItem label="代理">
<AccountInput
v-model:value="form.ownUserId"
:sys-origin="sysOrigin"
placeholder="请输入代理账号"
/>
</FormItem>
</Form>
</Modal>
</template>

View File

@ -0,0 +1,143 @@
<script lang="ts" setup>
import { reactive, ref, watch } from 'vue';
import { addBd, updateBd } from '#/api/legacy/team';
import AccountInput from '#/components/account-input.vue';
import {
Button,
Drawer,
Form,
FormItem,
Input,
message,
} from 'antdv-next';
import SystemRegionSelect from './system-region-select.vue';
const props = defineProps<{
open: boolean;
row: null | Record<string, any>;
sysOrigin: string;
}>();
const emit = defineEmits<{
close: [];
success: [];
}>();
const saving = ref(false);
const form = reactive({
contact: '',
id: '',
region: '',
sysOrigin: '',
userId: '',
});
const isUpdate = ref(false);
const title = ref('新增BD');
watch(
() => props.open,
(open) => {
if (!open) {
return;
}
isUpdate.value = !!props.row?.id;
title.value = isUpdate.value ? '编辑BD' : '新增BD';
form.id = String(props.row?.id || '');
form.userId = String(props.row?.userId || props.row?.userProfile?.id || '');
form.region = String(props.row?.region || '');
form.contact = String(props.row?.contact || '');
form.sysOrigin = String(props.row?.sysOrigin || props.sysOrigin || '');
},
{ immediate: true },
);
watch(
() => props.sysOrigin,
(value) => {
if (!isUpdate.value) {
form.sysOrigin = String(value || '');
}
},
{ immediate: true },
);
async function handleSubmit() {
if (!form.userId) {
message.warning('请输入BD用户ID');
return;
}
if (!form.region) {
message.warning('请选择区域');
return;
}
saving.value = true;
try {
const payload = { ...form, sysOrigin: form.sysOrigin || props.sysOrigin };
if (isUpdate.value) {
await updateBd(payload);
} else {
await addBd(payload);
}
message.success('保存成功');
emit('success');
emit('close');
} finally {
saving.value = false;
}
}
</script>
<template>
<Drawer
:open="open"
:title="title"
destroy-on-close
width="520px"
@close="emit('close')"
>
<Form layout="vertical">
<FormItem label="BD">
<AccountInput
v-model:value="form.userId"
:disabled="isUpdate"
:sys-origin="form.sysOrigin || props.sysOrigin"
placeholder="请输入BD用户ID"
/>
</FormItem>
<FormItem label="区域">
<SystemRegionSelect
v-model:value="form.region"
:sys-origin="form.sysOrigin || props.sysOrigin"
clearable
placeholder="请选择区域"
/>
</FormItem>
<FormItem label="联系方式">
<Input
v-model:value="form.contact"
placeholder="请输入联系方式"
/>
</FormItem>
</Form>
<template #footer>
<div class="drawer-footer">
<Button @click="emit('close')">取消</Button>
<Button :loading="saving" type="primary" @click="handleSubmit">
保存
</Button>
</div>
</template>
</Drawer>
</template>
<style scoped>
.drawer-footer {
display: flex;
justify-content: flex-end;
}
</style>

View File

@ -0,0 +1,189 @@
<script lang="ts" setup>
import { reactive, ref, watch } from 'vue';
import { useRouter } from 'vue-router';
import { deleteBdMember, pageBdMemberTable } from '#/api/legacy/team';
import AccountInput from '#/components/account-input.vue';
import SysOriginLabel from '#/components/sys-origin-label.vue';
import { formatDate } from '#/views/system/shared';
import {
Button,
Drawer,
Modal,
Pagination,
Space,
Table,
message,
} from 'antdv-next';
const props = defineProps<{
open: boolean;
row: null | Record<string, any>;
}>();
const router = useRouter();
const loading = ref(false);
const total = ref(0);
const list = ref<Array<Record<string, any>>>([]);
const sysOrigin = ref('');
const query = reactive({
agentId: '',
cursor: 1,
limit: 20,
userId: '',
});
const columns = [
{ dataIndex: 'sysOrigin', key: 'sysOrigin', title: '系统', width: 100 },
{ dataIndex: 'userProfile', key: 'userProfile', title: '用户', width: 260 },
{ dataIndex: 'anchorQuantity', key: 'anchorQuantity', title: '主播数量', width: 100 },
{ dataIndex: 'createTime', key: 'createTime', title: '创建时间', width: 180 },
{ dataIndex: 'actions', key: 'actions', title: '操作', width: 80 },
];
watch(
() => props.open,
async (open) => {
if (!open) {
return;
}
query.cursor = 1;
query.agentId = '';
query.userId = String(props.row?.userId || props.row?.userProfile?.id || '');
sysOrigin.value = String(
props.row?.userProfile?.originSys || props.row?.sysOrigin || '',
);
await loadData(true);
},
{ immediate: true },
);
function getUserText(profile?: Record<string, any>) {
if (!profile) {
return '-';
}
const nickname = profile.userNickname || profile.nickname || '-';
const account = profile.actualAccount || profile.account || profile.id;
return account ? `${nickname} / ${account}` : nickname;
}
function openUserDetails(userId?: number | string) {
if (!userId) {
return;
}
router.push(`/common/user/deatils/${userId}`);
}
async function loadData(reset = false) {
if (reset) {
query.cursor = 1;
}
loading.value = true;
try {
const result = await pageBdMemberTable({ ...query });
list.value = result.records || [];
total.value = result.total || 0;
} finally {
loading.value = false;
}
}
function handlePageChange(page: number, pageSize: number) {
query.cursor = page;
query.limit = pageSize;
loadData();
}
function handleDelete(record: Record<string, any>) {
Modal.confirm({
title: '您确定要删除吗?',
async onOk() {
await deleteBdMember(record.id);
message.success('删除成功');
await loadData(true);
},
});
}
</script>
<template>
<Drawer
:open="open"
:title="`BD团队代理成员 · ${props.row?.userProfile?.userNickname || props.row?.userId || ''}`"
destroy-on-close
width="960"
@close="$emit('close')"
>
<div class="toolbar">
<Space wrap>
<AccountInput
v-model:value="query.agentId"
:sys-origin="sysOrigin"
placeholder="用户ID"
style="width: 220px"
/>
<Button :loading="loading" type="primary" @click="loadData(true)">
搜索
</Button>
</Space>
</div>
<Table
:columns="columns"
:data-source="list"
:loading="loading"
:pagination="false"
row-key="id"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'sysOrigin'">
<SysOriginLabel :value="record.sysOrigin" />
</template>
<template v-else-if="column.key === 'userProfile'">
<Button
v-if="record.userProfile?.id"
type="link"
@click="openUserDetails(record.userProfile?.id)"
>
{{ getUserText(record.userProfile) }}
</Button>
<span v-else>{{ getUserText(record.userProfile) }}</span>
</template>
<template v-else-if="column.key === 'createTime'">
{{ formatDate(record.createTime) }}
</template>
<template v-else-if="column.key === 'actions'">
<Button danger size="small" type="link" @click="handleDelete(record)">
删除
</Button>
</template>
</template>
</Table>
<div class="pagination">
<Pagination
:current="query.cursor"
:page-size="query.limit"
:total="total"
show-size-changer
@change="handlePageChange"
@showSizeChange="handlePageChange"
/>
</div>
</Drawer>
</template>
<style scoped>
.toolbar {
margin-bottom: 16px;
}
.pagination {
display: flex;
justify-content: flex-end;
margin-top: 16px;
}
</style>

View File

@ -25,6 +25,9 @@ import {
} from '../shared';
const props = defineProps<{
defaultBdUserId?: string;
defaultRegion?: string;
lockBindBd?: boolean;
open: boolean;
sysOrigin: string;
}>();
@ -66,9 +69,11 @@ watch(
return;
}
Object.assign(form, createForm(), {
bdUserId: props.defaultBdUserId || '',
region: props.defaultRegion || '',
sysOrigin: props.sysOrigin || '',
});
bindBd.value = 'no';
bindBd.value = props.defaultBdUserId ? 'yes' : 'no';
formSettingDefault.value = false;
formContactAdd.value = false;
},
@ -158,7 +163,7 @@ async function handleSubmit() {
/>
</FormItem>
<FormItem label="绑定BD">
<RadioGroup v-model:value="bindBd">
<RadioGroup v-model:value="bindBd" :disabled="props.lockBindBd">
<Radio value="yes"></Radio>
<Radio value="no"></Radio>
</RadioGroup>