poi-plugin-koushu-rate 2.0.17 → 2.0.19

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.
@@ -0,0 +1,175 @@
1
+ {
2
+ "source": "用户装备拆解指南 2026-08-25(合并稀有/次级稀有)",
3
+ "rare": [
4
+ "10.5cm連装砲",
5
+ "10cm連装高角砲(砲架)",
6
+ "12.7cm単装高角砲(後期型)",
7
+ "12.7cm連装高角砲(後期型)",
8
+ "12.7cm連装砲C型改二",
9
+ "12.7cm連装砲D型改二",
10
+ "120mm/50 連装砲",
11
+ "12cm30連装噴進砲",
12
+ "12cm単装高角砲",
13
+ "12cm単装高角砲E型",
14
+ "13.8cm連装砲",
15
+ "130mm B-13連装砲",
16
+ "13号対空電探",
17
+ "13号対空電探改",
18
+ "13号対空電探改(後期型)",
19
+ "14cm連装砲改",
20
+ "15.2cm連装砲改",
21
+ "15.2cm三連装砲",
22
+ "15cm連装副砲",
23
+ "16inch Mk.I連装砲",
24
+ "16inch Mk.I三連装砲",
25
+ "16inch Mk.V連装砲",
26
+ "16inch三連装砲 Mk.6",
27
+ "16inch三連装砲 Mk.6 mod.2",
28
+ "16inch三連装砲 Mk.7",
29
+ "20.3cm(3号)連装砲",
30
+ "203mm/53 連装砲",
31
+ "20連装7inch UP Rocket Launchers",
32
+ "22号対水上電探改四",
33
+ "25mm三連装機銃",
34
+ "25mm三連装機銃 集中配備",
35
+ "2cm 四連装FlaK 38",
36
+ "3.7cm FlaK M42",
37
+ "30.5cm三連装砲改",
38
+ "32号対水上電探",
39
+ "33号対水上電探",
40
+ "35.6cm連装砲(ダズル迷彩)",
41
+ "35.6cm連装砲改",
42
+ "38.1cm Mk.I連装砲",
43
+ "381mm/50 三連装砲",
44
+ "381mm/50 三連装砲改",
45
+ "38cm連装砲",
46
+ "38cm連装砲改",
47
+ "38cm四連装砲",
48
+ "38cm四連装砲改",
49
+ "42号対空電探",
50
+ "46cm三連装砲",
51
+ "46cm三連装砲改",
52
+ "533mm 三連装魚雷",
53
+ "533mm五連装魚雷(初期型)",
54
+ "53cm連装魚雷",
55
+ "5inch 単装高角砲群",
56
+ "5inch単装砲 Mk.30",
57
+ "5inch連装砲 Mk.28 mod.2",
58
+ "61cm三連装(酸素)魚雷",
59
+ "61cm五連装(酸素)魚雷",
60
+ "6inch Mk.XXIII三連装砲",
61
+ "6inch三連装速射砲 Mk.16 mod.2",
62
+ "8cm高角砲",
63
+ "8inch三連装砲 Mk.9",
64
+ "90mm単装高角砲",
65
+ "94式高射装置",
66
+ "彩雲",
67
+ "大発動艇(八九式中戦車&陸戦隊)",
68
+ "二式12cm迫撃砲改",
69
+ "二式爆雷",
70
+ "二式大艇",
71
+ "二式複戦 屠龍",
72
+ "二式複戦 屠龍 丙型",
73
+ "二式艦上偵察機",
74
+ "発煙装置(煙幕)",
75
+ "彗星(六〇一空)",
76
+ "艦艇修理施設",
77
+ "艦載型 四式20cm対地噴進砲",
78
+ "九八式水上偵察機(夜偵)",
79
+ "九九式艦爆(江草隊)",
80
+ "九九式艦爆(熟練)",
81
+ "九九式艦爆二二型",
82
+ "九六式艦戦改",
83
+ "九六式陸攻",
84
+ "九七式艦攻(九三一空)",
85
+ "九七式艦攻(熟練)",
86
+ "九七式艦攻(友永隊)",
87
+ "九七式艦攻改 試製三号戊型(空六号電探改装備機)",
88
+ "九一式徹甲弾",
89
+ "烈風 一一型",
90
+ "零式艦戦21型(熟練)",
91
+ "零式艦戦32型(熟練)",
92
+ "零式艦戦62型(爆戦)",
93
+ "零式艦戦63型(爆戦)",
94
+ "零式水上偵察機11型乙",
95
+ "零式小型水上機",
96
+ "零戦52型丙(六〇一空)",
97
+ "流星(六〇一空)",
98
+ "流星改",
99
+ "流星改(熟練)",
100
+ "毘式40mm連装機銃",
101
+ "秋刀魚の缶詰",
102
+ "瑞雲12型",
103
+ "瑞雲12型(六三四空)",
104
+ "三式爆雷投射機 集中配備",
105
+ "三式弾",
106
+ "試製35.6cm三連装砲",
107
+ "試製41cm三連装砲",
108
+ "試製46cm連装砲",
109
+ "試製51cm連装砲",
110
+ "試製烈風 後期型",
111
+ "試製晴嵐",
112
+ "試製FaT仕様九五式酸素魚雷改",
113
+ "熟練甲板要員",
114
+ "熟練見張員",
115
+ "四式水中聴音機",
116
+ "天山(六〇一空)",
117
+ "天山一二型甲",
118
+ "通信装置&要員",
119
+ "新型高温高圧缶",
120
+ "洋上補給",
121
+ "一式陸攻",
122
+ "一式陸攻 二二型甲",
123
+ "一式戦 隼II型",
124
+ "増設バルジ(大型艦)",
125
+ "増設バルジ(中型艦)",
126
+ "戦闘糧食",
127
+ "戦闘糧食(特別なおにぎり)",
128
+ "照明弾",
129
+ "紫電二一型 紫電改",
130
+ "紫電改二",
131
+ "紫電一一型",
132
+ "Ar196改",
133
+ "Bf109T改",
134
+ "Bofors 40mm四連装機関砲",
135
+ "Bofors15.2cm連装砲 Model1930",
136
+ "F4F-3",
137
+ "F4F-4",
138
+ "F4U-1D",
139
+ "F6F-3",
140
+ "FM-2",
141
+ "Fulmar",
142
+ "FuMO25 レーダー",
143
+ "Fw190T改",
144
+ "GFCS Mk.37",
145
+ "Ju87C改",
146
+ "OS2U",
147
+ "OTO 152mm三連装速射砲",
148
+ "QF 2ポンド8連装ポンポン砲",
149
+ "QF 4.7inch砲 Mk.XII改",
150
+ "Re.2001 G改",
151
+ "Re.2001 OR改",
152
+ "Ro.44水上戦闘機",
153
+ "S9 Osprey",
154
+ "SB2C-3",
155
+ "SBD",
156
+ "SG レーダー(初期型)",
157
+ "SKC34 20.3cm連装砲",
158
+ "Skua",
159
+ "SKレーダー",
160
+ "SM.79",
161
+ "SOC Seagull",
162
+ "Spitfire Mk.I",
163
+ "Spitfire Mk.V",
164
+ "Swordfish",
165
+ "Swordfish Mk.II(熟練)",
166
+ "Swordfish Mk.II改(水偵型)",
167
+ "TBD",
168
+ "TBF",
169
+ "Type124 ASDIC",
170
+ "Type144/147 ASDIC",
171
+ "オ号観測機改",
172
+ "プリエーゼ式水中防御隔壁"
173
+ ],
174
+ "secondary": []
175
+ }
package/index.js CHANGED
@@ -1,10 +1,41 @@
1
1
  const React = require("react");
2
2
  const fs = require("fs");
