Springboot + vue + uni-app小程序web端全套家具商场

发布于:2025-07-01 ⋅ 阅读:(20) ⋅ 点赞:(0)

Springboot + vue + uni-app小程序web端全套家具商场

1、项目概述

​ 这是一个基于SpringBoot + Vue3 + uni-app的全栈家具电商平台,包含Web后台管理系统和微信小程序端,专注于家具产品的在线销售与管理。平台实现了从商品管理、订单处理到用户交互的完整电商业务流程。

2、技术栈

后端技术栈

  • 核心框架: Spring Boot 3
  • 数据库: MySQL 8.0
  • ORM框架: MyBatis-Plus
  • 认证授权: Spring Security + JWT
  • 缓存: Redis
  • 文件存储: 阿里云OSS/七牛云

前端管理系统

  • 前端框架: Vue 3 + Composition API
  • UI组件库: Element Plus
  • 状态管理: Pinia
  • 路由: Vue Router
  • HTTP客户端: Axios
  • 可视化: ECharts
  • 构建工具: Vite

微信小程序端

  • 开发框架: uni-app (基于Vue.js)
  • UI组件库: uView UI
  • 状态管理: Vuex
  • 网络请求: uni.request封装
  • 推送通知: 微信模板消息
3、系统功能模块

后台管理系统

商品管理:家具分类管理、商品SPU/SKU管理、商品上下架、商品评价管理

订单管理:订单列表与状态跟踪、退款/退货处理、订单统计与分析

用户管理:用户维护、用户行为分析

内容管理:首页轮播图配置、家具搭配推荐

数据统计:销售数据可视化、用户增长分析、商品热度排行

小程序端

首页:个性化推荐、促销活动展示、分类快捷入口

商品模块:家具分类浏览、商品搜索与筛选、商品详情、收藏与分享

购物流程:购物车管理、地址选择、支付、订单状态追踪

用户中心:个人信息管理、订单历史、收藏夹

4、项目截图
4.1、web端

登录页

image-20250630202657095

数据看板

主页推荐

image-20250630202806227

订单管理

image-20250630202836713

分类管理

image-20250630202944197

商品管理

image-20250630203042388

4.2、小程序端

登录页

image-20250630203508841

首页

image-20250630203221260

分类页

image-20250630203310539

详情页

image-20250630203354423

个人页面

image-20250630203433891

5、核心代码
5.1、管理端

项目结构

image-20250630204728940

核心代码

<template>
  <div class="dashboard-container">
    <!-- 顶部卡片区域 -->
    <el-row :gutter="20" class="mb-8">
      <el-col v-for="(item, index) in statItems" :key="index" :xs="24" :sm="12" :md="8" :lg="4" class="mb-4">
        <el-card shadow="hover" class="stat-card">
          <div class="stat-value">{{ dashboardData[item.key] }}</div>
          <div class="stat-label">{{ item.label }}</div>
        </el-card>
      </el-col>
    </el-row>

    <!-- 图表区域 -->
    <el-row :gutter="20" class="chart-row">
      <el-col :xs="12" :sm="24" :md="12" class="mb-8">
        <el-card shadow="hover">
          <template #header>
            <span class="chart-title">近七日订单趋势</span>
          </template>
          <div ref="orderChart" style="height: 400px;"></div>
        </el-card>
      </el-col>

      <el-col :xs="24" :sm="24" :md="12" class="mb-8">
        <el-card shadow="hover">
          <template #header>
            <span class="chart-title">近七日用户趋势</span>
          </template>
          <div ref="userChart" style="height: 400px;"></div>
        </el-card>
      </el-col>
    </el-row>
  </div>
</template>


<script setup>
import { ref, onMounted, watch, onActivated } from 'vue'
import { useRoute } from 'vue-router'
import * as echarts from 'echarts'
import { DashBoardAPI } from '@/api/admin/dashboard'

const route = useRoute()
let orderChartInstance = null
let userChartInstance = null
// 新增加载状态
const loading = ref(false)
// 响应式数据
const dashboardData = ref({
  orderCount: 0,
  orderPreCount: 0,
  orderNowCount: 0,
  orderReceiptCount: 0,
  orderFinishCount: 0,
  orderCancelCount: 0,
  orderCountList: [],
  userCountList: []
})

const orderChart = ref(null)
const userChart = ref(null)

// 统计项配置
const statItems = ref([
  { label: '订单总数', key: 'orderCount' },
  { label: '待付款', key: 'orderPreCount' },
  { label: '待发货', key: 'orderNowCount' },
  { label: '待收货', key: 'orderReceiptCount' },
  { label: '已完成', key: 'orderFinishCount' },
  { label: '已取消', key: 'orderCancelCount' }
])

// 防抖函数
const debounce = (fn, delay = 300) => {
  let timer
  return (...args) => {
    clearTimeout(timer)
    timer = setTimeout(() => fn.apply(this, args), delay)
  }
}

const fetchData = async () => {
  try {
    loading.value = true
    const res = await DashBoardAPI()
    dashboardData.value = res.data.data
    initCharts()
  } finally {
    loading.value = false
  }
}


// 生成最近7天日期
const generateDates = () => {
  const dates = []
  for (let i = 6; i >= 0; i--) {
    const date = new Date()
    date.setDate(date.getDate() - i)
    dates.push(`${date.getMonth() + 1}/${date.getDate()}`)
  }
  return dates
}

// 初始化图表
const initCharts = () => {
  const dates = generateDates()

  // 订单图表
  const orderChartInstance = echarts.init(orderChart.value)
  orderChartInstance.setOption({
    xAxis: {
      type: 'category',
      data: dates,
      axisLabel: {
        color: '#666'
      }
    },
    yAxis: {
      type: 'value'
    },
    series: [{
      data: dashboardData.value.orderCountList,
      type: 'bar',
      itemStyle: {
        color: '#409EFF'
      },
      barWidth: '30%'
    }],
    tooltip: {
      trigger: 'axis'
    },
    grid: {
      left: '3%',
      right: '3%',
      bottom: '3%',
      containLabel: true
    }
  })

  // 用户图表
  const userChartInstance = echarts.init(userChart.value)
  userChartInstance.setOption({
    xAxis: {
      type: 'category',
      data: dates,
      axisLabel: {
        color: '#666'
      }
    },
    yAxis: {
      type: 'value'
    },
    series: [{
      data: dashboardData.value.userCountList,
      type: 'bar',
      itemStyle: {
        color: '#67C23A'
      },
      barWidth: '30%'
    }],
    tooltip: {
      trigger: 'axis'
    },
    grid: {
      left: '3%',
      right: '3%',
      bottom: '3%',
      containLabel: true
    }
  })
}

