feat: 增加密码管理模块

This commit is contained in:
2025-06-11 23:43:40 +08:00
parent 38efe4483d
commit b5dd5703a5
15 changed files with 460 additions and 108 deletions

View File

@@ -0,0 +1,76 @@
<template>
<div class="password-card card-item">
<el-avatar class="avatar-info">{{ passwordItem.title.charAt(0) }}</el-avatar>
<div class="title-info">
<span>{{ passwordItem.title }}</span>
<span>{{ passwordItem.username }}</span>
</div>
<div class="button-info">
<el-button type="primary" :icon="Edit" circle
@click="editClick"/>
<el-button type="danger" :icon="DeleteFilled" circle
@click="deleteClick()" />
</div>
</div>
</template>
<script setup lang="ts">
import { Edit, DeleteFilled } from '@element-plus/icons-vue'
import { usePasswordStore } from '@/store/password.ts'
import { defineProps, onMounted, PropType } from 'vue'
import { commonElMessageBox } from 'vue3-common/utils/elUtil'
import { useRouter } from 'vue-router'
import { IPassword } from '@/types/password.ts'
const passwordStore = usePasswordStore()
const router = useRouter()
const props = defineProps({
passwordItem: {
required: true,
type: Object as PropType<IPassword>
}
})
onMounted(() => {
})
const editClick = async () => {
const id = props.passwordItem?.id
passwordStore.passwordApiType = 'UPDATE'
passwordStore.currentPassword = props.passwordItem as IPassword
await router.push({ name: 'MobileHomePasswordDetailId', params: { id } })
}
const deleteClick = () => {
passwordStore.passwordApiType = 'DELETE'
commonElMessageBox(`请确认是否删除该密码记录: ${props.passwordItem?.title}?`).then(() => {
passwordStore.handlePasswordApi(props.passwordItem as IPassword)
})
}
</script>
<style lang="scss">
.password-card {
padding: 10px;
display: grid;
grid-template-columns: 40px 1fr 80px;
align-items: center;
gap: 10px;
.avatar-info {
background-color: var(--el-color-primary);
}
.title-info {
display: flex;
flex-direction: column;
gap: 5px;
}
}
</style>

View File

@@ -0,0 +1,56 @@
<template>
<div class="mobile-common-query card-item">
<div class="query-item">
<span>分类</span>
<el-segmented v-model="passwordStore.queryInfo.category"
:options="queryCategoryList"
@change="passwordStore.queryPasswordList()"/>
</div>
<div class="query-item">
<span>所属人</span>
<el-segmented v-model="passwordStore.queryInfo.owner"
:options="queryOrderList"
@change="passwordStore.queryPasswordList()"/>
</div>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { usePasswordStore } from '@/store/password.ts'
import { ICategory } from '@/types'
const passwordStore = usePasswordStore()
const queryCategoryList = ref([] as ICategory[])
onMounted(async () => {
await passwordStore.queryPasswordCategory()
initCategoryList()
})
const queryOrderList = [
{
label: '哥哥',
value: '哥哥'
}, {
label: '宝宝',
value: '宝宝'
}
]
const initCategoryList = () => {
queryCategoryList.value = []
queryCategoryList.value.push({
label: '全部',
value: ''
})
passwordStore.categoryList.forEach((item) => {
queryCategoryList.value.push({
label: item,
value: item
})
})
}
</script>

View File

@@ -1,52 +0,0 @@
<template>
<div class="mobile-common-query card-item">
<div class="query-item">
<span>分类</span>
<el-segmented v-model="productStore.queryInfo.category"
:options="queryCategoryList"
@change="productStore.queryProductList()"/>
</div>
<div class="query-item">
<span>排序</span>
<el-segmented v-model="productStore.queryInfo.order"
:options="queryOrderList"
@change="productStore.queryProductList()"/>
</div>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { useProductStore } from '@/store/product.ts'
const productStore = useProductStore()
const queryCategoryList = ref([])
onMounted(async () => {
await productStore.queryProductCategory()
queryCategoryList.value = []
queryCategoryList.value.push({
label: '全部',
value: ''
})
productStore.categoryList.forEach((item) => {
queryCategoryList.value.push({
label: item,
value: item
})
})
})
const queryOrderList = [
{
label: '购买日期',
value: 'date'
}, {
label: '日均价格',
value: 'price'
}
]
</script>

