在电商平台中,商品管理模块是连接商家与消费者的重要桥梁。它不仅承担着商品信息的录入、修改与删除等基本功能,还涉及到库存管理、价格调整、上下架操作等多项关键任务。一个高效的商品管理模块能够帮助商家轻松管理各种商品,提高销售效率,并确保用户获得准确的信息。
本文将围绕电商后台管理系统中的商品管理模块的开发展开,带你深入了解如何构建一个功能全面、操作便捷的商品管理系统。我们将从需求分析开始,逐步实现商品的增删改查、分类管理、库存监控等功能。
有了订单管理模块的开发经验,相信你再来编写商品管理模块将会非常容易。商品管理模块与订单管理模块的开发过程基本类似,先布局页面,再将获取到的数据绑定到页面上,最后处理用户交互即可。相比订单管理模块,商品管理模块的新增商品功能略显复杂。
首先,在项目的 components
文件夹下新建一个 goods
子文件夹,创建两个 Vue 组件文件,分别命名为 Goods.vue
和 AddGoods .vue
。本节先只做商品列表页的开发,对于 AddGoods .vue
文件可以先不做处理。
在 Router.js
文件中注册新创建的这两个组件。首先引入组件:
import Goods from '../components/goods/Goods.vue';
import AddGoods from '../components/goods/AddGoods.vue';
在 home
路由下的 children
中新增两个子路由:
// 0是普通商品,1是秒杀商品,2是今日推荐
{
path: 'goods/:type',
component: Goods,
name: 'Goods'
},
{
path: 'addGoods/:type',
// 0是普通商品,1是秒杀商品,2是今日推荐
component: AddGoods,
name: 'AddGoods'
}
在 Mock.js
文件中新增一个获取商品数据的方法,代码如下:
// 模拟获取商品数据
// type: 商品类型。0为普通商品,1为秒杀商品,2为今日推荐
getGoods(type) {
let array = [];
for (let i = 0; i < mockjs.Random.integer(5, 10); i++) {
array.push(mockjs.mock({
'name': (type == 0 ? '普通商品' : type == 1 ? '秒杀商品' : '今日推荐') + i,
'img': mockjs.Random.dataImage('60x100', '商品示例图'),
'price': mockjs.Random.integer(20, 500) + '元',
'sellCount': mockjs.Random.integer(10, 100),
'count': mockjs.Random.integer(10, 100),
'back': mockjs.Random.integer(10, 100),
'backPrice': mockjs.Random.integer(0, 5000) + '元',
'owner': mockjs.Random.cname(),
'time': mockjs.Random.datetime('yyyy-MM-dd A HH:mm:ss'),
'state': mockjs.Random.boolean()
}));
}
return array;
}
Goods.vue
)以下是 Goods.vue
的完整代码:
<template>
<div class="content-container" direction="vertical">
<!-- input -->
<div>
<el-container class="content-row">
<div class="input-tip">商品名称:</div>
<div class="input-field">
<el-input v-model="queryParams.name"></el-input>
</div>
<div class="input-tip">商品编号:</div>
<div class="input-field">
<el-input v-model="queryParams.id"></el-input>
</div>
<div class="input-tip">商品分类:</div>
<div class="input-field">
<el-select style="width:150px;" v-model="queryParams.category" placeholder="请选择分类">
<el-option v-for="item in categorys" :key="item" :label="item" :value="item"></el-option>
</el-select>
</div>
</el-container>
<el-container class="content-row">
<div class="input-tip">是否上架:</div>
<div class="input-field">
<el-select v-model="sellModeString" style="width: 150px;">
<el-option key="0" label="否" value="0"></el-option>
<el-option key="1" label="是" value="1"></el-option>
<el-option key="2" label="全部" value="2"></el-option>
</el-select>
</div>
<div class="input-tip">是否过期:</div>
<div class="input-field">
<el-select v-model="expModeString" style="width:150px;">
<el-option key="0" label="否" value="0"></el-option>
<el-option key="1" label="是" value="1"></el-option>
<el-option key="2" label="全部" value="2"></el-option>
</el-select>
</div>
</el-container>
</div>
<!-- button -->
<div class="content-row">
<el-container>
<el-button type="primary" @click="requestData">检索</el-button>
<el-button type="primary" @click="clear">显示全部</el-button>
<el-button type="success" @click="addGood">新增商品</el-button>
</el-container>
</div>
<!-- list -->
<div>
<el-table :data="goodsData" tooltip-effect="dark" style="width:100%">
<el-table-column label="商品" width="100">
<template #default="scope">
<div style="text-align:center">
<el-image :src="scope.row.img" style="width: 60px; height: 100px"></el-image>
</div>
<div style="text-align:center">{{ scope.row.name }}</div>
</template>
</el-table-column>
<el-table-column label="价格" width="100" prop="price"></el-table-column>
<el-table-column label="销量" width="100" prop="sellCount"></el-table-column>
<el-table-column label="库存" width="100" prop="count"></el-table-column>
<el-table-column label="退款数量" width="100" prop="back"></el-table-column>
<el-table-column label="退款金额" width="100" prop="backPrice"></el-table-column>
<el-table-column label="操作" width="100" prop="name">
<template #default="scope">
<el-button @click="operate(scope.row)" :type="scope.row.state ? 'danger' : 'success'">
{{ scope.row.state ? '下架' : '上架' }}
</el-button>
</template>
</el-table-column>
<el-table-column label="管理员" width="100" prop="owner"></el-table-column>
<el-table-column label="更新时间" width="200" prop="time"></el-table-column>
</el-table>
</div>
</div>
</template>
<script setup>
import Mock from '../../mock/Mock'
import { ref, computed, onMounted } from 'vue'
import { onBeforeRouteUpdate, useRoute, useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
const route = useRoute()
const router = useRouter()
const goodsData = ref([])
// 模拟分类数据
const categorys = ref([
"全部",
"男装",
"女装"
])
const queryParams = ref({
name: "",
id: "",
category: "",
sellMode: 2, // 0否,1是,2全部
expMode: 2 // 0否,1是,2全部
})
const sellModeString = computed({
get() {
if (queryParams.value.sellMode === 2) {
return '全部'
}
return queryParams.value.sellMode === 0 ? '否' : '是'
},
set(val) {
queryParams.value.sellMode = val
}
})
const expModeString = computed({
get() {
if (queryParams.value.expMode === 2) {
return '全部'
}
return queryParams.value.expMode === 0 ? '否' : '是'
},
set(val) {
queryParams.value.expMode = val
}
})
// 组件挂载时获取数据
onMounted(() => {
goodsData.value = Mock.getGoods(route.params.type)
})
// 路由更新时刷新数据
onBeforeRouteUpdate((to) => {
goodsData.value = Mock.getGoods(to.params.type)
})
function requestData() {
ElMessage({
message: '筛选请求参数: ' + JSON.stringify(queryParams.value),
type: 'success'
})
goodsData.value = Mock.getGoods(route.params.type)
}
// 进行上架、下架操作
function operate(item) {
item.state = !item.state
}
// 清空筛选项
function clear() {
queryParams.value = {
name: "",
id: "",
category: "",
sellMode: 2,
expMode: 2,
}
goodsData.value = Mock.getGoods(route.params.type)
}
// 新增商品
function addGood() {
router.push({ name: 'AddGoods', params: { type: route.params.type } })
}
</script>
运行当前工程,访问商品列表页面,检查页面中的交互元素是否能正确执行对应的方法。
在商品管理列表页中,有一个“新增商品”按钮。点击此按钮后,会跳转到“新建商品”页面。在此页面中,需要对商品的诸多属性进行设置。为了方便管理和用户体验,可以使用 el-tab
组件将商品设置分成几个模块,如基础设置、价格库存设置和商品详情设置等。每一个设置模块都可以封装成一个独立的组件。
首先,在工程的 goods
文件夹下新建以下文件:
GoodsBaseSetting.vue
GoodsPriceSetting.vue
GoodsDetailSetting.vue
在 AddGoods.vue
中引入这3个子组件。
以下是 AddGoods.vue
的代码:
<script setup>
import { ref } from 'vue';
import BaseSetting from './GoodsBaseSetting.vue';
import PriceSetting from './GoodsPriceSetting.vue';
import DetailSetting from './GoodsDetailSetting.vue';
const activeTab = ref("1");
function handleClick(idx) {
// 处理Tab点击事件
}
</script>
<template>
<div class="content-container" direction="vertical">
<el-tabs v-model="activeTab" type="card" @tab-click="handleClick">
<el-tab-pane label="基础设置" name="1">
<BaseSetting></BaseSetting>
</el-tab-pane>
<el-tab-pane label="价格库存" name="2">
<PriceSetting></PriceSetting>
</el-tab-pane>
<el-tab-pane label="商品详情" name="3">
<DetailSetting></DetailSetting>
</el-tab-pane>
</el-tabs>
</div>
</template>
接下来编写 GoodsBaseSetting.vue
组件的代码:
<script setup>
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { ElMessage } from 'element-plus';
const router = useRouter();
const queryParams = ref({
name: "",
desc: "",
timeRange: "",
category: 0,
});
function cancel() {
router.go(-1);
}
function submit() {
ElMessage({
type: 'success',
message: '设置商品基本属性: ' + JSON.stringify(queryParams.value),
});
}
</script>
<template>
<div>
<el-container class="content-row">
<div class="input-tip">商品名称:</div>
<div class="input-field">
<el-input v-model="queryParams.name"></el-input>
</div>
</el-container>
<el-container class="content-row">
<div class="input-tip">商品简介:</div>
<div class="input-field">
<el-input type="textarea" :rows="3" v-model="queryParams.desc"></el-input>
</div>
</el-container>
<el-container class="content-row">
<div class="input-tip">商品封面:</div>
<el-upload :auto-upload="false" :limit="1" list-type="picture-card">
<el-icon><Plus/></el-icon>
</el-upload>
</el-container>
<el-container class="content-row">
<div class="input-tip">列表图片:</div>
<el-upload :auto-upload="false" :limit="5" list-type="picture-card">
<el-icon><Plus/></el-icon>
</el-upload>
</el-container>
<el-container class="content-row">
<div class="input-tip">上架日期:</div>
<div class="input-field">
<el-date-picker type="daterange" range-separator="至"
start-placeholder="开始日期" end-placeholder="结束日期"
v-model="queryParams.timeRange">
</el-date-picker>
</div>
</el-container>
<el-container class="content-row">
<div class="input-tip">商品分类:</div>
<div class="input-field">
<el-select style="width: 150px;" v-model="queryParams.category">
<el-option key="0" label="男装" :value="0"></el-option>
<el-option key="1" label="男鞋" :value="1"></el-option>
<el-option key="2" label="围巾" :value="2"></el-option>
</el-select>
</div>
<div style="margin-top:6px">
<el-button type="primary" size="small" round>添加分类</el-button>
</div>
</el-container>
<el-container class="content-row">
<el-button type="success" plain @click="submit">提交</el-button>
<div style="margin-left:40px"></div>
<el-button type="warning" plain @click="cancel">取消</el-button>
</el-container>
</div>
</template>
价格和库存配置模块相对简单,只需要布局一些输入框来接收用户的输入配置即可。在 GoodsPriceSetting.vue
中编写如下代码。
确保在 goods
文件夹下已经新建了 GoodsPriceSetting.vue
文件。
以下是 GoodsPriceSetting.vue
的代码:
<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
const router = useRouter()
const queryParams = ref({
maketPrice:0,
showPrice:0,
coin:0,
price:0,
limit:0,
count:0,
sellCount:0,
viewCount:0
})
function cancel(){
router.go(-1);
}
function submit() {
ElMessage({
type:'success',
message:'设置价格与库存:' + JSON.stringify(queryParams.value)
})
}
</script>
<template>
<div>
<div class="title">
<div style="line-height:35px;margin-left:20px">价格设置</div>
</div>
<el-container class="content-row">
<div class="input-tip">市场价:</div>
<div class="input-field">
<el-input v-model="queryParams.maketPrice"></el-input>
</div>
</el-container>
<el-container class="content-row">
<div class="input-tip">展示价:</div>
<div class="input-field">
<el-input v-model="queryParams.showPrice"></el-input>
</div>
</el-container>
<el-container class="content-row">
<div class="input-tip">积分数:</div>
<div class="input-field">
<el-input v-model="queryParams.coin"></el-input>
</div>
</el-container>
<el-container class="content-row">
<div class="input-tip">成本价:</div>
<div class="input-field">
<el-input v-model="queryParams.price"></el-input>
</div>
</el-container>
<el-container class="content-row">
<div class="input-tip">限购数:</div>
<div class="input-field">
<el-input v-model="queryParams.limit"></el-input>
</div>
</el-container>
<div class="title">
<div style="line-height:35px;margin-left:20px">库存设置</div>
</div>
<el-container class="content-row">
<div class="input-tip">库存数量:</div>
<div class="input-field">
<el-input v-model="queryParams.count"></el-input>
</div>
</el-container>
<el-container class="content-row">
<div class="input-tip">基础销量:</div>
<div class="input-field">
<el-input v-model="queryParams.sellCount"></el-input>
</div>
</el-container>
<el-container class="content-row">
<div class="input-tip">浏览数量:</div>
<div class="input-field">
<el-input v-model="queryParams.viewCount"></el-input>
</div>
</el-container>
<el-container class="content-row">
<el-button type="success" plain @click="submit">提交</el-button>
<div style="margin-left:40px"></div>
<el-button type="warning" plain @click="cancel">取消</el-button>
</el-container>
</div>
</template>
<style scoped>
.title {
background-color:#e1e1e1;
height: 35px;
margin-bottom: 15px;
}
</style>
<script setup>
import { ref } from 'vue';
引入 ref
函数来创建响应式数据。import { useRouter } from 'vue-router';
引入 useRouter
钩子函数来使用路由功能。import { ElMessage } from 'element-plus';
引入 ElMessage
来显示消息提示。const queryParams = ref({...})
定义了一个响应式对象 queryParams
,用于存储用户输入的商品价格和库存信息。
function cancel() {...}
定义了取消按钮的点击事件处理函数,点击后返回上一页。
function submit() {...}
定义了提交按钮的点击事件处理函数,点击后显示一个成功消息。
<template>
el-container
组件来布局输入框,每个 el-container
包含一个标签和一个输入框。v-model
双向绑定 queryParams
中对应的属性和输入框的值。<style scoped>
.title
设置标题的样式。.content-row
设置每一行内容之间的间距。.input-tip
设置输入框标签的样式。.input-field
设置输入框的样式。商品详情的定制性通常较强,通常在商品上架添加时进行定制化设置。商品详情编辑推荐使用富文本编辑器,这类编辑器能够将富文本内容转换为HTML格式,操作简便。尽管富文本编辑器需要支持多种样式的文本、图片和超链接等功能,其实现可能较为复杂,但幸运的是,互联网上提供了许多优秀的富文本编辑器插件,它们可以直接使用,避免了重复开发的工作。
首先,在项目工程目录下执行以下命令来安装wangEditor
富文本编辑器插件:
npm install wangeditor --save
安装成功后,在goods
文件夹下新建一个名为GoodsEdit.vue
的文件,在其中编写如下代码:
<script setup>
import E from "wangeditor"
import { ref, onMounted, getCurrentInstance } from 'vue'
// 用来获取当前组件实例
const instance = getCurrentInstance()
// 定义组件事件,内容改变时调用
const emit = defineEmits(['contentChange'])
// 编辑器对象的引用
let editor = null
const editorContent = ref('')
onMounted(()=>{
editor = new E(instance.proxy.$refs.editorElem);
// 编辑器的事件,每次改变会获取其html内容
editor.config.onchange = contentChange
editor.config.menus = [
// 菜单配置
'head', // 标题
'bold', // 粗体
'fontSize', // 字号
'fontName', // 字体
'italic', // 斜体
'underline', // 下划线
'strikeThrough', // 删除线
'foreColor', // 文字颜色
'backColor', // 背景颜色
'link', // 插入链接
'list', // 列表
'justify', // 对齐方式
'quote', // 引用
'emoticon', // 表情
'image', // 插入图片
'table', // 表格
'code', // 插入代码
'undo', // 撤销
'redo' // 重复
];
editor.create(); // 创建富文本实例
})
function contentChange(html) {
editorContent.value = html;
emit('contentChange', editorContent.value);
}
</script>
<template>
<div id="wangeditor">
<div ref="editorElem" style="text-align:left;"></div>
</div>
</template>
上述代码有详细的注释,通过一些简单的配置即可使用此富文本组件。
下面是修改后的GoodsDetailSetting.vue
文件的代码:
<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import GoodsEdit from './GoodsEdit.vue'
const router = useRouter()
const content = ref("")
// 富文本组件内容变化的回调
function contentChange(c) {
content.value = c;
}
function cancel() {
router.go(-1)
}
function submit() {
ElMessage({
type:'success',
message:'设置详情HTML:' + content.value
})
}
</script>
<template>
<div style="margin-bottom:20px">
<goods-edit @contentChange="contentChange"></goods-edit>
</div>
<el-container class="content-row">
<el-button type="success" plain @click="submit">提交</el-button>
<div style="margin-left:40px"></div>
<el-button type="warning" plain @click="cancel">取消</el-button>
</el-container>
</template>
运行代码,商品详情编辑页如图所示。
商品分类管理模块的实现相对简单,通过一个列表展示已有的分类,并提供删除和新增分类的功能。在 goods
文件夹下新建一个名为 GoodsCategory.vue
的文件,编写如下示例代码:
<script setup>
import { ref } from 'vue'
import { ElMessageBox } from 'element-plus'
const categoryList = ref([
{id:1231, name:"男装", manager:"管理员用户01"},
{id:1131, name:"男鞋", manager:"管理员用户01"},
{id:1031, name:"帽子", manager:"管理员用户01"}
])
function deleteCategory(index) {
categoryList.value.splice(index,1)
}
function addCategory() {
ElMessageBox.prompt('请输入分类名','新增分类',{
confirmButtonText: '确定',
cancelButtonText: '取消',
}).then(({value})=>{
categoryList.value.push({
id:1000,
name:value,
manager:"管理员用户01"
})
});
}
</script>
<template>
<div class="content-container" direction="vertical">
<el-container class="content-row">
<el-button type="primary" @click="addCategory">添加分类</el-button>
</el-container>
<div>
<el-table :data="categoryList" tooltip-effect="dark" style="width: 100%">
<el-table-column label="分类ID" width="100" prop="id"></el-table-column>
<el-table-column label="分类名称" width="100" prop="name"></el-table-column>
<el-table-column label="分类负责人" width="500" prop="manager"></el-table-column>
<el-table-column label="操作" width="200" prop="time">
<template #default="scope">
<el-button size="small" @click="deleteCategory(scope.$index)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
</div>
</template>
<template>
):
el-button
用于触发新增分类的按钮。el-table
用于展示分类列表,包含分类ID、分类名称、分类负责人和操作列。el-table-column
定义表格的列结构。@click
事件绑定 deleteCategory
方法。<script setup>
):
ref
定义 categoryList
,存储分类数据。deleteCategory
方法用于删除指定索引的分类。addCategory
方法使用 ElMessageBox.prompt
弹出对话框,获取用户输入的分类名,并将新分类添加到 categoryList
。<style scoped>
):
不要忘记在 Router.js
中补充对应的路由配置,确保可以从其他页面跳转到商品分类管理页面。
示例路由配置:
import { createRouter, createWebHistory } from 'vue-router';
import GoodsCategory from './components/goods/GoodsCategory.vue';
const routes = [
// 其他路由配置
{
path:'category',
component:GoodsCategory,
name:"GoodsCategory"
}
];
const router = createRouter({
history: createWebHistory(),
routes
});
export default router;
运行上面的代码后,你应该会看到一个商品分类管理页面,能够展示已有的分类,并提供删除和新增分类的功能。效果如图所示。