前端重构实现
This commit is contained in:
@@ -0,0 +1,489 @@
|
||||
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 - 验证码
|
||||
*/
|
||||
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,
|
||||
view: 'onboarding',
|
||||
loading: false
|
||||
});
|
||||
|
||||
// 尝试加载用户档案
|
||||
try {
|
||||
await get().loadUserProfile();
|
||||
} catch {
|
||||
// 档案不存在,继续入站流程
|
||||
}
|
||||
|
||||
return response;
|
||||
} 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;
|
||||
Reference in New Issue
Block a user