feat: split DA and API layers by domain, add parallel route docs

This commit is contained in:
2026-08-10 01:56:09 +08:00
parent 68c5e8ee04
commit 0394da6771
38 changed files with 1156 additions and 437 deletions
-90
View File
@@ -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 })
}
+7
View File
@@ -0,0 +1,7 @@
/**
* R2 收货地址接口归属文件。
* 后端 /api/addresses CRUD 就绪后,在这里补充:
* fetchAddresses / createAddress / updateAddress / deleteAddress / setDefaultAddress
* 返回类型优先直接对应 src/types/index.ts 的 AddressItem 契约。
*/
export {}
+45
View File
@@ -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()
}
+7
View File
@@ -0,0 +1,7 @@
/**
* R2 设计清单接口归属文件。
* 后端 /api/design-list CRUD 就绪后,在这里补充:
* fetchDesignList / createDesign / updateDesign / deleteDesign
* 设计数据(贴纸、掩膜、分类信息)以 JSON 方式随 items/designData 提交。
*/
export {}
+11
View File
@@ -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'
+7
View File
@@ -0,0 +1,7 @@
/**
* R3 订单接口归属文件。
* 后端 /api/orders 就绪后,在这里补充:
* createOrder / fetchOrders / fetchOrderDetail / payOrder
* 金额一律以服务端重算结果为准,前端只提交商品/设计数据与地址快照。
*/
export {}
+16
View File
@@ -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 })
}
+7
View File
@@ -0,0 +1,7 @@
/**
* R4 上传接口归属文件。
* 后端 COS/wordcloud 适配器就绪后,在这里补充:
* getUploadCredentials / uploadImage / generateWordCloud / getWordCloudJob / sketchImage
* 词云任务使用“创建任务 -> 轮询状态 -> 取结果链接”的异步模型。
*/
export {}
+24
View File
@@ -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')
}
-328
View File
@@ -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)
}
+42
View File
@@ -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)
}
+57
View File
@@ -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)
}
+47
View File
@@ -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'
+35
View File
@@ -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}`
}
+55
View File
@@ -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
}
+33
View File
@@ -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
}
+123
View File
@@ -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
}