View File

@@ -67,10 +67,8 @@
import { Edit, DeleteFilled } from '@element-plus/icons-vue'
import { useProductStore } from '@/store/product.ts'
import { defineProps, onMounted, PropType } from 'vue'
import { deepCopyObject } from 'vue3-common/utils/dataUtil'
import { commonElMessageBox } from 'vue3-common/utils/elUtil'
import { IProduct } from '@/types/product.ts'
import { isMobile } from 'vue3-common/utils/layoutUtil'
import { useRouter } from 'vue-router'
import { serviceFileRootPath } from '@/utils'
@@ -92,16 +90,8 @@ const editClick = async () => {
const id = props.productItem?.id
productStore.productApiType = 'UPDATE'
if (isMobile()) {
productStore.currentProduct = props.productItem as IProduct
await router.push({ name: 'MobileHomeProductDetailId', params: { id } })
} else {
productStore.productDialog = {
title: '编辑物品',
visible: true,
data: deepCopyObject(props.productItem)
}
}
productStore.currentProduct = props.productItem as IProduct
await router.push({ name: 'MobileHomeProductDetailId', params: { id } })
}
const deleteClick = () => {

View File

@@ -1,5 +1,5 @@
<template>
<div v-if="isMobile()" class="mobile-common-query card-item">
<div class="mobile-common-query card-item">
<div class="query-item">
<span>分类</span>
<el-segmented v-model="productStore.queryInfo.category"
@@ -14,37 +14,11 @@
@change="productStore.queryProductList()"/>
</div>
</div>
<div v-else class="common-query">
<el-select v-model="productStore.queryInfo.category"
placeholder="请选择类别" clearable
@change="productStore.queryProductList()"
style="width: 120px">
<el-option
v-for="(item, index) in productStore.categoryList"
:key="index" :label="item" :value="item"
/>
</el-select>
<el-select v-model="productStore.queryInfo.order"
placeholder="请选择排序" clearable
@change="productStore.queryProductList()"
style="width: 120px">
<el-option
v-for="(item, index) in queryOrderList"
:key="index" :label="item.label" :value="item.value"
/>
</el-select>
<el-button :icon="Refresh" circle @click="refreshClick"/>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { useProductStore } from '@/store/product.ts'
import { Refresh } from '@element-plus/icons-vue'
import { isMobile } from 'vue3-common/utils/layoutUtil'
import { ICategory } from '@/types'
const productStore = useProductStore()
@@ -79,12 +53,4 @@ const initCategoryList = () => {
})
})
}
const refreshClick = async () => {
productStore.queryInfo = {
category: '',
order: ''
}
await productStore.queryProductList()
}
</script>

View File

@@ -111,6 +111,12 @@
<span>清单</span>
</router-link>
<router-link v-if="path.includes('password')" to="/mobile-home/password/list"
@click="routerClick()" class="item">
<i class="fa-solid fa-list-ul"></i>
<span>清单</span>
</router-link>
<router-link v-if="path.includes('server')" to="/mobile-home/server/monitor"
@click="routerClick()" class="item">
<i class="fa-solid fa-desktop"></i>

94
src/store/password.ts Normal file
View File

