🔀 merge: main into develop
This commit is contained in:
commit
830c18c9e4
81
CHANGELOG.md
81
CHANGELOG.md
@ -19,6 +19,87 @@ This document records all notable changes to ShadCN Admin.
|
|||||||
---
|
---
|
||||||
|
|
||||||
|
|
||||||
|
## [1.3.6](https://github.com/perfect-panel/frontend/compare/v1.3.5...v1.3.6) (2025-12-30)
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes / 问题修复
|
||||||
|
|
||||||
|
* Update server and web version update descriptions to remove version prefix for clarity ([cbd6e29](https://github.com/perfect-panel/frontend/commit/cbd6e29deda1c7a913fae4edb169e0a0a43a3dd9))
|
||||||
|
|
||||||
|
## [1.3.5](https://github.com/perfect-panel/frontend/compare/v1.3.4...v1.3.5) (2025-12-30)
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes / 问题修复
|
||||||
|
|
||||||
|
* Update getUserSubscribe function to accept short and token parameters for improved URL generation ([39ebd09](https://github.com/perfect-panel/frontend/commit/39ebd09f0989b37308c1c803d8e380f967ab0db5))
|
||||||
|
|
||||||
|
## [1.3.4](https://github.com/perfect-panel/frontend/compare/v1.3.3...v1.3.4) (2025-12-29)
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes / 问题修复
|
||||||
|
|
||||||
|
* Only update internalValue in MonacoEditor if propValue has changed ([fe14002](https://github.com/perfect-panel/frontend/commit/fe1400235967f2ad0c1ffbc05d9e15bab370c664))
|
||||||
|
* Set modal prop to false for DropdownMenu in multiple components for improved user experience ([0253b62](https://github.com/perfect-panel/frontend/commit/0253b62b6365476591121b6f18dcb3f3261cb4ff))
|
||||||
|
* Update HTMLEditor onChange event and set DropdownMenu modal to false for better user experience ([ba65588](https://github.com/perfect-panel/frontend/commit/ba65588fdfcfba146cd76bfe7d14e053291f5926))
|
||||||
|
* Update SendCode parameters to use form.watch for email and telephone fields ([51a98af](https://github.com/perfect-panel/frontend/commit/51a98afcae2c8b83c7176898ad8ac2c8b11ad3d9))
|
||||||
|
|
||||||
|
## [1.3.3](https://github.com/perfect-panel/frontend/compare/v1.3.2...v1.3.3) (2025-12-29)
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes / 问题修复
|
||||||
|
|
||||||
|
* Update user_subscribe_id reference in RowMoreActions to use row.id for correct token reset and status toggle ([2156a7f](https://github.com/perfect-panel/frontend/commit/2156a7f1df52970b5ab0816dd1da5dc7cefc1dae))
|
||||||
|
|
||||||
|
## [1.3.2](https://github.com/perfect-panel/frontend/compare/v1.3.1...v1.3.2) (2025-12-29)
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes / 问题修复
|
||||||
|
|
||||||
|
* Refactor update dialog logic in SystemVersionCard for better clarity and handling of update states ([0690deb](https://github.com/perfect-panel/frontend/commit/0690debf6c1bab7472468c742f9b15639f904c20))
|
||||||
|
* Update default inventory value in SubscribeForm to -1 for better handling of inventory state ([2f50c6d](https://github.com/perfect-panel/frontend/commit/2f50c6df3345e26bc92f5730bc8b891d0515a367))
|
||||||
|
|
||||||
|
## [1.3.1](https://github.com/perfect-panel/frontend/compare/v1.3.0...v1.3.1) (2025-12-29)
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes / 问题修复
|
||||||
|
|
||||||
|
* : Update localization files and improve display logic for inventory handling ([83d821a](https://github.com/perfect-panel/frontend/commit/83d821a2dc24d97838af43ab610b65ff0d0d0d15))
|
||||||
|
* Update quantity handling in Purchase and Renewal components based on showOriginalPrice prop ([a274607](https://github.com/perfect-panel/frontend/commit/a2746073a636706b0e214eca84055735417680f3))
|
||||||
|
* Update SubscribeTable to display inventory using Display component for better clarity ([cc52e36](https://github.com/perfect-panel/frontend/commit/cc52e3614d9b6723f4ce8ab6385d626f0fcc54d7))
|
||||||
|
|
||||||
|
## [1.3.0](https://github.com/perfect-panel/frontend/compare/v1.2.4...v1.3.0) (2025-12-29)
|
||||||
|
|
||||||
|
### ✨ Features / 新功能
|
||||||
|
|
||||||
|
* Add original price display option and enhance inventory messages in subscription components ([543a7b9](https://github.com/perfect-panel/frontend/commit/543a7b9eb9b2cd278a70f668b0a0e0e9e261fe57))
|
||||||
|
* Added localized support for user subscription and deletion status, and optimized the subscription form and user interface. ([9f95cec](https://github.com/perfect-panel/frontend/commit/9f95cec876c9ac8bf00a6ca12a5c40243c7171af))
|
||||||
|
* Added reset and pause subscription functionality, and updated the status display. ([bc451ee](https://github.com/perfect-panel/frontend/commit/bc451eea16b51f1ab81b6cacf65201886866e1cc))
|
||||||
|
* Added the option to restore subscription, and updated the relevant description and confirmation information. ([5f5c339](https://github.com/perfect-panel/frontend/commit/5f5c33987e54533c5348223b3cebe31aef91aa25))
|
||||||
|
* Enhance DatePicker component with clear button and improved value handling ([b27b928](https://github.com/perfect-panel/frontend/commit/b27b9287be0eb26fe5d5189b618a32db340c506c))
|
||||||
|
* Refactor SubscriptionForm layout for improved readability and maintainability ([f432ba0](https://github.com/perfect-panel/frontend/commit/f432ba06f9d3a86ec00bfd316d1c020ddf779de6))
|
||||||
|
* Update API proxy target to use environment variable for improved configurability ([8d514df](https://github.com/perfect-panel/frontend/commit/8d514dfd8f0a8fb9acc97a288addd1b1cbc882b8))
|
||||||
|
* Update queryKey structure in Purchase and Renewal components for improved order creation ([9559e00](https://github.com/perfect-panel/frontend/commit/9559e003b7821303f7d0ac7f4da5fae4bf1ccad4))
|
||||||
|
* Update Vite configuration to load environment variables for improved API proxy setup ([d36a2c9](https://github.com/perfect-panel/frontend/commit/d36a2c902b7943441282278009c5e5210d30c746))
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes / 问题修复
|
||||||
|
|
||||||
|
* Replace tag removal icon with a button for better accessibility and event handling ([3751f64](https://github.com/perfect-panel/frontend/commit/3751f64f73cd70eaddd44cb650dd38949f0ca069))
|
||||||
|
* Uncomment navigation to dashboard for authenticated users ([4d15b2b](https://github.com/perfect-panel/frontend/commit/4d15b2b6fc8f0d178c7c749aa9e1d9826bb706f8))
|
||||||
|
|
||||||
|
### 📚 Documentation / 文档更新
|
||||||
|
|
||||||
|
* Update default administrator account information and security recommendations in installation guides ([7279275](https://github.com/perfect-panel/frontend/commit/7279275532ad0b73994ffe5cf2e4c2f8dabc280b))
|
||||||
|
|
||||||
|
## [1.2.4](https://github.com/perfect-panel/frontend/compare/v1.2.3...v1.2.4) (2025-12-28)
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes / 问题修复
|
||||||
|
|
||||||
|
* Add onSuccess callback to Unsubscribe component and conditionally render Renewal component ([7b5367a](https://github.com/perfect-panel/frontend/commit/7b5367a9a99ac8ae608a765b79a66c1f7380dcd8))
|
||||||
|
* Remove the system log dialog component from the system version card ([71cb827](https://github.com/perfect-panel/frontend/commit/71cb827918ee3250f0c9d06d46d876ce6799b8ac))
|
||||||
|
* Update invite link format in auth forms and sidebar to include hash fragment for routing. ([7a8c010](https://github.com/perfect-panel/frontend/commit/7a8c0102958a859c9e7476810d5c9b822f882692))
|
||||||
|
|
||||||
|
### 📚 Documentation / 文档更新
|
||||||
|
|
||||||
|
* Add one-click installation script for PPanel with Docker support ([912c5c4](https://github.com/perfect-panel/frontend/commit/912c5c4cb63eeb0ecbc33bef6b31bd50d83d6491))
|
||||||
|
|
||||||
|
### 🔧 Chores / 其他变更
|
||||||
|
|
||||||
|
* **release:** Release 1.2.4-dev.1 / 发布版本 1.2.4-dev.1 [skip ci] ([62d45bb](https://github.com/perfect-panel/frontend/commit/62d45bbac17fab9656c1cce029a379ce8bb757d6))
|
||||||
|
|
||||||
## [1.2.4-dev.1](https://github.com/perfect-panel/frontend/compare/v1.2.3...v1.2.4-dev.1) (2025-12-22)
|
## [1.2.4-dev.1](https://github.com/perfect-panel/frontend/compare/v1.2.3...v1.2.4-dev.1) (2025-12-22)
|
||||||
|
|
||||||
### 🐛 Bug Fixes / 问题修复
|
### 🐛 Bug Fixes / 问题修复
|
||||||
|
|||||||
@ -44,6 +44,8 @@
|
|||||||
"60003": "An existing subscription is detected. Please cancel it before proceeding.",
|
"60003": "An existing subscription is detected. Please cancel it before proceeding.",
|
||||||
"60004": "Unable to delete at the moment as the subscription has active users.",
|
"60004": "Unable to delete at the moment as the subscription has active users.",
|
||||||
"60005": "Single subscription mode has exceeded user limit",
|
"60005": "Single subscription mode has exceeded user limit",
|
||||||
|
"60006": "User quota limit has been reached, unable to continue.",
|
||||||
|
"60007": "Insufficient inventory, please try again later or contact the administrator.",
|
||||||
"70001": "Incorrect verification code, please re-enter.",
|
"70001": "Incorrect verification code, please re-enter.",
|
||||||
"80001": "Task was not successfully queued, please try again later.",
|
"80001": "Task was not successfully queued, please try again later.",
|
||||||
"90001": "Please disable DEBUG mode and try again.",
|
"90001": "Please disable DEBUG mode and try again.",
|
||||||
@ -61,5 +63,5 @@
|
|||||||
"system": "System",
|
"system": "System",
|
||||||
"toggle": "Toggle theme"
|
"toggle": "Toggle theme"
|
||||||
},
|
},
|
||||||
"unlimited": "unlimited"
|
"unlimited": "Unlimited"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -30,6 +30,7 @@
|
|||||||
"discountPercent": "Discount Percentage",
|
"discountPercent": "Discount Percentage",
|
||||||
"Hour": "Hour",
|
"Hour": "Hour",
|
||||||
"inventory": "Subscription Limit",
|
"inventory": "Subscription Limit",
|
||||||
|
"unlimitedInventory": "Unlimited (enter -1)",
|
||||||
"language": "Language",
|
"language": "Language",
|
||||||
"languageDescription": "Leave empty for default without language restriction",
|
"languageDescription": "Leave empty for default without language restriction",
|
||||||
"languagePlaceholder": "Language identifier for the subscription, e.g., en-US, zh-CN",
|
"languagePlaceholder": "Language identifier for the subscription, e.g., en-US, zh-CN",
|
||||||
@ -56,6 +57,8 @@
|
|||||||
"resetOn1st": "Reset on the 1st",
|
"resetOn1st": "Reset on the 1st",
|
||||||
"selectResetCycle": "Please select a reset cycle",
|
"selectResetCycle": "Please select a reset cycle",
|
||||||
"selectUnitTime": "Please select a unit of time",
|
"selectUnitTime": "Please select a unit of time",
|
||||||
|
"showOriginalPrice": "Show Original Price",
|
||||||
|
"showOriginalPriceDescription": "When enabled, the subscription card will display both the original price and the discounted price to help users understand the discount amount",
|
||||||
"speedLimit": "Speed Limit ",
|
"speedLimit": "Speed Limit ",
|
||||||
"traffic": "Traffic",
|
"traffic": "Traffic",
|
||||||
"unitPrice": "Unit Price",
|
"unitPrice": "Unit Price",
|
||||||
|
|||||||
@ -13,9 +13,9 @@
|
|||||||
"systemServices": "System Services",
|
"systemServices": "System Services",
|
||||||
"update": "Update",
|
"update": "Update",
|
||||||
"updateFailed": "Update failed",
|
"updateFailed": "Update failed",
|
||||||
"updateServerDescription": "Are you sure you want to update the server version from V{{current}} to V{{latest}}?",
|
"updateServerDescription": "Are you sure you want to update the server version from {{current}} to {{latest}}?",
|
||||||
"updateSuccess": "Update completed successfully",
|
"updateSuccess": "Update completed successfully",
|
||||||
"updateWebDescription": "Are you sure you want to update the web version from V{{current}} to V{{latest}}?",
|
"updateWebDescription": "Are you sure you want to update the web version from {{current}} to {{latest}}?",
|
||||||
"userUpdateSuccess": "User updated successfully",
|
"userUpdateSuccess": "User updated successfully",
|
||||||
"webVersion": "Web Version"
|
"webVersion": "Web Version"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -25,9 +25,11 @@
|
|||||||
"createSuccess": "Created successfully",
|
"createSuccess": "Created successfully",
|
||||||
"createUser": "Create User",
|
"createUser": "Create User",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
|
"deleted": "Deleted",
|
||||||
"deleteDescription": "This action cannot be undone.",
|
"deleteDescription": "This action cannot be undone.",
|
||||||
"deleteSubscriptionDescription": "This action cannot be undone.",
|
"deleteSubscriptionDescription": "This action cannot be undone.",
|
||||||
"deleteSuccess": "Deleted successfully",
|
"deleteSuccess": "Deleted successfully",
|
||||||
|
"isDeleted": "Status",
|
||||||
"deviceLimit": "Device Limit",
|
"deviceLimit": "Device Limit",
|
||||||
"download": "Download",
|
"download": "Download",
|
||||||
"downloadTraffic": "Download Traffic",
|
"downloadTraffic": "Download Traffic",
|
||||||
@ -51,6 +53,7 @@
|
|||||||
"loginStatus": "Login Status",
|
"loginStatus": "Login Status",
|
||||||
"manager": "Administrator",
|
"manager": "Administrator",
|
||||||
"more": "More",
|
"more": "More",
|
||||||
|
"normal": "Normal",
|
||||||
"notifySettingsTitle": "Notify Settings",
|
"notifySettingsTitle": "Notify Settings",
|
||||||
"offline": "Offline",
|
"offline": "Offline",
|
||||||
"online": "Online",
|
"online": "Online",
|
||||||
@ -80,6 +83,25 @@
|
|||||||
"toggleSubscriptionStatus": "Toggle Status",
|
"toggleSubscriptionStatus": "Toggle Status",
|
||||||
"toggleSubscriptionStatusDescription": "This will toggle the subscription status.",
|
"toggleSubscriptionStatusDescription": "This will toggle the subscription status.",
|
||||||
"resetTime": "Reset Time",
|
"resetTime": "Reset Time",
|
||||||
|
"resetToken": "Reset Subscription Address",
|
||||||
|
"resetTokenDescription": "This will reset the subscription address and regenerate a new token.",
|
||||||
|
"resetTokenSuccess": "Subscription address reset successfully",
|
||||||
|
"confirmResetToken": "Confirm Reset Subscription Address",
|
||||||
|
"stopSubscribe": "Stop Subscription",
|
||||||
|
"stopSubscribeDescription": "This will stop the subscription temporarily. User will not be able to use it.",
|
||||||
|
"stopSubscribeSuccess": "Subscription stopped successfully",
|
||||||
|
"confirmStopSubscribe": "Confirm Stop Subscription",
|
||||||
|
"resumeSubscribe": "Resume Subscription",
|
||||||
|
"resumeSubscribeDescription": "This will resume the subscription and allow the user to use it.",
|
||||||
|
"resumeSubscribeSuccess": "Subscription resumed successfully",
|
||||||
|
"confirmResumeSubscribe": "Confirm Resume Subscription",
|
||||||
|
"status": "Status",
|
||||||
|
"statusPending": "Pending",
|
||||||
|
"statusActive": "Active",
|
||||||
|
"statusFinished": "Finished",
|
||||||
|
"statusExpired": "Expired",
|
||||||
|
"statusDeducted": "Deducted",
|
||||||
|
"statusStopped": "Stopped",
|
||||||
"save": "Save",
|
"save": "Save",
|
||||||
"speedLimit": "Speed Limit",
|
"speedLimit": "Speed Limit",
|
||||||
"startTime": "startTime",
|
"startTime": "startTime",
|
||||||
|
|||||||
@ -44,6 +44,8 @@
|
|||||||
"60003": "检测到现有订阅,请先取消后再继续。",
|
"60003": "检测到现有订阅,请先取消后再继续。",
|
||||||
"60004": "由于订阅有活跃用户,暂时无法删除。",
|
"60004": "由于订阅有活跃用户,暂时无法删除。",
|
||||||
"60005": "单一订阅模式已超过用户限制",
|
"60005": "单一订阅模式已超过用户限制",
|
||||||
|
"60006": "用户配额已达到限制,无法继续操作。",
|
||||||
|
"60007": "库存不足,请稍后再试或联系管理员。",
|
||||||
"70001": "验证码不正确,请重新输入。",
|
"70001": "验证码不正确,请重新输入。",
|
||||||
"80001": "任务未成功加入队列,请稍后再试。",
|
"80001": "任务未成功加入队列,请稍后再试。",
|
||||||
"90001": "请禁用 DEBUG 模式后再试。",
|
"90001": "请禁用 DEBUG 模式后再试。",
|
||||||
|
|||||||
@ -30,6 +30,7 @@
|
|||||||
"discountPercent": "折扣百分比",
|
"discountPercent": "折扣百分比",
|
||||||
"Hour": "小时",
|
"Hour": "小时",
|
||||||
"inventory": "订阅库存",
|
"inventory": "订阅库存",
|
||||||
|
"unlimitedInventory": "无限制(输入 -1)",
|
||||||
"language": "语言",
|
"language": "语言",
|
||||||
"languageDescription": "留空为默认无语言限制",
|
"languageDescription": "留空为默认无语言限制",
|
||||||
"languagePlaceholder": "订阅的语言标识符,例如 en-US、zh-CN",
|
"languagePlaceholder": "订阅的语言标识符,例如 en-US、zh-CN",
|
||||||
@ -56,6 +57,8 @@
|
|||||||
"resetOn1st": "每月1日重置",
|
"resetOn1st": "每月1日重置",
|
||||||
"selectResetCycle": "请选择重置周期",
|
"selectResetCycle": "请选择重置周期",
|
||||||
"selectUnitTime": "请选择时间单位",
|
"selectUnitTime": "请选择时间单位",
|
||||||
|
"showOriginalPrice": "显示原价",
|
||||||
|
"showOriginalPriceDescription": "开启后,在订阅卡片上将会显示原价和折后价,帮助用户了解优惠幅度",
|
||||||
"speedLimit": "速度限制",
|
"speedLimit": "速度限制",
|
||||||
"traffic": "流量",
|
"traffic": "流量",
|
||||||
"unitPrice": "单价",
|
"unitPrice": "单价",
|
||||||
|
|||||||
@ -13,9 +13,9 @@
|
|||||||
"systemServices": "系统服务",
|
"systemServices": "系统服务",
|
||||||
"update": "更新",
|
"update": "更新",
|
||||||
"updateFailed": "更新失败",
|
"updateFailed": "更新失败",
|
||||||
"updateServerDescription": "确定要将服务器版本从 V{{current}} 更新到 V{{latest}} 吗?",
|
"updateServerDescription": "确定要将服务器版本从 {{current}} 更新到 {{latest}} 吗?",
|
||||||
"updateSuccess": "更新成功",
|
"updateSuccess": "更新成功",
|
||||||
"updateWebDescription": "确定要将前端版本从 V{{current}} 更新到 V{{latest}} 吗?",
|
"updateWebDescription": "确定要将前端版本从 {{current}} 更新到 {{latest}} 吗?",
|
||||||
"userUpdateSuccess": "用户端更新成功",
|
"userUpdateSuccess": "用户端更新成功",
|
||||||
"webVersion": "前端版本"
|
"webVersion": "前端版本"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -25,9 +25,11 @@
|
|||||||
"createSuccess": "创建成功",
|
"createSuccess": "创建成功",
|
||||||
"createUser": "创建用户",
|
"createUser": "创建用户",
|
||||||
"delete": "删除",
|
"delete": "删除",
|
||||||
|
"deleted": "已删除",
|
||||||
"deleteDescription": "此操作无法撤销。",
|
"deleteDescription": "此操作无法撤销。",
|
||||||
"deleteSubscriptionDescription": "此操作无法撤销。",
|
"deleteSubscriptionDescription": "此操作无法撤销。",
|
||||||
"deleteSuccess": "删除成功",
|
"deleteSuccess": "删除成功",
|
||||||
|
"isDeleted": "状态",
|
||||||
"deviceLimit": "IP限制",
|
"deviceLimit": "IP限制",
|
||||||
"download": "下载",
|
"download": "下载",
|
||||||
"downloadTraffic": "下载流量",
|
"downloadTraffic": "下载流量",
|
||||||
@ -51,6 +53,7 @@
|
|||||||
"loginStatus": "登录状态",
|
"loginStatus": "登录状态",
|
||||||
"manager": "管理员",
|
"manager": "管理员",
|
||||||
"more": "更多",
|
"more": "更多",
|
||||||
|
"normal": "正常",
|
||||||
"notifySettingsTitle": "通知设置",
|
"notifySettingsTitle": "通知设置",
|
||||||
"offline": "离线",
|
"offline": "离线",
|
||||||
"online": "在线",
|
"online": "在线",
|
||||||
@ -80,6 +83,25 @@
|
|||||||
"toggleSubscriptionStatus": "切换状态",
|
"toggleSubscriptionStatus": "切换状态",
|
||||||
"toggleSubscriptionStatusDescription": "将切换该订阅的启用/停用状态。",
|
"toggleSubscriptionStatusDescription": "将切换该订阅的启用/停用状态。",
|
||||||
"resetTime": "重置时间",
|
"resetTime": "重置时间",
|
||||||
|
"resetToken": "重置订阅地址",
|
||||||
|
"resetTokenDescription": "这将重置订阅地址并重新生成新的令牌。",
|
||||||
|
"resetTokenSuccess": "订阅地址重置成功",
|
||||||
|
"confirmResetToken": "确认重置订阅地址",
|
||||||
|
"stopSubscribe": "暂停订阅",
|
||||||
|
"stopSubscribeDescription": "这将暂时停止订阅。用户将无法使用。",
|
||||||
|
"stopSubscribeSuccess": "订阅已暂停",
|
||||||
|
"confirmStopSubscribe": "确认暂停订阅",
|
||||||
|
"resumeSubscribe": "恢复订阅",
|
||||||
|
"resumeSubscribeDescription": "这将恢复订阅,允许用户继续使用。",
|
||||||
|
"resumeSubscribeSuccess": "订阅已恢复",
|
||||||
|
"confirmResumeSubscribe": "确认恢复订阅",
|
||||||
|
"status": "状态",
|
||||||
|
"statusPending": "待处理",
|
||||||
|
"statusActive": "活跃",
|
||||||
|
"statusFinished": "已完成",
|
||||||
|
"statusExpired": "已过期",
|
||||||
|
"statusDeducted": "已扣除",
|
||||||
|
"statusStopped": "已停止",
|
||||||
"save": "保存",
|
"save": "保存",
|
||||||
"speedLimit": "速度限制",
|
"speedLimit": "速度限制",
|
||||||
"startTime": "开始时间",
|
"startTime": "开始时间",
|
||||||
|
|||||||
@ -28,7 +28,7 @@ export function Display<T extends number | undefined | null>({
|
|||||||
if (
|
if (
|
||||||
["traffic", "trafficSpeed", "number"].includes(type) &&
|
["traffic", "trafficSpeed", "number"].includes(type) &&
|
||||||
unlimited &&
|
unlimited &&
|
||||||
!value
|
(value === 0 || value === null || value === undefined)
|
||||||
) {
|
) {
|
||||||
return t("unlimited");
|
return t("unlimited");
|
||||||
}
|
}
|
||||||
@ -42,7 +42,7 @@ export function Display<T extends number | undefined | null>({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (type === "number") {
|
if (type === "number") {
|
||||||
return value ? value.toString() : "0";
|
return value !== null && value !== undefined ? value.toString() : "0";
|
||||||
}
|
}
|
||||||
|
|
||||||
return "0";
|
return "0";
|
||||||
|
|||||||
@ -538,7 +538,7 @@ export default function EmailSettingsForm() {
|
|||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<HTMLEditor
|
<HTMLEditor
|
||||||
onBlur={field.onChange}
|
onChange={field.onChange}
|
||||||
placeholder={t(
|
placeholder={t(
|
||||||
"email.inputPlaceholder",
|
"email.inputPlaceholder",
|
||||||
"Please enter"
|
"Please enter"
|
||||||
@ -642,7 +642,7 @@ export default function EmailSettingsForm() {
|
|||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<HTMLEditor
|
<HTMLEditor
|
||||||
onBlur={field.onChange}
|
onChange={field.onChange}
|
||||||
placeholder={t(
|
placeholder={t(
|
||||||
"email.inputPlaceholder",
|
"email.inputPlaceholder",
|
||||||
"Please enter"
|
"Please enter"
|
||||||
@ -713,7 +713,7 @@ export default function EmailSettingsForm() {
|
|||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<HTMLEditor
|
<HTMLEditor
|
||||||
onBlur={field.onChange}
|
onChange={field.onChange}
|
||||||
placeholder={t(
|
placeholder={t(
|
||||||
"email.inputPlaceholder",
|
"email.inputPlaceholder",
|
||||||
"Please enter"
|
"Please enter"
|
||||||
@ -795,7 +795,7 @@ export default function EmailSettingsForm() {
|
|||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<HTMLEditor
|
<HTMLEditor
|
||||||
onBlur={field.onChange}
|
onChange={field.onChange}
|
||||||
placeholder={t(
|
placeholder={t(
|
||||||
"email.inputPlaceholder",
|
"email.inputPlaceholder",
|
||||||
"Please enter"
|
"Please enter"
|
||||||
|
|||||||
@ -179,7 +179,7 @@ export default function RegisterForm({
|
|||||||
/>
|
/>
|
||||||
<SendCode
|
<SendCode
|
||||||
params={{
|
params={{
|
||||||
...form.getValues(),
|
email: form.watch("email"),
|
||||||
type: 1,
|
type: 1,
|
||||||
}}
|
}}
|
||||||
type="email"
|
type="email"
|
||||||
|
|||||||
@ -98,7 +98,7 @@ export default function ResetForm({
|
|||||||
/>
|
/>
|
||||||
<SendCode
|
<SendCode
|
||||||
params={{
|
params={{
|
||||||
...form.getValues(),
|
email: form.watch("email"),
|
||||||
type: 2,
|
type: 2,
|
||||||
}}
|
}}
|
||||||
type="email"
|
type="email"
|
||||||
|
|||||||
@ -15,7 +15,7 @@ export default function Auth() {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (user) {
|
if (user) {
|
||||||
// navigate({ to: "/dashboard" });
|
navigate({ to: "/dashboard" });
|
||||||
}
|
}
|
||||||
}, [navigate, user]);
|
}, [navigate, user]);
|
||||||
|
|
||||||
|
|||||||
@ -69,7 +69,7 @@ export default function SystemVersionCard() {
|
|||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const { data } = await basicCheckServiceVersion(
|
const { data } = await basicCheckServiceVersion(
|
||||||
{
|
{
|
||||||
service_name: "admin",
|
service_name: "admin-web-with-api",
|
||||||
secret: moduleConfig!.secret,
|
secret: moduleConfig!.secret,
|
||||||
},
|
},
|
||||||
{ skipErrorHandler: true }
|
{ skipErrorHandler: true }
|
||||||
@ -105,13 +105,13 @@ export default function SystemVersionCard() {
|
|||||||
setIsUpdatingWeb(true);
|
setIsUpdatingWeb(true);
|
||||||
try {
|
try {
|
||||||
await basicUpdateService({
|
await basicUpdateService({
|
||||||
service_name: "admin",
|
service_name: "admin-web-with-api",
|
||||||
secret: moduleConfig.secret,
|
secret: moduleConfig.secret,
|
||||||
});
|
});
|
||||||
toast.success(t("adminUpdateSuccess", "Admin updated successfully"));
|
toast.success(t("adminUpdateSuccess", "Admin updated successfully"));
|
||||||
|
|
||||||
await basicUpdateService({
|
await basicUpdateService({
|
||||||
service_name: "user",
|
service_name: "user-web-with-api",
|
||||||
secret: moduleConfig.secret,
|
secret: moduleConfig.secret,
|
||||||
});
|
});
|
||||||
toast.success(t("userUpdateSuccess", "User updated successfully"));
|
toast.success(t("userUpdateSuccess", "User updated successfully"));
|
||||||
@ -188,52 +188,52 @@ export default function SystemVersionCard() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<Badge>V{packageJson.version}</Badge>
|
<Badge>V{packageJson.version}</Badge>
|
||||||
{hasWebNewVersion && webVersionInfo && (
|
<AlertDialog onOpenChange={setOpenUpdateWeb} open={openUpdateWeb}>
|
||||||
<AlertDialog onOpenChange={setOpenUpdateWeb} open={openUpdateWeb}>
|
<AlertDialogTrigger asChild>
|
||||||
<AlertDialogTrigger asChild>
|
<Button
|
||||||
<Button
|
className="h-6 px-2 text-xs"
|
||||||
className="h-6 px-2 text-xs"
|
disabled={!hasWebNewVersion || isUpdatingWeb}
|
||||||
disabled={isUpdatingWeb}
|
size="sm"
|
||||||
size="sm"
|
variant="outline"
|
||||||
variant="outline"
|
>
|
||||||
>
|
<Icon className="mr-1 h-3 w-3" icon="mdi:download" />
|
||||||
<Icon className="mr-1 h-3 w-3" icon="mdi:download" />
|
{hasWebNewVersion && webVersionInfo
|
||||||
{t("update", "Update")} V{webVersionInfo.latest_version}
|
? `${t("update", "Update")} ${webVersionInfo.latest_version}`
|
||||||
|
: t("update", "Update")}
|
||||||
|
</Button>
|
||||||
|
</AlertDialogTrigger>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>
|
||||||
|
{t("confirmUpdate", "Confirm Update")}
|
||||||
|
</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
{webVersionInfo
|
||||||
|
? t(
|
||||||
|
"updateWebDescription",
|
||||||
|
"Are you sure you want to update the web version from {{current}} to {{latest}}?",
|
||||||
|
{
|
||||||
|
current: webVersionInfo.current_version,
|
||||||
|
latest: webVersionInfo.latest_version,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
: t(
|
||||||
|
"updateDescription",
|
||||||
|
"Are you sure you want to update?"
|
||||||
|
)}
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>{t("cancel", "Cancel")}</AlertDialogCancel>
|
||||||
|
<Button disabled={isUpdatingWeb} onClick={handleUpdateWeb}>
|
||||||
|
{isUpdatingWeb && (
|
||||||
|
<Icon className="mr-2 animate-spin" icon="mdi:loading" />
|
||||||
|
)}
|
||||||
|
{t("confirmUpdate", "Confirm Update")}
|
||||||
</Button>
|
</Button>
|
||||||
</AlertDialogTrigger>
|
</AlertDialogFooter>
|
||||||
<AlertDialogContent>
|
</AlertDialogContent>
|
||||||
<AlertDialogHeader>
|
</AlertDialog>
|
||||||
<AlertDialogTitle>
|
|
||||||
{t("confirmUpdate", "Confirm Update")}
|
|
||||||
</AlertDialogTitle>
|
|
||||||
<AlertDialogDescription>
|
|
||||||
{t(
|
|
||||||
"updateWebDescription",
|
|
||||||
"Are you sure you want to update the web version from V{{current}} to V{{latest}}?",
|
|
||||||
{
|
|
||||||
current: packageJson.version,
|
|
||||||
latest: webVersionInfo.latest_version,
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
</AlertDialogDescription>
|
|
||||||
</AlertDialogHeader>
|
|
||||||
<AlertDialogFooter>
|
|
||||||
<AlertDialogCancel>
|
|
||||||
{t("cancel", "Cancel")}
|
|
||||||
</AlertDialogCancel>
|
|
||||||
<Button disabled={isUpdatingWeb} onClick={handleUpdateWeb}>
|
|
||||||
{isUpdatingWeb && (
|
|
||||||
<Icon
|
|
||||||
className="mr-2 animate-spin"
|
|
||||||
icon="mdi:loading"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{t("confirmUpdate", "Confirm Update")}
|
|
||||||
</Button>
|
|
||||||
</AlertDialogFooter>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialog>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -251,62 +251,63 @@ export default function SystemVersionCard() {
|
|||||||
serverVersionInfo?.current_version ||
|
serverVersionInfo?.current_version ||
|
||||||
"1.0.0"}
|
"1.0.0"}
|
||||||
</Badge>
|
</Badge>
|
||||||
{hasServerNewVersion && serverVersionInfo && moduleConfig && (
|
<AlertDialog
|
||||||
<AlertDialog
|
onOpenChange={setOpenUpdateServer}
|
||||||
onOpenChange={setOpenUpdateServer}
|
open={openUpdateServer}
|
||||||
open={openUpdateServer}
|
>
|
||||||
>
|
<AlertDialogTrigger asChild>
|
||||||
<AlertDialogTrigger asChild>
|
<Button
|
||||||
|
className="h-6 px-2 text-xs"
|
||||||
|
disabled={!hasServerNewVersion || isUpdatingServer}
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
>
|
||||||
|
<Icon className="mr-1 h-3 w-3" icon="mdi:download" />
|
||||||
|
{hasServerNewVersion && serverVersionInfo
|
||||||
|
? `${t("update", "Update")} ${serverVersionInfo.latest_version}`
|
||||||
|
: t("update", "Update")}
|
||||||
|
</Button>
|
||||||
|
</AlertDialogTrigger>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>
|
||||||
|
{t("confirmUpdate", "Confirm Update")}
|
||||||
|
</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
{serverVersionInfo && moduleConfig
|
||||||
|
? t(
|
||||||
|
"updateServerDescription",
|
||||||
|
"Are you sure you want to update the server version from {{current}} to {{latest}}?",
|
||||||
|
{
|
||||||
|
current:
|
||||||
|
moduleConfig.service_version ||
|
||||||
|
serverVersionInfo.current_version,
|
||||||
|
latest: serverVersionInfo.latest_version,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
: t(
|
||||||
|
"updateDescription",
|
||||||
|
"Are you sure you want to update?"
|
||||||
|
)}
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>{t("cancel", "Cancel")}</AlertDialogCancel>
|
||||||
<Button
|
<Button
|
||||||
className="h-6 px-2 text-xs"
|
disabled={isUpdatingServer || !moduleConfig}
|
||||||
disabled={isUpdatingServer}
|
onClick={() =>
|
||||||
size="sm"
|
moduleConfig &&
|
||||||
variant="outline"
|
updateServerMutation.mutate(moduleConfig.service_name)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Icon className="mr-1 h-3 w-3" icon="mdi:download" />
|
{isUpdatingServer && (
|
||||||
{t("update", "Update")} V{serverVersionInfo.latest_version}
|
<Icon className="mr-2 animate-spin" icon="mdi:loading" />
|
||||||
|
)}
|
||||||
|
{t("confirmUpdate", "Confirm Update")}
|
||||||
</Button>
|
</Button>
|
||||||
</AlertDialogTrigger>
|
</AlertDialogFooter>
|
||||||
<AlertDialogContent>
|
</AlertDialogContent>
|
||||||
<AlertDialogHeader>
|
</AlertDialog>
|
||||||
<AlertDialogTitle>
|
|
||||||
{t("confirmUpdate", "Confirm Update")}
|
|
||||||
</AlertDialogTitle>
|
|
||||||
<AlertDialogDescription>
|
|
||||||
{t(
|
|
||||||
"updateServerDescription",
|
|
||||||
"Are you sure you want to update the server version from V{{current}} to V{{latest}}?",
|
|
||||||
{
|
|
||||||
current:
|
|
||||||
moduleConfig.service_version ||
|
|
||||||
serverVersionInfo.current_version,
|
|
||||||
latest: serverVersionInfo.latest_version,
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
</AlertDialogDescription>
|
|
||||||
</AlertDialogHeader>
|
|
||||||
<AlertDialogFooter>
|
|
||||||
<AlertDialogCancel>
|
|
||||||
{t("cancel", "Cancel")}
|
|
||||||
</AlertDialogCancel>
|
|
||||||
<Button
|
|
||||||
disabled={isUpdatingServer}
|
|
||||||
onClick={() =>
|
|
||||||
updateServerMutation.mutate(moduleConfig.service_name)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{isUpdatingServer && (
|
|
||||||
<Icon
|
|
||||||
className="mr-2 animate-spin"
|
|
||||||
icon="mdi:loading"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{t("confirmUpdate", "Confirm Update")}
|
|
||||||
</Button>
|
|
||||||
</AlertDialogFooter>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialog>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@ -63,7 +63,7 @@ interface SubscribeFormProps<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const defaultValues = {
|
const defaultValues = {
|
||||||
inventory: 0,
|
inventory: -1,
|
||||||
speed_limit: 0,
|
speed_limit: 0,
|
||||||
device_limit: 0,
|
device_limit: 0,
|
||||||
traffic: 0,
|
traffic: 0,
|
||||||
@ -278,7 +278,7 @@ export default function SubscribeForm<T extends Record<string, any>>({
|
|||||||
{trigger}
|
{trigger}
|
||||||
</Button>
|
</Button>
|
||||||
</SheetTrigger>
|
</SheetTrigger>
|
||||||
<SheetContent className="w-[800px] max-w-full gap-0 md:max-w-screen-md">
|
<SheetContent className="w-[800px] max-w-full gap-0 md:max-w-3xl">
|
||||||
<SheetHeader>
|
<SheetHeader>
|
||||||
<SheetTitle>{title}</SheetTitle>
|
<SheetTitle>{title}</SheetTitle>
|
||||||
</SheetHeader>
|
</SheetHeader>
|
||||||
@ -446,11 +446,10 @@ export default function SubscribeForm<T extends Record<string, any>>({
|
|||||||
<FormLabel>{t("form.inventory")}</FormLabel>
|
<FormLabel>{t("form.inventory")}</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<EnhancedInput
|
<EnhancedInput
|
||||||
min={0}
|
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
form.setValue(field.name, value);
|
form.setValue(field.name, value);
|
||||||
}}
|
}}
|
||||||
placeholder={t("form.noLimit")}
|
placeholder={t("form.unlimitedInventory")}
|
||||||
step={1}
|
step={1}
|
||||||
type="number"
|
type="number"
|
||||||
value={field.value}
|
value={field.value}
|
||||||
@ -901,6 +900,33 @@ export default function SubscribeForm<T extends Record<string, any>>({
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="show_original_price"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<FormLabel>
|
||||||
|
{t("form.showOriginalPrice")}
|
||||||
|
</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
{t("form.showOriginalPriceDescription")}
|
||||||
|
</FormDescription>
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={!!field.value}
|
||||||
|
onCheckedChange={(value) => {
|
||||||
|
form.setValue(field.name, value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</div>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
|
|||||||
@ -208,15 +208,14 @@ export default function SubscribeTable() {
|
|||||||
{
|
{
|
||||||
accessorKey: "inventory",
|
accessorKey: "inventory",
|
||||||
header: t("inventory"),
|
header: t("inventory"),
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => {
|
||||||
<Display
|
const inventory = row.getValue("inventory") as number;
|
||||||
type="number"
|
return inventory === -1 ? (
|
||||||
unlimited
|
<Display type="number" unlimited value={0} />
|
||||||
value={
|
) : (
|
||||||
row.getValue("inventory") === -1 ? 0 : row.getValue("inventory")
|
<Display type="number" unlimited value={inventory} />
|
||||||
}
|
);
|
||||||
/>
|
},
|
||||||
),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "quota",
|
accessorKey: "quota",
|
||||||
|
|||||||
@ -98,7 +98,7 @@ function DynamicField({
|
|||||||
field.generate ? (
|
field.generate ? (
|
||||||
field.generate.functions &&
|
field.generate.functions &&
|
||||||
field.generate.functions.length > 0 ? (
|
field.generate.functions.length > 0 ? (
|
||||||
<DropdownMenu>
|
<DropdownMenu modal={false}>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button size="sm" type="button" variant="ghost">
|
<Button size="sm" type="button" variant="ghost">
|
||||||
<Icon className="h-4 w-4" icon="mdi:key" />
|
<Icon className="h-4 w-4" icon="mdi:key" />
|
||||||
|
|||||||
@ -92,7 +92,7 @@ export default function User() {
|
|||||||
<Button variant="destructive">{t("delete", "Delete")}</Button>
|
<Button variant="destructive">{t("delete", "Delete")}</Button>
|
||||||
}
|
}
|
||||||
/>,
|
/>,
|
||||||
<DropdownMenu key="more">
|
<DropdownMenu key="more" modal={false}>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button variant="outline">{t("more", "More")}</Button>
|
<Button variant="outline">{t("more", "More")}</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
@ -176,6 +176,18 @@ export default function User() {
|
|||||||
accessorKey: "id",
|
accessorKey: "id",
|
||||||
header: "ID",
|
header: "ID",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "deleted_at",
|
||||||
|
header: t("isDeleted", "Deleted"),
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const deletedAt = row.getValue("deleted_at") as number | undefined;
|
||||||
|
return deletedAt ? (
|
||||||
|
<Badge variant="destructive">{t("deleted", "Deleted")}</Badge>
|
||||||
|
) : (
|
||||||
|
<Badge variant="outline">{t("normal", "Normal")}</Badge>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "auth_methods",
|
accessorKey: "auth_methods",
|
||||||
header: t("userName", "Username"),
|
header: t("userName", "Username"),
|
||||||
@ -371,16 +383,13 @@ function SubscriptionSheet({ userId }: { userId: number }) {
|
|||||||
<SheetTrigger asChild>
|
<SheetTrigger asChild>
|
||||||
<Button variant="secondary">{t("subscription", "Subscription")}</Button>
|
<Button variant="secondary">{t("subscription", "Subscription")}</Button>
|
||||||
</SheetTrigger>
|
</SheetTrigger>
|
||||||
<SheetContent
|
<SheetContent className="w-[1000px] max-w-full md:max-w-7xl" side="right">
|
||||||
className="w-[1000px] max-w-full md:max-w-screen-xl"
|
|
||||||
side="right"
|
|
||||||
>
|
|
||||||
<SheetHeader>
|
<SheetHeader>
|
||||||
<SheetTitle>
|
<SheetTitle>
|
||||||
{t("subscriptionList", "Subscription List")} · ID: {userId}
|
{t("subscriptionList", "Subscription List")} · ID: {userId}
|
||||||
</SheetTitle>
|
</SheetTitle>
|
||||||
</SheetHeader>
|
</SheetHeader>
|
||||||
<div className="mt-2">
|
<div className="mt-2 px-4">
|
||||||
<UserSubscription userId={userId} />
|
<UserSubscription userId={userId} />
|
||||||
</div>
|
</div>
|
||||||
</SheetContent>
|
</SheetContent>
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { Link } from "@tanstack/react-router";
|
import { Link } from "@tanstack/react-router";
|
||||||
|
import { Badge } from "@workspace/ui/components/badge";
|
||||||
import { Button } from "@workspace/ui/components/button";
|
import { Button } from "@workspace/ui/components/button";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
@ -16,7 +17,6 @@ import {
|
|||||||
deleteUserSubscribe,
|
deleteUserSubscribe,
|
||||||
getUserSubscribe,
|
getUserSubscribe,
|
||||||
resetUserSubscribeToken,
|
resetUserSubscribeToken,
|
||||||
resetUserSubscribeTraffic,
|
|
||||||
toggleUserSubscribeStatus,
|
toggleUserSubscribeStatus,
|
||||||
updateUserSubscribe,
|
updateUserSubscribe,
|
||||||
} from "@workspace/ui/services/admin/user";
|
} from "@workspace/ui/services/admin/user";
|
||||||
@ -33,7 +33,6 @@ export default function UserSubscription({ userId }: { userId: number }) {
|
|||||||
const { t } = useTranslation("user");
|
const { t } = useTranslation("user");
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const ref = useRef<ProTableActions>(null);
|
const ref = useRef<ProTableActions>(null);
|
||||||
const { getUserSubscribe: getUserSubscribeUrls } = useGlobalStore();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ProTable<API.UserSubscribe, Record<string, unknown>>
|
<ProTable<API.UserSubscribe, Record<string, unknown>>
|
||||||
@ -59,100 +58,14 @@ export default function UserSubscription({ userId }: { userId: number }) {
|
|||||||
title={t("editSubscription", "Edit Subscription")}
|
title={t("editSubscription", "Edit Subscription")}
|
||||||
trigger={t("edit", "Edit")}
|
trigger={t("edit", "Edit")}
|
||||||
/>,
|
/>,
|
||||||
<Button
|
<RowMoreActions
|
||||||
key="copy"
|
key="more"
|
||||||
onClick={async () => {
|
refresh={() => ref.current?.refresh()}
|
||||||
await navigator.clipboard.writeText(
|
row={row}
|
||||||
getUserSubscribeUrls(row.token)[0] || ""
|
token={row.token}
|
||||||
);
|
userId={userId}
|
||||||
toast.success(t("copySuccess", "Copied successfully"));
|
/>
|
||||||
}}
|
|
||||||
variant="secondary"
|
|
||||||
>
|
|
||||||
{t("copySubscription", "Copy Subscription")}
|
|
||||||
</Button>,
|
|
||||||
|
|
||||||
<ConfirmButton
|
|
||||||
cancelText={t("cancel", "Cancel")}
|
|
||||||
confirmText={t("confirm", "Confirm")}
|
|
||||||
description={t(
|
|
||||||
"resetSubscriptionTokenDescription",
|
|
||||||
"This will reset the subscription token. Old links will become invalid."
|
|
||||||
)}
|
|
||||||
key="reset-token"
|
|
||||||
onConfirm={async () => {
|
|
||||||
await resetUserSubscribeToken({ user_subscribe_id: row.id });
|
|
||||||
toast.success(t("resetSuccess", "Reset successfully"));
|
|
||||||
ref.current?.refresh();
|
|
||||||
}}
|
|
||||||
title={t("resetSubscriptionToken", "Reset Token")}
|
|
||||||
trigger={
|
|
||||||
<Button variant="outline">
|
|
||||||
{t("resetToken", "Reset Token")}
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
/>,
|
/>,
|
||||||
|
|
||||||
<ConfirmButton
|
|
||||||
cancelText={t("cancel", "Cancel")}
|
|
||||||
confirmText={t("confirm", "Confirm")}
|
|
||||||
description={t(
|
|
||||||
"resetSubscriptionTrafficDescription",
|
|
||||||
"This will reset the subscription traffic counters."
|
|
||||||
)}
|
|
||||||
key="reset-traffic"
|
|
||||||
onConfirm={async () => {
|
|
||||||
await resetUserSubscribeTraffic({ user_subscribe_id: row.id });
|
|
||||||
toast.success(t("resetSuccess", "Reset successfully"));
|
|
||||||
ref.current?.refresh();
|
|
||||||
}}
|
|
||||||
title={t("resetSubscriptionTraffic", "Reset Traffic")}
|
|
||||||
trigger={
|
|
||||||
<Button variant="outline">
|
|
||||||
{t("resetTraffic", "Reset Traffic")}
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
/>,
|
|
||||||
|
|
||||||
<ConfirmButton
|
|
||||||
cancelText={t("cancel", "Cancel")}
|
|
||||||
confirmText={t("confirm", "Confirm")}
|
|
||||||
description={t(
|
|
||||||
"toggleSubscriptionStatusDescription",
|
|
||||||
"This will toggle the subscription status."
|
|
||||||
)}
|
|
||||||
key="toggle-status"
|
|
||||||
onConfirm={async () => {
|
|
||||||
await toggleUserSubscribeStatus({ user_subscribe_id: row.id });
|
|
||||||
toast.success(t("updateSuccess", "Updated successfully"));
|
|
||||||
ref.current?.refresh();
|
|
||||||
}}
|
|
||||||
title={t("toggleSubscriptionStatus", "Toggle Status")}
|
|
||||||
trigger={
|
|
||||||
<Button variant="outline">
|
|
||||||
{t("toggleStatus", "Toggle Status")}
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
/>,
|
|
||||||
<ConfirmButton
|
|
||||||
cancelText={t("cancel", "Cancel")}
|
|
||||||
confirmText={t("confirm", "Confirm")}
|
|
||||||
description={t(
|
|
||||||
"deleteSubscriptionDescription",
|
|
||||||
"This action cannot be undone."
|
|
||||||
)}
|
|
||||||
key="delete"
|
|
||||||
onConfirm={async () => {
|
|
||||||
await deleteUserSubscribe({ user_subscribe_id: row.id });
|
|
||||||
toast.success(t("deleteSuccess", "Deleted successfully"));
|
|
||||||
ref.current?.refresh();
|
|
||||||
}}
|
|
||||||
title={t("confirmDelete", "Confirm Delete")}
|
|
||||||
trigger={
|
|
||||||
<Button variant="destructive">{t("delete", "Delete")}</Button>
|
|
||||||
}
|
|
||||||
/>,
|
|
||||||
<RowMoreActions key="more" subId={row.id} userId={userId} />,
|
|
||||||
],
|
],
|
||||||
}}
|
}}
|
||||||
columns={[
|
columns={[
|
||||||
@ -165,6 +78,51 @@ export default function UserSubscription({ userId }: { userId: number }) {
|
|||||||
header: t("subscriptionName", "Subscription Name"),
|
header: t("subscriptionName", "Subscription Name"),
|
||||||
cell: ({ row }) => row.original.subscribe.name,
|
cell: ({ row }) => row.original.subscribe.name,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "status",
|
||||||
|
header: t("status", "Status"),
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const status = row.getValue("status") as number;
|
||||||
|
const expireTime = row.original.expire_time;
|
||||||
|
|
||||||
|
// 如果过期时间为0,说明是永久订阅,应该显示为激活状态
|
||||||
|
const displayStatus = status === 3 && expireTime === 0 ? 1 : status;
|
||||||
|
|
||||||
|
const statusMap: Record<
|
||||||
|
number,
|
||||||
|
{
|
||||||
|
label: string;
|
||||||
|
variant: "default" | "secondary" | "destructive" | "outline";
|
||||||
|
}
|
||||||
|
> = {
|
||||||
|
0: { label: t("statusPending", "Pending"), variant: "outline" },
|
||||||
|
1: { label: t("statusActive", "Active"), variant: "default" },
|
||||||
|
2: {
|
||||||
|
label: t("statusFinished", "Finished"),
|
||||||
|
variant: "secondary",
|
||||||
|
},
|
||||||
|
3: {
|
||||||
|
label: t("statusExpired", "Expired"),
|
||||||
|
variant: "destructive",
|
||||||
|
},
|
||||||
|
4: {
|
||||||
|
label: t("statusDeducted", "Deducted"),
|
||||||
|
variant: "secondary",
|
||||||
|
},
|
||||||
|
5: {
|
||||||
|
label: t("statusStopped", "Stopped"),
|
||||||
|
variant: "destructive",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const statusInfo = statusMap[displayStatus] || {
|
||||||
|
label: "Unknown",
|
||||||
|
variant: "outline",
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Badge variant={statusInfo.variant}>{statusInfo.label}</Badge>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "upload",
|
accessorKey: "upload",
|
||||||
header: t("upload", "Upload"),
|
header: t("upload", "Upload"),
|
||||||
@ -216,10 +174,12 @@ export default function UserSubscription({ userId }: { userId: number }) {
|
|||||||
{
|
{
|
||||||
accessorKey: "expire_time",
|
accessorKey: "expire_time",
|
||||||
header: t("expireTime", "Expire Time"),
|
header: t("expireTime", "Expire Time"),
|
||||||
cell: ({ row }) =>
|
cell: ({ row }) => {
|
||||||
row.getValue("expire_time")
|
const expireTime = row.getValue("expire_time") as number;
|
||||||
? formatDate(row.getValue("expire_time"))
|
return expireTime && expireTime !== 0
|
||||||
: t("permanent", "Permanent"),
|
? formatDate(expireTime)
|
||||||
|
: t("permanent", "Permanent");
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "created_at",
|
accessorKey: "created_at",
|
||||||
@ -263,19 +223,72 @@ export default function UserSubscription({ userId }: { userId: number }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function RowMoreActions({ userId, subId }: { userId: number; subId: number }) {
|
function RowMoreActions({
|
||||||
|
userId,
|
||||||
|
row,
|
||||||
|
token,
|
||||||
|
refresh,
|
||||||
|
}: {
|
||||||
|
userId: number;
|
||||||
|
row: API.UserSubscribe;
|
||||||
|
token: string;
|
||||||
|
refresh: () => void;
|
||||||
|
}) {
|
||||||
const triggerRef = useRef<HTMLButtonElement>(null);
|
const triggerRef = useRef<HTMLButtonElement>(null);
|
||||||
|
const resetTokenRef = useRef<HTMLButtonElement>(null);
|
||||||
|
const toggleStatusRef = useRef<HTMLButtonElement>(null);
|
||||||
|
const deleteRef = useRef<HTMLButtonElement>(null);
|
||||||
const { t } = useTranslation("user");
|
const { t } = useTranslation("user");
|
||||||
|
const { getUserSubscribe: getUserSubscribeUrls } = useGlobalStore();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="inline-flex">
|
<div className="inline-flex">
|
||||||
<DropdownMenu>
|
<DropdownMenu modal={false}>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button variant="outline">{t("more", "More")}</Button>
|
<Button variant="outline">{t("more", "More")}</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end">
|
<DropdownMenuContent align="end">
|
||||||
|
<DropdownMenuItem
|
||||||
|
onSelect={async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
await navigator.clipboard.writeText(
|
||||||
|
getUserSubscribeUrls(row.short, token)[0] || ""
|
||||||
|
);
|
||||||
|
toast.success(t("copySuccess", "Copied successfully"));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("copySubscription", "Copy Subscription")}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onSelect={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
resetTokenRef.current?.click();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("resetToken", "Reset Subscription Address")}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onSelect={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
toggleStatusRef.current?.click();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{row.status === 5
|
||||||
|
? t("resumeSubscribe", "Resume Subscription")
|
||||||
|
: t("stopSubscribe", "Stop Subscription")}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="text-destructive"
|
||||||
|
onSelect={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
deleteRef.current?.click();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("delete", "Delete")}
|
||||||
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem asChild>
|
<DropdownMenuItem asChild>
|
||||||
<Link
|
<Link
|
||||||
search={{ user_id: userId, user_subscribe_id: subId }}
|
search={{ user_id: userId, user_subscribe_id: row.id }}
|
||||||
to="/dashboard/log/subscribe"
|
to="/dashboard/log/subscribe"
|
||||||
>
|
>
|
||||||
{t("subscriptionLogs", "Subscription Logs")}
|
{t("subscriptionLogs", "Subscription Logs")}
|
||||||
@ -283,7 +296,7 @@ function RowMoreActions({ userId, subId }: { userId: number; subId: number }) {
|
|||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem asChild>
|
<DropdownMenuItem asChild>
|
||||||
<Link
|
<Link
|
||||||
search={{ user_id: userId, user_subscribe_id: subId }}
|
search={{ user_id: userId, user_subscribe_id: row.id }}
|
||||||
to="/dashboard/log/reset-subscribe"
|
to="/dashboard/log/reset-subscribe"
|
||||||
>
|
>
|
||||||
{t("resetLogs", "Reset Logs")}
|
{t("resetLogs", "Reset Logs")}
|
||||||
@ -291,7 +304,7 @@ function RowMoreActions({ userId, subId }: { userId: number; subId: number }) {
|
|||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem asChild>
|
<DropdownMenuItem asChild>
|
||||||
<Link
|
<Link
|
||||||
search={{ user_id: userId, user_subscribe_id: subId }}
|
search={{ user_id: userId, user_subscribe_id: row.id }}
|
||||||
to="/dashboard/log/subscribe-traffic"
|
to="/dashboard/log/subscribe-traffic"
|
||||||
>
|
>
|
||||||
{t("trafficStats", "Traffic Stats")}
|
{t("trafficStats", "Traffic Stats")}
|
||||||
@ -299,7 +312,7 @@ function RowMoreActions({ userId, subId }: { userId: number; subId: number }) {
|
|||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem asChild>
|
<DropdownMenuItem asChild>
|
||||||
<Link
|
<Link
|
||||||
search={{ user_id: userId, subscribe_id: subId }}
|
search={{ user_id: userId, subscribe_id: row.id }}
|
||||||
to="/dashboard/log/traffic-details"
|
to="/dashboard/log/traffic-details"
|
||||||
>
|
>
|
||||||
{t("trafficDetails", "Traffic Details")}
|
{t("trafficDetails", "Traffic Details")}
|
||||||
@ -316,8 +329,78 @@ function RowMoreActions({ userId, subId }: { userId: number; subId: number }) {
|
|||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
|
||||||
|
{/* Hidden triggers for confirm dialogs */}
|
||||||
|
<ConfirmButton
|
||||||
|
cancelText={t("cancel", "Cancel")}
|
||||||
|
confirmText={t("confirm", "Confirm")}
|
||||||
|
description={t(
|
||||||
|
"resetTokenDescription",
|
||||||
|
"This will reset the subscription address and regenerate a new token."
|
||||||
|
)}
|
||||||
|
onConfirm={async () => {
|
||||||
|
await resetUserSubscribeToken({
|
||||||
|
user_subscribe_id: row.id,
|
||||||
|
});
|
||||||
|
toast.success(
|
||||||
|
t("resetTokenSuccess", "Subscription address reset successfully")
|
||||||
|
);
|
||||||
|
refresh();
|
||||||
|
}}
|
||||||
|
title={t("confirmResetToken", "Confirm Reset Subscription Address")}
|
||||||
|
trigger={<Button className="hidden" ref={resetTokenRef} />}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ConfirmButton
|
||||||
|
cancelText={t("cancel", "Cancel")}
|
||||||
|
confirmText={t("confirm", "Confirm")}
|
||||||
|
description={
|
||||||
|
row.status === 5
|
||||||
|
? t(
|
||||||
|
"resumeSubscribeDescription",
|
||||||
|
"This will resume the subscription and allow the user to use it."
|
||||||
|
)
|
||||||
|
: t(
|
||||||
|
"stopSubscribeDescription",
|
||||||
|
"This will stop the subscription temporarily. User will not be able to use it."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
onConfirm={async () => {
|
||||||
|
await toggleUserSubscribeStatus({
|
||||||
|
user_subscribe_id: row.id,
|
||||||
|
});
|
||||||
|
toast.success(
|
||||||
|
row.status === 5
|
||||||
|
? t("resumeSubscribeSuccess", "Subscription resumed successfully")
|
||||||
|
: t("stopSubscribeSuccess", "Subscription stopped successfully")
|
||||||
|
);
|
||||||
|
refresh();
|
||||||
|
}}
|
||||||
|
title={
|
||||||
|
row.status === 5
|
||||||
|
? t("confirmResumeSubscribe", "Confirm Resume Subscription")
|
||||||
|
: t("confirmStopSubscribe", "Confirm Stop Subscription")
|
||||||
|
}
|
||||||
|
trigger={<Button className="hidden" ref={toggleStatusRef} />}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ConfirmButton
|
||||||
|
cancelText={t("cancel", "Cancel")}
|
||||||
|
confirmText={t("confirm", "Confirm")}
|
||||||
|
description={t(
|
||||||
|
"deleteSubscriptionDescription",
|
||||||
|
"This action cannot be undone."
|
||||||
|
)}
|
||||||
|
onConfirm={async () => {
|
||||||
|
await deleteUserSubscribe({ user_subscribe_id: row.id });
|
||||||
|
toast.success(t("deleteSuccess", "Deleted successfully"));
|
||||||
|
refresh();
|
||||||
|
}}
|
||||||
|
title={t("confirmDelete", "Confirm Delete")}
|
||||||
|
trigger={<Button className="hidden" ref={deleteRef} />}
|
||||||
|
/>
|
||||||
|
|
||||||
<SubscriptionDetail
|
<SubscriptionDetail
|
||||||
subscriptionId={subId}
|
subscriptionId={row.id}
|
||||||
trigger={<Button className="hidden" ref={triggerRef} />}
|
trigger={<Button className="hidden" ref={triggerRef} />}
|
||||||
userId={userId}
|
userId={userId}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -95,145 +95,149 @@ export function SubscriptionForm({
|
|||||||
<SheetHeader>
|
<SheetHeader>
|
||||||
<SheetTitle>{title}</SheetTitle>
|
<SheetTitle>{title}</SheetTitle>
|
||||||
</SheetHeader>
|
</SheetHeader>
|
||||||
<ScrollArea className="h-[calc(100dvh-48px-36px-36px-env(safe-area-inset-top))]">
|
<ScrollArea className="h-[calc(100dvh-48px-36px-36px-env(safe-area-inset-top))] px-4">
|
||||||
<Form {...form}>
|
<div className="pr-4">
|
||||||
<form
|
<Form {...form}>
|
||||||
className="mt-4 space-y-4"
|
<form
|
||||||
onSubmit={form.handleSubmit(handleSubmit)}
|
className="mt-4 space-y-4"
|
||||||
>
|
onSubmit={form.handleSubmit(handleSubmit)}
|
||||||
<FormField
|
>
|
||||||
control={form.control}
|
<FormField
|
||||||
name="subscribe_id"
|
control={form.control}
|
||||||
render={({ field }) => (
|
name="subscribe_id"
|
||||||
<FormItem>
|
render={({ field }) => (
|
||||||
<FormLabel>{t("subscription", "Subscription")}</FormLabel>
|
<FormItem>
|
||||||
<FormControl>
|
<FormLabel>{t("subscription", "Subscription")}</FormLabel>
|
||||||
<Combobox<number, false>
|
<FormControl>
|
||||||
onChange={(value) => {
|
<Combobox<number, false>
|
||||||
form.setValue(field.name, value);
|
onChange={(value) => {
|
||||||
}}
|
form.setValue(field.name, value);
|
||||||
options={subscribes?.map((item) => ({
|
}}
|
||||||
value: item.id!,
|
options={subscribes?.map((item) => ({
|
||||||
label: item.name!,
|
value: item.id!,
|
||||||
}))}
|
label: item.name!,
|
||||||
placeholder="Select Subscription"
|
}))}
|
||||||
value={field.value}
|
placeholder="Select Subscription"
|
||||||
/>
|
value={field.value}
|
||||||
</FormControl>
|
/>
|
||||||
<FormMessage />
|
</FormControl>
|
||||||
</FormItem>
|
<FormMessage />
|
||||||
)}
|
</FormItem>
|
||||||
/>
|
)}
|
||||||
<FormField
|
/>
|
||||||
control={form.control}
|
<FormField
|
||||||
name="traffic"
|
control={form.control}
|
||||||
render={({ field }) => (
|
name="traffic"
|
||||||
<FormItem>
|
render={({ field }) => (
|
||||||
<FormLabel>{t("trafficLimit", "Traffic Limit")}</FormLabel>
|
<FormItem>
|
||||||
<FormControl>
|
<FormLabel>
|
||||||
<EnhancedInput
|
{t("trafficLimit", "Traffic Limit")}
|
||||||
placeholder={t("unlimited", "Unlimited")}
|
</FormLabel>
|
||||||
type="number"
|
<FormControl>
|
||||||
{...field}
|
<EnhancedInput
|
||||||
formatInput={(value) =>
|
placeholder={t("unlimited", "Unlimited")}
|
||||||
unitConversion("bytesToGb", value)
|
type="number"
|
||||||
}
|
{...field}
|
||||||
formatOutput={(value) =>
|
formatInput={(value) =>
|
||||||
unitConversion("gbToBytes", value)
|
unitConversion("bytesToGb", value)
|
||||||
}
|
|
||||||
onValueChange={(value) => {
|
|
||||||
form.setValue(field.name, value as number);
|
|
||||||
}}
|
|
||||||
suffix="GB"
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="upload"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>
|
|
||||||
{t("uploadTraffic", "Upload Traffic")}
|
|
||||||
</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<EnhancedInput
|
|
||||||
placeholder="0"
|
|
||||||
type="number"
|
|
||||||
{...field}
|
|
||||||
formatInput={(value) =>
|
|
||||||
unitConversion("bytesToGb", value)
|
|
||||||
}
|
|
||||||
formatOutput={(value) =>
|
|
||||||
unitConversion("gbToBytes", value)
|
|
||||||
}
|
|
||||||
onValueChange={(value) => {
|
|
||||||
form.setValue(field.name, value as number);
|
|
||||||
}}
|
|
||||||
suffix="GB"
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="download"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>
|
|
||||||
{t("downloadTraffic", "Download Traffic")}
|
|
||||||
</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<EnhancedInput
|
|
||||||
placeholder="0"
|
|
||||||
type="number"
|
|
||||||
{...field}
|
|
||||||
formatInput={(value) =>
|
|
||||||
unitConversion("bytesToGb", value)
|
|
||||||
}
|
|
||||||
formatOutput={(value) =>
|
|
||||||
unitConversion("gbToBytes", value)
|
|
||||||
}
|
|
||||||
onValueChange={(value) => {
|
|
||||||
form.setValue(field.name, value as number);
|
|
||||||
}}
|
|
||||||
suffix="GB"
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="expired_at"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>{t("expiredAt", "Expired At")}</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<DatePicker
|
|
||||||
onChange={(value: number | null | undefined) => {
|
|
||||||
if (value === field.value) {
|
|
||||||
form.setValue(field.name, 0);
|
|
||||||
} else {
|
|
||||||
form.setValue(field.name, value!);
|
|
||||||
}
|
}
|
||||||
}}
|
formatOutput={(value) =>
|
||||||
placeholder={t("permanent", "Permanent")}
|
unitConversion("gbToBytes", value)
|
||||||
value={field.value ?? undefined}
|
}
|
||||||
/>
|
onValueChange={(value) => {
|
||||||
</FormControl>
|
form.setValue(field.name, value as number);
|
||||||
<FormMessage />
|
}}
|
||||||
</FormItem>
|
suffix="GB"
|
||||||
)}
|
/>
|
||||||
/>
|
</FormControl>
|
||||||
</form>
|
<FormMessage />
|
||||||
</Form>
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="upload"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
{t("uploadTraffic", "Upload Traffic")}
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<EnhancedInput
|
||||||
|
placeholder="0"
|
||||||
|
type="number"
|
||||||
|
{...field}
|
||||||
|
formatInput={(value) =>
|
||||||
|
unitConversion("bytesToGb", value)
|
||||||
|
}
|
||||||
|
formatOutput={(value) =>
|
||||||
|
unitConversion("gbToBytes", value)
|
||||||
|
}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
form.setValue(field.name, value as number);
|
||||||
|
}}
|
||||||
|
suffix="GB"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="download"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
{t("downloadTraffic", "Download Traffic")}
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<EnhancedInput
|
||||||
|
placeholder="0"
|
||||||
|
type="number"
|
||||||
|
{...field}
|
||||||
|
formatInput={(value) =>
|
||||||
|
unitConversion("bytesToGb", value)
|
||||||
|
}
|
||||||
|
formatOutput={(value) =>
|
||||||
|
unitConversion("gbToBytes", value)
|
||||||
|
}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
form.setValue(field.name, value as number);
|
||||||
|
}}
|
||||||
|
suffix="GB"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="expired_at"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>{t("expiredAt", "Expired At")}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<DatePicker
|
||||||
|
onChange={(value: number | null | undefined) => {
|
||||||
|
form.setValue(field.name, value || 0);
|
||||||
|
}}
|
||||||
|
placeholder={t("permanent", "Permanent")}
|
||||||
|
value={
|
||||||
|
field.value && field.value > 0
|
||||||
|
? field.value
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
<SheetFooter className="flex-row justify-end gap-2 pt-3">
|
<SheetFooter className="flex-row justify-end gap-2 pt-3">
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@ -8,7 +8,7 @@ export interface GlobalStore {
|
|||||||
setCommon: (common: Partial<API.GetGlobalConfigResponse>) => void;
|
setCommon: (common: Partial<API.GetGlobalConfigResponse>) => void;
|
||||||
setUser: (user?: API.User) => void;
|
setUser: (user?: API.User) => void;
|
||||||
getUserInfo: () => Promise<void>;
|
getUserInfo: () => Promise<void>;
|
||||||
getUserSubscribe: (uuid: string, type?: string) => string[];
|
getUserSubscribe: (short: string, token: string, type?: string) => string[];
|
||||||
getAppSubLink: (url: string, schema?: string) => string;
|
getAppSubLink: (url: string, schema?: string) => string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -120,7 +120,7 @@ export const useGlobalStore = create<GlobalStore>((set, get) => ({
|
|||||||
console.error("Failed to refresh user:", error);
|
console.error("Failed to refresh user:", error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
getUserSubscribe: (uuid: string, type?: string) => {
|
getUserSubscribe: (short: string, token: string, type?: string) => {
|
||||||
const { pan_domain, subscribe_domain, subscribe_path } =
|
const { pan_domain, subscribe_domain, subscribe_path } =
|
||||||
get().common.subscribe || {};
|
get().common.subscribe || {};
|
||||||
const domains = subscribe_domain
|
const domains = subscribe_domain
|
||||||
@ -129,12 +129,13 @@ export const useGlobalStore = create<GlobalStore>((set, get) => ({
|
|||||||
|
|
||||||
return domains.map((domain) => {
|
return domains.map((domain) => {
|
||||||
if (pan_domain) {
|
if (pan_domain) {
|
||||||
if (type) return `https://${uuid}.${type}.${domain}`;
|
if (type)
|
||||||
return `https://${uuid}.${domain}`;
|
return `https://${short}.${type}.${domain}${subscribe_path}?token=${token}&type=${type}`;
|
||||||
|
return `https://${short}.${domain}${subscribe_path}?token=${token}`;
|
||||||
}
|
}
|
||||||
if (type)
|
if (type)
|
||||||
return `https://${domain}${subscribe_path}?token=${uuid}&type=${type}`;
|
return `https://${domain}${subscribe_path}?token=${token}&type=${type}`;
|
||||||
return `https://${domain}${subscribe_path}?token=${uuid}`;
|
return `https://${domain}${subscribe_path}?token=${token}`;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
getAppSubLink: (url: string, schema?: string) => {
|
getAppSubLink: (url: string, schema?: string) => {
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import tailwindcss from "@tailwindcss/vite";
|
|||||||
import { devtools } from "@tanstack/devtools-vite";
|
import { devtools } from "@tanstack/devtools-vite";
|
||||||
import { tanstackRouter } from "@tanstack/router-plugin/vite";
|
import { tanstackRouter } from "@tanstack/router-plugin/vite";
|
||||||
import viteReact from "@vitejs/plugin-react";
|
import viteReact from "@vitejs/plugin-react";
|
||||||
import { defineConfig, type Plugin } from "vite";
|
import { defineConfig, loadEnv, type Plugin } from "vite";
|
||||||
|
|
||||||
// Plugin to generate version.lock file after build
|
// Plugin to generate version.lock file after build
|
||||||
function versionLockPlugin(): Plugin {
|
function versionLockPlugin(): Plugin {
|
||||||
@ -24,34 +24,37 @@ function versionLockPlugin(): Plugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig(({ mode }) => {
|
||||||
base: "./",
|
const env = loadEnv(mode, process.cwd(), "");
|
||||||
plugins: [
|
|
||||||
devtools({ eventBusConfig: { port: 42_070 } }),
|
return {
|
||||||
tanstackRouter({
|
base: "./",
|
||||||
target: "react",
|
plugins: [
|
||||||
autoCodeSplitting: true,
|
devtools({ eventBusConfig: { port: 42_070 } }),
|
||||||
}),
|
tanstackRouter({
|
||||||
viteReact(),
|
target: "react",
|
||||||
tailwindcss(),
|
autoCodeSplitting: true,
|
||||||
versionLockPlugin(),
|
}),
|
||||||
],
|
viteReact(),
|
||||||
resolve: {
|
tailwindcss(),
|
||||||
alias: {
|
versionLockPlugin(),
|
||||||
"@": fileURLToPath(new URL("./src", import.meta.url)),
|
],
|
||||||
},
|
resolve: {
|
||||||
},
|
alias: {
|
||||||
server: {
|
"@": fileURLToPath(new URL("./src", import.meta.url)),
|
||||||
proxy: {
|
|
||||||
"/api": {
|
|
||||||
target: "https://api.ppanel.dev",
|
|
||||||
changeOrigin: true,
|
|
||||||
secure: false,
|
|
||||||
rewrite: (path) => path.replace(/^\/api/, ""),
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
server: {
|
||||||
build: {
|
proxy: {
|
||||||
assetsDir: "static",
|
"/api": {
|
||||||
},
|
target: env.VITE_API_BASE_URL || "https://api.ppanel.dev",
|
||||||
|
changeOrigin: true,
|
||||||
|
secure: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
assetsDir: "static",
|
||||||
|
},
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@ -44,6 +44,8 @@
|
|||||||
"60003": "An existing subscription is detected. Please cancel it before proceeding.",
|
"60003": "An existing subscription is detected. Please cancel it before proceeding.",
|
||||||
"60004": "Unable to delete at the moment as the subscription has active users.",
|
"60004": "Unable to delete at the moment as the subscription has active users.",
|
||||||
"60005": "Single subscription mode has exceeded user limit",
|
"60005": "Single subscription mode has exceeded user limit",
|
||||||
|
"60006": "User quota limit has been reached, unable to continue.",
|
||||||
|
"60007": "Insufficient inventory, please try again later or contact the administrator.",
|
||||||
"70001": "Incorrect verification code, please re-enter.",
|
"70001": "Incorrect verification code, please re-enter.",
|
||||||
"80001": "Task was not successfully queued, please try again later.",
|
"80001": "Task was not successfully queued, please try again later.",
|
||||||
"90001": "Please disable DEBUG mode and try again.",
|
"90001": "Please disable DEBUG mode and try again.",
|
||||||
|
|||||||
@ -6,6 +6,7 @@
|
|||||||
"duration": "Duration",
|
"duration": "Duration",
|
||||||
"fee": "Fee",
|
"fee": "Fee",
|
||||||
"gift": "gift Deduction",
|
"gift": "gift Deduction",
|
||||||
|
"originalPrice": "Original Price (Monthly)",
|
||||||
"price": "Price",
|
"price": "Price",
|
||||||
"productDiscount": "Product Discount",
|
"productDiscount": "Product Discount",
|
||||||
"total": "Total"
|
"total": "Total"
|
||||||
|
|||||||
@ -44,6 +44,8 @@
|
|||||||
"60003": "检测到现有订阅,请先取消后再继续。",
|
"60003": "检测到现有订阅,请先取消后再继续。",
|
||||||
"60004": "由于订阅有活跃用户,暂时无法删除。",
|
"60004": "由于订阅有活跃用户,暂时无法删除。",
|
||||||
"60005": "单一订阅模式已超过用户限制",
|
"60005": "单一订阅模式已超过用户限制",
|
||||||
|
"60006": "用户配额已达到限制,无法继续操作。",
|
||||||
|
"60007": "库存不足,请稍后再试或联系管理员。",
|
||||||
"70001": "验证码不正确,请重新输入。",
|
"70001": "验证码不正确,请重新输入。",
|
||||||
"80001": "任务未成功加入队列,请稍后再试。",
|
"80001": "任务未成功加入队列,请稍后再试。",
|
||||||
"90001": "请禁用 DEBUG 模式后再试。",
|
"90001": "请禁用 DEBUG 模式后再试。",
|
||||||
|
|||||||
@ -6,6 +6,7 @@
|
|||||||
"duration": "套餐时长",
|
"duration": "套餐时长",
|
||||||
"fee": "手续费",
|
"fee": "手续费",
|
||||||
"gift": "赠金抵扣",
|
"gift": "赠金抵扣",
|
||||||
|
"originalPrice": "原价(按月)",
|
||||||
"price": "价格",
|
"price": "价格",
|
||||||
"productDiscount": "商品折扣",
|
"productDiscount": "商品折扣",
|
||||||
"total": "总价"
|
"total": "总价"
|
||||||
|
|||||||
@ -33,7 +33,7 @@ export function UserNav() {
|
|||||||
|
|
||||||
if (user) {
|
if (user) {
|
||||||
return (
|
return (
|
||||||
<DropdownMenu>
|
<DropdownMenu modal={false}>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<div className="flex cursor-pointer items-center gap-2 rounded-full border bg-background px-2 py-1.5 transition-colors duration-200 hover:bg-accent">
|
<div className="flex cursor-pointer items-center gap-2 rounded-full border bg-background px-2 py-1.5 transition-colors duration-200 hover:bg-accent">
|
||||||
<Avatar className="h-6 w-6">
|
<Avatar className="h-6 w-6">
|
||||||
|
|||||||
@ -173,7 +173,7 @@ export default function RegisterForm({
|
|||||||
/>
|
/>
|
||||||
<SendCode
|
<SendCode
|
||||||
params={{
|
params={{
|
||||||
...form.getValues(),
|
email: form.watch("email"),
|
||||||
type: 1,
|
type: 1,
|
||||||
}}
|
}}
|
||||||
type="email"
|
type="email"
|
||||||
|
|||||||
@ -98,7 +98,7 @@ export default function ResetForm({
|
|||||||
/>
|
/>
|
||||||
<SendCode
|
<SendCode
|
||||||
params={{
|
params={{
|
||||||
...form.getValues(),
|
email: form.watch("email"),
|
||||||
type: 2,
|
type: 2,
|
||||||
}}
|
}}
|
||||||
type="email"
|
type="email"
|
||||||
|
|||||||
@ -127,7 +127,10 @@ export default function LoginForm({
|
|||||||
{mode === "code" && (
|
{mode === "code" && (
|
||||||
<SendCode
|
<SendCode
|
||||||
params={{
|
params={{
|
||||||
...form.getValues(),
|
telephone: form.watch("telephone"),
|
||||||
|
telephone_area_code: form.watch(
|
||||||
|
"telephone_area_code"
|
||||||
|
),
|
||||||
type: 2,
|
type: 2,
|
||||||
}}
|
}}
|
||||||
type="phone"
|
type="phone"
|
||||||
|
|||||||
@ -182,7 +182,10 @@ export default function RegisterForm({
|
|||||||
|
|
||||||
<SendCode
|
<SendCode
|
||||||
params={{
|
params={{
|
||||||
...form.getValues(),
|
telephone: form.watch("telephone"),
|
||||||
|
telephone_area_code: form.watch(
|
||||||
|
"telephone_area_code"
|
||||||
|
),
|
||||||
type: 1,
|
type: 1,
|
||||||
}}
|
}}
|
||||||
type="phone"
|
type="phone"
|
||||||
|
|||||||
@ -124,7 +124,8 @@ export default function ResetForm({
|
|||||||
/>
|
/>
|
||||||
<SendCode
|
<SendCode
|
||||||
params={{
|
params={{
|
||||||
...form.getValues(),
|
telephone: form.watch("telephone"),
|
||||||
|
telephone_area_code: form.watch("telephone_area_code"),
|
||||||
type: 2,
|
type: 2,
|
||||||
}}
|
}}
|
||||||
type="phone"
|
type="phone"
|
||||||
|
|||||||
@ -142,19 +142,44 @@ export function Content({ subscriptionData }: ProductShowcaseProps) {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
<Separator />
|
<Separator />
|
||||||
<CardFooter className="relative flex flex-col gap-4 p-4">
|
<CardFooter className="relative flex flex-col gap-4 p-4">
|
||||||
<motion.h2
|
{(() => {
|
||||||
animate={{ opacity: 1 }}
|
const hasDiscount = item.discount && item.discount.length > 0;
|
||||||
className="pb-4 font-semibold text-2xl sm:text-3xl"
|
const shouldShowOriginal = item.show_original_price !== false;
|
||||||
initial={{ opacity: 0 }}
|
|
||||||
transition={{ duration: 0.5, delay: 0.2 }}
|
const displayPrice =
|
||||||
>
|
shouldShowOriginal || !hasDiscount
|
||||||
<Display type="currency" value={item.unit_price} />
|
? item.unit_price
|
||||||
<span className="font-medium text-base">
|
: Math.round(
|
||||||
/
|
item.unit_price *
|
||||||
{unitTimeMap[item.unit_time!] ||
|
(item.discount?.[0]?.quantity ?? 1) *
|
||||||
t(item.unit_time || "Month", item.unit_time || "Month")}
|
((item.discount?.[0]?.discount ?? 100) / 100)
|
||||||
</span>
|
);
|
||||||
</motion.h2>
|
|
||||||
|
const displayQuantity =
|
||||||
|
shouldShowOriginal || !hasDiscount
|
||||||
|
? 1
|
||||||
|
: (item.discount?.[0]?.quantity ?? 1);
|
||||||
|
|
||||||
|
const unitTime =
|
||||||
|
unitTimeMap[item.unit_time!] ||
|
||||||
|
t(item.unit_time || "Month", item.unit_time || "Month");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.h2
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
className="pb-4 font-semibold text-2xl sm:text-3xl"
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
transition={{ duration: 0.5, delay: 0.2 }}
|
||||||
|
>
|
||||||
|
<Display type="currency" value={displayPrice} />
|
||||||
|
<span className="font-medium text-base">
|
||||||
|
{displayQuantity === 1
|
||||||
|
? `/${unitTime}`
|
||||||
|
: `/${displayQuantity} ${unitTime}`}
|
||||||
|
</span>
|
||||||
|
</motion.h2>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
<motion.div>
|
<motion.div>
|
||||||
<Button
|
<Button
|
||||||
asChild
|
asChild
|
||||||
|
|||||||
@ -282,6 +282,7 @@ export default function Content({
|
|||||||
...order,
|
...order,
|
||||||
quantity: params.quantity,
|
quantity: params.quantity,
|
||||||
unit_price: subscription?.unit_price,
|
unit_price: subscription?.unit_price,
|
||||||
|
show_original_price: subscription?.show_original_price,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@ -178,6 +178,7 @@ export default function Order() {
|
|||||||
order={{
|
order={{
|
||||||
...data,
|
...data,
|
||||||
unit_price: data?.subscribe?.unit_price,
|
unit_price: data?.subscribe?.unit_price,
|
||||||
|
show_original_price: data?.subscribe?.show_original_price,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@ -10,6 +10,7 @@ interface SubscribeBillingProps {
|
|||||||
unit_price: number;
|
unit_price: number;
|
||||||
unit_time: string;
|
unit_time: string;
|
||||||
subscribe_discount: number;
|
subscribe_discount: number;
|
||||||
|
show_original_price?: boolean;
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
}
|
}
|
||||||
@ -33,7 +34,19 @@ export function SubscribeBilling({ order }: Readonly<SubscribeBillingProps>) {
|
|||||||
{t(order?.unit_time || "Month", order?.unit_time || "Month")}
|
{t(order?.unit_time || "Month", order?.unit_time || "Month")}
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
)}
|
)}{" "}
|
||||||
|
{order?.show_original_price !== false &&
|
||||||
|
order?.type &&
|
||||||
|
[1, 2].includes(order?.type) && (
|
||||||
|
<li>
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
{t("billing.originalPrice", "Original Price (Monthly)")}
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<Display type="currency" value={order?.unit_price} />
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
)}{" "}
|
||||||
<li>
|
<li>
|
||||||
<span className="text-muted-foreground">
|
<span className="text-muted-foreground">
|
||||||
{t("billing.price", "Price")}
|
{t("billing.price", "Price")}
|
||||||
|
|||||||
@ -15,6 +15,7 @@ interface DurationSelectorProps {
|
|||||||
unitTime?: string;
|
unitTime?: string;
|
||||||
discounts?: Array<{ quantity: number; discount: number }>;
|
discounts?: Array<{ quantity: number; discount: number }>;
|
||||||
onChange: (value: number) => void;
|
onChange: (value: number) => void;
|
||||||
|
showOriginalPrice?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DurationSelector: React.FC<DurationSelectorProps> = ({
|
const DurationSelector: React.FC<DurationSelectorProps> = ({
|
||||||
@ -22,6 +23,7 @@ const DurationSelector: React.FC<DurationSelectorProps> = ({
|
|||||||
unitTime = "Month",
|
unitTime = "Month",
|
||||||
discounts = [],
|
discounts = [],
|
||||||
onChange,
|
onChange,
|
||||||
|
showOriginalPrice = true,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation("subscribe");
|
const { t } = useTranslation("subscribe");
|
||||||
const handleChange = useCallback(
|
const handleChange = useCallback(
|
||||||
@ -61,7 +63,7 @@ const DurationSelector: React.FC<DurationSelectorProps> = ({
|
|||||||
onValueChange={handleChange}
|
onValueChange={handleChange}
|
||||||
value={String(quantity)}
|
value={String(quantity)}
|
||||||
>
|
>
|
||||||
{unitTime !== "Minute" && (
|
{showOriginalPrice && unitTime !== "Minute" && (
|
||||||
<DurationOption label={`1 / ${t(unitTime)}`} value="1" />
|
<DurationOption label={`1 / ${t(unitTime)}`} value="1" />
|
||||||
)}
|
)}
|
||||||
{discounts?.map((item) => (
|
{discounts?.map((item) => (
|
||||||
@ -78,7 +80,7 @@ const DurationSelector: React.FC<DurationSelectorProps> = ({
|
|||||||
</span>
|
</span>
|
||||||
{discountPercentage > 0 ? (
|
{discountPercentage > 0 ? (
|
||||||
<Badge className="h-6 text-sm" variant="destructive">
|
<Badge className="h-6 text-sm" variant="destructive">
|
||||||
-{discountPercentage}% {t("discount", "Discount")}
|
-{discountPercentage.toFixed(2)}% {t("discount", "Discount")}
|
||||||
</Badge>
|
</Badge>
|
||||||
) : (
|
) : (
|
||||||
<span className="h-6 text-muted-foreground text-sm">--</span>
|
<span className="h-6 text-muted-foreground text-sm">--</span>
|
||||||
|
|||||||
@ -122,14 +122,39 @@ export default function Subscribe() {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
<Separator />
|
<Separator />
|
||||||
<CardFooter className="flex flex-col gap-2">
|
<CardFooter className="flex flex-col gap-2">
|
||||||
<h2 className="pb-8 font-semibold text-2xl sm:text-3xl">
|
{(() => {
|
||||||
<Display type="currency" value={item.unit_price} />
|
const hasDiscount = item.discount && item.discount.length > 0;
|
||||||
<span className="font-medium text-base">
|
const shouldShowOriginal = item.show_original_price !== false;
|
||||||
/
|
|
||||||
{unitTimeMap[item.unit_time!] ||
|
const displayPrice =
|
||||||
t(item.unit_time || "Month", item.unit_time || "Month")}
|
shouldShowOriginal || !hasDiscount
|
||||||
</span>
|
? item.unit_price
|
||||||
</h2>
|
: Math.round(
|
||||||
|
item.unit_price *
|
||||||
|
(item.discount?.[0]?.quantity ?? 1) *
|
||||||
|
((item.discount?.[0]?.discount ?? 100) / 100)
|
||||||
|
);
|
||||||
|
|
||||||
|
const displayQuantity =
|
||||||
|
shouldShowOriginal || !hasDiscount
|
||||||
|
? 1
|
||||||
|
: (item.discount?.[0]?.quantity ?? 1);
|
||||||
|
|
||||||
|
const unitTime =
|
||||||
|
unitTimeMap[item.unit_time!] ||
|
||||||
|
t(item.unit_time || "Month", item.unit_time || "Month");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<h2 className="pb-8 font-semibold text-2xl sm:text-3xl">
|
||||||
|
<Display type="currency" value={displayPrice} />
|
||||||
|
<span className="font-medium text-base">
|
||||||
|
{displayQuantity === 1
|
||||||
|
? `/${unitTime}`
|
||||||
|
: `/${displayQuantity} ${unitTime}`}
|
||||||
|
</span>
|
||||||
|
</h2>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
<Button
|
<Button
|
||||||
className="absolute bottom-0 w-full rounded-t-none rounded-b-xl"
|
className="absolute bottom-0 w-full rounded-t-none rounded-b-xl"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|||||||
@ -45,7 +45,13 @@ export default function Purchase({
|
|||||||
|
|
||||||
const { data: order } = useQuery({
|
const { data: order } = useQuery({
|
||||||
enabled: !!subscribe?.id,
|
enabled: !!subscribe?.id,
|
||||||
queryKey: ["preCreateOrder", params],
|
queryKey: [
|
||||||
|
"preCreateOrder",
|
||||||
|
subscribe?.id,
|
||||||
|
params.quantity,
|
||||||
|
params.payment,
|
||||||
|
params.coupon,
|
||||||
|
],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
try {
|
try {
|
||||||
const { data } = await preCreateOrder({
|
const { data } = await preCreateOrder({
|
||||||
@ -68,9 +74,13 @@ export default function Purchase({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (subscribe) {
|
if (subscribe) {
|
||||||
|
const defaultQuantity =
|
||||||
|
subscribe.show_original_price === false && subscribe.discount?.[0]
|
||||||
|
? subscribe.discount[0].quantity
|
||||||
|
: 1;
|
||||||
setParams((prev) => ({
|
setParams((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
quantity: 1,
|
quantity: defaultQuantity,
|
||||||
subscribe_id: subscribe?.id,
|
subscribe_id: subscribe?.id,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@ -127,6 +137,7 @@ export default function Purchase({
|
|||||||
...order,
|
...order,
|
||||||
quantity: params.quantity,
|
quantity: params.quantity,
|
||||||
unit_price: subscribe?.unit_price,
|
unit_price: subscribe?.unit_price,
|
||||||
|
show_original_price: subscribe?.show_original_price,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@ -139,6 +150,7 @@ export default function Purchase({
|
|||||||
handleChange("quantity", value);
|
handleChange("quantity", value);
|
||||||
}}
|
}}
|
||||||
quantity={params.quantity as number}
|
quantity={params.quantity as number}
|
||||||
|
showOriginalPrice={subscribe?.show_original_price}
|
||||||
unitTime={subscribe?.unit_time}
|
unitTime={subscribe?.unit_time}
|
||||||
/>
|
/>
|
||||||
<CouponInput
|
<CouponInput
|
||||||
|
|||||||
@ -44,7 +44,13 @@ export default function Renewal({ id, subscribe }: Readonly<RenewalProps>) {
|
|||||||
|
|
||||||
const { data: order } = useQuery({
|
const { data: order } = useQuery({
|
||||||
enabled: !!subscribe.id && open,
|
enabled: !!subscribe.id && open,
|
||||||
queryKey: ["preCreateOrder", params],
|
queryKey: [
|
||||||
|
"preCreateOrder",
|
||||||
|
subscribe.id,
|
||||||
|
params.quantity,
|
||||||
|
params.payment,
|
||||||
|
params.coupon,
|
||||||
|
],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
try {
|
try {
|
||||||
const { data } = await preCreateOrder({
|
const { data } = await preCreateOrder({
|
||||||
@ -66,9 +72,13 @@ export default function Renewal({ id, subscribe }: Readonly<RenewalProps>) {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (subscribe.id && id) {
|
if (subscribe.id && id) {
|
||||||
|
const defaultQuantity =
|
||||||
|
subscribe.show_original_price === false && subscribe.discount?.[0]
|
||||||
|
? subscribe.discount[0].quantity
|
||||||
|
: 1;
|
||||||
setParams((prev) => ({
|
setParams((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
quantity: 1,
|
quantity: defaultQuantity,
|
||||||
subscribe_id: subscribe.id,
|
subscribe_id: subscribe.id,
|
||||||
user_subscribe_id: id,
|
user_subscribe_id: id,
|
||||||
}));
|
}));
|
||||||
@ -126,6 +136,7 @@ export default function Renewal({ id, subscribe }: Readonly<RenewalProps>) {
|
|||||||
...order,
|
...order,
|
||||||
quantity: params.quantity,
|
quantity: params.quantity,
|
||||||
unit_price: subscribe?.unit_price,
|
unit_price: subscribe?.unit_price,
|
||||||
|
show_original_price: subscribe?.show_original_price,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@ -138,6 +149,7 @@ export default function Renewal({ id, subscribe }: Readonly<RenewalProps>) {
|
|||||||
handleChange("quantity", value);
|
handleChange("quantity", value);
|
||||||
}}
|
}}
|
||||||
quantity={params.quantity!}
|
quantity={params.quantity!}
|
||||||
|
showOriginalPrice={subscribe?.show_original_price}
|
||||||
unitTime={subscribe?.unit_time}
|
unitTime={subscribe?.unit_time}
|
||||||
/>
|
/>
|
||||||
<CouponInput
|
<CouponInput
|
||||||
|
|||||||
@ -218,337 +218,354 @@ export default function Content() {
|
|||||||
</Tabs>
|
</Tabs>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{userSubscribe.map((item) => (
|
{userSubscribe.map((item) => {
|
||||||
<Card
|
// 如果过期时间为0,说明是永久订阅,不应该显示过期状态
|
||||||
className={cn("relative", {
|
const isActuallyExpired =
|
||||||
"relative opacity-80 grayscale": item.status === 3,
|
item.status === 3 && item.expire_time !== 0;
|
||||||
"relative hidden opacity-60 blur-[0.3px] grayscale":
|
const shouldShowWatermark =
|
||||||
item.status === 4,
|
item.status === 2 || item.status === 4 || isActuallyExpired;
|
||||||
})}
|
|
||||||
key={item.id}
|
|
||||||
>
|
|
||||||
{item.status >= 2 && (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"pointer-events-none absolute top-0 left-0 z-10 h-full w-full overflow-hidden mix-blend-difference brightness-150 contrast-200 invert-[0.2]",
|
|
||||||
{
|
|
||||||
"text-destructive": item.status === 2,
|
|
||||||
"text-white": item.status === 3 || item.status === 4,
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="absolute inset-0">
|
|
||||||
{Array.from({ length: 16 }).map((_, i) => {
|
|
||||||
const row = Math.floor(i / 4);
|
|
||||||
const col = i % 4;
|
|
||||||
const top = 10 + row * 25 + (col % 2 === 0 ? 5 : -5);
|
|
||||||
const left = 5 + col * 30 + (row % 2 === 0 ? 0 : 10);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span
|
<Card
|
||||||
className="absolute rotate-[-30deg] whitespace-nowrap font-black text-lg opacity-40 shadow-[0px_0px_1px_rgba(255,255,255,0.5)]"
|
className={cn("relative", {
|
||||||
key={i}
|
"relative opacity-80 grayscale": isActuallyExpired,
|
||||||
style={{
|
"relative hidden opacity-60 blur-[0.3px] grayscale":
|
||||||
top: `${top}%`,
|
item.status === 4,
|
||||||
left: `${left}%`,
|
})}
|
||||||
}}
|
key={item.id}
|
||||||
>
|
>
|
||||||
{
|
{shouldShowWatermark && (
|
||||||
statusWatermarks[
|
<div
|
||||||
item.status as keyof typeof statusWatermarks
|
className={cn(
|
||||||
]
|
"pointer-events-none absolute top-0 left-0 z-10 h-full w-full overflow-hidden mix-blend-difference brightness-150 contrast-200 invert-[0.2]",
|
||||||
}
|
{
|
||||||
</span>
|
"text-destructive": item.status === 2,
|
||||||
);
|
"text-white": isActuallyExpired || item.status === 4,
|
||||||
})}
|
}
|
||||||
</div>
|
)}
|
||||||
</div>
|
>
|
||||||
)}
|
<div className="absolute inset-0">
|
||||||
<CardHeader className="flex flex-row flex-wrap items-center justify-between gap-2 space-y-0">
|
{Array.from({ length: 16 }).map((_, i) => {
|
||||||
<CardTitle className="font-medium">
|
const row = Math.floor(i / 4);
|
||||||
{item.subscribe.name}
|
const col = i % 4;
|
||||||
<p className="mt-1 text-foreground/50 text-sm">
|
const top = 10 + row * 25 + (col % 2 === 0 ? 5 : -5);
|
||||||
{formatDate(item.start_time)}
|
const left = 5 + col * 30 + (row % 2 === 0 ? 0 : 10);
|
||||||
</p>
|
|
||||||
</CardTitle>
|
return (
|
||||||
{item.status !== 4 && (
|
<span
|
||||||
<div className="flex flex-wrap gap-2">
|
className="absolute rotate-[-30deg] whitespace-nowrap font-black text-lg opacity-40 shadow-[0px_0px_1px_rgba(255,255,255,0.5)]"
|
||||||
<AlertDialog>
|
key={i}
|
||||||
<AlertDialogTrigger asChild>
|
style={{
|
||||||
<Button size="sm" variant="destructive">
|
top: `${top}%`,
|
||||||
{t("resetSubscription", "Reset Subscription")}
|
left: `${left}%`,
|
||||||
</Button>
|
|
||||||
</AlertDialogTrigger>
|
|
||||||
<AlertDialogContent>
|
|
||||||
<AlertDialogHeader>
|
|
||||||
<AlertDialogTitle>
|
|
||||||
{t("prompt", "Prompt")}
|
|
||||||
</AlertDialogTitle>
|
|
||||||
<AlertDialogDescription>
|
|
||||||
{t(
|
|
||||||
"confirmResetSubscription",
|
|
||||||
"Are you sure you want to reset your subscription?"
|
|
||||||
)}
|
|
||||||
</AlertDialogDescription>
|
|
||||||
</AlertDialogHeader>
|
|
||||||
<AlertDialogFooter>
|
|
||||||
<AlertDialogCancel>
|
|
||||||
{t("cancel", "Cancel")}
|
|
||||||
</AlertDialogCancel>
|
|
||||||
<AlertDialogAction
|
|
||||||
onClick={async () => {
|
|
||||||
await resetUserSubscribeToken({
|
|
||||||
user_subscribe_id: item.id,
|
|
||||||
});
|
|
||||||
await refetch();
|
|
||||||
toast.success(t("resetSuccess", "Reset Success"));
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t("confirm", "Confirm")}
|
{
|
||||||
</AlertDialogAction>
|
statusWatermarks[
|
||||||
</AlertDialogFooter>
|
item.status as keyof typeof statusWatermarks
|
||||||
</AlertDialogContent>
|
]
|
||||||
</AlertDialog>
|
}
|
||||||
<ResetTraffic
|
</span>
|
||||||
id={item.id}
|
);
|
||||||
replacement={item.subscribe.replacement}
|
})}
|
||||||
/>
|
</div>
|
||||||
{item.expire_time !== 0 && (
|
|
||||||
<Renewal id={item.id} subscribe={item.subscribe} />
|
|
||||||
)}
|
|
||||||
<Unsubscribe
|
|
||||||
allowDeduction={item.subscribe.allow_deduction}
|
|
||||||
id={item.id}
|
|
||||||
onSuccess={refetch}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</CardHeader>
|
<CardHeader className="flex flex-row flex-wrap items-center justify-between gap-2 space-y-0">
|
||||||
<CardContent>
|
<CardTitle className="font-medium">
|
||||||
<ul className="grid grid-cols-2 gap-3 *:flex *:flex-col *:justify-between lg:grid-cols-4">
|
{item.subscribe.name}
|
||||||
<li>
|
<p className="mt-1 text-foreground/50 text-sm">
|
||||||
<span className="text-muted-foreground">
|
{formatDate(item.start_time)}
|
||||||
{t("used", "Used")}
|
</p>
|
||||||
</span>
|
</CardTitle>
|
||||||
<span className="font-bold text-2xl">
|
{item.status !== 4 && (
|
||||||
<Display
|
<div className="flex flex-wrap gap-2">
|
||||||
type="traffic"
|
<AlertDialog>
|
||||||
unlimited={!item.traffic}
|
<AlertDialogTrigger asChild>
|
||||||
value={item.upload + item.download}
|
<Button size="sm" variant="destructive">
|
||||||
/>
|
{t("resetSubscription", "Reset Subscription")}
|
||||||
</span>
|
</Button>
|
||||||
</li>
|
</AlertDialogTrigger>
|
||||||
<li>
|
<AlertDialogContent>
|
||||||
<span className="text-muted-foreground">
|
<AlertDialogHeader>
|
||||||
{t("totalTraffic", "Total Traffic")}
|
<AlertDialogTitle>
|
||||||
</span>
|
{t("prompt", "Prompt")}
|
||||||
<span className="font-bold text-2xl">
|
</AlertDialogTitle>
|
||||||
<Display
|
<AlertDialogDescription>
|
||||||
type="traffic"
|
{t(
|
||||||
unlimited={!item.traffic}
|
"confirmResetSubscription",
|
||||||
value={item.traffic}
|
"Are you sure you want to reset your subscription?"
|
||||||
/>
|
)}
|
||||||
</span>
|
</AlertDialogDescription>
|
||||||
</li>
|
</AlertDialogHeader>
|
||||||
<li>
|
<AlertDialogFooter>
|
||||||
<span className="text-muted-foreground">
|
<AlertDialogCancel>
|
||||||
{t("nextResetDays", "Next Reset Days")}
|
{t("cancel", "Cancel")}
|
||||||
</span>
|
</AlertDialogCancel>
|
||||||
<span className="font-semibold text-2xl">
|
<AlertDialogAction
|
||||||
{item.reset_time
|
onClick={async () => {
|
||||||
? differenceInDays(
|
await resetUserSubscribeToken({
|
||||||
new Date(item.reset_time),
|
user_subscribe_id: item.id,
|
||||||
new Date()
|
});
|
||||||
)
|
await refetch();
|
||||||
: t("noReset", "No Reset")}
|
toast.success(
|
||||||
</span>
|
t("resetSuccess", "Reset Success")
|
||||||
</li>
|
);
|
||||||
<li>
|
}}
|
||||||
<span className="text-muted-foreground">
|
|
||||||
{t("expirationDays", "Expiration Days")}
|
|
||||||
</span>
|
|
||||||
<span className="font-semibold text-2xl">
|
|
||||||
{}
|
|
||||||
{item.expire_time
|
|
||||||
? differenceInDays(
|
|
||||||
new Date(item.expire_time),
|
|
||||||
new Date()
|
|
||||||
) || t("unknown", "Unknown")
|
|
||||||
: t("noLimit", "No Limit")}
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<Separator className="mt-4" />
|
|
||||||
<Accordion
|
|
||||||
className="w-full"
|
|
||||||
collapsible
|
|
||||||
defaultValue="0"
|
|
||||||
type="single"
|
|
||||||
>
|
|
||||||
{getUserSubscribe(item.token, protocol)?.map((url, index) => (
|
|
||||||
<AccordionItem key={url} value={String(index)}>
|
|
||||||
<AccordionTrigger className="hover:no-underline">
|
|
||||||
<div className="flex w-full flex-row items-center justify-between">
|
|
||||||
<CardTitle className="font-medium text-sm">
|
|
||||||
{t("subscriptionUrl", "Subscription URL")}{" "}
|
|
||||||
{index + 1}
|
|
||||||
</CardTitle>
|
|
||||||
|
|
||||||
<CopyToClipboard
|
|
||||||
onCopy={(_, result) => {
|
|
||||||
if (result) {
|
|
||||||
toast.success(t("copySuccess", "Copy Success"));
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
text={url}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className="mr-4 flex cursor-pointer rounded p-2 text-primary text-sm hover:bg-accent"
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
>
|
>
|
||||||
<Icon className="mr-2 size-5" icon="uil:copy" />
|
{t("confirm", "Confirm")}
|
||||||
{t("copy", "Copy")}
|
</AlertDialogAction>
|
||||||
</span>
|
</AlertDialogFooter>
|
||||||
</CopyToClipboard>
|
</AlertDialogContent>
|
||||||
</div>
|
</AlertDialog>
|
||||||
</AccordionTrigger>
|
<ResetTraffic
|
||||||
<AccordionContent>
|
id={item.id}
|
||||||
<div className="grid grid-cols-2 gap-4 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-6">
|
replacement={item.subscribe.replacement}
|
||||||
{applications
|
/>
|
||||||
?.filter(
|
{item.expire_time !== 0 && (
|
||||||
(application) =>
|
<Renewal id={item.id} subscribe={item.subscribe} />
|
||||||
!!(
|
)}
|
||||||
application.download_link?.[platform] &&
|
<Unsubscribe
|
||||||
application.scheme
|
allowDeduction={item.subscribe.allow_deduction}
|
||||||
)
|
id={item.id}
|
||||||
|
onSuccess={refetch}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<ul className="grid grid-cols-2 gap-3 *:flex *:flex-col *:justify-between lg:grid-cols-4">
|
||||||
|
<li>
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
{t("used", "Used")}
|
||||||
|
</span>
|
||||||
|
<span className="font-bold text-2xl">
|
||||||
|
<Display
|
||||||
|
type="traffic"
|
||||||
|
unlimited={!item.traffic}
|
||||||
|
value={item.upload + item.download}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
{t("totalTraffic", "Total Traffic")}
|
||||||
|
</span>
|
||||||
|
<span className="font-bold text-2xl">
|
||||||
|
<Display
|
||||||
|
type="traffic"
|
||||||
|
unlimited={!item.traffic}
|
||||||
|
value={item.traffic}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
{t("nextResetDays", "Next Reset Days")}
|
||||||
|
</span>
|
||||||
|
<span className="font-semibold text-2xl">
|
||||||
|
{item.reset_time
|
||||||
|
? differenceInDays(
|
||||||
|
new Date(item.reset_time),
|
||||||
|
new Date()
|
||||||
)
|
)
|
||||||
.map((application) => {
|
: t("noReset", "No Reset")}
|
||||||
const downloadUrl =
|
</span>
|
||||||
application.download_link?.[platform];
|
</li>
|
||||||
|
<li>
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
{t("expirationDays", "Expiration Days")}
|
||||||
|
</span>
|
||||||
|
<span className="font-semibold text-2xl">
|
||||||
|
{}
|
||||||
|
{item.expire_time
|
||||||
|
? differenceInDays(
|
||||||
|
new Date(item.expire_time),
|
||||||
|
new Date()
|
||||||
|
) || t("unknown", "Unknown")
|
||||||
|
: t("noLimit", "No Limit")}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<Separator className="mt-4" />
|
||||||
|
<Accordion
|
||||||
|
className="w-full"
|
||||||
|
collapsible
|
||||||
|
defaultValue="0"
|
||||||
|
type="single"
|
||||||
|
>
|
||||||
|
{getUserSubscribe(item.short, item.token, protocol)?.map(
|
||||||
|
(url, index) => (
|
||||||
|
<AccordionItem key={url} value={String(index)}>
|
||||||
|
<AccordionTrigger className="hover:no-underline">
|
||||||
|
<div className="flex w-full flex-row items-center justify-between">
|
||||||
|
<CardTitle className="font-medium text-sm">
|
||||||
|
{t("subscriptionUrl", "Subscription URL")}{" "}
|
||||||
|
{index + 1}
|
||||||
|
</CardTitle>
|
||||||
|
|
||||||
const handleCopy = (
|
<CopyToClipboard
|
||||||
_: string,
|
onCopy={(_, result) => {
|
||||||
result: boolean
|
if (result) {
|
||||||
) => {
|
|
||||||
if (result) {
|
|
||||||
const href = getAppSubLink(
|
|
||||||
url,
|
|
||||||
application.scheme
|
|
||||||
);
|
|
||||||
const showSuccessMessage = () => {
|
|
||||||
toast.success(
|
toast.success(
|
||||||
<>
|
t("copySuccess", "Copy Success")
|
||||||
<p>
|
|
||||||
{t("copySuccess", "Copy Success")}
|
|
||||||
</p>
|
|
||||||
<br />
|
|
||||||
<p>
|
|
||||||
{t(
|
|
||||||
"manualImportMessage",
|
|
||||||
"Please import manually"
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
text={url}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="mr-4 flex cursor-pointer rounded p-2 text-primary text-sm hover:bg-accent"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
className="mr-2 size-5"
|
||||||
|
icon="uil:copy"
|
||||||
|
/>
|
||||||
|
{t("copy", "Copy")}
|
||||||
|
</span>
|
||||||
|
</CopyToClipboard>
|
||||||
|
</div>
|
||||||
|
</AccordionTrigger>
|
||||||
|
<AccordionContent>
|
||||||
|
<div className="grid grid-cols-2 gap-4 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-6">
|
||||||
|
{applications
|
||||||
|
?.filter(
|
||||||
|
(application) =>
|
||||||
|
!!(
|
||||||
|
application.download_link?.[platform] &&
|
||||||
|
application.scheme
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.map((application) => {
|
||||||
|
const downloadUrl =
|
||||||
|
application.download_link?.[platform];
|
||||||
|
|
||||||
|
const handleCopy = (
|
||||||
|
_: string,
|
||||||
|
result: boolean
|
||||||
|
) => {
|
||||||
|
if (result) {
|
||||||
|
const href = getAppSubLink(
|
||||||
|
url,
|
||||||
|
application.scheme
|
||||||
|
);
|
||||||
|
const showSuccessMessage = () => {
|
||||||
|
toast.success(
|
||||||
|
<>
|
||||||
|
<p>
|
||||||
|
{t("copySuccess", "Copy Success")}
|
||||||
|
</p>
|
||||||
|
<br />
|
||||||
|
<p>
|
||||||
|
{t(
|
||||||
|
"manualImportMessage",
|
||||||
|
"Please import manually"
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isBrowser() && href) {
|
||||||
|
window.location.href = href;
|
||||||
|
const checkRedirect = setTimeout(() => {
|
||||||
|
if (window.location.href !== href) {
|
||||||
|
showSuccessMessage();
|
||||||
|
}
|
||||||
|
clearTimeout(checkRedirect);
|
||||||
|
}, 1000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showSuccessMessage();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isBrowser() && href) {
|
return (
|
||||||
window.location.href = href;
|
<div
|
||||||
const checkRedirect = setTimeout(() => {
|
className="flex size-full flex-col items-center justify-between gap-2 text-muted-foreground text-xs"
|
||||||
if (window.location.href !== href) {
|
key={application.name}
|
||||||
showSuccessMessage();
|
>
|
||||||
}
|
<span>{application.name}</span>
|
||||||
clearTimeout(checkRedirect);
|
|
||||||
}, 1000);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
showSuccessMessage();
|
{application.icon && (
|
||||||
}
|
<img
|
||||||
};
|
alt={application.name}
|
||||||
|
className="p-1"
|
||||||
return (
|
height={64}
|
||||||
<div
|
src={application.icon}
|
||||||
className="flex size-full flex-col items-center justify-between gap-2 text-muted-foreground text-xs"
|
width={64}
|
||||||
key={application.name}
|
/>
|
||||||
>
|
)}
|
||||||
<span>{application.name}</span>
|
<div className="flex">
|
||||||
|
{downloadUrl && (
|
||||||
{application.icon && (
|
<Button
|
||||||
<img
|
asChild
|
||||||
alt={application.name}
|
className={
|
||||||
className="p-1"
|
application.scheme
|
||||||
height={64}
|
? "rounded-r-none px-1.5"
|
||||||
src={application.icon}
|
: "px-1.5"
|
||||||
width={64}
|
}
|
||||||
/>
|
size="sm"
|
||||||
)}
|
variant="secondary"
|
||||||
<div className="flex">
|
>
|
||||||
{downloadUrl && (
|
<a
|
||||||
<Button
|
href={downloadUrl}
|
||||||
asChild
|
rel="noopener noreferrer"
|
||||||
className={
|
target="_blank"
|
||||||
application.scheme
|
>
|
||||||
? "rounded-r-none px-1.5"
|
{t("download", "Download")}
|
||||||
: "px-1.5"
|
</a>
|
||||||
}
|
</Button>
|
||||||
size="sm"
|
|
||||||
variant="secondary"
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
href={downloadUrl}
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
{t("download", "Download")}
|
|
||||||
</a>
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{application.scheme && (
|
|
||||||
<CopyToClipboard
|
|
||||||
onCopy={handleCopy}
|
|
||||||
text={getAppSubLink(
|
|
||||||
url,
|
|
||||||
application.scheme
|
|
||||||
)}
|
)}
|
||||||
>
|
|
||||||
<Button
|
{application.scheme && (
|
||||||
className={
|
<CopyToClipboard
|
||||||
downloadUrl
|
onCopy={handleCopy}
|
||||||
? "rounded-l-none p-2"
|
text={getAppSubLink(
|
||||||
: "p-2"
|
url,
|
||||||
}
|
application.scheme
|
||||||
size="sm"
|
)}
|
||||||
>
|
>
|
||||||
{t("import", "Import")}
|
<Button
|
||||||
</Button>
|
className={
|
||||||
</CopyToClipboard>
|
downloadUrl
|
||||||
)}
|
? "rounded-l-none p-2"
|
||||||
</div>
|
: "p-2"
|
||||||
</div>
|
}
|
||||||
);
|
size="sm"
|
||||||
})}
|
>
|
||||||
<div className="hidden size-full flex-col items-center justify-between gap-2 text-muted-foreground text-sm lg:flex">
|
{t("import", "Import")}
|
||||||
<span>{t("qrCode", "QR Code")}</span>
|
</Button>
|
||||||
<QRCodeCanvas
|
</CopyToClipboard>
|
||||||
bgColor="transparent"
|
)}
|
||||||
fgColor="rgb(59, 130, 246)"
|
</div>
|
||||||
size={80}
|
</div>
|
||||||
value={url}
|
);
|
||||||
/>
|
})}
|
||||||
<span className="text-center">
|
<div className="hidden size-full flex-col items-center justify-between gap-2 text-muted-foreground text-sm lg:flex">
|
||||||
{t("scanToSubscribe", "Scan to Subscribe")}
|
<span>{t("qrCode", "QR Code")}</span>
|
||||||
</span>
|
<QRCodeCanvas
|
||||||
</div>
|
bgColor="transparent"
|
||||||
</div>
|
fgColor="rgb(59, 130, 246)"
|
||||||
</AccordionContent>
|
size={80}
|
||||||
</AccordionItem>
|
value={url}
|
||||||
))}
|
/>
|
||||||
</Accordion>
|
<span className="text-center">
|
||||||
</CardContent>
|
{t("scanToSubscribe", "Scan to Subscribe")}
|
||||||
</Card>
|
</span>
|
||||||
))}
|
</div>
|
||||||
|
</div>
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</Accordion>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@ -164,6 +164,7 @@ export default function Page() {
|
|||||||
order={{
|
order={{
|
||||||
...data,
|
...data,
|
||||||
unit_price: data?.subscribe?.unit_price,
|
unit_price: data?.subscribe?.unit_price,
|
||||||
|
show_original_price: data?.subscribe?.show_original_price,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@ -147,8 +147,8 @@ function MobileBindDialog({
|
|||||||
/>
|
/>
|
||||||
<SendCode
|
<SendCode
|
||||||
params={{
|
params={{
|
||||||
telephone_area_code: form.getValues().area_code,
|
telephone_area_code: form.watch("area_code"),
|
||||||
telephone: form.getValues().mobile,
|
telephone: form.watch("mobile"),
|
||||||
type: 1,
|
type: 1,
|
||||||
}}
|
}}
|
||||||
type="phone"
|
type="phone"
|
||||||
|
|||||||
@ -8,7 +8,7 @@ export interface GlobalStore {
|
|||||||
setCommon: (common: Partial<API.GetGlobalConfigResponse>) => void;
|
setCommon: (common: Partial<API.GetGlobalConfigResponse>) => void;
|
||||||
setUser: (user?: API.User) => void;
|
setUser: (user?: API.User) => void;
|
||||||
getUserInfo: () => Promise<void>;
|
getUserInfo: () => Promise<void>;
|
||||||
getUserSubscribe: (uuid: string, type?: string) => string[];
|
getUserSubscribe: (short: string, token: string, type?: string) => string[];
|
||||||
getAppSubLink: (url: string, schema?: string) => string;
|
getAppSubLink: (url: string, schema?: string) => string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -120,7 +120,7 @@ export const useGlobalStore = create<GlobalStore>((set, get) => ({
|
|||||||
console.error("Failed to refresh user:", error);
|
console.error("Failed to refresh user:", error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
getUserSubscribe: (uuid: string, type?: string) => {
|
getUserSubscribe: (short: string, token: string, type?: string) => {
|
||||||
const { pan_domain, subscribe_domain, subscribe_path } =
|
const { pan_domain, subscribe_domain, subscribe_path } =
|
||||||
get().common.subscribe || {};
|
get().common.subscribe || {};
|
||||||
const domains = subscribe_domain
|
const domains = subscribe_domain
|
||||||
@ -129,12 +129,13 @@ export const useGlobalStore = create<GlobalStore>((set, get) => ({
|
|||||||
|
|
||||||
return domains.map((domain) => {
|
return domains.map((domain) => {
|
||||||
if (pan_domain) {
|
if (pan_domain) {
|
||||||
if (type) return `https://${uuid}.${type}.${domain}`;
|
if (type)
|
||||||
return `https://${uuid}.${domain}`;
|
return `https://${short}.${type}.${domain}${subscribe_path}?token=${token}&type=${type}`;
|
||||||
|
return `https://${short}.${domain}${subscribe_path}?token=${token}`;
|
||||||
}
|
}
|
||||||
if (type)
|
if (type)
|
||||||
return `https://${domain}${subscribe_path}?token=${uuid}&type=${type}`;
|
return `https://${domain}${subscribe_path}?token=${token}&type=${type}`;
|
||||||
return `https://${domain}${subscribe_path}?token=${uuid}`;
|
return `https://${domain}${subscribe_path}?token=${token}`;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
getAppSubLink: (url: string, schema?: string) => {
|
getAppSubLink: (url: string, schema?: string) => {
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import tailwindcss from "@tailwindcss/vite";
|
|||||||
import { devtools } from "@tanstack/devtools-vite";
|
import { devtools } from "@tanstack/devtools-vite";
|
||||||
import { tanstackRouter } from "@tanstack/router-plugin/vite";
|
import { tanstackRouter } from "@tanstack/router-plugin/vite";
|
||||||
import viteReact from "@vitejs/plugin-react";
|
import viteReact from "@vitejs/plugin-react";
|
||||||
import { defineConfig, type Plugin } from "vite";
|
import { defineConfig, loadEnv, type Plugin } from "vite";
|
||||||
|
|
||||||
// Plugin to generate version.lock file after build
|
// Plugin to generate version.lock file after build
|
||||||
function versionLockPlugin(): Plugin {
|
function versionLockPlugin(): Plugin {
|
||||||
@ -24,33 +24,37 @@ function versionLockPlugin(): Plugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig(({ mode }) => {
|
||||||
base: "./",
|
const env = loadEnv(mode, process.cwd(), "");
|
||||||
plugins: [
|
|
||||||
devtools({ eventBusConfig: { port: 42_069 } }),
|
return {
|
||||||
tanstackRouter({
|
base: "./",
|
||||||
target: "react",
|
plugins: [
|
||||||
autoCodeSplitting: true,
|
devtools({ eventBusConfig: { port: 42_069 } }),
|
||||||
}),
|
tanstackRouter({
|
||||||
viteReact(),
|
target: "react",
|
||||||
tailwindcss(),
|
autoCodeSplitting: true,
|
||||||
versionLockPlugin(),
|
}),
|
||||||
],
|
viteReact(),
|
||||||
resolve: {
|
tailwindcss(),
|
||||||
alias: {
|
versionLockPlugin(),
|
||||||
"@": fileURLToPath(new URL("./src", import.meta.url)),
|
],
|
||||||
},
|
resolve: {
|
||||||
},
|
alias: {
|
||||||
server: {
|
"@": fileURLToPath(new URL("./src", import.meta.url)),
|
||||||
proxy: {
|
|
||||||
"/api": {
|
|
||||||
target: "https://api.ppanel.dev",
|
|
||||||
changeOrigin: true,
|
|
||||||
secure: false,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
server: {
|
||||||
build: {
|
proxy: {
|
||||||
assetsDir: "static",
|
"/api": {
|
||||||
},
|
target: env.VITE_API_BASE_URL || "https://api.ppanel.dev",
|
||||||
|
changeOrigin: true,
|
||||||
|
secure: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
assetsDir: "static",
|
||||||
|
},
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@ -211,8 +211,19 @@ After successful installation, you can access:
|
|||||||
- **User Panel**: `http://your-server-ip:8080`
|
- **User Panel**: `http://your-server-ip:8080`
|
||||||
- **Admin Panel**: `http://your-server-ip:8080/admin/`
|
- **Admin Panel**: `http://your-server-ip:8080/admin/`
|
||||||
|
|
||||||
::: warning Default Credentials
|
::: warning Administrator Account
|
||||||
Please change the default admin password immediately after first login for security.
|
**Default Administrator Account** (if not configured in config file):
|
||||||
|
- **Email**: `admin@ppanel.dev`
|
||||||
|
- **Password**: `password`
|
||||||
|
|
||||||
|
**One-Click Installation Script** will automatically generate random administrator credentials displayed at the end:
|
||||||
|
- **Email**: `admin-[8 random characters]@ppanel.dev`
|
||||||
|
- **Password**: `[Randomly generated 16-character password]`
|
||||||
|
|
||||||
|
**Security Recommendations**:
|
||||||
|
- When using the one-click script, save the generated credentials securely
|
||||||
|
- Change your password immediately after first login
|
||||||
|
- If using default credentials, **must** change both email and password after first login
|
||||||
:::
|
:::
|
||||||
|
|
||||||
### Configure Reverse Proxy (Optional)
|
### Configure Reverse Proxy (Optional)
|
||||||
|
|||||||
@ -309,6 +309,14 @@ ps aux | grep ppanel
|
|||||||
- **User Panel**: `http://your-server-ip:8080`
|
- **User Panel**: `http://your-server-ip:8080`
|
||||||
- **Admin Panel**: `http://your-server-ip:8080/admin/`
|
- **Admin Panel**: `http://your-server-ip:8080/admin/`
|
||||||
|
|
||||||
|
::: warning Default Credentials
|
||||||
|
**Default Administrator Account**:
|
||||||
|
- **Email**: `admin@ppanel.dev`
|
||||||
|
- **Password**: `password`
|
||||||
|
|
||||||
|
**Security**: Change the default credentials immediately after first login.
|
||||||
|
:::
|
||||||
|
|
||||||
### Configure Firewall
|
### Configure Firewall
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@ -223,7 +223,11 @@ After successful installation, you can access:
|
|||||||
- **Admin Panel**: `http://your-server-ip:8080/admin/`
|
- **Admin Panel**: `http://your-server-ip:8080/admin/`
|
||||||
|
|
||||||
::: warning Default Credentials
|
::: warning Default Credentials
|
||||||
Please change the default admin password immediately after first login for security.
|
**Default Administrator Account** (if not configured):
|
||||||
|
- **Email**: `admin@ppanel.dev`
|
||||||
|
- **Password**: `password`
|
||||||
|
|
||||||
|
**Security**: Change the default credentials immediately after first login.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
### Configure Reverse Proxy (Recommended)
|
### Configure Reverse Proxy (Recommended)
|
||||||
|
|||||||
@ -232,6 +232,14 @@ docker volume rm ppanel-data
|
|||||||
```
|
```
|
||||||
:::
|
:::
|
||||||
|
|
||||||
|
::: warning Default Credentials
|
||||||
|
**Default Administrator Account**:
|
||||||
|
- **Email**: `admin@ppanel.dev`
|
||||||
|
- **Password**: `password`
|
||||||
|
|
||||||
|
**Security**: Change the default credentials immediately after first login.
|
||||||
|
:::
|
||||||
|
|
||||||
## Upgrading
|
## Upgrading
|
||||||
|
|
||||||
Upgrade PPanel directly from the **Admin Dashboard**. On the dashboard homepage, you can check for new versions and upgrade with one click.
|
Upgrade PPanel directly from the **Admin Dashboard**. On the dashboard homepage, you can check for new versions and upgrade with one click.
|
||||||
|
|||||||
@ -113,6 +113,12 @@ get_user_input() {
|
|||||||
|
|
||||||
log_info "✓ Redis password generated"
|
log_info "✓ Redis password generated"
|
||||||
|
|
||||||
|
# Administrator configuration
|
||||||
|
ADMIN_EMAIL="admin-$(generate_secret | cut -c1-8)@ppanel.dev"
|
||||||
|
ADMIN_PASSWORD=$(generate_secret | cut -c1-16)
|
||||||
|
|
||||||
|
log_info "✓ Administrator password generated"
|
||||||
|
|
||||||
# Generate JWT secret
|
# Generate JWT secret
|
||||||
JWT_SECRET=$(generate_secret)
|
JWT_SECRET=$(generate_secret)
|
||||||
log_info "✓ JWT secret generated"
|
log_info "✓ JWT secret generated"
|
||||||
@ -145,6 +151,10 @@ JwtAuth:
|
|||||||
AccessSecret: $JWT_SECRET
|
AccessSecret: $JWT_SECRET
|
||||||
AccessExpire: 604800
|
AccessExpire: 604800
|
||||||
|
|
||||||
|
Administrator:
|
||||||
|
Email: $ADMIN_EMAIL
|
||||||
|
Password: "$ADMIN_PASSWORD"
|
||||||
|
|
||||||
Logger:
|
Logger:
|
||||||
ServiceName: ApiService
|
ServiceName: ApiService
|
||||||
Mode: console
|
Mode: console
|
||||||
@ -343,8 +353,10 @@ show_access_info() {
|
|||||||
else
|
else
|
||||||
log_warn "Unable to get public IP, please configure manually for public access"
|
log_warn "Unable to get public IP, please configure manually for public access"
|
||||||
fi
|
fi
|
||||||
echo ""
|
echo "" log_info "Administrator Account:"
|
||||||
log_info "Database Information:"
|
echo " Email: $ADMIN_EMAIL"
|
||||||
|
echo " Password: $ADMIN_PASSWORD"
|
||||||
|
echo "" log_info "Database Information:"
|
||||||
echo " MySQL (Container Network):"
|
echo " MySQL (Container Network):"
|
||||||
echo " Address: mysql:3306"
|
echo " Address: mysql:3306"
|
||||||
echo " User: $MYSQL_USER"
|
echo " User: $MYSQL_USER"
|
||||||
|
|||||||
@ -113,6 +113,12 @@ get_user_input() {
|
|||||||
|
|
||||||
log_info "✓ 已生成 Redis 密码"
|
log_info "✓ 已生成 Redis 密码"
|
||||||
|
|
||||||
|
# 管理员配置
|
||||||
|
ADMIN_EMAIL="admin-$(generate_secret | cut -c1-8)@ppanel.dev"
|
||||||
|
ADMIN_PASSWORD=$(generate_secret | cut -c1-16)
|
||||||
|
|
||||||
|
log_info "✓ 已生成管理员密码"
|
||||||
|
|
||||||
# 生成 JWT 密钥
|
# 生成 JWT 密钥
|
||||||
JWT_SECRET=$(generate_secret)
|
JWT_SECRET=$(generate_secret)
|
||||||
log_info "✓ 已生成 JWT 密钥"
|
log_info "✓ 已生成 JWT 密钥"
|
||||||
@ -145,6 +151,10 @@ JwtAuth:
|
|||||||
AccessSecret: $JWT_SECRET
|
AccessSecret: $JWT_SECRET
|
||||||
AccessExpire: 604800
|
AccessExpire: 604800
|
||||||
|
|
||||||
|
Administrator:
|
||||||
|
Email: $ADMIN_EMAIL
|
||||||
|
Password: "$ADMIN_PASSWORD"
|
||||||
|
|
||||||
Logger:
|
Logger:
|
||||||
ServiceName: ApiService
|
ServiceName: ApiService
|
||||||
Mode: console
|
Mode: console
|
||||||
@ -346,6 +356,10 @@ show_access_info() {
|
|||||||
log_warn "未能获取公网IP,如需外网访问请手动配置"
|
log_warn "未能获取公网IP,如需外网访问请手动配置"
|
||||||
fi
|
fi
|
||||||
echo ""
|
echo ""
|
||||||
|
log_info "管理员账户:"
|
||||||
|
echo " 邮箱: $ADMIN_EMAIL"
|
||||||
|
echo " 密码: $ADMIN_PASSWORD"
|
||||||
|
echo ""
|
||||||
log_info "数据库信息:"
|
log_info "数据库信息:"
|
||||||
echo " MySQL (容器间通信):"
|
echo " MySQL (容器间通信):"
|
||||||
echo " 地址: mysql:3306"
|
echo " 地址: mysql:3306"
|
||||||
|
|||||||
@ -211,8 +211,19 @@ docker compose ps
|
|||||||
- **用户面板**: `http://your-server-ip:8080`
|
- **用户面板**: `http://your-server-ip:8080`
|
||||||
- **管理后台**: `http://your-server-ip:8080/admin`
|
- **管理后台**: `http://your-server-ip:8080/admin`
|
||||||
|
|
||||||
::: warning 默认凭据
|
::: warning 管理员账户
|
||||||
为了安全起见,首次登录后请立即修改默认管理员密码。
|
**默认管理员账户**(如果配置文件未设置):
|
||||||
|
- **邮箱**: `admin@ppanel.dev`
|
||||||
|
- **密码**: `password`
|
||||||
|
|
||||||
|
**一键安装脚本**会自动生成随机的管理员账户并在安装结束时显示:
|
||||||
|
- **邮箱**: `admin-[8位随机字符]@ppanel.dev`
|
||||||
|
- **密码**: `[随机生成的16位密码]`
|
||||||
|
|
||||||
|
**安全建议**:
|
||||||
|
- 使用一键安装脚本时,请妥善保管脚本生成的凭据
|
||||||
|
- 首次登录后立即修改密码
|
||||||
|
- 如果使用默认账户,**必须**在首次登录后修改邮箱和密码
|
||||||
:::
|
:::
|
||||||
|
|
||||||
### 配置反向代理(可选)
|
### 配置反向代理(可选)
|
||||||
|
|||||||
@ -309,6 +309,14 @@ ps aux | grep ppanel
|
|||||||
- **用户面板**: `http://your-server-ip:8080`
|
- **用户面板**: `http://your-server-ip:8080`
|
||||||
- **管理后台**: `http://your-server-ip:8080/admin/`
|
- **管理后台**: `http://your-server-ip:8080/admin/`
|
||||||
|
|
||||||
|
::: warning 默认凭据
|
||||||
|
**默认管理员账号**:
|
||||||
|
- **邮箱**: `admin@ppanel.dev`
|
||||||
|
- **密码**: `password`
|
||||||
|
|
||||||
|
**安全提醒**: 首次登录后请立即修改默认凭据。
|
||||||
|
:::
|
||||||
|
|
||||||
### 配置防火墙
|
### 配置防火墙
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@ -219,7 +219,11 @@ docker compose logs -f ppanel
|
|||||||
- **管理后台**: `http://your-server-ip:8080/admin/`
|
- **管理后台**: `http://your-server-ip:8080/admin/`
|
||||||
|
|
||||||
::: warning 默认凭据
|
::: warning 默认凭据
|
||||||
为了安全起见,首次登录后请立即修改默认管理员密码。
|
**默认管理员账号**(如果未配置时):
|
||||||
|
- **邮箱**: `admin@ppanel.dev`
|
||||||
|
- **密码**: `password`
|
||||||
|
|
||||||
|
**安全提醒**: 首次登录后请立即修改默认凭据。
|
||||||
:::
|
:::
|
||||||
|
|
||||||
### 配置反向代理(推荐)
|
### 配置反向代理(推荐)
|
||||||
|
|||||||
@ -232,6 +232,14 @@ docker volume rm ppanel-data
|
|||||||
```
|
```
|
||||||
:::
|
:::
|
||||||
|
|
||||||
|
::: warning 默认凭据
|
||||||
|
**默认管理员账号**:
|
||||||
|
- **邮箱**: `admin@ppanel.dev`
|
||||||
|
- **密码**: `password`
|
||||||
|
|
||||||
|
**安全提醒**: 首次登录后请立即修改默认凭据。
|
||||||
|
:::
|
||||||
|
|
||||||
## 升级
|
## 升级
|
||||||
|
|
||||||
直接从**管理后台**主页升级 PPanel。在仪表盘主页可以检查新版本并一键升级。
|
直接从**管理后台**主页升级 PPanel。在仪表盘主页可以检查新版本并一键升级。
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "frontend",
|
"name": "frontend",
|
||||||
"version": "1.2.4-dev.1",
|
"version": "1.3.6",
|
||||||
"private": true,
|
"private": true,
|
||||||
"homepage": "https://github.com/perfect-panel/frontend",
|
"homepage": "https://github.com/perfect-panel/frontend",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import {
|
|||||||
} from "@workspace/ui/components/popover";
|
} from "@workspace/ui/components/popover";
|
||||||
import { cn } from "@workspace/ui/lib/utils";
|
import { cn } from "@workspace/ui/lib/utils";
|
||||||
import { intlFormat } from "date-fns";
|
import { intlFormat } from "date-fns";
|
||||||
import { CalendarIcon } from "lucide-react";
|
import { CalendarIcon, X } from "lucide-react";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import type { DayPicker } from "react-day-picker";
|
import type { DayPicker } from "react-day-picker";
|
||||||
|
|
||||||
@ -34,6 +34,15 @@ export function DatePicker({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleClear = (e: React.MouseEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
setDate(undefined);
|
||||||
|
if (onChange) {
|
||||||
|
onChange(0);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover>
|
<Popover>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
@ -45,7 +54,19 @@ export function DatePicker({
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
>
|
>
|
||||||
{value ? intlFormat(value) : <span>{placeholder}</span>}
|
{value ? intlFormat(value) : <span>{placeholder}</span>}
|
||||||
<CalendarIcon className="size-4" />
|
<div className="flex items-center gap-2">
|
||||||
|
{value && (
|
||||||
|
<button
|
||||||
|
className="flex items-center"
|
||||||
|
onClick={handleClear}
|
||||||
|
onMouseDown={handleClear}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<X className="size-4 opacity-50 hover:opacity-100" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<CalendarIcon className="size-4" />
|
||||||
|
</div>
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent align="start" className="w-auto p-0">
|
<PopoverContent align="start" className="w-auto p-0">
|
||||||
|
|||||||
@ -60,7 +60,10 @@ export function MonacoEditor({
|
|||||||
const size = useSize(ref);
|
const size = useSize(ref);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setInternalValue(propValue);
|
// Only update internalValue if propValue has actually changed and is different from current value
|
||||||
|
if (propValue !== internalValue) {
|
||||||
|
setInternalValue(propValue);
|
||||||
|
}
|
||||||
}, [propValue]);
|
}, [propValue]);
|
||||||
|
|
||||||
const debouncedOnChange = useRef(
|
const debouncedOnChange = useRef(
|
||||||
|
|||||||
@ -128,13 +128,18 @@ export function TagInput({
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
>
|
>
|
||||||
{tag}
|
{tag}
|
||||||
<X
|
<button
|
||||||
className="size-4 cursor-pointer rounded-sm hover:text-destructive"
|
className="ml-1 inline-flex items-center justify-center"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
handleRemoveTag(index);
|
handleRemoveTag(index);
|
||||||
}}
|
}}
|
||||||
/>
|
onMouseDown={(e) => e.preventDefault()}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<X className="size-4 cursor-pointer rounded-sm hover:text-destructive" />
|
||||||
|
</button>
|
||||||
</Badge>
|
</Badge>
|
||||||
))}
|
))}
|
||||||
<div className="flex min-w-0 flex-1 items-center gap-2">
|
<div className="flex min-w-0 flex-1 items-center gap-2">
|
||||||
|
|||||||
@ -135,6 +135,14 @@ function handleError(response: {
|
|||||||
"components:error.60005",
|
"components:error.60005",
|
||||||
"Single subscription mode has exceeded user limit"
|
"Single subscription mode has exceeded user limit"
|
||||||
),
|
),
|
||||||
|
60006: t(
|
||||||
|
"components:error.60006",
|
||||||
|
"User quota limit has been reached, unable to continue."
|
||||||
|
),
|
||||||
|
60007: t(
|
||||||
|
"components:error.60007",
|
||||||
|
"Insufficient inventory, please try again later or contact the administrator."
|
||||||
|
),
|
||||||
70001: t(
|
70001: t(
|
||||||
"components:error.70001",
|
"components:error.70001",
|
||||||
"Incorrect verification code, please re-enter."
|
"Incorrect verification code, please re-enter."
|
||||||
@ -185,7 +193,7 @@ request.interceptors.request.use(
|
|||||||
request.interceptors.response.use(
|
request.interceptors.response.use(
|
||||||
(response) => {
|
(response) => {
|
||||||
const { code } = response.data;
|
const { code } = response.data;
|
||||||
if (code !== 200) {
|
if (code !== 200 && code !== 0) {
|
||||||
handleError({
|
handleError({
|
||||||
data: response.data,
|
data: response.data,
|
||||||
config: {
|
config: {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user