// 监听路由变化
watch(
  () => route.path,
  (newVal, oldVal) => {
    if (newVal === '/dashboard') { // 根据实际路由路径调整
      debounce(fetchData)()
    }
  }
)

// 处理keep-alive缓存
onActivated(() => {
  if (orderChartInstance) orderChartInstance.dispose()
  if (userChartInstance) userChartInstance.dispose()
  fetchData()
})

onMounted(() => {
  fetchData()
})

// 添加窗口resize监听
const handleResize = debounce(() => {
  orderChartInstance?.resize()
  userChartInstance?.resize()
}, 200)

window.addEventListener('resize', handleResize)
</script>

<style scoped>
.dashboard-container {
  padding: 20px;
  max-width: 1600px;
  margin: 0 auto;
}

.stat-card {
  text-align: center;
  transition: transform 0.3s;
  margin-bottom: 16px;
}

.stat-card:hover {
  transform: translateY(-3px);
}

.chart-row {
  display: flex;
  flex-wrap: wrap;
}

/* 响应式调整 */
@media (max-width: 768px) {
  .el-col-md-12 {
    max-width: 100%;
    flex: 0 0 100%;
  }

  .stat-card {
    margin-bottom: 12px;
  }
}

@media (min-width: 1200px) {
  .el-col-lg-8 {
    max-width: 33.3333%;
    flex: 0 0 33.3333%;
  }
}

.dashboard-container {
  padding: 20px;
}

.stat-card {
  text-align: center;
  transition: transform 0.3s;
}

.stat-card:hover {
  transform: translateY(-5px);
}

.stat-value {
  font-size: 24px;
  font-weight: bold;
  color: #409EFF;
  margin-bottom: 8px;
}

.stat-label {
  color: #666;
  font-size: 14px;
}

.chart-title {
  font-size: 16px;
  font-weight: bold;
  color: #333;
}

