基础插件修改

This commit is contained in:
lixin 2025-01-09 16:40:36 +08:00
parent 4dbcc7b837
commit 3dcd0ddf6c
66 changed files with 15670 additions and 112 deletions

864
build/cool/eps.d.ts vendored

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

5720
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,150 @@
<template>
<div class="assembly-banner">
<el-row class="item" :gutter="6" v-for="(item, index) in value" :key="index">
<el-col>
<div>
<div class="icon-row">
<cl-upload
:size="[80, 80]"
v-model="item.pic"
@success="onChange"
></cl-upload>
<div>
<el-button
:icon="Top"
text
circle
v-if="index"
type="primary"
@click="top(index)"
></el-button>
<el-button
:icon="Bottom"
text
circle
v-if="index != value.length - 1"
type="primary"
@click="bottom(index)"
></el-button>
<el-button
:icon="Delete"
text
circle
type="danger"
@click="del(index)"
></el-button>
</div>
</div>
<div class="tips">图片宽度 750px 高度不限制</div>
</div>
</el-col>
<el-col>
<assembly-link v-model="item.link"></assembly-link>
</el-col>
</el-row>
<div class="add" @click="add">
<el-button link type="primary">添加</el-button>
</div>
</div>
</template>
<script lang="ts" name="assembly-banner" setup>
import { ref, type PropType } from "vue";
import { Delete, Top, Bottom } from "@element-plus/icons-vue";
import { ElMessage } from "element-plus";
import { Form } from "../../types/form";
const props = defineProps({
modelValue: {
type: Array as PropType<{ pic: string; link: Form.Link }[]>,
default: () => [
{
pic: "",
link: {
name: "",
type: "",
appid: "",
page: ""
}
}
]
}
});
const emit = defineEmits(["update:modelValue"]);
const value = ref<any[]>(props.modelValue);
function add() {
value.value.push({
pic: "",
link: {
name: "",
type: "",
appid: "",
page: ""
}
});
onChange();
}
function top(index: number) {
if (index) {
swap(JSON.parse(JSON.stringify(value.value)), index, index - 1);
}
}
function bottom(index: number) {
if (value.value.length >= index) {
swap(JSON.parse(JSON.stringify(value.value)), index, index + 1);
}
}
//
function swap(arr: any[], index1: number, index2: number) {
[arr[index1], arr[index2]] = [arr[index2], arr[index1]];
value.value = arr;
onChange();
}
function del(index: number) {
if (value.value.length > 1) {
value.value.splice(index, 1);
onChange();
} else {
ElMessage.error("必须保留一张图片");
}
}
function onChange() {
emit("update:modelValue", value.value);
}
</script>
<style lang="scss" scoped>
.assembly-banner {
.item {
border: 1px solid var(--el-border-color);
border-radius: 6px;
padding: 10px;
box-sizing: border-box;
margin-bottom: 10px;
.icon-row {
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.tips {
font-size: 12px;
color: #bfbfbf;
}
}
.add {
margin-top: 20px;
display: flex;
align-items: center;
justify-content: center;
height: 30px;
border: 1px solid var(--el-border-color);
border-radius: 6px;
cursor: pointer;
}
}
</style>

View File

@ -0,0 +1,190 @@
<template>
<div class="assembly-coupon">
<div class="box">
<el-input placeholder="请指定优惠券" v-model="value.title" disabled class="le-url">
<template #append>
<el-button type="primary" @click="onTap"> 选择 </el-button>
</template>
</el-input>
</div>
<cl-dialog title="选择优惠券" height="500px" v-model="visible">
<cl-crud ref="Crud">
<cl-row>
<!-- 刷新按钮 -->
<cl-refresh-btn />
<cl-filter label="状态">
<cl-select :options="options.status" prop="status" :width="140" />
</cl-filter>
<cl-flex1 />
<!-- 关键字搜索 -->
<cl-search-key placeholder="搜索标题" />
</cl-row>
<cl-row>
<!-- 数据表格 -->
<cl-table ref="Table" :autoHeight="false" />
</cl-row>
<cl-row>
<cl-flex1 />
<!-- 分页控件 -->
<cl-pagination />
</cl-row>
</cl-crud>
</cl-dialog>
</div>
</template>
<script lang="ts" name="assembly-coupon" setup>
import { ref, reactive, type PropType } from "vue";
import { useCrud, useTable } from "@cool-vue/crud";
import dayjs from "dayjs";
import { useCool } from "/@/cool";
const { service } = useCool();
const options = reactive({
type: [
{
label: "满减券",
value: 0
}
],
status: [
{
label: "启用",
value: 1
},
{
label: "禁用",
value: 0
}
]
});
const props = defineProps({
modelValue: {
type: Object as PropType<Eps.MarketCouponInfoEntity>,
default: () => {
return {
id: 0,
title: "",
description: "",
amount: 0
};
}
}
});
// cl-table
const Table = useTable<Eps.MarketCouponInfoEntity>({
columns: [
{ label: "标题", prop: "title", minWidth: 200 },
{ label: "描述", prop: "description", showOverflowTooltip: true, minWidth: 200 },
{ label: "类型", prop: "type", minWidth: 120, dict: options.type },
{
label: "条件",
prop: "condition",
minWidth: 140,
formatter(row) {
switch (row.type) {
case 0:
return `${row.condition?.fullAmount}${row.amount}`;
default:
return "无门槛";
}
}
},
{ label: "金额", prop: "amount", minWidth: 120, sortable: "custom" },
{ label: "数量", prop: "num", minWidth: 120, sortable: "custom" },
{ label: "已领取", prop: "receivedNum", minWidth: 120, sortable: "custom" },
{ label: "状态", prop: "status", minWidth: 100, component: { name: "cl-switch" } },
{
label: "有效期",
prop: "startTime",
minWidth: 260,
formatter(row) {
return (
dayjs(row.startTime).format("YYYY-MM-DD") +
" ~ " +
dayjs(row.endTime).format("YYYY-MM-DD")
);
}
},
{
label: "创建时间",
prop: "createTime",
minWidth: 160,
component: { name: "cl-date-text" }
},
{
type: "op",
width: 160,
buttons: [
{
label: "选择TA",
type: "success",
onClick({ scope }) {
value.value = scope.row;
onChange();
visible.value = false;
}
}
]
}
]
});
// cl-crud
const Crud = useCrud(
{
service: service.market.coupon.info
},
(app) => {
app.refresh();
}
);
const emit = defineEmits(["update:modelValue"]);
const value = ref(props.modelValue);
const visible = ref(false);
function onTap() {
visible.value = true;
}
function onChange() {
emit("update:modelValue", value.value);
}
</script>
<style lang="scss" scoped>
.assembly-coupon {
box-sizing: border-box;
background-color: #f6f7fa;
border-radius: 10px;
padding: 6px;
margin-bottom: 10px;
.box {
box-sizing: border-box;
display: flex;
background-color: #ffffff;
padding: 10px;
}
.le-url {
width: 100%;
:deep(.el-input-group__append) {
width: 118px;
background: #623ceb;
border: 1px solid #623ceb;
text-align: center;
font-family: PingFang SC;
font-weight: 500;
color: #ffffff;
box-sizing: border-box;
}
}
}
</style>

View File

@ -0,0 +1,131 @@
<template>
<cl-crud ref="Crud">
<cl-row>
<!-- 刷新按钮 -->
<cl-refresh-btn />
<!-- 删除按钮 -->
<cl-multi-delete-btn />
<cl-filter label="状态">
<cl-select :options="options.status" prop="status" :width="140" />
</cl-filter>
<cl-flex1 />
<!-- 关键字搜索 -->
<cl-search-key placeholder="搜索标题" />
</cl-row>
<cl-row>
<!-- 数据表格 -->
<cl-table ref="Table" :autoHeight="false" />
</cl-row>
<cl-row>
<cl-flex1 />
<!-- 分页控件 -->
<cl-pagination />
</cl-row>
</cl-crud>
</template>
<script lang="ts" name="assembly-fixtures" setup>
import { useCrud, useTable } from "@cool-vue/crud";
import { useCool } from "/@/cool";
import { reactive } from "vue";
const { service } = useCool();
const props = defineProps({
multiple: Boolean
});
const options = reactive({
status: [
{
label: "已上架",
value: 1
},
{
label: "已下架",
value: 0
}
]
});
const emit = defineEmits(["change"]);
// cl-table
const Table = useTable({
columns: [
{ type: "selection" },
{ label: "页面名称", prop: "name", minWidth: 140 },
{
label: "是否首页",
prop: "isHome",
minWidth: 140,
dict: [
{
label: "是",
value: 1,
type: "primary"
},
{
label: "否",
value: 0,
type: "info"
}
]
},
{ label: "状态", prop: "status", minWidth: 100, component: { name: "cl-switch" } },
{
label: "更新时间",
prop: "updateTime",
minWidth: 160,
component: { name: "cl-date-text" }
},
{
type: "op",
width: 160,
buttons: [
{
label: "选择TA",
type: "success",
onClick({ scope }) {
select(scope.row);
}
}
]
}
]
});
// cl-crud
const Crud = useCrud(
{
service: service.fixtures.mould,
dict: {
label: {
multiDelete: "多选/确定"
}
},
onDelete(selection, { next }) {
choice(selection);
}
},
(app) => {
app.refresh();
}
);
//
function choice(items: any[]) {
if (props.multiple) {
emit("change", items);
}
}
//
function select(item: any) {
emit("change", item);
}
</script>

View File

@ -0,0 +1,286 @@
<template>
<div class="assembly-goods-list">
<div class="w-120">显示模式</div>
<el-radio-group v-model="value.mode" @change="changeGroup">
<el-radio value="mode-1">双列</el-radio>
<el-radio value="mode-2">三列</el-radio>
<el-radio value="mode-3">列表</el-radio>
<el-radio value="mode-4">单列</el-radio>
</el-radio-group>
<div class="w-120">商品卡片阴影</div>
<assembly-switch v-model="value.isShadow" />
<div class="w-120">商品来源</div>
<el-radio-group v-model="value.source" @change="changeSource">
<el-radio value="source-1">手动选择</el-radio>
<el-radio value="source-2">选择分类</el-radio>
<el-radio value="source-3">营销属性</el-radio>
</el-radio-group>
<div class="attribute-box" v-if="isSource3">
<el-radio-group v-model="value.attribute" @change="changeGroup">
<el-radio :value="0">正常</el-radio>
<el-radio :value="1">新品</el-radio>
<el-radio :value="2">热卖</el-radio>
<el-radio :value="3">推荐</el-radio>
<el-radio :value="4">特价</el-radio>
</el-radio-group>
</div>
<div class="attribute-num" v-if="!isSource1">
<div class="w-120 mt-10">显示商品数量</div>
<assembly-slider v-model="value.num" :min="1" :max="99" unit="件" :step="1" />
</div>
<div class="hand-select" v-if="isSource1">
<div class="flex-row goods-item" v-for="(item, index) in value.list" :key="index">
<el-image style="width: 60px; height: 60px" :src="item.mainPic" fit="cover" />
<div class="goods-title">
<div class="title">{{ item.title }}</div>
<div class="price">
<span class="p1">{{ item.price }}</span>
<span class="p2">已售 {{ item.sold }}</span>
<el-button
@click="delGoods(index)"
link
type="danger"
:icon="Delete"
circle
/>
</div>
</div>
</div>
<div class="add" @click="addGoods">
<el-button link type="primary">添加商品</el-button>
</div>
</div>
<div class="hand-select" v-if="isSource2">
<div class="flex-row type-item" v-for="(item, index) in value.type" :key="index">
<el-image style="width: 40px; height: 40px" :src="item.pic" fit="cover" />
<div class="type-name">
<span class="title">{{ item.name }}</span>
<el-button @click="delType(index)" link type="danger" :icon="Delete" circle />
</div>
</div>
<div class="add" @click="addGoodsType">
<el-button link type="primary">添加分类</el-button>
</div>
</div>
<cl-dialog title="选择数据" height="500px" v-model="visible">
<template v-if="isSource1">
<assembly-goods :multiple="true" @change="onTapGoodsDetails" />
</template>
<template v-if="isSource2">
<div class="goods-type">
<div
class="item"
v-for="(item, index) in goodsType"
:key="index"
@click="onTapGoodsType(item)"
>
<el-image
style="width: 100%; max-width: 100%; border-radius: 6px"
:src="item.pic"
fit="cover"
/>
<div class="name">{{ item.name }}</div>
</div>
</div>
</template>
</cl-dialog>
</div>
</template>
<script lang="ts" name="assembly-goods-list" setup>
import { ref, computed, type PropType } from "vue";
import { Delete } from "@element-plus/icons-vue";
import { Form } from "../../types/form";
import { useCool } from "/@/cool";
const { service } = useCool();
const props = defineProps({
modelValue: {
type: Object as PropType<Form.Goods>,
default: () => {
return {
mode: "mode-1",
source: "source-1",
attribute: 0,
num: 99,
gap: 0,
isShadow: false,
type: [],
list: []
};
}
}
});
const emit = defineEmits(["update:modelValue"]);
const value = ref(props.modelValue);
// Computed properties for conditional rendering
const isSource1 = computed(() => value.value.source === "source-1");
const isSource2 = computed(() => value.value.source === "source-2");
const isSource3 = computed(() => value.value.source === "source-3");
const visible = ref(false);
const goodsType = ref<any[]>([]);
function addGoods() {
visible.value = true;
}
//
function onTapGoodsDetails(item: any) {
const itemsToAdd = Array.isArray(item) ? item : [item];
//
const uniqueItems = itemsToAdd.filter(
(newItem) => !value.value.list.some((existingItem) => existingItem.id === newItem.id)
);
//
if (uniqueItems.length > 0) {
value.value.list.push(...uniqueItems);
onChange();
}
//
visible.value = false;
}
function onTapGoodsType(item: { name: string; pic: string; id: number; parentId: number }) {
// item type
const itemExists = value.value.type.some((existingItem) => existingItem.id === item.id);
// item type
if (!itemExists) {
value.value.type.push(item);
onChange();
}
//
visible.value = false;
}
function changeSource(mode: any) {
onChange();
}
function changeGroup(mode: any) {
onChange();
}
function delGoods(index: number) {
value.value.list.splice(index, 1);
onChange();
}
function delType(index: number) {
value.value.type.splice(index, 1);
onChange();
}
function addGoodsType() {
visible.value = true;
service.goods.type.list({ status: 1 }).then((res: any) => {
goodsType.value = res;
});
}
function onChange() {
emit("update:modelValue", value.value);
}
</script>
<style lang="scss" scoped>
.assembly-goods-list {
.w-120 {
width: 140px;
}
.mt-10 {
margin-top: 10px;
}
.attribute-box {
margin-top: 20px;
box-sizing: border-box;
background-color: #f6f7fa;
border-radius: 10px;
padding: 6px 25px;
}
.flex-row {
display: flex;
align-items: center;
}
.hand-select {
margin-top: 20px;
box-sizing: border-box;
background-color: #f6f7fa;
border-radius: 10px;
padding: 6px;
margin-bottom: 10px;
.add {
margin-top: 20px;
display: flex;
align-items: center;
justify-content: center;
height: 30px;
border: 1px solid var(--el-border-color);
background-color: #fff;
border-radius: 6px;
cursor: pointer;
}
.type-item {
background-color: #fff;
border-radius: 10px;
padding: 6px;
box-sizing: border-box;
margin-bottom: 6px;
.type-name {
flex: 1;
display: flex;
justify-content: space-between;
margin-left: 20px;
}
}
.goods-item {
background-color: #fff;
border-radius: 10px;
padding: 6px;
box-sizing: border-box;
position: relative;
margin-bottom: 6px;
.goods-title {
margin-left: 20px;
.title {
width: 200px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.price {
width: 200px;
display: flex;
justify-content: space-between;
.p1 {
color: red;
}
.p2 {
color: #a9a9a9;
}
}
}
}
}
}
.goods-type {
display: grid;
grid-template-columns: repeat(8, 1fr);
gap: 10px;
.item {
background-color: #f6f7fa;
border-radius: 10px;
box-sizing: border-box;
cursor: pointer;
padding: 6px;
.name {
text-align: center;
padding: 6px;
}
}
}
</style>

View File

@ -0,0 +1,147 @@
<template>
<cl-crud ref="Crud">
<cl-row>
<!-- 刷新按钮 -->
<cl-refresh-btn />
<!-- 删除按钮 -->
<cl-multi-delete-btn />
<cl-filter label="分类">
<cl-select tree :options="options.type" prop="typeId" all-levels-id :width="140" />
</cl-filter>
<cl-flex1 />
<!-- 关键字搜索 -->
<cl-search-key placeholder="搜索标题" />
</cl-row>
<cl-row>
<!-- 数据表格 -->
<cl-table ref="Table" :autoHeight="false" />
</cl-row>
<cl-row>
<cl-flex1 />
<!-- 分页控件 -->
<cl-pagination :page-sizes="[5, 7, 10, 20]" />
</cl-row>
</cl-crud>
</template>
<script lang="ts" name="assembly-goods" setup>
import { useCrud, useTable } from "@cool-vue/crud";
import { useCool } from "/@/cool";
import { computed, reactive } from "vue";
import { deepTree } from "/@/cool/utils";
import type { Dict } from "/$/dict/types";
const { service } = useCool();
const props = defineProps({
multiple: Boolean
});
const emit = defineEmits(["change"]);
const options = reactive({
type: [] as Dict.Item[],
status: [
{
label: "已上架",
value: 1
},
{
label: "已下架",
value: 0
}
]
});
// cl-table
const Table = useTable({
columns: [
{ type: "selection" },
{ label: "分类", prop: "typeId", minWidth: 160, dict: computed(() => options.type) },
{ label: "标题", prop: "title", minWidth: 240 },
{
label: "主图",
prop: "mainPic",
minWidth: 100,
component: { name: "cl-image", props: { size: 50 } }
},
{
label: "示例图",
prop: "pics",
minWidth: 100,
component: { name: "cl-image", props: { size: 50 } }
},
{ label: "价格", prop: "price", minWidth: 100, sortable: "custom" },
{ label: "已售", prop: "sold", minWidth: 100, sortable: "custom" },
{ label: "状态", prop: "status", minWidth: 100, component: { name: "cl-switch" } },
{ label: "排序", prop: "sortNum", minWidth: 100, sortable: "desc" },
{ label: "创建时间", prop: "createTime", minWidth: 160, sortable: "custom" },
{
type: "op",
width: 160,
buttons: [
{
label: "选择TA",
type: "success",
onClick({ scope }) {
select(scope.row);
}
}
]
}
]
});
// cl-crud
const Crud = useCrud(
{
service: service.goods.info,
async onRefresh(params, { next, done, render }) {
const { list, pagination } = await next(params);
list.forEach((e) => {
e.content = "";
});
render(list, pagination);
done();
},
dict: {
label: {
multiDelete: "多选/确定"
}
},
onDelete(selection, { next }) {
choice(selection);
}
},
(app) => {
app.refresh({ size: 5, page: 1 ,status: 1});
getTypes();
}
);
//
function choice(items: any[]) {
if (props.multiple) {
emit("change", items);
}
}
//
function select(item: any) {
emit("change", item);
}
//
function getTypes() {
service.goods.type.list().then((res) => {
res.forEach((e) => {
e.label = e.name;
e.value = e.id;
});
options.type = deepTree(res);
});
}
</script>

View File

@ -0,0 +1,131 @@
<template>
<div class="demo-group">
<div class="head" v-show="label">{{ label }}</div>
<draggable
v-model="list"
class="list"
tag="div"
item-key="id"
:group="{
name: 'A',
animation: 300,
ghostClass: 'Ghost',
dragClass: 'Drag',
draggable: '.is-drag',
put: isPut
}"
:clone="onClone"
>
<template #footer>
<div class="tips">可拖入多个组件<span>不包含组合组件</span></div>
</template>
<template #item="{ element: item, index }">
<div
class="item"
:class="{
active: dp.form.active == item.id
}"
@click.stop="dp.toDet(item)"
>
<el-icon
class="close"
@click.stop="remove(index)"
v-show="dp.form.active == item.id && item.isDel !== false"
>
<close-bold />
</el-icon>
<component
:is="item.component.name"
:data="item"
v-bind="item.component.props"
/>
</div>
</template>
</draggable>
</div>
</template>
<script lang="ts" setup name="assembly-group">
import { ref, watch } from "vue";
import Draggable from "vuedraggable/src/vuedraggable";
import { CloseBold } from "@element-plus/icons-vue";
import { useCool } from "/@/cool";
import { useDp } from "../../hooks";
const props = defineProps({
label: String,
children: Array
});
const emit = defineEmits(["update:children"]);
const { mitt } = useCool();
const { dp } = useDp();
const list = ref<any[]>(props.children || []);
const isPut = ref(true);
function remove(index: number) {
dp.clearConfig(list.value[index].id);
list.value?.splice(index, 1);
}
function onClone(data: any) {
mitt.emit("dp.pull", data);
return data;
}
mitt.on("dp.setActive", (id: string) => {
const d = list.value?.find((e) => e.id == id);
if (d) {
dp.toDet(d);
}
});
mitt.on("dp.pull", (d) => {
isPut.value = d?.component.name != "demo-group";
});
watch(
list,
(val) => {
emit("update:children", val);
},
{
deep: true
}
);
</script>
<style lang="scss" scoped>
.demo-group {
background-color: #fff;
.head {
line-height: 40px;
height: 40px;
padding: 0 12px;
font-size: 14px;
font-weight: bold;
}
.list {
background-color: #d9effe;
.tips {
color: #8c8c8c;
font-size: 12px;
padding: 15px;
}
.item {
&:nth-last-child(2) {
margin-bottom: 0;
}
}
}
}
</style>

