poi-plugin-kai-planner 1.0.12 → 1.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/README.md +15 -52
  2. package/package.json +1 -1
  3. package/src/app/KaiPlannerApp.js +227 -210
  4. package/src/app/tabs/acquisition/AcquisitionPlanForm.js +135 -0
  5. package/src/app/tabs/acquisition/AcquisitionTab.js +499 -0
  6. package/src/app/tabs/acquisition/components/AcquisitionTable.js +300 -0
  7. package/src/app/tabs/daily/DailyTabV2.js +562 -0
  8. package/src/app/tabs/wishlist/CreatePlanFormV2.js +189 -0
  9. package/src/app/tabs/wishlist/WishlistTabV2.js +972 -0
  10. package/src/app/tabs/wishlist/components/WishlistExpandedDetailV2.js +235 -0
  11. package/src/app/tabs/wishlist/components/WishlistTableV2.js +486 -0
  12. package/src/data/static/data_manifest.json +12 -39
  13. package/src/data/static/equip_base_cost.json +14 -0
  14. package/src/data/static/improvement_arrangement.json +124 -60
  15. package/src/data/static/improvement_consume_item.json +74 -2
  16. package/src/data/static/improvement_consume_step.json +36 -0
  17. package/src/services/acquisition/buildAcquisitionViewModel.js +189 -0
  18. package/src/services/common/noteText.js +21 -0
  19. package/src/services/daily/buildDailyViewModel.js +100 -102
  20. package/src/services/planner/calcRemainingPlan.js +172 -169
  21. package/src/services/planner/summarizeShortage.js +15 -6
  22. package/src/services/player/countPlayerEquipByMasterId.js +20 -12
  23. package/src/services/wishlist/buildEquipmentShortageDisplayRows.js +15 -7
  24. package/src/services/wishlist/buildPlanningSnapshot.js +113 -0
  25. package/src/services/wishlist/buildTodayStatusMeta.js +149 -0
  26. package/src/services/wishlist/buildWishlistViewModel.js +73 -39
  27. package/src/services/wishlist/wishlistActions.js +524 -420
  28. package/src/storage/acquisitionPlans/fileStore.js +210 -0
  29. package/src/storage/acquisitionPlans/localStorageStore.js +39 -0
  30. package/src/storage/acquisitionPlans/migrate.js +41 -0
  31. package/src/storage/acquisitionPlans/planStore.js +88 -0
  32. package/src/storage/acquisitionPlans/storeAdapter.js +83 -0
  33. package/src/storage/dailyFavorites/favoritesStore.js +62 -0
  34. package/src/storage/dailyFavorites/fileStore.js +210 -0
  35. package/src/storage/dailyFavorites/localStorageStore.js +39 -0
  36. package/src/storage/dailyFavorites/migrate.js +31 -0
  37. package/src/storage/dailyFavorites/storeAdapter.js +83 -0
  38. package/src/storage/userPlans/planStore.js +69 -64
