refactor: establish frontend request and state boundaries
This commit is contained in:
+193
-237
@@ -21,166 +21,185 @@ const THEME_STORAGE_KEY = "xiaobaiTheme";
|
||||
let activeThemeTransition = null;
|
||||
let themeSwitchSequence = 0;
|
||||
|
||||
const state = {
|
||||
user: null,
|
||||
csrfToken: "",
|
||||
authMode: "login",
|
||||
started: false,
|
||||
dashboard: null,
|
||||
filter: "all",
|
||||
query: "",
|
||||
sortKey: "streak",
|
||||
sortDirection: "desc",
|
||||
brokenQuery: "",
|
||||
brokenSortKey: "",
|
||||
brokenSortDirection: "desc",
|
||||
downQuery: "",
|
||||
downSortKey: "",
|
||||
downSortDirection: "asc",
|
||||
yesterdayFilter: "all",
|
||||
yesterdayQuery: "",
|
||||
yesterdaySortKey: "",
|
||||
yesterdaySortDirection: "desc",
|
||||
activeView: "sentimentCycleView",
|
||||
dragonTiger: null,
|
||||
dragonViewMode: "daily",
|
||||
dragonFilter: "all",
|
||||
dragonQuery: "",
|
||||
selectedDragonTraderId: "",
|
||||
hotMoneyProfiles: null,
|
||||
hotMoneyProfileQuery: "",
|
||||
selectedHotMoneyProfileId: "",
|
||||
rotationHistory: null,
|
||||
rotationHistoryKey: "",
|
||||
rotationSelectedSector: "",
|
||||
rotationSelectedDate: "",
|
||||
rotationMembers: null,
|
||||
rotationMembersKey: "",
|
||||
rotationMembersLoading: false,
|
||||
rotationOrder: localStorage.getItem("xiaobaiRotationOrder") === "latest" ? "latest" : "oldest",
|
||||
rotationLoading: false,
|
||||
auctionData: null,
|
||||
auctionDataset: "focus",
|
||||
auctionFilter: "all",
|
||||
auctionQuery: "",
|
||||
auctionSortKey: "attention_score",
|
||||
auctionSortDirection: "desc",
|
||||
auctionLoading: false,
|
||||
auctionTimer: null,
|
||||
themeLibrary: null,
|
||||
themeQuery: "",
|
||||
selectedThemeCode: "",
|
||||
themeDetail: null,
|
||||
themeLoading: false,
|
||||
popularityData: null,
|
||||
popularitySource: "combined",
|
||||
popularityQuery: "",
|
||||
popularityLoading: false,
|
||||
expandedLadderLevels: new Set(),
|
||||
ladderSortMode: "time",
|
||||
stockDetail: null,
|
||||
activeStock: null,
|
||||
stockDetailChartMode: "daily",
|
||||
stockDetailIntraday: null,
|
||||
stockDetailRequestSequence: 0,
|
||||
entityDetailItem: null,
|
||||
entityDetailPayload: null,
|
||||
entityDetailChartMode: "daily",
|
||||
entityDetailIntraday: null,
|
||||
entityDetailRequestSequence: 0,
|
||||
stockPreviewCode: "",
|
||||
stockPreviewType: "stock",
|
||||
stockPreviewItem: null,
|
||||
stockPreviewPayload: null,
|
||||
stockPreviewChart: "daily",
|
||||
stockPreviewFallback: null,
|
||||
watchlist: [],
|
||||
watchlistSelection: null,
|
||||
watchlistSearchResults: [],
|
||||
watchlistSearchRequestSequence: 0,
|
||||
editingDailyNoteId: 0,
|
||||
notes: [],
|
||||
tradeEntries: [],
|
||||
tradeSummary: {},
|
||||
editingTradeId: 0,
|
||||
initialStockOpened: false,
|
||||
screenerSetup: null,
|
||||
screenerSetupKey: "",
|
||||
screenerSetupRequestKey: "",
|
||||
screenerSetupPromise: null,
|
||||
selectedRegime: "",
|
||||
selectedStrategy: null,
|
||||
customStrategyDraft: null,
|
||||
screenerRunning: false,
|
||||
screenerRunningMode: "",
|
||||
screenerResults: { smart: null, curated: null, quant: null },
|
||||
screenerResultContexts: { smart: null, curated: null, quant: null },
|
||||
screenerResultStore: {},
|
||||
screenerTracking: null,
|
||||
screenerMode: ["smart", "curated", "quant"].includes(localStorage.getItem("xiaobaiScreenerMode"))
|
||||
? localStorage.getItem("xiaobaiScreenerMode")
|
||||
: "smart",
|
||||
curatedCategory: "全部",
|
||||
curatedSchool: "全部",
|
||||
curatedQuery: "",
|
||||
curatedViewMode: localStorage.getItem("xiaobaiCuratedViewMode") === "grid" ? "grid" : "list",
|
||||
selectedCuratedStrategyId: 0,
|
||||
quantFilters: [],
|
||||
quantScores: [],
|
||||
alerts: [],
|
||||
alertFilter: "all",
|
||||
alertUnreadCount: 0,
|
||||
assistantMessages: [],
|
||||
assistantLoading: false,
|
||||
assistantController: null,
|
||||
screenerMobileView: "strategy",
|
||||
sentimentHistory: null,
|
||||
sentimentRange: 20,
|
||||
sentimentHistoryKey: "",
|
||||
sentimentLoading: false,
|
||||
mentorSetup: null,
|
||||
selectedMentorId: "",
|
||||
mentorMessages: [],
|
||||
mentorLoading: false,
|
||||
mentorQuery: "",
|
||||
mentorGrade: "all",
|
||||
mentorDirectoryOpen: false,
|
||||
mentorSortMode: false,
|
||||
mentorSavingPreferences: false,
|
||||
mentorController: null,
|
||||
heavenSetup: null,
|
||||
heavenManualData: null,
|
||||
personalField: null,
|
||||
heavenPanel: "trend",
|
||||
heavenInterpretations: { trend: "", fortune: "", heart: "" },
|
||||
heavenReadingMode: "trend",
|
||||
heavenReadingTab: "current",
|
||||
heavenReadingHistory: { trend: [], fortune: [], heart: [] },
|
||||
heavenReadingSelectedId: 0,
|
||||
heavenReadingLoading: false,
|
||||
heavenReadingError: "",
|
||||
heartStage: "intro",
|
||||
heartTimer: null,
|
||||
heartSeconds: HEART_BREATH_TOTAL_MS / 1000,
|
||||
heartBreathingEndsAt: 0,
|
||||
heartLines: [],
|
||||
heartThrows: [],
|
||||
heartHexagram: null,
|
||||
heartCurtainTimer: null,
|
||||
heartStageToken: 0,
|
||||
heartRevealToken: 0,
|
||||
heavenPerformanceKey: "",
|
||||
heavenPerformancePanels: new Set(),
|
||||
heavenPerformanceActive: "",
|
||||
dashboardLoading: false,
|
||||
dashboardRequestSequence: 0,
|
||||
heavenRequestSequence: 0,
|
||||
dashboardRequestDate: "",
|
||||
adminModels: [],
|
||||
globalSearchResults: [],
|
||||
globalSearchActiveIndex: -1,
|
||||
globalSearchRequestSequence: 0,
|
||||
};
|
||||
const state = window.XiaobaiState.create({
|
||||
session: {
|
||||
user: null,
|
||||
csrfToken: "",
|
||||
authMode: "login",
|
||||
started: false,
|
||||
activeView: "sentimentCycleView",
|
||||
dashboardLoading: false,
|
||||
dashboardRequestSequence: 0,
|
||||
dashboardRequestDate: "",
|
||||
adminModels: [],
|
||||
globalSearchResults: [],
|
||||
globalSearchActiveIndex: -1,
|
||||
globalSearchRequestSequence: 0,
|
||||
},
|
||||
market: {
|
||||
dashboard: null,
|
||||
filter: "all",
|
||||
query: "",
|
||||
sortKey: "streak",
|
||||
sortDirection: "desc",
|
||||
brokenQuery: "",
|
||||
brokenSortKey: "",
|
||||
brokenSortDirection: "desc",
|
||||
downQuery: "",
|
||||
downSortKey: "",
|
||||
downSortDirection: "asc",
|
||||
yesterdayFilter: "all",
|
||||
yesterdayQuery: "",
|
||||
yesterdaySortKey: "",
|
||||
yesterdaySortDirection: "desc",
|
||||
dragonTiger: null,
|
||||
dragonViewMode: "daily",
|
||||
dragonFilter: "all",
|
||||
dragonQuery: "",
|
||||
selectedDragonTraderId: "",
|
||||
hotMoneyProfiles: null,
|
||||
hotMoneyProfileQuery: "",
|
||||
selectedHotMoneyProfileId: "",
|
||||
rotationHistory: null,
|
||||
rotationHistoryKey: "",
|
||||
rotationSelectedSector: "",
|
||||
rotationSelectedDate: "",
|
||||
rotationMembers: null,
|
||||
rotationMembersKey: "",
|
||||
rotationMembersLoading: false,
|
||||
rotationOrder: localStorage.getItem("xiaobaiRotationOrder") === "latest" ? "latest" : "oldest",
|
||||
rotationLoading: false,
|
||||
auctionData: null,
|
||||
auctionDataset: "focus",
|
||||
auctionFilter: "all",
|
||||
auctionQuery: "",
|
||||
auctionSortKey: "attention_score",
|
||||
auctionSortDirection: "desc",
|
||||
auctionLoading: false,
|
||||
auctionTimer: null,
|
||||
themeLibrary: null,
|
||||
themeQuery: "",
|
||||
selectedThemeCode: "",
|
||||
themeDetail: null,
|
||||
themeLoading: false,
|
||||
popularityData: null,
|
||||
popularitySource: "combined",
|
||||
popularityQuery: "",
|
||||
popularityLoading: false,
|
||||
expandedLadderLevels: new Set(),
|
||||
ladderSortMode: "time",
|
||||
sentimentHistory: null,
|
||||
sentimentRange: 20,
|
||||
sentimentHistoryKey: "",
|
||||
sentimentLoading: false,
|
||||
},
|
||||
details: {
|
||||
stockDetail: null,
|
||||
activeStock: null,
|
||||
stockDetailChartMode: "daily",
|
||||
stockDetailIntraday: null,
|
||||
stockDetailRequestSequence: 0,
|
||||
entityDetailItem: null,
|
||||
entityDetailPayload: null,
|
||||
entityDetailChartMode: "daily",
|
||||
entityDetailIntraday: null,
|
||||
entityDetailRequestSequence: 0,
|
||||
stockPreviewCode: "",
|
||||
stockPreviewType: "stock",
|
||||
stockPreviewItem: null,
|
||||
stockPreviewPayload: null,
|
||||
stockPreviewChart: "daily",
|
||||
stockPreviewFallback: null,
|
||||
initialStockOpened: false,
|
||||
},
|
||||
review: {
|
||||
watchlist: [],
|
||||
watchlistSelection: null,
|
||||
watchlistSearchResults: [],
|
||||
watchlistSearchRequestSequence: 0,
|
||||
editingDailyNoteId: 0,
|
||||
notes: [],
|
||||
tradeEntries: [],
|
||||
tradeSummary: {},
|
||||
editingTradeId: 0,
|
||||
alerts: [],
|
||||
alertFilter: "all",
|
||||
alertUnreadCount: 0,
|
||||
assistantMessages: [],
|
||||
assistantLoading: false,
|
||||
assistantController: null,
|
||||
},
|
||||
screener: {
|
||||
screenerSetup: null,
|
||||
screenerSetupKey: "",
|
||||
screenerSetupRequestKey: "",
|
||||
screenerSetupPromise: null,
|
||||
selectedRegime: "",
|
||||
selectedStrategy: null,
|
||||
customStrategyDraft: null,
|
||||
screenerRunning: false,
|
||||
screenerRunningMode: "",
|
||||
screenerResults: { smart: null, curated: null, quant: null },
|
||||
screenerResultContexts: { smart: null, curated: null, quant: null },
|
||||
screenerResultStore: {},
|
||||
screenerTracking: null,
|
||||
screenerMode: ["smart", "curated", "quant"].includes(localStorage.getItem("xiaobaiScreenerMode"))
|
||||
? localStorage.getItem("xiaobaiScreenerMode")
|
||||
: "smart",
|
||||
curatedCategory: "全部",
|
||||
curatedSchool: "全部",
|
||||
curatedQuery: "",
|
||||
curatedViewMode: localStorage.getItem("xiaobaiCuratedViewMode") === "grid" ? "grid" : "list",
|
||||
selectedCuratedStrategyId: 0,
|
||||
quantFilters: [],
|
||||
quantScores: [],
|
||||
screenerMobileView: "strategy",
|
||||
},
|
||||
mentor: {
|
||||
mentorSetup: null,
|
||||
selectedMentorId: "",
|
||||
mentorMessages: [],
|
||||
mentorLoading: false,
|
||||
mentorQuery: "",
|
||||
mentorGrade: "all",
|
||||
mentorDirectoryOpen: false,
|
||||
mentorSortMode: false,
|
||||
mentorSavingPreferences: false,
|
||||
mentorController: null,
|
||||
},
|
||||
heaven: {
|
||||
heavenSetup: null,
|
||||
heavenManualData: null,
|
||||
personalField: null,
|
||||
heavenPanel: "trend",
|
||||
heavenInterpretations: { trend: "", fortune: "", heart: "" },
|
||||
heavenReadingMode: "trend",
|
||||
heavenReadingTab: "current",
|
||||
heavenReadingHistory: { trend: [], fortune: [], heart: [] },
|
||||
heavenReadingSelectedId: 0,
|
||||
heavenReadingLoading: false,
|
||||
heavenReadingError: "",
|
||||
heartStage: "intro",
|
||||
heartTimer: null,
|
||||
heartSeconds: HEART_BREATH_TOTAL_MS / 1000,
|
||||
heartBreathingEndsAt: 0,
|
||||
heartLines: [],
|
||||
heartThrows: [],
|
||||
heartHexagram: null,
|
||||
heartCurtainTimer: null,
|
||||
heartStageToken: 0,
|
||||
heartRevealToken: 0,
|
||||
heavenPerformanceKey: "",
|
||||
heavenPerformancePanels: new Set(),
|
||||
heavenPerformanceActive: "",
|
||||
heavenRequestSequence: 0,
|
||||
},
|
||||
});
|
||||
|
||||
window.XiaobaiAPI.configure({
|
||||
csrfToken: () => state.csrfToken,
|
||||
onUnauthorized: () => showAuthGate("登录状态已失效,请重新登录。"),
|
||||
});
|
||||
|
||||
const elements = {
|
||||
tradeDate: document.querySelector("#tradeDate"),
|
||||
@@ -4698,39 +4717,16 @@ function scheduleMentorRender() {
|
||||
}
|
||||
|
||||
async function streamMentorRequest(body, signal, onDelta, onMeta) {
|
||||
const response = await fetch("/api/mentors/chat", {
|
||||
await window.XiaobaiAPI.streamNdjson("/api/mentors/chat", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...(state.csrfToken ? { "X-CSRF-Token": state.csrfToken } : {}),
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
body,
|
||||
signal,
|
||||
errorMessage: "问师暂不可用",
|
||||
onEvent: (event) => {
|
||||
if (event.type === "delta") onDelta(String(event.content || ""));
|
||||
if (event.type === "meta") onMeta(event);
|
||||
},
|
||||
});
|
||||
if (!response.ok) {
|
||||
const payload = await response.json().catch(() => ({}));
|
||||
throw new Error(payload.error || "问师暂不可用");
|
||||
}
|
||||
if (!response.body) throw new Error("当前浏览器不支持流式回答");
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = "";
|
||||
const consume = (line) => {
|
||||
if (!line.trim()) return;
|
||||
const event = JSON.parse(line);
|
||||
if (event.type === "delta") onDelta(String(event.content || ""));
|
||||
if (event.type === "meta") onMeta(event);
|
||||
if (event.type === "error") throw new Error(event.error || "问师回答失败");
|
||||
};
|
||||
while (true) {
|
||||
const { value, done } = await reader.read();
|
||||
buffer += decoder.decode(value || new Uint8Array(), { stream: !done });
|
||||
const lines = buffer.split("\n");
|
||||
buffer = lines.pop() || "";
|
||||
lines.forEach(consume);
|
||||
if (done) break;
|
||||
}
|
||||
if (buffer.trim()) consume(buffer);
|
||||
}
|
||||
|
||||
function useMentorQuickPrompt(prompt) {
|
||||
@@ -7907,41 +7903,15 @@ async function sendAssistantQuestion(event) {
|
||||
}
|
||||
|
||||
async function streamAssistantRequest(question, signal, onDelta) {
|
||||
const response = await fetch("/api/assistant/chat", {
|
||||
await window.XiaobaiAPI.streamNdjson("/api/assistant/chat", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...(state.csrfToken ? { "X-CSRF-Token": state.csrfToken } : {}),
|
||||
},
|
||||
body: JSON.stringify({ question, trade_date: elements.tradeDate.value }),
|
||||
body: { question, trade_date: elements.tradeDate.value },
|
||||
signal,
|
||||
});
|
||||
if (!response.ok) {
|
||||
const payload = await response.json().catch(() => ({}));
|
||||
throw new Error(payload.error || "复盘助手暂不可用");
|
||||
}
|
||||
if (!response.body) throw new Error("当前浏览器不支持流式回答");
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = "";
|
||||
while (true) {
|
||||
const { value, done } = await reader.read();
|
||||
buffer += decoder.decode(value || new Uint8Array(), { stream: !done });
|
||||
const lines = buffer.split("\n");
|
||||
buffer = lines.pop() || "";
|
||||
for (const line of lines) {
|
||||
if (!line.trim()) continue;
|
||||
const event = JSON.parse(line);
|
||||
errorMessage: "复盘助手暂不可用",
|
||||
onEvent: (event) => {
|
||||
if (event.type === "delta") onDelta(String(event.content || ""));
|
||||
if (event.type === "error") throw new Error(event.error || "复盘助手回答失败");
|
||||
}
|
||||
if (done) break;
|
||||
}
|
||||
if (buffer.trim()) {
|
||||
const event = JSON.parse(buffer);
|
||||
if (event.type === "delta") onDelta(String(event.content || ""));
|
||||
if (event.type === "error") throw new Error(event.error || "复盘助手回答失败");
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function stopAssistantResponse() {
|
||||
@@ -9167,21 +9137,7 @@ function formatMoneyMillion(value) {
|
||||
}
|
||||
|
||||
async function apiRequest(url, method = "GET", body = null, requestOptions = {}) {
|
||||
const options = { method, headers: {}, signal: requestOptions.signal };
|
||||
if (method !== "GET" && state.csrfToken) {
|
||||
options.headers["X-CSRF-Token"] = state.csrfToken;
|
||||
}
|
||||
if (body !== null) {
|
||||
options.headers["Content-Type"] = "application/json";
|
||||
options.body = JSON.stringify(body);
|
||||
}
|
||||
const response = await fetch(url, options);
|
||||
const payload = await response.json();
|
||||
if (response.status === 401 && !url.startsWith("/api/auth/")) {
|
||||
showAuthGate("登录状态已失效,请重新登录。");
|
||||
}
|
||||
if (!response.ok || payload.error) throw new Error(payload.error || "请求失败");
|
||||
return payload;
|
||||
return window.XiaobaiAPI.request(url, method, body, requestOptions);
|
||||
}
|
||||
|
||||
function setLoading(loading, text = "正在加载复盘数据", context = "default") {
|
||||
|
||||
+3
-1
@@ -1865,7 +1865,9 @@
|
||||
|
||||
<script src="/vendor/lucide.min.js" defer></script>
|
||||
<script src="/ui-core.js" defer></script>
|
||||
<script src="/shared/state.js?v=20260729-1" defer></script>
|
||||
<script src="/shared/api.js?v=20260729-1" defer></script>
|
||||
<script src="/heaven-loading-v2.js?v=20260728-2" defer></script>
|
||||
<script src="/app.js?v=20260728-3" defer></script>
|
||||
<script src="/app.js?v=20260729-4" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
(function exposeApiClient(global) {
|
||||
"use strict";
|
||||
|
||||
let csrfTokenSupplier = () => "";
|
||||
let unauthorizedHandler = () => {};
|
||||
|
||||
class ApiError extends Error {
|
||||
constructor(message, status = 0, payload = null) {
|
||||
super(message);
|
||||
this.name = "ApiError";
|
||||
this.status = status;
|
||||
this.payload = payload;
|
||||
}
|
||||
}
|
||||
|
||||
function configure(options = {}) {
|
||||
if (typeof options.csrfToken === "function") csrfTokenSupplier = options.csrfToken;
|
||||
if (typeof options.onUnauthorized === "function") unauthorizedHandler = options.onUnauthorized;
|
||||
}
|
||||
|
||||
function requestOptions(method, body, signal) {
|
||||
const normalizedMethod = String(method || "GET").toUpperCase();
|
||||
const options = { method: normalizedMethod, headers: {}, signal };
|
||||
const csrfToken = csrfTokenSupplier();
|
||||
if (!["GET", "HEAD", "OPTIONS"].includes(normalizedMethod) && csrfToken) {
|
||||
options.headers["X-CSRF-Token"] = csrfToken;
|
||||
}
|
||||
if (body !== null && body !== undefined) {
|
||||
options.headers["Content-Type"] = "application/json";
|
||||
options.body = JSON.stringify(body);
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
async function parseJson(response) {
|
||||
try {
|
||||
return await response.json();
|
||||
} catch (_error) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function handleUnauthorized(response, url) {
|
||||
if (response.status === 401 && !String(url).startsWith("/api/auth/")) {
|
||||
unauthorizedHandler({ response, url });
|
||||
}
|
||||
}
|
||||
|
||||
async function request(url, method = "GET", body = null, options = {}) {
|
||||
const response = await fetch(url, requestOptions(method, body, options.signal));
|
||||
const payload = await parseJson(response);
|
||||
handleUnauthorized(response, url);
|
||||
if (!response.ok || payload.error) {
|
||||
throw new ApiError(payload.error || "请求失败", response.status, payload);
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
|
||||
async function streamNdjson(url, options = {}) {
|
||||
const response = await fetch(
|
||||
url,
|
||||
requestOptions(options.method || "POST", options.body, options.signal),
|
||||
);
|
||||
if (!response.ok) {
|
||||
const payload = await parseJson(response);
|
||||
handleUnauthorized(response, url);
|
||||
throw new ApiError(
|
||||
payload.error || options.errorMessage || "流式请求暂不可用",
|
||||
response.status,
|
||||
payload,
|
||||
);
|
||||
}
|
||||
if (!response.body) throw new ApiError("当前浏览器不支持流式回答", response.status);
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = "";
|
||||
const consume = (line) => {
|
||||
if (!line.trim()) return;
|
||||
let event;
|
||||
try {
|
||||
event = JSON.parse(line);
|
||||
} catch (_error) {
|
||||
throw new ApiError("流式响应格式错误", response.status);
|
||||
}
|
||||
if (event.type === "error") {
|
||||
throw new ApiError(event.error || options.errorMessage || "流式请求失败", response.status, event);
|
||||
}
|
||||
options.onEvent?.(event);
|
||||
};
|
||||
while (true) {
|
||||
const { value, done } = await reader.read();
|
||||
buffer += decoder.decode(value || new Uint8Array(), { stream: !done });
|
||||
const lines = buffer.split("\n");
|
||||
buffer = lines.pop() || "";
|
||||
lines.forEach(consume);
|
||||
if (done) break;
|
||||
}
|
||||
if (buffer.trim()) consume(buffer);
|
||||
}
|
||||
|
||||
global.XiaobaiAPI = Object.freeze({ ApiError, configure, request, streamNdjson });
|
||||
})(window);
|
||||
@@ -0,0 +1,44 @@
|
||||
(function exposeStateStore(global) {
|
||||
"use strict";
|
||||
|
||||
function create(domains) {
|
||||
const owners = new Map();
|
||||
const stores = {};
|
||||
Object.entries(domains).forEach(([domain, values]) => {
|
||||
stores[domain] = { ...values };
|
||||
Object.keys(values).forEach((key) => {
|
||||
if (owners.has(key)) throw new Error(`Duplicate state field: ${key}`);
|
||||
owners.set(key, domain);
|
||||
});
|
||||
});
|
||||
|
||||
const proxy = new Proxy({}, {
|
||||
get(_target, key) {
|
||||
if (key === "domain") return (name) => stores[name];
|
||||
if (key === "domains") return Object.freeze({ ...stores });
|
||||
if (typeof key !== "string" || !owners.has(key)) return undefined;
|
||||
return stores[owners.get(key)][key];
|
||||
},
|
||||
set(_target, key, value) {
|
||||
if (typeof key !== "string" || !owners.has(key)) {
|
||||
throw new Error(`Unregistered application state field: ${String(key)}`);
|
||||
}
|
||||
stores[owners.get(key)][key] = value;
|
||||
return true;
|
||||
},
|
||||
has(_target, key) {
|
||||
return key === "domain" || key === "domains" || owners.has(key);
|
||||
},
|
||||
ownKeys() {
|
||||
return [...owners.keys()];
|
||||
},
|
||||
getOwnPropertyDescriptor(_target, key) {
|
||||
if (!owners.has(key)) return undefined;
|
||||
return { enumerable: true, configurable: true };
|
||||
},
|
||||
});
|
||||
return proxy;
|
||||
}
|
||||
|
||||
global.XiaobaiState = Object.freeze({ create });
|
||||
})(window);
|
||||
Reference in New Issue
Block a user