mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-06 21:24:10 +00:00
Step 5 of the planned vue->react migration. Settings is the first
entry whose state model didn't translate to the Vue-style "parent
passes a reactive object, children mutate it in place" pattern, so
the React port flips it to lifted state + a typed updateSetting
patch function.
* models/setting.ts — typed AllSetting class with the same field
defaults and equals() behavior the vue version had. The .js
twin is deleted; nothing else imported it.
* hooks/useAllSetting.ts — owns allSetting + oldAllSetting state,
exposes updateSetting(patch), saveDisabled is derived via useMemo
off equals() (no more 1Hz dirty-check timer).
* components/SettingListItem.tsx — children-based wrapper instead
of named slots. The vue twin stays alive because xray (BasicsTab,
DnsTab) still imports it; deleted when xray migrates.
The five tab components and the TwoFactorModal each accept
{ allSetting, updateSetting } and render with AntD v5's Collapse
items[] API. Every v-model:value="x" became
value={...} onChange={(e) => updateSetting({ key: e.target.value })}
or onChange={(v) => updateSetting({ key: v })} for non-input
controls.
SubscriptionFormatsTab is the trickiest — fragment / noises[] /
mux / direct routing rules are stored as JSON-encoded strings on
the wire. Parsing them once via useMemo per field, mutating the
parsed object on edit, and stringifying back into the patch keeps
the round-trip identical to the vue version.
SettingsPage hosts the tab navigation (with hash sync), the
save / restart action bar, the security-warnings alert banner,
and the restart flow that rebuilds the panel URL after the new
host/port/cert settings take effect.
84 lines
1.3 KiB
CSS
84 lines
1.3 KiB
CSS
.api-token-section {
|
|
padding: 8px 20px 16px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 12px;
|
|
}
|
|
|
|
.api-token-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 12px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.api-token-hint {
|
|
margin: 0;
|
|
font-size: 12.5px;
|
|
opacity: 0.7;
|
|
flex: 1;
|
|
min-width: 200px;
|
|
}
|
|
|
|
.api-token-row {
|
|
border: 1px solid rgba(128, 128, 128, 0.18);
|
|
border-radius: 8px;
|
|
padding: 10px 12px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
transition: opacity 0.15s;
|
|
}
|
|
|
|
.api-token-row.disabled {
|
|
opacity: 0.55;
|
|
}
|
|
|
|
.api-token-row-head {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 8px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.api-token-name-wrap {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 2px;
|
|
}
|
|
|
|
.api-token-name {
|
|
font-weight: 600;
|
|
font-size: 13.5px;
|
|
}
|
|
|
|
.api-token-created {
|
|
font-size: 11px;
|
|
opacity: 0.55;
|
|
}
|
|
|
|
.api-token-actions {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
.api-token-value-wrap {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.api-token-value {
|
|
flex: 1;
|
|
min-width: 0;
|
|
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
font-size: 12.5px;
|
|
padding: 4px 8px;
|
|
background: rgba(128, 128, 128, 0.08);
|
|
border-radius: 4px;
|
|
word-break: break-all;
|
|
}
|