Files
UserCard/manage-users.php
2026-02-23 21:07:09 +08:00

781 lines
25 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* 用户卡片管理页面
*/
// 注意在检查POST请求时我们需要尽早处理并退出避免输出其他内容
// 首先检查是否为POST请求
if ($_SERVER['REQUEST_METHOD'] == 'POST' && isset($_POST['action'])) {
// 包含必要的文件但不要输出任何内容
include 'common.php';
// 检查权限
if (!defined('__TYPECHO_ROOT_DIR__')) exit;
$user = Typecho_Widget::widget('Widget_User');
if (!$user->hasLogin() || !$user->pass('administrator', true)) {
header('Content-Type: application/json');
echo json_encode(['success' => false, 'message' => '无权限操作']);
exit;
}
// 设置响应头为JSON
header('Content-Type: application/json; charset=utf-8');
if ($_POST['action'] == 'save_rss' && isset($_POST['uid'], $_POST['user_feed'])) {
$uid = intval($_POST['uid']);
$user_feed = trim($_POST['user_feed']);
$user_url = isset($_POST['user_url']) ? trim($_POST['user_url']) : '';
try {
$db = Typecho_Db::get();
$db->query($db->update('table.users')->rows(array(
'user_feed' => $user_feed,
'url' => $user_url
))->where('uid = ?', $uid));
// 清除该用户的RSS缓存
$cacheKey = 'usercard_rss_' . md5($user_feed);
$cacheFile = dirname(__FILE__) . '/cache/' . $cacheKey . '.json';
if (file_exists($cacheFile)) {
@unlink($cacheFile);
}
echo json_encode(['success' => true, 'message' => '用户信息已保存!']);
exit;
} catch (Exception $e) {
echo json_encode(['success' => false, 'message' => '保存失败:' . $e->getMessage()]);
exit;
}
} elseif ($_POST['action'] == 'batch_save') {
// 批量保存
try {
$db = Typecho_Db::get();
foreach ($_POST['user_feed'] as $uid => $user_feed) {
$uid = intval($uid);
$user_feed = trim($user_feed);
$user_url = isset($_POST['user_url'][$uid]) ? trim($_POST['user_url'][$uid]) : '';
$db->query($db->update('table.users')->rows(array(
'user_feed' => $user_feed,
'url' => $user_url
))->where('uid = ?', $uid));
// 清除该用户的RSS缓存
if (!empty($user_feed)) {
$cacheKey = 'usercard_rss_' . md5($user_feed);
$cacheFile = dirname(__FILE__) . '/cache/' . $cacheKey . '.json';
if (file_exists($cacheFile)) {
@unlink($cacheFile);
}
}
}
echo json_encode(['success' => true, 'message' => '批量保存成功!']);
exit;
} catch (Exception $e) {
echo json_encode(['success' => false, 'message' => '批量保存失败:' . $e->getMessage()]);
exit;
}
}
// 如果不是上述两种action返回错误
echo json_encode(['success' => false, 'message' => '无效的操作']);
exit;
}
// 正常页面加载非AJAX请求
include 'common.php';
include 'header.php';
include 'menu.php';
if (!defined('__TYPECHO_ROOT_DIR__')) exit;
// 检查权限
$user = Typecho_Widget::widget('Widget_User');
if (!$user->hasLogin() || !$user->pass('administrator', true)) {
exit;
}
// 获取所有用户
$db = Typecho_Db::get();
$users = $db->fetchAll($db->select('uid', 'name', 'screenName', 'url', 'user_feed')
->from('table.users')
->order('uid', Typecho_Db::SORT_ASC));
// 统计信息
$totalUsers = count($users);
$withRss = 0;
$withoutRss = 0;
$withWebsite = 0;
foreach ($users as $user) {
if (!empty($user['user_feed'])) {
$withRss++;
} else {
$withoutRss++;
}
if (!empty($user['url'])) {
$withWebsite++;
}
}
// 获取用户头像首字母函数
function getUserInitial($name) {
if (empty($name)) {
return 'U';
}
// 去除首尾空格
$name = trim($name);
// 如果是中文,获取第一个汉字
if (preg_match('/^[\x{4e00}-\x{9fa5}]/u', $name)) {
// 获取第一个字符(支持中文字符)
return mb_substr($name, 0, 1, 'UTF-8');
} else {
// 非中文,获取第一个字母或数字
$firstChar = substr($name, 0, 1);
// 如果是字母,转换为大写
if (ctype_alpha($firstChar)) {
return strtoupper($firstChar);
}
// 如果是数字,直接返回
if (ctype_digit($firstChar)) {
return $firstChar;
}
// 其他字符,返回第一个字符的大写形式
return strtoupper($firstChar);
}
}
?>
<div class="main">
<div class="body container">
<!-- 弹窗容器 -->
<div id="toast-container" style="position: fixed; top: 20px; right: 20px; z-index: 9999;"></div>
<style>
/* 弹窗样式 */
.toast {
background: #43e97b;
color: white;
padding: 12px 24px;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(67, 233, 123, 0.3);
margin-bottom: 10px;
animation: slideInRight 0.3s ease, fadeOut 0.3s ease 2.7s forwards;
max-width: 300px;
word-wrap: break-word;
}
@keyframes slideInRight {
from {
opacity: 0;
transform: translateX(100%);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes fadeOut {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
.user-mgmt-section {
background: #fff;
border-radius: 10px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
}
.user-mgmt-title {
font-size: 16px;
font-weight: 600;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid #eee;
color: #333;
}
.user-stats-cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 15px;
}
.user-stat-card {
background: #f8f9fa;
border-radius: 8px;
padding: 20px 15px;
text-align: center;
transition: all 0.3s ease;
border: 1px solid #e9ecef;
}
.user-stat-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.user-stat-value {
font-size: 24px;
font-weight: 700;
margin: 0 0 10px 0;
}
.user-stat-label {
font-size: 12px;
color: #666;
margin: 0;
}
.user-stat-card.total .user-stat-value { color: #667eea; }
.user-stat-card.with-rss .user-stat-value { color: #43e97b; }
.user-stat-card.without-rss .user-stat-value { color: #fa709a; }
.user-stat-card.with-website .user-stat-value { color: #4facfe; }
.user-table-container {
overflow-x: auto;
}
.user-mgmt-table {
width: 100%;
border-collapse: collapse;
background: #fff;
}
.user-mgmt-table thead {
background: #f8f9fa;
}
.user-mgmt-table th {
padding: 15px;
text-align: left;
font-weight: 600;
color: #333;
border-bottom: 2px solid #e9ecef;
white-space: nowrap;
}
.user-mgmt-table td {
padding: 15px;
border-bottom: 1px solid #e9ecef;
vertical-align: middle;
}
.user-mgmt-table tbody tr:hover {
background: #f8f9fa;
}
.user-info {
display: flex;
align-items: center;
gap: 10px;
}
.user-avatar {
width: 36px;
height: 36px;
border-radius: 50%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: bold;
font-size: 16px;
flex-shrink: 0;
font-family: "Microsoft YaHei", "Segoe UI", Arial, sans-serif;
}
.user-details {
flex: 1;
min-width: 0;
}
.user-name {
font-weight: 600;
color: #333;
margin: 0 0 3px 0;
font-size: 14px;
}
.user-id {
font-size: 12px;
color: #999;
margin: 0;
}
.website-input-container,
.rss-input-container {
position: relative;
}
.url-input,
.rss-input {
width: 100%;
padding: 10px 12px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 13px;
transition: all 0.2s ease;
background: #fff;
}
.url-input:focus,
.rss-input:focus {
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
outline: none;
}
.url-input.has-value,
.rss-input.has-value {
border-color: #43e97b;
background: #f8fff9;
}
.url-input.empty,
.rss-input.empty {
border-color: #ddd;
background: #fff;
}
.input-status {
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
width: 8px;
height: 8px;
border-radius: 50%;
background: #ddd;
}
.input-status.has-value {
background: #43e97b;
}
.input-status.empty {
background: #ddd;
}
.url-input::placeholder,
.rss-input::placeholder {
color: #999;
font-size: 12px;
}
.form-actions {
display: flex;
justify-content: center;
gap: 15px;
margin: 30px 0 0 0;
padding: 20px;
background: #f8f9fa;
border-radius: 8px;
}
.btn {
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
line-height: 1.4;
}
.btn-save {
background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
color: white;
}
.btn-save:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(67, 233, 123, 0.3);
}
.btn-reset {
background: linear-gradient(135deg, #ff6b6b 0%, #ffa8a8 100%);
color: white;
}
.btn-reset:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(255, 107, 107, 0.3);
}
.btn-refresh {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.btn-refresh:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
}
.message {
padding: 15px 20px;
margin: 20px 0;
border-radius: 8px;
border: 1px solid;
animation: slideDown 0.3s ease;
}
.message.success {
background: #f0fff4;
border-color: #c6f6d5;
color: #22543d;
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@media (max-width: 768px) {
.user-stats-cards {
grid-template-columns: 1fr;
}
.user-mgmt-table {
display: block;
}
.form-actions {
flex-direction: column;
}
.btn {
width: 100%;
}
}
</style>
<div class="user-mgmt-section">
<div class="user-stats-cards">
<div class="user-stat-card total">
<div class="user-stat-value"><?php echo $totalUsers; ?></div>
<div class="user-stat-label">总用户数</div>
</div>
<div class="user-stat-card with-website">
<div class="user-stat-value"><?php echo $withWebsite; ?></div>
<div class="user-stat-label">有网站地址</div>
</div>
<div class="user-stat-card with-rss">
<div class="user-stat-value"><?php echo $withRss; ?></div>
<div class="user-stat-label">有RSS地址</div>
</div>
<div class="user-stat-card without-rss">
<div class="user-stat-value"><?php echo $withoutRss; ?></div>
<div class="user-stat-label">无RSS地址</div>
</div>
</div>
</div>
<form method="post" action="" id="batch-form">
<input type="hidden" name="action" value="batch_save">
<div class="user-mgmt-section">
<!--<div class="user-mgmt-title">RSS及网站地址管理</div>-->
<div class="user-table-container">
<table class="user-mgmt-table">
<thead>
<tr>
<th width="20%">用户信息</th>
<th width="15%">用户名</th>
<th width="30%">网站地址</th>
<th width="35%">RSS地址</th>
</tr>
</thead>
<tbody>
<?php foreach ($users as $user): ?>
<?php
$displayName = !empty($user['screenName']) ? $user['screenName'] : $user['name'];
$userUrl = !empty($user['url']) ? $user['url'] : '';
$hasWebsite = !empty($user['url']);
$hasRss = !empty($user['user_feed']);
$userInitial = getUserInitial($displayName);
?>
<tr>
<td>
<div class="user-info">
<div class="user-avatar">
<?php echo htmlspecialchars($userInitial); ?>
</div>
<div class="user-details">
<div class="user-name"><?php echo htmlspecialchars($displayName); ?></div>
<div class="user-id">UID: <?php echo $user['uid']; ?></div>
</div>
</div>
</td>
<td>
<div class="user-name"><?php echo htmlspecialchars($user['name']); ?></div>
</td>
<td>
<div class="website-input-container">
<input type="text"
name="user_url[<?php echo $user['uid']; ?>]"
value="<?php echo htmlspecialchars($userUrl); ?>"
class="url-input <?php echo $hasWebsite ? 'has-value' : 'empty'; ?>"
placeholder="https://example.com/"
data-uid="<?php echo $user['uid']; ?>"
data-type="url">
<div class="input-status <?php echo $hasWebsite ? 'has-value' : 'empty'; ?>"></div>
</div>
</td>
<td>
<div class="rss-input-container">
<input type="text"
name="user_feed[<?php echo $user['uid']; ?>]"
value="<?php echo htmlspecialchars($user['user_feed']); ?>"
class="rss-input <?php echo $hasRss ? 'has-value' : 'empty'; ?>"
placeholder="https://example.com/feed/"
data-uid="<?php echo $user['uid']; ?>"
data-type="rss">
<div class="input-status <?php echo $hasRss ? 'has-value' : 'empty'; ?>"></div>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<div class="form-actions">
<button type="button" class="btn btn-save" onclick="saveBatch()">
批量保存
</button>
<button type="button" class="btn btn-refresh" onclick="window.location.reload()">
刷新页面
</button>
</div>
</div>
</form>
</div>
</div>
<script>
// 显示弹窗
function showToast(message, type = 'success') {
const container = document.getElementById('toast-container');
const toast = document.createElement('div');
toast.className = 'toast';
toast.textContent = message;
// 设置不同颜色的弹窗
if (type === 'error') {
toast.style.background = 'linear-gradient(135deg, #ff6b6b 0%, #ffa8a8 100%)';
toast.style.boxShadow = '0 4px 12px rgba(255, 107, 107, 0.3)';
} else if (type === 'info') {
toast.style.background = 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)';
toast.style.boxShadow = '0 4px 12px rgba(102, 126, 234, 0.3)';
}
container.appendChild(toast);
// 3秒后移除弹窗
setTimeout(() => {
if (toast.parentNode === container) {
toast.style.animation = 'fadeOut 0.3s ease forwards';
setTimeout(() => {
if (toast.parentNode === container) {
container.removeChild(toast);
}
}, 300);
}
}, 2500);
}
// 批量保存
function saveBatch() {
const form = document.getElementById('batch-form');
const formData = new FormData(form);
// 检查是否有数据
let hasData = false;
const urlInputs = document.querySelectorAll('.url-input');
const rssInputs = document.querySelectorAll('.rss-input');
urlInputs.forEach(input => {
if (input.value.trim()) hasData = true;
});
rssInputs.forEach(input => {
if (input.value.trim()) hasData = true;
});
if (!hasData) {
if (confirm('当前没有设置任何网站地址和RSS地址确定要继续吗')) {
submitBatch(formData);
}
} else {
if (confirm('确定要保存所有用户的网站地址和RSS地址吗')) {
submitBatch(formData);
}
}
}
// 提交批量保存
function submitBatch(formData) {
fetch('', {
method: 'POST',
body: formData
})
.then(response => {
// 检查响应状态
if (!response.ok) {
throw new Error('网络响应不正常: ' + response.status);
}
return response.text(); // 先获取文本
})
.then(text => {
try {
// 尝试解析为JSON
const data = JSON.parse(text);
if (data.success) {
showToast(data.message);
// 更新输入框状态
updateInputStatus();
} else {
showToast(data.message || '保存失败,请重试!', 'error');
}
} catch (e) {
// 如果不是JSON显示原始响应以便调试
console.error('响应不是有效的JSON:', text);
showToast('服务器响应异常: ' + text.substring(0, 100), 'error');
}
})
.catch(error => {
console.error('Error:', error);
showToast('网络错误: ' + error.message, 'error');
});
}
// 更新输入框状态
function updateInputStatus() {
document.querySelectorAll('.url-input, .rss-input').forEach(function(input) {
const inputStatus = input.parentNode.querySelector('.input-status');
if (input.value.trim()) {
input.classList.remove('empty');
input.classList.add('has-value');
inputStatus.classList.remove('empty');
inputStatus.classList.add('has-value');
} else {
input.classList.remove('has-value');
input.classList.add('empty');
inputStatus.classList.remove('has-value');
inputStatus.classList.add('empty');
}
});
}
// 单个保存功能按Enter键保存
document.querySelectorAll('.url-input, .rss-input').forEach(function(input) {
input.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
e.preventDefault();
const uid = this.getAttribute('data-uid');
const type = this.getAttribute('data-type');
const value = this.value.trim();
const formData = new FormData();
formData.append('action', 'save_rss');
formData.append('uid', uid);
formData.append('user_feed', type === 'rss' ? value : '');
if (type === 'url') {
formData.append('user_url', value);
}
fetch('', {
method: 'POST',
body: formData
})
.then(response => {
if (!response.ok) {
throw new Error('网络响应不正常: ' + response.status);
}
return response.text();
})
.then(text => {
try {
const data = JSON.parse(text);
if (data.success) {
showToast(data.message);
// 更新输入框状态
const inputStatus = input.parentNode.querySelector('.input-status');
if (value) {
input.classList.remove('empty');
input.classList.add('has-value');
inputStatus.classList.remove('empty');
inputStatus.classList.add('has-value');
} else {
input.classList.remove('has-value');
input.classList.add('empty');
inputStatus.classList.remove('has-value');
inputStatus.classList.add('empty');
}
} else {
showToast(data.message || '保存失败,请重试!', 'error');
}
} catch (e) {
console.error('响应不是有效的JSON:', text);
showToast('服务器响应异常: ' + text.substring(0, 100), 'error');
}
})
.catch(error => {
console.error('Error:', error);
showToast('网络错误: ' + error.message, 'error');
});
}
});
// 输入时实时更新状态
input.addEventListener('input', function() {
const inputStatus = this.parentNode.querySelector('.input-status');
if (this.value.trim()) {
this.classList.remove('empty');
this.classList.add('has-value');
inputStatus.classList.remove('empty');
inputStatus.classList.add('has-value');
} else {
this.classList.remove('has-value');
this.classList.add('empty');
inputStatus.classList.remove('has-value');
inputStatus.classList.add('empty');
}
});
});
</script>
<?php
include 'copyright.php';
include 'common-js.php';
include 'footer.php';
?>