View File

@ -0,0 +1,411 @@
<template>
<div class="assembly-hot-image">
<div class="upload">
<div class="box">
<cl-upload
@success="onChange"
text="上传图片"
v-model="value.pic"
:size="[80, 80]"
></cl-upload>
<div class="link">
<assembly-link v-model="value.link" :prepend="false" @change="onChange" />
<div class="tips">建议图片宽度750px</div>
</div>
</div>
</div>
<div class="hot" v-if="value.pic">
<div ref="imageRect" class="attr-box">
<img :src="value.pic" class="icon" />
<div
v-for="(item, index) in value.attr"
:key="index"
class="hot-spot"
:style="{
width: `${item.relativeW}px`,
height: `${item.relativeH}px`,
top: `${item.relativeY}px`,
left: `${item.relativeX}px`,
zIndex: item.index
}"
>
<div class="link-name">{{ item.link.name }}</div>
</div>
</div>
</div>
<div class="add" @click="open" v-if="value.pic">
<el-button link type="primary">打开热区设置</el-button>
</div>
<cl-dialog title="图片热区设置" width="500px" v-model="showHotMap">
<el-scrollbar height="500px" @scroll="handleScroll">
<div class="container" ref="container">
<img ref="imageRef" :src="value.pic" draggable="false" class="icon" />
<div
v-for="(item, index) in attr"
:key="index"
class="draggable-resizable"
:style="{
width: `${item.w}px`,
height: `${item.h}px`,
zIndex: item.index,
left: `${item.x}px`,
top: `${item.y}px`
}"
>
<el-icon class="close" @click.stop="remove(index)">
<close-bold />
</el-icon>
<assembly-link
:ref="(el: any) => (linkRefs[index] = el)"
v-model="attr[index].link"
>
<div class="link-name" @click="addLink(index)">
{{ item.link.name }}
</div>
</assembly-link>
<div class="resize-handle top-left"></div>
<div class="resize-handle top-right"></div>
<div class="resize-handle bottom-left"></div>
<div class="resize-handle bottom-right"></div>
<div class="resize-handle top"></div>
<div class="resize-handle right"></div>
<div class="resize-handle bottom"></div>
<div class="resize-handle left"></div>
</div>
</div>
</el-scrollbar>
<template #footer>
<div>
<el-button type="primary" @click="add">添加热区</el-button>
<el-button type="success" @click="saveHotMap">保存热区</el-button>
</div>
</template>
</cl-dialog>
</div>
</template>
<script lang="ts" name="assembly-hot-image" setup>
import { ref, nextTick, type PropType } from "vue";
import { cloneDeep } from "lodash-es";
import { CloseBold } from "@element-plus/icons-vue";
import { Form } from "../../types/form";
import { DraggableResizableClass } from "../../static/js/DraggableResizableClass";
const props = defineProps({
modelValue: {
type: Object as PropType<Form.HotImage>,
default: () => {
return {
pic: "",
width: 0,
height: 0,
link: {},
attr: []
};
}
}
});
const emit = defineEmits(["update:modelValue"]);
const imageRef = ref<HTMLImageElement | null>(null);
const imageRect = ref<HTMLImageElement | null>(null);
const linkRefs = ref<any[]>([]);
const value = ref(props.modelValue);
const showHotMap = ref(false);
const attr = ref<Form.Hot[]>([]);
const container = ref<HTMLElement | null>(null);
let draggableResizableInstance: DraggableResizableClass | null = null;
function open() {
attr.value = cloneDeep(value.value.attr);
if (!attr.value.length) {
add();
}
showHotMap.value = true;
nextTick().then(() => {
if (container.value) {
draggableResizableInstance = new DraggableResizableClass(container.value, attr.value);
}
if (imageRef.value) {
const canvas = imageRef.value.getBoundingClientRect();
value.value.width = canvas.width;
value.value.height = canvas.height;
}
});
}
const scrollTop = ref(0);
const handleScroll = (event) => {
scrollTop.value = event.scrollTop;
};
function add() {
const len = attr.value.length;
let index = 1;
if (len) {
index = attr.value[len - 1].index + 1;
}
attr.value.push({
x: 20,
y: scrollTop.value + 20,
w: 100,
h: 60,
relativeX: 0,
relativeY: 0,
relativeW: 0,
relativeH: 0,
index: index,
link: {
page: "",
appid: "",
type: "",
name: "添加链接"
}
});
nextTick(() => {
if (draggableResizableInstance) {
draggableResizableInstance.updateListeners();
}
});
}
function addLink(index: number) {
linkRefs.value[index]?.open();
}
function remove(index: number) {
attr.value.splice(index, 1);
linkRefs.value.splice(index, 1);
if (draggableResizableInstance) {
draggableResizableInstance.updateListeners();
}
}
async function saveHotMap() {
adjustElements();
value.value.attr = attr.value;
onChange();
showHotMap.value = false;
}
//
function adjustElements() {
//
const { width, height } = imageRect.value!.getBoundingClientRect();
const scaleX = width / value.value.width;
const scaleY = height / value.value.height;
//
attr.value.forEach((element) => {
element.relativeX = Math.floor(element.x * scaleX);
element.relativeY = Math.floor(element.y * scaleY);
element.relativeW = Math.floor(element.w * scaleX);
element.relativeH = Math.floor(element.h * scaleY);
});
}
function onChange() {
emit("update:modelValue", value.value);
}
</script>
<style lang="scss" scoped>
.assembly-hot-image {
.upload {
box-sizing: border-box;
background-color: #f6f7fa;
border-radius: 10px;
padding: 6px;
margin-bottom: 10px;
.box {
box-sizing: border-box;
display: flex;
background-color: #ffffff;
padding: 10px;
border-radius: 6px;
.link {
box-sizing: border-box;
margin-left: 10px;
}
.tips {
font-size: 14px;
margin-top: 10px;
color: var(--el-color-warning);
}
}
}
.hot {
margin-top: 20px;
width: 100%;
padding: 10px;
box-sizing: border-box;
background-color: #f6f7fa;
border-radius: 10px;
min-height: 100px;
.attr-box {
position: relative;
display: inline-block;
width: 100%;
height: 100%;
.icon {
width: 100%;
height: auto;
max-width: 100%;
vertical-align: middle;
}
.hot-spot {
position: absolute;
background-color: rgb(73 104 217 / 35%);
border: 1px dashed white;
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
user-select: auto;
-webkit-user-select: auto;
-moz-user-select: auto;
user-select: none; /* 禁用选中 */
outline: none; /* 移除焦点样式 */
.link-name {
font-size: 12px;
color: #fff;
line-height: 16px;
}
}
}
}
.add {
margin-top: 20px;
display: flex;
align-items: center;
justify-content: center;
height: 30px;
border: 1px solid var(--el-border-color);
border-radius: 6px;
cursor: pointer;
}
}
.container {
position: relative;
width: 460px;
height: 100%;
.icon {
width: 100%;
height: auto;
max-width: 100%;
vertical-align: middle;
user-select: none; /* 禁用选中 */
}
.draggable-resizable {
position: absolute;
overflow: visible;
background-color: rgb(73 104 217 / 35%);
border: 1px dashed white;
display: flex;
justify-content: center;
align-items: center;
cursor: move;
user-select: auto;
-webkit-user-select: auto;
-moz-user-select: auto;
user-select: none; /* 禁用选中 */
outline: none; /* 移除焦点样式 */
.resize-handle {
position: absolute;
width: 10px;
height: 10px;
border-radius: 5px;
background: #fff;
bottom: 0;
right: 0;
z-index: 10;
cursor: se-resize; /* 确保缩放时光标正确 */
/* 添加特定位置的样式 */
&.top-left {
top: -5px;
left: -5px;
cursor: nw-resize;
}
&.top-right {
top: 0;
right: 0;
cursor: ne-resize;
}
&.bottom-left {
bottom: -5px;
left: -5px;
cursor: sw-resize;
}
&.bottom-right {
bottom: -5px;
right: -5px;
cursor: se-resize;
}
&.top {
top: -5px;
left: 50%;
transform: translateX(-50%);
cursor: n-resize;
width: 10px;
height: 10px;
}
&.right {
right: -5px;
top: 50%;
transform: translateY(-50%);
cursor: e-resize;
}
&.bottom {
bottom: -5px;
left: 50%;
transform: translateX(-50%);
cursor: s-resize;
width: 10px;
height: 10px;
}
&.left {
left: -5px;
top: 50%;
transform: translateY(-50%);
cursor: w-resize;
}
}
.link-name {
font-size: 14px;
color: #ffffff;
cursor: pointer;
max-width: 100px;
overflow: hidden;
user-select: none; /* 禁用选中 */
}
.link-name:hover {
color: var(--el-color-danger);
cursor: pointer;
}
.close {
position: absolute;
right: 0;
top: 0;
height: 14px;
width: 14px;
color: #fff;
z-index: 11;
background-color: var(--el-color-black);
padding: 1px;
cursor: pointer;
user-select: none; /* 禁用选中 */
&:hover {
background-color: red;
}
}
}
}
</style>

View File

@ -0,0 +1,424 @@
<template>
<div>
<div class="assembly-link">
<slot>
<el-input
@change="onChange"
v-model="value.name"
readonly
:disabled="true"
placeholder="跳转页面"
class="le-url"
>
<template #prepend v-if="prepend">{{ typeName }}</template>
<template #append>
<el-button type="primary" @click="onTap"> 选择 </el-button>
</template>
</el-input>
</slot>
</div>
<cl-dialog title="选择页面链接" height="550px" v-model="visible">
<el-tabs stretch v-model="activeName" @tab-click="handleClick">
<el-tab-pane label="平台页面" name="index">
<div class="index-box" v-for="(item, index) in data.index" :key="index">
<div class="index-title">
{{ item.name }}
</div>
<div class="index-menu">
<el-button
v-for="(but, b) in item.children"
:key="b"
@click="onTapIndex(but)"
plain
>
{{ but.name }}
</el-button>
</div>
</div>
</el-tab-pane>
<el-tab-pane label="商品分类" name="goodsType">
<div class="goods-type">
<div
class="item"
v-for="(item, index) in data.goodsType"
:key="index"
@click="onTapGoodsType(item)"
>
<el-image
style="width: 100%; max-width: 100%; border-radius: 6px"
:src="item.pic"
fit="cover"
/>
<div class="name">{{ item.name }}</div>
</div>
</div>
</el-tab-pane>
<el-tab-pane label="商品详情" name="goodsDetails">
<assembly-goods @change="onTapGoodsDetails" />
</el-tab-pane>
<el-tab-pane label="小程序" name="applet">
<el-alert
title="填写需要跳转的小程序信息 如果没有,请找到对方管理员咨询"
type="warning"
:closable="false"
/>
<el-row class="mt-20">
<el-col :span="8"></el-col>
<el-col :span="8">
<el-form label-position="top" :model="data.applet" :label-width="120">
<el-form-item
label="小程序名称"
prop="name"
:rules="[{ required: true, message: '小程序名称必填' }]"
>
<el-input v-model="data.applet.name" />
</el-form-item>
<el-form-item
label="appid"
prop="appid"
:rules="[{ required: true, message: 'appid必填' }]"
>
<el-input v-model="data.applet.appid" />
</el-form-item>
<el-form-item
label="小程序页面"
prop="page"
:rules="[{ required: true, message: '小程序页面必填' }]"
>
<el-input v-model="data.applet.page" />
</el-form-item>
</el-form>
</el-col>
<el-col :span="8"></el-col>
</el-row>
<div class="center mt-20">
<el-button
color="#626aef"
type="success"
@click="applet"
style="width: 200px"
>确定</el-button
>
</div>
</el-tab-pane>
<el-tab-pane label="web/H5" name="web">
<el-alert
title="网页链接的域名必须在小程序的白名单内"
type="warning"
:closable="false"
/>
<el-row class="mt-20">
<el-col :span="8"></el-col>
<el-col :span="8">
<el-form label-position="top" :model="data.web" :label-width="120">
<el-form-item
label="网页名称"
prop="name"
:rules="[{ required: true, message: '网页名称必填' }]"
>
<el-input v-model="data.web.name" />
</el-form-item>
<el-form-item
label="网页链接"
prop="page"
:rules="[{ required: true, message: '网页链接必填' }]"
>
<el-input v-model="data.web.page" />
</el-form-item>
</el-form>
</el-col>
<el-col :span="8"></el-col>
</el-row>
<div class="center mt-20">
<el-button
color="#626aef"
type="success"
@click="webH5"
style="width: 200px"
>确定</el-button
>
</div>
</el-tab-pane>
<el-tab-pane label="自定义页面" name="fixtures">
<assembly-fixtures @change="onTapFixtures" />
</el-tab-pane>
</el-tabs>
</cl-dialog>
</div>
</template>
<script lang="ts" name="assembly-link" setup>
import { ref, reactive,watch, nextTick, computed, type PropType } from "vue";
import { ElMessage } from "element-plus";
import { useCool } from "/@/cool";
import { Form } from "../../types/form";
const { service } = useCool();
const props = defineProps({
modelValue: {
type: Object as PropType<Form.Link>,
default: function () {
return {
page: "",
appid: "",
type: "",
name: ""
};
}
},
prepend: {
type: Boolean,
default: true
}
});
const emit = defineEmits(["update:modelValue"]);
const value = ref(props.modelValue);
watch(
() => props.modelValue,
async (newValue) => {
value.value = newValue;
}
);
const typeName = computed(() => {
const names = {
index: "平台",
goodsType: "分类",
goodsDetails: "商品",
web: "网页",
applet: "小程序",
fixtures: "自定义",
default: "页面"
};
const type = value.value.type || "default";
return names[type] || names.default;
});
function onChange() {
emit("update:modelValue", value.value);
}
const visible = ref(false);
const activeName = ref(value.value.type || "index");
function onTap() {
visible.value = true;
// modelValue type data.applet data.web
if (value.value.type === "applet") {
data.applet = {
name: value.value.name,
appid: value.value.appid,
page: value.value.page
};
} else if (value.value.type === "web") {
data.web = {
page: value.value.page,
name: value.value.name
};
}
nextTick().then(() => {
getData();
});
}
function open() {
onTap();
}
function handleClick() {
nextTick().then(() => {
getData();
});
}
const data = reactive<any>({
index: [],
goodsType: [],
applet: {
name: "",
appid: "",
page: ""
},
web: {
page: "",
name: ""
}
});
const selectedPage = ref({
name: "",
page: "",
appid: "",
id: 0
});
function setSelectedPage(item: any) {
selectedPage.value = { ...item };
}
function submitPage(type: string) {
const { name, page, appid } = selectedPage.value;
if (name === "") {
ElMessage.error(`请选择${typeName.value}`);
return;
}
value.value = {
name,
type,
page: page || value.value.page,
appid: appid || value.value.appid
};
onChange();
visible.value = false;
}
const applet = () => {
const { name, appid, page } = data.applet;
if (name === "" || appid === "" || page === "") {
ElMessage.error("请填写小程序信息");
return;
}
value.value = {
...data.applet,
type: "applet"
};
onChange();
visible.value = false;
};
const webH5 = () => {
const { name, page } = data.web;
if (name === "" || page === "") {
ElMessage.error("请填写网页信息");
return;
}
if (!page.startsWith("http")) {
ElMessage.error("请填写带https的网页链接");
return;
}
value.value = {
...data.web,
type: "web"
};
onChange();
visible.value = false;
};
function onTapIndex(item: { page: string; name: string }) {
setSelectedPage(item);
submitPage("index");
}
function onTapGoodsType(item: { name: string; id: number }) {
setSelectedPage({ ...item, page: `/pages/goods/list?typeId=${item.id}` });
submitPage("goodsType");
}
function onTapGoodsDetails(item: any) {
setSelectedPage({ name: item.title, id: item.id, page: `/pages/goods/detail?id=${item.id}` });
submitPage("goodsDetails");
}
function onTapFixtures(item: any) {
setSelectedPage({
name: item.name,
id: item.id,
page: `/uni_modules/cool-fixtures/pages/detail?id=${item.id}`
});
submitPage("fixtures");
}
function getData() {
switch (activeName.value) {
case "index":
service.fixtures.mould.getPlatformPages().then((res: any) => {
data.index = res;
});
break;
case "goodsType":
service.goods.type.list({ status: 1 }).then((res: any) => {
data.goodsType = res;
});
break;
default:
break;
}
}
defineExpose({ open });
</script>
<style lang="scss" scoped>
.assembly-link {
display: flex;
align-items: center;
justify-content: space-between;
}
.le-url {
width: 100%;
:deep(.el-input-group__append) {
width: 118px;
background: #623ceb;
border: 1px solid #623ceb;
text-align: center;
font-family: PingFang SC;
font-weight: 500;
color: #ffffff;
box-sizing: border-box;
}
}
.index-box {
margin-bottom: 20px;
.index-title {
font-size: 16px;
font-weight: bold;
margin-bottom: 10px;
}
.index-menu {
display: grid;
grid-template-columns: repeat(8, 1fr);
gap: 10px;
}
}
.goods-type {
display: grid;
grid-template-columns: repeat(8, 1fr);
gap: 10px;
.item {
background-color: #f6f7fa;
border-radius: 10px;
box-sizing: border-box;
cursor: pointer;
padding: 6px;
.name {
text-align: center;
padding: 6px;
}
}
}
.mt-20 {
margin-top: 20px;
}
.center {
display: flex;
justify-content: center;
align-items: center;
}
@media (max-width: 1600px) {
.goods-type {
grid-template-columns: repeat(4, 1fr) !important;
}
.index-menu {
grid-template-columns: repeat(4, 1fr) !important;
}
}
</style>

