first commit

This commit is contained in:
2025-07-14 14:55:25 +08:00
commit ace33d202f
473 changed files with 117431 additions and 0 deletions
+19
View File
@@ -0,0 +1,19 @@
<?php
/**
* 商业使用授权协议
*
* Copyright (c) 2025 [云泽网]. 保留所有权利.
*
* 本软件仅供评估使用。任何商业用途必须获得书面授权许可。
* 未经授权商业使用本软件属于侵权行为,将承担法律责任。
*
* 授权购买请联系: 357099073@qq.com
* 官方网站: https://www.yunzer.cn
*
* 评估用户须知:
* 1. 禁止移除版权声明
* 2. 禁止用于生产环境
* 3. 禁止转售或分发
*/
// 这是系统自动生成的公共文件
+521
View File
@@ -0,0 +1,521 @@
<?php
/**
* 商业使用授权协议
*
* Copyright (c) 2025 [云泽网]. 保留所有权利.
*
* 本软件仅供评估使用。任何商业用途必须获得书面授权许可。
* 未经授权商业使用本软件属于侵权行为,将承担法律责任。
*
* 授权购买请联系: 357099073@qq.com
* 官方网站: https://www.yunzer.cn
*
* 评估用户须知:
* 1. 禁止移除版权声明
* 2. 禁止用于生产环境
* 3. 禁止转售或分发
*/
/**
* 文章控制器
*/
namespace app\index\controller;
use app\index\controller\BaseController;
use app\index\model\Users;
use think\facade\Db;
use think\facade\View;
use think\facade\Request;
use app\index\model\Articles\Articles;
use app\index\model\Articles\ArticlesCategory;
use app\index\model\Authors\Authors;
use app\index\model\Resources\Resources;
class ArticlesController extends BaseController
{
//文章中心
public function index()
{
// 获取前端传来的分类ID
$cateid = input('cateid/d', 0); // 使用input助手函数获取参数,并转换为整数
$page = input('page/d', 1);
$limit = input('limit/d', 10);
// 获取所有顶级分类
$categories = ArticlesCategory::where('cid', 0)
->where('delete_time', null)
->where('status', 1)
->select()
->toArray();
// 获取顶级分类信息
$category = null;
if ($cateid > 0) {
$category = ArticlesCategory::where('id', $cateid)
->where('delete_time', null)
->where('status', 1)
->find();
}
// 获取所有子分类
$subCategories = [];
if ($cateid > 0) {
$subCategories = ArticlesCategory::where('cid', $cateid)
->where('delete_time', null)
->where('status', 1)
->select()
->toArray();
}
// 获取所有子分类ID
$subCategoryIds = array_column($subCategories, 'id');
if ($cateid > 0) {
$subCategoryIds[] = $cateid;
}
// 构建文章查询条件
$where = [
['delete_time', '=', null],
['status', '=', 2]
];
if (!empty($subCategoryIds)) {
$where[] = ['cate', 'in', $subCategoryIds];
}
// 查询文章
$articles = Articles::where($where)
->order('id DESC')
->page($page, $limit)
->select()
->toArray();
// 按子分类分组文章
$groupedArticles = [];
foreach ($subCategories as $subCategory) {
$groupedArticles[$subCategory['id']] = [
'id' => $subCategory['id'],
'name' => $subCategory['name'],
'desc' => $subCategory['desc'],
'image' => $subCategory['image'],
'list' => []
];
}
// 将文章分配到对应的子分类
foreach ($articles as $article) {
if (isset($groupedArticles[$article['cate']])) {
// 如果文章图片为空,使用分类图片
if (empty($article['image'])) {
$article['image'] = $groupedArticles[$article['cate']]['image'];
}
$groupedArticles[$article['cate']]['list'][] = $article;
}
}
// 获取总数
$total = Articles::where($where)->count();
// 准备返回数据
$data = [
'cate' => [
'id' => $cateid,
'name' => $category ? $category['name'] : '',
'desc' => $category ? $category['desc'] : '',
'image' => $category ? $category['image'] : '',
'subCategories' => array_values($groupedArticles),
'total' => $total,
'page' => $page,
'limit' => $limit
]
];
// 根据请求方式返回不同的输出
if (request()->isPost()) {
return json([
'code' => 1,
'msg' => '获取成功',
'data' => $data
]);
} else {
// 为视图准备数据
$viewData = [
'categories' => $categories,
'cate' => $data['cate']
];
return view('index', $viewData);
}
}
// 文章列表页
public function list()
{
// 获取分类ID
$cateId = Request::param('cate/d', 0);
// 构建查询条件
$where = [
['a.delete_time', '=', null],
['a.status', '=', 2]
];
if ($cateId > 0) {
$where[] = ['a.cate', '=', $cateId];
}
// 获取文章列表
$articles = Articles::alias('a')
->join('articles_category c', 'a.cate = c.id')
->where($where)
->field([
'a.*',
'IF(a.image IS NULL OR a.image = "", c.image, a.image) as image'
])
->order('a.id DESC')
->paginate([
'list_rows' => 10,
'query' => Request::instance()->param()
]);
// 获取分类信息
$category = null;
if ($cateId > 0) {
$category = ArticlesCategory::where('id', $cateId)
->where('delete_time', null)
->where('status', 3)
->find();
}
// 获取所有分类
$categories = ArticlesCategory::where('delete_time', null)
->where('status', 3)
->select()
->toArray();
// 根据请求方式返回不同的输出
if (request()->isPost()) {
return json([
'code' => 1,
'msg' => '获取成功',
'data' => [
'articles' => $articles->items(),
'category' => $category,
'categories' => $categories,
'total' => $articles->total(),
'current_page' => $articles->currentPage(),
'per_page' => $articles->listRows()
]
]);
} else {
// 将变量传递给视图
View::assign([
'articles' => $articles,
'category' => $category,
'categories' => $categories
]);
return view('list');
}
}
// 文章详情页
public function detail()
{
$id = Request::param('id/d', 0);
$article = Articles::where('id', $id)->find();
if (!$article) {
return json(['code' => 0, 'msg' => '文章不存在或已被删除']);
}
// 获取分类名称
$cateName = ArticlesCategory::where('id', $article['cate'])
->value('name');
// 获取作者信息
$authorInfo = Users::where('name', $article['author'])->find();
if ($authorInfo) {
// 统计作者的文章数
$articleCount = Articles::where('author', $article['author'])->count();
// 统计作者的资源数
$resourceCount = Resources::where('uploader', $article['author'])->count();
$authorData = [
'avatar' => $authorInfo['avatar'] ?: '/static/images/avatar.png',
'name' => $authorInfo['name'],
'resource_count' => $resourceCount,
'article_count' => $articleCount
];
} else {
$authorData = [
'avatar' => '/static/images/avatar.png',
'name' => $article['author'],
'resource_count' => 0,
'article_count' => 0
];
}
// 获取上一篇和下一篇文章
$prevArticle = Articles::where('id', '<', $id)
->where('delete_time', null)
->where('status', '<>', 3)
->where('cate', $article['cate'])
->field(['id', 'title'])
->order('id DESC')
->find();
$nextArticle = Articles::where('id', '>', $id)
->where('delete_time', null)
->where('status', '<>', 3)
->where('cate', $article['cate'])
->field(['id', 'title'])
->order('id ASC')
->find();
// 获取相关文章(同分类的其他文章)
$relatedArticles = Articles::alias('a')
->join('articles_category c', 'a.cate = c.id')
->where('a.cate', $article['cate'])
->where('a.id', '<>', $id)
->where('a.delete_time', null)
->where('a.status', '=', 2)
->field([
'a.id',
'a.title',
'IF(a.image IS NULL OR a.image = "", c.image, a.image) as image'
])
->order('a.id DESC')
->limit(3)
->select()
->toArray();
// 如果是 POST 请求,返回 JSON 数据
if (Request::isPost()) {
return json([
'code' => 1,
'msg' => '获取成功',
'data' => [
'authorInfo' => $authorData,
'article' => $article,
'cateName' => $cateName,
'prevArticle' => $prevArticle,
'nextArticle' => $nextArticle,
'relatedArticles' => $relatedArticles
]
]);
}
// GET 请求返回视图
View::assign([
'authorInfo' => $authorData,
'article' => $article,
'cateName' => $cateName,
'prevArticle' => $prevArticle,
'nextArticle' => $nextArticle,
'relatedArticles' => $relatedArticles
]);
return view('detail');
}
// 文章点赞
public function like()
{
if (!Request::isAjax()) {
return json(['code' => 0, 'msg' => '非法请求']);
}
$id = Request::param('id/d', 0);
// 检查文章是否存在
$article = Articles::where('id', $id)
->where('delete_time', null)
->where('status', 2)
->find();
if (!$article) {
return json(['code' => 0, 'msg' => '文章不存在或已被删除']);
}
// 更新点赞数
$result = Articles::where('id', $id)
->where('delete_time', null)
->inc('likes', 1)
->update();
if ($result) {
// 返回更新后的点赞数
$newLikes = $article['likes'] + 1;
return json([
'code' => 1,
'msg' => '点赞成功',
'data' => [
'likes' => $newLikes
]
]);
} else {
return json(['code' => 0, 'msg' => '点赞失败']);
}
}
// 提交评论
public function comment()
{
if (!Request::isAjax() || !Request::isPost()) {
return json(['code' => 0, 'msg' => '非法请求']);
}
$articleId = Request::param('article_id/d', 0);
$content = Request::param('content/s', '');
$parentId = Request::param('parent_id/d', 0);
if (empty($content)) {
return json(['code' => 0, 'msg' => '评论内容不能为空']);
}
// 检查文章是否存在
$article = Articles::where('id', $articleId)
->where('delete_time', null)
->where('status', 3)
->find();
if (!$article) {
return json(['code' => 0, 'msg' => '文章不存在或已被删除']);
}
// 添加评论
// $data = [
// 'article_id' => $articleId,
// 'content' => $content,
// 'parent_id' => $parentId,
// 'user_id' => $this->getUserId(),
// 'user_name' => $this->getUserName(),
// 'status' => 1,
// 'create_time' => time()
// ];
// $result = Db::table('yz_article_comment')->insert($data);
// if ($result) {
// return json(['code' => 1, 'msg' => '评论成功']);
// } else {
// return json(['code' => 0, 'msg' => '评论失败']);
// }
}
// 获取当前用户ID(示例方法,实际应根据您的用户系统实现)
private function getUserId()
{
// 这里应该返回当前登录用户的ID
return 1; // 示例返回值
}
// 获取当前用户名(示例方法,实际应根据您的用户系统实现)
private function getUserName()
{
// 这里应该返回当前登录用户的用户名
return '游客'; // 示例返回值
}
// 获取访问统计
public function viewStats()
{
$id = Request::param('id/d', 0);
// 获取总访问量
$totalViews = Articles::where('id', $id)
->value('views');
return json([
'code' => 1,
'data' => [
'total' => $totalViews
]
]);
}
/**
* 更新文章访问次数
*/
public function updateViews()
{
if (!Request::isPost()) {
return json(['code' => 0, 'msg' => '非法请求']);
}
$id = Request::post('id');
if (!$id) {
return json(['code' => 0, 'msg' => '参数错误']);
}
try {
// 更新访问次数
$article = Articles::where('id', $id)->find();
if (!$article) {
return json(['code' => 0, 'msg' => '文章不存在']);
}
// 更新访问次数
Articles::where('id', $id)->inc('views')->update();
// 获取更新后的访问次数
$newViews = Articles::where('id', $id)->value('views');
return json(['code' => 1, 'msg' => '更新成功', 'data' => ['views' => $newViews]]);
} catch (\Exception $e) {
return json(['code' => 0, 'msg' => '更新失败:' . $e->getMessage()]);
}
}
//获取作者信息
public function getAuthorInfo()
{
if (!Request::isPost()) {
return json(['code' => 0, 'msg' => '非法请求']);
}
$authorName = Request::post('name');
if (empty($authorName)) {
return json(['code' => 0, 'msg' => '作者名称不能为空']);
}
try {
// 获取作者基本信息
$author = Db::name('users')
->where('name', $authorName)
->field('id, name, avatar')
->find();
if (!$author) {
return json(['code' => 0, 'msg' => '作者不存在']);
}
// 获取作者发布的资源数量
$resourceCount = Db::name('resources')
->where('user_id', $author['id'])
->where('delete_time', null)
->where('status', 2) // 假设2是已发布状态
->count();
// 获取作者发布的文章数量
$articleCount = Db::name('articles')
->where('author', $authorName)
->where('delete_time', null)
->where('status', 2) // 假设2是已发布状态
->count();
return json([
'code' => 1,
'msg' => '获取成功',
'data' => [
'avatar' => $author['avatar'],
'name' => $author['name'],
'resource_count' => $resourceCount,
'article_count' => $articleCount
]
]);
} catch (\Exception $e) {
return json(['code' => 0, 'msg' => '获取作者信息失败:' . $e->getMessage()]);
}
}
}
+144
View File
@@ -0,0 +1,144 @@
<?php
/**
* 商业使用授权协议
*
* Copyright (c) 2025 [云泽网]. 保留所有权利.
*
* 本软件仅供评估使用。任何商业用途必须获得书面授权许可。
* 未经授权商业使用本软件属于侵权行为,将承担法律责任。
*
* 授权购买请联系: 357099073@qq.com
* 官方网站: https://www.yunzer.cn
*
* 评估用户须知:
* 1. 禁止移除版权声明
* 2. 禁止用于生产环境
* 3. 禁止转售或分发
*/
/**
* 前台系统-基础控制器
*/
namespace app\index\controller;
use app\AppApi;
use think\facade\Db;
use think\facade\View;
use think\facade\Cookie;
use think\facade\Config;
use think\exception\HttpResponseException;
use think\facade\Request;
use app\index\model\User;
class Base
{
public $config = [];
public $userId = null;
public $user = [];
public function __construct()
{
date_default_timezone_set('PRC');
# 获取配置
$this->config = Db::table('yz_admin_config')->select()->toArray();
// 将配置数据转换为键值对形式
$configData = [];
foreach ($this->config as $item) {
// 使用正确的字段名 config_name 和 config_value
if (isset($item['config_name']) && isset($item['config_value'])) {
$configData[$item['config_name']] = $item['config_value'];
}
}
$this->config = $configData;
# 获取用户信息
$this->userId = Cookie::get('user_id');
if (!empty($this->userId)) {
$this->user = User::where('uid', $this->userId)->find();
}
View::assign([
'user' => $this->user,
'config' => $this->config
]);
}
/**
* 返回json对象
*/
protected function returnCode($code, $data = [], $count = 10)
{
header('Content-type:application/json');
if ($code == 0) {
$arr = array(
'code' => $code,
'msg' => '成功',
'count' => $count,
'data' => $data
);
} else if ($code >= 1 && $code <= 100) {
$arr = array(
'code' => $code,
'msg' => $data
);
} else {
$appapi = new AppApi();
$arr = array(
'code' => $code,
'msg' => $appapi::errorTip($code)
);
}
echo json_encode($arr);
if ($code != 0) {
exit;
}
}
/**
* 操作成功跳转的快捷方法
* @access protected
* @param mixed $msg 提示信息
* @return void
*/
protected function success($msg = '')
{
$result = [
'code' => 1,
'msg' => $msg
];
$type = $this->getResponseType();
if ($type == 'html') {
$response = view(Config::get('app.dispatch_success_tmpl'), $result);
} else if ($type == 'json') {
$response = json($result);
}
throw new HttpResponseException($response);
}
/**
* 操作错误跳转的快捷方法
* @access protected
* @param mixed $msg 提示信息
* @return void
*/
protected function error($msg = '')
{
$result = [
'code' => 0,
'msg' => $msg
];
$response = view(Config::get('app.dispatch_error_tmpl'), $result);
throw new HttpResponseException($response);
}
/**
* 获取当前的response 输出类型
* @access protected
* @return string
*/
protected function getResponseType()
{
return Request::isJson() || Request::isAjax() ? 'json' : 'html';
}
}
+264
View File
@@ -0,0 +1,264 @@
<?php
/**
* 商业使用授权协议
*
* Copyright (c) 2025 [云泽网]. 保留所有权利.
*
* 本软件仅供评估使用。任何商业用途必须获得书面授权许可。
* 未经授权商业使用本软件属于侵权行为,将承担法律责任。
*
* 授权购买请联系: 357099073@qq.com
* 官方网站: https://www.yunzer.cn
*
* 评估用户须知:
* 1. 禁止移除版权声明
* 2. 禁止用于生产环境
* 3. 禁止转售或分发
*/
declare(strict_types=1);
namespace app\index\controller;
use think\App;
use think\facade\View;
use think\facade\Request;
use think\facade\Db;
use app\service\VisitStatsService;
use PHPMailer\PHPMailer\PHPMailer;
use app\index\model\MailConfig;
use app\index\model\AdminConfig;
use app\index\model\Users;
/**
* 前台控制器基础类
*/
abstract class BaseController
{
/**
* Request实例
* @var \think\Request
*/
protected $request;
protected $visitStats;
/**
* 应用实例
* @var \think\App
*/
protected $app;
/**
* 构造方法
* @access public
* @param App $app 应用对象
*/
public function __construct(App $app)
{
$this->app = $app;
$this->request = $this->app->request;
$this->visitStats = new VisitStatsService();
// 控制器初始化
$this->initialize();
}
/**
* 初始化
*/
protected function initialize()
{
// 记录访问
$this->visitStats->recordVisit($this->getControllerName());
// 获取配置
$configList = AdminConfig::where('config_status', 1)
->order('config_sort DESC')
->select()
->toArray();
// 将配置数据转换为键值对形式
$config = [];
foreach ($configList as $item) {
$config[$item['config_name']] = $item['config_value'];
}
// 判断用户是否登录
$userInfo = [];
if (session('user_id')) {
// 从数据库获取最新用户信息
$user = Users::where('uid', session('user_id'))->find();
if ($user) {
$userInfo = [
'id' => $user->uid,
'name' => $user->name,
'account' => $user->account,
'avatar' => $user->avatar ?? '/static/images/avatar.png',
'is_login' => true,
'last_login_time' => $user->last_login_time
];
} else {
// 用户不存在,清除session
session('user_id', null);
session('user_name', null);
$userInfo = ['is_login' => false];
}
} else {
$userInfo = ['is_login' => false];
}
// 设置通用变量
View::assign([
'config' => $config,
'userInfo' => $userInfo
]);
}
/**
* 获取控制器名称(移除Controller后缀)
* @return string
*/
public function getControllerName()
{
$className = get_class($this);
$className = substr($className, strrpos($className, '\\') + 1);
return str_replace('Controller', '', $className);
}
/**
* 渲染模板输出
* @param string $template 模板文件
* @param array $vars 模板变量
* @return string
*/
protected function fetch($template = '', $vars = [])
{
return View::fetch($template, $vars);
}
/**
* 操作成功跳转
* @param string $msg 提示信息
* @param string $url 跳转地址
* @param mixed $data 返回数据
* @param integer $wait 跳转等待时间
* @return \think\response\Json|string
*/
protected function success($msg = '', $url = null, $data = '', $wait = 3)
{
if (Request::isAjax()) {
return json([
'code' => 1,
'msg' => $msg,
'data' => $data,
'url' => $url
]);
}
return View::fetch('common/success', [
'msg' => $msg,
'url' => $url,
'data' => $data,
'wait' => $wait
]);
}
/**
* 操作失败跳转
* @param string $msg 提示信息
* @param string $url 跳转地址
* @param mixed $data 返回数据
* @param integer $wait 跳转等待时间
* @return \think\response\Json|string
*/
protected function error($msg = '', $url = null, $data = '', $wait = 3)
{
if (Request::isAjax()) {
return json([
'code' => 0,
'msg' => $msg,
'data' => $data,
'url' => $url
]);
}
return View::fetch('common/error', [
'msg' => $msg,
'url' => $url,
'data' => $data,
'wait' => $wait
]);
}
protected function sendEmail($to, $content, $title)
{
// 获取邮件配置
$mailConfig = MailConfig::where('id', 1)->find();
if (!$mailConfig) {
return '邮件配置不存在';
}
//实例化PHPMailer核心类
$mail = new PHPMailer();
//是否启用smtp的debug进行调试 开发环境建议开启 生产环境注释掉即可 默认关闭debug调试模式
$mail->SMTPDebug = 0;
//使用smtp鉴权方式发送邮件
$mail->isSMTP();
//smtp需要鉴权 这个必须是true
$mail->SMTPAuth = true;
//链接qq域名邮箱的服务器地址
$mail->Host = $mailConfig['smtp_host'];
//设置使用ssl加密方式登录鉴权
$mail->SMTPSecure = 'ssl';
//设置ssl连接smtp服务器的远程服务器端口号
$mail->Port = $mailConfig['smtp_port'];
//设置发件人的主机域
$mail->Hostname = $mailConfig['smtp_email'];
//设置发送的邮件的编码
$mail->CharSet = 'UTF-8';
//设置发件人姓名(昵称)
$mail->FromName = $mailConfig['smtp_name'];
//smtp登录的账号
$mail->Username = $mailConfig['smtp_email'];
//smtp登录的密码
$mail->Password = $mailConfig['smtp_password'];
//设置发件人邮箱地址
$mail->setFrom($mailConfig['smtp_email'], $mailConfig['smtp_name']);
//邮件正文是否为html编码
$mail->isHTML(true);
//设置收件人邮箱地址
$mail->addAddress($to);
//添加该邮件的主题
$mail->Subject = $title;
//添加邮件正文
$mail->Body = $content;
try {
$status = $mail->send();
if ($status) {
return '发送成功';
} else {
return '发送失败';
}
} catch (\Exception $e) {
return '发送失败:' . $e->getMessage();
}
}
}
+391
View File
@@ -0,0 +1,391 @@
<?php
/**
* 商业使用授权协议
*
* Copyright (c) 2025 [云泽网]. 保留所有权利.
*
* 本软件仅供评估使用。任何商业用途必须获得书面授权许可。
* 未经授权商业使用本软件属于侵权行为,将承担法律责任。
*
* 授权购买请联系: 357099073@qq.com
* 官方网站: https://www.yunzer.cn
*
* 评估用户须知:
* 1. 禁止移除版权声明
* 2. 禁止用于生产环境
* 3. 禁止转售或分发
*/
/**
* 游戏下载控制器
*/
namespace app\index\controller;
use app\index\controller\BaseController;
use think\facade\Db;
use think\facade\View;
use think\facade\Request;
use app\index\model\Resources\Resources;
use app\index\model\Resources\ResourcesCategory;
use app\index\model\Attachments;
class GameController extends BaseController
{
//资源中心
public function index()
{
// 获取前端传来的分类ID
$cateid = input('cateid/d', 0);
$page = input('page/d', 1);
$limit = input('limit/d', 10);
// 获取所有顶级分类
$categories = ResourcesCategory::where('cid', 0)
->where('delete_time', null)
->where('status', 1)
->select()
->toArray();
// 获取顶级分类信息
$category = null;
if ($cateid > 0) {
$category = ResourcesCategory::where('id', $cateid)
->where('delete_time', null)
->where('status', 1)
->find();
}
// 获取所有子分类
$subCategories = [];
if ($cateid > 0) {
$subCategories = ResourcesCategory::where('cid', $cateid)
->where('delete_time', null)
->where('status', 1)
->select()
->toArray();
}
// 获取所有子分类ID
$subCategoryIds = array_column($subCategories, 'id');
if ($cateid > 0) {
$subCategoryIds[] = $cateid;
}
// 构建游戏查询条件
$where = [
['delete_time', '=', null],
['status', '=', 1]
];
if (!empty($subCategoryIds)) {
$where[] = ['cate', 'in', $subCategoryIds];
}
// 查询游戏
$games = Resources::where($where)
->order('id DESC')
->page($page, $limit)
->select()
->toArray();
// 按子分类分组游戏
$groupedGames = [];
foreach ($subCategories as $subCategory) {
$groupedGames[$subCategory['id']] = [
'id' => $subCategory['id'],
'name' => $subCategory['name'],
'list' => []
];
}
// 将游戏分配到对应的子分类
foreach ($games as $game) {
if (isset($groupedGames[$game['cate']])) {
$groupedGames[$game['cate']]['list'][] = $game;
}
}
// 获取总数
$total = Resources::where($where)->count();
// 准备返回数据
$data = [
'cate' => [
'id' => $cateid,
'name' => $category ? $category['name'] : '',
'desc' => $category ? $category['desc'] : '',
'image' => $category ? $category['image'] : '',
'subCategories' => array_values($groupedGames),
'total' => $total,
'page' => $page,
'limit' => $limit
]
];
// 根据请求方式返回不同的输出
if (request()->isPost()) {
return json([
'code' => 1,
'msg' => '获取成功',
'data' => $data
]);
} else {
// 为视图准备数据
$viewData = [
'categories' => $categories,
'cate' => $data['cate']
];
return view('index', $viewData);
}
}
// 游戏列表页
public function list()
{
// 获取分类ID
$cateId = Request::param('cate/d', 0);
// 构建查询条件
$where = [
['a.delete_time', '=', null],
['a.status', '=', 1]
];
if ($cateId > 0) {
$where[] = ['a.cate', '=', $cateId];
}
// 获取游戏列表
$games = Resources::alias('a')
->join('resources_category c', 'a.cate = c.id')
->where($where)
->field([
'a.*',
'IF(a.icon IS NULL OR a.icon = "", c.icon, a.icon) as icon'
])
->order('a.id DESC')
->paginate([
'list_rows' => 10,
'query' => Request::instance()->param()
]);
// 获取分类信息
$category = null;
if ($cateId > 0) {
$category = ResourcesCategory::where('id', $cateId)
->where('delete_time', null)
->where('status', 1)
->find();
}
// 获取所有分类
$categories = ResourcesCategory::where('delete_time', null)
->where('status', 1)
->select()
->toArray();
// 如果是POST请求,返回JSON数据
if (Request::isPost()) {
return json([
'code' => 1,
'msg' => '获取成功',
'data' => [
'games' => $games->items(),
'total' => $games->total(),
'current_page' => $games->currentPage(),
'per_page' => $games->listRows(),
'category' => $category
]
]);
}
// GET请求返回渲染的视图
View::assign([
'games' => $games,
'category' => $category,
'categories' => $categories
]);
return View::fetch('list');
}
// 游戏详情页
public function detail()
{
$id = Request::param('id/d', 0);
$game = Resources::where('id', $id)->find();
if (!$game) {
return json(['code' => 0, 'msg' => '游戏不存在或已被删除']);
}
// 如果size没有,从附件表中获取
if (empty($game['size']) && !empty($game['fileurl'])) {
$attachment = Attachments::where('src', $game['fileurl'])
->find();
if ($attachment && !empty($attachment['size'])) {
$size = $attachment['size'];
// 转换文件大小为合适的单位
if ($size >= 1073741824) { // 1GB = 1024MB = 1024*1024KB = 1024*1024*1024B
$game['size'] = round($size / 1073741824, 2) . 'GB';
} elseif ($size >= 1048576) { // 1MB = 1024KB = 1024*1024B
$game['size'] = round($size / 1048576, 2) . 'MB';
} else {
$game['size'] = round($size / 1024, 2) . 'KB';
}
}
}
// 获取分类名称
$cateName = ResourcesCategory::where('id', $game['cate'])
->value('name');
// 获取上一个和下一个游戏
$prevGame = Resources::where('id', '<', $id)
->where('delete_time', null)
->where('status', 1)
->where('cate', $game['cate'])
->field(['id', 'title'])
->order('id DESC')
->find();
$nextGame = Resources::where('id', '>', $id)
->where('delete_time', null)
->where('status', 1)
->where('cate', $game['cate'])
->field(['id', 'title'])
->order('id ASC')
->find();
// 获取相关游戏(同分类的其他游戏)
$relatedGames = Db::table('yz_resources')
->alias('g')
->join('yz_resources_category c', 'g.cate = c.id')
->where('g.cate', $game['cate'])
->where('g.id', '<>', $id)
->where('g.delete_time', null)
->where('g.status', 1)
->field([
'g.id',
'g.title',
'IF(g.icon IS NULL OR g.icon = "", c.icon, g.icon) as icon'
])
->order('g.id DESC')
->limit(3)
->select()
->toArray();
// 如果是 AJAX 请求,返回 JSON 数据
if (Request::isAjax()) {
return json([
'code' => 1,
'msg' => '获取成功',
'data' => [
'game' => $game,
'cateName' => $cateName,
'prevGame' => $prevGame,
'nextGame' => $nextGame,
'relatedGames' => $relatedGames
]
]);
}
// 非 AJAX 请求返回视图
View::assign([
'game' => $game,
'cateName' => $cateName,
'prevGame' => $prevGame,
'nextGame' => $nextGame,
'relatedGames' => $relatedGames
]);
return View::fetch('detail');
}
// 游戏下载
public function downloadurl()
{
if (!Request::isAjax()) {
return json(['code' => 0, 'msg' => '非法请求']);
}
$id = Request::param('id/d', 0);
// 获取游戏信息
$game = Resources::where('id', $id)
->where('delete_time', null)
->find();
if (!$game) {
return json(['code' => 0, 'msg' => '游戏不存在']);
}
// 更新下载次数
$result = Resources::where('id', $id)
->where('delete_time', null)
->inc('downloads', 1)
->update();
if ($result) {
return json([
'code' => 1,
'msg' => '下载成功',
'data' => [
'url' => $game['url']
]
]);
} else {
return json(['code' => 0, 'msg' => '下载失败']);
}
}
// 获取访问统计
public function viewStats()
{
$id = Request::param('id/d', 0);
// 获取总访问量
$totalViews = Resources::where('id', $id)
->value('views');
return json([
'code' => 1,
'data' => [
'total' => $totalViews
]
]);
}
/**
* 更新游戏访问次数
*/
public function updateViews()
{
if (!Request::isPost()) {
return json(['code' => 0, 'msg' => '非法请求']);
}
$id = Request::post('id');
if (!$id) {
return json(['code' => 0, 'msg' => '参数错误']);
}
try {
// 更新访问次数
$game = Resources::where('id', $id)->find();
if (!$game) {
return json(['code' => 0, 'msg' => '游戏不存在']);
}
// 更新访问次数
Resources::where('id', $id)->inc('views')->update();
// 获取更新后的访问次数
$newViews = Resources::where('id', $id)->value('views');
return json(['code' => 1, 'msg' => '更新成功', 'data' => ['views' => $newViews]]);
} catch (\Exception $e) {
return json(['code' => 0, 'msg' => '更新失败:' . $e->getMessage()]);
}
}
}
+427
View File
@@ -0,0 +1,427 @@
<?php
/**
* 商业使用授权协议
*
* Copyright (c) 2025 [云泽网]. 保留所有权利.
*
* 本软件仅供评估使用。任何商业用途必须获得书面授权许可。
* 未经授权商业使用本软件属于侵权行为,将承担法律责任。
*
* 授权购买请联系: 357099073@qq.com
* 官方网站: https://www.yunzer.cn
*
* 评估用户须知:
* 1. 禁止移除版权声明
* 2. 禁止用于生产环境
* 3. 禁止转售或分发
*/
/**
* 后台管理系统-首页
*/
namespace app\index\controller;
use app\index\controller\BaseController;
use think\facade\Db;
use think\facade\View;
use think\facade\Env;
use think\facade\Config;
use app\index\model\Banner;
use app\index\model\Resources\ResourcesCategory;
use app\index\model\Articles\ArticlesCategory;
use app\index\model\Resources\Resources;
use app\index\model\Articles\Articles;
use app\index\model\MailConfig;
use \think\facade\Filesystem;
use app\index\model\Attachments;
class IndexController extends BaseController
{
/**
* 首页
*/
public function index()
{
// 获取banner列表
$bannerList = Banner::where('delete_time', null)
->order('sort DESC, id DESC')
->select()
->toArray();
View::assign('bannerList', $bannerList);
return View::fetch();
}
/**
* 获取站点资讯列表
*/
public function siteNewslist()
{
// 获取站点资讯分类(顶级分类id为1的子分类)
$categories = ArticlesCategory::where('cid', 1)
->where('delete_time', null)
->select()
->toArray();
$articles = [];
$categoryData = [];
// 提取分类名称和ID用于前端tab显示
foreach ($categories as $category) {
$categoryData[] = [
'id' => $category['id'],
'name' => $category['name']
];
// 获取该分类下的文章,限制4条
$articles = Articles::where('cate', $category['id'])
->where('delete_time', null)
->where('status', 2)
->order('id', 'desc')
->field('id, cate, title, image, author, publishdate, views')
->limit(4)
->select()
->toArray();
}
return json([
'code' => 1,
'msg' => '获取成功',
'articles' => $articles,
'categories' => $categoryData
]);
}
/**
* 获取技术文章列表
*/
public function technicalArticleslist()
{
// 获取技术文章分类(顶级分类id为3的子分类)
$categories = ArticlesCategory::where('cid', 3)
->where('delete_time', null)
->select()
->toArray();
// 组装分类数据,方便后续查找
$categoryData = [];
$categoryImageMap = [];
$articlesByCategory = [];
foreach ($categories as $category) {
$categoryData[] = [
'id' => $category['id'],
'name' => $category['name']
];
$categoryImageMap[$category['id']] = $category['image'] ?? '';
// 获取每个分类下的文章,限制12条
$articles = Articles::where('cate', $category['id'])
->where('delete_time', null)
->where('status', 2)
->order('id', 'desc')
->field('id, cate, title, image, author, publishdate, views')
->limit(12)
->select()
->toArray();
// 替换image为空的文章
foreach ($articles as &$article) {
if (empty($article['image']) && !empty($categoryImageMap[$article['cate']])) {
$article['image'] = $categoryImageMap[$article['cate']];
}
}
unset($article);
$articlesByCategory[$category['id']] = $articles;
}
return json([
'code' => 1,
'msg' => '获取成功',
'articles' => $articlesByCategory,
'categories' => $categoryData
]);
}
/**
* 获取banner列表
*/
public function bannerlist()
{
// 获取启用状态的banner列表,按排序倒序
$bannerList = Banner::where('delete_time', null)
->order('sort DESC, id DESC')
->select()
->toArray();
return json(['code' => 1, 'msg' => '获取成功', 'banner' => $bannerList]);
}
/**
* 获取资源下载列表
*/
public function resourcesList()
{
// 获取资源分类(顶级分类id为2的子分类)
$categories = ResourcesCategory::where('cid', 2)
->where('delete_time', null)
->select()
->toArray();
// 组装分类数据
$categoryData = [];
$categoryImageMap = [];
$resourcesByCategory = [];
foreach ($categories as $category) {
$categoryData[] = [
'id' => $category['id'],
'name' => $category['name']
];
$categoryImageMap[$category['id']] = $category['image'] ?? '';
// 获取每个分类下的资源,限制8条
$resources = Resources::where('cate', $category['id'])
->where('delete_time', null)
->where('status', 1)
->order('id', 'desc')
->field('id, cate, title, desc, downloads, create_time, icon, views, uploader')
->limit(8)
->select()
->toArray();
// 替换thumbnail为空的资源
foreach ($resources as &$resource) {
if (empty($resource['thumbnail']) && !empty($categoryImageMap[$resource['cate']])) {
$resource['thumbnail'] = $categoryImageMap[$resource['cate']];
}
}
unset($resource);
$resourcesByCategory[$category['id']] = $resources;
}
// 合并所有分类的资源
$allResources = [];
foreach ($resourcesByCategory as $resources) {
$allResources = array_merge($allResources, $resources);
}
// 按上传时间排序
usort($allResources, function ($a, $b) {
return $b['create_time'] - $a['create_time'];
});
// 只取最新的8条
$allResources = array_slice($allResources, 0, 8);
return json([
'code' => 1,
'msg' => '获取成功',
'resources' => $allResources,
'categories' => $categoryData
]);
}
/**
* 获取程序下载列表
*/
public function programList()
{
// 获取程序分类(顶级分类id为4的子分类)
$categories = ResourcesCategory::where('cid', 1)
->where('delete_time', null)
->select()
->toArray();
// 组装分类数据
$categoryData = [];
$categoryImageMap = [];
$programsByCategory = [];
foreach ($categories as $category) {
$categoryData[] = [
'id' => $category['id'],
'name' => $category['name']
];
$categoryImageMap[$category['id']] = $category['image'] ?? '';
// 获取每个分类下的程序,限制8条
$programs = Resources::where('cate', $category['id'])
->where('delete_time', null)
->where('status', 1)
->order('id', 'desc')
->field('id, cate, title, desc, downloads, create_time, icon, views, uploader')
->limit(8)
->select()
->toArray();
// 替换thumbnail为空的程序
foreach ($programs as &$program) {
if (empty($program['thumbnail']) && !empty($categoryImageMap[$program['cate']])) {
$program['thumbnail'] = $categoryImageMap[$program['cate']];
}
}
unset($program);
$programsByCategory[$category['id']] = $programs;
}
// 合并所有分类的程序
$allPrograms = [];
foreach ($programsByCategory as $programs) {
$allPrograms = array_merge($allPrograms, $programs);
}
// 按上传时间排序
usort($allPrograms, function ($a, $b) {
return $b['create_time'] - $a['create_time'];
});
// 只取最新的8条
$allPrograms = array_slice($allPrograms, 0, 8);
return json([
'code' => 1,
'msg' => '获取成功',
'programs' => $allPrograms,
'categories' => $categoryData
]);
}
/**
* 获取游戏下载列表
*/
public function gameList()
{
// 获取游戏分类(顶级分类id为8的子分类)
$categories = ResourcesCategory::where('cid', 8)
->where('delete_time', null)
->select()
->toArray();
// 组装分类数据
$categoryData = [];
$categoryImageMap = [];
$programsByCategory = [];
foreach ($categories as $category) {
$categoryData[] = [
'id' => $category['id'],
'name' => $category['name'],
'image' => $category['image'] ?? ''
];
// 获取每个分类下的游戏,限制8条
$programs = Resources::where('cate', $category['id'])
->where('delete_time', null)
->where('status', 1)
->order('id', 'desc')
->field('id, cate, title, desc, downloads, create_time, icon, views, uploader, number, url, code')
->limit(8)
->select()
->toArray();
// 处理游戏数据
foreach ($programs as &$program) {
// 如果没有图标,使用分类图片
if (empty($program['icon']) && !empty($category['image'])) {
$program['icon'] = $category['image'];
}
// 格式化时间
$program['create_time'] = date('Y-m-d H:i:s', $program['create_time']);
}
unset($program);
$programsByCategory[$category['id']] = $programs;
}
// 合并所有分类的游戏
$allPrograms = [];
foreach ($programsByCategory as $programs) {
$allPrograms = array_merge($allPrograms, $programs);
}
// 按上传时间排序
usort($allPrograms, function ($a, $b) {
return strtotime($b['create_time']) - strtotime($a['create_time']);
});
// 只取最新的8条
$allPrograms = array_slice($allPrograms, 0, 8);
return json([
'code' => 0,
'msg' => '获取成功',
'data' => [
'games' => $allPrograms,
'categories' => $categoryData
]
]);
}
//保存附件信息到数据库
private function saveAttachment($name, $type, $size, $src)
{
$data = [
'name' => $name,
'type' => $type,
'size' => $size,
'src' => $src,
'create_time' => time(),
'update_time' => time()
];
return Attachments::insertGetId($data);
}
//上传图片接口
public function update_imgs()
{
// 获取上传的文件
$file = request()->file();
$files = request()->file('file');
// 检查是否有文件上传
if (empty($file)) {
return json(['code' => 1, 'msg' => '没有文件上传']);
}
try {
// 验证上传的文件
validate([
'image' => 'filesize:51200|fileExt:jpg,png,gif,jpeg,webp'
])->check($file);
// 存储文件到public磁盘的uploads目录
$info = Filesystem::disk('public')->putFile('uploads', $files);
// 处理文件路径,统一使用正斜杠
$info = str_replace("\\", "/", $info);
$img = '/storage/' . $info;
// 保存附件信息
$fileName = $files->getOriginalName();
$fileSize = $files->getSize();
$attachmentId = $this->saveAttachment($fileName, 1, $fileSize, $img); // 1: 图片
// 返回成功信息
return json([
'code' => 0,
'data' => [
'url' => $img
],
'msg' => '上传成功'
]);
} catch (\think\exception\ValidateException $e) {
// 捕获验证异常并返回错误信息
return json(['code' => 1, 'msg' => $e->getMessage()]);
} catch (\Exception $e) {
// 捕获其他异常
return json(['code' => 1, 'msg' => '上传失败:' . $e->getMessage()]);
}
}
}
+427
View File
@@ -0,0 +1,427 @@
<?php
/**
* 商业使用授权协议
*
* Copyright (c) 2025 [云泽网]. 保留所有权利.
*
* 本软件仅供评估使用。任何商业用途必须获得书面授权许可。
* 未经授权商业使用本软件属于侵权行为,将承担法律责任。
*
* 授权购买请联系: 357099073@qq.com
* 官方网站: https://www.yunzer.cn
*
* 评估用户须知:
* 1. 禁止移除版权声明
* 2. 禁止用于生产环境
* 3. 禁止转售或分发
*/
/**
* 程序下载控制器
*/
namespace app\index\controller;
use app\index\controller\BaseController;
use think\Response;
use think\facade\Db;
use think\facade\View;
use think\facade\Request;
use app\index\model\Resources\Resources;
use app\index\model\Resources\ResourcesCategory;
use app\index\model\Attachments;
use app\index\model\Users;
use app\index\model\Articles\Articles;
class ProgramController extends BaseController
{
//资源中心
public function index()
{
// 获取前端传来的分类ID
$cateid = input('cateid/d', 0);
$page = input('page/d', 1);
$limit = input('limit/d', 10);
// 获取所有顶级分类
$categories = ResourcesCategory::where('cid', 0)
->where('delete_time', null)
->where('status', 1)
->select()
->toArray();
// 获取顶级分类信息
$category = null;
if ($cateid > 0) {
$category = ResourcesCategory::where('id', $cateid)
->where('delete_time', null)
->where('status', 1)
->find();
}
// 获取所有子分类
$subCategories = [];
if ($cateid > 0) {
$subCategories = ResourcesCategory::where('cid', $cateid)
->where('delete_time', null)
->where('status', 1)
->select()
->toArray();
}
// 获取所有子分类ID
$subCategoryIds = array_column($subCategories, 'id');
if ($cateid > 0) {
$subCategoryIds[] = $cateid;
}
// 构建资源查询条件
$where = [
['delete_time', '=', null],
['status', '=', 1]
];
if (!empty($subCategoryIds)) {
$where[] = ['cate', 'in', $subCategoryIds];
}
// 查询资源
$programs = Resources::where($where)
->order('id DESC')
->page($page, $limit)
->select()
->toArray();
// 处理每个资源的size
foreach ($programs as &$program) {
if (empty($program['size'])) {
// 从Attachments表中查找对应的src
$attachment = Attachments::where('src', $program['icon'])
->field('size')
->find();
if ($attachment) {
$program['size'] = $attachment['size'];
}
}
}
// 按子分类分组资源
$groupedPrograms = [];
foreach ($subCategories as $subCategory) {
$groupedPrograms[$subCategory['id']] = [
'id' => $subCategory['id'],
'name' => $subCategory['name'],
'list' => []
];
}
// 将资源分配到对应的子分类
foreach ($programs as $program) {
if (isset($groupedPrograms[$program['cate']])) {
$groupedPrograms[$program['cate']]['list'][] = $program;
}
}
// 获取总数
$total = Resources::where($where)->count();
// 准备返回数据
$data = [
'cate' => [
'id' => $cateid,
'name' => $category ? $category['name'] : '',
'desc' => $category ? $category['desc'] : '',
'image' => $category ? $category['image'] : '',
'subCategories' => array_values($groupedPrograms),
'total' => $total,
'page' => $page,
'limit' => $limit
]
];
// 根据请求方式返回不同的输出
if ($this->request->isPost()) {
return json([
'code' => 1,
'msg' => '获取成功',
'data' => $data
]);
}
// GET请求渲染页面
return view('index', [
'categories' => $categories,
'cate' => $data['cate']
]);
}
// 程序列表页
public function list()
{
// 获取分类ID
$cateId = Request::param('cate/d', 0);
// 构建查询条件
$where = [
['a.delete_time', '=', null],
['a.status', '=', 1]
];
if ($cateId > 0) {
$where[] = ['a.cate', '=', $cateId];
}
// 获取程序列表
$programs = Resources::alias('a')
->join('resources_category c', 'a.cate = c.id')
->where($where)
->field([
'a.*',
'IF(a.icon IS NULL OR a.icon = "", c.icon, a.icon) as icon'
])
->order('a.id DESC')
->paginate([
'list_rows' => 10,
'query' => Request::instance()->param()
]);
// 获取分类信息
$category = null;
if ($cateId > 0) {
$category = ResourcesCategory::where('id', $cateId)
->where('delete_time', null)
->where('status', 1)
->find();
}
// 获取所有分类
$categories = ResourcesCategory::where('delete_time', null)
->where('status', 1)
->select()
->toArray();
// 如果是POST请求,返回JSON数据
if (Request::isPost()) {
return json([
'code' => 1,
'msg' => '获取成功',
'data' => [
'programs' => $programs->items(),
'total' => $programs->total(),
'current_page' => $programs->currentPage(),
'per_page' => $programs->listRows(),
'category' => $category
]
]);
}
// GET请求返回渲染的视图
View::assign([
'programs' => $programs,
'category' => $category,
'categories' => $categories
]);
return View::fetch('list');
}
// 程序详情页
public function detail($id)
{
// 获取资源详情
$program = Resources::where('id', $id)
->where('delete_time', null)
->where('status', 1)
->find();
if (!$program) {
if ($this->request->isPost()) {
return json(['code' => 0, 'msg' => '资源不存在']);
}
$this->error('资源不存在');
}
// 获取分类名称
$cateName = ResourcesCategory::where('id', $program['cate'])
->value('name');
// 获取上传者信息
$uploaderInfo = Users::where('name', $program['uploader'])
->field(['name', 'avatar'])
->find();
if ($uploaderInfo) {
$uploaderInfo = $uploaderInfo->toArray();
// 如果没有头像,使用默认头像
if (empty($uploaderInfo['avatar'])) {
$uploaderInfo['avatar'] = '/static/images/avatar.png';
}
} else {
$uploaderInfo = [
'name' => $program['uploader'],
'avatar' => '/static/images/avatar.png'
];
}
// 添加上传者的资源数和文章数统计
$uploaderInfo['resource_count'] = Resources::where('uploader', $program['uploader'])
->where('delete_time', null)
->where('status', 1)
->count();
$uploaderInfo['article_count'] = Articles::where('author', $program['uploader'])
->where('delete_time', null)
->where('status', 2)
->count();
// 处理资源size
if (empty($program['size'])) {
$attachment = Attachments::where('src', $program['icon'])
->field('size')
->find();
if ($attachment) {
$program['size'] = $attachment['size'];
}
}
// 转换文件大小为合适的单位
if (!empty($program['size']) && is_numeric($program['size'])) {
$size = $program['size'];
if ($size >= 1073741824) { // 1GB = 1024MB = 1024*1024KB = 1024*1024*1024B
$program['size'] = round($size / 1073741824, 2) . 'GB';
} elseif ($size >= 1048576) { // 1MB = 1024KB = 1024*1024B
$program['size'] = round($size / 1048576, 2) . 'MB';
} else {
$program['size'] = round($size / 1024, 2) . 'KB';
}
}
// 获取上一个和下一个程序
$prevProgram = Resources::where('id', '<', $id)
->where('delete_time', null)
->where('status', 1)
->where('cate', $program['cate'])
->field(['id', 'title'])
->order('id DESC')
->find();
$nextProgram = Resources::where('id', '>', $id)
->where('delete_time', null)
->where('status', 1)
->where('cate', $program['cate'])
->field(['id', 'title'])
->order('id ASC')
->find();
// 获取相关程序(同分类的其他程序)
$relatedPrograms = Resources::alias('p')
->join('yz_resources_category c', 'p.cate = c.id')
->where('p.cate', $program['cate'])
->where('p.id', '<>', $id)
->where('p.delete_time', null)
->where('p.status', 1)
->field([
'p.id',
'p.title',
'COALESCE(p.icon, c.icon) as icon'
])
->order('p.id DESC')
->limit(3)
->select()
->toArray();
// 准备返回数据
$data = [
'program' => $program,
'uploaderInfo' => $uploaderInfo,
'cateName' => $cateName,
'prevProgram' => $prevProgram,
'nextProgram' => $nextProgram,
'relatedPrograms' => $relatedPrograms
];
// 根据请求方式返回不同的输出
if ($this->request->isPost()) {
return json([
'code' => 1,
'msg' => '获取成功',
'data' => $data
]);
}
// GET请求渲染页面
return view('', $data);
}
// 程序下载
public function download()
{
$id = Request::param('id/d', 0);
$program = Resources::where('id', $id)
->where('delete_time', null)
->where('status', 1)
->find();
if (!$program) {
return json(['code' => 0, 'msg' => '程序不存在或已被删除']);
}
// 更新下载次数
Resources::where('id', $id)->inc('downloads')->update();
return json([
'code' => 1,
'msg' => '获取成功',
'data' => [
'fileurl' => $program['fileurl']
]
]);
}
// 获取访问统计
public function viewStats()
{
$id = Request::param('id/d', 0);
// 获取总访问量
$totalViews = Resources::where('id', $id)
->value('views');
return json([
'code' => 1,
'data' => [
'total' => $totalViews
]
]);
}
/**
* 更新程序访问次数
*/
public function updateViews()
{
if (!Request::isPost()) {
return json(['code' => 0, 'msg' => '非法请求']);
}
$id = Request::post('id');
if (!$id) {
return json(['code' => 0, 'msg' => '参数错误']);
}
try {
// 更新访问次数
$program = Resources::where('id', $id)->find();
if (!$program) {
return json(['code' => 0, 'msg' => '程序不存在']);
}
// 更新访问次数
Resources::where('id', $id)->inc('views')->update();
// 获取更新后的访问次数
$newViews = Resources::where('id', $id)->value('views');
return json(['code' => 1, 'msg' => '更新成功', 'data' => ['views' => $newViews]]);
} catch (\Exception $e) {
return json(['code' => 0, 'msg' => '更新失败:' . $e->getMessage()]);
}
}
}
@@ -0,0 +1,309 @@
<?php
/**
* 商业使用授权协议
*
* Copyright (c) 2025 [云泽网]. 保留所有权利.
*
* 本软件仅供评估使用。任何商业用途必须获得书面授权许可。
* 未经授权商业使用本软件属于侵权行为,将承担法律责任。
*
* 授权购买请联系: 357099073@qq.com
* 官方网站: https://www.yunzer.cn
*
* 评估用户须知:
* 1. 禁止移除版权声明
* 2. 禁止用于生产环境
* 3. 禁止转售或分发
*/
/**
* 资源下载控制器
*/
namespace app\index\controller;
use app\index\controller\BaseController;
use think\facade\Db;
use think\facade\View;
use think\facade\Request;
use app\index\model\Resources\Resources;
use app\index\model\Resources\ResourcesCategory;
use app\index\model\Attachments;
use app\index\model\Users;
use app\index\model\Articles\Articles;
class ResourcesController extends BaseController
{
//资源中心
public function index()
{
// 获取所有顶级分类
$parentCategories = ResourcesCategory::where('cid', 0)
->where('status', 1)
->order('sort', 'asc')
->select();
// 获取每个顶级分类下的子分类
$categories = [];
foreach ($parentCategories as $parent) {
$subCategories = ResourcesCategory::where('cid', $parent->id)
->where('status', 1)
->order('sort', 'asc')
->select();
// 获取每个子分类下的资源数量
foreach ($subCategories as &$subCategory) {
$subCategory['resource_count'] = Resources::where('cate', $subCategory->id)
->where('status', 1)
->count();
}
$categories[] = [
'parent' => $parent,
'subCategories' => $subCategories
];
}
// 将数据传递给视图
View::assign('categories', $categories);
return View::fetch();
}
// 资源列表页
public function list()
{
$cid = input('cid/d', 0);
$page = input('page/d', 1);
// 获取分类信息
$category = ResourcesCategory::where('id', $cid)
->where('status', 1)
->find();
if (!$category) {
$this->error('分类不存在');
}
// 获取该分类下的资源,带分页
$resources = Resources::where('cate', $cid)
->where('status', 1)
->order('sort', 'asc')
->paginate([
'list_rows' => 20,
'page' => $page,
'query' => Request::instance()->param()
]);
// 将数据传递给视图
View::assign('category', $category);
View::assign('data', $resources);
View::assign('page', $resources->render()); // 新增这一行
return View::fetch('list');
}
// 资源详情页
public function detail()
{
$id = Request::param('id/d', 0);
$resources = Resources::where('id', $id)->find();
if (!$resources) {
return json(['code' => 0, 'msg' => '资源不存在或已被删除']);
}
// 如果size没有,从附件表中获取
if (empty($resources['size']) && !empty($resources['fileurl'])) {
$attachment = Attachments::where('src', $resources['fileurl'])
->find();
if ($attachment && !empty($attachment['size'])) {
$size = $attachment['size'];
// 转换文件大小为合适的单位
if ($size >= 1073741824) { // 1GB = 1024MB = 1024*1024KB = 1024*1024*1024B
$resources['size'] = round($size / 1073741824, 2) . 'GB';
} elseif ($size >= 1048576) { // 1MB = 1024KB = 1024*1024B
$resources['size'] = round($size / 1048576, 2) . 'MB';
} else {
$resources['size'] = round($size / 1024, 2) . 'KB';
}
}
}
// 获取分类名称
$cateName = ResourcesCategory::where('id', $resources['cate'])
->value('name');
// 获取作者信息
$authorInfo = Users::where('name', $resources['uploader'])->find();
// var_dump($authorInfo);
if ($authorInfo) {
// 统计作者的文章数
$resourcesCount = Articles::where('author', $resources['uploader'])->count();
// 统计作者的资源数
$resourceCount = Resources::where('uploader', $resources['uploader'])->count();
$authorData = [
'avatar' => $authorInfo['avatar'] ?: '/static/images/avatar.png',
'name' => $authorInfo['name'],
'resource_count' => $resourceCount,
'article_count' => $resourcesCount
];
} else {
$authorData = [
'avatar' => '/static/images/avatar.png',
'name' => $resources['author'],
'resource_count' => 0,
'article_count' => 0
];
}
// 获取上一个和下一个资源
$prevResources = Resources::where('id', '<', $id)
->where('delete_time', null)
->where('status', 1)
->where('cate', $resources['cate'])
->field(['id', 'title'])
->order('id DESC')
->find();
$nextResources = Resources::where('id', '>', $id)
->where('delete_time', null)
->where('status', 1)
->where('cate', $resources['cate'])
->field(['id', 'title'])
->order('id ASC')
->find();
// 获取相关资源(同分类的其他资源)
$relatedResourcess = Db::table('yz_resources')
->alias('g')
->join('yz_resources_category c', 'g.cate = c.id')
->where('g.cate', $resources['cate'])
->where('g.id', '<>', $id)
->where('g.delete_time', null)
->where('g.status', 1)
->field([
'g.id',
'g.title',
'IF(g.icon IS NULL OR g.icon = "", c.icon, g.icon) as icon'
])
->order('g.id DESC')
->limit(5)
->select()
->toArray();
// 如果是 AJAX 请求,返回 JSON 数据
if (Request::isAjax()) {
return json([
'code' => 1,
'msg' => '获取成功',
'data' => [
'resources' => $resources,
'cateName' => $cateName,
'prevResources' => $prevResources,
'nextResources' => $nextResources,
'relatedResourcess' => $relatedResourcess
]
]);
}
// 非 AJAX 请求返回视图
View::assign([
'resources' => $resources,
'cateName' => $cateName,
'authorInfo' => $authorData,
'prevResources' => $prevResources,
'nextResources' => $nextResources,
'relatedResourcess' => $relatedResourcess
]);
return View::fetch('detail');
}
// 资源下载
public function downloadurl()
{
if (!Request::isAjax()) {
return json(['code' => 0, 'msg' => '非法请求']);
}
$id = Request::param('id/d', 0);
// 获取资源信息
$resources = Resources::where('id', $id)
->where('delete_time', null)
->find();
if (!$resources) {
return json(['code' => 0, 'msg' => '资源不存在']);
}
// 更新下载次数
$result = Resources::where('id', $id)
->where('delete_time', null)
->inc('downloads', 1)
->update();
if ($result) {
return json([
'code' => 1,
'msg' => '下载成功',
'data' => [
'url' => $resources['url']
]
]);
} else {
return json(['code' => 0, 'msg' => '下载失败']);
}
}
// 获取访问统计
public function viewStats()
{
$id = Request::param('id/d', 0);
// 获取总访问量
$totalViews = Resources::where('id', $id)
->value('views');
return json([
'code' => 1,
'data' => [
'total' => $totalViews
]
]);
}
/**
* 更新资源访问次数
*/
public function updateViews()
{
if (!Request::isPost()) {
return json(['code' => 0, 'msg' => '非法请求']);
}
$id = Request::post('id');
if (!$id) {
return json(['code' => 0, 'msg' => '参数错误']);
}
try {
// 更新访问次数
$resources = Resources::where('id', $id)->find();
if (!$resources) {
return json(['code' => 0, 'msg' => '资源不存在']);
}
// 更新访问次数
Resources::where('id', $id)->inc('views')->update();
// 获取更新后的访问次数
$newViews = Resources::where('id', $id)->value('views');
return json(['code' => 1, 'msg' => '更新成功', 'data' => ['views' => $newViews]]);
} catch (\Exception $e) {
return json(['code' => 0, 'msg' => '更新失败:' . $e->getMessage()]);
}
}
}
+128
View File
@@ -0,0 +1,128 @@
<?php
/**
* 商业使用授权协议
*
* Copyright (c) 2025 [云泽网]. 保留所有权利.
*
* 本软件仅供评估使用。任何商业用途必须获得书面授权许可。
* 未经授权商业使用本软件属于侵权行为,将承担法律责任。
*
* 授权购买请联系: 357099073@qq.com
* 官方网站: https://www.yunzer.cn
*
* 评估用户须知:
* 1. 禁止移除版权声明
* 2. 禁止用于生产环境
* 3. 禁止转售或分发
*/
namespace app\index\controller;
use think\Db;
use app\index\controller\BaseController;
use app\index\model\Articles\Articles;
use app\index\model\Articles\ArticlesCategory;
use app\index\model\Resources\Resources;
use app\index\model\Resources\ResourcesCategory;
class SearchController extends BaseController
{
public function index()
{
$keyword = input('keyword', '');
$type = input('type', 'articles'); // 搜索类型:articles-文章,resources-资源
$page = input('page', 1);
$limit = input('limit', 10);
if (empty($keyword)) {
$this->error('请输入搜索关键词');
}
// 根据类型选择对应的表和分类表
if ($type == 'articles') {
$model = new Articles();
$categoryModel = new ArticlesCategory();
$detailUrl = '/index/articles/detail';
$categoryUrl = '/index/articles/category';
$status = 2; // 文章状态为2
$fields = 'id, title, cate, image, author, FROM_UNIXTIME(create_time, "%Y-%m-%d") as publishdate';
} else if ($type == 'resources') {
$model = new Resources();
$categoryModel = new ResourcesCategory();
$detailUrl = '/index/resources/detail';
$categoryUrl = '/index/resources/category';
$status = 1; // 资源状态为1
$fields = 'id, title, cate, icon, uploader, FROM_UNIXTIME(create_time, "%Y-%m-%d") as publishdate';
} else {
$this->error('无效的搜索类型');
}
// 搜索内容
$items = $model->where('title', 'like', "%{$keyword}%")
->where('status', $status)
->field($fields)
->order('create_time desc')
->page($page, $limit)
->select();
// 获取总数
$count = $model->where('title', 'like', "%{$keyword}%")
->where('status', $status)
->count();
// 获取分类名称和图片
foreach ($items as &$item) {
if ($type == 'articles') {
$category = $categoryModel->where('id', $item['cate'])
->field('id, name, image')
->find();
$item['category'] = $category ?: ['id' => 0, 'name' => '未分类', 'image' => ''];
$item['cate'] = $item['category']['name']; // 使用分类名称替换分类ID
// 如果文章的图片为空,使用分类的图片
if (empty($item['image'])) {
$item['image'] = $item['category']['image'];
}
if (empty($item['image'])) {
$item['image'] = '/static/images/default.jpg';
}
} else {
$category = $categoryModel->where('id', $item['cate'])
->field('id, name, icon, cid')
->find();
$item['category'] = $category ?: ['id' => 0, 'name' => '未分类', 'icon' => '', 'cid' => 0];
$item['cate'] = $item['category']['name']; // 使用分类名称替换分类ID
// 如果资源的图片为空,使用分类的图片
if (empty($item['icon'])) {
$item['icon'] = $item['category']['icon'];
}
if (empty($item['icon'])) {
$item['icon'] = '/static/images/default.jpg';
}
// 根据分类cid判断资源类型
if ($item['category']['cid'] == 8) {
$item['detail_url'] = url('game/detail', ['id' => $item['id']]);
} else {
$item['detail_url'] = url('program/detail', ['id' => $item['id']]);
}
}
}
// 准备视图数据
$viewData = [
'keyword' => $keyword,
'type' => $type,
'items' => $items,
'detailUrl' => $detailUrl,
'count' => $count,
'page' => $page,
'limit' => $limit
];
return view('index', $viewData);
}
}
@@ -0,0 +1,70 @@
<?php
/**
* 商业使用授权协议
*
* Copyright (c) 2025 [云泽网]. 保留所有权利.
*
* 本软件仅供评估使用。任何商业用途必须获得书面授权许可。
* 未经授权商业使用本软件属于侵权行为,将承担法律责任。
*
* 授权购买请联系: 357099073@qq.com
* 官方网站: https://www.yunzer.cn
*
* 评估用户须知:
* 1. 禁止移除版权声明
* 2. 禁止用于生产环境
* 3. 禁止转售或分发
*/
namespace app\index\controller;
use think\facade\Request;
use think\facade\Filesystem;
use think\Response;
use app\index\model\Resources\Resources;
class StorageController extends BaseController
{
/**
* 处理文件下载
* @return \think\Response
*/
public function download()
{
// 获取请求的文件路径
$path = Request::pathinfo();
// 移除 'storage/' 前缀
$path = str_replace('storage/', '', $path);
// 构建完整的文件路径
$filePath = public_path() . 'storage/' . $path;
// 检查文件是否存在
if (!file_exists($filePath)) {
return Response::create('文件不存在', 'html', 404);
}
// 获取文件信息
$fileInfo = pathinfo($filePath);
$fileName = $fileInfo['basename'];
$fileSize = filesize($filePath);
$fileType = mime_content_type($filePath);
// 设置响应头
$headers = [
'Content-Type' => $fileType,
'Content-Disposition' => 'attachment; filename="' . $fileName . '"',
'Content-Length' => $fileSize,
'Cache-Control' => 'no-cache, must-revalidate',
'Pragma' => 'no-cache',
'Expires' => '0'
];
// 读取文件内容
$content = file_get_contents($filePath);
// 返回文件下载响应
return Response::create($content, 'file', 200, $headers);
}
}
+754
View File
@@ -0,0 +1,754 @@
<?php
/**
* 商业使用授权协议
*
* Copyright (c) 2025 [云泽网]. 保留所有权利.
*
* 本软件仅供评估使用。任何商业用途必须获得书面授权许可。
* 未经授权商业使用本软件属于侵权行为,将承担法律责任。
*
* 授权购买请联系: 357099073@qq.com
* 官方网站: https://www.yunzer.cn
*
* 评估用户须知:
* 1. 禁止移除版权声明
* 2. 禁止用于生产环境
* 3. 禁止转售或分发
*/
namespace app\index\controller;
use think\Controller;
use app\index\model\Users;
use think\facade\Redirect;
use think\facade\View;
use \think\facade\Log;
use \think\facade\Cache;
use PHPMailer\PHPMailer\PHPMailer;
use think\Response;
use app\index\model\UserMessage;
use app\index\model\SystemNotice;
use Endroid\QrCode\QrCode;
use Endroid\QrCode\Writer\PngWriter;
class UserController extends BaseController
{
/**
* 用户登录
*
* @return \think\Response
*/
public function login()
{
if ($this->request->isPost()) {
$data = $this->request->post();
try {
// 验证数据
$validate = validate([
'account' => 'require|email',
'password' => 'require'
]);
if (!$validate->check($data)) {
return json(['code' => 1, 'msg' => $validate->getError()]);
}
// 查询用户
$user = Users::where('account', $data['account'])->find();
if (!$user) {
return json(['code' => 1, 'msg' => '用户不存在']);
}
// 验证密码
if ($user->password !== md5($data['password'])) {
return json(['code' => 1, 'msg' => '密码错误']);
}
// 登录成功,设置session
session('user_id', $user->id);
session('user_name', $user->name);
session('user_avatar', $user->avatar ?? '/static/images/avatar.png');
// 设置cookie,有效期7天
$expire = 7 * 24 * 3600;
cookie('user_id', $user->id, ['expire' => $expire]);
cookie('user_account', $user->account, ['expire' => $expire]);
cookie('user_name', $user->name, ['expire' => $expire]);
cookie('user_avatar', $user->avatar ?? '/static/images/avatar.png', ['expire' => $expire]);
// 记录登录日志
Log::record('用户登录成功:' . $user->account, 'info');
return json(['code' => 0, 'msg' => '登录成功']);
} catch (\Exception $e) {
Log::record('登录失败:' . $e->getMessage(), 'error');
return json(['code' => 1, 'msg' => '登录失败:' . $e->getMessage()]);
}
}
return view('login');
}
/**
* 用户注册
*
* @return \think\Response
*/
public function register()
{
if ($this->request->isPost()) {
$data = $this->request->post();
try {
// 验证数据
$validate = validate([
'account' => 'require|email|unique:users',
'code' => 'require|number|length:6',
'password' => 'require|min:6|max:20',
'repassword' => 'require|confirm:password'
], [
'account.require' => '账户不能为空',
'account.email' => '邮箱格式不正确',
'account.unique' => '该邮箱已注册',
'code.require' => '验证码不能为空',
'code.number' => '验证码必须为数字',
'code.length' => '验证码长度必须为6位',
'password.require' => '密码不能为空',
'password.min' => '密码长度不能小于6个字符',
'password.max' => '密码长度不能超过20个字符',
'repassword.require' => '确认密码不能为空',
'repassword.confirm' => '两次输入的密码不一致'
]);
if (!$validate->check($data)) {
return json(['code' => 1, 'msg' => $validate->getError()]);
}
// 验证邮箱验证码
$emailCode = cache('email_code_' . $data['account']);
if (!$emailCode || $emailCode != $data['code']) {
return json(['code' => 1, 'msg' => '验证码错误或已过期']);
}
// 创建用户
$user = new Users;
$user->account = $data['account'];
$user->password = md5($data['password']);
$user->name = $this->generateRandomName();
$user->create_time = time();
$user->save();
// 清除验证码缓存
cache('email_code_' . $data['account'], null);
return json(['code' => 0, 'msg' => '注册成功']);
} catch (\Exception $e) {
return json(['code' => 1, 'msg' => '注册失败:' . $e->getMessage()]);
}
}
return view('register');
}
/**
* 退出登录
*
* @return \think\Response
*/
public function logout()
{
try {
Log::record('用户退出登录', 'info');
// 清除所有会话和缓存数据
session(null);
Cache::tag('user_cache')->clear();
// 清除所有cookie
$cookies = [
'user_id',
'user_account',
'user_name',
'user_avatar',
'expire_time',
'is_auto_login',
'auto_login_attempted',
'PHPSESSID'
];
foreach ($cookies as $cookie) {
cookie($cookie, null, ['expire' => -1]);
}
return json(['code' => 0, 'msg' => '退出成功', 'data' => ['clear_storage' => true]]);
} catch (\Exception $e) {
Log::record('退出登录失败:' . $e->getMessage(), 'error');
return json(['code' => 1, 'msg' => '退出失败:' . $e->getMessage()]);
}
}
// 生成随机用户名
private function generateRandomName()
{
return '云朵_' . mt_rand(100000, 999999);
}
// 发送短信验证码
public function sendSmsCode()
{
if ($this->request->isPost()) {
$phone = $this->request->post('phone');
// 验证手机号
$validate = validate([
'phone' => 'require|mobile|unique:user'
], [
'phone.require' => '手机号不能为空',
'phone.mobile' => '手机号格式不正确',
'phone.unique' => '该手机号已注册'
]);
if (!$validate->check(['phone' => $phone])) {
return json(['code' => 0, 'msg' => $validate->getError()]);
}
// 生成6位随机验证码
$code = mt_rand(100000, 999999);
// 这里应该调用短信服务商API发送验证码
// 示例代码,实际使用时需要替换为真实的短信发送逻辑
try {
// TODO: 调用短信服务商API发送验证码
// $result = sendSms($phone, $code);
// 将验证码保存到缓存,有效期5分钟
cache('sms_code_' . $phone, $code, 300);
return json(['code' => 1, 'msg' => '验证码发送成功']);
} catch (\Exception $e) {
return json(['code' => 0, 'msg' => '验证码发送失败:' . $e->getMessage()]);
}
}
return json(['code' => 0, 'msg' => '非法请求']);
}
// 微信授权回调
// public function wechatCallback()
// {
// $code = $this->request->get('code');
// if (!$code) {
// return json(['code' => 0, 'msg' => '微信授权失败']);
// }
// try {
// // 这里应该调用微信API获取用户信息
// // 示例代码,实际使用时需要替换为真实的微信API调用逻辑
// // $wechatUser = getWechatUserInfo($code);
// // 模拟获取到的微信用户信息
// $wechatUser = [
// 'openid' => 'test_openid_' . time(),
// 'nickname' => '微信用户',
// 'avatar' => ''
// ];
// // 检查用户是否已注册
// $user = Users::where('openid', $wechatUser['openid'])->find();
// if ($user) {
// // 已注册,直接登录
// session('user_id', $user->id);
// return json(['code' => 1, 'msg' => '登录成功']);
// }
// // 未注册,返回注册所需信息
// return json([
// 'code' => 2,
// 'msg' => '需要注册',
// 'data' => $wechatUser
// ]);
// } catch (\Exception $e) {
// return json(['code' => 0, 'msg' => '微信授权失败:' . $e->getMessage()]);
// }
// }
// 发送邮箱验证码
public function sendEmailCode()
{
// 设置响应头
header('Content-Type: application/json; charset=utf-8');
if (!$this->request->isPost()) {
return json(['code' => 1, 'msg' => '请求方法无效']);
}
$email = $this->request->post('account');
if (empty($email)) {
return json(['code' => 1, 'msg' => '邮箱不能为空']);
}
// 验证邮箱格式
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
return json(['code' => 1, 'msg' => '邮箱格式不正确']);
}
// 检查邮箱是否已注册
$exists = Users::where('account', $email)->find();
if ($exists) {
return json(['code' => 1, 'msg' => '该邮箱已注册']);
}
// 生成6位随机验证码
$code = mt_rand(100000, 999999);
// 发送验证码邮件
$result = parent::sendEmail($email, "您的注册验证码是:{$code},有效期为5分钟。", '注册验证码');
if ($result === '发送成功') { // 修改这里的判断条件
// 将验证码存入缓存,有效期5分钟
cache('email_code_' . $email, $code, 300);
return json(['code' => 0, 'msg' => '验证码已发送']);
} else {
return json(['code' => 1, 'msg' => '发送失败:' . $result]);
}
}
//个人中心
public function profile()
{
// 检查用户是否登录
if (!cookie('user_account')) {
return redirect('/index/user/login');
}
// 获取用户信息
$user = Users::where('account', cookie('user_account'))->find();
// var_dump($user);
if (!$user) {
return redirect('/index/user/login');
}
View::assign('user', $user);
return $this->fetch();
}
//个人资料
public function saveBasic()
{
// 检查用户是否登录
if (!cookie('user_account')) {
return json(['code' => 1, 'msg' => '请先登录']);
}
// 获取用户信息
$user = Users::where('account', cookie('user_account'))->find();
if (!$user) {
return json(['code' => 1, 'msg' => '用户不存在']);
}
// 获取表单数据
$data = $this->request->post();
// 验证用户名
if (empty($data['name'])) {
return json(['code' => 1, 'msg' => '用户名不能为空']);
}
// 验证手机号格式
if (!empty($data['phone']) && !preg_match('/^1[3-9]\d{9}$/', $data['phone'])) {
return json(['code' => 1, 'msg' => '请检查手机号']);
}
// 检查用户名是否已被使用(排除当前用户)
$existingUser = Users::where('name', $data['name'])
->where('uid', '<>', $user->uid) // 排除当前用户
->find();
if ($existingUser) {
return json(['code' => 1, 'msg' => '该用户名已被使用']);
}
// 更新用户信息
$user->name = $data['name'];
$user->phone = $data['phone'] ?? '';
$user->sex = $data['sex'] ?? 0;
$user->qq = $data['qq'] ?? '';
$user->wechat = $data['wechat'] ?? '';
$user->update_time = time();
if ($user->save()) {
return json(['code' => 0, 'msg' => '保存成功']);
} else {
return json(['code' => 1, 'msg' => '保存失败']);
}
}
//更新头像
public function update_avatar()
{
// 检查用户是否登录
if (!cookie('user_account')) {
return json(['code' => 1, 'msg' => '请先登录']);
}
// 获取用户信息
$user = Users::where('account', cookie('user_account'))->find();
if (!$user) {
return json(['code' => 1, 'msg' => '用户不存在']);
}
// 获取上传的文件
$file = $this->request->file('avatar');
if (!$file) {
return json(['code' => 1, 'msg' => '请选择要上传的头像']);
}
try {
// 验证文件大小和类型
if ($file->getSize() > 2097152) { // 2MB
return json(['code' => 1, 'msg' => '图片大小不能超过2MB']);
}
$ext = strtolower($file->getOriginalExtension());
if (!in_array($ext, ['jpg', 'jpeg', 'png', 'gif', 'webp'])) {
return json(['code' => 1, 'msg' => '只支持jpg、jpeg、png、gif、webp格式的图片']);
}
// 移动到指定目录
$savename = \think\facade\Filesystem::disk('public')->putFile('avatar', $file);
if (!$savename) {
return json(['code' => 1, 'msg' => '图片上传失败']);
}
// 获取文件URL
$avatarUrl = '/storage/' . $savename;
// 更新用户头像
$user->avatar = $avatarUrl;
$user->update_time = time();
if ($user->save()) {
return json(['code' => 0, 'msg' => '头像更新成功', 'data' => ['url' => $avatarUrl]]);
} else {
return json(['code' => 1, 'msg' => '头像更新失败']);
}
} catch (\Exception $e) {
return json(['code' => 1, 'msg' => '系统错误:' . $e->getMessage()]);
}
}
/**
* 获取系统通知列表
*/
public function getNotifications()
{
// 检查用户是否登录
if (!cookie('user_account')) {
return json(['code' => 1, 'msg' => '请先登录']);
}
$type = $this->request->get('type', 'all'); // 获取通知类型:all, unread, read
$userId = cookie('user_id');
try {
// 构建查询条件
$where = [
['status', '=', 1] // 只获取启用的通知
];
// 查询系统通知
$notices = SystemNotice::where($where)
->order('is_top', 'desc') // 置顶的排在前面
->order('create_time', 'desc')
->select();
// 格式化数据
$data = [];
foreach ($notices as $notice) {
// 检查用户是否已读该通知
$isRead = SystemNotice::where([
['user_id', '=', $userId],
['notice_id', '=', $notice->id],
['is_read', '=', 1]
])->find();
// 根据type过滤
if ($type == 'unread' && $isRead)
continue;
if ($type == 'read' && !$isRead)
continue;
$data[] = [
'id' => $notice->id,
'title' => $notice->title,
'content' => $notice->content,
'type' => $notice->type,
'is_top' => $notice->is_top,
'is_read' => $isRead ? 1 : 0,
'create_time' => date('Y-m-d H:i:s', $notice->create_time)
];
}
return json(['code' => 0, 'msg' => '获取成功', 'data' => $data]);
} catch (\Exception $e) {
return json(['code' => 1, 'msg' => '获取失败:' . $e->getMessage()]);
}
}
/**
* 查看通知详情
*/
public function readNotification()
{
// 检查用户是否登录
if (!cookie('user_account')) {
return json(['code' => 1, 'msg' => '请先登录']);
}
$data = $this->request->post();
$noticeId = $data['id'] ?? 0;
$userId = cookie('user_id');
try {
// 查询通知
$notice = SystemNotice::where('id', $noticeId)
->where('status', 1)
->find();
if (!$notice) {
return json(['code' => 1, 'msg' => '通知不存在']);
}
// 记录用户已读状态
$message = SystemNotice::where([
['user_id', '=', $userId],
['notice_id', '=', $noticeId]
])->find();
if (!$message) {
// 创建新的已读记录
$message = new SystemNotice;
$message->user_id = $userId;
$message->notice_id = $noticeId;
$message->is_read = 1;
$message->read_time = time();
$message->save();
} elseif (!$message->is_read) {
// 更新已读状态
$message->is_read = 1;
$message->read_time = time();
$message->save();
}
return json(['code' => 0, 'msg' => '操作成功']);
} catch (\Exception $e) {
return json(['code' => 1, 'msg' => '操作失败:' . $e->getMessage()]);
}
}
/**
* 通知详情页面
*/
public function notificationDetail()
{
// 检查用户是否登录
if (!cookie('user_account')) {
return redirect('/index/user/login');
}
$noticeId = $this->request->get('id');
$userId = cookie('user_id');
try {
// 查询通知
$notice = SystemNotice::where('id', $noticeId)
->where('status', 1)
->find();
if (!$notice) {
return $this->error('通知不存在');
}
// 记录用户已读状态
$message = SystemNotice::where([
['user_id', '=', $userId],
['notice_id', '=', $noticeId]
])->find();
if (!$message) {
// 创建新的已读记录
$message = new SystemNotice;
$message->user_id = $userId;
$message->notice_id = $noticeId;
$message->is_read = 1;
$message->read_time = time();
$message->save();
} elseif (!$message->is_read) {
// 更新已读状态
$message->is_read = 1;
$message->read_time = time();
$message->save();
}
// 增加查看次数
$notice->view_count = $notice->view_count + 1;
$notice->save();
View::assign('notice', $notice);
return $this->fetch('notification_detail');
} catch (\Exception $e) {
return $this->error('获取通知详情失败:' . $e->getMessage());
}
}
/**
* 获取系统通知列表
*/
public function getMessages()
{
// 检查用户是否登录
if (!cookie('user_account')) {
return json(['code' => 1, 'msg' => '请先登录']);
}
$type = $this->request->get('type', 'all'); // 获取通知类型:all, unread, read
$userId = cookie('user_id');
try {
// 构建查询条件
$where = [
['status', '=', 1] // 只获取启用的通知
];
// 查询系统通知
$notices = UserMessage::where($where)
->order('is_top', 'desc') // 置顶的排在前面
->order('create_time', 'desc')
->select();
// 格式化数据
$data = [];
foreach ($notices as $notice) {
// 检查用户是否已读该通知
$isRead = UserMessage::where([
['user_id', '=', $userId],
['notice_id', '=', $notice->id],
['is_read', '=', 1]
])->find();
// 根据type过滤
if ($type == 'unread' && $isRead)
continue;
if ($type == 'read' && !$isRead)
continue;
$data[] = [
'id' => $notice->id,
'title' => $notice->title,
'content' => $notice->content,
'type' => $notice->type,
'is_top' => $notice->is_top,
'is_read' => $isRead ? 1 : 0,
'create_time' => date('Y-m-d H:i:s', $notice->create_time)
];
}
return json(['code' => 0, 'msg' => '获取成功', 'data' => $data]);
} catch (\Exception $e) {
return json(['code' => 1, 'msg' => '获取失败:' . $e->getMessage()]);
}
}
//修改密码
public function updatePassword()
{
// 检查用户是否登录
if (!cookie('user_account')) {
return redirect('/index/user/login');
}
// 获取用户信息
$user = Users::where('account', cookie('user_account'))->find();
if (!$user) {
return redirect('/index/user/login');
}
// 如果是GET请求,显示修改密码页面
if ($this->request->isGet()) {
return $this->fetch();
}
// 如果是POST请求,处理密码修改
if ($this->request->isPost()) {
$data = $this->request->post();
// 验证旧密码
if ($user->password !== md5($data['old_password'])) {
return json(['code' => 1, 'msg' => '旧密码错误']);
}
// 验证新密码
if ($data['new_password'] !== $data['confirm_password']) {
return json(['code' => 1, 'msg' => '两次输入的密码不一致']);
}
// 更新密码
$user->password = md5($data['new_password']);
$user->update_time = time();
if ($user->save()) {
// 清除登录状态
cookie('user_id', null, ['expire' => -1]);
cookie('user_account', null, ['expire' => -1]);
cookie('user_name', null, ['expire' => -1]);
cookie('user_avatar', null, ['expire' => -1]);
return json(['code' => 0, 'msg' => '密码修改成功,请重新登录']);
} else {
return json(['code' => 1, 'msg' => '密码修改失败']);
}
}
}
//生成二维码绑定微信
public function qrcode()
{
// 检查用户是否登录
if (!cookie('user_account')) {
return json(['code'=> -1,'msg'=> '请先登录']);
}
// 获取当前用户信息
$user = Users::where('account', cookie('user_account'))->find();
if (!$user) {
return json(['code' => -1, 'msg' => '用户信息获取失败']);
}
// 假设这里生成一个唯一的绑定标识,例如使用用户ID和时间戳组合
$bindToken = md5($user->id . time());
// 生成实际的绑定 URL
$domain = $this->request->domain();
$bindUrl = "{$domain}/wechat_bind?token={$bindToken}";
// 将绑定标识存入缓存,设置有效期,例如30分钟
cache('wechat_bind_token_' . $user->id, $bindToken, 1800);
try {
// 创建二维码实例
$qrCode = QrCode::create($bindUrl);
$writer = new PngWriter();
// 生成二维码图片
$result = $writer->write($qrCode);
$qrCodeDataUri = $result->getDataUri();
return json(['code' => 0, 'msg' => '二维码生成成功', 'data' => ['qrcode_url' => $qrCodeDataUri]]);
} catch (\Exception $e) {
return json(['code' => -1, 'msg' => '二维码生成失败: ' . $e->getMessage()]);
}
}
}
+867
View File
@@ -0,0 +1,867 @@
<?php
/**
* 商业使用授权协议
*
* Copyright (c) 2025 [云泽网]. 保留所有权利.
*
* 本软件仅供评估使用。任何商业用途必须获得书面授权许可。
* 未经授权商业使用本软件属于侵权行为,将承担法律责任。
*
* 授权购买请联系: 357099073@qq.com
* 官方网站: https://www.yunzer.cn
*
* 评估用户须知:
* 1. 禁止移除版权声明
* 2. 禁止用于生产环境
* 3. 禁止转售或分发
*/
namespace app\index\controller;
use think\facade\Request;
use think\facade\Log;
use think\facade\Cache;
use GuzzleHttp\Client;
use app\index\model\Users;
use app\index\model\AdminConfig;
class WechatController extends BaseController
{
/**
* 测试接口是否正常工作
*/
public function test()
{
try {
// 设置响应头
header('Content-Type: application/json; charset=utf-8');
$data = [
'time' => date('Y-m-d H:i:s'),
'status' => 'ok',
'server' => [
'REQUEST_METHOD' => $_SERVER['REQUEST_METHOD'] ?? '',
'REQUEST_URI' => $_SERVER['REQUEST_URI'] ?? '',
'HTTP_HOST' => $_SERVER['HTTP_HOST'] ?? '',
'REMOTE_ADDR' => $_SERVER['REMOTE_ADDR'] ?? '',
]
];
// 记录详细日志
Log::info('接口测试 - 请求信息:' . json_encode($data, JSON_UNESCAPED_UNICODE));
return json($data, JSON_UNESCAPED_UNICODE);
} catch (\Exception $e) {
$error = [
'error' => $e->getMessage(),
'time' => date('Y-m-d H:i:s')
];
Log::error('接口测试错误:' . json_encode($error, JSON_UNESCAPED_UNICODE));
return json($error, JSON_UNESCAPED_UNICODE);
}
}
public function index()
{
try {
// 设置响应头
header('Content-Type: text/html; charset=utf-8');
// 记录原始请求信息
$requestInfo = [
'time' => date('Y-m-d H:i:s'),
'method' => Request::method(),
'url' => Request::url(true),
'ip' => Request::ip(),
'real_ip' => Request::server('HTTP_X_REAL_IP'),
'forwarded_ip' => Request::server('HTTP_X_FORWARDED_FOR'),
'params' => Request::param(),
'headers' => getallheaders(),
'server' => [
'REQUEST_METHOD' => $_SERVER['REQUEST_METHOD'] ?? '',
'REQUEST_URI' => $_SERVER['REQUEST_URI'] ?? '',
'HTTP_HOST' => $_SERVER['HTTP_HOST'] ?? '',
'REMOTE_ADDR' => $_SERVER['REMOTE_ADDR'] ?? '',
'QUERY_STRING' => $_SERVER['QUERY_STRING'] ?? '',
'HTTP_USER_AGENT' => $_SERVER['HTTP_USER_AGENT'] ?? '',
'HTTP_REFERER' => $_SERVER['HTTP_REFERER'] ?? '',
'CONTENT_TYPE' => $_SERVER['CONTENT_TYPE'] ?? '',
'CONTENT_LENGTH' => $_SERVER['CONTENT_LENGTH'] ?? '',
'SERVER_NAME' => $_SERVER['SERVER_NAME'] ?? '',
'SERVER_ADDR' => $_SERVER['SERVER_ADDR'] ?? '',
'SERVER_PORT' => $_SERVER['SERVER_PORT'] ?? '',
'REQUEST_SCHEME' => $_SERVER['REQUEST_SCHEME'] ?? '',
],
'env' => [
'app_debug' => config('app.debug'),
'app_env' => config('app.env'),
'domain' => config('app.domain'),
]
];
Log::info('微信接口访问请求信息:' . json_encode($requestInfo, JSON_UNESCAPED_UNICODE));
// 获取原始数据
$rawData = file_get_contents("php://input");
if (!empty($rawData)) {
$rawData = mb_convert_encoding($rawData, 'UTF-8', 'UTF-8,GBK,GB2312');
Log::info('微信接口原始数据:' . $rawData);
}
// 检查请求方法
if (Request::method() == 'GET') {
Log::info('微信接口:GET请求,进行签名验证');
// 首次验证服务器地址的有效性
$this->checkSignature();
} elseif (Request::method() == 'POST') {
Log::info('微信接口:POST请求,处理消息');
// 接收消息并回复
$response = $this->receiveMessage();
// 直接输出回复的XML字符串
echo $response;
exit;
} else {
Log::error('微信接口:不支持的请求方法 - ' . Request::method());
echo '';
exit;
}
} catch (\Exception $e) {
Log::error('微信接口错误:' . $e->getMessage());
Log::error('错误堆栈:' . $e->getTraceAsString());
// 返回空字符串,避免微信服务器重试
echo '';
exit;
}
}
// 验证签名
protected function checkSignature()
{
try {
$signature = Request::get('signature');
$timestamp = Request::get('timestamp');
$nonce = Request::get('nonce');
$echostr = Request::get('echostr');
$debugInfo = [
'signature' => $signature,
'timestamp' => $timestamp,
'nonce' => $nonce,
'echostr' => $echostr,
'url' => Request::url(true),
'time' => date('Y-m-d H:i:s'),
'ip' => Request::ip(),
'headers' => getallheaders()
];
Log::info('微信验证参数:' . json_encode($debugInfo, JSON_UNESCAPED_UNICODE));
if (empty($signature) || empty($timestamp) || empty($nonce)) {
Log::error('微信验证参数缺失', $debugInfo);
exit;
}
$token = AdminConfig::where('config_name', 'wechat_token')->value('config_value');
if (empty($token)) {
Log::error('微信token未配置');
exit;
}
Log::info('微信token' . $token);
$tmpArr = array($token, $timestamp, $nonce);
sort($tmpArr, SORT_STRING);
$tmpStr = implode($tmpArr);
$tmpStr = sha1($tmpStr);
$verifyInfo = [
'tmpStr' => $tmpStr,
'signature' => $signature,
'token' => $token,
'timestamp' => $timestamp,
'nonce' => $nonce
];
Log::info('签名验证:' . json_encode($verifyInfo, JSON_UNESCAPED_UNICODE));
if ($tmpStr == $signature) {
Log::info('微信签名验证成功');
echo $echostr;
exit;
} else {
Log::error('微信签名验证失败', $verifyInfo);
exit;
}
} catch (\Exception $e) {
Log::error('微信验证签名错误:' . $e->getMessage());
Log::error('错误堆栈:' . $e->getTraceAsString());
exit;
}
}
// 接收消息
protected function receiveMessage()
{
try {
$postStr = file_get_contents("php://input");
if (empty($postStr)) {
Log::error('微信消息:接收数据为空');
return '';
}
// 转换编码
$postStr = mb_convert_encoding($postStr, 'UTF-8', 'UTF-8,GBK,GB2312');
Log::info('微信消息原始数据:' . $postStr);
// 使用DOMDocument替代simplexml_load_string
$dom = new \DOMDocument();
$dom->loadXML($postStr, LIBXML_NOCDATA | LIBXML_NOBLANKS);
$postObj = simplexml_import_dom($dom);
if ($postObj === false) {
Log::error('微信消息:XML解析失败');
return '';
}
// 检查消息类型
$msgType = strtolower((string) $postObj->MsgType);
Log::info('微信消息类型:' . $msgType);
// 处理事件消息
if ($msgType == 'event') {
$event = strtolower((string) $postObj->Event);
Log::info('微信事件类型:' . $event);
// 处理扫码事件和关注事件
if ($event == 'scan' || $event == 'subscribe') {
try {
$scene_str = trim((string) $postObj->EventKey);
// 如果是关注事件,需要去掉前缀 'qrscene_'
if ($event == 'subscribe' && strpos($scene_str, 'qrscene_') === 0) {
$scene_str = substr($scene_str, 8);
}
$fromUsername = trim((string) $postObj->FromUserName);
$toUsername = trim((string) $postObj->ToUserName);
$ticket = (string) $postObj->Ticket;
Log::info('微信扫码/关注事件详情', [
'event' => $event,
'scene_str' => $scene_str,
'fromUsername' => $fromUsername,
'toUsername' => $toUsername,
'ticket' => $ticket,
'time' => date('Y-m-d H:i:s'),
'raw_data' => $postStr
]);
// 创建票据保存目录
$upload_dir = 'public/storage/uploads/ticket/';
if (!is_dir($upload_dir)) {
if (!mkdir($upload_dir, 0755, true)) {
Log::error('创建票据保存目录失败:' . $upload_dir);
return 'success';
}
Log::info('创建票据保存目录成功:' . $upload_dir);
}
// 保存扫码数据
$data = [
'openid' => $fromUsername,
'scene_str' => $scene_str,
'ticket' => $ticket,
'scan_time' => time(),
'event' => $event,
'raw_data' => $postStr
];
$content = json_encode($data, JSON_UNESCAPED_UNICODE);
$file = $upload_dir . $scene_str . "_" . $ticket . ".json";
if (file_put_contents($file, $content) === false) {
Log::error('保存扫码数据失败:' . $file);
return 'success';
}
Log::info('保存扫码数据成功:' . $file);
// 获取用户信息
try {
$accessToken = $this->getGZHAccessToken();
Log::info('获取access_token成功:' . $accessToken);
$url = "https://api.weixin.qq.com/cgi-bin/user/info?access_token={$accessToken}&openid={$fromUsername}&lang=zh_CN";
$client = new Client(['verify' => false]);
$response = $client->get($url);
$userInfo = json_decode($response->getBody(), true);
Log::info('获取用户信息结果:' . json_encode($userInfo, JSON_UNESCAPED_UNICODE));
if (isset($userInfo['openid'])) {
// 先检查是否存在该openid的用户
$user = Users::where('openid', $fromUsername)->find();
if ($user) {
// 已存在用户,直接登录
$user->login_count = $user->login_count + 1; // 增加登录次数
$user->update_time = time(); // 更新登录时间
// 只在头像为空或为默认头像时更新
if (empty($user->avatar) || $user->avatar === '/static/images/avatar.png') {
if (!empty($userInfo['headimgurl'])) {
$user->avatar = $userInfo['headimgurl'];
} elseif (empty($user->avatar)) {
$user->avatar = '/static/images/avatar.png';
}
}
if (!$user->save()) {
Log::error('更新用户登录信息失败:' . json_encode($user->getError(), JSON_UNESCAPED_UNICODE));
return 'success';
}
Log::info('用户登录成功:' . json_encode($user->toArray(), JSON_UNESCAPED_UNICODE));
} else {
// 不存在用户,创建新用户
$user = new Users;
$user->openid = $fromUsername;
// 生成默认账号
$defaultAccount = 'wx_' . substr(md5($fromUsername), 0, 8);
$user->account = $defaultAccount;
$user->name = $defaultAccount; // 将默认账号同时设置为用户名
// 设置头像,确保有值
$user->avatar = !empty($userInfo['headimgurl']) ? $userInfo['headimgurl'] : '/static/images/avatar.png';
// 生成随机密码
$user->password = md5(uniqid() . rand(1000, 9999));
$user->create_time = time(); // 设置创建时间
$user->login_count = 1; // 首次登录,设置登录次数为1
if (!$user->save()) {
Log::error('创建用户失败:' . json_encode($user->getError(), JSON_UNESCAPED_UNICODE));
return 'success';
}
Log::info('创建新用户成功:' . json_encode($user->toArray(), JSON_UNESCAPED_UNICODE));
}
// 更新票据文件
$data = [
'uid' => (int)$user->uid, // 确保uid是整数
'name' => $user->name,
'avatar' => $user->avatar ?: '/static/images/avatar.png', // 确保avatar不为空
'openid' => $user->openid,
'user_account' => $user->account,
'user_password' => $user->password,
'expire_time' => time() + (7 * 24 * 3600), // 7天过期
'is_auto_login' => true,
'login_status' => 'success'
];
if (file_put_contents($file, json_encode($data, JSON_UNESCAPED_UNICODE)) === false) {
Log::error('更新票据文件失败:' . $file);
} else {
Log::info('更新票据文件成功:' . json_encode($data, JSON_UNESCAPED_UNICODE));
}
// 设置cookie
$expire = 7 * 24 * 3600; // 7天过期
cookie('user_account', $user->account, ['expire' => $expire]);
cookie('user_avatar', $user->avatar ?: '/static/images/avatar.png', ['expire' => $expire]);
cookie('user_name', $user->name, ['expire' => $expire]);
cookie('open_id', $user->openid, ['expire' => $expire]);
// 发送登录成功消息
$messageContent = [
'content' => "您好!\n您已成功登录系统。\n登录时间:" . date('Y-m-d H:i:s')
];
$this->sendCustomMessage($fromUsername, 'text', $messageContent);
Log::info('用户登录成功:' . json_encode([
'uid' => (int)$user->uid,
'name' => $user->name,
'openid' => $user->openid,
'account' => $user->account,
'cookies' => [
'user_account' => $user->account,
'user_avatar' => $user->avatar,
'user_name' => $user->name,
'open_id' => $user->openid
]
], JSON_UNESCAPED_UNICODE));
} else {
Log::error('获取用户信息失败:' . json_encode($userInfo, JSON_UNESCAPED_UNICODE));
}
} catch (\Exception $e) {
Log::error('获取用户信息失败:' . $e->getMessage());
Log::error('错误堆栈:' . $e->getTraceAsString());
}
} catch (\Exception $e) {
Log::error('处理扫码事件失败:' . $e->getMessage());
Log::error('错误堆栈:' . $e->getTraceAsString());
}
}
}
return 'success';
} catch (\Exception $e) {
Log::error('处理微信消息错误:' . $e->getMessage());
Log::error('错误堆栈:' . $e->getTraceAsString());
return 'success';
}
}
// 发送小程序卡片消息的方法
private function sendMiniProgramCard($openid)
{
$accessToken = $this->getGZHAccessToken();
print_r($accessToken);
if (!$accessToken) {
// 处理获取access_token失败的情况
return;
}
$postData = json_encode([
'touser' => $openid, // 接收者(用户)的openid
'msgtype' => 'miniprogrampage',
'miniprogrampage' => [
'title' => '小程序标题',
'appid' => '小程序appid',
'pagepath' => '小程序路径',
'thumb_media_id' => '你的thumb_media_id'
]
], JSON_UNESCAPED_UNICODE);
$url = "https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token={$accessToken}";
$result = json_decode(file_get_contents($url, false, stream_context_create([
'http' => [
'method' => 'POST',
'header' => "Content-type: application/json\r\n",
'content' => $postData,
'timeout' => 60 // 超时时间(单位:s
]
])), true);
// 处理返回结果
if ($result['errcode'] == 0) {
// 发送成功
} else {
// 发送失败
}
}
// 获取公众号Access token
public function getGZHAccessToken()
{
// 从数据库获取配置
$appid = AdminConfig::where('config_name', 'wechat_appid')->value('config_value');
$secret = AdminConfig::where('config_name', 'wechat_appsecret')->value('config_value');
if (empty($appid) || empty($secret)) {
throw new \Exception('微信配置信息未设置');
}
// 构建请求URL
$url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={$appid}&secret={$secret}";
try {
// 使用 GuzzleHttp 发送请求,禁用SSL验证
$client = new Client([
'verify' => false
]);
$response = $client->get($url);
$data = json_decode($response->getBody(), true);
if (!isset($data['access_token'])) {
throw new \Exception("获取access_token失败: {$data['errmsg']}", $data['errcode'] ?? -1);
}
return $data['access_token'];
} catch (\Exception $e) {
Log::error('获取access_token失败:' . $e->getMessage());
throw $e;
}
}
/**
* 获取微信登录二维码
* @return \think\response\Json
*/
public function getLoginTicket()
{
try {
Log::info('开始获取微信登录二维码');
// 获取access_token
$access_token = $this->getGZHAccessToken();
Log::info('获取access_token成功:' . $access_token);
// 构建请求URL - 使用正确的接口生成临时二维码
$url = "https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token={$access_token}";
// 生成唯一场景值
$scene_str = md5(uniqid() . time());
Log::info('生成场景值:' . $scene_str);
// 准备请求数据 - 生成临时二维码,有效期5分钟
$postData = json_encode([
'expire_seconds' => 300, // 5分钟有效期
'action_name' => 'QR_STR_SCENE',
'action_info' => [
'scene' => [
'scene_str' => $scene_str
]
]
]);
// 发送请求获取ticket
$client = new Client(['verify' => false]);
$response = $client->post($url, [
'body' => $postData,
'headers' => [
'Content-Type' => 'application/json'
]
]);
$result = json_decode($response->getBody(), true);
Log::info('微信返回结果:' . json_encode($result, JSON_UNESCAPED_UNICODE));
if (isset($result['errcode']) && $result['errcode'] != 0) {
Log::error('获取二维码失败:' . $result['errmsg']);
return json(['code' => 1, 'msg' => '获取二维码失败:' . $result['errmsg']]);
}
// 使用ticket获取二维码图片URL
$ticket = urlencode($result['ticket']);
$qrcodeUrl = "https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket={$ticket}";
// 将场景值保存到缓存中,用于后续验证
Cache::set('wx_login_scene_' . $result['ticket'], [
'scene_str' => $scene_str,
'create_time' => time(),
'expire_time' => time() + 300
], 300);
Log::info('二维码生成成功', [
'ticket' => $result['ticket'],
'scene_str' => $scene_str,
'expire_time' => time() + 300
]);
return json([
'code' => 0,
'msg' => '获取二维码成功',
'data' => [
'ticket' => $result['ticket'],
'expire_seconds' => $result['expire_seconds'],
'url' => $qrcodeUrl,
'scene_str' => $scene_str
]
]);
} catch (\Exception $e) {
Log::error('获取微信登录二维码失败:' . $e->getMessage());
Log::error('错误堆栈:' . $e->getTraceAsString());
return json(['code' => 1, 'msg' => '获取二维码失败:' . $e->getMessage()]);
}
}
/**
* 重新生成二维码
* @return \think\response\Json
*/
public function reGenerateQrcode()
{
try {
$scene_str = Request::post('scene_str');
if (empty($scene_str)) {
return json(['code' => 1, 'msg' => '参数错误']);
}
// 清除旧的缓存
Cache::delete('wx_login_scene_' . $scene_str);
// 获取access_token
$access_token = $this->getGZHAccessToken();
// 构建请求URL
$url = "https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token={$access_token}";
// 生成新的场景值
$new_scene_str = md5(uniqid() . time());
// 准备请求数据
$postData = json_encode([
'expire_seconds' => 300,
'action_name' => 'QR_STR_SCENE',
'action_info' => [
'scene' => [
'scene_str' => $new_scene_str
]
]
]);
// 发送请求获取ticket
$client = new Client(['verify' => false]);
$response = $client->post($url, [
'body' => $postData,
'headers' => [
'Content-Type' => 'application/json'
]
]);
$result = json_decode($response->getBody(), true);
if (isset($result['errcode']) && $result['errcode'] != 0) {
return json(['code' => 1, 'msg' => '获取二维码失败:' . $result['errmsg']]);
}
// 使用ticket获取二维码图片URL
$ticket = urlencode($result['ticket']);
$qrcodeUrl = "https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket={$ticket}";
// 将新的场景值保存到缓存中
Cache::set('wx_login_scene_' . $new_scene_str, [
'scene_str' => $new_scene_str,
'create_time' => time(),
'expire_time' => time() + 300
], 300);
return json([
'code' => 0,
'msg' => '获取二维码成功',
'data' => [
'ticket' => $result['ticket'],
'expire_seconds' => $result['expire_seconds'],
'url' => $qrcodeUrl,
'scene_str' => $new_scene_str
]
]);
} catch (\Exception $e) {
Log::error('重新生成二维码失败:' . $e->getMessage());
return json(['code' => 1, 'msg' => '重新生成二维码失败:' . $e->getMessage()]);
}
}
/**
* 检查微信扫码登录状态
* @return \think\response\Json
*/
public function checkLoginStatus()
{
try {
$scene_str = Request::post('scene_str');
$ticket = Request::post('ticket');
if (empty($scene_str) || empty($ticket)) {
return json(['code' => 0, 'msg' => '参数错误']);
}
// 检查票据文件
$file = 'public/storage/uploads/ticket/' . $scene_str . "_" . $ticket . ".json";
if (!file_exists($file)) {
return json(['code' => 0, 'msg' => '等待扫码']);
}
$loginData = json_decode(file_get_contents($file), true);
if (!$loginData) {
return json(['code' => 0, 'msg' => '等待扫码']);
}
// 检查是否已经扫码
if (!isset($loginData['openid'])) {
return json(['code' => 0, 'msg' => '等待扫码']);
}
// 检查登录状态
if (!isset($loginData['login_status']) || $loginData['login_status'] !== 'success') {
return json(['code' => 0, 'msg' => '正在处理登录,请稍候...']);
}
// 登录成功,设置session
session('user_id', $loginData['uid']);
session('user_name', $loginData['name']);
session('user_avatar', $loginData['avatar']);
session('user_account', $loginData['user_account']);
session('openid', $loginData['openid']);
// 删除临时文件
@unlink($file);
// 返回用户信息
return json([
'code' => 1,
'msg' => '登录成功',
'data' => [
'uid' => $loginData['uid'],
'name' => $loginData['name'],
'avatar' => $loginData['avatar'],
'openid' => $loginData['openid'],
'user_account' => $loginData['user_account'],
'user_password' => $loginData['user_password'],
'expire_time' => $loginData['expire_time'],
'is_auto_login' => $loginData['is_auto_login']
]
]);
} catch (\Exception $e) {
Log::error('检查登录状态失败:' . $e->getMessage());
return json(['code' => 0, 'msg' => '系统错误']);
}
}
/**
* 根据openid获取用户信息
* @param string $openid
* @return array|null
*/
private function getUserInfoByOpenid($openid)
{
try {
// 这里需要根据你的用户表结构来实现
// 示例:从用户表中查询openid对应的用户信息
$user = Users::where('openid', $openid)->find();
if ($user) {
return $user->toArray();
}
return null;
} catch (\Exception $e) {
Log::error('获取用户信息失败:' . $e->getMessage());
return null;
}
}
/**
* 测试微信服务器访问
*/
public function testWechat()
{
try {
// 记录所有请求信息
$requestInfo = [
'time' => date('Y-m-d H:i:s'),
'method' => Request::method(),
'url' => Request::url(true),
'ip' => Request::ip(),
'real_ip' => Request::server('HTTP_X_REAL_IP'),
'forwarded_ip' => Request::server('HTTP_X_FORWARDED_FOR'),
'params' => Request::param(),
'headers' => getallheaders(),
'server' => [
'REQUEST_METHOD' => $_SERVER['REQUEST_METHOD'] ?? '',
'REQUEST_URI' => $_SERVER['REQUEST_URI'] ?? '',
'HTTP_HOST' => $_SERVER['HTTP_HOST'] ?? '',
'REMOTE_ADDR' => $_SERVER['REMOTE_ADDR'] ?? '',
'QUERY_STRING' => $_SERVER['QUERY_STRING'] ?? '',
'HTTP_USER_AGENT' => $_SERVER['HTTP_USER_AGENT'] ?? '',
'HTTP_REFERER' => $_SERVER['HTTP_REFERER'] ?? '',
'CONTENT_TYPE' => $_SERVER['CONTENT_TYPE'] ?? '',
'CONTENT_LENGTH' => $_SERVER['CONTENT_LENGTH'] ?? '',
'SERVER_NAME' => $_SERVER['SERVER_NAME'] ?? '',
'SERVER_ADDR' => $_SERVER['SERVER_ADDR'] ?? '',
'SERVER_PORT' => $_SERVER['SERVER_PORT'] ?? '',
'REQUEST_SCHEME' => $_SERVER['REQUEST_SCHEME'] ?? '',
]
];
Log::info('微信测试接口访问:' . json_encode($requestInfo, JSON_UNESCAPED_UNICODE));
// 获取原始数据
$rawData = file_get_contents("php://input");
if (!empty($rawData)) {
$rawData = mb_convert_encoding($rawData, 'UTF-8', 'UTF-8,GBK,GB2312');
Log::info('微信测试接口原始数据:' . $rawData);
}
// 如果是GET请求,进行签名验证
if (Request::method() == 'GET') {
$signature = Request::get('signature');
$timestamp = Request::get('timestamp');
$nonce = Request::get('nonce');
$echostr = Request::get('echostr');
$verifyInfo = [
'signature' => $signature,
'timestamp' => $timestamp,
'nonce' => $nonce,
'echostr' => $echostr,
'time' => date('Y-m-d H:i:s'),
'ip' => Request::ip(),
'headers' => getallheaders()
];
Log::info('微信测试接口验证参数:' . json_encode($verifyInfo, JSON_UNESCAPED_UNICODE));
if (!empty($signature) && !empty($timestamp) && !empty($nonce)) {
$token = AdminConfig::where('config_name', 'wechat_token')->value('config_value');
if (empty($token)) {
Log::error('微信token未配置');
return 'token not configured';
}
$tmpArr = array($token, $timestamp, $nonce);
sort($tmpArr, SORT_STRING);
$tmpStr = implode($tmpArr);
$tmpStr = sha1($tmpStr);
if ($tmpStr == $signature) {
Log::info('微信测试接口验证成功');
return $echostr;
} else {
Log::error('微信测试接口验证失败');
return 'signature verification failed';
}
}
}
return 'success';
} catch (\Exception $e) {
Log::error('微信测试接口错误:' . $e->getMessage());
Log::error('错误堆栈:' . $e->getTraceAsString());
return 'error';
}
}
/**
* 发送客服消息
* @param string $openid 接收者openid
* @param string $type 消息类型
* @param array $content 消息内容
* @return bool
*/
private function sendCustomMessage($openid, $type = 'text', $content = [])
{
try {
$accessToken = $this->getGZHAccessToken();
$url = "https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token={$accessToken}";
$data = [
'touser' => $openid,
'msgtype' => $type
];
if ($type === 'text') {
$data['text'] = ['content' => $content['content'] ?? '登录成功'];
}
$jsonData = json_encode($data, JSON_UNESCAPED_UNICODE);
$client = new Client(['verify' => false]);
$response = $client->post($url, [
'body' => $jsonData,
'headers' => [
'Content-Type' => 'application/json; charset=utf-8'
]
]);
$result = json_decode($response->getBody(), true);
if (isset($result['errcode']) && $result['errcode'] == 0) {
Log::info('发送客服消息成功:' . json_encode($result, JSON_UNESCAPED_UNICODE));
return true;
} else {
Log::error('发送客服消息失败:' . json_encode($result, JSON_UNESCAPED_UNICODE));
return false;
}
} catch (\Exception $e) {
Log::error('发送客服消息异常:' . $e->getMessage());
return false;
}
}
}
+22
View File
@@ -0,0 +1,22 @@
<?php
/**
* 商业使用授权协议
*
* Copyright (c) 2025 [云泽网]. 保留所有权利.
*
* 本软件仅供评估使用。任何商业用途必须获得书面授权许可。
* 未经授权商业使用本软件属于侵权行为,将承担法律责任。
*
* 授权购买请联系: 357099073@qq.com
* 官方网站: https://www.yunzer.cn
*
* 评估用户须知:
* 1. 禁止移除版权声明
* 2. 禁止用于生产环境
* 3. 禁止转售或分发
*/
// 这是系统自动生成的event定义文件
return [
];
+22
View File
@@ -0,0 +1,22 @@
<?php
/**
* 商业使用授权协议
*
* Copyright (c) 2025 [云泽网]. 保留所有权利.
*
* 本软件仅供评估使用。任何商业用途必须获得书面授权许可。
* 未经授权商业使用本软件属于侵权行为,将承担法律责任。
*
* 授权购买请联系: 357099073@qq.com
* 官方网站: https://www.yunzer.cn
*
* 评估用户须知:
* 1. 禁止移除版权声明
* 2. 禁止用于生产环境
* 3. 禁止转售或分发
*/
// 这是系统自动生成的middleware定义文件
return [
];
+25
View File
@@ -0,0 +1,25 @@
<?php
/**
* 商业使用授权协议
*
* Copyright (c) 2025 [云泽网]. 保留所有权利.
*
* 本软件仅供评估使用。任何商业用途必须获得书面授权许可。
* 未经授权商业使用本软件属于侵权行为,将承担法律责任。
*
* 授权购买请联系: 357099073@qq.com
* 官方网站: https://www.yunzer.cn
*
* 评估用户须知:
* 1. 禁止移除版权声明
* 2. 禁止用于生产环境
* 3. 禁止转售或分发
*/
namespace app\index\model;
use think\Model;
class AdminConfig extends Model
{
}
+25
View File
@@ -0,0 +1,25 @@
<?php
/**
* 商业使用授权协议
*
* Copyright (c) 2025 [云泽网]. 保留所有权利.
*
* 本软件仅供评估使用。任何商业用途必须获得书面授权许可。
* 未经授权商业使用本软件属于侵权行为,将承担法律责任。
*
* 授权购买请联系: 357099073@qq.com
* 官方网站: https://www.yunzer.cn
*
* 评估用户须知:
* 1. 禁止移除版权声明
* 2. 禁止用于生产环境
* 3. 禁止转售或分发
*/
namespace app\index\model\Articles;
use think\Model;
class Articles extends Model
{
}
@@ -0,0 +1,25 @@
<?php
/**
* 商业使用授权协议
*
* Copyright (c) 2025 [云泽网]. 保留所有权利.
*
* 本软件仅供评估使用。任何商业用途必须获得书面授权许可。
* 未经授权商业使用本软件属于侵权行为,将承担法律责任。
*
* 授权购买请联系: 357099073@qq.com
* 官方网站: https://www.yunzer.cn
*
* 评估用户须知:
* 1. 禁止移除版权声明
* 2. 禁止用于生产环境
* 3. 禁止转售或分发
*/
namespace app\index\model\Articles;
use think\Model;
class ArticlesCategory extends Model
{
}
+25
View File
@@ -0,0 +1,25 @@
<?php
/**
* 商业使用授权协议
*
* Copyright (c) 2025 [云泽网]. 保留所有权利.
*
* 本软件仅供评估使用。任何商业用途必须获得书面授权许可。
* 未经授权商业使用本软件属于侵权行为,将承担法律责任。
*
* 授权购买请联系: 357099073@qq.com
* 官方网站: https://www.yunzer.cn
*
* 评估用户须知:
* 1. 禁止移除版权声明
* 2. 禁止用于生产环境
* 3. 禁止转售或分发
*/
namespace app\index\model;
use think\Model;
class Attachments extends Model
{
}
+25
View File
@@ -0,0 +1,25 @@
<?php
/**
* 商业使用授权协议
*
* Copyright (c) 2025 [云泽网]. 保留所有权利.
*
* 本软件仅供评估使用。任何商业用途必须获得书面授权许可。
* 未经授权商业使用本软件属于侵权行为,将承担法律责任。
*
* 授权购买请联系: 357099073@qq.com
* 官方网站: https://www.yunzer.cn
*
* 评估用户须知:
* 1. 禁止移除版权声明
* 2. 禁止用于生产环境
* 3. 禁止转售或分发
*/
namespace app\index\model;
use think\Model;
class Banner extends Model
{
}
+33
View File
@@ -0,0 +1,33 @@
<?php
/**
* 商业使用授权协议
*
* Copyright (c) 2025 [云泽网]. 保留所有权利.
*
* 本软件仅供评估使用。任何商业用途必须获得书面授权许可。
* 未经授权商业使用本软件属于侵权行为,将承担法律责任。
*
* 授权购买请联系: 357099073@qq.com
* 官方网站: https://www.yunzer.cn
*
* 评估用户须知:
* 1. 禁止移除版权声明
* 2. 禁止用于生产环境
* 3. 禁止转售或分发
*/
namespace app\index\model;
use think\Model;
class LoginVerification extends Model
{
protected $autoWriteTimestamp = 'datetime';
protected $createTime = 'created_at';
protected $updateTime = 'updated_at';
public function getExpiredAtAttribute($value)
{
return strtotime($value);
}
}
+25
View File
@@ -0,0 +1,25 @@
<?php
/**
* 商业使用授权协议
*
* Copyright (c) 2025 [云泽网]. 保留所有权利.
*
* 本软件仅供评估使用。任何商业用途必须获得书面授权许可。
* 未经授权商业使用本软件属于侵权行为,将承担法律责任。
*
* 授权购买请联系: 357099073@qq.com
* 官方网站: https://www.yunzer.cn
*
* 评估用户须知:
* 1. 禁止移除版权声明
* 2. 禁止用于生产环境
* 3. 禁止转售或分发
*/
namespace app\index\model;
use think\Model;
class MailConfig extends Model
{
}
+25
View File
@@ -0,0 +1,25 @@
<?php
/**
* 商业使用授权协议
*
* Copyright (c) 2025 [云泽网]. 保留所有权利.
*
* 本软件仅供评估使用。任何商业用途必须获得书面授权许可。
* 未经授权商业使用本软件属于侵权行为,将承担法律责任。
*
* 授权购买请联系: 357099073@qq.com
* 官方网站: https://www.yunzer.cn
*
* 评估用户须知:
* 1. 禁止移除版权声明
* 2. 禁止用于生产环境
* 3. 禁止转售或分发
*/
namespace app\index\model\Resources;
use think\Model;
class Resources extends Model
{
}
@@ -0,0 +1,25 @@
<?php
/**
* 商业使用授权协议
*
* Copyright (c) 2025 [云泽网]. 保留所有权利.
*
* 本软件仅供评估使用。任何商业用途必须获得书面授权许可。
* 未经授权商业使用本软件属于侵权行为,将承担法律责任。
*
* 授权购买请联系: 357099073@qq.com
* 官方网站: https://www.yunzer.cn
*
* 评估用户须知:
* 1. 禁止移除版权声明
* 2. 禁止用于生产环境
* 3. 禁止转售或分发
*/
namespace app\index\model\Resources;
use think\Model;
class ResourcesCategory extends Model
{
}
+25
View File
@@ -0,0 +1,25 @@
<?php
/**
* 商业使用授权协议
*
* Copyright (c) 2025 [云泽网]. 保留所有权利.
*
* 本软件仅供评估使用。任何商业用途必须获得书面授权许可。
* 未经授权商业使用本软件属于侵权行为,将承担法律责任。
*
* 授权购买请联系: 357099073@qq.com
* 官方网站: https://www.yunzer.cn
*
* 评估用户须知:
* 1. 禁止移除版权声明
* 2. 禁止用于生产环境
* 3. 禁止转售或分发
*/
namespace app\index\model;
use think\Model;
class SystemNotice extends Model
{
}
+25
View File
@@ -0,0 +1,25 @@
<?php
/**
* 商业使用授权协议
*
* Copyright (c) 2025 [云泽网]. 保留所有权利.
*
* 本软件仅供评估使用。任何商业用途必须获得书面授权许可。
* 未经授权商业使用本软件属于侵权行为,将承担法律责任。
*
* 授权购买请联系: 357099073@qq.com
* 官方网站: https://www.yunzer.cn
*
* 评估用户须知:
* 1. 禁止移除版权声明
* 2. 禁止用于生产环境
* 3. 禁止转售或分发
*/
namespace app\index\model;
use think\Model;
class UserMessage extends Model
{
}
+25
View File
@@ -0,0 +1,25 @@
<?php
/**
* 商业使用授权协议
*
* Copyright (c) 2025 [云泽网]. 保留所有权利.
*
* 本软件仅供评估使用。任何商业用途必须获得书面授权许可。
* 未经授权商业使用本软件属于侵权行为,将承担法律责任。
*
* 授权购买请联系: 357099073@qq.com
* 官方网站: https://www.yunzer.cn
*
* 评估用户须知:
* 1. 禁止移除版权声明
* 2. 禁止用于生产环境
* 3. 禁止转售或分发
*/
namespace app\index\model;
use think\Model;
class Users extends Model
{
}
+712
View File
@@ -0,0 +1,712 @@
{include file="component/head" /}
{include file="component/header" /}
<div class="main">
<div class="location">
<div class="container">
<div class="location-item">
<a href="/">首页</a>
<span>></span>
<a href="/index/articles/index">{$cateName}</a>
</div>
</div>
</div>
<div class="body-container">
<div class="article-detail-container">
<div class="article-header">
<h1 class="article-title">{$article.title}</h1>
{if $article.is_trans eq 1 && $article.transurl}
<div class="trans">转载至:<span class="trans-url" data-url="{$article.transurl}">{$article.transurl}</span>
</div>
{/if}
<div class="article-meta">
<span class="article-author"><i class="fa fa-user"></i> <span>{$article.author}</span></span>
<span class="article-date"><i class="fa fa-calendar"></i>
<span>{$article.create_time|date="Y-m-d"}</span></span>
<span class="article-views"><i class="fa-solid fa-eye"></i> <span>{$article.views}</span> 阅读</span>
</div>
</div>
<div class="article-content">
{$article.content|raw}
</div>
<div class="disclaimers">
<div class="disclaimer-item">
<div class="disclaimer-title">免责声明:</div>
<div class="disclaimer-content">
<?php echo $config['disclaimers'] ?>
</div>
</div>
</div>
<div class="article-tags">
<span class="tag-label">标签:</span>
<div>
{if $article.tags}
{volist name="article.tags|explode=',',','" id="tag"}
<span class="tag-item">{$tag}</span>
{/volist}
{else}
<span class="no-tags">暂无标签</span>
{/if}
</div>
</div>
<div class="article-actions">
<div class="action-item like-btn" data-id="{$article.id}">
<i class="fa fa-thumbs-up"></i>
<span class="action-text">点赞</span>
<span class="action-count">{$article.likes}</span>
</div>
<div class="action-item share-btn">
<i class="fa fa-share-alt"></i>
<span class="action-text">分享</span>
</div>
</div>
<div class="article-navigation">
<div class="prev-article">
{if $prevArticle}
<a href="/index/articles/detail?id={$prevArticle.id}">
<i class="fa fa-arrow-left"></i> 上一篇:{$prevArticle.title}
</a>
{else}
<span class="disabled"><i class="fa fa-arrow-left"></i> 没有上一篇了</span>
{/if}
</div>
<div class="next-article">
{if $nextArticle}
<a href="/index/articles/detail?id={$nextArticle.id}">
下一篇:{$nextArticle.title} <i class="fa fa-arrow-right"></i>
</a>
{else}
<span class="disabled">没有下一篇了 <i class="fa fa-arrow-right"></i></span>
{/if}
</div>
</div>
<div class="related-articles">
<h3 class="related-title">相关推荐</h3>
<div class="related-list">
{volist name="relatedArticles" id="related"}
<div class="related-item">
<a href="/index/articles/detail?id={$related.id}">
<div class="related-image">
<img src="{$related.image}" alt="{$related.title}">
</div>
<div class="related-info">
<div class="related-item-title">{$related.title}</div>
</div>
</a>
</div>
{/volist}
</div>
</div>
</div>
<div class="article-detail-right">
<div class="aboutauthor">
<div class="aboutauthor-title">关于作者</div>
<div class="aboutauthor-main">
<div class="aboutauthor-main-top">
<div class="aboutauthor-avatar">
<img src="{$authorInfo.avatar}" alt="作者头像">
</div>
<div class="aboutauthor-info">
<div class="author-name">{$authorInfo.name}</div>
</div>
</div>
<div class="aboutauthor-main-middle">
<div class="author-stats">
<div class="author-stats-item">
<h6>资源</h6>
<span class="count">{$authorInfo.resource_count}</span>
</div>
<div class="author-stats-item">
<h6>文章</h6>
<span class="count">{$authorInfo.article_count}</span>
</div>
<div class="author-stats-item">
<h6>粉丝</h6>
<span class="count">
0
</span>
</div>
</div>
</div>
</div>
<div class="aboutauthor-btn">
<button class="follow-btn">
<i class="fa fa-user-plus"></i> 关注他
</button>
<button class="message-btn">
<i class="fa fa-envelope"></i> 发私信
</button>
</div>
</div>
</div>
</div>
</div>
<!-- 返回顶部按钮 -->
<div class="go-to-top">
<i class="layui-icon-up"></i>
</div>
{include file="component/footer" /}
<script>
// 更新访问次数
async function updateArticleViews(articleId) {
try {
await fetch('/index/articles/updateViews', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: articleId })
});
} catch (error) {
console.error('更新访问次数失败:', error);
}
}
// 页面加载完成后执行
document.addEventListener('DOMContentLoaded', function () {
const articleId = '{$article.id}';
// 更新访问次数
updateArticleViews(articleId);
// 点赞功能
document.querySelector('.like-btn').addEventListener('click', async function () {
try {
const response = await fetch('/index/articles/like?id=' + articleId, {
method: 'POST',
headers: { 'X-Requested-With': 'XMLHttpRequest' }
});
const result = await response.json();
if (result.code === 1) {
const countElement = this.querySelector('.action-count');
countElement.textContent = parseInt(countElement.textContent) + 1;
this.classList.add('liked');
this.querySelector('i').style.color = '#f57005';
layer.msg('点赞成功', { icon: 1 });
} else {
layer.msg(result.msg, { icon: 2 });
}
} catch (error) {
layer.msg('点赞失败,请稍后重试', { icon: 2 });
}
});
// 分享功能
document.querySelector('.share-btn').addEventListener('click', function () {
const url = window.location.href;
navigator.clipboard.writeText(url).then(() => {
layer.msg('链接已复制到剪贴板', { icon: 1 });
}).catch(() => {
layer.msg('复制失败,请手动复制', { icon: 2 });
});
});
// 转载链接点击
document.querySelector('.trans-url')?.addEventListener('click', function () {
const url = this.dataset.url;
if (url) {
window.open(url, '_blank');
}
});
// 返回顶部
const goToTop = document.querySelector('.go-to-top');
window.addEventListener('scroll', function () {
if (window.scrollY > 300) {
goToTop.classList.add('show');
} else {
goToTop.classList.remove('show');
}
});
goToTop.addEventListener('click', function () {
window.scrollTo({ top: 0, behavior: 'smooth' });
});
});
</script>
<style>
.location {
max-width: 1200px;
margin: 30px auto;
}
.main .body-container {
display: flex;
max-width: 1200px;
margin: 30px auto;
gap: 30px;
}
.main .body-container .article-detail-right {
width: 30%;
}
.article-detail-container {
padding: 50px;
background: #fff;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
border-radius: 8px;
width: 70%;
}
.main .body-container .article-detail-right .aboutauthor {
background: #fff;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
border-radius: 8px;
}
.main .body-container .article-detail-right .aboutauthor-title {
height: 60px;
display: flex;
align-items: center;
padding-left: 20px;
border-bottom: 1px solid #eee;
font-weight: 700;
}
.main .body-container .article-detail-right .aboutauthor-main {
display: flex;
flex-direction: column;
}
.main .body-container .article-detail-right .aboutauthor-main .aboutauthor-main-top {
display: flex;
align-items: center;
padding-left: 20px !important;
padding: 20px 0;
border-bottom: 1px solid #efefef;
margin-bottom: 20px;
}
.main .body-container .article-detail-right .aboutauthor-main .aboutauthor-main-top .aboutauthor-avatar {
margin-right: 12px;
}
.main .body-container .article-detail-right .aboutauthor-main .aboutauthor-main-top .aboutauthor-info .author-name {
font-size: 20px;
font-weight: 700;
}
.main .body-container .article-detail-right .aboutauthor-main .aboutauthor-main-top .aboutauthor-avatar img {
width: 60px;
height: 60px;
border-radius: 4px;
box-sizing: border-box;
margin: 0px;
min-width: 0px;
max-width: 100%;
background-color: #fff;
}
.main .body-container .article-detail-right .aboutauthor-main .aboutauthor-main-middle {
/* margin-left: 20px; */
}
.main .body-container .article-detail-right .aboutauthor-main .aboutauthor-main-middle .author-stats {
display: flex;
justify-content: space-evenly;
}
.main .body-container .article-detail-right .aboutauthor-main .aboutauthor-main-middle .author-stats .author-stats-item {
display: flex;
flex-direction: column;
align-items: center;
}
.main .body-container .article-detail-right .aboutauthor-main .aboutauthor-main-middle .author-stats .author-stats-item .count {
/* font-size: 30px; */
font-weight: 700;
}
.main .body-container .article-detail-right .aboutauthor-btn {
display: flex;
justify-content: space-evenly;
padding: 20px 0;
}
.main .body-container .article-detail-right .aboutauthor-btn .follow-btn {
background-color: #0081ff;
color: #fff;
padding: 10px 20px;
border-radius: 8px;
border: none;
}
.main .body-container .article-detail-right .aboutauthor-btn .message-btn {
color: #0081ff;
padding: 10px 20px;
border-radius: 8px;
border: 1px solid #eee;
}
.article-header {
margin-bottom: 30px;
border-bottom: 1px solid #eee;
padding-bottom: 20px;
}
.article-title {
font-size: 28px;
font-weight: 700;
color: #333;
margin-bottom: 15px;
line-height: 1.4;
}
.article-meta {
display: flex;
flex-wrap: wrap;
gap: 20px;
color: #666;
font-size: 14px;
}
.article-meta span {
display: flex;
align-items: center;
}
.article-meta i {
margin-right: 5px;
}
.article-content {
line-height: 1.8;
color: #333;
font-size: 16px;
margin-bottom: 30px;
border-bottom: 1px solid #eee;
}
.article-content img {
max-width: 100%;
height: auto;
margin: 15px 0;
border-radius: 4px;
}
.article-tags {
margin: 20px 0;
display: flex;
align-items: center;
flex-wrap: wrap;
}
.tag-label {
font-weight: bold;
margin-right: 10px;
}
.tag-item {
background: #f2f2f2;
padding: 4px 10px;
border-radius: 15px;
font-size: 12px;
margin-right: 8px;
color: #666;
}
.article-actions {
display: flex;
justify-content: center;
gap: 40px;
margin: 30px 0;
padding: 20px 0;
border-top: 1px solid #eee;
border-bottom: 1px solid #eee;
}
.action-item {
display: flex;
flex-direction: column;
align-items: center;
cursor: pointer;
}
.action-item i {
font-size: 24px;
color: #666;
margin-bottom: 5px;
}
.action-text {
font-size: 14px;
color: #666;
}
.action-count {
font-size: 12px;
color: #999;
margin-top: 3px;
}
.article-navigation {
display: flex;
justify-content: space-between;
margin: 30px 0;
}
.prev-article,
.next-article {
max-width: 45%;
}
.prev-article a,
.next-article a {
color: #333 !important;
text-decoration: none;
}
.prev-article a:hover,
.next-article a:hover {
color: #f57005 !important;
transition: all 0.3s ease;
}
.disabled {
color: #999;
}
.related-articles {
margin: 40px 0;
}
.related-title {
font-size: 20px;
font-weight: 600;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 1px solid #eee;
}
.related-list {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
}
.related-item {
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: transform 0.3s;
}
.related-item:hover {
transform: translateY(-5px);
}
.related-item a {
text-decoration: none;
color: inherit;
}
.related-image img {
width: 100%;
height: 150px;
object-fit: cover;
}
.related-info {
padding: 10px;
}
.related-item-title {
font-size: 16px;
font-weight: 600;
margin-bottom: 5px;
color: #333;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.related-item-desc {
font-size: 12px;
color: #666;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.article-comments {
margin-top: 40px;
}
.comments-title {
font-size: 20px;
font-weight: 600;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 1px solid #eee;
}
.comment-form {
margin-bottom: 30px;
}
.comment-textarea {
width: 100%;
height: 100px;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
resize: none;
margin-bottom: 10px;
}
.comment-submit {
background: #f57005;
color: white;
border: none;
padding: 8px 20px;
border-radius: 4px;
cursor: pointer;
float: right;
}
.comment-item {
display: flex;
margin-bottom: 20px;
padding-bottom: 20px;
border-bottom: 1px solid #eee;
}
.comment-avatar img {
width: 50px;
height: 50px;
border-radius: 50%;
margin-right: 15px;
}
.comment-content {
flex: 1;
}
.comment-user {
font-weight: 600;
margin-bottom: 5px;
}
.comment-text {
line-height: 1.6;
margin-bottom: 10px;
}
.comment-footer {
display: flex;
justify-content: space-between;
color: #999;
font-size: 12px;
}
.comment-reply {
cursor: pointer;
color: #f57005;
}
.no-comments,
.no-related,
.no-tags {
color: #999;
text-align: center;
padding: 20px;
}
@media (max-width: 768px) {
.article-title {
font-size: 24px;
}
.related-list {
grid-template-columns: repeat(1, 1fr);
}
.article-meta {
gap: 10px;
}
}
/* 返回顶部按钮样式 */
.go-to-top {
position: fixed;
right: 30px;
bottom: 30px;
width: 40px;
height: 40px;
background: #f57005;
color: #fff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
opacity: 0;
visibility: hidden;
transition: all 0.3s ease;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
z-index: 1000;
}
.go-to-top.show {
opacity: 1;
visibility: visible;
}
.go-to-top:hover {
background: #e66600;
transform: translateY(-3px);
}
.go-to-top i {
font-size: 18px;
}
@media (max-width: 768px) {
.go-to-top {
right: 20px;
bottom: 20px;
width: 36px;
height: 36px;
}
}
.location-item a {
color: #000 !important;
}
.disclaimers {
color: #b1b1b1;
width: 80%;
margin: 20px auto;
margin-bottom: 60px;
}
.disclaimer-title {
font-size: 16px;
font-weight: 600;
margin-bottom: 10px;
}
.disclaimer-content {
font-size: 14px;
line-height: 1.6;
}
.disclaimer-content p {
margin-bottom: 0;
}
</style>
{include file="component/foot" /}
+562
View File
@@ -0,0 +1,562 @@
{include file="component/head" /}
{include file="component/header" /}
<!-- 简约现代文章中心 -->
<div class="modern-articles-page">
<!-- 简约标题区 -->
<div class="modern-header">
<div class="container">
<h1 class="modern-title">文章中心</h1>
<p class="modern-subtitle">探索知识与洞见</p>
</div>
</div>
<!-- 主要内容区 -->
<div class="container">
<div class="modern-layout">
<!-- 侧边分类导航 -->
<aside class="modern-sidebar">
<div class="sidebar-card">
<h3 class="sidebar-title">
<i class="layui-icon layui-icon-list"></i>
<span>分类导航</span>
</h3>
<ul class="category-menu">
{volist name="cate.subCategories" id="subCategory"}
<li class="menu-item {$cate.id == $subCategory.id ? 'active' : ''}"
data-cateid="{$subCategory.id}">
<span>{$subCategory.name}</span>
<i class="layui-icon layui-icon-right"></i>
</li>
{/volist}
</ul>
</div>
</aside>
<!-- 文章内容区 -->
<main class="modern-main">
<!-- 文章列表 -->
<div class="article-grid" id="articleList">
{volist name="cate.subCategories" id="subCategory"}
{if $cate.id == $subCategory.id}
{if !empty($subCategory.list)}
{volist name="subCategory.list" id="article"}
<article class="article-card">
<div class="card-image">
<img src="{$article.image}" alt="{$article.title}">
<div class="image-overlay"></div>
</div>
<div class="card-content">
<div class="meta-info">
<!-- <span class="category-tag">${article.category_name || '未分类'}</span> -->
<time class="publish-date">{:date('Y-m-d', $article['create_time'])}</time>
</div>
<a href="/index/articles/detail?id=${article.id}">
<h3 class="article-title">${article.title}</h3>
</a>
<div class="card-footer">
<div class="stats">
<span class="views"><i class="layui-icon layui-icon-eye"></i> ${article.views ||
0}</span>
<span class="likes"><i class="layui-icon layui-icon-praise"></i> ${article.likes ||
0}</span>
</div>
</div>
</div>
</article>
{/volist}
{else}
<div class="empty-state">
<div class="empty-icon">
<i class="layui-icon layui-icon-template-1"></i>
</div>
<h4>暂无文章</h4>
<p>当前分类下没有找到相关文章</p>
</div>
{/if}
{/if}
{/volist}
</div>
<!-- 分页 -->
<div class="modern-pagination" id="pagination"></div>
</main>
</div>
</div>
</div>
<script>
layui.use(['laypage', 'jquery'], function () {
var laypage = layui.laypage;
var $ = layui.jquery;
// 分类切换
$('.menu-item').on('click', function () {
var cateid = $(this).data('cateid');
var $menuItems = $('.menu-item');
// 更新选中状态
$menuItems.removeClass('active');
$(this).addClass('active');
// 加载文章
loadArticles(cateid, 1);
});
// 页面加载完成后,自动触发第一个分类的点击事件
$(document).ready(function () {
var $firstMenuItem = $('.menu-item').first();
if ($firstMenuItem.length > 0) {
$firstMenuItem.click();
}
});
// 加载文章函数
function loadArticles(cateid, page) {
$.ajax({
url: '/index/articles/list',
type: 'POST',
data: {
cate: cateid,
page: page
},
beforeSend: function () {
$('#articleList').html('<div class="loading-state"><i class="layui-icon layui-icon-loading"></i>加载中...</div>');
},
success: function (res) {
if (res.code === 1) {
var html = '';
if (res.data.articles && res.data.articles.length > 0) {
res.data.articles.forEach(function (article) {
html += `<article class="article-card">
<div class="card-image">
<img src="${article.image}" alt="${article.title}">
<div class="image-overlay"></div>
</div>
<div class="card-content">
<div class="meta-info">
</div>
<a href="/index/articles/detail?id=${article.id}" class="read-more">
<h3 class="article-title">${article.title}</h3>
</a>
<div class="card-footer">
<div class="stats">
<span class="views"><i class="layui-icon layui-icon-eye"></i> ${article.views || 0}</span>
<span class="likes"><i class="layui-icon layui-icon-praise"></i> ${article.likes || 0}</span>
</div>
<div class="times">
<time class="publish-date">${article.create_time ? new Date(article.create_time * 1000).toLocaleDateString() : ''}</time>
</div>
</div>
</div>
</article>`;
});
} else {
html = `<div class="empty-state">
<div class="empty-icon">
<i class="layui-icon layui-icon-template-1"></i>
</div>
<h4>暂无文章</h4>
<p>当前分类下没有找到相关文章</p>
</div>`;
}
$('#articleList').html(html);
// 渲染分页
laypage.render({
elem: 'pagination',
count: res.data.total || 0,
limit: res.data.per_page || 12,
curr: res.data.current_page || 1,
theme: '#1E9FFF',
layout: ['prev', 'page', 'next'],
jump: function (obj, first) {
if (!first) {
loadArticles(cateid, obj.curr);
}
}
});
}
}
});
}
});
</script>
{include file="component/footer" /}
<style>
/* 基础样式重置 */
.modern-articles-page {
font-family: 'Helvetica Neue', Arial, 'PingFang SC', 'Microsoft YaHei', sans-serif;
color: #333;
line-height: 1.6;
background-color: #f9fafc;
padding-bottom: 60px;
}
/* 标题区样式 */
.modern-header {
background: linear-gradient(135deg, #1E9FFF 0%, #0d8aff 100%);
color: white;
padding: 80px 0 60px;
text-align: center;
margin-bottom: 40px;
}
.modern-title {
font-size: 2.5rem;
font-weight: 300;
margin-bottom: 15px;
letter-spacing: 1px;
}
.modern-subtitle {
font-size: 1.1rem;
font-weight: 300;
opacity: 0.9;
margin: 0;
}
/* 布局结构 */
.modern-layout {
display: grid;
grid-template-columns: 260px 1fr;
gap: 30px;
}
/* 侧边栏样式 */
.modern-sidebar {
position: sticky;
top: 30px;
align-self: start;
}
.sidebar-card {
background: white;
border-radius: 8px;
box-shadow: 0 2px 15px rgba(0, 0, 0, 0.03);
overflow: hidden;
}
.sidebar-title {
font-size: 1.1rem;
font-weight: 500;
padding: 20px;
margin: 0;
display: flex;
align-items: center;
color: #555;
border-bottom: 1px solid #f0f0f0;
}
.sidebar-title i {
margin-right: 10px;
font-size: 1.2rem;
color: #1E9FFF;
}
.category-menu {
list-style: none;
padding: 0;
margin: 0;
}
.menu-item {
padding: 15px 20px;
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
transition: all 0.2s ease;
border-left: 3px solid transparent;
}
.menu-item:hover {
background-color: #f8fafd;
color: #1E9FFF;
}
.menu-item.active {
background-color: #f0f7ff;
border-left-color: #1E9FFF;
color: #1E9FFF;
font-weight: 500;
}
.menu-item i {
font-size: 0.9rem;
color: #aaa;
}
.menu-item.active i,
.menu-item:hover i {
color: #1E9FFF;
}
/* 主内容区样式 */
.modern-main {
background: transparent;
}
/* 文章网格布局 */
.article-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 25px;
margin-bottom: 40px;
}
/* 文章卡片样式 */
.article-card {
background: white;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 3px 15px rgba(0, 0, 0, 0.03);
transition: all 0.3s ease;
display: flex;
flex-direction: column;
}
.article-card:hover {
transform: translateY(-5px);
box-shadow: 0 5px 25px rgba(0, 0, 0, 0.08);
}
.card-image {
height: 135px;
position: relative;
overflow: hidden;
}
.card-image img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.5s ease;
}
.image-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(to top, rgba(0, 0, 0, 0.3), transparent);
}
.article-card:hover .card-image img {
transform: scale(1.05);
}
.card-content {
padding: 20px;
flex: 1;
display: flex;
flex-direction: column;
}
.card-content .read-more h3:hover {
color: #1E9FFF !important;
transition: all ease .5s;
font-weight:700;
}
.meta-info {
display: flex;
justify-content: space-between;
/* margin-bottom: 12px; */
font-size: 0.85rem;
color: #666;
}
.category-tag {
background: #f0f7ff;
color: #1E9FFF;
padding: 3px 10px;
border-radius: 4px;
font-size: 0.75rem;
}
.article-title {
font-size: 1rem;
font-weight: 500;
margin: 0 0 10px;
color: #333;
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
.article-excerpt {
font-size: 0.9rem;
color: #666;
margin: 0 0 20px;
flex: 1;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
.card-footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: auto;
}
.stats {
display: flex;
gap: 15px;
}
.times,
.stats {
font-size: 0.85rem;
color: #999;
}
.stats i {
margin-right: 3px;
}
.read-more {
color: #1E9FFF;
font-size: 0.9rem;
text-decoration: none;
font-weight: 500;
transition: all 0.2s ease;
display: inline-flex;
align-items: center;
}
.read-more:hover {
color: #0d8aff;
text-decoration: underline;
}
/* 分页样式 */
.modern-pagination {
text-align: center;
margin-top: 40px;
}
.layui-laypage a,
.layui-laypage span {
border-radius: 4px !important;
margin: 0 3px !important;
}
.layui-laypage a {
color: #666 !important;
}
.layui-laypage .layui-laypage-curr .layui-laypage-em {
background-color: #1E9FFF !important;
}
/* 空状态样式 */
.empty-state {
grid-column: 1 / -1;
text-align: center;
padding: 60px 20px;
background: white;
border-radius: 8px;
box-shadow: 0 3px 15px rgba(0, 0, 0, 0.03);
}
.empty-icon {
font-size: 3rem;
color: #ddd;
margin-bottom: 20px;
}
.empty-icon i {
font-size: inherit;
}
.empty-state h4 {
font-size: 1.2rem;
font-weight: 400;
color: #666;
margin: 0 0 10px;
}
.empty-state p {
color: #999;
font-size: 0.95rem;
margin: 0;
}
/* 加载状态 */
.loading-state {
grid-column: 1 / -1;
text-align: center;
padding: 40px;
color: #666;
}
.loading-state i {
font-size: 1.5rem;
margin-right: 10px;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
/* 响应式设计 */
@media (max-width: 992px) {
.modern-layout {
grid-template-columns: 1fr;
}
.modern-sidebar {
position: static;
margin-bottom: 30px;
}
.article-grid {
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
}
}
@media (max-width: 768px) {
.modern-header {
padding: 60px 0 40px;
}
.modern-title {
font-size: 2rem;
}
}
@media (max-width: 576px) {
.article-grid {
grid-template-columns: 1fr;
}
.modern-title {
font-size: 1.8rem;
}
.modern-subtitle {
font-size: 1rem;
}
}
</style>
{include file="component/foot" /}
+76
View File
@@ -0,0 +1,76 @@
<div class="container py-5">
<div class="row g-4">
<!-- 左侧分类列表 -->
<div class="col-lg-3">
<div class="category-sidebar">
<div class="sidebar-header">
<i class="layui-icon layui-icon-app"></i>
<span>文章分类</span>
</div>
<div class="category-list">
{volist name="categories" id="cate"}
<div class="category-item {$category.id == $cate.id ? 'active' : ''}" data-cateid="{$cate.id}">{$cate.name}</div>
{/volist}
</div>
</div>
</div>
<!-- 右侧文章列表 -->
<div class="col-lg-9">
{if $category}
<div class="category-header mb-4">
<h2 class="category-title">{$category.name}</h2>
<p class="category-desc">{$category.desc|default=''}</p>
</div>
{/if}
<div class="article-list">
{volist name="articles" id="article"}
<div class="article-item">
<div class="row g-0">
<div class="col-md-4">
<div class="article-image">
<img src="{$article.image|default='/static/images/default.jpg'}" alt="{$article.title}">
</div>
</div>
<div class="col-md-8">
<div class="article-content">
<h3 class="article-title">
<a href="/index/articles/detail?id={$article.id}">{$article.title}</a>
</h3>
<p class="article-desc">{$article.desc|default=''}</p>
<div class="article-meta">
<div class="article-stats">
<span><i class="layui-icon layui-icon-eye"></i> {$article.views|default=0}</span>
<span><i class="layui-icon layui-icon-praise"></i> {$article.likes|default=0}</span>
<span><i class="layui-icon layui-icon-date"></i> {$article.create_time|date="Y-m-d"}</span>
</div>
<a href="/index/articles/detail?id={$article.id}" class="btn-detail">查看详情</a>
</div>
</div>
</div>
</div>
</div>
{/volist}
</div>
<!-- 分页 -->
<div class="mt-5">
{$articles|raw}
</div>
</div>
</div>
</div>
<script>
layui.use(['layer'], function () {
var layer = layui.layer;
var $ = layui.$;
// 分类切换
$('.category-item').on('click', function() {
var cateid = $(this).data('cateid');
window.location.href = '/index/articles/list?cate=' + cateid;
});
});
</script>
+49
View File
@@ -0,0 +1,49 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>错误提示</title>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<link rel="stylesheet" href="/static/layui/css/layui.css">
<style>
.error-container {
text-align: center;
padding: 50px 20px;
}
.error-icon {
font-size: 48px;
color: #FF5722;
margin-bottom: 20px;
}
.error-message {
font-size: 18px;
color: #666;
margin-bottom: 30px;
}
.back-btn {
margin-top: 20px;
}
</style>
</head>
<body>
<div class="layui-fluid">
<div class="layui-row">
<div class="layui-col-md12">
<div class="error-container">
<div class="error-icon">
<i class="layui-icon layui-icon-face-cry"></i>
</div>
<div class="error-message">
<?php echo $msg; ?>
</div>
<div class="back-btn">
<a href="javascript:history.back();" class="layui-btn layui-btn-primary">返回上一页</a>
<a href="/" class="layui-btn">返回首页</a>
</div>
</div>
</div>
</div>
</div>
<script src="/static/layui/layui.js"></script>
</body>
</html>
+7
View File
@@ -0,0 +1,7 @@
<div class="main-header">
<!-- Banner轮播 -->
<div class="layui-carousel" id="test10" lay-filter="test10">
<div carousel-item="">
</div>
</div>
</div>
+344
View File
@@ -0,0 +1,344 @@
<script src="__LAYUI__/layui.js" charset="utf-8"></script>
<script src="__JS__/bootstrap.bundle.js"></script>
<script charset="UTF-8" id="LA_COLLECT" src="//www.yunzer.cn/plugins/js-sdk-pro.min.js"></script>
<script>LA.init({ id: "KoyzaWWEcLvPzkQn", ck: "KoyzaWWEcLvPzkQn", autoTrack: true, prefix: 'event' })</script>
<script src="__JS__/banner.js"></script>
<script>
// 在页面加载时立即执行
(function () {
// 检查是否已经刷新过
if (sessionStorage.getItem('has_refreshed') === 'true') {
return;
}
// 检查localStorage中是否有用户账号
var userAccount = localStorage.getItem('user_account');
if (userAccount) {
// 同步到cookie
document.cookie = "user_account=" + userAccount + "; path=/";
// 如果有其他必要的数据,也同步到cookie
var userId = localStorage.getItem('user_id');
var userName = localStorage.getItem('user_name');
var userAvatar = localStorage.getItem('user_avatar');
if (userId) document.cookie = "user_id=" + userId + "; path=/";
if (userName) document.cookie = "user_name=" + userName + "; path=/";
if (userAvatar) document.cookie = "user_avatar=" + userAvatar + "; path=/";
// 刷新页面以应用新的cookie,并标记已刷新
if (!document.cookie.includes('user_id')) {
sessionStorage.setItem('has_refreshed', 'true');
window.location.reload();
}
}
})();
// 搜索功能相关代码
layui.use(['layer'], function () {
var layer = layui.layer;
var $ = layui.jquery;
// 执行搜索
function executeSearch() {
var searchInput = document.getElementById('searchInput');
if (!searchInput) {
layer.msg('搜索组件初始化失败');
return;
}
var keyword = searchInput.value.trim();
var type = document.getElementById('searchType').value;
if (!keyword) {
layer.msg('请输入搜索关键词');
return;
}
// 跳转到统一的搜索结果页面
window.location.href = '/index/search/index?keyword=' + encodeURIComponent(keyword) + '&type=' + type;
}
// 绑定事件
$(function () {
var searchMask = $('#searchMask');
var searchInput = $('#searchInput');
var searchBtn = $('#searchBtn');
var mainSearchIcon = $('#mainSearchIcon');
var stickySearchIcon = $('#stickySearchIcon');
// 显示搜索框
function showSearch() {
searchMask.addClass('show');
setTimeout(function () {
searchInput.focus();
}, 300);
}
// 隐藏搜索框
function hideSearch() {
searchMask.removeClass('show');
searchInput.val('');
}
// 绑定搜索图标点击事件
mainSearchIcon.on('click', showSearch);
stickySearchIcon.on('click', showSearch);
// 绑定搜索按钮点击事件
searchBtn.on('click', function (e) {
e.preventDefault();
executeSearch();
});
// 绑定回车键搜索
searchInput.on('keypress', function (e) {
if (e.which === 13) {
e.preventDefault();
executeSearch();
}
});
// 点击遮罩层关闭搜索框
searchMask.on('click', function (e) {
if ($(e.target).hasClass('search-mask')) {
hideSearch();
}
});
// 绑定ESC键关闭搜索框
$(document).on('keydown', function (e) {
if (e.keyCode === 27 && searchMask.hasClass('show')) {
hideSearch();
}
});
// 输入框获得焦点时选中所有文本
searchInput.on('focus', function () {
this.select();
});
});
});
// 其他功能相关代码
layui.use(['carousel', 'form'], function () {
var carousel = layui.carousel;
var form = layui.form;
var $ = layui.$;
// 检查本地存储并自动登录
function checkAutoLogin() {
// 如果已经登录,不再执行自动登录
if ($('#userAvatarMain').length > 0) {
return;
}
// 如果已经尝试过自动登录,不再执行
if (sessionStorage.getItem('auto_login_attempted') === 'true') {
return;
}
// 从localStorage获取用户账号
var userAccount = localStorage.getItem('user_account');
if (userAccount) {
// 标记已尝试自动登录
sessionStorage.setItem('auto_login_attempted', 'true');
// 发送自动登录请求
$.ajax({
url: '/index/user/login',
type: 'POST',
data: {
account: userAccount,
password: atob(localStorage.getItem('user_password'))
},
dataType: 'json',
success: function (res) {
if (res.code === 0) {
// 设置cookie
document.cookie = "user_id=" + res.data.id + "; path=/";
document.cookie = "user_name=" + res.data.name + "; path=/";
document.cookie = "user_avatar=" + res.data.avatar + "; path=/";
document.cookie = "user_account=" + userAccount + "; path=/";
// 同时更新localStorage
localStorage.setItem('user_id', res.data.id);
localStorage.setItem('user_name', res.data.name);
localStorage.setItem('user_avatar', res.data.avatar);
// 登录成功,强制刷新页面
window.location.href = window.location.href + '?t=' + new Date().getTime();
} else {
// 登录失败,清除所有相关存储
localStorage.removeItem('user_account');
localStorage.removeItem('user_password');
sessionStorage.removeItem('auto_login_attempted');
}
}
});
}
}
// 页面加载时检查自动登录
checkAutoLogin();
$(document).ready(function () {
// 主导航头像
$("#userAvatarMain").click(function (e) {
e.stopPropagation();
$("#userDropdownMain").toggleClass("show");
$("#userDropdownSticky").removeClass("show"); // 保证只显示一个
});
// 固定导航头像
$("#userAvatarSticky").click(function (e) {
e.stopPropagation();
$("#userDropdownSticky").toggleClass("show");
$("#userDropdownMain").removeClass("show"); // 保证只显示一个
});
// 点击页面其他地方隐藏所有菜单
$(document).click(function (e) {
if (!$(e.target).closest('.user-dropdown, #userAvatarMain, #userAvatarSticky').length) {
$("#userDropdownMain, #userDropdownSticky").removeClass("show");
}
});
// 点击菜单项时隐藏菜单
$("#userDropdownMain li a, #userDropdownSticky li a").click(function () {
$("#userDropdownMain, #userDropdownSticky").removeClass("show");
});
});
// 退出登录
$('.logout-btn').on('click', function () {
layer.confirm('确定要退出登录吗?', {
btn: ['确定', '取消']
}, function () {
// 先发送退出请求
$.ajax({
url: '/index/user/logout',
type: 'POST',
dataType: 'json',
success: function (res) {
if (res.code === 0) {
// 清除localStorage
localStorage.removeItem('user_account');
localStorage.removeItem('user_password');
localStorage.removeItem('user_id');
localStorage.removeItem('user_name');
localStorage.removeItem('user_avatar');
// 清除sessionStorage
sessionStorage.removeItem('auto_login_attempted');
sessionStorage.removeItem('has_refreshed');
// 清除cookie
document.cookie = "user_id=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
document.cookie = "user_name=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
document.cookie = "user_avatar=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
document.cookie = "user_account=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
document.cookie = "user_password=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
// 强制刷新页面,不使用缓存
window.location.href = window.location.href + '?t=' + new Date().getTime();
}
}
});
});
});
// 监听滚动事件
$(window).scroll(function () {
var scrollTop = $(window).scrollTop();
if (scrollTop > 150) { // 当滚动超过150px时显示固定导航
$('.sticky-nav').fadeIn();
} else {
$('.sticky-nav').fadeOut();
}
});
// 公众号二维码
const trigger = document.querySelector('.qrcode-trigger');
const popup = document.querySelector('.qrcode-popup');
// // 鼠标移入显示二维码
// trigger.addEventListener('mouseenter', function () {
// popup.style.display = 'block';
// });
// // 鼠标移出隐藏二维码
// trigger.addEventListener('mouseleave', function () {
// popup.style.display = 'none';
// });
// // 鼠标移入二维码区域时保持显示
// popup.addEventListener('mouseenter', function () {
// popup.style.display = 'block';
// });
// // 鼠标移出二维码区域时隐藏
// popup.addEventListener('mouseleave', function () {
// popup.style.display = 'none';
// });
form.on('submit(accountLogin)', function (data) {
$.ajax({
url: '{:url("index/user/login")}',
type: 'POST',
data: data.field,
dataType: 'json',
success: function (res) {
if (res.code === 0) {
// 存储登录数据,设置7天过期
var expireTime = new Date().getTime() + 7 * 24 * 60 * 60 * 1000;
// 设置localStorage
localStorage.setItem('user_account', data.field.account);
localStorage.setItem('user_password', btoa(data.field.password));
localStorage.setItem('expire_time', expireTime);
localStorage.setItem('is_auto_login', 'true');
// 设置cookie
document.cookie = "user_id=" + res.data.id + "; path=/";
document.cookie = "user_name=" + res.data.name + "; path=/";
document.cookie = "user_avatar=" + res.data.avatar + "; path=/";
document.cookie = "expire_time=" + expireTime + "; path=/";
document.cookie = "is_auto_login=true; path=/";
document.cookie = "user_account=" + data.field.account + "; path=/";
document.cookie = "user_password=" + btoa(data.field.password) + "; path=/";
// 设置sessionStorage
sessionStorage.setItem('auto_login_attempted', 'true');
layer.msg('登录成功', {
icon: 1,
time: 2000,
shade: 0.3
}, function () {
// 获取当前页面URL,如果是从其他页面跳转来的,则返回上一页
var currentUrl = window.location.href;
var referrer = document.referrer;
// 如果是从登录页面跳转来的,则返回上一页
if (referrer && referrer.includes('/index/user/login')) {
window.location.href = referrer;
} else {
// 否则刷新当前页面
window.location.href = currentUrl + '?t=' + new Date().getTime();
}
});
layer.msg(res.msg, {
icon: 2,
time: 2000
});
}
}
});
return false;
});
});
</script>
</body>
</html>
+60
View File
@@ -0,0 +1,60 @@
<footer class="footer" style="background-image: url(__IMAGES__/footer-bg-1.png)">
<div class="container">
<div class="row" style="width: 100%;">
<div class="row-main">
<div class="mr-20">
<img src="{$config['logo']}" alt="" height="70">
<p class="text-white-50 my-4 f18" style="width: 400px;">美天智能科技,这里是介绍!</p>
</div>
<div style="display: flex; justify-content: space-between;width: 100%;margin-right: 200px;">
<div>
<h4 class="text-white f-20 font-weight-normal mb-3">关于我们</h4>
<ul class="list-unstyled footer-sub-menu">
<li><a href="#" class="footer-link">概况</a></li>
<li><a href="#" class="footer-link">资讯</a></li>
<li><a href="#" class="footer-link">加入我们</a></li>
<li><a href="#" class="footer-link">联系我们</a></li>
</ul>
</div>
<div>
<h4 class="text-white f-20 font-weight-normal mb-3">商务合作</h4>
<ul class="list-unstyled footer-sub-menu">
<li><a href="#" class="footer-link">商务合作</a></li>
</ul>
</div>
<div>
<h4 class="text-white f-20 font-weight-normal mb-3">服务支持</h4>
<ul class="list-unstyled footer-sub-menu">
<li><a href="#" class="footer-link">常见问答</a></li>
<li><a href="#" class="footer-link">软件下载</a></li>
<li><a href="#" class="footer-link">服务政策</a></li>
<li><a href="#" class="footer-link">投诉建议</a></li>
</ul>
</div>
</div>
<div>
<div class="text-center">
<img src="{$config['web_wechat']}" alt="微信二维码" class="img-fluid" style="max-width: 150px;">
<p class="text-white-50 mt-2">微信公众号</p>
</div>
</div>
</div>
</div>
</div>
</footer>
<section class="copyright text-center">
<div class="container wow fadeInUp animated" data-wow-delay="400ms"
style="visibility: visible; animation-delay: 400ms; animation-name: fadeInUp;">
<p class="copyright__text">Copyright <span class="dynamic-year">2025</span> | All Rights By <a
href="http://www.yunzer.cn">Yunzer</a></p>
</div>
<div class="container wow fadeInUp animated" data-wow-delay="400ms"
style="visibility: visible; animation-delay: 400ms; animation-name: fadeInUp;">
<a href="https://beian.miit.gov.cn/" target="_blank" rel="nofollow">{$config['admin_icp']}</a>
</div>
<div class="tongji">
<script id="LA-DATA-WIDGET" crossorigin="anonymous" charset="UTF-8"
src="https://v6-widget.51.la/v6/KoyzaWWEcLvPzkQn/quote.js?theme=#1690FF,#FFFFFF,#999999,#FFFFFF,#FFFFFF,#1690FF,12&f=12"></script>
</div>
</section>
+341
View File
@@ -0,0 +1,341 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{$config['web_title']}</title>
<link rel="stylesheet" href="__LAYUI__/css/layui.css">
<link rel="stylesheet" href="__CSS__/style.css">
<link rel="stylesheet" href="__CSS__/bootstrap.min.css">
<link rel="stylesheet" href="__CSS__/fontawesome.css">
<style>
/* 用户头像样式 */
#userAvatar {
width: 40px;
height: 40px;
cursor: pointer;
transition: all 0.3s ease;
}
#userAvatar:hover {
transform: scale(1.05);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
/* 下拉菜单容器 */
.user-dropdown {
position: absolute;
top: 50px;
right: 0;
width: 160px;
background: #fff;
border-radius: 4px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
opacity: 0;
visibility: hidden;
transform: translateY(-10px);
transition: all 0.3s ease;
z-index: 9999;
}
.user-dropdown.show {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
/* 下拉菜单列表 */
.user-dropdown ul {
margin: 0;
padding: 5px 0;
list-style: none;
}
/* 下拉菜单项 */
.user-dropdown li {
margin: 0;
padding: 0;
}
/* 下拉菜单链接 */
.user-dropdown li a {
display: flex;
align-items: center;
padding: 10px 15px;
color: #333;
text-decoration: none;
transition: all 0.3s ease;
}
/* 下拉菜单图标 */
.user-dropdown li a i {
margin-right: 10px;
font-size: 16px;
color: #666;
}
/* 下拉菜单文字 */
.user-dropdown li a span {
font-size: 14px;
}
/* 下拉菜单悬停效果 */
.user-dropdown li a:hover {
background: #f5f5f5;
color: #1E9FFF;
}
.user-dropdown li a:hover i {
color: #1E9FFF;
}
/* 分隔线 */
.user-dropdown li:not(:last-child) {
border-bottom: 1px solid #f0f0f0;
}
#userDropdownSticky a {
color: #0d6efd !important;
}
.main-menu__right {
display: flex;
align-items: center;
}
.username {
display: flex;
align-items: center;
}
.search-icon {
font-size: 20px;
cursor: pointer;
color: #333;
transition: color 0.3s ease;
}
.search-icon:hover {
color: #1e9fff;
}
.search-mask {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
z-index: 9999;
display: none;
opacity: 0;
transition: opacity 0.3s ease;
justify-content: center;
align-items: center;
}
.search-mask.show {
display: flex;
opacity: 1;
}
.search-container {
position: relative;
width: 80%;
padding: 20px;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
transform: translateY(-20px);
transition: transform 0.3s ease;
}
.search-mask.show .search-container {
transform: translateY(0);
}
.search-box {
display: flex;
align-items: center;
height: 60px;
background: #fff;
border-radius: 30px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
.search-box input {
flex: 1;
height: 100%;
padding: 0 20px;
border: none;
outline: none;
font-size: 16px;
}
.search-box button {
height: 100%;
padding: 0 30px;
border: none;
background: #1E9FFF;
color: #fff;
font-size: 16px;
cursor: pointer;
transition: background-color 0.3s;
}
.search-box button:hover {
background: #1a8fe6;
}
.search-type {
height: 100%;
padding: 0 15px;
border: none;
border-right: 1px solid #eee;
background: #f8f8f8;
color: #666;
font-size: 14px;
cursor: pointer;
outline: none;
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e");
background-repeat: no-repeat;
background-position: right 10px center;
background-size: 12px;
padding-right: 30px;
}
.search-type:hover {
background-color: #f0f0f0;
}
.search-type:focus {
background-color: #fff;
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);
}
</style>
<style>
/* Banner样式 */
.banner-content {
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
}
.banner-image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.banner-image img {
width: 100%;
height: 100%;
object-fit: cover;
object-position: center;
}
.banner-text {
position: absolute;
top: 40%;
left: 10%;
z-index: 1;
display: flex;
flex-direction: column;
align-items: flex-start;
color: #fff;
}
.banner-text a {
text-decoration: none;
margin-top: 30px;
}
.banner-title {
font-size: 4em;
font-weight: 600;
margin-bottom: 10px;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
}
.banner-desc {
font-size: 2em;
font-weight: 400;
max-width: 800px;
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.3);
}
.banner-btn {
background: #fff;
color: #000;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
transition: all 0.3s ease;
}
.banner-btn:hover {
background: #000;
color: #fff;
}
.banner-slider {
width: 100%;
height: 86vh;
overflow: hidden;
position: relative;
}
.banner-container {
width: 100%;
height: 100%;
}
.banner-slide {
display: block;
width: 100%;
height: 100%;
}
.banner-slide img {
width: 100%;
height: 100%;
object-fit: cover;
/* 关键:等比缩放并铺满 */
display: block;
}
.layui-carousel {
background: #f8f8f8;
margin: 0;
padding: 0;
}
/* 确保轮播容器和项目的高度正确 */
#test10,
#test10 [carousel-item],
#test10 [carousel-item]>* {
height: 86vh !important;
}
#test10 [carousel-item]>* {
background: none !important;
}
.main-content {
min-height: 23vh;
}
</style>
</head>
<body>
+908
View File
@@ -0,0 +1,908 @@
<?php
/**
* 商业使用授权协议
*
* Copyright (c) 2025 [云泽网]. 保留所有权利.
*
* 本软件仅供评估使用。任何商业用途必须获得书面授权许可。
* 未经授权商业使用本软件属于侵权行为,将承担法律责任。
*
* 授权购买请联系: 357099073@qq.com
* 官方网站: https://www.yunzer.cn
*
* 评估用户须知:
* 1. 禁止移除版权声明
* 2. 禁止用于生产环境
* 3. 禁止转售或分发
*/
// 获取当前登录状态
$isLoggedIn = false;
$userInfo = [
'is_login' => false,
'name' => '',
'avatar' => '/static/images/avatar.png' // 默认头像
];
// 检查cookie
$userAccount = cookie('user_account');
if ($userAccount) {
$isLoggedIn = true;
$userInfo = [
'is_login' => true,
'name' => cookie('user_name'),
'avatar' => cookie('user_avatar') ? cookie('user_avatar') : '/static/images/avatar.png'
];
}
// 添加一个隐藏的div来存储登录状态
$loginStatus = [
'isLoggedIn' => $isLoggedIn,
'userAccount' => $userAccount ?? ''
];
?>
<!-- 添加一个隐藏的div来存储登录状态 -->
<div id="loginStatus" style="display: none;" data-is-logged-in="{$isLoggedIn}" data-user-account="{$userAccount ?? ''}">
</div>
<div style="display: flex;flex-direction: column;">
<!-- 导航栏 -->
<div class="main-menu">
<div class="container">
<div class="main-menu__logo">
<a href="/index.html"><img src="__IMAGES__/logo1.png" width="186" alt="Logo"></a>
</div>
<div class="main-menu__nav">
<ul class="main-menu__list">
<li><a href="/">首页</a></li>
<li><a href="/index/articles/index?cateid=1">站点资讯</a></li>
<li><a href="/index/articles/index?cateid=3">技术文章</a></li>
<li><a href="/index/program/index?cateid=2">办公资源</a></li>
<li><a href="/index/program/index?cateid=1">程序下载</a></li>
<li><a href="/index/game/index?cateid=8">游戏下载</a></li>
</ul>
</div>
<div class="main-menu__search">
<i class="layui-icon layui-icon-search search-icon" id="mainSearchIcon"></i>
</div>
<div class="main-menu__right">
<div class="username">
<?php if ($userInfo['is_login']): ?>
<span class="username-text">{$userInfo.name}</span>
<?php endif; ?>
</div>
<div class="layui-inline">
<!-- 根据登录状态显示不同的内容 -->
<?php if ($isLoggedIn): ?>
<div class="layui-inline" style="position: relative;margin-left:20px;">
<img src="{$userInfo.avatar}" class="layui-circle"
style="width: 40px; height: 40px; cursor: pointer;" id="userAvatarSticky">
<div class="user-dropdown" id="userDropdownSticky">
<ul>
<li>
<a href="/index/user/profile"><i
class="layui-icon layui-icon-user"></i><span>个人中心</span></a>
</li>
<li>
<a href="/index/user/settings"><i
class="layui-icon layui-icon-set"></i><span>账号管理</span></a>
</li>
<li>
<a href="javascript:;" class="logout-btn"><i
class="layui-icon layui-icon-logout"></i><span>退出登录</span></a>
</li>
</ul>
</div>
</div>
<?php else: ?>
<div class="layui-inline">
<a href="/index/user/login" class="layui-btn layui-btn-normal">登录</a>
<a href="/index/user/register" class="layui-btn layui-btn-primary">注册</a>
</div>
<?php endif; ?>
</div>
</div>
</div>
</div>
</div>
<!-- 固定导航 -->
<div class="sticky-nav" style="display: none;">
<div class="container">
<div class="sticky-nav__logo">
<a href="/index.html"><img src="__IMAGES__/logo1.png" width="150" alt="Logo"></a>
</div>
<div class="sticky-nav__menu">
<ul>
<li><a href="/">首页</a></li>
<li><a href="/index/articles/index?cateid=1">站点资讯</a></li>
<li><a href="/index/articles/index?cateid=3">技术文章</a></li>
<li><a href="/index/program/index?cateid=2">办公资源</a></li>
<li><a href="/index/program/index?cateid=1">程序下载</a></li>
<li><a href="/index/game/index?cateid=8">游戏下载</a></li>
</ul>
</div>
<div class="sticky-nav__search">
<i class="layui-icon layui-icon-search search-icon" id="stickySearchIcon"></i>
</div>
<div class="sticky-nav__right">
<div class="main-menu__right">
<div class="username">
<?php if ($userInfo['is_login']): ?>
<span class="username-text">{$userInfo.name}</span>
<?php endif; ?>
</div>
<div class="layui-inline">
<!-- 根据登录状态显示不同的内容 -->
<?php if ($isLoggedIn): ?>
<div class="layui-inline" style="position: relative;margin-left:20px;">
<img src="{$userInfo.avatar}" class="layui-circle"
style="width: 40px; height: 40px; cursor: pointer;" id="userAvatarSticky">
<div class="user-dropdown" id="userDropdownSticky">
<ul>
<li>
<a href="/index/user/profile"><i
class="layui-icon layui-icon-user"></i><span>个人中心</span></a>
</li>
<li>
<a href="/index/user/settings"><i
class="layui-icon layui-icon-set"></i><span>账号管理</span></a>
</li>
<li>
<a href="javascript:;" class="logout-btn"><i
class="layui-icon layui-icon-logout"></i><span>退出登录</span></a>
</li>
</ul>
</div>
</div>
<?php else: ?>
<div class="layui-inline">
<a href="/index/user/login" class="layui-btn layui-btn-normal">登录</a>
<a href="/index/user/register" class="layui-btn layui-btn-primary">注册</a>
</div>
<?php endif; ?>
</div>
</div>
</div>
</div>
</div>
<!-- 搜索遮罩 -->
<div class="search-mask" id="searchMask">
<div class="search-container">
<div class="search-box">
<select id="searchType" class="search-type">
<option value="articles">文章</option>
<option value="resources">资源</option>
</select>
<input type="text" id="searchInput" placeholder="请输入搜索关键词">
<button id="searchBtn">搜索</button>
</div>
</div>
</div>
<script>
// 在页面加载时立即执行
(function () {
// 检查是否已经刷新过
if (sessionStorage.getItem('has_refreshed') === 'true') {
return;
}
// 检查localStorage中是否有用户账号
var userAccount = localStorage.getItem('user_account');
if (userAccount) {
// 同步到cookie
document.cookie = "user_account=" + userAccount + "; path=/";
// 如果有其他必要的数据,也同步到cookie
var userId = localStorage.getItem('user_id');
var userName = localStorage.getItem('user_name');
var userAvatar = localStorage.getItem('user_avatar');
if (userId) document.cookie = "user_id=" + userId + "; path=/";
if (userName) document.cookie = "user_name=" + userName + "; path=/";
if (userAvatar) document.cookie = "user_avatar=" + userAvatar + "; path=/";
// 刷新页面以应用新的cookie,并标记已刷新
if (!document.cookie.includes('user_id')) {
sessionStorage.setItem('has_refreshed', 'true');
window.location.reload();
}
}
})();
layui.use(['carousel', 'form', 'layer'], function () {
var carousel = layui.carousel, form = layui.form, layer = layui.layer, $ = layui.$;
// 检查本地存储并自动登录
function checkAutoLogin() {
// 如果已经登录,不再执行自动登录
if ($('#userAvatarMain').length > 0) {
return;
}
// 如果已经尝试过自动登录,不再执行
if (sessionStorage.getItem('auto_login_attempted') === 'true') {
return;
}
// 从localStorage获取用户账号
var userAccount = localStorage.getItem('user_account');
if (userAccount) {
// 标记已尝试自动登录
sessionStorage.setItem('auto_login_attempted', 'true');
// 发送自动登录请求
$.ajax({
url: '/index/user/login',
type: 'POST',
data: {
account: userAccount,
password: atob(localStorage.getItem('user_password'))
},
dataType: 'json',
success: function (res) {
if (res.code === 0) {
// 设置cookie
document.cookie = "user_id=" + res.data.id + "; path=/";
document.cookie = "user_name=" + res.data.name + "; path=/";
document.cookie = "user_avatar=" + res.data.avatar + "; path=/";
document.cookie = "user_account=" + userAccount + "; path=/";
// 同时更新localStorage
localStorage.setItem('user_id', res.data.id);
localStorage.setItem('user_name', res.data.name);
localStorage.setItem('user_avatar', res.data.avatar);
// 登录成功,强制刷新页面
window.location.href = window.location.href + '?t=' + new Date().getTime();
} else {
// 登录失败,清除所有相关存储
localStorage.removeItem('user_account');
localStorage.removeItem('user_password');
sessionStorage.removeItem('auto_login_attempted');
}
}
});
}
}
// 页面加载时检查自动登录
checkAutoLogin();
$(document).ready(function () {
// 主导航头像
$("#userAvatarMain").click(function (e) {
e.stopPropagation();
$("#userDropdownMain").toggleClass("show");
$("#userDropdownSticky").removeClass("show"); // 保证只显示一个
});
// 固定导航头像
$("#userAvatarSticky").click(function (e) {
e.stopPropagation();
$("#userDropdownSticky").toggleClass("show");
$("#userDropdownMain").removeClass("show"); // 保证只显示一个
});
// 点击页面其他地方隐藏所有菜单
$(document).click(function (e) {
if (!$(e.target).closest('.user-dropdown, #userAvatarMain, #userAvatarSticky').length) {
$("#userDropdownMain, #userDropdownSticky").removeClass("show");
}
});
// 点击菜单项时隐藏菜单
$("#userDropdownMain li a, #userDropdownSticky li a").click(function () {
$("#userDropdownMain, #userDropdownSticky").removeClass("show");
});
});
// 退出登录
$('.logout-btn').on('click', function () {
layer.confirm('确定要退出登录吗?', {
btn: ['确定', '取消']
}, function () {
// 先发送退出请求
$.ajax({
url: '/index/user/logout',
type: 'POST',
dataType: 'json',
success: function (res) {
if (res.code === 0) {
// 清除localStorage
localStorage.removeItem('user_account');
localStorage.removeItem('user_password');
localStorage.removeItem('user_id');
localStorage.removeItem('user_name');
localStorage.removeItem('user_avatar');
// 清除sessionStorage
sessionStorage.removeItem('auto_login_attempted');
sessionStorage.removeItem('has_refreshed');
// 清除cookie
document.cookie = "user_id=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
document.cookie = "user_name=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
document.cookie = "user_avatar=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
document.cookie = "user_account=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
document.cookie = "user_password=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
// 强制刷新页面,不使用缓存
window.location.href = window.location.href + '?t=' + new Date().getTime();
}
}
});
});
});
// 监听滚动事件
$(window).scroll(function () {
var scrollTop = $(window).scrollTop();
if (scrollTop > 150) { // 当滚动超过150px时显示固定导航
$('.sticky-nav').fadeIn();
} else {
$('.sticky-nav').fadeOut();
}
});
// 公众号二维码
const trigger = document.querySelector('.qrcode-trigger');
const popup = document.querySelector('.qrcode-popup');
// 鼠标移入显示二维码
trigger.addEventListener('mouseenter', function () {
popup.style.display = 'block';
});
// 鼠标移出隐藏二维码
trigger.addEventListener('mouseleave', function () {
popup.style.display = 'none';
});
// 鼠标移入二维码区域时保持显示
popup.addEventListener('mouseenter', function () {
popup.style.display = 'block';
});
// 鼠标移出二维码区域时隐藏
popup.addEventListener('mouseleave', function () {
popup.style.display = 'none';
});
form.on('submit(accountLogin)', function (data) {
$.ajax({
url: '{:url("index/user/login")}',
type: 'POST',
data: data.field,
dataType: 'json',
success: function (res) {
if (res.code === 0) {
// 存储登录数据,设置7天过期
var expireTime = new Date().getTime() + 7 * 24 * 60 * 60 * 1000;
// 设置localStorage
localStorage.setItem('user_account', data.field.account);
localStorage.setItem('user_password', btoa(data.field.password));
localStorage.setItem('expire_time', expireTime);
localStorage.setItem('is_auto_login', 'true');
// 设置cookie
document.cookie = "user_id=" + res.data.id + "; path=/";
document.cookie = "user_name=" + res.data.name + "; path=/";
document.cookie = "user_avatar=" + res.data.avatar + "; path=/";
document.cookie = "expire_time=" + expireTime + "; path=/";
document.cookie = "is_auto_login=true; path=/";
document.cookie = "user_account=" + data.field.account + "; path=/";
document.cookie = "user_password=" + btoa(data.field.password) + "; path=/";
// 设置sessionStorage
sessionStorage.setItem('auto_login_attempted', 'true');
layer.msg('登录成功', {
icon: 1,
time: 2000,
shade: 0.3
}, function () {
// 获取当前页面URL
var currentUrl = window.location.href;
// 如果当前页面是登录页面,则跳转到首页
if (currentUrl.includes('/index/user/login')) {
window.location.href = '/index.html';
} else {
// 否则刷新当前页面
window.location.href = currentUrl + '?t=' + new Date().getTime();
}
});
} else {
layer.msg(res.msg, {
icon: 2,
time: 2000
});
}
}
});
return false;
});
});
// 搜索功能相关代码
layui.use(['layer'], function () {
var layer = layui.layer;
var $ = layui.jquery;
// 执行搜索
function executeSearch() {
var searchInput = document.getElementById('searchInput');
if (!searchInput) {
layer.msg('搜索组件初始化失败');
return;
}
var keyword = searchInput.value.trim();
var type = document.getElementById('searchType').value;
if (!keyword) {
layer.msg('请输入搜索关键词');
return;
}
// 跳转到统一的搜索结果页面
window.location.href = '/index/search/index?keyword=' + encodeURIComponent(keyword) + '&type=' + type;
}
// 绑定事件
$(function() {
var searchMask = $('#searchMask');
var searchInput = $('#searchInput');
var searchBtn = $('#searchBtn');
var mainSearchIcon = $('#mainSearchIcon');
var stickySearchIcon = $('#stickySearchIcon');
// 显示搜索框
function showSearch() {
searchMask.addClass('show');
setTimeout(function() {
searchInput.focus();
}, 300);
}
// 隐藏搜索框
function hideSearch() {
searchMask.removeClass('show');
searchInput.val('');
}
// 绑定搜索图标点击事件
mainSearchIcon.on('click', showSearch);
stickySearchIcon.on('click', showSearch);
// 绑定搜索按钮点击事件
searchBtn.on('click', function(e) {
e.preventDefault();
executeSearch();
});
// 绑定回车键搜索
searchInput.on('keypress', function(e) {
if (e.which === 13) {
e.preventDefault();
executeSearch();
}
});
// 点击遮罩层关闭搜索框
searchMask.on('click', function(e) {
if ($(e.target).hasClass('search-mask')) {
hideSearch();
}
});
// 绑定ESC键关闭搜索框
$(document).on('keydown', function(e) {
if (e.keyCode === 27 && searchMask.hasClass('show')) {
hideSearch();
}
});
// 输入框获得焦点时选中所有文本
searchInput.on('focus', function() {
this.select();
});
});
});
</script>
<style>
/* 用户头像样式 */
#userAvatar {
width: 40px;
height: 40px;
cursor: pointer;
transition: all 0.3s ease;
}
#userAvatar:hover {
transform: scale(1.05);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
/* 下拉菜单容器 */
.user-dropdown {
position: absolute;
top: 50px;
right: 0;
width: 160px;
background: #fff;
border-radius: 4px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
opacity: 0;
visibility: hidden;
transform: translateY(-10px);
transition: all 0.3s ease;
z-index: 9999;
}
.user-dropdown.show {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
/* 下拉菜单列表 */
.user-dropdown ul {
margin: 0;
padding: 5px 0;
list-style: none;
}
/* 下拉菜单项 */
.user-dropdown li {
margin: 0;
padding: 0;
}
/* 下拉菜单链接 */
.user-dropdown li a {
display: flex;
align-items: center;
padding: 10px 15px;
color: #333;
text-decoration: none;
transition: all 0.3s ease;
}
/* 下拉菜单图标 */
.user-dropdown li a i {
margin-right: 10px;
font-size: 16px;
color: #666;
}
/* 下拉菜单文字 */
.user-dropdown li a span {
font-size: 14px;
}
/* 下拉菜单悬停效果 */
.user-dropdown li a:hover {
background: #f5f5f5;
color: #1E9FFF;
}
.user-dropdown li a:hover i {
color: #1E9FFF;
}
/* 分隔线 */
.user-dropdown li:not(:last-child) {
border-bottom: 1px solid #f0f0f0;
}
#userDropdownSticky a {
color: #0d6efd !important;
}
/* Banner样式 */
.banner-content {
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
}
.banner-image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.banner-image img {
width: 100%;
height: 100%;
object-fit: cover;
object-position: center;
}
.banner-text {
position: absolute;
top: 40%;
left: 10%;
z-index: 1;
display: flex;
flex-direction: column;
align-items: flex-start;
color: #fff;
}
.banner-text a {
text-decoration: none;
margin-top: 30px;
}
.banner-title {
font-size: 4em;
font-weight: 600;
margin-bottom: 10px;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
}
.banner-desc {
font-size: 2em;
font-weight: 400;
max-width: 800px;
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.3);
}
.banner-btn {
background: #fff;
color: #000;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
transition: all 0.3s ease;
}
.banner-btn:hover {
background: #000;
color: #fff;
}
.banner-slider {
width: 100%;
height: 86vh;
overflow: hidden;
position: relative;
}
.banner-container {
width: 100%;
height: 100%;
}
.banner-slide {
display: block;
width: 100%;
height: 100%;
}
.banner-slide img {
width: 100%;
height: 100%;
object-fit: cover;
/* 关键:等比缩放并铺满 */
display: block;
}
.layui-carousel {
background: #f8f8f8;
margin: 0;
padding: 0;
}
/* 确保轮播容器和项目的高度正确 */
#test10,
#test10 [carousel-item],
#test10 [carousel-item]>* {
height: 86vh !important;
}
#test10 [carousel-item]>* {
background: none !important;
}
.main-menu__right {
display: flex;
align-items: center;
}
.username {
display: flex;
align-items: center;
}
/* 搜索相关样式 */
.search-mask {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
z-index: 9999;
display: none;
opacity: 0;
transition: opacity 0.3s ease;
justify-content: center;
align-items: center;
}
.search-mask.show {
display: flex;
opacity: 1;
}
.search-container {
position: relative;
width: 80%;
padding: 20px;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
transform: translateY(-20px);
transition: transform 0.3s ease;
}
.search-mask.show .search-container {
transform: translateY(0);
}
.search-box {
display: flex;
align-items: center;
height: 60px;
background: #fff;
border-radius: 30px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
overflow: hidden;
}
.search-box input {
flex: 1;
height: 100%;
padding: 0 20px;
border: none;
outline: none;
font-size: 16px;
}
.search-box button {
height: 100%;
padding: 0 30px;
border: none;
background: #1E9FFF;
color: #fff;
font-size: 16px;
cursor: pointer;
transition: background-color 0.3s;
}
.search-box button:hover {
background: #1a8fe6;
}
.search-icon {
font-size: 20px;
cursor: pointer;
color: #333;
transition: color 0.3s ease;
}
.search-icon:hover {
color: #1e9fff;
}
.search-type {
height: 100%;
padding: 0 15px;
border: none;
border-right: 1px solid #eee;
background: #f8f8f8;
color: #666;
font-size: 14px;
cursor: pointer;
outline: none;
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e");
background-repeat: no-repeat;
background-position: right 10px center;
background-size: 12px;
padding-right: 30px;
}
.search-type:hover {
background-color: #f0f0f0;
}
.search-type:focus {
background-color: #fff;
box-shadow: inset 0 1px 2px rgba(0,0,0,0.1);
}
.search-results {
padding: 15px;
max-height: 370px;
overflow-y: auto;
}
.search-section {
margin-bottom: 20px;
}
.search-section h3 {
font-size: 16px;
color: #333;
margin-bottom: 10px;
padding-bottom: 5px;
border-bottom: 1px solid #eee;
}
.search-section ul {
list-style: none;
padding: 0;
margin: 0;
}
.search-section li {
padding: 8px 0;
border-bottom: 1px dashed #eee;
display: flex;
align-items: center;
justify-content: space-between;
}
.search-section li:last-child {
border-bottom: none;
}
.search-section a {
color: #333;
text-decoration: none;
flex: 1;
}
.search-section a:hover {
color: #1E9FFF;
}
.search-section .downloads {
color: #1E9FFF;
font-size: 12px;
margin-left: 10px;
}
/* 自定义滚动条样式 */
.search-results::-webkit-scrollbar {
width: 6px;
}
.search-results::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 3px;
}
.search-results::-webkit-scrollbar-thumb {
background: #ccc;
border-radius: 3px;
}
.search-results::-webkit-scrollbar-thumb:hover {
background: #999;
}
</style>
+201
View File
@@ -0,0 +1,201 @@
<?php
/**
* 商业使用授权协议
*
* Copyright (c) 2025 [云泽网]. 保留所有权利.
*
* 本软件仅供评估使用。任何商业用途必须获得书面授权许可。
* 未经授权商业使用本软件属于侵权行为,将承担法律责任。
*
* 授权购买请联系: 357099073@qq.com
* 官方网站: https://www.yunzer.cn
*
* 评估用户须知:
* 1. 禁止移除版权声明
* 2. 禁止用于生产环境
* 3. 禁止转售或分发
*/
// 获取当前登录状态
$isLoggedIn = false;
$userInfo = [
'is_login' => false,
'name' => '',
'avatar' => '/static/images/avatar.png' // 默认头像
];
// 检查cookie
$userAccount = cookie('user_account');
if ($userAccount) {
$isLoggedIn = true;
$userInfo = [
'is_login' => true,
'name' => cookie('user_name'),
'avatar' => cookie('user_avatar') ? cookie('user_avatar') : '/static/images/avatar.png'
];
}
// 添加一个隐藏的div来存储登录状态
$loginStatus = [
'isLoggedIn' => $isLoggedIn,
'userAccount' => $userAccount ?? ''
];
?>
<!-- 添加一个隐藏的div来存储登录状态 -->
<div id="loginStatus" style="display: none;" data-is-logged-in="{$isLoggedIn}" data-user-account="{$userAccount ?? ''}">
</div>
<div style="display: flex;flex-direction: column;">
<!-- <div class="topbar-one">
<div class="container">
<div style="width: 70%;">
<ul class="list-unstyled topbar-one__info">
<li class="topbar-one__info__item">
<span class="topbar-one__info__icon fas fa-phone-alt" style="margin-right: 10px;"></span>
<a href="{$config['web_phone']}">{$config['web_phone']}</a>
</li>
<li class="topbar-one__info__item">
<span class="topbar-one__info__icon fas fa-envelope" style="margin-right: 10px;"></span>
<a href="mailto:{$config['web_mail']}">{$config['web_mail']}</a>
</li>
</ul>
</div>
<div class="topbar-one__social" style="width: 30%;">
<a href="javascript:;" class="qrcode-trigger"><i class="layui-icon layui-icon-qrcode"></i> 公众号</a>
<div class="qrcode-popup"
style="display:none;position:absolute;right:54px;top:32px;background:#fff;padding:10px;box-shadow:0 0 10px rgba(0,0,0,0.1); z-index: 1000;">
<img src="{$config['web_wechat']}" alt="公众号二维码" style="width:180px;height:180px;">
</div>
</div>
</div>
</div> -->
<!-- 导航栏 -->
<div class="main-menu">
<div class="container">
<div class="main-menu__logo">
<a href="/"><img src="{$config['logo1']}" width="186" alt="Logo"></a>
</div>
<div class="main-menu__nav">
<ul class="main-menu__list">
<li><a href="/">首页</a></li>
<li><a href="/index/articles/index?cateid=1">站点资讯</a></li>
<li><a href="/index/articles/index?cateid=3">技术文章</a></li>
</ul>
</div>
<div class="main-menu__search">
<i class="layui-icon layui-icon-search search-icon" id="mainSearchIcon"></i>
</div>
<!-- 搜索蒙版 -->
<div class="search-mask" id="searchMask" style="">
<div class="search-container">
<div class="search-box">
<select id="searchType" class="search-type">
<option value="articles">文章</option>
<option value="resources">资源</option>
</select>
<input type="text" id="searchInput" placeholder="请输入搜索关键词">
<button class="search-btn" id="searchBtn">搜索</button>
</div>
</div>
</div>
<div class="main-menu__right">
<div class="username">
<?php if ($userInfo['is_login']): ?>
<span class="username-text">{$userInfo.name}</span>
<?php endif; ?>
</div>
<div class="layui-inline">
<!-- 根据登录状态显示不同的内容 -->
<?php if ($userInfo['is_login']): ?>
<div class="layui-inline" style="position: relative;margin-left:20px;">
<img src="{$userInfo.avatar}" class="layui-circle"
style="width: 40px; height: 40px; cursor: pointer;" id="userAvatarMain">
<div class="user-dropdown" id="userDropdownMain">
<ul>
<li>
<a href="/index/user/profile"><i
class="layui-icon layui-icon-user"></i><span>个人中心</span></a>
</li>
<li>
<a href="/index/user/settings"><i
class="layui-icon layui-icon-set"></i><span>账号管理</span></a>
</li>
<li>
<a href="javascript:;" class="logout-btn"><i
class="layui-icon layui-icon-logout"></i><span>退出登录</span></a>
</li>
</ul>
</div>
</div>
<?php else: ?>
<div class="layui-inline">
<a href="/index/user/login" class="layui-btn layui-btn-normal">登录</a>
<a href="/index/user/register" class="layui-btn layui-btn-primary">注册</a>
</div>
<?php endif; ?>
</div>
</div>
</div>
</div>
</div>
<!-- 固定导航 -->
<div class="sticky-nav" style="display: none;">
<div class="container">
<div class="sticky-nav__logo">
<a href="/"><img src="{$config['logo1']}" width="150" alt="Logo"></a>
</div>
<div class="sticky-nav__menu">
<ul>
<li><a href="/">首页</a></li>
<li><a href="/index/articles/index?cateid=1">站点资讯</a></li>
<li><a href="/index/articles/index?cateid=3">技术文章</a></li>
</ul>
</div>
<div class="sticky-nav__search">
<i class="layui-icon layui-icon-search search-icon" id="stickySearchIcon"></i>
</div>
<div class="sticky-nav__right">
<div class="main-menu__right">
<div class="username">
<?php if ($userInfo['is_login']): ?>
<span class="username-text">{$userInfo.name}</span>
<?php endif; ?>
</div>
<div class="layui-inline">
<!-- 根据登录状态显示不同的内容 -->
<?php if ($userInfo['is_login']): ?>
<div class="layui-inline" style="position: relative;margin-left:20px;">
<img src="{$userInfo.avatar}" class="layui-circle"
style="width: 40px; height: 40px; cursor: pointer;" id="userAvatarSticky">
<div class="user-dropdown" id="userDropdownSticky">
<ul>
<li>
<a href="/index/user/profile"><i
class="layui-icon layui-icon-user"></i><span>个人中心</span></a>
</li>
<li>
<a href="/index/user/settings"><i
class="layui-icon layui-icon-set"></i><span>账号管理</span></a>
</li>
<li>
<a href="javascript:;" class="logout-btn"><i
class="layui-icon layui-icon-logout"></i><span>退出登录</span></a>
</li>
</ul>
</div>
</div>
<?php else: ?>
<div class="layui-inline">
<a href="/index/user/login" class="layui-btn layui-btn-normal">登录</a>
<a href="/index/user/register" class="layui-btn layui-btn-primary">注册</a>
</div>
<?php endif; ?>
</div>
</div>
</div>
</div>
</div>
+6
View File
@@ -0,0 +1,6 @@
<main class="main-content">
<div class="container">
</div>
</main>
+724
View File
@@ -0,0 +1,724 @@
{include file="component/head" /}
{include file="component/header" /}
<div class="main">
<div class="main-top">
<div class="main-top-main">
<div class="main-title">
<?php echo $game['title']; ?>
</div>
<div class="location">
<div class="container">
<div class="location-item">
<a href="/">首页</a>
<span>></span>
<a href="/index/game/list" id="cateLink"><?php echo $cateName; ?></a>
</div>
</div>
</div>
</div>
</div>
<div class="detail-main">
<div class="detail-top">
<div class="detail-top-card">
<div class="detail-top-card-left">
<div class="article-cover">
<img src="<?php echo $game['icon'] ?: '/static/images/default-game.png'; ?>">
<!-- <img src="https://www.yunzer.cn/storage/uploads/20250523/b75a51fa606fd3a18261a6ea283d35fe.jpg" alt=""> -->
</div>
</div>
<div class="detail-top-card-right">
<div class="detail-top-card-right-top">
<div class="collect-btn">
<button class="btn btn-primary" id="collectBtn" data-game-id="<?php echo $game['id']; ?>">
<i class="fa-solid fa-heart"></i> 收藏
</button>
</div>
<div class="report-btn">
<button class="btn btn-primary" id="reportBtn" style="margin-left: 20px;">
<i class="fa-solid fa-flag"></i> 举报
</button>
</div>
</div>
<div class="detail-top-card-right-middle">
<div class="game-info">
<div class="title">Free</div>
<div class="infos">
<div class="infoitem"><span>更新时间:</span><span
class="infoitem-value"><?php echo date('Y-m-d', $game['create_time']); ?></span>
</div>
<div class="infoitem"><span>所属分类:</span><span
class="infoitem-value"><?php echo $cateName; ?></span></div>
<div class="infoitem"><span>程序编号:</span><span
class="infoitem-value"><?php echo $game['number']; ?></span></div>
<div class="infoitem"><span>查看:</span><span
class="infoitem-value"><?php echo $game['views']; ?></span></div>
<div class="infoitem"><span>下载:</span><span
class="infoitem-value"><?php echo $game['downloads']; ?></span></div>
</div>
</div>
</div>
<div class="detail-top-card-right-bottom">
<div class="game-actions1">
<div style="display: flex;gap: 30px;}">
<button id="downloadBtn" class="btn btn-primary">
<i class="fa-solid fa-download"></i> 立即下载
</button>
<button id="codeBtn" class="codebtn">
<i class="fa-solid fa-download"></i> 分享码:<?php echo $game['code']; ?>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="detail-middle">
<div class="detial-middle-left">
<div class="game-detail">
<div class="game-info">
<div class="game-content">
<div class="game-desc">
<?php echo $game['content']; ?>
</div>
</div>
<div class="game-actions">
<div style="display: flex;gap: 30px;}">
<button id="downloadBtn" class="btn btn-primary">
<i class="fa-solid fa-download"></i> 立即下载
</button>
<button id="codeBtn" class="codebtn">
<i class="fa-solid fa-download"></i> 分享码:<?php echo $game['code']; ?>
</button>
</div>
</div>
</div>
<div class="disclaimers">
<div class="disclaimer-item">
<div class="disclaimer-title">免责声明:</div>
<div class="disclaimer-content">
<?php echo $config['disclaimers'] ?>
</div>
</div>
</div>
<div class="game-navigation">
<div class="prev-game" id="prevGame">
</div>
<div class="next-game" id="nextGame">
</div>
</div>
<!-- 相关游戏 -->
<?php if (!empty($relatedGames)): ?>
<div class="related-games">
<h3>相关游戏</h3>
<div class="game-list">
<?php foreach ($relatedGames as $related): ?>
<div class="game-item"
onclick="window.location.href='/index/game/detail?id=<?php echo $related['id']; ?>'">
<div class="game-cover">
<img src="<?php echo $related['icon'] ?: '/static/images/default-game.png'; ?>"
alt="<?php echo $related['title']; ?>">
</div>
<div class="game-info">
<h4 class="game-title-1"><?php echo $related['title']; ?></h4>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
</div>
</div>
<div class="detial-middle-right">
</div>
</div>
</div>
</div>
<!-- 返回顶部按钮 -->
<div class="go-to-top" id="goToTop">
<i class="layui-icon layui-icon-top"></i>
</div>
{include file="component/footer" /}
<script>
// 页面加载完成后执行
document.addEventListener('DOMContentLoaded', function () {
// 获取游戏ID
const gameId = new URLSearchParams(window.location.search).get('id');
if (!gameId) {
alert('游戏ID不存在');
return;
}
// 获取游戏详情
fetch('/index/game/detail?id=' + gameId, {
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.json())
.then(result => {
if (result.code === 1) {
// 渲染上一篇
const prevGame = document.getElementById('prevGame');
if (result.data.prevGame) {
prevGame.innerHTML = `
<a href="/index/game/detail?id=${result.data.prevGame.id}">
<i class="fa fa-arrow-left"></i> 上一篇:${result.data.prevGame.title}
</a>
`;
} else {
prevGame.innerHTML = '<span class="disabled"><i class="fa fa-arrow-left"></i> 没有上一篇了</span>';
}
// 渲染下一篇
const nextGame = document.getElementById('nextGame');
if (result.data.nextGame) {
nextGame.innerHTML = `
<a href="/index/game/detail?id=${result.data.nextGame.id}">
下一篇:${result.data.nextGame.title} <i class="fa fa-arrow-right"></i>
</a>
`;
} else {
nextGame.innerHTML = '<span class="disabled">没有下一篇了 <i class="fa fa-arrow-right"></i></span>';
}
}
})
.catch(error => {
console.error('获取游戏详情失败:', error);
});
// 更新访问次数
updateGameViews(gameId);
// 下载功能
const downloadBtn = document.getElementById('downloadBtn');
if (downloadBtn) {
downloadBtn.addEventListener('click', function () {
fetch('/index/game/downloadurl?id=' + gameId, {
method: 'GET',
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => {
if (!response.ok) {
throw new Error('网络响应失败');
}
return response.json();
})
.then(data => {
if (data.code === 1) {
const downloadsElement = document.getElementById('gameDownloads');
if (downloadsElement) {
let downloads = parseInt(downloadsElement.textContent);
downloadsElement.textContent = downloads + 1;
}
// 直接使用返回的URL
if (data.data && data.data.url) {
window.open(data.data.url, '_blank');
} else {
alert('下载地址不存在');
}
} else {
alert('下载失败:' + data.msg);
}
})
.catch(error => {
console.error('下载请求失败:', error);
alert('下载请求失败,请稍后重试');
});
});
}
//复制分享码
const codeBtn = document.getElementById('codeBtn');
if (codeBtn) {
codeBtn.addEventListener('click', function () {
const code = '<?php echo $game['code']; ?>';
if (code) {
// 创建一个临时输入框
const tempInput = document.createElement('input');
tempInput.value = code;
document.body.appendChild(tempInput);
tempInput.select();
try {
// 尝试使用传统的复制方法
document.execCommand('copy');
layer.msg('分享码已复制到剪贴板');
} catch (err) {
console.error('复制失败:', err);
layer.msg('复制失败,请手动复制');
} finally {
// 移除临时输入框
document.body.removeChild(tempInput);
}
} else {
layer.msg('分享码不存在');
}
});
}
// 返回顶部功能
const goToTop = document.getElementById('goToTop');
// 监听滚动事件
window.addEventListener('scroll', function () {
if (window.pageYOffset > 300) {
goToTop.classList.add('show');
} else {
goToTop.classList.remove('show');
}
});
// 点击返回顶部
goToTop.addEventListener('click', function () {
window.scrollTo({
top: 0,
behavior: 'smooth'
});
});
});
// 更新游戏访问次数
function updateGameViews(gameId) {
fetch('/index/game/updateViews', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-Requested-With': 'XMLHttpRequest'
},
body: 'id=' + gameId
})
.then(response => response.json())
.then(result => {
if (result.code === 1) {
const viewsElement = document.querySelector('.game-views');
if (viewsElement) {
viewsElement.innerHTML = `<i class="fa-solid fa-eye"></i> ${result.data.views}`;
}
}
})
.catch(error => {
console.error('更新访问次数失败:', error);
});
}
</script>
<style>
.main-top {
width: 100%;
height: 400px;
background-color: #0081ff;
/* background: url('/static/images/top-bg.jpg') no-repeat center center; */
position: relative;
}
.main-top-card {
max-width: 1400px;
margin: 0 auto;
margin-top: 30px;
border-radius: 8px;
background-color: #fff;
height: 300px;
}
.main-top-main {
max-width: 1400px;
margin: 0 auto;
padding-top: 50px;
display: flex;
justify-content: space-between;
position: absolute;
top: 0;
left: 50%;
transform: translateX(-50%);
width: 100%;
z-index: 1;
}
.main-top-main .main-title {
font-size: 30px;
font-weight: 700;
max-width: 1000px;
color: #fff;
}
.detail-top {
max-width: 1400px;
/* height: 300px; */
margin: 30px auto;
position: relative;
background-color: #fff;
border-radius: 8px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
display: flex;
}
.detail-top-card {
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px;
}
.detail-top-card img {
width: 400px;
height: auto;
border-radius: 8px;
overflow: hidden;
}
.detail-top-card-right {
/* height: 230px; */
display: flex;
width: 100%;
margin-left: 20px;
flex-direction: column;
}
.detail-top-card-right-top {
display: flex;
justify-content: flex-end;
align-items: center;
}
.detail-top-card-right-middle {}
.detail-top-card-right-middle .game-info {
display: flex;
flex-direction: column;
justify-content: space-between;
height: 100%;
}
.detail-top-card-right-middle .game-info .title {
font-size: 40px;
font-weight: 700;
color: #42d697;
margin-bottom: 15px;
}
.detail-top-card-right-middle .game-info .infos {
display: flex;
}
.detail-top-card-right-middle .game-info .infos .infoitem {
margin-right: 60px;
}
.detail-top-card-right-middle .game-info .infos .infoitem span {
color: #7d879c;
}
.detail-middle {
max-width: 1400px;
margin: 0 auto;
}
.detail-main {
position: relative;
top: -200px;
left: 0;
width: 100%;
z-index: 2;
}
.location {
color: #fff;
display: flex;
align-items: center;
}
.location a {
color: #fff !important;
}
.game-detail {
padding: 50px;
background: #fff;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
border-radius: 8px;
}
.game-header {
margin-bottom: 30px;
border-bottom: 1px solid #eee;
padding-bottom: 20px;
}
.game-title {
font-size: 30px;
font-weight: 700;
color: #333;
margin-bottom: 15px;
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.game-title-1 {
font-size: 16px;
font-weight: 700;
color: #333;
margin-bottom: 15px;
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.game-meta {
display: flex;
flex-wrap: wrap;
gap: 20px;
color: #666;
font-size: 14px;
}
.game-meta span {
display: flex;
align-items: center;
}
.game-meta i {
margin-right: 5px;
}
.game-content {
line-height: 1.8;
color: #333;
font-size: 16px;
margin-bottom: 30px;
}
.game-cover {
margin-bottom: 20px;
}
.game-cover img {
width: 100%;
height: 300px;
object-fit: cover;
border-radius: 8px;
}
.game-desc {
margin-bottom: 30px;
}
.game-actions {
display: flex;
justify-content: center;
gap: 40px;
margin: 30px 0;
padding: 20px 0;
border-top: 1px solid #eee;
border-bottom: 1px solid #eee;
}
.game-actions1 {
display: flex;
margin: 20px 0;
}
.game-navigation {
display: flex;
justify-content: space-between;
margin: 30px 0;
}
.prev-game,
.next-game {
max-width: 45%;
}
.prev-game a,
.next-game a {
color: #333 !important;
text-decoration: none;
}
.prev-game a:hover,
.next-game a:hover {
color: #f57005 !important;
transition: all 0.3s ease;
}
.btn {
/* background: #f57005; */
color: #fff;
padding: 5px 15px;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
}
.btn:hover {
/* background: #e66600; */
transform: translateY(-2px);
}
.codebtn {
color: #0d6efd;
padding: 15px 30px;
border-radius: 8px;
border: 1px solid #0d6efd;
cursor: pointer;
transition: all 0.3s ease;
background-color: #fff;
}
.codebtn:hover {
transform: translateY(-2px);
}
.related-games {
margin: 40px 0;
}
.related-games h3 {
font-size: 20px;
font-weight: 600;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 1px solid #eee;
}
.related-title {
font-size: 20px;
font-weight: 600;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 1px solid #eee;
}
.game-list {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
}
.game-item {
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: transform 0.3s;
}
.game-item:hover {
transform: translateY(-5px);
}
.game-item a {
text-decoration: none;
color: inherit;
}
.game-cover img {
width: 100%;
height: 150px;
object-fit: cover;
}
.game-info {
padding: 10px;
}
.go-to-top {
position: fixed;
right: 30px;
bottom: 30px;
width: 40px;
height: 40px;
background: #f57005;
color: #fff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
opacity: 0;
visibility: hidden;
transition: all 0.3s ease;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
z-index: 1000;
}
.go-to-top.show {
opacity: 1;
visibility: visible;
}
.go-to-top:hover {
background: #e66600;
transform: translateY(-3px);
}
@media (max-width: 768px) {
.game-title {
font-size: 24px;
}
.game-list {
grid-template-columns: repeat(1, 1fr);
}
.game-meta {
gap: 10px;
}
.go-to-top {
right: 20px;
bottom: 20px;
width: 36px;
height: 36px;
}
}
.disclaimers {
color: #b1b1b1;
width: 80%;
margin: 20px auto;
margin-bottom: 60px;
}
.disclaimer-title {
font-size: 16px;
font-weight: 600;
margin-bottom: 10px;
}
.disclaimer-content {
font-size: 14px;
line-height: 1.6;
}
.disclaimer-content p {
margin-bottom: 0;
}
.infoitem-value {
border: 1px #0d6efd dashed;
padding: 3px 6px;
font-size: 13px;
background-color: #0081ff12;
border-radius: 5px;
}
</style>
{include file="component/foot" /}
+539
View File
@@ -0,0 +1,539 @@
{include file="component/head" /}
{include file="component/header" /}
<!-- 简约现代文章中心 -->
<div class="modern-games-page">
<!-- 简约标题区 -->
<div class="modern-header">
<div class="container">
<h1 class="modern-title">游戏中心</h1>
<p class="modern-subtitle">发现精彩游戏世界</p>
</div>
</div>
<!-- 主要内容区 -->
<div class="container">
<div class="modern-layout">
<!-- 侧边分类导航 -->
<aside class="modern-sidebar">
<div class="sidebar-card">
<h3 class="sidebar-title">
<i class="layui-icon layui-icon-list"></i>
<span>分类导航</span>
</h3>
<ul class="category-menu">
{volist name="cate.subCategories" id="subCategory"}
<li class="menu-item {$cate.id == $subCategory.id ? 'active' : ''}" data-cateid="{$subCategory.id}">
<span>{$subCategory.name}</span>
<i class="layui-icon layui-icon-right"></i>
</li>
{/volist}
</ul>
</div>
</aside>
<!-- 文章内容区 -->
<main class="modern-main">
<!-- 文章列表 -->
<div class="game-grid" id="gameList">
{volist name="cate.subCategories" id="subCategory"}
{if $cate.id == $subCategory.id}
{if !empty($subCategory.list)}
{volist name="subCategory.list" id="game"}
<game class="game-card">
<div class="card-image">
<img src="{$game.icon|default=$subCategory.icon|default='/static/images/default-game.jpg'}" alt="{$game.title}">
<div class="image-overlay"></div>
</div>
<div class="card-content">
<div class="meta-info">
<span class="category-tag">{$subCategory.name}</span>
<time class="publish-date">{$game.create_time|date="Y-m-d"}</time>
</div>
<h3 class="game-title">{$game.title}</h3>
<div class="card-footer">
<div class="stats">
<span class="views"><i class="layui-icon layui-icon-eye"></i> {$game.views|default=0}</span>
<span class="likes"><i class="layui-icon layui-icon-praise"></i> {$game.likes|default=0}</span>
</div>
<a href="/index/game/detail?id={$game.id}" class="read-more">阅读更多</a>
</div>
</div>
</game>
{/volist}
{else}
<div class="empty-state">
<div class="empty-icon">
<i class="layui-icon layui-icon-template-1"></i>
</div>
<h4>暂无文章</h4>
<p>当前分类下没有找到相关文章</p>
</div>
{/if}
{/if}
{/volist}
</div>
<!-- 分页 -->
<div class="modern-pagination" id="pagination"></div>
</main>
</div>
</div>
</div>
<script>
layui.use(['laypage', 'jquery'], function(){
var laypage = layui.laypage;
var $ = layui.jquery;
// 分类切换
$('.menu-item').on('click', function() {
var cateid = $(this).data('cateid');
var $menuItems = $('.menu-item');
// 更新选中状态
$menuItems.removeClass('active');
$(this).addClass('active');
// 加载文章
loadArticles(cateid, 1);
});
// 页面加载完成后,自动触发第一个分类的点击事件
$(document).ready(function() {
var $firstMenuItem = $('.menu-item').first();
if ($firstMenuItem.length > 0) {
$firstMenuItem.click();
}
});
// 加载文章函数
function loadArticles(cateid, page) {
$.ajax({
url: '/index/game/list',
type: 'POST',
data: {
cate: cateid,
page: page
},
beforeSend: function() {
$('#gameList').html('<div class="loading-state"><i class="layui-icon layui-icon-loading"></i>加载中...</div>');
},
success: function(res) {
if(res.code === 1) {
var html = '';
if(res.data.games && res.data.games.length > 0) {
res.data.games.forEach(function(game) {
html += `<game class="game-card">
<div class="card-image">
<img src="${game.icon || game.category_icon || '/static/images/default-game.jpg'}" alt="${game.title}">
<div class="image-overlay"></div>
</div>
<div class="card-content">
<div class="meta-info">
<span class="category-tag">${game.category_name || '未分类'}</span>
<time class="publish-date">${game.create_time || ''}</time>
</div>
<h3 class="game-title">${game.title}</h3>
<div class="card-footer">
<div class="stats">
<span class="views"><i class="layui-icon layui-icon-eye"></i> ${game.views || 0}</span>
<span class="likes"><i class="layui-icon layui-icon-praise"></i> ${game.likes || 0}</span>
</div>
<a href="/index/game/detail?id=${game.id}" class="read-more">阅读更多</a>
</div>
</div>
</game>`;
});
} else {
html = `<div class="empty-state">
<div class="empty-icon">
<i class="layui-icon layui-icon-template-1"></i>
</div>
<h4>暂无文章</h4>
<p>当前分类下没有找到相关文章</p>
</div>`;
}
$('#gameList').html(html);
// 渲染分页
laypage.render({
elem: 'pagination',
count: res.data.total || 0,
limit: res.data.per_page || 12,
curr: res.data.current_page || 1,
theme: '#1E9FFF',
layout: ['prev', 'page', 'next'],
jump: function(obj, first) {
if(!first) {
loadArticles(cateid, obj.curr);
}
}
});
}
}
});
}
});
</script>
{include file="component/footer" /}
<style>
/* 基础样式重置 */
.modern-games-page {
font-family: 'Helvetica Neue', Arial, 'PingFang SC', 'Microsoft YaHei', sans-serif;
color: #333;
line-height: 1.6;
background-color: #f9fafc;
padding-bottom: 60px;
}
/* 标题区样式 */
.modern-header {
background: linear-gradient(135deg, #1E9FFF 0%, #0d8aff 100%);
color: white;
padding: 80px 0 60px;
text-align: center;
margin-bottom: 40px;
}
.modern-title {
font-size: 2.5rem;
font-weight: 300;
margin-bottom: 15px;
letter-spacing: 1px;
}
.modern-subtitle {
font-size: 1.1rem;
font-weight: 300;
opacity: 0.9;
margin: 0;
}
/* 布局结构 */
.modern-layout {
display: grid;
grid-template-columns: 260px 1fr;
gap: 30px;
}
/* 侧边栏样式 */
.modern-sidebar {
position: sticky;
top: 30px;
align-self: start;
}
.sidebar-card {
background: white;
border-radius: 8px;
box-shadow: 0 2px 15px rgba(0, 0, 0, 0.03);
overflow: hidden;
}
.sidebar-title {
font-size: 1.1rem;
font-weight: 500;
padding: 20px;
margin: 0;
display: flex;
align-items: center;
color: #555;
border-bottom: 1px solid #f0f0f0;
}
.sidebar-title i {
margin-right: 10px;
font-size: 1.2rem;
color: #1E9FFF;
}
.category-menu {
list-style: none;
padding: 0;
margin: 0;
}
.menu-item {
padding: 15px 20px;
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
transition: all 0.2s ease;
border-left: 3px solid transparent;
}
.menu-item:hover {
background-color: #f8fafd;
color: #1E9FFF;
}
.menu-item.active {
background-color: #f0f7ff;
border-left-color: #1E9FFF;
color: #1E9FFF;
font-weight: 500;
}
.menu-item i {
font-size: 0.9rem;
color: #aaa;
}
.menu-item.active i,
.menu-item:hover i {
color: #1E9FFF;
}
/* 主内容区样式 */
.modern-main {
background: transparent;
}
/* 文章网格布局 */
.game-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20px;
margin-bottom: 40px;
}
/* 文章卡片样式 */
.game-card {
background: white;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 3px 15px rgba(0, 0, 0, 0.03);
transition: all 0.3s ease;
display: flex;
flex-direction: column;
}
.game-card:hover {
transform: translateY(-5px);
box-shadow: 0 5px 25px rgba(0, 0, 0, 0.08);
}
.card-image {
height: 180px;
position: relative;
overflow: hidden;
}
.card-image img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.5s ease;
}
.image-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(to top, rgba(0, 0, 0, 0.3), transparent);
}
.game-card:hover .card-image img {
transform: scale(1.05);
}
.card-content {
padding: 20px;
flex: 1;
display: flex;
flex-direction: column;
}
.meta-info {
display: flex;
justify-content: space-between;
margin-bottom: 12px;
font-size: 0.85rem;
color: #666;
}
.category-tag {
background: #f0f7ff;
color: #1E9FFF;
padding: 3px 10px;
border-radius: 4px;
font-size: 0.75rem;
}
.game-title {
font-size: 1rem;
font-weight: 500;
margin: 0 0 10px;
color: #333;
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
.game-excerpt {
font-size: 0.9rem;
color: #666;
margin: 0 0 20px;
flex: 1;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
.card-footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: auto;
}
.stats {
font-size: 0.85rem;
color: #999;
display: flex;
gap: 15px;
}
.stats i {
margin-right: 3px;
}
.read-more {
color: #1E9FFF;
font-size: 0.9rem;
text-decoration: none;
font-weight: 500;
transition: all 0.2s ease;
display: inline-flex;
align-items: center;
}
.read-more:hover {
color: #0d8aff;
text-decoration: underline;
}
/* 分页样式 */
.modern-pagination {
text-align: center;
margin-top: 40px;
}
.layui-laypage a,
.layui-laypage span {
border-radius: 4px !important;
margin: 0 3px !important;
}
.layui-laypage a {
color: #666 !important;
}
.layui-laypage .layui-laypage-curr .layui-laypage-em {
background-color: #1E9FFF !important;
}
/* 空状态样式 */
.empty-state {
grid-column: 1 / -1;
text-align: center;
padding: 60px 20px;
background: white;
border-radius: 8px;
box-shadow: 0 3px 15px rgba(0, 0, 0, 0.03);
}
.empty-icon {
font-size: 3rem;
color: #ddd;
margin-bottom: 20px;
}
.empty-icon i {
font-size: inherit;
}
.empty-state h4 {
font-size: 1.2rem;
font-weight: 400;
color: #666;
margin: 0 0 10px;
}
.empty-state p {
color: #999;
font-size: 0.95rem;
margin: 0;
}
/* 加载状态 */
.loading-state {
grid-column: 1 / -1;
text-align: center;
padding: 40px;
color: #666;
}
.loading-state i {
font-size: 1.5rem;
margin-right: 10px;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* 响应式设计 */
@media (max-width: 992px) {
.modern-layout {
grid-template-columns: 1fr;
}
.modern-sidebar {
position: static;
margin-bottom: 30px;
}
.game-grid {
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
}
}
@media (max-width: 768px) {
.modern-header {
padding: 60px 0 40px;
}
.modern-title {
font-size: 2rem;
}
}
@media (max-width: 576px) {
.game-grid {
grid-template-columns: 1fr;
}
.modern-title {
font-size: 1.8rem;
}
.modern-subtitle {
font-size: 1rem;
}
}
</style>
{include file="component/foot" /}
+76
View File
@@ -0,0 +1,76 @@
<div class="container py-5">
<div class="row g-4">
<!-- 左侧分类列表 -->
<div class="col-lg-3">
<div class="category-sidebar">
<div class="sidebar-header">
<i class="layui-icon layui-icon-app"></i>
<span>文章分类</span>
</div>
<div class="category-list">
{volist name="categories" id="cate"}
<div class="category-item {$category.id == $cate.id ? 'active' : ''}" data-cateid="{$cate.id}">{$cate.name}</div>
{/volist}
</div>
</div>
</div>
<!-- 右侧文章列表 -->
<div class="col-lg-9">
{if $category}
<div class="category-header mb-4">
<h2 class="category-title">{$category.name}</h2>
<p class="category-desc">{$category.desc|default=''}</p>
</div>
{/if}
<div class="article-list">
{volist name="games" id="article"}
<div class="article-item">
<div class="row g-0">
<div class="col-md-4">
<div class="article-image">
<img src="{$article.image|default='/static/images/default.jpg'}" alt="{$article.title}">
</div>
</div>
<div class="col-md-8">
<div class="article-content">
<h3 class="article-title">
<a href="/index/game/detail?id={$article.id}">{$article.title}</a>
</h3>
<p class="article-desc">{$article.desc|default=''}</p>
<div class="article-meta">
<div class="article-stats">
<span><i class="layui-icon layui-icon-eye"></i> {$article.views|default=0}</span>
<span><i class="layui-icon layui-icon-praise"></i> {$article.likes|default=0}</span>
<span><i class="layui-icon layui-icon-date"></i> {$article.create_time|date="Y-m-d"}</span>
</div>
<a href="/index/game/detail?id={$article.id}" class="btn-detail">查看详情</a>
</div>
</div>
</div>
</div>
</div>
{/volist}
</div>
<!-- 分页 -->
<div class="mt-5">
{$games|raw}
</div>
</div>
</div>
</div>
<script>
layui.use(['layer'], function () {
var layer = layui.layer;
var $ = layui.$;
// 分类切换
$('.category-item').on('click', function() {
var cateid = $(this).data('cateid');
window.location.href = '/index/game/list?cate=' + cateid;
});
});
</script>
+5
View File
@@ -0,0 +1,5 @@
{include file="component/head" /}
{include file="component/header" /}
{include file="component/main" /}
{include file="component/footer" /}
{include file="component/foot" /}
File diff suppressed because it is too large Load Diff
+535
View File
@@ -0,0 +1,535 @@
{include file="component/head" /}
{include file="component/header" /}
<!-- 简约现代文章中心 -->
<div class="modern-programs-page">
<!-- 简约标题区 -->
<div class="modern-header">
<div class="container">
<h1 class="modern-title">资源中心</h1>
<p class="modern-subtitle">发现优质程序与工具</p>
</div>
</div>
<!-- 主要内容区 -->
<div class="container">
<div class="modern-layout">
<!-- 侧边分类导航 -->
<aside class="modern-sidebar">
<div class="sidebar-card">
<h3 class="sidebar-title">
<i class="layui-icon layui-icon-list"></i>
<span>分类导航</span>
</h3>
<ul class="category-menu">
{volist name="cate.subCategories" id="subCategory"}
<li class="menu-item {$cate.id == $subCategory.id ? 'active' : ''}" data-cateid="{$subCategory.id}">
<span>{$subCategory.name}</span>
<i class="layui-icon layui-icon-right"></i>
</li>
{/volist}
</ul>
</div>
</aside>
<!-- 文章内容区 -->
<main class="modern-main">
<!-- 文章列表 -->
<div class="article-grid" id="programList">
{volist name="cate.subCategories" id="subCategory"}
{if $cate.id == $subCategory.id}
{if !empty($subCategory.list)}
{volist name="subCategory.list" id="program"}
<program class="program-card">
<div class="card-image">
<img src="{$program.icon|default='/static/images/default-program.jpg'}" alt="{$program.title}">
<div class="image-overlay"></div>
</div>
<div class="card-content">
<div class="meta-info">
<span class="category-tag">{$subCategory.name}</span>
<time class="publish-date">{$program.create_time|date="Y-m-d"}</time>
</div>
<h3 class="program-title">{$program.title}</h3>
<div class="card-footer">
<div class="stats">
<span class="views"><i class="layui-icon layui-icon-eye"></i> {$program.views|default=0}</span>
<span class="likes"><i class="layui-icon layui-icon-praise"></i> {$program.likes|default=0}</span>
</div>
<a href="/index/program/detail?id={$program.id}" class="read-more">阅读更多</a>
</div>
</div>
</program>
{/volist}
{else}
<div class="empty-state">
<div class="empty-icon">
<i class="layui-icon layui-icon-template-1"></i>
</div>
<h4>暂无文章</h4>
<p>当前分类下没有找到相关文章</p>
</div>
{/if}
{/if}
{/volist}
</div>
<!-- 分页 -->
<div class="modern-pagination" id="pagination"></div>
</main>
</div>
</div>
</div>
<script>
layui.use(['laypage', 'jquery'], function(){
var laypage = layui.laypage;
var $ = layui.jquery;
// 分类切换
$('.menu-item').on('click', function() {
var cateid = $(this).data('cateid');
var $menuItems = $('.menu-item');
// 更新选中状态
$menuItems.removeClass('active');
$(this).addClass('active');
// 加载文章
loadArticles(cateid, 1);
});
// 页面加载完成后,自动触发第一个分类的点击事件
$(document).ready(function() {
var $firstMenuItem = $('.menu-item').first();
if ($firstMenuItem.length > 0) {
$firstMenuItem.click();
}
});
// 加载文章函数
function loadArticles(cateid, page) {
$.ajax({
url: '/index/program/list',
type: 'POST',
data: {
cate: cateid,
page: page
},
beforeSend: function() {
$('#programList').html('<div class="loading-state"><i class="layui-icon layui-icon-loading"></i>加载中...</div>');
},
success: function(res) {
if(res.code === 1) {
var html = '';
if(res.data.programs && res.data.programs.length > 0) {
res.data.programs.forEach(function(program) {
html += `<program class="program-card">
<div class="card-image">
<img src="${program.icon || '/static/images/default-program.jpg'}" alt="${program.title}">
<div class="image-overlay"></div>
</div>
<div class="card-content">
<div class="meta-info">
<span class="category-tag">${program.category_name || '未分类'}</span>
<time class="publish-date">${program.create_time || ''}</time>
</div>
<h3 class="program-title">${program.title}</h3>
<div class="card-footer">
<div class="stats">
<span class="views"><i class="layui-icon layui-icon-eye"></i> ${program.views || 0}</span>
<span class="likes"><i class="layui-icon layui-icon-praise"></i> ${program.likes || 0}</span>
</div>
<a href="/index/program/detail?id=${program.id}" class="read-more">阅读更多</a>
</div>
</div>
</program>`;
});
} else {
html = `<div class="empty-state">
<div class="empty-icon">
<i class="layui-icon layui-icon-template-1"></i>
</div>
<h4>暂无文章</h4>
<p>当前分类下没有找到相关文章</p>
</div>`;
}
$('#programList').html(html);
// 渲染分页
laypage.render({
elem: 'pagination',
count: res.data.total || 0,
limit: res.data.per_page || 12,
curr: res.data.current_page || 1,
theme: '#1E9FFF',
layout: ['prev', 'page', 'next'],
jump: function(obj, first) {
if(!first) {
loadArticles(cateid, obj.curr);
}
}
});
}
}
});
}
});
</script>
{include file="component/footer" /}
<style>
/* 基础样式重置 */
.modern-programs-page {
font-family: 'Helvetica Neue', Arial, 'PingFang SC', 'Microsoft YaHei', sans-serif;
color: #333;
line-height: 1.6;
background-color: #f9fafc;
padding-bottom: 60px;
}
/* 标题区样式 */
.modern-header {
background: linear-gradient(135deg, #1E9FFF 0%, #0d8aff 100%);
color: white;
padding: 80px 0 60px;
text-align: center;
margin-bottom: 40px;
}
.modern-title {
font-size: 2.5rem;
font-weight: 300;
margin-bottom: 15px;
letter-spacing: 1px;
}
.modern-subtitle {
font-size: 1.1rem;
font-weight: 300;
opacity: 0.9;
margin: 0;
}
/* 布局结构 */
.modern-layout {
display: grid;
grid-template-columns: 260px 1fr;
gap: 30px;
}
/* 侧边栏样式 */
.modern-sidebar {
position: sticky;
top: 30px;
align-self: start;
}
.sidebar-card {
background: white;
border-radius: 8px;
box-shadow: 0 2px 15px rgba(0, 0, 0, 0.03);
overflow: hidden;
}
.sidebar-title {
font-size: 1.1rem;
font-weight: 500;
padding: 20px;
margin: 0;
display: flex;
align-items: center;
color: #555;
border-bottom: 1px solid #f0f0f0;
}
.sidebar-title i {
margin-right: 10px;
font-size: 1.2rem;
color: #1E9FFF;
}
.category-menu {
list-style: none;
padding: 0;
margin: 0;
}
.menu-item {
padding: 15px 20px;
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
transition: all 0.2s ease;
border-left: 3px solid transparent;
}
.menu-item:hover {
background-color: #f8fafd;
color: #1E9FFF;
}
.menu-item.active {
background-color: #f0f7ff;
border-left-color: #1E9FFF;
color: #1E9FFF;
font-weight: 500;
}
.menu-item i {
font-size: 0.9rem;
color: #aaa;
}
.menu-item.active i,
.menu-item:hover i {
color: #1E9FFF;
}
/* 主内容区样式 */
.modern-main {
background: transparent;
}
/* 文章网格布局 */
.article-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20px;
margin-bottom: 40px;
}
/* 文章卡片样式 */
.article-card {
background: white;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 3px 15px rgba(0, 0, 0, 0.03);
transition: all 0.3s ease;
display: flex;
flex-direction: column;
}
.article-card:hover {
transform: translateY(-5px);
box-shadow: 0 5px 25px rgba(0, 0, 0, 0.08);
}
.card-image {
height: 180px;
position: relative;
overflow: hidden;
}
.card-image img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.5s ease;
}
.image-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(to top, rgba(0, 0, 0, 0.3), transparent);
}
.article-card:hover .card-image img {
transform: scale(1.05);
}
.card-content {
padding: 20px;
flex: 1;
display: flex;
flex-direction: column;
}
.meta-info {
display: flex;
justify-content: space-between;
margin-bottom: 12px;
font-size: 0.85rem;
color: #666;
}
.category-tag {
background: #f0f7ff;
color: #1E9FFF;
padding: 3px 10px;
border-radius: 4px;
font-size: 0.75rem;
}
.program-title {
font-size: 1rem;
font-weight: 500;
margin: 0 0 10px;
color: #333;
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
.program-excerpt {
font-size: 0.9rem;
color: #666;
margin: 0 0 20px;
flex: 1;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
.card-footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: auto;
}
.stats {
font-size: 0.85rem;
color: #999;
display: flex;
gap: 15px;
}
.stats i {
margin-right: 3px;
}
.read-more {
color: #1E9FFF;
font-size: 0.9rem;
text-decoration: none;
font-weight: 500;
transition: all 0.2s ease;
display: inline-flex;
align-items: center;
}
.read-more:hover {
color: #0d8aff;
text-decoration: underline;
}
/* 分页样式 */
.modern-pagination {
text-align: center;
margin-top: 40px;
}
.layui-laypage a,
.layui-laypage span {
border-radius: 4px !important;
margin: 0 3px !important;
}
.layui-laypage a {
color: #666 !important;
}
.layui-laypage .layui-laypage-curr .layui-laypage-em {
background-color: #1E9FFF !important;
}
/* 空状态样式 */
.empty-state {
grid-column: 1 / -1;
text-align: center;
padding: 60px 20px;
background: white;
border-radius: 8px;
box-shadow: 0 3px 15px rgba(0, 0, 0, 0.03);
}
.empty-icon {
font-size: 3rem;
color: #ddd;
margin-bottom: 20px;
}
.empty-icon i {
font-size: inherit;
}
.empty-state h4 {
font-size: 1.2rem;
font-weight: 400;
color: #666;
margin: 0 0 10px;
}
.empty-state p {
color: #999;
font-size: 0.95rem;
margin: 0;
}
/* 加载状态 */
.loading-state {
grid-column: 1 / -1;
text-align: center;
padding: 40px;
color: #666;
}
.loading-state i {
font-size: 1.5rem;
margin-right: 10px;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* 响应式设计 */
@media (max-width: 1200px) {
.article-grid {
grid-template-columns: repeat(3, 1fr);
}
}
@media (max-width: 992px) {
.modern-layout {
grid-template-columns: 1fr;
}
.modern-sidebar {
position: static;
margin-bottom: 30px;
}
.article-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 576px) {
.article-grid {
grid-template-columns: 1fr;
}
.modern-title {
font-size: 1.8rem;
}
.modern-subtitle {
font-size: 1rem;
}
}
</style>
{include file="component/foot" /}
+261
View File
@@ -0,0 +1,261 @@
<div class="container py-5">
<div class="row g-4">
<!-- 左侧分类列表 -->
<div class="col-lg-3">
<div class="category-sidebar">
<div class="sidebar-header">
<i class="layui-icon layui-icon-app"></i>
<span>程序分类</span>
</div>
<div class="category-list">
{volist name="categories" id="cate"}
<div class="category-item {$category.id == $cate.id ? 'active' : ''}" data-cateid="{$cate.id}">{$cate.name}</div>
{/volist}
</div>
</div>
</div>
<!-- 右侧程序列表 -->
<div class="col-lg-9">
{if $category}
<div class="category-header mb-4">
<h2 class="category-title">{$category.name}</h2>
<p class="category-desc">{$category.desc|default=''}</p>
</div>
{/if}
<div class="program-list">
{if empty($programs)}
<div class="empty-state">
<i class="layui-icon layui-icon-face-surprised"></i>
<p>暂无程序</p>
</div>
{else}
{volist name="programs" id="program"}
<div class="program-item">
<div class="row g-0">
<div class="col-md-4">
<div class="program-image">
<img src="{$program.icon}" alt="{$program.title}">
</div>
</div>
<div class="col-md-8">
<div class="program-content">
<h3 class="program-title">
<a href="/index/program/detail?id={$program.id}">{$program.title}</a>
</h3>
<p class="program-desc">{$program.desc|default=''}</p>
<div class="program-meta">
<div class="program-stats">
<span><i class="layui-icon layui-icon-eye"></i> {$program.views|default=0}</span>
<span><i class="layui-icon layui-icon-download-circle"></i> {$program.downloads|default=0}</span>
<span><i class="layui-icon layui-icon-date"></i> {$program.create_time|date="Y-m-d"}</span>
</div>
<a href="/index/program/detail?id={$program.id}" class="btn-detail">查看详情</a>
</div>
</div>
</div>
</div>
</div>
{/volist}
{/if}
</div>
<!-- 分页 -->
{if !empty($programs)}
<div class="mt-5">
{$programs->render()|raw}
</div>
{/if}
</div>
</div>
</div>
<style>
.program-list {
display: flex;
flex-direction: column;
gap: 20px;
}
.program-item {
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
overflow: hidden;
transition: all 0.3s ease;
}
.program-item:hover {
transform: translateY(-5px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
}
.program-image {
height: 200px;
overflow: hidden;
}
.program-image img {
width: 100%;
height: 100%;
object-fit: cover;
}
.program-content {
padding: 20px;
}
.program-title {
font-size: 1.5rem;
margin-bottom: 10px;
}
.program-title a {
color: #333;
text-decoration: none;
}
.program-title a:hover {
color: #1E9FFF;
}
.program-desc {
color: #666;
margin-bottom: 15px;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.program-meta {
display: flex;
justify-content: space-between;
align-items: center;
color: #999;
}
.program-stats {
display: flex;
gap: 15px;
}
.program-stats span {
display: flex;
align-items: center;
gap: 5px;
}
.btn-detail {
padding: 6px 15px;
background: #1E9FFF;
color: #fff;
border-radius: 4px;
text-decoration: none;
transition: all 0.3s ease;
}
.btn-detail:hover {
background: #0d8aff;
color: #fff;
}
.category-sidebar {
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
padding: 20px;
}
.sidebar-header {
font-size: 1.2rem;
font-weight: bold;
margin-bottom: 15px;
display: flex;
align-items: center;
gap: 10px;
}
.category-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.category-item {
padding: 10px 15px;
border-radius: 4px;
cursor: pointer;
transition: all 0.3s ease;
}
.category-item:hover {
background: #f5f5f5;
}
.category-item.active {
background: #1E9FFF;
color: #fff;
}
.category-header {
background: #fff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
}
.category-title {
margin: 0;
color: #333;
}
.category-desc {
margin: 10px 0 0;
color: #666;
}
.empty-state {
text-align: center;
padding: 40px;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
}
.empty-state i {
font-size: 48px;
color: #999;
margin-bottom: 15px;
}
.empty-state p {
color: #666;
font-size: 16px;
margin: 0;
}
</style>
<script>
layui.use(['layer'], function () {
var layer = layui.layer;
var $ = layui.$;
// 分类切换
$('.category-item').on('click', function() {
var cateid = $(this).data('cateid');
window.location.href = '/index/program/list?cate=' + cateid;
});
});
</script>
// 页面加载完成后,自动触发第一个分类的点击事件
$(document).ready(function() {
var $firstMenuItem = $('.category-item').first();
if ($firstMenuItem.length > 0) {
$firstMenuItem.click();
}
});
});
</script>
+956
View File
@@ -0,0 +1,956 @@
{include file="component/head" /}
<link href="__STATIC__/css/lightbox.min.css" rel="stylesheet">
<link href="__CSS__/swiper-bundle.min.css" rel="stylesheet">
<script src="__JS__/jquery.min.js"></script>
<script src="__JS__/lightbox.min.js"></script>
<script src="__JS__/swiper-bundle.min.js"></script>
{include file="component/header" /}
<div class="main">
<div class="main-top">
<div class="main-top-main">
<div class="main-title">
<?php echo $resources['title']; ?>
</div>
<div class="location">
<div class="container">
<div class="location-item">
<a href="/">首页</a>
<span>></span>
<a href="/index/resources/list" id="cateLink"><?php echo $cateName; ?></a>
</div>
</div>
</div>
</div>
</div>
<div class="detail-main">
<div class="detail-top">
<div class="detail-top-left">
<div class="detail-top-card">
<div class="detail-top-card-left">
<div class="article-cover">
<div class="swiper resources-swiper">
<div class="swiper-wrapper">
{php}
// 兼容字符串和数组
$images = isset($resources['images']) ? $resources['images'] : [];
if (is_string($images)) {
$images = explode(',', $images);
}
$images = array_filter($images); // 移除空值
if (empty($images) && !empty($resources['icon'])) {
$images = [$resources['icon']];
}
{/php}
{volist name="images" id="image"}
<div class="swiper-slide">
<a href="<?php $img = trim($image, ', ');
echo (strpos($img, 'http') === 0 ? $img : request()->domain() . $img); ?>"
data-lightbox="resources-gallery">
<img src="<?php $img = trim($image, ', ');
echo (strpos($img, 'http') === 0 ? $img : request()->domain() . $img); ?>"
alt="<?php echo $resources['title']; ?>">
</a>
</div>
{/volist}
</div>
<div class="swiper-button-prev"></div>
<div class="swiper-button-next"></div>
<div class="swiper-pagination"></div>
</div>
</div>
</div>
<div class="detail-top-card-right">
<!-- <div class="detail-top-card-right-top">
<div class="collect-btn">
<button class="btn btn-primary" id="collectBtn"
data-resources-id="<?php echo $resources['id']; ?>">
<i class="fa-solid fa-heart"></i> 收藏
</button>
</div>
<div class="report-btn">
<button class="btn btn-primary" id="reportBtn" style="margin-left: 20px;">
<i class="fa-solid fa-flag"></i> 举报
</button>
</div>
</div> -->
<div class="detail-top-card-right-middle">
<div class="resources-info">
<div class="title">Free</div>
<div class="infos">
<div style="display: flex;">
<div class="infoitem">
<span>程序编号:</span>
<span class="infoitem-value"><?php echo $resources['number']; ?></span>
</div>
<div class="infoitem">
<span>所属分类:</span>
<span class="infoitem-value"><?php echo $cateName; ?></span>
</div>
</div>
<div style="display: flex;">
<div class="infoitem">
<span>更新时间:</span>
<span
class="infoitem-value"><?php echo date('Y-m-d', $resources['create_time']); ?></span>
</div>
<div class="infoitem">
<span>查看:</span>
<span class="infoitem-value"><?php echo $resources['views']; ?></span>
<span>次</span>
</div>
<div class="infoitem">
<span>下载:</span>
<span class="infoitem-value"><?php echo $resources['downloads']; ?></span>
<span>次</span>
</div>
</div>
</div>
</div>
</div>
<div class="detail-top-card-right-bottom">
<div class="resources-actions1">
<div style="display: flex;gap: 30px;}">
<button id="downloadBtn" class="btn btn-primary">
<i class="fa-solid fa-download"></i> 立即下载
</button>
<button id="codeBtn" class="codebtn">
<i class="fa-solid fa-download"></i> 分享码:<?php echo $resources['code']; ?>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="detail-top-right">
<div class="aboutauthor-main">
<div class="aboutauthor-main-top">
<div class="aboutauthor-avatar">
<img src="{$authorInfo.avatar}" alt="作者头像">
</div>
<div class="aboutauthor-info">
<div class="author-name">{$authorInfo.name}</div>
</div>
</div>
<div class="aboutauthor-main-middle">
<div class="author-stats">
<div class="author-stats-item">
<h6>资源</h6>
<span class="count">{$authorInfo.resource_count}</span>
</div>
<div class="author-stats-item">
<h6>文章</h6>
<span class="count">{$authorInfo.article_count}</span>
</div>
<div class="author-stats-item">
<h6>粉丝</h6>
<span class="count">
0
</span>
</div>
</div>
</div>
</div>
<div class="aboutauthor-btn">
<button class="follow-btn">
<i class="fa fa-user-plus"></i> 关注他
</button>
<button class="message-btn">
<i class="fa fa-envelope"></i> 发私信
</button>
</div>
</div>
</div>
<div class="detail-middle">
<div class="detial-middle-left">
<div class="resources-detail">
<div class="resources-info">
<div class="resources-content">
<div class="resources-desc">
<?php echo $resources['content']; ?>
</div>
</div>
<div class="resources-actions">
<div style="display: flex;gap: 30px;}">
<button id="downloadBtn" class="btn btn-primary">
<i class="fa-solid fa-download"></i> 立即下载
</button>
<button id="codeBtn" class="codebtn">
<i class="fa-solid fa-download"></i> 分享码:<?php echo $resources['code']; ?>
</button>
</div>
</div>
</div>
<div class="disclaimers">
<div class="disclaimer-item">
<div class="disclaimer-title">免责声明:</div>
<div class="disclaimer-content">
<?php echo $config['disclaimers'] ?>
</div>
</div>
</div>
<div class="resources-navigation">
<div class="prev-resources" id="prevResources">
</div>
<div class="next-resources" id="nextResources">
</div>
</div>
<!-- 相关资源 -->
<?php if (!empty($relatedResourcess)): ?>
<div class="related-resourcess">
<h3>相关资源</h3>
<div class="resources-list">
<?php foreach ($relatedResourcess as $related): ?>
<div class="resources-item"
onclick="window.location.href='/index/resources/detail?id=<?php echo $related['id']; ?>'">
<div class="resources-cover">
<img src="<?php echo $related['icon'] ?: '/static/images/default-resources.png'; ?>"
alt="<?php echo $related['title']; ?>">
</div>
<div class="resources-info">
<h4 class="resources-title-1"><?php echo $related['title']; ?></h4>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
</div>
</div>
<div class="detial-middle-right">
</div>
</div>
</div>
</div>
<!-- 返回顶部按钮 -->
<div class="go-to-top" id="goToTop">
<i class="layui-icon layui-icon-top"></i>
</div>
{include file="component/footer" /}
<script>
// 页面加载完成后执行
document.addEventListener('DOMContentLoaded', function () {
// 获取资源ID
const resourcesId = new URLSearchParams(window.location.search).get('id');
if (!resourcesId) {
alert('资源ID不存在');
return;
}
// 获取资源详情
fetch('/index/resources/detail?id=' + resourcesId, {
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.json())
.then(result => {
if (result.code === 1) {
// 渲染上一篇
const prevResources = document.getElementById('prevResources');
if (result.data.prevResources) {
prevResources.innerHTML = `
<a href="/index/resources/detail?id=${result.data.prevResources.id}">
<i class="fa fa-arrow-left"></i> 上一篇:${result.data.prevResources.title}
</a>
`;
} else {
prevResources.innerHTML = '<span class="disabled"><i class="fa fa-arrow-left"></i> 没有上一篇了</span>';
}
// 渲染下一篇
const nextResources = document.getElementById('nextResources');
if (result.data.nextResources) {
nextResources.innerHTML = `
<a href="/index/resources/detail?id=${result.data.nextResources.id}">
下一篇:${result.data.nextResources.title} <i class="fa fa-arrow-right"></i>
</a>
`;
} else {
nextResources.innerHTML = '<span class="disabled">没有下一篇了 <i class="fa fa-arrow-right"></i></span>';
}
}
})
.catch(error => {
console.error('获取资源详情失败:', error);
});
// 更新访问次数
updateResourcesViews(resourcesId);
// 下载功能
const downloadBtn = document.getElementById('downloadBtn');
if (downloadBtn) {
downloadBtn.addEventListener('click', function () {
fetch('/index/resources/downloadurl?id=' + resourcesId, {
method: 'GET',
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => {
if (!response.ok) {
throw new Error('网络响应失败');
}
return response.json();
})
.then(data => {
if (data.code === 1) {
const downloadsElement = document.getElementById('resourcesDownloads');
if (downloadsElement) {
let downloads = parseInt(downloadsElement.textContent);
downloadsElement.textContent = downloads + 1;
}
// 直接使用返回的URL
if (data.data && data.data.url) {
window.open(data.data.url, '_blank');
} else {
alert('下载地址不存在');
}
} else {
alert('下载失败:' + data.msg);
}
})
.catch(error => {
console.error('下载请求失败:', error);
alert('下载请求失败,请稍后重试');
});
});
}
const swiper = new Swiper('.resources-swiper', {
slidesPerView: 1,
spaceBetween: 30,
loop: true,
autoplay: {
delay: 3000,
disableOnInteraction: false,
},
pagination: {
el: '.swiper-pagination',
clickable: true,
},
navigation: {
nextEl: '.swiper-button-next',
prevEl: '.swiper-button-prev',
},
});
// 初始化 Lightbox
lightbox.option({
'resizeDuration': 200,
'wrapAround': true,
'albumLabel': "图片 %1 / %2",
'fadeDuration': 300,
'imageFadeDuration': 300,
'positionFromTop': 100,
'maxWidth': 1200,
'maxHeight': 800,
'disableScrolling': true,
'showImageNumberLabel': true,
'alwaysShowNavOnTouchDevices': true
});
//复制分享码
const codeBtn = document.getElementById('codeBtn');
if (codeBtn) {
codeBtn.addEventListener('click', function () {
const code = '<?php echo $resources['code']; ?>';
if (code) {
// 创建一个临时输入框
const tempInput = document.createElement('input');
tempInput.value = code;
document.body.appendChild(tempInput);
tempInput.select();
try {
// 尝试使用传统的复制方法
document.execCommand('copy');
layer.msg('分享码已复制到剪贴板');
} catch (err) {
console.error('复制失败:', err);
layer.msg('复制失败,请手动复制');
} finally {
// 移除临时输入框
document.body.removeChild(tempInput);
}
} else {
layer.msg('分享码不存在');
}
});
}
// 返回顶部功能
const goToTop = document.getElementById('goToTop');
if (goToTop) {
window.addEventListener('scroll', function () {
if (window.pageYOffset > 300) {
goToTop.classList.add('show');
} else {
goToTop.classList.remove('show');
}
});
goToTop.addEventListener('click', function () {
window.scrollTo({
top: 0,
behavior: 'smooth'
});
});
}
});
// 更新资源访问次数
function updateResourcesViews(resourcesId) {
fetch('/index/resources/updateViews', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-Requested-With': 'XMLHttpRequest'
},
body: 'id=' + resourcesId
})
.then(response => response.json())
.then(result => {
if (result.code === 1) {
const viewsElement = document.querySelector('.resources-views');
if (viewsElement) {
viewsElement.innerHTML = `<i class="fa-solid fa-eye"></i> ${result.data.views}`;
}
}
})
.catch(error => {
console.error('更新访问次数失败:', error);
});
}
</script>
<style>
.main-top {
width: 100%;
height: 400px;
background-color: #0081ff;
/* background: url('/static/images/top-bg.jpg') no-repeat center center; */
position: relative;
}
.main-top-card {
max-width: 1400px;
margin: 0 auto;
margin-top: 30px;
border-radius: 8px;
background-color: #fff;
height: 300px;
}
.main-top-main {
max-width: 1400px;
margin: 0 auto;
padding-top: 50px;
display: flex;
justify-content: space-between;
position: absolute;
top: 0;
left: 50%;
transform: translateX(-50%);
width: 100%;
z-index: 1;
}
.main-top-main .main-title {
font-size: 30px;
font-weight: 700;
max-width: 1000px;
color: #fff;
}
.detail-top {
max-width: 1400px;
/* height: 300px; */
margin: 30px auto;
position: relative;
background-color: #fff;
border-radius: 8px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
display: flex;
justify-content: space-between;
}
.detail-top-left {
width: 70%;
}
.detail-top-right {
width: 30%;
border-left: 1px solid #eee;
margin: 20px;
}
.detail-top-card {
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px;
}
.detail-top-card img {
width: 350px;
height: 260px;
border-radius: 8px;
object-fit: cover;
overflow: hidden;
}
.detail-top-card-right {
/* height: 230px; */
display: flex;
width: 100%;
margin-left: 20px;
flex-direction: column;
}
.detail-top-card-right-top {
display: flex;
margin-top: 25px;
}
.detail-top-card-right-middle {}
.detail-top-card-right-middle .resources-info {
display: flex;
flex-direction: column;
justify-content: space-between;
height: 100%;
}
.detail-top-card-right-middle .resources-info .title {
font-size: 30px;
font-weight: 700;
color: #42d697;
margin-bottom: 15px;
}
.detail-top-card-right-middle .resources-info .infos {
display: flex;
flex-direction: column;
gap: 20px;
}
.detail-top-card-right-middle .resources-info .infos .infoitem {
margin-right: 40px;
}
.detail-top-card-right-middle .resources-info .infos .infoitem span {
color: #7d879c;
}
.detail-middle {
max-width: 1400px;
margin: 0 auto;
}
.detail-main {
position: relative;
top: -200px;
left: 0;
width: 100%;
z-index: 2;
}
.location {
color: #fff;
display: flex;
align-items: center;
}
.location a {
color: #fff !important;
}
.resources-detail {
padding: 50px;
background: #fff;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
border-radius: 8px;
}
.resources-header {
margin-bottom: 30px;
border-bottom: 1px solid #eee;
padding-bottom: 20px;
}
.resources-title {
font-size: 30px;
font-weight: 700;
color: #333;
margin-bottom: 15px;
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.resources-title-1 {
font-size: 16px;
font-weight: 700;
color: #333;
margin-bottom: 15px;
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.resources-meta {
display: flex;
flex-wrap: wrap;
gap: 20px;
color: #666;
font-size: 14px;
}
.resources-meta span {
display: flex;
align-items: center;
}
.resources-meta i {
margin-right: 5px;
}
.resources-content {
line-height: 1.8;
color: #333;
font-size: 16px;
margin-bottom: 30px;
}
.resources-cover {
margin-bottom: 20px;
}
.resources-cover img {
width: 100%;
height: 300px;
object-fit: cover;
border-radius: 8px;
}
.resources-desc {
margin-bottom: 30px;
}
.resources-actions {
display: flex;
justify-content: center;
gap: 40px;
margin: 30px 0;
padding: 20px 0;
border-top: 1px solid #eee;
border-bottom: 1px solid #eee;
}
.resources-actions1 {
display: flex;
margin: 20px 0;
}
.resources-navigation {
display: flex;
justify-content: space-between;
margin: 30px 0;
}
.prev-resources,
.next-resources {
max-width: 45%;
}
.prev-resources a,
.next-resources a {
color: #333 !important;
text-decoration: none;
}
.prev-resources a:hover,
.next-resources a:hover {
color: #f57005 !important;
transition: all 0.3s ease;
}
.btn {
/* background: #f57005; */
color: #fff;
padding: 5px 15px;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
}
.btn:hover {
/* background: #e66600; */
transform: translateY(-2px);
}
.codebtn {
color: #0d6efd;
padding: 15px 30px;
border-radius: 8px;
border: 1px solid #0d6efd;
cursor: pointer;
transition: all 0.3s ease;
background-color: #fff;
}
.codebtn:hover {
transform: translateY(-2px);
}
.related-resourcess {
margin: 40px 0;
}
.related-resourcess h3 {
font-size: 20px;
font-weight: 600;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 1px solid #eee;
}
.related-title {
font-size: 20px;
font-weight: 600;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 1px solid #eee;
}
.resources-list {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 20px;
}
.resources-item {
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: transform 0.3s;
}
.resources-item:hover {
transform: translateY(-5px);
}
.resources-item a {
text-decoration: none;
color: inherit;
}
.resources-cover img {
width: 100%;
height: 150px;
object-fit: cover;
}
.resources-info {
padding: 10px;
}
.go-to-top {
position: fixed;
right: 30px;
bottom: 30px;
width: 40px;
height: 40px;
background: #f57005;
color: #fff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
opacity: 0;
visibility: hidden;
transition: all 0.3s ease;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
z-index: 1000;
}
.go-to-top.show {
opacity: 1;
visibility: visible;
}
.go-to-top:hover {
background: #e66600;
transform: translateY(-3px);
}
@media (max-width: 768px) {
.resources-title {
font-size: 24px;
}
.resources-list {
grid-template-columns: repeat(1, 1fr);
}
.resources-meta {
gap: 10px;
}
.go-to-top {
right: 20px;
bottom: 20px;
width: 36px;
height: 36px;
}
}
.disclaimers {
color: #b1b1b1;
width: 80%;
margin: 20px auto;
margin-bottom: 60px;
}
.disclaimer-title {
font-size: 16px;
font-weight: 600;
margin-bottom: 10px;
}
.disclaimer-content {
font-size: 14px;
line-height: 1.6;
}
.disclaimer-content p {
margin-bottom: 0;
}
.infoitem-value {
border: 1px #0d6efd dashed;
padding: 3px 6px;
font-size: 13px;
background-color: #0081ff12;
border-radius: 5px;
}
.detail-top-card-left {
width: 350px;
}
.swiper .swiper-button-prev,
.swiper .swiper-button-next {
color: #3881fd;
background: rgba(255, 255, 255, 0.9);
width: 40px;
height: 40px;
border-radius: 50%;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.swiper .swiper-button-prev:after,
.swiper .swiper-button-next:after {
font-size: 18px;
}
/* about author css */
.detail-top-right .aboutauthor-main {
display: flex;
flex-direction: column;
padding: 20px;
}
.detail-top-right .aboutauthor-main .aboutauthor-main-top {
display: flex;
align-items: center;
padding-left: 20px !important;
padding: 20px 0;
border-bottom: 1px solid #efefef;
margin-bottom: 20px;
}
.detail-top-right .aboutauthor-main .aboutauthor-main-top .aboutauthor-avatar {
margin-right: 12px;
}
.detail-top-right .aboutauthor-main .aboutauthor-main-top .aboutauthor-info .author-name {
font-size: 20px;
font-weight: 700;
}
.detail-top-right .aboutauthor-main .aboutauthor-main-top .aboutauthor-avatar img {
width: 60px;
height: 60px;
border-radius: 4px;
box-sizing: border-box;
margin: 0px;
min-width: 0px;
max-width: 100%;
background-color: #fff;
}
.detail-top-right .aboutauthor-main .aboutauthor-main-middle {
/* margin-left: 20px; */
}
.detail-top-right .aboutauthor-main .aboutauthor-main-middle .author-stats {
display: flex;
justify-content: space-evenly;
}
.detail-top-right .aboutauthor-main .aboutauthor-main-middle .author-stats .author-stats-item {
display: flex;
flex-direction: column;
align-items: center;
}
.detail-top-right .aboutauthor-main .aboutauthor-main-middle .author-stats .author-stats-item .count {
/* font-size: 30px; */
font-weight: 700;
}
.detail-top-right .aboutauthor-btn {
display: flex;
justify-content: space-evenly;
padding: 20px 0;
}
.detail-top-right .aboutauthor-btn .follow-btn {
background-color: #0081ff;
color: #fff;
padding: 10px 20px;
border-radius: 8px;
border: none;
}
.detail-top-right .aboutauthor-btn .message-btn {
color: #0081ff;
padding: 10px 20px;
border-radius: 8px;
border: 1px solid #eee;
}
</style>
{include file="component/foot" /}
+357
View File
@@ -0,0 +1,357 @@
{include file="component/head" /}
{include file="component/header" /}
<!-- 简约现代资源中心 -->
<div class="modern-resources-page">
<!-- 简约标题区 -->
<div class="modern-header">
<div class="container">
<h1 class="modern-title">资源中心</h1>
<p class="modern-subtitle">网络天下资源,一站式搜索</p>
</div>
</div>
<!-- 主要内容区 -->
<div class="container">
{volist name="categories" id="category"}
<div class="category-section">
<div class="category-header">
<div class="category-info">
<h2 class="category-title">{$category.parent.name}</h2>
</div>
<div class="category-count">
<span class="count-badge">{$category.subCategories|count} 个分类</span>
</div>
</div>
<div class="resource-grid">
{volist name="category.subCategories" id="subCategory"}
<a href="/index/resources/list?cid={$subCategory.id}" class="resource-card">
<div class="card-image">
<img src="{$subCategory.icon|default='/static/images/default-resource.jpg'}" alt="{$subCategory.name}">
<div class="image-overlay"></div>
</div>
<div class="card-content">
<div class="meta-info">
</div>
<h3 class="resource-title">{$subCategory.name}</h3>
<div class="card-footer">
<div class="resource-stats">
<span class="stat-item">
<i class="layui-icon layui-icon-template-1"></i>
<span>{$subCategory.resource_count} 个资源</span>
</span>
</div>
<div class="view-more">
<span>查看资源</span>
<i class="layui-icon layui-icon-right"></i>
</div>
</div>
</div>
</a>
{/volist}
</div>
</div>
{/volist}
</div>
</div>
<style>
/* 基础样式重置 */
.modern-resources-page {
font-family: 'Helvetica Neue', Arial, 'PingFang SC', 'Microsoft YaHei', sans-serif;
color: #333;
line-height: 1.6;
background-color: #f9fafc;
padding-bottom: 60px;
}
/* 标题区样式 */
.modern-header {
background: linear-gradient(135deg, #1E9FFF 0%, #0d8aff 100%);
color: white;
padding: 150px 0;
text-align: center;
margin-bottom: 40px;
position: relative;
overflow: hidden;
}
.modern-header::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: url('/static/images/pattern.png') repeat;
background-size: cover;
background-position: center;
opacity: 0.1;
}
.modern-title {
font-size: 2.5rem;
font-weight: 300;
margin-bottom: 15px;
letter-spacing: 1px;
position: relative;
}
.modern-subtitle {
font-size: 1.1rem;
font-weight: 300;
opacity: 0.9;
margin: 0;
position: relative;
}
/* 分类区块样式 */
.category-section {
margin-bottom: 60px;
background: white;
border-radius: 12px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
padding: 30px;
}
.category-header {
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 1px solid #f0f0f0;
display: flex;
justify-content: space-between;
align-items: center;
}
.category-info {
flex: 1;
}
.category-title {
font-size: 1.8rem;
font-weight: 500;
color: #333;
margin: 0 0 10px;
}
.category-subtitle {
font-size: 1rem;
color: #666;
margin: 0;
}
.category-count {
margin-left: 20px;
}
.count-badge {
background: #f0f7ff;
color: #1E9FFF;
padding: 6px 12px;
border-radius: 20px;
font-size: 0.85rem;
font-weight: 500;
}
/* 资源网格布局 */
.resource-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 25px;
}
/* 资源卡片样式 */
.resource-card {
background: white;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 3px 15px rgba(0, 0, 0, 0.03);
transition: all 0.3s ease;
display: flex;
flex-direction: column;
text-decoration: none;
color: inherit;
border: 1px solid #f0f0f0;
}
.resource-card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.08);
border-color: #e6f7ff;
}
.card-image {
height: 180px;
position: relative;
overflow: hidden;
background-color: #f5f7fa;
}
.card-image img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.5s ease;
}
.image-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(to top, rgba(0, 0, 0, 0.3), transparent);
}
.status-badge {
position: absolute;
top: 12px;
right: 12px;
padding: 4px 12px;
border-radius: 20px;
font-size: 0.75rem;
font-weight: 500;
z-index: 1;
}
.status-badge.active {
background: rgba(82, 196, 26, 0.9);
color: white;
}
.status-badge.inactive {
background: rgba(255, 77, 79, 0.9);
color: white;
}
.resource-card:hover .card-image img {
transform: scale(1.05);
}
.card-content {
padding: 20px;
flex: 1;
display: flex;
flex-direction: column;
}
.meta-info {
display: flex;
justify-content: space-between;
margin-bottom: 12px;
font-size: 0.85rem;
color: #666;
}
.resource-number {
background: #f0f7ff;
color: #1E9FFF;
padding: 3px 10px;
border-radius: 4px;
font-size: 0.75rem;
font-weight: 500;
}
.resource-title {
font-size: 1.1rem;
font-weight: 500;
margin: 0 0 15px;
color: #333;
line-height: 1.4;
}
.card-footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: auto;
padding-top: 15px;
border-top: 1px solid #f0f0f0;
}
.resource-stats {
font-size: 0.85rem;
color: #666;
}
.stat-item {
display: flex;
align-items: center;
gap: 5px;
}
.stat-item i {
font-size: 1rem;
color: #1E9FFF;
}
.view-more {
color: #1E9FFF;
font-size: 0.9rem;
font-weight: 500;
display: flex;
align-items: center;
gap: 5px;
}
.view-more i {
font-size: 0.8rem;
transition: transform 0.3s ease;
}
.resource-card:hover .view-more i {
transform: translateX(3px);
}
/* 响应式设计 */
@media (max-width: 1200px) {
.resource-grid {
grid-template-columns: repeat(3, 1fr);
}
}
@media (max-width: 992px) {
.resource-grid {
grid-template-columns: repeat(2, 1fr);
}
.category-section {
padding: 20px;
}
}
@media (max-width: 576px) {
.resource-grid {
grid-template-columns: 1fr;
}
.modern-title {
font-size: 1.8rem;
}
.modern-subtitle {
font-size: 1rem;
}
.category-title {
font-size: 1.5rem;
}
.category-header {
flex-direction: column;
align-items: flex-start;
gap: 10px;
}
.category-count {
margin-left: 0;
}
}
h2{
margin-bottom: 0 !important;
}
</style>
{include file="component/foot" /}
+306
View File
@@ -0,0 +1,306 @@
{include file="component/head" /}
{include file="component/header" /}
<!-- 简约现代资源列表页 -->
<div class="modern-resources-page">
<!-- 简约标题区 -->
<div class="modern-header">
<div class="container">
<h1 class="modern-title">{$category.name}</h1>
</div>
</div>
<!-- 主要内容区 -->
<div class="container">
<div class="modern-layout">
<!-- 资源列表网格 -->
<div class="resource-grid" id="resourceList">
{volist name="data" id="resource"}
<div class="resource-card">
<div class="card-image">
<img src="{$resource.icon|default='/static/images/default-resource.jpg'}"
alt="{$resource.title}">
<div class="image-overlay"></div>
</div>
<div class="card-content">
<div class="meta-info">
<span class="resource-number">{$resource.number}</span>
<time class="create-date">{$resource.create_time|date="Y-m-d"}</time>
</div>
<h3 class="resource-title">{$resource.title}</h3>
<div class="card-footer">
<div class="resource-stats">
<span class="stat-item">
<i class="layui-icon layui-icon-template-1"></i>
<span>资源详情</span>
</span>
</div>
<a href="/index/resources/detail?id={$resource.id}" class="view-more">
<span>查看详情</span>
<i class="layui-icon layui-icon-right"></i>
</a>
</div>
</div>
</div>
{/volist}
</div>
<!-- 分页 -->
<div class="pagination-container">
{$page|raw}
</div>
</div>
</div>
</div>
<style>
/* 基础样式重置 */
.modern-resources-page {
font-family: 'Helvetica Neue', Arial, 'PingFang SC', 'Microsoft YaHei', sans-serif;
color: #333;
line-height: 1.6;
background-color: #f9fafc;
padding-bottom: 60px;
}
/* 标题区样式 */
.modern-header {
background: linear-gradient(135deg, #1E9FFF 0%, #0d8aff 100%);
color: white;
padding: 150px 0;
text-align: center;
margin-bottom: 40px;
position: relative;
overflow: hidden;
}
.modern-header::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: url('/static/images/pattern.png') repeat;
background-size: cover;
background-position: center;
opacity: 0.1;
}
.modern-title {
font-size: 2.5rem;
font-weight: 300;
margin-bottom: 15px;
letter-spacing: 1px;
position: relative;
}
.modern-subtitle {
font-size: 1.1rem;
font-weight: 300;
opacity: 0.9;
margin: 0;
position: relative;
}
/* 资源网格布局 */
.resource-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 25px;
margin-bottom: 40px;
}
/* 资源卡片样式 */
.resource-card {
background: white;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 3px 15px rgba(0, 0, 0, 0.03);
transition: all 0.3s ease;
display: flex;
flex-direction: column;
border: 1px solid #f0f0f0;
}
.resource-card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.08);
border-color: #e6f7ff;
}
.card-image {
height: 180px;
position: relative;
overflow: hidden;
background-color: #f5f7fa;
}
.card-image img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.5s ease;
}
.image-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(to top, rgba(0, 0, 0, 0.3), transparent);
}
.resource-card:hover .card-image img {
transform: scale(1.05);
}
.card-content {
padding: 20px;
flex: 1;
display: flex;
flex-direction: column;
}
.meta-info {
display: flex;
justify-content: space-between;
margin-bottom: 12px;
font-size: 0.85rem;
color: #666;
}
.resource-number {
background: #f0f7ff;
color: #1E9FFF;
padding: 3px 10px;
border-radius: 4px;
font-size: 0.75rem;
font-weight: 500;
}
.resource-title {
font-size: 1.1rem;
font-weight: 500;
margin: 0 0 15px;
color: #333;
line-height: 1.4;
border-top: 1px solid #f0f0f0;
padding-top: 10px;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
white-space: normal;
max-height: 3.2em;
}
.card-footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: auto;
padding-top: 15px;
border-top: 1px solid #f0f0f0;
}
.resource-stats {
font-size: 0.85rem;
color: #666;
}
.stat-item {
display: flex;
align-items: center;
gap: 5px;
}
.stat-item i {
font-size: 1rem;
color: #1E9FFF;
}
.view-more {
color: #1E9FFF;
font-size: 0.9rem;
font-weight: 500;
text-decoration: none;
display: flex;
align-items: center;
gap: 5px;
}
.view-more i {
font-size: 0.8rem;
transition: transform 0.3s ease;
}
.resource-card:hover .view-more i {
transform: translateX(3px);
}
/* 分页样式 */
.pagination-container {
text-align: center;
margin-top: 40px;
}
.pagination-container .pagination {
display: inline-flex;
gap: 5px;
}
.pagination-container .pagination a,
.pagination-container .pagination span {
padding: 8px 16px;
border-radius: 4px;
background: white;
color: #666;
text-decoration: none;
transition: all 0.3s ease;
border: 1px solid #f0f0f0;
}
.pagination-container .pagination a:hover {
background: #f0f7ff;
color: #1E9FFF;
border-color: #e6f7ff;
}
.pagination-container .pagination .active {
background: #1E9FFF;
color: white;
border-color: #1E9FFF;
}
/* 响应式设计 */
@media (max-width: 1200px) {
.resource-grid {
grid-template-columns: repeat(3, 1fr);
}
}
@media (max-width: 992px) {
.resource-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 576px) {
.resource-grid {
grid-template-columns: 1fr;
}
.modern-title {
font-size: 1.8rem;
}
.modern-subtitle {
font-size: 1rem;
}
}
</style>
{include file="component/foot" /}
+132
View File
@@ -0,0 +1,132 @@
{include file="component/head" /}
{include file="component/header" /}
<div class="layui-container" style="padding: 20px 0;">
<div class="layui-row">
<div class="layui-col-md12">
<!-- 搜索头部 -->
<div class="layui-card">
<div class="layui-card-header" style="display: flex; align-items: center;">
<h6 class="layui-inline" style="margin-bottom: 0;padding:10px 0;">搜索结果:{$keyword}</h6>
<!-- <div class="layui-inline" style="float: right;">
<a href="/index/search/list?keyword={$keyword}&type=article" class="layui-btn layui-btn-sm {$type == 'article' ? 'layui-btn-normal' : 'layui-btn-primary'}">文章</a>
<a href="/index/search/list?keyword={$keyword}&type=resource" class="layui-btn layui-btn-sm {$type == 'resource' ? 'layui-btn-normal' : 'layui-btn-primary'}">资源</a>
</div> -->
</div>
</div>
<!-- 搜索结果列表 -->
<div class="layui-row layui-col-space20" style="margin-top: 20px;">
{if $items}
{volist name="items" id="item"}
<div class="layui-col-md12">
<div class="search-result-item" onclick="window.location.href='<?php echo $type == 'articles' ? url('articles/detail', ['id' => $item['id']]) : $item['detail_url'] ?>'">
<div class="search-result-image">
<?php if($type == 'article'): ?>
<img src="<?php echo $item['image'] ?: '/static/images/default.jpg' ?>" alt="<?php echo $item['title'] ?>">
<?php else: ?>
<img src="<?php echo $item['icon'] ?: '/static/images/default.jpg' ?>" alt="<?php echo $item['title'] ?>">
<?php endif; ?>
</div>
<div class="search-result-content">
<h3 class="search-result-title"><?php echo $item['title'] ?></h3>
<div class="search-result-meta">
<span class="layui-badge layui-bg-blue"><?php echo $item['cate'] ?></span>
<?php if($type == 'article'): ?>
<span class="layui-badge layui-bg-green">作者:<?php echo $item['author'] ?></span>
<?php else: ?>
<span class="layui-badge layui-bg-green">上传者:<?php echo $item['uploader'] ?></span>
<?php endif; ?>
<span class="layui-badge layui-bg-gray"><?php echo $item['publishdate'] ?></span>
</div>
</div>
</div>
</div>
{/volist}
{else}
<div class="layui-col-md12">
<div class="layui-card">
<div class="layui-card-body" style="text-align: center; padding: 50px 0;">
<i class="layui-icon layui-icon-face-surprised" style="font-size: 48px; color: #999;"></i>
<p style="margin-top: 15px; color: #999;">暂无相关{$type == 'article' ? '文章' : '资源'}</p>
</div>
</div>
</div>
{/if}
</div>
<!-- 分页 -->
<div id="pagination" style="text-align: center; margin-top: 20px;"></div>
</div>
</div>
</div>
{include file="component/footer" /}
{include file="component/foot" /}
<script>
layui.use(['laypage'], function(){
var laypage = layui.laypage;
//执行一个laypage实例
laypage.render({
elem: 'pagination',
count: {$count},
limit: {$limit},
curr: {$page},
jump: function(obj, first){
if(!first){
var url = '/index/search/list?keyword={$keyword}&type={$type}&page=' + obj.curr + '&limit=' + obj.limit;
window.location.href = url;
}
}
});
});
</script>
<style>
.search-result-item {
display: flex;
align-items: center;
padding: 15px;
transition: all 0.3s ease;
border-radius: 4px;
background-color: #fff;
}
.search-result-item:hover {
background-color:rgb(255, 255, 255);
transform: translateY(-2px);
box-shadow: 0 2px 8px rgba(0, 174, 255, 0.5);
}
.search-result-image {
margin-right: 20px;
flex-shrink: 0;
}
.search-result-image img {
width: 250px;
height: 140px;
object-fit: cover;
border-radius: 4px;
}
.search-result-content {
flex: 1;
}
.search-result-title {
font-size: 18px;
color: #333;
margin: 0 0 10px 0;
}
.search-result-meta {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.search-result-meta .layui-badge {
margin-right: 5px;
}
</style>
+268
View File
@@ -0,0 +1,268 @@
<div class="avatar-section">
<h2 class="section-title">修改头像</h2>
<div class="avatar-upload-container">
<div class="current-avatar">
<img src="{$user.avatar|default='/static/images/avatar.png'}" alt="当前头像" id="currentAvatar">
<p class="avatar-tip">当前头像</p>
</div>
<div class="upload-area" id="uploadArea">
<i class="layui-icon layui-icon-upload"></i>
<p>点击或拖拽图片到此处上传</p>
<p class="upload-tip">支持 jpg、png、gif 格式,大小不超过 2MB</p>
<input type="file" id="avatarFile" accept="image/*" style="display: none;">
</div>
</div>
<div class="avatar-preview" style="display: none;">
<h3>预览</h3>
<div class="preview-container">
<img src="" alt="预览图" id="previewImage">
</div>
<div class="preview-actions">
<button class="layui-btn" id="confirmUpload">确认上传</button>
<button class="layui-btn layui-btn-primary" id="cancelUpload">取消</button>
</div>
</div>
</div>
<style>
.avatar-section {
max-width: 800px;
margin: 0 auto;
}
.section-title {
font-size: 20px;
font-weight: 600;
color: #333;
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 1px solid #f0f0f0;
}
.avatar-upload-container {
display: flex;
gap: 40px;
margin-bottom: 32px;
}
.current-avatar {
text-align: center;
}
.current-avatar img {
width: 160px;
height: 160px;
border-radius: 50%;
object-fit: cover;
border: 3px solid #f5f5f5;
}
.avatar-tip {
margin-top: 12px;
color: #666;
}
.upload-area {
flex: 1;
border: 2px dashed #d9d9d9;
border-radius: 8px;
padding: 40px;
text-align: center;
cursor: pointer;
transition: all 0.3s;
}
.upload-area:hover {
border-color: #1677ff;
}
.upload-area .layui-icon {
font-size: 48px;
color: #999;
margin-bottom: 16px;
}
.upload-area p {
margin: 8px 0;
color: #666;
}
.upload-tip {
font-size: 12px;
color: #999;
}
.avatar-preview {
margin-top: 32px;
padding-top: 32px;
border-top: 1px solid #f0f0f0;
}
.avatar-preview h3 {
font-size: 16px;
color: #333;
margin-bottom: 16px;
}
.preview-container {
text-align: center;
margin-bottom: 24px;
}
.preview-container img {
max-width: 200px;
max-height: 200px;
border-radius: 8px;
}
.preview-actions {
text-align: center;
}
.preview-actions .layui-btn {
margin: 0 8px;
}
@media (max-width: 768px) {
.avatar-upload-container {
flex-direction: column;
gap: 24px;
}
.current-avatar img {
width: 120px;
height: 120px;
}
.upload-area {
padding: 24px;
}
}
</style>
<script>
layui.use(['upload', 'layer'], function () {
var upload = layui.upload;
var layer = layui.layer;
// 点击上传区域触发文件选择
document.getElementById('uploadArea').addEventListener('click', function () {
document.getElementById('avatarFile').click();
});
// 处理文件选择
document.getElementById('avatarFile').addEventListener('change', function (e) {
var file = e.target.files[0];
if (!file) return;
// 检查文件类型
if (!['image/jpeg', 'image/png', 'image/gif', 'image/webp'].includes(file.type)) {
layer.msg('请上传 jpg、png、webp 或 gif 格式的图片');
return;
}
// 检查文件大小
if (file.size > 2 * 1024 * 1024) {
layer.msg('图片大小不能超过 2MB');
return;
}
// 预览图片
var reader = new FileReader();
reader.onload = function (e) {
document.getElementById('previewImage').src = e.target.result;
document.querySelector('.avatar-preview').style.display = 'block';
};
reader.readAsDataURL(file);
});
// 确认上传
document.getElementById('confirmUpload').addEventListener('click', function () {
var file = document.getElementById('avatarFile').files[0];
if (!file) return;
var formData = new FormData();
formData.append('avatar', file);
// 显示上传中
var loadIndex = layer.load(2);
// 发送上传请求
fetch('/index/user/update_avatar', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
layer.close(loadIndex);
if (data.code === 0) {
// 更新cookie中的头像
document.cookie = "user_avatar=" + data.data.url + "; path=/";
// 更新localStorage中的头像
localStorage.setItem('user_avatar', data.data.url);
layer.msg('头像上传成功', {
icon: 1,
time: 1000
}, function () {
// 更新当前头像显示
document.getElementById('currentAvatar').src = data.data.url;
// 隐藏预览区域
document.querySelector('.avatar-preview').style.display = 'none';
// 清空文件输入
document.getElementById('avatarFile').value = '';
// 刷新页面以更新所有显示的头像
window.location.reload();
});
} else {
layer.msg(data.msg || '上传失败', { icon: 2 });
}
})
.catch(error => {
layer.close(loadIndex);
layer.msg('上传失败,请重试', { icon: 2 });
});
});
// 取消上传
document.getElementById('cancelUpload').addEventListener('click', function () {
document.querySelector('.avatar-preview').style.display = 'none';
document.getElementById('avatarFile').value = '';
});
// 拖拽上传
var uploadArea = document.getElementById('uploadArea');
uploadArea.addEventListener('dragover', function (e) {
e.preventDefault();
this.style.borderColor = '#1677ff';
});
uploadArea.addEventListener('dragleave', function (e) {
e.preventDefault();
this.style.borderColor = '#d9d9d9';
});
uploadArea.addEventListener('drop', function (e) {
e.preventDefault();
this.style.borderColor = '#d9d9d9';
var file = e.dataTransfer.files[0];
if (!file) return;
// 触发文件选择事件
var dataTransfer = new DataTransfer();
dataTransfer.items.add(file);
document.getElementById('avatarFile').files = dataTransfer.files;
// 手动触发change事件
var event = new Event('change');
document.getElementById('avatarFile').dispatchEvent(event);
});
});
</script>
+427
View File
@@ -0,0 +1,427 @@
<div class="basic-info">
<div class="layui-tab">
<ul class="layui-tab-title">
<li class="layui-this">个人资料</li>
<li>修改头像</li>
</ul>
<div class="layui-tab-content">
<div class="layui-tab-item layui-show">
<form class="layui-form" lay-filter="basicForm">
<div class="layui-form-item">
<label class="layui-form-label">用户名</label>
<div class="layui-input-block">
<input type="text" name="name" value="{$user.name}" placeholder="请输入用户名"
class="layui-input">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">账号</label>
<div class="layui-input-block" style="display: flex; align-items: center;">
<input type="text" value="{$user.account}" class="layui-input" disabled>
<button class="layui-btn" style="margin-left: 20px;">编辑</button>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">QQ</label>
<div class="layui-input-block">
<input type="text" name="qq" value="{$user.qq}" placeholder="请输入QQ号" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">微信</label>
<div class="layui-input-block">
{if $user.wechat}
<input type="text" name="wechat" value="{$user.wechat}" placeholder="请输入微信号"
class="layui-input">
{else}
<button class="layui-btn" id="bindWechat">绑定微信号</button>
{/if}
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">手机号</label>
<div class="layui-input-block">
<input type="text" name="phone" value="{$user.phone}" placeholder="请输入手机号"
class="layui-input">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">性别</label>
<div class="layui-input-block">
<input type="radio" name="sex" value="1" title="" {if $user.sex==1}checked{/if}>
<input type="radio" name="sex" value="2" title="" {if $user.sex==2}checked{/if}>
<input type="radio" name="sex" value="0" title="保密" {if $user.sex==0}checked{/if}>
</div>
</div>
<div class="layui-form-item">
<div class="layui-input-block">
<button class="layui-btn" lay-submit lay-filter="saveBasic">保存修改</button>
<button type="reset" class="layui-btn layui-btn-primary">重置</button>
</div>
</div>
</form>
</div>
<div class="layui-tab-item">
<div class="avatar-section">
<div class="avatar-upload-container">
<div class="current-avatar">
<img src="{$user.avatar|default='/static/images/avatar.png'}" alt="当前头像" id="currentAvatar">
<p class="avatar-tip">当前头像</p>
</div>
<div class="upload-area" id="uploadArea">
<i class="layui-icon layui-icon-upload"></i>
<p>点击或拖拽图片到此处上传</p>
<p class="upload-tip">支持 jpg、png、gif 格式,大小不超过 2MB</p>
<input type="file" id="avatarFile" accept="image/*" style="display: none;">
</div>
</div>
<div class="avatar-preview" style="display: none;">
<h3>预览</h3>
<div class="preview-container">
<img src="" alt="预览图" id="previewImage">
</div>
<div class="preview-actions">
<button class="layui-btn" id="confirmUpload">确认上传</button>
<button class="layui-btn layui-btn-primary" id="cancelUpload">取消</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
layui.use(['form', 'layer', 'upload'], function () {
var form = layui.form;
var layer = layui.layer;
var upload = layui.upload;
// 绑定微信号按钮点击事件
document.getElementById('bindWechat').addEventListener('click', function () {
// 发送 AJAX 请求调用 qrcode 接口
fetch('/index/user/qrcode')
.then(response => response.json())
.then(data => {
if (data.code === 0) {
// 二维码生成成功,这里可以添加显示二维码的逻辑,例如弹出一个窗口显示二维码
layer.open({
type: 1,
title: '微信绑定二维码',
content: `<img src="${data.data.qrcode_url}" alt="微信绑定二维码">`,
area: ['300px', '300px']
});
} else {
// 二维码生成失败,提示用户
layer.msg(data.msg, { icon: 2 });
}
})
.catch(error => {
// 请求出错,提示用户
layer.msg('请求出错,请稍后重试', { icon: 2 });
});
});
// 监听个人资料表单提交
form.on('submit(saveBasic)', function (data) {
// 发送AJAX请求保存数据
fetch('/index/user/saveBasic', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify(data.field)
})
.then(response => response.json())
.then(result => {
if (result.code === 0) {
layer.msg(result.msg, { icon: 1 }, function () {
// 保存成功后刷新页面
window.location.reload();
});
} else {
layer.msg(result.msg, { icon: 2 });
}
})
.catch(error => {
console.error('保存失败:', error);
layer.msg('保存失败,请稍后重试', { icon: 2 });
});
return false; // 阻止表单默认提交
});
// 点击上传区域触发文件选择
document.getElementById('uploadArea').addEventListener('click', function () {
document.getElementById('avatarFile').click();
});
// 处理文件选择
document.getElementById('avatarFile').addEventListener('change', function (e) {
var file = e.target.files[0];
if (!file) return;
// 检查文件类型
if (!['image/jpeg', 'image/png', 'image/gif', 'image/webp'].includes(file.type)) {
layer.msg('请上传 jpg、png、webp 或 gif 格式的图片');
return;
}
// 检查文件大小
if (file.size > 2 * 1024 * 1024) {
layer.msg('图片大小不能超过 2MB');
return;
}
// 预览图片
var reader = new FileReader();
reader.onload = function (e) {
document.getElementById('previewImage').src = e.target.result;
document.querySelector('.avatar-preview').style.display = 'block';
};
reader.readAsDataURL(file);
});
// 确认上传
document.getElementById('confirmUpload').addEventListener('click', function () {
var file = document.getElementById('avatarFile').files[0];
if (!file) return;
var formData = new FormData();
formData.append('avatar', file);
// 显示上传中
var loadIndex = layer.load(2);
// 发送上传请求
fetch('/index/user/update_avatar', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
layer.close(loadIndex);
if (data.code === 0) {
// 更新cookie中的头像
document.cookie = "user_avatar=" + data.data.url + "; path=/";
// 更新localStorage中的头像
localStorage.setItem('user_avatar', data.data.url);
layer.msg('头像上传成功', {
icon: 1,
time: 1000
}, function () {
// 更新当前头像显示
document.getElementById('currentAvatar').src = data.data.url;
// 隐藏预览区域
document.querySelector('.avatar-preview').style.display = 'none';
// 清空文件输入
document.getElementById('avatarFile').value = '';
// 刷新页面以更新所有显示的头像
window.location.reload();
});
} else {
layer.msg(data.msg || '上传失败', { icon: 2 });
}
})
.catch(error => {
layer.close(loadIndex);
layer.msg('上传失败,请重试', { icon: 2 });
});
});
// 取消上传
document.getElementById('cancelUpload').addEventListener('click', function () {
document.querySelector('.avatar-preview').style.display = 'none';
document.getElementById('avatarFile').value = '';
});
// 拖拽上传
var uploadArea = document.getElementById('uploadArea');
uploadArea.addEventListener('dragover', function (e) {
e.preventDefault();
this.style.borderColor = '#1677ff';
});
uploadArea.addEventListener('dragleave', function (e) {
e.preventDefault();
this.style.borderColor = '#d9d9d9';
});
uploadArea.addEventListener('drop', function (e) {
e.preventDefault();
this.style.borderColor = '#d9d9d9';
var file = e.dataTransfer.files[0];
if (!file) return;
// 触发文件选择事件
var dataTransfer = new DataTransfer();
dataTransfer.items.add(file);
document.getElementById('avatarFile').files = dataTransfer.files;
// 手动触发change事件
var event = new Event('change');
document.getElementById('avatarFile').dispatchEvent(event);
});
});
</script>
<style>
.basic-info {
max-width: 800px;
margin: 0 auto;
}
.section-title {
font-size: 20px;
font-weight: 600;
color: #333;
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 1px solid #f0f0f0;
}
.layui-form-label {
width: 100px;
}
.layui-input-block {
margin-left: 130px;
}
.layui-form-item {
margin-bottom: 24px;
}
.layui-textarea {
min-height: 120px;
}
.avatar-section {
max-width: 800px;
margin: 0 auto;
}
.avatar-upload-container {
display: flex;
gap: 40px;
margin-bottom: 32px;
}
.current-avatar {
text-align: center;
}
.current-avatar img {
width: 160px;
height: 160px;
border-radius: 50%;
object-fit: cover;
border: 3px solid #f5f5f5;
}
.avatar-tip {
margin-top: 12px;
color: #666;
}
.upload-area {
flex: 1;
border: 2px dashed #d9d9d9;
border-radius: 8px;
padding: 40px;
text-align: center;
cursor: pointer;
transition: all 0.3s;
}
.upload-area:hover {
border-color: #1677ff;
}
.upload-area .layui-icon {
font-size: 48px;
color: #999;
margin-bottom: 16px;
}
.upload-area p {
margin: 8px 0;
color: #666;
}
.upload-tip {
font-size: 12px;
color: #999;
}
.avatar-preview {
margin-top: 32px;
padding-top: 32px;
border-top: 1px solid #f0f0f0;
}
.avatar-preview h3 {
font-size: 16px;
color: #333;
margin-bottom: 16px;
}
.preview-container {
text-align: center;
margin-bottom: 24px;
}
.preview-container img {
max-width: 200px;
max-height: 200px;
border-radius: 8px;
}
.preview-actions {
text-align: center;
}
.preview-actions .layui-btn {
margin: 0 8px;
}
@media (max-width: 768px) {
.layui-form-label {
width: 80px;
}
.layui-input-block {
margin-left: 110px;
}
.avatar-upload-container {
flex-direction: column;
gap: 24px;
}
.current-avatar img {
width: 120px;
height: 120px;
}
.upload-area {
padding: 24px;
}
}
</style>
+326
View File
@@ -0,0 +1,326 @@
<div class="basic-info">
<div class="layui-tab">
<ul class="layui-tab-title">
<li class="layui-this">全部消息</li>
<li>未读消息</li>
<li>已读消息</li>
</ul>
<div class="layui-tab-content">
<div class="layui-tab-item layui-show">
<div class="message-list" id="allMessages">
<!-- 消息列表将通过JavaScript动态加载 -->
</div>
</div>
<div class="layui-tab-item">
<div class="message-list" id="unreadMessages">
<!-- 未读消息列表 -->
</div>
</div>
<div class="layui-tab-item">
<div class="message-list" id="readMessages">
<!-- 已读消息列表 -->
</div>
</div>
</div>
</div>
</div>
<style>
.basic-info {
max-width: 800px;
margin: 0 auto;
}
.section-title {
font-size: 20px;
font-weight: 600;
color: #333;
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 1px solid #f0f0f0;
}
.layui-form-label {
width: 100px;
}
.layui-input-block {
margin-left: 130px;
}
.layui-form-item {
margin-bottom: 24px;
}
.layui-textarea {
min-height: 120px;
}
.message-list {
margin-top: 20px;
}
.message-item {
padding: 16px;
border-bottom: 1px solid #f0f0f0;
display: flex;
align-items: flex-start;
gap: 16px;
cursor: pointer;
transition: background-color 0.3s;
}
.message-item:hover {
background-color: #f9f9f9;
}
.message-item.unread {
background-color: #f0f7ff;
}
.message-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
object-fit: cover;
}
.message-content {
flex: 1;
}
.message-title {
font-weight: 500;
margin-bottom: 4px;
color: #333;
}
.message-text {
color: #666;
font-size: 14px;
margin-bottom: 8px;
}
.message-meta {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 12px;
color: #999;
}
.message-time {
color: #999;
}
.message-actions {
display: flex;
gap: 8px;
}
.message-actions button {
padding: 4px 8px;
font-size: 12px;
border-radius: 4px;
background: none;
border: 1px solid #d9d9d9;
color: #666;
cursor: pointer;
transition: all 0.3s;
}
.message-actions button:hover {
border-color: #1677ff;
color: #1677ff;
}
.empty-message {
text-align: center;
padding: 40px 0;
color: #999;
}
@media (max-width: 768px) {
.layui-form-label {
width: 80px;
}
.layui-input-block {
margin-left: 110px;
}
.message-item {
padding: 12px;
}
.message-avatar {
width: 32px;
height: 32px;
}
}
</style>
<script>
layui.use(['element', 'layer'], function () {
var element = layui.element;
var layer = layui.layer;
// 加载消息列表
function loadMessages(type) {
var container = document.getElementById(type + 'Messages');
var loadIndex = layer.load(2);
fetch('/index/user/getMessages?type=' + type)
.then(response => response.json())
.then(data => {
layer.close(loadIndex);
if (data.code === 0) {
renderMessages(container, data.data);
} else {
layer.msg(data.msg || '加载失败', { icon: 2 });
}
})
.catch(error => {
layer.close(loadIndex);
layer.msg('加载失败,请重试', { icon: 2 });
});
}
// 渲染消息列表
function renderMessages(container, messages) {
if (!messages || messages.length === 0) {
container.innerHTML = '<div class="empty-message">暂无消息</div>';
return;
}
var html = '';
messages.forEach(function (message) {
html += `
<div class="message-item ${message.is_read ? '' : 'unread'}" data-id="${message.id}">
<img src="${message.avatar || '/static/images/avatar.png'}" class="message-avatar" alt="头像">
<div class="message-content">
<div class="message-title">${message.title}</div>
<div class="message-text">${message.content}</div>
<div class="message-meta">
<span class="message-time">${message.create_time}</span>
<div class="message-actions">
${!message.is_read ? '<button class="mark-read">标记已读</button>' : ''}
<button class="delete-message">删除</button>
</div>
</div>
</div>
</div>
`;
});
container.innerHTML = html;
// 绑定事件
bindMessageEvents(container);
}
// 绑定消息事件
function bindMessageEvents(container) {
// 标记已读
container.querySelectorAll('.mark-read').forEach(btn => {
btn.addEventListener('click', function (e) {
e.stopPropagation();
var messageId = this.closest('.message-item').dataset.id;
markAsRead(messageId);
});
});
// 删除消息
container.querySelectorAll('.delete-message').forEach(btn => {
btn.addEventListener('click', function (e) {
e.stopPropagation();
var messageId = this.closest('.message-item').dataset.id;
deleteMessage(messageId);
});
});
// 点击消息
container.querySelectorAll('.message-item').forEach(item => {
item.addEventListener('click', function () {
var messageId = this.dataset.id;
viewMessage(messageId);
});
});
}
// 标记已读
function markAsRead(messageId) {
fetch('/index/user/markMessageRead', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ id: messageId })
})
.then(response => response.json())
.then(data => {
if (data.code === 0) {
layer.msg('已标记为已读', { icon: 1 });
// 重新加载消息列表
loadMessages('all');
loadMessages('unread');
loadMessages('read');
} else {
layer.msg(data.msg || '操作失败', { icon: 2 });
}
})
.catch(error => {
layer.msg('操作失败,请重试', { icon: 2 });
});
}
// 删除消息
function deleteMessage(messageId) {
layer.confirm('确定要删除这条消息吗?', {
btn: ['确定', '取消']
}, function () {
fetch('/index/user/deleteMessage', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ id: messageId })
})
.then(response => response.json())
.then(data => {
if (data.code === 0) {
layer.msg('删除成功', { icon: 1 });
// 重新加载消息列表
loadMessages('all');
loadMessages('unread');
loadMessages('read');
} else {
layer.msg(data.msg || '删除失败', { icon: 2 });
}
})
.catch(error => {
layer.msg('删除失败,请重试', { icon: 2 });
});
});
}
// 查看消息详情
function viewMessage(messageId) {
layer.open({
type: 2,
title: '消息详情',
area: ['500px', '400px'],
content: '/index/user/messageDetail?id=' + messageId
});
}
// 监听标签切换
element.on('tab(messageTabs)', function (data) {
var type = ['all', 'unread', 'read'][data.index];
loadMessages(type);
});
// 初始加载全部消息
loadMessages('all');
});
</script>
@@ -0,0 +1,209 @@
<div class="notifications-section">
<div class="layui-tab layui-tab-brief" lay-filter="notificationTabs">
<ul class="layui-tab-title">
<li class="layui-this">全部通知</li>
<li>未读通知</li>
<li>已读通知</li>
</ul>
<div class="layui-tab-content">
<div class="layui-tab-item layui-show">
<div class="notification-list" id="allNotifications"></div>
</div>
<div class="layui-tab-item">
<div class="notification-list" id="unreadNotifications"></div>
</div>
<div class="layui-tab-item">
<div class="notification-list" id="readNotifications"></div>
</div>
</div>
</div>
</div>
<style>
.notifications-section {
max-width: 800px;
margin: 0 auto;
}
.section-title {
font-size: 20px;
font-weight: 600;
color: #333;
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 1px solid #f0f0f0;
}
.notification-list {
min-height: 200px;
}
.notification-item {
padding: 16px;
border-bottom: 1px solid #f0f0f0;
display: flex;
justify-content: space-between;
align-items: center;
}
.notification-item:hover {
background-color: #f9f9f9;
}
.notification-content {
flex: 1;
}
.notification-title {
font-size: 16px;
color: #333;
margin-bottom: 8px;
}
.notification-time {
font-size: 12px;
color: #999;
}
.notification-actions {
display: flex;
gap: 8px;
}
.notification-item.unread .notification-title {
font-weight: 600;
}
.notification-item.unread::before {
content: '';
display: inline-block;
width: 8px;
height: 8px;
background-color: #1677ff;
border-radius: 50%;
margin-right: 8px;
}
@media (max-width: 768px) {
.notification-actions {
flex-direction: column;
}
}
</style>
<script>
layui.use(['element', 'layer'], function () {
var element = layui.element;
var layer = layui.layer;
// 加载通知列表
function loadNotifications(type) {
var container = document.getElementById(type + 'Notifications');
container.innerHTML = '<div class="layui-anim layui-anim-upbit layui-anim-loop layui-anim-shrink" style="text-align: center; padding: 20px;"><i class="layui-icon layui-icon-loading layui-anim layui-anim-rotate layui-anim-loop"></i></div>';
fetch('/index/user/getNotifications?type=' + type)
.then(response => response.json())
.then(data => {
if (data.code === 0) {
if (data.data.length === 0) {
container.innerHTML = '<div class="layui-none">暂无通知</div>';
return;
}
var html = '';
data.data.forEach(function (notification) {
html += `
<div class="notification-item ${notification.is_read ? '' : 'unread'}" data-id="${notification.id}">
<div class="notification-content">
<div class="notification-title">${notification.title}</div>
<div class="notification-time">${notification.create_time}</div>
</div>
<div class="notification-actions">
<button class="layui-btn layui-btn-xs" onclick="viewNotification(${notification.id})">查看</button>
<button class="layui-btn layui-btn-xs layui-btn-danger" onclick="deleteNotification(${notification.id})">删除</button>
</div>
</div>
`;
});
container.innerHTML = html;
} else {
layer.msg(data.msg || '加载失败', { icon: 2 });
}
})
.catch(error => {
layer.msg('加载失败,请重试', { icon: 2 });
});
}
// 查看通知
window.viewNotification = function (notificationId) {
fetch('/index/user/readNotification', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ id: notificationId })
})
.then(response => response.json())
.then(data => {
if (data.code === 0) {
layer.open({
type: 2,
title: '通知详情',
area: ['500px', '400px'],
content: '/index/user/notificationDetail?id=' + notificationId
});
// 重新加载通知列表
loadNotifications('all');
loadNotifications('unread');
loadNotifications('read');
} else {
layer.msg(data.msg || '操作失败', { icon: 2 });
}
})
.catch(error => {
layer.msg('操作失败,请重试', { icon: 2 });
});
}
// 删除通知
window.deleteNotification = function (notificationId) {
layer.confirm('确定要删除这条通知吗?', {
btn: ['确定', '取消']
}, function () {
fetch('/index/user/deleteNotification', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ id: notificationId })
})
.then(response => response.json())
.then(data => {
if (data.code === 0) {
layer.msg('删除成功', { icon: 1 });
// 重新加载通知列表
loadNotifications('all');
loadNotifications('unread');
loadNotifications('read');
} else {
layer.msg(data.msg || '删除失败', { icon: 2 });
}
})
.catch(error => {
layer.msg('删除失败,请重试', { icon: 2 });
});
});
}
// 监听标签切换
element.on('tab(notificationTabs)', function (data) {
var type = ['all', 'unread', 'read'][data.index];
loadNotifications(type);
});
// 初始加载全部通知
loadNotifications('all');
});
</script>
+100
View File
@@ -0,0 +1,100 @@
<div class="security-section">
<h2 class="section-title">安全设置</h2>
<form class="layui-form" lay-filter="securityForm">
<div class="layui-form-item">
<label class="layui-form-label">登录密码</label>
<div class="layui-input-block">
<button type="button" class="layui-btn" onclick="changePassword()">修改密码</button>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">手机绑定</label>
<div class="layui-input-block">
<div class="phone-info">
<span id="phoneNumber">未绑定</span>
<button type="button" class="layui-btn layui-btn-primary" onclick="bindPhone()">绑定手机</button>
</div>
</div>
</div>
</form>
</div>
<style>
.security-section {
max-width: 800px;
margin: 0 auto;
}
.section-title {
font-size: 20px;
font-weight: 600;
color: #333;
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 1px solid #f0f0f0;
}
.phone-info,
.email-info {
display: flex;
align-items: center;
gap: 16px;
}
.layui-form-label {
width: 100px;
}
.layui-input-block {
margin-left: 130px;
}
@media (max-width: 768px) {
.layui-form-label {
width: 80px;
}
.layui-input-block {
margin-left: 110px;
}
}
</style>
<script>
layui.use(['form', 'layer'], function () {
var form = layui.form;
var layer = layui.layer;
// 加载用户安全信息
loadSecurityInfo();
});
// 修改密码
function changePassword() {
layer.open({
type: 2,
title: '修改密码',
area: ['500px', '400px'],
content: '/index/user/updatePassword',
end: function () {
// 检查是否需要跳转到登录页
if (window.needRedirect) {
window.location.href = '/index/user/login';
}
}
});
}
// 绑定手机
function bindPhone() {
layer.open({
type: 2,
title: '绑定手机',
area: ['500px', '400px'],
content: '/index/user/component/bindPhone'
});
}
</script>
+157
View File
@@ -0,0 +1,157 @@
<div class="sidebar">
<div class="user-info">
<div class="avatar-wrapper">
<img src="{$user.avatar|default='/static/images/avatar.png'}" class="avatar" alt="用户头像">
</div>
<h3 class="username">{$user.name}</h3>
<p class="email">{$user.account}</p>
</div>
<nav class="menu">
<a href="javascript:;" class="menu-item active" data-target="profile-basic">
<i class="icon">👤</i>
<span>个人资料</span>
</a>
<a href="javascript:;" class="menu-item" data-target="profile-wallet">
<i class="icon">💰</i>
<span>我的钱包</span>
</a>
<a href="javascript:;" class="menu-item" data-target="profile-messages">
<i class="icon">✉️</i>
<span>我的消息</span>
</a>
<a href="javascript:;" class="menu-item" data-target="profile-notifications">
<i class="icon">🔔</i>
<span>系统通知</span>
</a>
<a href="javascript:;" class="menu-item" data-target="profile-security">
<i class="icon">🛡️</i>
<span>安全设置</span>
</a>
</nav>
</div>
<style>
.sidebar {
width: 260px;
background: #fff;
border-radius: 12px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
padding: 24px 0;
height: 100%;
}
.user-info {
text-align: center;
padding: 0 24px 24px;
border-bottom: 1px solid #f0f0f0;
margin-bottom: 24px;
display: flex;
flex-direction: column;
align-items: center;
}
.avatar-wrapper {
width: 88px;
height: 88px;
margin: 0 auto 16px;
position: relative;
}
.avatar {
width: 100%;
height: 100%;
border-radius: 50%;
object-fit: cover;
border: 3px solid #f5f5f5;
transition: all 0.3s ease;
}
.avatar:hover {
transform: scale(1.05);
}
.username {
font-size: 18px;
font-weight: 600;
color: #333;
margin: 0 0 4px;
}
.email {
font-size: 14px;
color: #666;
margin: 0;
}
.menu {
display: flex;
flex-direction: column;
padding: 0 12px;
}
.menu-item {
display: flex;
align-items: center;
padding: 12px 16px;
color: #666;
text-decoration: none;
border-radius: 8px;
margin-bottom: 4px;
transition: all 0.3s ease;
}
.menu-item:hover {
background: #f5f7fa;
color: #1677ff;
}
.menu-item.active {
background: #e6f4ff;
color: #1677ff;
font-weight: 500;
}
.icon {
font-size: 20px;
margin-right: 12px;
width: 24px;
text-align: center;
}
span {
font-size: 15px;
}
/* 响应式适配 */
@media (max-width: 768px) {
.sidebar {
width: 100%;
border-radius: 0;
box-shadow: none;
}
}
i {
font-style: normal;
}
</style>
<script>
document.addEventListener('DOMContentLoaded', function () {
// 获取当前页面路径
const currentPath = window.location.pathname;
// 移除所有active类
document.querySelectorAll('.menu-item').forEach(item => {
item.classList.remove('active');
});
// 为当前页面对应的菜单项添加active类
document.querySelectorAll('.menu-item').forEach(item => {
if (item.getAttribute('href') === currentPath) {
item.classList.add('active');
}
});
});
</script>
+86
View File
@@ -0,0 +1,86 @@
<div class="basic-info">
<div class="layui-tab">
<ul class="layui-tab-title">
<li class="layui-this">积分</li>
<li>云币</li>
</ul>
<div class="layui-tab-content">
<div class="layui-tab-item layui-show">
<p>您当前的积分余额:<span id="points-balance">0</span></p>
</div>
<div class="layui-tab-item">
<p>您当前的云币余额:<span id="cloud-coins-balance">0</span></p>
<p>云币是真钱充值后获得的平台币。</p>
</div>
</div>
</div>
</div>
<script>
layui.use(['element'], function () {
var element = layui.element;
});
</script>
<script>
// JavaScript 部分,处理 tab 切换
const tabs = document.querySelectorAll('.tab');
const contents = document.querySelectorAll('.content-item');
tabs.forEach(tab => {
tab.addEventListener('click', () => {
// 移除所有 tab 的 active 类
tabs.forEach(t => t.classList.remove('active'));
// 给当前点击的 tab 添加 active 类
tab.classList.add('active');
const target = tab.dataset.target;
// 隐藏所有内容项
contents.forEach(content => {
content.classList.remove('active');
if (content.id === target) {
content.classList.add('active');
}
});
});
});
</script>
<style>
/* 样式部分 */
.tab-container {
display: flex;
border-bottom: 1px solid #ccc;
}
.tab {
padding: 10px 20px;
cursor: pointer;
border: 1px solid transparent;
border-bottom: none;
}
.tab.active {
border-color: #ccc;
border-bottom: 1px solid white;
margin-bottom: -1px;
}
.tab-content {
padding: 20px;
border: 1px solid #ccc;
border-top: none;
}
.content-item {
display: none;
}
.content-item.active {
display: block;
}
.layui-unselect.layui-tab-bar {
display: none;
}
</style>
+661
View File
@@ -0,0 +1,661 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="__LAYUI__/css/layui.css">
<script src="__JS__/gt4.js"></script>
<script src="__LAYUI__/layui.js" charset="utf-8"></script>
<title>用户登录</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background: #f5f7fa;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
margin: 0;
padding: 20px;
}
.login-container {
width: 420px;
background: #fff;
border-radius: 12px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.05);
overflow: hidden;
}
.login-header {
padding: 40px 40px 20px;
text-align: center;
}
.login-header h2 {
font-size: 28px;
color: #333;
margin: 0;
font-weight: 600;
}
.login-header p {
color: #666;
margin: 10px 0 0;
font-size: 15px;
}
.login-form {
padding: 20px 40px 40px;
}
.layui-form-item {
margin-bottom: 25px;
}
.layui-input {
height: 45px;
line-height: 45px;
border-radius: 8px;
border: 1px solid #e4e7ed;
padding: 0 15px;
transition: all 0.3s;
}
.layui-input:focus {
border-color: #409eff;
box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.1);
}
.layui-btn {
height: 45px;
line-height: 45px;
border-radius: 8px;
font-size: 16px;
background: #409eff;
transition: all 0.3s;
}
.layui-btn:hover {
background: #66b1ff;
transform: translateY(-1px);
}
.layui-tab {
margin: 0;
}
.layui-tab-title {
border: none;
padding: 0 40px;
}
.layui-tab-title li {
font-size: 15px;
color: #666;
padding: 0 20px;
}
.layui-tab-title .layui-this {
color: #409eff;
}
.layui-tab-title .layui-this:after {
border-bottom: 2px solid #409eff;
}
.layui-tab-content {
padding: 20px 0 0;
}
.wechat-login {
text-align: center;
padding: 30px 0;
/* padding-bottom: 60px; */
}
.wechat-login img {
width: 200px;
height: 200px;
transition: transform 0.3s;
}
.wechat-login img:hover {
transform: scale(1.1);
}
.wechat-login p {
margin-top: 15px;
color: #666;
}
#qrcode-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
#qrcode-loading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
#qrcode-loading .layui-icon {
font-size: 40px;
color: #409eff;
}
#qrcode-loading p {
margin-top: 10px;
color: #666;
}
.layui-input-inline {
margin-right: 0px !important;
}
#getCode {
height: 45px;
line-height: 45px;
padding: 0 20px;
font-size: 14px;
}
.layui-input-block {
display: flex;
justify-content: space-between;
}
.container {
width: 100%;
max-width: 400px;
/* margin: 50px auto; */
text-align: center;
}
.qrcode-container {
margin: 20px 0;
}
.qrcode-container img {
max-width: 200px;
cursor: pointer;
}
.qrcode-container p {
color: #666;
font-size: 14px;
margin-top: 10px;
}
.status {
color: #666;
margin: 10px 0;
}
.error {
color: #ff4d4f;
margin: 10px 0;
}
</style>
</head>
<body>
<div class="login-container">
<div class="login-header">
<h2>欢迎登录</h2>
<p>请选择登录方式</p>
</div>
<div class="layui-tab">
<ul class="layui-tab-title">
<li lay-id="account">账密登录</li>
<!-- <li lay-id="phone">手机验证码</li> -->
<li class="layui-this" lay-id="wechat">微信登录</li>
</ul>
<div class="layui-tab-content">
<div class="layui-tab-item" id="account">
<form action="#" method="post" class="layui-form login-form">
<div class="layui-form-item">
<div class="layui-input-block" style="margin-left: 0;">
<input type="text" name="account" required lay-verify="required" placeholder="请输入用户名"
autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<div class="layui-input-block" style="margin-left: 0;">
<input type="password" name="password" required lay-verify="required"
placeholder="请输入密码" autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<div id="gt-captcha" style="width: 100%;"></div>
</div>
<div class="layui-form-item" style="display: flex;flex-direction: column;align-items: center;">
<div class="layui-input-block" style="margin-left: 0;margin-bottom: 10px;">
<button class="layui-btn layui-btn-fluid" lay-submit
lay-filter="accountLogin">登录</button>
</div>
<div style="margin-bottom: 10px;color: #aaa;">or</div>
<div>
<a href="{:url('/index/user/register')}" class="">注册</a>
</div>
</div>
</form>
</div>
<!-- <div class="layui-tab-item" id="phone">
<form action="#" method="post" class="layui-form login-form">
<div class="layui-form-item">
<div class="layui-input-block" style="margin-left: 0;">
<input type="tel" name="phone" required lay-verify="required|phone" placeholder="请输入手机号"
autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<div class="layui-input-block" style="margin-left: 0;">
<div class="layui-input-inline" style="width: calc(100% - 120px);">
<input type="text" name="code" required lay-verify="required" placeholder="请输入验证码"
autocomplete="off" class="layui-input">
</div>
<div class="layui-input-inline" style="width: 110px;">
<button type="button" class="layui-btn" id="getCode">获取验证码</button>
</div>
</div>
</div>
<div class="layui-form-item">
<div id="gt-captcha" style="width: 100%;"></div>
</div>
<div class="layui-form-item">
<div class="layui-input-block" style="margin-left: 0;">
<button class="layui-btn layui-btn-fluid" lay-submit lay-filter="phoneLogin">登录</button>
</div>
</div>
</form>
</div> -->
<div class="layui-tab-item layui-show" id="wechat">
<div class="wechat-login">
<div class="container">
<h2>微信扫码登录</h2>
<div class="qrcode-container">
<img id="qrcode" src="__IMAGES__/loading.gif" alt="微信登录二维码" onclick="reGenerateQrcode()">
<!-- <p>点击二维码可刷新</p> -->
</div>
<div id="status" class="status">正在加载二维码...</div>
<div id="error" class="error"></div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
// 全局变量
var checkLoginTimer = null;
var currentSceneStr = '';
var currentTicket = '';
var qrcodeExpireTime = 0;
var $ = layui.jquery;
// 获取微信登录二维码
function getQrcode() {
$.ajax({
url: '/index/wechat/getLoginTicket',
type: 'GET',
success: function(res) {
if (res.code === 0) {
$('#qrcode').attr('src', res.data.url);
currentSceneStr = res.data.scene_str;
currentTicket = res.data.ticket;
// 设置二维码过期时间(5分钟)
qrcodeExpireTime = new Date().getTime() + (res.data.expire_seconds * 1000);
$('#status').text('等待扫码...');
$('#error').text('');
// 开始检查登录状态
startCheckLogin();
} else {
$('#error').text('获取二维码失败:' + res.msg);
}
},
error: function() {
$('#error').text('获取二维码失败,请刷新页面重试');
}
});
}
// 开始检查登录状态
function startCheckLogin() {
if (checkLoginTimer) {
clearInterval(checkLoginTimer);
}
checkLoginTimer = setInterval(function() {
// 检查二维码是否过期
if (new Date().getTime() > qrcodeExpireTime) {
clearInterval(checkLoginTimer);
$('#status').html('二维码已过期<br>请点击刷新');
$('#qrcode').css('opacity', '0.02');
$('#status').css({
'position': 'relative',
'top': '-150px',
'font-size': '20px',
'font-weight': 'bolder'
});
return;
}
$.ajax({
url: '/index/wechat/checkLoginStatus',
type: 'POST',
data: {
scene_str: currentSceneStr,
ticket: currentTicket
},
success: function(res) {
if (res.code === 1) {
// 登录成功
clearInterval(checkLoginTimer);
$('#status').text('登录成功,正在跳转...');
// 保存用户信息到localStorage
if (res.data) {
localStorage.setItem('user_account', res.data.user_account);
localStorage.setItem('expire_time', res.data.expire_time);
localStorage.setItem('is_auto_login', res.data.is_auto_login);
}
// 设置cookie
document.cookie = `user_account=${res.data.user_account}; path=/; max-age=${7*24*3600}`;
document.cookie = `user_avatar=${res.data.avatar}; path=/; max-age=${7*24*3600}`;
document.cookie = `user_name=${res.data.name}; path=/; max-age=${7*24*3600}`;
document.cookie = `open_id=${res.data.openid}; path=/; max-age=${7*24*3600}`;
// 跳转到首页或其他页面
setTimeout(function() {
window.location.href = '/';
}, 1000);
} else if (res.code === 0) {
// 更新状态信息
$('#status').text(res.msg);
}
},
error: function() {
$('#error').text('检查登录状态失败,请重试');
}
});
}, 2000); // 每2秒检查一次
}
// 重新生成二维码的全局函数
function reGenerateQrcode() {
if (!currentSceneStr) {
getQrcode();
return;
}
// 清除之前的定时器
if (checkLoginTimer) {
clearInterval(checkLoginTimer);
checkLoginTimer = null;
}
// 恢复二维码和状态文本的样式
$('#qrcode').css('opacity', '1');
$('#status').css({
'position': 'static',
'top': 'auto',
'font-size': '14px',
'font-weight': 'normal'
});
$.ajax({
url: '/index/wechat/reGenerateQrcode',
type: 'POST',
data: {
scene_str: currentSceneStr
},
success: function(res) {
if (res.code === 0) {
$('#qrcode').attr('src', res.data.url);
currentSceneStr = res.data.scene_str;
currentTicket = res.data.ticket;
// 设置新的二维码过期时间(5分钟)
qrcodeExpireTime = new Date().getTime() + (res.data.expire_seconds * 1000);
$('#status').text('等待扫码...');
$('#error').text('');
// 开始检查登录状态
startCheckLogin();
} else {
$('#error').text('刷新二维码失败:' + res.msg);
}
},
error: function() {
$('#error').text('刷新二维码失败,请重试');
}
});
}
layui.use(['form', 'element', 'jquery'], function () {
var form = layui.form;
var element = layui.element;
var layer = layui.layer;
// 页面加载完成后获取二维码
$(document).ready(function() {
getQrcode();
});
// 检查极验验证是否开启
<?php if ($config['geetest_open'] == 1): ?>
// 初始化极验验证
var handler = function (captchaObj) {
// 将验证码渲染到指定容器
captchaObj.appendTo('#gt-captcha');
// 账密登录表单提交
form.on('submit(accountLogin)', function (data) {
var validate = captchaObj.getValidate();
if (!validate) {
layer.msg('请完成验证码验证', {
icon: 2,
time: 2000
});
return false;
}
data.field.geetest_challenge = validate.geetest_challenge;
data.field.geetest_validate = validate.geetest_validate;
data.field.geetest_seccode = validate.geetest_seccode;
$.ajax({
url: '{:url("index/user/login")}',
type: 'POST',
data: data.field,
dataType: 'json',
success: function (res) {
if (res.code === 0) {
// 存储登录数据,设置7天过期
var expireTime = new Date().getTime() + 7 * 24 * 60 * 60 * 1000;
localStorage.setItem('user_account', data.field.account);
localStorage.setItem('user_password', btoa(data.field.password));
localStorage.setItem('expire_time', expireTime);
// 添加登录状态标记
localStorage.setItem('is_auto_login', 'true');
layer.msg('登录成功', {
icon: 1,
time: 2000,
shade: 0.3
}, function () {
window.location.href = '{:url("/")}';
});
} else {
layer.msg(res.msg, {
icon: 2,
time: 2000
});
}
}
});
return false;
});
// 手机验证码登录表单提交
form.on('submit(phoneLogin)', function (data) {
var validate = captchaObj.getValidate();
if (!validate) {
layer.msg('请完成验证码验证', {
icon: 2,
time: 2000
});
return false;
}
data.field.geetest_challenge = validate.geetest_challenge;
data.field.geetest_validate = validate.geetest_validate;
data.field.geetest_seccode = validate.geetest_seccode;
$.ajax({
url: '{:url("index/user/login")}',
type: 'POST',
data: data.field,
dataType: 'json',
success: function (res) {
if (res.code === 0) {
layer.msg('登录成功', {
icon: 1,
time: 2000,
shade: 0.3
}, function () {
window.location.href = '{:url("/")}';
});
} else {
layer.msg(res.msg, {
icon: 2,
time: 2000
});
}
}
});
return false;
});
// 点击获取验证码按钮时验证
var getCodeBtn = document.getElementById('getCode');
if (getCodeBtn) {
getCodeBtn.addEventListener('click', function () {
console.log('获取验证码');
});
}
};
// 直接使用配置初始化极验验证,并添加错误处理
initGeetest4({
captchaId: '{$config[\'geetest_id\']}',
offline: false,
new_captcha: true
}, handler, function (error) {
console.error('极验验证初始化失败:', error);
layer.msg('验证码初始化失败,请刷新页面重试', {
icon: 2,
time: 2000
});
});
<?php else: ?>
// 极验验证关闭,移除验证码容器
$('#gt-captcha').remove();
// 移除表单提交时的验证码验证逻辑
form.on('submit(accountLogin)', function (data) {
$.ajax({
url: '{:url("index/user/login")}',
type: 'POST',
data: data.field,
dataType: 'json',
success: function (res) {
if (res.code === 0) {
// 存储登录数据,设置7天过期
var expireTime = new Date().getTime() + 7 * 24 * 60 * 60 * 1000;
localStorage.setItem('user_account', data.field.account);
localStorage.setItem('user_password', btoa(data.field.password));
localStorage.setItem('expire_time', expireTime);
// 添加登录状态标记
localStorage.setItem('is_auto_login', 'true');
layer.msg('登录成功', {
icon: 1,
time: 2000,
shade: 0.3
}, function () {
window.location.href = '{:url("/")}';
});
} else {
layer.msg(res.msg, {
icon: 2,
time: 2000
});
}
}
});
return false;
});
form.on('submit(phoneLogin)', function (data) {
$.ajax({
url: '{:url("index/user/login")}',
type: 'POST',
data: data.field,
dataType: 'json',
success: function (res) {
if (res.code === 0) {
layer.msg('登录成功', {
icon: 1,
time: 2000,
shade: 0.3
}, function () {
window.location.href = '{:url("/")}';
});
} else {
layer.msg(res.msg, {
icon: 2,
time: 2000
});
}
}
});
return false;
});
// 移除获取验证码按钮的验证码验证逻辑
var getCodeBtn = document.getElementById('getCode');
if (getCodeBtn) {
getCodeBtn.addEventListener('click', function () {
console.log('获取验证码');
});
}
<?php endif; ?>
// 页面加载时检查是否有保存的登录数据
$(function () {
var expireTime = localStorage.getItem('expire_time');
var isAutoLogin = localStorage.getItem('is_auto_login');
if (expireTime && new Date().getTime() < expireTime && isAutoLogin === 'true') {
// 只填充账号,不填充密码
$('input[name="account"]').val(localStorage.getItem('user_account'));
} else {
// 如果过期或未开启自动登录,清除数据
localStorage.removeItem('user_account');
localStorage.removeItem('user_password');
localStorage.removeItem('expire_time');
localStorage.removeItem('is_auto_login');
}
});
});
</script>
<style>
.layui-form-item a {
color: #409eff;
}
.layui-form-item a:hover {
color: rgb(58, 125, 196);
}
</style>
</body>
</html>
+175
View File
@@ -0,0 +1,175 @@
{include file="component/head" /}
{include file="component/header" /}
<div class="profile-container">
<div class="profile-sidebar">
<div class="menu">
<div class="menu-item active" data-target="profile-basic">
<i class="layui-icon layui-icon-user"></i>
<span>基本资料</span>
</div>
<div class="menu-item" data-target="profile-wallet">
<i class="layui-icon layui-icon-wallet"></i>
<span>我的钱包</span>
</div>
<div class="menu-item" data-target="profile-messages">
<i class="layui-icon layui-icon-message"></i>
<span>我的消息</span>
</div>
<div class="menu-item" data-target="profile-notifications">
<i class="layui-icon layui-icon-notice"></i>
<span>系统通知</span>
</div>
<div class="menu-item" data-target="profile-security">
<i class="layui-icon layui-icon-password"></i>
<span>安全设置</span>
</div>
</div>
</div>
<div class="profile-main">
<div class="content-area">
<!-- 个人资料 -->
<div id="profile-basic" class="content-section active">
{include file="user/component/basic" /}
</div>
<!-- 我的钱包 -->
<div id="profile-wallet" class="content-section">
{include file="user/component/wallet" /}
</div>
<!-- 我的消息 -->
<div id="profile-messages" class="content-section">
{include file="user/component/messages" /}
</div>
<!-- 系统通知 -->
<div id="profile-notifications" class="content-section">
{include file="user/component/notifications" /}
</div>
<!-- 安全设置 -->
<div id="profile-security" class="content-section">
{include file="user/component/security" /}
</div>
</div>
</div>
</div>
<style>
.profile-container {
display: flex;
max-width: 1200px;
margin: 40px auto;
gap: 24px;
padding: 0 20px;
}
.profile-sidebar {
width: 260px;
flex-shrink: 0;
}
.menu {
background: #fff;
border-radius: 12px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
padding: 16px 0;
}
.menu-item {
padding: 16px 24px;
cursor: pointer;
display: flex;
align-items: center;
gap: 12px;
color: #666;
transition: all 0.3s;
}
.menu-item:hover {
color: #1677ff;
background: #f5f5f5;
}
.menu-item.active {
color: #1677ff;
background: #e6f4ff;
border-right: 3px solid #1677ff;
}
.menu-item .layui-icon {
font-size: 18px;
}
.profile-main {
flex: 1;
background: #fff;
border-radius: 12px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
padding: 32px;
}
.content-section {
display: none;
}
.content-section.active {
display: block;
}
/* 响应式适配 */
@media (max-width: 768px) {
.profile-container {
flex-direction: column;
margin: 20px auto;
}
.profile-sidebar {
width: 100%;
}
.menu {
display: flex;
overflow-x: auto;
padding: 8px;
}
.menu-item {
flex-shrink: 0;
border-right: none;
border-bottom: 3px solid transparent;
}
.menu-item.active {
border-right: none;
border-bottom: 3px solid #1677ff;
}
.profile-main {
padding: 20px;
}
}
</style>
<script>
layui.use(['jquery'], function(){
var $ = layui.jquery;
// 菜单切换
$('.menu-item').on('click', function(){
var target = $(this).data('target');
// 移除所有active类
$('.menu-item').removeClass('active');
$('.content-section').removeClass('active');
// 添加active类
$(this).addClass('active');
$('#' + target).addClass('active');
});
});
</script>
{include file="component/foot" /}
+240
View File
@@ -0,0 +1,240 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="__LAYUI__/css/layui.css">
<script src="__LAYUI__/layui.js" charset="utf-8"></script>
<title>用户注册</title>
<style>
/* 保持原有样式不变 */
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background: #f5f7fa;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
margin: 0;
padding: 20px;
}
.register-container {
width: 420px;
background: #fff;
border-radius: 12px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.05);
overflow: hidden;
}
.register-header {
padding: 40px 40px 20px;
text-align: center;
}
.register-header h2 {
font-size: 28px;
color: #333;
margin: 0;
font-weight: 600;
}
.register-header p {
color: #666;
margin: 10px 0 0;
font-size: 15px;
}
.register-form {
padding: 20px 40px 40px;
}
.layui-form-item {
margin-bottom: 25px;
}
.layui-input {
height: 45px;
line-height: 45px;
border-radius: 8px;
border: 1px solid #e4e7ed;
padding: 0 15px;
transition: all 0.3s;
}
.layui-input:focus {
border-color: #409eff;
box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.1);
}
.layui-btn {
height: 45px;
line-height: 45px;
border-radius: 8px;
font-size: 16px;
background: #409eff;
transition: all 0.3s;
}
.layui-btn:hover {
background: #66b1ff;
transform: translateY(-1px);
}
.layui-input-inline {
margin-right: 0px !important;
}
#getCode {
height: 45px;
line-height: 45px;
padding: 0 20px;
font-size: 14px;
}
.layui-input-block {
display: flex;
justify-content: space-between;
}
.login-link {
text-align: center;
margin-top: 20px;
color: #666;
}
.login-link a {
color: #409eff;
text-decoration: none;
}
.login-link a:hover {
text-decoration: underline;
}
</style>
</head>
<body>
<div class="register-container">
<div class="register-header">
<h2>用户注册</h2>
<p>创建您的账号</p>
</div>
<form action="#" method="post" class="layui-form register-form">
<div class="layui-form-item">
<div class="layui-input-block" style="margin-left: 0;">
<input type="account" name="account" required lay-verify="required|account" placeholder="请输入邮箱"
autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<div class="layui-input-block" style="margin-left: 0;">
<div class="layui-input-inline" style="width: calc(100% - 120px);">
<input type="text" name="code" required lay-verify="required" placeholder="请输入验证码"
autocomplete="off" class="layui-input">
</div>
<div class="layui-input-inline" style="width: 110px;">
<button type="button" class="layui-btn" id="getCode">获取验证码</button>
</div>
</div>
</div>
<div class="layui-form-item">
<div class="layui-input-block" style="margin-left: 0;">
<input type="password" name="password" required lay-verify="required" placeholder="请输入密码"
autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<div class="layui-input-block" style="margin-left: 0;">
<input type="password" name="repassword" required lay-verify="required" placeholder="请确认密码"
autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<div class="layui-input-block" style="margin-left: 0;">
<button class="layui-btn layui-btn-fluid" lay-submit lay-filter="register">注册</button>
</div>
</div>
<div class="login-link">
已有账号?<a href="{:url('/index/user/login')}">立即登录</a>
</div>
</form>
</div>
<script>
layui.use(['form', 'layer'], function () {
var form = layui.form;
var layer = layui.layer;
var $ = layui.$;
// 注册表单提交
form.on('submit(register)', function (data) {
$.ajax({
url: '{:url("index/user/register")}',
type: 'POST',
data: data.field,
success: function (res) {
if (res.code === 0) {
layer.msg('注册成功', {
icon: 1,
time: 1000
}, function () {
window.location.href = '{:url("index/user/login")}';
});
} else {
layer.msg(res.msg);
}
},
error: function () {
layer.msg('网络错误,请稍后重试');
}
});
return false;
});
// 获取验证码按钮点击事件
document.getElementById('getCode').addEventListener('click', function () {
var account = document.querySelector('input[name="account"]').value;
if (!account) {
layer.msg('请先输入邮箱地址');
return;
}
// 发送邮箱验证码请求
$.ajax({
url: '{:url("index/user/sendEmailCode")}',
type: 'POST',
data: { account: account },
dataType: 'json', // 添加这行
success: function (res) {
if (res.code === 0) {
layer.msg('验证码已发送,请查收邮件');
// 禁用按钮60秒
var btn = document.getElementById('getCode');
var countdown = 60;
btn.disabled = true;
var timer = setInterval(function () {
if (countdown > 0) {
btn.innerHTML = countdown + '秒后重试';
countdown--;
} else {
btn.disabled = false;
btn.innerHTML = '获取验证码';
clearInterval(timer);
}
}, 1000);
} else {
layer.msg(res.msg);
}
},
error: function (xhr, status, error) {
console.log(xhr.responseText); // 添加调试信息
layer.msg('网络错误,请稍后重试');
}
});
});
});
</script>
</body>
</html>
+91
View File
@@ -0,0 +1,91 @@
{include file="component/head" /}
<form class="layui-form" action="/index/user/updatePassword" method="post">
<div class="layui-form-item">
<label class="layui-form-label">旧密码</label>
<div class="layui-input-block">
<input type="password" name="old_password" required lay-verify="required" placeholder="请输入旧密码"
autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">新密码</label>
<div class="layui-input-block">
<input type="password" name="new_password" required lay-verify="required|password" placeholder="请输入新密码"
autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">确认密码</label>
<div class="layui-input-block">
<input type="password" name="confirm_password" required lay-verify="required|confirmPassword"
placeholder="请再次输入新密码" autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<div class="layui-input-block">
<button class="layui-btn" lay-submit lay-filter="updatePassword">立即修改</button>
<button type="reset" class="layui-btn layui-btn-primary">重置</button>
</div>
</div>
</form>
<script>
layui.use(['form', 'layer'], function () {
var form = layui.form;
var layer = layui.layer;
var $ = layui.$;
// 自定义验证规则
form.verify({
password: [
/^[\S]{6,20}$/,
'密码长度必须在6-20个字符之间'
],
confirmPassword: function (value) {
var password = document.querySelector('input[name=new_password]').value;
if (value !== password) {
return '两次输入的密码不一致';
}
}
});
// 监听提交
form.on('submit(updatePassword)', function (data) {
// 显示加载中
var loadIndex = layer.load(2);
$.ajax({
url: '/index/user/updatePassword',
type: 'POST',
data: data.field,
dataType: 'json',
success: function (res) {
layer.close(loadIndex);
if (res.code === 0) {
layer.msg(res.msg, {
icon: 1,
time: 1000,
end: function() {
// 设置跳转标记
parent.window.needRedirect = true;
// 关闭当前弹窗
var index = parent.layer.getFrameIndex(window.name);
parent.layer.close(index);
}
});
} else {
layer.msg(res.msg, { icon: 2 });
}
},
error: function() {
layer.close(loadIndex);
layer.msg('请求失败,请重试', { icon: 2 });
}
});
return false; // 阻止表单默认提交
});
});
</script>