feat(web): 记住密码/自动登录与版本更新放开到网页形态
- 登录页与首页移除 isDesktopRuntime 门控,网页浏览器同样显示记住密码/自动登录与更新版本入口 - 记住密码由 base64 明文改为 XOR+位移可逆加密存储(v1. 前缀),兼容解密历史值 - 网页形态进入登录页即回填凭据并尝试自动登录(登出/切号导航仍豁免) - 版本更新在无桌面桥时降级为 OSS 直链下载安装包,桌面端维持自动安装
This commit is contained in:
@@ -4,14 +4,12 @@
|
||||
<span class="header-title">数富AI</span>
|
||||
<div class="header-right">
|
||||
<button
|
||||
v-if="isDesktopRuntime"
|
||||
type="button"
|
||||
class="header-update-btn"
|
||||
title="检测更新"
|
||||
@click="toggleUpdatePanel"
|
||||
>↻</button>
|
||||
<button
|
||||
v-if="isDesktopRuntime"
|
||||
type="button"
|
||||
class="header-update-text"
|
||||
@click="toggleUpdatePanel"
|
||||
@@ -67,10 +65,9 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { restoreLoginUser } from '@/shared/auth/ensure-auth'
|
||||
import { getCurrentUserAppColumnRaw, type PermissionMenuItem } from '@/shared/api/permission'
|
||||
import { isDesktopRuntime } from '@/shared/bridges/pywebview'
|
||||
import { useVersionUpdate } from '@/shared/composables/useVersionUpdate'
|
||||
import { resolvePageHref } from '@/shared/page-prefix'
|
||||
|
||||
@@ -176,16 +173,8 @@ function toggleUpdatePanel() {
|
||||
onMounted(() => {
|
||||
void loadCurrentUser()
|
||||
void loadPermissions()
|
||||
// 版本检测/更新仅桌面客户端:桌面桥注入后立即检测一次(逻辑见 useVersionUpdate)
|
||||
watch(
|
||||
isDesktopRuntime,
|
||||
(runtime) => {
|
||||
if (runtime) {
|
||||
void runCheck()
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
// 版本检测/更新:桌面与网页形态统一挂载后检测一次(逻辑见 useVersionUpdate)
|
||||
void runCheck()
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -43,8 +43,8 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 记住密码 / 自动登录:仅桌面客户端提供(网页浏览器访问不存密码,见需求) -->
|
||||
<div v-if="isDesktopRuntime" class="login-options">
|
||||
<!-- 记住密码 / 自动登录:桌面与网页均提供(网页形态密码经可逆加密后落 localStorage) -->
|
||||
<div class="login-options">
|
||||
<label class="check-label">
|
||||
<input type="checkbox" class="check-input" v-model="rememberPassword" />
|
||||
<span>记住密码</span>
|
||||
@@ -59,8 +59,8 @@
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<!-- 版本更新(桌面端检测线上版本并下载更新包,对齐旧版客户端入口) -->
|
||||
<div v-if="isDesktopRuntime" class="login-update">
|
||||
<!-- 版本更新:桌面端自动下载安装;网页形态降级为下载最新安装包 -->
|
||||
<div 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>
|
||||
@@ -97,7 +97,6 @@
|
||||
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()
|
||||
@@ -132,18 +131,43 @@ const {
|
||||
doUpdate,
|
||||
} = useVersionUpdate()
|
||||
|
||||
function b64Encode(value: string): string {
|
||||
function b64Decode(value: string): string {
|
||||
if (!value) return ''
|
||||
try {
|
||||
return btoa(unescape(encodeURIComponent(value)))
|
||||
return decodeURIComponent(escape(atob(value)))
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
function b64Decode(value: string): string {
|
||||
if (!value) return ''
|
||||
// 记住密码存储:可逆加密(XOR+位移再 base64),带版本前缀 v1.。
|
||||
// 说明:纯前端 localStorage 无法做强密码保护,此仅规避明文与早期 base64 直存。
|
||||
const PWD_ENC_PREFIX = 'v1.'
|
||||
const PWD_ENC_KEY = [0x5a, 0x3c, 0x9f, 0x2e, 0x71, 0x8b, 0x1d, 0xe6]
|
||||
|
||||
function encryptRememberPwd(plain: string): string {
|
||||
const bytes = plain.split('').map((ch) => ch.charCodeAt(0))
|
||||
for (let i = 0; i < bytes.length; i += 1) {
|
||||
bytes[i] = (bytes[i] ^ PWD_ENC_KEY[i % PWD_ENC_KEY.length] ^ i) & 0xff
|
||||
}
|
||||
try {
|
||||
return decodeURIComponent(escape(atob(value)))
|
||||
return PWD_ENC_PREFIX + btoa(String.fromCharCode(...bytes))
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
function decryptRememberPwd(stored: string): string {
|
||||
if (!stored) return ''
|
||||
if (!stored.startsWith(PWD_ENC_PREFIX)) {
|
||||
// 兼容早期仅 base64 的历史值
|
||||
return b64Decode(stored)
|
||||
}
|
||||
try {
|
||||
const bytes = atob(stored.slice(PWD_ENC_PREFIX.length)).split('').map((ch) => ch.charCodeAt(0))
|
||||
return bytes
|
||||
.map((code, i) => String.fromCharCode((code ^ PWD_ENC_KEY[i % PWD_ENC_KEY.length] ^ i) & 0xff))
|
||||
.join('')
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
@@ -241,14 +265,14 @@ function saveCredentials(account: string) {
|
||||
return
|
||||
}
|
||||
lsSet(REMEMBER_USER_KEY, account)
|
||||
lsSet(REMEMBER_PWD_KEY, b64Encode(password.value))
|
||||
lsSet(REMEMBER_PWD_KEY, encryptRememberPwd(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))
|
||||
const pwd = decryptRememberPwd(lsGet(REMEMBER_PWD_KEY))
|
||||
if (account) username.value = account
|
||||
if (pwd) password.value = pwd
|
||||
const auto = lsGet(AUTO_LOGIN_KEY) === '1'
|
||||
@@ -314,10 +338,8 @@ async function submitLogin() {
|
||||
/* 忽略 */
|
||||
}
|
||||
}
|
||||
// 登录成功即持久化记住/自动登录凭据(桌面端专属勾选)
|
||||
if (isDesktopRuntime.value) {
|
||||
saveCredentials(account)
|
||||
}
|
||||
// 登录成功即持久化记住/自动登录凭据(桌面与网页均可,密码加密落 localStorage)
|
||||
saveCredentials(account)
|
||||
// 桌面端 Flask cookie 同步已随瘦身下线:登录态只存 localStorage(JWT 走 Bearer),无 cookie 依赖
|
||||
clearAppPermissionCaches()
|
||||
// SPA:登录成功后经路由回首页(URL 无 .html 后缀,见 src/router)
|
||||
@@ -345,16 +367,9 @@ onMounted(() => {
|
||||
lsSet(AUTO_LOGIN_KEY, '0')
|
||||
}
|
||||
|
||||
// 桌面桥注入完成后再恢复凭据/自动登录(isDesktopRuntime 在 pywebviewready 后置 true)
|
||||
watch(
|
||||
isDesktopRuntime,
|
||||
(runtime) => {
|
||||
if (!runtime) return
|
||||
loadCredentials()
|
||||
tryAutoLogin()
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
// 恢复记住的凭据并尝试自动登录(桌面与网页形态一致;登出/切号导航由 tryAutoLogin 内部豁免)
|
||||
loadCredentials()
|
||||
tryAutoLogin()
|
||||
})
|
||||
|
||||
// 勾选自动登录时隐含记住密码(自动登录依赖已存密码);反之取消记住密码则取消自动登录
|
||||
|
||||
@@ -109,17 +109,27 @@ export function useVersionUpdate() {
|
||||
|
||||
async function doUpdate() {
|
||||
if (updating.value || !fileUrl.value) return
|
||||
if (!window.confirm('有更新,是否现在更新?\n更新将下载安装包并重启程序。')) return
|
||||
const bridge = getPywebviewApi()
|
||||
const doUpdateApp = bridge?.do_update_app
|
||||
const desktopUpdate = Boolean(doUpdateApp)
|
||||
const tip = desktopUpdate
|
||||
? '有更新,是否现在更新?\n更新将下载安装包并重启程序。'
|
||||
: '发现新版本,是否下载最新安装包?'
|
||||
if (!window.confirm(tip)) return
|
||||
updating.value = true
|
||||
if (!desktopUpdate) {
|
||||
// 网页/无桌面桥形态:OSS 公开直链直接下载安装包(区别于桌面端自动下载安装)
|
||||
hint.value = '开始下载安装包,若浏览器未响应请再次点击...'
|
||||
window.location.href = fileUrl.value
|
||||
updating.value = false
|
||||
return
|
||||
}
|
||||
hint.value = '正在下载并准备更新,程序将自动退出...'
|
||||
try {
|
||||
const bridge = getPywebviewApi()
|
||||
const result = bridge?.do_update_app
|
||||
? await bridge.do_update_app(fileUrl.value)
|
||||
: undefined
|
||||
const result = await doUpdateApp!(fileUrl.value)
|
||||
hint.value = result?.success
|
||||
? '更新已启动,程序即将退出...'
|
||||
: (result?.error || '当前环境不支持自动更新')
|
||||
: (result?.error || '更新启动失败,请重试')
|
||||
} catch {
|
||||
hint.value = '请求更新失败,请重试'
|
||||
} finally {
|
||||
|
||||
Reference in New Issue
Block a user