View File

@ -0,0 +1,161 @@
<template>
<div class="assembly-list-menu">
<div
class="box"
v-for="(item, index) in value"
:key="index"
@contextmenu.stop.prevent="open($event, index)"
>
<div class="text mb-10 flex">
<div class="w-120">左侧文字</div>
<el-input
type="text"
:maxlength="12"
:show-word-limit="true"
v-model="value[index].text"
@change="onChange"
/>
</div>
<div class="text mb-10 flex">
<div class="w-120">右侧文字</div>
<el-input
type="text"
:maxlength="4"
:show-word-limit="true"
v-model="value[index].text2"
@change="onChange"
/>
</div>
<div class="color mb-10 flex">
<div class="w-120">文字颜色</div>
<el-color-picker v-model="value[index].color" @change="onChange" />
</div>
<div class="icon mb-10 flex">
<div class="w-120">图标</div>
<cl-upload
text="上传图标"
v-model="value[index].icon"
:size="[60, 60]"
@success="onChange"
/>
</div>
<div class="link mb-10">
<div>跳转链接</div>
<assembly-link v-model="value[index].link" @change="onChange" />
</div>
</div>
<div class="add" @click="add">
<el-button link type="primary">添加</el-button>
</div>
</div>
</template>
<script lang="ts" name="assembly-list-menu" setup>
import { ref, type PropType } from "vue";
import { cloneDeep } from "lodash-es";
import { ContextMenu } from "@cool-vue/crud";
import { Form } from "../../types/form";
const props = defineProps({
modelValue: {
type: Object as PropType<Form.Title[]>,
default: () => {
return [
{
text: "",
text2: "",
color: "",
icon: "",
link: {
name: "",
type: "",
appid: "",
page: ""
}
}
];
}
}
});
const emit = defineEmits(["update:modelValue"]);
const value = ref(props.modelValue);
function add() {
value.value.push({
text: "标题",
text2: "查看",
color: "#000",
icon: "",
link: {
name: "",
type: "",
appid: "",
page: ""
}
});
onChange();
}
function open(e: any, index: number) {
ContextMenu.open(e, {
list: [
{
label: "删除按钮",
callback(done) {
value.value.splice(index, 1);
onChange();
done();
}
},
{
label: "复制按钮",
callback(done) {
const clonedItem = cloneDeep(value.value[index]);
// indexclonedItem
value.value.splice(index + 1, 0, clonedItem);
onChange();
done();
}
}
]
});
}
function onChange() {
emit("update:modelValue", value.value);
}
</script>
<style lang="scss" scoped>
.assembly-list-menu {
margin-bottom: 10px;
.box {
padding: 10px;
border: 1px solid var(--el-border-color);
border-radius: 6px;
box-sizing: border-box;
margin-bottom: 10px;
}
.add {
margin-top: 20px;
display: flex;
align-items: center;
justify-content: center;
height: 30px;
border: 1px solid var(--el-border-color);
border-radius: 6px;
cursor: pointer;
}
.mb-10 {
margin-bottom: 10px;
}
.w-120 {
width: 120px;
}
.flex {
display: flex;
align-items: center;
justify-content: space-between;
}
}
</style>

View File

@ -0,0 +1,156 @@
<template>
<div class="assembly-menu">
<div class="mode mb-10">
<div class="w-80">模式</div>
<el-radio-group v-model="value.mode" @change="onChange">
<el-radio value="mode-1">文字+图标</el-radio>
<el-radio value="mode-2">单图片</el-radio>
</el-radio-group>
</div>
<template v-if="value.mode == 'mode-1'">
<div class="text mb-10">
<div class="w-120">按钮文字</div>
<el-input
type="text"
:maxlength="maxlength"
:show-word-limit="showWordLimit"
v-model="value.text"
@change="onChange"
/>
</div>
<div class="color mb-10 flex">
<div class="col-cen">
<div>图标</div>
<cl-upload
text="上传图标"
v-model="value.icon"
:size="[60, 60]"
@success="onChange"
/>
</div>
<div class="col-cen">
<div>文字颜色</div>
<el-color-picker v-model="value.color" @change="onChange" />
</div>
<div class="col-cen">
<div>背景颜色</div>
<el-color-picker
show-alpha
v-model="value.backgroundColor"
:predefine="['rgba(255, 255, 255, 0)']"
@change="onChange"
/>
</div>
</div>
</template>
<template v-else>
<div class="icon">
<div>
<span class="mr-10">按钮的图片</span>
<span class="tips" v-if="iconTips">{{ iconTips }}</span>
</div>
<cl-upload
text="上传图片"
v-model="value.icon"
:size="[80, 80]"
@success="onChange"
/>
</div>
</template>
<div class="link mb-10">
<div>跳转链接</div>
<assembly-link v-model="value.link" @change="onChange" />
</div>
</div>
</template>
<script lang="ts" name="assembly-menu" setup>
import { ref, type PropType } from "vue";
import { Form } from "../../types/form";
const props = defineProps({
modelValue: {
type: Object as PropType<Form.Menu>,
default: function () {
return {
text: "按钮",
useText: true,
mode: "mode-1",
link: {
page: "",
appid: "",
type: "",
name: ""
},
icon: "",
color: "#000",
backgroundColor: "#fff"
};
}
},
iconTips: {
type: String,
default: ""
},
showWordLimit: {
type: Boolean,
default: true
},
maxlength: {
type: Number,
default: 8
}
});
const emit = defineEmits(["update:modelValue", "change"]);
const value = ref(props.modelValue);
function onChange() {
emit("update:modelValue", value.value);
emit("change", value.value);
}
</script>
<style lang="scss" scoped>
.assembly-menu {
padding: 10px;
border: 1px solid var(--el-border-color);
border-radius: 6px;
box-sizing: border-box;
margin-bottom: 10px;
.mode {
display: flex;
align-items: center;
.w-80 {
width: 80px;
}
}
.text {
display: flex;
align-items: center;
.w-120 {
width: 120px;
}
}
.flex {
display: flex;
justify-content: space-between;
.col-cen {
display: flex;
flex-direction: column;
align-items: center;
}
}
.mb-10 {
margin-bottom: 10px;
}
.mr-10 {
margin-right: 10px;
}
.tips {
font-size: 12px;
color: #bfbfbf;
}
}
</style>

View File

@ -0,0 +1,117 @@
<template>
<div class="assembly-menus">
<div>
<div class="item" v-for="(item, index) in value" :key="index">
<div @contextmenu.stop.prevent="open($event, index)">
<assembly-menu
iconTips="建议尺寸200px*200px"
v-model="value[index]"
:maxlength="4"
@change="onChange"
/>
</div>
</div>
</div>
<div class="add" @click="add">
<el-button link type="primary">添加({{ value.length }}/20)</el-button>
</div>
</div>
</template>
<script lang="ts" name="assembly-menus" setup>
import { cloneDeep } from "lodash-es";
import { ref, type PropType } from "vue";
import { ElMessage } from "element-plus";
import { ContextMenu } from "@cool-vue/crud";
import { Form } from "../../types/form";
const props = defineProps({
modelValue: {
type: Array as PropType<Form.Menu[]>,
default: () => []
}
});
const emit = defineEmits(["update:modelValue"]);
const value = ref(props.modelValue);
function add() {
if (value.value.length >= 20) {
ElMessage.error("最多添加20个按钮");
return;
}
value.value.push({
text: "按钮",
useText: true,
mode: "mode-1",
link: {
page: "",
appid: "",
type: "",
name: ""
},
icon: "",
color: "#000",
backgroundColor: "#fff"
});
}
function open(e: any, index: number) {
ContextMenu.open(e, {
list: [
{
label: "删除按钮",
callback(done) {
value.value.splice(index, 1);
onChange();
done();
}
},
{
label: "复制按钮",
callback(done) {
const clonedItem = cloneDeep(value.value[index]);
// indexclonedItem
value.value.splice(index + 1, 0, clonedItem);
onChange();
done();
}
}
]
});
}
function onChange() {
emit("update:modelValue", value.value);
}
</script>
<style lang="scss" scoped>
.assembly-menus {
padding: 0;
box-sizing: border-box;
margin-bottom: 10px;
.tips {
font-size: 12px;
color: #bfbfbf;
}
.item {
margin-top: 20px;
}
.item:nth-child(1) {
margin-top: 0;
}
.add {
margin-top: 20px;
display: flex;
align-items: center;
justify-content: center;
height: 30px;
border: 1px solid var(--el-border-color);
border-radius: 6px;
cursor: pointer;
}
}
</style>

View File

@ -0,0 +1,67 @@
<template>
<div class="assembly-picture">
<div class="box">
<cl-upload v-model="value.pic" :size="[80, 80]" @success="onChange"></cl-upload>
<div class="link">
<assembly-link v-model="value.link" :prepend="false" @change="onChange" />
<div class="tips">建议图片宽度750px</div>
</div>
</div>
</div>
</template>
<script lang="ts" name="assembly-picture" setup>
import { ref, type PropType } from "vue";
import { Form } from "../../types/form";
const props = defineProps({
modelValue: {
type: Object as PropType<Form.Picture>,
default: () => {
return {
pic: "",
link: {
name: "",
type: "",
appid: "",
page: ""
}
};
}
}
});
const emit = defineEmits(["update:modelValue"]);
const value = ref(props.modelValue);
function onChange() {
emit("update:modelValue", value.value);
}
</script>
<style lang="scss" scoped>
.assembly-picture {
box-sizing: border-box;
background-color: #f6f7fa;
border-radius: 10px;
padding: 6px;
margin-bottom: 10px;
.box {
box-sizing: border-box;
display: flex;
background-color: #ffffff;
padding: 10px;
.link {
box-sizing: border-box;
margin-left: 10px;
}
.tips {
font-size: 14px;
margin-top: 10px;
color: var(--el-color-warning);
}
}
}
</style>

View File

@ -0,0 +1,453 @@
<template>
<div class="assembly-rubik-cube">
<div class="w-120">显示模式</div>
<el-radio-group v-model="value.mode" @change="changeGroup">
<el-radio v-for="mode in modes" :key="mode.type" :value="mode.type">
{{ mode.label }}
</el-radio>
</el-radio-group>
<div class="w-120">图片间隔</div>
<assembly-slider
v-model="value.gap"
:min="0"
:max="40"
unit="rpx"
:step="1"
@change="changeSlider"
/>
<div
class="box"
:class="[`is-${value.mode}`]"
:style="{ gap: `${value.gap / 2}px`, '--gap': `${value.gap / 2}px` }"
>
<div
class="item"
v-for="(item, index) in value.list"
:key="index"
:class="[`is-${value.mode}-item-${index}`]"
@click="onTap(index)"
>
<div class="circle" v-if="activate === index">
<el-icon :size="20" color="#4165d7">
<circle-check-filled />
</el-icon>
</div>
<div class="tips" v-if="!item.icon">
{{ item.tips }}
</div>
<el-image class="icon" :src="item.icon" fit="fill" v-else></el-image>
</div>
</div>
<div class="upload">
<div class="upload-box">
<cl-upload
text="上传图片"
v-model="value.list[activate].icon"
@success="onChange"
:size="[80, 80]"
/>
<div class="link">
<assembly-link
v-model="value.list[activate].link"
:prepend="false"
@change="onChange"
/>
<div class="tips">{{ value.list[activate].tips }}</div>
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup name="assembly-rubik-cube">
import { ref, type PropType } from "vue";
import { Form } from "../../types/form";
import { CircleCheckFilled } from "@element-plus/icons-vue";
const props = defineProps({
modelValue: {
type: Object as PropType<Form.RubikCube>,
default: () => ({
mode: "mode-1",
gap: 0,
list: [
{
icon: "",
tips: "宽度375",
link: {
name: "",
type: "",
appid: "",
page: ""
}
},
{
icon: "",
tips: "宽度375",
link: {
name: "",
type: "",
appid: "",
page: ""
}
}
]
})
}
});
const emit = defineEmits(["update:modelValue"]);
const value = ref(props.modelValue);
const modes: Form.RubikCubeMode[] = [
{
type: "mode-1",
label: "一行两个",
list: [
{
icon: "",
tips: "宽度375",
link: {
name: "",
type: "",
appid: "",
page: ""
}
},
{
icon: "",
tips: "宽度375",
link: {
name: "",
type: "",
appid: "",
page: ""
}
}
]
},
{
type: "mode-2",
label: "一行三个",
list: [
{
icon: "",
tips: "宽度250",
link: {
name: "",
type: "",
appid: "",
page: ""
}
},
{
icon: "",
tips: "宽度250",
link: {
name: "",
type: "",
appid: "",
page: ""
}
},
{
icon: "",
tips: "宽度250",
link: {
name: "",
type: "",
appid: "",
page: ""
}
}
]
},
{
type: "mode-3",
label: "左一右二",
list: [
{
icon: "",
tips: "宽375\n高750",
link: {
name: "",
type: "",
appid: "",
page: ""
}
},
{
icon: "",
tips: "宽375\n高375",
link: {
name: "",
type: "",
appid: "",
page: ""
}
},
{
icon: "",
tips: "宽375\n高375",
link: {
name: "",
type: "",
appid: "",
page: ""
}
}
]
},
{
type: "mode-4",
label: "上一下二",
list: [
{
icon: "",
tips: "宽750\n高375",
link: {
name: "",
type: "",
appid: "",
page: ""
}
},
{
icon: "",
tips: "宽375\n高375",
link: {
name: "",
type: "",
appid: "",
page: ""
}
},
{
icon: "",
tips: "宽375\n高375",
link: {
name: "",
type: "",
appid: "",
page: ""
}
}
]
}
];
const activate = ref(0);
function changeGroup(mode: any) {
const selectedMode = modes.find((m) => m.type === mode);
if (selectedMode) {
value.value.list = selectedMode.list;
activate.value = 0;
changeSlider(value.value.gap);
onChange();
}
}
function onTap(index: number) {
activate.value = index;
}
//
function changeSlider(val: number) {
switch (value.value.mode) {
case "mode-1":
//
value.value.list.forEach((e) => {
const width = 375;
e.tips = `宽度${width - (val ? val / 2 : 0)}\n高度不限`;
});
break;
case "mode-2":
//
value.value.list.forEach((e) => {
const width = 250;
e.tips = `宽度${width - (val ? Math.floor(val / 3) : 0)}\n高度不限`;
});
break;
case "mode-3":
//
value.value.list.forEach((e, index) => {
const width = 375;
const w = width - (val ? Math.floor(val / 2) : 0);
e.tips = index ? `${w}\n高度不限` : `${w}\n高度不限`;
});
break;
case "mode-4":
value.value.list.forEach((e, index) => {
if (index) {
const width = 375;
const w = width - (val ? Math.floor(val / 2) : 0);
e.tips = `${w}\n高度不限`;
} else {
e.tips = `宽750\n高度不限`;
}
});
break;
default:
break;
}
value.value.list;
}
function onChange() {
emit("update:modelValue", value.value);
}
</script>
<style lang="scss" scoped>
.assembly-rubik-cube {
.w-120 {
width: 120px;
}
.box {
display: flex;
margin-top: 10px;
.item {
display: flex;
align-items: center;
justify-content: center;
min-height: 100px;
background-color: lightblue;
box-sizing: border-box;
cursor: pointer;
position: relative;
.icon {
width: 100%;
max-width: 100%;
border: 0;
vertical-align: middle;
}
.tips {
font-size: 14px;
text-align: center;
white-space: pre-line;
}
.circle {
position: absolute;
top: 10px;
right: 10px;
z-index: 1;
}
}
}
.is-mode-1 {
display: grid;
grid-template-columns: repeat(2, 1fr);
.item {
&.is-mode-1-item-0 {
min-height: calc(150px - var(--gap) / 2);
background-color: lightgreen;
}
&.is-mode-1-item-1 {
min-height: calc(150px - var(--gap) / 2);
background-color: lightcoral;
}
}
}
.is-mode-2 {
display: grid;
grid-template-columns: repeat(3, 1fr);
.item {
&.is-mode-2-item-0 {
min-height: calc(100px - var(--gap) / 3);
}
&.is-mode-2-item-1 {
min-height: calc(100px - var(--gap) / 3);
background-color: lightgreen;
}
&.is-mode-2-item-2 {
min-height: calc(100px - var(--gap) / 3);
background-color: lightcoral;
}
}
}
.is-mode-3 {
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr 1fr;
.item {
&.is-mode-3-item-0 {
min-height: 300px;
grid-column: 1 / 2;
grid-row: 1 / 3;
background-color: lightblue;
}
&.is-mode-3-item-1 {
min-height: calc(150px - var(--gap) / 2);
grid-column: 2 / 3;
grid-row: 1 / 2;
background-color: lightgreen;
}
&.is-mode-3-item-2 {
min-height: calc(150px - var(--gap) / 2);
grid-column: 2 / 3;
grid-row: 2 / 3;
background-color: lightcoral;
}
}
}
.is-mode-4 {
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-rows: auto auto;
.item {
&.is-mode-4-item-0 {
min-height: calc(150px - var(--gap) / 2);
grid-column: 1 / 3;
grid-row: 1 / 2;
background-color: lightblue;
}
&.is-mode-4-item-1 {
min-height: calc(150px - var(--gap) / 2);
grid-column: 1 / 2;
grid-row: 2 / 3;
background-color: lightgreen;
}
&.is-mode-4-item-2 {
min-height: calc(150px - var(--gap) / 2);
grid-column: 2 / 3;
grid-row: 2 / 3;
background-color: lightcoral;
}
}
}
.upload {
margin-top: 20px;
box-sizing: border-box;
background-color: #f6f7fa;
border-radius: 10px;
padding: 6px;
margin-bottom: 10px;
.upload-box {
display: flex;
background-color: #ffffff;
padding: 10px;
border-radius: 6px;
.link {
margin-left: 10px;
}
.tips {
font-size: 14px;
margin-top: 10px;
color: var(--el-color-warning);
text-align: center;
}
}
}
}
</style>

View File

@ -0,0 +1,51 @@
<template>
<div class="assembly-slider">
<el-slider
v-model="value"
:min="min"
:max="max"
:step="step"
placement="right"
:format-tooltip="(n: number) => `${n}${unit}`"
@change="onChange"
/>
</div>
</template>
<script lang="ts" name="assembly-slider" setup>
import { ref } from "vue";
const props = defineProps({
modelValue: {
type: Number,
default: 0
},
min: Number,
max: Number,
step: Number,
unit: {
type: String,
default: "rpx"
}
});
const emit = defineEmits(["update:modelValue", "change"]);
const value = ref(props.modelValue);
function onChange() {
emit("update:modelValue", value.value);
emit("change", value.value);
}
</script>
<style lang="scss" scoped>
.assembly-slider {
padding: 0 20px;
box-sizing: border-box;
.tips {
font-size: 12px;
color: #bfbfbf;
}
}
</style>

View File

