From c25cf0e8b3e08a4372470679a6270b5537ff3f91 Mon Sep 17 00:00:00 2001 From: liu <2302563948@qq.com> Date: Mon, 6 Jul 2026 11:32:58 +0800 Subject: [PATCH] =?UTF-8?q?=E5=95=86=E5=93=81=E4=BB=A5=E5=8F=8A=E5=95=86?= =?UTF-8?q?=E5=93=81=E8=AF=A6=E6=83=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app.config.ts | 7 + src/components/ProductCard/index.less | 92 +++ src/components/ProductCard/index.tsx | 62 ++ src/custom-tab-bar/index.tsx | 12 +- src/mock/conversations.ts | 74 +++ src/mock/products.ts | 131 ++++ src/pages/chat/index.config.ts | 4 + src/pages/chat/index.less | 160 +++++ src/pages/chat/index.tsx | 168 +++++ src/pages/index/index.less | 111 +++- src/pages/index/index.tsx | 155 ++++- src/pages/messages/index.tsx | 6 +- src/pages/product-detail/index.config.ts | 4 + src/pages/product-detail/index.less | 237 +++++++ src/pages/product-detail/index.tsx | 411 ++++++++++++ src/pages/publish-article/index.config.ts | 4 + src/pages/publish-article/index.less | 109 ++++ src/pages/publish-article/index.tsx | 124 ++++ src/pages/publish-express/index.config.ts | 4 + src/pages/publish-express/index.less | 104 +++ src/pages/publish-express/index.tsx | 137 ++++ src/pages/publish-secondhand/index.config.ts | 4 + src/pages/publish-secondhand/index.less | 116 ++++ src/pages/publish-secondhand/index.tsx | 167 +++++ src/pages/system-messages/index.config.ts | 4 + src/pages/system-messages/index.less | 112 ++++ src/pages/system-messages/index.tsx | 64 ++ src/pages/task-detail/index.config.ts | 4 + src/pages/task-detail/index.less | 381 +++++++++++ src/pages/task-detail/index.tsx | 641 +++++++++++++++++++ src/pages/tasks/index.tsx | 2 +- src/types/product.ts | 19 + 32 files changed, 3585 insertions(+), 45 deletions(-) create mode 100644 src/components/ProductCard/index.less create mode 100644 src/components/ProductCard/index.tsx create mode 100644 src/mock/conversations.ts create mode 100644 src/mock/products.ts create mode 100644 src/pages/chat/index.config.ts create mode 100644 src/pages/chat/index.less create mode 100644 src/pages/chat/index.tsx create mode 100644 src/pages/product-detail/index.config.ts create mode 100644 src/pages/product-detail/index.less create mode 100644 src/pages/product-detail/index.tsx create mode 100644 src/pages/publish-article/index.config.ts create mode 100644 src/pages/publish-article/index.less create mode 100644 src/pages/publish-article/index.tsx create mode 100644 src/pages/publish-express/index.config.ts create mode 100644 src/pages/publish-express/index.less create mode 100644 src/pages/publish-express/index.tsx create mode 100644 src/pages/publish-secondhand/index.config.ts create mode 100644 src/pages/publish-secondhand/index.less create mode 100644 src/pages/publish-secondhand/index.tsx create mode 100644 src/pages/system-messages/index.config.ts create mode 100644 src/pages/system-messages/index.less create mode 100644 src/pages/system-messages/index.tsx create mode 100644 src/pages/task-detail/index.config.ts create mode 100644 src/pages/task-detail/index.less create mode 100644 src/pages/task-detail/index.tsx create mode 100644 src/types/product.ts diff --git a/src/app.config.ts b/src/app.config.ts index ccfc8c5..14ac5c0 100644 --- a/src/app.config.ts +++ b/src/app.config.ts @@ -2,6 +2,13 @@ export default defineAppConfig({ pages: [ 'pages/index/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/profile/index', 'pages/article/index', diff --git a/src/components/ProductCard/index.less b/src/components/ProductCard/index.less new file mode 100644 index 0000000..cc550ec --- /dev/null +++ b/src/components/ProductCard/index.less @@ -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; + } + } +} diff --git a/src/components/ProductCard/index.tsx b/src/components/ProductCard/index.tsx new file mode 100644 index 0000000..e779501 --- /dev/null +++ b/src/components/ProductCard/index.tsx @@ -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 ( + onClick?.(product)}> + {/* 商品图片 */} + + + {discount && discount < 10 && ( + {discount}折 + )} + + + {/* 商品信息 */} + + {product.title} + + {product.tags && product.tags.length > 0 && ( + + {product.tags.slice(0, 2).map((t) => ( + + {t} + + ))} + + )} + + + ¥{product.price.toFixed(1)} + {product.originalPrice && ( + + ¥{product.originalPrice.toFixed(1)} + + )} + + + + 已售{product.sales} + + + + ) +} diff --git a/src/custom-tab-bar/index.tsx b/src/custom-tab-bar/index.tsx index 940097d..51487ea 100644 --- a/src/custom-tab-bar/index.tsx +++ b/src/custom-tab-bar/index.tsx @@ -71,7 +71,17 @@ export default function CustomTabBar({ activeKey: propKey }: CustomTabBarProps) const handlePublishClose = () => setPublishVisible(false) const handlePublishSelect = (option: PublishOption) => { setPublishVisible(false) - Taro.showToast({ title: `${option.label}即将上线`, icon: 'none' }) + const pageMap: Record = { + 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' }) + } } // 将 4 个 tab 拆分为 [0,1] + 发布按钮 + [2,3] diff --git a/src/mock/conversations.ts b/src/mock/conversations.ts new file mode 100644 index 0000000..5966397 --- /dev/null +++ b/src/mock/conversations.ts @@ -0,0 +1,74 @@ +/** 对话消息 */ +export interface ChatMessage { + id: string + sender: 'me' | 'other' + content: string + time: string +} + +/** 按聊天对象 ID 存储的对话记录 */ +export const conversations: Record = { + // 张同学 + 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' }, + ], +} diff --git a/src/mock/products.ts b/src/mock/products.ts new file mode 100644 index 0000000..abacfec --- /dev/null +++ b/src/mock/products.ts @@ -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: ['帆布', '文艺'], + }, +] diff --git a/src/pages/chat/index.config.ts b/src/pages/chat/index.config.ts new file mode 100644 index 0000000..6c283a9 --- /dev/null +++ b/src/pages/chat/index.config.ts @@ -0,0 +1,4 @@ +export default definePageConfig({ + navigationBarTitleText: '对话', + navigationStyle: 'custom', +}) diff --git a/src/pages/chat/index.less b/src/pages/chat/index.less new file mode 100644 index 0000000..4f0393c --- /dev/null +++ b/src/pages/chat/index.less @@ -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; +} diff --git a/src/pages/chat/index.tsx b/src/pages/chat/index.tsx new file mode 100644 index 0000000..9c4baca --- /dev/null +++ b/src/pages/chat/index.tsx @@ -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 = { + 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([]) + 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 ( + + + + + + + + 未知用户 + + + + + + + 对话不存在 + + + ) + } + + const roleLabel = partner.role ? ROLE_LABEL[partner.role] : '' + + return ( + + {/* ===== 导航栏 ===== */} + + + + + + + {partner.name} + {roleLabel && {roleLabel}} + + + + + + + {/* ===== 消息列表 ===== */} + + + {messages.map((msg) => ( + + {msg.content} + {msg.time} + + ))} + + + + + {/* ===== 底部输入栏 ===== */} + + + setInputValue(String(e.detail.value))} + confirmType='send' + onConfirm={handleSend} + /> + + + 发送 + + + + ) +} diff --git a/src/pages/index/index.less b/src/pages/index/index.less index 5b5d25c..d36569f 100644 --- a/src/pages/index/index.less +++ b/src/pages/index/index.less @@ -80,30 +80,115 @@ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04); } -/* ========== 标签区域 ========== */ -.tabs-section { +/* ========== 统一板块 ========== */ +.main-tabs-section { + margin: 0 24px 20px; background: #fff; border-radius: 16px; 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 { - padding: 0 24px; +/* 商品双列网格 */ +.product-grid { + 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 { - background: #fff; - border-radius: 16px; - padding: 24px; - margin-bottom: 16px; - box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04); + padding: 24px 0; transition: transform 0.2s; + & + .article-item { + border-top: 1px solid #f2f3f5; + } + &:active { transform: scale(0.98); } diff --git a/src/pages/index/index.tsx b/src/pages/index/index.tsx index 3466b8e..5487d5b 100644 --- a/src/pages/index/index.tsx +++ b/src/pages/index/index.tsx @@ -1,10 +1,14 @@ import { useState, useMemo } from 'react' 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 ArticleCard from '@/components/ArticleCard' +import ProductCard from '@/components/ProductCard' import { banners, navItems, articles } from '@/mock/articles' +import { products } from '@/mock/products' 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 './index.less' @@ -21,22 +25,45 @@ function getNavBarHeight() { } } -/** 分类标签映射 */ -const TAB_LIST = [ - { title: '最新', value: 'latest' as ArticleCategory }, - { title: '推荐', value: 'recommend' as ArticleCategory }, - { title: '热点', value: 'hot' as ArticleCategory }, +/** 统一板块标签:校园商城 + 文章分类 */ +const MAIN_TABS = [ + { title: '校园商城', value: 'mall' }, + { title: '最新', value: 'latest' }, + { 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() { - const [activeTab, setActiveTab] = useState(0) + const [activeMainTab, setActiveMainTab] = useState(0) + const [activeCategory, setActiveCategory] = useState(0) const navBarHeight = useMemo(() => getNavBarHeight(), []) - /** 根据当前 tab 筛选文章 */ + /** 当前选中的板块 */ + const currentTab = MAIN_TABS[activeMainTab]?.value || 'mall' + + /** 根据文章分类筛 */ const filteredArticles = useMemo(() => { - const category = TAB_LIST[activeTab]?.value || 'latest' - return articles.filter((a) => a.category === category) - }, [activeTab]) + const value = MAIN_TABS[activeMainTab]?.value || 'latest' + if (value === 'mall') return articles.filter((a) => a.category === 'latest') + 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 = () => { @@ -57,6 +84,21 @@ export default function Index() { 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 ( {/* ========== 自定义导航栏 (毛玻璃) ========== */} @@ -128,36 +170,91 @@ export default function Index() { - {/* ========== 文章标签导航 (最新 / 推荐 / 热点) ========== */} - + {/* ========== 统一板块:校园商城 | 最新 | 推荐 | 热点 ========== */} + setActiveTab(e.detail.index)} + active={activeMainTab} + onChange={(e) => setActiveMainTab(e.detail.index)} sticky offsetTop={navBarHeight.totalHeight} color='#1989fa' titleActiveColor='#1989fa' titleInactiveColor='#646566' + animated + swipeable > - {TAB_LIST.map((tab) => ( + {MAIN_TABS.map((tab) => ( ))} - - {/* ========== 文章列表 ========== */} - - {filteredArticles.map((article) => ( - handleArticleClick(article.id)} - > - + {/* ===== 校园商城内容 ===== */} + {currentTab === 'mall' && ( + + {/* 商品分类标签 */} + + + {PRODUCT_CATEGORIES.map((cat, idx) => ( + setActiveCategory(idx)} + > + {cat.title} + + ))} + + + + {/* 商品双列网格 */} + + {filteredProducts.map((product) => ( + + ))} + {filteredProducts.length === 0 && ( + 暂无商品 + )} + + + {/* 商家入驻入口 */} + + + 🏪 + + 商家入驻 + 拥有自己的店铺,开启校园创业之旅 + + + + + + )} + + {/* ===== 文章列表内容 ===== */} + {currentTab !== 'mall' && ( + + {filteredArticles.map((article) => ( + handleArticleClick(article.id)} + > + + + ))} + {filteredArticles.length === 0 && ( + 暂无文章 + )} - ))} - {filteredArticles.length === 0 && ( - 暂无文章 )} diff --git a/src/pages/messages/index.tsx b/src/pages/messages/index.tsx index 00a1a6f..4c8e8aa 100644 --- a/src/pages/messages/index.tsx +++ b/src/pages/messages/index.tsx @@ -14,7 +14,11 @@ const ROLE_LABEL: Record = { export default function Messages() { 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}` }) + } } // 分组:置顶系统消息 + 普通聊天 diff --git a/src/pages/product-detail/index.config.ts b/src/pages/product-detail/index.config.ts new file mode 100644 index 0000000..146cbc4 --- /dev/null +++ b/src/pages/product-detail/index.config.ts @@ -0,0 +1,4 @@ +export default definePageConfig({ + navigationBarTitleText: '商品详情', + navigationStyle: 'custom', +}) diff --git a/src/pages/product-detail/index.less b/src/pages/product-detail/index.less new file mode 100644 index 0000000..9267066 --- /dev/null +++ b/src/pages/product-detail/index.less @@ -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; +} diff --git a/src/pages/product-detail/index.tsx b/src/pages/product-detail/index.tsx new file mode 100644 index 0000000..4b9f1f4 --- /dev/null +++ b/src/pages/product-detail/index.tsx @@ -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 ( + + + + + + + 商品详情 + + + + + + 商品不存在 + + + ) + } + + const discount = product.originalPrice + ? Math.round((product.price / product.originalPrice) * 10) + : null + + return ( + + {/* ===== 导航栏 ===== */} + + + + + + 商品详情 + + + + + + {/* ===== 滚动内容 ===== */} + + {/* 图片轮播 — 使用商品图片 + 模拟多张 */} + + {[product.image, product.image, product.image].map((img, idx) => ( + + + + ))} + + + {/* 价格信息 */} + + + ¥{product.price.toFixed(2)} + {product.originalPrice && ( + ¥{product.originalPrice.toFixed(2)} + )} + {discount && discount < 10 && ( + {discount}折 + )} + + {product.title} + {product.tags && product.tags.length > 0 && ( + + {product.tags.map((t) => ( + + {t} + + ))} + + )} + 已售 {product.sales} + + + {/* 规格选择 */} + openSku('cart')}> + 已选 + + 颜色: 黑色 / 版本: 标准版 ×{quantity} + + + + + {/* 商家信息 */} + + + + {product.merchant.name} + + + + + {/* 商品描述 */} + + 商品描述 + {product.desc} + + + {/* 推荐商品 */} + + 推荐商品 + + {recommendedProducts.length > 0 ? ( + recommendedProducts.map((p) => ( + handleNavigateToDetail(prod.id)} + /> + )) + ) : ( + 暂无推荐 + )} + + + + + + + {/* ===== 底部操作栏 ===== */} + + Taro.showToast({ title: '联系客服中...', icon: 'none' })} + /> + { + setIsFavorited(!isFavorited) + Taro.showToast({ + title: isFavorited ? '已取消收藏' : '已收藏', + icon: 'none', + }) + }} + /> + Taro.showToast({ title: '购物车即将上线', icon: 'none' })} + /> + openSku('cart')} + /> + openSku('buy')} + /> + + + {/* ===== SKU 选择弹窗 ===== */} + + + {/* 弹窗头部 — 商品概要 */} + + + + + ¥{product.price.toFixed(2)} + + + 库存 999 件 + + + + + {/* SKU 规格选择 */} + + { + if (goods) { + // SKU 变化时更新价格 + } + }} + /> + + + {/* 数量选择 */} + + 数量 + setQuantity(Number(e.detail))} + /> + + + {/* 确认按钮 */} + + + + + + + ) +} diff --git a/src/pages/publish-article/index.config.ts b/src/pages/publish-article/index.config.ts new file mode 100644 index 0000000..17f241d --- /dev/null +++ b/src/pages/publish-article/index.config.ts @@ -0,0 +1,4 @@ +export default definePageConfig({ + navigationBarTitleText: '发布文章', + navigationStyle: 'custom', +}) diff --git a/src/pages/publish-article/index.less b/src/pages/publish-article/index.less new file mode 100644 index 0000000..aa9c9e5 --- /dev/null +++ b/src/pages/publish-article/index.less @@ -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; +} diff --git a/src/pages/publish-article/index.tsx b/src/pages/publish-article/index.tsx new file mode 100644 index 0000000..e77377b --- /dev/null +++ b/src/pages/publish-article/index.tsx @@ -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([]) + + 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 ( + + {/* 导航栏 */} + + + + + + 发布文章 + + + + + + + {/* 基本信息卡片 */} + + 文章内容 + setTitle(String(e.detail.value))} + /> + + setContent(String(e.detail.value))} + /> + + + {/* 封面图片 */} + + 封面图片 + + 支持 jpg/png,最多 3 张 + + + + + + + + {/* 底部提交 */} + + + + + + + ) +} diff --git a/src/pages/publish-express/index.config.ts b/src/pages/publish-express/index.config.ts new file mode 100644 index 0000000..5490502 --- /dev/null +++ b/src/pages/publish-express/index.config.ts @@ -0,0 +1,4 @@ +export default definePageConfig({ + navigationBarTitleText: '快递代取', + navigationStyle: 'custom', +}) diff --git a/src/pages/publish-express/index.less b/src/pages/publish-express/index.less new file mode 100644 index 0000000..33cfa2a --- /dev/null +++ b/src/pages/publish-express/index.less @@ -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; +} diff --git a/src/pages/publish-express/index.tsx b/src/pages/publish-express/index.tsx new file mode 100644 index 0000000..b3665c7 --- /dev/null +++ b/src/pages/publish-express/index.tsx @@ -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 ( + + {/* 导航栏 */} + + + + + + 快递代取 + + + + + + + {/* 基本信息 */} + + 任务信息 + setTitle(String(e.detail.value))} + /> + + setDesc(String(e.detail.value))} + /> + + + {/* 详细信息 */} + + 详细信息 + setLocation(String(e.detail.value))} + /> + + setReward(String(e.detail.value))} + /> + {reward && ( + 报酬金额:¥{Number(reward).toFixed(2)} + )} + + setDeadline(String(e.detail.value))} + /> + + + + + + {/* 底部提交 */} + + + + + + + ) +} diff --git a/src/pages/publish-secondhand/index.config.ts b/src/pages/publish-secondhand/index.config.ts new file mode 100644 index 0000000..ea7a771 --- /dev/null +++ b/src/pages/publish-secondhand/index.config.ts @@ -0,0 +1,4 @@ +export default definePageConfig({ + navigationBarTitleText: '发布二手', + navigationStyle: 'custom', +}) diff --git a/src/pages/publish-secondhand/index.less b/src/pages/publish-secondhand/index.less new file mode 100644 index 0000000..0db7d04 --- /dev/null +++ b/src/pages/publish-secondhand/index.less @@ -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; +} diff --git a/src/pages/publish-secondhand/index.tsx b/src/pages/publish-secondhand/index.tsx new file mode 100644 index 0000000..98422bc --- /dev/null +++ b/src/pages/publish-secondhand/index.tsx @@ -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([]) + 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 ( + + {/* 导航栏 */} + + + + + + 发布二手 + + + + + + + {/* 商品信息 */} + + 商品信息 + setTitle(String(e.detail.value))} + /> + + setDesc(String(e.detail.value))} + /> + + + {/* 商品图片 */} + + 商品图片 + + 支持 jpg/png,最多 6 张,第一张为封面 + + + + + {/* 交易信息 */} + + 交易信息 + setPrice(String(e.detail.value))} + /> + {price && ( + 售价金额:¥{Number(price).toFixed(2)} + )} + + setLocation(String(e.detail.value))} + /> + + setTags(String(e.detail.value))} + /> + {tags && ( + 使用逗号分隔,如:数码,99新 + )} + + + + + + {/* 底部提交 */} + + + + + + + ) +} diff --git a/src/pages/system-messages/index.config.ts b/src/pages/system-messages/index.config.ts new file mode 100644 index 0000000..67edb6e --- /dev/null +++ b/src/pages/system-messages/index.config.ts @@ -0,0 +1,4 @@ +export default definePageConfig({ + navigationBarTitleText: '系统消息', + navigationStyle: 'custom', +}) diff --git a/src/pages/system-messages/index.less b/src/pages/system-messages/index.less new file mode 100644 index 0000000..3193548 --- /dev/null +++ b/src/pages/system-messages/index.less @@ -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; + } +} diff --git a/src/pages/system-messages/index.tsx b/src/pages/system-messages/index.tsx new file mode 100644 index 0000000..93a4337 --- /dev/null +++ b/src/pages/system-messages/index.tsx @@ -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 ( + + {/* ===== 导航栏 ===== */} + + + + + + 系统消息 + + + + + + {/* ===== 消息列表 ===== */} + + + {systemMsgs.length > 0 ? ( + systemMsgs.map((msg) => ( + + + + {msg.title.slice(0, 2)} + + {msg.title} + + {msg.content} + {msg.time} + + )) + ) : ( + + 暂无系统消息 + + )} + + + + ) +} diff --git a/src/pages/task-detail/index.config.ts b/src/pages/task-detail/index.config.ts new file mode 100644 index 0000000..b66081f --- /dev/null +++ b/src/pages/task-detail/index.config.ts @@ -0,0 +1,4 @@ +export default definePageConfig({ + navigationBarTitleText: '任务详情', + navigationStyle: 'custom', +}) diff --git a/src/pages/task-detail/index.less b/src/pages/task-detail/index.less new file mode 100644 index 0000000..f7bcb0d --- /dev/null +++ b/src/pages/task-detail/index.less @@ -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; +} diff --git a/src/pages/task-detail/index.tsx b/src/pages/task-detail/index.tsx new file mode 100644 index 0000000..2961939 --- /dev/null +++ b/src/pages/task-detail/index.tsx @@ -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 = { + express: '快递代取', + secondhand: '二手交易', + help: '求助', + activity: '校园活动', + carpool: '拼车出行', +} + +/** 任务类型 → 徽章CSS类名 */ +const TYPE_BADGE_CLASS: Record = { + express: 'badge-express', + secondhand: 'badge-secondhand', + help: 'badge-help', + activity: 'badge-activity', + carpool: 'badge-carpool', +} + +/** 任务状态 → 中文 */ +const STATUS_LABEL: Record = { + open: '待接单', + progress: '进行中', + done: '已完成', + cancelled: '已取消', +} + +/** 二手交易专用状态文案(与快递/求助不同) */ +const SECONDHAND_STATUS_LABEL: Record = { + open: '待售', + progress: '已预订', + done: '已售出', + cancelled: '已取消', +} + +/** 各类型 Steps 步骤配置 */ +const STATUS_STEPS_MAP: Record = { + 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 = { + 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(() => { + 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 ( + + 任务不存在 + + ) + } + + /* ---- 底部主按钮文案 ---- */ + 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 = () => ( + + + + {TYPE_BADGE_LABEL[task.type]} + + + {getStatusLabel(task)} + + + {task.title} + {task.desc} + {task.reward} + + + 📍 + 取件地点:{task.location} + + {task.deadline && ( + + + 截止时间:{task.deadline} + + )} + + 🕐 + 发布时间:{task.time} + + + ) + + const renderSecondhandInfo = () => ( + + {/* 商品图片 */} + {task.images && task.images.length > 0 && ( + <> + {task.images.length === 1 ? ( + + + + ) : ( + + {task.images.map((img, idx) => ( + + + + ))} + + )} + + )} + + + + {TYPE_BADGE_LABEL[task.type]} + + + {getStatusLabel(task)} + + + {task.title} + {task.desc} + + {task.tags && task.tags.length > 0 && ( + + {task.tags.map((t) => ( + + {t} + + ))} + + )} + + {task.reward} + + + + 📍 + 交易地点:{task.location} + + + 🕐 + 发布时间:{task.time} + + + ) + + const renderHelpInfo = () => ( + + + + {TYPE_BADGE_LABEL[task.type]} + + {task.tags && task.tags.length > 0 && ( + + {task.tags.map((t) => ( + + {t} + + ))} + + )} + + {getStatusLabel(task)} + + + {task.title} + {task.desc} + {task.reward} + + + 📍 + 帮助地点:{task.location} + + + 🕐 + 发布时间:{task.time} + + + ) + + const renderActivityInfo = () => { + const progress = task.maxParticipants + ? Math.round( + ((task.participants || 0) / task.maxParticipants) * 100, + ) + : 0 + + return ( + + {/* 活动图片(可选) */} + {task.images && task.images.length > 0 && ( + <> + {task.images.length === 1 ? ( + + + + ) : ( + + {task.images.map((img, idx) => ( + + + + ))} + + )} + + )} + + + + {TYPE_BADGE_LABEL[task.type]} + + + {getStatusLabel(task)} + + + {task.title} + {task.desc} + + {/* 参与人数 + 进度条 */} + + + + 👥 已报名 {task.participants || 0}/{task.maxParticipants || 0} 人 + + {progress}% + + + + + + + {task.tags && task.tags.length > 0 && ( + + {task.tags.map((t) => ( + + {t} + + ))} + + )} + + + + 📍 + 活动地点:{task.location} + + + 🕐 + 发布时间:{task.time} + + + ) + } + + const renderCarpoolInfo = () => { + const remainingSeats = + task.participants != null && task.maxParticipants != null + ? task.maxParticipants - task.participants + : null + + return ( + + + + {TYPE_BADGE_LABEL[task.type]} + + + {getStatusLabel(task)} + + + {task.title} + {task.desc} + + + + {task.reward} + + {remainingSeats !== null && ( + + 🚗 余{remainingSeats}座 + + )} + + + + + 📍 + 出发地点:{task.location} + + {task.deadline && ( + + + 出发时间:{task.deadline} + + )} + + 🕐 + 发布时间:{task.time} + + + {task.tags && task.tags.length > 0 && ( + + {task.tags.map((t) => ( + + {t} + + ))} + + )} + + ) + } + + 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 ( + + {/* ===== 自定义导航栏 ===== */} + + + + + + 任务详情 + + + + + {/* 导航栏占位 */} + + + {/* ===== 可滚动内容 ===== */} + + {/* 已取消提示条 */} + {task.status === 'cancelled' && ( + + ⚠️ + + 该任务已取消,不再接受新的操作 + + + )} + + {/* Steps 进度条 */} + {stepsConfig.length > 0 && ( + + = 0 ? activeStepIndex : -1 + } + direction='horizontal' + activeColor='#1989fa' + activeIcon='checked' + inactiveIcon='checked' + /> + + )} + + {/* 类型相关信息 */} + {renderTaskInfo()} + + {/* 联系发布者按钮(非已取消状态) */} + {task.status !== 'cancelled' && ( + + + + 联系发布者 + + + )} + + {/* 发布者信息卡 */} + + + + {task.publisher.name} + 发布于 {task.time} + + + + + + + {/* 推荐任务区 */} + + 推荐任务 + {recommendedTasks.length > 0 ? ( + recommendedTasks.map((t) => ( + handleNavigateToDetail(t.id)} + > + + + )) + ) : ( + 暂无推荐任务 + )} + + + {/* 底部安全区占位 */} + + + + {/* ===== 底部操作栏 ===== */} + + + + + + + ) +} diff --git a/src/pages/tasks/index.tsx b/src/pages/tasks/index.tsx index c9c53c9..fe57027 100644 --- a/src/pages/tasks/index.tsx +++ b/src/pages/tasks/index.tsx @@ -46,7 +46,7 @@ export default function Tasks() { }, [activeFilter, searchValue]) const handleTaskClick = (id: string) => { - Taro.showToast({ title: '任务详情即将上线', icon: 'none' }) + Taro.navigateTo({ url: `/pages/task-detail/index?id=${id}` }) } return ( diff --git a/src/types/product.ts b/src/types/product.ts new file mode 100644 index 0000000..843557b --- /dev/null +++ b/src/types/product.ts @@ -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[] +}