From c7b125535c10de3724be6bd7360ee7f8c3ade720 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B9=98=E9=A3=8E?= Date: Sat, 31 Jan 2026 22:37:05 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=95=B0=E6=8D=AE=E5=BA=93?= =?UTF-8?q?=E5=88=9D=E5=A7=8B=E5=8C=96=EF=BC=8C=E6=96=B0=E5=A2=9E=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E6=94=B6=E8=B4=A7=E5=9C=B0=E5=9D=80=E8=A1=A8=E4=BB=A5?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E5=A4=9A=E5=9C=B0=E5=9D=80=E7=AE=A1=E7=90=86?= =?UTF-8?q?=EF=BC=9B=E6=9B=B4=E6=96=B0=E7=AB=A0=E8=8A=82=E9=A1=B5=E9=9D=A2?= =?UTF-8?q?=E5=92=8C=E6=88=91=E7=9A=84=E9=A1=B5=E9=9D=A2=EF=BC=8C=E6=95=B4?= =?UTF-8?q?=E5=90=88=E5=BA=95=E9=83=A8=E5=AF=BC=E8=88=AA=E7=BB=84=E4=BB=B6?= =?UTF-8?q?=EF=BC=8C=E6=8F=90=E5=8D=87=E7=94=A8=E6=88=B7=E4=BD=93=E9=AA=8C?= =?UTF-8?q?=EF=BC=9B=E8=B0=83=E6=95=B4=E5=B0=8F=E7=A8=8B=E5=BA=8F=E8=AE=BE?= =?UTF-8?q?=E7=BD=AE=E9=A1=B5=E9=9D=A2=EF=BC=8C=E5=A2=9E=E5=8A=A0=E6=94=B6?= =?UTF-8?q?=E8=B4=A7=E5=9C=B0=E5=9D=80=E7=AE=A1=E7=90=86=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=EF=BC=8C=E4=BC=98=E5=8C=96=E6=A0=B7=E5=BC=8F=E4=B8=8E=E5=B8=83?= =?UTF-8?q?=E5=B1=80=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/db/init/route.ts | 24 +++ app/api/user/addresses/[id]/route.ts | 112 ++++++++++ app/api/user/addresses/route.ts | 68 ++++++ app/chapters/page.tsx | 29 +-- app/my/addresses/[id]/page.tsx | 196 ++++++++++++++++++ app/my/addresses/new/page.tsx | 163 +++++++++++++++ app/my/addresses/page.tsx | 141 +++++++++++++ app/my/page.tsx | 38 +--- app/my/settings/page.tsx | 21 +- miniprogram/app.json | 5 +- .../pages/address-edit/address-edit.js | 136 ++++++++++++ .../pages/address-edit/address-edit.json | 4 + .../pages/address-edit/address-edit.wxml | 40 ++++ .../pages/address-edit/address-edit.wxss | 21 ++ .../pages/address-list/address-list.js | 83 ++++++++ .../pages/address-list/address-list.json | 4 + .../pages/address-list/address-list.wxml | 46 ++++ .../pages/address-list/address-list.wxss | 44 ++++ miniprogram/pages/index/index.js | 12 +- miniprogram/pages/index/index.wxml | 2 +- miniprogram/pages/settings/settings.js | 79 ++----- miniprogram/pages/settings/settings.wxml | 26 +-- miniprogram/pages/settings/settings.wxss | 4 + next-env.d.ts | 2 +- 24 files changed, 1159 insertions(+), 141 deletions(-) create mode 100644 app/api/user/addresses/[id]/route.ts create mode 100644 app/api/user/addresses/route.ts create mode 100644 app/my/addresses/[id]/page.tsx create mode 100644 app/my/addresses/new/page.tsx create mode 100644 app/my/addresses/page.tsx create mode 100644 miniprogram/pages/address-edit/address-edit.js create mode 100644 miniprogram/pages/address-edit/address-edit.json create mode 100644 miniprogram/pages/address-edit/address-edit.wxml create mode 100644 miniprogram/pages/address-edit/address-edit.wxss create mode 100644 miniprogram/pages/address-list/address-list.js create mode 100644 miniprogram/pages/address-list/address-list.json create mode 100644 miniprogram/pages/address-list/address-list.wxml create mode 100644 miniprogram/pages/address-list/address-list.wxss diff --git a/app/api/db/init/route.ts b/app/api/db/init/route.ts index bb9eb742..958c75d8 100644 --- a/app/api/db/init/route.ts +++ b/app/api/db/init/route.ts @@ -154,6 +154,30 @@ export async function GET(request: NextRequest) { results.push('✅ 创建system_config表') } + // 6. 用户收货地址表(多地址,类似淘宝) + try { + await query('SELECT 1 FROM user_addresses LIMIT 1') + results.push('✅ user_addresses表已存在') + } catch (e) { + await query(` + CREATE TABLE IF NOT EXISTS user_addresses ( + id VARCHAR(50) PRIMARY KEY, + user_id VARCHAR(50) NOT NULL, + name VARCHAR(50) NOT NULL, + phone VARCHAR(20) NOT NULL, + province VARCHAR(50) NOT NULL, + city VARCHAR(50) NOT NULL, + district VARCHAR(50) NOT NULL, + detail VARCHAR(200) NOT NULL, + is_default TINYINT(1) DEFAULT 0, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX idx_user_id (user_id) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci + `) + results.push('✅ 创建user_addresses表') + } + console.log('[DB Init] 数据库升级完成') return NextResponse.json({ diff --git a/app/api/user/addresses/[id]/route.ts b/app/api/user/addresses/[id]/route.ts new file mode 100644 index 00000000..0e5aa508 --- /dev/null +++ b/app/api/user/addresses/[id]/route.ts @@ -0,0 +1,112 @@ +/** + * 用户收货地址 - 单条详情 / 编辑 / 删除 / 设为默认 + * GET: 详情 + * PUT: 更新(name, phone, detail 等;省/市/区可选) + * DELETE: 删除 + */ + +import { NextRequest, NextResponse } from 'next/server' +import { query } from '@/lib/db' + +async function getOne(id: string) { + const rows = await query( + `SELECT id, user_id, name, phone, province, city, district, detail, is_default, created_at, updated_at + FROM user_addresses WHERE id = ?`, + [id] + ) as any[] + if (!rows || rows.length === 0) return null + const r = rows[0] + return { + id: r.id, + userId: r.user_id, + name: r.name, + phone: r.phone, + province: r.province, + city: r.city, + district: r.district, + detail: r.detail, + isDefault: !!r.is_default, + fullAddress: `${r.province}${r.city}${r.district}${r.detail}`, + createdAt: r.created_at, + updatedAt: r.updated_at, + } +} + +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + try { + const { id } = await params + if (!id) { + return NextResponse.json({ success: false, message: '缺少地址 id' }, { status: 400 }) + } + const item = await getOne(id) + if (!item) { + return NextResponse.json({ success: false, message: '地址不存在' }, { status: 404 }) + } + return NextResponse.json({ success: true, item }) + } catch (e) { + console.error('[Addresses] GET one error:', e) + return NextResponse.json({ success: false, message: '获取地址失败' }, { status: 500 }) + } +} + +export async function PUT( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + try { + const { id } = await params + if (!id) { + return NextResponse.json({ success: false, message: '缺少地址 id' }, { status: 400 }) + } + const body = await request.json() + const { name, phone, province, city, district, detail, isDefault } = body + const existing = await query('SELECT user_id FROM user_addresses WHERE id = ?', [id]) as any[] + if (!existing || existing.length === 0) { + return NextResponse.json({ success: false, message: '地址不存在' }, { status: 404 }) + } + const userId = existing[0].user_id + const updates = [] + const values = [] + if (name !== undefined) { updates.push('name = ?'); values.push(name.trim()) } + if (phone !== undefined) { updates.push('phone = ?'); values.push(phone.trim()) } + if (province !== undefined) { updates.push('province = ?'); values.push((province == null ? '' : String(province)).trim()) } + if (city !== undefined) { updates.push('city = ?'); values.push((city == null ? '' : String(city)).trim()) } + if (district !== undefined) { updates.push('district = ?'); values.push((district == null ? '' : String(district)).trim()) } + if (detail !== undefined) { updates.push('detail = ?'); values.push(detail.trim()) } + if (isDefault === true) { + await query('UPDATE user_addresses SET is_default = 0 WHERE user_id = ?', [userId]) + updates.push('is_default = 1') + } else if (isDefault === false) { + updates.push('is_default = 0') + } + if (updates.length > 0) { + values.push(id) + await query(`UPDATE user_addresses SET ${updates.join(', ')}, updated_at = NOW() WHERE id = ?`, values) + } + const item = await getOne(id) + return NextResponse.json({ success: true, item, message: '更新成功' }) + } catch (e) { + console.error('[Addresses] PUT error:', e) + return NextResponse.json({ success: false, message: '更新地址失败' }, { status: 500 }) + } +} + +export async function DELETE( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + try { + const { id } = await params + if (!id) { + return NextResponse.json({ success: false, message: '缺少地址 id' }, { status: 400 }) + } + await query('DELETE FROM user_addresses WHERE id = ?', [id]) + return NextResponse.json({ success: true, message: '删除成功' }) + } catch (e) { + console.error('[Addresses] DELETE error:', e) + return NextResponse.json({ success: false, message: '删除地址失败' }, { status: 500 }) + } +} diff --git a/app/api/user/addresses/route.ts b/app/api/user/addresses/route.ts new file mode 100644 index 00000000..4db007b5 --- /dev/null +++ b/app/api/user/addresses/route.ts @@ -0,0 +1,68 @@ +/** + * 用户收货地址 - 列表与新建 + * GET: 列表(需 userId) + * POST: 新建(必填 userId, name, phone, detail;省/市/区可选) + */ + +import { NextRequest, NextResponse } from 'next/server' +import { query } from '@/lib/db' +import { randomUUID } from 'crypto' + +export async function GET(request: NextRequest) { + try { + const userId = request.nextUrl.searchParams.get('userId') + if (!userId) { + return NextResponse.json({ success: false, message: '缺少 userId' }, { status: 400 }) + } + const rows = await query( + `SELECT id, user_id, name, phone, province, city, district, detail, is_default, created_at, updated_at + FROM user_addresses WHERE user_id = ? ORDER BY is_default DESC, updated_at DESC`, + [userId] + ) as any[] + const list = (rows || []).map((r) => ({ + id: r.id, + userId: r.user_id, + name: r.name, + phone: r.phone, + province: r.province, + city: r.city, + district: r.district, + detail: r.detail, + isDefault: !!r.is_default, + fullAddress: `${r.province}${r.city}${r.district}${r.detail}`, + createdAt: r.created_at, + updatedAt: r.updated_at, + })) + return NextResponse.json({ success: true, list }) + } catch (e) { + console.error('[Addresses] GET error:', e) + return NextResponse.json({ success: false, message: '获取地址列表失败' }, { status: 500 }) + } +} + +export async function POST(request: NextRequest) { + try { + const body = await request.json() + const { userId, name, phone, province, city, district, detail, isDefault } = body + if (!userId || !name || !phone || !detail) { + return NextResponse.json( + { success: false, message: '缺少必填项:userId, name, phone, detail' }, + { status: 400 } + ) + } + const id = randomUUID().replace(/-/g, '').slice(0, 24) + const p = (v: string | undefined) => (v == null ? '' : String(v).trim()) + if (isDefault) { + await query('UPDATE user_addresses SET is_default = 0 WHERE user_id = ?', [userId]) + } + await query( + `INSERT INTO user_addresses (id, user_id, name, phone, province, city, district, detail, is_default) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, + [id, userId, name.trim(), phone.trim(), p(province), p(city), p(district), detail.trim(), isDefault ? 1 : 0] + ) + return NextResponse.json({ success: true, id, message: '添加成功' }) + } catch (e) { + console.error('[Addresses] POST error:', e) + return NextResponse.json({ success: false, message: '添加地址失败' }, { status: 500 }) + } +} diff --git a/app/chapters/page.tsx b/app/chapters/page.tsx index 0230ba31..a92a1174 100644 --- a/app/chapters/page.tsx +++ b/app/chapters/page.tsx @@ -2,10 +2,11 @@ import { useState } from "react" import { useRouter } from "next/navigation" -import { ChevronRight, Lock, Unlock, Book, BookOpen, Home, List, Sparkles, User, Users, Zap, Crown, Search } from "lucide-react" +import { ChevronRight, Lock, Unlock, Book, BookOpen, Sparkles, Zap, Crown, Search } from "lucide-react" import { useStore } from "@/lib/store" import { bookData, getTotalSectionCount, specialSections, getPremiumBookPrice, getExtraSectionsCount, BASE_SECTIONS_COUNT } from "@/lib/book-data" import { SearchModal } from "@/components/search-modal" +import { BottomNav } from "@/components/bottom-nav" export default function ChaptersPage() { const router = useRouter() @@ -209,31 +210,7 @@ export default function ChaptersPage() { - + ) } diff --git a/app/my/addresses/[id]/page.tsx b/app/my/addresses/[id]/page.tsx new file mode 100644 index 00000000..70aa07d7 --- /dev/null +++ b/app/my/addresses/[id]/page.tsx @@ -0,0 +1,196 @@ +"use client" + +import { useState, useEffect } from "react" +import { useRouter, useParams } from "next/navigation" +import { ChevronLeft } from "lucide-react" +import { useStore } from "@/lib/store" + +export default function EditAddressPage() { + const router = useRouter() + const params = useParams() + const id = params.id as string + const { user } = useStore() + const [loading, setLoading] = useState(false) + const [fetching, setFetching] = useState(true) + const [name, setName] = useState("") + const [phone, setPhone] = useState("") + const [province, setProvince] = useState("") + const [city, setCity] = useState("") + const [district, setDistrict] = useState("") + const [detail, setDetail] = useState("") + const [isDefault, setIsDefault] = useState(false) + + useEffect(() => { + if (!id) { + setFetching(false) + return + } + fetch(`/api/user/addresses/${id}`) + .then((res) => res.json()) + .then((data) => { + if (data.success && data.item) { + const item = data.item + setName(item.name) + setPhone(item.phone) + setProvince(item.province) + setCity(item.city) + setDistrict(item.district) + setDetail(item.detail) + setIsDefault(item.isDefault) + } + setFetching(false) + }) + .catch(() => setFetching(false)) + }, [id]) + + if (!user?.id) { + return ( +
+