@ -0,0 +1,149 @@
<template>
<div class="assembly-suspension">
<div
class="box"
v-for="(item, index) in value"
:key="index"
@contextmenu.stop.prevent="open($event, index)"
>
<div class="menu">
<cl-upload
text="上传图标"
v-model="value[index].icon"
:size="[60, 60]"
@success="onChange"
/>
<div class="ml-10">
<assembly-link
:prepend="false"
v-model="value[index].link"
@change="onChange"
/>
<span class="ml-10 text-warning">{{ isFirstButton(index) }}</span>
</div>
</div>
</div>
<div class="add" @click="add">
<el-button link type="primary">添加</el-button>
</div>
</div>
</template>
<script lang="ts" name="assembly-suspension" setup>
import { ref, computed, type PropType } from "vue";
import { cloneDeep } from "lodash-es";
import { ContextMenu } from "@cool-vue/crud";
import { Form } from "../../types/form";
const props = defineProps({
modelValue: {
type: Object as PropType<Form.Suspension[]>,
default: () => {
return [
{
icon: "https://tsb-yx.oss-cn-guangzhou.aliyuncs.com/app/mini/float-menu.png",
tips: "宽高128px",
link: {
name: "",
type: "",
appid: "",
page: ""
}
}
];
}
}
});
const emit = defineEmits(["update:modelValue"]);
//
const isExpand = computed(() => value.value.length > 1);
//
const isFirstButton = (index: number) => {
if (index === 0 && isExpand.value) {
return "默认第一个为展开按钮不跳转";
}
return value.value[index].tips;
};
const value = ref(props.modelValue);
function add() {
value.value.push({
icon: "",
tips: "宽度128px",
link: {
name: "",
type: "",
appid: "",
page: ""
}
});
onChange();
}
function open(e: any, index: number) {
ContextMenu.open(e, {
list: [
{
label: "删除按钮",
callback(done) {
value.value.splice(index, 1);
onChange();
done();
}
},
{
label: "复制按钮",
callback(done) {
const clonedItem = cloneDeep(value.value[index]);
// indexclonedItem
value.value.splice(index + 1, 0, clonedItem);
onChange();
done();
}
}
]
});
}
function onChange() {
emit("update:modelValue", value.value);
}
</script>
<style lang="scss" scoped>
.assembly-suspension {
margin-bottom: 10px;
.box {
box-sizing: border-box;
margin-bottom: 10px;
background-color: #f6f7fa;
border-radius: 10px;
padding: 6px;
.menu {
padding: 10px;
background-color: #ffffff;
display: flex;
justify-content: start;
align-items: center;
border-radius: 6px;
}
}
.add {
margin-top: 20px;
display: flex;
align-items: center;
justify-content: center;
height: 30px;
border: 1px solid var(--el-border-color);
border-radius: 6px;
cursor: pointer;
}
.ml-10 {
margin-left: 10px;
}
.text-warning {
color: var(--el-color-warning);
}
}
</style>

View File

@ -0,0 +1,41 @@
<template>
<div class="assembly-switch">
<el-switch
v-model="value"
active-text="启用"
inactive-text="关闭"
:active-value="true"
:inactive-value="false"
:inline-prompt="true"
style="--el-switch-on-color: #13ce66; --el-switch-off-color: #ff4949"
@change="onChange"
>
</el-switch>
</div>
</template>
<script lang="ts" name="assembly-switch" setup>
import { ref } from "vue";
const props = defineProps({
modelValue: Boolean
});
const emit = defineEmits(["update:modelValue", "change"]);
const value = ref(props.modelValue);
function onChange() {
emit("update:modelValue", value.value);
emit("change", value.value);
}
</script>
<style lang="scss" scoped>
.assembly-switch {
.tips {
font-size: 12px;
color: #bfbfbf;
}
}
</style>

View File

@ -0,0 +1,87 @@
<template>
<div class="assembly-title">
<div class="text mb-10 flex">
<div class="w-120">文字</div>
<el-input
type="text"
:maxlength="maxlength"
:show-word-limit="showWordLimit"
v-model="value.text"
@change="onChange"
/>
</div>
<div class="color mb-10 flex">
<div class="w-120">文字颜色</div>
<el-color-picker v-model="value.color" @change="onChange" />
</div>
<div class="icon mb-10 flex">
<div class="w-120">图标</div>
<cl-upload text="上传图标" v-model="value.icon" :size="[60, 60]" @success="onChange" />
</div>
<div class="link mb-10">
<div>跳转链接</div>
<assembly-link v-model="value.link" @change="onChange" />
</div>
</div>
</template>
<script lang="ts" name="assembly-title" setup>
import { ref, type PropType } from "vue";
import { Form } from "../../types/form";
const props = defineProps({
modelValue: {
type: Object as PropType<Form.Title>,
default: () => {
return {
text: "",
color: "",
icon: "",
link: {
name: "",
type: "",
appid: "",
page: ""
}
};
}
},
showWordLimit: {
type: Boolean,
default: true
},
maxlength: {
type: Number,
default: 20
}
});
const emit = defineEmits(["update:modelValue"]);
const value = ref(props.modelValue);
function onChange() {
emit("update:modelValue", value.value);
}
</script>
<style lang="scss" scoped>
.assembly-title {
padding: 10px;
border: 1px solid var(--el-border-color);
border-radius: 6px;
box-sizing: border-box;
margin-bottom: 10px;
.mb-10 {
margin-bottom: 10px;
}
.w-120 {
width: 120px;
}
.flex {
display: flex;
align-items: center;
justify-content: space-between;
}
}
</style>

View File

@ -0,0 +1,226 @@
<template>
<div class="dp-config">
<div class="head" v-show="t">
<span>{{ t }}</span>
<el-button text type="danger" @click="del" v-if="showDel">删除</el-button>
</div>
<div class="tips" v-if="tips">
<el-icon>
<warning-filled />
</el-icon>
<span>{{ tips }}</span>
</div>
<el-scrollbar class="scrollbar" v-if="visible">
<div class="form">
<cl-form inner ref="Form"> </cl-form>
<el-empty :image-size="100" v-show="!t" description="未选择组件" />
</div>
</el-scrollbar>
</div>
</template>
<script lang="tsx" setup>
import { useForm } from "@cool-vue/crud";
import { WarningFilled } from "@element-plus/icons-vue";
import { computed, nextTick, ref, watch } from "vue";
import { useCool } from "/@/cool";
import { useDp } from "../hooks";
const { mitt } = useCool();
const { dp } = useDp();
const Form = useForm();
const visible = ref(true);
const t = ref("");
const data = ref<Record<string, any>>({});
const tips = ref("");
//
const showDel = computed(() => data.value.id !== "header");
//
function del() {
clear();
if (data.value.name === "suspension") {
dp.removeSuspension();
} else {
dp.removeBy({ id: data.value.id });
}
}
//
function clear() {
Form.value?.close();
t.value = "";
tips.value = "";
}
//
function assignNestedProperties(obj: Record<string, any>) {
Object.keys(obj).forEach((key) => {
if (key.includes("-")) {
const [parentKey, childKey] = key.split("-");
if (!obj[parentKey]) {
obj[parentKey] = {};
}
obj[parentKey][childKey] = obj[key];
}
});
}
//
function refresh(options: any) {
data.value = options;
const { title, items = [] } = options.config;
t.value = title || "未配置";
tips.value = options.config.tips;
Form.value?.open({
form: options.component.props,
items,
props: {
labelPosition: "top"
},
op: {
hidden: true
}
});
}
// watch
let stopWatch: (() => void) | null = null;
//
function isEmpty(obj: Object) {
return Object.keys(obj).length === 0;
}
/**
* 设置新的 watch 监听器
* @param {Function} callback - 监听到数据变化时调用的回调函数
*/
function setupWatch(callback: (val: any) => void) {
stopWatch?.();
stopWatch = watch(
() => Form.value?.form,
(val) => {
if (val && !isEmpty(val)) {
assignNestedProperties(val);
callback(val);
}
},
{
immediate: true,
deep: true
}
);
}
/**
* 处理配置设置
* @param {Object} data - 包含 options cb 的配置对象
*/
function onSetConfig({ options, cb }: { options: any; cb: (val: any) => void }) {
visible.value = false;
nextTick(() => {
visible.value = true;
nextTick(() => {
refresh(options || {});
setupWatch(cb);
});
});
}
//
function onClearConfig() {
clear();
}
mitt.on("dp.setConfig", onSetConfig);
mitt.on("dp.clearConfig", onClearConfig);
</script>
<style lang="scss" scoped>
.dp-config {
height: 680px;
width: 350px;
overflow: hidden;
background-color: #fff;
border-radius: 5px;
.head {
display: flex;
align-items: center;
justify-content: space-between;
height: 54px;
font-size: 18px;
color: #262626;
padding: 0 15px;
border-bottom: 1px solid #ebeef5;
}
.tips {
display: inline-flex;
align-items: center;
background-color: #fff8d5;
color: #ffbb00;
margin: 10px 24px 0 24px;
padding: 0 20px 0 4px;
border-radius: 4px;
.el-icon {
margin: 5px;
font-size: 15px;
}
span {
font-size: 12px;
}
}
.scrollbar {
height: calc(100% - 55px);
}
.form {
padding: 16px 24px;
box-sizing: border-box;
:deep(.form-label) {
font-size: 16px;
color: #000;
span {
font-size: 12px;
color: #bfbfbf;
margin-left: 10px;
}
.text-warning {
color: var(--el-color-warning);
}
.text-success {
color: var(--el-color-success);
}
.text-primary {
color: var(--el-color-primary);
}
.text-danger {
color: var(--el-color-danger);
}
}
:deep(.el-form-item__label) {
color: #000;
font-size: 16px;
}
:deep(.cl-form-card__container > .cl-form-item__children) {
overflow: hidden;
}
:deep(.cl-form-card) {
background-color: #fff;
}
:deep(.el-form-item) {
margin-bottom: 18px;
}
}
}
</style>

View File

@ -0,0 +1,143 @@
<template>
<div class="fix-banner is-bg" :style="baseStyle">
<el-carousel
trigger="click"
:height="`${height / 2}px`"
:autoplay="false"
:type="mode"
:indicator-position="indicatorPosition"
>
<el-carousel-item v-for="(item, index) in list" :key="index">
<el-image :src="item.pic" fit="fill" class="pic">
<template #error>
<div class="image-slot">
<el-icon><icon-picture /></el-icon>
</div>
</template>
</el-image>
</el-carousel-item>
</el-carousel>
</div>
</template>
<script lang="ts" name="fix-banner" setup>
import { ref, computed, type PropType } from "vue";
import { Picture as IconPicture } from "@element-plus/icons-vue";
import { Form } from "../../types/form";
const props = defineProps({
mode: {
type: String,
default: ""
},
indicatorDots: {
type: Boolean,
default: true
},
height: {
type: Number,
default: 300
},
list: {
type: Array as PropType<{ pic: string; link: Form.Link }[]>,
default: () => {
return [
{
pic: "",
link: {
name: "",
type: "",
appid: "",
page: ""
}
}
];
}
},
styleSpacing: {
type: Object as PropType<Form.Spacing>,
default: () => {
return {
marginTop: 0,
marginBottom: 0,
marginLR: 0,
padding: 0,
borderTopLR: 0,
borderBottomLR: 0
};
}
},
styleColor: {
type: Object as PropType<Form.Color>,
default: () => {
return {
color: "#000",
backgroundColor: "#FFFFFF",
opacity: 1
};
}
}
});
const baseStyle = computed(() => {
return {
margin: `${props.styleSpacing.marginTop / 2}px ${props.styleSpacing.marginLR / 2}px ${props.styleSpacing.marginBottom / 2}px ${props.styleSpacing.marginLR / 2}px`,
color: props.styleColor.color,
padding: `${props.styleSpacing.padding / 2}px`,
borderRadius: `${props.styleSpacing.borderTopLR / 2}px ${props.styleSpacing.borderTopLR / 2}px ${props.styleSpacing.borderBottomLR / 2}px ${props.styleSpacing.borderBottomLR / 2}px`,
"--opacity": props.styleColor.opacity,
"--background": props.styleColor.backgroundColor
};
});
const indicatorPosition = computed(() => {
if (props.indicatorDots) return "";
return "none";
});
</script>
<style lang="scss" scoped>
.fix-banner {
box-sizing: border-box;
overflow: hidden;
.tips {
font-size: 12px;
color: #bfbfbf;
}
.pic {
width: 100%;
max-width: 100%;
height: 100%;
.image-slot {
height: 100%;
display: flex;
justify-content: center;
align-items: center;
.el-icon {
font-size: 60px;
color: var(--el-text-color-placeholder);
}
}
}
:deep(.el-carousel__item) {
text-align: center;
}
}
.is-bg {
position: relative;
z-index: 1; /* 确保 .is-bg 及其子元素位于 ::after 伪元素的上层 */
}
.is-bg::after {
content: "";
background-color: var(--background);
opacity: var(--opacity);
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
z-index: -1;
pointer-events: none;
}
</style>

View File

