Files
olons_admin/web/src/views/trade/order/index.vue
T

149 lines
12 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div>
<div class="n-layout-page-header"><n-card :bordered="false" title="订单列表">手动派单模式由客服匹配订单,会员确认时校验余额;自动派单模式仅匹配余额内订单并直接完成。</n-card></div>
<n-card :bordered="false" class="proCard">
<BasicForm ref="searchFormRef" @register="register" @submit="reloadTable" @reset="reloadTable" />
<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-space align="center"><n-switch v-model:value="autoRefresh"/><span>自动刷新</span><span v-if="autoRefresh">({{ refreshCountdown }}S)</span></n-space></template>
</BasicTable>
</n-card>
<n-modal v-model:show="showMatch" preset="card" title="匹配订单" class="match-order-modal" :mask-closable="false">
<n-spin :show="matchLoading">
<n-form label-placement="left" :label-width="90">
<n-form-item label="用户名称"><n-input :value="matchMember.username" disabled /></n-form-item>
<n-form-item label="账号余额"><n-input :value="yuanPlain(matchMember.availableAmount)" disabled /></n-form-item>
<n-form-item label="冻结金额"><n-input :value="yuanPlain(matchMember.frozenAmount)" disabled /></n-form-item>
<n-form-item label="选择商品">
<n-space vertical class="match-product-area">
<n-space align="center" wrap>
<span>商品名称</span><n-input v-model:value="productFilter.name" placeholder="商品名称" clearable class="filter-name" />
<span>商品价格</span><n-input-number v-model:value="productFilter.priceMin" :min="0" placeholder="最低价" clearable class="filter-price" />
<span>-</span><n-input-number v-model:value="productFilter.priceMax" :min="0" placeholder="最高价" clearable class="filter-price" />
<n-button type="primary" @click="searchProducts">筛选商品</n-button>
<n-button @click="clearProductFilter">清理缓存</n-button>
</n-space>
<n-data-table
:columns="productColumns"
:data="products"
:loading="productLoading"
:row-key="(row) => row.id"
:max-height="410"
striped
/>
<n-pagination
v-if="productPager.total > productPager.pageSize"
v-model:page="productPager.page"
:page-size="productPager.pageSize"
:item-count="productPager.total"
@update:page="loadProducts"
/>
</n-space>
</n-form-item>
<n-form-item label="商品单价">
<n-input :value="selectedProduct ? yuanPlain(selectedProduct.priceAmount) : ''" disabled placeholder="请先选择商品" />
</n-form-item>
<n-form-item label="商品数量">
<n-input-number v-model:value="matchForm.quantity" :min="1" :max="999" />
</n-form-item>
</n-form>
</n-spin>
<template #action>
<n-space>
<n-button @click="showMatch=false">取消</n-button>
<n-button type="primary" :loading="matchSubmitting" @click="confirmMatch">确定</n-button>
</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>
<n-descriptions-item label="商品/任务">{{ detail.productName || '-' }}</n-descriptions-item><n-descriptions-item label="规则等级ID">{{ detail.memberLevelId }}</n-descriptions-item>
<n-descriptions-item label="订单金额">{{ yuan(detail.totalAmount) }}</n-descriptions-item><n-descriptions-item label="佣金">{{ yuan(detail.commissionAmount) }}</n-descriptions-item>
<n-descriptions-item label="规则快照" :span="2"><n-code :code="JSON.stringify(detail.ruleSnapshot || {}, null, 2)" language="json" word-wrap /></n-descriptions-item>
</n-descriptions>
</n-modal>
</div>
</template>
<script lang="ts" setup>
import { computed, h, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue';
import { useRouter } from 'vue-router';
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 { 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=[
{title:'图片',key:'coverUrl',width:90,render:(row)=>h(NImage,{width:54,height:54,objectFit:'cover',src:row.coverUrl,previewDisabled:true})},
{title:'商品名称',key:'name',minWidth:300,ellipsis:{tooltip:true}},
{title:'商品单价',key:'priceAmount',width:130,sorter:'default',render:(row)=>yuanPlain(row.priceAmount)},
{title:'操作',key:'action',width:100,render:(row)=>selectedProduct.value?.id===row.id?h(NTag,{type:'success',bordered:false},{default:()=> '已选择'}):h(NButton,{size:'small',type:'primary',onClick:()=>selectProduct(row)},{default:()=> '选择'})},
];
async function openMatch(row){
current.value=row;Object.assign(matchMember,{id:row.memberId,username:row.username,availableAmount:row.availableAmount,frozenAmount:row.frozenAmount||0});
Object.assign(matchForm,{id:row.id,productId:0,quantity:1});Object.assign(productFilter,{name:'',priceMin:null,priceMax:null});selectedProduct.value=undefined;productPager.page=1;showMatch.value=true;await loadProducts();
}
async function loadProducts(){
productLoading.value=true;
try{const res:any=await TradeOrderMatchProducts({page:productPager.page,pageSize:productPager.pageSize,name:productFilter.name,priceMin:productFilter.priceMin,priceMax:productFilter.priceMax});products.value=res.list||[];productPager.total=res.totalCount||0}finally{productLoading.value=false}
}
function searchProducts(){productPager.page=1;loadProducts()}
function clearProductFilter(){Object.assign(productFilter,{name:'',priceMin:null,priceMax:null});productPager.page=1;selectedProduct.value=undefined;matchForm.productId=0;loadProducts();message.success('商品列表已刷新')}
function selectProduct(row){selectedProduct.value=row;matchForm.productId=row.id}
async function confirmMatch(){
if(!matchForm.productId){message.error('请先选择商品');return}
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}
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.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)=>{
clearInterval(timer);refreshCountdown.value=10;
if(!enabled)return;
timer=setInterval(()=>{
refreshCountdown.value-=1;
if(refreshCountdown.value>0)return;
reloadTable();refreshCountdown.value=10;
},1000);
});
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; }
</style>