修复token验证和左侧表单问题

This commit is contained in:
李志强 2025-08-19 17:48:53 +08:00
parent 1ceaeb9214
commit 2d44b8764d
39 changed files with 1461 additions and 554 deletions

View File

@ -22,6 +22,33 @@ use think\Model;
class AdminSysMenu extends Model
{
// 设置表名
protected $name = 'menu';
// 设置主键
protected $pk = 'smid';
// 自动时间戳
protected $autoWriteTimestamp = true;
protected $createTime = 'create_time';
protected $updateTime = 'update_time';
// 字段类型转换
protected $type = [
'smid' => 'integer',
'parent_id' => 'integer',
'type' => 'integer',
'sort' => 'integer',
'status' => 'integer',
'create_time' => 'integer',
'update_time' => 'integer'
];
// 允许写入的字段
protected $allowField = [
'parent_id', 'type', 'label', 'icon_class', 'sort', 'src', 'status'
];
/**
* 获取菜单树形结构
* @return array
@ -30,28 +57,36 @@ class AdminSysMenu extends Model
{
// 获取所有启用的菜单
$menus = self::where('status', 1)
->order('type', 'asc')
->order('sort', 'desc')
->order('smid', 'asc')
->select()
->toArray();
$menuTree = [];
return self::buildTree($menus);
}
// 先处理所有父菜单
/**
* 构建树形结构
* @param array $menus 菜单数组
* @param int $parent_id 父级ID
* @return array
*/
public static function buildTree($menus, $parent_id = 0)
{
$tree = [];
foreach ($menus as $menu) {
if ($menu['parent_id'] == 0) {
$menuTree[$menu['smid']] = $menu;
$menuTree[$menu['smid']]['children'] = [];
if ($menu['parent_id'] == $parent_id) {
$children = self::buildTree($menus, $menu['smid']);
if ($children) {
$menu['children'] = $children;
} else {
$menu['children'] = [];
}
$tree[] = $menu;
}
}
// 再处理子菜单
foreach ($menus as $menu) {
if ($menu['parent_id'] != 0 && isset($menuTree[$menu['parent_id']])) {
$menuTree[$menu['parent_id']]['children'][] = $menu;
}
}
return array_values($menuTree);
return $tree;
}
}

View File

@ -22,4 +22,28 @@ use think\Model;
class AdminUser extends Model
{
// 设置表名
protected $name = 'admin_user';
// 设置主键
protected $pk = 'uid';
// 自动时间戳
protected $autoWriteTimestamp = true;
protected $createTime = 'create_time';
protected $updateTime = 'update_time';
// 字段类型转换
protected $type = [
'uid' => 'integer',
'sex' => 'integer',
'status' => 'integer',
'create_time' => 'integer',
'update_time' => 'integer'
];
// 允许写入的字段
protected $allowField = [
'account', 'password', 'name', 'avatar', 'phone', 'sex', 'qq', 'wechat', 'status'
];
}

View File

@ -2,8 +2,8 @@
namespace app\api\controller;
use app\api\controller\BaseController;
use app\admin\model\AdminUser;
use app\index\model\AdminUserGroup;
use app\api\model\AdminUser;
use app\api\model\AdminSysMenu;
use think\facade\Log;
use think\facade\Cache;
@ -12,9 +12,9 @@ use think\Response;
class AdminController extends BaseController
{
/**
* 生成用户token
* 生成管理员token
*
* @param int $userId 用户ID
* @param int $userId 管理员ID
* @return string
*/
private function generateToken($userId)
@ -31,7 +31,27 @@ class AdminController extends BaseController
}
/**
* 用户登录接口
* 从token中获取管理员ID
*
* @param string $token
* @return int|null
*/
private function getUserIdFromToken($token)
{
try {
$data = json_decode(base64_decode($token), true);
if ($data && isset($data['user_id'])) {
return $data['user_id'];
}
} catch (\Exception $e) {
return null;
}
return null;
}
/**
* 管理员登录接口
*
* @return \think\Response
*/
@ -58,10 +78,10 @@ class AdminController extends BaseController
return json(['code' => 1, 'msg' => $validate->getError()]);
}
// 查询用户
// 查询管理员
$user = AdminUser::where('account', $data['account'])->find();
if (!$user) {
return json(['code' => 1, 'msg' => '用户不存在']);
return json(['code' => 1, 'msg' => '管理员不存在']);
}
// 验证密码
@ -69,23 +89,23 @@ class AdminController extends BaseController
return json(['code' => 1, 'msg' => '密码错误']);
}
// 生成JWT token这里使用简单的token实际项目中建议使用JWT
$token = $this->generateToken($user->id);
// 生成token
$token = $this->generateToken($user->uid);
// 将token存储到缓存中设置过期时间
Cache::set('user_token_' . $user->id, $token, 7 * 24 * 3600);
Cache::set('admin_token_' . $user->uid, $token, 7 * 24 * 3600);
// 记录登录日志
Log::record('用户登录成功:' . $user->account, 'info');
Log::record('管理员登录成功:' . $user->account, 'info');
// 返回用户信息和token
// 返回管理员信息和token
return json([
'code' => 0,
'msg' => '登录成功',
'data' => [
'token' => $token,
'user_info' => [
'id' => $user->id,
'id' => $user->uid,
'account' => $user->account,
'name' => $user->name,
'avatar' => $user->avatar ?? '/static/images/avatar.png',
@ -99,8 +119,201 @@ class AdminController extends BaseController
]);
} catch (\Exception $e) {
Log::record('登录失败:' . $e->getMessage(), 'error');
Log::record('管理员登录失败:' . $e->getMessage(), 'error');
return json(['code' => 1, 'msg' => '登录失败:' . $e->getMessage()]);
}
}
/**
* 退出登录接口
*
* @return \think\Response
*/
public function logout()
{
try {
$token = $this->request->header('Authorization');
if ($token) {
// 去掉Bearer前缀
if (strpos($token, 'Bearer ') === 0) {
$token = substr($token, 7);
}
// 从token中获取管理员ID
$userId = $this->getUserIdFromToken($token);
if ($userId) {
// 清除token缓存
Cache::delete('admin_token_' . $userId);
}
}
Log::record('管理员退出登录', 'info');
// 增加前端刷新指示
return json([
'code' => 0,
'msg' => '退出成功',
'refresh' => true // 前端可根据此字段判断是否需要刷新
]);
} catch (\Exception $e) {
Log::record('退出登录失败:' . $e->getMessage(), 'error');
return json(['code' => 1, 'msg' => '退出失败:' . $e->getMessage()]);
}
}
/**
* 获取管理员信息接口
*
* @return \think\Response
*/
public function info()
{
try {
$token = $this->request->header('Authorization');
if (!$token) {
return json(['code' => 1, 'msg' => '请先登录']);
}
// 去掉Bearer前缀
if (strpos($token, 'Bearer ') === 0) {
$token = substr($token, 7);
}
$userId = $this->getUserIdFromToken($token);
if (!$userId) {
return json(['code' => 1, 'msg' => 'token无效']);
}
// 验证token是否在缓存中
$cachedToken = Cache::get('admin_token_' . $userId);
if (!$cachedToken || $cachedToken !== $token) {
return json(['code' => 1, 'msg' => 'token已过期']);
}
// 获取管理员信息
$user = AdminUser::where('uid', $userId)->find();
if (!$user) {
return json(['code' => 1, 'msg' => '管理员不存在']);
}
return json([
'code' => 0,
'msg' => '获取成功',
'data' => [
'id' => $user->uid,
'account' => $user->account,
'name' => $user->name,
'avatar' => $user->avatar ?? '/static/images/avatar.png',
'phone' => $user->phone ?? '',
'sex' => $user->sex ?? 0,
'qq' => $user->qq ?? '',
'wechat' => $user->wechat ?? '',
'create_time' => $user->create_time
]
]);
} catch (\Exception $e) {
Log::record('获取管理员信息失败:' . $e->getMessage(), 'error');
return json(['code' => 1, 'msg' => '获取管理员信息失败:' . $e->getMessage()]);
}
}
/**
* 修改密码接口
*
* @return \think\Response
*/
public function changePassword()
{
if (!$this->request->isPost()) {
return json(['code' => 1, 'msg' => '请求方法错误']);
}
try {
$token = $this->request->header('Authorization');
if (!$token) {
return json(['code' => 1, 'msg' => '请先登录']);
}
// 去掉Bearer前缀
if (strpos($token, 'Bearer ') === 0) {
$token = substr($token, 7);
}
$userId = $this->getUserIdFromToken($token);
if (!$userId) {
return json(['code' => 1, 'msg' => 'token无效']);
}
// 验证token是否在缓存中
$cachedToken = Cache::get('admin_token_' . $userId);
if (!$cachedToken || $cachedToken !== $token) {
return json(['code' => 1, 'msg' => 'token已过期']);
}
$data = $this->request->post();
// 验证数据
$validate = validate([
'oldPassword' => 'require',
'newPassword' => 'require|min:6'
], [
'oldPassword.require' => '原密码不能为空',
'newPassword.require' => '新密码不能为空',
'newPassword.min' => '新密码长度不能少于6位'
]);
if (!$validate->check($data)) {
return json(['code' => 1, 'msg' => $validate->getError()]);
}
// 获取管理员信息
$user = AdminUser::where('uid', $userId)->find();
if (!$user) {
return json(['code' => 1, 'msg' => '管理员不存在']);
}
// 验证原密码
if ($user->password !== md5($data['oldPassword'])) {
return json(['code' => 1, 'msg' => '原密码错误']);
}
// 更新密码
$user->password = md5($data['newPassword']);
$user->save();
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 \think\Response
*/
public function menus()
{
try {
// 取消token验证直接获取菜单数据
$menus = AdminSysMenu::getMenuTree();
return json([
'code' => 0,
'msg' => '获取成功',
'data' => $menus
]);
} catch (\Exception $e) {
Log::record('获取菜单失败:' . $e->getMessage(), 'error');
return json(['code' => 1, 'msg' => '获取菜单失败:' . $e->getMessage()]);
}
}
}

View File

@ -1,402 +0,0 @@
<?php
/**
* 商业使用授权协议
*
* Copyright (c) 2025 [云泽网]. 保留所有权利.
*
* 本软件仅供评估使用。任何商业用途必须获得书面授权许可。
* 未经授权商业使用本软件属于侵权行为,将承担法律责任。
*
* 授权购买请联系: 357099073@qq.com
* 官方网站: https://www.yunzer.cn
*
* 评估用户须知:
* 1. 禁止移除版权声明
* 2. 禁止用于生产环境
* 3. 禁止转售或分发
*/
namespace app\api\controller;
use app\api\controller\BaseController;
use app\index\model\User\Users as Users;
use app\index\model\User\UsersGroup as UsersGroup;
use think\facade\Log;
use think\facade\Cache;
use think\Response;
class UserController extends BaseController
{
/**
* 用户登录接口
*
* @return \think\Response
*/
public function login()
{
if (!$this->request->isPost()) {
return json(['code' => 1, 'msg' => '请求方法错误']);
}
$data = $this->request->post();
try {
// 验证数据
$validate = validate([
'account' => 'require|email',
'password' => 'require'
], [
'account.require' => '账户不能为空',
'account.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' => '密码错误']);
}
// 生成JWT token这里使用简单的token实际项目中建议使用JWT
$token = $this->generateToken($user->id);
// 将token存储到缓存中设置过期时间
Cache::set('user_token_' . $user->id, $token, 7 * 24 * 3600);
// 记录登录日志
Log::record('用户登录成功:' . $user->account, 'info');
// 返回用户信息和token
return json([
'code' => 0,
'msg' => '登录成功',
'data' => [
'token' => $token,
'user_info' => [
'id' => $user->id,
'account' => $user->account,
'name' => $user->name,
'avatar' => $user->avatar ?? '/static/images/avatar.png',
'phone' => $user->phone ?? '',
'sex' => $user->sex ?? 0,
'qq' => $user->qq ?? '',
'wechat' => $user->wechat ?? '',
'create_time' => $user->create_time
]
]
]);
} catch (\Exception $e) {
Log::record('登录失败:' . $e->getMessage(), 'error');
return json(['code' => 1, 'msg' => '登录失败:' . $e->getMessage()]);
}
}
/**
* 用户注册接口
*
* @return \think\Response
*/
public function register()
{
if (!$this->request->isPost()) {
return json(['code' => 1, 'msg' => '请求方法错误']);
}
$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::get('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::delete('email_code_' . $data['account']);
return json(['code' => 0, 'msg' => '注册成功']);
} catch (\Exception $e) {
return json(['code' => 1, 'msg' => '注册失败:' . $e->getMessage()]);
}
}
/**
* 退出登录接口
*
* @return \think\Response
*/
public function logout()
{
try {
$token = $this->request->header('Authorization');
if ($token) {
// 从token中获取用户ID
$userId = $this->getUserIdFromToken($token);
if ($userId) {
// 清除token缓存
Cache::delete('user_token_' . $userId);
}
}
Log::record('用户退出登录', 'info');
return json(['code' => 0, 'msg' => '退出成功']);
} catch (\Exception $e) {
Log::record('退出登录失败:' . $e->getMessage(), 'error');
return json(['code' => 1, 'msg' => '退出失败:' . $e->getMessage()]);
}
}
/**
* 获取用户信息接口
*
* @return \think\Response
*/
public function getUserInfo()
{
try {
$token = $this->request->header('Authorization');
if (!$token) {
return json(['code' => 1, 'msg' => '请先登录']);
}
$userId = $this->getUserIdFromToken($token);
if (!$userId) {
return json(['code' => 1, 'msg' => 'token无效']);
}
// 验证token是否在缓存中
$cachedToken = Cache::get('user_token_' . $userId);
if (!$cachedToken || $cachedToken !== $token) {
return json(['code' => 1, 'msg' => 'token已过期']);
}
// 获取用户信息
$user = Users::find($userId);
if (!$user) {
return json(['code' => 1, 'msg' => '用户不存在']);
}
return json([
'code' => 0,
'msg' => '获取成功',
'data' => [
'id' => $user->id,
'account' => $user->account,
'name' => $user->name,
'avatar' => $user->avatar ?? '/static/images/avatar.png',
'phone' => $user->phone ?? '',
'sex' => $user->sex ?? 0,
'qq' => $user->qq ?? '',
'wechat' => $user->wechat ?? '',
'create_time' => $user->create_time
]
]);
} catch (\Exception $e) {
return json(['code' => 1, 'msg' => '获取用户信息失败:' . $e->getMessage()]);
}
}
/**
* 发送邮箱验证码接口
*
* @return \think\Response
*/
public function sendEmailCode()
{
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);
// 这里应该调用邮件发送服务
// 为了演示,我们直接返回成功
// 实际项目中需要实现邮件发送逻辑
// 将验证码存入缓存有效期5分钟
Cache::set('email_code_' . $email, $code, 300);
return json(['code' => 0, 'msg' => '验证码已发送']);
}
/**
* 修改密码接口
*
* @return \think\Response
*/
public function updatePassword()
{
if (!$this->request->isPost()) {
return json(['code' => 1, 'msg' => '请求方法错误']);
}
try {
$token = $this->request->header('Authorization');
if (!$token) {
return json(['code' => 1, 'msg' => '请先登录']);
}
$userId = $this->getUserIdFromToken($token);
if (!$userId) {
return json(['code' => 1, 'msg' => 'token无效']);
}
$data = $this->request->post();
// 验证数据
$validate = validate([
'old_password' => 'require',
'new_password' => 'require|min:6|max:20',
'confirm_password' => 'require|confirm:new_password'
], [
'old_password.require' => '旧密码不能为空',
'new_password.require' => '新密码不能为空',
'new_password.min' => '新密码长度不能小于6个字符',
'new_password.max' => '新密码长度不能超过20个字符',
'confirm_password.require' => '确认密码不能为空',
'confirm_password.confirm' => '两次输入的密码不一致'
]);
if (!$validate->check($data)) {
return json(['code' => 1, 'msg' => $validate->getError()]);
}
// 获取用户信息
$user = Users::find($userId);
if (!$user) {
return json(['code' => 1, 'msg' => '用户不存在']);
}
// 验证旧密码
if ($user->password !== md5($data['old_password'])) {
return json(['code' => 1, 'msg' => '旧密码错误']);
}
// 更新密码
$user->password = md5($data['new_password']);
$user->update_time = time();
if ($user->save()) {
// 清除token要求重新登录
Cache::delete('user_token_' . $userId);
return json(['code' => 0, 'msg' => '密码修改成功,请重新登录']);
} else {
return json(['code' => 1, 'msg' => '密码修改失败']);
}
} catch (\Exception $e) {
return json(['code' => 1, 'msg' => '密码修改失败:' . $e->getMessage()]);
}
}
/**
* 生成简单的token
*
* @param int $userId
* @return string
*/
private function generateToken($userId)
{
$data = [
'user_id' => $userId,
'timestamp' => time(),
'random' => mt_rand(100000, 999999)
];
return base64_encode(json_encode($data));
}
/**
* 从token中获取用户ID
*
* @param string $token
* @return int|null
*/
private function getUserIdFromToken($token)
{
try {
$data = json_decode(base64_decode($token), true);
if ($data && isset($data['user_id'])) {
return $data['user_id'];
}
} catch (\Exception $e) {
return null;
}
return null;
}
/**
* 生成随机用户名
*
* @return string
*/
private function generateRandomName()
{
return '云朵_' . mt_rand(100000, 999999);
}
}