.mb-8 {
  margin-bottom: 32px;
}
</style>
<template>
    <div class="product-container">
        <!-- 页面标题 -->
        <el-card class="header-card">
            <template #header>
                <div class="card-header">
                    <h2><el-icon>
                            <Goods />
                        </el-icon> 商品管理</h2>
                    <el-button type="primary" :icon="Plus" @click="openDialog">添加商品</el-button>
                </div>
            </template>

            <!-- 搜索区域 -->
            <div class="search-area">
                <el-input v-model="searchText" placeholder="请输入关键词搜索(名称、描述)" clearable class="search-input"
                    @clear="handleSearch" @keyup.enter="handleSearch">
                    <template #prefix>
                        <el-icon>
                            <Search />
                        </el-icon>
                    </template>
                    <template #append>
                        <el-button @click="handleSearch">搜索</el-button>
                    </template>
                </el-input>
            </div>
        </el-card>

        <!-- 数据表格 -->
        <el-card class="table-card">
            <el-table :data="currentPageData" border stripe highlight-current-row style="width: 100%"
                v-loading="loading" :header-cell-style="{ background: '#f5f7fa', color: '#606266' }">
                <el-table-column label="序号" width="60">
                    <template #default="{ $index }">
                        {{ (currentPage - 1) * pageSize + $index + 1 }}
                    </template>
                </el-table-column>
                <el-table-column prop="name" label="商品名称" width="150">
                    <template #default="{ row }">
                        <div class="product-name">
                            <el-tag size="small" effect="plain" class="product-tag">商品</el-tag>
                            {{ row.name }}
                        </div>
                    </template>
                </el-table-column>
                <el-table-column prop="description" label="商品描述" show-overflow-tooltip width="200" />
                <!-- 图片列 -->
                <el-table-column label="图片" width="180">
                    <template #default="{ row }">
                        <div class="image-container">
                            <el-image v-for="(img, index) in row.images" :key="index" :src="img"
                                :preview-src-list="row.images" :initial-index="index" fit="cover" class="product-image"
                                hide-on-click-modal>
                                <template #error>
                                    <div class="image-error">
                                        <el-icon>
                                            <Picture />
                                        </el-icon>
                                    </div>
                                </template>
                            </el-image>
                        </div>
                    </template>
                </el-table-column>
                <el-table-column prop="price" label="价格" width="100">
                    <template #default="{ row }">
                        <span class="price-tag">¥{{ row.price.toFixed(2) }}</span>
                    </template>
                </el-table-column>
                <el-table-column prop="stock" label="库存" width="100">
                    <template #default="{ row }">
                        <el-tag :type="row.stock > 10 ? 'success' : row.stock > 0 ? 'warning' : 'danger'"
                            effect="light">
                            {{ row.stock }}
                        </el-tag>
                    </template>
                </el-table-column>
                <el-table-column prop="categoryName" label="分类" width="120">
                    <template #default="{ row }">
                        <el-tag effect="plain" size="small">{{ row.categoryName }}</el-tag>
                    </template>
                </el-table-column>
                <el-table-column prop="createTime" label="创建时间" width="160">
                    <template #default="{ row }">
                        <el-tooltip :content="row.createTime" placement="top">
                            <span>{{ formatDate(row.createTime) }}</span>
                        </el-tooltip>
                    </template>
                </el-table-column>
                <!-- 规格列修改 -->
                <el-table-column label="规格" width="280">
                    <template #default="{ row }">
                        <div class="spec-container">
                            <div v-for="(spec, index) in row.groupedSpecs" :key="index" class="spec-item">
                                <el-tag size="small" effect="plain" type="info" class="spec-tag">{{ spec.name
                                }}:</el-tag>
                                <span class="spec-values">{{ spec.values.join('、') }}</span>
                            </div>
                        </div>
                    </template>
                </el-table-column>
                <!-- 属性列修改 -->
                <el-table-column label="属性" width="280">
                    <template #default="{ row }">
                        <div class="attr-container">
                            <div v-for="(attr, index) in row.attributeList" :key="index" class="attr-item">
                                <el-tag size="small" effect="plain" type="success" class="attr-tag">{{ attr.name
                                }}</el-tag>
                                <span class="attr-value">{{ attr.value }}</span>
                            </div>
                        </div>
                    </template>
                </el-table-column>
                <el-table-column label="操作" width="100" fixed="right">
                    <template #default="{ row }">
                        <el-button-group>
                            <!-- <el-button type="primary" size="small" :icon="Edit" plain>编辑</el-button> -->
                            <el-button type="danger" size="small" :icon="Delete" plain @click="handleDelete(row.id)">删除</el-button>
                        </el-button-group>
                    </template>
                </el-table-column>
            </el-table>

            <!-- 分页 -->
            <div class="pagination-container">
                <el-pagination background layout="total, sizes, prev, pager, next, jumper" :total="filteredData.length"
                    v-model:current-page="currentPage" v-model:page-size="pageSize" :page-sizes="[5, 10, 20, 50]"
                    @size-change="handleSizeChange" @current-change="handleCurrentChange" />
            </div>
        </el-card>

        <!-- 新增商品对话框 -->
        <el-dialog v-model="dialogVisible" title="新增商品" width="800px">
            <el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
                <!-- 商品基本信息 -->
                <el-form-item label="商品名称" prop="name">
                    <el-input v-model="form.name" placeholder="请输入商品名称" />
                </el-form-item>
                <el-form-item label="商品描述" prop="description">
                    <el-input v-model="form.description" type="textarea" placeholder="请输入商品描述" />
                </el-form-item>

                <!-- 价格库存 -->
                <el-row>
                    <el-col :span="12">
                        <el-form-item label="价格" prop="price">
                            <el-input-number v-model="form.price" :min="0" :precision="2" />
                        </el-form-item>
                    </el-col>
                    <el-col :span="12">
                        <el-form-item label="库存" prop="stock">
                            <el-input-number v-model="form.stock" :min="0" />
                        </el-form-item>
                    </el-col>
                </el-row>

                <!-- 分类选择 -->
                <el-form-item label="商品分类" prop="cascaderValue">
                    <el-cascader v-model="form.cascaderValue" :options="cascaderOptions" :props="cascaderProps"
                        placeholder="请选择商品分类" clearable />
                </el-form-item>

                <!-- 图片上传 -->
                <el-form-item label="商品图片" prop="images">
                    <el-upload v-model:file-list="fileList" multiple list-type="picture-card" :auto-upload="false"
                        :on-change="handleUploadChange" :on-remove="handleRemove">
                        <el-icon>
                            <Plus />
                        </el-icon>
                    </el-upload>
                </el-form-item>

                <!-- 规格管理 -->
                <el-form-item label="商品规格">
                    <div v-for="(spec, index) in form.specs" :key="index" class="spec-item">
                        <el-input v-model="spec.name" placeholder="规格名称" style="width: 120px" />
                        <el-input v-model="spec.values" placeholder="多个值用逗号隔开"
                            style="width: 200px; margin-left: 10px" />
                        <el-button type="danger" circle :icon="Delete" @click="removeSpec(index)"
                            style="margin-left: 10px" />
                    </div>
                    <el-button type="primary" @click="addSpec" :icon="Plus">添加规格</el-button>
                </el-form-item>

                <!-- 商品属性 -->
                <el-form-item label="商品属性">
                    <div v-for="(attr, index) in form.attributes" :key="index" class="attr-item">
                        <el-input v-model="attr.name" placeholder="属性名称" style="width: 120px" />
                        <el-input v-model="attr.value" placeholder="属性值" style="width: 200px; margin-left: 10px" />
                        <el-button type="danger" circle :icon="Delete" @click="removeAttr(index)"
                            style="margin-left: 10px" />
                    </div>
                    <el-button type="primary" @click="addAttr" :icon="Plus">添加属性</el-button>
                    <el-form-item label="是否热门">
                        <el-switch v-model="form.isHot" active-text="是" inactive-text="否" :active-value="1"
                            :inactive-value="0" />
                    </el-form-item>
                </el-form-item>
            </el-form>

            <template #footer>
                <el-button @click="dialogVisible = false">取消</el-button>
                <el-button type="primary" @click="submitForm">确认提交</el-button>
            </template>
        </el-dialog>
    </div>
</template>

<script setup>
import { ref, computed, onMounted, reactive } from 'vue'
import { useRoute } from 'vue-router' // 引入 useRoute
import {
    Search, Plus, Delete, Goods,
    Picture
} from '@element-plus/icons-vue'
import { getProductsAPI } from '@/api/product/product'
import { ElMessage, ElMessageBox } from 'element-plus'
import { getCategoriesAPI } from '@/api/category/category'
//上��图片
import { uploadFile } from '@/api/baseApi'
//新增接口
import { addProductAPI, deleteProductAPI } from '@/api/product/product'

const route = useRoute() // 获取路由实例
const categoryId = computed(() => route.params.id) // 获取路由参数 id
const products = ref([])
const searchText = ref('')
const currentPage = ref(1)
const pageSize = ref(5)
const loading = ref(false)
const categoryOptions = ref([])

// 在获取数据时处理规格分组
const getProductsData = async () => {
    loading.value = true
    try {
        const res = await getProductsAPI()
        products.value = res.data.data.map(product => {
            // 规格分组处理
            const groupedSpecs = product.specList.reduce((acc, spec) => {
                const existing = acc.find(item => item.name === spec.name)
                if (existing) {
                    existing.values.push(spec.value)
                } else {
                    acc.push({ name: spec.name, values: [spec.value] })
                }
                return acc
            }, [])

            return {
                ...product,
                groupedSpecs  // 添加分组后的规格数据
            }
        })
        ElMessage.success('商品数据加载成功')
    } catch (error) {
        ElMessage.error('获取商品数据失败')
        console.error(error)
    } finally {
        loading.value = false
    }
}

//获取新增分类
const categoryData = async () => {
    try {
        const res = await getCategoriesAPI()
        categoryOptions.value = res.data.data
    } catch (error) {
        console.error('获取分类数据失败', error)
    }
}

