基础插件修改
864
build/cool/eps.d.ts
vendored
5720
pnpm-lock.yaml
generated
Normal file
150
src/modules/fixtures/components/assembly/banner.vue
Normal 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>
|
190
src/modules/fixtures/components/assembly/coupon.vue
Normal 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>
|
131
src/modules/fixtures/components/assembly/fixtures.vue
Normal 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>
|
286
src/modules/fixtures/components/assembly/goods-list.vue
Normal 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>
|
147
src/modules/fixtures/components/assembly/goods.vue
Normal 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>
|
131
src/modules/fixtures/components/assembly/group.vue
Normal 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>
|
411
src/modules/fixtures/components/assembly/hot-image.vue
Normal 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>
|
424
src/modules/fixtures/components/assembly/link.vue
Normal 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>
|
161
src/modules/fixtures/components/assembly/list-menu.vue
Normal 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]);
|
||||
// 在index之后的位置插入clonedItem
|
||||
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>
|
156
src/modules/fixtures/components/assembly/menu.vue
Normal 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>
|
117
src/modules/fixtures/components/assembly/menus.vue
Normal 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]);
|
||||
// 在index之后的位置插入clonedItem
|
||||
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>
|
67
src/modules/fixtures/components/assembly/picture.vue
Normal 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>
|
453
src/modules/fixtures/components/assembly/rubik-cube.vue
Normal 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>
|
51
src/modules/fixtures/components/assembly/slider.vue
Normal 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>
|
149
src/modules/fixtures/components/assembly/suspension.vue
Normal 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]);
|
||||
// 在index之后的位置插入clonedItem
|
||||
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>
|
41
src/modules/fixtures/components/assembly/switch.vue
Normal 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>
|
87
src/modules/fixtures/components/assembly/title.vue
Normal 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>
|
226
src/modules/fixtures/components/config.vue
Normal 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>
|
143
src/modules/fixtures/components/fix/banner.vue
Normal 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>
|
256
src/modules/fixtures/components/fix/coupon.vue
Normal 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>
|
68
src/modules/fixtures/components/fix/empty.vue
Normal 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>
|
247
src/modules/fixtures/components/fix/goods-list.vue
Normal 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>
|
109
src/modules/fixtures/components/fix/hot-image.vue
Normal 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>
|
86
src/modules/fixtures/components/fix/line.vue
Normal 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>
|
180
src/modules/fixtures/components/fix/list-menu.vue
Normal 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>
|
229
src/modules/fixtures/components/fix/menus.vue
Normal 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>
|
108
src/modules/fixtures/components/fix/picture.vue
Normal 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>
|
318
src/modules/fixtures/components/fix/positioning.vue
Normal 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>
|
101
src/modules/fixtures/components/fix/rich-text.vue
Normal 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>
|
232
src/modules/fixtures/components/fix/rubik-cube.vue
Normal 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>
|
120
src/modules/fixtures/components/fix/search.vue
Normal 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>
|
150
src/modules/fixtures/components/fix/suspension.vue
Normal 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>
|
160
src/modules/fixtures/components/fix/title.vue
Normal 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>
|
117
src/modules/fixtures/components/fix/video.vue
Normal 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>
|
46
src/modules/fixtures/components/fix/wechat.vue
Normal 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>
|
505
src/modules/fixtures/components/index.vue
Normal 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>
|
212
src/modules/fixtures/components/lump.vue
Normal 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>
|
23
src/modules/fixtures/config.ts
Normal 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);
|
||||
}
|
||||
};
|
||||
};
|
1673
src/modules/fixtures/data/base.tsx
Normal file
11
src/modules/fixtures/data/index.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { Header, Base, Marketing, Ability, Other } from './base'
|
||||
|
||||
export function useData() {
|
||||
return {
|
||||
Header,
|
||||
Base,
|
||||
Marketing,
|
||||
Ability,
|
||||
Other
|
||||
};
|
||||
}
|
8
src/modules/fixtures/hooks/index.ts
Normal 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 };
|
||||
}
|
BIN
src/modules/fixtures/static/icon/banner.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
src/modules/fixtures/static/icon/coupon.png
Normal file
After Width: | Height: | Size: 2.9 KiB |
BIN
src/modules/fixtures/static/icon/empty.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
src/modules/fixtures/static/icon/goods-list.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
src/modules/fixtures/static/icon/hot-image.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
src/modules/fixtures/static/icon/line.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
src/modules/fixtures/static/icon/list-menu.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
src/modules/fixtures/static/icon/menus.png
Normal file
After Width: | Height: | Size: 2.0 KiB |
BIN
src/modules/fixtures/static/icon/picture.png
Normal file
After Width: | Height: | Size: 2.3 KiB |
BIN
src/modules/fixtures/static/icon/positioning.png
Normal file
After Width: | Height: | Size: 2.4 KiB |
BIN
src/modules/fixtures/static/icon/rich-text.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
src/modules/fixtures/static/icon/rubik-cube.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
src/modules/fixtures/static/icon/search.png
Normal file
After Width: | Height: | Size: 2.4 KiB |
BIN
src/modules/fixtures/static/icon/suspension.png
Normal file
After Width: | Height: | Size: 2.5 KiB |
BIN
src/modules/fixtures/static/icon/title.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
src/modules/fixtures/static/icon/video.png
Normal file
After Width: | Height: | Size: 2.0 KiB |
BIN
src/modules/fixtures/static/icon/wechat.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
159
src/modules/fixtures/static/js/DraggableResizableClass.ts
Normal 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
@ -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
@ -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;
|
||||
}
|
||||
}
|
211
src/modules/fixtures/views/edit.vue
Normal 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>
|
195
src/modules/fixtures/views/mould.vue
Normal 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>
|