feat: 二维码访问功能,统一端点管道增强,端点迁移至 Services 层
- 新增二维码生成端点,自动检测局域网 IP,前端扫一扫即可打开网站 - 提取 IApiResponse 接口,ServiceRequestBinder 支持强类型请求 DTO 绑定 - FileStream 端点迁移至 AppEndpoints 统一注册,管道支持 FileStreamResponse 原始文件返回 - 文件库端点全面使用 MapGet<TService, TRequest> 泛型注册 - 移除 Avalonia-API/Extensions 中的业务端点文件,统一由 Services 层管理
This commit is contained in:
@@ -21,6 +21,8 @@ const total = ref(0)
|
||||
const loading = ref(false)
|
||||
const scanningId = ref<number | null>(null)
|
||||
const errorMessage = ref('')
|
||||
const showQrCode = ref(false)
|
||||
const qrCodeData = ref<{ url: string; qrCodeBase64: string } | null>(null)
|
||||
|
||||
const totalPages = computed(() => Math.max(1, Math.ceil(total.value / pageSize)))
|
||||
const availableRoots = computed(() => roots.value.filter((root) => root.isAvailable))
|
||||
@@ -176,6 +178,15 @@ async function refreshAll() {
|
||||
await Promise.all([loadRoots(), loadFiles()])
|
||||
}
|
||||
|
||||
async function loadQrCode() {
|
||||
try {
|
||||
qrCodeData.value = await api.qrCode()
|
||||
showQrCode.value = true
|
||||
} catch (error) {
|
||||
setError(error)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
@@ -320,6 +331,7 @@ onMounted(async () => {
|
||||
</div>
|
||||
<div class="mobile-header-actions">
|
||||
<button v-if="!isBrowsingRoots" type="button" class="back-button" @click="backToRoots">返回</button>
|
||||
<button type="button" class="qr-button" title="生成二维码" @click="loadQrCode">二维码</button>
|
||||
<a href="/admin" class="admin-link">管理</a>
|
||||
</div>
|
||||
</header>
|
||||
@@ -431,5 +443,17 @@ onMounted(async () => {
|
||||
<span>{{ page }} / {{ totalPages }}</span>
|
||||
<button type="button" :disabled="page >= totalPages" @click="changePage(page + 1)">下一页</button>
|
||||
</nav>
|
||||
|
||||
<Teleport to="body">
|
||||
<div v-if="showQrCode" class="qr-overlay" @click.self="showQrCode = false">
|
||||
<div class="qr-modal">
|
||||
<h2>扫码访问</h2>
|
||||
<img v-if="qrCodeData" :src="qrCodeData.qrCodeBase64" alt="QR Code" class="qr-image" />
|
||||
<p v-else class="qr-hint">加载中...</p>
|
||||
<p class="qr-hint">使用手机扫描二维码,即可在局域网中打开此网站</p>
|
||||
<button type="button" class="primary-button qr-close" @click="showQrCode = false">关闭</button>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
// 扩展 Window 接口,声明 C# 桥接注入的全局属性
|
||||
declare global {
|
||||
interface Window {
|
||||
interface Window {
|
||||
/** 由 C# BridgeScript 注入,标记当前运行在 WebView2 环境中 */
|
||||
isWebView2?: boolean
|
||||
/** 由 WebView2 宿主注入,用于向 C# 发送消息 */
|
||||
invokeCSharpAction?: (message: string) => void
|
||||
__pcMediaOrigin?: string
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,9 @@ export const apiOrigin = (): string => HTTP_ORIGIN
|
||||
export const apiUrl = (path: string): string => {
|
||||
if (/^https?:\/\//i.test(path)) return path
|
||||
const normalized = path.startsWith('/') ? path : `/${path}`
|
||||
if (isWebView2() && window.__pcMediaOrigin) {
|
||||
return `${window.__pcMediaOrigin}${normalized}`
|
||||
}
|
||||
return `${isWebView2() ? '' : HTTP_ORIGIN}${normalized}`
|
||||
}
|
||||
|
||||
|
||||
@@ -90,4 +90,5 @@ export const api = {
|
||||
getTextPreview: (id: number) =>
|
||||
request<TextPreviewDto>(`files/text${qs({ id })}`),
|
||||
mediaUrl: (path: string) => apiUrl(path),
|
||||
qrCode: () => request<{ url: string; qrCodeBase64: string }>('qrcode'),
|
||||
}
|
||||
|
||||
@@ -595,6 +595,59 @@ a {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.qr-button {
|
||||
border-radius: 999px;
|
||||
padding: 8px 14px;
|
||||
color: var(--accent-strong);
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.qr-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(16, 24, 40, 0.55);
|
||||
backdrop-filter: blur(6px);
|
||||
}
|
||||
|
||||
.qr-modal {
|
||||
display: grid;
|
||||
justify-items: center;
|
||||
gap: 16px;
|
||||
border-radius: 18px;
|
||||
padding: 28px 24px 22px;
|
||||
background: #fff;
|
||||
box-shadow: 0 24px 60px rgba(16, 24, 40, 0.22);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.qr-modal h2 {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.qr-image {
|
||||
display: block;
|
||||
width: 240px;
|
||||
height: 240px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.qr-hint {
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.qr-close {
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
.admin-layout,
|
||||
.admin-browser {
|
||||
|
||||
Reference in New Issue
Block a user