@ -0,0 +1,256 @@
<template>
<div class="fix-coupon is-bg" :style="baseStyle">
<div class="coupon-activity" v-if="item.id">
<div class="a">
<div class="text">
<span class="name">{{ item?.title }}</span>
<span class="desc">{{ item?.description }}</span>
</div>
<span class="tag" v-if="item">点击领券</span>
</div>
<div class="b">
<template v-if="item">
<span class="amount">{{ item?.amount || 0 }}</span>
<span class="doc">
{{ doc }}
</span>
</template>
</div>
<div class="c"></div>
</div>
<template v-else>
<div class="empty">
<span>优惠券</span>
</div>
</template>
</div>
</template>
<script lang="ts" name="fix-coupon" setup>
import { computed, type PropType } from "vue";
import { Form } from "../../types/form";
const props = defineProps({
item: {
type: Object as PropType<Eps.MarketCouponInfoEntity>,
default: () => {
return {
id: 0,
title: "",
description: "",
amount: 0,
type: 0,
condition: ""
};
}
},
color: {
type: String,
default: "#2b2e3d"
},
styleSpacing: {
type: Object as PropType<Form.Spacing>,
default: () => {
return {
marginTop: 0,
marginBottom: 0,
marginLR: 0,
padding: 0,
borderTopLR: 0,
borderBottomLR: 0
};
}
},
styleColor: {
type: Object as PropType<Form.Color>,
default: () => {
return {
color: "#000",
backgroundColor: "#FFFFFF",
opacity: 1
};
}
}
});
/**
* 将十六进制颜色转换为 RGB
* @param hex - 十六进制颜色代码 (例如: #2b2e3d)
* @returns - RGB 颜色字符串 (例如: 43, 46, 61)
*/
function hexToRgb(hex: string) {
// #
hex = hex.replace(/^#/, "");
// 3 6
if (hex.length === 4) {
hex = hex
.split("")
.map((char: any, index: number) => (index > 0 ? char + char : char))
.join("");
}
const bigint = parseInt(hex, 16);
return [(bigint >> 16) & 255, (bigint >> 8) & 255, bigint & 255].join(", ");
}
const baseStyle = computed(() => {
const { color, styleSpacing, styleColor } = props;
//
const marginTop = styleSpacing.marginTop / 2 + "px";
const marginLR = styleSpacing.marginLR / 2 + "px";
const marginBottom = styleSpacing.marginBottom / 2 + "px";
const padding = styleSpacing.padding / 2 + "px";
const borderRadius = `${styleSpacing.borderTopLR / 2}px ${styleSpacing.borderTopLR / 2}px ${styleSpacing.borderBottomLR / 2}px ${styleSpacing.borderBottomLR / 2}px`;
const rgbaColor = hexToRgb(color);
//
return {
margin: `${marginTop} ${marginLR} ${marginBottom} ${marginLR}`,
color: styleColor.color,
padding,
"--opacity": styleColor.opacity,
"--background": styleColor.backgroundColor,
"--color": rgbaColor,
borderRadius
};
});
// 使
const doc = computed(() => {
const { type, condition } = props.item;
switch (type) {
case 0:
return `${condition?.fullAmount}可用`;
}
});
</script>
<style lang="scss" scoped>
.fix-coupon {
box-sizing: border-box;
overflow: hidden;
.empty {
height: 100px;
display: flex;
justify-content: center;
align-items: center;
span {
font-size: 26px;
color: var(--el-text-color-placeholder);
}
}
}
.is-bg {
position: relative;
z-index: 1; /* 确保 .is-bg 及其子元素位于 ::after 伪元素的上层 */
}
.is-bg::after {
content: "";
background-color: var(--background);
opacity: var(--opacity);
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
z-index: -1;
pointer-events: none;
}
.coupon-activity {
display: flex;
position: relative;
height: 80px;
letter-spacing: 1px;
margin: 0 auto;
.a {
background: linear-gradient(140deg, rgba(var(--color), 0.75), rgba(var(--color), 1));
height: 70px;
width: calc(100% - 125px);
border-radius: 6px;
position: absolute;
left: 12px;
bottom: 1px;
z-index: 2;
color: #fff;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 12px;
box-sizing: border-box;
.name {
display: block;
font-size: 14px;
font-weight: 500;
}
.desc {
font-size: 12px;
color: #ccc;
}
.tag {
padding: 1px 5px;
border-radius: 2px;
background-color: #eb10ab;
color: #fff;
font-size: 12px;
margin-right: 8px;
}
}
.b {
height: 80px;
width: 110px;
background-color: #e2e2e2;
box-sizing: border-box;
position: absolute;
right: 12px;
bottom: 0;
border-radius: 6px;
z-index: 3;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.amount {
font-size: 30px;
font-weight: bold;
line-height: 1;
color: rgba(var(--color), 1);
&::after {
content: "元";
font-size: 24px;
position: relative;
top: -2px;
}
}
.doc {
font-size: 12px;
color: #666;
}
}
.c {
content: "";
display: block;
height: 20px;
width: 20px;
background-color: #868686;
border-radius: 20px;
z-index: 1;
position: absolute;
right: 108px;
top: 1px;
}
}
</style>

View File

@ -0,0 +1,68 @@
<template>
<div class="fix-empty is-bg" :style="baseStyle"></div>
</template>
<script lang="ts" name="fix-empty" setup>
import { ref, computed, type PropType } from "vue";
import { Form } from "../../types/form";
const props = defineProps({
height: {
type: Number,
default: 20
},
styleSpacing: {
type: Object as PropType<Form.Spacing>,
default: () => {
return {
marginTop: 0,
marginBottom: 0,
marginLR: 0,
padding: 0,
borderTopLR: 0,
borderBottomLR: 0
};
}
},
styleColor: {
type: Object as PropType<Form.Color>,
default: () => {
return {
color: "#000",
backgroundColor: "#FFFFFF",
opacity: 1
};
}
}
});
const baseStyle = computed(() => {
return {
margin: `${props.styleSpacing.marginTop / 2}px ${props.styleSpacing.marginLR / 2}px ${props.styleSpacing.marginBottom / 2}px ${props.styleSpacing.marginLR / 2}px`,
color: props.styleColor.color,
padding: `${props.styleSpacing.padding / 2}px`,
"--opacity": props.styleColor.opacity,
"--background": props.styleColor.backgroundColor,
borderRadius: `${props.styleSpacing.borderTopLR / 2}px ${props.styleSpacing.borderTopLR / 2}px ${props.styleSpacing.borderBottomLR / 2}px ${props.styleSpacing.borderBottomLR / 2}px`,
height: `${props.height / 2}px`
};
});
</script>
<style lang="scss" scoped>
.is-bg {
position: relative;
z-index: 1; /* 确保 .is-bg 及其子元素位于 ::after 伪元素的上层 */
}
.is-bg::after {
content: "";
background-color: var(--background);
opacity: var(--opacity);
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
z-index: -1;
pointer-events: none;
}
</style>

View File

@ -0,0 +1,247 @@
<template>
<div class="fix-goods-list is-bg" :style="baseStyle">
<template v-if="list.length">
<div class="inner" :class="[`is-${data.mode}`]">
<div
class="grid-item"
v-for="(item, index) in list"
:key="index"
:class="[data.isShadow ? 'is-shadow' : '']"
>
<el-image class="icon" :src="item.mainPic" fit="cover" />
<div class="content">
<div class="title">{{ item.title }}</div>
<div class="price-sold">
<span class="price">{{ item.price }}</span>
<span class="sold">已售 {{ item.sold }}</span>
</div>
</div>
</div>
</div>
</template>
<template v-else>
<div class="empty">
<span>商品组</span>
</div>
</template>
</div>
</template>
<script lang="ts" name="fix-goods-list" setup>
import { ref, watch, computed, type PropType } from "vue";
import { Form } from "../../types/form";
import { useCool } from "/@/cool";
const { service } = useCool();
const props = defineProps({
data: {
type: Object as PropType<Form.Goods>,
default: () => {
({
mode: "mode-1",
source: "source-1",
attribute: 0,
num: 99,
gap: 0,
isShadow: false,
type: [],
list: []
});
}
},
styleSpacing: {
type: Object as PropType<Form.Spacing>,
default: () => {
return {
marginTop: 0,
marginBottom: 0,
marginLR: 0,
padding: 0,
borderTopLR: 0,
borderBottomLR: 0
};
}
},
styleColor: {
type: Object as PropType<Form.Color>,
default: () => {
return {
color: "#000",
backgroundColor: "#f5f6f7",
opacity: 1
};
}
}
});
const baseStyle = computed(() => {
return {
margin: `${props.styleSpacing.marginTop / 2}px ${props.styleSpacing.marginLR / 2}px ${props.styleSpacing.marginBottom / 2}px ${props.styleSpacing.marginLR / 2}px`,
color: props.styleColor.color,
padding: `${props.styleSpacing.padding / 2}px`,
"--opacity": props.styleColor.opacity,
"--background": props.styleColor.backgroundColor,
borderRadius: `${props.styleSpacing.borderTopLR / 2}px ${props.styleSpacing.borderTopLR / 2}px ${props.styleSpacing.borderBottomLR / 2}px ${props.styleSpacing.borderBottomLR / 2}px`
};
});
const list = ref<Array<Eps.GoodsInfoEntity>>([]);
//
const watchedData = computed(() => ({
source: props.data.source,
attribute: props.data.attribute,
num: props.data.num,
ids: props.data.list.map((e) => e.id),
typeIds: props.data.type.map((e) => e.id)
}));
watch(
() => watchedData.value,
async (newValue) => {
let goods = [];
try {
goods = await service.goods.info.getGoodsFromFixture({
source: newValue.source,
attribute: newValue.attribute,
num: newValue.num,
ids: newValue.ids,
typeIds: newValue.typeIds
});
} catch (error) {
console.log(error);
}
list.value = goods;
},
{
deep: true,
immediate: true
}
);
</script>
<style lang="scss" scoped>
.fix-goods-list {
box-sizing: border-box;
overflow: hidden;
.empty {
height: 150px;
display: flex;
justify-content: center;
align-items: center;
span {
font-size: 26px;
color: var(--el-text-color-placeholder);
}
}
.inner {
padding-bottom: 12px;
box-sizing: border-box;
.grid-item {
background-color: #fff;
border-radius: 6px;
box-sizing: border-box;
overflow: hidden;
}
.is-shadow {
box-shadow: 2px 2px 8px 2px rgba(128, 128, 128, 0.3);
}
.icon {
width: 100%;
max-width: 100%;
vertical-align: middle;
}
.content {
max-width: 100%;
padding: 6px;
box-sizing: border-box;
overflow: hidden;
.voucher {
font-size: 14px;
margin-top: 6px;
.tips {
color: #e6a23c;
font-size: 12px;
}
.di {
margin-left: 10px;
color: #e6a23c;
}
}
.title {
box-sizing: border-box;
white-space: nowrap; /* 禁止换行 */
overflow: hidden; /* 隐藏溢出的内容 */
text-overflow: ellipsis; /* 超出部分显示省略号 */
max-width: 100%; /* 确保元素最大宽度不超过容器 */
font-size: 14px;
}
.price-sold {
margin-top: 6px;
font-size: 14px;
display: flex;
justify-content: space-between;
.price {
color: red;
}
.sold {
color: #a9a9a9;
font-size: 12px;
}
}
}
}
.is-mode-1 {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
}
.is-mode-2 {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 10px;
}
.is-mode-3 {
display: grid;
grid-template-columns: 1fr;
gap: 10px;
.grid-item {
background-color: #fff;
border-top-right-radius: 6px;
border-top-left-radius: 6px;
border-bottom-right-radius: 6px;
border-bottom-left-radius: 6px;
overflow: hidden;
display: grid;
grid-template-columns: 100px 1fr;
gap: 20px;
}
}
.is-mode-4 {
display: grid;
grid-template-columns: 1fr;
gap: 10px;
.grid-item {
display: flex;
flex-direction: column;
background-color: #fff;
border-top-right-radius: 6px;
border-top-left-radius: 6px;
border-bottom-right-radius: 6px;
border-bottom-left-radius: 6px;
overflow: hidden;
}
}
}
.is-bg {
position: relative;
z-index: 1; /* 确保 .is-bg 及其子元素位于 ::after 伪元素的上层 */
}
.is-bg::after {
content: "";
background-color: var(--background);
opacity: var(--opacity);
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
z-index: -1;
pointer-events: none;
}
</style>

View File

@ -0,0 +1,109 @@
<template>
<div class="fix-hot-image is-bg" :style="baseStyle">
<div class="inner">
<template v-if="data.pic">
<img :src="data.pic" class="img" />
</template>
<template v-else>
<div class="empty">
<span>图片热区</span>
</div>
</template>
</div>
</div>
</template>
<script lang="ts" name="fix-hot-image" setup>
import { ref, computed, type PropType } from "vue";
import { Form } from "../../types/form";
const props = defineProps({
data: {
type: Object as PropType<Form.HotImage>,
default: () => {
return {
pic: ""
};
}
},
styleSpacing: {
type: Object as PropType<Form.Spacing>,
default: () => {
return {
marginTop: 0,
marginBottom: 0,
marginLR: 0,
padding: 0,
borderTopLR: 0,
borderBottomLR: 0
};
}
},
styleColor: {
type: Object as PropType<Form.Color>,
default: () => {
return {
color: "#000",
backgroundColor: "#FFFFFF",
opacity: 1
};
}
}
});
const baseStyle = computed(() => {
return {
margin: `${props.styleSpacing.marginTop / 2}px ${props.styleSpacing.marginLR / 2}px ${props.styleSpacing.marginBottom / 2}px ${props.styleSpacing.marginLR / 2}px`,
color: props.styleColor.color,
padding: `${props.styleSpacing.padding / 2}px`,
"--opacity": props.styleColor.opacity,
"--background": props.styleColor.backgroundColor,
borderRadius: `${props.styleSpacing.borderTopLR / 2}px ${props.styleSpacing.borderTopLR / 2}px ${props.styleSpacing.borderBottomLR / 2}px ${props.styleSpacing.borderBottomLR / 2}px`
};
});
</script>
<style lang="scss" scoped>
.fix-hot-image {
box-sizing: border-box;
overflow: hidden;
.inner {
height: 100%;
flex: 1;
display: flex;
align-items: center;
justify-content: center;
.img {
width: 100%;
max-width: 100%;
height: auto;
}
.empty {
height: 120px;
display: flex;
justify-content: center;
align-items: center;
span {
font-size: 26px;
color: var(--el-text-color-placeholder);
}
}
}
}
.is-bg {
position: relative;
z-index: 1; /* 确保 .is-bg 及其子元素位于 ::after 伪元素的上层 */
}
.is-bg::after {
content: "";
background-color: var(--background);
opacity: var(--opacity);
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
z-index: -1;
pointer-events: none;
}
</style>

View File

@ -0,0 +1,86 @@
<template>
<div class="fix-line is-bg" :style="baseStyle">
<div class="inner" :style="innerStyle"></div>
</div>
</template>
<script lang="ts" name="fix-line" setup>
import { computed, type PropType } from "vue";
import { Form } from "../../types/form";
const props = defineProps({
mode: {
type: String,
default: "solid"
},
color: {
type: String,
default: "#f5f6fa"
},
height: {
type: Number,
default: 4
},
styleSpacing: {
type: Object as PropType<Form.Spacing>,
default: () => {
return {
marginTop: 0,
marginBottom: 0,
marginLR: 0,
padding: 0,
borderTopLR: 0,
borderBottomLR: 0
};
}
},
styleColor: {
type: Object as PropType<Form.Color>,
default: () => {
return {
color: "#000",
backgroundColor: "#FFFFFF",
opacity: 1
};
}
}
});
const baseStyle = computed(() => {
return {
margin: `${props.styleSpacing.marginTop / 2}px ${props.styleSpacing.marginLR / 2}px ${props.styleSpacing.marginBottom / 2}px ${props.styleSpacing.marginLR / 2}px`,
color: props.styleColor.color,
padding: `${props.styleSpacing.padding / 2}px`,
"--opacity": props.styleColor.opacity,
"--background": props.styleColor.backgroundColor,
borderRadius: `${props.styleSpacing.borderTopLR / 2}px ${props.styleSpacing.borderTopLR / 2}px ${props.styleSpacing.borderBottomLR / 2}px ${props.styleSpacing.borderBottomLR / 2}px`
};
});
const innerStyle = computed(() => {
return {
borderBottom: `${props.height / 2}px ${props.mode} ${props.color}`
};
});
</script>
<style lang="scss" scoped>
.fix-line {
box-sizing: border-box;
overflow: hidden;
}
.is-bg {
position: relative;
z-index: 1; /* 确保 .is-bg 及其子元素位于 ::after 伪元素的上层 */
}
.is-bg::after {
content: "";
background-color: var(--background);
opacity: var(--opacity);
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
z-index: -1;
pointer-events: none;
}
</style>

View File

@ -0,0 +1,180 @@
<template>
<div class="fix-list-menu is-bg" :style="baseStyle">
<div class="inner" v-if="list.length">
<div
class="item"
:class="[`${isBorder ? 'is-border' : ''}`]"
v-for="(item, index) in list"
:key="index"
>
<div class="left">
<img v-if="item.icon" :src="item.icon" class="icon" />
<div class="text" :style="{ color: item.color }">
{{ item.text }}
</div>
</div>
<div class="right">
<span class="text">{{ item.text2 }}</span>
<el-icon color="#a8abb2" style="vertical-align: middle"
><arrow-right
/></el-icon>
</div>
</div>
</div>
<div class="empty" v-else>
<span>列表导航</span>
</div>
</div>
</template>
<script lang="ts" name="fix-list-menu" setup>
import { ref, computed, type PropType } from "vue";
import { ArrowRight } from "@element-plus/icons-vue";
import { Form } from "../../types/form";
const props = defineProps({
list: {
type: Array as PropType<Form.Title[]>,
default: () => {
return [
{
text: "",
text2: "",
color: "",
icon: "",
link: {
name: "",
type: "",
appid: "",
page: ""
}
}
];
}
},
isBorder: {
type: Boolean,
default: false
},
styleSpacing: {
type: Object as PropType<Form.Spacing>,
default: () => {
return {
marginTop: 0,
marginBottom: 0,
marginLR: 0,
padding: 0,
borderTopLR: 0,
borderBottomLR: 0
};
}
},
styleColor: {
type: Object as PropType<Form.Color>,
default: () => {
return {
color: "#000",
backgroundColor: "#FFFFFF",
opacity: 1
};
}
}
});
const baseStyle = computed(() => {
return {
margin: `${props.styleSpacing.marginTop / 2}px ${props.styleSpacing.marginLR / 2}px ${props.styleSpacing.marginBottom / 2}px ${props.styleSpacing.marginLR / 2}px`,
color: props.styleColor.color,
padding: `${props.styleSpacing.padding / 2}px`,
"--opacity": props.styleColor.opacity,
"--background": props.styleColor.backgroundColor,
borderRadius: `${props.styleSpacing.borderTopLR / 2}px ${props.styleSpacing.borderTopLR / 2}px ${props.styleSpacing.borderBottomLR / 2}px ${props.styleSpacing.borderBottomLR / 2}px`
};
});
</script>
<style lang="scss" scoped>
.fix-list-menu {
box-sizing: border-box;
overflow: hidden;
.empty {
height: 150px;
display: flex;
justify-content: center;
align-items: center;
span {
font-size: 26px;
color: var(--el-text-color-placeholder);
}
}
.inner {
.is-border {
border-bottom: 1px solid var(--el-border-color-light);
}
.is-border:last-child {
border-bottom: none;
}
.item {
height: 40px;
padding: 6px;
box-sizing: border-box;
display: flex;
justify-content: space-between;
align-items: center;
.left {
flex: 1;
height: 100%;
display: flex;
justify-content: flex-start;
align-items: center;
margin-right: 20px;
.icon {
margin-right: 6px;
width: 25px;
height: 25px;
}
.text {
width: 220px;
height: 30px;
line-height: 30px;
font-size: 16px;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
}
.right {
width: 80px;
height: 100%;
display: flex;
justify-content: flex-end;
align-items: center;
.text {
color: var(--el-text-color-placeholder);
font-size: 12px;
margin-right: 4px;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
}
}
}
}
.is-bg {
position: relative;
z-index: 1; /* 确保 .is-bg 及其子元素位于 ::after 伪元素的上层 */
}
.is-bg::after {
content: "";
background-color: var(--background);
opacity: var(--opacity);
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
z-index: -1;
pointer-events: none;
}
</style>

View File

@ -0,0 +1,229 @@
<template>
<div class="fix-menus is-bg" :style="baseStyle">
<div class="inner" v-if="list.length">
<el-carousel :height="height" :autoplay="false">
<el-carousel-item v-for="(item, index) in tabs" :key="index">
<div
class="row"
:class="[`is-${style.rowNum}`]"
v-for="(row, r) in item"
:key="r"
>
<div class="item" v-for="(col, c) in row" :key="c">
<div
:class="[`is-${style.shape}`, `is-${col.mode}`]"
:style="{ backgroundColor: col.backgroundColor }"
class="icon-box"
>
<el-image :src="col.icon" fit="cover" class="icon">
<template #error>
<div class="image-slot">
<el-icon><icon-picture /></el-icon>
</div>
</template>
</el-image>
</div>
<span
v-if="col.mode == 'mode-1'"
class="text"
:style="{ color: col.color }"
>{{ col.text }}</span
>
</div>
</div>
</el-carousel-item>
</el-carousel>
</div>
<div class="empty" v-else>
<span>按钮组</span>
</div>
</div>
</template>
<script lang="ts" name="fix-menus" setup>
import { ref, computed, type PropType } from "vue";
import { Form } from "../../types/form";
import { Picture as IconPicture } from "@element-plus/icons-vue";
const props = defineProps({
style: {
type: Object,
default: () => {
return {
shape: "round",
pageNum: 1,
rowNum: 3
};
}
},
list: {
type: Array as PropType<Form.Menu[]>,
default: () => []
},
styleSpacing: {
type: Object as PropType<Form.Spacing>,
default: () => {
return {
marginTop: 0,
marginBottom: 0,
marginLR: 0,
padding: 0,
borderTopLR: 0,
borderBottomLR: 0
};
}
},
styleColor: {
type: Object as PropType<Form.Color>,
default: () => {
return {
color: "#000",
backgroundColor: "#FFFFFF",
opacity: 1
};
}
}
});
const baseStyle = computed(() => {
return {
margin: `${props.styleSpacing.marginTop / 2}px ${props.styleSpacing.marginLR / 2}px ${props.styleSpacing.marginBottom / 2}px ${props.styleSpacing.marginLR / 2}px`,
color: props.styleColor.color,
padding: `${props.styleSpacing.padding / 2}px`,
borderRadius: `${props.styleSpacing.borderTopLR / 2}px ${props.styleSpacing.borderTopLR / 2}px ${props.styleSpacing.borderBottomLR / 2}px ${props.styleSpacing.borderBottomLR / 2}px`,
"--opacity": props.styleColor.opacity,
"--background": props.styleColor.backgroundColor
};
});
const height = computed(() => {
return 75 * props.style.pageNum + "px";
});
const tabs = computed(() => {
return paginateArray(props.list, props.style.pageNum, props.style.rowNum);
});
// 1 3
function paginateArray(data: Form.Menu[], pageNum: number, rowNum: number) {
const result: any[] = [];
const totalItems = data.length;
let pageIndex = 0;
while (pageIndex < totalItems) {
const page: any[] = [];
for (let i = 0; i < pageNum && pageIndex < totalItems; i++) {
const row: any[] = [];
for (let j = 0; j < rowNum && pageIndex < totalItems; j++) {
row.push(data[pageIndex]);
pageIndex++;
}
page.push(row);
}
result.push(page);
}
return result;
}
</script>
<style lang="scss" scoped>
.fix-menus {
min-height: 75px;
box-sizing: border-box;
overflow: hidden;
.inner {
height: 100%;
padding: 10px;
box-sizing: border-box;
.row {
margin-bottom: 10px;
.item {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 70px;
.is-round {
border-radius: 20px;
}
.is-mode-2 {
width: 60px !important;
height: 60px !important;
}
.icon-box {
width: 40px;
height: 40px;
overflow: hidden;
.icon {
width: 100%;
height: 100%;
.image-slot {
height: 100%;
display: flex;
justify-content: center;
align-items: center;
.el-icon {
font-size: 30px;
color: var(--el-text-color-placeholder);
}
}
}
}
.text {
font-size: 12px;
margin-top: 6px;
}
}
}
.row:last-child {
margin-bottom: 0;
}
.is-3 {
display: grid;
grid-template-columns: repeat(3, 1fr); /* 每行3列每列宽度相等 */
gap: 10px; /* 间距 */
}
.is-4 {
display: grid;
grid-template-columns: repeat(4, 1fr); /* 每行3列每列宽度相等 */
gap: 10px; /* 间距 */
}
.is-5 {
display: grid;
grid-template-columns: repeat(5, 1fr); /* 每行3列每列宽度相等 */
gap: 10px; /* 间距 */
}
}
.empty {
height: 75px;
display: flex;
justify-content: center;
align-items: center;
span {
font-size: 26px;
color: var(--el-text-color-placeholder);
}
}
.is-mode-1 {
justify-content: flex-start;
}
.is-mode-2 {
justify-content: center;
}
}
.is-bg {
position: relative;
z-index: 1; /* 确保 .is-bg 及其子元素位于 ::after 伪元素的上层 */
}
.is-bg::after {
content: "";
background-color: var(--background);
opacity: var(--opacity);
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
z-index: -1;
pointer-events: none;
}
</style>

View File

@ -0,0 +1,108 @@
<template>
<div class="fix-picture is-bg" :style="baseStyle">
<div class="inner">
<template v-if="data.pic">
<img :src="data.pic" class="img" />
</template>
<template v-else>
<div class="empty">
<el-icon><icon-picture /></el-icon>
</div>
</template>
</div>
</div>
</template>
<script lang="ts" name="fix-picture" setup>
import { computed, type PropType } from "vue";
import { Picture as IconPicture } from "@element-plus/icons-vue";
import { Form } from "../../types/form";
const props = defineProps({
data: {
type: Object as PropType<Form.Picture>,
default: () => {
return {
pic: ""
};
}
},
styleSpacing: {
type: Object as PropType<Form.Spacing>,
default: () => {
return {
marginTop: 0,
marginBottom: 0,
marginLR: 0,
padding: 0,
borderTopLR: 0,
borderBottomLR: 0
};
}
},
styleColor: {
type: Object as PropType<Form.Color>,
default: () => {
return {
color: "#000",
backgroundColor: "#FFFFFF",
opacity: 1
};
}
}
});
const baseStyle = computed(() => {
return {
margin: `${props.styleSpacing.marginTop / 2}px ${props.styleSpacing.marginLR / 2}px ${props.styleSpacing.marginBottom / 2}px ${props.styleSpacing.marginLR / 2}px`,
color: props.styleColor.color,
padding: `${props.styleSpacing.padding / 2}px`,
"--opacity": props.styleColor.opacity,
"--background": props.styleColor.backgroundColor,
borderRadius: `${props.styleSpacing.borderTopLR / 2}px ${props.styleSpacing.borderTopLR / 2}px ${props.styleSpacing.borderBottomLR / 2}px ${props.styleSpacing.borderBottomLR / 2}px`
};
});
</script>
<style lang="scss" scoped>
.fix-picture {
box-sizing: border-box;
overflow: hidden;
.inner {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
.img {
width: 100%;
max-width: 100%;
}
.empty {
height: 120px;
display: flex;
align-items: center;
justify-content: center;
.el-icon {
font-size: 60px;
color: var(--el-text-color-placeholder);
}
}
}
}
.is-bg {
position: relative;
z-index: 1; /* 确保 .is-bg 及其子元素位于 ::after 伪元素的上层 */
}
.is-bg::after {
content: "";
background-color: var(--background);
opacity: var(--opacity);
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
z-index: -1;
pointer-events: none;
}
</style>

View File

@ -0,0 +1,318 @@
<template>
<div
class="fix-positioning"
:style="baseStyle"
:class="[{ 'is-float': float.open && banner.open }]"
>
<div :class="[`is-${mode}`]" class="header is-bg">
<div class="item">
<div v-if="mode == 'mode-1'">
<div>xxx,下午好</div>
<div class="mt-10 font-12 row-center">
<el-icon><Location /></el-icon>
<span class="mx-6">南村番禺店</span>
<span class="text-grey">距你 0.7km</span>
<span class="mx-6">更多门店</span>
<el-icon><ArrowDown /></el-icon>
</div>
</div>
<div v-if="mode == 'mode-2'">
<div class="row-center">
<el-icon><Location /></el-icon>
<span class="mx-6">南村1号店</span>
<el-icon><ArrowRight /></el-icon>
</div>
<div class="mt-10 font-12">
<span class="text-grey mx-6">距离你 0.7km</span>
</div>
</div>
<div v-if="mode == 'mode-3'">
<div class="row-center">
<el-icon><Location /></el-icon>
<span class="mx-6">广州番禺南村镇建奇大厦5号...</span>
</div>
<div class="mt-10 font-12">
<span class="text-grey"></span>
<span class="text-grey mx-6">南村1号店</span>
<span class="text-grey">提供</span>
</div>
</div>
</div>
</div>
<div class="banner" v-if="banner.open">
<el-carousel
:class="[{ 'is-float': float.open }]"
:style="{ height: `${props.banner.height / 2}px` }"
>
<el-carousel-item v-for="(item, index) in banner.list" :key="index">
<img
:src="item.pic"
:class="[{ 'is-float': float.open }]"
class="banner-item"
:style="{ height: `${props.banner.height / 2}px` }"
/>
</el-carousel-item>
</el-carousel>
<div class="float" v-if="float.open">
<div class="left" :style="leftStyle">
<template v-if="float.left.mode === 'mode-1'">
<el-image class="icon" fit="cover" :src="float.left.icon"></el-image>
<div class="mt-20 text">{{ float.left.text }}</div>
</template>
</div>
<div class="right" :style="rightStyle">
<template v-if="float.right.mode === 'mode-1'">
<el-image class="icon" fit="cover" :src="float.right.icon"></el-image>
<div class="mt-20 text">{{ float.right.text }}</div>
</template>
</div>
</div>
</div>
</div>
</template>
<script lang="ts" name="fix-positioning" setup>
import { ref, computed, type PropType } from "vue";
import { Location, ArrowDown, ArrowRight } from "@element-plus/icons-vue";
import { Form } from "../../types/form";
const props = defineProps({
mode: {
type: String,
default: "mode-1"
},
styleSpacing: {
type: Object as PropType<Form.Spacing>,
default: () => {
return {
marginTop: 0,
marginBottom: 0,
marginLR: 0,
padding: 0,
borderTopLR: 0,
borderBottomLR: 0
};
}
},
styleColor: {
type: Object as PropType<Form.Color>,
default: () => {
return {
color: "#000",
backgroundColor: "#FFFFFF",
opacity: 1
};
}
},
banner: {
type: Object as PropType<{ open: boolean; height: number; list: Form.Banner[] }>,
default: () => {
return {
open: false,
height: 750,
list: []
};
}
},
float: {
type: Object,
default: () => {
return {
open: false,
left: {
text: "",
mode: "mode-1",
icon: "",
color: "#000",
backgroundColor: "#FFFFFF",
link: {
name: "",
type: "",
appid: "",
page: ""
}
},
right: {
text: "",
mode: "mode-1",
icon: "",
color: "#000",
backgroundColor: "#FFFFFF",
link: {
name: "",
appid: "",
type: "",
page: ""
}
}
};
}
}
});
const baseStyle = computed(() => {
return {
margin: `${props.styleSpacing.marginTop / 2}px ${props.styleSpacing.marginLR / 2}px ${props.styleSpacing.marginBottom / 2}px ${props.styleSpacing.marginLR / 2}px`,
color: props.styleColor.color,
borderRadius: `${props.styleSpacing.borderTopLR / 2}px ${props.styleSpacing.borderTopLR / 2}px ${props.styleSpacing.borderBottomLR / 2}px ${props.styleSpacing.borderBottomLR / 2}px`,
padding: `${props.styleSpacing.padding / 2}px`,
minHeight: `${props.banner.open ? 250 : 100}px`,
"--opacity": props.styleColor.opacity,
"--background": props.styleColor.backgroundColor
};
});
const leftStyle = computed(() => {
let url = "";
if (props.float.left.mode === "mode-2") {
url = props.float.left.icon;
}
return {
backgroundColor: props.float.left.backgroundColor,
color: props.float.left.color,
backgroundImage: `url('${url}')`
};
});
const rightStyle = computed(() => {
let url = "";
if (props.float.right.mode === "mode-2") {
url = props.float.right.icon;
}
return {
backgroundColor: props.float.right.backgroundColor,
color: props.float.right.color,
backgroundImage: `url('${url}')`
};
});
</script>
<style lang="scss" scoped>
.fix-positioning {
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
min-height: 80px;
overflow: hidden;
position: relative;
box-sizing: border-box;
.header {
position: absolute;
top: 0;
width: 100%;
padding: 10px;
box-sizing: border-box;
min-height: 80px;
.is-bg {
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
z-index: 1;
}
.item {
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
z-index: 2;
}
}
.banner {
width: 100%;
height: 100%;
position: relative;
.banner-item {
width: 100%;
max-width: 100%;
height: 100%;
}
.float {
display: flex;
justify-content: center;
position: absolute;
bottom: 10px;
left: 10px;
right: 10px;
.left {
margin-right: 15px;
}
.right {
margin-left: 15px;
}
.left,
.right {
width: 120px;
height: 140px;
border-radius: 6px;
background-size: 100% 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
.text {
font-size: 16px;
font-family: PingFang SC;
font-weight: bold;
}
.icon {
width: 70px;
height: 70px;
}
}
}
}
.is-mode-4 {
display: none;
}
.mt-10 {
margin-top: 10px;
}
.mt-20 {
margin-top: 20px;
}
.mx-6 {
margin: 0 6px;
}
.text-grey {
color: var(--el-text-color-placeholder);
}
.font-12 {
font-size: 12px;
}
.row-center {
display: flex;
align-items: center;
}
.flex-1 {
flex: 1;
}
}
.is-bg {
position: relative;
z-index: 1; /* 确保 .is-bg 及其子元素位于 ::after 伪元素的上层 */
}
.is-bg::after {
content: "";
background-color: var(--background);
opacity: var(--opacity);
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
z-index: -1;
pointer-events: none;
}
.is-float {
min-height: 350px;
:deep(.el-carousel__item) {
overflow: inherit !important;
}
}
</style>

View File

@ -0,0 +1,101 @@
<template>
<div class="fix-rich-text is-bg" :style="baseStyle">
<template v-if="data">
<div v-html="data" class="inner"></div>
</template>
<template v-else>
<div class="empty">
<span>富文本</span>
</div>
</template>
</div>
</template>
<script lang="ts" name="fix-rich-text" setup>
import { ref, computed, type PropType } from "vue";
import { Form } from "../../types/form";
const props = defineProps({
data: {
type: String,
default: ""
},
styleSpacing: {
type: Object as PropType<Form.Spacing>,
default: () => {
return {
marginTop: 0,
marginBottom: 0,
marginLR: 0,
padding: 0,
borderTopLR: 0,
borderBottomLR: 0
};
}
},
styleColor: {
type: Object as PropType<Form.Color>,
default: () => {
return {
color: "#000",
backgroundColor: "#FFFFFF",
opacity: 1
};
}
}
});
const baseStyle = computed(() => {
return {
margin: `${props.styleSpacing.marginTop / 2}px ${props.styleSpacing.marginLR / 2}px ${props.styleSpacing.marginBottom / 2}px ${props.styleSpacing.marginLR / 2}px`,
color: props.styleColor.color,
padding: `${props.styleSpacing.padding / 2}px`,
"--opacity": props.styleColor.opacity,
"--background": props.styleColor.backgroundColor,
borderRadius: `${props.styleSpacing.borderTopLR / 2}px ${props.styleSpacing.borderTopLR / 2}px ${props.styleSpacing.borderBottomLR / 2}px ${props.styleSpacing.borderBottomLR / 2}px`
};
});
</script>
<style lang="scss" scoped>
.fix-rich-text {
box-sizing: border-box;
overflow: hidden;
.empty {
height: 150px;
display: flex;
justify-content: center;
align-items: center;
span {
font-size: 26px;
color: var(--el-text-color-placeholder);
}
}
.inner {
width: 100%;
}
:deep(img) {
width: 100%;
max-width: 100%;
height: auto; /* 保持图像的原始纵横比 */
display: block; /* 消除图像下方的默认间隙(如果是内联元素的话)*/
}
}
.is-bg {
position: relative;
z-index: 1; /* 确保 .is-bg 及其子元素位于 ::after 伪元素的上层 */
}
.is-bg::after {
content: "";
background-color: var(--background);
opacity: var(--opacity);
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
z-index: -1;
pointer-events: none;
}
</style>

View File

@ -0,0 +1,232 @@
<template>
<div class="fix-rubik-cube is-bg" :style="baseStyle">
<div
class="box"
:class="[`is-${data.mode}`]"
:style="{
gap: baseGap,
'--gap': baseGap
}"
>
<div
class="item"
v-for="(item, index) in data.list"
:key="index"
:class="[`is-${data.mode}-item-${index}`, { haveIcon: item.icon }]"
>
<span class="tips" v-if="!item.icon">{{ item.tips }}</span>
<el-image class="icon" :src="item.icon" fit="fill" v-else></el-image>
</div>
</div>
</div>
</template>
<script lang="ts" name="fix-rubik-cube" setup>
import { computed, type PropType } from "vue";
import { Form } from "../../types/form";
const props = defineProps({
data: {
type: Object as PropType<Form.RubikCube>,
default: () => {
return {
mode: "mode-1",
gap: 0,
list: [
{
icon: "",
tips: "宽度375px",
link: {
name: "",
type: "",
appid: "",
page: ""
}
},
{
icon: "",
tips: "宽度375px",
link: {
name: "",
type: "",
appid: "",
page: ""
}
}
]
};
}
},
styleSpacing: {
type: Object as PropType<Form.Spacing>,
default: () => {
return {
marginTop: 0,
marginBottom: 0,
marginLR: 0,
padding: 0,
borderTopLR: 0,
borderBottomLR: 0
};
}
},
styleColor: {
type: Object as PropType<Form.Color>,
default: () => {
return {
color: "#000",
backgroundColor: "#FFFFFF",
opacity: 1
};
}
}
});
const baseGap = computed(() => {
if (props.data.gap) {
return `${Math.floor(props.data.gap / 2)}px`;
}
return "0px";
});
const baseStyle = computed(() => {
return {
margin: `${props.styleSpacing.marginTop / 2}px ${props.styleSpacing.marginLR / 2}px ${props.styleSpacing.marginBottom / 2}px ${props.styleSpacing.marginLR / 2}px`,
color: props.styleColor.color,
padding: `${props.styleSpacing.padding / 2}px`,
"--opacity": props.styleColor.opacity,
"--background": props.styleColor.backgroundColor,
borderRadius: `${props.styleSpacing.borderTopLR / 2}px ${props.styleSpacing.borderTopLR / 2}px ${props.styleSpacing.borderBottomLR / 2}px ${props.styleSpacing.borderBottomLR / 2}px`
};
});
</script>
<style lang="scss" scoped>
.is-bg {
position: relative;
z-index: 1; /* 确保 .is-bg 及其子元素位于 ::after 伪元素的上层 */
}
.is-bg::after {
content: "";
background-color: var(--background);
opacity: var(--opacity);
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
z-index: -1;
pointer-events: none;
}
.box {
display: flex;
.item {
display: flex;
align-items: center;
justify-content: center;
min-height: 100px;
background-color: lightblue;
box-sizing: border-box;
cursor: pointer;
background-size: 100% 100%;
.icon {
width: 100%;
max-width: 100%;
border: 0;
vertical-align: middle;
}
.tips {
font-size: 14px;
text-align: center;
white-space: pre-line;
}
}
.haveIcon {
background-color: transparent !important;
}
}
.is-mode-1 {
display: grid;
grid-template-columns: repeat(2, 1fr);
.item {
&.is-mode-1-item-0 {
min-height: calc(150px - var(--gap) / 2);
background-color: lightgreen;
}
&.is-mode-1-item-1 {
min-height: calc(150px - var(--gap) / 2);
background-color: lightcoral;
}
}
}
.is-mode-2 {
display: grid;
grid-template-columns: repeat(3, 1fr);
.item {
&.is-mode-2-item-0 {
min-height: calc(100px - var(--gap) / 3);
}
&.is-mode-2-item-1 {
min-height: calc(100px - var(--gap) / 3);
background-color: lightgreen;
}
&.is-mode-2-item-2 {
min-height: calc(100px - var(--gap) / 3);
background-color: lightcoral;
}
}
}
.is-mode-3 {
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr 1fr;
.item {
&.is-mode-3-item-0 {
min-height: 300px;
grid-column: 1 / 2;
grid-row: 1 / 3;
background-color: lightblue;
}
&.is-mode-3-item-1 {
min-height: calc(150px - var(--gap) / 2);
grid-column: 2 / 3;
grid-row: 1 / 2;
background-color: lightgreen;
}
&.is-mode-3-item-2 {
min-height: calc(150px - var(--gap) / 2);
grid-column: 2 / 3;
grid-row: 2 / 3;
background-color: lightcoral;
}
}
}
.is-mode-4 {
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-rows: auto auto;
.item {
&.is-mode-4-item-0 {
min-height: calc(150px - var(--gap) / 2);
grid-column: 1 / 3;
grid-row: 1 / 2;
background-color: lightblue;
}
&.is-mode-4-item-1 {
min-height: calc(150px - var(--gap) / 2);
grid-column: 1 / 2;
grid-row: 2 / 3;
background-color: lightgreen;
}
&.is-mode-4-item-2 {
min-height: calc(150px - var(--gap) / 2);
grid-column: 2 / 3;
grid-row: 2 / 3;
background-color: lightcoral;
}
}
}
</style>

View File

@ -0,0 +1,120 @@
<template>
<div class="fix-search is-bg" :style="baseStyle">
<div class="inner" :class="[`is-${mode}`]" :style="innerStyle">
<el-icon><Search /></el-icon>
<span class="text">{{ placeholder }}</span>
</div>
</div>
</template>
<script lang="ts" name="fix-search" setup>
import { ref, computed, type PropType } from "vue";
import { Search } from "@element-plus/icons-vue";
import { Form } from "../../types/form";
const props = defineProps({
mode: {
type: String,
default: "mode-1"
},
backgroundColor: {
type: String,
default: "#f6f7fa"
},
placeholder: {
type: String,
default: "请输入关键字进行搜索"
},
styleSpacing: {
type: Object as PropType<Form.Spacing>,
default: () => {
return {
marginTop: 0,
marginBottom: 0,
marginLR: 0,
padding: 0,
borderTopLR: 0,
borderBottomLR: 0
};
}
},
styleColor: {
type: Object as PropType<Form.Color>,
default: () => {
return {
color: "#000",
backgroundColor: "#FFFFFF",
opacity: 1
};
}
}
});
const baseStyle = computed(() => {
return {
margin: `${props.styleSpacing.marginTop / 2}px ${props.styleSpacing.marginLR / 2}px ${props.styleSpacing.marginBottom / 2}px ${props.styleSpacing.marginLR / 2}px`,
color: props.styleColor.color,
padding: `${props.styleSpacing.padding / 2}px`,
"--opacity": props.styleColor.opacity,
"--background": props.styleColor.backgroundColor
};
});
const innerStyle = computed(() => {
return {
borderRadius: `${props.styleSpacing.borderTopLR / 2}px ${props.styleSpacing.borderTopLR / 2}px ${props.styleSpacing.borderBottomLR / 2}px ${props.styleSpacing.borderBottomLR / 2}px`,
backgroundColor: props.backgroundColor
};
});
</script>
<style lang="scss" scoped>
.fix-search {
height: 60px;
display: flex;
align-items: center;
padding: 20px;
box-sizing: border-box;
overflow: hidden;
.tips {
font-size: 12px;
color: #bfbfbf;
}
.inner {
height: 40px;
flex: 1;
display: flex;
align-items: center;
padding: 0 10px;
.text {
font-size: 14px;
margin-left: 6px;
}
}
.is-mode-1 {
justify-content: flex-start;
}
.is-mode-2 {
justify-content: center;
}
.is-mode-3 {
justify-content: flex-end;
}
}
.is-bg {
position: relative;
z-index: 1; /* 确保 .is-bg 及其子元素位于 ::after 伪元素的上层 */
}
.is-bg::after {
content: "";
background-color: var(--background);
opacity: var(--opacity);
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
z-index: -1;
pointer-events: none;
}
</style>

View File

@ -0,0 +1,150 @@
<template>
<div class="fix-suspension">
<div class="menu" :style="innerStyle" @click="toggleMenu">
<el-image lazy class="icon" :src="list[0].icon"></el-image>
</div>
<transition-group name="menu-slide" tag="div">
<div
v-for="(item, index) in visibleItems"
:key="index"
class="sub-menu"
:style="subMenuStyle(index)"
>
<el-image lazy class="icon" :src="item.icon">
<template #placeholder>
<span></span>
</template>
<template #error>
<span></span>
</template>
</el-image>
</div>
</transition-group>
</div>
</template>
<script lang="ts" name="fix-suspension" setup>
import { computed, ref, type PropType } from "vue";
import { Form } from "../../types/form";
const props = defineProps({
mode: {
type: String,
default: "left"
},
backgroundColor: {
type: String,
default: "rgba(0, 0, 0, 0.8)"
},
offsetBottom: {
type: Number,
default: 50
},
shadow: {
type: Boolean,
default: false
},
list: {
type: Array as PropType<Form.Suspension[]>,
default: () => [
{
icon: "https://tsb-yx.oss-cn-guangzhou.aliyuncs.com/app/mini/float-menu.png",
tips: "宽高128px",
link: {
name: "",
type: "",
appid: "",
page: ""
}
}
]
}
});
const innerStyle = computed(() => {
return {
bottom: `${props.offsetBottom / 2}px`,
[props.mode]: "10px",
background: props.backgroundColor,
boxShadow: props.shadow ? "0 4px 10px rgba(0, 0, 0, 0.3)" : undefined
};
});
const isMenuOpen = ref(false);
const toggleMenu = () => {
if (props.list.length > 1) {
isMenuOpen.value = !isMenuOpen.value;
}
};
//
const visibleItems = computed(() => {
return isMenuOpen.value ? props.list.slice(1) : [];
});
//
const subMenuStyle = (index: number) => {
const delay = 0.1; // ()
return {
transition: `transform 0.3s ease, opacity 0.3s ease`,
transform: isMenuOpen.value ? `translateY(-${(index + 1) * 50}px)` : "translateY(0)",
opacity: isMenuOpen.value ? 1 : 0,
bottom: `${props.offsetBottom / 2}px`,
[props.mode]: "10px",
background: props.backgroundColor,
boxShadow: props.shadow ? "0 4px 10px rgba(0, 0, 0, 0.3)" : undefined,
//
transitionDelay: isMenuOpen.value
? `${index * delay}s` //
: `${(visibleItems.value.length - index - 1) * delay}s` //
};
};
</script>
<style lang="scss" scoped>
.fix-suspension {
box-sizing: border-box;
.menu {
position: absolute;
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
z-index: 101;
cursor: pointer;
transition: all 0.3s ease;
}
.icon {
width: 25px;
height: 25px;
user-select: none;
}
.sub-menu {
position: absolute;
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
z-index: 100;
opacity: 0;
transition: all 0.3s ease;
}
:deep(.slide-fade-enter-active, .slide-fade-leave-active) {
transition: all 0.3s ease;
}
:deep(.slide-fade-enter, .slide-fade-leave-to) {
opacity: 0;
transform: translateY(0);
}
:deep(.el-image__placeholder) {
background: transparent;
}
}
</style>

View File

@ -0,0 +1,160 @@
<template>
<div class="fix-title is-bg" :style="baseStyle">
<div class="inner">
<div class="left">
<img v-if="left.icon" :src="left.icon" class="icon" />
<div class="text" :style="{ color: left.color }" :class="[`is-${mode}`]">
{{ left.text }}
</div>
</div>
<div class="right">
<span class="text" :style="{ color: right.color }">{{ right.text }}</span>
<img v-if="right.icon" :src="right.icon" class="icon" />
</div>
</div>
</div>
</template>
<script lang="ts" name="fix-title" setup>
import { ref, computed, type PropType } from "vue";
import { Form } from "../../types/form";
const props = defineProps({
mode: {
type: String,
default: "left"
},
left: {
type: Object as PropType<Form.Title>,
default: () => {
return {
text: "标题内容",
color: "#000",
icon: "",
link: {
name: "",
type: "",
appid: "",
page: ""
}
};
}
},
right: {
type: Object as PropType<Form.Title>,
default: () => {
return {
text: "查看",
color: "#a8abb2",
icon: "",
link: {
name: "",
type: "",
appid: "",
page: ""
}
};
}
},
styleSpacing: {
type: Object as PropType<Form.Spacing>,
default: () => {
return {
marginTop: 0,
marginBottom: 0,
marginLR: 0,
padding: 0,
borderTopLR: 0,
borderBottomLR: 0
};
}
},
styleColor: {
type: Object as PropType<Form.Color>,
default: () => {
return {
color: "#000",
backgroundColor: "#FFFFFF",
opacity: 1
};
}
}
});
const baseStyle = computed(() => {
return {
margin: `${props.styleSpacing.marginTop / 2}px ${props.styleSpacing.marginLR / 2}px ${props.styleSpacing.marginBottom / 2}px ${props.styleSpacing.marginLR / 2}px`,
color: props.styleColor.color,
padding: `${props.styleSpacing.padding / 2}px`,
"--opacity": props.styleColor.opacity,
"--background": props.styleColor.backgroundColor,
borderRadius: `${props.styleSpacing.borderTopLR / 2}px ${props.styleSpacing.borderTopLR / 2}px ${props.styleSpacing.borderBottomLR / 2}px ${props.styleSpacing.borderBottomLR / 2}px`
};
});
</script>
<style lang="scss" scoped>
.fix-title {
box-sizing: border-box;
overflow: hidden;
.inner {
height: 40px;
flex: 1;
display: flex;
align-items: center;
justify-content: space-around;
padding: 10px;
.left {
display: flex;
align-items: center;
flex: 1;
.icon {
margin-right: 10px;
width: 30px;
height: 30px;
}
.text {
flex: 1;
font-size: 16px;
font-weight: bold;
}
.is-left {
text-align: left;
}
.is-center {
text-align: center;
}
}
.right {
display: flex;
justify-content: flex-end;
align-items: center;
.icon {
margin-left: 10px;
width: 20px;
height: 20px;
}
.text {
font-size: 12px;
}
}
}
}
.is-bg {
position: relative;
z-index: 1; /* 确保 .is-bg 及其子元素位于 ::after 伪元素的上层 */
}
.is-bg::after {
content: "";
background-color: var(--background);
opacity: var(--opacity);
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
z-index: -1;
pointer-events: none;
}
</style>

View File

@ -0,0 +1,117 @@
<template>
<div class="fix-video is-bg" :style="baseStyle">
<div class="inner" v-if="video">
<template v-if="mode == 'horizontal'">
<video :poster="cover" width="360" height="206" controls>
<source :src="video" type="video/mp4" />
</video>
</template>
<template v-else>
<video :poster="cover" width="360" height="612" controls>
<source :src="video" type="video/mp4" />
</video>
</template>
</div>
<template v-else>
<div class="empty">
<span>视频播放器</span>
</div>
</template>
</div>
</template>
<script lang="ts" name="fix-video" setup>
import { computed, type PropType } from "vue";
import { Form } from "../../types/form";
const props = defineProps({
mode: {
type: String,
default: "horizontal"
},
video: {
type: String,
default: ""
},
cover: {
type: String,
default: ""
},
type: {
type: String,
default: "local"
},
styleSpacing: {
type: Object as PropType<Form.Spacing>,
default: () => {
return {
marginTop: 0,
marginBottom: 0,
marginLR: 0,
padding: 0,
borderTopLR: 0,
borderBottomLR: 0
};
}
},
styleColor: {
type: Object as PropType<Form.Color>,
default: () => {
return {
color: "#000",
backgroundColor: "#FFFFFF",
opacity: 1
};
}
}
});
const baseStyle = computed(() => {
return {
margin: `${props.styleSpacing.marginTop / 2}px ${props.styleSpacing.marginLR / 2}px ${props.styleSpacing.marginBottom / 2}px ${props.styleSpacing.marginLR / 2}px`,
color: props.styleColor.color,
padding: `${props.styleSpacing.padding / 2}px`,
"--opacity": props.styleColor.opacity,
"--background": props.styleColor.backgroundColor,
borderRadius: `${props.styleSpacing.borderTopLR / 2}px ${props.styleSpacing.borderTopLR / 2}px ${props.styleSpacing.borderBottomLR / 2}px ${props.styleSpacing.borderBottomLR / 2}px`
};
});
</script>
<style lang="scss" scoped>
.fix-video {
box-sizing: border-box;
overflow: hidden;
.empty {
height: 100px;
display: flex;
justify-content: center;
align-items: center;
span {
font-size: 26px;
color: var(--el-text-color-placeholder);
}
}
}
.is-bg {
position: relative;
z-index: 1; /* 确保 .is-bg 及其子元素位于 ::after 伪元素的上层 */
}
.is-bg::after {
content: "";
background-color: var(--background);
opacity: var(--opacity);
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
z-index: -1;
pointer-events: none;
}
video {
margin: 0;
padding: 0;
display: block; /* 避免 inline 元素造成的间隙 */
object-fit: cover;
}
</style>

View File

@ -0,0 +1,46 @@
<template>
<div class="fix-wechat" :style="baseStyle">
<img
src="https://tsb-yx.oss-cn-guangzhou.aliyuncs.com/leadshop/pageicon/wechat_bg.png"
width="100%"
height="100%"
/>
</div>
</template>
<script lang="ts" name="fix-wechat" setup>
import { computed, type PropType } from "vue";
import { Form } from "../../types/form";
const props = defineProps({
styleSpacing: {
type: Object as PropType<Form.Spacing>,
default: () => {
return {
marginTop: 0,
marginBottom: 0,
marginLR: 0,
padding: 0,
borderTopLR: 0,
borderBottomLR: 0
};
}
}
});
const baseStyle = computed(() => {
return {
margin: `${props.styleSpacing.marginTop / 2}px ${props.styleSpacing.marginLR / 2}px ${props.styleSpacing.marginBottom / 2}px ${props.styleSpacing.marginLR / 2}px`,
padding: `${props.styleSpacing.padding / 2}px`,
borderRadius: `${props.styleSpacing.borderTopLR / 2}px ${props.styleSpacing.borderTopLR / 2}px ${props.styleSpacing.borderBottomLR / 2}px ${props.styleSpacing.borderBottomLR / 2}px`
};
});
</script>
<style lang="scss" scoped>
.fix-wechat {
box-sizing: border-box;
overflow: hidden;
height: 85px;
width: 100%;
}
</style>

View File

@ -0,0 +1,505 @@
<template>
<el-scrollbar>
<div class="dp-wrap">
<dp-lump :ref="setRefs('lump')" />
<div class="dp-device">
<div class="tool">
<slot name="tool"></slot>
</div>
<div
class="device"
:style="{
backgroundColor: body.backgroundColor,
backgroundImage: 'url(' + body.backgroundImage + ')'
}"
>
<div
:class="[
{
'header-fixed': header.component?.props?.fixed
}
]"
></div>
<div
:style="{
backgroundColor: header.component?.props?.backgroundColor,
color: header.component?.props?.color
}"
class="nav"
:class="[
{
'nav-border': header.component?.props?.border,
'nav-fixed': header.component?.props?.fixed
}
]"
@click="toDet(header)"
>
{{ header.component?.props?.title }}
</div>
<!-- 悬浮按钮 -->
<template v-if="suspensionData?.name">
<fix-suspension v-bind="suspensionData.component.props" />
</template>
<el-scrollbar class="scrollbar" :ref="setRefs('scrollbar')">
<draggable
v-model="pageData"
class="list"
tag="div"
item-key="id"
:group="{
name: 'A',
animation: 300,
ghostClass: 'Ghost',
dragClass: 'Drag',
draggable: '.is-drag'
}"
>
<template #item="{ element: item, index }">
<div
class="item"
:class="{
active: pageActive == item.id
}"
@click="toDet(item)"
>
<el-icon
class="close"
@click.stop="remove(index)"
v-show="pageActive == item.id"
>
<close-bold />
</el-icon>
<component
:is="item.component.name"
:data="item"
:key="item.id"
v-bind="item.component.props"
/>
</div>
</template>
<template #footer>
<div class="tips">点击或者拖动组件添加</div>
</template>
</draggable>
</el-scrollbar>
</div>
</div>
<dp-config />
</div>
</el-scrollbar>
</template>
<script lang="ts" setup>
import { onMounted, provide, nextTick, ref } from "vue";
import Draggable from "vuedraggable/src/vuedraggable";
import { CloseBold } from "@element-plus/icons-vue";
import { storage, useCool } from "/@/cool";
import DpConfig from "./config.vue";
import DpLump from "./lump.vue";
import { Dp } from "../types";
import { useData } from "../data";
//
interface BodyData {
backgroundColor: string;
backgroundImage: string;
}
// 使 Cool
const { mitt, refs, setRefs, service } = useCool();
const { Header } = useData();
//
const pageData = ref<Dp.Item[]>([]);
const pageActive = ref<string>("");
const suspensionData = ref<Dp.Item | null>(null);
const header = ref<Dp.Item>(Header);
const body = ref<BodyData>({ backgroundColor: "#f7f8fa", backgroundImage: "" });
// BODY
const setBody = (data: BodyData) => (body.value = data);
//
const setHeader = (data: Dp.Item) => {
data.config = Header.config;
header.value = data;
};
//
const handleSuspension = (data: Dp.Item) => {
if (suspensionData.value === null) {
suspensionData.value = data;
}
toDet(suspensionData.value);
};
const setSuspension = (data: Dp.Item) => {
data.config = refs.lump.getConfig(data.name);
suspensionData.value = data;
};
//
const setFormList = (list: Dp.Item[]) => {
pageData.value = list.map((e) => ({ ...e, config: refs.lump.getConfig(e.name) }));
toDet(pageData.value[0]);
};
//
const toDet = (item?: Dp.Item) => {
if (!item) return;
pageActive.value = item.id;
mitt.emit("dp.setConfig", {
options: item,
cb: (obj: any) => {
if (item.name === "header") {
header.value.component.props = obj;
} else if (item.name === "suspension") {
suspensionData.value!.component.props = obj;
} else {
const index = pageData.value.findIndex((e) => e.id === item.id);
if (index !== -1) {
Object.assign(pageData.value[index].component.props, obj);
}
}
}
});
};
//
const add = (data: Dp.Item) => {
if (data.name === "suspension") {
handleSuspension(data);
} else {
pageData.value.push(data);
toDet(data);
}
};
//
const clearConfig = (id?: string) => {
if (pageActive.value === id || !id) {
mitt.emit("dp.clearConfig");
}
};
//
const remove = (index: number) => {
if (index >= 0 && index < pageData.value.length) {
clearConfig(pageData.value[index].id);
pageData.value.splice(index, 1);
if (pageData.value.length) {
toDet(pageData.value[Math.max(index - 1, 0)]);
}
}
};
// ID
const removeBy = ({ id, index }: { id?: string; index?: number }) => {
if (id) {
index = pageData.value.findIndex((e) => e.id === id);
}
remove(index ?? -1);
};
//
const removeSuspension = () => (suspensionData.value = null);
//
const clear = () => {
clearConfig();
pageData.value = [];
};
//
const setActive = (id: string) => {
const item = pageData.value.find((e) => e.id === id);
if (item) {
toDet(item);
}
};
// props -
function removeHyphenatedPropertiesFromProps(props: Record<string, any>): Record<string, any> {
if (props === null || typeof props !== "object") {
return props;
}
// `-`
const result: Record<string, any> = {};
for (const [key, value] of Object.entries(props)) {
// `-`
if (!key.includes("-")) {
result[key] = value;
}
}
return result;
}
//
const getData = () => {
const deep = (arr: Dp.Item[]): any[] => {
const data = arr.map((e) => {
e.component.props = removeHyphenatedPropertiesFromProps(e.component.props);
return {
id: e.id,
name: e.name,
isOnly: e.isOnly || false,
label: e.component?.props?.label,
component: e.component
};
});
if (suspensionData.value) {
//
const suspension = {
id: suspensionData.value.id,
name: suspensionData.value.name,
label: suspensionData.value.label,
isOnly: true,
component: suspensionData.value.component
};
data.push(suspension);
}
const headerData = {
id: header.value.id,
name: header.value.name,
label: header.value.label,
isOnly: true,
component: header.value.component
};
data.unshift(headerData);
return data;
};
return deep(pageData.value);
};
//
const hasTemp = (name: string) => pageData.value.some((e) => e.name === name && e.isOnly);
//
const hasSuspension = () => {
return !(suspensionData.value === null);
};
// 稿
const saveDraft = () => {
storage.set(
"design.pageCode",
pageData.value.map((e) => ({ ...e, config: undefined }))
);
};
// 稿
const getDraft = () => {
const list: Dp.Item[] = storage.get("design.pageCode") || [];
pageData.value = list.map((e) => ({ ...e, config: refs.lump.getConfig(e.name) }));
toDet(pageData.value[0]);
};
//
const scrollToBottom = () =>
nextTick(() => {
if (refs.scrollbar) {
refs.scrollbar.scrollTo(0, 9999);
}
});
//
mitt.on("dp.setActive", setActive);
// API
const dp = {
pageData,
pageActive,
getData,
toDet,
setActive,
add,
remove,
removeSuspension,
removeBy,
clear,
hasTemp,
hasSuspension,
clearConfig,
saveDraft,
scrollToBottom,
setHeader,
setSuspension,
setBody,
setFormList
};
provide("dp", dp);
defineExpose(dp);
onMounted(() => {
// 稿
// getDraft();
});
</script>
<style lang="scss">
.Ghost {
opacity: 0.7;
}
</style>
<style lang="scss">
.dp-wrap {
display: flex;
height: 100%;
min-height: 700px;
background-color: #edf0f3;
padding: 10px;
box-sizing: border-box;
color: #000;
.dp-device {
display: flex;
justify-content: center;
align-items: center;
flex: 1;
position: relative;
.tool {
position: absolute;
right: 20px;
top: 20px;
}
.header-fixed {
height: 54px;
}
.device {
position: relative;
height: 667px;
width: 360px;
overflow: hidden;
border-radius: 20px;
background-color: #f7f8fa;
background-size: contain;
background-position: top center;
background-repeat: no-repeat;
.nav {
display: flex;
align-items: center;
justify-content: center;
height: 54px;
font-size: 18px;
background-color: #fff;
cursor: pointer;
}
.nav-border {
border-bottom: 1px solid #eee;
}
.nav-fixed {
position: absolute;
top: 0;
left: 0;
right: 0;
z-index: 999;
}
.scrollbar {
height: 612px;
width: 100%;
box-sizing: border-box;
.list {
height: 100%;
padding-bottom: 20px;
box-sizing: border-box;
.item {
position: relative;
box-sizing: border-box;
cursor: pointer;
width: 100%;
&:last-child {
margin-bottom: 0;
}
.close {
position: absolute;
right: 0;
top: 0;
height: 14px;
width: 14px;
color: #fff;
z-index: 9;
background-color: var(--color-primary);
padding: 1px;
cursor: pointer;
&:hover {
background-color: red;
}
}
&.sortable-ghost {
display: flex;
align-items: center;
min-height: 40px;
width: 100%;
padding: 0 18px;
box-sizing: border-box;
border: 1px dashed currentColor;
border-radius: 3px;
cursor: pointer;
color: #bfbfbf;
background-color: #fff;
opacity: 0.8;
img {
height: 20px;
width: 20px;
margin-right: 14px;
}
span {
font-size: 16px;
}
[class^="lump-"] {
width: 100%;
color: #fff;
.placeholder {
color: #fff;
}
}
}
&.active {
&::after {
display: block;
content: "";
position: absolute;
left: 0;
top: 0;
z-index: 99;
height: 100%;
width: 100%;
border: 2px solid var(--color-primary);
box-sizing: border-box;
pointer-events: none;
}
}
}
}
}
.tips {
text-align: center;
font-size: 14px;
padding: 10px 0 20px 0;
color: #999;
}
}
}
}
</style>

