From ed1c7cefd023c6be5d52c872c76bf2e02b650650 Mon Sep 17 00:00:00 2001 From: Sleck <15660928620@163.com> Date: Tue, 18 Feb 2025 18:10:06 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=20=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../LotteryContentWithDirHandle.tsx | 208 ++++++++++++------ src/pages/LotteryPage/LotteryTree.tsx | 14 +- src/services/lottery/lotteryNotionQueries.ts | 76 +++++++ 3 files changed, 231 insertions(+), 67 deletions(-) diff --git a/src/pages/LotteryPage/LotteryContentWithDirHandle.tsx b/src/pages/LotteryPage/LotteryContentWithDirHandle.tsx index 1982ef5..c9a3ae5 100644 --- a/src/pages/LotteryPage/LotteryContentWithDirHandle.tsx +++ b/src/pages/LotteryPage/LotteryContentWithDirHandle.tsx @@ -1,7 +1,7 @@ -import React, { useEffect, useState, useContext } from "react"; +import React, { useEffect, useState, useContext, useMemo } from "react"; import { Button, Card, Flex, Layout, Menu, Space, Typography, theme, BackTop, Spin } from "antd"; import { Content } from "antd/es/layout/layout"; -import LotteryTree from "./LotteryTree"; +import {LotteryTree} from "./LotteryTree"; import Sider from "antd/es/layout/Sider"; import { useLotteryData } from "./UseLotteryData"; import { @@ -12,8 +12,7 @@ import { UpOutlined } from "@ant-design/icons"; import { WorkshopPageContext } from "../WorkshopPage/WorkshopPageContext"; -import { NOTION_DATABASE_LOTTERY } from "../../services/lottery/lotteryNotionQueries"; - +import { NOTION_DATABASE_LOTTERY, LotteryConfig, areLotteryConfigsEqual } from "../../services/lottery/lotteryNotionQueries"; const { Text } = Typography; const COMMODITY_PATH = "CodeFunCore/src/main/resources/net/easecation/codefuncore/lottery/notion/"; @@ -25,15 +24,89 @@ const LotteryContentWithDirHandle: React.FC = () => { const { dirHandle, ensurePermission, messageApi, readFile, writeFile } = useContext(WorkshopPageContext); - const [currentTypes, setCurrentTypes] = useState([]); - const [currentType, setCurrentType] = useState(null); - const [missingTypes, setMissingTypes] = useState([]); // 远端新增数据 - const [localJson, setLocalJson] = useState<{ [key: string]: any } | null>(null); - const [loadingLocalJson, setLoadingLocalJson] = useState(false); - const [localFileExists, setLocalFileExists] = useState(false); - const [remoteJsonMap, setRemoteJsonMap] = useState<{ [key: string]: any }>({}); - const [remoteJsonLoading, setRemoteJsonLoading] = useState(true); // 初始化加载状态 - const [saving, setSaving] = useState(false); + const [currentTypes, setCurrentTypes] = useState([]); // Local types + const [currentType, setCurrentType] = useState(null); // Current selected type + const [missingTypes, setMissingTypes] = useState([]); // Missing types from remote + const [modifiedKeys, setModifiedKeys] = useState([]); // Local types + const [localJson, setLocalJson] = useState<{ [key: string]: any }>({}); // Local JSON data + const [loadingLocalJson, setLoadingLocalJson] = useState(false); // Loading state for local JSON + const [localFileExists, setLocalFileExists] = useState(false); // Check if local file exists + const [remoteJsonMap, setRemoteJsonMap] = useState<{ [key: string]: any }>({}); // Remote JSON data + const [remoteJsonLoading, setRemoteJsonLoading] = useState(true); // Loading state for remote JSON + const [saving, setSaving] = useState(false); // Saving state for sync + + // 🔹 Compare the values of keys in both local and remote JSON + const compareJsonData = (localJson: { [key: string]: any }, remoteJsonMap: { [key: string]: any }) => { + const modifiedKeys: string[] = []; + for (const key in localJson) { + if (localJson.hasOwnProperty(key) && remoteJsonMap.hasOwnProperty(key)) { + // If local and remote JSON data for the same key is different + if (JSON.stringify(localJson[key]) !== JSON.stringify(remoteJsonMap[key])) { + modifiedKeys.push(key); + } + } + } + return modifiedKeys; + }; + + // 🔹 Generate the menu items with missing and modified types + const generateMenuItems = () => { + return [ + // ✅ 远端新增数据(missingTypes) + ...missingTypes.map((type) => ({ + key: type, + label: ( + + + 新增 + + {type} + + ), + })), + + // // ✅ 远端有但本地有差异的 key(modified keys) + ...modifiedKeys.map((key) => ({ + key, + label: ( + + + 修改 + + {key} {/* Key with modification */} + + ), + })), + + // ✅ 本地已存在的数据(currentTypes) + ...currentTypes.filter(type => !modifiedKeys.includes(type)).map((type) => ({ + key: type, + label: type, // 默认样式 + })), + ]; + }; // 🔹 获取 notion.json 本地文件内容并更新 currentTypes const loadLocalCurrent = async () => { @@ -42,7 +115,7 @@ const LotteryContentWithDirHandle: React.FC = () => { if (!hasPermission || !dirHandle) return; const text = await readFile(`${COMMODITY_PATH}notion.json`); const format: { types: string[] } = JSON.parse(text); - + setCurrentTypes(format.types); setCurrentType(format.types[0]); // 默认选中第一个 } catch (error: any) { @@ -53,6 +126,32 @@ const LotteryContentWithDirHandle: React.FC = () => { // 获取远端 Lottery 数据 const { fileArray: remoteJsonMapData, refetch } = useLotteryData(NOTION_DATABASE_LOTTERY); + // 更新侧边栏数据 + const updateItems = () => { + let modifieds: string[] = []; + // 校验 remoteJsonMap 和 localJson 中的每个 LotteryConfig 是否相同 + Object.keys(remoteJsonMap).forEach((key) => { + if (remoteJsonMap[key] && localJson[key]) { + + // 假设 remoteJsonMap[key] 和 localJson[key] 都是 LotteryConfig 类型 + let temp = Object.keys(remoteJsonMap[key])[0]; + const remotConfig: LotteryConfig = remoteJsonMap[key][temp]; + const localConfig: LotteryConfig = localJson[key][temp]; + + // 校验两个 LotteryConfig 是否相同 + const isEqual = areLotteryConfigsEqual(remotConfig, localConfig); + + if (!isEqual) { + // console.log(`${key} 的配置有差异`); + modifieds.push(key); + } else { + // console.log(`${key} 的配置无差异`); + } + } + }); + setModifiedKeys(modifieds); + } + // 监听远端数据加载,并更新数据 useEffect(() => { if (remoteJsonMapData && Object.keys(remoteJsonMapData).length > 0) { @@ -61,6 +160,7 @@ const LotteryContentWithDirHandle: React.FC = () => { // 计算远端数据中 `currentTypes` 没有的 key const missingKeys = Object.keys(remoteJsonMapData).filter((key) => !currentTypes.includes(key)); setMissingTypes(missingKeys); + updateItems(); } }, [remoteJsonMapData, currentTypes]); @@ -73,21 +173,32 @@ const LotteryContentWithDirHandle: React.FC = () => { // 加载本地 JSON 文件 const loadLocalFile = async () => { - if (!currentType) return; + if (currentTypes.length === 0) return; // 如果没有类型,直接返回 setLoadingLocalJson(true); + + const allLocalJsonData: { [key: string]: any } = {}; // 用来存储所有加载的 JSON 数据 try { const hasPermission = await ensurePermission("read"); if (!hasPermission || !dirHandle) return; - const text = await readFile(`${COMMODITY_PATH}${currentType}.json`); - setLocalJson(JSON.parse(text)); - setLocalFileExists(true); - } catch (error: any) { - if (error?.message === "NotFoundError") { - setLocalFileExists(false); - setLocalJson(null); - } else { - messageApi.error("读取本地文件出错: " + error?.message); + + // 遍历 currentTypes 中的每个类型,尝试读取对应的文件 + for (const type of currentTypes) { + try { + const text = await readFile(`${COMMODITY_PATH}${type}.json`); + allLocalJsonData[type] = JSON.parse(text); // 存储读取的 JSON 数据 + } catch (error: any) { + if (error?.message === "NotFoundError") { + allLocalJsonData[type] = null; // 如果文件不存在,存储 null + } else { + messageApi.error(`读取 ${type}.json 文件出错: ${error?.message}`); // 错误提示 + } + } } + setLocalJson(allLocalJsonData); // 更新本地 JSON 数据 + setLocalFileExists(true); // 文件存在标记 + updateItems();//更新侧边栏 + } catch (error: any) { + messageApi.error("读取本地文件出错: " + error?.message); } finally { setLoadingLocalJson(false); } @@ -98,7 +209,7 @@ const LotteryContentWithDirHandle: React.FC = () => { if (dirHandle && currentType) { loadLocalFile(); } - }, [dirHandle, currentType]); + }, [dirHandle, currentType, currentTypes]); // 远端数据加载 const handleLoadRemoteJson = async () => { @@ -128,19 +239,21 @@ const LotteryContentWithDirHandle: React.FC = () => { `${COMMODITY_PATH}${currentType}.json`, JSON.stringify(remoteJsonMap[currentType], null, 4) ); - - setLocalJson(remoteJsonMap[currentType]); // 更新本地 JSON 数据 messageApi.success(`"${currentType}" 同步成功!`); // ✅ 检查 currentType 是否已经在 currentTypes 中 if (!currentTypes.includes(currentType)) { - const updatedTypes = [...currentTypes, currentType].sort(); // 按字母排序 + // 更新侧边栏 + let updatedTypes = missingTypes.filter(type => type !== currentType); + setMissingTypes(updatedTypes); + updatedTypes = [...currentTypes, currentType].sort(); // 按字母排序 setCurrentTypes(updatedTypes); // 更新 UI // ✅ 更新 `notion.json` const notionFilePath = `${COMMODITY_PATH}notion.json`; const updatedNotionData = JSON.stringify({ types: updatedTypes }, null, 4); await writeFile(notionFilePath, updatedNotionData); + setCurrentType(currentType); messageApi.success(`"${currentType}" 已添加到 notion.json`); } } catch (error: any) { @@ -150,40 +263,7 @@ const LotteryContentWithDirHandle: React.FC = () => { } }; - // 🔹 生成菜单 items(本地的 currentTypes + 远端缺失的 missingTypes) - const menuItems = [ - // ✅ 远端新增数据(missingTypes) - ...missingTypes.map((type) => ({ - key: type, - label: ( - - - 新增 - - {type} {/* 后面的 type 保持默认样式 */} - - ), - })), - - // ✅ 本地已存在的数据(currentTypes) - ...currentTypes.map((type) => ({ - key: type, - label: type, // 默认样式 - })), - ]; - - + const menuItems = useMemo(() => generateMenuItems(), [modifiedKeys, missingTypes, currentTypes, localJson]); return ( { } loading={loadingLocalJson} > - {localFileExists && localJson ? ( - + {localFileExists && currentType && localJson[currentType] ? ( + ) : ( 本地 JSON 文件未找到 )} diff --git a/src/pages/LotteryPage/LotteryTree.tsx b/src/pages/LotteryPage/LotteryTree.tsx index f36ca62..c08432c 100644 --- a/src/pages/LotteryPage/LotteryTree.tsx +++ b/src/pages/LotteryPage/LotteryTree.tsx @@ -1,9 +1,18 @@ import { Empty, Typography } from "antd"; import React, { useEffect } from "react"; +import { LotteryConfig, LotteryConfigItem } from "../../services/lottery/lotteryNotionQueries"; -const { Text, Paragraph } = Typography; +export const { Text, Paragraph } = Typography; -const LotteryTree: React.FC<{[key:string]: any}> = ({ checkable, fullJson, differentParts, checkedKeys, setCheckedKeys }) => { + +export interface DifferentPart { + key: string; + mode: 'add' | 'remove' | 'changed'; + from?: LotteryConfigItem + to?: LotteryConfigItem +} + +export const LotteryTree: React.FC<{[key:string]: any}> = ({ checkable, fullJson, differentParts, checkedKeys, setCheckedKeys }) => { useEffect(() => { if (checkable) { setCheckedKeys!(Object.keys(fullJson)); @@ -34,4 +43,3 @@ const LotteryTree: React.FC<{[key:string]: any}> = ({ checkable, fullJson, diffe ); } -export default LotteryTree; \ No newline at end of file diff --git a/src/services/lottery/lotteryNotionQueries.ts b/src/services/lottery/lotteryNotionQueries.ts index b24f80d..0eef518 100644 --- a/src/services/lottery/lotteryNotionQueries.ts +++ b/src/services/lottery/lotteryNotionQueries.ts @@ -1,5 +1,81 @@ export const NOTION_DATABASE_LOTTERY = '9e151c3d30b14d1bae8dd972d17198c1'; +export interface LotteryCollback { + type: string; + merchandise: string; +} + +export interface LotteryConfigItem { + weight: number; + merchandises?: [string] + subExchanges?: string; + condition?: string; + callback?: LotteryCollback +} + +export interface LotteryConfig { + spend?: string; + gain: LotteryConfigItem[] +} + + +// 校验两个 LotteryConfigItem 是否相同 +const areConfigItemsEqual = (item1: LotteryConfigItem, item2: LotteryConfigItem): boolean => { + // 比较 weight + if (item1.weight !== item2.weight) return false; + + // 比较 merchandises (如果有) + if (item1.merchandises && item2.merchandises) { + if (item1.merchandises.length !== item2.merchandises.length) return false; + for (let i = 0; i < item1.merchandises.length; i++) { + if (item1.merchandises[i] !== item2.merchandises[i]) return false; + } + } else if (item1.merchandises || item2.merchandises) { + return false; // 一个有 merchandises,另一个没有 + } + + // 比较 subExchanges: 处理 null, undefined 和 空字符串 + const normalizeSubExchanges = (subExchanges: string | null | undefined): string | null => + subExchanges === undefined || subExchanges === null ? null : subExchanges.trim() === "" ? null : subExchanges; + + if (normalizeSubExchanges(item1.subExchanges) !== normalizeSubExchanges(item2.subExchanges)) return false; + + // 比较 condition: 处理 null, undefined 和 空字符串 + const normalizeCondition = (condition: string | null | undefined): string | null => + condition === undefined || condition === null ? null : condition.trim() === "" ? null : condition; + + if (normalizeCondition(item1.condition) !== normalizeCondition(item2.condition)) return false; + + // 比较 callback (如果有) + if (item1.callback && item2.callback) { + if (item1.callback.type !== item2.callback.type) return false; + if (item1.callback.merchandise !== item2.callback.merchandise) return false; + } else if (item1.callback || item2.callback) { + return false; // 一个有 callback,另一个没有 + } + + return true; // 如果以上都相等,返回 true +}; + + +// 校验两个 LotteryConfig 是否相同 +export const areLotteryConfigsEqual = (config1: LotteryConfig, config2: LotteryConfig): boolean => { + // 确保 gain 是有效的数组 + if (!Array.isArray(config1.gain) || !Array.isArray(config2.gain)) return false; // 如果 gain 不是数组,则认为两个 config 不相同 + // 比较 gain 数组的长度 + if (config1.gain?.length !== config2.gain?.length) return false; + // 遍历 gain 数组并对比每个 item + for (let i = 0; i < config1.gain.length; i++) { + if (!areConfigItemsEqual(config1.gain[i], config2.gain[i])) { + console.log(JSON.stringify(config1.gain[i])); + console.log(JSON.stringify(config2.gain[i])); + return false; + } + } + + return true; // 所有 items 都相等 +}; + /** * 对应 PHP中 WORKSHOP_TYPES 的每个 typeId -> filter & sorts */