私域操盘手 - 移除微信号账号详情的好友列表的包裹层组件,防止标签过多页面变形
This commit is contained in:
@@ -21,7 +21,7 @@ export default function RootLayout({
|
||||
}) {
|
||||
return (
|
||||
<html lang="zh-CN" suppressHydrationWarning>
|
||||
<body className="bg-gray-100">
|
||||
<body className="font-sans">
|
||||
<AuthProvider>
|
||||
<AuthCheck>
|
||||
<ErrorBoundary>
|
||||
|
||||
@@ -527,31 +527,33 @@ export default function WechatAccountDetailPage() {
|
||||
|
||||
// 获取账号概览数据
|
||||
const fetchSummaryData = useCallback(async () => {
|
||||
if (!account?.wechatId || isLoading) return;
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const response = await fetchWechatAccountSummary(account?.wechatId || '');
|
||||
const response = await fetchWechatAccountSummary(account.wechatId);
|
||||
if (response.code === 200) {
|
||||
setAccountSummary(response.data);
|
||||
} else {
|
||||
toast({
|
||||
} else {
|
||||
toast({
|
||||
title: "获取账号概览失败",
|
||||
description: response.msg || "请稍后再试",
|
||||
variant: "destructive"
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("获取账号概览失败:", error);
|
||||
toast({
|
||||
title: "获取账号概览失败",
|
||||
description: "请检查网络连接或稍后再试",
|
||||
variant: "destructive"
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("获取账号概览失败:", error);
|
||||
toast({
|
||||
title: "获取账号概览失败",
|
||||
description: "请检查网络连接或稍后再试",
|
||||
variant: "destructive"
|
||||
});
|
||||
} finally {
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [account]);
|
||||
}, [account?.wechatId]);
|
||||
|
||||
// 在页面加载时获取账号概览数据
|
||||
// 统一在账号数据加载完成后获取概览数据
|
||||
useEffect(() => {
|
||||
if (account?.wechatId) {
|
||||
fetchSummaryData();
|
||||
@@ -724,7 +726,7 @@ export default function WechatAccountDetailPage() {
|
||||
<TabsList className="grid w-full grid-cols-2">
|
||||
<TabsTrigger value="overview">账号概览</TabsTrigger>
|
||||
<TabsTrigger value="friends">
|
||||
好友列表{activeTab === "friends" && friendsTotal > 0 ? ` (${friendsTotal})` : ''}
|
||||
好友列表{activeTab === "friends" && friendsTotal > 0 ? ` (${friendsTotal.toLocaleString()})` : ''}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
@@ -849,7 +851,7 @@ export default function WechatAccountDetailPage() {
|
||||
</div>
|
||||
<div className="text-sm text-gray-500">
|
||||
根据当前账号权重({accountSummary.accountWeight.scope}分),每日最多可添加{" "}
|
||||
<span className="font-medium text-blue-600">{accountSummary.statistics.addLimit}</span>{" "}
|
||||
<span className="font-medium text-blue-600">{accountSummary.statistics.addLimit.toLocaleString()}</span>{" "}
|
||||
个好友
|
||||
</div>
|
||||
</div>
|
||||
@@ -886,110 +888,80 @@ export default function WechatAccountDetailPage() {
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="friends" className="space-y-4 mt-4">
|
||||
<Card className="p-4">
|
||||
<div className="space-y-4">
|
||||
{/* 搜索栏 */}
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-3 top-2.5 h-4 w-4 text-gray-400" />
|
||||
<Input
|
||||
placeholder="搜索好友昵称/微信号/备注/标签"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
|
||||
className="pl-9"
|
||||
/>
|
||||
</div>
|
||||
<Button variant="outline" size="icon" onClick={handleSearch}>
|
||||
<Filter className="h-4 w-4" />
|
||||
</Button>
|
||||
<TabsContent value="friends">
|
||||
<div className="space-y-4">
|
||||
{/* 搜索栏 */}
|
||||
<div className="flex items-center space-x-2 bg-white p-4 rounded-lg shadow-sm">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-3 top-2.5 h-4 w-4 text-gray-400" />
|
||||
<Input
|
||||
placeholder="搜索好友昵称/微信号/备注/标签"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
|
||||
className="pl-9 bg-white border-gray-200 focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
<Button variant="outline" size="icon" onClick={handleSearch} className="bg-white hover:bg-gray-50">
|
||||
<Filter className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 好友列表 */}
|
||||
<div
|
||||
ref={friendsContainerRef}
|
||||
className="space-y-2 transition-all duration-300"
|
||||
style={{
|
||||
minHeight: '80px',
|
||||
height: `${getFriendsContainerHeight()}px`,
|
||||
overflowY: 'auto'
|
||||
}}
|
||||
>
|
||||
{isFetchingFriends && friends.length === 0 ? (
|
||||
<div className="flex justify-center items-center py-8">
|
||||
<Loader2 className="h-6 w-6 animate-spin text-blue-500" />
|
||||
</div>
|
||||
) : friends.length === 0 && hasFriendLoadError ? (
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
<p>加载好友失败,请重试</p>
|
||||
<Button variant="outline" size="sm" className="mt-2" onClick={() => fetchFriends(1, true)}>
|
||||
重新加载
|
||||
</Button>
|
||||
</div>
|
||||
) : friends.length === 0 ? (
|
||||
<div className="text-center py-8 text-gray-500">未找到匹配的好友</div>
|
||||
) : (
|
||||
<>
|
||||
{friends.map((friend) => (
|
||||
{/* 好友列表 */}
|
||||
<div
|
||||
ref={friendsContainerRef}
|
||||
className="space-y-2 min-h-[200px]"
|
||||
style={{ height: getFriendsContainerHeight() }}
|
||||
>
|
||||
{isFetchingFriends && friends.length === 0 ? (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-blue-500" />
|
||||
</div>
|
||||
) : friends.length === 0 ? (
|
||||
<div className="text-center py-8 text-gray-500">未找到匹配的好友</div>
|
||||
) : (
|
||||
<>
|
||||
{friends.map((friend) => (
|
||||
<div
|
||||
key={friend.id}
|
||||
className="flex items-center p-3 border rounded-lg hover:bg-gray-50 cursor-pointer"
|
||||
className="flex items-center p-3 bg-white border rounded-lg hover:bg-gray-50 cursor-pointer transition-colors duration-200"
|
||||
onClick={() => handleFriendClick(friend)}
|
||||
>
|
||||
<Avatar className="h-10 w-10 mr-3">
|
||||
<AvatarImage src={friend.avatar} />
|
||||
<AvatarFallback>{friend.nickname?.[0] || 'U'}</AvatarFallback>
|
||||
<AvatarFallback>{friend.nickname?.[0] || 'U'}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="font-medium truncate max-w-[180px]">
|
||||
<div className="font-medium truncate max-w-[180px]">
|
||||
{friend.nickname}
|
||||
{friend.remark && <span className="text-gray-500 ml-1 truncate">({friend.remark})</span>}
|
||||
{friend.remark && <span className="text-gray-500 ml-1 truncate">({friend.remark})</span>}
|
||||
</div>
|
||||
<ChevronRight className="h-4 w-4 text-gray-400" />
|
||||
</div>
|
||||
<div className="text-sm text-gray-500 truncate">{friend.wechatId}</div>
|
||||
<div className="flex flex-wrap gap-1 mt-1">
|
||||
{friend.tags.slice(0, 3).map((tag: FriendTag) => (
|
||||
<span key={tag.id} className={`text-xs px-2 py-0.5 rounded-full ${tag.color}`}>
|
||||
{tag.name}
|
||||
{friend.tags?.map((tag, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className="inline-flex items-center px-2 py-0.5 rounded text-xs bg-blue-100 text-blue-800"
|
||||
>
|
||||
{typeof tag === 'string' ? tag : tag.name}
|
||||
</span>
|
||||
))}
|
||||
{friend.tags.length > 3 && (
|
||||
<span className="text-xs px-2 py-0.5 rounded-full bg-gray-100 text-gray-800">
|
||||
+{friend.tags.length - 3}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* 懒加载指示器 */}
|
||||
{hasMoreFriends && (
|
||||
<div ref={friendsLoadingRef} className="py-4 flex justify-center">
|
||||
{isFetchingFriends && <Loader2 className="h-6 w-6 animate-spin text-blue-500" />}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 显示加载状态和总数 */}
|
||||
<div className="text-sm text-gray-500 text-center">
|
||||
{friendsTotal > 0 ? (
|
||||
<span>
|
||||
已加载 {Math.min(friends.length, friendsTotal)} / {friendsTotal} 条记录
|
||||
</span>
|
||||
) : !isFetchingFriends && !hasFriendLoadError && account ? (
|
||||
<span>
|
||||
共 {account.friendCount} 条记录
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
))}
|
||||
{hasMoreFriends && (
|
||||
<div ref={friendsLoadingRef} className="flex justify-center py-4">
|
||||
<Loader2 className="h-6 w-6 animate-spin text-blue-500" />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
|
||||
43
Cunkebao/components/AuthCheck.tsx
Normal file
43
Cunkebao/components/AuthCheck.tsx
Normal file
@@ -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 (
|
||||
<div className="flex h-screen w-screen items-center justify-center">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-blue-500"></div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!isAuthenticated && !PUBLIC_PATHS.includes(pathname)) {
|
||||
return null
|
||||
}
|
||||
|
||||
return <>{children}</>
|
||||
}
|
||||
46
Cunkebao/hooks/useAuth.ts
Normal file
46
Cunkebao/hooks/useAuth.ts
Normal file
@@ -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<AuthState>({
|
||||
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
|
||||
}
|
||||
@@ -22,6 +22,9 @@ const nextConfig = {
|
||||
parallelServerCompiles: true,
|
||||
},
|
||||
reactStrictMode: false,
|
||||
compiler: {
|
||||
styledComponents: true,
|
||||
},
|
||||
}
|
||||
|
||||
mergeConfig(nextConfig, userConfig)
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>恭喜,站点创建成功!</title>
|
||||
<style>
|
||||
.container {
|
||||
width: 60%;
|
||||
margin: 10% auto 0;
|
||||
background-color: #f0f0f0;
|
||||
padding: 2% 5%;
|
||||
border-radius: 10px
|
||||
}
|
||||
|
||||
ul {
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
ul li {
|
||||
line-height: 2.3
|
||||
}
|
||||
|
||||
a {
|
||||
color: #20a53a
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>恭喜, 站点创建成功!</h1>
|
||||
<h3>这是默认index.html,本页面由系统自动生成</h3>
|
||||
<ul>
|
||||
<li>本页面在FTP根目录下的index.html</li>
|
||||
<li>您可以修改、删除或覆盖本页面</li>
|
||||
<li>FTP相关信息,请到“面板系统后台 > FTP” 查看</li>
|
||||
</ul>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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任务
|
||||
@@ -1,173 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* 菜单表初始化脚本
|
||||
* 执行该脚本,会创建菜单表并插入初始菜单数据
|
||||
* 执行方式: php init-menu.php
|
||||
*/
|
||||
|
||||
// 定义应用目录
|
||||
define('APP_PATH', __DIR__ . '/../application/');
|
||||
define('RUNTIME_PATH', __DIR__ . '/../runtime/');
|
||||
define('ROOT_PATH', __DIR__ . '/../');
|
||||
|
||||
// 加载框架引导文件
|
||||
require __DIR__ . '/../thinkphp/base.php';
|
||||
|
||||
// 加载环境变量
|
||||
use think\facade\Env;
|
||||
$rootPath = realpath(__DIR__ . '/../');
|
||||
Env::load($rootPath . '/.env');
|
||||
|
||||
// 读取数据库配置
|
||||
$dbConfig = [
|
||||
'type' => 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";
|
||||
Reference in New Issue
Block a user