大致完成页面

This commit is contained in:
2026-02-05 09:01:16 +08:00
parent c411c23b9c
commit d51e6a8f72
40 changed files with 11023 additions and 737 deletions

View File

@@ -0,0 +1,183 @@
<template>
<view class="editor-wrapper">
<view class="item-list">
<view
v-for="(item, index) in items"
:key="item.id"
class="editor-item"
>
<view class="drag-handle">
<text class="drag-ic">⋮⋮</text>
</view>
<view class="item-content">
<view class="thumb-box" @click="handleUploadImage(index)">
<image v-if="item.imageUrl" :src="item.imageUrl" mode="aspectFill" class="thumb-img"></image>
<view v-else class="upload-ph">
<text class="plus">+</text>
</view>
<view class="remove-btn" @click.stop="$emit('remove', index)">
<text class="remove-txt">×</text>
</view>
</view>
<view class="form-box">
<view class="field-group">
<text class="field-label">图片名称:</text>
<input
class="field-input"
:value="item.name"
@input="e => updateItem(index, 'name', e.detail.value)"
placeholder="请输入图片名称"
/>
</view>
<view class="field-group">
<text class="field-label">链接地址:</text>
<view class="link-input-row">
<input
class="field-input link-input"
:value="item.link.value"
@input="e => updateLink(index, e.detail.value)"
placeholder="请输入链接路径或 URL"
/>
<view class="link-picker-btn" @click="$emit('select-link', index)">
<text class="link-ic">🔗</text>
</view>
</view>
</view>
</view>
</view>
<!-- 上下移动按钮 -->
<view class="move-actions">
<view class="move-btn" @click="$emit('move', index, -1)" v-if="index > 0">
<text class="move-ic">↑</text>
</view>
<view class="move-btn" @click="$emit('move', index, 1)" v-if="index < items.length - 1">
<text class="move-ic">↓</text>
</view>
</view>
</view>
</view>
<view v-if="items.length < max" class="add-action">
<view class="btn-add-item" @click="$emit('add')">
<text class="btn-add-txt">添加图片</text>
</view>
</view>
<view v-else class="max-tip">
<text class="max-tip-txt">最多添加 {{ max }} 张图片</text>
</view>
</view>
</template>
<script setup lang="uts">
import { type ConfigItem } from '@/pages/mall/admin/decoration/components/types.uts'
const props = defineProps<{
items: ConfigItem[]
max: number
}>()
const emit = defineEmits(['add', 'remove', 'move', 'upload', 'select-link', 'update-item'])
const handleUploadImage = (index: number) => {
uni.chooseImage({
count: 1,
success: (res) => {
emit('update-item', { index, key: 'imageUrl', value: res.tempFilePaths[0] })
}
})
}
const updateItem = (index: number, key: string, value: string) => {
emit('update-item', { index, key, value })
}
const updateLink = (index: number, value: string) => {
const link = { ...props.items[index].link, value }
emit('update-item', { index, key: 'link', value: link })
}
</script>
<style scoped lang="scss">
.editor-wrapper { margin-top: 10px; }
.editor-item {
display: flex;
flex-direction: row;
padding: 16px;
border: 1px dashed #d9d9d9;
border-radius: 4px;
margin-bottom: 16px;
position: relative;
background-color: #fafafa;
}
.drag-handle {
width: 30px;
display: flex;
align-items: center;
justify-content: center;
cursor: grab;
}
.drag-ic { color: #ccc; font-size: 18px; }
.item-content { flex: 1; display: flex; flex-direction: row; gap: 20px; }
.thumb-box {
width: 100px;
height: 60px;
background-color: #fff;
border: 1px solid #eee;
border-radius: 4px;
position: relative;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
.thumb-img { width: 100%; height: 100%; border-radius: 4px; }
.upload-ph { font-size: 24px; color: #999; }
.remove-btn {
position: absolute;
top: -8px;
right: -8px;
width: 16px;
height: 16px;
background-color: rgba(0,0,0,0.5);
color: #fff;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
z-index: 5;
}
.remove-txt { font-size: 12px; line-height: 1; }
.form-box { flex: 1; }
.field-group { display: flex; flex-direction: row; align-items: center; margin-bottom: 12px; }
.field-label { width: 70px; font-size: 13px; color: #666; }
.field-input { flex: 1; height: 32px; border: 1px solid #dcdfe6; border-radius: 4px; padding: 0 10px; font-size: 13px; background-color: #fff; }
.link-input-row { flex: 1; display: flex; flex-direction: row; align-items: center; }
.link-picker-btn { width: 32px; height: 32px; border: 1px solid #dcdfe6; border-left: none; border-radius: 0 4px 4px 0; display: flex; align-items: center; justify-content: center; cursor: pointer; background-color: #fff; }
.link-ic { font-size: 14px; }
.link-input { border-radius: 4px 0 0 4px; }
.add-action { margin-top: 10px; }
.btn-add-item {
width: 120px;
height: 36px;
background-color: #1890ff;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
.btn-add-txt { color: #fff; font-size: 14px; }
.max-tip { margin-top: 12px; }
.max-tip-txt { color: #ff4d4f; font-size: 12px; }
.move-actions { display: flex; flex-direction: column; gap: 10px; padding-left: 10px; }
.move-btn { cursor: pointer; color: #1890ff; font-size: 16px; }
</style>

View File

@@ -0,0 +1,60 @@
<template>
<view class="menu-column">
<view
v-for="item in categories"
:key="item.key"
:class="['menu-item', activeKey === item.key ? 'active' : '']"
@click="$emit('change', item.key)"
>
<text :class="['menu-txt', activeKey === item.key ? 'active-txt' : '']">{{ item.label }}</text>
<view v-if="activeKey === item.key" class="active-line"></view>
</view>
</view>
</template>
<script setup lang="uts">
import { type Category } from '@/pages/mall/admin/decoration/components/types.uts'
defineProps<{
categories: Category[]
activeKey: string
}>()
defineEmits(['change'])
</script>
<style scoped lang="scss">
.menu-column {
width: 200px;
background-color: #fff;
border-right: 1px solid #f0f0f0;
padding: 20px 0;
}
.menu-item {
height: 50px;
display: flex;
flex-direction: row;
align-items: center;
padding: 0 24px;
position: relative;
cursor: pointer;
}
.menu-item.active {
background-color: #f0f7ff;
}
.menu-txt { font-size: 14px; color: #666; }
.menu-txt.active-txt { color: #1890ff; font-weight: bold; }
.active-line {
position: absolute;
right: 0;
top: 15px;
bottom: 15px;
width: 3px;
background-color: #1890ff;
border-radius: 3px 0 0 3px;
}
</style>

View File

@@ -0,0 +1,737 @@
<template>
<view class="preview-column">
<view class="phone-mockup">
<view class="phone-inner">
<!-- 5. 开屏广告预览 (1:1 全屏覆盖) -->
<view v-if="activeKey === 'ad'" class="mock-ad-preview-v2">
<image v-if="activeConfig.items.length > 0" :src="activeConfig.items[0].imageUrl || '/static/logo.png'" mode="aspectFill" class="ad-full-image-v2"></image>
<view v-else class="ad-placeholder-v2">
<text class="ad-ph-txt">开屏广告预览区</text>
</view>
<view class="ad-skip-btn-v2">
<text class="ad-skip-txt">跳过 3s</text>
</view>
</view>
<!-- 常规页面内容 -->
<block v-if="activeKey !== 'ad'">
<view class="status-bar-mock"></view>
<scroll-view class="phone-body" :scroll-y="true">
<!-- 轮播图预览 -->
<view class="banner-area" :key="activeKey + '-' + itemsVersion">
<swiper
v-if="activeConfig.items.length > 0"
:circular="true"
:autoplay="isAutoplay"
:interval="3000"
:duration="500"
@change="onSwiperChange"
>
<swiper-item v-for="(item, index) in activeConfig.items" :key="index">
<view class="swiper-image-wrapper">
<image
:src="item.imageUrl || '/static/logo.png'"
mode="aspectFill"
class="banner-img"
></image>
</view>
</swiper-item>
</swiper>
<!-- 空态占位 -->
<view v-else class="preview-banner-placeholder">
<view class="ph-content">
<text class="ph-icon">🖼️</text>
<text class="ph-txt">暂无图片,请添加~</text>
</view>
</view>
</view>
<!-- 不同分类对应的 Mock 内容 -->
<view class="mock-section" :class="{ 'bg-red': activeKey === 'group' }">
<!-- 1. 精品推荐/热门/新品/促销 (1:1 垂直列表复刻) -->
<view
v-if="['jingpin', 'hot', 'new', 'promo'].includes(activeKey)"
class="mock-product-list"
>
<!-- 1:1 复刻标题栏(带分割线) -->
<view class="section-title-standard">
<view class="title-line"></view>
<view class="title-center">
<text class="title-ic-standard">{{ activeKey === 'hot' ? '🔥' : (activeKey === 'new' ? '🆕' : '💎') }}</text>
<text class="title-txt-standard">{{ activeKey === 'jingpin' ? '精品推荐' : (activeKey === 'hot' ? '热门榜单' : (activeKey === 'new' ? '首发新品' : '促销单品')) }}</text>
</view>
<view class="title-line"></view>
</view>
<!-- 列表布局 -->
<view class="product-list-vertical">
<view class="product-list-item" v-for="(item, i) in 4" :key="i">
<!-- 商品图片(圆角) -->
<view class="p-img-left" :class="'p-img-mock-' + i"></view>
<!-- 商品详情 -->
<view class="p-info-right">
<view class="p-name-box-standard">
<text class="p-name-txt-standard">{{ getMockName(i) }}</text>
</view>
<view class="p-bottom-row-standard">
<view class="p-price-box-standard">
<text class="p-symbol-standard">¥</text>
<text class="p-val-standard">{{ getMockPrice(i) }}</text>
</view>
<text class="p-sales-standard">已售{{ getMockSales(i) }}件</text>
</view>
</view>
</view>
</view>
</view>
<!-- 2. 拼团 (1:1 复刻) -->
<view v-if="activeKey === 'group'" class="mock-group-list-red">
<!-- 参团人数提示 -->
<view class="group-participation-bar">
<view class="avatar-stack">
<view class="a-item a1"></view>
<view class="a-item a2"></view>
<view class="a-item a3"></view>
</view>
<text class="group-p-txt">252人参与</text>
</view>
<view class="group-item-card" v-for="i in 3" :key="i">
<view class="g-img-left" :class="'g-img-mock-' + i"></view>
<view class="g-info-right">
<view class="g-name-box-v2">
<text class="g-name-txt-v2">{{ getGroupMockName(i) }}</text>
</view>
<view class="g-price-row-v2">
<text class="g-p-orig-v2">¥{{ getGroupMockOrigPrice(i) }}</text>
<view class="g-p-main-v2">
<text class="g-p-sym-v2">¥</text>
<text class="g-p-val-v2">{{ getGroupMockPrice(i) }}</text>
</view>
</view>
<view class="g-action-row-v2">
<view class="g-label-count"><text class="g-lc-txt">2人团</text></view>
<view class="g-btn-v2"><text class="g-btn-txt-v2">去拼团</text></view>
</view>
</view>
</view>
</view>
<!-- 3. 积分商城1:1 复刻 -->
<view v-if="activeKey === 'points'" class="mock-points-mall-v2">
<!-- 导航金刚区 -->
<view class="points-nav-row-v2">
<view class="p-nav-item-v2" v-for="(nav, idx) in pointsNavs" :key="idx">
<view class="p-nav-ic-v2" :style="{ backgroundColor: nav.color }">
<text class="p-nav-ic-emoji">{{ nav.emoji }}</text>
</view>
<text class="p-nav-txt-v2">{{ nav.title }}</text>
</view>
</view>
<view class="points-divider-v2"></view>
<!-- 列表标题 -->
<view class="points-section-header">
<text class="ps-title-v2">大家都在换</text>
<view class="ps-more-v2">
<text class="ps-more-txt">查看更多</text>
<text class="ps-more-ic">></text>
</view>
</view>
<view class="points-grid-v2">
<view class="points-card-v2" v-for="i in 4" :key="i">
<view class="pc-img-v2" :class="'pc-img-mock-' + i"></view>
<view class="pc-info-v2">
<text class="pc-title-v2">小米蓝牙耳机新款横板耳机新款横板耳...</text>
<view class="pc-price-v2">
<text class="pc-points-v2">{{ 666 + i*11 }}积分</text>
</view>
<text class="pc-ex-count-v2">999人兑换</text>
</view>
</view>
</view>
</view>
<!-- 4. 登录页预览 -->
<view v-if="activeKey === 'login'" class="mock-login-view">
<view class="login-box">
<view class="login-logo-mock"></view>
<view class="login-input-mock"><text class="l-in-txt">请输入手机号</text></view>
<view class="login-input-mock"><text class="l-in-txt">请输入验证码</text></view>
<view class="login-btn-mock"><text class="l-btn-txt">立即登录</text></view>
</view>
</view>
</view>
</scroll-view>
</block>
</view>
</view>
</view>
</template>
<script setup lang="uts">
import { ref, computed, watch } from 'vue'
import { type ConfigData } from '@/pages/mall/admin/decoration/components/types.uts'
const props = defineProps<{
activeKey: string
activeLabel: string
activeConfig: ConfigData
}>()
// 用于 Swiper 刷新的 key
const itemsVersion = ref(0)
const currentIndex = ref(0)
// 只有当图片数量超过1张时开启自动轮播
const isAutoplay = computed(() => props.activeConfig.items.length >= 2)
// 监听数据变化,更新版本以强制 Swiper 重绘(处理排序和增删)
watch(() => props.activeConfig.items, () => {
itemsVersion.value++
}, { deep: true })
// 切换分类时重置状态
watch(() => props.activeKey, () => {
currentIndex.value = 0
itemsVersion.value++
})
const onSwiperChange = (e : any) => {
currentIndex.value = e.detail.current
}
// 积分商城导航数据
const pointsNavs = [
{ title: '我的积分', color: '#ffb400', emoji: '⭐' },
{ title: '每日签到', color: '#4facfe', emoji: '📅' },
{ title: '积分抽奖', color: '#f06292', emoji: '🎡' },
{ title: '兑换记录', color: '#ffa726', emoji: '📝' }
]
// Mock 数据辅助函数
const getMockName = (i: number): string => {
const names = [
'MIUCHO可爱卡通学生通勤手提电...',
'贝昂智能空气循环风扇家用落地电...',
'真力时 (ZENITH) 瑞士手表DEFY...',
'小米保温杯云米电热杯茶叶杯水杯...'
]
return names[i % names.length]
}
const getMockPrice = (i: number): string => {
const prices = ['158.00', '1299.00', '61000.00', '100.00']
return prices[i % prices.length]
}
const getMockSales = (i: number): string => {
const sales = ['5495', '2899', '1108', '100']
return sales[i % sales.length]
}
const getGroupMockName = (i: number): string => {
const names = [
'FOMIX 蛋壳椅 进口头层牛皮橙色单人沙发椅Egg chair设计师蛋...',
'UR2024夏季新款女装复古纯欲氛围感一字肩短款T恤UWG440...',
'阿迪达斯官网 adidas BBALL CAP COT 男女训练运动帽子FO5270...',
'雅诗兰黛小棕瓶精华液 50ml'
]
return names[i % names.length]
}
const getGroupMockPrice = (i: number): string => {
const prices = ['999', '99', '77', '499']
return prices[i % prices.length]
}
const getGroupMockOrigPrice = (i: number): string => {
const prices = ['7580', '129', '100', '890']
return prices[i % prices.length]
}
</script>
<style scoped lang="scss">
.preview-column {
width: 420px;
background-color: #f7f8fa;
display: flex;
justify-content: center;
padding: 40px;
border-right: 1px solid #f0f0f0;
}
.phone-mockup {
width: 320px;
height: 640px;
background-color: #000;
border-radius: 36px;
padding: 12px;
box-shadow: 0 20px 40px rgba(0,0,0,0.15);
}
.phone-inner {
width: 100%;
height: 100%;
background-color: #f5f5f5;
border-radius: 28px;
overflow: hidden;
position: relative;
display: flex;
flex-direction: column;
}
.status-bar-mock { height: 24px; background-color: transparent; }
.phone-body { flex: 1; }
/* 轮播图 Swiper 1:1 */
.banner-area {
padding: 10px 12px;
background-color: #fff;
}
.preview-swiper {
width: 100%;
height: 110px; /* 根据 690*240 比例在预览窗口的换算高度 */
border-radius: 10px;
overflow: hidden;
background-color: #f9f9f9;
}
.swiper-image-wrapper {
width: 100%;
height: 100%;
}
.banner-img {
width: 100%;
height: 100%;
}
.preview-banner-placeholder {
width: 100%;
height: 110px;
background-color: #f0f0f0;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
}
.ph-content { display: flex; flex-direction: column; align-items: center; }
.ph-icon { font-size: 24px; margin-bottom: 4px; }
.ph-txt { font-size: 11px; color: #999; }
/* Mock 内容样式 */
.mock-section {
padding: 12px;
min-height: 200px;
transition: background-color 0.3s;
}
.bg-red {
background-color: #e93323;
}
/* 1:1 标准标题栏(带分割线) */
.section-title-standard {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
padding: 15px 0;
gap: 10px;
}
.title-line {
flex: 1;
height: 1px;
background-color: #eeeeee;
}
.title-center {
display: flex;
flex-direction: row;
align-items: center;
}
.title-ic-standard {
font-size: 14px;
margin-right: 4px;
}
.title-txt-standard {
font-size: 14px;
font-weight: 500;
color: #333333;
}
/* 垂直列表1:1 复刻 CRMEB */
.product-list-vertical {
display: flex;
flex-direction: column;
}
.product-list-item {
display: flex;
flex-direction: row;
background-color: #fff;
padding: 15px 12px;
border-radius: 8px;
margin-bottom: 10px;
box-shadow: 0 2px 8px rgba(0,0,0,0.02);
}
.p-img-left {
width: 90px;
height: 90px;
background-color: #f7f7f7;
border-radius: 6px;
margin-right: 12px;
}
/* Mock 占位背景色彩 */
.p-img-mock-0 { background-color: #eef2f9; }
.p-img-mock-1 { background-color: #fff1f0; }
.p-img-mock-2 { background-color: #f6ffed; }
.p-img-mock-3 { background-color: #fff7e6; }
.p-info-right {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 2px 0;
}
.p-name-box-standard {
margin-bottom: 4px;
}
.p-name-txt-standard {
font-size: 13px;
color: #333333;
line-height: 1.4;
/* 模拟两行省略 */
display: block;
overflow: hidden;
}
.p-bottom-row-standard {
display: flex;
flex-direction: column;
gap: 2px;
}
.p-price-box-standard {
display: flex;
flex-direction: row;
align-items: baseline;
}
.p-symbol-standard {
font-size: 11px;
color: #e93323;
font-weight: bold;
}
.p-val-standard {
font-size: 16px;
color: #e93323;
font-weight: bold;
}
.p-sales-standard {
font-size: 10px;
color: #999999;
}
/* 拼团 1:1 复刻 (红色背景配套) */
.mock-group-list-red {
display: flex;
flex-direction: column;
}
.group-participation-bar {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
padding: 5px 0 15px;
}
.avatar-stack {
display: flex;
flex-direction: row;
margin-right: 8px;
}
.a-item {
width: 18px;
height: 18px;
border-radius: 50%;
border: 1px solid #fff;
margin-left: -6px;
}
.a1 { background-color: #ffcdd2; margin-left: 0; }
.a2 { background-color: #f8bbd0; }
.a3 { background-color: #e1bee7; }
.group-p-txt {
font-size: 11px;
color: #fff;
}
.group-item-card {
display: flex;
flex-direction: row;
background-color: #fff;
border-radius: 10px;
padding: 10px;
margin-bottom: 12px;
}
.g-img-left {
width: 90px;
height: 90px;
background-color: #f5f5f5;
border-radius: 6px;
margin-right: 12px;
}
.g-img-mock-1 { background-color: #ffe0b2; }
.g-img-mock-2 { background-color: #c8e6c9; }
.g-img-mock-3 { background-color: #bbdefb; }
.g-info-right {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.g-name-txt-v2 {
font-size: 12px;
color: #333;
line-height: 1.4;
}
.g-price-row-v2 {
display: flex;
flex-direction: column;
}
.g-p-orig-v2 {
font-size: 10px;
color: #999;
text-decoration: line-through;
}
.g-p-main-v2 {
display: flex;
flex-direction: row;
align-items: baseline;
}
.g-p-sym-v2 {
font-size: 10px;
color: #e93323;
font-weight: bold;
}
.g-p-val-v2 {
font-size: 18px;
color: #e93323;
font-weight: bold;
}
.g-action-row-v2 {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
.g-label-count {
border: 1px solid #ff7f50;
border-radius: 4px;
padding: 0 4px;
}
.g-lc-txt {
font-size: 10px;
color: #ff7f50;
}
.g-btn-v2 {
background: linear-gradient(90deg, #ff7f50, #e93323);
padding: 4px 12px;
border-radius: 20px;
}
.g-btn-txt-v2 {
font-size: 11px;
color: #fff;
}
/* 积分商城 1:1 复刻 (白色卡片阴影) */
.mock-points-mall-v2 {
background-color: #fff;
border-radius: 12px;
margin-top: -10px;
padding-top: 15px;
}
.points-nav-row-v2 {
display: flex;
flex-direction: row;
justify-content: space-around;
margin-bottom: 12px;
}
.p-nav-item-v2 {
display: flex;
flex-direction: column;
align-items: center;
}
.p-nav-ic-v2 {
width: 38px;
height: 38px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 6px;
}
.p-nav-ic-emoji {
font-size: 18px;
}
.p-nav-txt-v2 {
font-size: 11px;
color: #666;
}
.points-divider-v2 {
height: 8px;
background-color: #f7f8fa;
}
.points-section-header {
display: flex;
flex-direction: row;
justify-content: space-between;
padding: 15px;
}
.ps-title-v2 {
font-size: 15px;
font-weight: bold;
color: #333;
}
.ps-more-v2 {
display: flex;
flex-direction: row;
align-items: center;
}
.ps-more-txt {
font-size: 11px;
color: #999;
}
.ps-more-ic {
font-size: 12px;
color: #ccc;
margin-left: 2px;
}
.points-grid-v2 {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
padding: 0 12px 12px;
}
.points-card-v2 {
background-color: #fff;
border: 1px solid #f8f8f8;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 6px rgba(0,0,0,0.02);
}
.pc-img-v2 {
width: 100%;
aspect-ratio: 1;
}
.pc-img-mock-1 { background-color: #fce4ec; }
.pc-img-mock-2 { background-color: #f3e5f5; }
.pc-img-mock-3 { background-color: #e1f5fe; }
.pc-img-mock-4 { background-color: #e8f5e9; }
.pc-info-v2 {
padding: 8px;
}
.pc-title-v2 {
font-size: 11px;
color: #333;
line-height: 1.4;
height: 32px;
display: block;
}
.pc-price-v2 {
margin: 4px 0;
}
.pc-points-v2 {
font-size: 13px;
font-weight: bold;
color: #e93323;
}
.pc-ex-count-v2 {
font-size: 9px;
color: #999;
}
/* 登录页 Mock */
.mock-login-view {
padding: 50px 24px;
}
.login-box {
display: flex;
flex-direction: column;
align-items: center;
}
.login-logo-mock {
width: 64px;
height: 64px;
background-color: #e93323;
border-radius: 12px;
margin-bottom: 40px;
}
.login-input-mock {
width: 100%;
height: 48px;
background-color: #fff;
border-radius: 24px;
margin-bottom: 16px;
padding: 0 20px;
display: flex;
align-items: center;
border: 1px solid #f0f0f0;
}
.l-in-txt { color: #ccc; font-size: 14px; }
.login-btn-mock {
width: 100%;
height: 48px;
background-color: #e93323;
border-radius: 24px;
display: flex;
align-items: center;
justify-content: center;
margin-top: 10px;
}
.l-btn-txt { color: #fff; font-size: 16px; font-weight: bold; }
/* 开屏广告 1:1 */
.mock-ad-preview-v2 {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #f5f5f5;
z-index: 100;
}
.ad-full-image-v2 {
width: 100%;
height: 100%;
}
.ad-placeholder-v2 {
width: 100%;
height: 100%;
background-color: #eee;
display: flex;
align-items: center;
justify-content: center;
}
.ad-skip-btn-v2 {
position: absolute;
top: 40px;
right: 20px;
background-color: rgba(0,0,0,0.4);
padding: 5px 12px;
border-radius: 20px;
}
.ad-skip-txt {
color: #fff;
font-size: 12px;
}
</style>

View File

@@ -0,0 +1,28 @@
export type LinkType = 'internal' | 'external' | 'miniProgram'
export interface LinkInfo {
type: LinkType
value: string
}
export interface ConfigItem {
id: number
name: string
imageUrl: string
link: LinkInfo
sort: number
}
export interface ConfigData {
enabled?: boolean
durationSeconds?: number
max: number
items: ConfigItem[]
}
export interface Category {
key: string
label: string
type: string
recommendSizeText: string
}

View File

@@ -0,0 +1,259 @@
<template>
<view class="admin-data-config anim-fade-in">
<!-- 顶部标题 -->
<view class="page-header border-shadow">
<view class="header-left">
<text class="page-title">数据配置</text>
</view>
<view class="header-right">
<view class="btn-save" @click="handleSave">
<text class="save-txt">保存</text>
</view>
</view>
</view>
<!-- 主内容区:三栏布局 -->
<view class="main-content">
<view class="card-container border-shadow">
<!-- A. 左栏:配置分类菜单 -->
<MenuSide
:categories="categories"
:activeKey="activeKey"
@change="k => activeKey = k"
/>
<!-- B. 中栏:手机预览 -->
<PhonePreview
:activeKey="activeKey"
:activeLabel="activeLabel"
:activeConfig="activeConfig"
/>
<!-- C. 右栏:配置表单 -->
<view class="settings-column">
<view class="settings-header">
<view class="title-marker"></view>
<text class="settings-title">{{ activeTitle }}</text>
</view>
<view class="settings-desc-box">
<text class="settings-desc">{{ activeCategory?.recommendSizeText }}</text>
</view>
<!-- 开屏广告特有字段 -->
<view v-if="activeKey === 'ad'" class="ad-special-fields">
<view class="form-row">
<text class="field-label">开启广告</text>
<switch :checked="activeConfig.enabled" @change="handleSwitchAd" color="#2d8cf0" />
</view>
<view class="form-row">
<text class="field-label">广告时间</text>
<view class="input-with-unit">
<input type="number" class="time-input" v-model="activeConfig.durationSeconds" />
<text class="unit-txt">单位(秒)</text>
</view>
</view>
</view>
<!-- 图片项编辑器 -->
<view v-if="activeKey !== 'ad' || activeConfig.enabled">
<CarouselEditor
:items="activeConfig.items"
:max="activeConfig.max"
@add="handleAddItem"
@remove="handleRemoveItem"
@move="handleMove"
@update-item="handleUpdateItem"
@select-link="handleSelectLink"
/>
</view>
<view v-else class="ad-disabled-placeholder">
<text class="disabled-txt">开屏广告已关闭,开启后可配置图片</text>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup lang="uts">
import { ref, computed, reactive } from 'vue'
import { type Category, type ConfigData, type LinkType } from '@/pages/mall/admin/decoration/components/types.uts'
import MenuSide from '@/pages/mall/admin/decoration/components/MenuSide.uvue'
import PhonePreview from '@/pages/mall/admin/decoration/components/PhonePreview.uvue'
import CarouselEditor from '@/pages/mall/admin/decoration/components/CarouselEditor.uvue'
// 状态定义
const activeKey = ref('jingpin')
const categories = reactive<Category[]>([
{ key: 'jingpin', label: '首页精品推荐图片', type: 'carousel', recommendSizeText: '建议尺寸690 * 240px拖拽图片可调整图片顺序哦最多添加五张' },
{ key: 'hot', label: '热门榜单推荐图片', type: 'carousel', recommendSizeText: '建议尺寸690 * 240px拖拽图片可调整图片顺序哦最多添加五张' },
{ key: 'new', label: '首发新品推荐图片', type: 'carousel', recommendSizeText: '建议尺寸690 * 240px拖拽图片可调整图片顺序哦最多添加五张' },
{ key: 'promo', label: '促销单品推荐图片', type: 'carousel', recommendSizeText: '建议尺寸690 * 240px拖拽图片可调整图片顺序哦最多添加五张' },
{ key: 'login', label: '后台登录页面幻灯片', type: 'carousel', recommendSizeText: '建议尺寸690 * 240px拖拽图片可调整图片顺序哦最多添加五张' },
{ key: 'group', label: '拼团列表轮播图', type: 'carousel', recommendSizeText: '建议尺寸710 * 300px拖拽图片可调整图片顺序哦最多添加五张' },
{ key: 'points', label: '积分商城轮播图', type: 'carousel', recommendSizeText: '建议尺寸710 * 300px拖拽图片可调整图片顺序哦最多添加五张' },
{ key: 'ad', label: '开屏广告', type: 'ad', recommendSizeText: '建议尺寸750 * 1334px拖拽图片可调整图片顺序哦最多添加五张' }
])
// 初始化数据
const configMap = reactive<Record<string, ConfigData>>({
'jingpin': { max: 5, items: [{ id: 1, name: '1', imageUrl: '', link: { type: 'internal', value: '/pages/points_mall/integral_index' }, sort: 0 }] },
'hot': { max: 5, items: [{ id: 1, name: '1', imageUrl: '', link: { type: 'internal', value: '/pages/index/index' }, sort: 0 }] },
'new': { max: 5, items: [{ id: 1, name: '1', imageUrl: '', link: { type: 'internal', value: '/pages/index/index' }, sort: 0 }] },
'promo': { max: 5, items: [{ id: 1, name: '1', imageUrl: '', link: { type: 'internal', value: '/pages/points_mall/integral_index' }, sort: 0 }] },
'login': { max: 5, items: [{ id: 1, name: '1', imageUrl: '', link: { type: 'internal', value: '' }, sort: 0 }] },
'group': { max: 5, items: [{ id: 1, name: '拼团', imageUrl: '', link: { type: 'internal', value: '/pages/activity/goods_combination/index' }, sort: 0 }] },
'points': { max: 5, items: [{ id: 1, name: '1', imageUrl: '', link: { type: 'internal', value: '/pages/points_mall/integral_index' }, sort: 0 }] },
'ad': { enabled: false, durationSeconds: 3, max: 5, items: [] }
})
// 计算属性
const activeCategory = computed(() => categories.find(c => c.key === activeKey.value))
const activeLabel = computed(() => activeCategory.value?.label ?? '')
const activeConfig = computed(() => configMap[activeKey.value])
const activeTitle = computed(() => {
if (activeKey.value === 'ad') return '引导页设置'
if (activeKey.value === 'login') return '幻灯片设置'
return '轮播图设置'
})
// 方法
const handleSave = () => {
uni.showLoading({ title: '保存中...' })
setTimeout(() => {
uni.hideLoading()
uni.showToast({ title: '保存成功', icon: 'success' })
}, 800)
}
const handleSwitchAd = (e: any) => {
configMap['ad'].enabled = e.detail.value
}
const handleAddItem = () => {
const config = activeConfig.value
if (config.items.length >= config.max) {
uni.showToast({ title: `最多添加 ${config.max} 条`, icon: 'none' })
return
}
config.items.push({
id: Date.now(),
name: (config.items.length + 1).toString(),
imageUrl: '',
link: { type: 'internal', value: '' },
sort: config.items.length
})
}
const handleRemoveItem = (index: number) => {
activeConfig.value.items.splice(index, 1)
}
const handleMove = (index: number, direction: number) => {
const items = activeConfig.value.items
const targetIndex = index + direction
if (targetIndex < 0 || targetIndex >= items.length) return
const temp = items[index]
items[index] = items[targetIndex]
items[targetIndex] = temp
}
const handleUpdateItem = (payload: any) => {
const { index, key, value } = payload
activeConfig.value.items[index][key] = value
}
const handleSelectLink = (index: number) => {
uni.showActionSheet({
itemList: ['内部页面', '外部链接', '其他小程序'],
success: (res) => {
const types: LinkType[] = ['internal', 'external', 'miniProgram']
activeConfig.value.items[index].link.type = types[res.tapIndex]
uni.showToast({ title: '功能建设中', icon: 'none' })
}
})
}
</script>
<style scoped lang="scss">
.admin-data-config {
background-color: #f0f2f5;
min-height: 100vh;
display: flex;
flex-direction: column;
}
.border-shadow {
background-color: #fff;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
}
.page-header {
height: 60px;
padding: 0 24px;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
z-index: 100;
}
.page-title { font-size: 16px; font-weight: bold; color: #333; }
.btn-save {
background-color: #2d8cf0;
padding: 6px 24px;
border-radius: 4px;
cursor: pointer;
}
.save-txt { color: #fff; font-size: 14px; }
.main-content {
flex: 1;
padding: 24px;
}
.card-container {
display: flex;
flex-direction: row;
min-height: 800px;
background-color: #fff;
border-radius: 8px;
overflow: hidden;
}
/* 右侧设置 */
.settings-column {
flex: 1;
padding: 30px;
background-color: #fff;
}
.settings-header {
display: flex;
flex-direction: row;
align-items: center;
margin-bottom: 8px;
}
.title-marker { width: 3px; height: 16px; background-color: #1890ff; margin-right: 10px; border-radius: 2px; }
.settings-title { font-size: 16px; font-weight: bold; color: #333; }
.settings-desc-box { margin-bottom: 24px; padding-left: 13px; }
.settings-desc { font-size: 13px; color: #999; }
/* 开屏广告特有样式 */
.ad-special-fields { padding: 20px; background-color: #f6f8fb; border-radius: 8px; margin-bottom: 20px; }
.form-row { display: flex; flex-direction: row; align-items: center; margin-bottom: 15px; }
.field-label { width: 80px; font-size: 14px; color: #333; }
.input-with-unit { display: flex; flex-direction: row; align-items: center; }
.time-input { width: 80px; height: 32px; border: 1px solid #dcdfe6; border-radius: 4px; padding: 0 10px; margin-right: 8px; background-color: #fff; }
.unit-txt { font-size: 12px; color: #999; }
.ad-disabled-placeholder { height: 200px; display: flex; align-items: center; justify-content: center; border: 1px dashed #eee; border-radius: 8px; }
.disabled-txt { color: #999; font-size: 14px; }
.anim-fade-in { animation: fadeIn 0.4s ease-out; }
@keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
</style>

View File

@@ -53,7 +53,7 @@
</view>
<!-- 样式1 会员卡 -->
<view v-if="selectedStyle === 1" class="member-card-s1">
<view v-if="selectedStyle === 1" class="member-card-s1" @click="handleMember">
<view class="mc-content-s1">
<view class="mc-left">
<text class="mc-ic">👑</text>
@@ -66,7 +66,7 @@
</view>
<!-- 样式2 会员卡 -->
<view v-if="selectedStyle === 2" class="member-card-s2">
<view v-if="selectedStyle === 2" class="member-card-s2" @click="handleMember">
<view class="mc-content-s2">
<view class="mc-left">
<text class="mc-ic">👑</text>
@@ -76,7 +76,7 @@
</view>
</view>
<view class="mc-right">
<text class="mc-btn-white">立即续费</text>
<text class="mc-btn-white">立即续费 ></text>
</view>
</view>
</view>
@@ -116,17 +116,17 @@
<text class="stat-label-s3">优惠券</text>
</view>
</view>
</view>
<!-- 样式3 会员卡 -->
<view class="member-card-s3">
<view class="mc-content-s3">
<view class="mct-left-s3">
<text class="mct-ic-s3">👑</text>
<text class="mct-txt-s3">开通会员VIP</text>
</view>
<view class="mct-right-s3">
<text class="mct-more-s3">会员可享多项权益 ></text>
</view>
<!-- 样式3 会员卡 -->
<view v-if="selectedStyle === 3" class="member-card-s3" @click="handleMember">
<view class="mc-content-s3">
<view class="mct-left-s3">
<text class="mct-ic-s3">👑</text>
<text class="mct-txt-s3">开通会员VIP</text>
</view>
<view class="mct-right-s3">
<text class="mct-more-s3">会员可享多项权益 ></text>
</view>
</view>
</view>
@@ -253,6 +253,10 @@ const merchantItems = [
const handleSave = () => {
uni.showToast({ title: '保存成功' })
}
const handleMember = () => {
uni.showToast({ title: '会员功能开发中' })
}
</script>
<style scoped lang="scss">
@@ -326,11 +330,17 @@ const handleSave = () => {
/* 样式1&2 头部渐变 */
.user-header-gradient {
background: linear-gradient(135deg, #eb3c2d 0%, #ff5e5e 100%);
padding: 25px 15px 15px;
padding: 25px 0 12px;
position: relative;
}
.header-top { display: flex; flex-direction: row; align-items: center; margin-bottom: 20px; }
.header-top {
display: flex;
flex-direction: row;
align-items: center;
margin-bottom: 20px;
padding: 0 15px;
}
.avatar-box { width: 50px; height: 50px; border-radius: 25px; border: 2px solid rgba(255,255,255,0.8); overflow: hidden; margin-right: 12px; }
.avatar-img { width: 100%; height: 100%; }
@@ -339,11 +349,11 @@ const handleSave = () => {
.bind-phone { background-color: rgba(0,0,0,0.15); align-self: flex-start; padding: 2px 8px; border-radius: 10px; }
.bind-txt { color: #fff; font-size: 10px; }
.header-icons { display: flex; flex-direction: row; gap: 15px; }
.header-icons { display: flex; flex-direction: row; gap: 15px; padding: 0 15px; }
.ic-msg, .ic-set { font-size: 16px; color: #fff; position: relative; }
.msg-dot { position: absolute; top: -5px; right: -5px; background-color: #fff; color: #f2270c; font-size: 9px; width: 12px; height: 12px; border-radius: 6px; text-align: center; }
.stats-row { display: flex; flex-direction: row; justify-content: space-around; padding: 10px 0; margin-bottom: 10px; }
.stats-row { display: flex; flex-direction: row; justify-content: space-around; padding: 10px 15px; margin-bottom: 0; }
.stat-item { display: flex; flex-direction: column; align-items: center; }
.stat-val { font-size: 16px; font-weight: bold; color: #fff; margin-bottom: 4px; }
.stat-label { font-size: 10px; color: rgba(255,255,255,0.8); }
@@ -351,9 +361,9 @@ const handleSave = () => {
/* 会员卡 样式1 */
.member-card-s1 {
background: linear-gradient(90deg, #fdf1d6 0%, #fbd795 100%);
margin: 0 -5px;
border-radius: 12px 12px 0 0;
padding: 15px;
margin: 12px 10px 4px;
border-radius: 12px;
padding: 15px 16px;
}
.mc-content-s1 {
display: flex;
@@ -367,12 +377,10 @@ const handleSave = () => {
/* 会员卡 样式2 */
.member-card-s2 {
background-color: rgba(255,255,255,0.25);
margin: 0 -5px;
border-radius: 12px 12px 0 0;
padding: 15px;
border-top: 1px solid rgba(255,255,255,0.3);
border-left: 1px solid rgba(255,255,255,0.3);
border-right: 1px solid rgba(255,255,255,0.3);
margin: 12px 10px 4px;
border-radius: 12px;
padding: 15px 16px;
border: 1px solid rgba(255,255,255,0.3);
}
.mc-content-s2 {
display: flex;
@@ -388,7 +396,7 @@ const handleSave = () => {
/* 样式3 头部 */
.user-header-s3 {
background-color: #fff;
padding: 30px 20px 0;
padding: 30px 15px 0;
}
.header-top-s3 {
display: flex;
@@ -473,9 +481,9 @@ const handleSave = () => {
.member-card-s3 {
background: #282828;
margin: 0 -5px;
border-radius: 12px 12px 0 0;
padding: 18px 15px;
margin: 12px 10px;
border-radius: 12px;
padding: 18px 16px;
}
.mc-content-s3 {
display: flex;