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,366 @@
|
||||
<template>
|
||||
<div class="legacy-login-root">
|
||||
<header class="header">
|
||||
<span class="header-title">数富AI</span>
|
||||
</header>
|
||||
|
||||
<div class="login-box">
|
||||
<h1 class="login-title">登录</h1>
|
||||
<p v-if="errorMessage" class="error-msg">{{ errorMessage }}</p>
|
||||
<form id="loginForm" @submit.prevent="submitLogin">
|
||||
<div class="form-group">
|
||||
<label for="username">用户名</label>
|
||||
<input id="username" v-model="username" type="text" placeholder="请输入用户名" required autofocus />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password">密码</label>
|
||||
<div class="password-field">
|
||||
<input
|
||||
id="password"
|
||||
v-model="password"
|
||||
:type="passwordVisible ? 'text' : 'password'"
|
||||
placeholder="请输入密码"
|
||||
required
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="password-toggle"
|
||||
:class="{ 'is-visible': passwordVisible }"
|
||||
:aria-pressed="passwordVisible"
|
||||
:aria-label="passwordVisible ? '隐藏密码' : '显示密码'"
|
||||
@click="togglePassword"
|
||||
>
|
||||
<svg class="icon-eye" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d="M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0"></path>
|
||||
<circle cx="12" cy="12" r="3"></circle>
|
||||
</svg>
|
||||
<svg class="icon-eye-off" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d="m2 2 20 20"></path>
|
||||
<path d="M10.733 5.076a10.744 10.744 0 0 1 9.205 6.272 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-1.651 2.59"></path>
|
||||
<path d="M14.084 14.158a3 3 0 0 1-4.242-4.242"></path>
|
||||
<path d="M17.479 17.499a10.75 10.75 0 0 1-5.479 1.501c-4.478 0-8.268-2.943-9.876-6.652a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 2.646-3.393"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn-login" id="btnLogin" :disabled="loggingIn">
|
||||
{{ loggingIn ? '登录中...' : '登录' }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
|
||||
const AUTH_TOKEN_KEY = 'aiimage_auth_token'
|
||||
const DEVICE_ID_KEY = 'aiimage_device_id'
|
||||
|
||||
const username = ref('')
|
||||
const password = ref('')
|
||||
const passwordVisible = ref(false)
|
||||
const loggingIn = ref(false)
|
||||
const errorMessage = ref('')
|
||||
|
||||
function togglePassword() {
|
||||
passwordVisible.value = !passwordVisible.value
|
||||
}
|
||||
|
||||
function isDesktopClient() {
|
||||
return Boolean(window.pywebview?.api) || window.location.pathname === '/login'
|
||||
}
|
||||
|
||||
function clearAppPermissionCaches() {
|
||||
try {
|
||||
const keys: string[] = []
|
||||
for (let i = 0; i < window.localStorage.length; i += 1) {
|
||||
const key = window.localStorage.key(i)
|
||||
if (key && key.indexOf('app_column_permissions:') === 0) keys.push(key)
|
||||
}
|
||||
keys.forEach((key) => window.localStorage.removeItem(key))
|
||||
} catch {
|
||||
/* 忽略 localStorage 异常 */
|
||||
}
|
||||
}
|
||||
|
||||
function makeBrowserDeviceId() {
|
||||
let deviceId = ''
|
||||
try {
|
||||
deviceId = window.localStorage.getItem(DEVICE_ID_KEY) || ''
|
||||
} catch {
|
||||
deviceId = ''
|
||||
}
|
||||
if (!deviceId) {
|
||||
deviceId = `web-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 12)}`
|
||||
try {
|
||||
window.localStorage.setItem(DEVICE_ID_KEY, deviceId)
|
||||
} catch {
|
||||
/* 忽略 */
|
||||
}
|
||||
}
|
||||
return deviceId
|
||||
}
|
||||
|
||||
async function fetchDeviceId(): Promise<string> {
|
||||
if (window.pywebview?.api?.get_device_id) {
|
||||
try {
|
||||
const res = await window.pywebview.api.get_device_id()
|
||||
if (res?.success && res.device_id) {
|
||||
try {
|
||||
window.localStorage.setItem(DEVICE_ID_KEY, res.device_id)
|
||||
} catch {
|
||||
/* 忽略 */
|
||||
}
|
||||
return res.device_id
|
||||
}
|
||||
} catch {
|
||||
/* 回退浏览器生成 */
|
||||
}
|
||||
}
|
||||
return makeBrowserDeviceId()
|
||||
}
|
||||
|
||||
async function submitLogin() {
|
||||
if (loggingIn.value) return
|
||||
const account = username.value.trim()
|
||||
if (!account || !password.value) {
|
||||
errorMessage.value = '请输入账号和密码'
|
||||
return
|
||||
}
|
||||
loggingIn.value = true
|
||||
errorMessage.value = ''
|
||||
try {
|
||||
const deviceId = await fetchDeviceId()
|
||||
if (!deviceId) {
|
||||
errorMessage.value = '未获取到设备ID,请在桌面端打开'
|
||||
return
|
||||
}
|
||||
const loginResp = await window.fetch('/newApi/login', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Device-Id': deviceId,
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
},
|
||||
body: JSON.stringify({ username: account, password: password.value, deviceId }),
|
||||
})
|
||||
const loginBody = await loginResp.json().catch(() => ({}))
|
||||
if (!loginBody || loginBody.success !== true || !loginBody.data) {
|
||||
errorMessage.value = (loginBody && (loginBody.message || loginBody.msg)) || '登录失败'
|
||||
return
|
||||
}
|
||||
const data = loginBody.data || {}
|
||||
if (data.token) {
|
||||
try {
|
||||
window.localStorage.setItem(AUTH_TOKEN_KEY, data.token)
|
||||
} catch {
|
||||
/* 忽略 */
|
||||
}
|
||||
}
|
||||
if (data.userId) {
|
||||
try {
|
||||
window.localStorage.setItem('uid', String(data.userId))
|
||||
} catch {
|
||||
/* 忽略 */
|
||||
}
|
||||
}
|
||||
if (data.username) {
|
||||
try {
|
||||
window.localStorage.setItem('username', String(data.username))
|
||||
} catch {
|
||||
/* 忽略 */
|
||||
}
|
||||
}
|
||||
// 把 Java 签发的 JWT 同步到 Python 同源 cookie,供后续页面跳转携带
|
||||
try {
|
||||
await window.fetch('/api/auth/sync', {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ token: data.token }),
|
||||
})
|
||||
} catch {
|
||||
/* cookie 同步失败不阻塞跳转(页面仍可读 localStorage) */
|
||||
}
|
||||
clearAppPermissionCaches()
|
||||
window.location.href = isDesktopClient() ? '/home' : '/home.html'
|
||||
} catch {
|
||||
errorMessage.value = '网络异常,请稍后重试'
|
||||
} finally {
|
||||
loggingIn.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const params = new URLSearchParams(window.location.search || '')
|
||||
if (params.get('logout') === '1' || params.get('switch') === '1') {
|
||||
try {
|
||||
window.localStorage.removeItem(AUTH_TOKEN_KEY)
|
||||
} catch {
|
||||
/* 忽略 */
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: "Microsoft YaHei", "PingFang SC", "Hiragino Sans GB", sans-serif;
|
||||
background: #d8e4ec;
|
||||
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;
|
||||
}
|
||||
|
||||
.login-box {
|
||||
background: #fff;
|
||||
border: 1px solid #b8d4e3;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.06);
|
||||
padding: 40px;
|
||||
width: 100%;
|
||||
max-width: 380px;
|
||||
}
|
||||
|
||||
.login-title {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
color: #555;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.form-group input {
|
||||
width: 100%;
|
||||
padding: 12px 14px;
|
||||
font-size: 14px;
|
||||
border: 1px solid #b8d4e3;
|
||||
border-radius: 8px;
|
||||
outline: none;
|
||||
transition: border-color 0.2s;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.form-group input:focus {
|
||||
border-color: #3498db;
|
||||
}
|
||||
|
||||
.password-field {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.password-field input {
|
||||
padding-right: 48px;
|
||||
}
|
||||
|
||||
.password-toggle {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 10px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
padding: 0;
|
||||
color: #71808d;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.password-toggle:hover {
|
||||
color: #3498db;
|
||||
background: #f1f7fb;
|
||||
}
|
||||
|
||||
.password-toggle svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.password-toggle .icon-eye-off {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.password-toggle.is-visible .icon-eye {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.password-toggle.is-visible .icon-eye-off {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.error-msg {
|
||||
color: #e74c3c;
|
||||
font-size: 13px;
|
||||
margin-bottom: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.btn-login {
|
||||
width: 100%;
|
||||
padding: 14px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #fff;
|
||||
background: linear-gradient(135deg, #3498db 0%, #2980b9 100%);
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-login:hover:not(:disabled) {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.btn-login:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user