商品以及商品详情
This commit is contained in:
@@ -2,6 +2,13 @@ export default defineAppConfig({
|
|||||||
pages: [
|
pages: [
|
||||||
'pages/index/index',
|
'pages/index/index',
|
||||||
'pages/tasks/index',
|
'pages/tasks/index',
|
||||||
|
'pages/task-detail/index',
|
||||||
|
'pages/publish-article/index',
|
||||||
|
'pages/publish-express/index',
|
||||||
|
'pages/publish-secondhand/index',
|
||||||
|
'pages/chat/index',
|
||||||
|
'pages/system-messages/index',
|
||||||
|
'pages/product-detail/index',
|
||||||
'pages/messages/index',
|
'pages/messages/index',
|
||||||
'pages/profile/index',
|
'pages/profile/index',
|
||||||
'pages/article/index',
|
'pages/article/index',
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
/* ========================================
|
||||||
|
商品卡片(双列网格用)
|
||||||
|
======================================== */
|
||||||
|
|
||||||
|
.product-card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
|
||||||
|
transition: transform 0.2s;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: scale(0.97);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 商品图片 ========== */
|
||||||
|
.product-image-wrap {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 340px;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.product-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.discount-tag {
|
||||||
|
position: absolute;
|
||||||
|
top: 12px;
|
||||||
|
left: 12px;
|
||||||
|
background: linear-gradient(135deg, #ee0a24, #ff6034);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 商品信息 ========== */
|
||||||
|
.product-info {
|
||||||
|
padding: 16px;
|
||||||
|
|
||||||
|
.product-title {
|
||||||
|
display: block;
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #1a1a1a;
|
||||||
|
line-height: 1.4;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-tags {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-price-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
|
||||||
|
.product-price {
|
||||||
|
font-size: 36px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #ee0a24;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-original-price {
|
||||||
|
font-size: 24px;
|
||||||
|
color: #c8c9cc;
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-meta {
|
||||||
|
.product-sales {
|
||||||
|
font-size: 22px;
|
||||||
|
color: #969799;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import { View, Text } from '@tarojs/components'
|
||||||
|
import { Image as VantImage, Tag } from '@antmjs/vantui'
|
||||||
|
import type { Product } from '@/types/product'
|
||||||
|
import './index.less'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
product: Product
|
||||||
|
onClick?: (product: Product) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ProductCard({ product, onClick }: Props) {
|
||||||
|
const discount = product.originalPrice
|
||||||
|
? Math.round((product.price / product.originalPrice) * 10)
|
||||||
|
: null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View className='product-card' onClick={() => onClick?.(product)}>
|
||||||
|
{/* 商品图片 */}
|
||||||
|
<View className='product-image-wrap'>
|
||||||
|
<VantImage
|
||||||
|
className='product-image'
|
||||||
|
src={product.image}
|
||||||
|
fit='cover'
|
||||||
|
width='100%'
|
||||||
|
height='100%'
|
||||||
|
radius={12}
|
||||||
|
/>
|
||||||
|
{discount && discount < 10 && (
|
||||||
|
<View className='discount-tag'>{discount}折</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 商品信息 */}
|
||||||
|
<View className='product-info'>
|
||||||
|
<Text className='product-title'>{product.title}</Text>
|
||||||
|
|
||||||
|
{product.tags && product.tags.length > 0 && (
|
||||||
|
<View className='product-tags'>
|
||||||
|
{product.tags.slice(0, 2).map((t) => (
|
||||||
|
<Tag key={t} type='danger' plain size='medium'>
|
||||||
|
{t}
|
||||||
|
</Tag>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<View className='product-price-row'>
|
||||||
|
<Text className='product-price'>¥{product.price.toFixed(1)}</Text>
|
||||||
|
{product.originalPrice && (
|
||||||
|
<Text className='product-original-price'>
|
||||||
|
¥{product.originalPrice.toFixed(1)}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className='product-meta'>
|
||||||
|
<Text className='product-sales'>已售{product.sales}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -71,8 +71,18 @@ export default function CustomTabBar({ activeKey: propKey }: CustomTabBarProps)
|
|||||||
const handlePublishClose = () => setPublishVisible(false)
|
const handlePublishClose = () => setPublishVisible(false)
|
||||||
const handlePublishSelect = (option: PublishOption) => {
|
const handlePublishSelect = (option: PublishOption) => {
|
||||||
setPublishVisible(false)
|
setPublishVisible(false)
|
||||||
|
const pageMap: Record<string, string> = {
|
||||||
|
article: '/pages/publish-article/index',
|
||||||
|
express: '/pages/publish-express/index',
|
||||||
|
goods: '/pages/publish-secondhand/index',
|
||||||
|
}
|
||||||
|
const url = pageMap[option.id]
|
||||||
|
if (url) {
|
||||||
|
Taro.navigateTo({ url })
|
||||||
|
} else {
|
||||||
Taro.showToast({ title: `${option.label}即将上线`, icon: 'none' })
|
Taro.showToast({ title: `${option.label}即将上线`, icon: 'none' })
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 将 4 个 tab 拆分为 [0,1] + 发布按钮 + [2,3]
|
// 将 4 个 tab 拆分为 [0,1] + 发布按钮 + [2,3]
|
||||||
const leftTabs = TAB_LIST.slice(0, 2)
|
const leftTabs = TAB_LIST.slice(0, 2)
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
/** 对话消息 */
|
||||||
|
export interface ChatMessage {
|
||||||
|
id: string
|
||||||
|
sender: 'me' | 'other'
|
||||||
|
content: string
|
||||||
|
time: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 按聊天对象 ID 存储的对话记录 */
|
||||||
|
export const conversations: Record<string, ChatMessage[]> = {
|
||||||
|
// 张同学
|
||||||
|
chat1: [
|
||||||
|
{ id: 'm1', sender: 'other', content: '你好!看到你发布的代取快递任务,我有空可以帮忙', time: '10:25' },
|
||||||
|
{ id: 'm2', sender: 'me', content: '太好了!是南区菜鸟驿站3号柜的快递,你方便什么时间?', time: '10:26' },
|
||||||
|
{ id: 'm3', sender: 'other', content: '我今天下午都有空,2点左右可以吗?', time: '10:28' },
|
||||||
|
{ id: 'm4', sender: 'me', content: '可以的,那就下午2点,我把取件码发给你', time: '10:30' },
|
||||||
|
{ id: 'm5', sender: 'other', content: '好的,那明天下午3点图书馆见,我带教材过去', time: '10:32' },
|
||||||
|
],
|
||||||
|
|
||||||
|
// 李同学
|
||||||
|
chat2: [
|
||||||
|
{ id: 'm1', sender: 'other', content: '同学你好,我已经到菜鸟驿站了,取件码发我一下', time: '09:05' },
|
||||||
|
{ id: 'm2', sender: 'me', content: '好的,取件码是 3-2-8866,谢谢!', time: '09:07' },
|
||||||
|
{ id: 'm3', sender: 'other', content: '收到了,正在取件中', time: '09:10' },
|
||||||
|
{ id: 'm4', sender: 'other', content: '那个快递我已经帮你取了,放你宿舍一楼前台了', time: '09:15' },
|
||||||
|
{ id: 'm5', sender: 'me', content: '太感谢了!报酬我微信转给你', time: '09:16' },
|
||||||
|
],
|
||||||
|
|
||||||
|
// 王同学
|
||||||
|
chat3: [
|
||||||
|
{ id: 'm1', sender: 'other', content: '你好,想问一下你之前发布的那个二手教材还有吗?', time: '昨天 16:20' },
|
||||||
|
{ id: 'm2', sender: 'me', content: '还有的,高等数学和线性代数两本都在', time: '昨天 16:25' },
|
||||||
|
{ id: 'm3', sender: 'other', content: '太好了,能看一下实物照片吗?', time: '昨天 16:28' },
|
||||||
|
{ id: 'm4', sender: 'me', content: '[图片]', time: '昨天 16:30' },
|
||||||
|
{ id: 'm5', sender: 'other', content: '不错,我明天去西区图书馆找你拿可以吗?', time: '昨天 16:35' },
|
||||||
|
{ id: 'm6', sender: 'me', content: '没问题,明天下午我在图书馆自习室', time: '昨天 16:38' },
|
||||||
|
],
|
||||||
|
|
||||||
|
// 骑手-刘师傅
|
||||||
|
chat4: [
|
||||||
|
{ id: 'm1', sender: 'other', content: '你好,我已经接到你的代取订单了', time: '11:00' },
|
||||||
|
{ id: 'm2', sender: 'me', content: '好的师傅,取件码 5-1-3321,菜鸟驿站2号柜', time: '11:02' },
|
||||||
|
{ id: 'm3', sender: 'other', content: '收到,预计15分钟到驿站', time: '11:05' },
|
||||||
|
{ id: 'm4', sender: 'other', content: '好的,快递已取到,预计15分钟送到北门,请准时来取', time: '11:20' },
|
||||||
|
{ id: 'm5', sender: 'me', content: '好的,我这就下楼', time: '11:21' },
|
||||||
|
],
|
||||||
|
|
||||||
|
// 骑手-陈师傅
|
||||||
|
chat5: [
|
||||||
|
{ id: 'm1', sender: 'other', content: '同学,你的快递到了,我在你宿舍楼下', time: '昨天 15:30' },
|
||||||
|
{ id: 'm2', sender: 'me', content: '好的,马上下来', time: '昨天 15:31' },
|
||||||
|
{ id: 'm3', sender: 'other', content: '同学你好,我已经到楼下了,你下来拿一下吧', time: '昨天 15:35' },
|
||||||
|
{ id: 'm4', sender: 'me', content: '不好意思刚有事耽搁了,这就来', time: '昨天 15:38' },
|
||||||
|
{ id: 'm5', sender: 'other', content: '没关系,我等你', time: '昨天 15:39' },
|
||||||
|
],
|
||||||
|
|
||||||
|
// 校园咖啡馆
|
||||||
|
chat6: [
|
||||||
|
{ id: 'm1', sender: 'other', content: '您好,欢迎光临校园咖啡馆!我们今日推荐:冰拿铁 9.9 元', time: '08:30' },
|
||||||
|
{ id: 'm2', sender: 'me', content: '来一杯冰拿铁,少冰', time: '08:32' },
|
||||||
|
{ id: 'm3', sender: 'other', content: '好的,冰拿铁一杯少冰,预计5分钟做好', time: '08:33' },
|
||||||
|
{ id: 'm4', sender: 'other', content: '您的拿铁已经做好了,请来取餐,谢谢!', time: '08:45' },
|
||||||
|
{ id: 'm5', sender: 'me', content: '好的,马上来', time: '08:46' },
|
||||||
|
],
|
||||||
|
|
||||||
|
// 二手书店
|
||||||
|
chat7: [
|
||||||
|
{ id: 'm1', sender: 'other', content: '您好,请问您之前咨询的数据结构教材还需要吗?', time: '周一 14:00' },
|
||||||
|
{ id: 'm2', sender: 'me', content: '需要的,还有货吗?', time: '周一 14:30' },
|
||||||
|
{ id: 'm3', sender: 'other', content: '那本数据结构还有货,可以来店里看看,西门旁边', time: '周一 15:00' },
|
||||||
|
{ id: 'm4', sender: 'me', content: '好的,明天下午我过去看看', time: '周一 15:15' },
|
||||||
|
{ id: 'm5', sender: 'other', content: '没问题,到了说是微信联系的就行', time: '周一 15:20' },
|
||||||
|
],
|
||||||
|
}
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
import type { Product } from '@/types/product'
|
||||||
|
|
||||||
|
export const products: Product[] = [
|
||||||
|
// === 数码 ===
|
||||||
|
{
|
||||||
|
id: 'p1',
|
||||||
|
title: '蓝牙无线耳机 降噪版',
|
||||||
|
desc: '高品质降噪蓝牙耳机,续航30小时,佩戴舒适',
|
||||||
|
price: 129,
|
||||||
|
originalPrice: 199,
|
||||||
|
image: 'https://picsum.photos/seed/earphone/400/400',
|
||||||
|
category: 'digital',
|
||||||
|
merchant: { name: '数码小店', avatar: 'https://picsum.photos/seed/ms1/100/100' },
|
||||||
|
sales: 326,
|
||||||
|
tags: ['热卖', '蓝牙5.3'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'p2',
|
||||||
|
title: '机械键盘 青轴 RGB背光',
|
||||||
|
desc: '87键紧凑布局,青轴手感,RGB幻彩背光,Type-C键线分离',
|
||||||
|
price: 199,
|
||||||
|
originalPrice: 299,
|
||||||
|
image: 'https://picsum.photos/seed/keyboard/400/400',
|
||||||
|
category: 'digital',
|
||||||
|
merchant: { name: '外设天地', avatar: 'https://picsum.photos/seed/ms2/100/100' },
|
||||||
|
sales: 158,
|
||||||
|
tags: ['电竞', 'RGB'],
|
||||||
|
},
|
||||||
|
|
||||||
|
// === 图书 ===
|
||||||
|
{
|
||||||
|
id: 'p3',
|
||||||
|
title: '数据结构(C语言版)教材',
|
||||||
|
desc: '计算机专业核心教材,全新正版,附习题答案',
|
||||||
|
price: 35,
|
||||||
|
originalPrice: 49,
|
||||||
|
image: 'https://picsum.photos/seed/dsbook/400/400',
|
||||||
|
category: 'book',
|
||||||
|
merchant: { name: '二手书店', avatar: 'https://picsum.photos/seed/ms3/100/100' },
|
||||||
|
sales: 82,
|
||||||
|
tags: ['教材', '九成新'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'p4',
|
||||||
|
title: '三体全集 刘慈欣 典藏版',
|
||||||
|
desc: '科幻巨著三体三部曲全集合订本,刘慈欣代表作',
|
||||||
|
price: 68,
|
||||||
|
originalPrice: 93,
|
||||||
|
image: 'https://picsum.photos/seed/threebody/400/400',
|
||||||
|
category: 'book',
|
||||||
|
merchant: { name: '校园书社', avatar: 'https://picsum.photos/seed/ms4/100/100' },
|
||||||
|
sales: 215,
|
||||||
|
tags: ['科幻', '畅销'],
|
||||||
|
},
|
||||||
|
|
||||||
|
// === 生活 ===
|
||||||
|
{
|
||||||
|
id: 'p5',
|
||||||
|
title: 'LED护眼台灯 三档调光',
|
||||||
|
desc: '无频闪护眼台灯,三档色温可调,USB充电,宿舍必备',
|
||||||
|
price: 49,
|
||||||
|
originalPrice: 79,
|
||||||
|
image: 'https://picsum.photos/seed/lamp/400/400',
|
||||||
|
category: 'life',
|
||||||
|
merchant: { name: '生活优选', avatar: 'https://picsum.photos/seed/ms5/100/100' },
|
||||||
|
sales: 487,
|
||||||
|
tags: ['宿舍', '护眼'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'p6',
|
||||||
|
title: '宿舍桌面收纳盒 大容量',
|
||||||
|
desc: '多层分区收纳,节省桌面空间,简约白色,承重强',
|
||||||
|
price: 19.9,
|
||||||
|
image: 'https://picsum.photos/seed/storage/400/400',
|
||||||
|
category: 'life',
|
||||||
|
merchant: { name: '收纳专家', avatar: 'https://picsum.photos/seed/ms6/100/100' },
|
||||||
|
sales: 632,
|
||||||
|
tags: ['收纳', '爆款'],
|
||||||
|
},
|
||||||
|
|
||||||
|
// === 美食 ===
|
||||||
|
{
|
||||||
|
id: 'p7',
|
||||||
|
title: '手冲咖啡礼盒 精品挂耳包',
|
||||||
|
desc: '埃塞俄比亚耶加雪菲挂耳咖啡,10包装,现磨新鲜',
|
||||||
|
price: 39.9,
|
||||||
|
originalPrice: 59,
|
||||||
|
image: 'https://picsum.photos/seed/coffee/400/400',
|
||||||
|
category: 'food',
|
||||||
|
merchant: { name: '校园咖啡馆', avatar: 'https://picsum.photos/seed/ms7/100/100' },
|
||||||
|
sales: 198,
|
||||||
|
tags: ['精品', '挂耳'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'p8',
|
||||||
|
title: '进口零食大礼包 30包组合',
|
||||||
|
desc: '日韩进口零食混装,膨化食品+糖果+巧克力,适合追剧',
|
||||||
|
price: 29.9,
|
||||||
|
originalPrice: 45,
|
||||||
|
image: 'https://picsum.photos/seed/snack/400/400',
|
||||||
|
category: 'food',
|
||||||
|
merchant: { name: '零食铺子', avatar: 'https://picsum.photos/seed/ms8/100/100' },
|
||||||
|
sales: 891,
|
||||||
|
tags: ['进口', '追剧'],
|
||||||
|
},
|
||||||
|
|
||||||
|
// === 服饰 ===
|
||||||
|
{
|
||||||
|
id: 'p9',
|
||||||
|
title: '校园文化T恤 纯棉短袖',
|
||||||
|
desc: '校名logo印花T恤,100%纯棉,多色可选,男女同款',
|
||||||
|
price: 39,
|
||||||
|
originalPrice: 69,
|
||||||
|
image: 'https://picsum.photos/seed/tshirt/400/400',
|
||||||
|
category: 'cloth',
|
||||||
|
merchant: { name: '校园文创', avatar: 'https://picsum.photos/seed/ms9/100/100' },
|
||||||
|
sales: 445,
|
||||||
|
tags: ['文创', '纯棉'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'p10',
|
||||||
|
title: '帆布单肩包 学院风大容量',
|
||||||
|
desc: '加厚帆布材质,大容量设计,可装14寸笔记本,上课通勤两用',
|
||||||
|
price: 45,
|
||||||
|
image: 'https://picsum.photos/seed/bag/400/400',
|
||||||
|
category: 'cloth',
|
||||||
|
merchant: { name: '包袋工坊', avatar: 'https://picsum.photos/seed/ms10/100/100' },
|
||||||
|
sales: 273,
|
||||||
|
tags: ['帆布', '文艺'],
|
||||||
|
},
|
||||||
|
]
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export default definePageConfig({
|
||||||
|
navigationBarTitleText: '对话',
|
||||||
|
navigationStyle: 'custom',
|
||||||
|
})
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
/* ========================================
|
||||||
|
对话页
|
||||||
|
======================================== */
|
||||||
|
|
||||||
|
.chat-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #f7f8fa;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 导航栏 ========== */
|
||||||
|
.chat-nav {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 100;
|
||||||
|
background: rgba(255, 255, 255, 0.9);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
-webkit-backdrop-filter: blur(20px);
|
||||||
|
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
||||||
|
|
||||||
|
.nav-content {
|
||||||
|
height: 44px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 24px;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.nav-back {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-left: -10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-info {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.nav-name {
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #1a1a1a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-role {
|
||||||
|
font-size: 22px;
|
||||||
|
color: #969799;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-placeholder {
|
||||||
|
width: 50px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 消息列表滚动区 ========== */
|
||||||
|
.chat-scroll {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 消息列表容器 ========== */
|
||||||
|
.message-list {
|
||||||
|
padding: 16px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 消息项 ========== */
|
||||||
|
.message-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
|
||||||
|
/* 接收消息:左对齐 */
|
||||||
|
&.received {
|
||||||
|
align-items: flex-start;
|
||||||
|
|
||||||
|
.bubble {
|
||||||
|
background: #fff;
|
||||||
|
color: #323233;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 发送消息:右对齐 */
|
||||||
|
&.sent {
|
||||||
|
align-items: flex-end;
|
||||||
|
|
||||||
|
.bubble {
|
||||||
|
background: linear-gradient(135deg, #1989fa 0%, #06b7f0 100%);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.bubble {
|
||||||
|
max-width: 70%;
|
||||||
|
padding: 20px 24px;
|
||||||
|
border-radius: 16px;
|
||||||
|
font-size: 30px;
|
||||||
|
line-height: 1.5;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.msg-time {
|
||||||
|
font-size: 22px;
|
||||||
|
color: #c8c9cc;
|
||||||
|
margin-top: 8px;
|
||||||
|
padding: 0 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 底部输入栏 ========== */
|
||||||
|
.chat-input-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 16px 24px;
|
||||||
|
background: #fff;
|
||||||
|
border-top: 1px solid #f2f3f5;
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
.input-wrap {
|
||||||
|
flex: 1;
|
||||||
|
background: #f7f8fa;
|
||||||
|
border-radius: 40px;
|
||||||
|
padding: 12px 24px;
|
||||||
|
|
||||||
|
.chat-input {
|
||||||
|
font-size: 28px;
|
||||||
|
height: 44px;
|
||||||
|
line-height: 44px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.send-btn {
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 12px 28px;
|
||||||
|
background: #1989fa;
|
||||||
|
border-radius: 40px;
|
||||||
|
|
||||||
|
.send-text {
|
||||||
|
font-size: 28px;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 底部安全区占位 ========== */
|
||||||
|
.scroll-bottom-safe {
|
||||||
|
height: 120px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
import { useState, useMemo, useCallback, useRef, useEffect } from 'react'
|
||||||
|
import Taro, { useDidShow } from '@tarojs/taro'
|
||||||
|
import { View, Text, ScrollView, Input } from '@tarojs/components'
|
||||||
|
import { Icon } from '@antmjs/vantui'
|
||||||
|
import { chatList } from '@/mock/messages'
|
||||||
|
import { conversations } from '@/mock/conversations'
|
||||||
|
import type { ChatMessage } from '@/mock/conversations'
|
||||||
|
import './index.less'
|
||||||
|
|
||||||
|
/** 角色标签映射 */
|
||||||
|
const ROLE_LABEL: Record<string, string> = {
|
||||||
|
rider: '骑手',
|
||||||
|
merchant: '商家',
|
||||||
|
user: '同学',
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStatusBarH(): number {
|
||||||
|
try { return Taro.getSystemInfoSync().statusBarHeight || 20 }
|
||||||
|
catch { return 20 }
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSafeBottom(): number {
|
||||||
|
try {
|
||||||
|
const info = Taro.getSystemInfoSync()
|
||||||
|
return (info.screenHeight - info.safeArea!.bottom) || 0
|
||||||
|
} catch { return 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ChatPage() {
|
||||||
|
const id = String(Taro.getCurrentInstance().router?.params?.id || '')
|
||||||
|
|
||||||
|
/* ---- 查找聊天对象 ---- */
|
||||||
|
const partner = useMemo(() => chatList.find((c) => c.id === id), [id])
|
||||||
|
|
||||||
|
/* ---- 对话消息 ---- */
|
||||||
|
const [messages, setMessages] = useState<ChatMessage[]>([])
|
||||||
|
const [inputValue, setInputValue] = useState('')
|
||||||
|
|
||||||
|
const statusBarH = useMemo(getStatusBarH, [])
|
||||||
|
const safeBottom = useMemo(getSafeBottom, [])
|
||||||
|
|
||||||
|
// 初始化消息
|
||||||
|
useDidShow(() => {
|
||||||
|
if (id && conversations[id]) {
|
||||||
|
setMessages(conversations[id])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/* ---- 事件 ---- */
|
||||||
|
const handleBack = useCallback(() => {
|
||||||
|
Taro.navigateBack()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleSend = useCallback(() => {
|
||||||
|
const text = inputValue.trim()
|
||||||
|
if (!text) return
|
||||||
|
|
||||||
|
const now = new Date()
|
||||||
|
const time = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`
|
||||||
|
|
||||||
|
const newMsg: ChatMessage = {
|
||||||
|
id: `m${Date.now()}`,
|
||||||
|
sender: 'me',
|
||||||
|
content: text,
|
||||||
|
time,
|
||||||
|
}
|
||||||
|
|
||||||
|
setMessages((prev) => [...prev, newMsg])
|
||||||
|
setInputValue('')
|
||||||
|
|
||||||
|
// 模拟对方自动回复(1.5 秒后)
|
||||||
|
if (partner) {
|
||||||
|
setTimeout(() => {
|
||||||
|
const autoReply: ChatMessage = {
|
||||||
|
id: `m${Date.now() + 1}`,
|
||||||
|
sender: 'other',
|
||||||
|
content: '好的,收到你的消息了~',
|
||||||
|
time: `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes() + 1).padStart(2, '0')}`,
|
||||||
|
}
|
||||||
|
setMessages((prev) => [...prev, autoReply])
|
||||||
|
}, 1500)
|
||||||
|
}
|
||||||
|
}, [inputValue, partner])
|
||||||
|
|
||||||
|
if (!partner) {
|
||||||
|
return (
|
||||||
|
<View className='chat-page'>
|
||||||
|
<View className='chat-nav' style={{ paddingTop: `${statusBarH}px` }}>
|
||||||
|
<View className='nav-content'>
|
||||||
|
<View className='nav-back' onClick={handleBack}>
|
||||||
|
<Icon name='arrow-left' size={22} color='#323233' />
|
||||||
|
</View>
|
||||||
|
<View className='nav-info'>
|
||||||
|
<Text className='nav-name'>未知用户</Text>
|
||||||
|
</View>
|
||||||
|
<View className='nav-placeholder' />
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<View style={{ height: `${statusBarH + 44}px` }} />
|
||||||
|
<View style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', flex: 1 }}>
|
||||||
|
<Text style={{ color: '#969799', fontSize: '30px' }}>对话不存在</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const roleLabel = partner.role ? ROLE_LABEL[partner.role] : ''
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View className='chat-page'>
|
||||||
|
{/* ===== 导航栏 ===== */}
|
||||||
|
<View className='chat-nav' style={{ paddingTop: `${statusBarH}px` }}>
|
||||||
|
<View className='nav-content'>
|
||||||
|
<View className='nav-back' onClick={handleBack}>
|
||||||
|
<Icon name='arrow-left' size={22} color='#323233' />
|
||||||
|
</View>
|
||||||
|
<View className='nav-info'>
|
||||||
|
<Text className='nav-name'>{partner.name}</Text>
|
||||||
|
{roleLabel && <Text className='nav-role'>{roleLabel}</Text>}
|
||||||
|
</View>
|
||||||
|
<View className='nav-placeholder' />
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<View style={{ height: `${statusBarH + 44}px` }} />
|
||||||
|
|
||||||
|
{/* ===== 消息列表 ===== */}
|
||||||
|
<ScrollView
|
||||||
|
className='chat-scroll'
|
||||||
|
scrollY
|
||||||
|
enhanced
|
||||||
|
showScrollbar={false}
|
||||||
|
scrollTop={99999}
|
||||||
|
scrollWithAnimation
|
||||||
|
>
|
||||||
|
<View className='message-list'>
|
||||||
|
{messages.map((msg) => (
|
||||||
|
<View
|
||||||
|
key={msg.id}
|
||||||
|
className={`message-item ${msg.sender === 'me' ? 'sent' : 'received'}`}
|
||||||
|
>
|
||||||
|
<Text className='bubble'>{msg.content}</Text>
|
||||||
|
<Text className='msg-time'>{msg.time}</Text>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
<View className='scroll-bottom-safe' />
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
{/* ===== 底部输入栏 ===== */}
|
||||||
|
<View className='chat-input-bar' style={{ paddingBottom: `${safeBottom}px` }}>
|
||||||
|
<View className='input-wrap'>
|
||||||
|
<Input
|
||||||
|
className='chat-input'
|
||||||
|
value={inputValue}
|
||||||
|
placeholder='输入消息...'
|
||||||
|
placeholderStyle='color: #c8c9cc;'
|
||||||
|
onInput={(e) => setInputValue(String(e.detail.value))}
|
||||||
|
confirmType='send'
|
||||||
|
onConfirm={handleSend}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<View className='send-btn' onClick={handleSend}>
|
||||||
|
<Text className='send-text'>发送</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
+98
-13
@@ -80,30 +80,115 @@
|
|||||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ========== 标签区域 ========== */
|
/* ========== 统一板块 ========== */
|
||||||
.tabs-section {
|
.main-tabs-section {
|
||||||
|
margin: 0 24px 20px;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
margin: 0 24px 20px;
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
.van-tabs__wrap {
|
/* 校园商城内容 */
|
||||||
border-radius: 16px 16px 0 0;
|
.mall-content {
|
||||||
|
padding: 16px 20px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 分类标签 */
|
||||||
|
.category-scroll {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
.category-tab {
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 10px 24px;
|
||||||
|
border-radius: 32px;
|
||||||
|
background: #f7f8fa;
|
||||||
|
font-size: 26px;
|
||||||
|
color: #646566;
|
||||||
|
transition: all 0.2s;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background: #1989fa;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ========== 文章列表 ========== */
|
/* 商品双列网格 */
|
||||||
.article-list {
|
.product-grid {
|
||||||
padding: 0 24px;
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
|
||||||
|
.empty-grid {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px 0;
|
||||||
|
font-size: 28px;
|
||||||
|
color: #c8c9cc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 商家入驻入口 */
|
||||||
|
.merchant-join-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 20px 24px;
|
||||||
|
background: linear-gradient(135deg, #f0f7ff 0%, #e8f4ff 100%);
|
||||||
|
border: 1px dashed #1989fa;
|
||||||
|
border-radius: 12px;
|
||||||
|
|
||||||
|
.join-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
.join-icon { font-size: 48px; }
|
||||||
|
|
||||||
|
.join-text-wrap {
|
||||||
|
.join-title {
|
||||||
|
display: block;
|
||||||
|
font-size: 30px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #1a1a1a;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.join-desc {
|
||||||
|
font-size: 24px;
|
||||||
|
color: #969799;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.join-arrow {
|
||||||
|
font-size: 36px;
|
||||||
|
color: #1989fa;
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 文章列表内容 */
|
||||||
|
.article-content {
|
||||||
|
padding: 0 20px 16px;
|
||||||
|
|
||||||
.article-item {
|
.article-item {
|
||||||
background: #fff;
|
padding: 24px 0;
|
||||||
border-radius: 16px;
|
|
||||||
padding: 24px;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
|
|
||||||
transition: transform 0.2s;
|
transition: transform 0.2s;
|
||||||
|
|
||||||
|
& + .article-item {
|
||||||
|
border-top: 1px solid #f2f3f5;
|
||||||
|
}
|
||||||
|
|
||||||
&:active {
|
&:active {
|
||||||
transform: scale(0.98);
|
transform: scale(0.98);
|
||||||
}
|
}
|
||||||
|
|||||||
+115
-18
@@ -1,10 +1,14 @@
|
|||||||
import { useState, useMemo } from 'react'
|
import { useState, useMemo } from 'react'
|
||||||
import Taro from '@tarojs/taro'
|
import Taro from '@tarojs/taro'
|
||||||
import { View, ScrollView } from '@tarojs/components'
|
import { View, Text, ScrollView } from '@tarojs/components'
|
||||||
import { Search, Swiper, SwiperItem, Grid, GridItem, Tabs, Tab, Image as VantImage } from '@antmjs/vantui'
|
import { Search, Swiper, SwiperItem, Grid, GridItem, Tabs, Tab, Image as VantImage } from '@antmjs/vantui'
|
||||||
import ArticleCard from '@/components/ArticleCard'
|
import ArticleCard from '@/components/ArticleCard'
|
||||||
|
import ProductCard from '@/components/ProductCard'
|
||||||
import { banners, navItems, articles } from '@/mock/articles'
|
import { banners, navItems, articles } from '@/mock/articles'
|
||||||
|
import { products } from '@/mock/products'
|
||||||
import type { ArticleCategory } from '@/types/article'
|
import type { ArticleCategory } from '@/types/article'
|
||||||
|
import type { ProductCategory } from '@/types/product'
|
||||||
|
import type { Product } from '@/types/product'
|
||||||
import CustomTabBar from "@/custom-tab-bar";
|
import CustomTabBar from "@/custom-tab-bar";
|
||||||
import './index.less'
|
import './index.less'
|
||||||
|
|
||||||
@@ -21,22 +25,45 @@ function getNavBarHeight() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 分类标签映射 */
|
/** 统一板块标签:校园商城 + 文章分类 */
|
||||||
const TAB_LIST = [
|
const MAIN_TABS = [
|
||||||
{ title: '最新', value: 'latest' as ArticleCategory },
|
{ title: '校园商城', value: 'mall' },
|
||||||
{ title: '推荐', value: 'recommend' as ArticleCategory },
|
{ title: '最新', value: 'latest' },
|
||||||
{ title: '热点', value: 'hot' as ArticleCategory },
|
{ title: '推荐', value: 'recommend' },
|
||||||
|
{ title: '热点', value: 'hot' },
|
||||||
|
]
|
||||||
|
|
||||||
|
/** 商品分类标签 */
|
||||||
|
const PRODUCT_CATEGORIES: { title: string; value: ProductCategory | 'all' }[] = [
|
||||||
|
{ title: '全部', value: 'all' },
|
||||||
|
{ title: '数码', value: 'digital' },
|
||||||
|
{ title: '图书', value: 'book' },
|
||||||
|
{ title: '生活', value: 'life' },
|
||||||
|
{ title: '美食', value: 'food' },
|
||||||
|
{ title: '服饰', value: 'cloth' },
|
||||||
]
|
]
|
||||||
|
|
||||||
export default function Index() {
|
export default function Index() {
|
||||||
const [activeTab, setActiveTab] = useState(0)
|
const [activeMainTab, setActiveMainTab] = useState(0)
|
||||||
|
const [activeCategory, setActiveCategory] = useState(0)
|
||||||
const navBarHeight = useMemo(() => getNavBarHeight(), [])
|
const navBarHeight = useMemo(() => getNavBarHeight(), [])
|
||||||
|
|
||||||
/** 根据当前 tab 筛选文章 */
|
/** 当前选中的板块 */
|
||||||
|
const currentTab = MAIN_TABS[activeMainTab]?.value || 'mall'
|
||||||
|
|
||||||
|
/** 根据文章分类筛 */
|
||||||
const filteredArticles = useMemo(() => {
|
const filteredArticles = useMemo(() => {
|
||||||
const category = TAB_LIST[activeTab]?.value || 'latest'
|
const value = MAIN_TABS[activeMainTab]?.value || 'latest'
|
||||||
return articles.filter((a) => a.category === category)
|
if (value === 'mall') return articles.filter((a) => a.category === 'latest')
|
||||||
}, [activeTab])
|
return articles.filter((a) => a.category === value)
|
||||||
|
}, [activeMainTab])
|
||||||
|
|
||||||
|
/** 根据当前分类筛选商品 */
|
||||||
|
const filteredProducts = useMemo(() => {
|
||||||
|
const catValue = PRODUCT_CATEGORIES[activeCategory]?.value || 'all'
|
||||||
|
if (catValue === 'all') return products
|
||||||
|
return products.filter((p) => p.category === catValue)
|
||||||
|
}, [activeCategory])
|
||||||
|
|
||||||
/** 搜索 */
|
/** 搜索 */
|
||||||
const handleSearch = () => {
|
const handleSearch = () => {
|
||||||
@@ -57,6 +84,21 @@ export default function Index() {
|
|||||||
Taro.navigateTo({ url: `/pages/article/index?id=${id}` })
|
Taro.navigateTo({ url: `/pages/article/index?id=${id}` })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 商品点击 */
|
||||||
|
const handleProductClick = (product: Product) => {
|
||||||
|
Taro.navigateTo({ url: `/pages/product-detail/index?id=${product.id}` })
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 查看全部商品 */
|
||||||
|
const handleViewAll = () => {
|
||||||
|
Taro.showToast({ title: '商城即将上线', icon: 'none' })
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 商家入驻 */
|
||||||
|
const handleMerchantJoin = () => {
|
||||||
|
Taro.showToast({ title: '商家入驻即将上线', icon: 'none' })
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View className='home-page'>
|
<View className='home-page'>
|
||||||
{/* ========== 自定义导航栏 (毛玻璃) ========== */}
|
{/* ========== 自定义导航栏 (毛玻璃) ========== */}
|
||||||
@@ -128,25 +170,78 @@ export default function Index() {
|
|||||||
</Grid>
|
</Grid>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* ========== 文章标签导航 (最新 / 推荐 / 热点) ========== */}
|
{/* ========== 统一板块:校园商城 | 最新 | 推荐 | 热点 ========== */}
|
||||||
<View className='tabs-section'>
|
<View className='main-tabs-section'>
|
||||||
<Tabs
|
<Tabs
|
||||||
active={activeTab}
|
active={activeMainTab}
|
||||||
onChange={(e) => setActiveTab(e.detail.index)}
|
onChange={(e) => setActiveMainTab(e.detail.index)}
|
||||||
sticky
|
sticky
|
||||||
offsetTop={navBarHeight.totalHeight}
|
offsetTop={navBarHeight.totalHeight}
|
||||||
color='#1989fa'
|
color='#1989fa'
|
||||||
titleActiveColor='#1989fa'
|
titleActiveColor='#1989fa'
|
||||||
titleInactiveColor='#646566'
|
titleInactiveColor='#646566'
|
||||||
|
animated
|
||||||
|
swipeable
|
||||||
>
|
>
|
||||||
{TAB_LIST.map((tab) => (
|
{MAIN_TABS.map((tab) => (
|
||||||
<Tab key={tab.value} title={tab.title} />
|
<Tab key={tab.value} title={tab.title} />
|
||||||
))}
|
))}
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
|
{/* ===== 校园商城内容 ===== */}
|
||||||
|
{currentTab === 'mall' && (
|
||||||
|
<View className='mall-content'>
|
||||||
|
{/* 商品分类标签 */}
|
||||||
|
<ScrollView
|
||||||
|
className='category-scroll'
|
||||||
|
scrollX
|
||||||
|
enhanced
|
||||||
|
showScrollbar={false}
|
||||||
|
>
|
||||||
|
<View className='category-tabs'>
|
||||||
|
{PRODUCT_CATEGORIES.map((cat, idx) => (
|
||||||
|
<View
|
||||||
|
key={cat.value}
|
||||||
|
className={`category-tab ${activeCategory === idx ? 'active' : ''}`}
|
||||||
|
onClick={() => setActiveCategory(idx)}
|
||||||
|
>
|
||||||
|
<Text className='category-text'>{cat.title}</Text>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
{/* 商品双列网格 */}
|
||||||
|
<View className='product-grid'>
|
||||||
|
{filteredProducts.map((product) => (
|
||||||
|
<ProductCard
|
||||||
|
key={product.id}
|
||||||
|
product={product}
|
||||||
|
onClick={handleProductClick}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{filteredProducts.length === 0 && (
|
||||||
|
<View className='empty-grid'>暂无商品</View>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* ========== 文章列表 ========== */}
|
{/* 商家入驻入口 */}
|
||||||
<View className='article-list'>
|
<View className='merchant-join-card' onClick={handleMerchantJoin}>
|
||||||
|
<View className='join-content'>
|
||||||
|
<Text className='join-icon'>🏪</Text>
|
||||||
|
<View className='join-text-wrap'>
|
||||||
|
<Text className='join-title'>商家入驻</Text>
|
||||||
|
<Text className='join-desc'>拥有自己的店铺,开启校园创业之旅</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<Text className='join-arrow'>›</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* ===== 文章列表内容 ===== */}
|
||||||
|
{currentTab !== 'mall' && (
|
||||||
|
<View className='article-content'>
|
||||||
{filteredArticles.map((article) => (
|
{filteredArticles.map((article) => (
|
||||||
<View
|
<View
|
||||||
key={article.id}
|
key={article.id}
|
||||||
@@ -160,6 +255,8 @@ export default function Index() {
|
|||||||
<View className='empty-tip'>暂无文章</View>
|
<View className='empty-tip'>暂无文章</View>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
{/* 底部安全距离 */}
|
{/* 底部安全距离 */}
|
||||||
<View className='bottom-safe' />
|
<View className='bottom-safe' />
|
||||||
|
|||||||
@@ -14,7 +14,11 @@ const ROLE_LABEL: Record<string, { text: string; className: string }> = {
|
|||||||
|
|
||||||
export default function Messages() {
|
export default function Messages() {
|
||||||
const handleChatClick = (item: ChatItem) => {
|
const handleChatClick = (item: ChatItem) => {
|
||||||
Taro.showToast({ title: `${item.name || item.title} 聊天即将上线`, icon: 'none' })
|
if (item.type === 'system') {
|
||||||
|
Taro.navigateTo({ url: '/pages/system-messages/index' })
|
||||||
|
} else {
|
||||||
|
Taro.navigateTo({ url: `/pages/chat/index?id=${item.id}` })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 分组:置顶系统消息 + 普通聊天
|
// 分组:置顶系统消息 + 普通聊天
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export default definePageConfig({
|
||||||
|
navigationBarTitleText: '商品详情',
|
||||||
|
navigationStyle: 'custom',
|
||||||
|
})
|
||||||
@@ -0,0 +1,237 @@
|
|||||||
|
/* ========================================
|
||||||
|
商品详情页
|
||||||
|
======================================== */
|
||||||
|
|
||||||
|
.product-detail-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #f7f8fa;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 导航栏 ========== */
|
||||||
|
.detail-nav {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 100;
|
||||||
|
background: rgba(255, 255, 255, 0.9);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
-webkit-backdrop-filter: blur(20px);
|
||||||
|
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
||||||
|
|
||||||
|
.nav-content {
|
||||||
|
height: 44px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 24px;
|
||||||
|
|
||||||
|
.nav-back {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-left: -10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-title {
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #1a1a1a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
width: 50px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 滚动区域 ========== */
|
||||||
|
.detail-scroll {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 图片轮播 ========== */
|
||||||
|
.image-swiper {
|
||||||
|
width: 100%;
|
||||||
|
height: 750px;
|
||||||
|
background: #fff;
|
||||||
|
|
||||||
|
.swiper-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 价格信息卡片 ========== */
|
||||||
|
.price-card {
|
||||||
|
background: #fff;
|
||||||
|
padding: 24px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
|
||||||
|
.price-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
|
||||||
|
.current-price {
|
||||||
|
font-size: 48px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #ee0a24;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.original-price {
|
||||||
|
font-size: 28px;
|
||||||
|
color: #c8c9cc;
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
|
||||||
|
.discount-badge {
|
||||||
|
font-size: 24px;
|
||||||
|
color: #ee0a24;
|
||||||
|
background: #fff0f0;
|
||||||
|
padding: 2px 10px;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-name {
|
||||||
|
display: block;
|
||||||
|
font-size: 34px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1a1a1a;
|
||||||
|
line-height: 1.4;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-tags {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-sales {
|
||||||
|
font-size: 26px;
|
||||||
|
color: #969799;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 规格选择行 ========== */
|
||||||
|
.sku-select-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 24px;
|
||||||
|
background: #fff;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
|
||||||
|
.sku-label {
|
||||||
|
font-size: 28px;
|
||||||
|
color: #646566;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sku-value {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 28px;
|
||||||
|
color: #1a1a1a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 商家信息 ========== */
|
||||||
|
.merchant-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 24px;
|
||||||
|
background: #fff;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
|
||||||
|
.merchant-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
.merchant-avatar {
|
||||||
|
width: 72px;
|
||||||
|
height: 72px;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.merchant-name {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #1a1a1a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.merchant-arrow {
|
||||||
|
font-size: 28px;
|
||||||
|
color: #c8c9cc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 商品描述 ========== */
|
||||||
|
.desc-card {
|
||||||
|
background: #fff;
|
||||||
|
padding: 24px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
|
||||||
|
.desc-title {
|
||||||
|
display: block;
|
||||||
|
font-size: 30px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1a1a1a;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desc-content {
|
||||||
|
display: block;
|
||||||
|
font-size: 28px;
|
||||||
|
color: #646566;
|
||||||
|
line-height: 1.8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 推荐商品 ========== */
|
||||||
|
.recommend-section {
|
||||||
|
padding: 0 24px 16px;
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
display: block;
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1a1a1a;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding-left: 8px;
|
||||||
|
border-left: 6px solid #1989fa;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.recommend-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
.empty-recommend {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px 0;
|
||||||
|
font-size: 28px;
|
||||||
|
color: #c8c9cc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 底部安全区 ========== */
|
||||||
|
.scroll-bottom-safe {
|
||||||
|
height: 120px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,411 @@
|
|||||||
|
import { useState, useMemo, useCallback } from 'react'
|
||||||
|
import Taro from '@tarojs/taro'
|
||||||
|
import { View, Text, ScrollView } from '@tarojs/components'
|
||||||
|
import {
|
||||||
|
Swiper,
|
||||||
|
SwiperItem,
|
||||||
|
Tag,
|
||||||
|
Image as VantImage,
|
||||||
|
Icon,
|
||||||
|
GoodsAction,
|
||||||
|
GoodsActionIcon,
|
||||||
|
GoodsActionButton,
|
||||||
|
Stepper,
|
||||||
|
Sku,
|
||||||
|
Popup,
|
||||||
|
Button,
|
||||||
|
} from '@antmjs/vantui'
|
||||||
|
import type { ISkuItem, IGoodItem } from '@antmjs/vantui/types/sku'
|
||||||
|
import { products } from '@/mock/products'
|
||||||
|
import ProductCard from '@/components/ProductCard'
|
||||||
|
import type { Product } from '@/types/product'
|
||||||
|
import './index.less'
|
||||||
|
|
||||||
|
/* ==============================
|
||||||
|
辅助函数
|
||||||
|
============================== */
|
||||||
|
|
||||||
|
function getStatusBarH(): number {
|
||||||
|
try { return Taro.getSystemInfoSync().statusBarHeight || 20 }
|
||||||
|
catch { return 20 }
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSafeBottom(): number {
|
||||||
|
try {
|
||||||
|
const info = Taro.getSystemInfoSync()
|
||||||
|
return (info.screenHeight - info.safeArea!.bottom) || 0
|
||||||
|
} catch { return 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 根据商品生成简单 SKU 数据 */
|
||||||
|
function buildSkuData(product: Product) {
|
||||||
|
const goodsList: IGoodItem[] = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
skuIds: [1, 11],
|
||||||
|
price: Math.round(product.price * 100),
|
||||||
|
stock: 999,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
skuIds: [1, 12],
|
||||||
|
price: Math.round(product.price * 100),
|
||||||
|
stock: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
skuIds: [2, 11],
|
||||||
|
price: Math.round(product.price * 100) + 1000,
|
||||||
|
stock: 500,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
skuIds: [2, 12],
|
||||||
|
price: Math.round(product.price * 100) + 1000,
|
||||||
|
stock: 300,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const sku: ISkuItem[] = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: '颜色',
|
||||||
|
items: [
|
||||||
|
{ id: 1, name: '黑色' },
|
||||||
|
{ id: 2, name: '白色' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: '版本',
|
||||||
|
items: [
|
||||||
|
{ id: 11, name: '标准版' },
|
||||||
|
{ id: 12, name: '高配版' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
return { goodsList, sku }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==============================
|
||||||
|
页面组件
|
||||||
|
============================== */
|
||||||
|
|
||||||
|
export default function ProductDetail() {
|
||||||
|
const id = String(Taro.getCurrentInstance().router?.params?.id || '')
|
||||||
|
|
||||||
|
/* ---- 数据 ---- */
|
||||||
|
const product = useMemo(() => products.find((p) => p.id === id), [id])
|
||||||
|
|
||||||
|
const statusBarH = useMemo(getStatusBarH, [])
|
||||||
|
const safeBottom = useMemo(getSafeBottom, [])
|
||||||
|
|
||||||
|
const [isFavorited, setIsFavorited] = useState(false)
|
||||||
|
|
||||||
|
/* ---- SKU 弹窗 ---- */
|
||||||
|
const [skuVisible, setSkuVisible] = useState(false)
|
||||||
|
const [skuAction, setSkuAction] = useState<'cart' | 'buy'>('cart')
|
||||||
|
const [quantity, setQuantity] = useState(1)
|
||||||
|
|
||||||
|
const skuData = useMemo(() => {
|
||||||
|
if (!product) return { goodsList: [], sku: [] }
|
||||||
|
return buildSkuData(product)
|
||||||
|
}, [product])
|
||||||
|
|
||||||
|
/* ---- 推荐商品 ---- */
|
||||||
|
const recommendedProducts = useMemo(() => {
|
||||||
|
if (!product) return []
|
||||||
|
return products
|
||||||
|
.filter((p) => p.category === product.category && p.id !== product.id)
|
||||||
|
.slice(0, 4)
|
||||||
|
}, [product])
|
||||||
|
|
||||||
|
/* ---- 事件 ---- */
|
||||||
|
const handleBack = useCallback(() => { Taro.navigateBack() }, [])
|
||||||
|
|
||||||
|
const handleNavigateToDetail = useCallback((targetId: string) => {
|
||||||
|
Taro.navigateTo({ url: `/pages/product-detail/index?id=${targetId}` })
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
/** 打开 SKU 弹窗 */
|
||||||
|
const openSku = (action: 'cart' | 'buy') => {
|
||||||
|
setSkuAction(action)
|
||||||
|
setSkuVisible(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSkuClose = () => setSkuVisible(false)
|
||||||
|
|
||||||
|
/** SKU 确认 */
|
||||||
|
const handleSkuConfirm = () => {
|
||||||
|
setSkuVisible(false)
|
||||||
|
if (skuAction === 'cart') {
|
||||||
|
Taro.showToast({ title: '已加入购物车', icon: 'success' })
|
||||||
|
} else {
|
||||||
|
Taro.showToast({ title: '下单成功', icon: 'success' })
|
||||||
|
}
|
||||||
|
setQuantity(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- 空状态 ---- */
|
||||||
|
if (!product) {
|
||||||
|
return (
|
||||||
|
<View className='product-detail-page'>
|
||||||
|
<View className='detail-nav' style={{ paddingTop: `${statusBarH}px` }}>
|
||||||
|
<View className='nav-content'>
|
||||||
|
<View className='nav-back' onClick={handleBack}>
|
||||||
|
<Icon name='arrow-left' size={22} color='#323233' />
|
||||||
|
</View>
|
||||||
|
<Text className='nav-title'>商品详情</Text>
|
||||||
|
<View className='nav-actions' />
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<View style={{ height: `${statusBarH + 44}px` }} />
|
||||||
|
<View style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', flex: 1 }}>
|
||||||
|
<Text style={{ color: '#969799', fontSize: '30px' }}>商品不存在</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const discount = product.originalPrice
|
||||||
|
? Math.round((product.price / product.originalPrice) * 10)
|
||||||
|
: null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View className='product-detail-page'>
|
||||||
|
{/* ===== 导航栏 ===== */}
|
||||||
|
<View className='detail-nav' style={{ paddingTop: `${statusBarH}px` }}>
|
||||||
|
<View className='nav-content'>
|
||||||
|
<View className='nav-back' onClick={handleBack}>
|
||||||
|
<Icon name='arrow-left' size={22} color='#323233' />
|
||||||
|
</View>
|
||||||
|
<Text className='nav-title'>商品详情</Text>
|
||||||
|
<View className='nav-actions' />
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<View style={{ height: `${statusBarH + 44}px` }} />
|
||||||
|
|
||||||
|
{/* ===== 滚动内容 ===== */}
|
||||||
|
<ScrollView className='detail-scroll' scrollY enhanced showScrollbar={false}>
|
||||||
|
{/* 图片轮播 — 使用商品图片 + 模拟多张 */}
|
||||||
|
<Swiper
|
||||||
|
className='image-swiper'
|
||||||
|
autoPlay={0}
|
||||||
|
loop
|
||||||
|
paginationVisible
|
||||||
|
paginationColor='#1989fa'
|
||||||
|
height={750}
|
||||||
|
>
|
||||||
|
{[product.image, product.image, product.image].map((img, idx) => (
|
||||||
|
<SwiperItem key={idx}>
|
||||||
|
<VantImage
|
||||||
|
className='swiper-image'
|
||||||
|
src={img}
|
||||||
|
fit='cover'
|
||||||
|
width='100%'
|
||||||
|
height='100%'
|
||||||
|
/>
|
||||||
|
</SwiperItem>
|
||||||
|
))}
|
||||||
|
</Swiper>
|
||||||
|
|
||||||
|
{/* 价格信息 */}
|
||||||
|
<View className='price-card'>
|
||||||
|
<View className='price-row'>
|
||||||
|
<Text className='current-price'>¥{product.price.toFixed(2)}</Text>
|
||||||
|
{product.originalPrice && (
|
||||||
|
<Text className='original-price'>¥{product.originalPrice.toFixed(2)}</Text>
|
||||||
|
)}
|
||||||
|
{discount && discount < 10 && (
|
||||||
|
<Text className='discount-badge'>{discount}折</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
<Text className='product-name'>{product.title}</Text>
|
||||||
|
{product.tags && product.tags.length > 0 && (
|
||||||
|
<View className='product-tags'>
|
||||||
|
{product.tags.map((t) => (
|
||||||
|
<Tag key={t} type='danger' plain>
|
||||||
|
{t}
|
||||||
|
</Tag>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
<Text className='product-sales'>已售 {product.sales}</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 规格选择 */}
|
||||||
|
<View className='sku-select-row' onClick={() => openSku('cart')}>
|
||||||
|
<Text className='sku-label'>已选</Text>
|
||||||
|
<View className='sku-value'>
|
||||||
|
<Text>颜色: 黑色 / 版本: 标准版 ×{quantity}</Text>
|
||||||
|
<Icon name='arrow' size={16} color='#c8c9cc' />
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 商家信息 */}
|
||||||
|
<View className='merchant-card'>
|
||||||
|
<View className='merchant-info'>
|
||||||
|
<VantImage
|
||||||
|
className='merchant-avatar'
|
||||||
|
src={product.merchant.avatar}
|
||||||
|
fit='cover'
|
||||||
|
width={72}
|
||||||
|
height={72}
|
||||||
|
round
|
||||||
|
/>
|
||||||
|
<Text className='merchant-name'>{product.merchant.name}</Text>
|
||||||
|
</View>
|
||||||
|
<Icon name='arrow' size={16} color='#c8c9cc' />
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 商品描述 */}
|
||||||
|
<View className='desc-card'>
|
||||||
|
<Text className='desc-title'>商品描述</Text>
|
||||||
|
<Text className='desc-content'>{product.desc}</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 推荐商品 */}
|
||||||
|
<View className='recommend-section'>
|
||||||
|
<Text className='section-title'>推荐商品</Text>
|
||||||
|
<View className='recommend-grid'>
|
||||||
|
{recommendedProducts.length > 0 ? (
|
||||||
|
recommendedProducts.map((p) => (
|
||||||
|
<ProductCard
|
||||||
|
key={p.id}
|
||||||
|
product={p}
|
||||||
|
onClick={(prod) => handleNavigateToDetail(prod.id)}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<Text className='empty-recommend'>暂无推荐</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className='scroll-bottom-safe' />
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
{/* ===== 底部操作栏 ===== */}
|
||||||
|
<GoodsAction safeAreaInsetBottom={false}>
|
||||||
|
<GoodsActionIcon
|
||||||
|
icon='chat-o'
|
||||||
|
text='客服'
|
||||||
|
onClick={() => Taro.showToast({ title: '联系客服中...', icon: 'none' })}
|
||||||
|
/>
|
||||||
|
<GoodsActionIcon
|
||||||
|
icon={isFavorited ? 'star' : 'star-o'}
|
||||||
|
text='收藏'
|
||||||
|
color={isFavorited ? '#ff976a' : undefined}
|
||||||
|
onClick={() => {
|
||||||
|
setIsFavorited(!isFavorited)
|
||||||
|
Taro.showToast({
|
||||||
|
title: isFavorited ? '已取消收藏' : '已收藏',
|
||||||
|
icon: 'none',
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<GoodsActionIcon
|
||||||
|
icon='cart-o'
|
||||||
|
text='购物车'
|
||||||
|
info='0'
|
||||||
|
onClick={() => Taro.showToast({ title: '购物车即将上线', icon: 'none' })}
|
||||||
|
/>
|
||||||
|
<GoodsActionButton
|
||||||
|
text='加入购物车'
|
||||||
|
type='warning'
|
||||||
|
onClick={() => openSku('cart')}
|
||||||
|
/>
|
||||||
|
<GoodsActionButton
|
||||||
|
text='立即购买'
|
||||||
|
type='danger'
|
||||||
|
onClick={() => openSku('buy')}
|
||||||
|
/>
|
||||||
|
</GoodsAction>
|
||||||
|
|
||||||
|
{/* ===== SKU 选择弹窗 ===== */}
|
||||||
|
<Popup
|
||||||
|
show={skuVisible}
|
||||||
|
position='bottom'
|
||||||
|
round
|
||||||
|
rootPortal
|
||||||
|
onClose={handleSkuClose}
|
||||||
|
overlayStyle={{ backgroundColor: 'rgba(0,0,0,0.5)' }}
|
||||||
|
>
|
||||||
|
<View style={{ background: '#fff', paddingBottom: `${safeBottom}px` }}>
|
||||||
|
{/* 弹窗头部 — 商品概要 */}
|
||||||
|
<View style={{ display: 'flex', padding: '24px', borderBottom: '1px solid #f2f3f5' }}>
|
||||||
|
<VantImage
|
||||||
|
src={product.image}
|
||||||
|
fit='cover'
|
||||||
|
width={160}
|
||||||
|
height={160}
|
||||||
|
radius={8}
|
||||||
|
/>
|
||||||
|
<View style={{ marginLeft: '20px', flex: 1 }}>
|
||||||
|
<Text style={{ fontSize: '40px', fontWeight: 700, color: '#ee0a24', display: 'block' }}>
|
||||||
|
¥{product.price.toFixed(2)}
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
fontSize: '24px',
|
||||||
|
color: '#969799',
|
||||||
|
display: 'block',
|
||||||
|
marginTop: '8px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
库存 999 件
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* SKU 规格选择 */}
|
||||||
|
<View style={{ padding: '0 24px', maxHeight: '500px', overflowY: 'auto' }}>
|
||||||
|
<Sku
|
||||||
|
goodsId={1}
|
||||||
|
goodsList={skuData.goodsList}
|
||||||
|
sku={skuData.sku}
|
||||||
|
onChange={(goods) => {
|
||||||
|
if (goods) {
|
||||||
|
// SKU 变化时更新价格
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 数量选择 */}
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
padding: '24px',
|
||||||
|
borderTop: '1px solid #f2f3f5',
|
||||||
|
borderBottom: '1px solid #f2f3f5',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text style={{ fontSize: '28px', color: '#646566' }}>数量</Text>
|
||||||
|
<Stepper
|
||||||
|
value={quantity}
|
||||||
|
min={1}
|
||||||
|
max={99}
|
||||||
|
onChange={(e) => setQuantity(Number(e.detail))}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 确认按钮 */}
|
||||||
|
<View style={{ padding: '24px' }}>
|
||||||
|
<Button
|
||||||
|
type={skuAction === 'buy' ? 'danger' : 'warning'}
|
||||||
|
round
|
||||||
|
block
|
||||||
|
onClick={handleSkuConfirm}
|
||||||
|
>
|
||||||
|
{skuAction === 'buy' ? '立即购买' : '加入购物车'}
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</Popup>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export default definePageConfig({
|
||||||
|
navigationBarTitleText: '发布文章',
|
||||||
|
navigationStyle: 'custom',
|
||||||
|
})
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
/* ========================================
|
||||||
|
发布文章页
|
||||||
|
======================================== */
|
||||||
|
|
||||||
|
.publish-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #f7f8fa;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 导航栏(复用 article 页样式) ========== */
|
||||||
|
.publish-nav {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 100;
|
||||||
|
background: rgba(255, 255, 255, 0.9);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
-webkit-backdrop-filter: blur(20px);
|
||||||
|
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
||||||
|
|
||||||
|
.nav-content {
|
||||||
|
height: 44px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 24px;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.nav-back {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-left: -10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-title {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #1a1a1a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-placeholder {
|
||||||
|
width: 50px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 滚动区域 ========== */
|
||||||
|
.publish-scroll {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 表单卡片 ========== */
|
||||||
|
.form-card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 16px;
|
||||||
|
margin: 16px 24px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
display: block;
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #323233;
|
||||||
|
padding: 24px 24px 0;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 图片上传区域 ========== */
|
||||||
|
.upload-section {
|
||||||
|
padding: 16px 24px 24px;
|
||||||
|
|
||||||
|
.upload-label {
|
||||||
|
display: block;
|
||||||
|
font-size: 26px;
|
||||||
|
color: #969799;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 底部提交栏 ========== */
|
||||||
|
.bottom-submit-bar {
|
||||||
|
padding: 16px 24px;
|
||||||
|
background: #fff;
|
||||||
|
border-top: 1px solid #f2f3f5;
|
||||||
|
|
||||||
|
.submit-btn {
|
||||||
|
:global(.van-button) {
|
||||||
|
height: 88px;
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 底部安全区占位 ========== */
|
||||||
|
.scroll-bottom-safe {
|
||||||
|
height: 160px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
import { useState, useMemo, useCallback } from 'react'
|
||||||
|
import Taro from '@tarojs/taro'
|
||||||
|
import { View, Text, ScrollView } from '@tarojs/components'
|
||||||
|
import { Field, Icon, Button, Uploader } from '@antmjs/vantui'
|
||||||
|
import './index.less'
|
||||||
|
|
||||||
|
function getStatusBarH(): number {
|
||||||
|
try { return Taro.getSystemInfoSync().statusBarHeight || 20 }
|
||||||
|
catch { return 20 }
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSafeBottom(): number {
|
||||||
|
try {
|
||||||
|
const info = Taro.getSystemInfoSync()
|
||||||
|
return (info.screenHeight - info.safeArea!.bottom) || 0
|
||||||
|
} catch { return 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function PublishArticle() {
|
||||||
|
const [title, setTitle] = useState('')
|
||||||
|
const [content, setContent] = useState('')
|
||||||
|
const [images, setImages] = useState<any[]>([])
|
||||||
|
|
||||||
|
const statusBarH = useMemo(getStatusBarH, [])
|
||||||
|
const safeBottom = useMemo(getSafeBottom, [])
|
||||||
|
|
||||||
|
const handleBack = useCallback(() => { Taro.navigateBack() }, [])
|
||||||
|
|
||||||
|
const handleSubmit = useCallback(() => {
|
||||||
|
if (!title.trim()) {
|
||||||
|
Taro.showToast({ title: '请输入文章标题', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!content.trim()) {
|
||||||
|
Taro.showToast({ title: '请输入正文内容', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
Taro.showToast({ title: '发布成功', icon: 'success' })
|
||||||
|
setTimeout(() => { Taro.navigateBack() }, 1500)
|
||||||
|
}, [title, content])
|
||||||
|
|
||||||
|
const handleUploadRead = useCallback((e: any) => {
|
||||||
|
const file = e.detail?.file || e.detail
|
||||||
|
if (file) {
|
||||||
|
setImages((prev) => [...prev, { url: file.path || file.url, name: file.name || 'image' }])
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleUploadDelete = useCallback((e: any) => {
|
||||||
|
const idx = e.detail?.index ?? e.detail
|
||||||
|
setImages((prev) => prev.filter((_, i) => i !== idx))
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View className='publish-page'>
|
||||||
|
{/* 导航栏 */}
|
||||||
|
<View className='publish-nav' style={{ paddingTop: `${statusBarH}px` }}>
|
||||||
|
<View className='nav-content'>
|
||||||
|
<View className='nav-back' onClick={handleBack}>
|
||||||
|
<Icon name='arrow-left' size={22} color='#323233' />
|
||||||
|
</View>
|
||||||
|
<Text className='nav-title'>发布文章</Text>
|
||||||
|
<View className='nav-placeholder' />
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<View style={{ height: `${statusBarH + 44}px` }} />
|
||||||
|
|
||||||
|
<ScrollView className='publish-scroll' scrollY enhanced showScrollbar={false}>
|
||||||
|
{/* 基本信息卡片 */}
|
||||||
|
<View className='form-card'>
|
||||||
|
<Text className='card-title'>文章内容</Text>
|
||||||
|
<Field
|
||||||
|
value={title}
|
||||||
|
placeholder='请输入文章标题'
|
||||||
|
border={false}
|
||||||
|
required
|
||||||
|
onInput={(e) => setTitle(String(e.detail.value))}
|
||||||
|
/>
|
||||||
|
<View style={{ height: '1px', background: '#f2f3f5', margin: '0 24px' }} />
|
||||||
|
<Field
|
||||||
|
value={content}
|
||||||
|
placeholder='请输入正文内容'
|
||||||
|
type='textarea'
|
||||||
|
autosize
|
||||||
|
border={false}
|
||||||
|
maxlength={5000}
|
||||||
|
showWordLimit
|
||||||
|
onInput={(e) => setContent(String(e.detail.value))}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 封面图片 */}
|
||||||
|
<View className='form-card'>
|
||||||
|
<Text className='card-title'>封面图片</Text>
|
||||||
|
<View className='upload-section'>
|
||||||
|
<Text className='upload-label'>支持 jpg/png,最多 3 张</Text>
|
||||||
|
<Uploader
|
||||||
|
fileList={images}
|
||||||
|
accept='image'
|
||||||
|
maxCount={3}
|
||||||
|
multiple
|
||||||
|
deletable
|
||||||
|
previewFullImage
|
||||||
|
uploadText='上传图片'
|
||||||
|
onAfterRead={handleUploadRead}
|
||||||
|
onDelete={handleUploadDelete}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className='scroll-bottom-safe' />
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
{/* 底部提交 */}
|
||||||
|
<View className='bottom-submit-bar' style={{ paddingBottom: `${safeBottom}px` }}>
|
||||||
|
<View className='submit-btn'>
|
||||||
|
<Button type='primary' round block onClick={handleSubmit}>
|
||||||
|
发布文章
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export default definePageConfig({
|
||||||
|
navigationBarTitleText: '快递代取',
|
||||||
|
navigationStyle: 'custom',
|
||||||
|
})
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
/* ========================================
|
||||||
|
快递代取发布页
|
||||||
|
======================================== */
|
||||||
|
|
||||||
|
.publish-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #f7f8fa;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 导航栏(复用 article 页样式) ========== */
|
||||||
|
.publish-nav {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 100;
|
||||||
|
background: rgba(255, 255, 255, 0.9);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
-webkit-backdrop-filter: blur(20px);
|
||||||
|
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
||||||
|
|
||||||
|
.nav-content {
|
||||||
|
height: 44px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 24px;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.nav-back {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-left: -10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-title {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #1a1a1a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-placeholder {
|
||||||
|
width: 50px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 滚动区域 ========== */
|
||||||
|
.publish-scroll {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 表单卡片 ========== */
|
||||||
|
.form-card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 16px;
|
||||||
|
margin: 16px 24px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
display: block;
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #323233;
|
||||||
|
padding: 24px 24px 0;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 金额单位提示 ========== */
|
||||||
|
.reward-hint {
|
||||||
|
padding: 0 24px 16px;
|
||||||
|
font-size: 24px;
|
||||||
|
color: #969799;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 底部提交栏 ========== */
|
||||||
|
.bottom-submit-bar {
|
||||||
|
padding: 16px 24px;
|
||||||
|
background: #fff;
|
||||||
|
border-top: 1px solid #f2f3f5;
|
||||||
|
|
||||||
|
.submit-btn {
|
||||||
|
:global(.van-button) {
|
||||||
|
height: 88px;
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 底部安全区占位 ========== */
|
||||||
|
.scroll-bottom-safe {
|
||||||
|
height: 160px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
import { useState, useMemo, useCallback } from 'react'
|
||||||
|
import Taro from '@tarojs/taro'
|
||||||
|
import { View, Text, ScrollView } from '@tarojs/components'
|
||||||
|
import { Field, Icon, Button } from '@antmjs/vantui'
|
||||||
|
import './index.less'
|
||||||
|
|
||||||
|
function getStatusBarH(): number {
|
||||||
|
try { return Taro.getSystemInfoSync().statusBarHeight || 20 }
|
||||||
|
catch { return 20 }
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSafeBottom(): number {
|
||||||
|
try {
|
||||||
|
const info = Taro.getSystemInfoSync()
|
||||||
|
return (info.screenHeight - info.safeArea!.bottom) || 0
|
||||||
|
} catch { return 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function PublishExpress() {
|
||||||
|
const [title, setTitle] = useState('')
|
||||||
|
const [desc, setDesc] = useState('')
|
||||||
|
const [location, setLocation] = useState('')
|
||||||
|
const [reward, setReward] = useState('')
|
||||||
|
const [deadline, setDeadline] = useState('')
|
||||||
|
|
||||||
|
const statusBarH = useMemo(getStatusBarH, [])
|
||||||
|
const safeBottom = useMemo(getSafeBottom, [])
|
||||||
|
|
||||||
|
const handleBack = useCallback(() => { Taro.navigateBack() }, [])
|
||||||
|
|
||||||
|
const handleSubmit = useCallback(() => {
|
||||||
|
if (!title.trim()) {
|
||||||
|
Taro.showToast({ title: '请输入任务标题', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!desc.trim()) {
|
||||||
|
Taro.showToast({ title: '请输入任务描述', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!location.trim()) {
|
||||||
|
Taro.showToast({ title: '请输入取件地点', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!reward.trim()) {
|
||||||
|
Taro.showToast({ title: '请输入报酬金额', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
Taro.showToast({ title: '发布成功', icon: 'success' })
|
||||||
|
setTimeout(() => { Taro.navigateBack() }, 1500)
|
||||||
|
}, [title, desc, location, reward])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View className='publish-page'>
|
||||||
|
{/* 导航栏 */}
|
||||||
|
<View className='publish-nav' style={{ paddingTop: `${statusBarH}px` }}>
|
||||||
|
<View className='nav-content'>
|
||||||
|
<View className='nav-back' onClick={handleBack}>
|
||||||
|
<Icon name='arrow-left' size={22} color='#323233' />
|
||||||
|
</View>
|
||||||
|
<Text className='nav-title'>快递代取</Text>
|
||||||
|
<View className='nav-placeholder' />
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<View style={{ height: `${statusBarH + 44}px` }} />
|
||||||
|
|
||||||
|
<ScrollView className='publish-scroll' scrollY enhanced showScrollbar={false}>
|
||||||
|
{/* 基本信息 */}
|
||||||
|
<View className='form-card'>
|
||||||
|
<Text className='card-title'>任务信息</Text>
|
||||||
|
<Field
|
||||||
|
value={title}
|
||||||
|
placeholder='简要描述代取任务,如:帮我取个快递'
|
||||||
|
border={false}
|
||||||
|
required
|
||||||
|
onInput={(e) => setTitle(String(e.detail.value))}
|
||||||
|
/>
|
||||||
|
<View style={{ height: '1px', background: '#f2f3f5', margin: '0 24px' }} />
|
||||||
|
<Field
|
||||||
|
value={desc}
|
||||||
|
placeholder='详细描述快递信息,如快递公司、柜号、包裹大小等'
|
||||||
|
type='textarea'
|
||||||
|
autosize
|
||||||
|
border={false}
|
||||||
|
maxlength={500}
|
||||||
|
showWordLimit
|
||||||
|
onInput={(e) => setDesc(String(e.detail.value))}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 详细信息 */}
|
||||||
|
<View className='form-card'>
|
||||||
|
<Text className='card-title'>详细信息</Text>
|
||||||
|
<Field
|
||||||
|
value={location}
|
||||||
|
placeholder='如:南区菜鸟驿站3号柜'
|
||||||
|
label='取件地点'
|
||||||
|
border={false}
|
||||||
|
required
|
||||||
|
onInput={(e) => setLocation(String(e.detail.value))}
|
||||||
|
/>
|
||||||
|
<View style={{ height: '1px', background: '#f2f3f5', margin: '0 24px' }} />
|
||||||
|
<Field
|
||||||
|
value={reward}
|
||||||
|
placeholder='如:5'
|
||||||
|
label='报酬 (元)'
|
||||||
|
type='digit'
|
||||||
|
border={false}
|
||||||
|
required
|
||||||
|
onInput={(e) => setReward(String(e.detail.value))}
|
||||||
|
/>
|
||||||
|
{reward && (
|
||||||
|
<Text className='reward-hint'>报酬金额:¥{Number(reward).toFixed(2)}</Text>
|
||||||
|
)}
|
||||||
|
<View style={{ height: '1px', background: '#f2f3f5', margin: '0 24px' }} />
|
||||||
|
<Field
|
||||||
|
value={deadline}
|
||||||
|
placeholder='如:今天18:00前'
|
||||||
|
label='截止时间'
|
||||||
|
border={false}
|
||||||
|
onInput={(e) => setDeadline(String(e.detail.value))}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className='scroll-bottom-safe' />
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
{/* 底部提交 */}
|
||||||
|
<View className='bottom-submit-bar' style={{ paddingBottom: `${safeBottom}px` }}>
|
||||||
|
<View className='submit-btn'>
|
||||||
|
<Button type='primary' round block onClick={handleSubmit}>
|
||||||
|
发布任务
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export default definePageConfig({
|
||||||
|
navigationBarTitleText: '发布二手',
|
||||||
|
navigationStyle: 'custom',
|
||||||
|
})
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
/* ========================================
|
||||||
|
发布二手页
|
||||||
|
======================================== */
|
||||||
|
|
||||||
|
.publish-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #f7f8fa;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 导航栏(复用 article 页样式) ========== */
|
||||||
|
.publish-nav {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 100;
|
||||||
|
background: rgba(255, 255, 255, 0.9);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
-webkit-backdrop-filter: blur(20px);
|
||||||
|
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
||||||
|
|
||||||
|
.nav-content {
|
||||||
|
height: 44px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 24px;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.nav-back {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-left: -10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-title {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #1a1a1a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-placeholder {
|
||||||
|
width: 50px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 滚动区域 ========== */
|
||||||
|
.publish-scroll {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 表单卡片 ========== */
|
||||||
|
.form-card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 16px;
|
||||||
|
margin: 16px 24px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
display: block;
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #323233;
|
||||||
|
padding: 24px 24px 0;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 图片上传区域 ========== */
|
||||||
|
.upload-section {
|
||||||
|
padding: 16px 24px 24px;
|
||||||
|
|
||||||
|
.upload-label {
|
||||||
|
display: block;
|
||||||
|
font-size: 26px;
|
||||||
|
color: #969799;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 标签提示 ========== */
|
||||||
|
.tag-hint {
|
||||||
|
padding: 0 24px 16px;
|
||||||
|
font-size: 24px;
|
||||||
|
color: #969799;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 底部提交栏 ========== */
|
||||||
|
.bottom-submit-bar {
|
||||||
|
padding: 16px 24px;
|
||||||
|
background: #fff;
|
||||||
|
border-top: 1px solid #f2f3f5;
|
||||||
|
|
||||||
|
.submit-btn {
|
||||||
|
:global(.van-button) {
|
||||||
|
height: 88px;
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 底部安全区占位 ========== */
|
||||||
|
.scroll-bottom-safe {
|
||||||
|
height: 160px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
import { useState, useMemo, useCallback } from 'react'
|
||||||
|
import Taro from '@tarojs/taro'
|
||||||
|
import { View, Text, ScrollView } from '@tarojs/components'
|
||||||
|
import { Field, Icon, Button, Uploader } from '@antmjs/vantui'
|
||||||
|
import './index.less'
|
||||||
|
|
||||||
|
function getStatusBarH(): number {
|
||||||
|
try { return Taro.getSystemInfoSync().statusBarHeight || 20 }
|
||||||
|
catch { return 20 }
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSafeBottom(): number {
|
||||||
|
try {
|
||||||
|
const info = Taro.getSystemInfoSync()
|
||||||
|
return (info.screenHeight - info.safeArea!.bottom) || 0
|
||||||
|
} catch { return 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function PublishSecondhand() {
|
||||||
|
const [title, setTitle] = useState('')
|
||||||
|
const [desc, setDesc] = useState('')
|
||||||
|
const [images, setImages] = useState<any[]>([])
|
||||||
|
const [price, setPrice] = useState('')
|
||||||
|
const [location, setLocation] = useState('')
|
||||||
|
const [tags, setTags] = useState('')
|
||||||
|
|
||||||
|
const statusBarH = useMemo(getStatusBarH, [])
|
||||||
|
const safeBottom = useMemo(getSafeBottom, [])
|
||||||
|
|
||||||
|
const handleBack = useCallback(() => { Taro.navigateBack() }, [])
|
||||||
|
|
||||||
|
const handleSubmit = useCallback(() => {
|
||||||
|
if (!title.trim()) {
|
||||||
|
Taro.showToast({ title: '请输入商品标题', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!desc.trim()) {
|
||||||
|
Taro.showToast({ title: '请输入商品描述', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!price.trim()) {
|
||||||
|
Taro.showToast({ title: '请输入售价', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
Taro.showToast({ title: '发布成功', icon: 'success' })
|
||||||
|
setTimeout(() => { Taro.navigateBack() }, 1500)
|
||||||
|
}, [title, desc, price])
|
||||||
|
|
||||||
|
const handleUploadRead = useCallback((e: any) => {
|
||||||
|
const file = e.detail?.file || e.detail
|
||||||
|
if (file) {
|
||||||
|
setImages((prev) => [...prev, { url: file.path || file.url, name: file.name || 'image' }])
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleUploadDelete = useCallback((e: any) => {
|
||||||
|
const idx = e.detail?.index ?? e.detail
|
||||||
|
setImages((prev) => prev.filter((_, i) => i !== idx))
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View className='publish-page'>
|
||||||
|
{/* 导航栏 */}
|
||||||
|
<View className='publish-nav' style={{ paddingTop: `${statusBarH}px` }}>
|
||||||
|
<View className='nav-content'>
|
||||||
|
<View className='nav-back' onClick={handleBack}>
|
||||||
|
<Icon name='arrow-left' size={22} color='#323233' />
|
||||||
|
</View>
|
||||||
|
<Text className='nav-title'>发布二手</Text>
|
||||||
|
<View className='nav-placeholder' />
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<View style={{ height: `${statusBarH + 44}px` }} />
|
||||||
|
|
||||||
|
<ScrollView className='publish-scroll' scrollY enhanced showScrollbar={false}>
|
||||||
|
{/* 商品信息 */}
|
||||||
|
<View className='form-card'>
|
||||||
|
<Text className='card-title'>商品信息</Text>
|
||||||
|
<Field
|
||||||
|
value={title}
|
||||||
|
placeholder='简要描述商品,如:出手99新 iPad Air 5'
|
||||||
|
border={false}
|
||||||
|
required
|
||||||
|
onInput={(e) => setTitle(String(e.detail.value))}
|
||||||
|
/>
|
||||||
|
<View style={{ height: '1px', background: '#f2f3f5', margin: '0 24px' }} />
|
||||||
|
<Field
|
||||||
|
value={desc}
|
||||||
|
placeholder='详细描述商品状况、购买时间、配件等'
|
||||||
|
type='textarea'
|
||||||
|
autosize
|
||||||
|
border={false}
|
||||||
|
maxlength={1000}
|
||||||
|
showWordLimit
|
||||||
|
onInput={(e) => setDesc(String(e.detail.value))}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 商品图片 */}
|
||||||
|
<View className='form-card'>
|
||||||
|
<Text className='card-title'>商品图片</Text>
|
||||||
|
<View className='upload-section'>
|
||||||
|
<Text className='upload-label'>支持 jpg/png,最多 6 张,第一张为封面</Text>
|
||||||
|
<Uploader
|
||||||
|
fileList={images}
|
||||||
|
accept='image'
|
||||||
|
maxCount={6}
|
||||||
|
multiple
|
||||||
|
deletable
|
||||||
|
previewFullImage
|
||||||
|
uploadText='上传图片'
|
||||||
|
onAfterRead={handleUploadRead}
|
||||||
|
onDelete={handleUploadDelete}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 交易信息 */}
|
||||||
|
<View className='form-card'>
|
||||||
|
<Text className='card-title'>交易信息</Text>
|
||||||
|
<Field
|
||||||
|
value={price}
|
||||||
|
placeholder='如:3200'
|
||||||
|
label='售价 (元)'
|
||||||
|
type='digit'
|
||||||
|
border={false}
|
||||||
|
required
|
||||||
|
onInput={(e) => setPrice(String(e.detail.value))}
|
||||||
|
/>
|
||||||
|
{price && (
|
||||||
|
<Text className='tag-hint'>售价金额:¥{Number(price).toFixed(2)}</Text>
|
||||||
|
)}
|
||||||
|
<View style={{ height: '1px', background: '#f2f3f5', margin: '0 24px' }} />
|
||||||
|
<Field
|
||||||
|
value={location}
|
||||||
|
placeholder='如:东区宿舍'
|
||||||
|
label='交易地点'
|
||||||
|
border={false}
|
||||||
|
onInput={(e) => setLocation(String(e.detail.value))}
|
||||||
|
/>
|
||||||
|
<View style={{ height: '1px', background: '#f2f3f5', margin: '0 24px' }} />
|
||||||
|
<Field
|
||||||
|
value={tags}
|
||||||
|
placeholder='如:数码,99新,在保'
|
||||||
|
label='商品标签'
|
||||||
|
border={false}
|
||||||
|
onInput={(e) => setTags(String(e.detail.value))}
|
||||||
|
/>
|
||||||
|
{tags && (
|
||||||
|
<Text className='tag-hint'>使用逗号分隔,如:数码,99新</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className='scroll-bottom-safe' />
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
{/* 底部提交 */}
|
||||||
|
<View className='bottom-submit-bar' style={{ paddingBottom: `${safeBottom}px` }}>
|
||||||
|
<View className='submit-btn'>
|
||||||
|
<Button type='primary' round block onClick={handleSubmit}>
|
||||||
|
发布商品
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export default definePageConfig({
|
||||||
|
navigationBarTitleText: '系统消息',
|
||||||
|
navigationStyle: 'custom',
|
||||||
|
})
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
/* ========================================
|
||||||
|
系统消息页
|
||||||
|
======================================== */
|
||||||
|
|
||||||
|
.system-msg-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #f7f8fa;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 导航栏 ========== */
|
||||||
|
.sys-nav {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 100;
|
||||||
|
background: rgba(255, 255, 255, 0.9);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
-webkit-backdrop-filter: blur(20px);
|
||||||
|
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
||||||
|
|
||||||
|
.nav-content {
|
||||||
|
height: 44px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 24px;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.nav-back {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-left: -10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-title {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #1a1a1a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-placeholder {
|
||||||
|
width: 50px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 滚动区域 ========== */
|
||||||
|
.sys-scroll {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 消息列表 ========== */
|
||||||
|
.sys-list {
|
||||||
|
padding: 16px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 消息卡片 ========== */
|
||||||
|
.sys-card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 24px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
|
||||||
|
|
||||||
|
.sys-card-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
|
||||||
|
.sys-card-icon {
|
||||||
|
width: 72px;
|
||||||
|
height: 72px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: linear-gradient(135deg, #1989fa 0%, #07c160 100%);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 36px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sys-card-title {
|
||||||
|
font-size: 30px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #1a1a1a;
|
||||||
|
margin-left: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sys-card-content {
|
||||||
|
font-size: 28px;
|
||||||
|
color: #646566;
|
||||||
|
line-height: 1.7;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sys-card-time {
|
||||||
|
text-align: right;
|
||||||
|
font-size: 24px;
|
||||||
|
color: #c8c9cc;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import { useMemo, useCallback } from 'react'
|
||||||
|
import Taro from '@tarojs/taro'
|
||||||
|
import { View, Text, ScrollView } from '@tarojs/components'
|
||||||
|
import { Icon } from '@antmjs/vantui'
|
||||||
|
import { chatList } from '@/mock/messages'
|
||||||
|
import './index.less'
|
||||||
|
|
||||||
|
function getStatusBarH(): number {
|
||||||
|
try { return Taro.getSystemInfoSync().statusBarHeight || 20 }
|
||||||
|
catch { return 20 }
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SystemMessages() {
|
||||||
|
const statusBarH = useMemo(getStatusBarH, [])
|
||||||
|
|
||||||
|
const systemMsgs = useMemo(
|
||||||
|
() => chatList.filter((m) => m.type === 'system'),
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleBack = useCallback(() => {
|
||||||
|
Taro.navigateBack()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View className='system-msg-page'>
|
||||||
|
{/* ===== 导航栏 ===== */}
|
||||||
|
<View className='sys-nav' style={{ paddingTop: `${statusBarH}px` }}>
|
||||||
|
<View className='nav-content'>
|
||||||
|
<View className='nav-back' onClick={handleBack}>
|
||||||
|
<Icon name='arrow-left' size={22} color='#323233' />
|
||||||
|
</View>
|
||||||
|
<Text className='nav-title'>系统消息</Text>
|
||||||
|
<View className='nav-placeholder' />
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<View style={{ height: `${statusBarH + 44}px` }} />
|
||||||
|
|
||||||
|
{/* ===== 消息列表 ===== */}
|
||||||
|
<ScrollView className='sys-scroll' scrollY enhanced showScrollbar={false}>
|
||||||
|
<View className='sys-list'>
|
||||||
|
{systemMsgs.length > 0 ? (
|
||||||
|
systemMsgs.map((msg) => (
|
||||||
|
<View key={msg.id} className='sys-card'>
|
||||||
|
<View className='sys-card-header'>
|
||||||
|
<View className='sys-card-icon'>
|
||||||
|
<Text>{msg.title.slice(0, 2)}</Text>
|
||||||
|
</View>
|
||||||
|
<Text className='sys-card-title'>{msg.title}</Text>
|
||||||
|
</View>
|
||||||
|
<Text className='sys-card-content'>{msg.content}</Text>
|
||||||
|
<Text className='sys-card-time'>{msg.time}</Text>
|
||||||
|
</View>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<View style={{ textAlign: 'center', padding: '80px 0' }}>
|
||||||
|
<Text style={{ fontSize: '28px', color: '#c8c9cc' }}>暂无系统消息</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</ScrollView>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export default definePageConfig({
|
||||||
|
navigationBarTitleText: '任务详情',
|
||||||
|
navigationStyle: 'custom',
|
||||||
|
})
|
||||||
@@ -0,0 +1,381 @@
|
|||||||
|
/* ========================================
|
||||||
|
任务详情页
|
||||||
|
======================================== */
|
||||||
|
|
||||||
|
.task-detail-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #f7f8fa;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 导航栏 ========== */
|
||||||
|
.detail-nav {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 100;
|
||||||
|
background: rgba(255, 255, 255, 0.9);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
-webkit-backdrop-filter: blur(20px);
|
||||||
|
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
||||||
|
|
||||||
|
.nav-content {
|
||||||
|
height: 44px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 24px;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.nav-back {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-left: -10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-title {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #1a1a1a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-placeholder {
|
||||||
|
width: 50px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 滚动区域 ========== */
|
||||||
|
.detail-scroll {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 已取消提示条 ========== */
|
||||||
|
.cancelled-banner {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin: 16px 24px 0;
|
||||||
|
padding: 16px 20px;
|
||||||
|
background: #fff0f0;
|
||||||
|
border: 1px solid #ffcccc;
|
||||||
|
border-radius: 12px;
|
||||||
|
|
||||||
|
.cancelled-icon {
|
||||||
|
font-size: 28px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cancelled-text {
|
||||||
|
font-size: 28px;
|
||||||
|
color: #ee0a24;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 内容卡片通用 ========== */
|
||||||
|
.content-card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 24px;
|
||||||
|
margin: 16px 24px;
|
||||||
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== Steps 进度条区域 ========== */
|
||||||
|
.steps-section {
|
||||||
|
margin-top: 16px;
|
||||||
|
padding: 28px 24px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 类型徽章 ========== */
|
||||||
|
.type-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 4px 14px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 24px;
|
||||||
|
color: #fff;
|
||||||
|
line-height: 1.5;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-express { background: #1989fa; }
|
||||||
|
.badge-secondhand { background: #ff976a; }
|
||||||
|
.badge-help { background: #ff8c00; }
|
||||||
|
.badge-activity { background: #6465e0; }
|
||||||
|
.badge-carpool { background: #07c160; }
|
||||||
|
|
||||||
|
/* ========== 状态徽章 ========== */
|
||||||
|
.status-badge {
|
||||||
|
font-size: 24px;
|
||||||
|
padding: 2px 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-open { color: #07c160; background: #e8f8ef; }
|
||||||
|
.status-progress { color: #1989fa; background: #e8f3ff; }
|
||||||
|
.status-done { color: #969799; background: #f2f3f5; }
|
||||||
|
.status-cancelled { color: #ee0a24; background: #fff0f0; }
|
||||||
|
|
||||||
|
/* ========== 类型/状态行 ========== */
|
||||||
|
.type-status-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
|
||||||
|
.participants-count {
|
||||||
|
font-size: 26px;
|
||||||
|
color: #646566;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 标题和描述 ========== */
|
||||||
|
.detail-title {
|
||||||
|
display: block;
|
||||||
|
font-size: 34px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1a1a1a;
|
||||||
|
line-height: 1.4;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-desc {
|
||||||
|
display: block;
|
||||||
|
font-size: 28px;
|
||||||
|
color: #646566;
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 分割线 ========== */
|
||||||
|
.detail-divider {
|
||||||
|
height: 1px;
|
||||||
|
background: #f2f3f5;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 信息行 ========== */
|
||||||
|
.info-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 28px;
|
||||||
|
color: #646566;
|
||||||
|
line-height: 1.6;
|
||||||
|
|
||||||
|
& + .info-row {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-icon {
|
||||||
|
margin-right: 8px;
|
||||||
|
font-size: 28px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 报酬/价格 ========== */
|
||||||
|
.reward-large {
|
||||||
|
display: block;
|
||||||
|
font-size: 44px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #ee0a24;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 标签行 ========== */
|
||||||
|
.tag-row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 二手商品图片 ========== */
|
||||||
|
.image-gallery {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.gallery-img {
|
||||||
|
width: 100%;
|
||||||
|
height: 420px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 单张图片(不需要 swiper) */
|
||||||
|
.single-image-wrap {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 拼车:报酬+余座行 ========== */
|
||||||
|
.reward-seat-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 12px;
|
||||||
|
|
||||||
|
.seats-info {
|
||||||
|
font-size: 28px;
|
||||||
|
color: #ff8c00;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 4px 16px;
|
||||||
|
background: #fff7e6;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 活动参与进度条 ========== */
|
||||||
|
.progress-section {
|
||||||
|
margin-top: 16px;
|
||||||
|
|
||||||
|
.progress-label {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
|
||||||
|
.progress-text {
|
||||||
|
font-size: 26px;
|
||||||
|
color: #646566;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-percent {
|
||||||
|
font-size: 26px;
|
||||||
|
color: #1989fa;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-track {
|
||||||
|
height: 12px;
|
||||||
|
background: #f2f3f5;
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, #1989fa, #06b7f0);
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 内联操作按钮 ========== */
|
||||||
|
.action-section {
|
||||||
|
margin: 0 24px 16px;
|
||||||
|
|
||||||
|
.action-contact-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 20px 0;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 16px;
|
||||||
|
font-size: 30px;
|
||||||
|
color: #1989fa;
|
||||||
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 发布者卡片 ========== */
|
||||||
|
.publisher-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.pub-avatar {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
border-radius: 50%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pub-info {
|
||||||
|
flex: 1;
|
||||||
|
margin-left: 20px;
|
||||||
|
|
||||||
|
.pub-name {
|
||||||
|
display: block;
|
||||||
|
font-size: 30px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #1a1a1a;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pub-time {
|
||||||
|
font-size: 26px;
|
||||||
|
color: #969799;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pub-contact-btn {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 推荐任务区域 ========== */
|
||||||
|
.recommend-section {
|
||||||
|
padding: 0 24px 16px;
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
display: block;
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1a1a1a;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding-left: 8px;
|
||||||
|
border-left: 6px solid #1989fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recommend-card-wrap {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-recommend {
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px 0;
|
||||||
|
font-size: 28px;
|
||||||
|
color: #c8c9cc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 底部操作栏 ========== */
|
||||||
|
.bottom-action-bar {
|
||||||
|
padding: 16px 24px;
|
||||||
|
background: #fff;
|
||||||
|
border-top: 1px solid #f2f3f5;
|
||||||
|
|
||||||
|
.cta-button {
|
||||||
|
:global(.van-button) {
|
||||||
|
height: 88px;
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 底部安全区占位 ========== */
|
||||||
|
.scroll-bottom-safe {
|
||||||
|
height: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 空状态 ========== */
|
||||||
|
.empty-state {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100vh;
|
||||||
|
font-size: 30px;
|
||||||
|
color: #969799;
|
||||||
|
}
|
||||||
@@ -0,0 +1,641 @@
|
|||||||
|
import { useMemo, useCallback } from 'react'
|
||||||
|
import Taro from '@tarojs/taro'
|
||||||
|
import { View, Text, Image, ScrollView } from '@tarojs/components'
|
||||||
|
import {
|
||||||
|
Steps,
|
||||||
|
Tag,
|
||||||
|
Icon,
|
||||||
|
Button,
|
||||||
|
Image as VantImage,
|
||||||
|
Swiper,
|
||||||
|
SwiperItem,
|
||||||
|
} from '@antmjs/vantui'
|
||||||
|
import type { Step } from '@antmjs/vantui/types/steps'
|
||||||
|
import type { Task, TaskType, TaskStatus } from '@/types/task'
|
||||||
|
import { tasks } from '@/mock/tasks'
|
||||||
|
import TaskCard from '@/components/TaskCard'
|
||||||
|
import './index.less'
|
||||||
|
|
||||||
|
/* ==============================
|
||||||
|
常量映射
|
||||||
|
============================== */
|
||||||
|
|
||||||
|
/** 任务类型 → 中文名称 */
|
||||||
|
const TYPE_BADGE_LABEL: Record<TaskType, string> = {
|
||||||
|
express: '快递代取',
|
||||||
|
secondhand: '二手交易',
|
||||||
|
help: '求助',
|
||||||
|
activity: '校园活动',
|
||||||
|
carpool: '拼车出行',
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 任务类型 → 徽章CSS类名 */
|
||||||
|
const TYPE_BADGE_CLASS: Record<TaskType, string> = {
|
||||||
|
express: 'badge-express',
|
||||||
|
secondhand: 'badge-secondhand',
|
||||||
|
help: 'badge-help',
|
||||||
|
activity: 'badge-activity',
|
||||||
|
carpool: 'badge-carpool',
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 任务状态 → 中文 */
|
||||||
|
const STATUS_LABEL: Record<TaskStatus, string> = {
|
||||||
|
open: '待接单',
|
||||||
|
progress: '进行中',
|
||||||
|
done: '已完成',
|
||||||
|
cancelled: '已取消',
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 二手交易专用状态文案(与快递/求助不同) */
|
||||||
|
const SECONDHAND_STATUS_LABEL: Record<TaskStatus, string> = {
|
||||||
|
open: '待售',
|
||||||
|
progress: '已预订',
|
||||||
|
done: '已售出',
|
||||||
|
cancelled: '已取消',
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 各类型 Steps 步骤配置 */
|
||||||
|
const STATUS_STEPS_MAP: Record<TaskType, Step[]> = {
|
||||||
|
express: [
|
||||||
|
{ text: '待接单', desc: '等待接单' },
|
||||||
|
{ text: '进行中', desc: '配送中' },
|
||||||
|
{ text: '已完成', desc: '任务完成' },
|
||||||
|
],
|
||||||
|
secondhand: [
|
||||||
|
{ text: '待售', desc: '商品待售' },
|
||||||
|
{ text: '已预订', desc: '已被预订' },
|
||||||
|
{ text: '已售出', desc: '交易完成' },
|
||||||
|
],
|
||||||
|
help: [
|
||||||
|
{ text: '待接单', desc: '等待响应' },
|
||||||
|
{ text: '进行中', desc: '帮助进行中' },
|
||||||
|
{ text: '已完成', desc: '帮助完成' },
|
||||||
|
],
|
||||||
|
activity: [
|
||||||
|
{ text: '待开始', desc: '活动待开始' },
|
||||||
|
{ text: '进行中', desc: '活动进行中' },
|
||||||
|
{ text: '已结束', desc: '活动已结束' },
|
||||||
|
],
|
||||||
|
carpool: [
|
||||||
|
{ text: '待出发', desc: '等待乘客' },
|
||||||
|
{ text: '进行中', desc: '行程途中' },
|
||||||
|
{ text: '已完成', desc: '行程结束' },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 状态 → Steps active 索引 */
|
||||||
|
const STATUS_TO_STEP_INDEX: Record<TaskStatus, number> = {
|
||||||
|
open: 0,
|
||||||
|
progress: 1,
|
||||||
|
done: 2,
|
||||||
|
cancelled: -1,
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==============================
|
||||||
|
辅助函数
|
||||||
|
============================== */
|
||||||
|
|
||||||
|
function getStatusBarH(): number {
|
||||||
|
try {
|
||||||
|
return Taro.getSystemInfoSync().statusBarHeight || 20
|
||||||
|
} catch {
|
||||||
|
return 20
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSafeBottom(): number {
|
||||||
|
try {
|
||||||
|
const info = Taro.getSystemInfoSync()
|
||||||
|
return (info.screenHeight - info.safeArea!.bottom) || 0
|
||||||
|
} catch {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取当前任务类型对应的状态文案 */
|
||||||
|
function getStatusLabel(task: Task): string {
|
||||||
|
if (task.type === 'secondhand') return SECONDHAND_STATUS_LABEL[task.status]
|
||||||
|
return STATUS_LABEL[task.status]
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==============================
|
||||||
|
页面组件
|
||||||
|
============================== */
|
||||||
|
|
||||||
|
export default function TaskDetail() {
|
||||||
|
const id = String(Taro.getCurrentInstance().router?.params?.id || '')
|
||||||
|
|
||||||
|
/* ---- 数据 ---- */
|
||||||
|
const task = useMemo(() => tasks.find((t) => t.id === id), [id])
|
||||||
|
|
||||||
|
const statusBarH = useMemo(getStatusBarH, [])
|
||||||
|
const safeBottom = useMemo(getSafeBottom, [])
|
||||||
|
|
||||||
|
const stepsConfig = useMemo<Step[]>(() => {
|
||||||
|
if (!task) return []
|
||||||
|
return STATUS_STEPS_MAP[task.type] || []
|
||||||
|
}, [task])
|
||||||
|
|
||||||
|
const activeStepIndex = useMemo(() => {
|
||||||
|
if (!task) return 0
|
||||||
|
return STATUS_TO_STEP_INDEX[task.status] ?? 0
|
||||||
|
}, [task])
|
||||||
|
|
||||||
|
/** 推荐任务:同类型优先,不足时补充其他类型(排除当前任务),最多 3 条 */
|
||||||
|
const recommendedTasks = useMemo(() => {
|
||||||
|
if (!task) return []
|
||||||
|
const sameType = tasks.filter(
|
||||||
|
(t) => t.type === task.type && t.id !== task.id,
|
||||||
|
)
|
||||||
|
if (sameType.length >= 3) return sameType.slice(0, 3)
|
||||||
|
const others = tasks.filter(
|
||||||
|
(t) => t.id !== task.id && t.type !== task.type,
|
||||||
|
)
|
||||||
|
return [...sameType, ...others].slice(0, 3)
|
||||||
|
}, [task])
|
||||||
|
|
||||||
|
/* ---- 事件 ---- */
|
||||||
|
const handleBack = useCallback(() => {
|
||||||
|
Taro.navigateBack()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleNavigateToDetail = useCallback((targetId: string) => {
|
||||||
|
Taro.navigateTo({ url: `/pages/task-detail/index?id=${targetId}` })
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handlePrimaryAction = useCallback(() => {
|
||||||
|
Taro.showToast({ title: '功能开发中', icon: 'none' })
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleContact = useCallback(() => {
|
||||||
|
Taro.showToast({ title: '联系功能开发中', icon: 'none' })
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
/* ---- 空状态 ---- */
|
||||||
|
if (!task) {
|
||||||
|
return (
|
||||||
|
<View className='task-detail-page'>
|
||||||
|
<View className='empty-state'>任务不存在</View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- 底部主按钮文案 ---- */
|
||||||
|
const getPrimaryBtnText = (): string => {
|
||||||
|
if (task.status === 'done') return '已完成'
|
||||||
|
if (task.status === 'cancelled') return '已取消'
|
||||||
|
if (task.status === 'open') {
|
||||||
|
return task.type === 'activity' ? '报名参加' : '接单'
|
||||||
|
}
|
||||||
|
// progress
|
||||||
|
return '标记完成'
|
||||||
|
}
|
||||||
|
|
||||||
|
const primaryBtnDisabled =
|
||||||
|
task.status === 'done' || task.status === 'cancelled'
|
||||||
|
|
||||||
|
/* ---- 类型信息渲染函数 ---- */
|
||||||
|
|
||||||
|
const renderExpressInfo = () => (
|
||||||
|
<View className='content-card'>
|
||||||
|
<View className='type-status-row'>
|
||||||
|
<Text className={`type-badge ${TYPE_BADGE_CLASS[task.type]}`}>
|
||||||
|
{TYPE_BADGE_LABEL[task.type]}
|
||||||
|
</Text>
|
||||||
|
<Text className={`status-badge status-${task.status}`}>
|
||||||
|
{getStatusLabel(task)}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<Text className='detail-title'>{task.title}</Text>
|
||||||
|
<Text className='detail-desc'>{task.desc}</Text>
|
||||||
|
<Text className='reward-large'>{task.reward}</Text>
|
||||||
|
<View className='detail-divider' />
|
||||||
|
<View className='info-row'>
|
||||||
|
<Text className='info-icon'>📍</Text>
|
||||||
|
<Text>取件地点:{task.location}</Text>
|
||||||
|
</View>
|
||||||
|
{task.deadline && (
|
||||||
|
<View className='info-row'>
|
||||||
|
<Text className='info-icon'>⏰</Text>
|
||||||
|
<Text>截止时间:{task.deadline}</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
<View className='info-row'>
|
||||||
|
<Text className='info-icon'>🕐</Text>
|
||||||
|
<Text>发布时间:{task.time}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
|
||||||
|
const renderSecondhandInfo = () => (
|
||||||
|
<View className='content-card'>
|
||||||
|
{/* 商品图片 */}
|
||||||
|
{task.images && task.images.length > 0 && (
|
||||||
|
<>
|
||||||
|
{task.images.length === 1 ? (
|
||||||
|
<View className='single-image-wrap'>
|
||||||
|
<VantImage
|
||||||
|
src={task.images[0]}
|
||||||
|
fit='cover'
|
||||||
|
width='100%'
|
||||||
|
height={420}
|
||||||
|
radius={12}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<Swiper
|
||||||
|
className='image-gallery'
|
||||||
|
autoPlay={0}
|
||||||
|
loop
|
||||||
|
paginationVisible
|
||||||
|
paginationColor='#1989fa'
|
||||||
|
height={420}
|
||||||
|
>
|
||||||
|
{task.images.map((img, idx) => (
|
||||||
|
<SwiperItem key={idx}>
|
||||||
|
<VantImage
|
||||||
|
src={img}
|
||||||
|
fit='cover'
|
||||||
|
width='100%'
|
||||||
|
height={420}
|
||||||
|
/>
|
||||||
|
</SwiperItem>
|
||||||
|
))}
|
||||||
|
</Swiper>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<View className='type-status-row'>
|
||||||
|
<Text className={`type-badge ${TYPE_BADGE_CLASS[task.type]}`}>
|
||||||
|
{TYPE_BADGE_LABEL[task.type]}
|
||||||
|
</Text>
|
||||||
|
<Text className={`status-badge status-${task.status}`}>
|
||||||
|
{getStatusLabel(task)}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<Text className='detail-title'>{task.title}</Text>
|
||||||
|
<Text className='detail-desc'>{task.desc}</Text>
|
||||||
|
|
||||||
|
{task.tags && task.tags.length > 0 && (
|
||||||
|
<View className='tag-row'>
|
||||||
|
{task.tags.map((t) => (
|
||||||
|
<Tag key={t} type='primary' plain>
|
||||||
|
{t}
|
||||||
|
</Tag>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Text className='reward-large'>{task.reward}</Text>
|
||||||
|
|
||||||
|
<View className='detail-divider' />
|
||||||
|
<View className='info-row'>
|
||||||
|
<Text className='info-icon'>📍</Text>
|
||||||
|
<Text>交易地点:{task.location}</Text>
|
||||||
|
</View>
|
||||||
|
<View className='info-row'>
|
||||||
|
<Text className='info-icon'>🕐</Text>
|
||||||
|
<Text>发布时间:{task.time}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
|
||||||
|
const renderHelpInfo = () => (
|
||||||
|
<View className='content-card'>
|
||||||
|
<View className='type-status-row'>
|
||||||
|
<Text className={`type-badge ${TYPE_BADGE_CLASS[task.type]}`}>
|
||||||
|
{TYPE_BADGE_LABEL[task.type]}
|
||||||
|
</Text>
|
||||||
|
{task.tags && task.tags.length > 0 && (
|
||||||
|
<View className='tag-row' style={{ marginTop: 0 }}>
|
||||||
|
{task.tags.map((t) => (
|
||||||
|
<Tag key={t} type='warning' size='medium'>
|
||||||
|
{t}
|
||||||
|
</Tag>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
<Text className={`status-badge status-${task.status}`}>
|
||||||
|
{getStatusLabel(task)}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<Text className='detail-title'>{task.title}</Text>
|
||||||
|
<Text className='detail-desc'>{task.desc}</Text>
|
||||||
|
<Text className='reward-large'>{task.reward}</Text>
|
||||||
|
<View className='detail-divider' />
|
||||||
|
<View className='info-row'>
|
||||||
|
<Text className='info-icon'>📍</Text>
|
||||||
|
<Text>帮助地点:{task.location}</Text>
|
||||||
|
</View>
|
||||||
|
<View className='info-row'>
|
||||||
|
<Text className='info-icon'>🕐</Text>
|
||||||
|
<Text>发布时间:{task.time}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
|
||||||
|
const renderActivityInfo = () => {
|
||||||
|
const progress = task.maxParticipants
|
||||||
|
? Math.round(
|
||||||
|
((task.participants || 0) / task.maxParticipants) * 100,
|
||||||
|
)
|
||||||
|
: 0
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View className='content-card'>
|
||||||
|
{/* 活动图片(可选) */}
|
||||||
|
{task.images && task.images.length > 0 && (
|
||||||
|
<>
|
||||||
|
{task.images.length === 1 ? (
|
||||||
|
<View className='single-image-wrap'>
|
||||||
|
<VantImage
|
||||||
|
src={task.images[0]}
|
||||||
|
fit='cover'
|
||||||
|
width='100%'
|
||||||
|
height={360}
|
||||||
|
radius={12}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<Swiper
|
||||||
|
className='image-gallery'
|
||||||
|
autoPlay={0}
|
||||||
|
loop
|
||||||
|
paginationVisible
|
||||||
|
paginationColor='#1989fa'
|
||||||
|
height={360}
|
||||||
|
>
|
||||||
|
{task.images.map((img, idx) => (
|
||||||
|
<SwiperItem key={idx}>
|
||||||
|
<VantImage
|
||||||
|
src={img}
|
||||||
|
fit='cover'
|
||||||
|
width='100%'
|
||||||
|
height={360}
|
||||||
|
/>
|
||||||
|
</SwiperItem>
|
||||||
|
))}
|
||||||
|
</Swiper>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<View className='type-status-row'>
|
||||||
|
<Text className={`type-badge ${TYPE_BADGE_CLASS[task.type]}`}>
|
||||||
|
{TYPE_BADGE_LABEL[task.type]}
|
||||||
|
</Text>
|
||||||
|
<Text className={`status-badge status-${task.status}`}>
|
||||||
|
{getStatusLabel(task)}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<Text className='detail-title'>{task.title}</Text>
|
||||||
|
<Text className='detail-desc'>{task.desc}</Text>
|
||||||
|
|
||||||
|
{/* 参与人数 + 进度条 */}
|
||||||
|
<View className='progress-section'>
|
||||||
|
<View className='progress-label'>
|
||||||
|
<Text className='progress-text'>
|
||||||
|
👥 已报名 {task.participants || 0}/{task.maxParticipants || 0} 人
|
||||||
|
</Text>
|
||||||
|
<Text className='progress-percent'>{progress}%</Text>
|
||||||
|
</View>
|
||||||
|
<View className='progress-track'>
|
||||||
|
<View
|
||||||
|
className='progress-bar'
|
||||||
|
style={{ width: `${progress}%` }}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{task.tags && task.tags.length > 0 && (
|
||||||
|
<View className='tag-row'>
|
||||||
|
{task.tags.map((t) => (
|
||||||
|
<Tag key={t} type='primary' plain>
|
||||||
|
{t}
|
||||||
|
</Tag>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<View className='detail-divider' />
|
||||||
|
<View className='info-row'>
|
||||||
|
<Text className='info-icon'>📍</Text>
|
||||||
|
<Text>活动地点:{task.location}</Text>
|
||||||
|
</View>
|
||||||
|
<View className='info-row'>
|
||||||
|
<Text className='info-icon'>🕐</Text>
|
||||||
|
<Text>发布时间:{task.time}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderCarpoolInfo = () => {
|
||||||
|
const remainingSeats =
|
||||||
|
task.participants != null && task.maxParticipants != null
|
||||||
|
? task.maxParticipants - task.participants
|
||||||
|
: null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View className='content-card'>
|
||||||
|
<View className='type-status-row'>
|
||||||
|
<Text className={`type-badge ${TYPE_BADGE_CLASS[task.type]}`}>
|
||||||
|
{TYPE_BADGE_LABEL[task.type]}
|
||||||
|
</Text>
|
||||||
|
<Text className={`status-badge status-${task.status}`}>
|
||||||
|
{getStatusLabel(task)}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<Text className='detail-title'>{task.title}</Text>
|
||||||
|
<Text className='detail-desc'>{task.desc}</Text>
|
||||||
|
|
||||||
|
<View className='reward-seat-row'>
|
||||||
|
<Text className='reward-large' style={{ marginTop: 0 }}>
|
||||||
|
{task.reward}
|
||||||
|
</Text>
|
||||||
|
{remainingSeats !== null && (
|
||||||
|
<Text className='seats-info'>
|
||||||
|
🚗 余{remainingSeats}座
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className='detail-divider' />
|
||||||
|
<View className='info-row'>
|
||||||
|
<Text className='info-icon'>📍</Text>
|
||||||
|
<Text>出发地点:{task.location}</Text>
|
||||||
|
</View>
|
||||||
|
{task.deadline && (
|
||||||
|
<View className='info-row'>
|
||||||
|
<Text className='info-icon'>⏰</Text>
|
||||||
|
<Text>出发时间:{task.deadline}</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
<View className='info-row'>
|
||||||
|
<Text className='info-icon'>🕐</Text>
|
||||||
|
<Text>发布时间:{task.time}</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{task.tags && task.tags.length > 0 && (
|
||||||
|
<View className='tag-row'>
|
||||||
|
{task.tags.map((t) => (
|
||||||
|
<Tag key={t} type='primary' plain>
|
||||||
|
{t}
|
||||||
|
</Tag>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderTaskInfo = () => {
|
||||||
|
switch (task.type) {
|
||||||
|
case 'express':
|
||||||
|
return renderExpressInfo()
|
||||||
|
case 'secondhand':
|
||||||
|
return renderSecondhandInfo()
|
||||||
|
case 'help':
|
||||||
|
return renderHelpInfo()
|
||||||
|
case 'activity':
|
||||||
|
return renderActivityInfo()
|
||||||
|
case 'carpool':
|
||||||
|
return renderCarpoolInfo()
|
||||||
|
default:
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- 主渲染 ---- */
|
||||||
|
return (
|
||||||
|
<View className='task-detail-page'>
|
||||||
|
{/* ===== 自定义导航栏 ===== */}
|
||||||
|
<View
|
||||||
|
className='detail-nav'
|
||||||
|
style={{ paddingTop: `${statusBarH}px` }}
|
||||||
|
>
|
||||||
|
<View className='nav-content'>
|
||||||
|
<View className='nav-back' onClick={handleBack}>
|
||||||
|
<Icon name='arrow-left' size={22} color='#323233' />
|
||||||
|
</View>
|
||||||
|
<Text className='nav-title'>任务详情</Text>
|
||||||
|
<View className='nav-placeholder' />
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 导航栏占位 */}
|
||||||
|
<View style={{ height: `${statusBarH + 44}px` }} />
|
||||||
|
|
||||||
|
{/* ===== 可滚动内容 ===== */}
|
||||||
|
<ScrollView
|
||||||
|
className='detail-scroll'
|
||||||
|
scrollY
|
||||||
|
enhanced
|
||||||
|
showScrollbar={false}
|
||||||
|
>
|
||||||
|
{/* 已取消提示条 */}
|
||||||
|
{task.status === 'cancelled' && (
|
||||||
|
<View className='cancelled-banner'>
|
||||||
|
<Text className='cancelled-icon'>⚠️</Text>
|
||||||
|
<Text className='cancelled-text'>
|
||||||
|
该任务已取消,不再接受新的操作
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Steps 进度条 */}
|
||||||
|
{stepsConfig.length > 0 && (
|
||||||
|
<View className='content-card steps-section'>
|
||||||
|
<Steps
|
||||||
|
steps={stepsConfig}
|
||||||
|
active={
|
||||||
|
activeStepIndex >= 0 ? activeStepIndex : -1
|
||||||
|
}
|
||||||
|
direction='horizontal'
|
||||||
|
activeColor='#1989fa'
|
||||||
|
activeIcon='checked'
|
||||||
|
inactiveIcon='checked'
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 类型相关信息 */}
|
||||||
|
{renderTaskInfo()}
|
||||||
|
|
||||||
|
{/* 联系发布者按钮(非已取消状态) */}
|
||||||
|
{task.status !== 'cancelled' && (
|
||||||
|
<View className='action-section' onClick={handleContact}>
|
||||||
|
<View className='action-contact-btn'>
|
||||||
|
<Icon name='chat-o' size={20} color='#1989fa' />
|
||||||
|
<Text style={{ marginLeft: '8px' }}>联系发布者</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 发布者信息卡 */}
|
||||||
|
<View className='content-card publisher-card'>
|
||||||
|
<Image
|
||||||
|
className='pub-avatar'
|
||||||
|
src={task.publisher.avatar}
|
||||||
|
mode='aspectFill'
|
||||||
|
/>
|
||||||
|
<View className='pub-info'>
|
||||||
|
<Text className='pub-name'>{task.publisher.name}</Text>
|
||||||
|
<Text className='pub-time'>发布于 {task.time}</Text>
|
||||||
|
</View>
|
||||||
|
<View className='pub-contact-btn'>
|
||||||
|
<Button
|
||||||
|
size='small'
|
||||||
|
plain
|
||||||
|
type='primary'
|
||||||
|
round
|
||||||
|
onClick={handleContact}
|
||||||
|
>
|
||||||
|
联系
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 推荐任务区 */}
|
||||||
|
<View className='recommend-section'>
|
||||||
|
<Text className='section-title'>推荐任务</Text>
|
||||||
|
{recommendedTasks.length > 0 ? (
|
||||||
|
recommendedTasks.map((t) => (
|
||||||
|
<View
|
||||||
|
key={t.id}
|
||||||
|
className='recommend-card-wrap'
|
||||||
|
onClick={() => handleNavigateToDetail(t.id)}
|
||||||
|
>
|
||||||
|
<TaskCard task={t} />
|
||||||
|
</View>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<Text className='empty-recommend'>暂无推荐任务</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 底部安全区占位 */}
|
||||||
|
<View className='scroll-bottom-safe' />
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
{/* ===== 底部操作栏 ===== */}
|
||||||
|
<View
|
||||||
|
className='bottom-action-bar'
|
||||||
|
style={{ paddingBottom: `${safeBottom}px` }}
|
||||||
|
>
|
||||||
|
<View className='cta-button'>
|
||||||
|
<Button
|
||||||
|
type='primary'
|
||||||
|
round
|
||||||
|
block
|
||||||
|
disabled={primaryBtnDisabled}
|
||||||
|
onClick={handlePrimaryAction}
|
||||||
|
>
|
||||||
|
{getPrimaryBtnText()}
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -46,7 +46,7 @@ export default function Tasks() {
|
|||||||
}, [activeFilter, searchValue])
|
}, [activeFilter, searchValue])
|
||||||
|
|
||||||
const handleTaskClick = (id: string) => {
|
const handleTaskClick = (id: string) => {
|
||||||
Taro.showToast({ title: '任务详情即将上线', icon: 'none' })
|
Taro.navigateTo({ url: `/pages/task-detail/index?id=${id}` })
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
/** 商品分类 */
|
||||||
|
export type ProductCategory = 'digital' | 'book' | 'life' | 'food' | 'cloth'
|
||||||
|
|
||||||
|
/** 商品数据 */
|
||||||
|
export interface Product {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
desc: string
|
||||||
|
price: number
|
||||||
|
originalPrice?: number // 原价(划线展示)
|
||||||
|
image: string
|
||||||
|
category: ProductCategory
|
||||||
|
merchant: {
|
||||||
|
name: string
|
||||||
|
avatar: string
|
||||||
|
}
|
||||||
|
sales: number // 已售数量
|
||||||
|
tags?: string[]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user