diff --git a/config/version b/config/version index 44e6d9a5..1a83aff2 100644 --- a/config/version +++ b/config/version @@ -1 +1 @@ -v1.7.2.4 +v1.7.2.5 \ No newline at end of file diff --git a/docs/Tasktracking/2026-04-25-user-panel-clash-link-and-quick-import.md b/docs/Tasktracking/2026-04-25-user-panel-clash-link-and-quick-import.md new file mode 100644 index 00000000..28729bb7 --- /dev/null +++ b/docs/Tasktracking/2026-04-25-user-panel-clash-link-and-quick-import.md @@ -0,0 +1,30 @@ +# 2026-04-25 — User Panel: Add Clash Link & Quick Import Button + +## Summary +Optimized the user panel (`/panel/user`) to show subscription info and add a one-click import dropdown. + +## Changes + +### Backend +- Added `settingService` field to `InboundController` +- New endpoint `GET /panel/api/inbounds/userSubscriptions` — returns `subId`, `subClashEnable`, `subClashUrl` for the logged-in user +- Route registered before `checkAdmin` middleware so non-admin users can access + +### Frontend (`web/html/user.html`) +- Redesigned page with 3 cards: + 1. **User Info** — traffic stats, expiry, status (polished) + 2. **Clash Link** — shows Clash subscription URL with copy button, or "暂无订阅" if not enabled + 3. **Quick Import** — dropdown button with Android/iOS/Desktop options with icons (visual only, functionality TBD) +- Added copy-to-clipboard via `ClipboardManager` + +### i18n +- Added keys to `translate.en_US.toml` and `translate.zh_CN.toml`: + - `clashUrl`, `quickImport`, `android`, `ios`, `desktop`, `copied`, `noSubscription` + +## Files Modified +- `web/controller/inbound.go` — added settingService, getUserSubscriptions method +- `web/controller/api.go` — registered new route +- `web/html/user.html` — redesigned user panel page +- `web/translation/translate.en_US.toml` — new i18n keys +- `web/translation/translate.zh_CN.toml` — new i18n keys +- `config/version` — bumped to v1.7.2.5 diff --git a/web/controller/api.go b/web/controller/api.go index b02af50d..a876d3a1 100644 --- a/web/controller/api.go +++ b/web/controller/api.go @@ -46,6 +46,7 @@ func (a *APIController) initRouter(g *gin.RouterGroup) { inbounds := api.Group("/inbounds") a.inboundController = &InboundController{} inbounds.GET("/userInfo", a.inboundController.getUserInfo) + inbounds.GET("/userSubscriptions", a.inboundController.getUserSubscriptions) inbounds.Use(a.checkAdmin) a.inboundController.initRouter(inbounds) diff --git a/web/controller/inbound.go b/web/controller/inbound.go index fbd915f6..901c5b1e 100644 --- a/web/controller/inbound.go +++ b/web/controller/inbound.go @@ -20,6 +20,7 @@ import ( type InboundController struct { inboundService service.InboundService xrayService service.XrayService + settingService service.SettingService } // NewInboundController creates a new InboundController and sets up its routes. @@ -479,3 +480,53 @@ func (a *InboundController) getUserInfo(c *gin.Context) { } jsonObj(c, traffic, nil) } + +// getUserSubscriptions returns subscription URLs for the logged-in user. +func (a *InboundController) getUserSubscriptions(c *gin.Context) { + user := session.GetLoginUser(c) + traffic, err := a.inboundService.GetClientTrafficByEmail(user.Username) + if err != nil || traffic == nil { + jsonObj(c, gin.H{"subClashEnable": false}, nil) + return + } + + subId := traffic.SubId + if subId == "" { + jsonObj(c, gin.H{"subClashEnable": false}, nil) + return + } + + settingsAny, err := a.settingService.GetDefaultSettings(c.Request.Host) + if err != nil { + jsonObj(c, gin.H{"subClashEnable": false}, nil) + return + } + + settings, ok := settingsAny.(map[string]any) + if !ok { + jsonObj(c, gin.H{"subClashEnable": false}, nil) + return + } + + subClashEnable := false + if v, ok := settings["subClashEnable"]; ok { + if b, ok2 := v.(bool); ok2 { + subClashEnable = b + } + } + + subClashUrl := "" + if subClashEnable { + if uri, ok := settings["subClashURI"]; ok { + if s, ok2 := uri.(string); ok2 && s != "" { + subClashUrl = s + subId + } + } + } + + jsonObj(c, gin.H{ + "subId": subId, + "subClashEnable": subClashEnable, + "subClashUrl": subClashUrl, + }, nil) +} diff --git a/web/html/user.html b/web/html/user.html index 38f8a041..ca07c6db 100644 --- a/web/html/user.html +++ b/web/html/user.html @@ -28,7 +28,8 @@ @@ -132,10 +177,13 @@ username: '', traffic: null, lang: '', + subClashEnable: false, + subClashUrl: '', }, async mounted() { this.lang = LanguageManager.getLanguage(); - await this.loadUserInfo(); + await Promise.all([this.loadUserInfo(), this.loadSubscriptions()]); + this.loading = false; }, methods: { async loadUserInfo() { @@ -148,7 +196,23 @@ } catch (e) { console.error("Failed to get user info:", e); } - this.loading = false; + }, + async loadSubscriptions() { + try { + const msg = await HttpUtil.get('/panel/api/inbounds/userSubscriptions'); + if (msg.success && msg.obj) { + this.subClashEnable = msg.obj.subClashEnable || false; + this.subClashUrl = msg.obj.subClashUrl || ''; + } + } catch (e) { + console.error("Failed to get subscriptions:", e); + } + }, + copy(text) { + ClipboardManager.copyText(text).then(ok => { + const messageType = ok ? 'success' : 'error'; + this.$message[messageType](ok ? '{{ i18n "pages.user.copied" }}' : 'Copy failed'); + }); }, formatExpiryTime(timestamp) { if (timestamp <= 0) return '{{ i18n "unlimited" }}'; @@ -166,4 +230,45 @@ }, }); + {{ template "page/body_end" .}} diff --git a/web/translation/translate.en_US.toml b/web/translation/translate.en_US.toml index 2fe6b018..3a93d8ec 100644 --- a/web/translation/translate.en_US.toml +++ b/web/translation/translate.en_US.toml @@ -333,6 +333,13 @@ "lastOnline" = "Last Online" "remained" = "Remained" "status" = "Status" +"clashUrl" = "Clash Link" +"quickImport" = "Quick Import" +"android" = "Android" +"ios" = "iOS" +"desktop" = "Desktop" +"copied" = "Copied" +"noSubscription" = "No subscription available" [pages.users] "title" = "User Management" diff --git a/web/translation/translate.zh_CN.toml b/web/translation/translate.zh_CN.toml index a9011120..db937804 100644 --- a/web/translation/translate.zh_CN.toml +++ b/web/translation/translate.zh_CN.toml @@ -333,6 +333,13 @@ "lastOnline" = "上次在线" "remained" = "剩余流量" "status" = "状态" +"clashUrl" = "Clash 链接" +"quickImport" = "一键添加" +"android" = "安卓" +"ios" = "iOS" +"desktop" = "桌面端" +"copied" = "已复制" +"noSubscription" = "暂无订阅" [pages.users] "title" = "用户管理"