189 lines
5.4 KiB
JavaScript
189 lines
5.4 KiB
JavaScript
import React, { useState, useEffect } from 'react';
|
||
import axios from 'axios';
|
||
import './PasswordManager.css';
|
||
import PasswordList from './PasswordList';
|
||
import PasswordForm from './PasswordForm';
|
||
import SearchBar from './SearchBar';
|
||
|
||
// API 地址配置:优先使用环境变量,否则根据构建模式自动选择
|
||
const API_BASE = process.env.REACT_APP_API_BASE ||
|
||
(process.env.NODE_ENV === 'production'
|
||
? 'https://keyvault.api.shumengya.top/api'
|
||
: 'http://localhost:8080/api');
|
||
|
||
const PasswordManager = () => {
|
||
const [entries, setEntries] = useState([]);
|
||
const [filteredEntries, setFilteredEntries] = useState([]);
|
||
const [searchKeyword, setSearchKeyword] = useState('');
|
||
const [editingEntry, setEditingEntry] = useState(null);
|
||
const [showForm, setShowForm] = useState(false);
|
||
const [loading, setLoading] = useState(true);
|
||
|
||
// PWA 安装提示
|
||
const [installPrompt, setInstallPrompt] = useState(null);
|
||
const [showInstallBanner, setShowInstallBanner] = useState(false);
|
||
|
||
useEffect(() => {
|
||
loadEntries();
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
filterEntries();
|
||
}, [searchKeyword, entries]);
|
||
|
||
// 监听 PWA 安装事件
|
||
useEffect(() => {
|
||
const handler = (e) => {
|
||
e.preventDefault();
|
||
setInstallPrompt(e);
|
||
setShowInstallBanner(true);
|
||
};
|
||
window.addEventListener('beforeinstallprompt', handler);
|
||
return () => window.removeEventListener('beforeinstallprompt', handler);
|
||
}, []);
|
||
|
||
const handleInstall = async () => {
|
||
if (!installPrompt) return;
|
||
installPrompt.prompt();
|
||
const { outcome } = await installPrompt.userChoice;
|
||
if (outcome === 'accepted') {
|
||
setShowInstallBanner(false);
|
||
setInstallPrompt(null);
|
||
}
|
||
};
|
||
|
||
const loadEntries = async () => {
|
||
try {
|
||
setLoading(true);
|
||
const response = await axios.get(`${API_BASE}/entries`);
|
||
setEntries(response.data.entries || []);
|
||
} catch (error) {
|
||
console.error('加载条目失败:', error);
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
const filterEntries = () => {
|
||
if (!searchKeyword.trim()) {
|
||
setFilteredEntries(entries);
|
||
return;
|
||
}
|
||
|
||
const keyword = searchKeyword.toLowerCase();
|
||
const filtered = entries.filter(entry =>
|
||
entry.account?.toLowerCase().includes(keyword) ||
|
||
entry.username?.toLowerCase().includes(keyword) ||
|
||
entry.email?.toLowerCase().includes(keyword) ||
|
||
entry.website?.toLowerCase().includes(keyword) ||
|
||
entry.officialName?.toLowerCase().includes(keyword) ||
|
||
(entry.software && entry.software.toLowerCase().includes(keyword)) ||
|
||
entry.tags?.toLowerCase().includes(keyword)
|
||
);
|
||
setFilteredEntries(filtered);
|
||
};
|
||
|
||
const handleAdd = () => {
|
||
setEditingEntry(null);
|
||
setShowForm(true);
|
||
};
|
||
|
||
const handleEdit = (entry) => {
|
||
setEditingEntry(entry);
|
||
setShowForm(true);
|
||
};
|
||
|
||
const handleDelete = async (id) => {
|
||
if (!window.confirm('确定要删除这条记录吗?')) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
await axios.delete(`${API_BASE}/entries/${id}`);
|
||
loadEntries();
|
||
} catch (error) {
|
||
console.error('删除失败:', error);
|
||
alert('删除失败,请重试');
|
||
}
|
||
};
|
||
|
||
const handleSave = async (entryData) => {
|
||
try {
|
||
if (editingEntry) {
|
||
await axios.put(`${API_BASE}/entries`, { ...entryData, id: editingEntry.id });
|
||
} else {
|
||
await axios.post(`${API_BASE}/entries`, entryData);
|
||
}
|
||
setShowForm(false);
|
||
setEditingEntry(null);
|
||
loadEntries();
|
||
} catch (error) {
|
||
console.error('保存失败:', error);
|
||
alert('保存失败,请重试');
|
||
}
|
||
};
|
||
|
||
const handleCancel = () => {
|
||
setShowForm(false);
|
||
setEditingEntry(null);
|
||
};
|
||
|
||
if (loading) {
|
||
return (
|
||
<div className="manager-loading">
|
||
<div className="loading-spinner"></div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div className="password-manager">
|
||
{/* PWA 安装横幅 */}
|
||
{showInstallBanner && (
|
||
<div className="pwa-install-banner">
|
||
<span className="pwa-banner-icon">🌱</span>
|
||
<span className="pwa-banner-text">将萌芽密码添加到桌面,随时快速访问</span>
|
||
<button className="pwa-install-btn" onClick={handleInstall}>添加到桌面</button>
|
||
<button className="pwa-dismiss-btn" onClick={() => setShowInstallBanner(false)}>✕</button>
|
||
</div>
|
||
)}
|
||
<nav className="manager-nav">
|
||
<div className="nav-content">
|
||
<img
|
||
src={`${process.env.PUBLIC_URL}/logo.png`}
|
||
alt="Logo"
|
||
className="nav-logo"
|
||
/>
|
||
<h1 className="nav-title">萌芽密码管理器</h1>
|
||
</div>
|
||
</nav>
|
||
<div className="manager-header">
|
||
<button className="add-button" onClick={handleAdd}>
|
||
+ 添加密码
|
||
</button>
|
||
</div>
|
||
|
||
<SearchBar
|
||
keyword={searchKeyword}
|
||
onKeywordChange={setSearchKeyword}
|
||
/>
|
||
|
||
<PasswordList
|
||
entries={filteredEntries}
|
||
onEdit={handleEdit}
|
||
onDelete={handleDelete}
|
||
/>
|
||
|
||
{showForm && (
|
||
<PasswordForm
|
||
entry={editingEntry}
|
||
onSave={handleSave}
|
||
onCancel={handleCancel}
|
||
/>
|
||
)}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default PasswordManager;
|