微信小程序上传

This commit is contained in:
2026-07-12 14:45:57 +08:00
parent d271ac5b13
commit becf91da52
45 changed files with 1815 additions and 23 deletions
@@ -0,0 +1,4 @@
export default definePageConfig({
navigationStyle: 'custom',
navigationBarTitleText: '好客权益',
});
@@ -0,0 +1,35 @@
import { View, Text, Button } from '@tarojs/components';
import Taro, { useDidShow } from '@tarojs/taro';
import TabMainHeader from '../../components/TabMainHeader';
import UserTabBar, { shouldRenderPageTabBar, syncTabBarSelected } from '../../components/UserTabBar';
import { isLoggedIn } from '../../lib/api';
export default function BenefitPage() {
const loggedIn = isLoggedIn();
useDidShow(() => {
syncTabBarSelected(2);
});
return (
<View className="u-page u-page--tab">
<TabMainHeader title="好客权益" />
<View className="u-card">
{loggedIn ? (
<View className="u-empty"></View>
) : (
<View>
<View className="u-empty"></View>
<Button
className="u-btn u-btn--block"
onClick={() => Taro.navigateTo({ url: '/pages/login/index' })}
>
</Button>
</View>
)}
</View>
{shouldRenderPageTabBar() ? <UserTabBar selected={2} /> : null}
</View>
);
}
@@ -0,0 +1,4 @@
export default definePageConfig({
navigationStyle: 'custom',
navigationBarTitleText: '杜康好客',
});
+123
View File
@@ -0,0 +1,123 @@
import { useEffect, useState } from 'react';
import { View, Text, Image } from '@tarojs/components';
import { useDidShow } from '@tarojs/taro';
import TabMainHeader from '../../components/TabMainHeader';
import CouponBadge from '../../components/CouponBadge';
import UserTabBar, { shouldRenderPageTabBar, syncTabBarSelected } from '../../components/UserTabBar';
import { request, toast } from '../../lib/api';
import { FALLBACK_CITY_CODE, getProductMainImage } from '../../lib/product-images';
type Product = {
id: string;
name: string;
subtitle?: string;
price: number;
benefitDisplay?: number;
mainImageUrl?: string | null;
carouselUrls?: string[] | null;
aromaType: string;
};
const AROMA_TABS = [
{ key: 'QINGXIANG', label: '清香型', open: true },
{ key: 'JIANGXIANG', label: '酱香型', open: false },
{ key: 'NONGXIANG', label: '浓香型', open: false },
] as const;
export default function HomePage() {
const [tab, setTab] = useState('QINGXIANG');
const [products, setProducts] = useState<Product[]>([]);
const [loading, setLoading] = useState(true);
const cityCode = FALLBACK_CITY_CODE;
useDidShow(() => {
syncTabBarSelected(0);
});
useEffect(() => {
setLoading(true);
request<Product[]>(`/catalog/products?cityCode=${encodeURIComponent(cityCode)}`)
.then((list) => setProducts(Array.isArray(list) ? list : []))
.catch((e) => toast(e instanceof Error ? e.message : '加载失败'))
.finally(() => setLoading(false));
}, [cityCode]);
function onAromaTabClick(key: string, open: boolean) {
if (!open) {
toast('暂未开放');
return;
}
setTab(key);
}
const filtered = products.filter((p) => p.aromaType === tab);
const onSale = tab === 'QINGXIANG';
return (
<View className="u-page u-page--tab home-page">
<TabMainHeader
title="杜康好客"
extra={(
<View style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
<View className="tab-main-city-pin" />
<Text className="tab-main-city-label"></Text>
</View>
)}
/>
<View className="home-aroma-nav">
{AROMA_TABS.map((t) => (
<Text
key={t.key}
className={`home-aroma-tab${tab === t.key ? ' home-aroma-tab--active' : ''}${!t.open ? ' home-aroma-tab--muted' : ''}`}
onClick={() => onAromaTabClick(t.key, t.open)}
>
{t.label}
</Text>
))}
</View>
<View className="home-product-list">
{loading ? <View className="home-empty"></View> : null}
{!loading && !onSale ? <View className="home-empty">线</View> : null}
{!loading && onSale && filtered.length === 0 ? (
<View className="home-empty"></View>
) : null}
{!loading &&
onSale &&
filtered.map((p) => {
const cover = getProductMainImage(p);
return (
<View key={p.id} className="home-product-card">
{cover ? (
<Image className="home-product-cover" src={cover} mode="aspectFill" />
) : (
<View className="home-product-cover--empty" />
)}
<View className="home-product-body">
<View className="home-product-row">
<Text className="home-product-name">{p.name}</Text>
<Text className="home-product-price">¥{Number(p.price).toFixed(2)}</Text>
</View>
{p.subtitle ? <Text className="home-product-sub">{p.subtitle}</Text> : null}
<View className="home-product-footer">
<CouponBadge amount={p.benefitDisplay ?? p.price} label="好客权益" />
</View>
</View>
<View className="home-product-actions">
<Text
className="home-buy-btn"
onClick={() => toast('详情页开发中')}
>
</Text>
</View>
</View>
);
})}
</View>
{shouldRenderPageTabBar() ? <UserTabBar selected={0} /> : null}
</View>
);
}
@@ -0,0 +1,3 @@
export default definePageConfig({
navigationBarTitleText: '登录',
});
+89
View File
@@ -0,0 +1,89 @@
.login-page {
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
padding: calc(64px + env(safe-area-inset-top, 0px)) 24px calc(24px + env(safe-area-inset-bottom, 0px));
background: linear-gradient(160deg, #7a0f16 0%, #a61d24 42%, #faf9f7 42%, #faf9f7 100%);
box-sizing: border-box;
}
.login-brand {
display: flex;
flex-direction: column;
align-items: center;
color: #fff;
margin-bottom: 32px;
}
.login-title {
font-size: 26px;
font-weight: 800;
letter-spacing: 2px;
}
.login-subtitle {
font-size: 13px;
opacity: 0.85;
margin-top: 4px;
letter-spacing: 4px;
}
.login-card {
width: 100%;
max-width: 400px;
background: #fff;
border-radius: 20px;
padding: 24px 20px;
box-shadow: 0 8px 30px rgba(93, 64, 55, 0.12);
display: flex;
flex-direction: column;
gap: 14px;
box-sizing: border-box;
}
.login-card-title {
font-size: 18px;
font-weight: 700;
text-align: center;
margin-bottom: 6px;
color: var(--color-on-surface, #1a1c1b);
}
.login-input {
width: 100%;
height: 48px;
padding: 0 14px;
border-radius: 12px;
background: #faf9f7;
font-size: 15px;
box-sizing: border-box;
}
.login-code-row {
display: flex;
gap: 8px;
align-items: center;
}
.login-input--code {
flex: 1;
}
.login-send {
flex-shrink: 0;
height: 48px;
padding: 0 12px;
border-radius: 12px;
background: rgba(166, 29, 36, 0.08);
color: #a61d24;
font-size: 13px;
font-weight: 600;
border: none;
line-height: 48px;
}
.login-msg {
color: #a61d24;
font-size: 13px;
}
+114
View File
@@ -0,0 +1,114 @@
import { useState } from 'react';
import { View, Text, Input, Button } from '@tarojs/components';
import Taro from '@tarojs/taro';
import { SmsScene } from '@dukang/shared-types';
import { request, saveAuth, toast, type SessionPayload } from '../../lib/api';
import './index.css';
export default function LoginPage() {
const [phone, setPhone] = useState('');
const [code, setCode] = useState(process.env.TARO_ENV === 'h5' ? '123456' : '');
const [cooldown, setCooldown] = useState(0);
const [loading, setLoading] = useState(false);
const [msg, setMsg] = useState('');
async function sendCode() {
if (cooldown > 0) return;
const normalized = phone.trim();
if (!/^1[3-9]\d{9}$/.test(normalized)) {
setMsg('请输入正确的手机号');
return;
}
setMsg('');
try {
await request('/auth/sms/send', {
method: 'POST',
data: { phone: normalized, scene: SmsScene.USER_LOGIN },
});
toast('验证码已发送');
setCooldown(60);
const timer = setInterval(() => {
setCooldown((c) => {
if (c <= 1) {
clearInterval(timer);
return 0;
}
return c - 1;
});
}, 1000);
} catch (e) {
setMsg(e instanceof Error ? e.message : '发送失败');
}
}
async function login() {
const normalized = phone.trim();
if (!/^1[3-9]\d{9}$/.test(normalized)) {
setMsg('请输入正确的手机号');
return;
}
if (!code.trim()) {
setMsg('请输入验证码');
return;
}
setLoading(true);
setMsg('');
try {
const data = await request<SessionPayload>('/auth/login/sms', {
method: 'POST',
data: { phone: normalized, code: code.trim() },
});
saveAuth(data);
toast('登录成功', 'success');
Taro.switchTab({ url: '/pages/home/index' });
} catch (e) {
setMsg(e instanceof Error ? e.message : '登录失败');
} finally {
setLoading(false);
}
}
return (
<View className="login-page">
<View className="login-brand">
<Text className="login-title"></Text>
<Text className="login-subtitle"></Text>
</View>
<View className="login-card">
<Text className="login-card-title"></Text>
<Input
className="login-input"
type="number"
maxlength={11}
placeholder="请输入手机号"
value={phone}
onInput={(e) => setPhone(e.detail.value)}
/>
<View className="login-code-row">
<Input
className="login-input login-input--code"
type="number"
maxlength={6}
placeholder="验证码"
value={code}
onInput={(e) => setCode(e.detail.value)}
/>
<Button className="login-send" disabled={cooldown > 0} onClick={() => void sendCode()}>
{cooldown > 0 ? `${cooldown}s` : '获取验证码'}
</Button>
</View>
{msg ? <Text className="login-msg">{msg}</Text> : null}
<Button
className="u-btn u-btn--primary u-btn--block"
loading={loading}
onClick={() => void login()}
>
</Button>
<Text className="u-muted" style={{ textAlign: 'center', marginTop: 8 }}>
Mock 123456
</Text>
</View>
</View>
);
}
@@ -0,0 +1,4 @@
export default definePageConfig({
navigationStyle: 'custom',
navigationBarTitleText: '我的',
});
+57
View File
@@ -0,0 +1,57 @@
import { useEffect, useState } from 'react';
import { View, Text, Button } from '@tarojs/components';
import Taro, { useDidShow } from '@tarojs/taro';
import TabMainHeader from '../../components/TabMainHeader';
import UserTabBar, { shouldRenderPageTabBar, syncTabBarSelected } from '../../components/UserTabBar';
import { isLoggedIn, logout, request, toast, type UserProfile } from '../../lib/api';
export default function MinePage() {
const [profile, setProfile] = useState<UserProfile | null>(null);
const loggedIn = isLoggedIn();
useDidShow(() => {
syncTabBarSelected(3);
});
useEffect(() => {
if (!loggedIn) return;
request<UserProfile>('/auth/me')
.then(setProfile)
.catch((e) => toast(e instanceof Error ? e.message : '加载失败'));
}, [loggedIn]);
return (
<View className="u-page u-page--tab">
<TabMainHeader title="我的" />
<View className="u-card">
{loggedIn ? (
<View>
<Text className="u-title" style={{ marginBottom: 4 }}>
{profile?.nickname || '用户'}
</Text>
<Text className="u-muted" style={{ display: 'block', marginBottom: 16 }}>
{profile?.phone || '未绑定手机'}
</Text>
<Text className="u-muted" style={{ display: 'block', marginBottom: 16 }}>
/ / h5-user
</Text>
<Button className="u-btn u-btn--block" onClick={() => logout()}>
退
</Button>
</View>
) : (
<View>
<View className="u-empty"></View>
<Button
className="u-btn u-btn--block"
onClick={() => Taro.navigateTo({ url: '/pages/login/index' })}
>
</Button>
</View>
)}
</View>
{shouldRenderPageTabBar() ? <UserTabBar selected={3} /> : null}
</View>
);
}
@@ -0,0 +1,4 @@
export default definePageConfig({
navigationStyle: 'custom',
navigationBarTitleText: '门店',
});
+55
View File
@@ -0,0 +1,55 @@
import { useEffect, useState } from 'react';
import { View, Text } from '@tarojs/components';
import { useDidShow } from '@tarojs/taro';
import TabMainHeader from '../../components/TabMainHeader';
import UserTabBar, { shouldRenderPageTabBar, syncTabBarSelected } from '../../components/UserTabBar';
import { request, toast } from '../../lib/api';
type Store = {
id: string;
name: string;
address?: string;
status?: string;
};
export default function StoresPage() {
const [stores, setStores] = useState<Store[]>([]);
const [loading, setLoading] = useState(true);
useDidShow(() => {
syncTabBarSelected(1);
});
useEffect(() => {
request<Store[]>('/stores')
.then((list) => setStores(Array.isArray(list) ? list : []))
.catch((e) => toast(e instanceof Error ? e.message : '加载失败'))
.finally(() => setLoading(false));
}, []);
return (
<View className="u-page u-page--tab">
<TabMainHeader title="门店" />
<View className="u-card">
{loading ? (
<View className="u-empty"></View>
) : stores.length === 0 ? (
<View className="u-empty"></View>
) : (
stores.map((s) => (
<View key={s.id} className="u-store-row">
<View className="u-store-avatar" />
<View style={{ flex: 1, minWidth: 0 }}>
<Text className="u-store-name">{s.name}</Text>
<Text className="u-muted" style={{ display: 'block', marginTop: '4px' }}>
{s.address || '地址待完善'}
</Text>
</View>
</View>
))
)}
</View>
{shouldRenderPageTabBar() ? <UserTabBar selected={1} /> : null}
</View>
);
}