From edf0f36940d7da64f63913a7ed456c908c9e2095 Mon Sep 17 00:00:00 2001 From: Sanaei Date: Sat, 23 May 2026 15:21:45 +0200 Subject: [PATCH] Frontend rewrite: React + TypeScript with AntD v6 (#4498) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore(frontend): add react+typescript toolchain alongside vue Step 0 of the planned vue->react migration. React 19, antd 5, i18next + react-i18next, typescript 5, and @vitejs/plugin-react 6 are added as dev/runtime deps alongside the existing vue stack. Both frameworks coexist in the build until the last entry flips. * vite.config.js: react() plugin runs next to vue(); new manualChunks for vendor-react / vendor-antd-react / vendor-icons-react / vendor-i18next. Existing vue chunks unchanged. * eslint.config.js: typescript-eslint + eslint-plugin-react-hooks rules scoped to *.{ts,tsx}; vue config untouched for *.{js,vue}. * tsconfig.json: strict, jsx: react-jsx, moduleResolution: bundler, allowJs: true (lets .tsx files import the remaining .js modules during incremental migration), @/* path alias. * env.d.ts: Vite client types + window.X_UI_BASE_PATH typing + SubPageData shape consumed by the subscription page. Vite stays pinned at 8.0.13 per the existing project policy. No existing .vue/.js source files touched in this step. eslint-plugin-react (not -hooks) is not included because its latest release does not yet support ESLint 10. react-hooks/purity covers the safety-critical case; revisit when the plugin updates. * refactor(frontend): port subpage to react+ts Step 1 of the planned vue->react migration. The standalone subscription page (sub/sub.go renders the HTML host; React mounts into #app) is the first entry off vue. Introduces two shared pieces both entries (and future ones) will use: * src/hooks/useTheme.tsx — React Context + useTheme hook + the same buildAntdThemeConfig (dark/ultra-dark token overrides) and pauseAnimationsUntilLeave helper the vue version exposes. Same localStorage keys (dark-mode, isUltraDarkThemeEnabled) and DOM side effects (body.className, html[data-theme]) so the two stay in sync across the coexistence period. * src/i18n/react.ts — i18next + react-i18next loader that reads the same web/translation/*.json files via import.meta.glob. The vue-i18n setup in src/i18n/index.js is untouched and still serves the remaining vue entries. SubPage.tsx mirrors the vue version's behavior: reads window.__SUB_PAGE_DATA__ injected by the Go sub server, renders QR codes / descriptions / Android+iOS deep-link dropdowns, supports theme cycle and language switch. Uses AntD v5 idioms: Descriptions items prop, Dropdown menu prop, Layout.Content. * refactor(frontend): port login to react+ts Step 2 of the planned vue->react migration. The login entry is the first to exercise AntD React's Form API (Form + Form.Item with name/rules + onFinish) and the existing axios/CSRF interceptors under React. * LoginPage.tsx: same form fields, conditional 2FA input, rotating headline ("Hello" / "Welcome to..."), drifting blob background, theme cycle + language popover. Headline transition switches from vue's to a CSS keyframe animation keyed off the visible word. * entries/login.tsx: setupAxios() + applyDocumentTitle() unchanged from the vue entry — both are framework-agnostic in src/utils and src/api/axios-init.js. useTheme hook, ThemeProvider, and i18n/react.ts loader introduced in step 1 are now shared across two entries; Vite extracts them as a small chunk in the build output. * refactor(frontend): port api-docs to react+ts Step 3 of the planned vue->react migration. The five api-docs files (ApiDocsPage, CodeBlock, EndpointRow, EndpointSection, plus the data-only endpoints.js) all move to react+ts. Also introduces components/AppSidebar.tsx — api-docs is the first authenticated page to need it. AppSidebar.vue stays in place for the six remaining vue entries (settings, inbounds, clients, xray, nodes, index); each gets switched to AppSidebar.tsx as its entry migrates. After the last entry flips, AppSidebar.vue is deleted. Notable transformations: * The scroll observer that highlights the active TOC link is a useEffect keyed on sections — re-registers whenever the visible set changes (search filter narrows it). Same behaviour as the vue watchEffect. * v-html="safeInlineHtml(...)" becomes dangerouslySetInnerHTML={{ __html: safeInlineHtml(...) }}. The helper still escapes everything except tags. * JSON syntax highlighter in CodeBlock is unchanged — pure regex on the escaped string, then rendered via dangerouslySetInnerHTML. * endpoints.js stays as JS (allowJs in tsconfig); only the consumer signatures (Endpoint, Section) are typed at the React boundary. * AppSidebar reuses pauseAnimationsUntilLeave + useTheme from step 1. Drawer + Sider keyed off the same localStorage flag (isSidebarCollapsed) and DOM theme attributes the vue version uses, so the two stay in sync during coexistence. * refactor(frontend): port nodes to react+ts Step 4 of the planned vue->react migration. The nodes entry brings in the largest shared-infrastructure batch so far — every authenticated react page from here on can lean on these. New shared pieces (live alongside their .vue counterparts during coexistence): * hooks/useMediaQuery.ts — useState + resize listener * hooks/useWebSocket.ts — wraps WebSocketClient, subscribes on mount and unsubscribes on unmount. The underlying client is a single module-level instance so multiple components on the same page share one socket. * hooks/useNodes.ts — node list state + CRUD + probe/test, including the totals memo (online/offline/avgLatency) used by the summary card. applyNodesEvent is the entry point for the heartbeat-pushed list. * components/CustomStatistic.tsx — thin Statistic wrapper, prefix + suffix slots become props. * components/Sparkline.tsx — the SVG line chart with measured-width axis scaling, gradient fill, tooltip overlay, and per-instance gradient id from React.useId. ResizeObserver lifecycle is in useEffect; the math is unchanged. Pages: * NodesPage — wires hooks + WebSocket together, renders summary card + NodeList, hosts the form modal. Uses Modal.useModal() for the delete confirm so the dialog inherits ConfigProvider theming. * NodeList — desktop renders a Table with expandable history rows; mobile flips to a vertical card list whose actions live in a bottom-right Dropdown. The IP-blur eye toggle persists across both. * NodeFormModal — controlled form (useState object, single setForm per change). The reset-on-open effect computes the next state once and applies it with eslint-disable to satisfy the new react-hooks/set-state-in-effect rule on a legitimate pattern. * NodeHistoryPanel — polls /panel/api/nodes/history/{id}/{metric}/ {bucket} every 15s, renders cpu+mem sparklines side-by-side. * refactor(frontend): port settings to react+ts 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. * refactor(frontend): port clients to react+ts Step 6 of the planned vue->react migration. Clients is the biggest data-CRUD page in the panel (1.1k-line ClientsPage, 4 modals, full table + mobile card list, WebSocket-driven realtime traffic + online updates). New shared infra (lives alongside vue twins until inbounds migrates): * hooks/useClients.ts — clients + inbounds list, CRUD + bulk delete + attach/detach + traffic reset, with WebSocket event handlers (traffic, client_stats, invalidate) and a small debounced refresh on the invalidate event. State managed via setState; the live client_stats event merges traffic snapshots row-by-row through a ref to avoid stale closure issues. * hooks/useDatepicker.ts — singleton "gregorian"/"jalalian" cache with subscribe/notify so multiple components can read the panel's Calendar Type without re-fetching. Mirrors useDatepicker.js. * components/DateTimePicker.tsx — AntD DatePicker wrapper. vue3-persian-datetime-picker has no React port; the Jalali UI calendar is deferred (read-only Jalali display via IntlUtil formatDate still works). The vue twin stays for inbounds. * pages/inbounds/QrPanel.tsx — copy/download/copy-as-png QR helper shared between clients (qr modal) and inbounds (still on vue). Vue twin stays alive at QrPanel.vue. * models/inbound.ts — slim port: only the TLS_FLOW_CONTROL constant the clients form needs. The full inbound model stays as inbound.js for now; inbounds will pull it in as inbound.ts. The clients page itself uses Modal.useModal() for all confirm dialogs (delete, bulk-delete, reset-traffic, delDepleted, reset-all) so the dialogs render themed. Filter state persists to localStorage under clientsFilterState. Sort + pagination state is local; pageSize seeds from /panel/setting/defaultSettings. The four modals share a controlled "open/onOpenChange" pattern that replaces vue's v-model:open. ClientFormModal computes attach/detach diffs from the inbound multi-select on submit; the parent's onSave callback routes them through useClients's attach()/ detach() after the main update succeeds. ESLint config: turned off four react-hooks v7 rules (react-compiler, preserve-manual-memoization, set-state-in-effect, purity). They're all React-Compiler-driven informational rules; we don't run the compiler and the patterns they flag (initial-fetch useEffect, derived computations using Date.now, inline arrow event handlers) are all idiomatic React. Disabling globally instead of per-line keeps the diff readable. * refactor(frontend): port index dashboard to react+ts Step 7 of the Vue→React migration. Ports the overview/index entry: dashboard page, status + xray cards, panel-update / log / backup / system-history / xray-metrics / xray-log / version modals, and the custom-geo subsection. Adds the shared JsonEditor (CodeMirror 6) and useStatus hook used by the config modal. Removes the unused react-hooks/set-state-in-effect disables now that the rule is off globally. * refactor(frontend): port xray to react+ts Step 8 of the Vue→React migration. Ports the xray config entry: page shell, basics/routing/outbounds/balancers/dns tabs, the rule + balancer + dns server + dns presets + warp + nord modals, the protocol-aware outbound form, and the shared FinalMaskForm (TCP/UDP masks + QUIC params). Adds useXraySetting that mirrors the legacy two-way sync between the JSON template string and the parsed templateSettings tree. The outbound model itself stays in JS so the class-driven form keeps its existing mutation API; instance access is typed loosely inside the form to match. The shared FinalMaskForm.vue and JsonEditor.vue stay alongside the new .tsx versions until step 9 — InboundFormModal.vue still imports them. Adds react-hooks/immutability and react-hooks/refs to the already-disabled react-compiler rule set; both flag the outbound form's instance-mutation pattern that doesn't run through useState. * Upgrade frontend deps (antd v6, i18n, TS) Bump frontend dependencies in package.json and regenerate package-lock.json. Notable updates: upgrade antd to v6, update i18next/react-i18next, axios, qs, vue-i18n, TypeScript and ESLint, plus related @rc-component packages and replacements (e.g. classnames/rc-util -> clsx/@rc-component/util). Lockfile changes reflect the new dependency tree required for Ant Design v6 and other package upgrades. * refactor(frontend): port inbounds to react+ts and drop vue toolchain Step 9 — the last entry. Ports the inbounds entry: page shell, list with desktop table + mobile cards, info modal, qr-code modal, share-link helpers, and the protocol-aware form modal (basics / protocol / stream / security / sniffing / advanced JSON). useInbounds replaces the Vue composable with WebSocket-driven traffic + client-stats merge. Inbound and DBInbound models stay in JS so the class-driven form keeps its mutation API; instance access is typed loosely inside the form to match. FinalMaskForm/JsonEditor/TextModal/PromptModal/InfinityIcon are the last shared bits to flip; their .vue counterparts go too. Toolchain cleanup now that no entry needs Vue: drop plugin-vue from vite.config, remove the .vue lint block + parser, prune vue / vue-i18n / ant-design-vue / @ant-design/icons-vue / vue3-persian-datetime-picker / moment-jalaali override from package.json, and switch utils/index.js to import { message } from 'antd' instead of ant-design-vue. * chore(frontend): adopt antd v6 api updates Sweep deprecated props across the React tree: - Modal: destroyOnClose -> destroyOnHidden, maskClosable -> mask.closable - Space: direction -> orientation (or removed when redundant) - Input.Group compact -> Space.Compact block - Drawer: width -> size - Spin: tip -> description - Progress: trailColor -> railColor - Alert: message -> title - Popover: overlayClassName -> rootClassName - BackTop -> FloatButton.BackTop Also refresh dashboard theming for v6: rename dark/ultra Layout and Menu tokens (siderBg, darkItemBg, darkSubMenuItemBg, darkPopupBg), tweak gauge size/stroke, add font-size overrides for Statistic and Progress so the overview numbers stay legible under v6 defaults. * chore(frontend): antd v6 polish, theme + modal fixes - adopt message.useMessage hook + messageBus bridge so HttpUtil messages inherit ConfigProvider theme tokens - replace deprecated antd APIs (List, Input addonBefore/After, Empty imageStyle); introduce InputAddon helper + SettingListItem custom rows - fix dark/ultra selectors in portaled modals (body.dark, html[data-theme='ultra-dark']) instead of nonexistent .is-dark/.is-ultra - add horizontal scroll to clients table; reorder node columns so actions+enable sit at the left - swap raw button for antd Button in NodeFormModal test connection - fix FinalMaskForm nested-form by hoisting it outside OutboundFormModal's parent Form - fix advanced "all" JSON tab in InboundFormModal — useMemo on a mutated ref was stale; compute on every render - fix chart-on-open for SystemHistory + XrayMetrics modals by adding open to effect deps (useRef.current doesn't trigger re-runs) - switch i18next interpolation to single-brace {var} to match locale files - drop residual Vue mentions in CI workflows and Go comments * fix(frontend): qr code collapse — open only first panel, allow toggle ClientQrModal and QrCodeModal both used activeKey without onChange, forcing every panel open and blocking user toggle. Switch to controlled state initialized to the first item's key on open, with onChange so clicks update state. Also remove unused AppBridge.tsx (superseded by per-page message.useMessage hook). * fix(frontend): hover cards, balancer load, routing dnd, modal a11y, outbound crash - ClientsPage/SettingsPage/XrayPage: add hoverable to bottom card/tabs so hover affordance matches the top card - BalancerFormModal: lazy-init useState from props + destroyOnHidden so the form mounts with saved values instead of relying on a useEffect sync that could miss the first open - RoutingTab: rewrite pointer drag — handlers are now defined inside the pointerdown closure so addEventListener/removeEventListener match; drag state lives on a ref (from/to/moved) so onUp reads the real indices, not stale closure values. Adds setPointerCapture so Windows and touch keep delivering events when the cursor leaves the handle. - OutboundFormModal/InboundFormModal: blur the focused input before switching tabs to silence the aria-hidden-on-focused-element warning - utils.isArrEmpty: return true for undefined/null arrays — the old form treated undefined as "not empty" which crashed VLESSSettings.fromJson when json.vnext was missing * fix(frontend): clipboard reliability + restyle login page - ClipboardManager.copyText: prefer navigator.clipboard on secure contexts, fall back to a focused on-screen textarea + execCommand. Old path used left:-9999px which failed selection in some browsers and swallowed execCommand's return value, so the "copied" toast appeared even when nothing made it to the clipboard. - LoginPage: richer gradient backdrop — five animated colour blobs, glassmorphic card (backdrop-filter blur + saturate), gradient brand text/accent, masked grid texture for depth, and a thin gradient border on the card. Light/dark/ultra each get their own palette. * Memoize compactAdvancedJson and update deps Wrap compactAdvancedJson in useCallback (dependent on messageApi) and add it to the dependency array of applyAdvancedJsonToBasic. This ensures a stable function reference for correct dependency tracking and avoids stale closures/unnecessary re-renders in InboundFormModal.tsx. * style(frontend): prettier charts, drop redundant frame, format net rates - Sparkline: multi-stop gradient fill, soft drop-shadow under the line, dashed grid, glowing pulse on the latest-point marker, pill-shaped tooltip with dashed crosshair - XrayMetricsModal: glow + pulse on the observatory alive dot, monospace stamps/listen text - SystemHistoryModal: keep just the modal's frame around the chart (the inner wrapper I'd added stacked a second border on top); strip the decimal from Net Up/Down (25.63 KB/s → 25 KB/s) only on this chart's formatter * style(frontend): refined dark/ultra palette + shared pro card frame - Dark tokens shifted to a cooler, Linear-style palette: page #1a1b1f, sidebar/header #15161a (recessed nav, darker than cards), card #23252b, elevated #2d2f37 - Ultra dark: page pure #000 for OLED, sidebar #050507 disappears into the frame, card #101013 with a clear step, elevated #1a1a1e - New styles/page-cards.css holds the card border/shadow/hover rules so all seven content pages (index, clients, inbounds, xray, settings, nodes, api-docs) share one definition instead of duplicating in each page CSS - Dashboard typography: uppercase card titles with letter-spacing, larger 17px stat values, subtle gradient divider between stat columns, ellipsis on action labels so "Backup & Restore" doesn't break the card height at mid widths - Light --bg-page stays at #e6e8ec for the contrast against white cards * fix(frontend): wireguard info alignment, blue login dark, embed gitkeep - align WireGuard info-modal fields with Protocol/Address/Port by wrapping values in Tag (matches the rest of the dl.info-list rows) - swap login dark palette from purple to pure blue blobs/accent/brand - pin web/dist/.gitkeep through gitignore so //go:embed all:dist never fails on a fresh clone with an empty dist directory * docs: refresh frontend docs for the React + TS + AntD 6 stack Update CONTRIBUTING.md and frontend/README.md to describe the migrated frontend accurately: - replace Vue 3 / Ant Design Vue 4 references with React 19 / AntD 6 / TS - swap composables -> hooks, vue-i18n -> react-i18next, createApp -> createRoot - mention the typecheck step (tsc --noEmit) in the PR checklist - document the Vite 8.0.13 pin and TypeScript strict mode in conventions - list the nodes and api-docs entries that were missing from the layout * style(frontend): improve readability and mobile polish - bump statistic title/value contrast in dark and ultra-dark so totals on the inbounds summary card stay legible - give index card actions explicit colors per theme so links like Stop, Logs, System History no longer fade into the card background - show the panel version as a tag next to "3X-UI" on mobile, mirroring the Xray version tag pattern, and turn it orange when an update is available - make the login settings button a proper circle by adding size="large" + an explicit border-radius fallback on .toolbar-btn * feat: jalali calendar support and date formatting fixes - Wire useDatepicker into IntlUtil and switch jalalian display locale to fa-IR for clean "1405/07/03 12:00:00" output (drops the awkward "AP" era suffix that "-u-ca-persian" produced) - Drop in persian-calendar-suite for the jalali date picker, with a light/dark/ultra theme map and CSS overrides so the inline-styled input stays readable and bg matches the surrounding container - Force LTR on the picker input so "1405/03/07 00:00" reads naturally - Pass calendar setting through ClientInfoModal, ClientsPage Duration tooltip, and ClientFormModal's expiry picker - Heuristic toMs() in ClientInfoModal so GORM's autoUpdateTime seconds render as a real date instead of "1348/11/01" - Persist UpdatedAt on the ClientRecord row in client_service.Update; previously only the inbound settings JSON was bumped, so the panel never saw a fresh updated_at after editing a client * feat(frontend): donate link, panel version label, login lang menu - Sidebar: add heart donate link to https://donate.sanaei.dev and small panel version under 3X-UI brand - Login: swap settings-cog for translation icon, drop title, render languages as a direct list - Vite dev: inject window.X_UI_CUR_VER from config/version so dev mode matches prod - Translations: add menu.donate across all locales * fix(xray-update): respect XUI_BIN_FOLDER on Windows The Windows update path hardcoded "bin/xray-windows-amd64.exe", ignoring the configured XUI_BIN_FOLDER. In dev mode (folder set to x-ui) this created a stray bin/ folder while the running binary stayed un-updated. * Bump Xray to v26.5.9 and minor cleanup Update Xray release URLs to v26.5.9 in the GitHub Actions workflow and DockerInit.sh. Remove the hardcoded skip for tagVersion "26.5.3" so it will be considered when collecting Xray versions. Apply small formatting fixes: remove an extra blank line in database/db.go, normalize spacing/alignment of Protocol constants in database/model/model.go, and trim a trailing blank line in web/controller/inbound.go. * fix(frontend): route remaining copy buttons through ClipboardManager Direct navigator.clipboard calls fail in non-secure contexts (HTTP on a LAN IP), making the API-docs code copy and security-tab token copy silently broken. Both now go through ClipboardManager which falls back to document.execCommand('copy') when navigator.clipboard is unavailable. * fix(db): store CreatedAt/UpdatedAt in milliseconds GORM's autoCreateTime/autoUpdateTime tags default to Unix seconds on int64 fields and overwrite the service-supplied UnixMilli value on save. The frontend interprets these timestamps as JS Date inputs (milliseconds), so created/updated columns rendered ~1970 dates. Adding the :milli qualifier makes GORM match what the service code and UI expect. * Improve legacy clipboard copy handling Refactor ClipboardManager._legacyCopy to better handle focus and selection when copying. The textarea is now appended to the active element's parent (or body) and placed off-screen with aria-hidden and readonly attributes. The code preserves and restores the previous document selection and active element, uses focus({preventScroll: true}) to avoid scrolling, and returns the execCommand('copy') result. This makes legacy copy behavior more robust and less disruptive to the page state. * fix(lint): drop redundant ok=false in clipboard fallback catch * chore(deps): bump golang.org/x/net to v0.55.0 for GO-2026-5026 --- .github/workflows/ci.yml | 2 - .github/workflows/codeql.yml | 2 - .github/workflows/release.yml | 4 +- .gitignore | 3 + CONTRIBUTING.md | 130 +- DockerInit.sh | 2 +- database/db.go | 1 - database/model/model.go | 36 +- frontend/README.md | 36 +- frontend/api-docs.html | 2 +- frontend/clients.html | 2 +- frontend/eslint.config.js | 75 +- frontend/inbounds.html | 2 +- frontend/index.html | 2 +- frontend/login.html | 2 +- frontend/nodes.html | 2 +- frontend/package-lock.json | 2365 +++++++++++++---- frontend/package.json | 41 +- frontend/settings.html | 2 +- frontend/src/components/AppSidebar.css | 287 ++ frontend/src/components/AppSidebar.tsx | 290 ++ frontend/src/components/AppSidebar.vue | 432 --- frontend/src/components/CustomStatistic.css | 52 + frontend/src/components/CustomStatistic.tsx | 14 + frontend/src/components/CustomStatistic.vue | 31 - frontend/src/components/DateTimePicker.css | 35 + frontend/src/components/DateTimePicker.tsx | 98 + frontend/src/components/DateTimePicker.vue | 366 --- frontend/src/components/FinalMaskForm.tsx | 738 +++++ frontend/src/components/FinalMaskForm.vue | 510 ---- frontend/src/components/InfinityIcon.tsx | 19 + frontend/src/components/InfinityIcon.vue | 18 - frontend/src/components/InputAddon.css | 40 + frontend/src/components/InputAddon.tsx | 21 + frontend/src/components/JsonEditor.css | 26 + frontend/src/components/JsonEditor.tsx | 179 ++ frontend/src/components/JsonEditor.vue | 185 -- frontend/src/components/PromptModal.tsx | 82 + frontend/src/components/PromptModal.vue | 52 - frontend/src/components/SettingListItem.css | 43 + frontend/src/components/SettingListItem.tsx | 36 + frontend/src/components/SettingListItem.vue | 35 - frontend/src/components/Sparkline.css | 44 + frontend/src/components/Sparkline.tsx | 368 +++ frontend/src/components/Sparkline.vue | 297 --- frontend/src/components/TableSortable.vue | 311 --- frontend/src/components/TextModal.tsx | 59 + frontend/src/components/TextModal.vue | 66 - frontend/src/composables/useDatepicker.js | 45 - frontend/src/composables/useMediaQuery.js | 26 - frontend/src/composables/useNodeList.js | 44 - frontend/src/composables/useStatus.js | 43 - frontend/src/composables/useTheme.js | 128 - frontend/src/composables/useWebSocket.js | 48 - frontend/src/entries/api-docs.js | 21 - frontend/src/entries/api-docs.tsx | 28 + frontend/src/entries/clients.js | 21 - frontend/src/entries/clients.tsx | 28 + frontend/src/entries/inbounds.js | 21 - frontend/src/entries/inbounds.tsx | 28 + frontend/src/entries/index.js | 23 - frontend/src/entries/index.tsx | 28 + frontend/src/entries/login.js | 23 - frontend/src/entries/login.tsx | 28 + frontend/src/entries/nodes.js | 21 - frontend/src/entries/nodes.tsx | 28 + frontend/src/entries/settings.js | 23 - frontend/src/entries/settings.tsx | 28 + frontend/src/entries/subpage.js | 20 - frontend/src/entries/subpage.tsx | 23 + frontend/src/entries/xray.js | 21 - frontend/src/entries/xray.tsx | 28 + frontend/src/env.d.ts | 65 + frontend/src/hooks/useAllSetting.ts | 69 + frontend/src/hooks/useClients.ts | 282 ++ frontend/src/hooks/useDatepicker.ts | 57 + frontend/src/hooks/useMediaQuery.ts | 15 + frontend/src/hooks/useNodes.ts | 177 ++ frontend/src/hooks/useStatus.ts | 35 + frontend/src/hooks/useTheme.tsx | 136 + frontend/src/hooks/useWebSocket.ts | 32 + frontend/src/hooks/useXraySetting.ts | 370 +++ frontend/src/i18n/index.js | 54 - frontend/src/i18n/react.ts | 43 + frontend/src/models/setting.js | 108 - frontend/src/models/setting.ts | 100 + frontend/src/models/status.js | 71 - frontend/src/models/status.ts | 120 + frontend/src/pages/api-docs/ApiDocsPage.css | 292 ++ frontend/src/pages/api-docs/ApiDocsPage.tsx | 247 ++ frontend/src/pages/api-docs/ApiDocsPage.vue | 561 ---- .../api-docs/{CodeBlock.vue => CodeBlock.css} | 67 - frontend/src/pages/api-docs/CodeBlock.tsx | 69 + frontend/src/pages/api-docs/EndpointRow.css | 93 + frontend/src/pages/api-docs/EndpointRow.tsx | 84 + frontend/src/pages/api-docs/EndpointRow.vue | 172 -- ...ndpointSection.vue => EndpointSection.css} | 67 +- .../src/pages/api-docs/EndpointSection.tsx | 90 + .../src/pages/clients/ClientBulkAddModal.css | 5 + .../src/pages/clients/ClientBulkAddModal.tsx | 341 +++ .../src/pages/clients/ClientBulkAddModal.vue | 267 -- .../src/pages/clients/ClientFormModal.css | 1 + .../src/pages/clients/ClientFormModal.tsx | 524 ++++ .../src/pages/clients/ClientFormModal.vue | 402 --- .../src/pages/clients/ClientInfoModal.css | 98 + .../src/pages/clients/ClientInfoModal.tsx | 292 ++ .../src/pages/clients/ClientInfoModal.vue | 411 --- frontend/src/pages/clients/ClientQrModal.tsx | 136 + frontend/src/pages/clients/ClientQrModal.vue | 97 - frontend/src/pages/clients/ClientsPage.css | 221 ++ frontend/src/pages/clients/ClientsPage.tsx | 908 +++++++ frontend/src/pages/clients/ClientsPage.vue | 1098 -------- frontend/src/pages/clients/useClients.js | 220 -- .../src/pages/inbounds/InboundFormModal.css | 87 + .../src/pages/inbounds/InboundFormModal.tsx | 2144 +++++++++++++++ .../src/pages/inbounds/InboundFormModal.vue | 2255 ---------------- .../src/pages/inbounds/InboundInfoModal.css | 215 ++ .../src/pages/inbounds/InboundInfoModal.tsx | 917 +++++++ .../src/pages/inbounds/InboundInfoModal.vue | 1134 -------- frontend/src/pages/inbounds/InboundList.css | 163 ++ frontend/src/pages/inbounds/InboundList.tsx | 647 +++++ frontend/src/pages/inbounds/InboundList.vue | 680 ----- frontend/src/pages/inbounds/InboundsPage.css | 50 + frontend/src/pages/inbounds/InboundsPage.tsx | 556 ++++ frontend/src/pages/inbounds/InboundsPage.vue | 559 ---- frontend/src/pages/inbounds/QrCodeModal.tsx | 150 ++ frontend/src/pages/inbounds/QrCodeModal.vue | 126 - frontend/src/pages/inbounds/QrPanel.css | 40 + frontend/src/pages/inbounds/QrPanel.tsx | 130 + frontend/src/pages/inbounds/QrPanel.vue | 157 -- frontend/src/pages/inbounds/useInbounds.js | 329 --- frontend/src/pages/inbounds/useInbounds.ts | 349 +++ frontend/src/pages/index/BackupModal.css | 57 + frontend/src/pages/index/BackupModal.tsx | 87 + frontend/src/pages/index/BackupModal.vue | 101 - .../src/pages/index/CustomGeoFormModal.tsx | 132 + .../src/pages/index/CustomGeoFormModal.vue | 106 - frontend/src/pages/index/CustomGeoSection.css | 81 + frontend/src/pages/index/CustomGeoSection.tsx | 283 ++ frontend/src/pages/index/CustomGeoSection.vue | 311 --- frontend/src/pages/index/IndexPage.css | 242 ++ frontend/src/pages/index/IndexPage.tsx | 502 ++++ frontend/src/pages/index/IndexPage.vue | 484 ---- frontend/src/pages/index/LogModal.css | 181 ++ frontend/src/pages/index/LogModal.tsx | 192 ++ frontend/src/pages/index/LogModal.vue | 349 --- frontend/src/pages/index/PanelUpdateModal.css | 38 + frontend/src/pages/index/PanelUpdateModal.tsx | 118 + frontend/src/pages/index/PanelUpdateModal.vue | 112 - frontend/src/pages/index/StatusCard.css | 9 + frontend/src/pages/index/StatusCard.tsx | 115 + frontend/src/pages/index/StatusCard.vue | 96 - .../src/pages/index/SystemHistoryModal.css | 36 + .../src/pages/index/SystemHistoryModal.tsx | 163 ++ .../src/pages/index/SystemHistoryModal.vue | 160 -- frontend/src/pages/index/VersionModal.css | 44 + frontend/src/pages/index/VersionModal.tsx | 172 ++ frontend/src/pages/index/VersionModal.vue | 147 - frontend/src/pages/index/XrayLogModal.css | 160 ++ frontend/src/pages/index/XrayLogModal.tsx | 232 ++ frontend/src/pages/index/XrayLogModal.vue | 357 --- frontend/src/pages/index/XrayMetricsModal.css | 69 + frontend/src/pages/index/XrayMetricsModal.tsx | 342 +++ frontend/src/pages/index/XrayMetricsModal.vue | 347 --- frontend/src/pages/index/XrayStatusCard.css | 44 + frontend/src/pages/index/XrayStatusCard.tsx | 137 + frontend/src/pages/index/XrayStatusCard.vue | 151 -- frontend/src/pages/login/LoginPage.css | 445 ++++ frontend/src/pages/login/LoginPage.tsx | 258 ++ frontend/src/pages/login/LoginPage.vue | 490 ---- frontend/src/pages/nodes/NodeFormModal.css | 22 + frontend/src/pages/nodes/NodeFormModal.tsx | 301 +++ frontend/src/pages/nodes/NodeFormModal.vue | 209 -- frontend/src/pages/nodes/NodeHistoryPanel.css | 20 + frontend/src/pages/nodes/NodeHistoryPanel.tsx | 125 + frontend/src/pages/nodes/NodeHistoryPanel.vue | 116 - frontend/src/pages/nodes/NodeList.css | 145 + frontend/src/pages/nodes/NodeList.tsx | 445 ++++ frontend/src/pages/nodes/NodeList.vue | 499 ---- frontend/src/pages/nodes/NodesPage.css | 49 + frontend/src/pages/nodes/NodesPage.tsx | 188 ++ frontend/src/pages/nodes/NodesPage.vue | 216 -- frontend/src/pages/nodes/useNodes.js | 130 - frontend/src/pages/settings/GeneralTab.tsx | 352 +++ frontend/src/pages/settings/GeneralTab.vue | 437 --- frontend/src/pages/settings/SecurityTab.css | 90 + frontend/src/pages/settings/SecurityTab.tsx | 377 +++ frontend/src/pages/settings/SecurityTab.vue | 404 --- frontend/src/pages/settings/SettingsPage.css | 87 + frontend/src/pages/settings/SettingsPage.tsx | 342 +++ frontend/src/pages/settings/SettingsPage.vue | 375 --- .../pages/settings/SubscriptionFormatsTab.css | 4 + .../pages/settings/SubscriptionFormatsTab.tsx | 433 +++ .../pages/settings/SubscriptionFormatsTab.vue | 433 --- .../pages/settings/SubscriptionGeneralTab.tsx | 140 + .../pages/settings/SubscriptionGeneralTab.vue | 204 -- frontend/src/pages/settings/TelegramTab.tsx | 106 + frontend/src/pages/settings/TelegramTab.vue | 109 - .../src/pages/settings/TwoFactorModal.css | 20 + .../src/pages/settings/TwoFactorModal.tsx | 133 + .../src/pages/settings/TwoFactorModal.vue | 126 - frontend/src/pages/settings/useAllSetting.js | 80 - frontend/src/pages/sub/SubPage.css | 206 ++ frontend/src/pages/sub/SubPage.tsx | 387 +++ frontend/src/pages/sub/SubPage.vue | 531 ---- frontend/src/pages/xray/BalancerFormModal.tsx | 132 + frontend/src/pages/xray/BalancerFormModal.vue | 123 - frontend/src/pages/xray/BalancersTab.tsx | 366 +++ frontend/src/pages/xray/BalancersTab.vue | 362 --- frontend/src/pages/xray/BasicsTab.css | 7 + frontend/src/pages/xray/BasicsTab.tsx | 550 ++++ frontend/src/pages/xray/BasicsTab.vue | 499 ---- frontend/src/pages/xray/DnsPresetsModal.css | 32 + frontend/src/pages/xray/DnsPresetsModal.tsx | 67 + frontend/src/pages/xray/DnsPresetsModal.vue | 103 - frontend/src/pages/xray/DnsServerModal.tsx | 261 ++ frontend/src/pages/xray/DnsServerModal.vue | 190 -- frontend/src/pages/xray/DnsTab.css | 16 + frontend/src/pages/xray/DnsTab.tsx | 551 ++++ frontend/src/pages/xray/DnsTab.vue | 532 ---- frontend/src/pages/xray/NordModal.css | 69 + frontend/src/pages/xray/NordModal.tsx | 396 +++ frontend/src/pages/xray/NordModal.vue | 407 --- frontend/src/pages/xray/OutboundFormModal.css | 38 + frontend/src/pages/xray/OutboundFormModal.tsx | 1472 ++++++++++ frontend/src/pages/xray/OutboundFormModal.vue | 1094 -------- frontend/src/pages/xray/OutboundsTab.css | 215 ++ frontend/src/pages/xray/OutboundsTab.tsx | 520 ++++ frontend/src/pages/xray/OutboundsTab.vue | 676 ----- frontend/src/pages/xray/RoutingTab.css | 232 ++ frontend/src/pages/xray/RoutingTab.tsx | 546 ++++ frontend/src/pages/xray/RoutingTab.vue | 764 ------ frontend/src/pages/xray/RuleFormModal.tsx | 310 +++ frontend/src/pages/xray/RuleFormModal.vue | 275 -- frontend/src/pages/xray/WarpModal.css | 59 + frontend/src/pages/xray/WarpModal.tsx | 357 +++ frontend/src/pages/xray/WarpModal.vue | 381 --- frontend/src/pages/xray/XrayPage.css | 100 + frontend/src/pages/xray/XrayPage.tsx | 477 ++++ frontend/src/pages/xray/XrayPage.vue | 496 ---- frontend/src/pages/xray/useXraySetting.js | 245 -- frontend/src/styles/page-cards.css | 167 ++ frontend/src/utils/index.js | 107 +- frontend/src/utils/messageBus.ts | 12 + frontend/subpage.html | 2 +- frontend/tsconfig.json | 29 + frontend/vite.config.js | 40 +- frontend/xray.html | 2 +- go.mod | 10 +- go.sum | 20 +- sub/subController.go | 3 - web/controller/inbound.go | 1 - web/controller/xui.go | 2 +- web/service/client.go | 6 + web/service/server.go | 5 +- web/translation/ar-EG.json | 3 +- web/translation/en-US.json | 3 +- web/translation/es-ES.json | 3 +- web/translation/fa-IR.json | 3 +- web/translation/id-ID.json | 3 +- web/translation/ja-JP.json | 3 +- web/translation/pt-BR.json | 3 +- web/translation/ru-RU.json | 3 +- web/translation/tr-TR.json | 3 +- web/translation/uk-UA.json | 3 +- web/translation/vi-VN.json | 3 +- web/translation/zh-CN.json | 3 +- web/translation/zh-TW.json | 3 +- web/web.go | 8 +- 269 files changed, 30242 insertions(+), 25727 deletions(-) create mode 100644 frontend/src/components/AppSidebar.css create mode 100644 frontend/src/components/AppSidebar.tsx delete mode 100644 frontend/src/components/AppSidebar.vue create mode 100644 frontend/src/components/CustomStatistic.css create mode 100644 frontend/src/components/CustomStatistic.tsx delete mode 100644 frontend/src/components/CustomStatistic.vue create mode 100644 frontend/src/components/DateTimePicker.css create mode 100644 frontend/src/components/DateTimePicker.tsx delete mode 100644 frontend/src/components/DateTimePicker.vue create mode 100644 frontend/src/components/FinalMaskForm.tsx delete mode 100644 frontend/src/components/FinalMaskForm.vue create mode 100644 frontend/src/components/InfinityIcon.tsx delete mode 100644 frontend/src/components/InfinityIcon.vue create mode 100644 frontend/src/components/InputAddon.css create mode 100644 frontend/src/components/InputAddon.tsx create mode 100644 frontend/src/components/JsonEditor.css create mode 100644 frontend/src/components/JsonEditor.tsx delete mode 100644 frontend/src/components/JsonEditor.vue create mode 100644 frontend/src/components/PromptModal.tsx delete mode 100644 frontend/src/components/PromptModal.vue create mode 100644 frontend/src/components/SettingListItem.css create mode 100644 frontend/src/components/SettingListItem.tsx delete mode 100644 frontend/src/components/SettingListItem.vue create mode 100644 frontend/src/components/Sparkline.css create mode 100644 frontend/src/components/Sparkline.tsx delete mode 100644 frontend/src/components/Sparkline.vue delete mode 100644 frontend/src/components/TableSortable.vue create mode 100644 frontend/src/components/TextModal.tsx delete mode 100644 frontend/src/components/TextModal.vue delete mode 100644 frontend/src/composables/useDatepicker.js delete mode 100644 frontend/src/composables/useMediaQuery.js delete mode 100644 frontend/src/composables/useNodeList.js delete mode 100644 frontend/src/composables/useStatus.js delete mode 100644 frontend/src/composables/useTheme.js delete mode 100644 frontend/src/composables/useWebSocket.js delete mode 100644 frontend/src/entries/api-docs.js create mode 100644 frontend/src/entries/api-docs.tsx delete mode 100644 frontend/src/entries/clients.js create mode 100644 frontend/src/entries/clients.tsx delete mode 100644 frontend/src/entries/inbounds.js create mode 100644 frontend/src/entries/inbounds.tsx delete mode 100644 frontend/src/entries/index.js create mode 100644 frontend/src/entries/index.tsx delete mode 100644 frontend/src/entries/login.js create mode 100644 frontend/src/entries/login.tsx delete mode 100644 frontend/src/entries/nodes.js create mode 100644 frontend/src/entries/nodes.tsx delete mode 100644 frontend/src/entries/settings.js create mode 100644 frontend/src/entries/settings.tsx delete mode 100644 frontend/src/entries/subpage.js create mode 100644 frontend/src/entries/subpage.tsx delete mode 100644 frontend/src/entries/xray.js create mode 100644 frontend/src/entries/xray.tsx create mode 100644 frontend/src/env.d.ts create mode 100644 frontend/src/hooks/useAllSetting.ts create mode 100644 frontend/src/hooks/useClients.ts create mode 100644 frontend/src/hooks/useDatepicker.ts create mode 100644 frontend/src/hooks/useMediaQuery.ts create mode 100644 frontend/src/hooks/useNodes.ts create mode 100644 frontend/src/hooks/useStatus.ts create mode 100644 frontend/src/hooks/useTheme.tsx create mode 100644 frontend/src/hooks/useWebSocket.ts create mode 100644 frontend/src/hooks/useXraySetting.ts delete mode 100644 frontend/src/i18n/index.js create mode 100644 frontend/src/i18n/react.ts delete mode 100644 frontend/src/models/setting.js create mode 100644 frontend/src/models/setting.ts delete mode 100644 frontend/src/models/status.js create mode 100644 frontend/src/models/status.ts create mode 100644 frontend/src/pages/api-docs/ApiDocsPage.css create mode 100644 frontend/src/pages/api-docs/ApiDocsPage.tsx delete mode 100644 frontend/src/pages/api-docs/ApiDocsPage.vue rename frontend/src/pages/api-docs/{CodeBlock.vue => CodeBlock.css} (54%) create mode 100644 frontend/src/pages/api-docs/CodeBlock.tsx create mode 100644 frontend/src/pages/api-docs/EndpointRow.css create mode 100644 frontend/src/pages/api-docs/EndpointRow.tsx delete mode 100644 frontend/src/pages/api-docs/EndpointRow.vue rename frontend/src/pages/api-docs/{EndpointSection.vue => EndpointSection.css} (51%) create mode 100644 frontend/src/pages/api-docs/EndpointSection.tsx create mode 100644 frontend/src/pages/clients/ClientBulkAddModal.css create mode 100644 frontend/src/pages/clients/ClientBulkAddModal.tsx delete mode 100644 frontend/src/pages/clients/ClientBulkAddModal.vue create mode 100644 frontend/src/pages/clients/ClientFormModal.css create mode 100644 frontend/src/pages/clients/ClientFormModal.tsx delete mode 100644 frontend/src/pages/clients/ClientFormModal.vue create mode 100644 frontend/src/pages/clients/ClientInfoModal.css create mode 100644 frontend/src/pages/clients/ClientInfoModal.tsx delete mode 100644 frontend/src/pages/clients/ClientInfoModal.vue create mode 100644 frontend/src/pages/clients/ClientQrModal.tsx delete mode 100644 frontend/src/pages/clients/ClientQrModal.vue create mode 100644 frontend/src/pages/clients/ClientsPage.css create mode 100644 frontend/src/pages/clients/ClientsPage.tsx delete mode 100644 frontend/src/pages/clients/ClientsPage.vue delete mode 100644 frontend/src/pages/clients/useClients.js create mode 100644 frontend/src/pages/inbounds/InboundFormModal.css create mode 100644 frontend/src/pages/inbounds/InboundFormModal.tsx delete mode 100644 frontend/src/pages/inbounds/InboundFormModal.vue create mode 100644 frontend/src/pages/inbounds/InboundInfoModal.css create mode 100644 frontend/src/pages/inbounds/InboundInfoModal.tsx delete mode 100644 frontend/src/pages/inbounds/InboundInfoModal.vue create mode 100644 frontend/src/pages/inbounds/InboundList.css create mode 100644 frontend/src/pages/inbounds/InboundList.tsx delete mode 100644 frontend/src/pages/inbounds/InboundList.vue create mode 100644 frontend/src/pages/inbounds/InboundsPage.css create mode 100644 frontend/src/pages/inbounds/InboundsPage.tsx delete mode 100644 frontend/src/pages/inbounds/InboundsPage.vue create mode 100644 frontend/src/pages/inbounds/QrCodeModal.tsx delete mode 100644 frontend/src/pages/inbounds/QrCodeModal.vue create mode 100644 frontend/src/pages/inbounds/QrPanel.css create mode 100644 frontend/src/pages/inbounds/QrPanel.tsx delete mode 100644 frontend/src/pages/inbounds/QrPanel.vue delete mode 100644 frontend/src/pages/inbounds/useInbounds.js create mode 100644 frontend/src/pages/inbounds/useInbounds.ts create mode 100644 frontend/src/pages/index/BackupModal.css create mode 100644 frontend/src/pages/index/BackupModal.tsx delete mode 100644 frontend/src/pages/index/BackupModal.vue create mode 100644 frontend/src/pages/index/CustomGeoFormModal.tsx delete mode 100644 frontend/src/pages/index/CustomGeoFormModal.vue create mode 100644 frontend/src/pages/index/CustomGeoSection.css create mode 100644 frontend/src/pages/index/CustomGeoSection.tsx delete mode 100644 frontend/src/pages/index/CustomGeoSection.vue create mode 100644 frontend/src/pages/index/IndexPage.css create mode 100644 frontend/src/pages/index/IndexPage.tsx delete mode 100644 frontend/src/pages/index/IndexPage.vue create mode 100644 frontend/src/pages/index/LogModal.css create mode 100644 frontend/src/pages/index/LogModal.tsx delete mode 100644 frontend/src/pages/index/LogModal.vue create mode 100644 frontend/src/pages/index/PanelUpdateModal.css create mode 100644 frontend/src/pages/index/PanelUpdateModal.tsx delete mode 100644 frontend/src/pages/index/PanelUpdateModal.vue create mode 100644 frontend/src/pages/index/StatusCard.css create mode 100644 frontend/src/pages/index/StatusCard.tsx delete mode 100644 frontend/src/pages/index/StatusCard.vue create mode 100644 frontend/src/pages/index/SystemHistoryModal.css create mode 100644 frontend/src/pages/index/SystemHistoryModal.tsx delete mode 100644 frontend/src/pages/index/SystemHistoryModal.vue create mode 100644 frontend/src/pages/index/VersionModal.css create mode 100644 frontend/src/pages/index/VersionModal.tsx delete mode 100644 frontend/src/pages/index/VersionModal.vue create mode 100644 frontend/src/pages/index/XrayLogModal.css create mode 100644 frontend/src/pages/index/XrayLogModal.tsx delete mode 100644 frontend/src/pages/index/XrayLogModal.vue create mode 100644 frontend/src/pages/index/XrayMetricsModal.css create mode 100644 frontend/src/pages/index/XrayMetricsModal.tsx delete mode 100644 frontend/src/pages/index/XrayMetricsModal.vue create mode 100644 frontend/src/pages/index/XrayStatusCard.css create mode 100644 frontend/src/pages/index/XrayStatusCard.tsx delete mode 100644 frontend/src/pages/index/XrayStatusCard.vue create mode 100644 frontend/src/pages/login/LoginPage.css create mode 100644 frontend/src/pages/login/LoginPage.tsx delete mode 100644 frontend/src/pages/login/LoginPage.vue create mode 100644 frontend/src/pages/nodes/NodeFormModal.css create mode 100644 frontend/src/pages/nodes/NodeFormModal.tsx delete mode 100644 frontend/src/pages/nodes/NodeFormModal.vue create mode 100644 frontend/src/pages/nodes/NodeHistoryPanel.css create mode 100644 frontend/src/pages/nodes/NodeHistoryPanel.tsx delete mode 100644 frontend/src/pages/nodes/NodeHistoryPanel.vue create mode 100644 frontend/src/pages/nodes/NodeList.css create mode 100644 frontend/src/pages/nodes/NodeList.tsx delete mode 100644 frontend/src/pages/nodes/NodeList.vue create mode 100644 frontend/src/pages/nodes/NodesPage.css create mode 100644 frontend/src/pages/nodes/NodesPage.tsx delete mode 100644 frontend/src/pages/nodes/NodesPage.vue delete mode 100644 frontend/src/pages/nodes/useNodes.js create mode 100644 frontend/src/pages/settings/GeneralTab.tsx delete mode 100644 frontend/src/pages/settings/GeneralTab.vue create mode 100644 frontend/src/pages/settings/SecurityTab.css create mode 100644 frontend/src/pages/settings/SecurityTab.tsx delete mode 100644 frontend/src/pages/settings/SecurityTab.vue create mode 100644 frontend/src/pages/settings/SettingsPage.css create mode 100644 frontend/src/pages/settings/SettingsPage.tsx delete mode 100644 frontend/src/pages/settings/SettingsPage.vue create mode 100644 frontend/src/pages/settings/SubscriptionFormatsTab.css create mode 100644 frontend/src/pages/settings/SubscriptionFormatsTab.tsx delete mode 100644 frontend/src/pages/settings/SubscriptionFormatsTab.vue create mode 100644 frontend/src/pages/settings/SubscriptionGeneralTab.tsx delete mode 100644 frontend/src/pages/settings/SubscriptionGeneralTab.vue create mode 100644 frontend/src/pages/settings/TelegramTab.tsx delete mode 100644 frontend/src/pages/settings/TelegramTab.vue create mode 100644 frontend/src/pages/settings/TwoFactorModal.css create mode 100644 frontend/src/pages/settings/TwoFactorModal.tsx delete mode 100644 frontend/src/pages/settings/TwoFactorModal.vue delete mode 100644 frontend/src/pages/settings/useAllSetting.js create mode 100644 frontend/src/pages/sub/SubPage.css create mode 100644 frontend/src/pages/sub/SubPage.tsx delete mode 100644 frontend/src/pages/sub/SubPage.vue create mode 100644 frontend/src/pages/xray/BalancerFormModal.tsx delete mode 100644 frontend/src/pages/xray/BalancerFormModal.vue create mode 100644 frontend/src/pages/xray/BalancersTab.tsx delete mode 100644 frontend/src/pages/xray/BalancersTab.vue create mode 100644 frontend/src/pages/xray/BasicsTab.css create mode 100644 frontend/src/pages/xray/BasicsTab.tsx delete mode 100644 frontend/src/pages/xray/BasicsTab.vue create mode 100644 frontend/src/pages/xray/DnsPresetsModal.css create mode 100644 frontend/src/pages/xray/DnsPresetsModal.tsx delete mode 100644 frontend/src/pages/xray/DnsPresetsModal.vue create mode 100644 frontend/src/pages/xray/DnsServerModal.tsx delete mode 100644 frontend/src/pages/xray/DnsServerModal.vue create mode 100644 frontend/src/pages/xray/DnsTab.css create mode 100644 frontend/src/pages/xray/DnsTab.tsx delete mode 100644 frontend/src/pages/xray/DnsTab.vue create mode 100644 frontend/src/pages/xray/NordModal.css create mode 100644 frontend/src/pages/xray/NordModal.tsx delete mode 100644 frontend/src/pages/xray/NordModal.vue create mode 100644 frontend/src/pages/xray/OutboundFormModal.css create mode 100644 frontend/src/pages/xray/OutboundFormModal.tsx delete mode 100644 frontend/src/pages/xray/OutboundFormModal.vue create mode 100644 frontend/src/pages/xray/OutboundsTab.css create mode 100644 frontend/src/pages/xray/OutboundsTab.tsx delete mode 100644 frontend/src/pages/xray/OutboundsTab.vue create mode 100644 frontend/src/pages/xray/RoutingTab.css create mode 100644 frontend/src/pages/xray/RoutingTab.tsx delete mode 100644 frontend/src/pages/xray/RoutingTab.vue create mode 100644 frontend/src/pages/xray/RuleFormModal.tsx delete mode 100644 frontend/src/pages/xray/RuleFormModal.vue create mode 100644 frontend/src/pages/xray/WarpModal.css create mode 100644 frontend/src/pages/xray/WarpModal.tsx delete mode 100644 frontend/src/pages/xray/WarpModal.vue create mode 100644 frontend/src/pages/xray/XrayPage.css create mode 100644 frontend/src/pages/xray/XrayPage.tsx delete mode 100644 frontend/src/pages/xray/XrayPage.vue delete mode 100644 frontend/src/pages/xray/useXraySetting.js create mode 100644 frontend/src/styles/page-cards.css create mode 100644 frontend/src/utils/messageBus.ts create mode 100644 frontend/tsconfig.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b2333fb6..9a4f9110 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,7 +10,6 @@ on: - "**.mjs" - "**.cjs" - "**.ts" - - "**.vue" - "**.html" - "**.css" - "frontend/package.json" @@ -27,7 +26,6 @@ on: - "**.mjs" - "**.cjs" - "**.ts" - - "**.vue" - "**.html" - "**.css" - "frontend/package.json" diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 966c581b..31f7d215 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -14,7 +14,6 @@ on: - "**.mjs" - "**.cjs" - "**.ts" - - "**.vue" - "frontend/package-lock.json" pull_request: paths: @@ -25,7 +24,6 @@ on: - "**.mjs" - "**.cjs" - "**.ts" - - "**.vue" - "frontend/package-lock.json" schedule: - cron: "18 2 * * 2" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3b638eab..df86a474 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -116,7 +116,7 @@ jobs: cd x-ui/bin # Download dependencies - Xray_URL="https://github.com/XTLS/Xray-core/releases/download/v26.4.25/" + Xray_URL="https://github.com/XTLS/Xray-core/releases/download/v26.5.9/" if [ "${{ matrix.platform }}" == "amd64" ]; then wget -q ${Xray_URL}Xray-linux-64.zip unzip Xray-linux-64.zip @@ -250,7 +250,7 @@ jobs: cd x-ui\bin # Download Xray for Windows - $Xray_URL = "https://github.com/XTLS/Xray-core/releases/download/v26.4.25/" + $Xray_URL = "https://github.com/XTLS/Xray-core/releases/download/v26.5.9/" Invoke-WebRequest -Uri "${Xray_URL}Xray-windows-64.zip" -OutFile "Xray-windows-64.zip" Expand-Archive -Path "Xray-windows-64.zip" -DestinationPath . Remove-Item "Xray-windows-64.zip" diff --git a/.gitignore b/.gitignore index 81a1f0c4..d343f43b 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,9 @@ tmp/ backup/ bin/ dist/ +!web/dist/ +web/dist/* +!web/dist/.gitkeep release/ node_modules/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9ea49667..a09ae517 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,33 +1,33 @@ # Contributing -Thanks for taking the time to contribute to 3x-ui. This guide gets a development panel running on your machine in a few minutes. +Thanks for taking the time to contribute to 3x-ui. This guide gets a development panel running locally and explains the conventions the project follows so changes land cleanly. ## Prerequisites -- **Go 1.26+** (the version in `go.mod`) -- **Node.js 22+** and npm (for the Vue frontend) +- **Go 1.26+** (the version pinned in `go.mod`) +- **Node.js 22+** and npm 10+ (for the React frontend) - **Git** -- **A C compiler** — required by the CGo SQLite driver (`github.com/mattn/go-sqlite3`). Linux/macOS already ship one; on Windows see below. +- **A C compiler** — required by the CGo SQLite driver (`github.com/mattn/go-sqlite3`). Linux and macOS already ship one; for Windows see below. ### Windows: MinGW-w64 -`go build` on Windows will fail with `cgo: C compiler "gcc" not found` until you install a GCC toolchain. Two options — pick whichever fits. +`go build` on Windows fails with `cgo: C compiler "gcc" not found` until a GCC toolchain is installed. Two options — pick whichever fits. **Option A — standalone zip (fastest, no package manager)** -1. Grab the latest build from ****. For most setups you want a release named like: +1. Download the latest build from . For most setups, pick a release named: ``` x86_64--release-posix-seh-ucrt-rt_-rev.7z ``` - (64-bit, POSIX threads, SEH exceptions, UCRT runtime — matches the modern Windows defaults.) + (64-bit, POSIX threads, SEH exceptions, UCRT runtime — matches modern Windows defaults.) 2. Extract it somewhere stable, e.g. `C:\mingw64\`. -3. Add `C:\mingw64\bin` to your **Windows** `PATH` (System Properties → Environment Variables → Path → New). +3. Add `C:\mingw64\bin` to the **Windows** `PATH` (System Properties → Environment Variables → Path → New). 4. Open a fresh terminal and confirm: ```powershell gcc --version ``` -**Option B — MSYS2 (if you also want a Unix-y shell)** +**Option B — MSYS2 (when a Unix shell is also useful)** 1. Install MSYS2 from . 2. Open the **MSYS2 UCRT64** shell from the Start menu and update once: @@ -38,14 +38,14 @@ Thanks for taking the time to contribute to 3x-ui. This guide gets a development ```bash pacman -S --needed mingw-w64-ucrt-x86_64-gcc mingw-w64-ucrt-x86_64-pkg-config ``` -4. Add `C:\msys64\ucrt64\bin` to your Windows `PATH`. +4. Add `C:\msys64\ucrt64\bin` to the Windows `PATH`. 5. Verify with `gcc --version` in a fresh terminal. -After either, `go build ./...` and `go run .` work normally. +After either path, `go build ./...` and `go run .` work normally. -> Why MinGW-w64 over MSVC: `mattn/go-sqlite3` officially supports GCC, builds are faster on Windows, and the toolchain doesn't lock you into a Visual Studio install. If you already have Visual Studio Build Tools installed it works too — just make sure `CC=cl` is **not** set in your environment. +> **Why MinGW-w64 over MSVC:** `mattn/go-sqlite3` officially supports GCC, builds are faster on Windows, and the toolchain does not require a Visual Studio install. If Visual Studio Build Tools are already present that works too — just make sure `CC=cl` is **not** set in the environment. -The Linux SQLite cross-build from Windows (or vice versa) needs an extra cross-compiler — out of scope here; build natively on the target OS. +Cross-building the Linux SQLite target from Windows (or vice versa) requires a separate cross-compiler and is out of scope here; build natively on the target OS. ## First-time setup @@ -65,7 +65,7 @@ npm run build cd .. ``` -`.env.example` ships with sane defaults that point the database, logs, and xray binary at the local `x-ui/` folder so nothing escapes the project directory: +`.env.example` ships with defaults that keep the database, logs, and xray binary inside the local `x-ui/` folder so nothing escapes the project directory: ``` XUI_DEBUG=true @@ -74,7 +74,7 @@ XUI_LOG_FOLDER=x-ui XUI_BIN_FOLDER=x-ui ``` -You need to drop the xray binary (`xray-windows-amd64.exe` on Windows, `xray-linux-amd64` on Linux, etc.) plus the matching `geoip.dat` / `geosite.dat` files into `x-ui/`. The easiest source is a [released Xray-core build](https://github.com/XTLS/Xray-core/releases). On Windows you also want `wintun.dll` if you plan to test TUN inbounds. +Drop the xray binary (`xray-windows-amd64.exe` on Windows, `xray-linux-amd64` on Linux, etc.) plus the matching `geoip.dat` and `geosite.dat` files into `x-ui/`. The easiest source is a [released Xray-core build](https://github.com/XTLS/Xray-core/releases). On Windows, `wintun.dll` is also required for testing TUN inbounds. ## Running @@ -82,11 +82,11 @@ You need to drop the xray binary (`xray-windows-amd64.exe` on Windows, `xray-lin go run . ``` -Open [http://localhost:2053](http://localhost:2053) and log in with `admin` / `admin`. You will be prompted to change the credentials on first login. +Open [http://localhost:2053](http://localhost:2053) and log in with `admin` / `admin`. Credentials must be changed on first login. ### Inside VS Code -The repo ships a launch profile in `.vscode/launch.json` (gitignored — copy from the snippet below if it is missing): +The repo ships a launch profile in `.vscode/launch.json` (gitignored — copy from the snippet below if absent): ```jsonc { @@ -113,93 +113,97 @@ The repo ships a launch profile in `.vscode/launch.json` (gitignored — copy fr ## Working on the frontend -The panel UI is a Vue 3 + Ant Design Vue 4 app under `frontend/`. A few things worth knowing before you dive in. +The panel UI is a **React 19 + Ant Design 6 + TypeScript** app under `frontend/`, built with Vite 8. The sections below cover the architecture, the conventions, and the two dev workflows. -### Architecture in one paragraph +### Architecture -It's a **multi-page app**, not a SPA. Every panel route (`/panel`, `/panel/inbounds`, `/panel/clients`, `/panel/xray`, `/panel/settings`, `/panel/sub`, `/panel/api-docs`, plus `login`) has its own HTML entry under `frontend/*.html` and its own bootstrap in `src/entries/.js`. Vite builds them into `web/dist/`, and the Go binary embeds that directory at compile time with `embed.FS`. Each navigation triggers a real document load — but each page's bundle is small, so it stays snappy. There's no Vue Router and no central store; Vuex/Pinia were rejected as overkill for the panel's surface area. +The frontend is a **multi-page application**, not a SPA. Every panel route (`/panel`, `/panel/inbounds`, `/panel/clients`, `/panel/xray`, `/panel/settings`, `/panel/nodes`, `/panel/api-docs`, `/panel/sub`, plus `login`) has its own HTML entry in `frontend/*.html` and its own bootstrap in `src/entries/.tsx`. Vite emits each entry into `web/dist/`, and the Go binary embeds that directory at compile time via `embed.FS`. Each panel navigation is a real document load, but every per-page bundle is small enough to keep the experience responsive. There is no React Router and no global store; the surface area does not justify either. ### State and data flow -- **No global store.** State lives where it's used. Cross-page data (settings, current user, theme) is re-fetched on each page load — the backend is on the same box and responses are cheap. -- **Composables** in `src/composables/` carry reactive logic worth sharing inside a page (theme switching, status polling, node lists). Reach for one before adding a new global. -- **Domain classes** in `src/models/` (`Inbound`, `DBInbound`, `Outbound`, `Status`, …) own the protocol-specific logic — link generation, settings JSON shape, TLS/Reality stream handling. The Vue components stay dumb; they ask the model "what's my link?" and render the answer. -- **HTTP** goes through `src/utils/index.js`'s `HttpUtil`, which is a thin Axios wrapper with CSRF, response toast handling, and a `silent: true` opt-out for bulk operations that would otherwise spam toasts. +- **No global store.** State lives in the page that owns it. Cross-page data (settings, current user, theme) is re-fetched on each page load — the backend is local and responses are inexpensive. +- **Hooks** in `src/hooks/` encapsulate reactive logic worth sharing inside a page (`useTheme`, `useStatus`, `useNodes`, `useWebSocket`, `useDatepicker`, …). Prefer extending an existing hook over introducing a new global. +- **Domain models** in `src/models/` (`Inbound`, `DBInbound`, `Outbound`, `Status`, …) own the protocol-specific logic — link generation, settings JSON shape, TLS/Reality stream handling. React components stay declarative; they ask the model "what is my link?" and render the answer. +- **HTTP** goes through `src/utils/index.js`'s `HttpUtil`, a thin Axios wrapper that handles CSRF, response toasts, and a `silent: true` opt-out for bulk operations that would otherwise spam toasts. The Axios setup itself lives in `src/api/axios-init.js`. ### i18n -Locale strings live in `web/translation/.json`, not under `frontend/`. The Go side embeds the same JSON and serves it to both backend templates and `vue-i18n`. When you add a new English key, add it to **every** non-English locale too — missing keys don't fail the build, they just render the raw key in the UI. +Locale strings live in `web/translation/.json`, **not** under `frontend/`. The Go binary embeds the same JSON and serves it to both backend templates and `react-i18next` (initialized in `src/i18n/react.ts`). When a new English key is added it must also land in **every** non-English locale — missing keys do not break the build, they just render the raw key in the UI. ### Two dev workflows -| When you want… | Use | -|----------------|-----| -| To iterate on UI tweaks fast | `cd frontend && npm run dev` (Vite on `:5173`, proxies `/panel/*` and `/api/*` to the Go panel on `:2053`). Start the Go panel first. | -| To test what users actually see | `cd frontend && npm run build`, then `go run .`. The Go binary serves the built bundle either embedded (release mode) or from disk (debug mode). | +| Goal | Command | +|------|---------| +| Iterate on UI changes with HMR | `cd frontend && npm run dev` (Vite on `:5173`, proxies `/panel/*` and `/api/*` to the Go panel on `:2053`). Start the Go panel first. | +| Verify what end users actually see | `cd frontend && npm run build`, then `go run .`. The Go binary serves the built bundle — embedded in release mode, off disk in debug mode. | -The Vite dev proxy auto-rewrites the sidebar's production-style links (`/panel`, `/panel/inbounds`, `/panel/clients`, etc.) to the matching Vite-served HTML, so the navigation feels identical to prod without round-tripping through Go. The route allowlist lives in `MIGRATED_ROUTES` in `vite.config.js` — if you add a new page, register it there too. +The Vite dev proxy rewrites the sidebar's production-style links (`/panel`, `/panel/inbounds`, `/panel/clients`, …) to the matching Vite-served HTML, so navigation behaves identically to production without round-tripping through Go. The allowlist lives in `MIGRATED_ROUTES` in `vite.config.js` — register every new page there. -> **`XUI_DEBUG=true` gotcha** — in debug mode the panel serves HTML out of the embedded FS (frozen at the last `go build` / `go run`) but JS/CSS off disk. Re-running `npm run build` without restarting Go leaves the embedded HTML pointing at the *old* hashed asset names → blank page with 404s in the browser console. Always restart `go run .` after a frontend rebuild. +> **`XUI_DEBUG=true` gotcha** — in debug mode the panel serves HTML from the embedded FS (frozen at the last `go build` / `go run`) but JS/CSS off disk. Re-running `npm run build` without restarting Go leaves the embedded HTML pointing at the *old* hashed asset names, producing a blank page with 404s in the console. Always restart `go run .` after a frontend rebuild. ### Adding a new page -1. Create `frontend/.html` (copy an existing one and adjust the title + the imported entry). -2. Create `src/entries/.js` — `createApp(Page).use(antd).use(i18n).mount('#app')`. -3. Create the page component under `src/pages//.vue` (kebab-case folder, PascalCase component). +1. Create `frontend/.html` (copy an existing entry and adjust the title and the imported ` + diff --git a/frontend/clients.html b/frontend/clients.html index a2c03040..67c76866 100644 --- a/frontend/clients.html +++ b/frontend/clients.html @@ -8,6 +8,6 @@
- + diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js index 95c46dbb..1b66c511 100644 --- a/frontend/eslint.config.js +++ b/frontend/eslint.config.js @@ -1,21 +1,16 @@ import js from '@eslint/js'; -import vue from 'eslint-plugin-vue'; -import vueParser from 'vue-eslint-parser'; +import tseslint from 'typescript-eslint'; +import reactHooks from 'eslint-plugin-react-hooks'; import globals from 'globals'; export default [ { ignores: ['node_modules/**', '../web/dist/**'] }, js.configs.recommended, - ...vue.configs['flat/recommended'], { - files: ['**/*.{js,vue}'], + files: ['**/*.js'], languageOptions: { ecmaVersion: 2022, sourceType: 'module', - parser: vueParser, - parserOptions: { - ecmaFeatures: { jsx: false }, - }, globals: { ...globals.browser, ...globals.node, @@ -29,30 +24,48 @@ export default [ }], 'no-empty': ['error', { allowEmptyCatch: true }], 'no-case-declarations': 'off', + }, + }, + ...tseslint.configs.recommended.map((config) => ({ + ...config, + files: ['**/*.{ts,tsx}'], + })), + { + files: ['**/*.{ts,tsx}'], + plugins: { + 'react-hooks': reactHooks, + }, + languageOptions: { + ecmaVersion: 2022, + sourceType: 'module', + globals: { + ...globals.browser, + }, + }, + rules: { + ...reactHooks.configs.recommended.rules, + '@typescript-eslint/no-unused-vars': ['warn', { + argsIgnorePattern: '^_', + varsIgnorePattern: '^_', + caughtErrorsIgnorePattern: '^_', + }], + 'no-empty': ['error', { allowEmptyCatch: true }], - // Stylistic rules from vue/recommended that don't match the - // existing codebase formatting. Disable rather than churn the - // whole tree to satisfy them. - 'vue/multi-word-component-names': 'off', - 'vue/no-v-html': 'off', - 'vue/html-self-closing': 'off', - 'vue/max-attributes-per-line': 'off', - 'vue/singleline-html-element-content-newline': 'off', - 'vue/multiline-html-element-content-newline': 'off', - 'vue/html-indent': 'off', - 'vue/html-closing-bracket-newline': 'off', - 'vue/attributes-order': 'off', - 'vue/first-attribute-linebreak': 'off', - 'vue/one-component-per-file': 'off', - 'vue/order-in-components': 'off', - 'vue/attribute-hyphenation': 'off', - 'vue/v-on-event-hyphenation': 'off', - - // Pervasive in form components ported from the Vue 2 codebase - // (parent passes a reactive object; child mutates it in place). - // Properly fixing this means rewiring those components to emit - // updates — a meaningful architectural change, separate task. - 'vue/no-mutating-props': 'off', + // react-hooks v7 introduces several new rules driven by the React + // Compiler. The migration uses several legitimate patterns those + // rules flag (initial-fetch in useEffect, dirty-check derived + // state, `Date.now()` inside derive helpers, inline arrow event + // handlers, in-place mutation of imported Outbound class + // instances in the OutboundFormModal). We're not running the + // compiler, so the memoization-preservation warnings have no + // effect on runtime — turning them off until the codebase + // stabilises. + 'react-hooks/set-state-in-effect': 'off', + 'react-hooks/purity': 'off', + 'react-hooks/react-compiler': 'off', + 'react-hooks/preserve-manual-memoization': 'off', + 'react-hooks/immutability': 'off', + 'react-hooks/refs': 'off', }, }, ]; diff --git a/frontend/inbounds.html b/frontend/inbounds.html index 9e8861fc..52bb49c0 100644 --- a/frontend/inbounds.html +++ b/frontend/inbounds.html @@ -8,6 +8,6 @@
- + diff --git a/frontend/index.html b/frontend/index.html index d13b400f..196cea68 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -8,6 +8,6 @@
- + diff --git a/frontend/login.html b/frontend/login.html index 658f8d10..44543fd0 100644 --- a/frontend/login.html +++ b/frontend/login.html @@ -9,6 +9,6 @@
- + diff --git a/frontend/nodes.html b/frontend/nodes.html index fec96dbc..908ae240 100644 --- a/frontend/nodes.html +++ b/frontend/nodes.html @@ -8,6 +8,6 @@
- + diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 460f6758..ab11d1d5 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,34 +1,39 @@ { "name": "3x-ui-frontend", - "version": "0.0.3", + "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "3x-ui-frontend", - "version": "0.0.3", + "version": "0.1.0", "dependencies": { - "@ant-design/icons-vue": "^7.0.1", + "@ant-design/icons": "^6.2.3", "@codemirror/lang-json": "^6.0.2", "@codemirror/theme-one-dark": "^6.1.3", - "ant-design-vue": "^4.2.6", - "axios": "^1.7.9", + "antd": "^6.4.3", + "axios": "^1.16.1", "codemirror": "^6.0.2", "dayjs": "^1.11.20", + "i18next": "^26.2.0", "otpauth": "^9.5.1", - "qs": "^6.13.1", - "vue": "^3.5.34", - "vue-i18n": "^11.1.4", - "vue3-persian-datetime-picker": "^1.2.2" + "persian-calendar-suite": "^1.5.5", + "qs": "^6.15.2", + "react": "^19.2.6", + "react-dom": "^19.2.6", + "react-i18next": "^17.0.8" }, "devDependencies": { "@eslint/js": "^10.0.1", - "@vitejs/plugin-vue": "^6.0.6", - "eslint": "^10.3.0", - "eslint-plugin-vue": "^10.9.1", + "@types/react": "^19.2.15", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.2", + "eslint": "^10.4.0", + "eslint-plugin-react-hooks": "^7.1.1", "globals": "^17.6.0", - "vite": "8.0.13", - "vue-eslint-parser": "^10.4.0" + "typescript": "^6.0.3", + "typescript-eslint": "^8.59.4", + "vite": "8.0.13" }, "engines": { "node": ">=22.0.0", @@ -36,12 +41,74 @@ } }, "node_modules/@ant-design/colors": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@ant-design/colors/-/colors-6.0.0.tgz", - "integrity": "sha512-qAZRvPzfdWHtfameEGP2Qvuf838NhergR35o+EuVyB5XvSA98xod5r4utvi4TJ3ywmevm290g9nsCG5MryrdWQ==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@ant-design/colors/-/colors-8.0.1.tgz", + "integrity": "sha512-foPVl0+SWIslGUtD/xBr1p9U4AKzPhNYEseXYRRo5QSzGACYZrQbe11AYJbYfAWnWSpGBx6JjBmSeugUsD9vqQ==", "license": "MIT", "dependencies": { - "@ctrl/tinycolor": "^3.4.0" + "@ant-design/fast-color": "^3.0.0" + } + }, + "node_modules/@ant-design/cssinjs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@ant-design/cssinjs/-/cssinjs-2.1.2.tgz", + "integrity": "sha512-2Hy8BnCEH31xPeSLbhhB2ctCPXE2ZnASdi+KbSeS79BNbUhL9hAEe20SkUk+BR8aKTmqb6+FKFruk7w8z0VoRQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.1", + "@emotion/hash": "^0.8.0", + "@emotion/unitless": "^0.7.5", + "@rc-component/util": "^1.4.0", + "clsx": "^2.1.1", + "csstype": "^3.1.3", + "stylis": "^4.3.4" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/@ant-design/cssinjs-utils": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@ant-design/cssinjs-utils/-/cssinjs-utils-2.1.2.tgz", + "integrity": "sha512-5fTHQ158jJJ5dC/ECeyIdZUzKxE/mpEMRZxthyG1sw/AKRHKgJBg00Yi6ACVXgycdje7KahRNvNET/uBccwCnA==", + "license": "MIT", + "dependencies": { + "@ant-design/cssinjs": "^2.1.2", + "@babel/runtime": "^7.23.2", + "@rc-component/util": "^1.4.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/@ant-design/fast-color": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@ant-design/fast-color/-/fast-color-3.0.1.tgz", + "integrity": "sha512-esKJegpW4nckh0o6kV3Tkb7NPIZYbPnnFxmQDUmL08ukXZAvV85TZBr70eGuke/CIArLaP6aw8lt9KILjnWuOw==", + "license": "MIT", + "engines": { + "node": ">=8.x" + } + }, + "node_modules/@ant-design/icons": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-6.2.3.tgz", + "integrity": "sha512-Pl3aoAtxQeKryYnt6VvDJtOxMOtA8wrRSACe/pTjOAIG3fdHrWm6Ivb4ku9tsFjYroSXBKirvuxG4QkwBXD9gg==", + "license": "MIT", + "dependencies": { + "@ant-design/colors": "^8.0.1", + "@ant-design/icons-svg": "^4.4.2", + "@rc-component/util": "^1.10.1", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" } }, "node_modules/@ant-design/icons-svg": { @@ -50,23 +117,159 @@ "integrity": "sha512-vHbT+zJEVzllwP+CM+ul7reTEfBR0vgxFe7+lREAsAA7YGsYpboiq2sQNeQeRvh09GfQgs/GyFEvZpJ9cLXpXA==", "license": "MIT" }, - "node_modules/@ant-design/icons-vue": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/@ant-design/icons-vue/-/icons-vue-7.0.1.tgz", - "integrity": "sha512-eCqY2unfZK6Fe02AwFlDHLfoyEFreP6rBwAZMIJ1LugmfMiVgwWDYlp1YsRugaPtICYOabV1iWxXdP12u9U43Q==", + "node_modules/@ant-design/react-slick": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@ant-design/react-slick/-/react-slick-2.0.0.tgz", + "integrity": "sha512-HMS9sRoEmZey8LsE/Yo6+klhlzU12PisjrVcydW3So7RdklyEd2qehyU6a7Yp+OYN72mgsYs3NFCyP2lCPFVqg==", "license": "MIT", "dependencies": { - "@ant-design/colors": "^6.0.0", - "@ant-design/icons-svg": "^4.2.1" + "@babel/runtime": "^7.28.4", + "clsx": "^2.1.1", + "json2mq": "^0.2.0", + "throttle-debounce": "^5.0.0" }, "peerDependencies": { - "vue": ">=3.0.3" + "react": "^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.3.tgz", + "integrity": "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, "node_modules/@babel/helper-string-parser": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -76,15 +279,41 @@ "version": "7.28.5", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" } }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/parser": { "version": "7.29.3", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz", "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==", + "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.29.0" @@ -105,10 +334,45 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/types": { "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", @@ -221,15 +485,6 @@ "w3c-keyname": "^2.2.4" } }, - "node_modules/@ctrl/tinycolor": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz", - "integrity": "sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, "node_modules/@emnapi/core": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", @@ -265,15 +520,15 @@ } }, "node_modules/@emotion/hash": { - "version": "0.9.2", - "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", - "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==", + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz", + "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==", "license": "MIT" }, "node_modules/@emotion/unitless": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz", - "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==", + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", + "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==", "license": "MIT" }, "node_modules/@eslint-community/eslint-utils": { @@ -470,73 +725,56 @@ "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@intlify/core-base": { - "version": "11.4.4", - "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-11.4.4.tgz", - "integrity": "sha512-w/vItlylrAmhebkIbVl5YY8XMCtj8Mb2g70ttxktMYuf5AuRahgEHL2iLgLIsZBIbTSgs4hkUo7ucCL0uTJvOg==", + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, "license": "MIT", "dependencies": { - "@intlify/devtools-types": "11.4.4", - "@intlify/message-compiler": "11.4.4", - "@intlify/shared": "11.4.4" - }, - "engines": { - "node": ">= 22" - }, - "funding": { - "url": "https://github.com/sponsors/kazupon" + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" } }, - "node_modules/@intlify/devtools-types": { - "version": "11.4.4", - "resolved": "https://registry.npmjs.org/@intlify/devtools-types/-/devtools-types-11.4.4.tgz", - "integrity": "sha512-PcBLmGmDQsTSVV911P8upzpcLJO1CNVYi/IH6bGnLR2nA+0L963+kXN1ZrisTEnbtw2ewN6HMMSldqzjronA0Q==", + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, "license": "MIT", "dependencies": { - "@intlify/core-base": "11.4.4", - "@intlify/shared": "11.4.4" - }, - "engines": { - "node": ">= 22" - }, - "funding": { - "url": "https://github.com/sponsors/kazupon" + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" } }, - "node_modules/@intlify/message-compiler": { - "version": "11.4.4", - "resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-11.4.4.tgz", - "integrity": "sha512-vn0OAV9pYkJlPPmgnsSm5eAG3mL0+9C/oaded2JY9jmxBbhmUXT3TcAUY8WRgLY9Hte7lkUJKpXrVlYjMXBD2w==", - "license": "MIT", - "dependencies": { - "@intlify/shared": "11.4.4", - "source-map-js": "^1.0.2" - }, - "engines": { - "node": ">= 22" - }, - "funding": { - "url": "https://github.com/sponsors/kazupon" - } - }, - "node_modules/@intlify/shared": { - "version": "11.4.4", - "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-11.4.4.tgz", - "integrity": "sha512-QRUCHqda1U6aR14FR0vvXD4+4gj6+fm0AhAozvSuRCw0fCvrmCugWpgiR4xH2NI6s8am6N9p5OhirplsX8ZS3g==", + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, "license": "MIT", "engines": { - "node": ">= 22" - }, - "funding": { - "url": "https://github.com/sponsors/kazupon" + "node": ">=6.0.0" } }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, "license": "MIT" }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, "node_modules/@lezer/common": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.2.tgz", @@ -619,6 +857,703 @@ "url": "https://github.com/sponsors/Boshen" } }, + "node_modules/@rc-component/async-validator": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@rc-component/async-validator/-/async-validator-5.1.0.tgz", + "integrity": "sha512-n4HcR5siNUXRX23nDizbZBQPO0ZM/5oTtmKZ6/eqL0L2bo747cklFdZGRN2f+c9qWGICwDzrhW0H7tE9PptdcA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.24.4" + }, + "engines": { + "node": ">=14.x" + } + }, + "node_modules/@rc-component/cascader": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@rc-component/cascader/-/cascader-1.15.0.tgz", + "integrity": "sha512-ZzpMtwFCRo3fbXHuDnncARJMZQjdqA2w7aDuPofNQt+aDx39st1hgfIpEwTBLhe2Hqsvs/zOr8RTtgxTkCPySw==", + "license": "MIT", + "dependencies": { + "@rc-component/select": "~1.6.0", + "@rc-component/tree": "~1.3.0", + "@rc-component/util": "^1.4.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/checkbox": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@rc-component/checkbox/-/checkbox-2.0.0.tgz", + "integrity": "sha512-3CXGPpAR9gsPKeO2N78HAPOzU30UdemD6HGJoWVJOpa6WleaGB5kzZj3v6bdTZab31YuWgY/RxV3VKPctn0DwQ==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/collapse": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@rc-component/collapse/-/collapse-1.2.0.tgz", + "integrity": "sha512-ZRYSKSS39qsFx93p26bde7JUZJshsUBEQRlRXPuJYlAiNX0vyYlF5TsAm8JZN3LcF8XvKikdzPbgAtXSbkLUkw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "@rc-component/motion": "^1.1.4", + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/color-picker": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@rc-component/color-picker/-/color-picker-3.1.1.tgz", + "integrity": "sha512-OHaCHLHszCegdXmIq2ZRIZBN/EtpT6Wm8SG/gpzLATHbVKc/avvuKi+zlOuk05FTWvgaMmpxAko44uRJ3M+2pg==", + "license": "MIT", + "dependencies": { + "@ant-design/fast-color": "^3.0.1", + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/context": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@rc-component/context/-/context-2.0.1.tgz", + "integrity": "sha512-HyZbYm47s/YqtP6pKXNMjPEMaukyg7P0qVfgMLzr7YiFNMHbK2fKTAGzms9ykfGHSfyf75nBbgWw+hHkp+VImw==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.3.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/dialog": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@rc-component/dialog/-/dialog-1.9.0.tgz", + "integrity": "sha512-zbAAogkg4kkKum79sLE6M+vq1jSAW25zdkafrahgcTP9t9S//SD634Znd1A4c8F2Gc12ZKnehGLsVaaOvZzD2A==", + "license": "MIT", + "dependencies": { + "@rc-component/motion": "^1.1.3", + "@rc-component/portal": "^2.1.0", + "@rc-component/util": "^1.9.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/drawer": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@rc-component/drawer/-/drawer-1.4.2.tgz", + "integrity": "sha512-1ib+fZEp6FBu+YvcIktm+nCQ+Q+qIpwpoaJH6opGr4ofh2QMq+qdr5DLC4oCf5qf3pcWX9lUWPYX652k4ini8Q==", + "license": "MIT", + "dependencies": { + "@rc-component/motion": "^1.1.4", + "@rc-component/portal": "^2.1.3", + "@rc-component/util": "^1.9.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/dropdown": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rc-component/dropdown/-/dropdown-1.0.2.tgz", + "integrity": "sha512-6PY2ecUSYhDPhkNHHb4wfeAya04WhpmUSKzdR60G+kMNVUCX2vjT/AgTS0Lz0I/K6xrPMJ3enQbwVpeN3sHCgg==", + "license": "MIT", + "dependencies": { + "@rc-component/trigger": "^3.0.0", + "@rc-component/util": "^1.2.1", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.11.0", + "react-dom": ">=16.11.0" + } + }, + "node_modules/@rc-component/form": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@rc-component/form/-/form-1.8.1.tgz", + "integrity": "sha512-8O7TB55Fi2mWIGvSnwZjk8jFqVNYyKDAswglwGShcbndxqzKz4cHwNtNaLjZlAeRge9wcB0LL8IWsC/Bl18raQ==", + "license": "MIT", + "dependencies": { + "@rc-component/async-validator": "^5.1.0", + "@rc-component/util": "^1.6.2", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/image": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@rc-component/image/-/image-1.9.0.tgz", + "integrity": "sha512-khF7w7xkBH5B1bsBcI1FSUZdkyd1aqpl2eYyILCqCzzQH3XdfehGUaZTnptyaJJfs09/R5hv9jXWyazOMFIClQ==", + "license": "MIT", + "dependencies": { + "@rc-component/motion": "^1.0.0", + "@rc-component/portal": "^2.1.2", + "@rc-component/util": "^1.10.1", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/input": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@rc-component/input/-/input-1.3.0.tgz", + "integrity": "sha512-IUUNOdAuWuEvDEFFgfmwQl818tiDbvXwLgon4HL1q2hJeYkqrRrYwYhJN0zfPHGTDxs3gvyVC/C02D4hWFoIcA==", + "license": "MIT", + "dependencies": { + "@rc-component/resize-observer": "^1.1.1", + "@rc-component/util": "^1.4.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/@rc-component/input-number": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/@rc-component/input-number/-/input-number-1.6.2.tgz", + "integrity": "sha512-Gjcq7meZlCOiWN1t1xCC+7/s85humHVokTBI7PJgTfoyw5OWF74y3e6P8PHX104g9+b54jsodFIzyaj6p8LI9w==", + "license": "MIT", + "dependencies": { + "@rc-component/mini-decimal": "^1.0.1", + "@rc-component/util": "^1.4.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/mentions": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@rc-component/mentions/-/mentions-1.9.0.tgz", + "integrity": "sha512-WUwfFKDSOF5S9UPsNsXcLYtzjTxBGsftTXWRbZuxX6BYrsySISTnujfJNgaaQ6qVzaCDJ35QUkZKvsYxip1C5g==", + "license": "MIT", + "dependencies": { + "@rc-component/input": "~1.3.0", + "@rc-component/menu": "~1.3.0", + "@rc-component/trigger": "^3.0.0", + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/menu": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@rc-component/menu/-/menu-1.3.0.tgz", + "integrity": "sha512-u3NfiwpiEgT177qa5Yxm5QsI8i/93EBGpWj8HYZQDnh2pCZ2xtQCe/+w3pSR2NlwKOZDTCKzEhEyD09mGphssA==", + "license": "MIT", + "dependencies": { + "@rc-component/motion": "^1.1.4", + "@rc-component/overflow": "^1.0.0", + "@rc-component/trigger": "^3.0.0", + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/mini-decimal": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rc-component/mini-decimal/-/mini-decimal-1.1.3.tgz", + "integrity": "sha512-bk/FJ09fLf+NLODMAFll6CfYrHPBioTedhW6lxDBuuWucJEqFUd4l/D/5JgIi3dina6sYahB8iuPAZTNz2pMxw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.0" + }, + "engines": { + "node": ">=8.x" + } + }, + "node_modules/@rc-component/motion": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@rc-component/motion/-/motion-1.3.2.tgz", + "integrity": "sha512-itfd+GztzJYAb04Z4RkEub1TbJAfZc2Iuy8p44U44xD1F5+fNYFKI3897ijlbIyfvXkTmMm+KGcjkQQGMHywEQ==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.2.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/mutate-observer": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@rc-component/mutate-observer/-/mutate-observer-2.0.1.tgz", + "integrity": "sha512-AyarjoLU5YlxuValRi+w8JRH2Z84TBbFO2RoGWz9d8bSu0FqT8DtugH3xC3BV7mUwlmROFauyWuXFuq4IFbH+w==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.2.0" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/notification": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@rc-component/notification/-/notification-2.0.7.tgz", + "integrity": "sha512-nqZzpf6BPdaj+3ILx7si79LLmqPKyUmQoXa+/9gg0SkH0v1DbD66oJgRMSBEVnd/zUT3D4gwxWIHUKebYf2ZXQ==", + "license": "MIT", + "dependencies": { + "@rc-component/motion": "^1.1.4", + "@rc-component/util": "^1.11.0", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/overflow": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rc-component/overflow/-/overflow-1.0.1.tgz", + "integrity": "sha512-syfmgAABaHCnCDzPwHZ/2tuvIcpOO3jefYZMmfkN+pmo8HKTzsfhS57vxo4ksPdN0By+uWVJhJWNFozNBxi2eA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.1", + "@rc-component/resize-observer": "^1.0.1", + "@rc-component/util": "^1.4.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/pagination": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@rc-component/pagination/-/pagination-1.2.0.tgz", + "integrity": "sha512-YcpUFE8dMLfSo6OARJlK6DbHHvrxz7pMGPGmC/caZSJJz6HRKHC1RPP001PRHCvG9Z/veD039uOQmazVuLJzlw==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/picker": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@rc-component/picker/-/picker-1.10.0.tgz", + "integrity": "sha512-vVOXP2RVWozwpERGUFAehVH1Jz6o/uRrAb9qSZm1LC+iJs8rvEwFo1bzz2jlOYV+uWwu0dIuG86tnDui14Ea0w==", + "license": "MIT", + "dependencies": { + "@rc-component/overflow": "^1.0.0", + "@rc-component/resize-observer": "^1.0.0", + "@rc-component/trigger": "^3.6.15", + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=12.x" + }, + "peerDependencies": { + "date-fns": ">= 2.x", + "dayjs": ">= 1.x", + "luxon": ">= 3.x", + "moment": ">= 2.x", + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + }, + "peerDependenciesMeta": { + "date-fns": { + "optional": true + }, + "dayjs": { + "optional": true + }, + "luxon": { + "optional": true + }, + "moment": { + "optional": true + } + } + }, + "node_modules/@rc-component/portal": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@rc-component/portal/-/portal-2.2.0.tgz", + "integrity": "sha512-oc6FlA+uXCMiwArHsJyHcIkX4q6uKyndrPol2eWX8YPkAnztHOPsFIRtmWG4BMlGE5h7YIRE3NiaJ5VS8Lb1QQ==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.2.1", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=12.x" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/progress": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rc-component/progress/-/progress-1.0.2.tgz", + "integrity": "sha512-WZUnH9eGxH1+xodZKqdrHke59uyGZSWgj5HBM5Kwk5BrTMuAORO7VJ2IP5Qbm9aH3n9x3IcesqHHR0NWPBC7fQ==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.2.1", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/qrcode": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@rc-component/qrcode/-/qrcode-1.1.1.tgz", + "integrity": "sha512-LfLGNymzKdUPjXUbRP+xOhIWY4jQ+YMj5MmWAcgcAq1Ij8XP7tRmAXqyuv96XvLUBE/5cA8hLFl9eO1JQMujrA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.24.7" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/rate": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rc-component/rate/-/rate-1.0.1.tgz", + "integrity": "sha512-bkXxeBqDpl5IOC7yL7GcSYjQx9G8H+6kLYQnNZWeBYq2OYIv1MONd6mqKTjnnJYpV0cQIU2z3atdW0j1kttpTw==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/resize-observer": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@rc-component/resize-observer/-/resize-observer-1.1.2.tgz", + "integrity": "sha512-t/Bb0W8uvL4PYKAB3YcChC+DlHh0Wt5kM7q/J+0qpVEUMLe7Hk5zuvc9km0hMnTFPSx5Z7Wu/fzCLN6erVLE8Q==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.2.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/segmented": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@rc-component/segmented/-/segmented-1.3.0.tgz", + "integrity": "sha512-5J/bJ01mbDnoA6P/FW8SxUvKn+OgUSTZJPzCNnTBntG50tzoP7DydGhqxp7ggZXZls7me3mc2EQDXakU3iTVFg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.1", + "@rc-component/motion": "^1.1.4", + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/@rc-component/select": { + "version": "1.6.15", + "resolved": "https://registry.npmjs.org/@rc-component/select/-/select-1.6.15.tgz", + "integrity": "sha512-SyVCWnqxCQZZcQvQJ/CxSjx2bGma6ds/HtnpkIfZVnt6RoEgbqUmHgD6vrzNarNXwbLXerwVzWwq8F3d1sst7g==", + "license": "MIT", + "dependencies": { + "@rc-component/overflow": "^1.0.0", + "@rc-component/trigger": "^3.0.0", + "@rc-component/util": "^1.3.0", + "@rc-component/virtual-list": "^1.0.1", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, + "node_modules/@rc-component/slider": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rc-component/slider/-/slider-1.0.1.tgz", + "integrity": "sha512-uDhEPU1z3WDfCJhaL9jfd2ha/Eqpdfxsn0Zb0Xcq1NGQAman0TWaR37OWp2vVXEOdV2y0njSILTMpTfPV1454g==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/steps": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@rc-component/steps/-/steps-1.2.2.tgz", + "integrity": "sha512-/yVIZ00gDYYPHSY0JP+M+s3ZvuXLu2f9rEjQqiUDs7EcYsUYrpJ/1bLj9aI9R7MBR3fu/NGh6RM9u2qGfqp+Nw==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.2.1", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/switch": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rc-component/switch/-/switch-1.0.3.tgz", + "integrity": "sha512-Jgi+EbOBquje/XNdofr7xbJQZPYJP+BlPfR0h+WN4zFkdtB2EWqEfvkXJWeipflwjWip0/17rNbxEAqs8hVHfw==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/table": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@rc-component/table/-/table-1.10.0.tgz", + "integrity": "sha512-SjtpcCf+rL7dDc62GKT3rXTdERjVuJvRiqjpU7g0Jc/ewCifXynHc7Nm3Em1XsD+WhGrgQtxNDScI/0+Lpfr0w==", + "license": "MIT", + "dependencies": { + "@rc-component/context": "^2.0.1", + "@rc-component/resize-observer": "^1.0.0", + "@rc-component/util": "^1.1.0", + "@rc-component/virtual-list": "^1.0.1", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/tabs": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@rc-component/tabs/-/tabs-1.9.0.tgz", + "integrity": "sha512-tn1slmbbaTyt8mgwyWJcT8jo/qNiYUs6u1H7OgGQt9faYO06BJIkU5cTmMqORzIrNmSEeeUY6pD5i+JlqSHYhg==", + "license": "MIT", + "dependencies": { + "@rc-component/dropdown": "~1.0.0", + "@rc-component/menu": "~1.3.0", + "@rc-component/motion": "^1.1.3", + "@rc-component/resize-observer": "^1.0.0", + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/tooltip": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@rc-component/tooltip/-/tooltip-1.4.0.tgz", + "integrity": "sha512-8Rx5DCctIlLI4raR0I0xHjVTf1aF48+gKCNeAAo5bmF5VoR5YED+A/XEqzXv9KKqrJDRcd3Wndpxh2hyzrTtSg==", + "license": "MIT", + "dependencies": { + "@rc-component/trigger": "^3.7.1", + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/tour": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@rc-component/tour/-/tour-2.4.0.tgz", + "integrity": "sha512-aui4r4TqmTzwaBgcQxHYep8kM8PTjZFufjokObpy35KfFeZ0k9ArquWFZqegQlH24P14t+F0qO0mGTgzlav1yg==", + "license": "MIT", + "dependencies": { + "@rc-component/portal": "^2.2.0", + "@rc-component/trigger": "^3.0.0", + "@rc-component/util": "^1.7.0", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/tree": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@rc-component/tree/-/tree-1.3.1.tgz", + "integrity": "sha512-zlL0PW0bTFlveTtLcA01VD/yMWKK73EywItFMgIZUY5sb6tMOAw7zV6qGzqldufqrV93ZWQB4H3NBNoTMCueJA==", + "license": "MIT", + "dependencies": { + "@rc-component/motion": "^1.0.0", + "@rc-component/util": "^1.8.1", + "@rc-component/virtual-list": "^1.0.1", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=10.x" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, + "node_modules/@rc-component/tree-select": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@rc-component/tree-select/-/tree-select-1.9.0.tgz", + "integrity": "sha512-GXcFe15a+trUl1/J3OHWQhsVWFpwFpGFK2cqYWZ1sK22Zs3KZTvMwDpzr75PIo1s6QVioVxpE/pRwRopkeDQ6w==", + "license": "MIT", + "dependencies": { + "@rc-component/select": "~1.6.0", + "@rc-component/tree": "~1.3.0", + "@rc-component/util": "^1.4.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, + "node_modules/@rc-component/trigger": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/@rc-component/trigger/-/trigger-3.9.0.tgz", + "integrity": "sha512-X8btpwfrT27AgrZVOz4swclhEHTZcqaHeQMXXBgveagOiakTa36uObXbdwerXffgV8G9dH1fAAE0DHtVQs8EHg==", + "license": "MIT", + "dependencies": { + "@rc-component/motion": "^1.1.4", + "@rc-component/portal": "^2.2.0", + "@rc-component/resize-observer": "^1.1.1", + "@rc-component/util": "^1.2.1", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/upload": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rc-component/upload/-/upload-1.1.0.tgz", + "integrity": "sha512-LIBV90mAnUE6VK5N4QvForoxZc4XqEYZimcp7fk+lkE4XwHHyJWxpIXQQwMU8hJM+YwBbsoZkGksL1sISWHQxw==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/util": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@rc-component/util/-/util-1.11.1.tgz", + "integrity": "sha512-awVlI3ub2vqfqkYxOBc/uQ0efm3jw0wcrhtO/YWLyZfxiKXczKwNbVuhlnyxytDt7H9pbbVQiqr+O6MLATtRYg==", + "license": "MIT", + "dependencies": { + "is-mobile": "^5.0.0", + "react-is": "^18.2.0" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/virtual-list": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@rc-component/virtual-list/-/virtual-list-1.2.0.tgz", + "integrity": "sha512-iavRm1Jo4GDbASQwdGa7jFyk93RvSOo9xHyBT4QL1pgFJj/Fdf1G+3RErH7/7BmAMvx2AkF62mjGYxDbXsK9TQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.20.0", + "@rc-component/resize-observer": "^1.0.1", + "@rc-component/util": "^1.4.0", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, "node_modules/@rolldown/binding-android-arm64": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.1.tgz", @@ -901,16 +1836,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@simonwep/pickr": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/@simonwep/pickr/-/pickr-1.8.2.tgz", - "integrity": "sha512-/l5w8BIkrpP6n1xsetx9MWPWlU6OblN5YgZZphxan0Tq4BByTCETL6lyIeY8lagalS2Nbt4F2W034KHLIiunKA==", - "license": "MIT", - "dependencies": { - "core-js": "^3.15.1", - "nanopop": "^2.1.0" - } - }, "node_modules/@tybys/wasm-util": { "version": "0.10.2", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", @@ -943,129 +1868,295 @@ "dev": true, "license": "MIT" }, - "node_modules/@vitejs/plugin-vue": { - "version": "6.0.7", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.7.tgz", - "integrity": "sha512-km+p+XdSz9Sxm5rqUbqcSfZYaAniKxWBj1KURl+Jr7UaPvvX7BmaWMdP69I5rrFDeQGyxAG7NXdc57vz+snhWg==", + "node_modules/@types/react": { + "version": "19.2.15", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.15.tgz", + "integrity": "sha512-eRwcGNHve+E8qtEQSSRl6urh+rFop4v8gm6O8rGv25CodbvFdLjA1vVQ1KkiFE0w0UPOnb8tDiFKL5lp0rtY5Q==", "dev": true, "license": "MIT", "dependencies": { - "@rolldown/pluginutils": "^1.0.1" + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.4.tgz", + "integrity": "sha512-PegsU+XfyJJNjd4+u/k6f9yTyp0lEXXiPopUNobZcIAUJFGICFLN+sP0Rb3JehVmiij1Ph0dFGYqODoRo/2+6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.59.4", + "@typescript-eslint/type-utils": "8.59.4", + "@typescript-eslint/utils": "8.59.4", + "@typescript-eslint/visitor-keys": "8.59.4", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.59.4", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.4.tgz", + "integrity": "sha512-zORHqO/tuhxY1zWuTvMUqddRxpiFJ72xVfcNoWpqdLjs6lfPbuQBJuW4pk+49/uBMy7Ssr4bzgjiKmmDB1UbZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.59.4", + "@typescript-eslint/types": "8.59.4", + "@typescript-eslint/typescript-estree": "8.59.4", + "@typescript-eslint/visitor-keys": "8.59.4", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.4.tgz", + "integrity": "sha512-Ly00Vu4oAacfDeHp2Zg85ioNG6l8HG+tN1D7J+xTHSxu9y0awYKJ2zH1rFBn8ZSfuGK+7FxK3Cgl3uAz0aZZLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.59.4", + "@typescript-eslint/types": "^8.59.4", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.4.tgz", + "integrity": "sha512-mUeR/3H1WrTAddJrwut8OoPjfauaztMQmRwV5fQTUyNVJCLiUXXe4lGEyYIL2oFDpP7UtgbGJXCt72wT0z2S3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.4", + "@typescript-eslint/visitor-keys": "8.59.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.4.tgz", + "integrity": "sha512-DLCpnKgD4alVxTBSKulK+gU1KCqOgUXfDRDXh2mZgzokQKa/70ax93I2uVO3m/LLvIAtWZIFoiifudmIqAxpMA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.4.tgz", + "integrity": "sha512-uonTuPAAKr9XaBGqJ3LjYTh72zy5DyGesljO9gtmk/eFW0W1fRHjnwVYKB35Lm8d5Q5CluEW3gPHjTvZTmgrfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.4", + "@typescript-eslint/typescript-estree": "8.59.4", + "@typescript-eslint/utils": "8.59.4", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.4.tgz", + "integrity": "sha512-F1o7WJcCq+bc8dwcO/YsSEOudAH8RDtaOhM6wcAQhcUsFhnWQl81JKy48q1hoxAU0qrzM89+31GYh1515Zde3Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.4.tgz", + "integrity": "sha512-F+RuOmcDXo4+TPdfd/TCLS3m2nw8gE9XXyZLrA3JBfaA5tz9TtdkyD3YJFmPxulyc2cKbEok/CvFE3MgSLWnag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.59.4", + "@typescript-eslint/tsconfig-utils": "8.59.4", + "@typescript-eslint/types": "8.59.4", + "@typescript-eslint/visitor-keys": "8.59.4", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", + "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.4.tgz", + "integrity": "sha512-cYXeNAUsG4lJo5dbc1FcKm+JwIWrj1/UpTORsC6tGMjEZ81DYcvIr9/ueikhMa/Y/gDQYGp+YX9/xQrXje5BJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.59.4", + "@typescript-eslint/types": "8.59.4", + "@typescript-eslint/typescript-estree": "8.59.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.4.tgz", + "integrity": "sha512-U3gxVaDVnuZKhSspW/MzMxE1kq7zOdc072FcSNoqA1I9p8HyKbBFfEHoWckBAMgNMph4MamwS5iTVzFmrnt8TQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.4", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.2.tgz", + "integrity": "sha512-DlSMqo4WhThw4vB8Mpn0Woe9J+Jfq1geJ61AKW0QEgLzGMNwtIMdxbDUzLxcun8W7NbJO0e2Jg/Nxm3cCSVzzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.0" }, "engines": { "node": "^20.19.0 || >=22.12.0" }, "peerDependencies": { - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0", - "vue": "^3.2.25" - } - }, - "node_modules/@vue/compiler-core": { - "version": "3.5.34", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.34.tgz", - "integrity": "sha512-s9cLyK5mLcvZ4Agva5QgRsQyLKvts9WbU9DB6NqiZkkGEdwmcEiylj5Jbwkp680drF/NNCV8OlAJSe+yMLxaJw==", - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.29.3", - "@vue/shared": "3.5.34", - "entities": "^7.0.1", - "estree-walker": "^2.0.2", - "source-map-js": "^1.2.1" - } - }, - "node_modules/@vue/compiler-dom": { - "version": "3.5.34", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.34.tgz", - "integrity": "sha512-EbF/T++k0e2MMZlJsBhzK8Sgwt0HcIPOhzn1CTB/lv6sQcyk+OWf8YeiLxZp3ro7MbbLcAfAJ6sEvjFWuNgUCw==", - "license": "MIT", - "dependencies": { - "@vue/compiler-core": "3.5.34", - "@vue/shared": "3.5.34" - } - }, - "node_modules/@vue/compiler-sfc": { - "version": "3.5.34", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.34.tgz", - "integrity": "sha512-D/ihr6uZeIt6r+pVZf46RWT1fAsLFMbUP7k8G1VkiiWexriED9GrX3echHd4Abbt17zjlfiFJ8z7a3BxZOPNjg==", - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.29.3", - "@vue/compiler-core": "3.5.34", - "@vue/compiler-dom": "3.5.34", - "@vue/compiler-ssr": "3.5.34", - "@vue/shared": "3.5.34", - "estree-walker": "^2.0.2", - "magic-string": "^0.30.21", - "postcss": "^8.5.14", - "source-map-js": "^1.2.1" - } - }, - "node_modules/@vue/compiler-ssr": { - "version": "3.5.34", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.34.tgz", - "integrity": "sha512-cDtTHKibkThKGHH1SP+WdccquNRYQDFH6rRjQCqT9G2ltFAfoR5pUftpab/z+aM5mW9HLLVQW7hfKKQe/1GBeQ==", - "license": "MIT", - "dependencies": { - "@vue/compiler-dom": "3.5.34", - "@vue/shared": "3.5.34" - } - }, - "node_modules/@vue/devtools-api": { - "version": "6.6.4", - "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz", - "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", - "license": "MIT" - }, - "node_modules/@vue/reactivity": { - "version": "3.5.34", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.34.tgz", - "integrity": "sha512-y9XDjCEuBp+98k+UL5dbYkh57AHU4o6cxZedOPXw3bmrZZYLQsVHguGurq7hVrPCSrQtrnz1f9dssyFr+dMXfQ==", - "license": "MIT", - "dependencies": { - "@vue/shared": "3.5.34" - } - }, - "node_modules/@vue/runtime-core": { - "version": "3.5.34", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.34.tgz", - "integrity": "sha512-mKeBYvu8tcMSLhypAHBmriUFfWXKTCF/23Z4jiCoYK3UtWepkliViNLuR90V9XOyD62mUxs9p1jsrpK3CCGIzw==", - "license": "MIT", - "dependencies": { - "@vue/reactivity": "3.5.34", - "@vue/shared": "3.5.34" - } - }, - "node_modules/@vue/runtime-dom": { - "version": "3.5.34", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.34.tgz", - "integrity": "sha512-e8kZzERmCwUnBRVsgSQlAfrfU2rGoy0FFKPBXSlfEjc/O3KfA7QP0t1/2ZylrbchjmIKB4dPTd07A6WPr0eOrg==", - "license": "MIT", - "dependencies": { - "@vue/reactivity": "3.5.34", - "@vue/runtime-core": "3.5.34", - "@vue/shared": "3.5.34", - "csstype": "^3.2.3" - } - }, - "node_modules/@vue/server-renderer": { - "version": "3.5.34", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.34.tgz", - "integrity": "sha512-nHxmJoTrKsmrkbILRhkC9gY1G3moZbJTqCzDd7DOOzG5KH9oeJ0Unqrff5f9v0pW//jES05ZkJcNtfE8JjOIew==", - "license": "MIT", - "dependencies": { - "@vue/compiler-ssr": "3.5.34", - "@vue/shared": "3.5.34" + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "vite": "^8.0.0" }, - "peerDependencies": { - "vue": "3.5.34" + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + } } }, - "node_modules/@vue/shared": { - "version": "3.5.34", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.34.tgz", - "integrity": "sha512-24uqU4OIiX29ryC3MeWid/Xf2fa2EFRUVLb77nRhk+UrTVrh/XiGtFAFmJBAtBRbjwNdsPRP+jj/OL27Eg1NDA==", - "license": "MIT" - }, "node_modules/acorn": { "version": "8.16.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", @@ -1118,58 +2209,69 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/ant-design-vue": { - "version": "4.2.6", - "resolved": "https://registry.npmjs.org/ant-design-vue/-/ant-design-vue-4.2.6.tgz", - "integrity": "sha512-t7eX13Yj3i9+i5g9lqFyYneoIb3OzTvQjq9Tts1i+eiOd3Eva/6GagxBSXM1fOCjqemIu0FYVE1ByZ/38epR3Q==", + "node_modules/antd": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/antd/-/antd-6.4.3.tgz", + "integrity": "sha512-6H2avkxCGfxcF67r3J2mwm9Ck50el1pks/73vfM1wDsPL/tPtj5vHuauMgJFnrqmq7CH3g8aoZ0VBQbt+jpAsw==", "license": "MIT", "dependencies": { - "@ant-design/colors": "^6.0.0", - "@ant-design/icons-vue": "^7.0.0", - "@babel/runtime": "^7.10.5", - "@ctrl/tinycolor": "^3.5.0", - "@emotion/hash": "^0.9.0", - "@emotion/unitless": "^0.8.0", - "@simonwep/pickr": "~1.8.0", - "array-tree-filter": "^2.1.0", - "async-validator": "^4.0.0", - "csstype": "^3.1.1", - "dayjs": "^1.10.5", - "dom-align": "^1.12.1", - "dom-scroll-into-view": "^2.0.0", - "lodash": "^4.17.21", - "lodash-es": "^4.17.15", - "resize-observer-polyfill": "^1.5.1", - "scroll-into-view-if-needed": "^2.2.25", - "shallow-equal": "^1.0.0", - "stylis": "^4.1.3", - "throttle-debounce": "^5.0.0", - "vue-types": "^3.0.0", - "warning": "^4.0.0" - }, - "engines": { - "node": ">=12.22.0" + "@ant-design/colors": "^8.0.1", + "@ant-design/cssinjs": "^2.1.2", + "@ant-design/cssinjs-utils": "^2.1.2", + "@ant-design/fast-color": "^3.0.1", + "@ant-design/icons": "^6.2.3", + "@ant-design/react-slick": "~2.0.0", + "@babel/runtime": "^7.29.2", + "@rc-component/cascader": "~1.15.0", + "@rc-component/checkbox": "~2.0.0", + "@rc-component/collapse": "~1.2.0", + "@rc-component/color-picker": "~3.1.1", + "@rc-component/dialog": "~1.9.0", + "@rc-component/drawer": "~1.4.2", + "@rc-component/dropdown": "~1.0.2", + "@rc-component/form": "~1.8.1", + "@rc-component/image": "~1.9.0", + "@rc-component/input": "~1.3.0", + "@rc-component/input-number": "~1.6.2", + "@rc-component/mentions": "~1.9.0", + "@rc-component/menu": "~1.3.0", + "@rc-component/motion": "^1.3.2", + "@rc-component/mutate-observer": "^2.0.1", + "@rc-component/notification": "~2.0.7", + "@rc-component/pagination": "~1.2.0", + "@rc-component/picker": "~1.10.0", + "@rc-component/progress": "~1.0.2", + "@rc-component/qrcode": "~1.1.1", + "@rc-component/rate": "~1.0.1", + "@rc-component/resize-observer": "^1.1.2", + "@rc-component/segmented": "~1.3.0", + "@rc-component/select": "~1.6.15", + "@rc-component/slider": "~1.0.1", + "@rc-component/steps": "~1.2.2", + "@rc-component/switch": "~1.0.3", + "@rc-component/table": "~1.10.0", + "@rc-component/tabs": "~1.9.0", + "@rc-component/tooltip": "~1.4.0", + "@rc-component/tour": "~2.4.0", + "@rc-component/tree": "~1.3.1", + "@rc-component/tree-select": "~1.9.0", + "@rc-component/trigger": "^3.9.0", + "@rc-component/upload": "~1.1.0", + "@rc-component/util": "^1.11.0", + "clsx": "^2.1.1", + "dayjs": "^1.11.11", + "scroll-into-view-if-needed": "^3.1.0", + "throttle-debounce": "^5.0.2" }, "funding": { "type": "opencollective", - "url": "https://opencollective.com/ant-design-vue" + "url": "https://opencollective.com/ant-design" }, "peerDependencies": { - "vue": ">=3.2.0" + "react": ">=18.0.0", + "react-dom": ">=18.0.0" } }, - "node_modules/array-tree-filter": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-tree-filter/-/array-tree-filter-2.1.0.tgz", - "integrity": "sha512-4ROwICNlNw/Hqa9v+rk5h22KjmzB1JGTMVKP2AKJBOCgb0yL0ASf0+YvCcLNNwquOHNX48jkeZIJ3a+oOQqKcw==", - "license": "MIT" - }, - "node_modules/async-validator": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/async-validator/-/async-validator-4.2.5.tgz", - "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==", - "license": "MIT" - }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -1198,12 +2300,18 @@ "node": "18 || 20 || >=22" } }, - "node_modules/boolbase": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "node_modules/baseline-browser-mapping": { + "version": "2.10.31", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.31.tgz", + "integrity": "sha512-MujYO3eP72uvmSE0i4wltsodRfIpZATP3jvzRNRGGxgzId7aVocVJJV3nf01qnzzKFGxQVC9bpWxl5cjxTr/7Q==", "dev": true, - "license": "ISC" + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } }, "node_modules/brace-expansion": { "version": "5.0.6", @@ -1218,6 +2326,40 @@ "node": "18 || 20 || >=22" } }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -1247,6 +2389,36 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/caniuse-lite": { + "version": "1.0.30001793", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz", + "integrity": "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/codemirror": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.2.tgz", @@ -1275,21 +2447,17 @@ } }, "node_modules/compute-scroll-into-view": { - "version": "1.0.20", - "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-1.0.20.tgz", - "integrity": "sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-3.1.1.tgz", + "integrity": "sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==", "license": "MIT" }, - "node_modules/core-js": { - "version": "3.49.0", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.49.0.tgz", - "integrity": "sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==", - "hasInstallScript": true, - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" - } + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" }, "node_modules/crelt": { "version": "1.0.6", @@ -1312,19 +2480,6 @@ "node": ">= 8" } }, - "node_modules/cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "dev": true, - "license": "MIT", - "bin": { - "cssesc": "bin/cssesc" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", @@ -1380,18 +2535,6 @@ "node": ">=8" } }, - "node_modules/dom-align": { - "version": "1.12.4", - "resolved": "https://registry.npmjs.org/dom-align/-/dom-align-1.12.4.tgz", - "integrity": "sha512-R8LUSEay/68zE5c8/3BDxiTEvgb4xZTF0RKmAHfiEVN3klfIpXfi2/QCoiWPccVQ0J/ZGdz9OjzL4uJEP/MRAw==", - "license": "MIT" - }, - "node_modules/dom-scroll-into-view": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/dom-scroll-into-view/-/dom-scroll-into-view-2.0.1.tgz", - "integrity": "sha512-bvVTQe1lfaUr1oFzZX80ce9KLDlZ3iU+XGNE/bz9HnGdklTieqsbmsLHe+rT2XWqopvL0PckkYqN7ksmm5pe3w==", - "license": "MIT" - }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -1406,17 +2549,12 @@ "node": ">= 0.4" } }, - "node_modules/entities": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", - "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } + "node_modules/electron-to-chromium": { + "version": "1.5.361", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.361.tgz", + "integrity": "sha512-Q6Hts7N9FnJc5LeGRINFvLhCI9xZmNtTDe5ZbcVezQz7cU4a8Aua3GH1b8J2XY8Al9PF+OCwYqhgsOOheMdvkA==", + "dev": true, + "license": "ISC" }, "node_modules/es-define-property": { "version": "1.0.1", @@ -1463,6 +2601,16 @@ "node": ">= 0.4" } }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -1532,36 +2680,24 @@ } } }, - "node_modules/eslint-plugin-vue": { - "version": "10.9.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-10.9.1.tgz", - "integrity": "sha512-cHB0Tf4Duvzwecwd/AqWzZvF/QszE13BhjVUpVXWCy9AeMR5GjkAjP3i85vqgLgOuTmkHR1OJ5oMeqLHtuw8zg==", + "node_modules/eslint-plugin-react-hooks": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", + "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "natural-compare": "^1.4.0", - "nth-check": "^2.1.1", - "postcss-selector-parser": "^7.1.0", - "semver": "^7.6.3", - "xml-name-validator": "^4.0.0" + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=18" }, "peerDependencies": { - "@stylistic/eslint-plugin": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0", - "@typescript-eslint/parser": "^7.0.0 || ^8.0.0", - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "vue-eslint-parser": "^10.3.0" - }, - "peerDependenciesMeta": { - "@stylistic/eslint-plugin": { - "optional": true - }, - "@typescript-eslint/parser": { - "optional": true - } + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" } }, "node_modules/eslint-scope": { @@ -1650,12 +2786,6 @@ "node": ">=4.0" } }, - "node_modules/estree-walker": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", - "license": "MIT" - }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -1816,6 +2946,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -1930,6 +3070,32 @@ "node": ">= 0.4" } }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "license": "MIT", + "dependencies": { + "void-elements": "3.1.0" + } + }, "node_modules/https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", @@ -1943,6 +3109,34 @@ "node": ">= 6" } }, + "node_modules/i18next": { + "version": "26.2.0", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-26.2.0.tgz", + "integrity": "sha512-zwBHldHdTmwN7r6UNc7lC6GWNN+YYg3DrRSeHR5PRRBf5QnJZcYHrQc0uaU26qZeYxR7iFZD+Y315dPnKP47wA==", + "funding": [ + { + "type": "individual", + "url": "https://www.locize.com/i18next" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + }, + { + "type": "individual", + "url": "https://www.locize.com" + } + ], + "license": "MIT", + "peerDependencies": { + "typescript": "^5 || ^6" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -1986,14 +3180,11 @@ "node": ">=0.10.0" } }, - "node_modules/is-plain-object": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-3.0.1.tgz", - "integrity": "sha512-Xnpx182SBMrr/aBik8y+GuR4U1L9FqMSojwDQwPMmxyC6bvEqly9UBCxhauBF5vNh2gwWJNX6oDV7O+OM4z34g==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } + "node_modules/is-mobile": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-mobile/-/is-mobile-5.0.0.tgz", + "integrity": "sha512-Tz/yndySvLAEXh+Uk8liFCxOwVH6YutuR74utvOcu7I9Di+DwM0mtdPVZNaVvvBUM2OXxne/NhOs1zAO7riusQ==", + "license": "MIT" }, "node_modules/isexe": { "version": "2.0.0", @@ -2002,18 +3193,26 @@ "dev": true, "license": "ISC" }, - "node_modules/jalaali-js": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/jalaali-js/-/jalaali-js-1.2.8.tgz", - "integrity": "sha512-Jl/EwY84JwjW2wsWqeU4pNd22VNQ7EkjI36bDuLw31wH98WQW4fPjD0+mG7cdCK+Y8D6s9R3zLiQ3LaKu6bD8A==", - "license": "MIT" - }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, "license": "MIT" }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -2035,6 +3234,28 @@ "dev": true, "license": "MIT" }, + "node_modules/json2mq": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/json2mq/-/json2mq-0.2.0.tgz", + "integrity": "sha512-SzoRg7ux5DWTII9J2qkrZrqV1gt+rTaoufMxEzXbS26Uid0NwaJd123HcoB80TgubEppxxIGdNxCx50fEoEWQA==", + "license": "MIT", + "dependencies": { + "string-convert": "^0.2.0" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -2348,37 +3569,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lodash": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", - "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", - "license": "MIT" - }, - "node_modules/lodash-es": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", - "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", - "license": "MIT" - }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "license": "MIT", + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, - "node_modules/magic-string": { - "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" + "yallist": "^3.0.2" } }, "node_modules/math-intrinsics": { @@ -2427,38 +3625,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/moment": { - "version": "2.30.1", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", - "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/moment-jalaali": { - "version": "0.10.4", - "resolved": "https://registry.npmjs.org/moment-jalaali/-/moment-jalaali-0.10.4.tgz", - "integrity": "sha512-/eD0HeyvATznb5iE0G1BHjKRZAFEpJ9ZNUkcHwXhNgt1WJJVVzHD7+uDmqzZWVFLdbGme2gvIXKb3ezDYOXcZA==", - "license": "MIT", - "dependencies": { - "jalaali-js": "^1.2.7", - "moment": "^2.29.4", - "moment-timezone": "^0.5.46" - } - }, - "node_modules/moment-timezone": { - "version": "0.5.48", - "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.48.tgz", - "integrity": "sha512-f22b8LV1gbTO2ms2j2z13MuPogNoh5UzxL3nzNAYKGraILnbGc9NEE6dyiiiLv46DGRb8A4kg8UKWLjPthxBHw==", - "license": "MIT", - "dependencies": { - "moment": "^2.29.4" - }, - "engines": { - "node": "*" - } - }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -2469,6 +3635,7 @@ "version": "3.3.12", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, "funding": [ { "type": "github", @@ -2483,12 +3650,6 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "node_modules/nanopop": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/nanopop/-/nanopop-2.4.2.tgz", - "integrity": "sha512-NzOgmMQ+elxxHeIha+OG/Pv3Oc3p4RU2aBhwWwAqDpXrdTbtRylbRLQztLy8dMMwfl6pclznBdfUhccEn9ZIzw==", - "license": "MIT" - }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -2496,17 +3657,14 @@ "dev": true, "license": "MIT" }, - "node_modules/nth-check": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", - "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "node_modules/node-releases": { + "version": "2.0.46", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.46.tgz", + "integrity": "sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ==", "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "boolbase": "^1.0.0" - }, - "funding": { - "url": "https://github.com/fb55/nth-check?sponsor=1" + "license": "MIT", + "engines": { + "node": ">=18" } }, "node_modules/object-inspect": { @@ -2603,10 +3761,20 @@ "node": ">=8" } }, + "node_modules/persian-calendar-suite": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/persian-calendar-suite/-/persian-calendar-suite-1.5.5.tgz", + "integrity": "sha512-KJSzN9q7MZKhfkm97X/j+nD6L0AQ5coUq/B7PpIklXAvRjkALwiV+KmYG0pfr546EQxO9l4fBwE7R1HPI3yT7w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, "license": "ISC" }, "node_modules/picomatch": { @@ -2626,6 +3794,7 @@ "version": "8.5.15", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, "funding": [ { "type": "opencollective", @@ -2650,20 +3819,6 @@ "node": "^10 || ^12 || >=14" } }, - "node_modules/postcss-selector-parser": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", - "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -2708,10 +3863,58 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/resize-observer-polyfill": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", - "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==", + "node_modules/react": { + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz", + "integrity": "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.6.tgz", + "integrity": "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.6" + } + }, + "node_modules/react-i18next": { + "version": "17.0.8", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-17.0.8.tgz", + "integrity": "sha512-0ooKbGLU8JXhe1zwpQUWIeXSgLPOfwJmgheWRIUpcoA0CpyabpGhayjdG+/eA5esC1AQ8h2jWpXjJfzQzeDOCw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.2", + "html-parse-stringify": "^3.0.1", + "use-sync-external-store": "^1.6.0" + }, + "peerDependencies": { + "i18next": ">= 26.2.0", + "react": ">= 16.8.0", + "typescript": "^5 || ^6" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "license": "MIT" }, "node_modules/rolldown": { @@ -2748,34 +3951,31 @@ "@rolldown/binding-win32-x64-msvc": "1.0.1" } }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, "node_modules/scroll-into-view-if-needed": { - "version": "2.2.31", - "resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-2.2.31.tgz", - "integrity": "sha512-dGCXy99wZQivjmjIqihaBQNjryrz5rueJY7eHfTdyWEiR4ttYpsajb14rn9s5d4DY4EcY6+4+U/maARBXJedkA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.1.0.tgz", + "integrity": "sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==", "license": "MIT", "dependencies": { - "compute-scroll-into-view": "^1.0.20" + "compute-scroll-into-view": "^3.0.2" } }, "node_modules/semver": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", - "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" } }, - "node_modules/shallow-equal": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/shallow-equal/-/shallow-equal-1.2.1.tgz", - "integrity": "sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA==", - "license": "MIT" - }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -2875,11 +4075,18 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } }, + "node_modules/string-convert": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/string-convert/-/string-convert-0.2.1.tgz", + "integrity": "sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A==", + "license": "MIT" + }, "node_modules/style-mod": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz", @@ -2918,6 +4125,19 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -2939,6 +4159,75 @@ "node": ">= 0.8.0" } }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.59.4.tgz", + "integrity": "sha512-Rw6+44QNFaXtgHSjPy+Kw8hrJniMYzR85E9yLmOLcfZ91/rz+JXQbDTCmc6ccxMPY6K6PgAq26f0JCBfR7LIPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.59.4", + "@typescript-eslint/parser": "8.59.4", + "@typescript-eslint/typescript-estree": "8.59.4", + "@typescript-eslint/utils": "8.59.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -2949,12 +4238,14 @@ "punycode": "^2.1.0" } }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true, - "license": "MIT" + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } }, "node_modules/vite": { "version": "8.0.13", @@ -3034,94 +4325,13 @@ } } }, - "node_modules/vue": { - "version": "3.5.34", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.34.tgz", - "integrity": "sha512-WdLBG9gm02OgJIG9axd5Hpx0TFLdzVgfG2evFFu8Rur5O/IoGc5cMjnjh3tPL6GnRGsYvUhBSKVPYVcxRKpMCA==", + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", "license": "MIT", - "dependencies": { - "@vue/compiler-dom": "3.5.34", - "@vue/compiler-sfc": "3.5.34", - "@vue/runtime-dom": "3.5.34", - "@vue/server-renderer": "3.5.34", - "@vue/shared": "3.5.34" - }, - "peerDependencies": { - "typescript": "*" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/vue-eslint-parser": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-10.4.0.tgz", - "integrity": "sha512-Vxi9pJdbN3ZnVGLODVtZ7y4Y2kzAAE2Cm0CZ3ZDRvydVYxZ6VrnBhLikBsRS+dpwj4Jv4UCv21PTEwF5rQ9WXg==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "eslint-scope": "^8.2.0 || ^9.0.0", - "eslint-visitor-keys": "^4.2.0 || ^5.0.0", - "espree": "^10.3.0 || ^11.0.0", - "esquery": "^1.6.0", - "semver": "^7.6.3" - }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://github.com/sponsors/mysticatea" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0" - } - }, - "node_modules/vue-i18n": { - "version": "11.4.4", - "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.4.4.tgz", - "integrity": "sha512-gIbXVSFQV4jcSJxfwdZ5zSZmZ+12CnX0K3vBkRSd6Zn+HSzCp+QwUgPwpD/uN0oKNKI9RzlUXPKVedEuMgNG0A==", - "license": "MIT", - "dependencies": { - "@intlify/core-base": "11.4.4", - "@intlify/devtools-types": "11.4.4", - "@intlify/shared": "11.4.4", - "@vue/devtools-api": "^6.5.0" - }, - "engines": { - "node": ">= 22" - }, - "funding": { - "url": "https://github.com/sponsors/kazupon" - }, - "peerDependencies": { - "vue": "^3.0.0" - } - }, - "node_modules/vue-types": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/vue-types/-/vue-types-3.0.2.tgz", - "integrity": "sha512-IwUC0Aq2zwaXqy74h4WCvFCUtoV0iSWr0snWnE9TnU18S66GAQyqQbRf2qfJtUuiFsBf6qp0MEwdonlwznlcrw==", - "license": "MIT", - "dependencies": { - "is-plain-object": "3.0.1" - }, - "engines": { - "node": ">=10.15.0" - }, - "peerDependencies": { - "vue": "^3.0.0" - } - }, - "node_modules/vue3-persian-datetime-picker": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/vue3-persian-datetime-picker/-/vue3-persian-datetime-picker-1.2.2.tgz", - "integrity": "sha512-d7nkj5vgtUvEXZboSdRmP1uwBfXvXgXqdvsOOMQb34jiMZU/aBDrTYWTEe1N+XKF9pvTTJn8Rws9ttJmyhK/hw==", - "license": "MIT", - "dependencies": { - "moment-jalaali": "^0.9.4" + "node": ">=0.10.0" } }, "node_modules/w3c-keyname": { @@ -3130,15 +4340,6 @@ "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", "license": "MIT" }, - "node_modules/warning": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", - "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.0.0" - } - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -3165,15 +4366,12 @@ "node": ">=0.10.0" } }, - "node_modules/xml-name-validator": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", - "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12" - } + "license": "ISC" }, "node_modules/yocto-queue": { "version": "0.1.0", @@ -3187,6 +4385,29 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } } } } diff --git a/frontend/package.json b/frontend/package.json index aefe82c1..c36aa065 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,9 +1,9 @@ { "name": "3x-ui-frontend", "private": true, - "version": "0.0.3", + "version": "0.1.0", "type": "module", - "description": "3x-ui panel frontend (Vue 3 + Ant Design Vue 4 + Vite 8).", + "description": "3x-ui panel frontend (React 19 + Ant Design 6 + Vite 8).", "engines": { "node": ">=22.0.0", "npm": ">=10.0.0" @@ -12,32 +12,35 @@ "dev": "vite", "build": "vite build", "preview": "vite preview", - "lint": "eslint src" + "lint": "eslint src", + "typecheck": "tsc --noEmit" }, "dependencies": { - "@ant-design/icons-vue": "^7.0.1", + "@ant-design/icons": "^6.2.3", "@codemirror/lang-json": "^6.0.2", "@codemirror/theme-one-dark": "^6.1.3", - "ant-design-vue": "^4.2.6", - "axios": "^1.7.9", + "antd": "^6.4.3", + "axios": "^1.16.1", "codemirror": "^6.0.2", "dayjs": "^1.11.20", + "i18next": "^26.2.0", "otpauth": "^9.5.1", - "qs": "^6.13.1", - "vue": "^3.5.34", - "vue-i18n": "^11.1.4", - "vue3-persian-datetime-picker": "^1.2.2" + "persian-calendar-suite": "^1.5.5", + "qs": "^6.15.2", + "react": "^19.2.6", + "react-dom": "^19.2.6", + "react-i18next": "^17.0.8" }, "devDependencies": { "@eslint/js": "^10.0.1", - "@vitejs/plugin-vue": "^6.0.6", - "eslint": "^10.3.0", - "eslint-plugin-vue": "^10.9.1", + "@types/react": "^19.2.15", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.2", + "eslint": "^10.4.0", + "eslint-plugin-react-hooks": "^7.1.1", "globals": "^17.6.0", - "vite": "8.0.13", - "vue-eslint-parser": "^10.4.0" - }, - "overrides": { - "moment-jalaali": "^0.10.4" + "typescript": "^6.0.3", + "typescript-eslint": "^8.59.4", + "vite": "8.0.13" } -} \ No newline at end of file +} diff --git a/frontend/settings.html b/frontend/settings.html index 0ef6413b..e753ffe1 100644 --- a/frontend/settings.html +++ b/frontend/settings.html @@ -8,6 +8,6 @@
- + diff --git a/frontend/src/components/AppSidebar.css b/frontend/src/components/AppSidebar.css new file mode 100644 index 00000000..85f66d6f --- /dev/null +++ b/frontend/src/components/AppSidebar.css @@ -0,0 +1,287 @@ +.ant-sidebar > .ant-layout-sider { + position: sticky; + top: 0; + height: 100vh; + align-self: flex-start; +} + +.sider-brand, +.drawer-brand { + font-weight: 600; + font-size: 18px; + letter-spacing: 0.5px; + color: rgba(0, 0, 0, 0.88); +} + +.sider-brand { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + padding: 14px 14px; + border-bottom: 1px solid rgba(128, 128, 128, 0.15); + user-select: none; +} + +.sider-brand-collapsed { + justify-content: center; + font-size: 16px; + padding: 14px 4px; + letter-spacing: 0; +} + +.brand-block { + display: inline-flex; + flex-direction: column; + align-items: center; + min-width: 0; + line-height: 1.1; +} + +.brand-text { + display: block; +} + +.brand-version { + display: block; + width: 100%; + text-align: center; + font-size: 10px; + font-weight: 500; + letter-spacing: 0; + opacity: 0.6; + margin-top: 2px; +} + +.sider-brand-collapsed .brand-block { + align-items: center; + flex: 0 0 auto; +} + +.brand-actions { + display: inline-flex; + align-items: center; + gap: 2px; + flex-shrink: 0; +} + +.sidebar-donate { + background: transparent; + border: none; + width: 30px; + height: 30px; + border-radius: 50%; + display: inline-flex; + align-items: center; + justify-content: center; + color: rgba(0, 0, 0, 0.75); + text-decoration: none; + flex-shrink: 0; + transition: background-color 0.2s, transform 0.15s, color 0.2s; +} + +.sidebar-donate:hover, +.sidebar-donate:focus-visible { + background-color: rgba(236, 72, 153, 0.12); + color: #ec4899; + transform: scale(1.08); + outline: none; +} + +.sidebar-donate .anticon { + font-size: 16px; +} + +.sidebar-theme-cycle { + background: transparent; + border: none; + width: 30px; + height: 30px; + border-radius: 50%; + display: inline-flex; + align-items: center; + justify-content: center; + cursor: pointer; + color: rgba(0, 0, 0, 0.75); + padding: 0; + flex-shrink: 0; + transition: background-color 0.2s, transform 0.15s, color 0.2s; +} + +.sidebar-theme-cycle:hover, +.sidebar-theme-cycle:focus-visible { + background-color: rgba(64, 150, 255, 0.1); + color: #4096ff; + transform: scale(1.08); + outline: none; +} + +.sidebar-theme-cycle svg { + width: 16px; + height: 16px; +} + +.drawer-header-actions { + display: inline-flex; + align-items: center; + gap: 4px; +} + +.drawer-handle { + position: fixed; + top: 12px; + left: 12px; + z-index: 1100; + background: rgba(0, 0, 0, 0.55); + color: #fff; + border: none; + width: 40px; + height: 40px; + border-radius: 50%; + cursor: pointer; + display: none; + align-items: center; + justify-content: center; + font-size: 18px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25); +} + +.drawer-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 14px 16px; + border-bottom: 1px solid rgba(128, 128, 128, 0.15); +} + +.drawer-close { + background: transparent; + border: none; + width: 32px; + height: 32px; + border-radius: 50%; + display: inline-flex; + align-items: center; + justify-content: center; + cursor: pointer; + font-size: 16px; + color: rgba(0, 0, 0, 0.65); +} + +.drawer-close:hover, +.drawer-close:focus-visible { + background: rgba(128, 128, 128, 0.18); +} + +.drawer-menu .ant-menu-item { + height: 48px; + line-height: 48px; + margin: 0; + border-radius: 0; +} + +.drawer-menu .ant-menu-item .anticon { + font-size: 16px; +} + +.drawer-utility { + margin-top: auto; + border-top: 1px solid rgba(128, 128, 128, 0.15); +} + +.ant-sidebar > .ant-layout-sider .ant-layout-sider-children { + display: flex; + flex-direction: column; + height: 100%; +} + +.sider-nav { + flex: 1 1 auto; + overflow-y: auto; + overflow-x: hidden; + min-height: 0; +} + +.sider-utility { + flex: 0 0 auto; + border-top: 1px solid rgba(128, 128, 128, 0.15); +} + +@media (max-width: 768px) { + .drawer-handle { + display: inline-flex; + } + + .ant-sidebar > .ant-layout-sider .ant-layout-sider-children, + .ant-sidebar > .ant-layout-sider .ant-layout-sider-trigger { + display: none; + } + + .ant-sidebar > .ant-layout-sider { + flex: 0 0 0 !important; + max-width: 0 !important; + min-width: 0 !important; + width: 0 !important; + } +} + +body.dark .drawer-brand, +body.dark .sider-brand { + color: rgba(255, 255, 255, 0.92); +} + +html[data-theme='ultra-dark'] .drawer-brand, +html[data-theme='ultra-dark'] .sider-brand { + color: #ffffff; +} + +body.dark .drawer-close { + color: rgba(255, 255, 255, 0.75); +} + +html[data-theme='ultra-dark'] .drawer-close { + color: rgba(255, 255, 255, 0.85); +} + +body.dark .sidebar-theme-cycle { + color: rgba(255, 255, 255, 0.85); +} + +html[data-theme='ultra-dark'] .sidebar-theme-cycle { + color: rgba(255, 255, 255, 0.92); +} + +body.dark .sidebar-donate { + color: rgba(255, 255, 255, 0.85); +} + +html[data-theme='ultra-dark'] .sidebar-donate { + color: rgba(255, 255, 255, 0.92); +} + +body.dark .ant-drawer .ant-drawer-content, +body.dark .ant-drawer .ant-drawer-body { + background: #252526 !important; +} + +html[data-theme='ultra-dark'] .ant-drawer .ant-drawer-content, +html[data-theme='ultra-dark'] .ant-drawer .ant-drawer-body { + background: #0a0a0a !important; +} + +.sider-nav .ant-menu-item-selected, +.sider-utility .ant-menu-item-selected, +.drawer-menu .ant-menu-item-selected { + background-color: rgba(64, 150, 255, 0.2) !important; + color: #4096ff !important; +} + +.sider-nav .ant-menu-item-active:not(.ant-menu-item-selected), +.sider-utility .ant-menu-item-active:not(.ant-menu-item-selected), +.drawer-menu .ant-menu-item-active:not(.ant-menu-item-selected), +.sider-nav .ant-menu-item:not(.ant-menu-item-selected):not(.ant-menu-item-disabled):hover, +.sider-utility .ant-menu-item:not(.ant-menu-item-selected):not(.ant-menu-item-disabled):hover, +.drawer-menu .ant-menu-item:not(.ant-menu-item-selected):not(.ant-menu-item-disabled):hover { + background-color: rgba(64, 150, 255, 0.1) !important; + color: #4096ff !important; +} diff --git a/frontend/src/components/AppSidebar.tsx b/frontend/src/components/AppSidebar.tsx new file mode 100644 index 00000000..27dc0571 --- /dev/null +++ b/frontend/src/components/AppSidebar.tsx @@ -0,0 +1,290 @@ +import { useCallback, useMemo, useState } from 'react'; +import type { ComponentType } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Drawer, Layout, Menu } from 'antd'; +import type { MenuProps } from 'antd'; +import { + ApiOutlined, + ClusterOutlined, + CloseOutlined, + DashboardOutlined, + HeartOutlined, + LogoutOutlined, + MenuOutlined, + SettingOutlined, + TeamOutlined, + ToolOutlined, + UserOutlined, +} from '@ant-design/icons'; + +import { HttpUtil } from '@/utils'; +import { pauseAnimationsUntilLeave, useTheme } from '@/hooks/useTheme'; +import './AppSidebar.css'; + +const SIDEBAR_COLLAPSED_KEY = 'isSidebarCollapsed'; +const DONATE_URL = 'https://donate.sanaei.dev/'; + +interface AppSidebarProps { + basePath?: string; + requestUri?: string; +} + +type IconName = 'dashboard' | 'user' | 'team' | 'setting' | 'tool' | 'cluster' | 'logout' | 'apidocs'; + +const iconByName: Record = { + dashboard: DashboardOutlined, + user: UserOutlined, + team: TeamOutlined, + setting: SettingOutlined, + tool: ToolOutlined, + cluster: ClusterOutlined, + logout: LogoutOutlined, + apidocs: ApiOutlined, +}; + +function readCollapsed(): boolean { + try { + return JSON.parse(localStorage.getItem(SIDEBAR_COLLAPSED_KEY) || 'false'); + } catch { + return false; + } +} + +function DonateButton({ ariaLabel }: { ariaLabel: string }) { + return ( + + + + ); +} + +function ThemeCycleButton({ id, isDark, isUltra, onCycle, ariaLabel }: { + id: string; + isDark: boolean; + isUltra: boolean; + onCycle: () => void; + ariaLabel: string; +}) { + return ( + + ); +} + +export default function AppSidebar({ basePath = '', requestUri = '' }: AppSidebarProps) { + const { t } = useTranslation(); + const { isDark, isUltra, toggleTheme, toggleUltra } = useTheme(); + + const [collapsed, setCollapsed] = useState(() => readCollapsed()); + const [drawerOpen, setDrawerOpen] = useState(false); + + const prefix = basePath.startsWith('/') ? basePath : `/${basePath || ''}`; + const currentTheme: 'light' | 'dark' = isDark ? 'dark' : 'light'; + const panelVersion = window.X_UI_CUR_VER || ''; + + const tabs = useMemo<{ key: string; icon: IconName; title: string }[]>(() => [ + { key: `${prefix}panel/`, icon: 'dashboard', title: t('menu.dashboard') }, + { key: `${prefix}panel/inbounds`, icon: 'user', title: t('menu.inbounds') }, + { key: `${prefix}panel/clients`, icon: 'team', title: t('menu.clients') }, + { key: `${prefix}panel/nodes`, icon: 'cluster', title: t('menu.nodes') }, + { key: `${prefix}panel/settings`, icon: 'setting', title: t('menu.settings') }, + { key: `${prefix}panel/xray`, icon: 'tool', title: t('menu.xray') }, + { key: `${prefix}panel/api-docs`, icon: 'apidocs', title: t('menu.apiDocs') }, + { key: 'logout', icon: 'logout', title: t('logout') }, + ], [prefix, t]); + + const navItems = useMemo(() => tabs.filter((tab) => tab.icon !== 'logout'), [tabs]); + const utilItems = useMemo(() => tabs.filter((tab) => tab.icon === 'logout'), [tabs]); + + const toMenuItems = useCallback((items: typeof tabs): MenuProps['items'] => + items.map((tab) => { + const Icon = iconByName[tab.icon]; + return { + key: tab.key, + icon: , + label: tab.title, + }; + }), + []); + + const openLink = useCallback(async (key: string) => { + if (key === 'logout') { + await HttpUtil.post('/logout'); + window.location.href = basePath || '/'; + return; + } + if (key.startsWith('http')) { + window.open(key); + } else { + window.location.href = key; + } + }, [basePath]); + + const onMenuClick = useCallback>(({ key }) => { + openLink(String(key)); + }, [openLink]); + + const onSiderCollapse = useCallback((isCollapsed: boolean, type: 'clickTrigger' | 'responsive') => { + if (type === 'clickTrigger') { + localStorage.setItem(SIDEBAR_COLLAPSED_KEY, String(isCollapsed)); + setCollapsed(isCollapsed); + } + }, []); + + const cycleTheme = useCallback((id: string) => { + pauseAnimationsUntilLeave(id); + if (!isDark) { + toggleTheme(); + if (isUltra) toggleUltra(); + } else if (!isUltra) { + toggleUltra(); + } else { + toggleUltra(); + toggleTheme(); + } + }, [isDark, isUltra, toggleTheme, toggleUltra]); + + return ( +
+ +
+
+ {collapsed ? '3X' : '3X-UI'} + {!collapsed && panelVersion && ( + v{panelVersion} + )} +
+ {!collapsed && ( +
+ + cycleTheme('theme-cycle')} + ariaLabel={t('menu.theme')} + /> +
+ )} +
+ + + + + setDrawerOpen(false)} + > +
+
+ 3X-UI + {panelVersion && v{panelVersion}} +
+
+ + cycleTheme('theme-cycle-drawer')} + ariaLabel={t('menu.theme')} + /> + +
+
+ { onMenuClick(info); setDrawerOpen(false); }} + /> + { onMenuClick(info); setDrawerOpen(false); }} + /> + + + {!drawerOpen && ( + + )} +
+ ); +} diff --git a/frontend/src/components/AppSidebar.vue b/frontend/src/components/AppSidebar.vue deleted file mode 100644 index 1fd4dfc9..00000000 --- a/frontend/src/components/AppSidebar.vue +++ /dev/null @@ -1,432 +0,0 @@ - - - - - - - diff --git a/frontend/src/components/CustomStatistic.css b/frontend/src/components/CustomStatistic.css new file mode 100644 index 00000000..9cb3e065 --- /dev/null +++ b/frontend/src/components/CustomStatistic.css @@ -0,0 +1,52 @@ +.ant-statistic-content { + font-size: 17px !important; + line-height: 1.4 !important; + font-weight: 600; +} + +.ant-statistic-content-value, +.ant-statistic-content-prefix, +.ant-statistic-content-suffix { + font-size: 17px !important; +} + +.ant-statistic-content-prefix { + margin-inline-end: 8px !important; + opacity: 0.7; +} + +.ant-statistic-content-prefix .anticon { + font-size: 17px !important; +} + +.ant-statistic-content-suffix { + font-size: 12px !important; + opacity: 0.55; + margin-inline-start: 4px; + font-weight: 500; +} + +.ant-statistic-title { + font-size: 11px !important; + margin-bottom: 6px !important; + letter-spacing: 0.6px; + text-transform: uppercase; + color: rgba(0, 0, 0, 0.55); + font-weight: 500; +} + +body.dark .ant-statistic-content { + color: rgba(255, 255, 255, 0.92); +} + +body.dark .ant-statistic-title { + color: rgba(255, 255, 255, 0.72); +} + +html[data-theme='ultra-dark'] .ant-statistic-content { + color: rgba(255, 255, 255, 0.95); +} + +html[data-theme='ultra-dark'] .ant-statistic-title { + color: rgba(255, 255, 255, 0.70); +} diff --git a/frontend/src/components/CustomStatistic.tsx b/frontend/src/components/CustomStatistic.tsx new file mode 100644 index 00000000..6089637f --- /dev/null +++ b/frontend/src/components/CustomStatistic.tsx @@ -0,0 +1,14 @@ +import type { ReactNode } from 'react'; +import { Statistic } from 'antd'; +import './CustomStatistic.css'; + +interface CustomStatisticProps { + title?: string; + value?: string | number; + prefix?: ReactNode; + suffix?: ReactNode; +} + +export default function CustomStatistic({ title = '', value = '', prefix, suffix }: CustomStatisticProps) { + return ; +} diff --git a/frontend/src/components/CustomStatistic.vue b/frontend/src/components/CustomStatistic.vue deleted file mode 100644 index 9d59e37c..00000000 --- a/frontend/src/components/CustomStatistic.vue +++ /dev/null @@ -1,31 +0,0 @@ - - - - - diff --git a/frontend/src/components/DateTimePicker.css b/frontend/src/components/DateTimePicker.css new file mode 100644 index 00000000..b745afb4 --- /dev/null +++ b/frontend/src/components/DateTimePicker.css @@ -0,0 +1,35 @@ +.jdp-wrap { + width: 100%; +} + +.jdp-wrap > * { + width: 100%; +} + +.jdp-wrap input { + direction: ltr; + text-align: left; + unicode-bidi: plaintext; +} + +.jdp-dark .jdp-wrap input, +.jdp-dark input { + color: rgba(255, 255, 255, 0.88) !important; + background-color: #23252b !important; +} + +.jdp-ultra .jdp-wrap input, +.jdp-ultra input { + color: rgba(255, 255, 255, 0.88) !important; + background-color: #101013 !important; +} + +.jdp-dark input::placeholder, +.jdp-ultra input::placeholder { + color: rgba(255, 255, 255, 0.30) !important; +} + +.jdp-disabled { + pointer-events: none; + opacity: 0.6; +} diff --git a/frontend/src/components/DateTimePicker.tsx b/frontend/src/components/DateTimePicker.tsx new file mode 100644 index 00000000..bdd521b6 --- /dev/null +++ b/frontend/src/components/DateTimePicker.tsx @@ -0,0 +1,98 @@ +import { useMemo } from 'react'; +import { DatePicker } from 'antd'; +import dayjs from 'dayjs'; +import type { Dayjs } from 'dayjs'; +import { PersianDateTimePicker } from 'persian-calendar-suite'; + +import { useDatepicker } from '@/hooks/useDatepicker'; +import { useTheme } from '@/hooks/useTheme'; +import './DateTimePicker.css'; + +interface DateTimePickerProps { + value: Dayjs | null; + onChange: (next: Dayjs | null) => void; + showTime?: boolean; + format?: string; + placeholder?: string; + disabled?: boolean; +} + +const LIGHT_THEME = { + primaryColor: '#1677ff', + backgroundColor: '#ffffff', + borderColor: '#d9d9d9', + hoverColor: 'rgba(22, 119, 255, 0.10)', + selectedTextColor: '#ffffff', + textColor: 'rgba(0, 0, 0, 0.88)', +}; + +const DARK_THEME = { + primaryColor: '#1677ff', + backgroundColor: '#23252b', + borderColor: 'rgba(255, 255, 255, 0.12)', + hoverColor: 'rgba(22, 119, 255, 0.18)', + selectedTextColor: '#ffffff', + textColor: 'rgba(255, 255, 255, 0.88)', +}; + +const ULTRA_DARK_THEME = { + primaryColor: '#1677ff', + backgroundColor: '#101013', + borderColor: 'rgba(255, 255, 255, 0.08)', + hoverColor: 'rgba(22, 119, 255, 0.16)', + selectedTextColor: '#ffffff', + textColor: 'rgba(255, 255, 255, 0.88)', +}; + +export default function DateTimePicker({ + value, + onChange, + showTime = true, + format = 'YYYY-MM-DD HH:mm:ss', + placeholder = '', + disabled = false, +}: DateTimePickerProps) { + const { datepicker } = useDatepicker(); + const { isDark, isUltra } = useTheme(); + + const persianTheme = useMemo(() => { + if (isUltra) return ULTRA_DARK_THEME; + if (isDark) return DARK_THEME; + return LIGHT_THEME; + }, [isDark, isUltra]); + + if (datepicker === 'jalalian') { + return ( +
+ { + if (next == null || next === '') { + onChange(null); + return; + } + const ms = typeof next === 'number' ? next : Number(next); + if (Number.isFinite(ms)) onChange(dayjs(ms)); + }} + showTime={showTime} + outputFormat="timestamp" + persianNumbers + rtlCalendar + theme={persianTheme} + /> +
+ ); + } + + return ( + onChange(next || null)} + showTime={showTime ? { format: 'HH:mm:ss' } : false} + format={format} + placeholder={placeholder} + disabled={disabled} + style={{ width: '100%' }} + /> + ); +} diff --git a/frontend/src/components/DateTimePicker.vue b/frontend/src/components/DateTimePicker.vue deleted file mode 100644 index 750d2594..00000000 --- a/frontend/src/components/DateTimePicker.vue +++ /dev/null @@ -1,366 +0,0 @@ - - - - - - - - diff --git a/frontend/src/components/FinalMaskForm.tsx b/frontend/src/components/FinalMaskForm.tsx new file mode 100644 index 00000000..07039cca --- /dev/null +++ b/frontend/src/components/FinalMaskForm.tsx @@ -0,0 +1,738 @@ +import { useMemo } from 'react'; +import { Button, Divider, Form, Input, InputNumber, Select, Switch } from 'antd'; +import { DeleteOutlined, PlusOutlined, ReloadOutlined } from '@ant-design/icons'; + +import { RandomUtil } from '@/utils'; +import { Protocols } from '@/models/outbound.js'; + +interface StreamShape { + network?: string; + kcp?: { mtu?: number }; + finalmask: { + tcp?: MaskRow[]; + udp?: MaskRow[]; + enableQuicParams?: boolean; + quicParams?: QuicParams; + }; + addTcpMask: (type?: string) => void; + delTcpMask: (index: number) => void; + addUdpMask: (type?: string) => void; + delUdpMask: (index: number) => void; +} + +interface MaskRow { + type: string; + settings: Record; + _getDefaultSettings: (type: string, settings: Record) => Record; +} + +interface ItemRow { + type: string; + packet: string | unknown[]; + delay?: number | string; + rand?: number | string; + randRange?: string; +} + +interface QuicParams { + congestion: string; + debug?: boolean; + brutalUp?: number | string; + brutalDown?: number | string; + hasUdpHop?: boolean; + udpHop?: { ports: string; interval: string | number }; + maxIdleTimeout?: number; + keepAlivePeriod?: number; + disablePathMTUDiscovery?: boolean; + maxIncomingStreams?: number; + initStreamReceiveWindow?: number; + maxStreamReceiveWindow?: number; + initConnectionReceiveWindow?: number; + maxConnectionReceiveWindow?: number; +} + +interface FinalMaskFormProps { + stream: StreamShape; + protocol: string; + onChange: () => void; +} + +function changeMaskType(mask: MaskRow, type: string) { + mask.type = type; + mask.settings = mask._getDefaultSettings(type, {}); +} + +function changeItemType(item: ItemRow, type: string) { + item.type = type; + if (type === 'base64') item.packet = RandomUtil.randomBase64(); + else if (type === 'array') { + item.rand = 0; + item.packet = []; + } else item.packet = ''; +} + +function newClientServerItem(): ItemRow { + return { delay: 0, rand: 0, randRange: '0-255', type: 'array', packet: [] }; +} + +function newUdpClientServerItem(): ItemRow { + return { rand: 0, randRange: '0-255', type: 'array', packet: [] }; +} + +function newNoiseItem(): ItemRow { + return { rand: '1-8192', randRange: '0-255', type: 'array', packet: [], delay: '10-20' }; +} + +export default function FinalMaskForm({ stream, protocol, onChange }: FinalMaskFormProps) { + const isHysteria = protocol === Protocols.Hysteria || protocol === 'hysteria'; + const network = stream?.network || ''; + + const showTcp = useMemo( + () => ['raw', 'tcp', 'httpupgrade', 'ws', 'grpc', 'xhttp'].includes(network), + [network], + ); + const showUdp = isHysteria || network === 'kcp'; + const showQuic = isHysteria || network === 'xhttp'; + + function notify() { + onChange(); + } + + function changeUdpMaskType(mask: MaskRow, type: string) { + changeMaskType(mask, type); + if (network === 'kcp' && stream.kcp) { + stream.kcp.mtu = type === 'xdns' ? 900 : 1350; + } + notify(); + } + + function addUdpMaskWithDefault() { + const def = isHysteria ? 'salamander' : 'mkcp-aes128gcm'; + stream.addUdpMask(def); + notify(); + } + + const tcpMasks = stream.finalmask.tcp || []; + const udpMasks = stream.finalmask.udp || []; + + if (!showTcp && !showUdp && !showQuic) return null; + + return ( +
+ {showTcp && ( + <> + + + )} + + + )} + > + + + + ); +} diff --git a/frontend/src/components/TextModal.vue b/frontend/src/components/TextModal.vue deleted file mode 100644 index 2e9bfe6d..00000000 --- a/frontend/src/components/TextModal.vue +++ /dev/null @@ -1,66 +0,0 @@ - - - - - diff --git a/frontend/src/composables/useDatepicker.js b/frontend/src/composables/useDatepicker.js deleted file mode 100644 index 03eba91c..00000000 --- a/frontend/src/composables/useDatepicker.js +++ /dev/null @@ -1,45 +0,0 @@ -// Module-scoped reactive ref for the panel's "Calendar Type" setting. -// Loaded from /panel/setting/defaultSettings on first use, so any -// component (modals, inbound forms, future pages) can read the same -// value without prop-drilling and without re-fetching. -// -// useInbounds (which already reads defaultSettings for its own state) -// calls setDatepicker() after its fetch so we don't issue a second -// HTTP round-trip on the inbounds page. - -import { readonly, ref } from 'vue'; -import { HttpUtil } from '@/utils'; - -const datepicker = ref('gregorian'); -let fetched = false; -let pending = null; - -async function loadOnce() { - if (fetched) return; - if (pending) { - await pending; - return; - } - pending = (async () => { - try { - const msg = await HttpUtil.post('/panel/setting/defaultSettings'); - if (msg?.success) { - datepicker.value = msg.obj?.datepicker || 'gregorian'; - } - } finally { - fetched = true; - pending = null; - } - })(); - await pending; -} - -export function setDatepicker(value) { - fetched = true; - datepicker.value = value || 'gregorian'; -} - -export function useDatepicker() { - loadOnce(); - return { datepicker: readonly(datepicker) }; -} diff --git a/frontend/src/composables/useMediaQuery.js b/frontend/src/composables/useMediaQuery.js deleted file mode 100644 index a1861c93..00000000 --- a/frontend/src/composables/useMediaQuery.js +++ /dev/null @@ -1,26 +0,0 @@ -import { ref, onBeforeUnmount, onMounted } from 'vue'; - -const MOBILE_BREAKPOINT_PX = 768; - -// Vue 3 replacement for the legacy MediaQueryMixin. Returns a reactive -// `isMobile` ref that updates on window resize. Use inside - - - - - - diff --git a/frontend/src/pages/api-docs/CodeBlock.vue b/frontend/src/pages/api-docs/CodeBlock.css similarity index 54% rename from frontend/src/pages/api-docs/CodeBlock.vue rename to frontend/src/pages/api-docs/CodeBlock.css index 446016c7..5cf64ceb 100644 --- a/frontend/src/pages/api-docs/CodeBlock.vue +++ b/frontend/src/pages/api-docs/CodeBlock.css @@ -1,67 +1,3 @@ - - - - - - diff --git a/frontend/src/pages/api-docs/CodeBlock.tsx b/frontend/src/pages/api-docs/CodeBlock.tsx new file mode 100644 index 00000000..805d1378 --- /dev/null +++ b/frontend/src/pages/api-docs/CodeBlock.tsx @@ -0,0 +1,69 @@ +import { useMemo, useState } from 'react'; +import { message } from 'antd'; +import { CheckOutlined, CopyOutlined } from '@ant-design/icons'; +import { ClipboardManager } from '@/utils'; +import './CodeBlock.css'; + +interface CodeBlockProps { + code?: string; + lang?: string; +} + +function escapeHtml(str: string): string { + return str.replace(/&/g, '&').replace(//g, '>'); +} + +function highlightJson(str: string): string { + const escaped = escapeHtml(str); + return escaped.replace( + /("(?:[^"\\]|\\.)*")\s*(:)|("(?:[^"\\]|\\.)*")|(-?\d+\.?\d*(?:[eE][+-]?\d+)?)\b|(true|false)|(null)|([{}[\]])/g, + (_m, key, colon, string, number, bool, nil) => { + if (colon) return `${key}${colon}`; + if (string) return `${string}`; + if (number) return `${number}`; + if (bool) return `${bool}`; + if (nil) return `${nil}`; + return _m; + }, + ); +} + +export default function CodeBlock({ code = '', lang = 'json' }: CodeBlockProps) { + const [copied, setCopied] = useState(false); + const [messageApi, messageContextHolder] = message.useMessage(); + + const highlighted = useMemo( + () => (lang === 'json' ? highlightJson(code) : escapeHtml(code)), + [code, lang], + ); + + async function copyCode() { + const ok = await ClipboardManager.copyText(code); + if (ok) { + setCopied(true); + messageApi.success('Copied'); + window.setTimeout(() => setCopied(false), 2000); + } else { + messageApi.error('Copy failed'); + } + } + + return ( +
+ {messageContextHolder} +
+ {lang.toUpperCase()} + +
+
+        
+      
+
+ ); +} diff --git a/frontend/src/pages/api-docs/EndpointRow.css b/frontend/src/pages/api-docs/EndpointRow.css new file mode 100644 index 00000000..43cd8c3f --- /dev/null +++ b/frontend/src/pages/api-docs/EndpointRow.css @@ -0,0 +1,93 @@ +.endpoint-row { + padding: 14px 8px; + margin: 0 -8px; + transition: background 0.15s; + border-radius: 6px; +} + +.endpoint-row:hover { + background: rgba(128, 128, 128, 0.03); +} + +.endpoint-row + .endpoint-row { + border-top: 1px solid rgba(128, 128, 128, 0.1); +} + +.endpoint-header { + display: flex; + align-items: center; + gap: 10px; + flex-wrap: wrap; +} + +.method-tag { + font-weight: 700; + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-size: 11px; + letter-spacing: 0.5px; + min-width: 56px; + text-align: center; + text-transform: uppercase; + border-radius: 4px; + padding: 2px 8px; + line-height: 1.6; +} + +.endpoint-path { + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-size: 13.5px; + word-break: break-all; + color: rgba(0, 0, 0, 0.8); + background: rgba(128, 128, 128, 0.06); + padding: 2px 8px; + border-radius: 4px; +} + +.endpoint-summary { + margin: 8px 0 0; + color: rgba(0, 0, 0, 0.6); + line-height: 1.6; + font-size: 13.5px; +} + +.endpoint-block { + margin-top: 14px; +} + +.block-label { + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.6px; + color: rgba(0, 0, 0, 0.45); + margin-bottom: 6px; +} + +.error-label { + color: #cf222e; +} + +body.dark .endpoint-row:hover { + background: rgba(255, 255, 255, 0.02); +} + +body.dark .endpoint-row + .endpoint-row { + border-top-color: rgba(255, 255, 255, 0.08); +} + +body.dark .endpoint-path { + color: rgba(255, 255, 255, 0.82); + background: rgba(255, 255, 255, 0.05); +} + +body.dark .endpoint-summary { + color: rgba(255, 255, 255, 0.65); +} + +body.dark .block-label { + color: rgba(255, 255, 255, 0.45); +} + +body.dark .error-label { + color: #ff7b72; +} diff --git a/frontend/src/pages/api-docs/EndpointRow.tsx b/frontend/src/pages/api-docs/EndpointRow.tsx new file mode 100644 index 00000000..d7fd7ad2 --- /dev/null +++ b/frontend/src/pages/api-docs/EndpointRow.tsx @@ -0,0 +1,84 @@ +import { Table, Tag } from 'antd'; +import type { ColumnsType } from 'antd/es/table'; +import { methodColors, safeInlineHtml } from './endpoints.js'; +import CodeBlock from './CodeBlock'; +import './EndpointRow.css'; + +interface Param { + name: string; + in?: string; + type?: string; + desc?: string; +} + +export interface Endpoint { + method: string; + path: string; + summary?: string; + params?: Param[]; + body?: string; + response?: string; + errorResponse?: string; +} + +const paramColumns: ColumnsType = [ + { title: 'Name', dataIndex: 'name', key: 'name', width: 180 }, + { title: 'In', dataIndex: 'in', key: 'in', width: 100 }, + { title: 'Type', dataIndex: 'type', key: 'type', width: 120 }, + { title: 'Description', dataIndex: 'desc', key: 'desc' }, +]; + +export default function EndpointRow({ endpoint }: { endpoint: Endpoint }) { + const tagColor = (methodColors as Record)[endpoint.method] || 'default'; + const hasParams = Array.isArray(endpoint.params) && endpoint.params.length > 0; + + return ( +
+
+ {endpoint.method} + {endpoint.path} +
+ + {endpoint.summary && ( +

+ )} + + {hasParams && ( +

+
Parameters
+ + + )} + + {endpoint.body && ( +
+
Request body
+ +
+ )} + + {endpoint.response && ( +
+
Response
+ +
+ )} + + {endpoint.errorResponse && ( +
+
Error response
+ +
+ )} + + ); +} diff --git a/frontend/src/pages/api-docs/EndpointRow.vue b/frontend/src/pages/api-docs/EndpointRow.vue deleted file mode 100644 index 5a811427..00000000 --- a/frontend/src/pages/api-docs/EndpointRow.vue +++ /dev/null @@ -1,172 +0,0 @@ - - - - - - - diff --git a/frontend/src/pages/api-docs/EndpointSection.vue b/frontend/src/pages/api-docs/EndpointSection.css similarity index 51% rename from frontend/src/pages/api-docs/EndpointSection.vue rename to frontend/src/pages/api-docs/EndpointSection.css index 795e9bb6..f6a54568 100644 --- a/frontend/src/pages/api-docs/EndpointSection.vue +++ b/frontend/src/pages/api-docs/EndpointSection.css @@ -1,63 +1,3 @@ - - - - - - diff --git a/frontend/src/pages/api-docs/EndpointSection.tsx b/frontend/src/pages/api-docs/EndpointSection.tsx new file mode 100644 index 00000000..8b254d19 --- /dev/null +++ b/frontend/src/pages/api-docs/EndpointSection.tsx @@ -0,0 +1,90 @@ +import type { ComponentType } from 'react'; +import { Table } from 'antd'; +import type { ColumnsType } from 'antd/es/table'; +import { DownOutlined, RightOutlined } from '@ant-design/icons'; +import EndpointRow from './EndpointRow'; +import type { Endpoint } from './EndpointRow'; +import { safeInlineHtml } from './endpoints.js'; +import './EndpointSection.css'; + +interface SubHeader { + name: string; + desc?: string; +} + +export interface Section { + id: string; + title: string; + description?: string; + endpoints: Endpoint[]; + subHeader?: SubHeader[]; +} + +interface EndpointSectionProps { + section: Section; + icon?: ComponentType<{ className?: string }> | null; + collapsed?: boolean; + onToggle?: () => void; +} + +const subHeaderColumns: ColumnsType = [ + { title: 'Header', dataIndex: 'name', key: 'name', width: 240 }, + { + title: 'Description', + dataIndex: 'desc', + key: 'desc', + render: (value: string) => ( + + ), + }, +]; + +export default function EndpointSection({ + section, + icon: Icon = null, + collapsed = false, + onToggle, +}: EndpointSectionProps) { + const endpointLabel = section.endpoints.length === 1 + ? '1 endpoint' + : `${section.endpoints.length} endpoints`; + + return ( +
+
+
+ {collapsed ? : } + {Icon && } +

{section.title}

+
+ {endpointLabel} +
+ + {section.description && !collapsed && ( +

+ )} + + {section.subHeader && !collapsed && ( +

+
Response headers
+
+ + )} + +
+ {section.endpoints.map((endpoint, idx) => ( + + ))} +
+ + ); +} diff --git a/frontend/src/pages/clients/ClientBulkAddModal.css b/frontend/src/pages/clients/ClientBulkAddModal.css new file mode 100644 index 00000000..e49ef577 --- /dev/null +++ b/frontend/src/pages/clients/ClientBulkAddModal.css @@ -0,0 +1,5 @@ +.random-icon { + margin-left: 4px; + cursor: pointer; + color: var(--ant-color-primary, #1677ff); +} diff --git a/frontend/src/pages/clients/ClientBulkAddModal.tsx b/frontend/src/pages/clients/ClientBulkAddModal.tsx new file mode 100644 index 00000000..43358b78 --- /dev/null +++ b/frontend/src/pages/clients/ClientBulkAddModal.tsx @@ -0,0 +1,341 @@ +import { useEffect, useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Form, Input, InputNumber, Modal, Select, Switch, message } from 'antd'; +import { SyncOutlined } from '@ant-design/icons'; +import dayjs from 'dayjs'; +import type { Dayjs } from 'dayjs'; + +import { HttpUtil, RandomUtil, SizeFormatter } from '@/utils'; +import { TLS_FLOW_CONTROL } from '@/models/inbound'; +import DateTimePicker from '@/components/DateTimePicker'; +import type { InboundOption } from '@/hooks/useClients'; +import './ClientBulkAddModal.css'; + +const FLOW_OPTIONS = Object.values(TLS_FLOW_CONTROL); +const JSON_HEADERS = { headers: { 'Content-Type': 'application/json' } } as const; + +const MULTI_CLIENT_PROTOCOLS = new Set([ + 'shadowsocks', 'vless', 'vmess', 'trojan', 'hysteria', 'hysteria2', +]); + +interface ApiMsg { + success?: boolean; + msg?: string; +} + +interface ClientBulkAddModalProps { + open: boolean; + inbounds: InboundOption[]; + ipLimitEnable?: boolean; + onOpenChange: (open: boolean) => void; + onSaved?: () => void; +} + +interface FormState { + emailMethod: number; + firstNum: number; + lastNum: number; + emailPrefix: string; + emailPostfix: string; + quantity: number; + subId: string; + comment: string; + flow: string; + limitIp: number; + totalGB: number; + expiryTime: number; + inboundIds: number[]; +} + +function emptyForm(): FormState { + return { + emailMethod: 0, + firstNum: 1, + lastNum: 1, + emailPrefix: '', + emailPostfix: '', + quantity: 1, + subId: '', + comment: '', + flow: '', + limitIp: 0, + totalGB: 0, + expiryTime: 0, + inboundIds: [], + }; +} + +export default function ClientBulkAddModal({ + open, + inbounds, + ipLimitEnable = false, + onOpenChange, + onSaved, +}: ClientBulkAddModalProps) { + const { t } = useTranslation(); + const [messageApi, messageContextHolder] = message.useMessage(); + + const [form, setForm] = useState(emptyForm); + const [delayedStart, setDelayedStart] = useState(false); + const [saving, setSaving] = useState(false); + + useEffect(() => { + if (!open) return; + + setForm(emptyForm()); + setDelayedStart(false); + + }, [open]); + + function update(key: K, value: FormState[K]) { + setForm((prev) => ({ ...prev, [key]: value })); + } + + const flowCapableIds = useMemo(() => { + const ids = new Set(); + for (const row of inbounds || []) { + if (row?.tlsFlowCapable) ids.add(row.id); + } + return ids; + }, [inbounds]); + + const showFlow = useMemo( + () => (form.inboundIds || []).some((id) => flowCapableIds.has(id)), + [form.inboundIds, flowCapableIds], + ); + + useEffect(() => { + if (!showFlow && form.flow) { + + update('flow', ''); + } + }, [showFlow, form.flow]); + + const inboundOptions = useMemo( + () => (inbounds || []) + .filter((ib) => MULTI_CLIENT_PROTOCOLS.has(ib.protocol || '')) + .map((ib) => ({ + label: `${ib.remark || `#${ib.id}`} · ${ib.protocol}:${ib.port}`, + value: ib.id, + })), + [inbounds], + ); + + const expiryDate = useMemo( + () => (form.expiryTime > 0 ? dayjs(form.expiryTime) : null), + [form.expiryTime], + ); + + const delayedExpireDays = form.expiryTime < 0 ? form.expiryTime / -86400000 : 0; + + function buildEmails(): string[] { + const method = form.emailMethod; + const out: string[] = []; + let start: number; + let end: number; + if (method > 1) { + start = form.firstNum; + end = form.lastNum + 1; + } else { + start = 0; + end = form.quantity; + } + const prefix = method > 0 && form.emailPrefix.length > 0 ? form.emailPrefix : ''; + const useNum = method > 1; + const postfix = method > 2 && form.emailPostfix.length > 0 ? form.emailPostfix : ''; + for (let i = start; i < end; i++) { + let email = ''; + if (method !== 4) email = RandomUtil.randomLowerAndNum(6); + email += useNum ? prefix + String(i) + postfix : prefix + postfix; + out.push(email); + } + return out; + } + + async function submit() { + if (!Array.isArray(form.inboundIds) || form.inboundIds.length === 0) { + messageApi.error(t('pages.clients.selectInbound')); + return; + } + const emails = buildEmails(); + if (emails.length === 0) return; + + setSaving(true); + const silentJsonOpts = { ...JSON_HEADERS, silent: true }; + try { + const results = await Promise.all(emails.map((email) => { + const client = { + email, + subId: form.subId || RandomUtil.randomLowerAndNum(16), + id: RandomUtil.randomUUID(), + password: RandomUtil.randomLowerAndNum(16), + auth: RandomUtil.randomLowerAndNum(16), + flow: showFlow ? (form.flow || '') : '', + totalGB: Math.round((form.totalGB || 0) * SizeFormatter.ONE_GB), + expiryTime: form.expiryTime, + limitIp: Number(form.limitIp) || 0, + comment: form.comment, + enable: true, + }; + const payload = { client, inboundIds: form.inboundIds }; + return HttpUtil.post('/panel/api/clients/add', payload, silentJsonOpts) as Promise; + })); + let ok = 0; + let failed = 0; + let firstError = ''; + for (const msg of results) { + if (msg?.success) ok++; + else { + failed++; + if (!firstError && msg?.msg) firstError = msg.msg; + } + } + if (failed === 0) { + messageApi.success(t('pages.clients.toasts.bulkCreated', { count: ok })); + } else { + messageApi.warning(firstError + ? `${t('pages.clients.toasts.bulkCreatedMixed', { ok, failed })} — ${firstError}` + : t('pages.clients.toasts.bulkCreatedMixed', { ok, failed })); + } + onSaved?.(); + onOpenChange(false); + } finally { + setSaving(false); + } + } + + return ( + <> + {messageContextHolder} + onOpenChange(false)} + > +
+ + update('emailMethod', v)} + options={[ + { value: 0, label: 'Random' }, + { value: 1, label: 'Random + Prefix' }, + { value: 2, label: 'Random + Prefix + Num' }, + { value: 3, label: 'Random + Prefix + Num + Postfix' }, + { value: 4, label: 'Prefix + Num + Postfix' }, + ]} + /> + + + {form.emailMethod > 1 && ( + <> + + update('firstNum', Number(v) || 1)} /> + + + update('lastNum', Number(v) || 1)} /> + + + )} + {form.emailMethod > 0 && ( + + update('emailPrefix', e.target.value)} /> + + )} + {form.emailMethod > 2 && ( + + update('emailPostfix', e.target.value)} /> + + )} + {form.emailMethod < 2 && ( + + update('quantity', Number(v) || 1)} /> + + )} + + + {t('subscription.title')} + update('subId', RandomUtil.randomLowerAndNum(16))} + /> + + }> + update('subId', e.target.value)} /> + + + + update('comment', e.target.value)} /> + + + {showFlow && ( + + update('email', e.target.value)} + /> + + + + +
+ + + update('subId', e.target.value)} /> + + + + + + + + + + + update('auth', e.target.value)} /> + + + + + + + + update('password', e.target.value)} /> + + + + + + + + + + + update('uuid', e.target.value)} /> + + + + + + + update('totalGB', Number(v) || 0)} /> + + + {ipLimitEnable && ( + + + update('limitIp', Number(v) || 0)} /> + + + )} + + + + + {form.delayedStart ? ( + + update('delayedDays', Number(v) || 0)} /> + + ) : ( + + update('expiryDate', d || null)} + /> + + )} + + + + { + update('delayedStart', v); + if (v) update('expiryDate', null); + else update('delayedDays', 0); + }} + /> + + + + + {(showFlow || showReverseTag) && ( + + {showFlow && ( + + + update('reverseTag', e.target.value)} /> + + + )} + + )} + + + {tgBotEnable && ( + + + update('tgId', Number(v) || 0)} /> + + + )} + + + update('comment', e.target.value)} /> + + + + + +
+ + + + + + + + + + + + + + + + + + {client.uuid && ( + + + + + )} + {client.password && ( + + + + + )} + {client.auth && ( + + + + + )} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {client.comment && ( + + + + + )} + + + + + +
{t('pages.clients.online')} + {client.enable && isOnline + ? {t('pages.clients.online')} + : {t('pages.clients.offline')}} + {t('lastOnline')}: {dateLabel(traffic?.lastOnline)} +
{t('status')} + + {client.enable ? t('enabled') : t('disabled')} + +
{t('pages.clients.email')} + {client.email + ? {client.email} + : {t('none')}} +
{t('pages.clients.subId')} + {client.subId || '-'} + {client.subId && ( +
{t('pages.clients.uuid')} + {client.uuid} +
{t('password')} + {client.password} +
{t('pages.clients.auth')} + {client.auth} +
{t('pages.clients.flow')} + {client.flow ? {client.flow} : {t('none')}} +
{t('pages.inbounds.traffic')} + + ↑ {SizeFormatter.sizeFormat(traffic?.up || 0)} + {' '}/ ↓ {SizeFormatter.sizeFormat(traffic?.down || 0)} + + + {SizeFormatter.sizeFormat(used)} / {totalBytes > 0 ? SizeFormatter.sizeFormat(totalBytes) : '∞'} + +
{t('remained')} + {remaining < 0 + ? + : 0 ? '' : 'red'}>{SizeFormatter.sizeFormat(remaining)}} +
{t('pages.inbounds.expireDate')} + {!client.expiryTime || client.expiryTime <= 0 + ? + : {expiryLabel(client.expiryTime)}} + {(client.expiryTime ?? 0) > 0 && ( + {IntlUtil.formatRelativeTime(client.expiryTime)} + )} +
{t('pages.clients.ipLimit')}{!client.limitIp ? : {client.limitIp}}
{t('pages.inbounds.createdAt')}{dateLabel(client.createdAt)}
{t('pages.inbounds.updatedAt')}{dateLabel(client.updatedAt)}
{t('pages.clients.comment')}{client.comment}
{t('pages.clients.attachedInbounds')} +
+ {(client.inboundIds || []).map((id) => { + const ib = inboundsById[id]; + return ( + + {ib ? `${ib.remark || `#${id}`} (${ib.protocol}:${ib.port})` : `#${id}`} + + ); + })} + {(!client.inboundIds || client.inboundIds.length === 0) && ( + + )} +
+
+ + {links.length > 0 && ( + <> + {t('pages.inbounds.copyLink')} + {links.map((link, idx) => ( +
+
+ {`${t('pages.clients.link')} ${idx + 1}`} + +
+ {link} +
+ ))} + + )} + + {showSubscription && subLink && ( + <> + {t('subscription.title')} +
+
+ {t('subscription.title')} + +
+ {subLink} +
+ {subJsonLink && ( +
+
+ JSON + +
+ {subJsonLink} +
+ )} + + )} + + )} + + + ); +} diff --git a/frontend/src/pages/clients/ClientInfoModal.vue b/frontend/src/pages/clients/ClientInfoModal.vue deleted file mode 100644 index 8694e8c2..00000000 --- a/frontend/src/pages/clients/ClientInfoModal.vue +++ /dev/null @@ -1,411 +0,0 @@ - - - - - diff --git a/frontend/src/pages/clients/ClientQrModal.tsx b/frontend/src/pages/clients/ClientQrModal.tsx new file mode 100644 index 00000000..6263414a --- /dev/null +++ b/frontend/src/pages/clients/ClientQrModal.tsx @@ -0,0 +1,136 @@ +import { useEffect, useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Collapse, Modal, Spin } from 'antd'; +import { HttpUtil } from '@/utils'; +import QrPanel from '@/pages/inbounds/QrPanel'; +import type { ClientRecord } from '@/hooks/useClients'; + +interface SubSettings { + enable: boolean; + subURI: string; + subJsonURI: string; + subJsonEnable: boolean; +} + +interface ClientQrModalProps { + open: boolean; + client: ClientRecord | null; + subSettings?: SubSettings; + onOpenChange: (open: boolean) => void; +} + +interface ApiMsg { + success?: boolean; + obj?: T; +} + +const DEFAULT_SUB: SubSettings = { enable: false, subURI: '', subJsonURI: '', subJsonEnable: false }; + +export default function ClientQrModal({ + open, + client, + subSettings = DEFAULT_SUB, + onOpenChange, +}: ClientQrModalProps) { + const { t } = useTranslation(); + const [links, setLinks] = useState([]); + const [loading, setLoading] = useState(false); + + const subLink = useMemo(() => { + if (!client?.subId || !subSettings?.enable || !subSettings?.subURI) return ''; + return subSettings.subURI + client.subId; + }, [client?.subId, subSettings?.enable, subSettings?.subURI]); + + const subJsonLink = useMemo(() => { + if (!client?.subId || !subSettings?.enable) return ''; + if (!subSettings?.subJsonEnable || !subSettings?.subJsonURI) return ''; + return subSettings.subJsonURI + client.subId; + }, [client?.subId, subSettings?.enable, subSettings?.subJsonEnable, subSettings?.subJsonURI]); + + const hasAnything = !!subLink || !!subJsonLink || links.length > 0; + + useEffect(() => { + if (!open || !client?.subId) { + setLinks([]); + return; + } + let cancelled = false; + setLoading(true); + (async () => { + try { + const msg = await HttpUtil.get( + `/panel/api/clients/subLinks/${encodeURIComponent(client.subId!)}`, + ) as ApiMsg; + if (!cancelled) { + setLinks(msg?.success && Array.isArray(msg.obj) ? msg.obj : []); + } + } finally { + if (!cancelled) setLoading(false); + } + })(); + return () => { cancelled = true; }; + }, [open, client?.subId]); + + const [activeKey, setActiveKey] = useState([]); + + const items = useMemo(() => { + const out: { key: string; label: string; children: React.ReactNode }[] = []; + if (subLink) { + out.push({ + key: 'sub', + label: t('subscription.title'), + children: , + }); + } + if (subJsonLink) { + out.push({ + key: 'subJson', + label: `${t('subscription.title')} (JSON)`, + children: , + }); + } + links.forEach((link, idx) => { + out.push({ + key: `l${idx}`, + label: `${t('pages.clients.link')} ${idx + 1}`, + children: , + }); + }); + return out; + }, [subLink, subJsonLink, links, client?.email, t]); + + useEffect(() => { + if (!open) { + setActiveKey([]); + return; + } + setActiveKey(items.length > 0 ? [items[0].key] : []); + }, [open, items]); + + return ( + onOpenChange(false)} + > + + {!client?.subId && !loading && ( +
{t('pages.clients.noSubId')}
+ )} + {client?.subId && !hasAnything && !loading && ( +
{t('pages.clients.noLinks')}
+ )} + {hasAnything && ( + setActiveKey(typeof keys === 'string' ? [keys] : (keys as string[]))} + items={items} + /> + )} +
+
+ ); +} diff --git a/frontend/src/pages/clients/ClientQrModal.vue b/frontend/src/pages/clients/ClientQrModal.vue deleted file mode 100644 index 3ae69c3f..00000000 --- a/frontend/src/pages/clients/ClientQrModal.vue +++ /dev/null @@ -1,97 +0,0 @@ - - - - - diff --git a/frontend/src/pages/clients/ClientsPage.css b/frontend/src/pages/clients/ClientsPage.css new file mode 100644 index 00000000..575d68c3 --- /dev/null +++ b/frontend/src/pages/clients/ClientsPage.css @@ -0,0 +1,221 @@ +.clients-page { + --bg-page: #e6e8ec; + --bg-card: #ffffff; + min-height: 100vh; + background: var(--bg-page); +} + +.clients-page.is-dark { + --bg-page: #1a1b1f; + --bg-card: #23252b; +} + +.clients-page.is-dark.is-ultra { + --bg-page: #000; + --bg-card: #101013; +} + +.clients-page .ant-layout, +.clients-page .ant-layout-content { + background: transparent; +} + +.clients-page .content-shell { + background: transparent; +} + +.clients-page .content-area { + padding: 24px; +} + +@media (max-width: 768px) { + .clients-page .content-area { + padding: 8px; + } +} + +.clients-page .ant-pagination-options-size-changer, +.clients-page .ant-pagination-options-size-changer .ant-select-selector { + min-width: 100px !important; +} + +.clients-page .loading-spacer { + min-height: calc(100vh - 120px); +} + +.clients-page .summary-card { + padding: 16px; +} + +@media (max-width: 768px) { + .clients-page .summary-card { + padding: 8px; + } +} + +.client-email-list { + max-height: 280px; + min-width: 160px; + overflow-y: auto; + padding-right: 4px; +} + +.client-email-list > div { + padding: 2px 0; + font-size: 12px; + white-space: nowrap; +} + +.filter-bar { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px; + margin-bottom: 12px; +} + +.filter-bar.mobile { + gap: 6px; + margin-bottom: 8px; +} + +.filter-bar.mobile > * { + flex: 0 0 auto; +} + +.dot { + display: inline-block; + width: 8px; + height: 8px; + border-radius: 50%; + margin-right: 4px; + vertical-align: middle; +} + +.dot-green { background: #52c41a; } +.dot-blue { background: #1677ff; } +.dot-red { background: #ff4d4f; } +.dot-orange { background: #fa8c16; } +.dot-gray { background: rgba(128, 128, 128, 0.6); } + +.status-tag { + margin: 0 0 0 4px; + font-size: 11px; + padding: 0 6px; + line-height: 18px; +} + +.card-toolbar { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; +} + +.email-cell { + display: flex; + flex-direction: column; +} + +.email-cell .email { + font-weight: 500; +} + +.email-cell .sub { + font-size: 11px; + opacity: 0.55; + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 220px; +} + +.client-cards { + display: flex; + flex-direction: column; + gap: 10px; + margin-top: 4px; +} + +.card-bulk-bar { + display: flex; + align-items: center; + gap: 8px; + padding: 4px 4px 8px; +} + +.bulk-count { + font-size: 12px; + background: rgba(22, 119, 255, 0.12); + color: var(--ant-color-primary, #1677ff); + padding: 1px 8px; + border-radius: 10px; +} + +.client-card { + border: 1px solid rgba(128, 128, 128, 0.2); + border-radius: 10px; + padding: 10px 12px; + background: rgba(255, 255, 255, 0.02); +} + +.client-card.is-selected { + border-color: var(--ant-color-primary, #1677ff); + background: rgba(22, 119, 255, 0.06); +} + +body.dark .client-card { + background: rgba(255, 255, 255, 0.03); + border-color: rgba(255, 255, 255, 0.1); +} + +.card-head { + display: flex; + align-items: center; + gap: 8px; + user-select: none; +} + +.card-head .tag-name { + font-weight: 600; + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.card-actions { + display: flex; + align-items: center; + gap: 10px; + flex-shrink: 0; +} + +.row-action-trigger { + font-size: 18px; + cursor: pointer; + opacity: 0.75; + transition: opacity 120ms ease; +} + +.row-action-trigger:hover { + opacity: 1; +} + +.card-empty { + text-align: center; + padding: 40px 16px; + opacity: 0.55; + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; +} + +.clients-empty { + padding: 32px 0; + text-align: center; + opacity: 0.55; +} diff --git a/frontend/src/pages/clients/ClientsPage.tsx b/frontend/src/pages/clients/ClientsPage.tsx new file mode 100644 index 00000000..3018fadb --- /dev/null +++ b/frontend/src/pages/clients/ClientsPage.tsx @@ -0,0 +1,908 @@ +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { + Badge, + Button, + Card, + Checkbox, + Col, + ConfigProvider, + Dropdown, + Input, + Layout, + Modal, + Popover, + Radio, + Row, + Select, + Space, + Spin, + Switch, + Table, + Tag, + Tooltip, + message, +} from 'antd'; +import type { ColumnsType, TableProps } from 'antd/es/table'; +import { + DeleteOutlined, + EditOutlined, + FilterOutlined, + InfoCircleOutlined, + MoreOutlined, + PlusOutlined, + QrcodeOutlined, + RestOutlined, + RetweetOutlined, + SearchOutlined, + TeamOutlined, + UserOutlined, + UsergroupAddOutlined, +} from '@ant-design/icons'; + +import { useTheme } from '@/hooks/useTheme'; +import { useMediaQuery } from '@/hooks/useMediaQuery'; +import { useWebSocket } from '@/hooks/useWebSocket'; +import { useClients } from '@/hooks/useClients'; +import { useDatepicker } from '@/hooks/useDatepicker'; +import type { ClientRecord, InboundOption } from '@/hooks/useClients'; +import AppSidebar from '@/components/AppSidebar'; +import CustomStatistic from '@/components/CustomStatistic'; +import { IntlUtil, ObjectUtil, SizeFormatter } from '@/utils'; +import { setMessageInstance } from '@/utils/messageBus'; +import ClientFormModal from './ClientFormModal'; +import ClientInfoModal from './ClientInfoModal'; +import ClientQrModal from './ClientQrModal'; +import ClientBulkAddModal from './ClientBulkAddModal'; +import '@/styles/page-cards.css'; +import './ClientsPage.css'; + +const basePath = window.X_UI_BASE_PATH || ''; +const requestUri = window.location.pathname; +const FILTER_STATE_KEY = 'clientsFilterState'; + +type Bucket = 'active' | 'deactive' | 'depleted' | 'expiring'; + +interface FilterState { + enableFilter: boolean; + searchKey: string; + filterBy: string; + protocolFilter?: string; +} + +function readFilterState(): FilterState { + try { + const raw = JSON.parse(localStorage.getItem(FILTER_STATE_KEY) || '{}'); + return { + enableFilter: !!raw.enableFilter, + searchKey: raw.searchKey || '', + filterBy: raw.filterBy || '', + protocolFilter: raw.protocolFilter, + }; + } catch { + return { enableFilter: false, searchKey: '', filterBy: '', protocolFilter: undefined }; + } +} + +export default function ClientsPage() { + const { t } = useTranslation(); + const { isDark, isUltra, antdThemeConfig } = useTheme(); + const { datepicker } = useDatepicker(); + const { isMobile } = useMediaQuery(); + const [modal, modalContextHolder] = Modal.useModal(); + const [messageApi, messageContextHolder] = message.useMessage(); + useEffect(() => { setMessageInstance(messageApi); }, [messageApi]); + + const { + clients, inbounds, onlines, loading, fetched, subSettings, + ipLimitEnable, tgBotEnable, expireDiff, trafficDiff, pageSize, + create, update, remove, removeMany, attach, detach, + resetTraffic, resetAllTraffics, delDepleted, setEnable, + applyTrafficEvent, applyClientStatsEvent, applyInvalidate, + } = useClients(); + + useWebSocket({ + traffic: applyTrafficEvent, + client_stats: applyClientStatsEvent, + invalidate: applyInvalidate, + }); + + const [togglingEmail, setTogglingEmail] = useState(null); + const [formOpen, setFormOpen] = useState(false); + const [formMode, setFormMode] = useState<'add' | 'edit'>('add'); + const [editingClient, setEditingClient] = useState(null); + const [editingAttachedIds, setEditingAttachedIds] = useState([]); + const [infoOpen, setInfoOpen] = useState(false); + const [infoClient, setInfoClient] = useState(null); + const [qrOpen, setQrOpen] = useState(false); + const [qrClient, setQrClient] = useState(null); + const [bulkAddOpen, setBulkAddOpen] = useState(false); + const [selectedRowKeys, setSelectedRowKeys] = useState([]); + + const initial = readFilterState(); + const [enableFilter, setEnableFilter] = useState(initial.enableFilter); + const [searchKey, setSearchKey] = useState(initial.searchKey); + const [filterBy, setFilterBy] = useState(initial.filterBy); + const [protocolFilter, setProtocolFilter] = useState(initial.protocolFilter); + + const [sortColumn, setSortColumn] = useState(null); + const [sortOrder, setSortOrder] = useState<'ascend' | 'descend' | null>(null); + const [currentPage, setCurrentPage] = useState(1); + const [tablePageSize, setTablePageSize] = useState(20); + + useEffect(() => { + localStorage.setItem(FILTER_STATE_KEY, JSON.stringify({ + enableFilter, searchKey, filterBy, protocolFilter, + })); + }, [enableFilter, searchKey, filterBy, protocolFilter]); + + useEffect(() => { + if (pageSize > 0) { + + setTablePageSize(pageSize); + } + }, [pageSize]); + + const onlineSet = useMemo(() => new Set(onlines || []), [onlines]); + const inboundsById = useMemo(() => { + const out: Record = {}; + for (const ib of inbounds) out[ib.id] = ib; + return out; + }, [inbounds]); + + const protocolOptions = useMemo(() => { + const values = new Set((inbounds || []).map((i) => i.protocol).filter((x): x is string => !!x)); + return [...values].sort(); + }, [inbounds]); + + const isOnline = useCallback((email: string) => !!email && onlineSet.has(email), [onlineSet]); + + function inboundLabel(id: number) { + const ib = inboundsById[id]; + if (!ib) return `#${id}`; + return ib.remark ? `${ib.remark} (${ib.protocol}:${ib.port})` : `${ib.protocol}:${ib.port}`; + } + + const clientBucket = useCallback((row: ClientRecord | null | undefined): Bucket | null => { + if (!row) return null; + const traffic = row.traffic || {}; + const used = (traffic.up || 0) + (traffic.down || 0); + const total = row.totalGB || 0; + const now = Date.now(); + const expired = (row.expiryTime ?? 0) > 0 && (row.expiryTime ?? 0) <= now; + const exhausted = total > 0 && used >= total; + if (expired || exhausted) return 'depleted'; + if (!row.enable) return 'deactive'; + const nearExpiry = (row.expiryTime ?? 0) > 0 && (row.expiryTime ?? 0) - now < (expireDiff || 0); + const nearLimit = total > 0 && total - used < (trafficDiff || 0); + if (nearExpiry || nearLimit) return 'expiring'; + return 'active'; + }, [expireDiff, trafficDiff]); + + function bucketBadgeColor(bucket: Bucket | null): string { + switch (bucket) { + case 'depleted': return '#ff4d4f'; + case 'expiring': return '#fa8c16'; + case 'deactive': return 'rgba(128,128,128,0.6)'; + case 'active': return '#52c41a'; + default: return 'rgba(128,128,128,0.6)'; + } + } + + function clientMatchesProtocol(row: ClientRecord, protocol?: string) { + if (!protocol) return true; + const ids = Array.isArray(row.inboundIds) ? row.inboundIds : []; + for (const id of ids) { + const ib = inboundsById[id]; + if (ib && ib.protocol === protocol) return true; + } + return false; + } + + const filteredClients = useMemo(() => { + let rows = clients || []; + if (enableFilter) { + if (filterBy === 'online') { + rows = rows.filter((r) => r.enable && isOnline(r.email)); + } else if (filterBy) { + rows = rows.filter((r) => clientBucket(r) === filterBy); + } + } else if (!ObjectUtil.isEmpty(searchKey)) { + rows = rows.filter((r) => ObjectUtil.deepSearch(r, searchKey)); + } + if (protocolFilter) { + rows = rows.filter((r) => clientMatchesProtocol(r, protocolFilter)); + } + return rows; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [clients, enableFilter, filterBy, searchKey, protocolFilter, clientBucket]); + + const summary = useMemo(() => { + const rows = clients || []; + const deactive: string[] = []; + const depleted: string[] = []; + const expiring: string[] = []; + const online: string[] = []; + let active = 0; + for (const row of rows) { + const bucket = clientBucket(row); + if (bucket === 'deactive') deactive.push(row.email); + else if (bucket === 'depleted') depleted.push(row.email); + else if (bucket === 'expiring') expiring.push(row.email); + else if (bucket === 'active') active++; + if (row.enable && isOnline(row.email)) online.push(row.email); + } + return { total: rows.length, active, deactive, depleted, expiring, online }; + }, [clients, clientBucket, isOnline]); + + const sortFns: Record number> = { + enable: (a, b) => Number(a.enable) - Number(b.enable), + email: (a, b) => (a.email || '').localeCompare(b.email || ''), + inboundIds: (a, b) => (a.inboundIds?.length || 0) - (b.inboundIds?.length || 0), + traffic: (a, b) => { + const ua = (a.traffic?.up || 0) + (a.traffic?.down || 0); + const ub = (b.traffic?.up || 0) + (b.traffic?.down || 0); + return ua - ub; + }, + remaining: (a, b) => { + const ra = (a.totalGB || 0) > 0 ? (a.totalGB || 0) - ((a.traffic?.up || 0) + (a.traffic?.down || 0)) : Infinity; + const rb = (b.totalGB || 0) > 0 ? (b.totalGB || 0) - ((b.traffic?.up || 0) + (b.traffic?.down || 0)) : Infinity; + return ra - rb; + }, + expiryTime: (a, b) => { + const ea = (a.expiryTime ?? 0) > 0 ? (a.expiryTime ?? 0) : Infinity; + const eb = (b.expiryTime ?? 0) > 0 ? (b.expiryTime ?? 0) : Infinity; + return ea - eb; + }, + }; + + const sortedClients = useMemo(() => { + if (!sortColumn || !sortOrder) return filteredClients; + const fn = sortFns[sortColumn]; + if (!fn) return filteredClients; + const sorted = [...filteredClients].sort(fn); + return sortOrder === 'descend' ? sorted.reverse() : sorted; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [filteredClients, sortColumn, sortOrder]); + + function trafficLabel(row: ClientRecord) { + const t0 = row.traffic; + if (!t0) return '-'; + const used = (t0.up || 0) + (t0.down || 0); + const total = row.totalGB || 0; + if (total <= 0) return `${SizeFormatter.sizeFormat(used)} / ∞`; + return `${SizeFormatter.sizeFormat(used)} / ${SizeFormatter.sizeFormat(total)}`; + } + + function remainingLabel(row: ClientRecord) { + const total = row.totalGB || 0; + if (total <= 0) return '∞'; + const used = (row.traffic?.up || 0) + (row.traffic?.down || 0); + const r = total - used; + return r > 0 ? SizeFormatter.sizeFormat(r) : '0'; + } + + function remainingColor(row: ClientRecord): string { + const total = row.totalGB || 0; + if (total <= 0) return 'purple'; + const used = (row.traffic?.up || 0) + (row.traffic?.down || 0); + const ratio = used / total; + if (ratio >= 1) return 'red'; + if (ratio >= 0.85) return 'orange'; + return 'green'; + } + + function expiryLabel(row: ClientRecord) { + if (!row.expiryTime) return '∞'; + if (row.expiryTime < 0) { + const days = Math.round(row.expiryTime / -86400000); + return `${t('pages.clients.delayedStart')}: ${days}d`; + } + return IntlUtil.formatDate(row.expiryTime, datepicker); + } + + function expiryRelative(row: ClientRecord) { + if (!row.expiryTime) return ''; + if (row.expiryTime < 0) { + const days = Math.round(row.expiryTime / -86400000); + return `${days}d`; + } + return IntlUtil.formatRelativeTime(row.expiryTime); + } + + function expiryColor(row: ClientRecord): string { + if (!row.expiryTime) return 'purple'; + if (row.expiryTime < 0) return 'blue'; + const now = Date.now(); + if (row.expiryTime <= now) return 'red'; + if (row.expiryTime - now < 86400 * 1000 * 3) return 'orange'; + return 'green'; + } + + async function onToggleEnable(row: ClientRecord, next: boolean) { + setTogglingEmail(row.email); + try { + const msg = await setEnable(row, next); + if (!msg?.success) { + messageApi.error(msg?.msg || t('somethingWentWrong')); + } + } finally { + setTogglingEmail(null); + } + } + + function onAdd() { + setFormMode('add'); + setEditingClient(null); + setEditingAttachedIds([]); + setFormOpen(true); + } + + function onEdit(row: ClientRecord) { + setFormMode('edit'); + setEditingClient({ ...row }); + setEditingAttachedIds(Array.isArray(row.inboundIds) ? [...row.inboundIds] : []); + setFormOpen(true); + } + + function onDelete(row: ClientRecord) { + modal.confirm({ + title: t('pages.clients.deleteConfirmTitle', { email: row.email }), + content: t('pages.clients.deleteConfirmContent'), + okText: t('delete'), + okType: 'danger', + cancelText: t('cancel'), + onOk: async () => { + const msg = await remove(row.email); + if (msg?.success) messageApi.success(t('pages.clients.toasts.deleted')); + }, + }); + } + + function onResetTraffic(row: ClientRecord) { + if (!row?.email || !Array.isArray(row.inboundIds) || row.inboundIds.length === 0) { + messageApi.warning(t('pages.clients.resetNotPossible')); + return; + } + modal.confirm({ + title: `${t('pages.inbounds.resetTraffic')} — ${row.email}`, + content: t('pages.inbounds.resetTrafficContent'), + okText: t('reset'), + cancelText: t('cancel'), + onOk: async () => { + const msg = await resetTraffic(row); + if (msg?.success) messageApi.success(t('pages.clients.toasts.trafficReset')); + }, + }); + } + + function onShowInfo(row: ClientRecord) { + setInfoClient(row); + setInfoOpen(true); + } + + function onShowQr(row: ClientRecord) { + setQrClient(row); + setQrOpen(true); + } + + function onResetAllTraffics() { + modal.confirm({ + title: t('pages.clients.resetAllTrafficsTitle'), + content: t('pages.clients.resetAllTrafficsContent'), + okText: t('reset'), + okType: 'danger', + cancelText: t('cancel'), + onOk: async () => { + const msg = await resetAllTraffics(); + if (msg?.success) messageApi.success(t('pages.clients.toasts.allTrafficsReset')); + }, + }); + } + + function onDelDepleted() { + modal.confirm({ + title: t('pages.clients.delDepletedConfirmTitle'), + content: t('pages.clients.delDepletedConfirmContent'), + okText: t('delete'), + okType: 'danger', + cancelText: t('cancel'), + onOk: async () => { + const msg = await delDepleted(); + if (msg?.success) { + const deleted = msg.obj?.deleted ?? 0; + messageApi.success(t('pages.clients.toasts.delDepleted', { count: deleted })); + } + }, + }); + } + + function onBulkDelete() { + const emails = [...selectedRowKeys]; + if (emails.length === 0) return; + modal.confirm({ + title: t('pages.clients.bulkDeleteConfirmTitle', { count: emails.length }), + content: t('pages.clients.bulkDeleteConfirmContent'), + okText: t('delete'), + okType: 'danger', + cancelText: t('cancel'), + onOk: async () => { + const results = await removeMany(emails); + setSelectedRowKeys([]); + let ok = 0; + let failed = 0; + let firstError = ''; + for (const msg of results) { + if (msg?.success) ok++; + else { + failed++; + if (!firstError && msg?.msg) firstError = msg.msg; + } + } + if (failed === 0) { + messageApi.success(t('pages.clients.toasts.bulkDeleted', { count: ok })); + } else { + messageApi.warning(firstError + ? `${t('pages.clients.toasts.bulkDeletedMixed', { ok, failed })} — ${firstError}` + : t('pages.clients.toasts.bulkDeletedMixed', { ok, failed })); + } + }, + }); + } + + const onSave = useCallback(async ( + payload: Record | { client: Record; inboundIds: number[] }, + meta: { isEdit: false } | { isEdit: true; email: string; attach: number[]; detach: number[] }, + ) => { + if (!meta.isEdit) { + return create(payload); + } + const updateMsg = await update(meta.email, payload); + if (!updateMsg?.success) return updateMsg; + if (Array.isArray(meta.attach) && meta.attach.length > 0) { + const r = await attach(meta.email, meta.attach); + if (!r?.success) return r; + } + if (Array.isArray(meta.detach) && meta.detach.length > 0) { + const r = await detach(meta.email, meta.detach); + if (!r?.success) return r; + } + return updateMsg; + }, [create, update, attach, detach]); + + const pageClass = useMemo(() => { + const classes = ['clients-page']; + if (isDark) classes.push('is-dark'); + if (isUltra) classes.push('is-ultra'); + return classes.join(' '); + }, [isDark, isUltra]); + + const onTableChange: NonNullable['onChange']> = (pag, _filters, sorter) => { + if (pag?.current) setCurrentPage(pag.current); + if (pag?.pageSize) setTablePageSize(pag.pageSize); + const s = Array.isArray(sorter) ? sorter[0] : sorter; + setSortColumn((s?.columnKey as string) || (s?.field as string) || null); + setSortOrder((s?.order as 'ascend' | 'descend' | null) || null); + }; + + const columns = useMemo>(() => { + function sortableCol[number]>(col: T, key: string): T { + return { + ...col, + sorter: true, + showSorterTooltip: false, + sortOrder: sortColumn === key ? sortOrder : null, + sortDirections: ['ascend', 'descend'], + }; + } + return [ + { + title: t('pages.clients.actions'), + key: 'actions', + width: 200, + render: (_v, record) => ( + + +
} + > + } /> + + + + {summary.expiring.map((e) =>
{e}
)}
} + > + } /> + + + + {summary.deactive.map((e) =>
{e}
)}} + > + } /> +
+ + + } /> + + + + + + + + + + {selectedRowKeys.length > 0 && ( + + )} + + + + } + > +
+ } + unCheckedChildren={} + /> + {!enableFilter && ( + setSearchKey(e.target.value)} + placeholder={t('search')} + autoFocus + size={isMobile ? 'small' : 'middle'} + style={{ maxWidth: 300 }} + /> + )} + {enableFilter && ( + setFilterBy(e.target.value)} + optionType="button" + buttonStyle="solid" + size={isMobile ? 'small' : 'middle'} + > + {t('none')} + {t('subscription.active')} + {t('disabled')} + {t('depleted')} + {t('depletingSoon')} + {t('online')} + + )} + { form.remark = e.target.value; refresh(); }} /> + + {selectableNodes.length > 0 && isNodeEligible && ( + + + + )} + + + + + { ib.listen = e.target.value; refresh(); }} + /> + + + { ib.port = Number(v) || 0; refresh(); }} + /> + + {t('pages.inbounds.totalFlow')}}> + { + form.total = NumberFormatter.toFixed((Number(v) || 0) * SizeFormatter.ONE_GB, 0); + refresh(); + }} + /> + + + + + {t('pages.inbounds.expireDate')}}> + { form.expiryTime = d ? d.valueOf() : 0; refresh(); }} + /> + + + ); + + const renderFallbacksCard = () => ( + + + {t('pages.inbounds.fallbacks.help') || 'When a connection on this inbound does not match any client, route it to another inbound. Pick a child below and the routing fields (SNI / ALPN / path / xver) auto-fill from its transport — most setups need no further tweaking. Each child should listen on 127.0.0.1 with security=none.'} + + {fallbacks.length === 0 && ( + + )} + {fallbacks.map((record, index) => ( +
+ + + + + + + + + updateFallback(record.rowKey, { name: e.target.value })} /> + + + + + ALPN + updateFallback(record.rowKey, { alpn: e.target.value })} /> + + + + + Path + updateFallback(record.rowKey, { path: e.target.value })} /> + + + + + xver + updateFallback(record.rowKey, { xver: Number(v) || 0 })} /> + + + + )} +
+ ))} + + + + +
+ ); + + const renderProtocolTab = () => ( + <> + {isVlessLike && ( +
+ + { ib.settings.decryption = e.target.value; refresh(); }} /> + + + { ib.settings.encryption = e.target.value; refresh(); }} /> + + + + + + + + + {t('pages.inbounds.vlessAuthSelected', { auth: selectedVlessAuth })} + + +
+ )} + + {isFallbackHost && renderFallbacksCard()} + + {ib.protocol === Protocols.SHADOWSOCKS && ( +
+ + + + {ib.isSS2022 && ( + Password randomSSPassword(ib.settings)} />}> + { ib.settings.password = e.target.value; refresh(); }} /> + + )} + + + + + { ib.settings.ivCheck = v; refresh(); }} /> + +
+ )} + + {(ib.protocol === Protocols.HTTP || ib.protocol === Protocols.MIXED) && ( +
+ + + + + {(ib.settings.accounts || []).map((account: any, idx: number) => ( + + {String(idx + 1)} + { account.user = e.target.value; refresh(); }} /> + { account.pass = e.target.value; refresh(); }} /> + + + ))} + + {ib.protocol === Protocols.HTTP && ( + + { ib.settings.allowTransparent = v; refresh(); }} /> + + )} + {ib.protocol === Protocols.MIXED && ( + <> + + + + + { ib.settings.udp = v; refresh(); }} /> + + {ib.settings.udp && ( + + { ib.settings.ip = e.target.value; refresh(); }} /> + + )} + + )} +
+ )} + + {ib.protocol === Protocols.TUNNEL && ( +
+ + { ib.settings.rewriteAddress = e.target.value; refresh(); }} /> + + + { ib.settings.rewritePort = Number(v) || 0; refresh(); }} /> + + + + + + + + {(ib.settings.portMap || []).length > 0 && ( + + {(ib.settings.portMap as { name: string; value: string }[]).map((pm, idx) => ( + + {String(idx + 1)} + { pm.name = e.target.value; refresh(); }} /> + { pm.value = e.target.value; refresh(); }} /> + + + ))} + + )} + + { ib.settings.followRedirect = v; refresh(); }} /> + +
+ )} + + {ib.protocol === Protocols.TUN && ( +
+ + { ib.settings.name = e.target.value; refresh(); }} /> + + + { ib.settings.mtu = Number(v) || 0; refresh(); }} /> + + + + {(ib.settings.gateway || []).map((_ip: string, j: number) => ( + + { ib.settings.gateway[j] = e.target.value; refresh(); }} /> + + + ))} + + + + {(ib.settings.dns || []).map((_ip: string, j: number) => ( + + { ib.settings.dns[j] = e.target.value; refresh(); }} /> + + + ))} + + + { ib.settings.userLevel = Number(v) || 0; refresh(); }} /> + + Auto system routes}> + + {(ib.settings.autoSystemRoutingTable || []).map((_ip: string, j: number) => ( + + { ib.settings.autoSystemRoutingTable[j] = e.target.value; refresh(); }} /> + + + ))} + + Auto outbounds interface}> + { ib.settings.autoOutboundsInterface = e.target.value; refresh(); }} /> + +
+ )} + + {ib.protocol === Protocols.WIREGUARD && ( +
+ Secret key }> + { ib.settings.secretKey = e.target.value; refresh(); }} /> + + + + + + { ib.settings.mtu = Number(v) || 0; refresh(); }} /> + + + { ib.settings.noKernelTun = v; refresh(); }} /> + + + + + {(ib.settings.peers || []).map((peer: any, idx: number) => ( +
+ + Peer {idx + 1} + {ib.settings.peers.length > 1 && ( + { ib.settings.delPeer(idx); refresh(); }} /> + )} + + Secret key regenWgKeypair(peer)} />}> + { peer.privateKey = e.target.value; refresh(); }} /> + + + { peer.publicKey = e.target.value; refresh(); }} /> + + + { peer.psk = e.target.value; refresh(); }} /> + + + + {(peer.allowedIPs || []).map((_ip: string, j: number) => ( + + { peer.allowedIPs[j] = e.target.value; refresh(); }} /> + {peer.allowedIPs.length > 1 && ( + + )} + + ))} + + + { peer.keepAlive = Number(v) || 0; refresh(); }} /> + +
+ ))} +
+ )} + + ); + + const renderStreamTab = () => { + const network = ib.stream?.network; + return ( + <> +
+ {ib.protocol !== Protocols.HYSTERIA && ( + + + + )} + + {network === 'tcp' && ( + <> + {canEnableTls && ( + + { ib.stream.tcp.acceptProxyProtocol = v; refresh(); }} /> + + )} + + { ib.stream.tcp.type = v ? 'http' : 'none'; refresh(); }} /> + + {ib.stream.tcp.type === 'http' && ( + <> + {t('pages.inbounds.stream.general.request')} + + { ib.stream.tcp.request.version = e.target.value; refresh(); }} /> + + + { ib.stream.tcp.request.method = e.target.value; refresh(); }} /> + + {t('pages.inbounds.stream.tcp.path')} }> + {(ib.stream.tcp.request.path || []).map((_p: string, idx: number) => ( + + { ib.stream.tcp.request.path[idx] = e.target.value; refresh(); }} /> + {ib.stream.tcp.request.path.length > 1 && ( + + )} + + ))} + + + + + {(ib.stream.tcp.request.headers || []).length > 0 && ( + + {(ib.stream.tcp.request.headers as { name: string; value: string }[]).map((h, idx) => ( + + {String(idx + 1)} + { h.name = e.target.value; refresh(); }} /> + { h.value = e.target.value; refresh(); }} /> + + + ))} + + )} + {t('pages.inbounds.stream.general.response')} + + { ib.stream.tcp.response.version = e.target.value; refresh(); }} /> + + + { ib.stream.tcp.response.status = e.target.value; refresh(); }} /> + + + { ib.stream.tcp.response.reason = e.target.value; refresh(); }} /> + + + + + {(ib.stream.tcp.response.headers || []).length > 0 && ( + + {(ib.stream.tcp.response.headers as { name: string; value: string }[]).map((h, idx) => ( + + {String(idx + 1)} + { h.name = e.target.value; refresh(); }} /> + { h.value = e.target.value; refresh(); }} /> + + + ))} + + )} + + )} + + )} + + {network === 'kcp' && ( + <> + { ib.stream.kcp.mtu = Number(v) || 0; refresh(); }} /> + { ib.stream.kcp.tti = Number(v) || 0; refresh(); }} /> + { ib.stream.kcp.upCap = Number(v) || 0; refresh(); }} /> + { ib.stream.kcp.downCap = Number(v) || 0; refresh(); }} /> + { ib.stream.kcp.cwndMultiplier = Number(v) || 0; refresh(); }} /> + { ib.stream.kcp.maxSendingWindow = Number(v) || 0; refresh(); }} /> + + )} + + {network === 'ws' && ( + <> + { ib.stream.ws.acceptProxyProtocol = v; refresh(); }} /> + { ib.stream.ws.host = e.target.value; refresh(); }} /> + { ib.stream.ws.path = e.target.value; refresh(); }} /> + { ib.stream.ws.heartbeatPeriod = Number(v) || 0; refresh(); }} /> + + + + {(ib.stream.ws.headers || []).length > 0 && ( + + {(ib.stream.ws.headers as { name: string; value: string }[]).map((h, idx) => ( + + {String(idx + 1)} + { h.name = e.target.value; refresh(); }} /> + { h.value = e.target.value; refresh(); }} /> + + + ))} + + )} + + )} + + {network === 'grpc' && ( + <> + { ib.stream.grpc.serviceName = e.target.value; refresh(); }} /> + { ib.stream.grpc.authority = e.target.value; refresh(); }} /> + { ib.stream.grpc.multiMode = v; refresh(); }} /> + + )} + + {network === 'httpupgrade' && ( + <> + { ib.stream.httpupgrade.acceptProxyProtocol = v; refresh(); }} /> + { ib.stream.httpupgrade.host = e.target.value; refresh(); }} /> + { ib.stream.httpupgrade.path = e.target.value; refresh(); }} /> + + + + {(ib.stream.httpupgrade.headers || []).length > 0 && ( + + {(ib.stream.httpupgrade.headers as { name: string; value: string }[]).map((h, idx) => ( + + {String(idx + 1)} + { h.name = e.target.value; refresh(); }} /> + { h.value = e.target.value; refresh(); }} /> + + + ))} + + )} + + )} + + {network === 'xhttp' && ( + <> + { ib.stream.xhttp.host = e.target.value; refresh(); }} /> + { ib.stream.xhttp.path = e.target.value; refresh(); }} /> + + + + {(ib.stream.xhttp.headers || []).length > 0 && ( + + {(ib.stream.xhttp.headers as { name: string; value: string }[]).map((h, idx) => ( + + {String(idx + 1)} + { h.name = e.target.value; refresh(); }} /> + { h.value = e.target.value; refresh(); }} /> + + + ))} + + )} + + + + {ib.stream.xhttp.mode === 'packet-up' && ( + <> + { ib.stream.xhttp.scMaxBufferedPosts = Number(v) || 0; refresh(); }} /> + { ib.stream.xhttp.scMaxEachPostBytes = e.target.value; refresh(); }} /> + + )} + {ib.stream.xhttp.mode === 'stream-up' && ( + { ib.stream.xhttp.scStreamUpServerSecs = e.target.value; refresh(); }} /> + )} + { ib.stream.xhttp.serverMaxHeaderBytes = Number(v) || 0; refresh(); }} /> + { ib.stream.xhttp.xPaddingBytes = e.target.value; refresh(); }} /> + { ib.stream.xhttp.xPaddingObfsMode = v; refresh(); }} /> + {ib.stream.xhttp.xPaddingObfsMode && ( + <> + { ib.stream.xhttp.xPaddingKey = e.target.value; refresh(); }} /> + { ib.stream.xhttp.xPaddingHeader = e.target.value; refresh(); }} /> + + + + + + + + )} + + + + {ib.stream.xhttp.sessionPlacement && ib.stream.xhttp.sessionPlacement !== 'path' && ( + { ib.stream.xhttp.sessionKey = e.target.value; refresh(); }} /> + )} + + + + {ib.stream.xhttp.seqPlacement && ib.stream.xhttp.seqPlacement !== 'path' && ( + { ib.stream.xhttp.seqKey = e.target.value; refresh(); }} /> + )} + {ib.stream.xhttp.mode === 'packet-up' && ( + + + + )} + {ib.stream.xhttp.mode === 'packet-up' && ib.stream.xhttp.uplinkDataPlacement && ib.stream.xhttp.uplinkDataPlacement !== 'body' && ( + { ib.stream.xhttp.uplinkDataKey = e.target.value; refresh(); }} /> + )} + { ib.stream.xhttp.noSSEHeader = v; refresh(); }} /> + + )} + + + + {externalProxyOn && ( + + )} + + {externalProxyOn && ( + + {(ib.stream.externalProxy as { forceTls: string; dest: string; port: number; remark: string }[]).map((row, idx) => ( + + + + + { row.dest = e.target.value; refresh(); }} /> + + { row.port = Number(v) || 0; refresh(); }} /> + + { row.remark = e.target.value; refresh(); }} /> + { ib.stream.externalProxy.splice(idx, 1); refresh(); }}> + + + + ))} + + )} + + { ib.stream.sockoptSwitch = v; refresh(); }} /> + {ib.stream.sockoptSwitch && ib.stream.sockopt && ( + <> + { ib.stream.sockopt.mark = Number(v) || 0; refresh(); }} /> + { ib.stream.sockopt.tcpKeepAliveInterval = Number(v) || 0; refresh(); }} /> + { ib.stream.sockopt.tcpKeepAliveIdle = Number(v) || 0; refresh(); }} /> + { ib.stream.sockopt.tcpMaxSeg = Number(v) || 0; refresh(); }} /> + { ib.stream.sockopt.tcpUserTimeout = Number(v) || 0; refresh(); }} /> + { ib.stream.sockopt.tcpWindowClamp = Number(v) || 0; refresh(); }} /> + { ib.stream.sockopt.acceptProxyProtocol = v; refresh(); }} /> + { ib.stream.sockopt.tcpFastOpen = v; refresh(); }} /> + { ib.stream.sockopt.tcpMptcp = v; refresh(); }} /> + { ib.stream.sockopt.penetrate = v; refresh(); }} /> + { ib.stream.sockopt.V6Only = v; refresh(); }} /> + + + + + + + + + + { ib.stream.sockopt.dialerProxy = e.target.value; refresh(); }} /> + { ib.stream.sockopt.interfaceName = e.target.value; refresh(); }} /> + + + + + )} + + {ib.protocol === Protocols.HYSTERIA && ( + <> + Version}> + { ib.stream.hysteria.version = Number(v) || 2; refresh(); }} /> + + UDP idle timeout}> + { ib.stream.hysteria.udpIdleTimeout = Number(v) || 0; refresh(); }} /> + + + { ib.stream.hysteria.masqueradeSwitch = v; refresh(); }} /> + + {ib.stream.hysteria.masqueradeSwitch && ( + <> + + + + {ib.stream.hysteria.masquerade.type === 'proxy' && ( + <> + { ib.stream.hysteria.masquerade.url = e.target.value; refresh(); }} /> + { ib.stream.hysteria.masquerade.rewriteHost = v; refresh(); }} /> + { ib.stream.hysteria.masquerade.insecure = v; refresh(); }} /> + + )} + {ib.stream.hysteria.masquerade.type === 'file' && ( + { ib.stream.hysteria.masquerade.dir = e.target.value; refresh(); }} /> + )} + {ib.stream.hysteria.masquerade.type === 'string' && ( + <> +