fix: eslint
This commit is contained in:
@@ -50,7 +50,7 @@ export function getCategoryList(params: any) {
|
||||
);
|
||||
}
|
||||
|
||||
// 获得商品分类列表
|
||||
/** 获得商品分类列表 */
|
||||
export function getCategorySimpleList() {
|
||||
return requestClient.get<MallCategoryApi.Category[]>(
|
||||
'/product/category/list',
|
||||
|
||||
@@ -50,6 +50,10 @@ export namespace MallCombinationActivityApi {
|
||||
products: CombinationProduct[];
|
||||
/** 图片 */
|
||||
picUrl?: string;
|
||||
/** 商品名称 */
|
||||
spuName?: string;
|
||||
/** 市场价 */
|
||||
marketPrice?: number;
|
||||
}
|
||||
|
||||
/** 扩展 SKU 配置 */
|
||||
|
||||
@@ -5,38 +5,24 @@ import { requestClient } from '#/api/request';
|
||||
export namespace MallKefuConversationApi {
|
||||
/** 客服会话 */
|
||||
export interface Conversation {
|
||||
/** 编号 */
|
||||
id: number;
|
||||
/** 会话所属用户 */
|
||||
userId: number;
|
||||
/** 会话所属用户头像 */
|
||||
userAvatar: string;
|
||||
/** 会话所属用户昵称 */
|
||||
userNickname: string;
|
||||
/** 最后聊天时间 */
|
||||
lastMessageTime: Date;
|
||||
/** 最后聊天内容 */
|
||||
lastMessageContent: string;
|
||||
/** 最后发送的消息类型 */
|
||||
lastMessageContentType: number;
|
||||
/** 管理端置顶 */
|
||||
adminPinned: boolean;
|
||||
/** 用户是否可见 */
|
||||
userDeleted: boolean;
|
||||
/** 管理员是否可见 */
|
||||
adminDeleted: boolean;
|
||||
/** 管理员未读消息数 */
|
||||
adminUnreadMessageCount: number;
|
||||
/** 创建时间 */
|
||||
createTime?: string;
|
||||
id: number; // 编号
|
||||
userId: number; // 会话所属用户
|
||||
userAvatar: string; // 会话所属用户头像
|
||||
userNickname: string; // 会话所属用户昵称
|
||||
lastMessageTime: Date; // 最后聊天时间
|
||||
lastMessageContent: string; // 最后聊天内容
|
||||
lastMessageContentType: number; // 最后发送的消息类型
|
||||
adminPinned: boolean; // 管理端置顶
|
||||
userDeleted: boolean; // 用户是否可见
|
||||
adminDeleted: boolean; // 管理员是否可见
|
||||
adminUnreadMessageCount: number; // 管理员未读消息数
|
||||
createTime?: string; // 创建时间
|
||||
}
|
||||
|
||||
/** 会话置顶请求 */
|
||||
export interface ConversationPinnedUpdate {
|
||||
/** 会话编号 */
|
||||
id: number;
|
||||
/** 是否置顶 */
|
||||
pinned: boolean;
|
||||
id: number; // 会话编号
|
||||
pinned: boolean; // 是否置顶
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,44 +5,29 @@ import { requestClient } from '#/api/request';
|
||||
export namespace MallKefuMessageApi {
|
||||
/** 客服消息 */
|
||||
export interface Message {
|
||||
/** 编号 */
|
||||
id: number;
|
||||
/** 会话编号 */
|
||||
conversationId: number;
|
||||
/** 发送人编号 */
|
||||
senderId: number;
|
||||
/** 发送人头像 */
|
||||
senderAvatar: string;
|
||||
/** 发送人类型 */
|
||||
senderType: number;
|
||||
/** 接收人编号 */
|
||||
receiverId: number;
|
||||
/** 接收人类型 */
|
||||
receiverType: number;
|
||||
/** 消息类型 */
|
||||
contentType: number;
|
||||
/** 消息内容 */
|
||||
content: string;
|
||||
/** 是否已读 */
|
||||
readStatus: boolean;
|
||||
/** 创建时间 */
|
||||
createTime: Date;
|
||||
id: number; // 编号
|
||||
conversationId: number; // 会话编号
|
||||
senderId: number; // 发送人编号
|
||||
senderAvatar: string; // 发送人头像
|
||||
senderType: number; // 发送人类型
|
||||
receiverId: number; // 接收人编号
|
||||
receiverType: number; // 接收人类型
|
||||
contentType: number; // 消息类型
|
||||
content: string; // 消息内容
|
||||
readStatus: boolean; // 是否已读
|
||||
createTime: Date; // 创建时间
|
||||
}
|
||||
|
||||
/** 发送消息请求 */
|
||||
export interface MessageSend {
|
||||
/** 会话编号 */
|
||||
conversationId: number;
|
||||
/** 消息类型 */
|
||||
contentType: number;
|
||||
/** 消息内容 */
|
||||
content: string;
|
||||
conversationId: number; // 会话编号
|
||||
contentType: number; // 消息类型
|
||||
content: string; // 消息内容
|
||||
}
|
||||
|
||||
/** 消息列表查询参数 */
|
||||
export interface MessageQuery extends PageParam {
|
||||
/** 会话编号 */
|
||||
conversationId: number;
|
||||
conversationId: number; // 会话编号
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -57,6 +57,10 @@ export namespace MallSeckillActivityApi {
|
||||
products?: SeckillProduct[];
|
||||
/** 图片 */
|
||||
picUrl?: string;
|
||||
/** 商品名称 */
|
||||
spuName?: string;
|
||||
/** 市场价 */
|
||||
marketPrice?: number;
|
||||
}
|
||||
|
||||
/** 扩展 SKU 配置 */
|
||||
|
||||
@@ -127,9 +127,7 @@ export default defineComponent({
|
||||
},
|
||||
default: () => {
|
||||
if (item.slot) {
|
||||
// TODO @xingyu:这里要 inline 掉么?
|
||||
const slotContent = getSlot(slots, item.slot, data);
|
||||
return slotContent;
|
||||
return getSlot(slots, item.slot, data);
|
||||
}
|
||||
if (!contentMinWidth) {
|
||||
return getContent();
|
||||
|
||||
@@ -8,12 +8,14 @@ import { $t } from '#/locales';
|
||||
|
||||
const appName = computed(() => preferences.app.name);
|
||||
const logo = computed(() => preferences.logo.source);
|
||||
const logoDark = computed(() => preferences.logo.sourceDark);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AuthPageLayout
|
||||
:app-name="appName"
|
||||
:logo="logo"
|
||||
:logo-dark="logoDark"
|
||||
:page-description="$t('authentication.pageDesc')"
|
||||
:page-title="$t('authentication.pageTitle')"
|
||||
>
|
||||
|
||||
102
apps/web-ele/src/store/mall/kefu.ts
Normal file
102
apps/web-ele/src/store/mall/kefu.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import type { MallKefuConversationApi } from '#/api/mall/promotion/kefu/conversation';
|
||||
import type { MallKefuMessageApi } from '#/api/mall/promotion/kefu/message';
|
||||
|
||||
import { isEmpty } from '@vben/utils';
|
||||
|
||||
import { acceptHMRUpdate, defineStore } from 'pinia';
|
||||
|
||||
import * as KeFuConversationApi from '#/api/mall/promotion/kefu/conversation';
|
||||
|
||||
interface MallKefuInfoVO {
|
||||
conversationList: MallKefuConversationApi.Conversation[]; // 会话列表
|
||||
conversationMessageList: Map<number, MallKefuMessageApi.Message[]>; // 会话消息
|
||||
}
|
||||
|
||||
export const useMallKefuStore = defineStore('mall-kefu', {
|
||||
state: (): MallKefuInfoVO => ({
|
||||
conversationList: [],
|
||||
conversationMessageList: new Map<number, MallKefuMessageApi.Message[]>(), // key 会话,value 会话消息列表
|
||||
}),
|
||||
getters: {
|
||||
getConversationList(): MallKefuConversationApi.Conversation[] {
|
||||
return this.conversationList;
|
||||
},
|
||||
getConversationMessageList(): (
|
||||
conversationId: number,
|
||||
) => MallKefuMessageApi.Message[] | undefined {
|
||||
return (conversationId: number) =>
|
||||
this.conversationMessageList.get(conversationId);
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
// ======================= 会话消息相关 =======================
|
||||
/** 缓存历史消息 */
|
||||
saveMessageList(
|
||||
conversationId: number,
|
||||
messageList: MallKefuMessageApi.Message[],
|
||||
) {
|
||||
this.conversationMessageList.set(conversationId, messageList);
|
||||
},
|
||||
|
||||
// ======================= 会话相关 =======================
|
||||
/** 加载会话缓存列表 */
|
||||
async setConversationList() {
|
||||
// TODO @jave:idea linter 告警,修复下;
|
||||
// TODO @jave:不使用 KeFuConversationApi.,直接用 getConversationList
|
||||
this.conversationList = await KeFuConversationApi.getConversationList();
|
||||
this.conversationSort();
|
||||
},
|
||||
/** 更新会话缓存已读 */
|
||||
async updateConversationStatus(conversationId: number) {
|
||||
if (isEmpty(this.conversationList)) {
|
||||
return;
|
||||
}
|
||||
const conversation = this.conversationList.find(
|
||||
(item) => item.id === conversationId,
|
||||
);
|
||||
conversation && (conversation.adminUnreadMessageCount = 0);
|
||||
},
|
||||
/** 更新会话缓存 */
|
||||
async updateConversation(conversationId: number) {
|
||||
if (isEmpty(this.conversationList)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const conversation =
|
||||
await KeFuConversationApi.getConversation(conversationId);
|
||||
this.deleteConversation(conversationId);
|
||||
conversation && this.conversationList.push(conversation);
|
||||
this.conversationSort();
|
||||
},
|
||||
/** 删除会话缓存 */
|
||||
deleteConversation(conversationId: number) {
|
||||
const index = this.conversationList.findIndex(
|
||||
(item) => item.id === conversationId,
|
||||
);
|
||||
// 存在则删除
|
||||
if (index !== -1) {
|
||||
this.conversationList.splice(index, 1);
|
||||
}
|
||||
},
|
||||
conversationSort() {
|
||||
// 按置顶属性和最后消息时间排序
|
||||
this.conversationList.sort((a, b) => {
|
||||
// 按照置顶排序,置顶的会在前面
|
||||
if (a.adminPinned !== b.adminPinned) {
|
||||
return a.adminPinned ? -1 : 1;
|
||||
}
|
||||
// 按照最后消息时间排序,最近的会在前面
|
||||
return (
|
||||
(b.lastMessageTime as unknown as number) -
|
||||
(a.lastMessageTime as unknown as number)
|
||||
);
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// 解决热更新问题
|
||||
const hot = import.meta.hot;
|
||||
if (hot) {
|
||||
hot.accept(acceptHMRUpdate(useMallKefuStore, hot));
|
||||
}
|
||||
@@ -3,7 +3,6 @@ import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { AiMusicApi } from '#/api/ai/music';
|
||||
|
||||
import { confirm, DocAlert, Page } from '@vben/common-ui';
|
||||
import { AiMusicStatusEnum } from '@vben/constants';
|
||||
|
||||
import { ElButton, ElLoading, ElMessage } from 'element-plus';
|
||||
|
||||
|
||||
@@ -203,5 +203,3 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
|
||||
|
||||
@@ -226,7 +226,7 @@ export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
label: '请求时间',
|
||||
field: 'beginTime',
|
||||
render: (val, data) => {
|
||||
if (data?.beginTime && data?.endTime) {
|
||||
if (val && data?.endTime) {
|
||||
return `${formatDateTime(val)} ~ ${formatDateTime(data.endTime)}`;
|
||||
}
|
||||
return '';
|
||||
@@ -245,7 +245,7 @@ export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
render: (val, data) => {
|
||||
if (val === 0) {
|
||||
return '正常';
|
||||
} else if (data && data.resultMsg) {
|
||||
} else if (val > 0 && data?.resultMsg) {
|
||||
return `失败 | ${val} | ${data.resultMsg}`;
|
||||
}
|
||||
return '';
|
||||
|
||||
@@ -14,7 +14,6 @@ const formData = ref<InfraApiAccessLogApi.ApiAccessLog>();
|
||||
const [Descriptions] = useDescription({
|
||||
border: true,
|
||||
column: 1,
|
||||
labelWidth: 110,
|
||||
schema: useDetailSchema(),
|
||||
});
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ const formData = ref<InfraApiErrorLogApi.ApiErrorLog>();
|
||||
const [Descriptions] = useDescription({
|
||||
border: true,
|
||||
column: 1,
|
||||
labelWidth: 110,
|
||||
schema: useDetailSchema(),
|
||||
});
|
||||
|
||||
|
||||
@@ -13,8 +13,8 @@ import { useDetailSchema } from '../data';
|
||||
const formData = ref<InfraJobLogApi.JobLog>();
|
||||
|
||||
const [Descriptions] = useDescription({
|
||||
border: true,
|
||||
column: 1,
|
||||
labelWidth: 140,
|
||||
schema: useDetailSchema(),
|
||||
});
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@ const nextTimes = ref<Date[]>([]); // 下一次执行时间
|
||||
const [Descriptions] = useDescription({
|
||||
border: true,
|
||||
column: 1,
|
||||
labelWidth: 140,
|
||||
schema: useDetailSchema(),
|
||||
});
|
||||
|
||||
|
||||
@@ -95,9 +95,9 @@ onActivated(() => {
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getOrderData();
|
||||
getProductData();
|
||||
getWalletRechargeData();
|
||||
loadOrderData();
|
||||
loadProductData();
|
||||
loadWalletRechargeData();
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ const router = useRouter();
|
||||
const menuList = [
|
||||
{
|
||||
name: '用户管理',
|
||||
icon: 'ep:user-filled',
|
||||
icon: 'lucide:user',
|
||||
bgColor: 'bg-red-400',
|
||||
routerName: 'MemberUser',
|
||||
},
|
||||
@@ -26,7 +26,7 @@ const menuList = [
|
||||
},
|
||||
{
|
||||
name: '订单管理',
|
||||
icon: 'ep:list',
|
||||
icon: 'lucide:list',
|
||||
bgColor: 'bg-yellow-500',
|
||||
routerName: 'TradeOrder',
|
||||
},
|
||||
@@ -44,13 +44,13 @@ const menuList = [
|
||||
},
|
||||
{
|
||||
name: '优惠券',
|
||||
icon: 'ep:ticket',
|
||||
icon: 'lucide:ticket',
|
||||
bgColor: 'bg-blue-500',
|
||||
routerName: 'PromotionCoupon',
|
||||
},
|
||||
{
|
||||
name: '拼团活动',
|
||||
icon: 'fa:group',
|
||||
icon: 'lucide:users',
|
||||
bgColor: 'bg-purple-500',
|
||||
routerName: 'PromotionBargainActivity',
|
||||
},
|
||||
|
||||
@@ -40,7 +40,7 @@ async function handleDelete(row: MallBrandApi.Brand) {
|
||||
});
|
||||
try {
|
||||
await deleteBrand(row.id!);
|
||||
message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
|
||||
ElMessage.success($t('ui.actionMessage.deleteSuccess', [row.name]));
|
||||
handleRefresh();
|
||||
} finally {
|
||||
loadingInstance.close();
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export { default as SkuTableSelect } from './sku-table-select.vue';
|
||||
export { default as SpuShowcase } from './spu-showcase.vue';
|
||||
export { default as SpuTableSelect } from './spu-table-select.vue';
|
||||
@@ -0,0 +1,123 @@
|
||||
<!-- SKU 选择弹窗组件 -->
|
||||
<script lang="ts" setup>
|
||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||
import type { MallSpuApi } from '#/api/mall/product/spu';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { fenToYuan } from '@vben/utils';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getSpu } from '#/api/mall/product/spu';
|
||||
|
||||
interface SpuData {
|
||||
spuId: number;
|
||||
}
|
||||
|
||||
const emit = defineEmits<{
|
||||
change: [sku: MallSpuApi.Sku];
|
||||
}>();
|
||||
|
||||
const spuId = ref<number>();
|
||||
|
||||
/** 表格列配置 */
|
||||
const gridColumns = computed<VxeGridProps['columns']>(() => [
|
||||
{
|
||||
type: 'radio',
|
||||
width: 55,
|
||||
},
|
||||
{
|
||||
field: 'picUrl',
|
||||
title: '图片',
|
||||
width: 100,
|
||||
align: 'center',
|
||||
cellRender: {
|
||||
name: 'CellImage',
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'properties',
|
||||
title: '规格',
|
||||
minWidth: 120,
|
||||
align: 'center',
|
||||
formatter: ({ cellValue }) => {
|
||||
return (
|
||||
cellValue?.map((p: MallSpuApi.Property) => p.valueName)?.join(' ') ||
|
||||
'-'
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'price',
|
||||
title: '销售价(元)',
|
||||
width: 120,
|
||||
align: 'center',
|
||||
formatter: ({ cellValue }) => {
|
||||
return fenToYuan(cellValue);
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
/** 处理选中 */
|
||||
function handleRadioChange() {
|
||||
const selectedRow = gridApi.grid.getRadioRecord() as MallSpuApi.Sku;
|
||||
if (selectedRow) {
|
||||
emit('change', selectedRow);
|
||||
modalApi.close();
|
||||
}
|
||||
}
|
||||
|
||||
// TODO @芋艿:要不要直接非 pager?
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
gridOptions: {
|
||||
columns: gridColumns.value,
|
||||
height: 400,
|
||||
border: true,
|
||||
showOverflow: true,
|
||||
radioConfig: {
|
||||
reserve: true,
|
||||
},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async () => {
|
||||
if (!spuId.value) {
|
||||
return { list: [], total: 0 };
|
||||
}
|
||||
const spu = await getSpu(spuId.value);
|
||||
return {
|
||||
list: spu.skus || [],
|
||||
total: spu.skus?.length || 0,
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
gridEvents: {
|
||||
radioChange: handleRadioChange,
|
||||
},
|
||||
});
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
destroyOnClose: true,
|
||||
onOpenChange: async (isOpen: boolean) => {
|
||||
if (!isOpen) {
|
||||
gridApi.grid.clearRadioRow();
|
||||
spuId.value = undefined;
|
||||
return;
|
||||
}
|
||||
const data = modalApi.getData<SpuData>();
|
||||
if (!data?.spuId) {
|
||||
return;
|
||||
}
|
||||
spuId.value = data.spuId;
|
||||
await gridApi.query();
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal class="w-[700px]" title="选择规格">
|
||||
<Grid />
|
||||
</Modal>
|
||||
</template>
|
||||
@@ -0,0 +1,142 @@
|
||||
<!-- 商品橱窗组件:用于展示和选择商品 SPU -->
|
||||
<script lang="ts" setup>
|
||||
import type { MallSpuApi } from '#/api/mall/product/spu';
|
||||
|
||||
import { computed, ref, watch } from 'vue';
|
||||
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
|
||||
import { ElImage, ElTooltip } from 'element-plus';
|
||||
|
||||
import { getSpuDetailList } from '#/api/mall/product/spu';
|
||||
|
||||
import SpuTableSelect from './spu-table-select.vue';
|
||||
|
||||
interface SpuShowcaseProps {
|
||||
modelValue?: number | number[];
|
||||
limit?: number;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<SpuShowcaseProps>(), {
|
||||
modelValue: undefined,
|
||||
limit: Number.MAX_VALUE,
|
||||
disabled: false,
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'change']);
|
||||
|
||||
const productSpus = ref<MallSpuApi.Spu[]>([]); // 已选择的商品列表
|
||||
const spuTableSelectRef = ref<InstanceType<typeof SpuTableSelect>>(); // 商品选择表格组件引用
|
||||
const isMultiple = computed(() => props.limit !== 1); // 是否为多选模式
|
||||
|
||||
/** 计算是否可以添加 */
|
||||
const canAdd = computed(() => {
|
||||
if (props.disabled) {
|
||||
return false;
|
||||
}
|
||||
if (!props.limit) {
|
||||
return true;
|
||||
}
|
||||
return productSpus.value.length < props.limit;
|
||||
});
|
||||
|
||||
/** 监听 modelValue 变化,加载商品详情 */
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
async (newValue) => {
|
||||
// eslint-disable-next-line unicorn/no-nested-ternary
|
||||
const ids = Array.isArray(newValue) ? newValue : newValue ? [newValue] : [];
|
||||
if (ids.length === 0) {
|
||||
productSpus.value = [];
|
||||
return;
|
||||
}
|
||||
// 只有商品发生变化时才重新查询
|
||||
if (
|
||||
productSpus.value.length === 0 ||
|
||||
productSpus.value.some((spu) => !ids.includes(spu.id!))
|
||||
) {
|
||||
productSpus.value = await getSpuDetailList(ids);
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
/** 打开商品选择对话框 */
|
||||
function handleOpenSpuSelect() {
|
||||
spuTableSelectRef.value?.open(productSpus.value);
|
||||
}
|
||||
|
||||
/** 选择商品后触发 */
|
||||
function handleSpuSelected(spus: MallSpuApi.Spu | MallSpuApi.Spu[]) {
|
||||
productSpus.value = Array.isArray(spus) ? spus : [spus];
|
||||
emitSpuChange();
|
||||
}
|
||||
|
||||
/** 删除商品 */
|
||||
function handleRemoveSpu(index: number) {
|
||||
productSpus.value.splice(index, 1);
|
||||
emitSpuChange();
|
||||
}
|
||||
|
||||
/** 触发变更事件 */
|
||||
function emitSpuChange() {
|
||||
if (props.limit === 1) {
|
||||
const spu = productSpus.value.length > 0 ? productSpus.value[0] : null;
|
||||
emit('update:modelValue', spu?.id || 0);
|
||||
emit('change', spu);
|
||||
} else {
|
||||
emit(
|
||||
'update:modelValue',
|
||||
productSpus.value.map((spu) => spu.id!),
|
||||
);
|
||||
emit('change', productSpus.value);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<!-- 已选商品列表 -->
|
||||
<div
|
||||
v-for="(spu, index) in productSpus"
|
||||
:key="spu.id"
|
||||
class="group relative h-[60px] w-[60px] overflow-hidden rounded-lg"
|
||||
>
|
||||
<ElTooltip :content="spu.name">
|
||||
<div class="relative h-full w-full">
|
||||
<ElImage
|
||||
:src="spu.picUrl"
|
||||
class="h-full w-full rounded-lg object-cover"
|
||||
:preview-src-list="[spu.picUrl!]"
|
||||
fit="cover"
|
||||
/>
|
||||
<!-- 删除按钮 -->
|
||||
<IconifyIcon
|
||||
v-if="!disabled"
|
||||
icon="ep:circle-close-filled"
|
||||
class="absolute -right-2 -top-2 cursor-pointer text-xl text-red-500 opacity-0 transition-opacity hover:text-red-600 group-hover:opacity-100"
|
||||
@click="handleRemoveSpu(index)"
|
||||
/>
|
||||
</div>
|
||||
</ElTooltip>
|
||||
</div>
|
||||
|
||||
<!-- 添加商品按钮 -->
|
||||
<ElTooltip v-if="canAdd" content="选择商品">
|
||||
<div
|
||||
class="hover:border-primary hover:bg-primary/5 flex h-[60px] w-[60px] cursor-pointer items-center justify-center rounded-lg border-2 border-dashed transition-colors"
|
||||
@click="handleOpenSpuSelect"
|
||||
>
|
||||
<IconifyIcon icon="ep:plus" class="text-xl text-gray-400" />
|
||||
</div>
|
||||
</ElTooltip>
|
||||
</div>
|
||||
|
||||
<!-- 商品选择对话框 -->
|
||||
<SpuTableSelect
|
||||
ref="spuTableSelectRef"
|
||||
:multiple="isMultiple"
|
||||
@change="handleSpuSelected"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,219 @@
|
||||
<!-- SPU 商品选择弹窗组件 -->
|
||||
<script lang="ts" setup>
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||
import type { MallCategoryApi } from '#/api/mall/product/category';
|
||||
import type { MallSpuApi } from '#/api/mall/product/spu';
|
||||
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { handleTree } from '@vben/utils';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getCategoryList } from '#/api/mall/product/category';
|
||||
import { getSpuPage } from '#/api/mall/product/spu';
|
||||
import { getRangePickerDefaultProps } from '#/utils';
|
||||
|
||||
interface SpuTableSelectProps {
|
||||
multiple?: boolean; // 是否单选:true - checkbox;false - radio
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<SpuTableSelectProps>(), {
|
||||
multiple: false,
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
change: [spu: MallSpuApi.Spu | MallSpuApi.Spu[]];
|
||||
}>();
|
||||
|
||||
const categoryList = ref<MallCategoryApi.Category[]>([]); // 分类列表
|
||||
const categoryTreeList = ref<any[]>([]); // 分类树
|
||||
|
||||
/** 单选:处理选中变化 */
|
||||
function handleRadioChange() {
|
||||
const selectedRow = gridApi.grid.getRadioRecord() as MallSpuApi.Spu;
|
||||
if (selectedRow) {
|
||||
emit('change', selectedRow);
|
||||
modalApi.close();
|
||||
}
|
||||
}
|
||||
|
||||
/** 搜索表单 Schema */
|
||||
const formSchema = computed<VbenFormSchema[]>(() => [
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '商品名称',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入商品名称',
|
||||
clearable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'categoryId',
|
||||
label: '商品分类',
|
||||
component: 'ApiTreeSelect',
|
||||
componentProps: {
|
||||
options: categoryTreeList,
|
||||
props: { label: 'name', children: 'children' },
|
||||
nodeKey: 'id',
|
||||
placeholder: '请选择商品分类',
|
||||
clearable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'createTime',
|
||||
label: '创建时间',
|
||||
component: 'RangePicker',
|
||||
componentProps: {
|
||||
...getRangePickerDefaultProps(),
|
||||
clearable: true,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
/** 表格列配置 */
|
||||
const gridColumns = computed<VxeGridProps['columns']>(() => {
|
||||
const columns: VxeGridProps['columns'] = [];
|
||||
if (props.multiple) {
|
||||
columns.push({ type: 'checkbox', width: 55 });
|
||||
} else {
|
||||
columns.push({ type: 'radio', width: 55 });
|
||||
}
|
||||
columns.push(
|
||||
{
|
||||
field: 'id',
|
||||
title: '商品编号',
|
||||
minWidth: 100,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
field: 'picUrl',
|
||||
title: '商品图',
|
||||
width: 100,
|
||||
align: 'center',
|
||||
cellRender: {
|
||||
name: 'CellImage',
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
title: '商品名称',
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'categoryId',
|
||||
title: '商品分类',
|
||||
minWidth: 120,
|
||||
formatter: ({ cellValue }) => {
|
||||
const category = categoryList.value?.find((c) => c.id === cellValue);
|
||||
return category?.name || '-';
|
||||
},
|
||||
},
|
||||
);
|
||||
return columns;
|
||||
});
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: formSchema.value,
|
||||
layout: 'horizontal',
|
||||
collapsed: false,
|
||||
},
|
||||
gridOptions: {
|
||||
columns: gridColumns.value,
|
||||
height: 500,
|
||||
border: true,
|
||||
checkboxConfig: {
|
||||
reserve: true,
|
||||
},
|
||||
radioConfig: {
|
||||
reserve: true,
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
async query({ page }: any, formValues: any) {
|
||||
return await getSpuPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
tabType: 0,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
gridEvents: {
|
||||
radioChange: handleRadioChange,
|
||||
},
|
||||
});
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
destroyOnClose: true,
|
||||
showConfirmButton: props.multiple, // 特殊:radio 单选情况下,走 handleRadioChange 处理。
|
||||
onConfirm: () => {
|
||||
const selectedRows = gridApi.grid.getCheckboxRecords() as MallSpuApi.Spu[];
|
||||
emit('change', selectedRows);
|
||||
modalApi.close();
|
||||
},
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
await gridApi.grid.clearCheckboxRow();
|
||||
await gridApi.grid.clearRadioRow();
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. 先查询数据
|
||||
await gridApi.query();
|
||||
// 2. 设置已选中行
|
||||
const data = modalApi.getData<MallSpuApi.Spu | MallSpuApi.Spu[]>();
|
||||
if (props.multiple && Array.isArray(data) && data.length > 0) {
|
||||
setTimeout(() => {
|
||||
const tableData = gridApi.grid.getTableData().fullData;
|
||||
data.forEach((spu) => {
|
||||
const row = tableData.find(
|
||||
(item: MallSpuApi.Spu) => item.id === spu.id,
|
||||
);
|
||||
if (row) {
|
||||
gridApi.grid.setCheckboxRow(row, true);
|
||||
}
|
||||
});
|
||||
}, 300);
|
||||
} else if (!props.multiple && data && !Array.isArray(data)) {
|
||||
setTimeout(() => {
|
||||
const tableData = gridApi.grid.getTableData().fullData;
|
||||
const row = tableData.find(
|
||||
(item: MallSpuApi.Spu) => item.id === data.id,
|
||||
);
|
||||
if (row) {
|
||||
gridApi.grid.setRadioRow(row);
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
/** 对外暴露的方法 */
|
||||
defineExpose({
|
||||
open: (data?: MallSpuApi.Spu | MallSpuApi.Spu[]) => {
|
||||
modalApi.setData(data).open();
|
||||
},
|
||||
});
|
||||
|
||||
/** 初始化分类数据 */
|
||||
onMounted(async () => {
|
||||
categoryList.value = await getCategoryList({});
|
||||
categoryTreeList.value = handleTree(categoryList.value, 'id', 'parentId');
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal title="选择商品" class="w-[950px]">
|
||||
<Grid />
|
||||
</Modal>
|
||||
</template>
|
||||
@@ -7,8 +7,6 @@ import { handleTree } from '@vben/utils';
|
||||
import { getCategoryList } from '#/api/mall/product/category';
|
||||
import { getRangePickerDefaultProps } from '#/utils';
|
||||
|
||||
// TODO @霖:所有 mall 的 search 少了,请输入 xxx;表单也是类似
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
|
||||
@@ -141,7 +141,7 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: {
|
||||
dictType: DICT_TYPE.COMMON_STATUS,
|
||||
type: DICT_TYPE.COMMON_STATUS,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -152,7 +152,7 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: {
|
||||
dictType: DICT_TYPE.PROMOTION_BANNER_POSITION,
|
||||
type: DICT_TYPE.PROMOTION_BANNER_POSITION,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,177 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import type { MallCombinationActivityApi } from '#/api/mall/promotion/combination/combinationActivity';
|
||||
|
||||
import { computed, ref, watch } from 'vue';
|
||||
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
|
||||
import * as CombinationActivityApi from '#/api/mall/promotion/combination/combinationActivity';
|
||||
import CombinationTableSelect from '#/views/mall/promotion/combination/components/combination-table-select.vue';
|
||||
|
||||
// 活动橱窗,一般用于装修时使用
|
||||
// 提供功能:展示活动列表、添加活动、删除活动
|
||||
defineOptions({ name: 'CombinationShowcase' });
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: [Array, Number],
|
||||
default: () => [],
|
||||
},
|
||||
// 限制数量:默认不限制
|
||||
limit: {
|
||||
type: Number,
|
||||
default: Number.MAX_VALUE,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'change']);
|
||||
|
||||
// 计算是否可以添加
|
||||
const canAdd = computed(() => {
|
||||
// 情况一:禁用时不可以添加
|
||||
if (props.disabled) return false;
|
||||
// 情况二:未指定限制数量时,可以添加
|
||||
if (!props.limit) return true;
|
||||
// 情况三:检查已添加数量是否小于限制数量
|
||||
return Activitys.value.length < props.limit;
|
||||
});
|
||||
|
||||
// 拼团活动列表
|
||||
const Activitys = ref<MallCombinationActivityApi.CombinationActivity[]>([]);
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
async () => {
|
||||
let ids;
|
||||
if (Array.isArray(props.modelValue)) {
|
||||
ids = props.modelValue;
|
||||
} else {
|
||||
ids = props.modelValue ? [props.modelValue] : [];
|
||||
}
|
||||
|
||||
// 不需要返显
|
||||
if (ids.length === 0) {
|
||||
Activitys.value = [];
|
||||
return;
|
||||
}
|
||||
// 只有活动发生变化之后,才会查询活动
|
||||
if (
|
||||
Activitys.value.length === 0 ||
|
||||
Activitys.value.some(
|
||||
(combinationActivity) => !ids.includes(combinationActivity.id!),
|
||||
)
|
||||
) {
|
||||
Activitys.value =
|
||||
await CombinationActivityApi.getCombinationActivityListByIds(
|
||||
ids as number[],
|
||||
);
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
/** 活动表格选择对话框 */
|
||||
const combinationActivityTableSelectRef = ref();
|
||||
// 打开对话框
|
||||
const openCombinationActivityTableSelect = () => {
|
||||
combinationActivityTableSelectRef.value.open(Activitys.value);
|
||||
};
|
||||
|
||||
/**
|
||||
* 选择活动后触发
|
||||
* @param activityVOs 选中的活动列表
|
||||
*/
|
||||
const handleActivitySelected = (
|
||||
activityVOs:
|
||||
| MallCombinationActivityApi.CombinationActivity
|
||||
| MallCombinationActivityApi.CombinationActivity[],
|
||||
) => {
|
||||
Activitys.value = Array.isArray(activityVOs) ? activityVOs : [activityVOs];
|
||||
emitActivityChange();
|
||||
};
|
||||
|
||||
/**
|
||||
* 删除活动
|
||||
* @param index 活动索引
|
||||
*/
|
||||
const handleRemoveActivity = (index: number) => {
|
||||
Activitys.value.splice(index, 1);
|
||||
emitActivityChange();
|
||||
};
|
||||
const emitActivityChange = () => {
|
||||
if (props.limit === 1) {
|
||||
const combinationActivity =
|
||||
Activitys.value.length > 0 ? Activitys.value[0] : null;
|
||||
emit('update:modelValue', combinationActivity?.id || 0);
|
||||
emit('change', combinationActivity);
|
||||
} else {
|
||||
emit(
|
||||
'update:modelValue',
|
||||
Activitys.value.map((combinationActivity) => combinationActivity.id),
|
||||
);
|
||||
emit('change', Activitys.value);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div class="gap-8px flex flex-wrap items-center">
|
||||
<div
|
||||
v-for="(combinationActivity, index) in Activitys"
|
||||
:key="combinationActivity.id"
|
||||
class="select-box spu-pic"
|
||||
>
|
||||
<el-tooltip :content="combinationActivity.name">
|
||||
<div class="relative h-full w-full">
|
||||
<el-image :src="combinationActivity.picUrl" class="h-full w-full" />
|
||||
<IconifyIcon
|
||||
v-show="!disabled"
|
||||
class="del-icon"
|
||||
icon="ep:circle-close-filled"
|
||||
@click="handleRemoveActivity(index)"
|
||||
/>
|
||||
</div>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<el-tooltip content="选择活动" v-if="canAdd">
|
||||
<div class="select-box" @click="openCombinationActivityTableSelect">
|
||||
<IconifyIcon icon="ep:plus" />
|
||||
</div>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<!-- 拼团活动选择对话框(表格形式) -->
|
||||
<CombinationTableSelect
|
||||
ref="combinationActivityTableSelectRef"
|
||||
:multiple="limit !== 1"
|
||||
@change="handleActivitySelected"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.select-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
cursor: pointer;
|
||||
border: 1px dashed var(--el-border-color-darker);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.spu-pic {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.del-icon {
|
||||
position: absolute;
|
||||
top: -10px;
|
||||
right: -10px;
|
||||
z-index: 1;
|
||||
width: 20px !important;
|
||||
height: 20px !important;
|
||||
}
|
||||
</style>
|
||||
@@ -1,395 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import type { MallCombinationActivityApi } from '#/api/mall/promotion/combination/combinationActivity';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { ContentWrap } from '@vben/common-ui';
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { getDictOptions } from '@vben/hooks';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
import {
|
||||
dateFormatter,
|
||||
fenToYuan,
|
||||
fenToYuanFormat,
|
||||
formatDate,
|
||||
handleTree,
|
||||
} from '@vben/utils';
|
||||
|
||||
import { CHANGE_EVENT } from 'element-plus';
|
||||
|
||||
import * as ProductCategoryApi from '#/api/mall/product/category';
|
||||
import * as CombinationActivityApi from '#/api/mall/promotion/combination/combinationActivity';
|
||||
|
||||
/**
|
||||
* 活动表格选择对话框
|
||||
* 1. 单选模式:
|
||||
* 1.1 点击表格左侧的单选框时,结束选择,并关闭对话框
|
||||
* 1.2 再次打开时,保持选中状态
|
||||
* 2. 多选模式:
|
||||
* 2.1 点击表格左侧的多选框时,记录选中的活动
|
||||
* 2.2 切换分页时,保持活动的选中状态
|
||||
* 2.3 点击右下角的确定按钮时,结束选择,关闭对话框
|
||||
* 2.4 再次打开时,保持选中状态
|
||||
*/
|
||||
defineOptions({ name: 'CombinationTableSelect' });
|
||||
|
||||
defineProps({
|
||||
// 多选模式
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
/** 确认选择时的触发事件 */
|
||||
const emits = defineEmits<{
|
||||
change: [
|
||||
CombinationActivityApi:
|
||||
| any
|
||||
| MallCombinationActivityApi.CombinationActivity
|
||||
| MallCombinationActivityApi.CombinationActivity[],
|
||||
];
|
||||
}>();
|
||||
// 列表的总页数
|
||||
const total = ref(0);
|
||||
// 列表的数据
|
||||
const list = ref<MallCombinationActivityApi.CombinationActivity[]>([]);
|
||||
// 列表的加载中
|
||||
const loading = ref(false);
|
||||
// 弹窗的是否展示
|
||||
const dialogVisible = ref(false);
|
||||
// 查询参数
|
||||
const queryParams = ref({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
name: undefined,
|
||||
status: undefined,
|
||||
});
|
||||
|
||||
/** 打开弹窗 */
|
||||
const open = (
|
||||
CombinationList?: MallCombinationActivityApi.CombinationActivity[],
|
||||
) => {
|
||||
// 重置
|
||||
checkedActivitys.value = [];
|
||||
checkedStatus.value = {};
|
||||
isCheckAll.value = false;
|
||||
isIndeterminate.value = false;
|
||||
|
||||
// 处理已选中
|
||||
if (CombinationList && CombinationList.length > 0) {
|
||||
checkedActivitys.value = [...CombinationList];
|
||||
checkedStatus.value = Object.fromEntries(
|
||||
CombinationList.map((activityVO) => [activityVO.id, true]),
|
||||
);
|
||||
}
|
||||
|
||||
dialogVisible.value = true;
|
||||
resetQuery();
|
||||
};
|
||||
// 提供 open 方法,用于打开弹窗
|
||||
defineExpose({ open });
|
||||
|
||||
/** 查询列表 */
|
||||
const getList = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const data = await CombinationActivityApi.getCombinationActivityPage(
|
||||
queryParams.value,
|
||||
);
|
||||
list.value = data.list;
|
||||
total.value = data.total;
|
||||
// checkbox绑定undefined会有问题,需要给一个bool值
|
||||
list.value.forEach(
|
||||
(activityVO) =>
|
||||
(checkedStatus.value[activityVO.id || ''] =
|
||||
checkedStatus.value[activityVO.id || ''] || false),
|
||||
);
|
||||
// 计算全选框状态
|
||||
calculateIsCheckAll();
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
const handleQuery = () => {
|
||||
queryParams.value.pageNo = 1;
|
||||
getList();
|
||||
};
|
||||
|
||||
/** 重置按钮操作 */
|
||||
const resetQuery = () => {
|
||||
queryParams.value = {
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
name: undefined,
|
||||
status: undefined,
|
||||
};
|
||||
getList();
|
||||
};
|
||||
|
||||
/**
|
||||
* 格式化拼团价格
|
||||
* @param products
|
||||
*/
|
||||
const formatCombinationPrice = (
|
||||
products: MallCombinationActivityApi.CombinationActivity[],
|
||||
) => {
|
||||
const combinationPrice = Math.min(
|
||||
...products.map((item) => item.combinationPrice || 0),
|
||||
);
|
||||
return `¥${fenToYuan(combinationPrice)}`;
|
||||
};
|
||||
|
||||
// 是否全选
|
||||
const isCheckAll = ref(false);
|
||||
// 全选框是否处于中间状态:不是全部选中 && 任意一个选中
|
||||
const isIndeterminate = ref(false);
|
||||
// 选中的活动
|
||||
const checkedActivitys = ref<MallCombinationActivityApi.CombinationActivity[]>(
|
||||
[],
|
||||
);
|
||||
// 选中状态:key为活动ID,value为是否选中
|
||||
const checkedStatus = ref<Record<string, boolean>>({});
|
||||
|
||||
// 选中的活动 activityId
|
||||
const selectedActivityId = ref();
|
||||
/** 单选中时触发 */
|
||||
const handleSingleSelected = (
|
||||
combinationActivityVO: MallCombinationActivityApi.CombinationActivity,
|
||||
) => {
|
||||
emits(CHANGE_EVENT, combinationActivityVO);
|
||||
// 关闭弹窗
|
||||
dialogVisible.value = false;
|
||||
// 记住上次选择的ID
|
||||
selectedActivityId.value = combinationActivityVO.id;
|
||||
};
|
||||
|
||||
/** 多选完成 */
|
||||
const handleEmitChange = () => {
|
||||
// 关闭弹窗
|
||||
dialogVisible.value = false;
|
||||
emits(CHANGE_EVENT, [...checkedActivitys.value]);
|
||||
};
|
||||
|
||||
/** 全选/全不选 */
|
||||
const handleCheckAll = (checked: boolean) => {
|
||||
isCheckAll.value = checked;
|
||||
isIndeterminate.value = false;
|
||||
|
||||
list.value.forEach((combinationActivity) =>
|
||||
handleCheckOne(checked, combinationActivity, false),
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 选中一行
|
||||
* @param checked 是否选中
|
||||
* @param combinationActivity 活动
|
||||
* @param isCalcCheckAll 是否计算全选
|
||||
*/
|
||||
const handleCheckOne = (
|
||||
checked: boolean,
|
||||
combinationActivity: MallCombinationActivityApi.CombinationActivity,
|
||||
isCalcCheckAll: boolean,
|
||||
) => {
|
||||
if (checked) {
|
||||
checkedActivitys.value.push(combinationActivity);
|
||||
checkedStatus.value[combinationActivity.id || ''] = true;
|
||||
} else {
|
||||
const index = findCheckedIndex(combinationActivity);
|
||||
if (index > -1) {
|
||||
checkedActivitys.value.splice(index, 1);
|
||||
checkedStatus.value[combinationActivity.id || ''] = false;
|
||||
isCheckAll.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 计算全选框状态
|
||||
if (isCalcCheckAll) {
|
||||
calculateIsCheckAll();
|
||||
}
|
||||
};
|
||||
|
||||
// 查找活动在已选中活动列表中的索引
|
||||
const findCheckedIndex = (
|
||||
activityVO: MallCombinationActivityApi.CombinationActivity,
|
||||
) => checkedActivitys.value.findIndex((item) => item.id === activityVO.id);
|
||||
|
||||
// 计算全选框状态
|
||||
const calculateIsCheckAll = () => {
|
||||
isCheckAll.value = list.value.every(
|
||||
(activityVO) => checkedStatus.value[activityVO.id || ''],
|
||||
);
|
||||
// 计算中间状态:不是全部选中 && 任意一个选中
|
||||
isIndeterminate.value =
|
||||
!isCheckAll.value &&
|
||||
list.value.some((activityVO) => checkedStatus.value[activityVO.id || '']);
|
||||
};
|
||||
|
||||
// 分类列表
|
||||
const categoryList = ref();
|
||||
// 分类树
|
||||
const categoryTreeList = ref();
|
||||
/** 初始化 */
|
||||
onMounted(async () => {
|
||||
await getList();
|
||||
// 获得分类树
|
||||
categoryList.value = await ProductCategoryApi.getCategoryList({});
|
||||
categoryTreeList.value = handleTree(categoryList.value, 'id', 'parentId');
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog
|
||||
v-model="dialogVisible"
|
||||
:append-to-body="true"
|
||||
title="选择活动"
|
||||
width="70%"
|
||||
>
|
||||
<ContentWrap>
|
||||
<!-- 搜索工作栏 -->
|
||||
<el-form
|
||||
:inline="true"
|
||||
:model="queryParams"
|
||||
class="-mb-15px"
|
||||
label-width="68px"
|
||||
>
|
||||
<el-form-item label="活动名称" prop="name">
|
||||
<el-input
|
||||
v-model="queryParams.name"
|
||||
placeholder="请输入活动名称"
|
||||
clearable
|
||||
@keyup.enter="handleQuery"
|
||||
class="!w-240px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="活动状态" prop="status">
|
||||
<el-select
|
||||
v-model="queryParams.status"
|
||||
placeholder="请选择活动状态"
|
||||
clearable
|
||||
class="!w-240px"
|
||||
>
|
||||
<el-option
|
||||
v-for="dict in getDictOptions(DICT_TYPE.COMMON_STATUS, 'number')"
|
||||
:key="dict.value"
|
||||
:label="dict.label"
|
||||
:value="dict.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button @click="handleQuery">
|
||||
<IconifyIcon class="mr-5px" icon="ep:search" />
|
||||
搜索
|
||||
</el-button>
|
||||
<el-button @click="resetQuery">
|
||||
<IconifyIcon class="mr-5px" icon="ep:refresh" />
|
||||
重置
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-table v-loading="loading" :data="list" show-overflow-tooltip>
|
||||
<!-- 1. 多选模式(不能使用type="selection",Element会忽略Header插槽) -->
|
||||
<el-table-column width="55" v-if="multiple">
|
||||
<template #header>
|
||||
<el-checkbox
|
||||
v-model="isCheckAll"
|
||||
:indeterminate="isIndeterminate"
|
||||
@change="handleCheckAll"
|
||||
/>
|
||||
</template>
|
||||
<template #default="{ row }">
|
||||
<el-checkbox
|
||||
v-model="checkedStatus[row.id]"
|
||||
@change="(checked: boolean) => handleCheckOne(checked, row, true)"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<!-- 2. 单选模式 -->
|
||||
<el-table-column label="#" width="55" v-else>
|
||||
<template #default="{ row }">
|
||||
<el-radio
|
||||
:value="row.id"
|
||||
v-model="selectedActivityId"
|
||||
@change="handleSingleSelected(row)"
|
||||
>
|
||||
<!-- 空格不能省略,是为了让单选框不显示label,如果不指定label不会有选中的效果 -->
|
||||
|
||||
</el-radio>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="活动编号" prop="id" min-width="80" />
|
||||
<el-table-column label="活动名称" prop="name" min-width="140" />
|
||||
<el-table-column label="活动时间" min-width="210">
|
||||
<template #default="scope">
|
||||
{{ formatDate(scope.row.startTime, 'YYYY-MM-DD') }}
|
||||
~ {{ formatDate(scope.row.endTime, 'YYYY-MM-DD') }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="商品图片" prop="spuName" min-width="80">
|
||||
<template #default="scope">
|
||||
<el-image
|
||||
:src="scope.row.picUrl"
|
||||
class="h-40px w-40px"
|
||||
:preview-src-list="[scope.row.picUrl]"
|
||||
preview-teleported
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="商品标题" prop="spuName" min-width="300" />
|
||||
<el-table-column
|
||||
label="原价"
|
||||
prop="marketPrice"
|
||||
min-width="100"
|
||||
:formatter="fenToYuanFormat"
|
||||
/>
|
||||
<el-table-column label="拼团价" prop="seckillPrice" min-width="100">
|
||||
<template #default="scope">
|
||||
{{ formatCombinationPrice(scope.row.products) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="开团组数" prop="groupCount" min-width="100" />
|
||||
<el-table-column
|
||||
label="成团组数"
|
||||
prop="groupSuccessCount"
|
||||
min-width="100"
|
||||
/>
|
||||
<el-table-column label="购买次数" prop="recordCount" min-width="100" />
|
||||
<el-table-column
|
||||
label="活动状态"
|
||||
align="center"
|
||||
prop="status"
|
||||
min-width="100"
|
||||
>
|
||||
<template #default="scope">
|
||||
<dict-tag
|
||||
:type="DICT_TYPE.COMMON_STATUS"
|
||||
:value="scope.row.status"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
label="创建时间"
|
||||
align="center"
|
||||
prop="createTime"
|
||||
:formatter="dateFormatter"
|
||||
width="180px"
|
||||
/>
|
||||
</el-table>
|
||||
<!-- 分页 -->
|
||||
<Pagination
|
||||
v-model:limit="queryParams.pageSize"
|
||||
v-model:page="queryParams.pageNo"
|
||||
:total="total"
|
||||
@pagination="getList"
|
||||
/>
|
||||
</ContentWrap>
|
||||
<template #footer v-if="multiple">
|
||||
<el-button type="primary" @click="handleEmitChange">确 定</el-button>
|
||||
<el-button @click="dialogVisible = false">取 消</el-button>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
@@ -0,0 +1 @@
|
||||
export { default as CombinationShowcase } from './showcase.vue';
|
||||
@@ -0,0 +1,148 @@
|
||||
<!-- 拼团活动橱窗组件:用于展示和选择拼团活动 -->
|
||||
<script lang="ts" setup>
|
||||
import type { MallCombinationActivityApi } from '#/api/mall/promotion/combination/combinationActivity';
|
||||
|
||||
import { computed, ref, watch } from 'vue';
|
||||
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
|
||||
import { ElImage, ElTooltip } from 'element-plus';
|
||||
|
||||
import { getCombinationActivityListByIds } from '#/api/mall/promotion/combination/combinationActivity';
|
||||
|
||||
import CombinationTableSelect from './table-select.vue';
|
||||
|
||||
interface CombinationShowcaseProps {
|
||||
modelValue?: number | number[];
|
||||
limit?: number;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<CombinationShowcaseProps>(), {
|
||||
modelValue: undefined,
|
||||
limit: Number.MAX_VALUE,
|
||||
disabled: false,
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'change']);
|
||||
|
||||
const activityList = ref<MallCombinationActivityApi.CombinationActivity[]>([]); // 已选择的活动列表
|
||||
const combinationTableSelectRef =
|
||||
ref<InstanceType<typeof CombinationTableSelect>>(); // 活动选择表格组件引用
|
||||
const isMultiple = computed(() => props.limit !== 1); // 是否为多选模式
|
||||
|
||||
/** 计算是否可以添加 */
|
||||
const canAdd = computed(() => {
|
||||
if (props.disabled) {
|
||||
return false;
|
||||
}
|
||||
if (!props.limit) {
|
||||
return true;
|
||||
}
|
||||
return activityList.value.length < props.limit;
|
||||
});
|
||||
|
||||
/** 监听 modelValue 变化,加载活动详情 */
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
async (newValue) => {
|
||||
// eslint-disable-next-line unicorn/no-nested-ternary
|
||||
const ids = Array.isArray(newValue) ? newValue : newValue ? [newValue] : [];
|
||||
if (ids.length === 0) {
|
||||
activityList.value = [];
|
||||
return;
|
||||
}
|
||||
// 只有活动发生变化时才重新查询
|
||||
if (
|
||||
activityList.value.length === 0 ||
|
||||
activityList.value.some((activity) => !ids.includes(activity.id!))
|
||||
) {
|
||||
activityList.value = await getCombinationActivityListByIds(ids);
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
/** 打开活动选择对话框 */
|
||||
function handleOpenActivitySelect() {
|
||||
combinationTableSelectRef.value?.open(activityList.value);
|
||||
}
|
||||
|
||||
/** 选择活动后触发 */
|
||||
function handleActivitySelected(
|
||||
activities:
|
||||
| MallCombinationActivityApi.CombinationActivity
|
||||
| MallCombinationActivityApi.CombinationActivity[],
|
||||
) {
|
||||
activityList.value = Array.isArray(activities) ? activities : [activities];
|
||||
emitActivityChange();
|
||||
}
|
||||
|
||||
/** 删除活动 */
|
||||
function handleRemoveActivity(index: number) {
|
||||
activityList.value.splice(index, 1);
|
||||
emitActivityChange();
|
||||
}
|
||||
|
||||
/** 触发变更事件 */
|
||||
function emitActivityChange() {
|
||||
if (props.limit === 1) {
|
||||
const activity =
|
||||
activityList.value.length > 0 ? activityList.value[0] : null;
|
||||
emit('update:modelValue', activity?.id || 0);
|
||||
emit('change', activity);
|
||||
} else {
|
||||
emit(
|
||||
'update:modelValue',
|
||||
activityList.value.map((activity) => activity.id!),
|
||||
);
|
||||
emit('change', activityList.value);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<!-- 已选活动列表 -->
|
||||
<div
|
||||
v-for="(activity, index) in activityList"
|
||||
:key="activity.id"
|
||||
class="group relative h-[60px] w-[60px] overflow-hidden rounded-lg"
|
||||
>
|
||||
<ElTooltip :content="activity.name">
|
||||
<div class="relative h-full w-full">
|
||||
<ElImage
|
||||
:src="activity.picUrl"
|
||||
class="h-full w-full rounded-lg object-cover"
|
||||
:preview-src-list="[activity.picUrl!]"
|
||||
fit="cover"
|
||||
/>
|
||||
<!-- 删除按钮 -->
|
||||
<IconifyIcon
|
||||
v-if="!disabled"
|
||||
icon="ep:circle-close-filled"
|
||||
class="absolute -right-2 -top-2 cursor-pointer text-xl text-red-500 opacity-0 transition-opacity hover:text-red-600 group-hover:opacity-100"
|
||||
@click="handleRemoveActivity(index)"
|
||||
/>
|
||||
</div>
|
||||
</ElTooltip>
|
||||
</div>
|
||||
|
||||
<!-- 添加活动按钮 -->
|
||||
<ElTooltip v-if="canAdd" content="选择活动">
|
||||
<div
|
||||
class="hover:border-primary hover:bg-primary/5 flex h-[60px] w-[60px] cursor-pointer items-center justify-center rounded-lg border-2 border-dashed transition-colors"
|
||||
@click="handleOpenActivitySelect"
|
||||
>
|
||||
<IconifyIcon icon="ep:plus" class="text-xl text-gray-400" />
|
||||
</div>
|
||||
</ElTooltip>
|
||||
</div>
|
||||
|
||||
<!-- 活动选择对话框 -->
|
||||
<CombinationTableSelect
|
||||
ref="combinationTableSelectRef"
|
||||
:multiple="isMultiple"
|
||||
@change="handleActivitySelected"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,285 @@
|
||||
<!-- 拼团活动选择弹窗组件 -->
|
||||
<script lang="ts" setup>
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||
import type { MallCategoryApi } from '#/api/mall/product/category';
|
||||
import type { MallCombinationActivityApi } from '#/api/mall/promotion/combination/combinationActivity';
|
||||
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { getDictOptions } from '@vben/hooks';
|
||||
import { fenToYuan, formatDate, handleTree } from '@vben/utils';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getCategoryList } from '#/api/mall/product/category';
|
||||
import { getCombinationActivityPage } from '#/api/mall/promotion/combination/combinationActivity';
|
||||
|
||||
interface CombinationTableSelectProps {
|
||||
multiple?: boolean; // 是否多选:true - checkbox;false - radio
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<CombinationTableSelectProps>(), {
|
||||
multiple: false,
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
change: [
|
||||
activity:
|
||||
| MallCombinationActivityApi.CombinationActivity
|
||||
| MallCombinationActivityApi.CombinationActivity[],
|
||||
];
|
||||
}>();
|
||||
|
||||
const categoryList = ref<MallCategoryApi.Category[]>([]); // 分类列表
|
||||
const categoryTreeList = ref<any[]>([]); // 分类树
|
||||
|
||||
/** 单选:处理选中变化 */
|
||||
function handleRadioChange() {
|
||||
const selectedRow =
|
||||
gridApi.grid.getRadioRecord() as MallCombinationActivityApi.CombinationActivity;
|
||||
if (selectedRow) {
|
||||
emit('change', selectedRow);
|
||||
modalApi.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化拼团价格
|
||||
* @param products
|
||||
*/
|
||||
const formatCombinationPrice = (
|
||||
products: MallCombinationActivityApi.CombinationProduct[],
|
||||
) => {
|
||||
if (!products || products.length === 0) return '-';
|
||||
const combinationPrice = Math.min(
|
||||
...products.map((item) => item.combinationPrice || 0),
|
||||
);
|
||||
return `¥${fenToYuan(combinationPrice)}`;
|
||||
};
|
||||
|
||||
/** 搜索表单 Schema */
|
||||
const formSchema = computed<VbenFormSchema[]>(() => [
|
||||
{
|
||||
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'),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
/** 表格列配置 */
|
||||
const gridColumns = computed<VxeGridProps['columns']>(() => {
|
||||
const columns: VxeGridProps['columns'] = [];
|
||||
if (props.multiple) {
|
||||
columns.push({ type: 'checkbox', width: 55 });
|
||||
} else {
|
||||
columns.push({ type: 'radio', width: 55 });
|
||||
}
|
||||
columns.push(
|
||||
{
|
||||
field: 'id',
|
||||
title: '活动编号',
|
||||
minWidth: 80,
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
title: '活动名称',
|
||||
minWidth: 140,
|
||||
},
|
||||
{
|
||||
field: 'activityTime',
|
||||
title: '活动时间',
|
||||
minWidth: 210,
|
||||
formatter: ({ row }) => {
|
||||
return `${formatDate(row.startTime, 'YYYY-MM-DD')} ~ ${formatDate(row.endTime, 'YYYY-MM-DD')}`;
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'picUrl',
|
||||
title: '商品图片',
|
||||
width: 100,
|
||||
cellRender: {
|
||||
name: 'CellImage',
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'spuName',
|
||||
title: '商品标题',
|
||||
minWidth: 300,
|
||||
},
|
||||
{
|
||||
field: 'marketPrice',
|
||||
title: '原价',
|
||||
minWidth: 100,
|
||||
formatter: ({ cellValue }) => {
|
||||
return cellValue ? `¥${fenToYuan(cellValue)}` : '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'products',
|
||||
title: '拼团价',
|
||||
minWidth: 100,
|
||||
formatter: ({ cellValue }) => {
|
||||
return formatCombinationPrice(cellValue);
|
||||
},
|
||||
},
|
||||
{
|
||||
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',
|
||||
},
|
||||
);
|
||||
return columns;
|
||||
});
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: formSchema.value,
|
||||
layout: 'horizontal',
|
||||
collapsed: false,
|
||||
},
|
||||
gridOptions: {
|
||||
columns: gridColumns.value,
|
||||
height: 500,
|
||||
border: true,
|
||||
checkboxConfig: {
|
||||
reserve: true,
|
||||
},
|
||||
radioConfig: {
|
||||
reserve: true,
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
async query({ page }: any, formValues: any) {
|
||||
return await getCombinationActivityPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
gridEvents: {
|
||||
radioChange: handleRadioChange,
|
||||
},
|
||||
});
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
destroyOnClose: true,
|
||||
showConfirmButton: props.multiple, // 特殊:radio 单选情况下,走 handleRadioChange 处理。
|
||||
onConfirm: () => {
|
||||
const selectedRows =
|
||||
gridApi.grid.getCheckboxRecords() as MallCombinationActivityApi.CombinationActivity[];
|
||||
emit('change', selectedRows);
|
||||
modalApi.close();
|
||||
},
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
gridApi.grid.clearCheckboxRow();
|
||||
gridApi.grid.clearRadioRow();
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. 先查询数据
|
||||
await gridApi.query();
|
||||
// 2. 设置已选中行
|
||||
const data = modalApi.getData<
|
||||
| MallCombinationActivityApi.CombinationActivity
|
||||
| MallCombinationActivityApi.CombinationActivity[]
|
||||
>();
|
||||
if (props.multiple && Array.isArray(data) && data.length > 0) {
|
||||
setTimeout(() => {
|
||||
const tableData = gridApi.grid.getTableData().fullData;
|
||||
data.forEach((activity) => {
|
||||
const row = tableData.find(
|
||||
(item: MallCombinationActivityApi.CombinationActivity) =>
|
||||
item.id === activity.id,
|
||||
);
|
||||
if (row) {
|
||||
gridApi.grid.setCheckboxRow(row, true);
|
||||
}
|
||||
});
|
||||
}, 300);
|
||||
} else if (!props.multiple && data && !Array.isArray(data)) {
|
||||
setTimeout(() => {
|
||||
const tableData = gridApi.grid.getTableData().fullData;
|
||||
const row = tableData.find(
|
||||
(item: MallCombinationActivityApi.CombinationActivity) =>
|
||||
item.id === data.id,
|
||||
);
|
||||
if (row) {
|
||||
gridApi.grid.setRadioRow(row);
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
/** 对外暴露的方法 */
|
||||
defineExpose({
|
||||
open: (
|
||||
data?:
|
||||
| MallCombinationActivityApi.CombinationActivity
|
||||
| MallCombinationActivityApi.CombinationActivity[],
|
||||
) => {
|
||||
modalApi.setData(data).open();
|
||||
},
|
||||
});
|
||||
|
||||
/** 初始化分类数据 */
|
||||
onMounted(async () => {
|
||||
categoryList.value = await getCategoryList({});
|
||||
categoryTreeList.value = handleTree(categoryList.value, 'id', 'parentId');
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal title="选择活动" class="w-[950px]">
|
||||
<Grid />
|
||||
</Modal>
|
||||
</template>
|
||||
@@ -169,9 +169,7 @@ const handleSliderChange = (prop: string) => {
|
||||
class="mb-0 w-full"
|
||||
>
|
||||
<ElSlider
|
||||
v-model="
|
||||
formData[data.prop as keyof ComponentStyle] as number
|
||||
"
|
||||
v-model="formData[data.prop]"
|
||||
:max="100"
|
||||
:min="0"
|
||||
show-input
|
||||
@@ -197,4 +195,14 @@ const handleSliderChange = (prop: string) => {
|
||||
:deep(.el-input-number) {
|
||||
width: 50px;
|
||||
}
|
||||
|
||||
:deep(.el-tree) {
|
||||
.el-tree-node__expand-icon {
|
||||
margin-right: -15px;
|
||||
}
|
||||
|
||||
.el-form-item {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -93,15 +93,11 @@ const handleDeleteComponent = () => {
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div class="component" :class="[{ active }]">
|
||||
<div
|
||||
:style="{
|
||||
...style,
|
||||
}"
|
||||
>
|
||||
<div class="component relative cursor-move" :class="[{ active }]">
|
||||
<div :style="style">
|
||||
<component :is="component.id" :property="component.property" />
|
||||
</div>
|
||||
<div class="component-wrap">
|
||||
<div class="component-wrap absolute left-[-2px] top-0 block h-full w-full">
|
||||
<!-- 左侧:组件名(悬浮的小贴条) -->
|
||||
<div class="component-name" v-if="component.name">
|
||||
{{ component.name }}
|
||||
@@ -150,19 +146,8 @@ $hover-border-width: 1px;
|
||||
$name-position: -85px;
|
||||
$toolbar-position: -55px;
|
||||
|
||||
/* 组件 */
|
||||
.component {
|
||||
position: relative;
|
||||
cursor: move;
|
||||
|
||||
.component-wrap {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -$active-border-width;
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
/* 鼠标放到组件上时 */
|
||||
&:hover {
|
||||
border: $hover-border-width dashed var(--el-color-primary);
|
||||
@@ -228,7 +213,7 @@ $toolbar-position: -55px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 组件选中时 */
|
||||
/* 选中状态 */
|
||||
&.active {
|
||||
margin-bottom: 4px;
|
||||
|
||||
|
||||
@@ -61,7 +61,10 @@ const handleCloneComponent = (component: DiyComponent<any>) => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElAside class="editor-left" width="261px">
|
||||
<ElAside
|
||||
class="editor-left z-[1] shrink-0 select-none shadow-[8px_0_8px_-8px_rgb(0_0_0/0.12)]"
|
||||
width="261px"
|
||||
>
|
||||
<ElScrollbar>
|
||||
<ElCollapse v-model="extendGroups">
|
||||
<ElCollapseItem
|
||||
@@ -71,7 +74,7 @@ const handleCloneComponent = (component: DiyComponent<any>) => {
|
||||
:title="group.name"
|
||||
>
|
||||
<draggable
|
||||
class="component-container"
|
||||
class="flex flex-wrap items-center"
|
||||
ghost-class="draggable-ghost"
|
||||
item-key="index"
|
||||
:list="group.components"
|
||||
@@ -79,13 +82,22 @@ const handleCloneComponent = (component: DiyComponent<any>) => {
|
||||
:group="{ name: 'component', pull: 'clone', put: false }"
|
||||
:clone="handleCloneComponent"
|
||||
:animation="200"
|
||||
:force-fallback="true"
|
||||
:force-fallback="false"
|
||||
>
|
||||
<template #item="{ element }">
|
||||
<div>
|
||||
<div class="drag-placement">组件放置区域</div>
|
||||
<div class="component">
|
||||
<IconifyIcon :icon="element.icon" :size="32" />
|
||||
<div class="hidden text-white">组件放置区域</div>
|
||||
<div
|
||||
class="component flex h-[86px] w-[86px] cursor-move flex-col items-center justify-center border-b border-r [&:nth-of-type(3n)]:border-r-0"
|
||||
:style="{
|
||||
borderColor: 'var(--el-border-color-lighter)',
|
||||
}"
|
||||
>
|
||||
<IconifyIcon
|
||||
:icon="element.icon"
|
||||
:size="32"
|
||||
class="mb-1 text-gray-500"
|
||||
/>
|
||||
<span class="mt-1 text-xs">{{ element.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -99,11 +111,6 @@ const handleCloneComponent = (component: DiyComponent<any>) => {
|
||||
|
||||
<style scoped lang="scss">
|
||||
.editor-left {
|
||||
z-index: 1;
|
||||
flex-shrink: 0;
|
||||
user-select: none;
|
||||
box-shadow: 8px 0 8px -8px rgb(0 0 0 / 12%);
|
||||
|
||||
:deep(.el-collapse) {
|
||||
border-top: none;
|
||||
}
|
||||
@@ -124,51 +131,19 @@ const handleCloneComponent = (component: DiyComponent<any>) => {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.component-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
width: 261px;
|
||||
}
|
||||
|
||||
.component {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 86px;
|
||||
height: 86px;
|
||||
cursor: move;
|
||||
border-right: 1px solid var(--el-border-color-lighter);
|
||||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||
|
||||
.el-icon {
|
||||
margin-bottom: 4px;
|
||||
color: gray;
|
||||
}
|
||||
}
|
||||
|
||||
/* 组件 hover 和 active 状态(需要 CSS 变量) */
|
||||
.component.active,
|
||||
.component:hover {
|
||||
color: var(--el-color-white);
|
||||
background: var(--el-color-primary);
|
||||
|
||||
.el-icon {
|
||||
:deep(.iconify) {
|
||||
color: var(--el-color-white);
|
||||
}
|
||||
}
|
||||
|
||||
.component:nth-of-type(3n) {
|
||||
border-right: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* 拖拽占位提示,默认不显示 */
|
||||
.drag-placement {
|
||||
display: none;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* 拖拽区域全局样式 */
|
||||
.drag-area {
|
||||
/* 拖拽到手机区域时的样式 */
|
||||
.draggable-ghost {
|
||||
@@ -204,14 +179,12 @@ const handleCloneComponent = (component: DiyComponent<any>) => {
|
||||
background: #5487df;
|
||||
}
|
||||
|
||||
/* 拖拽时隐藏组件 */
|
||||
.component {
|
||||
display: none;
|
||||
display: none; /* 拖拽时隐藏组件 */
|
||||
}
|
||||
|
||||
/* 拖拽时显示占位提示 */
|
||||
.drag-placement {
|
||||
display: block;
|
||||
.hidden {
|
||||
display: block !important; /* 拖拽时显示占位提示 */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ export interface CarouselProperty {
|
||||
indicator: 'dot' | 'number'; // 指示器样式:点 | 数字
|
||||
autoplay: boolean; // 是否自动播放
|
||||
interval: number; // 播放间隔
|
||||
height: number; // 轮播高度
|
||||
items: CarouselItemProperty[]; // 轮播内容
|
||||
style: ComponentStyle; // 组件样式
|
||||
}
|
||||
@@ -28,6 +29,7 @@ export const component = {
|
||||
indicator: 'dot',
|
||||
autoplay: false,
|
||||
interval: 3,
|
||||
height: 174,
|
||||
items: [
|
||||
{
|
||||
type: 'img',
|
||||
|
||||
@@ -29,7 +29,7 @@ const handleIndexChange = (index: number) => {
|
||||
</div>
|
||||
<div v-else class="relative">
|
||||
<ElCarousel
|
||||
height="174px"
|
||||
:height="`${property.height}px`"
|
||||
:type="property.type === 'card' ? 'card' : ''"
|
||||
:autoplay="property.autoplay"
|
||||
:interval="property.interval * 1000"
|
||||
|
||||
@@ -8,6 +8,8 @@ import {
|
||||
ElCard,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElInputNumber,
|
||||
ElRadio,
|
||||
ElRadioButton,
|
||||
ElRadioGroup,
|
||||
ElSlider,
|
||||
@@ -50,6 +52,14 @@ const formData = useVModel(props, 'modelValue', emit);
|
||||
</ElTooltip>
|
||||
</ElRadioGroup>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="高度" prop="height">
|
||||
<ElInputNumber
|
||||
class="mr-[10px] !w-1/2"
|
||||
controls-position="right"
|
||||
v-model="formData.height"
|
||||
/>
|
||||
px
|
||||
</ElFormItem>
|
||||
<ElFormItem label="指示器" prop="indicator">
|
||||
<ElRadioGroup v-model="formData.indicator">
|
||||
<ElRadio value="dot">小圆点</ElRadio>
|
||||
@@ -131,5 +141,3 @@ const formData = useVModel(props, 'modelValue', emit);
|
||||
</ElForm>
|
||||
</ComponentContainerProperty>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
||||
@@ -56,33 +56,15 @@ function handleToggleFab() {
|
||||
<ElButton type="primary" size="large" circle @click="handleToggleFab">
|
||||
<IconifyIcon
|
||||
icon="ep:plus"
|
||||
class="fab-icon"
|
||||
:class="[{ active: expanded }]"
|
||||
class="transition-transform duration-300"
|
||||
:class="expanded ? 'rotate-[135deg]' : 'rotate-0'"
|
||||
/>
|
||||
</ElButton>
|
||||
</div>
|
||||
<!-- 模态背景:展开时显示,点击后折叠 -->
|
||||
<div v-if="expanded" class="modal-bg" @click="handleToggleFab"></div>
|
||||
<div
|
||||
v-if="expanded"
|
||||
class="absolute left-[calc(50%-375px/2)] top-0 z-[11] h-full w-[375px] bg-black/40"
|
||||
@click="handleToggleFab"
|
||||
></div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
/* 模态背景 */
|
||||
.modal-bg {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: calc(50% - 375px / 2);
|
||||
z-index: 11;
|
||||
width: 375px;
|
||||
height: 100%;
|
||||
background-color: rgb(0 0 0 / 40%);
|
||||
}
|
||||
|
||||
.fab-icon {
|
||||
transform: rotate(0deg);
|
||||
transition: transform 0.3s;
|
||||
|
||||
&.active {
|
||||
transform: rotate(135deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -193,12 +193,15 @@ const handleAppLinkChange = (appLink: AppLink) => {
|
||||
<div
|
||||
v-for="(item, hotZoneIndex) in formData"
|
||||
:key="hotZoneIndex"
|
||||
class="hot-zone"
|
||||
class="group absolute z-10 flex cursor-move items-center justify-center border text-base opacity-80"
|
||||
:style="{
|
||||
width: `${item.width}px`,
|
||||
height: `${item.height}px`,
|
||||
top: `${item.top}px`,
|
||||
left: `${item.left}px`,
|
||||
color: 'var(--el-color-primary)',
|
||||
background: 'var(--el-color-primary-light-7)',
|
||||
borderColor: 'var(--el-color-primary)',
|
||||
}"
|
||||
@mousedown="handleMove(item, $event)"
|
||||
@dblclick="handleShowAppLinkDialog(item)"
|
||||
@@ -208,17 +211,18 @@ const handleAppLinkChange = (appLink: AppLink) => {
|
||||
</span>
|
||||
<IconifyIcon
|
||||
icon="ep:close"
|
||||
class="delete"
|
||||
class="absolute right-0 top-0 hidden cursor-pointer rounded-bl-[80%] p-[2px_2px_6px_6px] text-right text-white group-hover:block"
|
||||
:style="{ backgroundColor: 'var(--el-color-primary)' }"
|
||||
:size="14"
|
||||
@click="handleRemove(item)"
|
||||
/>
|
||||
|
||||
<!-- 8 个控制点 -->
|
||||
<span
|
||||
class="ctrl-dot"
|
||||
class="ctrl-dot absolute z-[11] h-2 w-2 rounded-full bg-white"
|
||||
v-for="(dot, dotIndex) in CONTROL_DOT_LIST"
|
||||
:key="dotIndex"
|
||||
:style="dot.style"
|
||||
:style="{ ...dot.style, border: 'inherit' }"
|
||||
@mousedown="handleResize(item, dot, $event)"
|
||||
></span>
|
||||
</div>
|
||||
@@ -236,49 +240,3 @@ const handleAppLinkChange = (appLink: AppLink) => {
|
||||
@app-link-change="handleAppLinkChange"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.hot-zone {
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
color: var(--el-color-primary);
|
||||
cursor: move;
|
||||
background: var(--el-color-primary-light-7);
|
||||
border: 1px solid var(--el-color-primary);
|
||||
opacity: 0.8;
|
||||
|
||||
/* 控制点 */
|
||||
.ctrl-dot {
|
||||
position: absolute;
|
||||
z-index: 11;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background-color: #fff;
|
||||
border: inherit;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.delete {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
display: none;
|
||||
padding: 2px 2px 6px 6px;
|
||||
color: #fff;
|
||||
text-align: right;
|
||||
cursor: pointer;
|
||||
background-color: var(--el-color-primary);
|
||||
border-radius: 0 0 0 80%;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.delete {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -18,31 +18,18 @@ const props = defineProps<{ property: HotZoneProperty }>();
|
||||
<div
|
||||
v-for="(item, index) in props.property.list"
|
||||
:key="index"
|
||||
class="hot-zone"
|
||||
class="absolute z-10 flex cursor-move items-center justify-center border text-sm opacity-80"
|
||||
:style="{
|
||||
width: `${item.width}px`,
|
||||
height: `${item.height}px`,
|
||||
top: `${item.top}px`,
|
||||
left: `${item.left}px`,
|
||||
color: 'var(--el-color-primary)',
|
||||
background: 'var(--el-color-primary-light-7)',
|
||||
borderColor: 'var(--el-color-primary)',
|
||||
}"
|
||||
>
|
||||
{{ item.name }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.hot-zone {
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 14px;
|
||||
color: var(--el-color-primary);
|
||||
cursor: move;
|
||||
background: var(--el-color-primary-light-7);
|
||||
border: 1px solid var(--el-color-primary);
|
||||
opacity: 0.8;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -59,26 +59,3 @@ const handleOpenEditDialog = () => {
|
||||
:img-url="formData.imgUrl"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.hot-zone {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
color: #fff;
|
||||
cursor: move;
|
||||
background: #409effbf;
|
||||
border: 1px solid var(--el-color-primary);
|
||||
|
||||
/* 控制点 */
|
||||
.ctrl-dot {
|
||||
position: absolute;
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
background-color: #fff;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -17,8 +17,6 @@ export interface MagicCubeItemProperty {
|
||||
height: number; // 高
|
||||
top: number; // 上
|
||||
left: number; // 左
|
||||
right: number; // 右
|
||||
bottom: number; // 下
|
||||
}
|
||||
|
||||
/** 定义组件 */
|
||||
|
||||
@@ -4,41 +4,28 @@ import { cloneDeep } from '@vben/utils';
|
||||
|
||||
/** 宫格导航属性 */
|
||||
export interface MenuGridProperty {
|
||||
// 列数
|
||||
column: number;
|
||||
// 导航菜单列表
|
||||
list: MenuGridItemProperty[];
|
||||
// 组件样式
|
||||
style: ComponentStyle;
|
||||
column: number; // 列数
|
||||
list: MenuGridItemProperty[]; // 导航菜单列表
|
||||
style: ComponentStyle; // 组件样式
|
||||
}
|
||||
|
||||
/** 宫格导航项目属性 */
|
||||
export interface MenuGridItemProperty {
|
||||
// 图标链接
|
||||
iconUrl: string;
|
||||
// 标题
|
||||
title: string;
|
||||
// 标题颜色
|
||||
titleColor: string;
|
||||
// 副标题
|
||||
subtitle: string;
|
||||
// 副标题颜色
|
||||
subtitleColor: string;
|
||||
// 链接
|
||||
url: string;
|
||||
// 角标
|
||||
iconUrl: string; // 图标链接
|
||||
title: string; // 标题
|
||||
titleColor: string; // 标题颜色
|
||||
subtitle: string; // 副标题
|
||||
subtitleColor: string; // 副标题颜色
|
||||
url: string; // 链接
|
||||
badge: {
|
||||
// 角标背景颜色
|
||||
bgColor: string;
|
||||
// 是否显示
|
||||
show: boolean;
|
||||
// 角标文字
|
||||
text: string;
|
||||
// 角标文字颜色
|
||||
textColor: string;
|
||||
bgColor: string; // 角标背景颜色
|
||||
show: boolean; // 是否显示
|
||||
text: string; // 角标文字
|
||||
textColor: string; // 角标文字颜色
|
||||
};
|
||||
}
|
||||
|
||||
/** 宫格导航项目默认属性 */
|
||||
export const EMPTY_MENU_GRID_ITEM_PROPERTY = {
|
||||
title: '标题',
|
||||
titleColor: '#333',
|
||||
@@ -51,7 +38,7 @@ export const EMPTY_MENU_GRID_ITEM_PROPERTY = {
|
||||
},
|
||||
} as MenuGridItemProperty;
|
||||
|
||||
// 定义组件
|
||||
/** 定义组件 */
|
||||
export const component = {
|
||||
id: 'MenuGrid',
|
||||
name: '宫格导航',
|
||||
|
||||
@@ -5,6 +5,7 @@ import { ElImage } from 'element-plus';
|
||||
|
||||
/** 宫格导航 */
|
||||
defineOptions({ name: 'MenuGrid' });
|
||||
|
||||
defineProps<{ property: MenuGridProperty }>();
|
||||
</script>
|
||||
|
||||
@@ -16,7 +17,6 @@ defineProps<{ property: MenuGridProperty }>();
|
||||
class="relative flex flex-col items-center pb-3.5 pt-5"
|
||||
:style="{ width: `${100 * (1 / property.column)}%` }"
|
||||
>
|
||||
<!-- 右上角角标 -->
|
||||
<span
|
||||
v-if="item.badge?.show"
|
||||
class="absolute left-1/2 top-2.5 z-10 h-5 rounded-full px-1.5 text-center text-xs leading-5"
|
||||
@@ -43,5 +43,3 @@ defineProps<{ property: MenuGridProperty }>();
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
||||
@@ -12,7 +12,12 @@ import {
|
||||
} from 'element-plus';
|
||||
|
||||
import UploadImg from '#/components/upload/image-upload.vue';
|
||||
import { AppLinkInput, Draggable } from '#/views/mall/promotion/components';
|
||||
import {
|
||||
AppLinkInput,
|
||||
ColorInput,
|
||||
Draggable,
|
||||
InputWithColor,
|
||||
} from '#/views/mall/promotion/components';
|
||||
|
||||
import ComponentContainerProperty from '../../component-container-property.vue';
|
||||
import { EMPTY_MENU_GRID_ITEM_PROPERTY } from './config';
|
||||
@@ -21,7 +26,9 @@ import { EMPTY_MENU_GRID_ITEM_PROPERTY } from './config';
|
||||
defineOptions({ name: 'MenuGridProperty' });
|
||||
|
||||
const props = defineProps<{ modelValue: MenuGridProperty }>();
|
||||
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
|
||||
const formData = useVModel(props, 'modelValue', emit);
|
||||
</script>
|
||||
|
||||
@@ -35,7 +42,6 @@ const formData = useVModel(props, 'modelValue', emit);
|
||||
<ElRadio :value="4">4个</ElRadio>
|
||||
</ElRadioGroup>
|
||||
</ElFormItem>
|
||||
|
||||
<ElCard header="菜单设置" class="property-group" shadow="never">
|
||||
<Draggable
|
||||
v-model="formData.list"
|
||||
@@ -87,5 +93,3 @@ const formData = useVModel(props, 'modelValue', emit);
|
||||
</ElForm>
|
||||
</ComponentContainerProperty>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
||||
@@ -4,28 +4,21 @@ import { cloneDeep } from '@vben/utils';
|
||||
|
||||
/** 列表导航属性 */
|
||||
export interface MenuListProperty {
|
||||
// 导航菜单列表
|
||||
list: MenuListItemProperty[];
|
||||
// 组件样式
|
||||
style: ComponentStyle;
|
||||
list: MenuListItemProperty[]; // 导航菜单列表
|
||||
style: ComponentStyle; // 组件样式
|
||||
}
|
||||
|
||||
/** 列表导航项目属性 */
|
||||
export interface MenuListItemProperty {
|
||||
// 图标链接
|
||||
iconUrl: string;
|
||||
// 标题
|
||||
title: string;
|
||||
// 标题颜色
|
||||
titleColor: string;
|
||||
// 副标题
|
||||
subtitle: string;
|
||||
// 副标题颜色
|
||||
subtitleColor: string;
|
||||
// 链接
|
||||
url: string;
|
||||
iconUrl: string; // 图标链接
|
||||
title: string; // 标题
|
||||
titleColor: string; // 标题颜色
|
||||
subtitle: string; // 副标题
|
||||
subtitleColor: string; // 副标题颜色
|
||||
url: string; // 链接
|
||||
}
|
||||
|
||||
/** 空的列表导航项目属性 */
|
||||
export const EMPTY_MENU_LIST_ITEM_PROPERTY = {
|
||||
title: '标题',
|
||||
titleColor: '#333',
|
||||
@@ -33,7 +26,7 @@ export const EMPTY_MENU_LIST_ITEM_PROPERTY = {
|
||||
subtitleColor: '#bbb',
|
||||
};
|
||||
|
||||
// 定义组件
|
||||
/** 定义组件 */
|
||||
export const component = {
|
||||
id: 'MenuList',
|
||||
name: '列表导航',
|
||||
|
||||
@@ -7,6 +7,7 @@ import { ElImage } from 'element-plus';
|
||||
|
||||
/** 列表导航 */
|
||||
defineOptions({ name: 'MenuList' });
|
||||
|
||||
defineProps<{ property: MenuListProperty }>();
|
||||
</script>
|
||||
|
||||
@@ -15,26 +16,20 @@ defineProps<{ property: MenuListProperty }>();
|
||||
<div
|
||||
v-for="(item, index) in property.list"
|
||||
:key="index"
|
||||
class="item flex h-[42px] flex-row items-center justify-between gap-1 px-3"
|
||||
class="flex h-[42px] flex-row items-center justify-between gap-1 border-t border-[#eee] px-3 first:border-t-0"
|
||||
>
|
||||
<div class="flex flex-1 flex-row items-center gap-2">
|
||||
<ElImage v-if="item.iconUrl" class="h-4 w-4" :src="item.iconUrl" />
|
||||
<span class="text-base" :style="{ color: item.titleColor }">{{
|
||||
item.title
|
||||
}}</span>
|
||||
<span class="text-base" :style="{ color: item.titleColor }">
|
||||
{{ item.title }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="item-center flex flex-row justify-center gap-1">
|
||||
<span class="text-xs" :style="{ color: item.subtitleColor }">{{
|
||||
item.subtitle
|
||||
}}</span>
|
||||
<span class="text-xs" :style="{ color: item.subtitleColor }">
|
||||
{{ item.subtitle }}
|
||||
</span>
|
||||
<IconifyIcon icon="ep:arrow-right" color="#000" :size="16" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.item + .item {
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -18,7 +18,9 @@ import { EMPTY_MENU_LIST_ITEM_PROPERTY } from './config';
|
||||
defineOptions({ name: 'MenuListProperty' });
|
||||
|
||||
const props = defineProps<{ modelValue: MenuListProperty }>();
|
||||
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
|
||||
const formData = useVModel(props, 'modelValue', emit);
|
||||
</script>
|
||||
|
||||
@@ -26,8 +28,6 @@ const formData = useVModel(props, 'modelValue', emit);
|
||||
<ComponentContainerProperty v-model="formData.style">
|
||||
<ElText tag="p"> 菜单设置 </ElText>
|
||||
<ElText type="info" size="small"> 拖动左侧的小圆点可以调整顺序 </ElText>
|
||||
|
||||
<!-- 表单 -->
|
||||
<ElForm label-width="60px" :model="formData" class="mt-2">
|
||||
<Draggable
|
||||
v-model="formData.list"
|
||||
@@ -64,5 +64,3 @@ const formData = useVModel(props, 'modelValue', emit);
|
||||
</ElForm>
|
||||
</ComponentContainerProperty>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
||||
@@ -4,40 +4,28 @@ import { cloneDeep } from '@vben/utils';
|
||||
|
||||
/** 菜单导航属性 */
|
||||
export interface MenuSwiperProperty {
|
||||
// 布局: 图标+文字 | 图标
|
||||
layout: 'icon' | 'iconText';
|
||||
// 行数
|
||||
row: number;
|
||||
// 列数
|
||||
column: number;
|
||||
// 导航菜单列表
|
||||
list: MenuSwiperItemProperty[];
|
||||
// 组件样式
|
||||
style: ComponentStyle;
|
||||
}
|
||||
/** 菜单导航项目属性 */
|
||||
export interface MenuSwiperItemProperty {
|
||||
// 图标链接
|
||||
iconUrl: string;
|
||||
// 标题
|
||||
title: string;
|
||||
// 标题颜色
|
||||
titleColor: string;
|
||||
// 链接
|
||||
url: string;
|
||||
// 角标
|
||||
badge: {
|
||||
// 角标背景颜色
|
||||
bgColor: string;
|
||||
// 是否显示
|
||||
show: boolean;
|
||||
// 角标文字
|
||||
text: string;
|
||||
// 角标文字颜色
|
||||
textColor: string;
|
||||
};
|
||||
layout: 'icon' | 'iconText'; // 布局:图标+文字 | 图标
|
||||
row: number; // 行数
|
||||
column: number; // 列数
|
||||
list: MenuSwiperItemProperty[]; // 导航菜单列表
|
||||
style: ComponentStyle; // 组件样式
|
||||
}
|
||||
|
||||
/** 菜单导航项目属性 */
|
||||
export interface MenuSwiperItemProperty {
|
||||
iconUrl: string; // 图标链接
|
||||
title: string; // 标题
|
||||
titleColor: string; // 标题颜色
|
||||
url: string; // 链接
|
||||
badge: {
|
||||
bgColor: string; // 角标背景颜色
|
||||
show: boolean; // 是否显示
|
||||
text: string; // 角标文字
|
||||
textColor: string; // 角标文字颜色
|
||||
}; // 角标
|
||||
}
|
||||
|
||||
/** 空菜单导航项目属性 */
|
||||
export const EMPTY_MENU_SWIPER_ITEM_PROPERTY = {
|
||||
title: '标题',
|
||||
titleColor: '#333',
|
||||
@@ -48,7 +36,7 @@ export const EMPTY_MENU_SWIPER_ITEM_PROPERTY = {
|
||||
},
|
||||
} as MenuSwiperItemProperty;
|
||||
|
||||
// 定义组件
|
||||
/** 定义组件 */
|
||||
export const component = {
|
||||
id: 'MenuSwiper',
|
||||
name: '菜单导航',
|
||||
|
||||
@@ -7,22 +7,19 @@ import { ElCarousel, ElCarouselItem, ElImage } from 'element-plus';
|
||||
|
||||
/** 菜单导航 */
|
||||
defineOptions({ name: 'MenuSwiper' });
|
||||
const props = defineProps<{ property: MenuSwiperProperty }>();
|
||||
// 标题的高度
|
||||
const TITLE_HEIGHT = 20;
|
||||
// 图标的高度
|
||||
const ICON_SIZE = 32;
|
||||
// 垂直间距:一行上下的间距
|
||||
const SPACE_Y = 16;
|
||||
|
||||
// 分页
|
||||
const pages = ref<MenuSwiperItemProperty[][]>([]);
|
||||
// 轮播图高度
|
||||
const carouselHeight = ref(0);
|
||||
// 行高
|
||||
const rowHeight = ref(0);
|
||||
// 列宽
|
||||
const columnWidth = ref('');
|
||||
const props = defineProps<{ property: MenuSwiperProperty }>();
|
||||
|
||||
const TITLE_HEIGHT = 20; // 标题的高度
|
||||
const ICON_SIZE = 32; // 图标的高度
|
||||
const SPACE_Y = 16; // 垂直间距:一行上下的间距
|
||||
|
||||
const pages = ref<MenuSwiperItemProperty[][]>([]); // 分页
|
||||
const carouselHeight = ref(0); // 轮播图高度
|
||||
|
||||
const rowHeight = ref(0); // 行高
|
||||
const columnWidth = ref(''); // 列宽
|
||||
|
||||
watch(
|
||||
() => props.property,
|
||||
() => {
|
||||
@@ -75,9 +72,7 @@ watch(
|
||||
class="relative flex flex-col items-center justify-center"
|
||||
:style="{ width: columnWidth, height: `${rowHeight}px` }"
|
||||
>
|
||||
<!-- 图标 + 角标 -->
|
||||
<div class="relative" :class="`h-${ICON_SIZE}px w-${ICON_SIZE}px`">
|
||||
<!-- 右上角角标 -->
|
||||
<span
|
||||
v-if="item.badge?.show"
|
||||
class="absolute -right-2.5 -top-2.5 z-10 h-5 rounded-[10px] px-1.5 text-center text-xs leading-5"
|
||||
@@ -94,7 +89,6 @@ watch(
|
||||
class="h-full w-full"
|
||||
/>
|
||||
</div>
|
||||
<!-- 标题 -->
|
||||
<span
|
||||
v-if="property.layout === 'iconText'"
|
||||
class="text-xs"
|
||||
|
||||
@@ -28,13 +28,14 @@ import { EMPTY_MENU_SWIPER_ITEM_PROPERTY } from './config';
|
||||
defineOptions({ name: 'MenuSwiperProperty' });
|
||||
|
||||
const props = defineProps<{ modelValue: MenuSwiperProperty }>();
|
||||
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
|
||||
const formData = useVModel(props, 'modelValue', emit);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ComponentContainerProperty v-model="formData.style">
|
||||
<!-- 表单 -->
|
||||
<ElForm label-width="80px" :model="formData" class="mt-2">
|
||||
<ElFormItem label="布局" prop="layout">
|
||||
<ElRadioGroup v-model="formData.layout">
|
||||
@@ -55,7 +56,6 @@ const formData = useVModel(props, 'modelValue', emit);
|
||||
<ElRadio :value="5">5列</ElRadio>
|
||||
</ElRadioGroup>
|
||||
</ElFormItem>
|
||||
|
||||
<ElCard header="菜单设置" class="property-group" shadow="never">
|
||||
<Draggable
|
||||
v-model="formData.list"
|
||||
@@ -101,5 +101,3 @@ const formData = useVModel(props, 'modelValue', emit);
|
||||
</ElForm>
|
||||
</ComponentContainerProperty>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
<script lang="ts" setup>
|
||||
import type { NavigationBarCellProperty } from '../config';
|
||||
|
||||
import type { Rect } from '#/views/mall/promotion/components/magic-cube-editor/util';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
|
||||
import { useVModel } from '@vueuse/core';
|
||||
import {
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElRadio,
|
||||
ElRadioButton,
|
||||
ElRadioGroup,
|
||||
ElSlider,
|
||||
ElSwitch,
|
||||
ElTooltip,
|
||||
} from 'element-plus';
|
||||
|
||||
import appNavBarMp from '#/assets/imgs/diy/app-nav-bar-mp.png';
|
||||
@@ -22,7 +25,7 @@ import {
|
||||
MagicCubeEditor,
|
||||
} from '#/views/mall/promotion/components';
|
||||
|
||||
// 导航栏属性面板
|
||||
/** 导航栏单元格属性面板 */
|
||||
defineOptions({ name: 'NavigationBarCellProperty' });
|
||||
|
||||
const props = defineProps({
|
||||
@@ -35,42 +38,44 @@ const props = defineProps({
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
|
||||
const cellList = useVModel(props, 'modelValue', emit);
|
||||
|
||||
// 单元格数量:小程序6个(右侧胶囊按钮占了2个),其它平台8个
|
||||
/**
|
||||
* 计算单元格数量
|
||||
* 1. 小程序:6 个(因为右侧有胶囊按钮占据 2 个格子的空间)
|
||||
* 2. 其它平台:8 个(全部空间可用)
|
||||
*/
|
||||
const cellCount = computed(() => (props.isMp ? 6 : 8));
|
||||
|
||||
// 转换为Rect格式的数据
|
||||
const rectList = computed<Rect[]>(() => {
|
||||
return cellList.value.map((cell) => ({
|
||||
left: cell.left,
|
||||
top: cell.top,
|
||||
width: cell.width,
|
||||
height: cell.height,
|
||||
right: cell.left + cell.width,
|
||||
bottom: cell.top + cell.height,
|
||||
}));
|
||||
});
|
||||
const selectedHotAreaIndex = ref(0); // 选中的热区
|
||||
|
||||
// 选中的热区
|
||||
const selectedHotAreaIndex = ref(0);
|
||||
const handleHotAreaSelected = (
|
||||
/** 处理热区被选中事件 */
|
||||
function handleHotAreaSelected(
|
||||
cellValue: NavigationBarCellProperty,
|
||||
index: number,
|
||||
) => {
|
||||
) {
|
||||
selectedHotAreaIndex.value = index;
|
||||
// 默认设置为选中文字,并设置属性
|
||||
if (!cellValue.type) {
|
||||
cellValue.type = 'text';
|
||||
cellValue.textColor = '#111111';
|
||||
}
|
||||
};
|
||||
// 如果点击的是搜索框,则初始化搜索框的属性
|
||||
if (cellValue.type === 'search') {
|
||||
cellValue.placeholderPosition = 'left';
|
||||
cellValue.backgroundColor = '#EEEEEE';
|
||||
cellValue.textColor = '#969799';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-40px flex items-center justify-center">
|
||||
<MagicCubeEditor
|
||||
v-model="rectList"
|
||||
v-model="cellList as any"
|
||||
:cols="cellCount"
|
||||
:cube-size="38"
|
||||
:rows="1"
|
||||
@@ -87,7 +92,10 @@ const handleHotAreaSelected = (
|
||||
<template v-for="(cell, cellIndex) in cellList" :key="cellIndex">
|
||||
<template v-if="selectedHotAreaIndex === Number(cellIndex)">
|
||||
<ElFormItem :prop="`cell[${cellIndex}].type`" label="类型">
|
||||
<ElRadioGroup v-model="cell.type">
|
||||
<ElRadioGroup
|
||||
v-model="cell.type"
|
||||
@change="handleHotAreaSelected(cell, cellIndex)"
|
||||
>
|
||||
<ElRadio value="text">文字</ElRadio>
|
||||
<ElRadio value="image">图片</ElRadio>
|
||||
<ElRadio value="search">搜索框</ElRadio>
|
||||
@@ -124,9 +132,32 @@ const handleHotAreaSelected = (
|
||||
</template>
|
||||
<!-- 3. 搜索框 -->
|
||||
<template v-else>
|
||||
<ElFormItem label="框体颜色" prop="backgroundColor">
|
||||
<ColorInput v-model="cell.backgroundColor" />
|
||||
</ElFormItem>
|
||||
<ElFormItem class="lef" label="文本颜色" prop="textColor">
|
||||
<ColorInput v-model="cell.textColor" />
|
||||
</ElFormItem>
|
||||
<ElFormItem :prop="`cell[${cellIndex}].placeholder`" label="提示文字">
|
||||
<ElInput v-model="cell.placeholder" maxlength="10" show-word-limit />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="文本位置" prop="placeholderPosition">
|
||||
<ElRadioGroup v-model="cell!.placeholderPosition">
|
||||
<ElTooltip content="居左" placement="top">
|
||||
<ElRadioButton value="left">
|
||||
<IconifyIcon icon="ant-design:align-left-outlined" />
|
||||
</ElRadioButton>
|
||||
</ElTooltip>
|
||||
<ElTooltip content="居中" placement="top">
|
||||
<ElRadioButton value="center">
|
||||
<IconifyIcon icon="ant-design:align-center-outlined" />
|
||||
</ElRadioButton>
|
||||
</ElTooltip>
|
||||
</ElRadioGroup>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="扫一扫" prop="showScan">
|
||||
<ElSwitch v-model="cell!.showScan" />
|
||||
</ElFormItem>
|
||||
<ElFormItem :prop="`cell[${cellIndex}].borderRadius`" label="圆角">
|
||||
<ElSlider
|
||||
v-model="cell.borderRadius"
|
||||
@@ -141,5 +172,3 @@ const handleHotAreaSelected = (
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
||||
@@ -2,56 +2,38 @@ import type { DiyComponent } from '../../../util';
|
||||
|
||||
/** 顶部导航栏属性 */
|
||||
export interface NavigationBarProperty {
|
||||
// 背景类型
|
||||
bgType: 'color' | 'img';
|
||||
// 背景颜色
|
||||
bgColor: string;
|
||||
// 图片链接
|
||||
bgImg: string;
|
||||
// 样式类型:默认 | 沉浸式
|
||||
styleType: 'inner' | 'normal';
|
||||
// 常驻显示
|
||||
alwaysShow: boolean;
|
||||
// 小程序单元格列表
|
||||
mpCells: NavigationBarCellProperty[];
|
||||
// 其它平台单元格列表
|
||||
otherCells: NavigationBarCellProperty[];
|
||||
// 本地变量
|
||||
bgType: 'color' | 'img'; // 背景类型
|
||||
bgColor: string; // 背景颜色
|
||||
bgImg: string; // 图片链接
|
||||
styleType: 'inner' | 'normal'; // 样式类型:默认 | 沉浸式
|
||||
alwaysShow: boolean; // 常驻显示
|
||||
mpCells: NavigationBarCellProperty[]; // 小程序单元格列表
|
||||
otherCells: NavigationBarCellProperty[]; // 其它平台单元格列表
|
||||
_local: {
|
||||
// 预览顶部导航(小程序)
|
||||
previewMp: boolean;
|
||||
// 预览顶部导航(非小程序)
|
||||
previewOther: boolean;
|
||||
};
|
||||
previewMp: boolean; // 预览顶部导航(小程序)
|
||||
previewOther: boolean; // 预览顶部导航(非小程序)
|
||||
}; // 本地变量
|
||||
}
|
||||
|
||||
/** 顶部导航栏 - 单元格 属性 */
|
||||
export interface NavigationBarCellProperty {
|
||||
// 类型:文字 | 图片 | 搜索框
|
||||
type: 'image' | 'search' | 'text';
|
||||
// 宽度
|
||||
width: number;
|
||||
// 高度
|
||||
height: number;
|
||||
// 顶部位置
|
||||
top: number;
|
||||
// 左侧位置
|
||||
left: number;
|
||||
// 文字内容
|
||||
text: string;
|
||||
// 文字颜色
|
||||
textColor: string;
|
||||
// 图片地址
|
||||
imgUrl: string;
|
||||
// 图片链接
|
||||
url: string;
|
||||
// 搜索框:提示文字
|
||||
placeholder: string;
|
||||
// 搜索框:边框圆角半径
|
||||
borderRadius: number;
|
||||
type: 'image' | 'search' | 'text'; // 类型:文字 | 图片 | 搜索框
|
||||
width: number; // 宽度
|
||||
height: number; // 高度
|
||||
top: number; // 顶部位置
|
||||
left: number; // 左侧位置
|
||||
text: string; // 文字内容
|
||||
textColor: string; // 文字颜色
|
||||
imgUrl: string; // 图片地址
|
||||
url: string; // 图片链接
|
||||
backgroundColor: string; // 搜索框:框体颜色
|
||||
placeholder: string; // 搜索框:提示文字
|
||||
placeholderPosition: string; // 搜索框:提示文字位置
|
||||
showScan: boolean; // 搜索框:是否显示扫一扫
|
||||
borderRadius: number; // 搜索框:边框圆角半径
|
||||
}
|
||||
|
||||
// 定义组件
|
||||
/** 定义组件 */
|
||||
export const component = {
|
||||
id: 'NavigationBar',
|
||||
name: '顶部导航栏',
|
||||
|
||||
@@ -18,7 +18,7 @@ defineOptions({ name: 'NavigationBar' });
|
||||
|
||||
const props = defineProps<{ property: NavigationBarProperty }>();
|
||||
|
||||
// 背景
|
||||
/** 计算背景样式 */
|
||||
const bgStyle = computed(() => {
|
||||
const background =
|
||||
props.property.bgType === 'img' && props.property.bgImg
|
||||
@@ -26,27 +26,31 @@ const bgStyle = computed(() => {
|
||||
: props.property.bgColor;
|
||||
return { background };
|
||||
});
|
||||
// 单元格列表
|
||||
|
||||
/** 获取当前预览的单元格列表 */
|
||||
const cellList = computed(() =>
|
||||
props.property._local?.previewMp
|
||||
? props.property.mpCells
|
||||
: props.property.otherCells,
|
||||
);
|
||||
// 单元格宽度
|
||||
|
||||
/** 计算单元格宽度 */
|
||||
const cellWidth = computed(() => {
|
||||
return props.property._local?.previewMp
|
||||
? (375 - 80 - 86) / 6
|
||||
: (375 - 90) / 8;
|
||||
});
|
||||
// 获得单元格样式
|
||||
const getCellStyle = (cell: NavigationBarCellProperty) => {
|
||||
|
||||
/** 获取单元格样式 */
|
||||
function getCellStyle(cell: NavigationBarCellProperty) {
|
||||
return {
|
||||
width: `${cell.width * cellWidth.value + (cell.width - 1) * 10}px`,
|
||||
left: `${cell.left * cellWidth.value + (cell.left + 1) * 10}px`,
|
||||
position: 'absolute',
|
||||
} as StyleValue;
|
||||
};
|
||||
// 获得搜索框属性
|
||||
}
|
||||
|
||||
/** 获取搜索框属性配置 */
|
||||
const getSearchProp = computed(() => (cell: NavigationBarCellProperty) => {
|
||||
return {
|
||||
height: 30,
|
||||
@@ -57,7 +61,10 @@ const getSearchProp = computed(() => (cell: NavigationBarCellProperty) => {
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<div class="navigation-bar" :style="bgStyle">
|
||||
<div
|
||||
class="flex h-[50px] items-center justify-between bg-white px-[6px]"
|
||||
:style="bgStyle"
|
||||
>
|
||||
<div class="flex h-full w-full items-center">
|
||||
<div
|
||||
v-for="(cell, cellIndex) in cellList"
|
||||
@@ -82,31 +89,3 @@ const getSearchProp = computed(() => (cell: NavigationBarCellProperty) => {
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<style lang="scss" scoped>
|
||||
.navigation-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 50px;
|
||||
padding: 0 6px;
|
||||
background: #fff;
|
||||
|
||||
/* 左边 */
|
||||
.left {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.center {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
line-height: 35px;
|
||||
color: #333;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 右边 */
|
||||
.right {
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,19 +2,31 @@
|
||||
import type { NavigationBarProperty } from './config';
|
||||
|
||||
import { useVModel } from '@vueuse/core';
|
||||
import {
|
||||
ElCard,
|
||||
ElCheckbox,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElRadio,
|
||||
ElRadioGroup,
|
||||
ElTooltip,
|
||||
} from 'element-plus';
|
||||
|
||||
import UploadImg from '#/components/upload/image-upload.vue';
|
||||
import { ColorInput } from '#/views/mall/promotion/components';
|
||||
|
||||
import NavigationBarCellProperty from './components/cell-property.vue';
|
||||
|
||||
// 导航栏属性面板
|
||||
/** 导航栏属性面板 */
|
||||
defineOptions({ name: 'NavigationBarProperty' });
|
||||
|
||||
const props = defineProps<{ modelValue: NavigationBarProperty }>();
|
||||
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
|
||||
// 表单校验
|
||||
const rules = {
|
||||
name: [{ required: true, message: '请输入页面名称', trigger: 'blur' }],
|
||||
};
|
||||
}; // 表单校验
|
||||
|
||||
const formData = useVModel(props, 'modelValue', emit);
|
||||
if (!formData.value._local) {
|
||||
@@ -23,47 +35,47 @@ if (!formData.value._local) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-form label-width="80px" :model="formData" :rules="rules">
|
||||
<el-form-item label="样式" prop="styleType">
|
||||
<el-radio-group v-model="formData!.styleType">
|
||||
<el-radio value="normal">标准</el-radio>
|
||||
<el-tooltip
|
||||
<ElForm label-width="80px" :model="formData" :rules="rules">
|
||||
<ElFormItem label="样式" prop="styleType">
|
||||
<ElRadioGroup v-model="formData!.styleType">
|
||||
<ElRadio value="normal">标准</ElRadio>
|
||||
<ElTooltip
|
||||
content="沉侵式头部仅支持微信小程序、APP,建议页面第一个组件为图片展示类组件"
|
||||
placement="top"
|
||||
>
|
||||
<el-radio value="inner">沉浸式</el-radio>
|
||||
</el-tooltip>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
<ElRadio value="inner">沉浸式</ElRadio>
|
||||
</ElTooltip>
|
||||
</ElRadioGroup>
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
label="常驻显示"
|
||||
prop="alwaysShow"
|
||||
v-if="formData.styleType === 'inner'"
|
||||
>
|
||||
<el-radio-group v-model="formData!.alwaysShow">
|
||||
<el-radio :value="false">关闭</el-radio>
|
||||
<el-tooltip
|
||||
<ElRadioGroup v-model="formData!.alwaysShow">
|
||||
<ElRadio :value="false">关闭</ElRadio>
|
||||
<ElTooltip
|
||||
content="常驻显示关闭后,头部小组件将在页面滑动时淡入"
|
||||
placement="top"
|
||||
>
|
||||
<el-radio :value="true">开启</el-radio>
|
||||
</el-tooltip>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="背景类型" prop="bgType">
|
||||
<el-radio-group v-model="formData.bgType">
|
||||
<el-radio value="color">纯色</el-radio>
|
||||
<el-radio value="img">图片</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
<ElRadio :value="true">开启</ElRadio>
|
||||
</ElTooltip>
|
||||
</ElRadioGroup>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="背景类型" prop="bgType">
|
||||
<ElRadioGroup v-model="formData.bgType">
|
||||
<ElRadio value="color">纯色</ElRadio>
|
||||
<ElRadio value="img">图片</ElRadio>
|
||||
</ElRadioGroup>
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
label="背景颜色"
|
||||
prop="bgColor"
|
||||
v-if="formData.bgType === 'color'"
|
||||
>
|
||||
<ColorInput v-model="formData.bgColor" />
|
||||
</el-form-item>
|
||||
<el-form-item label="背景图片" prop="bgImg" v-else>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="背景图片" prop="bgImg" v-else>
|
||||
<div class="flex items-center">
|
||||
<UploadImg
|
||||
v-model="formData.bgImg"
|
||||
@@ -74,44 +86,42 @@ if (!formData.value._local) {
|
||||
/>
|
||||
<span class="mb-2 ml-2 text-xs text-gray-400">建议宽度:750</span>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-card class="property-group" shadow="never">
|
||||
</ElFormItem>
|
||||
<ElCard class="property-group" shadow="never">
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<span>内容(小程序)</span>
|
||||
<el-form-item prop="_local.previewMp" class="mb-0">
|
||||
<el-checkbox
|
||||
<ElFormItem prop="_local.previewMp" class="mb-0">
|
||||
<ElCheckbox
|
||||
v-model="formData._local.previewMp"
|
||||
@change="
|
||||
formData._local.previewOther = !formData._local.previewMp
|
||||
"
|
||||
>
|
||||
预览
|
||||
</el-checkbox>
|
||||
</el-form-item>
|
||||
</ElCheckbox>
|
||||
</ElFormItem>
|
||||
</div>
|
||||
</template>
|
||||
<NavigationBarCellProperty v-model="formData.mpCells" is-mp />
|
||||
</el-card>
|
||||
<el-card class="property-group" shadow="never">
|
||||
</ElCard>
|
||||
<ElCard class="property-group" shadow="never">
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<span>内容(非小程序)</span>
|
||||
<el-form-item prop="_local.previewOther" class="mb-0">
|
||||
<el-checkbox
|
||||
<ElFormItem prop="_local.previewOther" class="mb-0">
|
||||
<ElCheckbox
|
||||
v-model="formData._local.previewOther"
|
||||
@change="
|
||||
formData._local.previewMp = !formData._local.previewOther
|
||||
"
|
||||
>
|
||||
预览
|
||||
</el-checkbox>
|
||||
</el-form-item>
|
||||
</ElCheckbox>
|
||||
</ElFormItem>
|
||||
</div>
|
||||
</template>
|
||||
<NavigationBarCellProperty v-model="formData.otherCells" :is-mp="false" />
|
||||
</el-card>
|
||||
</el-form>
|
||||
</ElCard>
|
||||
</ElForm>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
||||
@@ -2,27 +2,20 @@ import type { ComponentStyle, DiyComponent } from '../../../util';
|
||||
|
||||
/** 公告栏属性 */
|
||||
export interface NoticeBarProperty {
|
||||
// 图标地址
|
||||
iconUrl: string;
|
||||
// 公告内容列表
|
||||
contents: NoticeContentProperty[];
|
||||
// 背景颜色
|
||||
backgroundColor: string;
|
||||
// 文字颜色
|
||||
textColor: string;
|
||||
// 组件样式
|
||||
style: ComponentStyle;
|
||||
iconUrl: string; // 图标地址
|
||||
contents: NoticeContentProperty[]; // 公告内容列表
|
||||
backgroundColor: string; // 背景颜色
|
||||
textColor: string; // 文字颜色
|
||||
style: ComponentStyle; // 组件样式
|
||||
}
|
||||
|
||||
/** 内容属性 */
|
||||
export interface NoticeContentProperty {
|
||||
// 内容文字
|
||||
text: string;
|
||||
// 链接地址
|
||||
url: string;
|
||||
text: string; // 内容文字
|
||||
url: string; // 链接地址
|
||||
}
|
||||
|
||||
// 定义组件
|
||||
/** 定义组件 */
|
||||
export const component = {
|
||||
id: 'NoticeBar',
|
||||
name: '公告栏',
|
||||
|
||||
@@ -34,5 +34,3 @@ defineProps<{ property: NoticeBarProperty }>();
|
||||
<IconifyIcon icon="ep:arrow-right" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
||||
@@ -13,18 +13,17 @@ import {
|
||||
|
||||
import ComponentContainerProperty from '../../component-container-property.vue';
|
||||
|
||||
// 通知栏属性面板
|
||||
/** 公告栏属性面板 */
|
||||
defineOptions({ name: 'NoticeBarProperty' });
|
||||
|
||||
const props = defineProps<{ modelValue: NoticeBarProperty }>();
|
||||
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
|
||||
// 表单校验
|
||||
const formData = useVModel(props, 'modelValue', emit);
|
||||
const rules = {
|
||||
content: [{ required: true, message: '请输入公告', trigger: 'blur' }],
|
||||
};
|
||||
|
||||
const formData = useVModel(props, 'modelValue', emit);
|
||||
}; // 表单校验
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -60,5 +59,3 @@ const formData = useVModel(props, 'modelValue', emit);
|
||||
</ElForm>
|
||||
</ComponentContainerProperty>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
||||
@@ -2,15 +2,12 @@ import type { DiyComponent } from '../../../util';
|
||||
|
||||
/** 页面设置属性 */
|
||||
export interface PageConfigProperty {
|
||||
// 页面描述
|
||||
description: string;
|
||||
// 页面背景颜色
|
||||
backgroundColor: string;
|
||||
// 页面背景图片
|
||||
backgroundImage: string;
|
||||
description: string; // 页面描述
|
||||
backgroundColor: string; // 页面背景颜色
|
||||
backgroundImage: string; // 页面背景图片
|
||||
}
|
||||
|
||||
// 定义页面组件
|
||||
/** 定义页面组件 */
|
||||
export const component = {
|
||||
id: 'PageConfig',
|
||||
name: '页面设置',
|
||||
|
||||
@@ -7,24 +7,22 @@ import { ElForm, ElFormItem, ElInput } from 'element-plus';
|
||||
import UploadImg from '#/components/upload/image-upload.vue';
|
||||
import { ColorInput } from '#/views/mall/promotion/components';
|
||||
|
||||
// 导航栏属性面板
|
||||
/** 导航栏属性面板 */
|
||||
defineOptions({ name: 'PageConfigProperty' });
|
||||
|
||||
const props = defineProps<{ modelValue: PageConfigProperty }>();
|
||||
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
|
||||
// 表单校验
|
||||
const rules = {};
|
||||
|
||||
const formData = useVModel(props, 'modelValue', emit);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElForm label-width="80px" :model="formData" :rules="rules">
|
||||
<ElForm label-width="80px" :model="formData">
|
||||
<ElFormItem label="页面描述" prop="description">
|
||||
<ElInput
|
||||
type="textarea"
|
||||
v-model="formData!.description"
|
||||
type="textarea"
|
||||
placeholder="用户通过微信分享给朋友时,会自动显示页面描述"
|
||||
/>
|
||||
</ElFormItem>
|
||||
@@ -42,5 +40,3 @@ const formData = useVModel(props, 'modelValue', emit);
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
||||
@@ -2,19 +2,17 @@ import type { DiyComponent } from '../../../util';
|
||||
|
||||
/** 弹窗广告属性 */
|
||||
export interface PopoverProperty {
|
||||
list: PopoverItemProperty[];
|
||||
list: PopoverItemProperty[]; // 弹窗列表
|
||||
}
|
||||
|
||||
/** 弹窗广告项目属性 */
|
||||
export interface PopoverItemProperty {
|
||||
// 图片地址
|
||||
imgUrl: string;
|
||||
// 跳转连接
|
||||
url: string;
|
||||
// 显示类型:仅显示一次、每次启动都会显示
|
||||
showType: 'always' | 'once';
|
||||
imgUrl: string; // 图片地址
|
||||
url: string; // 跳转连接
|
||||
showType: 'always' | 'once'; // 显示类型:仅显示一次、每次启动都会显示
|
||||
}
|
||||
|
||||
// 定义组件
|
||||
/** 定义组件 */
|
||||
export const component = {
|
||||
id: 'Popover',
|
||||
name: '弹窗广告',
|
||||
@@ -9,18 +9,20 @@ import { ElImage } from 'element-plus';
|
||||
|
||||
/** 弹窗广告 */
|
||||
defineOptions({ name: 'Popover' });
|
||||
// 定义属性
|
||||
defineProps<{ property: PopoverProperty }>();
|
||||
|
||||
// 处理选中
|
||||
const activeIndex = ref(0);
|
||||
const handleActive = (index: number) => {
|
||||
const props = defineProps<{ property: PopoverProperty }>();
|
||||
|
||||
const activeIndex = ref(0); // 选中 index
|
||||
|
||||
/** 处理选中 */
|
||||
function handleActive(index: number) {
|
||||
activeIndex.value = index;
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-for="(item, index) in property.list"
|
||||
v-for="(item, index) in props.property.list"
|
||||
:key="index"
|
||||
class="absolute bottom-1/2 right-1/2 h-[454px] w-[292px] rounded border border-gray-300 bg-white p-0.5"
|
||||
:style="{
|
||||
@@ -40,5 +42,3 @@ const handleActive = (index: number) => {
|
||||
<div class="absolute right-1 top-1 text-xs">{{ index + 1 }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
@@ -13,11 +13,13 @@ import {
|
||||
import UploadImg from '#/components/upload/image-upload.vue';
|
||||
import { AppLinkInput, Draggable } from '#/views/mall/promotion/components';
|
||||
|
||||
// 弹窗广告属性面板
|
||||
/** 弹窗广告属性面板 */
|
||||
defineOptions({ name: 'PopoverProperty' });
|
||||
|
||||
const props = defineProps<{ modelValue: PopoverProperty }>();
|
||||
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
|
||||
const formData = useVModel(props, 'modelValue', emit);
|
||||
</script>
|
||||
|
||||
@@ -53,5 +55,3 @@ const formData = useVModel(props, 'modelValue', emit);
|
||||
</Draggable>
|
||||
</ElForm>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
@@ -2,63 +2,40 @@ import type { ComponentStyle, DiyComponent } from '../../../util';
|
||||
|
||||
/** 商品卡片属性 */
|
||||
export interface ProductCardProperty {
|
||||
// 布局类型:单列大图 | 单列小图 | 双列
|
||||
layoutType: 'oneColBigImg' | 'oneColSmallImg' | 'twoCol';
|
||||
// 商品字段
|
||||
layoutType: 'oneColBigImg' | 'oneColSmallImg' | 'twoCol'; // 布局类型:单列大图 | 单列小图 | 双列
|
||||
fields: {
|
||||
// 商品简介
|
||||
introduction: ProductCardFieldProperty;
|
||||
// 商品市场价
|
||||
marketPrice: ProductCardFieldProperty;
|
||||
// 商品名称
|
||||
name: ProductCardFieldProperty;
|
||||
// 商品价格
|
||||
price: ProductCardFieldProperty;
|
||||
// 商品销量
|
||||
salesCount: ProductCardFieldProperty;
|
||||
// 商品库存
|
||||
stock: ProductCardFieldProperty;
|
||||
};
|
||||
// 角标
|
||||
introduction: ProductCardFieldProperty; // 商品简介
|
||||
marketPrice: ProductCardFieldProperty; // 商品市场价
|
||||
name: ProductCardFieldProperty; // 商品名称
|
||||
price: ProductCardFieldProperty; // 商品价格
|
||||
salesCount: ProductCardFieldProperty; // 商品销量
|
||||
stock: ProductCardFieldProperty; // 商品库存
|
||||
}; // 商品字段
|
||||
badge: {
|
||||
// 角标图片
|
||||
imgUrl: string;
|
||||
// 是否显示
|
||||
show: boolean;
|
||||
};
|
||||
// 按钮
|
||||
imgUrl: string; // 角标图片
|
||||
show: boolean; // 是否显示
|
||||
}; // 角标
|
||||
btnBuy: {
|
||||
// 文字按钮:背景渐变起始颜色
|
||||
bgBeginColor: string;
|
||||
// 文字按钮:背景渐变结束颜色
|
||||
bgEndColor: string;
|
||||
// 图片按钮:图片地址
|
||||
imgUrl: string;
|
||||
// 文字
|
||||
text: string;
|
||||
// 类型:文字 | 图片
|
||||
type: 'img' | 'text';
|
||||
};
|
||||
// 上圆角
|
||||
borderRadiusTop: number;
|
||||
// 下圆角
|
||||
borderRadiusBottom: number;
|
||||
// 间距
|
||||
space: number;
|
||||
// 商品编号列表
|
||||
spuIds: number[];
|
||||
// 组件样式
|
||||
style: ComponentStyle;
|
||||
}
|
||||
// 商品字段
|
||||
export interface ProductCardFieldProperty {
|
||||
// 是否显示
|
||||
show: boolean;
|
||||
// 颜色
|
||||
color: string;
|
||||
bgBeginColor: string; // 文字按钮:背景渐变起始颜色
|
||||
bgEndColor: string; // 文字按钮:背景渐变结束颜色
|
||||
imgUrl: string; // 图片按钮:图片地址
|
||||
text: string; // 文字
|
||||
type: 'img' | 'text'; // 类型:文字 | 图片
|
||||
}; // 按钮
|
||||
borderRadiusTop: number; // 上圆角
|
||||
borderRadiusBottom: number; // 下圆角
|
||||
space: number; // 间距
|
||||
spuIds: number[]; // 商品编号列表
|
||||
style: ComponentStyle; // 组件样式
|
||||
}
|
||||
|
||||
// 定义组件
|
||||
/** 商品字段属性 */
|
||||
export interface ProductCardFieldProperty {
|
||||
show: boolean; // 是否显示
|
||||
color: string; // 颜色
|
||||
}
|
||||
|
||||
/** 定义组件 */
|
||||
export const component = {
|
||||
id: 'ProductCard',
|
||||
name: '商品卡片',
|
||||
|
||||
@@ -13,10 +13,11 @@ import * as ProductSpuApi from '#/api/mall/product/spu';
|
||||
|
||||
/** 商品卡片 */
|
||||
defineOptions({ name: 'ProductCard' });
|
||||
// 定义属性
|
||||
|
||||
const props = defineProps<{ property: ProductCardProperty }>();
|
||||
// 商品列表
|
||||
|
||||
const spuList = ref<MallSpuApi.Spu[]>([]);
|
||||
|
||||
watch(
|
||||
() => props.property.spuIds,
|
||||
async () => {
|
||||
@@ -28,28 +29,21 @@ watch(
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* 计算商品的间距
|
||||
* @param index 商品索引
|
||||
*/
|
||||
const calculateSpace = (index: number) => {
|
||||
// 商品的列数
|
||||
const columns = props.property.layoutType === 'twoCol' ? 2 : 1;
|
||||
// 第一列没有左边距
|
||||
const marginLeft = index % columns === 0 ? '0' : `${props.property.space}px`;
|
||||
// 第一行没有上边距
|
||||
const marginTop = index < columns ? '0' : `${props.property.space}px`;
|
||||
|
||||
/** 计算商品的间距 */
|
||||
function calculateSpace(index: number) {
|
||||
const columns = props.property.layoutType === 'twoCol' ? 2 : 1; // 商品的列数
|
||||
const marginLeft = index % columns === 0 ? '0' : `${props.property.space}px`; // 第一列没有左边距
|
||||
const marginTop = index < columns ? '0' : `${props.property.space}px`; // 第一行没有上边距
|
||||
return { marginLeft, marginTop };
|
||||
};
|
||||
}
|
||||
|
||||
// 容器
|
||||
const containerRef = ref();
|
||||
// 计算商品的宽度
|
||||
|
||||
/** 计算商品的宽度 */
|
||||
const calculateWidth = () => {
|
||||
let width = '100%';
|
||||
// 双列时每列的宽度为:(总宽度 - 间距)/ 2
|
||||
if (props.property.layoutType === 'twoCol') {
|
||||
// 双列时每列的宽度为:(总宽度 - 间距)/ 2
|
||||
width = `${(containerRef.value.offsetWidth - props.property.space) / 2}px`;
|
||||
}
|
||||
return { width };
|
||||
@@ -136,14 +130,14 @@ const calculateWidth = () => {
|
||||
class="text-[16px]"
|
||||
:style="{ color: property.fields.price.color }"
|
||||
>
|
||||
¥{{ fenToYuan(spu.price as any) }}
|
||||
¥{{ fenToYuan(spu.price!) }}
|
||||
</span>
|
||||
<!-- 市场价 -->
|
||||
<span
|
||||
v-if="property.fields.marketPrice.show && spu.marketPrice"
|
||||
class="ml-[4px] text-[10px] line-through"
|
||||
:style="{ color: property.fields.marketPrice.color }"
|
||||
>¥{{ fenToYuan(spu.marketPrice) }}
|
||||
>¥{{ fenToYuan(spu.marketPrice!) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-[12px]">
|
||||
@@ -186,5 +180,3 @@ const calculateWidth = () => {
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
||||
@@ -18,15 +18,18 @@ import {
|
||||
} from 'element-plus';
|
||||
|
||||
import UploadImg from '#/components/upload/image-upload.vue';
|
||||
import { SpuShowcase } from '#/views/mall/product/spu/components';
|
||||
import { ColorInput } from '#/views/mall/promotion/components';
|
||||
// TODO: 添加组件
|
||||
// import SpuShowcase from '#/views/mall/product/spu/components/spu-showcase.vue';
|
||||
|
||||
// 商品卡片属性面板
|
||||
import ComponentContainerProperty from '../../component-container-property.vue';
|
||||
|
||||
/** 商品卡片属性面板 */
|
||||
defineOptions({ name: 'ProductCardProperty' });
|
||||
|
||||
const props = defineProps<{ modelValue: ProductCardProperty }>();
|
||||
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
|
||||
const formData = useVModel(props, 'modelValue', emit);
|
||||
</script>
|
||||
|
||||
@@ -34,7 +37,7 @@ const formData = useVModel(props, 'modelValue', emit);
|
||||
<ComponentContainerProperty v-model="formData.style">
|
||||
<ElForm label-width="80px" :model="formData">
|
||||
<ElCard header="商品列表" class="property-group" shadow="never">
|
||||
<!-- <SpuShowcase v-model="formData.spuIds" /> -->
|
||||
<SpuShowcase v-model="formData.spuIds" />
|
||||
</ElCard>
|
||||
<ElCard header="商品样式" class="property-group" shadow="never">
|
||||
<ElFormItem label="布局" prop="type">
|
||||
@@ -174,5 +177,3 @@ const formData = useVModel(props, 'modelValue', emit);
|
||||
</ElForm>
|
||||
</ComponentContainerProperty>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
||||
@@ -2,42 +2,29 @@ import type { ComponentStyle, DiyComponent } from '../../../util';
|
||||
|
||||
/** 商品栏属性 */
|
||||
export interface ProductListProperty {
|
||||
// 布局类型:双列 | 三列 | 水平滑动
|
||||
layoutType: 'horizSwiper' | 'threeCol' | 'twoCol';
|
||||
// 商品字段
|
||||
layoutType: 'horizSwiper' | 'threeCol' | 'twoCol'; // 布局类型:双列 | 三列 | 水平滑动
|
||||
fields: {
|
||||
// 商品名称
|
||||
name: ProductListFieldProperty;
|
||||
// 商品价格
|
||||
price: ProductListFieldProperty;
|
||||
};
|
||||
// 角标
|
||||
name: ProductListFieldProperty; // 商品名称
|
||||
price: ProductListFieldProperty; // 商品价格
|
||||
}; // 商品字段
|
||||
badge: {
|
||||
// 角标图片
|
||||
imgUrl: string;
|
||||
// 是否显示
|
||||
show: boolean;
|
||||
};
|
||||
// 上圆角
|
||||
borderRadiusTop: number;
|
||||
// 下圆角
|
||||
borderRadiusBottom: number;
|
||||
// 间距
|
||||
space: number;
|
||||
// 商品编号列表
|
||||
spuIds: number[];
|
||||
// 组件样式
|
||||
style: ComponentStyle;
|
||||
}
|
||||
// 商品字段
|
||||
export interface ProductListFieldProperty {
|
||||
// 是否显示
|
||||
show: boolean;
|
||||
// 颜色
|
||||
color: string;
|
||||
imgUrl: string; // 角标图片
|
||||
show: boolean; // 是否显示
|
||||
}; // 角标
|
||||
borderRadiusTop: number; // 上圆角
|
||||
borderRadiusBottom: number; // 下圆角
|
||||
space: number; // 间距
|
||||
spuIds: number[]; // 商品编号列表
|
||||
style: ComponentStyle; // 组件样式
|
||||
}
|
||||
|
||||
// 定义组件
|
||||
/** 商品字段属性 */
|
||||
export interface ProductListFieldProperty {
|
||||
show: boolean; // 是否显示
|
||||
color: string; // 颜色
|
||||
}
|
||||
|
||||
/** 定义组件 */
|
||||
export const component = {
|
||||
id: 'ProductList',
|
||||
name: '商品栏',
|
||||
|
||||
@@ -13,10 +13,11 @@ import * as ProductSpuApi from '#/api/mall/product/spu';
|
||||
|
||||
/** 商品栏 */
|
||||
defineOptions({ name: 'ProductList' });
|
||||
// 定义属性
|
||||
|
||||
const props = defineProps<{ property: ProductListProperty }>();
|
||||
// 商品列表
|
||||
|
||||
const spuList = ref<MallSpuApi.Spu[]>([]);
|
||||
|
||||
watch(
|
||||
() => props.property.spuIds,
|
||||
async () => {
|
||||
@@ -27,19 +28,15 @@ watch(
|
||||
deep: true,
|
||||
},
|
||||
);
|
||||
// 手机宽度
|
||||
const phoneWidth = ref(375);
|
||||
// 容器
|
||||
const containerRef = ref();
|
||||
// 商品的列数
|
||||
const columns = ref(2);
|
||||
// 滚动条宽度
|
||||
const scrollbarWidth = ref('100%');
|
||||
// 商品图大小
|
||||
const imageSize = ref('0');
|
||||
// 商品网络列数
|
||||
const gridTemplateColumns = ref('');
|
||||
// 计算布局参数
|
||||
|
||||
const phoneWidth = ref(375); // 手机宽度
|
||||
const containerRef = ref(); // 容器
|
||||
const columns = ref(2); // 商品的列数
|
||||
const scrollbarWidth = ref('100%'); // 滚动条宽度
|
||||
const imageSize = ref('0'); // 商品图大小
|
||||
const gridTemplateColumns = ref(''); // 商品网络列数
|
||||
|
||||
/** 计算布局参数 */
|
||||
watch(
|
||||
() => [props.property, phoneWidth, spuList.value.length],
|
||||
() => {
|
||||
@@ -69,8 +66,9 @@ watch(
|
||||
},
|
||||
{ immediate: true, deep: true },
|
||||
);
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
// 提取手机宽度
|
||||
phoneWidth.value = containerRef.value?.wrapRef?.offsetWidth || 375;
|
||||
});
|
||||
</script>
|
||||
@@ -146,5 +144,3 @@ onMounted(() => {
|
||||
</div>
|
||||
</ElScrollbar>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { IconifyIcon } from '@vben/icons';
|
||||
import { useVModel } from '@vueuse/core';
|
||||
import {
|
||||
ElCard,
|
||||
ElCheckbox,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElRadioButton,
|
||||
@@ -16,17 +17,18 @@ import {
|
||||
} from 'element-plus';
|
||||
|
||||
import UploadImg from '#/components/upload/image-upload.vue';
|
||||
import { InputWithColor as ColorInput } from '#/views/mall/promotion/components';
|
||||
import { SpuShowcase } from '#/views/mall/product/spu/components';
|
||||
import { ColorInput } from '#/views/mall/promotion/components';
|
||||
|
||||
import ComponentContainerProperty from '../../component-container-property.vue';
|
||||
// TODO: 添加组件
|
||||
// import SpuShowcase from '#/views/mall/product/spu/components/spu-showcase.vue';
|
||||
|
||||
// 商品栏属性面板
|
||||
/** 商品栏属性面板 */
|
||||
defineOptions({ name: 'ProductListProperty' });
|
||||
|
||||
const props = defineProps<{ modelValue: ProductListProperty }>();
|
||||
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
|
||||
const formData = useVModel(props, 'modelValue', emit);
|
||||
</script>
|
||||
|
||||
@@ -34,7 +36,7 @@ const formData = useVModel(props, 'modelValue', emit);
|
||||
<ComponentContainerProperty v-model="formData.style">
|
||||
<ElForm label-width="80px" :model="formData">
|
||||
<ElCard header="商品列表" class="property-group" shadow="never">
|
||||
<!-- <SpuShowcase v-model="formData.spuIds" /> -->
|
||||
<SpuShowcase v-model="formData.spuIds" />
|
||||
</ElCard>
|
||||
<ElCard header="商品样式" class="property-group" shadow="never">
|
||||
<ElFormItem label="布局" prop="type">
|
||||
@@ -119,5 +121,3 @@ const formData = useVModel(props, 'modelValue', emit);
|
||||
</ElForm>
|
||||
</ComponentContainerProperty>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
||||
@@ -2,13 +2,11 @@ import type { ComponentStyle, DiyComponent } from '../../../util';
|
||||
|
||||
/** 营销文章属性 */
|
||||
export interface PromotionArticleProperty {
|
||||
// 文章编号
|
||||
id: number;
|
||||
// 组件样式
|
||||
style: ComponentStyle;
|
||||
id: number; // 文章编号
|
||||
style: ComponentStyle; // 组件样式
|
||||
}
|
||||
|
||||
// 定义组件
|
||||
/** 定义组件 */
|
||||
export const component = {
|
||||
id: 'PromotionArticle',
|
||||
name: '营销文章',
|
||||
|
||||
@@ -9,10 +9,10 @@ import * as ArticleApi from '#/api/mall/promotion/article/index';
|
||||
|
||||
/** 营销文章 */
|
||||
defineOptions({ name: 'PromotionArticle' });
|
||||
// 定义属性
|
||||
const props = defineProps<{ property: PromotionArticleProperty }>();
|
||||
// 商品列表
|
||||
const article = ref<MallArticleApi.Article>();
|
||||
|
||||
const props = defineProps<{ property: PromotionArticleProperty }>(); // 定义属性
|
||||
|
||||
const article = ref<MallArticleApi.Article>(); // 商品列表
|
||||
|
||||
watch(
|
||||
() => props.property.id,
|
||||
@@ -29,5 +29,3 @@ watch(
|
||||
<template>
|
||||
<div class="min-h-[30px]" v-dompurify-html="article?.content"></div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
||||
@@ -12,18 +12,19 @@ import * as ArticleApi from '#/api/mall/promotion/article/index';
|
||||
|
||||
import ComponentContainerProperty from '../../component-container-property.vue';
|
||||
|
||||
// 营销文章属性面板
|
||||
/** 营销文章属性面板 */
|
||||
defineOptions({ name: 'PromotionArticleProperty' });
|
||||
|
||||
const props = defineProps<{ modelValue: PromotionArticleProperty }>();
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
const formData = useVModel(props, 'modelValue', emit);
|
||||
// 文章列表
|
||||
const articles = ref<MallArticleApi.Article[]>([]);
|
||||
|
||||
// 加载中
|
||||
const loading = ref(false);
|
||||
// 查询文章列表
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
|
||||
const formData = useVModel(props, 'modelValue', emit);
|
||||
|
||||
const articles = ref<MallArticleApi.Article[]>([]); // 文章列表
|
||||
const loading = ref(false); // 加载中
|
||||
|
||||
/** 查询文章列表 */
|
||||
const queryArticleList = async (title?: string) => {
|
||||
loading.value = true;
|
||||
const { list } = await ArticleApi.getArticlePage({
|
||||
@@ -35,7 +36,7 @@ const queryArticleList = async (title?: string) => {
|
||||
loading.value = false;
|
||||
};
|
||||
|
||||
// 初始化
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
queryArticleList();
|
||||
});
|
||||
@@ -65,5 +66,3 @@ onMounted(() => {
|
||||
</ElForm>
|
||||
</ComponentContainerProperty>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
||||
@@ -2,64 +2,40 @@ import type { ComponentStyle, DiyComponent } from '../../../util';
|
||||
|
||||
/** 拼团属性 */
|
||||
export interface PromotionCombinationProperty {
|
||||
// 布局类型:单列 | 三列
|
||||
layoutType: 'oneColBigImg' | 'oneColSmallImg' | 'twoCol';
|
||||
// 商品字段
|
||||
layoutType: 'oneColBigImg' | 'oneColSmallImg' | 'twoCol'; // 布局类型:单列 | 三列
|
||||
fields: {
|
||||
// 商品简介
|
||||
introduction: PromotionCombinationFieldProperty;
|
||||
// 市场价
|
||||
marketPrice: PromotionCombinationFieldProperty;
|
||||
// 商品名称
|
||||
name: PromotionCombinationFieldProperty;
|
||||
// 商品价格
|
||||
price: PromotionCombinationFieldProperty;
|
||||
// 商品销量
|
||||
salesCount: PromotionCombinationFieldProperty;
|
||||
// 商品库存
|
||||
stock: PromotionCombinationFieldProperty;
|
||||
};
|
||||
// 角标
|
||||
introduction: PromotionCombinationFieldProperty; // 商品简介
|
||||
marketPrice: PromotionCombinationFieldProperty; // 市场价
|
||||
name: PromotionCombinationFieldProperty; // 商品名称
|
||||
price: PromotionCombinationFieldProperty; // 商品价格
|
||||
salesCount: PromotionCombinationFieldProperty; // 商品销量
|
||||
stock: PromotionCombinationFieldProperty; // 商品库存
|
||||
}; // 商品字段
|
||||
badge: {
|
||||
// 角标图片
|
||||
imgUrl: string;
|
||||
// 是否显示
|
||||
show: boolean;
|
||||
};
|
||||
// 按钮
|
||||
imgUrl: string; // 角标图片
|
||||
show: boolean; // 是否显示
|
||||
}; // 角标
|
||||
btnBuy: {
|
||||
// 文字按钮:背景渐变起始颜色
|
||||
bgBeginColor: string;
|
||||
// 文字按钮:背景渐变结束颜色
|
||||
bgEndColor: string;
|
||||
// 图片按钮:图片地址
|
||||
imgUrl: string;
|
||||
// 文字
|
||||
text: string;
|
||||
// 类型:文字 | 图片
|
||||
type: 'img' | 'text';
|
||||
};
|
||||
// 上圆角
|
||||
borderRadiusTop: number;
|
||||
// 下圆角
|
||||
borderRadiusBottom: number;
|
||||
// 间距
|
||||
space: number;
|
||||
// 拼团活动编号
|
||||
activityIds: number[];
|
||||
// 组件样式
|
||||
style: ComponentStyle;
|
||||
bgBeginColor: string; // 文字按钮:背景渐变起始颜色
|
||||
bgEndColor: string; // 文字按钮:背景渐变结束颜色
|
||||
imgUrl: string; // 图片按钮:图片地址
|
||||
text: string; // 文字
|
||||
type: 'img' | 'text'; // 类型:文字 | 图片
|
||||
}; // 按钮
|
||||
borderRadiusTop: number; // 上圆角
|
||||
borderRadiusBottom: number; // 下圆角
|
||||
space: number; // 间距
|
||||
activityIds: number[]; // 拼团活动编号
|
||||
style: ComponentStyle; // 组件样式
|
||||
}
|
||||
|
||||
// 商品字段
|
||||
/** 商品字段属性 */
|
||||
export interface PromotionCombinationFieldProperty {
|
||||
// 是否显示
|
||||
show: boolean;
|
||||
// 颜色
|
||||
color: string;
|
||||
show: boolean; // 是否显示
|
||||
color: string; // 颜色
|
||||
}
|
||||
|
||||
// 定义组件
|
||||
/** 定义组件 */
|
||||
export const component = {
|
||||
id: 'PromotionCombination',
|
||||
name: '拼团',
|
||||
|
||||
@@ -15,10 +15,10 @@ import * as CombinationActivityApi from '#/api/mall/promotion/combination/combin
|
||||
|
||||
/** 拼团卡片 */
|
||||
defineOptions({ name: 'PromotionCombination' });
|
||||
// 定义属性
|
||||
|
||||
const props = defineProps<{ property: PromotionCombinationProperty }>();
|
||||
// 商品列表
|
||||
const spuList = ref<MallSpuApi.Spu[]>([]);
|
||||
|
||||
const spuList = ref<MallSpuApi.Spu[]>([]); // 商品列表
|
||||
const spuIdList = ref<number[]>([]);
|
||||
const combinationActivityList = ref<
|
||||
MallCombinationActivityApi.CombinationActivity[]
|
||||
@@ -30,7 +30,7 @@ watch(
|
||||
try {
|
||||
// 新添加的拼团组件,是没有活动ID的
|
||||
const activityIds = props.property.activityIds;
|
||||
// 检查活动ID的有效性
|
||||
// 检查活动 ID 的有效性
|
||||
if (Array.isArray(activityIds) && activityIds.length > 0) {
|
||||
// 获取拼团活动详情列表
|
||||
combinationActivityList.value =
|
||||
@@ -70,32 +70,25 @@ watch(
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* 计算商品的间距
|
||||
* @param index 商品索引
|
||||
*/
|
||||
const calculateSpace = (index: number) => {
|
||||
// 商品的列数
|
||||
const columns = props.property.layoutType === 'twoCol' ? 2 : 1;
|
||||
// 第一列没有左边距
|
||||
const marginLeft = index % columns === 0 ? '0' : `${props.property.space}px`;
|
||||
// 第一行没有上边距
|
||||
const marginTop = index < columns ? '0' : `${props.property.space}px`;
|
||||
|
||||
/** 计算商品的间距 */
|
||||
function calculateSpace(index: number) {
|
||||
const columns = props.property.layoutType === 'twoCol' ? 2 : 1; // 商品的列数
|
||||
const marginLeft = index % columns === 0 ? '0' : `${props.property.space}px`; // 第一列没有左边距
|
||||
const marginTop = index < columns ? '0' : `${props.property.space}px`; // 第一行没有上边距
|
||||
return { marginLeft, marginTop };
|
||||
};
|
||||
}
|
||||
|
||||
// 容器
|
||||
const containerRef = ref();
|
||||
// 计算商品的宽度
|
||||
const calculateWidth = () => {
|
||||
|
||||
/** 计算商品的宽度 */
|
||||
function calculateWidth() {
|
||||
let width = '100%';
|
||||
// 双列时每列的宽度为:(总宽度 - 间距)/ 2
|
||||
if (props.property.layoutType === 'twoCol') {
|
||||
// 双列时每列的宽度为:(总宽度 - 间距)/ 2
|
||||
width = `${(containerRef.value.offsetWidth - props.property.space) / 2}px`;
|
||||
}
|
||||
return { width };
|
||||
};
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div
|
||||
@@ -117,7 +110,7 @@ const calculateWidth = () => {
|
||||
>
|
||||
<!-- 角标 -->
|
||||
<div
|
||||
v-if="property.badge.show"
|
||||
v-if="property.badge.show && property.badge.imgUrl"
|
||||
class="absolute left-0 top-0 z-[1] items-center justify-center"
|
||||
>
|
||||
<ElImage
|
||||
@@ -139,7 +132,7 @@ const calculateWidth = () => {
|
||||
<ElImage fit="cover" class="h-full w-full" :src="spu.picUrl" />
|
||||
</div>
|
||||
<div
|
||||
class="box-border flex flex-col gap-2 p-2"
|
||||
class="box-border flex flex-col gap-[8px] p-[8px]"
|
||||
:class="[
|
||||
{
|
||||
'w-full': property.layoutType !== 'oneColSmallImg',
|
||||
@@ -186,7 +179,7 @@ const calculateWidth = () => {
|
||||
class="ml-[4px] text-[10px] line-through"
|
||||
:style="{ color: property.fields.marketPrice.color }"
|
||||
>
|
||||
¥{{ fenToYuan(spu.marketPrice) }}
|
||||
¥{{ fenToYuan(spu.marketPrice!) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-[12px]">
|
||||
@@ -229,5 +222,3 @@ const calculateWidth = () => {
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import type { PromotionCombinationProperty } from './config';
|
||||
|
||||
import type { MallCombinationActivityApi } from '#/api/mall/promotion/combination/combinationActivity';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { CommonStatusEnum } from '@vben/constants';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
|
||||
import { useVModel } from '@vueuse/core';
|
||||
import {
|
||||
ElCard,
|
||||
ElCheckbox,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElRadioButton,
|
||||
ElRadioGroup,
|
||||
ElSlider,
|
||||
@@ -19,27 +17,20 @@ import {
|
||||
ElTooltip,
|
||||
} from 'element-plus';
|
||||
|
||||
import * as CombinationActivityApi from '#/api/mall/promotion/combination/combinationActivity';
|
||||
import UploadImg from '#/components/upload/image-upload.vue';
|
||||
import CombinationShowcase from '#/views/mall/promotion/combination/components/combination-showcase.vue';
|
||||
import { CombinationShowcase } from '#/views/mall/promotion/combination/components';
|
||||
import { ColorInput } from '#/views/mall/promotion/components';
|
||||
|
||||
// 拼团属性面板
|
||||
import ComponentContainerProperty from '../../component-container-property.vue';
|
||||
|
||||
/** 拼团属性面板 */
|
||||
defineOptions({ name: 'PromotionCombinationProperty' });
|
||||
|
||||
const props = defineProps<{ modelValue: PromotionCombinationProperty }>();
|
||||
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
|
||||
const formData = useVModel(props, 'modelValue', emit);
|
||||
// 活动列表
|
||||
const activityList = ref<MallCombinationActivityApi.CombinationActivity[]>([]);
|
||||
onMounted(async () => {
|
||||
const { list } = await CombinationActivityApi.getCombinationActivityPage({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
status: CommonStatusEnum.ENABLE,
|
||||
});
|
||||
activityList.value = list;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -66,8 +57,8 @@ onMounted(async () => {
|
||||
<IconifyIcon icon="fluent:text-column-two-24-filled" />
|
||||
</ElRadioButton>
|
||||
</ElTooltip>
|
||||
<!--<el-tooltip class="item" content="三列" placement="bottom">
|
||||
<el-radio-button value="threeCol">
|
||||
<!--<ElTooltip class="item" content="三列" placement="bottom">
|
||||
<ElRadioButton value="threeCol">
|
||||
<IconifyIcon icon="fluent:text-column-three-24-filled" />
|
||||
</ElRadioButton>
|
||||
</ElTooltip>-->
|
||||
@@ -115,8 +106,13 @@ onMounted(async () => {
|
||||
<ElSwitch v-model="formData.badge.show" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="角标" prop="badge.imgUrl" v-if="formData.badge.show">
|
||||
<UploadImg v-model="formData.badge.imgUrl" height="44px" width="72px">
|
||||
<template #tip> 建议尺寸:36 * 22</template>
|
||||
<UploadImg
|
||||
v-model="formData.badge.imgUrl"
|
||||
height="44px"
|
||||
width="72px"
|
||||
:show-description="false"
|
||||
>
|
||||
<template #tip> 建议尺寸:36 * 22 </template>
|
||||
</UploadImg>
|
||||
</ElFormItem>
|
||||
</ElCard>
|
||||
@@ -146,7 +142,7 @@ onMounted(async () => {
|
||||
width="56px"
|
||||
:show-description="false"
|
||||
>
|
||||
<template #tip> 建议尺寸:56 * 56</template>
|
||||
<template #tip> 建议尺寸:56 * 56 </template>
|
||||
</UploadImg>
|
||||
</ElFormItem>
|
||||
</template>
|
||||
@@ -186,5 +182,3 @@ onMounted(async () => {
|
||||
</ElForm>
|
||||
</ComponentContainerProperty>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
||||
@@ -2,64 +2,41 @@ import type { ComponentStyle, DiyComponent } from '../../../util';
|
||||
|
||||
/** 积分商城属性 */
|
||||
export interface PromotionPointProperty {
|
||||
// 布局类型:单列 | 三列
|
||||
layoutType: 'oneColBigImg' | 'oneColSmallImg' | 'twoCol';
|
||||
// 商品字段
|
||||
layoutType: 'oneColBigImg' | 'oneColSmallImg' | 'twoCol'; // 布局类型:单列 | 三列
|
||||
fields: {
|
||||
// 商品简介
|
||||
introduction: PromotionPointFieldProperty;
|
||||
// 市场价
|
||||
marketPrice: PromotionPointFieldProperty;
|
||||
// 商品名称
|
||||
name: PromotionPointFieldProperty;
|
||||
// 商品价格
|
||||
price: PromotionPointFieldProperty;
|
||||
// 商品销量
|
||||
salesCount: PromotionPointFieldProperty;
|
||||
// 商品库存
|
||||
stock: PromotionPointFieldProperty;
|
||||
};
|
||||
// 角标
|
||||
introduction: PromotionPointFieldProperty; // 商品简介
|
||||
marketPrice: PromotionPointFieldProperty; // 市场价
|
||||
name: PromotionPointFieldProperty; // 商品名称
|
||||
price: PromotionPointFieldProperty; // 商品价格
|
||||
salesCount: PromotionPointFieldProperty; // 商品销量
|
||||
stock: PromotionPointFieldProperty; // 商品库存
|
||||
}; // 商品字段
|
||||
badge: {
|
||||
// 角标图片
|
||||
imgUrl: string;
|
||||
// 是否显示
|
||||
show: boolean;
|
||||
};
|
||||
imgUrl: string; // 角标图片
|
||||
show: boolean; // 是否显示
|
||||
}; // 角标
|
||||
// 按钮
|
||||
btnBuy: {
|
||||
// 文字按钮:背景渐变起始颜色
|
||||
bgBeginColor: string;
|
||||
// 文字按钮:背景渐变结束颜色
|
||||
bgEndColor: string;
|
||||
// 图片按钮:图片地址
|
||||
imgUrl: string;
|
||||
// 文字
|
||||
text: string;
|
||||
// 类型:文字 | 图片
|
||||
type: 'img' | 'text';
|
||||
};
|
||||
// 上圆角
|
||||
borderRadiusTop: number;
|
||||
// 下圆角
|
||||
borderRadiusBottom: number;
|
||||
// 间距
|
||||
space: number;
|
||||
// 秒杀活动编号
|
||||
activityIds: number[];
|
||||
// 组件样式
|
||||
style: ComponentStyle;
|
||||
bgBeginColor: string; // 文字按钮:背景渐变起始颜色
|
||||
bgEndColor: string; // 文字按钮:背景渐变结束颜色
|
||||
imgUrl: string; // 图片按钮:图片地址
|
||||
text: string; // 文字
|
||||
type: 'img' | 'text'; // 类型:文字 | 图片
|
||||
}; // 按钮
|
||||
borderRadiusTop: number; // 上圆角
|
||||
borderRadiusBottom: number; // 下圆角
|
||||
space: number; // 间距
|
||||
activityIds: number[]; // 积分活动编号
|
||||
style: ComponentStyle; // 组件样式
|
||||
}
|
||||
|
||||
// 商品字段
|
||||
/** 商品字段属性 */
|
||||
export interface PromotionPointFieldProperty {
|
||||
// 是否显示
|
||||
show: boolean;
|
||||
// 颜色
|
||||
color: string;
|
||||
show: boolean; // 是否显示
|
||||
color: string; // 颜色
|
||||
}
|
||||
|
||||
// 定义组件
|
||||
/** 定义组件 */
|
||||
export const component = {
|
||||
id: 'PromotionPoint',
|
||||
name: '积分商城',
|
||||
|
||||
@@ -14,10 +14,10 @@ import * as PointActivityApi from '#/api/mall/promotion/point';
|
||||
|
||||
/** 积分商城卡片 */
|
||||
defineOptions({ name: 'PromotionPoint' });
|
||||
// 定义属性
|
||||
|
||||
const props = defineProps<{ property: PromotionPointProperty }>();
|
||||
// 商品列表
|
||||
const spuList = ref<MallPointActivityApi.SpuExtensionWithPoint[]>([]);
|
||||
|
||||
const spuList = ref<MallPointActivityApi.SpuExtensionWithPoint[]>([]); // 商品列表
|
||||
const spuIdList = ref<number[]>([]);
|
||||
const pointActivityList = ref<MallPointActivityApi.PointActivity[]>([]);
|
||||
|
||||
@@ -27,7 +27,7 @@ watch(
|
||||
try {
|
||||
// 新添加的积分商城组件,是没有活动ID的
|
||||
const activityIds = props.property.activityIds;
|
||||
// 检查活动ID的有效性
|
||||
// 检查活动 ID 的有效性
|
||||
if (Array.isArray(activityIds) && activityIds.length > 0) {
|
||||
// 获取积分商城活动详情列表
|
||||
pointActivityList.value =
|
||||
@@ -66,41 +66,33 @@ watch(
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* 计算商品的间距
|
||||
* @param index 商品索引
|
||||
*/
|
||||
const calculateSpace = (index: number) => {
|
||||
// 商品的列数
|
||||
const columns = props.property.layoutType === 'twoCol' ? 2 : 1;
|
||||
// 第一列没有左边距
|
||||
const marginLeft = index % columns === 0 ? '0' : `${props.property.space}px`;
|
||||
// 第一行没有上边距
|
||||
const marginTop = index < columns ? '0' : `${props.property.space}px`;
|
||||
|
||||
/** 计算商品的间距 */
|
||||
function calculateSpace(index: number) {
|
||||
const columns = props.property.layoutType === 'twoCol' ? 2 : 1; // 商品的列数
|
||||
const marginLeft = index % columns === 0 ? '0' : `${props.property.space}px`; // 第一列没有左边距
|
||||
const marginTop = index < columns ? '0' : `${props.property.space}px`; // 第一行没有上边距
|
||||
return { marginLeft, marginTop };
|
||||
};
|
||||
}
|
||||
|
||||
// 容器
|
||||
const containerRef = ref();
|
||||
// 计算商品的宽度
|
||||
const calculateWidth = () => {
|
||||
|
||||
/** 计算商品的宽度 */
|
||||
function calculateWidth() {
|
||||
let width = '100%';
|
||||
// 双列时每列的宽度为:(总宽度 - 间距)/ 2
|
||||
if (props.property.layoutType === 'twoCol') {
|
||||
// 双列时每列的宽度为:(总宽度 - 间距)/ 2
|
||||
width = `${(containerRef.value.offsetWidth - props.property.space) / 2}px`;
|
||||
}
|
||||
return { width };
|
||||
};
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div
|
||||
ref="containerRef"
|
||||
class="box-content flex min-h-[30px] w-full flex-row flex-wrap"
|
||||
ref="containerRef"
|
||||
>
|
||||
<div
|
||||
v-for="(spu, index) in spuList"
|
||||
:key="index"
|
||||
class="relative box-content flex flex-row flex-wrap overflow-hidden bg-white"
|
||||
:style="{
|
||||
...calculateSpace(index),
|
||||
...calculateWidth(),
|
||||
@@ -109,17 +101,18 @@ const calculateWidth = () => {
|
||||
borderBottomLeftRadius: `${property.borderRadiusBottom}px`,
|
||||
borderBottomRightRadius: `${property.borderRadiusBottom}px`,
|
||||
}"
|
||||
class="relative box-content flex flex-row flex-wrap overflow-hidden bg-white"
|
||||
v-for="(spu, index) in spuList"
|
||||
:key="index"
|
||||
>
|
||||
<!-- 角标 -->
|
||||
<div
|
||||
v-if="property.badge.show"
|
||||
v-if="property.badge.show && property.badge.imgUrl"
|
||||
class="absolute left-0 top-0 z-[1] items-center justify-center"
|
||||
>
|
||||
<ElImage
|
||||
fit="cover"
|
||||
:src="property.badge.imgUrl"
|
||||
class="h-[26px] w-[38px]"
|
||||
fit="cover"
|
||||
/>
|
||||
</div>
|
||||
<!-- 商品封面图 -->
|
||||
@@ -132,10 +125,10 @@ const calculateWidth = () => {
|
||||
},
|
||||
]"
|
||||
>
|
||||
<ElImage :src="spu.picUrl" class="h-full w-full" fit="cover" />
|
||||
<ElImage fit="cover" class="h-full w-full" :src="spu.picUrl" />
|
||||
</div>
|
||||
<div
|
||||
class="box-border flex flex-col gap-2 p-2"
|
||||
class="box-border flex flex-col gap-[8px] p-[8px]"
|
||||
:class="[
|
||||
{
|
||||
'w-full': property.layoutType !== 'oneColSmallImg',
|
||||
@@ -162,8 +155,8 @@ const calculateWidth = () => {
|
||||
<!-- 商品简介 -->
|
||||
<div
|
||||
v-if="property.fields.introduction.show"
|
||||
:style="{ color: property.fields.introduction.color }"
|
||||
class="truncate text-[12px]"
|
||||
:style="{ color: property.fields.introduction.color }"
|
||||
>
|
||||
{{ spu.introduction }}
|
||||
</div>
|
||||
@@ -171,8 +164,8 @@ const calculateWidth = () => {
|
||||
<!-- 积分 -->
|
||||
<span
|
||||
v-if="property.fields.price.show"
|
||||
:style="{ color: property.fields.price.color }"
|
||||
class="text-[16px]"
|
||||
:style="{ color: property.fields.price.color }"
|
||||
>
|
||||
{{ spu.point }}积分
|
||||
{{
|
||||
@@ -184,10 +177,10 @@ const calculateWidth = () => {
|
||||
<!-- 市场价 -->
|
||||
<span
|
||||
v-if="property.fields.marketPrice.show && spu.marketPrice"
|
||||
:style="{ color: property.fields.marketPrice.color }"
|
||||
class="ml-[4px] text-[10px] line-through"
|
||||
:style="{ color: property.fields.marketPrice.color }"
|
||||
>
|
||||
¥{{ fenToYuan(spu.marketPrice) }}
|
||||
¥{{ fenToYuan(spu.marketPrice!) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-[12px]">
|
||||
@@ -212,23 +205,21 @@ const calculateWidth = () => {
|
||||
<!-- 文字按钮 -->
|
||||
<span
|
||||
v-if="property.btnBuy.type === 'text'"
|
||||
class="rounded-full px-[12px] py-[4px] text-[12px] text-white"
|
||||
:style="{
|
||||
background: `linear-gradient(to right, ${property.btnBuy.bgBeginColor}, ${property.btnBuy.bgEndColor}`,
|
||||
}"
|
||||
class="rounded-full px-[12px] py-[4px] text-[12px] text-white"
|
||||
>
|
||||
{{ property.btnBuy.text }}
|
||||
</span>
|
||||
<!-- 图片按钮 -->
|
||||
<ElImage
|
||||
v-else
|
||||
:src="property.btnBuy.imgUrl"
|
||||
class="h-[28px] w-[28px] rounded-full"
|
||||
fit="cover"
|
||||
:src="property.btnBuy.imgUrl"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
||||
@@ -16,10 +16,13 @@ import {
|
||||
ElTooltip,
|
||||
} from 'element-plus';
|
||||
|
||||
import UploadImg from '#/components/upload/image-upload.vue';
|
||||
import { ColorInput } from '#/views/mall/promotion/components';
|
||||
import PointShowcase from '#/views/mall/promotion/point/components/point-showcase.vue';
|
||||
import { PointShowcase } from '#/views/mall/promotion/point/components';
|
||||
|
||||
// 秒杀属性面板
|
||||
import ComponentContainerProperty from '../../component-container-property.vue';
|
||||
|
||||
/** 积分属性面板 */
|
||||
defineOptions({ name: 'PromotionPointProperty' });
|
||||
|
||||
const props = defineProps<{ modelValue: PromotionPointProperty }>();
|
||||
@@ -29,11 +32,11 @@ const formData = useVModel(props, 'modelValue', emit);
|
||||
|
||||
<template>
|
||||
<ComponentContainerProperty v-model="formData.style">
|
||||
<ElForm :model="formData" label-width="80px">
|
||||
<ElCard class="property-group" header="积分商城活动" shadow="never">
|
||||
<ElForm label-width="80px" :model="formData">
|
||||
<ElCard header="积分商城活动" class="property-group" shadow="never">
|
||||
<PointShowcase v-model="formData.activityIds" />
|
||||
</ElCard>
|
||||
<ElCard class="property-group" header="商品样式" shadow="never">
|
||||
<ElCard header="商品样式" class="property-group" shadow="never">
|
||||
<ElFormItem label="布局" prop="type">
|
||||
<ElRadioGroup v-model="formData.layoutType">
|
||||
<ElTooltip class="item" content="单列大图" placement="bottom">
|
||||
@@ -51,11 +54,11 @@ const formData = useVModel(props, 'modelValue', emit);
|
||||
<IconifyIcon icon="fluent:text-column-two-24-filled" />
|
||||
</ElRadioButton>
|
||||
</ElTooltip>
|
||||
<!--<el-tooltip class="item" content="三列" placement="bottom">
|
||||
<el-radio-button value="threeCol">
|
||||
<!--<ElTooltip class="item" content="三列" placement="bottom">
|
||||
<ElRadioButton value="threeCol">
|
||||
<IconifyIcon icon="fluent:text-column-three-24-filled" />
|
||||
</el-radio-button>
|
||||
</el-tooltip>-->
|
||||
</ElRadioButton>
|
||||
</ElTooltip>-->
|
||||
</ElRadioGroup>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="商品名称" prop="fields.name.show">
|
||||
@@ -95,22 +98,22 @@ const formData = useVModel(props, 'modelValue', emit);
|
||||
</div>
|
||||
</ElFormItem>
|
||||
</ElCard>
|
||||
<ElCard class="property-group" header="角标" shadow="never">
|
||||
<ElCard header="角标" class="property-group" shadow="never">
|
||||
<ElFormItem label="角标" prop="badge.show">
|
||||
<ElSwitch v-model="formData.badge.show" />
|
||||
</ElFormItem>
|
||||
<ElFormItem v-if="formData.badge.show" label="角标" prop="badge.imgUrl">
|
||||
<ElFormItem label="角标" prop="badge.imgUrl" v-if="formData.badge.show">
|
||||
<UploadImg
|
||||
v-model="formData.badge.imgUrl"
|
||||
height="44px"
|
||||
width="72px"
|
||||
:show-description="false"
|
||||
>
|
||||
<template #tip> 建议尺寸:36 * 22</template>
|
||||
<template #tip> 建议尺寸:36 * 22 </template>
|
||||
</UploadImg>
|
||||
</ElFormItem>
|
||||
</ElCard>
|
||||
<ElCard class="property-group" header="按钮" shadow="never">
|
||||
<ElCard header="按钮" class="property-group" shadow="never">
|
||||
<ElFormItem label="按钮类型" prop="btnBuy.type">
|
||||
<ElRadioGroup v-model="formData.btnBuy.type">
|
||||
<ElRadioButton value="text">文字</ElRadioButton>
|
||||
@@ -136,20 +139,20 @@ const formData = useVModel(props, 'modelValue', emit);
|
||||
width="56px"
|
||||
:show-description="false"
|
||||
>
|
||||
<template #tip> 建议尺寸:56 * 56</template>
|
||||
<template #tip> 建议尺寸:56 * 56 </template>
|
||||
</UploadImg>
|
||||
</ElFormItem>
|
||||
</template>
|
||||
</ElCard>
|
||||
<ElCard class="property-group" header="商品样式" shadow="never">
|
||||
<ElCard header="商品样式" class="property-group" shadow="never">
|
||||
<ElFormItem label="上圆角" prop="borderRadiusTop">
|
||||
<ElSlider
|
||||
v-model="formData.borderRadiusTop"
|
||||
:max="100"
|
||||
:min="0"
|
||||
:show-input-controls="false"
|
||||
input-size="small"
|
||||
show-input
|
||||
input-size="small"
|
||||
:show-input-controls="false"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="下圆角" prop="borderRadiusBottom">
|
||||
@@ -157,9 +160,9 @@ const formData = useVModel(props, 'modelValue', emit);
|
||||
v-model="formData.borderRadiusBottom"
|
||||
:max="100"
|
||||
:min="0"
|
||||
:show-input-controls="false"
|
||||
input-size="small"
|
||||
show-input
|
||||
input-size="small"
|
||||
:show-input-controls="false"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="间隔" prop="space">
|
||||
@@ -167,14 +170,12 @@ const formData = useVModel(props, 'modelValue', emit);
|
||||
v-model="formData.space"
|
||||
:max="100"
|
||||
:min="0"
|
||||
:show-input-controls="false"
|
||||
input-size="small"
|
||||
show-input
|
||||
input-size="small"
|
||||
:show-input-controls="false"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElCard>
|
||||
</ElForm>
|
||||
</ComponentContainerProperty>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
||||
@@ -2,64 +2,40 @@ import type { ComponentStyle, DiyComponent } from '../../../util';
|
||||
|
||||
/** 秒杀属性 */
|
||||
export interface PromotionSeckillProperty {
|
||||
// 布局类型:单列 | 三列
|
||||
layoutType: 'oneColBigImg' | 'oneColSmallImg' | 'twoCol';
|
||||
// 商品字段
|
||||
layoutType: 'oneColBigImg' | 'oneColSmallImg' | 'twoCol'; // 布局类型:单列 | 三列
|
||||
fields: {
|
||||
// 商品简介
|
||||
introduction: PromotionSeckillFieldProperty;
|
||||
// 市场价
|
||||
marketPrice: PromotionSeckillFieldProperty;
|
||||
// 商品名称
|
||||
name: PromotionSeckillFieldProperty;
|
||||
// 商品价格
|
||||
price: PromotionSeckillFieldProperty;
|
||||
// 商品销量
|
||||
salesCount: PromotionSeckillFieldProperty;
|
||||
// 商品库存
|
||||
stock: PromotionSeckillFieldProperty;
|
||||
};
|
||||
// 角标
|
||||
introduction: PromotionSeckillFieldProperty; // 商品简介
|
||||
marketPrice: PromotionSeckillFieldProperty; // 市场价
|
||||
name: PromotionSeckillFieldProperty; // 商品名称
|
||||
price: PromotionSeckillFieldProperty; // 商品价格
|
||||
salesCount: PromotionSeckillFieldProperty; // 商品销量
|
||||
stock: PromotionSeckillFieldProperty; // 商品库存
|
||||
}; // 商品字段
|
||||
badge: {
|
||||
// 角标图片
|
||||
imgUrl: string;
|
||||
// 是否显示
|
||||
show: boolean;
|
||||
};
|
||||
// 按钮
|
||||
imgUrl: string; // 角标图片
|
||||
show: boolean; // 是否显示
|
||||
}; // 角标
|
||||
btnBuy: {
|
||||
// 文字按钮:背景渐变起始颜色
|
||||
bgBeginColor: string;
|
||||
// 文字按钮:背景渐变结束颜色
|
||||
bgEndColor: string;
|
||||
// 图片按钮:图片地址
|
||||
imgUrl: string;
|
||||
// 文字
|
||||
text: string;
|
||||
// 类型:文字 | 图片
|
||||
type: 'img' | 'text';
|
||||
};
|
||||
// 上圆角
|
||||
borderRadiusTop: number;
|
||||
// 下圆角
|
||||
borderRadiusBottom: number;
|
||||
// 间距
|
||||
space: number;
|
||||
// 秒杀活动编号
|
||||
activityIds: number[];
|
||||
// 组件样式
|
||||
style: ComponentStyle;
|
||||
bgBeginColor: string; // 文字按钮:背景渐变起始颜色
|
||||
bgEndColor: string; // 文字按钮:背景渐变结束颜色
|
||||
imgUrl: string; // 图片按钮:图片地址
|
||||
text: string; // 文字
|
||||
type: 'img' | 'text'; // 类型:文字 | 图片
|
||||
}; // 按钮
|
||||
borderRadiusTop: number; // 上圆角
|
||||
borderRadiusBottom: number; // 下圆角
|
||||
space: number; // 间距
|
||||
activityIds: number[]; // 秒杀活动编号
|
||||
style: ComponentStyle; // 组件样式
|
||||
}
|
||||
|
||||
// 商品字段
|
||||
/** 商品字段属性 */
|
||||
export interface PromotionSeckillFieldProperty {
|
||||
// 是否显示
|
||||
show: boolean;
|
||||
// 颜色
|
||||
color: string;
|
||||
show: boolean; // 是否显示
|
||||
color: string; // 颜色
|
||||
}
|
||||
|
||||
// 定义组件
|
||||
/** 定义组件 */
|
||||
export const component = {
|
||||
id: 'PromotionSeckill',
|
||||
name: '秒杀',
|
||||
|
||||
@@ -15,10 +15,9 @@ import * as SeckillActivityApi from '#/api/mall/promotion/seckill/seckillActivit
|
||||
|
||||
/** 秒杀卡片 */
|
||||
defineOptions({ name: 'PromotionSeckill' });
|
||||
// 定义属性
|
||||
const props = defineProps<{ property: PromotionSeckillProperty }>();
|
||||
// 商品列表
|
||||
const spuList = ref<MallSpuApi.Spu[]>([]);
|
||||
|
||||
const spuList = ref<MallSpuApi.Spu[]>([]); // 商品列表
|
||||
const spuIdList = ref<number[]>([]);
|
||||
const seckillActivityList = ref<MallSeckillActivityApi.SeckillActivity[]>([]);
|
||||
|
||||
@@ -28,7 +27,7 @@ watch(
|
||||
try {
|
||||
// 新添加的秒杀组件,是没有活动ID的
|
||||
const activityIds = props.property.activityIds;
|
||||
// 检查活动ID的有效性
|
||||
// 检查活动 ID 的有效性
|
||||
if (Array.isArray(activityIds) && activityIds.length > 0) {
|
||||
// 获取秒杀活动详情列表
|
||||
seckillActivityList.value =
|
||||
@@ -66,32 +65,25 @@ watch(
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* 计算商品的间距
|
||||
* @param index 商品索引
|
||||
*/
|
||||
const calculateSpace = (index: number) => {
|
||||
// 商品的列数
|
||||
const columns = props.property.layoutType === 'twoCol' ? 2 : 1;
|
||||
// 第一列没有左边距
|
||||
const marginLeft = index % columns === 0 ? '0' : `${props.property.space}px`;
|
||||
// 第一行没有上边距
|
||||
const marginTop = index < columns ? '0' : `${props.property.space}px`;
|
||||
|
||||
/** 计算商品的间距 */
|
||||
function calculateSpace(index: number) {
|
||||
const columns = props.property.layoutType === 'twoCol' ? 2 : 1; // 商品的列数
|
||||
const marginLeft = index % columns === 0 ? '0' : `${props.property.space}px`; // 第一列没有左边距
|
||||
const marginTop = index < columns ? '0' : `${props.property.space}px`; // 第一行没有上边距
|
||||
return { marginLeft, marginTop };
|
||||
};
|
||||
}
|
||||
|
||||
// 容器
|
||||
const containerRef = ref();
|
||||
// 计算商品的宽度
|
||||
const calculateWidth = () => {
|
||||
|
||||
/** 计算商品的宽度 */
|
||||
function calculateWidth() {
|
||||
let width = '100%';
|
||||
// 双列时每列的宽度为:(总宽度 - 间距)/ 2
|
||||
if (props.property.layoutType === 'twoCol') {
|
||||
// 双列时每列的宽度为:(总宽度 - 间距)/ 2
|
||||
width = `${(containerRef.value.offsetWidth - props.property.space) / 2}px`;
|
||||
}
|
||||
return { width };
|
||||
};
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div
|
||||
@@ -113,7 +105,7 @@ const calculateWidth = () => {
|
||||
>
|
||||
<!-- 角标 -->
|
||||
<div
|
||||
v-if="property.badge.show"
|
||||
v-if="property.badge.show && property.badge.imgUrl"
|
||||
class="absolute left-0 top-0 z-[1] items-center justify-center"
|
||||
>
|
||||
<ElImage
|
||||
@@ -135,7 +127,7 @@ const calculateWidth = () => {
|
||||
<ElImage fit="cover" class="h-full w-full" :src="spu.picUrl" />
|
||||
</div>
|
||||
<div
|
||||
class="box-border flex flex-col gap-2 p-2"
|
||||
class="box-border flex flex-col gap-[8px] p-[8px]"
|
||||
:class="[
|
||||
{
|
||||
'w-full': property.layoutType !== 'oneColSmallImg',
|
||||
@@ -147,7 +139,7 @@ const calculateWidth = () => {
|
||||
<!-- 商品名称 -->
|
||||
<div
|
||||
v-if="property.fields.name.show"
|
||||
class="text-sm"
|
||||
class="text-[14px]"
|
||||
:class="[
|
||||
{
|
||||
truncate: property.layoutType !== 'oneColSmallImg',
|
||||
@@ -162,7 +154,7 @@ const calculateWidth = () => {
|
||||
<!-- 商品简介 -->
|
||||
<div
|
||||
v-if="property.fields.introduction.show"
|
||||
class="truncate text-xs"
|
||||
class="truncate text-[12px]"
|
||||
:style="{ color: property.fields.introduction.color }"
|
||||
>
|
||||
{{ spu.introduction }}
|
||||
@@ -171,7 +163,7 @@ const calculateWidth = () => {
|
||||
<!-- 价格 -->
|
||||
<span
|
||||
v-if="property.fields.price.show"
|
||||
class="text-base"
|
||||
class="text-[16px]"
|
||||
:style="{ color: property.fields.price.color }"
|
||||
>
|
||||
¥{{ fenToYuan(spu.price || Infinity) }}
|
||||
@@ -179,13 +171,13 @@ const calculateWidth = () => {
|
||||
<!-- 市场价 -->
|
||||
<span
|
||||
v-if="property.fields.marketPrice.show && spu.marketPrice"
|
||||
class="ml-1 text-[10px] line-through"
|
||||
class="ml-[4px] text-[10px] line-through"
|
||||
:style="{ color: property.fields.marketPrice.color }"
|
||||
>
|
||||
¥{{ fenToYuan(spu.marketPrice) }}
|
||||
¥{{ fenToYuan(spu.marketPrice!) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-xs">
|
||||
<div class="text-[12px]">
|
||||
<!-- 销量 -->
|
||||
<span
|
||||
v-if="property.fields.salesCount.show"
|
||||
@@ -203,11 +195,11 @@ const calculateWidth = () => {
|
||||
</div>
|
||||
</div>
|
||||
<!-- 购买按钮 -->
|
||||
<div class="absolute bottom-2 right-2">
|
||||
<div class="absolute bottom-[8px] right-[8px]">
|
||||
<!-- 文字按钮 -->
|
||||
<span
|
||||
v-if="property.btnBuy.type === 'text'"
|
||||
class="rounded-full px-3 py-1 text-xs text-white"
|
||||
class="rounded-full px-[12px] py-[4px] text-[12px] text-white"
|
||||
:style="{
|
||||
background: `linear-gradient(to right, ${property.btnBuy.bgBeginColor}, ${property.btnBuy.bgEndColor}`,
|
||||
}"
|
||||
@@ -217,7 +209,7 @@ const calculateWidth = () => {
|
||||
<!-- 图片按钮 -->
|
||||
<ElImage
|
||||
v-else
|
||||
class="h-7 w-7 rounded-full"
|
||||
class="h-[28px] w-[28px] rounded-full"
|
||||
fit="cover"
|
||||
:src="property.btnBuy.imgUrl"
|
||||
/>
|
||||
@@ -225,5 +217,3 @@ const calculateWidth = () => {
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import type { PromotionSeckillProperty } from './config';
|
||||
|
||||
import type { MallSeckillActivityApi } from '#/api/mall/promotion/seckill/seckillActivity';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { CommonStatusEnum } from '@vben/constants';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
|
||||
import { useVModel } from '@vueuse/core';
|
||||
@@ -19,29 +14,23 @@ import {
|
||||
ElRadioGroup,
|
||||
ElSlider,
|
||||
ElSwitch,
|
||||
ElTooltip,
|
||||
} from 'element-plus';
|
||||
|
||||
import * as SeckillActivityApi from '#/api/mall/promotion/seckill/seckillActivity';
|
||||
import UploadImg from '#/components/upload/image-upload.vue';
|
||||
import { ColorInput } from '#/views/mall/promotion/components';
|
||||
import SeckillShowcase from '#/views/mall/promotion/seckill/components/seckill-showcase.vue';
|
||||
import { SeckillShowcase } from '#/views/mall/promotion/seckill/components';
|
||||
|
||||
// 秒杀属性面板
|
||||
import ComponentContainerProperty from '../../component-container-property.vue';
|
||||
|
||||
/** 秒杀属性面板 */
|
||||
defineOptions({ name: 'PromotionSeckillProperty' });
|
||||
|
||||
const props = defineProps<{ modelValue: PromotionSeckillProperty }>();
|
||||
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
|
||||
const formData = useVModel(props, 'modelValue', emit);
|
||||
// 活动列表
|
||||
const activityList = ref<MallSeckillActivityApi.SeckillActivity[]>([]);
|
||||
onMounted(async () => {
|
||||
const { list } = await SeckillActivityApi.getSeckillActivityPage({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
status: CommonStatusEnum.ENABLE,
|
||||
});
|
||||
activityList.value = list;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -68,10 +57,10 @@ onMounted(async () => {
|
||||
<IconifyIcon icon="fluent:text-column-two-24-filled" />
|
||||
</ElRadioButton>
|
||||
</ElTooltip>
|
||||
<!--<el-tooltip class="item" content="三列" placement="bottom">
|
||||
<el-radio-button value="threeCol">
|
||||
<!--<ElTooltip class="item" content="三列" placement="bottom">
|
||||
<ElRadioButton value="threeCol">
|
||||
<IconifyIcon icon="fluent:text-column-three-24-filled" />
|
||||
</el-radio-button>
|
||||
</ElRadioButton>
|
||||
</ElTooltip>-->
|
||||
</ElRadioGroup>
|
||||
</ElFormItem>
|
||||
@@ -116,14 +105,14 @@ onMounted(async () => {
|
||||
<ElFormItem label="角标" prop="badge.show">
|
||||
<ElSwitch v-model="formData.badge.show" />
|
||||
</ElFormItem>
|
||||
<ElFormItem v-if="formData.badge.show" label="角标" prop="badge.imgUrl">
|
||||
<ElFormItem label="角标" prop="badge.imgUrl" v-if="formData.badge.show">
|
||||
<UploadImg
|
||||
v-model="formData.badge.imgUrl"
|
||||
height="44px"
|
||||
width="72px"
|
||||
:show-description="false"
|
||||
>
|
||||
<template #tip> 建议尺寸:36 * 22</template>
|
||||
<template #tip> 建议尺寸:36 * 22 </template>
|
||||
</UploadImg>
|
||||
</ElFormItem>
|
||||
</ElCard>
|
||||
@@ -193,5 +182,3 @@ onMounted(async () => {
|
||||
</ElForm>
|
||||
</ComponentContainerProperty>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
||||
@@ -13,10 +13,10 @@ export interface SearchProperty {
|
||||
style: ComponentStyle;
|
||||
}
|
||||
|
||||
// 文字位置
|
||||
/** 文字位置 */
|
||||
export type PlaceholderPosition = 'center' | 'left';
|
||||
|
||||
// 定义组件
|
||||
/** 定义组件 */
|
||||
export const component = {
|
||||
id: 'SearchBar',
|
||||
name: '搜索框',
|
||||
|
||||
@@ -5,19 +5,19 @@ import { IconifyIcon } from '@vben/icons';
|
||||
|
||||
/** 搜索框 */
|
||||
defineOptions({ name: 'SearchBar' });
|
||||
|
||||
defineProps<{ property: SearchProperty }>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="search-bar"
|
||||
:style="{
|
||||
color: property.textColor,
|
||||
}"
|
||||
>
|
||||
<!-- 搜索框 -->
|
||||
<div
|
||||
class="inner"
|
||||
class="relative flex min-h-7 items-center text-sm"
|
||||
:style="{
|
||||
height: `${property.height}px`,
|
||||
background: property.backgroundColor,
|
||||
@@ -25,7 +25,7 @@ defineProps<{ property: SearchProperty }>();
|
||||
}"
|
||||
>
|
||||
<div
|
||||
class="placeholder"
|
||||
class="flex w-full items-center gap-0.5 overflow-hidden text-ellipsis whitespace-nowrap break-all px-2"
|
||||
:style="{
|
||||
justifyContent: property.placeholderPosition,
|
||||
}"
|
||||
@@ -33,11 +33,11 @@ defineProps<{ property: SearchProperty }>();
|
||||
<IconifyIcon icon="ep:search" />
|
||||
<span>{{ property.placeholder || '搜索商品' }}</span>
|
||||
</div>
|
||||
<div class="right">
|
||||
<div class="absolute right-2 flex items-center justify-center gap-2">
|
||||
<!-- 搜索热词 -->
|
||||
<span v-for="(keyword, index) in property.hotKeywords" :key="index">{{
|
||||
keyword
|
||||
}}</span>
|
||||
<span v-for="(keyword, index) in property.hotKeywords" :key="index">
|
||||
{{ keyword }}
|
||||
</span>
|
||||
<!-- 扫一扫 -->
|
||||
<IconifyIcon
|
||||
icon="ant-design:scan-outlined"
|
||||
@@ -47,37 +47,3 @@ defineProps<{ property: SearchProperty }>();
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.search-bar {
|
||||
/* 搜索框 */
|
||||
.inner {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 28px;
|
||||
font-size: 14px;
|
||||
|
||||
.placeholder {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding: 0 8px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
word-break: break-all;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.right {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
ElCard,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElRadioButton,
|
||||
ElRadioGroup,
|
||||
ElSlider,
|
||||
@@ -18,7 +19,7 @@ import {
|
||||
ElTooltip,
|
||||
} from 'element-plus';
|
||||
|
||||
import { Draggable } from '#/views/mall/promotion/components';
|
||||
import { ColorInput, Draggable } from '#/views/mall/promotion/components';
|
||||
|
||||
import ComponentContainerProperty from '../../component-container-property.vue';
|
||||
|
||||
@@ -26,10 +27,12 @@ import ComponentContainerProperty from '../../component-container-property.vue';
|
||||
defineOptions({ name: 'SearchProperty' });
|
||||
|
||||
const props = defineProps<{ modelValue: SearchProperty }>();
|
||||
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
|
||||
const formData = useVModel(props, 'modelValue', emit);
|
||||
|
||||
// 监听热词数组变化
|
||||
/** 监听热词数组变化 */
|
||||
watch(
|
||||
() => formData.value.hotKeywords,
|
||||
(newVal) => {
|
||||
@@ -45,8 +48,7 @@ watch(
|
||||
|
||||
<template>
|
||||
<ComponentContainerProperty v-model="formData.style">
|
||||
<!-- 表单 -->
|
||||
<ElForm label-width="80px" :model="formData" class="mt-2">
|
||||
<ElForm label-width="80px" :model="formData">
|
||||
<ElCard header="搜索热词" class="property-group" shadow="never">
|
||||
<Draggable
|
||||
v-model="formData.hotKeywords"
|
||||
@@ -56,7 +58,7 @@ watch(
|
||||
}"
|
||||
>
|
||||
<template #default="{ index }">
|
||||
<el-input
|
||||
<ElInput
|
||||
v-model="formData.hotKeywords[index]"
|
||||
placeholder="请输入热词"
|
||||
/>
|
||||
@@ -79,7 +81,7 @@ watch(
|
||||
</ElRadioGroup>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="提示文字" prop="placeholder">
|
||||
<el-input v-model="formData.placeholder" />
|
||||
<ElInput v-model="formData.placeholder" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="文本位置" prop="placeholderPosition">
|
||||
<ElRadioGroup v-model="formData!.placeholderPosition">
|
||||
@@ -110,12 +112,10 @@ watch(
|
||||
<ElFormItem label="框体颜色" prop="backgroundColor">
|
||||
<ColorInput v-model="formData.backgroundColor" />
|
||||
</ElFormItem>
|
||||
<ElFormItem class="lef" label="文本颜色" prop="textColor">
|
||||
<ElFormItem label="文本颜色" prop="textColor">
|
||||
<ColorInput v-model="formData.textColor" />
|
||||
</ElFormItem>
|
||||
</ElCard>
|
||||
</ElForm>
|
||||
</ComponentContainerProperty>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
||||
@@ -2,41 +2,29 @@ import type { DiyComponent } from '../../../util';
|
||||
|
||||
/** 底部导航菜单属性 */
|
||||
export interface TabBarProperty {
|
||||
// 选项列表
|
||||
items: TabBarItemProperty[];
|
||||
// 主题
|
||||
theme: string;
|
||||
// 样式
|
||||
style: TabBarStyle;
|
||||
items: TabBarItemProperty[]; // 选项列表
|
||||
theme: string; // 主题
|
||||
style: TabBarStyle; // 样式
|
||||
}
|
||||
|
||||
// 选项属性
|
||||
/** 选项属性 */
|
||||
export interface TabBarItemProperty {
|
||||
// 标签文字
|
||||
text: string;
|
||||
// 链接
|
||||
url: string;
|
||||
// 默认图标链接
|
||||
iconUrl: string;
|
||||
// 选中的图标链接
|
||||
activeIconUrl: string;
|
||||
text: string; // 标签文字
|
||||
url: string; // 链接
|
||||
iconUrl: string; // 默认图标链接
|
||||
activeIconUrl: string; // 选中的图标链接
|
||||
}
|
||||
|
||||
// 样式
|
||||
/** 样式 */
|
||||
export interface TabBarStyle {
|
||||
// 背景类型
|
||||
bgType: 'color' | 'img';
|
||||
// 背景颜色
|
||||
bgColor: string;
|
||||
// 图片链接
|
||||
bgImg: string;
|
||||
// 默认颜色
|
||||
color: string;
|
||||
// 选中的颜色
|
||||
activeColor: string;
|
||||
bgType: 'color' | 'img'; // 背景类型
|
||||
bgColor: string; // 背景颜色
|
||||
bgImg: string; // 图片链接
|
||||
color: string; // 默认颜色
|
||||
activeColor: string; // 选中的颜色
|
||||
}
|
||||
|
||||
// 定义组件
|
||||
/** 定义组件 */
|
||||
export const component = {
|
||||
id: 'TabBar',
|
||||
name: '底部导航',
|
||||
|
||||
@@ -5,15 +5,15 @@ import { IconifyIcon } from '@vben/icons';
|
||||
|
||||
import { ElImage } from 'element-plus';
|
||||
|
||||
/** 页面底部导航栏 */
|
||||
/** 底部导航 */
|
||||
defineOptions({ name: 'TabBar' });
|
||||
|
||||
defineProps<{ property: TabBarProperty }>();
|
||||
</script>
|
||||
<template>
|
||||
<div class="tab-bar">
|
||||
<div class="z-[2] w-full">
|
||||
<div
|
||||
class="tab-bar-bg"
|
||||
class="flex flex-row items-center justify-around py-2"
|
||||
:style="{
|
||||
background:
|
||||
property.style.bgType === 'color'
|
||||
@@ -26,12 +26,18 @@ defineProps<{ property: TabBarProperty }>();
|
||||
<div
|
||||
v-for="(item, index) in property.items"
|
||||
:key="index"
|
||||
class="tab-bar-item"
|
||||
class="tab-bar-item flex w-full flex-col items-center justify-center text-xs"
|
||||
>
|
||||
<ElImage :src="index === 0 ? item.activeIconUrl : item.iconUrl">
|
||||
<ElImage
|
||||
:src="index === 0 ? item.activeIconUrl : item.iconUrl"
|
||||
class="h-[26px] w-[26px] rounded"
|
||||
>
|
||||
<template #error>
|
||||
<div class="flex h-full w-full items-center justify-center">
|
||||
<IconifyIcon icon="ep:picture" />
|
||||
<IconifyIcon
|
||||
icon="ep:picture"
|
||||
class="h-[26px] w-[26px] rounded"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</ElImage>
|
||||
@@ -47,33 +53,3 @@ defineProps<{ property: TabBarProperty }>();
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<style lang="scss" scoped>
|
||||
.tab-bar {
|
||||
z-index: 2;
|
||||
width: 100%;
|
||||
|
||||
.tab-bar-bg {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
padding: 8px 0;
|
||||
|
||||
.tab-bar-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
font-size: 12px;
|
||||
|
||||
:deep(img),
|
||||
.el-icon {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -23,7 +23,8 @@ import {
|
||||
} from '#/views/mall/promotion/components';
|
||||
|
||||
import { component, THEME_LIST } from './config';
|
||||
// 底部导航栏
|
||||
|
||||
/** 底部导航栏 */
|
||||
defineOptions({ name: 'TabBarProperty' });
|
||||
|
||||
const props = defineProps<{ modelValue: TabBarProperty }>();
|
||||
@@ -33,7 +34,7 @@ const formData = useVModel(props, 'modelValue', emit);
|
||||
// 将数据库的值更新到右侧属性栏
|
||||
component.property.items = formData.value.items;
|
||||
|
||||
// 要的主题
|
||||
/** 处理主题变更 */
|
||||
const handleThemeChange = () => {
|
||||
const theme = THEME_LIST.find((theme) => theme.id === formData.value.theme);
|
||||
if (theme?.color) {
|
||||
@@ -44,7 +45,6 @@ const handleThemeChange = () => {
|
||||
|
||||
<template>
|
||||
<div class="tab-bar">
|
||||
<!-- 表单 -->
|
||||
<ElForm :model="formData" label-width="80px">
|
||||
<ElFormItem label="主题" prop="theme">
|
||||
<ElSelect v-model="formData!.theme" @change="handleThemeChange">
|
||||
@@ -89,7 +89,6 @@ const handleThemeChange = () => {
|
||||
<template #tip> 建议尺寸 375 * 50 </template>
|
||||
</UploadImg>
|
||||
</ElFormItem>
|
||||
|
||||
<ElText tag="p">图标设置</ElText>
|
||||
<ElText type="info" size="small">
|
||||
拖动左上角的小圆点可对其排序, 图标建议尺寸 44*44
|
||||
@@ -129,5 +128,3 @@ const handleThemeChange = () => {
|
||||
</ElForm>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
||||
@@ -2,46 +2,28 @@ import type { ComponentStyle, DiyComponent } from '../../../util';
|
||||
|
||||
/** 标题栏属性 */
|
||||
export interface TitleBarProperty {
|
||||
// 背景图
|
||||
bgImgUrl: string;
|
||||
// 偏移
|
||||
marginLeft: number;
|
||||
// 显示位置
|
||||
textAlign: 'center' | 'left';
|
||||
// 主标题
|
||||
title: string;
|
||||
// 副标题
|
||||
description: string;
|
||||
// 标题大小
|
||||
titleSize: number;
|
||||
// 描述大小
|
||||
descriptionSize: number;
|
||||
// 标题粗细
|
||||
titleWeight: number;
|
||||
// 描述粗细
|
||||
descriptionWeight: number;
|
||||
// 标题颜色
|
||||
titleColor: string;
|
||||
// 描述颜色
|
||||
descriptionColor: string;
|
||||
// 高度
|
||||
height: number;
|
||||
// 查看更多
|
||||
bgImgUrl: string; // 背景图
|
||||
marginLeft: number; // 偏移
|
||||
textAlign: 'center' | 'left'; // 显示位置
|
||||
title: string; // 主标题
|
||||
description: string; // 副标题
|
||||
titleSize: number; // 标题大小
|
||||
descriptionSize: number; // 描述大小
|
||||
titleWeight: number; // 标题粗细
|
||||
descriptionWeight: number; // 描述粗细
|
||||
titleColor: string; // 标题颜色
|
||||
descriptionColor: string; // 描述颜色
|
||||
height: number; // 高度
|
||||
more: {
|
||||
// 是否显示查看更多
|
||||
show: false;
|
||||
// 自定义文字
|
||||
text: string;
|
||||
// 样式选择
|
||||
type: 'all' | 'icon' | 'text';
|
||||
// 链接
|
||||
url: string;
|
||||
};
|
||||
// 组件样式
|
||||
style: ComponentStyle;
|
||||
show: false; // 是否显示查看更多
|
||||
text: string; // 自定义文字
|
||||
type: 'all' | 'icon' | 'text'; // 样式选择
|
||||
url: string; // 链接
|
||||
}; // 查看更多
|
||||
style: ComponentStyle; // 组件样式
|
||||
}
|
||||
|
||||
// 定义组件
|
||||
/** 定义组件 */
|
||||
export const component = {
|
||||
id: 'TitleBar',
|
||||
name: '标题栏',
|
||||
|
||||
@@ -11,7 +11,10 @@ defineOptions({ name: 'TitleBar' });
|
||||
defineProps<{ property: TitleBarProperty }>();
|
||||
</script>
|
||||
<template>
|
||||
<div class="title-bar" :style="{ height: `${property.height}px` }">
|
||||
<div
|
||||
class="relative box-border min-h-[20px] w-full"
|
||||
:style="{ height: `${property.height}px` }"
|
||||
>
|
||||
<ElImage
|
||||
v-if="property.bgImgUrl"
|
||||
:src="property.bgImgUrl"
|
||||
@@ -51,7 +54,7 @@ defineProps<{ property: TitleBarProperty }>();
|
||||
</div>
|
||||
<!-- 更多 -->
|
||||
<div
|
||||
class="more"
|
||||
class="absolute bottom-0 right-2 top-0 m-auto flex items-center justify-center text-[10px] text-[#969799]"
|
||||
v-show="property.more.show"
|
||||
:style="{
|
||||
color: property.descriptionColor,
|
||||
@@ -64,25 +67,3 @@ defineProps<{ property: TitleBarProperty }>();
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<style scoped lang="scss">
|
||||
.title-bar {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
min-height: 20px;
|
||||
|
||||
/* 更多 */
|
||||
.more {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 8px;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: auto;
|
||||
font-size: 10px;
|
||||
color: #969799;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -24,15 +24,16 @@ import {
|
||||
|
||||
import ComponentContainerProperty from '../../component-container-property.vue';
|
||||
|
||||
// 导航栏属性面板
|
||||
/** 导航栏属性面板 */
|
||||
defineOptions({ name: 'TitleBarProperty' });
|
||||
|
||||
const props = defineProps<{ modelValue: TitleBarProperty }>();
|
||||
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
|
||||
const formData = useVModel(props, 'modelValue', emit);
|
||||
|
||||
// 表单校验
|
||||
const rules = {};
|
||||
const rules = {}; // 表单校验
|
||||
</script>
|
||||
<template>
|
||||
<ComponentContainerProperty v-model="formData.style">
|
||||
@@ -167,5 +168,3 @@ const rules = {};
|
||||
</ElForm>
|
||||
</ComponentContainerProperty>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
||||
@@ -2,11 +2,10 @@ import type { ComponentStyle, DiyComponent } from '../../../util';
|
||||
|
||||
/** 用户卡片属性 */
|
||||
export interface UserCardProperty {
|
||||
// 组件样式
|
||||
style: ComponentStyle;
|
||||
style: ComponentStyle; // 组件样式
|
||||
}
|
||||
|
||||
// 定义组件
|
||||
/** 定义组件 */
|
||||
export const component = {
|
||||
id: 'UserCard',
|
||||
name: '用户卡片',
|
||||
|
||||
@@ -7,13 +7,14 @@ import { ElAvatar } from 'element-plus';
|
||||
|
||||
/** 用户卡片 */
|
||||
defineOptions({ name: 'UserCard' });
|
||||
// 定义属性
|
||||
|
||||
/** 定义属性 */
|
||||
defineProps<{ property: UserCardProperty }>();
|
||||
</script>
|
||||
<template>
|
||||
<div class="flex flex-col">
|
||||
<div class="flex items-center justify-between px-[18px] py-[24px]">
|
||||
<div class="flex flex-1 items-center gap-[16px]">
|
||||
<div class="flex items-center justify-between px-4 py-6">
|
||||
<div class="flex flex-1 items-center gap-4">
|
||||
<ElAvatar :size="60">
|
||||
<IconifyIcon icon="ep:avatar" :size="60" />
|
||||
</ElAvatar>
|
||||
@@ -21,15 +22,11 @@ defineProps<{ property: UserCardProperty }>();
|
||||
</div>
|
||||
<IconifyIcon icon="tdesign:qrcode" :size="20" />
|
||||
</div>
|
||||
<div
|
||||
class="flex items-center justify-between bg-white px-[20px] py-[8px] text-[12px]"
|
||||
>
|
||||
<div class="flex items-center justify-between bg-white px-5 py-2 text-xs">
|
||||
<span class="text-[#ff690d]">点击绑定手机号</span>
|
||||
<span class="rounded-[26px] bg-[#ff6100] px-[8px] py-[5px] text-white">
|
||||
<span class="rounded-[26px] bg-[#ff6100] px-2 py-1.5 text-white">
|
||||
去绑定
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
||||
@@ -5,16 +5,16 @@ import { useVModel } from '@vueuse/core';
|
||||
|
||||
import ComponentContainerProperty from '../../component-container-property.vue';
|
||||
|
||||
// 用户卡片属性面板
|
||||
/** 用户卡片属性面板 */
|
||||
defineOptions({ name: 'UserCardProperty' });
|
||||
|
||||
const props = defineProps<{ modelValue: UserCardProperty }>();
|
||||
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
|
||||
const formData = useVModel(props, 'modelValue', emit);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ComponentContainerProperty v-model="formData.style" />
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
||||
@@ -13,5 +13,3 @@ defineProps<{ property: UserCouponProperty }>();
|
||||
src="https://shopro.sheepjs.com/admin/static/images/shop/decorate/couponCardStyle.png"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
||||
@@ -16,5 +16,3 @@ const formData = useVModel(props, 'modelValue', emit);
|
||||
<template>
|
||||
<ComponentContainerProperty v-model="formData.style" />
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
||||
@@ -2,11 +2,10 @@ import type { ComponentStyle, DiyComponent } from '../../../util';
|
||||
|
||||
/** 用户订单属性 */
|
||||
export interface UserOrderProperty {
|
||||
// 组件样式
|
||||
style: ComponentStyle;
|
||||
style: ComponentStyle; // 组件样式
|
||||
}
|
||||
|
||||
// 定义组件
|
||||
/** 定义组件 */
|
||||
export const component = {
|
||||
id: 'UserOrder',
|
||||
name: '用户订单',
|
||||
|
||||
@@ -5,7 +5,8 @@ import { ElImage } from 'element-plus';
|
||||
|
||||
/** 用户订单 */
|
||||
defineOptions({ name: 'UserOrder' });
|
||||
// 定义属性
|
||||
|
||||
/** 定义属性 */
|
||||
defineProps<{ property: UserOrderProperty }>();
|
||||
</script>
|
||||
<template>
|
||||
@@ -13,5 +14,3 @@ defineProps<{ property: UserOrderProperty }>();
|
||||
src="https://shopro.sheepjs.com/admin/static/images/shop/decorate/orderCardStyle.png"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
||||
@@ -5,16 +5,16 @@ import { useVModel } from '@vueuse/core';
|
||||
|
||||
import ComponentContainerProperty from '../../component-container-property.vue';
|
||||
|
||||
// 用户订单属性面板
|
||||
/** 用户订单属性面板 */
|
||||
defineOptions({ name: 'UserOrderProperty' });
|
||||
|
||||
const props = defineProps<{ modelValue: UserOrderProperty }>();
|
||||
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
|
||||
const formData = useVModel(props, 'modelValue', emit);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ComponentContainerProperty v-model="formData.style" />
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
||||
@@ -2,11 +2,10 @@ import type { ComponentStyle, DiyComponent } from '../../../util';
|
||||
|
||||
/** 用户资产属性 */
|
||||
export interface UserWalletProperty {
|
||||
// 组件样式
|
||||
style: ComponentStyle;
|
||||
style: ComponentStyle; // 组件样式
|
||||
}
|
||||
|
||||
// 定义组件
|
||||
/** 定义组件 */
|
||||
export const component = {
|
||||
id: 'UserWallet',
|
||||
name: '用户资产',
|
||||
|
||||
@@ -5,7 +5,8 @@ import { ElImage } from 'element-plus';
|
||||
|
||||
/** 用户资产 */
|
||||
defineOptions({ name: 'UserWallet' });
|
||||
// 定义属性
|
||||
|
||||
/** 定义属性 */
|
||||
defineProps<{ property: UserWalletProperty }>();
|
||||
</script>
|
||||
<template>
|
||||
@@ -13,5 +14,3 @@ defineProps<{ property: UserWalletProperty }>();
|
||||
src="https://shopro.sheepjs.com/admin/static/images/shop/decorate/walletCardStyle.png"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
||||
@@ -5,16 +5,16 @@ import { useVModel } from '@vueuse/core';
|
||||
|
||||
import ComponentContainerProperty from '../../component-container-property.vue';
|
||||
|
||||
// 用户资产属性面板
|
||||
/** 用户资产属性面板 */
|
||||
defineOptions({ name: 'UserWalletProperty' });
|
||||
|
||||
const props = defineProps<{ modelValue: UserWalletProperty }>();
|
||||
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
|
||||
const formData = useVModel(props, 'modelValue', emit);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ComponentContainerProperty v-model="formData.style" />
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
||||
@@ -2,23 +2,18 @@ import type { ComponentStyle, DiyComponent } from '../../../util';
|
||||
|
||||
/** 视频播放属性 */
|
||||
export interface VideoPlayerProperty {
|
||||
// 视频链接
|
||||
videoUrl: string;
|
||||
// 封面链接
|
||||
posterUrl: string;
|
||||
// 是否自动播放
|
||||
autoplay: boolean;
|
||||
// 组件样式
|
||||
style: VideoPlayerStyle;
|
||||
videoUrl: string; // 视频链接
|
||||
posterUrl: string; // 封面链接
|
||||
autoplay: boolean; // 是否自动播放
|
||||
style: VideoPlayerStyle; // 组件样式
|
||||
}
|
||||
|
||||
// 视频播放样式
|
||||
/** 视频播放样式 */
|
||||
export interface VideoPlayerStyle extends ComponentStyle {
|
||||
// 视频高度
|
||||
height: number;
|
||||
height: number; // 视频高度
|
||||
}
|
||||
|
||||
// 定义组件
|
||||
/** 定义组件 */
|
||||
export const component = {
|
||||
id: 'VideoPlayer',
|
||||
name: '视频播放',
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user