MHSanaei 1 день назад
Родитель
Сommit
e5c0fe3edf
4 измененных файлов с 231 добавлено и 19 удалено
  1. 32 0
      web/controller/inbound.go
  2. 128 18
      web/html/modals/inbound_info_modal.html
  3. 37 0
      web/service/inbound.go
  4. 34 1
      web/service/tgbot.go

+ 32 - 0
web/controller/inbound.go

@@ -4,6 +4,7 @@ import (
 	"encoding/json"
 	"fmt"
 	"strconv"
+	"time"
 
 	"github.com/mhsanaei/3x-ui/v2/database/model"
 	"github.com/mhsanaei/3x-ui/v2/web/service"
@@ -193,6 +194,37 @@ func (a *InboundController) getClientIps(c *gin.Context) {
 		return
 	}
 
+	// Prefer returning a normalized string list for consistent UI rendering
+	type ipWithTimestamp struct {
+		IP        string `json:"ip"`
+		Timestamp int64  `json:"timestamp"`
+	}
+
+	var ipsWithTime []ipWithTimestamp
+	if err := json.Unmarshal([]byte(ips), &ipsWithTime); err == nil && len(ipsWithTime) > 0 {
+		formatted := make([]string, 0, len(ipsWithTime))
+		for _, item := range ipsWithTime {
+			if item.IP == "" {
+				continue
+			}
+			if item.Timestamp > 0 {
+				ts := time.Unix(item.Timestamp, 0).Local().Format("2006-01-02 15:04:05")
+				formatted = append(formatted, fmt.Sprintf("%s (%s)", item.IP, ts))
+				continue
+			}
+			formatted = append(formatted, item.IP)
+		}
+		jsonObj(c, formatted, nil)
+		return
+	}
+
+	var oldIps []string
+	if err := json.Unmarshal([]byte(ips), &oldIps); err == nil && len(oldIps) > 0 {
+		jsonObj(c, oldIps, nil)
+		return
+	}
+
+	// If parsing fails, return as string
 	jsonObj(c, ips, nil)
 }
 

+ 128 - 18
web/html/modals/inbound_info_modal.html

@@ -260,15 +260,31 @@
               v-if="app.ipLimitEnable && infoModal.clientSettings.limitIp > 0">
               <td>{{ i18n "pages.inbounds.IPLimitlog" }}</td>
               <td>
-                <a-tag>[[ infoModal.clientIps ]]</a-tag>
-                <a-icon type="sync" :spin="refreshing" @click="refreshIPs"
-                  :style="{ margin: '0 5px' }"></a-icon>
-                <a-tooltip :title="[[ dbInbound.address ]]">
-                  <template slot="title">
-                    <span>{{ i18n "pages.inbounds.IPLimitlogclear" }}</span>
-                  </template>
-                  <a-icon type="delete" @click="clearClientIps"></a-icon>
-                </a-tooltip>
+                <div
+                  style="max-height: 150px; overflow-y: auto; text-align: left;">
+                  <div
+                    v-if="infoModal.clientIpsArray && infoModal.clientIpsArray.length > 0">
+                    <a-tag
+                      v-for="(ipInfo, idx) in infoModal.clientIpsArray"
+                      :key="idx"
+                      color="blue"
+                      style="margin: 2px 0; display: block; font-family: monospace; font-size: 11px;">
+                      [[ formatIpInfo(ipInfo) ]]
+                    </a-tag>
+                  </div>
+                  <a-tag v-else>[[ infoModal.clientIps || 'No IP Record'
+                    ]]</a-tag>
+                </div>
+                <div style="margin-top: 5px;">
+                  <a-icon type="sync" :spin="refreshing" @click="refreshIPs"
+                    :style="{ margin: '0 5px' }"></a-icon>
+                  <a-tooltip>
+                    <template slot="title">
+                      <span>{{ i18n "pages.inbounds.IPLimitlogclear" }}</span>
+                    </template>
+                    <a-icon type="delete" @click="clearClientIps"></a-icon>
+                  </a-tooltip>
+                </div>
               </td>
             </tr>
           </table>
