Files
olons_admin/web/src/views/app-member/list/index.vue
T

1210 lines
41 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.
<template>
<div>
<div class="n-layout-page-header">
<n-card :bordered="false" title="会员列表"
>会员资料默认脱敏展示;余额修改会自动保留账变流水。</n-card
>
</div>
<n-card :bordered="false" class="proCard">
<BasicForm
ref="searchFormRef"
@register="register"
@submit="reloadTable"
@reset="reloadTable"
@keyup.enter="reloadTable"
/>
<div ref="memberTableWrapRef" class="member-table-wrap">
<BasicTable
ref="actionRef"
:columns="columns"
:request="loadDataTable"
:row-key="(row) => row.id"
:action-column="actionColumn"
:scroll-x="scrollX"
:resize-height-offset="-10000"
/>
<div
class="action-column-scrollbar"
aria-label="操作栏横向滚动条"
@scroll="syncActionScroll"
>
<div class="action-column-scrollbar-content"></div>
</div>
</div>
</n-card>
<n-modal
v-model:show="showEdit"
preset="card"
title="编辑会员"
class="edit-modal"
>
<n-spin :show="editLoading">
<n-form
ref="editFormRef"
:model="editForm"
:rules="editRules"
label-placement="left"
label-width="88"
>
<div class="edit-form-grid">
<n-form-item label="用户名称" path="username" required
><n-input v-model:value="editForm.username" placeholder="请输入用户名称"
/></n-form-item>
<n-form-item label="手机号码" path="mobile"
><n-input
v-model:value="editForm.mobile"
:placeholder="editForm.currentMobile || '留空不修改'"
/></n-form-item>
<n-form-item label="账户余额" path="availableAmountYuan"
><n-input-number
v-model:value="editForm.availableAmountYuan"
:precision="2"
class="form-number"
placeholder="请输入账户余额"
/></n-form-item>
<n-form-item label="冻结金额"
><n-input :value="money(editForm.frozenAmount)" disabled
/></n-form-item>
<n-form-item label="会员等级"
><n-select
v-model:value="editForm.levelId"
:options="memberLevelOptions"
placeholder="请选择会员等级"
/></n-form-item>
<n-form-item label="派单模式"
><n-select
v-model:value="editForm.autoDispatch"
:options="dict.getOptionUnRef('appMemberDispatchMode')"
/></n-form-item>
<n-form-item label="交易状态"
><n-select
v-model:value="editForm.riskStatus"
:options="dict.getOptionUnRef('appMemberRiskStatus')"
/></n-form-item>
<n-form-item label="登录密码" path="password"
><n-input
v-model:value="editForm.password"
type="password"
show-password-on="click"
placeholder="留空不修改"
/></n-form-item>
<n-form-item label="交易密码" path="tradePassword"
><n-input
v-model:value="editForm.tradePassword"
type="password"
show-password-on="click"
placeholder="留空不修改"
/></n-form-item>
<n-form-item label="上级ID"
><n-input-number v-model:value="editForm.parentId" :min="0" class="form-number"
/></n-form-item>
<n-form-item label="信用分"
><n-input-number
v-model:value="editForm.creditScore"
:min="0"
:max="1000"
class="form-number"
/></n-form-item>
<n-form-item class="edit-mark-item">
<n-alert type="warning" :show-icon="true" class="edit-mark-alert">
<n-checkbox
:checked="editForm.isTestAccount === 1"
@update:checked="editForm.isTestAccount = $event ? 1 : 2"
>标记为假人账号</n-checkbox
>
<div class="edit-mark-hint">标记后该会员将按模拟账号处理,请确认后再保存。</div>
</n-alert>
</n-form-item>
</div>
</n-form>
</n-spin>
<template #action
><n-space justify="end"
><n-button type="primary" :loading="saving" @click="saveMember">确定</n-button
><n-button @click="showEdit = false">取消</n-button></n-space
></template
>
</n-modal>
<n-modal v-model:show="showDetail" preset="card" title="会员详情" class="member-modal">
<n-spin :show="detailLoading">
<n-descriptions v-if="detail.id" bordered :column="2" label-placement="left">
<n-descriptions-item label="会员"
>{{ detail.username }}(#{{ detail.id }})</n-descriptions-item
>
<n-descriptions-item label="会员编号">{{ detail.memberNo }}</n-descriptions-item>
<n-descriptions-item label="手机号">{{ detail.mobile }}</n-descriptions-item>
<n-descriptions-item label="真实姓名">{{
detail.realNameMask || '-'
}}</n-descriptions-item>
<n-descriptions-item label="证件号">{{ detail.idNoMask || '-' }}</n-descriptions-item>
<n-descriptions-item label="团队层级">{{ detail.level }}</n-descriptions-item>
<n-descriptions-item label="可用余额">{{
money(detail.availableAmount)
}}</n-descriptions-item>
<n-descriptions-item label="冻结余额">{{
money(detail.frozenAmount)
}}</n-descriptions-item>
<n-descriptions-item label="收款账户"
>{{ detail.paymentAccountCount }} 个</n-descriptions-item
>
<n-descriptions-item label="收货地址">{{ detail.addressCount }} 个</n-descriptions-item>
<n-descriptions-item label="下级团队">{{ detail.teamCount }} 人</n-descriptions-item>
<n-descriptions-item label="注册时间">{{ detail.createdAt }}</n-descriptions-item>
<n-descriptions-item label="注册域名">{{ detail.registerDomain || '待绑定' }}</n-descriptions-item>
<n-descriptions-item label="域名状态">{{ dict.getLabel('appMemberDomainStatus', detail.domainStatus) }}</n-descriptions-item>
<n-descriptions-item label="绑定时间">{{ detail.domainBoundAt || '-' }}</n-descriptions-item>
</n-descriptions>
<n-divider />
<n-space>
<n-button v-if="hasPermission(['/appMember/paymentList'])" @click="openRelated('payment')"
>收款账户</n-button
>
<n-button v-if="hasPermission(['/appMember/addressList'])" @click="openRelated('address')"
>收货地址</n-button
>
<n-button v-if="hasPermission(['/appMember/team'])" @click="openTeam">团队查询</n-button>
<n-button
v-if="hasPermission(['/appMember/sensitive'])"
type="warning"
@click="openSensitive('member', detail.id)"
>授权查看明文</n-button
>
</n-space>
</n-spin>
</n-modal>
<n-modal
v-model:show="showRelated"
preset="card"
:title="relatedType === 'payment' ? '编辑银行卡信息' : '编辑收货地址信息'"
class="related-modal"
>
<n-spin :show="relatedLoading">
<n-form label-placement="left" label-width="110">
<template v-if="relatedType === 'payment'">
<n-alert v-if="paymentForm.id" type="info" :show-icon="false">
当前默认显示脱敏资料;留空保存不会覆盖原敏感字段或收款码。
<n-button
v-if="hasPermission(['/appMember/sensitive'])"
text
type="primary"
@click="openSensitive('payment', paymentForm.id)"
>授权查看明文</n-button>
</n-alert>
<n-form-item label="开户人"
><n-input v-model:value="paymentForm.holder" :placeholder="paymentForm.holderMask || '留空不修改'"
/></n-form-item>
<n-form-item label="身份证号"
><n-input v-model:value="paymentForm.identityNo" :placeholder="paymentForm.identityNoMask || '留空不修改'"
/></n-form-item>
<n-form-item label="开户银行"
><n-input v-model:value="paymentForm.bankName" placeholder="请输入开户银行"
/></n-form-item>
<n-form-item label="开户电话"
><n-input v-model:value="paymentForm.mobile" :placeholder="paymentForm.mobileMask || '留空不修改'"
/></n-form-item>
<n-form-item label="银行卡号"
><n-input v-model:value="paymentForm.bankAccount" :placeholder="paymentForm.accountNoMask || '留空不修改'"
/></n-form-item>
<n-form-item label="数字钱包"
><n-input v-model:value="paymentForm.digitalWallet" :placeholder="paymentForm.digitalRmbCodeMask || '留空不修改'"
/></n-form-item>
<n-form-item label="收款码">
<n-grid :cols="2" :x-gap="16">
<n-gi
><div class="upload-title">支付宝收款码</div
><UploadImage v-model:value="paymentForm.alipayQrCode" :max-number="1"
/></n-gi>
<n-gi
><div class="upload-title">微信收款码</div
><UploadImage v-model:value="paymentForm.wechatQrCode" :max-number="1"
/></n-gi>
</n-grid>
</n-form-item>
</template>
<template v-else>
<n-form-item label="真实姓名"
><n-input v-model:value="addressForm.receiver" placeholder="请输入真实姓名"
/></n-form-item>
<n-form-item label="联系电话"
><n-input v-model:value="addressForm.mobile" placeholder="请输入联系电话"
/></n-form-item>
<n-form-item label="地区"
><n-input v-model:value="addressForm.region" placeholder="请输入地区"
/></n-form-item>
<n-form-item label="详细地址"
><n-input v-model:value="addressForm.address" placeholder="请输入详细地址"
/></n-form-item>
</template>
</n-form>
</n-spin>
<template #action
><n-space justify="end"
><n-button type="primary" :loading="relatedSaving" @click="saveRelated">确定</n-button
><n-button @click="showRelated = false">取消</n-button></n-space
></template
>
</n-modal>
<n-modal v-model:show="showTeam" preset="card" title="团队查询" class="member-modal">
<n-space vertical>
<n-space>
<n-button type="error" @click="openStatus({ id: detail.id, memberStatus: 1 })"
>封禁</n-button
>
<n-button type="error" @click="submitTeamStatus(2)">封禁团队</n-button>
<n-button type="success" @click="submitTeamStatus(1)">解封团队</n-button>
</n-space>
<n-space
><span>查询层级</span
><n-input-number v-model:value="teamLevel" :min="1" :max="10" /><n-button
type="primary"
@click="loadTeam"
>查询</n-button
></n-space
>
<n-data-table :columns="teamColumns" :data="teamRows" :bordered="false" />
</n-space>
</n-modal>
<n-modal
v-model:show="showLedger"
preset="card"
:title="`账变记录 - ${ledgerMember?.username || ''}`"
class="member-modal"
>
<n-data-table
:columns="ledgerColumns"
:data="ledgerRows"
:loading="ledgerLoading"
:bordered="false"
/>
<n-empty v-if="!ledgerLoading && ledgerRows.length === 0" description="暂无账变记录" />
</n-modal>
<n-modal
v-model:show="showSensitive"
preset="dialog"
title="授权查看敏感信息"
:show-icon="false"
>
<n-alert type="error"
>明文仅用于当前业务目的,请勿复制传播。本次操作将记录查看人、对象、用途和结果。</n-alert
>
<n-form label-placement="top">
<n-form-item label="用途编码"
><n-input
v-model:value="sensitiveForm.purposeCode"
placeholder="如 customer_service_verify"
/></n-form-item>
<n-form-item label="用途说明"
><n-input v-model:value="sensitiveForm.purposeText" type="textarea"
/></n-form-item>
<n-checkbox v-model:checked="sensitiveForm.confirm">我确认业务需要并接受留痕</n-checkbox>
</n-form>
<n-code v-if="sensitiveResult" :code="sensitiveResult" language="json" word-wrap />
<template #action
><n-space
><n-button @click="showSensitive = false">关闭</n-button
><n-button type="error" @click="revealSensitive">确认查看</n-button></n-space
></template
>
</n-modal>
<n-modal v-model:show="showStatus" preset="dialog" title="信息" :show-icon="false">
<div>确定{{ statusForm.toStatus === 1 ? '启用' : '禁用' }}此用户吗?</div>
<template #action
><n-space
><n-button type="primary" @click="submitStatus">确定</n-button
><n-button @click="showStatus = false">取消</n-button></n-space
></template
>
</n-modal>
<n-modal v-model:show="showWallet" preset="dialog" title="奖励金" :show-icon="false">
<n-form label-placement="left" label-width="70"
><n-form-item label="金额"
><n-input-number
v-model:value="walletYuan"
:min="0.01"
:precision="2"
class="form-number"
placeholder="请输入赠送的金额" /></n-form-item
></n-form>
<template #action
><n-space
><n-button type="primary" @click="submitWallet">确定</n-button
><n-button @click="showWallet = false">取消</n-button></n-space
></template
>
</n-modal>
<n-modal v-model:show="showNotice" preset="card" title="发送弹幕" class="related-modal">
<n-form label-placement="left" label-width="100">
<n-form-item label="用户ID"
><n-input :value="String(noticeMember?.id || '')" disabled
/></n-form-item>
<n-form-item label="用户名"
><n-input :value="noticeMember?.username || ''" disabled
/></n-form-item>
<n-form-item label="用户手机"
><n-input :value="noticeMember?.mobile || ''" disabled
/></n-form-item>
<n-form-item label="发送内容"
><n-input v-model:value="noticeForm.content" placeholder="请输入发送内容"
/></n-form-item>
</n-form>
<template #action
><n-space
><n-button type="primary" @click="submitNotice">确定</n-button
><n-button @click="showNotice = false">取消</n-button></n-space
></template
>
</n-modal>
</div>
</template>
<script lang="ts" setup>
import { computed, h, onMounted, onUpdated, reactive, ref } from 'vue';
import { NButton, NSpace, NSwitch, useDialog, useMessage } from 'naive-ui';
import { DeleteOutlined, EditOutlined } from '@vicons/antd';
import { BasicTable, TableAction } from '@/components/Table';
import { BasicForm, useForm } from '@/components/Form';
import UploadImage from '@/components/Upload/uploadImage.vue';
import { usePermission } from '@/hooks/web/usePermission';
import { useDictStore } from '@/store/modules/dict';
import { adaTableScrollX } from '@/utils/hotgo';
import {
List,
View,
Edit,
Delete,
AutoDispatch,
Category,
PaymentList,
PaymentEdit,
PaymentDelete,
AddressList,
AddressEdit,
AddressDelete,
Sensitive,
Team,
StatusRequest,
LedgerList,
WalletRequest,
NoticeEdit,
} from '@/api/appMember';
import { List as MemberLevelList } from '@/api/memberLevel';
import { columns as baseColumns, schemas, loadOptions, MemberRecord } from './model';
const message = useMessage(),
dialog = useDialog(),
dict = useDictStore();
const { hasPermission } = usePermission();
const dispatchUpdatingIds = ref(new Set<number>());
const columns = baseColumns.map((column) =>
column.key === 'autoDispatch'
? {
...column,
render: (row: MemberRecord) =>
h(
NSwitch,
{
value: row.autoDispatch,
checkedValue: 1,
uncheckedValue: 2,
loading: dispatchUpdatingIds.value.has(row.id),
disabled: !hasPermission(['/appMember/autoDispatch']),
'onUpdate:value': (value: number) => toggleDispatch(row, value),
},
{ checked: () => '自动', unchecked: () => '手动' }
),
}
: column
);
const actionRef = ref(),
searchFormRef = ref<any>({}),
editFormRef = ref<any>(),
memberTableWrapRef = ref<HTMLElement | null>(null);
const [register] = useForm({
gridProps: { cols: '1 s:1 m:2 l:3 xl:4 2xl:4' },
labelWidth: 80,
schemas,
});
const showEdit = ref(false),
showDetail = ref(false),
showRelated = ref(false),
showRelatedForm = ref(false),
showTeam = ref(false),
showLedger = ref(false),
showSensitive = ref(false),
showStatus = ref(false),
showWallet = ref(false),
showNotice = ref(false),
saving = ref(false),
detailLoading = ref(false),
ledgerLoading = ref(false),
editLoading = ref(false),
relatedLoading = ref(false),
relatedSaving = ref(false);
const detail = ref<any>({}),
relatedRows = ref<any[]>([]),
teamRows = ref<any[]>([]),
relatedType = ref<'payment' | 'address'>('payment'),
teamLevel = ref(3),
sensitiveResult = ref(''),
walletYuan = ref<number | null>(null),
memberLevelOptions = ref<any[]>([]),
noticeMember = ref<MemberRecord | null>(null);
const editForm = reactive<any>({}),
paymentForm = reactive<any>({}),
addressForm = reactive<any>({}),
sensitiveForm = reactive<any>({
targetType: 'member',
targetId: 0,
purposeCode: '',
purposeText: '',
confirm: false,
}),
statusForm = reactive<any>({ memberId: 0, toStatus: 2, reasonCode: '', reasonText: '' }),
walletForm = reactive<any>({
memberId: 0,
adjustType: 1,
direction: 1,
reasonCode: '',
reasonText: '',
});
const editRules = {
username: [
{ required: true, message: '请输入用户名称', trigger: ['input', 'blur'] },
{ min: 3, max: 64, message: '用户名长度为3到64个字符', trigger: ['input', 'blur'] },
],
mobile: {
validator: () => true,
trigger: ['input', 'blur'],
},
availableAmountYuan: {
validator: (_rule, value) =>
value !== null && Number.isFinite(Number(value))
? true
: new Error('账户余额不能为空'),
trigger: ['input', 'blur'],
},
password: {
validator: (_rule, value) => {
const text = String(value || '');
if (!text) return true;
if (text.length < 6 || text.length > 64)
return new Error('密码长度为6到64个字符');
return true;
},
trigger: ['input', 'blur'],
},
tradePassword: {
validator: (_rule, value) =>
!value || String(value).length >= 6 ? true : new Error('交易密码不能少于6位'),
trigger: ['input', 'blur'],
},
};
const noticeForm = reactive({ memberId: 0, title: '', content: '', sourceType: 2 });
const noticeDateRange = ref<[string, string]>(['', '']);
const ledgerRows = ref<any[]>([]),
ledgerMember = ref<MemberRecord | null>(null);
const reset = (obj, value) => {
Object.keys(obj).forEach((k) => delete obj[k]);
Object.assign(obj, value);
};
const loadDataTable = (res) => List({ ...searchFormRef.value?.formModel, ...res });
const reloadTable = () => actionRef.value?.reload();
const money = (v) => `¥${((v || 0) / 100).toFixed(2)}`;
const actionColumn = reactive({
width: 180,
title: '操作',
key: 'action',
fixed: 'right',
render(record: MemberRecord) {
return h('div', { class: 'member-action-viewport' }, [
h('div', { class: 'member-action-grid' }, [
h(TableAction as any, {
style: 'button',
actions: [
{
label: '详情',
type: 'default',
onClick: () => openDetail(record.id),
auth: ['/appMember/view'],
},
{
label: '编辑',
type: 'primary',
icon: EditOutlined,
onClick: () => openEdit(record),
auth: ['/appMember/edit'],
},
{
label: '删除',
type: 'error',
icon: DeleteOutlined,
onClick: () => removeMember(record),
auth: ['/appMember/delete'],
},
{
label: '银行卡信息',
type: 'primary',
onClick: () => openRelated('payment', record.id),
auth: ['/appMember/paymentList'],
},
{
label: '地址信息',
type: 'primary',
onClick: () => openRelated('address', record.id),
auth: ['/appMember/addressList'],
},
{
label: '查看团队',
type: 'warning',
onClick: () => openTeam(record.id),
auth: ['/appMember/team'],
},
],
}),
h(TableAction as any, {
style: 'button',
actions: [
{
label: '账变',
type: 'warning',
onClick: () => openLedger(record),
auth: ['/appMember/walletRequest'],
},
{
label: '弹幕',
color: '#009688',
onClick: () => openNotice(record),
auth: ['/appMember/noticeEdit'],
},
{
label: '奖励金',
color: '#008f83',
onClick: () => openWallet(record, true),
auth: ['/appMember/walletRequest'],
},
{
label: record.memberCategory === 2 ? '设为真人' : '设为假人',
type: 'error',
onClick: () => toggleCategory(record),
auth: ['/appMember/edit'],
},
{
label: record.memberStatus === 2 ? '启用' : '禁用',
type: record.memberStatus === 2 ? 'success' : 'error',
onClick: () => openStatus(record),
auth: ['/appMember/statusRequest'],
},
],
}),
]),
]);
},
});
const scrollX = computed(() => adaTableScrollX(columns, actionColumn.width));
let actionScrollLeft = 0;
function syncActionViewports(left: number) {
memberTableWrapRef.value
?.querySelectorAll<HTMLElement>('.member-action-viewport')
.forEach((el) => {
el.scrollLeft = left;
});
}
function syncActionScroll(event: Event) {
const el = event.currentTarget as HTMLElement;
const externalMax = el.scrollWidth - el.clientWidth;
const viewport =
memberTableWrapRef.value?.querySelector<HTMLElement>('.member-action-viewport');
const actionMax = viewport ? viewport.scrollWidth - viewport.clientWidth : 0;
actionScrollLeft = externalMax > 0 ? (el.scrollLeft * actionMax) / externalMax : 0;
syncActionViewports(actionScrollLeft);
}
async function openEdit(row: MemberRecord) {
reset(editForm, {
id: row?.id || 0,
username: row?.username || '',
mobile: '',
currentMobile: row?.mobile || '',
password: '',
tradePassword: '',
parentId: row?.parentId || 0,
levelId: 0,
riskStatus: row?.riskStatus || 1,
creditScore: 100,
autoDispatch: row?.autoDispatch || 1,
memberCategory: row?.memberCategory || 1,
isTestAccount: row?.isTestAccount || 2,
availableAmount: row?.availableAmount || 0,
availableAmountYuan: (row?.availableAmount || 0) / 100,
frozenAmount: row?.frozenAmount || 0,
});
showEdit.value = true;
editLoading.value = true;
try {
const data: any = await View({ id: row.id });
Object.assign(editForm, {
levelId: data.levelId || 0,
riskStatus: data.riskStatus || 1,
creditScore: data.creditScore ?? 100,
availableAmount: data.availableAmount || 0,
availableAmountYuan: (data.availableAmount || 0) / 100,
frozenAmount: data.frozenAmount || 0,
currentMobile: data.mobile || row.mobile,
});
} finally {
editLoading.value = false;
}
}
async function saveMember() {
try {
await editFormRef.value?.validate();
} catch {
return;
}
saving.value = true;
try {
editForm.memberCategory = editForm.isTestAccount === 1 ? 2 : 1;
await Edit({
...editForm,
availableAmount: Math.round(Number(editForm.availableAmountYuan) * 100),
});
message.success('保存成功');
showEdit.value = false;
reloadTable();
} finally {
saving.value = false;
}
}
async function openDetail(id) {
showDetail.value = true;
detailLoading.value = true;
try {
detail.value = await View({ id });
} finally {
detailLoading.value = false;
}
}
async function toggleDispatch(row: MemberRecord, nextValue?: number) {
if (dispatchUpdatingIds.value.has(row.id)) return;
const autoDispatch = nextValue ?? (row.autoDispatch === 1 ? 2 : 1);
dispatchUpdatingIds.value.add(row.id);
try {
await AutoDispatch({ id: row.id, autoDispatch });
row.autoDispatch = autoDispatch;
message.success(`已切换为${autoDispatch === 1 ? '自动' : '手动'}派单`);
} finally {
dispatchUpdatingIds.value.delete(row.id);
}
}
function openStatus(row) {
const action = row.memberStatus === 1 ? '禁用' : '启用';
reset(statusForm, {
memberId: row.id,
toStatus: row.memberStatus === 1 ? 2 : 1,
reasonCode: 'manual_operation',
reasonText: `后台管理员${action}会员`,
});
showStatus.value = true;
}
async function submitStatus() {
await StatusRequest(statusForm);
message.success('操作已提交');
showStatus.value = false;
}
function openWallet(row, rewardOnly = false) {
reset(walletForm, {
memberId: row.id,
adjustType: rewardOnly ? 1 : 2,
direction: 1,
reasonCode: 'member_reward',
reasonText: '后台奖励金',
});
walletYuan.value = null;
showWallet.value = true;
}
async function submitWallet() {
if (walletYuan.value === null || walletYuan.value <= 0) {
message.warning('请输入大于0的金额');
return;
}
const params = {
...walletForm,
amount: Math.round(walletYuan.value * 100),
idempotencyKey: `wallet-${walletForm.memberId}-${Date.now()}`,
};
const r: any = await WalletRequest(params);
message.success(`审批已提交:${r.approvalNo}`);
showWallet.value = false;
}
function removeMember(row) {
dialog.warning({
title: '信息',
content: '确定删除此记录吗?',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: async () => {
await Delete({ id: row.id, reason: '后台管理员删除' });
message.success('会员已删除');
reloadTable();
},
});
}
async function openRelated(type: 'payment' | 'address', memberId = detail.value.id) {
detail.value = { ...detail.value, id: memberId };
relatedType.value = type;
showRelated.value = true;
showRelatedForm.value = false;
relatedLoading.value = true;
try {
await loadRelated();
if (type === 'payment') {
const byType = (accountType: number) =>
relatedRows.value.find((item) => item.accountType === accountType);
const bank = byType(1);
reset(paymentForm, {
memberId,
id: bank?.id || 0,
holder: '',
holderMask: bank?.holderMask || '',
identityNo: '',
identityNoMask: bank?.identityNoMask || '',
bankName: bank?.bankName || '',
mobile: '',
mobileMask: bank?.mobileMask || '',
bankAccount: '',
accountNoMask: bank?.accountNoMask || '',
digitalWallet: '',
digitalRmbCodeMask: bank?.digitalRmbCodeMask || '',
alipayQrCode: bank?.alipayQrUrl || '',
wechatQrCode: bank?.wechatQrUrl || '',
alipayQrAttachmentId: bank?.alipayQrAttachmentId || 0,
wechatQrAttachmentId: bank?.wechatQrAttachmentId || 0,
});
} else {
const row = relatedRows.value.find((item) => item.isDefault === 1) || relatedRows.value[0];
const region = [row?.province, row?.city, row?.district].filter(Boolean).join('');
reset(addressForm, {
id: row?.id || 0,
memberId,
receiver: '',
mobile: '',
region,
originalRegion: region,
province: row?.province || '',
city: row?.city || '',
district: row?.district || '',
address: '',
isDefault: 1,
});
}
} finally {
relatedLoading.value = false;
}
}
async function loadRelated() {
const r: any =
relatedType.value === 'payment'
? await PaymentList({ memberId: detail.value.id })
: await AddressList({ memberId: detail.value.id });
relatedRows.value = r.list || [];
}
function startRelatedAdd() {
showRelatedForm.value = true;
if (relatedType.value === 'payment')
reset(paymentForm, {
id: 0,
memberId: detail.value.id,
accountType: 1,
bankName: '',
holder: '',
mobile: '',
accountNo: '',
verifyStatus: 1,
isDefault: 2,
});
else
reset(addressForm, {
id: 0,
memberId: detail.value.id,
receiver: '',
mobile: '',
province: '',
city: '',
district: '',
address: '',
isDefault: 2,
});
}
async function saveRelated() {
relatedSaving.value = true;
try {
if (relatedType.value === 'payment') {
await PaymentEdit({
id: paymentForm.id,
memberId: paymentForm.memberId,
accountType: 1,
bankName: paymentForm.bankName,
holder: paymentForm.holder,
identityNo: paymentForm.identityNo,
mobile: paymentForm.mobile,
accountNo: paymentForm.bankAccount,
digitalRmbCode: paymentForm.digitalWallet,
alipayQrUrl: paymentForm.alipayQrCode,
wechatQrUrl: paymentForm.wechatQrCode,
alipayQrAttachmentId: paymentForm.alipayQrAttachmentId,
wechatQrAttachmentId: paymentForm.wechatQrAttachmentId,
verifyStatus: 1,
isDefault: 1,
});
} else {
const regionChanged = addressForm.region !== addressForm.originalRegion;
await AddressEdit({
...addressForm,
province: regionChanged ? addressForm.region : addressForm.province,
city: regionChanged ? '' : addressForm.city,
district: regionChanged ? '' : addressForm.district,
});
}
message.success('保存成功');
showRelated.value = false;
} finally {
relatedSaving.value = false;
}
}
async function deleteRelated(id) {
if (relatedType.value === 'payment') await PaymentDelete({ id });
else await AddressDelete({ id });
message.success('删除成功');
await loadRelated();
}
function editRelated(row) {
showRelatedForm.value = true;
if (relatedType.value === 'payment')
reset(paymentForm, {
id: row.id,
memberId: row.memberId,
accountType: row.accountType,
bankName: row.bankName,
holder: '',
mobile: '',
accountNo: '',
verifyStatus: row.verifyStatus,
isDefault: row.isDefault,
});
else
reset(addressForm, {
id: row.id,
memberId: row.memberId,
receiver: '',
mobile: '',
province: row.province,
city: row.city,
district: row.district,
address: '',
isDefault: row.isDefault,
});
}
function openSensitive(type, targetId) {
reset(sensitiveForm, {
targetType: type,
targetId,
purposeCode: '',
purposeText: '',
confirm: false,
});
sensitiveResult.value = '';
showSensitive.value = true;
}
async function revealSensitive() {
const r: any = await Sensitive(sensitiveForm);
sensitiveResult.value = JSON.stringify({ accessNo: r.accessNo, ...r.fields }, null, 2);
}
async function openTeam(memberId = detail.value.id) {
detail.value = { ...detail.value, id: memberId };
showTeam.value = true;
await loadTeam();
}
async function loadTeam() {
const r: any = await Team({
memberId: detail.value.id,
maxLevel: teamLevel.value,
page: 1,
pageSize: 100,
});
teamRows.value = r.list || [];
}
function submitTeamStatus(toStatus: number) {
const action = toStatus === 2 ? '封禁' : '解封';
const targets = teamRows.value.filter((row) => row.memberStatus !== toStatus);
if (targets.length === 0) {
message.warning(`没有可${action}的团队成员`);
return;
}
dialog.warning({
title: `${action}团队`,
content: `确定提交 ${targets.length} 名团队成员的${action}审批吗?`,
positiveText: '确定提交',
negativeText: '取消',
onPositiveClick: async () => {
for (const row of targets) {
await StatusRequest({
memberId: row.id,
toStatus,
reasonCode: 'risk_control',
reasonText: `后台批量${action}团队`,
});
}
message.success(`${action}团队审批已提交`);
showTeam.value = false;
},
});
}
async function openLedger(row: MemberRecord) {
ledgerMember.value = row;
showLedger.value = true;
ledgerLoading.value = true;
try {
const r: any = await LedgerList({ memberId: row.id, page: 1, pageSize: 100 });
ledgerRows.value = r.list || [];
} finally {
ledgerLoading.value = false;
}
}
function toggleCategory(row: MemberRecord) {
const next = row.memberCategory === 2 ? 1 : 2;
dialog.warning({
title: '信息',
content: `确定设为${next === 2 ? '假人' : '真人'}吗?`,
positiveText: '确定',
negativeText: '取消',
onPositiveClick: async () => {
await Category({ id: row.id, memberCategory: next });
message.success('会员类别已更新');
reloadTable();
},
});
}
function formatDate(value: Date) {
const pad = (n: number) => String(n).padStart(2, '0');
return `${value.getFullYear()}-${pad(value.getMonth() + 1)}-${pad(value.getDate())} ${pad(value.getHours())}:${pad(value.getMinutes())}:${pad(value.getSeconds())}`;
}
function openNotice(row) {
const start = new Date(),
end = new Date(start.getTime() + 30 * 24 * 60 * 60 * 1000);
noticeMember.value = row;
reset(noticeForm, { memberId: row.id, title: '会员弹幕', content: '', sourceType: 2 });
noticeDateRange.value = [formatDate(start), formatDate(end)];
showNotice.value = true;
}
async function submitNotice() {
if (!noticeForm.content.trim()) {
message.warning('请填写弹幕内容');
return;
}
await NoticeEdit({
...noticeForm,
startAt: noticeDateRange.value[0],
endAt: noticeDateRange.value[1],
});
message.success('弹幕已发送');
showNotice.value = false;
}
const relatedColumns = computed(() =>
relatedType.value === 'payment'
? [
{
title: '类型',
key: 'accountType',
render: (r) => dict.getLabel('paymentAccountType', r.accountType),
},
{ title: '持有人', key: 'holderMask' },
{ title: '账号', key: 'accountNoMask' },
{ title: '银行', key: 'bankName' },
{
title: '操作',
key: 'action',
render: (r) =>
h(NSpace, {}, () => [
h(NButton, { size: 'small', onClick: () => editRelated(r) }, () => '编辑'),
h(
NButton,
{ size: 'small', onClick: () => openSensitive('payment', r.id) },
() => '查看明文'
),
h(
NButton,
{ size: 'small', type: 'error', onClick: () => deleteRelated(r.id) },
() => '删除'
),
]),
},
]
: [
{ title: '收货人', key: 'receiverMask' },
{ title: '手机号', key: 'mobileMask' },
{ title: '地区', key: 'province', render: (r) => r.province + r.city + r.district },
{ title: '地址', key: 'addressMask' },
{
title: '操作',
key: 'action',
render: (r) =>
h(NSpace, {}, () => [
h(NButton, { size: 'small', onClick: () => editRelated(r) }, () => '编辑'),
h(
NButton,
{ size: 'small', onClick: () => openSensitive('address', r.id) },
() => '查看明文'
),
h(
NButton,
{ size: 'small', type: 'error', onClick: () => deleteRelated(r.id) },
() => '删除'
),
]),
},
]
);
const teamColumns = [
{ title: '层级', key: 'relativeLevel' },
{ title: '会员ID', key: 'id' },
{ title: '用户名', key: 'username' },
{ title: '手机号', key: 'mobile' },
{ title: '上级ID', key: 'parentId' },
{ title: '可用余额', key: 'availableAmount', render: (r) => money(r.availableAmount) },
{ title: '注册时间', key: 'createdAt' },
];
const ledgerColumns = [
{ title: '流水号', key: 'ledgerNo', width: 190 },
{ title: '业务类型', key: 'bizType', width: 130 },
{ title: '业务单号', key: 'bizNo', width: 170 },
{
title: '方向',
key: 'direction',
width: 80,
render: (r) => dict.getLabel('walletDirection', r.direction),
},
{ title: '发生金额', key: 'amount', width: 110, render: (r) => money(r.amount) },
{ title: '变更前', key: 'beforeAmount', width: 110, render: (r) => money(r.beforeAmount) },
{ title: '变更后', key: 'afterAmount', width: 110, render: (r) => money(r.afterAmount) },
{ title: '说明', key: 'remark', width: 180 },
{ title: '发生时间', key: 'occurredAt', width: 180 },
];
onMounted(async () => {
loadOptions();
const r: any = await MemberLevelList({ page: 1, pageSize: 100, status: 1 });
memberLevelOptions.value = (r.list || []).map((item) => ({ label: item.name, value: item.id }));
});
onUpdated(() => syncActionViewports(actionScrollLeft));
</script>
<style lang="less" scoped>
:global(.member-modal) {
width: 980px;
max-width: 94vw;
}
:global(.edit-modal) {
width: 700px;
max-width: 92vw;
}
:global(.related-modal) {
width: 680px;
max-width: 92vw;
}
.form-number {
width: 100%;
}
.edit-form-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
column-gap: 20px;
}
.edit-mark-item {
grid-column: 1 / -1;
margin-bottom: 0;
}
.edit-mark-alert {
width: 100%;
}
.edit-mark-hint {
margin-top: 4px;
color: #d97706;
font-size: 12px;
}
.upload-title {
margin-bottom: 8px;
color: var(--n-label-text-color);
}
.member-table-wrap {
position: relative;
}
:deep(.member-action-viewport) {
width: 100%;
overflow-x: hidden;
}
:deep(.member-action-grid) {
width: max-content;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 5px;
}
:deep(.member-action-viewport .tableAction) {
width: max-content;
min-width: 0;
}
:deep(.member-action-viewport .tableAction > .flex) {
justify-content: flex-start !important;
flex-wrap: nowrap;
}
:deep(.member-action-viewport .n-button) {
height: 24px;
padding: 0 7px;
font-size: 12px;
}
.action-column-scrollbar {
position: absolute;
right: 0;
bottom: 52px;
z-index: 5;
width: 180px;
height: 10px;
overflow-x: scroll;
overflow-y: hidden;
background: var(--card-color);
}
.action-column-scrollbar-content {
width: 420px;
height: 1px;
}
.action-column-scrollbar::-webkit-scrollbar {
height: 6px;
}
.action-column-scrollbar::-webkit-scrollbar-track {
background: #eef0f4;
border-radius: 5px;
}
.action-column-scrollbar::-webkit-scrollbar-thumb {
background: #aeb4bf;
border-radius: 5px;
}
.action-column-scrollbar::-webkit-scrollbar-thumb:hover {
background: #8c94a1;
}
@media (max-width: 680px) {
.edit-form-grid {
grid-template-columns: minmax(0, 1fr);
}
.edit-mark-item {
grid-column: auto;
}
}
</style>