View File

@ -0,0 +1,25 @@
<?php
/**
* 商业使用授权协议
*
* Copyright (c) 2025 [云泽网]. 保留所有权利.
*
* 本软件仅供评估使用。任何商业用途必须获得书面授权许可。
* 未经授权商业使用本软件属于侵权行为,将承担法律责任。
*
* 授权购买请联系: 357099073@qq.com
* 官方网站: https://www.yunzer.cn
*
* 评估用户须知:
* 1. 禁止移除版权声明
* 2. 禁止用于生产环境
* 3. 禁止转售或分发
*/
namespace app\api\model;
use think\Model;
class AdminConfig extends Model
{
}

View File

@ -0,0 +1,92 @@
<?php
/**
* 商业使用授权协议
*
* Copyright (c) 2025 [云泽网]. 保留所有权利.
*
* 本软件仅供评估使用。任何商业用途必须获得书面授权许可。
* 未经授权商业使用本软件属于侵权行为,将承担法律责任。
*
* 授权购买请联系: 357099073@qq.com
* 官方网站: https://www.yunzer.cn
*
* 评估用户须知:
* 1. 禁止移除版权声明
* 2. 禁止用于生产环境
* 3. 禁止转售或分发
*/
namespace app\api\model;
use think\Model;
class AdminSysMenu extends Model
{
// 设置表名
protected $name = 'admin_sys_menu';
// 设置主键
protected $pk = 'smid';
// 自动时间戳
protected $autoWriteTimestamp = true;
protected $createTime = 'create_time';
protected $updateTime = 'update_time';
// 字段类型转换
protected $type = [
'smid' => 'integer',
'parent_id' => 'integer',
'type' => 'integer',
'sort' => 'integer',
'status' => 'integer',
'create_time' => 'integer',
'update_time' => 'integer'
];
// 允许写入的字段
protected $allowField = [
'parent_id', 'type', 'label', 'icon_class', 'sort', 'src', 'status'
];
/**
* 获取菜单树形结构
* @return array
*/
public static function getMenuTree()
{
// 获取所有启用的菜单
$menus = self::where('status', 1)
->order('sort', 'desc')
->order('smid', 'asc')
->select()
->toArray();
return self::buildTree($menus);
}
/**
* 构建树形结构
* @param array $menus 菜单数组
* @param int $parent_id 父级ID
* @return array
*/
public static function buildTree($menus, $parent_id = 0)
{
$tree = [];
foreach ($menus as $menu) {
if ($menu['parent_id'] == $parent_id) {
$children = self::buildTree($menus, $menu['smid']);
if ($children) {
$menu['children'] = $children;
} else {
$menu['children'] = [];
}
$tree[] = $menu;
}
}
return $tree;
}
}

