fix(园区端): 接口无反应(代理补 /park/tenants,/park/auth)+ 园区端改为管理屏幕
- vite: 增 /park/tenants、/park/auth 代理键(此前仅 /park/api,/park/tenants 被当作 SPA 返回 HTML→JSON 解析失败→界面空白/无反应)。 - park.ts/types: 增 screens CRUD(按 tenant) + ParkScreen。 - 新增 screens-tab(屏幕管理):登记本园区大屏设备(device_id/name/role/location)+删除;腾换 index 的“配置/建园区”为“屏幕管理”。 (“创建园区”改为运营端平台功能,见下一步园区账密登录的园区端视图建设。)
This commit is contained in:
@@ -7,6 +7,7 @@ import type {
|
||||
ParkCompany,
|
||||
ScreenData,
|
||||
Tenant,
|
||||
ParkScreen,
|
||||
} from "@/types";
|
||||
|
||||
/** 园区端(/park/api/*,server-core /park 前缀子应用,多租户按 tenant_id 隔离)。 */
|
||||
@@ -44,4 +45,10 @@ export const parkApi = {
|
||||
screenData: (tenantId: string) => api.get<ScreenData>("/park/api/screen/data", { tenant_id: tenantId }),
|
||||
updateScreenData: (tenantId: string, body: Partial<ScreenData>) =>
|
||||
api.put<ScreenData>(`/park/api/screen/data?tenant_id=${tenantId}`, body),
|
||||
|
||||
// ---- 屏幕管理(园区端创建/管理大屏设备) ----
|
||||
screens: (tenantId: string) => api.get<ParkScreen[]>("/park/api/screens", { tenant_id: tenantId }),
|
||||
createScreen: (tenantId: string, body: Partial<ParkScreen>) =>
|
||||
api.post<ParkScreen>(`/park/api/screens?tenant_id=${tenantId}`, body),
|
||||
deleteScreen: (tenantId: string, id: string) => api.del<{ ok: boolean }>(`/park/api/screens/${id}?tenant_id=${tenantId}`),
|
||||
};
|
||||
|
||||
+11
-13
@@ -3,7 +3,7 @@ import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { ParkTenantProvider, useParkTenant } from "./tenant-context";
|
||||
import { TenantTab } from "./tenant-tab";
|
||||
import { ScreensTab } from "./screens-tab";
|
||||
import { ScreenTab } from "./screen-tab";
|
||||
import { CompaniesTab } from "./companies-tab";
|
||||
import { KbTab } from "./kb-tab";
|
||||
@@ -18,14 +18,12 @@ function TenantBar() {
|
||||
<span className="text-sm text-muted-foreground">
|
||||
当前园区:<b className="text-foreground">{current?.name || tenantId || "未选择"}</b>
|
||||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<Select value={tenantId} onValueChange={setTenantId}>
|
||||
<SelectTrigger className="w-64"><SelectValue placeholder="选择园区" /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{tenants.map((t) => <SelectItem key={t.id} value={t.id}>{t.name}</SelectItem>)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<Select value={tenantId} onValueChange={setTenantId}>
|
||||
<SelectTrigger className="w-64"><SelectValue placeholder="选择园区" /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{tenants.map((t) => <SelectItem key={t.id} value={t.id}>{t.name}</SelectItem>)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -34,20 +32,20 @@ function ParkPageInner() {
|
||||
const { tenantId } = useParkTenant();
|
||||
return (
|
||||
<div>
|
||||
<PageHeader title="园区管理" description="多园区隔离:大屏控制、入驻企业、知识库、智能体与大屏数据" />
|
||||
<PageHeader title="园区管理" description="多园区隔离:屏幕、大屏控制、入驻企业、知识库、智能体与大屏数据" />
|
||||
<TenantBar />
|
||||
<Card>
|
||||
<CardContent className="pt-4">
|
||||
<Tabs defaultValue="screen">
|
||||
<Tabs defaultValue="screens">
|
||||
<TabsList>
|
||||
<TabsTrigger value="tenant">园区配置</TabsTrigger>
|
||||
<TabsTrigger value="screens">屏幕管理</TabsTrigger>
|
||||
<TabsTrigger value="screen">大屏控制台</TabsTrigger>
|
||||
<TabsTrigger value="companies">入驻企业</TabsTrigger>
|
||||
<TabsTrigger value="kb">园区知识库</TabsTrigger>
|
||||
<TabsTrigger value="agent">园区智能体</TabsTrigger>
|
||||
<TabsTrigger value="data">大屏数据</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="tenant"><TenantTab /></TabsContent>
|
||||
<TabsContent value="screens"><ScreensTab key={tenantId} /></TabsContent>
|
||||
<TabsContent value="screen"><ScreenTab key={tenantId} /></TabsContent>
|
||||
<TabsContent value="companies"><CompaniesTab key={tenantId} /></TabsContent>
|
||||
<TabsContent value="kb"><KbTab key={tenantId} /></TabsContent>
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import * as React from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { DataTable, type Column } from "@/components/generic/DataTable";
|
||||
import { useResource } from "@/components/generic/useResource";
|
||||
import { parkApi } from "@/api/park";
|
||||
import { useParkTenant } from "./tenant-context";
|
||||
import type { ParkScreen } from "@/types";
|
||||
|
||||
const EMPTY = { device_id: "", name: "", role: "main", location: "" };
|
||||
|
||||
export function ScreensTab() {
|
||||
const { tenantId } = useParkTenant();
|
||||
const res = useResource(() => parkApi.screens(tenantId));
|
||||
const [form, setForm] = React.useState(EMPTY);
|
||||
const [busy, setBusy] = React.useState(false);
|
||||
|
||||
const create = async () => {
|
||||
setBusy(true);
|
||||
try {
|
||||
await parkApi.createScreen(tenantId, form);
|
||||
setForm(EMPTY);
|
||||
res.reload();
|
||||
} finally { setBusy(false); }
|
||||
};
|
||||
const remove = async (id: string) => {
|
||||
if (!window.confirm("确认删除该屏幕?")) return;
|
||||
await parkApi.deleteScreen(tenantId, id);
|
||||
res.reload();
|
||||
};
|
||||
|
||||
const columns: Column<ParkScreen>[] = [
|
||||
{ key: "device_id", header: "设备 ID", cell: (r) => <span className="font-mono text-xs">{r.device_id}</span> },
|
||||
{ key: "name", header: "名称", cell: (r) => r.name || "-" },
|
||||
{ key: "role", header: "角色", cell: (r) => (r.role === "secondary" ? "副屏" : "主屏") },
|
||||
{ key: "location", header: "位置", cell: (r) => r.location || "-" },
|
||||
{ key: "created_at", header: "登记", cell: (r) => r.created_at || "-" },
|
||||
{ key: "actions", header: "操作", cell: (r) => <Button size="sm" variant="ghost" className="text-destructive" onClick={() => remove(r.id)}>删除</Button> },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<p className="text-sm text-muted-foreground">登记本园区的屏幕设备(device_id 对应该大屏的 MQTT client_id),用于精准控制大屏。</p>
|
||||
{res.error ? <p className="text-sm text-destructive">{res.error}</p> : null}
|
||||
<Card>
|
||||
<CardHeader className="pb-2"><CardTitle className="text-base">已登记屏幕({res.data.length})</CardTitle></CardHeader>
|
||||
<CardContent>
|
||||
<DataTable columns={columns} data={res.data} loading={res.loading} empty="暂无屏幕,请新增" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="pb-2"><CardTitle className="text-base">新增屏幕</CardTitle></CardHeader>
|
||||
<CardContent className="grid grid-cols-1 gap-4 md:grid-cols-4">
|
||||
<div className="space-y-1.5"><Label>设备 ID(client_id)</Label><Input value={form.device_id} onChange={(e) => setForm((f) => ({ ...f, device_id: e.target.value }))} placeholder="大屏的 MQTT client_id" /></div>
|
||||
<div className="space-y-1.5"><Label>名称</Label><Input value={form.name} onChange={(e) => setForm((f) => ({ ...f, name: e.target.value }))} /></div>
|
||||
<div className="space-y-1.5"><Label>角色</Label>
|
||||
<Select value={form.role} onValueChange={(v) => setForm((f) => ({ ...f, role: v }))}>
|
||||
<SelectTrigger className="w-full"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="main">主屏</SelectItem>
|
||||
<SelectItem value="secondary">副屏</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-1.5"><Label>位置</Label><Input value={form.location} onChange={(e) => setForm((f) => ({ ...f, location: e.target.value }))} /></div>
|
||||
</CardContent>
|
||||
<CardContent className="pt-0"><Button onClick={create} disabled={busy || !form.device_id}>新增屏幕</Button></CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -166,6 +166,15 @@ export interface Tenant {
|
||||
username?: string;
|
||||
}
|
||||
|
||||
export interface ParkScreen {
|
||||
id: string;
|
||||
device_id: string;
|
||||
name?: string;
|
||||
role?: string; // main | secondary
|
||||
location?: string;
|
||||
created_at?: string;
|
||||
}
|
||||
|
||||
export interface DisplayDevice {
|
||||
device_id: string;
|
||||
role?: string;
|
||||
|
||||
+3
-1
@@ -22,8 +22,10 @@ export default defineConfig(({ mode }) => {
|
||||
"/api": { target: env.VITE_API_PROXY || "http://127.0.0.1:8090", changeOrigin: true },
|
||||
"/auth": { target: env.VITE_API_PROXY || "http://127.0.0.1:8090", changeOrigin: true },
|
||||
"/admin": { target: env.VITE_API_PROXY || "http://127.0.0.1:8090", changeOrigin: true },
|
||||
// 仅代理 /park/api(园区 API),不要把 SPA 路由 /park 代理到后端,否则刷新返回 404
|
||||
// 园区 SaaS API 子路径(勿代理 SPA 路由 /park 本身,否则刷新 404)
|
||||
"/park/api": { target: env.VITE_API_PROXY || "http://127.0.0.1:8090", changeOrigin: true },
|
||||
"/park/tenants": { target: env.VITE_API_PROXY || "http://127.0.0.1:8090", changeOrigin: true },
|
||||
"/park/auth": { target: env.VITE_API_PROXY || "http://127.0.0.1:8090", changeOrigin: true },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user