commit 3c348195b75b411ee95857f1b6f5e8cb345907fe Author: LL Date: Tue Nov 18 14:18:28 2025 +0800 初始化版本 diff --git a/api/head.php b/api/head.php new file mode 100644 index 0000000..b80b930 --- /dev/null +++ b/api/head.php @@ -0,0 +1,56 @@ + true, + CURLOPT_CONNECTTIMEOUT => 8, + CURLOPT_TIMEOUT => 12, + CURLOPT_SSL_VERIFYPEER => false, + CURLOPT_SSL_VERIFYHOST => false, + CURLOPT_USERAGENT => 'DiscussionProxy/1.0', + ]); + $body = curl_exec($ch); + $err = curl_error($ch); + curl_close($ch); + if ($err) { return null; } + $json = json_decode($body, true); + return is_array($json) ? $json : null; +} + +$resp = fetch_json($endpoint); +$url = ''; +if ($resp && ($resp['code'] ?? 0) == 200) { + $url = preg_replace('/[`\s]/', '', (string)($resp['data'] ?? '')); +} +if (!$url) { + // 远程失败时,回退到本地随机头像(遍历 avatars 下所有日期目录) + $root = __DIR__ . '/../avatars'; + $candidates = []; + if (is_dir($root)) { + $rii = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($root)); + foreach ($rii as $file) { + if ($file->isFile()) { + $ext = strtolower(pathinfo($file->getFilename(), PATHINFO_EXTENSION)); + if (in_array($ext, ['jpg','jpeg','png','gif','webp'])) { + $rel = str_replace('\\', '/', str_replace($_SERVER['DOCUMENT_ROOT'], '', $file->getPathname())); + $candidates[] = $rel; + } + } + } + if (!empty($candidates)) { + shuffle($candidates); + $url = $candidates[0]; + } + } + // 如果本地也没有,则后备一个 Unsplash 头像 + if (!$url) { + $url = 'https://images.unsplash.com/photo-1547425260-76bcadfb4f9b?w=200&h=200&fit=crop'; + } +} + +echo json_encode(['url' => $url], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); diff --git a/api/picture.php b/api/picture.php new file mode 100644 index 0000000..f598520 --- /dev/null +++ b/api/picture.php @@ -0,0 +1,70 @@ + 'https://api.iyuns.com/api/heisi', + 'wapmeinvpic' => 'https://api.iyuns.com/api/wapmeinvpic', + 'baisi' => 'https://api.iyuns.com/api/baisi', + 'meinvpic' => 'https://api.iyuns.com/api/meinvpic', + 'random4kPic' => 'https://api.iyuns.com/api/random4kPic', + 'jk' => 'https://api.iyuns.com/api/jk', +]; +if (!$type || !isset($map[$type])) { + $keys = array_keys($map); + $type = $keys[array_rand($keys)]; +} +$endpoint = $map[$type]; + +function fetch_json($url) { + $ch = curl_init($url); + curl_setopt_array($ch, [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_CONNECTTIMEOUT => 8, + CURLOPT_TIMEOUT => 12, + CURLOPT_SSL_VERIFYPEER => false, + CURLOPT_SSL_VERIFYHOST => false, + CURLOPT_USERAGENT => 'PictureProxy/1.0', + ]); + $body = curl_exec($ch); + $err = curl_error($ch); + curl_close($ch); + if ($err) { return null; } + $json = json_decode($body, true); + return is_array($json) ? $json : null; +} + +$resp = fetch_json($endpoint); +$url = ''; +if ($resp && ($resp['code'] ?? 0) == 200) { + $url = preg_replace('/[`\s]/', '', (string)($resp['data'] ?? '')); +} +if (!$url) { + // 远程失败时,回退到本地随机图片(遍历 pictures/images 下的所有图片) + $root = __DIR__ . '/../pictures/images'; + $candidates = []; + if (is_dir($root)) { + $rii = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($root)); + foreach ($rii as $file) { + if ($file->isFile()) { + $ext = strtolower(pathinfo($file->getFilename(), PATHINFO_EXTENSION)); + if (in_array($ext, ['jpg','jpeg','png','gif','webp'])) { + $rel = str_replace('\\', '/', str_replace($_SERVER['DOCUMENT_ROOT'], '', $file->getPathname())); + $candidates[] = $rel; + } + } + } + if (!empty($candidates)) { + shuffle($candidates); + $url = $candidates[0]; + $type = 'local'; + } + } + // 如果本地也没有,则后备一张 Unsplash 图片 + if (!$url) { + $url = 'https://images.unsplash.com/photo-1523206489230-c012c4783a90?w=1200&h=800&fit=crop'; + } +} + +echo json_encode(['url' => $url, 'source' => $type], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); diff --git a/api/review_action.php b/api/review_action.php new file mode 100644 index 0000000..e082af1 --- /dev/null +++ b/api/review_action.php @@ -0,0 +1,71 @@ + $ok, 'msg' => $msg], $extra), JSON_UNESCAPED_UNICODE); + exit; +} + +$action = isset($_POST['action']) ? $_POST['action'] : ''; +$file = isset($_POST['file']) ? $_POST['file'] : ''; +$password = isset($_POST['password']) ? $_POST['password'] : ''; + +// 会话已登录或提供了正确密码之一即可 +if (empty($_SESSION['admin_ok']) && $password !== $ADMIN_PASSWORD) { + jsonOut(false, '管理员密码错误或未登录'); +} + +if (!$action || !$file) { + jsonOut(false, '缺少必要参数'); +} + +// 基础安全处理,禁止路径穿越 +$basename = basename($file); +$pendingPath = $pendingDir . '/' . $basename; +if (!is_file($pendingPath)) { + jsonOut(false, '待审核文件不存在'); +} + +if ($action === 'reject') { + $ok = @unlink($pendingPath); + @unlink($pendingPath . '.json'); + jsonOut(!!$ok, $ok ? '已拒绝并删除' : '删除失败'); +} + +if ($action === 'approve') { + // 生成日期目录 + $dateDir = date('Y-m-d'); + $dstDir = $videosDirRoot . '/' . $dateDir; + if (!is_dir($dstDir)) { + @mkdir($dstDir, 0777, true); + } + // 根据扩展名生成唯一文件名 + $ext = strtolower(pathinfo($basename, PATHINFO_EXTENSION)); + if (!in_array($ext, ['mp4','webm','mov'])) { + // 不支持的扩展也允许,但统一改成 mp4 + $ext = 'mp4'; + } + $newName = 'video_' . date('Ymd_His') . '_' . mt_rand(1000,9999) . '.' . $ext; + $dstPath = $dstDir . '/' . $newName; + + $ok = @rename($pendingPath, $dstPath); + @rename($pendingPath . '.json', $dstPath . '.json'); + if (!$ok) { + jsonOut(false, '转存失败'); + } + + // 返回相对路径,便于前端显示 + $rel = 'videos/' . $dateDir . '/' . $newName; + jsonOut(true, '审核通过,已入库', ['path' => $rel]); +} + +jsonOut(false, '未知操作'); +?> diff --git a/api/review_picture_action.php b/api/review_picture_action.php new file mode 100644 index 0000000..c9ff4cf --- /dev/null +++ b/api/review_picture_action.php @@ -0,0 +1,56 @@ +$ok,'msg'=>$msg], $extra), JSON_UNESCAPED_UNICODE); + exit; +} + +$authed = !empty($_SESSION['admin_ok']); +if (!$authed) { + $pwd = trim($_POST['password'] ?? ''); + if ($pwd === $ADMIN_PASSWORD) $authed = true; +} +if (!$authed) out(false, '未授权或密码错误'); + +$action = $_POST['action'] ?? ''; +$file = basename($_POST['file'] ?? ''); +if (!$action || !$file) out(false, '缺少必要参数'); + +$pendingPath = $pendingDir . '/' . $file; +if (!is_file($pendingPath)) out(false, '待审核文件不存在'); + +if ($action === 'reject') { + $ok = @unlink($pendingPath); + @unlink($pendingPath . '.json'); + out(!!$ok, $ok ? '已拒绝并删除' : '删除失败'); +} + +if ($action === 'approve') { + $dateDir = date('Y-m-d'); + $dstDir = $picturesRoot . '/' . $dateDir; + if (!is_dir($dstDir)) @mkdir($dstDir, 0777, true); + + $ext = strtolower(pathinfo($file, PATHINFO_EXTENSION)); + if (!in_array($ext, ['jpg','jpeg','png','gif','webp'])) $ext = 'jpg'; + $newName = 'image_' . date('Ymd_His') . '_' . mt_rand(1000,9999) . '.' . $ext; + $dstPath = $dstDir . '/' . $newName; + + $ok = @rename($pendingPath, $dstPath); + @rename($pendingPath . '.json', $dstPath . '.json'); + if (!$ok) out(false, '转存失败'); + + // 相对路径(供前端或日志显示) + $rel = 'pictures/images/user/' . $dateDir . '/' . $newName; + out(true, '审核通过,已入库', ['path' => $rel]); +} + +out(false, '未知操作'); +?> + diff --git a/api/review_text_action.php b/api/review_text_action.php new file mode 100644 index 0000000..c0260ea --- /dev/null +++ b/api/review_text_action.php @@ -0,0 +1,61 @@ + $ok, 'msg' => $msg], JSON_UNESCAPED_UNICODE); + exit; +} + +$authed = !empty($_SESSION['admin_ok']); +if (!$authed) { + $pwd = trim($_POST['password'] ?? ''); + if ($pwd === $ADMIN_PASSWORD) $authed = true; +} +if (!$authed) resp(false, '未授权或密码错误'); + +$action = $_POST['action'] ?? ''; +$file = basename($_POST['file'] ?? ''); + +$pendingDir = __DIR__ . '/../texts_pending'; +$path = $pendingDir . '/' . $file; +if ($file === '' || !is_file($path)) resp(false, '待处理文件不存在'); + +if ($action === 'approve') { + $data = json_decode(@file_get_contents($path), true); + if (!is_array($data) || empty($data['content'])) { + resp(false, '文案数据无效'); + } + + $title = trim($data['title'] ?? ''); + $content = trim($data['content'] ?? ''); + $text = $title !== '' ? ('【' . $title . '】 ' . $content) : $content; + + // 追加到 texts/DATE/all.json + $dateDir = __DIR__ . '/../texts/' . date('Y-m-d'); + if (!is_dir($dateDir)) @mkdir($dateDir, 0777, true); + $allPath = $dateDir . '/all.json'; + $arr = []; + if (is_file($allPath)) { + $arr = json_decode(@file_get_contents($allPath), true); + if (!is_array($arr)) $arr = []; + } + $arr[] = [ + 'text' => $text, + 'source' => 'user', + 'created_at' => date('c'), + ]; + @file_put_contents($allPath, json_encode($arr, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)); + + @unlink($path); + resp(true, '已通过并入库'); +} else if ($action === 'reject') { + @unlink($path); + resp(true, '已拒绝并删除'); +} else { + resp(false, '未知操作'); +} +?> diff --git a/api/save_avatar.php b/api/save_avatar.php new file mode 100644 index 0000000..4942e6d --- /dev/null +++ b/api/save_avatar.php @@ -0,0 +1,61 @@ + $ok, 'message' => $message, 'path' => $path], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + exit; +} + +$raw = file_get_contents('php://input'); +$data = json_decode($raw, true); +$url = $data['url'] ?? ''; +if (!$url || !filter_var($url, FILTER_VALIDATE_URL)) { + respond(false, 'URL无效'); +} + +$scheme = parse_url($url, PHP_URL_SCHEME); +if (!in_array(strtolower($scheme), ['http','https'])) { + respond(false, '仅支持http/https图片地址'); +} + +$avatarsRoot = __DIR__ . '/../avatars'; +if (!is_dir($avatarsRoot)) { @mkdir($avatarsRoot, 0777, true); } +$today = (new DateTime('now'))->format('Y-m-d'); +$targetDir = $avatarsRoot . '/' . $today; +if (!is_dir($targetDir)) { @mkdir($targetDir, 0777, true); } + +// 推断扩展名 +$pathExt = strtolower(pathinfo(parse_url($url, PHP_URL_PATH) ?? '', PATHINFO_EXTENSION)); +$allowed = ['jpg','jpeg','png','gif','webp']; +if (!$pathExt || !in_array($pathExt, $allowed)) { $pathExt = 'jpg'; } + +// 使用URL的basename作为文件名,若没有则用hash +$basename = basename(parse_url($url, PHP_URL_PATH) ?: ''); +if (!$basename || $basename === '/' || $basename === '\\') { + $basename = md5($url) . '.' . $pathExt; +} +if (strpos($basename, '.') === false) { $basename .= '.' . $pathExt; } +$safeName = preg_replace('/[^\w\.-]+/u', '_', $basename); +$targetPath = $targetDir . '/' . $safeName; + +// 如果已存在,跳过下载 +if (file_exists($targetPath)) { + respond(true, '文件已存在,跳过下载', str_replace($_SERVER['DOCUMENT_ROOT'], '', $targetPath)); +} + +// 下载保存 +try { + $read = @fopen($url, 'rb'); + if (!$read) { respond(false, '无法读取远程图片'); } + $write = @fopen($targetPath, 'wb'); + if (!$write) { respond(false, '无法写入文件'); } + stream_copy_to_stream($read, $write); + fclose($read); + fclose($write); +} catch (Throwable $e) { + respond(false, '保存失败: ' . $e->getMessage()); +} + +respond(true, '保存成功', str_replace($_SERVER['DOCUMENT_ROOT'], '', $targetPath)); + diff --git a/api/save_picture.php b/api/save_picture.php new file mode 100644 index 0000000..30b4fbe --- /dev/null +++ b/api/save_picture.php @@ -0,0 +1,66 @@ + $ok, 'message' => $message, 'path' => $path], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + exit; +} + +// 读取JSON输入 +$raw = file_get_contents('php://input'); +$data = json_decode($raw, true); +$url = $data['url'] ?? ''; +if (!$url || !filter_var($url, FILTER_VALIDATE_URL)) { + respond(false, 'URL无效'); +} + +// 只允许 http/https +$scheme = parse_url($url, PHP_URL_SCHEME); +if (!in_array(strtolower($scheme), ['http','https'])) { + respond(false, '仅支持http/https图片地址'); +} + +$root = __DIR__ . '/../pictures'; +if (!is_dir($root)) { @mkdir($root, 0777, true); } + +// 解析远程路径:按照域名后的路径作为保存文件夹结构 +$path = parse_url($url, PHP_URL_PATH) ?: ''; +if (!$path) { respond(false, '无法解析远程路径'); } + +// 将路径拆分并进行安全过滤,避免非法字符与上级目录 +$segments = array_values(array_filter(explode('/', $path), function($s){ return $s !== ''; })); +if (!count($segments)) { respond(false, '路径为空'); } + +$filename = array_pop($segments); +$safeSegs = array_map(function($seg){ + $seg = preg_replace('/[^A-Za-z0-9._-]+/u', '_', $seg); + return trim($seg, '._-') ?: 'unk'; +}, $segments); +$safeName = preg_replace('/[^A-Za-z0-9._-]+/u', '_', $filename); +if (!$safeName) { $safeName = 'image_' . time() . '.jpg'; } + +$targetDir = $root; +foreach ($safeSegs as $seg) { $targetDir .= DIRECTORY_SEPARATOR . $seg; if (!is_dir($targetDir)) { @mkdir($targetDir, 0777, true); } } +$targetPath = $targetDir . DIRECTORY_SEPARATOR . $safeName; + +// 如果文件已存在则跳过下载 +if (file_exists($targetPath)) { + respond(true, '文件已存在,跳过下载', str_replace($_SERVER['DOCUMENT_ROOT'], '', $targetPath)); +} + +// 下载保存 +try { + $read = @fopen($url, 'rb'); + if (!$read) { respond(false, '无法读取远程图片'); } + $write = @fopen($targetPath, 'wb'); + if (!$write) { respond(false, '无法写入文件'); } + stream_copy_to_stream($read, $write); + fclose($read); + fclose($write); +} catch (Throwable $e) { + respond(false, '保存失败: ' . $e->getMessage()); +} + +respond(true, '保存成功', str_replace($_SERVER['DOCUMENT_ROOT'], '', $targetPath)); + diff --git a/api/save_text.php b/api/save_text.php new file mode 100644 index 0000000..d5f12d9 --- /dev/null +++ b/api/save_text.php @@ -0,0 +1,63 @@ + $ok, 'message' => $message, 'path' => $path], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + exit; +} + +$raw = file_get_contents('php://input'); +$data = json_decode($raw, true); +$text = trim($data['text'] ?? ''); +$source = trim($data['source'] ?? ''); +if ($text === '') { respond(false, '文本为空'); } + +// 根目录 +$textsRoot = __DIR__ . '/../texts'; +if (!is_dir($textsRoot)) { @mkdir($textsRoot, 0777, true); } +// 按来源(/后缀,即 yiyan/dujitang/renjian 等)创建子目录 +$safeSource = preg_replace('/[^A-Za-z0-9._-]+/u', '_', $source ?: 'unknown'); +$safeSource = trim($safeSource, '._-'); +if ($safeSource === '') { $safeSource = 'unknown'; } + +// 日期目录 +$today = (new DateTime('now'))->format('Y-m-d'); +$targetDir = $textsRoot . '/' . $safeSource . '/' . $today; +if (!is_dir($targetDir)) { @mkdir($targetDir, 0777, true); } + +// 改为保存到同一天的单个文件 all.json +$targetPath = $targetDir . '/all.json'; + +// 读取已有数据 +$entries = []; +if (file_exists($targetPath)) { + $rawJson = @file_get_contents($targetPath); + $decoded = json_decode($rawJson, true); + if (is_array($decoded)) { $entries = $decoded; } +} + +// 重复检查:文本完全相同则认为重复 +foreach ($entries as $e) { + if (isset($e['text']) && trim((string)$e['text']) === $text) { + respond(true, '文本已存在,跳过保存', str_replace($_SERVER['DOCUMENT_ROOT'], '', $targetPath)); + } +} + +$entries[] = [ + 'text' => $text, + 'source' => $source, + 'created_at' => (new DateTime('now'))->format(DateTime::ATOM), +]; + +try { + $json = json_encode($entries, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT); + if (@file_put_contents($targetPath, $json, LOCK_EX) === false) { + respond(false, '无法写入文件'); + } +} catch (Throwable $e) { + respond(false, '保存失败: ' . $e->getMessage()); +} + +respond(true, '保存成功', str_replace($_SERVER['DOCUMENT_ROOT'], '', $targetPath)); + diff --git a/api/save_video.php b/api/save_video.php new file mode 100644 index 0000000..448b216 --- /dev/null +++ b/api/save_video.php @@ -0,0 +1,107 @@ + $ok, 'message' => $message, 'path' => $path], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + exit; +} + +// 递归删除目录及其内容(用于清理旧视频) +function rrmdir($dir) { + if (!is_dir($dir)) return; + $items = @scandir($dir); + if ($items === false) return; + foreach ($items as $item) { + if ($item === '.' || $item === '..') continue; + $full = $dir . DIRECTORY_SEPARATOR . $item; + if (is_dir($full)) { + rrmdir($full); + } else { + @unlink($full); + } + } + @rmdir($dir); +} + +// 读取JSON输入 +$raw = file_get_contents('php://input'); +$data = json_decode($raw, true); +$url = $data['url'] ?? ''; +$title = $data['title'] ?? ''; + +if (!$url || !filter_var($url, FILTER_VALIDATE_URL)) { + respond(false, 'URL无效'); +} + +// 只允许 http/https +$scheme = parse_url($url, PHP_URL_SCHEME); +if (!in_array(strtolower($scheme), ['http','https'])) { + respond(false, '仅支持http/https视频地址'); +} + +$videosRoot = __DIR__ . '/../videos'; +if (!is_dir($videosRoot)) { + @mkdir($videosRoot, 0777, true); +} + +$today = (new DateTime('now'))->format('Y-m-d'); +$targetDir = $videosRoot . '/' . $today; +if (!is_dir($targetDir)) { + @mkdir($targetDir, 0777, true); +} + +// 生成文件名 +$basename = basename(parse_url($url, PHP_URL_PATH) ?: 'video_' . time() . '.mp4'); +if (strpos($basename, '.') === false) { $basename .= '.mp4'; } +$safeName = preg_replace('/[^\w\.-]+/u', '_', $basename); +$targetPath = $targetDir . '/' . $safeName; + +// 如果文件已存在则跳过下载 +$skipDownload = file_exists($targetPath); +$msg = '保存成功'; +if ($skipDownload) { + $msg = '文件已存在,跳过下载'; +} else { + // 下载保存 + try { + $read = @fopen($url, 'rb'); + if (!$read) { respond(false, '无法读取远程视频'); } + $write = @fopen($targetPath, 'wb'); + if (!$write) { respond(false, '无法写入文件'); } + stream_copy_to_stream($read, $write); + fclose($read); + fclose($write); + } catch (Throwable $e) { + respond(false, '保存失败: ' . $e->getMessage()); + } +} + +// 恢复清理逻辑:仅保留最近两天的视频目录,删除更早的目录 +try { + $dirs = @scandir($videosRoot); + if (is_array($dirs)) { + // 仅筛选形如 YYYY-MM-DD 的目录 + $dateDirs = []; + foreach ($dirs as $d) { + if ($d === '.' || $d === '..') continue; + $path = $videosRoot . DIRECTORY_SEPARATOR . $d; + if (is_dir($path) && preg_match('/^\d{4}-\d{2}-\d{2}$/', $d)) { + $dateDirs[] = $d; + } + } + // 按日期升序排序 + sort($dateDirs); + // 保留最近两天(末尾两项),其它全部删除 + if (count($dateDirs) > 30) { + $toDelete = array_slice($dateDirs, 0, count($dateDirs) - 2); + foreach ($toDelete as $del) { + rrmdir($videosRoot . DIRECTORY_SEPARATOR . $del); + } + } + } +} catch (Throwable $e) { + // 清理失败不影响主流程 +} + +respond(true, $msg, str_replace($_SERVER['DOCUMENT_ROOT'], '', $targetPath)); diff --git a/api/text.php b/api/text.php new file mode 100644 index 0000000..bac0e84 --- /dev/null +++ b/api/text.php @@ -0,0 +1,83 @@ + 'https://api.iyuns.com/api/dujitang', + 'renjian' => 'https://api.iyuns.com/api/renjian', + 'ktff'=> 'https://v.api.aa1.cn/api/api-wenan-ktff/index.php?type=5', // 新增文艺文案 + 'yiyan'=> 'https://v.api.aa1.cn/api/yiyan/index.php?type=json', // 新增纯文本一言 + 'wenan_anwei'=> 'https://v.api.aa1.cn/api/api-wenan-anwei/index.php?type=json', // 新增文艺文案 + 'wenan_gaoxiao'=> 'https://v.api.aa1.cn/api/api-wenan-gaoxiao/index.php?aa1=json', // 新增文艺文案 + 'wenan_shenhuifu'=> 'https://v.api.aa1.cn/api/api-wenan-shenhuifu/index.php?aa1=json', // 新增文艺文案 +]; +if (!$type || !isset($map[$type])) { + $keys = array_keys($map); + $type = $keys[array_rand($keys)]; +} +$endpoint = $map[$type]; + +function fetch_json($url) { + $ch = curl_init($url); + curl_setopt_array($ch, [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_CONNECTTIMEOUT => 8, + CURLOPT_TIMEOUT => 12, + CURLOPT_SSL_VERIFYPEER => false, + CURLOPT_SSL_VERIFYHOST => false, + CURLOPT_USERAGENT => 'DiscussionProxy/1.0', + ]); + $body = curl_exec($ch); + $err = curl_error($ch); + curl_close($ch); + if ($err) { return null; } + $json = json_decode($body, true); + return is_array($json) ? $json : null; +} + +$resp = fetch_json($endpoint); +$text = ''; +if ($resp && ($resp['code'] ?? 0) == 200) { + $fields = ['yiyan','data','text','anwei','gaoxiao','shenhuifu']; + foreach ($fields as $k) { + $t = trim((string)($resp[$k] ?? '')); + if ($t !== '') { $text = $t; break; } + if (is_array($resp['data'] ?? null)) { + $t = trim((string)($resp['data'][$k] ?? '')); + if ($t !== '') { $text = $t; break; } + } + } + $text = preg_replace('/[`]/', '', $text); +} +if (!$text) { + // 远程失败时,回退到本地随机文案 + $root = __DIR__ . '/../texts'; + $pool = []; + if (is_dir($root)) { + $rii = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($root)); + foreach ($rii as $file) { + if ($file->isFile() && strtolower($file->getFilename()) === 'all.json') { + $json = @file_get_contents($file->getPathname()); + if ($json) { + $arr = json_decode($json, true); + if (is_array($arr)) { + foreach ($arr as $it) { + $t = trim((string)($it['text'] ?? '')); + if ($t !== '') { $pool[] = $t; } + } + } + } + } + } + } + if (!empty($pool)) { + $text = $pool[array_rand($pool)]; + $type = 'local'; + } else { + // 如果本地也没有,则使用一个默认文案 + $text = '今天也要元气满满!'; + } +} + +echo json_encode(['text' => $text, 'source' => $type], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); diff --git a/api/upload_picture.php b/api/upload_picture.php new file mode 100644 index 0000000..22a24d9 --- /dev/null +++ b/api/upload_picture.php @@ -0,0 +1,92 @@ + 120) { + $ext = pathinfo($name, PATHINFO_EXTENSION); + $base = pathinfo($name, PATHINFO_FILENAME); + $base = substr($base, 0, 100); + $name = $base . ($ext ? ('.' . $ext) : ''); + } + return $name; +} + +function html($s) { return htmlspecialchars($s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); } + +$allowed = ['jpg','jpeg','png','gif','webp']; +$maxSize = 10 * 1024 * 1024; // 10MB +$title = isset($_POST['title']) ? trim($_POST['title']) : ''; + +if (!isset($_FILES['picture'])) { + echo '

未接收到文件。

返回图片投稿页

'; + exit; +} + +// 上传错误处理 +if (!empty($_FILES['picture']['error'])) { + $err = (int)$_FILES['picture']['error']; + if ($err === UPLOAD_ERR_INI_SIZE || $err === UPLOAD_ERR_FORM_SIZE) { + echo '

上传失败:文件大小超过服务器限制。请确保图片不超过 10MB。

返回图片投稿页

'; + exit; + } + echo '

上传失败,错误码:' . $err . '。

返回图片投稿页

'; + exit; +} + +if (!is_uploaded_file($_FILES['picture']['tmp_name'])) { + echo '

文件上传失败。

返回图片投稿页

'; + exit; +} + +$file = $_FILES['picture']; +$origName = $file['name'] ?? 'uploaded_image'; +$safeName = sanitizeFileName($origName); +$ext = strtolower(pathinfo($safeName, PATHINFO_EXTENSION)); +if (!in_array($ext, $allowed)) { + echo '

仅支持文件类型:jpg / jpeg / png / gif / webp。

返回图片投稿页

'; + exit; +} + +// 大小限制 10MB +$size = (int)($_FILES['picture']['size'] ?? 0); +if ($size > $maxSize) { + echo '

图片大小超过 10MB 限制,投稿失败。

返回图片投稿页

'; + exit; +} + +$unique = date('Ymd_His') . '_' . mt_rand(1000,9999) . '.' . $ext; +$target = $pendingDir . '/' . $unique; + +if (!move_uploaded_file($file['tmp_name'], $target)) { + echo '

保存文件失败,请稍后重试。

返回图片投稿页

'; + exit; +} + +$meta = [ + 'original_name' => $origName, + 'saved_name' => basename($target), + 'title' => $title, + 'uploaded_at' => date('c'), + 'size' => $file['size'] ?? 0, + 'type' => $file['type'] ?? '', +]; +@file_put_contents($pendingDir . '/' . basename($target) . '.json', json_encode($meta, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)); + +echo '
'; +echo '

投稿成功!图片已进入待审核队列。

'; +if (!empty($title)) { + echo '

标题:' . html($title) . '

'; +} +echo '

管理员可前往 图片审核页面 进行审批。

'; +echo '

继续投稿返回首页

'; +echo '
'; +?> diff --git a/api/upload_text.php b/api/upload_text.php new file mode 100644 index 0000000..e855d9e --- /dev/null +++ b/api/upload_text.php @@ -0,0 +1,36 @@ +'; + echo '

文案内容不能为空。

返回文案投稿页

'; + echo ''; + exit; +} + +$name = 'text_' . date('Ymd_His') . '_' . mt_rand(1000,9999) . '.json'; +$data = [ + 'title' => $title, + 'content' => $content, + 'uploaded_at' => date('c'), +]; +@file_put_contents($pendingDir . '/' . $name, json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)); + +echo '
'; +echo '

提交成功!文案已保存到待处理目录。

'; +if ($title !== '') echo '

标题:' . html($title) . '

'; +echo '

内容:

' . html($content) . '
'; +echo '

继续提交返回首页

'; +echo '
'; +?> diff --git a/api/upload_video.php b/api/upload_video.php new file mode 100644 index 0000000..f1e62bf --- /dev/null +++ b/api/upload_video.php @@ -0,0 +1,95 @@ + 120) { + $ext = pathinfo($name, PATHINFO_EXTENSION); + $base = pathinfo($name, PATHINFO_FILENAME); + $base = substr($base, 0, 100); + $name = $base . ($ext ? ('.' . $ext) : ''); + } + return $name; +} + +function html($s) { return htmlspecialchars($s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); } + +$allowed = ['mp4','webm','mov']; +$maxSize = 50 * 1024 * 1024; // 50MB +$title = isset($_POST['title']) ? trim($_POST['title']) : ''; + +if (!isset($_FILES['video'])) { + echo '

未接收到文件。

返回投稿页

'; + exit; +} + +// 上传错误处理 +if (!empty($_FILES['video']['error'])) { + $err = (int)$_FILES['video']['error']; + if ($err === UPLOAD_ERR_INI_SIZE || $err === UPLOAD_ERR_FORM_SIZE) { + echo '

上传失败:文件大小超过服务器限制。请确保视频不超过 50MB。

返回投稿页

'; + exit; + } + echo '

上传失败,错误码:' . $err . '。

返回投稿页

'; + exit; +} + +if (!is_uploaded_file($_FILES['video']['tmp_name'])) { + echo '

文件上传失败。

返回投稿页

'; + exit; +} + +$file = $_FILES['video']; +$origName = $file['name'] ?? 'uploaded_video'; +$safeName = sanitizeFileName($origName); +$ext = strtolower(pathinfo($safeName, PATHINFO_EXTENSION)); +if (!in_array($ext, $allowed)) { + echo '

仅支持文件类型:mp4 / webm / mov。

返回投稿页

'; + exit; +} + +// 大小限制 50MB +$size = (int)($_FILES['video']['size'] ?? 0); +if ($size > $maxSize) { + echo '

视频大小超过 50MB 限制,投稿失败。

返回投稿页

'; + exit; +} + +// 生成不重复的文件名(保留原名作为前缀,防止覆盖) +$unique = date('Ymd_His') . '_' . mt_rand(1000,9999) . '.' . $ext; +$target = $pendingDir . '/' . $unique; + +if (!move_uploaded_file($file['tmp_name'], $target)) { + echo '

保存文件失败,请稍后重试。

返回投稿页

'; + exit; +} + +// 可选:记录一个简单的元信息文件 +$meta = [ + 'original_name' => $origName, + 'saved_name' => basename($target), + 'title' => $title, + 'uploaded_at' => date('c'), + 'size' => $file['size'] ?? 0, + 'type' => $file['type'] ?? '', +]; +@file_put_contents($pendingDir . '/' . basename($target) . '.json', json_encode($meta, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)); + +echo '
'; +echo '

投稿成功!文件已进入待审核队列。

'; +if (!empty($title)) { + echo '

标题:' . html($title) . '

'; +} +echo '

管理员可前往 审核页面 进行审批。

'; +echo '

继续投稿返回首页

'; +echo '
'; +?> diff --git a/api/videos.php b/api/videos.php new file mode 100644 index 0000000..cfdfcb5 --- /dev/null +++ b/api/videos.php @@ -0,0 +1,133 @@ + 5) $count = 5; // 限制最多5条 +// 是否包含本地视频的开关 +$includeLocal = intval($_GET['include_local'] ?? 0) === 1; +// 本地视频条数上限(当开启包含本地时生效,默认与远程条数相同) +$localCount = intval($_GET['local_count'] ?? $count); +if ($localCount < 0) $localCount = 0; if ($localCount > 10) $localCount = 10; // 限制一次最多10条本地视频 + +function fetch_remote_url($endpoint) { + // 使用 curl 更稳妥地请求第三方接口 + $ch = curl_init($endpoint); + curl_setopt_array($ch, [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_CONNECTTIMEOUT => 8, + CURLOPT_TIMEOUT => 12, + CURLOPT_SSL_VERIFYPEER => false, + CURLOPT_SSL_VERIFYHOST => false, + CURLOPT_USERAGENT => 'VideoFetcher/1.0', + ]); + $body = curl_exec($ch); + $err = curl_error($ch); + curl_close($ch); + if ($err) { return null; } + $json = json_decode($body, true); + $candidate = ''; + if (is_array($json)) { + // 某些接口包含 code 字段,若存在则需为 200 + if (array_key_exists('code', $json) && intval($json['code']) !== 200) { + return null; + } + // 兼容不同字段名:mp4_video、data、url、mp4、video + foreach (['mp4_video','data','url','mp4','video','pic'] as $key) { + if (!empty($json[$key]) && is_string($json[$key])) { $candidate = $json[$key]; break; } + } + } else { + // 非JSON:可能直接返回URL + $candidate = trim((string)$body); + } + // 清理反引号和空白 + $candidate = preg_replace('/[`\s]/', '', (string)$candidate); + // 简单校验:必须是 http/https,且 URL 中包含常见视频扩展(允许出现在路径或查询字符串中) + if (!$candidate || !preg_match('/^https?:\/\//i', $candidate)) { return null; } + if (!preg_match('/\.(mp4|webm|mov)(?![A-Za-z])/i', $candidate)) { return null; } + return $candidate; +} + +// 远程接口视频列表 +$remote = []; +for ($i = 0; $i < $count; $i++) { + $u = null; + // 依次尝试多个来源,取第一个成功的(随机模式下,每次尝试顺序打乱) + $epOrder = $endpoints; + if ($random && count($epOrder) > 1) { shuffle($epOrder); } + foreach ($epOrder as $ep) { + $u = fetch_remote_url($ep); + if ($u) { break; } + } + if ($u) { + $remote[] = [ + 'url' => $u, + 'title' => '接口视频 ' . ($i + 1), + ]; + } +} +// 随机模式下,对远程列表打乱顺序 +if ($random && count($remote) > 1) { shuffle($remote); } + +// 同时把本地已保存的视频加入列表(可选) +$videosRoot = __DIR__ . '/../videos'; +$local = []; +// 构建本地视频列表,用于开关合并或远程失败时的回退 +if (is_dir($videosRoot)) { + $rii = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($videosRoot)); + foreach ($rii as $file) { + if ($file->isFile()) { + $ext = strtolower(pathinfo($file->getFilename(), PATHINFO_EXTENSION)); + if (in_array($ext, ['mp4','webm','mov'])) { + // 构造相对URL供前端播放 + $rel = str_replace('\\', '/', str_replace($_SERVER['DOCUMENT_ROOT'], '', $file->getPathname())); + $local[] = [ + 'url' => $rel, + 'title' => '本地:' . $file->getFilename() + ]; + } + } + } + // 本地视频顺序随机并裁剪到指定数量 + if (count($local) > 1) { shuffle($local); } + if ($localCount > 0 && count($local) > $localCount) { + $local = array_slice($local, 0, $localCount); + } +} + +// 合并/回退策略: +// 1) 远程成功: +// - 如果开启本地,则远程+本地合并并整体随机 +// - 如果未开启本地,则仅远程 +// 2) 远程失败: +// - 若本地有内容,则仅返回本地随机列表(忽略开关) +$list = []; +if (!empty($remote)) { + $list = $includeLocal ? array_merge($remote, $local) : $remote; + if (count($list) > 1 && ($includeLocal || $random)) { shuffle($list); } +} else if (!empty($local)) { + $list = $local; // 远程不可用时,回退到全部本地随机 +} + +// 如果仍为空,提供一个后备示例 +if (empty($list)) { + $list[] = [ + 'url' => 'https://sample-videos.com/video321/mp4/720/big_buck_bunny_720p_1mb.mp4', + 'title' => '示例视频' + ]; +} + +echo json_encode(['list' => $list], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); diff --git a/assets/script.js b/assets/script.js new file mode 100644 index 0000000..1d9fa84 --- /dev/null +++ b/assets/script.js @@ -0,0 +1,348 @@ +// 外部脚本文件:轻量的菜单高亮逻辑与占位 +document.addEventListener('DOMContentLoaded', () => { + // 菜单点击激活态 + const links = Array.from(document.querySelectorAll('.menu__link')); + links.forEach(link => { + link.addEventListener('click', () => { + links.forEach(l => l.classList.remove('is-active')); + link.classList.add('is-active'); + }); + }); + + // ===== 视频逻辑 ===== + const player = document.getElementById('player'); + const videoWrap = document.getElementById('videoWrap'); + const videoPanel = document.getElementById('video'); + const includeLocalToggle = document.getElementById('includeLocalToggle'); + // 底部工具栏与标题已移除,无需获取相关元素 + + if (!player || !videoWrap) return; // 仅在视频区存在时执行 + + let playlist = []; + let index = 0; + let includeLocal = includeLocalToggle ? includeLocalToggle.checked : false; + const BUFFER_SIZE = 3; // 预缓存条数 + + const loadVideo = (i, autoPlay = false) => { + if (!playlist.length) return; + index = (i + playlist.length) % playlist.length; + const item = playlist[index]; + player.src = item.url; + // 已移除标题展示,无需设置文本 + player.load(); + if (videoPanel) videoPanel.style.setProperty('--progress', '0'); + if (autoPlay) { + player.play().catch(() => {}); + } + }; + + // 按需拉取一条或多条新视频 + const fetchOne = async (n = 1) => { + try { + const url = `api/videos.php?count=${n}&include_local=${includeLocal ? 1 : 0}&local_count=${n}`; + const res = await fetch(url); + const data = await res.json(); + const items = Array.isArray(data.list) ? data.list : []; + if (items.length) { + const existing = new Set(playlist.map(i => i.url)); + const append = items.filter(it => it && it.url && !existing.has(it.url)); + playlist = playlist.concat(append); + } + } catch (e) { + // 忽略错误 + } + }; + + const ensureBuffer = async () => { + const ahead = playlist.length - index - 1; + if (ahead < BUFFER_SIZE) { + await fetchOne(BUFFER_SIZE - ahead); + } + }; + + const next = async () => { + await ensureBuffer(); + if (index + 1 < playlist.length) { + loadVideo(index + 1, true); + } else if (!playlist.length) { + // 仍无数据,尝试拉取 + await fetchOne(BUFFER_SIZE + 1); + if (playlist.length) loadVideo(0, true); + } + }; + const prev = async () => { + if (index - 1 >= 0) { + loadVideo(index - 1, true); + } + }; + + // 鼠标移入显示并播放,移出隐藏并暂停 + const showVideo = () => { + player.classList.add('visible'); + player.play().catch(() => {}); + // 用户首次交互后取消静音(可选) + if (player.muted) player.muted = false; + }; + const hideVideo = () => { + player.pause(); + player.classList.remove('visible'); + // 可选:player.currentTime = 0; // 回到开头 + }; + videoWrap.addEventListener('mouseenter', showVideo); + videoWrap.addEventListener('mouseleave', hideVideo); + + // 页面加载时若鼠标已在视频区域,立即显示 + if (videoWrap.matches(':hover')) showVideo(); + + // 单击下一条,双击上一条(去抖处理) + // 按最新需求:不再点击视频进入投稿,而是点击菜单栏“视频”进入投稿 + let clickTimer = null; + videoWrap.addEventListener('click', () => { + if (clickTimer) return; + clickTimer = setTimeout(() => { + clickTimer = null; + next(); + }, 250); + }); + videoWrap.addEventListener('dblclick', () => { + if (clickTimer) { + clearTimeout(clickTimer); + clickTimer = null; + } + prev(); + }); + + // 自动保存:播放到一半时保存(每个视频仅保存一次) + const savedURLs = new Set(); + const trySaveOnHalf = async () => { + const item = playlist[index]; + if (!item) return; + // 本地视频不入库(URL非http/https时跳过保存) + if (!/^https?:\/\//i.test(item.url || '')) return; + const d = player.duration; + const t = player.currentTime; + if (!isFinite(d) || d <= 0) return; // 元数据未就绪 + const reachedHalf = t / d >= 0.5; + if (!reachedHalf || savedURLs.has(item.url)) return; + try { + const res = await fetch('api/save_video.php', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ url: item.url, title: item.title || '' }), + }); + const data = await res.json(); + if (data && data.success) { + savedURLs.add(item.url); + } + } catch (e) { + // 忽略保存错误,不影响播放 + } + }; + player.addEventListener('timeupdate', trySaveOnHalf); + player.addEventListener('timeupdate', () => { + const d = player.duration; + const t = player.currentTime; + if (!isFinite(d) || d <= 0) return; + const p = Math.max(0, Math.min(1, t / d)); + if (videoPanel) videoPanel.style.setProperty('--progress', String(p * 360)); + }); + + // 播放结束自动下一条 + player.addEventListener('ended', () => { next(); }); + + // 从后端接口获取视频列表 + // 初始化:先拉取首条并填充缓存 + (async () => { + await fetchOne(BUFFER_SIZE + 1); + if (!playlist.length) { + // 无视频可用 + return; + } + loadVideo(0, false); + // 启动缓冲确保后续播放连续 + await ensureBuffer(); + })(); + + // 切换本地视频开关:重置播放列表并重新拉取 + if (includeLocalToggle) { + includeLocalToggle.addEventListener('change', async () => { + includeLocal = includeLocalToggle.checked; + // 重置播放列表 + playlist = []; + index = 0; + await fetchOne(BUFFER_SIZE + 1); + if (playlist.length) { + loadVideo(0, false); + await ensureBuffer(); + } + }); + } + + // ===== 讨论区气泡逻辑 ===== + const discussionWrap = document.getElementById('discussionWrap'); + if (discussionWrap) { + const fetchAvatar = async () => { + try { + const r = await fetch('api/head.php'); + const j = await r.json(); + return j.url || ''; + } catch { return ''; } + }; + const fetchText = async () => { + try { + const r = await fetch('api/text.php'); // 随机来源 + const j = await r.json(); + return { text: j.text || '', source: j.source || '' }; + } catch { return { text: '', source: '' }; } + }; + + const createBubble = (avatarUrl, text, side = Math.random() < 0.5 ? 'left' : 'right') => { + const item = document.createElement('div'); + item.className = 'bubble' + (side === 'right' ? ' right' : ''); + + const img = document.createElement('img'); + img.className = 'avatar'; + img.src = avatarUrl || 'https://images.unsplash.com/photo-1547425260-76bcadfb4f9b?w=200&h=200&fit=crop'; + img.alt = '头像'; + + const textEl = document.createElement('div'); + textEl.className = 'bubble__text'; + textEl.textContent = text || '...'; + + item.appendChild(img); + item.appendChild(textEl); + discussionWrap.appendChild(item); + // 滚动到底部 + discussionWrap.scrollTop = discussionWrap.scrollHeight; + + // 控制条数最多50条 + const nodes = discussionWrap.querySelectorAll('.bubble'); + if (nodes.length > 50) { + for (let i = 0; i < nodes.length - 50; i++) { + discussionWrap.removeChild(nodes[i]); + } + } + }; + + const savedAvatars = new Set(); + const savedTexts = new Set(); + + const spawn = async () => { + const [avatar, textResp] = await Promise.all([fetchAvatar(), fetchText()]); + const text = textResp.text; + const source = textResp.source; + createBubble(avatar, text); + // 保存到本地,避免重复 + try { + // 仅保存远程头像(http/https),本地相对路径跳过 + if (avatar && /^https?:\/\//i.test(avatar) && !savedAvatars.has(avatar)) { + savedAvatars.add(avatar); + await fetch('api/save_avatar.php', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ url: avatar }) + }); + } + } catch {} + try { + const key = text.trim(); + if (key && !savedTexts.has(key)) { + savedTexts.add(key); + await fetch('api/save_text.php', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ text: key, source }) + }); + } + } catch {} + }; + // 立即生成一个,然后每5秒生成一次(可在此处调整间隔) + spawn(); + setInterval(spawn, 5000); + } + + // ===== 图片区逻辑(淡进淡出,10s刷新,自动保存不清理) ===== + const pictureWrap = document.getElementById('pictureWrap'); + const pictureImg = document.getElementById('pictureImg'); + const galleryPanel = document.getElementById('gallery'); + if (pictureWrap && pictureImg) { + const sources = ['heisi', 'wapmeinvpic', 'baisi', 'meinvpic']; + const savedPics = new Set(); + + const fetchPicture = async () => { + const type = sources[Math.floor(Math.random() * sources.length)]; + try { + const r = await fetch(`api/picture.php?type=${encodeURIComponent(type)}`); + const j = await r.json(); + return { url: j.url || '', source: j.source || type }; + } catch { + return { url: '', source: type }; + } + }; + + const showPicture = async () => { + // 先淡出 + pictureImg.classList.remove('visible'); + const { url, source } = await fetchPicture(); + if (!url) return; // 拉取失败时跳过本次 + // 等图片加载完成后再淡入 + await new Promise(resolve => { + pictureImg.onload = () => { resolve(); }; + pictureImg.onerror = () => { resolve(); }; + pictureImg.src = url; + }); + // 加载完成但不再自动显示,仅悬停时显示 + + // 自动保存(避免重复),后端不做清理 + try { + // 仅保存远程图片(http/https),本地相对路径跳过 + if (/^https?:\/\//i.test(url) && !savedPics.has(url)) { + savedPics.add(url); + await fetch('api/save_picture.php', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ url }) + }); + } + } catch {} + }; + + // 悬停显隐 + const showPic = () => pictureImg.classList.add('visible'); + const hidePic = () => pictureImg.classList.remove('visible'); + pictureWrap.addEventListener('mouseenter', showPic); + pictureWrap.addEventListener('mouseleave', hidePic); + + // 页面加载时若鼠标已在图片区域,立即显示 + if (pictureWrap.matches(':hover')) showPic(); + + let picIntervalMs = 10000; + let picLastSwap = Date.now(); + const updatePicProgress = () => { + const elapsed = Date.now() - picLastSwap; + const p = Math.max(0, Math.min(1, elapsed / picIntervalMs)); + if (galleryPanel) galleryPanel.style.setProperty('--pic-progress', String(p * 360)); + requestAnimationFrame(updatePicProgress); + }; + requestAnimationFrame(updatePicProgress); + + (async () => { + const { url } = await fetchPicture(); + if (url) { + pictureImg.src = url; + picLastSwap = Date.now(); + if (galleryPanel) galleryPanel.style.setProperty('--pic-progress', '0'); + if (pictureWrap.matches(':hover')) showPic(); + } + })(); + setInterval(async () => { + const { url } = await fetchPicture(); + if (url) { + pictureImg.src = url; + picLastSwap = Date.now(); + if (galleryPanel) galleryPanel.style.setProperty('--pic-progress', '0'); + if (pictureWrap.matches(':hover')) showPic(); + } + }, picIntervalMs); + } +}); diff --git a/assets/style.css b/assets/style.css new file mode 100644 index 0000000..c68bd0f --- /dev/null +++ b/assets/style.css @@ -0,0 +1,440 @@ +/* 外部样式文件:从 index.php 迁移而来 */ +:root { + --red: #e32626; + --border: 2px solid var(--red); + --gap: 32px; + --page-max: 1200px; + --radius: 6px; + --shadow: 0 6px 14px rgba(226, 38, 38, 0.08); + --shadow-hover: 0 8px 20px rgba(226, 38, 38, 0.12); +} + +* { box-sizing: border-box; } +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, + 'Helvetica Neue', Arial, 'Noto Sans', 'PingFang SC', 'Microsoft YaHei', + sans-serif; + color: #333; + background: + radial-gradient(1200px 400px at 10% -20%, #fff5f5 0%, transparent 40%), + linear-gradient(#ffffff, #ffffff); +} + +.page { + max-width: var(--page-max); + padding: 16px; + margin: 0 auto; +} + +/* 顶部菜单条 */ +.menu { + position: sticky; + top: 0; + z-index: 100; + border: var(--border); + background: #fff; + min-height: 60px; + padding: 10px 16px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + margin-bottom: var(--gap); + border-radius: 12px; + box-shadow: 0 10px 24px rgba(226, 38, 38, 0.08); +} + +.menu__logo { + font-size: 18px; + font-weight: 700; + color: var(--red); + letter-spacing: 0.5px; +} + +.menu__nav { + list-style: none; + margin: 0; + padding: 0; + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; +} + +.menu__link { + display: inline-block; + text-decoration: none; + color: var(--red); + padding: 8px 14px; + border-radius: 999px; + border: 1px solid var(--red); + background: #fff; + transition: background 0.2s ease, border-color 0.2s ease, color 0.2s ease, box-shadow 0.2s ease; +} + +.menu__link:hover, +.menu__link:focus-visible { + background: #fff1f1; + outline: none; + box-shadow: 0 6px 14px rgba(226, 38, 38, 0.15); +} + +.menu__link.is-active { + background: var(--red); + color: #fff; + border-color: var(--red); + box-shadow: 0 6px 14px rgba(226, 38, 38, 0.18); +} + +/* 三列区域 */ +.content { + display: grid; + grid-template-columns: clamp(180px, 22%, 240px) 1fr clamp(220px, 26%, 300px); + gap: var(--gap); +} + +.panel { + border: var(--border); + border-radius: var(--radius); + background: #fff; + min-height: 380px; + display: flex; + align-items: center; + justify-content: center; + font-size: 16px; + color: var(--red); + text-align: center; + box-shadow: var(--shadow); + transition: transform 0.2s ease, box-shadow 0.2s ease; +} +.panel:hover { + transform: translateY(-2px); + box-shadow: var(--shadow-hover); +} + +/* 讨论区固定高度并使用内部滚动,不让页面无限增长 */ +#discussion.panel { + align-items: stretch; + justify-content: flex-start; + height: 380px; /* 与其他面板保持一致高度 */ +} + +.panel:hover { + transform: translateY(-2px); + box-shadow: 0 14px 28px rgba(226, 38, 38, 0.1); +} + +/* 响应式:窄屏改为纵向排列 */ +@media (max-width: 992px) { + .content { grid-template-columns: 1fr; } + .panel { min-height: 220px; } + #discussion.panel { height: 240px; } +} + +/* ===== 视频区域样式 ===== */ +.video-wrap { + position: relative; + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + padding: 4px; /* 边距缩小,减少白边 */ +} + +#video.panel { position: relative; } +#video.panel::before { + content: ""; + position: absolute; + inset: 0; + padding: 2px; + border-radius: var(--radius); + background: conic-gradient( + var(--red) 0deg, + var(--red) calc(var(--progress, 0) * 1deg), + rgba(226, 38, 38, 0.18) calc(var(--progress, 0) * 1deg), + rgba(226, 38, 38, 0.18) 360deg + ); + -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0); + -webkit-mask-composite: xor; + mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0); + mask-composite: exclude; + pointer-events: none; +} + +/* 视频面板固定高度,独立于图片面板的高度变化 */ +#video.panel { align-items: stretch; justify-content: flex-start; height: 380px; overflow: hidden; } + +/* 菜单栏中的“包含本地”控件样式 */ +.menu__controls { + background: #f6f6f6; + border: 1px solid var(--red); + color: var(--red); + padding: 6px 12px; + border-radius: 999px; + font-size: 13px; + box-shadow: 0 6px 14px rgba(226, 38, 38, 0.08); + user-select: none; +} +.menu__controls input[type="checkbox"] { vertical-align: middle; margin-right: 6px; } + +video#player { + width: 100%; + flex: 1 1 auto; + height: 100%; /* 使视频充满可用高度,被“框”包裹 */ + display: block; + border-radius: 6px; /* 圆角减小,进一步减少四角的白边显著程度 */ + background: #000; + object-fit: contain; /* 保持原视频比例 */ + opacity: 0; + transition: opacity 0.8s ease-in-out; +} +video#player.visible { opacity: 1; } + +/* 顶部提示气泡已移除 */ + +/* 工具栏与按钮已移除,根据用户需求隐藏相关样式占位 */ + +/* ===== 讨论区样式 ===== */ +.discussion-wrap { + width: 100%; + height: 100%; + padding: 12px; + display: flex; + flex-direction: column; + gap: 12px; + overflow-y: auto; + scrollbar-width: thin; + scrollbar-color: var(--red) #fff1f1; /* 滑块红 / 轨道浅红 */ +} +/* Webkit 内核滚动条 */ +.discussion-wrap::-webkit-scrollbar { + width: 8px; +} +.discussion-wrap::-webkit-scrollbar-track { + background: #fff1f1; + border-radius: 4px; +} +.discussion-wrap::-webkit-scrollbar-thumb { + background: var(--red); + border-radius: 4px; +} +.discussion-wrap::-webkit-scrollbar-thumb:hover { + background: #c53030; +} + +.bubble { + display: flex; + align-items: flex-start; + gap: 10px; + max-width: 92%; + animation: fadeIn 0.25s ease; +} +.bubble.right { flex-direction: row-reverse; margin-left: auto; } + +.avatar { + width: 36px; + height: 36px; + border-radius: 50%; + flex: 0 0 36px; + object-fit: cover; + border: 2px solid var(--red); +} + +.bubble__text { + background: #fff1f1; + border: 1px solid var(--red); + color: #a81515; + padding: 8px 12px; + border-radius: 12px; + box-shadow: 0 6px 14px rgba(226, 38, 38, 0.08); + font-size: 14px; + line-height: 1.5; +} + +@keyframes fadeIn { + from { opacity: 0; transform: translateY(6px); } + to { opacity: 1; transform: translateY(0); } +} + +/* ===== 图片区域样式 ===== */ +#gallery.panel { align-items: stretch; justify-content: flex-start; position: relative; } +#gallery.panel::before { + content: ""; + position: absolute; + inset: 0; + padding: 2px; + border-radius: var(--radius); + background: conic-gradient( + var(--red) 0deg, + var(--red) calc(var(--pic-progress, 0) * 1deg), + rgba(226, 38, 38, 0.18) calc(var(--pic-progress, 0) * 1deg), + rgba(226, 38, 38, 0.18) 360deg + ); + -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0); + -webkit-mask-composite: xor; + mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0); + mask-composite: exclude; + pointer-events: none; +} +.picture-wrap { + position: relative; + width: 100%; + height: 100%; + padding: 4px; /* 同步减少图片区的白边 */ + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; +} + +#pictureImg { + max-width: 100%; + max-height: 100%; + object-fit: cover; + border-radius: 6px; /* 圆角减小以减少角部白边 */ + box-shadow: 0 10px 22px rgba(226, 38, 38, 0.08); + opacity: 0; + transition: opacity 0.8s ease-in-out; +} +#pictureImg.visible { opacity: 1; } + +@media (max-width: 992px) { + #gallery.panel { height: 240px; } + #video.panel { height: 240px; } +} + +/* 移除对视频框悬停效果的覆盖,恢复默认面板的上浮动画 */ + +/* ===== 投稿(上传)页面通用样式 ===== */ +.upload-container { + max-width: 980px; + margin: 24px auto; + padding: 0 16px; +} +.upload-container h1 { + font-size: 20px; + color: var(--red); + margin: 0 0 16px; + font-weight: 600; +} + +.upload-title { + margin: 0 0 16px; + font-size: 22px; +} + +.upload-grid.two { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 16px; +} + +.upload-grid.video { + display: grid; + grid-template-columns: 1fr 360px; + gap: 16px; + align-items: start; +} + +@media (max-width: 880px) { + .upload-grid.two { grid-template-columns: 1fr; } + .upload-grid.video { grid-template-columns: 1fr; } +} + +.card { + border: var(--border); + border-radius: var(--radius); + padding: 16px; + background: #fff; + box-shadow: var(--shadow); +} + +.dropzone { + border: 2px dashed var(--red); + border-radius: var(--radius); + padding: 16px; + text-align: center; + background: #fff1f1; + color: var(--red); + cursor: pointer; + box-shadow: var(--shadow); + transition: background 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease; +} +.dropzone.dragging, +.dropzone:hover { background: #ffeaea; border-color: #c11; box-shadow: var(--shadow-hover); } + +.field { display: flex; flex-direction: column; gap: 8px; } +.field input[type="text"] { + padding: 10px 12px; + border: 1px solid var(--red); + border-radius: var(--radius); + font-size: 14px; + transition: border-color .2s ease; +} +.field input[type="text"]:focus { + border-color: #c53030; + outline: none; +} + +.preview-img { + width: 100%; + height: 280px; + object-fit: contain; + border: 1px solid var(--red); + border-radius: var(--radius); + background: #fafafa; +} +.video-preview { + width: 100%; + height: 220px; + object-fit: contain; + border-radius: var(--radius); + background: #000; +} +.meta { + font-size: 12px; + color: var(--red); + margin-top: 8px; +} +.errors { color: var(--red); font-size: 13px; min-height: 18px; } +.tips { color: var(--red); opacity: .8; font-size: 13px; } + +.actions { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; } + +.btn { + background: linear-gradient(135deg, var(--red) 0%, #c53030 100%); + color: #fff; + border: none; + padding: 10px 18px; + border-radius: var(--radius); + cursor: pointer; + font-size: 14px; + transition: opacity .25s ease, transform .2s ease; +} +.btn:hover { + opacity: .92; + transform: translateY(-1px); +} +.btn[disabled] { + opacity: .5; + cursor: not-allowed; +} + +.btn-ghost { + display: inline-block; + text-decoration: none; + color: var(--red); + padding: 8px 14px; + border-radius: var(--radius); + border: 1px solid var(--red); + background: #fff; + font-size: 14px; + transition: background .25s ease, transform .2s ease; +} +.btn-ghost:hover { + background: #fff1f1; + transform: translateY(-1px); +} + +.hidden-input { position: absolute; left: -9999px; } diff --git a/index.php b/index.php new file mode 100644 index 0000000..7e071de --- /dev/null +++ b/index.php @@ -0,0 +1,192 @@ + + + + + + 摸鱼办 + + + + + +
+ + +
+
+
+ +
+
+
+
+
+ +
+
+ + + diff --git a/review.php b/review.php new file mode 100644 index 0000000..e43aa6b --- /dev/null +++ b/review.php @@ -0,0 +1,174 @@ + + + + + + + 管理员登录 + + + + +
+ +
+

请输入管理员密码

+
+
+ + +
+
+ +
+ +
+ +
+
+
+ + + + + + + + + 视频审核 + + + + +
+ +
+

待审核列表(

+ +
+
+ +

当前无待审核视频。

+ +
+ +
+
文件:
+
原名:
+
标题:
+
+
+ + +
+
+ +
+
+ + + diff --git a/review_pictures.php b/review_pictures.php new file mode 100644 index 0000000..3bff1b9 --- /dev/null +++ b/review_pictures.php @@ -0,0 +1,172 @@ + + + + + + + 管理员登录 - 图片审核 + + + + +
+ +
+

请输入管理员密码

+
+
+ + +
+
+ +
+ +
+ +
+
+
+ + + + + + + + + 图片审核 + + + + +
+ +
+

待审核图片(

+
+
+ +

当前无待审核图片。

+ +
+ 待审核图片 +
+
文件:
+
原名:
+
标题:
+
+
+ + +
+
+ +
+
+ + + + + diff --git a/review_texts.php b/review_texts.php new file mode 100644 index 0000000..67f07d4 --- /dev/null +++ b/review_texts.php @@ -0,0 +1,180 @@ + + + + + + + 管理员登录 - 文案审核 + + + + +
+ +
+

请输入管理员密码

+
+
+ + +
+
+ +
+ +
+ +
+
+
+ + + + + + + + + 文案审核 + + + + +
+ +
+

待审核文案(

+
+
+ +

当前无待审核文案。

+ +
+

+
 strlen($preview)) ? '…' : ''; ?>
+
+
文件:
+
投稿时间:
+
+
+ + +
+
+ +
+
+ + + + diff --git a/upload.php b/upload.php new file mode 100644 index 0000000..931530f --- /dev/null +++ b/upload.php @@ -0,0 +1,131 @@ + + + + + + + 投稿视频 + + + +
+ +
+

上传你想分享的视频

+
+
+ +
+ 拖拽文件到此处,或点击选择文件 +
+ +
+
+ + +
+ + 管理员审核入口 +
+
+ +

说明:投稿后文件会进入“待审核”队列,管理员审核通过后才会出现在播放列表中。单个视频大小限制:50MB。

+
+
+
+ + + diff --git a/upload_picture.php b/upload_picture.php new file mode 100644 index 0000000..5e8b08e --- /dev/null +++ b/upload_picture.php @@ -0,0 +1,96 @@ + + + + + + + 图片投稿 + + + +
+ + +
+

提交你的图片

+
+
+
+

拖拽图片到此,或点击选择文件

+ +

支持 jpg / jpeg / png / gif / webp

+
+
+ + +
+
+
+ 预览图 +
未选择文件
+
+
+ + 管理员审核入口 +
+
+
+

说明:提交后图片会保存到“待处理”目录,管理员审核通过后将入库,随后可在本地随机图片中显示。单张图片大小限制:10MB。

+
+
+ + + diff --git a/upload_text.php b/upload_text.php new file mode 100644 index 0000000..d6d004e --- /dev/null +++ b/upload_text.php @@ -0,0 +1,54 @@ + + + + + + + 投稿文案 + + + + +
+ +
+

提交你的文案/句子

+
+
+ + +
+
+ + +
+
+ + 管理员审核入口 +
+

说明:提交后文案会保存到“待处理”目录,后续可按需添加审核或自动入库。

+
+
+
+ +