Merge remote-tracking branch 'yudao/dev' into dev

This commit is contained in:
jason
2025-12-01 20:47:51 +08:00
41 changed files with 451 additions and 261 deletions

View File

@@ -21,6 +21,7 @@ export namespace MallCombinationActivityApi {
limitDuration?: number; // 限制时长
combinationPrice?: number; // 拼团价格
products: CombinationProduct[]; // 商品列表
picUrl?: any;
}
/** 拼团活动所需属性 */

View File

@@ -31,6 +31,7 @@ export namespace MallSeckillActivityApi {
totalStock?: number; // 秒杀总库存
seckillPrice?: number; // 秒杀价格
products?: SeckillProduct[]; // 秒杀商品列表
picUrl?: any;
}
}

View File

@@ -45,7 +45,7 @@ const bpmnInstances = () => (window as any)?.bpmnInstances;
const generateStandardId = (type: string): string => {
const prefix = type === 'message' ? 'Message_' : 'Signal_';
const timestamp = Date.now();
const random = Math.random().toString(36).substring(2, 6).toUpperCase();
const random = Math.random().toString(36).slice(2, 6).toUpperCase();
return `${prefix}${timestamp}_${random}`;
};

View File

@@ -153,14 +153,14 @@ watch(
.header-editor {
.header-list {
max-height: 400px;
overflow-y: auto;
margin-bottom: 16px;
overflow-y: auto;
}
.header-item {
display: flex;
align-items: center;
gap: 8px;
align-items: center;
margin-bottom: 12px;
.header-key {
@@ -168,8 +168,8 @@ watch(
}
.separator {
color: #606266;
font-weight: 500;
color: #606266;
}
.header-value {

View File

@@ -1,3 +1,4 @@
<!-- eslint-disable prettier/prettier -->
<script lang="ts" setup>
import { inject, nextTick, onBeforeUnmount, ref, toRaw, watch } from 'vue';
@@ -203,7 +204,7 @@ const updateHttpExtensions = (force = false) => {
? String(!!rawValue)
: (rawValue === undefined
? ''
: String(rawValue));
: rawValue.toString());
desiredEntries.push([name, persisted]);
});

View File

@@ -13,7 +13,7 @@ import { useGridColumns, useGridFormSchema } from './data';
defineOptions({ name: 'BpmManagerTask' });
/** 查看历史 */
function handleHistory(row: BpmTaskApi.TaskManager) {
function handleHistory(row: BpmTaskApi.Task) {
router.push({
name: 'BpmProcessInstanceDetail',
query: {

View File

@@ -2,8 +2,8 @@ import type { Ref } from 'vue';
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeGridProps, VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MallSpuApi } from '#/api/mall/product/spu';
import type { MallCategoryApi } from '#/api/mall/product/category';
import type { MallSpuApi } from '#/api/mall/product/spu';
import { computed } from 'vue';

View File

@@ -133,6 +133,13 @@ export function useFormSchema(): VbenFormSchema[] {
placeholder: '请输入最大砍价金额',
},
},
// TODO @puhui999这里交互不太对可以对比下 element-plus 版本呢
{
fieldName: 'spuId',
label: '砍价商品',
component: 'Input',
rules: 'required',
},
];
}

View File

@@ -14,6 +14,7 @@ import {
updateBargainActivity,
} from '#/api/mall/promotion/bargain/bargainActivity';
import { $t } from '#/locales';
import { SpuShowcase } from '#/views/mall/product/spu/components';
import { useFormSchema } from '../data';
@@ -21,7 +22,7 @@ defineOptions({ name: 'PromotionBargainActivityForm' });
const emit = defineEmits(['success']);
const formData = ref<MallBargainActivityApi.BargainActivity>();
const formData = ref<Partial<MallBargainActivityApi.BargainActivity>>({});
const getTitle = computed(() => {
return formData.value?.id
? $t('ui.actionTitle.edit', ['砍价活动'])
@@ -49,8 +50,11 @@ const [Modal, modalApi] = useVbenModal({
}
modalApi.lock();
// 提交表单
const data =
(await formApi.getValues()) as MallBargainActivityApi.BargainActivity;
const values = await formApi.getValues();
const data = {
...values,
spuId: formData.value.spuId,
} as MallBargainActivityApi.BargainActivity;
try {
await (formData.value?.id
? updateBargainActivity(data)
@@ -65,7 +69,7 @@ const [Modal, modalApi] = useVbenModal({
},
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
formData.value = undefined;
formData.value = {};
return;
}
// 加载数据
@@ -86,8 +90,12 @@ const [Modal, modalApi] = useVbenModal({
</script>
<template>
<!-- TODO @puhui999这里缺少商品的选择 -->
<Modal class="w-2/5" :title="getTitle">
<Form class="mx-4" />
<Form class="mx-4">
<!-- 自定义插槽商品选择 -->
<template #spuId>
<SpuShowcase v-model="formData.spuId" :limit="1" />
</template>
</Form>
</Modal>
</template>

View File

@@ -105,11 +105,12 @@ export function useFormSchema(): VbenFormSchema[] {
options: getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean'),
},
},
// TODO @puhui999这里交互不太对可以对比下 element-plus 版本呢
{
// TODO <!-- TODO @puhui999这里缺少商品的选择 -->
fieldName: 'spuId',
label: '拼团商品',
component: 'Input',
rules: 'required',
},
];
}

View File

@@ -13,13 +13,16 @@ import {
updateCombinationActivity,
} from '#/api/mall/promotion/combination/combinationActivity';
import { $t } from '#/locales';
import { SpuShowcase } from '#/views/mall/product/spu/components';
import { useFormSchema } from '../data';
defineOptions({ name: 'CombinationActivityForm' });
const emit = defineEmits(['success']);
const formData = ref<MallCombinationActivityApi.CombinationActivity>();
const formData = ref<Partial<MallCombinationActivityApi.CombinationActivity>>(
{},
);
const getTitle = computed(() => {
return formData.value?.id
? $t('ui.actionTitle.edit', ['拼团活动'])
@@ -47,8 +50,11 @@ const [Modal, modalApi] = useVbenModal({
}
modalApi.lock();
// 提交表单
const data =
(await formApi.getValues()) as MallCombinationActivityApi.CombinationActivity;
const values = await formApi.getValues();
const data = {
...values,
spuId: formData.value.spuId,
} as MallCombinationActivityApi.CombinationActivity;
try {
await (formData.value?.id
? updateCombinationActivity(data)
@@ -63,7 +69,7 @@ const [Modal, modalApi] = useVbenModal({
},
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
formData.value = undefined;
formData.value = {};
return;
}
// 加载数据
@@ -86,6 +92,11 @@ const [Modal, modalApi] = useVbenModal({
<template>
<Modal class="w-3/5" :title="getTitle">
<Form />
<Form>
<!-- 自定义插槽商品选择 -->
<template #spuId>
<SpuShowcase v-model="formData.spuId" :limit="1" />
</template>
</Form>
</Modal>
</template>

View File

@@ -111,7 +111,6 @@ function emitActivityChange() {
>
<Tooltip :title="activity.name">
<div class="relative h-full w-full">
<!-- TODO @芋艿 -->
<Image
:src="activity.picUrl"
class="h-full w-full rounded-lg object-cover"

View File

@@ -133,9 +133,8 @@ function handleSliderChange(prop: string) {
</TabPane>
<!-- 每个组件的通用内容 -->
<!-- TODO @xingyu装修这里的样式貌似没 ele 版本的好看 -->
<TabPane tab="样式" key="style" force-render>
<p class="text-lg font-bold">组件样式</p>
<div class="mb-2 bg-gray-100 p-2 text-sm">组件样式</div>
<div class="flex flex-col gap-2 rounded-md p-4 shadow-lg">
<Form :model="formData">
<FormItem
@@ -181,7 +180,7 @@ function handleSliderChange(prop: string) {
class="mb-0 w-full"
>
<Row>
<Col :span="11">
<Col :span="19">
<Slider
v-model:value="
formData[dataRef.prop as keyof ComponentStyle]
@@ -192,8 +191,9 @@ function handleSliderChange(prop: string) {
class="mr-4"
/>
</Col>
<Col :span="2">
<Col :span="4">
<InputNumber
class="w-[50px]"
:max="100"
:min="0"
v-model:value="

View File

@@ -98,7 +98,7 @@ const handleDeleteComponent = () => {
<component :is="component.id" :property="component.property" />
</div>
<div
class="component-wrap absolute -bottom-1 -left-0.5 -right-0.5 -top-1 block h-full w-full"
class="component-wrap absolute -bottom-1 -left-0.5 -right-0.5 block h-full w-full"
>
<!-- 左侧组件名悬浮的小贴条 -->
<div class="component-name" v-if="component.name">
@@ -109,9 +109,6 @@ const handleDeleteComponent = () => {
class="component-toolbar"
v-if="showToolbar && component.name && active"
>
<!-- TODO @xingyu装修按钮少的时候会存在遮住的情况 -->
<!-- TODO @xingyu装修貌似中间的选中框框没全部框柱上面多了点下面少了点 -->
<!-- TODO @xingyu装修从左侧的组件拖拽到中间时没有这个组件的小卡片预览ele 版本是有的 -->
<VerticalButtonGroup size="small">
<Button
:disabled="!canMoveUp"
@@ -172,7 +169,6 @@ const handleDeleteComponent = () => {
</div>
</div>
</template>
<style scoped lang="scss">
$active-border-width: 2px;
$hover-border-width: 1px;

View File

@@ -100,5 +100,4 @@ function handleCloneComponent(component: DiyComponent<any>) {
</Collapse.Panel>
</Collapse>
</div>
<!-- TODO @xingyu装修ele 里面有一些 style看看是不是都迁移完了特别是 drag-area 是全局样式 -->
</template>

View File

@@ -106,7 +106,6 @@ watch(
</div>
</Carousel>
</template>
<style lang="scss">
// Ant Design Vue Carousel 样式调整
:deep(.ant-carousel .ant-carousel-dots) {

View File

@@ -14,7 +14,7 @@ defineProps<{ property: UserCardProperty }>();
<div class="flex flex-col">
<div class="flex items-center justify-between px-4 py-6">
<div class="flex flex-1 items-center gap-4">
<Avatar :size="60">
<Avatar :size="60" class="flex items-center">
<IconifyIcon icon="ep:avatar" :size="60" />
</Avatar>
<span class="text-[18px] font-bold">芋道源码</span>

View File

@@ -175,8 +175,7 @@ function handleComponentSelected(
index: number = -1,
) {
// 使用深拷贝避免响应式追踪循环警告
// TODO @xingyu【装修】这个是必须的么ele 没有哈。
selectedComponent.value = cloneDeep(component);
selectedComponent.value = component;
selectedComponentIndex.value = index;
}
@@ -344,7 +343,7 @@ onMounted(() => {
<!-- 中心设计区域ComponentContainer -->
<Col :span="12">
<div
class="relative flex max-h-[calc(80vh)] w-full flex-1 flex-col justify-center overflow-y-auto"
class="editor-center relative flex max-h-[calc(80vh)] w-full flex-1 flex-col overflow-y-auto"
@click="handlePageSelected"
>
<!-- 手机顶部 -->
@@ -378,20 +377,20 @@ onMounted(() => {
</div>
<!-- 手机页面编辑区域 -->
<div
class="min-h-full w-full"
class="mx-auto min-h-full w-96 bg-no-repeat"
:style="{
// backgroundColor: pageConfigComponent.property.backgroundColor,
backgroundImage: `url(${pageConfigComponent.property.backgroundImage})`,
}"
>
<div
class="relative mx-auto my-0 min-h-full w-96 items-center justify-center bg-auto bg-no-repeat"
class="relative my-0 min-h-full w-full items-center justify-center bg-auto bg-no-repeat"
>
<draggable
v-model="pageComponents"
:animation="200"
:force-fallback="false"
class="min-h-full w-full"
class="min-h-[70vh] w-full"
filter=".component-toolbar"
ghost-class="draggable-ghost"
group="component"
@@ -508,5 +507,4 @@ onMounted(() => {
</div>
</PreviewModal>
</Page>
<!-- TODO @xingyu装修这里改造完后类似 web-ele/src/views/mall/promotion/components/diy-editor/index.vue 里的全局样式递推到子组件里的就没没了类似 property-group -->
</template>

View File

@@ -1,7 +1,6 @@
<script setup lang="ts">
import { Space } from 'ant-design-vue';
// TODO @xingyu【装修】貌似上下移动的按钮被遮住了
/**
* 垂直按钮组
* Ant Design Vue 的按钮组,通过 Space 实现垂直布局

View File

@@ -60,14 +60,10 @@ export function useFormSchema(): VbenFormSchema[] {
rules: 'required',
defaultValue: PromotionProductScopeEnum.ALL.scope,
},
// TODO @puhui999 商品选择器优化
{
fieldName: 'productSpuIds',
label: '商品',
component: 'Input',
componentProps: {
placeholder: '请选择商品',
},
dependencies: {
triggerFields: ['productScope', 'productScopeValues'],
show: (model) =>
@@ -84,14 +80,10 @@ export function useFormSchema(): VbenFormSchema[] {
},
rules: 'required',
},
// TODO @puhui999 商品分类选择器优化
{
fieldName: 'productCategoryIds',
label: '商品分类',
component: 'Input',
componentProps: {
placeholder: '请选择商品分类',
},
dependencies: {
triggerFields: ['productScope', 'productScopeValues'],
show: (model) =>

View File

@@ -16,11 +16,18 @@ import {
updateCouponTemplate,
} from '#/api/mall/promotion/coupon/couponTemplate';
import { $t } from '#/locales';
import { ProductCategorySelect } from '#/views/mall/product/category/components';
import { SpuShowcase } from '#/views/mall/product/spu/components';
import { useFormSchema } from '../data';
const emit = defineEmits(['success']);
const formData = ref<MallCouponTemplateApi.CouponTemplate>();
const formData = ref<
Partial<MallCouponTemplateApi.CouponTemplate> & {
productCategoryIds?: number | number[];
productSpuIds?: number[];
}
>({});
const getTitle = computed(() => {
return formData.value?.id
? $t('ui.actionTitle.edit', ['优惠券模板'])
@@ -64,7 +71,7 @@ const [Modal, modalApi] = useVbenModal({
},
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
formData.value = undefined;
formData.value = {};
return;
}
// 加载数据
@@ -75,7 +82,7 @@ const [Modal, modalApi] = useVbenModal({
modalApi.lock();
try {
formData.value = await getCouponTemplate(data.id);
const processedData = await processLoadData(formData.value);
const processedData = await processLoadData(formData.value as any);
// 设置到表单
await formApi.setValues(processedData);
} finally {
@@ -144,6 +151,15 @@ async function processLoadData(
<template>
<Modal :title="getTitle" class="w-2/5">
<Form class="mx-4" />
<Form class="mx-4">
<!-- 自定义插槽商品选择 -->
<template #productSpuIds>
<SpuShowcase v-model="formData.productSpuIds" />
</template>
<!-- 自定义插槽分类选择 -->
<template #productCategoryIds>
<ProductCategorySelect v-model="formData.productCategoryIds" />
</template>
</Form>
</Modal>
</template>

View File

@@ -67,8 +67,15 @@ export function useFormSchema(): VbenFormSchema[] {
placeholder: '请输入备注',
rows: 4,
},
formItemClass: 'col-span-2',
},
{
fieldName: 'spuIds',
label: '活动商品',
component: 'Input',
rules: 'required',
formItemClass: 'col-span-2',
},
// TODO @puhui999少了商品选择~
];
}

View File

@@ -13,13 +13,18 @@ import {
updateDiscountActivity,
} from '#/api/mall/promotion/discount/discountActivity';
import { $t } from '#/locales';
import { SpuShowcase } from '#/views/mall/product/spu/components';
import { useFormSchema } from '../data';
defineOptions({ name: 'DiscountActivityForm' });
const emit = defineEmits(['success']);
const formData = ref<MallDiscountActivityApi.DiscountActivity>();
const formData = ref<
Partial<MallDiscountActivityApi.DiscountActivity> & {
spuIds?: number[];
}
>({});
const getTitle = computed(() => {
return formData.value?.id
? $t('ui.actionTitle.edit', ['限时折扣活动'])
@@ -69,7 +74,7 @@ const [Modal, modalApi] = useVbenModal({
},
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
formData.value = undefined;
formData.value = {};
return;
}
// 加载数据
@@ -91,6 +96,11 @@ const [Modal, modalApi] = useVbenModal({
<template>
<Modal class="w-3/5" :title="getTitle">
<Form />
<Form>
<!-- 自定义插槽商品选择 -->
<template #spuIds>
<SpuShowcase v-model="formData.spuIds" />
</template>
</Form>
</Modal>
</template>

View File

@@ -20,7 +20,6 @@ import {
} from '#/api/mall/promotion/diy/template';
import { DiyEditor, PAGE_LIBS } from '#/views/mall/promotion/components';
// TODO @xingyu【装修】左上角的“基础设施”、“首页”、“我的”切换时中间的编辑器内容没有正确切换。可对比 ele 版本的效果!
/** 装修模板表单 */
defineOptions({ name: 'DiyTemplateDecorate' });
@@ -28,34 +27,28 @@ const route = useRoute();
const { refreshTab } = useTabs();
const domain = import.meta.env.VITE_MALL_H5_DOMAIN;
// 特殊:存储 reset 重置时,当前 selectedTemplateItem 值,从而进行恢复
const DIY_PAGE_INDEX_KEY = 'diy_page_index';
const DIY_PAGE_INDEX_KEY = 'diy_page_index'; // 特殊:存储 reset 重置时,当前 selectedTemplateItem 值,从而进行恢复
const selectedTemplateItem = ref(0);
// 左上角工具栏操作按钮
const templateItems = ref([
{ key: 0, name: '基础设置', icon: 'lucide:settings' },
{ key: 1, name: '首页', icon: 'lucide:home' },
{ key: 2, name: '我的', icon: 'lucide:user' },
]);
]); // 左上角工具栏操作按钮
const formData = ref<MallDiyTemplateApi.DiyTemplateProperty>();
// 当前编辑的属性
const currentFormData = ref<
MallDiyPageApi.DiyPage | MallDiyTemplateApi.DiyTemplateProperty
>({
property: '',
} as MallDiyPageApi.DiyPage);
// templateItem 对应的缓存
} as MallDiyPageApi.DiyPage); // 当前编辑的属性
const currentFormDataMap = ref<
Map<string, MallDiyPageApi.DiyPage | MallDiyTemplateApi.DiyTemplateProperty>
>(new Map());
// 商城 H5 预览地址
const previewUrl = ref('');
// 模板组件库
const templateLibs = [] as DiyComponentLibrary[];
// 当前组件库
const libs = ref<DiyComponentLibrary[]>(templateLibs);
>(new Map()); // templateItem 对应的缓存
const previewUrl = ref(''); // 商城 H5 预览地址
const templateLibs = [] as DiyComponentLibrary[]; // 模板组件库
const libs = ref<DiyComponentLibrary[]>(templateLibs); // 当前组件库
/** 获取详情 */
async function getPageDetail(id: any) {
@@ -74,23 +67,23 @@ async function getPageDetail(id: any) {
}
/** 模板选项切换 */
function handleTemplateItemChange(val: any) {
const changeValue = val.target.value;
function handleTemplateItemChange(valObj: any) {
const val = valObj.target.value;
// 缓存模版编辑数据
currentFormDataMap.value.set(
templateItems.value[changeValue]!.name,
templateItems.value[selectedTemplateItem.value]?.name || '',
currentFormData.value!,
);
// 切换模版
selectedTemplateItem.value = changeValue;
// 读取模版缓存
const data = currentFormDataMap.value.get(
templateItems.value[changeValue]!.name,
templateItems.value[val]?.name || '',
);
// 切换模版
selectedTemplateItem.value = val;
// 情况一:编辑模板
if (changeValue === 0) {
if (val === 0) {
libs.value = templateLibs;
currentFormData.value = (isEmpty(data) ? formData.value : data) as
| MallDiyPageApi.DiyPage
@@ -104,7 +97,7 @@ function handleTemplateItemChange(val: any) {
isEmpty(data)
? formData.value!.pages.find(
(page: MallDiyPageApi.DiyPage) =>
page.name === templateItems.value[changeValue]!.name,
page.name === templateItems.value[val]?.name,
)
: data
) as MallDiyPageApi.DiyPage | MallDiyTemplateApi.DiyTemplateProperty;

View File

@@ -133,10 +133,6 @@ export function useFormSchema(): VbenFormSchema[] {
component: 'Input',
rules: 'required',
formItemClass: 'col-span-2',
// TODO @puhui999貌似 renderComponentContent 不需要哇?
renderComponentContent: () => ({
default: () => null,
}),
},
];
}

View File

@@ -42,7 +42,6 @@ function handleEdit(row: MallRewardActivityApi.RewardActivity) {
/** 关闭满减送活动 */
async function handleClose(row: MallRewardActivityApi.RewardActivity) {
// TODO @puhui999这个国际化需要加下哈closing、closeSuccess
const hideLoading = message.loading({
content: $t('ui.actionMessage.closing', [row.name]),
duration: 0,

View File

@@ -29,10 +29,10 @@ const emit = defineEmits(['success']);
const formData = ref<Partial<MallRewardActivityApi.RewardActivity>>({
conditionType: PromotionConditionTypeEnum.PRICE.type,
productScope: PromotionProductScopeEnum.ALL.scope,
rules: [],
});
// TODO @puhui999点击“编辑”后会出现 Cannot read properties of null (reading 'type') 报错;
const getTitle = computed(() => {
return formData.value?.id
? $t('ui.actionTitle.edit', ['满减送'])

View File

@@ -8,12 +8,14 @@ import { useVbenModal } from '@vben/common-ui';
import { Button, message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { getSpu } from '#/api/mall/product/spu';
import {
createSeckillActivity,
getSeckillActivity,
updateSeckillActivity,
} from '#/api/mall/promotion/seckill/seckillActivity';
import { $t } from '#/locales';
import { SpuSkuSelect } from '#/views/mall/product/spu/components';
import { useFormSchema } from '../data';
@@ -31,25 +33,37 @@ const spuId = ref<number>();
const spuName = ref<string>('');
const skuTableData = ref<any[]>([]);
// 选择商品(占位函数,实际需要对接商品选择组件)
const spuSkuSelectRef = ref(); // 商品选择弹窗 Ref
/** 打开商品选择弹窗 */
const handleSelectProduct = () => {
message.info('商品选择功能需要对接商品选择组件');
// TODO: 打开商品选择弹窗
// 实际使用时需要:
// 1. 打开商品选择弹窗
// 2. 选择商品后调用以下逻辑设置数据:
// spuId.value = selectedSpu.id;
// spuName.value = selectedSpu.name;
// skuTableData.value = selectedSkus.map(sku => ({
// skuId: sku.id,
// skuName: sku.name || '',
// picUrl: sku.picUrl || selectedSpu.picUrl || '',
// price: sku.price || 0,
// stock: 0,
// seckillPrice: 0,
// }));
spuSkuSelectRef.value?.open();
};
/** 选择商品后的回调 */
async function handleSpuSelected(selectedSpuId: number, skuIds?: number[]) {
const spu = await getSpu(selectedSpuId);
if (!spu) return;
spuId.value = spu.id;
spuName.value = spu.name || '';
// 筛选指定的 SKU
const selectedSkus = skuIds
? spu.skus?.filter((sku) => skuIds.includes(sku.id!))
: spu.skus;
skuTableData.value =
selectedSkus?.map((sku) => ({
skuId: sku.id!,
skuName: sku.name || '',
picUrl: sku.picUrl || spu.picUrl || '',
price: sku.price || 0,
stock: 0,
seckillPrice: 0,
})) || [];
}
// ================= end =================
const [Form, formApi] = useVbenForm({
@@ -137,10 +151,30 @@ const [Modal, modalApi] = useVbenModal({
await nextTick();
await formApi.setValues(formData.value);
// TODO: 加载商品和 SKU 信息
// 需要调用商品 API 获取 SPU 详情
// spuId.value = formData.value.spuId;
// await loadProductDetails(formData.value.spuId, formData.value.products);
// 加载商品和 SKU 信息
if (formData.value.spuId) {
const spu = await getSpu(formData.value.spuId);
if (spu) {
spuId.value = spu.id;
spuName.value = spu.name || '';
// 回填 SKU 配置
const products = formData.value.products || [];
skuTableData.value =
spu.skus
?.filter((sku) => products.some((p) => p.skuId === sku.id))
.map((sku) => {
const product = products.find((p) => p.skuId === sku.id);
return {
skuId: sku.id!,
skuName: sku.name || '',
picUrl: sku.picUrl || spu.picUrl || '',
price: sku.price || 0,
stock: product?.stock || 0,
seckillPrice: (product?.seckillPrice || 0) / 100, // 分转元
};
}) || [];
}
}
} finally {
modalApi.unlock();
}
@@ -154,7 +188,6 @@ const [Modal, modalApi] = useVbenModal({
<Form />
<!-- 商品选择区域 -->
<!-- TODO @puhui999这里缺少商品的选择 -->
<div class="mt-4">
<div class="mb-2 flex items-center">
<span class="text-sm font-medium">秒杀活动商品:</span>
@@ -218,4 +251,11 @@ const [Modal, modalApi] = useVbenModal({
</div>
</div>
</Modal>
<!-- 商品选择器弹窗 -->
<SpuSkuSelect
ref="spuSkuSelectRef"
:is-select-sku="true"
@select="handleSpuSelected"
/>
</template>

View File

@@ -133,7 +133,6 @@ function emitActivityChange() {
class="flex h-[60px] w-[60px] cursor-pointer items-center justify-center rounded-lg border border-dashed border-gray-300 hover:border-blue-400"
@click="handleOpenActivitySelect"
>
<!-- TODO @芋艿等待和 /Users/yunai/Java/yudao-ui-admin-vben-v5/apps/web-antd/src/views/mall/product/spu/components/spu-showcase.vue 进一步统一 -->
<IconifyIcon icon="lucide:plus" class="text-xl text-gray-400" />
</div>
</Tooltip>

View File

@@ -14,7 +14,7 @@ defineOptions({ name: 'TabNews' });
const props = defineProps<{
modelValue: Reply;
newsType: NewsType;
newsType?: NewsType;
}>();
const emit = defineEmits<{

View File

@@ -4,13 +4,13 @@ import type { MpMaterialApi } from '#/api/mp/material';
import { watch } from 'vue';
import { $t } from '@vben/locales';
import { openWindow } from '@vben/utils';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { WxVideoPlayer } from '#/views/mp/components';
import { useVideoGridColumns } from './data';
import {$t} from '@vben/locales';
const props = defineProps<{
list: MpMaterialApi.Material[];

View File

@@ -7,7 +7,7 @@ import { requestClient } from '#/api/request';
export namespace BpmTaskApi {
/** 流程任务 */
export interface Task {
id: number; // 编号
id: string; // 编号
name: string; // 任务名字
status: number; // 任务状态
createTime: number; // 创建时间

View File

@@ -8,22 +8,22 @@ import { z } from '#/adapter/form';
export const EVENT_EXECUTION_OPTIONS = [
{
label: 'start',
label: '开始',
value: 'start',
},
{
label: 'end',
label: '结束',
value: 'end',
},
];
export const EVENT_OPTIONS = [
{ label: 'create', value: 'create' },
{ label: 'assignment', value: 'assignment' },
{ label: 'complete', value: 'complete' },
{ label: 'delete', value: 'delete' },
{ label: 'update', value: 'update' },
{ label: 'timeout', value: 'timeout' },
{ label: '创建', value: 'create' },
{ label: '指派', value: 'assignment' },
{ label: '完成', value: 'complete' },
{ label: '删除', value: 'delete' },
{ label: '更新', value: 'update' },
{ label: '超时', value: 'timeout' },
];
/** 新增/修改的表单 */

View File

@@ -145,5 +145,6 @@ async function processLoadData(
<template>
<Modal :title="getTitle" class="w-2/5">
<Form class="mx-4" />
<!-- TODO @puhui999这里需要同步下 -->
</Modal>
</template>

View File

@@ -3,7 +3,7 @@ import type { MallDiyPageApi } from '#/api/mall/promotion/diy/page';
import type { MallDiyTemplateApi } from '#/api/mall/promotion/diy/template';
import type { DiyComponentLibrary } from '#/views/mall/promotion/components'; // 商城的 DIY 组件,在 DiyEditor 目录下
import { onMounted, reactive, ref } from 'vue';
import { onMounted, ref } from 'vue';
import { useRoute } from 'vue-router';
import { useTabs } from '@vben/hooks';
@@ -35,7 +35,7 @@ const { refreshTab } = useTabs();
const DIY_PAGE_INDEX_KEY = 'diy_page_index'; // 特殊:存储 reset 重置时,当前 selectedTemplateItem 值,从而进行恢复
const selectedTemplateItem = ref(0);
const templateItems = reactive([
const templateItems = ref([
{ name: '基础设置', icon: 'ep:iphone' },
{ name: '首页', icon: 'ep:home-filled' },
{ name: '我的', icon: 'ep:user-filled' },
@@ -77,11 +77,13 @@ async function getPageDetail(id: any) {
function handleTemplateItemChange(val: any) {
// 缓存模版编辑数据
currentFormDataMap.value.set(
templateItems[selectedTemplateItem.value]?.name || '',
templateItems.value[selectedTemplateItem.value]?.name || '',
currentFormData.value!,
);
// 读取模版缓存
const data = currentFormDataMap.value.get(templateItems[val]?.name || '');
const data = currentFormDataMap.value.get(
templateItems.value[val]?.name || '',
);
// 切换模版
selectedTemplateItem.value = val;
@@ -101,7 +103,7 @@ function handleTemplateItemChange(val: any) {
isEmpty(data)
? formData.value!.pages.find(
(page: MallDiyPageApi.DiyPage) =>
page.name === templateItems[val]?.name,
page.name === templateItems.value[val]?.name,
)
: data
) as MallDiyPageApi.DiyPage | MallDiyTemplateApi.DiyTemplateProperty;
@@ -114,7 +116,7 @@ async function submitForm() {
});
try {
// 对所有的 templateItems 都进行保存,有缓存则保存缓存,解决都有修改时只保存了当前所编辑的 templateItem导致装修效果存在差异
for (const [i, templateItem] of templateItems.entries()) {
for (const [i, templateItem] of templateItems.value.entries()) {
const data = currentFormDataMap.value.get(templateItem.name) as any;
// 情况一:基础设置
if (i === 0) {
@@ -188,7 +190,7 @@ onMounted(async () => {
:show-navigation-bar="selectedTemplateItem !== 0"
:show-page-config="selectedTemplateItem !== 0"
:show-tab-bar="selectedTemplateItem === 0"
:title="templateItems[selectedTemplateItem]?.name || ''"
:title="templateItems[selectedTemplateItem]?.name ?? ''"
@reset="handleEditorReset"
@save="submitForm"
>

View File

@@ -1,26 +1,19 @@
<script lang="ts" setup>
import { onMounted, reactive, ref } from 'vue';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MpMaterialApi } from '#/api/mp/material';
import { reactive, ref, watch } from 'vue';
import { NewsType } from '@vben/constants';
import { IconifyIcon } from '@vben/icons';
import { formatTime } from '@vben/utils';
import {
ElButton,
ElPagination,
ElRow,
ElTable,
ElTableColumn,
} from 'element-plus';
import { ElButton, ElPagination, ElRow } from 'element-plus';
import * as MpDraftApi from '#/api/mp/draft';
import * as MpFreePublishApi from '#/api/mp/freePublish';
import * as MpMaterialApi from '#/api/mp/material';
import News from '#/views/mp/components/wx-news/wx-news.vue';
import VideoPlayer from '#/views/mp/components/wx-video-play/wx-video-play.vue';
import VoicePlayer from '#/views/mp/components/wx-voice-play/wx-voice-play.vue';
// TODO @hw代码风格看看 antd 和 ele 是不是统一下; 等antd此组件修改完再调整
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getDraftPage } from '#/api/mp/draft';
import { getFreePublishPage } from '#/api/mp/freePublish';
import { getMaterialPage } from '#/api/mp/material';
import { WxNews, WxVideoPlayer, WxVoicePlayer } from '#/views/mp/components';
/** 微信素材选择 */
defineOptions({ name: 'WxMaterialSelect' });
@@ -49,33 +42,163 @@ const queryParams = reactive({
pageSize: 10,
}); // 查询参数
/** 选择素材 */
const voiceGridColumns: VxeTableGridOptions<MpMaterialApi.Material>['columns'] =
[
{
field: 'mediaId',
title: '编号',
align: 'center',
minWidth: 160,
},
{
field: 'name',
title: '文件名',
minWidth: 200,
},
{
field: 'voice',
title: '语音',
minWidth: 200,
align: 'center',
slots: { default: 'voice' },
},
{
field: 'createTime',
title: '上传时间',
width: 180,
formatter: 'formatDateTime',
},
{
title: '操作',
width: 140,
fixed: 'right',
align: 'center',
slots: { default: 'actions' },
},
];
const videoGridColumns: VxeTableGridOptions<MpMaterialApi.Material>['columns'] =
[
{
field: 'mediaId',
title: '编号',
minWidth: 160,
},
{
field: 'name',
title: '文件名',
minWidth: 200,
},
{
field: 'title',
title: '标题',
minWidth: 200,
},
{
field: 'introduction',
title: '介绍',
minWidth: 220,
},
{
field: 'video',
title: '视频',
minWidth: 220,
align: 'center',
slots: { default: 'video' },
},
{
field: 'createTime',
title: '上传时间',
width: 180,
formatter: 'formatDateTime',
},
{
title: '操作',
width: 140,
fixed: 'right',
align: 'center',
slots: { default: 'actions' },
},
];
const [VoiceGrid, voiceGridApi] = useVbenVxeGrid({
gridOptions: {
border: true,
columns: voiceGridColumns,
height: 'auto',
keepSource: true,
pagerConfig: {
enabled: true,
pageSize: 10,
},
proxyConfig: {
ajax: {
query: async ({ page }, { accountId }) => {
const finalAccountId = accountId ?? queryParams.accountId;
if (!finalAccountId) {
return { list: [], total: 0 };
}
return await getMaterialPage({
pageNo: page.currentPage,
pageSize: page.pageSize,
accountId: finalAccountId,
type: 'voice',
});
},
},
},
rowConfig: {
keyField: 'mediaId',
isHover: true,
},
toolbarConfig: {
refresh: true,
},
} as VxeTableGridOptions<MpMaterialApi.Material>,
});
const [VideoGrid, videoGridApi] = useVbenVxeGrid({
gridOptions: {
border: true,
columns: videoGridColumns,
height: 'auto',
keepSource: true,
pagerConfig: {
enabled: true,
pageSize: 10,
},
proxyConfig: {
ajax: {
query: async ({ page }, { accountId }) => {
const finalAccountId = accountId ?? queryParams.accountId;
if (finalAccountId === undefined || finalAccountId === null) {
return { list: [], total: 0 };
}
return await getMaterialPage({
pageNo: page.currentPage,
pageSize: page.pageSize,
accountId: finalAccountId,
type: 'video',
});
},
},
},
rowConfig: {
keyField: 'mediaId',
isHover: true,
},
toolbarConfig: {
refresh: true,
},
} as VxeTableGridOptions<MpMaterialApi.Material>,
});
function selectMaterialFun(item: any) {
emit('selectMaterial', item);
}
/** 获取分页数据 */
async function getPage() {
loading.value = true;
try {
if (props.type === 'news' && props.newsType === NewsType.Published) {
// 【图文】+ 【已发布】
await getFreePublishPageFun();
} else if (props.type === 'news' && props.newsType === NewsType.Draft) {
// 【图文】+ 【草稿】
await getDraftPageFun();
} else {
// 【素材】
await getMaterialPageFun();
}
} finally {
loading.value = false;
}
}
/** 获取素材分页 */
async function getMaterialPageFun() {
const data = await MpMaterialApi.getMaterialPage({
const data = await getMaterialPage({
...queryParams,
type: props.type,
});
@@ -83,9 +206,8 @@ async function getMaterialPageFun() {
total.value = data.total;
}
/** 获取已发布图文分页 */
async function getFreePublishPageFun() {
const data = await MpFreePublishApi.getFreePublishPage(queryParams);
const data = await getFreePublishPage(queryParams);
data.list.forEach((item: any) => {
const articles = item.content.newsItem;
articles.forEach((article: any) => {
@@ -96,9 +218,8 @@ async function getFreePublishPageFun() {
total.value = data.total;
}
/** 获取草稿图文分页 */
async function getDraftPageFun() {
const data = await MpDraftApi.getDraftPage(queryParams);
const data = await getDraftPage(queryParams);
data.list.forEach((draft: any) => {
const articles = draft.content.newsItem;
articles.forEach((article: any) => {
@@ -109,9 +230,57 @@ async function getDraftPageFun() {
total.value = data.total;
}
onMounted(async () => {
getPage();
});
async function getPage() {
if (props.type === 'voice') {
await voiceGridApi.reload({ accountId: queryParams.accountId });
return;
}
if (props.type === 'video') {
await videoGridApi.reload({ accountId: queryParams.accountId });
return;
}
loading.value = true;
try {
if (props.type === 'news' && props.newsType === NewsType.Published) {
await getFreePublishPageFun();
} else if (props.type === 'news' && props.newsType === NewsType.Draft) {
await getDraftPageFun();
} else {
await getMaterialPageFun();
}
} finally {
loading.value = false;
}
}
watch(
() => props.accountId,
(accountId) => {
queryParams.accountId = accountId;
queryParams.pageNo = 1;
getPage();
},
{ immediate: true },
);
watch(
() => props.type,
() => {
queryParams.pageNo = 1;
getPage();
},
);
watch(
() => props.newsType,
() => {
if (props.type === 'news') {
queryParams.pageNo = 1;
getPage();
}
},
);
</script>
<template>
@@ -152,90 +321,31 @@ onMounted(async () => {
</div>
<!-- 类型voice -->
<div v-else-if="props.type === 'voice'">
<!-- 列表 -->
<ElTable v-loading="loading" :data="list">
<ElTableColumn label="编号" align="center" prop="mediaId" />
<ElTableColumn label="文件名" align="center" prop="name" />
<ElTableColumn label="语音" align="center">
<template #default="scope">
<VoicePlayer :url="scope.row.url" />
</template>
</ElTableColumn>
<ElTableColumn
label="上传时间"
align="center"
prop="createTime"
width="180"
:formatter="
(row: any) => formatTime(row.createTime, 'YYYY-MM-DD HH:mm:ss')
"
/>
<ElTableColumn label="操作" align="center" fixed="right">
<template #default="scope">
<ElButton type="primary" link @click="selectMaterialFun(scope.row)">
选择
<IconifyIcon icon="lucide:plus" />
</ElButton>
</template>
</ElTableColumn>
</ElTable>
<!-- 分页组件 -->
<ElPagination
background
layout="prev, pager, next, sizes, total"
:total="total"
v-model:current-page="queryParams.pageNo"
v-model:page-size="queryParams.pageSize"
@current-change="getPage"
@size-change="getPage"
/>
<VoiceGrid>
<template #voice="{ row }">
<WxVoicePlayer :url="row.url" />
</template>
<template #actions="{ row }">
<ElButton type="primary" link @click="selectMaterialFun(row)">
选择
<IconifyIcon icon="lucide:plus" />
</ElButton>
</template>
</VoiceGrid>
</div>
<!-- 类型video -->
<div v-else-if="props.type === 'video'">
<!-- 列表 -->
<ElTable v-loading="loading" :data="list">
<ElTableColumn label="编号" align="center" prop="mediaId" />
<ElTableColumn label="文件名" align="center" prop="name" />
<ElTableColumn label="标题" align="center" prop="title" />
<ElTableColumn label="介绍" align="center" prop="introduction" />
<ElTableColumn label="视频" align="center">
<template #default="scope">
<VideoPlayer :url="scope.row.url" />
</template>
</ElTableColumn>
<ElTableColumn
label="上传时间"
align="center"
prop="createTime"
width="180"
:formatter="
(row: any) => formatTime(row.createTime, 'YYYY-MM-DD HH:mm:ss')
"
/>
<ElTableColumn
label="操作"
align="center"
fixed="right"
class-name="small-padding fixed-width"
>
<template #default="scope">
<ElButton type="primary" link @click="selectMaterialFun(scope.row)">
选择
<IconifyIcon icon="lucide:circle-plus" />
</ElButton>
</template>
</ElTableColumn>
</ElTable>
<!-- 分页组件 -->
<ElPagination
background
layout="prev, pager, next, sizes, total"
:total="total"
v-model:current-page="queryParams.pageNo"
v-model:page-size="queryParams.pageSize"
@current-change="getMaterialPageFun"
@size-change="getMaterialPageFun"
/>
<VideoGrid>
<template #video="{ row }">
<WxVideoPlayer :url="row.url" />
</template>
<template #actions="{ row }">
<ElButton type="primary" link @click="selectMaterialFun(row)">
选择
<IconifyIcon icon="lucide:circle-plus" />
</ElButton>
</template>
</VideoGrid>
</div>
<!-- 类型news -->
<div v-else-if="props.type === 'news'">
@@ -249,7 +359,7 @@ onMounted(async () => {
:key="item.mediaId"
>
<div v-if="item.content && item.content.newsItem">
<News :articles="item.content.newsItem" />
<WxNews :articles="item.content.newsItem" />
<ElRow class="flex justify-center pt-2.5">
<ElButton type="success" @click="selectMaterialFun(item)">
选择

View File

@@ -14,7 +14,7 @@ defineOptions({ name: 'TabNews' });
const props = defineProps<{
modelValue: Reply;
newsType: NewsType;
newsType?: NewsType;
}>();
const emit = defineEmits<{

View File

@@ -4,6 +4,7 @@ import type { MpMaterialApi } from '#/api/mp/material';
import { watch } from 'vue';
import { $t } from '@vben/locales';
import { openWindow } from '@vben/utils';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';

View File

@@ -24,8 +24,8 @@ import {
// VxeOptgroup,
// VxeOption,
// VxePulldown,
// VxeRadio,
// VxeRadioButton,
VxeRadio,
VxeRadioButton,
VxeRadioGroup,
VxeSelect,
VxeTooltip,
@@ -88,8 +88,8 @@ export function initVxeTable() {
// VxeUI.component(VxeOption);
VxeUI.component(VxePager);
// VxeUI.component(VxePulldown);
// VxeUI.component(VxeRadio);
// VxeUI.component(VxeRadioButton);
VxeUI.component(VxeRadio);
VxeUI.component(VxeRadioButton);
VxeUI.component(VxeRadioGroup);
VxeUI.component(VxeSelect);
// VxeUI.component(VxeSwitch);

View File

@@ -36,7 +36,9 @@
"downloadTemplateFail": "Download template failed",
"updating": "Updating {0}...",
"updateSuccess": "Update {0} successfully",
"updateFailed": "Update {0} failed"
"updateFailed": "Update {0} failed",
"closing": "Closing {0} ...",
"closeSuccess": "{0} closed successfully"
},
"placeholder": {
"input": "Please enter",

View File

@@ -36,7 +36,9 @@
"downloadTemplateFail": "下载模板失败",
"updating": "正在更新 {0}...",
"updateSuccess": "更新 {0} 成功",
"updateFailed": "更新 {0} 失败"
"updateFailed": "更新 {0} 失败",
"closing": "正在关闭 {0} ...",
"closeSuccess": "{0} 关闭成功"
},
"placeholder": {
"input": "请输入",