feat: 使用全局ip与port配置摄像头

This commit is contained in:
SikongJueluo 2025-07-09 21:54:34 +08:00
parent 48501d79e2
commit 15f9b68e7d
No known key found for this signature in database
1 changed files with 52 additions and 415 deletions

View File

@ -5,19 +5,7 @@
<div class="card bg-base-200 shadow-xl"> <div class="card bg-base-200 shadow-xl">
<div class="card-body"> <div class="card-body">
<h2 class="card-title text-primary"> <h2 class="card-title text-primary">
<svg <Settings class="w-6 h-6" />
class="w-6 h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 100 4m0-4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 100 4m0-4v2m0-6V4"
/>
</svg>
控制面板 控制面板
</h2> </h2>
@ -45,19 +33,7 @@
<div class="stats shadow"> <div class="stats shadow">
<div class="stat"> <div class="stat">
<div class="stat-figure text-secondary"> <div class="stat-figure text-secondary">
<svg <Video class="w-8 h-8" />
class="w-8 h-8"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z"
/>
</svg>
</div> </div>
<div class="stat-title">视频规格</div> <div class="stat-title">视频规格</div>
<div class="stat-value text-secondary"> <div class="stat-value text-secondary">
@ -71,19 +47,7 @@
<div class="stats shadow"> <div class="stats shadow">
<div class="stat"> <div class="stat">
<div class="stat-figure text-accent"> <div class="stat-figure text-accent">
<svg <Users class="w-8 h-8" />
class="w-8 h-8"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"
/>
</svg>
</div> </div>
<div class="stat-title">连接数</div> <div class="stat-title">连接数</div>
<div class="stat-value text-accent"> <div class="stat-value text-accent">
@ -124,70 +88,6 @@
</div> </div>
</div> </div>
<!-- 摄像头配置 -->
<div class="card bg-base-100 shadow-sm mt-4">
<div class="card-body">
<h3 class="card-title text-sm text-info">
<svg
class="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"
/>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
/>
</svg>
摄像头配置
</h3>
<div class="flex flex-row justify-around gap-4">
<div class="grow">
<IpInputField
v-model="tempCameraConfig.address"
required
/>
</div>
<div class="grow">
<PortInputField
v-model="tempCameraConfig.port"
required
/>
</div>
</div>
<div class="card-actions justify-end mt-4">
<button
class="btn btn-ghost"
@click="resetCameraConfig"
:disabled="isDefaultCamera"
>
<RotateCcw class="w-4 h-4" />
重置
</button>
<button
class="btn btn-primary"
@click="confirmCameraConfig"
:disabled="!isValidCameraConfig || !hasChangesCamera"
:class="{ loading: configuring }"
>
<Save class="w-4 h-4" v-if="!configuring" />
{{ configuring ? "配置中..." : "保存配置" }}
</button>
</div>
</div>
</div>
<!-- 操作按钮 --> <!-- 操作按钮 -->
<div class="card-actions justify-end mt-4"> <div class="card-actions justify-end mt-4">
<button <button
@ -195,26 +95,8 @@
@click="refreshStatus" @click="refreshStatus"
:disabled="loading" :disabled="loading"
> >
<svg <RefreshCw v-if="loading" class="animate-spin h-4 w-4 mr-2" />
v-if="loading" <RefreshCw v-else class="h-4 w-4 mr-2" />
class="animate-spin h-4 w-4 mr-2"
fill="none"
viewBox="0 0 24 24"
>
<circle
class="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
stroke-width="4"
></circle>
<path
class="opacity-75"
fill="currentColor"
d="m4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
{{ loading ? "刷新中..." : "刷新状态" }} {{ loading ? "刷新中..." : "刷新状态" }}
</button> </button>
<button <button
@ -222,26 +104,8 @@
@click="testConnection" @click="testConnection"
:disabled="testing" :disabled="testing"
> >
<svg <RefreshCw v-if="testing" class="animate-spin h-4 w-4 mr-2" />
v-if="testing" <TestTube v-else class="h-4 w-4 mr-2" />
class="animate-spin h-4 w-4 mr-2"
fill="none"
viewBox="0 0 24 24"
>
<circle
class="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
stroke-width="4"
></circle>
<path
class="opacity-75"
fill="currentColor"
d="m4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
{{ testing ? "测试中..." : "测试连接" }} {{ testing ? "测试中..." : "测试连接" }}
</button> </button>
</div> </div>
@ -252,19 +116,7 @@
<div class="card bg-base-200 shadow-xl"> <div class="card bg-base-200 shadow-xl">
<div class="card-body"> <div class="card-body">
<h2 class="card-title text-primary"> <h2 class="card-title text-primary">
<svg <Video class="w-6 h-6" />
class="w-6 h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z"
/>
</svg>
视频预览 视频预览
</h2> </h2>
@ -294,20 +146,7 @@
<div class="card bg-error text-white shadow-lg w-full max-w-lg"> <div class="card bg-error text-white shadow-lg w-full max-w-lg">
<div class="card-body"> <div class="card-body">
<h3 class="card-title flex items-center gap-2"> <h3 class="card-title flex items-center gap-2">
<svg <AlertTriangle class="h-6 w-6" />
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
/>
</svg>
视频流加载失败 视频流加载失败
</h3> </h3>
<p>无法连接到视频服务器请检查以下内容</p> <p>无法连接到视频服务器请检查以下内容</p>
@ -334,19 +173,7 @@
class="absolute inset-0 flex items-center justify-center text-white" class="absolute inset-0 flex items-center justify-center text-white"
> >
<div class="text-center"> <div class="text-center">
<svg <Video class="w-16 h-16 mx-auto mb-4 opacity-50" />
class="w-16 h-16 mx-auto mb-4 opacity-50"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z"
/>
</svg>
<p class="text-lg opacity-75">{{ videoStatus }}</p> <p class="text-lg opacity-75">{{ videoStatus }}</p>
<p class="text-sm opacity-60 mt-2"> <p class="text-sm opacity-60 mt-2">
点击"播放视频流"按钮开始查看实时视频 点击"播放视频流"按钮开始查看实时视频
@ -370,20 +197,7 @@
role="button" role="button"
class="btn btn-sm btn-outline btn-accent" class="btn btn-sm btn-outline btn-accent"
> >
<svg <MoreHorizontal class="w-4 h-4 mr-1" />
class="w-4 h-4 mr-1"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg>
更多功能 更多功能
</div> </div>
<ul <ul
@ -391,15 +205,22 @@
class="dropdown-content z-[1] menu p-2 shadow bg-base-200 rounded-box w-52" class="dropdown-content z-[1] menu p-2 shadow bg-base-200 rounded-box w-52"
> >
<li> <li>
<a @click="openInNewTab(streamInfo.htmlUrl)" <a @click="openInNewTab(streamInfo.htmlUrl)">
>在新标签打开视频页面</a <ExternalLink class="w-4 h-4" />
> 在新标签打开视频页面
</a>
</li> </li>
<li><a @click="takeSnapshot">获取并下载快照</a></li>
<li> <li>
<a @click="copyToClipboard(streamInfo.mjpegUrl)" <a @click="takeSnapshot">
>复制MJPEG地址</a <Camera class="w-4 h-4" />
> 获取并下载快照
</a>
</li>
<li>
<a @click="copyToClipboard(streamInfo.mjpegUrl)">
<Copy class="w-4 h-4" />
复制MJPEG地址
</a>
</li> </li>
</ul> </ul>
</div> </div>
@ -408,25 +229,7 @@
@click="startStream" @click="startStream"
:disabled="isPlaying" :disabled="isPlaying"
> >
<svg <Play class="w-4 h-4 mr-1" />
class="w-4 h-4 mr-1"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"
/>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
播放视频流 播放视频流
</button> </button>
<button <button
@ -434,25 +237,7 @@
@click="stopStream" @click="stopStream"
:disabled="!isPlaying" :disabled="!isPlaying"
> >
<svg <Square class="w-4 h-4 mr-1" />
class="w-4 h-4 mr-1"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 10a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1v-4z"
/>
</svg>
停止视频流 停止视频流
</button> </button>
</div> </div>
@ -464,19 +249,7 @@
<div class="card bg-base-200 shadow-xl"> <div class="card bg-base-200 shadow-xl">
<div class="card-body"> <div class="card-body">
<h2 class="card-title text-primary"> <h2 class="card-title text-primary">
<svg <FileText class="w-6 h-6" />
class="w-6 h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
/>
</svg>
操作日志 操作日志
</h2> </h2>
@ -511,21 +284,30 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, reactive, watch, onMounted, onUnmounted } from "vue"; import { ref, onMounted, onUnmounted } from "vue";
import { useStorage } from "@vueuse/core";
import { z } from "zod";
import { import {
Save, Settings,
RotateCcw, Video,
Users,
RefreshCw,
TestTube,
Play,
Square,
ExternalLink,
Camera,
Copy,
FileText,
AlertTriangle,
MoreHorizontal,
} from "lucide-vue-next"; } from "lucide-vue-next";
import { VideoStreamClient, CameraConfigRequest } from "@/APIClient"; import { VideoStreamClient, CameraConfigRequest } from "@/APIClient";
import { IpInputField, PortInputField } from "@/components/InputField"; import { useEquipments } from "@/stores/equipments";
const eqps = useEquipments();
// //
const loading = ref(false); const loading = ref(false);
const testing = ref(false); const testing = ref(false);
const configuring = ref(false);
const testingCamera = ref(false);
const isPlaying = ref(false); const isPlaying = ref(false);
const hasVideoError = ref(false); const hasVideoError = ref(false);
const videoStatus = ref('点击"播放视频流"按钮开始查看实时视频'); const videoStatus = ref('点击"播放视频流"按钮开始查看实时视频');
@ -551,92 +333,6 @@ const streamInfo = ref({
snapshotUrl: "", snapshotUrl: "",
}); });
//
const cameraConfigSchema = z.object({
address: z
.string()
.ip({ version: "v4", message: "请输入有效的IPv4地址" })
.min(1, "请输入IP地址"),
port: z
.number()
.int("端口必须是整数")
.min(1, "端口必须大于0")
.max(65535, "端口必须小于等于65535"),
});
type CameraConfig = z.infer<typeof cameraConfigSchema>;
//
const defaultCameraConfig: CameraConfig = {
address: "192.168.1.100",
port: 8080,
};
// 使 VueUse
const cameraConfig = useStorage<CameraConfig>(
"camera-config",
defaultCameraConfig,
localStorage,
{
serializer: {
read: (value: string) => {
try {
const parsed = JSON.parse(value);
const result = cameraConfigSchema.safeParse(parsed);
return result.success ? result.data : defaultCameraConfig;
} catch {
return defaultCameraConfig;
}
},
write: (value: CameraConfig) => JSON.stringify(value),
},
},
);
//
const tempCameraConfig = reactive<CameraConfig>({
address: cameraConfig.value.address,
port: cameraConfig.value.port,
});
//
const isValidCameraConfig = computed(() => {
return tempCameraConfig.address && tempCameraConfig.port &&
tempCameraConfig.port >= 1 && tempCameraConfig.port <= 65535 &&
/^(\d{1,3}\.){3}\d{1,3}$/.test(tempCameraConfig.address);
});
//
const hasChangesCamera = computed(() => {
return (
tempCameraConfig.address !== cameraConfig.value.address ||
tempCameraConfig.port !== cameraConfig.value.port
);
});
const isDefaultCamera = computed(() => {
return (
defaultCameraConfig.address === tempCameraConfig.address &&
defaultCameraConfig.port === tempCameraConfig.port
);
});
//
const resetCameraConfig = () => {
tempCameraConfig.address = defaultCameraConfig.address;
tempCameraConfig.port = defaultCameraConfig.port;
};
//
watch(
cameraConfig,
(newConfig) => {
tempCameraConfig.address = newConfig.address;
tempCameraConfig.port = newConfig.port;
},
{ deep: true },
);
const currentVideoSource = ref(""); const currentVideoSource = ref("");
const logs = ref<Array<{ time: Date; level: string; message: string }>>([]); const logs = ref<Array<{ time: Date; level: string; message: string }>>([]);
@ -656,71 +352,6 @@ const addLog = (level: string, message: string) => {
} }
}; };
//
const loadCameraConfig = async () => {
try {
addLog("info", "正在加载摄像头配置...");
const config = await videoClient.getCameraConfig();
if (config && config.address && config.port) {
//
cameraConfig.value = {
address: config.address,
port: config.port,
};
addLog("success", `摄像头配置加载成功: ${config.address}:${config.port}`);
} else {
addLog("warning", "未找到保存的摄像头配置,使用默认值");
}
} catch (error) {
addLog("error", `加载摄像头配置失败: ${error}`);
console.error("加载摄像头配置失败:", error);
}
};
//
const confirmCameraConfig = async () => {
if (!isValidCameraConfig.value) return;
configuring.value = true;
try {
addLog(
"info",
`正在配置摄像头: ${tempCameraConfig.address}:${tempCameraConfig.port}`,
);
//
await new Promise((resolve) => setTimeout(resolve, 500));
//
cameraConfig.value = {
address: tempCameraConfig.address,
port: tempCameraConfig.port,
};
// 使API
const result = await videoClient.configureCamera(
CameraConfigRequest.fromJS({
address: tempCameraConfig.address,
port: tempCameraConfig.port,
}),
);
if (result) {
addLog("success", "摄像头配置保存成功");
//
await refreshStatus();
} else {
addLog("error", "摄像头配置保存失败");
}
} catch (error) {
addLog("error", `配置摄像头失败: ${error}`);
console.error("配置摄像头失败:", error);
} finally {
configuring.value = false;
}
};
// //
const formatTime = (time: Date) => { const formatTime = (time: Date) => {
return time.toLocaleTimeString(); return time.toLocaleTimeString();
@ -798,6 +429,13 @@ const takeSnapshot = async () => {
const refreshStatus = async () => { const refreshStatus = async () => {
loading.value = true; loading.value = true;
try { try {
addLog("info", "正在配置并初始化摄像头...");
const boardconfig = new CameraConfigRequest({
address: eqps.boardAddr,
port: eqps.boardPort,
});
await videoClient.configureCamera(boardconfig);
addLog("info", "正在获取服务状态..."); addLog("info", "正在获取服务状态...");
// 使API // 使API
@ -909,7 +547,6 @@ const stopStream = () => {
// //
onMounted(async () => { onMounted(async () => {
addLog("info", "HTTP 视频流页面已加载"); addLog("info", "HTTP 视频流页面已加载");
await loadCameraConfig();
await refreshStatus(); await refreshStatus();
}); });