feat(web): 桌面端前端 Web 独立部署适配——浏览器降级桥+登录态引导+页面动态修正

- 新增 web-fallback.ts:无 pywebview 时以浏览器能力降级(文件选择/上传/模板下载/配置),
  其余桌面能力(enqueue_json/vc_*/folder 选择等)保持 undefined 交由页面既有请在本地客户端中打开提示
- 新增 ensure-auth.ts:无 uid 时经 /newApi/check_login 恢复登录态,无 token/失效跳登录页;
  21 个入口 main.ts 挂载前先引导(桌面端已注入 uid 时零开销)
- pywebview.ts:getPywebviewApi() 在纯浏览器环境返回降级桥;新增 isDesktopRuntime 标志
  (首页更新面板/退出链接据此区分桌面与 Web);hasPywebview 语义修正为真实桥判定
- 登录页跳转、首页登出链接、品牌检测页 hasBridge 判定(select_brand_folder 存在性)适配 Web
This commit is contained in:
2026-09-07 16:35:19 +08:00
parent c5982dd900
commit 30b8c910e8
28 changed files with 424 additions and 32 deletions
@@ -148,7 +148,8 @@ const runMode = ref<'immediate' | 'queue'>('immediate')
const submitting = ref(false)
const tasks = ref<BrandTaskItem[]>([])
const hasBridge = computed(() => Boolean(getPywebviewApi()))
// 品牌检测由桌面客户端本地执行:以"文件夹选择"桥(Web 降级桥不提供)判定桌面运行时,浏览器下仅可预览
const hasBridge = computed(() => Boolean(getPywebviewApi()?.select_brand_folder))
const hasUid = computed(() => {
const raw = typeof window === 'undefined' ? '' : window.localStorage.getItem('uid') || ''
const numeric = Number(raw.trim())
@@ -4,6 +4,7 @@
<span class="header-title">数富AI</span>
<div class="header-right">
<button
v-if="isDesktopRuntime"
type="button"
class="header-update-btn"
title="检测更新"
@@ -25,7 +26,7 @@
</div>
</div>
<span class="username-text">{{ username || '未登录' }}</span>
<a href="/logout" class="logout-link">退出</a>
<a :href="logoutHref" class="logout-link">退出</a>
</div>
</header>
@@ -78,6 +79,7 @@
import { computed, onMounted, ref } from 'vue'
import { requestGetJson } from '@/shared/api/http'
import { buildJavaUrl } from '@/shared/api/url'
import { isDesktopRuntime } from '@/shared/bridges/pywebview'
import { resolvePageHref } from '@/shared/page-prefix'
const username = ref('')
@@ -88,6 +90,11 @@ const updateReady = ref(false)
const updateFileUrl = ref('')
const toastText = ref('')
// 桌面端走 Flask /logout(清 cookie 302 登录页);Web 独立部署 / dev 跳登录页并清本地 tokenlogin.html?logout=1
const logoutHref = computed(() =>
isDesktopRuntime.value ? '/logout' : `${resolvePageHref('/new_web_source/login.html')}?logout=1`,
)
type PermissionState = { amazon: boolean; video: boolean; image: boolean }
const permissionState = ref<PermissionState>({ amazon: false, video: false, image: false })
const permissionUnavailable = ref(false)
@@ -95,7 +102,7 @@ const permissionReady = ref(false)
const amazonHref = computed(() => resolvePageHref('/new_web_source/amazon-console.html'))
const videoHref = computed(() => resolvePageHref('/new_web_source/image-video.html'))
const imageHref = computed(() => resolvePageHref('/image'))
const imageHref = computed(() => resolvePageHref('/new_web_source/image.html'))
const canShow = computed(() =>
permissionUnavailable.value ? { amazon: true, video: true, image: true } : permissionState.value,
@@ -172,7 +179,11 @@ async function loadCurrentUser() {
try {
const localName = typeof window === 'undefined' ? '' : window.localStorage.getItem('username') || ''
if (localName) username.value = localName
const resp = await window.fetch('/newApi/check_login', { credentials: 'include' })
const token = typeof window === 'undefined' ? '' : window.localStorage.getItem('aiimage_auth_token') || ''
const resp = await window.fetch('/newApi/check_login', {
credentials: 'include',
headers: token ? { Authorization: `Bearer ${token}` } : {},
})
const body = await resp.json().catch(() => ({}))
if (body?.success && body.data) {
if (body.data.username) username.value = body.data.username
@@ -257,7 +268,10 @@ async function doUpdate() {
onMounted(() => {
void loadCurrentUser()
void loadPermissions()
void fetchVersion().catch(() => undefined)
// 版本检测/更新由桌面客户端 Flask 提供,Web 版不发起
if (isDesktopRuntime.value) {
void fetchVersion().catch(() => undefined)
}
})
</script>
@@ -2,7 +2,7 @@
<div v-if="currentView === 'menu'" class="image-video-page">
<header class="page-header">
<span class="header-title">数富AI</span>
<a class="back-link" href="/home">返回首页</a>
<a class="back-link" :href="goHome()">返回首页</a>
</header>
<main class="entrances" :class="{ 'is-loading': permissionsLoading }" aria-label="视频与图片入口">
@@ -21,9 +21,12 @@
<button v-if="hasMenuPermission('mix-video')" type="button" class="entrance-btn" @click="showSoon('混剪')">
混剪
</button>
<a v-if="hasMenuPermission('image')" class="entrance-btn" href="/image">
<a v-if="hasMenuPermission('image') && isDesktop" class="entrance-btn" href="/new_web_source/image.html">
图片
</a>
<button v-else-if="hasMenuPermission('image')" type="button" class="entrance-btn" @click="showDesktopOnly('图片工作台')">
图片
</button>
</main>
<div class="toast" :class="{ show: Boolean(statusText), error: statusType === 'error' }">
@@ -75,10 +78,22 @@ import BrandTopBar from '@/pages/brand/components/BrandTopBar.vue'
import DeliveryVideoWorkspace from '@/pages/image-video/components/DeliveryVideoWorkspace.vue'
import { getCurrentUserAppColumnKeys } from '@/shared/api/permission'
import { getPywebviewApi } from '@/shared/bridges/pywebview'
import { isDesktopClientPage } from '@/shared/page-prefix'
import DownloadProgressPanel from '@/shared/components/DownloadProgressPanel.vue'
type ViewMode = 'menu' | 'delivery'
const isDesktop = isDesktopClientPage()
function goHome() {
// 桌面端首页是 Flask /home(重定向到 Vue home);dev 下是 MPA 的 /home.html
return isDesktop ? '/home' : '/home.html'
}
function showDesktopOnly(name: string) {
showStatus(`${name}为桌面客户端功能,请在本地客户端中打开`, 'error')
}
const currentView = ref<ViewMode>('menu')
const allowedColumnKeys = ref(new Set<string>())
const permissionsLoading = ref(true)
@@ -184,7 +199,7 @@ onBeforeUnmount(() => {
async function launchDesktop() {
const api = getPywebviewApi()
if (!api?.launch_yaoayanui) {
showStatus('当前环境不支持拉起桌面端', 'error')
showStatus('数字人为桌面客户端程序:请在本机客户端中打开后再点击', 'error')
return
}
@@ -246,7 +261,7 @@ async function launchDesktop() {
font-family: "Microsoft YaHei", "PingFang SC", "Hiragino Sans GB", sans-serif;
background:
linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5)),
url('/static/bg.jpg') center/cover no-repeat,
url('/bg.jpg') center/cover no-repeat,
#eef3f7;
}
@@ -53,6 +53,7 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { resolvePageHref } from '@/shared/page-prefix'
const AUTH_TOKEN_KEY = 'aiimage_auth_token'
const DEVICE_ID_KEY = 'aiimage_device_id'
@@ -185,7 +186,8 @@ async function submitLogin() {
/* cookie 同步失败不阻塞跳转(页面仍可读 localStorage */
}
clearAppPermissionCaches()
window.location.href = isDesktopClient() ? '/home' : '/home.html'
// 桌面端走 Flask /homeWeb 独立部署(路径含 /new_web_source)与 dev 走 resolvePageHref 归一
window.location.href = isDesktopClient() ? '/home' : resolvePageHref('/new_web_source/home.html')
} catch {
errorMessage.value = '网络异常,请稍后重试'
} finally {