适配安卓客户端

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">
<outputRelativeToContentRoot value="true" />
<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>
<module name="password-xl-service.main" />
</profile>

View File

@ -1,7 +1,7 @@
plugins {
id 'java'
id 'org.springframework.boot' version '3.3.1'
id 'io.spring.dependency-management' version '1.1.5'
id 'org.springframework.boot' version '3.3.2'
id 'io.spring.dependency-management' version '1.1.6'
id 'org.graalvm.buildtools.native' version '0.10.2'
}
@ -26,15 +26,18 @@ repositories {
dependencies {
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'
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'
}
tasks.bootBuildImage {
imageName = "${project.name}"
docker {
host = "////./pipe/dockerDesktopLinuxEngine"
}
}
tasks.register('printVersion') {

View File

@ -1,10 +1,5 @@
import {contextBridge, ipcRenderer} from 'electron'
// 设置环境变量
contextBridge.exposeInMainWorld('env', {
electron: true
})
// 开放给源码的API
contextBridge.exposeInMainWorld('electronAPI', {
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' {
export interface GlobalComponents {
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']
CancelAccount: typeof import('./components/common/setting/CancelAccount.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) => {
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('/')
return
}

View File

@ -46,9 +46,9 @@ const config = {
// /
pointCellSize: 3,
//
cellPointRadio: 0.3,
cellPointRadio: 0.33,
//
gesturePointColor: '#d5d5d5',
gesturePointColor: '#cacaca',
//
gesturePointErrorColor: '#F56C6C',
//
@ -336,6 +336,17 @@ const touchend = () => {
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 = () => {
if (!gestureDivRef.value || !canvasRef.value) return
@ -344,7 +355,8 @@ const initCanvas = () => {
console.log('画板大小:', size)
canvasRef.value.width = size
canvasRef.value.height = size
ctx = canvasRef.value.getContext('2d') as CanvasRenderingContext2D
ctx = createHDCanvas(canvasRef.value, size, size) as CanvasRenderingContext2D
let incrId = 0
allPointArray.length = 0

View File

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

View File

@ -9,17 +9,16 @@ import packageJson from '../../../../package.json'
</div>
<div style="padding: 10px;">
<el-descriptions :column="1">
<el-descriptions-item label="项目名称">{{ packageJson.name}}</el-descriptions-item>
<el-descriptions-item label="当前版本">{{ packageJson.version }}</el-descriptions-item>
<el-descriptions-item label="官方网站">
<el-descriptions-item label="项目名称:">{{ packageJson.name}}</el-descriptions-item>
<el-descriptions-item label="当前版本:">{{ packageJson.version }}</el-descriptions-item>
<el-descriptions-item label="官方网站:">
<el-link type="primary" target="_blank" :href="packageJson.homepage">{{ packageJson.homepage }}</el-link>
</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-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].weChat }}</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>
</div>
</template>

View File

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

View File

@ -321,6 +321,11 @@ watch(() => settingStore.setting.generateRule, (newValue: GenerateRule) => {
}, {
deep: true
})
const isAndroid = () => {
return !!window.androidAPI
}
</script>
<template>
@ -488,9 +493,8 @@ watch(() => settingStore.setting.generateRule, (newValue: GenerateRule) => {
<el-scrollbar :height="scrollbarHeight()">
<div class="function-div">
<div class="function-header" style="margin-bottom: 5px">
<el-text tag="b">默认排序规则</el-text>
<el-text tag="b">密码排序</el-text>
<div>
默认根据
<el-select v-model="settingStore.setting.sortField" size="small" style="width: 90px;">
<el-option value="addTime" 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.DESC" label="倒序"/>
</el-select>
排列
</div>
</div>
<el-divider class="function-line"/>
@ -524,7 +527,7 @@ watch(() => settingStore.setting.generateRule, (newValue: GenerateRule) => {
<div class="function-div">
<div class="function-header" style="margin-bottom: 5px">
<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="addTime" label="显示添加时间"/>
<el-option value="updateTime" label="显示修改时间"/>
@ -641,7 +644,7 @@ watch(() => settingStore.setting.generateRule, (newValue: GenerateRule) => {
</div>
</el-scrollbar>
</el-tab-pane>
<el-tab-pane>
<el-tab-pane v-if="!isAndroid()">
<template #label>
<el-text>
<span class="iconfont icon-recovery action-icon" style="color: #ffc400"></span>
@ -752,7 +755,7 @@ watch(() => settingStore.setting.generateRule, (newValue: GenerateRule) => {
</el-text>
</template>
<el-scrollbar :height="scrollbarHeight()">
<div style="padding: 0 15px">
<div style="padding: 0">
<About></About>
</div>
</el-scrollbar>
@ -765,7 +768,7 @@ watch(() => settingStore.setting.generateRule, (newValue: GenerateRule) => {
</el-text>
</template>
<el-scrollbar :height="scrollbarHeight()">
<div style="padding: 0 15px">
<div style="padding: 0">
<SupportMe></SupportMe>
</div>
</el-scrollbar>

View File

@ -93,7 +93,7 @@ const cardStyle = (password: Password) => {
<template>
<EmptyList v-if="!passwordStore.visPasswordArray.length"></EmptyList>
<el-scrollbar
height="calc(100vh - 90px)"
height="calc(100vh - 85px)"
v-if="passwordStore.visPasswordArray.length">
<div
style="display: grid;padding: 6px;"
@ -138,7 +138,7 @@ const cardStyle = (password: Password) => {
</li>
<li v-if="password.address">
<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">
{{ password.address }}
</el-link>

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">
import ElectronLoginForm from "@/components/login/ElectronLoginForm.vue";
import AndroidLoginForm from "@/components/login/AndroidLoginForm.vue";
const electronLoginFormRef = ref()
const androidLoginFormRef = ref()
const emits = defineEmits(['loginTypeChange'])
const isElectron = () => {
return window.env && window.env.electron
return !!window.electronAPI
}
const isAndroid = () => {
return !!window.androidAPI
}
const electronStore = () => {
console.log('使用本地存储electron')
emits('loginTypeChange','electrons')
emits('loginTypeChange','electron')
electronLoginFormRef.value.useLocalLogin()
}
const androidStore = () => {
console.log('使用本地存储android')
emits('loginTypeChange','android')
androidLoginFormRef.value.useLocalLogin()
}
</script>
<template>
@ -45,7 +56,13 @@ const electronStore = () => {
</div>
</el-col>
<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">
<div><el-text>本地存储</el-text></div>
</div>
@ -60,6 +77,7 @@ const electronStore = () => {
</el-col>
</el-row>
<ElectronLoginForm ref="electronLoginFormRef"></ElectronLoginForm>
<AndroidLoginForm ref="androidLoginFormRef"></AndroidLoginForm>
</div>
</template>
@ -137,12 +155,5 @@ const electronStore = () => {
background: rgba(40, 193, 39, 0.4);
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>

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 {DatabaseForPrivate} from "@/database/DatabaseForPrivate.ts";
import {DatabaseForElectron} from "@/database/DatabaseForElectron.ts";
import {DatabaseForAndroid} from "@/database/DatabaseForAndroid.ts";
export const useLoginStore = defineStore('loginStore', {
@ -32,6 +33,8 @@ export const useLoginStore = defineStore('loginStore', {
database = new DatabaseForPrivate()
} else if (loginForm.loginType === 'electron') {
database = new DatabaseForElectron()
} else if (loginForm.loginType === 'android') {
database = new DatabaseForAndroid()
} else {
console.error('未知的登录类型,无法自动登录:', loginForm.loginType)
resolve(false)

View File

@ -195,21 +195,18 @@ export const usePasswordStore = defineStore('passwordStore', {
let isDarkTheme = window.matchMedia("(prefers-color-scheme: dark)")
useDark().value = isDarkTheme.matches
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) {
useDark().value = true
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) {
useDark().value = false
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>;
}
export interface Env {
electron: true
}
declare global {
interface Window {
env: Env;
showOpenFilePicker: (options?: FilePickerOptions) => Promise<FileHandle[]>;
showSaveFilePicker: (options?: SaveFilePickerOptions) => Promise<FileHandle>;
electronAPI: {
@ -46,5 +41,11 @@ declare global {
deleteFile(fileName: string): Promise<RespData>;
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 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)'}">
<!-- 密码表头 -->
<PasswordHeader></PasswordHeader>

View File

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