// 搜索处理
const filteredData = computed(() => {
    let result = products.value

    // 根据路由参数筛选商品
    result = result.filter(item => item.categoryParentId == categoryId.value)

    // 文本搜索
    if (searchText.value) {
        const search = searchText.value.toLowerCase()
        result = result.filter(item => {
            return (
                item.name.toLowerCase().includes(search) ||
                item.description.toLowerCase().includes(search)
            )
        })
    }

    return result
})

// 当前页数据
const currentPageData = computed(() => {
    return filteredData.value.slice(
        (currentPage.value - 1) * pageSize.value,
        currentPage.value * pageSize.value
    )
})

// 分页事件处理
const handleSizeChange = (val) => {
    pageSize.value = val
    currentPage.value = 1
}

const handleCurrentChange = (val) => {
    currentPage.value = val
}

// 搜索事件
const handleSearch = () => {
    currentPage.value = 1
}

// 格式化日期
const formatDate = (dateString) => {
    const date = new Date(dateString)
    return date.toLocaleDateString('zh-CN', {
        year: 'numeric',
        month: '2-digit',
        day: '2-digit',
        hour: '2-digit',
        minute: '2-digit'
    })
}

// 对话框相关状态
const dialogVisible = ref(false)
const formRef = ref(null)

// 表单数据
const form = reactive({
    name: '',
    description: '',
    price: 0,
    stock: 0,
    cascaderValue: [], // 级联选择值
    isHot: 0,
    images: [],
    specs: [],
    attributes: []
})

// 文件列表
const fileList = ref([])

// 分类数据格式转换
const cascaderOptions = computed(() => {
    return categoryOptions.value.map(cat => ({
        value: cat.id,
        label: cat.categoryName,
        children: cat.children?.map(child => ({
            value: child.id,
            label: child.name
        })) || []
    }))
})

// 表单验证规则
const rules = reactive({
    name: [{ required: true, message: '请输入商品名称', trigger: 'blur' }],
    price: [{ required: true, message: '请输入价格', trigger: 'blur' }],
    stock: [{ required: true, message: '请输入库存', trigger: 'blur' }],
    cascaderValue: [{ required: true, message: '请选择商品分类', trigger: 'change' }]
})

// 图片上传处理
const handleUploadChange = async (file) => {
    try {
        const formData = new FormData()
        formData.append('file', file.raw)
        const res = await uploadFile(formData)
        form.images.push(res.data.data)
        ElMessage.success('图片上传成功')
    } catch (error) {
        ElMessage.error('图片上传失败')
        console.error(error)
    }
}

const handleRemove = (file) => {
    const index = form.images.indexOf(file.url)
    if (index > -1) {
        form.images.splice(index, 1)
    }
}

// 规格管理
const addSpec = () => {
    form.specs.push({ name: '', values: '' })
}

const removeSpec = (index) => {
    form.specs.splice(index, 1)
}

// 属性管理
const addAttr = () => {
    form.attributes.push({ name: '', value: '' })
}

const removeAttr = (index) => {
    form.attributes.splice(index, 1)
}

// 提交表单
const submitForm = async () => {
    try {
        await formRef.value.validate()

        const payload = {
            name: form.name,
            description: form.description,
            price: form.price,
            stock: form.stock,
            parentCategoryId: form.cascaderValue[0], // 第一个元素是父分类
            categoryId: form.cascaderValue[1],       // 第二个元素是子分类
            isHot: form.isHot ? 1 : 0,              // 处理是否热门
            images: form.images.join(','),          // 图片路径拼接
            spec: form.specs.map(spec => `${spec.name},${spec.values}`),
            attribute: form.attributes.map(attr => `${attr.name},${attr.value}`)
        }

        await addProductAPI(payload)
        ElMessage.success('商品添加成功')
        dialogVisible.value = false
        // 刷新商品列表
        getProductsData()
    } catch (error) {
        console.error('提交失败:', error)
        ElMessage.error('商品添加失败')
    }
}

const handleDelete = async (productId) => {
    try {
        // 添加确认对话框
        await ElMessageBox.confirm('确定要删除该商品吗?', '警告', {
            confirmButtonText: '确定',
            cancelButtonText: '取消',
            type: 'warning'
        })

        // 调用删除API
        await deleteProductAPI(productId)
        ElMessage.success('删除成功')
        // 重新获取商品列表数据
        await getProductsData()
    } catch (error) {
        // 处理用户取消的情况
        if (error !== 'cancel') {
            ElMessage.error('删除失败')
            console.error(error)
        }
    }
}

// 暴露打开对话框方法
const openDialog = () => {
    dialogVisible.value = true
    // 重置表单
    Object.assign(form, {
        name: '',
        description: '',
        price: 0,
        stock: 0,
        cascaderValue: [],
        images: [],
        specs: [],
        attributes: []
    })
    fileList.value = []
}

onMounted(() => {
    getProductsData()
    categoryData()
})
</script>

<style scoped>
.product-container {
    padding: 20px;
    background-color: #f5f7fa;
    min-height: calc(100vh - 120px);
}

.header-card {
    margin-bottom: 20px;
}

.card-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
}

.card-header h2 {
    margin: 0;
    font-size: 18px;
    display: flex;
    align-items: center;
    gap: 8px;
}

.search-area {
    display: flex;
    gap: 15px;
    flex-wrap: wrap;
    align-items: center;
}

.search-input {
    width: 350px;
}

.filter-select {
    width: 180px;
}

.stat-cards {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
    gap: 20px;
    margin-bottom: 20px;
}

.stat-card {
    text-align: center;
}

.stat-header {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    font-size: 14px;
    color: #606266;
}

.stat-value {
    font-size: 24px;
    font-weight: bold;
    color: #303133;
    margin-top: 10px;
}

.table-card {
    margin-bottom: 20px;
}

.pagination-container {
    margin-top: 20px;
    display: flex;
    justify-content: center;
}

.product-name {
    display: flex;
    align-items: center;
    gap: 8px;
}

.product-tag {
    font-size: 12px;
}

.price-tag {
    color: #f56c6c;
    font-weight: bold;
}

.spec-item,
.attr-item {
    display: flex;
    align-items: center;
    gap: 8px;
    margin-bottom: 5px;
}

.image-container {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
}

.product-image {
    width: 60px;
    height: 60px;
    border-radius: 4px;
    border: 1px solid #ebeef5;
    transition: transform 0.3s;
}