View File

@ -0,0 +1,25 @@
<?php
/**
* 商业使用授权协议
*
* Copyright (c) 2025 [云泽网]. 保留所有权利.
*
* 本软件仅供评估使用。任何商业用途必须获得书面授权许可。
* 未经授权商业使用本软件属于侵权行为,将承担法律责任。
*
* 授权购买请联系: 357099073@qq.com
* 官方网站: https://www.yunzer.cn
*
* 评估用户须知:
* 1. 禁止移除版权声明
* 2. 禁止用于生产环境
* 3. 禁止转售或分发
*/
namespace app\api\model;
use think\Model;
class AdminUser extends Model
{
}

View File

@ -0,0 +1,25 @@
<?php
/**
* 商业使用授权协议
*
* Copyright (c) 2025 [云泽网]. 保留所有权利.
*
* 本软件仅供评估使用。任何商业用途必须获得书面授权许可。
* 未经授权商业使用本软件属于侵权行为,将承担法律责任。
*
* 授权购买请联系: 357099073@qq.com
* 官方网站: https://www.yunzer.cn
*
* 评估用户须知:
* 1. 禁止移除版权声明
* 2. 禁止用于生产环境
* 3. 禁止转售或分发
*/
namespace app\api\model;
use think\Model;
class AdminUserGroup extends Model
{
}

25
app/api/model/ApiKey.php Normal file
View File

@ -0,0 +1,25 @@
<?php
/**
* 商业使用授权协议
*
* Copyright (c) 2025 [云泽网]. 保留所有权利.
*
* 本软件仅供评估使用。任何商业用途必须获得书面授权许可。
* 未经授权商业使用本软件属于侵权行为,将承担法律责任。
*
* 授权购买请联系: 357099073@qq.com
* 官方网站: https://www.yunzer.cn
*
* 评估用户须知:
* 1. 禁止移除版权声明
* 2. 禁止用于生产环境
* 3. 禁止转售或分发
*/
namespace app\api\model;
use think\Model;
class ApiKey extends Model
{
}

