Merge remote-tracking branch 'yudao/dev' into dev
commit
6138a610ee
|
@ -29,12 +29,14 @@ export namespace MallBrokerageUserApi {
|
|||
export interface CreateRequest {
|
||||
/** 用户编号 */
|
||||
userId: number;
|
||||
/** 推广员编号 */
|
||||
bindUserId: number;
|
||||
}
|
||||
|
||||
/** 修改推广员请求 */
|
||||
export interface UpdateBindUserRequest {
|
||||
/** 用户编号 */
|
||||
userId: number;
|
||||
id: number;
|
||||
/** 推广员编号 */
|
||||
bindUserId: number;
|
||||
}
|
||||
|
@ -42,15 +44,15 @@ export namespace MallBrokerageUserApi {
|
|||
/** 清除推广员请求 */
|
||||
export interface ClearBindUserRequest {
|
||||
/** 用户编号 */
|
||||
userId: number;
|
||||
id: number;
|
||||
}
|
||||
|
||||
/** 修改推广资格请求 */
|
||||
export interface UpdateBrokerageEnabledRequest {
|
||||
/** 用户编号 */
|
||||
userId: number;
|
||||
id: number;
|
||||
/** 是否启用分销 */
|
||||
brokerageEnabled: boolean;
|
||||
enabled: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1 @@
|
|||
export { default as CronTab } from './cron-tab.vue';
|
|
@ -75,7 +75,7 @@ defineExpose({
|
|||
<div class="inline-block text-center" :style="getStyle">
|
||||
<!-- 图片包装器 -->
|
||||
<div
|
||||
class="group relative cursor-pointer overflow-hidden rounded-full border border-gray-200 bg-white"
|
||||
class="bg-card group relative cursor-pointer overflow-hidden rounded-full border border-gray-200"
|
||||
:style="getImageWrapperStyle"
|
||||
@click="openModal"
|
||||
>
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { isValidColor, TinyColor } from '@vben/utils';
|
||||
|
||||
import { Tag } from 'ant-design-vue';
|
||||
|
||||
// import { isHexColor } from '@/utils/color' // TODO @芋艿:【可优化】增加 cssClass 的处理 https://gitee.com/yudaocode/yudao-ui-admin-vben/blob/v2.4.1/src/components/DictTag/src/DictTag.vue#L60
|
||||
import { getDictObj } from '#/utils';
|
||||
|
||||
interface DictTagProps {
|
||||
|
@ -23,11 +24,6 @@ interface DictTagProps {
|
|||
|
||||
const props = defineProps<DictTagProps>();
|
||||
|
||||
function isHexColor(color: string) {
|
||||
const reg = /^#(?:[0-9a-f]{3}|[0-9a-f]{6})$/i;
|
||||
return reg.test(color);
|
||||
}
|
||||
|
||||
/** 获取字典标签 */
|
||||
const dictTag = computed(() => {
|
||||
// 校验参数有效性
|
||||
|
@ -63,9 +59,14 @@ const dictTag = computed(() => {
|
|||
}
|
||||
}
|
||||
|
||||
if (isValidColor(dict.cssClass)) {
|
||||
colorType = new TinyColor(dict.cssClass).toHexString();
|
||||
}
|
||||
|
||||
return {
|
||||
label: dict.label || '',
|
||||
colorType,
|
||||
cssClass: dict.cssClass,
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
@ -73,13 +74,7 @@ const dictTag = computed(() => {
|
|||
<template>
|
||||
<Tag
|
||||
v-if="dictTag"
|
||||
:color="
|
||||
dictTag.colorType
|
||||
? dictTag.colorType
|
||||
: dictTag.cssClass && isHexColor(dictTag.cssClass)
|
||||
? dictTag.cssClass
|
||||
: ''
|
||||
"
|
||||
:color="dictTag.colorType ? dictTag.colorType : dictTag.cssClass"
|
||||
>
|
||||
{{ dictTag.label }}
|
||||
</Tag>
|
||||
|
|
|
@ -13,7 +13,7 @@ import {
|
|||
SelectOption,
|
||||
} from 'ant-design-vue';
|
||||
|
||||
import { getDictObj, getIntDictOptions, getStrDictOptions } from '#/utils';
|
||||
import { getDictOptions } from '#/utils';
|
||||
|
||||
defineOptions({ name: 'DictSelect' });
|
||||
|
||||
|
@ -25,17 +25,16 @@ const props = withDefaults(defineProps<DictSelectProps>(), {
|
|||
const attrs = useAttrs();
|
||||
|
||||
// 获得字典配置
|
||||
// TODO @dhb:可以使用 getDictOptions 替代么?
|
||||
const getDictOptions = computed(() => {
|
||||
const getDictOption = computed(() => {
|
||||
switch (props.valueType) {
|
||||
case 'bool': {
|
||||
return getDictObj(props.dictType, 'bool');
|
||||
return getDictOptions(props.dictType, 'boolean');
|
||||
}
|
||||
case 'int': {
|
||||
return getIntDictOptions(props.dictType);
|
||||
return getDictOptions(props.dictType, 'number');
|
||||
}
|
||||
case 'str': {
|
||||
return getStrDictOptions(props.dictType);
|
||||
return getDictOptions(props.dictType, 'string');
|
||||
}
|
||||
default: {
|
||||
return [];
|
||||
|
@ -47,7 +46,7 @@ const getDictOptions = computed(() => {
|
|||
<template>
|
||||
<Select v-if="selectType === 'select'" class="w-full" v-bind="attrs">
|
||||
<SelectOption
|
||||
v-for="(dict, index) in getDictOptions"
|
||||
v-for="(dict, index) in getDictOption"
|
||||
:key="index"
|
||||
:value="dict.value"
|
||||
>
|
||||
|
@ -56,7 +55,7 @@ const getDictOptions = computed(() => {
|
|||
</Select>
|
||||
<RadioGroup v-if="selectType === 'radio'" class="w-full" v-bind="attrs">
|
||||
<Radio
|
||||
v-for="(dict, index) in getDictOptions"
|
||||
v-for="(dict, index) in getDictOption"
|
||||
:key="index"
|
||||
:value="dict.value"
|
||||
>
|
||||
|
@ -65,7 +64,7 @@ const getDictOptions = computed(() => {
|
|||
</RadioGroup>
|
||||
<CheckboxGroup v-if="selectType === 'checkbox'" class="w-full" v-bind="attrs">
|
||||
<Checkbox
|
||||
v-for="(dict, index) in getDictOptions"
|
||||
v-for="(dict, index) in getDictOption"
|
||||
:key="index"
|
||||
:value="dict.value"
|
||||
>
|
||||
|
|
|
@ -238,7 +238,7 @@ defineExpose({ validate });
|
|||
<ErrorModal title="流程设计校验不通过" class="w-2/5">
|
||||
<div class="mb-2 text-base">以下节点配置不完善,请修改相关配置</div>
|
||||
<div
|
||||
class="mb-3 rounded-md bg-gray-100 p-2 text-sm"
|
||||
class="mb-3 rounded-md p-2 text-sm"
|
||||
v-for="(item, index) in errorModalApi.getData()"
|
||||
:key="index"
|
||||
>
|
||||
|
|
|
@ -201,7 +201,7 @@ onMounted(() => {
|
|||
</script>
|
||||
<template>
|
||||
<div class="simple-process-model-container">
|
||||
<div class="absolute right-0 top-0 bg-white">
|
||||
<div class="bg-card absolute right-0 top-0">
|
||||
<Row type="flex" justify="end">
|
||||
<ButtonGroup key="scale-control">
|
||||
<Button v-if="!readonly" @click="exportJson">
|
||||
|
@ -258,7 +258,7 @@ onMounted(() => {
|
|||
>
|
||||
<div class="mb-2">以下节点内容不完善,请修改后保存</div>
|
||||
<div
|
||||
class="line-height-normal mb-3 rounded bg-gray-100 p-2"
|
||||
class="line-height-normal mb-3 rounded p-2"
|
||||
v-for="(item, index) in errorNodes"
|
||||
:key="index"
|
||||
>
|
||||
|
|
|
@ -4,7 +4,7 @@ import type { PropType } from 'vue';
|
|||
|
||||
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 { IconifyIcon } from '@vben/icons';
|
||||
|
@ -41,6 +41,14 @@ const props = defineProps({
|
|||
|
||||
const { hasAccessByCodes } = useAccess();
|
||||
|
||||
/** 缓存处理后的actions */
|
||||
const processedActions = ref<any[]>([]);
|
||||
const processedDropdownActions = ref<any[]>([]);
|
||||
|
||||
/** 用于比较的字符串化版本 */
|
||||
const actionsStringified = ref('');
|
||||
const dropdownActionsStringified = ref('');
|
||||
|
||||
function isIfShow(action: ActionItem): boolean {
|
||||
const ifShow = action.ifShow;
|
||||
let isIfShow = true;
|
||||
|
@ -57,8 +65,8 @@ function isIfShow(action: ActionItem): boolean {
|
|||
return isIfShow;
|
||||
}
|
||||
|
||||
const getActions = computed(() => {
|
||||
const actions = toRaw(props.actions) || [];
|
||||
/** 处理actions的纯函数 */
|
||||
function processActions(actions: ActionItem[]): any[] {
|
||||
return actions
|
||||
.filter((action: ActionItem) => {
|
||||
return isIfShow(action);
|
||||
|
@ -74,30 +82,101 @@ const getActions = computed(() => {
|
|||
enable: !!popConfirm,
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const getDropdownList = computed((): any[] => {
|
||||
const dropDownActions = toRaw(props.dropDownActions) || [];
|
||||
/** 处理下拉菜单actions的纯函数 */
|
||||
function processDropdownActions(
|
||||
dropDownActions: ActionItem[],
|
||||
divider: boolean,
|
||||
): any[] {
|
||||
return dropDownActions
|
||||
.filter((action: ActionItem) => {
|
||||
return isIfShow(action);
|
||||
})
|
||||
.map((action: ActionItem, index: number) => {
|
||||
const { label, popConfirm } = action;
|
||||
delete action.icon;
|
||||
const processedAction = { ...action };
|
||||
delete processedAction.icon;
|
||||
return {
|
||||
...action,
|
||||
...processedAction,
|
||||
...popConfirm,
|
||||
onConfirm: popConfirm?.confirm,
|
||||
onCancel: popConfirm?.cancel,
|
||||
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) {
|
||||
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;
|
||||
if (attrs.confirm && isFunction(attrs.confirm)) {
|
||||
originAttrs.onConfirm = attrs.confirm;
|
||||
|
@ -107,34 +186,76 @@ function getPopConfirmProps(attrs: PopConfirm) {
|
|||
originAttrs.onCancel = attrs.cancel;
|
||||
delete originAttrs.cancel;
|
||||
}
|
||||
|
||||
popConfirmPropsMap.set(key, originAttrs);
|
||||
return originAttrs;
|
||||
}
|
||||
|
||||
/** 缓存Button属性 */
|
||||
const buttonPropsMap = new Map<string, any>();
|
||||
|
||||
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 = {
|
||||
type: action.type || 'primary',
|
||||
...action,
|
||||
disabled: action.disabled,
|
||||
loading: action.loading,
|
||||
size: action.size,
|
||||
};
|
||||
delete res.icon;
|
||||
|
||||
buttonPropsMap.set(key, 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) {
|
||||
const action = getDropdownList.value[e.key];
|
||||
if (action.onClick && isFunction(action.onClick)) {
|
||||
action.onClick();
|
||||
}
|
||||
}
|
||||
|
||||
/** 生成稳定的key */
|
||||
function getActionKey(action: ActionItem, index: number) {
|
||||
return `${action.label || ''}-${action.type || ''}-${index}`;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="table-actions">
|
||||
<Space
|
||||
:size="
|
||||
getActions?.some((item: ActionItem) => item.type === 'link') ? 0 : 8
|
||||
"
|
||||
>
|
||||
<template v-for="(action, index) in getActions" :key="index">
|
||||
<Space :size="spaceSize">
|
||||
<template
|
||||
v-for="(action, index) in getActions"
|
||||
:key="getActionKey(action, index)"
|
||||
>
|
||||
<Popconfirm
|
||||
v-if="action.popConfirm"
|
||||
v-bind="getPopConfirmProps(action.popConfirm)"
|
||||
|
@ -142,13 +263,7 @@ function handleMenuClick(e: any) {
|
|||
<template v-if="action.popConfirm.icon" #icon>
|
||||
<IconifyIcon :icon="action.popConfirm.icon" />
|
||||
</template>
|
||||
<Tooltip
|
||||
v-bind="
|
||||
typeof action.tooltip === 'string'
|
||||
? { title: action.tooltip }
|
||||
: { ...action.tooltip }
|
||||
"
|
||||
>
|
||||
<Tooltip v-bind="getTooltipProps(action.tooltip)">
|
||||
<Button v-bind="getButtonProps(action)">
|
||||
<template v-if="action.icon" #icon>
|
||||
<IconifyIcon :icon="action.icon" />
|
||||
|
@ -157,14 +272,7 @@ function handleMenuClick(e: any) {
|
|||
</Button>
|
||||
</Tooltip>
|
||||
</Popconfirm>
|
||||
<Tooltip
|
||||
v-else
|
||||
v-bind="
|
||||
typeof action.tooltip === 'string'
|
||||
? { title: action.tooltip }
|
||||
: { ...action.tooltip }
|
||||
"
|
||||
>
|
||||
<Tooltip v-else v-bind="getTooltipProps(action.tooltip)">
|
||||
<Button v-bind="getButtonProps(action)" @click="action.onClick">
|
||||
<template v-if="action.icon" #icon>
|
||||
<IconifyIcon :icon="action.icon" />
|
||||
|
@ -185,8 +293,13 @@ function handleMenuClick(e: any) {
|
|||
</Button>
|
||||
</slot>
|
||||
<template #overlay>
|
||||
<Menu @click="handleMenuClick">
|
||||
<Menu.Item v-for="(action, index) in getDropdownList" :key="index">
|
||||
<Menu>
|
||||
<Menu.Item
|
||||
v-for="(action, index) in getDropdownList"
|
||||
:key="index"
|
||||
:disabled="action.disabled"
|
||||
@click="!action.popConfirm && handleMenuClick({ key: index })"
|
||||
>
|
||||
<template v-if="action.popConfirm">
|
||||
<Popconfirm v-bind="getPopConfirmProps(action.popConfirm)">
|
||||
<template v-if="action.popConfirm.icon" #icon>
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
// TODO @芋艿:后续再优化
|
||||
// TODO @芋艿:可以共享么?
|
||||
|
||||
import type { DictItem } from '#/store';
|
||||
|
||||
import { isObject } from '@vben/utils';
|
||||
|
||||
import { useDictStore } from '#/store';
|
||||
|
@ -9,33 +11,103 @@ import { useDictStore } from '#/store';
|
|||
// 先临时移入到方法中
|
||||
// const dictStore = useDictStore();
|
||||
|
||||
// TODO @dhb: antd 组件的 color 类型
|
||||
/** AntD 组件的颜色类型 */
|
||||
type ColorType = 'error' | 'info' | 'success' | 'warning';
|
||||
|
||||
/** 字典值类型 */
|
||||
type DictValueType = 'boolean' | 'number' | 'string';
|
||||
|
||||
/** 基础字典数据类型 */
|
||||
export interface DictDataType {
|
||||
dictType?: string;
|
||||
label: string;
|
||||
value: boolean | number | string;
|
||||
colorType?: ColorType;
|
||||
colorType?: string;
|
||||
cssClass?: string;
|
||||
}
|
||||
|
||||
/** 数字类型字典数据 */
|
||||
export interface NumberDictDataType extends DictDataType {
|
||||
value: number;
|
||||
}
|
||||
|
||||
/** 字符串类型字典数据 */
|
||||
export interface StringDictDataType extends DictDataType {
|
||||
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 value 字典值
|
||||
* @returns 字典标签
|
||||
*/
|
||||
function getDictLabel(dictType: string, value: any) {
|
||||
function getDictLabel(dictType: string, value: any): string {
|
||||
const dictStore = useDictStore();
|
||||
const dictObj = dictStore.getDictData(dictType, value);
|
||||
return isObject(dictObj) ? dictObj.label : '';
|
||||
|
@ -43,103 +115,73 @@ function getDictLabel(dictType: string, value: any) {
|
|||
|
||||
/**
|
||||
* 获取字典对象
|
||||
*
|
||||
* @param dictType 字典类型
|
||||
* @param value 字典值
|
||||
* @returns 字典对象
|
||||
*/
|
||||
function getDictObj(dictType: string, value: any) {
|
||||
function getDictObj(dictType: string, value: any): DictItem | null {
|
||||
const dictStore = useDictStore();
|
||||
const dictObj = dictStore.getDictData(dictType, value);
|
||||
return isObject(dictObj) ? dictObj : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取字典数组 用于select radio 等
|
||||
*
|
||||
* 获取字典数组 - 优化版本,支持缓存和泛型
|
||||
* @param dictType 字典类型
|
||||
* @param valueType 字典值类型,默认 string 类型
|
||||
* @returns 字典数组
|
||||
*/
|
||||
function getDictOptions(
|
||||
function getDictOptions<T extends DictValueType = 'string'>(
|
||||
dictType: string,
|
||||
valueType: 'boolean' | 'number' | 'string' = 'string',
|
||||
): DictDataType[] {
|
||||
valueType: T = 'string' as T,
|
||||
): 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 dictOpts = dictStore.getDictOptions(dictType);
|
||||
const dictOptions: DictDataType[] = [];
|
||||
if (dictOpts.length > 0) {
|
||||
let dictValue: boolean | number | string = '';
|
||||
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,
|
||||
});
|
||||
});
|
||||
|
||||
if (dictOpts.length === 0) {
|
||||
return [] as any;
|
||||
}
|
||||
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[];
|
||||
// 转换成 number 类型的 NumberDictDataType 类型
|
||||
// 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;
|
||||
/**
|
||||
* 清空字典缓存
|
||||
*/
|
||||
export const clearDictCache = (): void => {
|
||||
dictCache.clear();
|
||||
};
|
||||
|
||||
// TODO @dhb52:下面的一系列方法,看看能不能复用 getDictOptions 方法
|
||||
export const getStrDictOptions = (dictType: string) => {
|
||||
// 获得通用的 DictDataType 列表
|
||||
const dictOptions = getDictOptions(dictType) as DictDataType[];
|
||||
// 转换成 string 类型的 StringDictDataType 类型
|
||||
// why 需要特殊转换:避免 IDEA 在 v-for="dict in getStrDictOptions(...)" 时,el-option 的 key 会告警
|
||||
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;
|
||||
/**
|
||||
* 删除指定字典类型的缓存
|
||||
* @param dictType 字典类型
|
||||
*/
|
||||
export const deleteDictCache = (dictType: string): void => {
|
||||
dictCache.delete(dictType);
|
||||
};
|
||||
|
||||
/** 字典类型枚举 - 按模块分组和排序 */
|
||||
enum DICT_TYPE {
|
||||
AI_GENERATE_MODE = 'ai_generate_mode', // AI 生成模式
|
||||
AI_IMAGE_STATUS = 'ai_image_status', // AI 图片状态
|
||||
|
@ -274,4 +316,12 @@ enum DICT_TYPE {
|
|||
TRADE_ORDER_TYPE = 'trade_order_type', // 订单 - 类型
|
||||
USER_TYPE = 'user_type',
|
||||
}
|
||||
export { DICT_TYPE, getDictLabel, getDictObj, getDictOptions };
|
||||
|
||||
export {
|
||||
type ColorType,
|
||||
DICT_TYPE,
|
||||
type DictValueType,
|
||||
getDictLabel,
|
||||
getDictObj,
|
||||
getDictOptions,
|
||||
};
|
||||
|
|
|
@ -66,7 +66,7 @@ function handleClick(doc: any) {
|
|||
<div
|
||||
v-for="(doc, index) in documentList"
|
||||
:key="index"
|
||||
class="cursor-pointer rounded-lg bg-white p-2 px-3 transition-all hover:bg-blue-50"
|
||||
class="bg-card cursor-pointer rounded-lg p-2 px-3 transition-all hover:bg-blue-50"
|
||||
@click="handleClick(doc)"
|
||||
>
|
||||
<div class="mb-1 text-sm text-gray-600">
|
||||
|
@ -89,7 +89,7 @@ function handleClick(doc: any) {
|
|||
class="border-b-solid border-b-gray-200 p-3 last:border-b-0"
|
||||
>
|
||||
<div
|
||||
class="mb-2 block w-fit rounded-sm bg-gray-50 px-2 py-1 text-xs text-gray-400"
|
||||
class="mb-2 block w-fit rounded-sm px-2 py-1 text-xs text-gray-400"
|
||||
>
|
||||
分段 {{ segment.id }}
|
||||
</div>
|
||||
|
|
|
@ -204,7 +204,7 @@ onUnmounted(async () => {
|
|||
</div>
|
||||
|
||||
<div
|
||||
class="sticky bottom-0 z-50 flex h-16 items-center justify-center bg-white shadow-sm"
|
||||
class="bg-card sticky bottom-0 z-50 flex h-16 items-center justify-center shadow-sm"
|
||||
>
|
||||
<Pagination
|
||||
:total="pageTotal"
|
||||
|
|
|
@ -231,7 +231,7 @@ defineExpose({ settingValues });
|
|||
@click="handleSizeClick(imageSize)"
|
||||
>
|
||||
<div
|
||||
class="flex h-12 w-12 flex-col items-center justify-center rounded-lg border bg-white p-0"
|
||||
class="bg-card flex h-12 w-12 flex-col items-center justify-center rounded-lg border p-0"
|
||||
:class="[
|
||||
selectSize === imageSize.key ? 'border-blue-500' : 'border-white',
|
||||
]"
|
||||
|
|
|
@ -182,7 +182,7 @@ defineExpose({ settingValues });
|
|||
@click="handleSizeClick(imageSize)"
|
||||
>
|
||||
<div
|
||||
class="flex h-12 w-12 items-center justify-center rounded-lg border bg-white p-0"
|
||||
class="bg-card flex h-12 w-12 items-center justify-center rounded-lg border p-0"
|
||||
:class="[
|
||||
selectSize === imageSize.key ? 'border-blue-500' : 'border-white',
|
||||
]"
|
||||
|
|
|
@ -90,13 +90,12 @@ onMounted(async () => {
|
|||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<div class="absolute inset-0 flex h-full w-full flex-row bg-gray-50">
|
||||
<div class="bg-card absolute inset-0 flex h-full w-full flex-row">
|
||||
<div class="left-0 flex w-96 flex-col p-4">
|
||||
<div class="segmented flex justify-center">
|
||||
<Segmented
|
||||
v-model:value="selectPlatform"
|
||||
:options="platformOptions"
|
||||
class="bg-gray-100"
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-8 h-full overflow-y-auto">
|
||||
|
@ -126,7 +125,7 @@ onMounted(async () => {
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-4 flex-1 bg-white">
|
||||
<div class="bg-card ml-4 flex-1">
|
||||
<ImageList ref="imageListRef" @on-regeneration="handleRegeneration" />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -45,7 +45,7 @@ onMounted(async () => {
|
|||
</script>
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<div class="bg-white p-5">
|
||||
<div class="bg-card p-5">
|
||||
<Input.Search
|
||||
v-model="queryParams.prompt"
|
||||
class="mb-5 w-full"
|
||||
|
@ -54,12 +54,12 @@ onMounted(async () => {
|
|||
@keyup.enter="handleQuery"
|
||||
/>
|
||||
<div
|
||||
class="grid grid-cols-[repeat(auto-fill,minmax(200px,1fr))] gap-2.5 bg-white shadow-sm"
|
||||
class="bg-card grid grid-cols-[repeat(auto-fill,minmax(200px,1fr))] gap-2.5 shadow-sm"
|
||||
>
|
||||
<div
|
||||
v-for="item in list"
|
||||
:key="item.id"
|
||||
class="relative cursor-pointer overflow-hidden bg-white transition-transform duration-300 hover:scale-105"
|
||||
class="bg-card relative cursor-pointer overflow-hidden transition-transform duration-300 hover:scale-105"
|
||||
>
|
||||
<Image
|
||||
:src="item.picUrl"
|
||||
|
|
|
@ -237,7 +237,7 @@ onMounted(async () => {
|
|||
<div v-else class="text-gray-400">暂无上传文件</div>
|
||||
</div>
|
||||
<!-- 文件内容预览 -->
|
||||
<div class="max-h-[600px] overflow-y-auto rounded-md bg-gray-50 p-4">
|
||||
<div class="max-h-[600px] overflow-y-auto rounded-md p-4">
|
||||
<div v-if="splitLoading" class="flex items-center justify-center py-5">
|
||||
<IconifyIcon icon="lucide:loader" class="is-loading" />
|
||||
<span class="ml-2.5">正在加载分段内容...</span>
|
||||
|
@ -258,7 +258,7 @@ onMounted(async () => {
|
|||
分片-{{ index + 1 }} · {{ segment.contentLength || 0 }} 字符数 ·
|
||||
{{ segment.tokens || 0 }} Token
|
||||
</div>
|
||||
<div class="rounded-md bg-white p-2">
|
||||
<div class="bg-card rounded-md p-2">
|
||||
{{ segment.content }}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -131,7 +131,7 @@ defineExpose({
|
|||
<div class="mx-auto">
|
||||
<!-- 头部导航栏 -->
|
||||
<div
|
||||
class="absolute left-0 right-0 top-0 z-10 flex h-12 items-center border-b bg-white px-4"
|
||||
class="bg-card absolute left-0 right-0 top-0 z-10 flex h-12 items-center border-b px-4"
|
||||
>
|
||||
<!-- 左侧标题 -->
|
||||
<div class="flex w-48 items-center overflow-hidden">
|
||||
|
|
|
@ -147,7 +147,7 @@ onMounted(() => {
|
|||
</template>
|
||||
<template #expand_content="{ row }">
|
||||
<div
|
||||
class="whitespace-pre-wrap border-l-4 border-blue-500 bg-gray-100 px-2.5 py-5 leading-5"
|
||||
class="whitespace-pre-wrap border-l-4 border-blue-500 px-2.5 py-5 leading-5"
|
||||
>
|
||||
<div class="mb-2 text-sm font-bold text-gray-600">完整内容:</div>
|
||||
{{ row.content }}
|
||||
|
|
|
@ -23,7 +23,7 @@ defineExpose({
|
|||
});
|
||||
</script>
|
||||
<template>
|
||||
<div class="flex w-80 flex-col bg-gray-50 p-5">
|
||||
<div class="bg-card flex w-80 flex-col rounded-lg p-5">
|
||||
<h3 class="text-primary h-7 w-full text-center text-xl leading-7">
|
||||
思维导图创作中心
|
||||
</h3>
|
||||
|
|
|
@ -40,9 +40,6 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||
component: 'ImageUpload',
|
||||
fieldName: 'avatar',
|
||||
label: '角色头像',
|
||||
componentProps: {
|
||||
maxSize: 1,
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
|
|
|
@ -40,7 +40,7 @@ function audioTimeUpdate(args: any) {
|
|||
|
||||
<template>
|
||||
<div
|
||||
class="b-1 b-l-none h-18 flex items-center justify-between border border-solid border-rose-100 bg-white px-2"
|
||||
class="b-1 b-l-none h-18 bg-card flex items-center justify-between border border-solid border-rose-100 px-2"
|
||||
>
|
||||
<!-- 歌曲信息 -->
|
||||
<div class="flex gap-2.5">
|
||||
|
|
|
@ -207,7 +207,7 @@ onBeforeUnmount(() => {
|
|||
<div class="mx-auto">
|
||||
<!-- 头部导航栏 -->
|
||||
<div
|
||||
class="absolute inset-x-0 top-0 z-10 flex h-12 items-center border-b bg-white px-5"
|
||||
class="bg-card absolute inset-x-0 top-0 z-10 flex h-12 items-center border-b px-5"
|
||||
>
|
||||
<!-- 左侧标题 -->
|
||||
<div class="flex w-48 items-center overflow-hidden">
|
||||
|
|
|
@ -5,7 +5,7 @@ import { ref } from '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>();
|
||||
|
@ -57,7 +57,7 @@ defineExpose({ validate });
|
|||
placeholder="请选择状态"
|
||||
>
|
||||
<Select.Option
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
|
||||
v-for="dict in getDictOptions(DICT_TYPE.COMMON_STATUS, 'number')"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
|
|
|
@ -11,7 +11,7 @@ import { Button, message, Textarea } from 'ant-design-vue';
|
|||
import {
|
||||
AiWriteTypeEnum,
|
||||
DICT_TYPE,
|
||||
getIntDictOptions,
|
||||
getDictOptions,
|
||||
WriteExample,
|
||||
} from '#/utils';
|
||||
|
||||
|
@ -211,22 +211,22 @@ function submit() {
|
|||
<ReuseLabel label="长度" />
|
||||
<Tag
|
||||
v-model="formData.length"
|
||||
:tags="getIntDictOptions(DICT_TYPE.AI_WRITE_LENGTH)"
|
||||
:tags="getDictOptions(DICT_TYPE.AI_WRITE_LENGTH, 'number')"
|
||||
/>
|
||||
<ReuseLabel label="格式" />
|
||||
<Tag
|
||||
v-model="formData.format"
|
||||
:tags="getIntDictOptions(DICT_TYPE.AI_WRITE_FORMAT)"
|
||||
:tags="getDictOptions(DICT_TYPE.AI_WRITE_FORMAT, 'number')"
|
||||
/>
|
||||
<ReuseLabel label="语气" />
|
||||
<Tag
|
||||
v-model="formData.tone"
|
||||
:tags="getIntDictOptions(DICT_TYPE.AI_WRITE_TONE)"
|
||||
:tags="getDictOptions(DICT_TYPE.AI_WRITE_TONE, 'number')"
|
||||
/>
|
||||
<ReuseLabel label="语言" />
|
||||
<Tag
|
||||
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">
|
||||
|
|
|
@ -400,7 +400,7 @@ onBeforeUnmount(() => {
|
|||
<div class="mx-auto">
|
||||
<!-- 头部导航栏 -->
|
||||
<div
|
||||
class="absolute inset-x-0 top-0 z-10 flex h-12 items-center border-b bg-white px-5"
|
||||
class="bg-card absolute inset-x-0 top-0 z-10 flex h-12 items-center border-b px-5"
|
||||
>
|
||||
<!-- 左侧标题 -->
|
||||
<div class="flex w-48 items-center overflow-hidden">
|
||||
|
|
|
@ -25,7 +25,7 @@ import {
|
|||
|
||||
import { DeptSelectModal, UserSelectModal } from '#/components/select-modal';
|
||||
import { ImageUpload } from '#/components/upload';
|
||||
import { DICT_TYPE, getBoolDictOptions, getIntDictOptions } from '#/utils';
|
||||
import { DICT_TYPE, getDictOptions } from '#/utils';
|
||||
|
||||
const props = defineProps({
|
||||
categoryList: {
|
||||
|
@ -295,7 +295,7 @@ defineExpose({ validate });
|
|||
<Radio.Group v-model:value="modelData.type">
|
||||
<!-- TODO BPMN 流程类型需要整合,暂时禁用 -->
|
||||
<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"
|
||||
:value="dict.value"
|
||||
:disabled="dict.value === 10"
|
||||
|
@ -307,10 +307,11 @@ defineExpose({ validate });
|
|||
<Form.Item label="是否可见" name="visible" class="mb-5">
|
||||
<Radio.Group v-model:value="modelData.visible">
|
||||
<Radio
|
||||
v-for="(dict, index) in getBoolDictOptions(
|
||||
v-for="dict in getDictOptions(
|
||||
DICT_TYPE.INFRA_BOOLEAN_STRING,
|
||||
'boolean',
|
||||
)"
|
||||
:key="index"
|
||||
:key="dict.label"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
|
@ -334,7 +335,7 @@ defineExpose({ validate });
|
|||
<div
|
||||
v-for="user in selectedStartUsers"
|
||||
:key="user.id"
|
||||
class="relative flex h-9 items-center rounded-full bg-gray-100 pr-2 hover:bg-gray-200"
|
||||
class="relative flex h-9 items-center rounded-full pr-2 hover:bg-gray-200"
|
||||
>
|
||||
<Avatar
|
||||
class="m-1"
|
||||
|
@ -370,7 +371,7 @@ defineExpose({ validate });
|
|||
<div
|
||||
v-for="dept in selectedStartDepts"
|
||||
:key="dept.id"
|
||||
class="relative flex h-9 items-center rounded-full bg-gray-100 pr-2 shadow-sm hover:bg-gray-200"
|
||||
class="relative flex h-9 items-center rounded-full pr-2 shadow-sm hover:bg-gray-200"
|
||||
>
|
||||
<IconifyIcon icon="lucide:building" class="size-6 px-1" />
|
||||
{{ dept.name }}
|
||||
|
@ -397,7 +398,7 @@ defineExpose({ validate });
|
|||
<div
|
||||
v-for="user in selectedManagerUsers"
|
||||
:key="user.id"
|
||||
class="relative flex h-9 items-center rounded-full bg-gray-100 pr-2 hover:bg-gray-200"
|
||||
class="hover:bg-primary-500 relative flex h-9 items-center rounded-full pr-2"
|
||||
>
|
||||
<Avatar
|
||||
class="m-1"
|
||||
|
|
|
@ -1,6 +1,13 @@
|
|||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
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';
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
|
@ -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 { JsonViewer, useVbenModal } from '@vben/common-ui';
|
||||
import { formatDateTime } from '@vben/utils';
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { Descriptions } from 'ant-design-vue';
|
||||
import { useDescription } from '#/components/description';
|
||||
|
||||
import { DictTag } from '#/components/dict-tag';
|
||||
import { DICT_TYPE } from '#/utils';
|
||||
import { useDetailSchema } from '../data';
|
||||
|
||||
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>
|
||||
|
||||
<template>
|
||||
|
@ -41,66 +48,6 @@ const [Modal, modalApi] = useVbenModal({
|
|||
:show-cancel-button="false"
|
||||
:show-confirm-button="false"
|
||||
>
|
||||
<Descriptions
|
||||
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>
|
||||
<Description :data="formData" />
|
||||
</Modal>
|
||||
</template>
|
||||
|
|
|
@ -1,6 +1,13 @@
|
|||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
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,
|
||||
|
@ -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 { JsonViewer, useVbenModal } from '@vben/common-ui';
|
||||
import { formatDateTime } from '@vben/utils';
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { Descriptions } from 'ant-design-vue';
|
||||
import { useDescription } from '#/components/description';
|
||||
|
||||
import { DictTag } from '#/components/dict-tag';
|
||||
import { DICT_TYPE } from '#/utils';
|
||||
import { useDetailSchema } from '../data';
|
||||
|
||||
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>
|
||||
|
||||
<template>
|
||||
|
@ -41,59 +48,6 @@ const [Modal, modalApi] = useVbenModal({
|
|||
:show-cancel-button="false"
|
||||
:show-confirm-button="false"
|
||||
>
|
||||
<Descriptions
|
||||
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>
|
||||
<Description :data="formData" />
|
||||
</Modal>
|
||||
</template>
|
||||
|
|
|
@ -122,13 +122,11 @@ getDetail();
|
|||
|
||||
<template>
|
||||
<Page auto-content-height v-loading="loading">
|
||||
<div
|
||||
class="flex h-[95%] flex-col rounded-md bg-white p-4 dark:bg-gray-800 dark:text-gray-300"
|
||||
>
|
||||
<div class="bg-card flex h-[95%] flex-col rounded-md p-4">
|
||||
<Steps
|
||||
type="navigation"
|
||||
v-model:current="currentStep"
|
||||
class="mb-8 rounded shadow-sm dark:bg-gray-700"
|
||||
class="mb-8 rounded shadow-sm"
|
||||
>
|
||||
<Steps.Step
|
||||
v-for="(step, index) in steps"
|
||||
|
|
|
@ -2,12 +2,13 @@ import type { VbenFormSchema } from '#/adapter/form';
|
|||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { DescriptionItemSchema } from '#/components/description';
|
||||
|
||||
import { h } from 'vue';
|
||||
import { h, markRaw } from 'vue';
|
||||
|
||||
import { formatDateTime } from '@vben/utils';
|
||||
|
||||
import { Timeline } from 'ant-design-vue';
|
||||
|
||||
import { CronTab } from '#/components/cron-tab';
|
||||
import { DictTag } from '#/components/dict-tag';
|
||||
import { DICT_TYPE, getDictOptions } from '#/utils';
|
||||
|
||||
|
@ -37,10 +38,12 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入处理器的名字',
|
||||
// readonly: ({ values }) => !!values.id,
|
||||
},
|
||||
dependencies: {
|
||||
triggerFields: ['id'],
|
||||
disabled: (values) => !!values.id,
|
||||
},
|
||||
rules: 'required',
|
||||
// TODO @芋艿:在修改场景下,禁止调整
|
||||
},
|
||||
{
|
||||
fieldName: 'handlerParam',
|
||||
|
@ -53,12 +56,8 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||
{
|
||||
fieldName: 'cronExpression',
|
||||
label: 'CRON 表达式',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入 CRON 表达式',
|
||||
},
|
||||
component: markRaw(CronTab),
|
||||
rules: 'required',
|
||||
// TODO @芋艿:未来支持动态的 CRON 表达式选择
|
||||
},
|
||||
{
|
||||
fieldName: 'retryCount',
|
||||
|
|
|
@ -139,8 +139,6 @@ const quickNavItems: WorkbenchQuickNavItem[] = [
|
|||
];
|
||||
|
||||
const router = useRouter();
|
||||
// 这是一个示例方法,实际项目中需要根据实际情况进行调整
|
||||
// This is a sample method, adjust according to the actual project requirements
|
||||
function navTo(nav: WorkbenchProjectItem | WorkbenchQuickNavItem) {
|
||||
if (nav.url?.startsWith('http')) {
|
||||
openWindow(nav.url);
|
||||
|
|
|
@ -30,9 +30,6 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||
fieldName: 'picUrl',
|
||||
label: '品牌图片',
|
||||
component: 'ImageUpload',
|
||||
componentProps: {
|
||||
maxSize: 1,
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
|
|
|
@ -57,9 +57,6 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||
fieldName: 'picUrl',
|
||||
label: '移动端分类图',
|
||||
component: 'ImageUpload',
|
||||
componentProps: {
|
||||
maxSize: 1,
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
|
|
|
@ -31,9 +31,6 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||
fieldName: 'userAvatar',
|
||||
label: '用户头像',
|
||||
component: 'ImageUpload',
|
||||
componentProps: {
|
||||
maxSize: 1,
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
|
@ -65,7 +62,7 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||
label: '评论图片',
|
||||
component: 'ImageUpload',
|
||||
componentProps: {
|
||||
maxSize: 9,
|
||||
maxNumber: 9,
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
|
|
|
@ -1,12 +1,7 @@
|
|||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeGridPropTypes } from '#/adapter/vxe-table';
|
||||
|
||||
import {
|
||||
DICT_TYPE,
|
||||
getDictOptions,
|
||||
getIntDictOptions,
|
||||
getRangePickerDefaultProps,
|
||||
} from '#/utils';
|
||||
import { DICT_TYPE, getDictOptions, getRangePickerDefaultProps } from '#/utils';
|
||||
|
||||
/** 新增/修改的表单 */
|
||||
export function useFormSchema(): VbenFormSchema[] {
|
||||
|
@ -29,9 +24,6 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||
fieldName: 'picUrl',
|
||||
label: '图标地址',
|
||||
component: 'ImageUpload',
|
||||
componentProps: {
|
||||
maxSize: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'sort',
|
||||
|
@ -75,7 +67,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
|
|||
component: 'Select',
|
||||
componentProps: {
|
||||
placeholder: '请选择状态',
|
||||
options: getIntDictOptions(DICT_TYPE.COMMON_STATUS),
|
||||
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
@ -52,9 +52,6 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||
fieldName: 'picUrl',
|
||||
label: '文章封面',
|
||||
component: 'ImageUpload',
|
||||
componentProps: {
|
||||
maxSize: 1,
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
|
|
|
@ -1,12 +1,7 @@
|
|||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeGridPropTypes } from '#/adapter/vxe-table';
|
||||
|
||||
import {
|
||||
DICT_TYPE,
|
||||
getDictOptions,
|
||||
getIntDictOptions,
|
||||
getRangePickerDefaultProps,
|
||||
} from '#/utils';
|
||||
import { DICT_TYPE, getDictOptions, getRangePickerDefaultProps } from '#/utils';
|
||||
|
||||
/** 新增/修改的表单 */
|
||||
export function useFormSchema(): VbenFormSchema[] {
|
||||
|
@ -29,9 +24,6 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||
fieldName: 'picUrl',
|
||||
label: '图片地址',
|
||||
component: 'ImageUpload',
|
||||
componentProps: {
|
||||
maxSize: 1,
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
|
@ -102,7 +94,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
|
|||
component: 'Select',
|
||||
componentProps: {
|
||||
placeholder: '请选择状态',
|
||||
options: getIntDictOptions(DICT_TYPE.COMMON_STATUS),
|
||||
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
@ -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>
|
||||
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>
|
||||
|
||||
<template>
|
||||
<Page>
|
||||
<DocAlert
|
||||
title="【营销】砍价活动"
|
||||
url="https://doc.iocoder.cn/mall/promotion-bargain/"
|
||||
/>
|
||||
<Button
|
||||
danger
|
||||
type="link"
|
||||
target="_blank"
|
||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
|
||||
>
|
||||
该功能支持 Vue3 + element-plus 版本!
|
||||
</Button>
|
||||
<br />
|
||||
<Button
|
||||
type="link"
|
||||
target="_blank"
|
||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/mall/promotion/bargain/activity/index"
|
||||
>
|
||||
可参考
|
||||
https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/mall/promotion/bargain/activity/index
|
||||
代码,pull request 贡献给我们!
|
||||
</Button>
|
||||
<Page auto-content-height>
|
||||
<template #doc>
|
||||
<DocAlert
|
||||
title="【营销】砍价活动"
|
||||
url="https://doc.iocoder.cn/mall/promotion-bargain/"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<FormModal @success="onRefresh" />
|
||||
|
||||
<Grid table-title="砍价活动列表">
|
||||
<template #toolbar-tools>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('ui.actionTitle.create', ['砍价活动']),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.ADD,
|
||||
auth: ['promotion:bargain-activity:create'],
|
||||
onClick: handleCreate,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('common.edit'),
|
||||
type: '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>
|
||||
</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>
|
|
@ -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>
|
||||
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>
|
||||
|
||||
<template>
|
||||
<Page>
|
||||
<DocAlert
|
||||
title="【营销】砍价活动"
|
||||
url="https://doc.iocoder.cn/mall/promotion-bargain/"
|
||||
/>
|
||||
<Button
|
||||
danger
|
||||
type="link"
|
||||
target="_blank"
|
||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
|
||||
>
|
||||
该功能支持 Vue3 + element-plus 版本!
|
||||
</Button>
|
||||
<br />
|
||||
<Button
|
||||
type="link"
|
||||
target="_blank"
|
||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/mall/promotion/bargain/record/index"
|
||||
>
|
||||
可参考
|
||||
https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/mall/promotion/bargain/record/index
|
||||
代码,pull request 贡献给我们!
|
||||
</Button>
|
||||
<Page auto-content-height>
|
||||
<template #doc>
|
||||
<DocAlert
|
||||
title="【营销】砍价活动"
|
||||
url="https://doc.iocoder.cn/mall/promotion-bargain/"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<HelpListModalApi />
|
||||
|
||||
<Grid table-title="砍价记录列表">
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '助力',
|
||||
type: 'link',
|
||||
icon: ACTION_ICON.VIEW,
|
||||
auth: ['promotion:bargain-help:query'],
|
||||
onClick: handleViewHelp.bind(null, row),
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { MallBargainHelpApi } from '#/api/mall/promotion/bargain/bargainHelp';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getBargainHelpPage } from '#/api/mall/promotion/bargain/bargainHelp';
|
||||
|
||||
import { useHelpGridColumns } from '../data';
|
||||
|
||||
/** 助力列表 */
|
||||
defineOptions({ name: 'BargainRecordListDialog' });
|
||||
|
||||
const recordId = ref<number>();
|
||||
const getTitle = computed(() => {
|
||||
return `助力列表 - 记录${recordId.value || ''}`;
|
||||
});
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
recordId.value = undefined;
|
||||
return;
|
||||
}
|
||||
// 获取传入的记录ID
|
||||
const data = modalApi.getData<{ recordId: number }>();
|
||||
if (data?.recordId) {
|
||||
recordId.value = data.recordId;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const [Grid] = useVbenVxeGrid({
|
||||
gridOptions: {
|
||||
columns: useHelpGridColumns(),
|
||||
height: 600,
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }) => {
|
||||
return await getBargainHelpPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
recordId: recordId.value,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: { code: 'query' },
|
||||
},
|
||||
} as VxeTableGridOptions<MallBargainHelpApi.BargainHelp>,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal class="w-2/5" :title="getTitle">
|
||||
<Grid class="mx-4" />
|
||||
</Modal>
|
||||
</template>
|
|
@ -0,0 +1,238 @@
|
|||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
import { formatDate } from '@vben/utils';
|
||||
|
||||
import { DICT_TYPE, getDictOptions } from '#/utils';
|
||||
|
||||
/** 表单配置 */
|
||||
export function useFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'id',
|
||||
component: 'Input',
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
show: () => false,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '活动名称',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入活动名称',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'status',
|
||||
label: '活动状态',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
placeholder: '请选择活动状态',
|
||||
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'startTime',
|
||||
label: '开始时间',
|
||||
component: 'DatePicker',
|
||||
componentProps: {
|
||||
placeholder: '请选择开始时间',
|
||||
showTime: false,
|
||||
valueFormat: 'x',
|
||||
format: 'YYYY-MM-DD',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'endTime',
|
||||
label: '结束时间',
|
||||
component: 'DatePicker',
|
||||
componentProps: {
|
||||
placeholder: '请选择结束时间',
|
||||
showTime: false,
|
||||
valueFormat: 'x',
|
||||
format: 'YYYY-MM-DD',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'userSize',
|
||||
label: '用户数量',
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
placeholder: '请输入用户数量',
|
||||
min: 2,
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'limitDuration',
|
||||
label: '限制时长',
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
placeholder: '请输入限制时长(小时)',
|
||||
min: 0,
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'totalLimitCount',
|
||||
label: '总限购数量',
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
placeholder: '请输入总限购数量',
|
||||
min: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'singleLimitCount',
|
||||
label: '单次限购数量',
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
placeholder: '请输入单次限购数量',
|
||||
min: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'virtualGroup',
|
||||
label: '虚拟成团',
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean'),
|
||||
},
|
||||
},
|
||||
{
|
||||
// TODO
|
||||
fieldName: 'spuId',
|
||||
label: '拼团商品',
|
||||
component: 'Input',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '活动名称',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入活动名称',
|
||||
clearable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'status',
|
||||
label: '活动状态',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
placeholder: '请选择活动状态',
|
||||
clearable: true,
|
||||
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'id',
|
||||
title: '活动编号',
|
||||
minWidth: 80,
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
title: '活动名称',
|
||||
minWidth: 140,
|
||||
},
|
||||
{
|
||||
field: 'activityTime',
|
||||
title: '活动时间',
|
||||
minWidth: 210,
|
||||
formatter: ({ row }) => {
|
||||
if (!row.startTime || !row.endTime) return '';
|
||||
return `${formatDate(row.startTime, 'YYYY-MM-DD')} ~ ${formatDate(row.endTime, 'YYYY-MM-DD')}`;
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'picUrl',
|
||||
title: '商品图片',
|
||||
minWidth: 80,
|
||||
cellRender: {
|
||||
name: 'CellImage',
|
||||
props: {
|
||||
height: 40,
|
||||
width: 40,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'spuName',
|
||||
title: '商品标题',
|
||||
minWidth: 300,
|
||||
},
|
||||
{
|
||||
field: 'marketPrice',
|
||||
title: '原价',
|
||||
minWidth: 100,
|
||||
formatter: ({ cellValue }) => {
|
||||
return `¥${(cellValue / 100).toFixed(2)}`;
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'combinationPrice',
|
||||
title: '拼团价',
|
||||
minWidth: 100,
|
||||
formatter: ({ row }) => {
|
||||
if (!row.products || row.products.length === 0) return '';
|
||||
const combinationPrice = Math.min(
|
||||
...row.products.map((item: any) => item.combinationPrice),
|
||||
);
|
||||
return `¥${(combinationPrice / 100).toFixed(2)}`;
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'groupCount',
|
||||
title: '开团组数',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
field: 'groupSuccessCount',
|
||||
title: '成团组数',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
field: 'recordCount',
|
||||
title: '购买次数',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
title: '活动状态',
|
||||
minWidth: 100,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.COMMON_STATUS },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '创建时间',
|
||||
width: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 200,
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
|
@ -1,32 +1,182 @@
|
|||
<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>
|
||||
|
||||
<template>
|
||||
<Page>
|
||||
<DocAlert
|
||||
title="【营销】拼团活动"
|
||||
url="https://doc.iocoder.cn/mall/promotion-combination/"
|
||||
/>
|
||||
<Button
|
||||
danger
|
||||
type="link"
|
||||
target="_blank"
|
||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
|
||||
>
|
||||
该功能支持 Vue3 + element-plus 版本!
|
||||
</Button>
|
||||
<br />
|
||||
<Button
|
||||
type="link"
|
||||
target="_blank"
|
||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/mall/promotion/combination/activity/index"
|
||||
>
|
||||
可参考
|
||||
https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/mall/promotion/combination/activity/index
|
||||
代码,pull request 贡献给我们!
|
||||
</Button>
|
||||
<Page auto-content-height>
|
||||
<template #doc>
|
||||
<DocAlert
|
||||
title="【营销】拼团活动"
|
||||
url="https://doc.iocoder.cn/mall/promotion-combination/"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<FormModal @success="onRefresh" />
|
||||
|
||||
<Grid table-title="拼团活动列表">
|
||||
<template #toolbar-tools>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('ui.actionTitle.create', ['拼团活动']),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.ADD,
|
||||
auth: ['promotion:combination-activity:create'],
|
||||
onClick: handleCreate,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('common.edit'),
|
||||
type: '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>
|
||||
</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>
|
||||
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>
|
||||
|
||||
<template>
|
||||
<Page>
|
||||
<DocAlert
|
||||
title="【营销】拼团活动"
|
||||
url="https://doc.iocoder.cn/mall/promotion-combination/"
|
||||
/>
|
||||
<Button
|
||||
danger
|
||||
type="link"
|
||||
target="_blank"
|
||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
|
||||
>
|
||||
该功能支持 Vue3 + element-plus 版本!
|
||||
</Button>
|
||||
<br />
|
||||
<Button
|
||||
type="link"
|
||||
target="_blank"
|
||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/mall/promotion/combination/record/index.vue"
|
||||
>
|
||||
可参考
|
||||
https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/mall/promotion/combination/record/index.vue
|
||||
代码,pull request 贡献给我们!
|
||||
</Button>
|
||||
<Page auto-content-height>
|
||||
<template #doc>
|
||||
<DocAlert
|
||||
title="【营销】拼团活动"
|
||||
url="https://doc.iocoder.cn/mall/promotion-combination/"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<UserListModal />
|
||||
|
||||
<Grid table-title="拼团记录列表">
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '查看成员',
|
||||
type: 'link',
|
||||
icon: ACTION_ICON.VIEW,
|
||||
onClick: handleViewUsers.bind(null, row),
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getCombinationRecordPage } from '#/api/mall/promotion/combination/combinationRecord';
|
||||
|
||||
import { useUserGridColumns } from '../data';
|
||||
|
||||
defineOptions({ name: 'CombinationUserList' });
|
||||
|
||||
const headId = ref<number>();
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
headId.value = undefined;
|
||||
return;
|
||||
}
|
||||
const data = modalApi.getData<{ headId: number }>();
|
||||
if (data?.headId) {
|
||||
headId.value = data.headId;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const [Grid] = useVbenVxeGrid({
|
||||
gridOptions: {
|
||||
columns: useUserGridColumns(),
|
||||
height: 600,
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }) => {
|
||||
// 暂时返回空数据,待API实现后替换
|
||||
return await getCombinationRecordPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
headId: headId.value,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
} as VxeTableGridOptions,
|
||||
});
|
||||
|
||||
const getTitle = computed(() => {
|
||||
return `拼团成员列表 (拼团ID: ${headId.value || ''})`;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal class="w-2/5" :title="getTitle">
|
||||
<Grid class="mx-4" />
|
||||
</Modal>
|
||||
</template>
|
|
@ -0,0 +1,129 @@
|
|||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
import { DICT_TYPE, getDictOptions, getRangePickerDefaultProps } from '#/utils';
|
||||
|
||||
import { discountFormat } from './formatter';
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'nickname',
|
||||
label: '会员昵称',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入会员昵称',
|
||||
clearable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'createTime',
|
||||
label: '领取时间',
|
||||
component: 'RangePicker',
|
||||
componentProps: {
|
||||
...getRangePickerDefaultProps(),
|
||||
clearable: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'nickname',
|
||||
title: '会员昵称',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
title: '优惠券名称',
|
||||
minWidth: 140,
|
||||
},
|
||||
{
|
||||
field: 'productScope',
|
||||
title: '类型',
|
||||
minWidth: 110,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.PROMOTION_PRODUCT_SCOPE },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'discountType',
|
||||
title: '优惠',
|
||||
minWidth: 110,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.PROMOTION_DISCOUNT_TYPE },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'discountPrice',
|
||||
title: '优惠力度',
|
||||
minWidth: 110,
|
||||
formatter: ({ row }) => {
|
||||
return discountFormat(row);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'takeType',
|
||||
title: '领取方式',
|
||||
minWidth: 110,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.PROMOTION_COUPON_TAKE_TYPE },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
title: '状态',
|
||||
minWidth: 110,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.PROMOTION_COUPON_STATUS },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '领取时间',
|
||||
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;
|
||||
}
|
|
@ -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>
|
||||
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>
|
||||
|
||||
<template>
|
||||
<Page>
|
||||
<DocAlert
|
||||
title="【营销】优惠劵"
|
||||
url="https://doc.iocoder.cn/mall/promotion-coupon/"
|
||||
/>
|
||||
<Button
|
||||
danger
|
||||
type="link"
|
||||
target="_blank"
|
||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
|
||||
>
|
||||
该功能支持 Vue3 + element-plus 版本!
|
||||
</Button>
|
||||
<br />
|
||||
<Button
|
||||
type="link"
|
||||
target="_blank"
|
||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/mall/promotion/coupon/index"
|
||||
>
|
||||
可参考
|
||||
https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/mall/promotion/coupon/index
|
||||
代码,pull request 贡献给我们!
|
||||
</Button>
|
||||
<Page auto-content-height>
|
||||
<template #doc>
|
||||
<DocAlert
|
||||
title="【营销】优惠劵"
|
||||
url="https://doc.iocoder.cn/mall/promotion-coupon/"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<Grid table-title="优惠券列表">
|
||||
<template #top>
|
||||
<Tabs v-model:active-key="activeTab" type="card" @change="onTabChange">
|
||||
<TabPane
|
||||
v-for="tab in statusTabs"
|
||||
:key="tab.value"
|
||||
:tab="tab.label"
|
||||
/>
|
||||
</Tabs>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '回收',
|
||||
type: 'link',
|
||||
danger: true,
|
||||
icon: ACTION_ICON.DELETE,
|
||||
auth: ['promotion:coupon:delete'],
|
||||
popConfirm: {
|
||||
title:
|
||||
'回收将会收回会员领取的待使用的优惠券,已使用的将无法回收,确定要回收所选优惠券吗?',
|
||||
confirm: handleDelete.bind(null, row),
|
||||
},
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
|
|
|
@ -0,0 +1,252 @@
|
|||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
// 格式化函数移到组件内部实现
|
||||
import { z } from '#/adapter/form';
|
||||
import {
|
||||
CommonStatusEnum,
|
||||
DICT_TYPE,
|
||||
getDictOptions,
|
||||
getRangePickerDefaultProps,
|
||||
} from '#/utils';
|
||||
|
||||
import {
|
||||
discountFormat,
|
||||
remainedCountFormat,
|
||||
takeLimitCountFormat,
|
||||
totalCountFormat,
|
||||
validityTypeFormat,
|
||||
} from '../formatter';
|
||||
|
||||
/** 新增/修改的表单 */
|
||||
export function useFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'id',
|
||||
component: 'Input',
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
show: () => false,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '优惠券名称',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入优惠券名称',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'description',
|
||||
label: '优惠券描述',
|
||||
component: 'Textarea',
|
||||
},
|
||||
// TODO
|
||||
{
|
||||
fieldName: 'productScope',
|
||||
label: '优惠类型',
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.PROMOTION_PRODUCT_SCOPE, 'number'),
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'takeType',
|
||||
label: '领取方式',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
placeholder: '请选择领取方式',
|
||||
options: getDictOptions(DICT_TYPE.PROMOTION_COUPON_TAKE_TYPE, 'number'),
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'validityType',
|
||||
label: '有效期类型',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
placeholder: '请选择有效期类型',
|
||||
options: getDictOptions(
|
||||
DICT_TYPE.PROMOTION_COUPON_TEMPLATE_VALIDITY_TYPE,
|
||||
'number',
|
||||
),
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'totalCount',
|
||||
label: '发放数量',
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
min: 0,
|
||||
placeholder: '请输入发放数量',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'takeLimitCount',
|
||||
label: '领取上限',
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
min: 0,
|
||||
placeholder: '请输入领取上限',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'status',
|
||||
label: '优惠券状态',
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
|
||||
buttonStyle: 'solid',
|
||||
optionType: 'button',
|
||||
},
|
||||
rules: z.number().default(CommonStatusEnum.ENABLE),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '优惠券名称',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入优惠券名称',
|
||||
clearable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'discountType',
|
||||
label: '优惠类型',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
placeholder: '请选择优惠类型',
|
||||
clearable: true,
|
||||
options: getDictOptions(DICT_TYPE.PROMOTION_DISCOUNT_TYPE, 'number'),
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'status',
|
||||
label: '优惠券状态',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
placeholder: '请选择优惠券状态',
|
||||
clearable: true,
|
||||
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'createTime',
|
||||
label: '创建时间',
|
||||
component: 'RangePicker',
|
||||
componentProps: {
|
||||
...getRangePickerDefaultProps(),
|
||||
clearable: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{ type: 'checkbox', width: 40 },
|
||||
{
|
||||
field: 'name',
|
||||
title: '优惠券名称',
|
||||
minWidth: 140,
|
||||
},
|
||||
{
|
||||
field: 'productScope',
|
||||
title: '类型',
|
||||
minWidth: 130,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.PROMOTION_PRODUCT_SCOPE },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'discountType',
|
||||
title: '优惠',
|
||||
minWidth: 110,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.PROMOTION_DISCOUNT_TYPE },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'discountPrice',
|
||||
title: '优惠力度',
|
||||
minWidth: 110,
|
||||
formatter: ({ row }) => {
|
||||
return discountFormat(row);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'takeType',
|
||||
title: '领取方式',
|
||||
minWidth: 100,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.PROMOTION_COUPON_TAKE_TYPE },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'validityType',
|
||||
title: '使用时间',
|
||||
minWidth: 180,
|
||||
formatter: ({ row }) => {
|
||||
return validityTypeFormat(row);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'totalCount',
|
||||
title: '发放数量',
|
||||
minWidth: 100,
|
||||
formatter: ({ row }) => {
|
||||
return totalCountFormat(row);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'remainedCount',
|
||||
title: '剩余数量',
|
||||
minWidth: 100,
|
||||
formatter: ({ row }) => {
|
||||
return remainedCountFormat(row);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'takeLimitCount',
|
||||
title: '领取上限',
|
||||
minWidth: 100,
|
||||
formatter: ({ row }) => {
|
||||
return takeLimitCountFormat(row);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
title: '状态',
|
||||
minWidth: 100,
|
||||
slots: { default: 'status' },
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '创建时间',
|
||||
width: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 120,
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
|
@ -1,32 +1,190 @@
|
|||
<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>
|
||||
|
||||
<template>
|
||||
<Page>
|
||||
<DocAlert
|
||||
title="【营销】优惠劵"
|
||||
url="https://doc.iocoder.cn/mall/promotion-coupon/"
|
||||
/>
|
||||
<Button
|
||||
danger
|
||||
type="link"
|
||||
target="_blank"
|
||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
|
||||
>
|
||||
该功能支持 Vue3 + element-plus 版本!
|
||||
</Button>
|
||||
<br />
|
||||
<Button
|
||||
type="link"
|
||||
target="_blank"
|
||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/mall/promotion/coupon/template/index"
|
||||
>
|
||||
可参考
|
||||
https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/mall/promotion/coupon/template/index
|
||||
代码,pull request 贡献给我们!
|
||||
</Button>
|
||||
<Page auto-content-height>
|
||||
<template #doc>
|
||||
<DocAlert
|
||||
title="【营销】优惠劵"
|
||||
url="https://doc.iocoder.cn/mall/promotion-coupon/"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<FormModal @success="onRefresh" />
|
||||
<Grid table-title="优惠券列表">
|
||||
<template #toolbar-tools>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('ui.actionTitle.create', ['优惠券模板']),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.ADD,
|
||||
auth: ['promotion:coupon-template:create'],
|
||||
onClick: handleCreate,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<template #status="{ row }">
|
||||
<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>
|
||||
</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>
|
|
@ -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>
|
||||
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>
|
||||
|
||||
<template>
|
||||
<Page>
|
||||
<DocAlert
|
||||
title="【营销】限时折扣"
|
||||
url="https://doc.iocoder.cn/mall/promotion-discount/"
|
||||
/>
|
||||
<Button
|
||||
danger
|
||||
type="link"
|
||||
target="_blank"
|
||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
|
||||
>
|
||||
该功能支持 Vue3 + element-plus 版本!
|
||||
</Button>
|
||||
<br />
|
||||
<Button
|
||||
type="link"
|
||||
target="_blank"
|
||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/mall/promotion/discountActivity/index"
|
||||
>
|
||||
可参考
|
||||
https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/mall/promotion/discountActivity/index
|
||||
代码,pull request 贡献给我们!
|
||||
</Button>
|
||||
<Page auto-content-height>
|
||||
<template #doc>
|
||||
<DocAlert
|
||||
title="【营销】限时折扣"
|
||||
url="https://doc.iocoder.cn/mall/promotion-discount/"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<FormModal @success="onRefresh" />
|
||||
|
||||
<Grid table-title="限时折扣活动列表">
|
||||
<template #toolbar-tools>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('ui.actionTitle.create', ['限时折扣活动']),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.ADD,
|
||||
auth: ['promotion:discount-activity:create'],
|
||||
onClick: handleCreate,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('common.edit'),
|
||||
type: '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>
|
||||
</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>
|
|
@ -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>
|
||||
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>
|
||||
|
||||
<template>
|
||||
<Page>
|
||||
<DocAlert title="【营销】商城装修" url="https://doc.iocoder.cn/mall/diy/" />
|
||||
<Button
|
||||
danger
|
||||
type="link"
|
||||
target="_blank"
|
||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
|
||||
>
|
||||
该功能支持 Vue3 + element-plus 版本!
|
||||
</Button>
|
||||
<br />
|
||||
<Button
|
||||
type="link"
|
||||
target="_blank"
|
||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/mall/promotion/diy/page/index"
|
||||
>
|
||||
可参考
|
||||
https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/mall/promotion/diy/page/index
|
||||
代码,pull request 贡献给我们!
|
||||
</Button>
|
||||
<Page auto-content-height>
|
||||
<template #doc>
|
||||
<DocAlert
|
||||
title="【营销】商城装修"
|
||||
url="https://doc.iocoder.cn/mall/diy/"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<FormModal @success="onRefresh" />
|
||||
|
||||
<Grid table-title="装修页面列表">
|
||||
<template #toolbar-tools>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('ui.actionTitle.create', ['装修页面']),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.ADD,
|
||||
auth: ['promotion:diy-page:create'],
|
||||
onClick: handleCreate,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '装修',
|
||||
type: '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>
|
||||
</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>
|
|
@ -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>
|
||||
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>
|
||||
|
||||
<template>
|
||||
<Page>
|
||||
<DocAlert title="【营销】商城装修" url="https://doc.iocoder.cn/mall/diy/" />
|
||||
<Button
|
||||
danger
|
||||
type="link"
|
||||
target="_blank"
|
||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
|
||||
>
|
||||
该功能支持 Vue3 + element-plus 版本!
|
||||
</Button>
|
||||
<br />
|
||||
<Button
|
||||
type="link"
|
||||
target="_blank"
|
||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/mall/promotion/diy/template/index"
|
||||
>
|
||||
可参考
|
||||
https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/mall/promotion/diy/template/index
|
||||
代码,pull request 贡献给我们!
|
||||
</Button>
|
||||
<Page auto-content-height>
|
||||
<template #doc>
|
||||
<DocAlert
|
||||
title="【营销】商城装修"
|
||||
url="https://doc.iocoder.cn/mall/diy/"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<FormModal @success="onRefresh" />
|
||||
|
||||
<Grid table-title="装修模板列表">
|
||||
<template #toolbar-tools>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('ui.actionTitle.create', ['装修模板']),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.ADD,
|
||||
auth: ['promotion:diy-template:create'],
|
||||
onClick: handleCreate,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '装修',
|
||||
type: '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>
|
||||
</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>
|
|
@ -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>
|
||||
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>
|
||||
|
||||
<template>
|
||||
<Page>
|
||||
<DocAlert
|
||||
title="【营销】积分商城活动"
|
||||
url="https://doc.iocoder.cn/mall/promotion-point/"
|
||||
/>
|
||||
<Button
|
||||
danger
|
||||
type="link"
|
||||
target="_blank"
|
||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
|
||||
>
|
||||
该功能支持 Vue3 + element-plus 版本!
|
||||
</Button>
|
||||
<br />
|
||||
<Button
|
||||
type="link"
|
||||
target="_blank"
|
||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/mall/promotion/point/activity/index"
|
||||
>
|
||||
可参考
|
||||
https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/mall/promotion/point/activity/index
|
||||
代码,pull request 贡献给我们!
|
||||
</Button>
|
||||
<Page auto-content-height>
|
||||
<template #doc>
|
||||
<DocAlert
|
||||
title="【营销】积分商城活动"
|
||||
url="https://doc.iocoder.cn/mall/promotion-point/"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<FormModal @success="onRefresh" />
|
||||
|
||||
<Grid table-title="积分商城活动列表">
|
||||
<template #toolbar-tools>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('ui.actionTitle.create', ['积分活动']),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.ADD,
|
||||
auth: ['promotion:point-activity:create'],
|
||||
onClick: handleCreate,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<template #redeemedQuantity="{ row }">
|
||||
{{ getRedeemedQuantity(row) }}
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('common.edit'),
|
||||
type: '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>
|
||||
</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>
|
|
@ -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>
|
||||
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>
|
||||
|
||||
<template>
|
||||
<Page>
|
||||
<DocAlert
|
||||
title="【营销】满减送"
|
||||
url="https://doc.iocoder.cn/mall/promotion-record/"
|
||||
/>
|
||||
<Button
|
||||
danger
|
||||
type="link"
|
||||
target="_blank"
|
||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
|
||||
>
|
||||
该功能支持 Vue3 + element-plus 版本!
|
||||
</Button>
|
||||
<br />
|
||||
<Button
|
||||
type="link"
|
||||
target="_blank"
|
||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/mall/promotion/rewardActivity/index"
|
||||
>
|
||||
可参考
|
||||
https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/mall/promotion/rewardActivity/index
|
||||
代码,pull request 贡献给我们!
|
||||
</Button>
|
||||
<Page auto-content-height>
|
||||
<template #doc>
|
||||
<DocAlert
|
||||
title="【营销】满减送"
|
||||
url="https://doc.iocoder.cn/mall/promotion-record/"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<FormModal @success="onRefresh" />
|
||||
|
||||
<Grid table-title="满减送活动列表">
|
||||
<template #toolbar-tools>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('ui.actionTitle.create', ['满减送活动']),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.ADD,
|
||||
auth: ['promotion:reward-activity:create'],
|
||||
onClick: handleCreate,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('common.edit'),
|
||||
type: '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>
|
||||
</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>
|
|
@ -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>
|
||||
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>
|
||||
|
||||
<template>
|
||||
<Page>
|
||||
<DocAlert
|
||||
title="【营销】秒杀活动"
|
||||
url="https://doc.iocoder.cn/mall/promotion-seckill/"
|
||||
/>
|
||||
<Button
|
||||
danger
|
||||
type="link"
|
||||
target="_blank"
|
||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
|
||||
>
|
||||
该功能支持 Vue3 + element-plus 版本!
|
||||
</Button>
|
||||
<br />
|
||||
<Button
|
||||
type="link"
|
||||
target="_blank"
|
||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/mall/promotion/seckill/activity/index"
|
||||
>
|
||||
可参考
|
||||
https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/mall/promotion/seckill/activity/index
|
||||
代码,pull request 贡献给我们!
|
||||
</Button>
|
||||
<Page auto-content-height>
|
||||
<template #doc>
|
||||
<DocAlert
|
||||
title="【营销】秒杀活动"
|
||||
url="https://doc.iocoder.cn/mall/promotion-seckill/"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<FormModal @success="onRefresh" />
|
||||
<Grid table-title="秒杀活动列表">
|
||||
<template #toolbar-tools>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('ui.actionTitle.create', ['秒杀活动']),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.ADD,
|
||||
auth: ['promotion:seckill-activity:create'],
|
||||
onClick: handleCreate,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #configIds="{ row }">
|
||||
<div class="flex flex-wrap gap-1">
|
||||
<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>
|
||||
</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 { MallSeckillConfigApi } from '#/api/mall/promotion/seckill/seckillConfig';
|
||||
|
||||
import { DICT_TYPE, getDictOptions, getIntDictOptions } from '#/utils';
|
||||
import { DICT_TYPE, getDictOptions } from '#/utils';
|
||||
|
||||
/** 新增/修改的表单 */
|
||||
export function useFormSchema(): VbenFormSchema[] {
|
||||
|
@ -83,7 +83,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
|
|||
component: 'Select',
|
||||
componentProps: {
|
||||
placeholder: '请选择状态',
|
||||
options: getIntDictOptions(DICT_TYPE.COMMON_STATUS),
|
||||
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
|
||||
},
|
||||
},
|
||||
];
|
||||
|
|
|
@ -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>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { MallBrokerageRecordApi } from '#/api/mall/trade/brokerage/record';
|
||||
|
||||
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>
|
||||
|
||||
<template>
|
||||
<Page>
|
||||
<DocAlert
|
||||
title="【交易】分销返佣"
|
||||
url="https://doc.iocoder.cn/mall/trade-brokerage/"
|
||||
/>
|
||||
<Button
|
||||
danger
|
||||
type="link"
|
||||
target="_blank"
|
||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
|
||||
>
|
||||
该功能支持 Vue3 + element-plus 版本!
|
||||
</Button>
|
||||
<br />
|
||||
<Button
|
||||
type="link"
|
||||
target="_blank"
|
||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/mall/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 auto-content-height>
|
||||
<template #doc>
|
||||
<DocAlert
|
||||
title="【交易】分销返佣"
|
||||
url="https://doc.iocoder.cn/mall/trade-brokerage/"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<Grid table-title="分销返佣记录">
|
||||
<template #userAvatar="{ row }">
|
||||
<Avatar :src="row.userAvatar" />
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
|
|
|
@ -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>
|
||||
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>
|
||||
|
||||
<template>
|
||||
<Page>
|
||||
<DocAlert
|
||||
title="【交易】分销返佣"
|
||||
url="https://doc.iocoder.cn/mall/trade-brokerage/"
|
||||
/>
|
||||
<Button
|
||||
danger
|
||||
type="link"
|
||||
target="_blank"
|
||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
|
||||
>
|
||||
该功能支持 Vue3 + element-plus 版本!
|
||||
</Button>
|
||||
<br />
|
||||
<Button
|
||||
type="link"
|
||||
target="_blank"
|
||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/mall/trade/brokerage/user/index"
|
||||
>
|
||||
可参考
|
||||
https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/mall/trade/brokerage/user/index
|
||||
代码,pull request 贡献给我们!
|
||||
</Button>
|
||||
<Page auto-content-height>
|
||||
<template #doc>
|
||||
<DocAlert
|
||||
title="【交易】分销返佣"
|
||||
url="https://doc.iocoder.cn/mall/trade-brokerage/"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<Grid table-title="分销用户列表">
|
||||
<template #toolbar-tools>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('ui.actionTitle.create', ['分销员']),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.ADD,
|
||||
auth: ['trade:brokerage-user:create'],
|
||||
onClick: openCreateUserForm,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #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>
|
||||
</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>
|
|
@ -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>
|
||||
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>
|
||||
|
||||
<template>
|
||||
<Page>
|
||||
<DocAlert
|
||||
title="【交易】分销返佣"
|
||||
url="https://doc.iocoder.cn/mall/trade-brokerage/"
|
||||
/>
|
||||
<Button
|
||||
danger
|
||||
type="link"
|
||||
target="_blank"
|
||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
|
||||
>
|
||||
该功能支持 Vue3 + element-plus 版本!
|
||||
</Button>
|
||||
<br />
|
||||
<Button
|
||||
type="link"
|
||||
target="_blank"
|
||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/mall/trade/brokerage/withdraw/index"
|
||||
>
|
||||
可参考
|
||||
https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/mall/trade/brokerage/withdraw/index
|
||||
代码,pull request 贡献给我们!
|
||||
</Button>
|
||||
<Page auto-content-height>
|
||||
<Grid table-title="佣金提现列表">
|
||||
<template #withdraw-info="{ row }">
|
||||
<div v-if="row.type === BrokerageWithdrawTypeEnum.WALLET.type">-</div>
|
||||
<div v-else>
|
||||
<div v-if="row.userAccount">账号:{{ row.userAccount }}</div>
|
||||
<div v-if="row.userName">真实姓名:{{ row.userName }}</div>
|
||||
<template v-if="row.type === BrokerageWithdrawTypeEnum.BANK.type">
|
||||
<div v-if="row.bankName">银行名称:{{ row.bankName }}</div>
|
||||
<div v-if="row.bankAddress">开户地址:{{ row.bankAddress }}</div>
|
||||
</template>
|
||||
<div v-if="row.qrCodeUrl" class="mt-2">
|
||||
<div>收款码:</div>
|
||||
<img :src="row.qrCodeUrl" class="mt-1 h-10 w-10" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #status-info="{ row }">
|
||||
<div>
|
||||
<DictTag
|
||||
:value="row.status"
|
||||
:type="DICT_TYPE.BROKERAGE_WITHDRAW_STATUS"
|
||||
/>
|
||||
<div v-if="row.auditTime" class="mt-1 text-xs text-gray-500">
|
||||
时间:{{ formatDateTime(row.auditTime) }}
|
||||
</div>
|
||||
<div v-if="row.auditReason" class="mt-1 text-xs text-gray-500">
|
||||
审核原因:{{ row.auditReason }}
|
||||
</div>
|
||||
<div v-if="row.transferErrorMsg" class="mt-1 text-xs text-red-500">
|
||||
转账失败原因:{{ row.transferErrorMsg }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
// 审核中状态且没有支付转账编号,显示通过和驳回按钮
|
||||
{
|
||||
label: '通过',
|
||||
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>
|
||||
</template>
|
||||
|
|
|
@ -137,9 +137,6 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||
fieldName: 'brokeragePosterUrls',
|
||||
label: '分销海报图',
|
||||
component: 'ImageUpload',
|
||||
componentProps: {
|
||||
maxSize: 1,
|
||||
},
|
||||
dependencies: {
|
||||
triggerFields: ['type'],
|
||||
show: (values) => values.type === 'brokerage',
|
||||
|
|
|
@ -31,9 +31,6 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||
component: 'ImageUpload',
|
||||
fieldName: 'logo',
|
||||
label: '公司 logo',
|
||||
componentProps: {
|
||||
maxSize: 1,
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
|
|
|
@ -26,9 +26,6 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||
component: 'ImageUpload',
|
||||
fieldName: 'logo',
|
||||
label: '门店logo',
|
||||
componentProps: {
|
||||
maxSize: 1,
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
|
|
|
@ -52,17 +52,11 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||
component: 'ImageUpload',
|
||||
fieldName: 'icon',
|
||||
label: '等级图标',
|
||||
componentProps: {
|
||||
maxSize: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'ImageUpload',
|
||||
fieldName: 'backgroundUrl',
|
||||
label: '等级背景图',
|
||||
componentProps: {
|
||||
maxSize: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'status',
|
||||
|
|
|
@ -56,9 +56,6 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||
component: 'ImageUpload',
|
||||
fieldName: 'avatar',
|
||||
label: '头像',
|
||||
componentProps: {
|
||||
maxSize: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
|
|
|
@ -2,12 +2,7 @@ import type { VbenFormSchema } from '#/adapter/form';
|
|||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
import { getAppList } from '#/api/pay/app';
|
||||
import {
|
||||
DICT_TYPE,
|
||||
getIntDictOptions,
|
||||
getRangePickerDefaultProps,
|
||||
getStrDictOptions,
|
||||
} from '#/utils';
|
||||
import { DICT_TYPE, getDictOptions, getRangePickerDefaultProps } from '#/utils';
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
|
@ -34,7 +29,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
|
|||
component: 'Select',
|
||||
componentProps: {
|
||||
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',
|
||||
componentProps: {
|
||||
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: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
|
|
|
@ -317,10 +317,12 @@ export function useDataGridColumns(): VxeTableGridOptions['columns'] {
|
|||
{
|
||||
field: 'colorType',
|
||||
title: '颜色类型',
|
||||
slots: { default: 'colorType' },
|
||||
},
|
||||
{
|
||||
field: 'cssClass',
|
||||
title: 'CSS Class',
|
||||
slots: { default: 'cssClass' },
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
|
|
|
@ -7,7 +7,7 @@ import { ref, watch } from 'vue';
|
|||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { downloadFileFromBlobPart, isEmpty } from '@vben/utils';
|
||||
|
||||
import { message } from 'ant-design-vue';
|
||||
import { message, Tag } from 'ant-design-vue';
|
||||
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
|
@ -177,6 +177,12 @@ watch(
|
|||
]"
|
||||
/>
|
||||
</template>
|
||||
<template #colorType="{ row }">
|
||||
<Tag :color="row.colorType">{{ row.colorType }}</Tag>
|
||||
</template>
|
||||
<template #cssClass="{ row }">
|
||||
<Tag :color="row.cssClass">{{ row.cssClass }}</Tag>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
|
|
|
@ -46,9 +46,6 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||
fieldName: 'logo',
|
||||
label: '应用图标',
|
||||
component: 'ImageUpload',
|
||||
componentProps: {
|
||||
limit: 1,
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
|
|
|
@ -150,8 +150,8 @@ export async function saveUserApi(user: UserInfo) {
|
|||
```ts
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export async function deleteUserApi(user: UserInfo) {
|
||||
return requestClient.delete<boolean>(`/user/${user.id}`, user);
|
||||
export async function deleteUserApi(userId: number) {
|
||||
return requestClient.delete<boolean>(`/user/${userId}`);
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
@ -180,8 +180,8 @@ export async function saveUserApi(user: UserInfo) {
|
|||
```ts
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export async function deleteUserApi(user: UserInfo) {
|
||||
return requestClient.delete<boolean>(`/user/${user.id}`, user);
|
||||
export async function deleteUserApi(userId: number) {
|
||||
return requestClient.delete<boolean>(`/user/${userId}`);
|
||||
}
|
||||
```
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue