feat: 二维码访问功能,统一端点管道增强,端点迁移至 Services 层

- 新增二维码生成端点,自动检测局域网 IP,前端扫一扫即可打开网站
  - 提取 IApiResponse 接口,ServiceRequestBinder 支持强类型请求 DTO 绑定
  - FileStream 端点迁移至 AppEndpoints 统一注册,管道支持 FileStreamResponse 原始文件返回
  - 文件库端点全面使用 MapGet<TService, TRequest> 泛型注册
  - 移除 Avalonia-API/Extensions 中的业务端点文件,统一由 Services 层管理
This commit is contained in:
2026-05-22 11:18:47 +08:00
parent a16c32b25e
commit d84bbb3a18
35 changed files with 888 additions and 284 deletions
+24
View File
@@ -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>
+2 -1
View File
@@ -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
}
}
+3
View File
@@ -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}`
}
+1
View File
@@ -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'),
}
+53
View File
@@ -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 {