View File

@ -0,0 +1,25 @@
<?php
/**
* 商业使用授权协议
*
* Copyright (c) 2025 [云泽网]. 保留所有权利.
*
* 本软件仅供评估使用。任何商业用途必须获得书面授权许可。
* 未经授权商业使用本软件属于侵权行为,将承担法律责任。
*
* 授权购买请联系: 357099073@qq.com
* 官方网站: https://www.yunzer.cn
*
* 评估用户须知:
* 1. 禁止移除版权声明
* 2. 禁止用于生产环境
* 3. 禁止转售或分发
*/
namespace app\api\model\Article;
use think\Model;
class Articles extends Model
{
}

View File

@ -0,0 +1,25 @@
<?php
/**
* 商业使用授权协议
*
* Copyright (c) 2025 [云泽网]. 保留所有权利.
*
* 本软件仅供评估使用。任何商业用途必须获得书面授权许可。
* 未经授权商业使用本软件属于侵权行为,将承担法律责任。
*
* 授权购买请联系: 357099073@qq.com
* 官方网站: https://www.yunzer.cn
*
* 评估用户须知:
* 1. 禁止移除版权声明
* 2. 禁止用于生产环境
* 3. 禁止转售或分发
*/
namespace app\api\model\Article;
use think\Model;
class ArticlesCategory extends Model
{
}

25
app/api/model/Banner.php Normal file
View File

@ -0,0 +1,25 @@
<?php
/**
* 商业使用授权协议
*
* Copyright (c) 2025 [云泽网]. 保留所有权利.
*
* 本软件仅供评估使用。任何商业用途必须获得书面授权许可。
* 未经授权商业使用本软件属于侵权行为,将承担法律责任。
*
* 授权购买请联系: 357099073@qq.com
* 官方网站: https://www.yunzer.cn
*
* 评估用户须知:
* 1. 禁止移除版权声明
* 2. 禁止用于生产环境
* 3. 禁止转售或分发
*/
namespace app\api\model;
use think\Model;
class Banner extends Model
{
}

54
app/api/model/Base.php Normal file
View File

