适配安卓客户端

This commit is contained in:
黄艳鹏 2024-08-08 15:17:44 +08:00
parent a0050dd94c
commit 7bf39b8a6d
21 changed files with 284 additions and 128 deletions

2
.idea/compiler.xml generated
View File

@ -5,7 +5,7 @@
<profile name="Gradle Imported" enabled="true"> <profile name="Gradle Imported" enabled="true">
<outputRelativeToContentRoot value="true" /> <outputRelativeToContentRoot value="true" />
<processorPath useClasspath="false"> <processorPath useClasspath="false">
<entry name="$USER_HOME$/.gradle/caches/modules-2/files-2.1/org.projectlombok/lombok/1.18.32/17d46b3e205515e1e8efd3ee4d57ce8018914163/lombok-1.18.32.jar" /> <entry name="$USER_HOME$/.gradle/caches/modules-2/files-2.1/org.projectlombok/lombok/1.18.34/ec547ef414ab1d2c040118fb9c1c265ada63af14/lombok-1.18.34.jar" />
</processorPath> </processorPath>
<module name="password-xl-service.main" /> <module name="password-xl-service.main" />
</profile> </profile>

View File

@ -1,7 +1,7 @@
plugins { plugins {
id 'java' id 'java'
id 'org.springframework.boot' version '3.3.1' id 'org.springframework.boot' version '3.3.2'
id 'io.spring.dependency-management' version '1.1.5' id 'io.spring.dependency-management' version '1.1.6'
id 'org.graalvm.buildtools.native' version '0.10.2' id 'org.graalvm.buildtools.native' version '0.10.2'
} }
@ -26,15 +26,18 @@ repositories {
dependencies { dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'cn.hutool:hutool-all:5.8.27' implementation 'cn.hutool:hutool-all:5.8.29'
compileOnly 'org.projectlombok:lombok' compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok'
implementation 'com.alibaba.fastjson2:fastjson2:2.0.51' implementation 'com.alibaba.fastjson2:fastjson2:2.0.52'
implementation 'io.hotmoka:toml4j:0.7.3' implementation 'io.hotmoka:toml4j:0.7.3'
} }
tasks.bootBuildImage { tasks.bootBuildImage {
imageName = "${project.name}" imageName = "${project.name}"
docker {
host = "////./pipe/dockerDesktopLinuxEngine"
}
} }
tasks.register('printVersion') { tasks.register('printVersion') {

View File

@ -1,10 +1,5 @@
import {contextBridge, ipcRenderer} from 'electron' import {contextBridge, ipcRenderer} from 'electron'
// 设置环境变量
contextBridge.exposeInMainWorld('env', {
electron: true
})
// 开放给源码的API // 开放给源码的API
contextBridge.exposeInMainWorld('electronAPI', { contextBridge.exposeInMainWorld('electronAPI', {
getFile: async (fileName) => ipcRenderer.invoke('get-file', fileName), getFile: async (fileName) => ipcRenderer.invoke('get-file', fileName),

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

@ -8,6 +8,7 @@ export {}
declare module 'vue' { declare module 'vue' {
export interface GlobalComponents { export interface GlobalComponents {
About: typeof import('./components/common/setting/About.vue')['default'] About: typeof import('./components/common/setting/About.vue')['default']
AndroidLoginForm: typeof import('./components/login/AndroidLoginForm.vue')['default']
BackupAndRecovery: typeof import('./components/common/setting/BackupAndRecovery.vue')['default'] BackupAndRecovery: typeof import('./components/common/setting/BackupAndRecovery.vue')['default']
CancelAccount: typeof import('./components/common/setting/CancelAccount.vue')['default'] CancelAccount: typeof import('./components/common/setting/CancelAccount.vue')['default']
CommonProblem: typeof import('./components/common/setting/CommonProblem.vue')['default'] CommonProblem: typeof import('./components/common/setting/CommonProblem.vue')['default']

View File

@ -14,7 +14,7 @@ const visFastLogin = ref(false)
// //
const fastLoginTip = (form: any) => { const fastLoginTip = (form: any) => {
console.log('快速登录提示') console.log('快速登录提示')
if (location.href.indexOf('autoLogin') !== -1 || (window.env && window.env.electron)) { if (location.href.indexOf('autoLogin') !== -1 || window.electronAPI || window.androidAPI) {
router.push('/') router.push('/')
return return
} }

View File

@ -46,9 +46,9 @@ const config = {
// / // /
pointCellSize: 3, pointCellSize: 3,
// //
cellPointRadio: 0.3, cellPointRadio: 0.33,
// //
gesturePointColor: '#d5d5d5', gesturePointColor: '#cacaca',
// //
gesturePointErrorColor: '#F56C6C', gesturePointErrorColor: '#F56C6C',
// //
@ -289,7 +289,7 @@ const mousemove = (e: MouseEvent) => {
// //
const mouseup = () => { const mouseup = () => {
if(!pressed.value) return if (!pressed.value) return
pressed.value = false pressed.value = false
hoverPoint.value = null hoverPoint.value = null
canvasUp() canvasUp()
@ -297,7 +297,7 @@ const mouseup = () => {
// //
const mouseleave = () => { const mouseleave = () => {
if(!pressed.value) return if (!pressed.value) return
pressed.value = false pressed.value = false
hoverPoint.value = null hoverPoint.value = null
canvasUp() canvasUp()
@ -330,12 +330,23 @@ const touchmove = (e: TouchEvent) => {
// //
const touchend = () => { const touchend = () => {
if(!pressed.value) return if (!pressed.value) return
pressed.value = false pressed.value = false
hoverPoint.value = null hoverPoint.value = null
canvasUp() canvasUp()
} }
function createHDCanvas(canvas: any, w: number, h: number) {
const ratio = window.devicePixelRatio || 1;
canvas.width = w * ratio; //
canvas.height = h * ratio; //
canvas.style.width = `${w}px`; //
canvas.style.height = `${h}px`; //
const ctx = canvas.getContext('2d')
ctx.scale(ratio, ratio)
return ctx;
}
// //
const initCanvas = () => { const initCanvas = () => {
if (!gestureDivRef.value || !canvasRef.value) return if (!gestureDivRef.value || !canvasRef.value) return
@ -344,7 +355,8 @@ const initCanvas = () => {
console.log('画板大小:', size) console.log('画板大小:', size)
canvasRef.value.width = size canvasRef.value.width = size
canvasRef.value.height = size canvasRef.value.height = size
ctx = canvasRef.value.getContext('2d') as CanvasRenderingContext2D
ctx = createHDCanvas(canvasRef.value, size, size) as CanvasRenderingContext2D
let incrId = 0 let incrId = 0
allPointArray.length = 0 allPointArray.length = 0

View File

@ -194,6 +194,7 @@ defineExpose({
<template> <template>
<el-dialog <el-dialog
top="20vh"
:width="['xs', 'sm'].includes(displaySize().value)?'95%':'400px'" :width="['xs', 'sm'].includes(displaySize().value)?'95%':'400px'"
v-model="visVerify" v-model="visVerify"
:close-on-click-modal="passwordStore.serviceStatus !== ServiceStatus.NO_LOGIN" :close-on-click-modal="passwordStore.serviceStatus !== ServiceStatus.NO_LOGIN"

View File

@ -9,17 +9,16 @@ import packageJson from '../../../../package.json'
</div> </div>
<div style="padding: 10px;"> <div style="padding: 10px;">
<el-descriptions :column="1"> <el-descriptions :column="1">
<el-descriptions-item label="项目名称">{{ packageJson.name}}</el-descriptions-item> <el-descriptions-item label="项目名称:">{{ packageJson.name}}</el-descriptions-item>
<el-descriptions-item label="当前版本">{{ packageJson.version }}</el-descriptions-item> <el-descriptions-item label="当前版本:">{{ packageJson.version }}</el-descriptions-item>
<el-descriptions-item label="官方网站"> <el-descriptions-item label="官方网站:">
<el-link type="primary" target="_blank" :href="packageJson.homepage">{{ packageJson.homepage }}</el-link> <el-link type="primary" target="_blank" :href="packageJson.homepage">{{ packageJson.homepage }}</el-link>
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="开源地址"> <el-descriptions-item label="开源地址:">
<el-link type="primary" target="_blank" :href="packageJson.repository.url">{{ packageJson.repository.url }}</el-link> <el-link type="primary" target="_blank" :href="packageJson.repository.url">{{ packageJson.repository.url }}</el-link>
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="作者名称:">{{ packageJson.contributors[0].name }}</el-descriptions-item> <el-descriptions-item label="作者邮箱:">{{ packageJson.contributors[0].email }}</el-descriptions-item>
<el-descriptions-item label="作者邮箱:">{{ packageJson.contributors[0].email }}</el-descriptions-item> <el-descriptions-item label="作者微信:">{{ packageJson.contributors[0].weChat }}</el-descriptions-item>
<el-descriptions-item label="作者微信:">{{ packageJson.contributors[0].weChat }}</el-descriptions-item>
</el-descriptions> </el-descriptions>
</div> </div>
</template> </template>

View File

@ -6,19 +6,22 @@ import {useLoginStore} from "@/stores/LoginStore.ts";
const loginStore = useLoginStore() const loginStore = useLoginStore()
const isElectron = () => { const isElectron = () => {
return window.env && window.env.electron return !!window.electronAPI
}
const isAndroid = () => {
return !!window.androidAPI
} }
</script> </script>
<template> <template>
<el-form label-width="110px"> <el-form label-width="80px">
<template v-if="loginStore.loginType === 'oss'"> <template v-if="loginStore.loginType === 'oss'">
<div style="text-align: center;margin-bottom: 5px;width: 100%"> <div style="text-align: center;margin-bottom: 5px;width: 100%">
<img alt="" class="login-type-image" style="height: 28px" src="@/assets/images/login/oss.png"> <img alt="" class="login-type-image" style="height: 28px" src="@/assets/images/login/oss.png">
<el-text style="font-size: 24px">阿里云OSS</el-text> <el-text style="font-size: 24px">阿里云OSS</el-text>
</div> </div>
<el-divider></el-divider> <el-divider></el-divider>
<div style="padding: 0 50px 0 20px"> <div>
<el-form-item label="region"> <el-form-item label="region">
<el-input :model-value="loginStore.loginForm.region" :readonly="true"> <el-input :model-value="loginStore.loginForm.region" :readonly="true">
<template #append> <template #append>
@ -55,7 +58,7 @@ const isElectron = () => {
</template> </template>
</el-input> </el-input>
</el-form-item> </el-form-item>
<el-form-item label="快速登录链接" v-if="!isElectron()"> <el-form-item label="快速登录链接" v-if="!isElectron() && !isAndroid()">
<el-input :model-value="getFastLoginLink(loginStore.loginForm)" :readonly="true"> <el-input :model-value="getFastLoginLink(loginStore.loginForm)" :readonly="true">
<template #append> <template #append>
<el-button @click="copyText(getFastLoginLink(loginStore.loginForm))" class="copy-btn"> <el-button @click="copyText(getFastLoginLink(loginStore.loginForm))" class="copy-btn">
@ -72,7 +75,7 @@ const isElectron = () => {
<el-text style="font-size: 24px">腾讯云COS</el-text> <el-text style="font-size: 24px">腾讯云COS</el-text>
</div> </div>
<el-divider></el-divider> <el-divider></el-divider>
<div style="padding: 0 50px 0 20px"> <div>
<el-form-item label="region"> <el-form-item label="region">
<el-input :model-value="loginStore.loginForm.region" :readonly="true"> <el-input :model-value="loginStore.loginForm.region" :readonly="true">
<template #append> <template #append>
@ -109,7 +112,7 @@ const isElectron = () => {
</template> </template>
</el-input> </el-input>
</el-form-item> </el-form-item>
<el-form-item label="快速登录链接" v-if="!isElectron()"> <el-form-item label="快速登录链接" v-if="!isElectron() && !isAndroid()">
<el-input :model-value="getFastLoginLink(loginStore.loginForm)" :readonly="true"> <el-input :model-value="getFastLoginLink(loginStore.loginForm)" :readonly="true">
<template #append> <template #append>
<el-button @click="copyText(getFastLoginLink(loginStore.loginForm))" class="copy-btn"> <el-button @click="copyText(getFastLoginLink(loginStore.loginForm))" class="copy-btn">
@ -126,7 +129,7 @@ const isElectron = () => {
<el-text style="font-size: 24px">私有服务</el-text> <el-text style="font-size: 24px">私有服务</el-text>
</div> </div>
<el-divider></el-divider> <el-divider></el-divider>
<div style="padding: 0 50px 0 20px"> <div>
<el-form-item label="服务地址"> <el-form-item label="服务地址">
<el-input :model-value="loginStore.loginForm.serverUrl" :readonly="true"> <el-input :model-value="loginStore.loginForm.serverUrl" :readonly="true">
<template #append> <template #append>
@ -154,7 +157,7 @@ const isElectron = () => {
</template> </template>
</el-input> </el-input>
</el-form-item> </el-form-item>
<el-form-item label="快速登录链接" v-if="!isElectron()"> <el-form-item label="快速登录链接" v-if="!isElectron() && !isAndroid()">
<el-input :model-value="getFastLoginLink(loginStore.loginForm)" :readonly="true"> <el-input :model-value="getFastLoginLink(loginStore.loginForm)" :readonly="true">
<template #append> <template #append>
<el-button @click="copyText(getFastLoginLink(loginStore.loginForm))" class="copy-btn"> <el-button @click="copyText(getFastLoginLink(loginStore.loginForm))" class="copy-btn">
@ -167,9 +170,9 @@ const isElectron = () => {
</template> </template>
<template v-else> <template v-else>
<div style="text-align: center;margin-top: 50px;width: 100%"> <div style="text-align: center;margin-top: 50px;width: 100%">
<img alt="" class="login-type-image" style="height: 150px" src="@/assets/images/login/local.png"> <img alt="" class="login-type-image" style="height: 130px" src="@/assets/images/login/local.png">
<div style="margin-top: 40px"> <div style="margin-top: 25px">
<el-text style="font-size: 24px">您正在使用本地文件存储</el-text> <el-text style="font-size: 22px">您正在使用本地文件存储</el-text>
</div> </div>
</div> </div>
<div style="padding: 0 50px 0 20px"> <div style="padding: 0 50px 0 20px">

View File

@ -321,6 +321,11 @@ watch(() => settingStore.setting.generateRule, (newValue: GenerateRule) => {
}, { }, {
deep: true deep: true
}) })
const isAndroid = () => {
return !!window.androidAPI
}
</script> </script>
<template> <template>
@ -488,9 +493,8 @@ watch(() => settingStore.setting.generateRule, (newValue: GenerateRule) => {
<el-scrollbar :height="scrollbarHeight()"> <el-scrollbar :height="scrollbarHeight()">
<div class="function-div"> <div class="function-div">
<div class="function-header" style="margin-bottom: 5px"> <div class="function-header" style="margin-bottom: 5px">
<el-text tag="b">默认排序规则</el-text> <el-text tag="b">密码排序</el-text>
<div> <div>
默认根据
<el-select v-model="settingStore.setting.sortField" size="small" style="width: 90px;"> <el-select v-model="settingStore.setting.sortField" size="small" style="width: 90px;">
<el-option value="addTime" label="添加时间"/> <el-option value="addTime" label="添加时间"/>
<el-option value="updateTime" label="修改时间"/> <el-option value="updateTime" label="修改时间"/>
@ -503,7 +507,6 @@ watch(() => settingStore.setting.generateRule, (newValue: GenerateRule) => {
<el-option :value="Sort.ASC" label="正序"/> <el-option :value="Sort.ASC" label="正序"/>
<el-option :value="Sort.DESC" label="倒序"/> <el-option :value="Sort.DESC" label="倒序"/>
</el-select> </el-select>
排列
</div> </div>
</div> </div>
<el-divider class="function-line"/> <el-divider class="function-line"/>
@ -524,7 +527,7 @@ watch(() => settingStore.setting.generateRule, (newValue: GenerateRule) => {
<div class="function-div"> <div class="function-div">
<div class="function-header" style="margin-bottom: 5px"> <div class="function-header" style="margin-bottom: 5px">
<el-text tag="b">在密码列表中显示时间</el-text> <el-text tag="b">在密码列表中显示时间</el-text>
<el-select v-model="settingStore.setting.showTimeForTable" size="small" style="width: 120px;"> <el-select v-model="settingStore.setting.showTimeForTable" size="small" style="width: 100px;">
<el-option value="no" label="不显示时间"/> <el-option value="no" label="不显示时间"/>
<el-option value="addTime" label="显示添加时间"/> <el-option value="addTime" label="显示添加时间"/>
<el-option value="updateTime" label="显示修改时间"/> <el-option value="updateTime" label="显示修改时间"/>
@ -641,7 +644,7 @@ watch(() => settingStore.setting.generateRule, (newValue: GenerateRule) => {
</div> </div>
</el-scrollbar> </el-scrollbar>
</el-tab-pane> </el-tab-pane>
<el-tab-pane> <el-tab-pane v-if="!isAndroid()">
<template #label> <template #label>
<el-text> <el-text>
<span class="iconfont icon-recovery action-icon" style="color: #ffc400"></span> <span class="iconfont icon-recovery action-icon" style="color: #ffc400"></span>
@ -649,67 +652,67 @@ watch(() => settingStore.setting.generateRule, (newValue: GenerateRule) => {
</el-text> </el-text>
</template> </template>
<el-scrollbar :height="scrollbarHeight()"> <el-scrollbar :height="scrollbarHeight()">
<div class="function-div"> <div class="function-div">
<div class="function-header" style="margin-bottom: 5px"> <div class="function-header" style="margin-bottom: 5px">
<el-text tag="b">备份密码</el-text> <el-text tag="b">备份密码</el-text>
<el-button plain type="primary" @click="refStore.backupAndRecoveryRef.backup(false)" size="small">备份 <el-button plain type="primary" @click="refStore.backupAndRecoveryRef.backup(false)" size="small">备份
</el-button> </el-button>
</div>
<el-divider class="function-line"/>
<el-text type="info" tag="p" style="text-indent: 10px">
密码备份功能可以安全的将加密后的密码文件导出在需要时通过
<el-text>恢复备份密码</el-text>
功能还原
</el-text>
</div> </div>
<el-divider class="function-line"/> <div class="function-div">
<el-text type="info" tag="p" style="text-indent: 10px"> <div class="function-header" style="margin-bottom: 5px">
密码备份功能可以安全的将加密后的密码文件导出在需要时通过 <el-text tag="b">恢复备份密码</el-text>
<el-text>恢复备份密码</el-text> <el-button plain type="primary" @click="refStore.backupAndRecoveryRef.recovery" size="small">恢复
功能还原 </el-button>
</el-text> </div>
</div> <el-divider class="function-line"/>
<div class="function-div"> <el-text type="info" tag="p" style="text-indent: 10px">
<div class="function-header" style="margin-bottom: 5px"> 此功能可以将备份文件中的密码恢复到密码列表在验证备份文件的主密码后您可以选择合并或覆盖方式恢复密码
<el-text tag="b">恢复备份密码</el-text> </el-text>
<el-button plain type="primary" @click="refStore.backupAndRecoveryRef.recovery" size="small">恢复
</el-button>
</div> </div>
<el-divider class="function-line"/>
<el-text type="info" tag="p" style="text-indent: 10px">
此功能可以将备份文件中的密码恢复到密码列表在验证备份文件的主密码后您可以选择合并或覆盖方式恢复密码
</el-text>
</div>
<div class="function-div"> <div class="function-div">
<div class="function-header" style="margin-bottom: 5px"> <div class="function-header" style="margin-bottom: 5px">
<el-text tag="b">导出密码</el-text> <el-text tag="b">导出密码</el-text>
<el-button plain type="primary" @click="refStore.exportExcelRef.exportExcel(false)" size="small">导出 <el-button plain type="primary" @click="refStore.exportExcelRef.exportExcel(false)" size="small">导出
</el-button> </el-button>
</div>
<el-divider class="function-line"/>
<el-text type="info" tag="p" style="text-indent: 10px">
导出密码会将敏感信息明文存储在excel表格中请注意密码安全若您仅需要迁移账号或备份密码建议使用密码备份与恢复功能更加高效安全
</el-text>
</div> </div>
<el-divider class="function-line"/> <div class="function-div">
<el-text type="info" tag="p" style="text-indent: 10px"> <div class="function-header" style="margin-bottom: 5px">
导出密码会将敏感信息明文存储在excel表格中请注意密码安全若您仅需要迁移账号或备份密码建议使用密码备份与恢复功能更加高效安全 <el-text tag="b">导入密码</el-text>
</el-text> <el-button plain type="primary" @click="refStore.importExcelRef.importExcel" size="small">导入
</div> </el-button>
<div class="function-div"> </div>
<div class="function-header" style="margin-bottom: 5px"> <el-divider class="function-line"/>
<el-text tag="b">导入密码</el-text> <el-text type="info" tag="p" style="text-indent: 10px">
<el-button plain type="primary" @click="refStore.importExcelRef.importExcel" size="small">导入 密码导入功能支持按照密码模板导入excel文件中的密码您可以通过下载模板功能获取密码模板
</el-button> </el-text>
</div> </div>
<el-divider class="function-line"/> <div class="function-div">
<el-text type="info" tag="p" style="text-indent: 10px"> <div class="function-header" style="margin-bottom: 5px">
密码导入功能支持按照密码模板导入excel文件中的密码您可以通过下载模板功能获取密码模板 <el-text tag="b">下载导入模板</el-text>
</el-text> <el-button plain type="primary" @click="refStore.exportExcelRef.exportExcel(true)" size="small">下载
</div> </el-button>
<div class="function-div"> </div>
<div class="function-header" style="margin-bottom: 5px"> <el-divider class="function-line"/>
<el-text tag="b">下载导入模板</el-text> <el-text type="info" tag="p" style="text-indent: 10px">
<el-button plain type="primary" @click="refStore.exportExcelRef.exportExcel(true)" size="small">下载 您可在下载的Excel模板中按照要求填写您的密码列表
</el-button> <el-text type="danger">密码名称为必填</el-text>
然后使用密码导入功能导入您的密码
</el-text>
</div> </div>
<el-divider class="function-line"/> </el-scrollbar>
<el-text type="info" tag="p" style="text-indent: 10px">
您可在下载的Excel模板中按照要求填写您的密码列表
<el-text type="danger">密码名称为必填</el-text>
然后使用密码导入功能导入您的密码
</el-text>
</div>
</el-scrollbar>
</el-tab-pane> </el-tab-pane>
<el-tab-pane> <el-tab-pane>
<template #label> <template #label>
@ -752,7 +755,7 @@ watch(() => settingStore.setting.generateRule, (newValue: GenerateRule) => {
</el-text> </el-text>
</template> </template>
<el-scrollbar :height="scrollbarHeight()"> <el-scrollbar :height="scrollbarHeight()">
<div style="padding: 0 15px"> <div style="padding: 0">
<About></About> <About></About>
</div> </div>
</el-scrollbar> </el-scrollbar>
@ -765,7 +768,7 @@ watch(() => settingStore.setting.generateRule, (newValue: GenerateRule) => {
</el-text> </el-text>
</template> </template>
<el-scrollbar :height="scrollbarHeight()"> <el-scrollbar :height="scrollbarHeight()">
<div style="padding: 0 15px"> <div style="padding: 0">
<SupportMe></SupportMe> <SupportMe></SupportMe>
</div> </div>
</el-scrollbar> </el-scrollbar>

View File

@ -93,7 +93,7 @@ const cardStyle = (password: Password) => {
<template> <template>
<EmptyList v-if="!passwordStore.visPasswordArray.length"></EmptyList> <EmptyList v-if="!passwordStore.visPasswordArray.length"></EmptyList>
<el-scrollbar <el-scrollbar
height="calc(100vh - 90px)" height="calc(100vh - 85px)"
v-if="passwordStore.visPasswordArray.length"> v-if="passwordStore.visPasswordArray.length">
<div <div
style="display: grid;padding: 6px;" style="display: grid;padding: 6px;"
@ -138,7 +138,7 @@ const cardStyle = (password: Password) => {
</li> </li>
<li v-if="password.address"> <li v-if="password.address">
<el-text class="password-field-name">地址:</el-text> <el-text class="password-field-name">地址:</el-text>
<el-text class="password-field-value" style="max-width: 20vw"> <el-text class="password-field-value">
<el-link v-if="isUrl(password.address)" type="primary" :href="password.address" target="_blank"> <el-link v-if="isUrl(password.address)" type="primary" :href="password.address" target="_blank">
{{ password.address }} {{ password.address }}
</el-link> </el-link>

View File

@ -97,7 +97,7 @@ const tableRowStyle = (data: { row: any, rowIndex: number }): CSSProperties => {
<el-table <el-table
:data="passwordStore.visPasswordArray" :data="passwordStore.visPasswordArray"
ref="passwordTableRef" ref="passwordTableRef"
height="calc(100vh - 120px)" height="calc(100vh - 120px)"
style="background-color: rgba(0,0,0,0);" style="background-color: rgba(0,0,0,0);"
:header-row-style="{'background-color':'rgba(0,0,0,0)'}" :header-row-style="{'background-color':'rgba(0,0,0,0)'}"
:header-cell-style="{'background-color':'rgba(0,0,0,0)'}" :header-cell-style="{'background-color':'rgba(0,0,0,0)'}"

View File

@ -0,0 +1,47 @@
<script setup lang="ts">
import {usePasswordStore} from "@/stores/PasswordStore.ts";
import {RespData} from "@/types";
import {useRouter} from "vue-router";
import {browserFingerprint, encryptAES} from "@/utils/security.ts";
import {DatabaseForAndroid} from "@/database/DatabaseForAndroid.ts";
const passwordStore = usePasswordStore()
const router = useRouter()
const useLocalLogin = async () => {
console.log('android 点击登录')
let database = new DatabaseForAndroid();
//
passwordStore.passwordManager.login(database).then((resp: RespData) => {
if (!resp.status) {
console.log('android 登录失败:', resp)
ElNotification.error({title: '登录失败', message: resp.message})
return
}
// 使
// sessionpinia
let fingerprint = browserFingerprint()
let loginForm = {loginType: 'android'}
let ciphertext = encryptAES(fingerprint, JSON.stringify(loginForm));
// sessionStorage
sessionStorage.setItem('loginForm', ciphertext)
console.log('android 登录 跳转首页')
router.push('/')
}).catch(() => null)
}
defineExpose({
useLocalLogin
})
</script>
<template>
</template>
<style scoped>
</style>

View File

@ -1,20 +1,31 @@
<!--登录类型选择组件--> <!--登录类型选择组件-->
<script setup lang="ts"> <script setup lang="ts">
import ElectronLoginForm from "@/components/login/ElectronLoginForm.vue"; import ElectronLoginForm from "@/components/login/ElectronLoginForm.vue";
import AndroidLoginForm from "@/components/login/AndroidLoginForm.vue";
const electronLoginFormRef = ref() const electronLoginFormRef = ref()
const androidLoginFormRef = ref()
const emits = defineEmits(['loginTypeChange']) const emits = defineEmits(['loginTypeChange'])
const isElectron = () => { const isElectron = () => {
return window.env && window.env.electron return !!window.electronAPI
}
const isAndroid = () => {
return !!window.androidAPI
} }
const electronStore = () => { const electronStore = () => {
console.log('使用本地存储electron') console.log('使用本地存储electron')
emits('loginTypeChange','electrons') emits('loginTypeChange','electron')
electronLoginFormRef.value.useLocalLogin() electronLoginFormRef.value.useLocalLogin()
} }
const androidStore = () => {
console.log('使用本地存储android')
emits('loginTypeChange','android')
androidLoginFormRef.value.useLocalLogin()
}
</script> </script>
<template> <template>
@ -45,7 +56,13 @@ const electronStore = () => {
</div> </div>
</el-col> </el-col>
<el-col :span="12" v-if="isElectron()"> <el-col :span="12" v-if="isElectron()">
<div class="login-type-item electron" @click="electronStore"> <div class="login-type-item local" @click="electronStore">
<img alt="" src="../../assets/images/login/local.png">
<div><el-text>本地存储</el-text></div>
</div>
</el-col>
<el-col :span="12" v-if="isAndroid()">
<div class="login-type-item local" @click="androidStore">
<img alt="" src="../../assets/images/login/local.png"> <img alt="" src="../../assets/images/login/local.png">
<div><el-text>本地存储</el-text></div> <div><el-text>本地存储</el-text></div>
</div> </div>
@ -60,6 +77,7 @@ const electronStore = () => {
</el-col> </el-col>
</el-row> </el-row>
<ElectronLoginForm ref="electronLoginFormRef"></ElectronLoginForm> <ElectronLoginForm ref="electronLoginFormRef"></ElectronLoginForm>
<AndroidLoginForm ref="androidLoginFormRef"></AndroidLoginForm>
</div> </div>
</template> </template>
@ -137,12 +155,5 @@ const electronStore = () => {
background: rgba(40, 193, 39, 0.4); background: rgba(40, 193, 39, 0.4);
box-shadow: 0 0 10px #bbb; box-shadow: 0 0 10px #bbb;
} }
.login-type-item.electron {
background: rgba(40, 193, 39, 0.3);
}
.login-type-item.electron:hover {
background: rgba(40, 193, 39, 0.4);
box-shadow: 0 0 10px #bbb;
}
</style> </style>

View File

@ -0,0 +1,80 @@
/**
* android存储引擎
*/
import {Database, RespData} from "@/types";
export class DatabaseForAndroid implements Database {
// 文件名称定义
private fileNames = {
store: 'store.json',
setting: 'setting.json',
}
// 登录并验证文件权限、初始化基本信息
async login(_form: any): Promise<RespData> {
console.log('使用android存储')
return Promise.resolve({status: true})
}
// 获取密码数据
async getStoreData(): Promise<string> {
return this.getFile(this.fileNames.store)
}
// 设置密码数据
async setStoreData(text: string) {
return this.uploadFile(this.fileNames.store, text)
}
// 删除密码数据
async deleteStoreData() {
return this.deleteFile(this.fileNames.store)
}
// 获取设置
async getSettingData(): Promise<string> {
return this.getFile(this.fileNames.setting)
}
// 更新设置
async setSettingData(text: string) {
return this.uploadFile(this.fileNames.setting, text)
}
// 删除设置数据
async deleteSettingData() {
return this.deleteFile(this.fileNames.setting)
}
// 获取android文件
private async getFile(fileName: string): Promise<string> {
console.log('获取android文件', fileName)
return new Promise(async (resolve) => {
let data = await window.androidAPI.getFile(fileName)
if (data) {
resolve(data);
} else {
resolve('')
}
})
}
// 上传android文件
private async uploadFile(fileName: string, content: string): Promise<RespData> {
console.log('上传android文件', fileName)
return new Promise(async (resolve) => {
window.androidAPI.uploadFile(fileName, content)
resolve({status: true})
})
}
// 删除android文件
private async deleteFile(fileName: string): Promise<RespData> {
console.log('删除android文件', fileName)
return new Promise((resolve) => {
window.androidAPI.deleteFile(fileName)
resolve({status: true})
})
}
}

View File

@ -8,6 +8,7 @@ import {useSettingStore} from "@/stores/SettingStore.ts";
import {useRefStore} from "@/stores/RefStore.ts"; import {useRefStore} from "@/stores/RefStore.ts";
import {DatabaseForPrivate} from "@/database/DatabaseForPrivate.ts"; import {DatabaseForPrivate} from "@/database/DatabaseForPrivate.ts";
import {DatabaseForElectron} from "@/database/DatabaseForElectron.ts"; import {DatabaseForElectron} from "@/database/DatabaseForElectron.ts";
import {DatabaseForAndroid} from "@/database/DatabaseForAndroid.ts";
export const useLoginStore = defineStore('loginStore', { export const useLoginStore = defineStore('loginStore', {
@ -30,8 +31,10 @@ export const useLoginStore = defineStore('loginStore', {
database = new DatabaseForCOS() database = new DatabaseForCOS()
} else if (loginForm.loginType === 'private') { } else if (loginForm.loginType === 'private') {
database = new DatabaseForPrivate() database = new DatabaseForPrivate()
} else if (loginForm.loginType === 'electron') { } else if (loginForm.loginType === 'electron') {
database = new DatabaseForElectron() database = new DatabaseForElectron()
} else if (loginForm.loginType === 'android') {
database = new DatabaseForAndroid()
} else { } else {
console.error('未知的登录类型,无法自动登录:', loginForm.loginType) console.error('未知的登录类型,无法自动登录:', loginForm.loginType)
resolve(false) resolve(false)

View File

@ -90,7 +90,7 @@ export const usePasswordStore = defineStore('passwordStore', {
return array return array
}, },
// 收藏的密码列表 // 收藏的密码列表
favoritePasswordArray(): Array<Password>{ favoritePasswordArray(): Array<Password> {
console.log('获取收藏的密码列表') console.log('获取收藏的密码列表')
let array = this.passwordArray.filter(p => p.favorite) let array = this.passwordArray.filter(p => p.favorite)
.sort((a: Password, b: Password) => { .sort((a: Password, b: Password) => {
@ -195,21 +195,18 @@ export const usePasswordStore = defineStore('passwordStore', {
let isDarkTheme = window.matchMedia("(prefers-color-scheme: dark)") let isDarkTheme = window.matchMedia("(prefers-color-scheme: dark)")
useDark().value = isDarkTheme.matches useDark().value = isDarkTheme.matches
this.topicMode = isDarkTheme.matches ? TopicMode.DARK : TopicMode.LIGHT this.topicMode = isDarkTheme.matches ? TopicMode.DARK : TopicMode.LIGHT
if (window.env && window.env.electron) { window.electronAPI?.setTopic('system');
window.electronAPI.setTopic('system'); window.androidAPI?.setTopic('system');
}
} else if (topic === TopicMode.DARK) { } else if (topic === TopicMode.DARK) {
useDark().value = true useDark().value = true
this.topicMode = TopicMode.DARK this.topicMode = TopicMode.DARK
if (window.env && window.env.electron) { window.electronAPI?.setTopic(this.topicMode);
window.electronAPI.setTopic(this.topicMode); window.androidAPI?.setTopic(this.topicMode);
}
} else if (topic === TopicMode.LIGHT) { } else if (topic === TopicMode.LIGHT) {
useDark().value = false useDark().value = false
this.topicMode = TopicMode.LIGHT this.topicMode = TopicMode.LIGHT
if (window.env && window.env.electron) { window.electronAPI?.setTopic(this.topicMode);
window.electronAPI.setTopic(this.topicMode); window.androidAPI?.setTopic(this.topicMode);
}
} }
}, },
// 全局加载 // 全局加载

View File

@ -31,13 +31,8 @@ interface FileSystemWritableFileStream extends WritableStream {
close(): Promise<void>; close(): Promise<void>;
} }
export interface Env {
electron: true
}
declare global { declare global {
interface Window { interface Window {
env: Env;
showOpenFilePicker: (options?: FilePickerOptions) => Promise<FileHandle[]>; showOpenFilePicker: (options?: FilePickerOptions) => Promise<FileHandle[]>;
showSaveFilePicker: (options?: SaveFilePickerOptions) => Promise<FileHandle>; showSaveFilePicker: (options?: SaveFilePickerOptions) => Promise<FileHandle>;
electronAPI: { electronAPI: {
@ -46,5 +41,11 @@ declare global {
deleteFile(fileName: string): Promise<RespData>; deleteFile(fileName: string): Promise<RespData>;
setTopic(topic: string): void; setTopic(topic: string): void;
} }
androidAPI: {
getFile(fileName: string): Promise<string>;
uploadFile(fileName: string, content: string): Promise<RespData>;
deleteFile(fileName: string): Promise<RespData>;
setTopic(topic: string): void;
}
} }
} }

View File

@ -75,7 +75,7 @@ if (['xs', 'sm'].includes(displaySize().value) && settingStore.setting.passwordD
</div> </div>
<!-- 手机版 --> <!-- 手机版 -->
<div v-else style="backdrop-filter: blur(50px);height: calc(100vh - 70px)" <div v-else style="backdrop-filter: blur(50px);height: 100vh"
:style="{'background-color': passwordStore.isDark?'rgba(0,0,0,0.4)':'rgba(255,255,255,0.4)'}"> :style="{'background-color': passwordStore.isDark?'rgba(0,0,0,0.4)':'rgba(255,255,255,0.4)'}">
<!-- 密码表头 --> <!-- 密码表头 -->
<PasswordHeader></PasswordHeader> <PasswordHeader></PasswordHeader>

View File

@ -12,7 +12,7 @@ const loginStep = ref(1)
const loginTypeChange = (type: string) => { const loginTypeChange = (type: string) => {
console.log('登录,选择了登录方式:', type) console.log('登录,选择了登录方式:', type)
loginStore.loginType = type; loginStore.loginType = type;
if (type === 'electron') { if (type === 'electron' || type === 'android') {
return return
} }