poi-plugin-kai-planner 1.0.0

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 (54) hide show
  1. package/README.md +48 -0
  2. package/index.js +35 -0
  3. package/package.json +25 -0
  4. package/src/app/KaiPlannerApp.js +211 -0
  5. package/src/app/tabs/daily/DailyTab.js +439 -0
  6. package/src/app/tabs/debug/DebugTab.js +554 -0
  7. package/src/app/tabs/wishlist/CreatePlanForm.js +185 -0
  8. package/src/app/tabs/wishlist/WishlistTab.js +704 -0
  9. package/src/app/tabs/wishlist/components/MouseComboBox.js +185 -0
  10. package/src/app/tabs/wishlist/components/WishlistExpandedDetail.js +170 -0
  11. package/src/app/tabs/wishlist/components/WishlistTable.js +253 -0
  12. package/src/core/poi/secretaryName.js +64 -0
  13. package/src/data/indexes/buildArrangementIndex.js +103 -0
  14. package/src/data/loaders/loadStaticData.js +182 -0
  15. package/src/data/static/data_manifest.json +15 -0
  16. package/src/data/static/equip_base_cost.json +5000 -0
  17. package/src/data/static/equipment_upgrade_path.json +3555 -0
  18. package/src/data/static/improvement_arrangement.json +15677 -0
  19. package/src/data/static/improvement_consume_item.json +8933 -0
  20. package/src/data/static/improvement_consume_step.json +9284 -0
  21. package/src/data/static/improvement_upgrade_cost.json +4766 -0
  22. package/src/data/static/improvement_upgrade_target.json +2641 -0
  23. package/src/data/static/material.json +90 -0
  24. package/src/services/common/secretaryDisplay.js +42 -0
  25. package/src/services/daily/buildDailyViewModel.js +402 -0
  26. package/src/services/planner/buildUpgradePath.js +79 -0
  27. package/src/services/planner/calcImproveSteps.js +104 -0
  28. package/src/services/planner/calcRemainingPlan.js +169 -0
  29. package/src/services/planner/calcRoutePlan.js +85 -0
  30. package/src/services/planner/calcUpgradeStep.js +85 -0
  31. package/src/services/planner/detectCurrentPosition.js +57 -0
  32. package/src/services/planner/summarizeShortage.js +76 -0
  33. package/src/services/player/countPlayerEquipByMasterId.js +27 -0
  34. package/src/services/player/getEquipOwnerShip.js +66 -0
  35. package/src/services/player/getPlayerData.js +14 -0
  36. package/src/services/player/getPlayerItemCountByUseitemId.js +45 -0
  37. package/src/services/player/getReduxStateFromEnvWindow.js +12 -0
  38. package/src/services/player/resolveMaterialKeyToUseitemId.js +54 -0
  39. package/src/services/static/indexes/buildConsumeIndexes.js +28 -0
  40. package/src/services/static/indexes/buildPathIndexes.js +29 -0
  41. package/src/services/static/indexes/buildUpgradeIndexes.js +57 -0
  42. package/src/services/static/version/dataSourceConfig.js +25 -0
  43. package/src/services/static/version/dataUpdateManager.js +176 -0
  44. package/src/services/static/version/versionStore.js +205 -0
  45. package/src/services/utils/toInt.js +11 -0
  46. package/src/services/utils/tokyoTime.js +58 -0
  47. package/src/services/wishlist/buildWishlistViewModel.js +56 -0
  48. package/src/services/wishlist/dropdownInteraction.js +30 -0
  49. package/src/services/wishlist/wishlistActions.js +485 -0
  50. package/src/storage/userPlans/fileStore.js +106 -0
  51. package/src/storage/userPlans/localStorageStore.js +50 -0
  52. package/src/storage/userPlans/migrate.js +60 -0
  53. package/src/storage/userPlans/planStore.js +107 -0
  54. package/src/storage/userPlans/storeAdapter.js +77 -0