.product-image:hover {
    transform: scale(1.05);
    box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}

.image-error {
    display: flex;
    justify-content: center;
    align-items: center;
    width: 100%;
    height: 100%;
    background-color: #f5f7fa;
    color: #909399;
}

/* 响应式调整 */
@media (max-width: 768px) {
    .search-input {
        width: 100%;
    }

    .filter-select {
        width: 100%;
    }

    .stat-cards {
        grid-template-columns: repeat(2, 1fr);
    }
}

/* 规格样式 */
.spec-container {
    display: flex;
    flex-direction: column;
    gap: 6px;
}

.spec-item {
    display: flex;
    align-items: center;
    line-height: 1.4;
}

.spec-tag {
    flex-shrink: 0;
}

.spec-values {
    margin-left: 4px;
    word-break: break-word;
}

/* 属性样式 */
.attr-container {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 8px;
}

.attr-item {
    display: flex;
    align-items: center;
    gap: 4px;
    min-width: 0;
}

.attr-tag {
    flex-shrink: 0;
}

.attr-value {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* 响应式调整 */
@media (max-width: 768px) {
    .attr-container {
        grid-template-columns: 1fr;
    }
}

.image-container {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
}

.product-image {
    width: 60px;
    height: 60px;
    border-radius: 4px;
    border: 1px solid #ebeef5;
    transition: transform 0.3s;
    cursor: pointer;
}

.product-image:hover {
    transform: scale(1.05);
    box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}

.image-error {
    display: flex;
    justify-content: center;
    align-items: center;
    width: 100%;
    height: 100%;
    background-color: #f5f7fa;
    color: #909399;
}

.spec-item,
.attr-item {
    margin-bottom: 10px;
    display: flex;
    align-items: center;
}
</style>
5.2、小程序端

项目结构

image-20250630204934463

核心代码

<script setup lang="ts">
import { onLoad } from '@dcloudio/uni-app';
import {
  getProductDetailAPI,
  getProductByCategoryAPI,
  getProductSpecAPI,
  getProductSkuAPI
} from '@/services/product'
import { ref, computed } from 'vue'
import AddressPanel from './component/AddressPanel.vue'
import ServicePanel from './component/ServicePanel.vue'
import type { SkuPopupEvent, SkuPopupInstanceType } from '@/components/vk-data-goods-sku-popup/vk-data-goods-sku-popup';
import { postMemberCartAPI } from '@/services/cart'
import { useAddressStore } from '@/stores/modules/address'
const { safeAreaInsets } = uni.getSystemInfoSync()
const goods = ref(null)
const categoryProductData = ref([])
// 是否显示sku
const isShowSku = ref(false)
const localdata = ref({})
// 按钮模式
enum SkuMode {
  Both = 1,
  Cart = 2,
  Buy = 3,
}
const mode = ref<SkuMode>(SkuMode.Cart)
// 打开SKU弹窗修改按钮模式
const openSkuPopup = (val: SkuMode) => {
  // 显示SKU弹窗
  isShowSku.value = true
  // 修改按钮模式
  mode.value = val
}


const query = defineProps<{
  id: number
}>()

const getProductDetailData = async () => {
  try {
    const res = await getProductDetailAPI(query.id)
    goods.value = res.data.data
    await getProductByCategoryData()

    // 获取规格数据
    const spec = await getProductSpecAPI(query.id)
    // 记录规格顺序(重要!)
    const specOrder = spec.data.data.map(v => v.name)

    // 获取SKU数据
    const skus = await getProductSkuAPI(query.id)

    // 构建 localdata
    localdata.value = {
      id: goods.value?.id,
      name: goods.value?.name,
      imageUrl: goods.value?.images[0],
      spec_list: spec.data.data.map(v => ({
        name: v.name,
        list: [...new Set(v.values)].map(value => ({
          name: value,
          id: value // 保持id与sku_id_arr对应
        }))
      })),
      sku_list: skus.data.data.map(v => ({
        _id: v.id,
        goods_id: v.goodsId,
        goods_name: v.goodsName,
        image: v.image,
        price: v.price * 100,
        stock: v.stock,
        sku_name_arr: specOrder.map(specName => {
          const specItem = v.specs.find(s => s.name === specName)
          return specItem ? specItem.value : ''
        }),
        sku_id_arr: specOrder.map(specName => {
          const specItem = v.specs.find(s => s.name === specName)
          return specItem ? specItem.value : '' // 保持与sku_name_arr一致
        })
      }))
    }

    // 调试输出
    console.log('规格顺序:', specOrder)
    console.log('处理后的SKU数据:', JSON.stringify(localdata.value.sku_list))
  } catch (error) {
    console.error('请求失败:', error)
    uni.showToast({ title: '数据加载失败', icon: 'none' })
  }
}

// SKU组件实例
const skuPopupRef = ref<SkuPopupInstanceType>()
// 计算被选中的值
const selectArrText = computed(() => {
  return skuPopupRef.value?.selectArr?.join(' ').trim() || '请选择商品规格'
})

const getProductByCategoryData = async () => {
  try {
    // 使用可选链操作符和.value访问
    const categoryId = goods.value?.categoryId
    if (!categoryId) return // 确保categoryId存在

    const res = await getProductByCategoryAPI(categoryId)
    categoryProductData.value = res.data.data
  } catch (error) {
    console.error('请求失败:', error)
    uni.showToast({ title: '数据加载失败', icon: 'none' })
  }
}

// 轮播图变化事件
const currentIndex = ref(0)
const onChange = (e: any) => {
  currentIndex.value = e.detail.current
}
const onTapImage = (url: string) => {
  uni.previewImage({
    urls: goods.value!.images,
    current: url,
  })
}

const popup = ref()
// 弹出层条件渲染
const popupName = ref<'address' | 'service'>()
const openPopup = (name: typeof popupName.value) => {
  // 修改弹出层名称
  popupName.value = name
  // 打开弹出层
  popup.value?.open()
}

// 加入购物车事件
const onAddCart = async (ev: SkuPopupEvent) => {
  let res = await postMemberCartAPI({ skuId: ev._id, number: ev.buy_num })
  if (res.data.code === 200) {
    uni.showToast({ title: '加入购物车成功', icon: 'none' })
  } else {
    uni.showToast({ title: '加入购物车失败', icon: 'none' })
  }
  isShowSku.value = false
}

const onByNow = async (ev: SkuPopupEvent) => {
  uni.navigateTo({ url: '/pagesOrder/create/create?skuId=' + ev._id + '&number=' + ev.buy_num })
}


const addressStore = useAddressStore()

// 收货地址
const selectAddress = computed(() => {
  return addressStore.selectedAddress
})

onLoad(() => {
  getProductDetailData() // 只需要调用这一个,它内部会触发第二个请求
})
</script>

<template>
  <!-- sku弹窗组件 -->
  <!-- SKU弹窗组件 -->
  <vk-data-goods-sku-popup v-model="isShowSku" :localdata="localdata" :mode="mode" add-cart-background-color="#FFA868"
    buy-now-background-color="#27BA9B" ref="skuPopupRef" :actived-style="{
      color: '#27BA9B',
      borderColor: '#27BA9B',
      backgroundColor: '#E9F8F5',
    }" @add-cart="onAddCart" @buy-now="onByNow" />
  <scroll-view scroll-y class="viewport">
    <!-- 基本信息 -->
    <view class="goods">
      <!-- 商品主图 -->
      <view class="preview">
        <swiper circular @change="onChange">
          <swiper-item v-for="item in goods?.images" :key="item">
            <image @tap="onTapImage(item)" mode="aspectFill" :src="item" />
          </swiper-item>
        </swiper>
        <view class="indicator">
          <text class="current">{{ currentIndex + 1 }}</text>
          <text class="split">/</text>
          <text class="total">{{ goods?.images.length }}</text>
        </view>
      </view>

      <!-- 商品简介 -->
      <view class="meta">
        <view class="price">
          <text class="symbol">¥</text>
          <text class="number">{{ goods?.price }}</text>
        </view>
        <view class="name ellipsis">{{ goods?.name }}</view>
        <view class="desc"> {{ goods?.description }} </view>
      </view>

      <!-- 操作面板 -->
      <!-- 操作面板 -->
      <view class="action">
        <view class="action">
          <view @tap="openSkuPopup(SkuMode.Both)" class="item arrow">
            <text class="label">选择</text>
            <text class="text ellipsis"> {{ selectArrText }} </text>
          </view>
        </view>
        <view @tap="openPopup('address')" class="item arrow">
          <text class="label">送至</text>
          <text class="text ellipsis">
            {{ selectAddress ? `${selectAddress.fullLocation} ${selectAddress.address}` : '请选择收获地址' }}
          </text>
        </view>
        <view @tap="openPopup('service')" class="item arrow">
          <text class="label">服务</text>
          <text class="text ellipsis"> 无忧退 快速退款 免费包邮 </text>
        </view>
      </view>
    </view>

    <!-- 商品详情 -->
    <view class="detail panel">
      <view class="title">
        <text>详情</text>
      </view>
      <view class="content">
        <view class="properties" v-for="item in goods?.attributes" :key="item.id">
          <!-- 属性详情 -->
          <view class="item">
            <text class="label">{{ item.name }}</text>
            <text class="value">{{ item.value }}</text>
          </view>
        </view>
      </view>
    </view>

    <!-- 同类推荐 -->
    <view class="similar panel">
      <view class="title">
        <text>同类推荐</text>
      </view>
      <view class="content">
        <navigator v-for="item in categoryProductData" :key="item.id" class="goods" hover-class="none"
          :url="`/pages/goods/goods?id=${item.id}`">
          <image class="image" mode="aspectFill" :src="item.imageUrl"></image>
          <view class="name ellipsis">{{ item.name }}</view>
          <view class="price">
            <text class="symbol">¥</text>
            <text class="number">{{ item.price }}</text>
          </view>
        </navigator>
      </view>
    </view>
  </scroll-view>

  <!-- 用户操作 -->
  <view class="toolbar" :style="{ paddingBottom: safeAreaInsets?.bottom + 'px' }">
    <view class="icons">
      <button class="icons-button"><text class="icon-heart"></text>收藏</button>
      <button class="icons-button" open-type="contact">
        <text class="icon-handset"></text>客服
      </button>
      <navigator class="icons-button" url="/pages/cart/cart2" open-type="navigate">
        <text class="icon-cart"></text>购物车
      </navigator>
    </view>
    <view class="buttons">
      <view class="addcart" @tap="openSkuPopup(SkuMode.Cart)"> 加入购物车 </view>
      <view class="buynow" @tap="openSkuPopup(SkuMode.Buy)"> 立即购买 </view>
    </view>
  </view>

  <uni-popup ref="popup" type="bottom" background-color="#fff">
    <AddressPanel v-if="popupName === 'address'" @close="popup?.close()" />
    <ServicePanel v-if="popupName === 'service'" @close="popup?.close()" />
  </uni-popup>
