feat: add web application

This commit is contained in:
richjohnrambo
2026-10-10 20:55:59 +08:00
parent 3eb5d78875
commit 50fcfd2d3d
571 changed files with 73992 additions and 135 deletions
+292
View File
@@ -0,0 +1,292 @@
<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">
<template #tableTitle>
<n-button v-if="hasPermission(['/appMember/edit'])" type="primary" @click="openEdit()">新增会员</n-button>
</template>
</BasicTable>
<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="editForm.id ? '编辑会员' : '新增会员'" class="edit-modal">
<n-spin :show="editLoading">
<n-form ref="editFormRef" :model="editForm" :rules="editRules" label-placement="left" label-width="100">
<n-form-item label="标记">
<n-checkbox :checked="editForm.isTestAccount === 1" @update:checked="editForm.isTestAccount = $event ? 1 : 2">标记</n-checkbox>
</n-form-item>
<n-form-item label="用户名称" path="username" required><n-input v-model:value="editForm.username" placeholder="请输入用户名称" /></n-form-item>
<n-form-item label="手机号码" path="mobile" :required="!editForm.id"><n-input v-model:value="editForm.mobile" :placeholder="editForm.id ? editForm.mobileMask || '留空不修改' : '请输入手机号码'" /></n-form-item>
<n-form-item label="账户余额"><n-input :value="money(editForm.availableAmount)" disabled /></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.riskStatus" :options="dict.getOptionUnRef('appMemberRiskStatus')" /></n-form-item>
<n-form-item label="登录密码" path="password" :required="!editForm.id"><n-input v-model:value="editForm.password" type="password" show-password-on="click" :placeholder="editForm.id ? '留空不修改' : '至少8位,含字母和数字'" /></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>
</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.mobileMask }}</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>
<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-form-item label="开户人"><n-input v-model:value="paymentForm.holder" placeholder="请输入开户人" /></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="请输入开户电话" /></n-form-item>
<n-form-item label="银行卡号"><n-input v-model:value="paymentForm.bankAccount" placeholder="请输入银行卡号" /></n-form-item>
<n-form-item label="数字钱包"><n-input v-model:value="paymentForm.digitalWallet" placeholder="请输入数字钱包" /></n-form-item>
<n-form-item label="云闪付账号"><n-input v-model:value="paymentForm.unionPayAccount" placeholder="请输入云闪付账号" /></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?.mobileMask || ''" 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, 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, schemas, loadOptions, MemberRecord } from './model';
const message=useMessage(), dialog=useDialog(), dict=useDictStore();
const { hasPermission }=usePermission(); 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:(_rule,value)=>editForm.id||String(value||'').trim()?true:new Error('请输入手机号码'),trigger:['input','blur']},
password:{validator:(_rule,value)=>{const text=String(value||'');if(editForm.id&&!text)return true;if(text.length<8||!/[A-Za-z]/.test(text)||!/\d/.test(text))return new Error('密码至少8位且必须同时包含字母和数字');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:'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:'',mobileMask:row?.mobileMask||'',password:'',tradePassword:'',parentId:row?.parentId||0,levelId:0,riskStatus:row?.riskStatus||1,creditScore:100,autoDispatch:row?.autoDispatch||2,memberCategory:row?.memberCategory||1,isTestAccount:row?.isTestAccount||2,availableAmount:row?.availableAmount||0,frozenAmount:row?.frozenAmount||0});
showEdit.value=true;
if(!row?.id)return;
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,frozenAmount:data.frozenAmount||0,mobileMask:data.mobileMask||row.mobileMask});
}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);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){await AutoDispatch({id:row.id,autoDispatch:row.autoDispatch===1?2:1});message.success('更新成功');reloadTable()}
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:'',bankName:bank?.bankName||'',mobile:'',bankAccount:'',digitalWallet:'',unionPayAccount:'',alipayQrCode:'',wechatQrCode:'',records:{1:bank,2:byType(2),3:byType(3),4:byType(4),5:byType(5)}});
}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'){
const common={memberId:paymentForm.memberId,holder:paymentForm.holder,mobile:paymentForm.mobile,verifyStatus:1,isDefault:1};
const accounts=[
{type:1,value:paymentForm.bankAccount,bankName:paymentForm.bankName},
{type:2,value:paymentForm.digitalWallet,bankName:''},
{type:3,value:paymentForm.unionPayAccount,bankName:''},
{type:4,value:paymentForm.alipayQrCode,bankName:''},
{type:5,value:paymentForm.wechatQrCode,bankName:''},
];
for(const account of accounts){
const old=paymentForm.records?.[account.type];
if(account.value||old?.id)await PaymentEdit({...common,id:old?.id||0,accountType:account.type,accountNo:account.value,bankName:account.bankName,isDefault:account.type===1?1:2});
}
}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:'mobileMask'},{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:720px;max-width:92vw}
:global(.related-modal){width:680px;max-width:92vw}
.form-number{width:100%}
.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}
</style>
+50
View File
@@ -0,0 +1,50 @@
import { computed, h, ref } from 'vue';
import { NTag } from 'naive-ui';
import { FormSchema } from '@/components/Form';
import { useDictStore } from '@/store/modules/dict';
import { defRangeShortcuts } from '@/utils/dateUtil';
const dict = useDictStore();
export interface MemberRecord {
id: number; memberNo: string; username: string; mobileMask: string; realNameMask: string;
availableAmount: number; frozenAmount: number; parentId: number; parentUsername: string;
inviteCode: string; memberStatus: number; riskStatus: number; autoDispatch: number; memberCategory: number;
isTestAccount: number; lastLoginAt?: string; lastLoginIpMask: string; lastLoginRegion: string;
todayTradeCount: number; createdAt: string;
}
export const schemas = ref<FormSchema[]>([
{ field: 'username', component: 'NInput', label: '用户名', componentProps: { placeholder: '请输入用户名' } },
{ field: 'mobile', component: 'NInput', label: '手机号', componentProps: { placeholder: '完整手机号精确查询' } },
{ field: 'memberStatus', component: 'NSelect', label: '状态', componentProps: { options: computed(() => dict.getOptionUnRef('appMemberStatus')), clearable: true } },
{ field: 'autoDispatch', component: 'NSelect', label: '派单模式', componentProps: { options: computed(() => dict.getOptionUnRef('appMemberDispatchMode')), clearable: true } },
{ field: 'memberCategory', component: 'NSelect', label: '类别', componentProps: { options: computed(() => dict.getOptionUnRef('appMemberCategory')), clearable: true } },
{ field: 'createdAt', component: 'NDatePicker', label: '注册时间', componentProps: { type: 'datetimerange', clearable: true, shortcuts: defRangeShortcuts() } },
]);
const money = (value: number) => `¥${((value || 0) / 100).toFixed(2)}`;
const tag = (type: string, value: number) => h(NTag, { type: dict.getType(type, value), bordered: false }, { default: () => dict.getLabel(type, value) || '-' });
export const columns = [
{ title: 'ID', key: 'id', width: 80 },
{ title: '会员编号', key: 'memberNo', width: 170 },
{ title: '账号', key: 'username', width: 130 },
{ title: '手机号', key: 'mobileMask', width: 130 },
{ title: '绑卡姓名', key: 'realNameMask', width: 100 },
{ title: '可用余额', key: 'availableAmount', width: 120, render: (row: MemberRecord) => money(row.availableAmount) },
{ title: '冻结余额', key: 'frozenAmount', width: 120, render: (row: MemberRecord) => money(row.frozenAmount) },
{ title: '上级用户', key: 'parentUsername', width: 120, render: (row: MemberRecord) => row.parentUsername || '-' },
{ title: '邀请码', key: 'inviteCode', width: 110 },
{ title: '状态', key: 'memberStatus', width: 90, render: (row: MemberRecord) => tag('appMemberStatus', row.memberStatus) },
{ title: '类别', key: 'memberCategory', width: 90, render: (row: MemberRecord) => tag('appMemberCategory', row.memberCategory) },
{ title: '派单模式', key: 'autoDispatch', width: 100, render: (row: MemberRecord) => tag('appMemberDispatchMode', row.autoDispatch) },
{ title: '今日交易', key: 'todayTradeCount', width: 90 },
{ title: '最后登录IP', key: 'lastLoginIpMask', width: 130, render: (row: MemberRecord) => row.lastLoginIpMask || '-' },
{ title: '最后登录地区', key: 'lastLoginRegion', width: 140, render: (row: MemberRecord) => row.lastLoginRegion || '-' },
{ title: '注册时间', key: 'createdAt', width: 180 },
];
export function loadOptions() {
dict.loadOptions(['appMemberStatus', 'appMemberRiskStatus', 'appMemberDispatchMode', 'appMemberCategory', 'paymentAccountType', 'walletAdjustType', 'walletDirection', 'sys_switch', 'member_disable_reason', 'wallet_adjust_reason']);
}