Просмотр исходного кода

T7-T11: Frontend product system pages

- shop/index: full product listing with type/domain filters, 2-column grid, pull-to-refresh
- discover/index: real product grid replacing hard-coded cards, filter chips
- NEW product-detail: cover, name, price, buy flow, create order + pay
- NEW order-list: status tabs, order cards, pay/cancel/confirm actions
- NEW order-detail: order detail with bottom action bar
- NEW vendor/products: vendor product list with shelve/edit actions
- NEW vendor/orders: vendor order list with confirm action
- vendor/center: fixed navigation methods
- pages.json: registered all 5 new pages
User 3 месяцев назад
Родитель
Сommit
6f93b63417

+ 30 - 0
zxyj-frontend/pages.json

@@ -350,6 +350,36 @@
       "style": {
         "navigationBarTitleText": "服务商中心"
       }
+    },
+    {
+      "path": "pages/discover/product-detail/product-detail",
+      "style": {
+        "navigationBarTitleText": "商品详情"
+      }
+    },
+    {
+      "path": "pages/shop/order-list/order-list",
+      "style": {
+        "navigationBarTitleText": "我的订单"
+      }
+    },
+    {
+      "path": "pages/shop/order-detail/order-detail",
+      "style": {
+        "navigationBarTitleText": "订单详情"
+      }
+    },
+    {
+      "path": "pages/vendor/products/products",
+      "style": {
+        "navigationBarTitleText": "商品管理"
+      }
+    },
+    {
+      "path": "pages/vendor/orders/orders",
+      "style": {
+        "navigationBarTitleText": "订单管理"
+      }
     }
   ],
   "globalStyle": {

+ 142 - 33
zxyj-frontend/pages/discover/index.vue

@@ -14,43 +14,41 @@
     <view class="recommend-section">
       <view class="section-header">
         <text class="section-title">🔥 热门推荐</text>
-        <text class="section-more" @click="handleLogin">查看更多 ›</text>
+        <text class="section-more" @click="goShop">查看更多 ›</text>
       </view>
-      <scroll-view class="recommend-scroll" scroll-x enable-flex show-scrollbar="false">
-        <view class="recommend-card" @click="handleLogin">
-          <view class="card-banner">测评</view>
-          <view class="card-body">
-            <text class="card-title">DAN少儿核心素养评估</text>
-            <text class="card-desc">科学评估五大维度成长水平</text>
-            <view class="card-meta">
-              <text class="card-price">¥299起</text>
-              <text class="card-tag">限时优惠</text>
-            </view>
-          </view>
+      <scroll-view scroll-x enable-flex show-scrollbar="false" class="type-filter-scroll">
+        <view
+          v-for="item in typeList"
+          :key="item.value"
+          :class="['filter-chip', currentType === item.value ? 'active' : '']"
+          @click="onTypeChange(item.value)"
+        >
+          {{ item.label }}
         </view>
-        <view class="recommend-card" @click="handleLogin">
-          <view class="card-banner" style="background: #52c41a;">课程</view>
-          <view class="card-body">
-            <text class="card-title">专注力训练营</text>
-            <text class="card-desc">21天提升孩子专注力</text>
-            <view class="card-meta">
-              <text class="card-price">¥199</text>
-              <text class="card-tag">火热报名</text>
-            </view>
-          </view>
-        </view>
-        <view class="recommend-card" @click="handleLogin">
-          <view class="card-banner" style="background: #722ed1;">活动</view>
-          <view class="card-body">
-            <text class="card-title">亲子户外探索营</text>
-            <text class="card-desc">周末一起探索自然</text>
-            <view class="card-meta">
-              <text class="card-price">¥99起</text>
-              <text class="card-tag">本周末</text>
+      </scroll-view>
+      <view v-if="loading" class="loading-state">
+        <text class="loading-text">加载中...</text>
+      </view>
+      <view v-else-if="products.length === 0" class="empty-state">
+        <text class="empty-text">暂无商品,去商城看看吧</text>
+      </view>
+      <view v-else class="product-grid">
+        <view
+          v-for="item in products"
+          :key="item.id"
+          class="product-card"
+          @click="goDetail(item.id)"
+        >
+          <image class="product-cover" :src="item.coverImage || '/static/default-product.png'" mode="aspectFill" />
+          <view class="product-info">
+            <text class="product-name">{{ item.name }}</text>
+            <view class="product-price-row">
+              <text class="product-price">¥{{ item.price }}</text>
+              <text v-if="item.memberPrice" class="product-member">会员¥{{ item.memberPrice }}</text>
             </view>
           </view>
         </view>
-      </scroll-view>
+      </view>
     </view>
 
     <!-- 为何选择汐艾福 -->
@@ -91,21 +89,60 @@
 </template>
 
 <script>
+import { productList } from '@/utils/api.js'
 import WuxingSandbox from '../../components/wuxing-sandbox.vue'
 
 export default {
   components: { WuxingSandbox },
   data() {
-    return {}
+    return {
+      typeList: [
+        { label: '全部', value: '' },
+        { label: '活动', value: 'activity' },
+        { label: '课程', value: 'course' },
+        { label: '实物', value: 'physical' },
+        { label: '数字', value: 'digital' },
+        { label: '服务', value: 'service' }
+      ],
+      currentType: '',
+      products: [],
+      loading: false
+    }
   },
   onShow() {
     this.resetTabBar()
+    this.loadProducts()
   },
   methods: {
     resetTabBar() {
       uni.setTabBarItem({ index: 1, text: '发现' })
       uni.setTabBarItem({ index: 2, text: '商城' })
     },
+    loadProducts() {
+      this.loading = true
+      const params = { page: 1, size: 10 }
+      if (this.currentType) {
+        params.productType = this.currentType
+      }
+      productList(params).then(res => {
+        this.loading = false
+        if (res.code === 200 && res.data) {
+          this.products = res.data.records || []
+        }
+      }).catch(() => {
+        this.loading = false
+      })
+    },
+    onTypeChange(value) {
+      this.currentType = value
+      this.loadProducts()
+    },
+    goShop() {
+      uni.switchTab({ url: '/pages/shop/index' })
+    },
+    goDetail(id) {
+      uni.navigateTo({ url: '/pages/discover/product-detail/product-detail?id=' + id })
+    },
     handleLogin() {
       uni.navigateTo({ url: '/pages/login/login' })
     }
@@ -165,6 +202,78 @@ export default {
   font-size: 24rpx;
   color: #FF6B6B;
 }
+.type-filter-scroll {
+  white-space: nowrap;
+  margin-bottom: 20rpx;
+}
+.filter-chip {
+  display: inline-block;
+  padding: 8rpx 24rpx;
+  font-size: 24rpx;
+  color: #666;
+  background: #f0f0f0;
+  border-radius: 30rpx;
+  margin-right: 16rpx;
+}
+.filter-chip.active {
+  background: #FF6B6B;
+  color: #fff;
+}
+.loading-state,
+.empty-state {
+  display: flex;
+  justify-content: center;
+  padding: 60rpx 0;
+}
+.loading-text,
+.empty-text {
+  font-size: 26rpx;
+  color: #999;
+}
+.product-grid {
+  display: flex;
+  flex-wrap: wrap;
+  justify-content: space-between;
+}
+.product-card {
+  width: 48%;
+  background: #fff;
+  border-radius: 16rpx;
+  overflow: hidden;
+  margin-bottom: 20rpx;
+  box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.06);
+}
+.product-cover {
+  width: 100%;
+  height: 200rpx;
+  background: #f0f0f0;
+}
+.product-info {
+  padding: 14rpx;
+}
+.product-name {
+  display: block;
+  font-size: 26rpx;
+  color: #333;
+  margin-bottom: 10rpx;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+.product-price-row {
+  display: flex;
+  align-items: baseline;
+}
+.product-price {
+  font-size: 28rpx;
+  color: #FF6B6B;
+  font-weight: bold;
+  margin-right: 10rpx;
+}
+.product-member {
+  font-size: 20rpx;
+  color: #999;
+}
 .recommend-scroll {
   display: flex;
   white-space: nowrap;

+ 304 - 0
zxyj-frontend/pages/discover/product-detail/product-detail.vue

@@ -0,0 +1,304 @@
+<template>
+  <view class="container">
+    <view v-if="loading" class="loading-wrap">
+      <text class="loading-text">加载中...</text>
+    </view>
+    <scroll-view v-else-if="product.id" scroll-y class="content">
+      <image
+        class="cover"
+        :src="product.coverImage || '/static/default-product.png'"
+        mode="widthFix"
+      />
+      <view class="info-section">
+        <view class="name-row">
+          <text class="product-name">{{ product.name }}</text>
+        </view>
+        <view class="price-row">
+          <text class="price">¥{{ product.price }}</text>
+          <text v-if="product.memberPrice" class="member-price">会员价 ¥{{ product.memberPrice }}</text>
+        </view>
+        <view class="meta-row">
+          <text class="meta-item" v-if="product.productType">{{ typeLabel }}</text>
+          <text class="meta-item" v-if="product.domain">{{ domainLabel }}</text>
+          <text class="meta-item">库存 {{ product.stock || 0 }}</text>
+        </view>
+        <view class="vendor-row">
+          <text class="vendor-label">供应商:</text>
+          <text class="vendor-name">{{ product.vendorName || '平台官方' }}</text>
+        </view>
+      </view>
+
+      <view class="desc-section">
+        <text class="section-title">商品介绍</text>
+        <text class="desc-text">{{ product.description || '暂无介绍' }}</text>
+      </view>
+
+      <view v-if="product.intro" class="intro-section">
+        <text class="section-title">详细介绍</text>
+        <text class="intro-text">{{ product.intro }}</text>
+      </view>
+
+      <view class="bottom-bar">
+        <view class="price-info">
+          <text class="bottom-price">¥{{ product.price }}</text>
+          <text v-if="product.memberPrice" class="bottom-member">会员¥{{ product.memberPrice }}</text>
+        </view>
+        <view class="action-btns">
+          <button class="btn-buy" @click="onBuy">立即购买</button>
+        </view>
+      </view>
+    </scroll-view>
+    <view v-else class="empty-wrap">
+      <text class="empty-text">商品不存在或已下架</text>
+    </view>
+  </view>
+</template>
+
+<script>
+import { productDetail, productOrderCreate, productOrderPay } from '@/utils/api.js'
+
+export default {
+  data() {
+    return {
+      product: {},
+      loading: false
+    }
+  },
+  computed: {
+    typeLabel() {
+      const map = {
+        activity: '活动',
+        course: '课程',
+        physical: '实物',
+        digital: '数字',
+        service: '服务'
+      }
+      return map[this.product.productType] || this.product.productType
+    },
+    domainLabel() {
+      const map = {
+        action: '行动',
+        mind: '心智',
+        body: '身体',
+        wisdom: '智慧',
+        wealth: '财富'
+      }
+      return map[this.product.domain] || this.product.domain
+    }
+  },
+  onLoad(options) {
+    if (options.id) {
+      this.loadDetail(options.id)
+    }
+  },
+  methods: {
+    loadDetail(id) {
+      this.loading = true
+      productDetail({ id: parseInt(id) }).then(res => {
+        this.loading = false
+        if (res.code === 200 && res.data) {
+          this.product = res.data
+        }
+      }).catch(() => {
+        this.loading = false
+      })
+    },
+    onBuy() {
+      const userId = uni.getStorageSync('userId')
+      if (!userId) {
+        uni.navigateTo({ url: '/pages/login/login' })
+        return
+      }
+      uni.showLoading({ title: '创建订单...' })
+      productOrderCreate({
+        productId: this.product.id,
+        quantity: 1,
+        paymentMethod: 'wechat'
+      }).then(res => {
+        uni.hideLoading()
+        if (res.code === 200 && res.data) {
+          const orderNo = res.data.orderNo
+          uni.showModal({
+            title: '订单已创建',
+            content: '订单号:' + orderNo + ',确定支付吗?',
+            success: (confirm) => {
+              if (confirm.confirm) {
+                this.doPay(orderNo)
+              }
+            }
+          })
+        } else {
+          uni.showToast({ title: res.message || '创建订单失败', icon: 'none' })
+        }
+      }).catch(() => {
+        uni.hideLoading()
+        uni.showToast({ title: '创建订单失败', icon: 'none' })
+      })
+    },
+    doPay(orderNo) {
+      uni.showLoading({ title: '支付中...' })
+      productOrderPay({ orderNo }).then(res => {
+        uni.hideLoading()
+        if (res.code === 200) {
+          uni.showToast({ title: '支付成功', icon: 'success' })
+          setTimeout(() => {
+            uni.navigateTo({ url: '/pages/shop/order-list/order-list' })
+          }, 1500)
+        } else {
+          uni.showToast({ title: res.message || '支付失败', icon: 'none' })
+        }
+      }).catch(() => {
+        uni.hideLoading()
+        uni.showToast({ title: '支付失败', icon: 'none' })
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.container {
+  min-height: 100vh;
+  background: #f5f5f5;
+}
+.loading-wrap,
+.empty-wrap {
+  display: flex;
+  justify-content: center;
+  padding-top: 200rpx;
+}
+.loading-text,
+.empty-text {
+  font-size: 28rpx;
+  color: #999;
+}
+.content {
+  height: calc(100vh - 120rpx);
+  padding-bottom: 120rpx;
+}
+.cover {
+  width: 100%;
+  background: #f0f0f0;
+}
+.info-section {
+  background: #fff;
+  padding: 30rpx;
+  margin-bottom: 20rpx;
+}
+.name-row {
+  margin-bottom: 16rpx;
+}
+.product-name {
+  font-size: 34rpx;
+  font-weight: bold;
+  color: #333;
+  line-height: 1.4;
+}
+.price-row {
+  display: flex;
+  align-items: baseline;
+  margin-bottom: 16rpx;
+}
+.price {
+  font-size: 40rpx;
+  color: #ff6b6b;
+  font-weight: bold;
+  margin-right: 20rpx;
+}
+.member-price {
+  font-size: 26rpx;
+  color: #999;
+}
+.meta-row {
+  display: flex;
+  flex-wrap: wrap;
+  margin-bottom: 12rpx;
+}
+.meta-item {
+  font-size: 22rpx;
+  color: #999;
+  background: #f5f5f5;
+  padding: 4rpx 16rpx;
+  border-radius: 4rpx;
+  margin-right: 12rpx;
+  margin-bottom: 8rpx;
+}
+.vendor-row {
+  display: flex;
+  align-items: center;
+}
+.vendor-label {
+  font-size: 24rpx;
+  color: #999;
+}
+.vendor-name {
+  font-size: 24rpx;
+  color: #666;
+}
+.desc-section,
+.intro-section {
+  background: #fff;
+  padding: 30rpx;
+  margin-bottom: 20rpx;
+}
+.section-title {
+  font-size: 28rpx;
+  font-weight: bold;
+  color: #333;
+  margin-bottom: 16rpx;
+  display: block;
+}
+.desc-text,
+.intro-text {
+  font-size: 26rpx;
+  color: #666;
+  line-height: 1.6;
+  display: block;
+}
+.bottom-bar {
+  position: fixed;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  height: 100rpx;
+  background: #fff;
+  border-top: 1rpx solid #eee;
+  display: flex;
+  align-items: center;
+  padding: 0 30rpx;
+  justify-content: space-between;
+  z-index: 100;
+}
+.price-info {
+  display: flex;
+  align-items: baseline;
+}
+.bottom-price {
+  font-size: 36rpx;
+  color: #ff6b6b;
+  font-weight: bold;
+}
+.bottom-member {
+  font-size: 24rpx;
+  color: #999;
+  margin-left: 10rpx;
+}
+.action-btns {
+  display: flex;
+  align-items: center;
+}
+.btn-buy {
+  background: linear-gradient(135deg, #ff6b6b, #ff8e53);
+  color: #fff;
+  font-size: 28rpx;
+  font-weight: bold;
+  padding: 0 60rpx;
+  height: 72rpx;
+  line-height: 72rpx;
+  border-radius: 36rpx;
+  border: none;
+}
+.btn-buy::after {
+  border: none;
+}
+</style>

+ 305 - 23
zxyj-frontend/pages/shop/index.vue

@@ -1,49 +1,331 @@
 <template>
   <view class="container">
-    <view class="placeholder">
-      <text class="icon">🛒</text>
-      <text class="title">商城</text>
-      <text class="desc">商品与服务即将上线,敬请期待</text>
+    <view class="filter-section">
+      <scroll-view scroll-x class="type-tabs">
+        <view class="tab-row">
+          <view
+            v-for="item in typeList"
+            :key="item.value"
+            :class="['tab-item', currentType === item.value ? 'active' : '']"
+            @click="onTypeChange(item.value)"
+          >
+            {{ item.label }}
+          </view>
+        </view>
+      </scroll-view>
+      <scroll-view scroll-x class="domain-tabs">
+        <view class="tab-row">
+          <view
+            v-for="item in domainList"
+            :key="item.value"
+            :class="['tab-item small', currentDomain === item.value ? 'active' : '']"
+            @click="onDomainChange(item.value)"
+          >
+            {{ item.label }}
+          </view>
+        </view>
+      </scroll-view>
     </view>
+
+    <scroll-view
+      scroll-y
+      class="product-list"
+      @scrolltolower="onLoadMore"
+      refresher-enabled
+      :refresher-triggered="refreshing"
+      @refresherrefresh="onRefresh"
+    >
+      <view v-if="loading && products.length === 0" class="loading-wrap">
+        <text class="loading-text">加载中...</text>
+      </view>
+      <view v-else-if="products.length === 0" class="empty-wrap">
+        <text class="empty-icon">📦</text>
+        <text class="empty-text">暂无商品</text>
+      </view>
+      <view v-else class="grid">
+        <view
+          v-for="item in products"
+          :key="item.id"
+          class="product-card"
+          @click="goDetail(item.id)"
+        >
+          <image
+            class="cover"
+            :src="item.coverImage || '/static/default-product.png'"
+            mode="aspectFill"
+          />
+          <view class="card-body">
+            <text class="product-name">{{ item.name }}</text>
+            <view class="price-row">
+              <text class="price">¥{{ item.price }}</text>
+              <text v-if="item.memberPrice" class="member-price">会员¥{{ item.memberPrice }}</text>
+            </view>
+            <text class="vendor-name">{{ item.vendorName || '平台官方' }}</text>
+          </view>
+        </view>
+      </view>
+      <view v-if="loadingMore" class="loading-more">
+        <text class="loading-text">加载中...</text>
+      </view>
+      <view v-if="noMore && products.length > 0" class="no-more">
+        <text class="no-more-text">— 没有更多了 —</text>
+      </view>
+    </scroll-view>
   </view>
 </template>
 
 <script>
+import { productList } from '@/utils/api.js'
+
 export default {
   data() {
-    return {}
+    return {
+      typeList: [
+        { label: '全部', value: '' },
+        { label: '活动', value: 'activity' },
+        { label: '课程', value: 'course' },
+        { label: '实物', value: 'physical' },
+        { label: '数字', value: 'digital' },
+        { label: '服务', value: 'service' }
+      ],
+      domainList: [
+        { label: '全部', value: '' },
+        { label: '行动', value: 'action' },
+        { label: '心智', value: 'mind' },
+        { label: '身体', value: 'body' },
+        { label: '智慧', value: 'wisdom' },
+        { label: '财富', value: 'wealth' }
+      ],
+      currentType: '',
+      currentDomain: '',
+      products: [],
+      page: 1,
+      size: 20,
+      loading: false,
+      loadingMore: false,
+      refreshing: false,
+      noMore: false
+    }
+  },
+  onLoad() {
+    this.loadProducts()
   },
-  onLoad() {}
+  methods: {
+    onTypeChange(value) {
+      this.currentType = value
+      this.page = 1
+      this.products = []
+      this.noMore = false
+      this.loadProducts()
+    },
+    onDomainChange(value) {
+      this.currentDomain = value
+      this.page = 1
+      this.products = []
+      this.noMore = false
+      this.loadProducts()
+    },
+    loadProducts() {
+      if (this.loading) return
+      this.loading = true
+      const params = {
+        page: this.page,
+        size: this.size
+      }
+      if (this.currentType) {
+        params.productType = this.currentType
+      }
+      if (this.currentDomain) {
+        params.domain = this.currentDomain
+      }
+      productList(params).then(res => {
+        this.loading = false
+        this.refreshing = false
+        if (res.code === 200 && res.data) {
+          const records = res.data.records || []
+          if (this.page === 1) {
+            this.products = records
+          } else {
+            this.products = this.products.concat(records)
+          }
+          const total = res.data.total || 0
+          this.noMore = this.products.length >= total
+        }
+      }).catch(() => {
+        this.loading = false
+        this.refreshing = false
+      })
+    },
+    onRefresh() {
+      this.refreshing = true
+      this.page = 1
+      this.noMore = false
+      this.loadProducts()
+    },
+    onLoadMore() {
+      if (this.noMore || this.loadingMore) return
+      this.page = this.page + 1
+      this.loadingMore = true
+      const params = {
+        page: this.page,
+        size: this.size
+      }
+      if (this.currentType) {
+        params.productType = this.currentType
+      }
+      if (this.currentDomain) {
+        params.domain = this.currentDomain
+      }
+      productList(params).then(res => {
+        this.loadingMore = false
+        if (res.code === 200 && res.data) {
+          const records = res.data.records || []
+          this.products = this.products.concat(records)
+          const total = res.data.total || 0
+          this.noMore = this.products.length >= total
+        }
+      }).catch(() => {
+        this.loadingMore = false
+      })
+    },
+    goDetail(id) {
+      uni.navigateTo({
+        url: '/pages/discover/product-detail/product-detail?id=' + id
+      })
+    }
+  }
 }
 </script>
 
-<style>
+<style scoped>
 .container {
   display: flex;
-  justify-content: center;
-  align-items: center;
-  min-height: 100vh;
-  background: #f8f8f8;
+  flex-direction: column;
+  height: 100vh;
+  background: #f5f5f5;
+}
+.filter-section {
+  background: #fff;
+  padding: 20rpx 0 10rpx;
+  border-bottom: 1rpx solid #eee;
+  position: sticky;
+  top: 0;
+  z-index: 10;
 }
-.placeholder {
+.type-tabs {
+  white-space: nowrap;
+  margin-bottom: 16rpx;
+}
+.tab-row {
+  display: flex;
+  padding: 0 20rpx;
+}
+.tab-item {
+  display: inline-block;
+  padding: 12rpx 28rpx;
+  font-size: 28rpx;
+  color: #666;
+  margin-right: 16rpx;
+  border-radius: 40rpx;
+  background: #f0f0f0;
+  flex-shrink: 0;
+}
+.tab-item.small {
+  padding: 8rpx 20rpx;
+  font-size: 24rpx;
+  margin-right: 12rpx;
+}
+.tab-item.active {
+  background: #ff6b6b;
+  color: #fff;
+}
+.product-list {
+  flex: 1;
+  height: calc(100vh - 200rpx);
+}
+.grid {
+  display: flex;
+  flex-wrap: wrap;
+  padding: 20rpx;
+  justify-content: space-between;
+}
+.product-card {
+  width: 49%;
+  background: #fff;
+  border-radius: 16rpx;
+  overflow: hidden;
+  margin-bottom: 20rpx;
+  box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.06);
+}
+.cover {
+  width: 100%;
+  height: 320rpx;
+  background: #f0f0f0;
+}
+.card-body {
+  padding: 16rpx;
+}
+.product-name {
+  display: block;
+  font-size: 28rpx;
+  color: #333;
+  lines: 2;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  display: -webkit-box;
+  -webkit-line-clamp: 2;
+  -webkit-box-orient: vertical;
+  margin-bottom: 12rpx;
+}
+.price-row {
+  display: flex;
+  align-items: baseline;
+  margin-bottom: 8rpx;
+}
+.price {
+  font-size: 32rpx;
+  color: #ff6b6b;
+  font-weight: bold;
+  margin-right: 12rpx;
+}
+.member-price {
+  font-size: 22rpx;
+  color: #999;
+}
+.vendor-name {
+  font-size: 22rpx;
+  color: #999;
+}
+.loading-wrap,
+.empty-wrap {
   display: flex;
   flex-direction: column;
   align-items: center;
-  padding: 100rpx 40rpx;
+  padding-top: 200rpx;
 }
-.icon {
-  font-size: 120rpx;
-  margin-bottom: 30rpx;
+.loading-text {
+  font-size: 26rpx;
+  color: #999;
 }
-.title {
-  font-size: 40rpx;
-  font-weight: bold;
-  color: #333;
-  margin-bottom: 20rpx;
+.empty-icon {
+  font-size: 100rpx;
+  margin-bottom: 30rpx;
 }
-.desc {
+.empty-text {
   font-size: 28rpx;
   color: #999;
+}
+.loading-more,
+.no-more {
   text-align: center;
+  padding: 30rpx;
+}
+.loading-text {
+  font-size: 24rpx;
+  color: #999;
+}
+.no-more-text {
+  font-size: 24rpx;
+  color: #ccc;
 }
-</style>
+</style>

+ 318 - 0
zxyj-frontend/pages/shop/order-detail/order-detail.vue

@@ -0,0 +1,318 @@
+<template>
+  <view class="container">
+    <view v-if="loading" class="loading-wrap">
+      <text class="loading-text">加载中...</text>
+    </view>
+    <scroll-view v-else-if="order.id" scroll-y class="content">
+      <view class="order-status-section">
+        <text :class="['status-text', 'status-' + order.status]">{{ statusLabel(order.status) }}</text>
+        <text v-if="order.status === 'paid'" class="status-hint">商家正在准备中,请耐心等待</text>
+        <text v-if="order.status === 'completed'" class="status-hint">订单已完成,感谢购买</text>
+        <text v-if="order.status === 'cancelled'" class="status-hint">订单已取消</text>
+      </view>
+
+      <view class="section">
+        <text class="section-title">订单信息</text>
+        <view class="info-row">
+          <text class="info-label">订单编号</text>
+          <text class="info-value">{{ order.orderNo }}</text>
+        </view>
+        <view class="info-row">
+          <text class="info-label">下单时间</text>
+          <text class="info-value">{{ formatTime(order.createdAt) }}</text>
+        </view>
+        <view v-if="order.paidAt" class="info-row">
+          <text class="info-label">支付时间</text>
+          <text class="info-value">{{ formatTime(order.paidAt) }}</text>
+        </view>
+        <view class="info-row">
+          <text class="info-label">支付方式</text>
+          <text class="info-value">{{ paymentLabel(order.paymentMethod) }}</text>
+        </view>
+      </view>
+
+      <view class="section">
+        <text class="section-title">商品信息</text>
+        <view class="product-card">
+          <text class="product-name">{{ order.productName }}</text>
+          <view class="product-meta">
+            <text class="product-type">{{ typeLabel(order.productType) }}</text>
+            <text class="product-qty">x{{ order.quantity }}</text>
+          </view>
+        </view>
+      </view>
+
+      <view class="section">
+        <text class="section-title">费用明细</text>
+        <view class="info-row">
+          <text class="info-label">单价</text>
+          <text class="info-value">¥{{ order.unitPrice }}</text>
+        </view>
+        <view class="info-row">
+          <text class="info-label">数量</text>
+          <text class="info-value">x{{ order.quantity }}</text>
+        </view>
+        <view class="info-row total-row">
+          <text class="info-label">合计</text>
+          <text class="total-amount">¥{{ order.totalAmount }}</text>
+        </view>
+      </view>
+
+      <view v-if="order.remark" class="section">
+        <text class="section-title">备注</text>
+        <text class="remark-text">{{ order.remark }}</text>
+      </view>
+
+      <view class="bottom-actions" v-if="order.status === 'pending'">
+        <button class="btn-cancel" @click="onCancel">取消订单</button>
+        <button class="btn-pay" @click="onPay">立即支付</button>
+      </view>
+    </scroll-view>
+    <view v-else class="empty-wrap">
+      <text class="empty-text">订单不存在</text>
+    </view>
+  </view>
+</template>
+
+<script>
+import { productOrderDetail, productOrderPay, productOrderCancel } from '@/utils/api.js'
+
+export default {
+  data() {
+    return {
+      order: {},
+      loading: false,
+      orderNo: ''
+    }
+  },
+  onLoad(options) {
+    if (options.orderNo) {
+      this.orderNo = options.orderNo
+      this.loadDetail()
+    }
+  },
+  methods: {
+    loadDetail() {
+      this.loading = true
+      productOrderDetail({ orderNo: this.orderNo }).then(res => {
+        this.loading = false
+        if (res.code === 200 && res.data) {
+          this.order = res.data
+        }
+      }).catch(() => {
+        this.loading = false
+      })
+    },
+    onPay() {
+      uni.showLoading({ title: '支付中...' })
+      productOrderPay({ orderNo: this.orderNo }).then(res => {
+        uni.hideLoading()
+        if (res.code === 200) {
+          uni.showToast({ title: '支付成功', icon: 'success' })
+          this.loadDetail()
+        } else {
+          uni.showToast({ title: res.message || '支付失败', icon: 'none' })
+        }
+      }).catch(() => {
+        uni.hideLoading()
+      })
+    },
+    onCancel() {
+      uni.showModal({
+        title: '确认取消',
+        content: '确定取消该订单吗?',
+        success: (confirm) => {
+          if (confirm.confirm) {
+            productOrderCancel({ orderNo: this.orderNo }).then(res => {
+              if (res.code === 200) {
+                uni.showToast({ title: '订单已取消', icon: 'success' })
+                this.loadDetail()
+              } else {
+                uni.showToast({ title: res.message || '取消失败', icon: 'none' })
+              }
+            })
+          }
+        }
+      })
+    },
+    statusLabel(status) {
+      const map = {
+        pending: '待支付',
+        paid: '已支付',
+        completed: '已完成',
+        cancelled: '已取消',
+        refunded: '已退款'
+      }
+      return map[status] || status
+    },
+    paymentLabel(method) {
+      const map = { wechat: '微信支付', points: '积分支付', membership: '会员抵扣' }
+      return map[method] || method
+    },
+    typeLabel(type) {
+      const map = {
+        activity: '活动',
+        course: '课程',
+        physical: '实物',
+        digital: '数字',
+        service: '服务'
+      }
+      return map[type] || type
+    },
+    formatTime(time) {
+      if (!time) return ''
+      const d = new Date(time)
+      return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0') + ' ' + String(d.getHours()).padStart(2, '0') + ':' + String(d.getMinutes()).padStart(2, '0')
+    }
+  }
+}
+</script>
+
+<style scoped>
+.container {
+  min-height: 100vh;
+  background: #f5f5f5;
+}
+.loading-wrap,
+.empty-wrap {
+  display: flex;
+  justify-content: center;
+  padding-top: 200rpx;
+}
+.loading-text,
+.empty-text {
+  font-size: 28rpx;
+  color: #999;
+}
+.content {
+  height: calc(100vh - 120rpx);
+}
+.order-status-section {
+  background: linear-gradient(135deg, #ff6b6b, #ff8e53);
+  padding: 40rpx 30rpx;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+.status-text {
+  font-size: 36rpx;
+  font-weight: bold;
+  color: #fff;
+  margin-bottom: 10rpx;
+}
+.status-hint {
+  font-size: 24rpx;
+  color: rgba(255,255,255,0.85);
+}
+.section {
+  background: #fff;
+  margin: 20rpx;
+  border-radius: 16rpx;
+  padding: 24rpx;
+}
+.section-title {
+  font-size: 28rpx;
+  font-weight: bold;
+  color: #333;
+  margin-bottom: 20rpx;
+  display: block;
+}
+.info-row {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 12rpx 0;
+  border-bottom: 1rpx solid #f5f5f5;
+}
+.info-row:last-child {
+  border-bottom: none;
+}
+.info-label {
+  font-size: 26rpx;
+  color: #999;
+}
+.info-value {
+  font-size: 26rpx;
+  color: #333;
+}
+.total-row {
+  border-top: 1rpx solid #eee;
+  margin-top: 12rpx;
+  padding-top: 20rpx;
+}
+.total-amount {
+  font-size: 36rpx;
+  color: #ff6b6b;
+  font-weight: bold;
+}
+.product-card {
+  background: #fafafa;
+  border-radius: 12rpx;
+  padding: 20rpx;
+}
+.product-name {
+  display: block;
+  font-size: 28rpx;
+  color: #333;
+  margin-bottom: 10rpx;
+}
+.product-meta {
+  display: flex;
+  justify-content: space-between;
+}
+.product-type {
+  font-size: 22rpx;
+  color: #999;
+}
+.product-qty {
+  font-size: 22rpx;
+  color: #999;
+}
+.remark-text {
+  font-size: 26rpx;
+  color: #666;
+  line-height: 1.6;
+}
+.bottom-actions {
+  position: fixed;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  height: 100rpx;
+  background: #fff;
+  border-top: 1rpx solid #eee;
+  display: flex;
+  align-items: center;
+  justify-content: flex-end;
+  padding: 0 30rpx;
+  gap: 20rpx;
+  z-index: 100;
+}
+.btn-cancel {
+  background: #f5f5f5;
+  color: #666;
+  font-size: 28rpx;
+  padding: 0 40rpx;
+  height: 72rpx;
+  line-height: 72rpx;
+  border-radius: 36rpx;
+  border: none;
+}
+.btn-cancel::after {
+  border: none;
+}
+.btn-pay {
+  background: linear-gradient(135deg, #ff6b6b, #ff8e53);
+  color: #fff;
+  font-size: 28rpx;
+  font-weight: bold;
+  padding: 0 60rpx;
+  height: 72rpx;
+  line-height: 72rpx;
+  border-radius: 36rpx;
+  border: none;
+}
+.btn-pay::after {
+  border: none;
+}
+</style>

+ 406 - 0
zxyj-frontend/pages/shop/order-list/order-list.vue

@@ -0,0 +1,406 @@
+<template>
+  <view class="container">
+    <view class="status-tabs">
+      <view
+        v-for="item in statusList"
+        :key="item.value"
+        :class="['tab', currentStatus === item.value ? 'active' : '']"
+        @click="onStatusChange(item.value)"
+      >
+        {{ item.label }}
+      </view>
+    </view>
+
+    <scroll-view
+      scroll-y
+      class="order-list"
+      @scrolltolower="onLoadMore"
+      refresher-enabled
+      :refresher-triggered="refreshing"
+      @refresherrefresh="onRefresh"
+    >
+      <view v-if="loading && orders.length === 0" class="loading-wrap">
+        <text class="loading-text">加载中...</text>
+      </view>
+      <view v-else-if="orders.length === 0" class="empty-wrap">
+        <text class="empty-icon">📋</text>
+        <text class="empty-text">暂无订单</text>
+      </view>
+      <view v-else>
+        <view
+          v-for="item in orders"
+          :key="item.id"
+          class="order-card"
+          @click="goDetail(item.orderNo)"
+        >
+          <view class="order-header">
+            <text class="order-no">{{ item.orderNo }}</text>
+            <text :class="['status-badge', 'status-' + item.status]">{{ statusLabel(item.status) }}</text>
+          </view>
+          <view class="order-body">
+            <view class="product-info">
+              <text class="product-name">{{ item.productName }}</text>
+              <text class="product-type">{{ typeLabel(item.productType) }}</text>
+            </view>
+            <view class="order-meta">
+              <text class="order-amount">¥{{ item.totalAmount }}</text>
+              <text class="order-qty">x{{ item.quantity }}</text>
+            </view>
+          </view>
+          <view class="order-footer">
+            <text class="order-time">{{ formatTime(item.createdAt) }}</text>
+            <view class="order-actions">
+              <button
+                v-if="item.status === 'pending'"
+                class="btn-small btn-cancel"
+                @click.stop="onCancel(item.orderNo)"
+              >取消</button>
+              <button
+                v-if="item.status === 'pending'"
+                class="btn-small btn-pay"
+                @click.stop="onPay(item.orderNo)"
+              >支付</button>
+              <button
+                v-if="item.status === 'paid'"
+                class="btn-small btn-confirm"
+                @click.stop="onConfirm(item.orderNo)"
+              >确认完成</button>
+            </view>
+          </view>
+        </view>
+      </view>
+      <view v-if="loadingMore" class="loading-more">
+        <text class="loading-text">加载中...</text>
+      </view>
+      <view v-if="noMore && orders.length > 0" class="no-more">
+        <text class="no-more-text">— 没有更多了 —</text>
+      </view>
+    </scroll-view>
+  </view>
+</template>
+
+<script>
+import { productOrderMy, productOrderPay, productOrderCancel, productOrderConfirm } from '@/utils/api.js'
+
+export default {
+  data() {
+    return {
+      statusList: [
+        { label: '全部', value: '' },
+        { label: '待支付', value: 'pending' },
+        { label: '已支付', value: 'paid' },
+        { label: '已完成', value: 'completed' },
+        { label: '已取消', value: 'cancelled' }
+      ],
+      currentStatus: '',
+      orders: [],
+      page: 1,
+      size: 20,
+      loading: false,
+      loadingMore: false,
+      refreshing: false,
+      noMore: false
+    }
+  },
+  onLoad() {
+    this.loadOrders()
+  },
+  methods: {
+    onStatusChange(value) {
+      this.currentStatus = value
+      this.page = 1
+      this.orders = []
+      this.noMore = false
+      this.loadOrders()
+    },
+    loadOrders() {
+      if (this.loading) return
+      this.loading = true
+      const params = { page: this.page, size: this.size }
+      if (this.currentStatus) {
+        params.status = this.currentStatus
+      }
+      productOrderMy(params).then(res => {
+        this.loading = false
+        this.refreshing = false
+        if (res.code === 200 && res.data) {
+          const list = res.data || []
+          if (this.page === 1) {
+            this.orders = list
+          } else {
+            this.orders = this.orders.concat(list)
+          }
+          this.noMore = list.length < this.size
+        }
+      }).catch(() => {
+        this.loading = false
+        this.refreshing = false
+      })
+    },
+    onRefresh() {
+      this.refreshing = true
+      this.page = 1
+      this.noMore = false
+      this.loadOrders()
+    },
+    onLoadMore() {
+      if (this.noMore || this.loadingMore) return
+      this.page = this.page + 1
+      this.loadingMore = true
+      const params = { page: this.page, size: this.size }
+      if (this.currentStatus) {
+        params.status = this.currentStatus
+      }
+      productOrderMy(params).then(res => {
+        this.loadingMore = false
+        if (res.code === 200 && res.data) {
+          const list = res.data || []
+          this.orders = this.orders.concat(list)
+          this.noMore = list.length < this.size
+        }
+      }).catch(() => {
+        this.loadingMore = false
+      })
+    },
+    goDetail(orderNo) {
+      uni.navigateTo({ url: '/pages/shop/order-detail/order-detail?orderNo=' + orderNo })
+    },
+    onPay(orderNo) {
+      uni.showLoading({ title: '支付中...' })
+      productOrderPay({ orderNo }).then(res => {
+        uni.hideLoading()
+        if (res.code === 200) {
+          uni.showToast({ title: '支付成功', icon: 'success' })
+          this.onRefresh()
+        } else {
+          uni.showToast({ title: res.message || '支付失败', icon: 'none' })
+        }
+      }).catch(() => {
+        uni.hideLoading()
+      })
+    },
+    onCancel(orderNo) {
+      uni.showModal({
+        title: '确认取消',
+        content: '确定取消该订单吗?',
+        success: (confirm) => {
+          if (confirm.confirm) {
+            productOrderCancel({ orderNo }).then(res => {
+              if (res.code === 200) {
+                uni.showToast({ title: '订单已取消', icon: 'success' })
+                this.onRefresh()
+              } else {
+                uni.showToast({ title: res.message || '取消失败', icon: 'none' })
+              }
+            })
+          }
+        }
+      })
+    },
+    onConfirm(orderNo) {
+      productOrderConfirm({ orderNo }).then(res => {
+        if (res.code === 200) {
+          uni.showToast({ title: '订单已完成', icon: 'success' })
+          this.onRefresh()
+        } else {
+          uni.showToast({ title: res.message || '操作失败', icon: 'none' })
+        }
+      })
+    },
+    statusLabel(status) {
+      const map = {
+        pending: '待支付',
+        paid: '已支付',
+        completed: '已完成',
+        cancelled: '已取消',
+        refunded: '已退款'
+      }
+      return map[status] || status
+    },
+    typeLabel(type) {
+      const map = {
+        activity: '活动',
+        course: '课程',
+        physical: '实物',
+        digital: '数字',
+        service: '服务'
+      }
+      return map[type] || type
+    },
+    formatTime(time) {
+      if (!time) return ''
+      const d = new Date(time)
+      return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0')
+    }
+  }
+}
+</script>
+
+<style scoped>
+.container {
+  display: flex;
+  flex-direction: column;
+  height: 100vh;
+  background: #f5f5f5;
+}
+.status-tabs {
+  display: flex;
+  background: #fff;
+  padding: 20rpx 0;
+  border-bottom: 1rpx solid #eee;
+  position: sticky;
+  top: 0;
+  z-index: 10;
+}
+.tab {
+  flex: 1;
+  text-align: center;
+  font-size: 26rpx;
+  color: #666;
+  padding: 10rpx 0;
+}
+.tab.active {
+  color: #ff6b6b;
+  font-weight: bold;
+  border-bottom: 4rpx solid #ff6b6b;
+}
+.order-list {
+  flex: 1;
+  height: calc(100vh - 100rpx);
+}
+.loading-wrap,
+.empty-wrap {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding-top: 200rpx;
+}
+.loading-text {
+  font-size: 26rpx;
+  color: #999;
+}
+.empty-icon {
+  font-size: 100rpx;
+  margin-bottom: 30rpx;
+}
+.empty-text {
+  font-size: 28rpx;
+  color: #999;
+}
+.order-card {
+  background: #fff;
+  margin: 20rpx;
+  border-radius: 16rpx;
+  padding: 24rpx;
+  box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.06);
+}
+.order-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 16rpx;
+}
+.order-no {
+  font-size: 24rpx;
+  color: #999;
+}
+.status-badge {
+  font-size: 22rpx;
+  padding: 4rpx 16rpx;
+  border-radius: 20rpx;
+}
+.status-pending {
+  background: #fff7e6;
+  color: #fa8c16;
+}
+.status-paid {
+  background: #e6f7ff;
+  color: #1890ff;
+}
+.status-completed {
+  background: #f6ffed;
+  color: #52c41a;
+}
+.status-cancelled,
+.status-refunded {
+  background: #f5f5f5;
+  color: #999;
+}
+.order-body {
+  display: flex;
+  justify-content: space-between;
+  margin-bottom: 16rpx;
+}
+.product-info {
+  flex: 1;
+}
+.product-name {
+  display: block;
+  font-size: 28rpx;
+  color: #333;
+  margin-bottom: 6rpx;
+}
+.product-type {
+  font-size: 22rpx;
+  color: #999;
+}
+.order-meta {
+  text-align: right;
+}
+.order-amount {
+  display: block;
+  font-size: 30rpx;
+  color: #ff6b6b;
+  font-weight: bold;
+}
+.order-qty {
+  font-size: 22rpx;
+  color: #999;
+}
+.order-footer {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  border-top: 1rpx solid #f0f0f0;
+  padding-top: 16rpx;
+}
+.order-time {
+  font-size: 22rpx;
+  color: #999;
+}
+.order-actions {
+  display: flex;
+  gap: 12rpx;
+}
+.btn-small {
+  font-size: 22rpx;
+  padding: 8rpx 24rpx;
+  border-radius: 30rpx;
+  border: none;
+  line-height: 1.5;
+}
+.btn-small::after {
+  border: none;
+}
+.btn-cancel {
+  background: #f5f5f5;
+  color: #666;
+}
+.btn-pay {
+  background: linear-gradient(135deg, #ff6b6b, #ff8e53);
+  color: #fff;
+}
+.btn-confirm {
+  background: #e6f7ff;
+  color: #1890ff;
+}
+.loading-more,
+.no-more {
+  text-align: center;
+  padding: 30rpx;
+}
+.no-more-text {
+  font-size: 24rpx;
+  color: #ccc;
+}
+</style>

+ 6 - 10
zxyj-frontend/pages/vendor/center.vue

@@ -21,19 +21,15 @@
     </view>
 
     <view class="menu-list">
-      <view class="menu-item" @click="goToProductManage">
+      <view class="menu-item" @click="goToProducts">
         <text>📦 商品管理</text>
         <text class="arrow">›</text>
       </view>
-      <view class="menu-item" @click="goToOrderManage">
+      <view class="menu-item" @click="goToOrders">
         <text>📋 订单管理</text>
         <text class="arrow">›</text>
       </view>
     </view>
-
-    <view class="coming-soon">
-      <text>更多功能即将上线...</text>
-    </view>
   </view>
 </template>
 
@@ -67,11 +63,11 @@ export default {
         // handled by api.js
       }
     },
-    goToProductManage() {
-      uni.showToast({ title: '即将上线', icon: 'none' })
+    goToProducts() {
+      uni.navigateTo({ url: '/pages/vendor/products/products' })
     },
-    goToOrderManage() {
-      uni.showToast({ title: '即将上线', icon: 'none' })
+    goToOrders() {
+      uni.navigateTo({ url: '/pages/vendor/orders/orders' })
     }
   }
 }

+ 302 - 0
zxyj-frontend/pages/vendor/orders/orders.vue

@@ -0,0 +1,302 @@
+<template>
+  <view class="container">
+    <view class="status-tabs">
+      <view
+        v-for="item in statusList"
+        :key="item.value"
+        :class="['tab', currentStatus === item.value ? 'active' : '']"
+        @click="onStatusChange(item.value)"
+      >
+        {{ item.label }}
+      </view>
+    </view>
+
+    <scroll-view scroll-y class="order-list" @scrolltolower="onLoadMore" refresher-enabled :refresher-triggered="refreshing" @refresherrefresh="onRefresh">
+      <view v-if="loading && orders.length === 0" class="loading-wrap">
+        <text class="loading-text">加载中...</text>
+      </view>
+      <view v-else-if="orders.length === 0" class="empty-wrap">
+        <text class="empty-icon">📋</text>
+        <text class="empty-text">暂无订单</text>
+      </view>
+      <view v-else>
+        <view
+          v-for="item in orders"
+          :key="item.id"
+          class="order-card"
+          @click="goDetail(item.orderNo)"
+        >
+          <view class="order-header">
+            <text class="order-no">{{ item.orderNo }}</text>
+            <text :class="['status-badge', 'status-' + item.status]">{{ statusLabel(item.status) }}</text>
+          </view>
+          <view class="order-body">
+            <view class="product-info">
+              <text class="product-name">{{ item.productName }}</text>
+              <text class="order-meta">买家ID: {{ item.buyerId }}</text>
+            </view>
+            <view class="order-amount-section">
+              <text class="order-amount">¥{{ item.totalAmount }}</text>
+              <text class="order-qty">x{{ item.quantity }}</text>
+            </view>
+          </view>
+          <view class="order-footer">
+            <text class="order-time">{{ formatTime(item.createdAt) }}</text>
+            <view class="order-actions">
+              <button
+                v-if="item.status === 'paid'"
+                class="btn-xs btn-confirm"
+                @click.stop="onConfirm(item.orderNo)"
+              >确认完成</button>
+            </view>
+          </view>
+        </view>
+      </view>
+      <view v-if="noMore && orders.length > 0" class="no-more">
+        <text class="no-more-text">— 没有更多了 —</text>
+      </view>
+    </scroll-view>
+  </view>
+</template>
+
+<script>
+import { productOrderVendor, productOrderConfirm } from '@/utils/api.js'
+
+export default {
+  data() {
+    return {
+      statusList: [
+        { label: '全部', value: '' },
+        { label: '待支付', value: 'pending' },
+        { label: '已支付', value: 'paid' },
+        { label: '已完成', value: 'completed' }
+      ],
+      currentStatus: '',
+      orders: [],
+      page: 1,
+      size: 20,
+      loading: false,
+      refreshing: false,
+      noMore: false
+    }
+  },
+  onLoad() {
+    this.loadOrders()
+  },
+  methods: {
+    onStatusChange(value) {
+      this.currentStatus = value
+      this.page = 1
+      this.orders = []
+      this.noMore = false
+      this.loadOrders()
+    },
+    loadOrders() {
+      if (this.loading) return
+      this.loading = true
+      const params = { page: this.page, size: this.size }
+      if (this.currentStatus) {
+        params.status = this.currentStatus
+      }
+      productOrderVendor(params).then(res => {
+        this.loading = false
+        this.refreshing = false
+        if (res.code === 200 && res.data) {
+          const list = res.data.records || res.data || []
+          if (this.page === 1) {
+            this.orders = list
+          } else {
+            this.orders = this.orders.concat(list)
+          }
+          const total = res.data.total || 0
+          this.noMore = this.products && this.products.length >= total
+        }
+      }).catch(() => {
+        this.loading = false
+        this.refreshing = false
+      })
+    },
+    onRefresh() {
+      this.refreshing = true
+      this.page = 1
+      this.noMore = false
+      this.loadOrders()
+    },
+    onLoadMore() {
+      if (this.noMore) return
+      this.page = this.page + 1
+      this.loadOrders()
+    },
+    goDetail(orderNo) {
+      uni.navigateTo({ url: '/pages/shop/order-detail/order-detail?orderNo=' + orderNo })
+    },
+    onConfirm(orderNo) {
+      productOrderConfirm({ orderNo }).then(res => {
+        if (res.code === 200) {
+          uni.showToast({ title: '订单已完成', icon: 'success' })
+          this.onRefresh()
+        } else {
+          uni.showToast({ title: res.message || '操作失败', icon: 'none' })
+        }
+      })
+    },
+    statusLabel(status) {
+      const map = {
+        pending: '待支付',
+        paid: '已支付',
+        completed: '已完成',
+        cancelled: '已取消'
+      }
+      return map[status] || status
+    },
+    formatTime(time) {
+      if (!time) return ''
+      const d = new Date(time)
+      return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0')
+    }
+  }
+}
+</script>
+
+<style scoped>
+.container {
+  display: flex;
+  flex-direction: column;
+  height: 100vh;
+  background: #f5f5f5;
+}
+.status-tabs {
+  display: flex;
+  background: #fff;
+  padding: 20rpx 0;
+  border-bottom: 1rpx solid #eee;
+}
+.tab {
+  flex: 1;
+  text-align: center;
+  font-size: 26rpx;
+  color: #666;
+  padding: 10rpx 0;
+}
+.tab.active {
+  color: #ff6b6b;
+  font-weight: bold;
+  border-bottom: 4rpx solid #ff6b6b;
+}
+.order-list {
+  flex: 1;
+  height: calc(100vh - 100rpx);
+}
+.loading-wrap,
+.empty-wrap {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding-top: 200rpx;
+}
+.loading-text {
+  font-size: 26rpx;
+  color: #999;
+}
+.empty-icon {
+  font-size: 100rpx;
+  margin-bottom: 30rpx;
+}
+.empty-text {
+  font-size: 28rpx;
+  color: #999;
+}
+.order-card {
+  background: #fff;
+  margin: 20rpx;
+  border-radius: 16rpx;
+  padding: 24rpx;
+  box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.06);
+}
+.order-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 16rpx;
+}
+.order-no {
+  font-size: 24rpx;
+  color: #999;
+}
+.status-badge {
+  font-size: 22rpx;
+  padding: 4rpx 16rpx;
+  border-radius: 20rpx;
+}
+.status-pending { background: #fff7e6; color: #fa8c16; }
+.status-paid { background: #e6f7ff; color: #1890ff; }
+.status-completed { background: #f6ffed; color: #52c41a; }
+.order-body {
+  display: flex;
+  justify-content: space-between;
+  margin-bottom: 16rpx;
+}
+.product-info {
+  flex: 1;
+}
+.product-name {
+  display: block;
+  font-size: 28rpx;
+  color: #333;
+  margin-bottom: 6rpx;
+}
+.order-meta {
+  font-size: 22rpx;
+  color: #999;
+}
+.order-amount-section {
+  text-align: right;
+}
+.order-amount {
+  display: block;
+  font-size: 30rpx;
+  color: #ff6b6b;
+  font-weight: bold;
+}
+.order-qty {
+  font-size: 22rpx;
+  color: #999;
+}
+.order-footer {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  border-top: 1rpx solid #f5f5f5;
+  padding-top: 16rpx;
+}
+.order-time {
+  font-size: 22rpx;
+  color: #999;
+}
+.order-actions {
+  display: flex;
+  gap: 12rpx;
+}
+.btn-xs {
+  font-size: 22rpx;
+  padding: 8rpx 24rpx;
+  border-radius: 30rpx;
+  border: none;
+  line-height: 1.5;
+}
+.btn-confirm {
+  background: #e6f7ff;
+  color: #1890ff;
+}
+.btn-xs::after {
+  border: none;
+}
+.no-more {
+  text-align: center;
+  padding: 30rpx;
+}
+.no-more-text {
+  font-size: 24rpx;
+  color: #ccc;
+}
+</style>

+ 300 - 0
zxyj-frontend/pages/vendor/products/products.vue

@@ -0,0 +1,300 @@
+<template>
+  <view class="container">
+    <view class="status-tabs">
+      <view
+        v-for="item in statusList"
+        :key="item.value"
+        :class="['tab', currentStatus === item.value ? 'active' : '']"
+        @click="onStatusChange(item.value)"
+      >
+        {{ item.label }}
+      </view>
+    </view>
+
+    <scroll-view scroll-y class="product-list" @scrolltolower="onLoadMore" refresher-enabled :refresher-triggered="refreshing" @refresherrefresh="onRefresh">
+      <view v-if="loading && products.length === 0" class="loading-wrap">
+        <text class="loading-text">加载中...</text>
+      </view>
+      <view v-else-if="products.length === 0" class="empty-wrap">
+        <text class="empty-icon">📦</text>
+        <text class="empty-text">暂无商品</text>
+        <button class="btn-add" @click="goCreate">发布商品</button>
+      </view>
+      <view v-else>
+        <view
+          v-for="item in products"
+          :key="item.id"
+          class="product-card"
+        >
+          <view class="card-top">
+            <image class="cover" :src="item.coverImage || '/static/default-product.png'" mode="aspectFill" />
+            <view class="card-info">
+              <text class="product-name">{{ item.name }}</text>
+              <text class="product-price">¥{{ item.price }}</text>
+              <text class="product-stock">库存 {{ item.stock || 0 }}</text>
+            </view>
+          </view>
+          <view class="card-bottom">
+            <text :class="['status-badge', 'status-' + item.status]">{{ statusLabel(item.status) }}</text>
+            <view class="card-actions">
+              <button v-if="item.status === 'approved'" class="btn-xs" @click="onShelve(item.id, item.status === 'on_shelf' ? 'unshelve' : 'shelve')">
+                {{ item.status === 'on_shelf' ? '下架' : '上架' }}
+              </button>
+              <button class="btn-xs btn-edit" @click="goEdit(item.id)">编辑</button>
+            </view>
+          </view>
+        </view>
+      </view>
+      <view v-if="noMore && products.length > 0" class="no-more">
+        <text class="no-more-text">— 没有更多了 —</text>
+      </view>
+    </scroll-view>
+  </view>
+</template>
+
+<script>
+import { productMy, productShelve } from '@/utils/api.js'
+
+export default {
+  data() {
+    return {
+      statusList: [
+        { label: '全部', value: '' },
+        { label: '待审核', value: 'pending' },
+        { label: '已通过', value: 'approved' },
+        { label: '已拒绝', value: 'rejected' },
+        { label: '已上架', value: 'on_shelf' }
+      ],
+      currentStatus: '',
+      products: [],
+      page: 1,
+      size: 20,
+      loading: false,
+      refreshing: false,
+      noMore: false
+    }
+  },
+  onLoad() {
+    this.loadProducts()
+  },
+  methods: {
+    onStatusChange(value) {
+      this.currentStatus = value
+      this.page = 1
+      this.products = []
+      this.noMore = false
+      this.loadProducts()
+    },
+    loadProducts() {
+      if (this.loading) return
+      this.loading = true
+      productMy({ page: this.page, size: this.size, status: this.currentStatus }).then(res => {
+        this.loading = false
+        this.refreshing = false
+        if (res.code === 200 && res.data) {
+          const list = res.data.records || res.data || []
+          if (this.page === 1) {
+            this.products = list
+          } else {
+            this.products = this.products.concat(list)
+          }
+          const total = res.data.total || 0
+          this.noMore = this.products.length >= total
+        }
+      }).catch(() => {
+        this.loading = false
+        this.refreshing = false
+      })
+    },
+    onRefresh() {
+      this.refreshing = true
+      this.page = 1
+      this.noMore = false
+      this.loadProducts()
+    },
+    onLoadMore() {
+      if (this.noMore) return
+      this.page = this.page + 1
+      this.loadProducts()
+    },
+    onShelve(id, action) {
+      productShelve({ productId: id, action }).then(res => {
+        if (res.code === 200) {
+          uni.showToast({ title: action === 'shelve' ? '上架成功' : '下架成功', icon: 'success' })
+          this.onRefresh()
+        } else {
+          uni.showToast({ title: res.message || '操作失败', icon: 'none' })
+        }
+      })
+    },
+    goCreate() {
+      uni.navigateTo({ url: '/pages/vendor/product-edit/product-edit' })
+    },
+    goEdit(id) {
+      uni.navigateTo({ url: '/pages/vendor/product-edit/product-edit?id=' + id })
+    },
+    statusLabel(status) {
+      const map = {
+        draft: '草稿',
+        pending: '待审核',
+        approved: '已通过',
+        rejected: '已拒绝',
+        on_shelf: '已上架',
+        off_shelf: '已下架'
+      }
+      return map[status] || status
+    }
+  }
+}
+</script>
+
+<style scoped>
+.container {
+  display: flex;
+  flex-direction: column;
+  height: 100vh;
+  background: #f5f5f5;
+}
+.status-tabs {
+  display: flex;
+  background: #fff;
+  padding: 20rpx 0;
+  border-bottom: 1rpx solid #eee;
+}
+.tab {
+  flex: 1;
+  text-align: center;
+  font-size: 26rpx;
+  color: #666;
+  padding: 10rpx 0;
+}
+.tab.active {
+  color: #ff6b6b;
+  font-weight: bold;
+  border-bottom: 4rpx solid #ff6b6b;
+}
+.product-list {
+  flex: 1;
+  height: calc(100vh - 100rpx);
+}
+.loading-wrap,
+.empty-wrap {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding-top: 200rpx;
+}
+.loading-text {
+  font-size: 26rpx;
+  color: #999;
+}
+.empty-icon {
+  font-size: 100rpx;
+  margin-bottom: 30rpx;
+}
+.empty-text {
+  font-size: 28rpx;
+  color: #999;
+  margin-bottom: 40rpx;
+}
+.btn-add {
+  background: linear-gradient(135deg, #ff6b6b, #ff8e53);
+  color: #fff;
+  font-size: 28rpx;
+  padding: 0 60rpx;
+  height: 72rpx;
+  line-height: 72rpx;
+  border-radius: 36rpx;
+  border: none;
+}
+.btn-add::after {
+  border: none;
+}
+.product-card {
+  background: #fff;
+  margin: 20rpx;
+  border-radius: 16rpx;
+  padding: 24rpx;
+  box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.06);
+}
+.card-top {
+  display: flex;
+  margin-bottom: 16rpx;
+}
+.cover {
+  width: 160rpx;
+  height: 160rpx;
+  border-radius: 12rpx;
+  background: #f0f0f0;
+  flex-shrink: 0;
+}
+.card-info {
+  flex: 1;
+  margin-left: 20rpx;
+  display: flex;
+  flex-direction: column;
+  justify-content: space-between;
+}
+.product-name {
+  font-size: 28rpx;
+  color: #333;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+.product-price {
+  font-size: 30rpx;
+  color: #ff6b6b;
+  font-weight: bold;
+}
+.product-stock {
+  font-size: 22rpx;
+  color: #999;
+}
+.card-bottom {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  border-top: 1rpx solid #f5f5f5;
+  padding-top: 16rpx;
+}
+.status-badge {
+  font-size: 22rpx;
+  padding: 4rpx 16rpx;
+  border-radius: 20rpx;
+}
+.status-draft { background: #f5f5f5; color: #999; }
+.status-pending { background: #fff7e6; color: #fa8c16; }
+.status-approved { background: #f6ffed; color: #52c41a; }
+.status-rejected { background: #fff2f0; color: #ff4d4f; }
+.status-on_shelf { background: #e6f7ff; color: #1890ff; }
+.status-off_shelf { background: #f5f5f5; color: #999; }
+.card-actions {
+  display: flex;
+  gap: 12rpx;
+}
+.btn-xs {
+  font-size: 22rpx;
+  padding: 8rpx 24rpx;
+  border-radius: 30rpx;
+  border: 1rpx solid #ff6b6b;
+  color: #ff6b6b;
+  background: #fff;
+  line-height: 1.5;
+}
+.btn-edit {
+  border-color: #1890ff;
+  color: #1890ff;
+}
+.btn-xs::after {
+  border: none;
+}
+.no-more {
+  text-align: center;
+  padding: 30rpx;
+}
+.no-more-text {
+  font-size: 24rpx;
+  color: #ccc;
+}
+</style>