</template>

<style lang="scss">
page {
  height: 100%;
  overflow: hidden;
  display: flex;
  flex-direction: column;
}

.viewport {
  background-color: #f4f4f4;
}

.panel {
  margin-top: 20rpx;
  background-color: #fff;

  .title {
    display: flex;
    justify-content: space-between;
    align-items: center;
    height: 90rpx;
    line-height: 1;
    padding: 30rpx 60rpx 30rpx 6rpx;
    position: relative;

    text {
      padding-left: 10rpx;
      font-size: 28rpx;
      color: #333;
      font-weight: 600;
      border-left: 4rpx solid #27ba9b;
    }

    navigator {
      font-size: 24rpx;
      color: #666;
    }
  }
}

.arrow {
  &::after {
    position: absolute;
    top: 50%;
    right: 30rpx;
    content: '\e6c2';
    color: #ccc;
    font-family: 'erabbit' !important;
    font-size: 32rpx;
    transform: translateY(-50%);
  }
}

/* 商品信息 */
.goods {
  background-color: #fff;

  .preview {
    height: 750rpx;
    position: relative;

    .image {
      width: 750rpx;
      height: 750rpx;
    }

    .indicator {
      height: 40rpx;
      padding: 0 24rpx;
      line-height: 40rpx;
      border-radius: 30rpx;
      color: #fff;
      font-family: Arial, Helvetica, sans-serif;
      background-color: rgba(0, 0, 0, 0.3);
      position: absolute;
      bottom: 30rpx;
      right: 30rpx;

      .current {
        font-size: 26rpx;
      }

      .split {
        font-size: 24rpx;
        margin: 0 1rpx 0 2rpx;
      }

      .total {
        font-size: 24rpx;
      }
    }
  }

  .meta {
    position: relative;
    border-bottom: 1rpx solid #eaeaea;

    .price {
      height: 130rpx;
      padding: 25rpx 30rpx 0;
      color: #fff;
      font-size: 34rpx;
      box-sizing: border-box;
      background-color: #35c8a9;
    }

    .number {
      font-size: 56rpx;
    }

    .brand {
      width: 160rpx;
      height: 80rpx;
      overflow: hidden;
      position: absolute;
      top: 26rpx;
      right: 30rpx;
    }

    .name {
      max-height: 88rpx;
      line-height: 1.4;
      margin: 20rpx;
      font-size: 32rpx;
      color: #333;
    }

    .desc {
      line-height: 1;
      padding: 0 20rpx 30rpx;
      font-size: 24rpx;
      color: #cf4444;
    }
  }

  .action {
    padding-left: 20rpx;

    .item {
      height: 90rpx;
      padding-right: 60rpx;
      border-bottom: 1rpx solid #eaeaea;
      font-size: 26rpx;
      color: #333;
      position: relative;
      display: flex;
      align-items: center;

      &:last-child {
        border-bottom: 0 none;
      }
    }

    .label {
      width: 60rpx;
      color: #898b94;
      margin: 0 16rpx 0 10rpx;
    }

    .text {
      flex: 1;
      -webkit-line-clamp: 1;
    }
  }
}

