feat(web): 前端 SPA 化 + 工具页任务面板统一与历史批量删除

- SPA 化:22 个 MPA html 入口与 *-main.ts 合并为 index.html + vue-router(URL 无 .html 后缀),
  页面跳转全部 router-link,/new_web_source/xxx.html 旧路径归一为 /xxx
- 任务面板统一:共享 TaskCenterPanel/TaskItemCard/TaskStatCards/HistoryTaskLayer,
  16 个工具页右侧统一为统计卡 + 当前任务 + 历史任务弹层(任务ID/开始/结束/状态必展示)
- 历史记录支持单条删除 + 批量勾选删除(确认框/全选/失败提示)
- Java 7 模块(dedupe/convert/split/productrisk/shopmatch/pricetrack/deletebrand)
  history 接口补齐任务时间字段(VO+Service,复用 biz_file_task 列)
- 图片工作台/API 层(brand/permission/user)既有未提交改动一并提交
This commit is contained in:
2026-09-08 10:02:55 +08:00
parent abcfa5bef7
commit e248b6e43a
125 changed files with 7482 additions and 4304 deletions
+24 -62
View File
@@ -26,49 +26,34 @@
</div>
</div>
<span class="username-text">{{ username || '未登录' }}</span>
<a :href="logoutHref" class="logout-link">退出</a>
<router-link :to="logoutHref" class="logout-link">退出</router-link>
</div>
</header>
<div class="entrances">
<a
<router-link
v-if="canShow.amazon"
:href="amazonHref || undefined"
:to="amazonHref"
class="entrance-btn"
:class="{ disabled: !amazonHref }"
data-column-key="brand"
>亚马逊</a>
<a
v-else-if="showAmazonDisabled"
class="entrance-btn disabled"
data-column-key="brand"
>亚马逊</a>
>亚马逊</router-link>
<a
<router-link
v-if="canShow.video"
:href="videoHref || undefined"
:to="videoHref"
class="entrance-btn wb"
:class="{ disabled: !videoHref }"
data-column-key="wb"
>视频</a>
<a
v-else-if="showVideoDisabled"
class="entrance-btn wb disabled"
data-column-key="wb"
>视频</a>
>视频</router-link>
<a
<router-link
v-if="canShow.image"
:href="imageHref || undefined"
:to="imageHref"
class="entrance-btn image"
:class="{ disabled: !imageHref }"
data-column-key="image"
>图片</a>
<a
v-else-if="showImageDisabled"
class="entrance-btn image disabled"
data-column-key="image"
>图片</a>
>图片</router-link>
</div>
<div class="toast" :class="{ show: Boolean(toastText) }">{{ toastText }}</div>
@@ -77,8 +62,8 @@
<script setup lang="ts">
import { computed, onMounted, ref, watch } from 'vue'
import { requestGetJson } from '@/shared/api/http'
import { buildJavaUrl } from '@/shared/api/url'
import { restoreLoginUser } from '@/shared/auth/ensure-auth'
import { getCurrentUserAppColumnRaw, type PermissionMenuItem } from '@/shared/api/permission'
import { getPywebviewApi, isDesktopRuntime } from '@/shared/bridges/pywebview'
import { resolvePageHref } from '@/shared/page-prefix'
@@ -90,7 +75,7 @@ const updateReady = ref(false)
const updateFileUrl = ref('')
const toastText = ref('')
// 桌面端原 Flask /logout 已随瘦身下线:统一跳登录页并清本地 token(login.html?logout=1
// 桌面端原 Flask /logout 已随瘦身下线:统一跳登录页并清本地 token(/login?logout=1
const logoutHref = computed(() => `${resolvePageHref('/new_web_source/login.html')}?logout=1`)
type PermissionState = { amazon: boolean; video: boolean; image: boolean }
@@ -105,10 +90,6 @@ const imageHref = computed(() => resolvePageHref('/new_web_source/image.html'))
const canShow = computed(() =>
permissionUnavailable.value ? { amazon: true, video: true, image: true } : permissionState.value,
)
// dev 环境没有 /image 等旧路径资源时,给出“仅桌面”灰卡而不是消失
const showAmazonDisabled = computed(() => !permissionUnavailable.value && permissionState.value.amazon && !amazonHref.value)
const showVideoDisabled = computed(() => !permissionUnavailable.value && permissionState.value.video && !videoHref.value)
const showImageDisabled = computed(() => !permissionUnavailable.value && permissionState.value.image && !imageHref.value)
function getUid() {
const raw = typeof window === 'undefined' ? '' : window.localStorage.getItem('uid') || ''
@@ -116,7 +97,7 @@ function getUid() {
return Number.isFinite(numeric) && numeric > 0 ? String(numeric) : ''
}
function computePermissionState(items: Array<Record<string, unknown>>): PermissionState {
function computePermissionState(items: PermissionMenuItem[]): PermissionState {
const allowedKeys: string[] = []
const allowedColumnKeys: string[] = []
const allowedRootKeys: string[] = []
@@ -126,7 +107,8 @@ function computePermissionState(items: Array<Record<string, unknown>>): Permissi
if (key && list.indexOf(key) < 0) list.push(key)
}
;(items || []).forEach((item) => {
push(allowedColumnKeys, item.columnKey || item.column_key)
push(allowedColumnKeys, item.columnKey)
push(allowedColumnKeys, item.column_key)
const root = item.rootColumnKey || item.root_column_key
if (root) {
hasRootMetadata = true
@@ -154,19 +136,10 @@ async function loadPermissions() {
return
}
try {
const resp = await requestGetJson<{ success: boolean; data?: Array<Record<string, unknown>> }>(
buildJavaUrl(`/api/admin/permission-users/${currentUid}/column-permissions?menuType=app`),
)
if (resp?.success && Array.isArray(resp.data)) {
permissionState.value = computePermissionState(resp.data)
try {
window.localStorage.setItem(`app_column_permissions:${currentUid}`, JSON.stringify(resp.data))
} catch {
/* 忽略 */
}
} else {
permissionUnavailable.value = true
}
// 经 shared/api/permission 层拉取原始权限菜单项(含本地缓存写入;接口失败走 catch)
const items = await getCurrentUserAppColumnRaw()
permissionState.value = computePermissionState(items)
permissionUnavailable.value = false
} catch {
permissionUnavailable.value = true
}
@@ -177,21 +150,10 @@ async function loadCurrentUser() {
try {
const localName = typeof window === 'undefined' ? '' : window.localStorage.getItem('username') || ''
if (localName) username.value = localName
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
if (body.data.userId) {
try {
window.localStorage.setItem('uid', String(body.data.userId))
} catch {
/* 忽略 */
}
}
// 经 shared/auth 层恢复登录快照(页面不得直连 /newApi,见 shared-api-allowlist 测试约定)
const restored = await restoreLoginUser()
if (restored?.username) {
username.value = restored.username
}
} catch {
/* 保留本地用户名 */