yumi-admin/apps/src/views/operate/user-manage.vue
2026-04-30 18:59:28 +08:00

626 lines
18 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script lang="ts" setup>
import dayjs from 'dayjs';
import {
computed,
reactive,
ref,
watch } from 'vue';
import { useRoute,
useRouter } from 'vue-router';
import { Page } from '@vben/common-ui';
import { useAccessStore } from '@vben/stores';
import { getCountryAlls } from '#/api/legacy/system';
import {
pageUserBaseInfo,
} from '#/api/legacy/user';
import { getAllowedSysOrigins } from '#/views/system/shared';
import {
Button,
Card,
Checkbox,
DatePicker,
Input,
Pagination,
Select,
Space,
TabPane,
Table,
Tabs,
Tag,
message
} from 'antdv-next';
import AccountHandleModal from '#/views/approval/components/account-handle-modal.vue';
import EditUserDrawer from '#/views/approval/components/edit-user-drawer.vue';
import AccountStatusLogModal from '#/views/app-system/components/account-status-log-modal.vue';
import ViolationHistoryModal from '#/views/app-system/components/violation-history-modal.vue';
import {
GENDER_OPTIONS,
ORIGIN_PLATFORM_OPTIONS,
REGISTER_ORIGIN_OPTIONS,
USER_TYPE_OPTIONS,
} from './constants';
import UserAuthInfoDrawer from './components/user-auth-info-drawer.vue';
import UserResetPasswordModal from './components/user-reset-password-modal.vue';
import {
getUserProfileAvatar,
getUserProfileId,
getUserProfileName,
} from './shared';
defineOptions({ name: 'OperateUserManage' });
function createQuery() {
return {
account: '',
authType: '',
countryCode: '',
del: '0',
deviceId: '',
endCreateDate: '',
originPlatform: '',
pageIndex: 1,
pageSize: 20,
startCreateDate: '',
sysOrigins: [] as string[],
userId: '',
userSex: '',
userType: '',
};
}
const router = useRouter();
const route = useRoute();
const accessStore = useAccessStore();
const sysOriginOptions = computed(() => {
const options = getAllowedSysOrigins(accessStore.accessCodes || []);
return options.length > 0 ? options : getAllowedSysOrigins([]);
});
const activeTab = ref('table');
const showAllCondition = ref(false);
const loading = ref(false);
const total = ref(0);
const list = ref<Array<Record<string, any>>>([]);
const countries = ref<Array<Record<string, any>>>([]);
const countryKeyword = ref('');
const rangeDate = ref<[string, string] | null>(null);
const editOpen = ref(false);
const accountHandleOpen = ref(false);
const accountLogOpen = ref(false);
const violationOpen = ref(false);
const authInfoOpen = ref(false);
const resetPasswordOpen = ref(false);
const activeUserId = ref<number | string>('');
const activeUserRecord = ref<Record<string, any> | null>(null);
const query = reactive(createQuery());
const columns = [
{ dataIndex: 'userProfile', key: 'userProfile', title: '用户', width: 280 },
{ dataIndex: 'goldBalance', key: 'goldBalance', title: '金币余额', width: 120 },
{ dataIndex: 'diamondBalance', key: 'diamondBalance', title: '钻石余额', width: 120 },
{
dataIndex: 'salaryDiamondBalance',
key: 'salaryDiamondBalance',
title: '工资钻石余额',
width: 140,
},
{ dataIndex: 'accountStatus', key: 'accountStatus', title: '账户', width: 120 },
{ dataIndex: 'registerSource', key: 'registerSource', title: '注册来源', width: 180 },
{ dataIndex: 'time', key: 'time', title: '时间', width: 220 },
{ dataIndex: 'actions', key: 'actions', title: '操作', width: 260 },
];
const filteredCountries = computed(() => {
const keyword = countryKeyword.value.trim().toLowerCase();
if (!keyword) {
return countries.value;
}
return countries.value.filter((item) =>
[
item.phonePrefix,
item.countryName,
item.aliasName,
item.alphaTwo,
item.alphaThree,
]
.filter(Boolean)
.join(' ')
.toLowerCase()
.includes(keyword),
);
});
watch(
sysOriginOptions,
(options) => {
if (query.sysOrigins.length === 0 && options.length > 0) {
query.sysOrigins = [String(options[0]?.value || '')];
void loadData(true);
}
},
{ immediate: true },
);
watch(rangeDate, (value) => {
query.startCreateDate = value?.[0] || '';
query.endCreateDate = value?.[1] || '';
});
watch(
() => route.query.tab,
(value) => {
if (value === 'role' || value === 'table') {
activeTab.value = String(value);
}
},
{ immediate: true },
);
async function loadCountries() {
countries.value = (await getCountryAlls()) || [];
}
function buildPayload() {
return {
account: query.account,
authType: query.authType,
countryCode: query.countryCode,
del: query.del,
deviceId: query.deviceId,
endCreateDate: query.endCreateDate,
originPlatform: query.originPlatform,
pageIndex: query.pageIndex,
pageSize: query.pageSize,
startCreateDate: query.startCreateDate,
sysOrigins: query.sysOrigins,
userIds: query.userId ? [query.userId] : [],
userSex: query.userSex,
userType: query.userType,
};
}
async function loadData(reset = false) {
if (query.sysOrigins.length === 0) {
return;
}
if (reset) {
query.pageIndex = 1;
}
loading.value = true;
try {
const result = await pageUserBaseInfo(buildPayload());
list.value = result.records || [];
total.value = result.total || 0;
} finally {
loading.value = false;
}
}
function handlePageChange(page: number, pageSize: number) {
query.pageIndex = page;
query.pageSize = pageSize;
void loadData();
}
function resolveRegisterSource(record: Record<string, any>) {
const authType = record.userRegisterInfo?.authType || '';
const platform = record.userRegisterInfo?.originPlatform || '';
const authLabel =
REGISTER_ORIGIN_OPTIONS.find((item) => item.value === authType)?.name ||
authType ||
'-';
const platformLabel =
ORIGIN_PLATFORM_OPTIONS.find((item) => item.value === platform)?.name ||
platform ||
'-';
return `${authLabel} / ${platformLabel}`;
}
function getStatusColor(status?: string) {
if (status === 'NORMAL') {
return 'success';
}
if (status === 'FREEZE') {
return 'warning';
}
if (status === 'ARCHIVE') {
return 'error';
}
return 'default';
}
function openUserDetails(userId?: number | string) {
if (!userId) {
return;
}
router.push(`/common/user/deatils/${userId}`);
}
function getUserShortId(profile?: Record<string, any> | null) {
return profile?.actualAccount || profile?.account || '';
}
function formatListDate(value?: number | string) {
if (!value) {
return '-';
}
const date = dayjs(value);
if (!date.isValid()) {
return String(value);
}
return date.format('YYYY MM DD');
}
function openEdit(record: Record<string, any>) {
activeUserId.value = record.userProfile?.id || '';
editOpen.value = true;
}
function openAccountHandle(record: Record<string, any>) {
activeUserId.value = record.userProfile?.id || '';
accountHandleOpen.value = true;
}
function openAccountLog(record: Record<string, any>) {
activeUserId.value = record.userProfile?.id || '';
accountLogOpen.value = true;
}
function openViolation(record: Record<string, any>) {
activeUserId.value = record.userProfile?.id || '';
activeUserRecord.value = record;
violationOpen.value = true;
}
function openAuthInfo(record: Record<string, any>) {
activeUserId.value = record.userProfile?.id || '';
authInfoOpen.value = true;
}
function openResetPassword(record: Record<string, any>) {
activeUserId.value = record.userProfile?.id || '';
resetPasswordOpen.value = true;
}
function buildEncodedAccount(sysOrigin: string, userId: number | string) {
return `*type:USER,accountType:LONG,sysOrigin:${sysOrigin},content:${userId}*`;
}
function openRunningWater(record: Record<string, any>, kind: 'diamond' | 'gold') {
const userId = record.userProfile?.id;
if (!userId) {
return;
}
const sysOrigin =
record.userProfile?.sysOriginChild ||
record.userProfile?.originSys ||
query.sysOrigins[0] ||
'';
const encoded = buildEncodedAccount(sysOrigin, userId);
if (kind === 'gold') {
router.push({
path: '/operate/manager/running/water',
query: { userId: encoded },
});
return;
}
if (kind === 'diamond') {
router.push({
path: '/user/diamond-run-water',
query: { sysOrigin, userId: encoded },
});
}
}
async function copyUserId(value?: number | string) {
if (!value) {
return;
}
await navigator.clipboard.writeText(String(value));
message.success('复制成功');
}
void loadCountries();
</script>
<template>
<Page title="用户管理">
<Card>
<Tabs v-model:activeKey="activeTab">
<TabPane key="table" tab="用户列表" />
<TabPane key="role" tab="用户列表(旧 role 版)" />
</Tabs>
<div class="toolbar">
<Space wrap>
<SysOriginSelect v-model:value="query.del" style="width: 120px" @change="loadData(true)"
:options="sysOriginOptions"
></SysOriginSelect>
<template v-if="showAllCondition">
<Select option-label-prop="label"
v-model:value="query.authType"
allow-clear
placeholder="注册来源"
style="width: 140px"
:options="REGISTER_ORIGIN_OPTIONS.map((item) => ({ label: `${item.name}`, value: item.value as any }))"
/>
<Select option-label-prop="label"
v-model:value="query.originPlatform"
allow-clear
placeholder="来源平台"
style="width: 140px"
:options="ORIGIN_PLATFORM_OPTIONS.map((item) => ({ label: `${item.name}`, value: item.value as any }))"
/>
<Select option-label-prop="label"
v-model:value="query.userSex"
allow-clear
placeholder="性别"
style="width: 120px"
:options="GENDER_OPTIONS.map((item) => ({ label: `${item.name}`, value: item.value as any }))"
/>
<Select option-label-prop="label"
v-model:value="query.countryCode"
allow-clear
show-search
:filter-option="false"
placeholder="国家"
style="width: 220px"
@change="loadData(true)"
@search="countryKeyword = $event"
:options="filteredCountries.map((item) => ({ label: `${`${item.phonePrefix || ''} ${item.countryName || item.aliasName || ''}`}`, value: item.alphaTwo as any }))"
/>
<Select option-label-prop="label"
v-model:value="query.userType"
allow-clear
placeholder="账号类型"
style="width: 140px"
:options="USER_TYPE_OPTIONS.map((item) => ({ label: `${item.name}`, value: item.value as any }))"
/>
</template>
<DatePicker.RangePicker
v-model:value="rangeDate"
show-time
value-format="x"
/>
<Input v-model:value="query.userId" allow-clear placeholder="长UID" style="width: 180px" />
<Input v-model:value="query.account" allow-clear placeholder="短账号" style="width: 180px" />
<Input v-model:value="query.deviceId" allow-clear placeholder="设备ID" style="width: 180px" />
<Button :loading="loading" type="primary" @click="loadData(true)">搜索</Button>
<Checkbox v-model:checked="showAllCondition">显示所有条件</Checkbox>
</Space>
</div>
<Table
:columns="columns"
:data-source="list"
:loading="loading"
:pagination="false"
row-key="id"
:scroll="{ x: 1580 }"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'userProfile'">
<div class="user-cell">
<button
v-if="getUserProfileId(record.userProfile)"
type="button"
class="user-cell__avatar-button"
@click="openUserDetails(getUserProfileId(record.userProfile))"
>
<img
:src="getUserProfileAvatar(record.userProfile) || 'https://dummyimage.com/56x56/e2e8f0/64748b&text=U'"
alt=""
class="user-cell__avatar"
>
</button>
<div v-else class="user-cell__avatar-shell">
<img
:src="getUserProfileAvatar(record.userProfile) || 'https://dummyimage.com/56x56/e2e8f0/64748b&text=U'"
alt=""
class="user-cell__avatar"
>
</div>
<div class="user-cell__info">
<div class="user-cell__name">{{ getUserProfileName(record.userProfile) }}</div>
<button
type="button"
class="user-cell__copy"
@click="copyUserId(getUserProfileId(record.userProfile))"
>
ID{{ getUserProfileId(record.userProfile) || '-' }}
</button>
<button
type="button"
class="user-cell__copy"
@click="copyUserId(getUserShortId(record.userProfile))"
>
短ID{{ getUserShortId(record.userProfile) || '-' }}
</button>
</div>
</div>
</template>
<template v-else-if="column.key === 'goldBalance'">
<Button size="small" type="link" @click="openRunningWater(record, 'gold')">
{{ record.goldBalance || 0 }}
</Button>
</template>
<template v-else-if="column.key === 'diamondBalance'">
<Button size="small" type="link" @click="openRunningWater(record, 'diamond')">
{{ record.diamondBalance || 0 }}
</Button>
</template>
<template v-else-if="column.key === 'salaryDiamondBalance'">
{{ record.salaryDiamondBalance || 0 }}
</template>
<template v-else-if="column.key === 'accountStatus'">
<Tag :color="getStatusColor(record.userProfile?.accountStatus)">
{{ record.userProfile?.accountStatusName || '-' }}
</Tag>
</template>
<template v-else-if="column.key === 'registerSource'">
{{ resolveRegisterSource(record) }}
</template>
<template v-else-if="column.key === 'time'">
<div>创建时间{{ formatListDate(record.userProfile?.createTime) }}</div>
<div>最近活跃{{ formatListDate(record.lastActiveTime) }}</div>
</template>
<template v-else-if="column.key === 'actions'">
<Space wrap>
<Button size="small" type="link" @click="openUserDetails(record.userProfile?.id)">
资料详情
</Button>
<Button size="small" type="link" @click="openEdit(record)">
编辑用户
</Button>
<Button size="small" type="link" @click="openViolation(record)">
违规记录
</Button>
<Button size="small" type="link" @click="openAccountHandle(record)">
账号处理
</Button>
<Button size="small" type="link" @click="openAccountLog(record)">
处理记录
</Button>
<Button size="small" type="link" @click="openAuthInfo(record)">
认证信息
</Button>
<Button
v-if="activeTab === 'table'"
size="small"
type="link"
@click="openResetPassword(record)"
>
重置密码
</Button>
</Space>
</template>
</template>
</Table>
<div class="pager">
<Pagination
:current="query.pageIndex"
:page-size="query.pageSize"
:total="total"
show-size-changer
@change="handlePageChange"
@showSizeChange="handlePageChange"
/>
</div>
</Card>
<EditUserDrawer
:open="editOpen"
:user-id="activeUserId"
@close="editOpen = false"
@success="editOpen = false; loadData()"
/>
<AccountHandleModal
:open="accountHandleOpen"
:user-id="activeUserId"
@close="accountHandleOpen = false"
@success="accountHandleOpen = false; loadData()"
/>
<AccountStatusLogModal
:open="accountLogOpen"
:user-id="activeUserId"
@close="accountLogOpen = false"
/>
<ViolationHistoryModal
:open="violationOpen"
:user-avatar="activeUserRecord?.userProfile?.userAvatar"
:user-id="activeUserId"
:user-nickname="activeUserRecord?.userProfile?.userNickname"
:vip-status-name="activeUserRecord?.userProfile?.vipStatusName"
@close="violationOpen = false"
/>
<UserAuthInfoDrawer
:open="authInfoOpen"
:user-id="activeUserId"
@close="authInfoOpen = false"
/>
<UserResetPasswordModal
:open="resetPasswordOpen"
:user-id="activeUserId"
@close="resetPasswordOpen = false"
@success="resetPasswordOpen = false"
/>
</Page>
</template>
<style scoped>
.toolbar {
margin-bottom: 16px;
}
.user-cell {
align-items: center;
display: flex;
gap: 12px;
}
.user-cell__avatar-button,
.user-cell__avatar-shell {
align-items: center;
background: transparent;
border: 0;
display: inline-flex;
flex-shrink: 0;
justify-content: center;
padding: 0;
}
.user-cell__avatar-button {
cursor: pointer;
}
.user-cell__avatar {
background: #e2e8f0;
border-radius: 999px;
height: 40px;
object-fit: cover;
width: 40px;
}
.user-cell__info {
display: grid;
gap: 4px;
min-width: 0;
}
.user-cell__name {
color: #0f172a;
font-weight: 600;
word-break: break-all;
}
.user-cell__copy {
background: transparent;
border: 0;
color: #2563eb;
cursor: pointer;
padding: 0;
text-align: left;
}
.pager {
display: flex;
justify-content: flex-end;
margin-top: 16px;
}
</style>