Browse Source

feat: 添加高级配置权限验证功能及相关组件

master
qinjian 1 month ago
parent
commit
b7182c7066
  1. 114
      client/src/sections/wuyuanbiaoba/components/AdvancedConfigAuth.jsx
  2. 1
      client/src/sections/wuyuanbiaoba/components/index.js
  3. 210
      client/src/sections/wuyuanbiaoba/container/index.jsx
  4. 1
      client/src/sections/wuyuanbiaoba/hooks/useAuth.js
  5. 4
      config.cjs

114
client/src/sections/wuyuanbiaoba/components/AdvancedConfigAuth.jsx

@ -0,0 +1,114 @@
import React, { useState } from 'react';
import { Card, Input, Button, message } from 'antd';
import { LockOutlined } from '@ant-design/icons';
/**
* 高级配置密码验证组件
*/
const AdvancedConfigAuth = ({ verifyPassword, onUnlock }) => {
const [password, setPassword] = useState('');
const [loading, setLoading] = useState(false);
const handleVerify = async () => {
if (!password) {
message.warning('请输入密码');
return;
}
setLoading(true);
try {
const isValid = await verifyPassword(password);
if (isValid) {
message.success('密码正确,已解锁高级配置');
onUnlock && onUnlock();
} else {
message.error('密码错误,请重试');
setPassword('');
}
} catch (error) {
console.error('密码验证失败:', error);
message.error('验证过程出错,请重试');
} finally {
setLoading(false);
}
};
const handleKeyPress = (e) => {
if (e.key === 'Enter') {
handleVerify();
}
};
return (
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
minHeight: 'calc(100vh - 92px)',
padding: '24px',
}}
>
<Card
style={{
width: 400,
textAlign: 'center',
boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
}}
>
<div
style={{
width: 64,
height: 64,
borderRadius: '50%',
backgroundColor: '#e6f4ff',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
margin: '0 auto 24px',
}}
>
<LockOutlined style={{ fontSize: 32, color: '#1890ff' }} />
</div>
<h2 style={{ marginBottom: 8 }}>内部高级配置</h2>
<p style={{ color: '#999', marginBottom: 24 }}>
请输入管理员访问密码以继续
</p>
<Input.Password
size="large"
placeholder="请输入密码"
value={password}
onChange={(e) => setPassword(e.target.value)}
onKeyPress={handleKeyPress}
prefix={<LockOutlined style={{ color: '#999' }} />}
style={{ marginBottom: 16 }}
/>
<Button
type="primary"
size="large"
block
loading={loading}
onClick={handleVerify}
>
解锁配置
</Button>
<div
style={{
marginTop: 24,
fontSize: 12,
color: '#999',
}}
>
视觉位移计配置工具 v{window.env?.FS_VERSION || ''} Build {new Date().getFullYear()}
</div>
</Card>
</div>
);
};
export default AdvancedConfigAuth;

1
client/src/sections/wuyuanbiaoba/components/index.js

@ -5,3 +5,4 @@ export { default as RealtimeCharts } from './RealtimeCharts';
export { default as RealtimeDataTable } from './RealtimeDataTable'; export { default as RealtimeDataTable } from './RealtimeDataTable';
export { default as TemplateModal } from './TemplateModal'; export { default as TemplateModal } from './TemplateModal';
export { default as TargetDetailModal } from './TargetDetailModal'; export { default as TargetDetailModal } from './TargetDetailModal';
export { default as AdvancedConfigAuth } from './AdvancedConfigAuth';

210
client/src/sections/wuyuanbiaoba/container/index.jsx

