You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

205 lines
8.1 KiB

/*
* 统一数据访问层
*
* 默认走本地数据层,方便网页端离线演示。
* 页面带 ?demo=0 时切换到真实后端,供之后继续联调。
*/
(() => {
'use strict';
const API_BASE = `${location.origin}/api`;
const SOURCE_KEY = 'youthome:web:dataSource';
const SOURCE_VERSION_KEY = 'youthome:web:sourceDefault:v2';
const TOKEN_KEY = 'youthome:web:accessToken';
const USER_KEY = 'youthome:web:authUser';
const demoMode = new URLSearchParams(location.search).get('demo');
if (demoMode === '0') {
localStorage.setItem(SOURCE_KEY, 'api');
localStorage.setItem(SOURCE_VERSION_KEY, 'true');
if ((sessionStorage.getItem(TOKEN_KEY) || localStorage.getItem(TOKEN_KEY) || '').startsWith('local-')) {
[localStorage, sessionStorage].forEach((storage) => {
storage.removeItem(TOKEN_KEY);
storage.removeItem(USER_KEY);
});
}
} else {
if (demoMode === '1' || localStorage.getItem(SOURCE_VERSION_KEY) !== 'true') {
localStorage.setItem(SOURCE_KEY, 'local');
localStorage.setItem(SOURCE_VERSION_KEY, 'true');
}
if (localStorage.getItem(SOURCE_KEY) === 'local') {
const existingToken = sessionStorage.getItem(TOKEN_KEY) || localStorage.getItem(TOKEN_KEY) || '';
if (existingToken && !existingToken.startsWith('local-')) {
[localStorage, sessionStorage].forEach((storage) => {
storage.removeItem(TOKEN_KEY);
storage.removeItem(USER_KEY);
});
}
window.YouthomeLocal?.useDemoAccount();
}
}
const source = () => (localStorage.getItem(SOURCE_KEY) === 'local' ? 'local' : 'api');
const isLocal = () => source() === 'local';
const token = () => sessionStorage.getItem(TOKEN_KEY) || localStorage.getItem(TOKEN_KEY);
const currentUser = () => {
try { return JSON.parse(sessionStorage.getItem(USER_KEY) || localStorage.getItem(USER_KEY) || 'null'); }
catch (error) { return null; }
};
const applicationKey = () => `youthome:web:activityApplications:${currentUser()?.id || 'guest'}`;
const applicationRecords = () => {
try { return JSON.parse(localStorage.getItem(applicationKey()) || '[]'); }
catch (error) { return []; }
};
const clearSession = () => {
localStorage.removeItem(TOKEN_KEY);
localStorage.removeItem(USER_KEY);
sessionStorage.removeItem(TOKEN_KEY);
sessionStorage.removeItem(USER_KEY);
};
const request = async (path, options = {}) => {
if (isLocal()) {
if (!window.YouthomeLocal) throw new Error('本地数据层未加载,请刷新页面重试');
return window.YouthomeLocal.request(path, options);
}
const headers = { ...(options.headers || {}) };
if (token()) headers.access_token = token();
const isForm = options.body instanceof FormData;
if (options.body && !isForm) headers['Content-Type'] = 'application/json';
let response;
try {
response = await fetch(`${API_BASE}${path}`, {
...options,
headers,
signal: AbortSignal.timeout(isForm ? 600000 : 20000)
});
} catch (error) {
throw new Error(error.name === 'TimeoutError' ? '请求超时,请稍后重试' : '无法连接后端,请稍后重试');
}
let result;
try { result = await response.json(); }
catch (error) { throw new Error(`请求失败(HTTP ${response.status})`); }
if (response.status === 401) {
clearSession();
throw new Error('登录已失效,请重新登录');
}
if (!response.ok || (typeof result.code === 'number' && result.code !== 200)) {
throw new Error(result.message || `请求失败(HTTP ${response.status})`);
}
return Object.prototype.hasOwnProperty.call(result, 'data') ? result.data : result;
};
// 后端头像字段是资源 key(如 man1),不是可直接访问的 URL
const avatarUrl = (value) => {
if (!value || typeof value !== 'string') return '';
if (/^(https?:|data:image\/|\.\.\/|<)/.test(value)) return value;
return '';
};
const me = () => currentUser();
window.YouthomeApi = {
source,
isLocal,
setSource(value) {
localStorage.setItem(SOURCE_KEY, value === 'local' ? 'local' : 'api');
localStorage.setItem(SOURCE_VERSION_KEY, 'true');
},
token,
currentUser,
applicationRecords,
saveApplication(record) {
const records = applicationRecords().filter((item) => item.activityId !== record.activityId);
records.push(record);
localStorage.setItem(applicationKey(), JSON.stringify(records));
},
removeApplication(activityId) {
localStorage.setItem(applicationKey(), JSON.stringify(applicationRecords().filter((item) => item.activityId !== activityId)));
},
clearSession,
request,
get: (path) => request(path),
post: (path, body) => request(path, { method: 'POST', ...(body === undefined ? {} : { body: JSON.stringify(body) }) }),
delete: (path) => request(path, { method: 'DELETE' }),
avatarUrl,
updateSessionUser(patch) {
const storage = sessionStorage.getItem(USER_KEY) ? sessionStorage : localStorage;
const merged = { ...(currentUser() || {}), ...patch };
storage.setItem(USER_KEY, JSON.stringify(merged));
return merged;
},
async profile() {
if (isLocal()) return window.YouthomeLocal?.account?.() || null;
const user = me();
if (!user) return null;
return request(`/users/profile?userId=${encodeURIComponent(user.id)}`);
},
async points() {
if (isLocal()) return Number(window.YouthomeLocal?.account?.()?.points || 0);
const data = await request('/profile/points');
return Number(data?.points ?? 0);
},
async checkIn() {
if (isLocal()) return null;
return request('/campus/points/check-in', { method: 'POST' });
},
async followStats() {
if (isLocal()) {
const account = window.YouthomeLocal?.account?.();
return { followingCount: window.YouthomeLocal?.following?.().length || 0, followersCount: account?.fans || 0 };
}
const user = me();
if (!user) return { followingCount: 0, followersCount: 0 };
return request(`/users/follow-status?userId=${encodeURIComponent(user.id)}`);
},
async favorites() {
if (isLocal()) return window.YouthomeLocal.bucketMoments('favorites');
const data = await request('/campus/moments/favorites?page=1&size=50');
return data?.items || [];
},
async following() {
if (isLocal()) return window.YouthomeLocal.following().map((nickname) => ({ nickname }));
const user = me();
if (!user) return [];
const data = await request(`/users/following?userId=${encodeURIComponent(user.id)}&page=1&size=50`);
return data?.items || [];
},
async toggleFollow(userId) {
if (isLocal() || !userId) return null;
return request(`/users/follow?userId=${encodeURIComponent(userId)}`, { method: 'POST' });
},
async joinedActivities() {
if (isLocal()) return window.YouthomeLocal.joinedActivities();
const data = await request('/campus/activities/joined?page=1&size=50');
return data?.items || [];
},
async registrations() {
if (isLocal()) return [];
const data = await request('/campus/activities/registrations/mine');
return Array.isArray(data) ? data : (data?.items || []);
},
async cancelRegistration(id) {
return request(`/campus/activities/registrations/${encodeURIComponent(id)}`, { method: 'DELETE' });
},
async joinActivity(id, remark = '') {
if (isLocal()) return null;
const query = remark ? `?remark=${encodeURIComponent(remark)}` : '';
return request(`/campus/activities/${encodeURIComponent(id)}/applications${query}`, { method: 'POST' });
},
async quitActivity(id) {
if (isLocal()) return window.YouthomeLocal.leaveActivity(id);
return request(`/campus/activities/quit?id=${encodeURIComponent(id)}`, { method: 'DELETE' });
},
async mallProducts() {
if (isLocal()) return [];
const data = await request('/campus/mall/products');
return data?.items || [];
},
async purchase(productId) {
return request('/campus/mall/purchase', { method: 'POST', body: JSON.stringify({ productId }) });
}
};
})();