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