package/README.md CHANGED
@@ -1,52 +1,15 @@
1
- # poi-plugin-kai-planner
2
-
3
- ## 本地数据
4
-
5
- - 静态原始数据:`src/data/static/*.json`
6
- - 运行时静态缓存:`runtime_data/static_data/cache/*.json`
7
- - 运行时缓存元数据:`runtime_data/static_data/meta.json`
8
-
9
- ## 计划存储
10
-
11
- - 用户计划优先写入:
12
- - 若 Electron 可提供 `userData` 目录,则写入 `<userData>/poi-plugin-kai-planner/userPlans/plans.v1.json`
13
- - 若无法获取稳定用户目录,则回退到 `<plugin_root>/runtime_data/userPlans/plans.v1.json`
14
- - 文件存储不可用时回退 `localStorage`:`kai_planner_user_plans_v1`
15
- - 远端静态数据缓存:
16
- - `runtime_data/static_data/meta.json`
17
- - `runtime_data/static_data/cache/*.json`
18
- - bundled 静态数据基线:`src/data/static/*.json`
19
-
20
- ## 常用命令
21
-
22
- - `npm test`
23
- - `npm run release:check`
24
- - `npm run release:pack`
25
-
26
- ## 文档导航
27
-
28
- - 架构说明:[docs/ARCHITECTURE.md](docs/ARCHITECTURE.md)
29
- - 数据版本策略:[docs/DATA_VERSIONING.md](docs/DATA_VERSIONING.md)
30
- - 计划存储策略:[docs/STORAGE_POLICY.md](docs/STORAGE_POLICY.md)
31
- - Wishlist 状态字段:[docs/WISHLIST_STATE_FIELDS.md](docs/WISHLIST_STATE_FIELDS.md)
32
- - npm/POI 发布流程:[docs/RELEASE_POI_NPM.md](docs/RELEASE_POI_NPM.md)
33
- - 私有仓库更新流程:[docs/PRIVATE_REPO_UPDATE.md](docs/PRIVATE_REPO_UPDATE.md)
34
- - 远端静态数据更新流程:[docs/REMOTE_DATA_UPDATE.md](docs/REMOTE_DATA_UPDATE.md)
35
-
36
- ## 2026-03 UI 更新
37
-
38
- - Daily 页面支持按装备类型筛选;类型名来自 POI 的 `$equipTypes[api_type[3]].api_name`
39
- - Daily 与 Wishlist 表格中的装备名称前都会显示装备类型 icon
40
- - Wishlist 主表中的优先级改为标签样式展示(P0-P5 固定颜色)
41
- - Wishlist 展开明细中的“当前持有”计数会去除改修星数大于 0 的装备以及计划起点装备
42
- - 页面顶部 tab、上一次数据源更新、检查更新与更新提示统一使用 `14px` 字号
43
- - Daily 与 Wishlist 主表的折叠箭头统一为:未展开 `▶`、已展开 `▼`
44
- - Daily 与 Wishlist 中的类型 filter 已调整为更接近 `poi-plugin-item-info` 的 `checkbox + icon` 轻量布局
45
-
46
- ## 装备类型 icon 兼容性说明
47
-
48
- - 当前所有装备类型 icon 均直接使用 POI 宿主的 `SlotitemIcon` 默认渲染尺寸
49
- - Daily 表格、Wishlist 表格、类型 filter 现在都直接从 `views/components/etc/icon` 引入宿主 `SlotitemIcon`
50
- - 插件侧不再对 `SlotitemIcon` 做 `zoom`、`transform: scale(...)` 或其它自定义缩放处理
51
- - 原因是不同用户的 POI / Electron / 系统缩放环境下,非宿主默认缩放可能导致 icon 错位、放大或整页异常
52
- - 若后续仍需优化观感,优先参考 `poi-plugin-item-info` 的宿主友好结构,通过 checkbox / label 布局、外层 gap 、margin 等轻量样式调整观感
1
+ # poi-plugin-kai-planner
2
+
3
+ 用于在 POI 浏览器里管理改修相关信息与个人规划。
4
+
5
+ ## 你可以用这个插件做什么
6
+
7
+ - 在`每日改修`页面查看当天可改修装备、基础消耗、秘书舰要求与可进化状态。
8
+ - 在`每日改修`页面把常看的装备标记为收藏,收藏项会优先排序并长期保存在本地。
9
+ - 在`改修心愿清单`页面创建、编辑、刷新和追踪个人改修计划,查看实时完成进度与资源缺口。
10
+ - 在`装备获取规划`页面管理想提前准备的装备,查看它们与未完成改修计划之间的待消耗关联。
11
+ - 在`Debug`页面查看插件运行时数据与调试信息。
12
+
13
+ ## 技术文档
14
+
15
+ 技术设计、存储策略、页面行为和实现约束请看 [docs/README.md](docs/README.md)。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "poi-plugin-kai-planner",
3
- "version": "1.0.12",
3
+ "version": "1.0.14",
4
4
  "main": "index.js",
5
5
  "author": "aulu",
6
6
  "contributors": ["aulu"],
