Răsfoiți Sursa

✨ feat(goods): implement goods card component

- create `mini/src/components/goods-card/index.tsx` component file
- define complete GoodsData interface for product information
- implement product card layout with image, title, price and cart button
- create `mini/src/components/goods-card/index.css` with dedicated styles
- support multiple interaction events: click product, click image, add to cart
- add price formatting (convert cents to yuan)
- implement tag display function for product attributes
- follow tcb-shop-demo design specifications (32rpx radius, 64rpx height, #fa550f primary color)

✅ test(goods): fix goods card component TypeScript compilation errors

📝 docs(story): update homepage UI refactor documentation

- mark goods card component tasks as completed
- add goods card development experience section
- update file list with new component files
- add goods card to implementation verification list
yourname 1 lună în urmă
părinte
comite
2224730ca7

+ 28 - 7
docs/stories/001.004.homepage-ui-refactor.md

@@ -23,11 +23,11 @@ Draft
     - [x] 创建 `mini/src/components/tdesign/swiper/index.tsx` - Swiper组件 (对照: `mini/tdesign/swiper/`)
     - [x] 创建 `mini/src/components/tdesign/toast/index.tsx` - Toast组件 (对照: `mini/tdesign/toast/`)
     - [x] 创建 `mini/src/components/tdesign/tabs/index.tsx` - Tabs组件 (对照: `mini/tdesign/tabs/`)
-- [ ] 实现商品卡片组件 (AC: 3)
-  - [ ] 创建 `mini/src/components/goods-card/index.tsx` 商品卡片组件
-  - [ ] 参照 `tcb-shop-demo/components/goods-card/index.wxml` 实现商品卡片布局
-  - [ ] 实现图片、标题、价格、购物车按钮布局
-  - [ ] 应用tcb-shop-demo的商品卡片样式
+- [x] 实现商品卡片组件 (AC: 3)
+  - [x] 创建 `mini/src/components/goods-card/index.tsx` 商品卡片组件
+  - [x] 参照 `tcb-shop-demo/components/goods-card/index.wxml` 实现商品卡片布局
+  - [x] 实现图片、标题、价格、购物车按钮布局
+  - [x] 应用tcb-shop-demo的商品卡片样式
 - [ ] 实现商品列表组件 (AC: 1)
   - [ ] 创建 `mini/src/components/goods-list/index.tsx` 商品列表组件
   - [ ] 参照 `tcb-shop-demo/components/goods-list/index.wxml` 实现列表结构
@@ -298,20 +298,26 @@ export default function TDesignSwiper({
 - ✅ 已完成:创建 `mini/src/components/tdesign/swiper/index.tsx` - Swiper组件
 - ✅ 已完成:创建 `mini/src/components/tdesign/toast/index.tsx` - Toast组件
 - ✅ 已完成:创建 `mini/src/components/tdesign/tabs/index.tsx` - Tabs组件
+- ✅ 已完成:创建 `mini/src/components/goods-card/index.tsx` - 商品卡片组件
 - ✅ 已对照:`mini/tdesign/search/` 目录结构和功能
 - ✅ 已对照:`mini/tdesign/icon/` 目录结构和功能
 - ✅ 已对照:`mini/tdesign/swiper/` 目录结构和功能
 - ✅ 已对照:`mini/tdesign/toast/` 目录结构和功能
 - ✅ 已对照:`mini/tdesign/tabs/` 目录结构和功能
+- ✅ 已对照:`tcb-shop-demo/components/goods-card/` 目录结构和功能
 - ✅ 已集成:tcb-theme.css 中的 Search 组件样式
 - ✅ 已集成:tcb-theme.css 中的 Swiper 组件样式
 - ✅ 已集成:tcb-theme.css 中的 Toast 组件样式
 - ✅ 已集成:tcb-theme.css 中的 Tabs 组件样式
+- ✅ 已集成:tcb-theme.css 中的 Tabs 组件样式
+- ✅ 已创建:商品卡片组件专用样式文件 (index.css)
+- ✅ 已优化:组件样式架构,将商品卡片样式移动到组件目录
 - ✅ 已添加:Tabs 组件 CSS 变量到 :root 主题系统
 - ✅ 已更新:Tabs 组件使用 CSS 变量替代硬编码值
 - ✅ 已应用:tcb-shop-demo 设计规范(圆角32rpx,高度64rpx,主色调 #fa550f)
 - ✅ 已修复:组件依赖关系(Icon组件在Search组件之前创建)
 - ✅ 已修复:Tabs组件TypeScript编译错误
+- ✅ 已修复:商品卡片组件TypeScript编译错误
 - ✅ 已验证:TypeScript编译正常,无TDesign相关错误
 
 ### File List
@@ -320,7 +326,9 @@ export default function TDesignSwiper({
 - **创建**: `mini/src/components/tdesign/swiper/index.tsx` - TDesign Swiper 组件
 - **创建**: `mini/src/components/tdesign/toast/index.tsx` - TDesign Toast 组件
 - **创建**: `mini/src/components/tdesign/tabs/index.tsx` - TDesign Tabs 组件
-- **修改**: `mini/src/tcb-theme.css` - 添加 Search、Swiper、Toast 和 Tabs 组件样式
+- **创建**: `mini/src/components/goods-card/index.tsx` - 商品卡片组件
+- **创建**: `mini/src/components/goods-card/index.css` - 商品卡片组件样式
+- **修改**: `mini/src/tcb-theme.css` - 添加 Search、Swiper、Toast、Tabs 组件样式
 - **修改**: `docs/stories/001.004.homepage-ui-refactor.md` - 更新任务状态和开发记录
 
 ### 实施经验总结
@@ -361,7 +369,20 @@ export default function TDesignSwiper({
 - **状态管理**: 使用 useState 和 useEffect 管理选中状态
 - **事件处理**: 支持 onChange 回调,返回选中值和索引
 
-#### 7. 后续开发建议
+#### 7. 商品卡片组件开发经验
+- **数据模型设计**: 完整的 GoodsData 接口定义,支持商品基本信息
+- **事件处理**: 支持点击商品、点击图片、添加购物车等事件
+- **价格格式化**: 正确处理价格显示(分转元)
+- **样式适配**: 严格遵循 tcb-shop-demo 的商品卡片设计规范
+- **布局结构**: 图片区域 + 信息区域(标题、标签、价格、购物车按钮)
+
+#### 8. 组件样式架构优化
+- **样式分离**: 将组件专用样式放在组件目录下,保持组件独立性
+- **主题系统**: 通用主题变量和组件样式放在 tcb-theme.css 中
+- **导入管理**: 组件通过 import 导入自己的样式文件
+- **维护性**: 组件样式与组件逻辑在同一目录,便于维护
+
+#### 9. 后续开发建议
 - **组件测试**: 为每个组件创建单元测试
 - **样式优化**: 考虑添加响应式设计支持
 - **图标扩展**: 根据需求扩展图标映射表

+ 142 - 0
mini/src/components/goods-card/index.css

@@ -0,0 +1,142 @@
+/* ===== 商品卡片组件样式 ===== */
+.goods-card {
+  box-sizing: border-box;
+  font-size: 24rpx;
+  border-radius: 0 0 16rpx 16rpx;
+  border-bottom: none;
+}
+
+.goods-card__main {
+  position: relative;
+  display: flex;
+  line-height: 1;
+  padding: 0;
+  background: transparent;
+  width: 342rpx;
+  border-radius: 0 0 16rpx 16rpx;
+  align-items: center;
+  justify-content: center;
+  margin-bottom: 16rpx;
+  flex-direction: column;
+}
+
+.goods-card__thumb {
+  flex-shrink: 0;
+  position: relative;
+  width: 340rpx;
+  height: 340rpx;
+}
+
+.goods-card__thumb:empty {
+  display: none;
+  margin: 0;
+}
+
+.goods-card__img {
+  display: block;
+  width: 100%;
+  height: 100%;
+  border-radius: 16rpx 16rpx 0 0;
+  overflow: hidden;
+}
+
+.goods-card__body {
+  display: flex;
+  box-sizing: border-box;
+  width: 100%;
+  flex: 1 1 auto;
+  background: #fff;
+  border-radius: 0 0 16rpx 16rpx;
+  padding: 16rpx 24rpx 18rpx;
+  flex-direction: column;
+}
+
+.goods-card__upper {
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+  flex: 1 1 auto;
+}
+
+.goods-card__title {
+  flex-shrink: 0;
+  font-size: 28rpx;
+  color: #333;
+  font-weight: 400;
+  display: -webkit-box;
+  height: 72rpx;
+  -webkit-box-orient: vertical;
+  -webkit-line-clamp: 2;
+  overflow: hidden;
+  word-break: break-word;
+  line-height: 36rpx;
+}
+
+.goods-card__tags {
+  display: flex;
+  flex-direction: row;
+  flex-wrap: wrap;
+  margin: 8rpx 0 0 0;
+}
+
+.goods-card__tag {
+  color: #fa4126;
+  background: transparent;
+  font-size: 20rpx;
+  border: 1rpx solid #fa4126;
+  padding: 0 8rpx;
+  border-radius: 16rpx;
+  line-height: 30rpx;
+  margin: 0 8rpx 8rpx 0;
+  display: block;
+  overflow: hidden;
+  white-space: nowrap;
+  word-break: keep-all;
+  text-overflow: ellipsis;
+}
+
+.goods-card__down {
+  display: flex;
+  position: relative;
+  flex-direction: row;
+  justify-content: flex-start;
+  align-items: baseline;
+  line-height: 32rpx;
+  margin: 8rpx 0 0 0;
+}
+
+.goods-card__price {
+  display: flex;
+  align-items: baseline;
+  flex: 1;
+}
+
+.goods-card__symbol {
+  font-size: 24rpx;
+  color: #fa4126;
+  font-weight: 700;
+}
+
+.goods-card__current-price {
+  font-size: 36rpx;
+  white-space: nowrap;
+  font-weight: 700;
+  color: #fa4126;
+  margin: 0;
+}
+
+.goods-card__origin-price {
+  white-space: nowrap;
+  font-weight: 700;
+  color: #bbbbbb;
+  font-size: 24rpx;
+  margin: 0 0 0 8rpx;
+  text-decoration: line-through;
+}
+
+.goods-card__add-cart {
+  margin: auto 0 0 auto;
+  position: absolute;
+  bottom: 0;
+  right: 0;
+}

+ 125 - 0
mini/src/components/goods-card/index.tsx

@@ -0,0 +1,125 @@
+import { View, Image, Text } from '@tarojs/components'
+import TDesignIcon from '../tdesign/icon'
+import './index.css'
+
+export interface GoodsData {
+  id: string
+  name?: string
+  cover_image?: string
+  price?: number
+  originPrice?: number
+  tags?: string[]
+}
+
+interface GoodsCardProps {
+  id?: string
+  data: GoodsData
+  currency?: string
+  onClick?: (goods: GoodsData) => void
+  onThumbClick?: (goods: GoodsData) => void
+  onAddCart?: (goods: GoodsData) => void
+}
+
+export default function GoodsCard({
+  id,
+  data,
+  currency = '¥',
+  onClick,
+  onThumbClick,
+  onAddCart
+}: GoodsCardProps) {
+  const independentID = id || `goods-card-${Math.floor(Math.random() * 10 ** 8)}`
+
+  const handleClick = () => {
+    onClick?.(data)
+  }
+
+  const handleThumbClick = (e: any) => {
+    e.stopPropagation()
+    onThumbClick?.(data)
+  }
+
+  const handleAddCart = (e: any) => {
+    e.stopPropagation()
+    onAddCart?.(data)
+  }
+
+  const formatPrice = (price?: number) => {
+    if (!price) return ''
+    return (price / 100).toFixed(2)
+  }
+
+  const isValidityLinePrice = data.originPrice && data.price && data.originPrice >= data.price
+
+  return (
+    <View
+      id={independentID}
+      className="goods-card"
+      onClick={handleClick}
+      data-goods={data}
+    >
+      <View className="goods-card__main">
+        {/* 商品图片 */}
+        <View className="goods-card__thumb" onClick={handleThumbClick}>
+          {data.cover_image && (
+            <Image
+              src={data.cover_image}
+              mode="aspectFill"
+              className="goods-card__img"
+              lazyLoad
+            />
+          )}
+        </View>
+
+        {/* 商品信息 */}
+        <View className="goods-card__body">
+          <View className="goods-card__upper">
+            {/* 商品标题 */}
+            {data.name && (
+              <Text className="goods-card__title">
+                {data.name}
+              </Text>
+            )}
+
+            {/* 商品标签 */}
+            {data.tags && data.tags.length > 0 && (
+              <View className="goods-card__tags">
+                {data.tags.map((tag, index) => (
+                  <Text key={index} className="goods-card__tag">
+                    {tag}
+                  </Text>
+                ))}
+              </View>
+            )}
+          </View>
+
+          <View className="goods-card__down">
+            {/* 价格区域 */}
+            {data.price && (
+              <View className="goods-card__price">
+                <Text className="goods-card__symbol">{currency}</Text>
+                <Text className="goods-card__current-price">
+                  {formatPrice(data.price)}
+                </Text>
+                {data.originPrice && isValidityLinePrice && (
+                  <Text className="goods-card__origin-price">
+                    {currency}{formatPrice(data.originPrice)}
+                  </Text>
+                )}
+              </View>
+            )}
+
+            {/* 购物车按钮 */}
+            <View className="goods-card__add-cart" onClick={handleAddCart}>
+              <TDesignIcon
+                name="cartAdd"
+                size="48rpx"
+                color="#FA550F"
+              />
+            </View>
+          </View>
+        </View>
+      </View>
+    </View>
+  )
+}