chore: 初始化仓库基线(AGENTS.md、git 规范、敏感文件排除)

This commit is contained in:
weijuesen
2026-08-10 22:30:53 +08:00
commit 84abf4454c
358 changed files with 75993 additions and 0 deletions
@@ -0,0 +1,6 @@
export default definePageConfig({
navigationBarTitleText: '视频播放',
navigationBarBackgroundColor: '#000000',
navigationBarTextStyle: 'white',
backgroundColor: '#000000',
});
@@ -0,0 +1,162 @@
@use '@/styles/variables.scss' as *;
.playerPage {
min-height: 100vh;
background: #000;
display: flex;
flex-direction: column;
}
.videoContainer {
width: 100%;
height: 422rpx;
background: #000;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.video {
width: 100%;
height: 100%;
}
.videoPlaceholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: rgba(255, 255, 255, 0.5);
}
.placeholderIcon {
font-size: 80rpx;
margin-bottom: $spacing-md;
}
.placeholderText {
font-size: $font-size-sm;
}
.loadingOverlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.6);
}
.loadingText {
font-size: $font-size-md;
color: $color-text-white;
}
.controls {
padding: $spacing-lg;
background: #1a1a1a;
}
.cameraTitle {
font-size: $font-size-lg;
font-weight: $font-weight-semibold;
color: $color-text-white;
margin-bottom: $spacing-md;
}
.formatSelector {
display: flex;
gap: $spacing-sm;
margin-bottom: $spacing-md;
}
.formatChip {
flex: 1;
text-align: center;
padding: $spacing-sm 0;
border-radius: $radius-sm;
background: #333;
font-size: $font-size-sm;
color: rgba(255, 255, 255, 0.6);
transition: all $transition-base;
white-space: nowrap;
}
.formatChipActive {
background: $color-primary;
color: $color-text-white;
}
.playBtn {
width: 100%;
height: $button-height-lg;
border-radius: $radius-button;
background: linear-gradient(135deg, $color-primary 0%, $color-primary-light 100%);
display: flex;
align-items: center;
justify-content: center;
transition: all $transition-base;
&:active {
opacity: 0.9;
transform: scale(0.98);
}
}
.playBtnText {
font-size: $font-size-md;
font-weight: $font-weight-medium;
color: $color-text-white;
}
.infoSection {
padding: $spacing-lg;
background: #1a1a1a;
margin-top: 2rpx;
}
.infoTitle {
font-size: $font-size-md;
font-weight: $font-weight-medium;
color: $color-text-white;
margin-bottom: $spacing-md;
}
.infoRow {
display: flex;
justify-content: space-between;
padding: $spacing-sm 0;
border-bottom: 2rpx solid #333;
}
.infoLabel {
font-size: $font-size-sm;
color: rgba(255, 255, 255, 0.6);
}
.infoValue {
font-size: $font-size-sm;
color: $color-text-white;
}
.errorWrap {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: $spacing-xl 0;
}
.errorIcon {
font-size: 56rpx;
margin-bottom: $spacing-sm;
}
.errorText {
font-size: $font-size-sm;
color: rgba(255, 255, 255, 0.6);
}
+172
View File
@@ -0,0 +1,172 @@
import React, { useState, useEffect } from 'react';
import { View, Text, Video } from '@tarojs/components';
import Taro, { useRouter } from '@tarojs/taro';
import styles from './index.module.scss';
import { getCameras, playCamera } from '@/api/video';
import { resolveUrl } from '@/api/config';
import type { Camera, VideoPlayResponse } from '@/types';
const VideoPlayerPage: React.FC = () => {
const router = useRouter();
const cameraId = router.params.id ? Number(router.params.id) : 0;
const cameraName = router.params.name ? decodeURIComponent(router.params.name) : '视频播放';
const directUrl = router.params.url ? decodeURIComponent(router.params.url) : '';
const [camera, setCamera] = useState<Camera | null>(null);
const [videoUrl, setVideoUrl] = useState<string>(directUrl);
const [format, setFormat] = useState<'hls' | 'flv' | 'webrtc'>('hls');
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
useEffect(() => {
if (directUrl) {
setVideoUrl(resolveUrl(directUrl));
return;
}
const fetchCamera = async () => {
try {
const cameras = await getCameras().catch(() => [] as Camera[]);
const found = cameras.find((c) => c.id === cameraId);
if (found) {
setCamera(found);
const url = found.hlsUrl || found.streamUrl || found.flvUrl;
if (url) {
setVideoUrl(resolveUrl(url));
}
}
} catch (err) {
console.error('[VideoPlayer] 获取摄像头信息失败:', err);
}
};
fetchCamera();
}, [cameraId, directUrl]);
const handlePlay = async () => {
if (!cameraId && !camera) return;
setLoading(true);
setError('');
const streamUrl = resolveUrl(`/api/v1/video/cameras/${cameraId || camera!.id}/live/stream`);
console.log('[VideoPlayer] 获取播放地址成功:', streamUrl);
setVideoUrl(streamUrl);
setLoading(false);
};
const formats: { key: 'hls' | 'flv' | 'webrtc'; label: string }[] = [
{ key: 'hls', label: 'HLS' },
{ key: 'flv', label: 'FLV' },
{ key: 'webrtc', label: 'WebRTC' },
];
const isLive = !directUrl;
return (
<View className={styles.playerPage}>
{/* 视频播放区域 */}
<View className={styles.videoContainer}>
{videoUrl ? (
<Video
className={styles.video}
src={videoUrl}
autoplay
controls
loop={!isLive}
muted={false}
showFullscreenBtn
showPlayBtn
showCenterPlayBtn
objectFit="contain"
onError={(e) => {
console.error('[VideoPlayer] 视频播放错误:', e);
setError('视频播放失败');
}}
/>
) : (
<View className={styles.videoPlaceholder}>
<Text className={styles.placeholderIcon}>📹</Text>
<Text className={styles.placeholderText}>
{loading ? '正在获取视频流...' : '点击下方按钮开始播放'}
</Text>
</View>
)}
{loading && videoUrl && (
<View className={styles.loadingOverlay}>
<Text className={styles.loadingText}>...</Text>
</View>
)}
</View>
{/* 控制区域 */}
{isLive && (
<View className={styles.controls}>
<Text className={styles.cameraTitle}>{camera?.name || cameraName}</Text>
<View className={styles.formatSelector}>
{formats.map((f) => (
<View
key={f.key}
className={`${styles.formatChip} ${format === f.key ? styles.formatChipActive : ''}`}
onClick={() => setFormat(f.key)}
>
<Text>{f.label}</Text>
</View>
))}
</View>
<View
className={styles.playBtn}
onClick={handlePlay}
>
<Text className={styles.playBtnText}>
{loading ? '加载中...' : videoUrl ? '重新播放' : '开始播放'}
</Text>
</View>
</View>
)}
{/* 摄像头信息 */}
{camera && (
<View className={styles.infoSection}>
<Text className={styles.infoTitle}></Text>
<View className={styles.infoRow}>
<Text className={styles.infoLabel}></Text>
<Text className={styles.infoValue}>{camera.code}</Text>
</View>
<View className={styles.infoRow}>
<Text className={styles.infoLabel}></Text>
<Text className={styles.infoValue}>{camera.isOnline ? '在线' : '离线'}</Text>
</View>
{camera.position && (
<View className={styles.infoRow}>
<Text className={styles.infoLabel}></Text>
<Text className={styles.infoValue}>{camera.position}</Text>
</View>
)}
{camera.resolution && (
<View className={styles.infoRow}>
<Text className={styles.infoLabel}></Text>
<Text className={styles.infoValue}>{camera.resolution}</Text>
</View>
)}
{camera.gbDeviceId && (
<View className={styles.infoRow}>
<Text className={styles.infoLabel}>ID</Text>
<Text className={styles.infoValue}>{camera.gbDeviceId}</Text>
</View>
)}
</View>
)}
{error && (
<View className={styles.errorWrap}>
<Text className={styles.errorIcon}></Text>
<Text className={styles.errorText}>{error}</Text>
</View>
)}
</View>
);
};
export default VideoPlayerPage;