@@ -29,12 +29,14 @@ export namespace MallBrokerageUserApi {
|
|||||||
export interface CreateRequest {
|
export interface CreateRequest {
|
||||||
/** 用户编号 */
|
/** 用户编号 */
|
||||||
userId: number;
|
userId: number;
|
||||||
|
/** 推广员编号 */
|
||||||
|
bindUserId: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 修改推广员请求 */
|
/** 修改推广员请求 */
|
||||||
export interface UpdateBindUserRequest {
|
export interface UpdateBindUserRequest {
|
||||||
/** 用户编号 */
|
/** 用户编号 */
|
||||||
userId: number;
|
id: number;
|
||||||
/** 推广员编号 */
|
/** 推广员编号 */
|
||||||
bindUserId: number;
|
bindUserId: number;
|
||||||
}
|
}
|
||||||
@@ -42,15 +44,15 @@ export namespace MallBrokerageUserApi {
|
|||||||
/** 清除推广员请求 */
|
/** 清除推广员请求 */
|
||||||
export interface ClearBindUserRequest {
|
export interface ClearBindUserRequest {
|
||||||
/** 用户编号 */
|
/** 用户编号 */
|
||||||
userId: number;
|
id: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 修改推广资格请求 */
|
/** 修改推广资格请求 */
|
||||||
export interface UpdateBrokerageEnabledRequest {
|
export interface UpdateBrokerageEnabledRequest {
|
||||||
/** 用户编号 */
|
/** 用户编号 */
|
||||||
userId: number;
|
id: number;
|
||||||
/** 是否启用分销 */
|
/** 是否启用分销 */
|
||||||
brokerageEnabled: boolean;
|
enabled: boolean;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,13 +28,10 @@ const props = withDefaults(defineProps<CropperAvatarProps>(), {
|
|||||||
const emit = defineEmits(['update:value', 'change']);
|
const emit = defineEmits(['update:value', 'change']);
|
||||||
|
|
||||||
const sourceValue = ref(props.value || '');
|
const sourceValue = ref(props.value || '');
|
||||||
const prefixCls = 'cropper-avatar';
|
|
||||||
const [CropperModal, modalApi] = useVbenModal({
|
const [CropperModal, modalApi] = useVbenModal({
|
||||||
connectedComponent: cropperModal,
|
connectedComponent: cropperModal,
|
||||||
});
|
});
|
||||||
|
|
||||||
const getClass = computed(() => [prefixCls]);
|
|
||||||
|
|
||||||
const getWidth = computed(() => `${`${props.width}`.replace(/px/, '')}px`);
|
const getWidth = computed(() => `${`${props.width}`.replace(/px/, '')}px`);
|
||||||
|
|
||||||
const getIconWidth = computed(
|
const getIconWidth = computed(
|
||||||
@@ -74,29 +71,42 @@ defineExpose({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div :class="getClass" :style="getStyle">
|
<!-- 头像容器 -->
|
||||||
|
<div class="inline-block text-center" :style="getStyle">
|
||||||
|
<!-- 图片包装器 -->
|
||||||
<div
|
<div
|
||||||
:class="`${prefixCls}-image-wrapper`"
|
class="group relative cursor-pointer overflow-hidden rounded-full border border-gray-200 bg-white"
|
||||||
:style="getImageWrapperStyle"
|
:style="getImageWrapperStyle"
|
||||||
@click="openModal"
|
@click="openModal"
|
||||||
>
|
>
|
||||||
<div :class="`${prefixCls}-image-mask`" :style="getImageWrapperStyle">
|
<!-- 遮罩层 -->
|
||||||
|
<div
|
||||||
|
class="duration-400 absolute inset-0 flex cursor-pointer items-center justify-center rounded-full bg-black bg-opacity-40 opacity-0 transition-opacity group-hover:opacity-100"
|
||||||
|
:style="getImageWrapperStyle"
|
||||||
|
>
|
||||||
<IconifyIcon
|
<IconifyIcon
|
||||||
icon="lucide:cloud-upload"
|
icon="lucide:cloud-upload"
|
||||||
class="text-gray-400"
|
class="m-auto text-gray-400"
|
||||||
:style="{
|
:style="{
|
||||||
...getImageWrapperStyle,
|
...getImageWrapperStyle,
|
||||||
width: `${getIconWidth}`,
|
width: getIconWidth,
|
||||||
height: `${getIconWidth}`,
|
height: getIconWidth,
|
||||||
lineHeight: `${getIconWidth}`,
|
lineHeight: getIconWidth,
|
||||||
}"
|
}"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<img v-if="sourceValue" :src="sourceValue" alt="avatar" />
|
<!-- 头像图片 -->
|
||||||
|
<img
|
||||||
|
v-if="sourceValue"
|
||||||
|
:src="sourceValue"
|
||||||
|
alt="avatar"
|
||||||
|
class="h-full w-full object-cover"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- 上传按钮 -->
|
||||||
<Button
|
<Button
|
||||||
v-if="showBtn"
|
v-if="showBtn"
|
||||||
:class="`${prefixCls}-upload-btn`"
|
class="mx-auto mt-2"
|
||||||
@click="openModal"
|
@click="openModal"
|
||||||
v-bind="btnProps"
|
v-bind="btnProps"
|
||||||
>
|
>
|
||||||
@@ -111,49 +121,3 @@ defineExpose({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.cropper-avatar {
|
|
||||||
display: inline-block;
|
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
&-image-wrapper {
|
|
||||||
overflow: hidden;
|
|
||||||
cursor: pointer;
|
|
||||||
background: #fff;
|
|
||||||
border: 1px solid #eee;
|
|
||||||
border-radius: 50%;
|
|
||||||
|
|
||||||
img {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&-image-mask {
|
|
||||||
position: absolute;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: inherit;
|
|
||||||
height: inherit;
|
|
||||||
cursor: pointer;
|
|
||||||
background: rgb(0 0 0 / 40%);
|
|
||||||
border: inherit;
|
|
||||||
border-radius: inherit;
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 0.4s;
|
|
||||||
|
|
||||||
::v-deep(svg) {
|
|
||||||
margin: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&-image-mask:hover {
|
|
||||||
opacity: 40;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-upload-btn {
|
|
||||||
margin: 10px auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -37,13 +37,20 @@ const cropper = ref<CropperType>();
|
|||||||
let scaleX = 1;
|
let scaleX = 1;
|
||||||
let scaleY = 1;
|
let scaleY = 1;
|
||||||
|
|
||||||
const prefixCls = 'cropper-am';
|
|
||||||
const [Modal, modalApi] = useVbenModal({
|
const [Modal, modalApi] = useVbenModal({
|
||||||
onConfirm: handleOk,
|
onConfirm: handleOk,
|
||||||
onOpenChange(isOpen) {
|
onOpenChange(isOpen) {
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
// 打开时,进行 loading 加载。后续 CropperImage 组件加载完毕,会自动关闭 loading(通过 handleReady)
|
// 打开时,进行 loading 加载。后续 CropperImage 组件加载完毕,会自动关闭 loading(通过 handleReady)
|
||||||
modalLoading(true);
|
modalLoading(true);
|
||||||
|
const img = new Image();
|
||||||
|
img.src = src.value;
|
||||||
|
img.addEventListener('load', () => {
|
||||||
|
modalLoading(false);
|
||||||
|
});
|
||||||
|
img.addEventListener('error', () => {
|
||||||
|
modalLoading(false);
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
// 关闭时,清空右侧预览
|
// 关闭时,清空右侧预览
|
||||||
previewSource.value = '';
|
previewSource.value = '';
|
||||||
@@ -121,9 +128,13 @@ async function handleOk() {
|
|||||||
:title="$t('ui.cropper.modalTitle')"
|
:title="$t('ui.cropper.modalTitle')"
|
||||||
class="w-2/3"
|
class="w-2/3"
|
||||||
>
|
>
|
||||||
<div :class="prefixCls">
|
<div class="flex h-96">
|
||||||
<div :class="`${prefixCls}-left`" class="w-full">
|
<!-- 左侧区域 -->
|
||||||
<div :class="`${prefixCls}-cropper`">
|
<div class="h-full w-3/5">
|
||||||
|
<!-- 裁剪器容器 -->
|
||||||
|
<div
|
||||||
|
class="relative h-[300px] bg-gradient-to-b from-neutral-50 to-neutral-200"
|
||||||
|
>
|
||||||
<CropperImage
|
<CropperImage
|
||||||
v-if="src"
|
v-if="src"
|
||||||
:circled="circled"
|
:circled="circled"
|
||||||
@@ -134,7 +145,8 @@ async function handleOk() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div :class="`${prefixCls}-toolbar`">
|
<!-- 工具栏 -->
|
||||||
|
<div class="mt-4 flex items-center justify-between">
|
||||||
<Upload
|
<Upload
|
||||||
:before-upload="handleBeforeUpload"
|
:before-upload="handleBeforeUpload"
|
||||||
:file-list="[]"
|
:file-list="[]"
|
||||||
@@ -208,7 +220,7 @@ async function handleOk() {
|
|||||||
>
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<div class="flex items-center justify-center">
|
<div class="flex items-center justify-center">
|
||||||
<IconifyIcon icon="vaadin--arrows-long-h" />
|
<IconifyIcon icon="vaadin:arrows-long-h" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</Button>
|
</Button>
|
||||||
@@ -258,16 +270,26 @@ async function handleOk() {
|
|||||||
</Space>
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div :class="`${prefixCls}-right`">
|
|
||||||
<div :class="`${prefixCls}-preview`">
|
<!-- 右侧区域 -->
|
||||||
|
<div class="h-full w-2/5">
|
||||||
|
<!-- 预览区域 -->
|
||||||
|
<div
|
||||||
|
class="mx-auto h-56 w-56 overflow-hidden rounded-full border border-gray-200"
|
||||||
|
>
|
||||||
<img
|
<img
|
||||||
v-if="previewSource"
|
v-if="previewSource"
|
||||||
:alt="$t('ui.cropper.preview')"
|
:alt="$t('ui.cropper.preview')"
|
||||||
:src="previewSource"
|
:src="previewSource"
|
||||||
|
class="h-full w-full object-cover"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 头像组合预览 -->
|
||||||
<template v-if="previewSource">
|
<template v-if="previewSource">
|
||||||
<div :class="`${prefixCls}-group`">
|
<div
|
||||||
|
class="mt-2 flex items-center justify-around border-t border-gray-200 pt-2"
|
||||||
|
>
|
||||||
<Avatar :src="previewSource" size="large" />
|
<Avatar :src="previewSource" size="large" />
|
||||||
<Avatar :size="48" :src="previewSource" />
|
<Avatar :size="48" :src="previewSource" />
|
||||||
<Avatar :size="64" :src="previewSource" />
|
<Avatar :size="64" :src="previewSource" />
|
||||||
@@ -278,76 +300,3 @@ async function handleOk() {
|
|||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.cropper-am {
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
&-left,
|
|
||||||
&-right {
|
|
||||||
height: 340px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-left {
|
|
||||||
width: 55%;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-right {
|
|
||||||
width: 45%;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-cropper {
|
|
||||||
height: 300px;
|
|
||||||
background: #eee;
|
|
||||||
background-image:
|
|
||||||
linear-gradient(
|
|
||||||
45deg,
|
|
||||||
rgb(0 0 0 / 25%) 25%,
|
|
||||||
transparent 0,
|
|
||||||
transparent 75%,
|
|
||||||
rgb(0 0 0 / 25%) 0
|
|
||||||
),
|
|
||||||
linear-gradient(
|
|
||||||
45deg,
|
|
||||||
rgb(0 0 0 / 25%) 25%,
|
|
||||||
transparent 0,
|
|
||||||
transparent 75%,
|
|
||||||
rgb(0 0 0 / 25%) 0
|
|
||||||
);
|
|
||||||
background-position:
|
|
||||||
0 0,
|
|
||||||
12px 12px;
|
|
||||||
background-size: 24px 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-toolbar {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-preview {
|
|
||||||
width: 220px;
|
|
||||||
height: 220px;
|
|
||||||
margin: 0 auto;
|
|
||||||
overflow: hidden;
|
|
||||||
border: 1px solid #eee;
|
|
||||||
border-radius: 50%;
|
|
||||||
|
|
||||||
img {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&-group {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-around;
|
|
||||||
padding-top: 8px;
|
|
||||||
margin-top: 8px;
|
|
||||||
border-top: 1px solid #eee;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ const imgElRef = ref<ElRef<HTMLImageElement>>();
|
|||||||
const cropper = ref<Cropper | null>();
|
const cropper = ref<Cropper | null>();
|
||||||
const isReady = ref(false);
|
const isReady = ref(false);
|
||||||
|
|
||||||
const prefixCls = 'cropper-image';
|
|
||||||
const debounceRealTimeCropped = useDebounceFn(realTimeCropped, 80);
|
const debounceRealTimeCropped = useDebounceFn(realTimeCropped, 80);
|
||||||
|
|
||||||
const getImageStyle = computed((): CSSProperties => {
|
const getImageStyle = computed((): CSSProperties => {
|
||||||
@@ -46,10 +45,9 @@ const getImageStyle = computed((): CSSProperties => {
|
|||||||
|
|
||||||
const getClass = computed(() => {
|
const getClass = computed(() => {
|
||||||
return [
|
return [
|
||||||
prefixCls,
|
|
||||||
attrs.class,
|
attrs.class,
|
||||||
{
|
{
|
||||||
[`${prefixCls}--circled`]: props.circled,
|
'cropper-image--circled': props.circled,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
@@ -115,10 +113,9 @@ function cropped() {
|
|||||||
imgInfo,
|
imgInfo,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
// eslint-disable-next-line unicorn/prefer-add-event-listener
|
fileReader.addEventListener('error', () => {
|
||||||
fileReader.onerror = () => {
|
|
||||||
emit('cropendError');
|
emit('cropendError');
|
||||||
};
|
});
|
||||||
}, 'image/png');
|
}, 'image/png');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,6 +154,7 @@ function getRoundedCanvas() {
|
|||||||
:crossorigin="crossorigin"
|
:crossorigin="crossorigin"
|
||||||
:src="src"
|
:src="src"
|
||||||
:style="getImageStyle"
|
:style="getImageStyle"
|
||||||
|
class="h-auto max-w-full"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ const dictTag = computed(() => {
|
|||||||
return {
|
return {
|
||||||
label: dict.label || '',
|
label: dict.label || '',
|
||||||
colorType,
|
colorType,
|
||||||
|
cssClass: dict.cssClass,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
SelectOption,
|
SelectOption,
|
||||||
} from 'ant-design-vue';
|
} from 'ant-design-vue';
|
||||||
|
|
||||||
import { getDictObj, getIntDictOptions, getStrDictOptions } from '#/utils';
|
import { getDictOptions } from '#/utils';
|
||||||
|
|
||||||
defineOptions({ name: 'DictSelect' });
|
defineOptions({ name: 'DictSelect' });
|
||||||
|
|
||||||
@@ -25,17 +25,16 @@ const props = withDefaults(defineProps<DictSelectProps>(), {
|
|||||||
const attrs = useAttrs();
|
const attrs = useAttrs();
|
||||||
|
|
||||||
// 获得字典配置
|
// 获得字典配置
|
||||||
// TODO @dhb:可以使用 getDictOptions 替代么?
|
const getDictOption = computed(() => {
|
||||||
const getDictOptions = computed(() => {
|
|
||||||
switch (props.valueType) {
|
switch (props.valueType) {
|
||||||
case 'bool': {
|
case 'bool': {
|
||||||
return getDictObj(props.dictType, 'bool');
|
return getDictOptions(props.dictType, 'boolean');
|
||||||
}
|
}
|
||||||
case 'int': {
|
case 'int': {
|
||||||
return getIntDictOptions(props.dictType);
|
return getDictOptions(props.dictType, 'number');
|
||||||
}
|
}
|
||||||
case 'str': {
|
case 'str': {
|
||||||
return getStrDictOptions(props.dictType);
|
return getDictOptions(props.dictType, 'string');
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
return [];
|
return [];
|
||||||
@@ -47,7 +46,7 @@ const getDictOptions = computed(() => {
|
|||||||
<template>
|
<template>
|
||||||
<Select v-if="selectType === 'select'" class="w-full" v-bind="attrs">
|
<Select v-if="selectType === 'select'" class="w-full" v-bind="attrs">
|
||||||
<SelectOption
|
<SelectOption
|
||||||
v-for="(dict, index) in getDictOptions"
|
v-for="(dict, index) in getDictOption"
|
||||||
:key="index"
|
:key="index"
|
||||||
:value="dict.value"
|
:value="dict.value"
|
||||||
>
|
>
|
||||||
@@ -56,7 +55,7 @@ const getDictOptions = computed(() => {
|
|||||||
</Select>
|
</Select>
|
||||||
<RadioGroup v-if="selectType === 'radio'" class="w-full" v-bind="attrs">
|
<RadioGroup v-if="selectType === 'radio'" class="w-full" v-bind="attrs">
|
||||||
<Radio
|
<Radio
|
||||||
v-for="(dict, index) in getDictOptions"
|
v-for="(dict, index) in getDictOption"
|
||||||
:key="index"
|
:key="index"
|
||||||
:value="dict.value"
|
:value="dict.value"
|
||||||
>
|
>
|
||||||
@@ -65,7 +64,7 @@ const getDictOptions = computed(() => {
|
|||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
<CheckboxGroup v-if="selectType === 'checkbox'" class="w-full" v-bind="attrs">
|
<CheckboxGroup v-if="selectType === 'checkbox'" class="w-full" v-bind="attrs">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
v-for="(dict, index) in getDictOptions"
|
v-for="(dict, index) in getDictOption"
|
||||||
:key="index"
|
:key="index"
|
||||||
:value="dict.value"
|
:value="dict.value"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import type { PropType } from 'vue';
|
|||||||
|
|
||||||
import type { ActionItem, PopConfirm } from './typing';
|
import type { ActionItem, PopConfirm } from './typing';
|
||||||
|
|
||||||
import { computed, toRaw } from 'vue';
|
import { computed, ref, toRaw, unref, watchEffect } from 'vue';
|
||||||
|
|
||||||
import { useAccess } from '@vben/access';
|
import { useAccess } from '@vben/access';
|
||||||
import { IconifyIcon } from '@vben/icons';
|
import { IconifyIcon } from '@vben/icons';
|
||||||
@@ -41,6 +41,14 @@ const props = defineProps({
|
|||||||
|
|
||||||
const { hasAccessByCodes } = useAccess();
|
const { hasAccessByCodes } = useAccess();
|
||||||
|
|
||||||
|
/** 缓存处理后的actions */
|
||||||
|
const processedActions = ref<any[]>([]);
|
||||||
|
const processedDropdownActions = ref<any[]>([]);
|
||||||
|
|
||||||
|
/** 用于比较的字符串化版本 */
|
||||||
|
const actionsStringified = ref('');
|
||||||
|
const dropdownActionsStringified = ref('');
|
||||||
|
|
||||||
function isIfShow(action: ActionItem): boolean {
|
function isIfShow(action: ActionItem): boolean {
|
||||||
const ifShow = action.ifShow;
|
const ifShow = action.ifShow;
|
||||||
let isIfShow = true;
|
let isIfShow = true;
|
||||||
@@ -57,8 +65,8 @@ function isIfShow(action: ActionItem): boolean {
|
|||||||
return isIfShow;
|
return isIfShow;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getActions = computed(() => {
|
/** 处理actions的纯函数 */
|
||||||
const actions = toRaw(props.actions) || [];
|
function processActions(actions: ActionItem[]): any[] {
|
||||||
return actions
|
return actions
|
||||||
.filter((action: ActionItem) => {
|
.filter((action: ActionItem) => {
|
||||||
return isIfShow(action);
|
return isIfShow(action);
|
||||||
@@ -74,30 +82,101 @@ const getActions = computed(() => {
|
|||||||
enable: !!popConfirm,
|
enable: !!popConfirm,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
|
|
||||||
const getDropdownList = computed((): any[] => {
|
/** 处理下拉菜单actions的纯函数 */
|
||||||
const dropDownActions = toRaw(props.dropDownActions) || [];
|
function processDropdownActions(
|
||||||
|
dropDownActions: ActionItem[],
|
||||||
|
divider: boolean,
|
||||||
|
): any[] {
|
||||||
return dropDownActions
|
return dropDownActions
|
||||||
.filter((action: ActionItem) => {
|
.filter((action: ActionItem) => {
|
||||||
return isIfShow(action);
|
return isIfShow(action);
|
||||||
})
|
})
|
||||||
.map((action: ActionItem, index: number) => {
|
.map((action: ActionItem, index: number) => {
|
||||||
const { label, popConfirm } = action;
|
const { label, popConfirm } = action;
|
||||||
delete action.icon;
|
const processedAction = { ...action };
|
||||||
|
delete processedAction.icon;
|
||||||
return {
|
return {
|
||||||
...action,
|
...processedAction,
|
||||||
...popConfirm,
|
...popConfirm,
|
||||||
onConfirm: popConfirm?.confirm,
|
onConfirm: popConfirm?.confirm,
|
||||||
onCancel: popConfirm?.cancel,
|
onCancel: popConfirm?.cancel,
|
||||||
text: label,
|
text: label,
|
||||||
divider: index < dropDownActions.length - 1 ? props.divider : false,
|
divider: index < dropDownActions.length - 1 ? divider : false,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 监听actions变化并更新缓存 */
|
||||||
|
watchEffect(() => {
|
||||||
|
const rawActions = toRaw(props.actions) || [];
|
||||||
|
const currentStringified = JSON.stringify(
|
||||||
|
rawActions.map((a) => ({
|
||||||
|
...a,
|
||||||
|
onClick: undefined, // 排除函数以便比较
|
||||||
|
popConfirm: a.popConfirm
|
||||||
|
? { ...a.popConfirm, confirm: undefined, cancel: undefined }
|
||||||
|
: undefined,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (currentStringified !== actionsStringified.value) {
|
||||||
|
actionsStringified.value = currentStringified;
|
||||||
|
processedActions.value = processActions(rawActions);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/** 监听dropDownActions变化并更新缓存 */
|
||||||
|
watchEffect(() => {
|
||||||
|
const rawDropDownActions = toRaw(props.dropDownActions) || [];
|
||||||
|
const currentStringified = JSON.stringify({
|
||||||
|
actions: rawDropDownActions.map((a) => ({
|
||||||
|
...a,
|
||||||
|
onClick: undefined, // 排除函数以便比较
|
||||||
|
popConfirm: a.popConfirm
|
||||||
|
? { ...a.popConfirm, confirm: undefined, cancel: undefined }
|
||||||
|
: undefined,
|
||||||
|
})),
|
||||||
|
divider: props.divider,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (currentStringified !== dropdownActionsStringified.value) {
|
||||||
|
dropdownActionsStringified.value = currentStringified;
|
||||||
|
processedDropdownActions.value = processDropdownActions(
|
||||||
|
rawDropDownActions,
|
||||||
|
props.divider,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const getActions = computed(() => processedActions.value);
|
||||||
|
|
||||||
|
const getDropdownList = computed(() => processedDropdownActions.value);
|
||||||
|
|
||||||
|
/** 缓存Space组件的size计算结果 */
|
||||||
|
const spaceSize = computed(() => {
|
||||||
|
return unref(getActions)?.some((item: ActionItem) => item.type === 'link')
|
||||||
|
? 0
|
||||||
|
: 8;
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 缓存PopConfirm属性 */
|
||||||
|
const popConfirmPropsMap = new Map<string, any>();
|
||||||
|
|
||||||
function getPopConfirmProps(attrs: PopConfirm) {
|
function getPopConfirmProps(attrs: PopConfirm) {
|
||||||
const originAttrs: any = attrs;
|
const key = JSON.stringify({
|
||||||
|
title: attrs.title,
|
||||||
|
okText: attrs.okText,
|
||||||
|
cancelText: attrs.cancelText,
|
||||||
|
disabled: attrs.disabled,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (popConfirmPropsMap.has(key)) {
|
||||||
|
return popConfirmPropsMap.get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
const originAttrs: any = { ...attrs };
|
||||||
delete originAttrs.icon;
|
delete originAttrs.icon;
|
||||||
if (attrs.confirm && isFunction(attrs.confirm)) {
|
if (attrs.confirm && isFunction(attrs.confirm)) {
|
||||||
originAttrs.onConfirm = attrs.confirm;
|
originAttrs.onConfirm = attrs.confirm;
|
||||||
@@ -107,34 +186,76 @@ function getPopConfirmProps(attrs: PopConfirm) {
|
|||||||
originAttrs.onCancel = attrs.cancel;
|
originAttrs.onCancel = attrs.cancel;
|
||||||
delete originAttrs.cancel;
|
delete originAttrs.cancel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
popConfirmPropsMap.set(key, originAttrs);
|
||||||
return originAttrs;
|
return originAttrs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 缓存Button属性 */
|
||||||
|
const buttonPropsMap = new Map<string, any>();
|
||||||
|
|
||||||
function getButtonProps(action: ActionItem) {
|
function getButtonProps(action: ActionItem) {
|
||||||
|
const key = JSON.stringify({
|
||||||
|
type: action.type,
|
||||||
|
disabled: action.disabled,
|
||||||
|
loading: action.loading,
|
||||||
|
size: action.size,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (buttonPropsMap.has(key)) {
|
||||||
|
return { ...buttonPropsMap.get(key) };
|
||||||
|
}
|
||||||
|
|
||||||
const res = {
|
const res = {
|
||||||
type: action.type || 'primary',
|
type: action.type || 'primary',
|
||||||
...action,
|
disabled: action.disabled,
|
||||||
|
loading: action.loading,
|
||||||
|
size: action.size,
|
||||||
};
|
};
|
||||||
delete res.icon;
|
|
||||||
|
buttonPropsMap.set(key, res);
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 缓存Tooltip属性 */
|
||||||
|
const tooltipPropsMap = new Map<string, any>();
|
||||||
|
|
||||||
|
function getTooltipProps(tooltip: any | string) {
|
||||||
|
if (!tooltip) return {};
|
||||||
|
|
||||||
|
const key = typeof tooltip === 'string' ? tooltip : JSON.stringify(tooltip);
|
||||||
|
|
||||||
|
if (tooltipPropsMap.has(key)) {
|
||||||
|
return tooltipPropsMap.get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result =
|
||||||
|
typeof tooltip === 'string' ? { title: tooltip } : { ...tooltip };
|
||||||
|
|
||||||
|
tooltipPropsMap.set(key, result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
function handleMenuClick(e: any) {
|
function handleMenuClick(e: any) {
|
||||||
const action = getDropdownList.value[e.key];
|
const action = unref(getDropdownList)[e.key];
|
||||||
if (action.onClick && isFunction(action.onClick)) {
|
if (action.onClick && isFunction(action.onClick)) {
|
||||||
action.onClick();
|
action.onClick();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 生成稳定的key */
|
||||||
|
function getActionKey(action: ActionItem, index: number) {
|
||||||
|
return `${action.label || ''}-${action.type || ''}-${index}`;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="table-actions">
|
<div class="table-actions">
|
||||||
<Space
|
<Space :size="spaceSize">
|
||||||
:size="
|
<template
|
||||||
getActions?.some((item: ActionItem) => item.type === 'link') ? 0 : 8
|
v-for="(action, index) in getActions"
|
||||||
"
|
:key="getActionKey(action, index)"
|
||||||
>
|
>
|
||||||
<template v-for="(action, index) in getActions" :key="index">
|
|
||||||
<Popconfirm
|
<Popconfirm
|
||||||
v-if="action.popConfirm"
|
v-if="action.popConfirm"
|
||||||
v-bind="getPopConfirmProps(action.popConfirm)"
|
v-bind="getPopConfirmProps(action.popConfirm)"
|
||||||
@@ -142,13 +263,7 @@ function handleMenuClick(e: any) {
|
|||||||
<template v-if="action.popConfirm.icon" #icon>
|
<template v-if="action.popConfirm.icon" #icon>
|
||||||
<IconifyIcon :icon="action.popConfirm.icon" />
|
<IconifyIcon :icon="action.popConfirm.icon" />
|
||||||
</template>
|
</template>
|
||||||
<Tooltip
|
<Tooltip v-bind="getTooltipProps(action.tooltip)">
|
||||||
v-bind="
|
|
||||||
typeof action.tooltip === 'string'
|
|
||||||
? { title: action.tooltip }
|
|
||||||
: { ...action.tooltip }
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<Button v-bind="getButtonProps(action)">
|
<Button v-bind="getButtonProps(action)">
|
||||||
<template v-if="action.icon" #icon>
|
<template v-if="action.icon" #icon>
|
||||||
<IconifyIcon :icon="action.icon" />
|
<IconifyIcon :icon="action.icon" />
|
||||||
@@ -157,14 +272,7 @@ function handleMenuClick(e: any) {
|
|||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Popconfirm>
|
</Popconfirm>
|
||||||
<Tooltip
|
<Tooltip v-else v-bind="getTooltipProps(action.tooltip)">
|
||||||
v-else
|
|
||||||
v-bind="
|
|
||||||
typeof action.tooltip === 'string'
|
|
||||||
? { title: action.tooltip }
|
|
||||||
: { ...action.tooltip }
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<Button v-bind="getButtonProps(action)" @click="action.onClick">
|
<Button v-bind="getButtonProps(action)" @click="action.onClick">
|
||||||
<template v-if="action.icon" #icon>
|
<template v-if="action.icon" #icon>
|
||||||
<IconifyIcon :icon="action.icon" />
|
<IconifyIcon :icon="action.icon" />
|
||||||
@@ -186,7 +294,10 @@ function handleMenuClick(e: any) {
|
|||||||
</slot>
|
</slot>
|
||||||
<template #overlay>
|
<template #overlay>
|
||||||
<Menu @click="handleMenuClick">
|
<Menu @click="handleMenuClick">
|
||||||
<Menu.Item v-for="(action, index) in getDropdownList" :key="index">
|
<Menu.Item
|
||||||
|
v-for="(action, index) in getDropdownList"
|
||||||
|
:key="`dropdown-${index}`"
|
||||||
|
>
|
||||||
<template v-if="action.popConfirm">
|
<template v-if="action.popConfirm">
|
||||||
<Popconfirm v-bind="getPopConfirmProps(action.popConfirm)">
|
<Popconfirm v-bind="getPopConfirmProps(action.popConfirm)">
|
||||||
<template v-if="action.popConfirm.icon" #icon>
|
<template v-if="action.popConfirm.icon" #icon>
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
// TODO @芋艿:后续再优化
|
// TODO @芋艿:后续再优化
|
||||||
// TODO @芋艿:可以共享么?
|
// TODO @芋艿:可以共享么?
|
||||||
|
|
||||||
|
import type { DictItem } from '#/store';
|
||||||
|
|
||||||
import { isObject } from '@vben/utils';
|
import { isObject } from '@vben/utils';
|
||||||
|
|
||||||
import { useDictStore } from '#/store';
|
import { useDictStore } from '#/store';
|
||||||
@@ -9,33 +11,103 @@ import { useDictStore } from '#/store';
|
|||||||
// 先临时移入到方法中
|
// 先临时移入到方法中
|
||||||
// const dictStore = useDictStore();
|
// const dictStore = useDictStore();
|
||||||
|
|
||||||
// TODO @dhb: antd 组件的 color 类型
|
/** AntD 组件的颜色类型 */
|
||||||
type ColorType = 'error' | 'info' | 'success' | 'warning';
|
type ColorType = 'error' | 'info' | 'success' | 'warning';
|
||||||
|
|
||||||
|
/** 字典值类型 */
|
||||||
|
type DictValueType = 'boolean' | 'number' | 'string';
|
||||||
|
|
||||||
|
/** 基础字典数据类型 */
|
||||||
export interface DictDataType {
|
export interface DictDataType {
|
||||||
dictType?: string;
|
dictType?: string;
|
||||||
label: string;
|
label: string;
|
||||||
value: boolean | number | string;
|
value: boolean | number | string;
|
||||||
colorType?: ColorType;
|
colorType?: string;
|
||||||
cssClass?: string;
|
cssClass?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 数字类型字典数据 */
|
||||||
export interface NumberDictDataType extends DictDataType {
|
export interface NumberDictDataType extends DictDataType {
|
||||||
value: number;
|
value: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 字符串类型字典数据 */
|
||||||
export interface StringDictDataType extends DictDataType {
|
export interface StringDictDataType extends DictDataType {
|
||||||
value: string;
|
value: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 布尔类型字典数据 */
|
||||||
|
export interface BooleanDictDataType extends DictDataType {
|
||||||
|
value: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 字典缓存管理器 */
|
||||||
|
class DictCacheManager {
|
||||||
|
private cache = new Map<string, DictDataType[]>();
|
||||||
|
private maxCacheSize = 100; // 最大缓存数量
|
||||||
|
|
||||||
|
/** 清空缓存 */
|
||||||
|
clear(): void {
|
||||||
|
this.cache.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除指定字典类型的缓存 */
|
||||||
|
delete(dictType: string): void {
|
||||||
|
const keysToDelete = [];
|
||||||
|
for (const key of this.cache.keys()) {
|
||||||
|
if (key.startsWith(`${dictType}:`)) {
|
||||||
|
keysToDelete.push(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
keysToDelete.forEach((key) => this.cache.delete(key));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取缓存数据 */
|
||||||
|
get(dictType: string, valueType: DictValueType): DictDataType[] | undefined {
|
||||||
|
return this.cache.get(this.getCacheKey(dictType, valueType));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 设置缓存数据 */
|
||||||
|
set(dictType: string, valueType: DictValueType, data: DictDataType[]): void {
|
||||||
|
const key = this.getCacheKey(dictType, valueType);
|
||||||
|
|
||||||
|
// 如果缓存数量超过限制,删除最早的缓存
|
||||||
|
if (this.cache.size >= this.maxCacheSize) {
|
||||||
|
const firstKey = this.cache.keys().next().value;
|
||||||
|
if (firstKey) {
|
||||||
|
this.cache.delete(firstKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.cache.set(key, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取缓存键 */
|
||||||
|
private getCacheKey(dictType: string, valueType: DictValueType): string {
|
||||||
|
return `${dictType}:${valueType}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 字典缓存实例 */
|
||||||
|
const dictCache = new DictCacheManager();
|
||||||
|
|
||||||
|
/** 值转换器映射 */
|
||||||
|
const valueConverters: Record<
|
||||||
|
DictValueType,
|
||||||
|
(value: any) => boolean | number | string
|
||||||
|
> = {
|
||||||
|
boolean: (value: any) => `${value}` === 'true',
|
||||||
|
number: (value: any) => Number.parseInt(`${value}`, 10),
|
||||||
|
string: (value: any) => `${value}`,
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取字典标签
|
* 获取字典标签
|
||||||
*
|
|
||||||
* @param dictType 字典类型
|
* @param dictType 字典类型
|
||||||
* @param value 字典值
|
* @param value 字典值
|
||||||
* @returns 字典标签
|
* @returns 字典标签
|
||||||
*/
|
*/
|
||||||
function getDictLabel(dictType: string, value: any) {
|
function getDictLabel(dictType: string, value: any): string {
|
||||||
const dictStore = useDictStore();
|
const dictStore = useDictStore();
|
||||||
const dictObj = dictStore.getDictData(dictType, value);
|
const dictObj = dictStore.getDictData(dictType, value);
|
||||||
return isObject(dictObj) ? dictObj.label : '';
|
return isObject(dictObj) ? dictObj.label : '';
|
||||||
@@ -43,103 +115,73 @@ function getDictLabel(dictType: string, value: any) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取字典对象
|
* 获取字典对象
|
||||||
*
|
|
||||||
* @param dictType 字典类型
|
* @param dictType 字典类型
|
||||||
* @param value 字典值
|
* @param value 字典值
|
||||||
* @returns 字典对象
|
* @returns 字典对象
|
||||||
*/
|
*/
|
||||||
function getDictObj(dictType: string, value: any) {
|
function getDictObj(dictType: string, value: any): DictItem | null {
|
||||||
const dictStore = useDictStore();
|
const dictStore = useDictStore();
|
||||||
const dictObj = dictStore.getDictData(dictType, value);
|
const dictObj = dictStore.getDictData(dictType, value);
|
||||||
return isObject(dictObj) ? dictObj : null;
|
return isObject(dictObj) ? dictObj : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取字典数组 用于select radio 等
|
* 获取字典数组 - 优化版本,支持缓存和泛型
|
||||||
*
|
|
||||||
* @param dictType 字典类型
|
* @param dictType 字典类型
|
||||||
* @param valueType 字典值类型,默认 string 类型
|
* @param valueType 字典值类型,默认 string 类型
|
||||||
* @returns 字典数组
|
* @returns 字典数组
|
||||||
*/
|
*/
|
||||||
function getDictOptions(
|
function getDictOptions<T extends DictValueType = 'string'>(
|
||||||
dictType: string,
|
dictType: string,
|
||||||
valueType: 'boolean' | 'number' | 'string' = 'string',
|
valueType: T = 'string' as T,
|
||||||
): DictDataType[] {
|
): T extends 'number'
|
||||||
|
? NumberDictDataType[]
|
||||||
|
: T extends 'boolean'
|
||||||
|
? BooleanDictDataType[]
|
||||||
|
: StringDictDataType[] {
|
||||||
|
// 检查缓存
|
||||||
|
const cachedData = dictCache.get(dictType, valueType);
|
||||||
|
if (cachedData) {
|
||||||
|
return cachedData as any;
|
||||||
|
}
|
||||||
|
|
||||||
const dictStore = useDictStore();
|
const dictStore = useDictStore();
|
||||||
const dictOpts = dictStore.getDictOptions(dictType);
|
const dictOpts = dictStore.getDictOptions(dictType);
|
||||||
const dictOptions: DictDataType[] = [];
|
|
||||||
if (dictOpts.length > 0) {
|
if (dictOpts.length === 0) {
|
||||||
let dictValue: boolean | number | string = '';
|
return [] as any;
|
||||||
dictOpts.forEach((d) => {
|
|
||||||
switch (valueType) {
|
|
||||||
case 'boolean': {
|
|
||||||
dictValue = `${d.value}` === 'true';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'number': {
|
|
||||||
dictValue = Number.parseInt(`${d.value}`);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'string': {
|
|
||||||
dictValue = `${d.value}`;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
// No default
|
|
||||||
}
|
|
||||||
dictOptions.push({
|
|
||||||
value: dictValue,
|
|
||||||
label: d.label,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
return dictOptions.length > 0 ? dictOptions : [];
|
|
||||||
|
const converter = valueConverters[valueType];
|
||||||
|
const dictOptions: DictDataType[] = dictOpts.map((d: DictItem) => ({
|
||||||
|
value: converter(d.value),
|
||||||
|
label: d.label,
|
||||||
|
colorType: d.colorType,
|
||||||
|
cssClass: d.cssClass,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 缓存结果
|
||||||
|
dictCache.set(dictType, valueType, dictOptions);
|
||||||
|
|
||||||
|
return dictOptions as any;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO @dhb52:下面的一系列方法,看看能不能复用 getDictOptions 方法
|
/**
|
||||||
export const getIntDictOptions = (dictType: string): NumberDictDataType[] => {
|
* 清空字典缓存
|
||||||
// 获得通用的 DictDataType 列表
|
*/
|
||||||
const dictOptions = getDictOptions(dictType) as DictDataType[];
|
export const clearDictCache = (): void => {
|
||||||
// 转换成 number 类型的 NumberDictDataType 类型
|
dictCache.clear();
|
||||||
// why 需要特殊转换:避免 IDEA 在 v-for="dict in getIntDictOptions(...)" 时,el-option 的 key 会告警
|
|
||||||
const dictOption: NumberDictDataType[] = [];
|
|
||||||
dictOptions.forEach((dict: DictDataType) => {
|
|
||||||
dictOption.push({
|
|
||||||
...dict,
|
|
||||||
value: Number.parseInt(`${dict.value}`),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return dictOption;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO @dhb52:下面的一系列方法,看看能不能复用 getDictOptions 方法
|
/**
|
||||||
export const getStrDictOptions = (dictType: string) => {
|
* 删除指定字典类型的缓存
|
||||||
// 获得通用的 DictDataType 列表
|
* @param dictType 字典类型
|
||||||
const dictOptions = getDictOptions(dictType) as DictDataType[];
|
*/
|
||||||
// 转换成 string 类型的 StringDictDataType 类型
|
export const deleteDictCache = (dictType: string): void => {
|
||||||
// why 需要特殊转换:避免 IDEA 在 v-for="dict in getStrDictOptions(...)" 时,el-option 的 key 会告警
|
dictCache.delete(dictType);
|
||||||
const dictOption: StringDictDataType[] = [];
|
|
||||||
dictOptions.forEach((dict: DictDataType) => {
|
|
||||||
dictOption.push({
|
|
||||||
...dict,
|
|
||||||
value: `${dict.value}`,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return dictOption;
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO @dhb52:下面的一系列方法,看看能不能复用 getDictOptions 方法
|
|
||||||
export const getBoolDictOptions = (dictType: string) => {
|
|
||||||
const dictOption: DictDataType[] = [];
|
|
||||||
const dictOptions = getDictOptions(dictType) as DictDataType[];
|
|
||||||
dictOptions.forEach((dict: DictDataType) => {
|
|
||||||
dictOption.push({
|
|
||||||
...dict,
|
|
||||||
value: `${dict.value}` === 'true',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return dictOption;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** 字典类型枚举 - 按模块分组和排序 */
|
||||||
enum DICT_TYPE {
|
enum DICT_TYPE {
|
||||||
AI_GENERATE_MODE = 'ai_generate_mode', // AI 生成模式
|
AI_GENERATE_MODE = 'ai_generate_mode', // AI 生成模式
|
||||||
AI_IMAGE_STATUS = 'ai_image_status', // AI 图片状态
|
AI_IMAGE_STATUS = 'ai_image_status', // AI 图片状态
|
||||||
@@ -274,4 +316,12 @@ enum DICT_TYPE {
|
|||||||
TRADE_ORDER_TYPE = 'trade_order_type', // 订单 - 类型
|
TRADE_ORDER_TYPE = 'trade_order_type', // 订单 - 类型
|
||||||
USER_TYPE = 'user_type',
|
USER_TYPE = 'user_type',
|
||||||
}
|
}
|
||||||
export { DICT_TYPE, getDictLabel, getDictObj, getDictOptions };
|
|
||||||
|
export {
|
||||||
|
type ColorType,
|
||||||
|
DICT_TYPE,
|
||||||
|
type DictValueType,
|
||||||
|
getDictLabel,
|
||||||
|
getDictObj,
|
||||||
|
getDictOptions,
|
||||||
|
};
|
||||||
|
|||||||
@@ -40,9 +40,6 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||||||
component: 'ImageUpload',
|
component: 'ImageUpload',
|
||||||
fieldName: 'avatar',
|
fieldName: 'avatar',
|
||||||
label: '角色头像',
|
label: '角色头像',
|
||||||
componentProps: {
|
|
||||||
maxSize: 1,
|
|
||||||
},
|
|
||||||
rules: 'required',
|
rules: 'required',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { ref } from 'vue';
|
|||||||
|
|
||||||
import { Form, Input, Select } from 'ant-design-vue';
|
import { Form, Input, Select } from 'ant-design-vue';
|
||||||
|
|
||||||
import { DICT_TYPE, getIntDictOptions } from '#/utils';
|
import { DICT_TYPE, getDictOptions } from '#/utils';
|
||||||
|
|
||||||
// 创建本地数据副本
|
// 创建本地数据副本
|
||||||
const modelData = defineModel<any>();
|
const modelData = defineModel<any>();
|
||||||
@@ -57,7 +57,7 @@ defineExpose({ validate });
|
|||||||
placeholder="请选择状态"
|
placeholder="请选择状态"
|
||||||
>
|
>
|
||||||
<Select.Option
|
<Select.Option
|
||||||
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
|
v-for="dict in getDictOptions(DICT_TYPE.COMMON_STATUS, 'number')"
|
||||||
:key="dict.value"
|
:key="dict.value"
|
||||||
:value="dict.value"
|
:value="dict.value"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { Button, message, Textarea } from 'ant-design-vue';
|
|||||||
import {
|
import {
|
||||||
AiWriteTypeEnum,
|
AiWriteTypeEnum,
|
||||||
DICT_TYPE,
|
DICT_TYPE,
|
||||||
getIntDictOptions,
|
getDictOptions,
|
||||||
WriteExample,
|
WriteExample,
|
||||||
} from '#/utils';
|
} from '#/utils';
|
||||||
|
|
||||||
@@ -211,22 +211,22 @@ function submit() {
|
|||||||
<ReuseLabel label="长度" />
|
<ReuseLabel label="长度" />
|
||||||
<Tag
|
<Tag
|
||||||
v-model="formData.length"
|
v-model="formData.length"
|
||||||
:tags="getIntDictOptions(DICT_TYPE.AI_WRITE_LENGTH)"
|
:tags="getDictOptions(DICT_TYPE.AI_WRITE_LENGTH, 'number')"
|
||||||
/>
|
/>
|
||||||
<ReuseLabel label="格式" />
|
<ReuseLabel label="格式" />
|
||||||
<Tag
|
<Tag
|
||||||
v-model="formData.format"
|
v-model="formData.format"
|
||||||
:tags="getIntDictOptions(DICT_TYPE.AI_WRITE_FORMAT)"
|
:tags="getDictOptions(DICT_TYPE.AI_WRITE_FORMAT, 'number')"
|
||||||
/>
|
/>
|
||||||
<ReuseLabel label="语气" />
|
<ReuseLabel label="语气" />
|
||||||
<Tag
|
<Tag
|
||||||
v-model="formData.tone"
|
v-model="formData.tone"
|
||||||
:tags="getIntDictOptions(DICT_TYPE.AI_WRITE_TONE)"
|
:tags="getDictOptions(DICT_TYPE.AI_WRITE_TONE, 'number')"
|
||||||
/>
|
/>
|
||||||
<ReuseLabel label="语言" />
|
<ReuseLabel label="语言" />
|
||||||
<Tag
|
<Tag
|
||||||
v-model="formData.language"
|
v-model="formData.language"
|
||||||
:tags="getIntDictOptions(DICT_TYPE.AI_WRITE_LANGUAGE)"
|
:tags="getDictOptions(DICT_TYPE.AI_WRITE_LANGUAGE, 'number')"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="mt-3 flex items-center justify-center">
|
<div class="mt-3 flex items-center justify-center">
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import {
|
|||||||
|
|
||||||
import { DeptSelectModal, UserSelectModal } from '#/components/select-modal';
|
import { DeptSelectModal, UserSelectModal } from '#/components/select-modal';
|
||||||
import { ImageUpload } from '#/components/upload';
|
import { ImageUpload } from '#/components/upload';
|
||||||
import { DICT_TYPE, getBoolDictOptions, getIntDictOptions } from '#/utils';
|
import { DICT_TYPE, getDictOptions } from '#/utils';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
categoryList: {
|
categoryList: {
|
||||||
@@ -295,7 +295,7 @@ defineExpose({ validate });
|
|||||||
<Radio.Group v-model:value="modelData.type">
|
<Radio.Group v-model:value="modelData.type">
|
||||||
<!-- TODO BPMN 流程类型需要整合,暂时禁用 -->
|
<!-- TODO BPMN 流程类型需要整合,暂时禁用 -->
|
||||||
<Radio
|
<Radio
|
||||||
v-for="dict in getIntDictOptions(DICT_TYPE.BPM_MODEL_TYPE)"
|
v-for="dict in getDictOptions(DICT_TYPE.BPM_MODEL_TYPE, 'number')"
|
||||||
:key="dict.value"
|
:key="dict.value"
|
||||||
:value="dict.value"
|
:value="dict.value"
|
||||||
:disabled="dict.value === 10"
|
:disabled="dict.value === 10"
|
||||||
@@ -307,10 +307,11 @@ defineExpose({ validate });
|
|||||||
<Form.Item label="是否可见" name="visible" class="mb-5">
|
<Form.Item label="是否可见" name="visible" class="mb-5">
|
||||||
<Radio.Group v-model:value="modelData.visible">
|
<Radio.Group v-model:value="modelData.visible">
|
||||||
<Radio
|
<Radio
|
||||||
v-for="(dict, index) in getBoolDictOptions(
|
v-for="dict in getDictOptions(
|
||||||
DICT_TYPE.INFRA_BOOLEAN_STRING,
|
DICT_TYPE.INFRA_BOOLEAN_STRING,
|
||||||
|
'boolean',
|
||||||
)"
|
)"
|
||||||
:key="index"
|
:key="dict.label"
|
||||||
:value="dict.value"
|
:value="dict.value"
|
||||||
>
|
>
|
||||||
{{ dict.label }}
|
{{ dict.label }}
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
import type { VbenFormSchema } from '#/adapter/form';
|
import type { VbenFormSchema } from '#/adapter/form';
|
||||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { DescriptionItemSchema } from '#/components/description';
|
||||||
|
|
||||||
|
import { h } from 'vue';
|
||||||
|
|
||||||
|
import { JsonViewer } from '@vben/common-ui';
|
||||||
|
import { formatDateTime } from '@vben/utils';
|
||||||
|
|
||||||
|
import { DictTag } from '#/components/dict-tag';
|
||||||
import { DICT_TYPE, getDictOptions, getRangePickerDefaultProps } from '#/utils';
|
import { DICT_TYPE, getDictOptions, getRangePickerDefaultProps } from '#/utils';
|
||||||
|
|
||||||
/** 列表的搜索表单 */
|
/** 列表的搜索表单 */
|
||||||
@@ -136,3 +143,101 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 详情页的字段 */
|
||||||
|
export function useDetailSchema(): DescriptionItemSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
field: 'id',
|
||||||
|
label: '日志编号',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'traceId',
|
||||||
|
label: '链路追踪',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'applicationName',
|
||||||
|
label: '应用名',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'userId',
|
||||||
|
label: '用户Id',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'userType',
|
||||||
|
label: '用户类型',
|
||||||
|
content: (data) => {
|
||||||
|
return h(DictTag, {
|
||||||
|
type: DICT_TYPE.USER_TYPE,
|
||||||
|
value: data.userType,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'userIp',
|
||||||
|
label: '用户IP',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'userAgent',
|
||||||
|
label: '用户UA',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'requestMethod',
|
||||||
|
label: '请求信息',
|
||||||
|
content: (data) => {
|
||||||
|
return `${data.requestMethod} ${data.requestUrl}`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'requestParams',
|
||||||
|
label: '请求参数',
|
||||||
|
content: (data) => {
|
||||||
|
return h(JsonViewer, {
|
||||||
|
value: data.requestParams,
|
||||||
|
previewMode: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'responseBody',
|
||||||
|
label: '请求结果',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'beginTime',
|
||||||
|
label: '请求时间',
|
||||||
|
content: (data) => {
|
||||||
|
return `${formatDateTime(data?.beginTime)} ~ ${formatDateTime(data?.endTime)}`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'duration',
|
||||||
|
label: '请求耗时',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'resultCode',
|
||||||
|
label: '操作结果',
|
||||||
|
content: (data) => {
|
||||||
|
return data.resultCode === 0
|
||||||
|
? '成功'
|
||||||
|
: `失败 | ${data.resultCode} | ${data.resultMsg}`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'operateModule',
|
||||||
|
label: '操作模块',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'operateName',
|
||||||
|
label: '操作名',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'operateType',
|
||||||
|
label: '操作类型',
|
||||||
|
content: (data) =>
|
||||||
|
h(DictTag, {
|
||||||
|
type: DICT_TYPE.INFRA_OPERATE_TYPE,
|
||||||
|
value: data?.operateType,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,13 +3,11 @@ import type { InfraApiAccessLogApi } from '#/api/infra/api-access-log';
|
|||||||
|
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
|
|
||||||
import { JsonViewer, useVbenModal } from '@vben/common-ui';
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
import { formatDateTime } from '@vben/utils';
|
|
||||||
|
|
||||||
import { Descriptions } from 'ant-design-vue';
|
import { useDescription } from '#/components/description';
|
||||||
|
|
||||||
import { DictTag } from '#/components/dict-tag';
|
import { useDetailSchema } from '../data';
|
||||||
import { DICT_TYPE } from '#/utils';
|
|
||||||
|
|
||||||
const formData = ref<InfraApiAccessLogApi.ApiAccessLog>();
|
const formData = ref<InfraApiAccessLogApi.ApiAccessLog>();
|
||||||
|
|
||||||
@@ -32,6 +30,15 @@ const [Modal, modalApi] = useVbenModal({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const [Description] = useDescription({
|
||||||
|
componentProps: {
|
||||||
|
bordered: true,
|
||||||
|
column: 1,
|
||||||
|
class: 'mx-4',
|
||||||
|
},
|
||||||
|
schema: useDetailSchema(),
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -41,66 +48,6 @@ const [Modal, modalApi] = useVbenModal({
|
|||||||
:show-cancel-button="false"
|
:show-cancel-button="false"
|
||||||
:show-confirm-button="false"
|
:show-confirm-button="false"
|
||||||
>
|
>
|
||||||
<Descriptions
|
<Description :data="formData" />
|
||||||
bordered
|
|
||||||
:column="1"
|
|
||||||
size="middle"
|
|
||||||
class="mx-4"
|
|
||||||
:label-style="{ width: '110px' }"
|
|
||||||
>
|
|
||||||
<Descriptions.Item label="日志编号">
|
|
||||||
{{ formData?.id }}
|
|
||||||
</Descriptions.Item>
|
|
||||||
<Descriptions.Item label="链路追踪">
|
|
||||||
{{ formData?.traceId }}
|
|
||||||
</Descriptions.Item>
|
|
||||||
<Descriptions.Item label="应用名">
|
|
||||||
{{ formData?.applicationName }}
|
|
||||||
</Descriptions.Item>
|
|
||||||
<Descriptions.Item label="用户信息">
|
|
||||||
{{ formData?.userId }}
|
|
||||||
<DictTag :type="DICT_TYPE.USER_TYPE" :value="formData?.userType" />
|
|
||||||
</Descriptions.Item>
|
|
||||||
<Descriptions.Item label="用户IP">
|
|
||||||
{{ formData?.userIp }}
|
|
||||||
</Descriptions.Item>
|
|
||||||
<Descriptions.Item label="用户UA">
|
|
||||||
{{ formData?.userAgent }}
|
|
||||||
</Descriptions.Item>
|
|
||||||
<Descriptions.Item label="请求信息">
|
|
||||||
{{ formData?.requestMethod }} {{ formData?.requestUrl }}
|
|
||||||
</Descriptions.Item>
|
|
||||||
<Descriptions.Item label="请求参数">
|
|
||||||
<JsonViewer :value="formData?.requestParams" preview-mode />
|
|
||||||
</Descriptions.Item>
|
|
||||||
<Descriptions.Item label="请求结果">
|
|
||||||
{{ formData?.responseBody }}
|
|
||||||
</Descriptions.Item>
|
|
||||||
<Descriptions.Item label="请求时间">
|
|
||||||
{{ formatDateTime(formData?.beginTime || '') }} ~
|
|
||||||
{{ formatDateTime(formData?.endTime || '') }}
|
|
||||||
</Descriptions.Item>
|
|
||||||
<Descriptions.Item label="请求耗时">
|
|
||||||
{{ formData?.duration }} ms
|
|
||||||
</Descriptions.Item>
|
|
||||||
<Descriptions.Item label="操作结果">
|
|
||||||
<div v-if="formData?.resultCode === 0">正常</div>
|
|
||||||
<div v-else-if="formData && formData?.resultCode > 0">
|
|
||||||
失败 | {{ formData?.resultCode }} | {{ formData?.resultMsg }}
|
|
||||||
</div>
|
|
||||||
</Descriptions.Item>
|
|
||||||
<Descriptions.Item label="操作模块">
|
|
||||||
{{ formData?.operateModule }}
|
|
||||||
</Descriptions.Item>
|
|
||||||
<Descriptions.Item label="操作名">
|
|
||||||
{{ formData?.operateName }}
|
|
||||||
</Descriptions.Item>
|
|
||||||
<Descriptions.Item label="操作类型">
|
|
||||||
<DictTag
|
|
||||||
:type="DICT_TYPE.INFRA_OPERATE_TYPE"
|
|
||||||
:value="formData?.operateType"
|
|
||||||
/>
|
|
||||||
</Descriptions.Item>
|
|
||||||
</Descriptions>
|
|
||||||
</Modal>
|
</Modal>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
import type { VbenFormSchema } from '#/adapter/form';
|
import type { VbenFormSchema } from '#/adapter/form';
|
||||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { DescriptionItemSchema } from '#/components/description';
|
||||||
|
|
||||||
|
import { h } from 'vue';
|
||||||
|
|
||||||
|
import { JsonViewer } from '@vben/common-ui';
|
||||||
|
import { formatDateTime } from '@vben/utils';
|
||||||
|
|
||||||
|
import { DictTag } from '#/components/dict-tag';
|
||||||
import {
|
import {
|
||||||
DICT_TYPE,
|
DICT_TYPE,
|
||||||
getDictOptions,
|
getDictOptions,
|
||||||
@@ -121,3 +128,102 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 详情页的字段 */
|
||||||
|
export function useDetailSchema(): DescriptionItemSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
field: 'id',
|
||||||
|
label: '日志编号',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'traceId',
|
||||||
|
label: '链路追踪',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'applicationName',
|
||||||
|
label: '应用名',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'userId',
|
||||||
|
label: '用户Id',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'userType',
|
||||||
|
label: '用户类型',
|
||||||
|
content: (data) => {
|
||||||
|
return h(DictTag, {
|
||||||
|
type: DICT_TYPE.USER_TYPE,
|
||||||
|
value: data.userType,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'userIp',
|
||||||
|
label: '用户IP',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'userAgent',
|
||||||
|
label: '用户UA',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'requestMethod',
|
||||||
|
label: '请求信息',
|
||||||
|
content: (data) => {
|
||||||
|
return `${data.requestMethod} ${data.requestUrl}`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'requestParams',
|
||||||
|
label: '请求参数',
|
||||||
|
content: (data) => {
|
||||||
|
return h(JsonViewer, {
|
||||||
|
value: data.requestParams,
|
||||||
|
previewMode: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'exceptionTime',
|
||||||
|
label: '异常时间',
|
||||||
|
content: (data) => {
|
||||||
|
return formatDateTime(data?.exceptionTime) as string;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'exceptionName',
|
||||||
|
label: '异常名',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'exceptionStackTrace',
|
||||||
|
label: '异常堆栈',
|
||||||
|
content: (data) => {
|
||||||
|
return h(JsonViewer, {
|
||||||
|
value: data.exceptionStackTrace,
|
||||||
|
previewMode: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'processStatus',
|
||||||
|
label: '处理状态',
|
||||||
|
content: (data) => {
|
||||||
|
return h(DictTag, {
|
||||||
|
type: DICT_TYPE.INFRA_API_ERROR_LOG_PROCESS_STATUS,
|
||||||
|
value: data?.processStatus,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'processUserId',
|
||||||
|
label: '处理人',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'processTime',
|
||||||
|
label: '处理时间',
|
||||||
|
content: (data) => {
|
||||||
|
return formatDateTime(data?.processTime) as string;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,13 +3,11 @@ import type { InfraApiErrorLogApi } from '#/api/infra/api-error-log';
|
|||||||
|
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
|
|
||||||
import { JsonViewer, useVbenModal } from '@vben/common-ui';
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
import { formatDateTime } from '@vben/utils';
|
|
||||||
|
|
||||||
import { Descriptions } from 'ant-design-vue';
|
import { useDescription } from '#/components/description';
|
||||||
|
|
||||||
import { DictTag } from '#/components/dict-tag';
|
import { useDetailSchema } from '../data';
|
||||||
import { DICT_TYPE } from '#/utils';
|
|
||||||
|
|
||||||
const formData = ref<InfraApiErrorLogApi.ApiErrorLog>();
|
const formData = ref<InfraApiErrorLogApi.ApiErrorLog>();
|
||||||
|
|
||||||
@@ -32,6 +30,15 @@ const [Modal, modalApi] = useVbenModal({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const [Description] = useDescription({
|
||||||
|
componentProps: {
|
||||||
|
bordered: true,
|
||||||
|
column: 1,
|
||||||
|
class: 'mx-4',
|
||||||
|
},
|
||||||
|
schema: useDetailSchema(),
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -41,59 +48,6 @@ const [Modal, modalApi] = useVbenModal({
|
|||||||
:show-cancel-button="false"
|
:show-cancel-button="false"
|
||||||
:show-confirm-button="false"
|
:show-confirm-button="false"
|
||||||
>
|
>
|
||||||
<Descriptions
|
<Description :data="formData" />
|
||||||
bordered
|
|
||||||
:column="1"
|
|
||||||
size="middle"
|
|
||||||
class="mx-4"
|
|
||||||
:label-style="{ width: '110px' }"
|
|
||||||
>
|
|
||||||
<Descriptions.Item label="日志编号">
|
|
||||||
{{ formData?.id }}
|
|
||||||
</Descriptions.Item>
|
|
||||||
<Descriptions.Item label="链路追踪">
|
|
||||||
{{ formData?.traceId }}
|
|
||||||
</Descriptions.Item>
|
|
||||||
<Descriptions.Item label="应用名">
|
|
||||||
{{ formData?.applicationName }}
|
|
||||||
</Descriptions.Item>
|
|
||||||
<Descriptions.Item label="用户编号">
|
|
||||||
{{ formData?.userId }}
|
|
||||||
<DictTag :type="DICT_TYPE.USER_TYPE" :value="formData?.userType" />
|
|
||||||
</Descriptions.Item>
|
|
||||||
<Descriptions.Item label="用户IP">
|
|
||||||
{{ formData?.userIp }}
|
|
||||||
</Descriptions.Item>
|
|
||||||
<Descriptions.Item label="用户UA">
|
|
||||||
{{ formData?.userAgent }}
|
|
||||||
</Descriptions.Item>
|
|
||||||
<Descriptions.Item label="请求信息">
|
|
||||||
{{ formData?.requestMethod }} {{ formData?.requestUrl }}
|
|
||||||
</Descriptions.Item>
|
|
||||||
<Descriptions.Item label="请求参数">
|
|
||||||
<JsonViewer :value="formData?.requestParams" preview-mode />
|
|
||||||
</Descriptions.Item>
|
|
||||||
<Descriptions.Item label="异常时间">
|
|
||||||
{{ formatDateTime(formData?.exceptionTime || '') }}
|
|
||||||
</Descriptions.Item>
|
|
||||||
<Descriptions.Item label="异常名">
|
|
||||||
{{ formData?.exceptionName }}
|
|
||||||
</Descriptions.Item>
|
|
||||||
<Descriptions.Item v-if="formData?.exceptionStackTrace" label="异常堆栈">
|
|
||||||
<JsonViewer :value="formData?.exceptionStackTrace" preview-mode />
|
|
||||||
</Descriptions.Item>
|
|
||||||
<Descriptions.Item label="处理状态">
|
|
||||||
<DictTag
|
|
||||||
:type="DICT_TYPE.INFRA_API_ERROR_LOG_PROCESS_STATUS"
|
|
||||||
:value="formData?.processStatus"
|
|
||||||
/>
|
|
||||||
</Descriptions.Item>
|
|
||||||
<Descriptions.Item v-if="formData?.processUserId" label="处理人">
|
|
||||||
{{ formData?.processUserId }}
|
|
||||||
</Descriptions.Item>
|
|
||||||
<Descriptions.Item v-if="formData?.processTime" label="处理时间">
|
|
||||||
{{ formatDateTime(formData?.processTime || '') }}
|
|
||||||
</Descriptions.Item>
|
|
||||||
</Descriptions>
|
|
||||||
</Modal>
|
</Modal>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -139,8 +139,6 @@ const quickNavItems: WorkbenchQuickNavItem[] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
// 这是一个示例方法,实际项目中需要根据实际情况进行调整
|
|
||||||
// This is a sample method, adjust according to the actual project requirements
|
|
||||||
function navTo(nav: WorkbenchProjectItem | WorkbenchQuickNavItem) {
|
function navTo(nav: WorkbenchProjectItem | WorkbenchQuickNavItem) {
|
||||||
if (nav.url?.startsWith('http')) {
|
if (nav.url?.startsWith('http')) {
|
||||||
openWindow(nav.url);
|
openWindow(nav.url);
|
||||||
|
|||||||
@@ -30,9 +30,6 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||||||
fieldName: 'picUrl',
|
fieldName: 'picUrl',
|
||||||
label: '品牌图片',
|
label: '品牌图片',
|
||||||
component: 'ImageUpload',
|
component: 'ImageUpload',
|
||||||
componentProps: {
|
|
||||||
maxSize: 1,
|
|
||||||
},
|
|
||||||
rules: 'required',
|
rules: 'required',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -57,9 +57,6 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||||||
fieldName: 'picUrl',
|
fieldName: 'picUrl',
|
||||||
label: '移动端分类图',
|
label: '移动端分类图',
|
||||||
component: 'ImageUpload',
|
component: 'ImageUpload',
|
||||||
componentProps: {
|
|
||||||
maxSize: 1,
|
|
||||||
},
|
|
||||||
rules: 'required',
|
rules: 'required',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -31,9 +31,6 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||||||
fieldName: 'userAvatar',
|
fieldName: 'userAvatar',
|
||||||
label: '用户头像',
|
label: '用户头像',
|
||||||
component: 'ImageUpload',
|
component: 'ImageUpload',
|
||||||
componentProps: {
|
|
||||||
maxSize: 1,
|
|
||||||
},
|
|
||||||
rules: 'required',
|
rules: 'required',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -65,7 +62,7 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||||||
label: '评论图片',
|
label: '评论图片',
|
||||||
component: 'ImageUpload',
|
component: 'ImageUpload',
|
||||||
componentProps: {
|
componentProps: {
|
||||||
maxSize: 9,
|
maxNumber: 9,
|
||||||
},
|
},
|
||||||
rules: 'required',
|
rules: 'required',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,12 +1,7 @@
|
|||||||
import type { VbenFormSchema } from '#/adapter/form';
|
import type { VbenFormSchema } from '#/adapter/form';
|
||||||
import type { VxeGridPropTypes } from '#/adapter/vxe-table';
|
import type { VxeGridPropTypes } from '#/adapter/vxe-table';
|
||||||
|
|
||||||
import {
|
import { DICT_TYPE, getDictOptions, getRangePickerDefaultProps } from '#/utils';
|
||||||
DICT_TYPE,
|
|
||||||
getDictOptions,
|
|
||||||
getIntDictOptions,
|
|
||||||
getRangePickerDefaultProps,
|
|
||||||
} from '#/utils';
|
|
||||||
|
|
||||||
/** 新增/修改的表单 */
|
/** 新增/修改的表单 */
|
||||||
export function useFormSchema(): VbenFormSchema[] {
|
export function useFormSchema(): VbenFormSchema[] {
|
||||||
@@ -29,9 +24,6 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||||||
fieldName: 'picUrl',
|
fieldName: 'picUrl',
|
||||||
label: '图标地址',
|
label: '图标地址',
|
||||||
component: 'ImageUpload',
|
component: 'ImageUpload',
|
||||||
componentProps: {
|
|
||||||
maxSize: 1,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldName: 'sort',
|
fieldName: 'sort',
|
||||||
@@ -75,7 +67,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
|
|||||||
component: 'Select',
|
component: 'Select',
|
||||||
componentProps: {
|
componentProps: {
|
||||||
placeholder: '请选择状态',
|
placeholder: '请选择状态',
|
||||||
options: getIntDictOptions(DICT_TYPE.COMMON_STATUS),
|
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -52,9 +52,6 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||||||
fieldName: 'picUrl',
|
fieldName: 'picUrl',
|
||||||
label: '文章封面',
|
label: '文章封面',
|
||||||
component: 'ImageUpload',
|
component: 'ImageUpload',
|
||||||
componentProps: {
|
|
||||||
maxSize: 1,
|
|
||||||
},
|
|
||||||
rules: 'required',
|
rules: 'required',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,12 +1,7 @@
|
|||||||
import type { VbenFormSchema } from '#/adapter/form';
|
import type { VbenFormSchema } from '#/adapter/form';
|
||||||
import type { VxeGridPropTypes } from '#/adapter/vxe-table';
|
import type { VxeGridPropTypes } from '#/adapter/vxe-table';
|
||||||
|
|
||||||
import {
|
import { DICT_TYPE, getDictOptions, getRangePickerDefaultProps } from '#/utils';
|
||||||
DICT_TYPE,
|
|
||||||
getDictOptions,
|
|
||||||
getIntDictOptions,
|
|
||||||
getRangePickerDefaultProps,
|
|
||||||
} from '#/utils';
|
|
||||||
|
|
||||||
/** 新增/修改的表单 */
|
/** 新增/修改的表单 */
|
||||||
export function useFormSchema(): VbenFormSchema[] {
|
export function useFormSchema(): VbenFormSchema[] {
|
||||||
@@ -29,9 +24,6 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||||||
fieldName: 'picUrl',
|
fieldName: 'picUrl',
|
||||||
label: '图片地址',
|
label: '图片地址',
|
||||||
component: 'ImageUpload',
|
component: 'ImageUpload',
|
||||||
componentProps: {
|
|
||||||
maxSize: 1,
|
|
||||||
},
|
|
||||||
rules: 'required',
|
rules: 'required',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -102,7 +94,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
|
|||||||
component: 'Select',
|
component: 'Select',
|
||||||
componentProps: {
|
componentProps: {
|
||||||
placeholder: '请选择状态',
|
placeholder: '请选择状态',
|
||||||
options: getIntDictOptions(DICT_TYPE.COMMON_STATUS),
|
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
262
apps/web-antd/src/views/mall/promotion/bargain/activity/data.ts
Normal file
262
apps/web-antd/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' },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -1,32 +1,178 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { DocAlert, Page } from '@vben/common-ui';
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { MallBargainActivityApi } from '#/api/mall/promotion/bargain/bargainActivity';
|
||||||
|
|
||||||
import { Button } from 'ant-design-vue';
|
import { confirm, DocAlert, Page, useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { message } from 'ant-design-vue';
|
||||||
|
|
||||||
|
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 hideLoading = message.loading({
|
||||||
|
content: '确认关闭该砍价活动吗?',
|
||||||
|
key: 'action_key_msg',
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await closeBargainActivity(row.id as number);
|
||||||
|
message.success({
|
||||||
|
content: '关闭成功',
|
||||||
|
key: 'action_key_msg',
|
||||||
|
});
|
||||||
|
onRefresh();
|
||||||
|
} finally {
|
||||||
|
hideLoading();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除砍价活动 */
|
||||||
|
async function handleDelete(row: MallBargainActivityApi.BargainActivity) {
|
||||||
|
const hideLoading = message.loading({
|
||||||
|
content: $t('ui.actionMessage.deleting', [row.name]),
|
||||||
|
key: 'action_key_msg',
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await deleteBargainActivity(row.id as number);
|
||||||
|
message.success({
|
||||||
|
content: $t('ui.actionMessage.deleteSuccess', [row.name]),
|
||||||
|
key: 'action_key_msg',
|
||||||
|
});
|
||||||
|
onRefresh();
|
||||||
|
} finally {
|
||||||
|
hideLoading();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Page>
|
<Page auto-content-height>
|
||||||
<DocAlert
|
<template #doc>
|
||||||
title="【营销】砍价活动"
|
<DocAlert
|
||||||
url="https://doc.iocoder.cn/mall/promotion-bargain/"
|
title="【营销】砍价活动"
|
||||||
/>
|
url="https://doc.iocoder.cn/mall/promotion-bargain/"
|
||||||
<Button
|
/>
|
||||||
danger
|
</template>
|
||||||
type="link"
|
|
||||||
target="_blank"
|
<FormModal @success="onRefresh" />
|
||||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
|
|
||||||
>
|
<Grid table-title="砍价活动列表">
|
||||||
该功能支持 Vue3 + element-plus 版本!
|
<template #toolbar-tools>
|
||||||
</Button>
|
<TableAction
|
||||||
<br />
|
:actions="[
|
||||||
<Button
|
{
|
||||||
type="link"
|
label: $t('ui.actionTitle.create', ['砍价活动']),
|
||||||
target="_blank"
|
type: 'primary',
|
||||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/mall/promotion/bargain/activity/index"
|
icon: ACTION_ICON.ADD,
|
||||||
>
|
auth: ['promotion:bargain-activity:create'],
|
||||||
可参考
|
onClick: handleCreate,
|
||||||
https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/mall/promotion/bargain/activity/index
|
},
|
||||||
代码,pull request 贡献给我们!
|
]"
|
||||||
</Button>
|
/>
|
||||||
|
</template>
|
||||||
|
<template #actions="{ row }">
|
||||||
|
<TableAction
|
||||||
|
:actions="[
|
||||||
|
{
|
||||||
|
label: $t('common.edit'),
|
||||||
|
type: 'link',
|
||||||
|
icon: ACTION_ICON.EDIT,
|
||||||
|
auth: ['promotion:bargain-activity:update'],
|
||||||
|
onClick: handleEdit.bind(null, row),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '关闭',
|
||||||
|
type: 'link',
|
||||||
|
danger: true,
|
||||||
|
icon: ACTION_ICON.DELETE,
|
||||||
|
auth: ['promotion:bargain-activity:close'],
|
||||||
|
ifShow: row.status === 0,
|
||||||
|
onClick: handleClose.bind(null, row),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('common.delete'),
|
||||||
|
type: 'link',
|
||||||
|
danger: 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>
|
</Page>
|
||||||
</template>
|
</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 { message } from 'ant-design-vue';
|
||||||
|
|
||||||
|
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');
|
||||||
|
message.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-antd/src/views/mall/promotion/bargain/record/data.ts
Normal file
161
apps/web-antd/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: '结束时间',
|
||||||
|
width: 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',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -1,32 +1,83 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { DocAlert, Page } from '@vben/common-ui';
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { MallBargainRecordApi } from '#/api/mall/promotion/bargain/bargainRecord';
|
||||||
|
|
||||||
import { Button } from 'ant-design-vue';
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Page>
|
<Page auto-content-height>
|
||||||
<DocAlert
|
<template #doc>
|
||||||
title="【营销】砍价活动"
|
<DocAlert
|
||||||
url="https://doc.iocoder.cn/mall/promotion-bargain/"
|
title="【营销】砍价活动"
|
||||||
/>
|
url="https://doc.iocoder.cn/mall/promotion-bargain/"
|
||||||
<Button
|
/>
|
||||||
danger
|
</template>
|
||||||
type="link"
|
|
||||||
target="_blank"
|
<HelpListModalApi />
|
||||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
|
|
||||||
>
|
<Grid table-title="砍价记录列表">
|
||||||
该功能支持 Vue3 + element-plus 版本!
|
<template #actions="{ row }">
|
||||||
</Button>
|
<TableAction
|
||||||
<br />
|
:actions="[
|
||||||
<Button
|
{
|
||||||
type="link"
|
label: '助力',
|
||||||
target="_blank"
|
type: 'link',
|
||||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/mall/promotion/bargain/record/index"
|
icon: ACTION_ICON.VIEW,
|
||||||
>
|
auth: ['promotion:bargain-help:query'],
|
||||||
可参考
|
onClick: handleViewHelp.bind(null, row),
|
||||||
https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/mall/promotion/bargain/record/index
|
},
|
||||||
代码,pull request 贡献给我们!
|
]"
|
||||||
</Button>
|
/>
|
||||||
|
</template>
|
||||||
|
</Grid>
|
||||||
</Page>
|
</Page>
|
||||||
</template>
|
</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: '创建时间',
|
||||||
|
width: 180,
|
||||||
|
formatter: 'formatDateTime',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
width: 200,
|
||||||
|
fixed: 'right',
|
||||||
|
slots: { default: 'actions' },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -1,32 +1,182 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { DocAlert, Page } from '@vben/common-ui';
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { MallCombinationActivityApi } from '#/api/mall/promotion/combination/combinationActivity';
|
||||||
|
|
||||||
import { Button } from 'ant-design-vue';
|
import { confirm, DocAlert, Page, useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { message } from 'ant-design-vue';
|
||||||
|
|
||||||
|
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 hideLoading = message.loading({
|
||||||
|
content: $t('ui.actionMessage.processing'),
|
||||||
|
key: 'action_key_msg',
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await closeCombinationActivity(row.id as number);
|
||||||
|
message.success({
|
||||||
|
content: '关闭成功',
|
||||||
|
key: 'action_key_msg',
|
||||||
|
});
|
||||||
|
onRefresh();
|
||||||
|
} finally {
|
||||||
|
hideLoading();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除拼团活动 */
|
||||||
|
async function handleDelete(
|
||||||
|
row: MallCombinationActivityApi.CombinationActivity,
|
||||||
|
) {
|
||||||
|
const hideLoading = message.loading({
|
||||||
|
content: $t('ui.actionMessage.deleting', [row.name]),
|
||||||
|
key: 'action_key_msg',
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await deleteCombinationActivity(row.id as number);
|
||||||
|
message.success({
|
||||||
|
content: $t('ui.actionMessage.deleteSuccess', [row.name]),
|
||||||
|
key: 'action_key_msg',
|
||||||
|
});
|
||||||
|
onRefresh();
|
||||||
|
} finally {
|
||||||
|
hideLoading();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Page>
|
<Page auto-content-height>
|
||||||
<DocAlert
|
<template #doc>
|
||||||
title="【营销】拼团活动"
|
<DocAlert
|
||||||
url="https://doc.iocoder.cn/mall/promotion-combination/"
|
title="【营销】拼团活动"
|
||||||
/>
|
url="https://doc.iocoder.cn/mall/promotion-combination/"
|
||||||
<Button
|
/>
|
||||||
danger
|
</template>
|
||||||
type="link"
|
|
||||||
target="_blank"
|
<FormModal @success="onRefresh" />
|
||||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
|
|
||||||
>
|
<Grid table-title="拼团活动列表">
|
||||||
该功能支持 Vue3 + element-plus 版本!
|
<template #toolbar-tools>
|
||||||
</Button>
|
<TableAction
|
||||||
<br />
|
:actions="[
|
||||||
<Button
|
{
|
||||||
type="link"
|
label: $t('ui.actionTitle.create', ['拼团活动']),
|
||||||
target="_blank"
|
type: 'primary',
|
||||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/mall/promotion/combination/activity/index"
|
icon: ACTION_ICON.ADD,
|
||||||
>
|
auth: ['promotion:combination-activity:create'],
|
||||||
可参考
|
onClick: handleCreate,
|
||||||
https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/mall/promotion/combination/activity/index
|
},
|
||||||
代码,pull request 贡献给我们!
|
]"
|
||||||
</Button>
|
/>
|
||||||
|
</template>
|
||||||
|
<template #actions="{ row }">
|
||||||
|
<TableAction
|
||||||
|
:actions="[
|
||||||
|
{
|
||||||
|
label: $t('common.edit'),
|
||||||
|
type: 'link',
|
||||||
|
icon: ACTION_ICON.EDIT,
|
||||||
|
auth: ['promotion:combination-activity:update'],
|
||||||
|
onClick: handleEdit.bind(null, row),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '关闭',
|
||||||
|
type: 'link',
|
||||||
|
danger: true,
|
||||||
|
icon: ACTION_ICON.DELETE,
|
||||||
|
auth: ['promotion:combination-activity:close'],
|
||||||
|
ifShow: row.status === 0,
|
||||||
|
onClick: handleClose.bind(null, row),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('common.delete'),
|
||||||
|
type: 'link',
|
||||||
|
danger: 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>
|
</Page>
|
||||||
</template>
|
</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 { message } from 'ant-design-vue';
|
||||||
|
|
||||||
|
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');
|
||||||
|
message.success($t('ui.actionMessage.operationSuccess'));
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async onOpenChange(isOpen: boolean) {
|
||||||
|
if (!isOpen) {
|
||||||
|
formData.value = undefined;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 加载数据
|
||||||
|
const data =
|
||||||
|
modalApi.getData<MallCombinationActivityApi.CombinationActivity>();
|
||||||
|
if (!data || !data.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
modalApi.lock();
|
||||||
|
try {
|
||||||
|
formData.value = await getCombinationActivity(data.id as number);
|
||||||
|
// 设置到 values
|
||||||
|
if (formData.value) {
|
||||||
|
await formApi.setValues(formData.value);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal class="w-3/5" :title="getTitle">
|
||||||
|
<Form />
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
import type { VbenFormSchema } from '#/adapter/form';
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
|
||||||
|
import { DICT_TYPE, getDictOptions } from '#/utils';
|
||||||
|
|
||||||
|
/** 列表的搜索表单 */
|
||||||
|
export function useGridFormSchema(): VbenFormSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
fieldName: 'status',
|
||||||
|
label: '拼团状态',
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请选择拼团状态',
|
||||||
|
clearable: true,
|
||||||
|
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'createTime',
|
||||||
|
label: '创建时间',
|
||||||
|
component: 'RangePicker',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: ['开始时间', '结束时间'],
|
||||||
|
clearable: true,
|
||||||
|
valueFormat: 'YYYY-MM-DD HH:mm:ss',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 列表的字段 */
|
||||||
|
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
field: 'id',
|
||||||
|
title: '拼团编号',
|
||||||
|
minWidth: 80,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'avatar',
|
||||||
|
title: '头像',
|
||||||
|
minWidth: 80,
|
||||||
|
cellRender: {
|
||||||
|
name: 'CellImage',
|
||||||
|
props: {
|
||||||
|
height: 40,
|
||||||
|
width: 40,
|
||||||
|
shape: 'circle',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'nickname',
|
||||||
|
title: '昵称',
|
||||||
|
minWidth: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'headId',
|
||||||
|
title: '开团团长',
|
||||||
|
minWidth: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'picUrl',
|
||||||
|
title: '拼团商品图',
|
||||||
|
minWidth: 80,
|
||||||
|
cellRender: {
|
||||||
|
name: 'CellImage',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'spuName',
|
||||||
|
title: '拼团商品',
|
||||||
|
minWidth: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'activityName',
|
||||||
|
title: '拼团活动',
|
||||||
|
minWidth: 140,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'userSize',
|
||||||
|
title: '几人团',
|
||||||
|
minWidth: 80,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'userCount',
|
||||||
|
title: '参与人数',
|
||||||
|
minWidth: 80,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'createTime',
|
||||||
|
title: '参团时间',
|
||||||
|
width: 180,
|
||||||
|
formatter: 'formatDateTime',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'endTime',
|
||||||
|
title: '结束时间',
|
||||||
|
width: 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: '参团时间',
|
||||||
|
width: 180,
|
||||||
|
formatter: 'formatDateTime',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'endTime',
|
||||||
|
title: '结束时间',
|
||||||
|
width: 180,
|
||||||
|
formatter: 'formatDateTime',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'status',
|
||||||
|
title: '拼团状态',
|
||||||
|
minWidth: 100,
|
||||||
|
cellRender: {
|
||||||
|
name: 'CellDict',
|
||||||
|
props: { type: DICT_TYPE.COMMON_STATUS },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -1,32 +1,81 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { DocAlert, Page } from '@vben/common-ui';
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
|
||||||
import { Button } from 'ant-design-vue';
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Page>
|
<Page auto-content-height>
|
||||||
<DocAlert
|
<template #doc>
|
||||||
title="【营销】拼团活动"
|
<DocAlert
|
||||||
url="https://doc.iocoder.cn/mall/promotion-combination/"
|
title="【营销】拼团活动"
|
||||||
/>
|
url="https://doc.iocoder.cn/mall/promotion-combination/"
|
||||||
<Button
|
/>
|
||||||
danger
|
</template>
|
||||||
type="link"
|
|
||||||
target="_blank"
|
<UserListModal />
|
||||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
|
|
||||||
>
|
<Grid table-title="拼团记录列表">
|
||||||
该功能支持 Vue3 + element-plus 版本!
|
<template #actions="{ row }">
|
||||||
</Button>
|
<TableAction
|
||||||
<br />
|
:actions="[
|
||||||
<Button
|
{
|
||||||
type="link"
|
label: '查看成员',
|
||||||
target="_blank"
|
type: 'link',
|
||||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/mall/promotion/combination/record/index.vue"
|
icon: ACTION_ICON.VIEW,
|
||||||
>
|
onClick: handleViewUsers.bind(null, row),
|
||||||
可参考
|
},
|
||||||
https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/mall/promotion/combination/record/index.vue
|
]"
|
||||||
代码,pull request 贡献给我们!
|
/>
|
||||||
</Button>
|
</template>
|
||||||
|
</Grid>
|
||||||
</Page>
|
</Page>
|
||||||
</template>
|
</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-antd/src/views/mall/promotion/coupon/data.ts
Normal file
129
apps/web-antd/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: '领取时间',
|
||||||
|
width: 180,
|
||||||
|
formatter: 'formatDateTime',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'useTime',
|
||||||
|
title: '使用时间',
|
||||||
|
width: 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-antd/src/views/mall/promotion/coupon/formatter.ts
Normal file
65
apps/web-antd/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)}`;
|
||||||
|
}
|
||||||
@@ -1,32 +1,132 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { DocAlert, Page } from '@vben/common-ui';
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { MallCouponApi } from '#/api/mall/promotion/coupon/coupon';
|
||||||
|
|
||||||
import { Button } from 'ant-design-vue';
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import { DocAlert, Page } from '@vben/common-ui';
|
||||||
|
import { $t } from '@vben/locales';
|
||||||
|
|
||||||
|
import { message, TabPane, Tabs } from 'ant-design-vue';
|
||||||
|
|
||||||
|
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 hideLoading = message.loading({
|
||||||
|
content: $t('ui.actionMessage.deleting', [row.name]),
|
||||||
|
key: 'action_key_msg',
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await deleteCoupon(row.id as number);
|
||||||
|
message.success({
|
||||||
|
content: '回收成功',
|
||||||
|
key: 'action_key_msg',
|
||||||
|
});
|
||||||
|
onRefresh();
|
||||||
|
} finally {
|
||||||
|
hideLoading();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 刷新表格 */
|
||||||
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Page>
|
<Page auto-content-height>
|
||||||
<DocAlert
|
<template #doc>
|
||||||
title="【营销】优惠劵"
|
<DocAlert
|
||||||
url="https://doc.iocoder.cn/mall/promotion-coupon/"
|
title="【营销】优惠劵"
|
||||||
/>
|
url="https://doc.iocoder.cn/mall/promotion-coupon/"
|
||||||
<Button
|
/>
|
||||||
danger
|
</template>
|
||||||
type="link"
|
|
||||||
target="_blank"
|
<Grid table-title="优惠券列表">
|
||||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
|
<template #top>
|
||||||
>
|
<Tabs v-model:active-key="activeTab" type="card" @change="onTabChange">
|
||||||
该功能支持 Vue3 + element-plus 版本!
|
<TabPane
|
||||||
</Button>
|
v-for="tab in statusTabs"
|
||||||
<br />
|
:key="tab.value"
|
||||||
<Button
|
:tab="tab.label"
|
||||||
type="link"
|
/>
|
||||||
target="_blank"
|
</Tabs>
|
||||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/mall/promotion/coupon/index"
|
</template>
|
||||||
>
|
<template #actions="{ row }">
|
||||||
可参考
|
<TableAction
|
||||||
https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/mall/promotion/coupon/index
|
:actions="[
|
||||||
代码,pull request 贡献给我们!
|
{
|
||||||
</Button>
|
label: '回收',
|
||||||
|
type: 'link',
|
||||||
|
danger: true,
|
||||||
|
icon: ACTION_ICON.DELETE,
|
||||||
|
auth: ['promotion:coupon:delete'],
|
||||||
|
popConfirm: {
|
||||||
|
title:
|
||||||
|
'回收将会收回会员领取的待使用的优惠券,已使用的将无法回收,确定要回收所选优惠券吗?',
|
||||||
|
confirm: handleDelete.bind(null, row),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Grid>
|
||||||
</Page>
|
</Page>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
252
apps/web-antd/src/views/mall/promotion/coupon/template/data.ts
Normal file
252
apps/web-antd/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: '创建时间',
|
||||||
|
width: 180,
|
||||||
|
formatter: 'formatDateTime',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
width: 120,
|
||||||
|
fixed: 'right',
|
||||||
|
slots: { default: 'actions' },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -1,32 +1,190 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { DocAlert, Page } from '@vben/common-ui';
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { MallCouponTemplateApi } from '#/api/mall/promotion/coupon/couponTemplate';
|
||||||
|
|
||||||
import { Button } from 'ant-design-vue';
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import { DocAlert, Page, useVbenModal } from '@vben/common-ui';
|
||||||
|
import { $t } from '@vben/locales';
|
||||||
|
|
||||||
|
import { message, Switch } from 'ant-design-vue';
|
||||||
|
|
||||||
|
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 hideLoading = message.loading({
|
||||||
|
content: $t('ui.actionMessage.deleting', [row.name]),
|
||||||
|
key: 'action_key_msg',
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await deleteCouponTemplate(row.id as number);
|
||||||
|
message.success({
|
||||||
|
content: $t('ui.actionMessage.deleteSuccess', [row.name]),
|
||||||
|
key: 'action_key_msg',
|
||||||
|
});
|
||||||
|
onRefresh();
|
||||||
|
} finally {
|
||||||
|
hideLoading();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 hideLoading = message.loading({
|
||||||
|
content: `正在${text}优惠券模板...`,
|
||||||
|
key: 'status_key_msg',
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await updateCouponTemplateStatus(row.id as number, row.status as number);
|
||||||
|
message.success({
|
||||||
|
content: `${text}成功`,
|
||||||
|
key: 'status_key_msg',
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
// 异常时,需要将 row.status 状态重置回之前的
|
||||||
|
row.status =
|
||||||
|
row.status === CommonStatusEnum.ENABLE
|
||||||
|
? CommonStatusEnum.DISABLE
|
||||||
|
: CommonStatusEnum.ENABLE;
|
||||||
|
} finally {
|
||||||
|
hideLoading();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Page>
|
<Page auto-content-height>
|
||||||
<DocAlert
|
<template #doc>
|
||||||
title="【营销】优惠劵"
|
<DocAlert
|
||||||
url="https://doc.iocoder.cn/mall/promotion-coupon/"
|
title="【营销】优惠劵"
|
||||||
/>
|
url="https://doc.iocoder.cn/mall/promotion-coupon/"
|
||||||
<Button
|
/>
|
||||||
danger
|
</template>
|
||||||
type="link"
|
|
||||||
target="_blank"
|
<FormModal @success="onRefresh" />
|
||||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
|
<Grid table-title="优惠券列表">
|
||||||
>
|
<template #toolbar-tools>
|
||||||
该功能支持 Vue3 + element-plus 版本!
|
<TableAction
|
||||||
</Button>
|
:actions="[
|
||||||
<br />
|
{
|
||||||
<Button
|
label: $t('ui.actionTitle.create', ['优惠券模板']),
|
||||||
type="link"
|
type: 'primary',
|
||||||
target="_blank"
|
icon: ACTION_ICON.ADD,
|
||||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/mall/promotion/coupon/template/index"
|
auth: ['promotion:coupon-template:create'],
|
||||||
>
|
onClick: handleCreate,
|
||||||
可参考
|
},
|
||||||
https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/mall/promotion/coupon/template/index
|
]"
|
||||||
代码,pull request 贡献给我们!
|
/>
|
||||||
</Button>
|
</template>
|
||||||
|
<template #status="{ row }">
|
||||||
|
<Switch
|
||||||
|
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: 'link',
|
||||||
|
icon: ACTION_ICON.EDIT,
|
||||||
|
auth: ['promotion:coupon-template:update'],
|
||||||
|
onClick: handleEdit.bind(null, row),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('common.delete'),
|
||||||
|
type: 'link',
|
||||||
|
danger: 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>
|
</Page>
|
||||||
</template>
|
</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 { message } from 'ant-design-vue';
|
||||||
|
|
||||||
|
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');
|
||||||
|
message.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-antd/src/views/mall/promotion/discountActivity/data.ts
Normal file
159
apps/web-antd/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: '创建时间',
|
||||||
|
width: 180,
|
||||||
|
formatter: 'formatDateTime',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
width: 150,
|
||||||
|
fixed: 'right',
|
||||||
|
slots: { default: 'actions' },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -1,32 +1,178 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { DocAlert, Page } from '@vben/common-ui';
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { MallDiscountActivityApi } from '#/api/mall/promotion/discount/discountActivity';
|
||||||
|
|
||||||
import { Button } from 'ant-design-vue';
|
import { confirm, DocAlert, Page, useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { message } from 'ant-design-vue';
|
||||||
|
|
||||||
|
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 hideLoading = message.loading({
|
||||||
|
content: '正在关闭中',
|
||||||
|
key: 'action_key_msg',
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await closeDiscountActivity(row.id as number);
|
||||||
|
message.success({
|
||||||
|
content: '关闭成功',
|
||||||
|
key: 'action_key_msg',
|
||||||
|
});
|
||||||
|
onRefresh();
|
||||||
|
} finally {
|
||||||
|
hideLoading();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除满减活动 */
|
||||||
|
async function handleDelete(row: MallDiscountActivityApi.DiscountActivity) {
|
||||||
|
const hideLoading = message.loading({
|
||||||
|
content: $t('ui.actionMessage.deleting', [row.name]),
|
||||||
|
key: 'action_key_msg',
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await deleteDiscountActivity(row.id as number);
|
||||||
|
message.success({
|
||||||
|
content: $t('ui.actionMessage.deleteSuccess', [row.name]),
|
||||||
|
key: 'action_key_msg',
|
||||||
|
});
|
||||||
|
onRefresh();
|
||||||
|
} finally {
|
||||||
|
hideLoading();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Page>
|
<Page auto-content-height>
|
||||||
<DocAlert
|
<template #doc>
|
||||||
title="【营销】限时折扣"
|
<DocAlert
|
||||||
url="https://doc.iocoder.cn/mall/promotion-discount/"
|
title="【营销】限时折扣"
|
||||||
/>
|
url="https://doc.iocoder.cn/mall/promotion-discount/"
|
||||||
<Button
|
/>
|
||||||
danger
|
</template>
|
||||||
type="link"
|
|
||||||
target="_blank"
|
<FormModal @success="onRefresh" />
|
||||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
|
|
||||||
>
|
<Grid table-title="限时折扣活动列表">
|
||||||
该功能支持 Vue3 + element-plus 版本!
|
<template #toolbar-tools>
|
||||||
</Button>
|
<TableAction
|
||||||
<br />
|
:actions="[
|
||||||
<Button
|
{
|
||||||
type="link"
|
label: $t('ui.actionTitle.create', ['限时折扣活动']),
|
||||||
target="_blank"
|
type: 'primary',
|
||||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/mall/promotion/discountActivity/index"
|
icon: ACTION_ICON.ADD,
|
||||||
>
|
auth: ['promotion:discount-activity:create'],
|
||||||
可参考
|
onClick: handleCreate,
|
||||||
https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/mall/promotion/discountActivity/index
|
},
|
||||||
代码,pull request 贡献给我们!
|
]"
|
||||||
</Button>
|
/>
|
||||||
|
</template>
|
||||||
|
<template #actions="{ row }">
|
||||||
|
<TableAction
|
||||||
|
:actions="[
|
||||||
|
{
|
||||||
|
label: $t('common.edit'),
|
||||||
|
type: 'link',
|
||||||
|
icon: ACTION_ICON.EDIT,
|
||||||
|
auth: ['promotion:discount-activity:update'],
|
||||||
|
onClick: handleEdit.bind(null, row),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '关闭',
|
||||||
|
type: 'link',
|
||||||
|
danger: true,
|
||||||
|
icon: ACTION_ICON.DELETE,
|
||||||
|
auth: ['promotion:discount-activity:close'],
|
||||||
|
ifShow: row.status === 0,
|
||||||
|
onClick: handleClose.bind(null, row),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('common.delete'),
|
||||||
|
type: 'link',
|
||||||
|
danger: 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>
|
</Page>
|
||||||
</template>
|
</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 { message } from 'ant-design-vue';
|
||||||
|
|
||||||
|
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');
|
||||||
|
message.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-antd/src/views/mall/promotion/diy/page/data.ts
Normal file
109
apps/web-antd/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: '创建时间',
|
||||||
|
width: 180,
|
||||||
|
formatter: 'formatDateTime',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
width: 200,
|
||||||
|
fixed: 'right',
|
||||||
|
slots: { default: 'actions' },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -1,29 +1,141 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { DocAlert, Page } from '@vben/common-ui';
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { MallDiyPageApi } from '#/api/mall/promotion/diy/page';
|
||||||
|
|
||||||
import { Button } from 'ant-design-vue';
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Page>
|
<Page auto-content-height>
|
||||||
<DocAlert title="【营销】商城装修" url="https://doc.iocoder.cn/mall/diy/" />
|
<template #doc>
|
||||||
<Button
|
<DocAlert
|
||||||
danger
|
title="【营销】商城装修"
|
||||||
type="link"
|
url="https://doc.iocoder.cn/mall/diy/"
|
||||||
target="_blank"
|
/>
|
||||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
|
</template>
|
||||||
>
|
|
||||||
该功能支持 Vue3 + element-plus 版本!
|
<FormModal @success="onRefresh" />
|
||||||
</Button>
|
|
||||||
<br />
|
<Grid table-title="装修页面列表">
|
||||||
<Button
|
<template #toolbar-tools>
|
||||||
type="link"
|
<TableAction
|
||||||
target="_blank"
|
:actions="[
|
||||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/mall/promotion/diy/page/index"
|
{
|
||||||
>
|
label: $t('ui.actionTitle.create', ['装修页面']),
|
||||||
可参考
|
type: 'primary',
|
||||||
https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/mall/promotion/diy/page/index
|
icon: ACTION_ICON.ADD,
|
||||||
代码,pull request 贡献给我们!
|
auth: ['promotion:diy-page:create'],
|
||||||
</Button>
|
onClick: handleCreate,
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template #actions="{ row }">
|
||||||
|
<TableAction
|
||||||
|
:actions="[
|
||||||
|
{
|
||||||
|
label: '装修',
|
||||||
|
type: 'link',
|
||||||
|
icon: ACTION_ICON.EDIT,
|
||||||
|
auth: ['promotion:diy-page:update'],
|
||||||
|
onClick: handleDecorate.bind(null, row),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('common.edit'),
|
||||||
|
type: 'link',
|
||||||
|
icon: ACTION_ICON.EDIT,
|
||||||
|
auth: ['promotion:diy-page:update'],
|
||||||
|
onClick: handleEdit.bind(null, row),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('common.delete'),
|
||||||
|
type: 'link',
|
||||||
|
danger: 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>
|
</Page>
|
||||||
</template>
|
</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 { message } from 'ant-design-vue';
|
||||||
|
|
||||||
|
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');
|
||||||
|
message.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-antd/src/views/mall/promotion/diy/template/data.ts
Normal file
120
apps/web-antd/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: '是否使用',
|
||||||
|
width: 100,
|
||||||
|
cellRender: {
|
||||||
|
name: 'CellDict',
|
||||||
|
props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'remark',
|
||||||
|
title: '备注',
|
||||||
|
minWidth: 200,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'createTime',
|
||||||
|
title: '创建时间',
|
||||||
|
width: 180,
|
||||||
|
formatter: 'formatDateTime',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
width: 250,
|
||||||
|
fixed: 'right',
|
||||||
|
slots: { default: 'actions' },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -1,29 +1,167 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { DocAlert, Page } from '@vben/common-ui';
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { MallDiyTemplateApi } from '#/api/mall/promotion/diy/template';
|
||||||
|
|
||||||
import { Button } from 'ant-design-vue';
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
|
import { confirm, DocAlert, Page, useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { message } from 'ant-design-vue';
|
||||||
|
|
||||||
|
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);
|
||||||
|
message.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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Page>
|
<Page auto-content-height>
|
||||||
<DocAlert title="【营销】商城装修" url="https://doc.iocoder.cn/mall/diy/" />
|
<template #doc>
|
||||||
<Button
|
<DocAlert
|
||||||
danger
|
title="【营销】商城装修"
|
||||||
type="link"
|
url="https://doc.iocoder.cn/mall/diy/"
|
||||||
target="_blank"
|
/>
|
||||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
|
</template>
|
||||||
>
|
|
||||||
该功能支持 Vue3 + element-plus 版本!
|
<FormModal @success="onRefresh" />
|
||||||
</Button>
|
|
||||||
<br />
|
<Grid table-title="装修模板列表">
|
||||||
<Button
|
<template #toolbar-tools>
|
||||||
type="link"
|
<TableAction
|
||||||
target="_blank"
|
:actions="[
|
||||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/mall/promotion/diy/template/index"
|
{
|
||||||
>
|
label: $t('ui.actionTitle.create', ['装修模板']),
|
||||||
可参考
|
type: 'primary',
|
||||||
https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/mall/promotion/diy/template/index
|
icon: ACTION_ICON.ADD,
|
||||||
代码,pull request 贡献给我们!
|
auth: ['promotion:diy-template:create'],
|
||||||
</Button>
|
onClick: handleCreate,
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template #actions="{ row }">
|
||||||
|
<TableAction
|
||||||
|
:actions="[
|
||||||
|
{
|
||||||
|
label: '装修',
|
||||||
|
type: 'link',
|
||||||
|
icon: ACTION_ICON.EDIT,
|
||||||
|
auth: ['promotion:diy-template:update'],
|
||||||
|
onClick: handleDecorate.bind(null, row),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('common.edit'),
|
||||||
|
type: 'link',
|
||||||
|
icon: ACTION_ICON.EDIT,
|
||||||
|
auth: ['promotion:diy-template:update'],
|
||||||
|
onClick: handleEdit.bind(null, row),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '使用',
|
||||||
|
type: 'link' as const,
|
||||||
|
auth: ['promotion:diy-template:use'],
|
||||||
|
ifShow: !row.used,
|
||||||
|
onClick: handleUse.bind(null, row),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('common.delete'),
|
||||||
|
type: 'link' as const,
|
||||||
|
danger: 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>
|
</Page>
|
||||||
</template>
|
</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 { message } from 'ant-design-vue';
|
||||||
|
|
||||||
|
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');
|
||||||
|
message.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>
|
||||||
140
apps/web-antd/src/views/mall/promotion/point/activity/data.ts
Normal file
140
apps/web-antd/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: '活动状态',
|
||||||
|
width: 100,
|
||||||
|
cellRender: {
|
||||||
|
name: 'CellDict',
|
||||||
|
props: { type: DICT_TYPE.COMMON_STATUS },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'stock',
|
||||||
|
title: '库存',
|
||||||
|
width: 80,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'totalStock',
|
||||||
|
title: '总库存',
|
||||||
|
width: 80,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'redeemedQuantity',
|
||||||
|
title: '已兑换数量',
|
||||||
|
width: 100,
|
||||||
|
slots: { default: 'redeemedQuantity' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'createTime',
|
||||||
|
title: '创建时间',
|
||||||
|
width: 180,
|
||||||
|
formatter: 'formatDateTime',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
width: 150,
|
||||||
|
fixed: 'right',
|
||||||
|
slots: { default: 'actions' },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -1,32 +1,161 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { DocAlert, Page } from '@vben/common-ui';
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { MallPointActivityApi } from '#/api/mall/promotion/point';
|
||||||
|
|
||||||
import { Button } from 'ant-design-vue';
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
import { confirm, DocAlert, Page, useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { message } from 'ant-design-vue';
|
||||||
|
|
||||||
|
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);
|
||||||
|
message.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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Page>
|
<Page auto-content-height>
|
||||||
<DocAlert
|
<template #doc>
|
||||||
title="【营销】积分商城活动"
|
<DocAlert
|
||||||
url="https://doc.iocoder.cn/mall/promotion-point/"
|
title="【营销】积分商城活动"
|
||||||
/>
|
url="https://doc.iocoder.cn/mall/promotion-point/"
|
||||||
<Button
|
/>
|
||||||
danger
|
</template>
|
||||||
type="link"
|
|
||||||
target="_blank"
|
<FormModal @success="onRefresh" />
|
||||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
|
|
||||||
>
|
<Grid table-title="积分商城活动列表">
|
||||||
该功能支持 Vue3 + element-plus 版本!
|
<template #toolbar-tools>
|
||||||
</Button>
|
<TableAction
|
||||||
<br />
|
:actions="[
|
||||||
<Button
|
{
|
||||||
type="link"
|
label: $t('ui.actionTitle.create', ['积分活动']),
|
||||||
target="_blank"
|
type: 'primary',
|
||||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/mall/promotion/point/activity/index"
|
icon: ACTION_ICON.ADD,
|
||||||
>
|
auth: ['promotion:point-activity:create'],
|
||||||
可参考
|
onClick: handleCreate,
|
||||||
https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/mall/promotion/point/activity/index
|
},
|
||||||
代码,pull request 贡献给我们!
|
]"
|
||||||
</Button>
|
/>
|
||||||
|
</template>
|
||||||
|
<template #redeemedQuantity="{ row }">
|
||||||
|
{{ getRedeemedQuantity(row) }}
|
||||||
|
</template>
|
||||||
|
<template #actions="{ row }">
|
||||||
|
<TableAction
|
||||||
|
:actions="[
|
||||||
|
{
|
||||||
|
label: $t('common.edit'),
|
||||||
|
type: 'link',
|
||||||
|
icon: ACTION_ICON.EDIT,
|
||||||
|
auth: ['promotion:point-activity:update'],
|
||||||
|
onClick: handleEdit.bind(null, row),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '关闭',
|
||||||
|
type: 'link',
|
||||||
|
danger: true,
|
||||||
|
auth: ['promotion:point-activity:close'],
|
||||||
|
ifShow: row.status === 0,
|
||||||
|
onClick: handleClose.bind(null, row),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('common.delete'),
|
||||||
|
type: 'link',
|
||||||
|
danger: 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>
|
</Page>
|
||||||
</template>
|
</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 { message } from 'ant-design-vue';
|
||||||
|
|
||||||
|
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');
|
||||||
|
message.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-antd/src/views/mall/promotion/rewardActivity/data.ts
Normal file
166
apps/web-antd/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: '活动开始时间',
|
||||||
|
width: 180,
|
||||||
|
formatter: 'formatDateTime',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'endTime',
|
||||||
|
title: '活动结束时间',
|
||||||
|
width: 180,
|
||||||
|
formatter: 'formatDateTime',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'status',
|
||||||
|
title: '状态',
|
||||||
|
minWidth: 100,
|
||||||
|
cellRender: {
|
||||||
|
name: 'CellDict',
|
||||||
|
props: { type: DICT_TYPE.COMMON_STATUS },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'createTime',
|
||||||
|
title: '创建时间',
|
||||||
|
width: 180,
|
||||||
|
formatter: 'formatDateTime',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
width: 180,
|
||||||
|
fixed: 'right',
|
||||||
|
slots: { default: 'actions' },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -1,32 +1,178 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { DocAlert, Page } from '@vben/common-ui';
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { MallRewardActivityApi } from '#/api/mall/promotion/reward/rewardActivity';
|
||||||
|
|
||||||
import { Button } from 'ant-design-vue';
|
import { confirm, DocAlert, Page, useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { message } from 'ant-design-vue';
|
||||||
|
|
||||||
|
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 hideLoading = message.loading({
|
||||||
|
content: '正在关闭中',
|
||||||
|
key: 'action_key_msg',
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await closeRewardActivity(row.id as number);
|
||||||
|
message.success({
|
||||||
|
content: '关闭成功',
|
||||||
|
key: 'action_key_msg',
|
||||||
|
});
|
||||||
|
onRefresh();
|
||||||
|
} finally {
|
||||||
|
hideLoading();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除活动 */
|
||||||
|
async function handleDelete(row: MallRewardActivityApi.RewardActivity) {
|
||||||
|
const hideLoading = message.loading({
|
||||||
|
content: $t('ui.actionMessage.deleting', [row.name]),
|
||||||
|
key: 'action_key_msg',
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await deleteRewardActivity(row.id as number);
|
||||||
|
message.success({
|
||||||
|
content: $t('ui.actionMessage.deleteSuccess', [row.name]),
|
||||||
|
key: 'action_key_msg',
|
||||||
|
});
|
||||||
|
onRefresh();
|
||||||
|
} finally {
|
||||||
|
hideLoading();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Page>
|
<Page auto-content-height>
|
||||||
<DocAlert
|
<template #doc>
|
||||||
title="【营销】满减送"
|
<DocAlert
|
||||||
url="https://doc.iocoder.cn/mall/promotion-record/"
|
title="【营销】满减送"
|
||||||
/>
|
url="https://doc.iocoder.cn/mall/promotion-record/"
|
||||||
<Button
|
/>
|
||||||
danger
|
</template>
|
||||||
type="link"
|
|
||||||
target="_blank"
|
<FormModal @success="onRefresh" />
|
||||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
|
|
||||||
>
|
<Grid table-title="满减送活动列表">
|
||||||
该功能支持 Vue3 + element-plus 版本!
|
<template #toolbar-tools>
|
||||||
</Button>
|
<TableAction
|
||||||
<br />
|
:actions="[
|
||||||
<Button
|
{
|
||||||
type="link"
|
label: $t('ui.actionTitle.create', ['满减送活动']),
|
||||||
target="_blank"
|
type: 'primary',
|
||||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/mall/promotion/rewardActivity/index"
|
icon: ACTION_ICON.ADD,
|
||||||
>
|
auth: ['promotion:reward-activity:create'],
|
||||||
可参考
|
onClick: handleCreate,
|
||||||
https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/mall/promotion/rewardActivity/index
|
},
|
||||||
代码,pull request 贡献给我们!
|
]"
|
||||||
</Button>
|
/>
|
||||||
|
</template>
|
||||||
|
<template #actions="{ row }">
|
||||||
|
<TableAction
|
||||||
|
:actions="[
|
||||||
|
{
|
||||||
|
label: $t('common.edit'),
|
||||||
|
type: 'link',
|
||||||
|
icon: ACTION_ICON.EDIT,
|
||||||
|
auth: ['promotion:reward-activity:update'],
|
||||||
|
onClick: handleEdit.bind(null, row),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '关闭',
|
||||||
|
type: 'link',
|
||||||
|
danger: true,
|
||||||
|
icon: ACTION_ICON.DELETE,
|
||||||
|
auth: ['promotion:reward-activity:close'],
|
||||||
|
ifShow: row.status === 0,
|
||||||
|
onClick: handleClose.bind(null, row),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('common.delete'),
|
||||||
|
type: 'link',
|
||||||
|
danger: 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>
|
</Page>
|
||||||
</template>
|
</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 { message } from 'ant-design-vue';
|
||||||
|
|
||||||
|
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');
|
||||||
|
message.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-antd/src/views/mall/promotion/seckill/activity/data.ts
Normal file
126
apps/web-antd/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: '秒杀时段',
|
||||||
|
width: 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',
|
||||||
|
width: 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')}`;
|
||||||
|
}
|
||||||
@@ -1,32 +1,198 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { DocAlert, Page } from '@vben/common-ui';
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { MallSeckillActivityApi } from '#/api/mall/promotion/seckill/seckillActivity';
|
||||||
|
|
||||||
import { Button } from 'ant-design-vue';
|
import { onMounted } from 'vue';
|
||||||
|
|
||||||
|
import { DocAlert, Page, useVbenModal } from '@vben/common-ui';
|
||||||
|
import { $t } from '@vben/locales';
|
||||||
|
|
||||||
|
import { message, Tag } from 'ant-design-vue';
|
||||||
|
|
||||||
|
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 hideLoading = message.loading({
|
||||||
|
content: $t('ui.actionMessage.closing', [row.name]),
|
||||||
|
key: 'action_key_msg',
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await closeSeckillActivity(row.id as number);
|
||||||
|
message.success({
|
||||||
|
content: '关闭成功',
|
||||||
|
key: 'action_key_msg',
|
||||||
|
});
|
||||||
|
onRefresh();
|
||||||
|
} finally {
|
||||||
|
hideLoading();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除活动 */
|
||||||
|
async function handleDelete(row: MallSeckillActivityApi.SeckillActivity) {
|
||||||
|
const hideLoading = message.loading({
|
||||||
|
content: $t('ui.actionMessage.deleting', [row.name]),
|
||||||
|
key: 'action_key_msg',
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await deleteSeckillActivity(row.id as number);
|
||||||
|
message.success({
|
||||||
|
content: $t('ui.actionMessage.deleteSuccess', [row.name]),
|
||||||
|
key: 'action_key_msg',
|
||||||
|
});
|
||||||
|
onRefresh();
|
||||||
|
} finally {
|
||||||
|
hideLoading();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Page>
|
<Page auto-content-height>
|
||||||
<DocAlert
|
<template #doc>
|
||||||
title="【营销】秒杀活动"
|
<DocAlert
|
||||||
url="https://doc.iocoder.cn/mall/promotion-seckill/"
|
title="【营销】秒杀活动"
|
||||||
/>
|
url="https://doc.iocoder.cn/mall/promotion-seckill/"
|
||||||
<Button
|
/>
|
||||||
danger
|
</template>
|
||||||
type="link"
|
|
||||||
target="_blank"
|
<FormModal @success="onRefresh" />
|
||||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
|
<Grid table-title="秒杀活动列表">
|
||||||
>
|
<template #toolbar-tools>
|
||||||
该功能支持 Vue3 + element-plus 版本!
|
<TableAction
|
||||||
</Button>
|
:actions="[
|
||||||
<br />
|
{
|
||||||
<Button
|
label: $t('ui.actionTitle.create', ['秒杀活动']),
|
||||||
type="link"
|
type: 'primary',
|
||||||
target="_blank"
|
icon: ACTION_ICON.ADD,
|
||||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/mall/promotion/seckill/activity/index"
|
auth: ['promotion:seckill-activity:create'],
|
||||||
>
|
onClick: handleCreate,
|
||||||
可参考
|
},
|
||||||
https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/mall/promotion/seckill/activity/index
|
]"
|
||||||
代码,pull request 贡献给我们!
|
/>
|
||||||
</Button>
|
</template>
|
||||||
|
|
||||||
|
<template #configIds="{ row }">
|
||||||
|
<div class="flex flex-wrap gap-1">
|
||||||
|
<Tag
|
||||||
|
v-for="(configId, index) in row.configIds"
|
||||||
|
:key="index"
|
||||||
|
class="mr-1"
|
||||||
|
>
|
||||||
|
{{ formatConfigNames(configId) }}
|
||||||
|
</Tag>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #timeRange="{ row }">
|
||||||
|
{{ formatTimeRange(row.startTime, row.endTime) }}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #actions="{ row }">
|
||||||
|
<TableAction
|
||||||
|
:actions="[
|
||||||
|
{
|
||||||
|
label: $t('common.edit'),
|
||||||
|
type: 'link',
|
||||||
|
icon: ACTION_ICON.EDIT,
|
||||||
|
auth: ['promotion:seckill-activity:update'],
|
||||||
|
onClick: handleEdit.bind(null, row),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '关闭',
|
||||||
|
type: 'link',
|
||||||
|
danger: true,
|
||||||
|
auth: ['promotion:seckill-activity:close'],
|
||||||
|
ifShow: row.status === 0,
|
||||||
|
popConfirm: {
|
||||||
|
title: '确认关闭该秒杀活动吗?',
|
||||||
|
confirm: handleClose.bind(null, row),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('common.delete'),
|
||||||
|
type: 'link',
|
||||||
|
danger: 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>
|
</Page>
|
||||||
</template>
|
</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 { message } from 'ant-design-vue';
|
||||||
|
|
||||||
|
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');
|
||||||
|
message.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>
|
||||||
@@ -2,7 +2,7 @@ import type { VbenFormSchema } from '#/adapter/form';
|
|||||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
import type { MallSeckillConfigApi } from '#/api/mall/promotion/seckill/seckillConfig';
|
import type { MallSeckillConfigApi } from '#/api/mall/promotion/seckill/seckillConfig';
|
||||||
|
|
||||||
import { DICT_TYPE, getDictOptions, getIntDictOptions } from '#/utils';
|
import { DICT_TYPE, getDictOptions } from '#/utils';
|
||||||
|
|
||||||
/** 新增/修改的表单 */
|
/** 新增/修改的表单 */
|
||||||
export function useFormSchema(): VbenFormSchema[] {
|
export function useFormSchema(): VbenFormSchema[] {
|
||||||
@@ -83,7 +83,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
|
|||||||
component: 'Select',
|
component: 'Select',
|
||||||
componentProps: {
|
componentProps: {
|
||||||
placeholder: '请选择状态',
|
placeholder: '请选择状态',
|
||||||
options: getIntDictOptions(DICT_TYPE.COMMON_STATUS),
|
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
128
apps/web-antd/src/views/mall/trade/brokerage/record/data.ts
Normal file
128
apps/web-antd/src/views/mall/trade/brokerage/record/data.ts
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
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: '头像',
|
||||||
|
width: 70,
|
||||||
|
slots: { default: 'userAvatar' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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: '解冻时间',
|
||||||
|
width: 180,
|
||||||
|
formatter: 'formatDateTime',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'createTime',
|
||||||
|
title: '创建时间',
|
||||||
|
width: 180,
|
||||||
|
formatter: 'formatDateTime',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -1,32 +1,63 @@
|
|||||||
<script lang="ts" setup>
|
<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 { DocAlert, Page } from '@vben/common-ui';
|
||||||
|
|
||||||
import { Button } from 'ant-design-vue';
|
import { Avatar } from 'ant-design-vue';
|
||||||
|
|
||||||
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Page>
|
<Page auto-content-height>
|
||||||
<DocAlert
|
<template #doc>
|
||||||
title="【交易】分销返佣"
|
<DocAlert
|
||||||
url="https://doc.iocoder.cn/mall/trade-brokerage/"
|
title="【交易】分销返佣"
|
||||||
/>
|
url="https://doc.iocoder.cn/mall/trade-brokerage/"
|
||||||
<Button
|
/>
|
||||||
danger
|
</template>
|
||||||
type="link"
|
|
||||||
target="_blank"
|
<Grid table-title="分销返佣记录">
|
||||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
|
<template #userAvatar="{ row }">
|
||||||
>
|
<Avatar :src="row.userAvatar" />
|
||||||
该功能支持 Vue3 + element-plus 版本!
|
</template>
|
||||||
</Button>
|
</Grid>
|
||||||
<br />
|
|
||||||
<Button
|
|
||||||
type="link"
|
|
||||||
target="_blank"
|
|
||||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/mall/trade/brokerage/record/index"
|
|
||||||
>
|
|
||||||
可参考
|
|
||||||
https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/mall/trade/brokerage/record/index
|
|
||||||
代码,pull request 贡献给我们!
|
|
||||||
</Button>
|
|
||||||
</Page>
|
</Page>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
133
apps/web-antd/src/views/mall/trade/brokerage/user/data.ts
Normal file
133
apps/web-antd/src/views/mall/trade/brokerage/user/data.ts
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
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: '头像',
|
||||||
|
width: 70,
|
||||||
|
slots: { default: 'avatar' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'nickname',
|
||||||
|
title: '昵称',
|
||||||
|
minWidth: 80,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'brokerageUserCount',
|
||||||
|
title: '推广人数',
|
||||||
|
width: 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: '成为推广员时间',
|
||||||
|
width: 180,
|
||||||
|
formatter: 'formatDateTime',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'bindUserId',
|
||||||
|
title: '上级推广员编号',
|
||||||
|
width: 150,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'bindUserTime',
|
||||||
|
title: '推广员绑定时间',
|
||||||
|
width: 180,
|
||||||
|
formatter: 'formatDateTime',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
width: 150,
|
||||||
|
fixed: 'right',
|
||||||
|
slots: { default: 'actions' },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -1,32 +1,227 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { DocAlert, Page } from '@vben/common-ui';
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { MallBrokerageUserApi } from '#/api/mall/trade/brokerage/user';
|
||||||
|
|
||||||
import { Button } from 'ant-design-vue';
|
import { useAccess } from '@vben/access';
|
||||||
|
import { DocAlert, Page, useVbenModal } from '@vben/common-ui';
|
||||||
|
import { $t } from '@vben/locales';
|
||||||
|
|
||||||
|
import { Avatar, message, Switch } from 'ant-design-vue';
|
||||||
|
|
||||||
|
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 hideLoading = message.loading({
|
||||||
|
content: `正在清除"${row.nickname}"的上级推广人...`,
|
||||||
|
key: 'clear_bind_user_msg',
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await clearBindUser({ id: row.id as number });
|
||||||
|
message.success({
|
||||||
|
content: '清除成功',
|
||||||
|
key: 'clear_bind_user_msg',
|
||||||
|
});
|
||||||
|
onRefresh();
|
||||||
|
} finally {
|
||||||
|
hideLoading();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 推广资格:开通/关闭 */
|
||||||
|
async function handleBrokerageEnabledChange(
|
||||||
|
row: MallBrokerageUserApi.BrokerageUser,
|
||||||
|
) {
|
||||||
|
const text = row.brokerageEnabled ? '开通' : '关闭';
|
||||||
|
const hideLoading = message.loading({
|
||||||
|
content: `正在${text}"${row.nickname}"的推广资格...`,
|
||||||
|
key: 'brokerage_enabled_msg',
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await updateBrokerageEnabled({
|
||||||
|
id: row.id as number,
|
||||||
|
enabled: row.brokerageEnabled as boolean,
|
||||||
|
});
|
||||||
|
message.success({
|
||||||
|
content: `${text}成功`,
|
||||||
|
key: 'brokerage_enabled_msg',
|
||||||
|
});
|
||||||
|
onRefresh();
|
||||||
|
} catch {
|
||||||
|
// 异常时,需要重置回之前的值
|
||||||
|
row.brokerageEnabled = !row.brokerageEnabled;
|
||||||
|
} finally {
|
||||||
|
hideLoading();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Page>
|
<Page auto-content-height>
|
||||||
<DocAlert
|
<template #doc>
|
||||||
title="【交易】分销返佣"
|
<DocAlert
|
||||||
url="https://doc.iocoder.cn/mall/trade-brokerage/"
|
title="【交易】分销返佣"
|
||||||
/>
|
url="https://doc.iocoder.cn/mall/trade-brokerage/"
|
||||||
<Button
|
/>
|
||||||
danger
|
</template>
|
||||||
type="link"
|
|
||||||
target="_blank"
|
<Grid table-title="分销用户列表">
|
||||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
|
<template #toolbar-tools>
|
||||||
>
|
<TableAction
|
||||||
该功能支持 Vue3 + element-plus 版本!
|
:actions="[
|
||||||
</Button>
|
{
|
||||||
<br />
|
label: $t('ui.actionTitle.create', ['分销员']),
|
||||||
<Button
|
type: 'primary',
|
||||||
type="link"
|
icon: ACTION_ICON.ADD,
|
||||||
target="_blank"
|
auth: ['trade:brokerage-user:create'],
|
||||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/mall/trade/brokerage/user/index"
|
onClick: openCreateUserForm,
|
||||||
>
|
},
|
||||||
可参考
|
]"
|
||||||
https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/mall/trade/brokerage/user/index
|
/>
|
||||||
代码,pull request 贡献给我们!
|
</template>
|
||||||
</Button>
|
|
||||||
|
<template #avatar="{ row }">
|
||||||
|
<Avatar :src="row.avatar" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #brokerageEnabled="{ row }">
|
||||||
|
<Switch
|
||||||
|
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: 'link',
|
||||||
|
auth: ['trade:brokerage-user:user-query'],
|
||||||
|
onClick: openBrokerageUserTable.bind(null, row),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '推广订单',
|
||||||
|
type: 'link',
|
||||||
|
auth: ['trade:brokerage-user:order-query'],
|
||||||
|
onClick: openBrokerageOrderTable.bind(null, row),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '修改上级推广人',
|
||||||
|
type: 'link',
|
||||||
|
auth: ['trade:brokerage-user:update-bind-user'],
|
||||||
|
onClick: openUpdateBindUserForm.bind(null, row),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '清除上级推广人',
|
||||||
|
type: 'link',
|
||||||
|
auth: ['trade:brokerage-user:clear-bind-user'],
|
||||||
|
onClick: handleClearBindUser.bind(null, row),
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- 修改上级推广人表单 -->
|
||||||
|
<UserUpdateModal @success="onRefresh" />
|
||||||
|
<!-- 推广人列表 -->
|
||||||
|
<UserListModal />
|
||||||
|
<!-- 推广订单列表 -->
|
||||||
|
<OrderListModal />
|
||||||
|
<!-- 创建分销员 -->
|
||||||
|
<UserCreateModal @success="onRefresh" />
|
||||||
</Page>
|
</Page>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -0,0 +1,193 @@
|
|||||||
|
<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 { Avatar, Tag } from 'ant-design-vue';
|
||||||
|
|
||||||
|
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: '头像',
|
||||||
|
width: 70,
|
||||||
|
slots: { default: 'avatar' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'sourceUserNickname',
|
||||||
|
title: '昵称',
|
||||||
|
minWidth: 80,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'price',
|
||||||
|
title: '佣金',
|
||||||
|
minWidth: 100,
|
||||||
|
formatter: ({ row }) => `¥${fenToYuan(row.price)}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'status',
|
||||||
|
title: '状态',
|
||||||
|
minWidth: 85,
|
||||||
|
slots: { default: '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="推广订单列表">
|
||||||
|
<template #avatar="{ row }">
|
||||||
|
<Avatar :src="row.sourceUserAvatar" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #status="{ row }">
|
||||||
|
<template
|
||||||
|
v-for="dict in getDictOptions(DICT_TYPE.BROKERAGE_RECORD_STATUS)"
|
||||||
|
:key="dict.value"
|
||||||
|
>
|
||||||
|
<Tag v-if="dict.value === row.status" :color="dict.colorType">
|
||||||
|
{{ dict.label }}
|
||||||
|
</Tag>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</Grid>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
<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 { $t } from '@vben/locales';
|
||||||
|
import { formatDate, isEmpty } from '@vben/utils';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Avatar,
|
||||||
|
Descriptions,
|
||||||
|
DescriptionsItem,
|
||||||
|
InputSearch,
|
||||||
|
message,
|
||||||
|
Tag,
|
||||||
|
} from 'ant-design-vue';
|
||||||
|
|
||||||
|
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');
|
||||||
|
message.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)) {
|
||||||
|
message.warning(`请先输入${userType}编号后重试!!!`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
userType === '推广员' &&
|
||||||
|
formData.value?.bindUserId === formData.value?.userId
|
||||||
|
) {
|
||||||
|
message.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) {
|
||||||
|
message.warning(`${userType}不存在`);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
message.warning(`${userType}不存在`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal title="创建分销员" class="w-2/5">
|
||||||
|
<div class="mr-2 flex items-center">
|
||||||
|
分销员编号:
|
||||||
|
<InputSearch
|
||||||
|
v-model:value="formData.userId"
|
||||||
|
placeholder="请输入推广员编号"
|
||||||
|
enter-button
|
||||||
|
class="mx-2 w-52"
|
||||||
|
@search="handleGetUser(formData?.userId, '分销员')"
|
||||||
|
/>
|
||||||
|
上级推广人编号:
|
||||||
|
<InputSearch
|
||||||
|
v-model:value="formData.bindUserId"
|
||||||
|
placeholder="请输入推广员编号"
|
||||||
|
enter-button
|
||||||
|
class="mx-2 w-52"
|
||||||
|
@search="handleGetUser(formData?.bindUserId, '推广员')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4">
|
||||||
|
<!-- 展示分销员的信息 -->
|
||||||
|
<Descriptions
|
||||||
|
title="分销员信息"
|
||||||
|
class="mt-4"
|
||||||
|
v-if="userInfo.user"
|
||||||
|
:column="1"
|
||||||
|
bordered
|
||||||
|
>
|
||||||
|
<DescriptionsItem label="头像">
|
||||||
|
<Avatar :src="userInfo.user?.avatar" />
|
||||||
|
</DescriptionsItem>
|
||||||
|
<DescriptionsItem label="昵称">
|
||||||
|
{{ userInfo.user?.nickname }}
|
||||||
|
</DescriptionsItem>
|
||||||
|
</Descriptions>
|
||||||
|
<!-- 展示上级推广人的信息 -->
|
||||||
|
<Descriptions
|
||||||
|
title="上级推广人信息"
|
||||||
|
class="mt-4"
|
||||||
|
v-if="userInfo.bindUser"
|
||||||
|
:column="1"
|
||||||
|
bordered
|
||||||
|
>
|
||||||
|
<DescriptionsItem label="头像">
|
||||||
|
<Avatar :src="userInfo.bindUser?.avatar" />
|
||||||
|
</DescriptionsItem>
|
||||||
|
<DescriptionsItem label="昵称">
|
||||||
|
{{ userInfo.bindUser?.nickname }}
|
||||||
|
</DescriptionsItem>
|
||||||
|
<DescriptionsItem label="推广资格">
|
||||||
|
<Tag v-if="userInfo.bindUser?.brokerageEnabled" color="success">
|
||||||
|
有
|
||||||
|
</Tag>
|
||||||
|
<Tag v-else>无</Tag>
|
||||||
|
</DescriptionsItem>
|
||||||
|
<DescriptionsItem label="成为推广员的时间">
|
||||||
|
{{ formatDate(userInfo.bindUser?.brokerageTime) }}
|
||||||
|
</DescriptionsItem>
|
||||||
|
</Descriptions>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
<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 { Avatar, Tag } from 'ant-design-vue';
|
||||||
|
|
||||||
|
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: '头像',
|
||||||
|
width: 70,
|
||||||
|
slots: { default: 'avatar' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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 #avatar="{ row }">
|
||||||
|
<Avatar :src="row.avatar" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #brokerageEnabled="{ row }">
|
||||||
|
<Tag v-if="row.brokerageEnabled" color="success">有</Tag>
|
||||||
|
<Tag v-else>无</Tag>
|
||||||
|
</template>
|
||||||
|
</Grid>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { MallBrokerageUserApi } from '#/api/mall/trade/brokerage/user';
|
||||||
|
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
import { $t } from '@vben/locales';
|
||||||
|
import { formatDate } from '@vben/utils';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Avatar,
|
||||||
|
Descriptions,
|
||||||
|
DescriptionsItem,
|
||||||
|
InputSearch,
|
||||||
|
message,
|
||||||
|
Tag,
|
||||||
|
} from 'ant-design-vue';
|
||||||
|
|
||||||
|
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) {
|
||||||
|
message.error('请先查询并确认推广人');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
modalApi.lock();
|
||||||
|
try {
|
||||||
|
await updateBindUser(formData.value);
|
||||||
|
// 关闭并提示
|
||||||
|
await modalApi.close();
|
||||||
|
emit('success');
|
||||||
|
message.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) {
|
||||||
|
message.error('不能绑定自己为推广人');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
bindUser.value = await getBrokerageUser(formData.value.bindUserId);
|
||||||
|
if (!bindUser.value) {
|
||||||
|
message.warning('推广员不存在');
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
message.warning('推广员不存在');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal title="修改上级推广人" class="w-2/5">
|
||||||
|
<div class="mr-2 flex items-center">
|
||||||
|
推广员编号:
|
||||||
|
<InputSearch
|
||||||
|
v-model:value="formData.bindUserId"
|
||||||
|
placeholder="请输入推广员编号"
|
||||||
|
enter-button
|
||||||
|
class="mx-2 w-52"
|
||||||
|
@search="handleGetUser"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 展示上级推广人的信息 -->
|
||||||
|
<Descriptions class="mt-4" v-if="bindUser" :column="1" bordered>
|
||||||
|
<DescriptionsItem label="头像">
|
||||||
|
<Avatar :src="bindUser.avatar" />
|
||||||
|
</DescriptionsItem>
|
||||||
|
<DescriptionsItem label="昵称">
|
||||||
|
{{ bindUser.nickname }}
|
||||||
|
</DescriptionsItem>
|
||||||
|
<DescriptionsItem label="推广资格">
|
||||||
|
<Tag v-if="bindUser.brokerageEnabled" color="success">有</Tag>
|
||||||
|
<Tag v-else>无</Tag>
|
||||||
|
</DescriptionsItem>
|
||||||
|
<DescriptionsItem label="成为推广员的时间">
|
||||||
|
{{ formatDate(bindUser.brokerageTime) }}
|
||||||
|
</DescriptionsItem>
|
||||||
|
</Descriptions>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
145
apps/web-antd/src/views/mall/trade/brokerage/withdraw/data.ts
Normal file
145
apps/web-antd/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' },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -1,32 +1,195 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { DocAlert, Page } from '@vben/common-ui';
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { MallBrokerageWithdrawApi } from '#/api/mall/trade/brokerage/withdraw';
|
||||||
|
|
||||||
import { Button } from 'ant-design-vue';
|
import { h } from 'vue';
|
||||||
|
|
||||||
|
import { confirm, Page, prompt } from '@vben/common-ui';
|
||||||
|
import { formatDateTime } from '@vben/utils';
|
||||||
|
|
||||||
|
import { Input, message } from 'ant-design-vue';
|
||||||
|
|
||||||
|
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);
|
||||||
|
message.success($t('ui.actionMessage.operationSuccess'));
|
||||||
|
onRefresh();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('审核失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 审核驳回 */
|
||||||
|
function handleReject(row: MallBrokerageWithdrawApi.BrokerageWithdraw) {
|
||||||
|
prompt({
|
||||||
|
component: () => {
|
||||||
|
return h(Input, {
|
||||||
|
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);
|
||||||
|
message.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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Page>
|
<Page auto-content-height>
|
||||||
<DocAlert
|
<Grid table-title="佣金提现列表">
|
||||||
title="【交易】分销返佣"
|
<template #withdraw-info="{ row }">
|
||||||
url="https://doc.iocoder.cn/mall/trade-brokerage/"
|
<div v-if="row.type === BrokerageWithdrawTypeEnum.WALLET.type">-</div>
|
||||||
/>
|
<div v-else>
|
||||||
<Button
|
<div v-if="row.userAccount">账号:{{ row.userAccount }}</div>
|
||||||
danger
|
<div v-if="row.userName">真实姓名:{{ row.userName }}</div>
|
||||||
type="link"
|
<template v-if="row.type === BrokerageWithdrawTypeEnum.BANK.type">
|
||||||
target="_blank"
|
<div v-if="row.bankName">银行名称:{{ row.bankName }}</div>
|
||||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
|
<div v-if="row.bankAddress">开户地址:{{ row.bankAddress }}</div>
|
||||||
>
|
</template>
|
||||||
该功能支持 Vue3 + element-plus 版本!
|
<div v-if="row.qrCodeUrl" class="mt-2">
|
||||||
</Button>
|
<div>收款码:</div>
|
||||||
<br />
|
<img :src="row.qrCodeUrl" class="mt-1 h-10 w-10" />
|
||||||
<Button
|
</div>
|
||||||
type="link"
|
</div>
|
||||||
target="_blank"
|
</template>
|
||||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/mall/trade/brokerage/withdraw/index"
|
|
||||||
>
|
<template #status-info="{ row }">
|
||||||
可参考
|
<div>
|
||||||
https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/mall/trade/brokerage/withdraw/index
|
<DictTag
|
||||||
代码,pull request 贡献给我们!
|
:value="row.status"
|
||||||
</Button>
|
: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: '通过',
|
||||||
|
type: 'link' as const,
|
||||||
|
icon: ACTION_ICON.EDIT,
|
||||||
|
auth: ['trade:brokerage-withdraw:audit'],
|
||||||
|
ifShow:
|
||||||
|
row.status === BrokerageWithdrawStatusEnum.AUDITING.status &&
|
||||||
|
!row.payTransferId,
|
||||||
|
onClick: () => handleApprove(row),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '驳回',
|
||||||
|
type: 'link' as const,
|
||||||
|
danger: true,
|
||||||
|
icon: ACTION_ICON.DELETE,
|
||||||
|
auth: ['trade:brokerage-withdraw:audit'],
|
||||||
|
ifShow:
|
||||||
|
row.status === BrokerageWithdrawStatusEnum.AUDITING.status &&
|
||||||
|
!row.payTransferId,
|
||||||
|
onClick: () => handleReject(row),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '重新转账',
|
||||||
|
type: 'link' as const,
|
||||||
|
icon: ACTION_ICON.REFRESH,
|
||||||
|
auth: ['trade:brokerage-withdraw:audit'],
|
||||||
|
ifShow:
|
||||||
|
row.status === BrokerageWithdrawStatusEnum.WITHDRAW_FAIL.status,
|
||||||
|
onClick: () => handleRetryTransfer(row),
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Grid>
|
||||||
</Page>
|
</Page>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -137,9 +137,6 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||||||
fieldName: 'brokeragePosterUrls',
|
fieldName: 'brokeragePosterUrls',
|
||||||
label: '分销海报图',
|
label: '分销海报图',
|
||||||
component: 'ImageUpload',
|
component: 'ImageUpload',
|
||||||
componentProps: {
|
|
||||||
maxSize: 1,
|
|
||||||
},
|
|
||||||
dependencies: {
|
dependencies: {
|
||||||
triggerFields: ['type'],
|
triggerFields: ['type'],
|
||||||
show: (values) => values.type === 'brokerage',
|
show: (values) => values.type === 'brokerage',
|
||||||
|
|||||||
@@ -31,9 +31,6 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||||||
component: 'ImageUpload',
|
component: 'ImageUpload',
|
||||||
fieldName: 'logo',
|
fieldName: 'logo',
|
||||||
label: '公司 logo',
|
label: '公司 logo',
|
||||||
componentProps: {
|
|
||||||
maxSize: 1,
|
|
||||||
},
|
|
||||||
rules: 'required',
|
rules: 'required',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -26,9 +26,6 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||||||
component: 'ImageUpload',
|
component: 'ImageUpload',
|
||||||
fieldName: 'logo',
|
fieldName: 'logo',
|
||||||
label: '门店logo',
|
label: '门店logo',
|
||||||
componentProps: {
|
|
||||||
maxSize: 1,
|
|
||||||
},
|
|
||||||
rules: 'required',
|
rules: 'required',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -52,17 +52,11 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||||||
component: 'ImageUpload',
|
component: 'ImageUpload',
|
||||||
fieldName: 'icon',
|
fieldName: 'icon',
|
||||||
label: '等级图标',
|
label: '等级图标',
|
||||||
componentProps: {
|
|
||||||
maxSize: 1,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
component: 'ImageUpload',
|
component: 'ImageUpload',
|
||||||
fieldName: 'backgroundUrl',
|
fieldName: 'backgroundUrl',
|
||||||
label: '等级背景图',
|
label: '等级背景图',
|
||||||
componentProps: {
|
|
||||||
maxSize: 1,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldName: 'status',
|
fieldName: 'status',
|
||||||
|
|||||||
@@ -56,9 +56,6 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||||||
component: 'ImageUpload',
|
component: 'ImageUpload',
|
||||||
fieldName: 'avatar',
|
fieldName: 'avatar',
|
||||||
label: '头像',
|
label: '头像',
|
||||||
componentProps: {
|
|
||||||
maxSize: 1,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
component: 'Input',
|
component: 'Input',
|
||||||
|
|||||||
@@ -2,12 +2,7 @@ import type { VbenFormSchema } from '#/adapter/form';
|
|||||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
|
||||||
import { getAppList } from '#/api/pay/app';
|
import { getAppList } from '#/api/pay/app';
|
||||||
import {
|
import { DICT_TYPE, getDictOptions, getRangePickerDefaultProps } from '#/utils';
|
||||||
DICT_TYPE,
|
|
||||||
getIntDictOptions,
|
|
||||||
getRangePickerDefaultProps,
|
|
||||||
getStrDictOptions,
|
|
||||||
} from '#/utils';
|
|
||||||
|
|
||||||
/** 列表的搜索表单 */
|
/** 列表的搜索表单 */
|
||||||
export function useGridFormSchema(): VbenFormSchema[] {
|
export function useGridFormSchema(): VbenFormSchema[] {
|
||||||
@@ -34,7 +29,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
|
|||||||
component: 'Select',
|
component: 'Select',
|
||||||
componentProps: {
|
componentProps: {
|
||||||
allowClear: true,
|
allowClear: true,
|
||||||
options: getStrDictOptions(DICT_TYPE.PAY_CHANNEL_CODE),
|
options: getDictOptions(DICT_TYPE.PAY_CHANNEL_CODE, 'string'),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -63,7 +58,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
|
|||||||
component: 'Select',
|
component: 'Select',
|
||||||
componentProps: {
|
componentProps: {
|
||||||
allowClear: true,
|
allowClear: true,
|
||||||
options: getIntDictOptions(DICT_TYPE.PAY_REFUND_STATUS),
|
options: getDictOptions(DICT_TYPE.PAY_REFUND_STATUS, 'number'),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -111,6 +111,9 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
pagerConfig: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
rowConfig: {
|
rowConfig: {
|
||||||
keyField: 'id',
|
keyField: 'id',
|
||||||
isHover: true,
|
isHover: true,
|
||||||
|
|||||||
@@ -46,9 +46,6 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||||||
fieldName: 'logo',
|
fieldName: 'logo',
|
||||||
label: '应用图标',
|
label: '应用图标',
|
||||||
component: 'ImageUpload',
|
component: 'ImageUpload',
|
||||||
componentProps: {
|
|
||||||
limit: 1,
|
|
||||||
},
|
|
||||||
rules: 'required',
|
rules: 'required',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -150,8 +150,8 @@ export async function saveUserApi(user: UserInfo) {
|
|||||||
```ts
|
```ts
|
||||||
import { requestClient } from '#/api/request';
|
import { requestClient } from '#/api/request';
|
||||||
|
|
||||||
export async function deleteUserApi(user: UserInfo) {
|
export async function deleteUserApi(userId: number) {
|
||||||
return requestClient.delete<boolean>(`/user/${user.id}`, user);
|
return requestClient.delete<boolean>(`/user/${userId}`);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -180,8 +180,8 @@ export async function saveUserApi(user: UserInfo) {
|
|||||||
```ts
|
```ts
|
||||||
import { requestClient } from '#/api/request';
|
import { requestClient } from '#/api/request';
|
||||||
|
|
||||||
export async function deleteUserApi(user: UserInfo) {
|
export async function deleteUserApi(userId: number) {
|
||||||
return requestClient.delete<boolean>(`/user/${user.id}`, user);
|
return requestClient.delete<boolean>(`/user/${userId}`);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,15 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { DrawerProps, ExtendedDrawerApi } from './drawer';
|
import type { DrawerProps, ExtendedDrawerApi } from './drawer';
|
||||||
|
|
||||||
import { computed, provide, ref, unref, useId, watch } from 'vue';
|
import {
|
||||||
|
computed,
|
||||||
|
onDeactivated,
|
||||||
|
provide,
|
||||||
|
ref,
|
||||||
|
unref,
|
||||||
|
useId,
|
||||||
|
watch,
|
||||||
|
} from 'vue';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
useIsMobile,
|
useIsMobile,
|
||||||
@@ -94,6 +102,16 @@ const {
|
|||||||
// },
|
// },
|
||||||
// );
|
// );
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在开启keepAlive情况下 直接通过浏览器按钮/手势等返回 不会关闭弹窗
|
||||||
|
*/
|
||||||
|
onDeactivated(() => {
|
||||||
|
// 如果弹窗没有被挂载到内容区域,则关闭弹窗
|
||||||
|
if (!appendToMain.value) {
|
||||||
|
props.drawerApi?.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
function interactOutside(e: Event) {
|
function interactOutside(e: Event) {
|
||||||
if (!closeOnClickModal.value || submitting.value) {
|
if (!closeOnClickModal.value || submitting.value) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import {
|
|||||||
h,
|
h,
|
||||||
inject,
|
inject,
|
||||||
nextTick,
|
nextTick,
|
||||||
onDeactivated,
|
|
||||||
provide,
|
provide,
|
||||||
reactive,
|
reactive,
|
||||||
ref,
|
ref,
|
||||||
@@ -72,13 +71,6 @@ export function useVbenDrawer<
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
|
||||||
* 在开启keepAlive情况下 直接通过浏览器按钮/手势等返回 不会关闭弹窗
|
|
||||||
*/
|
|
||||||
onDeactivated(() => {
|
|
||||||
(extendedApi as ExtendedDrawerApi)?.close?.();
|
|
||||||
});
|
|
||||||
|
|
||||||
return [Drawer, extendedApi as ExtendedDrawerApi] as const;
|
return [Drawer, extendedApi as ExtendedDrawerApi] as const;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,16 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { ExtendedModalApi, ModalProps } from './modal';
|
import type { ExtendedModalApi, ModalProps } from './modal';
|
||||||
|
|
||||||
import { computed, nextTick, provide, ref, unref, useId, watch } from 'vue';
|
import {
|
||||||
|
computed,
|
||||||
|
nextTick,
|
||||||
|
onDeactivated,
|
||||||
|
provide,
|
||||||
|
ref,
|
||||||
|
unref,
|
||||||
|
useId,
|
||||||
|
watch,
|
||||||
|
} from 'vue';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
useIsMobile,
|
useIsMobile,
|
||||||
@@ -135,6 +144,16 @@ watch(
|
|||||||
// },
|
// },
|
||||||
// );
|
// );
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在开启keepAlive情况下 直接通过浏览器按钮/手势等返回 不会关闭弹窗
|
||||||
|
*/
|
||||||
|
onDeactivated(() => {
|
||||||
|
// 如果弹窗没有被挂载到内容区域,则关闭弹窗
|
||||||
|
if (!appendToMain.value) {
|
||||||
|
props.modalApi?.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
function handleFullscreen() {
|
function handleFullscreen() {
|
||||||
props.modalApi?.setState((prev) => {
|
props.modalApi?.setState((prev) => {
|
||||||
// if (prev.fullscreen) {
|
// if (prev.fullscreen) {
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import {
|
|||||||
h,
|
h,
|
||||||
inject,
|
inject,
|
||||||
nextTick,
|
nextTick,
|
||||||
onDeactivated,
|
|
||||||
provide,
|
provide,
|
||||||
reactive,
|
reactive,
|
||||||
ref,
|
ref,
|
||||||
@@ -71,13 +70,6 @@ export function useVbenModal<TParentModalProps extends ModalProps = ModalProps>(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
|
||||||
* 在开启keepAlive情况下 直接通过浏览器按钮/手势等返回 不会关闭弹窗
|
|
||||||
*/
|
|
||||||
onDeactivated(() => {
|
|
||||||
(extendedApi as ExtendedModalApi)?.close?.();
|
|
||||||
});
|
|
||||||
|
|
||||||
return [Modal, extendedApi as ExtendedModalApi] as const;
|
return [Modal, extendedApi as ExtendedModalApi] as const;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,6 +122,7 @@ export function useVbenModal<TParentModalProps extends ModalProps = ModalProps>(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
injectData.extendApi?.(extendedApi);
|
injectData.extendApi?.(extendedApi);
|
||||||
|
|
||||||
return [Modal, extendedApi] as const;
|
return [Modal, extendedApi] as const;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ import type { Component } from 'vue';
|
|||||||
|
|
||||||
import type { AnyPromiseFunction } from '@vben/types';
|
import type { AnyPromiseFunction } from '@vben/types';
|
||||||
|
|
||||||
import { computed, ref, unref, useAttrs, watch } from 'vue';
|
import { computed, nextTick, ref, unref, useAttrs, watch } from 'vue';
|
||||||
|
|
||||||
import { LoaderCircle } from '@vben/icons';
|
import { LoaderCircle } from '@vben/icons';
|
||||||
|
|
||||||
import { get, isEqual, isFunction } from '@vben-core/shared/utils';
|
import { cloneDeep, get, isEqual, isFunction } from '@vben-core/shared/utils';
|
||||||
|
|
||||||
import { objectOmit } from '@vueuse/core';
|
import { objectOmit } from '@vueuse/core';
|
||||||
|
|
||||||
@@ -104,6 +104,8 @@ const refOptions = ref<OptionsItem[]>([]);
|
|||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
// 首次是否加载过了
|
// 首次是否加载过了
|
||||||
const isFirstLoaded = ref(false);
|
const isFirstLoaded = ref(false);
|
||||||
|
// 标记是否有待处理的请求
|
||||||
|
const hasPendingRequest = ref(false);
|
||||||
|
|
||||||
const getOptions = computed(() => {
|
const getOptions = computed(() => {
|
||||||
const { labelField, valueField, childrenField, numberToString } = props;
|
const { labelField, valueField, childrenField, numberToString } = props;
|
||||||
@@ -146,18 +148,26 @@ const bindProps = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
async function fetchApi() {
|
async function fetchApi() {
|
||||||
let { api, beforeFetch, afterFetch, params, resultField } = props;
|
const { api, beforeFetch, afterFetch, resultField } = props;
|
||||||
|
|
||||||
if (!api || !isFunction(api) || loading.value) {
|
if (!api || !isFunction(api)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 如果正在加载,标记有待处理的请求并返回
|
||||||
|
if (loading.value) {
|
||||||
|
hasPendingRequest.value = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
refOptions.value = [];
|
refOptions.value = [];
|
||||||
try {
|
try {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
|
let finalParams = unref(mergedParams);
|
||||||
if (beforeFetch && isFunction(beforeFetch)) {
|
if (beforeFetch && isFunction(beforeFetch)) {
|
||||||
params = (await beforeFetch(params)) || params;
|
finalParams = (await beforeFetch(cloneDeep(finalParams))) || finalParams;
|
||||||
}
|
}
|
||||||
let res = await api(params);
|
let res = await api(finalParams);
|
||||||
if (afterFetch && isFunction(afterFetch)) {
|
if (afterFetch && isFunction(afterFetch)) {
|
||||||
res = (await afterFetch(res)) || res;
|
res = (await afterFetch(res)) || res;
|
||||||
}
|
}
|
||||||
@@ -177,6 +187,13 @@ async function fetchApi() {
|
|||||||
isFirstLoaded.value = false;
|
isFirstLoaded.value = false;
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
|
// 如果有待处理的请求,立即触发新的请求
|
||||||
|
if (hasPendingRequest.value) {
|
||||||
|
hasPendingRequest.value = false;
|
||||||
|
// 使用 nextTick 确保状态更新完成后再触发新请求
|
||||||
|
await nextTick();
|
||||||
|
fetchApi();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,7 +207,7 @@ async function handleFetchForVisible(visible: boolean) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const params = computed(() => {
|
const mergedParams = computed(() => {
|
||||||
return {
|
return {
|
||||||
...props.params,
|
...props.params,
|
||||||
...unref(innerParams),
|
...unref(innerParams),
|
||||||
@@ -198,7 +215,7 @@ const params = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
params,
|
mergedParams,
|
||||||
(value, oldValue) => {
|
(value, oldValue) => {
|
||||||
if (isEqual(value, oldValue)) {
|
if (isEqual(value, oldValue)) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
Reference in New Issue
Block a user