请先登录

+
+ ) + } + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + if (!name.trim()) { + alert("请输入收货人姓名") + return + } + if (!/^1[3-9]\d{9}$/.test(phone)) { + alert("请输入正确的手机号") + return + } + // 省/市/区为选填 + if (!detail.trim()) { + alert("请输入详细地址") + return + } + setLoading(true) + try { + const res = await fetch(`/api/user/addresses/${id}`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + name: name.trim(), + phone: phone.trim(), + province: (province ?? "").trim(), + city: (city ?? "").trim(), + district: (district ?? "").trim(), + detail: detail.trim(), + isDefault, + }), + }) + const data = await res.json() + if (data.success) { + router.push("/my/addresses") + } else { + alert(data.message || "保存失败") + } + } catch { + alert("保存失败") + } finally { + setLoading(false) + } + } + + if (fetching) { + return ( +
+
加载中...
+
+ ) + } + + return ( +
+
+
+ +

编辑地址

+
+
+
+ +
+
+
+ + setName(e.target.value)} + placeholder="请输入收货人姓名" + className="flex-1 bg-transparent text-white text-sm text-right placeholder-white/30 outline-none" + /> +
+
+ + setPhone(e.target.value.replace(/\D/g, ""))} + placeholder="请输入手机号" + className="flex-1 bg-transparent text-white text-sm text-right placeholder-white/30 outline-none" + /> +
+
+ +
+ setProvince(e.target.value)} + placeholder="省" + className="flex-1 max-w-24 bg-transparent text-white text-sm text-right placeholder-white/30 outline-none" + /> + setCity(e.target.value)} + placeholder="市" + className="flex-1 max-w-24 bg-transparent text-white text-sm text-right placeholder-white/30 outline-none" + /> + setDistrict(e.target.value)} + placeholder="区" + className="flex-1 max-w-24 bg-transparent text-white text-sm text-right placeholder-white/30 outline-none" + /> +
+
+
+ +