3
- const path = require("path");
4
- const { WindowEnv } = require("views/components/etc/window-env");
5
- const { SlotitemIcon, MaterialIcon } = require("views/components/etc/icon");
6
-
7
- const LEVEL_ONE_ROWS = [
3
+ const path = require("path");
4
+ const { WindowEnv } = require("views/components/etc/window-env");
5
+ const { SlotitemIcon, MaterialIcon } = require("views/components/etc/icon");
6
+
7
+ function readPluginVersion() {
8
+ try {
9
+ const info = require("./package.json")
10
+ return String((info && info.version) || "0.0.0")
11
+ } catch (_) {
12
+ return "0.0.0"
13
+ }
14
+ }
15
+
16
+ const PLUGIN_VERSION = readPluginVersion()
17
+ const HELP_PAGE_REVISION = "1"
18
+ const HELP_VERSION = PLUGIN_VERSION + "." + HELP_PAGE_REVISION
19
+ const HELP_UPDATE_NOTES = {
20
+ "2.0.17": [
21
+ "新增使用说明页与更新提示。",
22
+ "新增稀有素材消耗提示。",
23
+ "素材计算新增排序与开发资材期望统计。",
24
+ "“我变强了!”新增完成度、库存合并与进化目标标记。",
25
+ ],
26
+ "2.0.18": [
27
+ "新增稀有素材消耗提示;",
28
+ "优化了装备进化在我变强了页中的显示逻辑,现在会和进化后的改修计划分开统计,以方便检查阶段性目标的完成情况;",
29
+ "优化了我变强了页中的目标/已有装备的显示,现在不会自动拆分已完成的装备单独置顶显示了;",
30
+ "在改修列表页新增了改修计划的显示(装备名称的下方)。",
31
+ ],
32
+ "2.0.19": [
33
+ "“我变强了”页中,完成数与目标数相等并置顶时,完成数列改为显示 clear!。",
34
+ ],
35
+ }
36
+ const CURRENT_HELP_UPDATE_NOTES = HELP_UPDATE_NOTES[PLUGIN_VERSION] || ["本次更新暂无详细说明。"]
37
+
38
+ const LEVEL_ONE_ROWS = [
8
39
  { range: "★0→1", p: 1.0 },
9
40
  { range: "★1→2", p: 1.0 },
10
41
  { range: "★2→3", p: 1.0 },
@@ -37,10 +68,33 @@ const PLAN_LEVELS_MAX = PLAN_LEVELS_6.concat([
37
68
  ]);
38
69
 
39
70
  const WEEKDAY_KEYS = ["monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"];
40
- const WEEKDAY_LABELS = { monday: "周一", tuesday: "周二", wednesday: "周三", thursday: "周四", friday: "周五", saturday: "周六", sunday: "周日" };
41
- const ALL_DAYS_KEY = "all";
42
-
43
- function japanWeekdayKey() {
71
+ const WEEKDAY_LABELS = { monday: "周一", tuesday: "周二", wednesday: "周三", thursday: "周四", friday: "周五", saturday: "周六", sunday: "周日" };
72
+ const ALL_DAYS_KEY = "all";
73
+ const RARE_MATERIAL_KEYS = new Set([
74
+ "新型砲熕兵装資材",
75
+ "新型兵装資材",
76
+ "戦闘詳報",
77
+ "勲章",
78
+ "ネ式エンジン",
79
+ "熟練搭乗員",
80
+ "海外艦最新技術",
81
+ "新型航空兵装資材",
82
+ "工廠資源",
83
+ "新型噴進装備開発資材",
84
+ "潜水艦補給物資",
85
+ ]);
86
+
87
+ let RARE_EQUIP_NAMES = null
88
+ let SECONDARY_EQUIP_NAMES = null
89
+ function getRareEquipSets() {
90
+ if (RARE_EQUIP_NAMES) return { rare: RARE_EQUIP_NAMES, secondary: SECONDARY_EQUIP_NAMES }
91
+ const data = loadJson("equipment_rarity.json") || {}
92
+ RARE_EQUIP_NAMES = new Set(data.rare || [])
93
+ SECONDARY_EQUIP_NAMES = new Set(data.secondary || [])
94
+ return { rare: RARE_EQUIP_NAMES, secondary: SECONDARY_EQUIP_NAMES }
95
+ }
96
+
97
+ function japanWeekdayKey() {
44
98
  const japanNow = new Date(Date.now() + 9 * 60 * 60 * 1000)
45
99
  return ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"][japanNow.getUTCDay()]
46
100
  }
@@ -158,17 +212,36 @@ const CSS = `
158
212
  padding: 12px 14px 20px;
159
213
  border-bottom: 1px solid rgba(255, 255, 255, 0.12);
160
214
  }
161
- .kr2-title-row {
162
- display: flex;
163
- flex-direction: column;
164
- align-items: stretch;
165
- justify-content: flex-start;
166
- gap: 12px;
167
- }
168
- .kr2-title {
169
- font-size: 18px;
170
- font-weight: 800;
171
- }
215
+ .kr2-title-row {
216
+ display: flex;
217
+ flex-direction: column;
218
+ align-items: stretch;
219
+ justify-content: flex-start;
220
+ gap: 12px;
221
+ }
222
+ .kr2-title-line {
223
+ display: flex;
224
+ align-items: center;
225
+ gap: 10px;
226
+ flex-wrap: wrap;
227
+ }
228
+ .kr2-title {
229
+ font-size: 18px;
230
+ font-weight: 800;
231
+ }
232
+ .kr2-help-btn {
233
+ padding: 4px 10px;
234
+ border: 1px solid #5f6b7c;
235
+ border-radius: 4px;
236
+ background: #2f343c;
237
+ color: #8abbff;
238
+ font-size: 12px;
239
+ cursor: pointer;
240
+ }
241
+ .kr2-help-btn:hover {
242
+ background: #383e47;
243
+ color: #a8c8ff;
244
+ }
172
245
  .kr2-title-note {
173
246
  margin-top: 2px;
174
247
  font-size: 18px;
@@ -570,17 +643,41 @@ const CSS = `
570
643
  color: #d6e4f5;
571
644
  }
572
645
 
573
- .kr2-strong {
574
- margin-top: 12px;
575
- flex: 1;
646
+ .kr2-strong {
647
+ margin-top: 12px;
648
+ flex: 1;
576
649
  min-height: 60vh;
577
650
  overflow-x: hidden;
578
651
  overflow-y: auto;
579
652
  }
580
- .kr2-strong-table {
581
- border-collapse: separate;
582
- border-spacing: 0;
583
- }
653
+ .kr2-table.kr2-strong-table {
654
+ border-collapse: separate;
655
+ border-spacing: 0;
656
+ table-layout: fixed;
657
+ width: 100%;
658
+ }
659
+ .kr2-strong-table th:nth-child(1),
660
+ .kr2-strong-table td:nth-child(1) {
661
+ width: 100px;
662
+ }
663
+ .kr2-strong-table th:nth-child(2),
664
+ .kr2-strong-table td:nth-child(2) {
665
+ width: 32%;
666
+ }
667
+ .kr2-strong-table th:nth-child(4),
668
+ .kr2-strong-table td:nth-child(4) {
669
+ width: 48px;
670
+ padding-left: 2px !important;
671
+ padding-right: 2px !important;
672
+ white-space: nowrap;
673
+ }
674
+ .kr2-strong-table th:nth-child(5),
675
+ .kr2-strong-table td:nth-child(5) {
676
+ width: 64px;
677
+ padding-left: 2px !important;
678
+ padding-right: 2px !important;
679
+ white-space: nowrap;
680
+ }
584
681
  .kr2-strong-cat-th {
585
682
  position: relative;
586
683
  cursor: pointer;
@@ -631,9 +728,10 @@ const CSS = `
631
728
  .kr2-strong-table tbody tr:last-child td {
632
729
  border-bottom: 0;
633
730
  }
634
- .kr2-strong-equip {
635
- text-align: left !important;
636
- white-space: normal;
731
+ .kr2-strong-equip {
732
+ text-align: center !important;
733
+ white-space: normal;
734
+ font-weight: 700;
637
735
  }
638
736
  .kr2-strong-level {
639
737
  display: inline-flex;
@@ -649,10 +747,20 @@ const CSS = `
649
747
  color: #ffffff;
650
748
  font-weight: 700;
651
749
  }
652
- .kr2-strong-stock {
653
- white-space: normal;
654
- }
655
- .kr2-strong-stock-name {
750
+ .kr2-strong-stock {
751
+ white-space: normal;
752
+ text-align: center !important;
753
+ }
754
+ .kr2-strong-stock-th {
755
+ text-align: center !important;
756
+ }
757
+ .kr2-strong-stock-line {
758
+ display: block;
759
+ }
760
+ .kr2-strong-stock-line + .kr2-strong-stock-line {
761
+ margin-top: 2px;
762
+ }
763
+ .kr2-strong-stock-name {
656
764
  color: #ffffff;
657
765
  font-weight: 700;
658
766
  }
@@ -687,10 +795,28 @@ const CSS = `
687
795
  .kr2-strong-owned.kr2-strong-stock-short {
688
796
  color: #ff6b6b;
689
797
  }
690
- .kr2-strong-owned.kr2-strong-stock-clear {
691
- color: #4fc3f7;
692
- }
693
- .kr2-strong-clear-row td {
798
+ .kr2-strong-owned.kr2-strong-stock-clear {
799
+ color: #4fc3f7;
800
+ }
801
+ .kr2-strong-owned-line {
802
+ display: block;
803
+ }
804
+ .kr2-strong-owned-line + .kr2-strong-owned-line {
805
+ margin-top: 2px;
806
+ }
807
+ .kr2-strong-owned-line-clear {
808
+ color: #4fc3f7;
809
+ }
810
+ .kr2-strong-owned-line-short {
811
+ color: #ff6b6b;
812
+ }
813
+ .kr2-strong-owned-line-missing {
814
+ color: #abb3bf;
815
+ }
816
+ .kr2-strong-completion-done {
817
+ color: #4fc3f7;
818
+ }
819
+ .kr2-strong-clear-row td {
694
820
  color: #4fc3f7;
695
821
  }
696
822
  .kr2-strong-clear-row .kr2-strong-target,
@@ -866,12 +992,18 @@ const CSS = `
866
992
  color: #abb3bf;
867
993
  cursor: default;
868
994
  }
869
- .kr2-row-not-improveable .kr2-name,
870
- .kr2-row-not-improveable .kr2-secretary,
871
- .kr2-row-not-improveable .kr2-meta {
872
- color: #abb3bf;
873
- }
874
- .kr2-reco-empty {
995
+ .kr2-row-not-improveable .kr2-name,
996
+ .kr2-row-not-improveable .kr2-secretary,
997
+ .kr2-row-not-improveable .kr2-meta {
998
+ color: #abb3bf;
999
+ }
1000
+ .kr2-row-clear {
1001
+ background: rgba(79, 195, 247, 0.14);
1002
+ }
1003
+ .kr2-row-clear:hover {
1004
+ background: rgba(79, 195, 247, 0.2);
1005
+ }
1006
+ .kr2-reco-empty {
875
1007
  min-height: 34px;
876
1008
  }
877
1009
  .kr2-not-improveable-group {
@@ -961,14 +1093,35 @@ const CSS = `
961
1093
  font-size: 9px;
962
1094
  line-height: 1.2;
963
1095
  }
964
- .kr2-name {
965
- flex: 1 1 0;
966
- min-width: 0;
967
- font-weight: 600;
968
- white-space: nowrap;
969
- overflow: hidden;
970
- text-overflow: ellipsis;
971
- }
1096
+ .kr2-name-cell {
1097
+ flex: 1 1 0;
1098
+ min-width: 0;
1099
+ display: flex;
1100
+ flex-direction: column;
1101
+ justify-content: center;
1102
+ gap: 1px;
1103
+ }
1104
+ .kr2-name {
1105
+ font-weight: 600;
1106
+ white-space: nowrap;
1107
+ overflow: hidden;
1108
+ text-overflow: ellipsis;
1109
+ }
1110
+ .kr2-name-completion {
1111
+ font-size: 11px;
1112
+ line-height: 1.2;
1113
+ white-space: nowrap;
1114
+ font-weight: 400;
1115
+ }
1116
+ .kr2-name-completion-incomplete {
1117
+ color: #ff6b6b;
1118
+ }
1119
+ .kr2-name-completion-done-active {
1120
+ color: #4fc3f7;
1121
+ }
1122
+ .kr2-name-completion-clear {
1123
+ color: #4fc3f7;
1124
+ }
972
1125
  .kr2-secretary {
973
1126
  flex: 1 1 0;
974
1127
  min-width: 0;
@@ -1117,7 +1270,17 @@ const CSS = `
1117
1270
  align-items: flex-end;
1118
1271
  gap: 6px;
1119
1272
  }
1120
- .kr2-material-list {
1273
+ .kr2-rare-warning {
1274
+ color: #ff6b6b;
1275
+ font-size: 11px;
1276
+ white-space: nowrap;
1277
+ }
1278
+ .kr2-rare-warning-secondary {
1279
+ color: #ffb3b3;
1280
+ font-size: 11px;
1281
+ white-space: nowrap;
1282
+ }
1283
+ .kr2-material-list {
1121
1284
  display: flex;
1122
1285
  flex-direction: column;
1123
1286
  align-items: flex-end;
@@ -1235,11 +1398,64 @@ const CSS = `
1235
1398
  font-size: 12px;
1236
1399
  margin-top: 2px;
1237
1400
  }
1238
- .kr2-modal-actions {
1239
- display: flex;
1240
- gap: 8px;
1241
- }
1242
- .kr2-dev-scroll {
1401
+ .kr2-modal-actions {
1402
+ display: flex;
1403
+ gap: 8px;
1404
+ }
1405
+ .kr2-modal-close {
1406
+ width: 30px;
1407
+ height: 30px;
1408
+ padding: 0;
1409
+ display: flex;
1410
+ align-items: center;
1411
+ justify-content: center;
1412
+ border: 1px solid #5f6b7c;
1413
+ border-radius: 4px;
1414
+ background: #2f343c;
1415
+ color: #f6f7f9;
1416
+ font-size: 18px;
1417
+ line-height: 1;
1418
+ cursor: pointer;
1419
+ }
1420
+ .kr2-modal-close:hover {
1421
+ background: #383e47;
1422
+ color: #fff;
1423
+ }
1424
+ .kr2-help-modal {
1425
+ width: 720px;
1426
+ }
1427
+ .kr2-help-body {
1428
+ flex: 1;
1429
+ min-height: 0;
1430
+ overflow-y: auto;
1431
+ padding: 14px 16px 18px;
1432
+ }
1433
+ .kr2-help-lead {
1434
+ margin-bottom: 10px;
1435
+ color: #8abbff;
1436
+ font-size: 14px;
1437
+ font-weight: 800;
1438
+ }
1439
+ .kr2-help-section + .kr2-help-section {
1440
+ margin-top: 14px;
1441
+ }
1442
+ .kr2-help-section-title {
1443
+ margin-bottom: 6px;
1444
+ color: #8abbff;
1445
+ font-size: 14px;
1446
+ font-weight: 800;
1447
+ }
1448
+ .kr2-help-list {
1449
+ margin: 0;
1450
+ padding-left: 20px;
1451
+ color: #d6d9de;
1452
+ font-size: 13px;
1453
+ line-height: 1.7;
1454
+ }
1455
+ .kr2-help-list li + li {
1456
+ margin-top: 2px;
1457
+ }
1458
+ .kr2-dev-scroll {
1243
1459
  flex: 1;
1244
1460
  min-height: 0;
1245
1461
  overflow-y: auto;
@@ -2365,7 +2581,20 @@ function buildRecommendation(normal, certain, p) {
2365
2581
  return { expected, text: "相同", tone: "same" }
2366
2582
  }
2367
2583
 
2368
- function MaterialDetail({ materials, onMaterialClick, inventoryByEquip, onInventoryClick, useItemCounts }) {
2584
+ function classifyRareMaterial(materials, kcDevData) {
2585
+ const sets = getRareEquipSets()
2586
+ let hasSecondary = false
2587
+ for (const item of materials || []) {
2588
+ if (!item) continue
2589
+ if (item.item_material_key != null && RARE_MATERIAL_KEYS.has(String(item.item_material_key))) return "rare"
2590
+ const name = String(item.item_name || "")
2591
+ if (sets.rare.has(name)) return "rare"
2592
+ if (sets.secondary.has(name)) hasSecondary = true
2593
+ }
2594
+ return hasSecondary ? "secondary" : null
2595
+ }
2596
+
2597
+ function MaterialDetail({ materials, onMaterialClick, inventoryByEquip, onInventoryClick, useItemCounts }) {
2369
2598
  const items = (materials || []).filter((item) => item && (item.item_name || item.item_material_key || item.item_equipment_id != null))
2370
2599
  if (items.length === 0) {
2371
2600
  return React.createElement("div", { className: "kr2-material-empty" }, "无消耗装备")
@@ -2877,7 +3106,143 @@ function PlanningPage({ rows, inventoryByEquip, useItemCounts, selection, onTogg
2877
3106
  )
2878
3107
  }
2879
3108
 
2880
- class StrongPage extends React.Component {
3109
+ function formatStrongLevel(level) {
3110
+ const n = Number(level)
3111
+ if (n >= 10) return "max"
3112
+ return "+" + String(n)
3113
+ }
3114
+
3115
+ function buildStrongEntries(rows, inventoryByEquip, selection) {
3116
+ const inventoryLoaded = inventoryByEquip && Object.keys(inventoryByEquip).length > 0
3117
+ const entries = []
3118
+ const demandMap = {}
3119
+ for (const row of rows || []) {
3120
+ const sel = (selection || {})[row.id] || { selected: true, target: "max", qty: 1 }
3121
+ const qty = Math.max(1, Math.min(99, Math.round(Number(sel.qty) || 1)))
3122
+ const evoUpgradeId = sel.evoUpgradeId || null
3123
+ const evoUpgrade = evoUpgradeId ? (row.upgrades || []).find((u) => String(u.upgrade_id) === String(evoUpgradeId)) : null
3124
+ const displayRow = evoUpgrade
3125
+ ? Object.assign({}, row, { id: row.id + ":evo:" + evoUpgrade.upgrade_id, name: evoUpgrade.targetName, category: evoUpgrade.targetCategory || row.category })
3126
+ : row
3127
+ const targetEquipId = evoUpgrade ? String(evoUpgrade.upgrade_id) : row.id
3128
+ const targetLevelValue = evoUpgrade ? 0 : (row.improveable === false ? 0 : (sel.target === "max" ? 10 : 6))
3129
+ const bucket = demandMap[targetEquipId] || (demandMap[targetEquipId] = { equipId: targetEquipId, displayRow, sourceRowIds: [], sourceRowIdsByLevel: {}, demands: {}, evolvedLevels: {} })
3130
+ if (bucket.sourceRowIds.indexOf(row.id) < 0) bucket.sourceRowIds.push(row.id)
3131
+ const levelKey = String(targetLevelValue)
3132
+ const levelSources = bucket.sourceRowIdsByLevel[levelKey] || (bucket.sourceRowIdsByLevel[levelKey] = [])
3133
+ if (levelSources.indexOf(row.id) < 0) levelSources.push(row.id)
3134
+ if (evoUpgrade) bucket.evolvedLevels[String(targetLevelValue)] = true
3135
+ bucket.demands[String(targetLevelValue)] = (bucket.demands[String(targetLevelValue)] || 0) + qty
3136
+ }
3137
+ for (const bucket of Object.values(demandMap)) {
3138
+ const inv = (inventoryByEquip || {})[bucket.equipId]
3139
+ const inventoryLevels = []
3140
+ if (inv && inv.levels) {
3141
+ for (const level of Object.keys(inv.levels)) {
3142
+ const count = Math.max(0, Math.round(Number(inv.levels[level]) || 0))
3143
+ if (count > 0) inventoryLevels.push({ level: Number(level), count })
3144
+ }
3145
+ inventoryLevels.sort((a, b) => b.level - a.level)
3146
+ }
3147
+ const remaining = {}
3148
+ for (const lv of inventoryLevels) remaining[String(lv.level)] = lv.count
3149
+ const targetLevels = Object.keys(bucket.demands).map(Number).sort((a, b) => b - a)
3150
+ const allocation = {}
3151
+ const shortfall = {}
3152
+ for (const tl of targetLevels) {
3153
+ let need = bucket.demands[String(tl)] || 0
3154
+ for (const lv of inventoryLevels) {
3155
+ if (need <= 0) break
3156
+ const avail = remaining[String(lv.level)] || 0
3157
+ if (avail <= 0) continue
3158
+ const take = Math.min(need, avail)
3159
+ const key = String(tl) + "|" + String(lv.level)
3160
+ const alloc = allocation[key] || (allocation[key] = { targetLevelValue: tl, inventoryLevel: lv.level, qty: 0, stock: 0 })
3161
+ alloc.qty += take
3162
+ alloc.stock += take
3163
+ remaining[String(lv.level)] = avail - take
3164
+ need -= take
3165
+ }
3166
+ if (need > 0) shortfall[String(tl)] = need
3167
+ }
3168
+ for (const key of Object.keys(allocation)) {
3169
+ const alloc = allocation[key]
3170
+ entries.push({ key: bucket.equipId + ":" + key, row: bucket.displayRow, sourceRowIds: bucket.sourceRowIdsByLevel[String(alloc.targetLevelValue)] || bucket.sourceRowIds, evolved: !!bucket.evolvedLevels[String(alloc.targetLevelValue)], targetLevel: formatStrongLevel(alloc.targetLevelValue), targetLevelValue: alloc.targetLevelValue, qty: alloc.qty, inventoryLevel: alloc.inventoryLevel, stock: alloc.stock })
3171
+ }
3172
+ for (const tl of Object.keys(shortfall)) {
3173
+ const value = Number(tl)
3174
+ entries.push({ key: bucket.equipId + ":short:" + tl, row: bucket.displayRow, sourceRowIds: bucket.sourceRowIdsByLevel[String(value)] || bucket.sourceRowIds, evolved: !!bucket.evolvedLevels[String(value)], targetLevel: formatStrongLevel(value), targetLevelValue: value, qty: shortfall[tl], inventoryLevel: null, stock: inventoryLoaded ? 0 : null, notFound: true })
3175
+ }
3176
+ }
3177
+ for (const item of entries) {
3178
+ item.clear = item.qty > 0 && item.stock != null && item.inventoryLevel >= item.targetLevelValue && item.stock >= item.qty
3179
+ item.completed = item.clear ? item.qty : 0
3180
+ item.levelShort = item.inventoryLevel != null && item.inventoryLevel < item.targetLevelValue
3181
+ item.countShort = item.stock != null && item.qty > 0 && item.stock < item.qty
3182
+ item.countEnough = item.stock != null && item.stock >= item.qty
3183
+ }
3184
+ const levelRank = (item) => item.inventoryLevel == null ? -1 : item.inventoryLevel
3185
+ entries.sort((a, b) => {
3186
+ const ac = a.clear ? 1 : 0
3187
+ const bc = b.clear ? 1 : 0
3188
+ if (ac !== bc) return bc - ac
3189
+ if (a.clear) return b.qty - a.qty || levelRank(b) - levelRank(a)
3190
+ const sa = a.stock == null ? -1 : a.stock
3191
+ const sb = b.stock == null ? -1 : b.stock
3192
+ return sb - sa || b.qty - a.qty || levelRank(b) - levelRank(a)
3193
+ })
3194
+ return { entries, inventoryLoaded }
3195
+ }
3196
+
3197
+ function buildStrongGroups(entries) {
3198
+ const groupsByKey = {}
3199
+ for (const item of entries || []) {
3200
+ const key = item.row.id + "|" + String(item.targetLevelValue)
3201
+ const group = groupsByKey[key] || (groupsByKey[key] = {
3202
+ key,
3203
+ row: item.row,
3204
+ targetLevel: item.targetLevel,
3205
+ targetLevelValue: item.targetLevelValue,
3206
+ items: [],
3207
+ sourceRowIds: [],
3208
+ qty: 0,
3209
+ completed: 0,
3210
+ stock: 0,
3211
+ stockLoaded: false,
3212
+ maxInventoryLevel: -1,
3213
+ evolved: false,
3214
+ })
3215
+ group.items.push(item)
3216
+ group.qty += item.qty
3217
+ group.completed += item.completed || 0
3218
+ if (item.stock != null) {
3219
+ group.stock += item.stock
3220
+ group.stockLoaded = true
3221
+ }
3222
+ if (item.inventoryLevel != null) group.maxInventoryLevel = Math.max(group.maxInventoryLevel, item.inventoryLevel)
3223
+ if (item.evolved) group.evolved = true
3224
+ for (const rowId of item.sourceRowIds || []) {
3225
+ if (group.sourceRowIds.indexOf(rowId) < 0) group.sourceRowIds.push(rowId)
3226
+ }
3227
+ }
3228
+ const groups = Object.keys(groupsByKey).map((key) => groupsByKey[key])
3229
+ for (const group of groups) {
3230
+ group.items.sort((a, b) => (b.inventoryLevel == null ? -1 : b.inventoryLevel) - (a.inventoryLevel == null ? -1 : a.inventoryLevel))
3231
+ group.clear = group.qty > 0 && group.completed >= group.qty
3232
+ }
3233
+ groups.sort((a, b) => {
3234
+ const ac = a.clear ? 1 : 0
3235
+ const bc = b.clear ? 1 : 0
3236
+ if (ac !== bc) return bc - ac
3237
+ if (a.clear) return b.qty - a.qty || b.maxInventoryLevel - a.maxInventoryLevel
3238
+ const sa = a.stockLoaded ? a.stock : -1
3239
+ const sb = b.stockLoaded ? b.stock : -1
3240
+ return sb - sa || b.qty - a.qty || b.maxInventoryLevel - a.maxInventoryLevel
3241
+ })
3242
+ return groups
3243
+ }
3244
+
3245
+ class StrongPage extends React.Component {
2881
3246
  catRef = React.createRef()
2882
3247
  state = {
2883
3248
  categoryOpen: false,
@@ -2916,96 +3281,28 @@ class StrongPage extends React.Component {
2916
3281
  })
2917
3282
  }
2918
3283
 
2919
- render() {
2920
- const { rows, inventoryByEquip, selection } = this.props
2921
- const inventoryLoaded = inventoryByEquip && Object.keys(inventoryByEquip).length > 0
2922
- const entries = []
2923
- const levelText = (level) => {
2924
- const n = Number(level)
2925
- if (n >= 10) return "max"
2926
- return "+" + String(n)
2927
- }
2928
- const demandMap = {}
2929
- for (const row of rows || []) {
2930
- const sel = (selection || {})[row.id] || { selected: true, target: "max", qty: 1 }
2931
- const qty = Math.max(1, Math.min(99, Math.round(Number(sel.qty) || 1)))
2932
- const evoUpgradeId = sel.evoUpgradeId || null
2933
- const evoUpgrade = evoUpgradeId ? (row.upgrades || []).find((u) => String(u.upgrade_id) === String(evoUpgradeId)) : null
2934
- const displayRow = evoUpgrade
2935
- ? Object.assign({}, row, { id: row.id + ":evo:" + evoUpgrade.upgrade_id, name: evoUpgrade.targetName, category: evoUpgrade.targetCategory || row.category })
2936
- : row
2937
- const targetEquipId = evoUpgrade ? String(evoUpgrade.upgrade_id) : row.id
2938
- const targetLevelValue = evoUpgrade ? 0 : (row.improveable === false ? 0 : (sel.target === "max" ? 10 : 6))
2939
- const bucket = demandMap[targetEquipId] || (demandMap[targetEquipId] = { equipId: targetEquipId, displayRow, demands: {} })
2940
- bucket.demands[String(targetLevelValue)] = (bucket.demands[String(targetLevelValue)] || 0) + qty
2941
- }
2942
- for (const bucket of Object.values(demandMap)) {
2943
- const inv = (inventoryByEquip || {})[bucket.equipId]
2944
- const inventoryLevels = []
2945
- if (inv && inv.levels) {
2946
- for (const level of Object.keys(inv.levels)) {
2947
- const count = Math.max(0, Math.round(Number(inv.levels[level]) || 0))
2948
- if (count > 0) inventoryLevels.push({ level: Number(level), count })
2949
- }
2950
- inventoryLevels.sort((a, b) => b.level - a.level)
2951
- }
2952
- const remaining = {}
2953
- for (const lv of inventoryLevels) remaining[String(lv.level)] = lv.count
2954
- const targetLevels = Object.keys(bucket.demands).map(Number).sort((a, b) => b - a)
2955
- const allocation = {}
2956
- const shortfall = {}
2957
- for (const tl of targetLevels) {
2958
- let need = bucket.demands[String(tl)] || 0
2959
- for (const lv of inventoryLevels) {
2960
- if (need <= 0) break
2961
- const avail = remaining[String(lv.level)] || 0
2962
- if (avail <= 0) continue
2963
- const take = Math.min(need, avail)
2964
- const key = String(tl) + "|" + String(lv.level)
2965
- const alloc = allocation[key] || (allocation[key] = { targetLevelValue: tl, inventoryLevel: lv.level, qty: 0, stock: 0 })
2966
- alloc.qty += take
2967
- alloc.stock += take
2968
- remaining[String(lv.level)] = avail - take
2969
- need -= take
2970
- }
2971
- if (need > 0) shortfall[String(tl)] = need
2972
- }
2973
- for (const key of Object.keys(allocation)) {
2974
- const alloc = allocation[key]
2975
- entries.push({ key: bucket.equipId + ":" + key, row: bucket.displayRow, targetLevel: levelText(alloc.targetLevelValue), targetLevelValue: alloc.targetLevelValue, qty: alloc.qty, inventoryLevel: alloc.inventoryLevel, stock: alloc.stock })
2976
- }
2977
- for (const tl of Object.keys(shortfall)) {
2978
- const value = Number(tl)
2979
- entries.push({ key: bucket.equipId + ":short:" + tl, row: bucket.displayRow, targetLevel: levelText(value), targetLevelValue: value, qty: shortfall[tl], inventoryLevel: null, stock: inventoryLoaded ? 0 : null, notFound: true })
2980
- }
2981
- }
2982
- for (const item of entries) {
2983
- item.clear = item.qty > 0 && item.stock != null && item.inventoryLevel === item.targetLevelValue && item.stock >= item.qty
2984
- item.levelShort = item.inventoryLevel != null && item.inventoryLevel < item.targetLevelValue
2985
- item.countShort = item.stock != null && item.qty > 0 && item.stock < item.qty
2986
- item.countEnough = item.stock != null && item.stock >= item.qty
2987
- }
2988
- const levelRank = (item) => item.inventoryLevel == null ? -1 : item.inventoryLevel
2989
- entries.sort((a, b) => {
2990
- const ac = a.clear ? 1 : 0
2991
- const bc = b.clear ? 1 : 0
2992
- if (ac !== bc) return bc - ac
2993
- if (a.clear) return b.qty - a.qty || levelRank(b) - levelRank(a)
2994
- const sa = a.stock == null ? -1 : a.stock
2995
- const sb = b.stock == null ? -1 : b.stock
2996
- return sb - sa || b.qty - a.qty || levelRank(b) - levelRank(a)
2997
- })
2998
- const categories = Array.from(new Set(entries.map((item) => item.row.category).filter(Boolean))).sort((a, b) => a.localeCompare(b, "zh-Hans-CN"))
2999
- const selectedCategories = this.state.selectedCategories
3000
- const visibleEntries = selectedCategories == null ? entries : entries.filter((item) => selectedCategories.indexOf(item.row.category) >= 0)
3001
- const stockText = (item) => {
3002
- const nameNode = React.createElement("span", { className: "kr2-strong-stock-name" }, item.row.name)
3003
- if (item.notFound || item.stock == null) return nameNode
3004
- const levelPart = levelText(item.inventoryLevel)
3005
- return React.createElement("span", null, nameNode, " ", levelPart)
3006
- }
3007
- const stockClass = (item) => item.notFound || item.stock == null ? "kr2-strong-stock-notfound" : item.clear ? "kr2-strong-stock-clear" : item.levelShort ? "kr2-strong-stock-short" : "kr2-strong-stock-ok"
3008
- const ownedClass = (item) => item.stock == null ? "kr2-strong-stock-missing" : item.clear || item.countEnough ? "kr2-strong-stock-clear" : item.countShort ? "kr2-strong-stock-short" : "kr2-strong-stock-ok"
3284
+ render() {
3285
+ const { rows, inventoryByEquip, selection } = this.props
3286
+ const { entries } = buildStrongEntries(rows, inventoryByEquip, selection)
3287
+ const groups = buildStrongGroups(entries)
3288
+ const categories = Array.from(new Set(groups.map((group) => group.row.category).filter(Boolean))).sort((a, b) => a.localeCompare(b, "zh-Hans-CN"))
3289
+ const selectedCategories = this.state.selectedCategories
3290
+ const visibleGroups = selectedCategories == null ? groups : groups.filter((group) => selectedCategories.indexOf(group.row.category) >= 0)
3291
+ const stockText = (item) => {
3292
+ const nameNode = React.createElement("span", { className: "kr2-strong-stock-name" }, item.row.name)
3293
+ if (item.notFound || item.stock == null) return nameNode
3294
+ const levelPart = formatStrongLevel(item.inventoryLevel)
3295
+ return React.createElement("span", null, nameNode, " ", levelPart)
3296
+ }
3297
+ const stockClass = (item) => item.notFound || item.stock == null ? "kr2-strong-stock-notfound" : item.clear ? "kr2-strong-stock-clear" : item.levelShort ? "kr2-strong-stock-short" : "kr2-strong-stock-ok"
3298
+ const groupOwnedClass = (group) => !group.stockLoaded ? "kr2-strong-stock-missing" : group.clear ? "kr2-strong-stock-clear" : "kr2-strong-stock-short"
3299
+ const completionLineClass = (item, group) => "kr2-strong-owned-line " + (item.stock == null ? "kr2-strong-owned-line-missing" : group.qty > 0 && (item.completed || 0) >= group.qty ? "kr2-strong-owned-line-clear" : "kr2-strong-owned-line-short")
3300
+ const completionLineText = (item, group) => {
3301
+ if (item.stock == null) return "--"
3302
+ const completed = item.completed || 0
3303
+ if (group.qty > 0 && completed >= group.qty) return String(completed)
3304
+ return completed > 0 ? React.createElement("span", { className: "kr2-strong-completion-done" }, String(completed)) : String(completed)
3305
+ }
3009
3306
  const catStyle = this.state.catPos ? { position: "fixed", top: this.state.catPos.top, left: this.state.catPos.left, minWidth: Math.max(180, this.state.catPos.width) } : null
3010
3307
  const categoryHead = React.createElement(
3011
3308
  "th",
@@ -3036,24 +3333,30 @@ class StrongPage extends React.Component {
3036
3333
  React.createElement(
3037
3334
  "thead",
3038
3335
  null,
3039
- React.createElement("tr", null, categoryHead, React.createElement("th", null, "目标装备"), React.createElement("th", null, "目标数"), React.createElement("th", null, "库存"), React.createElement("th", null, "完成数"))
3336
+ React.createElement("tr", null, categoryHead, React.createElement("th", null, "目标装备"), React.createElement("th", { className: "kr2-strong-stock-th" }, "库存装备"), React.createElement("th", null, "完成数"), React.createElement("th", null, "目标数"))
3040
3337
  ),
3041
3338
  React.createElement(
3042
3339
  "tbody",
3043
3340
  null,
3044
- visibleEntries.length === 0
3045
- ? React.createElement("tr", null, React.createElement("td", { colSpan: 5 }, "无匹配分类"))
3046
- : visibleEntries.map((item) =>
3047
- React.createElement(
3048
- "tr",
3049
- { key: item.key, className: item.clear ? "kr2-strong-clear-row" : null },
3050
- React.createElement("td", { className: "kr2-strong-cat" }, item.row.category || "-"),
3051
- React.createElement("td", { className: "kr2-strong-equip" }, item.row.name, " ", React.createElement("span", { className: "kr2-strong-level" }, item.targetLevel)),
3052
- React.createElement("td", { className: "kr2-strong-target" }, String(item.qty)),
3053
- React.createElement("td", { className: "kr2-strong-stock " + stockClass(item) }, stockText(item)),
3054
- React.createElement("td", { className: "kr2-strong-owned " + ownedClass(item) }, item.stock == null ? "--" : item.clear ? "clear!" : String(item.stock))
3055
- )
3056
- )
3341
+ visibleGroups.length === 0
3342
+ ? React.createElement("tr", null, React.createElement("td", { colSpan: 5 }, "无匹配分类"))
3343
+ : visibleGroups.map((group) =>
3344
+ React.createElement(
3345
+ "tr",
3346
+ { key: group.key, className: group.clear ? "kr2-strong-clear-row" : null },
3347
+ React.createElement("td", { className: "kr2-strong-cat" }, group.row.category || "-"),
3348
+ React.createElement(
3349
+ "td",
3350
+ { className: "kr2-strong-equip" },
3351
+ group.row.name + (group.evolved ? "(进化)" : ""),
3352
+ !group.evolved && group.row.improveable !== false ? " " : null,
3353
+ !group.evolved && group.row.improveable !== false ? React.createElement("span", { className: "kr2-strong-level" }, group.targetLevel) : null
3354
+ ),
3355
+ React.createElement("td", { className: "kr2-strong-stock" }, group.items.map((item) => React.createElement("div", { key: item.key, className: "kr2-strong-stock-line " + stockClass(item), "data-level": item.inventoryLevel == null ? "" : String(item.inventoryLevel) }, stockText(item)))),
3356
+ React.createElement("td", { className: "kr2-strong-owned " + groupOwnedClass(group), "data-completed": String(group.completed || 0), "data-stock": group.stockLoaded ? String(group.stock) : "", "data-level": String(group.maxInventoryLevel) }, group.clear ? "clear!" : group.items.map((item) => React.createElement("div", { key: item.key, className: completionLineClass(item, group), "data-level": item.inventoryLevel == null ? "" : String(item.inventoryLevel) }, completionLineText(item, group)))),
3357
+ React.createElement("td", { className: "kr2-strong-target", "data-target": String(group.qty) }, String(group.qty))
3358
+ )
3359
+ )
3057
3360
  )
3058
3361
  )
3059
3362
  )
@@ -3087,7 +3390,8 @@ class LevelOneTable extends React.Component {
3087
3390
  const normal = phase.consume_improvement_min
3088
3391
  const materials = phase.materials || []
3089
3392
  const hasMaterials = materials.some((item) => item && (item.item_name || item.item_material_key || item.item_equipment_id != null))
3090
- const rows = []
3393
+ const rareWarning = null
3394
+ const rows = []
3091
3395
  LEVEL_ONE_ROWS.forEach((rate) => {
3092
3396
  const key = rate.range
3093
3397
  rows.push(
@@ -3106,7 +3410,8 @@ class LevelOneTable extends React.Component {
3106
3410
  "div",
3107
3411
  { className: "kr2-material-cell" },
3108
3412
  React.createElement(RateCell, { range: "素材详情", open: !!this.state.open[key], onToggle: () => this.toggle(key) }),
3109
- !!this.state.open[key]
3413
+ rareWarning,
3414
+ !!this.state.open[key]
3110
3415
  ? React.createElement(MaterialDetail, { materials, onMaterialClick: this.props.onMaterialClick, inventoryByEquip: this.props.inventoryByEquip, useItemCounts: this.props.useItemCounts, onInventoryClick: this.props.onInventoryClick })
3111
3416
  : null
3112
3417
  )
@@ -3156,7 +3461,9 @@ class LevelTwoTable extends React.Component {
3156
3461
  const rec = buildRecommendation(normal, certain, p)
3157
3462
  const materials = step ? step.materials || [] : []
3158
3463
  const hasMaterials = materials.some((item) => item && (item.item_name || item.item_material_key || item.item_equipment_id != null))
3159
- const rangeNode = maxBlue
3464
+ const rareTier = rec.tone === "save" ? classifyRareMaterial(materials, kcDevData) : null
3465
+ const rareWarning = rareTier === "rare" ? React.createElement("div", { className: "kr2-rare-warning" }, "稀有素材消耗注意!") : rareTier === "secondary" ? React.createElement("div", { className: "kr2-rare-warning-secondary" }, "次级稀有素材消耗注意!") : null
3466
+ const rangeNode = maxBlue
3160
3467
  ? React.createElement("span", null, "★9→", React.createElement("span", { className: "kr2-level-max-blue" }, "max"))
3161
3468
  : range
3162
3469
  rows.push(
@@ -3192,7 +3499,8 @@ class LevelTwoTable extends React.Component {
3192
3499
  "div",
3193
3500
  { className: "kr2-material-cell" },
3194
3501
  React.createElement(RateCell, { range: rec.text, open: !!this.state.open[key], onToggle: () => this.toggle(key), tone: rec.tone }),
3195
- !!this.state.open[key]
3502
+ rareWarning,
3503
+ !!this.state.open[key]
3196
3504
  ? React.createElement(MaterialDetail, { materials, onMaterialClick: this.props.onMaterialClick, inventoryByEquip: this.props.inventoryByEquip, useItemCounts: this.props.useItemCounts, onInventoryClick: this.props.onInventoryClick })
3197
3505
  : null
3198
3506
  )
@@ -3270,13 +3578,14 @@ class LevelSection extends React.Component {
3270
3578
  }
3271
3579
  }
3272
3580
 
3273
- function EquipmentRow({ row, expanded, onToggle, onMaterialClick, inventoryByEquip, useItemCounts, onInventoryClick, isFavorite, onFavoriteClick, secretaryText, kcDevData }) {
3274
- return React.createElement(
3275
- "div",
3276
- null,
3277
- React.createElement(
3278
- "div",
3279
- { className: "kr2-row" + (row.improveable === false ? " kr2-row-not-improveable" : ""), onClick: row.improveable === false ? null : onToggle },
3581
+ function EquipmentRow({ row, expanded, onToggle, onMaterialClick, inventoryByEquip, useItemCounts, onInventoryClick, isFavorite, onFavoriteClick, secretaryText, kcDevData, completion }) {
3582
+ const clear = !!(completion && completion.target > 0 && completion.completed >= completion.target)
3583
+ return React.createElement(
3584
+ "div",
3585
+ null,
3586
+ React.createElement(
3587
+ "div",
3588
+ { className: "kr2-row" + (row.improveable === false ? " kr2-row-not-improveable" : "") + (clear ? " kr2-row-clear" : ""), onClick: row.improveable === false ? null : onToggle },
3280
3589
  React.createElement(
3281
3590
  "button",
3282
3591
  { className: "kr2-fav-btn" + (isFavorite ? " kr2-fav-active" : ""), onClick: (e) => { e.stopPropagation(); onFavoriteClick && onFavoriteClick(row.id) }, title: isFavorite ? "取消收藏" : "收藏" },
@@ -3288,7 +3597,24 @@ function EquipmentRow({ row, expanded, onToggle, onMaterialClick, inventoryByEqu
3288
3597
  React.createElement(SlotitemIcon, { slotitemId: row.icon, className: "kr2-icon" }),
3289
3598
  React.createElement("span", { className: "kr2-arrow" }, row.improveable === false ? "" : expanded ? "▼" : "▶")
3290
3599
  ),
3291
- React.createElement("span", { className: "kr2-name" }, row.name),
3600
+ React.createElement(
3601
+ "span",
3602
+ { className: "kr2-name-cell" },
3603
+ React.createElement("span", { className: "kr2-name" }, row.name),
3604
+ completion
3605
+ ? clear
3606
+ ? React.createElement("span", { className: "kr2-name-completion kr2-name-completion-clear" }, "(", String(completion.completed), "/", String(completion.target), ")clear!")
3607
+ : React.createElement(
3608
+ "span",
3609
+ { className: "kr2-name-completion kr2-name-completion-incomplete" },
3610
+ "完成数(",
3611
+ React.createElement("span", { className: "kr2-name-completion-done" + (completion.completed > 0 ? " kr2-name-completion-done-active" : "") }, String(completion.completed)),
3612
+ "/",
3613
+ String(completion.target),
3614
+ ")"
3615
+ )
3616
+ : null
3617
+ ),
3292
3618
  React.createElement("span", { className: "kr2-secretary" }, row.improveable === false ? "" : (secretaryText || "-")),
3293
3619
  React.createElement(
3294
3620
  "span",
@@ -3552,14 +3878,96 @@ function readPlanIncludeDev() {
3552
3878
  }
3553
3879
  }
3554
3880
 
3555
- function savePlanIncludeDev(value) {
3556
- try {
3557
- const storage = typeof localStorage !== "undefined" ? localStorage : (typeof window !== "undefined" ? window.localStorage : null)
3558
- if (storage) storage.setItem(PLAN_INCLUDE_DEV_STORAGE_KEY, value ? "1" : "0")
3559
- } catch (_) {}
3560
- }
3561
-
3562
- class KoushuRateApp extends React.Component {
3881
+ function savePlanIncludeDev(value) {
3882
+ try {
3883
+ const storage = typeof localStorage !== "undefined" ? localStorage : (typeof window !== "undefined" ? window.localStorage : null)
3884
+ if (storage) storage.setItem(PLAN_INCLUDE_DEV_STORAGE_KEY, value ? "1" : "0")
3885
+ } catch (_) {}
3886
+ }
3887
+
3888
+ function HelpModal({ onClose, updateNotes }) {
3889
+ const sections = []
3890
+ if (updateNotes && updateNotes.length) sections.push({ title: PLUGIN_VERSION + "更新", items: updateNotes })
3891
+ sections.push(
3892
+ {
3893
+ title: "1.改修列表",
3894
+ items: [
3895
+ "显示每日改修装备,打开插件时默认自动跳转到当前星期数;",
3896
+ "装备行上最右侧为改修推荐星级,数据参考nga梦美改修2.0贴(插件版头有网站链接),点击表头可以按照星级排序;",
3897
+ "点击对应装备行可展开详细改修数据,默认只展开6-10级的改修(也可手动点开查看),最右侧确保推荐是按照螺丝期望自动计算的,未考虑稀有素材和紫菜的消耗情况(请注意甄别);",
3898
+ "点击装备行最左侧的星星可以收藏该装备,点亮插件右上角的收藏按钮会隐藏其他未收藏的装备;",
3899
+ "点击确保推荐列中的“推荐确保”/“不确保”按钮会显示消耗素材情况以及库存素材数量(默认不统计带有星级的素材),点击素材会弹出装备开发配方列表,点击库存可查看已拥有的素材详情(主要是查看星级);",
3900
+ ],
3901
+ },
3902
+ {
3903
+ title: "2.素材计算",
3904
+ items: [
3905
+ "只有在改修列表中点击收藏的装备才会显示在该页中;",
3906
+ "为了让插件看起来更加简洁,改修目标只设置了+6、max和进化三个选项,输入目标数量后,插件会自动计算所需的素材汇总情况;",
3907
+ "勾选“统计素材开发的紫菜消耗期望”会计算当所有改修素材均来自开发时所预计消耗的紫菜数量(图一乐功能);",
3908
+ "可对开发资材、螺丝以及素材汇总中的需求、库存和紫菜开发期望进行排序;",
3909
+ ],
3910
+ },
3911
+ {
3912
+ title: "3.我变强了!",
3913
+ items: [
3914
+ "所有收藏的装备均会在该页中显示(无论在素材计算页中是否勾选),插件会自动计算目标装备与库存装备的星级、数量差异,并默认倒序排列;",
3915
+ "已完成的装备会自动置顶并显示clear!;",
3916
+ "计划的完成情况会同步显示在改修列表页的装备行上;",
3917
+ "注意在素材计算页选择“进化”的装备会和进化后的同名装备分开计算,用来锚定阶段性目标。",
3918
+ ],
3919
+ },
3920
+ {
3921
+ title: "其他常规功能",
3922
+ items: [
3923
+ "模糊搜索,搜索栏支持输入简体中文;",
3924
+ "可在插件版头按照装备大类进行筛选;",
3925
+ "插件也加入了不可改修装备,同样可以收藏,并正常显示在我变强了页中。",
3926
+ ],
3927
+ },
3928
+ )
3929
+ const updateSection = sections[0] && sections[0].title === PLUGIN_VERSION + "更新" ? sections[0] : null
3930
+ const guideSections = updateSection ? sections.slice(1) : sections
3931
+ const renderSection = (section, key) =>
3932
+ React.createElement(
3933
+ "section",
3934
+ { key, className: "kr2-help-section" },
3935
+ React.createElement("div", { className: "kr2-help-section-title" }, section.title),
3936
+ React.createElement(
3937
+ "ul",
3938
+ { className: "kr2-help-list" },
3939
+ section.items.map((text, itemIndex) => React.createElement("li", { key: itemIndex }, text))
3940
+ )
3941
+ )
3942
+ return React.createElement(
3943
+ "div",
3944
+ { className: "kr2-modal-backdrop", onClick: onClose },
3945
+ React.createElement(
3946
+ "div",
3947
+ { className: "kr2-modal kr2-help-modal", role: "dialog", "aria-modal": "true", "aria-label": "使用说明", onClick: (e) => e.stopPropagation() },
3948
+ React.createElement(
3949
+ "div",
3950
+ { className: "kr2-modal-head" },
3951
+ React.createElement(
3952
+ "div",
3953
+ null,
3954
+ React.createElement("div", { className: "kr2-modal-title" }, "使用说明"),
3955
+ React.createElement("div", { className: "kr2-modal-sub" }, "螺丝计算器 v" + PLUGIN_VERSION)
3956
+ ),
3957
+ React.createElement("button", { type: "button", className: "kr2-modal-close", onClick: onClose, title: "关闭", "aria-label": "关闭使用说明" }, "×")
3958
+ ),
3959
+ React.createElement(
3960
+ "div",
3961
+ { className: "kr2-help-body" },
3962
+ updateSection ? renderSection(updateSection, "update") : null,
3963
+ React.createElement("div", { className: "kr2-help-lead" }, "功能主页"),
3964
+ guideSections.map((section, index) => renderSection(section, index))
3965
+ )
3966
+ )
3967
+ )
3968
+ }
3969
+
3970
+ class KoushuRateApp extends React.Component {
3563
3971
  state = {
3564
3972
  query: "",
3565
3973
  day: japanWeekdayKey(),
@@ -3583,13 +3991,22 @@ class KoushuRateApp extends React.Component {
3583
3991
  starSort: readUiState().starSort || null,
3584
3992
  activeTab: "list",
3585
3993
  improveableOpen: true,
3586
- nonImproveableOpen: false,
3587
- planSelection: readPlanSelection(),
3588
- includeDevExpected: readPlanIncludeDev(),
3589
- }
3590
-
3591
- componentDidMount() {
3592
- this.refresh()
3994
+ nonImproveableOpen: false,
3995
+ planSelection: readPlanSelection(),
3996
+ includeDevExpected: readPlanIncludeDev(),
3997
+ helpOpen: false,
3998
+ }
3999
+
4000
+ componentDidMount() {
4001
+ const uiState = readUiState()
4002
+ const legacyVersion = uiState.helpVersion ? String(uiState.helpVersion).split(".").slice(0, 3).join(".") : ""
4003
+ const seenVersion = String(uiState.pluginVersion || legacyVersion || "").trim()
4004
+ const firstInstall = !seenVersion
4005
+ const isUpdate = !!seenVersion && seenVersion !== PLUGIN_VERSION
4006
+ if (firstInstall || isUpdate) {
4007
+ this.setState({ helpOpen: true })
4008
+ }
4009
+ this.refresh()
3593
4010
  this.checkAkashiUpdate(false)
3594
4011
  refreshKcDevData().catch(() => {})
3595
4012
  this.loadKcDevExpectations()
@@ -3597,11 +4014,20 @@ class KoushuRateApp extends React.Component {
3597
4014
  this.inventoryTimer = setInterval(() => this.refreshInventory(), 1000)
3598
4015
  }
3599
4016
 
3600
- componentWillUnmount() {
3601
- if (this.inventoryTimer) clearInterval(this.inventoryTimer)
3602
- }
3603
-
3604
- loadKcDevExpectations = () => {
4017
+ componentWillUnmount() {
4018
+ if (this.inventoryTimer) clearInterval(this.inventoryTimer)
4019
+ }
4020
+
4021
+ openHelp = () => {
4022
+ this.setState({ helpOpen: true })
4023
+ }
4024
+
4025
+ closeHelp = () => {
4026
+ saveUiState({ helpVersion: HELP_VERSION, pluginVersion: PLUGIN_VERSION })
4027
+ this.setState({ helpOpen: false })
4028
+ }
4029
+
4030
+ loadKcDevExpectations = () => {
3605
4031
  loadKcDevData(false)
3606
4032
  .then((data) => this.setState({ kcDevData: data }))
3607
4033
  .catch(() => {})
@@ -3847,8 +4273,9 @@ class KoushuRateApp extends React.Component {
3847
4273
  if (ib >= 0) return 1
3848
4274
  return a.localeCompare(b, "zh-Hans-CN")
3849
4275
  })
3850
- const kcDevPopup = this.state.kcDevPopup
3851
- const inventoryPopup = this.state.inventoryPopup
4276
+ const kcDevPopup = this.state.kcDevPopup
4277
+ const inventoryPopup = this.state.inventoryPopup
4278
+ const helpOpen = this.state.helpOpen
3852
4279
  const kcDevSort = (kcDevPopup && kcDevPopup.sort) || { key: "rate", dir: "desc" }
3853
4280
  const kcDevFormulas = kcDevPopup && Array.isArray(kcDevPopup.formulas) ? kcDevPopup.formulas.slice() : []
3854
4281
  if (kcDevSort.key === "rate" || kcDevSort.key === "failRate") {
@@ -3857,10 +4284,21 @@ class KoushuRateApp extends React.Component {
3857
4284
  return kcDevSort.dir === "asc" ? diff : -diff
3858
4285
  })
3859
4286
  }
3860
- const inventoryByEquip = this.state.inventoryByEquip || {}
3861
- const useItemCounts = this.state.useItemCounts || {}
3862
- const favoritesOnly = !!this.state.favoritesOnly
3863
- let rows = this.state.rows.filter((row) => {
4287
+ const inventoryByEquip = this.state.inventoryByEquip || {}
4288
+ const useItemCounts = this.state.useItemCounts || {}
4289
+ const favoritesOnly = !!this.state.favoritesOnly
4290
+ const planSelection = this.state.planSelection || {}
4291
+ const favoriteRows = this.state.rows.filter((row) => !!this.state.favorites[row.id])
4292
+ const strongSummary = {}
4293
+ for (const item of buildStrongEntries(favoriteRows, inventoryByEquip, planSelection).entries) {
4294
+ const rowIds = item.sourceRowIds && item.sourceRowIds.length ? item.sourceRowIds : [item.row.id]
4295
+ for (const rowId of rowIds) {
4296
+ const summary = strongSummary[rowId] || (strongSummary[rowId] = { completed: 0, target: 0 })
4297
+ summary.completed += item.completed || 0
4298
+ summary.target += item.qty
4299
+ }
4300
+ }
4301
+ let rows = this.state.rows.filter((row) => {
3864
4302
  if (favoritesOnly && !this.state.favorites[row.id]) return false
3865
4303
  if (row.improveable !== false && day !== ALL_DAYS_KEY && (row.days || []).indexOf(day) < 0) return false
3866
4304
  if (category !== "all" && row.category !== category) return false
@@ -3899,9 +4337,10 @@ class KoushuRateApp extends React.Component {
3899
4337
  onInventoryClick: this.openInventoryPopup,
3900
4338
  isFavorite: !!this.state.favorites[row.id],
3901
4339
  onFavoriteClick: this.toggleFavorite,
3902
- secretaryText: secretaryText(row.secretaries, day),
3903
- kcDevData: this.state.kcDevData,
3904
- })
4340
+ secretaryText: secretaryText(row.secretaries, day),
4341
+ kcDevData: this.state.kcDevData,
4342
+ completion: strongSummary[row.id],
4343
+ })
3905
4344
 
3906
4345
  return React.createElement(
3907
4346
  "div",
@@ -3914,11 +4353,16 @@ class KoushuRateApp extends React.Component {
3914
4353
  React.createElement(
3915
4354
  "div",
3916
4355
  { className: "kr2-title-row" },
3917
- React.createElement(
3918
- "div",
3919
- null,
3920
- React.createElement("div", { className: "kr2-title" }, "螺丝计算器"),
3921
- React.createElement(
4356
+ React.createElement(
4357
+ "div",
4358
+ null,
4359
+ React.createElement(
4360
+ "div",
4361
+ { className: "kr2-title-line" },
4362
+ React.createElement("div", { className: "kr2-title" }, "螺丝计算器"),
4363
+ React.createElement("button", { type: "button", className: "kr2-help-btn", onClick: this.openHelp }, "使用说明")
4364
+ ),
4365
+ React.createElement(
3922
4366
  "div",
3923
4367
  { className: "kr2-title-note" },
3924
4368
  "螺丝确保仅供参考,稀有素材消耗请酌情考虑~(∠・ω< )⌒★"
@@ -4084,10 +4528,10 @@ class KoushuRateApp extends React.Component {
4084
4528
  ),
4085
4529
  this.state.activeTab === "plan"
4086
4530
  ? React.createElement(PlanningPage, {
4087
- rows: this.state.rows.filter((row) => !!this.state.favorites[row.id]),
4088
- inventoryByEquip,
4089
- useItemCounts,
4090
- selection: this.state.planSelection || {},
4531
+ rows: this.state.rows.filter((row) => !!this.state.favorites[row.id]),
4532
+ inventoryByEquip,
4533
+ useItemCounts,
4534
+ selection: planSelection,
4091
4535
  onToggle: this.togglePlanRow,
4092
4536
  onTarget: this.setPlanTarget,
4093
4537
  onEvo: this.setPlanEvo,
@@ -4099,14 +4543,17 @@ class KoushuRateApp extends React.Component {
4099
4543
  onToggleIncludeDevExpected: this.toggleIncludeDevExpected,
4100
4544
  })
4101
4545
  : null,
4102
- this.state.activeTab === "strong"
4103
- ? React.createElement(StrongPage, {
4104
- rows: this.state.rows.filter((row) => !!this.state.favorites[row.id]),
4105
- inventoryByEquip,
4106
- selection: this.state.planSelection || {},
4107
- })
4108
- : null,
4109
- kcDevPopup
4546
+ this.state.activeTab === "strong"
4547
+ ? React.createElement(StrongPage, {
4548
+ rows: this.state.rows.filter((row) => !!this.state.favorites[row.id]),
4549
+ inventoryByEquip,
4550
+ selection: planSelection,
4551
+ })
4552
+ : null,
4553
+ helpOpen
4554
+ ? React.createElement(HelpModal, { onClose: this.closeHelp, updateNotes: CURRENT_HELP_UPDATE_NOTES })
4555
+ : null,
4556
+ kcDevPopup
4110
4557
  ? React.createElement(
4111
4558
  "div",
4112
4559
  { className: "kr2-modal-backdrop", onClick: this.closeKcDevPopup },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "poi-plugin-koushu-rate",
3
- "version": "2.0.17",
3
+ "version": "2.0.19",
4
4
  "description": "全装备改修资材计算与推荐",
5
5
  "main": "index.js",
6
6
  "files": [