first commit
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"navigationBarTitleText": "确认下单"
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
.checkout-page {
|
||||
padding: 0 24px 24px;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
font-size: 40px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
/* 画布展示区 */
|
||||
.checkout-canvas {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
.canvas-area {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background: rgba(0,0,0,0.03);
|
||||
border: 2px dashed var(--line-star);
|
||||
}
|
||||
|
||||
.checkout-image {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* 商品信息 */
|
||||
.checkout-info {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 20px 0;
|
||||
border-bottom: 1px dashed rgba(0,0,0,0.06);
|
||||
}
|
||||
|
||||
.info-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
font-size: 28px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.info-value {
|
||||
font-size: 28px;
|
||||
color: var(--text-primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.total-row {
|
||||
padding-top: 24px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.info-total {
|
||||
font-size: 40px;
|
||||
font-weight: 800;
|
||||
color: var(--accent-pink);
|
||||
}
|
||||
|
||||
/* 底部操作 */
|
||||
.checkout-actions {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: var(--bg-card);
|
||||
border-top: var(--line-card);
|
||||
padding: 20px 32px calc(20px + env(safe-area-inset-bottom));
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
z-index: 500;
|
||||
}
|
||||
|
||||
.checkout-actions .btn-outline,
|
||||
.checkout-actions .btn-gradient {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
import { View, Text, Image } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { useState, useEffect } from 'react'
|
||||
import './index.scss'
|
||||
import { getDesignList, designToOrder } from '../../utils/store'
|
||||
import ThemeToggle from '../../components/ThemeToggle'
|
||||
import { useThemeContext } from '../../context/ThemeContext'
|
||||
|
||||
export default function CheckoutPage() {
|
||||
const { theme } = useThemeContext()
|
||||
const [design, setDesign] = useState<any>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const params = Taro.getCurrentInstance().router?.params
|
||||
const dId = params?.designId
|
||||
if (!dId) return
|
||||
const list = getDesignList()
|
||||
const d = list.find(x => x.id === dId)
|
||||
setDesign(d || null)
|
||||
}, [])
|
||||
|
||||
if (!design) {
|
||||
return (
|
||||
<View className={`theme-${theme}`}>
|
||||
<View className='checkout-page'>
|
||||
<View className='page-header dashed-card mt-20'>
|
||||
<Text className='page-title'>设计不存在</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const maskStyle: any = { width: 300, height: 420 } // fallback
|
||||
if (design.designData?.category?.mask) {
|
||||
const m = design.designData.category.mask
|
||||
maskStyle.width = m.width
|
||||
maskStyle.height = m.height
|
||||
if (m.shape === 'circle') maskStyle.borderRadius = '50%'
|
||||
else maskStyle.borderRadius = m.borderRadius || 0
|
||||
}
|
||||
|
||||
const handleConfirm = () => {
|
||||
Taro.showModal({
|
||||
title: '确认下单',
|
||||
content: '确认后将从设计清单生成订单',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
designToOrder(design.id)
|
||||
Taro.showToast({ title: '下单成功', icon: 'success' })
|
||||
setTimeout(() => {
|
||||
Taro.switchTab({ url: '/pages/index/index' })
|
||||
}, 1500)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const hasStickers = design.designData?.stickers && design.designData.stickers.length > 0
|
||||
const hasLegacyImage = design.designData?.imageSrc
|
||||
|
||||
return (
|
||||
<View className={`theme-${theme}`}>
|
||||
<View className='checkout-page'>
|
||||
<ThemeToggle />
|
||||
|
||||
<View className='page-header dashed-card mt-20'>
|
||||
<View className='star-badge' />
|
||||
<View className='flex-between' style={{ width: '100%' }}>
|
||||
<Text className='back-btn' onClick={() => Taro.navigateBack()}>←</Text>
|
||||
<Text className='page-title'>设计效果确认</Text>
|
||||
<View style={{ width: '60px' }} />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 效果展示区 */}
|
||||
<View className='checkout-canvas dashed-card mt-20'>
|
||||
<View className='star-badge' />
|
||||
<View className='canvas-area' style={maskStyle}>
|
||||
{hasStickers ? (
|
||||
design.designData.stickers.map((s: any) => (
|
||||
<Image
|
||||
key={s.id}
|
||||
className='checkout-image'
|
||||
src={s.src}
|
||||
style={{
|
||||
transform: `translate(${s.x}px, ${s.y}px) scale(${s.scale || 1})`,
|
||||
width: s.width || 200,
|
||||
height: s.height || 200
|
||||
}}
|
||||
mode='aspectFit'
|
||||
/>
|
||||
))
|
||||
) : hasLegacyImage ? (
|
||||
<Image
|
||||
className='checkout-image'
|
||||
src={design.designData.imageSrc}
|
||||
style={{
|
||||
transform: `translate(${design.designData.imagePos?.x || 0}px, ${design.designData.imagePos?.y || 0}px) scale(${design.designData.imagePos?.scale || 1})`
|
||||
}}
|
||||
mode='aspectFit'
|
||||
/>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 商品信息 */}
|
||||
<View className='checkout-info dashed-card mt-20'>
|
||||
<View className='star-badge' />
|
||||
<View className='info-row'>
|
||||
<Text className='info-label'>商品</Text>
|
||||
<Text className='info-value'>{design.productName}</Text>
|
||||
</View>
|
||||
<View className='info-row'>
|
||||
<Text className='info-label'>数量</Text>
|
||||
<Text className='info-value'>{design.count} 件</Text>
|
||||
</View>
|
||||
<View className='info-row'>
|
||||
<Text className='info-label'>单价</Text>
|
||||
<Text className='info-value'>¥{design.unitPrice}</Text>
|
||||
</View>
|
||||
<View className='info-row total-row'>
|
||||
<Text className='info-label'>总价</Text>
|
||||
<Text className='info-total'>¥{(design.unitPrice * design.count).toFixed(2)}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 底部操作 */}
|
||||
<View className='checkout-actions'>
|
||||
<View className='btn-outline' onClick={() => Taro.navigateBack()}>
|
||||
<Text>返回修改</Text>
|
||||
</View>
|
||||
<View className='btn-gradient' onClick={handleConfirm}>
|
||||
<Text>确认下单</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={{ height: '40px' }} />
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"navigationBarTitleText": "设计清单"
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
.design-page {
|
||||
padding: 0 24px 24px;
|
||||
}
|
||||
|
||||
/* 状态 Tab */
|
||||
.status-tabs {
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.tabs-scroll {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
display: inline-block;
|
||||
padding: 20px 32px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tab-label {
|
||||
font-size: 28px;
|
||||
color: var(--text-secondary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.tab-item.active .tab-label {
|
||||
color: var(--accent-pink);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.tab-underline {
|
||||
position: absolute;
|
||||
bottom: 8px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 40px;
|
||||
height: 4px;
|
||||
background: var(--accent-pink);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* 设计卡片列表 */
|
||||
.design-list {
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
|
||||
.design-card {
|
||||
margin-bottom: 20px;
|
||||
padding: 28px;
|
||||
}
|
||||
|
||||
.design-body {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.design-icon {
|
||||
font-size: 56px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.design-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.design-title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.design-name {
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.design-meta {
|
||||
font-size: 24px;
|
||||
color: var(--text-secondary);
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.design-total {
|
||||
font-size: 26px;
|
||||
font-weight: 600;
|
||||
color: var(--accent-pink);
|
||||
}
|
||||
|
||||
.design-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.design-btn {
|
||||
padding: 16px 40px;
|
||||
font-size: 26px;
|
||||
}
|
||||
|
||||
/* 空状态 */
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 100px 40px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 80px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.empty-sub {
|
||||
font-size: 26px;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
import { View, Text, ScrollView } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import './index.scss'
|
||||
import { getDesignList, getTheme, type DesignItem } from '../../utils/store'
|
||||
import ThemeToggle from '../../components/ThemeToggle'
|
||||
import { useThemeContext } from '../../context/ThemeContext'
|
||||
|
||||
const STATUS_TABS = [
|
||||
{ code: 'all', label: '全部' },
|
||||
{ code: 'undesigned', label: '未设计' },
|
||||
{ code: 'designing', label: '设计中' },
|
||||
{ code: 'ordered', label: '已下单' }
|
||||
]
|
||||
|
||||
const STATUS_STYLE: Record<string, { label: string; cls: string }> = {
|
||||
undesigned: { label: '未设计', cls: 'badge-pink' },
|
||||
designing: { label: '设计中', cls: 'badge-blue' },
|
||||
ordered: { label: '已下单', cls: 'badge-green' }
|
||||
}
|
||||
|
||||
export default function DesignListPage() {
|
||||
const { theme } = useThemeContext()
|
||||
const [activeTab, setActiveTab] = useState('all')
|
||||
const [list, setList] = useState<DesignItem[]>([])
|
||||
|
||||
const load = useCallback(() => {
|
||||
setList(getDesignList())
|
||||
}, [])
|
||||
|
||||
// 页面每次显示时刷新(保证加入清单后切回能看到最新数据)
|
||||
useEffect(() => {
|
||||
const onShow = () => load()
|
||||
const page = Taro.getCurrentInstance().page
|
||||
if (page) {
|
||||
const orig = page.onShow
|
||||
page.onShow = function () {
|
||||
onShow()
|
||||
if (orig) orig.apply(this)
|
||||
}
|
||||
}
|
||||
load()
|
||||
}, [load])
|
||||
|
||||
const filtered = activeTab === 'all'
|
||||
? list
|
||||
: list.filter(d => d.status === activeTab)
|
||||
|
||||
const goDesign = (item: DesignItem) => {
|
||||
if (item.status === 'ordered') {
|
||||
Taro.showToast({ title: '该商品已下单,请查看订单', icon: 'none' })
|
||||
return
|
||||
}
|
||||
Taro.navigateTo({
|
||||
url: `/pages/diy/index?source=designList&designId=${item.id}`
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<View className={`theme-${theme}`}>
|
||||
<View className='design-page'>
|
||||
<ThemeToggle />
|
||||
|
||||
<View className='page-header dashed-card mt-20'>
|
||||
<View className='star-badge' />
|
||||
<Text className='page-title'>我的设计清单</Text>
|
||||
</View>
|
||||
|
||||
{/* 状态筛选 */}
|
||||
<View className='status-tabs'>
|
||||
<ScrollView className='tabs-scroll' scrollX>
|
||||
{STATUS_TABS.map(tab => (
|
||||
<View
|
||||
key={tab.code}
|
||||
className={`tab-item ${activeTab === tab.code ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab(tab.code)}
|
||||
>
|
||||
<Text className='tab-label'>{tab.label}</Text>
|
||||
{activeTab === tab.code && <View className='tab-underline' />}
|
||||
</View>
|
||||
))}
|
||||
</ScrollView>
|
||||
</View>
|
||||
|
||||
{/* 列表 */}
|
||||
<ScrollView className='design-list' scrollY>
|
||||
{filtered.map(item => {
|
||||
const style = STATUS_STYLE[item.status]
|
||||
return (
|
||||
<View key={item.id} className='design-card dashed-card'>
|
||||
<View className='star-badge' />
|
||||
<View className='design-body'>
|
||||
<Text className='design-icon'>{item.productIcon}</Text>
|
||||
<View className='design-info'>
|
||||
<View className='design-title-row'>
|
||||
<Text className='design-name'>{item.productName}</Text>
|
||||
<Text className={`badge ${style.cls}`}>{style.label}</Text>
|
||||
</View>
|
||||
<Text className='design-meta'>数量: {item.count} 件 | 单价: ¥{item.unitPrice}</Text>
|
||||
<Text className='design-total'>小计: ¥{(item.unitPrice * item.count).toFixed(2)}</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View className='design-actions'>
|
||||
{item.status !== 'ordered' && (
|
||||
<View className='btn-gradient design-btn' onClick={() => goDesign(item)}>
|
||||
<Text>{item.status === 'undesigned' ? '开始设计' : '继续设计'}</Text>
|
||||
</View>
|
||||
)}
|
||||
{item.status === 'ordered' && (
|
||||
<View className='btn-outline design-btn' onClick={() => Taro.switchTab({ url: '/pages/orders/index' })}>
|
||||
<Text>查看订单</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
})}
|
||||
|
||||
{filtered.length === 0 && (
|
||||
<View className='empty-state'>
|
||||
<Text className='empty-icon'>📐</Text>
|
||||
<Text className='empty-text'>还没有设计清单</Text>
|
||||
<Text className='empty-sub'>去首页逛逛,找到喜欢的定制品</Text>
|
||||
<View className='btn-gradient' onClick={() => Taro.switchTab({ url: '/pages/index/index' })}>
|
||||
<Text>去逛逛</Text>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</ScrollView>
|
||||
|
||||
<View style={{ height: '40px' }} />
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"navigationBarTitleText": "DIY工作台",
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
.diy-page {
|
||||
padding: 0 24px 24px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* 画布区域 */
|
||||
.canvas-wrapper {
|
||||
padding: 40px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.canvas-area {
|
||||
position: relative;
|
||||
background: linear-gradient(135deg, rgba(255, 154, 158, 0.08) 0%, rgba(160, 196, 255, 0.08) 100%);
|
||||
overflow: hidden;
|
||||
border-radius: 16px;
|
||||
min-width: 200px;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.mask-border {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
border: 3px dashed var(--accent-blue);
|
||||
pointer-events: none;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.canvas-image {
|
||||
position: absolute;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
z-index: 5;
|
||||
border: 2px solid transparent;
|
||||
}
|
||||
|
||||
.canvas-image.active {
|
||||
border-color: var(--accent-pink);
|
||||
}
|
||||
|
||||
.canvas-image.overlap {
|
||||
border-color: #ff4757;
|
||||
box-shadow: 0 0 12px rgba(255, 71, 87, 0.5);
|
||||
}
|
||||
|
||||
/* 贴纸面板 */
|
||||
.sticker-panel {
|
||||
padding: 28px;
|
||||
}
|
||||
|
||||
.sticker-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.sticker-thumb {
|
||||
position: relative;
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
border: 2px dashed var(--line-star);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
background: var(--bg-input);
|
||||
}
|
||||
|
||||
.sticker-thumb.active {
|
||||
border-color: var(--accent-pink);
|
||||
border-width: 3px;
|
||||
}
|
||||
|
||||
.sticker-thumb.overlap {
|
||||
border-color: #ff4757;
|
||||
}
|
||||
|
||||
.sticker-thumb-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.sticker-del {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
background: rgba(255, 71, 87, 0.85);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.del-icon {
|
||||
color: #fff;
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.sticker-add-btn {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
border: 2px dashed var(--line-star);
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--bg-input);
|
||||
}
|
||||
|
||||
.add-icon {
|
||||
font-size: 48px;
|
||||
color: var(--accent-pink);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.add-label {
|
||||
font-size: 22px;
|
||||
color: var(--text-secondary);
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.overlap-hint {
|
||||
display: block;
|
||||
margin-top: 16px;
|
||||
font-size: 26px;
|
||||
color: #ff4757;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 工具栏 */
|
||||
.toolbar {
|
||||
padding: 28px 32px;
|
||||
}
|
||||
|
||||
.tool-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.tool-row:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.tool-label {
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.tool-btn {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
background: linear-gradient(135deg, rgba(255, 154, 158, 0.08) 0%, rgba(160, 196, 255, 0.08) 100%);
|
||||
border: 2px dashed var(--accent-blue);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 32px;
|
||||
color: var(--text-primary);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.tool-value {
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
color: var(--accent-pink);
|
||||
min-width: 80px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.tool-hint {
|
||||
font-size: 24px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* 预览弹窗 */
|
||||
.preview-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
.preview-card {
|
||||
background: var(--bg-card);
|
||||
width: 100%;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
.preview-title {
|
||||
font-size: 36px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
display: block;
|
||||
margin-bottom: 24px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.preview-canvas {
|
||||
background: linear-gradient(135deg, rgba(255, 154, 158, 0.08) 0%, rgba(160, 196, 255, 0.08) 100%);
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
margin: 0 auto 24px;
|
||||
border: 3px dashed var(--accent-blue);
|
||||
position: relative;
|
||||
min-width: 200px;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.preview-image {
|
||||
position: absolute;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.preview-image.overlap {
|
||||
border: 2px solid #ff4757;
|
||||
}
|
||||
|
||||
.product-info {
|
||||
text-align: center;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.product-name {
|
||||
font-size: 30px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.product-size {
|
||||
font-size: 24px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.preview-actions {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.preview-actions .btn-outline,
|
||||
.preview-actions .btn-gradient {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.preview-actions .btn-gradient.disabled {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* 按钮 */
|
||||
.action-btns {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.mt-20 {
|
||||
margin-top: 20px;
|
||||
}
|
||||
@@ -0,0 +1,392 @@
|
||||
import { View, Text, Image } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { useState, useEffect } from 'react'
|
||||
import './index.scss'
|
||||
import { getProductById } from '../../utils/productConfig'
|
||||
import { getDesignList, setDesignList, updateDesign, type DesignItem, type StickerItem } from '../../utils/store'
|
||||
import ThemeToggle from '../../components/ThemeToggle'
|
||||
import { useThemeContext } from '../../context/ThemeContext'
|
||||
|
||||
export default function DIYPage() {
|
||||
const { theme } = useThemeContext()
|
||||
const [category, setCategory] = useState<any>(null)
|
||||
const [step, setStep] = useState(1)
|
||||
const [designId, setDesignId] = useState('')
|
||||
const [quantity, setQuantity] = useState(1)
|
||||
const [stickers, setStickers] = useState<StickerItem[]>([])
|
||||
const [activeStickerId, setActiveStickerId] = useState<string | null>(null)
|
||||
const [isDragging, setIsDragging] = useState(false)
|
||||
const [startPos, setStartPos] = useState({ x: 0, y: 0 })
|
||||
const [previewMode, setPreviewMode] = useState(false)
|
||||
const [hasOverlap, setHasOverlap] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const params = Taro.getCurrentInstance().router?.params
|
||||
const source = params?.source
|
||||
const productId = params?.productId || params?.category
|
||||
|
||||
if (source === 'product' && productId) {
|
||||
const product = getProductById(productId)
|
||||
if (product) {
|
||||
setCategory(product)
|
||||
setQuantity(Number(params?.quantity) || 1)
|
||||
const newDesign: DesignItem = {
|
||||
id: 'DSG' + Date.now(),
|
||||
productId: product.id,
|
||||
productName: product.name,
|
||||
productIcon: product.icon,
|
||||
unitPrice: product.price,
|
||||
count: Number(params?.quantity) || 1,
|
||||
status: 'designing',
|
||||
createdAt: new Date().toISOString().slice(0, 10)
|
||||
}
|
||||
const list = getDesignList()
|
||||
setDesignList([...list, newDesign])
|
||||
setDesignId(newDesign.id)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (source === 'designList' && params?.designId) {
|
||||
const dId = params.designId
|
||||
const list = getDesignList()
|
||||
const design = list.find(d => d.id === dId)
|
||||
if (design) {
|
||||
setDesignId(dId)
|
||||
setQuantity(design.count)
|
||||
const product = getProductById(design.productId)
|
||||
if (product) setCategory(product)
|
||||
if (design.designData?.stickers) {
|
||||
setStickers(design.designData.stickers)
|
||||
} else if (design.designData?.imageSrc) {
|
||||
// 兼容旧版数据
|
||||
const s: StickerItem = {
|
||||
id: 'legacy_' + Date.now(),
|
||||
src: design.designData.imageSrc,
|
||||
x: design.designData.imagePos?.x || 0,
|
||||
y: design.designData.imagePos?.y || 0,
|
||||
scale: design.designData.imagePos?.scale || 1,
|
||||
width: 200,
|
||||
height: 200,
|
||||
isOverlapping: false
|
||||
}
|
||||
setStickers([s])
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 默认情况(从首页 old category 参数兼容)
|
||||
if (productId) {
|
||||
const found = getProductById(productId)
|
||||
if (found) {
|
||||
setCategory(found)
|
||||
const newDesign: DesignItem = {
|
||||
id: 'DSG' + Date.now(),
|
||||
productId: found.id,
|
||||
productName: found.name,
|
||||
productIcon: found.icon,
|
||||
unitPrice: found.price,
|
||||
count: 1,
|
||||
status: 'designing',
|
||||
createdAt: new Date().toISOString().slice(0, 10)
|
||||
}
|
||||
const list = getDesignList()
|
||||
setDesignList([...list, newDesign])
|
||||
setDesignId(newDesign.id)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
// 矩形碰撞检测
|
||||
const checkOverlap = (list: StickerItem[]) => {
|
||||
const newList = list.map(s => ({ ...s, isOverlapping: false }))
|
||||
for (let i = 0; i < newList.length; i++) {
|
||||
for (let j = i + 1; j < newList.length; j++) {
|
||||
const a = newList[i]
|
||||
const b = newList[j]
|
||||
const aw = a.width * a.scale
|
||||
const ah = a.height * a.scale
|
||||
const bw = b.width * b.scale
|
||||
const bh = b.height * b.scale
|
||||
if (
|
||||
a.x < b.x + bw &&
|
||||
a.x + aw > b.x &&
|
||||
a.y < b.y + bh &&
|
||||
a.y + ah > b.y
|
||||
) {
|
||||
newList[i].isOverlapping = true
|
||||
newList[j].isOverlapping = true
|
||||
}
|
||||
}
|
||||
}
|
||||
const overlapAny = newList.some(s => s.isOverlapping)
|
||||
setHasOverlap(overlapAny)
|
||||
return newList
|
||||
}
|
||||
|
||||
const addSticker = () => {
|
||||
Taro.chooseImage({
|
||||
count: 1,
|
||||
sizeType: ['compressed'],
|
||||
sourceType: ['album', 'camera'],
|
||||
success: (res) => {
|
||||
const src = res.tempFilePaths[0]
|
||||
// 获取图片尺寸用于碰撞检测
|
||||
Taro.getImageInfo({
|
||||
src,
|
||||
success: (info) => {
|
||||
const newSticker: StickerItem = {
|
||||
id: 'stk_' + Date.now(),
|
||||
src,
|
||||
x: 0,
|
||||
y: 0,
|
||||
scale: 1,
|
||||
width: info.width,
|
||||
height: info.height,
|
||||
isOverlapping: false
|
||||
}
|
||||
const next = [...stickers, newSticker]
|
||||
const checked = checkOverlap(next)
|
||||
setStickers(checked)
|
||||
setActiveStickerId(newSticker.id)
|
||||
},
|
||||
fail: () => {
|
||||
// fallback 尺寸
|
||||
const newSticker: StickerItem = {
|
||||
id: 'stk_' + Date.now(),
|
||||
src,
|
||||
x: 0,
|
||||
y: 0,
|
||||
scale: 1,
|
||||
width: 200,
|
||||
height: 200,
|
||||
isOverlapping: false
|
||||
}
|
||||
const next = [...stickers, newSticker]
|
||||
const checked = checkOverlap(next)
|
||||
setStickers(checked)
|
||||
setActiveStickerId(newSticker.id)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const deleteSticker = (id: string) => {
|
||||
const next = stickers.filter(s => s.id !== id)
|
||||
const checked = checkOverlap(next)
|
||||
setStickers(checked)
|
||||
if (activeStickerId === id) setActiveStickerId(null)
|
||||
}
|
||||
|
||||
const handleStickerTouchStart = (e: any, id: string) => {
|
||||
const touch = e.touches[0]
|
||||
const s = stickers.find(x => x.id === id)
|
||||
if (!s) return
|
||||
setActiveStickerId(id)
|
||||
setIsDragging(true)
|
||||
setStartPos({ x: touch.clientX - s.x, y: touch.clientY - s.y })
|
||||
}
|
||||
|
||||
const handleStickerTouchMove = (e: any) => {
|
||||
if (!isDragging || !activeStickerId) return
|
||||
const touch = e.touches[0]
|
||||
const next = stickers.map(s => {
|
||||
if (s.id !== activeStickerId) return s
|
||||
return { ...s, x: touch.clientX - startPos.x, y: touch.clientY - startPos.y }
|
||||
})
|
||||
const checked = checkOverlap(next)
|
||||
setStickers(checked)
|
||||
}
|
||||
|
||||
const handleTouchEnd = () => setIsDragging(false)
|
||||
|
||||
const handleScale = (id: string, delta: number) => {
|
||||
const next = stickers.map(s => {
|
||||
if (s.id !== id) return s
|
||||
return { ...s, scale: Math.max(0.3, Math.min(3, s.scale + delta)) }
|
||||
})
|
||||
const checked = checkOverlap(next)
|
||||
setStickers(checked)
|
||||
}
|
||||
|
||||
const goBack = () => {
|
||||
Taro.navigateBack()
|
||||
}
|
||||
|
||||
const getMaskStyle = () => {
|
||||
if (!category) return { width: 300, height: 420 }
|
||||
const base: any = { width: category.mask.width, height: category.mask.height }
|
||||
if (category.mask.shape === 'rect') {
|
||||
base.borderRadius = category.mask.borderRadius || 0
|
||||
}
|
||||
if (category.mask.shape === 'circle') {
|
||||
base.borderRadius = '50%'
|
||||
}
|
||||
return base
|
||||
}
|
||||
|
||||
const handleComplete = () => {
|
||||
if (hasOverlap) {
|
||||
Taro.showToast({ title: '贴纸不能重叠', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (designId) {
|
||||
updateDesign(designId, {
|
||||
designData: {
|
||||
stickers,
|
||||
category
|
||||
}
|
||||
})
|
||||
}
|
||||
setPreviewMode(false)
|
||||
Taro.navigateTo({ url: `/pages/checkout/index?designId=${designId}` })
|
||||
}
|
||||
|
||||
if (!category) {
|
||||
return (
|
||||
<View className={`theme-${theme}`}>
|
||||
<View className='diy-page'>
|
||||
<Text className='page-title'>加载中...</Text>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<View className={`theme-${theme}`}>
|
||||
<View className='diy-page'>
|
||||
<ThemeToggle />
|
||||
|
||||
<View className='page-header dashed-card mt-20'>
|
||||
<View className='star-badge' />
|
||||
<View className='flex-between' style={{ width: '100%' }}>
|
||||
<Text className='back-btn' onClick={goBack}>←</Text>
|
||||
<Text className='page-title'>设计工作台</Text>
|
||||
<View style={{ width: '60px' }} />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 画布区域 */}
|
||||
<View className='canvas-wrapper dashed-card mt-20'>
|
||||
<View className='star-badge' />
|
||||
<View className='canvas-area' style={getMaskStyle()}>
|
||||
{stickers.map(s => (
|
||||
<Image
|
||||
key={s.id}
|
||||
className={`canvas-image ${s.isOverlapping ? 'overlap' : ''} ${activeStickerId === s.id ? 'active' : ''}`}
|
||||
src={s.src}
|
||||
style={{
|
||||
transform: `translate(${s.x}px, ${s.y}px) scale(${s.scale})`,
|
||||
opacity: isDragging && activeStickerId === s.id ? 0.8 : 1,
|
||||
zIndex: activeStickerId === s.id ? 10 : 5,
|
||||
width: s.width || 200,
|
||||
height: s.height || 200
|
||||
}}
|
||||
onTouchStart={(e) => handleStickerTouchStart(e, s.id)}
|
||||
onTouchMove={handleStickerTouchMove}
|
||||
onTouchEnd={handleTouchEnd}
|
||||
onClick={() => setActiveStickerId(s.id)}
|
||||
mode='aspectFit'
|
||||
/>
|
||||
))}
|
||||
<View className='mask-border' style={getMaskStyle()} />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 贴纸列表与控制 */}
|
||||
<View className='sticker-panel dashed-card mt-20'>
|
||||
<View className='star-badge' />
|
||||
<Text className='section-title'>贴纸</Text>
|
||||
<View className='sticker-list'>
|
||||
{stickers.map(s => (
|
||||
<View key={s.id} className={`sticker-thumb ${s.isOverlapping ? 'overlap' : ''} ${activeStickerId === s.id ? 'active' : ''}`}>
|
||||
<Image src={s.src} className='sticker-thumb-img' mode='aspectFit' onClick={() => setActiveStickerId(s.id)} />
|
||||
<View className='sticker-del' onClick={() => deleteSticker(s.id)}>
|
||||
<Text className='del-icon'>×</Text>
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
<View className='sticker-add-btn' onClick={addSticker}>
|
||||
<Text className='add-icon'>+</Text>
|
||||
<Text className='add-label'>添加</Text>
|
||||
</View>
|
||||
</View>
|
||||
{hasOverlap && <Text className='overlap-hint'>⚠️ 有贴纸重叠,请调整位置</Text>}
|
||||
</View>
|
||||
|
||||
{/* 控制工具栏 */}
|
||||
{activeStickerId && (
|
||||
<View className='toolbar dashed-card mt-20'>
|
||||
<View className='star-badge' />
|
||||
<View className='tool-row'>
|
||||
<Text className='tool-label'>缩放</Text>
|
||||
<View className='flex-center' style={{ gap: '20px' }}>
|
||||
<View className='tool-btn' onClick={() => handleScale(activeStickerId, -0.1)}>-</View>
|
||||
<Text className='tool-value'>
|
||||
{Math.round((stickers.find(s => s.id === activeStickerId)?.scale || 1) * 100)}%
|
||||
</Text>
|
||||
<View className='tool-btn' onClick={() => handleScale(activeStickerId, 0.1)}>+</View>
|
||||
</View>
|
||||
</View>
|
||||
<View className='tool-row'>
|
||||
<Text className='tool-label'>提示</Text>
|
||||
<Text className='tool-hint'>拖动调整位置,贴纸不可重叠</Text>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 底部操作按钮 */}
|
||||
<View className='action-btns mt-20'>
|
||||
<View className='btn-gradient' onClick={() => setPreviewMode(true)}>
|
||||
<Text>预览效果</Text>
|
||||
</View>
|
||||
<View className='btn-outline' onClick={addSticker}>
|
||||
<Text>添加贴纸</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 预览弹层 */}
|
||||
{previewMode && (
|
||||
<View className='preview-overlay'>
|
||||
<View className='preview-card dashed-card'>
|
||||
<View className='star-badge' />
|
||||
<Text className='preview-title'>确认效果</Text>
|
||||
<View className='preview-canvas' style={getMaskStyle()}>
|
||||
{stickers.map(s => (
|
||||
<Image
|
||||
key={s.id}
|
||||
className={`preview-image ${s.isOverlapping ? 'overlap' : ''}`}
|
||||
src={s.src}
|
||||
style={{
|
||||
transform: `translate(${s.x}px, ${s.y}px) scale(${s.scale})`,
|
||||
width: s.width || 200,
|
||||
height: s.height || 200
|
||||
}}
|
||||
mode='aspectFit'
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
<View className='product-info'>
|
||||
<Text className='product-name'>{category.name}</Text>
|
||||
<Text className='product-size'>画布尺寸: {category.mask.width} × {category.mask.height} px</Text>
|
||||
{hasOverlap && <Text className='overlap-hint'>贴纸有重叠,不可提交</Text>}
|
||||
</View>
|
||||
<View className='preview-actions'>
|
||||
<View className='btn-outline' onClick={() => setPreviewMode(false)}>
|
||||
<Text>返回修改</Text>
|
||||
</View>
|
||||
<View className={`btn-gradient ${hasOverlap ? 'disabled' : ''}`} onClick={handleComplete}>
|
||||
<Text>确认完成</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View style={{ height: '40px' }} />
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"navigationBarTitleText": "智绘微刻",
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
.index-page {
|
||||
padding: 0 24px 24px;
|
||||
}
|
||||
|
||||
/* 顶部标题栏 */
|
||||
.header-bar {
|
||||
padding: 20px 0 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-size: 48px;
|
||||
font-weight: 800;
|
||||
color: #5c3a3a;
|
||||
letter-spacing: 4px;
|
||||
}
|
||||
|
||||
/* 搜索框卡片 */
|
||||
.search-card {
|
||||
padding: 20px 24px;
|
||||
}
|
||||
|
||||
.search-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: #f8f9fa;
|
||||
border-radius: 40px;
|
||||
padding: 16px 24px;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
font-size: 28px;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
flex: 1;
|
||||
font-size: 28px;
|
||||
color: #5c3a3a;
|
||||
background: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.search-input::placeholder {
|
||||
color: #b08d8d;
|
||||
font-size: 26px;
|
||||
}
|
||||
|
||||
.search-result-hint {
|
||||
margin-top: 16px;
|
||||
padding-top: 16px;
|
||||
border-top: 2px dashed #eee;
|
||||
}
|
||||
|
||||
.hint-text {
|
||||
font-size: 24px;
|
||||
color: #b08d8d;
|
||||
}
|
||||
|
||||
/* 成品展示轮播 */
|
||||
.showcase-section {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.showcase-swiper {
|
||||
height: 460rpx;
|
||||
}
|
||||
|
||||
.showcase-swiper-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 10rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.showcase-swiper-card {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.showcase-image-wrapper {
|
||||
width: 100%;
|
||||
height: 240rpx;
|
||||
border-radius: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.showcase-emoji {
|
||||
font-size: 80px;
|
||||
}
|
||||
|
||||
.showcase-info {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.showcase-info .showcase-title {
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
color: #5c3a3a;
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.showcase-info .showcase-desc {
|
||||
font-size: 24px;
|
||||
color: #b08d8d;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.showcase-tag {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
background: rgba(255, 154, 158, 0.15);
|
||||
color: #ff6b81;
|
||||
font-size: 20px;
|
||||
padding: 4px 16px;
|
||||
border-radius: 20px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 热门品类 */
|
||||
.category-section {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.category-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.category-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
padding: 32px 20px;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
.category-item:active {
|
||||
transform: scale(0.96);
|
||||
}
|
||||
|
||||
.category-icon {
|
||||
font-size: 56px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.category-icon-img {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
object-fit: contain;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.category-name {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: #5c3a3a;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.category-desc {
|
||||
font-size: 22px;
|
||||
color: #b08d8d;
|
||||
}
|
||||
|
||||
/* 空状态 */
|
||||
.empty-category {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 60px 40px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 64px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 30px;
|
||||
font-weight: 600;
|
||||
color: #5c3a3a;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.empty-sub {
|
||||
font-size: 24px;
|
||||
color: #b08d8d;
|
||||
}
|
||||
|
||||
/* 底部安全区 */
|
||||
.safe-bottom-placeholder {
|
||||
height: 160px;
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
import { View, Text, Swiper, SwiperItem, Input, Image } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { useState } from 'react'
|
||||
import './index.scss'
|
||||
import { CATEGORIES } from '../../utils/productConfig'
|
||||
import ThemeToggle from '../../components/ThemeToggle'
|
||||
import { useThemeContext } from '../../context/ThemeContext'
|
||||
|
||||
// 成品展示数据
|
||||
const SHOWCASES = [
|
||||
{ title: '毕业纪念笔记本', desc: '全班名字组成校徽', color: '#FFE4EC' },
|
||||
{ title: '铜质杯垫', desc: '金属质感桌面艺术', color: '#E8D5C4' },
|
||||
{ title: '竹制笔盒', desc: '自然竹纹文房雅器', color: '#E0F0D9' },
|
||||
{ title: '书本型灯', desc: '温暖光影点亮心意', color: '#FFF3CD' },
|
||||
{ title: '情侣定制礼', desc: '两个人的名字交织', color: '#FCE4EC' },
|
||||
{ title: '企业年会礼', desc: '员工名字组成Logo', color: '#E3F2FD' }
|
||||
]
|
||||
|
||||
export default function Index() {
|
||||
const { theme } = useThemeContext()
|
||||
const [searchKey, setSearchKey] = useState('')
|
||||
|
||||
const navigateToProduct = (categoryId: string) => {
|
||||
Taro.navigateTo({ url: `/pages/product/index?id=${categoryId}` })
|
||||
}
|
||||
|
||||
const filteredCategories = searchKey.trim()
|
||||
? CATEGORIES.filter(
|
||||
(c) => c.name.includes(searchKey) || c.desc.includes(searchKey)
|
||||
)
|
||||
: CATEGORIES
|
||||
|
||||
const handleSearch = (e: any) => {
|
||||
setSearchKey(e.detail.value)
|
||||
}
|
||||
|
||||
return (
|
||||
<View className={`theme-${theme}`}>
|
||||
<View className='index-page'>
|
||||
<ThemeToggle />
|
||||
|
||||
{/* 顶部标题栏 */}
|
||||
<View className='header-bar'>
|
||||
<Text className='header-title'>智绘微刻</Text>
|
||||
</View>
|
||||
|
||||
{/* 搜索框 */}
|
||||
<View className='search-card dashed-card'>
|
||||
<View className='star-badge' />
|
||||
<View className='search-inner'>
|
||||
<Text className='search-icon'>🔍</Text>
|
||||
<Input
|
||||
className='search-input'
|
||||
type='text'
|
||||
placeholder='搜索定制品类:笔记本、杯垫、书灯...'
|
||||
value={searchKey}
|
||||
onInput={handleSearch}
|
||||
confirmType='search'
|
||||
/>
|
||||
</View>
|
||||
{searchKey.trim() && (
|
||||
<View className='search-result-hint'>
|
||||
<Text className='hint-text'>
|
||||
{filteredCategories.length > 0
|
||||
? `找到 ${filteredCategories.length} 个相关品类`
|
||||
: '没有找到相关品类,试试看其他关键词'}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* 成品展示轮播 */}
|
||||
<View className='showcase-section mt-20'>
|
||||
<Text className='section-title'>定制成品展示</Text>
|
||||
<Swiper
|
||||
className='showcase-swiper'
|
||||
indicatorColor='#e0e0e0'
|
||||
indicatorActiveColor='#ff9a9e'
|
||||
circular
|
||||
autoplay
|
||||
interval={3000}
|
||||
duration={500}
|
||||
previousMargin='40rpx'
|
||||
nextMargin='40rpx'
|
||||
indicatorDots
|
||||
>
|
||||
{SHOWCASES.map((item, idx) => (
|
||||
<SwiperItem key={idx} className='showcase-swiper-item'>
|
||||
<View className='showcase-swiper-card dashed-card'>
|
||||
<View className='star-badge' />
|
||||
<View
|
||||
className='showcase-image-wrapper'
|
||||
style={{ background: item.color }}
|
||||
>
|
||||
<Text className='showcase-emoji'>🎁</Text>
|
||||
</View>
|
||||
<View className='showcase-info'>
|
||||
<Text className='showcase-title'>{item.title}</Text>
|
||||
<Text className='showcase-desc'>{item.desc}</Text>
|
||||
</View>
|
||||
<View className='showcase-tag'>成品案例</View>
|
||||
</View>
|
||||
</SwiperItem>
|
||||
))}
|
||||
</Swiper>
|
||||
</View>
|
||||
|
||||
{/* 热门品类 */}
|
||||
<View className='category-section mt-20'>
|
||||
<Text className='section-title'>热门品类</Text>
|
||||
<View className='category-grid'>
|
||||
{filteredCategories.map((cat) => (
|
||||
<View
|
||||
key={cat.id}
|
||||
className='category-item dashed-card'
|
||||
onClick={() => navigateToProduct(cat.id)}
|
||||
>
|
||||
<View className='star-badge' />
|
||||
{cat.images && cat.images.length > 0 ? (
|
||||
<Image className='category-icon-img' src={cat.images[0]} mode='aspectFit' />
|
||||
) : (
|
||||
<Text className='category-icon'>{cat.icon}</Text>
|
||||
)}
|
||||
<Text className='category-name'>{cat.name}</Text>
|
||||
<Text className='category-desc'>{cat.desc}</Text>
|
||||
<Text className='category-price'>¥{cat.price} 起</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
{filteredCategories.length === 0 && (
|
||||
<View className='empty-category dashed-card'>
|
||||
<Text className='empty-icon'>🔍</Text>
|
||||
<Text className='empty-text'>未找到匹配的品类</Text>
|
||||
<Text className='empty-sub'>尝试搜索“笔记本”、“杯垫”等</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View className='safe-bottom-placeholder' />
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '订单详情'
|
||||
})
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -0,0 +1,241 @@
|
||||
.order-detail-page {
|
||||
padding: 0 24px 24px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* 返回按钮 */
|
||||
.back-btn {
|
||||
font-size: 40px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
/* 状态卡片 */
|
||||
.status-card {
|
||||
padding: 32px;
|
||||
}
|
||||
|
||||
.status-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
font-size: 26px;
|
||||
}
|
||||
|
||||
.status-desc {
|
||||
font-size: 26px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* 物流时间轴 */
|
||||
.logistics-timeline {
|
||||
position: relative;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.logistics-timeline::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 8px;
|
||||
top: 8px;
|
||||
bottom: 8px;
|
||||
width: 2px;
|
||||
background: var(--line-star);
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.timeline-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 24px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.timeline-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.timeline-dot {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 50%;
|
||||
background: var(--bg-card);
|
||||
border: 3px solid var(--line-star);
|
||||
margin-right: 20px;
|
||||
flex-shrink: 0;
|
||||
margin-left: -20px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.timeline-item.active .timeline-dot {
|
||||
background: var(--accent-pink);
|
||||
border-color: var(--accent-pink);
|
||||
}
|
||||
|
||||
.timeline-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.timeline-title {
|
||||
font-size: 28px;
|
||||
color: var(--text-primary);
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.timeline-time {
|
||||
font-size: 24px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* 地址卡片 */
|
||||
.address-card {
|
||||
padding: 32px;
|
||||
}
|
||||
|
||||
.address-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.address-icon {
|
||||
font-size: 32px;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.address-title {
|
||||
font-size: 30px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.address-name,
|
||||
.address-phone,
|
||||
.address-detail {
|
||||
font-size: 26px;
|
||||
color: var(--text-secondary);
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
/* 商品卡片 */
|
||||
.product-card {
|
||||
padding: 32px;
|
||||
}
|
||||
|
||||
.product-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.product-icon-wrapper {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
background: var(--bg-input);
|
||||
border-radius: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.product-icon {
|
||||
font-size: 56px;
|
||||
}
|
||||
|
||||
.product-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.product-name {
|
||||
font-size: 30px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.product-sku {
|
||||
font-size: 24px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.price-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 0;
|
||||
border-top: 1px dashed rgba(0,0,0,0.06);
|
||||
}
|
||||
|
||||
.price-label {
|
||||
font-size: 26px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.price-value {
|
||||
font-size: 28px;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.price-row.total {
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
.total-label {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.total-value {
|
||||
font-size: 36px;
|
||||
font-weight: 800;
|
||||
color: var(--accent-pink);
|
||||
}
|
||||
|
||||
/* 信息卡片 */
|
||||
.info-card {
|
||||
padding: 32px;
|
||||
}
|
||||
|
||||
.info-card .info-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
.info-card .info-label {
|
||||
font-size: 26px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.info-card .info-value {
|
||||
font-size: 26px;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* 预览卡片 */
|
||||
.preview-card {
|
||||
padding: 32px;
|
||||
}
|
||||
|
||||
.design-preview {
|
||||
position: relative;
|
||||
background: linear-gradient(135deg, rgba(255, 154, 158, 0.08) 0%, rgba(160, 196, 255, 0.08) 100%);
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
border: 3px dashed var(--accent-blue);
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.preview-sticker {
|
||||
position: absolute;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
import { View, Text, Image } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { useState, useEffect } from 'react'
|
||||
import './index.scss'
|
||||
import { getOrderList, getDesignList } from '../../utils/store'
|
||||
import ThemeToggle from '../../components/ThemeToggle'
|
||||
import { useThemeContext } from '../../context/ThemeContext'
|
||||
|
||||
export default function OrderDetailPage() {
|
||||
const { theme } = useThemeContext()
|
||||
const [order, setOrder] = useState<any>(null)
|
||||
const [design, setDesign] = useState<any>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const params = Taro.getCurrentInstance().router?.params
|
||||
const orderId = params?.orderId
|
||||
if (!orderId) return
|
||||
const orders = getOrderList()
|
||||
const found = orders.find(o => o.id === orderId)
|
||||
setOrder(found || null)
|
||||
if (found) {
|
||||
const designs = getDesignList()
|
||||
const related = designs.find(d => d.orderId === orderId)
|
||||
setDesign(related || null)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const STATUS_MAP: Record<string, { label: string; badge: string; desc: string }> = {
|
||||
pending: { label: '待付款', badge: 'badge-pink', desc: '您尚未付款,请及时支付' },
|
||||
paid: { label: '待发货', badge: 'badge-blue', desc: '商家正在准备发货' },
|
||||
shipping: { label: '待收货', badge: 'badge-blue', desc: '快递运输中' },
|
||||
done: { label: '已完成', badge: 'badge-green', desc: '交易已完成' }
|
||||
}
|
||||
|
||||
if (!order) {
|
||||
return (
|
||||
<View className={`theme-${theme}`}>
|
||||
<View className='order-detail-page'>
|
||||
<View className='page-header dashed-card mt-20'>
|
||||
<Text className='page-title'>订单未找到</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const status = STATUS_MAP[order.statusCode] || STATUS_MAP.pending
|
||||
|
||||
return (
|
||||
<View className={`theme-${theme}`}>
|
||||
<View className='order-detail-page'>
|
||||
<ThemeToggle />
|
||||
|
||||
<View className='page-header dashed-card mt-20'>
|
||||
<View className='star-badge' />
|
||||
<View className='flex-between' style={{ width: '100%' }}>
|
||||
<Text className='back-btn' onClick={() => Taro.navigateBack()}>←</Text>
|
||||
<Text className='page-title'>订单详情</Text>
|
||||
<View style={{ width: '60px' }} />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 状态卡片 */}
|
||||
<View className='status-card dashed-card mt-20'>
|
||||
<View className='star-badge' />
|
||||
<View className='status-top'>
|
||||
<Text className={`badge ${status.badge} status-badge`}>{status.label}</Text>
|
||||
<Text className='status-desc'>{status.desc}</Text>
|
||||
</View>
|
||||
<View className='logistics-timeline'>
|
||||
<View className='timeline-item active'>
|
||||
<View className='timeline-dot' />
|
||||
<View className='timeline-content'>
|
||||
<Text className='timeline-title'>已支付</Text>
|
||||
<Text className='timeline-time'>{order.date}</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View className='timeline-item'>
|
||||
<View className='timeline-dot' />
|
||||
<View className='timeline-content'>
|
||||
<Text className='timeline-title'>商家发货</Text>
|
||||
<Text className='timeline-time'>待更新</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View className='timeline-item'>
|
||||
<View className='timeline-dot' />
|
||||
<View className='timeline-content'>
|
||||
<Text className='timeline-title'>运输中</Text>
|
||||
<Text className='timeline-time'>待更新</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View className='timeline-item'>
|
||||
<View className='timeline-dot' />
|
||||
<View className='timeline-content'>
|
||||
<Text className='timeline-title'>已签收</Text>
|
||||
<Text className='timeline-time'>待更新</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 地址信息 */}
|
||||
<View className='address-card dashed-card mt-20'>
|
||||
<View className='star-badge' />
|
||||
<View className='address-header'>
|
||||
<Text className='address-icon'>📍</Text>
|
||||
<Text className='address-title'>收货地址</Text>
|
||||
</View>
|
||||
<Text className='address-name'>收货人:智绘微刻用户</Text>
|
||||
<Text className='address-phone'>138****8888</Text>
|
||||
<Text className='address-detail'>广东省深圳市南山区科技园智绘微刻总部</Text>
|
||||
</View>
|
||||
|
||||
{/* 商品信息 */}
|
||||
<View className='product-card dashed-card mt-20'>
|
||||
<View className='star-badge' />
|
||||
<Text className='section-title'>商品信息</Text>
|
||||
<View className='product-row'>
|
||||
<View className='product-icon-wrapper'>
|
||||
<Text className='product-icon'>{order.productIcon}</Text>
|
||||
</View>
|
||||
<View className='product-info'>
|
||||
<Text className='product-name'>{order.productName}</Text>
|
||||
<Text className='product-sku'>{order.sku}</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View className='price-row'>
|
||||
<Text className='price-label'>商品总价</Text>
|
||||
<Text className='price-value'>{order.price}</Text>
|
||||
</View>
|
||||
<View className='price-row'>
|
||||
<Text className='price-label'>运费</Text>
|
||||
<Text className='price-value'>¥0.00</Text>
|
||||
</View>
|
||||
<View className='price-row total'>
|
||||
<Text className='price-label total-label'>实付款</Text>
|
||||
<Text className='price-value total-value'>{order.price}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 订单信息 */}
|
||||
<View className='info-card dashed-card mt-20'>
|
||||
<View className='star-badge' />
|
||||
<Text className='section-title'>订单信息</Text>
|
||||
<View className='info-row'>
|
||||
<Text className='info-label'>订单编号</Text>
|
||||
<Text className='info-value'>{order.id}</Text>
|
||||
</View>
|
||||
<View className='info-row'>
|
||||
<Text className='info-label'>创建时间</Text>
|
||||
<Text className='info-value'>{order.date}</Text>
|
||||
</View>
|
||||
<View className='info-row'>
|
||||
<Text className='info-label'>支付方式</Text>
|
||||
<Text className='info-value'>微信支付</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 设计效果 */}
|
||||
{design?.designData?.stickers ? (
|
||||
<View className='preview-card dashed-card mt-20'>
|
||||
<View className='star-badge' />
|
||||
<Text className='section-title'>设计效果</Text>
|
||||
<View className='design-preview' style={{ width: design.designData.category?.mask?.width || 300, height: design.designData.category?.mask?.height || 420 }}>
|
||||
{design.designData.stickers.map((s: any) => (
|
||||
<Image key={s.id} className='preview-sticker' src={s.src} style={{ transform: `translate(${s.x}px, ${s.y}px) scale(${s.scale || 1})`, width: s.width || 200, height: s.height || 200 }} mode='aspectFit' />
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
) : design?.designData?.imageSrc ? (
|
||||
<View className='preview-card dashed-card mt-20'>
|
||||
<View className='star-badge' />
|
||||
<Text className='section-title'>设计效果</Text>
|
||||
<View className='design-preview' style={{ width: design.designData.category?.mask?.width || 300, height: design.designData.category?.mask?.height || 420 }}>
|
||||
<Image className='preview-sticker' src={design.designData.imageSrc} style={{ transform: `translate(${design.designData.imagePos?.x || 0}px, ${design.designData.imagePos?.y || 0}px) scale(${design.designData.imagePos?.scale || 1})` }} mode='aspectFit' />
|
||||
</View>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
<View style={{ height: '40px' }} />
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"navigationBarTitleText": "订单列表",
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
.orders-page {
|
||||
padding: 0 24px 24px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
padding: 50px 32px 24px;
|
||||
margin: 20px 0 0;
|
||||
}
|
||||
|
||||
/* 状态Tab */
|
||||
.status-tabs {
|
||||
margin: 20px 0;
|
||||
}
|
||||
.tabs-scroll {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.tab-item {
|
||||
display: inline-block;
|
||||
padding: 20px 32px;
|
||||
position: relative;
|
||||
}
|
||||
.tab-label {
|
||||
font-size: 28px;
|
||||
color: #b08d8d;
|
||||
font-weight: 500;
|
||||
}
|
||||
.tab-item.active .tab-label {
|
||||
color: #ff9a9e;
|
||||
font-weight: 700;
|
||||
}
|
||||
.tab-underline {
|
||||
position: absolute;
|
||||
bottom: 8px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 40px;
|
||||
height: 4px;
|
||||
background: #ff9a9e;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* 订单卡片 */
|
||||
.orders-list {
|
||||
padding: 0;
|
||||
}
|
||||
.order-card {
|
||||
margin-bottom: 24px;
|
||||
padding: 28px;
|
||||
}
|
||||
|
||||
.order-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 2px dashed #fce4ec;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.order-date {
|
||||
font-size: 24px;
|
||||
color: #b08d8d;
|
||||
}
|
||||
|
||||
.order-body {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.order-icon-wrapper {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
background: linear-gradient(135deg, #fff0f5 0%, #e3f2fd 100%);
|
||||
border-radius: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 20px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.order-icon {
|
||||
font-size: 56px;
|
||||
}
|
||||
.order-info {
|
||||
flex: 1;
|
||||
}
|
||||
.order-product {
|
||||
font-size: 30px;
|
||||
font-weight: 700;
|
||||
color: #5c3a3a;
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.order-sku {
|
||||
font-size: 24px;
|
||||
color: #b08d8d;
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.order-meta {
|
||||
font-size: 22px;
|
||||
color: #ccc;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.order-footer {
|
||||
padding-top: 16px;
|
||||
border-top: 2px dashed #fce4ec;
|
||||
}
|
||||
.order-price-bar {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: flex-end;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.price-label {
|
||||
font-size: 24px;
|
||||
color: #b08d8d;
|
||||
margin-right: 8px;
|
||||
}
|
||||
.price-value {
|
||||
font-size: 36px;
|
||||
font-weight: 800;
|
||||
color: #ff6b81;
|
||||
}
|
||||
.order-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 16px;
|
||||
}
|
||||
.order-btn {
|
||||
padding: 14px 28px !important;
|
||||
font-size: 26px !important;
|
||||
border-radius: 40px !important;
|
||||
}
|
||||
|
||||
/* 空状态 */
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 120px 40px;
|
||||
}
|
||||
.empty-icon {
|
||||
font-size: 100px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.empty-text {
|
||||
font-size: 36px;
|
||||
font-weight: 700;
|
||||
color: #5c3a3a;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.empty-sub {
|
||||
font-size: 28px;
|
||||
color: #b08d8d;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
import { View, Text, ScrollView } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { useState, useEffect } from 'react'
|
||||
import './index.scss'
|
||||
import { getOrderList, type OrderItem } from '../../utils/store'
|
||||
import ThemeToggle from '../../components/ThemeToggle'
|
||||
import { useThemeContext } from '../../context/ThemeContext'
|
||||
|
||||
const STATUS_TABS = [
|
||||
{ code: 'all', label: '全部' },
|
||||
{ code: 'pending', label: '待付款' },
|
||||
{ code: 'paid', label: '待发货' },
|
||||
{ code: 'shipping', label: '待收货' },
|
||||
{ code: 'done', label: '已完成' }
|
||||
]
|
||||
|
||||
const STATUS_MAP: Record<string, { label: string; badge: string }> = {
|
||||
pending: { label: '待付款', badge: 'badge-pink' },
|
||||
paid: { label: '待发货', badge: 'badge-blue' },
|
||||
shipping: { label: '待收货', badge: 'badge-blue' },
|
||||
done: { label: '已完成', badge: 'badge-green' },
|
||||
cancelled: { label: '已取消', badge: 'badge-gray' }
|
||||
}
|
||||
|
||||
export default function OrdersPage() {
|
||||
const { theme } = useThemeContext()
|
||||
const [activeTab, setActiveTab] = useState('all')
|
||||
const [orders, setOrders] = useState<OrderItem[]>([])
|
||||
|
||||
const load = () => setOrders(getOrderList())
|
||||
|
||||
useEffect(load, [])
|
||||
|
||||
// 读取从 profile 跳转过来的筛选状态
|
||||
useEffect(() => {
|
||||
const init = () => {
|
||||
const filter = Taro.getStorageSync('orders:filter')
|
||||
if (filter && STATUS_TABS.some(t => t.code === filter)) {
|
||||
setActiveTab(filter)
|
||||
Taro.removeStorageSync('orders:filter')
|
||||
}
|
||||
load()
|
||||
}
|
||||
init()
|
||||
// 页面每次显示时刷新
|
||||
const page = Taro.getCurrentInstance().page
|
||||
if (page) {
|
||||
const orig = page.onShow
|
||||
page.onShow = function () {
|
||||
init()
|
||||
if (orig) orig.apply(this)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
// 页面每次显示时刷新(处理下单后同步)
|
||||
useEffect(() => {
|
||||
Taro.eventCenter?.once?.('orders:refresh', load)
|
||||
load()
|
||||
return () => { Taro.eventCenter?.off?.('orders:refresh', load) }
|
||||
}, [])
|
||||
|
||||
const filteredOrders = activeTab === 'all'
|
||||
? orders
|
||||
: orders.filter(o => o.statusCode === activeTab)
|
||||
|
||||
const goDetail = (order: OrderItem) => {
|
||||
Taro.navigateTo({ url: `/pages/orderDetail/index?orderId=${order.id}` })
|
||||
}
|
||||
|
||||
return (
|
||||
<View className={`theme-${theme}`}>
|
||||
<View className='orders-page'>
|
||||
<ThemeToggle />
|
||||
|
||||
<View className='page-header dashed-card mt-20'>
|
||||
<View className='star-badge' />
|
||||
<Text className='page-title'>我的订单</Text>
|
||||
</View>
|
||||
|
||||
{/* 状态筛选Tab */}
|
||||
<View className='status-tabs'>
|
||||
<ScrollView className='tabs-scroll' scrollX>
|
||||
{STATUS_TABS.map((tab) => (
|
||||
<View
|
||||
key={tab.code}
|
||||
className={`tab-item ${activeTab === tab.code ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab(tab.code)}
|
||||
>
|
||||
<Text className='tab-label'>{tab.label}</Text>
|
||||
{activeTab === tab.code && <View className='tab-underline' />}
|
||||
</View>
|
||||
))}
|
||||
</ScrollView>
|
||||
</View>
|
||||
|
||||
{/* 订单列表 */}
|
||||
<ScrollView className='orders-list' scrollY>
|
||||
{filteredOrders.map((order) => {
|
||||
const status = STATUS_MAP[order.statusCode] || STATUS_MAP.pending
|
||||
return (
|
||||
<View key={order.id} className='order-card dashed-card' onClick={() => goDetail(order)}>
|
||||
<View className='star-badge' />
|
||||
<View className='order-header'>
|
||||
<Text className='order-date'>{order.date}</Text>
|
||||
<Text className={`badge ${status.badge}`}>{status.label}</Text>
|
||||
</View>
|
||||
<View className='order-body'>
|
||||
<View className='order-icon-wrapper'>
|
||||
<Text className='order-icon'>{order.productIcon}</Text>
|
||||
</View>
|
||||
<View className='order-info'>
|
||||
<Text className='order-product'>{order.productName}</Text>
|
||||
<Text className='order-sku'>{order.sku}</Text>
|
||||
<Text className='order-meta'>数量: {order.count} 件 | {order.id}</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View className='order-footer'>
|
||||
<View className='order-price-bar'>
|
||||
<Text className='price-label'>实付款</Text>
|
||||
<Text className='price-value'>{order.price}</Text>
|
||||
</View>
|
||||
<View className='order-actions'>
|
||||
{order.statusCode === 'pending' && (
|
||||
<View className='btn-gradient order-btn'>
|
||||
<Text>立即付款</Text>
|
||||
</View>
|
||||
)}
|
||||
{order.statusCode === 'paid' && (
|
||||
<View className='btn-outline order-btn'>
|
||||
<Text>催发货</Text>
|
||||
</View>
|
||||
)}
|
||||
{order.statusCode === 'shipping' && (
|
||||
<>
|
||||
<View className='btn-outline order-btn' onClick={(e) => { e.stopPropagation(); Taro.showToast({ title: '查看物流', icon: 'none' }) }}>
|
||||
<Text>查看物流</Text>
|
||||
</View>
|
||||
<View className='btn-gradient order-btn' onClick={(e) => { e.stopPropagation(); Taro.showToast({ title: '确认收货', icon: 'none' }) }}>
|
||||
<Text>确认收货</Text>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
{order.statusCode === 'done' && (
|
||||
<>
|
||||
<View className='btn-outline order-btn' onClick={(e) => { e.stopPropagation(); Taro.showToast({ title: '申请售后', icon: 'none' }) }}>
|
||||
<Text>申请售后</Text>
|
||||
</View>
|
||||
<View className='btn-gradient order-btn' onClick={(e) => { e.stopPropagation(); Taro.showToast({ title: '再来一单', icon: 'none' }) }}>
|
||||
<Text>再来一单</Text>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
<View className='btn-outline order-btn' onClick={(e) => { e.stopPropagation(); goDetail(order) }}>
|
||||
<Text>详情</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
})}
|
||||
|
||||
{filteredOrders.length === 0 && (
|
||||
<View className='empty-state'>
|
||||
<Text className='empty-icon'>📦</Text>
|
||||
<Text className='empty-text'>暂无订单</Text>
|
||||
<Text className='empty-sub'>去首页开始定制吧</Text>
|
||||
<View className='btn-gradient' onClick={() => Taro.switchTab({ url: '/pages/index/index' })}>
|
||||
<Text>去定制</Text>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</ScrollView>
|
||||
|
||||
<View style={{ height: '40px' }} />
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"navigationBarTitleText": "商品详情"
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
.product-page {
|
||||
padding: 0 24px 24px;
|
||||
padding-bottom: calc(24px + 160px); /* 为底部 action-bar 留空 */
|
||||
}
|
||||
|
||||
/* 返回按钮 */
|
||||
.back-btn {
|
||||
font-size: 40px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
/* Hero 轮播 */
|
||||
.hero-section {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.hero-swiper {
|
||||
height: 400rpx;
|
||||
}
|
||||
|
||||
.hero-card {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.hero-icon {
|
||||
font-size: 100px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.hero-name {
|
||||
font-size: 36px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.hero-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
/* 基本信息 */
|
||||
.info-section {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 20px 0;
|
||||
border-bottom: 1px dashed rgba(0,0,0,0.06);
|
||||
}
|
||||
|
||||
.info-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
font-size: 28px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.info-price {
|
||||
font-size: 40px;
|
||||
font-weight: 800;
|
||||
color: var(--accent-pink);
|
||||
}
|
||||
|
||||
.info-value {
|
||||
font-size: 28px;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* 详细介绍 */
|
||||
.detail-section {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.detail-text {
|
||||
font-size: 28px;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
/* 底部操作栏 */
|
||||
.action-bar .price-summary {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.summary-label {
|
||||
font-size: 22px;
|
||||
color: var(--text-secondary);
|
||||
display: block;
|
||||
}
|
||||
|
||||
.summary-price {
|
||||
font-size: 36px;
|
||||
font-weight: 700;
|
||||
color: var(--accent-pink);
|
||||
}
|
||||
|
||||
/* 弹窗 */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
padding: 48px;
|
||||
}
|
||||
|
||||
.modal-card {
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: 36px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
text-align: center;
|
||||
margin-bottom: 24px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.modal-product {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.modal-icon {
|
||||
font-size: 48px;
|
||||
}
|
||||
|
||||
.modal-name {
|
||||
font-size: 32px;
|
||||
color: var(--text-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.quantity-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.qty-label {
|
||||
font-size: 28px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.qty-control {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.qty-btn {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 12px;
|
||||
background: var(--bg-input);
|
||||
border: var(--line-card);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 32px;
|
||||
color: var(--text-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.qty-num {
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
min-width: 48px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.modal-total {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 32px;
|
||||
padding-top: 24px;
|
||||
border-top: 1px dashed rgba(0,0,0,0.06);
|
||||
}
|
||||
|
||||
.total-label {
|
||||
font-size: 28px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.total-price {
|
||||
font-size: 36px;
|
||||
font-weight: 800;
|
||||
color: var(--accent-pink);
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.modal-actions .btn-outline,
|
||||
.modal-actions .btn-gradient {
|
||||
flex: 1;
|
||||
padding: 20px 0;
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
import { View, Text, Image, Swiper, SwiperItem } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { useState } from 'react'
|
||||
import './index.scss'
|
||||
import { getProductById } from '../../utils/productConfig'
|
||||
import { addDesign } from '../../utils/store'
|
||||
import ThemeToggle from '../../components/ThemeToggle'
|
||||
import { useThemeContext } from '../../context/ThemeContext'
|
||||
|
||||
const HERO_COLORS = ['#FFE4EC', '#FFF0F5', '#FCE4EC']
|
||||
|
||||
export default function ProductPage() {
|
||||
const { theme } = useThemeContext()
|
||||
const [showModal, setShowModal] = useState(false)
|
||||
const [quantity, setQuantity] = useState(1)
|
||||
|
||||
const params = Taro.getCurrentInstance().router?.params
|
||||
const productId = params?.id || ''
|
||||
const product = getProductById(productId)
|
||||
|
||||
if (!product) {
|
||||
return (
|
||||
<View className={`theme-${theme}`}>
|
||||
<View className='product-page'>
|
||||
<View className='page-header dashed-card mt-20'>
|
||||
<Text className='page-title'>商品未找到</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const handleAddToList = () => {
|
||||
setQuantity(1)
|
||||
setShowModal(true)
|
||||
}
|
||||
|
||||
const confirmAdd = () => {
|
||||
addDesign(product, quantity)
|
||||
setShowModal(false)
|
||||
Taro.showToast({ title: `已加入设计清单 x${quantity}`, icon: 'success' })
|
||||
setTimeout(() => Taro.navigateBack(), 1200)
|
||||
}
|
||||
|
||||
const goToDIY = () => {
|
||||
Taro.navigateTo({
|
||||
url: `/pages/diy/index?source=product&productId=${product.id}&quantity=1`
|
||||
})
|
||||
}
|
||||
|
||||
const modQty = (delta: number) => {
|
||||
setQuantity(q => Math.max(1, Math.min(99, q + delta)))
|
||||
}
|
||||
|
||||
return (
|
||||
<View className={`theme-${theme}`}>
|
||||
<View className='product-page'>
|
||||
<ThemeToggle />
|
||||
|
||||
<View className='page-header dashed-card mt-20'>
|
||||
<View className='star-badge' />
|
||||
<View className='flex-between'>
|
||||
<Text className='back-btn' onClick={() => Taro.navigateBack()}>←</Text>
|
||||
<Text className='page-title'>商品详情</Text>
|
||||
<View style={{ width: '60px' }} />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 轮播 Hero */}
|
||||
<View className='hero-section mt-20'>
|
||||
<Swiper
|
||||
className='hero-swiper'
|
||||
circular
|
||||
autoplay
|
||||
interval={3000}
|
||||
duration={500}
|
||||
indicatorDots
|
||||
indicatorColor='rgba(0,0,0,0.2)'
|
||||
indicatorActiveColor='#ff9a9e'
|
||||
>
|
||||
{product.images && product.images.length > 0 ? (
|
||||
product.images.map((img, idx) => (
|
||||
<SwiperItem key={idx}>
|
||||
<View className='hero-card dashed-card'>
|
||||
<View className='star-badge' />
|
||||
<Image className='hero-image' src={img} mode='aspectFit' />
|
||||
</View>
|
||||
</SwiperItem>
|
||||
))
|
||||
) : (
|
||||
HERO_COLORS.map((color, idx) => (
|
||||
<SwiperItem key={idx}>
|
||||
<View className='hero-card dashed-card' style={{ background: color }}>
|
||||
<View className='star-badge' />
|
||||
<Text className='hero-icon'>{product.icon}</Text>
|
||||
<Text className='hero-name'>{product.name}</Text>
|
||||
</View>
|
||||
</SwiperItem>
|
||||
))
|
||||
)}
|
||||
</Swiper>
|
||||
</View>
|
||||
|
||||
{/* 基本信息 */}
|
||||
<View className='info-section dashed-card mt-20'>
|
||||
<View className='star-badge' />
|
||||
<View className='info-row'>
|
||||
<Text className='info-label'>单价</Text>
|
||||
<Text className='info-price'>¥{product.price}</Text>
|
||||
</View>
|
||||
<View className='info-row'>
|
||||
<Text className='info-label'>工期</Text>
|
||||
<Text className='info-value'>{product.leadTime}</Text>
|
||||
</View>
|
||||
<View className='info-row'>
|
||||
<Text className='info-label'>尺寸</Text>
|
||||
<Text className='info-value'>{product.mask.width} × {product.mask.height} px</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 详细介绍 */}
|
||||
<View className='detail-section dashed-card mt-20'>
|
||||
<View className='star-badge' />
|
||||
<Text className='section-title'>商品介绍</Text>
|
||||
<Text className='detail-text'>{product.description}</Text>
|
||||
</View>
|
||||
|
||||
{/* 底部操作栏 */}
|
||||
<View className='action-bar'>
|
||||
<View className='price-summary'>
|
||||
<Text className='summary-label'>起价</Text>
|
||||
<Text className='summary-price'>¥{product.price}</Text>
|
||||
</View>
|
||||
<View className='btn-outline' onClick={handleAddToList}>
|
||||
<Text>加入设计清单</Text>
|
||||
</View>
|
||||
<View className='btn-gradient' onClick={goToDIY}>
|
||||
<Text>立即下单</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 安全区占位(防止内容被 action-bar 遮挡) */}
|
||||
<View className='safe-bottom-placeholder' />
|
||||
|
||||
{/* 数量选择弹窗 */}
|
||||
{showModal && (
|
||||
<View className='modal-overlay'>
|
||||
<View className='modal-card dashed-card'>
|
||||
<View className='star-badge' />
|
||||
<Text className='modal-title'>加入设计清单</Text>
|
||||
<View className='modal-product'>
|
||||
<Text className='modal-icon'>{product.icon}</Text>
|
||||
<Text className='modal-name'>{product.name}</Text>
|
||||
</View>
|
||||
<View className='quantity-row'>
|
||||
<Text className='qty-label'>数量</Text>
|
||||
<View className='qty-control'>
|
||||
<View className='qty-btn' onClick={() => modQty(-1)}>-</View>
|
||||
<Text className='qty-num'>{quantity}</Text>
|
||||
<View className='qty-btn' onClick={() => modQty(1)}>+</View>
|
||||
</View>
|
||||
</View>
|
||||
<View className='modal-total'>
|
||||
<Text className='total-label'>小计</Text>
|
||||
<Text className='total-price'>¥{(product.price * quantity).toFixed(2)}</Text>
|
||||
</View>
|
||||
<View className='modal-actions'>
|
||||
<View className='btn-outline' onClick={() => setShowModal(false)}>
|
||||
<Text>取消</Text>
|
||||
</View>
|
||||
<View className='btn-gradient' onClick={confirmAdd}>
|
||||
<Text>确定</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"navigationBarTitleText": "个人中心",
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
.profile-page {
|
||||
padding: 0 24px 24px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* 用户头部 */
|
||||
.profile-header {
|
||||
padding: 40px 32px 32px;
|
||||
margin: 20px 0 0;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
border-radius: 50%;
|
||||
border: 4px dashed var(--line-star);
|
||||
margin-right: 24px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.avatar-placeholder {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, var(--bg-card) 0%, rgba(91, 140, 255, 0.1) 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 24px;
|
||||
border: 4px dashed var(--line-star);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.avatar-icon {
|
||||
font-size: 56px;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-size: 40px;
|
||||
font-weight: 800;
|
||||
color: var(--text-primary);
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.user-level {
|
||||
font-size: 24px;
|
||||
color: var(--text-secondary);
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* 5状态快捷入口 - 2+3布局 */
|
||||
.status-grid {
|
||||
background: var(--bg-card);
|
||||
border: var(--line-card);
|
||||
border-radius: 20px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.status-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.status-row.row2 {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 2px 1fr;
|
||||
}
|
||||
|
||||
.status-row.row3 {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 2px 1fr 2px 1fr;
|
||||
}
|
||||
|
||||
.status-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.status-num {
|
||||
font-size: 36px;
|
||||
font-weight: 800;
|
||||
color: var(--accent-pink);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.status-label {
|
||||
font-size: 24px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.status-divider {
|
||||
width: 2px;
|
||||
height: 48px;
|
||||
background: var(--line-star);
|
||||
opacity: 0.25;
|
||||
}
|
||||
|
||||
.status-divider-h {
|
||||
height: 2px;
|
||||
background: var(--line-star);
|
||||
opacity: 0.15;
|
||||
margin: 0 16px;
|
||||
}
|
||||
|
||||
/* 菜单网格 */
|
||||
.menu-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.menu-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 32px 16px;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.menu-card:active {
|
||||
transform: scale(0.96);
|
||||
}
|
||||
|
||||
.menu-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.menu-label {
|
||||
font-size: 26px;
|
||||
color: var(--text-primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 企业定制 */
|
||||
.enterprise-section {
|
||||
padding: 32px;
|
||||
}
|
||||
|
||||
.enterprise-title {
|
||||
font-size: 30px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.enterprise-desc {
|
||||
font-size: 24px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.enterprise-btn {
|
||||
padding: 16px 32px !important;
|
||||
font-size: 26px !important;
|
||||
border-radius: 40px !important;
|
||||
}
|
||||
|
||||
/* 登录弹窗 */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
padding: 48px;
|
||||
}
|
||||
|
||||
.login-card {
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
padding: 40px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.login-title {
|
||||
font-size: 36px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
display: block;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.login-desc {
|
||||
font-size: 26px;
|
||||
color: var(--text-secondary);
|
||||
display: block;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.avatar-btn {
|
||||
width: 160px;
|
||||
height: 160px;
|
||||
border-radius: 50%;
|
||||
margin: 0 auto 24px;
|
||||
background: var(--bg-input);
|
||||
border: 3px dashed var(--line-star);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
line-height: 1;
|
||||
font-size: 0;
|
||||
}
|
||||
|
||||
.avatar-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.avatar-placeholder {
|
||||
font-size: 24px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.nickname-input {
|
||||
width: 100%;
|
||||
height: 80px;
|
||||
background: var(--bg-input);
|
||||
border: 3px dashed var(--line-star);
|
||||
border-radius: 16px;
|
||||
padding: 0 24px;
|
||||
font-size: 28px;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 32px;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.login-actions {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.login-actions .btn-outline,
|
||||
.login-actions .btn-gradient {
|
||||
flex: 1;
|
||||
padding: 20px 0;
|
||||
text-align: center;
|
||||
}
|
||||
@@ -0,0 +1,261 @@
|
||||
import { View, Text, Image, Input, Button } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { useState, useEffect } from 'react'
|
||||
import './index.scss'
|
||||
import { getOrderList, getUserInfo, setUserInfo, getDesignList } from '../../utils/store'
|
||||
import ThemeToggle from '../../components/ThemeToggle'
|
||||
import { useThemeContext } from '../../context/ThemeContext'
|
||||
|
||||
const MENU_ITEMS = [
|
||||
{ icon: '🎨', label: '我的设计', path: '/pages/designList/index' },
|
||||
{ icon: '✨', label: '词云生成', path: '/pages/wordcloud/index' },
|
||||
{ icon: '📍', label: '收货地址', path: '' },
|
||||
{ icon: '📞', label: '联系客服', path: '/pages/service/index' },
|
||||
{ icon: '📋', label: '使用帮助', path: '' },
|
||||
{ icon: '⚙️', label: '设置', path: '' }
|
||||
]
|
||||
|
||||
export default function ProfilePage() {
|
||||
const { theme } = useThemeContext()
|
||||
const [isLoggedIn, setIsLoggedIn] = useState(false)
|
||||
const [userInfo, setUserInfoState] = useState({ nickName: '', avatarUrl: '' })
|
||||
const [showLogin, setShowLogin] = useState(false)
|
||||
const [loginNickName, setLoginNickName] = useState('')
|
||||
const [loginAvatarUrl, setLoginAvatarUrl] = useState('')
|
||||
const [orderStats, setOrderStats] = useState({
|
||||
toDesign: 0, pending: 0, paid: 0, shipping: 0, done: 0
|
||||
})
|
||||
|
||||
const loadStats = () => {
|
||||
const orders = getOrderList()
|
||||
const designs = getDesignList()
|
||||
setOrderStats({
|
||||
toDesign: designs.filter(d => d.status === 'undesigned' || d.status === 'designing').length,
|
||||
pending: orders.filter(o => o.statusCode === 'pending').length,
|
||||
paid: orders.filter(o => o.statusCode === 'paid').length,
|
||||
shipping: orders.filter(o => o.statusCode === 'shipping').length,
|
||||
done: orders.filter(o => o.statusCode === 'done').length
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const info = getUserInfo()
|
||||
if (info) {
|
||||
setIsLoggedIn(true)
|
||||
setUserInfoState({ nickName: info.nickName || '', avatarUrl: info.avatarUrl || '' })
|
||||
}
|
||||
loadStats()
|
||||
}, [])
|
||||
|
||||
const handleOpenLogin = () => setShowLogin(true)
|
||||
const handleCloseLogin = () => setShowLogin(false)
|
||||
|
||||
const handleLoginSubmit = () => {
|
||||
Taro.login({
|
||||
success: () => {
|
||||
const mockOpenid = 'mock_' + Date.now().toString(36) + '_' + Math.random().toString(36).slice(2, 6)
|
||||
const info = {
|
||||
openid: mockOpenid,
|
||||
nickName: loginNickName || '微信用户',
|
||||
avatarUrl: loginAvatarUrl || '',
|
||||
loginAt: Date.now()
|
||||
}
|
||||
setUserInfo(info)
|
||||
setUserInfoState({ nickName: info.nickName, avatarUrl: info.avatarUrl })
|
||||
setIsLoggedIn(true)
|
||||
setShowLogin(false)
|
||||
Taro.showToast({ title: '登录成功', icon: 'success' })
|
||||
},
|
||||
fail: () => {
|
||||
Taro.showToast({ title: '登录失败,请重试', icon: 'none' })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const onChooseAvatar = (e: any) => {
|
||||
setLoginAvatarUrl(e.detail.avatarUrl || '')
|
||||
}
|
||||
|
||||
const handleQuickClick = (type: string) => {
|
||||
if (!isLoggedIn) {
|
||||
Taro.showToast({ title: '请先登录', icon: 'none' })
|
||||
setShowLogin(true)
|
||||
return
|
||||
}
|
||||
switch (type) {
|
||||
case 'toDesign':
|
||||
Taro.switchTab({ url: '/pages/designList/index' })
|
||||
break
|
||||
case 'pending':
|
||||
// 跳转订单页并带状态参数 - 这里用 event 或 storage 传递
|
||||
Taro.setStorageSync('orders:filter', 'pending')
|
||||
Taro.switchTab({ url: '/pages/orders/index' })
|
||||
break
|
||||
case 'paid':
|
||||
Taro.setStorageSync('orders:filter', 'paid')
|
||||
Taro.switchTab({ url: '/pages/orders/index' })
|
||||
break
|
||||
case 'shipping':
|
||||
Taro.setStorageSync('orders:filter', 'shipping')
|
||||
Taro.switchTab({ url: '/pages/orders/index' })
|
||||
break
|
||||
case 'done':
|
||||
Taro.setStorageSync('orders:filter', 'done')
|
||||
Taro.switchTab({ url: '/pages/orders/index' })
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const handleMenuClick = (path: string) => {
|
||||
if (!path) {
|
||||
Taro.showToast({ title: '功能开发中', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (path.startsWith('/pages/')) {
|
||||
if (path.includes('designList') || path.includes('orders')) {
|
||||
if (!isLoggedIn) {
|
||||
Taro.showToast({ title: '请先登录', icon: 'none' })
|
||||
setShowLogin(true)
|
||||
return
|
||||
}
|
||||
Taro.switchTab({ url: path })
|
||||
} else {
|
||||
Taro.navigateTo({ url: path })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<View className={`theme-${theme}`}>
|
||||
<View className='profile-page'>
|
||||
<ThemeToggle />
|
||||
|
||||
{/* 用户信息头部 */}
|
||||
<View className='profile-header dashed-card mt-20'>
|
||||
<View className='star-badge' />
|
||||
<View className='user-info'>
|
||||
{isLoggedIn && userInfo.avatarUrl ? (
|
||||
<Image className='avatar' src={userInfo.avatarUrl} mode='aspectFill' />
|
||||
) : (
|
||||
<View className='avatar-placeholder' onClick={handleOpenLogin}>
|
||||
<Text className='avatar-icon'>👤</Text>
|
||||
</View>
|
||||
)}
|
||||
<View className='user-meta'>
|
||||
{isLoggedIn ? (
|
||||
<>
|
||||
<Text className='user-name'>{userInfo.nickName}</Text>
|
||||
<Text className='user-level'>智绘微刻会员</Text>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Text className='user-name' onClick={handleOpenLogin}>点击登录</Text>
|
||||
<Text className='user-level'>授权微信,开启专属定制</Text>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 5状态快捷入口 */}
|
||||
<View className='status-grid'>
|
||||
<View className='status-row row2'>
|
||||
<View className='status-item' onClick={() => handleQuickClick('toDesign')}>
|
||||
<Text className='status-num'>{orderStats.toDesign}</Text>
|
||||
<Text className='status-label'>待设计</Text>
|
||||
</View>
|
||||
<View className='status-divider' />
|
||||
<View className='status-item' onClick={() => handleQuickClick('pending')}>
|
||||
<Text className='status-num'>{orderStats.pending}</Text>
|
||||
<Text className='status-label'>待付款</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View className='status-divider-h' />
|
||||
<View className='status-row row3'>
|
||||
<View className='status-item' onClick={() => handleQuickClick('paid')}>
|
||||
<Text className='status-num'>{orderStats.paid}</Text>
|
||||
<Text className='status-label'>待发货</Text>
|
||||
</View>
|
||||
<View className='status-divider' />
|
||||
<View className='status-item' onClick={() => handleQuickClick('shipping')}>
|
||||
<Text className='status-num'>{orderStats.shipping}</Text>
|
||||
<Text className='status-label'>待收货</Text>
|
||||
</View>
|
||||
<View className='status-divider' />
|
||||
<View className='status-item' onClick={() => handleQuickClick('done')}>
|
||||
<Text className='status-num'>{orderStats.done}</Text>
|
||||
<Text className='status-label'>已完成</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 功能菜单 6宫格 */}
|
||||
<View className='mt-20'>
|
||||
<Text className='section-title'>常用功能</Text>
|
||||
<View className='menu-grid'>
|
||||
{MENU_ITEMS.map((item, idx) => (
|
||||
<View key={idx} className='menu-card dashed-card' onClick={() => handleMenuClick(item.path)}>
|
||||
<View className='star-badge' />
|
||||
<Text className='menu-icon'>{item.icon}</Text>
|
||||
<Text className='menu-label'>{item.label}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 企业定制入口 */}
|
||||
<View className='enterprise-section dashed-card mt-20'>
|
||||
<View className='star-badge' />
|
||||
<View className='flex-between'>
|
||||
<View className='flex-column'>
|
||||
<Text className='enterprise-title'>🏢 企业批量定制</Text>
|
||||
<Text className='enterprise-desc'>年会礼品、入职纪念、团建伴手礼</Text>
|
||||
</View>
|
||||
<View className='btn-gradient enterprise-btn'>
|
||||
<Text>立即咨询</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={{ height: '40px' }} />
|
||||
|
||||
{/* 登录弹窗 */}
|
||||
{showLogin && (
|
||||
<View className='modal-overlay'>
|
||||
<View className='login-card dashed-card'>
|
||||
<View className='star-badge' />
|
||||
<Text className='login-title'>登录智绘微刻</Text>
|
||||
<Text className='login-desc'>登录后同步您的设计清单和订单</Text>
|
||||
<Button
|
||||
className='avatar-btn'
|
||||
openType='chooseAvatar'
|
||||
onChooseAvatar={onChooseAvatar}
|
||||
>
|
||||
{loginAvatarUrl ? (
|
||||
<Image className='avatar-img' src={loginAvatarUrl} mode='aspectFill' />
|
||||
) : (
|
||||
<Text className='avatar-placeholder'>点击选择头像</Text>
|
||||
)}
|
||||
</Button>
|
||||
<Input
|
||||
type='nickname'
|
||||
className='nickname-input'
|
||||
placeholder='请输入昵称'
|
||||
value={loginNickName}
|
||||
onInput={(e: any) => setLoginNickName(e.detail.value)}
|
||||
/>
|
||||
<View className='login-actions'>
|
||||
<View className='btn-outline' onClick={handleCloseLogin}>
|
||||
<Text>取消</Text>
|
||||
</View>
|
||||
<View className='btn-gradient' onClick={handleLoginSubmit}>
|
||||
<Text>登录</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"navigationBarTitleText": "客服中心",
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
.service-page {
|
||||
padding: 0 24px 24px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
padding: 50px 32px 24px;
|
||||
margin: 20px 0 0;
|
||||
}
|
||||
|
||||
/* 客服英雄区 */
|
||||
.service-hero {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
padding: 50px 40px;
|
||||
}
|
||||
.service-icon {
|
||||
font-size: 80px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.service-title {
|
||||
font-size: 36px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
display: block;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.service-desc {
|
||||
font-size: 24px;
|
||||
color: var(--text-secondary);
|
||||
display: block;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
.service-btn {
|
||||
padding: 24px 56px !important;
|
||||
font-size: 30px !important;
|
||||
border-radius: 50px !important;
|
||||
}
|
||||
|
||||
/* 联系方式 */
|
||||
.contact-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
.contact-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 28px 32px;
|
||||
}
|
||||
.contact-icon {
|
||||
font-size: 48px;
|
||||
margin-right: 24px;
|
||||
}
|
||||
.contact-info {
|
||||
flex: 1;
|
||||
}
|
||||
.contact-label {
|
||||
font-size: 24px;
|
||||
color: var(--text-secondary);
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.contact-value {
|
||||
font-size: 30px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* FAQ */
|
||||
.faq-card {
|
||||
margin-bottom: 20px;
|
||||
padding: 28px 32px;
|
||||
}
|
||||
.faq-q {
|
||||
font-size: 30px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
display: block;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.faq-a {
|
||||
font-size: 26px;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* 企业专属 */
|
||||
.enterprise-service {
|
||||
padding: 32px;
|
||||
}
|
||||
.enterprise-title {
|
||||
font-size: 30px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.enterprise-desc {
|
||||
font-size: 24px;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 24px;
|
||||
display: block;
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import { View, Text } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import './index.scss'
|
||||
import ThemeToggle from '../../components/ThemeToggle'
|
||||
import { useThemeContext } from '../../context/ThemeContext'
|
||||
|
||||
const FAQ_ITEMS = [
|
||||
{ q: '定制周期需要多久?', a: '通常下单后3-5个工作日内发货,批量订单(50件以上)可能需要7-10个工作日。' },
|
||||
{ q: '可以修改设计稿吗?', a: '生产前可随时在设计工作台修改。确认下单后进入排队生产阶段,不可再修改。' },
|
||||
{ q: '支持退款/售后吗?', a: '非质量问题定制商品不支持7天无理由退款。如收到商品有破损或雕刻缺陷,请在签收48小时内联系客服处理。' },
|
||||
{ q: '可以开发票吗?', a: '支持开具增值税普通发票。请在下单时填写发票信息,或联系客服补开。' }
|
||||
]
|
||||
|
||||
export default function ServicePage() {
|
||||
const { theme } = useThemeContext()
|
||||
|
||||
return (
|
||||
<View className={`theme-${theme}`}>
|
||||
<View className='service-page'>
|
||||
<ThemeToggle />
|
||||
<View className='page-header dashed-card mt-20'>
|
||||
<View className='star-badge' />
|
||||
<Text className='page-title'>联系客服</Text>
|
||||
</View>
|
||||
|
||||
{/* 客服入口 */}
|
||||
<View className='service-hero dashed-card mt-20'>
|
||||
<View className='star-badge' />
|
||||
<Text className='service-icon'>💬</Text>
|
||||
<Text className='service-title'>智绘微刻客服中心</Text>
|
||||
<Text className='service-desc'>工作日 09:00 - 18:00 在线为您服务</Text>
|
||||
<View className='btn-gradient service-btn' onClick={() => Taro.showToast({ title: '跳转客服会话', icon: 'none' })}>
|
||||
<Text>🚀 发起咨询</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 联系方式 */}
|
||||
<View className='mt-20'>
|
||||
<Text className='section-title'>联系方式</Text>
|
||||
<View className='contact-list'>
|
||||
<View className='contact-item dashed-card'>
|
||||
<View className='star-badge' />
|
||||
<Text className='contact-icon'>📞</Text>
|
||||
<View className='contact-info'>
|
||||
<Text className='contact-label'>客服电话</Text>
|
||||
<Text className='contact-value'>400-XXX-XXXX</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View className='contact-item dashed-card' onClick={() => Taro.setClipboardData({ data: 'smart_engraving' })}>
|
||||
<View className='star-badge' />
|
||||
<Text className='contact-icon'>💬</Text>
|
||||
<View className='contact-info'>
|
||||
<Text className='contact-label'>客服微信</Text>
|
||||
<Text className='contact-value'>smart_engraving(点击复制)</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View className='contact-item dashed-card'>
|
||||
<View className='star-badge' />
|
||||
<Text className='contact-icon'>📧</Text>
|
||||
<View className='contact-info'>
|
||||
<Text className='contact-label'>商务邮箱</Text>
|
||||
<Text className='contact-value'>biz@smart-engraving.com</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 常见问题 */}
|
||||
<View className='mt-20'>
|
||||
<Text className='section-title'>常见问题</Text>
|
||||
{FAQ_ITEMS.map((item, idx) => (
|
||||
<View key={idx} className='faq-card dashed-card'>
|
||||
<View className='star-badge' />
|
||||
<Text className='faq-q'>Q: {item.q}</Text>
|
||||
<Text className='faq-a'>A: {item.a}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
|
||||
{/* B端专属 */}
|
||||
<View className='enterprise-service dashed-card mt-20 mb-20'>
|
||||
<View className='star-badge' />
|
||||
<Text className='enterprise-title'>🏢 企业专属客户经理</Text>
|
||||
<Text className='enterprise-desc'>批量订单(50件以上)享专属报价、优先排产、专人对接</Text>
|
||||
<View className='btn-outline enterprise-btn'>
|
||||
<Text>预约企业咨询</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={{ height: '40px' }} />
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"navigationBarTitleText": "词云生成",
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -0,0 +1,314 @@
|
||||
.wordcloud-page {
|
||||
min-height: 100vh;
|
||||
padding: 0 24px 24px;
|
||||
}
|
||||
|
||||
/* 顶部导航 */
|
||||
.page-header {
|
||||
padding: 50px 32px 24px;
|
||||
margin: 20px 0 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
font-size: 40px;
|
||||
color: var(--text-primary);
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
/* 步骤指示器 */
|
||||
.step-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 32px;
|
||||
margin: 0 24px;
|
||||
}
|
||||
|
||||
.step-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.step-circle {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 50%;
|
||||
background: var(--bg-input);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 12px;
|
||||
border: 4px dashed var(--bg-input);
|
||||
}
|
||||
|
||||
.step-circle.active {
|
||||
background: var(--btn-gradient);
|
||||
border-color: var(--accent-pink);
|
||||
}
|
||||
|
||||
.step-num {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.step-circle.active .step-num {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.step-label {
|
||||
font-size: 24px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.step-label.active {
|
||||
color: var(--accent-pink);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.step-line {
|
||||
position: absolute;
|
||||
top: 30px;
|
||||
left: 60%;
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
background: var(--bg-input);
|
||||
}
|
||||
|
||||
.step-line.active {
|
||||
background: var(--btn-gradient);
|
||||
}
|
||||
|
||||
/* 步骤内容 */
|
||||
.step-content {
|
||||
padding: 32px;
|
||||
}
|
||||
|
||||
/* 上传区域 */
|
||||
.upload-area {
|
||||
min-height: 400px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.upload-placeholder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 60px;
|
||||
}
|
||||
|
||||
.upload-icon {
|
||||
font-size: 80px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.upload-title {
|
||||
font-size: 32px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.upload-desc {
|
||||
font-size: 26px;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.upload-tip {
|
||||
font-size: 22px;
|
||||
color: var(--accent-blue);
|
||||
background: rgba(91, 140, 255, 0.1);
|
||||
padding: 8px 20px;
|
||||
border-radius: 50px;
|
||||
border: var(--line-card);
|
||||
}
|
||||
|
||||
.preview-image {
|
||||
width: 100%;
|
||||
height: 400px;
|
||||
border-radius: 24px;
|
||||
}
|
||||
|
||||
/* 按钮 */
|
||||
.action-btns {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
/* 输入区域 */
|
||||
.input-section {
|
||||
padding: 32px;
|
||||
}
|
||||
|
||||
.names-input {
|
||||
width: 100%;
|
||||
min-height: 300px;
|
||||
background: var(--bg-input);
|
||||
border-radius: 16px;
|
||||
padding: 24px;
|
||||
font-size: 28px;
|
||||
line-height: 1.6;
|
||||
box-sizing: border-box;
|
||||
border: var(--line-card);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.input-stats {
|
||||
display: block;
|
||||
text-align: right;
|
||||
font-size: 24px;
|
||||
color: var(--text-secondary);
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.input-tips {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.tip-item {
|
||||
display: block;
|
||||
font-size: 24px;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
/* 生成中 */
|
||||
.generating-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 80px 40px;
|
||||
}
|
||||
|
||||
.generating-icon {
|
||||
font-size: 120px;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { transform: scale(1); opacity: 1; }
|
||||
50% { transform: scale(1.2); opacity: 0.7; }
|
||||
}
|
||||
|
||||
.generating-title {
|
||||
font-size: 40px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
.generating-subtitle {
|
||||
font-size: 28px;
|
||||
color: var(--text-secondary);
|
||||
margin-top: 16px;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
width: 80%;
|
||||
height: 16px;
|
||||
background: rgba(255, 154, 158, 0.15);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: var(--btn-gradient);
|
||||
border-radius: 8px;
|
||||
transition: width 0.5s ease;
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
color: var(--accent-pink);
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
/* 结果面板 */
|
||||
.result-panel {
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.result-image-wrapper {
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.result-image {
|
||||
width: 100%;
|
||||
height: 500px;
|
||||
background: linear-gradient(135deg, rgba(255, 154, 158, 0.08) 0%, rgba(160, 196, 255, 0.08) 100%);
|
||||
}
|
||||
|
||||
.watermark-badge {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
color: var(--text-primary);
|
||||
padding: 8px 20px;
|
||||
border-radius: 8px;
|
||||
font-size: 22px;
|
||||
border: var(--line-card);
|
||||
}
|
||||
|
||||
.result-actions {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
margin: 32px 0;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.action-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.action-label {
|
||||
font-size: 24px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* 跳转DIY卡片 */
|
||||
.diy-jump-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.diy-jump-title {
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.diy-jump-desc {
|
||||
font-size: 24px;
|
||||
color: var(--text-secondary);
|
||||
display: block;
|
||||
}
|
||||
|
||||
.diy-jump-arrow {
|
||||
font-size: 40px;
|
||||
color: var(--accent-pink);
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
import { View, Text, Image, Button, Textarea, Input } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { useState } from 'react'
|
||||
import './index.scss'
|
||||
import ThemeToggle from '../../components/ThemeToggle'
|
||||
import { useThemeContext } from '../../context/ThemeContext'
|
||||
|
||||
export default function WordCloudPage() {
|
||||
const { theme } = useThemeContext()
|
||||
const [step, setStep] = useState(1)
|
||||
const [baseImage, setBaseImage] = useState('')
|
||||
const [namesText, setNamesText] = useState('')
|
||||
const [generatedImage, setGeneratedImage] = useState('')
|
||||
const [isGenerating, setIsGenerating] = useState(false)
|
||||
const [progress, setProgress] = useState(0)
|
||||
|
||||
const steps = [
|
||||
{ num: 1, label: '上传底图' },
|
||||
{ num: 2, label: '输入名单' },
|
||||
{ num: 3, label: '生成预览' }
|
||||
]
|
||||
|
||||
const chooseImage = () => {
|
||||
Taro.chooseImage({
|
||||
count: 1, sizeType: ['compressed'], sourceType: ['album', 'camera'],
|
||||
success: (res) => { setBaseImage(res.tempFilePaths[0]); setStep(2) }
|
||||
})
|
||||
}
|
||||
|
||||
const handleGenerate = () => {
|
||||
if (!namesText.trim()) {
|
||||
Taro.showToast({ title: '请先输入名字', icon: 'none' })
|
||||
return
|
||||
}
|
||||
setStep(3); setIsGenerating(true); setProgress(0)
|
||||
const timer = setInterval(() => {
|
||||
setProgress((prev) => {
|
||||
if (prev >= 100) {
|
||||
clearInterval(timer); setIsGenerating(false)
|
||||
setGeneratedImage(baseImage)
|
||||
return 100
|
||||
}
|
||||
return prev + Math.random() * 15
|
||||
})
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
const handleExportImage = () => {
|
||||
if (!generatedImage) return
|
||||
Taro.saveImageToPhotosAlbum({
|
||||
filePath: generatedImage,
|
||||
success: () => Taro.showToast({ title: '已保存到相册', icon: 'success' }),
|
||||
fail: () => Taro.showToast({ title: '保存失败', icon: 'none' })
|
||||
})
|
||||
}
|
||||
|
||||
const goBack = () => { if (step > 1) setStep(step - 1); else Taro.navigateBack() }
|
||||
|
||||
return (
|
||||
<View className={`theme-${theme}`}>
|
||||
<View className='wordcloud-page'>
|
||||
<ThemeToggle />
|
||||
|
||||
<View className='page-header flex-between'>
|
||||
<Text className='back-btn' onClick={goBack}>←</Text>
|
||||
<Text className='page-title'>AI词云生成</Text>
|
||||
<View style={{ width: '60px' }} />
|
||||
</View>
|
||||
|
||||
{/* 步骤指示器 */}
|
||||
<View className='step-indicator dashed-card'>
|
||||
{steps.map((s, idx) => (
|
||||
<View key={s.num} className='step-item'>
|
||||
<View className={`step-circle ${step >= s.num ? 'active' : ''}`}>
|
||||
<Text className='step-num'>{s.num}</Text>
|
||||
</View>
|
||||
<Text className={`step-label ${step >= s.num ? 'active' : ''}`}>{s.label}</Text>
|
||||
{idx < steps.length - 1 && <View className={`step-line ${step > s.num ? 'active' : ''}`} />}
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
|
||||
{/* 步骤1: 上传底图 */}
|
||||
{step === 1 && (
|
||||
<View className='step-content'>
|
||||
<View className='upload-area dashed-card' onClick={chooseImage}>
|
||||
{baseImage ? (
|
||||
<Image className='preview-image' src={baseImage} mode='aspectFit' />
|
||||
) : (
|
||||
<View className='upload-placeholder'>
|
||||
<Text className='upload-icon'>📷</Text>
|
||||
<Text className='upload-title'>点击上传底图</Text>
|
||||
<Text className='upload-desc'>支持照片、Logo、图案等</Text>
|
||||
<Text className='upload-tip'>系统将用名字填充到深色区域</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
{baseImage && (
|
||||
<View className='action-btns'>
|
||||
<View className='btn-gradient' onClick={() => setStep(2)}>
|
||||
<Text>下一步:输入名单</Text>
|
||||
</View>
|
||||
<View className='btn-outline' onClick={() => setBaseImage('')}>
|
||||
<Text>重新选择</Text>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 步骤2: 输入名单 */}
|
||||
{step === 2 && (
|
||||
<View className='step-content'>
|
||||
<View className='input-section dashed-card'>
|
||||
<Text className='section-title'>输入名字名单</Text>
|
||||
<Textarea
|
||||
className='names-input'
|
||||
placeholder={`例如:\n张三\n李四\n王五`}
|
||||
value={namesText}
|
||||
onInput={(e) => setNamesText(e.detail.value)}
|
||||
maxlength={5000}
|
||||
/>
|
||||
<Text className='input-stats'>
|
||||
已输入 {namesText.split(/\n|,|,/).filter(n => n.trim()).length} 个名字
|
||||
</Text>
|
||||
<View className='input-tips'>
|
||||
<Text className='tip-item'>名字会随机分布在底图的深色区域</Text>
|
||||
<Text className='tip-item'>建议输入10-200个名字效果最佳</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View className='action-btns'>
|
||||
<View className='btn-gradient' onClick={handleGenerate}>
|
||||
<Text>开始生成词云</Text>
|
||||
</View>
|
||||
<View className='btn-outline' onClick={() => setStep(1)}>
|
||||
<Text>上一步</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 步骤3: 生成中/预览 */}
|
||||
{step === 3 && (
|
||||
<View className='step-content'>
|
||||
{isGenerating ? (
|
||||
<View className='generating-panel'>
|
||||
<Text className='generating-icon'>✨</Text>
|
||||
<Text className='generating-title'>正在生成词云...</Text>
|
||||
<Text className='generating-subtitle'>AI正在将名字融入图案</Text>
|
||||
<View className='progress-bar'>
|
||||
<View className='progress-fill' style={{ width: `${Math.min(progress, 100)}%` }} />
|
||||
</View>
|
||||
<Text className='progress-text'>{Math.min(Math.round(progress), 100)}%</Text>
|
||||
</View>
|
||||
) : (
|
||||
<View className='result-panel'>
|
||||
<View className='result-image-wrapper dashed-card'>
|
||||
<Image className='result-image' src={generatedImage || ''} mode='aspectFit' />
|
||||
<View className='watermark-badge'>
|
||||
<Text>智绘微刻预览</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View className='result-actions'>
|
||||
<View className='action-btn' onClick={handleExportImage}>
|
||||
<Text className='action-icon'>💾</Text>
|
||||
<Text className='action-label'>保存图片</Text>
|
||||
</View>
|
||||
<View className='action-btn' onClick={handleGenerate}>
|
||||
<Text className='action-icon'>🔄</Text>
|
||||
<Text className='action-label'>重新生成</Text>
|
||||
</View>
|
||||
<View className='action-btn' onClick={() => setStep(2)}>
|
||||
<Text className='action-icon'>✏️</Text>
|
||||
<Text className='action-label'>修改名单</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user