支持自定义字段、维护捐赠者信息

This commit is contained in:
huangyp 2024-10-11 14:04:38 +08:00
parent dc390458b3
commit dd4ee2c326
19 changed files with 152 additions and 51 deletions

2
.idea/misc.xml generated
View File

@ -4,7 +4,7 @@
<component name="FrameworkDetectionExcludesConfiguration">
<file type="web" url="file://$PROJECT_DIR$/password-xl-service" />
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_21_PREVIEW" project-jdk-name="graalvm-21" project-jdk-type="JavaSDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_21_PREVIEW" project-jdk-name="21" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

1
.idea/modules.xml generated
View File

@ -3,7 +3,6 @@
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/password-xl.iml" filepath="$PROJECT_DIR$/password-xl.iml" />
<module fileurl="file://$PROJECT_DIR$/.idea/modules/password-xl-service.main.iml" filepath="$PROJECT_DIR$/.idea/modules/password-xl-service.main.iml" />
<module fileurl="file://$PROJECT_DIR$/password-xl-web.iml" filepath="$PROJECT_DIR$/password-xl-web.iml" />
</modules>
</component>

View File

@ -48,10 +48,10 @@
### 未来计划
- [ ] 浏览器插件,自动找出密码并填入(安全问题?)
- [ ] 安卓客户端、安卓密码管理器、指纹
- [ ] 苹果客户端、密码管理器
- [ ] PC客户端
- [ ] Linux 客户端
- [x] 安卓客户端
- [x] 苹果客户端
- [x] PC客户端
- [x] Linux 客户端
- [ ] 小程序客户端
- [ ] 主题功能,默认,小棕熊,可爱,可换背景
- [ ] 密钥文件存储
@ -68,16 +68,16 @@
- [x] 超时锁定 1
- [x] 回收站 1
- [x] 密码备份 2
- [ ] 分词搜索 1
- [ ] 分词搜索,近义词搜索(实现难度较大,暂不考虑) 1
- [x] 手势密码
- [ ] 自定义字段 1
- [x] 自定义字段 1
- [x] 注销账户 1
- [x] 本地存储 3
- [ ] 漫游式引导 2
- [x] 漫游式引导 2
- [ ] 阿里云注册指引、腾讯云注册指引 10
- [ ] 私有部署前端教程、私有部署后端教程、打包代码 10
- [ ] 部署服务,设置演示账号 4
- [ ] 整体说明文档 10
- [ ] 私有服务 6
- [ ] 官方服务 12
- [x] 私有部署前端教程、私有部署后端教程 10
- [x] 部署服务,设置演示账号 4
- [x] 整体说明文档 10
- [x] 私有服务 6
- [x] 官方服务 12
- [ ] 密码防丢 1

View File

@ -1,8 +1,8 @@
@font-face {
font-family: "iconfont"; /* Project id 4580024 */
src: url('iconfont.woff2?t=1720790383207') format('woff2'),
url('iconfont.woff?t=1720790383207') format('woff'),
url('iconfont.ttf?t=1720790383207') format('truetype');
src: url('iconfont.woff2?t=1728618428208') format('woff2'),
url('iconfont.woff?t=1728618428208') format('woff'),
url('iconfont.ttf?t=1728618428208') format('truetype');
}
.iconfont {
@ -13,6 +13,10 @@
-moz-osx-font-smoothing: grayscale;
}
.icon-clean:before {
content: "\e626";
}
.icon-money:before {
content: "\e643";
}

View File

@ -1,6 +1,13 @@
<!--关于项目-->
<script setup lang="ts">
import packageJson from '../../../../package.json'
const newFunctions = ref([
'密码支持自定义信息',
'导出、导入Excel',
'支持Windows、Android客户端',
'支持停用动态壁纸',
])
</script>
<template>
@ -21,6 +28,14 @@ import packageJson from '../../../../package.json'
<el-descriptions-item label="作者微信:">{{ packageJson.contributors[0].weChat }}</el-descriptions-item>
</el-descriptions>
</div>
<div>
<h2 style="margin-top: 0">最近更新</h2>
</div>
<div>
<ol style="padding-left: 25px;line-height: 26px">
<li v-for="item in newFunctions">{{ item }}</li>
</ol>
</div>
</template>
<style>

View File