View File

@ -0,0 +1,212 @@
<template>
<div class="dp-lump">
<el-scrollbar>
<div class="group" v-for="(item, index) in tab.list" :key="index">
<p class="label">{{ item.label }}</p>
<draggable
v-model="item.children"
class="list"
item-key="label"
:sort="false"
:group="{
name: 'A',
pull: 'clone',
put: false
}"
:clone="onClone"
@end="onEnd"
>
<template #item="{ element: item }">
<div class="item" @click="add(item)">
<img :src="icons[item.name]" />
<span>{{ item.label }}</span>
</div>
</template>
</draggable>
</div>
</el-scrollbar>
</div>
</template>
<script lang="tsx" setup>
import { ElMessage } from "element-plus";
import { cloneDeep } from "lodash-es";
import { reactive } from "vue";
import Draggable from "vuedraggable/src/vuedraggable";
import { useDp } from "../hooks";
import { Dp } from "../types";
import { useCool } from "/@/cool";
import { uuid } from "/@/cool/utils";
import { useData } from "../data";
const { mitt } = useCool();
const { dp } = useDp();
const { Base, Marketing, Ability, Other } = useData();
//
const files: any = import.meta.glob("/src/modules/fixtures/static/icon/*", {
eager: true
});
const icons = reactive<any>({});
for (const i in files) {
icons[i.replace("/src/modules/fixtures/static/icon/", "").replace(".png", "")] =
files[i].default;
}
//
const tab = reactive<{ list: { label: string; children: Dp.Item[] }[] }>({
list: [
{
label: "基础组件",
children: Base
},
{
label: "营销组件",
children: Marketing
},
{
label: "功能组件",
children: Ability
},
{
label: "其他组件",
children: Other
}
]
});
//
function parse(item: Dp.Item) {
function next(data: Dp.Item, options: any = {}) {
const { autoInc = true } = options;
const d: Dp.Item = cloneDeep({
...data,
id: uuid()
});
if (autoInc) {
//
data._index = data._index !== undefined ? data._index + 1 : 0;
if (data._index > 0) {
d.label += data._index;
}
}
return d;
}
let v: any = null;
if (item.isOnly) {
if (dp.hasTemp(item.name)) {
ElMessage.warning("该组件只可以添加一个");
return undefined;
}
v = next(item);
} else {
v = next(item);
}
return v;
}
//
function getConfig(name: string) {
let d: any = null;
tab.list.find((e) => {
d = e.children?.find((a) => a.name == name);
return !!d;
});
return d ? parse(d)?.config : {};
}
//
let _d: any = null;
function onClone(item: any) {
if (item.name === "suspension" && dp.hasSuspension()) {
return;
}
//
_d = parse(item);
return _d;
}
function add(item: any) {
dp.add(parse(item));
dp.scrollToBottom();
}
function onEnd() {
if (_d) {
mitt.emit("dp.setActive", _d.id);
}
}
defineExpose({
getConfig
});
</script>
<style lang="scss" scoped>
.dp-lump {
width: 350px;
background-color: #fff;
border-radius: 5px;
.group {
.label {
font-size: 15px;
padding: 15px 20px;
}
.list {
display: flex;
flex-wrap: wrap;
padding: 0 8px;
.item {
display: flex;
align-items: center;
height: 40px;
width: calc(50% - 16px);
margin: 0 8px 6px 8px;
padding: 0 8px 0 18px;
box-sizing: border-box;
border: 1px dashed currentColor;
border-radius: 3px;
cursor: pointer;
color: #bfbfbf;
img {
height: 20px;
width: 20px;
margin-right: 14px;
}
span {
font-size: 15px;
}
&:hover {
border-color: var(--color-primary);
}
}
}
&:last-child {
.list {
padding-bottom: 20px;
}
}
}
}
</style>

