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:
Pine
2026-08-24 18:22:44 +08:00
parent a935ad8904
commit e022146c51
5 changed files with 104 additions and 14 deletions
+7
View File
@@ -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
View File
@@ -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>
+74
View File
@@ -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>
);
}
+9
View File
@@ -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
View File
@@ -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 },
},
},
};