497 lines
14 KiB
JavaScript
497 lines
14 KiB
JavaScript
import { create } from 'zustand';
|
|
import { persist } from 'zustand/middleware';
|
|
import * as authService from '../services/auth';
|
|
import * as userProfileService from '../services/userProfile';
|
|
import * as lifeEventService from '../services/lifeEvent';
|
|
import * as epicScriptService from '../services/epicScript';
|
|
import * as lifePathService from '../services/lifePath';
|
|
|
|
/**
|
|
* 默认注册数据
|
|
*/
|
|
const defaultRegistrationData = {
|
|
id: null,
|
|
nickname: '',
|
|
gender: '',
|
|
zodiac: '',
|
|
mbti: '',
|
|
profession: '',
|
|
hobbies: [],
|
|
childhood: { date: '', text: '' },
|
|
joy: { date: '', text: '' },
|
|
low: { date: '', text: '' },
|
|
future: { vision: '', ideal: '' }
|
|
};
|
|
|
|
/**
|
|
* 应用状态存储
|
|
* 使用 Zustand 进行状态管理,支持 localStorage 持久化
|
|
*/
|
|
const useStore = create(
|
|
persist(
|
|
(set, get) => ({
|
|
// 认证状态
|
|
isLoggedIn: false,
|
|
phone: '',
|
|
userId: null,
|
|
|
|
// 视图状态
|
|
view: 'login',
|
|
currentStep: 1,
|
|
|
|
// 用户注册数据
|
|
registrationData: { ...defaultRegistrationData },
|
|
|
|
// 生命事件
|
|
lifeEvents: [],
|
|
|
|
// 剧本
|
|
scripts: [],
|
|
selectedScriptId: null,
|
|
|
|
// 路径
|
|
selectedPath: null,
|
|
|
|
// 加载状态
|
|
loading: false,
|
|
error: null,
|
|
|
|
/**
|
|
* 设置加载状态
|
|
*/
|
|
setLoading: (loading) => set({ loading }),
|
|
|
|
/**
|
|
* 设置错误信息
|
|
*/
|
|
setError: (error) => set({ error }),
|
|
|
|
/**
|
|
* 获取短信验证码
|
|
* @param {string} phone - 手机号
|
|
*/
|
|
getSmsCode: async (phone) => {
|
|
set({ loading: true, error: null });
|
|
try {
|
|
const response = await authService.getSmsCode(phone);
|
|
set({ loading: false });
|
|
return response;
|
|
} catch (error) {
|
|
set({ loading: false, error: error.message });
|
|
throw error;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* 登录
|
|
* @param {string} phone - 手机号
|
|
* @param {string} smsCode - 验证码
|
|
* @returns {Promise<Object>} 包含 hasProfile 标识,用于判断是否需要跳转到 onboarding
|
|
*/
|
|
login: async (phone, smsCode) => {
|
|
set({ loading: true, error: null });
|
|
try {
|
|
const response = await authService.login({ phone, smsCode });
|
|
const { userId } = response.data || {};
|
|
|
|
set({
|
|
isLoggedIn: true,
|
|
phone,
|
|
userId,
|
|
loading: false
|
|
});
|
|
|
|
// 尝试加载用户档案,判断用户是否已完成注册
|
|
let hasProfile = false;
|
|
try {
|
|
const profileData = await get().loadUserProfile();
|
|
// 检查档案是否完整(有昵称和未来愿景)
|
|
hasProfile = !!(profileData && profileData.nickname && profileData.future?.vision);
|
|
} catch {
|
|
// 档案不存在,需要进入入站流程
|
|
hasProfile = false;
|
|
}
|
|
|
|
// 根据档案状态设置视图
|
|
set({ view: hasProfile ? 'dashboard' : 'onboarding' });
|
|
|
|
return { ...response, hasProfile };
|
|
} catch (error) {
|
|
set({ loading: false, error: error.message });
|
|
throw error;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* 设置登录状态(本地模式)
|
|
*/
|
|
setLogin: (isLoggedIn, phone = '') => set({
|
|
isLoggedIn,
|
|
phone,
|
|
view: isLoggedIn ? 'onboarding' : 'login'
|
|
}),
|
|
|
|
/**
|
|
* 登出
|
|
*/
|
|
logout: async () => {
|
|
try {
|
|
await authService.logout();
|
|
} catch {
|
|
// 忽略登出错误
|
|
}
|
|
set({
|
|
isLoggedIn: false,
|
|
phone: '',
|
|
userId: null,
|
|
view: 'login',
|
|
currentStep: 1,
|
|
registrationData: { ...defaultRegistrationData },
|
|
lifeEvents: [],
|
|
scripts: [],
|
|
selectedScriptId: null,
|
|
selectedPath: null
|
|
});
|
|
},
|
|
|
|
/**
|
|
* 设置当前视图
|
|
*/
|
|
setView: (view) => set({ view }),
|
|
|
|
/**
|
|
* 设置当前步骤
|
|
*/
|
|
setCurrentStep: (step) => set({ currentStep: step }),
|
|
|
|
/**
|
|
* 加载用户档案
|
|
*/
|
|
loadUserProfile: async () => {
|
|
set({ loading: true });
|
|
try {
|
|
const response = await userProfileService.getCurrentProfile();
|
|
if (response.data) {
|
|
const profileData = userProfileService.transformToFrontendFormat(response.data);
|
|
set({
|
|
registrationData: { ...defaultRegistrationData, ...profileData },
|
|
loading: false
|
|
});
|
|
return profileData;
|
|
}
|
|
set({ loading: false });
|
|
return null;
|
|
} catch (error) {
|
|
set({ loading: false });
|
|
throw error;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* 更新注册数据(本地)
|
|
*/
|
|
updateRegistration: (data) => set((state) => ({
|
|
registrationData: { ...state.registrationData, ...data }
|
|
})),
|
|
|
|
/**
|
|
* 保存用户档案到后端
|
|
*/
|
|
saveUserProfile: async () => {
|
|
const { registrationData } = get();
|
|
set({ loading: true, error: null });
|
|
try {
|
|
let response;
|
|
if (registrationData.id) {
|
|
// 更新
|
|
response = await userProfileService.updateProfile(registrationData);
|
|
} else {
|
|
// 创建
|
|
response = await userProfileService.createProfile(registrationData);
|
|
}
|
|
|
|
if (response.data) {
|
|
const profileData = userProfileService.transformToFrontendFormat(response.data);
|
|
set({
|
|
registrationData: { ...registrationData, ...profileData },
|
|
loading: false
|
|
});
|
|
} else {
|
|
set({ loading: false });
|
|
}
|
|
return response;
|
|
} catch (error) {
|
|
set({ loading: false, error: error.message });
|
|
throw error;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* 加载生命事件列表
|
|
*/
|
|
loadLifeEvents: async () => {
|
|
set({ loading: true });
|
|
try {
|
|
const response = await lifeEventService.getEventList();
|
|
const events = lifeEventService.transformListToFrontend(response.data || []);
|
|
set({ lifeEvents: events, loading: false });
|
|
return events;
|
|
} catch (error) {
|
|
set({ loading: false });
|
|
throw error;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* 添加生命事件
|
|
*/
|
|
addLifeEvent: async (event) => {
|
|
set({ loading: true, error: null });
|
|
try {
|
|
const response = await lifeEventService.createEvent(event);
|
|
if (response.data) {
|
|
const newEvent = lifeEventService.transformToFrontendFormat(response.data);
|
|
set((state) => ({
|
|
lifeEvents: [...state.lifeEvents, newEvent],
|
|
loading: false
|
|
}));
|
|
return newEvent;
|
|
}
|
|
set({ loading: false });
|
|
return null;
|
|
} catch (error) {
|
|
set({ loading: false, error: error.message });
|
|
// 降级到本地存储
|
|
const localEvent = { ...event, id: Date.now().toString() };
|
|
set((state) => ({
|
|
lifeEvents: [...state.lifeEvents, localEvent]
|
|
}));
|
|
return localEvent;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* 删除生命事件
|
|
*/
|
|
deleteLifeEvent: async (id) => {
|
|
set({ loading: true });
|
|
try {
|
|
await lifeEventService.deleteEvent(id);
|
|
set((state) => ({
|
|
lifeEvents: state.lifeEvents.filter(e => e.id !== id),
|
|
loading: false
|
|
}));
|
|
} catch (error) {
|
|
set({ loading: false });
|
|
// 降级到本地删除
|
|
set((state) => ({
|
|
lifeEvents: state.lifeEvents.filter(e => e.id !== id)
|
|
}));
|
|
}
|
|
},
|
|
|
|
/**
|
|
* 加载剧本列表
|
|
*/
|
|
loadScripts: async () => {
|
|
set({ loading: true });
|
|
try {
|
|
const response = await epicScriptService.getScriptList();
|
|
const scripts = epicScriptService.transformListToFrontend(response.data || []);
|
|
// 找到选中的剧本
|
|
const selectedScript = scripts.find(s => s.isSelected);
|
|
set({
|
|
scripts,
|
|
selectedScriptId: selectedScript?.id || scripts[0]?.id || null,
|
|
loading: false
|
|
});
|
|
return scripts;
|
|
} catch (error) {
|
|
set({ loading: false });
|
|
throw error;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* 添加剧本
|
|
*/
|
|
addScript: async (script) => {
|
|
set({ loading: true, error: null });
|
|
try {
|
|
const response = await epicScriptService.createScript({
|
|
...script,
|
|
isSelected: true
|
|
});
|
|
if (response.data) {
|
|
const newScript = epicScriptService.transformToFrontendFormat(response.data);
|
|
set((state) => ({
|
|
scripts: [newScript, ...state.scripts],
|
|
selectedScriptId: newScript.id,
|
|
loading: false
|
|
}));
|
|
return newScript;
|
|
}
|
|
set({ loading: false });
|
|
return null;
|
|
} catch (error) {
|
|
set({ loading: false, error: error.message });
|
|
// 降级到本地存储
|
|
const localScript = {
|
|
...script,
|
|
id: Date.now().toString(),
|
|
date: new Date().toLocaleDateString()
|
|
};
|
|
set((state) => ({
|
|
scripts: [localScript, ...state.scripts],
|
|
selectedScriptId: localScript.id
|
|
}));
|
|
return localScript;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* 设置选中的剧本ID
|
|
*/
|
|
setSelectedScriptId: async (id) => {
|
|
set({ selectedScriptId: id });
|
|
try {
|
|
await epicScriptService.selectScript(id);
|
|
} catch {
|
|
// 忽略错误,本地已更新
|
|
}
|
|
},
|
|
|
|
/**
|
|
* 获取选中的剧本
|
|
*/
|
|
getSelectedScript: () => {
|
|
const state = get();
|
|
return state.scripts.find(s => s.id === state.selectedScriptId);
|
|
},
|
|
|
|
/**
|
|
* 删除剧本
|
|
*/
|
|
deleteScript: async (id) => {
|
|
set({ loading: true });
|
|
try {
|
|
await epicScriptService.deleteScript(id);
|
|
set((state) => {
|
|
const newScripts = state.scripts.filter(s => s.id !== id);
|
|
return {
|
|
scripts: newScripts,
|
|
selectedScriptId: state.selectedScriptId === id
|
|
? (newScripts[0]?.id || null)
|
|
: state.selectedScriptId,
|
|
loading: false
|
|
};
|
|
});
|
|
} catch (error) {
|
|
set({ loading: false });
|
|
// 降级到本地删除
|
|
set((state) => {
|
|
const newScripts = state.scripts.filter(s => s.id !== id);
|
|
return {
|
|
scripts: newScripts,
|
|
selectedScriptId: state.selectedScriptId === id
|
|
? (newScripts[0]?.id || null)
|
|
: state.selectedScriptId
|
|
};
|
|
});
|
|
}
|
|
},
|
|
|
|
/**
|
|
* 加载路径
|
|
*/
|
|
loadPath: async (scriptId) => {
|
|
if (!scriptId) return null;
|
|
set({ loading: true });
|
|
try {
|
|
const response = await lifePathService.getPathByScriptId(scriptId);
|
|
if (response.data) {
|
|
const path = lifePathService.transformToFrontendFormat(response.data);
|
|
set({ selectedPath: path.content, loading: false });
|
|
return path;
|
|
}
|
|
set({ loading: false });
|
|
return null;
|
|
} catch {
|
|
set({ loading: false });
|
|
return null;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* 设置路径
|
|
*/
|
|
setPath: async (pathContent, scriptId) => {
|
|
set({ selectedPath: pathContent });
|
|
|
|
if (scriptId) {
|
|
try {
|
|
// 检查是否已有路径
|
|
const existingPath = await lifePathService.getPathByScriptId(scriptId).catch(() => null);
|
|
|
|
if (existingPath?.data?.id) {
|
|
// 更新
|
|
await lifePathService.updatePath({
|
|
id: existingPath.data.id,
|
|
scriptId,
|
|
content: pathContent
|
|
});
|
|
} else {
|
|
// 创建
|
|
await lifePathService.createPath({
|
|
scriptId,
|
|
content: pathContent
|
|
});
|
|
}
|
|
} catch {
|
|
// 忽略错误,本地已更新
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* 清除所有数据
|
|
*/
|
|
clear: async () => {
|
|
try {
|
|
await authService.logout();
|
|
} catch {
|
|
// 忽略错误
|
|
}
|
|
localStorage.removeItem('life_trajectory_v3');
|
|
localStorage.removeItem('access_token');
|
|
localStorage.removeItem('refresh_token');
|
|
set({
|
|
isLoggedIn: false,
|
|
phone: '',
|
|
userId: null,
|
|
view: 'login',
|
|
currentStep: 1,
|
|
registrationData: { ...defaultRegistrationData },
|
|
lifeEvents: [],
|
|
scripts: [],
|
|
selectedScriptId: null,
|
|
selectedPath: null,
|
|
loading: false,
|
|
error: null
|
|
});
|
|
window.location.reload();
|
|
}
|
|
}),
|
|
{
|
|
name: 'life_trajectory_v3',
|
|
onRehydrateStorage: () => (state, error) => {
|
|
if (error) {
|
|
console.error('Failed to load state from localStorage:', error);
|
|
}
|
|
}
|
|
}
|
|
)
|
|
);
|
|
|
|
export default useStore;
|