uniapp+node.js前后端平台的店铺展示(左商品分类右该分类商品+上方不同的tab)(社区管理平台的小程序)
@TOC
👍 点赞,你的认可是我创作的动力!
⭐️ 收藏,你的青睐是我努力的方向!
✏️ 评论,你的意见是我进步的财富!
0前提
温馨提示:我做的思路可能是复杂化了或者说代码写的不规范,如果你觉得可以更加简便的话欢迎分享到评论区或者自己改写一下我的代码,我的后端是写的很简单的没有什么路由分发是直接写的,你可以自由优化,以及在需要验证用户是否登录和验证用户token是否正确的我没有进行验证,你们可以自行添加 小程序的其他部分你可以看看我往期的文章
1.一些准备
1.1表
表4.13 店铺表shop
字段名称 | 类型(长度) | 允许空 | 主键 | 外键 | 自增 | 唯一 | 说明 |
---|---|---|---|---|---|---|---|
id | int | 否 | 是 | 否 | 是 | 是 | 店铺id |
name | varchar(20) | 否 | 否 | 否 | 否 | 否 | 名字 |
classification | varchar(20) | 否 | 否 | 否 | 否 | 否 | 分类 |
slogan | varchar(20) | 否 | 否 | 否 | 否 | 否 | 标语 |
introduction | varchar(20) | 否 | 否 | 否 | 否 | 否 | 介绍 |
phone | varchar(20) | 否 | 否 | 否 | 否 | 否 | 电话 |
address | varchar(20) | 否 | 否 | 否 | 否 | 否 | 地址 |
score | int | 是 | 否 | 否 | 否 | 否 | 评分 |
coverImage | varchar(50) | 否 | 否 | 否 | 否 | 否 | 封面表 |
state | int | 否 | 否 | 否 | 否 | 否 | 审核状态 |
businessId | int | 否 | 否 | 是 | 否 | 否 | 商户id |
communityId | int | 否 | 否 | 是 | 否 | 否 | 小区id |
creatTime | timestamp | 否 | 否 | 是 | 否 | 否 | 创建时间 |
updateTime | timestamp | 否 | 否 | 是 | 否 | 否 | 更新时间 |
表4.14 店铺评论表shopComment
字段名称 | 类型(长度) | 允许空 | 主键 | 外键 | 自增 | 唯一 | 说明 |
---|---|---|---|---|---|---|---|
id | int | 否 | 是 | 否 | 是 | 是 | 店铺评论id |
content | varchar(200) | 否 | 否 | 否 | 否 | 否 | 评论内容 |
images | varchar(200) | 是 | 否 | 否 | 否 | 否 | 详情图片地址(可选) |
score | int | 否 | 否 | 否 | 否 | 否 | 评分 |
shopId | int | 否 | 否 | 是 | 否 | 否 | 店铺id(外键关联店铺表) |
userId | int | 否 | 否 | 是 | 否 | 否 | 用户id(外键关联用户表) |
creatTime | timestamp | 否 | 否 | 否 | 否 | 否 | 创建时间 |
updateTime | timestamp | 是 | 否 | 否 | 否 | 否 | 更新时间(可选) |
注意:我将content
字段的长度从varchar(20)
修改为varchar(200)
,因为通常评论内容会超过20个字符。
表4.15 商品分类表shopClassification
字段名称 | 类型(长度) | 允许空 | 主键 | 外键 | 自增 | 唯一 | 说明 |
---|---|---|---|---|---|---|---|
id | int | 否 | 是 | 否 | 是 | 是 | 商品分类id |
name | varchar(20) | 否 | 否 | 否 | 否 | 否 | 分类名字 |
shopId | int | 否 | 否 | 是 | 否 | 否 | 店铺id(外键关联店铺表) |
creatTime | timestamp | 否 | 否 | 否 | 否 | 否 | 创建时间 |
updateTime | timestamp | 是 | 否 | 否 | 否 | 否 | 更新时间(可选) |
表4.16 商品表commodity
字段名称 | 类型(长度) | 允许空 | 主键 | 外键 | 自增 | 唯一 | 说明 |
---|---|---|---|---|---|---|---|
id | int | 否 | 是 | 否 | 是 | 是 | 商品id |
name | varchar(20) | 否 | 否 | 否 | 否 | 否 | 商品名字 |
price | double | 否 | 否 | 否 | 否 | 否 | 商品价格 |
stock | int | 否 | 否 | 否 | 否 | 否 | 商品库存 |
coverImage | varchar(50) | 否 | 否 | 否 | 否 | 否 | 商品封面图片地址 |
images | varchar(200) | 是 | 否 | 否 | 否 | 否 | 商品详情图片地址(可选) |
sale | int | 是 | 否 | 否 | 否 | 否 | 商品销量(可选) |
state | int | 否 | 否 | 否 | 否 | 否 | 商品上下架状态 |
shopId | int | 否 | 否 | 是 | 否 | 否 | 店铺id(外键关联店铺表) |
shopCmomentId | int | 否 | 否 | 是 | 否 | 否 | 商品分类id(外键关联商品分类表) |
creatTime | timestamp | 否 | 否 | 否 | 否 | 否 | 创建时间 |
updateTime | timestamp | 是 | 否 | 否 | 否 | 否 | 更新时间(可选) |
1.2总体思路
描述:用户在进入小区店铺的时候可以看到店铺的展示,名字、标语、评分等,中间放三个tab可以任意切换,第一个让用户看到的就是商品列表,左边是商品分类右边是商品列表,点击商品任意分类会会显示对应商品分类的商品,第二个就是让用户看到这个小区店铺的评价,用户在完成订单之后也可以对店铺进行评分和评论,第三个就是让用户看到这个小区店铺的商家详情(地址、电话等等信息)
实现:当用户进入店铺页的时候,传这个店铺id,获取这个店铺的展示信息,后端接口多个查询然后集合返回一个信息。前端利用选项卡组件形成多个tab,然后用css样式写成左右分区。
2.前端
代码实现:
<view>
<!-- 头部店铺信息 -->
<view class="head">
<view class="shop-info">
<text class="shop-name">{{ shop.shopInfo.name }}</text>
<uni-rate :touchable="false" :value="shop.shopInfo.score" @change="onChange" />
</view>
<img :src="shop.shopInfo.coverImage" class="shop-avatar" alt="店铺头像" />
</view>
<!-- 顶部通知栏 -->
<uni-notice-bar show-icon scrollable :text="shop.shopInfo.slogan" />
<!-- 选项卡 -->
<uni-section title="店铺详情" type="line">
<view class="uni-padding-wrap uni-common-mt">
<uni-segmented-control :current="current" :values="items" :style-type="styleType" :active-color="activeColor" @clickItem="onClickItem" />
</view>
<view class="content">
<view v-if="current === 0">
<!-- 左侧商品分类菜单导航 -->
<view class="classification-nav">
<view
class="classification-item"
v-for="(classification, index) in shop.commodityClassifications"
:key="index"
:class="{ active: currentCategory.id === classification.id }"
@click="selectCategory(classification)"
>
{{ classification.name }}
</view>
</view>
<!-- 右侧商品列表 -->
<view class="commodity-list" :style="{ height: commodityListHeight }">
<view v-for="(commodity, index) in currentCategory.commodities" :key="index" class="commodity-item" @click="">
<image :src="commodity.coverImage" class="commodity-cover" />
<text class="commodity-name">{{ commodity.name }}</text>
<text class="commodity-price">价格:{{ commodity.price }}</text>
<text class="commodity-sale">销量:{{ commodity.sale }}</text>
<!-- 加入购物车按钮 -->
<button class="add-to-cart-button" @click="addToCart(commodity.id)">加入购物车</button>
</view>
</view>
</view>
<!-- 店铺评论 -->
<view v-if="current === 1">
<uni-card
:title="comment.name"
:sub-title="'评分:' + comment.score"
:extra="comment.createTime"
:thumbnail="comment.avatar"
@click="onClick"
v-for="(comment, index) in shop.commentList"
:key="index"
>
<text class="uni-body">{{ comment.content }}</text>
</uni-card>
</view>
<!-- 店铺介绍 -->
<view v-if="current === 2">
<view class="content-text">店铺介绍:{{ shop.shopInfo.introduction }}</view>
<view class="content-text">店铺地址:{{ shop.shopInfo.address }}</view>
<view class="content-text">店铺电话:{{ shop.shopInfo.phone }}</view>
<!-- 其他店铺信息字段 -->
</view>
</view>
</uni-section>
<!-- 商品导航栏 -->
<view class="goods-nav" style="position: fixed; bottom: 0; left: 0; right: 0">
<uni-goods-nav :options="options" :fill="true" :button-group="buttonGroup" @click="gogowuche" @buttonClick="gogowuche" />
</view>
</view>
</template>
export default {
data() {
return {
shop: {},
currentCategory: {}, // 当前选中的商品分类
options: [
// {
// icon: 'headphones',
// text: '客服'
// },
// {
// icon: 'shop',
// text: '店铺'
// },
{
icon: 'cart',
text: '购物车',
info: 0
}
],
buttonGroup: [
{
text: '立即购买',
backgroundColor: '#ffa200',
color: '#fff'
}
],
items: ['商品', '评论', '商家'],
current: 0,
activeColor: '#007aff',
styleType: 'button',
// 新增变量
commodityListHeight: 'calc(100vh - 120rpx)' // 120rpx 是商品导航栏的高度
};
},
onLoad(option) {
const shopId = option.id;
this.shopDetails(shopId);
this.options[0].info = 0;
},
methods: {
// 选中商品分类
selectCategory(classification) {
this.currentCategory = classification;
},
onClickItem(e) {
if (this.current !== e.currentIndex) {
this.current = e.currentIndex;
}
},
//获取店铺详情的信息
async shopDetails(shopId) {
try {
const res = await this.$myRequest({
method: 'get',
url: `/shopDetails?shopId=${shopId}`
});
console.log(res.data.data);
// 假设后端返回错误时包含一个名为 'error' 的字段
if (res.error) {
this.$message.error(res.error);
} else {
// 更新店铺详情数据
const shopData = res.data.data;
// 默认选中第一个商品分类
shopData.commodityClassifications.forEach((classification) => {
classification.commodities = shopData.commodities.filter((commodity) => commodity.shopCategoryId === classification.id);
});
this.currentCategory = shopData.commodityClassifications[0];
this.shop = shopData;
}
} catch (error) {
// 捕获异常,显示通用错误消息或者其他处理
console.error('请求发生错误:', error);
}
},
// 加入购物车
async addToCart(commodityId) {
this.options[0].info++;
try {
const res = await this.$myRequest({
method: 'post',
url: `/addToCart`,
data: {
userId: this.$store.state.user.id,
commodityId: commodityId
}
});
// 假设后端返回错误时包含一个名为 'error' 的字段
} catch (error) {
// 捕获异常,显示通用错误消息或者其他处理
console.error('请求发生错误:', error);
}
},
gogowuche() {
uni.navigateTo({
url: '/pages/gowuche/gowuche'
});
}
}
};
3.后端
(接口编写逻辑:接口名字-接收前端传值-sql语句-sql操作-返回信息) 接口分别是:获取特定小区店铺信息,将这个店铺选中的商品加入购物车。
代码实现:
// 获取店铺详情
app.get('/shopDetails', (req, res) => {
const shopId = req.query.shopId;
// 查询店铺信息
const shopQuery = `SELECT * FROM shop WHERE id = ?`;
connection.query(shopQuery, [shopId], (err, shopResults) => {
if (err) {
console.error('获取店铺信息失败:', err);
return res.status(500).json({
error: '获取店铺信息失败,请稍后重试'
});
}
if (shopResults.length === 0) {
return res.status(404).json({
error: '未找到该店铺信息'
});
}
const shopInfo = shopResults[0];
// 查询店铺的商品分类列表
const classificationQuery = `SELECT * FROM shopclassification WHERE shopId = ?`;
connection.query(classificationQuery, [shopId], (err, classificationResults) => {
if (err) {
console.error('获取商品分类列表失败:', err);
return res.status(500).json({
error: '获取商品分类列表失败,请稍后重试'
});
}
// 查询店铺的商品列表
const commodityQuery = `SELECT * FROM commodity WHERE shopId = ?`;
connection.query(commodityQuery, [shopId], (err, commodityResults) => {
if (err) {
console.error('获取商品列表失败:', err);
return res.status(500).json({
error: '获取商品列表失败,请稍后重试'
});
}
// 查询店铺的评论列表
const commentQuery = `SELECT shopcomment.*, user.avatar, user.name
FROM shopcomment
INNER JOIN user ON shopcomment.userId = user.id
WHERE shopcomment.shopId = ?`;
connection.query(commentQuery, [shopId], (err, commentResults) => {
if (err) {
console.error('获取店铺评论失败:', err);
return res.status(500).json({
error: '获取店铺评论失败,请稍后重试'
});
}
// 遍历结果集,仅修改创建时间字段
commentResults.forEach(commentResults => {
commentResults.createTime = formatTime(commentResults.createTime);
});
// 构造返回数据对象
const shopDetails = {
shopInfo: shopInfo,
commodityClassifications: classificationResults, // 商品分类列表
commodities: commodityResults, // 商品列表
commentList: commentResults // 店铺评论列表
};
// 返回店铺详情数据
res.json({
data: shopDetails
});
});
});
});
});
});
// 添加商品到购物车的接口
app.post('/addToCart', (req, res) => {
// 从请求体中获取商品id和用户id
const {
userId,
commodityId
} = req.body;
// 查询购物车中是否已存在该商品
const queryExistingProduct = 'SELECT * FROM shopcart WHERE userId = ? AND commodityId = ?';
connection.query(queryExistingProduct, [userId, commodityId], (err, results) => {
if (err) {
console.error('查询购物车失败:', err);
return res.status(500).json({
error: '查询购物车失败,请稍后重试'
});
}
// 如果购物车中已存在该商品,则更新数量
if (results.length > 0) {
const newQuantity = results[0].count + 1;
const updateQuery = 'UPDATE shopcart SET count = ? WHERE userId = ? AND commodityId = ?';
connection.query(updateQuery, [newQuantity, userId, commodityId], (err, results) => {
if (err) {
console.error('更新购物车失败:', err);
return res.status(500).json({
error: '更新购物车失败,请稍后重试'
});
}
res.status(200).json({
message: '商品数量已更新'
});
});
} else {
// 如果购物车中不存在该商品,则插入新记录
const insertQuery = 'INSERT INTO shopcart (userId, commodityId, count) VALUES (?, ?, 1)';
connection.query(insertQuery, [userId, commodityId], (err, results) => {
if (err) {
console.error('添加到购物车失败:', err);
return res.status(500).json({
error: '添加到购物车失败,请稍后重试'
});
}
res.status(200).json({
message: '商品已添加到购物车'
});
});
}
});
});
4.验证结果
5.每日一题:
v-model 是如何实现的
v-model 是 Vue.js 中的一个重要指令,用于实现表单输入元素和 Vue 实例的数据之间的双向绑定。它简化了表单元素和数据的同步过程。 v-model 的实现原理主要依赖于 Vue.js 的响应式系统和事件系统。 响应式系统:Vue.js 在初始化数据时,会通过 Object.defineProperty() 方法将数据转化为 getter 和 setter。当数据变化时,会触发 setter,通知所有订阅该数据的 Watcher,执行对应的更新函数。 事件系统:当输入框的值发生改变时(比如用户输入或者选择了一个新的值),会触发一个 input 事件(或者其他事件,取决于元素类型)。在事件处理函数中,Vue.js 会通过 setter 将新的值赋给数据。 结合这两个系统,v-model 的实现原理可以简单概括为: 当输入框的值发生改变时,会触发一个 input 事件(或其他事件),在事件处理函数中,Vue.js 会通过 setter 将新的值赋给数据。 当数据发生改变时(比如通过 Ajax 获取了新的数据),会触发 getter,获取新的值,并通过 DOM 操作更新到输入框中。 这样,就实现了数据和输入框的双向绑定,即 v-model。 语法糖 在编程中,“语法糖”(Syntactic Sugar)是指一种编程语法,这种语法对语言的功能并没有影响,但是更方便程序员使用。v-model 就是一个典型的语法糖。 对于表单输入元素,我们通常需要监听元素的输入事件,并在事件处理函数中更新数据;同时,当数据发生变化时,我们也需要通过 DOM 操作更新元素的显示值。这些操作比较繁琐,而且容易出错。Vue.js 的 v-model 指令就将这些操作封装在一起,让我们只需要写一行代码就可以实现双向绑定,大大简化了我们的工作。因此,v-model 可以看作是一个语法糖。 例如,下面的代码:
<input type="text" v-model="message">
实际上等价于下面的代码:
<input type="text" :value="message" @input="message = $event.target.value">
但是使用 v-model 语法更简洁、更易于理解。
转载自:https://juejin.cn/post/7371288282606993420