@@ -97,47 +264,80 @@ export default function ComparePage() {
-
+ {isReadOnly &&
}
-
-
-
+ >
+ )}
{hasResults && (
<>
@@ -146,8 +346,22 @@ export default function ComparePage() {
| {t('节点', 'Node')} |
- {targetA || 'A'} |
- {targetB || 'B'} |
+
+
+ {targetA || 'A'}
+ {resolvedIpA && !IP_REGEX.test(targetA.trim()) && (
+ {resolvedIpA}
+ )}
+
+ |
+
+
+ {targetB || 'B'}
+ {resolvedIpB && !IP_REGEX.test(targetB.trim()) && (
+ {resolvedIpB}
+ )}
+
+ |
{t('差值', 'Diff')} |
@@ -221,6 +435,12 @@ export default function ComparePage() {
)}
>
)}
+
+
setShowShareModal(false)}
+ shareUrl={shareUrl || ''}
+ />
)
}
diff --git a/src/client/components/ExpirationBanner.css b/src/client/components/ExpirationBanner.css
new file mode 100644
index 0000000..9057c12
--- /dev/null
+++ b/src/client/components/ExpirationBanner.css
@@ -0,0 +1,51 @@
+.expiration-banner {
+ background: rgba(245, 158, 11, 0.1);
+ padding: 12px 1.5rem;
+ margin-bottom: 2rem;
+ border-radius: 12px;
+ border: 1px solid rgba(245, 158, 11, 0.2);
+}
+
+[data-theme='dark'] .expiration-banner {
+ background: rgba(245, 158, 11, 0.05);
+}
+
+.banner-content {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 12px;
+ flex-wrap: wrap;
+}
+
+.banner-icon {
+ font-size: 1.1rem;
+}
+
+.banner-text {
+ color: var(--warning-color);
+ font-size: 0.9rem;
+ font-weight: 500;
+ text-align: center;
+}
+
+.banner-copy-btn {
+ background: var(--warning-color);
+ color: white;
+ border: none;
+ padding: 6px 14px;
+ border-radius: 6px;
+ font-size: 0.8rem;
+ font-weight: 600;
+ cursor: pointer;
+ transition: opacity 0.2s;
+}
+
+.banner-copy-btn:hover {
+ opacity: 0.9;
+}
+
+@media (max-width: 600px) {
+ .expiration-banner { padding: 1rem; }
+ .banner-content { flex-direction: column; gap: 8px; }
+}
diff --git a/src/client/components/ExpirationBanner.tsx b/src/client/components/ExpirationBanner.tsx
new file mode 100644
index 0000000..2837b93
--- /dev/null
+++ b/src/client/components/ExpirationBanner.tsx
@@ -0,0 +1,43 @@
+import { useState } from 'react'
+import { useLanguage } from '../contexts/LanguageContext'
+import Toast from './Toast'
+import './ExpirationBanner.css'
+
+interface ExpirationBannerProps {
+ shareUrl?: string
+}
+
+export default function ExpirationBanner({ shareUrl }: ExpirationBannerProps) {
+ const { t } = useLanguage()
+ const [showToast, setShowToast] = useState(false)
+
+ const handleCopy = () => {
+ if (shareUrl) {
+ navigator.clipboard.writeText(shareUrl)
+ setShowToast(true)
+ }
+ }
+
+ return (
+ <>
+