@ -8,7 +8,7 @@ import {useRefStore} from "@/stores/RefStore.ts";
const passwordStore = usePasswordStore()
const refStore = useRefStore()
const headers = ['名称', '地址', '用户名', '密码', '标签', '备注', '创建时间', '收藏'];
const headers = ['名称', '地址', '用户名', '密码', '标签', '备注', '创建时间', '收藏', '自定义信息'];
//
const exportExcel = async (downloadTemplate: boolean) => {
@ -40,7 +40,16 @@ const startExportExcel = async (downloadTemplate: boolean) => {
labels = getLabelNamesByIds(item.labels)
}
let createTime = formatterDate(item.addTime, 'YYYY-MM-DD HH:mm:ss');
return [item.title, item.address, item.username, item.password, labels, item.remark, createTime, item.favorite ? '是' : '否'];
let customFieldStr = ''
if (item.customFields && item.customFields.length) {
for (let i = 0; i < item.customFields.length; i++) {
customFieldStr += item.customFields[i].key + '' + item.customFields[i].val
if (i !== item.customFields.length - 1) {
customFieldStr += '\r\n'
}
}
}
return [item.title, item.address, item.username, item.password, labels, item.remark, createTime, item.favorite ? '是' : '否', customFieldStr];
});
console.log('导出密码,导出数据:', contents.length)
//
@ -79,6 +88,9 @@ const startExportExcel = async (downloadTemplate: boolean) => {
case 8: //
worksheet.getColumn(index).width = 10;
break;
case 9: //
worksheet.getColumn(index).width = 20;
break;
default:
break;
}

View File

