feat(web): 桌面登录页记住密码/自动登录与版本更新,首页版本更新入口与背景修复
- 登录页(仅桌面端)新增"记住密码/自动登录"勾选与"更新版本"按钮:登录成功持久化凭据 (base64 存桌面 localStorage),打开登录页恢复勾选并静默登录;显式登出/切号后关闭自动登录 - 新增 shared/composables/useVersionUpdate 统一版本检测:/api/version/latest 与本机版本比较, 通过 pywebview 桥 get_app_version 读本地 exe 版本,缺桥时降级为可手动更新 - 首页保留 ↻ 图标并加显式"更新版本"按钮,接入新检测逻辑 - 首页背景改画到 .home-root,避免被 SPA 全局样式时序覆盖导致图不显示
This commit is contained in:
@@ -43,29 +43,140 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 记住密码 / 自动登录:仅桌面客户端提供(网页浏览器访问不存密码,见需求) -->
|
||||
<div v-if="isDesktopRuntime" class="login-options">
|
||||
<label class="check-label">
|
||||
<input type="checkbox" class="check-input" v-model="rememberPassword" />
|
||||
<span>记住密码</span>
|
||||
</label>
|
||||
<label class="check-label">
|
||||
<input type="checkbox" class="check-input" v-model="autoLogin" />
|
||||
<span>自动登录</span>
|
||||
</label>
|
||||
</div>
|
||||
<button type="submit" class="btn-login" id="btnLogin" :disabled="loggingIn">
|
||||
{{ loggingIn ? '登录中...' : '登录' }}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<!-- 版本更新(桌面端检测线上版本并下载更新包,对齐旧版客户端入口) -->
|
||||
<div v-if="isDesktopRuntime" class="login-update">
|
||||
<button type="button" class="link-update" @click="toggleUpdate">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d="M21 12a9 9 0 1 1-2.64-6.36"></path>
|
||||
<path d="M21 3v6h-6"></path>
|
||||
</svg>
|
||||
更新版本
|
||||
</button>
|
||||
<div v-if="updateOpen" class="update-panel">
|
||||
<div class="update-panel-title">软件更新</div>
|
||||
<div class="update-row"><span>当前版本</span><b>{{ currentVersion || '—' }}</b></div>
|
||||
<div class="update-row"><span>最新版本</span><b>{{ latestVersion || '—' }}</b></div>
|
||||
<div class="update-actions">
|
||||
<button type="button" class="btn-mini" :disabled="checking" @click="runCheck">
|
||||
{{ checking ? '检测中...' : '检测' }}
|
||||
</button>
|
||||
<button
|
||||
v-if="hasUpdate || canDownload"
|
||||
type="button"
|
||||
class="btn-mini btn-mini-green"
|
||||
:disabled="updating"
|
||||
@click="doUpdate"
|
||||
>
|
||||
{{ updating ? '更新中...' : '立即更新' }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="update-hint">{{ hint }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { onMounted, ref, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { loginWithDevice } from '@/shared/api/user'
|
||||
import { isDesktopRuntime } from '@/shared/bridges/pywebview'
|
||||
import { useVersionUpdate } from '@/shared/composables/useVersionUpdate'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const AUTH_TOKEN_KEY = 'aiimage_auth_token'
|
||||
const DEVICE_ID_KEY = 'aiimage_device_id'
|
||||
|
||||
const REMEMBER_USER_KEY = 'aiimage_remember_user'
|
||||
const REMEMBER_PWD_KEY = 'aiimage_remember_pwd'
|
||||
const AUTO_LOGIN_KEY = 'aiimage_auto_login'
|
||||
|
||||
const username = ref('')
|
||||
const password = ref('')
|
||||
const passwordVisible = ref(false)
|
||||
const loggingIn = ref(false)
|
||||
const errorMessage = ref('')
|
||||
const rememberPassword = ref(false)
|
||||
const autoLogin = ref(false)
|
||||
const updateOpen = ref(false)
|
||||
|
||||
// 版本检测 / 更新:登录页与首页共用逻辑
|
||||
const {
|
||||
checking,
|
||||
updating,
|
||||
currentVersion,
|
||||
latestVersion,
|
||||
hasUpdate,
|
||||
canDownload,
|
||||
hint,
|
||||
checked,
|
||||
runCheck,
|
||||
doUpdate,
|
||||
} = useVersionUpdate()
|
||||
|
||||
function b64Encode(value: string): string {
|
||||
try {
|
||||
return btoa(unescape(encodeURIComponent(value)))
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
function b64Decode(value: string): string {
|
||||
if (!value) return ''
|
||||
try {
|
||||
return decodeURIComponent(escape(atob(value)))
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
function lsGet(key: string): string {
|
||||
try {
|
||||
return window.localStorage.getItem(key) || ''
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
function lsSet(key: string, value: string) {
|
||||
try {
|
||||
window.localStorage.setItem(key, value)
|
||||
} catch {
|
||||
/* 忽略 localStorage 异常 */
|
||||
}
|
||||
}
|
||||
|
||||
function lsRemove(key: string) {
|
||||
try {
|
||||
window.localStorage.removeItem(key)
|
||||
} catch {
|
||||
/* 忽略 */
|
||||
}
|
||||
}
|
||||
|
||||
function hasLogoutParam(): boolean {
|
||||
const params = new URLSearchParams(window.location.search || '')
|
||||
return params.get('logout') === '1' || params.get('switch') === '1'
|
||||
}
|
||||
|
||||
function togglePassword() {
|
||||
passwordVisible.value = !passwordVisible.value
|
||||
@@ -121,6 +232,45 @@ async function fetchDeviceId(): Promise<string> {
|
||||
return makeBrowserDeviceId()
|
||||
}
|
||||
|
||||
/** 登录成功后按勾选状态持久化账号凭据(仅桌面端登录页可勾选,密码 base64 简单编码) */
|
||||
function saveCredentials(account: string) {
|
||||
if (!rememberPassword.value) {
|
||||
lsRemove(REMEMBER_USER_KEY)
|
||||
lsRemove(REMEMBER_PWD_KEY)
|
||||
lsRemove(AUTO_LOGIN_KEY)
|
||||
return
|
||||
}
|
||||
lsSet(REMEMBER_USER_KEY, account)
|
||||
lsSet(REMEMBER_PWD_KEY, b64Encode(password.value))
|
||||
lsSet(AUTO_LOGIN_KEY, autoLogin.value ? '1' : '0')
|
||||
}
|
||||
|
||||
/** 打开登录页时恢复记住的账号/勾选状态 */
|
||||
function loadCredentials() {
|
||||
const account = lsGet(REMEMBER_USER_KEY)
|
||||
const pwd = b64Decode(lsGet(REMEMBER_PWD_KEY))
|
||||
if (account) username.value = account
|
||||
if (pwd) password.value = pwd
|
||||
const auto = lsGet(AUTO_LOGIN_KEY) === '1'
|
||||
autoLogin.value = auto
|
||||
rememberPassword.value = auto || Boolean(pwd)
|
||||
}
|
||||
|
||||
/** 打开登录页即用记住的凭据静默登录(登出/切号导航除外) */
|
||||
function tryAutoLogin() {
|
||||
if (hasLogoutParam()) return
|
||||
if (!autoLogin.value) return
|
||||
if (!username.value || !lsGet(REMEMBER_PWD_KEY)) return
|
||||
void submitLogin()
|
||||
}
|
||||
|
||||
function toggleUpdate() {
|
||||
updateOpen.value = !updateOpen.value
|
||||
if (updateOpen.value && !checked.value && !checking.value) {
|
||||
void runCheck()
|
||||
}
|
||||
}
|
||||
|
||||
async function submitLogin() {
|
||||
if (loggingIn.value) return
|
||||
const account = username.value.trim()
|
||||
@@ -164,6 +314,10 @@ async function submitLogin() {
|
||||
/* 忽略 */
|
||||
}
|
||||
}
|
||||
// 登录成功即持久化记住/自动登录凭据(桌面端专属勾选)
|
||||
if (isDesktopRuntime.value) {
|
||||
saveCredentials(account)
|
||||
}
|
||||
// 桌面端 Flask cookie 同步已随瘦身下线:登录态只存 localStorage(JWT 走 Bearer),无 cookie 依赖
|
||||
clearAppPermissionCaches()
|
||||
// SPA:登录成功后经路由回首页(URL 无 .html 后缀,见 src/router)
|
||||
@@ -186,7 +340,29 @@ onMounted(() => {
|
||||
} catch {
|
||||
/* 忽略 */
|
||||
}
|
||||
// 显式登出/切号后不再自动登录(保留记住的账号,仅关闭自动登录)
|
||||
autoLogin.value = false
|
||||
lsSet(AUTO_LOGIN_KEY, '0')
|
||||
}
|
||||
|
||||
// 桌面桥注入完成后再恢复凭据/自动登录(isDesktopRuntime 在 pywebviewready 后置 true)
|
||||
watch(
|
||||
isDesktopRuntime,
|
||||
(runtime) => {
|
||||
if (!runtime) return
|
||||
loadCredentials()
|
||||
tryAutoLogin()
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
})
|
||||
|
||||
// 勾选自动登录时隐含记住密码(自动登录依赖已存密码);反之取消记住密码则取消自动登录
|
||||
watch(autoLogin, (checkedValue) => {
|
||||
if (checkedValue) rememberPassword.value = true
|
||||
})
|
||||
watch(rememberPassword, (checkedValue) => {
|
||||
if (!checkedValue) autoLogin.value = false
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -233,6 +409,7 @@ body {
|
||||
}
|
||||
|
||||
.login-box {
|
||||
position: relative;
|
||||
background: #fff;
|
||||
border: 1px solid #b8d4e3;
|
||||
border-radius: 12px;
|
||||
@@ -332,6 +509,32 @@ body {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 记住密码 / 自动登录 */
|
||||
.login-options {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
margin: -4px 0 16px;
|
||||
}
|
||||
|
||||
.check-label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 13px;
|
||||
color: #555;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.check-input {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
accent-color: #3498db;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-login {
|
||||
width: 100%;
|
||||
padding: 14px;
|
||||
@@ -353,4 +556,104 @@ body {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* 版本更新入口与面板 */
|
||||
.login-update {
|
||||
margin-top: 14px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.link-update {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
padding: 4px 8px;
|
||||
font-size: 13px;
|
||||
color: #71808d;
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.link-update svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
.link-update:hover {
|
||||
color: #3498db;
|
||||
background: #f1f7fb;
|
||||
border-color: #d5e6f2;
|
||||
}
|
||||
|
||||
.update-panel {
|
||||
margin-top: 10px;
|
||||
padding: 12px 14px;
|
||||
background: #f6fafd;
|
||||
border: 1px solid #d5e6f2;
|
||||
border-radius: 8px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.update-panel-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.update-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
font-size: 13px;
|
||||
color: #555;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.update-row b {
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.update-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.btn-mini {
|
||||
padding: 5px 12px;
|
||||
font-size: 13px;
|
||||
color: #fff;
|
||||
background: #3498db;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-mini:hover:not(:disabled) {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.btn-mini:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-mini-green {
|
||||
background: #28a745;
|
||||
}
|
||||
|
||||
.btn-mini-green:hover:not(:disabled) {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.update-hint {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
margin-top: 8px;
|
||||
min-height: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user