@@ -542,12 +558,73 @@
       <script>
   function refreshIPs(email) {
     return HttpUtil.post(`/panel/api/inbounds/clientIps/${email}`).then((msg) => {
-      if (msg.success) {
-        try {
-          return JSON.parse(msg.obj).join(', ');
-        } catch (e) {
-          return msg.obj;
+      if (!msg.success) {
+        return { text: 'No IP Record', array: [] };
+      }
+
+      const formatIpRecord = (record) => {
+        if (record == null) {
+          return '';
+        }
+        if (typeof record === 'string' || typeof record === 'number') {
+          return String(record);
+        }
+        const ip = record.ip || record.IP || '';
+        const timestamp = record.timestamp || record.Timestamp || 0;
+        if (!ip) {
+          return String(record);
+        }
+        if (!timestamp) {
+          return String(ip);
+        }
+        const date = new Date(Number(timestamp) * 1000);
+        const timeStr = date
+          .toLocaleString('en-GB', {
+            year: 'numeric',
+            month: '2-digit',
+            day: '2-digit',
+            hour: '2-digit',
+            minute: '2-digit',
+            second: '2-digit',
+            hour12: false,
+          })
+          .replace(',', '');
+        return `${ip} (${timeStr})`;
+      };
+
+      try {
+        let ips = msg.obj;
+        // If msg.obj is a string, try to parse it
+        if (typeof ips === 'string') {
+          try {
+            ips = JSON.parse(ips);
+          } catch (e) {
+            return { text: String(ips), array: [String(ips)] };
+          }
+        }
+
+        // Normalize single object response to array
+        if (ips && !Array.isArray(ips) && typeof ips === 'object') {
+          ips = [ips];
         }
+
+        // New format or object array
+        if (Array.isArray(ips) && ips.length > 0 && typeof ips[0] === 'object') {
+          const result = ips.map((item) => formatIpRecord(item)).filter(Boolean);
+          return { text: result.join(' | '), array: result };
+        }
+
+        // Old format - simple array of IPs
+        if (Array.isArray(ips) && ips.length > 0) {
+          const result = ips.map((ip) => String(ip));
+          return { text: result.join(', '), array: result };
+        }
+
+        // Fallback for any other format
+        return { text: String(ips), array: [String(ips)] };
+
+      } catch (e) {
+        return { text: 'Error loading IPs', array: [] };
       }
     });
   }
@@ -566,6 +643,7 @@
     subLink: '',
     subJsonLink: '',
     clientIps: '',
+    clientIpsArray: [],
     show(dbInbound, index) {
       this.index = index;
       this.inbound = dbInbound.toInbound();
@@ -583,8 +661,9 @@
         ].includes(this.inbound.protocol)
       ) {
         if (app.ipLimitEnable && this.clientSettings.limitIp) {
-          refreshIPs(this.clientStats.email).then((ips) => {
-            this.clientIps = ips;
+          refreshIPs(this.clientStats.email).then((result) => {
+            this.clientIps = result.text;
+            this.clientIpsArray = result.array;
           })
         }
       }
@@ -655,6 +734,35 @@
       },
     },
     methods: {
+      formatIpInfo(ipInfo) {
+        if (ipInfo == null) {
+          return '';
+        }
+        if (typeof ipInfo === 'string' || typeof ipInfo === 'number') {
+          return String(ipInfo);
+        }
+        const ip = ipInfo.ip || ipInfo.IP || '';
+        const timestamp = ipInfo.timestamp || ipInfo.Timestamp || 0;
+        if (!ip) {
+          return String(ipInfo);
+        }
+        if (!timestamp) {
+          return String(ip);
+        }
+        const date = new Date(Number(timestamp) * 1000);
+        const timeStr = date
+          .toLocaleString('en-GB', {
+            year: 'numeric',
+            month: '2-digit',
+            day: '2-digit',
+            hour: '2-digit',
+            minute: '2-digit',
+            second: '2-digit',
+            hour12: false,
+          })
+          .replace(',', '');
+        return `${ip} (${timeStr})`;
+      },
       copy(content) {
         ClipboardManager
           .copyText(content)
@@ -672,8 +780,9 @@
       refreshIPs() {
         this.refreshing = true;
         refreshIPs(this.infoModal.clientStats.email)
-          .then((ips) => {
-            this.infoModal.clientIps = ips;
+          .then((result) => {
+            this.infoModal.clientIps = result.text;
+            this.infoModal.clientIpsArray = result.array;
           })
           .finally(() => {
             this.refreshing = false;
@@ -686,6 +795,7 @@
               return;
             }
             this.infoModal.clientIps = 'No IP Record';
+            this.infoModal.clientIpsArray = [];
           })
           .catch(() => {});
       },

+ 37 - 0
web/service/inbound.go

@@ -2141,6 +2141,43 @@ func (s *InboundService) GetInboundClientIps(clientEmail string) (string, error)
 	if err != nil {
 		return "", err
 	}
+	
+	if InboundClientIps.Ips == "" {
+		return "", nil
+	}
+	
+	// Try to parse as new format (with timestamps)
+	type IPWithTimestamp struct {
+		IP        string `json:"ip"`
+		Timestamp int64  `json:"timestamp"`
+	}
+	
+	var ipsWithTime []IPWithTimestamp
+	err = json.Unmarshal([]byte(InboundClientIps.Ips), &ipsWithTime)
+	
+	// If successfully parsed as new format, return with timestamps
+	if err == nil && len(ipsWithTime) > 0 {
+		return InboundClientIps.Ips, nil
+	}
+	
+	// Otherwise, assume it's old format (simple string array)
+	// Try to parse as simple array and convert to new format
+	var oldIps []string
+	err = json.Unmarshal([]byte(InboundClientIps.Ips), &oldIps)
+	if err == nil && len(oldIps) > 0 {
+		// Convert old format to new format with current timestamp
+		newIpsWithTime := make([]IPWithTimestamp, len(oldIps))
+		for i, ip := range oldIps {
+			newIpsWithTime[i] = IPWithTimestamp{
+				IP:        ip,
+				Timestamp: time.Now().Unix(),
+			}
+		}
+		result, _ := json.Marshal(newIpsWithTime)
+		return string(result), nil
+	}
+	
+	// Return as-is if parsing fails
 	return InboundClientIps.Ips, nil
 }
 

+ 34 - 1
web/service/tgbot.go

@@ -5,6 +5,7 @@ import (
 	"crypto/rand"
 	"embed"
 	"encoding/base64"
+	"encoding/json"
 	"errors"
 	"fmt"
 	"io"
@@ -3083,9 +3084,41 @@ func (t *Tgbot) searchClientIps(chatId int64, email string, messageID ...int) {
 		ips = t.I18nBot("tgbot.noIpRecord")
 	}
 
+	formattedIps := ips
+	if err == nil && len(ips) > 0 {
+		type ipWithTimestamp struct {
+			IP        string `json:"ip"`
+			Timestamp int64  `json:"timestamp"`
+		}
+
+		var ipsWithTime []ipWithTimestamp
+		if json.Unmarshal([]byte(ips), &ipsWithTime) == nil && len(ipsWithTime) > 0 {
+			lines := make([]string, 0, len(ipsWithTime))
+			for _, item := range ipsWithTime {
+				if item.IP == "" {
+					continue
+				}
+				if item.Timestamp > 0 {
+					ts := time.Unix(item.Timestamp, 0).Format("2006-01-02 15:04:05")
+					lines = append(lines, fmt.Sprintf("%s (%s)", item.IP, ts))
+					continue
+				}
+				lines = append(lines, item.IP)
+			}
+			if len(lines) > 0 {
+				formattedIps = strings.Join(lines, "\n")
+			}
+		} else {
+			var oldIps []string
+			if json.Unmarshal([]byte(ips), &oldIps) == nil && len(oldIps) > 0 {
+				formattedIps = strings.Join(oldIps, "\n")
+			}
+		}
+	}
+
 	output := ""
 	output += t.I18nBot("tgbot.messages.email", "Email=="+email)
-	output += t.I18nBot("tgbot.messages.ips", "IPs=="+ips)
+	output += t.I18nBot("tgbot.messages.ips", "IPs=="+formattedIps)
 	output += t.I18nBot("tgbot.messages.refreshedOn", "Time=="+time.Now().Format("2006-01-02 15:04:05"))
 
 	inlineKeyboard := tu.InlineKeyboard(