refactor(decoration): 重构店铺装修组件配置并规范化环境变量
- 重构装修组件配置,新增 icon 与 group 分类字段 - 将组件"图片"重命名为"热区","图片轮播"重命名为"轮播图" - 新增搜索、公告、视频组件配置项 - 统一为各图片项添加 model 字段标识跳转模式 - 将装修页面组件间 pageGap 间距由 10 调整为 0 - 移除 im 模块中 VUE_APP_COMMON 变量,统一使用 VUE_APP_BUYER - 为 im 环境变量与配置文件添加详细注释说明
33
im/.env
@@ -1,10 +1,37 @@
|
||||
# ==============================================================================
|
||||
# lilishop-im 生产环境默认环境变量配置
|
||||
# 执行 npm run build 进行生产打包时,默认会加载此文件中的环境变量
|
||||
# 注意:以 VUE_APP_ 开头的变量会被 webpack.DefinePlugin 静态嵌入客户端代码中
|
||||
# 变量修改后需要重新构建/运行项目方可生效
|
||||
#
|
||||
# 【协议提示】
|
||||
# - 线上生产环境配置了 SSL/HTTPS 证书时,WebSocket 使用 wss:// 协议
|
||||
# - 本地开发环境未配置 SSL 证书时,WebSocket 必须使用 ws:// 协议,否则会连接失败
|
||||
# ==============================================================================
|
||||
|
||||
# 当前运行环境模式:production (生产环境)
|
||||
NODE_ENV=production
|
||||
|
||||
# 是否开启预览模式 (true: 开启, false: 关闭)
|
||||
VUE_APP_PREVIEW=false
|
||||
|
||||
# IM 服务的 HTTP 接口请求基准地址 (用于登录认证、好友、群组、聊天记录查询等)
|
||||
VUE_APP_API_BASE_URL=https://im-api.pickmall.cn
|
||||
|
||||
# IM 服务的 WebSocket 实时通讯地址 (支持 ws:// 或 wss://,生产环境已配置 SSL 证书使用 wss://;本地无证书环境必须使用 ws://)
|
||||
VUE_APP_WEB_SOCKET_URL=wss://im-api.pickmall.cn/lili/webSocket
|
||||
VUE_APP_COMMON=https://buyer-api.pickmall.cn
|
||||
|
||||
# 买家端业务接口基准地址 (用于获取买家用户信息、商品信息、订单信息等)
|
||||
VUE_APP_BUYER=https://buyer-api.pickmall.cn
|
||||
VUE_APP_SELLER=https://store-api.pickmall.cn
|
||||
|
||||
# 商家端业务接口基准地址 (用于获取店铺资料、商家客服相关信息等)
|
||||
VUE_APP_SELLER=https://admin-api.pickmall.cn
|
||||
|
||||
# IM 网站/系统标题名称 (显示在浏览器标题栏与系统头部)
|
||||
VUE_APP_WEBSITE_NAME="LiLi IM"
|
||||
|
||||
# PC 端商城前台首页及商品详情跳转前缀地址 (点击聊天中的商品卡片时跳转)
|
||||
VUE_APP_PC_URL=https://pc-b2b2c.pickmall.cn/
|
||||
VUE_APP_PC_STORE=https://store-b2b2c.pickmall.cn/
|
||||
|
||||
# PC 端商家管理后台跳转前缀地址 (点击聊天中的订单信息时跳转商家端订单页面)
|
||||
VUE_APP_PC_STORE=https://store-b2b2c.pickmall.cn/
|
||||
|
||||
@@ -1,10 +1,44 @@
|
||||
# ==============================================================================
|
||||
# lilishop-im 本地开发环境配置文件
|
||||
# 执行 npm run serve 启动本地开发服务时,会优先加载此文件并覆盖 .env 中的同名变量
|
||||
# 注意:以 VUE_APP_ 开头的变量会被 webpack.DefinePlugin 静态嵌入客户端代码中
|
||||
# 变量修改后需要重新启动开发服务方可生效
|
||||
#
|
||||
# 【重要提示】
|
||||
# 1. 本地联调如果使用本地或局域网服务(如 127.0.0.1 / localhost),WebSocket 必须使用 ws:// 协议!
|
||||
# 例如: ws://127.0.0.1:8885/lili/webSocket
|
||||
# 切勿在无 SSL 证书的本地环境下使用 wss://,否则会导致 WebSocket 握手失败、聊天无法连通!
|
||||
# 2. 只有在线上域名并配置了 SSL/HTTPS 证书的情况下才使用 wss:// 协议。
|
||||
# ==============================================================================
|
||||
|
||||
# 当前运行环境模式:development (本地开发环境)
|
||||
NODE_ENV=development
|
||||
|
||||
# 是否开启预览模式 (true: 开启, false: 关闭)
|
||||
VUE_APP_PREVIEW=false
|
||||
|
||||
# IM 服务的 HTTP 接口请求基准地址
|
||||
# 线上演示示例: https://im-api.pickmall.cn
|
||||
# 本地联调示例: http://127.0.0.1:8885
|
||||
VUE_APP_API_BASE_URL=https://im-api.pickmall.cn
|
||||
|
||||
# IM 服务的 WebSocket 实时通讯地址
|
||||
# 【重要提示】本地联调未配置 SSL 证书时务必使用 ws:// 协议,切勿写成 wss://!
|
||||
# 本地联调推荐配置: ws://127.0.0.1:8885/lili/webSocket
|
||||
# 线上已配置证书时: wss://im-api.pickmall.cn/lili/webSocket
|
||||
VUE_APP_WEB_SOCKET_URL=wss://im-api.pickmall.cn/lili/webSocket
|
||||
VUE_APP_COMMON=https://buyer-api.pickmall.cn
|
||||
|
||||
# 买家端业务接口基准地址 (本地联调示例: http://127.0.0.1:8888)
|
||||
VUE_APP_BUYER=https://buyer-api.pickmall.cn
|
||||
VUE_APP_SELLER=https://store-api.pickmall.cn
|
||||
|
||||
# 商家端业务接口基准地址 (本地联调示例: http://127.0.0.1:8889)
|
||||
VUE_APP_SELLER=https://admin-api.pickmall.cn
|
||||
|
||||
# IM 网站/系统标题名称 (显示在浏览器标题栏与系统头部)
|
||||
VUE_APP_WEBSITE_NAME="LiLi IM"
|
||||
|
||||
# PC 端商城前台首页及商品详情跳转前缀地址 (点击聊天中的商品卡片时跳转)
|
||||
VUE_APP_PC_URL=https://pc-b2b2c.pickmall.cn/
|
||||
VUE_APP_PC_STORE=https://store-b2b2c.pickmall.cn/
|
||||
|
||||
# PC 端商家管理后台跳转前缀地址 (点击聊天中的订单信息时跳转商家端订单页面)
|
||||
VUE_APP_PC_STORE=https://store-b2b2c.pickmall.cn/
|
||||
|
||||
@@ -1,40 +1,58 @@
|
||||
/**
|
||||
* ==============================================================================
|
||||
* Babel 转译器配置文件 (babel.config.js)
|
||||
* ==============================================================================
|
||||
*
|
||||
* 主要功能:
|
||||
* 1. 采用 Vue CLI 官方预设进行现代 JavaScript (ES6+) 向下兼容转译
|
||||
* 2. 集成 PrismJS 插件,用于在 IM 聊天对话框中展示富文本代码块时的多语言语法高亮
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
// Vue CLI 官方 Babel 预设,自动包含常用语法转换与 polyfill 支持
|
||||
presets: ["@vue/cli-plugin-babel/preset"],
|
||||
|
||||
plugins: [
|
||||
/**
|
||||
* PrismJS 语法高亮插件配置
|
||||
* babel-plugin-prismjs: 按需加载 PrismJS 的语言包和样式主题,优化打包体积
|
||||
*/
|
||||
[
|
||||
"prismjs",
|
||||
{
|
||||
// 需要按需引入并支持语法高亮的编程/标记语言列表
|
||||
languages: [
|
||||
"html",
|
||||
"css",
|
||||
"less",
|
||||
"javascript",
|
||||
"typescript",
|
||||
"json",
|
||||
"xml",
|
||||
"bash",
|
||||
"nginx",
|
||||
"sql",
|
||||
"docker",
|
||||
"php",
|
||||
"java",
|
||||
"go",
|
||||
"python",
|
||||
"ruby",
|
||||
"rust",
|
||||
"objectivec",
|
||||
"c",
|
||||
"csharp",
|
||||
"cpp",
|
||||
"lua",
|
||||
"shell",
|
||||
"vim",
|
||||
"yaml",
|
||||
"yml",
|
||||
"md",
|
||||
"erlang",
|
||||
"ini",
|
||||
"html", // HTML 标记语言
|
||||
"css", // CSS 样式表
|
||||
"less", // Less 预处理样式
|
||||
"javascript", // JavaScript 脚本语言
|
||||
"typescript", // TypeScript
|
||||
"json", // JSON 数据格式
|
||||
"xml", // XML 文档
|
||||
"bash", // Bash 脚本
|
||||
"nginx", // Nginx 服务器配置文件
|
||||
"sql", // 数据库 SQL 语句
|
||||
"docker", // Dockerfile 配置
|
||||
"php", // PHP 语言
|
||||
"java", // Java 语言
|
||||
"go", // Go 语言
|
||||
"python", // Python 语言
|
||||
"ruby", // Ruby 语言
|
||||
"rust", // Rust 语言
|
||||
"objectivec", // Objective-C 语言
|
||||
"c", // C 语言
|
||||
"csharp", // C# 语言
|
||||
"cpp", // C++ 语言
|
||||
"lua", // Lua 脚本
|
||||
"shell", // Shell 命令行脚本
|
||||
"vim", // Vim 配置与脚本
|
||||
"yaml", // YAML 配置
|
||||
"yml", // YML 配置
|
||||
"md", // Markdown 文本
|
||||
"erlang", // Erlang 语言
|
||||
"ini", // INI 配置文件
|
||||
],
|
||||
// 代码高亮使用的默认主题 (采用深色风格的 okaidia 主题)
|
||||
theme: "okaidia",
|
||||
},
|
||||
],
|
||||
|
||||
@@ -1,18 +1,28 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es6",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"src/*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist"
|
||||
],
|
||||
"include": [
|
||||
"src/**/*"
|
||||
]
|
||||
}
|
||||
// ==============================================================================
|
||||
// JavaScript 项目语言服务配置文件 (jsconfig.json)
|
||||
// 用于指导 VS Code 等 IDE 进行代码跳转、智能提示和路径别名映射解析
|
||||
// ==============================================================================
|
||||
"compilerOptions": {
|
||||
// 指定编译后的 JavaScript 目标语言版本
|
||||
"target": "es6",
|
||||
// 解析非相对模块名称的基准目录(当前根目录)
|
||||
"baseUrl": ".",
|
||||
// 配置模块名到基于 baseUrl 的路径映射(与 vue.config.js 中的 webpack alias 保持一致)
|
||||
"paths": {
|
||||
// 将 @/* 映射至 src/*,以便在编辑器中编写 import ... from '@/...' 时支持快捷跳转
|
||||
"@/*": [
|
||||
"src/*"
|
||||
]
|
||||
}
|
||||
},
|
||||
// 排除在代码智能索引和语法检查之外的目录
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist"
|
||||
],
|
||||
// 包含在智能提示与类型检查范围内的源文件匹配规则
|
||||
"include": [
|
||||
"src/**/*"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,30 +1,74 @@
|
||||
|
||||
/**
|
||||
* 读取公共配置API路径。
|
||||
* 工程会读取根目录下的 env 。
|
||||
* 根目录下的.env文件是用来定义环境变量的。
|
||||
* 它可以包含各种键值对,每个键代表一个环境变量。
|
||||
* 这些环境变量可以在应用程序的整个生命周期内使用,例如在代码中访问、配置webpack或在构建部署过程中使用不同的值。
|
||||
* env中的 ODE_ENV指定当前的环境。
|
||||
* 可以分别创建.env.development、.env.production等文件。
|
||||
* 默认.env 文件中的变量作为开发环境的环境变量。
|
||||
* env文件修改的话需要重新编译项目。
|
||||
* ==============================================================================
|
||||
* 全局业务配置文件
|
||||
* ==============================================================================
|
||||
*
|
||||
* 说明:
|
||||
* 1. 本文件整合并导出了项目运行所需的全局配置参数,统一由环境变量 process.env 读取。
|
||||
* 2. 环境变量定义在项目根目录下的 .env(生产)与 .env.development(开发)文件中。
|
||||
* 3. 只有以 VUE_APP_ 开头的环境变量才会被 Vue CLI 编译并暴露给前端客户端代码。
|
||||
* 4. 修改 .env 或 .env.development 文件后,必须重新启动开发服务或重新打包构建方可生效。
|
||||
*
|
||||
* 核心配置项包含:
|
||||
* - 站点名称
|
||||
* - 后端 IM 服务 API 接口与 WebSocket 实时通讯地址
|
||||
* - 买家端/商家端跨系统接口基准地址
|
||||
* - PC 端前台商城与商家端管理后台的外链跳转地址
|
||||
*/
|
||||
export default {
|
||||
// 网站名称
|
||||
/**
|
||||
* 站点/系统标题名称
|
||||
* 用于页面 document.title 设置及系统头部品牌展示
|
||||
* 默认值: "LiLi IM"
|
||||
*/
|
||||
WEBSITE_NAME: process.env.VUE_APP_WEBSITE_NAME || "LiLi IM",
|
||||
// 默认请求IM的API
|
||||
BASE_API_URL: process.env.VUE_APP_API_BASE_URL || "",
|
||||
// 默认请求的WS
|
||||
BASE_WS_URL: process.env.VUE_APP_WEB_SOCKET_URL || "",
|
||||
// 默认请求公有接口相关 API
|
||||
BASE_COMMON: process.env.VUE_APP_COMMON || "",
|
||||
// 默认请求用户相关API
|
||||
BASE_BUYER: process.env.VUE_APP_BUYER || "",
|
||||
// 默认请求卖家相关的API
|
||||
|
||||
/**
|
||||
* IM 专属后端 HTTP API 接口基准请求地址
|
||||
* 用于 Axios 请求实例 (如获取会话历史、发送单聊/群聊富文本、撤回消息、好友列表等)
|
||||
* 对应环境变量: VUE_APP_API_BASE_URL
|
||||
*/
|
||||
BASE_API_URL: process.env.VUE_APP_API_BASE_URL || "",
|
||||
|
||||
/**
|
||||
* IM 实时通讯 WebSocket 连接基准地址
|
||||
* 用于前端与后端建立长连接,实时收发在线聊天消息、输入状态提示、系统通知等
|
||||
*
|
||||
* 【重要提示】协议选用原则:
|
||||
* - 本地开发/联调环境(无 SSL 证书)必须使用 "ws://" 协议(如 ws://127.0.0.1:8885/lili/webSocket),切勿写成 wss:// 导致握手失败!
|
||||
* - 生产/线上环境配置了 HTTPS/SSL 证书时,必须使用 "wss://" 协议(如 wss://im-api.pickmall.cn/lili/webSocket)
|
||||
*
|
||||
* 对应环境变量: VUE_APP_WEB_SOCKET_URL
|
||||
*/
|
||||
BASE_WS_URL: process.env.VUE_APP_WEB_SOCKET_URL || "",
|
||||
|
||||
/**
|
||||
* 买家端 (Buyer) 接口基准地址
|
||||
* 用于在聊天窗口中查询买家用户信息、买家端商品详情、买家订单卡片数据等
|
||||
* 对应环境变量: VUE_APP_BUYER
|
||||
*/
|
||||
BASE_BUYER: process.env.VUE_APP_BUYER || "",
|
||||
|
||||
/**
|
||||
* 商家端/卖家端 (Seller) 接口基准地址
|
||||
* 用于获取当前店铺信息、客服坐席列表、商家端关联配置等
|
||||
* 对应环境变量: VUE_APP_SELLER
|
||||
*/
|
||||
BASE_SELLER: process.env.VUE_APP_SELLER || "",
|
||||
// 点击商品跳转到的地址
|
||||
|
||||
/**
|
||||
* PC 买家端商城首页/商品详情页面跳转基准前缀
|
||||
* 当用户在聊天窗口中点击商品链接或商品卡片时,在新窗口打开对应的商城商品详情页
|
||||
* 默认值: "https://pc-b2b2c.pickmall.cn"
|
||||
* 对应环境变量: VUE_APP_PC_URL
|
||||
*/
|
||||
PC_URL: process.env.VUE_APP_PC_URL || "https://pc-b2b2c.pickmall.cn",
|
||||
// 订单跳转商家订单页面
|
||||
PC_STORE: process.env.VUE_APP_PC_STORE || 'https://store-b2b2c.pickmall.cn',
|
||||
|
||||
/**
|
||||
* PC 商家管理后台跳转基准前缀
|
||||
* 当客服人员在聊天面板中点击买家咨询的订单卡片时,跳转至商家管理后台对应的订单详情页
|
||||
* 默认值: "https://store-b2b2c.pickmall.cn"
|
||||
* 对应环境变量: VUE_APP_PC_STORE
|
||||
*/
|
||||
PC_STORE: process.env.VUE_APP_PC_STORE || "https://store-b2b2c.pickmall.cn",
|
||||
};
|
||||
|
||||
@@ -154,7 +154,7 @@ export const del = (url, data = {}, options = {}) => {
|
||||
*/
|
||||
export const upload = (url, data = {}, options = {}) => {
|
||||
return request({
|
||||
url: config.BASE_COMMON + url,
|
||||
url: config.BASE_BUYER + url,
|
||||
method: "post",
|
||||
data: data,
|
||||
...options,
|
||||
|
||||
133
im/vue.config.js
@@ -1,58 +1,118 @@
|
||||
/**
|
||||
* ==============================================================================
|
||||
* Vue CLI 配置文件 (vue.config.js)
|
||||
* ==============================================================================
|
||||
*
|
||||
* 官方文档: https://cli.vuejs.org/zh/config/
|
||||
* 主要配置功能:
|
||||
* 1. 部署子路径与打包输出目录设置
|
||||
* 2. 本地开发服务器 (devServer) 端口及代理跨域规则
|
||||
* 3. Gzip 打包压缩插件 (CompressionWebpackPlugin) 配置
|
||||
* 4. SVG 图标雪碧图 (svg-sprite-loader) 解析
|
||||
* 5. 全局 Less 样式变量自动注入 (style-resources-loader)
|
||||
* 6. 构建优化与 SourceMap 控制
|
||||
*/
|
||||
|
||||
const path = require('path')
|
||||
|
||||
// compression-webpack-plugin,添加压缩文件类型
|
||||
// 引入 Gzip 压缩插件,用于生产环境减小打包产物体积,提升加载速度
|
||||
const CompressionWebpackPlugin = require('compression-webpack-plugin')
|
||||
|
||||
// 需要进行 Gzip 压缩的文件后缀类型
|
||||
const productionGzipExtensions = ['js', 'css']
|
||||
|
||||
// 判断当前是否为生产构建环境
|
||||
const isProd = process.env.NODE_ENV === 'production'
|
||||
|
||||
/**
|
||||
* 路径转换辅助函数:将相对路径转换为绝对路径
|
||||
* @param {string} dir 相对 src 或根目录的路径
|
||||
* @returns {string} 绝对路径
|
||||
*/
|
||||
function resolve(dir) {
|
||||
return path.join(__dirname, dir)
|
||||
}
|
||||
|
||||
// vue.config.js
|
||||
const vueConfig = {
|
||||
// 公共路径(必须有的)
|
||||
/**
|
||||
* 部署应用包时的基本 URL (部署路径)
|
||||
* 默认使用根路径 '/',若部署在二级子目录下(例如 '/im/'),可通过环境变量 VUE_APP_PUBLIC_PATH 指定
|
||||
*/
|
||||
publicPath: process.env.VUE_APP_PUBLIC_PATH || '/',
|
||||
// 输出文件目录
|
||||
|
||||
/**
|
||||
* 生产环境构建输出文件的目录名称 (执行 npm run build 后的打包产物目录)
|
||||
*/
|
||||
outputDir: 'dist',
|
||||
// 静态资源存放的文件夹(相对于ouputDir)
|
||||
|
||||
/**
|
||||
* 放置生成的静态资源 (js、css、img、fonts) 的相对于 outputDir 的目录
|
||||
*/
|
||||
assetsDir: 'static',
|
||||
|
||||
/**
|
||||
* 是否使用包含运行时编译器的 Vue 构建版本
|
||||
* 设置为 false 可以减少打包体积约 10KB
|
||||
*/
|
||||
runtimeCompiler: false,
|
||||
|
||||
/**
|
||||
* 本地开发服务器配置
|
||||
*/
|
||||
devServer: {
|
||||
// 本地开发运行端口号,默认为 8000
|
||||
port: 8000,
|
||||
|
||||
/**
|
||||
* 本地开发跨域代理配置 (Proxy)
|
||||
* 若本地直接请求后端 API 遇到跨域 (CORS) 限制,可在此开启反向代理
|
||||
*/
|
||||
// proxy: {
|
||||
// //配置跨域
|
||||
// '/api': {
|
||||
// target: 'http://api.xxxx.com', // 后台接口域名
|
||||
// ws: false, //如果要代理 websockets,配置这个参数
|
||||
// secure: false, // 如果是https接口,需要配置这个参数
|
||||
// changeOrigin: true, //是否跨域
|
||||
// target: 'http://127.0.0.1:8885', // 后台 IM 接口域名或 IP
|
||||
// ws: true, // 是否代理 websockets
|
||||
// secure: false, // 若为 https 自签名证书接口设为 false
|
||||
// changeOrigin: true, // 是否允许跨域改变 host 来源
|
||||
// pathRewrite: {
|
||||
// '^/api': '/api',
|
||||
// '^/api': '/api', // 路径重写规则
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
},
|
||||
|
||||
/**
|
||||
* Webpack 原生对象式配置
|
||||
*/
|
||||
configureWebpack: {
|
||||
// webpack plugins
|
||||
// Webpack 插件列表
|
||||
plugins: [
|
||||
// 配置大文件压缩相关参数
|
||||
/**
|
||||
* 配置 Gzip 压缩插件
|
||||
* 对体积大于 10KB 的 JS/CSS 文件生成对应的 .gz 预压缩文件
|
||||
* 需配合 Nginx 的 gzip_static on 指令直接下发,减少服务器 CPU 动态压缩消耗
|
||||
*/
|
||||
new CompressionWebpackPlugin({
|
||||
filename: '[path][base].gz',
|
||||
algorithm: 'gzip',
|
||||
test: new RegExp('\\.(' + productionGzipExtensions.join('|') + ')$'),
|
||||
threshold: 10240,
|
||||
minRatio: 0.8,
|
||||
filename: '[path][base].gz', // 压缩后的文件命名规则
|
||||
algorithm: 'gzip', // 压缩算法
|
||||
test: new RegExp('\\.(' + productionGzipExtensions.join('|') + ')$'), // 匹配要压缩的文件类型
|
||||
threshold: 10240, // 仅对超过 10KB 的资源进行压缩 (单位: 字节)
|
||||
minRatio: 0.8, // 只有压缩率好于 0.8 时才生成压缩文件
|
||||
}),
|
||||
],
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* Webpack 链式配置 (Chain Webpack),细粒度修改内部 loader 与规则
|
||||
*/
|
||||
chainWebpack: config => {
|
||||
// 1. 配置路径别名:设置 '@' 指向 'src' 目录,便于在代码中快速引入组件和工具库
|
||||
config.resolve.alias.set('@', resolve('src'))
|
||||
|
||||
// 2. 配置内置 svg 规则:排除 src/icons/svg 目录,避免与下面的 svg-sprite-loader 冲突
|
||||
config.module.rule('svg').exclude.add(resolve('src/icons/svg')).end()
|
||||
|
||||
// 3. 配置自定义 svg-icon 规则:将 src/icons/svg 下的 SVG 文件打包为 SVG Sprite 雪碧图
|
||||
// 可以在项目中使用 <svg-icon icon-class="icon-name" /> 统一渲染矢量图标
|
||||
config.module
|
||||
.rule('icons')
|
||||
.test(/\.svg$/)
|
||||
@@ -61,35 +121,54 @@ const vueConfig = {
|
||||
.use('svg-sprite-loader')
|
||||
.loader('svg-sprite-loader')
|
||||
.options({
|
||||
symbolId: 'icon-[name]',
|
||||
symbolId: 'icon-[name]', // 生成的 symbol ID 命名规则
|
||||
})
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 第三方插件配置项
|
||||
*/
|
||||
pluginOptions: {
|
||||
// 自动在所有单文件组件中引入 Less 全局变量,无需在每个 .vue 文件手动 @import
|
||||
'style-resources-loader': {
|
||||
preProcessor: 'less',
|
||||
patterns: [
|
||||
//全局加载 less 变量
|
||||
// 全局 Less 主题变量文件绝对路径
|
||||
path.resolve(__dirname, './src/assets/css/variable.less'),
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* CSS 预处理器相关配置
|
||||
*/
|
||||
css: {
|
||||
loaderOptions: {
|
||||
less: {
|
||||
lessOptions: {
|
||||
modifyVars: {},
|
||||
javascriptEnabled: true,
|
||||
modifyVars: {}, // Less 变量覆盖
|
||||
javascriptEnabled: true, // 允许 Less 中执行 JavaScript 表达式 (Ant Design / 部分组件库必需)
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// 是否为生产环境构建生成 source map?
|
||||
/**
|
||||
* 是否为生产环境构建生成 Source Map
|
||||
* 设置为 false 可以大幅加速生产打包速度,并防止源码泄露
|
||||
*/
|
||||
productionSourceMap: false,
|
||||
// lintOnSave: undefined,
|
||||
|
||||
/**
|
||||
* 是否在保存代码时通过 eslint-loader 执行检查
|
||||
* 设置为 false 关闭保存时的编译中断
|
||||
*/
|
||||
lintOnSave: false,
|
||||
// babel-loader no-ignore node_modules/*
|
||||
|
||||
/**
|
||||
* 默认情况下 babel-loader 会忽略所有 node_modules 中的文件
|
||||
* 若依赖库使用了 ES6+ 特性需要被转译,可在此数组中显式配置该依赖名称
|
||||
*/
|
||||
transpileDependencies: [],
|
||||
}
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 124 KiB |
|
Before Width: | Height: | Size: 149 KiB |
|
Before Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 111 KiB |
|
Before Width: | Height: | Size: 69 KiB |
|
Before Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 6.5 KiB |
|
Before Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 24 KiB |
@@ -114,7 +114,7 @@ const DEFAULTS = {
|
||||
paddingTop: 10,
|
||||
paddingBottom: 10,
|
||||
paddingX: 12,
|
||||
pageGap: 10,
|
||||
pageGap: 0,
|
||||
contentGap: 0,
|
||||
borderRadius: 0,
|
||||
imgRadius: 0,
|
||||
|
||||
@@ -98,7 +98,7 @@ const DEFAULTS = {
|
||||
paddingBottom: 10,
|
||||
contentGap: 10,
|
||||
paddingX: 0,
|
||||
pageGap: 10,
|
||||
pageGap: 0,
|
||||
borderRadius: 0,
|
||||
};
|
||||
const DEFAULT_ITEM = { title: "文章列表", categoryId: "", count: 6 };
|
||||
|
||||
@@ -37,7 +37,7 @@ const DEFAULTS = {
|
||||
bgColor2: "#f5f5f5",
|
||||
height: 10,
|
||||
paddingX: 0,
|
||||
pageGap: 10,
|
||||
pageGap: 0,
|
||||
};
|
||||
|
||||
export default {
|
||||
|
||||
@@ -89,7 +89,7 @@ const DEFAULTS = {
|
||||
paddingBottom: 10,
|
||||
contentGap: 5,
|
||||
paddingX: 12,
|
||||
pageGap: 10,
|
||||
pageGap: 0,
|
||||
borderRadius: 0,
|
||||
};
|
||||
const DEFAULT_ITEM = { title: "优惠券", count: 5 };
|
||||
|
||||
@@ -108,7 +108,7 @@ const DEFAULTS = {
|
||||
paddingTop: 10,
|
||||
paddingBottom: 10,
|
||||
paddingX: 12,
|
||||
pageGap: 10,
|
||||
pageGap: 0,
|
||||
borderRadius: 0,
|
||||
imgRadius: 0,
|
||||
};
|
||||
|
||||
@@ -339,7 +339,7 @@ const DEFAULTS = {
|
||||
paddingBottom: 10,
|
||||
paddingX: 12,
|
||||
contentGap: 10,
|
||||
pageGap: 10,
|
||||
pageGap: 0,
|
||||
borderRadius: 0,
|
||||
contentRadius: 7,
|
||||
};
|
||||
|
||||
@@ -47,7 +47,7 @@ const DEFAULTS = {
|
||||
lineStyle: "dashed",
|
||||
lineHeight: 1,
|
||||
paddingX: 0,
|
||||
pageGap: 10,
|
||||
pageGap: 0,
|
||||
};
|
||||
|
||||
export default {
|
||||
|
||||
@@ -179,7 +179,7 @@ const DEFAULTS = {
|
||||
paddingBottom: 10,
|
||||
paddingX: 12,
|
||||
contentGap: 10,
|
||||
pageGap: 10,
|
||||
pageGap: 0,
|
||||
borderRadius: 0,
|
||||
imgRadius: 30,
|
||||
};
|
||||
|
||||
@@ -164,7 +164,7 @@ const DEFAULTS = {
|
||||
paddingTop: 0,
|
||||
paddingBottom: 0,
|
||||
paddingX: 12,
|
||||
pageGap: 10,
|
||||
pageGap: 0,
|
||||
borderRadius: 0,
|
||||
itemColor: "#333333",
|
||||
};
|
||||
|
||||
@@ -231,7 +231,7 @@ const DEFAULTS = {
|
||||
paddingBottom: 10,
|
||||
contentGap: 10,
|
||||
paddingX: 12,
|
||||
pageGap: 10,
|
||||
pageGap: 0,
|
||||
borderRadius: 0,
|
||||
imgRadius: 10,
|
||||
showGoodsName: true,
|
||||
|
||||
@@ -58,7 +58,7 @@ const DEFAULTS = {
|
||||
bgColor1: "#FFFFFF",
|
||||
bgColor2: "#FFFFFF",
|
||||
paddingX: 12,
|
||||
pageGap: 10,
|
||||
pageGap: 0,
|
||||
borderRadius: 0,
|
||||
};
|
||||
|
||||
|
||||
@@ -228,7 +228,7 @@ const DEFAULTS = {
|
||||
paddingTop: 10,
|
||||
paddingBottom: 10,
|
||||
paddingX: 0,
|
||||
pageGap: 10,
|
||||
pageGap: 0,
|
||||
borderRadius: 0,
|
||||
imgRadius: 7,
|
||||
};
|
||||
|
||||
@@ -157,7 +157,7 @@ const DEFAULTS = {
|
||||
paddingTop: 10,
|
||||
paddingBottom: 10,
|
||||
paddingX: 12,
|
||||
pageGap: 10,
|
||||
pageGap: 0,
|
||||
radiusTop: 0,
|
||||
radiusBottom: 0,
|
||||
color: "#282828",
|
||||
|
||||
@@ -115,7 +115,7 @@ const DEFAULTS = {
|
||||
paddingTop: 10,
|
||||
paddingBottom: 10,
|
||||
paddingX: 12,
|
||||
pageGap: 10,
|
||||
pageGap: 0,
|
||||
borderRadius: 0,
|
||||
imgRadius: 0,
|
||||
};
|
||||
|
||||
@@ -851,7 +851,7 @@ export const modelData = [
|
||||
bgColor: "#f5f5f5",
|
||||
height: 10,
|
||||
paddingX: 0,
|
||||
pageGap: 10
|
||||
pageGap: 0
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -866,7 +866,7 @@ export const modelData = [
|
||||
lineStyle: "dashed",
|
||||
lineHeight: 1,
|
||||
paddingX: 0,
|
||||
pageGap: 10
|
||||
pageGap: 0
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -879,7 +879,7 @@ export const modelData = [
|
||||
content: "",
|
||||
bgColor: "#FFFFFF",
|
||||
paddingX: 12,
|
||||
pageGap: 10,
|
||||
pageGap: 0,
|
||||
borderRadius: 0
|
||||
}
|
||||
},
|
||||
@@ -988,7 +988,7 @@ export const modelData = [
|
||||
list: [{ title: "文章列表", categoryId: "", count: 3 }],
|
||||
bgColor: "#FFFFFF",
|
||||
paddingX: 0,
|
||||
pageGap: 10,
|
||||
pageGap: 0,
|
||||
borderRadius: 0
|
||||
}
|
||||
},
|
||||
@@ -1002,7 +1002,7 @@ export const modelData = [
|
||||
list: [{ title: "店铺列表", count: 4 }],
|
||||
bgColor: "#FFFFFF",
|
||||
paddingX: 0,
|
||||
pageGap: 10,
|
||||
pageGap: 0,
|
||||
borderRadius: 0
|
||||
}
|
||||
},
|
||||
@@ -1044,7 +1044,7 @@ export const modelData = [
|
||||
paddingBottom: 10,
|
||||
paddingX: 12,
|
||||
contentGap: 10,
|
||||
pageGap: 10,
|
||||
pageGap: 0,
|
||||
borderRadius: 0,
|
||||
contentRadius: 7
|
||||
}
|
||||
|
||||
@@ -34,12 +34,13 @@
|
||||
<el-button size="default" type="primary" @click="handleSpinShow">保存页面</el-button>
|
||||
|
||||
<el-dialog
|
||||
title="保存中"
|
||||
:title="progress ? '保存页面' : '保存中'"
|
||||
v-model="saveDialog"
|
||||
:show-close="true"
|
||||
:close-on-click-modal="false"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
@closed="handleDialogClosed"
|
||||
>
|
||||
<div v-if="progress">
|
||||
<div class="model-item">
|
||||
@@ -56,7 +57,7 @@
|
||||
</div>
|
||||
<div class="model-item">
|
||||
将当前装修内容写入到本地缓存中,下次进入页面时可继续使用
|
||||
<el-button type="small" @click="witeLocalStore">写入</el-button>
|
||||
<el-button size="small" @click="witeLocalStore">写入</el-button>
|
||||
</div>
|
||||
|
||||
<el-button type="primary" @click="save()">保存</el-button>
|
||||
@@ -84,7 +85,7 @@ const appStore = useAppStore();
|
||||
|
||||
const hasCache = ref(false); // 是否有缓存
|
||||
const progress = ref(true); // 展示进度
|
||||
const num = ref(20); // 提交进度
|
||||
const num = ref(0); // 提交进度
|
||||
const saveDialog = ref(false); // 加载状态
|
||||
const way = ref([
|
||||
// 装修tab栏切换
|
||||
@@ -119,10 +120,17 @@ function startEditName() {
|
||||
|
||||
const submitWay = reactive({
|
||||
// 表单信息
|
||||
pageShow: route.query.type || false,
|
||||
pageShow: route.query.type === "true" || route.query.type === true || route.query.type === "OPEN",
|
||||
pageClientType: "H5"
|
||||
});
|
||||
|
||||
watch(
|
||||
() => route.query.type,
|
||||
(val) => {
|
||||
submitWay.pageShow = val === "true" || val === true || val === "OPEN";
|
||||
}
|
||||
);
|
||||
|
||||
// 模板名称与「页面设置」面板共用 store 中的同一份数据,双向同步
|
||||
const pageName = computed({
|
||||
get() {
|
||||
@@ -191,9 +199,16 @@ function clickBtn(val) {
|
||||
* 加载,并保存模板
|
||||
*/
|
||||
function handleSpinShow() {
|
||||
progress.value = true;
|
||||
num.value = 0;
|
||||
saveDialog.value = true;
|
||||
}
|
||||
|
||||
function handleDialogClosed() {
|
||||
progress.value = true;
|
||||
num.value = 0;
|
||||
}
|
||||
|
||||
// 填写是否发布,模板名称之后保存
|
||||
function save() {
|
||||
// 楼层装修页面(非开屏/弹窗广告)需校验页面设置的必填项
|
||||
@@ -220,49 +235,57 @@ function save() {
|
||||
return false;
|
||||
}
|
||||
|
||||
submitWay?.pageShow === true
|
||||
? (submitWay.pageShow = "OPEN")
|
||||
: (submitWay.pageShow = "CLOSE");
|
||||
|
||||
submitWay.name = pageName.value;
|
||||
submitWay.pageData = JSON.stringify(appStore.styleStore);
|
||||
submitWay.pageType = "INDEX";
|
||||
submitWay.pageType = route.query.pagetype || "INDEX";
|
||||
// submitWay.pageType = props.pagetype;
|
||||
const payload = {
|
||||
...submitWay,
|
||||
pageShow: submitWay.pageShow ? "OPEN" : "CLOSE",
|
||||
name: pageName.value,
|
||||
pageData: JSON.stringify(appStore.styleStore),
|
||||
pageType: route.query.pagetype || "INDEX",
|
||||
};
|
||||
|
||||
if (route.query.pagetype == 'ALERT' || route.query.pagetype == 'OPEN_SCREEN_ANIMATION') {
|
||||
update(submitWay);
|
||||
update(payload);
|
||||
} else {
|
||||
route.query.id ? updateHome() : submit(submitWay);
|
||||
route.query.id ? updateHome(payload) : submit(payload);
|
||||
}
|
||||
|
||||
// route.query.id ? update() : submit(submitWay);
|
||||
}
|
||||
|
||||
// 更新
|
||||
function updateHome() {
|
||||
function updateHome(payload) {
|
||||
progress.value = false;
|
||||
API_Other.updateHome(route.query.id, {
|
||||
num.value = 20;
|
||||
const data = payload || {
|
||||
pageData: JSON.stringify(appStore.styleStore),
|
||||
name: pageName.value,
|
||||
pageShow: submitWay.pageShow,
|
||||
pageType: submitWay.pageType,
|
||||
pageShow: submitWay.pageShow ? "OPEN" : "CLOSE",
|
||||
pageType: submitWay.pageType || route.query.pagetype || "INDEX",
|
||||
pageClientType: "H5",
|
||||
})
|
||||
};
|
||||
API_Other.updateHome(route.query.id, data)
|
||||
.then(res => {
|
||||
num.value = 50;
|
||||
num.value = 60;
|
||||
if (res.success) {
|
||||
num.value = 80;
|
||||
num.value = 100;
|
||||
/**制作保存成功动画¸ */
|
||||
setTimeout(() => {
|
||||
saveDialog.value = false;
|
||||
progress.value = true;
|
||||
num.value = 0;
|
||||
Message.success("修改成功");
|
||||
goBack();
|
||||
}, 1000);
|
||||
}, 800);
|
||||
} else {
|
||||
progress.value = true;
|
||||
num.value = 0;
|
||||
saveDialog.value = false;
|
||||
Message.error("修改失败,请稍后重试");
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
progress.value = true;
|
||||
num.value = 0;
|
||||
saveDialog.value = false;
|
||||
Message.error("修改失败,请稍后重试");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -274,6 +297,7 @@ function update(submitWay) {
|
||||
return false;
|
||||
}
|
||||
progress.value = false;
|
||||
num.value = 20;
|
||||
|
||||
let id = route.query.id;
|
||||
const pagetype = route.query.pagetype;
|
||||
@@ -283,29 +307,40 @@ function update(submitWay) {
|
||||
OPEN_SCREEN_ANIMATION: "OPEN_SCREEN_ANIMATION",
|
||||
}[pagetype ? pagetype : "INDEX"];
|
||||
|
||||
const payload = { ...submitWay };
|
||||
if (pagetype) {
|
||||
submitWay.pageData = JSON.stringify(
|
||||
payload.pageData = JSON.stringify(
|
||||
appStore.openStyleStore[0]
|
||||
);
|
||||
submitWay.pageType = pageType;
|
||||
payload.pageType = pageType;
|
||||
id = appStore.openStoreId;
|
||||
}
|
||||
|
||||
API_Other.updateHome(id, submitWay)
|
||||
API_Other.updateHome(id, payload)
|
||||
.then((res) => {
|
||||
num.value = 50;
|
||||
num.value = 60;
|
||||
if (res.success) {
|
||||
num.value = 80;
|
||||
num.value = 100;
|
||||
/**制作保存成功动画¸ */
|
||||
setTimeout(() => {
|
||||
saveDialog.value = false;
|
||||
progress.value = true;
|
||||
num.value = 0;
|
||||
Message.success("修改成功");
|
||||
goBack();
|
||||
}, 1000);
|
||||
}, 800);
|
||||
} else {
|
||||
progress.value = true;
|
||||
num.value = 0;
|
||||
saveDialog.value = false;
|
||||
Message.error("修改失败,请稍后重试");
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
progress.value = true;
|
||||
num.value = 0;
|
||||
saveDialog.value = false;
|
||||
Message.error("修改失败,请稍后重试");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -313,28 +348,42 @@ function update(submitWay) {
|
||||
|
||||
// 返回查询数据页面
|
||||
function goBack() {
|
||||
router.go(-1);
|
||||
if (window.history.state?.back) {
|
||||
router.go(-1);
|
||||
} else {
|
||||
window.close();
|
||||
}
|
||||
}
|
||||
|
||||
// 保存
|
||||
function submit(submitWay) {
|
||||
progress.value = false;
|
||||
num.value = 20;
|
||||
API_Other.setHomeSetup(submitWay)
|
||||
.then(res => {
|
||||
num.value = 50;
|
||||
num.value = 60;
|
||||
if (res.success) {
|
||||
num.value = 80;
|
||||
num.value = 100;
|
||||
/**制作保存成功动画¸ */
|
||||
setTimeout(() => {
|
||||
saveDialog.value = false;
|
||||
progress.value = true;
|
||||
num.value = 0;
|
||||
Message.success("保存成功");
|
||||
goBack();
|
||||
}, 1000);
|
||||
}, 800);
|
||||
} else {
|
||||
progress.value = true;
|
||||
num.value = 0;
|
||||
saveDialog.value = false;
|
||||
Message.error("保存失败,请稍后重试");
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
progress.value = true;
|
||||
num.value = 0;
|
||||
saveDialog.value = false;
|
||||
Message.error("保存失败,请稍后重试");
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
}
|
||||
.layout {
|
||||
padding: 8px;
|
||||
margin: 4px 0;
|
||||
margin: 0;
|
||||
background: #fff;
|
||||
}
|
||||
.layout,
|
||||
|
||||
@@ -58,7 +58,7 @@ export default {
|
||||
return {
|
||||
background: this.res.bgColor || "#FFFFFF",
|
||||
padding: `10px ${this.res.paddingX || 0}px`,
|
||||
marginBottom: `${this.res.pageGap !== undefined ? this.res.pageGap : 10}px`,
|
||||
marginBottom: `${this.res.pageGap !== undefined ? this.res.pageGap : 0}px`,
|
||||
borderRadius: `${this.res.borderRadius || 0}px`,
|
||||
overflow: "hidden",
|
||||
};
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
:style="{
|
||||
background: blankBg,
|
||||
height: px(res.height ?? 10),
|
||||
margin: `0 ${px(res.paddingX ?? 0)} ${px(res.pageGap ?? 10)}`,
|
||||
margin: `0 ${px(res.paddingX ?? 0)} ${px(res.pageGap ?? 0)}`,
|
||||
}"
|
||||
></div>
|
||||
</template>
|
||||
|
||||
@@ -145,7 +145,7 @@ export default {
|
||||
return {
|
||||
background: this.res.bgColor || "#FFFFFF",
|
||||
padding: `10px ${this.res.paddingX || 0}px`,
|
||||
marginBottom: `${this.res.pageGap !== undefined ? this.res.pageGap : 10}px`,
|
||||
marginBottom: `${this.res.pageGap !== undefined ? this.res.pageGap : 0}px`,
|
||||
borderRadius: `${this.res.borderRadius || 0}px`,
|
||||
overflow: "hidden",
|
||||
};
|
||||
|
||||
@@ -9,11 +9,11 @@
|
||||
/>
|
||||
<div v-else class="integral-head-title" :style="headTitleStyle">{{ title }}</div>
|
||||
<div class="integral-head-space"></div>
|
||||
<div class="integral-head-more" :style="moreStyle">更多 ></div>
|
||||
<div class="integral-head-more" :style="moreStyle">更多 ></div>
|
||||
</div>
|
||||
<!-- 老数据:原始四列样式 -->
|
||||
<div v-if="!styled" class="join-box">
|
||||
<div class="join-item" v-for="item in 4" :key="item">
|
||||
<div class="join-item" v-for="item in count" :key="item">
|
||||
<div class="item-img-box">
|
||||
<img
|
||||
class="item-img"
|
||||
@@ -71,6 +71,45 @@
|
||||
</template>
|
||||
<script setup>
|
||||
import { computed } from "vue";
|
||||
const INTEGRAL_DEMO = [
|
||||
{
|
||||
goodsName: "小米小爱音箱 Play 增强版 智能红外遥控",
|
||||
points: 1200,
|
||||
originalPrice: 149,
|
||||
thumbnail: "/demo/xiaomi-phone.png",
|
||||
},
|
||||
{
|
||||
goodsName: "Apple 20W USB-C 官方原装快充充电器",
|
||||
points: 1500,
|
||||
originalPrice: 149,
|
||||
thumbnail: "/demo/apple-phone.png",
|
||||
},
|
||||
{
|
||||
goodsName: "米家自动洗手机 套装 抑菌洗手液",
|
||||
points: 800,
|
||||
originalPrice: 79,
|
||||
thumbnail: "/demo/xiaomi-phone.png",
|
||||
},
|
||||
{
|
||||
goodsName: "多功能桌面风扇 超长续航三档风力",
|
||||
points: 600,
|
||||
originalPrice: 59,
|
||||
thumbnail: "/demo/vivo-phone.png",
|
||||
},
|
||||
{
|
||||
goodsName: "精品保温杯 316不锈钢 480ml",
|
||||
points: 990,
|
||||
originalPrice: 99,
|
||||
thumbnail: "/demo/apple-phone.png",
|
||||
},
|
||||
{
|
||||
goodsName: "无线双模鼠标 2.4G+蓝牙 轻音便携",
|
||||
points: 750,
|
||||
originalPrice: 69,
|
||||
thumbnail: "/demo/vivo-phone.png",
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
const props = defineProps(["res"]);
|
||||
|
||||
@@ -79,9 +118,27 @@ const title = computed(
|
||||
);
|
||||
const firstItem = computed(() => (props.res.list && props.res.list[0]) || {});
|
||||
const layoutStyle = computed(() => props.res.layoutStyle || "1");
|
||||
const count = computed(() => {
|
||||
const c = firstItem.value?.count ?? props.res?.count;
|
||||
if (c !== undefined && c !== null && !isNaN(Number(c)) && Number(c) > 0) {
|
||||
return Number(c);
|
||||
}
|
||||
return layoutStyle.value === "1" ? 4 : 6;
|
||||
});
|
||||
|
||||
const displayData = computed(() => {
|
||||
const data = (firstItem.value && firstItem.value.data) || [];
|
||||
return layoutStyle.value === "1" ? data.slice(0, 4) : data.slice(0, 6);
|
||||
const raw = (firstItem.value && firstItem.value.data) || [];
|
||||
const data = raw.length ? raw : INTEGRAL_DEMO;
|
||||
const targetCount = count.value;
|
||||
if (data.length >= targetCount) {
|
||||
return data.slice(0, targetCount);
|
||||
}
|
||||
// 如果设置的数量大于现有模拟数据长度,循环补齐,使预览数量与配置数量严格一致
|
||||
const result = [...data];
|
||||
while (result.length < targetCount) {
|
||||
result.push({ ...data[result.length % data.length] });
|
||||
}
|
||||
return result.slice(0, targetCount);
|
||||
});
|
||||
|
||||
// 面板保存过样式字段才启用新样式,老数据保持原样
|
||||
@@ -94,7 +151,7 @@ const wrapperStyle = computed(() => {
|
||||
return {
|
||||
background: res.bgColor || "#FFFFFF",
|
||||
padding: `0 ${res.paddingX || 0}px`,
|
||||
marginBottom: `${res.pageGap !== undefined ? res.pageGap : 10}px`,
|
||||
marginBottom: `${res.pageGap !== undefined ? res.pageGap : 0}px`,
|
||||
borderRadius: `${res.borderRadius || 0}px`,
|
||||
overflow: "hidden",
|
||||
};
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
:style="{
|
||||
background: lineBg,
|
||||
padding: `0 ${px(res.paddingX ?? 0)}`,
|
||||
marginBottom: px(res.pageGap ?? 10),
|
||||
marginBottom: px(res.pageGap ?? 0),
|
||||
}"
|
||||
>
|
||||
<div
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<span class="pt-success">{{ firstItem.groupSuccess }}</span>
|
||||
</template>
|
||||
<span class="pt-space"></span>
|
||||
<span class="pt-more" :style="moreStyle">更多 ></span>
|
||||
<span class="pt-more" :style="moreStyle">更多 ></span>
|
||||
</div>
|
||||
<div class="pt-body">
|
||||
<!-- 样式一:纵向商品行 -->
|
||||
@@ -154,7 +154,7 @@
|
||||
<span v-else class="lv-head-title">{{ firstItem.title || "精彩直播" }}</span>
|
||||
<span class="lv-head-sub" :style="titleStyle">{{ firstItem.title1 || "精彩抢先看" }}</span>
|
||||
<span class="lv-space"></span>
|
||||
<span class="lv-more" :style="moreStyle">更多 ></span>
|
||||
<span class="lv-more" :style="moreStyle">更多 ></span>
|
||||
</div>
|
||||
<div class="lv-head-en">Exciting live streaming</div>
|
||||
<!-- 单行:横向滑动卡片 -->
|
||||
@@ -201,7 +201,7 @@
|
||||
<span class="sk-divider"></span>
|
||||
<span class="sk-head-sub" :style="titleStyle">{{ firstItem.title1 || "邀请你的好友来参与" }}</span>
|
||||
<span class="sk-space"></span>
|
||||
<span class="sk-go">GO ></span>
|
||||
<span class="sk-go">GO ></span>
|
||||
</div>
|
||||
<!-- 样式一:横向滑动 -->
|
||||
<div v-if="layoutStyle === '1'" class="sk-scroll" :style="gapStyle">
|
||||
@@ -246,7 +246,7 @@
|
||||
<span class="sk-divider"></span>
|
||||
<span class="sk-head-sub" :style="titleStyle">{{ firstItem.title1 || "邀请好友来砍价" }}</span>
|
||||
<span class="sk-space"></span>
|
||||
<span class="sk-go">GO ></span>
|
||||
<span class="sk-go">GO ></span>
|
||||
</div>
|
||||
<!-- 样式一:横向滑动 -->
|
||||
<div v-if="layoutStyle === '1'" class="sk-scroll" :style="gapStyle">
|
||||
@@ -287,7 +287,7 @@
|
||||
<div v-if="styled && (res.icon || res.moreUrl)" class="promo-head">
|
||||
<img v-if="res.icon" class="promo-head-icon" :src="res.icon" alt="" />
|
||||
<div class="promo-head-space"></div>
|
||||
<div v-if="res.moreUrl" class="promo-more" :style="moreStyle">更多 ></div>
|
||||
<div v-if="res.moreUrl" class="promo-more" :style="moreStyle">更多 ></div>
|
||||
</div>
|
||||
<div class="join-list" :style="gapStyle">
|
||||
<div
|
||||
@@ -343,6 +343,155 @@
|
||||
</template>
|
||||
<script setup>
|
||||
import { computed } from "vue";
|
||||
const SECKILL_DEMO = [
|
||||
{
|
||||
goodsName: "iPhone 16 Pro 256GB 黑色钛金属",
|
||||
price: 6999,
|
||||
originalPrice: 8999,
|
||||
thumbnail: "/demo/apple-phone.png",
|
||||
},
|
||||
{
|
||||
goodsName: "Xiaomi 15 Pro 16GB+512GB 沉墨色",
|
||||
price: 4699,
|
||||
originalPrice: 5299,
|
||||
thumbnail: "/demo/xiaomi-phone.png",
|
||||
},
|
||||
{
|
||||
goodsName: "vivo X200 Pro 16GB+512GB 钛色",
|
||||
price: 4899,
|
||||
originalPrice: 5499,
|
||||
thumbnail: "/demo/vivo-phone.png",
|
||||
},
|
||||
{
|
||||
goodsName: "Apple Watch Series 10 46mm GPS款",
|
||||
price: 2699,
|
||||
originalPrice: 3199,
|
||||
thumbnail: "/demo/apple-phone.png",
|
||||
},
|
||||
{
|
||||
goodsName: "AirPods Pro (第二代) 配USB-C充电盒",
|
||||
price: 1399,
|
||||
originalPrice: 1899,
|
||||
thumbnail: "/demo/apple-phone.png",
|
||||
},
|
||||
{
|
||||
goodsName: "Xiaomi 平板 6S Pro 12.4英寸",
|
||||
price: 2799,
|
||||
originalPrice: 3299,
|
||||
thumbnail: "/demo/xiaomi-phone.png",
|
||||
},
|
||||
];
|
||||
|
||||
const PINTUAN_DEMO = [
|
||||
{
|
||||
goodsName: "iPhone 16 Pro 5G智能手机 256GB 白色钛金属",
|
||||
price: 7999,
|
||||
originalPrice: 8999,
|
||||
thumbnail: "/demo/apple-phone.png",
|
||||
groupNum: 2,
|
||||
joined: 128,
|
||||
},
|
||||
{
|
||||
goodsName: "Xiaomi 15 5G智能手机 12GB+256GB 白色",
|
||||
price: 3699,
|
||||
originalPrice: 4299,
|
||||
thumbnail: "/demo/xiaomi-phone.png",
|
||||
groupNum: 2,
|
||||
joined: 256,
|
||||
},
|
||||
{
|
||||
goodsName: "vivo X200 5G智能手机 12GB+256GB 宝石蓝",
|
||||
price: 3999,
|
||||
originalPrice: 4599,
|
||||
thumbnail: "/demo/vivo-phone.png",
|
||||
groupNum: 3,
|
||||
joined: 89,
|
||||
},
|
||||
{
|
||||
goodsName: "Apple AirPods 4 主动降噪无线耳机",
|
||||
price: 1099,
|
||||
originalPrice: 1399,
|
||||
thumbnail: "/demo/apple-phone.png",
|
||||
groupNum: 2,
|
||||
joined: 412,
|
||||
},
|
||||
{
|
||||
goodsName: "小米手环 9 Pro 晨曦金 金属机身",
|
||||
price: 349,
|
||||
originalPrice: 399,
|
||||
thumbnail: "/demo/xiaomi-phone.png",
|
||||
groupNum: 2,
|
||||
joined: 680,
|
||||
},
|
||||
{
|
||||
goodsName: "vivo Pad3 Pro 13英寸 3.1K 超清大屏",
|
||||
price: 2999,
|
||||
originalPrice: 3399,
|
||||
thumbnail: "/demo/vivo-phone.png",
|
||||
groupNum: 2,
|
||||
joined: 195,
|
||||
},
|
||||
];
|
||||
|
||||
const KANJIA_DEMO = [
|
||||
{
|
||||
goodsName: "米家智能空气炸锅 6.5L 嫩烤大容量",
|
||||
purchasePrice: 0,
|
||||
price: 0,
|
||||
originalPrice: 299,
|
||||
thumbnail: "/demo/xiaomi-phone.png",
|
||||
},
|
||||
{
|
||||
goodsName: "电动声波牙刷 T300 高频震动洁齿",
|
||||
purchasePrice: 0,
|
||||
price: 0,
|
||||
originalPrice: 129,
|
||||
thumbnail: "/demo/vivo-phone.png",
|
||||
},
|
||||
{
|
||||
goodsName: "小米随身手持小风扇 大风力轻便便携",
|
||||
purchasePrice: 0,
|
||||
price: 0,
|
||||
originalPrice: 59,
|
||||
thumbnail: "/demo/xiaomi-phone.png",
|
||||
},
|
||||
{
|
||||
goodsName: "智能温湿度计 电子墨水屏高精度",
|
||||
purchasePrice: 0,
|
||||
price: 0,
|
||||
originalPrice: 69,
|
||||
thumbnail: "/demo/apple-phone.png",
|
||||
},
|
||||
];
|
||||
|
||||
const LIVE_DEMO = [
|
||||
{
|
||||
cover: "/demo/apple-phone.png",
|
||||
title: "双11新品首发专场 抢先看",
|
||||
anchorName: "官方品牌主播",
|
||||
status: "LIVE",
|
||||
tagText: "直播中",
|
||||
tagClass: "lv-tag-live",
|
||||
},
|
||||
{
|
||||
cover: "/demo/xiaomi-phone.png",
|
||||
title: "米粉狂欢日 爆款好物直降",
|
||||
anchorName: "好物推荐官",
|
||||
status: "PREVIEW",
|
||||
startTime: "10/22 19:00",
|
||||
tagText: "预告",
|
||||
tagClass: "lv-tag-preview",
|
||||
},
|
||||
{
|
||||
cover: "/demo/vivo-phone.png",
|
||||
title: "旗舰影像新机实测现场",
|
||||
anchorName: "数码达人",
|
||||
status: "REPLAY",
|
||||
tagText: "回放",
|
||||
tagClass: "lv-tag-replay",
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
const props = defineProps(["res"]);
|
||||
|
||||
@@ -354,7 +503,8 @@ const isLive = computed(() => firstItem.value.type === "LIVE");
|
||||
const isSeckill = computed(() => firstItem.value.type === "SECKILL");
|
||||
const isKanjia = computed(() => firstItem.value.type === "KANJIA");
|
||||
const seckillData = computed(() => {
|
||||
const data = (firstItem.value && firstItem.value.data) || [];
|
||||
const raw = (firstItem.value && firstItem.value.data) || [];
|
||||
const data = raw.length ? raw : (isKanjia.value ? KANJIA_DEMO : SECKILL_DEMO);
|
||||
return layoutStyle.value === "2" ? data.slice(0, 4) : data.slice(0, 6);
|
||||
});
|
||||
// 老数据未保存过样式时,给个红→白渐变兜底背景
|
||||
@@ -382,9 +532,12 @@ const fmtLiveTime = (t) => {
|
||||
return s;
|
||||
};
|
||||
const liveData = computed(() => {
|
||||
const data = (firstItem.value && firstItem.value.data) || [];
|
||||
const raw = (firstItem.value && firstItem.value.data) || [];
|
||||
if (!raw.length) {
|
||||
return LIVE_DEMO;
|
||||
}
|
||||
const limit = layoutStyle.value === "2" ? 3 : 6;
|
||||
return data.slice(0, limit).map((item) => {
|
||||
return raw.slice(0, limit).map((item) => {
|
||||
const tag = liveTagOf(item.status != null ? item.status : item.liveStatus);
|
||||
return {
|
||||
cover: item.cover || item.roomImg || item.shareImg || item.thumbnail || "",
|
||||
@@ -398,11 +551,12 @@ const liveData = computed(() => {
|
||||
});
|
||||
});
|
||||
const displayData = computed(() => {
|
||||
const data = (firstItem.value && firstItem.value.data) || [];
|
||||
const raw = (firstItem.value && firstItem.value.data) || [];
|
||||
const data = raw.length ? raw : PINTUAN_DEMO;
|
||||
if (layoutStyle.value === "3" || layoutStyle.value === "4") {
|
||||
return data.slice(0, 6);
|
||||
}
|
||||
return data.slice(0, 2);
|
||||
return data.slice(0, 4);
|
||||
});
|
||||
const headTitleImage = computed(() =>
|
||||
props.res.headTitleType === "image" ? props.res.headTitleImage : ""
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
:style="{
|
||||
background: richBg,
|
||||
padding: `0 ${px(res.paddingX ?? 12)}`,
|
||||
marginBottom: px(res.pageGap ?? 10),
|
||||
marginBottom: px(res.pageGap ?? 0),
|
||||
borderRadius: px(res.borderRadius ?? 0),
|
||||
overflow: 'hidden',
|
||||
}"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div class="shop-head" v-if="headIcon || title || hasMore">
|
||||
<img v-if="headIcon" class="shop-head-icon" :src="headIcon" alt="" />
|
||||
<div class="shop-title" :style="titleStyle">{{ title }}</div>
|
||||
<div v-if="hasMore" class="shop-more" :style="moreStyle">更多 ></div>
|
||||
<div v-if="hasMore" class="shop-more" :style="moreStyle">更多 ></div>
|
||||
</div>
|
||||
|
||||
<!-- 老数据:两列网格 -->
|
||||
@@ -14,7 +14,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 样式一:店铺卡片三列 -->
|
||||
<!-- 样式一:店铺卡片三列(全景大片橱窗) -->
|
||||
<div v-else-if="shopStyle === 'one'" class="shop-grid">
|
||||
<div class="shop-card" v-for="(shop, i) in previewShops" :key="i">
|
||||
<img
|
||||
@@ -25,6 +25,7 @@
|
||||
alt=""
|
||||
/>
|
||||
<div v-else class="shop-card-bg shop-card-bg-empty" :style="logoStyle"></div>
|
||||
<div class="shop-card-badge" v-if="isSelf(shop)">自营</div>
|
||||
<div class="shop-card-body">
|
||||
<img v-if="showLogo" class="shop-card-logo" :src="shop.logo" alt="" />
|
||||
<div v-if="showName" class="shop-card-name" :style="nameStyle">{{ shop.name }}</div>
|
||||
@@ -32,19 +33,21 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 样式二:店铺卡片 + 商品列表 -->
|
||||
<!-- 样式二:店铺卡片 + 商品列表(大牌名店街 + 精品橱窗) -->
|
||||
<div v-else class="shop-street-list">
|
||||
<div class="shop-street-card" v-for="(shop, i) in previewShops" :key="i">
|
||||
<div class="shop-street-head">
|
||||
<img v-if="showLogo" class="shop-street-logo" :src="shop.logo" alt="" />
|
||||
<div v-if="showName" class="shop-street-name" :style="nameStyle">{{ shop.name }}</div>
|
||||
<img
|
||||
v-if="shop.goodsImage"
|
||||
class="shop-street-banner"
|
||||
:style="logoStyle"
|
||||
:src="shop.goodsImage"
|
||||
alt=""
|
||||
/>
|
||||
<div class="shop-street-info">
|
||||
<img v-if="showLogo" class="shop-street-logo" :src="shop.logo" alt="" />
|
||||
<div class="shop-street-meta">
|
||||
<div class="shop-street-name-row">
|
||||
<div v-if="showName" class="shop-street-name" :style="nameStyle">{{ shop.name }}</div>
|
||||
<span class="shop-street-tag" v-if="isSelf(shop)">自营</span>
|
||||
</div>
|
||||
<div class="shop-street-desc">热卖好物甄选中</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="shop-street-enter">进店逛逛 ></div>
|
||||
</div>
|
||||
<div class="shop-street-goods">
|
||||
<div
|
||||
@@ -52,9 +55,13 @@
|
||||
v-for="(goods, goodsIndex) in shop.goods"
|
||||
:key="goodsIndex"
|
||||
>
|
||||
<img class="street-goods-img" :src="goods.img" alt="" />
|
||||
<div class="street-goods-cover">
|
||||
<img class="street-goods-img" :src="goods.img" alt="" />
|
||||
</div>
|
||||
<div class="street-goods-name">{{ goods.name }}</div>
|
||||
<div class="street-goods-price">¥{{ $filters.unitPrice(goods.price) }}</div>
|
||||
<div class="street-goods-price">
|
||||
<span class="street-goods-symbol">¥</span>{{ $filters.unitPrice(goods.price) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -138,8 +145,9 @@ export default {
|
||||
id: s.id,
|
||||
name: s.storeName,
|
||||
logo: s.storeLogo,
|
||||
selfOperated: s.selfOperated,
|
||||
goods,
|
||||
goodsImage: goods[0]?.img || s.goodsImage || s.storeBanner || "",
|
||||
goodsImage: goods[0]?.img || s.goodsImage || s.storeBanner || s.storeLogo || "",
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -174,7 +182,7 @@ export default {
|
||||
return {
|
||||
background: this.res.bgColor || "#FFFFFF",
|
||||
padding: `10px ${this.res.paddingX || 0}px`,
|
||||
marginBottom: `${this.res.pageGap !== undefined ? this.res.pageGap : 10}px`,
|
||||
marginBottom: `${this.res.pageGap !== undefined ? this.res.pageGap : 0}px`,
|
||||
borderRadius: `${this.res.borderRadius || 0}px`,
|
||||
overflow: "hidden",
|
||||
};
|
||||
@@ -205,6 +213,17 @@ export default {
|
||||
this.loadGoodsImages();
|
||||
},
|
||||
methods: {
|
||||
isSelf(shop) {
|
||||
if (!shop) return false;
|
||||
return Boolean(
|
||||
shop.selfOperated === true ||
|
||||
shop.selfOperated === 'true' ||
|
||||
shop.selfOperated === 1 ||
|
||||
shop.selfOperated === '1' ||
|
||||
(shop.name && shop.name.includes('自营')) ||
|
||||
(shop.storeName && shop.storeName.includes('自营'))
|
||||
);
|
||||
},
|
||||
loadGoodsImages() {
|
||||
if (this.first.dataMode !== "custom") return;
|
||||
(this.first.shopList || []).forEach((shop) => {
|
||||
@@ -282,132 +301,213 @@ export default {
|
||||
color: #333;
|
||||
margin-left: 8px;
|
||||
}
|
||||
/* 样式一 */
|
||||
/* 样式一:全景大片橱窗 */
|
||||
.shop-grid {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding: 0 12px;
|
||||
}
|
||||
.shop-card {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
border-radius: 6px;
|
||||
height: 112px;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
background: #f7f7f7;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
.shop-card-bg {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 56px;
|
||||
height: 100%;
|
||||
display: block;
|
||||
object-fit: cover;
|
||||
background: #eee;
|
||||
}
|
||||
.shop-card-bg-empty {
|
||||
background: linear-gradient(135deg, #f5f5f5, #e8e8e8);
|
||||
}
|
||||
.shop-card-badge {
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
right: 5px;
|
||||
z-index: 2;
|
||||
font-size: 10px;
|
||||
color: #fff;
|
||||
background: #e1251b;
|
||||
padding: 1px 5px;
|
||||
border-radius: 4px;
|
||||
line-height: 1.3;
|
||||
font-weight: 500;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
.shop-card-body {
|
||||
padding: 4px;
|
||||
text-align: center;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 2;
|
||||
padding: 16px 6px 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
background: linear-gradient(180deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.25) 35%, rgba(0, 0, 0, 0.72) 100%);
|
||||
}
|
||||
.shop-card-logo {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
flex-shrink: 0;
|
||||
display: block;
|
||||
width: 60px;
|
||||
height: 24px;
|
||||
margin: -12px auto 0;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 3px;
|
||||
object-fit: contain;
|
||||
background: #fff;
|
||||
padding: 1px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
.shop-card-name {
|
||||
font-size: 12px;
|
||||
margin-top: 3px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
/* 样式二:店铺卡片 + 商品列表 */
|
||||
|
||||
/* 样式二:大牌名店街 + 精品橱窗 */
|
||||
.shop-street-list {
|
||||
padding: 0 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
gap: 10px;
|
||||
}
|
||||
.shop-street-card {
|
||||
border-radius: 6px;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
background: #f7f7f7;
|
||||
background: #fbfbfb;
|
||||
border: 1px solid #f0f0f0;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
.shop-street-head {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 34px;
|
||||
padding: 0 8px;
|
||||
box-sizing: border-box;
|
||||
background: $theme_color;
|
||||
overflow: hidden;
|
||||
justify-content: space-between;
|
||||
padding: 8px 10px;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #f5f5f5;
|
||||
}
|
||||
.shop-street-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
.shop-street-logo {
|
||||
flex-shrink: 0;
|
||||
width: 44px;
|
||||
height: 16px;
|
||||
padding: 1px 3px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
padding: 2px;
|
||||
box-sizing: border-box;
|
||||
object-fit: contain;
|
||||
background: #fff;
|
||||
border-radius: 2px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid #eee;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
.shop-street-meta {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
.shop-street-name-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
.shop-street-name {
|
||||
max-width: calc(100% - 110px);
|
||||
margin-left: 6px;
|
||||
color: #fff;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.shop-street-banner {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 80px;
|
||||
height: 34px;
|
||||
object-fit: cover;
|
||||
.shop-street-tag {
|
||||
flex-shrink: 0;
|
||||
font-size: 9px;
|
||||
color: #fff;
|
||||
background: #e1251b;
|
||||
padding: 0 4px;
|
||||
border-radius: 3px;
|
||||
line-height: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.shop-street-desc {
|
||||
font-size: 10px;
|
||||
color: #999;
|
||||
margin-top: 2px;
|
||||
}
|
||||
.shop-street-enter {
|
||||
flex-shrink: 0;
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
color: $theme_color;
|
||||
background: #fff;
|
||||
border: 1px solid rgba(255, 60, 42, 0.25);
|
||||
padding: 3px 8px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 1px 2px rgba(255, 60, 42, 0.05);
|
||||
cursor: pointer;
|
||||
margin-left: 8px;
|
||||
}
|
||||
.shop-street-goods {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
padding: 6px;
|
||||
gap: 8px;
|
||||
padding: 8px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.street-goods-item {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
border-radius: 4px;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
border: 1px solid #f2f2f2;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.02);
|
||||
}
|
||||
.street-goods-cover {
|
||||
width: 100%;
|
||||
aspect-ratio: 1 / 1;
|
||||
overflow: hidden;
|
||||
background: #fafafa;
|
||||
}
|
||||
.street-goods-img {
|
||||
width: 100%;
|
||||
height: 58px;
|
||||
height: 100%;
|
||||
display: block;
|
||||
object-fit: cover;
|
||||
background: #eee;
|
||||
}
|
||||
.street-goods-name {
|
||||
padding: 4px 4px 0;
|
||||
color: #282828;
|
||||
padding: 4px 6px 0;
|
||||
color: #333;
|
||||
font-size: 11px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.street-goods-price {
|
||||
padding: 2px 4px 5px;
|
||||
padding: 2px 6px 6px;
|
||||
color: $theme_color;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.street-goods-symbol {
|
||||
font-size: 9px;
|
||||
font-weight: normal;
|
||||
margin-right: 1px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<span class="title-sub" :style="subStyle" v-if="item.title1">{{ item.title1 }}</span>
|
||||
</div>
|
||||
<div v-if="item.showMore" class="title-more" :style="moreStyle">
|
||||
{{ item.moreText || "更多" }} >
|
||||
{{ item.moreText || "更多" }} >
|
||||
</div>
|
||||
</div>
|
||||
<!-- 老数据:保持原样 -->
|
||||
|
||||
@@ -24,6 +24,11 @@
|
||||
{{ formatTime(row.createTime) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="修改时间" width="180">
|
||||
<template #default="{ row }">
|
||||
{{ formatTime(row.updateTime) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="120" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-switch
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
<template>
|
||||
<div class="layout">
|
||||
<el-form ref="formValidateRef" label-width="200px" label-position="right" :model="formValidate" :rules="ruleValidate">
|
||||
<el-form-item label="数据脱敏级别" prop="sensitiveLevel">
|
||||
<el-radio-group v-model="formValidate.sensitiveLevel">
|
||||
<el-radio :value="0">不做脱敏</el-radio>
|
||||
<el-radio :value="1">管理端脱敏</el-radio>
|
||||
<el-radio :value="2">全端脱敏</el-radio>
|
||||
</el-radio-group>
|
||||
<span class="desc">控制手机号、收货地址、身份证等敏感信息脱敏显示策略</span>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="演示站点保护" prop="isDemoSite">
|
||||
<el-switch v-model="formValidate.isDemoSite" active-text="开启" inactive-text="关闭" />
|
||||
<span class="desc">开启后拦截核心管理端修改与删除请求,防止演示数据被篡改</span>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="买家端Token有效期" prop="buyerTokenExpireTime">
|
||||
<el-input type="number" v-model="formValidate.buyerTokenExpireTime">
|
||||
<template #append>分钟</template>
|
||||
</el-input>
|
||||
<span class="desc">买家免密登录会话有效时长</span>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="管理/商家端Token有效期" prop="managerTokenExpireTime">
|
||||
<el-input type="number" v-model="formValidate.managerTokenExpireTime">
|
||||
<template #append>分钟</template>
|
||||
</el-input>
|
||||
<span class="desc">管理后台及店铺端操作人员会话有效时长</span>
|
||||
</el-form-item>
|
||||
|
||||
<div class="label-btns" style="margin-left: 200px;">
|
||||
<el-button type="primary" @click="submit('formValidateRef')">保存设置</el-button>
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { getCurrentInstance, reactive, ref } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { setSetting } from "@/api/index";
|
||||
import { handleSubmit } from "./validate";
|
||||
|
||||
const props = defineProps(["res", "type"]);
|
||||
const { proxy } = getCurrentInstance();
|
||||
|
||||
const formValidateRef = ref(null);
|
||||
const formValidate = ref({
|
||||
sensitiveLevel: 1,
|
||||
isDemoSite: false,
|
||||
buyerTokenExpireTime: 259200,
|
||||
managerTokenExpireTime: 86400,
|
||||
});
|
||||
|
||||
const ruleValidate = reactive({
|
||||
buyerTokenExpireTime: [
|
||||
{ required: true, message: "请输入买家端Token有效时间", trigger: "blur" }
|
||||
],
|
||||
managerTokenExpireTime: [
|
||||
{ required: true, message: "请输入管理端Token有效时间", trigger: "blur" }
|
||||
],
|
||||
});
|
||||
|
||||
function submit(name) {
|
||||
handleSubmit(proxy, name)
|
||||
.then(() => {
|
||||
setupSetting();
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
function setupSetting() {
|
||||
setSetting(props.type, formValidate.value).then((res) => {
|
||||
if (res.success) {
|
||||
ElMessage.success("保存成功!");
|
||||
} else {
|
||||
ElMessage.error(res.message || "保存失败!");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function init() {
|
||||
if (props.res) {
|
||||
try {
|
||||
const parsed = typeof props.res === "string" ? JSON.parse(props.res) : props.res;
|
||||
formValidate.value = {
|
||||
sensitiveLevel: Number(parsed.sensitiveLevel ?? 1),
|
||||
isDemoSite: Boolean(parsed.isDemoSite ?? false),
|
||||
buyerTokenExpireTime: Number(parsed.buyerTokenExpireTime ?? 259200),
|
||||
managerTokenExpireTime: Number(parsed.managerTokenExpireTime ?? 86400),
|
||||
};
|
||||
} catch (e) {
|
||||
console.error("解析安全设置失败", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init();
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "./style.scss";
|
||||
|
||||
:deep(.el-form-item__label) {
|
||||
white-space: nowrap !important;
|
||||
}
|
||||
|
||||
:deep(.el-form-item__content) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: nowrap;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.el-radio-group {
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.desc {
|
||||
margin-left: 12px;
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,128 @@
|
||||
<template>
|
||||
<div class="layout">
|
||||
<el-form ref="formValidateRef" label-width="160px" label-position="right" :model="formValidate" :rules="ruleValidate">
|
||||
<el-form-item label="有效时间(秒)" prop="effectiveTime">
|
||||
<el-input-number v-model="formValidate.effectiveTime" :min="30" :max="3600" />
|
||||
<span class="desc">包含滑块生成及校验结果缓存的有效时间(秒)</span>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="水印文字" prop="watermark">
|
||||
<el-input class="label-appkey" v-model="formValidate.watermark" placeholder="例如: LILI-SHOP" />
|
||||
<span class="desc">滑块背景图水印文本</span>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="干扰块数量" prop="interfereNum">
|
||||
<el-input-number v-model="formValidate.interfereNum" :min="0" :max="2" />
|
||||
<span class="desc">滑块干扰块数量,最大为 2</span>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="容错像素" prop="faultTolerant">
|
||||
<el-input-number v-model="formValidate.faultTolerant" :min="0" :max="20" />
|
||||
<span class="desc">滑块比对横坐标容错像素范围</span>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="登录免验证码" prop="loginBypassEnabled">
|
||||
<el-switch v-model="formValidate.loginBypassEnabled" active-text="开启" inactive-text="关闭" />
|
||||
<span class="desc">开启后登录流程跳过滑块校验,仅建议本地或测试验收环境开启</span>
|
||||
</el-form-item>
|
||||
|
||||
<div class="label-btns">
|
||||
<el-button type="primary" @click="submit('formValidateRef')">保存</el-button>
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { getCurrentInstance, reactive, ref } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { setSetting } from "@/api/index";
|
||||
import { handleSubmit } from "./validate";
|
||||
|
||||
const props = defineProps(["res", "type"]);
|
||||
const { proxy } = getCurrentInstance();
|
||||
|
||||
const formValidateRef = ref(null);
|
||||
const formValidate = ref({
|
||||
effectiveTime: 300,
|
||||
watermark: "LILI-SHOP",
|
||||
interfereNum: 0,
|
||||
faultTolerant: 3,
|
||||
loginBypassEnabled: false,
|
||||
});
|
||||
|
||||
const ruleValidate = reactive({
|
||||
effectiveTime: [
|
||||
{ required: true, message: "请输入有效时间", trigger: "blur" }
|
||||
],
|
||||
watermark: [
|
||||
{ required: true, message: "请输入水印文字", trigger: "blur" }
|
||||
],
|
||||
interfereNum: [
|
||||
{ required: true, message: "请输入干扰块数量", trigger: "blur" }
|
||||
],
|
||||
faultTolerant: [
|
||||
{ required: true, message: "请输入容错像素", trigger: "blur" }
|
||||
],
|
||||
});
|
||||
|
||||
function submit(name) {
|
||||
handleSubmit(proxy, name)
|
||||
.then(() => {
|
||||
setupSetting();
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
function setupSetting() {
|
||||
setSetting(props.type, formValidate.value).then((res) => {
|
||||
if (res.success) {
|
||||
ElMessage.success("保存成功!");
|
||||
} else {
|
||||
ElMessage.error(res.message || "保存失败!");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function init() {
|
||||
if (props.res) {
|
||||
try {
|
||||
const parsed = typeof props.res === "string" ? JSON.parse(props.res) : props.res;
|
||||
formValidate.value = {
|
||||
effectiveTime: Number(parsed.effectiveTime ?? 300),
|
||||
watermark: String(parsed.watermark ?? "LILI-SHOP"),
|
||||
interfereNum: Number(parsed.interfereNum ?? 0),
|
||||
faultTolerant: Number(parsed.faultTolerant ?? 3),
|
||||
loginBypassEnabled: Boolean(parsed.loginBypassEnabled ?? false),
|
||||
};
|
||||
} catch (e) {
|
||||
console.error("解析验证码设置失败", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init();
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "./style.scss";
|
||||
|
||||
:deep(.el-form-item__label) {
|
||||
white-space: nowrap !important;
|
||||
}
|
||||
|
||||
:deep(.el-form-item__content) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: nowrap;
|
||||
white-space: nowrap;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
.desc {
|
||||
margin-left: 10px;
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
@@ -75,6 +75,14 @@ const setting = ref([
|
||||
type: "LIVE_SETTING",
|
||||
name: "直播设置",
|
||||
},
|
||||
{
|
||||
type: "VERIFICATION_SETTING",
|
||||
name: "验证码设置",
|
||||
},
|
||||
{
|
||||
type: "SECURITY_SETTING",
|
||||
name: "安全设置",
|
||||
},
|
||||
]);
|
||||
const authLogin = ref([
|
||||
// 登录设置
|
||||
|
||||
@@ -9,6 +9,8 @@ import SMS_SETTING from "./setting/SMS_SETTING";
|
||||
import IM_SETTING from "./setting/IM_SETTING";
|
||||
import LIVE_SETTING from "./setting/LIVE_SETTING";
|
||||
import WITHDRAWAL_SETTING from "./setting/WITHDRAWAL_SETTING";
|
||||
import VERIFICATION_SETTING from "./setting/VERIFICATION_SETTING";
|
||||
import SECURITY_SETTING from "./setting/SECURITY_SETTING";
|
||||
import ALIPAY_PAYMENT from "./pay/ALIPAY_PAYMENT";
|
||||
import WECHAT_PAYMENT from "./pay/WECHAT_PAYMENT";
|
||||
import PAYMENT_SUPPORT from "./pay/PAYMENT_SUPPORT";
|
||||
@@ -27,6 +29,8 @@ export default {
|
||||
IM_SETTING: markRaw(IM_SETTING),
|
||||
LIVE_SETTING: markRaw(LIVE_SETTING),
|
||||
WITHDRAWAL_SETTING: markRaw(WITHDRAWAL_SETTING),
|
||||
VERIFICATION_SETTING: markRaw(VERIFICATION_SETTING),
|
||||
SECURITY_SETTING: markRaw(SECURITY_SETTING),
|
||||
PAYMENT_SUPPORT: markRaw(PAYMENT_SUPPORT),
|
||||
WECHAT_PAYMENT: markRaw(WECHAT_PAYMENT),
|
||||
ALIPAY_PAYMENT: markRaw(ALIPAY_PAYMENT),
|
||||
|
||||
350
seller/src/views/shop/wap/config-panel/PanelAdCube.vue
Normal file
@@ -0,0 +1,350 @@
|
||||
<template>
|
||||
<div class="panel-ad-cube">
|
||||
<div class="panel-header">
|
||||
<span class="panel-name">图片魔方</span>
|
||||
<div class="panel-tabs">
|
||||
<div
|
||||
v-for="tab in tabs"
|
||||
:key="tab.key"
|
||||
class="panel-tab"
|
||||
:class="{ active: activeTab === tab.key }"
|
||||
@click="activeTab = tab.key"
|
||||
>
|
||||
{{ tab.title }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="activeTab === 'content'">
|
||||
<div class="panel-card panel-card-fill">
|
||||
<div class="section-title">图片设置</div>
|
||||
<div class="section-tip">当前风格:{{ res.name }},请按布局区域配置对应图片</div>
|
||||
<div class="img-item" v-for="(item, index) in res.options.list" :key="index">
|
||||
<div class="img-item-head">
|
||||
<span>图片 {{ index + 1 }}</span>
|
||||
<span class="img-size" v-if="item.size">建议尺寸 {{ item.size }}</span>
|
||||
</div>
|
||||
<div class="img-preview" v-if="item.img">
|
||||
<img :src="item.img" alt="" />
|
||||
</div>
|
||||
<div class="img-placeholder" v-else>尺寸 {{ item.size || "不限" }}</div>
|
||||
<div class="field-row" v-if="item.title !== undefined">
|
||||
<div class="field-label">文字</div>
|
||||
<el-input v-model="item.title" maxlength="10" />
|
||||
</div>
|
||||
<div class="btn-row">
|
||||
<el-button plain type="primary" @click="pickImage(item)">选择照片</el-button>
|
||||
<el-button plain type="primary" @click="pickLink(item)">选择链接</el-button>
|
||||
</div>
|
||||
<div class="link-view" v-if="item.url && item.url.___type">
|
||||
已选链接:{{ linkText(item.url) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div class="panel-card">
|
||||
<div class="section-title">风格选择</div>
|
||||
<div class="section-tip">切换风格将重置当前已配置的图片内容</div>
|
||||
<div class="style-grid">
|
||||
<div
|
||||
v-for="style in styleList"
|
||||
:key="style.type"
|
||||
class="style-item"
|
||||
:class="{ active: res.type === style.type }"
|
||||
@click="changeStyle(style)"
|
||||
>
|
||||
{{ style.name }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel-card">
|
||||
<div class="section-title">颜色设置</div>
|
||||
<gradient-field
|
||||
v-model:start="res.options.bgColor1"
|
||||
v-model:end="res.options.bgColor2"
|
||||
:default-start="defaults.bgColor1"
|
||||
:default-end="defaults.bgColor2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="panel-card">
|
||||
<div class="section-title">边距设置</div>
|
||||
<slider-field v-model="res.options.paddingTop" label="上边距" :min="0" :max="100" />
|
||||
<slider-field v-model="res.options.paddingBottom" label="下边距" :min="0" :max="100" />
|
||||
<slider-field v-model="res.options.paddingX" label="左右边距" :min="0" :max="40" />
|
||||
<slider-field v-model="res.options.pageGap" label="页面间距" :min="0" :max="100" />
|
||||
<slider-field v-model="res.options.contentGap" label="内容间距" :min="0" :max="20" />
|
||||
</div>
|
||||
|
||||
<div class="panel-card panel-card-fill">
|
||||
<div class="section-title">圆角设置</div>
|
||||
<slider-field v-model="res.options.borderRadius" label="背景圆角" :min="0" :max="30" />
|
||||
<slider-field v-model="res.options.imgRadius" label="内容圆角" :min="0" :max="30" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<liliDialog ref="liliDialog" @selectedLink="selectedLink"></liliDialog>
|
||||
|
||||
<el-dialog v-model="picModelFlag" width="1200px" append-to-body destroy-on-close>
|
||||
<ossManage
|
||||
@callback="callbackSelected"
|
||||
:is-component="true"
|
||||
:initialize="picModelFlag"
|
||||
:max-select="1"
|
||||
ref="ossManage"
|
||||
/>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ossManage from "@/views/sys/oss-manage/ossManage";
|
||||
import ways from "@/views/lili-dialog/wap.js";
|
||||
import { modelData } from "../config";
|
||||
// import ColorField from "../controls/ColorField.vue";
|
||||
import GradientField from "../controls/GradientField.vue";
|
||||
import SliderField from "../controls/SliderField.vue";
|
||||
|
||||
const DEFAULTS = {
|
||||
bgColor1: "#FFFFFF",
|
||||
bgColor2: "#FFFFFF",
|
||||
paddingTop: 10,
|
||||
paddingBottom: 10,
|
||||
paddingX: 12,
|
||||
pageGap: 0,
|
||||
contentGap: 0,
|
||||
borderRadius: 0,
|
||||
imgRadius: 0,
|
||||
};
|
||||
|
||||
export default {
|
||||
name: "PanelAdCube",
|
||||
hideDecorateTitle: true,
|
||||
components: { ossManage, GradientField, SliderField },
|
||||
props: {
|
||||
res: { type: Object, required: true },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
defaults: DEFAULTS,
|
||||
activeTab: "content",
|
||||
tabs: [
|
||||
{ key: "content", title: "内容" },
|
||||
{ key: "style", title: "样式" },
|
||||
],
|
||||
picModelFlag: false,
|
||||
currentItem: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
// 图片魔方的可选风格(config 中 drawer 标记的条目)
|
||||
styleList() {
|
||||
return modelData.filter((m) => m.drawer);
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.res.options = Object.assign({}, DEFAULTS, this.res.options);
|
||||
},
|
||||
methods: {
|
||||
linkText(url) {
|
||||
const way = ways.find((e) => e.name === url.___type);
|
||||
const typeTitle = way ? way.title : "发现";
|
||||
const name =
|
||||
url.name || url.goodsName || url.memberName || url.title || "";
|
||||
return name ? `${typeTitle} - ${name}` : typeTitle;
|
||||
},
|
||||
changeStyle(style) {
|
||||
if (style.type === this.res.type) return;
|
||||
this.$emit("handleDrawer", style);
|
||||
},
|
||||
pickImage(item) {
|
||||
this.currentItem = item;
|
||||
this.picModelFlag = true;
|
||||
this.$nextTick(() => {
|
||||
const oss = this.$refs.ossManage;
|
||||
if (!oss) return;
|
||||
oss.selectImage = true;
|
||||
oss.fileType = "pic";
|
||||
oss.changeFileType();
|
||||
});
|
||||
},
|
||||
callbackSelected(val) {
|
||||
this.picModelFlag = false;
|
||||
if (val && val.url && this.currentItem) {
|
||||
this.currentItem.img = val.url;
|
||||
}
|
||||
},
|
||||
pickLink(item) {
|
||||
this.currentItem = item;
|
||||
const dialog = this.$refs.liliDialog;
|
||||
dialog.clearGoodsSelected();
|
||||
dialog.goodsFlag = false;
|
||||
dialog.flag = true;
|
||||
},
|
||||
selectedLink(val) {
|
||||
if (!this.currentItem) return;
|
||||
delete val.selected;
|
||||
delete val.intro;
|
||||
delete val.mobileIntro;
|
||||
this.currentItem.url = val;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "../controls/panel-common.scss";
|
||||
.panel-ad-cube {
|
||||
background: #f5f7fa;
|
||||
margin: 0 -20px;
|
||||
padding: 0 12px 12px;
|
||||
min-height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.panel-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 50px;
|
||||
margin-bottom: 12px;
|
||||
padding: 0 14px;
|
||||
background: #ffffff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04);
|
||||
box-sizing: border-box;
|
||||
.panel-name {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #303133;
|
||||
}
|
||||
}
|
||||
.panel-tabs {
|
||||
display: inline-flex;
|
||||
background: #f4f4f5;
|
||||
border-radius: 8px;
|
||||
padding: 3px;
|
||||
.panel-tab {
|
||||
padding: 5px 14px;
|
||||
font-size: 13px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
color: #606266;
|
||||
transition: all 0.2s ease;
|
||||
&.active {
|
||||
background: #ffffff;
|
||||
color: #409eff;
|
||||
font-weight: 600;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
}
|
||||
}
|
||||
.panel-card {
|
||||
background: #ffffff;
|
||||
border-radius: 8px;
|
||||
padding: 14px;
|
||||
margin-bottom: 12px;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
.section-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
margin: 0 0 12px;
|
||||
}
|
||||
.section-tip {
|
||||
font-size: 13px;
|
||||
color: #909399;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.img-item {
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
margin-bottom: 10px;
|
||||
.img-item-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-size: 14px;
|
||||
color: #303133;
|
||||
margin-bottom: 8px;
|
||||
.img-size {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
}
|
||||
.img-preview {
|
||||
border: 1px dashed #dcdfe6;
|
||||
border-radius: 6px;
|
||||
padding: 6px;
|
||||
margin-bottom: 10px;
|
||||
img {
|
||||
width: 100%;
|
||||
display: block;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
.img-placeholder {
|
||||
height: 80px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px dashed #dcdfe6;
|
||||
border-radius: 6px;
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.field-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
.field-label {
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
margin-right: 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
.btn-row {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
.link-view {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.style-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
gap: 8px;
|
||||
.style-item {
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 6px;
|
||||
padding: 10px 4px;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
color: #606266;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
&:hover {
|
||||
border-color: #409eff;
|
||||
color: #409eff;
|
||||
}
|
||||
&.active {
|
||||
border-color: #409eff;
|
||||
background: #ecf5ff;
|
||||
color: #409eff;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
69
seller/src/views/shop/wap/config-panel/PanelBlank.vue
Normal file
@@ -0,0 +1,69 @@
|
||||
<template>
|
||||
<div class="panel-blank">
|
||||
<div class="panel-header">
|
||||
<span class="panel-name">{{ res.name }}</span>
|
||||
</div>
|
||||
|
||||
<div class="panel-card">
|
||||
<div class="section-title">颜色设置</div>
|
||||
<gradient-field
|
||||
v-model:start="res.options.bgColor1"
|
||||
v-model:end="res.options.bgColor2"
|
||||
:default-start="defaults.bgColor1"
|
||||
:default-end="defaults.bgColor2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="panel-card">
|
||||
<div class="section-title">高度设置</div>
|
||||
<slider-field v-model="res.options.height" label="空白高度" :min="1" :max="100" />
|
||||
</div>
|
||||
|
||||
<div class="panel-card panel-card-fill">
|
||||
<div class="section-title">边距设置</div>
|
||||
<slider-field v-model="res.options.paddingX" label="左右边距" :min="0" :max="40" />
|
||||
<slider-field v-model="res.options.pageGap" label="页面间距" :min="0" :max="100" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import GradientField from "../controls/GradientField.vue";
|
||||
import SliderField from "../controls/SliderField.vue";
|
||||
|
||||
const DEFAULTS = {
|
||||
bgColor: "#f5f5f5",
|
||||
bgColor1: "#f5f5f5",
|
||||
bgColor2: "#f5f5f5",
|
||||
height: 10,
|
||||
paddingX: 0,
|
||||
pageGap: 0,
|
||||
};
|
||||
|
||||
export default {
|
||||
name: "PanelBlank",
|
||||
hideDecorateTitle: true,
|
||||
components: { GradientField, SliderField },
|
||||
props: {
|
||||
res: { type: Object, required: true },
|
||||
},
|
||||
data() {
|
||||
return { defaults: DEFAULTS };
|
||||
},
|
||||
created() {
|
||||
this.res.options = Object.assign({}, DEFAULTS, this.res.options);
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "../controls/panel-common.scss";
|
||||
|
||||
.panel-blank {
|
||||
background: #f5f7fa;
|
||||
margin: 0 -20px;
|
||||
padding: 0 12px 12px;
|
||||
min-height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
</style>
|
||||
461
seller/src/views/shop/wap/config-panel/PanelCarousel.vue
Normal file
@@ -0,0 +1,461 @@
|
||||
<template>
|
||||
<div class="panel-carousel">
|
||||
<div class="panel-header">
|
||||
<span class="panel-name">{{ res.name }}</span>
|
||||
<div class="panel-tabs">
|
||||
<div
|
||||
v-for="tab in tabs"
|
||||
:key="tab.key"
|
||||
class="panel-tab"
|
||||
:class="{ active: activeTab === tab.key }"
|
||||
@click="activeTab = tab.key"
|
||||
>
|
||||
{{ tab.title }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="activeTab === 'content'">
|
||||
<div class="panel-card panel-card-fill">
|
||||
<div class="section-title">图片设置</div>
|
||||
<div class="img-item" v-for="(item, index) in res.options.list" :key="index">
|
||||
<div class="img-item-head">
|
||||
<span>图片 {{ index + 1 }}</span>
|
||||
<div class="img-item-ops">
|
||||
<el-icon
|
||||
:class="{ disabled: index === 0 }"
|
||||
@click="moveItem(index, -1)"
|
||||
><Top
|
||||
/></el-icon>
|
||||
<el-icon
|
||||
:class="{ disabled: index === res.options.list.length - 1 }"
|
||||
@click="moveItem(index, 1)"
|
||||
><Bottom
|
||||
/></el-icon>
|
||||
<el-icon class="del" @click="removeItem(index)"><Delete /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
<div class="img-item-body">
|
||||
<div class="img-thumb" v-if="item.img">
|
||||
<img :src="item.img" alt="" />
|
||||
</div>
|
||||
<div class="img-thumb placeholder" v-else>750*350</div>
|
||||
<div class="img-item-fields">
|
||||
<div class="btn-row">
|
||||
<el-button plain type="primary" @click="pickImage(item)">选择照片</el-button>
|
||||
<el-button plain type="primary" @click="pickLink(item)">选择链接</el-button>
|
||||
</div>
|
||||
<div class="link-view" v-if="item.url && item.url.___type">
|
||||
已选链接:{{ linkText(item.url) }}
|
||||
</div>
|
||||
<div class="link-view empty" v-else>未设置链接</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-button
|
||||
v-if="res.options.list.length < 10"
|
||||
class="add-btn"
|
||||
plain
|
||||
type="primary"
|
||||
@click="addItem"
|
||||
>添加图片</el-button
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div class="panel-card">
|
||||
<div class="section-title">轮播风格</div>
|
||||
<div class="ctrl-row">
|
||||
<div class="ctrl-label">风格</div>
|
||||
<div class="ctrl-body">
|
||||
<div class="seg layout-seg">
|
||||
<div
|
||||
v-for="opt in swiperStyleOptions"
|
||||
:key="opt.value"
|
||||
class="seg-item"
|
||||
:class="{ active: res.options.swiperStyle === opt.value }"
|
||||
@click="res.options.swiperStyle = opt.value"
|
||||
>
|
||||
{{ opt.title }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section-tip" v-if="res.options.swiperStyle === 'card'">
|
||||
卡片风格下,两侧会露出前后图片的一部分
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel-card panel-card-fill">
|
||||
<div class="section-title">指示器设置</div>
|
||||
<div class="ctrl-row">
|
||||
<div class="ctrl-label">指示器样式</div>
|
||||
<div class="ctrl-body">
|
||||
<div class="seg layout-seg-3">
|
||||
<div
|
||||
v-for="opt in indicatorStyleOptions"
|
||||
:key="opt.value"
|
||||
class="seg-item"
|
||||
:class="{ active: res.options.indicatorStyle === opt.value }"
|
||||
@click="res.options.indicatorStyle = opt.value"
|
||||
>
|
||||
{{ opt.title }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ctrl-row" v-if="res.options.indicatorStyle !== 'none'">
|
||||
<div class="ctrl-label">指示器位置</div>
|
||||
<div class="ctrl-body">
|
||||
<div class="seg layout-seg-3">
|
||||
<div
|
||||
v-for="opt in indicatorPositionOptions"
|
||||
:key="opt.value"
|
||||
class="seg-item"
|
||||
:class="{ active: res.options.indicatorPosition === opt.value }"
|
||||
@click="res.options.indicatorPosition = opt.value"
|
||||
>
|
||||
{{ opt.title }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ctrl-row" v-if="res.options.indicatorStyle !== 'none'">
|
||||
<div class="ctrl-label">色调</div>
|
||||
<div class="ctrl-body">
|
||||
<el-radio-group v-model="res.options.indicatorColorMode">
|
||||
<el-radio value="theme">跟随主题风格</el-radio>
|
||||
<el-radio value="custom">自定义</el-radio>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
</div>
|
||||
<template
|
||||
v-if="
|
||||
res.options.indicatorStyle !== 'none' &&
|
||||
res.options.indicatorColorMode === 'custom'
|
||||
"
|
||||
>
|
||||
<color-field
|
||||
v-model="res.options.indicatorColor"
|
||||
label="指示器颜色"
|
||||
:default-value="defaults.indicatorColor"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="panel-card">
|
||||
<div class="section-title">颜色设置</div>
|
||||
<gradient-field
|
||||
v-model:start="res.options.bgColor1"
|
||||
v-model:end="res.options.bgColor2"
|
||||
:default-start="defaults.bgColor1"
|
||||
:default-end="defaults.bgColor2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="panel-card">
|
||||
<div class="section-title">边距设置</div>
|
||||
<slider-field v-model="res.options.paddingTop" label="上边距" :min="0" :max="100" />
|
||||
<slider-field v-model="res.options.paddingBottom" label="下边距" :min="0" :max="100" />
|
||||
<slider-field v-model="res.options.paddingX" label="左右边距" :min="0" :max="40" />
|
||||
<slider-field v-model="res.options.pageGap" label="页面间距" :min="0" :max="100" />
|
||||
</div>
|
||||
|
||||
<div class="panel-card panel-card-fill">
|
||||
<div class="section-title">圆角设置</div>
|
||||
<slider-field v-model="res.options.borderRadius" label="背景圆角" :min="0" :max="30" />
|
||||
<slider-field v-model="res.options.imgRadius" label="内容圆角" :min="0" :max="30" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<liliDialog ref="liliDialog" @selectedLink="selectedLink"></liliDialog>
|
||||
|
||||
<el-dialog v-model="picModelFlag" width="1200px" append-to-body destroy-on-close>
|
||||
<ossManage
|
||||
@callback="callbackSelected"
|
||||
:is-component="true"
|
||||
:initialize="picModelFlag"
|
||||
:max-select="1"
|
||||
ref="ossManage"
|
||||
/>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Top, Bottom, Delete } from "@element-plus/icons-vue";
|
||||
import ColorField from "../controls/ColorField.vue";
|
||||
import GradientField from "../controls/GradientField.vue";
|
||||
import SliderField from "../controls/SliderField.vue";
|
||||
import ossManage from "@/views/sys/oss-manage/ossManage";
|
||||
import ways from "@/views/lili-dialog/wap.js";
|
||||
|
||||
const DEFAULTS = {
|
||||
swiperStyle: "full",
|
||||
indicatorStyle: "round",
|
||||
indicatorPosition: "center",
|
||||
indicatorColorMode: "theme",
|
||||
indicatorColor: "#409eff",
|
||||
bgColor1: "#FFFFFF",
|
||||
bgColor2: "#FFFFFF",
|
||||
paddingTop: 10,
|
||||
paddingBottom: 10,
|
||||
paddingX: 12,
|
||||
pageGap: 0,
|
||||
borderRadius: 0,
|
||||
imgRadius: 7,
|
||||
};
|
||||
|
||||
export default {
|
||||
name: "PanelCarousel",
|
||||
hideDecorateTitle: true,
|
||||
components: { ColorField, GradientField, SliderField, ossManage, Top, Bottom, Delete },
|
||||
props: {
|
||||
res: { type: Object, required: true },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
defaults: DEFAULTS,
|
||||
activeTab: "content",
|
||||
tabs: [
|
||||
{ key: "content", title: "内容" },
|
||||
{ key: "style", title: "样式" },
|
||||
],
|
||||
swiperStyleOptions: [
|
||||
{ value: "full", title: "通栏" },
|
||||
{ value: "card", title: "卡片" },
|
||||
],
|
||||
indicatorStyleOptions: [
|
||||
{ value: "round", title: "圆形" },
|
||||
{ value: "line", title: "长条" },
|
||||
{ value: "none", title: "无" },
|
||||
],
|
||||
indicatorPositionOptions: [
|
||||
{ value: "left", title: "居左" },
|
||||
{ value: "center", title: "居中" },
|
||||
{ value: "right", title: "居右" },
|
||||
],
|
||||
picModelFlag: false,
|
||||
currentItem: null,
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.res.options = Object.assign({}, DEFAULTS, this.res.options);
|
||||
if (!Array.isArray(this.res.options.list)) {
|
||||
this.res.options.list = [];
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
linkText(url) {
|
||||
const way = ways.find((e) => e.name === url.___type);
|
||||
const typeTitle = way ? way.title : "发现";
|
||||
const name =
|
||||
url.name || url.goodsName || url.memberName || url.title || "";
|
||||
return name ? `${typeTitle} - ${name}` : typeTitle;
|
||||
},
|
||||
pickImage(item) {
|
||||
this.currentItem = item;
|
||||
this.picModelFlag = true;
|
||||
this.$nextTick(() => {
|
||||
const oss = this.$refs.ossManage;
|
||||
if (!oss) return;
|
||||
oss.selectImage = true;
|
||||
oss.fileType = "pic";
|
||||
oss.changeFileType();
|
||||
});
|
||||
},
|
||||
callbackSelected(val) {
|
||||
this.picModelFlag = false;
|
||||
if (val && val.url && this.currentItem) {
|
||||
this.currentItem.img = val.url;
|
||||
}
|
||||
},
|
||||
pickLink(item) {
|
||||
this.currentItem = item;
|
||||
const dialog = this.$refs.liliDialog;
|
||||
dialog.clearGoodsSelected();
|
||||
dialog.goodsFlag = false;
|
||||
dialog.flag = true;
|
||||
},
|
||||
selectedLink(val) {
|
||||
if (!this.currentItem) return;
|
||||
delete val.selected;
|
||||
delete val.intro;
|
||||
delete val.mobileIntro;
|
||||
this.currentItem.url = val;
|
||||
},
|
||||
moveItem(index, dir) {
|
||||
const list = this.res.options.list;
|
||||
const target = index + dir;
|
||||
if (target < 0 || target >= list.length) return;
|
||||
list.splice(target, 1, ...list.splice(index, 1, list[target]));
|
||||
},
|
||||
removeItem(index) {
|
||||
this.res.options.list.splice(index, 1);
|
||||
},
|
||||
addItem() {
|
||||
this.res.options.list.push({
|
||||
img: "",
|
||||
url: "",
|
||||
link: "",
|
||||
size: "750*350",
|
||||
model: "link",
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "../controls/field-common.scss";
|
||||
@import "../controls/panel-common.scss";
|
||||
.panel-carousel {
|
||||
background: #f5f7fa;
|
||||
margin: 0 -20px;
|
||||
padding: 0 12px 12px;
|
||||
min-height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.panel-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 50px;
|
||||
margin-bottom: 12px;
|
||||
padding: 0 14px;
|
||||
background: #ffffff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04);
|
||||
box-sizing: border-box;
|
||||
.panel-name {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #303133;
|
||||
}
|
||||
}
|
||||
.panel-tabs {
|
||||
display: inline-flex;
|
||||
background: #f4f4f5;
|
||||
border-radius: 8px;
|
||||
padding: 3px;
|
||||
.panel-tab {
|
||||
padding: 5px 14px;
|
||||
font-size: 13px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
color: #606266;
|
||||
transition: all 0.2s ease;
|
||||
&.active {
|
||||
background: #ffffff;
|
||||
color: #409eff;
|
||||
font-weight: 600;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
}
|
||||
}
|
||||
.panel-card {
|
||||
background: #ffffff;
|
||||
border-radius: 8px;
|
||||
padding: 14px;
|
||||
margin-bottom: 12px;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
.section-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
margin: 0 0 12px;
|
||||
}
|
||||
.section-tip {
|
||||
font-size: 13px;
|
||||
color: #909399;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.img-item {
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
margin-bottom: 10px;
|
||||
.img-item-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-size: 14px;
|
||||
color: #303133;
|
||||
margin-bottom: 8px;
|
||||
.img-item-ops {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
.el-icon {
|
||||
padding: 4px;
|
||||
cursor: pointer;
|
||||
color: #606266;
|
||||
&:hover {
|
||||
color: #409eff;
|
||||
}
|
||||
&.del:hover {
|
||||
color: #e1251b;
|
||||
}
|
||||
&.disabled {
|
||||
color: #dcdfe6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.img-item-body {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
.img-thumb {
|
||||
width: 132px;
|
||||
height: 62px;
|
||||
border: 1px dashed #dcdfe6;
|
||||
border-radius: 6px;
|
||||
padding: 3px;
|
||||
box-sizing: border-box;
|
||||
flex-shrink: 0;
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
border-radius: 4px;
|
||||
object-fit: cover;
|
||||
}
|
||||
&.placeholder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
.img-item-fields {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
.link-view {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
.btn-row {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
.link-view {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.add-btn {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
145
seller/src/views/shop/wap/config-panel/PanelCoupon.vue
Normal file
@@ -0,0 +1,145 @@
|
||||
<template>
|
||||
<div class="panel-coupon">
|
||||
<div class="panel-header">
|
||||
<span class="panel-name">{{ res.name }}</span>
|
||||
<div class="panel-tabs">
|
||||
<div
|
||||
v-for="tab in tabs"
|
||||
:key="tab.key"
|
||||
class="panel-tab"
|
||||
:class="{ active: activeTab === tab.key }"
|
||||
@click="activeTab = tab.key"
|
||||
>
|
||||
{{ tab.title }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="activeTab === 'content'">
|
||||
<div class="panel-card panel-card-fill">
|
||||
<div class="section-title">展示设置</div>
|
||||
<div class="ctrl-row">
|
||||
<div class="ctrl-label">标题</div>
|
||||
<div class="ctrl-body">
|
||||
<el-input v-model="firstItem.title" />
|
||||
</div>
|
||||
</div>
|
||||
<slider-field v-model="firstItem.count" label="展示数量" :min="1" :max="10" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div class="panel-card">
|
||||
<div class="section-title">布局设置</div>
|
||||
<div class="ctrl-row">
|
||||
<div class="ctrl-label">展示样式</div>
|
||||
<div class="ctrl-body">
|
||||
<div class="seg layout-seg">
|
||||
<div
|
||||
v-for="opt in layoutOptions"
|
||||
:key="opt.value"
|
||||
class="seg-item"
|
||||
:class="{ active: (res.options.layoutStyle || '1') === opt.value }"
|
||||
@click="res.options.layoutStyle = opt.value"
|
||||
>
|
||||
{{ opt.title }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel-card">
|
||||
<div class="section-title">颜色设置</div>
|
||||
<gradient-field
|
||||
v-model:start="res.options.bgColor1"
|
||||
v-model:end="res.options.bgColor2"
|
||||
:default-start="defaults.bgColor1"
|
||||
:default-end="defaults.bgColor2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="panel-card">
|
||||
<div class="section-title">边距设置</div>
|
||||
<slider-field v-model="res.options.paddingTop" label="上边距" :min="0" :max="100" />
|
||||
<slider-field v-model="res.options.paddingBottom" label="下边距" :min="0" :max="100" />
|
||||
<slider-field v-model="res.options.contentGap" label="内容间距" :min="0" :max="20" />
|
||||
<slider-field v-model="res.options.paddingX" label="左右边距" :min="0" :max="40" />
|
||||
<slider-field v-model="res.options.pageGap" label="页面间距" :min="0" :max="100" />
|
||||
</div>
|
||||
|
||||
<div class="panel-card panel-card-fill">
|
||||
<div class="section-title">圆角设置</div>
|
||||
<slider-field v-model="res.options.borderRadius" label="背景圆角" :min="0" :max="30" />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import GradientField from "../controls/GradientField.vue";
|
||||
import SliderField from "../controls/SliderField.vue";
|
||||
|
||||
const DEFAULTS = {
|
||||
layoutStyle: "1",
|
||||
bgColor: "#FFFFFF",
|
||||
bgColor1: "#FFFFFF",
|
||||
bgColor2: "#FFFFFF",
|
||||
paddingTop: 10,
|
||||
paddingBottom: 10,
|
||||
contentGap: 5,
|
||||
paddingX: 12,
|
||||
pageGap: 0,
|
||||
borderRadius: 0,
|
||||
};
|
||||
const DEFAULT_ITEM = { title: "优惠券", count: 5 };
|
||||
|
||||
export default {
|
||||
name: "PanelCoupon",
|
||||
hideDecorateTitle: true,
|
||||
components: { GradientField, SliderField },
|
||||
props: {
|
||||
res: { type: Object, required: true },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
defaults: DEFAULTS,
|
||||
activeTab: "content",
|
||||
tabs: [
|
||||
{ key: "content", title: "内容" },
|
||||
{ key: "style", title: "样式" },
|
||||
],
|
||||
layoutOptions: [
|
||||
{ value: "1", title: "样式一" },
|
||||
{ value: "2", title: "样式二" },
|
||||
{ value: "3", title: "样式三" },
|
||||
{ value: "4", title: "样式四" },
|
||||
],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
firstItem() {
|
||||
return this.res.options.list[0];
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.res.options = Object.assign({}, DEFAULTS, this.res.options);
|
||||
if (!Array.isArray(this.res.options.list) || !this.res.options.list[0]) {
|
||||
this.res.options.list = [{ ...DEFAULT_ITEM }];
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "../controls/field-common.scss";
|
||||
@import "../controls/panel-common.scss";
|
||||
|
||||
.panel-coupon {
|
||||
background: #f5f7fa;
|
||||
margin: 0 -20px;
|
||||
padding: 0 12px 12px;
|
||||
min-height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
</style>
|
||||
268
seller/src/views/shop/wap/config-panel/PanelFlexOne.vue
Normal file
@@ -0,0 +1,268 @@
|
||||
<template>
|
||||
<div class="panel-flex-one">
|
||||
<div class="panel-header">
|
||||
<span class="panel-name">{{ res.name }}</span>
|
||||
<div class="panel-tabs">
|
||||
<div
|
||||
v-for="tab in tabs"
|
||||
:key="tab.key"
|
||||
class="panel-tab"
|
||||
:class="{ active: activeTab === tab.key }"
|
||||
@click="activeTab = tab.key"
|
||||
>
|
||||
{{ tab.title }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="activeTab === 'content'">
|
||||
<div class="panel-card panel-card-fill">
|
||||
<div class="section-title">内容设置</div>
|
||||
<div class="img-preview" v-if="item.img">
|
||||
<img :src="item.img" alt="" />
|
||||
</div>
|
||||
<div class="img-placeholder" v-else>尺寸不限</div>
|
||||
<div class="btn-row">
|
||||
<el-button plain type="primary" @click="pickImage">选择照片</el-button>
|
||||
<el-button plain type="primary" @click="openHotzone">绘制热区</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div class="panel-card">
|
||||
<div class="section-title">颜色设置</div>
|
||||
<color-field
|
||||
v-model="res.options.bgColor"
|
||||
label="背景颜色"
|
||||
:default-value="defaults.bgColor"
|
||||
/>
|
||||
</div>
|
||||
<div class="panel-card">
|
||||
<div class="section-title">边距设置</div>
|
||||
<slider-field
|
||||
v-model="res.options.paddingTop"
|
||||
label="上边距"
|
||||
:min="0"
|
||||
:max="100"
|
||||
/>
|
||||
<slider-field
|
||||
v-model="res.options.paddingBottom"
|
||||
label="下边距"
|
||||
:min="0"
|
||||
:max="100"
|
||||
/>
|
||||
<slider-field
|
||||
v-model="res.options.paddingX"
|
||||
label="左右边距"
|
||||
:min="0"
|
||||
:max="40"
|
||||
/>
|
||||
<slider-field
|
||||
v-model="res.options.pageGap"
|
||||
label="页面间距"
|
||||
:min="0"
|
||||
:max="100"
|
||||
/>
|
||||
</div>
|
||||
<div class="panel-card panel-card-fill">
|
||||
<div class="section-title">圆角设置</div>
|
||||
<slider-field
|
||||
v-model="res.options.borderRadius"
|
||||
label="背景圆角"
|
||||
:min="0"
|
||||
:max="30"
|
||||
/>
|
||||
<slider-field
|
||||
v-model="res.options.imgRadius"
|
||||
label="内容圆角"
|
||||
:min="0"
|
||||
:max="30"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<hotzone ref="hotzone" @changeZone="changeZone"></hotzone>
|
||||
|
||||
<el-dialog v-model="picModelFlag" width="1200px" append-to-body destroy-on-close>
|
||||
<ossManage
|
||||
@callback="callbackSelected"
|
||||
:is-component="true"
|
||||
:initialize="picModelFlag"
|
||||
:max-select="1"
|
||||
ref="ossManage"
|
||||
/>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ColorField from "../controls/ColorField.vue";
|
||||
import SliderField from "../controls/SliderField.vue";
|
||||
import hotzone from "@/views/shop/hotzone";
|
||||
import ossManage from "@/views/sys/oss-manage/ossManage";
|
||||
import { ElMessage } from "element-plus";
|
||||
|
||||
const DEFAULTS = {
|
||||
bgColor: "#FFFFFF",
|
||||
paddingTop: 10,
|
||||
paddingBottom: 10,
|
||||
paddingX: 12,
|
||||
pageGap: 0,
|
||||
borderRadius: 0,
|
||||
imgRadius: 0,
|
||||
};
|
||||
|
||||
export default {
|
||||
name: "PanelFlexOne",
|
||||
hideDecorateTitle: true,
|
||||
components: { ColorField, SliderField, hotzone, ossManage },
|
||||
props: {
|
||||
res: { type: Object, required: true },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
defaults: DEFAULTS,
|
||||
activeTab: "content",
|
||||
tabs: [
|
||||
{ key: "content", title: "内容" },
|
||||
{ key: "style", title: "样式" },
|
||||
],
|
||||
picModelFlag: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
item() {
|
||||
return this.res.options.list[0];
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.res.options = Object.assign({}, DEFAULTS, this.res.options);
|
||||
if (!Array.isArray(this.res.options.list) || !this.res.options.list.length) {
|
||||
this.res.options.list = [
|
||||
{ img: "", url: "", link: "", size: "750*280", model: "hotzone" },
|
||||
];
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
pickImage() {
|
||||
this.picModelFlag = true;
|
||||
this.$nextTick(() => {
|
||||
const oss = this.$refs.ossManage;
|
||||
if (!oss) return;
|
||||
oss.selectImage = true;
|
||||
oss.fileType = "pic";
|
||||
oss.changeFileType();
|
||||
});
|
||||
},
|
||||
callbackSelected(val) {
|
||||
this.picModelFlag = false;
|
||||
if (val && val.url) {
|
||||
this.item.img = val.url;
|
||||
}
|
||||
},
|
||||
openHotzone() {
|
||||
if (!this.item.img) {
|
||||
ElMessage.warning("请先选择照片");
|
||||
return;
|
||||
}
|
||||
this.$refs.hotzone.open(this.item);
|
||||
},
|
||||
changeZone(zoneInfo) {
|
||||
this.item.zoneInfo = zoneInfo;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "../controls/panel-common.scss";
|
||||
.panel-flex-one {
|
||||
background: #f5f7fa;
|
||||
margin: 0 -20px;
|
||||
padding: 0 12px 12px;
|
||||
min-height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.panel-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 50px;
|
||||
margin-bottom: 12px;
|
||||
padding: 0 14px;
|
||||
background: #ffffff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04);
|
||||
box-sizing: border-box;
|
||||
.panel-name {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #303133;
|
||||
}
|
||||
}
|
||||
.panel-card {
|
||||
background: #ffffff;
|
||||
border-radius: 8px;
|
||||
padding: 14px;
|
||||
margin-bottom: 12px;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
.panel-tabs {
|
||||
display: inline-flex;
|
||||
background: #f4f4f5;
|
||||
border-radius: 8px;
|
||||
padding: 3px;
|
||||
.panel-tab {
|
||||
padding: 5px 14px;
|
||||
font-size: 13px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
color: #606266;
|
||||
transition: all 0.2s ease;
|
||||
&.active {
|
||||
background: #ffffff;
|
||||
color: #409eff;
|
||||
font-weight: 600;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
}
|
||||
}
|
||||
.section-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
margin: 0 0 12px;
|
||||
}
|
||||
.section-tip {
|
||||
font-size: 13px;
|
||||
color: #909399;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.img-preview {
|
||||
border: 1px dashed #dcdfe6;
|
||||
border-radius: 6px;
|
||||
padding: 6px;
|
||||
margin-bottom: 10px;
|
||||
img {
|
||||
width: 100%;
|
||||
display: block;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
.img-placeholder {
|
||||
height: 120px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px dashed #dcdfe6;
|
||||
border-radius: 6px;
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.btn-row {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
</style>
|
||||
619
seller/src/views/shop/wap/config-panel/PanelGoods.vue
Normal file
@@ -0,0 +1,619 @@
|
||||
<template>
|
||||
<div class="panel-goods">
|
||||
<div class="panel-header">
|
||||
<span class="panel-name">{{ res.name }}</span>
|
||||
<div class="panel-tabs">
|
||||
<div
|
||||
v-for="tab in tabs"
|
||||
:key="tab.key"
|
||||
class="panel-tab"
|
||||
:class="{ active: activeTab === tab.key }"
|
||||
@click="activeTab = tab.key"
|
||||
>
|
||||
{{ tab.title }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 旧版数据提示:保留原渲染,手动升级后才按新版配置 -->
|
||||
<div v-if="legacy" class="panel-card panel-card-fill">
|
||||
<div class="section-title">商品列表</div>
|
||||
<div class="section-tip">
|
||||
该楼层为旧版商品列表(分类标签 + 绑定商品),当前仍按旧版展示。
|
||||
升级后可使用展现形式、购物车按钮、商品筛选等新版功能,旧的分类与商品绑定将被清空。
|
||||
</div>
|
||||
<el-button type="primary" plain @click="upgradeLegacy">升级为新版</el-button>
|
||||
</div>
|
||||
|
||||
<template v-else-if="activeTab === 'content'">
|
||||
<div class="panel-card">
|
||||
<div class="section-title">展现形式</div>
|
||||
<div class="ctrl-row">
|
||||
<div class="ctrl-label">展现形式</div>
|
||||
<div class="ctrl-body">
|
||||
<div class="seg layout-seg">
|
||||
<div
|
||||
v-for="opt in layoutOptions"
|
||||
:key="opt.value"
|
||||
class="seg-item seg-icon"
|
||||
:class="{ active: firstItem.layout === opt.value }"
|
||||
@click="changeLayout(opt.value)"
|
||||
>
|
||||
<span class="ly-ico" :class="'ly-' + opt.value"><i></i><i></i><i></i></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel-card">
|
||||
<div class="section-title">显示内容</div>
|
||||
<div class="ctrl-row">
|
||||
<div class="ctrl-label">展示信息</div>
|
||||
<div class="ctrl-body">
|
||||
<el-checkbox v-model="firstItem.showName">商品名称</el-checkbox>
|
||||
<el-checkbox v-model="firstItem.showPrice">商品价格</el-checkbox>
|
||||
<el-checkbox v-model="firstItem.showSales">已售数量</el-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel-card">
|
||||
<div class="section-title">购物车按钮</div>
|
||||
<div class="ctrl-row">
|
||||
<div class="ctrl-label">是否显示</div>
|
||||
<div class="ctrl-body">
|
||||
<div class="seg layout-seg">
|
||||
<div
|
||||
class="seg-item"
|
||||
:class="{ active: firstItem.cartShow !== false }"
|
||||
@click="firstItem.cartShow = true"
|
||||
>
|
||||
显示
|
||||
</div>
|
||||
<div
|
||||
class="seg-item"
|
||||
:class="{ active: firstItem.cartShow === false }"
|
||||
@click="firstItem.cartShow = false"
|
||||
>
|
||||
隐藏
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ctrl-row" v-if="firstItem.cartShow !== false">
|
||||
<div class="ctrl-label">按钮样式</div>
|
||||
<div class="ctrl-body">
|
||||
<div class="seg layout-seg-3">
|
||||
<div
|
||||
v-for="opt in cartStyleOptions"
|
||||
:key="opt.value"
|
||||
class="seg-item seg-icon"
|
||||
:class="{ active: firstItem.cartStyle === opt.value }"
|
||||
@click="firstItem.cartStyle = opt.value"
|
||||
>
|
||||
<span v-if="opt.value === 'text'" class="cart-text-ico">加购</span>
|
||||
<span v-else-if="opt.value === 'plus'" class="cart-plus-ico">+</span>
|
||||
<span v-else class="cart-cart-ico">
|
||||
<svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor">
|
||||
<path
|
||||
d="M7 4h-2l-1 2h2l2.4 9.6a2 2 0 0 0 1.9 1.4h7.9a2 2 0 0 0 1.9-1.5l1.6-6.5h-12l-.8-3h11.5l.5-2h-13.4l-.0-.0zM10 20a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3zm8 0a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ctrl-row" v-if="firstItem.cartShow !== false">
|
||||
<div class="ctrl-label">按钮效果</div>
|
||||
<div class="ctrl-body">
|
||||
<div class="seg layout-seg">
|
||||
<div
|
||||
class="seg-item"
|
||||
:class="{ active: firstItem.cartAction !== 'add' }"
|
||||
@click="firstItem.cartAction = 'detail'"
|
||||
>
|
||||
进入商品详情页
|
||||
</div>
|
||||
<div
|
||||
class="seg-item"
|
||||
:class="{ active: firstItem.cartAction === 'add' }"
|
||||
@click="firstItem.cartAction = 'add'"
|
||||
>
|
||||
商品加购
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel-card panel-card-fill">
|
||||
<div class="section-title">商品设置</div>
|
||||
<div class="ctrl-row">
|
||||
<div class="ctrl-label">选择方式</div>
|
||||
<div class="ctrl-body">
|
||||
<el-radio-group v-model="firstItem.dataMode">
|
||||
<el-radio value="category">店铺分类</el-radio>
|
||||
<el-radio value="goods">指定商品</el-radio>
|
||||
<el-radio value="brand">商品品牌</el-radio>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="firstItem.dataMode === 'category'">
|
||||
<div class="ctrl-row">
|
||||
<div class="ctrl-label">店铺分类</div>
|
||||
<div class="ctrl-body">
|
||||
<el-cascader
|
||||
v-model="firstItem.categoryId"
|
||||
:options="categoryTree"
|
||||
:props="cascaderProps"
|
||||
placeholder="全部店铺分类"
|
||||
clearable
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<slider-field v-model="firstItem.count" label="商品数量" :min="1" :max="50" />
|
||||
<div class="ctrl-row">
|
||||
<div class="ctrl-label">商品排序</div>
|
||||
<div class="ctrl-body">
|
||||
<div class="seg layout-seg-3">
|
||||
<div
|
||||
v-for="opt in sortOptions"
|
||||
:key="opt.value"
|
||||
class="seg-item"
|
||||
:class="{ active: firstItem.sortType === opt.value }"
|
||||
@click="firstItem.sortType = opt.value"
|
||||
>
|
||||
{{ opt.label }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else-if="firstItem.dataMode === 'brand'">
|
||||
<div class="ctrl-row">
|
||||
<div class="ctrl-label">商品品牌</div>
|
||||
<div class="ctrl-body">
|
||||
<el-select v-model="firstItem.brandId" placeholder="全部品牌" clearable filterable>
|
||||
<el-option v-for="b in brandList" :key="b.id" :label="b.name" :value="b.id" />
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
<slider-field v-model="firstItem.count" label="商品数量" :min="1" :max="50" />
|
||||
<div class="ctrl-row">
|
||||
<div class="ctrl-label">商品排序</div>
|
||||
<div class="ctrl-body">
|
||||
<div class="seg layout-seg-3">
|
||||
<div
|
||||
v-for="opt in sortOptions"
|
||||
:key="opt.value"
|
||||
class="seg-item"
|
||||
:class="{ active: firstItem.sortType === opt.value }"
|
||||
@click="firstItem.sortType = opt.value"
|
||||
>
|
||||
{{ opt.label }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
|
||||
<template v-else>
|
||||
<div class="goods-wall">
|
||||
<div class="goods-wall-item" v-for="(g, i) in firstItem.goodsList" :key="g.id">
|
||||
<img :src="g.thumbnail" alt="" />
|
||||
<div class="goods-wall-del" @click="firstItem.goodsList.splice(i, 1)">
|
||||
<el-icon><Close /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
<div class="goods-wall-add" @click="pickGoods">
|
||||
<el-icon><Plus /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section-tip">最多选择 50 个商品,展示顺序与选择顺序一致</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div class="panel-card">
|
||||
<div class="section-title">颜色设置</div>
|
||||
<gradient-field
|
||||
v-model:start="res.options.bgColor1"
|
||||
v-model:end="res.options.bgColor2"
|
||||
:default-start="defaults.bgColor1"
|
||||
:default-end="defaults.bgColor2"
|
||||
/>
|
||||
<color-field
|
||||
v-model="res.options.titleColor"
|
||||
label="商品标题颜色"
|
||||
:default-value="defaults.titleColor"
|
||||
/>
|
||||
<div class="ctrl-row">
|
||||
<div class="ctrl-label">色调</div>
|
||||
<div class="ctrl-body">
|
||||
<div class="seg layout-seg">
|
||||
<div
|
||||
class="seg-item"
|
||||
:class="{ active: res.options.toneMode !== 'custom' }"
|
||||
@click="res.options.toneMode = 'theme'"
|
||||
>
|
||||
跟随主题风格
|
||||
</div>
|
||||
<div
|
||||
class="seg-item"
|
||||
:class="{ active: res.options.toneMode === 'custom' }"
|
||||
@click="res.options.toneMode = 'custom'"
|
||||
>
|
||||
自定义
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<color-field
|
||||
v-if="res.options.toneMode === 'custom'"
|
||||
v-model="res.options.priceColor"
|
||||
label="价格颜色"
|
||||
:default-value="defaults.priceColor"
|
||||
/>
|
||||
<color-field
|
||||
v-model="res.options.salesColor"
|
||||
label="已售数量颜色"
|
||||
:default-value="defaults.salesColor"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="panel-card">
|
||||
<div class="section-title">边距设置</div>
|
||||
<slider-field v-model="res.options.paddingTop" label="上边距" :min="0" :max="100" />
|
||||
<slider-field v-model="res.options.paddingBottom" label="下边距" :min="0" :max="100" />
|
||||
<slider-field v-model="res.options.paddingX" label="左右边距" :min="0" :max="40" />
|
||||
<slider-field v-model="res.options.contentGap" label="内容间距" :min="0" :max="50" />
|
||||
<slider-field v-model="res.options.pageGap" label="页面间距" :min="0" :max="100" />
|
||||
</div>
|
||||
|
||||
<div class="panel-card panel-card-fill">
|
||||
<div class="section-title">圆角设置</div>
|
||||
<slider-field v-model="res.options.borderRadius" label="背景圆角" :min="0" :max="30" />
|
||||
<slider-field v-model="res.options.contentRadius" label="内容圆角" :min="0" :max="30" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<liliDialog ref="liliDialogRef" @selectedGoodsData="onGoodsSelected"></liliDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Close, Plus } from "@element-plus/icons-vue";
|
||||
import GradientField from "../controls/GradientField.vue";
|
||||
import ColorField from "../controls/ColorField.vue";
|
||||
import SliderField from "../controls/SliderField.vue";
|
||||
import { getShopGoodsLabelListSeller, getBrandListData } from "@/api/goods";
|
||||
|
||||
const DEFAULTS = {
|
||||
bgColor1: "#FFFFFF",
|
||||
bgColor2: "#FFFFFF",
|
||||
titleColor: "#282828",
|
||||
toneMode: "theme",
|
||||
priceColor: "#FF5C58",
|
||||
salesColor: "#666666",
|
||||
paddingTop: 10,
|
||||
paddingBottom: 10,
|
||||
paddingX: 12,
|
||||
contentGap: 10,
|
||||
pageGap: 0,
|
||||
borderRadius: 0,
|
||||
contentRadius: 7,
|
||||
};
|
||||
const DEFAULT_ITEM = {
|
||||
layout: "list",
|
||||
showName: true,
|
||||
showPrice: true,
|
||||
showSales: true,
|
||||
cartShow: true,
|
||||
cartStyle: "plus",
|
||||
cartAction: "detail",
|
||||
dataMode: "category",
|
||||
categoryId: "",
|
||||
brandId: "",
|
||||
storeId: "",
|
||||
goodsList: [],
|
||||
count: 6,
|
||||
sortType: "default",
|
||||
};
|
||||
|
||||
export default {
|
||||
name: "PanelGoods",
|
||||
hideDecorateTitle: true,
|
||||
components: { GradientField, ColorField, SliderField, Close, Plus },
|
||||
props: {
|
||||
res: { type: Object, required: true },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
defaults: DEFAULTS,
|
||||
activeTab: "content",
|
||||
tabs: [
|
||||
{ key: "content", title: "内容" },
|
||||
{ key: "style", title: "样式" },
|
||||
],
|
||||
legacy: false,
|
||||
layoutOptions: [
|
||||
{ value: "card", label: "卡片" },
|
||||
{ value: "list", label: "列表" },
|
||||
{ value: "three", label: "三列" },
|
||||
{ value: "big", label: "大图" },
|
||||
],
|
||||
sortOptions: [
|
||||
{ value: "default", label: "综合" },
|
||||
{ value: "sales", label: "销量" },
|
||||
{ value: "price", label: "价格" },
|
||||
],
|
||||
cascaderProps: {
|
||||
label: "label",
|
||||
value: "value",
|
||||
children: "children",
|
||||
checkStrictly: true,
|
||||
emitPath: false,
|
||||
},
|
||||
categoryTree: [],
|
||||
brandList: [],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
firstItem() {
|
||||
return this.res.options.list[0];
|
||||
},
|
||||
// 卡片/大图支持文字按钮,两列/三列只有图标按钮
|
||||
cartStyleOptions() {
|
||||
const base = [
|
||||
{ value: "plus", label: "圆形加号" },
|
||||
{ value: "cart", label: "购物车" },
|
||||
];
|
||||
if (this.firstItem.layout === "card" || this.firstItem.layout === "big") {
|
||||
return [{ value: "text", label: "加购文字" }, ...base];
|
||||
}
|
||||
return base;
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.res.options = Object.assign({}, DEFAULTS, this.res.options);
|
||||
if (!Array.isArray(this.res.options.list) || !this.res.options.list[0]) {
|
||||
this.res.options.list = [{ ...DEFAULT_ITEM }];
|
||||
}
|
||||
const item = this.firstItem;
|
||||
if (item.layout === undefined && Array.isArray(item.titleWay)) {
|
||||
this.legacy = true;
|
||||
return;
|
||||
}
|
||||
this.fillItemDefaults(item);
|
||||
this.loadCategories();
|
||||
this.loadBrands();
|
||||
},
|
||||
methods: {
|
||||
formatShopCategory(list) {
|
||||
return (list || []).map((item) => ({
|
||||
value: item.id,
|
||||
label: item.labelName,
|
||||
children: item.children && item.children.length ? this.formatShopCategory(item.children) : undefined,
|
||||
}));
|
||||
},
|
||||
loadCategories() {
|
||||
getShopGoodsLabelListSeller().then((res) => {
|
||||
if (res && res.success) {
|
||||
this.categoryTree = this.formatShopCategory(res.result || []);
|
||||
}
|
||||
});
|
||||
},
|
||||
loadBrands() {
|
||||
getBrandListData().then((res) => {
|
||||
this.brandList = Array.isArray(res) ? res : (res && res.result) || [];
|
||||
});
|
||||
},
|
||||
fillItemDefaults(item) {
|
||||
Object.keys(DEFAULT_ITEM).forEach((key) => {
|
||||
if (item[key] === undefined) {
|
||||
item[key] = Array.isArray(DEFAULT_ITEM[key])
|
||||
? [...DEFAULT_ITEM[key]]
|
||||
: DEFAULT_ITEM[key];
|
||||
}
|
||||
});
|
||||
},
|
||||
upgradeLegacy() {
|
||||
const item = this.firstItem;
|
||||
delete item.titleWay;
|
||||
delete item.listWay;
|
||||
this.fillItemDefaults(item);
|
||||
this.legacy = false;
|
||||
this.loadCategories();
|
||||
this.loadBrands();
|
||||
},
|
||||
changeLayout(val) {
|
||||
this.firstItem.layout = val;
|
||||
// 两列/三列不支持文字按钮,切布局时兜底
|
||||
if (
|
||||
(val === "list" || val === "three") &&
|
||||
this.firstItem.cartStyle === "text"
|
||||
) {
|
||||
this.firstItem.cartStyle = "plus";
|
||||
}
|
||||
},
|
||||
pickGoods() {
|
||||
const dialog = this.$refs.liliDialogRef;
|
||||
dialog.clearGoodsSelected();
|
||||
dialog.goodsFlag = true;
|
||||
dialog.flag = true;
|
||||
},
|
||||
onGoodsSelected(val) {
|
||||
if (!Array.isArray(val) || !val.length) return;
|
||||
const list = val.map((item) => ({
|
||||
id: item.id,
|
||||
goodsId: item.goodsId,
|
||||
goodsName: item.goodsName,
|
||||
price: item.price,
|
||||
thumbnail: item.thumbnail,
|
||||
buyCount: item.buyCount,
|
||||
}));
|
||||
const exist = new Set(this.firstItem.goodsList.map((g) => g.id));
|
||||
const merged = this.firstItem.goodsList.concat(
|
||||
list.filter((g) => !exist.has(g.id))
|
||||
);
|
||||
this.firstItem.goodsList = merged.slice(0, 50);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "../controls/field-common.scss";
|
||||
@import "../controls/panel-common.scss";
|
||||
|
||||
.seg-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 40px;
|
||||
padding: 6px 10px;
|
||||
}
|
||||
|
||||
// 展现形式小图标
|
||||
.ly-ico {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
height: 14px;
|
||||
i {
|
||||
display: block;
|
||||
background: currentColor;
|
||||
border-radius: 1px;
|
||||
}
|
||||
}
|
||||
.ly-card {
|
||||
i:first-child {
|
||||
width: 8px;
|
||||
height: 10px;
|
||||
}
|
||||
i:not(:first-child) {
|
||||
width: 12px;
|
||||
height: 2px;
|
||||
}
|
||||
flex-wrap: wrap;
|
||||
width: 24px;
|
||||
align-content: center;
|
||||
}
|
||||
.ly-list {
|
||||
i {
|
||||
width: 8px;
|
||||
height: 10px;
|
||||
&:nth-child(3) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
.ly-three {
|
||||
i {
|
||||
width: 6px;
|
||||
height: 10px;
|
||||
}
|
||||
}
|
||||
.ly-big {
|
||||
i:first-child {
|
||||
width: 22px;
|
||||
height: 10px;
|
||||
}
|
||||
i:not(:first-child) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
// 购物车按钮样式小图标
|
||||
.cart-text-ico {
|
||||
display: inline-block;
|
||||
padding: 1px 8px;
|
||||
border-radius: 8px;
|
||||
background: #c0c4cc;
|
||||
color: #fff;
|
||||
font-size: 11px;
|
||||
line-height: 14px;
|
||||
}
|
||||
.cart-plus-ico {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
background: #c0c4cc;
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
}
|
||||
.seg-item.active .cart-text-ico,
|
||||
.seg-item.active .cart-plus-ico {
|
||||
background: #409eff;
|
||||
}
|
||||
.cart-cart-ico {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.goods-wall {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.goods-wall-item {
|
||||
position: relative;
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
border: 1px solid #ebeef5;
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
.goods-wall-del {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: 2px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 50%;
|
||||
background: rgba(0, 0, 0, 0.55);
|
||||
color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
.goods-wall-add {
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
border: 1px dashed #dcdfe6;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #909399;
|
||||
font-size: 22px;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
border-color: #409eff;
|
||||
color: #409eff;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
86
seller/src/views/shop/wap/config-panel/PanelLine.vue
Normal file
@@ -0,0 +1,86 @@
|
||||
<template>
|
||||
<div class="panel-line">
|
||||
<div class="panel-header">
|
||||
<span class="panel-name">{{ res.name }}</span>
|
||||
</div>
|
||||
|
||||
<div class="panel-card">
|
||||
<div class="section-title">颜色设置</div>
|
||||
<gradient-field
|
||||
v-model:start="res.options.bgColor1"
|
||||
v-model:end="res.options.bgColor2"
|
||||
:default-start="defaults.bgColor1"
|
||||
:default-end="defaults.bgColor2"
|
||||
/>
|
||||
<color-field
|
||||
v-model="res.options.lineColor"
|
||||
label="线条颜色"
|
||||
:default-value="defaults.lineColor"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="panel-card panel-card-fill">
|
||||
<div class="section-title">样式设置</div>
|
||||
<segment-field
|
||||
v-model="res.options.lineStyle"
|
||||
label="线条样式"
|
||||
:options="lineStyleOptions"
|
||||
/>
|
||||
<slider-field v-model="res.options.lineHeight" label="线条高度" :min="1" :max="100" />
|
||||
<slider-field v-model="res.options.paddingX" label="左右边距" :min="0" :max="40" />
|
||||
<slider-field v-model="res.options.pageGap" label="页面间距" :min="0" :max="100" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ColorField from "../controls/ColorField.vue";
|
||||
import GradientField from "../controls/GradientField.vue";
|
||||
import SliderField from "../controls/SliderField.vue";
|
||||
import SegmentField from "../controls/SegmentField.vue";
|
||||
|
||||
const DEFAULTS = {
|
||||
bgColor: "#FFFFFF",
|
||||
bgColor1: "#FFFFFF",
|
||||
bgColor2: "#FFFFFF",
|
||||
lineColor: "#979797",
|
||||
lineStyle: "dashed",
|
||||
lineHeight: 1,
|
||||
paddingX: 0,
|
||||
pageGap: 0,
|
||||
};
|
||||
|
||||
export default {
|
||||
name: "PanelLine",
|
||||
hideDecorateTitle: true,
|
||||
components: { ColorField, GradientField, SliderField, SegmentField },
|
||||
props: {
|
||||
res: { type: Object, required: true },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
defaults: DEFAULTS,
|
||||
lineStyleOptions: [
|
||||
{ label: "虚线", value: "dashed" },
|
||||
{ label: "实线", value: "solid" },
|
||||
{ label: "点状线", value: "dotted" },
|
||||
],
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.res.options = Object.assign({}, DEFAULTS, this.res.options);
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "../controls/panel-common.scss";
|
||||
|
||||
.panel-line {
|
||||
background: #f5f7fa;
|
||||
margin: 0 -20px;
|
||||
padding: 0 12px 12px;
|
||||
min-height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
</style>
|
||||
427
seller/src/views/shop/wap/config-panel/PanelNotice.vue
Normal file
@@ -0,0 +1,427 @@
|
||||
<template>
|
||||
<div class="panel-notice">
|
||||
<div class="panel-header">
|
||||
<span class="panel-name">{{ res.name }}</span>
|
||||
<div class="panel-tabs">
|
||||
<div
|
||||
v-for="tab in tabs"
|
||||
:key="tab.key"
|
||||
class="panel-tab"
|
||||
:class="{ active: activeTab === tab.key }"
|
||||
@click="activeTab = tab.key"
|
||||
>
|
||||
{{ tab.title }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="activeTab === 'content'">
|
||||
<div class="panel-card">
|
||||
<div class="section-title">图标设置</div>
|
||||
<div class="section-tip">支持上传 1 张图片,建议宽度 130*36px</div>
|
||||
<div class="icon-row">
|
||||
<div class="icon-preview" v-if="item.img">
|
||||
<img :src="item.img" alt="" />
|
||||
</div>
|
||||
<div class="icon-placeholder" v-else>图标</div>
|
||||
<el-button plain type="primary" @click="pickImage">上传图标</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel-card panel-card-fill">
|
||||
<div class="section-title">公告设置</div>
|
||||
<div class="section-tip">最多可添加 10 个版块</div>
|
||||
<div class="notice-item" v-for="(notice, index) in item.title" :key="index">
|
||||
<div class="notice-item-head">
|
||||
<span>版块 {{ index + 1 }}</span>
|
||||
<div class="img-item-ops">
|
||||
<el-icon :class="{ disabled: index === 0 }" @click="moveItem(index, -1)"><Top /></el-icon>
|
||||
<el-icon
|
||||
:class="{ disabled: index === item.title.length - 1 }"
|
||||
@click="moveItem(index, 1)"
|
||||
><Bottom
|
||||
/></el-icon>
|
||||
<el-icon class="del" @click="removeItem(index)"><Delete /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field-row">
|
||||
<div class="field-label">标题</div>
|
||||
<el-input v-model="notice.context" maxlength="50" placeholder="请输入公告内容" />
|
||||
</div>
|
||||
<div class="field-row">
|
||||
<div class="field-label">链接</div>
|
||||
<div class="link-text" v-if="notice.url && notice.url.___type">{{ linkText(notice.url) }}</div>
|
||||
<el-button plain type="primary" @click="pickLink(notice)">选择链接</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<el-button
|
||||
v-if="item.title.length < 10"
|
||||
class="add-btn"
|
||||
plain
|
||||
type="primary"
|
||||
@click="addItem"
|
||||
>+ 添加模块</el-button
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div class="panel-card">
|
||||
<div class="section-title">文字设置</div>
|
||||
<div class="ctrl-row">
|
||||
<div class="ctrl-label">滚动方向</div>
|
||||
<div class="ctrl-body">
|
||||
<div class="seg layout-seg">
|
||||
<div
|
||||
class="seg-item"
|
||||
:class="{ active: item.direction !== 'horizontal' }"
|
||||
@click="item.direction = 'vertical'"
|
||||
>
|
||||
上下
|
||||
</div>
|
||||
<div
|
||||
class="seg-item"
|
||||
:class="{ active: item.direction === 'horizontal' }"
|
||||
@click="item.direction = 'horizontal'"
|
||||
>
|
||||
左右
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ctrl-row">
|
||||
<div class="ctrl-label">文本位置</div>
|
||||
<div class="ctrl-body">
|
||||
<div class="seg layout-seg-3">
|
||||
<div
|
||||
v-for="opt in alignOptions"
|
||||
:key="opt.value"
|
||||
class="seg-item"
|
||||
:class="{ active: (item.textAlign || 'left') === opt.value }"
|
||||
@click="item.textAlign = opt.value"
|
||||
>
|
||||
{{ opt.title }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<color-field
|
||||
v-model="item.color"
|
||||
label="文字颜色"
|
||||
:default-value="defaults.itemColor"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="panel-card">
|
||||
<div class="section-title">颜色设置</div>
|
||||
<gradient-field
|
||||
v-model:start="res.options.bgColor1"
|
||||
v-model:end="res.options.bgColor2"
|
||||
:default-start="defaults.bgColor1"
|
||||
:default-end="defaults.bgColor2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="panel-card">
|
||||
<div class="section-title">边距设置</div>
|
||||
<slider-field v-model="res.options.paddingTop" label="上边距" :min="0" :max="100" />
|
||||
<slider-field v-model="res.options.paddingBottom" label="下边距" :min="0" :max="100" />
|
||||
<slider-field v-model="res.options.paddingX" label="左右边距" :min="0" :max="40" />
|
||||
<slider-field v-model="res.options.pageGap" label="页面间距" :min="0" :max="100" />
|
||||
</div>
|
||||
|
||||
<div class="panel-card panel-card-fill">
|
||||
<div class="section-title">圆角设置</div>
|
||||
<slider-field v-model="res.options.borderRadius" label="背景圆角" :min="0" :max="30" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<liliDialog ref="liliDialog" @selectedLink="selectedLink"></liliDialog>
|
||||
|
||||
<el-dialog v-model="picModelFlag" width="1200px" append-to-body destroy-on-close>
|
||||
<ossManage
|
||||
@callback="callbackSelected"
|
||||
:is-component="true"
|
||||
:initialize="picModelFlag"
|
||||
:max-select="1"
|
||||
ref="ossManage"
|
||||
/>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Top, Bottom, Delete } from "@element-plus/icons-vue";
|
||||
import ColorField from "../controls/ColorField.vue";
|
||||
import GradientField from "../controls/GradientField.vue";
|
||||
import SliderField from "../controls/SliderField.vue";
|
||||
import ossManage from "@/views/sys/oss-manage/ossManage";
|
||||
import ways from "@/views/lili-dialog/wap.js";
|
||||
|
||||
const DEFAULTS = {
|
||||
bgColor1: "#FFFFFF",
|
||||
bgColor2: "#FFFFFF",
|
||||
paddingTop: 0,
|
||||
paddingBottom: 0,
|
||||
paddingX: 12,
|
||||
pageGap: 0,
|
||||
borderRadius: 0,
|
||||
itemColor: "#333333",
|
||||
};
|
||||
|
||||
export default {
|
||||
name: "PanelNotice",
|
||||
hideDecorateTitle: true,
|
||||
components: { ColorField, GradientField, SliderField, ossManage, Top, Bottom, Delete },
|
||||
props: {
|
||||
res: { type: Object, required: true },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
defaults: DEFAULTS,
|
||||
activeTab: "content",
|
||||
tabs: [
|
||||
{ key: "content", title: "内容" },
|
||||
{ key: "style", title: "样式" },
|
||||
],
|
||||
alignOptions: [
|
||||
{ value: "left", title: "居左" },
|
||||
{ value: "center", title: "居中" },
|
||||
{ value: "right", title: "居右" },
|
||||
],
|
||||
picModelFlag: false,
|
||||
currentItem: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
item() {
|
||||
return this.res.options.list[0];
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.res.options = Object.assign({}, DEFAULTS, this.res.options);
|
||||
if (!Array.isArray(this.res.options.list) || !this.res.options.list.length) {
|
||||
this.res.options.list = [{ title: [], color: "#333333", direction: "vertical", img: "" }];
|
||||
}
|
||||
const item = this.item;
|
||||
if (!Array.isArray(item.title) || !item.title.length) {
|
||||
item.title = [{ context: "这是一条公告", url: "" }];
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
linkText(url) {
|
||||
const way = ways.find((e) => e.name === url.___type);
|
||||
const typeTitle = way ? way.title : "发现";
|
||||
const name =
|
||||
url.name || url.goodsName || url.memberName || url.title || "";
|
||||
return name ? `${typeTitle} - ${name}` : typeTitle;
|
||||
},
|
||||
pickImage() {
|
||||
this.picModelFlag = true;
|
||||
this.$nextTick(() => {
|
||||
const oss = this.$refs.ossManage;
|
||||
if (!oss) return;
|
||||
oss.selectImage = true;
|
||||
oss.fileType = "pic";
|
||||
oss.changeFileType();
|
||||
});
|
||||
},
|
||||
callbackSelected(val) {
|
||||
this.picModelFlag = false;
|
||||
if (val && val.url) {
|
||||
this.item.img = val.url;
|
||||
}
|
||||
},
|
||||
pickLink(notice) {
|
||||
this.currentItem = notice;
|
||||
const dialog = this.$refs.liliDialog;
|
||||
dialog.clearGoodsSelected();
|
||||
dialog.goodsFlag = false;
|
||||
dialog.flag = true;
|
||||
},
|
||||
selectedLink(val) {
|
||||
if (!this.currentItem) return;
|
||||
delete val.selected;
|
||||
delete val.intro;
|
||||
delete val.mobileIntro;
|
||||
this.currentItem.url = val;
|
||||
},
|
||||
moveItem(index, dir) {
|
||||
const list = this.item.title;
|
||||
const target = index + dir;
|
||||
if (target < 0 || target >= list.length) return;
|
||||
list.splice(target, 1, ...list.splice(index, 1, list[target]));
|
||||
},
|
||||
removeItem(index) {
|
||||
this.item.title.splice(index, 1);
|
||||
},
|
||||
addItem() {
|
||||
this.item.title.push({ context: "", url: "" });
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "../controls/field-common.scss";
|
||||
@import "../controls/panel-common.scss";
|
||||
.panel-notice {
|
||||
background: #f5f7fa;
|
||||
margin: 0 -20px;
|
||||
padding: 0 12px 12px;
|
||||
min-height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.panel-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 50px;
|
||||
margin-bottom: 12px;
|
||||
padding: 0 14px;
|
||||
background: #ffffff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04);
|
||||
box-sizing: border-box;
|
||||
.panel-name {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #303133;
|
||||
}
|
||||
}
|
||||
.panel-tabs {
|
||||
display: inline-flex;
|
||||
background: #f4f4f5;
|
||||
border-radius: 8px;
|
||||
padding: 3px;
|
||||
.panel-tab {
|
||||
padding: 5px 14px;
|
||||
font-size: 13px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
color: #606266;
|
||||
transition: all 0.2s ease;
|
||||
&.active {
|
||||
background: #ffffff;
|
||||
color: #409eff;
|
||||
font-weight: 600;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
}
|
||||
}
|
||||
.panel-card {
|
||||
background: #ffffff;
|
||||
border-radius: 8px;
|
||||
padding: 14px;
|
||||
margin-bottom: 12px;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
.section-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
margin: 0 0 12px;
|
||||
}
|
||||
.section-tip {
|
||||
font-size: 13px;
|
||||
color: #909399;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.icon-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.icon-preview {
|
||||
width: 88px;
|
||||
height: 32px;
|
||||
border: 1px dashed #dcdfe6;
|
||||
border-radius: 6px;
|
||||
padding: 3px;
|
||||
box-sizing: border-box;
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
.icon-placeholder {
|
||||
width: 88px;
|
||||
height: 32px;
|
||||
border: 1px dashed #dcdfe6;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
}
|
||||
.notice-item {
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
margin-bottom: 10px;
|
||||
.notice-item-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-size: 14px;
|
||||
color: #303133;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
.img-item-ops {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
.el-icon {
|
||||
padding: 4px;
|
||||
cursor: pointer;
|
||||
color: #606266;
|
||||
&:hover {
|
||||
color: #409eff;
|
||||
}
|
||||
&.del:hover {
|
||||
color: #e1251b;
|
||||
}
|
||||
&.disabled {
|
||||
color: #dcdfe6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
}
|
||||
.field-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.field-label {
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
margin-right: 10px;
|
||||
flex-shrink: 0;
|
||||
width: 32px;
|
||||
}
|
||||
.el-input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.el-button {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
.link-text {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.add-btn {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
213
seller/src/views/shop/wap/config-panel/PanelPageSetting.vue
Normal file
@@ -0,0 +1,213 @@
|
||||
<template>
|
||||
<div class="panel-page-setting">
|
||||
<div class="panel-header">
|
||||
<span class="panel-name">页面设置</span>
|
||||
<div class="panel-tabs">
|
||||
<div
|
||||
v-for="tab in tabs"
|
||||
:key="tab.key"
|
||||
class="panel-tab"
|
||||
:class="{ active: activeTab === tab.key }"
|
||||
@click="activeTab = tab.key"
|
||||
>
|
||||
{{ tab.title }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="activeTab === 'content'">
|
||||
<div class="panel-card panel-card-fill">
|
||||
<div class="section-title">基础设置</div>
|
||||
<div class="ctrl-row">
|
||||
<div class="ctrl-label"><span class="required-star">*</span>模板名称</div>
|
||||
<div class="ctrl-body">
|
||||
<el-input v-model="res.name" maxlength="30" placeholder="请输入模板名称" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="ctrl-row">
|
||||
<div class="ctrl-label"><span class="required-star">*</span>页面标题</div>
|
||||
<div class="ctrl-body">
|
||||
<el-input v-model="res.title" maxlength="30" placeholder="请输入页面标题" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div class="panel-card panel-card-fill">
|
||||
<div class="section-title">颜色设置</div>
|
||||
<segment-field v-model="res.bgType" label="背景设置" :options="bgTypeOptions" />
|
||||
<color-field
|
||||
v-if="res.bgType === 'color'"
|
||||
v-model="res.bgColor"
|
||||
label="背景颜色"
|
||||
:default-value="defaults.bgColor"
|
||||
/>
|
||||
<template v-else>
|
||||
<div class="ctrl-row">
|
||||
<div class="ctrl-label">背景图片</div>
|
||||
<div class="ctrl-body bg-image-body">
|
||||
<div class="bg-image-preview" v-if="res.bgImage">
|
||||
<img :src="res.bgImage" alt="" />
|
||||
<el-icon class="bg-image-clear" @click="clearImage"><Close /></el-icon>
|
||||
</div>
|
||||
<el-button plain type="primary" @click="pickImage">上传图片</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section-tip upload-tip">上传图片 建议:宽度750px</div>
|
||||
<segment-field v-model="res.bgFill" label="填充方式" :options="bgFillOptions" />
|
||||
</template>
|
||||
<color-field
|
||||
v-model="res.topColor"
|
||||
label="顶部颜色"
|
||||
:default-value="defaults.topColor"
|
||||
/>
|
||||
<segment-field v-model="res.titleColor" label="标题颜色" :options="titleColorOptions" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-dialog v-model="picModelFlag" width="1200px" append-to-body destroy-on-close>
|
||||
<ossManage
|
||||
@callback="callbackSelected"
|
||||
:is-component="true"
|
||||
:initialize="picModelFlag"
|
||||
:max-select="1"
|
||||
ref="ossManage"
|
||||
/>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ColorField from "../controls/ColorField.vue";
|
||||
import SegmentField from "../controls/SegmentField.vue";
|
||||
import ossManage from "@/views/sys/oss-manage/ossManage";
|
||||
import { Close } from "@element-plus/icons-vue";
|
||||
|
||||
const DEFAULTS = {
|
||||
name: "",
|
||||
title: "",
|
||||
bgType: "color",
|
||||
bgColor: "#F5F5F5",
|
||||
bgImage: "",
|
||||
bgFill: "stretch",
|
||||
topColor: "#ffffff",
|
||||
titleColor: "black",
|
||||
};
|
||||
|
||||
export default {
|
||||
name: "PanelPageSetting",
|
||||
components: { ColorField, SegmentField, ossManage, Close },
|
||||
props: {
|
||||
res: { type: Object, required: true },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
defaults: DEFAULTS,
|
||||
activeTab: "content",
|
||||
tabs: [
|
||||
{ key: "content", title: "内容" },
|
||||
{ key: "style", title: "样式" },
|
||||
],
|
||||
bgTypeOptions: [
|
||||
{ label: "背景色", value: "color" },
|
||||
{ label: "背景图", value: "image" },
|
||||
],
|
||||
bgFillOptions: [
|
||||
{ label: "拉伸", value: "stretch" },
|
||||
{ label: "缩放", value: "contain" },
|
||||
{ label: "填充", value: "cover" },
|
||||
],
|
||||
titleColorOptions: [
|
||||
{ label: "白色", value: "white" },
|
||||
{ label: "黑色", value: "black" },
|
||||
],
|
||||
picModelFlag: false,
|
||||
};
|
||||
},
|
||||
created() {
|
||||
Object.keys(DEFAULTS).forEach((key) => {
|
||||
if (this.res[key] === undefined) {
|
||||
this.res[key] = DEFAULTS[key];
|
||||
}
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
pickImage() {
|
||||
this.picModelFlag = true;
|
||||
this.$nextTick(() => {
|
||||
const oss = this.$refs.ossManage;
|
||||
if (!oss) return;
|
||||
oss.selectImage = true;
|
||||
oss.fileType = "pic";
|
||||
oss.changeFileType();
|
||||
});
|
||||
},
|
||||
callbackSelected(val) {
|
||||
this.picModelFlag = false;
|
||||
if (val && val.url) {
|
||||
this.res.bgImage = val.url;
|
||||
}
|
||||
},
|
||||
clearImage() {
|
||||
this.res.bgImage = "";
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "../controls/field-common.scss";
|
||||
@import "../controls/panel-common.scss";
|
||||
|
||||
.panel-page-setting {
|
||||
background: #f5f7fa;
|
||||
margin: 0 -20px;
|
||||
padding: 0 12px 12px;
|
||||
min-height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.required-star {
|
||||
color: #f56c6c;
|
||||
margin-right: 2px;
|
||||
}
|
||||
.bg-image-body {
|
||||
align-items: center;
|
||||
}
|
||||
.bg-image-preview {
|
||||
position: relative;
|
||||
width: 88px;
|
||||
height: 48px;
|
||||
border: 1px dashed #dcdfe6;
|
||||
border-radius: 6px;
|
||||
padding: 3px;
|
||||
box-sizing: border-box;
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
object-fit: cover;
|
||||
}
|
||||
.bg-image-clear {
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
right: -8px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
background: #909399;
|
||||
color: #ffffff;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
&:hover {
|
||||
background: #f56c6c;
|
||||
}
|
||||
}
|
||||
}
|
||||
.upload-tip {
|
||||
margin: -8px 0 12px 102px;
|
||||
}
|
||||
</style>
|
||||
98
seller/src/views/shop/wap/config-panel/PanelRichText.vue
Normal file
@@ -0,0 +1,98 @@
|
||||
<template>
|
||||
<div class="panel-rich-text">
|
||||
<div class="panel-header">
|
||||
<span class="panel-name">{{ res.name }}</span>
|
||||
<div class="panel-tabs">
|
||||
<div
|
||||
v-for="tab in tabs"
|
||||
:key="tab.key"
|
||||
class="panel-tab"
|
||||
:class="{ active: activeTab === tab.key }"
|
||||
@click="activeTab = tab.key"
|
||||
>
|
||||
{{ tab.title }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="activeTab === 'content'">
|
||||
<div class="panel-card panel-card-fill">
|
||||
<div class="section-title">富文本内容</div>
|
||||
<editor v-model="res.options.content" height="300px" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div class="panel-card">
|
||||
<div class="section-title">颜色设置</div>
|
||||
<gradient-field
|
||||
v-model:start="res.options.bgColor1"
|
||||
v-model:end="res.options.bgColor2"
|
||||
:default-start="defaults.bgColor1"
|
||||
:default-end="defaults.bgColor2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="panel-card">
|
||||
<div class="section-title">边距设置</div>
|
||||
<slider-field v-model="res.options.paddingX" label="左右边距" :min="0" :max="40" />
|
||||
<slider-field v-model="res.options.pageGap" label="页面间距" :min="0" :max="100" />
|
||||
</div>
|
||||
|
||||
<div class="panel-card panel-card-fill">
|
||||
<div class="section-title">圆角设置</div>
|
||||
<slider-field v-model="res.options.borderRadius" label="背景圆角" :min="0" :max="30" />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import editor from "@/views/lili-components/editor";
|
||||
import GradientField from "../controls/GradientField.vue";
|
||||
import SliderField from "../controls/SliderField.vue";
|
||||
|
||||
const DEFAULTS = {
|
||||
content: "",
|
||||
bgColor: "#FFFFFF",
|
||||
bgColor1: "#FFFFFF",
|
||||
bgColor2: "#FFFFFF",
|
||||
paddingX: 12,
|
||||
pageGap: 0,
|
||||
borderRadius: 0,
|
||||
};
|
||||
|
||||
export default {
|
||||
name: "PanelRichText",
|
||||
hideDecorateTitle: true,
|
||||
components: { editor, GradientField, SliderField },
|
||||
props: {
|
||||
res: { type: Object, required: true },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
defaults: DEFAULTS,
|
||||
activeTab: "content",
|
||||
tabs: [
|
||||
{ key: "content", title: "内容" },
|
||||
{ key: "style", title: "样式" },
|
||||
],
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.res.options = Object.assign({}, DEFAULTS, this.res.options);
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "../controls/panel-common.scss";
|
||||
|
||||
.panel-rich-text {
|
||||
background: #f5f7fa;
|
||||
margin: 0 -20px;
|
||||
padding: 0 12px 12px;
|
||||
min-height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
</style>
|
||||
280
seller/src/views/shop/wap/config-panel/PanelSearch.vue
Normal file
@@ -0,0 +1,280 @@
|
||||
<template>
|
||||
<div class="panel-search">
|
||||
<div class="panel-header">
|
||||
<span class="panel-name">{{ res.name }}</span>
|
||||
<div class="panel-tabs">
|
||||
<div
|
||||
v-for="tab in tabs"
|
||||
:key="tab.key"
|
||||
class="panel-tab"
|
||||
:class="{ active: activeTab === tab.key }"
|
||||
@click="activeTab = tab.key"
|
||||
>
|
||||
{{ tab.title }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="activeTab === 'content'">
|
||||
<div class="panel-card">
|
||||
<div class="section-title">提示文字设置</div>
|
||||
<div class="field-row">
|
||||
<div class="field-label">提示文字</div>
|
||||
<el-input
|
||||
v-model="res.options.list[0].title"
|
||||
size="default"
|
||||
maxlength="20"
|
||||
placeholder="搜索商品名称"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel-card panel-card-fill">
|
||||
<div class="section-title">搜索热词</div>
|
||||
<div class="hot-word" v-for="(word, index) in res.options.hotWords" :key="index">
|
||||
<el-input
|
||||
v-model="res.options.hotWords[index]"
|
||||
size="default"
|
||||
maxlength="10"
|
||||
placeholder="选填,不超过十个字"
|
||||
/>
|
||||
<el-icon class="del" @click="removeWord(index)"><CircleClose /></el-icon>
|
||||
</div>
|
||||
<el-button class="add-btn" plain type="primary" @click="addWord">
|
||||
+ 添加热词
|
||||
</el-button>
|
||||
<div class="field-row interval-row">
|
||||
<div class="field-label">切换时间</div>
|
||||
<el-input-number
|
||||
class="interval-input"
|
||||
v-model="res.options.hotInterval"
|
||||
:min="1"
|
||||
:max="60"
|
||||
size="default"
|
||||
controls-position="right"
|
||||
/>
|
||||
<span class="unit">秒</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div class="panel-card">
|
||||
<div class="section-title">文本位置</div>
|
||||
<div class="ctrl-row">
|
||||
<div class="ctrl-label">文本位置</div>
|
||||
<div class="ctrl-body">
|
||||
<div class="seg layout-seg">
|
||||
<div
|
||||
class="seg-item"
|
||||
:class="{ active: res.options.textAlign !== 'center' }"
|
||||
@click="res.options.textAlign = 'left'"
|
||||
>
|
||||
居左
|
||||
</div>
|
||||
<div
|
||||
class="seg-item"
|
||||
:class="{ active: res.options.textAlign === 'center' }"
|
||||
@click="res.options.textAlign = 'center'"
|
||||
>
|
||||
居中
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel-card">
|
||||
<div class="section-title">颜色设置</div>
|
||||
<gradient-field
|
||||
v-model:start="res.options.bgColor1"
|
||||
v-model:end="res.options.bgColor2"
|
||||
:default-start="defaults.bgColor1"
|
||||
:default-end="defaults.bgColor2"
|
||||
/>
|
||||
<color-field
|
||||
v-model="res.options.boxColor"
|
||||
label="框体颜色"
|
||||
:default-value="defaults.boxColor"
|
||||
/>
|
||||
<color-field
|
||||
v-model="res.options.textColor"
|
||||
label="输入文字颜色"
|
||||
:default-value="defaults.textColor"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="panel-card">
|
||||
<div class="section-title">边距设置</div>
|
||||
<slider-field v-model="res.options.paddingX" label="左右边距" :min="0" :max="20" />
|
||||
</div>
|
||||
|
||||
<div class="panel-card panel-card-fill">
|
||||
<div class="section-title">圆角设置</div>
|
||||
<slider-field v-model="res.options.borderRadius" label="背景圆角" :min="0" :max="30" />
|
||||
<slider-field v-model="res.options.boxRadius" label="内容圆角" :min="0" :max="30" />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ColorField from "../controls/ColorField.vue";
|
||||
import GradientField from "../controls/GradientField.vue";
|
||||
import SliderField from "../controls/SliderField.vue";
|
||||
import { CircleClose } from "@element-plus/icons-vue";
|
||||
|
||||
const DEFAULTS = {
|
||||
textAlign: "left",
|
||||
bgColor1: "#FFFFFF",
|
||||
bgColor2: "#FFFFFF",
|
||||
boxColor: "#EDEDED",
|
||||
textColor: "#999999",
|
||||
paddingX: 12,
|
||||
borderRadius: 0,
|
||||
boxRadius: 15,
|
||||
hotWords: [],
|
||||
hotInterval: 3,
|
||||
};
|
||||
|
||||
export default {
|
||||
name: "PanelSearch",
|
||||
hideDecorateTitle: true,
|
||||
components: { ColorField, GradientField, SliderField, CircleClose },
|
||||
props: {
|
||||
res: { type: Object, required: true },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
defaults: DEFAULTS,
|
||||
activeTab: "content",
|
||||
tabs: [
|
||||
{ key: "content", title: "内容" },
|
||||
{ key: "style", title: "样式" },
|
||||
],
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.res.options = Object.assign({}, DEFAULTS, this.res.options);
|
||||
if (!Array.isArray(this.res.options.list) || !this.res.options.list.length) {
|
||||
this.res.options.list = [{ title: "搜索" }];
|
||||
}
|
||||
if (!Array.isArray(this.res.options.hotWords)) {
|
||||
this.res.options.hotWords = [];
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
addWord() {
|
||||
this.res.options.hotWords.push("");
|
||||
},
|
||||
removeWord(index) {
|
||||
this.res.options.hotWords.splice(index, 1);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "../controls/field-common.scss";
|
||||
@import "../controls/panel-common.scss";
|
||||
.panel-search {
|
||||
background: #f5f7fa;
|
||||
margin: 0 -20px;
|
||||
padding: 0 12px 12px;
|
||||
min-height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.panel-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 50px;
|
||||
margin-bottom: 12px;
|
||||
padding: 0 14px;
|
||||
background: #ffffff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04);
|
||||
box-sizing: border-box;
|
||||
.panel-name {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #303133;
|
||||
}
|
||||
}
|
||||
.panel-tabs {
|
||||
display: inline-flex;
|
||||
background: #f4f4f5;
|
||||
border-radius: 8px;
|
||||
padding: 3px;
|
||||
.panel-tab {
|
||||
padding: 5px 14px;
|
||||
font-size: 13px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
color: #606266;
|
||||
transition: all 0.2s ease;
|
||||
&.active {
|
||||
background: #ffffff;
|
||||
color: #409eff;
|
||||
font-weight: 600;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
}
|
||||
}
|
||||
.panel-card {
|
||||
background: #ffffff;
|
||||
border-radius: 8px;
|
||||
padding: 14px;
|
||||
margin-bottom: 12px;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
.section-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
margin: 0 0 12px;
|
||||
}
|
||||
.field-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.field-label {
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
margin-right: 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
.hot-word {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 10px;
|
||||
.del {
|
||||
cursor: pointer;
|
||||
color: #909399;
|
||||
font-size: 18px;
|
||||
flex-shrink: 0;
|
||||
&:hover {
|
||||
color: #e1251b;
|
||||
}
|
||||
}
|
||||
}
|
||||
.add-btn {
|
||||
width: 100%;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
.interval-row {
|
||||
.interval-input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
:deep(.el-input__inner) {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
.unit {
|
||||
margin-left: 8px;
|
||||
font-size: 13px;
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
297
seller/src/views/shop/wap/config-panel/PanelTitle.vue
Normal file
@@ -0,0 +1,297 @@
|
||||
<template>
|
||||
<div class="panel-title">
|
||||
<div class="panel-header">
|
||||
<span class="panel-name">{{ res.name }}</span>
|
||||
<div class="panel-tabs">
|
||||
<div
|
||||
v-for="tab in tabs"
|
||||
:key="tab.key"
|
||||
class="panel-tab"
|
||||
:class="{ active: activeTab === tab.key }"
|
||||
@click="activeTab = tab.key"
|
||||
>
|
||||
{{ tab.title }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="activeTab === 'content'">
|
||||
<div class="panel-card">
|
||||
<div class="section-title">文本标题</div>
|
||||
<div class="ctrl-row">
|
||||
<div class="ctrl-label">标题名称</div>
|
||||
<div class="ctrl-body">
|
||||
<el-input v-model="item.title" maxlength="20" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="ctrl-row">
|
||||
<div class="ctrl-label">副标题</div>
|
||||
<div class="ctrl-body">
|
||||
<el-input v-model="item.title1" maxlength="20" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="ctrl-row">
|
||||
<div class="ctrl-label">右侧按钮</div>
|
||||
<div class="ctrl-body">
|
||||
<el-radio-group v-model="item.showMore">
|
||||
<el-radio :value="true">显示</el-radio>
|
||||
<el-radio :value="false">隐藏</el-radio>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="item.showMore">
|
||||
<div class="ctrl-row">
|
||||
<div class="ctrl-label">右侧文字</div>
|
||||
<div class="ctrl-body">
|
||||
<el-input v-model="item.moreText" maxlength="6" placeholder="更多" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="ctrl-row">
|
||||
<div class="ctrl-label">链接</div>
|
||||
<div class="ctrl-body">
|
||||
<span class="link-text" v-if="item.moreUrl && item.moreUrl.___type">
|
||||
{{ linkText(item.moreUrl) }}
|
||||
</span>
|
||||
<el-button plain type="primary" @click="pickLink">选择链接</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="panel-card panel-card-fill">
|
||||
<div class="section-title">选择风格</div>
|
||||
<div class="ctrl-row">
|
||||
<div class="ctrl-label">风格</div>
|
||||
<div class="ctrl-body">
|
||||
<el-radio-group v-model="item.bgType">
|
||||
<el-radio value="image">背景图片</el-radio>
|
||||
<el-radio value="color">背景色</el-radio>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ctrl-row" v-if="item.bgType === 'color'">
|
||||
<div class="ctrl-label">背景色</div>
|
||||
<div class="ctrl-body">
|
||||
<el-color-picker v-model="item.bk_color" />
|
||||
<el-input v-model="item.bk_color" class="ctrl-color-input" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="ctrl-row" v-else>
|
||||
<div class="ctrl-label">上传背景图</div>
|
||||
<div class="ctrl-body">
|
||||
<div class="bg-preview" v-if="item.bgImage">
|
||||
<img :src="item.bgImage" alt="" />
|
||||
</div>
|
||||
<el-button plain type="primary" @click="pickImage">上传背景图</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section-tip" v-if="item.bgType !== 'color'">建议 910px*86px</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div class="panel-card">
|
||||
<div class="section-title">文字设置</div>
|
||||
<color-field v-model="item.color" label="标题颜色" :default-value="defaults.color" />
|
||||
<slider-field v-model="item.fontSize" label="标题文字" :min="12" :max="40" />
|
||||
<div class="ctrl-row">
|
||||
<div class="ctrl-label">文本样式</div>
|
||||
<div class="ctrl-body">
|
||||
<div class="seg layout-seg-3">
|
||||
<div
|
||||
v-for="opt in textStyleOptions"
|
||||
:key="opt.value"
|
||||
class="seg-item"
|
||||
:class="{ active: (item.textStyle || 'normal') === opt.value }"
|
||||
@click="item.textStyle = opt.value"
|
||||
>
|
||||
{{ opt.title }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<color-field v-model="item.color1" label="副标题颜色" :default-value="defaults.color1" />
|
||||
<slider-field v-model="item.subFontSize" label="副标题文字" :min="10" :max="30" />
|
||||
<template v-if="item.showMore">
|
||||
<color-field v-model="item.moreColor" label="按钮颜色" :default-value="defaults.moreColor" />
|
||||
<slider-field v-model="item.moreFontSize" label="按钮文字" :min="10" :max="24" />
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="panel-card">
|
||||
<div class="section-title">边距设置</div>
|
||||
<slider-field v-model="res.options.paddingTop" label="上边距" :min="0" :max="100" />
|
||||
<slider-field v-model="res.options.paddingBottom" label="下边距" :min="0" :max="100" />
|
||||
<slider-field v-model="res.options.paddingX" label="左右边距" :min="0" :max="40" />
|
||||
<slider-field v-model="res.options.pageGap" label="页面间距" :min="0" :max="100" />
|
||||
</div>
|
||||
|
||||
<div class="panel-card panel-card-fill">
|
||||
<div class="section-title">圆角设置</div>
|
||||
<slider-field v-model="res.options.radiusTop" label="上圆角" :min="0" :max="30" />
|
||||
<slider-field v-model="res.options.radiusBottom" label="下圆角" :min="0" :max="30" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<liliDialog ref="liliDialog" @selectedLink="selectedLink"></liliDialog>
|
||||
|
||||
<el-dialog v-model="picModelFlag" width="1200px" append-to-body destroy-on-close>
|
||||
<ossManage
|
||||
@callback="callbackSelected"
|
||||
:is-component="true"
|
||||
:initialize="picModelFlag"
|
||||
:max-select="1"
|
||||
ref="ossManage"
|
||||
/>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ColorField from "../controls/ColorField.vue";
|
||||
import SliderField from "../controls/SliderField.vue";
|
||||
import ossManage from "@/views/sys/oss-manage/ossManage";
|
||||
import ways from "@/views/lili-dialog/wap.js";
|
||||
|
||||
const DEFAULTS = {
|
||||
paddingTop: 10,
|
||||
paddingBottom: 10,
|
||||
paddingX: 12,
|
||||
pageGap: 0,
|
||||
radiusTop: 0,
|
||||
radiusBottom: 0,
|
||||
color: "#282828",
|
||||
color1: "#282828",
|
||||
moreColor: "#282828",
|
||||
};
|
||||
|
||||
export default {
|
||||
name: "PanelTitle",
|
||||
hideDecorateTitle: true,
|
||||
components: { ColorField, SliderField, ossManage },
|
||||
props: {
|
||||
res: { type: Object, required: true },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
defaults: DEFAULTS,
|
||||
activeTab: "content",
|
||||
tabs: [
|
||||
{ key: "content", title: "内容" },
|
||||
{ key: "style", title: "样式" },
|
||||
],
|
||||
textStyleOptions: [
|
||||
{ value: "normal", title: "正常" },
|
||||
{ value: "italic", title: "斜体" },
|
||||
{ value: "bold", title: "加粗" },
|
||||
],
|
||||
picModelFlag: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
item() {
|
||||
return this.res.options.list[0];
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.res.options = Object.assign({}, DEFAULTS, this.res.options);
|
||||
if (!Array.isArray(this.res.options.list) || !this.res.options.list.length) {
|
||||
this.res.options.list = [
|
||||
{
|
||||
title: "标题",
|
||||
title1: "副标题",
|
||||
color: "#282828",
|
||||
color1: "#282828",
|
||||
bk_color: "#FFFFFF",
|
||||
textAlign: "left",
|
||||
},
|
||||
];
|
||||
}
|
||||
const item = this.item;
|
||||
if (item.showMore === undefined) item.showMore = true;
|
||||
if (!item.moreText) item.moreText = "更多";
|
||||
if (!item.bgType) item.bgType = "color";
|
||||
if (!item.fontSize) item.fontSize = 20;
|
||||
if (!item.subFontSize) item.subFontSize = 14;
|
||||
if (!item.moreFontSize) item.moreFontSize = 12;
|
||||
if (!item.moreColor) item.moreColor = "#282828";
|
||||
},
|
||||
methods: {
|
||||
linkText(url) {
|
||||
const way = ways.find((e) => e.name === url.___type);
|
||||
const typeTitle = way ? way.title : "发现";
|
||||
const name =
|
||||
url.name || url.goodsName || url.memberName || url.title || "";
|
||||
return name ? `${typeTitle} - ${name}` : typeTitle;
|
||||
},
|
||||
pickImage() {
|
||||
this.picModelFlag = true;
|
||||
this.$nextTick(() => {
|
||||
const oss = this.$refs.ossManage;
|
||||
if (!oss) return;
|
||||
oss.selectImage = true;
|
||||
oss.fileType = "pic";
|
||||
oss.changeFileType();
|
||||
});
|
||||
},
|
||||
callbackSelected(val) {
|
||||
this.picModelFlag = false;
|
||||
if (val && val.url) {
|
||||
this.item.bgImage = val.url;
|
||||
}
|
||||
},
|
||||
pickLink() {
|
||||
const dialog = this.$refs.liliDialog;
|
||||
dialog.clearGoodsSelected();
|
||||
dialog.goodsFlag = false;
|
||||
dialog.flag = true;
|
||||
},
|
||||
selectedLink(val) {
|
||||
delete val.selected;
|
||||
delete val.intro;
|
||||
delete val.mobileIntro;
|
||||
this.item.moreUrl = val;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "../controls/field-common.scss";
|
||||
@import "../controls/panel-common.scss";
|
||||
|
||||
.panel-title {
|
||||
background: #f5f7fa;
|
||||
margin: 0 -20px;
|
||||
padding: 0 12px 12px;
|
||||
min-height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.link-text {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.bg-preview {
|
||||
width: 132px;
|
||||
height: 30px;
|
||||
border: 1px dashed #dcdfe6;
|
||||
border-radius: 6px;
|
||||
padding: 2px;
|
||||
box-sizing: border-box;
|
||||
margin-right: 10px;
|
||||
flex-shrink: 0;
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
object-fit: cover;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
300
seller/src/views/shop/wap/config-panel/PanelVideo.vue
Normal file
@@ -0,0 +1,300 @@
|
||||
<template>
|
||||
<div class="panel-video">
|
||||
<div class="panel-header">
|
||||
<span class="panel-name">{{ res.name }}</span>
|
||||
<div class="panel-tabs">
|
||||
<div
|
||||
v-for="tab in tabs"
|
||||
:key="tab.key"
|
||||
class="panel-tab"
|
||||
:class="{ active: activeTab === tab.key }"
|
||||
@click="activeTab = tab.key"
|
||||
>
|
||||
{{ tab.title }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="activeTab === 'content'">
|
||||
<div class="panel-card panel-card-fill">
|
||||
<div class="section-title">视频内容设置</div>
|
||||
<div class="ctrl-row">
|
||||
<div class="ctrl-label">上传视频</div>
|
||||
<div class="ctrl-body">
|
||||
<div class="upload-state" :class="{ filled: item.videoUrl }">
|
||||
{{ item.videoUrl ? "已选择视频" : "未选择视频" }}
|
||||
</div>
|
||||
<el-button plain type="primary" @click="pickFile('videoUrl')">
|
||||
{{ item.videoUrl ? "重新选择" : "上传视频" }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ctrl-row">
|
||||
<div class="ctrl-label">视频封面</div>
|
||||
<div class="ctrl-body">
|
||||
<div class="cover-preview" v-if="item.poster">
|
||||
<img :src="item.poster" alt="" />
|
||||
</div>
|
||||
<div class="cover-placeholder" v-else>封面</div>
|
||||
<el-button plain type="primary" @click="pickFile('poster')">
|
||||
{{ item.poster ? "重新选择" : "上传封面" }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ctrl-row">
|
||||
<div class="ctrl-label">自动播放</div>
|
||||
<div class="ctrl-body">
|
||||
<el-switch v-model="item.autoplay" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="ctrl-row">
|
||||
<div class="ctrl-label">画面比例</div>
|
||||
<div class="ctrl-body">
|
||||
<div class="seg layout-seg-3">
|
||||
<div
|
||||
v-for="opt in aspectOptions"
|
||||
:key="opt"
|
||||
class="seg-item"
|
||||
:class="{ active: (item.aspectRatio || '16:9') === opt }"
|
||||
@click="item.aspectRatio = opt"
|
||||
>
|
||||
{{ opt }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div class="panel-card">
|
||||
<div class="section-title">颜色设置</div>
|
||||
<gradient-field
|
||||
v-model:start="res.options.bgColor1"
|
||||
v-model:end="res.options.bgColor2"
|
||||
:default-start="defaults.bgColor1"
|
||||
:default-end="defaults.bgColor2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="panel-card">
|
||||
<div class="section-title">边距设置</div>
|
||||
<slider-field v-model="res.options.paddingTop" label="上边距" :min="0" :max="100" />
|
||||
<slider-field v-model="res.options.paddingBottom" label="下边距" :min="0" :max="100" />
|
||||
<slider-field v-model="res.options.paddingX" label="左右边距" :min="0" :max="40" />
|
||||
<slider-field v-model="res.options.pageGap" label="页面间距" :min="0" :max="100" />
|
||||
</div>
|
||||
|
||||
<div class="panel-card panel-card-fill">
|
||||
<div class="section-title">圆角设置</div>
|
||||
<slider-field v-model="res.options.borderRadius" label="背景圆角" :min="0" :max="30" />
|
||||
<slider-field v-model="res.options.imgRadius" label="内容圆角" :min="0" :max="30" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-dialog v-model="picModelFlag" width="1200px" append-to-body destroy-on-close>
|
||||
<ossManage
|
||||
@callback="callbackSelected"
|
||||
:is-component="true"
|
||||
:initialize="picModelFlag"
|
||||
:max-select="1"
|
||||
ref="ossManage"
|
||||
/>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import GradientField from "../controls/GradientField.vue";
|
||||
import SliderField from "../controls/SliderField.vue";
|
||||
import ossManage from "@/views/sys/oss-manage/ossManage";
|
||||
|
||||
const DEFAULTS = {
|
||||
bgColor1: "#FFFFFF",
|
||||
bgColor2: "#FFFFFF",
|
||||
paddingTop: 10,
|
||||
paddingBottom: 10,
|
||||
paddingX: 12,
|
||||
pageGap: 0,
|
||||
borderRadius: 0,
|
||||
imgRadius: 0,
|
||||
};
|
||||
|
||||
export default {
|
||||
name: "PanelVideo",
|
||||
hideDecorateTitle: true,
|
||||
components: { GradientField, SliderField, ossManage },
|
||||
props: {
|
||||
res: { type: Object, required: true },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
defaults: DEFAULTS,
|
||||
activeTab: "content",
|
||||
tabs: [
|
||||
{ key: "content", title: "内容" },
|
||||
{ key: "style", title: "样式" },
|
||||
],
|
||||
aspectOptions: ["16:9", "4:3", "1:1"],
|
||||
picModelFlag: false,
|
||||
currentField: "videoUrl",
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
item() {
|
||||
return this.res.options.list[0];
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.res.options = Object.assign({}, DEFAULTS, this.res.options);
|
||||
if (!Array.isArray(this.res.options.list) || !this.res.options.list.length) {
|
||||
this.res.options.list = [
|
||||
{ videoUrl: "", poster: "", autoplay: false, aspectRatio: "16:9" },
|
||||
];
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
pickFile(field) {
|
||||
this.currentField = field;
|
||||
this.picModelFlag = true;
|
||||
this.$nextTick(() => {
|
||||
const oss = this.$refs.ossManage;
|
||||
if (!oss) return;
|
||||
oss.selectImage = true;
|
||||
oss.fileType = field === "videoUrl" ? "video" : "pic";
|
||||
oss.changeFileType();
|
||||
});
|
||||
},
|
||||
callbackSelected(val) {
|
||||
this.picModelFlag = false;
|
||||
if (val && val.url) {
|
||||
this.item[this.currentField] = val.url;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "../controls/field-common.scss";
|
||||
@import "../controls/panel-common.scss";
|
||||
.panel-video {
|
||||
background: #f5f7fa;
|
||||
margin: 0 -20px;
|
||||
padding: 0 12px 12px;
|
||||
min-height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.panel-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 50px;
|
||||
margin-bottom: 12px;
|
||||
padding: 0 14px;
|
||||
background: #ffffff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04);
|
||||
box-sizing: border-box;
|
||||
.panel-name {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #303133;
|
||||
}
|
||||
}
|
||||
.panel-tabs {
|
||||
display: inline-flex;
|
||||
background: #f4f4f5;
|
||||
border-radius: 8px;
|
||||
padding: 3px;
|
||||
.panel-tab {
|
||||
padding: 5px 14px;
|
||||
font-size: 13px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
color: #606266;
|
||||
transition: all 0.2s ease;
|
||||
&.active {
|
||||
background: #ffffff;
|
||||
color: #409eff;
|
||||
font-weight: 600;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
}
|
||||
}
|
||||
.panel-card {
|
||||
background: #ffffff;
|
||||
border-radius: 8px;
|
||||
padding: 14px;
|
||||
margin-bottom: 12px;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
.section-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
margin: 0 0 12px;
|
||||
}
|
||||
.upload-state {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
&.filled {
|
||||
color: #409eff;
|
||||
}
|
||||
}
|
||||
.cover-preview {
|
||||
width: 88px;
|
||||
height: 44px;
|
||||
border: 1px dashed #dcdfe6;
|
||||
border-radius: 6px;
|
||||
padding: 3px;
|
||||
box-sizing: border-box;
|
||||
flex-shrink: 0;
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
object-fit: cover;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
.cover-placeholder {
|
||||
width: 88px;
|
||||
height: 44px;
|
||||
border: 1px dashed #dcdfe6;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.seg {
|
||||
display: inline-flex;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
.seg-item {
|
||||
padding: 6px 14px;
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
cursor: pointer;
|
||||
border-left: 1px solid #dcdfe6;
|
||||
transition: all 0.2s ease;
|
||||
&:first-child {
|
||||
border-left: none;
|
||||
}
|
||||
&.active {
|
||||
background: #ecf5ff;
|
||||
color: #409eff;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
43
seller/src/views/shop/wap/config-panel/index.js
Normal file
@@ -0,0 +1,43 @@
|
||||
// 店铺端组件配置面板映射:type → 面板组件
|
||||
// 面板契约:props res(整个楼层条目),直接修改 res.options.*,保存链路自动感知
|
||||
import PanelBlank from "./PanelBlank.vue";
|
||||
import PanelLine from "./PanelLine.vue";
|
||||
import PanelRichText from "./PanelRichText.vue";
|
||||
import PanelCoupon from "./PanelCoupon.vue";
|
||||
import PanelFlexOne from "./PanelFlexOne.vue";
|
||||
import PanelCarousel from "./PanelCarousel.vue";
|
||||
import PanelAdCube from "./PanelAdCube.vue";
|
||||
import PanelSearch from "./PanelSearch.vue";
|
||||
import PanelNotice from "./PanelNotice.vue";
|
||||
import PanelVideo from "./PanelVideo.vue";
|
||||
import PanelTitle from "./PanelTitle.vue";
|
||||
import PanelGoods from "./PanelGoods.vue";
|
||||
|
||||
// 图片魔方及其全部可选风格共用 PanelAdCube
|
||||
const adCubeTypes = [
|
||||
"tpl_ad_list",
|
||||
"leftOneRightTwo",
|
||||
"leftTwoRightOne",
|
||||
"flexThree",
|
||||
"flexTwo",
|
||||
"topOneBottomTwo",
|
||||
"topTwoBottomOne",
|
||||
"flexFive",
|
||||
"flexFour",
|
||||
"textPicture",
|
||||
];
|
||||
|
||||
export default {
|
||||
blank: PanelBlank,
|
||||
line: PanelLine,
|
||||
richText: PanelRichText,
|
||||
coupon: PanelCoupon,
|
||||
flexOne: PanelFlexOne,
|
||||
carousel: PanelCarousel,
|
||||
search: PanelSearch,
|
||||
notice: PanelNotice,
|
||||
video: PanelVideo,
|
||||
title: PanelTitle,
|
||||
goods: PanelGoods,
|
||||
...Object.fromEntries(adCubeTypes.map((t) => [t, PanelAdCube])),
|
||||
};
|
||||
42
seller/src/views/shop/wap/controls/ColorField.vue
Normal file
@@ -0,0 +1,42 @@
|
||||
<template>
|
||||
<div class="ctrl-row">
|
||||
<div class="ctrl-label">{{ label }}</div>
|
||||
<div class="ctrl-body">
|
||||
<el-color-picker
|
||||
:model-value="modelValue"
|
||||
@update:model-value="onInput"
|
||||
/>
|
||||
<el-input
|
||||
:model-value="modelValue"
|
||||
class="ctrl-color-input"
|
||||
placeholder="#FFFFFF"
|
||||
@update:model-value="onInput"
|
||||
/>
|
||||
<el-button link type="primary" @click="onReset">重置</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "ColorField",
|
||||
props: {
|
||||
modelValue: { type: String, default: "" },
|
||||
defaultValue: { type: String, default: "" },
|
||||
label: { type: String, default: "" },
|
||||
},
|
||||
emits: ["update:modelValue"],
|
||||
methods: {
|
||||
onInput(val) {
|
||||
this.$emit("update:modelValue", val);
|
||||
},
|
||||
onReset() {
|
||||
this.$emit("update:modelValue", this.defaultValue);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "./field-common.scss";
|
||||
</style>
|
||||
75
seller/src/views/shop/wap/controls/GradientField.vue
Normal file
@@ -0,0 +1,75 @@
|
||||
<template>
|
||||
<div class="ctrl-row">
|
||||
<div class="ctrl-label">{{ label }}</div>
|
||||
<div class="ctrl-body gradient-body">
|
||||
<div class="gradient-row">
|
||||
<el-color-picker
|
||||
:model-value="start"
|
||||
@update:model-value="$emit('update:start', $event)"
|
||||
/>
|
||||
<el-input
|
||||
:model-value="start"
|
||||
class="ctrl-color-input"
|
||||
placeholder="#FFFFFF"
|
||||
@update:model-value="$emit('update:start', $event)"
|
||||
/>
|
||||
<el-button link type="primary" @click="$emit('update:start', defaultStart)">重置</el-button>
|
||||
</div>
|
||||
<div class="gradient-row">
|
||||
<el-color-picker
|
||||
:model-value="end"
|
||||
@update:model-value="$emit('update:end', $event)"
|
||||
/>
|
||||
<el-input
|
||||
:model-value="end"
|
||||
class="ctrl-color-input"
|
||||
placeholder="#FFFFFF"
|
||||
@update:model-value="$emit('update:end', $event)"
|
||||
/>
|
||||
<el-button link type="primary" @click="$emit('update:end', defaultEnd)">重置</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "GradientField",
|
||||
props: {
|
||||
start: { type: String, default: "" },
|
||||
end: { type: String, default: "" },
|
||||
defaultStart: { type: String, default: "" },
|
||||
defaultEnd: { type: String, default: "" },
|
||||
label: { type: String, default: "背景颜色" },
|
||||
},
|
||||
emits: ["update:start", "update:end"],
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "./field-common.scss";
|
||||
|
||||
.gradient-body {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.gradient-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
position: relative;
|
||||
|
||||
// 两个色板之间的连接线(对齐色板中心)
|
||||
& + .gradient-row::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 15px;
|
||||
top: -14px;
|
||||
width: 2px;
|
||||
height: 10px;
|
||||
background: #dcdfe6;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
42
seller/src/views/shop/wap/controls/SegmentField.vue
Normal file
@@ -0,0 +1,42 @@
|
||||
<template>
|
||||
<div class="ctrl-row">
|
||||
<div class="ctrl-label">{{ label }}</div>
|
||||
<div class="ctrl-body">
|
||||
<el-radio-group
|
||||
:model-value="modelValue"
|
||||
@update:model-value="onInput"
|
||||
>
|
||||
<el-radio-button
|
||||
v-for="opt in options"
|
||||
:key="opt.value"
|
||||
:value="opt.value"
|
||||
>{{ opt.label }}</el-radio-button
|
||||
>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "SegmentField",
|
||||
props: {
|
||||
modelValue: { type: String, default: "" },
|
||||
options: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
label: { type: String, default: "" },
|
||||
},
|
||||
emits: ["update:modelValue"],
|
||||
methods: {
|
||||
onInput(val) {
|
||||
this.$emit("update:modelValue", val);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "./field-common.scss";
|
||||
</style>
|
||||
57
seller/src/views/shop/wap/controls/SliderField.vue
Normal file
@@ -0,0 +1,57 @@
|
||||
<template>
|
||||
<div class="ctrl-row">
|
||||
<div class="ctrl-label">{{ label }}</div>
|
||||
<div class="ctrl-body">
|
||||
<el-slider
|
||||
class="ctrl-slider"
|
||||
:model-value="modelValue"
|
||||
:min="min"
|
||||
:max="max"
|
||||
@update:model-value="onInput"
|
||||
/>
|
||||
<el-input-number
|
||||
class="ctrl-number"
|
||||
:model-value="modelValue"
|
||||
:min="min"
|
||||
:max="max"
|
||||
@update:model-value="onInput"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "SliderField",
|
||||
props: {
|
||||
modelValue: { type: Number, default: 0 },
|
||||
min: { type: Number, default: 0 },
|
||||
max: { type: Number, default: 100 },
|
||||
label: { type: String, default: "" },
|
||||
},
|
||||
emits: ["update:modelValue"],
|
||||
methods: {
|
||||
onInput(val) {
|
||||
this.$emit("update:modelValue", val);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "./field-common.scss";
|
||||
|
||||
.ctrl-body {
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.ctrl-slider {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.ctrl-number {
|
||||
width: 130px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
||||
28
seller/src/views/shop/wap/controls/field-common.scss
Normal file
@@ -0,0 +1,28 @@
|
||||
.ctrl-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 16px;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.ctrl-label {
|
||||
width: 90px;
|
||||
flex-shrink: 0;
|
||||
line-height: 32px;
|
||||
color: #606266;
|
||||
font-size: 14px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.ctrl-body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.ctrl-color-input {
|
||||
width: 110px;
|
||||
}
|
||||
179
seller/src/views/shop/wap/controls/panel-common.scss
Normal file
@@ -0,0 +1,179 @@
|
||||
// 新配置面板共用的骨架样式(灰底 + 白色卡片 + 头部 tabs)
|
||||
// 使用方式:面板根类名需与 .panel-root 一起用,或在面板 scoped 样式里 @import 后把根类加上
|
||||
.panel-search,
|
||||
.panel-promotion,
|
||||
.panel-coupon,
|
||||
.panel-title,
|
||||
.panel-shop,
|
||||
.panel-goods,
|
||||
.panel-notice,
|
||||
.panel-video,
|
||||
.panel-article,
|
||||
.panel-page-setting {
|
||||
background: #f5f7fa;
|
||||
margin: 0 -20px;
|
||||
padding: 0 12px 12px;
|
||||
min-height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.panel-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 50px;
|
||||
margin-bottom: 12px;
|
||||
padding: 0 14px;
|
||||
background: #ffffff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04);
|
||||
box-sizing: border-box;
|
||||
.panel-name {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #303133;
|
||||
}
|
||||
}
|
||||
.panel-tabs {
|
||||
display: inline-flex;
|
||||
background: #f4f4f5;
|
||||
border-radius: 8px;
|
||||
padding: 3px;
|
||||
.panel-tab {
|
||||
padding: 5px 14px;
|
||||
font-size: 13px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
color: #606266;
|
||||
transition: all 0.2s ease;
|
||||
&.active {
|
||||
background: #ffffff;
|
||||
color: #409eff;
|
||||
font-weight: 600;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
}
|
||||
}
|
||||
.panel-card {
|
||||
background: #ffffff;
|
||||
border-radius: 8px;
|
||||
padding: 14px;
|
||||
margin-bottom: 12px;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
.panel-card-fill {
|
||||
flex: 1;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
// 面板根容器改为纵向 flex,让最后一张卡片可撑满到底
|
||||
.panel-flex-one,
|
||||
.panel-carousel,
|
||||
.panel-ad-cube,
|
||||
.panel-menu,
|
||||
.panel-search,
|
||||
.panel-notice,
|
||||
.panel-video,
|
||||
.panel-title,
|
||||
.panel-integral,
|
||||
.panel-coupon,
|
||||
.panel-article,
|
||||
.panel-shop,
|
||||
.panel-goods,
|
||||
.panel-blank,
|
||||
.panel-line,
|
||||
.panel-rich-text,
|
||||
.panel-promotion,
|
||||
.panel-page-setting {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.section-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
margin: 0 0 12px;
|
||||
}
|
||||
.section-tip {
|
||||
font-size: 13px;
|
||||
color: #909399;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.setting-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin: 14px 0;
|
||||
.setting-label {
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
.setting-value {
|
||||
margin-left: 8px;
|
||||
color: #303133;
|
||||
}
|
||||
}
|
||||
}
|
||||
.seg {
|
||||
display: inline-flex;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
.seg-item {
|
||||
padding: 6px 14px;
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
cursor: pointer;
|
||||
border-left: 1px solid #dcdfe6;
|
||||
transition: all 0.2s ease;
|
||||
&:first-child {
|
||||
border-left: none;
|
||||
}
|
||||
&.active {
|
||||
background: #ecf5ff;
|
||||
color: #409eff;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 选项平铺选择器:一行两个,超出换行
|
||||
.layout-seg {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
width: 100%;
|
||||
.seg-item {
|
||||
flex: 1 0 40%;
|
||||
padding: 6px 0;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
box-sizing: border-box;
|
||||
&:nth-child(3) {
|
||||
border-left: none;
|
||||
}
|
||||
&:nth-child(n + 3) {
|
||||
border-top: 1px solid #dcdfe6;
|
||||
}
|
||||
}
|
||||
&.layout-seg-3 {
|
||||
flex-wrap: nowrap;
|
||||
.seg-item {
|
||||
flex: 1 1 0;
|
||||
&:nth-child(3) {
|
||||
border-left: 1px solid #dcdfe6;
|
||||
}
|
||||
&:nth-child(n + 3) {
|
||||
border-top: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 选项平铺选择器(独立用法):一行三个
|
||||
.layout-seg-3 {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
width: 100%;
|
||||
.seg-item {
|
||||
flex: 1 1 0;
|
||||
padding: 6px 0;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
|
||||
:deep(.el-overlay) {
|
||||
z-index: 2000;
|
||||
}
|
||||
.decorate-view-link{
|
||||
font-size: 12px;
|
||||
margin: 0 4px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
color: #999;
|
||||
|
||||
.decorate-view-link {
|
||||
font-size: 12px;
|
||||
margin: 0 4px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
color: #999;
|
||||
}
|
||||
.decorate-view-btn {
|
||||
flex-shrink: 0;
|
||||
@@ -22,111 +22,179 @@
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.decorate-view-style {
|
||||
border: 1px solid #ededed;
|
||||
background: #f7f7fa;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
padding: 30px 0 0 0;
|
||||
margin-bottom: 20px;
|
||||
cursor: pointer;
|
||||
> .select-style {
|
||||
background: #ededed;
|
||||
padding: 15px 0;
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
.decorate-border {
|
||||
border: 1px solid #ededed;
|
||||
margin: 10px 0;
|
||||
padding: 0 10px;
|
||||
}
|
||||
.drawer {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 14px;
|
||||
> .drawer-item {
|
||||
cursor: pointer;
|
||||
border: 1px solid #ededed;
|
||||
background: #f9f0ff;
|
||||
width: calc(50% - 7px);
|
||||
box-sizing: border-box;
|
||||
color: #9254de;
|
||||
border-radius: 0.8em;
|
||||
height: 60px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
.hidden-input {
|
||||
display: none;
|
||||
}
|
||||
.decorate {
|
||||
padding: 0 20px;
|
||||
height: calc(100vh - 120px);
|
||||
overflow-y: auto;
|
||||
padding-bottom: 120px;
|
||||
}
|
||||
.decorate-title {
|
||||
height: 50px;
|
||||
line-height: 50px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.decorate-list {
|
||||
overflow: hidden;
|
||||
}
|
||||
.decorate-view {
|
||||
display: flex;
|
||||
margin: 20px 0;
|
||||
align-items: center;
|
||||
}
|
||||
.decorate-item-box {
|
||||
background: #fff;
|
||||
padding: 10px;
|
||||
border: 1px solid #ededed;
|
||||
}
|
||||
.decorate-item-title {
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
line-height: 40px;
|
||||
padding: 0 10px;
|
||||
}
|
||||
.decorate-view-title {
|
||||
margin-right: 10px;
|
||||
}
|
||||
.decorate-item {
|
||||
border: 1px solid #ededed;
|
||||
background: #f7f7fa;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
padding: 30px 0 0 0;
|
||||
margin-bottom: 20px;
|
||||
cursor: pointer;
|
||||
|
||||
>.select-style {
|
||||
background: #ededed;
|
||||
border-top-left-radius: 0.4em;
|
||||
margin-bottom: 20px;
|
||||
border-top-right-radius: 0.4em;
|
||||
padding: 15px 0;
|
||||
font-size: 15px;
|
||||
}
|
||||
.show-image {
|
||||
max-width: 50px;
|
||||
}
|
||||
.tips {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
> span {
|
||||
color: $theme_color;
|
||||
}
|
||||
}
|
||||
.selectBtn {
|
||||
margin-left: 10px;
|
||||
}
|
||||
.bing-goods-list {
|
||||
}
|
||||
|
||||
.decorate-border {
|
||||
border: 1px solid #ededed;
|
||||
margin: 10px 0;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.drawer {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 14px;
|
||||
|
||||
>.drawer-item {
|
||||
cursor: pointer;
|
||||
border: 1px solid #ededed;
|
||||
background: #f9f0ff;
|
||||
width: calc(50% - 7px);
|
||||
box-sizing: border-box;
|
||||
color: #9254de;
|
||||
border-radius: 0.8em;
|
||||
height: 60px;
|
||||
display: flex;
|
||||
> .bing-goods-item {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
> img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.hidden-input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.decorate {
|
||||
padding: 0 20px;
|
||||
height: calc(100vh - 96px);
|
||||
overflow-y: auto;
|
||||
background: #f5f7fa;
|
||||
}
|
||||
|
||||
.decorate-title {
|
||||
height: 50px;
|
||||
line-height: 50px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.decorate-list {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.decorate-view {
|
||||
display: flex;
|
||||
margin: 12px 0;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.decorate-item-box {
|
||||
background: #fff;
|
||||
padding: 10px;
|
||||
border: 1px solid #ededed;
|
||||
}
|
||||
|
||||
.decorate-item-title {
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
line-height: 40px;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.decorate-view-title {
|
||||
margin-right: 10px;
|
||||
|
||||
}
|
||||
|
||||
.decorate-item {
|
||||
background: #ededed;
|
||||
border-top-left-radius: 0.4em;
|
||||
margin-bottom: 20px;
|
||||
border-top-right-radius: 0.4em;
|
||||
}
|
||||
|
||||
.show-image {
|
||||
max-width: 50px;
|
||||
}
|
||||
|
||||
.tips {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
|
||||
>span {
|
||||
color: $theme_color;
|
||||
}
|
||||
}
|
||||
|
||||
.selectBtn {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.bing-goods-list {
|
||||
display: flex;
|
||||
|
||||
>.bing-goods-item {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
|
||||
>img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.align-text {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.card-box {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.decorate-notice {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.card {}
|
||||
|
||||
.card:hover {
|
||||
background: #ededed;
|
||||
}
|
||||
|
||||
.active {
|
||||
background: #ededed;
|
||||
}
|
||||
.title-item{
|
||||
border-bottom: 1px solid #ededed;
|
||||
padding:10px 10px 10px 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
cursor: pointer;
|
||||
>.title-btn{
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
.title-goodsName{
|
||||
/* 适用于webkit内核和移动端 */
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
|
||||
}
|
||||
.title-tooltip{
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="decorate">
|
||||
<div class="decorate-title">
|
||||
<div class="decorate-title" v-if="!hidePanelTitle">
|
||||
{{ res.name }}
|
||||
|
||||
<el-button
|
||||
@@ -20,14 +20,24 @@
|
||||
style="margin-left: 20px"
|
||||
size="small"
|
||||
plain
|
||||
v-if="res.type == 'promotions' || res.drawerPromotions"
|
||||
v-if="res.drawerPromotions"
|
||||
type="primary"
|
||||
@click="selectPromotions()"
|
||||
>选择促销活动</el-button
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- 新组件独立配置面板(按 type 分发) -->
|
||||
<component
|
||||
v-if="panelComponent"
|
||||
:is="panelComponent"
|
||||
:res="res"
|
||||
@handleDrawer="emit('handleDrawer', $event)"
|
||||
/>
|
||||
<!-- 老组件遗留配置分支 -->
|
||||
<template v-else>
|
||||
<el-alert
|
||||
v-if="res.name == '商品分类'"
|
||||
v-if="res.name == '商品列表'"
|
||||
type="warning"
|
||||
title="装修提示"
|
||||
:closable="false"
|
||||
@@ -95,9 +105,7 @@
|
||||
<div
|
||||
class="decorate-list"
|
||||
v-if="
|
||||
(res.type != 'tpl_ad_list' &&
|
||||
res.type != 'tpl_activity_list' &&
|
||||
res.type != 'promotions') ||
|
||||
(res.type != 'tpl_ad_list' && res.type != 'tpl_activity_list') ||
|
||||
res.drawer ||
|
||||
res.drawerPromotions
|
||||
"
|
||||
@@ -107,7 +115,7 @@
|
||||
v-for="(item, index) in res.options.list"
|
||||
:key="index"
|
||||
>
|
||||
<div class="decorate-item-title">
|
||||
<div class="decorate-item-title" v-if="res.type != 'video'">
|
||||
<div>卡片</div>
|
||||
<el-icon
|
||||
v-if="res.close || res.type == 'promotionDetail'"
|
||||
@@ -247,7 +255,7 @@
|
||||
@click="changeTextAlign('left')"
|
||||
>
|
||||
<img
|
||||
:src="alignTextLeftImg"
|
||||
:src="alignTextLeft"
|
||||
class="align-text"
|
||||
alt=""
|
||||
/>
|
||||
@@ -258,7 +266,7 @@
|
||||
@click="changeTextAlign('center')"
|
||||
>
|
||||
<img
|
||||
:src="alignTextCenterImg"
|
||||
:src="alignTextCenter"
|
||||
class="align-text"
|
||||
alt=""
|
||||
/>
|
||||
@@ -269,7 +277,7 @@
|
||||
@click="changeTextAlign('right')"
|
||||
>
|
||||
<img
|
||||
:src="alignTextRightImg"
|
||||
:src="alignTextRight"
|
||||
class="align-text"
|
||||
alt=""
|
||||
/>
|
||||
@@ -373,21 +381,17 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 视频楼层配置 -->
|
||||
<video-config
|
||||
v-if="res.type == 'video'"
|
||||
:item="item"
|
||||
@select-file="(field) => handleClickFile(item, index, field)"
|
||||
/>
|
||||
|
||||
<!-- 填写链接 -->
|
||||
|
||||
<div class="decorate-view" v-if="res.onlyImg">
|
||||
<div class="decorate-view-title">选择模式</div>
|
||||
|
||||
<div>
|
||||
<el-radio-group v-model="item.model">
|
||||
<el-radio value="link">链接</el-radio>
|
||||
<el-radio value="hotzone">热区</el-radio>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="decorate-view" v-if="!res.notLink">
|
||||
<div class="decorate-view-title">选择链接</div>
|
||||
<div class="decorate-view-title">{{ item.model === "hotzone" ? "热区" : "选择链接" }}</div>
|
||||
<div
|
||||
v-if="item.url && item.url.length != 0"
|
||||
class="decorate-view-link"
|
||||
@@ -493,7 +497,7 @@
|
||||
>
|
||||
|
||||
<liliDialog
|
||||
ref="liliDialog"
|
||||
ref="liliDialogRef"
|
||||
@selectedLink="selectedLink"
|
||||
@selectedGoodsData="selectedGoodsData"
|
||||
></liliDialog>
|
||||
@@ -520,26 +524,36 @@
|
||||
<hotzone ref="hotzoneRef" @changeZone="changeZone"></hotzone>
|
||||
|
||||
<el-dialog v-model="picModelFlag" width="1200px" append-to-body destroy-on-close>
|
||||
<ossManage @callback="callbackSelected" :is-component="true" :initialize="picModelFlag" :max-select="1" ref="ossManageRef" />
|
||||
<ossManage
|
||||
@callback="callbackSelected"
|
||||
:is-component="true"
|
||||
:initialize="picModelFlag"
|
||||
:max-select="1"
|
||||
ref="ossManageRef"
|
||||
/>
|
||||
</el-dialog>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { ref, watch, nextTick } from "vue";
|
||||
import { ref, watch, nextTick, computed } from "vue";
|
||||
import { CircleClose } from "@element-plus/icons-vue";
|
||||
import { Message } from "@/utils/message";
|
||||
import alignTextLeftImg from "@/assets/align-text-left.png";
|
||||
import alignTextCenterImg from "@/assets/align-text-center.png";
|
||||
import ossManage from "@/views/sys/oss-manage/ossManage";
|
||||
import { modelData } from "./config";
|
||||
import hotzone from "@/views/shop/hotzone";
|
||||
import ways from "@/views/lili-dialog/wap.js";
|
||||
import categoryTemplate from "@/views/lili-dialog/template/category";
|
||||
import VideoConfig from "./video-config";
|
||||
import panelMap from "./config-panel";
|
||||
import alignTextLeft from "@/assets/align-text-left.png";
|
||||
import alignTextCenter from "@/assets/align-text-center.png";
|
||||
import alignTextRight from "@/assets/align-text-right.png";
|
||||
|
||||
const props = defineProps(["res"]);
|
||||
const emit = defineEmits(["handleDrawer"]);
|
||||
|
||||
// alignTextLeftImg/alignTextCenterImg、ways(选择链接的类型)、modelData(装修数据) 为 import 常量,模板中直接使用
|
||||
// alignTextLeft/alignTextCenter/alignTextRight、ways(选择链接的类型)、modelData(装修数据) 为 import 常量,模板中直接使用
|
||||
const picModelFlag = ref(false); //图片选择器
|
||||
const linkType = ref("goods"); // dialog弹窗口类型
|
||||
const styleFlag = ref(false); //广告魔方开关
|
||||
@@ -547,14 +561,26 @@ const textAlign = ref(
|
||||
props.res.options.list[0] ? props.res.options.list[0].textAlign : "center"
|
||||
); //文字对齐方式
|
||||
const promotionsFlag = ref(false); //广告魔方开关
|
||||
const selectedLinkIndex = ref(""); //选择链接的索引
|
||||
const selectedGoods = ref(""); // 已选商品
|
||||
const selectedLinks = ref(""); // 已选链接
|
||||
const modelList = ref(""); // 装修列表
|
||||
const enableSelectCategory = ref(false); //商品是否绑定分类
|
||||
const pendingCategory = ref(null); // 选择分类弹窗草稿
|
||||
const goodsSelectedIndex = ref(0); //绑定商品分类的索引
|
||||
const pictureField = ref("img"); //图片选择器回写的字段
|
||||
|
||||
// 新组件按 type 分发到独立配置面板;老组件走下方遗留分支
|
||||
const panelComponent = computed(
|
||||
() => (props.res && panelMap[props.res.type]) || null
|
||||
);
|
||||
// 面板声明 hideDecorateTitle 时,标题行由面板自己渲染
|
||||
const hidePanelTitle = computed(
|
||||
() => !!(panelComponent.value && panelComponent.value.hideDecorateTitle)
|
||||
);
|
||||
|
||||
// hotzoneRef/ossManageRef:模板 ref 与组件 import 同名,改名避免互相覆盖
|
||||
const liliDialog = ref(null);
|
||||
const liliDialogRef = ref(null);
|
||||
const hotzoneRef = ref(null);
|
||||
const ossManageRef = ref(null);
|
||||
|
||||
@@ -722,9 +748,9 @@ function clickDrawer(item, index) {
|
||||
}
|
||||
// 打开图片选择器
|
||||
function liliDialogFlag(flag) {
|
||||
liliDialog.value.clearGoodsSelected();
|
||||
liliDialog.value.goodsFlag = flag;
|
||||
liliDialog.value.flag = true;
|
||||
liliDialogRef.value.clearGoodsSelected();
|
||||
liliDialogRef.value.goodsFlag = flag;
|
||||
liliDialogRef.value.flag = true;
|
||||
}
|
||||
|
||||
function changeTextAlign(val) {
|
||||
@@ -743,9 +769,24 @@ function clickLink(val, index, oval) {
|
||||
liliDialogFlag(false);
|
||||
}
|
||||
}
|
||||
function addZone(zoneInfo) {
|
||||
selectedLinks.value.zoneInfo.push(zoneInfo);
|
||||
}
|
||||
function changeZone(zoneInfo) {
|
||||
selectedLinks.value.zoneInfo = zoneInfo;
|
||||
}
|
||||
//点击图片解析成base64
|
||||
function changeFile(item, index) {
|
||||
const file = document.getElementById("files" + index).files[0];
|
||||
if (file == void 0) return false;
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(file);
|
||||
nextTick((res) => {
|
||||
reader.onload = (e) => {
|
||||
item.img = e.target.result;
|
||||
};
|
||||
});
|
||||
}
|
||||
//添加设置
|
||||
function addDecorate(type, data) {
|
||||
if (type === "notice") {
|
||||
@@ -768,20 +809,43 @@ function addDecorate(type, data) {
|
||||
props.res.options.list.push(way);
|
||||
}
|
||||
}
|
||||
function addHotPoint() {
|
||||
let way = {
|
||||
img: "https://picsum.photos/id/264/200/200",
|
||||
text: "标题",
|
||||
link: "",
|
||||
url: "",
|
||||
};
|
||||
props.res.options.list[0].points.push(way);
|
||||
}
|
||||
// 图片选择器回显
|
||||
function callbackSelected(val) {
|
||||
picModelFlag.value = false;
|
||||
if (!val?.url) return;
|
||||
selectedGoods.value.img = val.url;
|
||||
if (pictureField.value === "bgImage") {
|
||||
if (!selectedGoods.value.style) {
|
||||
selectedGoods.value.style = {};
|
||||
}
|
||||
selectedGoods.value.style.bgImage = val.url;
|
||||
return;
|
||||
}
|
||||
selectedGoods.value[pictureField.value] = val.url;
|
||||
}
|
||||
// 点击选择图片
|
||||
function handleClickFile(item, index) {
|
||||
function handleClickFile(item, index, field = "img") {
|
||||
pictureField.value = field;
|
||||
selectedGoods.value = item;
|
||||
picModelFlag.value = true;
|
||||
nextTick(() => {
|
||||
if (ossManageRef.value) {
|
||||
ossManageRef.value.selectImage = true;
|
||||
const oss = ossManageRef.value;
|
||||
if (!oss) return;
|
||||
oss.selectImage = true;
|
||||
if (field === "videoUrl") {
|
||||
oss.fileType = "video";
|
||||
} else {
|
||||
oss.fileType = "pic";
|
||||
}
|
||||
oss.changeFileType();
|
||||
});
|
||||
}
|
||||
function removeNotice(index) {
|
||||
|
||||
@@ -3,37 +3,42 @@
|
||||
<!-- 拖拽栏 ,展示栏 -->
|
||||
<div class="model-view">
|
||||
<div class="model-view-menu">
|
||||
<draggable
|
||||
class="model-view-menu-item"
|
||||
:list="modelData"
|
||||
:item-key="getModelKey"
|
||||
:clone="cloneModel"
|
||||
:move="handleMove"
|
||||
v-bind="{
|
||||
group: { name: 'model', pull: 'clone', put: false, animation: 150 },
|
||||
sort: false,
|
||||
ghostClass: 'ghost',
|
||||
}"
|
||||
<div
|
||||
v-for="grp in modelGroups"
|
||||
:key="grp.key"
|
||||
class="model-group"
|
||||
>
|
||||
<template #item="{ element: model }">
|
||||
<li
|
||||
v-if="!model.drawer && !model.drawerPromotions"
|
||||
class="model-item"
|
||||
>
|
||||
<img src alt />
|
||||
<span>{{ model.name }}</span>
|
||||
</li>
|
||||
</template>
|
||||
</draggable>
|
||||
<div class="model-group-title">{{ grp.title }}</div>
|
||||
<draggable
|
||||
class="model-view-menu-item"
|
||||
:list="grp.list"
|
||||
:item-key="getModelKey"
|
||||
:clone="cloneModel"
|
||||
:move="handleMove"
|
||||
:group="{ name: 'model', pull: 'clone', put: false, animation: 150 }"
|
||||
:sort="false"
|
||||
ghost-class="ghost"
|
||||
>
|
||||
<template #item="{ element: model }">
|
||||
<li class="model-item">
|
||||
<el-icon v-if="model.icon && iconMap[model.icon]" :size="22">
|
||||
<component :is="iconMap[model.icon]" />
|
||||
</el-icon>
|
||||
<span>{{ model.name }}</span>
|
||||
</li>
|
||||
</template>
|
||||
</draggable>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="model-view-content">
|
||||
<div class="content">
|
||||
<div class="wap-title">首页</div>
|
||||
<div class="page-setting-entry" @click="showPageSetting">页面设置</div>
|
||||
<div class="content" :style="canvasStyle">
|
||||
<div class="wap-title" :style="titleBarStyle">{{ contentData.title || "店铺首页" }}</div>
|
||||
<draggable
|
||||
class="draggable"
|
||||
group="model"
|
||||
ghostClass="ghost"
|
||||
ghost-class="ghost"
|
||||
@add="handleContentlAdd"
|
||||
@end="handleContentlEnd"
|
||||
v-model="contentData.list"
|
||||
@@ -41,22 +46,40 @@
|
||||
>
|
||||
<template #item="{ element, index }">
|
||||
<div class="list">
|
||||
<component
|
||||
<div class="floor-name">{{ element.name || element.type }}</div>
|
||||
<div
|
||||
v-if="templates[element.type]"
|
||||
class="component"
|
||||
:class="{ active: selected == index }"
|
||||
@click="handleComponent(element, index)"
|
||||
:is="templates[element.type]"
|
||||
:res="element.options"
|
||||
></component>
|
||||
<el-icon
|
||||
v-if="selected == index"
|
||||
@click="closeComponent(index)"
|
||||
class="close"
|
||||
:size="25"
|
||||
color="#e1251b"
|
||||
>
|
||||
<CircleClose />
|
||||
</el-icon>
|
||||
<component :is="templates[element.type]" :res="element.options"></component>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="component unknown-component"
|
||||
@click="handleUnknownComponent(element, index)"
|
||||
>
|
||||
未知组件「{{ element.type }}」已下线,点击删除
|
||||
</div>
|
||||
<div v-if="element.hidden" class="floor-hidden-mask">已隐藏</div>
|
||||
<div v-if="selected == index" class="floor-tools">
|
||||
<el-icon
|
||||
:class="{ disabled: index === 0 }"
|
||||
@click="moveComponent(index, -1)"
|
||||
><Top
|
||||
/></el-icon>
|
||||
<el-icon
|
||||
:class="{ disabled: index === contentData.list.length - 1 }"
|
||||
@click="moveComponent(index, 1)"
|
||||
><Bottom
|
||||
/></el-icon>
|
||||
<el-icon @click="copyComponent(index)"><CopyDocument /></el-icon>
|
||||
<el-icon @click="closeComponent(index)"><Delete /></el-icon>
|
||||
<el-icon @click="toggleHide(index)"
|
||||
><View v-if="!element.hidden" /><Hide v-else
|
||||
/></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</draggable>
|
||||
@@ -70,30 +93,89 @@
|
||||
v-if="decorateData"
|
||||
:res="decorateData"
|
||||
></decorate>
|
||||
<PanelPageSetting v-else :res="contentData" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { ref, watch, onMounted, nextTick } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import { CircleClose } from "@element-plus/icons-vue";
|
||||
import { ref, watch, nextTick, onMounted, computed } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import {
|
||||
Picture,
|
||||
PictureFilled,
|
||||
Document,
|
||||
Grid,
|
||||
Search,
|
||||
Goods,
|
||||
Bell,
|
||||
VideoCamera,
|
||||
Ticket,
|
||||
Collection,
|
||||
Crop,
|
||||
Minus,
|
||||
EditPen,
|
||||
Top,
|
||||
Bottom,
|
||||
CopyDocument,
|
||||
Delete,
|
||||
View,
|
||||
Hide,
|
||||
} from "@element-plus/icons-vue";
|
||||
import templates from "./template/index";
|
||||
import Draggable from "vuedraggable";
|
||||
import { modelData } from "./config";
|
||||
import decorate from "./decorate";
|
||||
import PanelPageSetting from "./config-panel/PanelPageSetting.vue";
|
||||
import * as API_Other from "@/api/other";
|
||||
import { getStore } from "@/libs/storage";
|
||||
import { Modal } from "@/utils/message";
|
||||
import { Message, Modal } from "@/utils/message";
|
||||
import { useAppStore } from "@/stores/app";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const appStore = useAppStore();
|
||||
|
||||
// templates: 模板类型,modelData: 装修模型,均为 import 常量,模板中直接使用
|
||||
// script setup 下 :is 无法按字符串解析局部导入,用映射表渲染组件图标
|
||||
const iconMap = {
|
||||
Picture,
|
||||
PictureFilled,
|
||||
Document,
|
||||
Grid,
|
||||
Search,
|
||||
Goods,
|
||||
Bell,
|
||||
VideoCamera,
|
||||
Ticket,
|
||||
Collection,
|
||||
Crop,
|
||||
Minus,
|
||||
EditPen,
|
||||
};
|
||||
|
||||
const selected = ref(0); // 已选下标
|
||||
// 页面设置默认值(老数据没有这些字段,加载时合并)
|
||||
const PAGE_SETTING_DEFAULTS = {
|
||||
name: "",
|
||||
title: "",
|
||||
bgType: "color", // color | image
|
||||
bgColor: "#F5F5F5",
|
||||
bgImage: "",
|
||||
bgFill: "stretch", // stretch 拉伸 | contain 缩放 | cover 填充
|
||||
topColor: "#ffffff",
|
||||
titleColor: "black", // black | white
|
||||
};
|
||||
function mergePageData(data) {
|
||||
const source = data && typeof data === "object" ? data : {};
|
||||
return {
|
||||
...PAGE_SETTING_DEFAULTS,
|
||||
...source,
|
||||
list: Array.isArray(source.list) ? source.list : [],
|
||||
};
|
||||
}
|
||||
const contentData = ref({
|
||||
// 总数据
|
||||
...PAGE_SETTING_DEFAULTS,
|
||||
list: [],
|
||||
});
|
||||
const decorateData = ref(""); // 装修数据
|
||||
@@ -103,9 +185,52 @@ watch(
|
||||
(val) => {
|
||||
appStore.styleStore = val;
|
||||
},
|
||||
{ deep: true }
|
||||
{ deep: true, immediate: true }
|
||||
);
|
||||
|
||||
// 画布背景:背景色或背景图(拉伸/缩放/填充)
|
||||
const canvasStyle = computed(() => {
|
||||
const d = contentData.value;
|
||||
if (d.bgType === "image" && d.bgImage) {
|
||||
const size =
|
||||
{ stretch: "100% 100%", contain: "contain", cover: "cover" }[d.bgFill] ||
|
||||
"100% 100%";
|
||||
return {
|
||||
backgroundImage: `url(${d.bgImage})`,
|
||||
backgroundSize: size,
|
||||
backgroundRepeat: "no-repeat",
|
||||
backgroundColor: d.bgColor || "#F5F5F5",
|
||||
};
|
||||
}
|
||||
return { backgroundColor: d.bgColor || "#F5F5F5" };
|
||||
});
|
||||
// 画布顶部标题栏:背景用顶部颜色,文字色随标题颜色
|
||||
const titleBarStyle = computed(() => {
|
||||
const d = contentData.value;
|
||||
return {
|
||||
background: d.topColor || "#ffffff",
|
||||
color: d.titleColor === "black" ? "#000" : "#fff",
|
||||
};
|
||||
});
|
||||
// 清空楼层选中,右侧回到页面设置面板
|
||||
function showPageSetting() {
|
||||
selected.value = -1;
|
||||
decorateData.value = "";
|
||||
}
|
||||
|
||||
// 组件面板三类分组(drawer 标记的是图片魔方的可选风格,不进左侧组件列表)
|
||||
const modelGroups = computed(() => {
|
||||
const groups = [
|
||||
{ key: "base", title: "基础组件" },
|
||||
{ key: "marketing", title: "商品营销" },
|
||||
{ key: "tools", title: "工具组件" },
|
||||
];
|
||||
return groups.map((g) => ({
|
||||
...g,
|
||||
list: modelData.filter((m) => m.group === g.key && !m.drawer),
|
||||
}));
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
init();
|
||||
});
|
||||
@@ -118,7 +243,12 @@ function getContentItemKey(element) {
|
||||
}
|
||||
function cloneModel(model) {
|
||||
const key = Date.now() + "_" + Math.ceil(Math.random() * 99999);
|
||||
const cloned = JSON.parse(JSON.stringify(model));
|
||||
// 图片魔方拖入画布时默认使用「左一右二」风格
|
||||
const source =
|
||||
model.type === "tpl_ad_list"
|
||||
? modelData.find((m) => m.type === "leftOneRightTwo") || model
|
||||
: model;
|
||||
const cloned = JSON.parse(JSON.stringify(source));
|
||||
return {
|
||||
...cloned,
|
||||
key,
|
||||
@@ -127,29 +257,72 @@ function cloneModel(model) {
|
||||
}
|
||||
// 初始化数据
|
||||
function init() {
|
||||
// 先读缓存,如果缓存有值则读缓存。
|
||||
const cache = getStore('managerMobilePageCache')
|
||||
if(cache){
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
content: '获取到本地有缓存数据,是否使用缓存数据?',
|
||||
okText: '使用',
|
||||
cancelText: '取消',
|
||||
// 先读缓存,如果缓存有值则询问是否使用缓存
|
||||
const cache = getStore("sellerMobilePageCache") || getStore("managerMobilePageCache");
|
||||
if (cache) {
|
||||
Modal.confirm({
|
||||
title: "提示",
|
||||
content: "获取到本地有缓存数据,是否使用缓存数据?",
|
||||
okText: "使用",
|
||||
cancelText: "取消",
|
||||
onOk: () => {
|
||||
const data = JSON.parse(cache);
|
||||
contentData.value = data;
|
||||
}
|
||||
contentData.value = mergePageData(data);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (!route.query.id) {
|
||||
if (route.query.mode !== "add" && (!route.query.pagetype || route.query.pagetype === "INDEX" || route.query.pagetype === "STORE")) {
|
||||
API_Other.getHomeList({
|
||||
pageNumber: 1,
|
||||
pageSize: 10,
|
||||
pageClientType: "H5",
|
||||
}).then((res) => {
|
||||
const records = (res && res.result && res.result.records) || [];
|
||||
const active = records.find((r) => r.pageShow === "OPEN" || r.pageShow === true) || records[0];
|
||||
if (!active || !active.id) return;
|
||||
router.replace({
|
||||
path: route.path,
|
||||
query: {
|
||||
...route.query,
|
||||
id: active.id,
|
||||
name: active.name,
|
||||
type: active.pageShow === "OPEN" || active.pageShow === true,
|
||||
},
|
||||
});
|
||||
API_Other.getHomeData(active.id).then((detail) => {
|
||||
const parsed = JSON.parse(detail.result.pageData);
|
||||
contentData.value = mergePageData(parsed);
|
||||
if (!contentData.value.name) {
|
||||
contentData.value.name = active.name || "";
|
||||
}
|
||||
const first = contentData.value.list[0];
|
||||
if (first && templates[first.type]) {
|
||||
handleComponent(first, 0);
|
||||
}
|
||||
});
|
||||
});
|
||||
return false;
|
||||
}
|
||||
if (!route.query.id) return false;
|
||||
// 新建页面:模板名称取路由参数
|
||||
if (!contentData.value.name) {
|
||||
contentData.value.name = route.query.name || "店铺首页模板";
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
API_Other.getHomeData(route.query.id).then((res) => {
|
||||
contentData.value = JSON.parse(res.result.pageData);
|
||||
|
||||
handleComponent(contentData.value.list[0], 0);
|
||||
const parsed = JSON.parse(res.result.pageData);
|
||||
contentData.value = mergePageData(parsed);
|
||||
if (!contentData.value.name) {
|
||||
contentData.value.name = route.query.name || res.result.name || "";
|
||||
}
|
||||
const first = contentData.value.list[0];
|
||||
if (first && templates[first.type]) {
|
||||
handleComponent(first, 0);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
// 中间组件拖动,右侧数据绑定不变
|
||||
@@ -165,17 +338,15 @@ function closeComponent(index) {
|
||||
|
||||
// 如果当前楼层不为一
|
||||
if (contentData.value.list.length > 1) {
|
||||
// 如果当前最底层 给下一层赋值
|
||||
|
||||
if (index - 1 == -1) {
|
||||
handleComponent(contentData.value.list[index], index);
|
||||
} else {
|
||||
// 如果不是最底层给上一层赋值
|
||||
handleComponent(contentData.value.list[index - 1], index - 1);
|
||||
}
|
||||
contentData.value.list.splice(index, 1);
|
||||
} else {
|
||||
contentData.value.list.splice(index, 1);
|
||||
selected.value = -1;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -185,9 +356,37 @@ function handleComponent(val, index) {
|
||||
selected.value = index;
|
||||
decorateData.value = val;
|
||||
}
|
||||
// 右侧栏回调
|
||||
// 已下线的未知组件:点击占位条直接删除
|
||||
function handleUnknownComponent(element, index) {
|
||||
console.warn(`[page-decoration] 未知组件类型: ${element.type}`);
|
||||
closeComponent(index);
|
||||
}
|
||||
// 上移/下移楼层
|
||||
function moveComponent(index, dir) {
|
||||
const target = index + dir;
|
||||
if (target < 0 || target >= contentData.value.list.length) return;
|
||||
const list = contentData.value.list;
|
||||
[list[index], list[target]] = [list[target], list[index]];
|
||||
selected.value = target;
|
||||
handleComponent(contentData.value.list[target], target);
|
||||
}
|
||||
// 复制楼层
|
||||
function copyComponent(index) {
|
||||
const cloned = JSON.parse(JSON.stringify(contentData.value.list[index]));
|
||||
const key = Date.now() + "_" + Math.ceil(Math.random() * 99999);
|
||||
cloned.key = key;
|
||||
cloned.model = cloned.type + "_" + key;
|
||||
contentData.value.list.splice(index + 1, 0, cloned);
|
||||
handleComponent(contentData.value.list[index + 1], index + 1);
|
||||
}
|
||||
// 隐藏/显示楼层(渲染端跳过 hidden 楼层)
|
||||
function toggleHide(index) {
|
||||
const item = contentData.value.list[index];
|
||||
item.hidden = !item.hidden;
|
||||
}
|
||||
// 右侧栏回调(广告魔方风格切换等)
|
||||
function handleDrawer(val) {
|
||||
const newIndex = selected.value;
|
||||
let newIndex = selected.value;
|
||||
decorateData.value = "";
|
||||
contentData.value.list[newIndex] = {
|
||||
...val,
|
||||
@@ -196,11 +395,13 @@ function handleDrawer(val) {
|
||||
},
|
||||
model: val.type,
|
||||
};
|
||||
contentData.value = JSON.parse(JSON.stringify(contentData.value));
|
||||
contentData.value.list = JSON.parse(
|
||||
JSON.stringify(contentData.value.list)
|
||||
);
|
||||
decorateData.value = contentData.value.list[newIndex];
|
||||
}
|
||||
|
||||
// 封装拖拽参数(原名 package,ESM 严格模式下为保留字,改名 packageModel)
|
||||
// 封装拖拽参数
|
||||
function packageModel(val, newIndex) {
|
||||
contentData.value.list[newIndex] = "";
|
||||
val = JSON.parse(JSON.stringify(val));
|
||||
@@ -217,7 +418,6 @@ function packageModel(val, newIndex) {
|
||||
// 拖动
|
||||
function handleContentlAdd(evt) {
|
||||
const { newIndex } = evt;
|
||||
|
||||
packageModel(contentData.value.list[newIndex], newIndex);
|
||||
handleComponent(contentData.value.list[newIndex], newIndex);
|
||||
}
|
||||
@@ -227,4 +427,35 @@ function handleMove() {
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
@import "./style.scss";
|
||||
|
||||
// 页面设置:画布容器支持悬浮入口、画布背景可透出到拖拽区
|
||||
.model-view-content {
|
||||
position: relative;
|
||||
}
|
||||
.page-setting-entry {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
right: 16px;
|
||||
z-index: 200;
|
||||
padding: 6px 14px;
|
||||
font-size: 13px;
|
||||
color: #409eff;
|
||||
background: #ffffff;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
&:hover {
|
||||
border-color: #409eff;
|
||||
color: #409eff;
|
||||
box-shadow: 0 2px 8px rgba(64, 158, 255, 0.2);
|
||||
}
|
||||
}
|
||||
.content > .draggable {
|
||||
background: transparent;
|
||||
}
|
||||
.wap-title {
|
||||
border-bottom: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,7 +1,26 @@
|
||||
<template>
|
||||
<!-- 预览保存 -->
|
||||
<div class="model-title">
|
||||
<div>店铺装修</div>
|
||||
<div class="model-title-left">
|
||||
<el-button size="default" @click="goBack">返回</el-button>
|
||||
<div class="page-name">
|
||||
<template v-if="!editingName">
|
||||
<span class="page-name-label">当前页面:{{ pageName }}</span>
|
||||
<el-icon class="page-name-edit" @click="startEditName"><Edit /></el-icon>
|
||||
</template>
|
||||
<el-input
|
||||
v-else
|
||||
ref="nameInput"
|
||||
v-model="pageName"
|
||||
size="small"
|
||||
style="width: 200px"
|
||||
maxlength="30"
|
||||
placeholder="请输入模板名称"
|
||||
@blur="editingName = false"
|
||||
@keyup.enter="editingName = false"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="btns">
|
||||
<el-button
|
||||
@click="clickBtn(item)"
|
||||
@@ -12,29 +31,22 @@
|
||||
>{{ item.title }}</el-button>
|
||||
</div>
|
||||
<div class="model-title-view-btn">
|
||||
<!-- TODO 后期会补全 目前版本暂无 -->
|
||||
<!-- <Poptip placement="bottom" width="100">
|
||||
<el-button size="default" @click="creatQrCode">预览模板</el-button>
|
||||
<template #content><div class="default-view-content">
|
||||
<div>临时预览</div>
|
||||
<div ref="qrCodeUrl"></div>
|
||||
</div>
|
||||
</template></el-popconfirm>-->
|
||||
<el-button size="default" type="default" v-if="hasCache" @click="clearCache">清空本地缓存</el-button>
|
||||
<el-button size="default" type="primary" @click="handleSpinShow">保存模板</el-button>
|
||||
<el-button size="default" type="primary" @click="handleSpinShow">保存页面</el-button>
|
||||
|
||||
<el-dialog
|
||||
title="保存中"
|
||||
:title="progress ? '保存页面' : '保存中'"
|
||||
v-model="saveDialog"
|
||||
:show-close="true"
|
||||
:close-on-click-modal="false"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
@closed="handleDialogClosed"
|
||||
>
|
||||
<div v-if="progress">
|
||||
<div class="model-item">
|
||||
模板名称
|
||||
<el-input style="width: 200px" v-model="submitWay.name" />
|
||||
<el-input style="width: 200px" v-model="pageName" placeholder="请输入模板名称" />
|
||||
</div>
|
||||
<div class="model-item">
|
||||
是否立即发布
|
||||
@@ -46,7 +58,7 @@
|
||||
</div>
|
||||
<div class="model-item">
|
||||
将当前装修内容写入到本地缓存中,下次进入页面时可继续使用
|
||||
<el-button type="small" @click="witeLocalStore">写入</el-button>
|
||||
<el-button size="small" @click="witeLocalStore">写入</el-button>
|
||||
</div>
|
||||
|
||||
<el-button type="primary" @click="save()">保存</el-button>
|
||||
@@ -57,12 +69,13 @@
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { onMounted, reactive, ref, watch } from "vue";
|
||||
import { reactive, ref, watch, onMounted, nextTick, computed } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { useAppStore } from "@/stores/app";
|
||||
import { Message } from "@/utils/message";
|
||||
import { getStore, setStore } from "@/libs/storage";
|
||||
import * as API_Other from "@/api/other.js";
|
||||
import { Edit } from "@element-plus/icons-vue";
|
||||
import { getStore, setStore } from "@/libs/storage";
|
||||
import { Message } from "@/utils/message";
|
||||
import { useAppStore } from "@/stores/app";
|
||||
|
||||
const props = defineProps(["pagetype"]);
|
||||
const emit = defineEmits(["selected"]);
|
||||
@@ -73,9 +86,10 @@ const appStore = useAppStore();
|
||||
|
||||
const hasCache = ref(false); // 是否有缓存
|
||||
const progress = ref(true); // 展示进度
|
||||
const num = ref(20); // 提交进度
|
||||
const num = ref(0); // 提交进度
|
||||
const saveDialog = ref(false); // 加载状态
|
||||
const way = ref([
|
||||
// 装修tab栏切换
|
||||
{
|
||||
title: "首页",
|
||||
name: "index",
|
||||
@@ -85,11 +99,48 @@ const way = ref([
|
||||
|
||||
const submitWay = reactive({
|
||||
// 表单信息
|
||||
pageShow: route.query.type || false,
|
||||
name: route.query.name || "模板名称",
|
||||
pageClientType: "H5"
|
||||
pageShow: route.query.type === "true" || route.query.type === true || route.query.type === "OPEN",
|
||||
name: route.query.name || "店铺首页模板",
|
||||
pageClientType: "H5",
|
||||
});
|
||||
|
||||
// 模板名称(优先读写 store 中的 name,便于与页面设置面板联动)
|
||||
const editingName = ref(false);
|
||||
const nameInput = ref(null);
|
||||
const pageName = computed({
|
||||
get() {
|
||||
const fromStore = appStore.styleStore && appStore.styleStore.name;
|
||||
return fromStore || submitWay.name;
|
||||
},
|
||||
set(val) {
|
||||
submitWay.name = val;
|
||||
if (appStore.styleStore && typeof appStore.styleStore === "object") {
|
||||
appStore.styleStore.name = val;
|
||||
}
|
||||
},
|
||||
});
|
||||
function startEditName() {
|
||||
editingName.value = true;
|
||||
nextTick(() => {
|
||||
nameInput.value?.focus();
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const store = appStore.styleStore;
|
||||
if (store && typeof store === "object" && !store.name && route.query.name) {
|
||||
store.name = route.query.name;
|
||||
}
|
||||
hasCache.value = !!(getStore("sellerMobilePageCache") || getStore("managerMobilePageCache"));
|
||||
});
|
||||
|
||||
watch(
|
||||
() => route.query.type,
|
||||
(val) => {
|
||||
submitWay.pageShow = val === "true" || val === true || val === "OPEN";
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.pagetype,
|
||||
(val) => {
|
||||
@@ -103,109 +154,144 @@ watch(
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
hasCache.value = getStore('managerMobilePageCache') ? true : false;
|
||||
});
|
||||
|
||||
function clearCache() {
|
||||
setStore('managerMobilePageCache','');
|
||||
Message.success('清空成功')
|
||||
setStore("sellerMobilePageCache", "");
|
||||
setStore("managerMobilePageCache", "");
|
||||
hasCache.value = false;
|
||||
Message.success("清空成功");
|
||||
}
|
||||
// 将楼层装修的内容写入到本地缓存中
|
||||
|
||||
function witeLocalStore() {
|
||||
setStore('managerMobilePageCache', appStore.styleStore)
|
||||
Message.success('写入成功')
|
||||
setStore("sellerMobilePageCache", appStore.styleStore);
|
||||
hasCache.value = true;
|
||||
Message.success("写入成功");
|
||||
}
|
||||
|
||||
function clickBtn(val) {
|
||||
way.value.forEach((item, index) => {
|
||||
way.value.forEach((item) => {
|
||||
item.selected = false;
|
||||
});
|
||||
val.selected = true;
|
||||
emit("selected", val.name);
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载,并保存模板
|
||||
*/
|
||||
function handleSpinShow() {
|
||||
progress.value = true;
|
||||
num.value = 0;
|
||||
saveDialog.value = true;
|
||||
}
|
||||
|
||||
// 填写是否发布,模板名称之后保存
|
||||
function handleDialogClosed() {
|
||||
progress.value = true;
|
||||
num.value = 0;
|
||||
}
|
||||
|
||||
function save() {
|
||||
if (
|
||||
appStore.styleStore == void 0 &&
|
||||
route.query.pagetype != 'SPECIAL'
|
||||
) {
|
||||
if (!pageName.value || !pageName.value.trim()) {
|
||||
Message.error("请填写模板名称");
|
||||
return false;
|
||||
}
|
||||
const title = appStore.styleStore && appStore.styleStore.title;
|
||||
if (!title || !String(title).trim()) {
|
||||
Message.error("请填写页面标题");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (appStore.styleStore == void 0 && route.query.pagetype != "SPECIAL") {
|
||||
Message.error("请装修楼层");
|
||||
return false;
|
||||
}
|
||||
|
||||
submitWay?.pageShow === true
|
||||
? (submitWay.pageShow = "OPEN")
|
||||
: (submitWay.pageShow = "CLOSE");
|
||||
const payload = {
|
||||
...submitWay,
|
||||
pageShow: submitWay.pageShow ? "OPEN" : "CLOSE",
|
||||
name: pageName.value,
|
||||
pageData: JSON.stringify(appStore.styleStore),
|
||||
pageType: route.query.pagetype || "INDEX",
|
||||
};
|
||||
|
||||
submitWay.pageData = JSON.stringify(appStore.styleStore);
|
||||
submitWay.pageType = route.query.pagetype || "INDEX";
|
||||
|
||||
route.query.id ? updateHome() : submit(submitWay);
|
||||
route.query.id ? updateHome(payload) : submit(payload);
|
||||
}
|
||||
|
||||
// 更新
|
||||
function updateHome() {
|
||||
function updateHome(payload) {
|
||||
progress.value = false;
|
||||
API_Other.updateHome(route.query.id, {
|
||||
num.value = 20;
|
||||
const data = payload || {
|
||||
pageData: JSON.stringify(appStore.styleStore),
|
||||
name: submitWay.name,
|
||||
pageShow: submitWay.pageShow,
|
||||
pageType: submitWay.pageType,
|
||||
name: pageName.value,
|
||||
pageShow: submitWay.pageShow ? "OPEN" : "CLOSE",
|
||||
pageType: submitWay.pageType || route.query.pagetype || "INDEX",
|
||||
pageClientType: "H5",
|
||||
})
|
||||
.then(res => {
|
||||
num.value = 50;
|
||||
};
|
||||
API_Other.updateHome(route.query.id, data)
|
||||
.then((res) => {
|
||||
num.value = 60;
|
||||
if (res.success) {
|
||||
num.value = 80;
|
||||
/**制作保存成功动画¸ */
|
||||
num.value = 100;
|
||||
setTimeout(() => {
|
||||
saveDialog.value = false;
|
||||
progress.value = true;
|
||||
num.value = 0;
|
||||
Message.success("修改成功");
|
||||
goBack();
|
||||
}, 1000);
|
||||
}, 800);
|
||||
} else {
|
||||
progress.value = true;
|
||||
num.value = 0;
|
||||
saveDialog.value = false;
|
||||
Message.error("修改失败,请稍后重试");
|
||||
}
|
||||
})
|
||||
.catch(error => {});
|
||||
.catch(() => {
|
||||
progress.value = true;
|
||||
num.value = 0;
|
||||
saveDialog.value = false;
|
||||
Message.error("修改失败,请稍后重试");
|
||||
});
|
||||
}
|
||||
|
||||
// 返回查询数据页面
|
||||
function goBack() {
|
||||
router.go(-1);
|
||||
if (window.history.state?.back) {
|
||||
router.go(-1);
|
||||
} else {
|
||||
router.push("/wapList");
|
||||
}
|
||||
}
|
||||
|
||||
// 保存
|
||||
function submit(submitWay) {
|
||||
function submit(payload) {
|
||||
progress.value = false;
|
||||
API_Other.setHomeSetup(submitWay)
|
||||
.then(res => {
|
||||
num.value = 50;
|
||||
num.value = 20;
|
||||
const data = payload || {
|
||||
...submitWay,
|
||||
pageShow: submitWay.pageShow ? "OPEN" : "CLOSE",
|
||||
name: pageName.value,
|
||||
pageData: JSON.stringify(appStore.styleStore),
|
||||
pageType: route.query.pagetype || "INDEX",
|
||||
};
|
||||
API_Other.setHomeSetup(data)
|
||||
.then((res) => {
|
||||
num.value = 60;
|
||||
if (res.success) {
|
||||
num.value = 80;
|
||||
/**制作保存成功动画¸ */
|
||||
num.value = 100;
|
||||
setTimeout(() => {
|
||||
saveDialog.value = false;
|
||||
progress.value = true;
|
||||
num.value = 0;
|
||||
Message.success("保存成功");
|
||||
goBack();
|
||||
}, 1000);
|
||||
}, 800);
|
||||
} else {
|
||||
progress.value = true;
|
||||
num.value = 0;
|
||||
saveDialog.value = false;
|
||||
Message.error("保存失败,请稍后重试");
|
||||
}
|
||||
})
|
||||
.catch(error => {});
|
||||
.catch(() => {
|
||||
progress.value = true;
|
||||
num.value = 0;
|
||||
saveDialog.value = false;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
@@ -219,17 +305,44 @@ function submit(submitWay) {
|
||||
}
|
||||
}
|
||||
.model-title {
|
||||
height: 70px;
|
||||
height: 60px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
> .model-title-view-btn {
|
||||
> * {
|
||||
margin: 0 10px;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
padding: 0 16px;
|
||||
margin-bottom: 12px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04);
|
||||
.model-title-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
.page-name {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 14px;
|
||||
color: #303133;
|
||||
font-weight: 500;
|
||||
}
|
||||
.page-name-edit {
|
||||
cursor: pointer;
|
||||
color: #909399;
|
||||
font-size: 14px;
|
||||
&:hover {
|
||||
color: #409eff;
|
||||
}
|
||||
}
|
||||
> .model-title-view-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
.btns {
|
||||
* {
|
||||
|
||||
@@ -1,12 +1,42 @@
|
||||
.model-view {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
height: calc(100vh - 96px);
|
||||
.model-view-menu {
|
||||
flex: 1.5;
|
||||
min-width: 250px;
|
||||
> .model-view-menu-item {
|
||||
overflow-y: auto;
|
||||
background: #ffffff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04);
|
||||
padding: 14px 12px;
|
||||
box-sizing: border-box;
|
||||
.model-view-menu-item {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
gap: 8px;
|
||||
}
|
||||
.model-group {
|
||||
margin-bottom: 16px;
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
.model-group-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
font-size: 15px;
|
||||
font-weight: 400;
|
||||
color: #303133;
|
||||
margin: 0 0 12px;
|
||||
&::before {
|
||||
content: "";
|
||||
width: 3px;
|
||||
height: 12px;
|
||||
border-radius: 2px;
|
||||
background: #409eff;
|
||||
margin-right: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.model-config {
|
||||
@@ -16,17 +46,44 @@
|
||||
flex: 6;
|
||||
}
|
||||
.model-item {
|
||||
line-height: 1.75;
|
||||
font-size: 13px;
|
||||
margin: 0 5px 10px 5px;
|
||||
border: 1px solid #ededed;
|
||||
background: #f6f6f9;
|
||||
width: 70px;
|
||||
height: 70px;
|
||||
line-height: 1.4;
|
||||
font-size: 12px;
|
||||
border: 1px solid #ebeef5;
|
||||
background: #ffffff;
|
||||
border-radius: 8px;
|
||||
height: 78px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
color: #606266;
|
||||
cursor: grab;
|
||||
transition: all 0.2s ease;
|
||||
.el-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 8px;
|
||||
background: #f0f5ff;
|
||||
color: #409eff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
&:hover {
|
||||
border-color: #c6e2ff;
|
||||
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.15);
|
||||
transform: translateY(-2px);
|
||||
color: #303133;
|
||||
.el-icon {
|
||||
background: #409eff;
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
&:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
}
|
||||
}
|
||||
.model-view-content {
|
||||
@@ -36,6 +93,8 @@
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
.wap-title {
|
||||
line-height: 44px;
|
||||
@@ -48,12 +107,12 @@
|
||||
.content {
|
||||
box-sizing: border-box;
|
||||
margin: 20px 0;
|
||||
padding: 50px 13px;
|
||||
width: 360px;
|
||||
background: url("../../../assets/iPhoneX_model.png") no-repeat;
|
||||
height: 780px;
|
||||
background-size: 100% 100%;
|
||||
overflow: hidden;
|
||||
width: 375px;
|
||||
background: #ebebeb;
|
||||
min-height: calc(100vh - 200px);
|
||||
border-radius: 8px;
|
||||
overflow: visible;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -61,8 +120,7 @@
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
min-height: 0;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
overflow: visible;
|
||||
background: #ebebeb;
|
||||
padding-bottom: 100px;
|
||||
box-sizing: border-box;
|
||||
@@ -77,11 +135,67 @@
|
||||
.list {
|
||||
position: relative;
|
||||
}
|
||||
.close {
|
||||
.floor-name {
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
top: 10px;
|
||||
z-index: 99;
|
||||
right: calc(100% + 8px);
|
||||
top: 8px;
|
||||
z-index: 60;
|
||||
background: #ffffff;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 4px;
|
||||
padding: 2px 8px;
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
white-space: nowrap;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
|
||||
pointer-events: none;
|
||||
}
|
||||
.floor-tools {
|
||||
position: absolute;
|
||||
right: -56px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
z-index: 100;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #ffffff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
padding: 4px 0;
|
||||
.el-icon {
|
||||
font-size: 20px;
|
||||
padding: 9px 12px;
|
||||
cursor: pointer;
|
||||
color: #606266;
|
||||
&:hover {
|
||||
color: #409eff;
|
||||
}
|
||||
&.disabled {
|
||||
color: #dcdfe6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
}
|
||||
.floor-hidden-mask {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 50;
|
||||
background: rgba(0, 0, 0, 0.45);
|
||||
color: #ffffff;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
pointer-events: none;
|
||||
}
|
||||
.unknown-component {
|
||||
padding: 16px 10px;
|
||||
background: #fff2f0;
|
||||
border: 1px dashed #ffccc7;
|
||||
color: #cf1322;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.model-btn {
|
||||
@@ -97,15 +211,16 @@
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
line-height: 2;
|
||||
:deep(img) {
|
||||
:deep(img){
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
.model-config {
|
||||
overflow-y: auto;
|
||||
background: #f5f7fa;
|
||||
}
|
||||
|
||||
.active {
|
||||
box-sizing: border-box;
|
||||
border: 1px solid $theme_color;
|
||||
border: 2px solid $theme_color;
|
||||
}
|
||||
|
||||
28
seller/src/views/shop/wap/template/cubeStyle.js
Normal file
@@ -0,0 +1,28 @@
|
||||
// 图片魔方各布局预览共用的样式字段(面板配置;老数据无字段时返回空对象,保持原样)
|
||||
// 画布 375px ≈ 750rpx,预览按数值一半像素呈现
|
||||
const px = (v) => `${(Number(v) || 0) / 2}px`;
|
||||
|
||||
export function cubeWrapperStyle(res) {
|
||||
if (!res || res.paddingTop === undefined) return {};
|
||||
const bg =
|
||||
!res.bgColor2 || res.bgColor1 === res.bgColor2
|
||||
? res.bgColor1
|
||||
: `linear-gradient(180deg, ${res.bgColor1}, ${res.bgColor2})`;
|
||||
return {
|
||||
padding: `${px(res.paddingTop)} ${px(res.paddingX)} ${px(res.paddingBottom)}`,
|
||||
background: bg,
|
||||
borderRadius: px(res.borderRadius),
|
||||
marginBottom: px(res.pageGap),
|
||||
overflow: Number(res.borderRadius) > 0 ? "hidden" : "visible",
|
||||
};
|
||||
}
|
||||
|
||||
export function cubeImgStyle(res) {
|
||||
if (!res || res.imgRadius === undefined) return {};
|
||||
return { borderRadius: px(res.imgRadius), overflow: "hidden" };
|
||||
}
|
||||
|
||||
export function cubeGapStyle(res) {
|
||||
if (!res || res.contentGap === undefined) return {};
|
||||
return { gap: px(res.contentGap) };
|
||||
}
|
||||
@@ -12,20 +12,34 @@ import tpl_flex_four from "./tpl_flex_four";
|
||||
import tpl_text_picture from "./tpl_text_picture";
|
||||
import tpl_menu from "./tpl_menu";
|
||||
import tpl_search from "./tpl_search";
|
||||
import tpl_join_group from "./tpl_join_group";
|
||||
import tpl_promotion_detail from "./tpl_promotion_detail";
|
||||
import tpl_integral from "./tpl_integral";
|
||||
import tpl_spike from "./tpl_spike";
|
||||
import tpl_group from "./tpl_group";
|
||||
import tpl_ad_list from "./tpl_view_list";
|
||||
import tpl_activity_list from './tpl_view_list'
|
||||
import tpl_activity_list from './tpl_view_list';
|
||||
// import tpl_hot_area from './tpl_hot_area';
|
||||
import tpl_video from "./tpl_video";
|
||||
import tpl_goods from "./tpl_goods";
|
||||
import tpl_notice from "./tpl_notice";
|
||||
import tpl_promotions from "./tpl_promotions";
|
||||
import tpl_blank from "./tpl_blank";
|
||||
import tpl_line from "./tpl_line";
|
||||
import tpl_rich_text from "./tpl_rich_text";
|
||||
import tpl_coupon from "./tpl_coupon";
|
||||
import tpl_article from "./tpl_article";
|
||||
import tpl_shop from "./tpl_shop";
|
||||
import tpl_home_comb from "./tpl_home_comb";
|
||||
|
||||
export default {
|
||||
homeComb: tpl_home_comb,
|
||||
notice: tpl_notice,
|
||||
carousel: tpl_banner,
|
||||
title: tpl_title,
|
||||
leftOneRightTwo: tpl_left_one_right_two,
|
||||
leftTwoRightOne: tpl_left_two_right_one,
|
||||
topOneBottomTwo:tpl_top_one_bottom_two,
|
||||
topTwoBottomOne:tpl_top_two_bottom_one,
|
||||
topOneBottomTwo: tpl_top_one_bottom_two,
|
||||
topTwoBottomOne: tpl_top_two_bottom_one,
|
||||
flexThree: tpl_flex_three,
|
||||
flexFive: tpl_flex_five,
|
||||
flexFour: tpl_flex_four,
|
||||
@@ -33,12 +47,21 @@ export default {
|
||||
textPicture: tpl_text_picture,
|
||||
menu: tpl_menu,
|
||||
search: tpl_search,
|
||||
joinGroup: tpl_join_group,
|
||||
promotionDetail: tpl_promotion_detail,
|
||||
flexOne: tpl_flex_one,
|
||||
goods: tpl_goods,
|
||||
integral:tpl_integral,
|
||||
spike:tpl_spike,
|
||||
group:tpl_group,
|
||||
integral: tpl_integral,
|
||||
spike: tpl_spike,
|
||||
group: tpl_group,
|
||||
// hotArea: tpl_hot_area,
|
||||
video: tpl_video,
|
||||
tpl_ad_list,
|
||||
tpl_activity_list
|
||||
promotions: tpl_promotions,
|
||||
tpl_activity_list,
|
||||
blank: tpl_blank,
|
||||
line: tpl_line,
|
||||
richText: tpl_rich_text,
|
||||
coupon: tpl_coupon,
|
||||
article: tpl_article,
|
||||
shop: tpl_shop,
|
||||
};
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
}
|
||||
.layout {
|
||||
padding: 8px;
|
||||
margin: 4px 0;
|
||||
margin: 0;
|
||||
background: #fff;
|
||||
}
|
||||
.layout,
|
||||
|
||||
154
seller/src/views/shop/wap/template/tpl_article.vue
Normal file
@@ -0,0 +1,154 @@
|
||||
<template>
|
||||
<div class="tpl-article" :class="{ styled: styled }" :style="wrapperStyle">
|
||||
<div class="article-title" v-if="title">{{ title }}</div>
|
||||
<template v-if="articleList.length">
|
||||
<div class="article-item" v-for="item in articleList" :key="item.id">
|
||||
<div class="article-text">
|
||||
<div class="article-name" :style="nameStyle">{{ item.title }}</div>
|
||||
<div class="article-time" v-if="item.createTime">{{ item.createTime }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="article-item" v-for="i in 3" :key="i">
|
||||
<div class="article-text">
|
||||
<div class="article-name" :style="nameStyle">文章标题示例文章标题示例</div>
|
||||
</div>
|
||||
<div class="article-thumb"></div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getArticle } from "@/api/pages";
|
||||
|
||||
export default {
|
||||
name: "tpl_article",
|
||||
props: {
|
||||
res: { type: Object, default: () => ({}) },
|
||||
},
|
||||
data() {
|
||||
return { articleList: [] };
|
||||
},
|
||||
computed: {
|
||||
first() {
|
||||
return (this.res.list && this.res.list[0]) || {};
|
||||
},
|
||||
title() {
|
||||
return this.first.title || "";
|
||||
},
|
||||
count() {
|
||||
return this.first.count || 3;
|
||||
},
|
||||
categoryId() {
|
||||
return this.first.categoryId || "";
|
||||
},
|
||||
// 面板保存过样式字段才启用自定义样式,老数据保持原样
|
||||
styled() {
|
||||
return this.res && this.res.paddingTop !== undefined;
|
||||
},
|
||||
nameStyle() {
|
||||
return this.styled && this.res.titleColor
|
||||
? { color: this.res.titleColor }
|
||||
: {};
|
||||
},
|
||||
wrapperStyle() {
|
||||
if (!this.styled) {
|
||||
return {
|
||||
background: this.res.bgColor || "#FFFFFF",
|
||||
padding: `10px ${this.res.paddingX || 0}px`,
|
||||
marginBottom: `${this.res.pageGap !== undefined ? this.res.pageGap : 0}px`,
|
||||
borderRadius: `${this.res.borderRadius || 0}px`,
|
||||
overflow: "hidden",
|
||||
};
|
||||
}
|
||||
const res = this.res;
|
||||
const bg =
|
||||
!res.bgColor2 || res.bgColor1 === res.bgColor2
|
||||
? res.bgColor1
|
||||
: `linear-gradient(180deg, ${res.bgColor1}, ${res.bgColor2})`;
|
||||
const px = (v) => `${(Number(v) || 0) / 2}px`;
|
||||
return {
|
||||
background: bg,
|
||||
padding: `${px(res.paddingTop)} ${px(res.paddingX)} ${px(res.paddingBottom)}`,
|
||||
marginBottom: px(res.pageGap),
|
||||
borderRadius: px(res.borderRadius),
|
||||
overflow: Number(res.borderRadius) > 0 ? "hidden" : "visible",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
rowGap: px(res.contentGap),
|
||||
};
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
categoryId() {
|
||||
this.loadArticles();
|
||||
},
|
||||
count() {
|
||||
this.loadArticles();
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.loadArticles();
|
||||
},
|
||||
methods: {
|
||||
loadArticles() {
|
||||
const params = { pageNumber: 1, pageSize: this.count };
|
||||
if (this.categoryId) params.categoryId = this.categoryId;
|
||||
getArticle(params)
|
||||
.then((res) => {
|
||||
const records = res && res.result && res.result.records;
|
||||
this.articleList = Array.isArray(records)
|
||||
? records.slice(0, this.count)
|
||||
: [];
|
||||
})
|
||||
.catch(() => {
|
||||
this.articleList = [];
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.article-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
padding: 4px 12px 10px;
|
||||
}
|
||||
.article-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
border-top: 1px solid #f5f5f5;
|
||||
}
|
||||
.tpl-article.styled .article-item {
|
||||
border-top: none;
|
||||
padding: 0;
|
||||
}
|
||||
.article-text {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.article-name {
|
||||
font-size: 13px;
|
||||
color: #333;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.article-time {
|
||||
font-size: 11px;
|
||||
color: #999;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.article-thumb {
|
||||
width: 56px;
|
||||
height: 42px;
|
||||
background: #f0f0f0;
|
||||
border-radius: 4px;
|
||||
margin-left: 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -1,17 +1,151 @@
|
||||
<template>
|
||||
<div class="layout">
|
||||
<el-carousel class="carousel" v-if="res" autoplay :interval="5000" height="150px">
|
||||
<el-carousel-item v-for="(item, index) in res.list" :key="index">
|
||||
<div>
|
||||
<img alt="轮播图" class="image-mode" :src="item.img" />
|
||||
<div class="layout" :style="wrapperStyle">
|
||||
<div class="banner-box" :style="cardMode ? {} : boxStyle">
|
||||
<el-carousel
|
||||
class="carousel"
|
||||
v-if="res && !cardMode"
|
||||
autoplay
|
||||
:interval="5000"
|
||||
height="150px"
|
||||
:indicator-position="customIndicator ? 'none' : ''"
|
||||
@change="onChange"
|
||||
>
|
||||
<el-carousel-item v-for="(item, index) in res.list" :key="index">
|
||||
<div class="slide-wrap">
|
||||
<img alt="轮播图" class="image-mode" :style="boxStyle" :src="item.img" />
|
||||
</div>
|
||||
</el-carousel-item>
|
||||
</el-carousel>
|
||||
<!-- 卡片风格:当前图 82% 居中放大,两侧 9% 露出并缩小,循环切换 -->
|
||||
<div
|
||||
v-if="res && cardMode"
|
||||
ref="cardPreview"
|
||||
class="card-preview"
|
||||
@mouseenter="stopAuto"
|
||||
@mouseleave="startAuto"
|
||||
>
|
||||
<div
|
||||
class="card-track"
|
||||
:style="{ transform: `translateX(${trackOffset}px)` }"
|
||||
>
|
||||
<div
|
||||
class="card-slide"
|
||||
:class="{ active: index === current }"
|
||||
v-for="(item, index) in res.list"
|
||||
:key="index"
|
||||
>
|
||||
<img alt="轮播图" class="card-img" :style="boxStyle" :src="item.img" />
|
||||
</div>
|
||||
</div>
|
||||
</el-carousel-item>
|
||||
</el-carousel>
|
||||
</div>
|
||||
<div
|
||||
v-if="customIndicator && res.indicatorStyle !== 'none' && res.list.length > 1"
|
||||
class="indicator"
|
||||
:class="'pos-' + (res.indicatorPosition || 'center')"
|
||||
>
|
||||
<div
|
||||
v-for="(item, i) in res.list"
|
||||
:key="i"
|
||||
class="dot"
|
||||
:class="[res.indicatorStyle || 'round', { active: i === current }]"
|
||||
:style="i === current ? { background: indicatorColor } : {}"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { ref, computed, watch, onBeforeUnmount } from "vue";
|
||||
|
||||
defineOptions({ title: "导航栏" });
|
||||
defineProps(["res"]);
|
||||
const props = defineProps(["res"]);
|
||||
|
||||
const current = ref(0);
|
||||
const previewWidth = ref(0);
|
||||
const cardPreview = ref(null);
|
||||
let timer = null;
|
||||
|
||||
// 配置过样式字段才启用自定义样式/指示器,老数据保持原样
|
||||
const customIndicator = computed(
|
||||
() => props.res && props.res.indicatorStyle !== undefined
|
||||
);
|
||||
const cardMode = computed(() => props.res && props.res.swiperStyle === "card");
|
||||
const indicatorColor = computed(() =>
|
||||
props.res.indicatorColorMode === "custom" && props.res.indicatorColor
|
||||
? props.res.indicatorColor
|
||||
: "#409eff"
|
||||
);
|
||||
// 卡片宽 82%,两侧各露出 9%
|
||||
const trackOffset = computed(
|
||||
() => previewWidth.value * 0.09 - current.value * previewWidth.value * 0.82
|
||||
);
|
||||
const wrapperStyle = computed(() => {
|
||||
const res = props.res || {};
|
||||
if (res.paddingTop === undefined) return {};
|
||||
const bg =
|
||||
!res.bgColor2 || res.bgColor1 === res.bgColor2
|
||||
? res.bgColor1
|
||||
: `linear-gradient(180deg, ${res.bgColor1}, ${res.bgColor2})`;
|
||||
// 画布 375px ≈ 750rpx,预览按一半像素呈现
|
||||
const px = (v) => `${(Number(v) || 0) / 2}px`;
|
||||
return {
|
||||
padding: `${px(res.paddingTop)} ${px(res.paddingX)} ${px(res.paddingBottom)}`,
|
||||
background: bg,
|
||||
borderRadius: px(res.borderRadius),
|
||||
marginBottom: px(res.pageGap),
|
||||
};
|
||||
});
|
||||
const boxStyle = computed(() => {
|
||||
const res = props.res || {};
|
||||
if (res.imgRadius === undefined) return {};
|
||||
return { borderRadius: `${(Number(res.imgRadius) || 0) / 2}px` };
|
||||
});
|
||||
|
||||
function onChange(index) {
|
||||
current.value = index;
|
||||
}
|
||||
|
||||
function measure() {
|
||||
if (cardPreview.value) {
|
||||
previewWidth.value = cardPreview.value.offsetWidth;
|
||||
}
|
||||
}
|
||||
|
||||
function startAuto() {
|
||||
stopAuto();
|
||||
if (!cardMode.value || !props.res.list || props.res.list.length < 2) return;
|
||||
timer = setInterval(() => {
|
||||
current.value = (current.value + 1) % props.res.list.length;
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
function stopAuto() {
|
||||
if (timer) {
|
||||
clearInterval(timer);
|
||||
timer = null;
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
cardMode,
|
||||
(val) => {
|
||||
stopAuto();
|
||||
if (val) {
|
||||
measure();
|
||||
startAuto();
|
||||
}
|
||||
},
|
||||
{ immediate: true, flush: "post" }
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.res.list && props.res.list.length,
|
||||
(len) => {
|
||||
if (current.value >= len) current.value = 0;
|
||||
}
|
||||
);
|
||||
|
||||
onBeforeUnmount(stopAuto);
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import "./tpl.scss";
|
||||
@@ -19,4 +153,79 @@ defineProps(["res"]);
|
||||
.image-mode {
|
||||
height: 150px;
|
||||
}
|
||||
</style>
|
||||
.banner-box {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.slide-wrap {
|
||||
height: 100%;
|
||||
padding: 0 3px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.image-mode {
|
||||
width: 100%;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
}
|
||||
.card-preview {
|
||||
overflow: hidden;
|
||||
height: 150px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.card-track {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
transition: transform 0.5s ease;
|
||||
width: 100%;
|
||||
}
|
||||
.card-slide {
|
||||
flex: 0 0 82%;
|
||||
padding: 0 3px;
|
||||
box-sizing: border-box;
|
||||
transform: scale(0.9);
|
||||
transition: transform 0.4s ease;
|
||||
&.active {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
.card-img {
|
||||
width: 100%;
|
||||
height: 150px;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
}
|
||||
.indicator {
|
||||
position: absolute;
|
||||
bottom: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
z-index: 2;
|
||||
&.pos-left {
|
||||
left: 12px;
|
||||
}
|
||||
&.pos-center {
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
&.pos-right {
|
||||
right: 12px;
|
||||
}
|
||||
.dot {
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
transition: background 0.3s ease;
|
||||
&.round {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
margin: 0 3px;
|
||||
}
|
||||
&.line {
|
||||
width: 12px;
|
||||
height: 3px;
|
||||
border-radius: 2px;
|
||||
margin: 0 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
35
seller/src/views/shop/wap/template/tpl_blank.vue
Normal file
@@ -0,0 +1,35 @@
|
||||
<template>
|
||||
<div
|
||||
class="tpl-blank"
|
||||
:style="{
|
||||
background: blankBg,
|
||||
height: px(res.height ?? 10),
|
||||
margin: `0 ${px(res.paddingX ?? 0)} ${px(res.pageGap ?? 0)}`,
|
||||
}"
|
||||
></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
const px = (v) => `${(Number(v) || 0) / 2}px`;
|
||||
|
||||
export default {
|
||||
name: "tpl_blank",
|
||||
props: {
|
||||
res: { type: Object, default: () => ({}) },
|
||||
},
|
||||
methods: {
|
||||
px,
|
||||
},
|
||||
computed: {
|
||||
blankBg() {
|
||||
const res = this.res || {};
|
||||
if (res.bgColor1 !== undefined) {
|
||||
return !res.bgColor2 || res.bgColor1 === res.bgColor2
|
||||
? res.bgColor1
|
||||
: `linear-gradient(180deg, ${res.bgColor1}, ${res.bgColor2})`;
|
||||
}
|
||||
return res.bgColor || "#f5f5f5";
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
441
seller/src/views/shop/wap/template/tpl_coupon.vue
Normal file
@@ -0,0 +1,441 @@
|
||||
<template>
|
||||
<div class="tpl-coupon" :class="{ styled: styled }" :style="wrapperStyle">
|
||||
<div class="coupon-title" v-if="title">{{ title }}</div>
|
||||
|
||||
<!-- 老数据保持原样 -->
|
||||
<div v-if="!styled" class="coupon-list">
|
||||
<div class="coupon-item" v-for="i in 2" :key="i">
|
||||
<div class="coupon-left">
|
||||
<div class="coupon-amount">
|
||||
<span class="coupon-symbol">¥</span>10
|
||||
</div>
|
||||
<div class="coupon-threshold">满100可用</div>
|
||||
</div>
|
||||
<div class="coupon-right">
|
||||
<div class="coupon-name">优惠券名称</div>
|
||||
<div class="coupon-btn">立即领取</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 样式一:红色大卡横滑,右侧竖排领取 -->
|
||||
<div v-else-if="layoutStyle === '1'" class="cp-scroll" :style="scrollGap">
|
||||
<div class="cp1-item" v-for="(item, i) in displayData" :key="i">
|
||||
<div class="cp1-left">
|
||||
<div class="cp1-amount">
|
||||
<template v-if="item.couponType == 'DISCOUNT'">
|
||||
<span class="cp1-value">{{ item.couponDiscount }}</span
|
||||
><span class="cp1-symbol">折</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="cp1-symbol">¥</span
|
||||
><span class="cp1-value">{{ item.price }}</span>
|
||||
</template>
|
||||
</div>
|
||||
<div class="cp1-threshold">{{ thresholdText(item) }}</div>
|
||||
</div>
|
||||
<div class="cp1-right">
|
||||
<span>领</span><span>取</span>
|
||||
</div>
|
||||
<i class="cp-notch cp-notch-t"></i>
|
||||
<i class="cp-notch cp-notch-b"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 样式二:白底小卡,底部去领取按钮 -->
|
||||
<div v-else-if="layoutStyle === '2'" class="cp-scroll" :style="scrollGap">
|
||||
<div class="cp2-item" v-for="(item, i) in displayData" :key="i">
|
||||
<div class="cp2-top">
|
||||
<div class="cp2-amount">
|
||||
<template v-if="item.couponType == 'DISCOUNT'">
|
||||
<span class="cp2-value">{{ item.couponDiscount }}</span
|
||||
><span class="cp2-symbol">折</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="cp2-symbol">¥</span
|
||||
><span class="cp2-value">{{ item.price }}</span>
|
||||
</template>
|
||||
</div>
|
||||
<div class="cp2-threshold">{{ thresholdText(item) }}</div>
|
||||
</div>
|
||||
<div class="cp2-btn">去领取</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 样式三:浅粉虚线卡,右侧竖排立即领取 -->
|
||||
<div v-else-if="layoutStyle === '3'" class="cp-scroll" :style="scrollGap">
|
||||
<div class="cp3-item" v-for="(item, i) in displayData" :key="i">
|
||||
<div class="cp3-left">
|
||||
<div class="cp3-amount">
|
||||
<template v-if="item.couponType == 'DISCOUNT'">
|
||||
<span class="cp3-value">{{ item.couponDiscount }}</span
|
||||
><span class="cp3-symbol">折</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="cp3-symbol">¥</span
|
||||
><span class="cp3-value">{{ item.price }}</span>
|
||||
</template>
|
||||
</div>
|
||||
<div class="cp3-threshold">{{ thresholdText(item) }}</div>
|
||||
</div>
|
||||
<div class="cp3-right">
|
||||
<span>立</span><span>即</span><span>领</span><span>取</span>
|
||||
</div>
|
||||
<i class="cp-notch cp-notch-t"></i>
|
||||
<i class="cp-notch cp-notch-b"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 样式四:浅粉渐变 + 右侧渐变竖条领取 -->
|
||||
<div v-else class="cp-scroll" :style="scrollGap">
|
||||
<div class="cp4-item" v-for="(item, i) in displayData" :key="i">
|
||||
<div class="cp4-left">
|
||||
<div class="cp4-amount">
|
||||
<template v-if="item.couponType == 'DISCOUNT'">
|
||||
<span class="cp4-value">{{ item.couponDiscount }}</span
|
||||
><span class="cp4-symbol">折</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="cp4-symbol">¥</span
|
||||
><span class="cp4-value">{{ item.price }}</span>
|
||||
</template>
|
||||
</div>
|
||||
<div class="cp4-threshold">{{ thresholdText(item) }}</div>
|
||||
</div>
|
||||
<div class="cp4-right">
|
||||
<span>领</span><span>取</span>
|
||||
</div>
|
||||
<i class="cp-notch cp-notch-t"></i>
|
||||
<i class="cp-notch cp-notch-b"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "tpl_coupon",
|
||||
props: {
|
||||
res: { type: Object, default: () => ({}) },
|
||||
},
|
||||
computed: {
|
||||
firstItem() {
|
||||
return (this.res.list && this.res.list[0]) || {};
|
||||
},
|
||||
title() {
|
||||
return this.firstItem.title || "";
|
||||
},
|
||||
layoutStyle() {
|
||||
return (this.res && this.res.layoutStyle) || "1";
|
||||
},
|
||||
displayData() {
|
||||
const data = this.firstItem.data || [];
|
||||
return data.slice(0, this.firstItem.count || 5);
|
||||
},
|
||||
// 面板保存过样式字段才启用自定义样式,老数据保持原样
|
||||
styled() {
|
||||
return this.res && this.res.paddingTop !== undefined;
|
||||
},
|
||||
scrollGap() {
|
||||
if (!this.styled) return {};
|
||||
return { columnGap: `${(Number(this.res.contentGap) || 0) / 2}px` };
|
||||
},
|
||||
wrapperStyle() {
|
||||
if (!this.styled) {
|
||||
return {
|
||||
background: this.res.bgColor || "#FFFFFF",
|
||||
padding: `10px ${this.res.paddingX || 0}px`,
|
||||
marginBottom: `${this.res.pageGap !== undefined ? this.res.pageGap : 0}px`,
|
||||
borderRadius: `${this.res.borderRadius || 0}px`,
|
||||
overflow: "hidden",
|
||||
};
|
||||
}
|
||||
const res = this.res;
|
||||
const bg =
|
||||
!res.bgColor2 || res.bgColor1 === res.bgColor2
|
||||
? res.bgColor1
|
||||
: `linear-gradient(180deg, ${res.bgColor1}, ${res.bgColor2})`;
|
||||
const px = (v) => `${(Number(v) || 0) / 2}px`;
|
||||
return {
|
||||
background: bg,
|
||||
padding: `${px(res.paddingTop)} ${px(res.paddingX)} ${px(res.paddingBottom)}`,
|
||||
marginBottom: px(res.pageGap),
|
||||
borderRadius: px(res.borderRadius),
|
||||
overflow: "hidden",
|
||||
};
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
thresholdText(item) {
|
||||
const t = Number(item.consumeThreshold);
|
||||
return t > 0 ? `满${item.consumeThreshold}元可用` : "无门槛使用";
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.tpl-coupon {
|
||||
font-size: 13px;
|
||||
}
|
||||
.coupon-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
padding: 4px 10px 10px;
|
||||
}
|
||||
.coupon-item {
|
||||
display: flex;
|
||||
margin: 0 10px 10px;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(90deg, #ffece8, #fff5f4);
|
||||
}
|
||||
.coupon-left {
|
||||
width: 90px;
|
||||
padding: 12px 0;
|
||||
text-align: center;
|
||||
color: #e1251b;
|
||||
border-right: 1px dashed #f0c0bb;
|
||||
}
|
||||
.coupon-amount {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.coupon-symbol {
|
||||
font-size: 12px;
|
||||
}
|
||||
.coupon-threshold {
|
||||
font-size: 11px;
|
||||
}
|
||||
.coupon-right {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 12px;
|
||||
}
|
||||
.coupon-name {
|
||||
color: #333;
|
||||
}
|
||||
.coupon-btn {
|
||||
background: #e1251b;
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
padding: 4px 10px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
.tpl-coupon.styled .coupon-item {
|
||||
margin: 0;
|
||||
}
|
||||
.tpl-coupon.styled .coupon-list {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
/* 参考 CRMEB 优惠券组件 */
|
||||
.tpl-coupon.styled .coupon-title {
|
||||
padding: 0 0 8px;
|
||||
}
|
||||
.cp-scroll {
|
||||
display: flex;
|
||||
column-gap: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.cp-notch {
|
||||
position: absolute;
|
||||
right: 30px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: #fff;
|
||||
}
|
||||
.cp-notch-t {
|
||||
top: -4px;
|
||||
}
|
||||
.cp-notch-b {
|
||||
bottom: -4px;
|
||||
}
|
||||
|
||||
/* 样式一:红色大卡 */
|
||||
.cp1-item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
width: 132px;
|
||||
height: 66px;
|
||||
border-radius: 6px;
|
||||
background: linear-gradient(135deg, #ff5a36, #e1251b);
|
||||
color: #fff;
|
||||
}
|
||||
.cp1-left {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding-left: 12px;
|
||||
}
|
||||
.cp1-value {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.cp1-symbol {
|
||||
font-size: 11px;
|
||||
}
|
||||
.cp1-threshold {
|
||||
font-size: 10px;
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
margin-top: 2px;
|
||||
}
|
||||
.cp1-right {
|
||||
width: 30px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
border-left: 1px dashed rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
.cp1-item .cp-notch {
|
||||
right: 26px;
|
||||
}
|
||||
|
||||
/* 样式二:白底小卡 */
|
||||
.cp2-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-shrink: 0;
|
||||
width: 74px;
|
||||
border-radius: 6px;
|
||||
background: #fff;
|
||||
border-left: 2px solid #f5483a;
|
||||
border-right: 2px solid #f5483a;
|
||||
overflow: hidden;
|
||||
}
|
||||
.cp2-top {
|
||||
padding: 8px 0 6px;
|
||||
text-align: center;
|
||||
}
|
||||
.cp2-value {
|
||||
font-size: 17px;
|
||||
font-weight: 700;
|
||||
color: #e1251b;
|
||||
}
|
||||
.cp2-symbol {
|
||||
font-size: 10px;
|
||||
color: #e1251b;
|
||||
}
|
||||
.cp2-threshold {
|
||||
font-size: 9px;
|
||||
color: #999;
|
||||
margin-top: 1px;
|
||||
}
|
||||
.cp2-btn {
|
||||
background: linear-gradient(90deg, #ff6a3d, #e1251b);
|
||||
color: #fff;
|
||||
font-size: 10px;
|
||||
text-align: center;
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
/* 样式三:浅粉虚线卡 */
|
||||
.cp3-item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
width: 138px;
|
||||
height: 60px;
|
||||
border-radius: 6px;
|
||||
background: #fdf1ec;
|
||||
border: 1px dashed #f5483a;
|
||||
}
|
||||
.cp3-left {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding-left: 12px;
|
||||
}
|
||||
.cp3-value {
|
||||
font-size: 19px;
|
||||
font-weight: 700;
|
||||
color: #e1251b;
|
||||
}
|
||||
.cp3-symbol {
|
||||
font-size: 10px;
|
||||
color: #e1251b;
|
||||
}
|
||||
.cp3-threshold {
|
||||
font-size: 10px;
|
||||
color: #e1251b;
|
||||
margin-top: 1px;
|
||||
}
|
||||
.cp3-right {
|
||||
width: 32px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 11px;
|
||||
color: #e1251b;
|
||||
border-left: 1px dashed #f5483a;
|
||||
}
|
||||
.cp3-item .cp-notch {
|
||||
right: 28px;
|
||||
}
|
||||
|
||||
/* 样式四:浅粉渐变 + 渐变竖条 */
|
||||
.cp4-item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
width: 116px;
|
||||
height: 54px;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.cp4-left {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding-left: 10px;
|
||||
background: linear-gradient(135deg, #fff3ec, #ffe0d3);
|
||||
}
|
||||
.cp4-value {
|
||||
font-size: 17px;
|
||||
font-weight: 700;
|
||||
color: #e1251b;
|
||||
}
|
||||
.cp4-symbol {
|
||||
font-size: 10px;
|
||||
color: #e1251b;
|
||||
}
|
||||
.cp4-threshold {
|
||||
font-size: 9px;
|
||||
color: #e1251b;
|
||||
margin-top: 1px;
|
||||
}
|
||||
.cp4-right {
|
||||
width: 26px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 11px;
|
||||
color: #fff;
|
||||
background: linear-gradient(180deg, #ff6a3d, #e1251b);
|
||||
}
|
||||
.cp4-item .cp-notch {
|
||||
right: 22px;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
.cp4-item .cp-notch-t {
|
||||
top: -3px;
|
||||
}
|
||||
.cp4-item .cp-notch-b {
|
||||
bottom: -3px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,17 +1,24 @@
|
||||
|
||||
<template>
|
||||
<div class="layout">
|
||||
<img class="image-mode" :src="res.list[0].img" alt="">
|
||||
<img class="image-mode" :src="res.list[1].img" alt="">
|
||||
<img class="image-mode" :src="res.list[2].img" alt="">
|
||||
<img class="image-mode" :src="res.list[3].img" alt="">
|
||||
<img class="image-mode" :src="res.list[4].img" alt="">
|
||||
<div class="layout" :style="[wrapperStyle, gapStyle]">
|
||||
<img class="image-mode" :style="imgStyle" :src="res.list[0].img" alt="">
|
||||
<img class="image-mode" :style="imgStyle" :src="res.list[1].img" alt="">
|
||||
<img class="image-mode" :style="imgStyle" :src="res.list[2].img" alt="">
|
||||
<img class="image-mode" :style="imgStyle" :src="res.list[3].img" alt="">
|
||||
<img class="image-mode" :style="imgStyle" :src="res.list[4].img" alt="">
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from "vue";
|
||||
import { cubeWrapperStyle, cubeImgStyle, cubeGapStyle } from "./cubeStyle";
|
||||
|
||||
defineOptions({ title: "五列单行图片模块" });
|
||||
defineProps(["res"]);
|
||||
const props = defineProps(["res"]);
|
||||
|
||||
const wrapperStyle = computed(() => cubeWrapperStyle(props.res));
|
||||
const imgStyle = computed(() => cubeImgStyle(props.res));
|
||||
const gapStyle = computed(() => cubeGapStyle(props.res));
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import "./tpl.scss";
|
||||
@@ -23,6 +30,7 @@ defineProps(["res"]);
|
||||
background-size: cover;
|
||||
}
|
||||
img {
|
||||
width: 67px !important;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
|
||||
<template>
|
||||
<div class="layout">
|
||||
<img class="image-mode" :src="res.list[0].img" alt="">
|
||||
<img class="image-mode" :src="res.list[1].img" alt="">
|
||||
<img class="image-mode" :src="res.list[2].img" alt="">
|
||||
<img class="image-mode" :src="res.list[3].img" alt="">
|
||||
<div class="layout" :style="[wrapperStyle, gapStyle]">
|
||||
<img class="image-mode" :style="imgStyle" :src="res.list[0].img" alt="">
|
||||
<img class="image-mode" :style="imgStyle" :src="res.list[1].img" alt="">
|
||||
<img class="image-mode" :style="imgStyle" :src="res.list[2].img" alt="">
|
||||
<img class="image-mode" :style="imgStyle" :src="res.list[3].img" alt="">
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineOptions({
|
||||
title: "四列单行图片模块",
|
||||
});
|
||||
import { computed } from "vue";
|
||||
import { cubeWrapperStyle, cubeImgStyle, cubeGapStyle } from "./cubeStyle";
|
||||
|
||||
defineProps(["res"]);
|
||||
defineOptions({ title: "四列单行图片模块" });
|
||||
const props = defineProps(["res"]);
|
||||
|
||||
const wrapperStyle = computed(() => cubeWrapperStyle(props.res));
|
||||
const imgStyle = computed(() => cubeImgStyle(props.res));
|
||||
const gapStyle = computed(() => cubeGapStyle(props.res));
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import "./tpl.scss";
|
||||
@@ -28,6 +32,7 @@ defineProps(["res"]);
|
||||
}
|
||||
|
||||
img{
|
||||
width: 84px !important;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,12 +1,32 @@
|
||||
<template>
|
||||
<div class="layout">
|
||||
<div class="flex-one">
|
||||
<div
|
||||
class="layout"
|
||||
:style="{
|
||||
background: wrapperBg,
|
||||
padding: `${px(res.paddingTop ?? 0)} ${px(res.paddingX ?? 0)} ${px(res.paddingBottom ?? 0)}`,
|
||||
marginBottom: px(res.pageGap ?? 0),
|
||||
borderRadius: px(res.borderRadius ?? 0),
|
||||
overflow: (res.borderRadius ?? 0) > 0 ? 'hidden' : 'visible',
|
||||
}"
|
||||
>
|
||||
<div class="flex-one" :style="{ borderRadius: px(res.imgRadius ?? 0) }">
|
||||
<img :src="res.list[0].img" alt="">
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
defineProps(['res']);
|
||||
import { computed } from 'vue'
|
||||
const props = defineProps(['res'])
|
||||
const px = (v) => `${(Number(v) || 0) / 2}px`
|
||||
const wrapperBg = computed(() => {
|
||||
const r = props.res || {}
|
||||
if (r.bgColor1 !== undefined) {
|
||||
return !r.bgColor2 || r.bgColor1 === r.bgColor2
|
||||
? r.bgColor1
|
||||
: `linear-gradient(180deg, ${r.bgColor1}, ${r.bgColor2})`
|
||||
}
|
||||
return r.bgColor ?? 'transparent'
|
||||
})
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import './tpl.scss';
|
||||
@@ -19,4 +39,4 @@ defineProps(['res']);
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
|
||||
<template>
|
||||
<div class="layout">
|
||||
<img class="image-mode" :src="res.list[0].img" alt="">
|
||||
<img class="image-mode" :src="res.list[1].img" alt="">
|
||||
<img class="image-mode" :src="res.list[2].img" alt="">
|
||||
<div class="layout" :style="[wrapperStyle, gapStyle]">
|
||||
<img class="image-mode" :style="imgStyle" :src="res.list[0].img" alt="">
|
||||
<img class="image-mode" :style="imgStyle" :src="res.list[1].img" alt="">
|
||||
<img class="image-mode" :style="imgStyle" :src="res.list[2].img" alt="">
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineOptions({
|
||||
title: "三列单行图片模块",
|
||||
});
|
||||
import { computed } from "vue";
|
||||
import { cubeWrapperStyle, cubeImgStyle, cubeGapStyle } from "./cubeStyle";
|
||||
|
||||
defineProps(["res"]);
|
||||
defineOptions({ title: "三列单行图片模块" });
|
||||
const props = defineProps(["res"]);
|
||||
|
||||
const wrapperStyle = computed(() => cubeWrapperStyle(props.res));
|
||||
const imgStyle = computed(() => cubeImgStyle(props.res));
|
||||
const gapStyle = computed(() => cubeGapStyle(props.res));
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import "./tpl.scss";
|
||||
@@ -25,6 +29,7 @@ defineProps(["res"]);
|
||||
background-size: cover;
|
||||
}
|
||||
img{
|
||||
width: 111px !important;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,18 +1,25 @@
|
||||
<template>
|
||||
<div class="layout">
|
||||
<div class="flex-two">
|
||||
<div class="flex-item">
|
||||
<div class="layout" :style="wrapperStyle">
|
||||
<div class="flex-two" :style="gapStyle">
|
||||
<div class="flex-item" :style="imgStyle">
|
||||
<img :src="res.list[0].img" alt />
|
||||
</div>
|
||||
<div class="flex-item">
|
||||
<div class="flex-item" :style="imgStyle">
|
||||
<img :src="res.list[1].img" alt />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { computed } from "vue";
|
||||
import { cubeWrapperStyle, cubeImgStyle, cubeGapStyle } from "./cubeStyle";
|
||||
|
||||
defineOptions({ title: "两张横图" });
|
||||
defineProps(["res"]);
|
||||
const props = defineProps(["res"]);
|
||||
|
||||
const wrapperStyle = computed(() => cubeWrapperStyle(props.res));
|
||||
const imgStyle = computed(() => cubeImgStyle(props.res));
|
||||
const gapStyle = computed(() => cubeGapStyle(props.res));
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import "./tpl.scss";
|
||||
@@ -23,10 +30,11 @@ defineProps(["res"]);
|
||||
overflow: hidden;
|
||||
}
|
||||
.flex-item {
|
||||
width: 50%;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
> img {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,97 +1,484 @@
|
||||
<template>
|
||||
<div v-if="goodsBlock" class="layout">
|
||||
<div class="goods-cell-title">
|
||||
<div
|
||||
class="goods-item-title"
|
||||
:class="{ selected: selected.index == index }"
|
||||
@click="handleClickTitle(title, index)"
|
||||
v-for="(title, index) in goodsBlock.titleWay"
|
||||
:key="index"
|
||||
>
|
||||
<h4>{{ title.title }}</h4>
|
||||
<div>{{ title.desc }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="goods-list">
|
||||
<template v-for="(item, item_index) in goodsBlock.listWay" :key="item_index">
|
||||
<div v-if="isGoodsVisible(item)" class="goods-item">
|
||||
<div class="goods-img">
|
||||
<el-icon
|
||||
:size="20"
|
||||
color="#e1251b"
|
||||
@click="closeGoods(item, item_index)"
|
||||
class="goods-icon"
|
||||
>
|
||||
<CircleClose />
|
||||
</el-icon>
|
||||
<img :src="item.img" alt />
|
||||
<div class="tpl-goods" :class="{ 'legacy-mode': !isNew }" :style="wrapperStyle">
|
||||
<!-- 新版商品列表 -->
|
||||
<template v-if="isNew">
|
||||
<div v-if="displayGoods.length" class="g-list" :class="'mode-' + layout" :style="listStyle">
|
||||
<div class="g-item" v-for="(g, i) in displayGoods" :key="i" :style="itemStyle">
|
||||
<div class="g-imgbox">
|
||||
<img :src="g.img" alt="" />
|
||||
</div>
|
||||
<div class="goods-desc">
|
||||
<div class="goods-title">
|
||||
{{ item.title }}
|
||||
<div class="g-info">
|
||||
<div v-if="showName" class="g-name" :style="nameStyle">{{ g.name }}</div>
|
||||
<div v-if="showPrice" class="g-price" :style="priceStyle">
|
||||
¥{{ $filters.unitPrice(g.price) }}
|
||||
</div>
|
||||
<div class="goods-bottom">
|
||||
<div class="goods-price">¥{{ $filters.unitPrice(item.price) }}</div>
|
||||
<div class="g-bottom">
|
||||
<span
|
||||
v-if="showSales && g.sales != null"
|
||||
class="g-sales"
|
||||
:style="salesStyle"
|
||||
>已售 {{ g.sales }} 件</span
|
||||
>
|
||||
<span
|
||||
v-if="cartShow"
|
||||
class="g-cart"
|
||||
:class="'cart-' + cartStyle"
|
||||
:style="cartStyleObj"
|
||||
>
|
||||
<template v-if="cartStyle === 'text'">加购</template>
|
||||
<template v-else-if="cartStyle === 'plus'">+</template>
|
||||
<svg v-else viewBox="0 0 24 24" width="14" height="14" fill="currentColor">
|
||||
<path
|
||||
d="M7 4h-2l-1 2h2l2.4 9.6a2 2 0 0 0 1.9 1.4h7.9a2 2 0 0 0 1.9-1.5l1.6-6.5h-12l-.8-3h11.5l.5-2h-13.4zM10 20a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3zm8 0a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="g-empty">暂无商品</div>
|
||||
</template>
|
||||
|
||||
<!-- 旧版:分类标签 + 绑定商品 -->
|
||||
<template v-else-if="goodsBlock">
|
||||
<div class="goods-cell-title">
|
||||
<div
|
||||
class="goods-item-title"
|
||||
:class="{ selected: selected.index == index }"
|
||||
@click="handleClickTitle(title, index)"
|
||||
v-for="(title, index) in goodsBlock.titleWay"
|
||||
:key="index"
|
||||
>
|
||||
<h4>{{ title.title }}</h4>
|
||||
<div>{{ title.desc }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="goods-list">
|
||||
<template v-for="(item, item_index) in goodsBlock.listWay" :key="item_index">
|
||||
<div v-if="isGoodsVisible(item)" class="goods-item">
|
||||
<div class="goods-img">
|
||||
<img :src="item.img" alt />
|
||||
</div>
|
||||
<div class="goods-desc">
|
||||
<div class="goods-title">
|
||||
{{ item.title }}
|
||||
</div>
|
||||
<div class="goods-bottom">
|
||||
<div class="goods-price">¥{{ $filters.unitPrice(item.price) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { computed, reactive, watch } from "vue";
|
||||
import { CircleClose } from "@element-plus/icons-vue";
|
||||
|
||||
const props = defineProps(["res"]);
|
||||
<script>
|
||||
import { getGoodsListData } from "@/api/goods";
|
||||
|
||||
const selected = reactive({
|
||||
index: 0,
|
||||
val: "",
|
||||
});
|
||||
|
||||
const goodsBlock = computed(() => props.res?.list?.[0] || null);
|
||||
|
||||
watch(
|
||||
goodsBlock,
|
||||
(val) => {
|
||||
if (!val) {
|
||||
return;
|
||||
}
|
||||
const firstGoods = (val.listWay || []).find(Boolean);
|
||||
if (firstGoods) {
|
||||
selected.val = firstGoods.type;
|
||||
} else if (val.titleWay?.[0]?.title) {
|
||||
selected.val = val.titleWay[0].title;
|
||||
export default {
|
||||
name: "tpl_goods",
|
||||
props: {
|
||||
res: { type: Object, default: () => ({}) },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selected: { index: 0, val: "" },
|
||||
goodsData: [],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
first() {
|
||||
return (this.res.list && this.res.list[0]) || {};
|
||||
},
|
||||
// 新版标记:面板写入过 layout 字段
|
||||
isNew() {
|
||||
return this.first.layout !== undefined;
|
||||
},
|
||||
layout() {
|
||||
return this.first.layout || "list";
|
||||
},
|
||||
showName() {
|
||||
return this.first.showName !== false;
|
||||
},
|
||||
showPrice() {
|
||||
return this.first.showPrice !== false;
|
||||
},
|
||||
showSales() {
|
||||
return this.first.showSales !== false;
|
||||
},
|
||||
cartShow() {
|
||||
return this.first.cartShow !== false;
|
||||
},
|
||||
cartStyle() {
|
||||
return this.first.cartStyle || "plus";
|
||||
},
|
||||
themePrice() {
|
||||
return "#FF5C58";
|
||||
},
|
||||
priceColor() {
|
||||
return this.res.toneMode === "custom" && this.res.priceColor
|
||||
? this.res.priceColor
|
||||
: this.themePrice;
|
||||
},
|
||||
priceStyle() {
|
||||
return { color: this.priceColor };
|
||||
},
|
||||
nameStyle() {
|
||||
return this.res.titleColor ? { color: this.res.titleColor } : {};
|
||||
},
|
||||
salesStyle() {
|
||||
return this.res.salesColor ? { color: this.res.salesColor } : {};
|
||||
},
|
||||
cartStyleObj() {
|
||||
return { background: this.priceColor };
|
||||
},
|
||||
px() {
|
||||
return (v) => `${(Number(v) || 0) / 2}px`;
|
||||
},
|
||||
wrapperStyle() {
|
||||
const res = this.res;
|
||||
if (!this.isNew) {
|
||||
return {};
|
||||
}
|
||||
const bg =
|
||||
!res.bgColor2 || res.bgColor1 === res.bgColor2
|
||||
? res.bgColor1
|
||||
: `linear-gradient(180deg, ${res.bgColor1}, ${res.bgColor2})`;
|
||||
return {
|
||||
background: bg,
|
||||
padding: `${this.px(res.paddingTop)} ${this.px(res.paddingX)} ${this.px(res.paddingBottom)}`,
|
||||
marginBottom: this.px(res.pageGap),
|
||||
borderRadius: this.px(res.borderRadius),
|
||||
overflow: Number(res.borderRadius) > 0 ? "hidden" : "visible",
|
||||
};
|
||||
},
|
||||
listStyle() {
|
||||
const gap = this.px(this.res.contentGap ?? 10);
|
||||
return { gap, "--g-gap": gap };
|
||||
},
|
||||
itemStyle() {
|
||||
const r = this.px(this.res.contentRadius);
|
||||
return { borderRadius: r, overflow: Number(this.res.contentRadius) > 0 ? "hidden" : "visible" };
|
||||
},
|
||||
displayGoods() {
|
||||
if (this.first.dataMode === "goods") {
|
||||
return (this.first.goodsList || []).map((g) => ({
|
||||
name: g.goodsName,
|
||||
price: g.price,
|
||||
img: g.thumbnail,
|
||||
sales: g.buyCount != null ? g.buyCount : null,
|
||||
}));
|
||||
}
|
||||
return this.goodsData;
|
||||
},
|
||||
goodsBlock() {
|
||||
return this.res && this.res.list && this.res.list[0] ? this.res.list[0] : null;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
"first.dataMode": "loadGoods",
|
||||
"first.categoryId": "loadGoods",
|
||||
"first.brandId": "loadGoods",
|
||||
"first.storeId": "loadGoods",
|
||||
"first.count": "loadGoods",
|
||||
"first.sortType": "loadGoods",
|
||||
},
|
||||
created() {
|
||||
if (this.isNew) {
|
||||
this.loadGoods();
|
||||
} else {
|
||||
const block = this.goodsBlock;
|
||||
if (block) {
|
||||
const firstGoods = (block.listWay || []).find(Boolean);
|
||||
if (firstGoods) {
|
||||
this.selected.val = firstGoods.type;
|
||||
} else if (block.titleWay && block.titleWay[0]) {
|
||||
this.selected.val = block.titleWay[0].title;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
function isGoodsVisible(item) {
|
||||
if (!item) {
|
||||
return false;
|
||||
}
|
||||
if (item.___index != undefined) {
|
||||
return selected.index == item.___index;
|
||||
}
|
||||
return selected.val == item.type;
|
||||
}
|
||||
|
||||
function closeGoods(val, index) {
|
||||
goodsBlock.value.listWay.splice(index, 1);
|
||||
}
|
||||
|
||||
function handleClickTitle(val, index) {
|
||||
selected.index = index;
|
||||
selected.val = val.title;
|
||||
}
|
||||
methods: {
|
||||
loadGoods() {
|
||||
const item = this.first;
|
||||
if (!this.isNew || item.dataMode === "goods") {
|
||||
return;
|
||||
}
|
||||
const params = {
|
||||
pageNumber: 1,
|
||||
pageSize: item.count || 6,
|
||||
goodsStatus: "UPPER",
|
||||
};
|
||||
if (item.dataMode === "category" && item.categoryId) {
|
||||
// category_path 以逗号包裹,前后补逗号避免误匹配
|
||||
params.categoryPath = `,${item.categoryId},`;
|
||||
}
|
||||
if (item.dataMode === "brand" && item.brandId) {
|
||||
params.brandId = item.brandId;
|
||||
}
|
||||
if (item.dataMode === "store" && item.storeId) {
|
||||
params.storeId = item.storeId;
|
||||
}
|
||||
if (item.sortType === "sales") {
|
||||
params.sort = "buyCount";
|
||||
params.order = "desc";
|
||||
} else if (item.sortType === "price") {
|
||||
params.sort = "price";
|
||||
params.order = "asc";
|
||||
}
|
||||
getGoodsListData(params)
|
||||
.then((res) => {
|
||||
const records = (res && res.result && res.result.records) || [];
|
||||
this.goodsData = records.map((g) => ({
|
||||
name: g.goodsName,
|
||||
price: g.price,
|
||||
img: g.thumbnail || g.small || g.original,
|
||||
sales: g.buyCount != null ? g.buyCount : null,
|
||||
}));
|
||||
})
|
||||
.catch(() => {
|
||||
this.goodsData = [];
|
||||
});
|
||||
},
|
||||
isGoodsVisible(item) {
|
||||
if (!item) {
|
||||
return false;
|
||||
}
|
||||
if (item.___index != undefined) {
|
||||
return this.selected.index == item.___index;
|
||||
}
|
||||
return this.selected.val == item.type;
|
||||
},
|
||||
handleClickTitle(val, index) {
|
||||
this.selected.index = index;
|
||||
this.selected.val = val.title;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.layout {
|
||||
padding: 8px 0;
|
||||
background: #e8e8e8;
|
||||
.tpl-goods {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.legacy-mode {
|
||||
background: #e8e8e8;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.g-empty {
|
||||
padding: 30px 0;
|
||||
text-align: center;
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.g-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.g-item {
|
||||
background: #fff;
|
||||
display: flex;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.g-imgbox {
|
||||
flex-shrink: 0;
|
||||
background: #f5f5f5;
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.g-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.g-name {
|
||||
font-size: 13px;
|
||||
color: #282828;
|
||||
line-height: 1.4;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.g-bottom {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.g-price {
|
||||
color: $theme_color;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.g-sales {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: 11px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.g-cart {
|
||||
flex-shrink: 0;
|
||||
margin-left: auto;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
&.cart-plus,
|
||||
&.cart-cart {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
font-size: 15px;
|
||||
line-height: 1;
|
||||
}
|
||||
&.cart-text {
|
||||
border-radius: 12px;
|
||||
padding: 3px 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 卡片:左图右文(参考 CRMEB 详细列表,左侧 112x112 缩略图,右侧标题置顶、价格销量置底) */
|
||||
.mode-card {
|
||||
flex-direction: column;
|
||||
.g-item {
|
||||
flex-direction: row;
|
||||
align-items: stretch;
|
||||
padding: 10px;
|
||||
}
|
||||
.g-imgbox {
|
||||
width: 112px;
|
||||
height: 112px;
|
||||
margin-right: 12px;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.g-info {
|
||||
min-height: 112px;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.g-name {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.g-price {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
margin-top: auto;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.g-bottom {
|
||||
margin-top: 0;
|
||||
}
|
||||
.g-sales {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 列表:两列网格 */
|
||||
.mode-list {
|
||||
.g-item {
|
||||
width: calc((100% - var(--g-gap, 5px)) / 2);
|
||||
flex-direction: column;
|
||||
}
|
||||
.g-imgbox {
|
||||
width: 100%;
|
||||
aspect-ratio: 1;
|
||||
}
|
||||
.g-info {
|
||||
padding: 8px 10px 10px;
|
||||
min-height: 64px;
|
||||
}
|
||||
.g-name {
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 三列 */
|
||||
.mode-three {
|
||||
.g-item {
|
||||
width: calc((100% - var(--g-gap, 5px) * 2) / 3);
|
||||
flex-direction: column;
|
||||
}
|
||||
.g-imgbox {
|
||||
width: 100%;
|
||||
aspect-ratio: 1;
|
||||
}
|
||||
.g-info {
|
||||
padding: 6px;
|
||||
min-height: 48px;
|
||||
}
|
||||
.g-name {
|
||||
-webkit-line-clamp: 1;
|
||||
font-size: 11px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.g-price {
|
||||
font-size: 12px;
|
||||
}
|
||||
.g-sales {
|
||||
font-size: 10px;
|
||||
}
|
||||
.g-cart {
|
||||
&.cart-plus,
|
||||
&.cart-cart {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 大图:一行一个 */
|
||||
.mode-big {
|
||||
flex-direction: column;
|
||||
.g-item {
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
}
|
||||
.g-imgbox {
|
||||
width: 100%;
|
||||
aspect-ratio: 3 / 2;
|
||||
}
|
||||
.g-info {
|
||||
padding: 10px 12px 12px;
|
||||
min-height: 64px;
|
||||
}
|
||||
.g-name {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.g-price {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 旧版样式 */
|
||||
.selected {
|
||||
> h4 {
|
||||
color: #000 !important;
|
||||
@@ -105,7 +492,6 @@ function handleClickTitle(val, index) {
|
||||
padding: 10px;
|
||||
transition: 0.35s;
|
||||
display: flex;
|
||||
// cursor: pointer;
|
||||
> .goods-item-title {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
@@ -156,9 +542,7 @@ function handleClickTitle(val, index) {
|
||||
display: -webkit-box;
|
||||
font-weight: 500;
|
||||
-webkit-box-orient: vertical;
|
||||
|
||||
-webkit-line-clamp: 2;
|
||||
|
||||
overflow: hidden;
|
||||
}
|
||||
> .goods-bottom {
|
||||
@@ -169,9 +553,4 @@ function handleClickTitle(val, index) {
|
||||
}
|
||||
}
|
||||
}
|
||||
.goods-icon {
|
||||
right: 5px;
|
||||
top: 5px;
|
||||
position: absolute;
|
||||
}
|
||||
</style>
|
||||
|
||||
564
seller/src/views/shop/wap/template/tpl_home_comb.vue
Normal file
@@ -0,0 +1,564 @@
|
||||
<template>
|
||||
<div class="tpl-home-comb" :style="wrapperStyle">
|
||||
<!-- 高斯模糊轮播图背景 + 底部渐变遮罩 -->
|
||||
<div class="comb-bg">
|
||||
<img v-if="currentBannerImg" :src="currentBannerImg" alt="" />
|
||||
<div class="comb-mask"></div>
|
||||
</div>
|
||||
|
||||
<!-- 1. 搜索栏(可选左侧 Logo + 搜索框 + 小程序胶囊) -->
|
||||
<div class="comb-search-row">
|
||||
<img
|
||||
v-if="res.searType === 'logo' && res.logoUrl"
|
||||
class="comb-logo"
|
||||
:src="res.logoUrl"
|
||||
alt="logo"
|
||||
/>
|
||||
<div class="comb-search-box" :style="searchBoxStyle">
|
||||
<el-icon class="search-ico"><Search /></el-icon>
|
||||
<span class="search-text">{{ displayHotWord }}</span>
|
||||
</div>
|
||||
<div class="capsule">
|
||||
<span class="cap-icon">
|
||||
<svg width="18" height="5" viewBox="0 0 18 5" fill="currentColor">
|
||||
<circle cx="2.5" cy="2.5" r="2" />
|
||||
<circle cx="9" cy="2.5" r="2" />
|
||||
<circle cx="15.5" cy="2.5" r="2" />
|
||||
</svg>
|
||||
</span>
|
||||
<span class="cap-divider"></span>
|
||||
<span class="cap-icon">
|
||||
<svg width="15" height="15" viewBox="0 0 15 15" fill="none">
|
||||
<circle cx="7.5" cy="7.5" r="6" stroke="currentColor" stroke-width="1.6" />
|
||||
<circle cx="7.5" cy="7.5" r="2.2" fill="currentColor" />
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 2. 分类选项卡 -->
|
||||
<div v-if="res.showTabs !== false" class="comb-tabs">
|
||||
<div class="comb-tabs-scroll">
|
||||
<div
|
||||
v-for="(tab, idx) in displayTabs"
|
||||
:key="idx"
|
||||
class="comb-tab-item"
|
||||
:class="{ active: activeTabIndex === idx }"
|
||||
:style="{
|
||||
color:
|
||||
activeTabIndex === idx
|
||||
? res.tabActiveColor || '#E93323'
|
||||
: res.tabColor || '#FFFFFF',
|
||||
}"
|
||||
@click.stop="activeTabIndex = idx"
|
||||
>
|
||||
{{ tab.title || "未命名" }}
|
||||
<span
|
||||
v-if="activeTabIndex === idx"
|
||||
class="tab-line"
|
||||
:style="{ background: res.tabActiveColor || '#E93323' }"
|
||||
></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="comb-tab-more" :style="{ color: res.tabColor || '#FFFFFF' }">
|
||||
<el-icon><ArrowDown /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 3. 轮播图区域(样式1:卡片露边 / 样式2:通栏) -->
|
||||
<div class="comb-banner" :class="{ 'no-tabs': res.showTabs === false, 'is-card': cardMode }">
|
||||
<!-- 样式2:通栏轮播 -->
|
||||
<el-carousel
|
||||
v-if="!cardMode && bannerList.length"
|
||||
class="comb-carousel"
|
||||
autoplay
|
||||
:interval="5000"
|
||||
height="146px"
|
||||
indicator-position="none"
|
||||
@change="onSlideChange"
|
||||
>
|
||||
<el-carousel-item v-for="(item, index) in bannerList" :key="index">
|
||||
<div class="slide-wrap">
|
||||
<img
|
||||
v-if="item.img"
|
||||
class="slide-img"
|
||||
:style="bannerImgStyle"
|
||||
:src="item.img"
|
||||
alt=""
|
||||
/>
|
||||
<div v-else class="slide-empty" :style="bannerImgStyle">750*332</div>
|
||||
</div>
|
||||
</el-carousel-item>
|
||||
</el-carousel>
|
||||
|
||||
<!-- 样式1:卡片风格(两侧露出前后图片) -->
|
||||
<div
|
||||
v-else-if="cardMode && bannerList.length"
|
||||
ref="cardPreview"
|
||||
class="card-preview"
|
||||
@mouseenter="stopBannerTimer"
|
||||
@mouseleave="startBannerTimer"
|
||||
>
|
||||
<div
|
||||
class="card-track"
|
||||
:style="{ transform: `translateX(${trackOffset}px)` }"
|
||||
>
|
||||
<div
|
||||
v-for="(item, index) in cardSlides"
|
||||
:key="index"
|
||||
class="card-slide"
|
||||
:class="{ active: index === activeTrackIndex }"
|
||||
>
|
||||
<img
|
||||
v-if="item.img"
|
||||
class="card-img"
|
||||
:style="bannerImgStyle"
|
||||
:src="item.img"
|
||||
alt=""
|
||||
/>
|
||||
<div v-else class="slide-empty" :style="bannerImgStyle">750*332</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="slide-empty" :style="bannerImgStyle">请添加轮播图</div>
|
||||
|
||||
<!-- 自定义指示器 -->
|
||||
<div
|
||||
v-if="res.indicatorStyle !== 'none' && bannerList.length > 1"
|
||||
class="comb-indicator"
|
||||
:class="'pos-' + (res.indicatorPosition || 'center')"
|
||||
>
|
||||
<div
|
||||
v-for="(item, i) in bannerList"
|
||||
:key="i"
|
||||
class="dot"
|
||||
:class="[res.indicatorStyle || 'round', { active: i === currentSlide }]"
|
||||
:style="
|
||||
i === currentSlide
|
||||
? { background: res.indicatorColor || '#E93323' }
|
||||
: {}
|
||||
"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch, onMounted, onBeforeUnmount } from "vue";
|
||||
import { Search, ArrowDown } from "@element-plus/icons-vue";
|
||||
|
||||
defineOptions({ title: "头部组件" });
|
||||
const props = defineProps(["res"]);
|
||||
|
||||
const px = (v) => `${(Number(v) || 0) / 2}px`;
|
||||
|
||||
// 搜索热词轮换
|
||||
const hotIndex = ref(0);
|
||||
let hotTimer = null;
|
||||
const validHotWords = computed(() =>
|
||||
(props.res?.hotWords || []).filter((w) => w && String(w).trim())
|
||||
);
|
||||
const displayHotWord = computed(() => {
|
||||
if (validHotWords.value.length) {
|
||||
return validHotWords.value[hotIndex.value % validHotWords.value.length];
|
||||
}
|
||||
return props.res?.placeWords || "搜索商品名称";
|
||||
});
|
||||
|
||||
function stopHotTimer() {
|
||||
if (hotTimer) {
|
||||
clearInterval(hotTimer);
|
||||
hotTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [validHotWords.value.length, props.res?.hotInterval],
|
||||
() => {
|
||||
stopHotTimer();
|
||||
hotIndex.value = 0;
|
||||
if (validHotWords.value.length > 1) {
|
||||
hotTimer = setInterval(() => {
|
||||
hotIndex.value = (hotIndex.value + 1) % validHotWords.value.length;
|
||||
}, (Number(props.res?.hotInterval) || 3) * 1000);
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// 选项卡
|
||||
const activeTabIndex = ref(0);
|
||||
const displayTabs = computed(() => {
|
||||
const custom = Array.isArray(props.res?.tabList) ? props.res.tabList : [];
|
||||
return [{ title: "首页", isHome: true }, ...custom];
|
||||
});
|
||||
|
||||
// 轮播图
|
||||
const currentSlide = ref(0);
|
||||
const previewWidth = ref(375);
|
||||
const cardPreview = ref(null);
|
||||
let bannerTimer = null;
|
||||
|
||||
const bannerList = computed(() =>
|
||||
Array.isArray(props.res?.list) ? props.res.list : []
|
||||
);
|
||||
const cardMode = computed(() => (props.res?.swiperStyle || "card") === "card");
|
||||
const currentBannerImg = computed(() => {
|
||||
const list = bannerList.value;
|
||||
if (!list.length) return "";
|
||||
const item = list[currentSlide.value % list.length] || list[0];
|
||||
return item?.img || "";
|
||||
});
|
||||
|
||||
const cardSlides = computed(() => {
|
||||
const list = bannerList.value;
|
||||
if (list.length <= 1) return list;
|
||||
return [list[list.length - 1], ...list, list[0]];
|
||||
});
|
||||
|
||||
const activeTrackIndex = computed(() =>
|
||||
bannerList.value.length > 1 ? currentSlide.value + 1 : currentSlide.value
|
||||
);
|
||||
|
||||
const trackOffset = computed(
|
||||
() =>
|
||||
previewWidth.value * 0.06 -
|
||||
activeTrackIndex.value * previewWidth.value * 0.88
|
||||
);
|
||||
|
||||
function measureCard() {
|
||||
if (cardPreview.value) {
|
||||
previewWidth.value = cardPreview.value.offsetWidth || 375;
|
||||
}
|
||||
}
|
||||
|
||||
function startBannerTimer() {
|
||||
stopBannerTimer();
|
||||
if (!cardMode.value || bannerList.value.length < 2) return;
|
||||
bannerTimer = setInterval(() => {
|
||||
currentSlide.value = (currentSlide.value + 1) % bannerList.value.length;
|
||||
}, 4000);
|
||||
}
|
||||
|
||||
function stopBannerTimer() {
|
||||
if (bannerTimer) {
|
||||
clearInterval(bannerTimer);
|
||||
bannerTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
function onSlideChange(idx) {
|
||||
currentSlide.value = idx;
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [cardMode.value, bannerList.value.length],
|
||||
() => {
|
||||
if (currentSlide.value >= bannerList.value.length) {
|
||||
currentSlide.value = 0;
|
||||
}
|
||||
stopBannerTimer();
|
||||
if (cardMode.value) {
|
||||
measureCard();
|
||||
startBannerTimer();
|
||||
}
|
||||
},
|
||||
{ immediate: true, flush: "post" }
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
measureCard();
|
||||
startBannerTimer();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
stopHotTimer();
|
||||
stopBannerTimer();
|
||||
});
|
||||
|
||||
// 样式计算
|
||||
const wrapperStyle = computed(() => ({
|
||||
marginBottom: px(props.res?.pageGap ?? 0),
|
||||
}));
|
||||
|
||||
const searchBoxStyle = computed(() => {
|
||||
const r = props.res || {};
|
||||
const align = r.textAlign || "left";
|
||||
return {
|
||||
background: r.boxColor || "#FFFFFF",
|
||||
color: r.textColor || "#303133",
|
||||
borderRadius: px(r.searchRadius ?? 30),
|
||||
justifyContent:
|
||||
align === "center"
|
||||
? "center"
|
||||
: align === "right"
|
||||
? "flex-end"
|
||||
: "flex-start",
|
||||
};
|
||||
});
|
||||
|
||||
const bannerImgStyle = computed(() => ({
|
||||
borderRadius: px(props.res?.bannerRadius ?? 14),
|
||||
}));
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.tpl-home-comb {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
padding: 8px 10px 10px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.comb-bg {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(180deg, #e93323 0%, #f5f5f5 100%);
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transform: scale(1.25);
|
||||
filter: blur(22px);
|
||||
display: block;
|
||||
}
|
||||
.comb-mask {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
height: 90px;
|
||||
background: linear-gradient(180deg, rgba(245, 245, 245, 0) 0%, #f5f5f5 100%);
|
||||
}
|
||||
}
|
||||
|
||||
.comb-search-row {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.comb-logo {
|
||||
width: 69px;
|
||||
height: 24px;
|
||||
object-fit: contain;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.comb-search-box {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
height: 30px;
|
||||
padding: 0 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 12px;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
.search-ico {
|
||||
font-size: 13px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.search-text {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.capsule {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 30px;
|
||||
padding: 0 10px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.6);
|
||||
box-sizing: border-box;
|
||||
color: #303133;
|
||||
.cap-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
svg {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
.cap-divider {
|
||||
width: 1px;
|
||||
height: 14px;
|
||||
background: rgba(0, 0, 0, 0.12);
|
||||
margin: 0 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.comb-tabs {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 36px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.comb-tabs-scroll {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
overflow-x: auto;
|
||||
scrollbar-width: none;
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.comb-tab-item {
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
font-size: 13px;
|
||||
line-height: 34px;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
&.active {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.tab-line {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: 2px;
|
||||
transform: translateX(-50%);
|
||||
width: 18px;
|
||||
height: 2px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.comb-tab-more {
|
||||
flex-shrink: 0;
|
||||
padding-left: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.comb-banner {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
margin-top: 6px;
|
||||
height: 146px;
|
||||
overflow: hidden;
|
||||
&.is-card {
|
||||
margin-left: -10px;
|
||||
margin-right: -10px;
|
||||
}
|
||||
&.no-tabs {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.comb-carousel {
|
||||
height: 146px;
|
||||
}
|
||||
|
||||
.slide-wrap {
|
||||
width: 100%;
|
||||
height: 146px;
|
||||
}
|
||||
|
||||
.slide-img {
|
||||
width: 100%;
|
||||
height: 146px;
|
||||
object-fit: fill;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.slide-empty {
|
||||
width: 100%;
|
||||
height: 146px;
|
||||
background: rgba(255, 255, 255, 0.75);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.card-preview {
|
||||
width: 100%;
|
||||
height: 146px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.card-track {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
transition: transform 0.45s ease;
|
||||
}
|
||||
|
||||
.card-slide {
|
||||
flex: 0 0 88%;
|
||||
padding: 0 3px;
|
||||
box-sizing: border-box;
|
||||
transform: scale(0.92);
|
||||
transition: transform 0.35s ease;
|
||||
&.active {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.card-img {
|
||||
width: 100%;
|
||||
height: 146px;
|
||||
object-fit: fill;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.comb-indicator {
|
||||
position: absolute;
|
||||
bottom: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
z-index: 3;
|
||||
&.pos-left {
|
||||
left: 16px;
|
||||
}
|
||||
&.pos-center {
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
&.pos-right {
|
||||
right: 16px;
|
||||
}
|
||||
.dot {
|
||||
background: rgba(255, 255, 255, 0.65);
|
||||
transition: all 0.25s ease;
|
||||
&.round {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
margin: 0 3px;
|
||||
}
|
||||
&.line {
|
||||
width: 12px;
|
||||
height: 3px;
|
||||
border-radius: 2px;
|
||||
margin: 0 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
29
seller/src/views/shop/wap/template/tpl_hot_area.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<template>
|
||||
<div class="layout">
|
||||
<div class="hot-area">
|
||||
<!-- <img :src="res.list[0].img" alt="" /> -->
|
||||
<hotzone @change="changeHotzone" :zonesInit="res.list[0].zoneInfo" :image="res.list[0].img"></hotzone>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import hotzone from "@/views/shop/hotzone";
|
||||
|
||||
const props = defineProps(["res"]);
|
||||
|
||||
function changeHotzone(info) {
|
||||
props.res.list[0].zoneInfo = info;
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import "./tpl.scss";
|
||||
.hot-area {
|
||||
width: 100%;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
> img {
|
||||
width: 300px;
|
||||
height: 500px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||