feat(frontend-vue): 亚马逊运营工具台与新功能页接入——新版工具台(amazon-console+tool-catalog)、品牌检测/ASIN变体采集/查询ASIN 三个工具页 Vue 化并对接既有后端与桌面桥接;桌面首页/登录页 Vue 化(沿用旧视觉);补充 vc_*/get_device_id/save_file_from_url 桥类型、dev 代理;品牌默认勾选排名+FBM;修正 query-asin 标题
含未提交新版改造累积:Brand*Tab 套入工具台壳(AmazonToolPageShell/AmazonTopBar)与目录/权限联动。
This commit is contained in:
@@ -0,0 +1,471 @@
|
||||
<template>
|
||||
<div class="home-root" :class="{ 'nav-permissions-loading': !permissionReady }">
|
||||
<header class="header">
|
||||
<span class="header-title">数富AI</span>
|
||||
<div class="header-right">
|
||||
<button
|
||||
type="button"
|
||||
class="header-update-btn"
|
||||
title="检测更新"
|
||||
@click="toggleUpdatePanel"
|
||||
>↻</button>
|
||||
<div class="header-update-panel" v-show="updatePanel">
|
||||
<div class="header-update-title"><span>↻</span> 软件更新</div>
|
||||
<div class="update-block">
|
||||
<span class="update-version-text">当前版本: {{ versionText }}</span>
|
||||
<button type="button" class="btn-check-update" @click="checkUpdate">
|
||||
<span>↻</span> 检测
|
||||
</button>
|
||||
</div>
|
||||
<div class="update-hint">{{ updateHint }}</div>
|
||||
<div style="margin-top: 4px;">
|
||||
<button type="button" class="btn-download-update" v-if="updateReady" @click="doUpdate">
|
||||
立即更新
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<span class="username-text">{{ username || '未登录' }}</span>
|
||||
<a href="/logout" class="logout-link">退出</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="entrances">
|
||||
<a
|
||||
v-if="canShow.amazon"
|
||||
:href="amazonHref || undefined"
|
||||
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>
|
||||
|
||||
<a
|
||||
v-if="canShow.video"
|
||||
:href="videoHref || undefined"
|
||||
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>
|
||||
|
||||
<a
|
||||
v-if="canShow.image"
|
||||
:href="imageHref || undefined"
|
||||
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>
|
||||
</div>
|
||||
|
||||
<div class="toast" :class="{ show: Boolean(toastText) }">{{ toastText }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { requestGetJson } from '@/shared/api/http'
|
||||
import { buildJavaUrl } from '@/shared/api/url'
|
||||
import { resolvePageHref } from '@/shared/page-prefix'
|
||||
|
||||
const username = ref('')
|
||||
const versionText = ref('—')
|
||||
const updatePanel = ref(false)
|
||||
const updateHint = ref('')
|
||||
const updateReady = ref(false)
|
||||
const updateFileUrl = ref('')
|
||||
const toastText = ref('')
|
||||
|
||||
type PermissionState = { amazon: boolean; video: boolean; image: boolean }
|
||||
const permissionState = ref<PermissionState>({ amazon: false, video: false, image: false })
|
||||
const permissionUnavailable = ref(false)
|
||||
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 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') || ''
|
||||
const numeric = Number(raw.trim())
|
||||
return Number.isFinite(numeric) && numeric > 0 ? String(numeric) : ''
|
||||
}
|
||||
|
||||
function computePermissionState(items: Array<Record<string, unknown>>): PermissionState {
|
||||
const allowedKeys: string[] = []
|
||||
const allowedColumnKeys: string[] = []
|
||||
const allowedRootKeys: string[] = []
|
||||
let hasRootMetadata = false
|
||||
const push = (list: string[], value: unknown) => {
|
||||
const key = String(value || '').trim().toLowerCase()
|
||||
if (key && list.indexOf(key) < 0) list.push(key)
|
||||
}
|
||||
;(items || []).forEach((item) => {
|
||||
push(allowedColumnKeys, item.columnKey || item.column_key)
|
||||
const root = item.rootColumnKey || item.root_column_key
|
||||
if (root) {
|
||||
hasRootMetadata = true
|
||||
push(allowedRootKeys, root)
|
||||
}
|
||||
;[item.columnKey, item.column_key, item.routePath, item.route_path].forEach((v) => push(allowedKeys, v))
|
||||
})
|
||||
const videoVisible = allowedKeys.indexOf('wb') >= 0
|
||||
const imageVisible = allowedKeys.indexOf('image') >= 0
|
||||
let amazonVisible =
|
||||
allowedRootKeys.indexOf('brand_front_tools') >= 0 ||
|
||||
allowedRootKeys.indexOf('brand_operation_tools') >= 0 ||
|
||||
allowedRootKeys.indexOf('brand_logistics_tools') >= 0
|
||||
if (!hasRootMetadata) {
|
||||
amazonVisible = allowedColumnKeys.some((key) => key !== 'wb' && key !== 'image')
|
||||
}
|
||||
return { amazon: amazonVisible, video: videoVisible, image: imageVisible }
|
||||
}
|
||||
|
||||
async function loadPermissions() {
|
||||
const currentUid = getUid()
|
||||
if (!currentUid) {
|
||||
permissionUnavailable.value = true
|
||||
permissionReady.value = true
|
||||
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
|
||||
}
|
||||
} catch {
|
||||
permissionUnavailable.value = true
|
||||
}
|
||||
permissionReady.value = true
|
||||
}
|
||||
|
||||
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 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 {
|
||||
/* 忽略 */
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
/* 保留本地用户名 */
|
||||
}
|
||||
}
|
||||
|
||||
function toggleUpdatePanel() {
|
||||
updatePanel.value = !updatePanel.value
|
||||
}
|
||||
|
||||
function showTip(message: string) {
|
||||
toastText.value = message
|
||||
window.setTimeout(() => {
|
||||
if (toastText.value === message) toastText.value = ''
|
||||
}, 2600)
|
||||
}
|
||||
|
||||
async function fetchVersion() {
|
||||
try {
|
||||
const resp = await window.fetch('/api/version', { credentials: 'same-origin' })
|
||||
const data = await resp.json().catch(() => ({}))
|
||||
const current = String((data && data.version) || '').replace(/^v/i, '')
|
||||
if (current) versionText.value = current || '—'
|
||||
return data || {}
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
async function checkUpdate() {
|
||||
updateHint.value = '正在检测更新...'
|
||||
updateReady.value = false
|
||||
try {
|
||||
const data = await fetchVersion()
|
||||
if (data.has_update && data.latest_version) {
|
||||
updateHint.value =
|
||||
`发现新版本 v${String(data.latest_version).replace(/^v/i, '')}` +
|
||||
(data.desc ? `:${data.desc}` : '')
|
||||
if (data.file_url) {
|
||||
updateReady.value = true
|
||||
updateFileUrl.value = data.file_url
|
||||
} else {
|
||||
updateHint.value = '暂无下载地址,请关注官方渠道。'
|
||||
}
|
||||
} else {
|
||||
updateHint.value = '已是最新版本'
|
||||
}
|
||||
} catch {
|
||||
updateHint.value = '检测更新失败'
|
||||
}
|
||||
}
|
||||
|
||||
async function doUpdate() {
|
||||
if (!updateFileUrl.value) return
|
||||
if (!window.confirm('有更新,是否现在更新?\n更新将下载安装包并重启程序。')) return
|
||||
updateHint.value = '正在下载并准备更新,程序将自动退出...'
|
||||
updateReady.value = false
|
||||
try {
|
||||
const resp = await window.fetch('/api/update/do', {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ file_url: updateFileUrl.value }),
|
||||
})
|
||||
const result = await resp.json().catch(() => ({}))
|
||||
updateHint.value = result.success ? '更新已启动,程序即将退出...' : (result.error || '更新启动失败')
|
||||
} catch {
|
||||
updateHint.value = '请求更新失败,请重试'
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
void loadCurrentUser()
|
||||
void loadPermissions()
|
||||
void fetchVersion().catch(() => undefined)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
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;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
.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 {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.header-right a,
|
||||
.header-right span {
|
||||
font-size: 14px;
|
||||
color: #555;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.header-right a:hover {
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
.logout-link {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.header-update-btn {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
background: rgba(102, 126, 234, 0.12);
|
||||
color: #4c5bd4;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 15px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.header-update-btn:hover {
|
||||
background: rgba(102, 126, 234, 0.2);
|
||||
}
|
||||
|
||||
.header-update-panel {
|
||||
position: absolute;
|
||||
top: 44px;
|
||||
right: 0;
|
||||
width: 280px;
|
||||
padding: 14px 16px;
|
||||
background: rgba(255, 255, 255, 0.98);
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
|
||||
border: 1px solid rgba(0, 0, 0, 0.04);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.header-update-title {
|
||||
font-size: 13px;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.update-block {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.update-version-text {
|
||||
font-size: 13px;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.btn-check-update {
|
||||
padding: 6px 12px;
|
||||
font-size: 13px;
|
||||
background: #667eea;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-check-update:hover {
|
||||
background: #5a6fd6;
|
||||
}
|
||||
|
||||
.btn-download-update {
|
||||
margin-top: 8px;
|
||||
padding: 6px 12px;
|
||||
font-size: 13px;
|
||||
background: #28a745;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-download-update:hover {
|
||||
background: #218838;
|
||||
}
|
||||
|
||||
.update-hint {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
margin-top: 8px;
|
||||
min-height: 18px;
|
||||
}
|
||||
|
||||
.home-root.nav-permissions-loading .entrance-btn {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.entrances {
|
||||
display: flex;
|
||||
gap: 32px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.entrance-btn {
|
||||
width: 160px;
|
||||
height: 100px;
|
||||
background: #c5c1c1;
|
||||
border: 4px solid #b8d4e3;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.entrance-btn:hover {
|
||||
background: #f8fbfd;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.entrance-btn.disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.9;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.toast {
|
||||
position: fixed;
|
||||
bottom: 40px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: rgba(0, 0, 0, 0.75);
|
||||
color: #fff;
|
||||
padding: 12px 24px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
pointer-events: none;
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.toast.show {
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user