/* 商品详情 */
.detail {
  padding-left: 20rpx;

  .content {
    margin-left: -20rpx;

    .image {
      width: 100%;
    }
  }

  .properties {
    padding: 0 20rpx;
    margin-bottom: 30rpx;

    .item {
      display: flex;
      line-height: 2;
      padding: 10rpx;
      font-size: 26rpx;
      color: #333;
      border-bottom: 1rpx dashed #ccc;
    }

    .label {
      width: 200rpx;
    }

    .value {
      flex: 1;
    }
  }
}

/* 同类推荐 */
.similar {
  .content {
    padding: 0 20rpx 200rpx;
    background-color: #f4f4f4;
    display: flex;
    flex-wrap: wrap;

    .goods {
      width: 340rpx;
      padding: 24rpx 20rpx 20rpx;
      margin: 20rpx 7rpx;
      border-radius: 10rpx;
      background-color: #fff;
    }

    .image {
      width: 300rpx;
      height: 260rpx;
    }

    .name {
      height: 80rpx;
      margin: 10rpx 0;
      font-size: 26rpx;
      color: #262626;
    }

    .price {
      line-height: 1;
      font-size: 20rpx;
      color: #cf4444;
    }

    .number {
      font-size: 26rpx;
      margin-left: 2rpx;
    }
  }

  navigator {
    &:nth-child(even) {
      margin-right: 0;
    }
  }
}

/* 底部工具栏 */
.toolbar {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 1;
  background-color: #fff;
  height: 100rpx;
  padding: 0 20rpx var(--window-bottom);
  border-top: 1rpx solid #eaeaea;
  display: flex;
  justify-content: space-between;
  align-items: center;
  box-sizing: content-box;

  .buttons {
    display: flex;

    &>view {
      width: 220rpx;
      text-align: center;
      line-height: 72rpx;
      font-size: 26rpx;
      color: #fff;
      border-radius: 72rpx;
    }

    .addcart {
      background-color: #ffa868;
    }

    .buynow,
    .payment {
      background-color: #27ba9b;
      margin-left: 20rpx;
    }
  }

  .icons {
    padding-right: 10rpx;
    display: flex;
    align-items: center;
    flex: 1;

    .icons-button {
      flex: 1;
      text-align: center;
      line-height: 1.4;
      padding: 0;
      margin: 0;
      border-radius: 0;
      font-size: 20rpx;
      color: #333;
      background-color: #fff;

      &::after {
        border: none;
      }
    }

    text {
      display: block;
      font-size: 34rpx;
    }
  }
}
</style>
5.3、后端

项目结构

image-20250630205453248

核心代码

@Service
public class ProductSkuServiceImpl 
    extends ServiceImpl<ProductSkuMapper, ProductSku>
    implements ProductSkuService {

    @Autowired
    private IProductSpecService productSpecService;

    @Autowired
    private ProductMapper productManager;

    /**
     * 根据商品id获取商品sku信息
     *
     * @param productId
     * @return
     */
    @Override
    public List<ProductSkuVO> getProductById(Integer productId) {
        Product product = productManager.selectById(productId);
        List<ProductSku> list = this.lambdaQuery()
                .eq(ProductSku::getProductId, productId)
                .list();
        List<ProductSkuVO> vos = new ArrayList<>();
        for (ProductSku productSku : list) {
            ProductSkuVO vo = new ProductSkuVO();
            vo.setId(productSku.getId());
            vo.setGoodsId(productSku.getProductId());
            vo.setGoodsName(product.getName());
            vo.setStock(productSku.getStock());
            vo.setPrice(productSku.getPrice());
            vo.setImage(productSku.getImage());
            List<ProductSpec> specs = productSpecService.lambdaQuery()
                    .eq(ProductSpec::getProductSkuId, productSku.getId())
                    .list();
            vo.setSpecs(specs);
            vos.add(vo);
        }
        return vos;
    }


    @Override
    public List<ProductSkuVO> getSkuById(Integer skuId) {
        List<ProductSku> list = this.lambdaQuery()
                .eq(ProductSku::getId, skuId)
                .list();

        List<ProductSkuVO> vos = new ArrayList<>();
        for (ProductSku productSku : list) {
            ProductSkuVO vo = new ProductSkuVO();
            Product product = productManager.selectById(productSku.getProductId());
            vo.setId(productSku.getId());
            vo.setGoodsId(productSku.getProductId());
            vo.setGoodsName(product.getName());
            vo.setStock(productSku.getStock());
            vo.setPrice(productSku.getPrice());
            vo.setImage(productSku.getImage());
            List<ProductSpec> specs = productSpecService.lambdaQuery()
                    .eq(ProductSpec::getProductSkuId, productSku.getId())
                    .list();
            vo.setSpecs(specs);
            vos.add(vo);
        }
        return vos;
    }
}
@Service
public class ProductServiceImpl extends ServiceImpl<ProductMapper, Product> implements IProductService {

