mirror of
https://github.com/jason5ng32/MyIP.git
synced 2026-11-04 08:55:47 +00:00
Feat(ip-infos): point score details to a live blocklist check
Score details is one of the most-opened panels, but the score is not a live reading. A line under it links to Blocklist Check with the IP carried as `?q=`, which the tool now prefills without running: the visitor may be signed out or out of quota. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
117ebaa65a
commit
017e5c9af3
9 files changed
+77
-4
No files matched your search
+3
-1
@@ -160,7 +160,9 @@ Every "business state → color" mapping goes through `use-status-tone.js`
|
||||
- **Shareable tool input** — watch `useActiveValue(() => route.query.q, {
|
||||
pathOf: () => route.path })` (immediate, so mount, later changes and a `q`
|
||||
changed while cached all run; another page's `q` never does), `router.replace`
|
||||
it on every run (AsnProfile); the URL is `/tools/<slug>?q=`.
|
||||
it on every run (AsnProfile); the URL is `/tools/<slug>?q=`. A `q` runs
|
||||
the tool only when a run needs no sign-in or quota (AsnProfile,
|
||||
IpCalculator); otherwise it just prefills (IpBlocklist).
|
||||
- **Fixed option sets** — a closed list wider than one line is a `Select`.
|
||||
- **Qualifier + input + run** — `Select` + `Input` in one `ButtonGroup`, the run
|
||||
Button in a second (DnsResolver); trigger `w-auto shrink-0`; never wraps.
|
||||
|
||||
@@ -201,8 +201,10 @@
|
||||
// IP Blocklist Check: which blocklists an IP is on — live DNSBLs and static
|
||||
// threat lists — via /api/ipblocklist (the private API's /iphitlist; signed-in
|
||||
// users, original site only). Verdict / row split live in
|
||||
// utils/features/ip-blocklist.js.
|
||||
import { ref, computed } from 'vue';
|
||||
// utils/features/ip-blocklist.js. A `?q=` IP is prefilled, never run: the
|
||||
// visitor may be signed out or out of quota.
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useMainStore } from '@/store';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { trackEvent } from '@/utils/analytics';
|
||||
@@ -210,6 +212,7 @@ import { authenticatedFetch } from '@/utils/authenticated-fetch';
|
||||
import { isoToDateTime, formatIsoDate } from '@/utils/time-utils.js';
|
||||
import { blocklistVerdict, listedCounts, splitPart, itemTone, reasonMessage } from '@/utils/features/ip-blocklist.js';
|
||||
import { selectableIPs, classifyTarget } from '@/composables/use-globalping-measurement';
|
||||
import { useActiveValue } from '@/composables/use-route-active.js';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from '@/components/ui/select';
|
||||
import { Input } from '@/components/ui/input';
|
||||
@@ -226,6 +229,8 @@ import {
|
||||
|
||||
const { t, te } = useI18n();
|
||||
const store = useMainStore();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const lang = computed(() => store.lang);
|
||||
|
||||
// The backend waits up to 30s on the upstream; give it room to answer first.
|
||||
@@ -254,6 +259,29 @@ const manualIP = ref('');
|
||||
const inputInvalid = ref(false);
|
||||
const targetIP = computed(() => (manualMode.value ? manualIP.value.trim() : selectedIP.value));
|
||||
|
||||
// Shareable `?q=` (use-route-active.js): a stored IP is picked in the
|
||||
// dropdown, any other goes to manual entry.
|
||||
const prefill = (ip) => {
|
||||
inputInvalid.value = false;
|
||||
if (allIPs.value.some((item) => item.ip === ip)) {
|
||||
useStored.value = true;
|
||||
selectedIP.value = ip;
|
||||
} else {
|
||||
useStored.value = false;
|
||||
manualIP.value = ip;
|
||||
}
|
||||
};
|
||||
const sharedQuery = useActiveValue(() => route.query.q, { pathOf: () => route.path });
|
||||
watch(sharedQuery, (q) => {
|
||||
if (typeof q === 'string' && q.trim() && q.trim() !== targetIP.value) prefill(q.trim());
|
||||
}, { immediate: true });
|
||||
|
||||
// Keep `?q=` in step with the last run without growing history.
|
||||
const syncQuery = (q) => {
|
||||
if (route.query.q === q) return;
|
||||
router.replace({ query: { ...route.query, q } });
|
||||
};
|
||||
|
||||
const status = ref('idle'); // 'idle' | 'running'
|
||||
const isRunning = computed(() => status.value === 'running');
|
||||
const canRun = computed(() => !isRunning.value && !!store.user && !!targetIP.value);
|
||||
@@ -316,6 +344,7 @@ const runCheck = async () => {
|
||||
quotaExceeded.value = true;
|
||||
return;
|
||||
}
|
||||
syncQuery(targetIP.value);
|
||||
status.value = 'running';
|
||||
errorMsg.value = '';
|
||||
quotaExceeded.value = false;
|
||||
|
||||
@@ -234,6 +234,16 @@
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<!-- Point to a blocklist check of this IP. -->
|
||||
<p v-if="blocklistHref"
|
||||
class="border-t pt-3 flex flex-wrap items-center gap-x-1.5 gap-y-1 text-xs leading-relaxed text-muted-foreground">
|
||||
<span>{{ t('ipInfos.scoreDetails.blocklistHint') }}</span>
|
||||
<a :href="blocklistHref" @click="openBlocklist"
|
||||
class="inline-flex items-center gap-0.5 font-medium text-foreground underline-offset-2 hover:underline">
|
||||
{{ t('ipInfos.scoreDetails.blocklistLink') }}<ArrowRight class="size-3" />
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
@@ -311,6 +321,7 @@
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import { useMainStore } from '@/store';
|
||||
import { useI18n, I18nT } from 'vue-i18n';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { trackEvent } from '@/utils/analytics';
|
||||
import { fetchWithTimeout } from '@/utils/fetch-with-timeout.js';
|
||||
import { loadAsnInfoInto } from '@/composables/use-asn-info.js';
|
||||
@@ -318,6 +329,9 @@ import { loadAsnConnectivityInto } from '@/utils/ip/asn-connectivity.js';
|
||||
import { toBgpPrefix } from '@/utils/ip/bgp-prefix.js';
|
||||
import { getZoneUtcOffset, getZoneLocalTime, formatIsoDate } from '@/utils/time-utils.js';
|
||||
import { buildScoreExplanation, listScoreTags } from '@/utils/ip/ip-score-details.js';
|
||||
import { isToolAvailable } from '@/utils/tool-availability.js';
|
||||
import { TOOL_BY_SLUG } from '@/data/tools.js';
|
||||
import { isPlainClick } from '@/utils/nav-target.js';
|
||||
import ASNInfo from './ASNInfo.vue';
|
||||
import ASNHistory from './ASNHistory.vue';
|
||||
import CountryTraffic from './CountryTraffic.vue';
|
||||
@@ -335,6 +349,7 @@ import { Button } from '@/components/ui/button';
|
||||
import { Icon } from '@iconify/vue';
|
||||
import {
|
||||
Activity,
|
||||
ArrowRight,
|
||||
Building2,
|
||||
Check,
|
||||
ChevronDown,
|
||||
@@ -384,7 +399,7 @@ const props = defineProps({
|
||||
});
|
||||
|
||||
// A host dialog listens to close itself first: before the Benefits & Usage
|
||||
// dialog stacks on it (`view-usage`) or ASN Info's profile link changes page (`open-tool`).
|
||||
// dialog stacks on it (`view-usage`) or a tool link (ASN Profile, Blocklist Check) changes page (`open-tool`).
|
||||
const emit = defineEmits(['view-usage', 'open-tool']);
|
||||
|
||||
const openUsageDialog = () => {
|
||||
@@ -495,6 +510,21 @@ const toggleScoreDetails = () => {
|
||||
// A different IP starts collapsed.
|
||||
watch(() => props.data.ip, () => { isScoreDetailsOpen.value = false; });
|
||||
|
||||
// Blocklist Check entry: hidden when the tool is gated off; `q` only prefills.
|
||||
const router = useRouter();
|
||||
const blocklistHref = computed(() => (
|
||||
isToolAvailable(TOOL_BY_SLUG.get('blocklist'), props.configs) && props.data.ip
|
||||
? `/tools/blocklist?q=${encodeURIComponent(props.data.ip)}`
|
||||
: ''
|
||||
));
|
||||
const openBlocklist = (e) => {
|
||||
if (!isPlainClick(e)) return;
|
||||
e.preventDefault();
|
||||
trackEvent('IPCheck', 'BlocklistCTAClick', 'Open Blocklist Check');
|
||||
emit('open-tool');
|
||||
router.push({ path: '/tools/blocklist', query: { q: props.data.ip } });
|
||||
};
|
||||
|
||||
// Consumer opt-in + a Cloudflare key on the deployment + a country to query.
|
||||
const canShowCountryTraffic = computed(() =>
|
||||
props.countryTrafficMode !== 'off' && !props.collapsed
|
||||
|
||||
@@ -1643,6 +1643,8 @@
|
||||
}
|
||||
},
|
||||
"tagsTitle": "All attributes",
|
||||
"blocklistHint": "The score is an overall profile of this IP. Want to know if it's blocklisted right now?",
|
||||
"blocklistLink": "Check blocklists live",
|
||||
"tagState": {
|
||||
"yes": "Yes",
|
||||
"no": "No",
|
||||
|
||||
@@ -1643,6 +1643,8 @@
|
||||
}
|
||||
},
|
||||
"tagsTitle": "Tous les attributs",
|
||||
"blocklistHint": "Le score reflète le profil global de cette IP. Vous voulez savoir si elle figure sur une liste noire en ce moment ?",
|
||||
"blocklistLink": "Vérifier les listes noires en direct",
|
||||
"tagState": {
|
||||
"yes": "Oui",
|
||||
"no": "Non",
|
||||
|
||||
@@ -1643,6 +1643,8 @@
|
||||
}
|
||||
},
|
||||
"tagsTitle": "Todos os atributos",
|
||||
"blocklistHint": "A pontuação reflete o perfil geral deste IP. Quer saber se ele está em alguma lista negra agora?",
|
||||
"blocklistLink": "Verificar listas negras em tempo real",
|
||||
"tagState": {
|
||||
"yes": "Sim",
|
||||
"no": "Não",
|
||||
|
||||
@@ -1643,6 +1643,8 @@
|
||||
}
|
||||
},
|
||||
"tagsTitle": "Все атрибуты",
|
||||
"blocklistHint": "Оценка отражает общий профиль этого IP. Хотите узнать, есть ли он в чёрных списках прямо сейчас?",
|
||||
"blocklistLink": "Проверить по чёрным спискам сейчас",
|
||||
"tagState": {
|
||||
"yes": "Да",
|
||||
"no": "Нет",
|
||||
|
||||
@@ -1643,6 +1643,8 @@
|
||||
}
|
||||
},
|
||||
"tagsTitle": "全部屬性",
|
||||
"blocklistHint": "評分反映的是這個 IP 的綜合輪廓。想知道它此刻有沒有上黑名單?",
|
||||
"blocklistLink": "即時檢查黑名單",
|
||||
"tagState": {
|
||||
"yes": "是",
|
||||
"no": "否",
|
||||
|
||||
@@ -1643,6 +1643,8 @@
|
||||
}
|
||||
},
|
||||
"tagsTitle": "全部属性",
|
||||
"blocklistHint": "评分反映的是这个 IP 的综合画像。想知道它此刻有没有上黑名单?",
|
||||
"blocklistLink": "实时检查黑名单",
|
||||
"tagState": {
|
||||
"yes": "是",
|
||||
"no": "否",
|
||||
|
||||
Reference in new issue
Block a user