@ -0,0 +1,54 @@
<?php
/**
* 商业使用授权协议
*
* Copyright (c) 2025 [云泽网]. 保留所有权利.
*
* 本软件仅供评估使用。任何商业用途必须获得书面授权许可。
* 未经授权商业使用本软件属于侵权行为,将承担法律责任。
*
* 授权购买请联系: 357099073@qq.com
* 官方网站: https://www.yunzer.cn
*
* 评估用户须知:
* 1. 禁止移除版权声明
* 2. 禁止用于生产环境
* 3. 禁止转售或分发
*/
/**
* 后台管理系统
*/
namespace app\api\model;
use think\Model;
use think\facade\App;
class Base extends Model{
public function logs($data=null,$fileName=''){
if(is_null($data) || is_null($fileName)){
return false;
}
//获取Runtime路径
$path = App::getRuntimePath() . 'logs' . DIRECTORY_SEPARATOR;
if(!is_dir($path)){
$mkdir_re = mkdir($path,0777,TRUE);
if(!$mkdir_re){
$this -> logs($data,$fileName);
}
}
$info = ['data'=>$data];
$content = json_encode($info, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL;
if(empty($fileName)){
$filePath = $path . "/" . date("Ymd",time()).'.info.log';
}else{
$filePath = $path . "/" . $fileName . '.info.log';
}
$time = "[".date("Y-m-d H:i:s",time())."]";
$re = file_put_contents($filePath, $time." ".$content , FILE_APPEND);
if(!$re){
$this -> logs($data,$fileName);
}else{
return true;
}
}
}

View File

@ -0,0 +1,34 @@
<?php
/**
* 商业使用授权协议
*
* Copyright (c) 2025 [云泽网]. 保留所有权利.
*
* 本软件仅供评估使用。任何商业用途必须获得书面授权许可。
* 未经授权商业使用本软件属于侵权行为,将承担法律责任。
*
* 授权购买请联系: 357099073@qq.com
* 官方网站: https://www.yunzer.cn
*
* 评估用户须知:
* 1. 禁止移除版权声明
* 2. 禁止用于生产环境
* 3. 禁止转售或分发
*/
namespace app\api\model\ContentPush;
use think\Model;
class ContentPush extends Model
{
protected $name = 'content_push';
// 自动写入时间戳
protected $autoWriteTimestamp = true;
// 定义时间戳字段名
protected $createTime = 'create_time';
protected $updateTime = 'update_time';
protected $deleteTime = 'delete_time';
}

View File

@ -0,0 +1,34 @@
<?php
/**
* 商业使用授权协议
*
* Copyright (c) 2025 [云泽网]. 保留所有权利.
*
* 本软件仅供评估使用。任何商业用途必须获得书面授权许可。
* 未经授权商业使用本软件属于侵权行为,将承担法律责任。
*
* 授权购买请联系: 357099073@qq.com
* 官方网站: https://www.yunzer.cn
*
* 评估用户须知:
* 1. 禁止移除版权声明
* 2. 禁止用于生产环境
* 3. 禁止转售或分发
*/
namespace app\api\model\ContentPush;
use think\Model;
class ContentPushSetting extends Model
{
protected $name = 'content_push_setting';
// 自动写入时间戳
protected $autoWriteTimestamp = true;
// 定义时间戳字段名
protected $createTime = 'create_time';
protected $updateTime = 'update_time';
protected $deleteTime = 'delete_time';
}

View File

@ -0,0 +1,25 @@
<?php
/**
* 商业使用授权协议
*
* Copyright (c) 2025 [云泽网]. 保留所有权利.
*
* 本软件仅供评估使用。任何商业用途必须获得书面授权许可。
* 未经授权商业使用本软件属于侵权行为,将承担法律责任。
*
* 授权购买请联系: 357099073@qq.com
* 官方网站: https://www.yunzer.cn
*
* 评估用户须知:
* 1. 禁止移除版权声明
* 2. 禁止用于生产环境
* 3. 禁止转售或分发
*/
namespace app\api\model;
use think\Model;
class DailyStats extends Model
{
}

View File

@ -0,0 +1,25 @@
<?php
/**
* 商业使用授权协议
*
* Copyright (c) 2025 [云泽网]. 保留所有权利.
*
* 本软件仅供评估使用。任何商业用途必须获得书面授权许可。
* 未经授权商业使用本软件属于侵权行为,将承担法律责任。
*
* 授权购买请联系: 357099073@qq.com
* 官方网站: https://www.yunzer.cn
*
* 评估用户须知:
* 1. 禁止移除版权声明
* 2. 禁止用于生产环境
* 3. 禁止转售或分发
*/
namespace app\api\model\Log;
use think\Model;
class LogsLogin extends Model
{
}

View File

@ -0,0 +1,25 @@
<?php
/**
* 商业使用授权协议
*
* Copyright (c) 2025 [云泽网]. 保留所有权利.
*
* 本软件仅供评估使用。任何商业用途必须获得书面授权许可。
* 未经授权商业使用本软件属于侵权行为,将承担法律责任。
*
* 授权购买请联系: 357099073@qq.com
* 官方网站: https://www.yunzer.cn
*
* 评估用户须知:
* 1. 禁止移除版权声明
* 2. 禁止用于生产环境
* 3. 禁止转售或分发
*/
namespace app\api\model\Log;
use think\Model;
class LogsOperation extends Model
{
}

View File

@ -0,0 +1,25 @@
<?php
/**
* 商业使用授权协议
*
* Copyright (c) 2025 [云泽网]. 保留所有权利.
*
* 本软件仅供评估使用。任何商业用途必须获得书面授权许可。
* 未经授权商业使用本软件属于侵权行为,将承担法律责任。
*
* 授权购买请联系: 357099073@qq.com
* 官方网站: https://www.yunzer.cn
*
* 评估用户须知:
* 1. 禁止移除版权声明
* 2. 禁止用于生产环境
* 3. 禁止转售或分发
*/
namespace app\api\model;
use think\Model;
class MailConfig extends Model
{
}

View File

@ -0,0 +1,30 @@
<?php
/**
* 商业使用授权协议
*
* Copyright (c) 2025 [云泽网]. 保留所有权利.
*
* 本软件仅供评估使用。任何商业用途必须获得书面授权许可。
* 未经授权商业使用本软件属于侵权行为,将承担法律责任。
*
* 授权购买请联系: 357099073@qq.com
* 官方网站: https://www.yunzer.cn
*
* 评估用户须知:
* 1. 禁止移除版权声明
* 2. 禁止用于生产环境
* 3. 禁止转售或分发
*/
namespace app\api\model\Resource;
use think\Model;
class Resource extends Model
{
// 设置当前模型对应的数据表名称(不含前缀)
protected $name = 'resources';
// 设置主键
protected $pk = 'id';
}

View File

@ -0,0 +1,30 @@
<?php
/**
* 商业使用授权协议
*
* Copyright (c) 2025 [云泽网]. 保留所有权利.
*
* 本软件仅供评估使用。任何商业用途必须获得书面授权许可。
* 未经授权商业使用本软件属于侵权行为,将承担法律责任。
*
* 授权购买请联系: 357099073@qq.com
* 官方网站: https://www.yunzer.cn
*
* 评估用户须知:
* 1. 禁止移除版权声明
* 2. 禁止用于生产环境
* 3. 禁止转售或分发
*/
namespace app\api\model\Resource;
use think\Model;
class ResourceCategory extends Model
{
// 设置当前模型对应的数据表名称(不含前缀)
protected $name = 'resources_category';
// 设置主键
protected $pk = 'id';
}

View File

@ -0,0 +1,25 @@
<?php
/**
* 商业使用授权协议
*
* Copyright (c) 2025 [云泽网]. 保留所有权利.
*
* 本软件仅供评估使用。任何商业用途必须获得书面授权许可。
* 未经授权商业使用本软件属于侵权行为,将承担法律责任。
*
* 授权购买请联系: 357099073@qq.com
* 官方网站: https://www.yunzer.cn
*
* 评估用户须知:
* 1. 禁止移除版权声明
* 2. 禁止用于生产环境
* 3. 禁止转售或分发
*/
namespace app\api\model\User;
use think\Model;
class Users extends Model
{
}

View File

@ -0,0 +1,25 @@
<?php
/**
* 商业使用授权协议
*
* Copyright (c) 2025 [云泽网]. 保留所有权利.
*
* 本软件仅供评估使用。任何商业用途必须获得书面授权许可。
* 未经授权商业使用本软件属于侵权行为,将承担法律责任。
*
* 授权购买请联系: 357099073@qq.com
* 官方网站: https://www.yunzer.cn
*
* 评估用户须知:
* 1. 禁止移除版权声明
* 2. 禁止用于生产环境
* 3. 禁止转售或分发
*/
namespace app\api\model\User;
use think\Model;
class UsersGroup extends Model
{
}

View File

@ -0,0 +1,68 @@
<?php
/**
* 商业使用授权协议
*
* Copyright (c) 2025 [云泽网]. 保留所有权利.
*
* 本软件仅供评估使用。任何商业用途必须获得书面授权许可。
* 未经授权商业使用本软件属于侵权行为,将承担法律责任。
*
* 授权购买请联系: 357099073@qq.com
* 官方网站: https://www.yunzer.cn
*
* 评估用户须知:
* 1. 禁止移除版权声明
* 2. 禁止用于生产环境
* 3. 禁止转售或分发
*/
/**
* 配置表
*/
namespace app\api\model;
use think\Model;
class YzAdminConfig extends Model
{
// 设置当前模型对应的数据表名称(不含前缀)
protected $name = 'admin_config';
// 设置主键
protected $pk = 'config_id';
/**
* 列出全部配置key对应value
*/
public function getAll(){
$aList = static::where('config_status',1)->order('config_sort DESC')->select()->toArray();
if(empty($aList)){
return [];
}else{
$return = [];
foreach($aList as $k=>$v){
$return[$v['config_name']] = $v['config_value'];
}
}
return $return;
}
/**
* 多条数据更新
*/
public function updateAll($data){
$lists = static::order('config_sort DESC,config_id')->select()->toArray();
if(empty($lists)){
return false;
}else{
foreach($lists as &$lists_v){
$lists_v['config_value'] = $data[$lists_v['config_name']];
}
$save = static::saveAll($lists);
if(empty($save)){
return false;
}
}
return true;
}
}

View File

@ -0,0 +1,25 @@
<?php
/**
* 商业使用授权协议
*
* Copyright (c) 2025 [云泽网]. 保留所有权利.
*
* 本软件仅供评估使用。任何商业用途必须获得书面授权许可。
* 未经授权商业使用本软件属于侵权行为,将承担法律责任。
*
* 授权购买请联系: 357099073@qq.com
* 官方网站: https://www.yunzer.cn
*
* 评估用户须知:
* 1. 禁止移除版权声明
* 2. 禁止用于生产环境
* 3. 禁止转售或分发
*/
namespace app\api\model;
use think\Model;
class ZIconfont extends Model
{
}

View File

@ -1,4 +1,4 @@
VITE_APP_ENV=development
VITE_APP_DEBUG_MODE=true
VITE_APP_TITLE=项目管理系统
VITE_APP_API_BASE_URL=https://www.yunzer.cn/api
VITE_APP_API_BASE_URL=https://www.yunzer.cn/api

View File

@ -14,6 +14,7 @@ declare module 'vue' {
ElCol: typeof import('element-plus/es')['ElCol']
ElColorPicker: typeof import('element-plus/es')['ElColorPicker']
ElContainer: typeof import('element-plus/es')['ElContainer']
ElDialog: typeof import('element-plus/es')['ElDialog']
ElDivider: typeof import('element-plus/es')['ElDivider']
ElDropdown: typeof import('element-plus/es')['ElDropdown']
ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
@ -26,8 +27,11 @@ declare module 'vue' {
ElMain: typeof import('element-plus/es')['ElMain']
ElMenu: typeof import('element-plus/es')['ElMenu']
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
ElOption: typeof import('element-plus/es')['ElOption']
ElPagination: typeof import('element-plus/es')['ElPagination']
ElRow: typeof import('element-plus/es')['ElRow']
ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
ElSelect: typeof import('element-plus/es')['ElSelect']
ElStatistic: typeof import('element-plus/es')['ElStatistic']
ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
ElSwitch: typeof import('element-plus/es')['ElSwitch']
@ -37,4 +41,7 @@ declare module 'vue' {
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
}
export interface GlobalDirectives {
vLoading: typeof import('element-plus/es')['ElLoadingDirective']
}
}

View File

@ -2,19 +2,15 @@
import { useDark } from '@vueuse/core'
import { onMounted } from 'vue'
import useColorStore from '@/store/color'
import useUserStore from '@/store/modules/user'
import ENV_CONFIG from '@/config/env'
useDark()
const colorStore = useColorStore()
const userStore = useUserStore()
onMounted(() => {
//
document.title = ENV_CONFIG.APP_TITLE
//
userStore.initUserState()
//
colorStore.primaryChange(colorStore.primary)
})

View File

@ -4,48 +4,16 @@ import api from './user'
export interface MenuItem {
smid: number
label: string
icon_class: string
type: number
src: string
sort: number
status: number
parent_id: number
icon_class: string
sort: string
status: number
children?: MenuItem[]
}
export interface RoleItem {
group_id: number
group_name: string
status: number
create_time: string
}
// 获取所有菜单列表
export const getMenuList = () => {
return api.get('/menu/list')
}
// 根据用户角色获取菜单
// 获取管理员菜单接口
export const getUserMenus = () => {
return api.get('/menu/userMenus')
}
// 获取角色列表
export const getRoleList = () => {
return api.get('/menu/roles')
}
// 获取菜单详情
export const getMenuDetail = (id: number) => {
return api.get('/menu/detail', { params: { id } })
}
// 临时菜单接口(使用用户控制器)
export const getTempUserMenus = () => {
return api.get('/user/menus')
}
// 或者直接使用完整路径如果baseURL有问题
export const getTempUserMenusDirect = () => {
return api.get('https://www.yunzer.cn/api/user/menus')
return api.get('/admin/menus')
}

View File

@ -54,19 +54,19 @@ export const login = (data: { account: string; password: string }) => {
return api.post('/admin/login', data)
}
// 获取用户信息接口
// 获取管理员信息接口
export const getUserInfo = () => {
return api.get('/user/info')
return api.get('/admin/info')
}
// 用户登出接口
// 管理员登出接口
export const logout = () => {
return api.post('/user/logout')
return api.post('/admin/logout')
}
// 修改密码接口
export const changePassword = (data: { oldPassword: string; newPassword: string }) => {
return api.post('/user/change-password', data)
return api.post('/admin/change-password', data)
}
export default api

View File

@ -9,8 +9,8 @@ const ENV_CONFIG = {
APP_TITLE: import.meta.env.VITE_APP_TITLE || '项目管理系统',
// 本地存储key
TOKEN_KEY: 'token',
USER_INFO_KEY: 'userInfo',
TOKEN_KEY: 'admin_token',
USER_INFO_KEY: 'admin_user_info',
// 是否为开发环境
IS_DEV: import.meta.env.DEV,

View File

@ -8,7 +8,6 @@ import 'element-plus/theme-chalk/dark/css-vars.css'
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import useUserStore from '@/store/modules/user'
const app = createApp(App)
@ -21,8 +20,4 @@ for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.use(pinia).use(ElementPlus).use(router)
// 初始化用户状态
const userStore = useUserStore()
userStore.initUserState()
app.mount('#app')

View File

@ -1,8 +1,8 @@
import { RouteRecordRaw, createRouter, createWebHashHistory } from 'vue-router'
import * as MenuUtil from '@/util/menu'
import useFastnavStore from '@/store/fastnav'
import useUserStore from '@/store/modules/user'
import useMenuStore from '@/store/modules/menu'
// 只保留非菜单相关的静态路由
const routes: RouteRecordRaw[] = [
@ -15,7 +15,16 @@ const routes: RouteRecordRaw[] = [
path: '/',
name: 'Main',
component: () => import('@/views/main/index.vue'),
children: [], // 预定义空的子路由数组
children: [
{
path: '',
name: 'Home',
component: () => import('@/views/home/index.vue'),
meta: {
title: '工作台'
}
}
],
},
{
path: '/:catchAll(.*)',
@ -31,8 +40,93 @@ const router = createRouter({
let isAddRoute = false
let isUserStateInitialized = false
// 使用相对路径vite 的 import.meta.glob 会返回 /src/views/... 格式的key
const views = import.meta.glob('@/views/**/*.vue')
// 创建一个映射,将 @/views/... 格式的路径映射到实际的组件
const viewsMap = new Map()
for (const [key, component] of Object.entries(views)) {
// 将 /src/views/... 转换为 @/views/... 格式作为key
const mappedKey = key.replace('/src/views/', '@/views/')
viewsMap.set(mappedKey, component)
// 同时保留原始key
viewsMap.set(key, component)
}
// 智能查找组件文件
function findComponent(componentName: string, routePath: string): any {
// 处理 routePath去除开头的斜杠
let cleanPath = routePath.startsWith('/') ? routePath.slice(1) : routePath
// 只保留第一个斜杠后的路径(去掉多余的斜杠)
cleanPath = cleanPath.replace(/\/+/g, '/')
// 可能的路径组合
const possiblePaths = [
// 1. 直接匹配:/src/views/路径.vue
`/src/views/${cleanPath}.vue`,
// 2. index.vue 结尾:/src/views/路径/index.vue
`/src/views/${cleanPath}/index.vue`,
// 3. 小写路径匹配
`/src/views/${cleanPath.toLowerCase()}.vue`,
`/src/views/${cleanPath.toLowerCase()}/index.vue`,
].filter(Boolean)
// 精确匹配 - 使用映射查找
for (const path of possiblePaths) {
if (viewsMap.get(path)) {
return viewsMap.get(path)
}
}
// 模糊匹配 - 改进逻辑
const searchTerms = [componentName.toLowerCase()]
// 优先匹配更精确的路径
for (const [path, component] of Object.entries(views)) {
// 检查路径是否包含组件名(不区分大小写)
if (path.toLowerCase().includes(componentName.toLowerCase())) {
return component
}
}
return null
}
// 从菜单数据生成路由
function generateRoutesFromMenus(menus: any[]): RouteRecordRaw[] {
const routes: RouteRecordRaw[] = []
for (const menu of menus) {
// 只处理有src字段的菜单项type为1表示页面
if (menu.type === 1 && menu.src) {
const route: RouteRecordRaw = {
path: `/${menu.src}`,
name: menu.label,
component: findComponent(menu.label, menu.src),
meta: {
title: menu.label
}
}
if (route.component) {
routes.push(route)
}
}
// 递归处理子菜单
if (menu.children && menu.children.length > 0) {
const childRoutes = generateRoutesFromMenus(menu.children)
routes.push(...childRoutes)
}
}
return routes
}
router.beforeEach(async (to, _from, next) => {
const userStore = useUserStore()
const menuStore = useMenuStore()
// 等待用户状态初始化完成
if (!isUserStateInitialized) {
@ -59,9 +153,9 @@ router.beforeEach(async (to, _from, next) => {
// 已经添加过动态路由
if (isAddRoute) {
if (to.meta.desc) {
if (to.meta.title) {
const fastnavStore = useFastnavStore()
fastnavStore.addData(to.meta.desc as string, to.path)
fastnavStore.addData(to.meta.title as string, to.path)
}
next()
return
@ -70,16 +164,24 @@ router.beforeEach(async (to, _from, next) => {
// 添加主页子路由
isAddRoute = true
// 获取菜单数据
await menuStore.fetchUserMenus()
// 找到主路由
const mainRoute = router.options.routes.find((v) => v.path == '/')!
// 根据用户类型生成菜单和路由(默认使用管理员类型)
const [_genMenus, genRoutes] = MenuUtil.gen(1)
// 根据菜单数据生成路由
const genRoutes = generateRoutesFromMenus(menuStore.getMenus)
// 设置主路由的重定向和子路由
if (genRoutes.length > 0) {
mainRoute.redirect = genRoutes[0].path
mainRoute.children = genRoutes
// 保留默认的Home页面路由添加动态路由
const existingChildren = mainRoute.children || []
const homeRoute = existingChildren.find(route => route.name === 'Home')
mainRoute.children = homeRoute ? [homeRoute, ...genRoutes] : genRoutes
// 始终重定向到工作台
mainRoute.redirect = '/'
// 添加主路由(包含子路由)
router.addRoute(mainRoute)
@ -87,8 +189,8 @@ router.beforeEach(async (to, _from, next) => {
// 重新导航到目标路由
next({ ...to, replace: true })
} else {
// 如果没有生成路由,至少重定向到登录页
next({ path: '/login' })
// 如果没有生成路由,重定向到工作台
next({ path: '/' })
}
})

View File

@ -1,5 +1,5 @@
import { defineStore } from 'pinia'
import { getUserMenus, getMenuList, getTempUserMenus, type MenuItem } from '@/api/menu'
import { getUserMenus, type MenuItem } from '@/api/menu'
import ENV_CONFIG from '@/config/env'
interface MenuState {
@ -8,12 +8,6 @@ interface MenuState {
error: string | null
}
interface ApiResponse<T> {
code: number
message: string
data: T
}
const useMenuStore = defineStore('menu', {
state: (): MenuState => ({
menus: [],
@ -33,43 +27,22 @@ const useMenuStore = defineStore('menu', {
},
actions: {
// 获取用户菜单(使用临时接口)
// 获取管理员菜单
async fetchUserMenus() {
this.loading = true
this.error = null
try {
const response = await getTempUserMenus() as unknown as ApiResponse<MenuItem[]>
const response = await getUserMenus() as unknown as MenuItem[]
if (response.code === 200 && response.data) {
this.menus = response.data
if (response && Array.isArray(response)) {
this.menus = response
} else {
throw new Error(response.message || '获取菜单失败')
throw new Error('获取菜单失败')
}
} catch (error: any) {
this.error = error.message || '获取菜单失败'
console.error('获取用户菜单失败:', error)
} finally {
this.loading = false
}
},
// 获取所有菜单(管理员用)
async fetchAllMenus() {
this.loading = true
this.error = null
try {
const response = await getMenuList() as unknown as ApiResponse<MenuItem[]>
if (response.code === 200 && response.data) {
this.menus = response.data
} else {
throw new Error(response.message || '获取菜单失败')
}
} catch (error: any) {
this.error = error.message || '获取菜单失败'
console.error('获取所有菜单失败:', error)
console.error('获取管理员菜单失败:', error)
} finally {
this.loading = false
}

View File

@ -103,6 +103,7 @@ const useUserStore = defineStore('user', {
try {
// 验证token是否有效
const response = await getUserInfoApi() as unknown as UserInfo
if (response && response.id) {
this.token = token
this.userInfo = response
@ -113,10 +114,21 @@ const useUserStore = defineStore('user', {
}
} catch (error: any) {
console.error('获取用户信息失败:', error)
// 网络错误时,不清除本地状态,保持用户登录状态
// 如果有本地token和用户信息先保持登录状态
// 只有在明确知道token无效时才清除
if (error.response?.status === 401) {
this.clearUserState()
} else {
// 网络错误或其他错误,使用本地存储的数据
try {
const userInfo = JSON.parse(userInfoStr)
this.token = token
this.userInfo = userInfo
this.isLogin = true
} catch (parseError) {
// 本地数据解析失败,清除状态
this.clearUserState()
}
}
}
}

View File

@ -0,0 +1,234 @@
<template>
<div class="article-list">
<el-card>
<div class="header">
<el-input
v-model="search"
placeholder="搜索文章标题"
clearable
class="search-input"
@keyup.enter="fetchArticles"
/>
<el-select
v-model="selectedCategory"
placeholder="筛选分类"
clearable
class="category-select"
style="width: 150px"
@change="fetchArticles"
>
<el-option
v-for="item in categoryOptions"
:key="item"
:label="item"
:value="item"
/>
</el-select>
<el-button type="primary" @click="fetchArticles">搜索</el-button>
<el-button type="success" @click="handlePublish">发布文章</el-button>
</div>
<el-table
:data="articles"
style="width: 100%; margin-top: 20px"
v-loading="loading"
border
>
<el-table-column prop="id" label="ID" width="80" align="center" />
<el-table-column prop="title" label="标题" min-width="200" />
<el-table-column prop="author" label="作者" width="120" />
<el-table-column prop="category" label="分类" width="120" />
<el-table-column prop="create_time" label="创建时间" width="180" />
<el-table-column label="操作" width="220">
<template #default="scope">
<el-button size="small" @click="handleEdit(scope.row)">编辑</el-button>
<el-button size="small" type="danger" @click="handleDelete(scope.row)">删除</el-button>
<el-button size="small" type="primary" @click="handlePublishSingle(scope.row)">发布</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination">
<el-pagination
background
layout="prev, pager, next, jumper"
:total="total"
:page-size="pageSize"
:current-page="page"
@current-change="handlePageChange"
/>
</div>
</el-card>
<!-- 发布文章对话框 -->
<el-dialog v-model="publishDialogVisible" title="发布文章" width="500px">
<el-form :model="publishForm" label-width="80px">
<el-form-item label="标题">
<el-input v-model="publishForm.title" />
</el-form-item>
<el-form-item label="作者">
<el-input v-model="publishForm.author" />
</el-form-item>
<el-form-item label="分类">
<el-select v-model="publishForm.category" placeholder="请选择分类">
<el-option
v-for="item in categoryOptions"
:key="item"
:label="item"
:value="item"
/>
</el-select>
</el-form-item>
<el-form-item label="内容">
<el-input
v-model="publishForm.content"
type="textarea"
:rows="4"
placeholder="请输入文章内容"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="publishDialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitPublish">发布</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
// API
// import { getArticleList } from '@/api/article'
const articles = ref<any[]>([])
const loading = ref(false)
const search = ref('')
const page = ref(1)
const pageSize = ref(10)
const total = ref(0)
const selectedCategory = ref('')
const categoryOptions = ref<string[]>([
'前端', '后端', '数据库', '架构', '安全', '运维', '测试'
])
//
const publishDialogVisible = ref(false)
const publishForm = ref({
title: '',
author: '',
category: '',
content: ''
})
function fetchArticles() {
loading.value = true
// API
setTimeout(() => {
//
const all = [
{ id: 1, title: 'Vue3 入门', author: '张三', category: '前端', create_time: '2024-06-01 10:00' },
{ id: 2, title: 'TypeScript 实践', author: '李四', category: '前端', create_time: '2024-06-02 11:00' },
{ id: 3, title: 'PHP 高级技巧', author: '王五', category: '后端', create_time: '2024-06-03 12:00' },
{ id: 4, title: '数据库优化', author: '赵六', category: '数据库', create_time: '2024-06-04 13:00' },
{ id: 5, title: '云原生架构', author: '钱七', category: '架构', create_time: '2024-06-05 14:00' },
{ id: 6, title: '安全最佳实践', author: '孙八', category: '安全', create_time: '2024-06-06 15:00' },
{ id: 7, title: '性能调优', author: '周九', category: '运维', create_time: '2024-06-07 16:00' },
{ id: 8, title: '微服务设计', author: '吴十', category: '架构', create_time: '2024-06-08 17:00' },
{ id: 9, title: '前端工程化', author: '郑十一', category: '前端', create_time: '2024-06-09 18:00' },
{ id: 10, title: '测试驱动开发', author: '冯十二', category: '测试', create_time: '2024-06-10 19:00' },
{ id: 11, title: '持续集成', author: '褚十三', category: '运维', create_time: '2024-06-11 20:00' }
]
let filtered = all
if (search.value) {
filtered = filtered.filter(a => a.title.includes(search.value))
}
if (selectedCategory.value) {
filtered = filtered.filter(a => a.category === selectedCategory.value)
}
total.value = filtered.length
const start = (page.value - 1) * pageSize.value
articles.value = filtered.slice(start, start + pageSize.value)
loading.value = false
}, 500)
}
function handleEdit(row: any) {
//
alert('编辑文章: ' + row.title)
}
function handleDelete(row: any) {
//
if (confirm('确定要删除文章 "' + row.title + '" 吗?')) {
// API
articles.value = articles.value.filter(a => a.id !== row.id)
total.value--
}
}
function handlePageChange(val: number) {
page.value = val
fetchArticles()
}
//
function handlePublish() {
publishForm.value = {
title: '',
author: '',
category: '',
content: ''
}
publishDialogVisible.value = true
}
// API
function handlePublishSingle(row: any) {
alert('发布文章: ' + row.title)
}
//
function submitPublish() {
if (!publishForm.value.title || !publishForm.value.author || !publishForm.value.category) {
alert('请填写完整信息')
return
}
// API
const newId = Math.max(...articles.value.map(a => a.id), 0) + 1
articles.value.unshift({
id: newId,
title: publishForm.value.title,
author: publishForm.value.author,
category: publishForm.value.category,
create_time: new Date().toISOString().slice(0, 16).replace('T', ' ')
})
total.value++
publishDialogVisible.value = false
//
page.value = 1
fetchArticles()
}
onMounted(() => {
fetchArticles()
})
</script>
<style scoped>
.article-list {
padding: 20px;
}
.header {
display: flex;
align-items: center;
gap: 10px;
}
.search-input {
width: 300px;
}
.category-select {
min-width: 120px;
}
.pagination {
margin-top: 20px;
text-align: right;
}
</style>

View File

@ -59,29 +59,29 @@
<el-aside :width="menuStore.width">
<el-menu
router
:default-active="route.path"
:default-active="route.path === '/' ? '/' : route.path"
:collapse="menuStore.collapse"
style="height: 100%;"
>
<template v-for="menu in menus" :key="menu.smid">
<el-sub-menu
v-if="menu.children && menu.children.length > 0"
:index="menu.smid.toString()"
:index="menu.src || menu.smid.toString()"
>
<template #title>
<el-icon>
<component :is="menu.icon_class"></component>
<Setting />
</el-icon>
<span>{{ menu.label }}</span>
</template>
<el-menu-item
v-for="child in menu.children"
:key="child.smid"
:index="child.smid.toString()"
:index="child.src || child.smid.toString()"
@click="handleMenuClick(child)"
>
<el-icon>
<component :is="child.icon_class"></component>
<Setting />
</el-icon>
<span>{{ child.label }}</span>
</el-menu-item>
@ -90,11 +90,12 @@
<el-menu-item
v-else
:key="menu.smid"
:index="menu.smid.toString()"
:index="menu.smid === 0 ? '/' : (menu.src || menu.smid.toString())"
@click="handleMenuClick(menu)"
>
<el-icon>
<component :is="menu.icon_class"></component>
<Monitor v-if="menu.smid === 0" />
<Setting v-else />
</el-icon>
<template #title>{{ menu.label }}</template>
</el-menu-item>
@ -165,9 +166,19 @@ import useMenuStore from '@/store/modules/menu'
import useFastnavStore from '@/store/fastnav'
import useMenuCollapseStore from '@/store/menu'
import useColorStore from '@/store/color'
import { onMounted, ref } from 'vue'
import { onMounted, ref, computed } from 'vue'
import { useRoute, useRouter, RouterView } from 'vue-router'
import { useFullscreen, useDark } from '@vueuse/core'
import {
Setting,
User,
Document,
Folder,
Monitor,
Grid,
Menu,
House
} from '@element-plus/icons-vue'
const route = useRoute()
const router = useRouter()
@ -183,6 +194,25 @@ const colorStore = useColorStore()
const menus = ref<any[]>([])
const collapse = ref(true)
//
const iconMap: Record<string, any> = {
'layui-icon-senior': Setting,
'layui-icon-user': User,
'layui-icon-file': Document,
'layui-icon-folder': Folder,
'layui-icon-monitor': Monitor,
'layui-icon-grid': Grid,
'layui-icon-menu': Menu,
'layui-icon-home': House,
//
'': Setting
}
//
const getIconComponent = (iconClass: string) => {
return iconMap[iconClass] || Setting
}
//
const tagMenuReloadDisabled = ref(false)
const tagMenuCloseDisabled = ref(false)
@ -191,9 +221,26 @@ const tagMenuLeftDisabled = ref(false)
const tagMenuRightDisabled = ref(false)
onMounted(async () => {
//
await menuDataStore.fetchUserMenus()
menus.value = menuDataStore.getMenus
// store
if (menuDataStore.getMenus.length === 0) {
await menuDataStore.fetchUserMenus()
}
//
const workbenchMenu = {
smid: 0,
label: '工作台',
type: 1,
src: '',
parent_id: 0,
icon_class: '',
sort: '999999',
status: 1,
children: []
}
//
menus.value = [workbenchMenu, ...menuDataStore.getMenus]
})
//
@ -263,12 +310,13 @@ const handleTagCommand = (command: string) => {
//
const handleMenuClick = (menu: any) => {
if (menu.type === 1 && menu.src) {
//
const title = menu.label
const path = '/' + menu.src.replace('/', '_')
//
console.log('内部跳转:', title, path)
if (menu.smid === 0) {
//
router.push('/')
} else if (menu.type === 1 && menu.src) {
// - 使src
const path = '/' + menu.src
router.push(path)
} else if (menu.type === 2 && menu.src) {
//
window.open(menu.src, '_blank')

View File

@ -53,9 +53,9 @@ export default defineConfig({
},
// 服务器配置
// server: {
// port: 3000,
// open: true,
// cors: true
// }
})
server: {
port: 5173, // 保持你当前使用的端口
open: true,
cors: true
}
})

View File

@ -37,4 +37,14 @@ Route::post('index/wechat/reGenerateQrcode', 'index/wechat/reGenerateQrcode');
Route::get('index/wechat/testWechat', 'index/wechat/testWechat');
Route::post('index/wechat/testWechat', 'index/wechat/testWechat');
// ... existing code ...
// API路由组
Route::group('api', function () {
// 管理员登录
Route::post('admin/login', 'api/Admin/login');
// 管理员相关接口
Route::get('admin/info', 'api/Admin/info');
Route::post('admin/logout', 'api/Admin/logout');
Route::post('admin/change-password', 'api/Admin/changePassword');
Route::get('admin/menus', 'api/Admin/menus');
});