feat(common): 抽取通用 Badge 和 ArrowRight 组件并统一替换内联角标
- 新增 Badge 组件,替代项目内多处重复数字角标代码,支持 count、max、颜色、尺寸等配置 - 新增 ArrowRight 组件,替代项目内多处 '>' 文本箭头,采用 CSS 绘制,支持颜色和尺寸配置 - user、index 和 store页面相关角标已改用 Badge 组件,简化代码和统一样式 - user 页面箭头符号替换为 ArrowRight 组件,提升图标一致性和样式可控性 - app.config.ts 调整 tabbar 配置,将“订单”页替换为“购物车”页
This commit is contained in:
@@ -286,3 +286,47 @@
|
|||||||
1. 角标位置 `top-0 right-2` → `-top-1 -right-1`:让小红点紧贴 cell 右上角外缘,更像标准徽标
|
1. 角标位置 `top-0 right-2` → `-top-1 -right-1`:让小红点紧贴 cell 右上角外缘,更像标准徽标
|
||||||
2. 角标尺寸 `text-xs` + 自由拉伸 → `min-w-[16px] h-4` + 10px/12px 字体:单数字也保持圆形,多位数字自动变椭圆
|
2. 角标尺寸 `text-xs` + 自由拉伸 → `min-w-[16px] h-4` + 10px/12px 字体:单数字也保持圆形,多位数字自动变椭圆
|
||||||
3. 0 判断 `item.count && item.count > 0` → `item.count && Number(item.count) > 0`:用 `Number()` 包一层,防止后端返回字符串 "0" 时 `> 0` 误判
|
3. 0 判断 `item.count && item.count > 0` → `item.count && Number(item.count) > 0`:用 `Number()` 包一层,防止后端返回字符串 "0" 时 `> 0` 误判
|
||||||
|
|
||||||
|
## 抽取通用 Badge 组件(2026-07-15 23:10)
|
||||||
|
- 新文件:`src/components/common/Badge/index.tsx`
|
||||||
|
- 用途:替代项目里多处复制粘贴的内联角标 View(user.tsx / index/index.tsx / store/center/index.tsx 都有 `min-w-[16px] h-4 px-1 bg-red-500 rounded-full` 这种重复写法)
|
||||||
|
- API:
|
||||||
|
```ts
|
||||||
|
interface BadgeProps {
|
||||||
|
count?: number | null // 0 / null / undefined / 负数 → 不渲染
|
||||||
|
max?: number // 默认 99,超过显示 `${max}+`
|
||||||
|
className?: string // 外层定位 className,如 'absolute -top-1 -right-1'
|
||||||
|
color?: string // 默认 #ef4444(red-500)
|
||||||
|
fontSize?: number // 默认 10px
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- user.tsx 已替换:内联 `{Number(item.count) > 0 && <View ...>}` → `<Badge count={item.count} className='absolute -top-1 -right-1' />`
|
||||||
|
- 风格遵循项目 common 组件约定:函数式组件 + 默认导出 + Taro View/Text + Tailwind className
|
||||||
|
- 待办(未做):首页 `src/pages/index/index.tsx:248` 和门店中心 `src/pages/store/center/index.tsx:218` 也有相同内联角标写法,后续可统一替换为 Badge
|
||||||
|
|
||||||
|
## 抽取通用 ArrowRight 组件(2026-07-15 23:13)
|
||||||
|
- 新文件:`src/components/common/ArrowRight/index.tsx`
|
||||||
|
- 用途:替代项目里到处可见的 `{'>'}` / `'>'` 文本箭头
|
||||||
|
- 实现:CSS V 形(border-top + border-right + rotate(45deg)),颜色用 `currentColor` 跟父级 `text-*` 类走
|
||||||
|
- API:
|
||||||
|
```ts
|
||||||
|
interface ArrowRightProps {
|
||||||
|
className?: string // 颜色用 text-* 类(如 'text-gray-400'),间距用 ml-1 等
|
||||||
|
size?: number // V 形边长 px,默认 6
|
||||||
|
thickness?: number // 描边粗细 px,默认 1.5
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- user.tsx 已替换 3 处 `{'>'}`:
|
||||||
|
1. 用户卡片头部箭头(白色 60% 透明)→ `<ArrowRight className='text-white text-opacity-60' />`
|
||||||
|
2. 「全部订单 >」→ 拆成 `<View className='flex items-center text-xs text-gray-400'><Text>全部订单</Text><ArrowRight className='ml-1' /></View>`
|
||||||
|
3. 菜单项末尾箭头(amber-700 / gray-300)→ `<ArrowRight className={...} />`
|
||||||
|
- 关键点:Tailwind 的 `text-opacity-60` 会通过 `--tw-text-opacity` 影响 color,currentColor 会继承这个带 opacity 的 color,所以 ArrowRight 的透明度也会跟随
|
||||||
|
- 待办(未做):其它页面也有 `{'>'}` 文本箭头,后续可统一替换
|
||||||
|
|
||||||
|
## 统一替换 index/index.tsx 和 store/center 的内联角标为 Badge(2026-07-15 23:17)
|
||||||
|
- Badge 组件 API 扩展:新增 `size`(默认 16)和 `fontWeight`(默认 'normal')参数;padding 从 className 的 `px-1` 改为 inline style 根据 size 自动计算(16→2px,20→3px),避免与调用方 className 冲突
|
||||||
|
- `src/pages/index/index.tsx:248`:数字角标(16x16/10px)→ `<Badge count={newOrderCount} className='absolute -top-1 -right-1' />`
|
||||||
|
- 注意:保留了 `isClerk && newOrderCount > 0` 外层判断(虽然 Badge 内部 0 不渲染,但下面还有 `newOrderCount === 0` 的小红点逻辑,保留判断让语义更明确)
|
||||||
|
- `src/pages/store/center/index.tsx:218`:数字角标(20x20/11px/加粗)→ `<Badge count={getBadgeCount(card.key)} className='absolute -top-1 -right-1' size={20} fontSize={11} fontWeight='bold' />`
|
||||||
|
- 简化了外层判断:`card.showBadge && getBadgeCount > 0` → `card.showBadge`(Badge 内部已处理 0 不渲染)
|
||||||
|
- 项目内联角标已全部统一为 Badge 组件,共 3 处(user.tsx / index/index.tsx / store/center/index.tsx)
|
||||||
|
|||||||
@@ -160,10 +160,10 @@ export default {
|
|||||||
text: '分类',
|
text: '分类',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
pagePath: 'pages/order/list',
|
pagePath: 'pages/shop/cart',
|
||||||
iconPath: 'assets/tabbar/order.png',
|
iconPath: 'assets/tabbar/cart.png',
|
||||||
selectedIconPath: 'assets/tabbar/order-active.png',
|
selectedIconPath: 'assets/tabbar/cart-active.png',
|
||||||
text: '订单',
|
text: '购物车',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
pagePath: 'pages/user/user',
|
pagePath: 'pages/user/user',
|
||||||
|
|||||||
BIN
src/assets/tabbar/cart-active.png
Normal file
BIN
src/assets/tabbar/cart-active.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
BIN
src/assets/tabbar/cart.png
Normal file
BIN
src/assets/tabbar/cart.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
42
src/components/common/ArrowRight/index.tsx
Normal file
42
src/components/common/ArrowRight/index.tsx
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { View } from '@tarojs/components'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 右箭头图标(CSS V 形,颜色跟随 currentColor)
|
||||||
|
*
|
||||||
|
* 颜色通过 className 里的 text-* 类控制(如 'text-gray-400' / 'text-white'),
|
||||||
|
* 因为内部用 currentColor,会自动继承父元素 text color。
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* <View className='flex items-center text-gray-400'>
|
||||||
|
* <Text>全部订单</Text>
|
||||||
|
* <ArrowRight className='ml-1' />
|
||||||
|
* </View>
|
||||||
|
*/
|
||||||
|
export interface ArrowRightProps {
|
||||||
|
/** 颜色/间距等 className(颜色用 text-* 类,跟随 currentColor) */
|
||||||
|
className?: string
|
||||||
|
/** V 形边长 px,默认 6 */
|
||||||
|
size?: number
|
||||||
|
/** 描边粗细 px,默认 1.5 */
|
||||||
|
thickness?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const ArrowRight: React.FC<ArrowRightProps> = ({
|
||||||
|
className = '',
|
||||||
|
size = 6,
|
||||||
|
thickness = 1.5,
|
||||||
|
}) => (
|
||||||
|
<View
|
||||||
|
className={`inline-flex items-center justify-center ${className}`}
|
||||||
|
style={{
|
||||||
|
width: `${size}px`,
|
||||||
|
height: `${size}px`,
|
||||||
|
borderTop: `${thickness}px solid currentColor`,
|
||||||
|
borderRight: `${thickness}px solid currentColor`,
|
||||||
|
transform: 'rotate(45deg)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default ArrowRight
|
||||||
72
src/components/common/Badge/index.tsx
Normal file
72
src/components/common/Badge/index.tsx
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { View, Text } from '@tarojs/components'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数字角标
|
||||||
|
* - count 为 0 / null / undefined 时不渲染
|
||||||
|
* - count > max 时显示 `${max}+`(默认 99+)
|
||||||
|
* - 通过 className 控制定位(通常配合父元素 relative + absolute -top-1 -right-1)
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* <View className='relative'>
|
||||||
|
* <Text>📦</Text>
|
||||||
|
* <Badge count={14} className='absolute -top-1 -right-1' />
|
||||||
|
* </View>
|
||||||
|
*
|
||||||
|
* // 大尺寸 + 加粗(如门店中心卡片角标)
|
||||||
|
* <Badge count={5} className='absolute -top-1 -right-1' size={20} fontSize={11} fontWeight='bold' />
|
||||||
|
*/
|
||||||
|
export interface BadgeProps {
|
||||||
|
/** 数量;为 0 / null / undefined 时不渲染 */
|
||||||
|
count?: number | null
|
||||||
|
/** 超过此值显示 `${max}+`,默认 99 */
|
||||||
|
max?: number
|
||||||
|
/** 外层定位/样式 className(如 'absolute -top-1 -right-1') */
|
||||||
|
className?: string
|
||||||
|
/** 背景色,默认红色 #ef4444 */
|
||||||
|
color?: string
|
||||||
|
/** 字体大小 px,默认 10 */
|
||||||
|
fontSize?: number
|
||||||
|
/** 角标尺寸 px(min-width 与 height 同步),默认 16 */
|
||||||
|
size?: number
|
||||||
|
/** 字重,默认 'normal' */
|
||||||
|
fontWeight?: 'normal' | 'medium' | 'bold'
|
||||||
|
}
|
||||||
|
|
||||||
|
const Badge: React.FC<BadgeProps> = ({
|
||||||
|
count,
|
||||||
|
max = 99,
|
||||||
|
className = '',
|
||||||
|
color = '#ef4444',
|
||||||
|
fontSize = 10,
|
||||||
|
size = 16,
|
||||||
|
fontWeight = 'normal',
|
||||||
|
}) => {
|
||||||
|
// 0 / null / undefined / 负数 都不渲染
|
||||||
|
const n = Number(count) || 0
|
||||||
|
if (n <= 0) return null
|
||||||
|
|
||||||
|
const display = n > max ? `${max}+` : String(n)
|
||||||
|
// 横向 padding 跟随 size:16→2px,20→3px,避免多位数字挤压
|
||||||
|
const padding = Math.max(2, Math.round(size / 8))
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
className={`rounded-full text-white flex items-center justify-center ${className}`}
|
||||||
|
style={{
|
||||||
|
background: color,
|
||||||
|
minWidth: `${size}px`,
|
||||||
|
height: `${size}px`,
|
||||||
|
fontSize: `${fontSize}px`,
|
||||||
|
lineHeight: `${fontSize + 2}px`,
|
||||||
|
fontWeight,
|
||||||
|
paddingLeft: `${padding}px`,
|
||||||
|
paddingRight: `${padding}px`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text>{display}</Text>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Badge
|
||||||
@@ -15,6 +15,7 @@ import type { CmsAd } from '@/api/cms/cmsAd/model'
|
|||||||
import type { CmsArticle } from '@/api/cms/cmsArticle/model'
|
import type { CmsArticle } from '@/api/cms/cmsArticle/model'
|
||||||
import type { ShopGoods } from '@/api/shop/shopGoods/model'
|
import type { ShopGoods } from '@/api/shop/shopGoods/model'
|
||||||
import Price from '@/components/common/Price'
|
import Price from '@/components/common/Price'
|
||||||
|
import Badge from '@/components/common/Badge'
|
||||||
import { getCompressedImageUrl } from '@/utils/image'
|
import { getCompressedImageUrl } from '@/utils/image'
|
||||||
|
|
||||||
definePageConfig({
|
definePageConfig({
|
||||||
@@ -245,11 +246,7 @@ const IndexPage: React.FC = () => {
|
|||||||
<View onClick={handleMessageClick} className='relative'>
|
<View onClick={handleMessageClick} className='relative'>
|
||||||
<Text className='text-xl'>🔔</Text>
|
<Text className='text-xl'>🔔</Text>
|
||||||
{isClerk && newOrderCount > 0 && (
|
{isClerk && newOrderCount > 0 && (
|
||||||
<View className='absolute -top-1 -right-1 min-w-[16px] h-4 px-1 bg-red-500 rounded-full flex items-center justify-center'>
|
<Badge count={newOrderCount} className='absolute -top-1 -right-1' />
|
||||||
<Text className='text-white text-[10px] leading-none font-medium'>
|
|
||||||
{newOrderCount > 99 ? '99+' : newOrderCount}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
)}
|
)}
|
||||||
{isClerk && newOrderCount === 0 && (
|
{isClerk && newOrderCount === 0 && (
|
||||||
<View className='absolute top-0 right-0 w-2 h-2 bg-red-500 rounded-full' />
|
<View className='absolute top-0 right-0 w-2 h-2 bg-red-500 rounded-full' />
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import Taro from '@tarojs/taro'
|
|||||||
import { getMyClerk } from '@/api/shop/shopStoreUser'
|
import { getMyClerk } from '@/api/shop/shopStoreUser'
|
||||||
import { listShopDealerApply } from '@/api/shop/shopDealerApply'
|
import { listShopDealerApply } from '@/api/shop/shopDealerApply'
|
||||||
import { pageShopOrder } from '@/api/shop/shopOrder'
|
import { pageShopOrder } from '@/api/shop/shopOrder'
|
||||||
|
import Badge from '@/components/common/Badge'
|
||||||
|
|
||||||
definePageConfig({
|
definePageConfig({
|
||||||
navigationBarTitleText: '门店中心',
|
navigationBarTitleText: '门店中心',
|
||||||
@@ -214,12 +215,8 @@ export default function StoreCenterPage() {
|
|||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* 待处理角标 */}
|
{/* 待处理角标 */}
|
||||||
{card.showBadge && getBadgeCount(card.key) > 0 && (
|
{card.showBadge && (
|
||||||
<View className='absolute -top-1 -right-1 min-w-[20px] h-[20px] rounded-full bg-red-500 flex items-center justify-center px-1.5'>
|
<Badge count={getBadgeCount(card.key)} className='absolute -top-1 -right-1' size={20} fontSize={11} fontWeight='bold' />
|
||||||
<Text className='text-white text-[11px] font-bold'>
|
|
||||||
{getBadgeCount(card.key) > 99 ? '99+' : getBadgeCount(card.key)}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 箭头 */}
|
{/* 箭头 */}
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import type { UserRole } from '@/api/system/userRole/model'
|
|||||||
import { checkAndCacheVipStatus } from '@/utils/vip'
|
import { checkAndCacheVipStatus } from '@/utils/vip'
|
||||||
import { requireLogin } from '@/utils/login-guard'
|
import { requireLogin } from '@/utils/login-guard'
|
||||||
import MemberBadge from '@/components/business/MemberBadge'
|
import MemberBadge from '@/components/business/MemberBadge'
|
||||||
|
import Badge from '@/components/common/Badge'
|
||||||
|
import ArrowRight from '@/components/common/ArrowRight'
|
||||||
|
|
||||||
const UserPage: React.FC = () => {
|
const UserPage: React.FC = () => {
|
||||||
const scrollHeight = useScrollHeight()
|
const scrollHeight = useScrollHeight()
|
||||||
@@ -215,7 +217,7 @@ const UserPage: React.FC = () => {
|
|||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
{isLoggedIn && <Text className='text-white text-opacity-60 text-sm'>{'>'}</Text>}
|
{isLoggedIn && <ArrowRight className='text-white text-opacity-60' />}
|
||||||
</View>
|
</View>
|
||||||
{/* 数据概览 */}
|
{/* 数据概览 */}
|
||||||
{isLoggedIn && (
|
{isLoggedIn && (
|
||||||
@@ -258,9 +260,10 @@ const UserPage: React.FC = () => {
|
|||||||
<View className='bg-white rounded-xl mx-3 mt-3 p-4'>
|
<View className='bg-white rounded-xl mx-3 mt-3 p-4'>
|
||||||
<View className='flex justify-between items-center mb-3'>
|
<View className='flex justify-between items-center mb-3'>
|
||||||
<Text className='text-base font-medium text-gray-800'>我的订单</Text>
|
<Text className='text-base font-medium text-gray-800'>我的订单</Text>
|
||||||
<Text className='text-xs text-gray-400' onClick={handleViewAllOrders}>
|
<View className='flex items-center text-xs text-gray-400' onClick={handleViewAllOrders}>
|
||||||
全部订单 {'>'}
|
<Text>全部订单</Text>
|
||||||
</Text>
|
<ArrowRight className='ml-1' />
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<View className='grid grid-cols-4 gap-2'>
|
<View className='grid grid-cols-4 gap-2'>
|
||||||
{[
|
{[
|
||||||
@@ -277,12 +280,8 @@ const UserPage: React.FC = () => {
|
|||||||
>
|
>
|
||||||
<Text className='text-xl mb-1'>{item.icon}</Text>
|
<Text className='text-xl mb-1'>{item.icon}</Text>
|
||||||
<Text className='text-xs text-gray-600'>{item.label}</Text>
|
<Text className='text-xs text-gray-600'>{item.label}</Text>
|
||||||
{/* 数量角标:紧贴图标右上角;为 0 时不渲染 */}
|
{/* 数量角标:count 为 0 时组件内部不渲染 */}
|
||||||
{Number(item.count) > 0 && (
|
<Badge count={item.count} className='absolute -top-1 -right-1' />
|
||||||
<View className='absolute -top-1 -right-1 bg-red-500 text-white rounded-full min-w-[16px] h-4 px-1 flex items-center justify-center' style={{ fontSize: '10px', lineHeight: '12px' }}>
|
|
||||||
{Number(item.count) > 99 ? '99+' : item.count}
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</View>
|
</View>
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
@@ -313,7 +312,7 @@ const UserPage: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<Text className={`text-sm ${(item as any).highlight ? 'text-amber-700' : 'text-gray-300'}`}>{'>'}</Text>
|
<ArrowRight className={(item as any).highlight ? 'text-amber-700' : 'text-gray-300'} />
|
||||||
</View>
|
</View>
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
Reference in New Issue
Block a user