@@ -1,211 +1,228 @@
1
- // src/app/KaiPlannerApp.js
2
- const React = require("react");
3
- const { DailyTab } = require("./tabs/daily/DailyTab");
4
- const { checkAndUpdateData } = require("../services/static/version/dataUpdateManager");
5
- const { readMeta } = require("../services/static/version/versionStore");
6
-
7
- const placeholderStyle = {
8
- padding: 12,
9
- background: "#111827",
10
- color: "#e5e7eb",
11
- borderRadius: 8,
12
- };
13
-
14
- function pad2(n) {
15
- return String(n).padStart(2, "0");
16
- }
17
-
18
- function formatSimpleTime(iso) {
19
- if (!iso) return "-";
20
- const d = new Date(iso);
21
- if (!Number.isFinite(d.getTime())) return "-";
22
- return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())} ${pad2(d.getHours())}:${pad2(
23
- d.getMinutes()
24
- )}:${pad2(d.getSeconds())}`;
25
- }
26
-
27
- function readBundledManifestUpdatedAt() {
28
- try {
29
- // eslint-disable-next-line global-require
30
- const m = require("../data/static/data_manifest.json");
31
- if (m && m.updated_at) return String(m.updated_at);
32
- } catch {}
33
- return null;
34
- }
35
-
36
- function readEnableDebugFlag() {
37
- try {
38
- // eslint-disable-next-line global-require
39
- const pkg = require("../../package.json");
40
- const poiPlugin = pkg && pkg.poiPlugin ? pkg.poiPlugin : {};
41
- if (poiPlugin.enableDebug == null) return true;
42
- return !!poiPlugin.enableDebug;
43
- } catch {
44
- return true;
45
- }
46
- }
47
-
48
- class KaiPlannerApp extends React.Component {
49
- constructor(props) {
50
- super(props);
51
- this.state = {
52
- tab: "daily", // daily | wishlist | debug
53
- wishlistLoadError: null,
54
- debugLoadError: null,
55
- checkingUpdate: false,
56
- lastUpdateAtText: "-",
57
- updateTip: "",
58
- };
59
- this._WishlistTab = null;
60
- this._DebugTab = null;
61
- this.enableDebug = readEnableDebugFlag();
62
- this.handleCheckUpdate = this.handleCheckUpdate.bind(this);
63
- }
64
-
65
- componentDidMount() {
66
- this.syncUpdateMeta();
67
- this.handleCheckUpdate(false);
68
- }
69
-
70
- syncUpdateMeta() {
71
- try {
72
- const meta = readMeta();
73
- const fallback = readBundledManifestUpdatedAt();
74
- this.setState({
75
- lastUpdateAtText: formatSimpleTime((meta && meta.lastUpdateAt) || fallback),
76
- });
77
- } catch {}
78
- }
79
-
80
- async handleCheckUpdate(force) {
81
- if (this.state.checkingUpdate) return;
82
- this.setState({ checkingUpdate: true, updateTip: force ? "正在检查..." : "" });
83
- try {
84
- const res = await checkAndUpdateData({ force: !!force });
85
- this.syncUpdateMeta();
86
- if (force) {
87
- if (res && res.ok && res.updated) this.setState({ updateTip: `已更新到数据版本 ${res.remoteVersion}` });
88
- else if (res && res.ok && !res.updated) this.setState({ updateTip: "当前已是最新数据" });
89
- else if (res && res.reason === "data_source_not_configured") this.setState({ updateTip: "未配置远端数据源" });
90
- else this.setState({ updateTip: `检查失败${res && res.error ? `:${res.error}` : ""}` });
91
- }
92
- } catch (e) {
93
- if (force) this.setState({ updateTip: `检查失败:${String(e && (e.stack || e))}` });
94
- } finally {
95
- this.setState({ checkingUpdate: false });
96
- }
97
- }
98
-
99
- render() {
100
- const tab = !this.enableDebug && this.state.tab === "debug" ? "daily" : this.state.tab;
101
-
102
- const tabButtonStyle = (key) => ({
103
- padding: "6px 10px",
104
- borderRadius: 8,
105
- cursor: "pointer",
106
- border: "1px solid #374151",
107
- background: tab === key ? "#1f2937" : "transparent",
108
- color: "#e5e7eb",
109
- marginRight: 8,
110
- fontSize: 14,
111
- });
112
-
113
- let content = null;
114
- if (tab === "daily") {
115
- content = React.createElement(DailyTab, {
116
- envWindow: this.props.envWindow,
117
- envCtx: this.props.envCtx,
118
- });
119
- } else if (tab === "wishlist") {
120
- if (!this._WishlistTab && !this.state.wishlistLoadError) {
121
- try {
122
- // eslint-disable-next-line global-require
123
- this._WishlistTab = require("./tabs/wishlist/WishlistTab");
124
- } catch (e) {
125
- this.setState({ wishlistLoadError: String(e && e.stack ? e.stack : e) });
126
- }
127
- }
128
-
129
- if (this.state.wishlistLoadError) {
130
- content = React.createElement(
131
- "div",
132
- { style: { ...placeholderStyle, whiteSpace: "pre-wrap" } },
133
- "【改修心愿清单】模块加载失败(已隔离,不影响 Daily/Debug)\n\n错误信息:\n" + this.state.wishlistLoadError
134
- );
135
- } else if (this._WishlistTab) {
136
- content = React.createElement(this._WishlistTab, {
137
- envWindow: this.props.envWindow,
138
- envCtx: this.props.envCtx,
139
- });
140
- } else {
141
- content = React.createElement("div", { style: placeholderStyle }, "【改修心愿清单】加载中...");
142
- }
143
- } else if (tab === "debug") {
144
- if (!this._DebugTab && !this.state.debugLoadError) {
145
- try {
146
- // eslint-disable-next-line global-require
147
- this._DebugTab = require("./tabs/debug/DebugTab");
148
- } catch (e) {
149
- this.setState({ debugLoadError: String(e && e.stack ? e.stack : e) });
150
- }
151
- }
152
-
153
- if (this.state.debugLoadError) {
154
- content = React.createElement(
155
- "div",
156
- { style: { ...placeholderStyle, whiteSpace: "pre-wrap" } },
157
- "【Debug】模块加载失败(已隔离,不影响 Daily/Wishlist)\n\n错误信息:\n" + this.state.debugLoadError
158
- );
159
- } else if (this._DebugTab) {
160
- content = React.createElement(this._DebugTab, {
161
- envWindow: this.props.envWindow,
162
- envCtx: this.props.envCtx,
163
- });
164
- } else {
165
- content = React.createElement("div", { style: placeholderStyle }, "【Debug】加载中...");
166
- }
167
- } else {
168
- content = React.createElement("div", { style: placeholderStyle }, "未知标签页");
169
- }
170
-
171
- return React.createElement(
172
- "div",
173
- { style: { padding: 12 } },
174
- React.createElement(
175
- "div",
176
- { style: { marginBottom: 12, display: "flex", alignItems: "center", justifyContent: "space-between", gap: 12 } },
177
- React.createElement(
178
- "div",
179
- null,
180
- React.createElement("button", { style: tabButtonStyle("daily"), onClick: () => this.setState({ tab: "daily" }) }, "每日改修"),
181
- React.createElement(
182
- "button",
183
- { style: tabButtonStyle("wishlist"), onClick: () => this.setState({ tab: "wishlist" }) },
184
- "改修心愿清单"
185
- ),
186
- this.enableDebug
187
- ? React.createElement(
188
- "button",
189
- { style: tabButtonStyle("debug"), onClick: () => this.setState({ tab: "debug" }) },
190
- "Debug"
191
- )
192
- : null
193
- ),
194
- React.createElement(
195
- "div",
196
- { style: { display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap", justifyContent: "flex-end" } },
197
- React.createElement("span", { style: { fontSize: 14, opacity: 0.8 } }, `上一次数据源更新:${this.state.lastUpdateAtText}`),
198
- React.createElement(
199
- "button",
200
- { style: tabButtonStyle("__check__"), onClick: () => this.handleCheckUpdate(true), disabled: this.state.checkingUpdate },
201
- this.state.checkingUpdate ? "检查中..." : "检查更新"
202
- ),
203
- this.state.updateTip ? React.createElement("span", { style: { fontSize: 14, opacity: 0.85 } }, this.state.updateTip) : null
204
- )
205
- ),
206
- content
207
- );
208
- }
209
- }
210
-
1
+ const React = require("react");
2
+ const { DailyTabV2 } = require("./tabs/daily/DailyTabV2");
3
+ const { checkAndUpdateData } = require("../services/static/version/dataUpdateManager");
4
+ const { readMeta } = require("../services/static/version/versionStore");
5
+
6
+ const placeholderStyle = {
7
+ padding: 12,
8
+ background: "#111827",
9
+ color: "#e5e7eb",
10
+ borderRadius: 8,
11
+ };
12
+
13
+ function pad2(n) {
14
+ return String(n).padStart(2, "0");
15
+ }
16
+
17
+ function formatSimpleTime(iso) {
18
+ if (!iso) return "-";
19
+ const date = new Date(iso);
20
+ if (!Number.isFinite(date.getTime())) return "-";
21
+ return `${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())} ${pad2(date.getHours())}:${pad2(
22
+ date.getMinutes()
23
+ )}:${pad2(date.getSeconds())}`;
24
+ }
25
+
26
+ function readBundledManifestUpdatedAt() {
27
+ try {
28
+ // eslint-disable-next-line global-require
29
+ const manifest = require("../data/static/data_manifest.json");
30
+ if (manifest && manifest.updated_at) return String(manifest.updated_at);
31
+ } catch {}
32
+ return null;
33
+ }
34
+
35
+ function readEnableDebugFlag() {
36
+ try {
37
+ // eslint-disable-next-line global-require
38
+ const pkg = require("../../package.json");
39
+ const poiPlugin = pkg && pkg.poiPlugin ? pkg.poiPlugin : {};
40
+ if (poiPlugin.enableDebug == null) return true;
41
+ return !!poiPlugin.enableDebug;
42
+ } catch {
43
+ return true;
44
+ }
45
+ }
46
+
47
+ class KaiPlannerApp extends React.Component {
48
+ constructor(props) {
49
+ super(props);
50
+ this.state = {
51
+ tab: "daily",
52
+ wishlistLoadError: null,
53
+ acquisitionLoadError: null,
54
+ debugLoadError: null,
55
+ checkingUpdate: false,
56
+ lastUpdateAtText: "-",
57
+ updateTip: "",
58
+ };
59
+ this._WishlistTab = null;
60
+ this._AcquisitionTab = null;
61
+ this._DebugTab = null;
62
+ this.enableDebug = readEnableDebugFlag();
63
+ this.handleCheckUpdate = this.handleCheckUpdate.bind(this);
64
+ }
65
+
66
+ componentDidMount() {
67
+ this.syncUpdateMeta();
68
+ this.handleCheckUpdate(false);
69
+ }
70
+
71
+ syncUpdateMeta() {
72
+ try {
73
+ const meta = readMeta();
74
+ const fallback = readBundledManifestUpdatedAt();
75
+ this.setState({
76
+ lastUpdateAtText: formatSimpleTime((meta && meta.lastUpdateAt) || fallback),
77
+ });
78
+ } catch {}
79
+ }
80
+
81
+ async handleCheckUpdate(force) {
82
+ if (this.state.checkingUpdate) return;
83
+ this.setState({ checkingUpdate: true, updateTip: force ? "正在检查..." : "" });
84
+ try {
85
+ const result = await checkAndUpdateData({ force: !!force });
86
+ this.syncUpdateMeta();
87
+ if (force) {
88
+ if (result && result.ok && result.updated) this.setState({ updateTip: `已更新到数据版本 ${result.remoteVersion}` });
89
+ else if (result && result.ok && !result.updated) this.setState({ updateTip: "当前已是最新数据。" });
90
+ else if (result && result.reason === "data_source_not_configured") this.setState({ updateTip: "未配置远端数据源。" });
91
+ else this.setState({ updateTip: `检查失败${result && result.error ? `:${result.error}` : ""}` });
92
+ }
93
+ } catch (error) {
94
+ if (force) this.setState({ updateTip: `检查失败:${String(error && (error.stack || error))}` });
95
+ } finally {
96
+ this.setState({ checkingUpdate: false });
97
+ }
98
+ }
99
+
100
+ render() {
101
+ const tab = !this.enableDebug && this.state.tab === "debug" ? "daily" : this.state.tab;
102
+ const tabButtonStyle = (key) => ({
103
+ padding: "6px 10px",
104
+ borderRadius: 8,
105
+ cursor: "pointer",
106
+ border: "1px solid #374151",
107
+ background: tab === key ? "#1f2937" : "transparent",
108
+ color: "#e5e7eb",
109
+ marginRight: 8,
110
+ fontSize: 14,
111
+ });
112
+
113
+ let content = null;
114
+ if (tab === "daily") {
115
+ content = React.createElement(DailyTabV2, {
116
+ envWindow: this.props.envWindow,
117
+ envCtx: this.props.envCtx,
118
+ });
119
+ } else if (tab === "wishlist") {
120
+ if (!this._WishlistTab && !this.state.wishlistLoadError) {
121
+ try {
122
+ // eslint-disable-next-line global-require
123
+ this._WishlistTab = require("./tabs/wishlist/WishlistTabV2");
124
+ } catch (error) {
125
+ this.setState({ wishlistLoadError: String(error && error.stack ? error.stack : error) });
126
+ }
127
+ }
128
+
129
+ if (this.state.wishlistLoadError) {
130
+ content = React.createElement(
131
+ "div",
132
+ { style: { ...placeholderStyle, whiteSpace: "pre-wrap" } },
133
+ "【改修规划】模块加载失败(已隔离,不影响 Daily/Debug)\n\n错误信息:\n" + this.state.wishlistLoadError
134
+ );
135
+ } else if (this._WishlistTab) {
136
+ content = React.createElement(this._WishlistTab, {
137
+ envWindow: this.props.envWindow,
138
+ envCtx: this.props.envCtx,
139
+ });
140
+ } else {
141
+ content = React.createElement("div", { style: placeholderStyle }, "【改修规划】加载中...");
142
+ }
143
+ } else if (tab === "acquisition") {
144
+ if (!this._AcquisitionTab && !this.state.acquisitionLoadError) {
145
+ try {
146
+ // eslint-disable-next-line global-require
147
+ this._AcquisitionTab = require("./tabs/acquisition/AcquisitionTab");
148
+ } catch (error) {
149
+ this.setState({ acquisitionLoadError: String(error && error.stack ? error.stack : error) });
150
+ }
151
+ }
152
+
153
+ if (this.state.acquisitionLoadError) {
154
+ content = React.createElement(
155
+ "div",
156
+ { style: { ...placeholderStyle, whiteSpace: "pre-wrap" } },
157
+ "【装备获取规划】模块加载失败(已隔离,不影响其他页面)\n\n错误信息:\n" + this.state.acquisitionLoadError
158
+ );
159
+ } else if (this._AcquisitionTab) {
160
+ content = React.createElement(this._AcquisitionTab, {
161
+ envWindow: this.props.envWindow,
162
+ envCtx: this.props.envCtx,
163
+ });
164
+ } else {
165
+ content = React.createElement("div", { style: placeholderStyle }, "【装备获取规划】加载中...");
166
+ }
167
+ } else if (tab === "debug") {
168
+ if (!this._DebugTab && !this.state.debugLoadError) {
169
+ try {
170
+ // eslint-disable-next-line global-require
171
+ this._DebugTab = require("./tabs/debug/DebugTab");
172
+ } catch (error) {
173
+ this.setState({ debugLoadError: String(error && error.stack ? error.stack : error) });
174
+ }
175
+ }
176
+
177
+ if (this.state.debugLoadError) {
178
+ content = React.createElement(
179
+ "div",
180
+ { style: { ...placeholderStyle, whiteSpace: "pre-wrap" } },
181
+ "【Debug】模块加载失败(已隔离,不影响 Daily/Wishlist/装备获取规划)\n\n错误信息:\n" + this.state.debugLoadError
182
+ );
183
+ } else if (this._DebugTab) {
184
+ content = React.createElement(this._DebugTab, {
185
+ envWindow: this.props.envWindow,
186
+ envCtx: this.props.envCtx,
187
+ });
188
+ } else {
189
+ content = React.createElement("div", { style: placeholderStyle }, "【Debug】加载中...");
190
+ }
191
+ } else {
192
+ content = React.createElement("div", { style: placeholderStyle }, "未知标签页。");
193
+ }
194
+
195
+ return React.createElement(
196
+ "div",
197
+ { style: { padding: 12 } },
198
+ React.createElement(
199
+ "div",
200
+ { style: { marginBottom: 12, display: "flex", alignItems: "center", justifyContent: "space-between", gap: 12 } },
201
+ React.createElement(
202
+ "div",
203
+ null,
204
+ React.createElement("button", { style: tabButtonStyle("daily"), onClick: () => this.setState({ tab: "daily" }) }, "每日改修"),
205
+ React.createElement("button", { style: tabButtonStyle("wishlist"), onClick: () => this.setState({ tab: "wishlist" }) }, "改修规划"),
206
+ React.createElement("button", { style: tabButtonStyle("acquisition"), onClick: () => this.setState({ tab: "acquisition" }) }, "装备获取规划"),
207
+ this.enableDebug
208
+ ? React.createElement("button", { style: tabButtonStyle("debug"), onClick: () => this.setState({ tab: "debug" }) }, "Debug")
209
+ : null
210
+ ),
211
+ React.createElement(
212
+ "div",
213
+ { style: { display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap", justifyContent: "flex-end" } },
214
+ React.createElement("span", { style: { fontSize: 14, opacity: 0.8 } }, `上一次数据源更新:${this.state.lastUpdateAtText}`),
215
+ React.createElement(
216
+ "button",
217
+ { style: tabButtonStyle("__check__"), onClick: () => this.handleCheckUpdate(true), disabled: this.state.checkingUpdate },
218
+ this.state.checkingUpdate ? "检查中..." : "检查更新"
219
+ ),
220
+ this.state.updateTip ? React.createElement("span", { style: { fontSize: 14, opacity: 0.85 } }, this.state.updateTip) : null
221
+ )
222
+ ),
223
+ content
224
+ );
225
+ }
226
+ }
227
+
211
228
  module.exports = { KaiPlannerApp };
@@ -0,0 +1,135 @@
1
+ const React = require("react");
2
+ const { MouseComboBox } = require("../wishlist/components/MouseComboBox");
3
+ const { EVENT_COMBO_CLOSE_ALL, isInComboRoot } = require("../../../services/wishlist/dropdownInteraction");
4
+ const { countNoteUnits } = require("../../../services/common/noteText");
5
+
6
+ const NOTE_MAX_UNITS = 100;
7
+
8
+ function AcquisitionPlanForm({
9
+ visible,
10
+ mode,
11
+ form,
12
+ error,
13
+ equipOptions,
14
+ equipInputValue,
15
+ selectedEquipText,
16
+ onPatch,
17
+ onSelectEquip,
18
+ onClearEquip,
19
+ onSubmit,
20
+ onCancel,
21
+ }) {
22
+ if (!visible) return null;
23
+
24
+ const panelStyle = {
25
+ marginTop: 10,
26
+ padding: 10,
27
+ border: "1px solid rgba(255,255,255,0.12)",
28
+ borderRadius: 8,
29
+ background: "rgba(0,0,0,0.12)",
30
+ };
31
+ const formRow = { display: "flex", gap: 8, alignItems: "center", flexWrap: "wrap", marginTop: 8 };
32
+ const labelStyle = { width: 96, opacity: 0.88, fontWeight: 700 };
33
+ const inputStyle = {
34
+ padding: "6px 8px",
35
+ borderRadius: 8,
36
+ border: "1px solid rgba(255,255,255,0.12)",
37
+ background: "rgba(0,0,0,0.15)",
38
+ color: "#e5e7eb",
39
+ minWidth: 180,
40
+ };
41
+ const noteCount = countNoteUnits(form.note || "");
42
+
43
+ return React.createElement(
44
+ "div",
45
+ {
46
+ style: panelStyle,
47
+ onMouseDownCapture: (event) => {
48
+ if (isInComboRoot(event && event.target)) return;
49
+ if (typeof document !== "undefined" && typeof CustomEvent === "function" && document.dispatchEvent) {
50
+ document.dispatchEvent(new CustomEvent(EVENT_COMBO_CLOSE_ALL));
51
+ }
52
+ },
53
+ },
54
+ React.createElement("div", { style: { fontWeight: 800 } }, mode === "edit" ? "编辑装备获取规划" : "新增装备获取规划"),
55
+ React.createElement(
56
+ "div",
57
+ { style: formRow },
58
+ React.createElement("div", { style: labelStyle }, "装备:"),
59
+ React.createElement(MouseComboBox, {
60
+ inputStyle: { ...inputStyle, minWidth: 590 },
61
+ dropdownWidth: 590,
62
+ disabled: mode === "edit",
63
+ inputValue: equipInputValue,
64
+ placeholder: mode === "edit" ? "编辑模式下不可修改装备" : "搜索并选择装备(名称/equipId)",
65
+ onInputChange: (value) => onPatch({ equipQuery: value }),
66
+ options: equipOptions,
67
+ emptyText: "无匹配装备",
68
+ onSelect: (value, option) => onSelectEquip(value, option),
69
+ }),
70
+ selectedEquipText && mode !== "edit"
71
+ ? React.createElement(
72
+ "button",
73
+ {
74
+ type: "button",
75
+ onClick: () => onClearEquip && onClearEquip(),
76
+ style: {
77
+ border: "1px solid rgba(147,197,253,0.45)",
78
+ background: "rgba(59,130,246,0.14)",
79
+ color: "#93c5fd",
80
+ borderRadius: 8,
81
+ padding: "4px 8px",
82
+ cursor: "pointer",
83
+ },
84
+ },
85
+ `已选:${selectedEquipText} ×`
86
+ )
87
+ : null
88
+ ),
89
+ React.createElement(
90
+ "div",
91
+ { style: formRow },
92
+ React.createElement("div", { style: labelStyle }, "预期获取数量:"),
93
+ React.createElement("input", {
94
+ style: { ...inputStyle, minWidth: 120 },
95
+ type: "number",
96
+ min: 1,
97
+ step: 1,
98
+ value: String(form.expectedCount || "1"),
99
+ onChange: (event) => onPatch({ expectedCount: event.target.value }),
100
+ placeholder: "必须大于 0",
101
+ })
102
+ ),
103
+ React.createElement(
104
+ "div",
105
+ { style: { ...formRow, alignItems: "flex-start" } },
106
+ React.createElement("div", { style: { ...labelStyle, marginTop: 6 } }, "备注:"),
107
+ React.createElement(
108
+ "div",
109
+ { style: { minWidth: 590, flex: 1, maxWidth: 720 } },
110
+ React.createElement("textarea", {
111
+ style: { ...inputStyle, minWidth: 590, minHeight: 88, resize: "vertical", width: "100%", boxSizing: "border-box" },
112
+ rows: 4,
113
+ value: form.note || "",
114
+ onChange: (event) => onPatch({ note: event.target.value }),
115
+ placeholder: "可填写 常用开发公式+秘书舰 或 可通过什么路径获取(任务/改修进化)",
116
+ }),
117
+ React.createElement(
118
+ "div",
119
+ { style: { marginTop: 4, opacity: 0.7, fontSize: 12, textAlign: "right" } },
120
+ `当前字数 ${noteCount}/${NOTE_MAX_UNITS}`
121
+ )
122
+ )
123
+ ),
124
+ React.createElement(
125
+ "div",
126
+ { style: { ...formRow, marginTop: 10 } },
127
+ React.createElement("button", { onClick: onSubmit }, mode === "edit" ? "保存" : "确认新增"),
128
+ React.createElement("button", { onClick: onCancel }, "取消")
129
+ ),
130
+ error ? React.createElement("div", { style: { marginTop: 8, color: "#fca5a5" } }, error) : null
131
+ );
132
+ }
133
+
134
+ module.exports = { AcquisitionPlanForm };
135
+