feat: split DA and API layers by domain, add parallel route docs
This commit is contained in:
@@ -1,90 +0,0 @@
|
||||
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,7 @@
|
||||
/**
|
||||
* R2 收货地址接口归属文件。
|
||||
* 后端 /api/addresses CRUD 就绪后,在这里补充:
|
||||
* fetchAddresses / createAddress / updateAddress / deleteAddress / setDefaultAddress
|
||||
* 返回类型优先直接对应 src/types/index.ts 的 AddressItem 契约。
|
||||
*/
|
||||
export {}
|
||||
@@ -0,0 +1,45 @@
|
||||
import http, { clearToken, getToken, setToken } from '../request'
|
||||
|
||||
export { getToken }
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
/**
|
||||
* 首次注册:用 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()
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* R2 设计清单接口归属文件。
|
||||
* 后端 /api/design-list CRUD 就绪后,在这里补充:
|
||||
* fetchDesignList / createDesign / updateDesign / deleteDesign
|
||||
* 设计数据(贴纸、掩膜、分类信息)以 JSON 方式随 items/designData 提交。
|
||||
*/
|
||||
export {}
|
||||
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* API 层聚合入口:页面统一从 '../../utils/api' 导入。
|
||||
* 新接口写在 api/<domain>.ts,本文件只负责 re-export,不要往单文件堆代码。
|
||||
*/
|
||||
export * from './auth'
|
||||
export * from './user'
|
||||
export * from './product'
|
||||
export * from './address'
|
||||
export * from './design'
|
||||
export * from './order'
|
||||
export * from './upload'
|
||||
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* R3 订单接口归属文件。
|
||||
* 后端 /api/orders 就绪后,在这里补充:
|
||||
* createOrder / fetchOrders / fetchOrderDetail / payOrder
|
||||
* 金额一律以服务端重算结果为准,前端只提交商品/设计数据与地址快照。
|
||||
*/
|
||||
export {}
|
||||
@@ -0,0 +1,16 @@
|
||||
import http from '../request'
|
||||
|
||||
/** 商品列表 */
|
||||
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,7 @@
|
||||
/**
|
||||
* R4 上传接口归属文件。
|
||||
* 后端 COS/wordcloud 适配器就绪后,在这里补充:
|
||||
* getUploadCredentials / uploadImage / generateWordCloud / getWordCloudJob / sketchImage
|
||||
* 词云任务使用“创建任务 -> 轮询状态 -> 取结果链接”的异步模型。
|
||||
*/
|
||||
export {}
|
||||
@@ -0,0 +1,24 @@
|
||||
import http from '../request'
|
||||
|
||||
/** 后端用户信息(对应 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 updateProfile(profile: { nickname?: string; avatar?: string }): Promise<UserProfile> {
|
||||
return http.patch<UserProfile>('/api/users/me', profile)
|
||||
}
|
||||
|
||||
/** 获取当前登录用户信息 */
|
||||
export async function getMe(): Promise<UserProfile> {
|
||||
return http.get<UserProfile>('/api/users/me')
|
||||
}
|
||||
@@ -1,328 +0,0 @@
|
||||
import Taro from '@tarojs/taro'
|
||||
import type { DesignItem, OrderItem, AddressItem, StickerItem } from '../types'
|
||||
import { PRODUCT_ICON_MAP } from './productConfig'
|
||||
import { assetUrl } from './asset'
|
||||
|
||||
import { invalidateAuthCache } from './authState'
|
||||
// 重新导出类型,供各页面从 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)
|
||||
}
|
||||
invalidateAuthCache()
|
||||
Taro.eventCenter?.trigger('authStateChanged', { openid: 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)
|
||||
invalidateAuthCache()
|
||||
Taro.eventCenter?.trigger('authStateChanged', { openid: '' })
|
||||
}
|
||||
|
||||
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 = assetUrl(product?.iconImg || PRODUCT_ICON_MAP[product?.id] || '/icon/四角星.svg')
|
||||
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 rawIcon = design.productIcon?.startsWith('/icon/') || /^https?:/i.test(design.productIcon || '')
|
||||
? design.productIcon
|
||||
: PRODUCT_ICON_MAP[design.productId] || '/icon/四角星.svg'
|
||||
|
||||
const iconImg = assetUrl(rawIcon)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// 跟随系统模式下,系统主题由 onThemeChange/getAppBaseInfo 写入缓存,供 resolveTheme 使用
|
||||
let detectedSystemTheme: 'light' | 'dark' | null = null
|
||||
|
||||
export function setDetectedSystemTheme(t: 'light' | 'dark'): void {
|
||||
detectedSystemTheme = t
|
||||
}
|
||||
|
||||
/** 根据自动模式解析实际生效主题 */
|
||||
export function resolveTheme(mode: ThemeMode): 'light' | 'dark' {
|
||||
if (mode === 'auto') {
|
||||
// darkmode 开启后 getAppBaseInfo().theme 能拿到系统主题;缓存仅用于启动时加速
|
||||
if (detectedSystemTheme) return detectedSystemTheme
|
||||
try {
|
||||
const info = Taro.getAppBaseInfo()
|
||||
if (info.theme === 'dark' || info.theme === 'light') return info.theme
|
||||
} catch { /* ignore */ }
|
||||
return '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,42 @@
|
||||
import Taro from '@tarojs/taro'
|
||||
import type { AddressItem } from '../../types'
|
||||
import { key } from './keys'
|
||||
|
||||
// ---------- 收货地址 ----------
|
||||
|
||||
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,57 @@
|
||||
import Taro from '@tarojs/taro'
|
||||
import type { DesignItem } from '../../types'
|
||||
import { assetUrl } from '../asset'
|
||||
import { PRODUCT_ICON_MAP } from '../productConfig'
|
||||
import { key, keyFor } from './keys'
|
||||
|
||||
/** 跨用户读取指定 openid 的设计清单(账号管理用) */
|
||||
export function getDesignListFor(openid: string): DesignItem[] {
|
||||
try { return Taro.getStorageSync(keyFor('design_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 = assetUrl(product?.iconImg || PRODUCT_ICON_MAP[product?.id] || '/icon/四角星.svg')
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* DA 层聚合入口:页面统一从 '../../utils/store' 导入。
|
||||
* 新增领域逻辑请写在 store/<domain>.ts,本文件只负责 re-export。
|
||||
*/
|
||||
export { isMockOpenid } from './keys'
|
||||
export {
|
||||
getUserInfoRaw,
|
||||
setUserInfoRaw,
|
||||
getUserInfoByOpenid,
|
||||
clearUserInfo,
|
||||
setUserInfo,
|
||||
getUserInfo,
|
||||
listAllUsers,
|
||||
createUser,
|
||||
switchUser,
|
||||
deleteUser,
|
||||
verifyAdminPassword
|
||||
} from './user'
|
||||
export {
|
||||
getDesignList,
|
||||
setDesignList,
|
||||
addDesign,
|
||||
updateDesign,
|
||||
removeDesigns,
|
||||
removeDesign
|
||||
} from './design'
|
||||
export {
|
||||
getOrderList,
|
||||
setOrderList,
|
||||
designToOrder
|
||||
} from './order'
|
||||
export {
|
||||
getAddressList,
|
||||
setAddressList,
|
||||
addAddress,
|
||||
updateAddress,
|
||||
deleteAddress,
|
||||
getDefaultAddress
|
||||
} from './address'
|
||||
export {
|
||||
getTheme,
|
||||
setTheme,
|
||||
setDetectedSystemTheme,
|
||||
resolveTheme
|
||||
} from './theme'
|
||||
export type { ThemeMode } from './theme'
|
||||
export type { DesignItem, OrderItem, AddressItem, StickerItem } from '../../types'
|
||||
@@ -0,0 +1,35 @@
|
||||
import Taro from '@tarojs/taro'
|
||||
|
||||
/** 当前登录用户信息在 Storage 中的 key */
|
||||
export const USER_KEY = 'smart_user_info'
|
||||
/** 当前激活 openid 在 Storage 中的 key */
|
||||
export 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;没有登录态时回退 _guest_ 并缓存 */
|
||||
function getActiveOpenid(): string {
|
||||
let user: any = null
|
||||
try {
|
||||
user = Taro.getStorageSync(USER_KEY)
|
||||
} catch {
|
||||
user = null
|
||||
}
|
||||
const openid = user?.openid || Taro.getStorageSync(ACTIVE_USER_KEY) || '_guest_'
|
||||
Taro.setStorageSync(ACTIVE_USER_KEY, openid)
|
||||
return openid
|
||||
}
|
||||
|
||||
/** 按当前用户生成作用域 storage key */
|
||||
export function key(scope: string): string {
|
||||
const prefix = getActiveOpenid()
|
||||
return `${scope}_${prefix}`
|
||||
}
|
||||
|
||||
/** 按指定 openid 生成作用域 storage key */
|
||||
export function keyFor(scope: string, openid: string): string {
|
||||
return `${scope}_${openid}`
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import Taro from '@tarojs/taro'
|
||||
import type { OrderItem } from '../../types'
|
||||
import { assetUrl } from '../asset'
|
||||
import { PRODUCT_ICON_MAP } from '../productConfig'
|
||||
import { getDesignList, setDesignList } from './design'
|
||||
import { key, keyFor } from './keys'
|
||||
|
||||
/** 跨用户读取指定 openid 的订单列表(账号管理用) */
|
||||
export function getOrderListFor(openid: string): OrderItem[] {
|
||||
try { return Taro.getStorageSync(keyFor('order_list', openid)) || [] } catch { return [] }
|
||||
}
|
||||
|
||||
// ---------- 订单 ----------
|
||||
|
||||
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 rawIcon = design.productIcon?.startsWith('/icon/') || /^https?:/i.test(design.productIcon || '')
|
||||
? design.productIcon
|
||||
: PRODUCT_ICON_MAP[design.productId] || '/icon/四角星.svg'
|
||||
|
||||
const iconImg = assetUrl(rawIcon)
|
||||
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import Taro from '@tarojs/taro'
|
||||
|
||||
const THEME_KEY = 'smart_theme'
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// 跟随系统模式下,系统主题由 onThemeChange/getAppBaseInfo 写入缓存,供 resolveTheme 使用
|
||||
let detectedSystemTheme: 'light' | 'dark' | null = null
|
||||
|
||||
export function setDetectedSystemTheme(t: 'light' | 'dark'): void {
|
||||
detectedSystemTheme = t
|
||||
}
|
||||
|
||||
/** 根据自动模式解析实际生效主题 */
|
||||
export function resolveTheme(mode: ThemeMode): 'light' | 'dark' {
|
||||
if (mode === 'auto') {
|
||||
if (detectedSystemTheme) return detectedSystemTheme
|
||||
try {
|
||||
const info = Taro.getAppBaseInfo()
|
||||
if (info.theme === 'dark' || info.theme === 'light') return info.theme
|
||||
} catch { /* ignore */ }
|
||||
return 'light'
|
||||
}
|
||||
return mode
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
import Taro from '@tarojs/taro'
|
||||
import { invalidateAuthCache } from '../authState'
|
||||
import { getDesignListFor } from './design'
|
||||
import { ACTIVE_USER_KEY, USER_KEY } from './keys'
|
||||
import { getOrderListFor } from './order'
|
||||
|
||||
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)
|
||||
}
|
||||
invalidateAuthCache()
|
||||
Taro.eventCenter?.trigger('authStateChanged', { openid: 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)
|
||||
invalidateAuthCache()
|
||||
Taro.eventCenter?.trigger('authStateChanged', { openid: '' })
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user