@@ -0,0 +1,94 @@
import { defineStore } from 'pinia'
import type { IHandleApi } from 'vue3-common/types'
import { ElMessage } from 'element-plus'
import { IPassword } from '@/types/password.ts'
export const usePasswordStore = defineStore('password', {
state: () => ({
passwordList: [] as IPassword[],
queryInfo: {
category: '',
owner: '哥哥'
},
passwordApiType: 'ADD' as IHandleApi,
categoryList: [] as string[],
currentPassword: {
title: '',
website: '',
username: '',
password: '',
category: '',
owner: '',
remark: ''
} as IPassword,
isScrollEnd: false,
isRefresh: false
}),
actions: {
async queryPasswordList() {
const password: IPassword = {
id: 1,
category: '生活',
owner: '哥哥',
password: '123123',
remark: '12',
title: '支付宝',
username: '13814079242',
website: ''
}
const password2: IPassword = {
id: 2,
category: '生活',
owner: '哥哥',
password: '123123',
remark: '12',
title: '支付宝',
username: '13814079242',
website: ''
}
this.passwordList = []
this.passwordList.push(password)
this.passwordList.push(password2)
// this.productList = await queryProductListApi(this.queryInfo)
},
async queryPasswordCategory() {
// this.categoryList = await queryProductCategoryApi()
this.categoryList = ['生活', '工作']
},
async handlePasswordApi(password: IPassword) {
switch (this.passwordApiType) {
case 'ADD':
// await addProductApi(product)
ElMessage.success('新增密码记录成功')
break
case 'UPDATE':
// await updateProductApi(product.id as number, product)
ElMessage.success('更新密码记录成功')
break
case 'DELETE':
// await deleteProductApi(product.id as number)
ElMessage.success('删除密码记录成功')
break
default:
break
}
await this.refreshInfo()
},
async refreshInfo() {
await this.queryPasswordList()
this.isRefresh = !this.isRefresh
},
getEmptyPassword(): IPassword {
return {
category: '',
owner: '',
password: '',
remark: '',
title: '',
username: '',
website: ''
}
}
}
})

View File