View File

@ -0,0 +1,23 @@
import type { ModuleConfig } from "/@/cool";
export default (): ModuleConfig => {
// 扫描文件
const files: any = import.meta.glob(
"./components/{assembly,fix}/*",
{
eager: true
}
);
return {
order: 22,
label: "页面装修",
description: "自定义布局页面",
author: "柠檬不甜",
version: "1.0.4",
updateTime: "2024-10-25",
components: Object.values(files),
onLoad: () => {
console.log(files);
}
};
};

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,11 @@
import { Header, Base, Marketing, Ability, Other } from './base'
export function useData() {
return {
Header,
Base,
Marketing,
Ability,
Other
};
}

View File

@ -0,0 +1,8 @@
import { inject } from "vue";
import { Dp } from "../types";
export function useDp() {
const dp = inject("dp") as Dp.Provide;
return { dp };
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -0,0 +1,159 @@
export class DraggableResizableClass {
private container: HTMLElement;
private items: any[];
private containerRect: DOMRect;
constructor(container: HTMLElement, items: any[]) {
this.container = container;
this.items = items;
this.containerRect = this.container.getBoundingClientRect();
this.init();
}
private init() {
this.updateListeners();
}
public updateListeners() {
const elements = this.container.querySelectorAll(".draggable-resizable");
elements.forEach((el, index) => {
const handles = (el as HTMLElement).querySelectorAll(".resize-handle");
handles.forEach(handle => {
handle.addEventListener("mousedown", (e) => this.onResizeMouseDown(e, index, handle as HTMLElement));
});
(el as HTMLElement).addEventListener("mousedown", (e) => this.onMouseDown(e, index));
});
}
private onMouseDown(event: MouseEvent, index: number) {
if ((event.target as HTMLElement).classList.contains("resize-handle")) {
return;
}
const item = this.items[index];
const startX = event.clientX;
const startY = event.clientY;
const startLeft = item.x;
const startTop = item.y;
const onMouseMove = (e: MouseEvent) => {
const dx = e.clientX - startX;
const dy = e.clientY - startY;
item.x = Math.min(
Math.max(startLeft + dx, 0),
this.containerRect.width - item.w
);
item.y = Math.min(
Math.max(startTop + dy, 0),
this.containerRect.height - item.h
);
this.updateStyles(index);
};
const onMouseUp = () => {
document.removeEventListener("mousemove", onMouseMove);
document.removeEventListener("mouseup", onMouseUp);
};
document.addEventListener("mousemove", onMouseMove);
document.addEventListener("mouseup", onMouseUp);
}
private onResizeMouseDown(event: MouseEvent, index: number, handle: HTMLElement) {
event.stopPropagation(); // Prevent triggering drag event
const item = this.items[index];
const startX = event.clientX;
const startY = event.clientY;
const startWidth = item.w;
const startHeight = item.h;
const startLeft = item.x;
const startTop = item.y;
const handleClass = handle.classList[1]; // Get the class of the handle
const onMouseMove = (e: MouseEvent) => {
let newWidth = startWidth;
let newHeight = startHeight;
let newLeft = startLeft;
let newTop = startTop;
switch (handleClass) {
case 'top-left':
newWidth = Math.max(startWidth - (e.clientX - startX), 60);
newHeight = Math.max(startHeight - (e.clientY - startY), 30);
newLeft = startLeft + (startWidth - newWidth);
newTop = startTop + (startHeight - newHeight);
break;
case 'top-right':
newWidth = Math.max(startWidth + (e.clientX - startX), 60);
newHeight = Math.max(startHeight - (e.clientY - startY), 30);
newTop = startTop + (startHeight - newHeight);
break;
case 'bottom-left':
newWidth = Math.max(startWidth - (e.clientX - startX), 60);
newHeight = Math.max(startHeight + (e.clientY - startY), 30);
newLeft = startLeft + (startWidth - newWidth);
break;
case 'bottom-right':
newWidth = Math.max(startWidth + (e.clientX - startX), 60);
newHeight = Math.max(startHeight + (e.clientY - startY), 30);
break;
case 'top':
newHeight = Math.max(startHeight - (e.clientY - startY), 30);
newTop = startTop + (startHeight - newHeight);
break;
case 'right':
newWidth = Math.max(startWidth + (e.clientX - startX), 60);
break;
case 'bottom':
newHeight = Math.max(startHeight + (e.clientY - startY), 30);
break;
case 'left':
newWidth = Math.max(startWidth - (e.clientX - startX), 60);
newLeft = startLeft + (startWidth - newWidth);
break;
}
// Ensure resizing does not go outside container boundaries
newWidth = Math.min(Math.max(newWidth, 60), this.containerRect.width - newLeft);
newHeight = Math.min(Math.max(newHeight, 30), this.containerRect.height - newTop);
newLeft = Math.max(Math.min(newLeft, this.containerRect.width - newWidth), 0);
newTop = Math.max(Math.min(newTop, this.containerRect.height - newHeight), 0);
item.w = newWidth;
item.h = newHeight;
item.x = newLeft;
item.y = newTop;
this.updateStyles(index);
};
const onMouseUp = () => {
document.removeEventListener("mousemove", onMouseMove);
document.removeEventListener("mouseup", onMouseUp);
};
document.addEventListener("mousemove", onMouseMove);
document.addEventListener("mouseup", onMouseUp);
}
private updateStyles(index: number) {
const item = this.items[index];
const element = this.container.querySelectorAll(".draggable-resizable")[index] as HTMLElement;
element.style.left = `${item.x}px`;
element.style.top = `${item.y}px`;
element.style.width = `${item.w}px`;
element.style.height = `${item.h}px`;
}
}

109
src/modules/fixtures/types/form.d.ts vendored Normal file
View File

@ -0,0 +1,109 @@
export declare namespace Form {
interface Menu {
text: string,
useText: boolean,
mode: string,
link: Link,
icon: string,
color: string,
backgroundColor: string
}
interface Link {
page: string,
appid: string,
type: string,
name: string
}
interface Banner {
pic: string,
link: Link
}
interface Spacing {
marginTop: number,
marginBottom: number,
marginLR: number,
padding: number,
borderTopLR: number,
borderBottomLR: number
}
interface Color {
color: string,
backgroundColor: string,
opacity: number
}
interface Picture {
pic: string,
link: Link
}
interface Title {
text: string,
text2?: string,
color: string,
icon: string,
link: Link
}
interface Hot {
x: number,
y: number,
w: number,
h: number,
relativeX: number,
relativeY: number,
relativeW: number,
relativeH: number,
index: number,
link: Link
}
interface HotImage {
pic: string,
link: Link,
width: number,
height: number,
attr: Hot[]
}
interface RubikCubeMode {
type: string,
label: string,
list: {
icon: string,
tips: string,
link: Link
}[]
}
interface RubikCube {
mode: string,
gap: number,
list: {
icon: string,
tips: string,
link: Link
}[]
}
interface Goods {
mode: string,
source: string,
gap: number,
num: number,
attribute: number,
isVoucher: boolean,
isShadow: boolean,
type: { name: string, pic?: string, id: number }[],
list: {
mainPic: string,
title: string,
price: number,
sold: number,
attribute?: number,
id: number,
}[]
}
interface Suspension {
icon: string;
tips: string,
link: Link
}
}

42
src/modules/fixtures/types/index.d.ts vendored Normal file
View File

@ -0,0 +1,42 @@
export declare namespace Dp {
interface Item {
label: string;
name: string;
isOnly?: boolean;
component: {
name?: string;
props: {
children?: any[];
label?: string;
[key: string]: any;
};
};
config?: {
tips?: string;
items?: ClForm.Item[];
[key: string]: any;
};
[key: string]: any;
}
interface Provide {
pageActive: string;
pageData: any[];
getData(): any[];
toDet(item: any): void;
setActive(id: string): void;
add(data: any): void;
removeSuspension(): void;
remove(index: number): void;
removeBy(options: { id?: string; index?: number }): void;
clear(): boolean;
hasTemp(name: string): boolean;
hasSuspension(): boolean;
clearConfig(id?: string): void;
saveDraft(): void;
scrollToBottom(): void;
setHeader(): void;
setSuspension(): void;
setFormList(): void;
}
}

View File

@ -0,0 +1,211 @@
<template>
<div class="form">
<div class="container">
<dp :ref="setRefs('dp')">
<template #tool>
<div class="tools">
<el-link
type="primary"
href="https://www.gaoding.com/editor/ps#/"
target="_blank"
>在线ps工具</el-link
>
<el-link
type="primary"
href="https://editor.ibaotu.com/complex/tempcenter"
target="_blank"
>图片资源库</el-link
>
<el-button link :icon="Position" type="primary" @click="preview"
>小程序预览页面</el-button
>
</div>
</template>
</dp>
</div>
<div class="footer">
<el-button @click="clear">清空</el-button>
<el-button type="success" @click="save">保存页面</el-button>
<el-button type="primary" @click="send">发布上线</el-button>
<el-button type="info" @click="create">预览代码</el-button>
</div>
<cl-editor-preview title="代码预览" name="monaco" :ref="setRefs('preview')" />
<cl-dialog width="350px" title="小程序页面预览码" v-model="options.visible">
<div class="preview-box">
<el-image fit="cover" style="width: 200px; height: 200px" :src="options.url" />
<div style="padding: 6px; margin-top: 10px">
<span>微信扫码预览该页面</span>
</div>
</div>
</cl-dialog>
</div>
</template>
<script lang="ts" setup>
import { useCool } from "/@/cool";
import { onMounted, reactive } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import { Position } from "@element-plus/icons-vue";
import Dp from "../components/index.vue";
const { refs, setRefs, service, route } = useCool();
//
const options = reactive<any>({
visible: false,
loading: false,
url: ""
});
//
function preview() {
if (options.loading) return;
options.loading = true;
const data = refs.dp.getData();
service.fixtures.mould
.getFixturesPreviewCode({ id: Number(route.query.id), form: data })
.then((res: any) => {
options.url = res;
options.visible = true;
})
.catch((e) => {
ElMessage.error(e.message);
})
.finally(() => {
options.loading = false;
});
}
//
function save() {
const data = refs.dp.getData();
service.fixtures.mould
.update({ form: data, id: Number(route.query.id) })
.then(() => {
ElMessage.success("保存页面成功");
})
.catch((e) => {
ElMessage.success(e.message);
});
}
//
function send() {
const data = refs.dp.getData();
service.fixtures.mould
.update({ data: data, form: data, id: Number(route.query.id) })
.then(() => {
ElMessage.success("页面发布成功");
})
.catch((e) => {
ElMessage.success(e.message);
});
}
function create() {
refs.preview.open(refs.dp.getData());
}
function clear() {
ElMessageBox.confirm("是否清空列表所有数据?", "提示", {
type: "warning"
})
.then(() => {
refs.dp.clear();
})
.catch(() => null);
}
function getPageData() {
const id = Number(route.query.id);
//
service.fixtures.mould
.info({ id })
.then((res: any) => {
const form = res?.form;
//
if (!Array.isArray(form) || form.length === 0) {
return;
}
// header
const header = form.shift();
setHeaderAndBody(header, res);
//
const updatedData = filterOutSuspension(form);
//
refs.dp.setFormList(updatedData);
})
.catch((error: any) => {
console.error("Failed to fetch page form:", error);
ElMessage.error("获取页面数据失败");
});
}
// Header Body
function setHeaderAndBody(header: any, res: any) {
refs.dp.setBody({
backgroundColor: res.backgroundColor,
backgroundImage: res.backgroundImage
});
refs.dp.setHeader(header);
}
//
function filterOutSuspension(form: any[]) {
//
const suspensionIndex = form.findIndex((f: { name: string }) => f.name === "suspension");
//
if (suspensionIndex !== -1) {
const suspension = form[suspensionIndex];
refs.dp.setSuspension(suspension);
//
return form.filter((_, index) => index !== suspensionIndex);
}
//
return form;
}
onMounted(() => {
getPageData();
});
</script>
<style lang="scss" scoped>
.form {
background-color: #fff;
position: relative;
min-width: 1300px;
height: 100%;
overflow: hidden;
.container {
height: calc(100% - 80px);
.tools {
display: flex;
flex-direction: column;
}
}
.footer {
display: flex;
justify-content: center;
padding-top: 20px;
height: 80px;
width: 100%;
box-sizing: border-box;
background-color: #fff;
border-top: 1px solid #ebeef5;
z-index: 9;
}
}
.preview-box {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
</style>

View File

@ -0,0 +1,195 @@
<template>
<cl-crud ref="Crud">
<cl-row>
<!-- 刷新按钮 -->
<cl-refresh-btn />
<!-- 新增按钮 -->
<cl-add-btn />
<!-- 删除按钮 -->
<cl-multi-delete-btn />
<cl-flex1 />
<!-- 关键字搜索 -->
<cl-search-key placeholder="页面名称" />
</cl-row>
<cl-row>
<!-- 数据表格 -->
<cl-table ref="Table" />
</cl-row>
<cl-row>
<cl-flex1 />
<!-- 分页控件 -->
<cl-pagination />
</cl-row>
<!-- 新增编辑 -->
<cl-upsert ref="Upsert" />
</cl-crud>
</template>
<script lang="ts" name="fixtures-mould" setup>
import { useCrud, useTable, useUpsert } from "@cool-vue/crud";
import { useCool } from "/@/cool";
const { service, router } = useCool();
// cl-upsert
const Upsert = useUpsert({
items: [
{
label: "页面名称",
prop: "name",
required: true,
component: {
name: "el-input"
}
},
{
label: "页面背景色",
prop: "background",
value: "#f6f7fa",
component: {
name: "el-color-picker",
props: {
"show-alpha": true,
predefine: ["#f6f7fa"]
}
}
},
{
label: "页面背景图",
prop: "backgroundImage",
component: {
name: "cl-upload"
}
},
{
label: "是否首页",
prop: "isHome",
required: true,
value: 0,
component: {
name: "el-radio-group",
options: [
{
label: "是",
value: 1
},
{
label: "否",
value: 0
}
]
}
},
{
label: "状态栏占位",
prop: "statusBar",
value: 1,
component: {
name: "el-switch",
props: {
"active-text": "显示",
"inactive-text": "隐藏",
"active-value": 1,
"inactive-value": 0,
"inline-prompt": true,
style: "--el-switch-on-color: #13ce66; --el-switch-off-color: #ff4949"
}
}
},
{
label: "状态栏颜色",
prop: "statusBarColor",
value: "#fff",
component: {
name: "el-color-picker",
props: {
"show-alpha": true,
predefine: ["#f6f7fa", "rgba(0, 0, 0, 0)"]
}
}
},
{
label: "是否启用",
prop: "status",
value: 1,
component: {
name: "el-switch",
props: {
"active-text": "是",
"inactive-text": "否",
"active-value": 1,
"inactive-value": 0,
"inline-prompt": true,
style: "--el-switch-on-color: #13ce66; --el-switch-off-color: #ff4949"
}
}
}
]
});
// cl-table
const Table = useTable({
columns: [
{ type: "selection" },
{ label: "页面名称", prop: "name", minWidth: 140 },
{
label: "是否首页",
prop: "isHome",
minWidth: 140,
dict: [
{
label: "是",
value: 1,
type: "primary"
},
{
label: "否",
value: 0,
type: "info"
}
]
},
{ label: "状态", prop: "status", minWidth: 100, component: { name: "cl-switch" } },
{
label: "更新时间",
prop: "updateTime",
minWidth: 160,
component: { name: "cl-date-text" }
},
{
type: "op",
width: 240,
buttons: [
{
label: "装修",
onClick(data: any) {
router.push("/fixtures/mould/edit?id=" + data.scope.row.id);
}
},
"edit",
"delete"
]
}
]
});
// cl-crud
const Crud = useCrud(
{
service: service.fixtures.mould
},
(app) => {
app.refresh();
}
);
//
function refresh(params?: any) {
Crud.value?.refresh(params);
}
</script>