dd45ffe34c
每项删除前均做过全仓库引用核对(含 tests/、两个前端、Python 侧):
- Java 7 个 DTO:BrandTaskRunRequest、LegacyBrandTaskCreateVo、Ziniao{SwitchShopRequest,CurrentShopVo,LoginUrlVo,SwitchShopVo}、
AppearancePatentParsedGroupManifestDto —— 全仓库仅命中声明行本身
- frontend-vue 整棵 src/pages/image/(16 文件):9a487ae9 起首页一级入口已删("图片已并入视频页"),
该子树完全自闭环;同步移除 /image 路由与 ImageVideoPage 的图片入口
- frontend-vue 两个孤儿组件 AiModuleSwitch/AiWorkflowShell:无引用,且位于 src/shared/components
而非 unplugin-vue-components 默认扫描的 src/components,不存在自动注册
- backend/tool/devices.py + .device_id:app_client/tool/devices.py 的冗余副本(活的是 app_client 那份),
app.py 只注册 version_bp,无任何引用
- admin-frontend-vue 的一次性 Playwright 调试脚本 rescan.tmp.mjs / diag-role-price.mjs
- backend-java/scripts/*_report.json ×4:可由 n1_scan.py 等重跑的审计快照
- frontend-vue/DESIGN.md:Last refreshed 2026-06-09,仍是 MPA 时代内容
- backend/requirement.txt:pip freeze 63 条裁剪为实际 import 的 5 条;run.sh 硬依赖该文件存在,故保留文件本身
验证:mvn compile 通过、vue-tsc --noEmit 通过、残留引用 grep 零命中。
注:仓库既有的 4 个失败测试类(FlywayMigration*DocTest、MigrationInventoryTest、HttpClientTimeoutEffectiveTest)
经 stash 对照实验确认与本次改动无关(相同 Failures/Errors 计数)。
493 lines
12 KiB
Vue
493 lines
12 KiB
Vue
<template>
|
|
<div v-if="currentView === 'menu'" class="image-video-page">
|
|
<header class="page-header">
|
|
<span class="header-title">数富AI</span>
|
|
<router-link class="back-link" :to="goHome()">返回首页</router-link>
|
|
</header>
|
|
|
|
<main class="entrances" :class="{ 'is-loading': permissionsLoading }" aria-label="视频与图片入口">
|
|
<button
|
|
v-if="hasMenuPermission('digital-human')"
|
|
type="button"
|
|
class="entrance-btn"
|
|
:disabled="launching"
|
|
@click="launchDesktop"
|
|
>
|
|
数字人
|
|
</button>
|
|
<button v-if="hasMenuPermission('delivery-video')" type="button" class="entrance-btn" @click="openDeliveryWorkspace">
|
|
带货视频
|
|
</button>
|
|
<button v-if="hasMenuPermission('mix-video')" type="button" class="entrance-btn" @click="showSoon('混剪')">
|
|
混剪
|
|
</button>
|
|
</main>
|
|
|
|
<div class="toast" :class="{ show: Boolean(statusText), error: statusType === 'error' }">
|
|
{{ statusText }}
|
|
</div>
|
|
|
|
<section v-if="launching || launchProgress.stage" class="launch-progress" :class="{ error: launchProgress.stage === 'failed' }">
|
|
<div class="launch-progress__head">
|
|
<span>{{ launchProgress.message || '正在准备数字人程序...' }}</span>
|
|
<strong>{{ launchProgress.percent }}%</strong>
|
|
</div>
|
|
<div class="launch-progress__track">
|
|
<div class="launch-progress__bar" :style="{ width: `${launchProgress.percent}%` }"></div>
|
|
</div>
|
|
<div v-if="launchProgress.total > 0" class="launch-progress__meta">
|
|
{{ formatBytes(launchProgress.downloaded) }} / {{ formatBytes(launchProgress.total) }}
|
|
</div>
|
|
</section>
|
|
</div>
|
|
|
|
<PageShell v-else theme="dark">
|
|
<div class="delivery-page">
|
|
<BrandTopBar
|
|
active="image-video"
|
|
:show-nav="false"
|
|
:show-home-link="false"
|
|
:show-actions="false"
|
|
>
|
|
<template #logoExtra>
|
|
<button type="button" class="delivery-page__top-return" @click="goBackToMenu">
|
|
返回二级菜单
|
|
</button>
|
|
</template>
|
|
</BrandTopBar>
|
|
|
|
<main class="delivery-page__body">
|
|
<DeliveryVideoWorkspace />
|
|
</main>
|
|
<DownloadProgressPanel />
|
|
</div>
|
|
</PageShell>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
|
import { useRoute, useRouter } from 'vue-router'
|
|
|
|
import PageShell from '@/components/layout/PageShell.vue'
|
|
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 DownloadProgressPanel from '@/shared/components/DownloadProgressPanel.vue'
|
|
|
|
type ViewMode = 'menu' | 'delivery'
|
|
|
|
const route = useRoute()
|
|
const router = useRouter()
|
|
|
|
function goHome() {
|
|
// 桌面端与 Web 统一为 SPA 首页路径
|
|
return '/home'
|
|
}
|
|
|
|
/** 视图由路由 query 驱动:?view=delivery 为带货视频工作台,否则二级菜单页 */
|
|
const currentView = ref<ViewMode>(route.query.view === 'delivery' ? 'delivery' : 'menu')
|
|
const allowedColumnKeys = ref(new Set<string>())
|
|
const permissionsLoading = ref(true)
|
|
const launching = ref(false)
|
|
const statusText = ref('')
|
|
const statusType = ref<'normal' | 'error'>('normal')
|
|
const launchProgress = ref({
|
|
stage: '',
|
|
message: '',
|
|
percent: 0,
|
|
downloaded: 0,
|
|
total: 0,
|
|
})
|
|
let statusTimer: number | undefined
|
|
let launchProgressTimer: number | undefined
|
|
|
|
function showStatus(message: string, type: 'normal' | 'error' = 'normal') {
|
|
statusText.value = message
|
|
statusType.value = type
|
|
if (statusTimer) {
|
|
window.clearTimeout(statusTimer)
|
|
}
|
|
statusTimer = window.setTimeout(() => {
|
|
statusText.value = ''
|
|
}, 2200)
|
|
}
|
|
|
|
function showSoon(name: string) {
|
|
showStatus(`${name}暂未开通,敬请期待`)
|
|
}
|
|
|
|
function hasMenuPermission(columnKey: string) {
|
|
return allowedColumnKeys.value.has('wb') || allowedColumnKeys.value.has(columnKey)
|
|
}
|
|
|
|
onMounted(async () => {
|
|
try {
|
|
allowedColumnKeys.value = new Set(await getCurrentUserAppColumnKeys())
|
|
} catch (_error) {
|
|
showStatus('菜单权限加载失败', 'error')
|
|
} finally {
|
|
permissionsLoading.value = false
|
|
}
|
|
})
|
|
|
|
watch(
|
|
() => route.query.view,
|
|
(view) => {
|
|
currentView.value = view === 'delivery' ? 'delivery' : 'menu'
|
|
},
|
|
)
|
|
|
|
function openDeliveryWorkspace() {
|
|
router.push({ path: '/image-video', query: { view: 'delivery' } })
|
|
}
|
|
|
|
function goBackToMenu() {
|
|
router.push({ path: '/image-video' })
|
|
}
|
|
|
|
|
|
function formatBytes(value: number) {
|
|
const size = Number(value || 0)
|
|
if (!Number.isFinite(size) || size <= 0) return '未知大小'
|
|
if (size < 1024) return `${size} B`
|
|
if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)} KB`
|
|
if (size < 1024 * 1024 * 1024) return `${(size / 1024 / 1024).toFixed(1)} MB`
|
|
return `${(size / 1024 / 1024 / 1024).toFixed(2)} GB`
|
|
}
|
|
|
|
function resetLaunchProgress() {
|
|
launchProgress.value = {
|
|
stage: '',
|
|
message: '',
|
|
percent: 0,
|
|
downloaded: 0,
|
|
total: 0,
|
|
}
|
|
}
|
|
|
|
function handleLaunchProgress(event: Event) {
|
|
const detail = (event as CustomEvent<{
|
|
stage?: string
|
|
message?: string
|
|
percent?: number
|
|
downloaded?: number
|
|
total?: number
|
|
}>).detail
|
|
if (!detail) return
|
|
launchProgress.value = {
|
|
stage: detail.stage || '',
|
|
message: detail.message || '',
|
|
percent: Math.max(0, Math.min(100, Math.round(Number(detail.percent || 0)))),
|
|
downloaded: Number(detail.downloaded || 0),
|
|
total: Number(detail.total || 0),
|
|
}
|
|
if (launchProgressTimer) {
|
|
window.clearTimeout(launchProgressTimer)
|
|
}
|
|
if (detail.stage === 'success') {
|
|
launchProgressTimer = window.setTimeout(resetLaunchProgress, 1800)
|
|
}
|
|
}
|
|
|
|
if (typeof window !== 'undefined') {
|
|
window.addEventListener('pywebview-yaoayanui-progress', handleLaunchProgress)
|
|
}
|
|
|
|
onBeforeUnmount(() => {
|
|
if (statusTimer) window.clearTimeout(statusTimer)
|
|
if (launchProgressTimer) window.clearTimeout(launchProgressTimer)
|
|
window.removeEventListener('pywebview-yaoayanui-progress', handleLaunchProgress)
|
|
})
|
|
|
|
async function launchDesktop() {
|
|
const api = getPywebviewApi()
|
|
if (!api?.launch_yaoayanui) {
|
|
showStatus('数字人为桌面客户端程序:请在本机客户端中打开后再点击', 'error')
|
|
return
|
|
}
|
|
|
|
launching.value = true
|
|
launchProgress.value = {
|
|
stage: 'checking',
|
|
message: '正在检查本地数字人程序...',
|
|
percent: 0,
|
|
downloaded: 0,
|
|
total: 0,
|
|
}
|
|
try {
|
|
const result = await api.launch_yaoayanui()
|
|
if (result?.success) {
|
|
showStatus('已发送启动请求')
|
|
if (!launchProgress.value.stage || launchProgress.value.stage === 'checking') {
|
|
launchProgress.value = {
|
|
stage: 'success',
|
|
message: '数字人程序已启动',
|
|
percent: 100,
|
|
downloaded: 0,
|
|
total: 0,
|
|
}
|
|
launchProgressTimer = window.setTimeout(resetLaunchProgress, 1800)
|
|
}
|
|
return
|
|
}
|
|
launchProgress.value = {
|
|
...launchProgress.value,
|
|
stage: 'failed',
|
|
message: result?.error || '启动失败',
|
|
percent: 0,
|
|
}
|
|
showStatus(result?.error || '启动失败', 'error')
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : String(error)
|
|
launchProgress.value = {
|
|
...launchProgress.value,
|
|
stage: 'failed',
|
|
message,
|
|
percent: 0,
|
|
}
|
|
showStatus(message, 'error')
|
|
} finally {
|
|
launching.value = false
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.image-video-page {
|
|
min-height: 100vh;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 40px;
|
|
color: #333;
|
|
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)),
|
|
image-set(url('/bg.webp') 1x, url('/bg.jpg') 1x) center/cover no-repeat,
|
|
#eef3f7;
|
|
}
|
|
|
|
.page-header {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
height: 56px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 0 24px;
|
|
background: rgba(255, 255, 255, 0.5);
|
|
}
|
|
|
|
.header-title {
|
|
color: #333;
|
|
font-size: 18px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.back-link {
|
|
color: #555;
|
|
font-size: 14px;
|
|
text-decoration: none;
|
|
}
|
|
|
|
.back-link:hover {
|
|
color: #667eea;
|
|
}
|
|
|
|
.entrances {
|
|
display: flex;
|
|
gap: 32px;
|
|
flex-wrap: wrap;
|
|
justify-content: center;
|
|
}
|
|
|
|
.entrances.is-loading {
|
|
visibility: hidden;
|
|
}
|
|
|
|
.entrance-btn {
|
|
width: 160px;
|
|
height: 100px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: #333;
|
|
background: #c5c1c1;
|
|
border: 4px solid #b8d4e3;
|
|
border-radius: 12px;
|
|
cursor: pointer;
|
|
font: inherit;
|
|
font-size: 18px;
|
|
font-weight: 600;
|
|
text-decoration: none;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.entrance-btn:hover:not(:disabled) {
|
|
background: #f8fbfd;
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
|
}
|
|
|
|
.entrance-btn:disabled {
|
|
cursor: wait;
|
|
opacity: 0.72;
|
|
}
|
|
|
|
.toast {
|
|
position: fixed;
|
|
bottom: 40px;
|
|
left: 50%;
|
|
padding: 12px 24px;
|
|
max-width: min(420px, calc(100vw - 48px));
|
|
color: #fff;
|
|
background: rgba(0, 0, 0, 0.75);
|
|
border-radius: 8px;
|
|
font-size: 14px;
|
|
text-align: center;
|
|
opacity: 0;
|
|
pointer-events: none;
|
|
transform: translateX(-50%);
|
|
transition: opacity 0.3s;
|
|
}
|
|
|
|
.toast.show {
|
|
opacity: 1;
|
|
}
|
|
|
|
.toast.error {
|
|
background: rgba(176, 42, 55, 0.92);
|
|
}
|
|
|
|
.launch-progress {
|
|
position: fixed;
|
|
left: 50%;
|
|
bottom: 96px;
|
|
width: min(520px, calc(100vw - 48px));
|
|
padding: 14px 16px;
|
|
border: 1px solid rgba(102, 126, 234, 0.28);
|
|
border-radius: 8px;
|
|
background: rgba(255, 255, 255, 0.92);
|
|
box-shadow: 0 10px 28px rgba(20, 32, 55, 0.16);
|
|
transform: translateX(-50%);
|
|
}
|
|
|
|
.launch-progress.error {
|
|
border-color: rgba(176, 42, 55, 0.34);
|
|
}
|
|
|
|
.launch-progress__head {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 12px;
|
|
color: #2d3748;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.launch-progress__head span {
|
|
min-width: 0;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.launch-progress__head strong {
|
|
color: #4456c9;
|
|
font-size: 13px;
|
|
}
|
|
|
|
.launch-progress.error .launch-progress__head strong {
|
|
color: #b02a37;
|
|
}
|
|
|
|
.launch-progress__track {
|
|
height: 8px;
|
|
margin-top: 10px;
|
|
overflow: hidden;
|
|
border-radius: 999px;
|
|
background: #e6ebf5;
|
|
}
|
|
|
|
.launch-progress__bar {
|
|
height: 100%;
|
|
border-radius: inherit;
|
|
background: linear-gradient(90deg, #667eea, #52c2ff);
|
|
transition: width 0.2s ease;
|
|
}
|
|
|
|
.launch-progress.error .launch-progress__bar {
|
|
background: #b02a37;
|
|
}
|
|
|
|
.launch-progress__meta {
|
|
margin-top: 7px;
|
|
color: #68758a;
|
|
font-size: 12px;
|
|
text-align: right;
|
|
}
|
|
|
|
.delivery-page {
|
|
display: flex;
|
|
flex-direction: column;
|
|
height: 100%;
|
|
min-height: 0;
|
|
background: #0f0f0f;
|
|
}
|
|
|
|
:global(html),
|
|
:global(body),
|
|
:global(#app) {
|
|
min-height: 100%;
|
|
background: #0f0f0f;
|
|
}
|
|
|
|
.delivery-page__body {
|
|
flex: 1 1 auto;
|
|
min-height: 0;
|
|
overflow: auto;
|
|
padding: 12px 20px 24px;
|
|
background: #0f0f0f;
|
|
}
|
|
|
|
.delivery-page__top-return {
|
|
min-height: 34px;
|
|
padding: 0 12px;
|
|
border: 1px solid #404040;
|
|
border-radius: 8px;
|
|
background: #181818;
|
|
color: #d7d7d7;
|
|
font: inherit;
|
|
font-size: 13px;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.delivery-page__top-return:hover {
|
|
border-color: #b58e55;
|
|
color: #fff;
|
|
}
|
|
|
|
@media (max-width: 640px) {
|
|
.image-video-page {
|
|
padding: 88px 24px 40px;
|
|
}
|
|
|
|
.entrances {
|
|
width: 100%;
|
|
gap: 18px;
|
|
}
|
|
|
|
.entrance-btn {
|
|
width: 100%;
|
|
max-width: 220px;
|
|
}
|
|
|
|
.delivery-page__body {
|
|
padding: 16px;
|
|
}
|
|
}
|
|
</style>
|