+ {/* 搜索栏 */}
+
+
+
+ setSearchQuery(e.target.value)}
+ onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
+ className="pl-9 bg-white border-gray-200 focus:border-blue-500"
+ />
+
+
- {/* 好友列表 */}
-
- {isFetchingFriends && friends.length === 0 ? (
-
-
-
- ) : friends.length === 0 && hasFriendLoadError ? (
-
-
加载好友失败,请重试
-
-
- ) : friends.length === 0 ? (
-
未找到匹配的好友
- ) : (
- <>
- {friends.map((friend) => (
+ {/* 好友列表 */}
+
+ {isFetchingFriends && friends.length === 0 ? (
+
+
+
+ ) : friends.length === 0 ? (
+
未找到匹配的好友
+ ) : (
+ <>
+ {friends.map((friend) => (
handleFriendClick(friend)}
>
- {friend.nickname?.[0] || 'U'}
+ {friend.nickname?.[0] || 'U'}
-
+
{friend.nickname}
- {friend.remark && ({friend.remark})}
+ {friend.remark && ({friend.remark})}
{friend.wechatId}
- {friend.tags.slice(0, 3).map((tag: FriendTag) => (
-
- {tag.name}
+ {friend.tags?.map((tag, index) => (
+
+ {typeof tag === 'string' ? tag : tag.name}
))}
- {friend.tags.length > 3 && (
-
- +{friend.tags.length - 3}
-
- )}
- ))}
-
- {/* 懒加载指示器 */}
- {hasMoreFriends && (
-
- {isFetchingFriends && }
-
- )}
- >
- )}
-
-
- {/* 显示加载状态和总数 */}
-
- {friendsTotal > 0 ? (
-
- 已加载 {Math.min(friends.length, friendsTotal)} / {friendsTotal} 条记录
-
- ) : !isFetchingFriends && !hasFriendLoadError && account ? (
-
- 共 {account.friendCount} 条记录
-
- ) : null}
-
+ ))}
+ {hasMoreFriends && (
+
+
+
+ )}
+ >
+ )}
-
+
diff --git a/Cunkebao/components/AuthCheck.tsx b/Cunkebao/components/AuthCheck.tsx
new file mode 100644
index 00000000..61bc91e4
--- /dev/null
+++ b/Cunkebao/components/AuthCheck.tsx
@@ -0,0 +1,43 @@
+"use client"
+
+import { useEffect } from 'react'
+import { useRouter, usePathname } from 'next/navigation'
+import { useAuth } from '@/hooks/useAuth'
+
+// 不需要登录的公共页面路径
+const PUBLIC_PATHS = [
+ '/login',
+ '/register',
+ '/forgot-password',
+ '/reset-password',
+ '/404',
+ '/500'
+]
+
+export function AuthCheck({ children }: { children: React.ReactNode }) {
+ const router = useRouter()
+ const pathname = usePathname()
+ const { isAuthenticated, isLoading } = useAuth()
+
+ useEffect(() => {
+ if (!isLoading && !isAuthenticated && !PUBLIC_PATHS.includes(pathname)) {
+ // 保存当前URL,登录后可以重定向回来
+ const returnUrl = encodeURIComponent(window.location.href)
+ router.push(`/login?returnUrl=${returnUrl}`)
+ }
+ }, [isAuthenticated, isLoading, pathname, router])
+
+ if (isLoading) {
+ return (
+
+ )
+ }
+
+ if (!isAuthenticated && !PUBLIC_PATHS.includes(pathname)) {
+ return null
+ }
+
+ return <>{children}>
+}
\ No newline at end of file
diff --git a/Cunkebao/hooks/useAuth.ts b/Cunkebao/hooks/useAuth.ts
new file mode 100644
index 00000000..33994766
--- /dev/null
+++ b/Cunkebao/hooks/useAuth.ts
@@ -0,0 +1,46 @@
+import { useState, useEffect } from 'react'
+
+export interface AuthState {
+ isAuthenticated: boolean
+ isLoading: boolean
+ user: any | null
+}
+
+export function useAuth(): AuthState {
+ const [state, setState] = useState
({
+ isAuthenticated: false,
+ isLoading: true,
+ user: null
+ })
+
+ useEffect(() => {
+ const checkAuth = async () => {
+ try {
+ // 检查本地存储的token
+ const token = localStorage.getItem('token')
+ if (!token) {
+ setState({ isAuthenticated: false, isLoading: false, user: null })
+ return
+ }
+
+ // TODO: 这里可以添加token验证的API调用
+ // const response = await validateToken(token)
+ // if (response.valid) {
+ // setState({ isAuthenticated: true, isLoading: false, user: response.user })
+ // } else {
+ // setState({ isAuthenticated: false, isLoading: false, user: null })
+ // }
+
+ // 临时:仅检查token存在性
+ setState({ isAuthenticated: true, isLoading: false, user: { token } })
+ } catch (error) {
+ console.error('Auth check failed:', error)
+ setState({ isAuthenticated: false, isLoading: false, user: null })
+ }
+ }
+
+ checkAuth()
+ }, [])
+
+ return state
+}
\ No newline at end of file
diff --git a/Cunkebao/next.config.mjs b/Cunkebao/next.config.mjs
index a4c88a09..7d39de61 100644
--- a/Cunkebao/next.config.mjs
+++ b/Cunkebao/next.config.mjs
@@ -22,6 +22,9 @@ const nextConfig = {
parallelServerCompiles: true,
},
reactStrictMode: false,
+ compiler: {
+ styledComponents: true,
+ },
}
mergeConfig(nextConfig, userConfig)
diff --git a/Server/index.html b/Server/index.html
deleted file mode 100644
index 86aeca22..00000000
--- a/Server/index.html
+++ /dev/null
@@ -1,39 +0,0 @@
-
-
-
-
- 恭喜,站点创建成功!
-
-
-
-
-
恭喜, 站点创建成功!
-
这是默认index.html,本页面由系统自动生成
-
- - 本页面在FTP根目录下的index.html
- - 您可以修改、删除或覆盖本页面
- - FTP相关信息,请到“面板系统后台 > FTP” 查看
-
-
-
-
\ No newline at end of file
diff --git a/Server/scripts/README.md b/Server/scripts/README.md
deleted file mode 100644
index 2077eeae..00000000
--- a/Server/scripts/README.md
+++ /dev/null
@@ -1,180 +0,0 @@
-# 定时任务和队列使用说明
-
-## 一、环境准备
-
-### 1. 安装Redis
-确保服务器已安装Redis,并正常运行。
-
-```bash
-# Ubuntu/Debian系统
-sudo apt-get update
-sudo apt-get install redis-server
-
-# CentOS系统
-sudo yum install redis
-sudo systemctl start redis
-```
-
-### 2. 安装PHP的Redis扩展
-
-```bash
-sudo pecl install redis
-```
-
-### 3. 修改队列配置
-
-编辑 `config/queue.php` 文件,根据实际环境修改Redis连接信息。
-
-## 二、系统组件说明
-
-### 1. 命令行任务
-已实现的命令行任务:
-- `device:list`: 获取设备列表命令
-
-### 2. 队列任务
-已实现的队列任务:
-- `DeviceListJob`: 处理设备列表获取并自动翻页的任务
-
-## 三、配置步骤
-
-### 1. 确保API连接信息正确
-在 `.env` 文件中确保以下配置正确:
-- `api.wechat_url`: API基础URL地址
-- `api.username`: API登录用户名
-- `api.password`: API登录密码
-
-## 四、系统架构说明
-
-### 1. 授权服务
-系统使用了公共授权服务 `app\common\service\AuthService`,用于管理API授权信息:
-- 提供静态方法 `getSystemAuthorization()`,可被所有定时任务和控制器复用
-- 从环境变量(.env文件)中读取API连接信息
-- 自动缓存授权Token,有效期为10分钟,避免频繁请求API
-- 缓存失效后自动重新获取授权信息
-
-### 2. 队列任务
-队列任务统一放置在 `application/job` 目录中:
-- 目前已实现 `DeviceListJob` 用于获取设备列表
-- 每个任务类需实现 `fire` 方法来处理队列任务
-- 任务可以通过 `think queue:work` 命令处理
-- 失败任务会自动重试,最多3次
-
-### 3. 定时命令
-定时命令统一放置在 `application/common/command` 目录中:
-- 继承自 `BaseCommand` 基类
-- 需要在 `application/command.php` 中注册命令
-- 可通过 `php think` 命令调用
-
-## 五、配置定时任务
-
-### 1. 编辑crontab配置
-
-```bash
-crontab -e
-```
-
-### 2. 直接配置PHP命令执行定时任务
-
-```
-# 每5分钟执行一次设备列表获取任务
-*/5 * * * * cd /www/wwwroot/yi.54iis.com && php think device:list >> /www/wwwroot/yi.54iis.com/logs/device_list.log 2>&1
-```
-
-说明:
-- `cd /www/wwwroot/yi.54iis.com`: 切换到项目目录
-- `php think device:list`: 执行设备列表命令
-- `>> /www/wwwroot/yi.54iis.com/logs/device_list.log 2>&1`: 将输出和错误信息追加到日志文件
-
-### 3. 创建日志目录
-
-```bash
-# 确保日志目录存在
-mkdir -p /www/wwwroot/yi.54iis.com/logs
-chmod 755 /www/wwwroot/yi.54iis.com/logs
-```
-
-## 六、配置队列处理进程
-
-### 1. 使用crontab监控队列进程
-
-```
-# 每分钟检查队列进程,如果不存在则启动
-* * * * * ps aux | grep "php think queue:work" | grep -v grep > /dev/null || (cd /www/wwwroot/yi.54iis.com && nohup php think queue:work --queue device_list --tries 3 --sleep 3 >> /www/wwwroot/yi.54iis.com/logs/queue_worker.log 2>&1 &)
-```
-
-说明:
-- `ps aux | grep "php think queue:work" | grep -v grep > /dev/null`: 检查队列进程是否存在
-- `||`: 如果前面的命令失败(进程不存在),则执行后面的命令
-- `(cd /www/wwwroot/yi.54iis.com && nohup...)`: 进入项目目录并启动队列处理进程
-
-### 2. 或者使用supervisor管理队列进程(推荐)
-
-如果服务器上安装了supervisor,可以创建配置文件 `/etc/supervisor/conf.d/device_queue.conf`:
-
-```ini
-[program:device_queue]
-process_name=%(program_name)s_%(process_num)02d
-command=php /www/wwwroot/yi.54iis.com/think queue:work --queue device_list --tries 3 --sleep 3
-autostart=true
-autorestart=true
-user=www
-numprocs=1
-redirect_stderr=true
-stdout_logfile=/www/wwwroot/yi.54iis.com/logs/queue_worker.log
-```
-
-然后重新加载supervisor配置:
-
-```bash
-sudo supervisorctl reread
-sudo supervisorctl update
-sudo supervisorctl start device_queue:*
-```
-
-## 七、测试
-
-### 1. 手动执行命令
-
-```bash
-# 进入项目目录
-cd /www/wwwroot/yi.54iis.com
-
-# 执行设备列表获取命令
-php think device:list
-```
-
-### 2. 查看日志
-
-```bash
-# 查看定时任务日志
-cat /www/wwwroot/yi.54iis.com/logs/device_list.log
-
-# 查看队列处理日志
-cat /www/wwwroot/yi.54iis.com/logs/queue_worker.log
-```
-
-## 八、新增定时任务流程
-
-如需添加新的定时任务,请按照以下步骤操作:
-
-1. 在 `application/common/command` 目录下创建新的命令类
-2. 在 `application/job` 目录下创建对应的队列任务处理类
-3. 在 `application/command.php` 文件中注册新命令
-4. 更新crontab配置,添加新的命令执行计划
-
-示例:添加一个每天凌晨2点执行的数据备份任务
-
-```
-0 2 * * * cd /www/wwwroot/yi.54iis.com && php think backup:data >> /www/wwwroot/yi.54iis.com/logs/backup_data.log 2>&1
-```
-
-新增的定时任务可直接使用 `AuthService::getSystemAuthorization()` 获取授权信息,无需重复实现授权逻辑。
-
-## 九、注意事项
-
-1. 确保PHP命令可以正常执行(如果默认PHP版本不匹配,可能需要使用完整路径,例如 `/www/server/php/74/bin/php`)
-2. 确保Redis服务正常运行
-3. 确保API连接信息配置正确
-4. 确保日志目录存在且有写入权限
-5. 定时任务执行用户需要有项目目录的读写权限
-6. 如果使用宝塔面板,可以在【计划任务】中配置上述crontab任务
\ No newline at end of file
diff --git a/Server/scripts/init-menu.php b/Server/scripts/init-menu.php
deleted file mode 100644
index d1ed8ad5..00000000
--- a/Server/scripts/init-menu.php
+++ /dev/null
@@ -1,173 +0,0 @@
- Env::get('database.type', 'mysql'),
- 'hostname' => Env::get('database.hostname', '127.0.0.1'),
- 'database' => Env::get('database.database', 'database'),
- 'username' => Env::get('database.username', 'root'),
- 'password' => Env::get('database.password', 'root'),
- 'hostport' => Env::get('database.hostport', '3306'),
- 'charset' => Env::get('database.charset', 'utf8mb4'),
- 'prefix' => Env::get('database.prefix', 'tk_'),
-];
-
-// 连接数据库
-try {
- $dsn = "{$dbConfig['type']}:host={$dbConfig['hostname']};port={$dbConfig['hostport']};dbname={$dbConfig['database']};charset={$dbConfig['charset']}";
- $pdo = new PDO($dsn, $dbConfig['username'], $dbConfig['password']);
- $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
- echo "数据库连接成功!\n";
-} catch (PDOException $e) {
- die("数据库连接失败: " . $e->getMessage() . "\n");
-}
-
-// 创建菜单表SQL
-$createTableSql = "
-CREATE TABLE IF NOT EXISTS `{$dbConfig['prefix']}menus` (
- `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '菜单ID',
- `title` varchar(50) NOT NULL COMMENT '菜单名称',
- `path` varchar(100) NOT NULL COMMENT '路由路径',
- `icon` varchar(50) DEFAULT NULL COMMENT '图标名称',
- `parent_id` int(11) NOT NULL DEFAULT '0' COMMENT '父菜单ID,0表示顶级菜单',
- `status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '状态:1启用,0禁用',
- `sort` int(11) NOT NULL DEFAULT '0' COMMENT '排序,数值越小越靠前',
- `create_time` int(11) DEFAULT NULL COMMENT '创建时间',
- `update_time` int(11) DEFAULT NULL COMMENT '更新时间',
- PRIMARY KEY (`id`),
- KEY `idx_parent_id` (`parent_id`),
- KEY `idx_status` (`status`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统菜单表';
-";
-
-// 执行创建表SQL
-try {
- $pdo->exec($createTableSql);
- echo "菜单表创建成功!\n";
-} catch (PDOException $e) {
- echo "菜单表创建失败: " . $e->getMessage() . "\n";
-}
-
-// 检查表中是否已有数据
-$checkSql = "SELECT COUNT(*) FROM `{$dbConfig['prefix']}menus`";
-try {
- $count = $pdo->query($checkSql)->fetchColumn();
- if ($count > 0) {
- echo "菜单表中已有 {$count} 条数据,跳过数据初始化\n";
- exit(0);
- }
-} catch (PDOException $e) {
- echo "检查数据失败: " . $e->getMessage() . "\n";
- exit(1);
-}
-
-// 插入顶级菜单数据
-$topMenus = [
- ['仪表盘', '/dashboard', 'LayoutDashboard', 0, 1, 10],
- ['项目管理', '/dashboard/projects', 'FolderKanban', 0, 1, 20],
- ['客户池', '/dashboard/customers', 'Users', 0, 1, 30],
- ['管理员权限', '/dashboard/admins', 'Settings', 0, 1, 40],
- ['系统设置', '/settings', 'Cog', 0, 1, 50],
-];
-
-$insertTopMenuSql = "INSERT INTO `{$dbConfig['prefix']}menus`
- (`title`, `path`, `icon`, `parent_id`, `status`, `sort`, `create_time`, `update_time`)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)";
-
-$timestamp = time();
-$insertStmt = $pdo->prepare($insertTopMenuSql);
-
-$pdo->beginTransaction();
-try {
- foreach ($topMenus as $index => $menu) {
- $insertStmt->execute([
- $menu[0], // title
- $menu[1], // path
- $menu[2], // icon
- $menu[3], // parent_id
- $menu[4], // status
- $menu[5], // sort
- $timestamp,
- $timestamp
- ]);
- }
- $pdo->commit();
- echo "顶级菜单数据插入成功!\n";
-} catch (PDOException $e) {
- $pdo->rollBack();
- echo "顶级菜单数据插入失败: " . $e->getMessage() . "\n";
- exit(1);
-}
-
-// 查询刚插入的顶级菜单ID
-$menuIds = [];
-$queryTopMenuSql = "SELECT id, title FROM `{$dbConfig['prefix']}menus` WHERE parent_id = 0";
-try {
- $topMenusResult = $pdo->query($queryTopMenuSql)->fetchAll(PDO::FETCH_ASSOC);
- foreach ($topMenusResult as $menu) {
- $menuIds[$menu['title']] = $menu['id'];
- }
-} catch (PDOException $e) {
- echo "查询顶级菜单失败: " . $e->getMessage() . "\n";
- exit(1);
-}
-
-// 插入子菜单数据
-$subMenus = [
- ['项目列表', '/dashboard/projects', 'List', $menuIds['项目管理'], 1, 21],
- ['新建项目', '/dashboard/projects/new', 'PlusCircle', $menuIds['项目管理'], 1, 22],
- ['客户管理', '/dashboard/customers', 'Users', $menuIds['客户池'], 1, 31],
- ['客户分析', '/dashboard/customers/analytics', 'BarChart', $menuIds['客户池'], 1, 32],
- ['管理员列表', '/dashboard/admins', 'UserCog', $menuIds['管理员权限'], 1, 41],
- ['角色管理', '/dashboard/admins/roles', 'ShieldCheck', $menuIds['管理员权限'], 1, 42],
- ['权限设置', '/dashboard/admins/permissions', 'Lock', $menuIds['管理员权限'], 1, 43],
- ['基本设置', '/settings/general', 'Settings', $menuIds['系统设置'], 1, 51],
- ['安全设置', '/settings/security', 'Shield', $menuIds['系统设置'], 1, 52],
-];
-
-$insertSubMenuSql = "INSERT INTO `{$dbConfig['prefix']}menus`
- (`title`, `path`, `icon`, `parent_id`, `status`, `sort`, `create_time`, `update_time`)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)";
-
-$pdo->beginTransaction();
-try {
- $insertStmt = $pdo->prepare($insertSubMenuSql);
- foreach ($subMenus as $menu) {
- $insertStmt->execute([
- $menu[0], // title
- $menu[1], // path
- $menu[2], // icon
- $menu[3], // parent_id
- $menu[4], // status
- $menu[5], // sort
- $timestamp,
- $timestamp
- ]);
- }
- $pdo->commit();
- echo "子菜单数据插入成功!\n";
-} catch (PDOException $e) {
- $pdo->rollBack();
- echo "子菜单数据插入失败: " . $e->getMessage() . "\n";
- exit(1);
-}
-
-echo "菜单初始化完成!\n";
\ No newline at end of file