@ -1,5 +1,6 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import { Tabs, Typography } from "antd"; import { Tabs, Typography, Menu } from "antd";
import { MonitorOutlined, LockOutlined } from "@ant-design/icons";
import ExcelJS from "exceljs"; import ExcelJS from "exceljs";
import { import {
@ -10,6 +11,7 @@ import {
RealtimeDataTable, RealtimeDataTable,
TemplateModal, TemplateModal,
TargetDetailModal, TargetDetailModal,
AdvancedConfigAuth,
} from "../components"; } from "../components";
import { import {
WebSocketProvider, WebSocketProvider,
@ -18,6 +20,7 @@ import {
} from "../actions/websocket.jsx"; } from "../actions/websocket.jsx";
import { useTemplateStorage } from "../hooks/useTemplateStorage.js"; import { useTemplateStorage } from "../hooks/useTemplateStorage.js";
import { useTargetStorage } from "../hooks/useTargetStorage.js"; import { useTargetStorage } from "../hooks/useTargetStorage.js";
import { useAuth } from "../hooks/useAuth.js";
import { useRef } from "react"; import { useRef } from "react";
const { Title } = Typography; const { Title } = Typography;
@ -93,6 +96,12 @@ const WuyuanbiaobaContent = () => {
// //
const [selectedTargetId, setSelectedTargetId] = useState(null); const [selectedTargetId, setSelectedTargetId] = useState(null);
//
const [currentMenu, setCurrentMenu] = useState("monitor");
// Hook
const { isUnlocked, verifyPassword, logout } = useAuth();
// //
const processRealtimeData = (data) => { const processRealtimeData = (data) => {
if (!data || !data.data || !Array.isArray(data.data)) { if (!data || !data.data || !Array.isArray(data.data)) {
@ -483,91 +492,156 @@ const WuyuanbiaobaContent = () => {
backgroundColor: "#f0f2f5", backgroundColor: "#f0f2f5",
}} }}
> >
{/* Header 区域 */} {/* Header 区域 - 固定在顶部 */}
<div <div
style={{ style={{
position: "fixed",
top: 0,
left: 0,
right: 0,
height: "60px", height: "60px",
backgroundColor: "white", backgroundColor: "white",
color: "#333", color: "#333",
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
// justifyContent: "space-between",
padding: "0 24px", padding: "0 24px",
flexShrink: 0,
borderBottom: "1px solid #d9d9d9", borderBottom: "1px solid #d9d9d9",
zIndex: 1000,
}} }}
> >
<Title level={3} style={{ color: "#333", margin: 0 }}> <Title level={3} style={{ color: "#333", margin: 0 }}>
视觉位移计配置工具 视觉位移计配置工具
</Title> </Title>
</div> <Menu
mode="horizontal"
{/* 中间区域 - 固定视口剩余高度 */} selectedKeys={[currentMenu]}
<div onClick={({ key }) => setCurrentMenu(key)}
style={{
height: "calc(100vh - 60px)",
display: "flex",
margin: "16px",
}}
>
{/* Camera 区域 */}
<CameraView
selectedTargetId={selectedTargetId}
onClearSelection={handleClearSelection}
onRectangleClick={handleRectangleClick}
selectedTemplate={
selectedTemplate ? getTemplateByKey(selectedTemplate) : null
}
// Hook
targets={targetListData}
targetsLoading={targetsLoading}
onRefreshTargets={refreshTargets}
/>
{/* 右侧 Target List / Temp List 区域 */}
<div
style={{ style={{
flex: 1, border: "none",
backgroundColor: "white", fontSize: "18px",
display: "flex", justifyContent: "flex-end"
flexDirection: "column",
}} }}
> items={[
<Tabs {
defaultActiveKey="target-list" key: "monitor",
items={tabItems} icon: <MonitorOutlined />,
style={{ label: "实时监控",
height: "100%", },
display: "flex", {
flexDirection: "column", key: "advanced",
}} icon: <LockOutlined />,
centered label: "高级配置",
size="large" },
tabBarGutter={64} ]}
/>
</div>
</div>
{/* 底部区域 - 在视口下方,需要滚动查看 */}
<div
style={{
margin: "16px",
marginTop: 0,
minHeight: "600px",
display: "flex",
backgroundColor: "white",
}}
>
{/* Charts 区域 */}
<RealtimeCharts
tableData={tableData}
lastUpdateTime={lastUpdateTime}
onDataExport={dataExport}
exportCount={exportCountRef.current}
setExportCount={setExportCount}
/> />
</div>
{/* Table 区域 - 使用采样数据显示 */} {/* 内容区域 - 添加顶部padding以避免被固定header遮挡 */}
<RealtimeDataTable realtimeData={realtimeData} /> <div style={{ paddingTop: "60px" }}>
{/* 实时监控页面 */}
{currentMenu === "monitor" && (
<>
{/* 中间区域 - 固定视口剩余高度 */}
<div
style={{
height: "calc(100vh - 60px)",
display: "flex",
margin: "16px",
}}
>
{/* Camera 区域 */}
<CameraView
selectedTargetId={selectedTargetId}
onClearSelection={handleClearSelection}
onRectangleClick={handleRectangleClick}
selectedTemplate={
selectedTemplate
? getTemplateByKey(selectedTemplate)
: null
}
// Hook
targets={targetListData}
targetsLoading={targetsLoading}
onRefreshTargets={refreshTargets}
/>
{/* 右侧 Target List / Temp List 区域 */}
<div
style={{
flex: 1,
backgroundColor: "white",
display: "flex",
flexDirection: "column",
}}
>
<Tabs
defaultActiveKey="target-list"
items={tabItems}
style={{
height: "100%",
display: "flex",
flexDirection: "column",
}}
centered
size="large"
tabBarGutter={64}
/>
</div>
</div>
{/* 底部区域 - 在视口下方,需要滚动查看 */}
<div
style={{
margin: "16px",
marginTop: 0,
minHeight: "600px",
display: "flex",
backgroundColor: "white",
}}
>
{/* Charts 区域 */}
<RealtimeCharts
tableData={tableData}
lastUpdateTime={lastUpdateTime}
onDataExport={dataExport}
exportCount={exportCountRef.current}
setExportCount={setExportCount}
/>
{/* Table 区域 - 使用采样数据显示 */}
<RealtimeDataTable realtimeData={realtimeData} />
</div>
</>
)}
{/* 高级配置页面 */}
{currentMenu === "advanced" && (
<div style={{ margin: "16px" }}>
{isUnlocked ? (
<div
style={{
padding: "24px",
backgroundColor: "white",
minHeight: "calc(100vh - 92px)",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<div style={{ textAlign: "center", color: "#999" }}>
<LockOutlined style={{ fontSize: 64, marginBottom: 16 }} />
<div style={{ fontSize: 18 }}>高级配置功能开发中...</div>
</div>
</div>
) : (
<AdvancedConfigAuth
verifyPassword={verifyPassword}
onUnlock={() => {}}
/>
)}
</div>
)}
</div> </div>
{/* 模板编辑模态框 */} {/* 模板编辑模态框 */}

1
client/src/sections/wuyuanbiaoba/hooks/useAuth.js

@ -0,0 +1 @@
import { useState, useEffect, useCallback } from "react"; const CORRECT_PASSWORD_HASH = "77796ac7e66ecc44954287ed7de7096c4016dd6ffb2763091c4eb3bc4d28b6dc", AUTH_STATUS_KEY = "advanced_config_unlocked"; async function generatePasswordHash(e) { try { var t = (new TextEncoder).encode(e), c = await crypto.subtle.digest("SHA-256", t); return Array.from(new Uint8Array(c)).map(e => e.toString(16).padStart(2, "0")).join("") } catch (e) { throw console.error("密码哈希生成失败:", e), e } } function checkUnlockStatus() { return "true" === sessionStorage.getItem(AUTH_STATUS_KEY) } function setUnlockStatus(e) { e ? sessionStorage.setItem(AUTH_STATUS_KEY, "true") : sessionStorage.removeItem(AUTH_STATUS_KEY) } function useAuth() { let [e, t] = useState(checkUnlockStatus); return useEffect(() => { t(checkUnlockStatus()) }, []), { isUnlocked: e, verifyPassword: useCallback(async e => { try { return e ? await generatePasswordHash(e) === CORRECT_PASSWORD_HASH && (setUnlockStatus(!0), t(!0), !0) : !1 } catch (e) { return console.error("密码验证失败:", e), !1 } }, []), logout: useCallback(() => { setUnlockStatus(!1), t(!1) }, []) } } export { useAuth, generatePasswordHash, checkUnlockStatus };

4
config.cjs

@ -1,8 +1,11 @@
const path = require('path') const path = require('path')
const packageJson = require('./package.json')
const flag = process.env.npm_lifecycle_script.includes('--mode localdev') ? 'localdev' : null; const flag = process.env.npm_lifecycle_script.includes('--mode localdev') ? 'localdev' : null;
if (flag) { if (flag) {
process.env.FS_FLAG = flag; process.env.FS_FLAG = flag;
} }
process.env.FS_VERSION = packageJson.version;
module.exports = { module.exports = {
env: process.env.NODE_ENV || 'development', // 运行环境 development | production env: process.env.NODE_ENV || 'development', // 运行环境 development | production
port: process.env.PORT || 5000, // 服务端口 port: process.env.PORT || 5000, // 服务端口
@ -12,6 +15,7 @@ module.exports = {
'/client/assets/script/peace.js' '/client/assets/script/peace.js'
], ],
flag: flag, // 自定义环境标识 flag: flag, // 自定义环境标识
version: packageJson.version, // 版本号
proxy: [{ // 代理配置 proxy: [{ // 代理配置
path: '/_api', path: '/_api',
target: process.env.API, target: process.env.API,

Loading…
Cancel
Save