添加登录和后端校验

完成后端设计(未在本仓库体现),通过安全的手段完成了登录鉴权
This commit is contained in:
2026-08-06 16:27:36 +08:00
commit b19a56003f
286 changed files with 51650 additions and 0 deletions
+90
View File
@@ -0,0 +1,90 @@
import http, { setToken, clearToken, getToken } from './request'
export { getToken }
/**
* 后端 API 方法集合
* 对应 wxmp_backend 的路由(见后端 Swagger /docs
* 说明:登录会调用后端 /api/auth/login,成功后把 accessToken 存入本地,
* 供后续请求自动携带。购物车/订单等仍可用本地 store.ts 作为离线兜底。
*/
export interface LoginResult {
accessToken: string
isNewUser: boolean // 首次登录(未设资料)为 true,前端需引导补填头像/姓名
nickname: string | null
avatar: string | null
}
/**
* 登录:把 wx.login() 的 code 交给后端,后端用 code2Session 换 openid
* 自动注册/续登并签发 accessToken(个人主体无需手机号)。
* 返回 isNewUser 供前端判断是否需补全资料。
*/
export async function login(code: string): Promise<LoginResult> {
const data = await http.post<LoginResult>('/api/auth/login', { code }, { auth: false })
if (data?.accessToken) {
setToken(data.accessToken)
}
return data
}
/** 更新当前用户资料(昵称/头像),新用户在补填后提交到后端持久化 */
export async function updateProfile(profile: { nickname?: string; avatar?: string }): Promise<UserProfile> {
return http.patch<UserProfile>('/api/users/me', profile)
}
/**
* 首次注册:用 registerTicket + wx.getPhoneNumber 的 code 验证手机号,完成后自动登录
*/
export async function registerWithPhone(
registerTicket: string,
phoneCode: string,
profile?: { nickname?: string; avatar?: string },
): Promise<{ accessToken: string }> {
const data = await http.post<{ accessToken: string }>(
'/api/auth/register',
{ registerTicket, phoneCode, ...profile },
{ auth: false },
)
setToken(data.accessToken)
return data
}
/** 登出:仅清除本地 token(后端 JWT 无状态,无需撤销) */
export function logout(): void {
clearToken()
}
/** 后端用户信息(对应 User 模型;openpid 为主标识,openid 可空) */
export interface UserProfile {
id: string
openpid?: string | null
openid?: string | null
unionid?: string | null
nickname?: string | null
avatar?: string | null
phone?: string | null
createdAt?: string
updatedAt?: string
}
/** 获取当前登录用户信息 */
export async function getMe(): Promise<UserProfile> {
return http.get<UserProfile>('/api/users/me')
}
/** 商品列表 */
export async function fetchProducts() {
return http.get('/api/products', { auth: false })
}
/** 商品详情 */
export async function fetchProduct(id: string) {
return http.get(`/api/products/${id}`, { auth: false })
}
/** 分类列表 */
export async function fetchCategories() {
return http.get('/api/categories', { auth: false })
}
+204
View File
@@ -0,0 +1,204 @@
/**
* 产品品类配置文件
*
* ⚠️ 重要:当实际产品尺寸确定后,请修改本文件中的 mask 参数
* 修改后页面会自动生效,无需改动其他代码
*/
import { ProductCategory } from '../types'
export const PRODUCTS: ProductCategory[] = [
{
id: 'notebook-small',
name: '微雕笔记本(小)',
desc: '便携随行,记录点滴',
icon: '📓',
iconImg: '/icon/书本.png',
price: 12,
originalPrice: 18,
leadTime: '3-5个工作日',
description:
'精选优质纸张,封面采用高档PU材质,手感细腻。内页可定制横线、方格或空白三种版式。激光微雕工艺将名字或图案精准刻印在封面,永不褪色。小巧轻便,适合随身携带,无论是课堂笔记还是日常备忘,都是你的专属伴侣。',
subtitle: '掌心间的专属印记 · 激光微雕 · 三色内页可选',
tone: [28, 22, 18],
story: '灵感源自手账文化中对书写仪式感的追求。我们相信,在数字洪流的时代,纸笔的温度依然不可替代。当激光微雕在PU封面上留下专属印记,每一笔书写都在与过去对话。笔记本小巧到足以放入任何口袋,却承载着独一无二的记忆。',
scene: '课堂笔记、日常备忘、旅行手账、心情日记。适合送给同学、伴侣或作为活动伴手礼——让每一个人都拥有刻着自己名字的专属笔记本。',
tags: ['伴手礼', '学生', '手账', '便携', '激光微雕'],
specs: [
['尺寸', '105 × 148mmA6'],
['页数', '120张/240页'],
['内页版式', '横线 / 方格 / 空白 可选'],
['封面材质', '高档PU皮革'],
['雕刻工艺', 'CO2 激光微雕(永不褪色)'],
['适用场景', '课堂、旅行、日常备忘']
],
images: ['/img/book_small/The1.jpg'],
mask: {
shape: 'rect',
width: 300,
height: 420,
borderRadius: 8
}
},
{
id: 'notebook-large',
name: '微雕笔记本(大)',
desc: '大开本,更多创作空间',
icon: '📔',
iconImg: '/icon/书本_大.png',
price: 45,
originalPrice: 58,
leadTime: '3-5个工作日',
description:
'A4尺寸大开本,180°平摊设计,书写更自由。封面可选真皮、仿布纹或磨砂材质,激光微雕区域更大,适合呈现全班名字、团队口号或公司Logo等大篇幅内容。是毕业纪念、企业年会礼品的首选。',
subtitle: '大篇幅定制首选 · 180°平摊 · 毕业/团建必备',
tone: [35, 28, 22],
story: '每一次告别都值得被郑重书写。毕业季的教室里,笔记本摊开在课桌上,全班的名字被激光刻印在封面中央,成为永恒的同学录。180°平摊设计让书写不再被订装束缚,A4大尺寸给你尽情挥洒的天地。这是送给一群人最好的告别礼物。',
scene: '毕业纪念册、公司年会礼品、团队建设伴手礼、客户答谢。特别适合需要在封面呈现大量文字或图案的定制需求——班级名单、公司愿景、团队口号一网打尽。',
tags: ['毕业礼', '团建', '企业定制', '大篇幅', '平摊设计'],
specs: [
['尺寸', '210 × 297mmA4'],
['页数', '100张/200页'],
['封面材质', '真皮 / 仿布纹 / 磨砂 可选'],
['开合适用', '180°平摊设计'],
['雕刻工艺', 'CO2 激光微雕(高清晰度)'],
['定制面积', '封面整面可雕']
],
images: ['/img/book_big/The1.jpg', '/img/book_big/The2.jpg'],
mask: {
shape: 'rect',
width: 340,
height: 480,
borderRadius: 8
}
},
{
id: 'coaster',
name: '铜质杯垫',
desc: '金属质感,桌面艺术',
icon: '☕',
iconImg: '/icon/杯子.png',
price: 25,
originalPrice: 35,
leadTime: '5-7个工作日',
description:
'采用优质黄铜材质,表面经拉丝工艺处理,呈现哑光金属质感。背面配有防滑软木垫,保护桌面同时防止滑动。激光微雕可在表面刻印名字、日期或祝福语,是乔迁新居、新婚贺礼的精致之选。',
subtitle: '黄铜质感 · 拉丝哑光 · 桌面上的微型雕塑',
tone: [42, 30, 18],
story: '设计灵感来自日式侘寂美学。岁月会在黄铜表面留下温柔的氧化痕迹,就如同一杯茶从热到凉的过程。拉丝处理后的哑光表面在灯光下呈现细腻的金属纹理,激光微雕的名字成为这件器物上最冷静的注脚。它不只是杯垫,更是桌面上的微型雕塑。',
scene: '乔迁送礼、新婚贺礼、咖啡角布置、办公桌美学。适合送给喜欢手冲咖啡的朋友,或作为婚礼宾客的回礼——每一只杯垫上都刻着宾客的名字,让礼物本身成为座位卡。',
tags: ['乔迁礼', '新婚贺礼', '桌面美学', '黄铜', '手冲咖啡'],
specs: [
['材质', '黄铜 + 背面软木防滑垫'],
['表面处理', '拉丝哑光'],
['直径', '100mm'],
['厚度', '3mm'],
['雕刻工艺', '光纤激光精细微雕'],
['耐温', '≤120°C']
],
images: ['/img/cup/The1.jpg'],
mask: {
shape: 'circle',
width: 280,
height: 280
}
},
{
id: 'penbox',
name: '竹制笔盒',
desc: '自然竹纹,文房雅器',
icon: '✏️',
iconImg: '/icon/笔盒.png',
price: 75,
originalPrice: 98,
leadTime: '7-10个工作日',
description:
'精选天然楠竹,经多道工序打磨抛光,保留天然竹纹肌理。盒盖采用磁吸开合,内部设有分层隔板。激光微雕在竹面上呈现深浅不一的雕刻效果,融入墨香气质。适合书房案头、书法爱好者的文房伴侣。',
subtitle: '天然楠竹 · 磁吸开合 · 文房雅器之选',
tone: [30, 24, 16],
story: '每一只笔盒都来自同一片竹林的不同部位,因此纹路绝不重复——正如没有两个人的书法风格完全相同。我们保留了楠竹天然的温润触感,只在表面以激光刻下名字或一句箴言。打开磁吸盒盖时的清脆声响,是文房中的小小仪式。',
scene: '书房案头、书法爱好者装备、教师节礼物、书房暖房礼。适合送给写字好看的人——因为懂书写的人,才懂一只好笔盒的分量。',
tags: ['文房', '书法', '教师节', '书房', '天然材质'],
specs: [
['材质', '天然楠竹'],
['开合方式', '磁吸式盒盖'],
['内部结构', '分层隔板(可调节)'],
['尺寸', '200 × 65 × 30mm'],
['表面处理', '手工打磨抛光,无漆'],
['雕刻工艺', 'CO2 激光烧蚀(深浅可调)']
],
images: ['/img/penbox/The1.jpg', '/img/penbox/The2.jpg', '/img/penbox/The3.jpg'],
mask: {
shape: 'rect',
width: 320,
height: 160,
borderRadius: 12
}
},
{
id: 'booklamp',
name: '书本型灯',
desc: '温暖光影,点亮心意',
icon: '💡',
iconImg: '/icon/书灯.png',
price: 45,
originalPrice: 68,
leadTime: '5-7个工作日',
description:
'开合式设计,打开即亮,合拢即灭,如魔法书般充满惊喜。采用LED暖光灯源,光线柔和不刺眼,可持续点亮6-8小时。封面激光微雕可定制文字、图案或照片,让温暖的灯光承载专属记忆。是情侣礼物、毕业礼物的创意之选。',
subtitle: '开合即亮 · LED暖光 · 魔法书般的惊喜',
tone: [25, 20, 15],
story: '灵感来自《哈利·波特》中的魔法课本——打开一本书,光芒倾泻而出。我们采用杜邦纸与实木封面结合,让灯体既轻薄又坚固。激光微雕在封面留下你的名字或一幅小画,当灯在深夜亮起,那是专属于你的温暖角落。',
scene: '情侣礼物、毕业纪念、床头夜灯、露营氛围灯、儿童房。特别适合在封面刻上两个人的名字和一句情话,在情人节或纪念日送出——打开书灯,就像打开一封会发光的情书。',
tags: ['情侣礼', '毕业纪念', '夜灯', '氛围灯', '创意礼物'],
specs: [
['光源', 'LED 暖白光(3000K'],
['续航', '6-8小时(内置可充电锂电池)'],
['充电', 'USB Type-C 接口'],
['封面材质', '杜邦纸 + 实木封面'],
['展开尺寸', '210 × 150mm'],
['雕刻工艺', '激光微雕(支持文字+简单图案)']
],
images: ['/img/booklight/The1.jpg', '/img/booklight/The2.jpg'],
mask: {
shape: 'rect',
width: 320,
height: 240,
borderRadius: 4
}
}
]
/** CATEGORIES 为 PRODUCTS 的别名,供页面引用 */
export const CATEGORIES = PRODUCTS
/**
* 根据产品ID查找品类配置
*/
export const getProductById = (id: string): ProductCategory | undefined => {
return PRODUCTS.find(p => p.id === id)
}
/**
* 产品 ID 到 icon 图片路径的映射
* 用于兼容旧数据(旧数据 productIcon 存的是 emoji
*/
export const PRODUCT_ICON_MAP: Record<string, string> = {
'notebook-small': '/icon/书本.png',
'notebook-large': '/icon/书本_大.png',
'coaster': '/icon/杯子.png',
'penbox': '/icon/笔盒.png',
'booklamp': '/icon/书灯.png'
}
/**
* 获取商品对应的 icon 图片路径
* - 新数据:直接返回 product.iconImg
* - 旧数据:通过 productId 查映射表
* - 兜底:返回四角星
*/
export function getProductIconImg(product: any): string {
if (product?.iconImg) return product.iconImg
if (product?.id) return PRODUCT_ICON_MAP[product.id] || '/icon/四角星.png'
return '/icon/四角星.png'
}
+126
View File
@@ -0,0 +1,126 @@
import Taro from '@tarojs/taro'
/**
* 后端接口请求封装
* 后端统一响应:{ code, message, data }code === 0 表示成功
* token 通过 Authorization: Bearer <token> 传入
*/
export const BASE_URL = 'https://wxbackend.tokenleaping.com'
const TOKEN_KEY = 'smart_access_token'
/** 安全隐藏 loading:没有活动 loading 时调用 hideLoading 会抛 hideToast:fail,这里吞掉 */
export function safeHideLoading(): void {
try { Taro.hideLoading() } catch { /* ignore */ }
}
// ---------- token 存取 ----------
export function getToken(): string {
try {
return Taro.getStorageSync(TOKEN_KEY) || ''
} catch {
return ''
}
}
export function setToken(token: string): void {
Taro.setStorageSync(TOKEN_KEY, token)
}
export function clearToken(): void {
try {
Taro.removeStorageSync(TOKEN_KEY)
} catch {
/* ignore */
}
}
// ---------- 类型 ----------
interface ApiResponse<T = unknown> {
code: number
message: string
data: T
}
export interface RequestOptions {
method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'
data?: unknown
/** 额外请求头 */
headers?: Record<string, string>
/** 是否自动携带 token,默认 true */
auth?: boolean
}
// ---------- request ----------
/**
* 发请求。成功(code===0)返回 data;业务失败抛 Error(message)HTTP 401 清 token 并抛错。
* 调用方可自行 catch 或用 onUnauthorized 钩子统一处理跳登录。
*/
export async function request<T = unknown>(
path: string,
options: RequestOptions = {},
): Promise<T> {
const { method = 'GET', data, headers = {}, auth = true } = options
const token = getToken()
const finalHeaders: Record<string, string> = {
'Content-Type': 'application/json',
...(auth && token ? { Authorization: `Bearer ${token}` } : {}),
...headers,
}
let res: Taro.request.SuccessCallbackResult
try {
res = await Taro.request({
url: `${BASE_URL}${path}`,
method,
data,
header: finalHeaders,
})
} catch (e) {
// 网络层异常
throw new Error('网络异常,请稍后重试')
}
// HTTP 未授权:清 token,交回调处理
if (res.statusCode === 401) {
clearToken()
onUnauthorized?.()
throw new Error('登录已过期,请重新登录')
}
const body = res.data as ApiResponse<T>
// 后端业务码非 0:当作错误抛出
if (body && typeof body.code === 'number' && body.code !== 0) {
throw new Error(body.message || '请求失败')
}
return (body && typeof body.code === 'number' ? body.data : body) as T
}
/** 401 钩子,由业务方注册(如跳转登录页) */
export let onUnauthorized: (() => void) | null = null
export function setOnUnauthorized(handler: () => void): void {
onUnauthorized = handler
}
// ---------- 便捷方法 ----------
export const http = {
get: <T = unknown>(path: string, options?: RequestOptions) =>
request<T>(path, { ...options, method: 'GET' }),
post: <T = unknown>(path: string, data?: unknown, options?: RequestOptions) =>
request<T>(path, { ...options, method: 'POST', data }),
put: <T = unknown>(path: string, data?: unknown, options?: RequestOptions) =>
request<T>(path, { ...options, method: 'PUT', data }),
patch: <T = unknown>(path: string, data?: unknown, options?: RequestOptions) =>
request<T>(path, { ...options, method: 'PATCH', data }),
del: <T = unknown>(path: string, options?: RequestOptions) =>
request<T>(path, { ...options, method: 'DELETE' }),
}
export default http
+308
View File
@@ -0,0 +1,308 @@
import Taro from '@tarojs/taro'
import type { DesignItem, OrderItem, AddressItem, StickerItem } from '../types'
import { PRODUCT_ICON_MAP } from './productConfig'
// 重新导出类型,供各页面从 store 直接引用(修正原「声明但不导出」的编译错误)
export type { DesignItem, OrderItem, AddressItem, StickerItem }
const THEME_KEY = 'smart_theme'
const USER_KEY = 'smart_user_info'
const ACTIVE_USER_KEY = 'smart_active_openid'
/** 判断 openid 是否为历史版 mock 残留(形如 mock_xxx */
export function isMockOpenid(openid?: string | null): boolean {
return !!openid && (openid.startsWith('mock_') || openid.startsWith('mock-'))
}
// ---------- 当前用户 openid 管理 ----------
function getActiveOpenid(): string {
const user = getUserInfoRaw()
const openid = user?.openid || Taro.getStorageSync(ACTIVE_USER_KEY) || '_guest_'
Taro.setStorageSync(ACTIVE_USER_KEY, openid)
return openid
}
function key(scope: string): string {
const prefix = getActiveOpenid()
return `${scope}_${prefix}`
}
// ---------- 用户数据 ----------
const USER_REGISTRY_KEY = 'smart_user_registry'
const ADMIN_PASSWORD = 'zhihui2024'
// ---------- 用户注册表 ----------
function getUserRegistry(): string[] {
try { return Taro.getStorageSync(USER_REGISTRY_KEY) || [] } catch { return [] }
}
function saveToRegistry(openid: string) {
const list = getUserRegistry()
if (!list.includes(openid)) {
list.push(openid)
Taro.setStorageSync(USER_REGISTRY_KEY, list)
}
}
function removeFromRegistry(openid: string) {
const list = getUserRegistry().filter(id => id !== openid)
Taro.setStorageSync(USER_REGISTRY_KEY, list)
}
// ---------- 用户数据 ----------
export function getUserInfoRaw(): any {
try { return Taro.getStorageSync(USER_KEY) } catch { return null }
}
export function setUserInfoRaw(info: any) {
Taro.setStorageSync(USER_KEY, info)
if (info?.openid) {
Taro.setStorageSync(ACTIVE_USER_KEY, info.openid)
// 为每个用户备份独立副本,方便切换账号时读取
Taro.setStorageSync(`user_info_${info.openid}`, info)
saveToRegistry(info.openid)
}
}
export function getUserInfoByOpenid(openid: string): any | null {
try {
const backup = Taro.getStorageSync(`user_info_${openid}`)
if (backup) return backup
} catch {}
const current = getUserInfoRaw()
if (current?.openid === openid) return current
return null
}
export function clearUserInfo() {
// 仅退出登录,不删除用户数据
Taro.removeStorageSync(USER_KEY)
Taro.removeStorageSync(ACTIVE_USER_KEY)
}
export function setUserInfo(info: any) {
setUserInfoRaw(info)
}
export function getUserInfo() {
return getUserInfoRaw()
}
// ---------- 用户数据库管理 ----------
export function listAllUsers() {
const registry = getUserRegistry()
return registry.map(openid => {
const info = getUserInfoByOpenid(openid)
const dList = getDesignListFor(openid)
const oList = getOrderListFor(openid)
return {
openid,
nickName: info?.nickName || '未知用户',
avatarUrl: info?.avatarUrl || '',
loginAt: info?.loginAt || 0,
designCount: dList.length,
orderCount: oList.length
}
})
}
export function createUser(nickName: string, avatarUrl?: string) {
const openid = 'mock_' + Date.now().toString(36) + '_' + Math.random().toString(36).slice(2, 6)
const info = { openid, nickName: nickName || '微信用户', avatarUrl: avatarUrl || '', loginAt: Date.now() }
setUserInfoRaw(info)
return info
}
export function switchUser(openid: string) {
const info = getUserInfoByOpenid(openid)
if (!info) return false
Taro.setStorageSync(USER_KEY, info)
Taro.setStorageSync(ACTIVE_USER_KEY, openid)
return true
}
export function deleteUser(openid: string) {
// 删除该用户的所有数据
Taro.removeStorageSync(`user_info_${openid}`)
Taro.removeStorageSync(`design_list_${openid}`)
Taro.removeStorageSync(`order_list_${openid}`)
Taro.removeStorageSync(`address_list_${openid}`)
removeFromRegistry(openid)
// 如果删的是当前登录用户,清掉登录态
const current = getUserInfoRaw()
if (current?.openid === openid) {
clearUserInfo()
}
}
export function verifyAdminPassword(password: string): boolean {
return password === ADMIN_PASSWORD
}
// ---------- 跨用户读取辅助函数 ----------
function keyFor(scope: string, openid: string) {
return `${scope}_${openid}`
}
function getDesignListFor(openid: string): DesignItem[] {
try { return Taro.getStorageSync(keyFor('design_list', openid)) || [] } catch { return [] }
}
function getOrderListFor(openid: string): OrderItem[] {
try { return Taro.getStorageSync(keyFor('order_list', openid)) || [] } catch { return [] }
}
// ---------- 设计清单 ----------
export function getDesignList(): DesignItem[] {
try { return Taro.getStorageSync(key('design_list')) || [] } catch { return [] }
}
export function setDesignList(list: DesignItem[]) {
Taro.setStorageSync(key('design_list'), list)
}
export function addDesign(product: any, count: number): DesignItem {
const list = getDesignList()
const iconImg = product?.iconImg || PRODUCT_ICON_MAP[product?.id] || '/icon/四角星.png'
const item: DesignItem = {
id: 'DSG' + Date.now(),
productId: product.id,
productName: product.name,
productIcon: iconImg,
unitPrice: product.price,
count,
status: 'undesigned',
createdAt: new Date().toISOString().slice(0, 10)
}
setDesignList([...list, item])
return item
}
export function updateDesign(id: string, patch: Partial<DesignItem>) {
const list = getDesignList()
const idx = list.findIndex(d => d.id === id)
if (idx === -1) return
list[idx] = { ...list[idx], ...patch }
setDesignList(list)
}
/** 批量删除设计条目 */
export function removeDesigns(ids: string[]) {
const list = getDesignList().filter(d => !ids.includes(d.id))
setDesignList(list)
}
/** 根据ID删除单条设计 */
export function removeDesign(id: string) {
const list = getDesignList().filter(d => d.id !== id)
setDesignList(list)
}
// ---------- 订单 ----------
export function getOrderList(): OrderItem[] {
try { return Taro.getStorageSync(key('order_list')) || [] } catch { return [] }
}
export function setOrderList(list: OrderItem[]) {
Taro.setStorageSync(key('order_list'), list)
}
export function designToOrder(designId: string): OrderItem | null {
const dList = getDesignList()
const oList = getOrderList()
const design = dList.find(d => d.id === designId)
if (!design) return null
const iconImg = design.productIcon?.startsWith('/icon/')
? design.productIcon
: PRODUCT_ICON_MAP[design.productId] || '/icon/四角星.png'
const order: OrderItem = {
id: 'ORD' + Date.now().toString().slice(-9),
productName: design.productName,
productIcon: iconImg,
statusCode: 'pending',
date: new Date().toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }).replace(/\//g, '-'),
price: '¥' + (design.unitPrice * design.count).toFixed(2),
count: design.count,
sku: `${design.productName} × ${design.count}`
}
const dIdx = dList.findIndex(d => d.id === designId)
if (dIdx !== -1) {
dList[dIdx].status = 'ordered'
dList[dIdx].orderId = order.id
}
setDesignList(dList)
setOrderList([order, ...oList])
return order
}
// ---------- 主题 ----------
export type ThemeMode = 'light' | 'dark' | 'auto'
export function getTheme(): ThemeMode {
try { return Taro.getStorageSync(THEME_KEY) || 'auto' } catch { return 'auto' }
}
export function setTheme(theme: ThemeMode) {
Taro.setStorageSync(THEME_KEY, theme)
}
/** 根据自动模式解析实际生效主题 */
export function resolveTheme(mode: ThemeMode): 'light' | 'dark' {
if (mode === 'auto') {
const info = Taro.getSystemInfoSync()
return info.theme === 'dark' ? 'dark' : 'light'
}
return mode
}
// ---------- 收货地址 ----------
export function getAddressList(): AddressItem[] {
try { return Taro.getStorageSync(key('address_list')) || [] } catch { return [] }
}
export function setAddressList(list: AddressItem[]) {
Taro.setStorageSync(key('address_list'), list)
}
export function addAddress(addr: Omit<AddressItem, 'id'>): AddressItem {
const list = getAddressList()
const item: AddressItem = { ...addr, id: 'ADR' + Date.now() }
// 若设为默认,取消其他默认
if (item.isDefault) {
list.forEach(a => { a.isDefault = false })
}
setAddressList([item, ...list])
return item
}
export function updateAddress(id: string, patch: Partial<AddressItem>) {
const list = getAddressList()
const idx = list.findIndex(a => a.id === id)
if (idx === -1) return
if (patch.isDefault) list.forEach(a => { a.isDefault = false })
list[idx] = { ...list[idx], ...patch }
setAddressList(list)
}
export function deleteAddress(id: string) {
const list = getAddressList().filter(a => a.id !== id)
setAddressList(list)
}
export function getDefaultAddress(): AddressItem | undefined {
return getAddressList().find(a => a.isDefault)
}
+24
View File
@@ -0,0 +1,24 @@
import Taro from '@tarojs/taro'
/** 实际生效主题对应的窗口/页面背景色 */
export function themePageColor(theme: 'light' | 'dark'): string {
return theme === 'dark' ? '#1e1e2f' : '#ffffff'
}
/**
* 同步窗口/页面背景到当前主题:
* - 自定义导航区(状态栏下方、胶囊按钮区域)在 iOS 上由 backgroundColorTop 控制,
* 仅设 backgroundColor 覆盖不到,因此需要同时设置三项
* - 下拉背景文字样式一并切换,避免深色模式下白字看不清
*
* @param bgOverride 传入时使用该背景色(用于强制顶部深色的页面,如商品详情)
*/
export function applyPageBackground(theme: 'light' | 'dark', bgOverride?: string) {
const bg = bgOverride ?? themePageColor(theme)
Taro.setBackgroundColor({
backgroundColor: bg,
backgroundColorTop: bg,
backgroundColorBottom: bg
})
Taro.setBackgroundTextStyle({ textStyle: theme === 'dark' ? 'light' : 'dark' })
}