添加登录和后端校验
完成后端设计(未在本仓库体现),通过安全的手段完成了登录鉴权
This commit is contained in:
@@ -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 })
|
||||
}
|
||||
@@ -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 × 148mm(A6)'],
|
||||
['页数', '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 × 297mm(A4)'],
|
||||
['页数', '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'
|
||||
}
|
||||
@@ -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
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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' })
|
||||
}
|
||||
Reference in New Issue
Block a user