Merge branch 'master' of https://gitee.com/yudaocode/yudao-ui-admin-vben into dev
commit
1d5bfaac5a
|
|
@ -21,7 +21,8 @@
|
|||
// CSS 变量提示
|
||||
"vunguyentuan.vscode-css-variables",
|
||||
// 在 package.json 中显示 PNPM catalog 的版本
|
||||
"antfu.pnpm-catalog-lens"
|
||||
"antfu.pnpm-catalog-lens",
|
||||
"augment.vscode-augment"
|
||||
],
|
||||
"unwantedRecommendations": [
|
||||
// 和 volar 冲突
|
||||
|
|
|
|||
|
|
@ -4,11 +4,12 @@ export namespace ErpProductCategoryApi {
|
|||
/** ERP 产品分类信息 */
|
||||
export interface ProductCategory {
|
||||
id?: number; // 分类编号
|
||||
parentId: number; // 父分类编号
|
||||
parentId?: number; // 父分类编号
|
||||
name: string; // 分类名称
|
||||
code: string; // 分类编码
|
||||
sort: number; // 分类排序
|
||||
status: number; // 开启状态
|
||||
code?: string; // 分类编码
|
||||
sort?: number; // 分类排序
|
||||
status?: number; // 开启状态
|
||||
children?: ProductCategory[]; // 子分类
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import type { PageParam, PageResult } from '@vben/request';
|
|||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
namespace ErpStockOutApi {
|
||||
export namespace ErpStockOutApi {
|
||||
/** 其它出库单信息 */
|
||||
export interface StockOut {
|
||||
id?: number; // 出库编号
|
||||
|
|
@ -13,6 +13,24 @@ namespace ErpStockOutApi {
|
|||
totalPrice: number; // 合计金额,单位:元
|
||||
status: number; // 状态
|
||||
remark: string; // 备注
|
||||
fileUrl?: string; // 附件
|
||||
items?: StockOutItem[]; // 出库产品清单
|
||||
}
|
||||
|
||||
/** 其它出库单产品信息 */
|
||||
export interface StockOutItem {
|
||||
id?: number; // 编号
|
||||
warehouseId?: number; // 仓库编号
|
||||
productId?: number; // 产品编号
|
||||
productName?: string; // 产品名称
|
||||
productUnitId?: number; // 产品单位编号
|
||||
productUnitName?: string; // 产品单位名称
|
||||
productBarCode?: string; // 产品条码
|
||||
count: number; // 数量
|
||||
productPrice: number; // 产品单价
|
||||
totalPrice: number; // 总价
|
||||
stockCount?: number; // 库存数量
|
||||
remark?: string; // 备注
|
||||
}
|
||||
|
||||
/** 其它出库单分页查询参数 */
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ export namespace InfraFileConfigApi {
|
|||
accessKey?: string;
|
||||
accessSecret?: string;
|
||||
pathStyle?: boolean;
|
||||
enablePublicAccess?: boolean;
|
||||
domain: string;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,2 +1,2 @@
|
|||
export { default as Tinyflow } from './tinyflow.vue';
|
||||
export { default as Tinyflow } from './Tinyflow.vue';
|
||||
export * from './ui/typing';
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import { isNumber } from '@vben/utils';
|
|||
import { Button, Input, Select } from 'ant-design-vue';
|
||||
|
||||
import { testWorkflow } from '#/api/ai/workflow';
|
||||
import { Tinyflow } from '#/components/tinyflow';
|
||||
import { Tinyflow } from '#/components/Tinyflow';
|
||||
|
||||
defineProps<{
|
||||
provider: any;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,125 @@
|
|||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { ErpProductCategoryApi } from '#/api/erp/product/category';
|
||||
|
||||
import { handleTree } from '@vben/utils';
|
||||
|
||||
import { z } from '#/adapter/form';
|
||||
import { getProductCategoryList } from '#/api/erp/product/category';
|
||||
import { CommonStatusEnum, DICT_TYPE, getDictOptions } from '#/utils';
|
||||
|
||||
/** 新增/修改的表单 */
|
||||
export function useFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'id',
|
||||
component: 'Input',
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
show: () => false,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'parentId',
|
||||
label: '上级分类',
|
||||
component: 'ApiTreeSelect',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
api: async () => {
|
||||
const data = await getProductCategoryList();
|
||||
data.unshift({
|
||||
id: 0,
|
||||
name: '顶级分类',
|
||||
});
|
||||
return handleTree(data);
|
||||
},
|
||||
labelField: 'name',
|
||||
valueField: 'id',
|
||||
childrenField: 'children',
|
||||
placeholder: '请选择上级分类',
|
||||
treeDefaultExpandAll: true,
|
||||
},
|
||||
rules: 'selectRequired',
|
||||
},
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '分类名称',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入分类名称',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'code',
|
||||
label: '分类编码',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入分类编码',
|
||||
},
|
||||
rules: z.string().regex(/^[A-Z]+$/, '分类编码必须为大写字母'),
|
||||
},
|
||||
{
|
||||
fieldName: 'sort',
|
||||
label: '显示顺序',
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
min: 0,
|
||||
controlsPosition: 'right',
|
||||
placeholder: '请输入显示顺序',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
|
||||
{
|
||||
fieldName: 'status',
|
||||
label: '状态',
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
|
||||
buttonStyle: 'solid',
|
||||
optionType: 'button',
|
||||
},
|
||||
rules: z.number().default(CommonStatusEnum.ENABLE),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns(): VxeTableGridOptions<ErpProductCategoryApi.ProductCategory>['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'name',
|
||||
title: '分类名称',
|
||||
align: 'left',
|
||||
treeNode: true,
|
||||
},
|
||||
{
|
||||
field: 'code',
|
||||
title: '分类编码',
|
||||
},
|
||||
{
|
||||
field: 'sort',
|
||||
title: '显示顺序',
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
title: '分类状态',
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.COMMON_STATUS },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '创建时间',
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 220,
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -1,34 +1,166 @@
|
|||
<script lang="ts" setup>
|
||||
import { DocAlert, Page } from '@vben/common-ui';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { ErpProductCategoryApi } from '#/api/erp/product/category';
|
||||
|
||||
import { Button } from 'ant-design-vue';
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { DocAlert, Page, useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
deleteProductCategory,
|
||||
getProductCategoryList,
|
||||
} from '#/api/erp/product/category';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useGridColumns } from './data';
|
||||
import Form from './modules/form.vue';
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: Form,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 刷新表格 */
|
||||
function onRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 切换树形展开/收缩状态 */
|
||||
const isExpanded = ref(true);
|
||||
function toggleExpand() {
|
||||
isExpanded.value = !isExpanded.value;
|
||||
gridApi.grid.setAllTreeExpand(isExpanded.value);
|
||||
}
|
||||
|
||||
/** 创建分类 */
|
||||
function handleCreate() {
|
||||
formModalApi.setData(null).open();
|
||||
}
|
||||
|
||||
/** 添加下级分类 */
|
||||
function handleAppend(row: ErpProductCategoryApi.ProductCategory) {
|
||||
formModalApi.setData({ parentId: row.id }).open();
|
||||
}
|
||||
|
||||
/** 编辑分类 */
|
||||
function handleEdit(row: ErpProductCategoryApi.ProductCategory) {
|
||||
formModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 删除分类 */
|
||||
async function handleDelete(row: ErpProductCategoryApi.ProductCategory) {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.deleting', [row.name]),
|
||||
key: 'action_key_msg',
|
||||
});
|
||||
try {
|
||||
await deleteProductCategory(row.id as number);
|
||||
message.success({
|
||||
content: $t('ui.actionMessage.deleteSuccess', [row.name]),
|
||||
key: 'action_key_msg',
|
||||
});
|
||||
onRefresh();
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
gridOptions: {
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async () => {
|
||||
return await getProductCategoryList();
|
||||
},
|
||||
},
|
||||
},
|
||||
pagerConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: true,
|
||||
search: true,
|
||||
},
|
||||
treeConfig: {
|
||||
transform: true,
|
||||
rowField: 'id',
|
||||
parentField: 'parentId',
|
||||
expandAll: true,
|
||||
accordion: false,
|
||||
},
|
||||
} as VxeTableGridOptions<ErpProductCategoryApi.ProductCategory>,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page>
|
||||
<Page auto-content-height>
|
||||
<template #doc>
|
||||
<DocAlert
|
||||
title="【产品】产品信息、分类、单位"
|
||||
url="https://doc.iocoder.cn/erp/product/"
|
||||
/>
|
||||
</template>
|
||||
<Button
|
||||
danger
|
||||
type="link"
|
||||
target="_blank"
|
||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
|
||||
>
|
||||
该功能支持 Vue3 + element-plus 版本!
|
||||
</Button>
|
||||
<br />
|
||||
<Button
|
||||
type="link"
|
||||
target="_blank"
|
||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/erp/product/category/index"
|
||||
>
|
||||
可参考
|
||||
https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/erp/product/category/index
|
||||
代码,pull request 贡献给我们!
|
||||
</Button>
|
||||
<FormModal @success="onRefresh" />
|
||||
<Grid table-title="产品分类列表">
|
||||
<template #toolbar-tools>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('ui.actionTitle.create', ['产品分类']),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.ADD,
|
||||
auth: ['erp:product-category:create'],
|
||||
onClick: handleCreate,
|
||||
},
|
||||
{
|
||||
label: isExpanded ? '收缩' : '展开',
|
||||
type: 'primary',
|
||||
onClick: toggleExpand,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '新增下级',
|
||||
type: 'link',
|
||||
icon: ACTION_ICON.ADD,
|
||||
auth: ['erp:product-category:create'],
|
||||
onClick: handleAppend.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: $t('common.edit'),
|
||||
type: 'link',
|
||||
icon: ACTION_ICON.EDIT,
|
||||
auth: ['erp:product-category:update'],
|
||||
onClick: handleEdit.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: $t('common.delete'),
|
||||
type: 'link',
|
||||
danger: true,
|
||||
icon: ACTION_ICON.DELETE,
|
||||
auth: ['erp:product-category:delete'],
|
||||
ifShow: !(row.children && row.children.length > 0),
|
||||
popConfirm: {
|
||||
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
|
||||
confirm: handleDelete.bind(null, row),
|
||||
},
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,92 @@
|
|||
<script lang="ts" setup>
|
||||
import type { ErpProductCategoryApi } from '#/api/erp/product/category';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import {
|
||||
createProductCategory,
|
||||
getProductCategory,
|
||||
updateProductCategory,
|
||||
} from '#/api/erp/product/category';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useFormSchema } from '../data';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const formData = ref<ErpProductCategoryApi.ProductCategory>();
|
||||
const getTitle = computed(() => {
|
||||
return formData.value?.id
|
||||
? $t('ui.actionTitle.edit', ['分类'])
|
||||
: $t('ui.actionTitle.create', ['分类']);
|
||||
});
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
formItemClass: 'col-span-2',
|
||||
labelWidth: 80,
|
||||
},
|
||||
layout: 'horizontal',
|
||||
schema: useFormSchema(),
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
// 提交表单
|
||||
const data =
|
||||
(await formApi.getValues()) as ErpProductCategoryApi.ProductCategory;
|
||||
try {
|
||||
await (formData.value?.id
|
||||
? updateProductCategory(data)
|
||||
: createProductCategory(data));
|
||||
// 关闭并提示
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
message.success($t('ui.actionMessage.operationSuccess'));
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
formData.value = undefined;
|
||||
return;
|
||||
}
|
||||
// 加载数据
|
||||
let data = modalApi.getData<ErpProductCategoryApi.ProductCategory>();
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
if (data.id) {
|
||||
modalApi.lock();
|
||||
try {
|
||||
data = await getProductCategory(data.id);
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
}
|
||||
// 设置到 values
|
||||
formData.value = data;
|
||||
await formApi.setValues(formData.value);
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="getTitle">
|
||||
<Form class="mx-4" />
|
||||
</Modal>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,220 @@
|
|||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
import { handleTree } from '@vben/utils';
|
||||
|
||||
import { z } from '#/adapter/form';
|
||||
import { getProductCategorySimpleList } from '#/api/erp/product/category';
|
||||
import { getProductUnitSimpleList } from '#/api/erp/product/unit';
|
||||
import { CommonStatusEnum, DICT_TYPE, getDictOptions } from '#/utils';
|
||||
|
||||
/** 新增/修改的表单 */
|
||||
export function useFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'id',
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
show: () => false,
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'name',
|
||||
label: '名称',
|
||||
rules: 'required',
|
||||
componentProps: {
|
||||
placeholder: '请输入名称',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'barCode',
|
||||
label: '条码',
|
||||
component: 'Input',
|
||||
rules: 'required',
|
||||
componentProps: {
|
||||
placeholder: '请输入条码',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'categoryId',
|
||||
label: '分类',
|
||||
component: 'ApiTreeSelect',
|
||||
componentProps: {
|
||||
api: async () => {
|
||||
const data = await getProductCategorySimpleList();
|
||||
return handleTree(data);
|
||||
},
|
||||
|
||||
labelField: 'name',
|
||||
valueField: 'id',
|
||||
childrenField: 'children',
|
||||
placeholder: '请选择分类',
|
||||
treeDefaultExpandAll: true,
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'unitId',
|
||||
label: '单位',
|
||||
component: 'ApiSelect',
|
||||
componentProps: {
|
||||
api: getProductUnitSimpleList,
|
||||
labelField: 'name',
|
||||
valueField: 'id',
|
||||
placeholder: '请选择单位',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'status',
|
||||
label: '状态',
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
|
||||
buttonStyle: 'solid',
|
||||
optionType: 'button',
|
||||
},
|
||||
rules: z.number().default(CommonStatusEnum.ENABLE),
|
||||
},
|
||||
{
|
||||
fieldName: 'standard',
|
||||
label: '规格',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入规格',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'expiryDay',
|
||||
label: '保质期天数',
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
placeholder: '请输入保质期天数',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'weight',
|
||||
label: '重量(kg)',
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
placeholder: '请输入重量(kg)',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'purchasePrice',
|
||||
label: '采购价格',
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
placeholder: '请输入采购价格,单位:元',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'salePrice',
|
||||
label: '销售价格',
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
placeholder: '请输入销售价格,单位:元',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'minPrice',
|
||||
label: '最低价格',
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
placeholder: '请输入最低价格,单位:元',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'remark',
|
||||
label: '备注',
|
||||
component: 'Textarea',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '名称',
|
||||
component: 'Input',
|
||||
},
|
||||
{
|
||||
fieldName: 'categoryId',
|
||||
label: '分类',
|
||||
component: 'ApiTreeSelect',
|
||||
componentProps: {
|
||||
api: async () => {
|
||||
const data = await getProductCategorySimpleList();
|
||||
return handleTree(data);
|
||||
},
|
||||
|
||||
labelField: 'name',
|
||||
valueField: 'id',
|
||||
childrenField: 'children',
|
||||
placeholder: '请选择分类',
|
||||
treeDefaultExpandAll: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'barCode',
|
||||
title: '条码',
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
title: '名称',
|
||||
},
|
||||
{
|
||||
field: 'standard',
|
||||
title: '规格',
|
||||
},
|
||||
{
|
||||
field: 'categoryName',
|
||||
title: '分类',
|
||||
},
|
||||
{
|
||||
field: 'unitName',
|
||||
title: '单位',
|
||||
},
|
||||
{
|
||||
field: 'purchasePrice',
|
||||
title: '采购价格',
|
||||
},
|
||||
{
|
||||
field: 'salePrice',
|
||||
title: '销售价格',
|
||||
},
|
||||
{
|
||||
field: 'minPrice',
|
||||
title: '最低价格',
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
title: '状态',
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.COMMON_STATUS },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '创建时间',
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 130,
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -1,34 +1,152 @@
|
|||
<script lang="ts" setup>
|
||||
import { DocAlert, Page } from '@vben/common-ui';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { ErpProductApi } from '#/api/erp/product/product';
|
||||
|
||||
import { Button } from 'ant-design-vue';
|
||||
import { DocAlert, Page, useVbenModal } from '@vben/common-ui';
|
||||
import { downloadFileFromBlobPart } from '@vben/utils';
|
||||
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
deleteProduct,
|
||||
exportProduct,
|
||||
getProductPage,
|
||||
} from '#/api/erp/product/product';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useGridColumns, useGridFormSchema } from './data';
|
||||
import Form from './modules/form.vue';
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: Form,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 刷新表格 */
|
||||
function onRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 导出表格 */
|
||||
async function handleExport() {
|
||||
const data = await exportProduct(await gridApi.formApi.getValues());
|
||||
downloadFileFromBlobPart({ fileName: '产品信息.xls', source: data });
|
||||
}
|
||||
|
||||
/** 创建产品 */
|
||||
function handleCreate() {
|
||||
formModalApi.setData(null).open();
|
||||
}
|
||||
|
||||
/** 编辑产品 */
|
||||
function handleEdit(row: ErpProductApi.Product) {
|
||||
formModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 删除产品 */
|
||||
async function handleDelete(row: ErpProductApi.Product) {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.deleting', [row.name]),
|
||||
key: 'action_key_msg',
|
||||
});
|
||||
try {
|
||||
await deleteProduct(row.id as number);
|
||||
message.success({
|
||||
content: $t('ui.actionMessage.deleteSuccess', [row.name]),
|
||||
key: 'action_key_msg',
|
||||
});
|
||||
onRefresh();
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getProductPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: true,
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<ErpProductApi.Product>,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page>
|
||||
<Page auto-content-height>
|
||||
<template #doc>
|
||||
<DocAlert
|
||||
title="【产品】产品信息、分类、单位"
|
||||
url="https://doc.iocoder.cn/erp/product/"
|
||||
/>
|
||||
</template>
|
||||
<Button
|
||||
danger
|
||||
type="link"
|
||||
target="_blank"
|
||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
|
||||
>
|
||||
该功能支持 Vue3 + element-plus 版本!
|
||||
</Button>
|
||||
<br />
|
||||
<Button
|
||||
type="link"
|
||||
target="_blank"
|
||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/erp/product/product/index"
|
||||
>
|
||||
可参考
|
||||
https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/erp/product/product/index
|
||||
代码,pull request 贡献给我们!
|
||||
</Button>
|
||||
<FormModal @success="onRefresh" />
|
||||
<Grid table-title="产品列表">
|
||||
<template #toolbar-tools>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('ui.actionTitle.create', ['产品']),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.ADD,
|
||||
auth: ['erp:product:create'],
|
||||
onClick: handleCreate,
|
||||
},
|
||||
{
|
||||
label: $t('ui.actionTitle.export'),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.DOWNLOAD,
|
||||
auth: ['erp:product:export'],
|
||||
onClick: handleExport,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('common.edit'),
|
||||
type: 'link',
|
||||
icon: ACTION_ICON.EDIT,
|
||||
auth: ['erp:product:update'],
|
||||
onClick: handleEdit.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: $t('common.delete'),
|
||||
type: 'link',
|
||||
danger: true,
|
||||
icon: ACTION_ICON.DELETE,
|
||||
auth: ['erp:product:delete'],
|
||||
popConfirm: {
|
||||
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
|
||||
confirm: handleDelete.bind(null, row),
|
||||
},
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,86 @@
|
|||
<script lang="ts" setup>
|
||||
import type { ErpProductApi } from '#/api/erp/product/product';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import {
|
||||
createProduct,
|
||||
getProduct,
|
||||
updateProduct,
|
||||
} from '#/api/erp/product/product';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useFormSchema } from '../data';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const formData = ref<ErpProductApi.Product>();
|
||||
const getTitle = computed(() => {
|
||||
return formData.value?.id
|
||||
? $t('ui.actionTitle.edit', ['产品'])
|
||||
: $t('ui.actionTitle.create', ['产品']);
|
||||
});
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
},
|
||||
// 一共2列
|
||||
wrapperClass: 'grid-cols-2',
|
||||
layout: 'horizontal',
|
||||
schema: useFormSchema(),
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
// 提交表单
|
||||
const data = (await formApi.getValues()) as ErpProductApi.Product;
|
||||
try {
|
||||
await (formData.value?.id ? updateProduct(data) : createProduct(data));
|
||||
// 关闭并提示
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
message.success($t('ui.actionMessage.operationSuccess'));
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
formData.value = undefined;
|
||||
return;
|
||||
}
|
||||
// 加载数据
|
||||
const data = modalApi.getData<ErpProductApi.Product>();
|
||||
if (!data || !data.id) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
try {
|
||||
formData.value = await getProduct(data.id as number);
|
||||
// 设置到 values
|
||||
await formApi.setValues(formData.value);
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal class="w-2/5" :title="getTitle">
|
||||
<Form class="mx-4" />
|
||||
</Modal>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
import { z } from '#/adapter/form';
|
||||
import { CommonStatusEnum, DICT_TYPE, getDictOptions } from '#/utils';
|
||||
|
||||
/** 新增/修改的表单 */
|
||||
export function useFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'id',
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
show: () => false,
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'name',
|
||||
label: '单位名称',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'status',
|
||||
label: '单位状态',
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
|
||||
buttonStyle: 'solid',
|
||||
optionType: 'button',
|
||||
},
|
||||
rules: z.number().default(CommonStatusEnum.ENABLE),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '单位名称',
|
||||
component: 'Input',
|
||||
},
|
||||
{
|
||||
fieldName: 'status',
|
||||
label: '单位状态',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'id',
|
||||
title: '单位编号',
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
title: '单位名称',
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
title: '单位状态',
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.COMMON_STATUS },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '创建时间',
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 130,
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -1,34 +1,152 @@
|
|||
<script lang="ts" setup>
|
||||
import { DocAlert, Page } from '@vben/common-ui';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { ErpProductUnitApi } from '#/api/erp/product/unit';
|
||||
|
||||
import { Button } from 'ant-design-vue';
|
||||
import { DocAlert, Page, useVbenModal } from '@vben/common-ui';
|
||||
import { downloadFileFromBlobPart } from '@vben/utils';
|
||||
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
deleteProductUnit,
|
||||
exportProductUnit,
|
||||
getProductUnitPage,
|
||||
} from '#/api/erp/product/unit';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useGridColumns, useGridFormSchema } from './data';
|
||||
import Form from './modules/form.vue';
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: Form,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 刷新表格 */
|
||||
function onRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 导出表格 */
|
||||
async function handleExport() {
|
||||
const data = await exportProductUnit(await gridApi.formApi.getValues());
|
||||
downloadFileFromBlobPart({ fileName: '产品单位.xls', source: data });
|
||||
}
|
||||
|
||||
/** 创建产品单位 */
|
||||
function handleCreate() {
|
||||
formModalApi.setData(null).open();
|
||||
}
|
||||
|
||||
/** 编辑产品单位 */
|
||||
function handleEdit(row: ErpProductUnitApi.ProductUnit) {
|
||||
formModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 删除产品单位 */
|
||||
async function handleDelete(row: ErpProductUnitApi.ProductUnit) {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.deleting', [row.name]),
|
||||
key: 'action_key_msg',
|
||||
});
|
||||
try {
|
||||
await deleteProductUnit(row.id as number);
|
||||
message.success({
|
||||
content: $t('ui.actionMessage.deleteSuccess', [row.name]),
|
||||
key: 'action_key_msg',
|
||||
});
|
||||
onRefresh();
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getProductUnitPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: true,
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<ErpProductUnitApi.ProductUnit>,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page>
|
||||
<Page auto-content-height>
|
||||
<template #doc>
|
||||
<DocAlert
|
||||
title="【产品】产品信息、分类、单位"
|
||||
url="https://doc.iocoder.cn/erp/product/"
|
||||
/>
|
||||
</template>
|
||||
<Button
|
||||
danger
|
||||
type="link"
|
||||
target="_blank"
|
||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
|
||||
>
|
||||
该功能支持 Vue3 + element-plus 版本!
|
||||
</Button>
|
||||
<br />
|
||||
<Button
|
||||
type="link"
|
||||
target="_blank"
|
||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/erp/product/unit/index"
|
||||
>
|
||||
可参考
|
||||
https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/erp/product/unit/index
|
||||
代码,pull request 贡献给我们!
|
||||
</Button>
|
||||
<FormModal @success="onRefresh" />
|
||||
<Grid table-title="产品单位列表">
|
||||
<template #toolbar-tools>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('ui.actionTitle.create', ['产品单位']),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.ADD,
|
||||
auth: ['erp:product-unit:create'],
|
||||
onClick: handleCreate,
|
||||
},
|
||||
{
|
||||
label: $t('ui.actionTitle.export'),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.DOWNLOAD,
|
||||
auth: ['erp:product-unit:export'],
|
||||
onClick: handleExport,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('common.edit'),
|
||||
type: 'link',
|
||||
icon: ACTION_ICON.EDIT,
|
||||
auth: ['erp:product-unit:update'],
|
||||
onClick: handleEdit.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: $t('common.delete'),
|
||||
type: 'link',
|
||||
danger: true,
|
||||
icon: ACTION_ICON.DELETE,
|
||||
auth: ['erp:product-unit:delete'],
|
||||
popConfirm: {
|
||||
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
|
||||
confirm: handleDelete.bind(null, row),
|
||||
},
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,88 @@
|
|||
<script lang="ts" setup>
|
||||
import type { ErpProductUnitApi } from '#/api/erp/product/unit';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import {
|
||||
createProductUnit,
|
||||
getProductUnit,
|
||||
updateProductUnit,
|
||||
} from '#/api/erp/product/unit';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useFormSchema } from '../data';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const formData = ref<ErpProductUnitApi.ProductUnit>();
|
||||
const getTitle = computed(() => {
|
||||
return formData.value?.id
|
||||
? $t('ui.actionTitle.edit', ['产品单位'])
|
||||
: $t('ui.actionTitle.create', ['产品单位']);
|
||||
});
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
formItemClass: 'col-span-2',
|
||||
labelWidth: 80,
|
||||
},
|
||||
layout: 'horizontal',
|
||||
schema: useFormSchema(),
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
// 提交表单
|
||||
const data = (await formApi.getValues()) as ErpProductUnitApi.ProductUnit;
|
||||
try {
|
||||
await (formData.value?.id
|
||||
? updateProductUnit(data)
|
||||
: createProductUnit(data));
|
||||
// 关闭并提示
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
message.success($t('ui.actionMessage.operationSuccess'));
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
formData.value = undefined;
|
||||
return;
|
||||
}
|
||||
// 加载数据
|
||||
const data = modalApi.getData<ErpProductUnitApi.ProductUnit>();
|
||||
if (!data || !data.id) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
try {
|
||||
formData.value = await getProductUnit(data.id as number);
|
||||
// 设置到 values
|
||||
await formApi.setValues(formData.value);
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal class="w-2/5" :title="getTitle">
|
||||
<Form class="mx-4" />
|
||||
</Modal>
|
||||
</template>
|
||||
|
|
@ -114,7 +114,11 @@ const [Modal, modalApi] = useVbenModal({
|
|||
// 提交表单
|
||||
const data =
|
||||
(await formApi.getValues()) as ErpPurchaseOrderApi.PurchaseOrder;
|
||||
data.items = formData.value?.items;
|
||||
data.items = formData.value?.items?.map((item) => ({
|
||||
...item,
|
||||
// 解决新增采购订单报错
|
||||
id: undefined,
|
||||
}));
|
||||
// 将文件数组转换为字符串
|
||||
if (data.fileUrl && Array.isArray(data.fileUrl)) {
|
||||
data.fileUrl = data.fileUrl.length > 0 ? data.fileUrl[0] : '';
|
||||
|
|
|
|||
|
|
@ -88,7 +88,10 @@ const [Modal, modalApi] = useVbenModal({
|
|||
modalApi.lock();
|
||||
// 提交表单
|
||||
const data = (await formApi.getValues()) as ErpStockInApi.StockIn;
|
||||
data.items = formData.value?.items;
|
||||
data.items = formData.value?.items?.map((item) => ({
|
||||
...item,
|
||||
id: undefined,
|
||||
}));
|
||||
// 将文件数组转换为字符串
|
||||
if (data.fileUrl && Array.isArray(data.fileUrl)) {
|
||||
data.fileUrl = data.fileUrl.length > 0 ? data.fileUrl[0] : '';
|
||||
|
|
|
|||
|
|
@ -0,0 +1,329 @@
|
|||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
import { createRequiredValidation } from '#/adapter/vxe-table';
|
||||
import { getProductSimpleList } from '#/api/erp/product/product';
|
||||
import { getCustomerSimpleList } from '#/api/erp/sale/customer';
|
||||
import { getSimpleUserList } from '#/api/system/user';
|
||||
import { DICT_TYPE, getDictOptions } from '#/utils';
|
||||
|
||||
/** 表单的配置项 */
|
||||
export function useFormSchema(formType: string): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
style: { display: 'none' },
|
||||
},
|
||||
fieldName: 'id',
|
||||
label: 'ID',
|
||||
hideLabel: true,
|
||||
formItemClass: 'hidden',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '系统自动生成',
|
||||
disabled: true,
|
||||
},
|
||||
fieldName: 'no',
|
||||
label: '出库单号',
|
||||
disabled: formType === 'detail',
|
||||
},
|
||||
{
|
||||
component: 'DatePicker',
|
||||
componentProps: {
|
||||
placeholder: '选择出库时间',
|
||||
showTime: true,
|
||||
format: 'YYYY-MM-DD HH:mm:ss',
|
||||
valueFormat: 'x',
|
||||
style: { width: '100%' },
|
||||
},
|
||||
disabled: formType === 'detail',
|
||||
fieldName: 'outTime',
|
||||
label: '出库时间',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'ApiSelect',
|
||||
componentProps: {
|
||||
placeholder: '请选择客户',
|
||||
allowClear: true,
|
||||
showSearch: true,
|
||||
api: getCustomerSimpleList,
|
||||
fieldNames: {
|
||||
label: 'name',
|
||||
value: 'id',
|
||||
},
|
||||
},
|
||||
disabled: formType === 'detail',
|
||||
fieldName: 'customerId',
|
||||
label: '客户',
|
||||
},
|
||||
|
||||
{
|
||||
component: 'Textarea',
|
||||
componentProps: {
|
||||
placeholder: '请输入备注',
|
||||
autoSize: { minRows: 2, maxRows: 4 },
|
||||
class: 'w-full',
|
||||
},
|
||||
disabled: formType === 'detail',
|
||||
fieldName: 'remark',
|
||||
label: '备注',
|
||||
formItemClass: 'col-span-3',
|
||||
},
|
||||
{
|
||||
component: 'FileUpload',
|
||||
disabled: formType === 'detail',
|
||||
componentProps: {
|
||||
maxNumber: 1,
|
||||
maxSize: 10,
|
||||
accept: [
|
||||
'pdf',
|
||||
'doc',
|
||||
'docx',
|
||||
'xls',
|
||||
'xlsx',
|
||||
'txt',
|
||||
'jpg',
|
||||
'jpeg',
|
||||
'png',
|
||||
],
|
||||
showDescription: true,
|
||||
},
|
||||
fieldName: 'fileUrl',
|
||||
label: '附件',
|
||||
formItemClass: 'col-span-3',
|
||||
},
|
||||
{
|
||||
fieldName: 'product',
|
||||
disabled: formType === 'detail',
|
||||
label: '产品清单',
|
||||
component: 'Input',
|
||||
formItemClass: 'col-span-3',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 出库产品清单表格列定义 */
|
||||
export function useStockInItemTableColumns(
|
||||
isValidating?: any,
|
||||
): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{ type: 'seq', title: '序号', minWidth: 50, fixed: 'left' },
|
||||
{
|
||||
field: 'warehouseId',
|
||||
title: '仓库名称',
|
||||
minWidth: 150,
|
||||
slots: { default: 'warehouseId' },
|
||||
className: createRequiredValidation(isValidating, 'warehouseId'),
|
||||
},
|
||||
{
|
||||
field: 'productId',
|
||||
title: '产品名称',
|
||||
minWidth: 200,
|
||||
slots: { default: 'productId' },
|
||||
className: createRequiredValidation(isValidating, 'productId'),
|
||||
},
|
||||
{
|
||||
field: 'stockCount',
|
||||
title: '库存',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
field: 'productBarCode',
|
||||
title: '条码',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'productUnitName',
|
||||
title: '单位',
|
||||
minWidth: 80,
|
||||
},
|
||||
{
|
||||
field: 'count',
|
||||
title: '数量',
|
||||
minWidth: 120,
|
||||
slots: { default: 'count' },
|
||||
className: createRequiredValidation(isValidating, 'count'),
|
||||
},
|
||||
{
|
||||
field: 'productPrice',
|
||||
title: '产品单价',
|
||||
minWidth: 120,
|
||||
slots: { default: 'productPrice' },
|
||||
},
|
||||
{
|
||||
field: 'totalPrice',
|
||||
title: '金额',
|
||||
minWidth: 120,
|
||||
formatter: 'formatAmount2',
|
||||
},
|
||||
{
|
||||
field: 'remark',
|
||||
title: '备注',
|
||||
minWidth: 150,
|
||||
slots: { default: 'remark' },
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 50,
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'no',
|
||||
label: '出库单号',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入出库单号',
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'productId',
|
||||
label: '产品',
|
||||
component: 'ApiSelect',
|
||||
componentProps: {
|
||||
placeholder: '请选择产品',
|
||||
allowClear: true,
|
||||
showSearch: true,
|
||||
api: getProductSimpleList,
|
||||
labelField: 'name',
|
||||
valueField: 'id',
|
||||
filterOption: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'outTime',
|
||||
label: '出库时间',
|
||||
component: 'RangePicker',
|
||||
componentProps: {
|
||||
placeholder: ['开始日期', '结束日期'],
|
||||
showTime: true,
|
||||
format: 'YYYY-MM-DD HH:mm:ss',
|
||||
valueFormat: 'YYYY-MM-DD HH:mm:ss',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'customerId',
|
||||
label: '客户',
|
||||
component: 'ApiSelect',
|
||||
componentProps: {
|
||||
placeholder: '请选择客户',
|
||||
allowClear: true,
|
||||
showSearch: true,
|
||||
api: getCustomerSimpleList,
|
||||
labelField: 'name',
|
||||
valueField: 'id',
|
||||
filterOption: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'status',
|
||||
label: '状态',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
placeholder: '请选择状态',
|
||||
allowClear: true,
|
||||
options: getDictOptions(DICT_TYPE.ERP_AUDIT_STATUS, 'number'),
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'remark',
|
||||
label: '备注',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入备注',
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'creator',
|
||||
label: '创建人',
|
||||
component: 'ApiSelect',
|
||||
componentProps: {
|
||||
placeholder: '请选择创建人',
|
||||
allowClear: true,
|
||||
showSearch: true,
|
||||
api: getSimpleUserList,
|
||||
labelField: 'nickname',
|
||||
valueField: 'id',
|
||||
filterOption: false,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
type: 'checkbox',
|
||||
width: 50,
|
||||
fixed: 'left',
|
||||
},
|
||||
{
|
||||
field: 'no',
|
||||
title: '出库单号',
|
||||
minWidth: 180,
|
||||
},
|
||||
{
|
||||
field: 'productNames',
|
||||
title: '产品信息',
|
||||
minWidth: 200,
|
||||
showOverflow: 'tooltip',
|
||||
},
|
||||
{
|
||||
field: 'customerName',
|
||||
title: '客户',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'outTime',
|
||||
title: '出库时间',
|
||||
minWidth: 180,
|
||||
cellRender: {
|
||||
name: 'CellDateTime',
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'creatorName',
|
||||
title: '创建人',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
field: 'totalCount',
|
||||
title: '数量',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
field: 'totalPrice',
|
||||
title: '价格',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
title: '状态',
|
||||
minWidth: 90,
|
||||
fixed: 'right',
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.ERP_AUDIT_STATUS },
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 300,
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -1,34 +1,219 @@
|
|||
<script lang="ts" setup>
|
||||
import { DocAlert, Page } from '@vben/common-ui';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { ErpStockOutApi } from '#/api/erp/stock/out';
|
||||
|
||||
import { Button } from 'ant-design-vue';
|
||||
import { DocAlert, Page, useVbenModal } from '@vben/common-ui';
|
||||
import { downloadFileFromBlobPart } from '@vben/utils';
|
||||
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
deleteStockOut,
|
||||
exportStockOut,
|
||||
getStockOutPage,
|
||||
updateStockOutStatus,
|
||||
} from '#/api/erp/stock/out';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useGridColumns, useGridFormSchema } from './data';
|
||||
import StockInForm from './modules/form.vue';
|
||||
|
||||
/** 其它出库单管理 */
|
||||
defineOptions({ name: 'ErpStockOut' });
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: StockInForm,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 刷新表格 */
|
||||
function onRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 导出出库单 */
|
||||
async function handleExport() {
|
||||
const data = await exportStockOut(await gridApi.formApi.getValues());
|
||||
downloadFileFromBlobPart({ fileName: '其它出库单.xls', source: data });
|
||||
}
|
||||
|
||||
/** 新增/编辑/详情 */
|
||||
function openForm(type: string, id?: number) {
|
||||
formModalApi.setData({ type, id }).open();
|
||||
}
|
||||
|
||||
/** 删除 */
|
||||
async function handleDelete(ids: any[]) {
|
||||
const hideLoading = message.loading({
|
||||
content: '删除中...',
|
||||
duration: 0,
|
||||
key: 'action_process_msg',
|
||||
});
|
||||
try {
|
||||
await deleteStockOut(ids);
|
||||
message.success({
|
||||
content: '删除成功',
|
||||
key: 'action_process_msg',
|
||||
});
|
||||
onRefresh();
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
/** 审核/反审核 */
|
||||
async function handleUpdateStatus(id: any, status: number) {
|
||||
const statusText = status === 20 ? '审核' : '反审核';
|
||||
const hideLoading = message.loading({
|
||||
content: `${statusText}中...`,
|
||||
duration: 0,
|
||||
key: 'action_process_msg',
|
||||
});
|
||||
try {
|
||||
await updateStockOutStatus({ id, status });
|
||||
message.success({
|
||||
content: `${statusText}成功`,
|
||||
key: 'action_process_msg',
|
||||
});
|
||||
onRefresh();
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getStockOutPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: true,
|
||||
search: true,
|
||||
},
|
||||
checkboxConfig: {
|
||||
reserve: true,
|
||||
},
|
||||
} as VxeTableGridOptions<ErpStockOutApi.StockOut>,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page>
|
||||
<Page auto-content-height>
|
||||
<template #doc>
|
||||
<DocAlert
|
||||
title="【库存】其它入库、其它出库"
|
||||
url="https://doc.iocoder.cn/erp/stock-in-out/"
|
||||
/>
|
||||
</template>
|
||||
<Button
|
||||
danger
|
||||
type="link"
|
||||
target="_blank"
|
||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
|
||||
>
|
||||
该功能支持 Vue3 + element-plus 版本!
|
||||
</Button>
|
||||
<br />
|
||||
<Button
|
||||
type="link"
|
||||
target="_blank"
|
||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/erp/stock/out/index"
|
||||
>
|
||||
可参考
|
||||
https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/erp/stock/out/index
|
||||
代码,pull request 贡献给我们!
|
||||
</Button>
|
||||
<Grid table-title="其它出库单列表">
|
||||
<template #toolbar-tools>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('ui.actionTitle.create', ['其它出库']),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.ADD,
|
||||
auth: ['erp:stock-out:create'],
|
||||
onClick: () => openForm('create'),
|
||||
},
|
||||
{
|
||||
label: $t('ui.actionTitle.export'),
|
||||
type: 'default',
|
||||
icon: ACTION_ICON.DOWNLOAD,
|
||||
auth: ['erp:stock-out:export'],
|
||||
onClick: handleExport,
|
||||
},
|
||||
{
|
||||
label: '批量删除',
|
||||
type: 'default',
|
||||
danger: true,
|
||||
icon: ACTION_ICON.DELETE,
|
||||
auth: ['erp:stock-out:delete'],
|
||||
popConfirm: {
|
||||
title: '是否删除所选中数据?',
|
||||
confirm: () => {
|
||||
const checkboxRecords = gridApi.grid.getCheckboxRecords();
|
||||
if (checkboxRecords.length === 0) {
|
||||
message.warning('请选择要删除的数据');
|
||||
return;
|
||||
}
|
||||
handleDelete(checkboxRecords.map((item) => item.id));
|
||||
},
|
||||
},
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '详情',
|
||||
icon: ACTION_ICON.VIEW,
|
||||
auth: ['erp:stock-out:query'],
|
||||
onClick: () => openForm('detail', row.id),
|
||||
},
|
||||
{
|
||||
label: '编辑',
|
||||
auth: ['erp:stock-out:update'],
|
||||
icon: ACTION_ICON.EDIT,
|
||||
disabled: row.status !== 10,
|
||||
onClick: () => openForm('update', row.id),
|
||||
},
|
||||
{
|
||||
label: '审核',
|
||||
auth: ['erp:stock-out:update'],
|
||||
ifShow: row.status === 10,
|
||||
popConfirm: {
|
||||
title: '确认要审核该入库单吗?',
|
||||
confirm: () => handleUpdateStatus(row.id, 20),
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '反审核',
|
||||
danger: true,
|
||||
auth: ['erp:stock-out:update'],
|
||||
ifShow: row.status === 20,
|
||||
popConfirm: {
|
||||
title: '确认要反审核该入库单吗?',
|
||||
confirm: () => handleUpdateStatus(row.id, 10),
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '删除',
|
||||
danger: true,
|
||||
auth: ['erp:stock-out:delete'],
|
||||
disabled: row.status !== 10,
|
||||
popConfirm: {
|
||||
title: '确认要删除该出库单吗?',
|
||||
confirm: () => handleDelete([row.id]),
|
||||
},
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
|
||||
<!-- 表单弹窗 -->
|
||||
<FormModal @success="onRefresh" />
|
||||
</Page>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,196 @@
|
|||
<script lang="ts" setup>
|
||||
import type { ErpStockOutApi } from '#/api/erp/stock/out';
|
||||
|
||||
import { computed, nextTick, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import {
|
||||
createStockOut,
|
||||
getStockOut,
|
||||
updateStockOut,
|
||||
updateStockOutStatus,
|
||||
} from '#/api/erp/stock/out';
|
||||
|
||||
import { useFormSchema } from '../data';
|
||||
import StockInItemForm from './stock-out-item-form.vue';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const formData = ref<ErpStockOutApi.StockOut>();
|
||||
const formType = ref('');
|
||||
const itemFormRef = ref();
|
||||
|
||||
const getTitle = computed(() => {
|
||||
if (formType.value === 'create') return '添加其它出库单';
|
||||
if (formType.value === 'update') return '编辑其它出库单';
|
||||
return '其它出库单详情';
|
||||
});
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
labelWidth: 120,
|
||||
},
|
||||
wrapperClass: 'grid-cols-3',
|
||||
layout: 'vertical',
|
||||
schema: useFormSchema(formType.value),
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
const handleUpdateItems = (items: ErpStockOutApi.StockOutItem[]) => {
|
||||
formData.value = modalApi.getData<ErpStockOutApi.StockOut>();
|
||||
if (formData.value) {
|
||||
formData.value.items = items;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 创建或更新其它出库单
|
||||
*/
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
await nextTick();
|
||||
|
||||
const itemFormInstance = Array.isArray(itemFormRef.value)
|
||||
? itemFormRef.value[0]
|
||||
: itemFormRef.value;
|
||||
if (itemFormInstance && typeof itemFormInstance.validate === 'function') {
|
||||
try {
|
||||
const isValid = await itemFormInstance.validate();
|
||||
if (!isValid) {
|
||||
message.error('产品清单验证失败,请检查必填项');
|
||||
return;
|
||||
}
|
||||
} catch (error: any) {
|
||||
message.error(error.message || '产品清单验证失败');
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
message.error('产品清单验证方法不存在');
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证产品清单不能为空
|
||||
if (!formData.value?.items || formData.value.items.length === 0) {
|
||||
message.error('产品清单不能为空,请至少添加一个产品');
|
||||
return;
|
||||
}
|
||||
|
||||
modalApi.lock();
|
||||
// 提交表单
|
||||
const data = (await formApi.getValues()) as ErpStockOutApi.StockOut;
|
||||
data.items = formData.value?.items?.map((item) => ({
|
||||
...item,
|
||||
id: undefined,
|
||||
}));
|
||||
// 将文件数组转换为字符串
|
||||
if (data.fileUrl && Array.isArray(data.fileUrl)) {
|
||||
data.fileUrl = data.fileUrl.length > 0 ? data.fileUrl[0] : '';
|
||||
}
|
||||
try {
|
||||
await (formType.value === 'create'
|
||||
? createStockOut(data)
|
||||
: updateStockOut(data));
|
||||
// 关闭并提示
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
message.success(formType.value === 'create' ? '新增成功' : '更新成功');
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
formData.value = undefined;
|
||||
return;
|
||||
}
|
||||
// 加载数据
|
||||
const data = modalApi.getData<{ id?: number; type: string }>();
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
formType.value = data.type;
|
||||
formApi.updateSchema(useFormSchema(formType.value));
|
||||
if (!data.id) {
|
||||
// 初始化空的表单数据
|
||||
formData.value = { items: [] } as unknown as ErpStockOutApi.StockOut;
|
||||
await nextTick();
|
||||
const itemFormInstance = Array.isArray(itemFormRef.value)
|
||||
? itemFormRef.value[0]
|
||||
: itemFormRef.value;
|
||||
if (itemFormInstance && typeof itemFormInstance.init === 'function') {
|
||||
itemFormInstance.init([]);
|
||||
}
|
||||
// 如果是新增,自动添加一行
|
||||
if (formType.value === 'create' && itemFormInstance) {
|
||||
itemFormInstance.handleAdd();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
modalApi.lock();
|
||||
try {
|
||||
formData.value = await getStockOut(data.id);
|
||||
// 设置到 values
|
||||
await formApi.setValues(formData.value);
|
||||
// 初始化子表单
|
||||
await nextTick();
|
||||
const itemFormInstance = Array.isArray(itemFormRef.value)
|
||||
? itemFormRef.value[0]
|
||||
: itemFormRef.value;
|
||||
if (itemFormInstance && typeof itemFormInstance.init === 'function') {
|
||||
itemFormInstance.init(formData.value.items || []);
|
||||
}
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
/** 审核/反审核 */
|
||||
async function handleUpdateStatus(id: number, status: number) {
|
||||
try {
|
||||
await updateStockOutStatus({ id, status });
|
||||
message.success(status === 20 ? '审核成功' : '反审核成功');
|
||||
emit('success');
|
||||
await modalApi.close();
|
||||
} catch (error: any) {
|
||||
message.error(error.message || '操作失败');
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({ modalApi, handleUpdateStatus });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal
|
||||
v-bind="$attrs"
|
||||
:title="getTitle"
|
||||
class="w-4/5"
|
||||
:closable="true"
|
||||
:mask-closable="true"
|
||||
:show-confirm-button="formType !== 'detail'"
|
||||
>
|
||||
<Form class="mx-3">
|
||||
<template #product="slotProps">
|
||||
<StockInItemForm
|
||||
v-bind="slotProps"
|
||||
ref="itemFormRef"
|
||||
class="w-full"
|
||||
:items="formData?.items ?? []"
|
||||
:disabled="formType === 'detail'"
|
||||
@update:items="handleUpdateItems"
|
||||
/>
|
||||
</template>
|
||||
</Form>
|
||||
</Modal>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,367 @@
|
|||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { ErpStockOutApi } from '#/api/erp/stock/out';
|
||||
|
||||
import { nextTick, onMounted, ref, watch } from 'vue';
|
||||
|
||||
import { erpPriceMultiply } from '@vben/utils';
|
||||
|
||||
import { Input, InputNumber, Select } from 'ant-design-vue';
|
||||
|
||||
import { TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getProductSimpleList } from '#/api/erp/product/product';
|
||||
import { getStockCount } from '#/api/erp/stock/stock';
|
||||
import { getWarehouseSimpleList } from '#/api/erp/stock/warehouse';
|
||||
|
||||
import { useStockInItemTableColumns } from '../data';
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
items: () => [],
|
||||
disabled: false,
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:items']);
|
||||
|
||||
interface Props {
|
||||
items?: ErpStockOutApi.StockOutItem[];
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const tableData = ref<ErpStockOutApi.StockOutItem[]>([]);
|
||||
const productOptions = ref<any[]>([]);
|
||||
const warehouseOptions = ref<any[]>([]);
|
||||
const isValidating = ref(false);
|
||||
|
||||
/** 表格配置 */
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
gridOptions: {
|
||||
editConfig: {
|
||||
trigger: 'click',
|
||||
mode: 'cell',
|
||||
},
|
||||
columns: useStockInItemTableColumns(isValidating),
|
||||
data: tableData.value,
|
||||
border: true,
|
||||
showOverflow: true,
|
||||
autoResize: true,
|
||||
minHeight: 250,
|
||||
keepSource: true,
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
},
|
||||
pagerConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
toolbarConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
showFooter: true,
|
||||
footerCellClassName: 'stock-in-footer-cell',
|
||||
footerMethod: ({
|
||||
columns,
|
||||
}: {
|
||||
columns: VxeTableGridOptions['columns'];
|
||||
}) => {
|
||||
const footers: any[][] = [];
|
||||
const sums = getSummaries();
|
||||
const footerData: any[] = [];
|
||||
columns!.forEach((column, columnIndex: number) => {
|
||||
if (columnIndex === 0) {
|
||||
footerData.push('合计');
|
||||
} else if (column.field === 'count') {
|
||||
footerData.push(sums.count);
|
||||
} else if (column.field === 'totalPrice') {
|
||||
footerData.push(sums.totalPrice);
|
||||
} else {
|
||||
footerData.push('');
|
||||
}
|
||||
});
|
||||
footers.push(footerData);
|
||||
return footers;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
/** 监听外部传入的列数据 */
|
||||
watch(
|
||||
() => props.items,
|
||||
async (items) => {
|
||||
if (!items) {
|
||||
return;
|
||||
}
|
||||
await nextTick();
|
||||
tableData.value = [...items];
|
||||
await nextTick();
|
||||
gridApi.grid.reloadData(tableData.value);
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
},
|
||||
);
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(async () => {
|
||||
productOptions.value = await getProductSimpleList();
|
||||
warehouseOptions.value = await getWarehouseSimpleList();
|
||||
});
|
||||
|
||||
function handleAdd() {
|
||||
const newRow = {
|
||||
warehouseId: undefined,
|
||||
productId: undefined,
|
||||
productName: '',
|
||||
productUnitId: undefined,
|
||||
productUnitName: '',
|
||||
productBarCode: '',
|
||||
count: 1,
|
||||
productPrice: 0,
|
||||
totalPrice: 0,
|
||||
stockCount: 0,
|
||||
remark: '',
|
||||
};
|
||||
tableData.value.push(newRow);
|
||||
gridApi.grid.insertAt(newRow, -1);
|
||||
emit('update:items', [...tableData.value]);
|
||||
// 触发表格重新渲染以更新cellClassName
|
||||
nextTick(() => {
|
||||
gridApi.grid.refreshColumn();
|
||||
});
|
||||
}
|
||||
|
||||
function handleDelete(row: ErpStockOutApi.StockOutItem) {
|
||||
gridApi.grid.remove(row);
|
||||
const index = tableData.value.findIndex((item) => item.id === row.id);
|
||||
if (index !== -1) {
|
||||
tableData.value.splice(index, 1);
|
||||
}
|
||||
emit('update:items', [...tableData.value]);
|
||||
}
|
||||
|
||||
async function handleWarehouseChange(warehouseId: any, row: any) {
|
||||
const warehouse = warehouseOptions.value.find((w) => w.id === warehouseId);
|
||||
if (!warehouse) {
|
||||
return;
|
||||
}
|
||||
|
||||
row.warehouseId = warehouseId;
|
||||
|
||||
// 如果已选择产品,重新获取库存
|
||||
if (row.productId) {
|
||||
const stockCount = await getStockCount(row.productId, warehouseId);
|
||||
row.stockCount = stockCount || 0;
|
||||
}
|
||||
|
||||
handleUpdateValue(row);
|
||||
}
|
||||
|
||||
async function handleProductChange(productId: any, row: any) {
|
||||
const product = productOptions.value.find((p) => p.id === productId);
|
||||
if (!product) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取库存数量
|
||||
const stockCount = row.warehouseId
|
||||
? await getStockCount(productId, row.warehouseId)
|
||||
: await getStockCount(productId);
|
||||
|
||||
row.productId = productId;
|
||||
row.productUnitId = product.unitId;
|
||||
row.productBarCode = product.barCode;
|
||||
row.productUnitName = product.unitName;
|
||||
row.productName = product.name;
|
||||
row.stockCount = stockCount || 0;
|
||||
row.productPrice = product.purchasePrice || 0;
|
||||
row.count = row.count || 1;
|
||||
|
||||
handlePriceChange(row);
|
||||
}
|
||||
|
||||
function handlePriceChange(row: any) {
|
||||
if (row.productPrice && row.count) {
|
||||
row.totalPrice = erpPriceMultiply(row.productPrice, row.count) ?? 0;
|
||||
}
|
||||
handleUpdateValue(row);
|
||||
}
|
||||
|
||||
function handleUpdateValue(row: any) {
|
||||
const index = tableData.value.findIndex((item) => item.id === row.id);
|
||||
if (index === -1) {
|
||||
tableData.value.push(row);
|
||||
} else {
|
||||
tableData.value[index] = row;
|
||||
}
|
||||
emit('update:items', [...tableData.value]);
|
||||
// 触发表格重新渲染以更新cellClassName
|
||||
nextTick(() => {
|
||||
gridApi.grid.refreshColumn();
|
||||
});
|
||||
}
|
||||
|
||||
const getSummaries = (): {
|
||||
count: number;
|
||||
totalPrice: number;
|
||||
} => {
|
||||
return {
|
||||
count: tableData.value.reduce((sum, item) => sum + (item.count || 0), 0),
|
||||
totalPrice: tableData.value.reduce(
|
||||
(sum, item) => sum + (item.totalPrice || 0),
|
||||
0,
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
/** 验证表单 */
|
||||
function validate(): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
isValidating.value = true;
|
||||
|
||||
// 触发表格重新渲染以显示验证错误
|
||||
nextTick(() => {
|
||||
gridApi.grid.refreshColumn();
|
||||
});
|
||||
|
||||
// 验证是否有产品清单
|
||||
if (!tableData.value || tableData.value.length === 0) {
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证每一行的必填字段
|
||||
for (const item of tableData.value) {
|
||||
if (
|
||||
!item.warehouseId ||
|
||||
!item.productId ||
|
||||
!item.count ||
|
||||
item.count <= 0
|
||||
) {
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 验证通过,清除验证状态
|
||||
isValidating.value = false;
|
||||
nextTick(() => {
|
||||
gridApi.grid.refreshColumn();
|
||||
});
|
||||
|
||||
resolve(true);
|
||||
});
|
||||
}
|
||||
|
||||
/** 初始化表格数据 */
|
||||
function init(items: ErpStockOutApi.StockOutItem[]) {
|
||||
tableData.value = items || [];
|
||||
gridApi.grid.reloadData(tableData.value);
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
validate,
|
||||
init,
|
||||
handleAdd,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full">
|
||||
<div class="mb-4 flex justify-between">
|
||||
<span class="text-lg font-medium"></span>
|
||||
</div>
|
||||
|
||||
<Grid>
|
||||
<template #warehouseId="{ row }">
|
||||
<Select
|
||||
v-model:value="row.warehouseId"
|
||||
:options="warehouseOptions"
|
||||
:field-names="{ label: 'name', value: 'id' }"
|
||||
placeholder="请选择仓库"
|
||||
:disabled="disabled"
|
||||
show-search
|
||||
@change="(value) => handleWarehouseChange(value, row)"
|
||||
class="w-full"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #productId="{ row }">
|
||||
<Select
|
||||
v-model:value="row.productId"
|
||||
:options="productOptions"
|
||||
:field-names="{ label: 'name', value: 'id' }"
|
||||
placeholder="请选择产品"
|
||||
:disabled="disabled"
|
||||
show-search
|
||||
@change="(value) => handleProductChange(value, row)"
|
||||
class="w-full"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #count="{ row }">
|
||||
<InputNumber
|
||||
v-model:value="row.count"
|
||||
:disabled="disabled"
|
||||
:min="0.001"
|
||||
:precision="3"
|
||||
@change="() => handlePriceChange(row)"
|
||||
class="w-full"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #productPrice="{ row }">
|
||||
<InputNumber
|
||||
v-model:value="row.productPrice"
|
||||
:disabled="disabled"
|
||||
:min="0.01"
|
||||
:precision="2"
|
||||
@change="() => handlePriceChange(row)"
|
||||
class="w-full"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #remark="{ row }">
|
||||
<Input
|
||||
v-model:value="row.remark"
|
||||
:disabled="disabled"
|
||||
placeholder="请输入备注"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
v-if="!disabled"
|
||||
:actions="[
|
||||
{
|
||||
label: '删除',
|
||||
type: 'link',
|
||||
danger: true,
|
||||
popConfirm: {
|
||||
title: '确认删除该产品吗?',
|
||||
confirm: handleDelete.bind(null, row),
|
||||
},
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #bottom>
|
||||
<TableAction
|
||||
v-if="!disabled"
|
||||
class="mt-4 flex justify-center"
|
||||
:actions="[
|
||||
{
|
||||
label: '添加产品',
|
||||
type: 'default',
|
||||
onClick: handleAdd,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
:deep(.vxe-table .vxe-footer--column.stock-in-footer-cell .vxe-cell) {
|
||||
background-color: #f5f5f5 !important;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -5,7 +5,7 @@ import type { DescriptionItemSchema } from '#/components/description';
|
|||
import { h } from 'vue';
|
||||
|
||||
import { JsonViewer } from '@vben/common-ui';
|
||||
import { formatDateTime, isObject } from '@vben/utils';
|
||||
import { formatDateTime } from '@vben/utils';
|
||||
|
||||
import { DictTag } from '#/components/dict-tag';
|
||||
import { DICT_TYPE, getDictOptions, getRangePickerDefaultProps } from '#/utils';
|
||||
|
|
@ -192,13 +192,13 @@ export function useDetailSchema(): DescriptionItemSchema[] {
|
|||
field: 'requestParams',
|
||||
label: '请求参数',
|
||||
content: (data) => {
|
||||
if (isObject(data.requestParams)) {
|
||||
if (data.requestParams) {
|
||||
return h(JsonViewer, {
|
||||
value: data.requestParams,
|
||||
value: JSON.parse(data.requestParams),
|
||||
previewMode: true,
|
||||
});
|
||||
}
|
||||
return data.requestParams;
|
||||
return '';
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -5,7 +5,9 @@ import type { DescriptionItemSchema } from '#/components/description';
|
|||
import { h } from 'vue';
|
||||
|
||||
import { JsonViewer } from '@vben/common-ui';
|
||||
import { formatDateTime, isObject } from '@vben/utils';
|
||||
import { formatDateTime } from '@vben/utils';
|
||||
|
||||
import { Textarea } from 'ant-design-vue';
|
||||
|
||||
import { DictTag } from '#/components/dict-tag';
|
||||
import {
|
||||
|
|
@ -177,13 +179,13 @@ export function useDetailSchema(): DescriptionItemSchema[] {
|
|||
field: 'requestParams',
|
||||
label: '请求参数',
|
||||
content: (data) => {
|
||||
if (isObject(data.requestParams)) {
|
||||
if (data.requestParams) {
|
||||
return h(JsonViewer, {
|
||||
value: data.requestParams,
|
||||
value: JSON.parse(data.requestParams),
|
||||
previewMode: true,
|
||||
});
|
||||
}
|
||||
return data.requestParams;
|
||||
return '';
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -201,13 +203,10 @@ export function useDetailSchema(): DescriptionItemSchema[] {
|
|||
field: 'exceptionStackTrace',
|
||||
label: '异常堆栈',
|
||||
content: (data) => {
|
||||
if (isObject(data.exceptionStackTrace)) {
|
||||
return h(JsonViewer, {
|
||||
value: data.exceptionStackTrace,
|
||||
previewMode: true,
|
||||
});
|
||||
}
|
||||
return data.exceptionStackTrace;
|
||||
return h(Textarea, {
|
||||
value: data.exceptionStackTrace,
|
||||
rows: 20,
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -200,6 +200,25 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||
},
|
||||
defaultValue: false,
|
||||
},
|
||||
{
|
||||
fieldName: 'config.enablePublicAccess',
|
||||
label: '公开访问',
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
options: [
|
||||
{ label: '公开', value: true },
|
||||
{ label: '私有', value: false },
|
||||
],
|
||||
buttonStyle: 'solid',
|
||||
optionType: 'button',
|
||||
},
|
||||
rules: 'required',
|
||||
dependencies: {
|
||||
triggerFields: ['storage'],
|
||||
show: (formValues) => formValues.storage === 20,
|
||||
},
|
||||
defaultValue: false,
|
||||
},
|
||||
// 通用
|
||||
{
|
||||
fieldName: 'config.domain',
|
||||
|
|
|
|||
|
|
@ -19,3 +19,5 @@ VITE_INJECT_APP_LOADING=true
|
|||
VITE_APP_DEFAULT_USERNAME=admin
|
||||
# 默认登录密码
|
||||
VITE_APP_DEFAULT_PASSWORD=admin123
|
||||
|
||||
VITE_MALL_H5_DOMAIN='http://mall.yudao.iocoder.cn'
|
||||
|
|
|
|||
|
|
@ -21,3 +21,5 @@ VITE_INJECT_APP_LOADING=true
|
|||
|
||||
# 打包后是否生成dist.zip
|
||||
VITE_ARCHIVER=true
|
||||
|
||||
VITE_MALL_H5_DOMAIN='http://mall.yudao.iocoder.cn'
|
||||
|
|
|
|||
|
|
@ -50,7 +50,9 @@
|
|||
"highlight.js": "catalog:",
|
||||
"pinia": "catalog:",
|
||||
"vue": "catalog:",
|
||||
"vue-router": "catalog:"
|
||||
"vue-dompurify-html": "catalog:",
|
||||
"vue-router": "catalog:",
|
||||
"vuedraggable": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"unplugin-element-plus": "catalog:"
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ export namespace InfraFileConfigApi {
|
|||
accessKey?: string;
|
||||
accessSecret?: string;
|
||||
pathStyle?: boolean;
|
||||
enablePublicAccess?: boolean;
|
||||
domain: string;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -48,6 +48,8 @@ export namespace MallCombinationActivityApi {
|
|||
combinationPrice?: number;
|
||||
/** 商品列表 */
|
||||
products: CombinationProduct[];
|
||||
/** 图片 */
|
||||
picUrl?: string;
|
||||
}
|
||||
|
||||
/** 扩展 SKU 配置 */
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ export function deleteDiyPage(id: number) {
|
|||
|
||||
/** 获得装修页面属性 */
|
||||
export function getDiyPageProperty(id: number) {
|
||||
return requestClient.get<string>(`/promotion/diy-page/get-property?id=${id}`);
|
||||
return requestClient.get(`/promotion/diy-page/get-property?id=${id}`);
|
||||
}
|
||||
|
||||
/** 更新装修页面属性 */
|
||||
|
|
|
|||
|
|
@ -55,6 +55,8 @@ export namespace MallSeckillActivityApi {
|
|||
seckillPrice?: number;
|
||||
/** 秒杀商品列表 */
|
||||
products?: SeckillProduct[];
|
||||
/** 图片 */
|
||||
picUrl?: string;
|
||||
}
|
||||
|
||||
/** 扩展 SKU 配置 */
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 3.5 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 8.7 KiB |
|
|
@ -1,4 +1,5 @@
|
|||
import { createApp, watchEffect } from 'vue';
|
||||
import VueDOMPurifyHTML from 'vue-dompurify-html';
|
||||
|
||||
import { registerAccessDirective } from '@vben/access';
|
||||
import { registerLoadingDirective } from '@vben/common-ui';
|
||||
|
|
@ -34,7 +35,7 @@ async function bootstrap(namespace: string) {
|
|||
// zIndex: 2000,
|
||||
// });
|
||||
const app = createApp(App);
|
||||
|
||||
app.use(VueDOMPurifyHTML);
|
||||
// 注册Element Plus提供的v-loading指令
|
||||
app.directive('loading', ElLoading.directive);
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,241 @@
|
|||
<script lang="ts" setup>
|
||||
import type { ButtonInstance, ScrollbarInstance } from 'element-plus';
|
||||
|
||||
import type { AppLink } from './data';
|
||||
|
||||
import { nextTick, ref } from 'vue';
|
||||
|
||||
import { getUrlNumberValue } from '@vben/utils';
|
||||
|
||||
import { ElScrollbar } from 'element-plus';
|
||||
|
||||
import ProductCategorySelect from '#/views/mall/product/category/components/product-category-select.vue';
|
||||
|
||||
import { APP_LINK_GROUP_LIST, APP_LINK_TYPE_ENUM } from './data';
|
||||
|
||||
// APP 链接选择弹框
|
||||
defineOptions({ name: 'AppLinkSelectDialog' });
|
||||
// 处理绑定值更新
|
||||
const emit = defineEmits<{
|
||||
appLinkChange: [appLink: AppLink];
|
||||
change: [link: string];
|
||||
}>();
|
||||
// 选中的分组,默认选中第一个
|
||||
const activeGroup = ref(APP_LINK_GROUP_LIST[0]?.name);
|
||||
// 选中的 APP 链接
|
||||
const activeAppLink = ref({} as AppLink);
|
||||
|
||||
/** 打开弹窗 */
|
||||
const dialogVisible = ref(false);
|
||||
const open = (link: string) => {
|
||||
activeAppLink.value.path = link;
|
||||
dialogVisible.value = true;
|
||||
// 滚动到当前的链接
|
||||
const group = APP_LINK_GROUP_LIST.find((group) =>
|
||||
group.links.some((linkItem) => {
|
||||
const sameLink = isSameLink(linkItem.path, link);
|
||||
if (sameLink) {
|
||||
activeAppLink.value = { ...linkItem, path: link };
|
||||
}
|
||||
return sameLink;
|
||||
}),
|
||||
);
|
||||
if (group) {
|
||||
// 使用 nextTick 的原因:可能 Dom 还没生成,导致滚动失败
|
||||
nextTick(() => handleGroupSelected(group.name));
|
||||
}
|
||||
};
|
||||
defineExpose({ open });
|
||||
|
||||
// 处理 APP 链接选中
|
||||
const handleAppLinkSelected = (appLink: AppLink) => {
|
||||
if (!isSameLink(appLink.path, activeAppLink.value.path)) {
|
||||
activeAppLink.value = appLink;
|
||||
}
|
||||
switch (appLink.type) {
|
||||
case APP_LINK_TYPE_ENUM.PRODUCT_CATEGORY_LIST: {
|
||||
detailSelectDialog.value.visible = true;
|
||||
detailSelectDialog.value.type = appLink.type;
|
||||
// 返显
|
||||
detailSelectDialog.value.id =
|
||||
getUrlNumberValue(
|
||||
'id',
|
||||
`http://127.0.0.1${activeAppLink.value.path}`,
|
||||
) || undefined;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
dialogVisible.value = false;
|
||||
emit('change', activeAppLink.value.path);
|
||||
emit('appLinkChange', activeAppLink.value);
|
||||
};
|
||||
|
||||
// 分组标题引用列表
|
||||
const groupTitleRefs = ref<HTMLInputElement[]>([]);
|
||||
/**
|
||||
* 处理右侧链接列表滚动
|
||||
* @param {object} param0 滚动事件参数
|
||||
* @param {number} param0.scrollTop 滚动条的位置
|
||||
*/
|
||||
const handleScroll = ({ scrollTop }: { scrollTop: number }) => {
|
||||
const titleEl = groupTitleRefs.value.find((titleEl: HTMLInputElement) => {
|
||||
// 获取标题的位置信息
|
||||
const { offsetHeight, offsetTop } = titleEl;
|
||||
// 判断标题是否在可视范围内
|
||||
return scrollTop >= offsetTop && scrollTop < offsetTop + offsetHeight;
|
||||
});
|
||||
// 只需处理一次
|
||||
if (titleEl && activeGroup.value !== titleEl.textContent) {
|
||||
activeGroup.value = titleEl.textContent || '';
|
||||
// 同步左侧的滚动条位置
|
||||
scrollToGroupBtn(activeGroup.value);
|
||||
}
|
||||
};
|
||||
|
||||
// 右侧滚动条
|
||||
const linkScrollbar = ref<ScrollbarInstance>();
|
||||
// 处理分组选中
|
||||
const handleGroupSelected = (group: string) => {
|
||||
activeGroup.value = group;
|
||||
const titleRef = groupTitleRefs.value.find(
|
||||
(item: HTMLInputElement) => item.textContent === group,
|
||||
);
|
||||
if (titleRef) {
|
||||
// 滚动分组标题
|
||||
linkScrollbar.value?.setScrollTop(titleRef.offsetTop);
|
||||
}
|
||||
};
|
||||
|
||||
// 分组滚动条
|
||||
const groupScrollbar = ref<ScrollbarInstance>();
|
||||
// 分组引用列表
|
||||
const groupBtnRefs = ref<ButtonInstance[]>([]);
|
||||
// 自动滚动分组按钮,确保分组按钮保持在可视区域内
|
||||
const scrollToGroupBtn = (group: string) => {
|
||||
const groupBtn = groupBtnRefs.value
|
||||
.map((btn: ButtonInstance) => btn.ref)
|
||||
.find((ref: HTMLButtonElement | undefined) => ref?.textContent === group);
|
||||
if (groupBtn) {
|
||||
groupScrollbar.value?.setScrollTop(groupBtn.offsetTop);
|
||||
}
|
||||
};
|
||||
|
||||
// 是否为相同的链接(不比较参数,只比较链接)
|
||||
const isSameLink = (link1: string, link2: string) => {
|
||||
return link2 ? link1.split('?')[0] === link2.split('?')[0] : false;
|
||||
};
|
||||
|
||||
// 详情选择对话框
|
||||
const detailSelectDialog = ref<{
|
||||
id?: number;
|
||||
type?: APP_LINK_TYPE_ENUM;
|
||||
visible: boolean;
|
||||
}>({
|
||||
visible: false,
|
||||
id: undefined,
|
||||
type: undefined,
|
||||
});
|
||||
// 处理详情选择
|
||||
const handleProductCategorySelected = (id: number) => {
|
||||
const url = new URL(activeAppLink.value.path, 'http://127.0.0.1');
|
||||
// 修改 id 参数
|
||||
url.searchParams.set('id', `${id}`);
|
||||
// 排除域名
|
||||
activeAppLink.value.path = `${url.pathname}${url.search}`;
|
||||
// 关闭对话框
|
||||
detailSelectDialog.value.visible = false;
|
||||
// 重置 id
|
||||
detailSelectDialog.value.id = undefined;
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<el-dialog v-model="dialogVisible" title="选择链接" width="65%">
|
||||
<div class="flex h-[500px] gap-2">
|
||||
<!-- 左侧分组列表 -->
|
||||
<ElScrollbar
|
||||
wrap-class="h-full"
|
||||
ref="groupScrollbar"
|
||||
view-class="flex flex-col"
|
||||
>
|
||||
<el-button
|
||||
v-for="(group, groupIndex) in APP_LINK_GROUP_LIST"
|
||||
:key="groupIndex"
|
||||
class="ml-0 mr-4 w-[90px] justify-start"
|
||||
:class="[{ active: activeGroup === group.name }]"
|
||||
ref="groupBtnRefs"
|
||||
:text="activeGroup !== group.name"
|
||||
:type="activeGroup === group.name ? 'primary' : 'default'"
|
||||
@click="handleGroupSelected(group.name)"
|
||||
>
|
||||
{{ group.name }}
|
||||
</el-button>
|
||||
</ElScrollbar>
|
||||
<!-- 右侧链接列表 -->
|
||||
<ElScrollbar
|
||||
class="h-full flex-1"
|
||||
@scroll="handleScroll"
|
||||
ref="linkScrollbar"
|
||||
>
|
||||
<div
|
||||
v-for="(group, groupIndex) in APP_LINK_GROUP_LIST"
|
||||
:key="groupIndex"
|
||||
>
|
||||
<!-- 分组标题 -->
|
||||
<div class="font-bold" ref="groupTitleRefs">{{ group.name }}</div>
|
||||
<!-- 链接列表 -->
|
||||
<el-tooltip
|
||||
v-for="(appLink, appLinkIndex) in group.links"
|
||||
:key="appLinkIndex"
|
||||
:content="appLink.path"
|
||||
placement="bottom"
|
||||
:show-after="300"
|
||||
>
|
||||
<el-button
|
||||
class="mb-2 ml-0 mr-2"
|
||||
:type="
|
||||
isSameLink(appLink.path, activeAppLink.path)
|
||||
? 'primary'
|
||||
: 'default'
|
||||
"
|
||||
@click="handleAppLinkSelected(appLink)"
|
||||
>
|
||||
{{ appLink.name }}
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</ElScrollbar>
|
||||
</div>
|
||||
<!-- 底部对话框操作按钮 -->
|
||||
<template #footer>
|
||||
<el-button type="primary" @click="handleSubmit">确 定</el-button>
|
||||
<el-button @click="dialogVisible = false">取 消</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<el-dialog v-model="detailSelectDialog.visible" title="" width="50%">
|
||||
<el-form class="min-h-[200px]">
|
||||
<el-form-item
|
||||
label="选择分类"
|
||||
v-if="
|
||||
detailSelectDialog.type === APP_LINK_TYPE_ENUM.PRODUCT_CATEGORY_LIST
|
||||
"
|
||||
>
|
||||
<ProductCategorySelect
|
||||
v-model="detailSelectDialog.id"
|
||||
:parent-id="0"
|
||||
@update:model-value="handleProductCategorySelected"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-dialog>
|
||||
</template>
|
||||
<style lang="scss" scoped>
|
||||
:deep(.el-button + .el-button) {
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,236 @@
|
|||
// APP 链接分组
|
||||
export interface AppLinkGroup {
|
||||
// 分组名称
|
||||
name: string;
|
||||
// 链接列表
|
||||
links: AppLink[];
|
||||
}
|
||||
|
||||
// APP 链接
|
||||
export interface AppLink {
|
||||
// 链接名称
|
||||
name: string;
|
||||
// 链接地址
|
||||
path: string;
|
||||
// 链接的类型
|
||||
type?: APP_LINK_TYPE_ENUM;
|
||||
}
|
||||
|
||||
// APP 链接类型(需要特殊处理,例如商品详情)
|
||||
export enum APP_LINK_TYPE_ENUM {
|
||||
// 拼团活动
|
||||
ACTIVITY_COMBINATION,
|
||||
// 积分商城活动
|
||||
ACTIVITY_POINT,
|
||||
// 秒杀活动
|
||||
ACTIVITY_SECKILL,
|
||||
// 文章详情
|
||||
ARTICLE_DETAIL,
|
||||
// 优惠券详情
|
||||
COUPON_DETAIL,
|
||||
// 自定义页面详情
|
||||
DIY_PAGE_DETAIL,
|
||||
// 品类列表
|
||||
PRODUCT_CATEGORY_LIST,
|
||||
// 拼团商品详情
|
||||
PRODUCT_DETAIL_COMBINATION,
|
||||
// 商品详情
|
||||
PRODUCT_DETAIL_NORMAL,
|
||||
// 秒杀商品详情
|
||||
PRODUCT_DETAIL_SECKILL,
|
||||
// 商品列表
|
||||
PRODUCT_LIST,
|
||||
}
|
||||
|
||||
// APP 链接列表(做一下持久化?)
|
||||
export const APP_LINK_GROUP_LIST = [
|
||||
{
|
||||
name: '商城',
|
||||
links: [
|
||||
{
|
||||
name: '首页',
|
||||
path: '/pages/index/index',
|
||||
},
|
||||
{
|
||||
name: '商品分类',
|
||||
path: '/pages/index/category',
|
||||
type: APP_LINK_TYPE_ENUM.PRODUCT_CATEGORY_LIST,
|
||||
},
|
||||
{
|
||||
name: '购物车',
|
||||
path: '/pages/index/cart',
|
||||
},
|
||||
{
|
||||
name: '个人中心',
|
||||
path: '/pages/index/user',
|
||||
},
|
||||
{
|
||||
name: '商品搜索',
|
||||
path: '/pages/index/search',
|
||||
},
|
||||
{
|
||||
name: '自定义页面',
|
||||
path: '/pages/index/page',
|
||||
type: APP_LINK_TYPE_ENUM.DIY_PAGE_DETAIL,
|
||||
},
|
||||
{
|
||||
name: '客服',
|
||||
path: '/pages/chat/index',
|
||||
},
|
||||
{
|
||||
name: '系统设置',
|
||||
path: '/pages/public/setting',
|
||||
},
|
||||
{
|
||||
name: '常见问题',
|
||||
path: '/pages/public/faq',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: '商品',
|
||||
links: [
|
||||
{
|
||||
name: '商品列表',
|
||||
path: '/pages/goods/list',
|
||||
type: APP_LINK_TYPE_ENUM.PRODUCT_LIST,
|
||||
},
|
||||
{
|
||||
name: '商品详情',
|
||||
path: '/pages/goods/index',
|
||||
type: APP_LINK_TYPE_ENUM.PRODUCT_DETAIL_NORMAL,
|
||||
},
|
||||
{
|
||||
name: '拼团商品详情',
|
||||
path: '/pages/goods/groupon',
|
||||
type: APP_LINK_TYPE_ENUM.PRODUCT_DETAIL_COMBINATION,
|
||||
},
|
||||
{
|
||||
name: '秒杀商品详情',
|
||||
path: '/pages/goods/seckill',
|
||||
type: APP_LINK_TYPE_ENUM.PRODUCT_DETAIL_SECKILL,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: '营销活动',
|
||||
links: [
|
||||
{
|
||||
name: '拼团订单',
|
||||
path: '/pages/activity/groupon/order',
|
||||
},
|
||||
{
|
||||
name: '营销商品',
|
||||
path: '/pages/activity/index',
|
||||
},
|
||||
{
|
||||
name: '拼团活动',
|
||||
path: '/pages/activity/groupon/list',
|
||||
type: APP_LINK_TYPE_ENUM.ACTIVITY_COMBINATION,
|
||||
},
|
||||
{
|
||||
name: '秒杀活动',
|
||||
path: '/pages/activity/seckill/list',
|
||||
type: APP_LINK_TYPE_ENUM.ACTIVITY_SECKILL,
|
||||
},
|
||||
{
|
||||
name: '积分商城活动',
|
||||
path: '/pages/activity/point/list',
|
||||
type: APP_LINK_TYPE_ENUM.ACTIVITY_POINT,
|
||||
},
|
||||
{
|
||||
name: '签到中心',
|
||||
path: '/pages/app/sign',
|
||||
},
|
||||
{
|
||||
name: '优惠券中心',
|
||||
path: '/pages/coupon/list',
|
||||
},
|
||||
{
|
||||
name: '优惠券详情',
|
||||
path: '/pages/coupon/detail',
|
||||
type: APP_LINK_TYPE_ENUM.COUPON_DETAIL,
|
||||
},
|
||||
{
|
||||
name: '文章详情',
|
||||
path: '/pages/public/richtext',
|
||||
type: APP_LINK_TYPE_ENUM.ARTICLE_DETAIL,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: '分销商城',
|
||||
links: [
|
||||
{
|
||||
name: '分销中心',
|
||||
path: '/pages/commission/index',
|
||||
},
|
||||
{
|
||||
name: '推广商品',
|
||||
path: '/pages/commission/goods',
|
||||
},
|
||||
{
|
||||
name: '分销订单',
|
||||
path: '/pages/commission/order',
|
||||
},
|
||||
{
|
||||
name: '我的团队',
|
||||
path: '/pages/commission/team',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: '支付',
|
||||
links: [
|
||||
{
|
||||
name: '充值余额',
|
||||
path: '/pages/pay/recharge',
|
||||
},
|
||||
{
|
||||
name: '充值记录',
|
||||
path: '/pages/pay/recharge-log',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: '用户中心',
|
||||
links: [
|
||||
{
|
||||
name: '用户信息',
|
||||
path: '/pages/user/info',
|
||||
},
|
||||
{
|
||||
name: '用户订单',
|
||||
path: '/pages/order/list',
|
||||
},
|
||||
{
|
||||
name: '售后订单',
|
||||
path: '/pages/order/aftersale/list',
|
||||
},
|
||||
{
|
||||
name: '商品收藏',
|
||||
path: '/pages/user/goods-collect',
|
||||
},
|
||||
{
|
||||
name: '浏览记录',
|
||||
path: '/pages/user/goods-log',
|
||||
},
|
||||
{
|
||||
name: '地址管理',
|
||||
path: '/pages/user/address/list',
|
||||
},
|
||||
{
|
||||
name: '用户佣金',
|
||||
path: '/pages/user/wallet/commission',
|
||||
},
|
||||
{
|
||||
name: '用户余额',
|
||||
path: '/pages/user/wallet/money',
|
||||
},
|
||||
{
|
||||
name: '用户积分',
|
||||
path: '/pages/user/wallet/score',
|
||||
},
|
||||
],
|
||||
},
|
||||
] as AppLinkGroup[];
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
<script lang="ts" setup>
|
||||
import { ref, watch } from 'vue';
|
||||
|
||||
import AppLinkSelectDialog from './app-link-select-dialog.vue';
|
||||
|
||||
// APP 链接输入框
|
||||
defineOptions({ name: 'AppLinkInput' });
|
||||
// 定义属性
|
||||
const props = defineProps({
|
||||
// 当前选中的链接
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
// setter
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [link: string];
|
||||
}>();
|
||||
// 当前的链接
|
||||
const appLink = ref('');
|
||||
// 选择对话框
|
||||
const dialogRef = ref();
|
||||
// 处理打开对话框
|
||||
const handleOpenDialog = () => dialogRef.value?.open(appLink.value);
|
||||
// 处理 APP 链接选中
|
||||
const handleLinkSelected = (link: string) => (appLink.value = link);
|
||||
|
||||
// getter
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
() => (appLink.value = props.modelValue),
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
watch(
|
||||
() => appLink.value,
|
||||
() => emit('update:modelValue', appLink.value),
|
||||
);
|
||||
</script>
|
||||
<template>
|
||||
<el-input v-model="appLink" placeholder="输入或选择链接">
|
||||
<template #append>
|
||||
<el-button @click="handleOpenDialog">选择</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
<AppLinkSelectDialog ref="dialogRef" @change="handleLinkSelected" />
|
||||
</template>
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { PREDEFINE_COLORS } from '#/utils/constants';
|
||||
|
||||
// 颜色输入框
|
||||
defineOptions({ name: 'ColorInput' });
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
const color = computed({
|
||||
get: () => {
|
||||
return props.modelValue;
|
||||
},
|
||||
set: (val: string) => {
|
||||
emit('update:modelValue', val);
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-input v-model="color">
|
||||
<template #prepend>
|
||||
<el-color-picker v-model="color" :predefine="PREDEFINE_COLORS" />
|
||||
</template>
|
||||
</el-input>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
:deep(.el-input-group__prepend) {
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,200 @@
|
|||
<script setup lang="ts">
|
||||
import type { ComponentStyle } from '#/components/diy-editor/util';
|
||||
|
||||
import { useVModel } from '@vueuse/core';
|
||||
import {
|
||||
ElCard,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElRadio,
|
||||
ElRadioGroup,
|
||||
ElSlider,
|
||||
ElTabPane,
|
||||
ElTabs,
|
||||
ElTree,
|
||||
} from 'element-plus';
|
||||
|
||||
import ColorInput from '#/components/color-input/index.vue';
|
||||
import UploadImg from '#/components/upload/image-upload.vue';
|
||||
|
||||
/**
|
||||
* 组件容器属性:目前右边部分
|
||||
* 用于包裹组件,为组件提供 背景、外边距、内边距、边框等样式
|
||||
*/
|
||||
defineOptions({ name: 'ComponentContainer' });
|
||||
|
||||
const props = defineProps<{ modelValue: ComponentStyle }>();
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
const formData = useVModel(props, 'modelValue', emit);
|
||||
|
||||
const treeData = [
|
||||
{
|
||||
label: '外部边距',
|
||||
prop: 'margin',
|
||||
children: [
|
||||
{
|
||||
label: '上',
|
||||
prop: 'marginTop',
|
||||
},
|
||||
{
|
||||
label: '右',
|
||||
prop: 'marginRight',
|
||||
},
|
||||
{
|
||||
label: '下',
|
||||
prop: 'marginBottom',
|
||||
},
|
||||
{
|
||||
label: '左',
|
||||
prop: 'marginLeft',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '内部边距',
|
||||
prop: 'padding',
|
||||
children: [
|
||||
{
|
||||
label: '上',
|
||||
prop: 'paddingTop',
|
||||
},
|
||||
{
|
||||
label: '右',
|
||||
prop: 'paddingRight',
|
||||
},
|
||||
{
|
||||
label: '下',
|
||||
prop: 'paddingBottom',
|
||||
},
|
||||
{
|
||||
label: '左',
|
||||
prop: 'paddingLeft',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '边框圆角',
|
||||
prop: 'borderRadius',
|
||||
children: [
|
||||
{
|
||||
label: '上左',
|
||||
prop: 'borderTopLeftRadius',
|
||||
},
|
||||
{
|
||||
label: '上右',
|
||||
prop: 'borderTopRightRadius',
|
||||
},
|
||||
{
|
||||
label: '下右',
|
||||
prop: 'borderBottomRightRadius',
|
||||
},
|
||||
{
|
||||
label: '下左',
|
||||
prop: 'borderBottomLeftRadius',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const handleSliderChange = (prop: string) => {
|
||||
switch (prop) {
|
||||
case 'borderRadius': {
|
||||
formData.value.borderTopLeftRadius = formData.value.borderRadius;
|
||||
formData.value.borderTopRightRadius = formData.value.borderRadius;
|
||||
formData.value.borderBottomRightRadius = formData.value.borderRadius;
|
||||
formData.value.borderBottomLeftRadius = formData.value.borderRadius;
|
||||
break;
|
||||
}
|
||||
case 'margin': {
|
||||
formData.value.marginTop = formData.value.margin;
|
||||
formData.value.marginRight = formData.value.margin;
|
||||
formData.value.marginBottom = formData.value.margin;
|
||||
formData.value.marginLeft = formData.value.margin;
|
||||
break;
|
||||
}
|
||||
case 'padding': {
|
||||
formData.value.paddingTop = formData.value.padding;
|
||||
formData.value.paddingRight = formData.value.padding;
|
||||
formData.value.paddingBottom = formData.value.padding;
|
||||
formData.value.paddingLeft = formData.value.padding;
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElTabs stretch>
|
||||
<!-- 每个组件的自定义内容 -->
|
||||
<ElTabPane label="内容" v-if="$slots.default">
|
||||
<slot></slot>
|
||||
</ElTabPane>
|
||||
|
||||
<!-- 每个组件的通用内容 -->
|
||||
<ElTabPane label="样式" lazy>
|
||||
<ElCard header="组件样式" class="property-group">
|
||||
<ElForm :model="formData" label-width="80px">
|
||||
<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" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="上传图片" prop="bgImg" v-else>
|
||||
<UploadImg
|
||||
v-model="formData.bgImg"
|
||||
:limit="1"
|
||||
:show-description="false"
|
||||
>
|
||||
<template #tip>建议宽度 750px</template>
|
||||
</UploadImg>
|
||||
</ElFormItem>
|
||||
<ElTree
|
||||
:data="treeData"
|
||||
:expand-on-click-node="false"
|
||||
default-expand-all
|
||||
>
|
||||
<template #default="{ node, data }">
|
||||
<ElFormItem
|
||||
:label="data.label"
|
||||
:prop="data.prop"
|
||||
:label-width="node.level === 1 ? '80px' : '62px'"
|
||||
class="mb-0 w-full"
|
||||
>
|
||||
<ElSlider
|
||||
v-model="
|
||||
formData[data.prop as keyof ComponentStyle] as number
|
||||
"
|
||||
:max="100"
|
||||
:min="0"
|
||||
show-input
|
||||
input-size="small"
|
||||
:show-input-controls="false"
|
||||
@input="handleSliderChange(data.prop)"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</template>
|
||||
</ElTree>
|
||||
<slot name="style" :style="formData"></slot>
|
||||
</ElForm>
|
||||
</ElCard>
|
||||
</ElTabPane>
|
||||
</ElTabs>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
:deep(.el-slider__runway) {
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
:deep(.el-input-number) {
|
||||
width: 50px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,269 @@
|
|||
<script setup lang="ts">
|
||||
import type {
|
||||
ComponentStyle,
|
||||
DiyComponent,
|
||||
} from '#/components/diy-editor/util';
|
||||
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
|
||||
import { ElButton, ElTooltip } from 'element-plus';
|
||||
|
||||
import { components } from '#/components/diy-editor/components/mobile';
|
||||
import VerticalButtonGroup from '#/components/vertical-button-group/index.vue';
|
||||
|
||||
/**
|
||||
* 组件容器:目前在中间部分
|
||||
* 用于包裹组件,为组件提供 背景、外边距、内边距、边框等样式
|
||||
*/
|
||||
defineOptions({ name: 'ComponentContainer', components });
|
||||
|
||||
const props = defineProps({
|
||||
component: {
|
||||
type: Object as () => DiyComponentWithStyle,
|
||||
required: true,
|
||||
},
|
||||
active: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
canMoveUp: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
canMoveDown: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
showToolbar: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emits = defineEmits<{
|
||||
(e: 'move', direction: number): void;
|
||||
(e: 'copy'): void;
|
||||
(e: 'delete'): void;
|
||||
}>();
|
||||
|
||||
type DiyComponentWithStyle = DiyComponent<any> & {
|
||||
property: { style?: ComponentStyle };
|
||||
};
|
||||
/**
|
||||
* 组件样式
|
||||
*/
|
||||
const style = computed(() => {
|
||||
const componentStyle = props.component.property.style;
|
||||
if (!componentStyle) {
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
marginTop: `${componentStyle.marginTop || 0}px`,
|
||||
marginBottom: `${componentStyle.marginBottom || 0}px`,
|
||||
marginLeft: `${componentStyle.marginLeft || 0}px`,
|
||||
marginRight: `${componentStyle.marginRight || 0}px`,
|
||||
paddingTop: `${componentStyle.paddingTop || 0}px`,
|
||||
paddingRight: `${componentStyle.paddingRight || 0}px`,
|
||||
paddingBottom: `${componentStyle.paddingBottom || 0}px`,
|
||||
paddingLeft: `${componentStyle.paddingLeft || 0}px`,
|
||||
borderTopLeftRadius: `${componentStyle.borderTopLeftRadius || 0}px`,
|
||||
borderTopRightRadius: `${componentStyle.borderTopRightRadius || 0}px`,
|
||||
borderBottomRightRadius: `${componentStyle.borderBottomRightRadius || 0}px`,
|
||||
borderBottomLeftRadius: `${componentStyle.borderBottomLeftRadius || 0}px`,
|
||||
overflow: 'hidden',
|
||||
background:
|
||||
componentStyle.bgType === 'color'
|
||||
? componentStyle.bgColor
|
||||
: `url(${componentStyle.bgImg})`,
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* 移动组件
|
||||
* @param direction 移动方向
|
||||
*/
|
||||
const handleMoveComponent = (direction: number) => {
|
||||
emits('move', direction);
|
||||
};
|
||||
|
||||
/**
|
||||
* 复制组件
|
||||
*/
|
||||
const handleCopyComponent = () => {
|
||||
emits('copy');
|
||||
};
|
||||
|
||||
/**
|
||||
* 删除组件
|
||||
*/
|
||||
const handleDeleteComponent = () => {
|
||||
emits('delete');
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div class="component" :class="[{ active }]">
|
||||
<div
|
||||
:style="{
|
||||
...style,
|
||||
}"
|
||||
>
|
||||
<component :is="component.id" :property="component.property" />
|
||||
</div>
|
||||
<div class="component-wrap">
|
||||
<!-- 左侧:组件名(悬浮的小贴条) -->
|
||||
<div class="component-name" v-if="component.name">
|
||||
{{ component.name }}
|
||||
</div>
|
||||
<!-- 右侧:组件操作工具栏 -->
|
||||
<div
|
||||
class="component-toolbar"
|
||||
v-if="showToolbar && component.name && active"
|
||||
>
|
||||
<VerticalButtonGroup type="primary">
|
||||
<ElTooltip content="上移" placement="right">
|
||||
<ElButton
|
||||
:disabled="!canMoveUp"
|
||||
@click.stop="handleMoveComponent(-1)"
|
||||
>
|
||||
<IconifyIcon icon="ep:arrow-up" />
|
||||
</ElButton>
|
||||
</ElTooltip>
|
||||
<ElTooltip content="下移" placement="right">
|
||||
<ElButton
|
||||
:disabled="!canMoveDown"
|
||||
@click.stop="handleMoveComponent(1)"
|
||||
>
|
||||
<IconifyIcon icon="ep:arrow-down" />
|
||||
</ElButton>
|
||||
</ElTooltip>
|
||||
<ElTooltip content="复制" placement="right">
|
||||
<ElButton @click.stop="handleCopyComponent()">
|
||||
<IconifyIcon icon="ep:copy-document" />
|
||||
</ElButton>
|
||||
</ElTooltip>
|
||||
<ElTooltip content="删除" placement="right">
|
||||
<ElButton @click.stop="handleDeleteComponent()">
|
||||
<IconifyIcon icon="ep:delete" />
|
||||
</ElButton>
|
||||
</ElTooltip>
|
||||
</VerticalButtonGroup>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
$active-border-width: 2px;
|
||||
$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);
|
||||
box-shadow: 0 0 5px 0 rgb(24 144 255 / 30%);
|
||||
|
||||
.component-name {
|
||||
top: $hover-border-width;
|
||||
|
||||
/* 防止加了边框之后,位置移动 */
|
||||
left: $name-position - $hover-border-width;
|
||||
}
|
||||
}
|
||||
|
||||
/* 左侧:组件名称 */
|
||||
.component-name {
|
||||
position: absolute;
|
||||
top: $active-border-width;
|
||||
left: $name-position;
|
||||
display: block;
|
||||
width: 80px;
|
||||
height: 25px;
|
||||
font-size: 12px;
|
||||
line-height: 25px;
|
||||
color: #6a6a6a;
|
||||
text-align: center;
|
||||
background: #fff;
|
||||
box-shadow:
|
||||
0 0 4px #00000014,
|
||||
0 2px 6px #0000000f,
|
||||
0 4px 8px 2px #0000000a;
|
||||
|
||||
/* 右侧小三角 */
|
||||
&::after {
|
||||
position: absolute;
|
||||
top: 7.5px;
|
||||
right: -10px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
content: ' ';
|
||||
border: 5px solid transparent;
|
||||
border-left-color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
/* 右侧:组件操作工具栏 */
|
||||
.component-toolbar {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: $toolbar-position;
|
||||
display: none;
|
||||
|
||||
/* 左侧小三角 */
|
||||
&::before {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: -10px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
content: ' ';
|
||||
border: 5px solid transparent;
|
||||
border-right-color: #2d8cf0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 组件选中时 */
|
||||
&.active {
|
||||
margin-bottom: 4px;
|
||||
|
||||
.component-wrap {
|
||||
margin-bottom: $active-border-width + $active-border-width;
|
||||
border: $active-border-width solid var(--el-color-primary) !important;
|
||||
box-shadow: 0 0 10px 0 rgb(24 144 255 / 30%);
|
||||
|
||||
.component-name {
|
||||
top: 0 !important;
|
||||
|
||||
/* 防止加了边框之后,位置移动 */
|
||||
left: $name-position - $active-border-width !important;
|
||||
color: #fff;
|
||||
background: var(--el-color-primary);
|
||||
|
||||
&::after {
|
||||
border-left-color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.component-toolbar {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,221 @@
|
|||
<script setup lang="ts">
|
||||
import type {
|
||||
DiyComponent,
|
||||
DiyComponentLibrary,
|
||||
} from '#/components/diy-editor/util';
|
||||
|
||||
import { reactive, watch } from 'vue';
|
||||
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
import { cloneDeep } from '@vben/utils';
|
||||
|
||||
import { ElAside, ElCollapse, ElCollapseItem, ElScrollbar } from 'element-plus';
|
||||
import draggable from 'vuedraggable';
|
||||
|
||||
import { componentConfigs } from './mobile/index';
|
||||
|
||||
/** 组件库:目前左侧的【基础组件】、【图文组件】部分 */
|
||||
defineOptions({ name: 'ComponentLibrary' });
|
||||
|
||||
// 组件列表
|
||||
const props = defineProps<{
|
||||
list: DiyComponentLibrary[];
|
||||
}>();
|
||||
// 组件分组
|
||||
const groups = reactive<any[]>([]);
|
||||
// 展开的折叠面板
|
||||
const extendGroups = reactive<string[]>([]);
|
||||
|
||||
// 监听 list 属性,按照 DiyComponentLibrary 的 name 分组
|
||||
watch(
|
||||
() => props.list,
|
||||
() => {
|
||||
// 清除旧数据
|
||||
extendGroups.length = 0;
|
||||
groups.length = 0;
|
||||
// 重新生成数据
|
||||
props.list.forEach((group) => {
|
||||
// 是否展开分组
|
||||
if (group.extended) {
|
||||
extendGroups.push(group.name);
|
||||
}
|
||||
// 查找组件
|
||||
const components = group.components
|
||||
.map((name) => componentConfigs[name] as DiyComponent<any>)
|
||||
.filter(Boolean);
|
||||
if (components.length > 0) {
|
||||
groups.push({
|
||||
name: group.name,
|
||||
components,
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
},
|
||||
);
|
||||
|
||||
// 克隆组件
|
||||
const handleCloneComponent = (component: DiyComponent<any>) => {
|
||||
const instance = cloneDeep(component);
|
||||
instance.uid = Date.now();
|
||||
return instance;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElAside class="editor-left" width="261px">
|
||||
<ElScrollbar>
|
||||
<ElCollapse v-model="extendGroups">
|
||||
<ElCollapseItem
|
||||
v-for="group in groups"
|
||||
:key="group.name"
|
||||
:name="group.name"
|
||||
:title="group.name"
|
||||
>
|
||||
<draggable
|
||||
class="component-container"
|
||||
ghost-class="draggable-ghost"
|
||||
item-key="index"
|
||||
:list="group.components"
|
||||
:sort="false"
|
||||
:group="{ name: 'component', pull: 'clone', put: false }"
|
||||
:clone="handleCloneComponent"
|
||||
:animation="200"
|
||||
:force-fallback="true"
|
||||
>
|
||||
<template #item="{ element }">
|
||||
<div>
|
||||
<div class="drag-placement">组件放置区域</div>
|
||||
<div class="component">
|
||||
<IconifyIcon :icon="element.icon" :size="32" />
|
||||
<span class="mt-1 text-xs">{{ element.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</draggable>
|
||||
</ElCollapseItem>
|
||||
</ElCollapse>
|
||||
</ElScrollbar>
|
||||
</ElAside>
|
||||
</template>
|
||||
|
||||
<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;
|
||||
}
|
||||
|
||||
:deep(.el-collapse-item__wrap) {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
:deep(.el-collapse-item__content) {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
:deep(.el-collapse-item__header) {
|
||||
height: 32px;
|
||||
padding: 0 24px;
|
||||
line-height: 32px;
|
||||
background-color: var(--el-bg-color-page);
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.component-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
.component.active,
|
||||
.component:hover {
|
||||
color: var(--el-color-white);
|
||||
background: var(--el-color-primary);
|
||||
|
||||
.el-icon {
|
||||
color: var(--el-color-white);
|
||||
}
|
||||
}
|
||||
|
||||
.component:nth-of-type(3n) {
|
||||
border-right: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* 拖拽占位提示,默认不显示 */
|
||||
.drag-placement {
|
||||
display: none;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.drag-area {
|
||||
/* 拖拽到手机区域时的样式 */
|
||||
.draggable-ghost {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
|
||||
/* 条纹背景 */
|
||||
background: linear-gradient(
|
||||
45deg,
|
||||
#91a8d5 0,
|
||||
#91a8d5 10%,
|
||||
#94b4eb 10%,
|
||||
#94b4eb 50%,
|
||||
#91a8d5 50%,
|
||||
#91a8d5 60%,
|
||||
#94b4eb 60%,
|
||||
#94b4eb
|
||||
);
|
||||
background-size: 1rem 1rem;
|
||||
transition: all 0.5s;
|
||||
|
||||
span {
|
||||
display: inline-block;
|
||||
width: 140px;
|
||||
height: 25px;
|
||||
font-size: 12px;
|
||||
line-height: 25px;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
background: #5487df;
|
||||
}
|
||||
|
||||
/* 拖拽时隐藏组件 */
|
||||
.component {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 拖拽时显示占位提示 */
|
||||
.drag-placement {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
import type {
|
||||
ComponentStyle,
|
||||
DiyComponent,
|
||||
} from '#/components/diy-editor/util';
|
||||
|
||||
/** 轮播图属性 */
|
||||
export interface CarouselProperty {
|
||||
// 类型:默认 | 卡片
|
||||
type: 'card' | 'default';
|
||||
// 指示器样式:点 | 数字
|
||||
indicator: 'dot' | 'number';
|
||||
// 是否自动播放
|
||||
autoplay: boolean;
|
||||
// 播放间隔
|
||||
interval: number;
|
||||
// 轮播内容
|
||||
items: CarouselItemProperty[];
|
||||
// 组件样式
|
||||
style: ComponentStyle;
|
||||
}
|
||||
// 轮播内容属性
|
||||
export interface CarouselItemProperty {
|
||||
// 类型:图片 | 视频
|
||||
type: 'img' | 'video';
|
||||
// 图片链接
|
||||
imgUrl: string;
|
||||
// 视频链接
|
||||
videoUrl: string;
|
||||
// 跳转链接
|
||||
url: string;
|
||||
}
|
||||
|
||||
// 定义组件
|
||||
export const component = {
|
||||
id: 'Carousel',
|
||||
name: '轮播图',
|
||||
icon: 'system-uicons:carousel',
|
||||
property: {
|
||||
type: 'default',
|
||||
indicator: 'dot',
|
||||
autoplay: false,
|
||||
interval: 3,
|
||||
items: [
|
||||
{
|
||||
type: 'img',
|
||||
imgUrl: 'https://static.iocoder.cn/mall/banner-01.jpg',
|
||||
videoUrl: '',
|
||||
},
|
||||
{
|
||||
type: 'img',
|
||||
imgUrl: 'https://static.iocoder.cn/mall/banner-02.jpg',
|
||||
videoUrl: '',
|
||||
},
|
||||
] as CarouselItemProperty[],
|
||||
style: {
|
||||
bgType: 'color',
|
||||
bgColor: '#fff',
|
||||
marginBottom: 8,
|
||||
} as ComponentStyle,
|
||||
},
|
||||
} as DiyComponent<CarouselProperty>;
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
<script setup lang="ts">
|
||||
import type { CarouselProperty } from './config';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
|
||||
import { ElCarousel, ElCarouselItem, ElImage } from 'element-plus';
|
||||
|
||||
/** 轮播图 */
|
||||
defineOptions({ name: 'Carousel' });
|
||||
|
||||
defineProps<{ property: CarouselProperty }>();
|
||||
|
||||
const currentIndex = ref(0);
|
||||
const handleIndexChange = (index: number) => {
|
||||
currentIndex.value = index + 1;
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<!-- 无图片 -->
|
||||
<div
|
||||
class="flex h-[250px] items-center justify-center bg-gray-300"
|
||||
v-if="property.items.length === 0"
|
||||
>
|
||||
<IconifyIcon icon="tdesign:image" class="text-[120px] text-gray-800" />
|
||||
</div>
|
||||
<div v-else class="relative">
|
||||
<ElCarousel
|
||||
height="174px"
|
||||
:type="property.type === 'card' ? 'card' : ''"
|
||||
:autoplay="property.autoplay"
|
||||
:interval="property.interval * 1000"
|
||||
:indicator-position="property.indicator === 'number' ? 'none' : undefined"
|
||||
@change="handleIndexChange"
|
||||
>
|
||||
<ElCarouselItem v-for="(item, index) in property.items" :key="index">
|
||||
<ElImage class="h-full w-full" :src="item.imgUrl" />
|
||||
</ElCarouselItem>
|
||||
</ElCarousel>
|
||||
<div
|
||||
v-if="property.indicator === 'number'"
|
||||
class="absolute bottom-[10px] right-[10px] rounded-xl bg-black px-[8px] py-[2px] text-[10px] text-white opacity-40"
|
||||
>
|
||||
{{ currentIndex }} / {{ property.items.length }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
|
@ -0,0 +1,133 @@
|
|||
<script setup lang="ts">
|
||||
import type { CarouselProperty } from './config';
|
||||
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
|
||||
import { useVModel } from '@vueuse/core';
|
||||
import {
|
||||
ElCard,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElRadioButton,
|
||||
ElRadioGroup,
|
||||
ElSlider,
|
||||
ElSwitch,
|
||||
ElText,
|
||||
ElTooltip,
|
||||
} from 'element-plus';
|
||||
|
||||
import AppLinkInput from '#/components/app-link-input/index.vue';
|
||||
import ComponentContainerProperty from '#/components/diy-editor/components/component-container-property.vue';
|
||||
import Draggable from '#/components/draggable/index.vue';
|
||||
import UploadFile from '#/components/upload/file-upload.vue';
|
||||
import UploadImg from '#/components/upload/image-upload.vue';
|
||||
|
||||
// 轮播图属性面板
|
||||
defineOptions({ name: 'CarouselProperty' });
|
||||
|
||||
const props = defineProps<{ modelValue: CarouselProperty }>();
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
const formData = useVModel(props, 'modelValue', emit);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ComponentContainerProperty v-model="formData.style">
|
||||
<ElForm label-width="80px" :model="formData">
|
||||
<ElCard header="样式设置" class="property-group" shadow="never">
|
||||
<ElFormItem label="样式" prop="type">
|
||||
<ElRadioGroup v-model="formData.type">
|
||||
<ElTooltip class="item" content="默认" placement="bottom">
|
||||
<ElRadioButton value="default">
|
||||
<IconifyIcon icon="system-uicons:carousel" />
|
||||
</ElRadioButton>
|
||||
</ElTooltip>
|
||||
<ElTooltip class="item" content="卡片" placement="bottom">
|
||||
<ElRadioButton value="card">
|
||||
<IconifyIcon icon="ic:round-view-carousel" />
|
||||
</ElRadioButton>
|
||||
</ElTooltip>
|
||||
</ElRadioGroup>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="指示器" prop="indicator">
|
||||
<ElRadioGroup v-model="formData.indicator">
|
||||
<ElRadio value="dot">小圆点</ElRadio>
|
||||
<ElRadio value="number">数字</ElRadio>
|
||||
</ElRadioGroup>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="是否轮播" prop="autoplay">
|
||||
<ElSwitch v-model="formData.autoplay" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="播放间隔" prop="interval" v-if="formData.autoplay">
|
||||
<ElSlider
|
||||
v-model="formData.interval"
|
||||
:max="10"
|
||||
:min="0.5"
|
||||
:step="0.5"
|
||||
show-input
|
||||
input-size="small"
|
||||
:show-input-controls="false"
|
||||
/>
|
||||
<ElText type="info">单位:秒</ElText>
|
||||
</ElFormItem>
|
||||
</ElCard>
|
||||
<ElCard header="内容设置" class="property-group" shadow="never">
|
||||
<Draggable v-model="formData.items" :empty-item="{ type: 'img' }">
|
||||
<template #default="{ element }">
|
||||
<ElFormItem
|
||||
label="类型"
|
||||
prop="type"
|
||||
class="mb-2"
|
||||
label-width="40px"
|
||||
>
|
||||
<ElRadioGroup v-model="element.type">
|
||||
<ElRadio value="img">图片</ElRadio>
|
||||
<ElRadio value="video">视频</ElRadio>
|
||||
</ElRadioGroup>
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
label="图片"
|
||||
class="mb-2"
|
||||
label-width="40px"
|
||||
v-if="element.type === 'img'"
|
||||
>
|
||||
<UploadImg
|
||||
v-model="element.imgUrl"
|
||||
draggable="false"
|
||||
height="80px"
|
||||
width="100%"
|
||||
class="min-w-[80px]"
|
||||
:show-description="false"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<template v-else>
|
||||
<ElFormItem label="封面" class="mb-2" label-width="40px">
|
||||
<UploadImg
|
||||
v-model="element.imgUrl"
|
||||
draggable="false"
|
||||
:show-description="false"
|
||||
height="80px"
|
||||
width="100%"
|
||||
class="min-w-[80px]"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="视频" class="mb-2" label-width="40px">
|
||||
<UploadFile
|
||||
v-model="element.videoUrl"
|
||||
:file-type="['mp4']"
|
||||
:limit="1"
|
||||
:file-size="100"
|
||||
class="min-w-[80px]"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</template>
|
||||
<ElFormItem label="链接" class="mb-2" label-width="40px">
|
||||
<AppLinkInput v-model="element.url" />
|
||||
</ElFormItem>
|
||||
</template>
|
||||
</Draggable>
|
||||
</ElCard>
|
||||
</ElForm>
|
||||
</ComponentContainerProperty>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
import type { DiyComponent } from '#/components/diy-editor/util';
|
||||
|
||||
/** 分割线属性 */
|
||||
export interface DividerProperty {
|
||||
// 高度
|
||||
height: number;
|
||||
// 线宽
|
||||
lineWidth: number;
|
||||
// 边距类型
|
||||
paddingType: 'horizontal' | 'none';
|
||||
// 颜色
|
||||
lineColor: string;
|
||||
// 类型
|
||||
borderType: 'dashed' | 'dotted' | 'none' | 'solid';
|
||||
}
|
||||
|
||||
// 定义组件
|
||||
export const component = {
|
||||
id: 'Divider',
|
||||
name: '分割线',
|
||||
icon: 'tdesign:component-divider-vertical',
|
||||
property: {
|
||||
height: 30,
|
||||
lineWidth: 1,
|
||||
paddingType: 'none',
|
||||
lineColor: '#dcdfe6',
|
||||
borderType: 'solid',
|
||||
},
|
||||
} as DiyComponent<DividerProperty>;
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
<script setup lang="ts">
|
||||
import type { DividerProperty } from './config';
|
||||
|
||||
/** 页面顶部导航栏 */
|
||||
defineOptions({ name: 'Divider' });
|
||||
|
||||
defineProps<{ property: DividerProperty }>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex items-center"
|
||||
:style="{
|
||||
height: `${property.height}px`,
|
||||
}"
|
||||
>
|
||||
<div
|
||||
class="w-full"
|
||||
:style="{
|
||||
borderTopStyle: property.borderType,
|
||||
borderTopColor: property.lineColor,
|
||||
borderTopWidth: `${property.lineWidth}px`,
|
||||
margin: property.paddingType === 'none' ? '0' : '0px 16px',
|
||||
}"
|
||||
></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
<script setup lang="ts">
|
||||
import type { DividerProperty } from './config';
|
||||
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
|
||||
import { useVModel } from '@vueuse/core';
|
||||
import {
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElRadioButton,
|
||||
ElRadioGroup,
|
||||
ElSlider,
|
||||
ElTooltip,
|
||||
} from 'element-plus';
|
||||
|
||||
import ColorInput from '#/components/input-with-color/index.vue';
|
||||
|
||||
// 导航栏属性面板
|
||||
defineOptions({ name: 'DividerProperty' });
|
||||
const props = defineProps<{ modelValue: DividerProperty }>();
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
const formData = useVModel(props, 'modelValue', emit);
|
||||
|
||||
// 线类型
|
||||
const BORDER_TYPES = [
|
||||
{
|
||||
icon: 'vaadin:line-h',
|
||||
text: '实线',
|
||||
type: 'solid',
|
||||
},
|
||||
{
|
||||
icon: 'tabler:line-dashed',
|
||||
text: '虚线',
|
||||
type: 'dashed',
|
||||
},
|
||||
{
|
||||
icon: 'tabler:line-dotted',
|
||||
text: '点线',
|
||||
type: 'dotted',
|
||||
},
|
||||
{
|
||||
icon: 'entypo:progress-empty',
|
||||
text: '无',
|
||||
type: 'none',
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElForm label-width="80px" :model="formData">
|
||||
<ElFormItem label="高度" prop="height">
|
||||
<ElSlider
|
||||
v-model="formData.height"
|
||||
:min="1"
|
||||
:max="100"
|
||||
show-input
|
||||
input-size="small"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="选择样式" prop="borderType">
|
||||
<ElRadioGroup v-model="formData!.borderType">
|
||||
<ElTooltip
|
||||
placement="top"
|
||||
v-for="(item, index) in BORDER_TYPES"
|
||||
:key="index"
|
||||
:content="item.text"
|
||||
>
|
||||
<ElRadioButton :value="item.type">
|
||||
<IconifyIcon :icon="item.icon" />
|
||||
</ElRadioButton>
|
||||
</ElTooltip>
|
||||
</ElRadioGroup>
|
||||
</ElFormItem>
|
||||
<template v-if="formData.borderType !== 'none'">
|
||||
<ElFormItem label="线宽" prop="lineWidth">
|
||||
<ElSlider
|
||||
v-model="formData.lineWidth"
|
||||
:min="1"
|
||||
:max="30"
|
||||
show-input
|
||||
input-size="small"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="左右边距" prop="paddingType">
|
||||
<ElRadioGroup v-model="formData!.paddingType">
|
||||
<ElTooltip content="无边距" placement="top">
|
||||
<ElRadioButton value="none">
|
||||
<IconifyIcon icon="tabler:box-padding" />
|
||||
</ElRadioButton>
|
||||
</ElTooltip>
|
||||
<ElTooltip content="左右留边" placement="top">
|
||||
<ElRadioButton value="horizontal">
|
||||
<IconifyIcon icon="vaadin:padding" />
|
||||
</ElRadioButton>
|
||||
</ElTooltip>
|
||||
</ElRadioGroup>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="颜色">
|
||||
<!-- 分割线颜色 -->
|
||||
<ColorInput v-model="formData.lineColor" />
|
||||
</ElFormItem>
|
||||
</template>
|
||||
</ElForm>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
import type { DiyComponent } from '#/components/diy-editor/util';
|
||||
|
||||
/** 弹窗广告属性 */
|
||||
export interface PopoverProperty {
|
||||
list: PopoverItemProperty[];
|
||||
}
|
||||
|
||||
export interface PopoverItemProperty {
|
||||
// 图片地址
|
||||
imgUrl: string;
|
||||
// 跳转连接
|
||||
url: string;
|
||||
// 显示类型:仅显示一次、每次启动都会显示
|
||||
showType: 'always' | 'once';
|
||||
}
|
||||
|
||||
// 定义组件
|
||||
export const component = {
|
||||
id: 'Popover',
|
||||
name: '弹窗广告',
|
||||
icon: 'carbon:popup',
|
||||
position: 'fixed',
|
||||
property: {
|
||||
list: [{ showType: 'once' }],
|
||||
},
|
||||
} as DiyComponent<PopoverProperty>;
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
<script setup lang="ts">
|
||||
import type { PopoverProperty } from './config';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
|
||||
import { ElImage } from 'element-plus';
|
||||
|
||||
/** 弹窗广告 */
|
||||
defineOptions({ name: 'Popover' });
|
||||
// 定义属性
|
||||
defineProps<{ property: PopoverProperty }>();
|
||||
|
||||
// 处理选中
|
||||
const activeIndex = ref(0);
|
||||
const handleActive = (index: number) => {
|
||||
activeIndex.value = index;
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div
|
||||
v-for="(item, index) in 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="{
|
||||
zIndex: 100 + index + (activeIndex === index ? 100 : 0),
|
||||
marginRight: `${-146 - index * 20}px`,
|
||||
marginBottom: `${-227 - index * 20}px`,
|
||||
}"
|
||||
@click="handleActive(index)"
|
||||
>
|
||||
<ElImage :src="item.imgUrl" fit="contain" class="h-full w-full">
|
||||
<template #error>
|
||||
<div class="flex h-full w-full items-center justify-center">
|
||||
<IconifyIcon icon="ep:picture" />
|
||||
</div>
|
||||
</template>
|
||||
</ElImage>
|
||||
<div class="absolute right-1 top-1 text-xs">{{ index + 1 }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
<script setup lang="ts">
|
||||
import type { PopoverProperty } from './config';
|
||||
|
||||
import { useVModel } from '@vueuse/core';
|
||||
import {
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElRadio,
|
||||
ElRadioGroup,
|
||||
ElTooltip,
|
||||
} from 'element-plus';
|
||||
|
||||
import AppLinkInput from '#/components/app-link-input/index.vue';
|
||||
import Draggable from '#/components/draggable/index.vue';
|
||||
import UploadImg from '#/components/upload/image-upload.vue';
|
||||
|
||||
// 弹窗广告属性面板
|
||||
defineOptions({ name: 'PopoverProperty' });
|
||||
|
||||
const props = defineProps<{ modelValue: PopoverProperty }>();
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
const formData = useVModel(props, 'modelValue', emit);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElForm label-width="80px" :model="formData">
|
||||
<Draggable v-model="formData.list" :empty-item="{ showType: 'once' }">
|
||||
<template #default="{ element, index }">
|
||||
<ElFormItem label="图片" :prop="`list[${index}].imgUrl`">
|
||||
<UploadImg
|
||||
v-model="element.imgUrl"
|
||||
height="56px"
|
||||
width="56px"
|
||||
:show-description="false"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="跳转链接" :prop="`list[${index}].url`">
|
||||
<AppLinkInput v-model="element.url" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="显示次数" :prop="`list[${index}].showType`">
|
||||
<ElRadioGroup v-model="element.showType">
|
||||
<ElTooltip
|
||||
content="只显示一次,下次打开时不显示"
|
||||
placement="bottom"
|
||||
>
|
||||
<ElRadio value="once">一次</ElRadio>
|
||||
</ElTooltip>
|
||||
<ElTooltip content="每次打开时都会显示" placement="bottom">
|
||||
<ElRadio value="always">不限</ElRadio>
|
||||
</ElTooltip>
|
||||
</ElRadioGroup>
|
||||
</ElFormItem>
|
||||
</template>
|
||||
</Draggable>
|
||||
</ElForm>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
// 导出所有优惠券相关组件
|
||||
export { CouponDiscount } from './coupon-discount';
|
||||
export { CouponDiscountDesc } from './coupon-discount-desc';
|
||||
export { CouponValidTerm } from './coupon-validTerm';
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
import type {
|
||||
ComponentStyle,
|
||||
DiyComponent,
|
||||
} from '#/components/diy-editor/util';
|
||||
|
||||
/** 商品卡片属性 */
|
||||
export interface CouponCardProperty {
|
||||
// 列数
|
||||
columns: number;
|
||||
// 背景图
|
||||
bgImg: string;
|
||||
// 文字颜色
|
||||
textColor: string;
|
||||
// 按钮样式
|
||||
button: {
|
||||
// 背景颜色
|
||||
bgColor: string;
|
||||
// 颜色
|
||||
color: string;
|
||||
};
|
||||
// 间距
|
||||
space: number;
|
||||
// 优惠券编号列表
|
||||
couponIds: number[];
|
||||
// 组件样式
|
||||
style: ComponentStyle;
|
||||
}
|
||||
|
||||
// 定义组件
|
||||
export const component = {
|
||||
id: 'CouponCard',
|
||||
name: '优惠券',
|
||||
icon: 'ep:ticket',
|
||||
property: {
|
||||
columns: 1,
|
||||
bgImg: '',
|
||||
textColor: '#E9B461',
|
||||
button: {
|
||||
color: '#434343',
|
||||
bgColor: '',
|
||||
},
|
||||
space: 0,
|
||||
couponIds: [],
|
||||
style: {
|
||||
bgType: 'color',
|
||||
bgColor: '',
|
||||
marginBottom: 8,
|
||||
} as ComponentStyle,
|
||||
},
|
||||
} as DiyComponent<CouponCardProperty>;
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
import type { MallCouponTemplateApi } from '#/api/mall/promotion/coupon/couponTemplate';
|
||||
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
import { floatToFixed2 } from '@vben/utils';
|
||||
|
||||
import { PromotionDiscountTypeEnum } from '#/utils/constants';
|
||||
|
||||
// 优惠描述
|
||||
export const CouponDiscountDesc = defineComponent({
|
||||
name: 'CouponDiscountDesc',
|
||||
props: {
|
||||
coupon: {
|
||||
type: Object as () => MallCouponTemplateApi.CouponTemplate,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const coupon = props.coupon as MallCouponTemplateApi.CouponTemplate;
|
||||
// 使用条件
|
||||
const useCondition =
|
||||
coupon.usePrice > 0 ? `满${floatToFixed2(coupon.usePrice)}元,` : '';
|
||||
// 优惠描述
|
||||
const discountDesc =
|
||||
coupon.discountType === PromotionDiscountTypeEnum.PRICE.type
|
||||
? `减${floatToFixed2(coupon.discountPrice)}元`
|
||||
: `打${coupon.discountPercent / 10}折`;
|
||||
return () => (
|
||||
<div>
|
||||
<span>{useCondition}</span>
|
||||
<span>{discountDesc}</span>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
import type { MallCouponTemplateApi } from '#/api/mall/promotion/coupon/couponTemplate';
|
||||
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
import { floatToFixed2 } from '@vben/utils';
|
||||
|
||||
import { PromotionDiscountTypeEnum } from '#/utils/constants';
|
||||
|
||||
// 优惠值
|
||||
export const CouponDiscount = defineComponent({
|
||||
name: 'CouponDiscount',
|
||||
props: {
|
||||
coupon: {
|
||||
type: Object as () => MallCouponTemplateApi.CouponTemplate,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const coupon = props.coupon as MallCouponTemplateApi.CouponTemplate;
|
||||
// 折扣
|
||||
let value = `${coupon.discountPercent / 10}`;
|
||||
let suffix = ' 折';
|
||||
// 满减
|
||||
if (coupon.discountType === PromotionDiscountTypeEnum.PRICE.type) {
|
||||
value = floatToFixed2(coupon.discountPrice);
|
||||
suffix = ' 元';
|
||||
}
|
||||
return () => (
|
||||
<div>
|
||||
<span class={'text-20px font-bold'}>{value}</span>
|
||||
<span>{suffix}</span>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
import type { MallCouponTemplateApi } from '#/api/mall/promotion/coupon/couponTemplate';
|
||||
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
import { formatDate } from '@vben/utils';
|
||||
|
||||
import { CouponTemplateValidityTypeEnum } from '#/utils/constants';
|
||||
|
||||
// 有效期
|
||||
export const CouponValidTerm = defineComponent({
|
||||
name: 'CouponValidTerm',
|
||||
props: {
|
||||
coupon: {
|
||||
type: Object as () => MallCouponTemplateApi.CouponTemplate,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const coupon = props.coupon as MallCouponTemplateApi.CouponTemplate;
|
||||
const text =
|
||||
coupon.validityType === CouponTemplateValidityTypeEnum.DATE.type
|
||||
? `有效期:${formatDate(coupon.validStartTime, 'YYYY-MM-DD')} 至 ${formatDate(
|
||||
coupon.validEndTime,
|
||||
'YYYY-MM-DD',
|
||||
)}`
|
||||
: `领取后第 ${coupon.fixedStartTerm} - ${coupon.fixedEndTerm} 天内可用`;
|
||||
return () => <div>{text}</div>;
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,162 @@
|
|||
<script setup lang="ts">
|
||||
import type { CouponCardProperty } from './config';
|
||||
|
||||
import type { MallCouponTemplateApi } from '#/api/mall/promotion/coupon/couponTemplate';
|
||||
|
||||
import { onMounted, ref, watch } from 'vue';
|
||||
|
||||
import { ElScrollbar } from 'element-plus';
|
||||
|
||||
import * as CouponTemplateApi from '#/api/mall/promotion/coupon/couponTemplate';
|
||||
|
||||
import {
|
||||
CouponDiscount,
|
||||
CouponDiscountDesc,
|
||||
CouponValidTerm,
|
||||
} from './component';
|
||||
|
||||
/** 商品卡片 */
|
||||
defineOptions({ name: 'CouponCard' });
|
||||
// 定义属性
|
||||
const props = defineProps<{ property: CouponCardProperty }>();
|
||||
// 商品列表
|
||||
const couponList = ref<MallCouponTemplateApi.CouponTemplate[]>([]);
|
||||
watch(
|
||||
() => props.property.couponIds,
|
||||
async () => {
|
||||
if (props.property.couponIds?.length > 0) {
|
||||
couponList.value = await CouponTemplateApi.getCouponTemplateList(
|
||||
props.property.couponIds,
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
deep: true,
|
||||
},
|
||||
);
|
||||
|
||||
// 手机宽度
|
||||
const phoneWidth = ref(375);
|
||||
// 容器
|
||||
const containerRef = ref();
|
||||
// 滚动条宽度
|
||||
const scrollbarWidth = ref('100%');
|
||||
// 优惠券的宽度
|
||||
const couponWidth = ref(375);
|
||||
// 计算布局参数
|
||||
watch(
|
||||
() => [props.property, phoneWidth, couponList.value.length],
|
||||
() => {
|
||||
// 每列的宽度为:(总宽度 - 间距 * (列数 - 1))/ 列数
|
||||
couponWidth.value =
|
||||
(phoneWidth.value - props.property.space * (props.property.columns - 1)) /
|
||||
props.property.columns;
|
||||
// 显示滚动条
|
||||
scrollbarWidth.value = `${
|
||||
couponWidth.value * couponList.value.length +
|
||||
props.property.space * (couponList.value.length - 1)
|
||||
}px`;
|
||||
},
|
||||
{ immediate: true, deep: true },
|
||||
);
|
||||
onMounted(() => {
|
||||
// 提取手机宽度
|
||||
phoneWidth.value = containerRef.value?.wrapRef?.offsetWidth || 375;
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<ElScrollbar class="z-10 min-h-[30px]" wrap-class="w-full" ref="containerRef">
|
||||
<div
|
||||
class="flex flex-row text-xs"
|
||||
:style="{
|
||||
gap: `${property.space}px`,
|
||||
width: scrollbarWidth,
|
||||
}"
|
||||
>
|
||||
<div
|
||||
class="box-content"
|
||||
:style="{
|
||||
background: property.bgImg
|
||||
? `url(${property.bgImg}) 100% center / 100% 100% no-repeat`
|
||||
: '#fff',
|
||||
width: `${couponWidth}px`,
|
||||
color: property.textColor,
|
||||
}"
|
||||
v-for="(coupon, index) in couponList"
|
||||
:key="index"
|
||||
>
|
||||
<!-- 布局1:1列-->
|
||||
<div
|
||||
v-if="property.columns === 1"
|
||||
class="ml-4 flex flex-row justify-between p-2"
|
||||
>
|
||||
<div class="flex flex-col justify-evenly gap-1">
|
||||
<!-- 优惠值 -->
|
||||
<CouponDiscount :coupon="coupon" />
|
||||
<!-- 优惠描述 -->
|
||||
<CouponDiscountDesc :coupon="coupon" />
|
||||
<!-- 有效期 -->
|
||||
<CouponValidTerm :coupon="coupon" />
|
||||
</div>
|
||||
<div class="flex flex-col justify-evenly">
|
||||
<div
|
||||
class="rounded-full px-2 py-0.5"
|
||||
:style="{
|
||||
color: property.button.color,
|
||||
background: property.button.bgColor,
|
||||
}"
|
||||
>
|
||||
立即领取
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 布局2:2列-->
|
||||
<div
|
||||
v-else-if="property.columns === 2"
|
||||
class="ml-4 flex flex-row justify-between p-2"
|
||||
>
|
||||
<div class="flex flex-col justify-evenly gap-1">
|
||||
<!-- 优惠值 -->
|
||||
<CouponDiscount :coupon="coupon" />
|
||||
<!-- 优惠描述 -->
|
||||
<CouponDiscountDesc :coupon="coupon" />
|
||||
<!-- 领取说明 -->
|
||||
<div v-if="coupon.totalCount >= 0">
|
||||
仅剩:{{ coupon.totalCount - coupon.takeCount }}张
|
||||
</div>
|
||||
<div v-else-if="coupon.totalCount === -1">仅剩:不限制</div>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<div
|
||||
class="h-full w-5 rounded-full px-0.5 py-2 text-center"
|
||||
:style="{
|
||||
color: property.button.color,
|
||||
background: property.button.bgColor,
|
||||
}"
|
||||
>
|
||||
立即领取
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 布局3:3列-->
|
||||
<div v-else class="flex flex-col items-center justify-around gap-1 p-1">
|
||||
<!-- 优惠值 -->
|
||||
<CouponDiscount :coupon="coupon" />
|
||||
<!-- 优惠描述 -->
|
||||
<CouponDiscountDesc :coupon="coupon" />
|
||||
<div
|
||||
class="rounded-full px-2 py-0.5"
|
||||
:style="{
|
||||
color: property.button.color,
|
||||
background: property.button.bgColor,
|
||||
}"
|
||||
>
|
||||
立即领取
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ElScrollbar>
|
||||
</template>
|
||||
<style scoped lang="scss"></style>
|
||||
|
|
@ -0,0 +1,161 @@
|
|||
<script setup lang="ts">
|
||||
import type { CouponCardProperty } from './config';
|
||||
|
||||
import type { MallCouponTemplateApi } from '#/api/mall/promotion/coupon/couponTemplate';
|
||||
|
||||
import { ref, watch } from 'vue';
|
||||
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
import { floatToFixed2 } from '@vben/utils';
|
||||
|
||||
import { useVModel } from '@vueuse/core';
|
||||
import {
|
||||
ElCard,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElRadioButton,
|
||||
ElRadioGroup,
|
||||
ElSlider,
|
||||
ElTooltip,
|
||||
} from 'element-plus';
|
||||
|
||||
import * as CouponTemplateApi from '#/api/mall/promotion/coupon/couponTemplate';
|
||||
import ColorInput from '#/components/color-input/index.vue';
|
||||
import ComponentContainerProperty from '#/components/diy-editor/components/component-container-property.vue';
|
||||
import UploadImg from '#/components/upload/image-upload.vue';
|
||||
import {
|
||||
CouponTemplateTakeTypeEnum,
|
||||
PromotionDiscountTypeEnum,
|
||||
} from '#/utils/constants';
|
||||
import CouponSelect from '#/views/mall/promotion/coupon/components/coupon-select.vue';
|
||||
|
||||
// 优惠券卡片属性面板
|
||||
defineOptions({ name: 'CouponCardProperty' });
|
||||
|
||||
const props = defineProps<{ modelValue: CouponCardProperty }>();
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
const formData = useVModel(props, 'modelValue', emit);
|
||||
|
||||
// 优惠券列表
|
||||
const couponList = ref<MallCouponTemplateApi.CouponTemplate[]>([]);
|
||||
const couponSelectDialog = ref();
|
||||
// 添加优惠券
|
||||
const handleAddCoupon = () => {
|
||||
couponSelectDialog.value.open();
|
||||
};
|
||||
const handleCouponSelect = () => {
|
||||
formData.value.couponIds = couponList.value.map((coupon) => coupon.id);
|
||||
};
|
||||
|
||||
watch(
|
||||
() => formData.value.couponIds,
|
||||
async () => {
|
||||
if (formData.value.couponIds?.length > 0) {
|
||||
couponList.value = await CouponTemplateApi.getCouponTemplateList(
|
||||
formData.value.couponIds,
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
deep: true,
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ComponentContainerProperty v-model="formData.style">
|
||||
<ElForm label-width="80px" :model="formData">
|
||||
<ElCard header="优惠券列表" class="property-group" shadow="never">
|
||||
<div
|
||||
v-for="(coupon, index) in couponList"
|
||||
:key="index"
|
||||
class="flex items-center justify-between"
|
||||
>
|
||||
<ElText size="large" truncated>{{ coupon.name }}</ElText>
|
||||
<ElText type="info" truncated>
|
||||
<span v-if="coupon.usePrice > 0">
|
||||
满{{ floatToFixed2(coupon.usePrice) }}元,
|
||||
</span>
|
||||
<span
|
||||
v-if="
|
||||
coupon.discountType === PromotionDiscountTypeEnum.PRICE.type
|
||||
"
|
||||
>
|
||||
减{{ floatToFixed2(coupon.discountPrice) }}元
|
||||
</span>
|
||||
<span v-else> 打{{ coupon.discountPercent }}折 </span>
|
||||
</ElText>
|
||||
</div>
|
||||
<ElFormItem label-width="0">
|
||||
<ElButton
|
||||
@click="handleAddCoupon"
|
||||
type="primary"
|
||||
plain
|
||||
class="mt-2 w-full"
|
||||
>
|
||||
<IconifyIcon icon="ep:plus" class="mr-1" /> 添加
|
||||
</ElButton>
|
||||
</ElFormItem>
|
||||
</ElCard>
|
||||
<ElCard header="优惠券样式" class="property-group" shadow="never">
|
||||
<ElFormItem label="列数" prop="type">
|
||||
<ElRadioGroup v-model="formData.columns">
|
||||
<ElTooltip class="item" content="一列" placement="bottom">
|
||||
<ElRadioButton :value="1">
|
||||
<IconifyIcon icon="fluent:text-column-one-24-filled" />
|
||||
</ElRadioButton>
|
||||
</ElTooltip>
|
||||
<ElTooltip class="item" content="二列" placement="bottom">
|
||||
<ElRadioButton :value="2">
|
||||
<IconifyIcon icon="fluent:text-column-two-24-filled" />
|
||||
</ElRadioButton>
|
||||
</ElTooltip>
|
||||
<ElTooltip class="item" content="三列" placement="bottom">
|
||||
<ElRadioButton :value="3">
|
||||
<IconifyIcon icon="fluent:text-column-three-24-filled" />
|
||||
</ElRadioButton>
|
||||
</ElTooltip>
|
||||
</ElRadioGroup>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="背景图片" prop="bgImg">
|
||||
<UploadImg
|
||||
v-model="formData.bgImg"
|
||||
height="80px"
|
||||
width="100%"
|
||||
class="min-w-[160px]"
|
||||
:show-description="false"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="文字颜色" prop="textColor">
|
||||
<ColorInput v-model="formData.textColor" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="按钮背景" prop="button.bgColor">
|
||||
<ColorInput v-model="formData.button.bgColor" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="按钮文字" prop="button.color">
|
||||
<ColorInput v-model="formData.button.color" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="间隔" prop="space">
|
||||
<ElSlider
|
||||
v-model="formData.space"
|
||||
:max="100"
|
||||
:min="0"
|
||||
show-input
|
||||
input-size="small"
|
||||
:show-input-controls="false"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElCard>
|
||||
</ElForm>
|
||||
</ComponentContainerProperty>
|
||||
<!-- 优惠券选择 -->
|
||||
<CouponSelect
|
||||
ref="couponSelectDialog"
|
||||
v-model:multiple-selection="couponList"
|
||||
:take-type="CouponTemplateTakeTypeEnum.USER.type"
|
||||
@change="handleCouponSelect"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
import type { DiyComponent } from '#/components/diy-editor/util';
|
||||
|
||||
// 悬浮按钮属性
|
||||
export interface FloatingActionButtonProperty {
|
||||
// 展开方向
|
||||
direction: 'horizontal' | 'vertical';
|
||||
// 是否显示文字
|
||||
showText: boolean;
|
||||
// 按钮列表
|
||||
list: FloatingActionButtonItemProperty[];
|
||||
}
|
||||
|
||||
// 悬浮按钮项属性
|
||||
export interface FloatingActionButtonItemProperty {
|
||||
// 图片地址
|
||||
imgUrl: string;
|
||||
// 跳转连接
|
||||
url: string;
|
||||
// 文字
|
||||
text: string;
|
||||
// 文字颜色
|
||||
textColor: string;
|
||||
}
|
||||
|
||||
// 定义组件
|
||||
export const component = {
|
||||
id: 'FloatingActionButton',
|
||||
name: '悬浮按钮',
|
||||
icon: 'tabler:float-right',
|
||||
position: 'fixed',
|
||||
property: {
|
||||
direction: 'vertical',
|
||||
showText: true,
|
||||
list: [{ textColor: '#fff' }],
|
||||
},
|
||||
} as DiyComponent<FloatingActionButtonProperty>;
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
<script setup lang="ts">
|
||||
import type { FloatingActionButtonProperty } from './config';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
|
||||
import { ElImage, ElMessage } from 'element-plus';
|
||||
|
||||
/** 悬浮按钮 */
|
||||
defineOptions({ name: 'FloatingActionButton' });
|
||||
// 定义属性
|
||||
defineProps<{ property: FloatingActionButtonProperty }>();
|
||||
|
||||
// 是否展开
|
||||
const expanded = ref(false);
|
||||
// 处理展开/折叠
|
||||
const handleToggleFab = () => {
|
||||
expanded.value = !expanded.value;
|
||||
};
|
||||
|
||||
const handleActive = (index: number) => {
|
||||
ElMessage.success(`点击了${index}`);
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div
|
||||
class="absolute bottom-8 right-[calc(50%-375px/2+32px)] z-20 flex items-center gap-3"
|
||||
:class="[
|
||||
{
|
||||
'flex-row': property.direction === 'horizontal',
|
||||
'flex-col': property.direction === 'vertical',
|
||||
},
|
||||
]"
|
||||
>
|
||||
<template v-if="expanded">
|
||||
<div
|
||||
v-for="(item, index) in property.list"
|
||||
:key="index"
|
||||
class="flex flex-col items-center"
|
||||
@click="handleActive(index)"
|
||||
>
|
||||
<ElImage :src="item.imgUrl" fit="contain" class="h-7 w-7">
|
||||
<template #error>
|
||||
<div class="flex h-full w-full items-center justify-center">
|
||||
<IconifyIcon icon="ep:picture" :color="item.textColor" />
|
||||
</div>
|
||||
</template>
|
||||
</ElImage>
|
||||
<span
|
||||
v-if="property.showText"
|
||||
class="mt-1 text-xs"
|
||||
:style="{ color: item.textColor }"
|
||||
>
|
||||
{{ item.text }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
<!-- todo: @owen 使用APP主题色 -->
|
||||
<el-button type="primary" size="large" circle @click="handleToggleFab">
|
||||
<IconifyIcon
|
||||
icon="ep:plus"
|
||||
class="fab-icon"
|
||||
:class="[{ active: expanded }]"
|
||||
/>
|
||||
</el-button>
|
||||
</div>
|
||||
<!-- 模态背景:展开时显示,点击后折叠 -->
|
||||
<div v-if="expanded" class="modal-bg" @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>
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
<script setup lang="ts">
|
||||
import type { FloatingActionButtonProperty } from './config';
|
||||
|
||||
import { useVModel } from '@vueuse/core';
|
||||
import {
|
||||
ElCard,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElRadio,
|
||||
ElRadioGroup,
|
||||
ElSwitch,
|
||||
} from 'element-plus';
|
||||
|
||||
import AppLinkInput from '#/components/app-link-input/index.vue';
|
||||
import Draggable from '#/components/draggable/index.vue';
|
||||
import InputWithColor from '#/components/input-with-color/index.vue';
|
||||
import UploadImg from '#/components/upload/image-upload.vue';
|
||||
|
||||
// 悬浮按钮属性面板
|
||||
defineOptions({ name: 'FloatingActionButtonProperty' });
|
||||
|
||||
const props = defineProps<{ modelValue: FloatingActionButtonProperty }>();
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
const formData = useVModel(props, 'modelValue', emit);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElForm label-width="80px" :model="formData">
|
||||
<ElCard header="按钮配置" class="property-group" shadow="never">
|
||||
<ElFormItem label="展开方向" prop="direction">
|
||||
<ElRadioGroup v-model="formData.direction">
|
||||
<ElRadio value="vertical">垂直</ElRadio>
|
||||
<ElRadio value="horizontal">水平</ElRadio>
|
||||
</ElRadioGroup>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="显示文字" prop="showText">
|
||||
<ElSwitch v-model="formData.showText" />
|
||||
</ElFormItem>
|
||||
</ElCard>
|
||||
<ElCard header="按钮列表" class="property-group" shadow="never">
|
||||
<Draggable v-model="formData.list" :empty-item="{ textColor: '#fff' }">
|
||||
<template #default="{ element, index }">
|
||||
<ElFormItem label="图标" :prop="`list[${index}].imgUrl`">
|
||||
<UploadImg
|
||||
v-model="element.imgUrl"
|
||||
height="56px"
|
||||
width="56px"
|
||||
:show-description="false"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="文字" :prop="`list[${index}].text`">
|
||||
<InputWithColor
|
||||
v-model="element.text"
|
||||
v-model:color="element.textColor"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="跳转链接" :prop="`list[${index}].url`">
|
||||
<AppLinkInput v-model="element.url" />
|
||||
</ElFormItem>
|
||||
</template>
|
||||
</Draggable>
|
||||
</ElCard>
|
||||
</ElForm>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
|
@ -0,0 +1,175 @@
|
|||
import type { StyleValue } from 'vue';
|
||||
|
||||
import type { HotZoneItemProperty } from '#/components/diy-editor/components/mobile/HotZone/config';
|
||||
|
||||
// 热区的最小宽高
|
||||
export const HOT_ZONE_MIN_SIZE = 100;
|
||||
|
||||
// 控制的类型
|
||||
export enum CONTROL_TYPE_ENUM {
|
||||
LEFT,
|
||||
TOP,
|
||||
WIDTH,
|
||||
HEIGHT,
|
||||
}
|
||||
|
||||
// 定义热区的控制点
|
||||
export interface ControlDot {
|
||||
position: string;
|
||||
types: CONTROL_TYPE_ENUM[];
|
||||
style: StyleValue;
|
||||
}
|
||||
|
||||
// 热区的8个控制点
|
||||
export const CONTROL_DOT_LIST = [
|
||||
{
|
||||
position: '左上角',
|
||||
types: [
|
||||
CONTROL_TYPE_ENUM.LEFT,
|
||||
CONTROL_TYPE_ENUM.TOP,
|
||||
CONTROL_TYPE_ENUM.WIDTH,
|
||||
CONTROL_TYPE_ENUM.HEIGHT,
|
||||
],
|
||||
style: { left: '-5px', top: '-5px', cursor: 'nwse-resize' },
|
||||
},
|
||||
{
|
||||
position: '上方中间',
|
||||
types: [CONTROL_TYPE_ENUM.TOP, CONTROL_TYPE_ENUM.HEIGHT],
|
||||
style: {
|
||||
left: '50%',
|
||||
top: '-5px',
|
||||
cursor: 'n-resize',
|
||||
transform: 'translateX(-50%)',
|
||||
},
|
||||
},
|
||||
{
|
||||
position: '右上角',
|
||||
types: [
|
||||
CONTROL_TYPE_ENUM.TOP,
|
||||
CONTROL_TYPE_ENUM.WIDTH,
|
||||
CONTROL_TYPE_ENUM.HEIGHT,
|
||||
],
|
||||
style: { right: '-5px', top: '-5px', cursor: 'nesw-resize' },
|
||||
},
|
||||
{
|
||||
position: '右侧中间',
|
||||
types: [CONTROL_TYPE_ENUM.WIDTH],
|
||||
style: {
|
||||
right: '-5px',
|
||||
top: '50%',
|
||||
cursor: 'e-resize',
|
||||
transform: 'translateX(-50%)',
|
||||
},
|
||||
},
|
||||
{
|
||||
position: '右下角',
|
||||
types: [CONTROL_TYPE_ENUM.WIDTH, CONTROL_TYPE_ENUM.HEIGHT],
|
||||
style: { right: '-5px', bottom: '-5px', cursor: 'nwse-resize' },
|
||||
},
|
||||
{
|
||||
position: '下方中间',
|
||||
types: [CONTROL_TYPE_ENUM.HEIGHT],
|
||||
style: {
|
||||
left: '50%',
|
||||
bottom: '-5px',
|
||||
cursor: 's-resize',
|
||||
transform: 'translateX(-50%)',
|
||||
},
|
||||
},
|
||||
{
|
||||
position: '左下角',
|
||||
types: [
|
||||
CONTROL_TYPE_ENUM.LEFT,
|
||||
CONTROL_TYPE_ENUM.WIDTH,
|
||||
CONTROL_TYPE_ENUM.HEIGHT,
|
||||
],
|
||||
style: { left: '-5px', bottom: '-5px', cursor: 'nesw-resize' },
|
||||
},
|
||||
{
|
||||
position: '左侧中间',
|
||||
types: [CONTROL_TYPE_ENUM.LEFT, CONTROL_TYPE_ENUM.WIDTH],
|
||||
style: {
|
||||
left: '-5px',
|
||||
top: '50%',
|
||||
cursor: 'w-resize',
|
||||
transform: 'translateX(-50%)',
|
||||
},
|
||||
},
|
||||
] as ControlDot[];
|
||||
|
||||
// region 热区的缩放
|
||||
// 热区的缩放比例
|
||||
export const HOT_ZONE_SCALE_RATE = 2;
|
||||
// 缩小:缩回适合手机屏幕的大小
|
||||
export const zoomOut = (list?: HotZoneItemProperty[]) => {
|
||||
return (
|
||||
list?.map((hotZone) => ({
|
||||
...hotZone,
|
||||
left: (hotZone.left /= HOT_ZONE_SCALE_RATE),
|
||||
top: (hotZone.top /= HOT_ZONE_SCALE_RATE),
|
||||
width: (hotZone.width /= HOT_ZONE_SCALE_RATE),
|
||||
height: (hotZone.height /= HOT_ZONE_SCALE_RATE),
|
||||
})) || []
|
||||
);
|
||||
};
|
||||
// 放大:作用是为了方便在电脑屏幕上编辑
|
||||
export const zoomIn = (list?: HotZoneItemProperty[]) => {
|
||||
return (
|
||||
list?.map((hotZone) => ({
|
||||
...hotZone,
|
||||
left: (hotZone.left *= HOT_ZONE_SCALE_RATE),
|
||||
top: (hotZone.top *= HOT_ZONE_SCALE_RATE),
|
||||
width: (hotZone.width *= HOT_ZONE_SCALE_RATE),
|
||||
height: (hotZone.height *= HOT_ZONE_SCALE_RATE),
|
||||
})) || []
|
||||
);
|
||||
};
|
||||
// endregion
|
||||
|
||||
/**
|
||||
* 封装热区拖拽
|
||||
*
|
||||
* 注:为什么不使用vueuse的useDraggable。在本场景下,其使用方式比较复杂
|
||||
* @param hotZone 热区
|
||||
* @param downEvent 鼠标按下事件
|
||||
* @param callback 回调函数
|
||||
*/
|
||||
export const useDraggable = (
|
||||
hotZone: HotZoneItemProperty,
|
||||
downEvent: MouseEvent,
|
||||
callback: (
|
||||
left: number,
|
||||
top: number,
|
||||
width: number,
|
||||
height: number,
|
||||
moveWidth: number,
|
||||
moveHeight: number,
|
||||
) => void,
|
||||
) => {
|
||||
// 阻止事件冒泡
|
||||
downEvent.stopPropagation();
|
||||
|
||||
// 移动前的鼠标坐标
|
||||
const { clientX: startX, clientY: startY } = downEvent;
|
||||
// 移动前的热区坐标、大小
|
||||
const { left, top, width, height } = hotZone;
|
||||
|
||||
// 监听鼠标移动
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
// 移动宽度
|
||||
const moveWidth = e.clientX - startX;
|
||||
// 移动高度
|
||||
const moveHeight = e.clientY - startY;
|
||||
// 移动回调
|
||||
callback(left, top, width, height, moveWidth, moveHeight);
|
||||
};
|
||||
|
||||
// 松开鼠标后,结束拖拽
|
||||
const handleMouseUp = () => {
|
||||
document.removeEventListener('mousemove', handleMouseMove);
|
||||
document.removeEventListener('mouseup', handleMouseUp);
|
||||
};
|
||||
|
||||
document.addEventListener('mousemove', handleMouseMove);
|
||||
document.addEventListener('mouseup', handleMouseUp);
|
||||
};
|
||||
|
|
@ -0,0 +1,284 @@
|
|||
<script setup lang="ts">
|
||||
import type { ControlDot } from './controller';
|
||||
|
||||
import type { AppLink } from '#/components/app-link-input/data';
|
||||
import type { HotZoneItemProperty } from '#/components/diy-editor/components/mobile/HotZone/config';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
|
||||
import { ElButton, ElDialog, ElImage } from 'element-plus';
|
||||
|
||||
import {
|
||||
CONTROL_DOT_LIST,
|
||||
CONTROL_TYPE_ENUM,
|
||||
HOT_ZONE_MIN_SIZE,
|
||||
useDraggable,
|
||||
zoomIn,
|
||||
zoomOut,
|
||||
} from './controller';
|
||||
|
||||
/** 热区编辑对话框 */
|
||||
defineOptions({ name: 'HotZoneEditDialog' });
|
||||
|
||||
// 定义属性
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Array<HotZoneItemProperty>,
|
||||
default: () => [],
|
||||
},
|
||||
imgUrl: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
const formData = ref<HotZoneItemProperty[]>([]);
|
||||
|
||||
// 弹窗的是否显示
|
||||
const dialogVisible = ref(false);
|
||||
// 打开弹窗
|
||||
const open = () => {
|
||||
// 放大
|
||||
formData.value = zoomIn(props.modelValue);
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
// 提供 open 方法,用于打开弹窗
|
||||
defineExpose({ open });
|
||||
|
||||
// 热区容器
|
||||
const container = ref<HTMLDivElement>();
|
||||
|
||||
// 增加热区
|
||||
const handleAdd = () => {
|
||||
formData.value.push({
|
||||
width: HOT_ZONE_MIN_SIZE,
|
||||
height: HOT_ZONE_MIN_SIZE,
|
||||
top: 0,
|
||||
left: 0,
|
||||
} as HotZoneItemProperty);
|
||||
};
|
||||
// 删除热区
|
||||
const handleRemove = (hotZone: HotZoneItemProperty) => {
|
||||
formData.value = formData.value.filter((item) => item !== hotZone);
|
||||
};
|
||||
|
||||
// 移动热区
|
||||
const handleMove = (item: HotZoneItemProperty, e: MouseEvent) => {
|
||||
useDraggable(item, e, (left, top, _, __, moveWidth, moveHeight) => {
|
||||
setLeft(item, left + moveWidth);
|
||||
setTop(item, top + moveHeight);
|
||||
});
|
||||
};
|
||||
|
||||
// 调整热区大小、位置
|
||||
const handleResize = (
|
||||
item: HotZoneItemProperty,
|
||||
ctrlDot: ControlDot,
|
||||
e: MouseEvent,
|
||||
) => {
|
||||
useDraggable(item, e, (left, top, width, height, moveWidth, moveHeight) => {
|
||||
ctrlDot.types.forEach((type) => {
|
||||
switch (type) {
|
||||
case CONTROL_TYPE_ENUM.HEIGHT: {
|
||||
{
|
||||
// 左移时,宽度为减少
|
||||
const direction = ctrlDot.types.includes(CONTROL_TYPE_ENUM.TOP)
|
||||
? -1
|
||||
: 1;
|
||||
setHeight(item, height + moveHeight * direction);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case CONTROL_TYPE_ENUM.LEFT: {
|
||||
setLeft(item, left + moveWidth);
|
||||
break;
|
||||
}
|
||||
case CONTROL_TYPE_ENUM.TOP: {
|
||||
setTop(item, top + moveHeight);
|
||||
break;
|
||||
}
|
||||
case CONTROL_TYPE_ENUM.WIDTH: {
|
||||
{
|
||||
// 上移时,高度为减少
|
||||
const direction = ctrlDot.types.includes(CONTROL_TYPE_ENUM.LEFT)
|
||||
? -1
|
||||
: 1;
|
||||
setWidth(item, width + moveWidth * direction);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// 设置X轴坐标
|
||||
const setLeft = (item: HotZoneItemProperty, left: number) => {
|
||||
// 不能超出容器
|
||||
if (left >= 0 && left <= container.value!.offsetWidth - item.width) {
|
||||
item.left = left;
|
||||
}
|
||||
};
|
||||
// 设置Y轴坐标
|
||||
const setTop = (item: HotZoneItemProperty, top: number) => {
|
||||
// 不能超出容器
|
||||
if (top >= 0 && top <= container.value!.offsetHeight - item.height) {
|
||||
item.top = top;
|
||||
}
|
||||
};
|
||||
// 设置宽度
|
||||
const setWidth = (item: HotZoneItemProperty, width: number) => {
|
||||
// 不能小于最小宽度 && 不能超出容器右边
|
||||
if (
|
||||
width >= HOT_ZONE_MIN_SIZE &&
|
||||
item.left + width <= container.value!.offsetWidth
|
||||
) {
|
||||
item.width = width;
|
||||
}
|
||||
};
|
||||
// 设置高度
|
||||
const setHeight = (item: HotZoneItemProperty, height: number) => {
|
||||
// 不能小于最小高度 && 不能超出容器底部
|
||||
if (
|
||||
height >= HOT_ZONE_MIN_SIZE &&
|
||||
item.top + height <= container.value!.offsetHeight
|
||||
) {
|
||||
item.height = height;
|
||||
}
|
||||
};
|
||||
|
||||
// 处理对话框关闭
|
||||
const handleSubmit = () => {
|
||||
// 会自动触发handleClose
|
||||
dialogVisible.value = false;
|
||||
};
|
||||
|
||||
// 处理对话框关闭
|
||||
const handleClose = () => {
|
||||
// 缩小
|
||||
const list = zoomOut(formData.value);
|
||||
emit('update:modelValue', list);
|
||||
};
|
||||
|
||||
const activeHotZone = ref<HotZoneItemProperty>();
|
||||
const appLinkDialogRef = ref();
|
||||
const handleShowAppLinkDialog = (hotZone: HotZoneItemProperty) => {
|
||||
activeHotZone.value = hotZone;
|
||||
appLinkDialogRef.value.open(hotZone.url);
|
||||
};
|
||||
const handleAppLinkChange = (appLink: AppLink) => {
|
||||
if (!appLink || !activeHotZone.value) return;
|
||||
activeHotZone.value.name = appLink.name;
|
||||
activeHotZone.value.url = appLink.path;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
title="设置热区"
|
||||
width="780"
|
||||
@close="handleClose"
|
||||
>
|
||||
<div ref="container" class="w-750px relative h-full">
|
||||
<ElImage
|
||||
:src="imgUrl"
|
||||
class="w-750px pointer-events-none h-full select-none"
|
||||
/>
|
||||
<div
|
||||
v-for="(item, hotZoneIndex) in formData"
|
||||
:key="hotZoneIndex"
|
||||
class="hot-zone"
|
||||
:style="{
|
||||
width: `${item.width}px`,
|
||||
height: `${item.height}px`,
|
||||
top: `${item.top}px`,
|
||||
left: `${item.left}px`,
|
||||
}"
|
||||
@mousedown="handleMove(item, $event)"
|
||||
@dblclick="handleShowAppLinkDialog(item)"
|
||||
>
|
||||
<span class="pointer-events-none select-none">{{
|
||||
item.name || '双击选择链接'
|
||||
}}</span>
|
||||
<IconifyIcon
|
||||
icon="ep:close"
|
||||
class="delete"
|
||||
:size="14"
|
||||
@click="handleRemove(item)"
|
||||
/>
|
||||
|
||||
<!-- 8个控制点 -->
|
||||
<span
|
||||
class="ctrl-dot"
|
||||
v-for="(dot, dotIndex) in CONTROL_DOT_LIST"
|
||||
:key="dotIndex"
|
||||
:style="dot.style"
|
||||
@mousedown="handleResize(item, dot, $event)"
|
||||
></span>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<ElButton @click="handleAdd" type="primary" plain>
|
||||
<IconifyIcon icon="ep:plus" class="mr-5px" />
|
||||
添加热区
|
||||
</ElButton>
|
||||
<ElButton @click="handleSubmit" type="primary" plain>
|
||||
<IconifyIcon icon="ep:check" class="mr-5px" />
|
||||
确定
|
||||
</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
<AppLinkSelectDialog
|
||||
ref="appLinkDialogRef"
|
||||
@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>
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
import type {
|
||||
ComponentStyle,
|
||||
DiyComponent,
|
||||
} from '#/components/diy-editor/util';
|
||||
|
||||
/** 热区属性 */
|
||||
export interface HotZoneProperty {
|
||||
// 图片地址
|
||||
imgUrl: string;
|
||||
// 导航菜单列表
|
||||
list: HotZoneItemProperty[];
|
||||
// 组件样式
|
||||
style: ComponentStyle;
|
||||
}
|
||||
|
||||
/** 热区项目属性 */
|
||||
export interface HotZoneItemProperty {
|
||||
// 链接的名称
|
||||
name: string;
|
||||
// 链接
|
||||
url: string;
|
||||
// 宽
|
||||
width: number;
|
||||
// 高
|
||||
height: number;
|
||||
// 上
|
||||
top: number;
|
||||
// 左
|
||||
left: number;
|
||||
}
|
||||
|
||||
// 定义组件
|
||||
export const component = {
|
||||
id: 'HotZone',
|
||||
name: '热区',
|
||||
icon: 'tabler:hand-click',
|
||||
property: {
|
||||
imgUrl: '',
|
||||
list: [] as HotZoneItemProperty[],
|
||||
style: {
|
||||
bgType: 'color',
|
||||
bgColor: '#fff',
|
||||
marginBottom: 8,
|
||||
} as ComponentStyle,
|
||||
},
|
||||
} as DiyComponent<HotZoneProperty>;
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
<script setup lang="ts">
|
||||
import type { HotZoneProperty } from './config';
|
||||
|
||||
import { ElImage } from 'element-plus';
|
||||
|
||||
/** 热区 */
|
||||
defineOptions({ name: 'HotZone' });
|
||||
const props = defineProps<{ property: HotZoneProperty }>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="min-h-30px relative h-full w-full">
|
||||
<ElImage
|
||||
:src="props.property.imgUrl"
|
||||
class="pointer-events-none h-full w-full select-none"
|
||||
/>
|
||||
<div
|
||||
v-for="(item, index) in props.property.list"
|
||||
:key="index"
|
||||
class="hot-zone"
|
||||
:style="{
|
||||
width: `${item.width}px`,
|
||||
height: `${item.height}px`,
|
||||
top: `${item.top}px`,
|
||||
left: `${item.left}px`,
|
||||
}"
|
||||
>
|
||||
{{ 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>
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
<script setup lang="ts">
|
||||
import type { HotZoneProperty } from './config';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useVModel } from '@vueuse/core';
|
||||
import { ElButton, ElForm, ElFormItem, ElText } from 'element-plus';
|
||||
|
||||
import ComponentContainerProperty from '#/components/diy-editor/components/component-container-property.vue';
|
||||
import UploadImg from '#/components/upload/image-upload.vue';
|
||||
|
||||
import HotZoneEditDialog from './components/hot-zone-edit-dialog/index.vue';
|
||||
|
||||
/** 热区属性面板 */
|
||||
defineOptions({ name: 'HotZoneProperty' });
|
||||
|
||||
const props = defineProps<{ modelValue: HotZoneProperty }>();
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
const formData = useVModel(props, 'modelValue', emit);
|
||||
|
||||
// 热区编辑对话框
|
||||
const editDialogRef = ref();
|
||||
// 打开热区编辑对话框
|
||||
const handleOpenEditDialog = () => {
|
||||
editDialogRef.value.open();
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ComponentContainerProperty v-model="formData.style">
|
||||
<!-- 表单 -->
|
||||
<ElForm label-width="80px" :model="formData" class="mt-2">
|
||||
<ElFormItem label="上传图片" prop="imgUrl">
|
||||
<UploadImg
|
||||
v-model="formData.imgUrl"
|
||||
height="50px"
|
||||
width="auto"
|
||||
class="min-w-[80px]"
|
||||
:show-description="false"
|
||||
>
|
||||
<template #tip>
|
||||
<ElText type="info" size="small"> 推荐宽度 750</ElText>
|
||||
</template>
|
||||
</UploadImg>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
|
||||
<ElButton type="primary" plain class="w-full" @click="handleOpenEditDialog">
|
||||
设置热区
|
||||
</ElButton>
|
||||
</ComponentContainerProperty>
|
||||
<!-- 热区编辑对话框 -->
|
||||
<HotZoneEditDialog
|
||||
ref="editDialogRef"
|
||||
v-model="formData.list"
|
||||
: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>
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
import type {
|
||||
ComponentStyle,
|
||||
DiyComponent,
|
||||
} from '#/components/diy-editor/util';
|
||||
|
||||
/** 图片展示属性 */
|
||||
export interface ImageBarProperty {
|
||||
// 图片链接
|
||||
imgUrl: string;
|
||||
// 跳转链接
|
||||
url: string;
|
||||
// 组件样式
|
||||
style: ComponentStyle;
|
||||
}
|
||||
|
||||
// 定义组件
|
||||
export const component = {
|
||||
id: 'ImageBar',
|
||||
name: '图片展示',
|
||||
icon: 'ep:picture',
|
||||
property: {
|
||||
imgUrl: '',
|
||||
url: '',
|
||||
style: {
|
||||
bgType: 'color',
|
||||
bgColor: '#fff',
|
||||
marginBottom: 8,
|
||||
} as ComponentStyle,
|
||||
},
|
||||
} as DiyComponent<ImageBarProperty>;
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
<script setup lang="ts">
|
||||
import type { ImageBarProperty } from './config';
|
||||
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
|
||||
import { ElImage } from 'element-plus';
|
||||
|
||||
/** 图片展示 */
|
||||
defineOptions({ name: 'ImageBar' });
|
||||
|
||||
defineProps<{ property: ImageBarProperty }>();
|
||||
</script>
|
||||
<template>
|
||||
<!-- 无图片 -->
|
||||
<div
|
||||
class="flex h-12 items-center justify-center bg-gray-300"
|
||||
v-if="!property.imgUrl"
|
||||
>
|
||||
<IconifyIcon icon="ep:picture" class="text-3xl text-gray-600" />
|
||||
</div>
|
||||
<ElImage class="min-h-8" v-else :src="property.imgUrl" />
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
/* 图片 */
|
||||
img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
<script setup lang="ts">
|
||||
import type { ImageBarProperty } from './config';
|
||||
|
||||
import { useVModel } from '@vueuse/core';
|
||||
import { ElForm, ElFormItem } from 'element-plus';
|
||||
|
||||
import AppLinkInput from '#/components/app-link-input/index.vue';
|
||||
import ComponentContainerProperty from '#/components/diy-editor/components/component-container-property.vue';
|
||||
import UploadImg from '#/components/upload/image-upload.vue';
|
||||
|
||||
// 图片展示属性面板
|
||||
defineOptions({ name: 'ImageBarProperty' });
|
||||
|
||||
const props = defineProps<{ modelValue: ImageBarProperty }>();
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
const formData = useVModel(props, 'modelValue', emit);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ComponentContainerProperty v-model="formData.style">
|
||||
<ElForm label-width="80px" :model="formData">
|
||||
<ElFormItem label="上传图片" prop="imgUrl">
|
||||
<UploadImg
|
||||
v-model="formData.imgUrl"
|
||||
draggable="false"
|
||||
height="80px"
|
||||
width="100%"
|
||||
class="min-w-[80px]"
|
||||
:show-description="false"
|
||||
>
|
||||
<template #tip> 建议宽度750 </template>
|
||||
</UploadImg>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="链接" prop="url">
|
||||
<AppLinkInput v-model="formData.url" />
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
</ComponentContainerProperty>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
import { defineAsyncComponent } from 'vue';
|
||||
|
||||
/*
|
||||
* 组件注册
|
||||
*
|
||||
* 组件规范:
|
||||
* 1. 每个子目录就是一个独立的组件,每个目录包括以下三个文件:
|
||||
* 2. config.ts:组件配置,必选,用于定义组件、组件默认的属性、定义属性的类型
|
||||
* 3. index.vue:组件展示,用于展示组件的渲染效果。可以不提供,如 Page(页面设置),只需要属性配置表单即可
|
||||
* 4. property.vue:组件属性表单,用于配置组件,必选,
|
||||
*
|
||||
* 注:
|
||||
* 组件ID以config.ts中配置的id为准,与组件目录的名称无关,但还是建议组件目录的名称与组件ID保持一致
|
||||
*/
|
||||
|
||||
// 导入组件界面模块
|
||||
const viewModules: Record<string, any> = import.meta.glob('./*/*.vue');
|
||||
// 导入配置模块
|
||||
const configModules: Record<string, any> = import.meta.glob('./*/config.ts', {
|
||||
eager: true,
|
||||
});
|
||||
|
||||
// 界面模块
|
||||
const components: Record<string, any> = {};
|
||||
// 组件配置模块
|
||||
const componentConfigs: Record<string, any> = {};
|
||||
|
||||
// 组件界面的类型
|
||||
type ViewType = 'index' | 'property';
|
||||
|
||||
/**
|
||||
* 注册组件的界面模块
|
||||
*
|
||||
* @param componentId 组件ID
|
||||
* @param configPath 配置模块的文件路径
|
||||
* @param viewType 组件界面的类型
|
||||
*/
|
||||
const registerComponentViewModule = (
|
||||
componentId: string,
|
||||
configPath: string,
|
||||
viewType: ViewType,
|
||||
) => {
|
||||
const viewPath = configPath.replace('config.ts', `${viewType}.vue`);
|
||||
const viewModule = viewModules[viewPath];
|
||||
if (viewModule) {
|
||||
// 定义异步组件
|
||||
components[componentId] = defineAsyncComponent(viewModule);
|
||||
}
|
||||
};
|
||||
|
||||
// 注册
|
||||
Object.keys(configModules).forEach((modulePath: string) => {
|
||||
const component = configModules[modulePath].component;
|
||||
const componentId = component?.id;
|
||||
if (componentId) {
|
||||
// 注册组件
|
||||
componentConfigs[componentId] = component;
|
||||
// 注册预览界面
|
||||
registerComponentViewModule(componentId, modulePath, 'index');
|
||||
// 注册属性配置表单
|
||||
registerComponentViewModule(
|
||||
`${componentId}Property`,
|
||||
modulePath,
|
||||
'property',
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export { componentConfigs, components };
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
import type {
|
||||
ComponentStyle,
|
||||
DiyComponent,
|
||||
} from '#/components/diy-editor/util';
|
||||
|
||||
/** 广告魔方属性 */
|
||||
export interface MagicCubeProperty {
|
||||
// 上圆角
|
||||
borderRadiusTop: number;
|
||||
// 下圆角
|
||||
borderRadiusBottom: number;
|
||||
// 间隔
|
||||
space: number;
|
||||
// 导航菜单列表
|
||||
list: MagicCubeItemProperty[];
|
||||
// 组件样式
|
||||
style: ComponentStyle;
|
||||
}
|
||||
|
||||
/** 广告魔方项目属性 */
|
||||
export interface MagicCubeItemProperty {
|
||||
// 图标链接
|
||||
imgUrl: string;
|
||||
// 链接
|
||||
url: string;
|
||||
// 宽
|
||||
width: number;
|
||||
// 高
|
||||
height: number;
|
||||
// 上
|
||||
top: number;
|
||||
// 左
|
||||
left: number;
|
||||
// 右
|
||||
right: number;
|
||||
// 下
|
||||
bottom: number;
|
||||
}
|
||||
|
||||
// 定义组件
|
||||
export const component = {
|
||||
id: 'MagicCube',
|
||||
name: '广告魔方',
|
||||
icon: 'bi:columns',
|
||||
property: {
|
||||
borderRadiusTop: 0,
|
||||
borderRadiusBottom: 0,
|
||||
space: 0,
|
||||
list: [],
|
||||
style: {
|
||||
bgType: 'color',
|
||||
bgColor: '#fff',
|
||||
marginBottom: 8,
|
||||
} as ComponentStyle,
|
||||
},
|
||||
} as DiyComponent<MagicCubeProperty>;
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
<script setup lang="ts">
|
||||
import type { MagicCubeProperty } from './config';
|
||||
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
|
||||
import { ElImage } from 'element-plus';
|
||||
|
||||
/** 广告魔方 */
|
||||
defineOptions({ name: 'MagicCube' });
|
||||
const props = defineProps<{ property: MagicCubeProperty }>();
|
||||
// 一个方块的大小
|
||||
const CUBE_SIZE = 93.75;
|
||||
/**
|
||||
* 计算方块的行数
|
||||
* 行数用于计算魔方的总体高度,存在以下情况:
|
||||
* 1. 没有数据时,默认就只显示一行的高度
|
||||
* 2. 底部的空白不算高度,例如只有第一行有数据,那么就只显示一行的高度
|
||||
* 3. 顶部及中间的空白算高度,例如一共有四行,只有最后一行有数据,那么也显示四行的高度
|
||||
*/
|
||||
const rowCount = computed(() => {
|
||||
let count = 0;
|
||||
if (props.property.list.length > 0) {
|
||||
// 最大行号
|
||||
count = Math.max(
|
||||
...props.property.list.map((item) => item.top + item.height),
|
||||
);
|
||||
}
|
||||
// 保证至少有一行
|
||||
return count === 0 ? 1 : count;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="relative"
|
||||
:style="{
|
||||
height: `${rowCount * CUBE_SIZE}px`,
|
||||
width: `${4 * CUBE_SIZE}px`,
|
||||
padding: `${property.space}px`,
|
||||
}"
|
||||
>
|
||||
<div
|
||||
v-for="(item, index) in property.list"
|
||||
:key="index"
|
||||
class="absolute"
|
||||
:style="{
|
||||
width: `${item.width * CUBE_SIZE - property.space}px`,
|
||||
height: `${item.height * CUBE_SIZE - property.space}px`,
|
||||
top: `${item.top * CUBE_SIZE}px`,
|
||||
left: `${item.left * CUBE_SIZE}px`,
|
||||
}"
|
||||
>
|
||||
<ElImage
|
||||
class="h-full w-full"
|
||||
fit="cover"
|
||||
:src="item.imgUrl"
|
||||
:style="{
|
||||
borderTopLeftRadius: `${property.borderRadiusTop}px`,
|
||||
borderTopRightRadius: `${property.borderRadiusTop}px`,
|
||||
borderBottomLeftRadius: `${property.borderRadiusBottom}px`,
|
||||
borderBottomRightRadius: `${property.borderRadiusBottom}px`,
|
||||
}"
|
||||
>
|
||||
<template #error>
|
||||
<div class="image-slot">
|
||||
<div
|
||||
class="flex items-center justify-center"
|
||||
:style="{
|
||||
width: `${item.width * CUBE_SIZE}px`,
|
||||
height: `${item.height * CUBE_SIZE}px`,
|
||||
}"
|
||||
>
|
||||
<IconifyIcon icon="ep-picture" color="gray" :size="CUBE_SIZE" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</ElImage>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
<script setup lang="ts">
|
||||
import type { MagicCubeProperty } from './config';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useVModel } from '@vueuse/core';
|
||||
import { ElForm, ElFormItem, ElSlider, ElText } from 'element-plus';
|
||||
|
||||
import AppLinkInput from '#/components/app-link-input/index.vue';
|
||||
import ComponentContainerProperty from '#/components/diy-editor/components/component-container-property.vue';
|
||||
import MagicCubeEditor from '#/components/magic-cube-editor/index.vue';
|
||||
import UploadImg from '#/components/upload/image-upload.vue';
|
||||
|
||||
/** 广告魔方属性面板 */
|
||||
defineOptions({ name: 'MagicCubeProperty' });
|
||||
|
||||
const props = defineProps<{ modelValue: MagicCubeProperty }>();
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
const formData = useVModel(props, 'modelValue', emit);
|
||||
|
||||
// 选中的热区
|
||||
const selectedHotAreaIndex = ref(-1);
|
||||
const handleHotAreaSelected = (_: any, index: number) => {
|
||||
selectedHotAreaIndex.value = index;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ComponentContainerProperty v-model="formData.style">
|
||||
<!-- 表单 -->
|
||||
<ElForm label-width="80px" :model="formData" class="mt-2">
|
||||
<ElText tag="p"> 魔方设置 </ElText>
|
||||
<ElText type="info" size="small"> 每格尺寸187 * 187 </ElText>
|
||||
<MagicCubeEditor
|
||||
class="my-4"
|
||||
v-model="formData.list"
|
||||
:rows="4"
|
||||
:cols="4"
|
||||
@hot-area-selected="handleHotAreaSelected"
|
||||
/>
|
||||
<template v-for="(hotArea, index) in formData.list" :key="index">
|
||||
<template v-if="selectedHotAreaIndex === index">
|
||||
<ElFormItem label="上传图片" :prop="`list[${index}].imgUrl`">
|
||||
<UploadImg
|
||||
v-model="hotArea.imgUrl"
|
||||
height="80px"
|
||||
width="80px"
|
||||
:show-description="false"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="链接" :prop="`list[${index}].url`">
|
||||
<AppLinkInput v-model="hotArea.url" />
|
||||
</ElFormItem>
|
||||
</template>
|
||||
</template>
|
||||
<ElFormItem label="上圆角" prop="borderRadiusTop">
|
||||
<ElSlider
|
||||
v-model="formData.borderRadiusTop"
|
||||
:max="100"
|
||||
:min="0"
|
||||
show-input
|
||||
input-size="small"
|
||||
:show-input-controls="false"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="下圆角" prop="borderRadiusBottom">
|
||||
<ElSlider
|
||||
v-model="formData.borderRadiusBottom"
|
||||
:max="100"
|
||||
:min="0"
|
||||
show-input
|
||||
input-size="small"
|
||||
:show-input-controls="false"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="间隔" prop="space">
|
||||
<ElSlider
|
||||
v-model="formData.space"
|
||||
:max="100"
|
||||
:min="0"
|
||||
show-input
|
||||
input-size="small"
|
||||
:show-input-controls="false"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
</ComponentContainerProperty>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
import type {
|
||||
ComponentStyle,
|
||||
DiyComponent,
|
||||
} from '#/components/diy-editor/util';
|
||||
|
||||
import { cloneDeep } from '@vben/utils';
|
||||
|
||||
/** 宫格导航属性 */
|
||||
export interface MenuGridProperty {
|
||||
// 列数
|
||||
column: number;
|
||||
// 导航菜单列表
|
||||
list: MenuGridItemProperty[];
|
||||
// 组件样式
|
||||
style: ComponentStyle;
|
||||
}
|
||||
|
||||
/** 宫格导航项目属性 */
|
||||
export interface MenuGridItemProperty {
|
||||
// 图标链接
|
||||
iconUrl: string;
|
||||
// 标题
|
||||
title: string;
|
||||
// 标题颜色
|
||||
titleColor: string;
|
||||
// 副标题
|
||||
subtitle: string;
|
||||
// 副标题颜色
|
||||
subtitleColor: string;
|
||||
// 链接
|
||||
url: string;
|
||||
// 角标
|
||||
badge: {
|
||||
// 角标背景颜色
|
||||
bgColor: string;
|
||||
// 是否显示
|
||||
show: boolean;
|
||||
// 角标文字
|
||||
text: string;
|
||||
// 角标文字颜色
|
||||
textColor: string;
|
||||
};
|
||||
}
|
||||
|
||||
export const EMPTY_MENU_GRID_ITEM_PROPERTY = {
|
||||
title: '标题',
|
||||
titleColor: '#333',
|
||||
subtitle: '副标题',
|
||||
subtitleColor: '#bbb',
|
||||
badge: {
|
||||
show: false,
|
||||
textColor: '#fff',
|
||||
bgColor: '#FF6000',
|
||||
},
|
||||
} as MenuGridItemProperty;
|
||||
|
||||
// 定义组件
|
||||
export const component = {
|
||||
id: 'MenuGrid',
|
||||
name: '宫格导航',
|
||||
icon: 'bi:grid-3x3-gap',
|
||||
property: {
|
||||
column: 3,
|
||||
list: [cloneDeep(EMPTY_MENU_GRID_ITEM_PROPERTY)],
|
||||
style: {
|
||||
bgType: 'color',
|
||||
bgColor: '#fff',
|
||||
marginBottom: 8,
|
||||
marginLeft: 8,
|
||||
marginRight: 8,
|
||||
padding: 8,
|
||||
paddingTop: 8,
|
||||
paddingRight: 8,
|
||||
paddingBottom: 8,
|
||||
paddingLeft: 8,
|
||||
borderRadius: 8,
|
||||
borderTopLeftRadius: 8,
|
||||
borderTopRightRadius: 8,
|
||||
borderBottomRightRadius: 8,
|
||||
borderBottomLeftRadius: 8,
|
||||
} as ComponentStyle,
|
||||
},
|
||||
} as DiyComponent<MenuGridProperty>;
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
<script setup lang="ts">
|
||||
import type { MenuGridProperty } from './config';
|
||||
|
||||
import { ElImage } from 'element-plus';
|
||||
|
||||
/** 宫格导航 */
|
||||
defineOptions({ name: 'MenuGrid' });
|
||||
defineProps<{ property: MenuGridProperty }>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-row flex-wrap">
|
||||
<div
|
||||
v-for="(item, index) in property.list"
|
||||
:key="index"
|
||||
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"
|
||||
:style="{
|
||||
color: item.badge.textColor,
|
||||
backgroundColor: item.badge.bgColor,
|
||||
}"
|
||||
>
|
||||
{{ item.badge.text }}
|
||||
</span>
|
||||
<ElImage v-if="item.iconUrl" class="h-7 w-7" :src="item.iconUrl" />
|
||||
<span
|
||||
class="mt-2 h-4 text-xs leading-4"
|
||||
:style="{ color: item.titleColor }"
|
||||
>
|
||||
{{ item.title }}
|
||||
</span>
|
||||
<span
|
||||
class="mt-1.5 h-3 text-xs leading-3"
|
||||
:style="{ color: item.subtitleColor }"
|
||||
>
|
||||
{{ item.subtitle }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
<script setup lang="ts">
|
||||
import type { MenuGridProperty } from './config';
|
||||
|
||||
import { useVModel } from '@vueuse/core';
|
||||
import {
|
||||
ElCard,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElRadio,
|
||||
ElRadioGroup,
|
||||
ElSwitch,
|
||||
} from 'element-plus';
|
||||
|
||||
import AppLinkInput from '#/components/app-link-input/index.vue';
|
||||
import ComponentContainerProperty from '#/components/diy-editor/components/component-container-property.vue';
|
||||
import Draggable from '#/components/draggable/index.vue';
|
||||
import UploadImg from '#/components/upload/image-upload.vue';
|
||||
|
||||
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>
|
||||
|
||||
<template>
|
||||
<ComponentContainerProperty v-model="formData.style">
|
||||
<!-- 表单 -->
|
||||
<ElForm label-width="80px" :model="formData" class="mt-2">
|
||||
<ElFormItem label="每行数量" prop="column">
|
||||
<ElRadioGroup v-model="formData.column">
|
||||
<ElRadio :value="3">3个</ElRadio>
|
||||
<ElRadio :value="4">4个</ElRadio>
|
||||
</ElRadioGroup>
|
||||
</ElFormItem>
|
||||
|
||||
<ElCard header="菜单设置" class="property-group" shadow="never">
|
||||
<Draggable
|
||||
v-model="formData.list"
|
||||
:empty-item="EMPTY_MENU_GRID_ITEM_PROPERTY"
|
||||
>
|
||||
<template #default="{ element }">
|
||||
<ElFormItem label="图标" prop="iconUrl">
|
||||
<UploadImg
|
||||
v-model="element.iconUrl"
|
||||
height="80px"
|
||||
width="80px"
|
||||
:show-description="false"
|
||||
>
|
||||
<template #tip> 建议尺寸:44 * 44 </template>
|
||||
</UploadImg>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="标题" prop="title">
|
||||
<InputWithColor
|
||||
v-model="element.title"
|
||||
v-model:color="element.titleColor"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="副标题" prop="subtitle">
|
||||
<InputWithColor
|
||||
v-model="element.subtitle"
|
||||
v-model:color="element.subtitleColor"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="链接" prop="url">
|
||||
<AppLinkInput v-model="element.url" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="显示角标" prop="badge.show">
|
||||
<ElSwitch v-model="element.badge.show" />
|
||||
</ElFormItem>
|
||||
<template v-if="element.badge.show">
|
||||
<ElFormItem label="角标内容" prop="badge.text">
|
||||
<InputWithColor
|
||||
v-model="element.badge.text"
|
||||
v-model:color="element.badge.textColor"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="背景颜色" prop="badge.bgColor">
|
||||
<ColorInput v-model="element.badge.bgColor" />
|
||||
</ElFormItem>
|
||||
</template>
|
||||
</template>
|
||||
</Draggable>
|
||||
</ElCard>
|
||||
</ElForm>
|
||||
</ComponentContainerProperty>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
import type {
|
||||
ComponentStyle,
|
||||
DiyComponent,
|
||||
} from '#/components/diy-editor/util';
|
||||
|
||||
import { cloneDeep } from '@vben/utils';
|
||||
|
||||
/** 列表导航属性 */
|
||||
export interface MenuListProperty {
|
||||
// 导航菜单列表
|
||||
list: MenuListItemProperty[];
|
||||
// 组件样式
|
||||
style: ComponentStyle;
|
||||
}
|
||||
|
||||
/** 列表导航项目属性 */
|
||||
export interface MenuListItemProperty {
|
||||
// 图标链接
|
||||
iconUrl: string;
|
||||
// 标题
|
||||
title: string;
|
||||
// 标题颜色
|
||||
titleColor: string;
|
||||
// 副标题
|
||||
subtitle: string;
|
||||
// 副标题颜色
|
||||
subtitleColor: string;
|
||||
// 链接
|
||||
url: string;
|
||||
}
|
||||
|
||||
export const EMPTY_MENU_LIST_ITEM_PROPERTY = {
|
||||
title: '标题',
|
||||
titleColor: '#333',
|
||||
subtitle: '副标题',
|
||||
subtitleColor: '#bbb',
|
||||
};
|
||||
|
||||
// 定义组件
|
||||
export const component = {
|
||||
id: 'MenuList',
|
||||
name: '列表导航',
|
||||
icon: 'fa-solid:list',
|
||||
property: {
|
||||
list: [cloneDeep(EMPTY_MENU_LIST_ITEM_PROPERTY)],
|
||||
style: {
|
||||
bgType: 'color',
|
||||
bgColor: '#fff',
|
||||
marginBottom: 8,
|
||||
} as ComponentStyle,
|
||||
},
|
||||
} as DiyComponent<MenuListProperty>;
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
<script setup lang="ts">
|
||||
import type { MenuListProperty } from './config';
|
||||
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
|
||||
import { ElImage } from 'element-plus';
|
||||
|
||||
/** 列表导航 */
|
||||
defineOptions({ name: 'MenuList' });
|
||||
defineProps<{ property: MenuListProperty }>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex min-h-[42px] flex-col">
|
||||
<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"
|
||||
>
|
||||
<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>
|
||||
</div>
|
||||
<div class="item-center flex flex-row justify-center gap-1">
|
||||
<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>
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
<script setup lang="ts">
|
||||
import type { MenuListProperty } from './config';
|
||||
|
||||
import { useVModel } from '@vueuse/core';
|
||||
import { ElForm, ElFormItem, ElText } from 'element-plus';
|
||||
|
||||
import AppLinkInput from '#/components/app-link-input/index.vue';
|
||||
import ComponentContainerProperty from '#/components/diy-editor/components/component-container-property.vue';
|
||||
import Draggable from '#/components/draggable/index.vue';
|
||||
import InputWithColor from '#/components/input-with-color/index.vue';
|
||||
import UploadImg from '#/components/upload/image-upload.vue';
|
||||
|
||||
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>
|
||||
|
||||
<template>
|
||||
<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"
|
||||
:empty-item="EMPTY_MENU_LIST_ITEM_PROPERTY"
|
||||
>
|
||||
<template #default="{ element }">
|
||||
<ElFormItem label="图标" prop="iconUrl">
|
||||
<UploadImg
|
||||
v-model="element.iconUrl"
|
||||
height="80px"
|
||||
width="80px"
|
||||
:show-description="false"
|
||||
>
|
||||
<template #tip> 建议尺寸:44 * 44 </template>
|
||||
</UploadImg>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="标题" prop="title">
|
||||
<InputWithColor
|
||||
v-model="element.title"
|
||||
v-model:color="element.titleColor"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="副标题" prop="subtitle">
|
||||
<InputWithColor
|
||||
v-model="element.subtitle"
|
||||
v-model:color="element.subtitleColor"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="链接" prop="url">
|
||||
<AppLinkInput v-model="element.url" />
|
||||
</ElFormItem>
|
||||
</template>
|
||||
</Draggable>
|
||||
</ElForm>
|
||||
</ComponentContainerProperty>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
import type {
|
||||
ComponentStyle,
|
||||
DiyComponent,
|
||||
} from '#/components/diy-editor/util';
|
||||
|
||||
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;
|
||||
};
|
||||
}
|
||||
|
||||
export const EMPTY_MENU_SWIPER_ITEM_PROPERTY = {
|
||||
title: '标题',
|
||||
titleColor: '#333',
|
||||
badge: {
|
||||
show: false,
|
||||
textColor: '#fff',
|
||||
bgColor: '#FF6000',
|
||||
},
|
||||
} as MenuSwiperItemProperty;
|
||||
|
||||
// 定义组件
|
||||
export const component = {
|
||||
id: 'MenuSwiper',
|
||||
name: '菜单导航',
|
||||
icon: 'bi:grid-3x2-gap',
|
||||
property: {
|
||||
layout: 'iconText',
|
||||
row: 1,
|
||||
column: 3,
|
||||
list: [cloneDeep(EMPTY_MENU_SWIPER_ITEM_PROPERTY)],
|
||||
style: {
|
||||
bgType: 'color',
|
||||
bgColor: '#fff',
|
||||
marginBottom: 8,
|
||||
} as ComponentStyle,
|
||||
},
|
||||
} as DiyComponent<MenuSwiperProperty>;
|
||||
|
|
@ -0,0 +1,137 @@
|
|||
<script setup lang="ts">
|
||||
import type { MenuSwiperItemProperty, MenuSwiperProperty } from './config';
|
||||
|
||||
import { ref, watch } from 'vue';
|
||||
|
||||
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('');
|
||||
watch(
|
||||
() => props.property,
|
||||
() => {
|
||||
// 计算列宽:每一列的百分比
|
||||
columnWidth.value = `${100 * (1 / props.property.column)}%`;
|
||||
// 计算行高:图标 + 文字(仅显示图片时为0) + 垂直间距 * 2
|
||||
rowHeight.value =
|
||||
(props.property.layout === 'iconText'
|
||||
? ICON_SIZE + TITLE_HEIGHT
|
||||
: ICON_SIZE) +
|
||||
SPACE_Y * 2;
|
||||
// 计算轮播的高度:行数 * 行高
|
||||
carouselHeight.value = props.property.row * rowHeight.value;
|
||||
|
||||
// 每页数量:行数 * 列数
|
||||
const pageSize = props.property.row * props.property.column;
|
||||
// 清空分页
|
||||
pages.value = [];
|
||||
// 每一页的菜单
|
||||
let pageItems: MenuSwiperItemProperty[] = [];
|
||||
for (const item of props.property.list) {
|
||||
// 本页满员,新建下一页
|
||||
if (pageItems.length === pageSize) {
|
||||
pageItems = [];
|
||||
}
|
||||
// 增加一页
|
||||
if (pageItems.length === 0) {
|
||||
pages.value.push(pageItems);
|
||||
}
|
||||
// 本页增加一个
|
||||
pageItems.push(item);
|
||||
}
|
||||
},
|
||||
{ immediate: true, deep: true },
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElCarousel
|
||||
:height="`${carouselHeight}px`"
|
||||
:autoplay="false"
|
||||
arrow="hover"
|
||||
indicator-position="outside"
|
||||
>
|
||||
<ElCarouselItem v-for="(page, pageIndex) in pages" :key="pageIndex">
|
||||
<div class="flex flex-row flex-wrap">
|
||||
<div
|
||||
v-for="(item, index) in page"
|
||||
:key="index"
|
||||
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"
|
||||
:style="{
|
||||
color: item.badge.textColor,
|
||||
backgroundColor: item.badge.bgColor,
|
||||
}"
|
||||
>
|
||||
{{ item.badge.text }}
|
||||
</span>
|
||||
<ElImage
|
||||
v-if="item.iconUrl"
|
||||
:src="item.iconUrl"
|
||||
class="h-full w-full"
|
||||
/>
|
||||
</div>
|
||||
<!-- 标题 -->
|
||||
<span
|
||||
v-if="property.layout === 'iconText'"
|
||||
class="text-xs"
|
||||
:style="{
|
||||
color: item.titleColor,
|
||||
height: `${TITLE_HEIGHT}px`,
|
||||
lineHeight: `${TITLE_HEIGHT}px`,
|
||||
}"
|
||||
>
|
||||
{{ item.title }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</ElCarouselItem>
|
||||
</ElCarousel>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
// 重写指示器样式,与 APP 保持一致
|
||||
:root {
|
||||
.el-carousel__indicator {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
|
||||
.el-carousel__button {
|
||||
--el-carousel-indicator-height: 6px;
|
||||
--el-carousel-indicator-width: 6px;
|
||||
--el-carousel-indicator-out-color: #ff6000;
|
||||
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.el-carousel__indicator.is-active {
|
||||
.el-carousel__button {
|
||||
--el-carousel-indicator-width: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,103 @@
|
|||
<script setup lang="ts">
|
||||
import type { MenuSwiperProperty } from './config';
|
||||
|
||||
import { cloneDeep } from '@vben/utils';
|
||||
|
||||
import { useVModel } from '@vueuse/core';
|
||||
import {
|
||||
ElCard,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElRadio,
|
||||
ElRadioGroup,
|
||||
ElSwitch,
|
||||
} from 'element-plus';
|
||||
|
||||
import AppLinkInput from '#/components/app-link-input/index.vue';
|
||||
import ColorInput from '#/components/color-input/index.vue';
|
||||
import ComponentContainerProperty from '#/components/diy-editor/components/component-container-property.vue';
|
||||
import Draggable from '#/components/draggable/index.vue';
|
||||
import InputWithColor from '#/components/input-with-color/index.vue';
|
||||
import UploadImg from '#/components/upload/image-upload.vue';
|
||||
|
||||
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">
|
||||
<ElRadio value="iconText">图标+文字</ElRadio>
|
||||
<ElRadio value="icon">仅图标</ElRadio>
|
||||
</ElRadioGroup>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="行数" prop="row">
|
||||
<ElRadioGroup v-model="formData.row">
|
||||
<ElRadio :value="1">1行</ElRadio>
|
||||
<ElRadio :value="2">2行</ElRadio>
|
||||
</ElRadioGroup>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="列数" prop="column">
|
||||
<ElRadioGroup v-model="formData.column">
|
||||
<ElRadio :value="3">3列</ElRadio>
|
||||
<ElRadio :value="4">4列</ElRadio>
|
||||
<ElRadio :value="5">5列</ElRadio>
|
||||
</ElRadioGroup>
|
||||
</ElFormItem>
|
||||
|
||||
<ElCard header="菜单设置" class="property-group" shadow="never">
|
||||
<Draggable
|
||||
v-model="formData.list"
|
||||
:empty-item="cloneDeep(EMPTY_MENU_SWIPER_ITEM_PROPERTY)"
|
||||
>
|
||||
<template #default="{ element }">
|
||||
<ElFormItem label="图标" prop="iconUrl">
|
||||
<UploadImg
|
||||
v-model="element.iconUrl"
|
||||
height="80px"
|
||||
width="80px"
|
||||
:show-description="false"
|
||||
>
|
||||
<template #tip> 建议尺寸:98 * 98 </template>
|
||||
</UploadImg>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="标题" prop="title">
|
||||
<InputWithColor
|
||||
v-model="element.title"
|
||||
v-model:color="element.titleColor"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="链接" prop="url">
|
||||
<AppLinkInput v-model="element.url" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="显示角标" prop="badge.show">
|
||||
<ElSwitch v-model="element.badge.show" />
|
||||
</ElFormItem>
|
||||
<template v-if="element.badge.show">
|
||||
<ElFormItem label="角标内容" prop="badge.text">
|
||||
<InputWithColor
|
||||
v-model="element.badge.text"
|
||||
v-model:color="element.badge.textColor"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="背景颜色" prop="badge.bgColor">
|
||||
<ColorInput v-model="element.badge.bgColor" />
|
||||
</ElFormItem>
|
||||
</template>
|
||||
</template>
|
||||
</Draggable>
|
||||
</ElCard>
|
||||
</ElForm>
|
||||
</ComponentContainerProperty>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
|
@ -0,0 +1,143 @@
|
|||
<script lang="ts" setup>
|
||||
import type { NavigationBarCellProperty } from '../config';
|
||||
|
||||
import type { Rect } from '#/components/magic-cube-editor/util';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVModel } from '@vueuse/core';
|
||||
import {
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElRadio,
|
||||
ElRadioGroup,
|
||||
ElSlider,
|
||||
} from 'element-plus';
|
||||
|
||||
import appNavBarMp from '#/assets/imgs/diy/app-nav-bar-mp.png';
|
||||
import AppLinkInput from '#/components/app-link-input/index.vue';
|
||||
import ColorInput from '#/components/color-input/index.vue';
|
||||
import MagicCubeEditor from '#/components/magic-cube-editor/index.vue';
|
||||
import UploadImg from '#/components/upload/image-upload.vue';
|
||||
|
||||
// 导航栏属性面板
|
||||
defineOptions({ name: 'NavigationBarCellProperty' });
|
||||
|
||||
const props = defineProps({
|
||||
isMp: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
modelValue: {
|
||||
type: Array as () => NavigationBarCellProperty[],
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
const cellList = useVModel(props, 'modelValue', emit);
|
||||
|
||||
// 单元格数量:小程序6个(右侧胶囊按钮占了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 handleHotAreaSelected = (
|
||||
cellValue: NavigationBarCellProperty,
|
||||
index: number,
|
||||
) => {
|
||||
selectedHotAreaIndex.value = index;
|
||||
if (!cellValue.type) {
|
||||
cellValue.type = 'text';
|
||||
cellValue.textColor = '#111111';
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-40px flex items-center justify-center">
|
||||
<MagicCubeEditor
|
||||
v-model="rectList"
|
||||
:cols="cellCount"
|
||||
:cube-size="38"
|
||||
:rows="1"
|
||||
class="m-b-16px"
|
||||
@hot-area-selected="handleHotAreaSelected"
|
||||
/>
|
||||
<img
|
||||
v-if="isMp"
|
||||
alt=""
|
||||
style="width: 76px; height: 30px"
|
||||
:src="appNavBarMp"
|
||||
/>
|
||||
</div>
|
||||
<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">
|
||||
<ElRadio value="text">文字</ElRadio>
|
||||
<ElRadio value="image">图片</ElRadio>
|
||||
<ElRadio value="search">搜索框</ElRadio>
|
||||
</ElRadioGroup>
|
||||
</ElFormItem>
|
||||
<!-- 1. 文字 -->
|
||||
<template v-if="cell.type === 'text'">
|
||||
<ElFormItem :prop="`cell[${cellIndex}].text`" label="内容">
|
||||
<ElInput v-model="cell!.text" maxlength="10" show-word-limit />
|
||||
</ElFormItem>
|
||||
<ElFormItem :prop="`cell[${cellIndex}].text`" label="颜色">
|
||||
<ColorInput v-model="cell!.textColor" />
|
||||
</ElFormItem>
|
||||
<ElFormItem :prop="`cell[${cellIndex}].url`" label="链接">
|
||||
<AppLinkInput v-model="cell.url" />
|
||||
</ElFormItem>
|
||||
</template>
|
||||
<!-- 2. 图片 -->
|
||||
<template v-else-if="cell.type === 'image'">
|
||||
<ElFormItem :prop="`cell[${cellIndex}].imgUrl`" label="图片">
|
||||
<UploadImg
|
||||
v-model="cell.imgUrl"
|
||||
:limit="1"
|
||||
height="56px"
|
||||
width="56px"
|
||||
:show-description="false"
|
||||
>
|
||||
<template #tip>建议尺寸 56*56</template>
|
||||
</UploadImg>
|
||||
</ElFormItem>
|
||||
<ElFormItem :prop="`cell[${cellIndex}].url`" label="链接">
|
||||
<AppLinkInput v-model="cell.url" />
|
||||
</ElFormItem>
|
||||
</template>
|
||||
<!-- 3. 搜索框 -->
|
||||
<template v-else>
|
||||
<ElFormItem :prop="`cell[${cellIndex}].placeholder`" label="提示文字">
|
||||
<ElInput v-model="cell.placeholder" maxlength="10" show-word-limit />
|
||||
</ElFormItem>
|
||||
<ElFormItem :prop="`cell[${cellIndex}].borderRadius`" label="圆角">
|
||||
<ElSlider
|
||||
v-model="cell.borderRadius"
|
||||
:max="100"
|
||||
:min="0"
|
||||
:show-input-controls="false"
|
||||
input-size="small"
|
||||
show-input
|
||||
/>
|
||||
</ElFormItem>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
import type { DiyComponent } from '#/components/diy-editor/util';
|
||||
|
||||
/** 顶部导航栏属性 */
|
||||
export interface NavigationBarProperty {
|
||||
// 背景类型
|
||||
bgType: 'color' | 'img';
|
||||
// 背景颜色
|
||||
bgColor: string;
|
||||
// 图片链接
|
||||
bgImg: string;
|
||||
// 样式类型:默认 | 沉浸式
|
||||
styleType: 'inner' | 'normal';
|
||||
// 常驻显示
|
||||
alwaysShow: boolean;
|
||||
// 小程序单元格列表
|
||||
mpCells: NavigationBarCellProperty[];
|
||||
// 其它平台单元格列表
|
||||
otherCells: NavigationBarCellProperty[];
|
||||
// 本地变量
|
||||
_local: {
|
||||
// 预览顶部导航(小程序)
|
||||
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;
|
||||
}
|
||||
|
||||
// 定义组件
|
||||
export const component = {
|
||||
id: 'NavigationBar',
|
||||
name: '顶部导航栏',
|
||||
icon: 'tabler:layout-navbar',
|
||||
property: {
|
||||
bgType: 'color',
|
||||
bgColor: '#fff',
|
||||
bgImg: '',
|
||||
styleType: 'normal',
|
||||
alwaysShow: true,
|
||||
mpCells: [
|
||||
{
|
||||
type: 'text',
|
||||
textColor: '#111111',
|
||||
},
|
||||
],
|
||||
otherCells: [
|
||||
{
|
||||
type: 'text',
|
||||
textColor: '#111111',
|
||||
},
|
||||
],
|
||||
_local: {
|
||||
previewMp: true,
|
||||
previewOther: false,
|
||||
},
|
||||
},
|
||||
} as DiyComponent<NavigationBarProperty>;
|
||||
|
|
@ -0,0 +1,112 @@
|
|||
<script setup lang="ts">
|
||||
import type { StyleValue } from 'vue';
|
||||
|
||||
import type {
|
||||
NavigationBarCellProperty,
|
||||
NavigationBarProperty,
|
||||
} from './config';
|
||||
|
||||
import type { SearchProperty } from '#/components/diy-editor/components/mobile/search-bar/config';
|
||||
|
||||
import { computed } from 'vue';
|
||||
|
||||
import appNavbarMp from '#/assets/imgs/diy/app-nav-bar-mp.png';
|
||||
import SearchBar from '#/components/diy-editor/components/mobile/search-bar/index.vue';
|
||||
|
||||
/** 页面顶部导航栏 */
|
||||
defineOptions({ name: 'NavigationBar' });
|
||||
|
||||
const props = defineProps<{ property: NavigationBarProperty }>();
|
||||
|
||||
// 背景
|
||||
const bgStyle = computed(() => {
|
||||
const background =
|
||||
props.property.bgType === 'img' && props.property.bgImg
|
||||
? `url(${props.property.bgImg}) no-repeat top center / 100% 100%`
|
||||
: 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) => {
|
||||
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,
|
||||
showScan: false,
|
||||
placeholder: cell.placeholder,
|
||||
borderRadius: cell.borderRadius,
|
||||
} as SearchProperty;
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<div class="navigation-bar" :style="bgStyle">
|
||||
<div class="flex h-full w-full items-center">
|
||||
<div
|
||||
v-for="(cell, cellIndex) in cellList"
|
||||
:key="cellIndex"
|
||||
:style="getCellStyle(cell)"
|
||||
>
|
||||
<span v-if="cell.type === 'text'">{{ cell.text }}</span>
|
||||
<img
|
||||
v-else-if="cell.type === 'image'"
|
||||
:src="cell.imgUrl"
|
||||
alt=""
|
||||
class="h-full w-full"
|
||||
/>
|
||||
<SearchBar v-else :property="getSearchProp(cell)" />
|
||||
</div>
|
||||
</div>
|
||||
<img
|
||||
v-if="property._local?.previewMp"
|
||||
:src="appNavbarMp"
|
||||
alt=""
|
||||
style="width: 86px; height: 30px"
|
||||
/>
|
||||
</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>
|
||||
|
|
@ -0,0 +1,117 @@
|
|||
<script setup lang="ts">
|
||||
import type { NavigationBarProperty } from './config';
|
||||
|
||||
import { useVModel } from '@vueuse/core';
|
||||
|
||||
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) {
|
||||
formData.value._local = { previewMp: true, previewOther: false };
|
||||
}
|
||||
</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
|
||||
content="沉侵式头部仅支持微信小程序、APP,建议页面第一个组件为图片展示类组件"
|
||||
placement="top"
|
||||
>
|
||||
<el-radio value="inner">沉浸式</el-radio>
|
||||
</el-tooltip>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
label="常驻显示"
|
||||
prop="alwaysShow"
|
||||
v-if="formData.styleType === 'inner'"
|
||||
>
|
||||
<el-radio-group v-model="formData!.alwaysShow">
|
||||
<el-radio :value="false">关闭</el-radio>
|
||||
<el-tooltip
|
||||
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
|
||||
label="背景颜色"
|
||||
prop="bgColor"
|
||||
v-if="formData.bgType === 'color'"
|
||||
>
|
||||
<ColorInput v-model="formData.bgColor" />
|
||||
</el-form-item>
|
||||
<el-form-item label="背景图片" prop="bgImg" v-else>
|
||||
<div class="flex items-center">
|
||||
<UploadImg
|
||||
v-model="formData.bgImg"
|
||||
:limit="1"
|
||||
width="56px"
|
||||
height="56px"
|
||||
:show-description="false"
|
||||
/>
|
||||
<span class="mb-2 ml-2 text-xs text-gray-400">建议宽度:750</span>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-card 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
|
||||
v-model="formData._local.previewMp"
|
||||
@change="
|
||||
formData._local.previewOther = !formData._local.previewMp
|
||||
"
|
||||
>
|
||||
预览
|
||||
</el-checkbox>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</template>
|
||||
<NavigationBarCellProperty v-model="formData.mpCells" is-mp />
|
||||
</el-card>
|
||||
<el-card 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
|
||||
v-model="formData._local.previewOther"
|
||||
@change="
|
||||
formData._local.previewMp = !formData._local.previewOther
|
||||
"
|
||||
>
|
||||
预览
|
||||
</el-checkbox>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</template>
|
||||
<NavigationBarCellProperty v-model="formData.otherCells" :is-mp="false" />
|
||||
</el-card>
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
import type {
|
||||
ComponentStyle,
|
||||
DiyComponent,
|
||||
} from '#/components/diy-editor/util';
|
||||
|
||||
/** 公告栏属性 */
|
||||
export interface NoticeBarProperty {
|
||||
// 图标地址
|
||||
iconUrl: string;
|
||||
// 公告内容列表
|
||||
contents: NoticeContentProperty[];
|
||||
// 背景颜色
|
||||
backgroundColor: string;
|
||||
// 文字颜色
|
||||
textColor: string;
|
||||
// 组件样式
|
||||
style: ComponentStyle;
|
||||
}
|
||||
|
||||
/** 内容属性 */
|
||||
export interface NoticeContentProperty {
|
||||
// 内容文字
|
||||
text: string;
|
||||
// 链接地址
|
||||
url: string;
|
||||
}
|
||||
|
||||
// 定义组件
|
||||
export const component = {
|
||||
id: 'NoticeBar',
|
||||
name: '公告栏',
|
||||
icon: 'ep:bell',
|
||||
property: {
|
||||
iconUrl: 'http://mall.yudao.iocoder.cn/static/images/xinjian.png',
|
||||
contents: [
|
||||
{
|
||||
text: '',
|
||||
url: '',
|
||||
},
|
||||
],
|
||||
backgroundColor: '#fff',
|
||||
textColor: '#333',
|
||||
style: {
|
||||
bgType: 'color',
|
||||
bgColor: '#fff',
|
||||
marginBottom: 8,
|
||||
} as ComponentStyle,
|
||||
},
|
||||
} as DiyComponent<NoticeBarProperty>;
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
<script setup lang="ts">
|
||||
import type { NoticeBarProperty } from './config';
|
||||
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
|
||||
import { ElCarousel, ElCarouselItem, ElDivider, ElImage } from 'element-plus';
|
||||
|
||||
/** 公告栏 */
|
||||
defineOptions({ name: 'NoticeBar' });
|
||||
|
||||
defineProps<{ property: NoticeBarProperty }>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex items-center py-1 text-xs"
|
||||
:style="{
|
||||
backgroundColor: property.backgroundColor,
|
||||
color: property.textColor,
|
||||
}"
|
||||
>
|
||||
<ElImage :src="property.iconUrl" class="h-[18px]" />
|
||||
<ElDivider direction="vertical" />
|
||||
<ElCarousel
|
||||
height="24px"
|
||||
direction="vertical"
|
||||
:autoplay="true"
|
||||
class="flex-1 pr-2"
|
||||
>
|
||||
<ElCarouselItem v-for="(item, index) in property.contents" :key="index">
|
||||
<div class="h-6 truncate leading-6">{{ item.text }}</div>
|
||||
</ElCarouselItem>
|
||||
</ElCarousel>
|
||||
<IconifyIcon icon="ep:arrow-right" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
<script setup lang="ts">
|
||||
import type { NoticeBarProperty } from './config';
|
||||
|
||||
import { useVModel } from '@vueuse/core';
|
||||
import { ElCard, ElForm, ElFormItem, ElInput } from 'element-plus';
|
||||
|
||||
import AppLinkInput from '#/components/app-link-input/index.vue';
|
||||
import ColorInput from '#/components/color-input/index.vue';
|
||||
import ComponentContainerProperty from '#/components/diy-editor/components/component-container-property.vue';
|
||||
import Draggable from '#/components/draggable/index.vue';
|
||||
import UploadImg from '#/components/upload/image-upload.vue';
|
||||
|
||||
// 通知栏属性面板
|
||||
defineOptions({ name: 'NoticeBarProperty' });
|
||||
const props = defineProps<{ modelValue: NoticeBarProperty }>();
|
||||
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
|
||||
// 表单校验
|
||||
const rules = {
|
||||
content: [{ required: true, message: '请输入公告', trigger: 'blur' }],
|
||||
};
|
||||
|
||||
const formData = useVModel(props, 'modelValue', emit);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ComponentContainerProperty v-model="formData.style">
|
||||
<ElForm label-width="80px" :model="formData" :rules="rules">
|
||||
<ElFormItem label="公告图标" prop="iconUrl">
|
||||
<UploadImg
|
||||
v-model="formData.iconUrl"
|
||||
height="48px"
|
||||
:show-description="false"
|
||||
>
|
||||
<template #tip>建议尺寸:24 * 24</template>
|
||||
</UploadImg>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="背景颜色" prop="backgroundColor">
|
||||
<ColorInput v-model="formData.backgroundColor" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="文字颜色" prop="文字颜色">
|
||||
<ColorInput v-model="formData.textColor" />
|
||||
</ElFormItem>
|
||||
<ElCard header="公告内容" class="property-group" shadow="never">
|
||||
<Draggable v-model="formData.contents">
|
||||
<template #default="{ element }">
|
||||
<ElFormItem label="公告" prop="text" label-width="40px">
|
||||
<ElInput v-model="element.text" placeholder="请输入公告" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="链接" prop="url" label-width="40px">
|
||||
<AppLinkInput v-model="element.url" />
|
||||
</ElFormItem>
|
||||
</template>
|
||||
</Draggable>
|
||||
</ElCard>
|
||||
</ElForm>
|
||||
</ComponentContainerProperty>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
import type { DiyComponent } from '#/components/diy-editor/util';
|
||||
|
||||
/** 页面设置属性 */
|
||||
export interface PageConfigProperty {
|
||||
// 页面描述
|
||||
description: string;
|
||||
// 页面背景颜色
|
||||
backgroundColor: string;
|
||||
// 页面背景图片
|
||||
backgroundImage: string;
|
||||
}
|
||||
|
||||
// 定义页面组件
|
||||
export const component = {
|
||||
id: 'PageConfig',
|
||||
name: '页面设置',
|
||||
icon: 'ep:document',
|
||||
property: {
|
||||
description: '',
|
||||
backgroundColor: '#f5f5f5',
|
||||
backgroundImage: '',
|
||||
},
|
||||
} as DiyComponent<PageConfigProperty>;
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
<script setup lang="ts">
|
||||
import type { PageConfigProperty } from './config';
|
||||
|
||||
import { useVModel } from '@vueuse/core';
|
||||
import { ElForm, ElFormItem, ElInput } from 'element-plus';
|
||||
|
||||
import ColorInput from '#/components/input-with-color/index.vue';
|
||||
import UploadImg from '#/components/upload/image-upload.vue';
|
||||
|
||||
// 导航栏属性面板
|
||||
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">
|
||||
<ElFormItem label="页面描述" prop="description">
|
||||
<ElInput
|
||||
type="textarea"
|
||||
v-model="formData!.description"
|
||||
placeholder="用户通过微信分享给朋友时,会自动显示页面描述"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="背景颜色" prop="backgroundColor">
|
||||
<ColorInput v-model="formData!.backgroundColor" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="背景图片" prop="backgroundImage">
|
||||
<UploadImg
|
||||
v-model="formData!.backgroundImage"
|
||||
:limit="1"
|
||||
:show-description="false"
|
||||
>
|
||||
<template #tip>建议宽度 750px</template>
|
||||
</UploadImg>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
import type {
|
||||
ComponentStyle,
|
||||
DiyComponent,
|
||||
} from '#/components/diy-editor/util';
|
||||
|
||||
/** 商品卡片属性 */
|
||||
export interface ProductCardProperty {
|
||||
// 布局类型:单列大图 | 单列小图 | 双列
|
||||
layoutType: 'oneColBigImg' | 'oneColSmallImg' | 'twoCol';
|
||||
// 商品字段
|
||||
fields: {
|
||||
// 商品简介
|
||||
introduction: ProductCardFieldProperty;
|
||||
// 商品市场价
|
||||
marketPrice: ProductCardFieldProperty;
|
||||
// 商品名称
|
||||
name: ProductCardFieldProperty;
|
||||
// 商品价格
|
||||
price: ProductCardFieldProperty;
|
||||
// 商品销量
|
||||
salesCount: ProductCardFieldProperty;
|
||||
// 商品库存
|
||||
stock: ProductCardFieldProperty;
|
||||
};
|
||||
// 角标
|
||||
badge: {
|
||||
// 角标图片
|
||||
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;
|
||||
}
|
||||
|
||||
// 定义组件
|
||||
export const component = {
|
||||
id: 'ProductCard',
|
||||
name: '商品卡片',
|
||||
icon: 'fluent:text-column-two-left-24-filled',
|
||||
property: {
|
||||
layoutType: 'oneColBigImg',
|
||||
fields: {
|
||||
name: { show: true, color: '#000' },
|
||||
introduction: { show: true, color: '#999' },
|
||||
price: { show: true, color: '#ff3000' },
|
||||
marketPrice: { show: true, color: '#c4c4c4' },
|
||||
salesCount: { show: true, color: '#c4c4c4' },
|
||||
stock: { show: false, color: '#c4c4c4' },
|
||||
},
|
||||
badge: { show: false, imgUrl: '' },
|
||||
btnBuy: {
|
||||
type: 'text',
|
||||
text: '立即购买',
|
||||
// todo: @owen 根据主题色配置
|
||||
bgBeginColor: '#FF6000',
|
||||
bgEndColor: '#FE832A',
|
||||
imgUrl: '',
|
||||
},
|
||||
borderRadiusTop: 6,
|
||||
borderRadiusBottom: 6,
|
||||
space: 8,
|
||||
spuIds: [],
|
||||
style: {
|
||||
bgType: 'color',
|
||||
bgColor: '',
|
||||
marginLeft: 8,
|
||||
marginRight: 8,
|
||||
marginBottom: 8,
|
||||
} as ComponentStyle,
|
||||
},
|
||||
} as DiyComponent<ProductCardProperty>;
|
||||
|
|
@ -0,0 +1,190 @@
|
|||
<script setup lang="ts">
|
||||
import type { ProductCardProperty } from './config';
|
||||
|
||||
import type { MallSpuApi } from '#/api/mall/product/spu';
|
||||
|
||||
import { ref, watch } from 'vue';
|
||||
|
||||
import { fenToYuan } from '@vben/utils';
|
||||
|
||||
import { ElImage } from 'element-plus';
|
||||
|
||||
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 () => {
|
||||
spuList.value = await ProductSpuApi.getSpuDetailList(props.property.spuIds);
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
deep: true,
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* 计算商品的间距
|
||||
* @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`;
|
||||
|
||||
return { marginLeft, marginTop };
|
||||
};
|
||||
|
||||
// 容器
|
||||
const containerRef = ref();
|
||||
// 计算商品的宽度
|
||||
const calculateWidth = () => {
|
||||
let width = '100%';
|
||||
// 双列时每列的宽度为:(总宽度 - 间距)/ 2
|
||||
if (props.property.layoutType === 'twoCol') {
|
||||
width = `${(containerRef.value.offsetWidth - props.property.space) / 2}px`;
|
||||
}
|
||||
return { width };
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div
|
||||
class="box-content flex min-h-[30px] w-full flex-row flex-wrap"
|
||||
ref="containerRef"
|
||||
>
|
||||
<div
|
||||
class="relative box-content flex flex-row flex-wrap overflow-hidden bg-white"
|
||||
:style="{
|
||||
...calculateSpace(index),
|
||||
...calculateWidth(),
|
||||
borderTopLeftRadius: `${property.borderRadiusTop}px`,
|
||||
borderTopRightRadius: `${property.borderRadiusTop}px`,
|
||||
borderBottomLeftRadius: `${property.borderRadiusBottom}px`,
|
||||
borderBottomRightRadius: `${property.borderRadiusBottom}px`,
|
||||
}"
|
||||
v-for="(spu, index) in spuList"
|
||||
:key="index"
|
||||
>
|
||||
<!-- 角标 -->
|
||||
<div
|
||||
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]"
|
||||
/>
|
||||
</div>
|
||||
<!-- 商品封面图 -->
|
||||
<div
|
||||
class="h-[140px]"
|
||||
:class="[
|
||||
{
|
||||
'w-full': property.layoutType !== 'oneColSmallImg',
|
||||
'w-[140px]': property.layoutType === 'oneColSmallImg',
|
||||
},
|
||||
]"
|
||||
>
|
||||
<ElImage fit="cover" class="h-full w-full" :src="spu.picUrl" />
|
||||
</div>
|
||||
<div
|
||||
class="box-border flex flex-col gap-[8px] p-[8px]"
|
||||
:class="[
|
||||
{
|
||||
'w-full': property.layoutType !== 'oneColSmallImg',
|
||||
'w-[calc(100%-140px-16px)]':
|
||||
property.layoutType === 'oneColSmallImg',
|
||||
},
|
||||
]"
|
||||
>
|
||||
<!-- 商品名称 -->
|
||||
<div
|
||||
v-if="property.fields.name.show"
|
||||
class="text-[14px]"
|
||||
:class="[
|
||||
{
|
||||
truncate: property.layoutType !== 'oneColSmallImg',
|
||||
'line-clamp-2 overflow-ellipsis':
|
||||
property.layoutType === 'oneColSmallImg',
|
||||
},
|
||||
]"
|
||||
:style="{ color: property.fields.name.color }"
|
||||
>
|
||||
{{ spu.name }}
|
||||
</div>
|
||||
<!-- 商品简介 -->
|
||||
<div
|
||||
v-if="property.fields.introduction.show"
|
||||
class="truncate text-[12px]"
|
||||
:style="{ color: property.fields.introduction.color }"
|
||||
>
|
||||
{{ spu.introduction }}
|
||||
</div>
|
||||
<div>
|
||||
<!-- 价格 -->
|
||||
<span
|
||||
v-if="property.fields.price.show"
|
||||
class="text-[16px]"
|
||||
:style="{ color: property.fields.price.color }"
|
||||
>
|
||||
¥{{ fenToYuan(spu.price as any) }}
|
||||
</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) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-[12px]">
|
||||
<!-- 销量 -->
|
||||
<span
|
||||
v-if="property.fields.salesCount.show"
|
||||
:style="{ color: property.fields.salesCount.color }"
|
||||
>
|
||||
已售{{ (spu.salesCount || 0) + (spu.virtualSalesCount || 0) }}件
|
||||
</span>
|
||||
<!-- 库存 -->
|
||||
<span
|
||||
v-if="property.fields.stock.show"
|
||||
:style="{ color: property.fields.stock.color }"
|
||||
>
|
||||
库存{{ spu.stock || 0 }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 购买按钮 -->
|
||||
<div class="absolute bottom-[8px] right-[8px]">
|
||||
<!-- 文字按钮 -->
|
||||
<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}`,
|
||||
}"
|
||||
>
|
||||
{{ property.btnBuy.text }}
|
||||
</span>
|
||||
<!-- 图片按钮 -->
|
||||
<ElImage
|
||||
v-else
|
||||
class="h-[28px] w-[28px] rounded-full"
|
||||
fit="cover"
|
||||
:src="property.btnBuy.imgUrl"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
|
@ -0,0 +1,177 @@
|
|||
<script setup lang="ts">
|
||||
import type { ProductCardProperty } from './config';
|
||||
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
|
||||
import { useVModel } from '@vueuse/core';
|
||||
import {
|
||||
ElCard,
|
||||
ElCheckbox,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElRadioButton,
|
||||
ElRadioGroup,
|
||||
ElSlider,
|
||||
ElSwitch,
|
||||
ElTooltip,
|
||||
} from 'element-plus';
|
||||
|
||||
import ColorInput from '#/components/color-input/index.vue';
|
||||
import UploadImg from '#/components/upload/image-upload.vue';
|
||||
import SpuShowcase from '#/views/mall/product/spu/components/spu-showcase.vue';
|
||||
|
||||
// 商品卡片属性面板
|
||||
defineOptions({ name: 'ProductCardProperty' });
|
||||
|
||||
const props = defineProps<{ modelValue: ProductCardProperty }>();
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
const formData = useVModel(props, 'modelValue', emit);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ComponentContainerProperty v-model="formData.style">
|
||||
<ElForm label-width="80px" :model="formData">
|
||||
<ElCard header="商品列表" class="property-group" shadow="never">
|
||||
<SpuShowcase v-model="formData.spuIds" />
|
||||
</ElCard>
|
||||
<ElCard header="商品样式" class="property-group" shadow="never">
|
||||
<ElFormItem label="布局" prop="type">
|
||||
<ElRadioGroup v-model="formData.layoutType">
|
||||
<ElTooltip class="item" content="单列大图" placement="bottom">
|
||||
<ElRadioButton value="oneColBigImg">
|
||||
<IconifyIcon icon="fluent:text-column-one-24-filled" />
|
||||
</ElRadioButton>
|
||||
</ElTooltip>
|
||||
<ElTooltip class="item" content="单列小图" placement="bottom">
|
||||
<ElRadioButton value="oneColSmallImg">
|
||||
<IconifyIcon icon="fluent:text-column-two-left-24-filled" />
|
||||
</ElRadioButton>
|
||||
</ElTooltip>
|
||||
<ElTooltip class="item" content="双列" placement="bottom">
|
||||
<ElRadioButton value="twoCol">
|
||||
<IconifyIcon icon="fluent:text-column-two-24-filled" />
|
||||
</ElRadioButton>
|
||||
</ElTooltip>
|
||||
</ElRadioGroup>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="商品名称" prop="fields.name.show">
|
||||
<div class="flex gap-2">
|
||||
<ColorInput v-model="formData.fields.name.color" />
|
||||
<ElCheckbox v-model="formData.fields.name.show" />
|
||||
</div>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="商品简介" prop="fields.introduction.show">
|
||||
<div class="flex gap-2">
|
||||
<ColorInput v-model="formData.fields.introduction.color" />
|
||||
<ElCheckbox v-model="formData.fields.introduction.show" />
|
||||
</div>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="商品价格" prop="fields.price.show">
|
||||
<div class="flex gap-2">
|
||||
<ColorInput v-model="formData.fields.price.color" />
|
||||
<ElCheckbox v-model="formData.fields.price.show" />
|
||||
</div>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="市场价" prop="fields.marketPrice.show">
|
||||
<div class="flex gap-2">
|
||||
<ColorInput v-model="formData.fields.marketPrice.color" />
|
||||
<ElCheckbox v-model="formData.fields.marketPrice.show" />
|
||||
</div>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="商品销量" prop="fields.salesCount.show">
|
||||
<div class="flex gap-2">
|
||||
<ColorInput v-model="formData.fields.salesCount.color" />
|
||||
<ElCheckbox v-model="formData.fields.salesCount.show" />
|
||||
</div>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="商品库存" prop="fields.stock.show">
|
||||
<div class="flex gap-2">
|
||||
<ColorInput v-model="formData.fields.stock.color" />
|
||||
<ElCheckbox v-model="formData.fields.stock.show" />
|
||||
</div>
|
||||
</ElFormItem>
|
||||
</ElCard>
|
||||
<ElCard header="角标" class="property-group" shadow="never">
|
||||
<ElFormItem label="角标" prop="badge.show">
|
||||
<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"
|
||||
:show-description="false"
|
||||
>
|
||||
<template #tip> 建议尺寸:36 * 22 </template>
|
||||
</UploadImg>
|
||||
</ElFormItem>
|
||||
</ElCard>
|
||||
<ElCard header="按钮" class="property-group" shadow="never">
|
||||
<ElFormItem label="按钮类型" prop="btnBuy.type">
|
||||
<ElRadioGroup v-model="formData.btnBuy.type">
|
||||
<ElRadioButton value="text">文字</ElRadioButton>
|
||||
<ElRadioButton value="img">图片</ElRadioButton>
|
||||
</ElRadioGroup>
|
||||
</ElFormItem>
|
||||
<template v-if="formData.btnBuy.type === 'text'">
|
||||
<ElFormItem label="按钮文字" prop="btnBuy.text">
|
||||
<ElInput v-model="formData.btnBuy.text" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="左侧背景" prop="btnBuy.bgBeginColor">
|
||||
<ColorInput v-model="formData.btnBuy.bgBeginColor" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="右侧背景" prop="btnBuy.bgEndColor">
|
||||
<ColorInput v-model="formData.btnBuy.bgEndColor" />
|
||||
</ElFormItem>
|
||||
</template>
|
||||
<template v-else>
|
||||
<ElFormItem label="图片" prop="btnBuy.imgUrl">
|
||||
<UploadImg
|
||||
v-model="formData.btnBuy.imgUrl"
|
||||
height="56px"
|
||||
width="56px"
|
||||
:show-description="false"
|
||||
>
|
||||
<template #tip> 建议尺寸:56 * 56 </template>
|
||||
</UploadImg>
|
||||
</ElFormItem>
|
||||
</template>
|
||||
</ElCard>
|
||||
<ElCard header="商品样式" class="property-group" shadow="never">
|
||||
<ElFormItem label="上圆角" prop="borderRadiusTop">
|
||||
<ElSlider
|
||||
v-model="formData.borderRadiusTop"
|
||||
:max="100"
|
||||
:min="0"
|
||||
show-input
|
||||
input-size="small"
|
||||
:show-input-controls="false"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="下圆角" prop="borderRadiusBottom">
|
||||
<ElSlider
|
||||
v-model="formData.borderRadiusBottom"
|
||||
:max="100"
|
||||
:min="0"
|
||||
show-input
|
||||
input-size="small"
|
||||
:show-input-controls="false"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="间隔" prop="space">
|
||||
<ElSlider
|
||||
v-model="formData.space"
|
||||
:max="100"
|
||||
:min="0"
|
||||
show-input
|
||||
input-size="small"
|
||||
:show-input-controls="false"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElCard>
|
||||
</ElForm>
|
||||
</ComponentContainerProperty>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
import type {
|
||||
ComponentStyle,
|
||||
DiyComponent,
|
||||
} from '#/components/diy-editor/util';
|
||||
|
||||
/** 商品栏属性 */
|
||||
export interface ProductListProperty {
|
||||
// 布局类型:双列 | 三列 | 水平滑动
|
||||
layoutType: 'horizSwiper' | 'threeCol' | 'twoCol';
|
||||
// 商品字段
|
||||
fields: {
|
||||
// 商品名称
|
||||
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;
|
||||
}
|
||||
|
||||
// 定义组件
|
||||
export const component = {
|
||||
id: 'ProductList',
|
||||
name: '商品栏',
|
||||
icon: 'fluent:text-column-two-24-filled',
|
||||
property: {
|
||||
layoutType: 'twoCol',
|
||||
fields: {
|
||||
name: { show: true, color: '#000' },
|
||||
price: { show: true, color: '#ff3000' },
|
||||
},
|
||||
badge: { show: false, imgUrl: '' },
|
||||
borderRadiusTop: 8,
|
||||
borderRadiusBottom: 8,
|
||||
space: 8,
|
||||
spuIds: [],
|
||||
style: {
|
||||
bgType: 'color',
|
||||
bgColor: '',
|
||||
marginLeft: 8,
|
||||
marginRight: 8,
|
||||
marginBottom: 8,
|
||||
} as ComponentStyle,
|
||||
},
|
||||
} as DiyComponent<ProductListProperty>;
|
||||
|
|
@ -0,0 +1,150 @@
|
|||
<script setup lang="ts">
|
||||
import type { ProductListProperty } from './config';
|
||||
|
||||
import type { MallSpuApi } from '#/api/mall/product/spu';
|
||||
|
||||
import { onMounted, ref, watch } from 'vue';
|
||||
|
||||
import { fenToYuan } from '@vben/utils';
|
||||
|
||||
import { ElImage, ElScrollbar } from 'element-plus';
|
||||
|
||||
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 () => {
|
||||
spuList.value = await ProductSpuApi.getSpuDetailList(props.property.spuIds);
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
deep: true,
|
||||
},
|
||||
);
|
||||
// 手机宽度
|
||||
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],
|
||||
() => {
|
||||
// 计算列数
|
||||
columns.value = props.property.layoutType === 'twoCol' ? 2 : 3;
|
||||
// 每列的宽度为:(总宽度 - 间距 * (列数 - 1))/ 列数
|
||||
const productWidth =
|
||||
(phoneWidth.value - props.property.space * (columns.value - 1)) /
|
||||
columns.value;
|
||||
// 商品图布局:2列时,左右布局 3列时,上下布局
|
||||
imageSize.value = columns.value === 2 ? '64px' : `${productWidth}px`;
|
||||
// 根据布局类型,计算行数、列数
|
||||
if (props.property.layoutType === 'horizSwiper') {
|
||||
// 单行显示
|
||||
gridTemplateColumns.value = `repeat(auto-fill, ${productWidth}px)`;
|
||||
// 显示滚动条
|
||||
scrollbarWidth.value = `${
|
||||
productWidth * spuList.value.length +
|
||||
props.property.space * (spuList.value.length - 1)
|
||||
}px`;
|
||||
} else {
|
||||
// 指定列数
|
||||
gridTemplateColumns.value = `repeat(${columns.value}, auto)`;
|
||||
// 不滚动
|
||||
scrollbarWidth.value = '100%';
|
||||
}
|
||||
},
|
||||
{ immediate: true, deep: true },
|
||||
);
|
||||
onMounted(() => {
|
||||
// 提取手机宽度
|
||||
phoneWidth.value = containerRef.value?.wrapRef?.offsetWidth || 375;
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<ElScrollbar class="z-10 min-h-[30px]" wrap-class="w-full" ref="containerRef">
|
||||
<!-- 商品网格 -->
|
||||
<div
|
||||
class="grid overflow-x-auto"
|
||||
:style="{
|
||||
gridGap: `${property.space}px`,
|
||||
gridTemplateColumns,
|
||||
width: scrollbarWidth,
|
||||
}"
|
||||
>
|
||||
<!-- 商品 -->
|
||||
<div
|
||||
class="relative box-content flex flex-row flex-wrap overflow-hidden bg-white"
|
||||
:style="{
|
||||
borderTopLeftRadius: `${property.borderRadiusTop}px`,
|
||||
borderTopRightRadius: `${property.borderRadiusTop}px`,
|
||||
borderBottomLeftRadius: `${property.borderRadiusBottom}px`,
|
||||
borderBottomRightRadius: `${property.borderRadiusBottom}px`,
|
||||
}"
|
||||
v-for="(spu, index) in spuList"
|
||||
:key="index"
|
||||
>
|
||||
<!-- 角标 -->
|
||||
<div
|
||||
v-if="property.badge.show"
|
||||
class="absolute left-0 top-0 z-10 items-center justify-center"
|
||||
>
|
||||
<ElImage
|
||||
fit="cover"
|
||||
:src="property.badge.imgUrl"
|
||||
class="h-[26px] w-[38px]"
|
||||
/>
|
||||
</div>
|
||||
<!-- 商品封面图 -->
|
||||
<ElImage
|
||||
fit="cover"
|
||||
:src="spu.picUrl"
|
||||
:style="{ width: imageSize, height: imageSize }"
|
||||
/>
|
||||
<div
|
||||
class="box-border flex flex-col gap-2 p-2"
|
||||
:class="[
|
||||
{
|
||||
'w-[calc(100%-64px)]': columns === 2,
|
||||
'w-full': columns === 3,
|
||||
},
|
||||
]"
|
||||
>
|
||||
<!-- 商品名称 -->
|
||||
<div
|
||||
v-if="property.fields.name.show"
|
||||
class="truncate text-xs"
|
||||
:style="{ color: property.fields.name.color }"
|
||||
>
|
||||
{{ spu.name }}
|
||||
</div>
|
||||
<div>
|
||||
<!-- 商品价格 -->
|
||||
<span
|
||||
v-if="property.fields.price.show"
|
||||
class="text-xs"
|
||||
:style="{ color: property.fields.price.color }"
|
||||
>
|
||||
¥{{ fenToYuan(spu.price || 0) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ElScrollbar>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
<script setup lang="ts">
|
||||
import type { ProductListProperty } from './config';
|
||||
|
||||
import { useVModel } from '@vueuse/core';
|
||||
import {
|
||||
ElCard,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElRadioButton,
|
||||
ElRadioGroup,
|
||||
ElSlider,
|
||||
ElSwitch,
|
||||
ElTooltip,
|
||||
} from 'element-plus';
|
||||
|
||||
import ComponentContainerProperty from '#/components/diy-editor/components/component-container-property.vue';
|
||||
import ColorInput from '#/components/input-with-color/index.vue';
|
||||
import UploadImg from '#/components/upload/image-upload.vue';
|
||||
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>
|
||||
|
||||
<template>
|
||||
<ComponentContainerProperty v-model="formData.style">
|
||||
<ElForm label-width="80px" :model="formData">
|
||||
<ElCard header="商品列表" class="property-group" shadow="never">
|
||||
<SpuShowcase v-model="formData.spuIds" />
|
||||
</ElCard>
|
||||
<ElCard header="商品样式" class="property-group" shadow="never">
|
||||
<ElFormItem label="布局" prop="type">
|
||||
<ElRadioGroup v-model="formData.layoutType">
|
||||
<ElTooltip class="item" content="双列" placement="bottom">
|
||||
<ElRadioButton value="twoCol">
|
||||
<IconifyIcon icon="fluent:text-column-two-24-filled" />
|
||||
</ElRadioButton>
|
||||
</ElTooltip>
|
||||
<ElTooltip class="item" content="三列" placement="bottom">
|
||||
<ElRadioButton value="threeCol">
|
||||
<IconifyIcon icon="fluent:text-column-three-24-filled" />
|
||||
</ElRadioButton>
|
||||
</ElTooltip>
|
||||
<ElTooltip class="item" content="水平滑动" placement="bottom">
|
||||
<ElRadioButton value="horizSwiper">
|
||||
<IconifyIcon icon="system-uicons:carousel" />
|
||||
</ElRadioButton>
|
||||
</ElTooltip>
|
||||
</ElRadioGroup>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="商品名称" prop="fields.name.show">
|
||||
<div class="flex gap-2">
|
||||
<ColorInput v-model="formData.fields.name.color" />
|
||||
<ElCheckbox v-model="formData.fields.name.show" />
|
||||
</div>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="商品价格" prop="fields.price.show">
|
||||
<div class="flex gap-2">
|
||||
<ColorInput v-model="formData.fields.price.color" />
|
||||
<ElCheckbox v-model="formData.fields.price.show" />
|
||||
</div>
|
||||
</ElFormItem>
|
||||
</ElCard>
|
||||
<ElCard header="角标" class="property-group" shadow="never">
|
||||
<ElFormItem label="角标" prop="badge.show">
|
||||
<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"
|
||||
:show-description="false"
|
||||
>
|
||||
<template #tip> 建议尺寸:36 * 22 </template>
|
||||
</UploadImg>
|
||||
</ElFormItem>
|
||||
</ElCard>
|
||||
<ElCard header="商品样式" class="property-group" shadow="never">
|
||||
<ElFormItem label="上圆角" prop="borderRadiusTop">
|
||||
<ElSlider
|
||||
v-model="formData.borderRadiusTop"
|
||||
:max="100"
|
||||
:min="0"
|
||||
show-input
|
||||
input-size="small"
|
||||
:show-input-controls="false"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="下圆角" prop="borderRadiusBottom">
|
||||
<ElSlider
|
||||
v-model="formData.borderRadiusBottom"
|
||||
:max="100"
|
||||
:min="0"
|
||||
show-input
|
||||
input-size="small"
|
||||
:show-input-controls="false"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="间隔" prop="space">
|
||||
<ElSlider
|
||||
v-model="formData.space"
|
||||
:max="100"
|
||||
:min="0"
|
||||
show-input
|
||||
input-size="small"
|
||||
:show-input-controls="false"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElCard>
|
||||
</ElForm>
|
||||
</ComponentContainerProperty>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
import type {
|
||||
ComponentStyle,
|
||||
DiyComponent,
|
||||
} from '#/components/diy-editor/util';
|
||||
|
||||
/** 营销文章属性 */
|
||||
export interface PromotionArticleProperty {
|
||||
// 文章编号
|
||||
id: number;
|
||||
// 组件样式
|
||||
style: ComponentStyle;
|
||||
}
|
||||
|
||||
// 定义组件
|
||||
export const component = {
|
||||
id: 'PromotionArticle',
|
||||
name: '营销文章',
|
||||
icon: 'ph:article-medium',
|
||||
property: {
|
||||
style: {
|
||||
bgType: 'color',
|
||||
bgColor: '',
|
||||
marginLeft: 8,
|
||||
marginRight: 8,
|
||||
marginBottom: 8,
|
||||
} as ComponentStyle,
|
||||
},
|
||||
} as DiyComponent<PromotionArticleProperty>;
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
<script setup lang="ts">
|
||||
import type { PromotionArticleProperty } from './config';
|
||||
|
||||
import type { MallArticleApi } from '#/api/mall/promotion/article';
|
||||
|
||||
import { ref, watch } from 'vue';
|
||||
|
||||
import * as ArticleApi from '#/api/mall/promotion/article/index';
|
||||
|
||||
/** 营销文章 */
|
||||
defineOptions({ name: 'PromotionArticle' });
|
||||
// 定义属性
|
||||
const props = defineProps<{ property: PromotionArticleProperty }>();
|
||||
// 商品列表
|
||||
const article = ref<MallArticleApi.Article>();
|
||||
|
||||
watch(
|
||||
() => props.property.id,
|
||||
async () => {
|
||||
if (props.property.id) {
|
||||
article.value = await ArticleApi.getArticle(props.property.id);
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
},
|
||||
);
|
||||
</script>
|
||||
<template>
|
||||
<div class="min-h-[30px]" v-dompurify-html="article?.content"></div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
<script setup lang="ts">
|
||||
import type { PromotionArticleProperty } from './config';
|
||||
|
||||
import type { MallArticleApi } from '#/api/mall/promotion/article';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { useVModel } from '@vueuse/core';
|
||||
import { ElForm, ElFormItem, ElOption, ElSelect } from 'element-plus';
|
||||
|
||||
import * as ArticleApi from '#/api/mall/promotion/article/index';
|
||||
import ComponentContainerProperty from '#/components/diy-editor/components/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 queryArticleList = async (title?: string) => {
|
||||
loading.value = true;
|
||||
const { list } = await ArticleApi.getArticlePage({
|
||||
title,
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
});
|
||||
articles.value = list;
|
||||
loading.value = false;
|
||||
};
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
queryArticleList();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ComponentContainerProperty v-model="formData.style">
|
||||
<ElForm label-width="40px" :model="formData">
|
||||
<ElFormItem label="文章" prop="id">
|
||||
<ElSelect
|
||||
v-model="formData.id"
|
||||
placeholder="请选择文章"
|
||||
class="w-full"
|
||||
filterable
|
||||
remote
|
||||
:remote-method="queryArticleList"
|
||||
:loading="loading"
|
||||
>
|
||||
<ElOption
|
||||
v-for="article in articles"
|
||||
:key="article.id"
|
||||
:label="article.title"
|
||||
:value="article.id"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
</ComponentContainerProperty>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
import type {
|
||||
ComponentStyle,
|
||||
DiyComponent,
|
||||
} from '#/components/diy-editor/util';
|
||||
|
||||
/** 拼团属性 */
|
||||
export interface PromotionCombinationProperty {
|
||||
// 布局类型:单列 | 三列
|
||||
layoutType: 'oneColBigImg' | 'oneColSmallImg' | 'twoCol';
|
||||
// 商品字段
|
||||
fields: {
|
||||
// 商品简介
|
||||
introduction: PromotionCombinationFieldProperty;
|
||||
// 市场价
|
||||
marketPrice: PromotionCombinationFieldProperty;
|
||||
// 商品名称
|
||||
name: PromotionCombinationFieldProperty;
|
||||
// 商品价格
|
||||
price: PromotionCombinationFieldProperty;
|
||||
// 商品销量
|
||||
salesCount: PromotionCombinationFieldProperty;
|
||||
// 商品库存
|
||||
stock: PromotionCombinationFieldProperty;
|
||||
};
|
||||
// 角标
|
||||
badge: {
|
||||
// 角标图片
|
||||
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;
|
||||
}
|
||||
|
||||
// 商品字段
|
||||
export interface PromotionCombinationFieldProperty {
|
||||
// 是否显示
|
||||
show: boolean;
|
||||
// 颜色
|
||||
color: string;
|
||||
}
|
||||
|
||||
// 定义组件
|
||||
export const component = {
|
||||
id: 'PromotionCombination',
|
||||
name: '拼团',
|
||||
icon: 'mdi:account-group',
|
||||
property: {
|
||||
layoutType: 'oneColBigImg',
|
||||
fields: {
|
||||
name: { show: true, color: '#000' },
|
||||
introduction: { show: true, color: '#999' },
|
||||
price: { show: true, color: '#ff3000' },
|
||||
marketPrice: { show: true, color: '#c4c4c4' },
|
||||
salesCount: { show: true, color: '#c4c4c4' },
|
||||
stock: { show: false, color: '#c4c4c4' },
|
||||
},
|
||||
badge: { show: false, imgUrl: '' },
|
||||
btnBuy: {
|
||||
type: 'text',
|
||||
text: '去拼团',
|
||||
bgBeginColor: '#FF6000',
|
||||
bgEndColor: '#FE832A',
|
||||
imgUrl: '',
|
||||
},
|
||||
borderRadiusTop: 8,
|
||||
borderRadiusBottom: 8,
|
||||
space: 8,
|
||||
style: {
|
||||
bgType: 'color',
|
||||
bgColor: '',
|
||||
marginLeft: 8,
|
||||
marginRight: 8,
|
||||
marginBottom: 8,
|
||||
} as ComponentStyle,
|
||||
},
|
||||
} as DiyComponent<PromotionCombinationProperty>;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue