| | import { create } from 'zustand'; |
| | import { persist, createJSONStorage } from 'zustand/middleware'; |
| | import { UserAccount, LeadData, AdminConfig, WeddingStyle, GeneratedResult, CompositionMode, Resolution, SubjectType, GenerationStatus, Language, PointHistory, FeedbackItem, GenerationLog } from './types'; |
| | import { TRANSLATIONS } from './constants/translations'; |
| |
|
| | |
| | const DEFAULT_CONFIG: AdminConfig = { |
| | promoText: "🎉 Spring Wedding Expo: Book online today and get a Free Makeup Trial + ¥500 Voucher!", |
| | promoEnds: new Date(Date.now() + 86400000).toISOString(), |
| | contactPhone: '0592-8888888', |
| | showBanner: true, |
| | footerAddress: "No. 188, Huandao Road, Siming District, Xiamen", |
| | logoUrl: "", |
| | shareTitle: "Help me choose a wedding style!", |
| | shareDesc: "I found this amazing AI fitting room at Romantic Life.", |
| | shareImage: "", |
| | redPacketMax: 2000, |
| | slashDifficulty: 0.98, |
| | crmApiUrl: '', |
| | geminiApiKey: '', |
| | geminiApiUrl: '', |
| | pointsShare: 10, |
| | pointsInvite: 50, |
| | pointsBook: 100, |
| | pointsVipCost: 100, |
| | |
| | qrCodeUrl: "https://api.qrserver.com/v1/create-qr-code/?size=150x150&data=https://xmlove520.dpdns.org" |
| | }; |
| |
|
| | |
| | interface UserState { |
| | currentUser: UserAccount | undefined; |
| | allUsers: UserAccount[]; |
| | leads: LeadData[]; |
| | feedback: FeedbackItem[]; |
| | guestFavorites: string[]; |
| | logs: GenerationLog[]; |
| | |
| | |
| | login: (phone: string, password?: string) => { success: boolean; msg: string }; |
| | register: (phone: string, name: string, pass: string) => { success: boolean; msg: string }; |
| | logout: () => void; |
| | updateUser: (id: string, updates: Partial<UserAccount>) => void; |
| | resetPassword: (phone: string, newPass: string) => boolean; |
| | addPoints: (amount: number, reason: string) => void; |
| | toggleFavorite: (styleId: string) => void; |
| | |
| | |
| | addLead: (lead: LeadData) => void; |
| | updateLeadStatus: (id: string, status: LeadData['status']) => void; |
| | deleteLead: (id: string) => void; |
| | setLeads: (leads: LeadData[]) => void; |
| | |
| | |
| | addFeedback: (item: FeedbackItem) => void; |
| | |
| | |
| | addLog: (log: GenerationLog) => void; |
| | } |
| |
|
| | export const useUserStore = create<UserState>()( |
| | persist( |
| | (set, get) => ({ |
| | currentUser: undefined, |
| | allUsers: [], |
| | leads: [], |
| | feedback: [], |
| | guestFavorites: [], |
| | logs: [], |
| |
|
| | login: (phone, password) => { |
| | const { allUsers } = get(); |
| | const user = allUsers.find(u => u.phone === phone); |
| | if (user) { |
| | if (user.password && password && user.password !== password) { |
| | return { success: false, msg: "Incorrect password" }; |
| | } |
| | set({ currentUser: user }); |
| | return { success: true, msg: `Welcome ${user.name}` }; |
| | } |
| | return { success: false, msg: "User not found" }; |
| | }, |
| |
|
| | register: (phone, name, pass) => { |
| | const { allUsers } = get(); |
| | if (allUsers.find(u => u.phone === phone)) { |
| | return { success: false, msg: "Phone already registered" }; |
| | } |
| | const newUser: UserAccount = { |
| | id: 'user_' + Date.now(), |
| | name, |
| | phone, |
| | password: pass, |
| | points: 100, |
| | isVip: false, |
| | joinDate: Date.now(), |
| | history: [], |
| | role: 'user', |
| | redPacketBalance: 1980, |
| | slashProgress: {}, |
| | favorites: [] |
| | }; |
| | set({ allUsers: [...allUsers, newUser], currentUser: newUser }); |
| | return { success: true, msg: "Registration successful" }; |
| | }, |
| |
|
| | logout: () => set({ currentUser: undefined }), |
| |
|
| | updateUser: (id, updates) => { |
| | const { allUsers, currentUser } = get(); |
| | const newAllUsers = allUsers.map(u => u.id === id ? { ...u, ...updates } : u); |
| | |
| | let newCurrentUser = currentUser; |
| | if (currentUser && currentUser.id === id) { |
| | newCurrentUser = { ...currentUser, ...updates }; |
| | } |
| |
|
| | set({ allUsers: newAllUsers, currentUser: newCurrentUser }); |
| | }, |
| |
|
| | resetPassword: (phone, newPass) => { |
| | const { allUsers } = get(); |
| | const index = allUsers.findIndex(u => u.phone === phone); |
| | if (index !== -1) { |
| | const updatedUsers = [...allUsers]; |
| | updatedUsers[index] = { ...updatedUsers[index], password: newPass }; |
| | set({ allUsers: updatedUsers }); |
| | return true; |
| | } |
| | return false; |
| | }, |
| |
|
| | addPoints: (amount, reason) => { |
| | const { currentUser, updateUser } = get(); |
| | if (!currentUser) return; |
| | |
| | const newHistory: PointHistory = { |
| | id: Date.now().toString(), |
| | action: 'share', |
| | points: amount, |
| | timestamp: Date.now(), |
| | desc: reason |
| | }; |
| | |
| | updateUser(currentUser.id, { |
| | points: currentUser.points + amount, |
| | history: [...currentUser.history, newHistory] |
| | }); |
| | }, |
| |
|
| | toggleFavorite: (styleId) => { |
| | const { currentUser, updateUser, guestFavorites } = get(); |
| | |
| | if (currentUser) { |
| | |
| | const currentFavs = currentUser.favorites || []; |
| | const newFavs = currentFavs.includes(styleId) |
| | ? currentFavs.filter(id => id !== styleId) |
| | : [...currentFavs, styleId]; |
| | updateUser(currentUser.id, { favorites: newFavs }); |
| | } else { |
| | |
| | const newFavs = guestFavorites.includes(styleId) |
| | ? guestFavorites.filter(id => id !== styleId) |
| | : [...guestFavorites, styleId]; |
| | set({ guestFavorites: newFavs }); |
| | } |
| | }, |
| |
|
| | |
| | addLead: (lead) => set(state => ({ leads: [lead, ...state.leads] })), |
| | updateLeadStatus: (id, status) => set(state => ({ leads: state.leads.map(l => l.id === id ? { ...l, status } : l) })), |
| | deleteLead: (id) => set(state => ({ leads: state.leads.filter(l => l.id !== id) })), |
| | setLeads: (leads) => set({ leads }), |
| | |
| | |
| | addFeedback: (item) => set(state => ({ feedback: [item, ...state.feedback] })), |
| | |
| | |
| | addLog: (log) => set(state => ({ logs: [log, ...state.logs].slice(0, 2000) })), |
| | }), |
| | { |
| | name: 'rl_user_storage', |
| | partialize: (state) => ({ |
| | allUsers: state.allUsers, |
| | leads: state.leads, |
| | currentUser: state.currentUser, |
| | feedback: state.feedback, |
| | guestFavorites: state.guestFavorites, |
| | logs: state.logs |
| | }), |
| | } |
| | ) |
| | ); |
| |
|
| | |
| | interface UIState { |
| | language: Language; |
| | adminConfig: AdminConfig; |
| | |
| | |
| | modals: { |
| | auth: boolean; |
| | userCenter: boolean; |
| | admin: boolean; |
| | about: boolean; |
| | feedback: boolean; |
| | share: boolean; |
| | consult: boolean; |
| | analysis: boolean; |
| | redPacket: boolean; |
| | slash: boolean; |
| | }; |
| |
|
| | |
| | setLanguage: (lang: Language) => void; |
| | setAdminConfig: (config: AdminConfig) => void; |
| | toggleModal: (modal: keyof UIState['modals'], isOpen: boolean) => void; |
| | closeAllModals: () => void; |
| | |
| | |
| | toastMsg: string | null; |
| | showToast: (msg: string) => void; |
| | } |
| |
|
| | export const useUIStore = create<UIState>()( |
| | persist( |
| | (set) => ({ |
| | language: 'zh', |
| | adminConfig: DEFAULT_CONFIG, |
| | modals: { |
| | auth: false, |
| | userCenter: false, |
| | admin: false, |
| | about: false, |
| | feedback: false, |
| | share: false, |
| | consult: false, |
| | analysis: false, |
| | redPacket: false, |
| | slash: false, |
| | }, |
| | toastMsg: null, |
| |
|
| | setLanguage: (lang) => set({ language: lang }), |
| | setAdminConfig: (config) => set({ adminConfig: config }), |
| | toggleModal: (modal, isOpen) => set(state => ({ modals: { ...state.modals, [modal]: isOpen } })), |
| | closeAllModals: () => set(state => { |
| | const closed = Object.keys(state.modals).reduce((acc, key) => ({...acc, [key]: false}), {} as any); |
| | return { modals: closed }; |
| | }), |
| | showToast: (msg) => { |
| | set({ toastMsg: msg }); |
| | setTimeout(() => set({ toastMsg: null }), 3000); |
| | } |
| | }), |
| | { |
| | name: 'rl_ui_storage', |
| | partialize: (state) => ({ language: state.language, adminConfig: state.adminConfig }), |
| | } |
| | ) |
| | ); |
| |
|
| | |
| | interface GenerationState { |
| | uploadedImages: string[]; |
| | selectedStyle: WeddingStyle | null; |
| | customStyleImage: string | null; |
| | |
| | |
| | filter: string; |
| | blurAmount: number; |
| | compositionMode: CompositionMode; |
| | resolution: Resolution; |
| | subjectType: SubjectType; |
| | customPrompt: string; |
| |
|
| | |
| | results: Record<string, GeneratedResult>; |
| | status: GenerationStatus; |
| | progress: { current: number, total: number, statusMsg?: string } | null; |
| | errorMsg: string | null; |
| | |
| | |
| | isScanning: boolean; |
| | scanStep: number; |
| | recommendedStyleIds: string[]; |
| | analysisResult: { faceShape: string; skinTone: string; bestVibe: string; }; |
| |
|
| | |
| | setUploadedImages: (images: string[]) => void; |
| | setSelectedStyle: (style: WeddingStyle | null) => void; |
| | setCustomStyleImage: (img: string | null) => void; |
| | |
| | setGenerationConfig: (config: Partial<{ filter: string, blurAmount: number, compositionMode: CompositionMode, resolution: Resolution, subjectType: SubjectType, customPrompt: string }>) => void; |
| | |
| | setStatus: (status: GenerationStatus) => void; |
| | setProgress: (progress: GenerationState['progress']) => void; |
| | setErrorMsg: (msg: string | null) => void; |
| | addResult: (result: GeneratedResult) => void; |
| | setResults: (results: Record<string, GeneratedResult>) => void; |
| | |
| | resetGeneration: () => void; |
| | |
| | |
| | startScanning: () => void; |
| | setScanStep: (step: number) => void; |
| | setAnalysisData: (recs: string[], result: { faceShape: string; skinTone: string; bestVibe: string; }) => void; |
| | stopScanning: () => void; |
| | } |
| |
|
| | export const useGenerationStore = create<GenerationState>()((set) => ({ |
| | uploadedImages: [], |
| | selectedStyle: null, |
| | customStyleImage: null, |
| | |
| | filter: 'none', |
| | blurAmount: 0, |
| | compositionMode: 'classic', |
| | resolution: 'standard', |
| | subjectType: 'female', |
| | customPrompt: '', |
| |
|
| | results: {}, |
| | status: 'idle', |
| | progress: null, |
| | errorMsg: null, |
| | |
| | isScanning: false, |
| | scanStep: 0, |
| | recommendedStyleIds: [], |
| | analysisResult: { faceShape: '', skinTone: '', bestVibe: '' }, |
| |
|
| | setUploadedImages: (images) => set({ uploadedImages: images }), |
| | setSelectedStyle: (style) => set({ selectedStyle: style }), |
| | setCustomStyleImage: (img) => set({ customStyleImage: img }), |
| | |
| | setGenerationConfig: (config) => set(state => ({ ...state, ...config })), |
| | |
| | setStatus: (status) => set({ status }), |
| | setProgress: (progress) => set({ progress }), |
| | setErrorMsg: (errorMessage) => set({ errorMsg: errorMessage }), |
| | addResult: (result) => set(state => ({ results: { ...state.results, [result.styleId]: result } })), |
| | setResults: (results) => set({ results }), |
| | |
| | resetGeneration: () => set({ |
| | status: 'idle', |
| | results: {}, |
| | uploadedImages: [], |
| | selectedStyle: null, |
| | customStyleImage: null, |
| | filter: 'none', |
| | blurAmount: 0, |
| | customPrompt: '', |
| | compositionMode: 'classic', |
| | resolution: 'standard', |
| | progress: null, |
| | recommendedStyleIds: [] |
| | }), |
| | |
| | startScanning: () => set({ isScanning: true, scanStep: 0 }), |
| | setScanStep: (step) => set({ scanStep: step }), |
| | setAnalysisData: (recs, result) => set({ recommendedStyleIds: recs, analysisResult: result }), |
| | stopScanning: () => set({ isScanning: false }) |
| | })); |