feat: 新增商城模块,新增会员中心的会员详情的订单管理,售后管理,收藏记录,优惠券,推广用户的展示
parent
280e79c55f
commit
4cc5d8bf92
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,2 @@
|
|||
export { default as SummaryCard } from './summary-card.vue';
|
||||
export type { SummaryCardProps } from './typing';
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
<script lang="ts" setup>
|
||||
import type { SummaryCardProps } from './typing';
|
||||
|
||||
import { CountTo } from '@vben/common-ui';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
|
||||
import { ElTooltip } from 'element-plus';
|
||||
|
||||
/** 统计卡片 */
|
||||
defineOptions({ name: 'SummaryCard' });
|
||||
|
||||
defineProps<SummaryCardProps>();
|
||||
</script>
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-row items-center gap-3 rounded bg-[var(--el-bg-color-overlay)] p-4"
|
||||
>
|
||||
<div
|
||||
class="flex h-12 w-12 flex-shrink-0 items-center justify-center rounded"
|
||||
:class="`${iconColor} ${iconBgColor}`"
|
||||
>
|
||||
<IconifyIcon v-if="icon" :icon="icon" class="!text-6" />
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="flex items-center gap-1">
|
||||
<span class="text-base">{{ title }}</span>
|
||||
<ElTooltip :content="tooltip" placement="topLeft" v-if="tooltip">
|
||||
<IconifyIcon
|
||||
icon="lucide:circle-alert"
|
||||
class="item-center !text-3 flex"
|
||||
/>
|
||||
</ElTooltip>
|
||||
</div>
|
||||
<div class="flex flex-row items-baseline gap-2">
|
||||
<div class="text-lg">
|
||||
<CountTo
|
||||
:prefix="prefix"
|
||||
:end-val="value ?? 0"
|
||||
:decimals="decimals ?? 0"
|
||||
/>
|
||||
</div>
|
||||
<span
|
||||
v-if="percent !== undefined"
|
||||
:class="Number(percent) > 0 ? 'text-red-500' : 'text-green-500'"
|
||||
>
|
||||
<span class="text-sm">{{ Math.abs(Number(percent)) }}%</span>
|
||||
<IconifyIcon
|
||||
:icon="
|
||||
Number(percent) > 0 ? 'lucide:chevron-up' : 'lucide:chevron-down'
|
||||
"
|
||||
class="ml-0.5 !text-sm"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
export interface SummaryCardProps {
|
||||
title: string;
|
||||
tooltip?: string;
|
||||
icon?: string;
|
||||
iconColor?: string;
|
||||
iconBgColor?: string;
|
||||
prefix?: string;
|
||||
value?: number;
|
||||
decimals?: number;
|
||||
percent?: number | string;
|
||||
}
|
||||
|
|
@ -0,0 +1,177 @@
|
|||
<script lang="ts" setup>
|
||||
import type {
|
||||
AnalysisOverviewItem,
|
||||
WorkbenchProjectItem,
|
||||
WorkbenchQuickNavItem,
|
||||
} from '@vben/common-ui';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import {
|
||||
AnalysisOverview,
|
||||
DocAlert,
|
||||
Page,
|
||||
WorkbenchQuickNav,
|
||||
} from '@vben/common-ui';
|
||||
import {
|
||||
SvgBellIcon,
|
||||
SvgCakeIcon,
|
||||
SvgCardIcon,
|
||||
SvgDownloadIcon,
|
||||
} from '@vben/icons';
|
||||
import { isString, openWindow } from '@vben/utils';
|
||||
|
||||
import { getUserCountComparison } from '#/api/mall/statistics/member';
|
||||
import { getOrderComparison } from '#/api/mall/statistics/trade';
|
||||
|
||||
/** 商城首页 */
|
||||
defineOptions({ name: 'MallHome' });
|
||||
|
||||
const loading = ref(true); // 加载中
|
||||
const orderComparison = ref(); // 交易对照数据
|
||||
const userComparison = ref(); // 用户对照数据
|
||||
|
||||
/** 查询交易对照卡片数据 */
|
||||
const getOrder = async () => {
|
||||
orderComparison.value = await getOrderComparison();
|
||||
};
|
||||
|
||||
/** 查询会员用户数量对照卡片数据 */
|
||||
const getUserCount = async () => {
|
||||
userComparison.value = await getUserCountComparison();
|
||||
};
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(async () => {
|
||||
loading.value = true;
|
||||
await Promise.all([getOrder(), getUserCount()]);
|
||||
loading.value = false;
|
||||
});
|
||||
|
||||
const overviewItems: AnalysisOverviewItem[] = [
|
||||
{
|
||||
icon: SvgCardIcon,
|
||||
title: '今日销售额',
|
||||
totalTitle: '昨日数据',
|
||||
totalValue: orderComparison.value?.reference?.orderPayPrice || 0,
|
||||
value: orderComparison.value?.orderPayPrice || 0,
|
||||
},
|
||||
{
|
||||
icon: SvgCakeIcon,
|
||||
title: '今日用户访问量',
|
||||
totalTitle: '总访问量',
|
||||
totalValue: userComparison.value?.reference?.visitUserCount || 0,
|
||||
value: userComparison.value?.visitUserCount || 0,
|
||||
},
|
||||
{
|
||||
icon: SvgDownloadIcon,
|
||||
title: '今日订单量',
|
||||
totalTitle: '总订单量',
|
||||
totalValue: orderComparison.value?.orderPayCount || 0,
|
||||
value: orderComparison.value?.reference?.orderPayCount || 0,
|
||||
},
|
||||
{
|
||||
icon: SvgBellIcon,
|
||||
title: '今日会员注册量',
|
||||
totalTitle: '总会员注册量',
|
||||
totalValue: userComparison.value?.registerUserCount || 0,
|
||||
value: userComparison.value?.reference?.registerUserCount || 0,
|
||||
},
|
||||
];
|
||||
|
||||
// 同样,这里的 url 也可以使用以 http 开头的外部链接
|
||||
const quickNavItems: WorkbenchQuickNavItem[] = [
|
||||
{
|
||||
color: '#1fdaca',
|
||||
icon: 'ep:user-filled',
|
||||
title: '用户管理',
|
||||
url: 'MemberUser',
|
||||
},
|
||||
{
|
||||
color: '#ff6b6b',
|
||||
icon: 'fluent-mdl2:product',
|
||||
title: '商品管理',
|
||||
url: 'ProductSpu',
|
||||
},
|
||||
{
|
||||
color: '#7c3aed',
|
||||
icon: 'ep:list',
|
||||
title: '订单管理',
|
||||
url: 'TradeOrder',
|
||||
},
|
||||
{
|
||||
color: '#3fb27f',
|
||||
icon: 'ri:refund-2-line',
|
||||
title: '售后管理',
|
||||
url: 'TradeAfterSale',
|
||||
},
|
||||
{
|
||||
color: '#4daf1bc9',
|
||||
icon: 'fa-solid:project-diagram',
|
||||
title: '分销管理',
|
||||
url: 'TradeBrokerageUser',
|
||||
},
|
||||
{
|
||||
color: '#1a73e8',
|
||||
icon: 'ep:ticket',
|
||||
title: '优惠券',
|
||||
url: 'PromotionCoupon',
|
||||
},
|
||||
{
|
||||
color: '#4daf1bc9',
|
||||
icon: 'fa:group',
|
||||
title: '拼团活动',
|
||||
url: 'PromotionBargainActivity',
|
||||
},
|
||||
{
|
||||
color: '#1a73e8',
|
||||
icon: 'vaadin:money-withdraw',
|
||||
title: '佣金提现',
|
||||
url: 'TradeBrokerageWithdraw',
|
||||
},
|
||||
{
|
||||
color: '#1a73e8',
|
||||
icon: 'vaadin:money-withdraw',
|
||||
title: '数据统计',
|
||||
url: 'TradeBrokerageWithdraw',
|
||||
},
|
||||
];
|
||||
|
||||
const router = useRouter();
|
||||
function navTo(nav: WorkbenchProjectItem | WorkbenchQuickNavItem) {
|
||||
if (nav.url?.startsWith('http')) {
|
||||
openWindow(nav.url);
|
||||
return;
|
||||
}
|
||||
if (nav.url?.startsWith('/')) {
|
||||
router.push(nav.url).catch((error) => {
|
||||
console.error('Navigation failed:', error);
|
||||
});
|
||||
} else if (isString(nav.url)) {
|
||||
router.push({ name: nav.url });
|
||||
} else {
|
||||
console.warn(`Unknown URL for navigation item: ${nav.title} -> ${nav.url}`);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<template #doc>
|
||||
<DocAlert
|
||||
title="商城手册(功能开启)"
|
||||
url="https://doc.iocoder.cn/mall/build/"
|
||||
/>
|
||||
</template>
|
||||
<AnalysisOverview :items="overviewItems" />
|
||||
<div class="mt-5 w-full lg:w-2/5">
|
||||
<WorkbenchQuickNav
|
||||
:items="quickNavItems"
|
||||
class="mt-5 lg:mt-0"
|
||||
title="快捷导航"
|
||||
@click="navTo"
|
||||
/>
|
||||
</div>
|
||||
</Page>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,132 @@
|
|||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeGridPropTypes } from '#/adapter/vxe-table';
|
||||
|
||||
import { z } from '#/adapter/form';
|
||||
import {
|
||||
CommonStatusEnum,
|
||||
DICT_TYPE,
|
||||
getDictOptions,
|
||||
getRangePickerDefaultProps,
|
||||
} from '#/utils';
|
||||
|
||||
/** 新增/修改的表单 */
|
||||
export function useFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'id',
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
show: () => false,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '品牌名称',
|
||||
component: 'Input',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'picUrl',
|
||||
label: '品牌图片',
|
||||
component: 'ImageUpload',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'sort',
|
||||
label: '品牌排序',
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
min: 0,
|
||||
controlsPosition: 'right',
|
||||
placeholder: '请输入品牌排序',
|
||||
},
|
||||
rules: z.number().min(0).default(1),
|
||||
},
|
||||
{
|
||||
fieldName: 'status',
|
||||
label: '品牌状态',
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
|
||||
buttonStyle: 'solid',
|
||||
optionType: 'button',
|
||||
},
|
||||
rules: z.number().default(CommonStatusEnum.ENABLE),
|
||||
},
|
||||
{
|
||||
fieldName: 'description',
|
||||
label: '品牌描述',
|
||||
component: 'Textarea',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '品牌名称',
|
||||
component: 'Input',
|
||||
},
|
||||
{
|
||||
fieldName: 'status',
|
||||
label: '品牌状态',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'createTime',
|
||||
label: '创建时间',
|
||||
component: 'RangePicker',
|
||||
componentProps: {
|
||||
...getRangePickerDefaultProps(),
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 表格列配置 */
|
||||
export function useGridColumns(): VxeGridPropTypes.Columns {
|
||||
return [
|
||||
{
|
||||
field: 'name',
|
||||
title: '分类名称',
|
||||
fixed: 'left',
|
||||
},
|
||||
{
|
||||
field: 'picUrl',
|
||||
title: '品牌图片',
|
||||
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: 180,
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -0,0 +1,125 @@
|
|||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { MallBrandApi } from '#/api/mall/product/brand';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { ElLoading, ElMessage } from 'element-plus';
|
||||
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { deleteBrand, getBrandPage } from '#/api/mall/product/brand';
|
||||
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: MallBrandApi.Brand) {
|
||||
formModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 删除品牌 */
|
||||
async function handleDelete(row: MallBrandApi.Brand) {
|
||||
const loadingInstance = ElLoading.service({
|
||||
text: $t('ui.actionMessage.deleting', [row.name]),
|
||||
fullscreen: true,
|
||||
});
|
||||
try {
|
||||
await deleteBrand(row.id as number);
|
||||
ElMessage.success($t('ui.actionMessage.deleteSuccess', [row.name]));
|
||||
onRefresh();
|
||||
} finally {
|
||||
loadingInstance.close();
|
||||
}
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getBrandPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: { code: 'query' },
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<MallBrandApi.Brand>,
|
||||
});
|
||||
</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: ['product:brand:create'],
|
||||
onClick: handleCreate,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('common.edit'),
|
||||
type: 'primary',
|
||||
link: true,
|
||||
icon: ACTION_ICON.EDIT,
|
||||
auth: ['product:brand:update'],
|
||||
onClick: handleEdit.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: $t('common.delete'),
|
||||
type: 'danger',
|
||||
link: true,
|
||||
icon: ACTION_ICON.DELETE,
|
||||
auth: ['product:brand:delete'],
|
||||
popConfirm: {
|
||||
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
|
||||
confirm: handleDelete.bind(null, row),
|
||||
},
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
<script lang="ts" setup>
|
||||
import type { MallBrandApi } from '#/api/mall/product/brand';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { createBrand, getBrand, updateBrand } from '#/api/mall/product/brand';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useFormSchema } from '../data';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
|
||||
const formData = ref<MallBrandApi.Brand>();
|
||||
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 MallBrandApi.Brand;
|
||||
try {
|
||||
await (formData.value?.id ? updateBrand(data) : createBrand(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<MallBrandApi.Brand>();
|
||||
if (!data || !data.id) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
try {
|
||||
formData.value = await getBrand(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,139 @@
|
|||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { MallCategoryApi } from '#/api/mall/product/category';
|
||||
|
||||
import { handleTree } from '@vben/utils';
|
||||
|
||||
import { z } from '#/adapter/form';
|
||||
import { getCategoryList } from '#/api/mall/product/category';
|
||||
import { CommonStatusEnum, DICT_TYPE, getDictOptions } from '#/utils';
|
||||
|
||||
/** 新增/修改的表单 */
|
||||
export function useFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'id',
|
||||
component: 'Input',
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
show: () => false,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'parentId',
|
||||
label: '上级分类',
|
||||
component: 'ApiTreeSelect',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
api: async () => {
|
||||
const data = await getCategoryList({ parentId: 0 });
|
||||
data.unshift({
|
||||
id: 0,
|
||||
name: '顶级分类',
|
||||
picUrl: '',
|
||||
sort: 0,
|
||||
status: 0,
|
||||
});
|
||||
return handleTree(data);
|
||||
},
|
||||
labelField: 'name',
|
||||
valueField: 'id',
|
||||
childrenField: 'children',
|
||||
placeholder: '请选择上级分类',
|
||||
treeDefaultExpandAll: true,
|
||||
},
|
||||
rules: 'selectRequired',
|
||||
},
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '分类名称',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入分类名称',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'picUrl',
|
||||
label: '移动端分类图',
|
||||
component: 'ImageUpload',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'sort',
|
||||
label: '分类排序',
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
min: 0,
|
||||
controlsPosition: 'right',
|
||||
placeholder: '请输入分类排序',
|
||||
},
|
||||
rules: z.number().min(0).default(1),
|
||||
},
|
||||
{
|
||||
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',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns(): VxeTableGridOptions<MallCategoryApi.Category>['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'name',
|
||||
title: '分类名称',
|
||||
align: 'left',
|
||||
fixed: 'left',
|
||||
treeNode: true,
|
||||
},
|
||||
{
|
||||
field: 'picUrl',
|
||||
title: '移动端分类图',
|
||||
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: 300,
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -0,0 +1,187 @@
|
|||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { MallCategoryApi } from '#/api/mall/product/category';
|
||||
|
||||
import { ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { DocAlert, Page, useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { ElLoading, ElMessage } from 'element-plus';
|
||||
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { deleteCategory, getCategoryList } from '#/api/mall/product/category';
|
||||
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({}).open();
|
||||
}
|
||||
|
||||
/** 添加下级分类 */
|
||||
function handleAppend(row: MallCategoryApi.Category) {
|
||||
formModalApi.setData({ parentId: row.id }).open();
|
||||
}
|
||||
|
||||
/** 编辑分类 */
|
||||
function handleEdit(row: MallCategoryApi.Category) {
|
||||
formModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 查看商品操作 */
|
||||
const router = useRouter(); // 路由
|
||||
const handleViewSpu = (id: number) => {
|
||||
router.push({
|
||||
name: 'ProductSpu',
|
||||
query: { categoryId: id },
|
||||
});
|
||||
};
|
||||
|
||||
/** 删除分类 */
|
||||
async function handleDelete(row: MallCategoryApi.Category) {
|
||||
const loadingInstance = ElLoading.service({
|
||||
text: $t('ui.actionMessage.deleting', [row.name]),
|
||||
fullscreen: true,
|
||||
});
|
||||
try {
|
||||
await deleteCategory(row.id as number);
|
||||
ElMessage.success($t('ui.actionMessage.deleteSuccess', [row.name]));
|
||||
onRefresh();
|
||||
} finally {
|
||||
loadingInstance.close();
|
||||
}
|
||||
}
|
||||
|
||||
/** 切换树形展开/收缩状态 */
|
||||
const isExpanded = ref(false);
|
||||
function toggleExpand() {
|
||||
isExpanded.value = !isExpanded.value;
|
||||
gridApi.grid.setAllTreeExpand(isExpanded.value);
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
pagerConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async (_, formValues) => {
|
||||
return await getCategoryList(formValues);
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: { code: 'query' },
|
||||
},
|
||||
treeConfig: {
|
||||
parentField: 'parentId',
|
||||
rowField: 'id',
|
||||
transform: true,
|
||||
reserve: true,
|
||||
},
|
||||
} as VxeTableGridOptions<MallCategoryApi.Category>,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<template #doc>
|
||||
<DocAlert
|
||||
title="【产品】产品管理、产品分类"
|
||||
url="https://doc.iocoder.cn/crm/product/"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<FormModal @success="onRefresh" />
|
||||
<Grid>
|
||||
<template #toolbar-tools>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('ui.actionTitle.create', ['分类']),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.ADD,
|
||||
auth: ['product:category:create'],
|
||||
onClick: handleCreate,
|
||||
},
|
||||
{
|
||||
label: isExpanded ? '收缩' : '展开',
|
||||
type: 'primary',
|
||||
onClick: toggleExpand,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<template #name="{ row }">
|
||||
<div class="flex w-full items-center gap-1">
|
||||
<span class="flex-auto">{{ row.name }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '新增下级',
|
||||
type: 'primary',
|
||||
link: true,
|
||||
icon: ACTION_ICON.ADD,
|
||||
auth: ['product:category:create'],
|
||||
onClick: handleAppend.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: $t('common.edit'),
|
||||
type: 'primary',
|
||||
link: true,
|
||||
icon: ACTION_ICON.EDIT,
|
||||
auth: ['product:category:update'],
|
||||
onClick: handleEdit.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: '查看商品',
|
||||
type: 'primary',
|
||||
link: true,
|
||||
icon: ACTION_ICON.VIEW,
|
||||
auth: ['product:category:update'],
|
||||
ifShow: row.parentId > 0,
|
||||
onClick: handleViewSpu.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: $t('common.delete'),
|
||||
type: 'danger',
|
||||
link: true,
|
||||
icon: ACTION_ICON.DELETE,
|
||||
auth: ['product:category: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 { MallCategoryApi } from '#/api/mall/product/category';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import {
|
||||
createCategory,
|
||||
getCategory,
|
||||
updateCategory,
|
||||
} from '#/api/mall/product/category';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useFormSchema } from '../data';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const formData = ref<MallCategoryApi.Category>();
|
||||
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 MallCategoryApi.Category;
|
||||
try {
|
||||
await (formData.value?.id ? updateCategory(data) : createCategory(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;
|
||||
}
|
||||
// 加载数据
|
||||
let data = modalApi.getData<MallCategoryApi.Category>();
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
try {
|
||||
if (data.id) {
|
||||
data = await getCategory(data.id as number);
|
||||
}
|
||||
// 设置到 values
|
||||
formData.value = data;
|
||||
await formApi.setValues(data);
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal class="w-2/5" :title="getTitle">
|
||||
<Form class="mx-4" />
|
||||
</Modal>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,202 @@
|
|||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeGridPropTypes } from '#/adapter/vxe-table';
|
||||
import type { MallCommentApi } from '#/api/mall/product/comment';
|
||||
|
||||
import { getSpuSimpleList } from '#/api/mall/product/spu';
|
||||
import { getRangePickerDefaultProps } from '#/utils';
|
||||
|
||||
/** 新增/修改的表单 */
|
||||
export function useFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'id',
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
show: () => false,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'spuId',
|
||||
label: '商品',
|
||||
component: 'ApiSelect',
|
||||
componentProps: {
|
||||
api: getSpuSimpleList,
|
||||
labelField: 'name',
|
||||
valueField: 'id',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'userAvatar',
|
||||
label: '用户头像',
|
||||
component: 'ImageUpload',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'userNickname',
|
||||
label: '用户名称',
|
||||
component: 'Input',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'content',
|
||||
label: '评论内容',
|
||||
component: 'Textarea',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'descriptionScores',
|
||||
label: '描述星级',
|
||||
component: 'Rate',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'benefitScores',
|
||||
label: '服务星级',
|
||||
component: 'Rate',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'picUrls',
|
||||
label: '评论图片',
|
||||
component: 'ImageUpload',
|
||||
componentProps: {
|
||||
maxNumber: 9,
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'replyStatus',
|
||||
label: '回复状态',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options: [
|
||||
{ label: '已回复', value: true },
|
||||
{ label: '未回复', value: false },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'spuName',
|
||||
label: '商品名称',
|
||||
component: 'Input',
|
||||
},
|
||||
{
|
||||
fieldName: 'userNickname',
|
||||
label: '用户名称',
|
||||
component: 'Input',
|
||||
},
|
||||
{
|
||||
fieldName: 'orderId',
|
||||
label: '订单编号',
|
||||
component: 'Input',
|
||||
},
|
||||
{
|
||||
fieldName: 'createTime',
|
||||
label: '评论时间',
|
||||
component: 'RangePicker',
|
||||
componentProps: {
|
||||
...getRangePickerDefaultProps(),
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 表格列配置 */
|
||||
export function useGridColumns<T = MallCommentApi.Comment>(
|
||||
onStatusChange?: (
|
||||
newStatus: boolean,
|
||||
row: T,
|
||||
) => PromiseLike<boolean | undefined>,
|
||||
): VxeGridPropTypes.Columns {
|
||||
return [
|
||||
{
|
||||
field: 'id',
|
||||
title: '评论编号',
|
||||
fixed: 'left',
|
||||
},
|
||||
{
|
||||
field: 'skuPicUrl',
|
||||
title: '商品图片',
|
||||
cellRender: {
|
||||
name: 'CellImage',
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'spuName',
|
||||
title: '商品名称',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'skuProperties',
|
||||
title: '商品属性',
|
||||
minWidth: 200,
|
||||
formatter: ({ cellValue }) => {
|
||||
return cellValue && cellValue.length > 0
|
||||
? cellValue
|
||||
.map((item: any) => `${item.propertyName} : ${item.valueName}`)
|
||||
.join('\n')
|
||||
: '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'userNickname',
|
||||
title: '用户名称',
|
||||
},
|
||||
{
|
||||
field: 'descriptionScores',
|
||||
title: '商品评分',
|
||||
},
|
||||
{
|
||||
field: 'benefitScores',
|
||||
title: '服务评分',
|
||||
},
|
||||
{
|
||||
field: 'content',
|
||||
title: '评论内容',
|
||||
},
|
||||
{
|
||||
field: 'picUrls',
|
||||
title: '评论图片',
|
||||
cellRender: {
|
||||
name: 'CellImages',
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'replyContent',
|
||||
title: '回复内容',
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '评论时间',
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'visible',
|
||||
title: '是否展示',
|
||||
align: 'center',
|
||||
cellRender: {
|
||||
attrs: { beforeChange: onStatusChange },
|
||||
name: 'CellSwitch',
|
||||
props: {
|
||||
checkedValue: true,
|
||||
unCheckedValue: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 80,
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -0,0 +1,159 @@
|
|||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { MallCommentApi } from '#/api/mall/product/comment';
|
||||
|
||||
import { h } from 'vue';
|
||||
|
||||
import { confirm, DocAlert, Page, prompt, useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { ElInput, ElMessage } from 'element-plus';
|
||||
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
getCommentPage,
|
||||
replyComment,
|
||||
updateCommentVisible,
|
||||
} from '#/api/mall/product/comment';
|
||||
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 handleReply(row: MallCommentApi.Comment) {
|
||||
prompt({
|
||||
component: () => {
|
||||
return h(ElInput, {
|
||||
type: 'textarea',
|
||||
});
|
||||
},
|
||||
content: row.content
|
||||
? `用户评论:${row.content}\n请输入回复内容:`
|
||||
: '请输入回复内容:',
|
||||
title: '回复评论',
|
||||
modelPropName: 'value',
|
||||
}).then(async (val) => {
|
||||
if (val) {
|
||||
await replyComment({
|
||||
id: row.id as number,
|
||||
replyContent: val,
|
||||
});
|
||||
onRefresh();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** 更新状态 */
|
||||
async function handleStatusChange(
|
||||
newStatus: boolean,
|
||||
row: MallCommentApi.Comment,
|
||||
): Promise<boolean | undefined> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const text = newStatus ? '展示' : '隐藏';
|
||||
confirm({
|
||||
content: `确认要${text + row.id}评论吗?`,
|
||||
})
|
||||
.then(async () => {
|
||||
// 更新状态
|
||||
const res = await updateCommentVisible({
|
||||
id: row.id as number,
|
||||
visible: newStatus,
|
||||
});
|
||||
if (res) {
|
||||
// 提示并返回成功
|
||||
ElMessage.success(`${text}成功`);
|
||||
resolve(true);
|
||||
} else {
|
||||
reject(new Error('操作失败'));
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
reject(new Error('取消操作'));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(handleStatusChange),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getCommentPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: { code: 'query' },
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<MallCommentApi.Comment>,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<template #doc>
|
||||
<DocAlert
|
||||
title="【商品】商品评价"
|
||||
url="https://doc.iocoder.cn/mall/product-comment/"
|
||||
/>
|
||||
</template>
|
||||
<FormModal @success="onRefresh" />
|
||||
<Grid table-title="评论列表">
|
||||
<template #toolbar-tools>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('ui.actionTitle.create', ['虚拟评论']),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.ADD,
|
||||
auth: ['product:comment:create'],
|
||||
onClick: handleCreate,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '回复',
|
||||
type: 'primary',
|
||||
link: true,
|
||||
auth: ['product:comment:update'],
|
||||
onClick: handleReply.bind(null, row),
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
<script lang="ts" setup>
|
||||
import type { MallCommentApi } from '#/api/mall/product/comment';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { createComment, getComment } from '#/api/mall/product/comment';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useFormSchema } from '../data';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
|
||||
const formData = ref<MallCommentApi.Comment>();
|
||||
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 MallCommentApi.Comment;
|
||||
try {
|
||||
await createComment(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<MallCommentApi.Comment>();
|
||||
if (!data || !data.id) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
try {
|
||||
formData.value = await getComment(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,176 @@
|
|||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
import { getPropertySimpleList } from '#/api/mall/product/property';
|
||||
|
||||
// ============================== 属性 ==============================
|
||||
|
||||
/** 类型新增/修改的表单 */
|
||||
export function usePropertyFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'id',
|
||||
component: 'Input',
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
show: () => false,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '名称',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入名称',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'remark',
|
||||
label: '备注',
|
||||
component: 'Textarea',
|
||||
componentProps: {
|
||||
placeholder: '请输入备注',
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 类型列表的搜索表单 */
|
||||
export function usePropertyGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '名称',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入名称',
|
||||
clearable: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 类型列表的字段 */
|
||||
export function usePropertyGridColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'id',
|
||||
title: '编号',
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
title: '名称',
|
||||
},
|
||||
{
|
||||
field: 'remark',
|
||||
title: '备注',
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '创建时间',
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 160,
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
// ============================== 值数据 ==============================
|
||||
|
||||
/** 数据新增/修改的表单 */
|
||||
export function useValueFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'id',
|
||||
component: 'Input',
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
show: () => false,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'propertyId',
|
||||
label: '属性编号',
|
||||
component: 'ApiSelect',
|
||||
componentProps: (values) => {
|
||||
return {
|
||||
api: getPropertySimpleList,
|
||||
labelField: 'name',
|
||||
valueField: 'id',
|
||||
disabled: !!values.id,
|
||||
};
|
||||
},
|
||||
rules: 'required',
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '名称',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入名称',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'remark',
|
||||
label: '备注',
|
||||
component: 'Textarea',
|
||||
componentProps: {
|
||||
placeholder: '请输入备注',
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 字典数据列表搜索表单 */
|
||||
export function useValueGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '名称',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
clearable: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 字典数据表格列
|
||||
*/
|
||||
export function useValueGridColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'id',
|
||||
title: '编号',
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
title: '属性值名称',
|
||||
},
|
||||
{
|
||||
field: 'remark',
|
||||
title: '备注',
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
field: 'createTime',
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 160,
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { DocAlert, Page } from '@vben/common-ui';
|
||||
|
||||
import PropertyGrid from './modules/property-grid.vue';
|
||||
import ValueGrid from './modules/value-grid.vue';
|
||||
|
||||
const searchPropertyId = ref<number>(); // 搜索的属性ID
|
||||
|
||||
function handlePropertyIdSelect(propertyId: number) {
|
||||
searchPropertyId.value = propertyId;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<template #doc>
|
||||
<DocAlert
|
||||
title="【商品】商品属性"
|
||||
url="https://doc.iocoder.cn/mall/product-property/"
|
||||
/>
|
||||
</template>
|
||||
<div class="flex h-full">
|
||||
<!-- 左侧属性列表 -->
|
||||
<div class="w-1/2 pr-3">
|
||||
<PropertyGrid @select="handlePropertyIdSelect" />
|
||||
</div>
|
||||
<!-- 右侧属性数据列表 -->
|
||||
<div class="w-1/2">
|
||||
<ValueGrid :property-id="searchPropertyId" />
|
||||
</div>
|
||||
</div>
|
||||
</Page>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
<script lang="ts" setup>
|
||||
import type { MallPropertyApi } from '#/api/mall/product/property';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import {
|
||||
createProperty,
|
||||
getProperty,
|
||||
updateProperty,
|
||||
} from '#/api/mall/product/property';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { usePropertyFormSchema } from '../data';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const formData = ref<MallPropertyApi.Property>();
|
||||
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: 80,
|
||||
},
|
||||
layout: 'horizontal',
|
||||
schema: usePropertyFormSchema(),
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
// 提交表单
|
||||
const data = (await formApi.getValues()) as MallPropertyApi.Property;
|
||||
try {
|
||||
await (formData.value?.id ? updateProperty(data) : createProperty(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<MallPropertyApi.Property>();
|
||||
if (!data || !data.id) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
try {
|
||||
formData.value = await getProperty(data.id as number);
|
||||
// 设置到 values
|
||||
if (formData.value) {
|
||||
await formApi.setValues(formData.value);
|
||||
}
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="getTitle">
|
||||
<Form class="mx-4" />
|
||||
</Modal>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,140 @@
|
|||
<script lang="ts" setup>
|
||||
import type {
|
||||
VxeGridListeners,
|
||||
VxeTableGridOptions,
|
||||
} from '#/adapter/vxe-table';
|
||||
import type { MallPropertyApi } from '#/api/mall/product/property';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { ElLoading, ElMessage } from 'element-plus';
|
||||
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { deleteProperty, getPropertyPage } from '#/api/mall/product/property';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { usePropertyGridColumns, usePropertyGridFormSchema } from '../data';
|
||||
import PropertyForm from './property-form.vue';
|
||||
|
||||
const emit = defineEmits(['select']);
|
||||
|
||||
const [PropertyFormModal, propertyFormModalApi] = useVbenModal({
|
||||
connectedComponent: PropertyForm,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 刷新表格 */
|
||||
function onRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 创建属性 */
|
||||
function handleCreate() {
|
||||
propertyFormModalApi.setData(null).open();
|
||||
}
|
||||
|
||||
/** 编辑属性 */
|
||||
function handleEdit(row: any) {
|
||||
propertyFormModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 删除属性 */
|
||||
async function handleDelete(row: MallPropertyApi.Property) {
|
||||
const loadingInstance = ElLoading.service({
|
||||
text: $t('ui.actionMessage.deleting', [row.name]),
|
||||
fullscreen: true,
|
||||
});
|
||||
try {
|
||||
await deleteProperty(row.id as number);
|
||||
ElMessage.success($t('ui.actionMessage.deleteSuccess', [row.name]));
|
||||
onRefresh();
|
||||
} finally {
|
||||
loadingInstance.close();
|
||||
}
|
||||
}
|
||||
|
||||
/** 表格事件 */
|
||||
const gridEvents: VxeGridListeners<MallPropertyApi.Property> = {
|
||||
cellClick: ({ row }) => {
|
||||
emit('select', row.id);
|
||||
},
|
||||
};
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: usePropertyGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: usePropertyGridColumns(),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getPropertyPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isCurrent: true,
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: { code: 'query' },
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<MallPropertyApi.Property>,
|
||||
gridEvents,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-full">
|
||||
<PropertyFormModal @success="onRefresh" />
|
||||
|
||||
<Grid table-title="属性列表">
|
||||
<template #toolbar-tools>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('ui.actionTitle.create', ['属性']),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.ADD,
|
||||
auth: ['product:property:create'],
|
||||
onClick: handleCreate,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('common.edit'),
|
||||
type: 'primary',
|
||||
link: true,
|
||||
icon: ACTION_ICON.EDIT,
|
||||
auth: ['product:property:update'],
|
||||
onClick: handleEdit.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: $t('common.delete'),
|
||||
type: 'danger',
|
||||
link: true,
|
||||
icon: ACTION_ICON.DELETE,
|
||||
auth: ['product:property:delete'],
|
||||
popConfirm: {
|
||||
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
|
||||
confirm: handleDelete.bind(null, row),
|
||||
},
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
<script lang="ts" setup>
|
||||
import type { MallPropertyApi } from '#/api/mall/product/property';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import {
|
||||
createPropertyValue,
|
||||
getPropertyValue,
|
||||
updatePropertyValue,
|
||||
} from '#/api/mall/product/property';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useValueFormSchema } from '../data';
|
||||
|
||||
defineOptions({ name: 'MallPropertyValueForm' });
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const formData = ref<MallPropertyApi.PropertyValue>();
|
||||
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: 80,
|
||||
},
|
||||
layout: 'horizontal',
|
||||
schema: useValueFormSchema(),
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
// 提交表单
|
||||
const data = (await formApi.getValues()) as MallPropertyApi.PropertyValue;
|
||||
try {
|
||||
await (formData.value?.id
|
||||
? updatePropertyValue(data)
|
||||
: createPropertyValue(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<
|
||||
MallPropertyApi.PropertyValue | { propertyId?: string }
|
||||
>();
|
||||
|
||||
// 如果有ID,表示是编辑
|
||||
if (data && 'id' in data && data.id) {
|
||||
modalApi.lock();
|
||||
try {
|
||||
formData.value = await getPropertyValue(data.id as number);
|
||||
// 设置到 values
|
||||
if (formData.value) {
|
||||
await formApi.setValues(formData.value);
|
||||
}
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
} else if (data && 'propertyId' in data && data.propertyId) {
|
||||
// 新增时,如果传入了propertyId,则需要设置
|
||||
await formApi.setValues({
|
||||
propertyId: data.propertyId,
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="getTitle">
|
||||
<Form class="mx-4" />
|
||||
</Modal>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,149 @@
|
|||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { MallPropertyApi } from '#/api/mall/product/property';
|
||||
|
||||
import { watch } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { ElLoading, ElMessage } from 'element-plus';
|
||||
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
deletePropertyValue,
|
||||
getPropertyValuePage,
|
||||
} from '#/api/mall/product/property';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useValueGridColumns, useValueGridFormSchema } from '../data';
|
||||
import ValueForm from './value-form.vue';
|
||||
|
||||
const props = defineProps({
|
||||
propertyId: {
|
||||
type: Number,
|
||||
default: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
const [ValueFormModal, valueFormModalApi] = useVbenModal({
|
||||
connectedComponent: ValueForm,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 刷新表格 */
|
||||
function onRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 创建字典数据 */
|
||||
function handleCreate() {
|
||||
valueFormModalApi.setData({ propertyId: props.propertyId }).open();
|
||||
}
|
||||
|
||||
/** 编辑字典数据 */
|
||||
function handleEdit(row: MallPropertyApi.PropertyValue) {
|
||||
valueFormModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 删除字典数据 */
|
||||
async function handleDelete(row: MallPropertyApi.PropertyValue) {
|
||||
const loadingInstance = ElLoading.service({
|
||||
text: $t('ui.actionMessage.deleting', [row.name]),
|
||||
fullscreen: true,
|
||||
});
|
||||
try {
|
||||
await deletePropertyValue(row.id as number);
|
||||
ElMessage.success($t('ui.actionMessage.deleteSuccess', [row.name]));
|
||||
onRefresh();
|
||||
} finally {
|
||||
loadingInstance.close();
|
||||
}
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useValueGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useValueGridColumns(),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getPropertyValuePage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
propertyId: props.propertyId,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: { code: 'query' },
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<MallPropertyApi.PropertyValue>,
|
||||
});
|
||||
|
||||
/** 监听 dictType 变化,重新查询 */
|
||||
watch(
|
||||
() => props.propertyId,
|
||||
() => {
|
||||
if (props.propertyId) {
|
||||
onRefresh();
|
||||
}
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex h-full flex-col">
|
||||
<ValueFormModal @success="onRefresh" />
|
||||
|
||||
<Grid table-title="属性值列表">
|
||||
<template #toolbar-tools>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('ui.actionTitle.create', ['属性值']),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.ADD,
|
||||
auth: ['product:property:create'],
|
||||
onClick: handleCreate,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('common.edit'),
|
||||
type: 'primary',
|
||||
link: true,
|
||||
icon: ACTION_ICON.EDIT,
|
||||
auth: ['product:property:update'],
|
||||
onClick: handleEdit.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: $t('common.delete'),
|
||||
type: 'danger',
|
||||
link: true,
|
||||
icon: ACTION_ICON.DELETE,
|
||||
auth: ['product:property:delete'],
|
||||
popConfirm: {
|
||||
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
|
||||
confirm: handleDelete.bind(null, row),
|
||||
},
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,117 @@
|
|||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { MallSpuApi } from '#/api/mall/product/spu';
|
||||
|
||||
import { handleTree } from '@vben/utils';
|
||||
|
||||
import { getCategoryList } from '#/api/mall/product/category';
|
||||
import { getRangePickerDefaultProps } from '#/utils';
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '商品名称',
|
||||
component: 'Input',
|
||||
},
|
||||
{
|
||||
fieldName: 'categoryId',
|
||||
label: '商品分类',
|
||||
component: 'ApiTreeSelect',
|
||||
componentProps: {
|
||||
api: async () => {
|
||||
const res = await getCategoryList({});
|
||||
return handleTree(res, 'id', 'parentId', 'children');
|
||||
},
|
||||
fieldNames: { label: 'name', value: 'id', children: 'children' },
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'createTime',
|
||||
label: '创建时间',
|
||||
component: 'RangePicker',
|
||||
componentProps: {
|
||||
...getRangePickerDefaultProps(),
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns<T = MallSpuApi.Spu>(
|
||||
onStatusChange?: (
|
||||
newStatus: number,
|
||||
row: T,
|
||||
) => PromiseLike<boolean | undefined>,
|
||||
): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
type: 'expand',
|
||||
width: 80,
|
||||
slots: { content: 'expand_content' },
|
||||
fixed: 'left',
|
||||
},
|
||||
{
|
||||
field: 'id',
|
||||
title: '商品编号',
|
||||
fixed: 'left',
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
title: '商品名称',
|
||||
fixed: 'left',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'picUrl',
|
||||
title: '商品图片',
|
||||
cellRender: {
|
||||
name: 'CellImage',
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'price',
|
||||
title: '价格',
|
||||
formatter: 'formatAmount2',
|
||||
},
|
||||
{
|
||||
field: 'salesCount',
|
||||
title: '销量',
|
||||
},
|
||||
{
|
||||
field: 'stock',
|
||||
title: '库存',
|
||||
},
|
||||
{
|
||||
field: 'sort',
|
||||
title: '排序',
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
title: '销售状态',
|
||||
cellRender: {
|
||||
attrs: { beforeChange: onStatusChange },
|
||||
name: 'CellSwitch',
|
||||
props: {
|
||||
checkedValue: 1,
|
||||
checkedChildren: '上架',
|
||||
unCheckedValue: 0,
|
||||
unCheckedChildren: '下架',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '创建时间',
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 300,
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -0,0 +1,349 @@
|
|||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { MallSpuApi } from '#/api/mall/product/spu';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { confirm, DocAlert, Page } from '@vben/common-ui';
|
||||
import {
|
||||
downloadFileFromBlobPart,
|
||||
fenToYuan,
|
||||
handleTree,
|
||||
treeToString,
|
||||
} from '@vben/utils';
|
||||
|
||||
import { ElDescriptions, ElLoading, ElMessage, ElTabs } from 'element-plus';
|
||||
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getCategoryList } from '#/api/mall/product/category';
|
||||
import {
|
||||
deleteSpu,
|
||||
exportSpu,
|
||||
getSpuPage,
|
||||
getTabsCount,
|
||||
updateStatus,
|
||||
} from '#/api/mall/product/spu';
|
||||
import { $t } from '#/locales';
|
||||
import { ProductSpuStatusEnum } from '#/utils';
|
||||
|
||||
import { useGridColumns, useGridFormSchema } from './data';
|
||||
|
||||
const { push } = useRouter();
|
||||
const tabType = ref(0);
|
||||
|
||||
const categoryList = ref();
|
||||
|
||||
// tabs 数据
|
||||
const tabsData = ref([
|
||||
{
|
||||
name: '出售中',
|
||||
type: 0,
|
||||
count: 0,
|
||||
},
|
||||
{
|
||||
name: '仓库中',
|
||||
type: 1,
|
||||
count: 0,
|
||||
},
|
||||
{
|
||||
name: '已售罄',
|
||||
type: 2,
|
||||
count: 0,
|
||||
},
|
||||
{
|
||||
name: '警戒库存',
|
||||
type: 3,
|
||||
count: 0,
|
||||
},
|
||||
{
|
||||
name: '回收站',
|
||||
type: 4,
|
||||
count: 0,
|
||||
},
|
||||
]);
|
||||
|
||||
/** 刷新表格 */
|
||||
function onRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 获得每个 Tab 的数量 */
|
||||
async function getTabCount() {
|
||||
const res = await getTabsCount();
|
||||
for (const objName in res) {
|
||||
const index = Number(objName);
|
||||
if (tabsData.value[index]) {
|
||||
tabsData.value[index].count = res[objName] as number;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 创建商品 */
|
||||
function handleCreate() {
|
||||
push({ name: 'ProductSpuAdd' });
|
||||
}
|
||||
|
||||
/** 导出表格 */
|
||||
async function handleExport() {
|
||||
const data = await exportSpu(await gridApi.formApi.getValues());
|
||||
downloadFileFromBlobPart({ fileName: '商品.xls', source: data });
|
||||
}
|
||||
|
||||
/** 编辑商品 */
|
||||
function handleEdit(row: MallSpuApi.Spu) {
|
||||
push({ name: 'ProductSpuEdit', params: { id: row.id } });
|
||||
}
|
||||
|
||||
/** 删除商品 */
|
||||
async function handleDelete(row: MallSpuApi.Spu) {
|
||||
const hideLoading = ElLoading.service({
|
||||
text: $t('ui.actionMessage.deleting', [row.name]),
|
||||
fullscreen: true,
|
||||
});
|
||||
try {
|
||||
await deleteSpu(row.id as number);
|
||||
ElMessage.success($t('ui.actionMessage.deleteSuccess', [row.name]));
|
||||
onRefresh();
|
||||
} finally {
|
||||
hideLoading.close();
|
||||
}
|
||||
}
|
||||
|
||||
/** 添加到仓库 / 回收站的状态 */
|
||||
async function handleStatus02Change(row: MallSpuApi.Spu, newStatus: number) {
|
||||
// 二次确认
|
||||
const text =
|
||||
newStatus === ProductSpuStatusEnum.RECYCLE.status
|
||||
? '加入到回收站'
|
||||
: '恢复到仓库';
|
||||
confirm(`确认要"${row.name}"${text}吗?`)
|
||||
.then(async () => {
|
||||
await updateStatus({ id: row.id as number, status: newStatus });
|
||||
ElMessage.success(`${text}成功`);
|
||||
onRefresh();
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessage.error(`${text}失败`);
|
||||
});
|
||||
}
|
||||
|
||||
/** 更新状态 */
|
||||
async function handleStatusChange(
|
||||
newStatus: number,
|
||||
row: MallSpuApi.Spu,
|
||||
): Promise<boolean | undefined> {
|
||||
return new Promise((resolve, reject) => {
|
||||
// 二次确认
|
||||
const text = row.status ? '上架' : '下架';
|
||||
confirm({
|
||||
content: `确认要${text + row.name}吗?`,
|
||||
})
|
||||
.then(async () => {
|
||||
// 更新状态
|
||||
const res = await updateStatus({
|
||||
id: row.id as number,
|
||||
status: newStatus,
|
||||
});
|
||||
if (res) {
|
||||
// 提示并返回成功
|
||||
ElMessage.success(`${text}成功`);
|
||||
resolve(true);
|
||||
} else {
|
||||
reject(new Error('操作失败'));
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
reject(new Error('取消操作'));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/** 查看商品详情 */
|
||||
function handleDetail(row: MallSpuApi.Spu) {
|
||||
push({ name: 'ProductSpuDetail', params: { id: row.id } });
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(handleStatusChange),
|
||||
height: 'auto',
|
||||
cellConfig: {
|
||||
height: 80,
|
||||
},
|
||||
expandConfig: {
|
||||
height: 100,
|
||||
},
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getSpuPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
tabType: tabType.value,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
resizable: true,
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: { code: 'query' },
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<MallSpuApi.Spu>,
|
||||
});
|
||||
|
||||
function onChangeTab(key: any) {
|
||||
tabType.value = Number(key);
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getTabCount();
|
||||
getCategoryList({}).then((res) => {
|
||||
categoryList.value = handleTree(res, 'id', 'parentId', 'children');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<template #doc>
|
||||
<DocAlert
|
||||
title="【商品】商品 SPU 与 SKU"
|
||||
url="https://doc.iocoder.cn/mall/product-spu-sku/"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<Grid>
|
||||
<template #top>
|
||||
<ElTabs class="border-none" @change="onChangeTab">
|
||||
<ElTabs.TabPane
|
||||
v-for="item in tabsData"
|
||||
:key="item.type"
|
||||
:tab="`${item.name} (${item.count})`"
|
||||
/>
|
||||
</ElTabs>
|
||||
</template>
|
||||
<template #toolbar-tools>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('ui.actionTitle.create', ['商品']),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.ADD,
|
||||
auth: ['product:spu:create'],
|
||||
onClick: handleCreate,
|
||||
},
|
||||
{
|
||||
label: $t('ui.actionTitle.export'),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.DOWNLOAD,
|
||||
auth: ['product:spu:export'],
|
||||
onClick: handleExport,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<template #expand_content="{ row }">
|
||||
<ElDescriptions
|
||||
:column="4"
|
||||
class="mt-4"
|
||||
:label-style="{
|
||||
width: '100px',
|
||||
fontWeight: 'bold',
|
||||
fontSize: '14px',
|
||||
}"
|
||||
:content-style="{ width: '100px', fontSize: '14px' }"
|
||||
>
|
||||
<ElDescriptions.Item label="商品分类">
|
||||
{{ treeToString(categoryList, row.categoryId) }}
|
||||
</ElDescriptions.Item>
|
||||
<ElDescriptions.Item label="商品名称">
|
||||
{{ row.name }}
|
||||
</ElDescriptions.Item>
|
||||
|
||||
<ElDescriptions.Item label="市场价">
|
||||
{{ fenToYuan(row.marketPrice) }} 元
|
||||
</ElDescriptions.Item>
|
||||
<ElDescriptions.Item label="成本价">
|
||||
{{ fenToYuan(row.costPrice) }} 元
|
||||
</ElDescriptions.Item>
|
||||
<ElDescriptions.Item label="浏览量">
|
||||
{{ row.browseCount }}
|
||||
</ElDescriptions.Item>
|
||||
<ElDescriptions.Item label="虚拟销量">
|
||||
{{ row.virtualSalesCount }}
|
||||
</ElDescriptions.Item>
|
||||
</ElDescriptions>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('common.edit'),
|
||||
type: 'primary',
|
||||
link: true,
|
||||
icon: ACTION_ICON.EDIT,
|
||||
auth: ['product:spu:update'],
|
||||
onClick: handleEdit.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: $t('common.detail'),
|
||||
type: 'primary',
|
||||
link: true,
|
||||
icon: ACTION_ICON.VIEW,
|
||||
onClick: handleDetail.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: $t('common.delete'),
|
||||
type: 'danger',
|
||||
link: true,
|
||||
icon: ACTION_ICON.DELETE,
|
||||
auth: ['product:spu:delete'],
|
||||
ifShow: () => row.type === 4,
|
||||
popConfirm: {
|
||||
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
|
||||
confirm: handleDelete.bind(null, row),
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '恢复',
|
||||
type: 'primary',
|
||||
link: true,
|
||||
icon: ACTION_ICON.EDIT,
|
||||
auth: ['product:spu:update'],
|
||||
ifShow: () => row.type === 4,
|
||||
onClick: handleStatus02Change.bind(
|
||||
null,
|
||||
row,
|
||||
ProductSpuStatusEnum.DISABLE.status,
|
||||
),
|
||||
},
|
||||
{
|
||||
label: '回收',
|
||||
type: 'danger',
|
||||
link: true,
|
||||
icon: ACTION_ICON.EDIT,
|
||||
auth: ['product:spu:update'],
|
||||
ifShow: () => row.type !== 4,
|
||||
onClick: handleStatus02Change.bind(
|
||||
null,
|
||||
row,
|
||||
ProductSpuStatusEnum.RECYCLE.status,
|
||||
),
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
<script lang="ts" setup></script>
|
||||
|
||||
<template>detail</template>
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
<script lang="ts" setup></script>
|
||||
|
||||
<template>form</template>
|
||||
|
|
@ -0,0 +1,135 @@
|
|||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeGridPropTypes } from '#/adapter/vxe-table';
|
||||
|
||||
import { DICT_TYPE, getDictOptions, getRangePickerDefaultProps } from '#/utils';
|
||||
|
||||
/** 新增/修改的表单 */
|
||||
export function useFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'id',
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
show: () => false,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '分类名称',
|
||||
component: 'Input',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'picUrl',
|
||||
label: '图标地址',
|
||||
component: 'ImageUpload',
|
||||
},
|
||||
{
|
||||
fieldName: 'sort',
|
||||
label: '排序',
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
min: 0,
|
||||
controlsPosition: 'right',
|
||||
placeholder: '请输入排序',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'status',
|
||||
label: '状态',
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
|
||||
buttonStyle: 'solid',
|
||||
optionType: 'button',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '分类名称',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入分类名称',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'status',
|
||||
label: '状态',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
placeholder: '请选择状态',
|
||||
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'createTime',
|
||||
label: '创建时间',
|
||||
component: 'RangePicker',
|
||||
componentProps: {
|
||||
...getRangePickerDefaultProps(),
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 表格列配置 */
|
||||
export function useGridColumns(): VxeGridPropTypes.Columns {
|
||||
return [
|
||||
{
|
||||
title: '编号',
|
||||
field: 'id',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '分类名称',
|
||||
field: 'name',
|
||||
minWidth: 240,
|
||||
},
|
||||
{
|
||||
title: '分类图片',
|
||||
field: 'picUrl',
|
||||
width: 80,
|
||||
cellRender: {
|
||||
name: 'CellImage',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
field: 'status',
|
||||
width: 150,
|
||||
cellRender: {
|
||||
name: 'CellDictTag',
|
||||
props: {
|
||||
dictType: DICT_TYPE.COMMON_STATUS,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '排序',
|
||||
field: 'sort',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
field: 'createTime',
|
||||
width: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 180,
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -0,0 +1,128 @@
|
|||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { MallArticleCategoryApi } from '#/api/mall/promotion/articleCategory';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { ElLoading, ElMessage } from 'element-plus';
|
||||
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
deleteArticleCategory,
|
||||
getArticleCategoryPage,
|
||||
} from '#/api/mall/promotion/articleCategory';
|
||||
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: MallArticleCategoryApi.ArticleCategory) {
|
||||
formModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 删除分类 */
|
||||
async function handleDelete(row: MallArticleCategoryApi.ArticleCategory) {
|
||||
const loadingInstance = ElLoading.service({
|
||||
text: $t('ui.actionMessage.deleting', [row.name]),
|
||||
fullscreen: true,
|
||||
});
|
||||
try {
|
||||
await deleteArticleCategory(row.id as number);
|
||||
ElMessage.success($t('ui.actionMessage.deleteSuccess', [row.name]));
|
||||
onRefresh();
|
||||
} finally {
|
||||
loadingInstance.close();
|
||||
}
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getArticleCategoryPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: { code: 'query' },
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<MallArticleCategoryApi.ArticleCategory>,
|
||||
});
|
||||
</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: ['promotion:article-category:create'],
|
||||
onClick: handleCreate,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('common.edit'),
|
||||
type: 'primary',
|
||||
link: true,
|
||||
icon: ACTION_ICON.EDIT,
|
||||
auth: ['promotion:article-category:update'],
|
||||
onClick: handleEdit.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: $t('common.delete'),
|
||||
type: 'danger',
|
||||
link: true,
|
||||
icon: ACTION_ICON.DELETE,
|
||||
auth: ['promotion:article-category:delete'],
|
||||
popConfirm: {
|
||||
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
|
||||
confirm: handleDelete.bind(null, row),
|
||||
},
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
<script lang="ts" setup>
|
||||
import type { MallArticleCategoryApi } from '#/api/mall/promotion/articleCategory';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import {
|
||||
createArticleCategory,
|
||||
getArticleCategory,
|
||||
updateArticleCategory,
|
||||
} from '#/api/mall/promotion/articleCategory';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useFormSchema } from '../data';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
|
||||
const formData = ref<MallArticleCategoryApi.ArticleCategory>();
|
||||
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 MallArticleCategoryApi.ArticleCategory;
|
||||
try {
|
||||
await (formData.value?.id
|
||||
? updateArticleCategory(data)
|
||||
: createArticleCategory(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<MallArticleCategoryApi.ArticleCategory>();
|
||||
if (!data || !data.id) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
try {
|
||||
formData.value = await getArticleCategory(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,210 @@
|
|||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeGridPropTypes } from '#/adapter/vxe-table';
|
||||
|
||||
import { z } from '#/adapter/form';
|
||||
import { getSimpleArticleCategoryList } from '#/api/mall/promotion/articleCategory';
|
||||
import {
|
||||
CommonStatusEnum,
|
||||
DICT_TYPE,
|
||||
getDictOptions,
|
||||
getRangePickerDefaultProps,
|
||||
} from '#/utils';
|
||||
|
||||
/** 新增/修改的表单 */
|
||||
export function useFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'id',
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
show: () => false,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'title',
|
||||
label: '文章标题',
|
||||
component: 'Input',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'categoryId',
|
||||
label: '文章分类',
|
||||
component: 'ApiSelect',
|
||||
componentProps: {
|
||||
api: getSimpleArticleCategoryList,
|
||||
labelField: 'name',
|
||||
valueField: 'id',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'author',
|
||||
label: '文章作者',
|
||||
component: 'Input',
|
||||
},
|
||||
{
|
||||
fieldName: 'introduction',
|
||||
label: '文章简介',
|
||||
component: 'Input',
|
||||
},
|
||||
{
|
||||
fieldName: 'picUrl',
|
||||
label: '文章封面',
|
||||
component: 'ImageUpload',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'recommendHot',
|
||||
label: '是否热门',
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean'),
|
||||
buttonStyle: 'solid',
|
||||
optionType: 'button',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'recommendBanner',
|
||||
label: '是否轮播图',
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean'),
|
||||
buttonStyle: 'solid',
|
||||
optionType: 'button',
|
||||
},
|
||||
},
|
||||
{
|
||||
// TODO: 商品关联
|
||||
fieldName: 'spuId',
|
||||
label: '商品关联',
|
||||
component: 'Input',
|
||||
},
|
||||
{
|
||||
fieldName: 'sort',
|
||||
label: '排序',
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
min: 0,
|
||||
controlsPosition: 'right',
|
||||
placeholder: '请输入品牌排序',
|
||||
},
|
||||
rules: z.number().min(0).default(1),
|
||||
},
|
||||
{
|
||||
fieldName: 'status',
|
||||
label: '状态',
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
|
||||
buttonStyle: 'solid',
|
||||
optionType: 'button',
|
||||
},
|
||||
rules: z.number().default(CommonStatusEnum.ENABLE),
|
||||
},
|
||||
{
|
||||
fieldName: 'description',
|
||||
label: '文章内容',
|
||||
component: 'RichTextarea',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '文章分类',
|
||||
component: 'ApiSelect',
|
||||
componentProps: {
|
||||
api: getSimpleArticleCategoryList,
|
||||
labelField: 'name',
|
||||
valueField: 'id',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'title',
|
||||
label: '文章标题',
|
||||
component: 'Input',
|
||||
},
|
||||
{
|
||||
fieldName: 'status',
|
||||
label: '状态',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'createTime',
|
||||
label: '创建时间',
|
||||
component: 'RangePicker',
|
||||
componentProps: {
|
||||
...getRangePickerDefaultProps(),
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 表格列配置 */
|
||||
export function useGridColumns(): VxeGridPropTypes.Columns {
|
||||
return [
|
||||
{
|
||||
field: 'id',
|
||||
title: '编号',
|
||||
fixed: 'left',
|
||||
},
|
||||
{
|
||||
field: 'title',
|
||||
title: '标题',
|
||||
},
|
||||
{
|
||||
field: 'picUrl',
|
||||
title: '封面',
|
||||
cellRender: {
|
||||
name: 'CellImage',
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'categoryId',
|
||||
title: '分类',
|
||||
},
|
||||
{
|
||||
field: 'browseCount',
|
||||
title: '浏览量',
|
||||
},
|
||||
{
|
||||
field: 'author',
|
||||
title: '作者',
|
||||
},
|
||||
{
|
||||
field: 'introduction',
|
||||
title: '文章简介',
|
||||
},
|
||||
{
|
||||
field: 'sort',
|
||||
title: '排序',
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
title: '状态',
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.COMMON_STATUS },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '创建时间',
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 180,
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -0,0 +1,125 @@
|
|||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { MallArticleApi } from '#/api/mall/promotion/article';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { ElLoading, ElMessage } from 'element-plus';
|
||||
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { deleteArticle, getArticlePage } from '#/api/mall/promotion/article';
|
||||
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: MallArticleApi.Article) {
|
||||
formModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 删除品牌 */
|
||||
async function handleDelete(row: MallArticleApi.Article) {
|
||||
const loadingInstance = ElLoading.service({
|
||||
text: $t('ui.actionMessage.deleting', [row.title]),
|
||||
fullscreen: true,
|
||||
});
|
||||
try {
|
||||
await deleteArticle(row.id as number);
|
||||
ElMessage.success($t('ui.actionMessage.deleteSuccess', [row.title]));
|
||||
onRefresh();
|
||||
} finally {
|
||||
loadingInstance.close();
|
||||
}
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getArticlePage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: { code: 'query' },
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<MallArticleApi.Article>,
|
||||
});
|
||||
</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: ['promotion:article:create'],
|
||||
onClick: handleCreate,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('common.edit'),
|
||||
type: 'primary',
|
||||
link: true,
|
||||
icon: ACTION_ICON.EDIT,
|
||||
auth: ['promotion:article:update'],
|
||||
onClick: handleEdit.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: $t('common.delete'),
|
||||
type: 'danger',
|
||||
link: true,
|
||||
icon: ACTION_ICON.DELETE,
|
||||
auth: ['promotion:article:delete'],
|
||||
popConfirm: {
|
||||
title: $t('ui.actionMessage.deleteConfirm', [row.title]),
|
||||
confirm: handleDelete.bind(null, row),
|
||||
},
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
<script lang="ts" setup>
|
||||
import type { MallArticleApi } from '#/api/mall/promotion/article';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import {
|
||||
createArticle,
|
||||
getArticle,
|
||||
updateArticle,
|
||||
} from '#/api/mall/promotion/article';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useFormSchema } from '../data';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
|
||||
const formData = ref<MallArticleApi.Article>();
|
||||
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 MallArticleApi.Article;
|
||||
try {
|
||||
await (formData.value?.id ? updateArticle(data) : createArticle(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<MallArticleApi.Article>();
|
||||
if (!data || !data.id) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
try {
|
||||
formData.value = await getArticle(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,175 @@
|
|||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeGridPropTypes } from '#/adapter/vxe-table';
|
||||
|
||||
import { DICT_TYPE, getDictOptions, getRangePickerDefaultProps } from '#/utils';
|
||||
|
||||
/** 新增/修改的表单 */
|
||||
export function useFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'id',
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
show: () => false,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'title',
|
||||
label: 'Banner标题',
|
||||
component: 'Input',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'picUrl',
|
||||
label: '图片地址',
|
||||
component: 'ImageUpload',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'position',
|
||||
label: '定位',
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.PROMOTION_BANNER_POSITION, 'number'),
|
||||
buttonStyle: 'solid',
|
||||
optionType: 'button',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'url',
|
||||
label: '跳转地址',
|
||||
component: 'Input',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'sort',
|
||||
label: '排序',
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
min: 0,
|
||||
controlsPosition: 'right',
|
||||
placeholder: '请输入排序',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'status',
|
||||
label: '状态',
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
|
||||
buttonStyle: 'solid',
|
||||
optionType: 'button',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'memo',
|
||||
label: '描述',
|
||||
component: 'Textarea',
|
||||
componentProps: {
|
||||
rows: 4,
|
||||
placeholder: '请输入描述',
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'title',
|
||||
label: 'Banner标题',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入Banner标题',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'status',
|
||||
label: '状态',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
placeholder: '请选择状态',
|
||||
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'createTime',
|
||||
label: '创建时间',
|
||||
component: 'RangePicker',
|
||||
componentProps: {
|
||||
...getRangePickerDefaultProps(),
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 表格列配置 */
|
||||
export function useGridColumns(): VxeGridPropTypes.Columns {
|
||||
return [
|
||||
{
|
||||
title: 'Banner标题',
|
||||
field: 'title',
|
||||
},
|
||||
{
|
||||
title: '图片',
|
||||
field: 'picUrl',
|
||||
width: 80,
|
||||
cellRender: {
|
||||
name: 'CellImage',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
field: 'status',
|
||||
width: 150,
|
||||
cellRender: {
|
||||
name: 'CellDictTag',
|
||||
props: {
|
||||
dictType: DICT_TYPE.COMMON_STATUS,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '定位',
|
||||
field: 'position',
|
||||
width: 150,
|
||||
cellRender: {
|
||||
name: 'CellDictTag',
|
||||
props: {
|
||||
dictType: DICT_TYPE.PROMOTION_BANNER_POSITION,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '跳转地址',
|
||||
field: 'url',
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
field: 'createTime',
|
||||
width: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
title: '排序',
|
||||
field: 'sort',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '描述',
|
||||
field: 'memo',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 180,
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -0,0 +1,125 @@
|
|||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { MallBannerApi } from '#/api/mall/market/banner';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { ElLoading, ElMessage } from 'element-plus';
|
||||
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { deleteBanner, getBannerPage } from '#/api/mall/market/banner';
|
||||
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();
|
||||
}
|
||||
|
||||
/** 创建Banner */
|
||||
function handleCreate() {
|
||||
formModalApi.setData(null).open();
|
||||
}
|
||||
|
||||
/** 编辑Banner */
|
||||
function handleEdit(row: MallBannerApi.Banner) {
|
||||
formModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 删除Banner */
|
||||
async function handleDelete(row: MallBannerApi.Banner) {
|
||||
const loadingInstance = ElLoading.service({
|
||||
text: $t('ui.actionMessage.deleting', [row.title]),
|
||||
fullscreen: true,
|
||||
});
|
||||
try {
|
||||
await deleteBanner(row.id as number);
|
||||
ElMessage.success($t('ui.actionMessage.deleteSuccess', [row.title]));
|
||||
onRefresh();
|
||||
} finally {
|
||||
loadingInstance.close();
|
||||
}
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getBannerPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: { code: 'query' },
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<MallBannerApi.Banner>,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<FormModal @success="onRefresh" />
|
||||
<Grid table-title="Banner列表">
|
||||
<template #toolbar-tools>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('ui.actionTitle.create', ['Banner']),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.ADD,
|
||||
auth: ['promotion:banner:create'],
|
||||
onClick: handleCreate,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('common.edit'),
|
||||
type: 'primary',
|
||||
link: true,
|
||||
icon: ACTION_ICON.EDIT,
|
||||
auth: ['promotion:banner:update'],
|
||||
onClick: handleEdit.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: $t('common.delete'),
|
||||
type: 'danger',
|
||||
link: true,
|
||||
icon: ACTION_ICON.DELETE,
|
||||
auth: ['promotion:banner:delete'],
|
||||
popConfirm: {
|
||||
title: $t('ui.actionMessage.deleteConfirm', [row.title]),
|
||||
confirm: handleDelete.bind(null, row),
|
||||
},
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
<script lang="ts" setup>
|
||||
import type { MallBannerApi } from '#/api/mall/market/banner';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import {
|
||||
createBanner,
|
||||
getBanner,
|
||||
updateBanner,
|
||||
} from '#/api/mall/market/banner';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useFormSchema } from '../data';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
|
||||
const formData = ref<MallBannerApi.Banner>();
|
||||
const getTitle = computed(() => {
|
||||
return formData.value?.id
|
||||
? $t('ui.actionTitle.edit', ['Banner'])
|
||||
: $t('ui.actionTitle.create', ['Banner']);
|
||||
});
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
formItemClass: 'col-span-2',
|
||||
labelWidth: 100,
|
||||
},
|
||||
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 MallBannerApi.Banner;
|
||||
try {
|
||||
await (formData.value?.id ? updateBanner(data) : createBanner(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<MallBannerApi.Banner>();
|
||||
if (!data || !data.id) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
try {
|
||||
formData.value = await getBanner(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,262 @@
|
|||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
import { formatDate } from '@vben/utils';
|
||||
|
||||
import { DICT_TYPE, getDictOptions } from '#/utils';
|
||||
|
||||
/** 新增/修改的表单 */
|
||||
export function useFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'id',
|
||||
component: 'Input',
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
show: () => false,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '活动名称',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入活动名称',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'startTime',
|
||||
label: '开始时间',
|
||||
component: 'DatePicker',
|
||||
componentProps: {
|
||||
format: 'YYYY-MM-DD HH:mm:ss',
|
||||
valueFormat: 'YYYY-MM-DD HH:mm:ss',
|
||||
placeholder: '请选择开始时间',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'endTime',
|
||||
label: '结束时间',
|
||||
component: 'DatePicker',
|
||||
componentProps: {
|
||||
format: 'YYYY-MM-DD HH:mm:ss',
|
||||
valueFormat: 'YYYY-MM-DD HH:mm:ss',
|
||||
placeholder: '请选择结束时间',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'bargainFirstPrice',
|
||||
label: '砍价起始价格(元)',
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
min: 0,
|
||||
precision: 2,
|
||||
step: 0.01,
|
||||
placeholder: '请输入砍价起始价格',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'bargainMinPrice',
|
||||
label: '砍价底价(元)',
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
min: 0,
|
||||
precision: 2,
|
||||
step: 0.01,
|
||||
placeholder: '请输入砍价底价',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'stock',
|
||||
label: '活动库存',
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
min: 1,
|
||||
placeholder: '请输入活动库存',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'helpMaxCount',
|
||||
label: '助力人数',
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
min: 1,
|
||||
placeholder: '请输入助力人数',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'bargainCount',
|
||||
label: '砍价次数',
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
min: 1,
|
||||
placeholder: '请输入砍价次数',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'totalLimitCount',
|
||||
label: '购买限制',
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
min: 1,
|
||||
placeholder: '请输入购买限制',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'randomMinPrice',
|
||||
label: '最小砍价金额(元)',
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
min: 0,
|
||||
precision: 2,
|
||||
step: 0.01,
|
||||
placeholder: '请输入最小砍价金额',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'randomMaxPrice',
|
||||
label: '最大砍价金额(元)',
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
min: 0,
|
||||
precision: 2,
|
||||
step: 0.01,
|
||||
placeholder: '请输入最大砍价金额',
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '活动名称',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入活动名称',
|
||||
clearable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'status',
|
||||
label: '活动状态',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
placeholder: '请选择活动状态',
|
||||
clearable: true,
|
||||
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'id',
|
||||
title: '活动编号',
|
||||
minWidth: 80,
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
title: '活动名称',
|
||||
minWidth: 140,
|
||||
},
|
||||
{
|
||||
field: 'activityTime',
|
||||
title: '活动时间',
|
||||
minWidth: 210,
|
||||
formatter: ({ row }) => {
|
||||
if (!row.startTime || !row.endTime) return '';
|
||||
return `${formatDate(row.startTime, 'YYYY-MM-DD')} ~ ${formatDate(row.endTime, 'YYYY-MM-DD')}`;
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'picUrl',
|
||||
title: '商品图片',
|
||||
minWidth: 80,
|
||||
cellRender: {
|
||||
name: 'CellImage',
|
||||
props: {
|
||||
height: 40,
|
||||
width: 40,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'spuName',
|
||||
title: '商品标题',
|
||||
minWidth: 300,
|
||||
},
|
||||
{
|
||||
field: 'bargainFirstPrice',
|
||||
title: '起始价格',
|
||||
minWidth: 100,
|
||||
formatter: 'formatAmount2',
|
||||
},
|
||||
{
|
||||
field: 'bargainMinPrice',
|
||||
title: '砍价底价',
|
||||
minWidth: 100,
|
||||
formatter: 'formatAmount2',
|
||||
},
|
||||
{
|
||||
field: 'recordUserCount',
|
||||
title: '总砍价人数',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
field: 'recordSuccessUserCount',
|
||||
title: '成功砍价人数',
|
||||
minWidth: 110,
|
||||
},
|
||||
{
|
||||
field: 'helpUserCount',
|
||||
title: '助力人数',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
title: '活动状态',
|
||||
minWidth: 100,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.COMMON_STATUS },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'stock',
|
||||
title: '库存',
|
||||
minWidth: 80,
|
||||
},
|
||||
{
|
||||
field: 'totalStock',
|
||||
title: '总库存',
|
||||
minWidth: 80,
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '创建时间',
|
||||
width: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 150,
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -0,0 +1,173 @@
|
|||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { MallBargainActivityApi } from '#/api/mall/promotion/bargain/bargainActivity';
|
||||
|
||||
import { confirm, DocAlert, Page, useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { ElLoading, ElMessage } from 'element-plus';
|
||||
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
closeBargainActivity,
|
||||
deleteBargainActivity,
|
||||
getBargainActivityPage,
|
||||
} from '#/api/mall/promotion/bargain/bargainActivity';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useGridColumns, useGridFormSchema } from './data';
|
||||
import Form from './modules/form.vue';
|
||||
|
||||
defineOptions({ name: 'PromotionBargainActivity' });
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: Form,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 刷新表格 */
|
||||
function onRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 创建砍价活动 */
|
||||
function handleCreate() {
|
||||
formModalApi.setData(null).open();
|
||||
}
|
||||
|
||||
/** 编辑砍价活动 */
|
||||
function handleEdit(row: MallBargainActivityApi.BargainActivity) {
|
||||
formModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 关闭砍价活动 */
|
||||
async function handleClose(row: MallBargainActivityApi.BargainActivity) {
|
||||
try {
|
||||
await confirm({
|
||||
content: '确认关闭该砍价活动吗?',
|
||||
});
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
const loadingInstance = ElLoading.service({
|
||||
text: '确认关闭该砍价活动吗?',
|
||||
fullscreen: true,
|
||||
});
|
||||
try {
|
||||
await closeBargainActivity(row.id as number);
|
||||
ElMessage.success('关闭成功');
|
||||
onRefresh();
|
||||
} finally {
|
||||
loadingInstance.close();
|
||||
}
|
||||
}
|
||||
|
||||
/** 删除砍价活动 */
|
||||
async function handleDelete(row: MallBargainActivityApi.BargainActivity) {
|
||||
const loadingInstance = ElLoading.service({
|
||||
text: $t('ui.actionMessage.deleting', [row.name]),
|
||||
fullscreen: true,
|
||||
});
|
||||
try {
|
||||
await deleteBargainActivity(row.id as number);
|
||||
ElMessage.success($t('ui.actionMessage.deleteSuccess', [row.name]));
|
||||
onRefresh();
|
||||
} finally {
|
||||
loadingInstance.close();
|
||||
}
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getBargainActivityPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: { code: 'query' },
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<MallBargainActivityApi.BargainActivity>,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<template #doc>
|
||||
<DocAlert
|
||||
title="【营销】砍价活动"
|
||||
url="https://doc.iocoder.cn/mall/promotion-bargain/"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<FormModal @success="onRefresh" />
|
||||
|
||||
<Grid table-title="砍价活动列表">
|
||||
<template #toolbar-tools>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('ui.actionTitle.create', ['砍价活动']),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.ADD,
|
||||
auth: ['promotion:bargain-activity:create'],
|
||||
onClick: handleCreate,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('common.edit'),
|
||||
type: 'primary',
|
||||
link: true,
|
||||
icon: ACTION_ICON.EDIT,
|
||||
auth: ['promotion:bargain-activity:update'],
|
||||
onClick: handleEdit.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: '关闭',
|
||||
type: 'danger',
|
||||
link: true,
|
||||
icon: ACTION_ICON.DELETE,
|
||||
auth: ['promotion:bargain-activity:close'],
|
||||
ifShow: row.status === 0,
|
||||
onClick: handleClose.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: $t('common.delete'),
|
||||
type: 'danger',
|
||||
link: true,
|
||||
icon: ACTION_ICON.DELETE,
|
||||
auth: ['promotion:bargain-activity:delete'],
|
||||
ifShow: row.status !== 0,
|
||||
popConfirm: {
|
||||
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
|
||||
confirm: handleDelete.bind(null, row),
|
||||
},
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
<script lang="ts" setup>
|
||||
import type { MallBargainActivityApi } from '#/api/mall/promotion/bargain/bargainActivity';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import {
|
||||
createBargainActivity,
|
||||
getBargainActivity,
|
||||
updateBargainActivity,
|
||||
} from '#/api/mall/promotion/bargain/bargainActivity';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useFormSchema } from '../data';
|
||||
|
||||
defineOptions({ name: 'PromotionBargainActivityForm' });
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
|
||||
const formData = ref<MallBargainActivityApi.BargainActivity>();
|
||||
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 MallBargainActivityApi.BargainActivity;
|
||||
try {
|
||||
await (formData.value?.id
|
||||
? updateBargainActivity(data)
|
||||
: createBargainActivity(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<MallBargainActivityApi.BargainActivity>();
|
||||
if (!data || !data.id) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
try {
|
||||
formData.value = await getBargainActivity(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,161 @@
|
|||
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: 'status',
|
||||
label: '砍价状态',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
placeholder: '请选择砍价状态',
|
||||
clearable: true,
|
||||
options: getDictOptions(
|
||||
DICT_TYPE.PROMOTION_BARGAIN_RECORD_STATUS,
|
||||
'number',
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'createTime',
|
||||
label: '创建时间',
|
||||
component: 'RangePicker',
|
||||
componentProps: {
|
||||
...getRangePickerDefaultProps(),
|
||||
clearable: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'id',
|
||||
title: '编号',
|
||||
minWidth: 50,
|
||||
},
|
||||
{
|
||||
field: 'avatar',
|
||||
title: '用户头像',
|
||||
minWidth: 120,
|
||||
cellRender: {
|
||||
name: 'CellImage',
|
||||
props: {
|
||||
height: 40,
|
||||
width: 40,
|
||||
shape: 'circle',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'nickname',
|
||||
title: '用户昵称',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '发起时间',
|
||||
width: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'activity.name',
|
||||
title: '砍价活动',
|
||||
minWidth: 150,
|
||||
},
|
||||
{
|
||||
field: 'activity.bargainMinPrice',
|
||||
title: '最低价',
|
||||
minWidth: 100,
|
||||
formatter: 'formatAmount2',
|
||||
},
|
||||
{
|
||||
field: 'bargainPrice',
|
||||
title: '当前价',
|
||||
minWidth: 100,
|
||||
formatter: 'formatAmount2',
|
||||
},
|
||||
{
|
||||
field: 'activity.helpMaxCount',
|
||||
title: '总砍价次数',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
field: 'helpCount',
|
||||
title: '剩余砍价次数',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
title: '砍价状态',
|
||||
minWidth: 100,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.PROMOTION_BARGAIN_RECORD_STATUS },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'endTime',
|
||||
title: '结束时间',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'orderId',
|
||||
title: '订单编号',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 100,
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 助力列表表格列配置 */
|
||||
export function useHelpGridColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'userId',
|
||||
title: '用户编号',
|
||||
minWidth: 80,
|
||||
},
|
||||
{
|
||||
field: 'avatar',
|
||||
title: '用户头像',
|
||||
minWidth: 80,
|
||||
cellRender: {
|
||||
name: 'CellImage',
|
||||
props: {
|
||||
height: 40,
|
||||
width: 40,
|
||||
shape: 'circle',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'nickname',
|
||||
title: '用户昵称',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
field: 'reducePrice',
|
||||
title: '砍价金额',
|
||||
minWidth: 100,
|
||||
formatter: 'formatAmount2',
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '助力时间',
|
||||
width: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { MallBargainRecordApi } from '#/api/mall/promotion/bargain/bargainRecord';
|
||||
|
||||
import { DocAlert, Page, useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getBargainRecordPage } from '#/api/mall/promotion/bargain/bargainRecord';
|
||||
|
||||
import { useGridColumns, useGridFormSchema } from './data';
|
||||
import HelpListModal from './modules/list.vue';
|
||||
|
||||
defineOptions({ name: 'PromotionBargainRecord' });
|
||||
|
||||
const [HelpListModalApi, helpListModalApi] = useVbenModal({
|
||||
connectedComponent: HelpListModal,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 查看助力详情 */
|
||||
function handleViewHelp(row: MallBargainRecordApi.BargainRecord) {
|
||||
helpListModalApi.setData({ recordId: row.id }).open();
|
||||
}
|
||||
|
||||
const [Grid] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getBargainRecordPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: { code: 'query' },
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<MallBargainRecordApi.BargainRecord>,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<template #doc>
|
||||
<DocAlert
|
||||
title="【营销】砍价活动"
|
||||
url="https://doc.iocoder.cn/mall/promotion-bargain/"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<HelpListModalApi />
|
||||
|
||||
<Grid table-title="砍价记录列表">
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '助力',
|
||||
type: 'primary',
|
||||
link: true,
|
||||
icon: ACTION_ICON.VIEW,
|
||||
auth: ['promotion:bargain-help:query'],
|
||||
onClick: handleViewHelp.bind(null, row),
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { MallBargainHelpApi } from '#/api/mall/promotion/bargain/bargainHelp';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getBargainHelpPage } from '#/api/mall/promotion/bargain/bargainHelp';
|
||||
|
||||
import { useHelpGridColumns } from '../data';
|
||||
|
||||
/** 助力列表 */
|
||||
defineOptions({ name: 'BargainRecordListDialog' });
|
||||
|
||||
const recordId = ref<number>();
|
||||
const getTitle = computed(() => {
|
||||
return `助力列表 - 记录${recordId.value || ''}`;
|
||||
});
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
recordId.value = undefined;
|
||||
return;
|
||||
}
|
||||
// 获取传入的记录ID
|
||||
const data = modalApi.getData<{ recordId: number }>();
|
||||
if (data?.recordId) {
|
||||
recordId.value = data.recordId;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const [Grid] = useVbenVxeGrid({
|
||||
gridOptions: {
|
||||
columns: useHelpGridColumns(),
|
||||
height: 600,
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }) => {
|
||||
return await getBargainHelpPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
recordId: recordId.value,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: { code: 'query' },
|
||||
},
|
||||
} as VxeTableGridOptions<MallBargainHelpApi.BargainHelp>,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal class="w-2/5" :title="getTitle">
|
||||
<Grid class="mx-4" />
|
||||
</Modal>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,238 @@
|
|||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
import { formatDate } from '@vben/utils';
|
||||
|
||||
import { DICT_TYPE, getDictOptions } from '#/utils';
|
||||
|
||||
/** 表单配置 */
|
||||
export function useFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'id',
|
||||
component: 'Input',
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
show: () => false,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '活动名称',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入活动名称',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'status',
|
||||
label: '活动状态',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
placeholder: '请选择活动状态',
|
||||
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'startTime',
|
||||
label: '开始时间',
|
||||
component: 'DatePicker',
|
||||
componentProps: {
|
||||
placeholder: '请选择开始时间',
|
||||
showTime: false,
|
||||
valueFormat: 'x',
|
||||
format: 'YYYY-MM-DD',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'endTime',
|
||||
label: '结束时间',
|
||||
component: 'DatePicker',
|
||||
componentProps: {
|
||||
placeholder: '请选择结束时间',
|
||||
showTime: false,
|
||||
valueFormat: 'x',
|
||||
format: 'YYYY-MM-DD',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'userSize',
|
||||
label: '用户数量',
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
placeholder: '请输入用户数量',
|
||||
min: 2,
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'limitDuration',
|
||||
label: '限制时长',
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
placeholder: '请输入限制时长(小时)',
|
||||
min: 0,
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'totalLimitCount',
|
||||
label: '总限购数量',
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
placeholder: '请输入总限购数量',
|
||||
min: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'singleLimitCount',
|
||||
label: '单次限购数量',
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
placeholder: '请输入单次限购数量',
|
||||
min: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'virtualGroup',
|
||||
label: '虚拟成团',
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean'),
|
||||
},
|
||||
},
|
||||
{
|
||||
// TODO
|
||||
fieldName: 'spuId',
|
||||
label: '拼团商品',
|
||||
component: 'Input',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '活动名称',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入活动名称',
|
||||
clearable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'status',
|
||||
label: '活动状态',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
placeholder: '请选择活动状态',
|
||||
clearable: true,
|
||||
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'id',
|
||||
title: '活动编号',
|
||||
minWidth: 80,
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
title: '活动名称',
|
||||
minWidth: 140,
|
||||
},
|
||||
{
|
||||
field: 'activityTime',
|
||||
title: '活动时间',
|
||||
minWidth: 210,
|
||||
formatter: ({ row }) => {
|
||||
if (!row.startTime || !row.endTime) return '';
|
||||
return `${formatDate(row.startTime, 'YYYY-MM-DD')} ~ ${formatDate(row.endTime, 'YYYY-MM-DD')}`;
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'picUrl',
|
||||
title: '商品图片',
|
||||
minWidth: 80,
|
||||
cellRender: {
|
||||
name: 'CellImage',
|
||||
props: {
|
||||
height: 40,
|
||||
width: 40,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'spuName',
|
||||
title: '商品标题',
|
||||
minWidth: 300,
|
||||
},
|
||||
{
|
||||
field: 'marketPrice',
|
||||
title: '原价',
|
||||
minWidth: 100,
|
||||
formatter: ({ cellValue }) => {
|
||||
return `¥${(cellValue / 100).toFixed(2)}`;
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'combinationPrice',
|
||||
title: '拼团价',
|
||||
minWidth: 100,
|
||||
formatter: ({ row }) => {
|
||||
if (!row.products || row.products.length === 0) return '';
|
||||
const combinationPrice = Math.min(
|
||||
...row.products.map((item: any) => item.combinationPrice),
|
||||
);
|
||||
return `¥${(combinationPrice / 100).toFixed(2)}`;
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'groupCount',
|
||||
title: '开团组数',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
field: 'groupSuccessCount',
|
||||
title: '成团组数',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
field: 'recordCount',
|
||||
title: '购买次数',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
title: '活动状态',
|
||||
minWidth: 100,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.COMMON_STATUS },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '创建时间',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 200,
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -0,0 +1,177 @@
|
|||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { MallCombinationActivityApi } from '#/api/mall/promotion/combination/combinationActivity';
|
||||
|
||||
import { confirm, DocAlert, Page, useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { ElLoading, ElMessage } from 'element-plus';
|
||||
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
closeCombinationActivity,
|
||||
deleteCombinationActivity,
|
||||
getCombinationActivityPage,
|
||||
} from '#/api/mall/promotion/combination/combinationActivity';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useGridColumns, useGridFormSchema } from './data';
|
||||
import CombinationActivityForm from './modules/form.vue';
|
||||
|
||||
defineOptions({ name: 'PromotionCombinationActivity' });
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: CombinationActivityForm,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 刷新表格 */
|
||||
function onRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 创建拼团活动 */
|
||||
function handleCreate() {
|
||||
formModalApi.setData(null).open();
|
||||
}
|
||||
|
||||
/** 编辑拼团活动 */
|
||||
function handleEdit(row: MallCombinationActivityApi.CombinationActivity) {
|
||||
formModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 关闭拼团活动 */
|
||||
async function handleClose(
|
||||
row: MallCombinationActivityApi.CombinationActivity,
|
||||
) {
|
||||
try {
|
||||
await confirm({
|
||||
content: '确认关闭该拼团活动吗?',
|
||||
});
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
const loadingInstance = ElLoading.service({
|
||||
text: '关闭中...',
|
||||
fullscreen: true,
|
||||
});
|
||||
try {
|
||||
await closeCombinationActivity(row.id as number);
|
||||
ElMessage.success('关闭成功');
|
||||
onRefresh();
|
||||
} finally {
|
||||
loadingInstance.close();
|
||||
}
|
||||
}
|
||||
|
||||
/** 删除拼团活动 */
|
||||
async function handleDelete(
|
||||
row: MallCombinationActivityApi.CombinationActivity,
|
||||
) {
|
||||
const loadingInstance = ElLoading.service({
|
||||
text: $t('ui.actionMessage.deleting', [row.name]),
|
||||
fullscreen: true,
|
||||
});
|
||||
try {
|
||||
await deleteCombinationActivity(row.id as number);
|
||||
ElMessage.success($t('ui.actionMessage.deleteSuccess', [row.name]));
|
||||
onRefresh();
|
||||
} finally {
|
||||
loadingInstance.close();
|
||||
}
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getCombinationActivityPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: { code: 'query' },
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<MallCombinationActivityApi.CombinationActivity>,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<template #doc>
|
||||
<DocAlert
|
||||
title="【营销】拼团活动"
|
||||
url="https://doc.iocoder.cn/mall/promotion-combination/"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<FormModal @success="onRefresh" />
|
||||
|
||||
<Grid table-title="拼团活动列表">
|
||||
<template #toolbar-tools>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('ui.actionTitle.create', ['拼团活动']),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.ADD,
|
||||
auth: ['promotion:combination-activity:create'],
|
||||
onClick: handleCreate,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('common.edit'),
|
||||
type: 'primary',
|
||||
link: true,
|
||||
icon: ACTION_ICON.EDIT,
|
||||
auth: ['promotion:combination-activity:update'],
|
||||
onClick: handleEdit.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: '关闭',
|
||||
type: 'danger',
|
||||
link: true,
|
||||
icon: ACTION_ICON.DELETE,
|
||||
auth: ['promotion:combination-activity:close'],
|
||||
ifShow: row.status === 0,
|
||||
onClick: handleClose.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: $t('common.delete'),
|
||||
type: 'danger',
|
||||
link: true,
|
||||
icon: ACTION_ICON.DELETE,
|
||||
auth: ['promotion:combination-activity:delete'],
|
||||
ifShow: row.status !== 0,
|
||||
popConfirm: {
|
||||
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
|
||||
confirm: handleDelete.bind(null, row),
|
||||
},
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
<script lang="ts" setup>
|
||||
import type { MallCombinationActivityApi } from '#/api/mall/promotion/combination/combinationActivity';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenForm, useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
import {
|
||||
createCombinationActivity,
|
||||
getCombinationActivity,
|
||||
updateCombinationActivity,
|
||||
} from '#/api/mall/promotion/combination/combinationActivity';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useFormSchema } from '../data';
|
||||
|
||||
defineOptions({ name: 'CombinationActivityForm' });
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const formData = ref<MallCombinationActivityApi.CombinationActivity>();
|
||||
const getTitle = computed(() => {
|
||||
return formData.value?.id
|
||||
? $t('ui.actionTitle.edit', ['拼团活动'])
|
||||
: $t('ui.actionTitle.create', ['拼团活动']);
|
||||
});
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
labelWidth: 100,
|
||||
},
|
||||
wrapperClass: 'grid-cols-2',
|
||||
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 MallCombinationActivityApi.CombinationActivity;
|
||||
try {
|
||||
await (formData.value?.id
|
||||
? updateCombinationActivity(data)
|
||||
: createCombinationActivity(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<MallCombinationActivityApi.CombinationActivity>();
|
||||
if (!data || !data.id) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
try {
|
||||
formData.value = await getCombinationActivity(data.id as number);
|
||||
// 设置到 values
|
||||
if (formData.value) {
|
||||
await formApi.setValues(formData.value);
|
||||
}
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal class="w-3/5" :title="getTitle">
|
||||
<Form />
|
||||
</Modal>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,177 @@
|
|||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
import { DICT_TYPE, getDictOptions } from '#/utils';
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'status',
|
||||
label: '拼团状态',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
placeholder: '请选择拼团状态',
|
||||
clearable: true,
|
||||
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'createTime',
|
||||
label: '创建时间',
|
||||
component: 'RangePicker',
|
||||
componentProps: {
|
||||
placeholder: ['开始时间', '结束时间'],
|
||||
clearable: true,
|
||||
valueFormat: 'YYYY-MM-DD HH:mm:ss',
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'id',
|
||||
title: '拼团编号',
|
||||
minWidth: 80,
|
||||
},
|
||||
{
|
||||
field: 'avatar',
|
||||
title: '头像',
|
||||
minWidth: 80,
|
||||
cellRender: {
|
||||
name: 'CellImage',
|
||||
props: {
|
||||
height: 40,
|
||||
width: 40,
|
||||
shape: 'circle',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'nickname',
|
||||
title: '昵称',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
field: 'headId',
|
||||
title: '开团团长',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
field: 'picUrl',
|
||||
title: '拼团商品图',
|
||||
minWidth: 80,
|
||||
cellRender: {
|
||||
name: 'CellImage',
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'spuName',
|
||||
title: '拼团商品',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'activityName',
|
||||
title: '拼团活动',
|
||||
minWidth: 140,
|
||||
},
|
||||
{
|
||||
field: 'userSize',
|
||||
title: '几人团',
|
||||
minWidth: 80,
|
||||
},
|
||||
{
|
||||
field: 'userCount',
|
||||
title: '参与人数',
|
||||
minWidth: 80,
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '参团时间',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'endTime',
|
||||
title: '结束时间',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
title: '拼团状态',
|
||||
minWidth: 100,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.COMMON_STATUS },
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 100,
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 用户列表表格列配置 */
|
||||
export function useUserGridColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'id',
|
||||
title: '编号',
|
||||
minWidth: 80,
|
||||
},
|
||||
{
|
||||
field: 'avatar',
|
||||
title: '用户头像',
|
||||
minWidth: 80,
|
||||
cellRender: {
|
||||
name: 'CellImage',
|
||||
props: {
|
||||
height: 40,
|
||||
width: 40,
|
||||
shape: 'circle',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'nickname',
|
||||
title: '用户昵称',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
field: 'headId',
|
||||
title: '开团团长',
|
||||
minWidth: 100,
|
||||
formatter: ({ cellValue }) => {
|
||||
return cellValue === 0 ? '团长' : '团员';
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '参团时间',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'endTime',
|
||||
title: '结束时间',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
title: '拼团状态',
|
||||
minWidth: 100,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.COMMON_STATUS },
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
import { DocAlert, Page, useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getCombinationRecordPage } from '#/api/mall/promotion/combination/combinationRecord';
|
||||
|
||||
import { useGridColumns, useGridFormSchema } from './data';
|
||||
import CombinationUserList from './modules/list.vue';
|
||||
|
||||
defineOptions({ name: 'PromotionCombinationRecord' });
|
||||
|
||||
const [UserListModal, userListModalApi] = useVbenModal({
|
||||
connectedComponent: CombinationUserList,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 查看拼团用户 */
|
||||
function handleViewUsers(row: any) {
|
||||
userListModalApi.setData({ recordId: row.id }).open();
|
||||
}
|
||||
|
||||
const [Grid] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getCombinationRecordPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: { code: 'query' },
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<template #doc>
|
||||
<DocAlert
|
||||
title="【营销】拼团活动"
|
||||
url="https://doc.iocoder.cn/mall/promotion-combination/"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<UserListModal />
|
||||
|
||||
<Grid table-title="拼团记录列表">
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '查看成员',
|
||||
type: 'primary',
|
||||
link: true,
|
||||
icon: ACTION_ICON.VIEW,
|
||||
onClick: handleViewUsers.bind(null, row),
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getCombinationRecordPage } from '#/api/mall/promotion/combination/combinationRecord';
|
||||
|
||||
import { useUserGridColumns } from '../data';
|
||||
|
||||
defineOptions({ name: 'CombinationUserList' });
|
||||
|
||||
const headId = ref<number>();
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
headId.value = undefined;
|
||||
return;
|
||||
}
|
||||
const data = modalApi.getData<{ headId: number }>();
|
||||
if (data?.headId) {
|
||||
headId.value = data.headId;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const [Grid] = useVbenVxeGrid({
|
||||
gridOptions: {
|
||||
columns: useUserGridColumns(),
|
||||
height: 600,
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }) => {
|
||||
// 暂时返回空数据,待API实现后替换
|
||||
return await getCombinationRecordPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
headId: headId.value,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
} as VxeTableGridOptions,
|
||||
});
|
||||
|
||||
const getTitle = computed(() => {
|
||||
return `拼团成员列表 (拼团ID: ${headId.value || ''})`;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal class="w-2/5" :title="getTitle">
|
||||
<Grid class="mx-4" />
|
||||
</Modal>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,129 @@
|
|||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
import { DICT_TYPE, getDictOptions, getRangePickerDefaultProps } from '#/utils';
|
||||
|
||||
import { discountFormat } from './formatter';
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'nickname',
|
||||
label: '会员昵称',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入会员昵称',
|
||||
clearable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'createTime',
|
||||
label: '领取时间',
|
||||
component: 'RangePicker',
|
||||
componentProps: {
|
||||
...getRangePickerDefaultProps(),
|
||||
clearable: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'nickname',
|
||||
title: '会员昵称',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
title: '优惠券名称',
|
||||
minWidth: 140,
|
||||
},
|
||||
{
|
||||
field: 'productScope',
|
||||
title: '类型',
|
||||
minWidth: 110,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.PROMOTION_PRODUCT_SCOPE },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'discountType',
|
||||
title: '优惠',
|
||||
minWidth: 110,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.PROMOTION_DISCOUNT_TYPE },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'discountPrice',
|
||||
title: '优惠力度',
|
||||
minWidth: 110,
|
||||
formatter: ({ row }) => {
|
||||
return discountFormat(row);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'takeType',
|
||||
title: '领取方式',
|
||||
minWidth: 110,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.PROMOTION_COUPON_TAKE_TYPE },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
title: '状态',
|
||||
minWidth: 110,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.PROMOTION_COUPON_STATUS },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '领取时间',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'useTime',
|
||||
title: '使用时间',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 100,
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 获取状态选项卡配置 */
|
||||
export function getStatusTabs() {
|
||||
const tabs = [
|
||||
{
|
||||
label: '全部',
|
||||
value: 'all',
|
||||
},
|
||||
];
|
||||
|
||||
// 添加字典状态选项
|
||||
const statusOptions = getDictOptions(DICT_TYPE.PROMOTION_COUPON_STATUS);
|
||||
for (const option of statusOptions) {
|
||||
tabs.push({
|
||||
label: option.label,
|
||||
value: String(option.value),
|
||||
});
|
||||
}
|
||||
|
||||
return tabs;
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
import type { MallCouponTemplateApi } from '#/api/mall/promotion/coupon/couponTemplate';
|
||||
|
||||
import { floatToFixed2, formatDate } from '@vben/utils';
|
||||
|
||||
import {
|
||||
CouponTemplateValidityTypeEnum,
|
||||
PromotionDiscountTypeEnum,
|
||||
} from '#/utils';
|
||||
|
||||
// 格式化【优惠金额/折扣】
|
||||
export function discountFormat(row: MallCouponTemplateApi.CouponTemplate) {
|
||||
if (row.discountType === PromotionDiscountTypeEnum.PRICE.type) {
|
||||
return `¥${floatToFixed2(row.discountPrice)}`;
|
||||
}
|
||||
if (row.discountType === PromotionDiscountTypeEnum.PERCENT.type) {
|
||||
return `${row.discountPercent}%`;
|
||||
}
|
||||
return `未知【${row.discountType}】`;
|
||||
}
|
||||
|
||||
// 格式化【领取上限】
|
||||
export function takeLimitCountFormat(
|
||||
row: MallCouponTemplateApi.CouponTemplate,
|
||||
) {
|
||||
if (row.takeLimitCount) {
|
||||
if (row.takeLimitCount === -1) {
|
||||
return '无领取限制';
|
||||
}
|
||||
return `${row.takeLimitCount} 张/人`;
|
||||
} else {
|
||||
return ' ';
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化【有效期限】
|
||||
export function validityTypeFormat(row: MallCouponTemplateApi.CouponTemplate) {
|
||||
if (row.validityType === CouponTemplateValidityTypeEnum.DATE.type) {
|
||||
return `${formatDate(row.validStartTime)} 至 ${formatDate(row.validEndTime)}`;
|
||||
}
|
||||
if (row.validityType === CouponTemplateValidityTypeEnum.TERM.type) {
|
||||
return `领取后第 ${row.fixedStartTerm} - ${row.fixedEndTerm} 天内可用`;
|
||||
}
|
||||
return `未知【${row.validityType}】`;
|
||||
}
|
||||
|
||||
// 格式化【totalCount】
|
||||
export function totalCountFormat(row: MallCouponTemplateApi.CouponTemplate) {
|
||||
if (row.totalCount === -1) {
|
||||
return '不限制';
|
||||
}
|
||||
return row.totalCount;
|
||||
}
|
||||
|
||||
// 格式化【剩余数量】
|
||||
export function remainedCountFormat(row: MallCouponTemplateApi.CouponTemplate) {
|
||||
if (row.totalCount === -1) {
|
||||
return '不限制';
|
||||
}
|
||||
return row.totalCount - row.takeCount;
|
||||
}
|
||||
|
||||
// 格式化【最低消费】
|
||||
export function usePriceFormat(row: MallCouponTemplateApi.CouponTemplate) {
|
||||
return `¥${floatToFixed2(row.usePrice)}`;
|
||||
}
|
||||
|
|
@ -0,0 +1,129 @@
|
|||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { MallCouponApi } from '#/api/mall/promotion/coupon/coupon';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { DocAlert, Page } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { ElLoading, ElMessage } from 'element-plus';
|
||||
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
deleteCoupon,
|
||||
getCouponPage,
|
||||
} from '#/api/mall/promotion/coupon/coupon';
|
||||
|
||||
import { getStatusTabs, useGridColumns, useGridFormSchema } from './data';
|
||||
|
||||
defineOptions({ name: 'PromotionCoupon' });
|
||||
|
||||
const activeTab = ref('all');
|
||||
const statusTabs = ref(getStatusTabs());
|
||||
|
||||
/** 删除优惠券 */
|
||||
async function handleDelete(row: MallCouponApi.Coupon) {
|
||||
const loadingInstance = ElLoading.service({
|
||||
text: $t('ui.actionMessage.deleting', [row.name]),
|
||||
fullscreen: true,
|
||||
});
|
||||
try {
|
||||
await deleteCoupon(row.id as number);
|
||||
ElMessage.success('回收成功');
|
||||
onRefresh();
|
||||
} finally {
|
||||
loadingInstance.close();
|
||||
}
|
||||
}
|
||||
|
||||
/** 刷新表格 */
|
||||
function onRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** Tab切换 */
|
||||
function onTabChange(tabName: string) {
|
||||
activeTab.value = tabName;
|
||||
// 设置状态查询参数
|
||||
const formValues = gridApi.formApi.getValues();
|
||||
const status = tabName === 'all' ? undefined : Number(tabName);
|
||||
gridApi.formApi.setValues({ ...formValues, status });
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
const params = {
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
// Tab状态过滤
|
||||
status:
|
||||
activeTab.value === 'all' ? undefined : Number(activeTab.value),
|
||||
};
|
||||
return await getCouponPage(params);
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: { code: 'query' },
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<MallCouponApi.Coupon>,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<template #doc>
|
||||
<DocAlert
|
||||
title="【营销】优惠劵"
|
||||
url="https://doc.iocoder.cn/mall/promotion-coupon/"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<Grid table-title="优惠券列表">
|
||||
<template #top>
|
||||
<Tabs v-model:active-key="activeTab" type="card" @change="onTabChange">
|
||||
<TabPane
|
||||
v-for="tab in statusTabs"
|
||||
:key="tab.value"
|
||||
:tab="tab.label"
|
||||
/>
|
||||
</Tabs>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '回收',
|
||||
type: 'danger',
|
||||
link: true,
|
||||
icon: ACTION_ICON.DELETE,
|
||||
auth: ['promotion:coupon:delete'],
|
||||
popConfirm: {
|
||||
title:
|
||||
'回收将会收回会员领取的待使用的优惠券,已使用的将无法回收,确定要回收所选优惠券吗?',
|
||||
confirm: handleDelete.bind(null, row),
|
||||
},
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,252 @@
|
|||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
// 格式化函数移到组件内部实现
|
||||
import { z } from '#/adapter/form';
|
||||
import {
|
||||
CommonStatusEnum,
|
||||
DICT_TYPE,
|
||||
getDictOptions,
|
||||
getRangePickerDefaultProps,
|
||||
} from '#/utils';
|
||||
|
||||
import {
|
||||
discountFormat,
|
||||
remainedCountFormat,
|
||||
takeLimitCountFormat,
|
||||
totalCountFormat,
|
||||
validityTypeFormat,
|
||||
} from '../formatter';
|
||||
|
||||
/** 新增/修改的表单 */
|
||||
export function useFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'id',
|
||||
component: 'Input',
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
show: () => false,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '优惠券名称',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入优惠券名称',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'description',
|
||||
label: '优惠券描述',
|
||||
component: 'Textarea',
|
||||
},
|
||||
// TODO
|
||||
{
|
||||
fieldName: 'productScope',
|
||||
label: '优惠类型',
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.PROMOTION_PRODUCT_SCOPE, 'number'),
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'takeType',
|
||||
label: '领取方式',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
placeholder: '请选择领取方式',
|
||||
options: getDictOptions(DICT_TYPE.PROMOTION_COUPON_TAKE_TYPE, 'number'),
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'validityType',
|
||||
label: '有效期类型',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
placeholder: '请选择有效期类型',
|
||||
options: getDictOptions(
|
||||
DICT_TYPE.PROMOTION_COUPON_TEMPLATE_VALIDITY_TYPE,
|
||||
'number',
|
||||
),
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'totalCount',
|
||||
label: '发放数量',
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
min: 0,
|
||||
placeholder: '请输入发放数量',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'takeLimitCount',
|
||||
label: '领取上限',
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
min: 0,
|
||||
placeholder: '请输入领取上限',
|
||||
},
|
||||
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',
|
||||
componentProps: {
|
||||
placeholder: '请输入优惠券名称',
|
||||
clearable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'discountType',
|
||||
label: '优惠类型',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
placeholder: '请选择优惠类型',
|
||||
clearable: true,
|
||||
options: getDictOptions(DICT_TYPE.PROMOTION_DISCOUNT_TYPE, 'number'),
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'status',
|
||||
label: '优惠券状态',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
placeholder: '请选择优惠券状态',
|
||||
clearable: true,
|
||||
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'createTime',
|
||||
label: '创建时间',
|
||||
component: 'RangePicker',
|
||||
componentProps: {
|
||||
...getRangePickerDefaultProps(),
|
||||
clearable: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{ type: 'checkbox', width: 40 },
|
||||
{
|
||||
field: 'name',
|
||||
title: '优惠券名称',
|
||||
minWidth: 140,
|
||||
},
|
||||
{
|
||||
field: 'productScope',
|
||||
title: '类型',
|
||||
minWidth: 130,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.PROMOTION_PRODUCT_SCOPE },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'discountType',
|
||||
title: '优惠',
|
||||
minWidth: 110,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.PROMOTION_DISCOUNT_TYPE },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'discountPrice',
|
||||
title: '优惠力度',
|
||||
minWidth: 110,
|
||||
formatter: ({ row }) => {
|
||||
return discountFormat(row);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'takeType',
|
||||
title: '领取方式',
|
||||
minWidth: 100,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.PROMOTION_COUPON_TAKE_TYPE },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'validityType',
|
||||
title: '使用时间',
|
||||
minWidth: 180,
|
||||
formatter: ({ row }) => {
|
||||
return validityTypeFormat(row);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'totalCount',
|
||||
title: '发放数量',
|
||||
minWidth: 100,
|
||||
formatter: ({ row }) => {
|
||||
return totalCountFormat(row);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'remainedCount',
|
||||
title: '剩余数量',
|
||||
minWidth: 100,
|
||||
formatter: ({ row }) => {
|
||||
return remainedCountFormat(row);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'takeLimitCount',
|
||||
title: '领取上限',
|
||||
minWidth: 100,
|
||||
formatter: ({ row }) => {
|
||||
return takeLimitCountFormat(row);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
title: '状态',
|
||||
minWidth: 100,
|
||||
slots: { default: 'status' },
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '创建时间',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 120,
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -0,0 +1,185 @@
|
|||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { MallCouponTemplateApi } from '#/api/mall/promotion/coupon/couponTemplate';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
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 {
|
||||
deleteCouponTemplate,
|
||||
getCouponTemplatePage,
|
||||
updateCouponTemplateStatus,
|
||||
} from '#/api/mall/promotion/coupon/couponTemplate';
|
||||
import { CommonStatusEnum } from '#/utils';
|
||||
|
||||
import { useGridColumns, useGridFormSchema } from './data';
|
||||
import Form from './modules/form.vue';
|
||||
|
||||
defineOptions({ name: 'PromotionCouponTemplate' });
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: Form,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 刷新表格 */
|
||||
function onRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 编辑优惠券模板 */
|
||||
function handleEdit(row: MallCouponTemplateApi.CouponTemplate) {
|
||||
formModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 创建优惠券模板 */
|
||||
function handleCreate() {
|
||||
formModalApi.setData(null).open();
|
||||
}
|
||||
|
||||
/** 删除优惠券模板 */
|
||||
async function handleDelete(row: MallCouponTemplateApi.CouponTemplate) {
|
||||
const loadingInstance = ElLoading.service({
|
||||
text: $t('ui.actionMessage.deleting', [row.name]),
|
||||
fullscreen: true,
|
||||
});
|
||||
try {
|
||||
await deleteCouponTemplate(row.id as number);
|
||||
ElMessage.success($t('ui.actionMessage.deleteSuccess', [row.name]));
|
||||
onRefresh();
|
||||
} finally {
|
||||
loadingInstance.close();
|
||||
}
|
||||
}
|
||||
|
||||
const checkedIds = ref<number[]>([]);
|
||||
function handleRowCheckboxChange({
|
||||
records,
|
||||
}: {
|
||||
records: MallCouponTemplateApi.CouponTemplate[];
|
||||
}) {
|
||||
checkedIds.value = records.map((item) => item.id as number);
|
||||
}
|
||||
|
||||
/** 优惠券模板状态修改 */
|
||||
async function handleStatusChange(row: MallCouponTemplateApi.CouponTemplate) {
|
||||
const text = row.status === CommonStatusEnum.ENABLE ? '启用' : '停用';
|
||||
const loadingInstance = ElLoading.service({
|
||||
text: `正在${text}优惠券模板...`,
|
||||
fullscreen: true,
|
||||
});
|
||||
try {
|
||||
await updateCouponTemplateStatus(row.id as number, row.status as 0 | 1);
|
||||
ElMessage.success(`${text}成功`);
|
||||
} catch {
|
||||
// 异常时,需要将 row.status 状态重置回之前的
|
||||
row.status =
|
||||
row.status === CommonStatusEnum.ENABLE
|
||||
? CommonStatusEnum.DISABLE
|
||||
: CommonStatusEnum.ENABLE;
|
||||
} finally {
|
||||
loadingInstance.close();
|
||||
}
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getCouponTemplatePage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: { code: 'query' },
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<MallCouponTemplateApi.CouponTemplate>,
|
||||
gridEvents: {
|
||||
checkboxAll: handleRowCheckboxChange,
|
||||
checkboxChange: handleRowCheckboxChange,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<template #doc>
|
||||
<DocAlert
|
||||
title="【营销】优惠劵"
|
||||
url="https://doc.iocoder.cn/mall/promotion-coupon/"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<FormModal @success="onRefresh" />
|
||||
<Grid table-title="优惠券列表">
|
||||
<template #toolbar-tools>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('ui.actionTitle.create', ['优惠券模板']),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.ADD,
|
||||
auth: ['promotion:coupon-template:create'],
|
||||
onClick: handleCreate,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<template #status="{ row }">
|
||||
<ElSwitch
|
||||
v-model:checked="row.status"
|
||||
:checked-value="CommonStatusEnum.ENABLE"
|
||||
:un-checked-value="CommonStatusEnum.DISABLE"
|
||||
@change="handleStatusChange(row)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('common.edit'),
|
||||
type: 'primary',
|
||||
link: true,
|
||||
icon: ACTION_ICON.EDIT,
|
||||
auth: ['promotion:coupon-template:update'],
|
||||
onClick: handleEdit.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: $t('common.delete'),
|
||||
type: 'danger',
|
||||
link: true,
|
||||
icon: ACTION_ICON.DELETE,
|
||||
auth: ['promotion:coupon-template: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 { MallCouponTemplateApi } from '#/api/mall/promotion/coupon/couponTemplate';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import {
|
||||
createCouponTemplate,
|
||||
getCouponTemplate,
|
||||
updateCouponTemplate,
|
||||
} from '#/api/mall/promotion/coupon/couponTemplate';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useFormSchema } from '../data';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const formData = ref<MallCouponTemplateApi.CouponTemplate>();
|
||||
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: 80,
|
||||
},
|
||||
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 MallCouponTemplateApi.CouponTemplate;
|
||||
try {
|
||||
await (formData.value?.id
|
||||
? updateCouponTemplate(data)
|
||||
: createCouponTemplate(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<MallCouponTemplateApi.CouponTemplate>();
|
||||
if (!data || !data.id) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
try {
|
||||
formData.value = await getCouponTemplate(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,159 @@
|
|||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
import { formatDate } from '@vben/utils';
|
||||
|
||||
import { DICT_TYPE, getDictOptions } from '#/utils';
|
||||
|
||||
/** 表单配置 */
|
||||
export function useFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'id',
|
||||
component: 'Input',
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
show: () => false,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '活动名称',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入活动名称',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'status',
|
||||
label: '活动状态',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
placeholder: '请选择活动状态',
|
||||
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'startTime',
|
||||
label: '开始时间',
|
||||
component: 'DatePicker',
|
||||
componentProps: {
|
||||
placeholder: '请选择开始时间',
|
||||
showTime: false,
|
||||
valueFormat: 'x',
|
||||
format: 'YYYY-MM-DD',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'endTime',
|
||||
label: '结束时间',
|
||||
component: 'DatePicker',
|
||||
componentProps: {
|
||||
placeholder: '请选择结束时间',
|
||||
showTime: false,
|
||||
valueFormat: 'x',
|
||||
format: 'YYYY-MM-DD',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'remark',
|
||||
label: '备注',
|
||||
component: 'Textarea',
|
||||
componentProps: {
|
||||
placeholder: '请输入备注',
|
||||
rows: 4,
|
||||
},
|
||||
},
|
||||
// TODO
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '活动名称',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入活动名称',
|
||||
clearable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'status',
|
||||
label: '活动状态',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
placeholder: '请选择活动状态',
|
||||
clearable: true,
|
||||
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'activeTime',
|
||||
label: '活动时间',
|
||||
component: 'RangePicker',
|
||||
componentProps: {
|
||||
placeholder: ['开始时间', '结束时间'],
|
||||
clearable: true,
|
||||
valueFormat: 'YYYY-MM-DD HH:mm:ss',
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'id',
|
||||
title: '活动编号',
|
||||
minWidth: 80,
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
title: '活动名称',
|
||||
minWidth: 140,
|
||||
},
|
||||
{
|
||||
field: 'activityTime',
|
||||
title: '活动时间',
|
||||
minWidth: 210,
|
||||
formatter: ({ row }) => {
|
||||
if (!row.startTime || !row.endTime) return '';
|
||||
return `${formatDate(row.startTime, 'YYYY-MM-DD')} ~ ${formatDate(row.endTime, 'YYYY-MM-DD')}`;
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
title: '活动状态',
|
||||
minWidth: 100,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.COMMON_STATUS },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'remark',
|
||||
title: '备注',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '创建时间',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 150,
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -0,0 +1,173 @@
|
|||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { MallDiscountActivityApi } from '#/api/mall/promotion/discount/discountActivity';
|
||||
|
||||
import { confirm, DocAlert, Page, useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { ElLoading, ElMessage } from 'element-plus';
|
||||
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
closeDiscountActivity,
|
||||
deleteDiscountActivity,
|
||||
getDiscountActivityPage,
|
||||
} from '#/api/mall/promotion/discount/discountActivity';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useGridColumns, useGridFormSchema } from './data';
|
||||
import DiscountActivityForm from './modules/form.vue';
|
||||
|
||||
defineOptions({ name: 'PromotionDiscountActivity' });
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: DiscountActivityForm,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 刷新表格 */
|
||||
function onRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 创建满减活动 */
|
||||
function handleCreate() {
|
||||
formModalApi.setData(null).open();
|
||||
}
|
||||
|
||||
/** 编辑满减活动 */
|
||||
function handleEdit(row: MallDiscountActivityApi.DiscountActivity) {
|
||||
formModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 关闭满减活动 */
|
||||
async function handleClose(row: MallDiscountActivityApi.DiscountActivity) {
|
||||
try {
|
||||
await confirm({
|
||||
content: '确认关闭该限时折扣活动吗?',
|
||||
});
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
const loadingInstance = ElLoading.service({
|
||||
text: '正在关闭中',
|
||||
fullscreen: true,
|
||||
});
|
||||
try {
|
||||
await closeDiscountActivity(row.id as number);
|
||||
ElMessage.success('关闭成功');
|
||||
onRefresh();
|
||||
} finally {
|
||||
loadingInstance.close();
|
||||
}
|
||||
}
|
||||
|
||||
/** 删除满减活动 */
|
||||
async function handleDelete(row: MallDiscountActivityApi.DiscountActivity) {
|
||||
const loadingInstance = ElLoading.service({
|
||||
text: $t('ui.actionMessage.deleting', [row.name]),
|
||||
fullscreen: true,
|
||||
});
|
||||
try {
|
||||
await deleteDiscountActivity(row.id as number);
|
||||
ElMessage.success($t('ui.actionMessage.deleteSuccess', [row.name]));
|
||||
onRefresh();
|
||||
} finally {
|
||||
loadingInstance.close();
|
||||
}
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getDiscountActivityPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: { code: 'query' },
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<MallDiscountActivityApi.DiscountActivity>,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<template #doc>
|
||||
<DocAlert
|
||||
title="【营销】限时折扣"
|
||||
url="https://doc.iocoder.cn/mall/promotion-discount/"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<FormModal @success="onRefresh" />
|
||||
|
||||
<Grid table-title="限时折扣活动列表">
|
||||
<template #toolbar-tools>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('ui.actionTitle.create', ['限时折扣活动']),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.ADD,
|
||||
auth: ['promotion:discount-activity:create'],
|
||||
onClick: handleCreate,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('common.edit'),
|
||||
type: 'primary',
|
||||
link: true,
|
||||
icon: ACTION_ICON.EDIT,
|
||||
auth: ['promotion:discount-activity:update'],
|
||||
onClick: handleEdit.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: '关闭',
|
||||
type: 'danger',
|
||||
link: true,
|
||||
icon: ACTION_ICON.DELETE,
|
||||
auth: ['promotion:discount-activity:close'],
|
||||
ifShow: row.status === 0,
|
||||
onClick: handleClose.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: $t('common.delete'),
|
||||
type: 'danger',
|
||||
link: true,
|
||||
icon: ACTION_ICON.DELETE,
|
||||
auth: ['promotion:discount-activity:delete'],
|
||||
ifShow: row.status !== 0,
|
||||
popConfirm: {
|
||||
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
|
||||
confirm: handleDelete.bind(null, row),
|
||||
},
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
<script lang="ts" setup>
|
||||
import type { MallDiscountActivityApi } from '#/api/mall/promotion/discount/discountActivity';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenForm, useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
import {
|
||||
createDiscountActivity,
|
||||
getDiscountActivity,
|
||||
updateDiscountActivity,
|
||||
} from '#/api/mall/promotion/discount/discountActivity';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useFormSchema } from '../data';
|
||||
|
||||
defineOptions({ name: 'DiscountActivityForm' });
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const formData = ref<MallDiscountActivityApi.DiscountActivity>();
|
||||
const getTitle = computed(() => {
|
||||
return formData.value?.id
|
||||
? $t('ui.actionTitle.edit', ['限时折扣活动'])
|
||||
: $t('ui.actionTitle.create', ['限时折扣活动']);
|
||||
});
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
labelWidth: 100,
|
||||
},
|
||||
wrapperClass: 'grid-cols-2',
|
||||
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 MallDiscountActivityApi.DiscountActivity;
|
||||
|
||||
// 确保必要的默认值
|
||||
if (!data.products) {
|
||||
data.products = [];
|
||||
}
|
||||
|
||||
try {
|
||||
await (formData.value?.id
|
||||
? updateDiscountActivity(data)
|
||||
: createDiscountActivity(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<MallDiscountActivityApi.DiscountActivity>();
|
||||
if (!data || !data.id) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
try {
|
||||
formData.value = await getDiscountActivity(data.id as number);
|
||||
// 设置到 values
|
||||
if (formData.value) {
|
||||
await formApi.setValues(formData.value);
|
||||
}
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal class="w-3/5" :title="getTitle">
|
||||
<Form />
|
||||
</Modal>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
/** 表单配置 */
|
||||
export function useFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'id',
|
||||
component: 'Input',
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
show: () => false,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '页面名称',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入页面名称',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'remark',
|
||||
label: '备注',
|
||||
component: 'Textarea',
|
||||
componentProps: {
|
||||
placeholder: '请输入备注',
|
||||
rows: 4,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'previewPicUrls',
|
||||
component: 'ImageUpload',
|
||||
label: '预览图',
|
||||
componentProps: {
|
||||
maxNumber: 10,
|
||||
multiple: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '页面名称',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入页面名称',
|
||||
clearable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'createTime',
|
||||
label: '创建时间',
|
||||
component: 'RangePicker',
|
||||
componentProps: {
|
||||
placeholder: ['开始时间', '结束时间'],
|
||||
clearable: true,
|
||||
valueFormat: 'YYYY-MM-DD HH:mm:ss',
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'id',
|
||||
title: '编号',
|
||||
minWidth: 80,
|
||||
},
|
||||
{
|
||||
field: 'previewPicUrls',
|
||||
title: '预览图',
|
||||
minWidth: 120,
|
||||
cellRender: {
|
||||
name: 'CellImages',
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
title: '页面名称',
|
||||
minWidth: 150,
|
||||
},
|
||||
{
|
||||
field: 'remark',
|
||||
title: '备注',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '创建时间',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 200,
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -0,0 +1,143 @@
|
|||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { MallDiyPageApi } from '#/api/mall/promotion/diy/page';
|
||||
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { DocAlert, Page, useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { deleteDiyPage, getDiyPagePage } from '#/api/mall/promotion/diy/page';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useGridColumns, useGridFormSchema } from './data';
|
||||
import DiyPageForm from './modules/form.vue';
|
||||
|
||||
defineOptions({ name: 'PromotionDiyPage' });
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: DiyPageForm,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
const { push } = useRouter();
|
||||
|
||||
/** 刷新表格 */
|
||||
function onRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 创建DIY页面 */
|
||||
function handleCreate() {
|
||||
formModalApi.setData(null).open();
|
||||
}
|
||||
|
||||
/** 编辑DIY页面 */
|
||||
function handleEdit(row: MallDiyPageApi.DiyPage) {
|
||||
formModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 装修页面 */
|
||||
function handleDecorate(row: MallDiyPageApi.DiyPage) {
|
||||
// 跳转到装修页面
|
||||
push({ name: 'DiyPageDecorate', params: { id: row.id } });
|
||||
}
|
||||
|
||||
/** 删除DIY页面 */
|
||||
async function handleDelete(row: MallDiyPageApi.DiyPage) {
|
||||
await deleteDiyPage(row.id as number);
|
||||
onRefresh();
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getDiyPagePage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: { code: 'query' },
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<MallDiyPageApi.DiyPage>,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<template #doc>
|
||||
<DocAlert
|
||||
title="【营销】商城装修"
|
||||
url="https://doc.iocoder.cn/mall/diy/"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<FormModal @success="onRefresh" />
|
||||
|
||||
<Grid table-title="装修页面列表">
|
||||
<template #toolbar-tools>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('ui.actionTitle.create', ['装修页面']),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.ADD,
|
||||
auth: ['promotion:diy-page:create'],
|
||||
onClick: handleCreate,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '装修',
|
||||
type: 'primary',
|
||||
link: true,
|
||||
icon: ACTION_ICON.EDIT,
|
||||
auth: ['promotion:diy-page:update'],
|
||||
onClick: handleDecorate.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: $t('common.edit'),
|
||||
type: 'primary',
|
||||
link: true,
|
||||
icon: ACTION_ICON.EDIT,
|
||||
auth: ['promotion:diy-page:update'],
|
||||
onClick: handleEdit.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: $t('common.delete'),
|
||||
type: 'danger',
|
||||
link: true,
|
||||
icon: ACTION_ICON.DELETE,
|
||||
auth: ['promotion:diy-page:delete'],
|
||||
popConfirm: {
|
||||
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
|
||||
confirm: handleDelete.bind(null, row),
|
||||
},
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
<script lang="ts" setup>
|
||||
import type { MallDiyPageApi } from '#/api/mall/promotion/diy/page';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenForm, useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
import {
|
||||
createDiyPage,
|
||||
getDiyPage,
|
||||
updateDiyPage,
|
||||
} from '#/api/mall/promotion/diy/page';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useFormSchema } from '../data';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const formData = ref<MallDiyPageApi.DiyPage>();
|
||||
const getTitle = computed(() => {
|
||||
return formData.value?.id
|
||||
? $t('ui.actionTitle.edit', ['装修页面'])
|
||||
: $t('ui.actionTitle.create', ['装修页面']);
|
||||
});
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
labelWidth: 100,
|
||||
},
|
||||
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 MallDiyPageApi.DiyPage;
|
||||
|
||||
// 确保必要的默认值
|
||||
if (!data.previewPicUrls) {
|
||||
data.previewPicUrls = [];
|
||||
}
|
||||
|
||||
try {
|
||||
await (formData.value?.id ? updateDiyPage(data) : createDiyPage(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<MallDiyPageApi.DiyPage>();
|
||||
if (!data || !data.id) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
try {
|
||||
formData.value = await getDiyPage(data.id as number);
|
||||
// 设置到 values
|
||||
if (formData.value) {
|
||||
await formApi.setValues(formData.value);
|
||||
}
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal class="w-2/5" :title="getTitle">
|
||||
<Form />
|
||||
</Modal>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,120 @@
|
|||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
import { DICT_TYPE } from '#/utils/dict';
|
||||
|
||||
/** 表单配置 */
|
||||
export function useFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'id',
|
||||
component: 'Input',
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
show: () => false,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '模板名称',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入模板名称',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'remark',
|
||||
label: '备注',
|
||||
component: 'Textarea',
|
||||
componentProps: {
|
||||
placeholder: '请输入备注',
|
||||
rows: 4,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'previewPicUrls',
|
||||
component: 'ImageUpload',
|
||||
label: '预览图',
|
||||
componentProps: {
|
||||
maxNumber: 10,
|
||||
multiple: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '模板名称',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入模板名称',
|
||||
clearable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'createTime',
|
||||
label: '创建时间',
|
||||
component: 'RangePicker',
|
||||
componentProps: {
|
||||
placeholder: ['开始时间', '结束时间'],
|
||||
clearable: true,
|
||||
valueFormat: 'YYYY-MM-DD HH:mm:ss',
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'id',
|
||||
title: '编号',
|
||||
minWidth: 80,
|
||||
},
|
||||
{
|
||||
field: 'previewPicUrls',
|
||||
title: '预览图',
|
||||
minWidth: 120,
|
||||
cellRender: {
|
||||
name: 'CellImages',
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
title: '模板名称',
|
||||
minWidth: 150,
|
||||
},
|
||||
{
|
||||
field: 'used',
|
||||
title: '是否使用',
|
||||
minWidth: 100,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'remark',
|
||||
title: '备注',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '创建时间',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 250,
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -0,0 +1,170 @@
|
|||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { MallDiyTemplateApi } from '#/api/mall/promotion/diy/template';
|
||||
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { confirm, DocAlert, Page, useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
deleteDiyTemplate,
|
||||
getDiyTemplatePage,
|
||||
useDiyTemplate,
|
||||
} from '#/api/mall/promotion/diy/template';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useGridColumns, useGridFormSchema } from './data';
|
||||
import DiyTemplateForm from './modules/form.vue';
|
||||
|
||||
defineOptions({ name: 'PromotionDiyTemplate' });
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: DiyTemplateForm,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
/** 刷新表格 */
|
||||
function onRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 创建DIY模板 */
|
||||
function handleCreate() {
|
||||
formModalApi.setData(null).open();
|
||||
}
|
||||
|
||||
/** 编辑DIY模板 */
|
||||
function handleEdit(row: MallDiyTemplateApi.DiyTemplate) {
|
||||
formModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 装修模板 */
|
||||
function handleDecorate(row: MallDiyTemplateApi.DiyTemplate) {
|
||||
// 跳转到装修页面
|
||||
router.push({ name: 'DiyTemplateDecorate', params: { id: row.id } });
|
||||
}
|
||||
|
||||
/** 使用模板 */
|
||||
async function handleUse(row: MallDiyTemplateApi.DiyTemplate) {
|
||||
confirm({
|
||||
content: `是否使用模板"${row.name}"?`,
|
||||
}).then(async () => {
|
||||
// 发起删除
|
||||
await useDiyTemplate(row.id as number);
|
||||
ElMessage.success('使用成功');
|
||||
onRefresh();
|
||||
});
|
||||
}
|
||||
|
||||
/** 删除DIY模板 */
|
||||
async function handleDelete(row: MallDiyTemplateApi.DiyTemplate) {
|
||||
await deleteDiyTemplate(row.id as number);
|
||||
onRefresh();
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getDiyTemplatePage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: { code: 'query' },
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<MallDiyTemplateApi.DiyTemplate>,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<template #doc>
|
||||
<DocAlert
|
||||
title="【营销】商城装修"
|
||||
url="https://doc.iocoder.cn/mall/diy/"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<FormModal @success="onRefresh" />
|
||||
|
||||
<Grid table-title="装修模板列表">
|
||||
<template #toolbar-tools>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('ui.actionTitle.create', ['装修模板']),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.ADD,
|
||||
auth: ['promotion:diy-template:create'],
|
||||
onClick: handleCreate,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '装修',
|
||||
type: 'primary',
|
||||
link: true,
|
||||
icon: ACTION_ICON.EDIT,
|
||||
auth: ['promotion:diy-template:update'],
|
||||
onClick: handleDecorate.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: $t('common.edit'),
|
||||
type: 'primary',
|
||||
link: true,
|
||||
icon: ACTION_ICON.EDIT,
|
||||
auth: ['promotion:diy-template:update'],
|
||||
onClick: handleEdit.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: '使用',
|
||||
type: 'primary',
|
||||
link: true,
|
||||
auth: ['promotion:diy-template:use'],
|
||||
ifShow: !row.used,
|
||||
onClick: handleUse.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: $t('common.delete'),
|
||||
type: 'danger',
|
||||
link: true,
|
||||
icon: ACTION_ICON.DELETE,
|
||||
auth: ['promotion:diy-template:delete'],
|
||||
ifShow: !row.used,
|
||||
popConfirm: {
|
||||
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
|
||||
confirm: handleDelete.bind(null, row),
|
||||
},
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
<script lang="ts" setup>
|
||||
import type { MallDiyTemplateApi } from '#/api/mall/promotion/diy/template';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenForm, useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
import {
|
||||
createDiyTemplate,
|
||||
getDiyTemplate,
|
||||
updateDiyTemplate,
|
||||
} from '#/api/mall/promotion/diy/template';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useFormSchema } from '../data';
|
||||
|
||||
/** 提交表单 */
|
||||
const emit = defineEmits(['success']);
|
||||
|
||||
const formData = ref<MallDiyTemplateApi.DiyTemplate>();
|
||||
const getTitle = computed(() => {
|
||||
return formData.value?.id
|
||||
? $t('ui.actionTitle.edit', ['装修模板'])
|
||||
: $t('ui.actionTitle.create', ['装修模板']);
|
||||
});
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
labelWidth: 100,
|
||||
},
|
||||
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 MallDiyTemplateApi.DiyTemplate;
|
||||
|
||||
// 确保必要的默认值
|
||||
if (!data.previewPicUrls) {
|
||||
data.previewPicUrls = [];
|
||||
}
|
||||
if (data.used === undefined) {
|
||||
data.used = false;
|
||||
}
|
||||
|
||||
try {
|
||||
await (formData.value?.id
|
||||
? updateDiyTemplate(data)
|
||||
: createDiyTemplate(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<MallDiyTemplateApi.DiyTemplate>();
|
||||
if (!data || !data.id) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
try {
|
||||
formData.value = await getDiyTemplate(data.id as number);
|
||||
// 设置到 values
|
||||
if (formData.value) {
|
||||
await formApi.setValues(formData.value);
|
||||
}
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal class="w-2/5" :title="getTitle">
|
||||
<Form />
|
||||
</Modal>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
<script lang="ts" setup>
|
||||
import { Page } from '@vben/common-ui';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page>
|
||||
<Button
|
||||
danger
|
||||
type="link"
|
||||
target="_blank"
|
||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
|
||||
>
|
||||
该功能支持 Vue3 + element-plus 版本!
|
||||
</Button>
|
||||
<br />
|
||||
<Button
|
||||
type="link"
|
||||
target="_blank"
|
||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/mall/promotion/kefu/index"
|
||||
>
|
||||
可参考
|
||||
https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/mall/promotion/kefu/index
|
||||
代码,pull request 贡献给我们!
|
||||
</Button>
|
||||
</Page>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,140 @@
|
|||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
import { DICT_TYPE } from '#/utils/dict';
|
||||
|
||||
/** 表单配置 */
|
||||
export function useFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'id',
|
||||
component: 'Input',
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
show: () => false,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'spuId',
|
||||
label: '积分商城活动商品',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请选择商品',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'sort',
|
||||
label: '排序',
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
placeholder: '请输入排序',
|
||||
min: 0,
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'remark',
|
||||
label: '备注',
|
||||
component: 'Textarea',
|
||||
componentProps: {
|
||||
placeholder: '请输入备注',
|
||||
rows: 4,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'status',
|
||||
label: '活动状态',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
placeholder: '请选择活动状态',
|
||||
clearable: true,
|
||||
dictType: DICT_TYPE.COMMON_STATUS,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'id',
|
||||
title: '活动编号',
|
||||
minWidth: 80,
|
||||
},
|
||||
{
|
||||
field: 'picUrl',
|
||||
title: '商品图片',
|
||||
minWidth: 80,
|
||||
cellRender: {
|
||||
name: 'CellImage',
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'spuName',
|
||||
title: '商品标题',
|
||||
minWidth: 300,
|
||||
},
|
||||
{
|
||||
field: 'marketPrice',
|
||||
title: '原价',
|
||||
minWidth: 100,
|
||||
formatter: 'formatAmount2',
|
||||
},
|
||||
{
|
||||
field: 'point',
|
||||
title: '兑换积分',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
field: 'price',
|
||||
title: '兑换金额',
|
||||
minWidth: 100,
|
||||
formatter: 'formatAmount2',
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
title: '活动状态',
|
||||
minWidth: 100,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.COMMON_STATUS },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'stock',
|
||||
title: '库存',
|
||||
minWidth: 80,
|
||||
},
|
||||
{
|
||||
field: 'totalStock',
|
||||
title: '总库存',
|
||||
minWidth: 80,
|
||||
},
|
||||
{
|
||||
field: 'redeemedQuantity',
|
||||
title: '已兑换数量',
|
||||
minWidth: 100,
|
||||
slots: { default: 'redeemedQuantity' },
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '创建时间',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 150,
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -0,0 +1,162 @@
|
|||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { MallPointActivityApi } from '#/api/mall/promotion/point';
|
||||
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { confirm, DocAlert, Page, useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
closePointActivity,
|
||||
deletePointActivity,
|
||||
getPointActivityPage,
|
||||
} from '#/api/mall/promotion/point';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useGridColumns, useGridFormSchema } from './data';
|
||||
import PointActivityForm from './modules/form.vue';
|
||||
|
||||
defineOptions({ name: 'PromotionPointActivity' });
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: PointActivityForm,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 获得商品已兑换数量 */
|
||||
const getRedeemedQuantity = computed(
|
||||
() => (row: MallPointActivityApi.PointActivity) =>
|
||||
(row.totalStock || 0) - (row.stock || 0),
|
||||
);
|
||||
|
||||
/** 刷新表格 */
|
||||
function onRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 创建积分活动 */
|
||||
function handleCreate() {
|
||||
formModalApi.setData(null).open();
|
||||
}
|
||||
|
||||
/** 编辑积分活动 */
|
||||
function handleEdit(row: MallPointActivityApi.PointActivity) {
|
||||
formModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 关闭积分活动 */
|
||||
function handleClose(row: MallPointActivityApi.PointActivity) {
|
||||
confirm({
|
||||
content: '确认关闭该积分商城活动吗?',
|
||||
}).then(async () => {
|
||||
await closePointActivity(row.id);
|
||||
ElMessage.success('关闭成功');
|
||||
onRefresh();
|
||||
});
|
||||
}
|
||||
|
||||
/** 删除积分活动 */
|
||||
async function handleDelete(row: MallPointActivityApi.PointActivity) {
|
||||
await deletePointActivity(row.id);
|
||||
onRefresh();
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getPointActivityPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: { code: 'query' },
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<MallPointActivityApi.PointActivity>,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<template #doc>
|
||||
<DocAlert
|
||||
title="【营销】积分商城活动"
|
||||
url="https://doc.iocoder.cn/mall/promotion-point/"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<FormModal @success="onRefresh" />
|
||||
|
||||
<Grid table-title="积分商城活动列表">
|
||||
<template #toolbar-tools>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('ui.actionTitle.create', ['积分活动']),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.ADD,
|
||||
auth: ['promotion:point-activity:create'],
|
||||
onClick: handleCreate,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<template #redeemedQuantity="{ row }">
|
||||
{{ getRedeemedQuantity(row) }}
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('common.edit'),
|
||||
type: 'primary',
|
||||
link: true,
|
||||
icon: ACTION_ICON.EDIT,
|
||||
auth: ['promotion:point-activity:update'],
|
||||
onClick: handleEdit.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: '关闭',
|
||||
type: 'danger',
|
||||
link: true,
|
||||
auth: ['promotion:point-activity:close'],
|
||||
ifShow: row.status === 0,
|
||||
onClick: handleClose.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: $t('common.delete'),
|
||||
type: 'danger',
|
||||
link: true,
|
||||
icon: ACTION_ICON.DELETE,
|
||||
auth: ['promotion:point-activity:delete'],
|
||||
ifShow: row.status !== 0,
|
||||
popConfirm: {
|
||||
title: $t('ui.actionMessage.deleteConfirm', [row.spuName]),
|
||||
confirm: handleDelete.bind(null, row),
|
||||
},
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,107 @@
|
|||
<script lang="ts" setup>
|
||||
import type { MallPointActivityApi } from '#/api/mall/promotion/point';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenForm, useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
import {
|
||||
createPointActivity,
|
||||
getPointActivity,
|
||||
updatePointActivity,
|
||||
} from '#/api/mall/promotion/point';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useFormSchema } from '../data';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const formData = ref<MallPointActivityApi.PointActivity>();
|
||||
const getTitle = computed(() => {
|
||||
return formData.value?.id
|
||||
? $t('ui.actionTitle.edit', ['积分活动'])
|
||||
: $t('ui.actionTitle.create', ['积分活动']);
|
||||
});
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
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 MallPointActivityApi.PointActivity;
|
||||
|
||||
// 确保必要的默认值
|
||||
if (!data.products) {
|
||||
data.products = [];
|
||||
}
|
||||
if (!data.sort) {
|
||||
data.sort = 0;
|
||||
}
|
||||
|
||||
try {
|
||||
await (formData.value?.id
|
||||
? updatePointActivity(data)
|
||||
: createPointActivity(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<MallPointActivityApi.PointActivity>();
|
||||
if (!data || !data.id) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
try {
|
||||
formData.value = await getPointActivity(data.id);
|
||||
// 设置到 values
|
||||
if (formData.value) {
|
||||
await formApi.setValues(formData.value);
|
||||
}
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal class="w-3/5" :title="getTitle">
|
||||
<div class="p-4">
|
||||
<div class="mb-4 rounded border border-yellow-200 bg-yellow-50 p-4">
|
||||
<p class="text-yellow-800">
|
||||
<strong>注意:</strong>
|
||||
积分活动涉及复杂的商品选择和SKU配置,当前为简化版本。
|
||||
完整的商品选择和积分配置功能需要在后续版本中完善。
|
||||
</p>
|
||||
</div>
|
||||
<Form />
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,166 @@
|
|||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
import { DICT_TYPE, getDictOptions } from '#/utils';
|
||||
|
||||
/** 表单配置 */
|
||||
export function useFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'id',
|
||||
component: 'Input',
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
show: () => false,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '活动名称',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入活动名称',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'startTime',
|
||||
label: '开始时间',
|
||||
component: 'DatePicker',
|
||||
componentProps: {
|
||||
placeholder: '请选择开始时间',
|
||||
showTime: true,
|
||||
valueFormat: 'x',
|
||||
format: 'YYYY-MM-DD HH:mm:ss',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'endTime',
|
||||
label: '结束时间',
|
||||
component: 'DatePicker',
|
||||
componentProps: {
|
||||
placeholder: '请选择结束时间',
|
||||
showTime: true,
|
||||
valueFormat: 'x',
|
||||
format: 'YYYY-MM-DD HH:mm:ss',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'conditionType',
|
||||
label: '条件类型',
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.PROMOTION_CONDITION_TYPE, 'number'),
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'productScope',
|
||||
label: '商品范围',
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.PROMOTION_PRODUCT_SCOPE, 'number'),
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'remark',
|
||||
label: '备注',
|
||||
component: 'Textarea',
|
||||
componentProps: {
|
||||
placeholder: '请输入备注',
|
||||
rows: 4,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '活动名称',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入活动名称',
|
||||
clearable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'status',
|
||||
label: '活动状态',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
placeholder: '请选择活动状态',
|
||||
clearable: true,
|
||||
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'createTime',
|
||||
label: '活动时间',
|
||||
component: 'RangePicker',
|
||||
componentProps: {
|
||||
placeholder: ['活动开始日期', '活动结束日期'],
|
||||
clearable: true,
|
||||
valueFormat: 'YYYY-MM-DD HH:mm:ss',
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'name',
|
||||
title: '活动名称',
|
||||
minWidth: 140,
|
||||
},
|
||||
{
|
||||
field: 'productScope',
|
||||
title: '活动范围',
|
||||
minWidth: 100,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.PROMOTION_PRODUCT_SCOPE },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'startTime',
|
||||
title: '活动开始时间',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'endTime',
|
||||
title: '活动结束时间',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
title: '状态',
|
||||
minWidth: 100,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.COMMON_STATUS },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '创建时间',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 180,
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -0,0 +1,173 @@
|
|||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { MallRewardActivityApi } from '#/api/mall/promotion/reward/rewardActivity';
|
||||
|
||||
import { confirm, DocAlert, Page, useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { ElLoading, ElMessage } from 'element-plus';
|
||||
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
closeRewardActivity,
|
||||
deleteRewardActivity,
|
||||
getRewardActivityPage,
|
||||
} from '#/api/mall/promotion/reward/rewardActivity';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useGridColumns, useGridFormSchema } from './data';
|
||||
import RewardActivityForm from './modules/form.vue';
|
||||
|
||||
defineOptions({ name: 'PromotionRewardActivity' });
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: RewardActivityForm,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 刷新表格 */
|
||||
function onRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 创建满减送活动 */
|
||||
function handleCreate() {
|
||||
formModalApi.setData(null).open();
|
||||
}
|
||||
|
||||
/** 编辑满减送活动 */
|
||||
function handleEdit(row: MallRewardActivityApi.RewardActivity) {
|
||||
formModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 关闭活动 */
|
||||
async function handleClose(row: MallRewardActivityApi.RewardActivity) {
|
||||
try {
|
||||
await confirm({
|
||||
content: '确认关闭该满减送活动吗?',
|
||||
});
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
const loadingInstance = ElLoading.service({
|
||||
text: '正在关闭中',
|
||||
fullscreen: true,
|
||||
});
|
||||
try {
|
||||
await closeRewardActivity(row.id as number);
|
||||
ElMessage.success('关闭成功');
|
||||
onRefresh();
|
||||
} finally {
|
||||
loadingInstance.close();
|
||||
}
|
||||
}
|
||||
|
||||
/** 删除活动 */
|
||||
async function handleDelete(row: MallRewardActivityApi.RewardActivity) {
|
||||
const loadingInstance = ElLoading.service({
|
||||
text: $t('ui.actionMessage.deleting', [row.name]),
|
||||
fullscreen: true,
|
||||
});
|
||||
try {
|
||||
await deleteRewardActivity(row.id as number);
|
||||
ElMessage.success($t('ui.actionMessage.deleteSuccess', [row.name]));
|
||||
onRefresh();
|
||||
} finally {
|
||||
loadingInstance.close();
|
||||
}
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getRewardActivityPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: { code: 'query' },
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<MallRewardActivityApi.RewardActivity>,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<template #doc>
|
||||
<DocAlert
|
||||
title="【营销】满减送"
|
||||
url="https://doc.iocoder.cn/mall/promotion-record/"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<FormModal @success="onRefresh" />
|
||||
|
||||
<Grid table-title="满减送活动列表">
|
||||
<template #toolbar-tools>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('ui.actionTitle.create', ['满减送活动']),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.ADD,
|
||||
auth: ['promotion:reward-activity:create'],
|
||||
onClick: handleCreate,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('common.edit'),
|
||||
type: 'primary',
|
||||
link: true,
|
||||
icon: ACTION_ICON.EDIT,
|
||||
auth: ['promotion:reward-activity:update'],
|
||||
onClick: handleEdit.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: '关闭',
|
||||
type: 'danger',
|
||||
link: true,
|
||||
icon: ACTION_ICON.DELETE,
|
||||
auth: ['promotion:reward-activity:close'],
|
||||
ifShow: row.status === 0,
|
||||
onClick: handleClose.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: $t('common.delete'),
|
||||
type: 'danger',
|
||||
link: true,
|
||||
icon: ACTION_ICON.DELETE,
|
||||
auth: ['promotion:reward-activity:delete'],
|
||||
ifShow: row.status !== 0,
|
||||
popConfirm: {
|
||||
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
|
||||
confirm: handleDelete.bind(null, row),
|
||||
},
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
<script lang="ts" setup>
|
||||
import type { MallRewardActivityApi } from '#/api/mall/promotion/reward/rewardActivity';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenForm, useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
import {
|
||||
createRewardActivity,
|
||||
getReward,
|
||||
updateRewardActivity,
|
||||
} from '#/api/mall/promotion/reward/rewardActivity';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useFormSchema } from '../data';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const formData = ref<MallRewardActivityApi.RewardActivity>();
|
||||
const getTitle = computed(() => {
|
||||
return formData.value?.id
|
||||
? $t('ui.actionTitle.edit', ['满减送活动'])
|
||||
: $t('ui.actionTitle.create', ['满减送活动']);
|
||||
});
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
labelWidth: 100,
|
||||
},
|
||||
wrapperClass: 'grid-cols-2',
|
||||
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 MallRewardActivityApi.RewardActivity;
|
||||
|
||||
// 确保必要的默认值
|
||||
if (!data.rules) {
|
||||
data.rules = [];
|
||||
}
|
||||
|
||||
try {
|
||||
await (formData.value?.id
|
||||
? updateRewardActivity(data)
|
||||
: createRewardActivity(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<MallRewardActivityApi.RewardActivity>();
|
||||
if (!data || !data.id) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
try {
|
||||
formData.value = await getReward(data.id as number);
|
||||
// 设置到 values
|
||||
if (formData.value) {
|
||||
await formApi.setValues(formData.value);
|
||||
}
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal class="w-4/5" :title="getTitle">
|
||||
<Form />
|
||||
|
||||
<!-- 简化说明 -->
|
||||
<div class="mt-4 rounded bg-blue-50 p-4">
|
||||
<p class="text-sm text-blue-600">
|
||||
<strong>说明:</strong> 当前为简化版本的满减送活动表单。
|
||||
复杂的商品选择、优惠规则配置等功能已简化,仅保留基础字段配置。
|
||||
如需完整功能,请参考原始 Element UI 版本的实现。
|
||||
</p>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,126 @@
|
|||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
import { DICT_TYPE, getDictOptions } from '#/utils';
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '活动名称',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入活动名称',
|
||||
clearable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'status',
|
||||
label: '活动状态',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
placeholder: '请选择活动状态',
|
||||
clearable: true,
|
||||
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'id',
|
||||
title: '活动编号',
|
||||
minWidth: 80,
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
title: '活动名称',
|
||||
minWidth: 140,
|
||||
},
|
||||
{
|
||||
field: 'configIds',
|
||||
title: '秒杀时段',
|
||||
minWidth: 220,
|
||||
slots: { default: 'configIds' },
|
||||
},
|
||||
{
|
||||
field: 'startTime',
|
||||
title: '活动时间',
|
||||
minWidth: 210,
|
||||
slots: { default: 'timeRange' },
|
||||
},
|
||||
{
|
||||
field: 'picUrl',
|
||||
title: '商品图片',
|
||||
minWidth: 80,
|
||||
cellRender: {
|
||||
name: 'CellImage',
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'spuName',
|
||||
title: '商品标题',
|
||||
minWidth: 300,
|
||||
},
|
||||
{
|
||||
field: 'marketPrice',
|
||||
title: '原价',
|
||||
minWidth: 100,
|
||||
formatter: ({ row }) => `¥${(row.marketPrice / 100).toFixed(2)}`,
|
||||
},
|
||||
{
|
||||
field: 'seckillPrice',
|
||||
title: '秒杀价',
|
||||
minWidth: 100,
|
||||
formatter: ({ row }) => {
|
||||
if (!(row.products || row.products.length === 0)) {
|
||||
return '¥0.00';
|
||||
}
|
||||
const seckillPrice = Math.min(
|
||||
...row.products.map((item: any) => item.seckillPrice),
|
||||
);
|
||||
return `¥${(seckillPrice / 100).toFixed(2)}`;
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
title: '活动状态',
|
||||
align: 'center',
|
||||
minWidth: 100,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.COMMON_STATUS },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'stock',
|
||||
title: '库存',
|
||||
align: 'center',
|
||||
minWidth: 80,
|
||||
},
|
||||
{
|
||||
field: 'totalStock',
|
||||
title: '总库存',
|
||||
align: 'center',
|
||||
minWidth: 80,
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '创建时间',
|
||||
align: 'center',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
align: 'center',
|
||||
width: 150,
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
import { formatDate } from '@vben/utils';
|
||||
|
||||
// 全局变量,用于存储配置列表
|
||||
let configList: any[] = [];
|
||||
|
||||
/** 设置配置列表 */
|
||||
export function setConfigList(list: any[]) {
|
||||
configList = list;
|
||||
}
|
||||
|
||||
/** 格式化配置名称 */
|
||||
export function formatConfigNames(configId: number): string {
|
||||
const config = configList.find((item) => item.id === configId);
|
||||
return config === null || config === undefined
|
||||
? ''
|
||||
: `${config.name}[${config.startTime} ~ ${config.endTime}]`;
|
||||
}
|
||||
|
||||
/** 格式化秒杀价格 */
|
||||
export function formatSeckillPrice(products: any[]): string {
|
||||
if (!products || products.length === 0) {
|
||||
return '¥0.00';
|
||||
}
|
||||
const seckillPrice = Math.min(...products.map((item) => item.seckillPrice));
|
||||
return `¥${(seckillPrice / 100).toFixed(2)}`;
|
||||
}
|
||||
|
||||
/** 格式化活动时间范围 */
|
||||
export function formatTimeRange(
|
||||
startTime: Date | string,
|
||||
endTime: Date | string,
|
||||
): string {
|
||||
return `${formatDate(startTime, 'YYYY-MM-DD')} ~ ${formatDate(endTime, 'YYYY-MM-DD')}`;
|
||||
}
|
||||
|
|
@ -0,0 +1,193 @@
|
|||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { MallSeckillActivityApi } from '#/api/mall/promotion/seckill/seckillActivity';
|
||||
|
||||
import { onMounted } from 'vue';
|
||||
|
||||
import { DocAlert, Page, useVbenModal } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { ElLoading, ElMessage, ElTag } from 'element-plus';
|
||||
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
closeSeckillActivity,
|
||||
deleteSeckillActivity,
|
||||
getSeckillActivityPage,
|
||||
} from '#/api/mall/promotion/seckill/seckillActivity';
|
||||
import { getSimpleSeckillConfigList } from '#/api/mall/promotion/seckill/seckillConfig';
|
||||
|
||||
import { useGridColumns, useGridFormSchema } from './data';
|
||||
import { formatConfigNames, formatTimeRange, setConfigList } from './formatter';
|
||||
import Form from './modules/form.vue';
|
||||
|
||||
defineOptions({ name: 'SeckillActivity' });
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: Form,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 刷新表格 */
|
||||
function onRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 编辑活动 */
|
||||
function handleEdit(row: MallSeckillActivityApi.SeckillActivity) {
|
||||
formModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 创建活动 */
|
||||
function handleCreate() {
|
||||
formModalApi.setData(null).open();
|
||||
}
|
||||
|
||||
/** 关闭活动 */
|
||||
async function handleClose(row: MallSeckillActivityApi.SeckillActivity) {
|
||||
const loadingInstance = ElLoading.service({
|
||||
text: $t('ui.actionMessage.closing', [row.name]),
|
||||
fullscreen: true,
|
||||
});
|
||||
try {
|
||||
await closeSeckillActivity(row.id as number);
|
||||
ElMessage.success('关闭成功');
|
||||
onRefresh();
|
||||
} finally {
|
||||
loadingInstance.close();
|
||||
}
|
||||
}
|
||||
|
||||
/** 删除活动 */
|
||||
async function handleDelete(row: MallSeckillActivityApi.SeckillActivity) {
|
||||
const loadingInstance = ElLoading.service({
|
||||
text: $t('ui.actionMessage.deleting', [row.name]),
|
||||
fullscreen: true,
|
||||
});
|
||||
try {
|
||||
await deleteSeckillActivity(row.id as number);
|
||||
ElMessage.success($t('ui.actionMessage.deleteSuccess', [row.name]));
|
||||
onRefresh();
|
||||
} finally {
|
||||
loadingInstance.close();
|
||||
}
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getSeckillActivityPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: { code: 'query' },
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<MallSeckillActivityApi.SeckillActivity>,
|
||||
});
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(async () => {
|
||||
// 获得秒杀时间段配置
|
||||
const configList = await getSimpleSeckillConfigList();
|
||||
setConfigList(configList);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<template #doc>
|
||||
<DocAlert
|
||||
title="【营销】秒杀活动"
|
||||
url="https://doc.iocoder.cn/mall/promotion-seckill/"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<FormModal @success="onRefresh" />
|
||||
<Grid table-title="秒杀活动列表">
|
||||
<template #toolbar-tools>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('ui.actionTitle.create', ['秒杀活动']),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.ADD,
|
||||
auth: ['promotion:seckill-activity:create'],
|
||||
onClick: handleCreate,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #configIds="{ row }">
|
||||
<div class="flex flex-wrap gap-1">
|
||||
<ElTag
|
||||
v-for="(configId, index) in row.configIds"
|
||||
:key="index"
|
||||
class="mr-1"
|
||||
>
|
||||
{{ formatConfigNames(configId) }}
|
||||
</ElTag>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #timeRange="{ row }">
|
||||
{{ formatTimeRange(row.startTime, row.endTime) }}
|
||||
</template>
|
||||
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('common.edit'),
|
||||
type: 'primary',
|
||||
link: true,
|
||||
icon: ACTION_ICON.EDIT,
|
||||
auth: ['promotion:seckill-activity:update'],
|
||||
onClick: handleEdit.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: '关闭',
|
||||
type: 'danger',
|
||||
link: true,
|
||||
auth: ['promotion:seckill-activity:close'],
|
||||
ifShow: row.status === 0,
|
||||
popConfirm: {
|
||||
title: '确认关闭该秒杀活动吗?',
|
||||
confirm: handleClose.bind(null, row),
|
||||
},
|
||||
},
|
||||
{
|
||||
label: $t('common.delete'),
|
||||
type: 'danger',
|
||||
link: true,
|
||||
auth: ['promotion:seckill-activity:delete'],
|
||||
ifShow: row.status !== 0,
|
||||
popConfirm: {
|
||||
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
|
||||
confirm: handleDelete.bind(null, row),
|
||||
},
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,121 @@
|
|||
<script lang="ts" setup>
|
||||
import type { MallSeckillActivityApi } from '#/api/mall/promotion/seckill/seckillActivity';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import {
|
||||
createSeckillActivity,
|
||||
getSeckillActivity,
|
||||
updateSeckillActivity,
|
||||
} from '#/api/mall/promotion/seckill/seckillActivity';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const formData = ref<MallSeckillActivityApi.SeckillActivity>();
|
||||
const getTitle = computed(() => {
|
||||
return formData.value?.id
|
||||
? $t('ui.actionTitle.edit', ['秒杀活动'])
|
||||
: $t('ui.actionTitle.create', ['秒杀活动']);
|
||||
});
|
||||
|
||||
// 简化的表单配置,实际项目中应该有完整的字段配置
|
||||
const formSchema = [
|
||||
{
|
||||
fieldName: 'id',
|
||||
component: 'Input',
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
show: () => false,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '活动名称',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入活动名称',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'status',
|
||||
label: '活动状态',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
placeholder: '请选择活动状态',
|
||||
options: [
|
||||
{ label: '开启', value: 0 },
|
||||
{ label: '关闭', value: 1 },
|
||||
],
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
];
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
formItemClass: 'col-span-2',
|
||||
labelWidth: 80,
|
||||
},
|
||||
layout: 'horizontal',
|
||||
schema: formSchema,
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
// 提交表单
|
||||
const data =
|
||||
(await formApi.getValues()) as MallSeckillActivityApi.SeckillActivity;
|
||||
try {
|
||||
await (formData.value?.id
|
||||
? updateSeckillActivity(data)
|
||||
: createSeckillActivity(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<MallSeckillActivityApi.SeckillActivity>();
|
||||
if (!data || !data.id) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
try {
|
||||
formData.value = await getSeckillActivity(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,151 @@
|
|||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { MallSeckillConfigApi } from '#/api/mall/promotion/seckill/seckillConfig';
|
||||
|
||||
import { DICT_TYPE, getDictOptions } from '#/utils';
|
||||
|
||||
/** 新增/修改的表单 */
|
||||
export function useFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'id',
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
show: () => false,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '秒杀时段名称',
|
||||
component: 'Input',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'startTime',
|
||||
label: '开始时间点',
|
||||
component: 'TimePicker',
|
||||
componentProps: {
|
||||
format: 'HH:mm',
|
||||
valueFormat: 'HH:mm',
|
||||
placeholder: '请选择开始时间点',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'endTime',
|
||||
label: '结束时间点',
|
||||
component: 'TimePicker',
|
||||
componentProps: {
|
||||
format: 'HH:mm',
|
||||
valueFormat: 'HH:mm',
|
||||
placeholder: '请选择结束时间点',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'sliderPicUrls',
|
||||
label: '秒杀轮播图',
|
||||
component: 'ImageUpload',
|
||||
componentProps: {
|
||||
multiple: true,
|
||||
maxNumber: 5,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'status',
|
||||
label: '状态',
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
|
||||
buttonStyle: 'solid',
|
||||
optionType: 'button',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '秒杀时段名称',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入秒杀时段名称',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'status',
|
||||
label: '状态',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
placeholder: '请选择状态',
|
||||
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 表格列配置 */
|
||||
export function useGridColumns<T = MallSeckillConfigApi.SeckillConfig>(
|
||||
onStatusChange?: (
|
||||
newStatus: number,
|
||||
row: T,
|
||||
) => PromiseLike<boolean | undefined>,
|
||||
): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
title: '秒杀时段名称',
|
||||
field: 'name',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
title: '开始时间点',
|
||||
field: 'startTime',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
title: '结束时间点',
|
||||
field: 'endTime',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
title: '秒杀轮播图',
|
||||
field: 'sliderPicUrls',
|
||||
minWidth: 100,
|
||||
cellRender: {
|
||||
name: 'CellImages',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '活动状态',
|
||||
field: 'status',
|
||||
minWidth: 100,
|
||||
cellRender: {
|
||||
attrs: { beforeChange: onStatusChange },
|
||||
name: 'CellSwitch',
|
||||
props: {
|
||||
checkedValue: 1,
|
||||
checkedChildren: '启用',
|
||||
unCheckedValue: 0,
|
||||
unCheckedChildren: '禁用',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
field: 'createTime',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 180,
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -0,0 +1,157 @@
|
|||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { MallSeckillConfigApi } from '#/api/mall/promotion/seckill/seckillConfig';
|
||||
|
||||
import { confirm, Page, useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { ElLoading, ElMessage } from 'element-plus';
|
||||
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
deleteSeckillConfig,
|
||||
getSeckillConfigPage,
|
||||
updateSeckillConfigStatus,
|
||||
} from '#/api/mall/promotion/seckill/seckillConfig';
|
||||
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: MallSeckillConfigApi.SeckillConfig) {
|
||||
formModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 删除秒杀时段 */
|
||||
async function handleDelete(row: MallSeckillConfigApi.SeckillConfig) {
|
||||
const loadingInstance = ElLoading.service({
|
||||
text: $t('ui.actionMessage.deleting', [row.name]),
|
||||
fullscreen: true,
|
||||
});
|
||||
try {
|
||||
await deleteSeckillConfig(row.id as number);
|
||||
ElMessage.success($t('ui.actionMessage.deleteSuccess', [row.name]));
|
||||
onRefresh();
|
||||
} finally {
|
||||
loadingInstance.close();
|
||||
}
|
||||
}
|
||||
|
||||
/** 修改状态 */
|
||||
async function handleStatusChange(
|
||||
newStatus: number,
|
||||
row: MallSeckillConfigApi.SeckillConfig,
|
||||
): Promise<boolean | undefined> {
|
||||
return new Promise((resolve, reject) => {
|
||||
// 二次确认
|
||||
const text = row.status === 0 ? '启用' : '停用';
|
||||
confirm({
|
||||
content: `确认要${text + row.name}吗?`,
|
||||
})
|
||||
.then(async () => {
|
||||
// 更新状态
|
||||
const res = await updateSeckillConfigStatus(row.id, newStatus);
|
||||
if (res) {
|
||||
// 提示并返回成功
|
||||
ElMessage.success(`${text}成功`);
|
||||
resolve(true);
|
||||
} else {
|
||||
reject(new Error('操作失败'));
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
reject(new Error('取消操作'));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(handleStatusChange),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getSeckillConfigPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: { code: 'query' },
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<MallSeckillConfigApi.SeckillConfig>,
|
||||
});
|
||||
</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: ['promotion:seckill-config:create'],
|
||||
onClick: handleCreate,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('common.edit'),
|
||||
type: 'primary',
|
||||
link: true,
|
||||
icon: ACTION_ICON.EDIT,
|
||||
auth: ['promotion:seckill-config:update'],
|
||||
onClick: handleEdit.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: $t('common.delete'),
|
||||
type: 'danger',
|
||||
link: true,
|
||||
icon: ACTION_ICON.DELETE,
|
||||
auth: ['promotion:seckill-config:delete'],
|
||||
popConfirm: {
|
||||
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
|
||||
confirm: handleDelete.bind(null, row),
|
||||
},
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
<script lang="ts" setup>
|
||||
import type { MallSeckillConfigApi } from '#/api/mall/promotion/seckill/seckillConfig';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import {
|
||||
createSeckillConfig,
|
||||
getSeckillConfig,
|
||||
updateSeckillConfig,
|
||||
} from '#/api/mall/promotion/seckill/seckillConfig';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useFormSchema } from '../data';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
|
||||
const formData = ref<MallSeckillConfigApi.SeckillConfig>();
|
||||
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: 100,
|
||||
},
|
||||
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 MallSeckillConfigApi.SeckillConfig;
|
||||
try {
|
||||
await (formData.value?.id
|
||||
? updateSeckillConfig(data)
|
||||
: createSeckillConfig(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<MallSeckillConfigApi.SeckillConfig>();
|
||||
if (!data || !data.id) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
try {
|
||||
formData.value = await getSeckillConfig(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,34 @@
|
|||
<script lang="ts" setup>
|
||||
import { DocAlert, Page } from '@vben/common-ui';
|
||||
|
||||
import { ElButton } from 'element-plus';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page>
|
||||
<DocAlert
|
||||
title="【统计】会员、商品、交易统计"
|
||||
url="https://doc.iocoder.cn/mall/statistics/"
|
||||
/>
|
||||
<ElButton
|
||||
danger
|
||||
type="primary"
|
||||
link
|
||||
target="_blank"
|
||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
|
||||
>
|
||||
该功能支持 Vue3 + element-plus 版本!
|
||||
</ElButton>
|
||||
<br />
|
||||
<ElButton
|
||||
type="primary"
|
||||
link
|
||||
target="_blank"
|
||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/mall/statistics/member/index"
|
||||
>
|
||||
可参考
|
||||
https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/mall/statistics/member/index
|
||||
代码,pull request 贡献给我们!
|
||||
</ElButton>
|
||||
</Page>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
<script lang="ts" setup>
|
||||
import { DocAlert, Page } from '@vben/common-ui';
|
||||
|
||||
import { ElButton } from 'element-plus';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page>
|
||||
<DocAlert
|
||||
title="【统计】会员、商品、交易统计"
|
||||
url="https://doc.iocoder.cn/mall/statistics/"
|
||||
/>
|
||||
<ElButton
|
||||
danger
|
||||
type="primary"
|
||||
link
|
||||
target="_blank"
|
||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
|
||||
>
|
||||
该功能支持 Vue3 + element-plus 版本!
|
||||
</ElButton>
|
||||
<br />
|
||||
<ElButton
|
||||
type="primary"
|
||||
link
|
||||
target="_blank"
|
||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/mall/statistics/product/index"
|
||||
>
|
||||
可参考
|
||||
https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/mall/statistics/product/index
|
||||
代码,pull request 贡献给我们!
|
||||
</ElButton>
|
||||
</Page>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
<script lang="ts" setup>
|
||||
import { DocAlert, Page } from '@vben/common-ui';
|
||||
|
||||
import { ElButton } from 'element-plus';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page>
|
||||
<DocAlert
|
||||
title="【统计】会员、商品、交易统计"
|
||||
url="https://doc.iocoder.cn/mall/statistics/"
|
||||
/>
|
||||
<ElButton
|
||||
danger
|
||||
type="primary"
|
||||
link
|
||||
target="_blank"
|
||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
|
||||
>
|
||||
该功能支持 Vue3 + element-plus 版本!
|
||||
</ElButton>
|
||||
<br />
|
||||
<ElButton
|
||||
type="primary"
|
||||
link
|
||||
target="_blank"
|
||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/mall/statistics/trade/index"
|
||||
>
|
||||
可参考
|
||||
https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/mall/statistics/trade/index
|
||||
代码,pull request 贡献给我们!
|
||||
</ElButton>
|
||||
</Page>
|
||||
</template>
|
||||
|
|
@ -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' },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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' },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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' },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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: '商城开通提现的付款方式',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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' },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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' },
|
||||
},
|
||||
];
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue