feat: add web application
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
<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" />
|
||||
<BasicTable ref="actionRef" :columns="columns" :request="loadDataTable" :row-key="r=>r.id" :action-column="actionColumn" :scroll-x="scrollX" :resize-height-offset="-10000"><template #tableTitle><n-button v-if="hasPermission(['/memberLevel/save'])" type="primary" @click="openEdit()">新增等级</n-button></template></BasicTable>
|
||||
</n-card>
|
||||
<n-modal v-model:show="showEdit" preset="card" :title="form.id?'编辑等级':'新增等级'" class="level-modal">
|
||||
<n-alert type="info" :show-icon="false">金额以元录入,服务端按分保存;佣金以百分比录入,服务端按万分比保存。</n-alert>
|
||||
<n-form ref="formRef" :model="form" :rules="rules" label-placement="left" label-width="110">
|
||||
<n-grid :cols="2" :x-gap="16">
|
||||
<n-form-item-gi label="等级编码" path="levelCode" required><n-input v-model:value="form.levelCode" placeholder="如 VIP1" /></n-form-item-gi>
|
||||
<n-form-item-gi label="等级名称" path="name" required><n-input v-model:value="form.name" /></n-form-item-gi>
|
||||
<n-form-item-gi label="图标地址"><n-input v-model:value="form.iconUrl" /></n-form-item-gi>
|
||||
<n-form-item-gi label="状态"><n-select v-model:value="form.status" :options="dict.getOptionUnRef('sys_normal_disable')" /></n-form-item-gi>
|
||||
<n-form-item-gi label="升级价格(元)"><n-input-number v-model:value="form.upgradeYuan" :min="0" :precision="2" /></n-form-item-gi>
|
||||
<n-form-item-gi label="赠送金额(元)"><n-input-number v-model:value="form.giftYuan" :min="0" :precision="2" /></n-form-item-gi>
|
||||
<n-form-item-gi label="每日接单次数"><n-input-number v-model:value="form.dailyOrderLimit" :min="0" /></n-form-item-gi>
|
||||
<n-form-item-gi label="佣金比例(%)"><n-input-number v-model:value="form.commissionPercent" :min="0" :max="100" :precision="2" /></n-form-item-gi>
|
||||
<n-form-item-gi label="每日提现次数"><n-input-number v-model:value="form.dailyWithdrawLimit" :min="0" /></n-form-item-gi>
|
||||
<n-form-item-gi label="排序"><n-input-number v-model:value="form.sort" /></n-form-item-gi>
|
||||
<n-form-item-gi label="备注" :span="2"><n-input v-model:value="form.remark" type="textarea" /></n-form-item-gi>
|
||||
</n-grid>
|
||||
</n-form>
|
||||
<template #action><n-space justify="end"><n-button @click="showEdit=false">取消</n-button><n-button v-if="hasPermission(['/memberLevel/save'])" type="primary" :loading="saving" @click="saveLevel">保存</n-button></n-space></template>
|
||||
</n-modal>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { computed,h,onMounted,reactive,ref } from 'vue';import { useDialog,useMessage } from 'naive-ui';import { BasicTable,TableAction } from '@/components/Table';import { BasicForm,useForm } from '@/components/Form';import { usePermission } from '@/hooks/web/usePermission';import { useDictStore } from '@/store/modules/dict';import { adaTableScrollX } from '@/utils/hotgo';import { List,View,Save,Status,Delete } from '@/api/memberLevel';import { columns,schemas,loadOptions } from './model';
|
||||
const message=useMessage(),dialog=useDialog(),dict=useDictStore();const{hasPermission}=usePermission();const actionRef=ref(),searchFormRef=ref<any>({}),formRef=ref<any>(),showEdit=ref(false),saving=ref(false);const form=reactive<any>({});const rules={levelCode:{required:true,message:'请输入等级编码',trigger:['input','blur']},name:{required:true,message:'请输入等级名称',trigger:['input','blur']}};const [register]=useForm({gridProps:{cols:'1 s:1 m:2 l:3'},labelWidth:80,schemas});const loadDataTable=res=>List({...searchFormRef.value?.formModel,...res});const reloadTable=()=>actionRef.value?.reload();const reset=(o,v)=>{Object.keys(o).forEach(k=>delete o[k]);Object.assign(o,v)};
|
||||
const actionColumn=reactive({width:210,title:'操作',key:'action',fixed:'right',render:row=>h(TableAction as any,{style:'button',actions:[{label:'编辑',onClick:()=>openEdit(row),auth:['/memberLevel/view']},{label:row.status===1?'停用':'启用',onClick:()=>toggleStatus(row),auth:['/memberLevel/status']},{label:'删除',type:'error',onClick:()=>remove(row),auth:['/memberLevel/delete']}]})});const scrollX=computed(()=>adaTableScrollX(columns,actionColumn.width));
|
||||
function emptyForm(){return{id:0,levelCode:'',name:'',iconAttachmentId:0,iconUrl:'',upgradeYuan:0,giftYuan:0,dailyOrderLimit:0,commissionPercent:0,dailyWithdrawLimit:0,sort:0,status:1,remark:''}}
|
||||
async function openEdit(row?){reset(form,emptyForm());showEdit.value=true;if(!row)return;const r:any=await View({id:row.id});const v=r.published||r.draft;Object.assign(form,{id:r.level.id,levelCode:r.level.levelCode,name:r.level.name,iconAttachmentId:r.level.iconAttachmentId,iconUrl:r.level.iconUrl,sort:r.level.sort,status:r.level.status,remark:r.level.remark||'',upgradeYuan:(v?.upgradeAmount||0)/100,giftYuan:(v?.giftAmount||0)/100,dailyOrderLimit:v?.dailyOrderLimit||0,commissionPercent:(v?.commissionRate||0)/100,dailyWithdrawLimit:v?.dailyWithdrawLimit||0})}
|
||||
async function saveLevel(){try{await formRef.value?.validate()}catch{return}saving.value=true;try{await Save({...form,upgradeAmount:Math.round(form.upgradeYuan*100),giftAmount:Math.round(form.giftYuan*100),commissionRate:Math.round(form.commissionPercent*100)});message.success('保存成功');showEdit.value=false;reloadTable();dict.loadOptions(['memberLevelOption'])}finally{saving.value=false}}
|
||||
async function toggleStatus(row){await Status({id:row.id,status:row.status===1?2:1});message.success('状态已更新');reloadTable()}
|
||||
function remove(row){dialog.warning({title:'删除等级',content:'仅无会员且未被任务大厅引用的等级可删除。',positiveText:'确认删除',negativeText:'取消',onPositiveClick:async()=>{await Delete({id:row.id,reason:'后台删除未使用等级'});message.success('删除成功');reloadTable()}})}
|
||||
onMounted(loadOptions);
|
||||
</script>
|
||||
<style lang="less" scoped>:deep(.level-modal){width:min(980px,94vw)}</style>
|
||||
@@ -0,0 +1,18 @@
|
||||
import { computed,h,ref } from 'vue';
|
||||
import { NTag } from 'naive-ui';
|
||||
import { FormSchema } from '@/components/Form';
|
||||
import { useDictStore } from '@/store/modules/dict';
|
||||
import { renderImage } from '@/utils';
|
||||
const dict=useDictStore();
|
||||
export const schemas=ref<FormSchema[]>([
|
||||
{field:'name',component:'NInput',label:'等级名称',componentProps:{placeholder:'请输入等级名称'}},
|
||||
{field:'status',component:'NSelect',label:'状态',componentProps:{options:computed(()=>dict.getOptionUnRef('sys_normal_disable')),clearable:true}},
|
||||
]);
|
||||
const money=(v)=>`¥${((v||0)/100).toFixed(2)}`;const tag=(type,value)=>h(NTag,{type:dict.getType(type,value),bordered:false},{default:()=>dict.getLabel(type,value)||'-'});
|
||||
export const columns=[
|
||||
{title:'ID',key:'id',width:70},{title:'图标',key:'iconUrl',width:80,render:r=>renderImage(r.iconUrl)},{title:'等级编码',key:'levelCode',width:120},{title:'等级名称',key:'name',width:120},
|
||||
{title:'升级价格',key:'upgradeAmount',width:110,render:r=>money(r.upgradeAmount)},{title:'赠送金额',key:'giftAmount',width:110,render:r=>money(r.giftAmount)},
|
||||
{title:'接单次数/日',key:'dailyOrderLimit',width:110},{title:'佣金比例',key:'commissionRate',width:100,render:r=>`${((r.commissionRate||0)/100).toFixed(2)}%`},{title:'提现次数/日',key:'dailyWithdrawLimit',width:110},
|
||||
{title:'状态',key:'status',width:90,render:r=>tag('sys_normal_disable',r.status)},{title:'会员数',key:'memberCount',width:90},{title:'创建时间',key:'createdAt',width:180},
|
||||
];
|
||||
export const loadOptions=()=>dict.loadOptions(['sys_normal_disable']);
|
||||
@@ -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>
|
||||
@@ -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']);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<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" />
|
||||
<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/noticeEdit'])" type="primary" @click="openCreate">创建通知</n-button></template>
|
||||
</BasicTable>
|
||||
</n-card>
|
||||
<n-modal v-model:show="showCreate" preset="card" title="创建会员通知" class="notice-modal">
|
||||
<n-form label-placement="left" label-width="100">
|
||||
<n-form-item label="接收会员ID"><n-input-number v-model:value="form.memberId" :min="0" /><n-text depth="3">0 表示全体会员</n-text></n-form-item>
|
||||
<n-form-item label="标题"><n-input v-model:value="form.title" /></n-form-item>
|
||||
<n-form-item label="内容"><n-input v-model:value="form.content" type="textarea" /></n-form-item>
|
||||
<n-form-item label="来源"><n-select v-model:value="form.sourceType" :options="dict.getOptionUnRef('memberNoticeSource')" /></n-form-item>
|
||||
<n-form-item label="有效期"><n-date-picker v-model:formatted-value="dateRange" type="datetimerange" value-format="yyyy-MM-dd HH:mm:ss" clearable /></n-form-item>
|
||||
</n-form>
|
||||
<template #action><n-space justify="end"><n-button @click="showCreate=false">取消</n-button><n-button type="primary" @click="createNotice">创建</n-button></n-space></template>
|
||||
</n-modal>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { computed, h, onMounted, reactive, ref } from 'vue';
|
||||
import { useDialog, useMessage } from 'naive-ui';
|
||||
import { BasicTable, TableAction } from '@/components/Table';
|
||||
import { BasicForm, useForm } from '@/components/Form';
|
||||
import { usePermission } from '@/hooks/web/usePermission';
|
||||
import { useDictStore } from '@/store/modules/dict';
|
||||
import { adaTableScrollX } from '@/utils/hotgo';
|
||||
import { NoticeList, NoticeEdit, NoticeRevoke } from '@/api/appMember';
|
||||
import { columns, schemas, loadOptions } from './model';
|
||||
const message=useMessage(),dialog=useDialog(),dict=useDictStore();const {hasPermission}=usePermission();const actionRef=ref(),searchFormRef=ref<any>({}),showCreate=ref(false),dateRange=ref<[string,string]|null>(null);const form=reactive({memberId:0,title:'',content:'',sourceType:1});
|
||||
const [register]=useForm({gridProps:{cols:'1 s:1 m:2 l:3'},labelWidth:70,schemas});const loadDataTable=(res)=>NoticeList({...searchFormRef.value?.formModel,...res});const reloadTable=()=>actionRef.value?.reload();
|
||||
const actionColumn=reactive({width:100,title:'操作',key:'action',fixed:'right',render:(row)=>h(TableAction as any,{style:'button',actions:[{label:'撤回',disabled:row.noticeStatus===2,onClick:()=>revoke(row),auth:['/appMember/noticeRevoke']}]})});const scrollX=computed(()=>adaTableScrollX(columns,actionColumn.width));
|
||||
function openCreate(){Object.assign(form,{memberId:0,title:'',content:'',sourceType:1});dateRange.value=null;showCreate.value=true}
|
||||
async function createNotice(){if(!dateRange.value){message.error('请选择有效期');return}await NoticeEdit({...form,startAt:dateRange.value[0],endAt:dateRange.value[1]});message.success('创建成功');showCreate.value=false;reloadTable()}
|
||||
function revoke(row){dialog.warning({title:'撤回通知',content:'撤回后前台不再展示,历史记录仍保留。',positiveText:'确认撤回',negativeText:'取消',onPositiveClick:async()=>{await NoticeRevoke({id:row.id,reason:'后台管理员撤回'});message.success('已撤回');reloadTable()}})}
|
||||
onMounted(loadOptions);
|
||||
</script>
|
||||
<style lang="less" scoped>:deep(.notice-modal){width:min(720px,92vw)}</style>
|
||||
@@ -0,0 +1,20 @@
|
||||
import { computed, h, ref } from 'vue';
|
||||
import { NTag } from 'naive-ui';
|
||||
import { FormSchema } from '@/components/Form';
|
||||
import { useDictStore } from '@/store/modules/dict';
|
||||
const dict = useDictStore();
|
||||
export const schemas = ref<FormSchema[]>([
|
||||
{ field: 'title', component: 'NInput', label: '标题', componentProps: { placeholder: '请输入标题' } },
|
||||
{ field: 'sourceType', component: 'NSelect', label: '来源', componentProps: { options: computed(() => dict.getOptionUnRef('memberNoticeSource')), clearable: true } },
|
||||
{ field: 'noticeStatus', component: 'NSelect', label: '状态', componentProps: { options: computed(() => dict.getOptionUnRef('memberNoticeStatus')), clearable: true } },
|
||||
]);
|
||||
const tag = (type, value) => 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: 'username', width: 120 },
|
||||
{ title: '标题', key: 'title', width: 180 }, { title: '内容', key: 'content', width: 320, ellipsis: { tooltip: true } },
|
||||
{ title: '来源', key: 'sourceType', width: 110, render: (r) => tag('memberNoticeSource', r.sourceType) },
|
||||
{ title: '状态', key: 'noticeStatus', width: 90, render: (r) => tag('memberNoticeStatus', r.noticeStatus) },
|
||||
{ title: '开始时间', key: 'startAt', width: 180 }, { title: '结束时间', key: 'endAt', width: 180 },
|
||||
{ title: '创建时间', key: 'createdAt', width: 180 },
|
||||
];
|
||||
export const loadOptions = () => dict.loadOptions(['memberNoticeSource', 'memberNoticeStatus']);
|
||||
@@ -0,0 +1,247 @@
|
||||
<template>
|
||||
<div>
|
||||
<n-card :bordered="false" class="proCard">
|
||||
<BasicTable
|
||||
ref="tableRef"
|
||||
openChecked
|
||||
:columns="columns"
|
||||
:request="loadTable"
|
||||
:row-key="(row) => row.id"
|
||||
:action-column="actionColumn"
|
||||
:scroll-x="scrollX"
|
||||
:pagination="pagination"
|
||||
:checked-row-keys="checkedIds"
|
||||
@update:checked-row-keys="onCheckedRow"
|
||||
>
|
||||
<template #tableTitle>
|
||||
<n-button v-if="hasPermission(['/taskHall/delete'])" type="primary" @click="batchRemove">
|
||||
删除
|
||||
</n-button>
|
||||
<n-button
|
||||
v-if="hasPermission(['/taskHall/edit'])"
|
||||
type="primary"
|
||||
class="min-left-space"
|
||||
@click="openEdit()"
|
||||
>
|
||||
添加
|
||||
</n-button>
|
||||
</template>
|
||||
</BasicTable>
|
||||
</n-card>
|
||||
|
||||
<n-modal
|
||||
v-model:show="showEdit"
|
||||
preset="card"
|
||||
:title="form.id ? '编辑大厅任务' : '添加大厅任务'"
|
||||
class="hall-modal"
|
||||
>
|
||||
<n-spin :show="loading">
|
||||
<n-form ref="formRef" :model="form" :rules="rules" label-placement="left" label-width="90">
|
||||
<n-form-item label="名称" path="name">
|
||||
<n-input v-model:value="form.name" placeholder="请输入名称" />
|
||||
</n-form-item>
|
||||
<n-form-item label="会员等级" path="levelId">
|
||||
<n-select
|
||||
v-model:value="form.levelId"
|
||||
:options="dict.getOptionUnRef('memberLevelOption')"
|
||||
placeholder="请选择会员等级"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item label="比例">
|
||||
<n-input value="以会员为主" readonly />
|
||||
</n-form-item>
|
||||
<n-form-item label="简介" path="summary">
|
||||
<n-input v-model:value="form.summary" placeholder="请输入简介" />
|
||||
</n-form-item>
|
||||
<n-form-item label="图标" path="iconUrl">
|
||||
<UploadImage v-model:value="form.iconUrl" :max-number="1" />
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</n-spin>
|
||||
<template #action>
|
||||
<n-space justify="end">
|
||||
<n-button @click="showEdit = false">取消</n-button>
|
||||
<n-button type="primary" :loading="saving" @click="saveHall">确定</n-button>
|
||||
</n-space>
|
||||
</template>
|
||||
</n-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, h, onMounted, reactive, ref } from 'vue';
|
||||
import { useDialog, useMessage } from 'naive-ui';
|
||||
import { BasicTable, TableAction } from '@/components/Table';
|
||||
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 { Delete, Edit, List, View } from '@/api/taskHall';
|
||||
import { columns, loadOptions } from './model';
|
||||
|
||||
const message = useMessage();
|
||||
const dialog = useDialog();
|
||||
const dict = useDictStore();
|
||||
const { hasPermission } = usePermission();
|
||||
const tableRef = ref();
|
||||
const formRef = ref();
|
||||
const checkedIds = ref<number[]>([]);
|
||||
const showEdit = ref(false);
|
||||
const loading = ref(false);
|
||||
const saving = ref(false);
|
||||
const form = reactive<any>({});
|
||||
const pagination = { pageSize: 30, pageSizes: [10, 20, 30, 40, 50, 60] };
|
||||
|
||||
const rules = {
|
||||
name: { required: true, message: '请输入名称', trigger: ['input', 'blur'] },
|
||||
levelId: { required: true, type: 'number', message: '请选择会员等级', trigger: 'change' },
|
||||
summary: { required: true, message: '请输入简介', trigger: ['input', 'blur'] },
|
||||
iconUrl: { required: true, message: '请上传图标', trigger: 'change' },
|
||||
};
|
||||
|
||||
const actionColumn = reactive({
|
||||
width: 180,
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
fixed: 'right',
|
||||
render: (row) =>
|
||||
h(TableAction as any, {
|
||||
style: 'button',
|
||||
actions: [
|
||||
{
|
||||
label: '编辑',
|
||||
type: 'primary',
|
||||
auth: ['/taskHall/view'],
|
||||
onClick: () => openEdit(row),
|
||||
},
|
||||
{
|
||||
label: '删除',
|
||||
type: 'error',
|
||||
auth: ['/taskHall/delete'],
|
||||
onClick: () => remove(row),
|
||||
},
|
||||
],
|
||||
}),
|
||||
});
|
||||
const scrollX = computed(() => adaTableScrollX(columns, actionColumn.width));
|
||||
|
||||
function emptyForm() {
|
||||
return {
|
||||
id: 0,
|
||||
version: 0,
|
||||
hallCode: '',
|
||||
name: '',
|
||||
levelId: null,
|
||||
iconAttachmentId: 0,
|
||||
iconUrl: '',
|
||||
summary: '',
|
||||
commissionRate: 0,
|
||||
clientRoute: '',
|
||||
sort: 0,
|
||||
remark: '',
|
||||
};
|
||||
}
|
||||
|
||||
function resetForm(value) {
|
||||
Object.keys(form).forEach((key) => delete form[key]);
|
||||
Object.assign(form, value);
|
||||
}
|
||||
|
||||
const loadTable = (params) => List(params);
|
||||
|
||||
function onCheckedRow(rowKeys: number[]) {
|
||||
checkedIds.value = rowKeys;
|
||||
}
|
||||
|
||||
function reload() {
|
||||
checkedIds.value = [];
|
||||
tableRef.value?.reload();
|
||||
}
|
||||
|
||||
async function openEdit(row?) {
|
||||
resetForm(emptyForm());
|
||||
showEdit.value = true;
|
||||
if (!row) return;
|
||||
loading.value = true;
|
||||
try {
|
||||
const result: any = await View({ id: row.id });
|
||||
Object.assign(form, result, { levelId: result.levelIds?.[0] ?? null });
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function saveHall() {
|
||||
try {
|
||||
await formRef.value?.validate();
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
saving.value = true;
|
||||
try {
|
||||
const hallCode = form.hallCode || `HALL_${Date.now()}`;
|
||||
await Edit({
|
||||
...form,
|
||||
hallCode,
|
||||
clientRoute: form.clientRoute || `/task-hall/${hallCode.toLowerCase()}`,
|
||||
levelIds: [form.levelId],
|
||||
});
|
||||
message.success(form.id ? '编辑成功' : '添加成功');
|
||||
showEdit.value = false;
|
||||
reload();
|
||||
dict.loadOptions(['taskHallOption']);
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function remove(row) {
|
||||
dialog.warning({
|
||||
title: '提示',
|
||||
content: '确定删除此记录吗?',
|
||||
positiveText: '确定',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: async () => {
|
||||
await Delete({ id: row.id, reason: '后台删除任务大厅' });
|
||||
message.success('删除成功');
|
||||
reload();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function batchRemove() {
|
||||
if (checkedIds.value.length === 0) {
|
||||
message.warning('请选择数据');
|
||||
return;
|
||||
}
|
||||
dialog.warning({
|
||||
title: '提示',
|
||||
content: '确定删除吗?',
|
||||
positiveText: '确定',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: async () => {
|
||||
let successCount = 0;
|
||||
let failedCount = 0;
|
||||
for (const id of checkedIds.value) {
|
||||
try {
|
||||
await Delete({ id, reason: '后台批量删除任务大厅' });
|
||||
successCount++;
|
||||
} catch {
|
||||
failedCount++;
|
||||
}
|
||||
}
|
||||
if (successCount > 0) message.success(`成功删除 ${successCount} 条数据`);
|
||||
if (failedCount > 0) message.warning(`${failedCount} 条数据删除失败`);
|
||||
reload();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(loadOptions);
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
:deep(.hall-modal) {
|
||||
width: min(750px, 94vw);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,20 @@
|
||||
import { renderImage } from '@/utils';
|
||||
import { useDictStore } from '@/store/modules/dict';
|
||||
|
||||
const dict = useDictStore();
|
||||
|
||||
export const columns = [
|
||||
{ title: 'ID', key: 'id', width: 90, sorter: true },
|
||||
{ title: '名称', key: 'name', width: 160 },
|
||||
{ title: '图标', key: 'iconUrl', width: 100, render: (row) => renderImage(row.iconUrl) },
|
||||
{
|
||||
title: '比例',
|
||||
key: 'commissionRate',
|
||||
width: 120,
|
||||
render: (row) => `${((row.commissionRate || 0) / 100).toFixed(2)}%`,
|
||||
},
|
||||
{ title: '简介', key: 'summary', width: 260, ellipsis: { tooltip: true } },
|
||||
{ title: '创建时间', key: 'createdAt', width: 180 },
|
||||
];
|
||||
|
||||
export const loadOptions = () => dict.loadOptions(['memberLevelOption']);
|
||||
@@ -0,0 +1,40 @@
|
||||
<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" />
|
||||
<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/walletRequest'])" type="primary" @click="showCreate=true">发起调账</n-button></template>
|
||||
</BasicTable>
|
||||
</n-card>
|
||||
<n-modal v-model:show="showCreate" preset="dialog" title="发起资金调账" :show-icon="false">
|
||||
<n-alert type="warning" :show-icon="false">必须填写业务原因。金额以元录入,后端按分记账并校验幂等键。</n-alert>
|
||||
<n-form label-placement="top">
|
||||
<n-form-item label="会员ID"><n-input-number v-model:value="form.memberId" :min="1" /></n-form-item>
|
||||
<n-form-item label="调整类型"><n-select v-model:value="form.adjustType" :options="dict.getOptionUnRef('walletAdjustType')" /></n-form-item>
|
||||
<n-form-item label="资金方向"><n-select v-model:value="form.direction" :options="dict.getOptionUnRef('walletDirection')" /></n-form-item>
|
||||
<n-form-item label="金额(元)"><n-input-number v-model:value="amountYuan" :min="0.01" :precision="2" /></n-form-item>
|
||||
<n-form-item label="原因"><n-select v-model:value="form.reasonCode" :options="dict.getOptionUnRef('wallet_adjust_reason')" /></n-form-item>
|
||||
<n-form-item label="详细说明"><n-input v-model:value="form.reasonText" type="textarea" /></n-form-item>
|
||||
</n-form>
|
||||
<template #action><n-space><n-button @click="showCreate=false">取消</n-button><n-button type="primary" @click="createRequest">提交审批</n-button></n-space></template>
|
||||
</n-modal>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { computed, h, onMounted, reactive, ref } from 'vue';
|
||||
import { useMessage } from 'naive-ui';
|
||||
import { BasicTable, TableAction } from '@/components/Table';
|
||||
import { BasicForm, useForm } from '@/components/Form';
|
||||
import { usePermission } from '@/hooks/web/usePermission';
|
||||
import { useDictStore } from '@/store/modules/dict';
|
||||
import { adaTableScrollX } from '@/utils/hotgo';
|
||||
import { WalletList, WalletRequest, WalletExecute } from '@/api/appMember';
|
||||
import { columns, schemas, loadOptions } from './model';
|
||||
const message=useMessage(),dict=useDictStore();const {hasPermission}=usePermission();const actionRef=ref(),searchFormRef=ref<any>({}),showCreate=ref(false),amountYuan=ref(0);const form=reactive({memberId:0,adjustType:1,direction:1,reasonCode:'',reasonText:''});
|
||||
const [register]=useForm({gridProps:{cols:'1 s:1 m:2 l:3 xl:4'},labelWidth:80,schemas});const loadDataTable=(res)=>WalletList({...searchFormRef.value?.formModel,...res});const reloadTable=()=>actionRef.value?.reload();
|
||||
const actionColumn=reactive({width:120,title:'操作',key:'action',fixed:'right',render:(row)=>h(TableAction as any,{style:'button',actions:[{label:'执行',disabled:row.requestStatus===4,onClick:()=>execute(row),auth:['/appMember/walletExecute']}]})});const scrollX=computed(()=>adaTableScrollX(columns,actionColumn.width));
|
||||
async function createRequest(){const r:any=await WalletRequest({...form,amount:Math.round(amountYuan.value*100),idempotencyKey:`wallet-${form.memberId}-${Date.now()}`});message.success(`已提交审批:${r.approvalNo}`);showCreate.value=false;reloadTable()}
|
||||
async function execute(row){await WalletExecute({id:row.id});message.success('调账执行成功,账变流水已生成');reloadTable()}
|
||||
onMounted(loadOptions);
|
||||
</script>
|
||||
@@ -0,0 +1,24 @@
|
||||
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 const schemas = ref<FormSchema[]>([
|
||||
{ field: 'memberId', component: 'NInputNumber', label: '会员ID', componentProps: { min: 1, clearable: true } },
|
||||
{ field: 'requestStatus', component: 'NSelect', label: '申请状态', componentProps: { options: computed(() => dict.getOptionUnRef('walletRequestStatus')), clearable: true } },
|
||||
{ field: 'createdAt', component: 'NDatePicker', label: '申请时间', componentProps: { type: 'datetimerange', clearable: true, shortcuts: defRangeShortcuts() } },
|
||||
]);
|
||||
const tag = (type, value) => h(NTag, { type: dict.getType(type, value), bordered: false }, { default: () => dict.getLabel(type, value) || '-' });
|
||||
export const columns = [
|
||||
{ title: '申请单号', key: 'requestNo', width: 210 }, { title: '会员ID', key: 'memberId', width: 90 },
|
||||
{ title: '用户名', key: 'username', width: 120 }, { title: '手机号', key: 'mobileMask', width: 130 },
|
||||
{ title: '类型', key: 'adjustType', width: 110, render: (r) => tag('walletAdjustType', r.adjustType) },
|
||||
{ title: '方向', key: 'direction', width: 90, render: (r) => tag('walletDirection', r.direction) },
|
||||
{ title: '金额', key: 'amount', width: 120, render: (r) => `¥${((r.amount || 0) / 100).toFixed(2)}` },
|
||||
{ title: '原因', key: 'reasonText', width: 220, ellipsis: { tooltip: true } },
|
||||
{ title: '状态', key: 'requestStatus', width: 100, render: (r) => tag('walletRequestStatus', r.requestStatus) },
|
||||
{ title: '审批ID', key: 'approvalRequestId', width: 100 }, { title: '账变ID', key: 'ledgerId', width: 90 },
|
||||
{ title: '申请时间', key: 'createdAt', width: 180 },
|
||||
];
|
||||
export const loadOptions = () => dict.loadOptions(['walletAdjustType', 'walletDirection', 'walletRequestStatus', 'wallet_adjust_reason']);
|
||||
Reference in New Issue
Block a user