All checks were successful
Build and Deploy (tool.xpcool.com) / build-and-deploy (push) Successful in 1m6s
视觉体系(核心是「令牌 → TDesign 桥接」) - 新增 tokens.scss:--tk-* 设计令牌(青玉 Jade 单一强调色、冷调中性色阶、 圆角/间距/字号/z-index/动效规范、按语言切换字体栈) - 新增 tdesign-theme.scss:--tk-* → --td-* 桥接(含完整冷调灰阶 1-14) → 60+ 个未改动的存量工具页零成本继承新配色与圆角 - index.scss 重写:只过渡颜色、统一 focus-visible、reduced-motion 降级 - 新增 v-reveal 滚动进入指令 布局与交互 - MainLayout 重写:毛玻璃 sticky 顶栏 + 三区布局 + 页脚(含备案号) - 新增 CommandPalette(⌘K/Ctrl+K)、LocaleSwitch(显示语言代码不用国旗)、 ThemeSwitch(三态;图标显示生效 mode、菜单勾选用户偏好 preference) - SideNav 重写:内联筛选、可折叠分组持久化、活动态指示条、需联网标记 - HomeView 重写:左对齐 Hero + 真实可用搜索 + 概览数字条 + 服务端工具专区 国际化与主题 - vue-i18n 11 + zh-CN/en-US/ja-JP/ko-KR 完整语言包(58 个工具名与描述全译) - helpers 取词带中文兜底;index.html 防闪烁内联脚本与 store 三处同步 - 主题三态 light/dark/auto,preference 与 mode 分离 服务端对接(service.xpcool.com /api/service/open/tools/*) - 新增 openTools.ts:接口元数据与调用函数共用同一份路径定义 - 改造/新建 6 页:ip-info、uuid、hash(服务端 MD5 两端口径对比)、 timestamp(服务端时钟 + 时钟偏移)、random-string、api-status - 均为「本机 / 服务端」双模式,默认本机、不自动打网络请求 - 后端未实现的 5 个接口用 BackendPending 如实标注,不伪装可用 修复 - 空分类渲染缺陷(health/study/travel 无工具却渲染空区块),分类 9 → 6 - UnoCSS panel 快捷类补内边距;IpInfoView 漏 onBeforeUnmount; SideNav 末尾 import;HomeView 错误导入 SUPPORTED_LOCALES 其他 - 新增 src/config.ts 集中站点常量,页脚展示备案号并链工信部核验 - docs/api-contract.md 按线上实际重写;PROJECT_STATE.md 全面更新 - 构建通过:vue-tsc --noEmit + vite build
117 lines
2.9 KiB
Vue
117 lines
2.9 KiB
Vue
<template>
|
||
<div class="page-wrap">
|
||
<h2 class="page-title">{{ t('tool.phone-locate.name') }}</h2>
|
||
<p class="page-desc">{{ t('tool.phone-locate.desc') }}</p>
|
||
|
||
<BackendPending />
|
||
|
||
<t-card class="tool-section" title="查询号码">
|
||
<div class="form-row">
|
||
<t-input v-model="phone" placeholder="输入 11 位手机号" maxlength="11" clearable @enter="query" />
|
||
<t-button theme="primary" :disabled="loading" @click="query">
|
||
{{ loading ? '查询中…' : '查询' }}
|
||
</t-button>
|
||
</div>
|
||
</t-card>
|
||
|
||
<ErrorAlert :message="error" @close="error = ''" />
|
||
|
||
<t-card v-if="data" class="tool-section" title="归属地信息">
|
||
<div class="phone-main">
|
||
<span class="phone-no">{{ data.phone }}</span>
|
||
<t-tag theme="primary" variant="light">{{ data.carrier }}</t-tag>
|
||
</div>
|
||
<div class="phone-grid">
|
||
<div class="pi"><span>归属地</span><b>{{ data.area }}</b></div>
|
||
<div class="pi"><span>运营商</span><b>{{ data.carrier }}</b></div>
|
||
<div class="pi"><span>邮编</span><b>{{ data.postcode || '—' }}</b></div>
|
||
</div>
|
||
</t-card>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref } from 'vue'
|
||
import ErrorAlert from '@/components/common/ErrorAlert.vue'
|
||
import { api } from '@/api/client'
|
||
import BackendPending from '@/components/common/BackendPending.vue'
|
||
import { useI18n } from 'vue-i18n'
|
||
|
||
/** 页面标题与说明改由语言包提供;接口尚未上线,用 BackendPending 说明现状 */
|
||
const { t } = useI18n()
|
||
|
||
interface PhoneData {
|
||
phone: string
|
||
area: string
|
||
carrier: string
|
||
postcode: string
|
||
updated_at: string
|
||
}
|
||
|
||
const phone = ref('')
|
||
const data = ref<PhoneData | null>(null)
|
||
const error = ref('')
|
||
const loading = ref(false)
|
||
|
||
async function query() {
|
||
error.value = ''
|
||
data.value = null
|
||
const p = phone.value.trim()
|
||
if (!/^1\d{10}$/.test(p)) {
|
||
error.value = '请输入 11 位有效手机号'
|
||
return
|
||
}
|
||
loading.value = true
|
||
const res = await api.get<PhoneData>('/api/tools/phone-locate', { phone: p })
|
||
loading.value = false
|
||
if (res.ok) {
|
||
data.value = res.data
|
||
} else {
|
||
error.value = res.message
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
.page-wrap {
|
||
max-width: 720px;
|
||
}
|
||
.form-row {
|
||
display: flex;
|
||
gap: 8px;
|
||
}
|
||
.phone-main {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
padding: 8px 0 16px;
|
||
}
|
||
.phone-no {
|
||
font-family: ui-monospace, Consolas, monospace;
|
||
font-size: 28px;
|
||
font-weight: 700;
|
||
color: var(--td-text-color-primary);
|
||
}
|
||
.phone-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||
gap: 8px;
|
||
}
|
||
.pi {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
padding: 10px;
|
||
border: 1px solid var(--td-border-level-1-color);
|
||
border-radius: 8px;
|
||
}
|
||
.pi span {
|
||
font-size: 12px;
|
||
color: var(--td-text-color-placeholder);
|
||
}
|
||
.pi b {
|
||
font-size: 15px;
|
||
color: var(--td-text-color-primary);
|
||
}
|
||
</style>
|