fix: eslint

This commit is contained in:
hw
2025-11-07 09:43:39 +08:00
680 changed files with 15309 additions and 9837 deletions

View File

@@ -50,7 +50,7 @@ export function getCategoryList(params: any) {
);
}
// 获得商品分类列表
/** 获得商品分类列表 */
export function getCategorySimpleList() {
return requestClient.get<MallCategoryApi.Category[]>(
'/product/category/list',

View File

@@ -50,6 +50,10 @@ export namespace MallCombinationActivityApi {
products: CombinationProduct[];
/** 图片 */
picUrl?: string;
/** 商品名称 */
spuName?: string;
/** 市场价 */
marketPrice?: number;
}
/** 扩展 SKU 配置 */

View File

@@ -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; // 是否置顶
}
}

View File

@@ -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; // 会话编号
}
}

View File

@@ -57,6 +57,10 @@ export namespace MallSeckillActivityApi {
products?: SeckillProduct[];
/** 图片 */
picUrl?: string;
/** 商品名称 */
spuName?: string;
/** 市场价 */
marketPrice?: number;
}
/** 扩展 SKU 配置 */

View File

@@ -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();

View File

@@ -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')"
>

View 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 @javeidea 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));
}

View File

@@ -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';

View File

@@ -203,5 +203,3 @@ const [Grid, gridApi] = useVbenVxeGrid({
</Grid>
</Page>
</template>

View File

@@ -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 '';

View File

@@ -14,7 +14,6 @@ const formData = ref<InfraApiAccessLogApi.ApiAccessLog>();
const [Descriptions] = useDescription({
border: true,
column: 1,
labelWidth: 110,
schema: useDetailSchema(),
});

View File

@@ -14,7 +14,6 @@ const formData = ref<InfraApiErrorLogApi.ApiErrorLog>();
const [Descriptions] = useDescription({
border: true,
column: 1,
labelWidth: 110,
schema: useDetailSchema(),
});

View File

@@ -13,8 +13,8 @@ import { useDetailSchema } from '../data';
const formData = ref<InfraJobLogApi.JobLog>();
const [Descriptions] = useDescription({
border: true,
column: 1,
labelWidth: 140,
schema: useDetailSchema(),
});

View File

@@ -16,7 +16,6 @@ const nextTimes = ref<Date[]>([]); // 下一次执行时间
const [Descriptions] = useDescription({
border: true,
column: 1,
labelWidth: 140,
schema: useDetailSchema(),
});

View File

@@ -95,9 +95,9 @@ onActivated(() => {
/** 初始化 */
onMounted(() => {
getOrderData();
getProductData();
getWalletRechargeData();
loadOrderData();
loadProductData();
loadWalletRechargeData();
});
</script>

View File

@@ -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',
},

View File

@@ -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();

View File

@@ -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';

View File

@@ -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>

View File

@@ -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>

View File

@@ -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 - checkboxfalse - 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>

View File

@@ -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 [

View File

@@ -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,
},
},
},

View File

@@ -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>

View File

@@ -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为活动IDvalue为是否选中
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不会有选中的效果 -->
&nbsp;
</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>

View File

@@ -0,0 +1 @@
export { default as CombinationShowcase } from './showcase.vue';

View File

@@ -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>

View File

@@ -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 - checkboxfalse - 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>

View File

@@ -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>

View File

@@ -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;

View File

@@ -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; /* 拖拽时显示占位提示 */
}
}
}

View File

@@ -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',

View File

@@ -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"

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -17,8 +17,6 @@ export interface MagicCubeItemProperty {
height: number; // 高
top: number; // 上
left: number; // 左
right: number; // 右
bottom: number; // 下
}
/** 定义组件 */

View File

@@ -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: '宫格导航',

View File

@@ -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>

View File

@@ -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>

View File

@@ -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: '列表导航',

View File

@@ -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>

View File

@@ -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>

View File

@@ -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: '菜单导航',

View File

@@ -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"

View File

@@ -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>

View File

@@ -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>

View File

@@ -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: '顶部导航栏',

View File

@@ -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>

View File

@@ -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>

View File

@@ -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: '公告栏',

View File

@@ -34,5 +34,3 @@ defineProps<{ property: NoticeBarProperty }>();
<IconifyIcon icon="ep:arrow-right" />
</div>
</template>
<style scoped lang="scss"></style>

View File

@@ -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>

View File

@@ -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: '页面设置',

View File

@@ -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>

View File

@@ -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: '弹窗广告',

View File

@@ -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>

View File

@@ -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>

View File

@@ -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: '商品卡片',

View File

@@ -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>

View File

@@ -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>

View File

@@ -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: '商品栏',

View File

@@ -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>

View File

@@ -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>

View File

@@ -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: '营销文章',

View File

@@ -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>

View File

@@ -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>

View File

@@ -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: '拼团',

View File

@@ -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>

View File

@@ -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>

View File

@@ -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: '积分商城',

View File

@@ -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>

View File

@@ -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>

View File

@@ -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: '秒杀',

View File

@@ -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>

View File

@@ -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>

View File

@@ -13,10 +13,10 @@ export interface SearchProperty {
style: ComponentStyle;
}
// 文字位置
/** 文字位置 */
export type PlaceholderPosition = 'center' | 'left';
// 定义组件
/** 定义组件 */
export const component = {
id: 'SearchBar',
name: '搜索框',

View File

@@ -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>

View File

@@ -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>

View File

@@ -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: '底部导航',

View File

@@ -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>

View File

@@ -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>

View File

@@ -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: '标题栏',

View File

@@ -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>

View File

@@ -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>

View File

@@ -2,11 +2,10 @@ import type { ComponentStyle, DiyComponent } from '../../../util';
/** 用户卡片属性 */
export interface UserCardProperty {
// 组件样式
style: ComponentStyle;
style: ComponentStyle; // 组件样式
}
// 定义组件
/** 定义组件 */
export const component = {
id: 'UserCard',
name: '用户卡片',

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -16,5 +16,3 @@ const formData = useVModel(props, 'modelValue', emit);
<template>
<ComponentContainerProperty v-model="formData.style" />
</template>
<style scoped lang="scss"></style>

View File

@@ -2,11 +2,10 @@ import type { ComponentStyle, DiyComponent } from '../../../util';
/** 用户订单属性 */
export interface UserOrderProperty {
// 组件样式
style: ComponentStyle;
style: ComponentStyle; // 组件样式
}
// 定义组件
/** 定义组件 */
export const component = {
id: 'UserOrder',
name: '用户订单',

View File

@@ -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>

View File

@@ -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>

View File

@@ -2,11 +2,10 @@ import type { ComponentStyle, DiyComponent } from '../../../util';
/** 用户资产属性 */
export interface UserWalletProperty {
// 组件样式
style: ComponentStyle;
style: ComponentStyle; // 组件样式
}
// 定义组件
/** 定义组件 */
export const component = {
id: 'UserWallet',
name: '用户资产',

View File

@@ -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>

View File

@@ -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>

View File

@@ -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