@@ -0,0 +1,185 @@
1
+ /* src/app/tabs/wishlist/components/MouseComboBox.js */
2
+
3
+ const React = require("react");
4
+ const {
5
+ COMBO_ROOT_ATTR,
6
+ EVENT_COMBO_OPENED,
7
+ EVENT_COMBO_CLOSE_ALL,
8
+ shouldCloseOnDocMouseDown,
9
+ shouldCloseOnComboOpened,
10
+ } = require("../../../../services/wishlist/dropdownInteraction");
11
+ let comboSeq = 1;
12
+
13
+ class MouseComboBox extends React.Component {
14
+ constructor(props) {
15
+ super(props);
16
+ this.state = { open: false };
17
+ this._rootRef = null;
18
+ this._id = `combo_${comboSeq++}`;
19
+ this.onDocMouseDown = this.onDocMouseDown.bind(this);
20
+ this.onComboOpened = this.onComboOpened.bind(this);
21
+ this.onCloseAll = this.onCloseAll.bind(this);
22
+ this.openDropdown = this.openDropdown.bind(this);
23
+ this.closeDropdown = this.closeDropdown.bind(this);
24
+ }
25
+
26
+ componentDidMount() {
27
+ if (typeof document !== "undefined" && document.addEventListener) {
28
+ document.addEventListener("mousedown", this.onDocMouseDown);
29
+ document.addEventListener(EVENT_COMBO_OPENED, this.onComboOpened);
30
+ document.addEventListener(EVENT_COMBO_CLOSE_ALL, this.onCloseAll);
31
+ }
32
+ }
33
+
34
+ componentWillUnmount() {
35
+ if (typeof document !== "undefined" && document.removeEventListener) {
36
+ document.removeEventListener("mousedown", this.onDocMouseDown);
37
+ document.removeEventListener(EVENT_COMBO_OPENED, this.onComboOpened);
38
+ document.removeEventListener(EVENT_COMBO_CLOSE_ALL, this.onCloseAll);
39
+ }
40
+ }
41
+
42
+ onDocMouseDown(e) {
43
+ if (shouldCloseOnDocMouseDown({ isOpen: this.state.open, rootRef: this._rootRef, target: e && e.target })) {
44
+ this.closeDropdown();
45
+ }
46
+ }
47
+
48
+ onComboOpened(e) {
49
+ if (
50
+ shouldCloseOnComboOpened({
51
+ isOpen: this.state.open,
52
+ selfId: this._id,
53
+ openedId: e && e.detail ? e.detail.id : "",
54
+ })
55
+ ) {
56
+ this.closeDropdown();
57
+ }
58
+ }
59
+
60
+ onCloseAll() {
61
+ this.closeDropdown();
62
+ }
63
+
64
+ openDropdown() {
65
+ if (this.state.open) return;
66
+ this.setState({ open: true });
67
+ if (typeof document !== "undefined" && typeof CustomEvent === "function" && document.dispatchEvent) {
68
+ document.dispatchEvent(new CustomEvent(EVENT_COMBO_OPENED, { detail: { id: this._id } }));
69
+ }
70
+ }
71
+
72
+ closeDropdown() {
73
+ if (!this.state.open) return;
74
+ this.setState({ open: false });
75
+ }
76
+
77
+ render() {
78
+ const {
79
+ disabled,
80
+ inputValue,
81
+ placeholder,
82
+ onInputChange,
83
+ onSelect,
84
+ options,
85
+ emptyText,
86
+ footerTip,
87
+ inputStyle,
88
+ dropdownWidth,
89
+ maxHeight,
90
+ } = this.props;
91
+
92
+ const wrapStyle = { position: "relative", display: "inline-block", minWidth: dropdownWidth || 300 };
93
+ const listStyle = {
94
+ position: "absolute",
95
+ top: "100%",
96
+ left: 0,
97
+ right: 0,
98
+ marginTop: 4,
99
+ maxHeight: maxHeight || 320,
100
+ overflowY: "auto",
101
+ border: "1px solid rgba(255,255,255,0.14)",
102
+ borderRadius: 8,
103
+ background: "rgba(12,18,28,0.98)",
104
+ zIndex: 30,
105
+ boxShadow: "0 10px 20px rgba(0,0,0,0.35)",
106
+ };
107
+
108
+ return React.createElement(
109
+ "div",
110
+ {
111
+ style: wrapStyle,
112
+ [COMBO_ROOT_ATTR]: "1",
113
+ ref: (el) => {
114
+ this._rootRef = el;
115
+ },
116
+ },
117
+ React.createElement("input", {
118
+ style: inputStyle,
119
+ disabled: !!disabled,
120
+ value: inputValue || "",
121
+ placeholder,
122
+ onFocus: this.openDropdown,
123
+ onClick: this.openDropdown,
124
+ onChange: (e) => {
125
+ if (onInputChange) onInputChange(e.target.value);
126
+ if (!this.state.open) this.openDropdown();
127
+ },
128
+ }),
129
+ this.state.open && !disabled
130
+ ? React.createElement(
131
+ "div",
132
+ { style: listStyle },
133
+ (options || []).length
134
+ ? (options || []).map((o) =>
135
+ React.createElement(
136
+ "div",
137
+ {
138
+ key: String(o.value),
139
+ onMouseDown: (e) => {
140
+ e.preventDefault();
141
+ e.stopPropagation();
142
+ if (o.disabled) return;
143
+ if (onSelect) onSelect(o.value, o);
144
+ this.closeDropdown();
145
+ },
146
+ style: {
147
+ padding: "7px 10px",
148
+ borderBottom: "1px solid rgba(255,255,255,0.06)",
149
+ opacity: o.disabled ? 0.55 : 1,
150
+ cursor: o.disabled ? "not-allowed" : "pointer",
151
+ whiteSpace: "nowrap",
152
+ overflow: "hidden",
153
+ textOverflow: "ellipsis",
154
+ },
155
+ title: o.label,
156
+ },
157
+ o.label
158
+ )
159
+ )
160
+ : React.createElement(
161
+ "div",
162
+ { style: { padding: "8px 10px", opacity: 0.72 } },
163
+ emptyText || "无匹配项"
164
+ ),
165
+ footerTip
166
+ ? React.createElement(
167
+ "div",
168
+ {
169
+ style: {
170
+ padding: "8px 10px",
171
+ opacity: 0.7,
172
+ borderTop: "1px solid rgba(255,255,255,0.08)",
173
+ fontSize: 11,
174
+ },
175
+ },
176
+ footerTip
177
+ )
178
+ : null
179
+ )
180
+ : null
181
+ );
182
+ }
183
+ }
184
+
185
+ module.exports = { MouseComboBox };
@@ -0,0 +1,170 @@
1
+ /* src/app/tabs/wishlist/components/WishlistExpandedDetail.js */
2
+
3
+ const React = require("react");
4
+
5
+ function WishlistExpandedDetail({ plan, planWrap, masterEquipsById, getEquipName, toInt, stockStatusMark }) {
6
+ const result = planWrap.result;
7
+ const shortage = result && result.shortage ? result.shortage : null;
8
+ const position = planWrap.position;
9
+ const completedCount = planWrap.completedCount || 0;
10
+ const fullSteps = Array.isArray(planWrap.fullSteps)
11
+ ? planWrap.fullSteps
12
+ : Array.isArray(plan.snapshotSteps)
13
+ ? plan.snapshotSteps
14
+ : [];
15
+ const completedSteps = Array.isArray(planWrap.completedSteps) ? planWrap.completedSteps : fullSteps.slice(0, completedCount);
16
+ const remainingSteps = Array.isArray(planWrap.remainingSteps) ? planWrap.remainingSteps : fullSteps.slice(completedCount);
17
+ const tableStyle = { width: "100%", borderCollapse: "collapse", marginTop: 8 };
18
+ const th = { textAlign: "left", padding: "8px", borderBottom: "1px solid rgba(255,255,255,0.12)", opacity: 0.9 };
19
+ const td = { padding: "8px", borderBottom: "1px solid rgba(255,255,255,0.08)", verticalAlign: "top" };
20
+ const realtimeStartText =
21
+ position && position.isValid
22
+ ? `${getEquipName(masterEquipsById, position.currentEquipId)} +${position.currentLevel}${
23
+ planWrap.ownerShipName ? `(${planWrap.ownerShipName})` : ""
24
+ }`
25
+ : "失效";
26
+
27
+ const renderNeedOwnedList = (rows) => {
28
+ if (!rows || !rows.length) return React.createElement("div", null, "无");
29
+ return React.createElement(
30
+ "div",
31
+ null,
32
+ rows.map((r, i) =>
33
+ React.createElement(
34
+ "div",
35
+ { key: `n_${i}`, style: { marginTop: i === 0 ? 0 : 4 } },
36
+ `${r.name}(${r.need}/${r.owned})`,
37
+ stockStatusMark(r.need, r.owned)
38
+ )
39
+ )
40
+ );
41
+ };
42
+
43
+ const renderItemCost = (step, kind) => {
44
+ const items = ((step && step.calcCost) ? step.calcCost.items : []) || [];
45
+ const picked = items.filter((x) => x && x.kind === kind);
46
+ if (!picked.length) return "0";
47
+ return picked.map((x) => `${x.name}:${x.count}`).join(";");
48
+ };
49
+
50
+ const renderStepText = (s) =>
51
+ s.kind === "improve"
52
+ ? `${getEquipName(masterEquipsById, s.equipId)} +${s.fromStar} -> +${s.toStar}`
53
+ : `${getEquipName(masterEquipsById, s.equipId)} MAX -> ${getEquipName(masterEquipsById, s.nextEquipId)} +0`;
54
+
55
+ const renderStepsTable = ({ title, steps, statusText, startIndex }) =>
56
+ React.createElement(
57
+ React.Fragment,
58
+ null,
59
+ React.createElement("div", { style: { marginTop: 10, fontWeight: 700 } }, title),
60
+ React.createElement(
61
+ "table",
62
+ { style: tableStyle },
63
+ React.createElement(
64
+ "thead",
65
+ null,
66
+ React.createElement(
67
+ "tr",
68
+ null,
69
+ React.createElement("th", { style: th }, "任务步骤"),
70
+ React.createElement("th", { style: th }, "资材消耗"),
71
+ React.createElement("th", { style: th }, "螺丝消耗"),
72
+ React.createElement("th", { style: th }, "装备消耗"),
73
+ React.createElement("th", { style: th }, "道具消耗")
74
+ )
75
+ ),
76
+ React.createElement(
77
+ "tbody",
78
+ null,
79
+ ...(steps.length
80
+ ? steps.map((s, i) =>
81
+ React.createElement(
82
+ "tr",
83
+ { key: `${title}_${i}` },
84
+ React.createElement("td", { style: td }, `${statusText} ${startIndex + i + 1}. ${renderStepText(s)}`),
85
+ React.createElement("td", { style: td }, toInt((s.calcCost || {}).dev, 0)),
86
+ React.createElement("td", { style: td }, toInt((s.calcCost || {}).screw, 0)),
87
+ React.createElement("td", { style: td }, renderItemCost(s, "equipment")),
88
+ React.createElement("td", { style: td }, renderItemCost(s, "material"))
89
+ )
90
+ )
91
+ : [
92
+ React.createElement(
93
+ "tr",
94
+ { key: `${title}_none` },
95
+ React.createElement("td", { style: td, colSpan: 5 }, "无")
96
+ ),
97
+ ])
98
+ )
99
+ )
100
+ );
101
+
102
+ return React.createElement(
103
+ "div",
104
+ null,
105
+ React.createElement(
106
+ "table",
107
+ { style: tableStyle },
108
+ React.createElement(
109
+ "thead",
110
+ null,
111
+ React.createElement(
112
+ "tr",
113
+ null,
114
+ React.createElement("th", { style: th }, "起点装备(实时状态)"),
115
+ React.createElement("th", { style: th }, "待消耗资材"),
116
+ React.createElement("th", { style: th }, "待消耗螺丝")
117
+ )
118
+ ),
119
+ React.createElement(
120
+ "tbody",
121
+ null,
122
+ React.createElement(
123
+ "tr",
124
+ null,
125
+ React.createElement("td", { style: td }, realtimeStartText),
126
+ React.createElement("td", { style: td }, shortage ? shortage.dev.need : "-"),
127
+ React.createElement("td", { style: td }, shortage ? shortage.screw.need : "-")
128
+ )
129
+ )
130
+ ),
131
+ React.createElement(
132
+ "table",
133
+ { style: tableStyle },
134
+ React.createElement(
135
+ "thead",
136
+ null,
137
+ React.createElement(
138
+ "tr",
139
+ null,
140
+ React.createElement("th", { style: th }, "待消耗装备(待消耗/当前持有)"),
141
+ React.createElement("th", { style: th }, "待消耗道具(待消耗/当前持有)")
142
+ )
143
+ ),
144
+ React.createElement(
145
+ "tbody",
146
+ null,
147
+ React.createElement(
148
+ "tr",
149
+ null,
150
+ React.createElement("td", { style: td }, renderNeedOwnedList(shortage && shortage.equipments)),
151
+ React.createElement("td", { style: td }, renderNeedOwnedList(shortage && shortage.materials))
152
+ )
153
+ )
154
+ ),
155
+ renderStepsTable({ title: "已完成步骤(实时)", steps: completedSteps, statusText: "[已完成]", startIndex: 0 }),
156
+ renderStepsTable({
157
+ title: "未完成步骤(实时)",
158
+ steps: remainingSteps,
159
+ statusText: "[未完成]",
160
+ startIndex: completedSteps.length,
161
+ }),
162
+ React.createElement(
163
+ "div",
164
+ { style: { marginTop: 6, opacity: 0.75 } },
165
+ "说明:6→7~MAX→upgrade 的资材和螺丝消耗,按照改修确保值展示。"
166
+ )
167
+ );
168
+ }
169
+
170
+ module.exports = { WishlistExpandedDetail };
@@ -0,0 +1,253 @@
1
+ /* src/app/tabs/wishlist/components/WishlistTable.js */
2
+
3
+ const React = require("react");
4
+
5
+ function WishlistTable({
6
+ rows,
7
+ expandedById,
8
+ rebindInputById,
9
+ editById,
10
+ priorities,
11
+ styles,
12
+ getPlanStartSnapshotText,
13
+ isTargetImprovable,
14
+ onToggleExpand,
15
+ onRefreshPlan,
16
+ onDeletePlan,
17
+ onRebindInputChange,
18
+ onRebindConfirm,
19
+ onStartEdit,
20
+ onCancelEdit,
21
+ onPatchEdit,
22
+ onSaveEdit,
23
+ renderExpandedRow,
24
+ }) {
25
+ const { row, input, th, td } = styles;
26
+
27
+ return React.createElement(
28
+ "table",
29
+ { style: { width: "100%", borderCollapse: "collapse" } },
30
+ React.createElement(
31
+ "thead",
32
+ null,
33
+ React.createElement(
34
+ "tr",
35
+ null,
36
+ React.createElement("th", { style: th }, "目标装备"),
37
+ React.createElement("th", { style: th }, "起点装备(快照)"),
38
+ React.createElement("th", { style: th }, "优先级"),
39
+ React.createElement("th", { style: th }, "目标星数"),
40
+ React.createElement("th", { style: th }, "备注"),
41
+ React.createElement("th", { style: th }, "已完成步数"),
42
+ React.createElement("th", { style: th }, "今日可改修"),
43
+ React.createElement("th", { style: th }, "操作")
44
+ )
45
+ ),
46
+ React.createElement(
47
+ "tbody",
48
+ null,
49
+ (rows || []).flatMap((rowVm) => {
50
+ const p = rowVm.plan;
51
+ const wrap = rowVm.wrap || {};
52
+ const completedCount = rowVm.completedCount || 0;
53
+ const totalSteps = rowVm.totalSteps || 0;
54
+ const today = rowVm.today || "不可";
55
+ const isExp = !!(expandedById || {})[p.id];
56
+ const targetName = rowVm.targetName || "";
57
+ const expandIcon = isExp ? "▾" : "▸";
58
+ const edit = (editById || {})[p.id] || null;
59
+ const isEditing = !!(edit && edit.isEditing);
60
+ const canEditTargetLevel = isTargetImprovable ? !!isTargetImprovable(p.targetEquipId) : true;
61
+
62
+ const main = React.createElement(
63
+ "tr",
64
+ {
65
+ key: `row_${p.id}`,
66
+ onClick: () => onToggleExpand(p.id, isExp),
67
+ style: { cursor: "pointer" },
68
+ title: "点击展开/收起",
69
+ },
70
+ React.createElement(
71
+ "td",
72
+ { style: td },
73
+ React.createElement(
74
+ "div",
75
+ { style: { display: "flex", gap: 8, alignItems: "center" } },
76
+ React.createElement(
77
+ "button",
78
+ {
79
+ onClick: (e) => {
80
+ e.preventDefault();
81
+ e.stopPropagation();
82
+ onToggleExpand(p.id, isExp);
83
+ },
84
+ style: {
85
+ width: 28,
86
+ height: 28,
87
+ borderRadius: 8,
88
+ border: "1px solid rgba(255,255,255,0.12)",
89
+ background: "rgba(255,255,255,0.06)",
90
+ color: "#e5e7eb",
91
+ cursor: "pointer",
92
+ },
93
+ title: "展开/收起",
94
+ },
95
+ expandIcon
96
+ ),
97
+ React.createElement("span", null, `${targetName} (equipId:${p.targetEquipId})`)
98
+ )
99
+ ),
100
+ React.createElement("td", { style: td }, getPlanStartSnapshotText(p)),
101
+ React.createElement(
102
+ "td",
103
+ { style: td },
104
+ isEditing
105
+ ? React.createElement(
106
+ "select",
107
+ {
108
+ value: String(edit.priority || "P0"),
109
+ onClick: (e) => {
110
+ e.preventDefault();
111
+ e.stopPropagation();
112
+ },
113
+ onChange: (e) => onPatchEdit(p.id, { priority: e.target.value }),
114
+ style: { ...input, width: 80 },
115
+ },
116
+ (priorities || []).map((x) => React.createElement("option", { key: x, value: x }, x))
117
+ )
118
+ : p.priority || "P0"
119
+ ),
120
+ React.createElement(
121
+ "td",
122
+ { style: td },
123
+ isEditing
124
+ ? React.createElement("input", {
125
+ type: "number",
126
+ min: 0,
127
+ max: 10,
128
+ disabled: !canEditTargetLevel || !!edit.saving,
129
+ value: String(edit.targetLevel == null ? "0" : edit.targetLevel),
130
+ onClick: (e) => {
131
+ e.preventDefault();
132
+ e.stopPropagation();
133
+ },
134
+ onChange: (e) => onPatchEdit(p.id, { targetLevel: e.target.value }),
135
+ style: { ...input, width: 88, opacity: canEditTargetLevel ? 1 : 0.6 },
136
+ title: canEditTargetLevel ? "输入目标星数(0-10)" : "该目标装备不可改修,目标星数固定为 0",
137
+ })
138
+ : p.targetLevel
139
+ ),
140
+ React.createElement("td", { style: td }, p.note || ""),
141
+ React.createElement("td", { style: td }, `${completedCount}/${totalSteps}`),
142
+ React.createElement("td", { style: td }, today),
143
+ React.createElement(
144
+ "td",
145
+ { style: td },
146
+ React.createElement(
147
+ "div",
148
+ { style: row },
149
+ React.createElement(
150
+ "button",
151
+ {
152
+ onClick: (e) => {
153
+ e.preventDefault();
154
+ e.stopPropagation();
155
+ onRefreshPlan(p.id);
156
+ },
157
+ disabled: isEditing,
158
+ },
159
+ "刷新"
160
+ ),
161
+ isEditing
162
+ ? React.createElement(
163
+ "button",
164
+ {
165
+ onClick: (e) => {
166
+ e.preventDefault();
167
+ e.stopPropagation();
168
+ onSaveEdit(p.id);
169
+ },
170
+ disabled: !!edit.saving,
171
+ },
172
+ edit.saving ? "保存中..." : "保存"
173
+ )
174
+ : React.createElement(
175
+ "button",
176
+ {
177
+ onClick: (e) => {
178
+ e.preventDefault();
179
+ e.stopPropagation();
180
+ onStartEdit(p);
181
+ },
182
+ },
183
+ "编辑"
184
+ ),
185
+ isEditing
186
+ ? React.createElement(
187
+ "button",
188
+ {
189
+ onClick: (e) => {
190
+ e.preventDefault();
191
+ e.stopPropagation();
192
+ onCancelEdit(p.id);
193
+ },
194
+ disabled: !!edit.saving,
195
+ },
196
+ "取消"
197
+ )
198
+ : null,
199
+ React.createElement(
200
+ "button",
201
+ {
202
+ onClick: (e) => {
203
+ e.preventDefault();
204
+ e.stopPropagation();
205
+ onDeletePlan(p.id);
206
+ },
207
+ disabled: isEditing,
208
+ },
209
+ "删除"
210
+ )
211
+ )
212
+ )
213
+ );
214
+
215
+ const expanded = isExp
216
+ ? React.createElement(
217
+ "tr",
218
+ { key: `exp_${p.id}` },
219
+ React.createElement(
220
+ "td",
221
+ { style: { ...td, background: "rgba(0,0,0,0.12)" }, colSpan: 8 },
222
+ renderExpandedRow(p, wrap),
223
+ !wrap.position || !wrap.position.isValid
224
+ ? React.createElement(
225
+ "div",
226
+ { style: { ...row, marginTop: 8 } },
227
+ React.createElement("input", {
228
+ style: { ...input, width: 200 },
229
+ placeholder: "重新绑定起点 api_id",
230
+ value: (rebindInputById || {})[p.id] || "",
231
+ onChange: (e) => onRebindInputChange(p.id, e.target.value),
232
+ }),
233
+ React.createElement(
234
+ "button",
235
+ {
236
+ onClick: () => onRebindConfirm(p.id, (rebindInputById || {})[p.id]),
237
+ },
238
+ "重新绑定"
239
+ ),
240
+ null
241
+ )
242
+ : null
243
+ )
244
+ )
245
+ : null;
246
+
247
+ return expanded ? [main, expanded] : [main];
248
+ })
249
+ )
250
+ );
251
+ }
252
+
253
+ module.exports = { WishlistTable };
@@ -0,0 +1,64 @@
1
+ /* src/core/poi/secretaryName.js */
2
+ /* Resolve secretary display name:
3
+ - secretary_id is ship sortno
4
+ - if secretary_variant === "b": go one step via aftershipid (api_id)
5
+ */
6
+
7
+ function toInt(x) {
8
+ if (x === null || x === undefined) return null;
9
+ const n = Number(x);
10
+ return Number.isFinite(n) ? n : null;
11
+ }
12
+
13
+ /**
14
+ * @param {Object} params
15
+ * @param {number} params.secretarySortno - master ships api_sortno
16
+ * @param {string|null} params.secretaryVariant - only "b" or null
17
+ * @param {Object} params.shipBySortno - map sortno -> ship
18
+ * @param {Object} params.shipByApiId - map api_id -> ship
19
+ * @returns {{ name: string, baseName: string, variantName: string|null, usedVariant: boolean, debug?: any }}
20
+ */
21
+ function resolveSecretaryName({ secretarySortno, secretaryVariant, shipBySortno, shipByApiId }) {
22
+ const baseShip = shipBySortno ? shipBySortno[String(secretarySortno)] : null;
23
+ const baseName = baseShip && baseShip.api_name ? String(baseShip.api_name) : `UnknownShip(sortno=${secretarySortno})`;
24
+
25
+ // only "b" supported; walk aftershipid exactly once
26
+ if (secretaryVariant === "b") {
27
+ const afterApiId = baseShip ? toInt(baseShip.api_aftershipid) : null;
28
+ if (afterApiId != null && shipByApiId && shipByApiId[String(afterApiId)]) {
29
+ const afterShip = shipByApiId[String(afterApiId)];
30
+ const variantName = afterShip && afterShip.api_name ? String(afterShip.api_name) : null;
31
+ if (variantName) {
32
+ return {
33
+ name: variantName,
34
+ baseName,
35
+ variantName,
36
+ usedVariant: true,
37
+ };
38
+ }
39
+ }
40
+ // fallback: cannot find after form, use baseName
41
+ return {
42
+ name: baseName,
43
+ baseName,
44
+ variantName: null,
45
+ usedVariant: false,
46
+ debug: {
47
+ reason: "aftership_not_found",
48
+ afterApiId: baseShip ? baseShip.api_aftershipid : null,
49
+ },
50
+ };
51
+ }
52
+
53
+ // default base name
54
+ return {
55
+ name: baseName,
56
+ baseName,
57
+ variantName: null,
58
+ usedVariant: false,
59
+ };
60
+ }
61
+
62
+ module.exports = {
63
+ resolveSecretaryName,
64
+ };