@@ -1,5 +1,5 @@
import { defineStore } from 'pinia'
import type { IDialog, IHandleApi } from 'vue3-common/types'
import type { IHandleApi } from 'vue3-common/types'
import { ElMessage } from 'element-plus'
import { IProduct } from '@/types/product'
import {
@@ -30,11 +30,6 @@ export const useProductStore = defineStore('product', {
name: '',
imageUrl: ''
} as IProduct,
productDialog: {
visible: false,
title: '',
data: {}
} as IDialog<IProduct>,
productImagePath: ImagePathEnum.PRODUCT,
pageInfo: {
currentPage: 1,
@@ -68,7 +63,6 @@ export const useProductStore = defineStore('product', {
default:
break
}
this.productDialog.visible = false
await this.refreshInfo()
},
async refreshInfo() {

View File

@@ -82,4 +82,4 @@ span, input {
font-style: normal;
}
@import "//at.alicdn.com/t/c/font_4793264_rlx9eslbk6o.css";
@import "//at.alicdn.com/t/c/font_4793264_befv3sf3zdg.css";

31
src/types/password.ts Normal file
View File

@@ -0,0 +1,31 @@
export interface IPassword {
id?: number;
/**
* 标题
*/
title: string;
/**
* 网站
*/
website: string;
/**
* 账号
*/
username: string;
/**
* 密码
*/
password: string;
/**
* 分类
*/
category: string;
/**
* 所属人
*/
owner: string;
/**
* 备注
*/
remark: string;
}

View File

@@ -313,6 +313,21 @@ export const ledgerRules = {
]
}
export const passwordRules = {
title: [
{ required: true, message: '请输入标题', trigger: 'blur' }
],
username: [
{ required: true, message: '请输入账号', trigger: 'blur' }
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' }
],
owner: [
{ required: true, message: '请选择所属人', trigger: 'change' }
]
}
export const accountRules = {
accountName: [
{ required: true, message: '请输入账号名称', trigger: 'blur' }

View File

@@ -239,10 +239,20 @@ export const mobileHomeMeta: IRouteMetaConfig = {
title: '内容',
icon: ''
},
'/mobile-home/password': {
title: '密码',
icon: 'home-password',
order: 14,
redirect: '/mobile-home/password/list'
},
'/mobile-home/password/list': {
title: '清单',
icon: ''
},
'/mobile-home/profile': {
title: '个人信息',
icon: '',
order: 14,
order: 15,
hidden: true,
redirect: '/mobile-home/profile/index'
},

View File

@@ -0,0 +1,112 @@
<template>
<div class="mobile-password-detail common-mobile-view">
<el-form ref="formRef" :model="passwordStore.currentPassword"
:rules="rules" label-width="70px">
<el-form-item label="标题" prop="title">
<el-input v-model="passwordStore.currentPassword.title" autocomplete="off"
placeholder="请输入标题" clearable
show-word-limit maxlength="20">
</el-input>
</el-form-item>
<el-form-item label="账号" prop="username">
<el-input v-model="passwordStore.currentPassword.username" autocomplete="off"
placeholder="请输入账号" clearable
show-word-limit maxlength="20">
</el-input>
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input v-model="passwordStore.currentPassword.password" autocomplete="off"
placeholder="请输入密码" type="password" clearable
show-password show-word-limit maxlength="20">
</el-input>
</el-form-item>
<el-form-item label="网站">
<el-input v-model="passwordStore.currentPassword.website" autocomplete="off"
placeholder="请输入网站" clearable
type="textarea" :rows="3"
show-word-limit maxlength="20">
</el-input>
</el-form-item>
<el-form-item label="所属人" prop="owner">
<el-select v-model="passwordStore.currentPassword.owner"
placeholder="请选择所属人" clearable>
<el-option
v-for="(item, index) in ['哥哥', '宝宝']"
:key="index" :label="item" :value="item"
/>
</el-select>
</el-form-item>
<el-form-item label="分类">
<el-select v-model="passwordStore.currentPassword.category"
placeholder="请选择物品分类" clearable
:allow-create="true" :filterable="true">
<el-option
v-for="(item, index) in passwordStore.categoryList"
:key="index" :label="item" :value="item"
/>
</el-select>
</el-form-item>
<el-form-item label="备注">
<el-input v-model="passwordStore.currentPassword.remark" autocomplete="off"
placeholder="请输入备注" clearable
type="textarea" :rows="3"
show-word-limit maxlength="20">
</el-input>
</el-form-item>
</el-form>
<div class="button-container">
<el-button type="primary" @click="confirmClick">确认</el-button>
<el-button type="info" @click="cancelClick">取消</el-button>
</div>
</div>
</template>
<script lang="ts" setup>
import { usePasswordStore } from '@/store/password.ts'
import { useAppStore } from '@/store/app.ts'
import { FormRules } from 'element-plus'
import { passwordRules } from '@/utils/element/elRules'
import { onMounted, reactive, ref } from 'vue'
const formRef = ref()
const rules = reactive<FormRules>(passwordRules)
const passwordStore = usePasswordStore()
const appStore = useAppStore()
onMounted(async () => {
appStore.isShowMobileBack = true
})
/**
* 点击确认按钮
*/
const confirmClick = () => {
formRef.value.validate().then(async () => {
await passwordStore.handlePasswordApi(passwordStore.currentPassword)
history.back()
})
}
const cancelClick = () => {
history.back()
}
</script>
<style scoped lang="scss">
.mobile-password-detail {
display: flex;
flex-direction: column;
gap: 10px;
.button-container {
align-self: center;
}
}
</style>

View File

@@ -0,0 +1,50 @@
<template>
<div class="mobile-product-list common-mobile-view">
<password-query />
<el-empty v-if="passwordStore.passwordList.length === 0" description="暂无密码记录"/>
<div v-else class="product-list">
<password-card v-for="(item, index) in passwordStore.passwordList"
:key="index" :password-item="item"/>
</div>
<draggable-button @click="addClick"/>
</div>
</template>
<script lang="ts" setup>
import DraggableButton from '@/components/Mobile/DraggableButton.vue'
import PasswordQuery from '@/components/Home/Password/PasswordQuery.vue'
import PasswordCard from '@/components/Home/Password/PasswordCard.vue'
import { usePasswordStore } from '@/store/password.ts'
import { onMounted } from 'vue'
import { useRouter } from 'vue-router'
const passwordStore = usePasswordStore()
const router = useRouter()
onMounted(async () => {
await passwordStore.queryPasswordList()
})
const addClick = () => {
passwordStore.passwordApiType = 'ADD'
passwordStore.currentPassword = passwordStore.getEmptyPassword()
router.push({ name: 'MobileHomePasswordDetailId', params: { id: 0 } })
}
</script>
<style lang="scss">
.mobile-product-list {
display: flex;
flex-direction: column;
gap: 10px;
.product-list {
display: flex;
flex-direction: column;
gap: 10px;
}
}
</style>

View File

@@ -68,6 +68,10 @@
title="家庭账本" sub-title="守护家庭财富"
router="/mobile-home/ledger/list"/>
<module-card icon="iconfont icon-ai-password" color="#4361EE"
title="密码管家" sub-title="守护数字安全"
router="/mobile-home/password/list"/>
<module-card icon="iconfont icon-fuwuqi" color="#FF6B6B"
title="服务监控" sub-title="掌握系统健康"
router="/mobile-home/server/monitor"/>