feat: 完善手动派单后台操作

This commit is contained in:
richjohnrambo
2026-10-13 22:13:47 +08:00
parent aa4799f2b9
commit ad6d0bbe01
11 changed files with 351 additions and 55 deletions
+3
View File
@@ -4,6 +4,9 @@ export const TradeOrderList = (params) => http.request({ url: '/tradeOrder/list'
export const TradeOrderView = (params) => http.request({ url: '/tradeOrder/view', method: 'get', params });
export const TradeOrderMatchProducts = (params) => http.request({ url: '/tradeOrder/matchProducts', method: 'get', params });
export const TradeOrderMatch = (params) => http.request({ url: '/tradeOrder/match', method: 'POST', params });
export const TradeOrderUpdateAmount = (params) => http.request({ url: '/tradeOrder/updateAmount', method: 'POST', params });
export const TradeOrderForcePay = (params) => http.request({ url: '/tradeOrder/forcePay', method: 'POST', params });
export const TradeOrderFreeze = (params) => http.request({ url: '/tradeOrder/freeze', method: 'POST', params });
export const TradeOrderApply = (params) => http.request({ url: '/tradeOrder/apply', method: 'POST', params });
export const TradeOrderExecute = (params) => http.request({ url: '/tradeOrder/execute', method: 'POST', params });
+34 -6
View File
@@ -355,7 +355,7 @@
<script lang="ts" setup>
import { computed, h, onMounted, onUpdated, reactive, ref } from 'vue';
import { NButton, NSpace, useDialog, useMessage } from 'naive-ui';
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';
@@ -384,12 +384,33 @@
NoticeEdit,
} from '@/api/appMember';
import { List as MemberLevelList } from '@/api/memberLevel';
import { columns, schemas, loadOptions, MemberRecord } from './model';
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>(),
@@ -642,10 +663,17 @@
detailLoading.value = false;
}
}
async function toggleDispatch(row) {
await AutoDispatch({ id: row.id, autoDispatch: row.autoDispatch === 1 ? 2 : 1 });
message.success('更新成功');
reloadTable();
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 ? '禁用' : '启用';
+29 -8
View File
@@ -1,6 +1,6 @@
<template>
<div>
<div class="n-layout-page-header"><n-card :bordered="false" title="订单列表">仅“手动领取”且“匹配中”的订单可在操作栏选择商品完成匹配;自动领取订单由系统自动匹配。</n-card></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">
@@ -54,6 +54,15 @@
</n-space>
</template>
</n-modal>
<n-modal v-model:show="showAmount" preset="card" title="更改金额" class="amount-modal" :mask-closable="false">
<n-form label-placement="left" :label-width="90">
<n-form-item label="用户名称"><n-input :value="amountMember.username" disabled /></n-form-item>
<n-form-item label="账号余额"><n-input :value="yuanPlain(amountMember.availableAmount)" disabled /></n-form-item>
<n-form-item label="更改金额"><n-input-number v-model:value="amountForm.amount" :min="0.01" :precision="2" class="w-full" /></n-form-item>
<n-form-item label="更改数量"><n-input-number v-model:value="amountForm.quantity" :min="1" :max="999" class="w-full" /></n-form-item>
</n-form>
<template #action><n-space><n-button @click="showAmount=false">取消</n-button><n-button type="primary" :loading="amountSubmitting" @click="confirmAmount">确定</n-button></n-space></template>
</n-modal>
<n-modal v-model:show="showDetail" preset="card" title="订单详情" class="max-w-900px">
<n-descriptions v-if="detail" bordered label-placement="left" :column="2">
<n-descriptions-item label="订单号">{{ detail.orderNo }}</n-descriptions-item><n-descriptions-item label="会员">{{ detail.username }} / {{ detail.mobile }}</n-descriptions-item>
@@ -67,15 +76,18 @@
<script lang="ts" setup>
import { computed, h, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue';
import { useRouter } from 'vue-router';
import { NButton, NImage, NTag, useMessage } from 'naive-ui';
import { NButton, NImage, NTag, useDialog, useMessage } from 'naive-ui';
import { BasicTable, TableAction } from '@/components/Table';import { BasicForm,useForm } from '@/components/Form';import { adaTableScrollX } from '@/utils/hotgo';
import { TradeOrderList,TradeOrderMatch,TradeOrderMatchProducts,TradeOrderView } from '@/api/trade';import { columns,schemas,loadOptions } from './model';
const message=useMessage();const router=useRouter();const actionRef=ref(),searchFormRef=ref<any>({}),showDetail=ref(false),showMatch=ref(false),detail=ref<any>(),current=ref<any>(),autoRefresh=ref(false),refreshCountdown=ref(10);let timer:any;
import { TradeOrderForcePay,TradeOrderFreeze,TradeOrderList,TradeOrderMatch,TradeOrderMatchProducts,TradeOrderUpdateAmount,TradeOrderView } from '@/api/trade';import { columns,schemas,loadOptions } from './model';
const message=useMessage();const dialog=useDialog();const router=useRouter();const actionRef=ref(),searchFormRef=ref<any>({}),showDetail=ref(false),showMatch=ref(false),showAmount=ref(false),detail=ref<any>(),current=ref<any>(),autoRefresh=ref(false),refreshCountdown=ref(10);let timer:any;
const [register]=useForm({gridProps:{cols:'1 s:1 m:2 l:3 xl:5'},labelWidth:80,schemas});const loadDataTable=(res)=>TradeOrderList({...searchFormRef.value?.formModel,...res});const reloadTable=()=>actionRef.value?.reload();const yuan=(v)=>`¥${((v||0)/100).toFixed(2)}`;
const yuanPlain=(v)=>((v||0)/100).toFixed(2);
const matchLoading=ref(false),productLoading=ref(false),matchSubmitting=ref(false),products=ref<any[]>([]),selectedProduct=ref<any>();
const matchMember=reactive({id:0,username:'',availableAmount:0,frozenAmount:0});
const matchForm=reactive({id:0,productId:0,quantity:1});
const amountMember=reactive({username:'',availableAmount:0});
const amountForm=reactive<{id:number;amount:number|null;quantity:number}>({id:0,amount:null,quantity:1});
const amountSubmitting=ref(false);
const productFilter=reactive<{name:string;priceMin:number|null;priceMax:number|null}>({name:'',priceMin:null,priceMax:null});
const productPager=reactive({page:1,pageSize:10,total:0});
const productColumns=[
@@ -100,12 +112,20 @@ async function confirmMatch(){
matchSubmitting.value=true;try{await TradeOrderMatch(matchForm);message.success('订单匹配成功');showMatch.value=false;reloadTable()}finally{matchSubmitting.value=false}
}
async function view(row){detail.value=await TradeOrderView({id:row.id});showDetail.value=true}
const actionColumn=reactive({width:210,title:'操作',key:'action',fixed:'right',render:(row)=>{
function openAmount(row){Object.assign(amountMember,{username:row.username,availableAmount:row.availableAmount});Object.assign(amountForm,{id:row.id,amount:Number(row.totalAmount||0)/100,quantity:row.quantity||1});showAmount.value=true}
async function confirmAmount(){if(!amountForm.amount||amountForm.amount<=0){message.error('请输入正确金额');return}amountSubmitting.value=true;try{await TradeOrderUpdateAmount({id:amountForm.id,amount:amountForm.amount.toFixed(2),quantity:amountForm.quantity});message.success('订单金额已更改');showAmount.value=false;reloadTable()}finally{amountSubmitting.value=false}}
function forcePay(row){dialog.warning({title:'强制付款',content:`确认对订单 ${row.orderNo} 执行强制付款吗?允许付款后余额为负数。`,positiveText:'确认付款',negativeText:'取消',onPositiveClick:async()=>{await TradeOrderForcePay({id:row.id});message.success('强制付款成功');reloadTable()}})}
function freezeOrder(row){dialog.warning({title:'冻结订单',content:`确认冻结订单 ${row.orderNo} 吗?冻结后会员需要联系客服处理。`,positiveText:'确认冻结',negativeText:'取消',onPositiveClick:async()=>{await TradeOrderFreeze({id:row.id});message.success('订单已冻结');reloadTable()}})}
const actionColumn=reactive({width:300,title:'操作',key:'action',fixed:'right',render:(row)=>{
const actions:any[]=[];
if(row.canMatch)actions.push({label:'匹配订单',type:'primary',onClick:()=>openMatch(row)});
if(row.orderType===2&&row.orderStatus===2&&row.canReview)actions.push({label:'审核任务',type:'warning',onClick:()=>router.push({name:'task_submission_list',query:{tradeOrderId:String(row.id)}})});
else if(row.orderType===2&&row.orderStatus===2)actions.push({label:'等待会员提交',disabled:true,title:'会员提交任务凭证后即可审核'});
actions.push({label:'详情',onClick:()=>view(row)});
if(row.canChangeAmount)actions.push({label:'更改金额',type:'primary',onClick:()=>openAmount(row)});
if(row.canForcePay)actions.push({label:'强制付款',type:'primary',onClick:()=>forcePay(row)});
if(row.canFreeze)actions.push({label:'冻结订单',type:'warning',onClick:()=>freezeOrder(row)});
const hasManualActions=row.canChangeAmount||row.canForcePay||row.canFreeze;
if(!hasManualActions&&row.orderType===2&&row.orderStatus===2&&row.canReview)actions.push({label:'审核任务',type:'warning',onClick:()=>router.push({name:'task_submission_list',query:{tradeOrderId:String(row.id)}})});
else if(!hasManualActions&&row.orderType===2&&row.orderStatus===2)actions.push({label:'等待会员付款',disabled:true,title:'等待会员完成付款'});
if(!hasManualActions)actions.push({label:'详情',onClick:()=>view(row)});
return h(TableAction as any,{style:'button',actions});
}});const scrollX=computed(()=>adaTableScrollX(columns,actionColumn.width));
watch(autoRefresh,(enabled)=>{
@@ -121,6 +141,7 @@ onBeforeUnmount(()=>clearInterval(timer));onMounted(loadOptions);
</script>
<style lang="less" scoped>
:deep(.match-order-modal) { width: min(1040px, 94vw); }
:deep(.amount-modal) { width: min(620px, 92vw); }
.match-product-area { width: 100%; }
.filter-name { width: 180px; }
.filter-price { width: 130px; }
+2 -2
View File
@@ -19,7 +19,7 @@ export const columns = [
{ title: '绑卡姓名', key: 'realNameMask', width: 110 },
{ title: '用户名', key: 'username', width: 120 },
{ title: '手机号', key: 'mobile', width: 130 },
{ title: '自动派单', key: 'autoDispatch', width: 100, render: (r) => tag('tradeAutoDispatch', r.autoDispatch) },
{ title: '派单模式', key: 'autoDispatch', width: 100, render: (r) => tag('tradeAutoDispatch', r.autoDispatch) },
{ title: '余额', key: 'availableAmount', width: 120, render: (r) => yuan(r.availableAmount) },
{ title: '当日交易', key: 'todayTradeCount', width: 90 },
{ title: '商品/任务', key: 'productName', width: 160, ellipsis: { tooltip: true } },
@@ -31,7 +31,7 @@ export const columns = [
{ title: '解冻时间', key: 'unfrozenAt', width: 180 },
{ title: '消息', key: 'message', width: 180, ellipsis: { tooltip: true } },
{ title: '订单状态', key: 'orderStatus', width: 110, render: (r) => r.orderType === 2 && r.orderStatus === 2
? h(NTag, { type: 'warning', bordered: false }, { default: () => '待会员提交' })
? h(NTag, { type: 'warning', bordered: false }, { default: () => '待付款' })
: tag('tradeOrderStatus', r.orderStatus) },
{ title: '冻结状态', key: 'freezeStatus', width: 110, render: (r) => tag('tradeFreezeStatus', r.freezeStatus) },
];