1210 lines
41 KiB
Vue
1210 lines
41 KiB
Vue
<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>
|