在电商系统二开项目里,前端 UniApp 端的改造成本往往被低估。
很多团队拿到 LikeShop 单商户标准版源码后,第一反应是直接改页面:改首页样式、改商品卡片、改配色。结果改着改着就发现:同样的商品卡片在首页、分类页、搜索页、活动页各写了一遍,改一个样式要改四五处;主题色散落在几十个页面文件里,客户换个品牌色要全局搜索替换;多端兼容出问题,小程序正常 H5 错乱。
本质原因是没有用好组件化体系和主题变量机制。LikeShop 单商户标准版的 UniApp 端本身已经做了分层的组件设计和 SCSS 变量体系,二开时顺着这套架构扩展,效率会比直接改页面高很多。
这篇文章从目录结构、组件复用体系、主题适配三个层面,讲讲基于这套源码做二开的正确姿势,附核心代码示例。
一、先理清目录结构:不要上来就改页面
二开的第一步不是写代码,是先搞清楚哪些是基础层、哪些是业务层。乱改基础组件,后续升级版本会非常痛苦。
LikeShop 单商户标准版 UniApp 端的典型目录结构大致如下:
├── components/ # 公共组件库(二开尽量复用,少改源码)
│ ├── base/ # 基础组件:按钮、卡片、弹窗、空状态
│ ├── business/ # 业务组件:商品卡片、价格组件、规格选择器
│ └── layout/ # 布局组件:导航栏、底部Tab、分割线
├── pages/ # 业务页面(二开主要修改区)
│ ├── index/ # 首页
│ ├── goods/ # 商品列表、详情
│ ├── cart/ # 购物车
│ ├── user/ # 个人中心
│ └── ...
├── static/ # 静态资源
├── style/ # 全局样式与主题变量(主题适配核心)
│ ├── variables.scss # 主题变量:主色、字号、间距、圆角
│ ├── mixin.scss # 通用混入:文本省略、flex布局
│ └── common.scss # 全局公共样式
├── utils/ # 工具函数
├── api/ # 接口请求
└── uni.scss # 全局入口样式文件
二开原则:
style/里的变量是主题总开关,改配色优先改变量,不要去页面里硬写颜色;components/business/里的业务组件优先复用,新需求尽量扩展组件属性,不要复制一份重写;pages/是业务层,可以自由改,但尽量抽组件,不要把大段逻辑堆在页面里。
二、组件复用体系:三层组件化设计
这套源码的组件是分层设计的,越底层越稳定,越上层越贴近业务。二开时在对应层级扩展,不要跨层乱改。
2.1 第一层:基础组件(Base Components)
基础组件是和业务无关的通用 UI 单元,比如按钮、空状态、加载更多、弹窗、分割线。它们不包含任何商品、订单相关的业务逻辑,只负责展示和交互。
二开建议:
- 不要直接修改原有基础组件的默认样式,容易影响所有引用页面;
- 需要新的基础组件时,在
components/base/下新增,不要堆在页面里; - 基础组件尽量用 Props 驱动,不要写死业务数据。
示例:一个通用的空状态组件(简化版)
<!-- components/base/empty/empty.vue -->
<template>
<view class="empty-wrap">
<image class="empty-icon" :src="icon" mode="aspectFit" />
<text class="empty-text">{{ text }}</text>
<slot></slot>
</view>
</template>
<script>
export default {
name: 'BaseEmpty',
props: {
icon: {
type: String,
default: '/static/images/empty.png'
},
text: {
type: String,
default: '暂无数据'
}
}
}
</script>
<style lang="scss" scoped>
.empty-wrap {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80rpx 0;
.empty-icon {
width: 160rpx;
height: 160rpx;
margin-bottom: 20rpx;
}
.empty-text {
font-size: 28rpx;
color: $text-color-secondary;
}
}
</style>
2.2 第二层:业务组件(Business Components)
这是二开复用率最高的一层。商品卡片、价格展示、规格选择、订单状态条、优惠券卡片,这些和业务强相关、但多个页面都会用到的单元,都封装成业务组件。
最典型的就是商品卡片:首页瀑布流用、分类页用、搜索结果用、活动页也用。如果每个页面各写一遍,改一次样式要改四五处。
正确做法 :封装一个 goods-card 业务组件,通过 Props 控制展示形态(大图模式/列表模式/瀑布流模式),所有页面复用同一个组件。
示例:商品卡片组件(简化版)
<!-- components/business/goods-card/goods-card.vue -->
<template>
<view class="goods-card" :class="mode" @click="goDetail">
<image class="goods-img" :src="goods.image" mode="aspectFill" />
<view class="goods-info">
<text class="goods-name">{{ goods.name }}</text>
<view class="goods-price">
<text class="symbol">¥</text>
<text class="price">{{ goods.price }}</text>
<text v-if="goods.originalPrice" class="original-price">
¥{{ goods.originalPrice }}
</text>
</view>
<view v-if="showSales" class="goods-sales">
已售 {{ goods.sales }}
</view>
</view>
</view>
</template>
<script>
export default {
name: 'GoodsCard',
props: {
goods: {
type: Object,
required: true
},
mode: {
type: String,
default: 'grid' // grid 网格 / list 列表
},
showSales: {
type: Boolean,
default: true
}
},
methods: {
goDetail() {
uni.navigateTo({
url: `/pages/goods/detail?id=${this.goods.id}`
})
}
}
}
</script>
<style lang="scss" scoped>
.goods-card {
background: #fff;
border-radius: $border-radius-base;
overflow: hidden;
.goods-img {
width: 100%;
height: 320rpx;
}
.goods-info {
padding: 16rpx;
.goods-name {
font-size: 28rpx;
color: $text-color-primary;
line-height: 1.4;
@include text-ellipsis(2);
}
.goods-price {
margin-top: 10rpx;
.symbol {
font-size: 24rpx;
color: $price-color;
}
.price {
font-size: 32rpx;
font-weight: bold;
color: $price-color;
}
.original-price {
margin-left: 10rpx;
font-size: 24rpx;
color: $text-color-tertiary;
text-decoration: line-through;
}
}
}
&.list {
display: flex;
.goods-img {
width: 200rpx;
height: 200rpx;
flex-shrink: 0;
}
}
}
</style>
页面里使用就非常简洁:
<!-- 首页网格模式 -->
<goods-card
v-for="item in goodsList"
:key="item.id"
:goods="item"
mode="grid"
/>
<!-- 分类页列表模式 -->
<goods-card
v-for="item in goodsList"
:key="item.id"
:goods="item"
mode="list"
:show-sales="false"
/>
二开建议:
- 新增展示字段优先加 Props,不要直接改组件内部逻辑;
- 新的业务场景(如拼团商品、秒杀商品),优先扩展现有组件,而不是新建一个几乎一样的;
- 组件内部的颜色、间距尽量用主题变量,不要硬写死色值。
2.3 第三层:页面级区块(Page Sections)
更复杂的页面模块,比如首页轮播区、秒杀专区、品牌专区,它们只在首页出现,但内部又包含多个子组件。这类可以封装成页面区块组件,放在 components/business/ 或单独的 sections/ 目录。
好处是首页不会堆上千行代码,每个区块独立维护,二开加新板块时直接新增一个区块组件,插入首页即可。
三、自定义主题适配:变量化 + 混入
客户最常见的二开需求就是换主题色、改整体风格。如果颜色散落在每个页面文件里,改起来就是灾难。正确的做法是所有视觉常量都抽到 SCSS 变量里,改主题只改变量文件。
3.1 主题变量文件
style/variables.scss 是主题总开关。LikeShop 标准版里已经预设了一套变量,二开时直接覆盖即可。
/* style/variables.scss */
// 主题色
$primary-color: #FF6B35; // 品牌主色
$primary-color-light: #FFE5D9; // 主色浅色
$price-color: #E63946; // 价格色
$success-color: #2A9D8F;
$warning-color: #F4A261;
$danger-color: #E63946;
// 文字色
$text-color-primary: #1A1A1A;
$text-color-secondary: #666666;
$text-color-tertiary: #999999;
$text-color-placeholder: #CCCCCC;
// 背景色
$bg-color-page: #F5F5F5;
$bg-color-card: #FFFFFF;
// 圆角
$border-radius-sm: 8rpx;
$border-radius-base: 12rpx;
$border-radius-lg: 20rpx;
// 间距
$spacing-xs: 10rpx;
$spacing-sm: 20rpx;
$spacing-base: 30rpx;
$spacing-lg: 40rpx;
// 字号
$font-size-xs: 22rpx;
$font-size-sm: 24rpx;
$font-size-base: 28rpx;
$font-size-lg: 32rpx;
$font-size-xl: 36rpx;
客户要换品牌色,只需要改 $primary-color 这一个变量,所有按钮、图标、高亮文字、选中状态都会同步变色,不用全局替换。
3.2 全局混入(Mixin)
重复的样式逻辑不要到处复制,用混入复用。
/* style/mixin.scss */
// 文本省略
@mixin text-ellipsis($line: 1) {
@if $line == 1 {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
} @else {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: $line;
overflow: hidden;
}
}
// flex 居中
@mixin flex-center {
display: flex;
align-items: center;
justify-content: center;
}
// 1px 边框(解决小程序边框变粗问题)
@mixin border-1px($color: #eee, $position: bottom) {
position: relative;
&::after {
content: '';
position: absolute;
#{$position}: 0;
left: 0;
width: 100%;
height: 1px;
background: $color;
transform: scaleY(0.5);
}
}
组件和页面里直接引用:
@import '@/style/mixin.scss';
.title {
font-size: $font-size-lg;
color: $text-color-primary;
@include text-ellipsis(2);
}
3.3 全局样式引入
在 uni.scss 中统一引入变量和混入,这样每个页面和组件都能直接使用变量,不用每个文件都 import。
/* uni.scss */
@import './style/variables.scss';
@import './style/mixin.scss';
@import './style/common.scss';
注意:UniApp 中
uni.scss里的变量会自动注入到每个页面和组件,不用重复引入;但 mixin 不会自动注入,使用时仍需在 style 块中 import。
3.4 深色模式/多主题切换(进阶)
如果客户需要支持浅色/深色双主题,或者多套品牌主题切换,可以在变量体系基础上扩展:
- 定义两套变量映射;
- 通过 page 根节点的 class 切换主题;
- 变量用 CSS 变量(CSS Custom Properties)实现运行时切换。
简化示例:
/* 浅色主题 */
page.theme-light {
--primary-color: #FF6B35;
--bg-page: #F5F5F5;
--text-primary: #1A1A1A;
}
/* 深色主题 */
page.theme-dark {
--primary-color: #FF8C5A;
--bg-page: #1A1A1A;
--text-primary: #F0F0F0;
}
// 组件内使用
.goods-card {
background: var(--bg-card);
color: var(--text-primary);
}
JS 端切换主题时给 page 加对应 class 即可。适合做用户可切换的多主题需求。
四、二开最佳实践
4.1 尽量扩展,不要修改源码
原有组件能满足 80% 需求的,就加 Props 扩展,不要复制一份重写。复制的组件越多,后续维护成本越高,官方版本升级时迁移也越痛苦。
4.2 样式用变量,少硬写
颜色、字号、间距、圆角,凡是视觉常量都尽量用主题变量。这样换主题、做风格统一调整时,只改变量文件一处。
4.3 页面逻辑抽组件
页面文件超过 300 行就要考虑拆组件了。把独立的功能区块拆成子组件,页面只负责数据组装和状态管理,可读性和可维护性会好很多。
4.4 多端兼容注意
- 小程序不支持部分 CSS 特性(如
%计算、某些伪类),样式改动后要在微信开发者工具里验证; - H5 端要注意浏览器默认样式差异,统一用 reset;
- APP 端的导航栏、状态栏高度和小程序不同,自定义导航栏时要用
uni.getSystemInfoSync()动态计算。
4.5 保留升级余地
二开时尽量不要改动 components/base/ 和 style/variables.scss 的原有变量名,可以新增变量和扩展类名。这样后续官方更新版本时,冲突最少,合并成本最低。
五、常见坑
- 样式不生效
坑:改了变量文件,页面颜色没变。
排查:检查uni.scss是否正确引入变量文件;组件内是否硬写了颜色覆盖了变量;scoped 样式下深度选择器是否正确。 - 组件重复渲染
坑:列表里用业务组件,滚动卡顿。
优化:给组件加v-memo或合理使用computed;图片懒加载;长列表用uni-list或虚拟列表。 - 多端样式不一致
坑:H5 正常,小程序错位。
排查:避免使用小程序不支持的 CSS 属性;rpx 单位在不同端的渲染差异;自定义组件样式隔离导致的继承问题。 - 主题色改不全
坑:改了主色变量,个别按钮、图标还是旧颜色。
原因:那些地方是硬写的色值,没有用变量。全局搜一下色值,统一替换成变量。
结语
UniApp 端的二开,拼的不是写代码的速度,是架构的利用程度。
LikeShop 单商户标准版已经把基础组件、业务组件、主题变量这几层搭好了,二开时顺着这套体系走:复用组件、扩展属性、改变量、抽区块,效率会比直接在页面上硬改高很多,后续维护和升级也更轻松。
对做二开的团队来说,最重要的不是功能实现得快,是改完之后代码还干净、还能迭代。组件复用和主题变量,就是保证代码干净的两件基础工具。