chore: sync project updates
This commit is contained in:
@@ -1,155 +1,188 @@
|
||||
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);
|
||||
|
||||
useEffect(() => {
|
||||
loadEntries();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
filterEntries();
|
||||
}, [searchKeyword, entries]);
|
||||
|
||||
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.accountType?.toLowerCase().includes(keyword) ||
|
||||
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">
|
||||
<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;
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user