feat: 新增商城模块,新增会员中心的会员详情的订单管理,售后管理,收藏记录,优惠券,推广用户的展示
This commit is contained in:
140
apps/web-ele/src/views/mall/trade/afterSale/data.ts
Normal file
140
apps/web-ele/src/views/mall/trade/afterSale/data.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeGridPropTypes } from '#/adapter/vxe-table';
|
||||
|
||||
import { DICT_TYPE, getDictOptions, getRangePickerDefaultProps } from '#/utils';
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'spuName',
|
||||
label: '商品名称',
|
||||
component: 'Input',
|
||||
},
|
||||
{
|
||||
fieldName: 'no',
|
||||
label: '退款编号',
|
||||
component: 'Input',
|
||||
},
|
||||
{
|
||||
fieldName: 'orderNo',
|
||||
label: '订单编号',
|
||||
component: 'Input',
|
||||
},
|
||||
{
|
||||
fieldName: 'status',
|
||||
label: '售后状态',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.TRADE_AFTER_SALE_STATUS, 'number'),
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'status',
|
||||
label: '售后方式',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.TRADE_AFTER_SALE_WAY, 'number'),
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'type',
|
||||
label: '售后类型',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.TRADE_AFTER_SALE_TYPE, 'number'),
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'createTime',
|
||||
label: '创建时间',
|
||||
component: 'RangePicker',
|
||||
componentProps: {
|
||||
...getRangePickerDefaultProps(),
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 表格列配置 */
|
||||
export function useGridColumns(): VxeGridPropTypes.Columns {
|
||||
return [
|
||||
{
|
||||
field: 'no',
|
||||
title: '退款编号',
|
||||
fixed: 'left',
|
||||
},
|
||||
{
|
||||
field: 'orderNo',
|
||||
title: '订单编号',
|
||||
fixed: 'left',
|
||||
slots: { default: 'orderNo' },
|
||||
},
|
||||
{
|
||||
field: 'spuName',
|
||||
title: '商品名称',
|
||||
align: 'left',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'picUrl',
|
||||
title: '商品图片',
|
||||
cellRender: {
|
||||
name: 'CellImage',
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'properties',
|
||||
title: '商品属性',
|
||||
minWidth: 200,
|
||||
formatter: ({ cellValue }) => {
|
||||
return cellValue && cellValue.length > 0
|
||||
? cellValue
|
||||
.map((item: any) => `${item.propertyName} : ${item.valueName}`)
|
||||
.join('\n')
|
||||
: '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'refundPrice',
|
||||
title: '订单金额',
|
||||
formatter: 'formatAmount2',
|
||||
},
|
||||
{
|
||||
field: 'user.nickname',
|
||||
title: '买家',
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '申请时间',
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'content',
|
||||
title: '售后状态',
|
||||
cellRender: {
|
||||
name: 'CellDictTag',
|
||||
props: {
|
||||
dictType: DICT_TYPE.TRADE_AFTER_SALE_STATUS,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'way',
|
||||
title: '售后方式',
|
||||
cellRender: {
|
||||
name: 'CellDictTag',
|
||||
props: {
|
||||
dictType: DICT_TYPE.TRADE_AFTER_SALE_WAY,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 80,
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
120
apps/web-ele/src/views/mall/trade/afterSale/index.vue
Normal file
120
apps/web-ele/src/views/mall/trade/afterSale/index.vue
Normal file
@@ -0,0 +1,120 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { MallAfterSaleApi } from '#/api/mall/trade/afterSale';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { DocAlert, Page } from '@vben/common-ui';
|
||||
|
||||
import { ElButton, ElTabs } from 'element-plus';
|
||||
|
||||
import { TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getAfterSalePage } from '#/api/mall/trade/afterSale';
|
||||
import { DICT_TYPE, getDictOptions } from '#/utils';
|
||||
|
||||
import { useGridColumns, useGridFormSchema } from './data';
|
||||
|
||||
const { push } = useRouter();
|
||||
|
||||
const status = ref('0');
|
||||
const statusTabs = ref([
|
||||
{
|
||||
label: '全部',
|
||||
value: '0',
|
||||
},
|
||||
]);
|
||||
|
||||
/** 处理退款 */
|
||||
function openAfterSaleDetail(row: MallAfterSaleApi.AfterSale) {
|
||||
push({ name: 'TradeAfterSaleDetail', params: { id: row.id } });
|
||||
}
|
||||
|
||||
// TODO @xingyu:缺详情页
|
||||
/** 查看订单详情 */
|
||||
function openOrderDetail(row: MallAfterSaleApi.AfterSale) {
|
||||
push({ name: 'TradeOrderDetail', params: { id: row.id } });
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getAfterSalePage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
status: status.value === '0' ? undefined : status.value,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: { code: 'query' },
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<MallAfterSaleApi.AfterSale>,
|
||||
});
|
||||
|
||||
function onChangeStatus(key: number | string) {
|
||||
status.value = key.toString();
|
||||
gridApi.query();
|
||||
}
|
||||
onMounted(() => {
|
||||
for (const dict of getDictOptions(DICT_TYPE.TRADE_AFTER_SALE_STATUS)) {
|
||||
statusTabs.value.push({
|
||||
label: dict.label,
|
||||
value: dict.value.toString(),
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<template #doc>
|
||||
<DocAlert
|
||||
title="【交易】交易订单"
|
||||
url="https://doc.iocoder.cn/mall/trade-order/"
|
||||
/>
|
||||
</template>
|
||||
<Grid table-title="退款列表">
|
||||
<template #top>
|
||||
<ElTabs class="border-none" @change="onChangeStatus">
|
||||
<ElTabs.TabPane
|
||||
v-for="tab in statusTabs"
|
||||
:key="tab.value"
|
||||
:tab="tab.label"
|
||||
/>
|
||||
</ElTabs>
|
||||
</template>
|
||||
<template #orderNo="{ row }">
|
||||
<ElButton type="primary" link @click="openOrderDetail(row)">
|
||||
{{ row.orderNo }}
|
||||
</ElButton>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '处理退款',
|
||||
type: 'primary',
|
||||
link: true,
|
||||
onClick: openAfterSaleDetail.bind(null, row),
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
135
apps/web-ele/src/views/mall/trade/brokerage/record/data.ts
Normal file
135
apps/web-ele/src/views/mall/trade/brokerage/record/data.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
import { fenToYuan } from '@vben/utils';
|
||||
|
||||
import { DICT_TYPE, getDictOptions, getRangePickerDefaultProps } from '#/utils';
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'userId',
|
||||
label: '用户编号',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入用户编号',
|
||||
clearable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'bizType',
|
||||
label: '业务类型',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
placeholder: '请选择业务类型',
|
||||
clearable: true,
|
||||
options: getDictOptions(DICT_TYPE.BROKERAGE_RECORD_BIZ_TYPE, 'number'),
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'status',
|
||||
label: '状态',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
placeholder: '请选择状态',
|
||||
clearable: true,
|
||||
options: getDictOptions(DICT_TYPE.BROKERAGE_RECORD_STATUS, 'number'),
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'createTime',
|
||||
label: '创建时间',
|
||||
component: 'RangePicker',
|
||||
componentProps: {
|
||||
...getRangePickerDefaultProps(),
|
||||
clearable: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'id',
|
||||
title: '编号',
|
||||
minWidth: 60,
|
||||
},
|
||||
{
|
||||
field: 'userId',
|
||||
title: '用户编号',
|
||||
minWidth: 80,
|
||||
},
|
||||
{
|
||||
field: 'userAvatar',
|
||||
title: '头像',
|
||||
minWidth: 70,
|
||||
cellRender: {
|
||||
name: 'CellImage',
|
||||
props: {
|
||||
height: 40,
|
||||
width: 40,
|
||||
shape: 'circle',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'userNickname',
|
||||
title: '昵称',
|
||||
minWidth: 80,
|
||||
},
|
||||
{
|
||||
field: 'bizType',
|
||||
title: '业务类型',
|
||||
minWidth: 85,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.BROKERAGE_RECORD_BIZ_TYPE },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'bizId',
|
||||
title: '业务编号',
|
||||
minWidth: 80,
|
||||
},
|
||||
{
|
||||
field: 'title',
|
||||
title: '标题',
|
||||
minWidth: 110,
|
||||
},
|
||||
{
|
||||
field: 'price',
|
||||
title: '金额',
|
||||
minWidth: 60,
|
||||
formatter: ({ row }) => `¥${fenToYuan(row.price)}`,
|
||||
},
|
||||
{
|
||||
field: 'description',
|
||||
title: '说明',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
title: '状态',
|
||||
minWidth: 85,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.BROKERAGE_RECORD_STATUS },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'unfreezeTime',
|
||||
title: '解冻时间',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '创建时间',
|
||||
width: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
];
|
||||
}
|
||||
57
apps/web-ele/src/views/mall/trade/brokerage/record/index.vue
Normal file
57
apps/web-ele/src/views/mall/trade/brokerage/record/index.vue
Normal file
@@ -0,0 +1,57 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { MallBrokerageRecordApi } from '#/api/mall/trade/brokerage/record';
|
||||
|
||||
import { DocAlert, Page } from '@vben/common-ui';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getBrokerageRecordPage } from '#/api/mall/trade/brokerage/record';
|
||||
|
||||
import { useGridColumns, useGridFormSchema } from './data';
|
||||
|
||||
defineOptions({ name: 'TradeBrokerageRecord' });
|
||||
|
||||
const [Grid] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
showOverflow: 'tooltip',
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getBrokerageRecordPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: { code: 'query' },
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<MallBrokerageRecordApi.BrokerageRecord>,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<template #doc>
|
||||
<DocAlert
|
||||
title="【交易】分销返佣"
|
||||
url="https://doc.iocoder.cn/mall/trade-brokerage/"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<Grid table-title="分销返佣记录" />
|
||||
</Page>
|
||||
</template>
|
||||
140
apps/web-ele/src/views/mall/trade/brokerage/user/data.ts
Normal file
140
apps/web-ele/src/views/mall/trade/brokerage/user/data.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
import { fenToYuan } from '@vben/utils';
|
||||
|
||||
import { getRangePickerDefaultProps } from '#/utils';
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'bindUserId',
|
||||
label: '推广员编号',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入推广员编号',
|
||||
clearable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'brokerageEnabled',
|
||||
label: '推广资格',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
placeholder: '请选择推广资格',
|
||||
clearable: true,
|
||||
options: [
|
||||
{ label: '有', value: true },
|
||||
{ label: '无', value: false },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'createTime',
|
||||
label: '创建时间',
|
||||
component: 'RangePicker',
|
||||
componentProps: {
|
||||
...getRangePickerDefaultProps(),
|
||||
clearable: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'id',
|
||||
title: '用户编号',
|
||||
minWidth: 80,
|
||||
},
|
||||
{
|
||||
field: 'avatar',
|
||||
title: '头像',
|
||||
minWidth: 70,
|
||||
cellRender: {
|
||||
name: 'CellImage',
|
||||
props: {
|
||||
width: 24,
|
||||
height: 24,
|
||||
shape: 'circle',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'nickname',
|
||||
title: '昵称',
|
||||
minWidth: 80,
|
||||
},
|
||||
{
|
||||
field: 'brokerageUserCount',
|
||||
title: '推广人数',
|
||||
minWidth: 80,
|
||||
},
|
||||
{
|
||||
field: 'brokerageOrderCount',
|
||||
title: '推广订单数量',
|
||||
minWidth: 110,
|
||||
},
|
||||
{
|
||||
field: 'brokerageOrderPrice',
|
||||
title: '推广订单金额',
|
||||
minWidth: 110,
|
||||
formatter: ({ row }) => `¥${fenToYuan(row.brokerageOrderPrice)}`,
|
||||
},
|
||||
{
|
||||
field: 'withdrawPrice',
|
||||
title: '已提现金额',
|
||||
minWidth: 100,
|
||||
formatter: ({ row }) => `¥${fenToYuan(row.withdrawPrice)}`,
|
||||
},
|
||||
{
|
||||
field: 'withdrawCount',
|
||||
title: '已提现次数',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
field: 'price',
|
||||
title: '未提现金额',
|
||||
minWidth: 100,
|
||||
formatter: ({ row }) => `¥${fenToYuan(row.price)}`,
|
||||
},
|
||||
{
|
||||
field: 'frozenPrice',
|
||||
title: '冻结中佣金',
|
||||
minWidth: 100,
|
||||
formatter: ({ row }) => `¥${fenToYuan(row.frozenPrice)}`,
|
||||
},
|
||||
{
|
||||
field: 'brokerageEnabled',
|
||||
title: '推广资格',
|
||||
minWidth: 80,
|
||||
slots: { default: 'brokerageEnabled' },
|
||||
},
|
||||
{
|
||||
field: 'brokerageTime',
|
||||
title: '成为推广员时间',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'bindUserId',
|
||||
title: '上级推广员编号',
|
||||
minWidth: 150,
|
||||
},
|
||||
{
|
||||
field: 'bindUserTime',
|
||||
title: '推广员绑定时间',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 150,
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
221
apps/web-ele/src/views/mall/trade/brokerage/user/index.vue
Normal file
221
apps/web-ele/src/views/mall/trade/brokerage/user/index.vue
Normal file
@@ -0,0 +1,221 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { MallBrokerageUserApi } from '#/api/mall/trade/brokerage/user';
|
||||
|
||||
import { useAccess } from '@vben/access';
|
||||
import { DocAlert, Page, useVbenModal } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { ElLoading, ElMessage, ElSwitch } from 'element-plus';
|
||||
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
clearBindUser,
|
||||
getBrokerageUserPage,
|
||||
updateBrokerageEnabled,
|
||||
} from '#/api/mall/trade/brokerage/user';
|
||||
|
||||
import { useGridColumns, useGridFormSchema } from './data';
|
||||
import BrokerageOrderListModal from './modules/order-list-modal.vue';
|
||||
import BrokerageUserCreateForm from './modules/user-create-form.vue';
|
||||
import BrokerageUserListModal from './modules/user-list-modal.vue';
|
||||
import BrokerageUserUpdateForm from './modules/user-update-form.vue';
|
||||
|
||||
defineOptions({ name: 'TradeBrokerageUser' });
|
||||
|
||||
const { hasAccessByCodes } = useAccess();
|
||||
|
||||
/** 刷新表格 */
|
||||
function onRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
const [OrderListModal, OrderListModalApi] = useVbenModal({
|
||||
connectedComponent: BrokerageOrderListModal,
|
||||
});
|
||||
|
||||
const [UserCreateModal, UserCreateModalApi] = useVbenModal({
|
||||
connectedComponent: BrokerageUserCreateForm,
|
||||
});
|
||||
|
||||
const [UserListModal, UserListModalApi] = useVbenModal({
|
||||
connectedComponent: BrokerageUserListModal,
|
||||
});
|
||||
|
||||
const [UserUpdateModal, UserUpdateModalApi] = useVbenModal({
|
||||
connectedComponent: BrokerageUserUpdateForm,
|
||||
});
|
||||
|
||||
/** 打开推广人列表 */
|
||||
function openBrokerageUserTable(row: MallBrokerageUserApi.BrokerageUser) {
|
||||
UserListModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 打开推广订单列表 */
|
||||
function openBrokerageOrderTable(row: MallBrokerageUserApi.BrokerageUser) {
|
||||
OrderListModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 打开表单:修改上级推广人 */
|
||||
function openUpdateBindUserForm(row: MallBrokerageUserApi.BrokerageUser) {
|
||||
UserUpdateModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 创建分销员 */
|
||||
function openCreateUserForm() {
|
||||
UserCreateModalApi.open();
|
||||
}
|
||||
|
||||
/** 清除上级推广人 */
|
||||
async function handleClearBindUser(row: MallBrokerageUserApi.BrokerageUser) {
|
||||
const loadingInstance = ElLoading.service({
|
||||
text: `正在清除"${row.nickname}"的上级推广人...`,
|
||||
fullscreen: true,
|
||||
});
|
||||
try {
|
||||
await clearBindUser({ id: row.id as number });
|
||||
ElMessage.success('清除成功');
|
||||
onRefresh();
|
||||
} finally {
|
||||
loadingInstance.close();
|
||||
}
|
||||
}
|
||||
|
||||
/** 推广资格:开通/关闭 */
|
||||
async function handleBrokerageEnabledChange(
|
||||
row: MallBrokerageUserApi.BrokerageUser,
|
||||
) {
|
||||
const text = row.brokerageEnabled ? '开通' : '关闭';
|
||||
const loadingInstance = ElLoading.service({
|
||||
text: `正在${text}"${row.nickname}"的推广资格...`,
|
||||
fullscreen: true,
|
||||
});
|
||||
try {
|
||||
await updateBrokerageEnabled({
|
||||
id: row.id as number,
|
||||
enabled: row.brokerageEnabled as boolean,
|
||||
});
|
||||
ElMessage.success(`${text}成功`);
|
||||
onRefresh();
|
||||
} catch {
|
||||
// 异常时,需要重置回之前的值
|
||||
row.brokerageEnabled = !row.brokerageEnabled;
|
||||
} finally {
|
||||
loadingInstance.close();
|
||||
}
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
showOverflow: 'tooltip',
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getBrokerageUserPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: { code: 'query' },
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<MallBrokerageUserApi.BrokerageUser>,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<template #doc>
|
||||
<DocAlert
|
||||
title="【交易】分销返佣"
|
||||
url="https://doc.iocoder.cn/mall/trade-brokerage/"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<Grid table-title="分销用户列表">
|
||||
<template #toolbar-tools>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('ui.actionTitle.create', ['分销员']),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.ADD,
|
||||
auth: ['trade:brokerage-user:create'],
|
||||
onClick: openCreateUserForm,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #brokerageEnabled="{ row }">
|
||||
<ElSwitch
|
||||
v-model:checked="row.brokerageEnabled"
|
||||
:disabled="
|
||||
!hasAccessByCodes(['trade:brokerage-user:update-bind-user'])
|
||||
"
|
||||
checked-children="有"
|
||||
un-checked-children="无"
|
||||
@change="handleBrokerageEnabledChange(row)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:drop-down-actions="[
|
||||
{
|
||||
label: '推广人',
|
||||
type: 'primary',
|
||||
link: true,
|
||||
auth: ['trade:brokerage-user:user-query'],
|
||||
onClick: openBrokerageUserTable.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: '推广订单',
|
||||
type: 'primary',
|
||||
link: true,
|
||||
auth: ['trade:brokerage-user:order-query'],
|
||||
onClick: openBrokerageOrderTable.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: '修改上级推广人',
|
||||
type: 'primary',
|
||||
link: true,
|
||||
auth: ['trade:brokerage-user:update-bind-user'],
|
||||
onClick: openUpdateBindUserForm.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: '清除上级推广人',
|
||||
type: 'primary',
|
||||
link: true,
|
||||
auth: ['trade:brokerage-user:clear-bind-user'],
|
||||
onClick: handleClearBindUser.bind(null, row),
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
|
||||
<!-- 修改上级推广人表单 -->
|
||||
<UserUpdateModal @success="onRefresh" />
|
||||
<!-- 推广人列表 -->
|
||||
<UserListModal />
|
||||
<!-- 推广订单列表 -->
|
||||
<OrderListModal />
|
||||
<!-- 创建分销员 -->
|
||||
<UserCreateModal @success="onRefresh" />
|
||||
</Page>
|
||||
</template>
|
||||
@@ -0,0 +1,185 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { MallBrokerageRecordApi } from '#/api/mall/trade/brokerage/record';
|
||||
import type { MallBrokerageUserApi } from '#/api/mall/trade/brokerage/user';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { fenToYuan } from '@vben/utils';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getBrokerageRecordPage } from '#/api/mall/trade/brokerage/record';
|
||||
import { DICT_TYPE, getDictOptions, getRangePickerDefaultProps } from '#/utils';
|
||||
import { BrokerageRecordBizTypeEnum } from '#/utils/constants';
|
||||
|
||||
/** 推广订单列表 */
|
||||
defineOptions({ name: 'BrokerageOrderListModal' });
|
||||
|
||||
const userId = ref<number>();
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
userId.value = undefined;
|
||||
return;
|
||||
}
|
||||
// 加载数据
|
||||
const data = modalApi.getData<MallBrokerageUserApi.BrokerageUser>();
|
||||
if (!data || !data.id) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
try {
|
||||
userId.value = data.id;
|
||||
// 等待弹窗打开后再查询
|
||||
setTimeout(() => {
|
||||
gridApi.query();
|
||||
}, 100);
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
/** 搜索表单配置 */
|
||||
function useFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'sourceUserLevel',
|
||||
label: '用户类型',
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
options: [
|
||||
{ label: '全部', value: 0 },
|
||||
{ label: '一级推广人', value: 1 },
|
||||
{ label: '二级推广人', value: 2 },
|
||||
],
|
||||
buttonStyle: 'solid',
|
||||
optionType: 'button',
|
||||
},
|
||||
defaultValue: 0,
|
||||
},
|
||||
{
|
||||
fieldName: 'status',
|
||||
label: '状态',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
placeholder: '请选择状态',
|
||||
clearable: true,
|
||||
options: getDictOptions(DICT_TYPE.BROKERAGE_RECORD_STATUS, 'number'),
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'createTime',
|
||||
label: '创建时间',
|
||||
component: 'RangePicker',
|
||||
componentProps: {
|
||||
...getRangePickerDefaultProps(),
|
||||
clearable: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 表格列配置 */
|
||||
function useColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'bizId',
|
||||
title: '订单编号',
|
||||
minWidth: 80,
|
||||
},
|
||||
{
|
||||
field: 'sourceUserId',
|
||||
title: '用户编号',
|
||||
minWidth: 80,
|
||||
},
|
||||
{
|
||||
field: 'sourceUserAvatar',
|
||||
title: '头像',
|
||||
minWidth: 70,
|
||||
cellRender: {
|
||||
name: 'CellImage',
|
||||
props: {
|
||||
width: 24,
|
||||
height: 24,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'sourceUserNickname',
|
||||
title: '昵称',
|
||||
minWidth: 80,
|
||||
},
|
||||
{
|
||||
field: 'price',
|
||||
title: '佣金',
|
||||
minWidth: 100,
|
||||
formatter: ({ row }) => `¥${fenToYuan(row.price)}`,
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
title: '状态',
|
||||
minWidth: 85,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.BROKERAGE_RECORD_STATUS },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '创建时间',
|
||||
width: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useColumns(),
|
||||
height: '600',
|
||||
keepSource: true,
|
||||
showOverflow: 'tooltip',
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
// 处理全部的情况
|
||||
const params = {
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
userId: userId.value,
|
||||
bizType: BrokerageRecordBizTypeEnum.ORDER.type,
|
||||
sourceUserLevel:
|
||||
formValues.sourceUserLevel === 0
|
||||
? undefined
|
||||
: formValues.sourceUserLevel,
|
||||
status: formValues.status,
|
||||
createTime: formValues.createTime,
|
||||
};
|
||||
return await getBrokerageRecordPage(params);
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: { code: 'query' },
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<MallBrokerageRecordApi.BrokerageRecord>,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal title="推广订单列表" class="w-3/5">
|
||||
<Grid table-title="推广订单列表" />
|
||||
</Modal>
|
||||
</template>
|
||||
@@ -0,0 +1,181 @@
|
||||
<script lang="ts" setup>
|
||||
import type { MallBrokerageUserApi } from '#/api/mall/trade/brokerage/user';
|
||||
|
||||
import { reactive, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
import { formatDate, isEmpty } from '@vben/utils';
|
||||
|
||||
import {
|
||||
ElAvatar,
|
||||
ElDescriptions,
|
||||
ElDescriptionsItem,
|
||||
ElInput,
|
||||
ElMessage,
|
||||
ElTag,
|
||||
} from 'element-plus';
|
||||
|
||||
import {
|
||||
createBrokerageUser,
|
||||
getBrokerageUser,
|
||||
} from '#/api/mall/trade/brokerage/user';
|
||||
import { getUser } from '#/api/member/user';
|
||||
|
||||
defineOptions({ name: 'BrokerageUserCreateForm' });
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
|
||||
const formData = ref<any>({
|
||||
userId: undefined,
|
||||
bindUserId: undefined,
|
||||
});
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
if (!formData.value) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
// 提交表单
|
||||
try {
|
||||
await createBrokerageUser(formData.value);
|
||||
// 关闭并提示
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
ElMessage.success($t('ui.actionMessage.operationSuccess'));
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
onOpenChange: async (isOpen: boolean) => {
|
||||
if (!isOpen) {
|
||||
return;
|
||||
}
|
||||
formData.value = {
|
||||
userId: undefined,
|
||||
bindUserId: undefined,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
/** 用户信息 */
|
||||
const userInfo = reactive<{
|
||||
bindUser: MallBrokerageUserApi.BrokerageUser | undefined;
|
||||
user: MallBrokerageUserApi.BrokerageUser | undefined;
|
||||
}>({
|
||||
bindUser: undefined,
|
||||
user: undefined,
|
||||
});
|
||||
|
||||
/** 查询推广员和分销员 */
|
||||
async function handleGetUser(id: any, userType: string) {
|
||||
if (isEmpty(id)) {
|
||||
ElMessage.warning(`请先输入${userType}编号后重试!!!`);
|
||||
return;
|
||||
}
|
||||
if (
|
||||
userType === '推广员' &&
|
||||
formData.value?.bindUserId === formData.value?.userId
|
||||
) {
|
||||
ElMessage.error('不能绑定自己为推广人');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const user =
|
||||
userType === '推广员' ? await getBrokerageUser(id) : await getUser(id);
|
||||
if (userType === '推广员') {
|
||||
userInfo.bindUser = user as MallBrokerageUserApi.BrokerageUser;
|
||||
} else {
|
||||
userInfo.user = user as MallBrokerageUserApi.BrokerageUser;
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
ElMessage.warning(`${userType}不存在`);
|
||||
}
|
||||
} catch {
|
||||
ElMessage.warning(`${userType}不存在`);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal title="创建分销员" class="w-2/5">
|
||||
<div class="mr-2 flex items-center">
|
||||
分销员编号:
|
||||
<ElInput
|
||||
v-model="formData.userId"
|
||||
placeholder="请输入推广员编号"
|
||||
class="mx-2 !w-52"
|
||||
>
|
||||
<template #append>
|
||||
<ElButton
|
||||
type="primary"
|
||||
@click="handleGetUser(formData?.userId, '分销员')"
|
||||
>
|
||||
<IconifyIcon icon="lucide:search" :size="15" />
|
||||
</ElButton>
|
||||
</template>
|
||||
</ElInput>
|
||||
上级推广人编号:
|
||||
<ElInput
|
||||
v-model="formData.bindUserId"
|
||||
placeholder="请输入推广员编号"
|
||||
class="mx-2 !w-52"
|
||||
>
|
||||
<template #append>
|
||||
<ElButton
|
||||
type="primary"
|
||||
@click="handleGetUser(formData?.bindUserId, '推广员')"
|
||||
>
|
||||
<IconifyIcon icon="lucide:search" :size="15" />
|
||||
</ElButton>
|
||||
</template>
|
||||
</ElInput>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<!-- 展示分销员的信息 -->
|
||||
<ElDescriptions
|
||||
title="分销员信息"
|
||||
class="mt-4"
|
||||
v-if="userInfo.user"
|
||||
:column="1"
|
||||
bordered
|
||||
>
|
||||
<ElDescriptionsItem label="头像">
|
||||
<ElAvatar :src="userInfo.user?.avatar" />
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="昵称">
|
||||
{{ userInfo.user?.nickname }}
|
||||
</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
<!-- 展示上级推广人的信息 -->
|
||||
<ElDescriptions
|
||||
title="上级推广人信息"
|
||||
class="mt-4"
|
||||
v-if="userInfo.bindUser"
|
||||
:column="1"
|
||||
bordered
|
||||
>
|
||||
<ElDescriptionsItem label="头像">
|
||||
<ElAvatar :src="userInfo.bindUser?.avatar" />
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="昵称">
|
||||
{{ userInfo.bindUser?.nickname }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="推广资格">
|
||||
<ElTag v-if="userInfo.bindUser?.brokerageEnabled" color="success">
|
||||
有
|
||||
</ElTag>
|
||||
<ElTag v-else>无</ElTag>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="成为推广员的时间">
|
||||
{{ formatDate(userInfo.bindUser?.brokerageTime) }}
|
||||
</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
@@ -0,0 +1,160 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { MallBrokerageUserApi } from '#/api/mall/trade/brokerage/user';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { ElTag } from 'element-plus';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getBrokerageUserPage } from '#/api/mall/trade/brokerage/user';
|
||||
import { getRangePickerDefaultProps } from '#/utils';
|
||||
|
||||
defineOptions({ name: 'BrokerageUserListModal' });
|
||||
|
||||
const bindUserId = ref<number>();
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
onOpenChange: async (isOpen: boolean) => {
|
||||
if (!isOpen) {
|
||||
bindUserId.value = undefined;
|
||||
return;
|
||||
}
|
||||
const data = modalApi.getData<MallBrokerageUserApi.BrokerageUser>();
|
||||
if (!data || !data.id) {
|
||||
return;
|
||||
}
|
||||
bindUserId.value = data.id;
|
||||
// 等待弹窗打开后再查询
|
||||
setTimeout(() => {
|
||||
gridApi.query();
|
||||
}, 100);
|
||||
},
|
||||
});
|
||||
|
||||
/** 搜索表单配置 */
|
||||
function useFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'level',
|
||||
label: '用户类型',
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
options: [
|
||||
{ label: '全部', value: undefined },
|
||||
{ label: '一级推广人', value: '1' },
|
||||
{ label: '二级推广人', value: '2' },
|
||||
],
|
||||
buttonStyle: 'solid',
|
||||
optionType: 'button',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'bindUserTime',
|
||||
label: '绑定时间',
|
||||
component: 'RangePicker',
|
||||
componentProps: {
|
||||
...getRangePickerDefaultProps(),
|
||||
clearable: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 表格列配置 */
|
||||
function useColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'id',
|
||||
title: '用户编号',
|
||||
minWidth: 80,
|
||||
},
|
||||
{
|
||||
field: 'avatar',
|
||||
title: '头像',
|
||||
minWidth: 70,
|
||||
cellRender: {
|
||||
name: 'CellImage',
|
||||
props: {
|
||||
width: 24,
|
||||
height: 24,
|
||||
shape: 'circle',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'nickname',
|
||||
title: '昵称',
|
||||
minWidth: 80,
|
||||
},
|
||||
{
|
||||
field: 'brokerageUserCount',
|
||||
title: '推广人数',
|
||||
minWidth: 80,
|
||||
},
|
||||
{
|
||||
field: 'brokerageOrderCount',
|
||||
title: '推广订单数量',
|
||||
minWidth: 110,
|
||||
},
|
||||
{
|
||||
field: 'brokerageEnabled',
|
||||
title: '推广资格',
|
||||
minWidth: 80,
|
||||
slots: { default: 'brokerageEnabled' },
|
||||
},
|
||||
{
|
||||
field: 'bindUserTime',
|
||||
title: '绑定时间',
|
||||
width: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useColumns(),
|
||||
height: '600',
|
||||
keepSource: true,
|
||||
showOverflow: 'tooltip',
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getBrokerageUserPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
bindUserId: bindUserId.value,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: { code: 'query' },
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<MallBrokerageUserApi.BrokerageUser>,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal title="推广人列表" class="w-3/5">
|
||||
<Grid table-title="推广人列表">
|
||||
<template #brokerageEnabled="{ row }">
|
||||
<ElTag v-if="row.brokerageEnabled" color="success">有</ElTag>
|
||||
<ElTag v-else>无</ElTag>
|
||||
</template>
|
||||
</Grid>
|
||||
</Modal>
|
||||
</template>
|
||||
@@ -0,0 +1,136 @@
|
||||
<script lang="ts" setup>
|
||||
import type { MallBrokerageUserApi } from '#/api/mall/trade/brokerage/user';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
import { formatDate } from '@vben/utils';
|
||||
|
||||
import {
|
||||
ElAvatar,
|
||||
ElDescriptions,
|
||||
ElDescriptionsItem,
|
||||
ElInput,
|
||||
ElMessage,
|
||||
ElTag,
|
||||
} from 'element-plus';
|
||||
|
||||
import {
|
||||
getBrokerageUser,
|
||||
updateBindUser,
|
||||
} from '#/api/mall/trade/brokerage/user';
|
||||
|
||||
/** 修改分销用户 */
|
||||
defineOptions({ name: 'BrokerageUserUpdateForm' });
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
|
||||
const formData = ref<any>();
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
if (!formData.value) {
|
||||
return;
|
||||
}
|
||||
// 未查找到合适的上级
|
||||
if (!bindUser.value) {
|
||||
ElMessage.error('请先查询并确认推广人');
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
try {
|
||||
await updateBindUser(formData.value);
|
||||
// 关闭并提示
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
ElMessage.success($t('ui.actionMessage.operationSuccess'));
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
onOpenChange: async (isOpen: boolean) => {
|
||||
if (!isOpen) {
|
||||
formData.value = {
|
||||
id: 0,
|
||||
bindUserId: 0,
|
||||
};
|
||||
return;
|
||||
}
|
||||
const data = modalApi.getData<MallBrokerageUserApi.BrokerageUser>();
|
||||
if (!data || !data.id) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
try {
|
||||
formData.value = {
|
||||
id: data.id,
|
||||
bindUserId: data.bindUserId,
|
||||
};
|
||||
if (data.bindUserId) {
|
||||
await handleGetUser();
|
||||
}
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const bindUser = ref<MallBrokerageUserApi.BrokerageUser>();
|
||||
|
||||
/** 查询推广员 */
|
||||
async function handleGetUser() {
|
||||
if (!formData.value) {
|
||||
return;
|
||||
}
|
||||
if (formData.value.bindUserId === formData.value.id) {
|
||||
ElMessage.error('不能绑定自己为推广人');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
bindUser.value = await getBrokerageUser(formData.value.bindUserId);
|
||||
if (!bindUser.value) {
|
||||
ElMessage.warning('推广员不存在');
|
||||
}
|
||||
} catch {
|
||||
ElMessage.warning('推广员不存在');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal title="修改上级推广人" class="w-2/5">
|
||||
<div class="mr-2 flex items-center">
|
||||
推广员编号:
|
||||
<ElInput
|
||||
v-model="formData.bindUserId"
|
||||
placeholder="请输入推广员编号"
|
||||
class="mx-2 !w-52"
|
||||
>
|
||||
<template #append>
|
||||
<ElButton type="primary" @click="handleGetUser">
|
||||
<IconifyIcon icon="lucide:search" :size="15" />
|
||||
</ElButton>
|
||||
</template>
|
||||
</ElInput>
|
||||
</div>
|
||||
|
||||
<!-- 展示上级推广人的信息 -->
|
||||
<ElDescriptions class="mt-4" v-if="bindUser" :column="1" bordered>
|
||||
<ElDescriptionsItem label="头像">
|
||||
<ElAvatar :src="bindUser.avatar" />
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="昵称">
|
||||
{{ bindUser.nickname }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="推广资格">
|
||||
<ElTag v-if="bindUser.brokerageEnabled" color="success">有</ElTag>
|
||||
<ElTag v-else>无</ElTag>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="成为推广员的时间">
|
||||
{{ formatDate(bindUser.brokerageTime) }}
|
||||
</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
</Modal>
|
||||
</template>
|
||||
145
apps/web-ele/src/views/mall/trade/brokerage/withdraw/data.ts
Normal file
145
apps/web-ele/src/views/mall/trade/brokerage/withdraw/data.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
import { DICT_TYPE, getDictOptions, getRangePickerDefaultProps } from '#/utils';
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'userId',
|
||||
label: '用户编号',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入用户编号',
|
||||
clearable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'type',
|
||||
label: '提现类型',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
placeholder: '请选择提现类型',
|
||||
clearable: true,
|
||||
options: getDictOptions(DICT_TYPE.BROKERAGE_WITHDRAW_TYPE, 'number'),
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'userAccount',
|
||||
label: '账号',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入账号',
|
||||
clearable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'userName',
|
||||
label: '真实名字',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入真实名字',
|
||||
clearable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'bankName',
|
||||
label: '提现银行',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
placeholder: '请选择提现银行',
|
||||
clearable: true,
|
||||
options: getDictOptions(DICT_TYPE.BROKERAGE_BANK_NAME, 'string'),
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'status',
|
||||
label: '状态',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
placeholder: '请选择状态',
|
||||
clearable: true,
|
||||
options: getDictOptions(DICT_TYPE.BROKERAGE_WITHDRAW_STATUS, 'number'),
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'createTime',
|
||||
label: '申请时间',
|
||||
component: 'RangePicker',
|
||||
componentProps: {
|
||||
...getRangePickerDefaultProps(),
|
||||
clearable: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'id',
|
||||
title: '编号',
|
||||
minWidth: 80,
|
||||
},
|
||||
{
|
||||
field: 'userId',
|
||||
title: '用户编号:',
|
||||
minWidth: 80,
|
||||
},
|
||||
{
|
||||
field: 'userNickname',
|
||||
title: '用户昵称:',
|
||||
minWidth: 80,
|
||||
},
|
||||
{
|
||||
field: 'price',
|
||||
title: '提现金额',
|
||||
minWidth: 80,
|
||||
formatter: 'formatAmount2',
|
||||
},
|
||||
{
|
||||
field: 'feePrice',
|
||||
title: '提现手续费',
|
||||
minWidth: 80,
|
||||
formatter: 'formatAmount2',
|
||||
},
|
||||
{
|
||||
field: 'type',
|
||||
title: '提现方式',
|
||||
minWidth: 120,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.BROKERAGE_WITHDRAW_TYPE },
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '提现信息',
|
||||
minWidth: 200,
|
||||
slots: { default: 'withdraw-info' },
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '申请时间',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'remark',
|
||||
title: '备注',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
minWidth: 200,
|
||||
slots: { default: 'status-info' },
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 150,
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
195
apps/web-ele/src/views/mall/trade/brokerage/withdraw/index.vue
Normal file
195
apps/web-ele/src/views/mall/trade/brokerage/withdraw/index.vue
Normal file
@@ -0,0 +1,195 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { MallBrokerageWithdrawApi } from '#/api/mall/trade/brokerage/withdraw';
|
||||
|
||||
import { h } from 'vue';
|
||||
|
||||
import { confirm, Page, prompt } from '@vben/common-ui';
|
||||
import { formatDateTime } from '@vben/utils';
|
||||
|
||||
import { ElInput, ElMessage } from 'element-plus';
|
||||
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
approveBrokerageWithdraw,
|
||||
getBrokerageWithdrawPage,
|
||||
rejectBrokerageWithdraw,
|
||||
} from '#/api/mall/trade/brokerage/withdraw';
|
||||
import { DictTag } from '#/components/dict-tag';
|
||||
import { $t } from '#/locales';
|
||||
import {
|
||||
BrokerageWithdrawStatusEnum,
|
||||
BrokerageWithdrawTypeEnum,
|
||||
DICT_TYPE,
|
||||
} from '#/utils';
|
||||
|
||||
import { useGridColumns, useGridFormSchema } from './data';
|
||||
|
||||
/** 分销佣金提现 */
|
||||
defineOptions({ name: 'BrokerageWithdraw' });
|
||||
|
||||
/** 刷新表格 */
|
||||
function onRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 审核通过 */
|
||||
async function handleApprove(row: MallBrokerageWithdrawApi.BrokerageWithdraw) {
|
||||
try {
|
||||
await confirm('确定要审核通过吗?');
|
||||
await approveBrokerageWithdraw(row.id);
|
||||
ElMessage.success($t('ui.actionMessage.operationSuccess'));
|
||||
onRefresh();
|
||||
} catch (error) {
|
||||
console.error('审核失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/** 审核驳回 */
|
||||
function handleReject(row: MallBrokerageWithdrawApi.BrokerageWithdraw) {
|
||||
prompt({
|
||||
component: () => {
|
||||
return h(ElInput, {
|
||||
placeholder: '请输入驳回原因',
|
||||
allowClear: true,
|
||||
rules: [{ required: true, message: '请输入驳回原因' }],
|
||||
});
|
||||
},
|
||||
content: '请输入驳回原因',
|
||||
title: '驳回',
|
||||
modelPropName: 'value',
|
||||
}).then(async (val) => {
|
||||
if (val) {
|
||||
await rejectBrokerageWithdraw({
|
||||
id: row.id as number,
|
||||
auditReason: val,
|
||||
});
|
||||
onRefresh();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** 重新转账 */
|
||||
async function handleRetryTransfer(
|
||||
row: MallBrokerageWithdrawApi.BrokerageWithdraw,
|
||||
) {
|
||||
try {
|
||||
await confirm('确定要重新转账吗?');
|
||||
await approveBrokerageWithdraw(row.id);
|
||||
ElMessage.success($t('ui.actionMessage.operationSuccess'));
|
||||
onRefresh();
|
||||
} catch (error) {
|
||||
console.error('重新转账失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
cellConfig: {
|
||||
height: 80,
|
||||
},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getBrokerageWithdrawPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: { code: 'query' },
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<MallBrokerageWithdrawApi.BrokerageWithdraw>,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<Grid table-title="佣金提现列表">
|
||||
<template #withdraw-info="{ row }">
|
||||
<div v-if="row.type === BrokerageWithdrawTypeEnum.WALLET.type">-</div>
|
||||
<div v-else>
|
||||
<div v-if="row.userAccount">账号:{{ row.userAccount }}</div>
|
||||
<div v-if="row.userName">真实姓名:{{ row.userName }}</div>
|
||||
<template v-if="row.type === BrokerageWithdrawTypeEnum.BANK.type">
|
||||
<div v-if="row.bankName">银行名称:{{ row.bankName }}</div>
|
||||
<div v-if="row.bankAddress">开户地址:{{ row.bankAddress }}</div>
|
||||
</template>
|
||||
<div v-if="row.qrCodeUrl" class="mt-2">
|
||||
<div>收款码:</div>
|
||||
<img :src="row.qrCodeUrl" class="mt-1 h-10 w-10" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #status-info="{ row }">
|
||||
<div>
|
||||
<DictTag
|
||||
:value="row.status"
|
||||
:type="DICT_TYPE.BROKERAGE_WITHDRAW_STATUS"
|
||||
/>
|
||||
<div v-if="row.auditTime" class="mt-1 text-xs text-gray-500">
|
||||
时间:{{ formatDateTime(row.auditTime) }}
|
||||
</div>
|
||||
<div v-if="row.auditReason" class="mt-1 text-xs text-gray-500">
|
||||
审核原因:{{ row.auditReason }}
|
||||
</div>
|
||||
<div v-if="row.transferErrorMsg" class="mt-1 text-xs text-red-500">
|
||||
转账失败原因:{{ row.transferErrorMsg }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
// 审核中状态且没有支付转账编号,显示通过和驳回按钮
|
||||
{
|
||||
label: '通过',
|
||||
link: true,
|
||||
icon: ACTION_ICON.EDIT,
|
||||
auth: ['trade:brokerage-withdraw:audit'],
|
||||
ifShow:
|
||||
row.status === BrokerageWithdrawStatusEnum.AUDITING.status &&
|
||||
!row.payTransferId,
|
||||
onClick: () => handleApprove(row),
|
||||
},
|
||||
{
|
||||
label: '驳回',
|
||||
type: 'danger',
|
||||
link: true,
|
||||
icon: ACTION_ICON.DELETE,
|
||||
auth: ['trade:brokerage-withdraw:audit'],
|
||||
ifShow:
|
||||
row.status === BrokerageWithdrawStatusEnum.AUDITING.status &&
|
||||
!row.payTransferId,
|
||||
onClick: () => handleReject(row),
|
||||
},
|
||||
{
|
||||
label: '重新转账',
|
||||
link: true,
|
||||
icon: ACTION_ICON.REFRESH,
|
||||
auth: ['trade:brokerage-withdraw:audit'],
|
||||
ifShow:
|
||||
row.status === BrokerageWithdrawStatusEnum.WITHDRAW_FAIL.status,
|
||||
onClick: () => handleRetryTransfer(row),
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
243
apps/web-ele/src/views/mall/trade/config/data.ts
Normal file
243
apps/web-ele/src/views/mall/trade/config/data.ts
Normal file
@@ -0,0 +1,243 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
|
||||
import { DICT_TYPE, getDictOptions } from '#/utils';
|
||||
|
||||
/** 售后表单 */
|
||||
export function useFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'id',
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
show: () => false,
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'type',
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
show: () => false,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'afterSaleRefundReasons',
|
||||
label: '退款理由',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
mode: 'tags',
|
||||
placeholder: '请直接输入退款理由',
|
||||
class: 'w-full',
|
||||
},
|
||||
dependencies: {
|
||||
triggerFields: ['type'],
|
||||
show: (values) => values.type === 'afterSale',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'afterSaleReturnReasons',
|
||||
label: '退货理由',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
mode: 'tags',
|
||||
placeholder: '请直接输入退货理由',
|
||||
class: 'w-full',
|
||||
},
|
||||
dependencies: {
|
||||
triggerFields: ['type'],
|
||||
show: (values) => values.type === 'afterSale',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'deliveryExpressFreeEnabled',
|
||||
label: '启用包邮',
|
||||
component: 'Switch',
|
||||
dependencies: {
|
||||
triggerFields: ['type'],
|
||||
show: (values) => values.type === 'delivery',
|
||||
},
|
||||
description: '商城是否启用全场包邮',
|
||||
},
|
||||
{
|
||||
fieldName: 'deliveryExpressFreePrice',
|
||||
label: '满额包邮',
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
min: 0,
|
||||
precision: 2,
|
||||
class: 'w-full',
|
||||
},
|
||||
rules: 'required',
|
||||
dependencies: {
|
||||
triggerFields: ['type'],
|
||||
show: (values) => values.type === 'delivery',
|
||||
},
|
||||
description: '商城商品满多少金额即可包邮,单位:元',
|
||||
},
|
||||
{
|
||||
fieldName: 'deliveryPickUpEnabled',
|
||||
label: '启用门店自提',
|
||||
component: 'Switch',
|
||||
dependencies: {
|
||||
triggerFields: ['type'],
|
||||
show: (values) => values.type === 'delivery',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'brokerageEnabled',
|
||||
label: '启用分佣',
|
||||
component: 'Switch',
|
||||
description: '商城是否开启分销模式',
|
||||
dependencies: {
|
||||
triggerFields: ['type'],
|
||||
show: (values) => values.type === 'brokerage',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'brokerageEnabledCondition',
|
||||
label: '分佣模式',
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
options: getDictOptions(
|
||||
DICT_TYPE.BROKERAGE_ENABLED_CONDITION,
|
||||
'number',
|
||||
),
|
||||
buttonStyle: 'solid',
|
||||
optionType: 'button',
|
||||
class: 'w-full',
|
||||
},
|
||||
rules: 'required',
|
||||
dependencies: {
|
||||
triggerFields: ['type'],
|
||||
show: (values) => values.type === 'brokerage',
|
||||
},
|
||||
description:
|
||||
'人人分销:每个用户都可以成为推广员 \n 单级分销:每个用户只能有一个上级推广员',
|
||||
},
|
||||
{
|
||||
fieldName: 'brokerageBindMode',
|
||||
label: '分销关系绑定',
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.BROKERAGE_BIND_MODE, 'number'),
|
||||
buttonStyle: 'solid',
|
||||
optionType: 'button',
|
||||
class: 'w-full',
|
||||
},
|
||||
rules: 'required',
|
||||
dependencies: {
|
||||
triggerFields: ['type'],
|
||||
show: (values) => values.type === 'brokerage',
|
||||
},
|
||||
description:
|
||||
'首次绑定:只要用户没有推广人,随时都可以绑定推广关系 \n 注册绑定:只有新用户注册时或首次进入系统时才可以绑定推广关系',
|
||||
},
|
||||
{
|
||||
fieldName: 'brokeragePosterUrls',
|
||||
label: '分销海报图',
|
||||
component: 'ImageUpload',
|
||||
dependencies: {
|
||||
triggerFields: ['type'],
|
||||
show: (values) => values.type === 'brokerage',
|
||||
},
|
||||
description: '个人中心分销海报图片,建议尺寸 600x1000',
|
||||
},
|
||||
{
|
||||
fieldName: 'brokerageFirstPercent',
|
||||
label: '一级返佣比例',
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
min: 0,
|
||||
max: 100,
|
||||
class: 'w-full',
|
||||
},
|
||||
rules: 'required',
|
||||
dependencies: {
|
||||
triggerFields: ['type'],
|
||||
show: (values) => values.type === 'brokerage',
|
||||
},
|
||||
description: '订单交易成功后给推广人返佣的百分比',
|
||||
},
|
||||
{
|
||||
fieldName: 'brokerageSecondPercent',
|
||||
label: '二级返佣比例',
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
min: 0,
|
||||
max: 100,
|
||||
class: 'w-full',
|
||||
},
|
||||
rules: 'required',
|
||||
dependencies: {
|
||||
triggerFields: ['type'],
|
||||
show: (values) => values.type === 'brokerage',
|
||||
},
|
||||
description: '订单交易成功后给推广人的推荐人返佣的百分比',
|
||||
},
|
||||
{
|
||||
fieldName: 'brokerageFrozenDays',
|
||||
label: '佣金冻结天数',
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
min: 0,
|
||||
class: 'w-full',
|
||||
},
|
||||
rules: 'required',
|
||||
dependencies: {
|
||||
triggerFields: ['type'],
|
||||
show: (values) => values.type === 'brokerage',
|
||||
},
|
||||
description:
|
||||
'防止用户退款,佣金被提现了,所以需要设置佣金冻结时间,单位:天',
|
||||
},
|
||||
{
|
||||
fieldName: 'brokerageWithdrawMinPrice',
|
||||
label: '提现最低金额',
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
min: 0,
|
||||
precision: 2,
|
||||
class: 'w-full',
|
||||
},
|
||||
rules: 'required',
|
||||
dependencies: {
|
||||
triggerFields: ['type'],
|
||||
show: (values) => values.type === 'brokerage',
|
||||
},
|
||||
description: '用户提现最低金额限制,单位:元',
|
||||
},
|
||||
{
|
||||
fieldName: 'brokerageWithdrawFeePercent',
|
||||
label: '提现手续费',
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
min: 0,
|
||||
max: 100,
|
||||
class: 'w-full',
|
||||
},
|
||||
rules: 'required',
|
||||
dependencies: {
|
||||
triggerFields: ['type'],
|
||||
show: (values) => values.type === 'brokerage',
|
||||
},
|
||||
description:
|
||||
'提现手续费百分比,范围 0-100,0 为无提现手续费。例:设置 10,即收取 10% 手续费,提现10 元,到账 9 元,1 元手续费',
|
||||
},
|
||||
{
|
||||
fieldName: 'brokerageWithdrawTypes',
|
||||
label: '提现方式',
|
||||
component: 'CheckboxGroup',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.BROKERAGE_WITHDRAW_TYPE, 'number'),
|
||||
class: 'w-full',
|
||||
},
|
||||
rules: 'required',
|
||||
dependencies: {
|
||||
triggerFields: ['type'],
|
||||
show: (values) => values.type === 'brokerage',
|
||||
},
|
||||
description: '商城开通提现的付款方式',
|
||||
},
|
||||
];
|
||||
}
|
||||
98
apps/web-ele/src/views/mall/trade/config/index.vue
Normal file
98
apps/web-ele/src/views/mall/trade/config/index.vue
Normal file
@@ -0,0 +1,98 @@
|
||||
<script lang="ts" setup>
|
||||
import type { MallTradeConfigApi } from '#/api/mall/trade/config';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { DocAlert, Page } from '@vben/common-ui';
|
||||
|
||||
import { ElCard, ElMessage, ElTabPane, ElTabs } from 'element-plus';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { getTradeConfig, saveTradeConfig } from '#/api/mall/trade/config';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useFormSchema } from './data';
|
||||
|
||||
const activeKey = ref('afterSale');
|
||||
const formData = ref<MallTradeConfigApi.Config & { type?: string }>();
|
||||
|
||||
function handleTabChange(key: any) {
|
||||
activeKey.value = key;
|
||||
formData.value!.type = activeKey.value;
|
||||
formApi.setValues(formData.value!);
|
||||
formApi.updateSchema(useFormSchema());
|
||||
}
|
||||
|
||||
async function loadConfig() {
|
||||
const res = await getTradeConfig();
|
||||
if (res) {
|
||||
formData.value = res;
|
||||
// 金额缩小
|
||||
formData.value.deliveryExpressFreePrice =
|
||||
formData.value.deliveryExpressFreePrice! / 100 || 0;
|
||||
formData.value.brokerageWithdrawMinPrice =
|
||||
formData.value.brokerageWithdrawMinPrice! / 100 || 0;
|
||||
formData.value!.type = activeKey.value;
|
||||
formApi.updateSchema(useFormSchema());
|
||||
await formApi.setValues(formData.value);
|
||||
}
|
||||
}
|
||||
|
||||
async function onSubmit() {
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
// 提交表单
|
||||
const data = (await formApi.getValues()) as MallTradeConfigApi.Config;
|
||||
formApi.setState({ commonConfig: { disabled: true } });
|
||||
try {
|
||||
await saveTradeConfig(data);
|
||||
ElMessage.success($t('ui.actionMessage.operationSuccess'));
|
||||
} finally {
|
||||
formApi.setState({ commonConfig: { disabled: false } });
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadConfig();
|
||||
});
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
// 所有表单项
|
||||
labelClass: 'w-2/6',
|
||||
},
|
||||
wrapperClass: 'grid-cols-1',
|
||||
actionWrapperClass: 'text-center',
|
||||
handleSubmit: onSubmit,
|
||||
layout: 'horizontal',
|
||||
resetButtonOptions: {
|
||||
show: false,
|
||||
},
|
||||
schema: useFormSchema(),
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page>
|
||||
<template #doc>
|
||||
<DocAlert
|
||||
title="【交易】交易订单"
|
||||
url="https://doc.iocoder.cn/mall/trade-order/"
|
||||
/>
|
||||
<DocAlert
|
||||
title="【交易】购物车"
|
||||
url="https://doc.iocoder.cn/mall/trade-cart/"
|
||||
/>
|
||||
</template>
|
||||
<ElCard>
|
||||
<ElTabs :active-key="activeKey" @change="handleTabChange">
|
||||
<ElTabPane tab="售后" key="afterSale" :force-render="true" />
|
||||
<ElTabPane tab="配送" key="delivery" :force-render="true" />
|
||||
<ElTabPane tab="分销" key="brokerage" :force-render="true" />
|
||||
</ElTabs>
|
||||
<Form class="w-3/5" />
|
||||
</ElCard>
|
||||
</Page>
|
||||
</template>
|
||||
130
apps/web-ele/src/views/mall/trade/delivery/express/data.ts
Normal file
130
apps/web-ele/src/views/mall/trade/delivery/express/data.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
import { z } from '#/adapter/form';
|
||||
import { CommonStatusEnum, DICT_TYPE, getDictOptions } from '#/utils';
|
||||
|
||||
/** 新增/修改的表单 */
|
||||
export function useFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'id',
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
show: () => false,
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'code',
|
||||
label: '公司编码',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'name',
|
||||
label: '公司名称',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'ImageUpload',
|
||||
fieldName: 'logo',
|
||||
label: '公司 logo',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'sort',
|
||||
label: '显示顺序',
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
min: 0,
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'status',
|
||||
label: '开启状态',
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
|
||||
buttonStyle: 'solid',
|
||||
optionType: 'button',
|
||||
},
|
||||
rules: z.number().default(CommonStatusEnum.ENABLE),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '快递公司名称',
|
||||
component: 'Input',
|
||||
},
|
||||
{
|
||||
fieldName: 'code',
|
||||
label: '快递公司编号',
|
||||
component: 'Input',
|
||||
},
|
||||
{
|
||||
fieldName: 'status',
|
||||
label: '状态',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'id',
|
||||
title: '编号',
|
||||
},
|
||||
{
|
||||
field: 'code',
|
||||
title: '公司编码',
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
title: '公司名称',
|
||||
},
|
||||
{
|
||||
field: 'logo',
|
||||
title: '公司 logo',
|
||||
cellRender: {
|
||||
name: 'CellImage',
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'sort',
|
||||
title: '显示顺序',
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
title: '状态',
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.COMMON_STATUS },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '创建时间',
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 130,
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
141
apps/web-ele/src/views/mall/trade/delivery/express/index.vue
Normal file
141
apps/web-ele/src/views/mall/trade/delivery/express/index.vue
Normal file
@@ -0,0 +1,141 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { MallDeliveryExpressApi } from '#/api/mall/trade/delivery/express';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
import { downloadFileFromBlobPart } from '@vben/utils';
|
||||
|
||||
import { ElLoading, ElMessage } from 'element-plus';
|
||||
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
deleteDeliveryExpress,
|
||||
exportDeliveryExpress,
|
||||
getDeliveryExpressPage,
|
||||
} from '#/api/mall/trade/delivery/express';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useGridColumns, useGridFormSchema } from './data';
|
||||
import Form from './modules/form.vue';
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: Form,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 刷新表格 */
|
||||
function onRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 导出表格 */
|
||||
async function handleExport() {
|
||||
const data = await exportDeliveryExpress(await gridApi.formApi.getValues());
|
||||
downloadFileFromBlobPart({ fileName: '快递公司.xls', source: data });
|
||||
}
|
||||
|
||||
/** 创建快递公司 */
|
||||
function handleCreate() {
|
||||
formModalApi.setData(null).open();
|
||||
}
|
||||
|
||||
/** 编辑快递公司 */
|
||||
function handleEdit(row: MallDeliveryExpressApi.DeliveryExpress) {
|
||||
formModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 删除快递公司 */
|
||||
async function handleDelete(row: MallDeliveryExpressApi.DeliveryExpress) {
|
||||
const hideLoading = ElLoading.service({
|
||||
text: $t('ui.actionMessage.deleting', [row.name]),
|
||||
});
|
||||
try {
|
||||
await deleteDeliveryExpress(row.id as number);
|
||||
ElMessage.success($t('ui.actionMessage.deleteSuccess', [row.name]));
|
||||
onRefresh();
|
||||
} finally {
|
||||
hideLoading.close();
|
||||
}
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getDeliveryExpressPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: { code: 'query' },
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<MallDeliveryExpressApi.DeliveryExpress>,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<FormModal @success="onRefresh" />
|
||||
<Grid table-title="快递公司列表">
|
||||
<template #toolbar-tools>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('ui.actionTitle.create', ['快递公司']),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.ADD,
|
||||
auth: ['trade:delivery:express:create'],
|
||||
onClick: handleCreate,
|
||||
},
|
||||
{
|
||||
label: $t('ui.actionTitle.export'),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.DOWNLOAD,
|
||||
auth: ['trade:delivery:express:export'],
|
||||
onClick: handleExport,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('common.edit'),
|
||||
link: true,
|
||||
icon: ACTION_ICON.EDIT,
|
||||
auth: ['trade:delivery:express:update'],
|
||||
onClick: handleEdit.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: $t('common.delete'),
|
||||
link: true,
|
||||
type: 'danger',
|
||||
icon: ACTION_ICON.DELETE,
|
||||
auth: ['trade:delivery:express:delete'],
|
||||
popConfirm: {
|
||||
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
|
||||
confirm: handleDelete.bind(null, row),
|
||||
},
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
@@ -0,0 +1,89 @@
|
||||
<script lang="ts" setup>
|
||||
import type { MallDeliveryExpressApi } from '#/api/mall/trade/delivery/express';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import {
|
||||
createDeliveryExpress,
|
||||
getDeliveryExpress,
|
||||
updateDeliveryExpress,
|
||||
} from '#/api/mall/trade/delivery/express';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useFormSchema } from '../data';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const formData = ref<MallDeliveryExpressApi.DeliveryExpress>();
|
||||
const getTitle = computed(() => {
|
||||
return formData.value?.id
|
||||
? $t('ui.actionTitle.edit', ['快递公司'])
|
||||
: $t('ui.actionTitle.create', ['快递公司']);
|
||||
});
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
formItemClass: 'col-span-2',
|
||||
labelWidth: 120,
|
||||
},
|
||||
layout: 'horizontal',
|
||||
schema: useFormSchema(),
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
// 提交表单
|
||||
const data =
|
||||
(await formApi.getValues()) as MallDeliveryExpressApi.DeliveryExpress;
|
||||
try {
|
||||
await (formData.value?.id
|
||||
? updateDeliveryExpress(data)
|
||||
: createDeliveryExpress(data));
|
||||
// 关闭并提示
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
ElMessage.success($t('ui.actionMessage.operationSuccess'));
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
formData.value = undefined;
|
||||
return;
|
||||
}
|
||||
// 加载数据
|
||||
const data = modalApi.getData<MallDeliveryExpressApi.DeliveryExpress>();
|
||||
if (!data || !data.id) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
try {
|
||||
formData.value = await getDeliveryExpress(data.id as number);
|
||||
// 设置到 values
|
||||
await formApi.setValues(formData.value);
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal class="w-2/5" :title="getTitle">
|
||||
<Form class="mx-4" />
|
||||
</Modal>
|
||||
</template>
|
||||
@@ -0,0 +1,102 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
import { z } from '#/adapter/form';
|
||||
import { CommonStatusEnum, DICT_TYPE, getDictOptions } from '#/utils';
|
||||
|
||||
/** 新增/修改的表单 */
|
||||
export function useFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'id',
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
show: () => false,
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'name',
|
||||
label: '模板名称',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'chargeMode',
|
||||
label: '计费方式',
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.EXPRESS_CHARGE_MODE, 'number'),
|
||||
buttonStyle: 'solid',
|
||||
optionType: 'button',
|
||||
},
|
||||
rules: z.number().default(CommonStatusEnum.ENABLE),
|
||||
},
|
||||
{
|
||||
fieldName: 'sort',
|
||||
label: '显示顺序',
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
min: 0,
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '模板名称',
|
||||
component: 'Input',
|
||||
},
|
||||
{
|
||||
fieldName: 'chargeMode',
|
||||
label: '计费方式',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
options: getDictOptions(DICT_TYPE.EXPRESS_CHARGE_MODE, 'number'),
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'id',
|
||||
title: '编号',
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
title: '模板名称',
|
||||
},
|
||||
{
|
||||
field: 'chargeMode',
|
||||
title: '计费方式',
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.EXPRESS_CHARGE_MODE },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'sort',
|
||||
title: '显示顺序',
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '创建时间',
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 130,
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { MallDeliveryExpressTemplateApi } from '#/api/mall/trade/delivery/expressTemplate';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { ElLoading, ElMessage } from 'element-plus';
|
||||
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
deleteDeliveryExpressTemplate,
|
||||
getDeliveryExpressTemplatePage,
|
||||
} from '#/api/mall/trade/delivery/expressTemplate';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useGridColumns, useGridFormSchema } from './data';
|
||||
import Form from './modules/form.vue';
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: Form,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 刷新表格 */
|
||||
function onRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 创建快递模板 */
|
||||
function handleCreate() {
|
||||
formModalApi.setData(null).open();
|
||||
}
|
||||
|
||||
/** 编辑快递模板 */
|
||||
function handleEdit(row: MallDeliveryExpressTemplateApi.ExpressTemplate) {
|
||||
formModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 删除快递模板 */
|
||||
async function handleDelete(
|
||||
row: MallDeliveryExpressTemplateApi.ExpressTemplate,
|
||||
) {
|
||||
const hideLoading = ElLoading.service({
|
||||
text: $t('ui.actionMessage.deleting', [row.name]),
|
||||
});
|
||||
try {
|
||||
await deleteDeliveryExpressTemplate(row.id as number);
|
||||
ElMessage.success($t('ui.actionMessage.deleteSuccess', [row.name]));
|
||||
onRefresh();
|
||||
} finally {
|
||||
hideLoading.close();
|
||||
}
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getDeliveryExpressTemplatePage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: { code: 'query' },
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<MallDeliveryExpressTemplateApi.ExpressTemplate>,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<FormModal @success="onRefresh" />
|
||||
<Grid table-title="快递模板列表">
|
||||
<template #toolbar-tools>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('ui.actionTitle.create', ['快递模板']),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.ADD,
|
||||
auth: ['trade:delivery:express-template:create'],
|
||||
onClick: handleCreate,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('common.edit'),
|
||||
link: true,
|
||||
icon: ACTION_ICON.EDIT,
|
||||
auth: ['trade:delivery:express-template:update'],
|
||||
onClick: handleEdit.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: $t('common.delete'),
|
||||
link: true,
|
||||
type: 'danger',
|
||||
icon: ACTION_ICON.DELETE,
|
||||
auth: ['trade:delivery:express-template:delete'],
|
||||
popConfirm: {
|
||||
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
|
||||
confirm: handleDelete.bind(null, row),
|
||||
},
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
@@ -0,0 +1,91 @@
|
||||
<script lang="ts" setup>
|
||||
import type { MallDeliveryExpressTemplateApi } from '#/api/mall/trade/delivery/expressTemplate';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import {
|
||||
createDeliveryExpressTemplate,
|
||||
getDeliveryExpressTemplate,
|
||||
updateDeliveryExpressTemplate,
|
||||
} from '#/api/mall/trade/delivery/expressTemplate';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useFormSchema } from '../data';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const formData = ref<MallDeliveryExpressTemplateApi.ExpressTemplate>();
|
||||
const getTitle = computed(() => {
|
||||
return formData.value?.id
|
||||
? $t('ui.actionTitle.edit', ['快递模板'])
|
||||
: $t('ui.actionTitle.create', ['快递模板']);
|
||||
});
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
formItemClass: 'col-span-2',
|
||||
labelWidth: 120,
|
||||
},
|
||||
layout: 'horizontal',
|
||||
schema: useFormSchema(),
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
// TODO @xingyu:城市处理;
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
// 提交表单
|
||||
const data =
|
||||
(await formApi.getValues()) as MallDeliveryExpressTemplateApi.ExpressTemplate;
|
||||
try {
|
||||
await (formData.value?.id
|
||||
? updateDeliveryExpressTemplate(data)
|
||||
: createDeliveryExpressTemplate(data));
|
||||
// 关闭并提示
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
ElMessage.success($t('ui.actionMessage.operationSuccess'));
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
formData.value = undefined;
|
||||
return;
|
||||
}
|
||||
// 加载数据
|
||||
const data =
|
||||
modalApi.getData<MallDeliveryExpressTemplateApi.ExpressTemplate>();
|
||||
if (!data || !data.id) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
try {
|
||||
formData.value = await getDeliveryExpressTemplate(data.id as number);
|
||||
// 设置到 values
|
||||
await formApi.setValues(formData.value);
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal class="w-2/5" :title="getTitle">
|
||||
<Form class="mx-4" />
|
||||
</Modal>
|
||||
</template>
|
||||
127
apps/web-ele/src/views/mall/trade/delivery/pickUpOrder/data.ts
Normal file
127
apps/web-ele/src/views/mall/trade/delivery/pickUpOrder/data.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeGridPropTypes } from '#/adapter/vxe-table';
|
||||
import type { MallDeliveryPickUpStoreApi } from '#/api/mall/trade/delivery/pickUpStore';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { getSimpleDeliveryPickUpStoreList } from '#/api/mall/trade/delivery/pickUpStore';
|
||||
import {
|
||||
DeliveryTypeEnum,
|
||||
DICT_TYPE,
|
||||
getRangePickerDefaultProps,
|
||||
} from '#/utils';
|
||||
|
||||
const pickUpStoreList = ref<MallDeliveryPickUpStoreApi.PickUpStore[]>([]);
|
||||
|
||||
getSimpleDeliveryPickUpStoreList().then((res) => {
|
||||
pickUpStoreList.value = res;
|
||||
});
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'createTime',
|
||||
label: '创建时间',
|
||||
component: 'RangePicker',
|
||||
componentProps: {
|
||||
...getRangePickerDefaultProps(),
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'pickUpStoreId',
|
||||
label: '自提门店',
|
||||
component: 'ApiSelect',
|
||||
componentProps: {
|
||||
api: getSimpleDeliveryPickUpStoreList,
|
||||
fieldNames: {
|
||||
label: 'name',
|
||||
value: 'id',
|
||||
},
|
||||
},
|
||||
dependencies: {
|
||||
triggerFields: ['deliveryType'],
|
||||
trigger: (values) =>
|
||||
values.deliveryType === DeliveryTypeEnum.PICK_UP.type,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 表格列配置 */
|
||||
export function useGridColumns(): VxeGridPropTypes.Columns {
|
||||
return [
|
||||
{
|
||||
field: 'no',
|
||||
title: '订单号',
|
||||
fixed: 'left',
|
||||
minWidth: 180,
|
||||
},
|
||||
{
|
||||
field: 'user.nickname',
|
||||
title: '用户信息',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
field: 'brokerageUser.nickname',
|
||||
title: '推荐人信息',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
field: 'spuName',
|
||||
title: '商品信息',
|
||||
minWidth: 100,
|
||||
formatter: ({ row }) => {
|
||||
if (row.items.length > 1) {
|
||||
return row.items.map((item: any) => item.spuName).join(',');
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'payPrice',
|
||||
title: '实付金额(元)',
|
||||
formatter: 'formatAmount2',
|
||||
minWidth: 180,
|
||||
},
|
||||
{
|
||||
field: 'storeStaffName',
|
||||
title: '核销员',
|
||||
minWidth: 160,
|
||||
},
|
||||
{
|
||||
field: 'pickUpStoreId',
|
||||
title: '核销门店',
|
||||
minWidth: 160,
|
||||
formatter: ({ row }) => {
|
||||
return pickUpStoreList.value.find(
|
||||
(item) => item.id === row.pickUpStoreId,
|
||||
)?.name;
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'payStatus',
|
||||
title: '支付状态',
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING },
|
||||
},
|
||||
minWidth: 80,
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
title: '订单状态',
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.TRADE_ORDER_STATUS },
|
||||
},
|
||||
minWidth: 80,
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '下单时间',
|
||||
formatter: 'formatDateTime',
|
||||
minWidth: 160,
|
||||
},
|
||||
];
|
||||
}
|
||||
241
apps/web-ele/src/views/mall/trade/delivery/pickUpOrder/index.vue
Normal file
241
apps/web-ele/src/views/mall/trade/delivery/pickUpOrder/index.vue
Normal file
@@ -0,0 +1,241 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { MallOrderApi } from '#/api/mall/trade/order';
|
||||
|
||||
import { h, onMounted, ref } from 'vue';
|
||||
|
||||
import { Page, prompt } from '@vben/common-ui';
|
||||
import { fenToYuan } from '@vben/utils';
|
||||
|
||||
import { ElCard, ElInput, ElMessage } from 'element-plus';
|
||||
|
||||
import { TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
getOrderByPickUpVerifyCode,
|
||||
getOrderPage,
|
||||
getOrderSummary,
|
||||
} from '#/api/mall/trade/order';
|
||||
import { SummaryCard } from '#/components/summary-card';
|
||||
import { DeliveryTypeEnum, TradeOrderStatusEnum } from '#/utils';
|
||||
|
||||
import { useGridColumns, useGridFormSchema } from './data';
|
||||
|
||||
const summary = ref<MallOrderApi.OrderSummary>();
|
||||
|
||||
async function getOrderSum() {
|
||||
const query = await gridApi.formApi.getValues();
|
||||
query.deliveryType = DeliveryTypeEnum.PICK_UP.type;
|
||||
const res = await getOrderSummary(query as any);
|
||||
summary.value = res;
|
||||
}
|
||||
|
||||
/** 核销 */
|
||||
async function handlePickup(pickUpVerifyCode?: string) {
|
||||
if (!pickUpVerifyCode) {
|
||||
await prompt({
|
||||
component: () => {
|
||||
return h(ElInput, {});
|
||||
},
|
||||
content: '请输入核销码',
|
||||
title: '核销订单',
|
||||
modelPropName: 'value',
|
||||
}).then(async (val) => {
|
||||
if (val) {
|
||||
pickUpVerifyCode = val;
|
||||
}
|
||||
});
|
||||
}
|
||||
if (!pickUpVerifyCode) {
|
||||
return;
|
||||
}
|
||||
const data = await getOrderByPickUpVerifyCode(pickUpVerifyCode);
|
||||
if (data?.deliveryType !== DeliveryTypeEnum.PICK_UP.type) {
|
||||
ElMessage.error('未查询到订单');
|
||||
return;
|
||||
}
|
||||
if (data?.status !== TradeOrderStatusEnum.UNDELIVERED.status) {
|
||||
ElMessage.error('订单不是待核销状态');
|
||||
}
|
||||
}
|
||||
|
||||
const port = ref('');
|
||||
const ports = ref([]);
|
||||
const reader = ref('');
|
||||
const serialPort = ref(false); // 是否连接扫码枪
|
||||
|
||||
/** 连接扫码枪 */
|
||||
async function connectToSerialPort() {
|
||||
try {
|
||||
// 判断浏览器支持串口通信
|
||||
if (
|
||||
'serial' in navigator &&
|
||||
navigator.serial !== null &&
|
||||
typeof navigator.serial === 'object' &&
|
||||
'requestPort' in navigator.serial
|
||||
) {
|
||||
// 提示用户选择一个串口
|
||||
port.value = await (navigator.serial as any).requestPort();
|
||||
} else {
|
||||
ElMessage.error('浏览器不支持扫码枪连接,请更换浏览器重试');
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取用户之前授予该网站访问权限的所有串口。
|
||||
ports.value = await (navigator.serial as any).getPorts();
|
||||
|
||||
// 等待串口打开
|
||||
await (port.value as any).open({
|
||||
baudRate: 9600,
|
||||
dataBits: 8,
|
||||
stopBits: 2,
|
||||
});
|
||||
|
||||
ElMessage.success('成功连接扫码枪');
|
||||
serialPort.value = true;
|
||||
readData();
|
||||
} catch (error) {
|
||||
// 处理连接串口出错的情况
|
||||
console.error('Error connecting to serial port:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/** 监听扫码枪输入 */
|
||||
async function readData() {
|
||||
reader.value = (port.value as any).readable.getReader();
|
||||
let data = ''; // 扫码数据
|
||||
// 监听来自串口的数据
|
||||
while (true) {
|
||||
const { value, done } = await (reader.value as any).read();
|
||||
if (done) {
|
||||
// 允许稍后关闭串口
|
||||
(reader.value as any).releaseLock();
|
||||
break;
|
||||
}
|
||||
// 获取发送的数据
|
||||
const serialData = new TextDecoder().decode(value);
|
||||
data = `${data}${serialData}`;
|
||||
if (serialData.includes('\r')) {
|
||||
// 读取结束
|
||||
const codeData = data.replace('\r', '');
|
||||
data = ''; // 清空下次读取不会叠加
|
||||
console.warn(`二维码数据:${codeData}`);
|
||||
// 处理拿到数据逻辑
|
||||
handlePickup(codeData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function cutPort() {
|
||||
if (port.value === '') {
|
||||
ElMessage.warning('请先连接或打开扫码枪');
|
||||
} else {
|
||||
await (reader.value as any).cancel();
|
||||
await (port.value as any).close();
|
||||
port.value = '';
|
||||
console.warn('断开扫码枪连接');
|
||||
ElMessage.success('已成功断开扫码枪连接');
|
||||
serialPort.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getOrderPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
deliveryType: DeliveryTypeEnum.PICK_UP.type,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: { code: 'query' },
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<MallOrderApi.Order>,
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
getOrderSum();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<ElCard class="mb-4 h-[10%]">
|
||||
<div class="flex flex-row gap-4">
|
||||
<SummaryCard
|
||||
class="flex flex-1"
|
||||
title="订单数量"
|
||||
icon="icon-park-outline:transaction-order"
|
||||
icon-color="bg-blue-100"
|
||||
icon-bg-color="text-blue-500"
|
||||
:value="summary?.orderCount || 0"
|
||||
/>
|
||||
<SummaryCard
|
||||
class="flex flex-1"
|
||||
title="订单金额"
|
||||
icon="streamline:money-cash-file-dollar-common-money-currency-cash-file"
|
||||
icon-color="bg-purple-100"
|
||||
icon-bg-color="text-purple-500"
|
||||
prefix="¥"
|
||||
:decimals="2"
|
||||
:value="Number(fenToYuan(summary?.orderPayPrice || 0))"
|
||||
/>
|
||||
<SummaryCard
|
||||
class="flex flex-1"
|
||||
title="退款单数"
|
||||
icon="heroicons:receipt-refund"
|
||||
icon-color="bg-yellow-100"
|
||||
icon-bg-color="text-yellow-500"
|
||||
:value="summary?.afterSaleCount || 0"
|
||||
/>
|
||||
<SummaryCard
|
||||
class="flex flex-1"
|
||||
title="退款金额"
|
||||
icon="ri:refund-2-line"
|
||||
icon-color="bg-green-100"
|
||||
icon-bg-color="text-green-500"
|
||||
prefix="¥"
|
||||
:decimals="2"
|
||||
:value="Number(fenToYuan(summary?.afterSalePrice || 0))"
|
||||
/>
|
||||
</div>
|
||||
</ElCard>
|
||||
<Grid class="h-4/5" table-title="核销订单">
|
||||
<template #toolbar-tools>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '核销',
|
||||
type: 'primary',
|
||||
icon: 'lucide:circle-check-big',
|
||||
auth: ['trade:order:pick-up'],
|
||||
onClick: handlePickup.bind(null, undefined),
|
||||
},
|
||||
{
|
||||
label: serialPort ? '断开扫描枪' : '连接扫描枪',
|
||||
type: 'primary',
|
||||
icon: serialPort ? 'lucide:circle-x' : 'lucide:circle-play',
|
||||
link: true,
|
||||
onClick: serialPort ? cutPort : connectToSerialPort,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
245
apps/web-ele/src/views/mall/trade/delivery/pickUpStore/data.ts
Normal file
245
apps/web-ele/src/views/mall/trade/delivery/pickUpStore/data.ts
Normal file
@@ -0,0 +1,245 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
import { z } from '#/adapter/form';
|
||||
import { getAreaTree } from '#/api/system/area';
|
||||
import { getSimpleUserList } from '#/api/system/user';
|
||||
import {
|
||||
CommonStatusEnum,
|
||||
DICT_TYPE,
|
||||
getDictOptions,
|
||||
getRangePickerDefaultProps,
|
||||
} from '#/utils';
|
||||
|
||||
/** 新增/修改的表单 */
|
||||
export function useFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'id',
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
show: () => false,
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'ImageUpload',
|
||||
fieldName: 'logo',
|
||||
label: '门店logo',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'name',
|
||||
label: '门店名称',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'phone',
|
||||
label: '门店手机',
|
||||
rules: 'mobileRequired',
|
||||
},
|
||||
{
|
||||
component: 'Textarea',
|
||||
fieldName: 'introduction',
|
||||
label: '门店简介',
|
||||
},
|
||||
{
|
||||
fieldName: 'areaId',
|
||||
label: '地址',
|
||||
component: 'ApiTreeSelect',
|
||||
componentProps: {
|
||||
api: () => getAreaTree(),
|
||||
fieldNames: { label: 'name', value: 'id', children: 'children' },
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'detailAddress',
|
||||
label: '详细地址',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'TimePicker',
|
||||
fieldName: 'openingTime',
|
||||
label: '营业开始时间',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'TimePicker',
|
||||
fieldName: 'closingTime',
|
||||
label: '营业结束时间',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'longitude',
|
||||
label: '经度',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'latitude',
|
||||
label: '纬度',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'getGeo',
|
||||
label: '获取经纬度',
|
||||
},
|
||||
{
|
||||
fieldName: 'status',
|
||||
label: '门店状态',
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
|
||||
buttonStyle: 'solid',
|
||||
optionType: 'button',
|
||||
},
|
||||
rules: z.number().default(CommonStatusEnum.ENABLE),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 绑定店员的表单 */
|
||||
export function useBindFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'id',
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
show: () => false,
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'name',
|
||||
label: '门店名称',
|
||||
dependencies: {
|
||||
triggerFields: ['id'],
|
||||
disabled: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'ApiSelect',
|
||||
fieldName: 'verifyUserIds',
|
||||
label: '门店店员',
|
||||
rules: 'required',
|
||||
componentProps: {
|
||||
api: () => getSimpleUserList(),
|
||||
fieldNames: { label: 'nickname', value: 'id' },
|
||||
mode: 'tags',
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
fieldName: 'verifyUsers',
|
||||
label: '店员列表',
|
||||
rules: 'required',
|
||||
componentProps: {
|
||||
options: [],
|
||||
mode: 'tags',
|
||||
},
|
||||
dependencies: {
|
||||
triggerFields: ['verifyUserIds'],
|
||||
trigger(values, form) {
|
||||
form.setFieldValue('verifyUsers', values.verifyUserIds);
|
||||
},
|
||||
disabled: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'phone',
|
||||
label: '门店手机',
|
||||
component: 'Input',
|
||||
},
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '门店名称',
|
||||
component: 'Input',
|
||||
},
|
||||
{
|
||||
fieldName: 'status',
|
||||
label: '门店状态',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'createTime',
|
||||
label: '创建时间',
|
||||
component: 'RangePicker',
|
||||
componentProps: {
|
||||
...getRangePickerDefaultProps(),
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'id',
|
||||
title: '编号',
|
||||
},
|
||||
{
|
||||
field: 'logo',
|
||||
title: '门店logo',
|
||||
cellRender: {
|
||||
name: 'CellImage',
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
title: '门店名称',
|
||||
},
|
||||
{
|
||||
field: 'phone',
|
||||
title: '门店手机',
|
||||
},
|
||||
{
|
||||
field: 'detailAddress',
|
||||
title: '地址',
|
||||
},
|
||||
{
|
||||
field: 'openingTime',
|
||||
title: '营业时间',
|
||||
formatter: ({ row }) => {
|
||||
return `${row.openingTime} ~ ${row.closingTime}`;
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
title: '开启状态',
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.COMMON_STATUS },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '创建时间',
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 200,
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
145
apps/web-ele/src/views/mall/trade/delivery/pickUpStore/index.vue
Normal file
145
apps/web-ele/src/views/mall/trade/delivery/pickUpStore/index.vue
Normal file
@@ -0,0 +1,145 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { MallDeliveryPickUpStoreApi } from '#/api/mall/trade/delivery/pickUpStore';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { ElLoading, ElMessage } from 'element-plus';
|
||||
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
deleteDeliveryPickUpStore,
|
||||
getDeliveryPickUpStorePage,
|
||||
} from '#/api/mall/trade/delivery/pickUpStore';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useGridColumns, useGridFormSchema } from './data';
|
||||
import BindForm from './modules/bind-form.vue';
|
||||
import Form from './modules/form.vue';
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: Form,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
const [BindFormModal, bindFormModalApi] = useVbenModal({
|
||||
connectedComponent: BindForm,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 刷新表格 */
|
||||
function onRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 创建门店 */
|
||||
function handleCreate() {
|
||||
formModalApi.setData(null).open();
|
||||
}
|
||||
|
||||
/** 编辑门店 */
|
||||
function handleEdit(row: MallDeliveryPickUpStoreApi.PickUpStore) {
|
||||
formModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 绑定店员 */
|
||||
function handleBind(row: MallDeliveryPickUpStoreApi.PickUpStore) {
|
||||
bindFormModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 删除门店 */
|
||||
async function handleDelete(row: MallDeliveryPickUpStoreApi.PickUpStore) {
|
||||
const hideLoading = ElLoading.service({
|
||||
text: $t('ui.actionMessage.deleting', [row.name]),
|
||||
});
|
||||
try {
|
||||
await deleteDeliveryPickUpStore(row.id as number);
|
||||
ElMessage.success($t('ui.actionMessage.deleteSuccess', [row.name]));
|
||||
onRefresh();
|
||||
} finally {
|
||||
hideLoading.close();
|
||||
}
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getDeliveryPickUpStorePage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: { code: 'query' },
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<MallDeliveryPickUpStoreApi.PickUpStore>,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<FormModal @success="onRefresh" />
|
||||
<BindFormModal />
|
||||
<Grid table-title="门店列表">
|
||||
<template #toolbar-tools>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('ui.actionTitle.create', ['门店']),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.ADD,
|
||||
auth: ['trade:delivery:pick-up-store:create'],
|
||||
onClick: handleCreate,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('common.edit'),
|
||||
link: true,
|
||||
icon: ACTION_ICON.EDIT,
|
||||
auth: ['trade:delivery:pick-up-store:update'],
|
||||
onClick: handleEdit.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: '绑定店员',
|
||||
link: true,
|
||||
icon: ACTION_ICON.ADD,
|
||||
auth: ['trade:delivery:pick-up-store:update'],
|
||||
onClick: handleBind.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: $t('common.delete'),
|
||||
link: true,
|
||||
type: 'danger',
|
||||
icon: ACTION_ICON.DELETE,
|
||||
auth: ['trade:delivery:pick-up-store:delete'],
|
||||
popConfirm: {
|
||||
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
|
||||
confirm: handleDelete.bind(null, row),
|
||||
},
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
@@ -0,0 +1,87 @@
|
||||
<script lang="ts" setup>
|
||||
import type { MallDeliveryPickUpStoreApi } from '#/api/mall/trade/delivery/pickUpStore';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import {
|
||||
bindStoreStaffId,
|
||||
getDeliveryPickUpStore,
|
||||
} from '#/api/mall/trade/delivery/pickUpStore';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useBindFormSchema } from '../data';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const formData = ref<MallDeliveryPickUpStoreApi.PickUpStore>();
|
||||
const getTitle = computed(() => {
|
||||
return formData.value?.id
|
||||
? $t('ui.actionTitle.edit', ['绑定店员'])
|
||||
: $t('ui.actionTitle.create', ['绑定店员']);
|
||||
});
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
formItemClass: 'col-span-2',
|
||||
labelWidth: 120,
|
||||
},
|
||||
layout: 'horizontal',
|
||||
schema: useBindFormSchema(),
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
// 提交表单
|
||||
const data =
|
||||
(await formApi.getValues()) as MallDeliveryPickUpStoreApi.BindStaffRequest;
|
||||
try {
|
||||
await bindStoreStaffId(data);
|
||||
// 关闭并提示
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
ElMessage.success($t('ui.actionMessage.operationSuccess'));
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
formData.value = undefined;
|
||||
return;
|
||||
}
|
||||
// 加载数据
|
||||
const data =
|
||||
modalApi.getData<MallDeliveryPickUpStoreApi.BindStaffRequest>();
|
||||
if (!data || !data.id) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
try {
|
||||
formData.value = await getDeliveryPickUpStore(data.id as number);
|
||||
// 设置到 values
|
||||
await formApi.setValues(formData.value);
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal class="w-2/5" :title="getTitle">
|
||||
<Form class="mx-4" />
|
||||
</Modal>
|
||||
</template>
|
||||
@@ -0,0 +1,89 @@
|
||||
<script lang="ts" setup>
|
||||
import type { MallDeliveryPickUpStoreApi } from '#/api/mall/trade/delivery/pickUpStore';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import {
|
||||
createDeliveryPickUpStore,
|
||||
getDeliveryPickUpStore,
|
||||
updateDeliveryPickUpStore,
|
||||
} from '#/api/mall/trade/delivery/pickUpStore';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useFormSchema } from '../data';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const formData = ref<MallDeliveryPickUpStoreApi.PickUpStore>();
|
||||
const getTitle = computed(() => {
|
||||
return formData.value?.id
|
||||
? $t('ui.actionTitle.edit', ['自提订单'])
|
||||
: $t('ui.actionTitle.create', ['自提订单']);
|
||||
});
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
formItemClass: 'col-span-2',
|
||||
labelWidth: 120,
|
||||
},
|
||||
layout: 'horizontal',
|
||||
schema: useFormSchema(),
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
// 提交表单
|
||||
const data =
|
||||
(await formApi.getValues()) as MallDeliveryPickUpStoreApi.PickUpStore;
|
||||
try {
|
||||
await (formData.value?.id
|
||||
? updateDeliveryPickUpStore(data)
|
||||
: createDeliveryPickUpStore(data));
|
||||
// 关闭并提示
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
ElMessage.success($t('ui.actionMessage.operationSuccess'));
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
formData.value = undefined;
|
||||
return;
|
||||
}
|
||||
// 加载数据
|
||||
const data = modalApi.getData<MallDeliveryPickUpStoreApi.PickUpStore>();
|
||||
if (!data || !data.id) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
try {
|
||||
formData.value = await getDeliveryPickUpStore(data.id as number);
|
||||
// 设置到 values
|
||||
await formApi.setValues(formData.value);
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal class="w-2/5" :title="getTitle">
|
||||
<Form class="mx-4" />
|
||||
</Modal>
|
||||
</template>
|
||||
214
apps/web-ele/src/views/mall/trade/order/data.ts
Normal file
214
apps/web-ele/src/views/mall/trade/order/data.ts
Normal file
@@ -0,0 +1,214 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { MallDeliveryPickUpStoreApi } from '#/api/mall/trade/delivery/pickUpStore';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { getSimpleDeliveryExpressList } from '#/api/mall/trade/delivery/express';
|
||||
import { getSimpleDeliveryPickUpStoreList } from '#/api/mall/trade/delivery/pickUpStore';
|
||||
import {
|
||||
DeliveryTypeEnum,
|
||||
DICT_TYPE,
|
||||
getDictOptions,
|
||||
getRangePickerDefaultProps,
|
||||
} from '#/utils';
|
||||
|
||||
const pickUpStoreList = ref<MallDeliveryPickUpStoreApi.PickUpStore[]>([]);
|
||||
|
||||
getSimpleDeliveryPickUpStoreList().then((res) => {
|
||||
pickUpStoreList.value = res;
|
||||
});
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'status',
|
||||
label: '订单状态',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.TRADE_ORDER_STATUS, 'number'),
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'payChannelCode',
|
||||
label: '支付方式',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.PAY_CHANNEL_CODE, 'number'),
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '品牌名称',
|
||||
component: 'Input',
|
||||
},
|
||||
{
|
||||
fieldName: 'createTime',
|
||||
label: '创建时间',
|
||||
component: 'RangePicker',
|
||||
componentProps: {
|
||||
...getRangePickerDefaultProps(),
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'terminal',
|
||||
label: '订单来源',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.TERMINAL, 'number'),
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'deliveryType',
|
||||
label: '配送方式',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.TRADE_DELIVERY_TYPE, 'number'),
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'logisticsId',
|
||||
label: '快递公司',
|
||||
component: 'ApiSelect',
|
||||
componentProps: {
|
||||
api: getSimpleDeliveryExpressList,
|
||||
labelField: 'name',
|
||||
valueField: 'id',
|
||||
},
|
||||
dependencies: {
|
||||
triggerFields: ['deliveryType'],
|
||||
show: (values) => values.deliveryType === DeliveryTypeEnum.EXPRESS.type,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'pickUpStoreId',
|
||||
label: '自提门店',
|
||||
component: 'ApiSelect',
|
||||
componentProps: {
|
||||
api: getSimpleDeliveryPickUpStoreList,
|
||||
labelField: 'name',
|
||||
valueField: 'id',
|
||||
},
|
||||
dependencies: {
|
||||
triggerFields: ['deliveryType'],
|
||||
show: (values) => values.deliveryType === DeliveryTypeEnum.PICK_UP.type,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'pickUpVerifyCode',
|
||||
label: '核销码',
|
||||
component: 'Input',
|
||||
dependencies: {
|
||||
triggerFields: ['deliveryType'],
|
||||
show: (values) => values.deliveryType === DeliveryTypeEnum.PICK_UP.type,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 表格列配置 */
|
||||
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
type: 'expand',
|
||||
width: 80,
|
||||
slots: { content: 'expand_content' },
|
||||
fixed: 'left',
|
||||
},
|
||||
{
|
||||
field: 'no',
|
||||
title: '订单号',
|
||||
fixed: 'left',
|
||||
minWidth: 180,
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '下单时间',
|
||||
formatter: 'formatDateTime',
|
||||
minWidth: 160,
|
||||
},
|
||||
{
|
||||
field: 'terminal',
|
||||
title: '订单来源',
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.TERMINAL },
|
||||
},
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'payChannelCode',
|
||||
title: '支付方式',
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.PAY_CHANNEL_CODE },
|
||||
},
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'payTime',
|
||||
title: '支付时间',
|
||||
formatter: 'formatDateTime',
|
||||
minWidth: 160,
|
||||
},
|
||||
{
|
||||
field: 'type',
|
||||
title: '订单类型',
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.TRADE_ORDER_TYPE },
|
||||
},
|
||||
minWidth: 80,
|
||||
},
|
||||
|
||||
{
|
||||
field: 'payPrice',
|
||||
title: '实际支付',
|
||||
formatter: 'formatAmount2',
|
||||
minWidth: 180,
|
||||
},
|
||||
{
|
||||
field: 'user',
|
||||
title: '买家/收货人',
|
||||
formatter: ({ row }) => {
|
||||
if (row.deliveryType === DeliveryTypeEnum.EXPRESS.type) {
|
||||
return `买家:${row.user?.nickname} / 收货人: ${row.receiverName} ${row.receiverMobile}${row.receiverAreaName}${row.receiverDetailAddress}`;
|
||||
}
|
||||
if (row.deliveryType === DeliveryTypeEnum.PICK_UP.type) {
|
||||
return `门店名称:${pickUpStoreList.value.find((item) => item.id === row.pickUpStoreId)?.name} /
|
||||
门店手机:${pickUpStoreList.value.find((item) => item.id === row.pickUpStoreId)?.phone} /
|
||||
自提门店:${pickUpStoreList.value.find((item) => item.id === row.pickUpStoreId)?.detailAddress}
|
||||
`;
|
||||
}
|
||||
return '';
|
||||
},
|
||||
minWidth: 180,
|
||||
},
|
||||
{
|
||||
field: 'deliveryType',
|
||||
title: '配送方式',
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.TRADE_DELIVERY_TYPE },
|
||||
},
|
||||
minWidth: 80,
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
title: '订单状态',
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.TRADE_ORDER_STATUS },
|
||||
},
|
||||
minWidth: 80,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 180,
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
217
apps/web-ele/src/views/mall/trade/order/index.vue
Normal file
217
apps/web-ele/src/views/mall/trade/order/index.vue
Normal file
@@ -0,0 +1,217 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { MallOrderApi } from '#/api/mall/trade/order';
|
||||
|
||||
import { h } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { DocAlert, Page, prompt, useVbenModal } from '@vben/common-ui';
|
||||
import { fenToYuan } from '@vben/utils';
|
||||
|
||||
import { ElImage, ElInput, ElTag } from 'element-plus';
|
||||
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getOrderPage, updateOrderRemark } from '#/api/mall/trade/order';
|
||||
import { DictTag } from '#/components/dict-tag';
|
||||
import { $t } from '#/locales';
|
||||
import { DeliveryTypeEnum, DICT_TYPE, TradeOrderStatusEnum } from '#/utils';
|
||||
|
||||
import { useGridColumns, useGridFormSchema } from './data';
|
||||
import DeleveryForm from './modules/delevery-form.vue';
|
||||
|
||||
const [DeleveryFormModal, deleveryFormModalApi] = useVbenModal({
|
||||
connectedComponent: DeleveryForm,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 刷新表格 */
|
||||
function onRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
const { push } = useRouter();
|
||||
// TODO xingyu:貌似详情,还点不进去哇?
|
||||
/** 详情 */
|
||||
function handleDetail(row: MallOrderApi.Order) {
|
||||
push({ name: 'TradeOrderDetail', params: { id: row.id } });
|
||||
}
|
||||
|
||||
/** 发货 */
|
||||
function handleDelivery(row: MallOrderApi.Order) {
|
||||
deleveryFormModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 备注 */
|
||||
function handleRemake(row: MallOrderApi.Order) {
|
||||
prompt({
|
||||
component: () => {
|
||||
return h(ElInput, {
|
||||
defaultValue: row.remark,
|
||||
rows: 3,
|
||||
type: 'textarea',
|
||||
});
|
||||
},
|
||||
content: '请输入订单备注',
|
||||
title: '订单备注',
|
||||
modelPropName: 'value',
|
||||
}).then(async (val) => {
|
||||
if (val) {
|
||||
await updateOrderRemark({
|
||||
id: row.id as number,
|
||||
remark: val,
|
||||
});
|
||||
onRefresh();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
expandConfig: {
|
||||
trigger: 'row',
|
||||
expandAll: true,
|
||||
padding: true,
|
||||
},
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getOrderPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: { code: 'query' },
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<MallOrderApi.Order>,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<template #doc>
|
||||
<DocAlert
|
||||
title="【交易】交易订单"
|
||||
url="https://doc.iocoder.cn/mall/trade-order/"
|
||||
/>
|
||||
<DocAlert
|
||||
title="【交易】购物车"
|
||||
url="https://doc.iocoder.cn/mall/trade-cart/"
|
||||
/>
|
||||
</template>
|
||||
<DeleveryFormModal @success="onRefresh" />
|
||||
<Grid table-title="订单列表">
|
||||
<template #expand_content="{ row }">
|
||||
<div class="order-items">
|
||||
<div v-for="item in row.items" :key="item.id" class="order-item">
|
||||
<div class="order-item-image">
|
||||
<ElImage :src="item.picUrl" :width="40" :height="40" />
|
||||
</div>
|
||||
<div class="order-item-content">
|
||||
<div class="order-item-name">
|
||||
{{ item.spuName }}
|
||||
<ElTag
|
||||
v-for="property in item.properties"
|
||||
:key="property.id"
|
||||
class="ml-1"
|
||||
>
|
||||
{{ property.propertyName }}: {{ property.valueName }}
|
||||
</ElTag>
|
||||
</div>
|
||||
<div class="order-item-info">
|
||||
<span
|
||||
>原价:{{ fenToYuan(item.price) }} 元 / 数量:{{
|
||||
item.count
|
||||
}}个</span
|
||||
>
|
||||
<DictTag
|
||||
:type="DICT_TYPE.TRADE_ORDER_ITEM_AFTER_SALE_STATUS"
|
||||
:value="item.afterSaleStatus"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('common.detail'),
|
||||
link: true,
|
||||
icon: ACTION_ICON.VIEW,
|
||||
auth: ['trade:order:query'],
|
||||
onClick: handleDetail.bind(null, row),
|
||||
},
|
||||
]"
|
||||
:drop-down-actions="[
|
||||
{
|
||||
label: '发货',
|
||||
link: true,
|
||||
ifShow: () =>
|
||||
row.deliveryType === DeliveryTypeEnum.EXPRESS.type &&
|
||||
row.status === TradeOrderStatusEnum.UNDELIVERED.status,
|
||||
onClick: handleDelivery.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: '备注',
|
||||
link: true,
|
||||
onClick: handleRemake.bind(null, row),
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.order-items {
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.order-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.order-item-image {
|
||||
flex-shrink: 0;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.order-item-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.order-item-name {
|
||||
margin-bottom: 4px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.order-item-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,131 @@
|
||||
<script lang="ts" setup>
|
||||
import type { MallOrderApi } from '#/api/mall/trade/order';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { getSimpleDeliveryExpressList } from '#/api/mall/trade/delivery/express';
|
||||
import { deliveryOrder } from '#/api/mall/trade/order';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
|
||||
const formData = ref<MallOrderApi.DeliveryRequest>();
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
formItemClass: 'col-span-2',
|
||||
labelWidth: 120,
|
||||
},
|
||||
layout: 'horizontal',
|
||||
schema: [
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'id',
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
show: () => false,
|
||||
},
|
||||
},
|
||||
// TODO @xingyu:发货默认选中第一个?
|
||||
{
|
||||
fieldName: 'expressType',
|
||||
label: '发货方式',
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
options: [
|
||||
{ label: '快递', value: 'express' },
|
||||
{ label: '无需发货', value: 'none' },
|
||||
],
|
||||
buttonStyle: 'solid',
|
||||
optionType: 'button',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'logisticsId',
|
||||
label: '物流公司',
|
||||
component: 'ApiSelect',
|
||||
componentProps: {
|
||||
api: getSimpleDeliveryExpressList,
|
||||
fieldNames: {
|
||||
label: 'name',
|
||||
value: 'id',
|
||||
},
|
||||
},
|
||||
dependencies: {
|
||||
triggerFields: ['expressType'],
|
||||
show: (values) => values.expressType === 'express',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'logisticsNo',
|
||||
label: '物流单号',
|
||||
component: 'Input',
|
||||
dependencies: {
|
||||
triggerFields: ['expressType'],
|
||||
show: (values) => values.expressType === 'express',
|
||||
},
|
||||
},
|
||||
],
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
// 提交表单
|
||||
const data = (await formApi.getValues()) as MallOrderApi.DeliveryRequest;
|
||||
if (data.expressType === 'none') {
|
||||
// 无需发货的情况
|
||||
data.logisticsId = 0;
|
||||
data.logisticsNo = '';
|
||||
}
|
||||
try {
|
||||
await deliveryOrder(data);
|
||||
// 关闭并提示
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
ElMessage.success($t('ui.actionMessage.operationSuccess'));
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
formData.value = undefined;
|
||||
return;
|
||||
}
|
||||
// 加载数据
|
||||
const data = modalApi.getData<MallOrderApi.Order>();
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
try {
|
||||
if (data.logisticsId === 0) {
|
||||
await formApi.setValues({ expressType: 'none' });
|
||||
}
|
||||
// 设置到 values
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal class="w-1/3" title="发货">
|
||||
<Form class="mx-4" />
|
||||
</Modal>
|
||||
</template>
|
||||
Reference in New Issue
Block a user