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[]
+}