@ -1,7 +1,15 @@
<script setup lang="ts">
import ExcelJS from 'exceljs';
import {Label, Password, PasswordStatus} from "@/types";
import {comparePassword, displaySize, formatterDate, getPasswordLabelNames, incrId, parseDate, parseLabels} from "@/utils/global.ts";
import {
comparePassword,
displaySize,
formatterDate,
getPasswordLabelNames,
incrId,
parseDate,
parseLabels
} from "@/utils/global.ts";
import {Buffer} from "buffer";
import {usePasswordStore} from "@/stores/PasswordStore.ts";
import {useRefStore} from "@/stores/RefStore.ts";
@ -110,6 +118,7 @@ const importData = async (buffer: Buffer) => {
addTime: headers.indexOf('创建时间'),
favorite: headers.indexOf('收藏'),
labels: headers.indexOf('标签'),
customFields: headers.indexOf('自定义信息'),
};
if (!columnIndex.title) {
@ -139,6 +148,7 @@ const importData = async (buffer: Buffer) => {
let addTime = columnIndex.addTime !== -1 ? row.getCell(columnIndex.addTime) : null
let labels = columnIndex.labels !== -1 ? row.getCell(columnIndex.labels) : null
let favorite = columnIndex.favorite !== -1 ? row.getCell(columnIndex.favorite) : null
let customFields = columnIndex.customFields !== -1 ? row.getCell(columnIndex.customFields) : null
//
let favoriteValue = favorite && favorite.value === '是'
@ -176,12 +186,23 @@ const importData = async (buffer: Buffer) => {
deleteTime: 0,
favoriteTime: favoriteValue ? Date.now() : 0,
favorite: favoriteValue,
customFields: {},
customFields: [],
labels: passwordLabel,
status: PasswordStatus.NORMAL,
bgColor: '',
};
if (customFields && customFields.value) {
let fieldStr = customFields.value.split('\r\n');
for (let j = 0; j < fieldStr.length; j++) {
let field = fieldStr[j].split("")
password.customFields.push({
key: field[0],
val: field.length > 1 ? field[1] : ''
})
}
}
//
importPasswords.value.push(password);
}
@ -404,6 +425,13 @@ defineExpose({
</el-tag>
</template>
</el-table-column>
<el-table-column label="自定义信息" min-width="150px" prop="customFields">
<template #default="scope">
<el-tag v-for="field in scope.row.customFields" class="table-label">
{{ field.key }}{{ field.val }}
</el-tag>
</template>
</el-table-column>
</el-table>
<template #footer>
<el-tooltip content="请选择要导入的密码" placement="top" :disabled="!importBtnDis()">

View File

@ -680,7 +680,7 @@ const isAndroid = () => {
<div class="function-div">
<div class="function-header" style="margin-bottom: 5px">
<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">导出 Excel
</el-button>
</div>
<el-divider class="function-line"/>
@ -691,7 +691,7 @@ const isAndroid = () => {
<div class="function-div">
<div class="function-header" style="margin-bottom: 5px">
<el-text tag="b">导入密码</el-text>
<el-button plain type="primary" @click="refStore.importExcelRef.importExcel" size="small">导入
<el-button plain type="primary" @click="refStore.importExcelRef.importExcel" size="small">导入 Excel
</el-button>
</div>
<el-divider class="function-line"/>

View File

@ -3,6 +3,7 @@
import packageJson from '../../../../package.json'
const donateArray = [
{name: 'Seeking dream', money: 10, time: '2024-10-11'},
{name: 'Ching', money: 10, time: '2024-07-13'},
{name: '李春雨', money: 5, time: '2024-07-12'},
{name: '张博文', money: 50, time: '2024-07-12'},
@ -66,20 +67,20 @@ const donateArray = [
</li>
</ul>
<el-row>
<el-col :xs="{span:24}" :sm="{span:24}" :md="{span:9}">
<div style="text-align: center;position: relative;left: -10px">
<el-col :xs="{span:24}" :sm="{span:24}" :md="{span:9}" style="text-align: center">
<div style="text-align: center;">
<h3 style="margin-top: 4px">微信赞赏码</h3>
<div style="padding: 5px 20px;text-align: center">
<img alt="" style="width: 80%;border-radius: 50%" src="../../../assets/images/donate.png">
<img alt="" style="width: 80%;max-width: 300px;border-radius: 50%" src="../../../assets/images/donate.png">
</div>
</div>
</el-col>
<el-col :xs="{span:24}" :sm="{span:24}" :md="{span:15}" style="text-align: center">
<el-col :xs="{span:24}" :sm="{span:24}" :md="{span:15}" style="text-align: center;padding-right: 10px">
<h3 style="margin-top: 4px">捐赠者列表</h3>
<el-table :data="donateArray" border stripe>
<el-table-column label="名称" prop="name"></el-table-column>
<el-table-column label="时间" min-width="120px" prop="time"></el-table-column>
<el-table-column label="金额" min-width="80px" prop="money"></el-table-column>
<el-table-column label="时间" min-width="60px" prop="time"></el-table-column>
<el-table-column label="金额" min-width="40px" prop="money"></el-table-column>
</el-table>
</el-col>
</el-row>

View File

@ -88,6 +88,14 @@ const cardStyle = (password: Password) => {
};
}
const getBackStype = () =>{
if(settingStore.setting.dynamicBackground){
return {'background-color':passwordStore.isDark?'rgba(0,0,0,0.1)':'rgba(255,255,255,0.1)'}
}else{
return {'background-color':passwordStore.isDark?'rgba(0,0,0,1)':'rgba(255,255,255,1)'}
}
}
</script>
<template>
@ -99,7 +107,7 @@ const cardStyle = (password: Password) => {
style="display: grid;padding: 6px;"
:style="{'grid-template-columns':'repeat('+getRowCount()+', 1fr)'}">
<div v-for="password in passwordStore.visPasswordArray">
<el-card body-style="height: 100%;" :style="{'background-color':passwordStore.isDark?'rgba(0,0,0,0.1)':'rgba(255,255,255,0.1)'}" class="password-card">
<el-card body-style="height: 100%;" :style="getBackStype" class="password-card">
<template #header>
<div class="password-header-div" :style="cardStyle(password)">
<div>
@ -131,7 +139,7 @@ const cardStyle = (password: Password) => {
&& !password.password
&& !password.remark
&& !password.labels.length
&& !(password.customFields && Object.keys(password.customFields).length > 0)">
&& !(password.customFields && password.customFields.length > 0)">
<el-text style="margin: 20px 0">
空空如也
</el-text>
@ -187,11 +195,13 @@ const cardStyle = (password: Password) => {
</el-text>
<div class="clear"></div>
</li>
<li v-for="field in password.customFields">
<el-text class="password-field-name">{{ field }}:</el-text>
<el-text class="password-field-value">{{ password.customFields[field] }}</el-text>
<div class="clear"></div>
</li>
<template v-for="field in password.customFields">
<li v-if="field.key || field.val">
<el-text class="password-field-name">{{ field.key }}:</el-text>
<el-text class="password-field-value">{{ field.val }}</el-text>
<div class="clear"></div>
</li>
</template>
</ul>
<template #footer>
<div style="display: flex;justify-content: space-between">
@ -227,7 +237,6 @@ const cardStyle = (password: Password) => {
display: flex;
flex-direction: column;
height: calc(100% - 12px);
background-color: rgba(255, 255, 255, 0.6);
}
.password-strength {

View File

@ -44,7 +44,7 @@ const initPasswordForm = (): Password => {
//
favorite: false,
//
customFields: {},
customFields: [],
// id
labels: [],
// or
@ -165,6 +165,13 @@ const getDefaultExpandedKeys = (): number[] => {
return passwordStore.labelArray.map(label => label.id);
}
const addField = () => {
passwordForm.value.customFields.push({
key: '',
val: '',
})
}
//
const savePassword = async (passwordFormFormRef: any) => {
console.log('保存密码')
@ -217,6 +224,10 @@ const handleKeyDown = (event: KeyboardEvent) => {
}
};
const delField = (index) => {
passwordForm.value.customFields.splice(index, 1)
}
onMounted(() => {
document.addEventListener('keydown', handleKeyDown);
});
@ -247,7 +258,7 @@ onBeforeUnmount(() => {
<el-input :ref="(el: any) => refStore.passwordFormTitleRef = el" v-model="passwordForm.title" autocomplete="new-password" clearable></el-input>
</el-form-item>
<el-form-item label="地址" prop="address">
<el-input v-model="passwordForm.address" autocomplete="new-password" clearable></el-input>
<el-input placeholder="https://" v-model="passwordForm.address" autocomplete="new-password" clearable></el-input>
</el-form-item>
<el-form-item label="用户名" prop="username">
<el-autocomplete
@ -361,10 +372,22 @@ onBeforeUnmount(() => {
class="bg-color-item">
<span class="iconfont icon-check-mark" style="font-size: 14px" v-show="passwordForm.bgColor === color"></span>
</div>
</el-form-item>
<el-form-item label="自定义">
<el-card v-if="passwordForm.customFields && passwordForm.customFields.length > 0">
<el-row v-for="(field,index) in passwordForm.customFields"
:style="{'margin-bottom': index !== passwordForm.customFields.length - 1?'15px':'0'}">
<el-input style="width: 30%" v-model="field.key" placeholder="名称"></el-input>
<el-input style="width: 54%;margin-left: 3%" v-model="field.val" placeholder="内容"></el-input>
<el-button style="width: 10%;margin-left: 3%;" @click="delField(index)" type="danger" plain>
<span class="iconfont icon-clean"></span>
</el-button>
</el-row>
</el-card>
<el-button v-else @click="addField()" type="primary" plain>添加自定义信息</el-button>
</el-form-item>
</el-form>
<div style="text-align: right">
<div style="display: flex;justify-content: end">
<el-button @click="savePassword(refStore.passwordFormFormRef)" :ref="(el: any) => refStore.passwordFormSaveBtnRef = el" type="primary">保存</el-button>
</div>
</el-drawer>
@ -446,4 +469,5 @@ onBeforeUnmount(() => {
text-align: center;
line-height: 25px;
}
</style>

View File

@ -133,7 +133,7 @@ export class PasswordManagerImpl implements PasswordManager {
deleteTime: 0,
favoriteTime: 0,
favorite: true,
customFields: {},
customFields: [],
labels: [label.id],
status: PasswordStatus.NORMAL,
bgColor: ''

View File

@ -128,9 +128,14 @@ export const usePasswordStore = defineStore('passwordStore', {
password.address,
password.username,
password.password,
password.remark,
...Object.entries(password.customFields).flat()
password.remark
].filter(Boolean);
if (password.customFields) {
for (let i = 0; i < password.customFields.length; i++) {
searchFields.push(password.customFields[i].key);
searchFields.push(password.customFields[i].val);
}
}
for (const field of searchFields) {
if (searchStr(this.filterCondition.searchText.trim(), field)) {
textSearchResult = true

View File

@ -1,6 +1,12 @@
// 密码
import {Ref} from "vue";
// 自定义字段
export interface CustomField {
key: string;
val: string;
}
// 密码
export interface Password {
id: number,
@ -14,7 +20,7 @@ export interface Password {
deleteTime: number,
favoriteTime: number,
favorite: boolean,
customFields: { [key: string]: string },
customFields: CustomField[],
labels: Array<number>,
status: PasswordStatus,
bgColor: string,
@ -77,7 +83,7 @@ export interface Setting {
autoGeneratePassword: boolean,// 添加密码时是否默认自动一次
generateRule: GenerateRule, // 密码生成规则
easyConfuseChat: string,// 易混淆字符
customFields: Array<string>, // 默认自定义字段
customFields: CustomField[], // 默认自定义字段
timeoutLock: number, // 超时锁定(秒)
passwordDisplayMode: PasswordDisplayMode, // 密码展示方式
autoLogin: boolean,// 记住登录信息

View File

@ -49,8 +49,6 @@ export async function copyText(text: string, silent: boolean = false) {
// 文本搜索
export const searchStr = (searchText: string, value: string): boolean => {
if (!value) return false;
const lowerSearchText = searchText.toLowerCase();
const lowerValue = value.toLowerCase();
@ -170,8 +168,9 @@ export const sharePassword = (password: Password) => {
}
// 自定义字段
if (password.customFields) {
for (let field in password.customFields) {
text += field + ': ' + password.customFields[field] + '\r\n'
for (let i = 0; i < password.customFields.length; i++) {
let field = password.customFields[i];
text += field.key + ': ' + field.val + '\r\n'
}
}
if (password.remark) {

View File

@ -131,7 +131,6 @@ if (['xs', 'sm'].includes(displaySize().value) && settingStore.setting.passwordD
.password-card {
margin: 10px;
height: calc(100vh - 24px);
background-color: rgba(255, 255, 255, 0.4);
backdrop-filter: blur(50px);
}