    @Autowired
    private ProductMapper productMapper;

    @Autowired
    private IProductImageService productImageService;

    @Autowired
    private IProductAttributeService productAttributeService;

    /**
     * 商品列表分页查询
     *
     * @param page
     * @return
     */
    @Override
    public IPage<ProductVO> getList(IPage<ProductVO> page, Integer hotId) {
        return productMapper.getList(page, hotId);
    }

    /**
     * 根据分类id查询商品
     *
     * @param categoryId
     * @return
     */
    @Override
    public List<ProductVO> getByCategoryId(Integer categoryId) {
        List<Product> list = this.lambdaQuery()
                .eq(Product::getCategoryId, categoryId)
                .list();
        List<ProductVO> vos = new ArrayList<>();
        for (Product product : list) {
            ProductVO vo = new ProductVO();
            BeanUtils.copyProperties(product, vo);
            List<ProductImage> images = productImageService.lambdaQuery()
                    .eq(ProductImage::getProductId, product.getId())
                    .list();
            if (images.size() > 0) {
                vo.setImageUrl(images.get(0).getImageUrl());
            }
            vos.add(vo);
        }
        return vos;
    }

    /**
     * 根据id查询商品详情
     *
     * @param id
     * @return
     */
    @Override
    public ProductDetailVO getProductDetailById(Integer id) {
        Product product = this.getById(id);
        if (product != null) {
            ProductDetailVO vo = new ProductDetailVO();
            BeanUtils.copyProperties(product, vo);
            //获取商品图片
            List<ProductImage> productImages = productImageService.lambdaQuery()
                    .eq(ProductImage::getProductId, product.getId())
                    .list();
            if (!productImages.isEmpty()) {
                List<String> imageList =  productImages.stream().map(ProductImage::getImageUrl).collect(Collectors.toList());
                vo.setImages(imageList);
            }
            //获取属性值
            List<ProductAttribute> productAttributeList = productAttributeService.lambdaQuery()
                    .eq(ProductAttribute::getProductId, product.getId())
                    .list();
            if (!productAttributeList.isEmpty()) {
                vo.setAttributes(productAttributeList);
            }
            return vo;
        }
        return null;
    }
}

package com.funrniur.app.service.impl;

import com.funrniur.app.dto.CartDTO;
import com.funrniur.app.mapper.ProductMapper;
import com.funrniur.app.service.IProductService;
import com.funrniur.app.service.IProductSpecService;
import com.funrniur.app.service.ProductSkuService;
import com.funrniur.app.vo.CartVO;
import com.funrniur.common.login.LoginUser;
import com.funrniur.common.login.LoginUserHolder;
import com.funrniur.model.entity.Cart;
import com.funrniur.app.mapper.CartMapper;
import com.funrniur.app.service.ICartService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.funrniur.model.entity.Product;
import com.funrniur.model.entity.ProductSku;
import com.funrniur.model.entity.ProductSpec;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;

/**
 * <p>
 * 购物车表 服务实现类
 * </p>
 *
 * @author chen
 * @since 2025-03-09
 */
@Service
public class CartServiceImpl extends ServiceImpl<CartMapper, Cart> implements ICartService {

    @Autowired
    private ProductSkuService productSkuService;

    @Autowired
    private ProductMapper productMapper;

    @Autowired
    private IProductSpecService productSpecService;

    /**
     * 添加购物车
     *
     * @param cartDTO
     */
    @Override
    public void addCart(CartDTO cartDTO) {
        LoginUser loginUser = LoginUserHolder.getLoginUser();
        ProductSku sku =  this.productSkuService.getById(cartDTO.getSkuId());

        Cart dbCart = this.lambdaQuery()
                .eq(Cart::getUserId, loginUser.getUserId())
                .eq(Cart::getProductSkuId, cartDTO.getSkuId())
                .eq(Cart::getProductId, sku.getProductId())
                .one();
        if (dbCart != null){
            dbCart.setQuantity(dbCart.getQuantity() + cartDTO.getNumber());
            this.updateById(dbCart);
            return;
        }

        Cart cart = new Cart();

        cart.setUserId(loginUser.getUserId().intValue());
        cart.setProductId(sku.getProductId());
        cart.setProductSkuId(cartDTO.getSkuId());
        cart.setQuantity(cartDTO.getNumber());
        cart.setAddTime(LocalDateTime.now());
        cart.setSelected(0);

        this.save(cart);
    }

    /**
     * 获取购物车列表
     *
     * @return
     */
    @Override
    public List<CartVO> getList() {
        LoginUser loginUser = LoginUserHolder.getLoginUser();
        List<Cart> list = this.lambdaQuery()
                .eq(Cart::getUserId, loginUser.getUserId())
                .list();
        if (list == null || list.isEmpty()){
            return Collections.emptyList();
        }

        return list.stream().map(cart -> {
            CartVO cartVO = new CartVO();
            cartVO.setId(cart.getId());
            cartVO.setSkuId(cart.getProductSkuId());
            cartVO.setNumber(cart.getQuantity());
            cartVO.setProductId(cart.getProductId());

            Product product = productMapper.selectById(cart.getProductId());
            cartVO.setName(product.getName());


            ProductSku productSku = productSkuService.getById(cart.getProductSkuId());
            cartVO.setImage(productSku.getImage());
            cartVO.setPrice(productSku.getPrice());
            cartVO.setStock(productSku.getStock());
            cartVO.setSelected(cart.getSelected());

            List<ProductSpec> specList = productSpecService.lambdaQuery()
                    .eq(ProductSpec::getProductSkuId, cart.getProductSkuId())
                    .list();

            String attrsText = specList.stream()
                    .map(ProductSpec::getValue) // 提取规格名称
                    .collect(Collectors.joining(" ")); // 用空格连接
            cartVO.setAttrsText(attrsText);

            return cartVO;
        }).collect(Collectors.toList());
    }
}


网站公告

今日签到

点亮在社区的每一天
去签到