Files
UserCard/manage-users.php

781 lines
25 KiB
PHP
Raw Normal View History

2026-02-23 21:07:09 +08:00
<?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';
?>