This commit is contained in:
wong
2025-04-28 17:57:34 +08:00
15 changed files with 459 additions and 475 deletions

View File

@@ -6,34 +6,21 @@ use think\Controller;
use think\facade\Env;
use app\common\service\AuthService;
class BaseController extends Controller {
class BaseController extends Controller
{
/**
* 令牌
*
* @var string
*/
protected $token = '';
protected $baseUrl;
protected $authorization = '';
public function __construct() {
public function __construct()
{
parent::__construct();
$this->baseUrl = Env::get('api.wechat_url');
// 允许跨域
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: *');
header('Access-Control-Allow-Headers: *');
$this->authorization = AuthService::getSystemAuthorization();
}
}

View File

@@ -14,46 +14,46 @@ class AllowCrossDomain
*/
public function handle($request, \Closure $next)
{
// 获取当前请求的域名
$origin = $request->header('origin');
// 当请求使用 credentials 模式时,不能使用通配符
// 必须指定具体的域名或提取请求中的 Origin
$allowOrigin = '*';
if ($origin) {
// 如果需要限制特定域名,可以在这里判断
// 以下是允许的域名列表,如果请求来自这些域名之一,则允许跨域
$allowDomains = [ /* */ ];
// 如果请求来源在允许列表中,直接使用该源
if (in_array($origin, $allowDomains)) {
$allowOrigin = $origin;
}
}
// 设置允许的请求头信息
$allowHeaders = [
'Authorization', 'Content-Type', 'If-Match', 'If-Modified-Since',
'If-None-Match', 'If-Unmodified-Since', 'X-Requested-With',
'X-Token', 'X-Api-Token', 'Accept', 'Origin'
];
$response = $next($request);
// 添加跨域响应头
$response->header([
'Access-Control-Allow-Origin' => $allowOrigin,
'Access-Control-Allow-Headers' => implode(', ', $allowHeaders),
'Access-Control-Allow-Methods' => 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Credentials' => 'true',
'Access-Control-Max-Age' => '86400',
]);
// 对于预检请求,直接返回成功响应
if ($request->method(true) == 'OPTIONS') {
return response()->code(200);
}
return $response;
// // 获取当前请求的域名
// $origin = $request->header('origin');
//
// // 当请求使用 credentials 模式时,不能使用通配符
// // 必须指定具体的域名或提取请求中的 Origin
// $allowOrigin = '*';
// if ($origin) {
// // 如果需要限制特定域名,可以在这里判断
// // 以下是允许的域名列表,如果请求来自这些域名之一,则允许跨域
// $allowDomains = [ /* */ ];
//
// // 如果请求来源在允许列表中,直接使用该源
// if (in_array($origin, $allowDomains)) {
// $allowOrigin = $origin;
// }
// }
//
// // 设置允许的请求头信息
// $allowHeaders = [
// 'Authorization', 'Content-Type', 'If-Match', 'If-Modified-Since',
// 'If-None-Match', 'If-Unmodified-Since', 'X-Requested-With',
// 'X-Token', 'X-Api-Token', 'Accept', 'Origin'
// ];
//
// $response = $next($request);
//
// // 添加跨域响应头
// $response->header([
// 'Access-Control-Allow-Origin' => $allowOrigin,
// 'Access-Control-Allow-Headers' => implode(', ', $allowHeaders),
// 'Access-Control-Allow-Methods' => 'GET, POST, PUT, DELETE, OPTIONS',
// 'Access-Control-Allow-Credentials' => 'true',
// 'Access-Control-Max-Age' => '86400',
// ]);
//
// // 对于预检请求,直接返回成功响应
// if ($request->method(true) == 'OPTIONS') {
// return response()->code(200);
// }
//
// return $response;
}
}

View File

@@ -27,4 +27,6 @@ return [
'httponly' => '',
// 是否使用 setcookie
'setcookie' => true,
// 跨站需要
'samesite' => 'None',
];

View File

@@ -14,12 +14,10 @@ namespace think;
////处理跨域预检请求
if($_SERVER['REQUEST_METHOD'] == 'OPTIONS'){
//允许的源域名
header("Access-Control-Allow-Origin: *");
//允许的请求头信息
header("Access-Control-Allow-Headers: Origin, X-Requested-With, Content-Type, Accept, Authorization");
//允许的请求类型
header('Access-Control-Allow-Methods: GET, POST, PUT,DELETE,OPTIONS,PATCH');
header("Access-Control-Allow-Origin: " . (isset($_SERVER['HTTP_ORIGIN']) ? $_SERVER['HTTP_ORIGIN'] : '*'));
header("Access-Control-Allow-Headers: Origin, X-Requested-With, Content-Type, Accept, Authorization, Cookie");
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS, PATCH');
header("Access-Control-Allow-Credentials: true");
exit;
}
@@ -32,4 +30,4 @@ require __DIR__ . '/../thinkphp/base.php';
// 支持事先使用静态方法设置Request对象和Config对象
// 执行应用并响应
Container::get('app')->run()->send();
Container::get('app')->run()->send();

View File

@@ -12,9 +12,9 @@
use think\facade\Route;
// 允许跨域
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Origin: ' . (isset($_SERVER['HTTP_ORIGIN']) ? $_SERVER['HTTP_ORIGIN'] : '*'));
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS, PATCH');
header('Access-Control-Allow-Headers: Authorization, Content-Type, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since, X-Requested-With, X-Token, X-Api-Token');
header('Access-Control-Allow-Headers: Cookie, Authorization, Content-Type, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since, X-Requested-With, X-Token, X-Api-Token');
header('Access-Control-Max-Age: 1728000');
header('Access-Control-Allow-Credentials: true');

View File

@@ -7,6 +7,7 @@ import { toast } from "sonner"
import useAuthCheck from "@/hooks/useAuthCheck"
import { getAdminInfo, getGreeting } from "@/lib/utils"
import ClientOnly from "@/components/ClientOnly"
import { apiRequest } from '@/lib/api-utils'
interface DashboardStats {
companyCount: number
@@ -28,16 +29,14 @@ export default function DashboardPage() {
useAuthCheck()
useEffect(() => {
const fetchData = async () => {
setIsLoading(true)
const fetchDashboardData = async () => {
try {
// 获取统计信息
const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/dashboard/base`)
const data = await response.json()
if (data.code === 200) {
setStats(data.data)
setIsLoading(true)
const result = await apiRequest('/dashboard/base')
if (result.code === 200 && result.data) {
setStats(result.data)
} else {
toast.error(data.msg || "获取统计信息失败")
toast.error(result.msg || "获取仪表盘数据失败")
}
// 获取用户信息
@@ -49,13 +48,14 @@ export default function DashboardPage() {
}
} catch (error) {
toast.error("网络错误,请稍后重试")
console.error("获取仪表盘数据失败:", error)
toast.error("网络错误,请稍后再试")
} finally {
setIsLoading(false)
}
}
fetchData()
fetchDashboardData()
}, [])
// 单独处理问候语,避免依赖问题

View File

@@ -13,6 +13,7 @@ import Link from "next/link"
import { toast, Toaster } from "sonner"
import Image from "next/image"
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from "@/components/ui/dialog"
import { apiRequest } from "@/lib/api-utils"
// 为React.use添加类型声明
declare module 'react' {
@@ -69,30 +70,14 @@ export default function EditProjectPage({ params }: { params: { id: string } })
useEffect(() => {
const fetchProject = async () => {
setIsLoading(true)
try {
const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/company/detail/${id}`)
const result = await apiRequest(`/company/detail/${id}`)
// 检查响应状态
if (!response.ok) {
toast.error(`获取失败: ${response.status} ${response.statusText}`);
setIsLoading(false);
return;
}
// 检查内容类型是否为JSON
const contentType = response.headers.get("content-type");
if (!contentType || !contentType.includes("application/json")) {
toast.error("服务器返回了非JSON格式的数据");
setIsLoading(false);
return;
}
const data = await response.json()
if (data.code === 200) {
setProject(data.data)
if (result.code === 200) {
setProject(result.data)
} else {
toast.error(data.msg || "获取项目信息失败")
toast.error(result.msg || "获取项目信息失败")
}
} catch (error) {
toast.error("网络错误,请稍后重试")
@@ -106,54 +91,25 @@ export default function EditProjectPage({ params }: { params: { id: string } })
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
if (password && password !== confirmPassword) {
toast.error("两次输入的密码不一致")
return
}
setIsSubmitting(true)
try {
const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/company/update`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
id: parseInt(id),
name: project?.name,
account: project?.account,
memo: project?.memo,
phone: project?.phone,
username: project?.username,
status: project?.status,
...(password && { password })
}),
const result = await apiRequest('/company/update', 'POST', {
id: id,
name: project?.name,
account: project?.account,
password: password,
memo: project?.memo,
phone: project?.phone,
username: project?.username,
status: project?.status.toString(),
})
// 检查响应状态
if (!response.ok) {
toast.error(`更新失败: ${response.status} ${response.statusText}`);
setIsSubmitting(false);
return;
}
// 检查内容类型是否为JSON
const contentType = response.headers.get("content-type");
if (!contentType || !contentType.includes("application/json")) {
toast.error("服务器返回了非JSON格式的数据");
setIsSubmitting(false);
return;
}
const data = await response.json()
if (data.code === 200) {
toast.success("更新成功")
if (result.code === 200) {
toast.success("项目更新成功")
router.push("/dashboard/projects")
} else {
toast.error(data.msg)
toast.error(result.msg || "更新失败")
}
} catch (error) {
toast.error("网络错误,请稍后重试")
@@ -177,33 +133,12 @@ export default function EditProjectPage({ params }: { params: { id: string } })
pollingCountRef.current = 0;
try {
const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/v1/api/device/add`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
accountId: project.s2_accountId
}),
const result = await apiRequest('/v1/api/device/add', 'POST', {
accountId: project.s2_accountId
})
// 检查响应状态
if (!response.ok) {
toast.error(`请求失败: ${response.status} ${response.statusText}`);
return;
}
// 检查内容类型是否为JSON
const contentType = response.headers.get("content-type");
if (!contentType || !contentType.includes("application/json")) {
toast.error("服务器返回了非JSON格式的数据");
return;
}
const data = await response.json()
if (data.code === 200 && data.data?.qrCode) {
setQrCodeData(data.data.qrCode)
if (result.code === 200 && result.data?.qrCode) {
setQrCodeData(result.data.qrCode)
setIsModalOpen(true)
// 五秒后开始轮询
@@ -211,7 +146,7 @@ export default function EditProjectPage({ params }: { params: { id: string } })
startPolling();
}, 5000);
} else {
toast.error(data.msg || "获取设备二维码失败")
toast.error(result.msg || "获取设备二维码失败")
}
} catch (error) {
toast.error("网络错误,请稍后重试")
@@ -248,33 +183,11 @@ export default function EditProjectPage({ params }: { params: { id: string } })
}
try {
const accountId = project.s2_accountId;
// 通过URL参数传递accountId
const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/devices/add-results?accountId=${accountId}`, {
method: "GET",
headers: {
"Content-Type": "application/json"
}
});
const result = await apiRequest(`/devices/add-results?accountId=${project.s2_accountId}`)
// 检查响应状态和内容类型
if (!response.ok) {
console.error("轮询请求失败:", response.status, response.statusText);
return;
}
// 检查内容类型是否为JSON
const contentType = response.headers.get("content-type");
if (!contentType || !contentType.includes("application/json")) {
console.error("轮询请求返回的不是JSON格式:", contentType);
return;
}
const data = await response.json();
if (data.code === 200) {
if (result.code === 200) {
// 检查是否最后一次轮询且设备未添加
if (pollingCountRef.current >= MAX_POLLING_COUNT && !data.added) {
if (pollingCountRef.current >= MAX_POLLING_COUNT && !result.data.added) {
setPollingStatus("error");
setIsQrCodeBroken(true);
stopPolling();
@@ -282,9 +195,9 @@ export default function EditProjectPage({ params }: { params: { id: string } })
}
// 检查设备是否已添加成功
if (data.added) {
if (result.data.added) {
setPollingStatus("success");
setAddedDevice(data.device);
setAddedDevice(result.data.device);
stopPolling();
// 刷新设备列表
@@ -293,7 +206,7 @@ export default function EditProjectPage({ params }: { params: { id: string } })
}
} else {
// 请求失败但继续轮询
console.error("轮询请求失败:", data.msg);
console.error("轮询请求失败:", result.msg);
}
} catch (error) {
console.error("轮询请求出错:", error);
@@ -304,27 +217,12 @@ export default function EditProjectPage({ params }: { params: { id: string } })
// 刷新项目数据的方法
const refreshProjectData = async () => {
try {
const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/company/detail/${id}`)
const result = await apiRequest(`/company/detail/${id}`)
// 检查响应状态
if (!response.ok) {
toast.error(`刷新失败: ${response.status} ${response.statusText}`);
return;
}
// 检查内容类型是否为JSON
const contentType = response.headers.get("content-type");
if (!contentType || !contentType.includes("application/json")) {
toast.error("服务器返回了非JSON格式的数据");
return;
}
const data = await response.json()
if (data.code === 200) {
setProject(data.data)
if (result.code === 200) {
setProject(result.data)
} else {
toast.error(data.msg || "刷新项目信息失败")
toast.error(result.msg || "刷新项目信息失败")
}
} catch (error) {
toast.error("网络错误,请稍后重试")

View File

@@ -13,6 +13,7 @@ import { toast } from "sonner"
import { use } from "react"
import Image from "next/image"
import { Badge } from "@/components/ui/badge"
import { apiRequest } from '@/lib/api-utils'
interface ProjectProfile {
id: number
@@ -69,75 +70,68 @@ export default function ProjectDetailPage({ params }: ProjectDetailPageProps) {
const [subUsers, setSubUsers] = useState<SubUser[]>([])
const [activeTab, setActiveTab] = useState("overview")
useEffect(() => {
const fetchProjectProfile = async () => {
const fetchProject = async () => {
try {
setIsLoading(true)
try {
const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/company/profile/${id}`)
const data = await response.json()
if (data.code === 200) {
setProfile(data.data)
} else {
toast.error(data.msg || "获取项目信息失败")
}
} catch (error) {
toast.error("网络错误,请稍后重试")
} finally {
setIsLoading(false)
const result = await apiRequest(`/company/profile/${id}`)
if (result.code === 200 && result.data) {
setProfile(result.data)
} else {
toast.error(result.msg || "获取项目信息失败")
}
} catch (error) {
console.error("获取项目信息失败:", error)
toast.error("网络错误,请稍后再试")
} finally {
setIsLoading(false)
}
}
fetchProjectProfile()
useEffect(() => {
fetchProject()
}, [id])
useEffect(() => {
const fetchDevices = async () => {
if (activeTab === "devices") {
setIsDevicesLoading(true)
try {
const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/company/devices?companyId=${id}`)
const data = await response.json()
if (data.code === 200) {
setDevices(data.data)
} else {
toast.error(data.msg || "获取设备列表失败")
}
} catch (error) {
toast.error("网络错误,请稍后重试")
} finally {
setIsDevicesLoading(false)
}
const fetchDevices = async () => {
try {
setIsDevicesLoading(true)
const result = await apiRequest(`/company/devices?companyId=${id}`)
if (result.code === 200 && result.data) {
setDevices(result.data)
} else {
toast.error(result.msg || "获取设备列表失败")
}
} catch (error) {
console.error("获取设备列表失败:", error)
toast.error("网络错误,请稍后再试")
} finally {
setIsDevicesLoading(false)
}
}
useEffect(() => {
fetchDevices()
}, [activeTab, id])
useEffect(() => {
const fetchSubUsers = async () => {
if (activeTab === "accounts") {
setIsSubUsersLoading(true)
try {
const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/company/subusers?companyId=${id}`)
const data = await response.json()
if (data.code === 200) {
setSubUsers(data.data)
} else {
toast.error(data.msg || "获取子账号列表失败")
}
} catch (error) {
toast.error("网络错误,请稍后重试")
} finally {
setIsSubUsersLoading(false)
setIsSubUsersLoading(true)
try {
const result = await apiRequest(`/company/subusers?companyId=${id}`)
if (result.code === 200) {
setSubUsers(result.data)
} else {
toast.error(result.msg || "获取子账号列表失败")
}
} catch (error) {
toast.error("网络错误,请稍后重试")
} finally {
setIsSubUsersLoading(false)
}
}
fetchSubUsers()
}, [activeTab, id])
}, [id])
if (isLoading) {
return <div className="flex items-center justify-center min-h-screen">...</div>

View File

@@ -12,6 +12,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
import { ArrowLeft } from "lucide-react"
import Link from "next/link"
import { toast, Toaster } from "sonner"
import { apiRequest } from "@/lib/api-utils"
export default function NewProjectPage() {
const router = useRouter()
@@ -53,29 +54,21 @@ export default function NewProjectPage() {
setIsSubmitting(true)
try {
const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/company/add`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
name: formData.name,
account: formData.account,
password: formData.password,
memo: formData.description,
phone: formData.phone,
username: formData.nickname,
status: parseInt(formData.status),
}),
const result = await apiRequest('/company/add', 'POST', {
name: formData.name,
account: formData.account,
password: formData.password,
memo: formData.description,
phone: formData.phone,
username: formData.nickname,
status: parseInt(formData.status),
})
const data = await response.json()
if (data.code === 200) {
toast.success("创建成功")
if (result.code === 200) {
toast.success("项目创建成功")
router.push("/dashboard/projects")
} else {
toast.error(data.msg)
toast.error(result.msg || "创建失败")
}
} catch (error) {
toast.error("网络错误,请稍后重试")

View File

@@ -19,6 +19,7 @@ import {
import { Badge } from "@/components/ui/badge"
import { PaginationControls } from "@/components/ui/pagination-controls"
import { useTabContext } from "@/app/dashboard/layout"
import { apiRequest } from "@/lib/api-utils"
interface Project {
id: number
@@ -73,15 +74,14 @@ export default function ProjectsPage() {
const fetchProjects = async () => {
setIsLoading(true)
try {
const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/company/list?page=${currentPage}&limit=${pageSize}`)
const data = await response.json()
const result = await apiRequest(`/company/list?page=${currentPage}&limit=${pageSize}`)
if (data.code === 200) {
setProjects(data.data.list)
setTotalItems(data.data.total)
setTotalPages(Math.ceil(data.data.total / pageSize))
if (result.code === 200) {
setProjects(result.data.list)
setTotalItems(result.data.total)
setTotalPages(Math.ceil(result.data.total / pageSize))
} else {
toast.error(data.msg || "获取项目列表失败")
toast.error(result.msg || "获取项目列表失败")
setProjects([])
setTotalItems(0)
setTotalPages(0)
@@ -124,33 +124,24 @@ export default function ProjectsPage() {
setIsDeleting(true)
try {
const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/company/delete`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
id: deletingProjectId
}),
const result = await apiRequest('/company/delete', 'POST', {
id: deletingProjectId
})
const data = await response.json()
if (data.code === 200) {
if (result.code === 200) {
toast.success("删除成功")
// Fetch projects again after delete
const fetchProjects = async () => {
setIsLoading(true)
try {
const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/company/list?page=${currentPage}&limit=${pageSize}`)
const data = await response.json()
if (data.code === 200) {
setProjects(data.data.list)
setTotalItems(data.data.total)
setTotalPages(Math.ceil(data.data.total / pageSize))
if (currentPage > Math.ceil(data.data.total / pageSize) && Math.ceil(data.data.total / pageSize) > 0) {
setCurrentPage(Math.ceil(data.data.total / pageSize));
const result = await apiRequest(`/company/list?page=${currentPage}&limit=${pageSize}`)
if (result.code === 200) {
setProjects(result.data.list)
setTotalItems(result.data.total)
setTotalPages(Math.ceil(result.data.total / pageSize))
if (currentPage > Math.ceil(result.data.total / pageSize) && Math.ceil(result.data.total / pageSize) > 0) {
setCurrentPage(Math.ceil(result.data.total / pageSize));
}
} else {
setProjects([]); setTotalItems(0); setTotalPages(0);
@@ -160,7 +151,7 @@ export default function ProjectsPage() {
}
fetchProjects();
} else {
toast.error(data.msg || "删除失败")
toast.error(result.msg || "删除失败")
}
} catch (error) {
toast.error("网络错误,请稍后重试")

View File

@@ -24,6 +24,7 @@ import {
CardTitle,
} from "@/components/ui/card"
import { toast } from "sonner"
import { apiRequest } from '@/lib/api-utils'
const formSchema = z.object({
name: z.string().min(2, "项目名称至少需要2个字符"),
@@ -63,35 +64,39 @@ export default function ProjectCreate({ onSuccess }: ProjectCreateProps) {
const onSubmit = async (values: z.infer<typeof formSchema>) => {
setIsLoading(true)
try {
const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/company/create`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
name: values.name,
account: values.account,
password: values.password,
phone: values.phone || null,
realname: values.realname || null,
nickname: values.nickname || null,
memo: values.memo || null,
}),
// 从localStorage获取token和admin_id
const token = localStorage.getItem('admin_token')
const adminId = localStorage.getItem('admin_id')
// 设置cookie
if (token && adminId) {
const domain = new URL(process.env.NEXT_PUBLIC_API_BASE_URL || '').hostname
document.cookie = `admin_token=${token}; path=/; domain=${domain}`
document.cookie = `admin_id=${adminId}; path=/; domain=${domain}`
}
const result = await apiRequest('/company/create', 'POST', {
name: values.name,
account: values.account,
password: values.password,
phone: values.phone || null,
realname: values.realname || null,
nickname: values.nickname || null,
memo: values.memo || null,
})
const data = await response.json()
if (data.code === 200) {
if (result.code === 200) {
toast.success("项目创建成功")
form.reset()
if (onSuccess) {
onSuccess()
}
} else {
toast.error(data.msg || "创建项目失败")
toast.error(result.msg || "创建项目失败")
}
} catch (error) {
toast.error("网络错误,请稍后重试")
console.error("创建项目失败:", error)
toast.error("网络错误,请稍后再试")
} finally {
setIsLoading(false)
}

View File

@@ -10,6 +10,7 @@ import { ArrowLeft, Edit } from "lucide-react"
import { toast } from "sonner"
import Image from "next/image"
import { Badge } from "@/components/ui/badge"
import { apiRequest } from '@/lib/api-utils'
interface ProjectProfile {
id: number
@@ -63,74 +64,67 @@ export default function ProjectDetail({ projectId, onEdit }: ProjectDetailProps)
const [subUsers, setSubUsers] = useState<SubUser[]>([])
const [activeTab, setActiveTab] = useState("overview")
useEffect(() => {
const fetchProjectProfile = async () => {
const fetchProject = async () => {
try {
setIsLoading(true)
try {
const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/company/profile/${projectId}`)
const data = await response.json()
if (data.code === 200) {
setProfile(data.data)
} else {
toast.error(data.msg || "获取项目信息失败")
}
} catch (error) {
toast.error("网络错误,请稍后重试")
} finally {
setIsLoading(false)
const result = await apiRequest(`/company/profile/${projectId}`)
if (result.code === 200 && result.data) {
setProfile(result.data)
} else {
toast.error(result.msg || "获取项目信息失败")
}
} catch (error) {
console.error("获取项目信息失败:", error)
toast.error("网络错误,请稍后再试")
} finally {
setIsLoading(false)
}
}
fetchProjectProfile()
const fetchDevices = async () => {
try {
setIsLoading(true)
const result = await apiRequest(`/company/devices?companyId=${projectId}`)
if (result.code === 200 && result.data) {
setDevices(result.data)
} else {
toast.error(result.msg || "获取设备列表失败")
}
} catch (error) {
console.error("获取设备列表失败:", error)
toast.error("网络错误,请稍后再试")
} finally {
setIsLoading(false)
}
}
const fetchSubusers = async () => {
try {
setIsLoading(true)
const result = await apiRequest(`/company/subusers?companyId=${projectId}`)
if (result.code === 200 && result.data) {
setSubUsers(result.data)
} else {
toast.error(result.msg || "获取子用户列表失败")
}
} catch (error) {
console.error("获取子用户列表失败:", error)
toast.error("网络错误,请稍后再试")
} finally {
setIsLoading(false)
}
}
useEffect(() => {
fetchProject()
}, [projectId])
useEffect(() => {
const fetchDevices = async () => {
if (activeTab === "devices") {
setIsDevicesLoading(true)
try {
const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/company/devices?companyId=${projectId}`)
const data = await response.json()
if (data.code === 200) {
setDevices(data.data)
} else {
toast.error(data.msg || "获取设备列表失败")
}
} catch (error) {
toast.error("网络错误,请稍后重试")
} finally {
setIsDevicesLoading(false)
}
}
}
fetchDevices()
}, [activeTab, projectId])
useEffect(() => {
const fetchSubUsers = async () => {
if (activeTab === "accounts") {
setIsSubUsersLoading(true)
try {
const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/company/subusers?companyId=${projectId}`)
const data = await response.json()
if (data.code === 200) {
setSubUsers(data.data)
} else {
toast.error(data.msg || "获取子账号列表失败")
}
} catch (error) {
toast.error("网络错误,请稍后重试")
} finally {
setIsSubUsersLoading(false)
}
}
}
fetchSubUsers()
fetchSubusers()
}, [activeTab, projectId])
if (isLoading) {

View File

@@ -24,6 +24,8 @@ import {
CardTitle,
} from "@/components/ui/card"
import { toast } from "sonner"
import { apiRequest } from '@/lib/api-utils'
import { useRouter } from "next/navigation"
const formSchema = z.object({
name: z.string().min(2, "项目名称至少需要2个字符"),
@@ -50,6 +52,8 @@ interface ProjectData {
export default function ProjectEdit({ projectId, onSuccess }: ProjectEditProps) {
const [isLoading, setIsLoading] = useState(false)
const [isFetching, setIsFetching] = useState(true)
const [project, setProject] = useState<ProjectData | null>(null)
const router = useRouter()
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
@@ -66,74 +70,54 @@ export default function ProjectEdit({ projectId, onSuccess }: ProjectEditProps)
// 获取项目数据
useEffect(() => {
const fetchProject = async () => {
setIsFetching(true)
try {
const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/company/profile/${projectId}`)
const data = await response.json()
if (data.code === 200) {
const project = data.data
setIsLoading(true)
const result = await apiRequest(`/company/profile/${projectId}`)
if (result.code === 200 && result.data) {
setProject(result.data)
form.reset({
name: project.name || "",
account: project.account || "",
name: result.data.name || "",
account: result.data.account || "",
password: "",
confirmPassword: "",
phone: project.phone || "",
memo: project.memo || "",
phone: result.data.phone || "",
memo: result.data.memo || "",
})
} else {
toast.error(data.msg || "获取项目信息失败")
toast.error(result.msg || "获取项目信息失败")
}
} catch (error) {
toast.error("网络错误,请稍后重试")
console.error("获取项目信息失败:", error)
toast.error("网络错误,请稍后再试")
} finally {
setIsFetching(false)
setIsLoading(false)
}
}
fetchProject()
}, [projectId, form])
const onSubmit = async (values: z.infer<typeof formSchema>) => {
// 检查密码是否匹配
if (values.password && values.password !== values.confirmPassword) {
toast.error("两次输入的密码不一致")
return
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setIsLoading(true)
try {
// 准备请求数据,根据需要添加或移除字段
const updateData: Record<string, any> = {
id: parseInt(projectId),
name: values.name,
account: values.account,
memo: values.memo,
phone: values.phone,
}
// 如果提供了密码,则包含密码字段
if (values.password) {
updateData.password = values.password
}
const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/company/update`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(updateData),
const result = await apiRequest('/company/update', 'POST', {
id: projectId,
name: project?.name,
account: project?.account,
memo: project?.memo,
phone: project?.phone,
username: project?.username,
status: project?.status,
...(form.getValues('password') && { password: form.getValues('password') })
})
const data = await response.json()
if (data.code === 200) {
toast.success("项目更新成功")
if (onSuccess) {
onSuccess()
}
if (result.code === 200) {
toast.success("更新成功")
router.push("/dashboard/projects")
} else {
toast.error(data.msg || "更新项目失败")
toast.error(result.msg)
}
} catch (error) {
toast.error("网络错误,请稍后重试")
@@ -154,7 +138,7 @@ export default function ProjectEdit({ projectId, onSuccess }: ProjectEditProps)
</CardHeader>
<CardContent>
<Form {...form}>
<form id="edit-project-form" onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
<form id="edit-project-form" onSubmit={handleSubmit} className="space-y-8">
<FormField
control={form.control}
name="name"

View File

@@ -0,0 +1,139 @@
"use client"
import { useState, useEffect } from "react"
import { apiRequest } from '@/lib/api-utils'
import { toast } from "sonner"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
import { useRouter } from "next/navigation"
interface Project {
id: number
name: string
account: string
phone: string
status: number
createTime: string
}
export default function ProjectList() {
const [projects, setProjects] = useState<Project[]>([])
const [isLoading, setIsLoading] = useState(false)
const router = useRouter()
const fetchProjects = async () => {
try {
setIsLoading(true)
const params = new URLSearchParams()
params.append('page', '1')
params.append('limit', '10')
const result = await apiRequest(`/company/list?${params.toString()}`)
if (result.code === 200 && result.data) {
setProjects(result.data)
} else {
toast.error(result.msg || "获取项目列表失败")
}
} catch (error) {
console.error("获取项目列表失败:", error)
toast.error("网络错误,请稍后再试")
} finally {
setIsLoading(false)
}
}
const handleDelete = async (id: number) => {
try {
setIsLoading(true)
const result = await apiRequest('/company/delete', 'POST', { id })
if (result.code === 200) {
toast.success("删除成功")
fetchProjects()
} else {
toast.error(result.msg || "删除失败")
}
} catch (error) {
console.error("删除项目失败:", error)
toast.error("网络错误,请稍后再试")
} finally {
setIsLoading(false)
}
}
useEffect(() => {
fetchProjects()
}, [])
return (
<div className="space-y-4">
<div className="flex justify-between items-center">
<h2 className="text-2xl font-bold"></h2>
<Button onClick={() => router.push('/dashboard/projects/create')}>
</Button>
</div>
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{projects.map((project) => (
<TableRow key={project.id}>
<TableCell>{project.name}</TableCell>
<TableCell>{project.account}</TableCell>
<TableCell>{project.phone || '-'}</TableCell>
<TableCell>
<Badge variant={project.status === 1 ? "success" : "destructive"}>
{project.status === 1 ? "正常" : "禁用"}
</Badge>
</TableCell>
<TableCell>{project.createTime}</TableCell>
<TableCell>
<div className="space-x-2">
<Button
variant="outline"
size="sm"
onClick={() => router.push(`/dashboard/projects/${project.id}`)}
>
</Button>
<Button
variant="outline"
size="sm"
onClick={() => router.push(`/dashboard/projects/${project.id}/edit`)}
>
</Button>
<Button
variant="destructive"
size="sm"
onClick={() => handleDelete(project.id)}
>
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</div>
)
}

View File

@@ -3,79 +3,78 @@ import { getConfig } from './config';
/**
* API响应接口
*/
export interface ApiResponse<T = any> {
interface ApiResponse {
code: number;
msg: string;
data: T | null;
data?: any;
}
/**
* API请求函数
* @param endpoint API端点
* @param method HTTP方法
* @param data 请求数据
* @param body 请求数据
* @param headers 请求头
* @returns API响应
*/
export async function apiRequest<T = any>(
export async function apiRequest(
endpoint: string,
method: 'GET' | 'POST' | 'PUT' | 'DELETE' = 'GET',
data?: any
): Promise<ApiResponse<T>> {
// 获取API基础URL
const { apiBaseUrl } = getConfig();
const url = `${apiBaseUrl}${endpoint}`;
method: string = 'GET',
body?: any,
headers: HeadersInit = {}
): Promise<ApiResponse> {
const url = `${process.env.NEXT_PUBLIC_API_BASE_URL}${endpoint}`;
// 构建请求头
const headers: HeadersInit = {
'Content-Type': 'application/json',
};
// 从localStorage获取token和admin_id
const token = typeof window !== 'undefined' ? localStorage.getItem('admin_token') : null;
const adminId = typeof window !== 'undefined' ? localStorage.getItem('admin_id') : null;
// 添加认证信息(如果有)
if (typeof window !== 'undefined') {
const token = localStorage.getItem('admin_token');
if (token) {
// 设置Cookie中的认证信息
document.cookie = `admin_token=${token}; path=/`;
}
// 设置cookie
if (typeof window !== 'undefined' && token && adminId) {
const domain = new URL(process.env.NEXT_PUBLIC_API_BASE_URL || '').hostname;
document.cookie = `admin_token=${token}; path=/; domain=${domain}`;
document.cookie = `admin_id=${adminId}; path=/; domain=${domain}`;
}
// 构建请求选项
const defaultHeaders = {
'Content-Type': 'application/json',
'Accept': 'application/json',
...headers
};
const options: RequestInit = {
method,
headers,
credentials: 'same-origin', // 改为same-origin避免跨域请求发送Cookie
headers: defaultHeaders,
credentials: 'include', // 允许跨域请求携带cookies
mode: 'cors', // 明确指定使用CORS模式
...(body && { body: JSON.stringify(body) })
};
// 添加请求体针对POST、PUT请求
if (method !== 'GET' && data) {
options.body = JSON.stringify(data);
}
try {
// 发送请求
const response = await fetch(url, options);
const data = await response.json();
// 解析响应
const result = await response.json();
// 如果响应状态码不是2xx或者接口返回的code不是200抛出错误
if (!response.ok || (result && result.code !== 200)) {
// 如果接口返回的code不是200抛出错误
if (data && data.code !== 200) {
// 如果是认证错误,清除登录信息
if (result.code === 401) {
if (data.code === 401) {
if (typeof window !== 'undefined') {
localStorage.removeItem('admin_id');
localStorage.removeItem('admin_name');
localStorage.removeItem('admin_account');
localStorage.removeItem('admin_token');
// 清除cookie
const domain = new URL(process.env.NEXT_PUBLIC_API_BASE_URL || '').hostname;
document.cookie = 'admin_token=; path=/; domain=' + domain + '; expires=Thu, 01 Jan 1970 00:00:00 GMT';
document.cookie = 'admin_id=; path=/; domain=' + domain + '; expires=Thu, 01 Jan 1970 00:00:00 GMT';
}
}
throw result; // 抛出响应结果作为错误
throw data; // 抛出响应结果作为错误
}
return result;
return data;
} catch (error) {
// 直接抛出错误,由调用方处理
console.error('API请求失败:', error);
throw error;
}
}