Files
SingBox-Gopanel/frontend/admin/src-reverse/pages/finance/FinancePlanPage.js
CN-JS-HuiBai 609ab002b3
Some checks failed
build / build (api, amd64, linux) (push) Failing after -52s
build / build (api, arm64, linux) (push) Failing after -52s
build / build (api.exe, amd64, windows) (push) Failing after -52s
修复丢失的前端文件
2026-04-18 22:03:26 +08:00

313 lines
10 KiB
JavaScript

import { makeRecoveredPage } from "../../runtime/makeRecoveredPage.js";
export default makeRecoveredPage({
title: "Finance Plan",
routePath: "/finance/plan",
moduleId: "p3t",
featureKey: "finance.plan",
translationNamespaces: ["subscribe", "common"],
summary:
"The plan management page is already fully visible in the compiled bundle: it loads the complete plan table, supports inline visibility/sale/renew toggles, drag-sort persistence, modal add/edit, delete confirmation, markdown content editing, and server-group binding.",
api: [
{
name: "plan.fetch",
method: "GET",
path: "{secure}/plan/fetch",
queryKey: ["planList"],
purpose: "Loads the entire plan table used by desktop and mobile layouts.",
responseShape: [
{
id: "number",
show: "boolean",
sell: "boolean",
renew: "boolean",
name: "string",
tags: ["string"],
group: {
id: "number",
name: "string",
},
users_count: "number",
active_users_count: "number",
transfer_enable: "number",
speed_limit: "number | null",
capacity_limit: "number | null",
device_limit: "number | null",
reset_traffic_method: "number | null",
content: "string",
prices: {
monthly: "number | null",
quarterly: "number | null",
half_yearly: "number | null",
yearly: "number | null",
two_yearly: "number | null",
three_yearly: "number | null",
onetime: "number | null",
reset_traffic: "number | null",
},
},
],
},
{
name: "plan.save",
method: "POST",
path: "{secure}/plan/save",
purpose: "Submits both add and edit modal payloads.",
notes: [
"The compiled dialog uses the same save endpoint for create and edit.",
"The currently edited row is distinguished by the presence of id in the form payload.",
],
},
{
name: "plan.update",
method: "POST",
path: "{secure}/plan/update",
purpose: "Inline boolean updates for show/sell/renew toggles.",
payloadExamples: [{ id: 1, show: true }, { id: 1, sell: false }, { id: 1, renew: true }],
},
{
name: "plan.drop",
method: "POST",
path: "{secure}/plan/drop",
purpose: "Deletes a plan after confirmation.",
payloadExamples: [{ id: 1 }],
},
{
name: "plan.sort",
method: "POST",
path: "{secure}/plan/sort",
purpose: "Persists drag-and-drop ordering when sort mode is saved.",
payloadExamples: [{ ids: [1, 3, 2] }],
},
{
name: "server.group.fetch",
method: "GET",
path: "{secure}/server/group/fetch",
purpose: "Populates the group selector inside the add/edit dialog.",
notes: ["The group list is refreshed every time the plan modal opens."],
},
],
stateModel: {
list: {
sorting: [],
columnVisibility: {
"drag-handle": false,
},
rowSelection: {},
columnFilters: [],
pagination: {
pageSize: 20,
pageIndex: 0,
},
draggableRows: false,
localSortedRowsMirror: "A local copy of the fetched plan list is mutated while drag-sort mode is active.",
},
modal: {
isOpen: false,
editingPlan: null,
isSubmitting: false,
previewMarkdown: false,
groupOptions: [],
},
},
tableModel: {
toolbar: {
actions: [
"Add plan button opens the plan dialog through context state.",
"Name search writes directly into the table filter for the name column.",
"Sort button toggles drag-sort mode; a second click persists order through plan.sort.",
],
},
columns: [
{
key: "drag-handle",
visibleWhen: "sort mode only",
role: "Drag handle used by the generic table component callbacks.",
},
{
key: "id",
titleKey: "plan.columns.id",
renderer: "Outline badge",
},
{
key: "show",
titleKey: "plan.columns.show",
renderer: "Boolean switch",
updateApi: "plan.update",
},
{
key: "sell",
titleKey: "plan.columns.sell",
renderer: "Boolean switch",
updateApi: "plan.update",
},
{
key: "renew",
titleKey: "plan.columns.renew",
tooltipKey: "plan.columns.renew_tooltip",
renderer: "Boolean switch",
updateApi: "plan.update",
},
{
key: "name",
titleKey: "plan.columns.name",
renderer: "Long truncated text cell",
},
{
key: "users_count",
titleKey: "plan.columns.stats",
renderer:
"Two compact stat pills: total users_count and active_users_count, with active-rate percentage when users_count > 0.",
},
{
key: "group",
titleKey: "plan.columns.group",
renderer: "Single badge using group.name",
},
{
key: "prices",
titleKey: "plan.columns.price",
renderer:
"Dynamic period badges for each non-null price; shows a dashed no-price badge when no prices exist.",
supportedPeriods: [
"monthly",
"quarterly",
"half_yearly",
"yearly",
"two_yearly",
"three_yearly",
"onetime",
"reset_traffic",
],
},
{
key: "actions",
titleKey: "plan.columns.actions",
renderer: "Edit action + delete confirmation action",
},
],
mobileLayout: {
primaryField: "name",
gridFields: ["prices", "users_count", "group"],
},
},
formModel: {
schema: {
id: "number | null",
group_id: "number | string | null",
name: "string, required, max 250",
tags: "string[] | null",
content: "string | null",
transfer_enable: "number | string, required",
prices: {
monthly: "number | string | null",
quarterly: "number | string | null",
half_yearly: "number | string | null",
yearly: "number | string | null",
two_yearly: "number | string | null",
three_yearly: "number | string | null",
onetime: "number | string | null",
reset_traffic: "number | string | null",
},
speed_limit: "number | string | null",
capacity_limit: "number | string | null",
device_limit: "number | string | null",
force_update: "boolean",
reset_traffic_method: "number | null",
users_count: "number | undefined",
active_users_count: "number | undefined",
},
defaults: {
id: null,
group_id: null,
name: "",
tags: [],
content: "",
transfer_enable: "",
prices: {
monthly: "",
quarterly: "",
half_yearly: "",
yearly: "",
two_yearly: "",
three_yearly: "",
onetime: "",
reset_traffic: "",
},
speed_limit: "",
capacity_limit: "",
device_limit: "",
force_update: false,
reset_traffic_method: null,
},
fields: [
"name",
"tags",
"group_id",
"transfer_enable",
"speed_limit",
"device_limit",
"capacity_limit",
"reset_traffic_method",
"prices.*",
"content",
],
editOnlyControls: ["force_update"],
resetTrafficMethodOptions: [
{ value: null, labelKey: "plan.form.reset_method.options.follow_system" },
{ value: 0, labelKey: "plan.form.reset_method.options.monthly_first" },
{ value: 1, labelKey: "plan.form.reset_method.options.monthly_reset" },
{ value: 2, labelKey: "plan.form.reset_method.options.no_reset" },
{ value: 3, labelKey: "plan.form.reset_method.options.yearly_first" },
{ value: 4, labelKey: "plan.form.reset_method.options.yearly_reset" },
],
markdownEditor: {
templateButton: "plan.form.content.template.button",
templateContentKey: "plan.form.content.template.content",
previewToggleKeys: [
"plan.form.content.preview_button.show",
"plan.form.content.preview_button.hide",
],
notes: [
"The editor is configured for markdown view with menu enabled and HTML view disabled.",
"Preview is rendered with markdown-it using html:true.",
],
},
priceAutoFill: {
trigger: "Base price input",
formula: "basePrice * months * discount, rounded to 2 decimals and applied to all price slots.",
matrix: {
monthly: { months: 1, discount: 1 },
quarterly: { months: 3, discount: 0.95 },
half_yearly: { months: 6, discount: 0.9 },
yearly: { months: 12, discount: 0.85 },
two_yearly: { months: 24, discount: 0.8 },
three_yearly: { months: 36, discount: 0.75 },
onetime: { months: 1, discount: 1 },
reset_traffic: { months: 1, discount: 1 },
},
},
},
interactions: [
"Entering sort mode exposes the drag handle column, disables normal pagination, and inflates pageSize so the full list can be reordered at once.",
"Saving sort mode posts the current local row id order to plan.sort and exits drag mode after success.",
"Deleting a row asks for confirmation and refetches the plan list after a success toast.",
"Opening the edit dialog preloads the selected row into react-hook-form and keeps id in the payload.",
"The group field includes an inline 'add group' dialog trigger and refreshes the available groups afterwards.",
"Validation errors are aggregated into a toast message by joining field error messages with line breaks.",
],
notes: [
"The plan page uses the subscribe translation namespace even though it belongs to finance routing.",
"The table is pinned with the actions column on the right in its initial state.",
"Desktop sorting/search/filter behavior is driven through the shared table abstraction rather than page-local DOM code.",
],
sourceRefs: [
"frontend/admin/reverse/output/index-CO3BwsT2.pretty.js:27357",
"frontend/admin/reverse/output/index-CO3BwsT2.pretty.js:295538",
"frontend/admin/reverse/output/index-CO3BwsT2.pretty.js:295602",
"frontend/admin/reverse/output/index-CO3BwsT2.pretty.js:295972",
"frontend/admin/reverse/output/index-CO3BwsT2.pretty.js:296385",
"frontend/admin/reverse/output/index-CO3BwsT2.pretty.js:296492",
],
});