chuangte_bike_newxcx/page_shanghu/guanli/bike_track.vue
2025-04-30 18:03:27 +08:00

960 lines
25 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="page">
<u-navbar title="轨迹" :border-bottom="false" :background="bgc" title-color='#000' title-size='36' height='45'>
</u-navbar>
<!-- 地图容器 -->
<view class="map-container">
<map id="map" ref="map" :latitude="latitude" :longitude="longitude" class="map" :polyline="currentPolyline"
:markers="markers" :scale="18" :polygons="polygon">
<cover-view class="park" @click="toggleIconAndCallout">
<cover-image class="img" src="https://lxnapi.ccttiot.com/bike/img/static/uRiYQZQEb3l2LsltEsyW"
mode=""></cover-image>
</cover-view>
</map>
</view>
<!-- 轨迹信息面板 -->
<view class="track-panel">
<!-- 时间范围选择 -->
<view class="time-range" v-if="type == 1">
<view class="time-picker" @click="time1 = true">
<text>{{ startTime }}</text>
<u-icon name="arrow-down" size="14" color="#666"></u-icon>
</view>
<text class="separator">至</text>
<view class="time-picker" @click="time2 = true">
<text>{{ endTime }}</text>
<u-icon name="arrow-down" size="14" color="#666"></u-icon>
</view>
</view>
<!-- 快捷时间选择器 -->
<view class="time-selector" v-if="qufen != 123">
<view class="time-btn" v-for="(item, index) in timeOptions" :key="index"
:class="{ active: activeTime === item.value }" @tap="selectTime(item.value)">
{{ item.label }}
</view>
</view>
<!-- 设备信息 -->
<view class="device-info">
<view class="device-item">
<text class="device-number" style="margin-right: 200rpx;">车牌:{{ vehicleNum == null ? '' : vehicleNum }}</text><text class="device-number">SN{{ deviceNumber }}</text>
</view>
<view class="device-item">
<text class="value">状态:{{zhuangtai}}</text>
<text class="value">速度:{{ currentSpeed }}km/h</text>
</view>
<view class="device-item">
<text class="value">电压:{{ currentBattery }}V</text>
</view>
<view class="device-item">
<text class="value">卫星:{{ satellites }}</text>
<text class="value">信号:{{ signal }}</text>
</view>
<view class="device-item">
<text class="value">电门:{{ quality }}</text>
<text class="value">锁状态:{{ lockStatus }}</text>
</view>
<view class="device-item">
<text class="value">定位:{{ location }}</text>
</view>
</view>
<!-- 轨迹回放控制面板 -->
<view class="playback-panel">
<view class="playback-controls">
<!-- 进度条行 -->
<view class="progress-row">
<slider class="progress-slider" :value="currentProgress" :min="0" :max="trackPoints.length - 1"
:step="1" @change="onSliderChange" @changing="onSliderChanging" />
</view>
<!-- 控制按钮行 -->
<view class="control-row">
<view class="left-controls">
<view class="play-btn" @tap="togglePlay">
<text>{{ isPlaying ? '暂停' : '播放' }}</text>
</view>
<view class="speed-control">
<text class="speed-label">{{ playbackSpeed }}x</text>
<view class="speed-buttons">
<text @tap="changeSpeed('down')" class="speed-btn">-</text>
<text @tap="changeSpeed('up')" class="speed-btn">+</text>
</view>
</view>
</view>
<view class="point-info">
<text>{{ currentPointTime }}</text>
</view>
</view>
</view>
</view>
<!-- 时间选择器组件 -->
<u-picker mode="time" v-model="time1" :params="params" :default-time="startTime" @confirm="confirm1"
:start-year="startYear" :end-year="endYear" title="选择开始时间"></u-picker>
<u-picker mode="time" v-model="time2" :params="params" :default-time="endTime" @confirm="confirm2"
:start-year="startYear" :end-year="endYear" title="选择结束时间"></u-picker>
</view>
</view>
</template>
<script>
export default {
data() {
return {
bgc: {
backgroundColor: "#fff",
},
latitude: '',
longitude: '',
currentPolyline: [], // 当前显示的轨迹线
polygon: [], // 用于存储区域多边形
markers: [],
activeTime: '3',
deviceNumber: '',
speed: '0.00',
voltage: '0.000',
location: '',
// 区域和停车场相关数据
parkingList: [],
showIconAndCallout: false,
areaId: '',
// 轨迹回放相关数据
trackPoints: [], // 存储轨迹点数据
currentProgress: 0, // 当前进度
isPlaying: false, // 是否正在播放
playbackTimer: null, // 回放定时器
currentPointTime: '', // 当前点的时间
playbackSpeed: 1, // 播放速度倍数
speedOptions: [0.5, 1, 2, 4, 8], // 可选的播放速度
currentSpeedIndex: 1, // 当前速度索引
currentSpeed: '0.00', // 当前估算速度
playbackInterval: 1000, // 播放间隔(毫秒)
currentBattery: '未知', // 当前电池电量
currentLockStatus: '关锁', // 当前锁状态
// 时间选择相关
time1: false,
time2: false,
params: {
year: true,
month: true,
day: true,
hour: true,
minute: true,
second: false
},
startYear: 2022,
endYear: new Date().getFullYear(),
startTime: '',
endTime: '',
sn: '',
timeOptions: [
{ label: '1小时', value: '1' },
{ label: '3小时', value: '3' },
{ label: '6小时', value: '6' },
{ label: '12小时', value: '12' }
],
type: '',
qufen:'',
orid:'',
id:'',
lockStatus:'',
satellites:'',
signal:'',
quality:'',
includePoints: [], // 添加includePoints数组
vehicleNum:'',
zhuangtai:''
}
},
onLoad(e) {
console.log(e);
this.sn = e.sn
this.type = e.type
if(e.id){
this.id = e.id
}
if(e.qufen){
this.orid = e.orid
this.qufen = e.qufen
this.startTime = e.startTime
this.endTime = e.endTime == null ? this.formatCurrentTime() : e.endTime
console.log(this.startTime, this.endTime,'0000')
}else{
if (e.type == 1) {
const now = new Date()
const threeHoursAgo = new Date(now - 3 * 60 * 60 * 1000)
this.startTime = this.formatDateTime(threeHoursAgo)
this.endTime = this.formatDateTime(now)
} else {
this.startTime = e.startTime
this.endTime = e.endTime == null ? this.formatCurrentTime() : e.endTime
console.log(this.startTime, this.endTime,'1111')
}
}
this.getDeviceInfo()
this.updateTrackData()
},
methods: {
getDeviceInfo() {
this.$u.get(`/bst/device?id=${this.id}`).then((res) => {
if (res.code === 200) {
this.vehicleNum = res.data.vehicleNum
this.deviceNumber = res.data.sn
this.voltage = res.data.voltage
this.areaId = res.data.areaId
this.latitude = res.data.latitude
this.longitude = res.data.longitude
this.getArea()
}
})
},
getParking() {
let data = {
areaId: this.areaId
}
this.$u.get('/bst/areaSub/listByAreaId', data).then((res) => {
if (res.code === 200) {
const type1Data = []
const type2Data = []
const type3Data = []
res.data.forEach(row => {
if (row.type == 1) type1Data.push(row)
else if (row.type == 2) type2Data.push(row)
else if (row.type == 3) type3Data.push(row)
})
const validBoundaries = type1Data.map(row => row.boundaryStr).filter(boundary =>
typeof boundary === 'string' && boundary.trim() !== '')
const polygons = this.convertBoundaryToPolygons(validBoundaries, 1)
const validBoundaries1 = type2Data.map(row => row.boundaryStr).filter(boundary =>
typeof boundary === 'string' && boundary.trim() !== '')
const polygons1 = this.convertBoundaryToPolygons(validBoundaries1, 2)
const validBoundaries2 = type3Data.map(row => row.boundaryStr).filter(boundary =>
typeof boundary === 'string' && boundary.trim() !== '')
const polygons2 = this.convertBoundaryToPolygons(validBoundaries2, 3)
this.polygon = [...this.polygon, ...polygons2, ...polygons1, ...polygons]
this.parkingList = res.data
}
})
},
// 获取区域数据
getArea() {
this.$u.get(`/bst/area/${this.areaId}`).then((res) => {
if (res.code === 200) {
const polygons = this.convertBoundaryToPolygon(res.data.boundaryStr)
if (polygons) {
this.polygon.push(polygons)
}
this.getParking()
}
})
},
formatCurrentTime() {
const now = new Date();
const padZero = num => num < 10 ? `0${num}` : num;
return `${now.getFullYear()}-${padZero(now.getMonth() + 1)}-${padZero(now.getDate())} ${padZero(now.getHours())}:${padZero(now.getMinutes())}:${padZero(now.getSeconds())}`;
},
updateTrackData() {
console.log(this.startTime, this.endTime)
const formattedStartTime = this.startTime
const formattedEndTime = this.endTime
if(this.orid){
this.$u.get('/bst/locationLog/listAll?orderId=' + this.orid + '&startTime=' + formattedStartTime + '&endTime=' + formattedEndTime).then((res) => {
if (res.code === 200) {
if (!res.data || res.data.length === 0) {
uni.showToast({
title: '该时间段内无轨迹数据',
icon: 'none',
duration: 2000
});
this.trackPoints = []
this.currentPolyline = []
this.markers = []
return
}
// 按时间排序轨迹点
const sortedData = res.data.sort((a, b) => {
return new Date(a.at) - new Date(b.at)
})
this.trackPoints = sortedData.map(point => ({
latitude: parseFloat(point.latitude),
longitude: parseFloat(point.longitude),
time: point.at,
status: point.status,
onlineStatus: point.onlineStatus,
remainingPower: point.voltage || 0,
lockStatus:point.lockStatus,
satellites:point.satellites,
signal:point.signal,
quality:point.quality
}))
console.log(this.trackPoints,'000');
if (this.trackPoints.length > 0) {
this.currentProgress = 0;
this.updateMapPoint(0);
}
}
}).catch(error => {
uni.showToast({
title: '获取轨迹数据失败',
icon: 'none',
duration: 2000
})
console.error("Error fetching track data:", error);
})
}else{
this.$u.get('/bst/locationLog/listAll?deviceId=' + this.id + '&startTime=' + formattedStartTime + '&endTime=' + formattedEndTime).then((res) => {
if (res.code === 200) {
if (!res.data || res.data.length === 0) {
uni.showToast({
title: '该时间段内无轨迹数据',
icon: 'none',
duration: 2000
});
this.trackPoints = []
this.currentPolyline = []
this.markers = []
return
}
// 按时间排序轨迹点
const sortedData = res.data.sort((a, b) => {
return new Date(a.at) - new Date(b.at)
})
this.trackPoints = sortedData.map(point => ({
latitude: parseFloat(point.latitude),
longitude: parseFloat(point.longitude),
time: point.at,
status: point.status,
onlineStatus: point.onlineStatus,
remainingPower: point.voltage || 0,
lockStatus:point.lockStatus,
satellites:point.satellites,
signal:point.signal,
quality:point.quality
}))
if (this.trackPoints.length > 0) {
this.currentProgress = 0;
this.updateMapPoint(0);
}
}
}).catch(error => {
uni.showToast({
title: '获取轨迹数据失败',
icon: 'none',
duration: 2000
})
console.error("Error fetching track data:", error);
})
}
},
// 转换多个边界数据为多边形
convertBoundaryToPolygons(boundaries, num) {
if (!boundaries || !boundaries.length) return []
const colors = {
1: { fill: "#3A7EDB40", stroke: "#3A7EDB" },
2: { fill: "#FFF5D640", stroke: "#FFC107" },
3: { fill: "#FFE0E040", stroke: "#FF473E" }
}
return boundaries.map(boundary => {
if (!boundary) return null
let coords
try {
coords = JSON.parse(boundary)
} catch (error) {
console.error("Error parsing boundary JSON:", error)
return null
}
if (!Array.isArray(coords)) {
console.error("Parsed boundary is not an array:", coords)
return null
}
const points = coords.map(coord => ({
latitude: coord[1],
longitude: coord[0]
}))
return {
points: points,
fillColor: colors[num].fill,
strokeColor: colors[num].stroke,
strokeWidth: 1,
zIndex: 1
}
}).filter(Boolean)
},
// 切换图标和标注显示
toggleIconAndCallout() {
this.showIconAndCallout = !this.showIconAndCallout
if (this.showIconAndCallout) {
const newMarkers = []
this.parkingList.forEach(item => {
newMarkers.push({
id: parseFloat(item.id),
latitude: parseFloat(item.latitude),
longitude: parseFloat(item.longitude),
width: 20,
height: 28.95,
iconPath: item.type == 1 ?
'https://lxnapi.ccttiot.com/bike/img/static/up2xXqAgwCX5iER600k3' : item.type == 2 ?
'https://lxnapi.ccttiot.com/bike/img/static/u53BAQcFIX3vxsCzEZ7t' :
'https://lxnapi.ccttiot.com/bike/img/static/uDNY5Q4zOiZTCBTA2Jdq',
callout: {
content: item.name,
color: '#ffffff',
fontSize: 14,
borderRadius: 10,
bgColor: item.type == 1 ? '#3A7EDB' : item.type == 2 ? '#FFC107' : '#FF473E',
padding: 6,
display: 'ALWAYS'
}
})
})
this.markers = newMarkers
} else {
this.markers = []
}
},
// 转换边界数据为多边形
convertBoundaryToPolygon(boundary) {
if (!boundary) return null;
const points = JSON.parse(boundary).map(coord => ({
latitude: coord[1],
longitude: coord[0]
}))
return {
points: points,
fillColor: "#55888840",
strokeColor: "#22FF0040",
strokeWidth: 1,
zIndex: 1
}
},
updateMapPoint(index) {
if (!this.trackPoints[index]) return
const currentPoint = this.trackPoints[index]
const prevPoint = index > 0 ? this.trackPoints[index - 1] : null
// 计算速度
if (prevPoint) {
const distance = this.calculateDistance(
prevPoint.latitude,
prevPoint.longitude,
currentPoint.latitude,
currentPoint.longitude
)
const timeGap = (new Date(currentPoint.time) - new Date(prevPoint.time)) / 1000
this.currentSpeed = timeGap > 0 ? ((distance / timeGap) * 3.6).toFixed(1) : '0.00'
} else {
this.currentSpeed = '0.00'
}
// 更新信息显示
this.currentPointTime = currentPoint.time
this.location = `${currentPoint.longitude}, ${currentPoint.latitude}`
this.currentBattery = currentPoint.remainingPower == null ? '0.00' : currentPoint.remainingPower.toFixed(2)
this.lockStatus = currentPoint.lockStatus == '0' ? '关锁' : '开锁'
this.satellites = currentPoint.satellites
this.signal = currentPoint.signal
this.quality = currentPoint.quality == '0' ? '关' : '开'
// 更新地图中心点
this.latitude = currentPoint.latitude
this.longitude = currentPoint.longitude
// 更新includePoints
this.includePoints = this.trackPoints.slice(0, index + 1).map(point => ({
latitude: point.latitude,
longitude: point.longitude
}))
// 更新轨迹线
this.currentPolyline = [
{
points: this.trackPoints,
width: 4,
arrowLine: true,
color: 'rgb(165,208,240)',
zIndex: 2
},{
points: this.trackPoints.slice(0, index + 1),
width: 4,
arrowLine: true,
color: '#00AF99',
zIndex: 2
}
]
// 更新markers
let iconPath = ''
let calloutColor = '#2679D1'
let calloutBgColor = '#D4ECFF'
switch (currentPoint.status) {
case '0':
iconPath = currentPoint.onlineStatus == '0' ?
'https://lxnapi.ccttiot.com/bike/img/static/uQRng4QNKA38Amk8Wgt5' :
'https://lxnapi.ccttiot.com/bike/img/static/uocjFo8Ar2BJVpzC2G2f';
calloutColor = '#ffffff'
calloutBgColor = '#000000'
this.zhuangtai = '仓库中'
break;
case '1':
iconPath = currentPoint.onlineStatus == '0' ?
'https://lxnapi.ccttiot.com/bike/img/static/uzhMeExOQJbMcZtrfGUV' :
'https://lxnapi.ccttiot.com/bike/img/static/uheL17wVZn24BwCwEztT';
this.zhuangtai = '待租'
break;
case '2':
iconPath = currentPoint.onlineStatus == '0' ?
'https://lxnapi.ccttiot.com/bike/img/static/uR3DQEssiK62ovhh88y8' :
'https://lxnapi.ccttiot.com/bike/img/static/u460R1NKWHEpHbt0U4H7';
this.zhuangtai = '预约中'
break;
case '3':
iconPath = currentPoint.onlineStatus == '0' ?
'https://lxnapi.ccttiot.com/bike/img/static/uG13E7BpUFF44wVYC9no' :
'https://lxnapi.ccttiot.com/bike/img/static/uHQIdWCTmtUztl49wBKU';
this.zhuangtai = '骑行中'
break;
case '4':
iconPath = currentPoint.onlineStatus == '0' ?
'https://lxnapi.ccttiot.com/bike/img/static/uRod2zf3t9dAOYafWoWt' :
'https://lxnapi.ccttiot.com/bike/img/static/uZpXq3TBtM5gVgJJeImY';
this.zhuangtai = '临时锁车'
break;
case '6':
iconPath = currentPoint.onlineStatus == '0' ?
'https://lxnapi.ccttiot.com/bike/img/static/uhZudZM3nEKj0tYKlho2' :
'https://lxnapi.ccttiot.com/bike/img/static/ujur6TezvPf4buFAqPHo';
this.zhuangtai = '调度中'
break;
case '8':
iconPath = currentPoint.onlineStatus == '0' ?
'https://lxnapi.ccttiot.com/bike/img/static/ucBKG3ebYRAToVweJihu' :
'https://lxnapi.ccttiot.com/bike/img/static/uyK7Vg4Lu8xb3oNVuG2l';
calloutColor = '#ffffff';
calloutBgColor = '#000000';
this.zhuangtai = '禁用'
break;
default:
iconPath = 'https://lxnapi.ccttiot.com/bike/img/static/uzhMeExOQJbMcZtrfGUV';
}
this.markers = [{
id: 1,
latitude: currentPoint.latitude,
longitude: currentPoint.longitude,
width: 40,
height: 47,
iconPath: iconPath,
callout: {
content: ` ${this.currentSpeed}km/h`,
color: calloutColor,
fontSize: 10,
borderRadius: 10,
bgColor: calloutBgColor,
padding: 2,
display: 'ALWAYS'
}
}]
},
calculateDistance(lat1, lon1, lat2, lon2) {
const R = 6371000
const φ1 = this.toRadians(lat1)
const φ2 = this.toRadians(lat2)
const Δφ = this.toRadians(lat2 - lat1)
const Δλ = this.toRadians(lon2 - lon1)
const a = Math.sin(Δφ / 2) * Math.sin(Δφ / 2) +
Math.cos(φ1) * Math.cos(φ2) *
Math.sin(Δλ / 2) * Math.sin(Δλ / 2)
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a))
return R * c
},
toRadians(degrees) {
return degrees * Math.PI / 180
},
changeSpeed(direction) {
if (direction === 'up' && this.currentSpeedIndex < this.speedOptions.length - 1) {
this.currentSpeedIndex++
} else if (direction === 'down' && this.currentSpeedIndex > 0) {
this.currentSpeedIndex--
}
this.playbackSpeed = this.speedOptions[this.currentSpeedIndex]
this.playbackInterval = 1000 / this.playbackSpeed
if (this.isPlaying) {
this.stopPlayback();
this.startPlayback();
}
},
onSliderChange(e) {
const index = parseInt(e.detail.value);
this.currentProgress = index;
this.updateMapPoint(index);
},
onSliderChanging(e) {
const index = parseInt(e.detail.value);
this.updateMapPoint(index);
},
togglePlay() {
this.isPlaying = !this.isPlaying;
if (this.isPlaying) {
this.startPlayback();
} else {
this.stopPlayback();
}
},
startPlayback() {
if (this.playbackTimer) return
this.playbackTimer = setInterval(() => {
if (this.currentProgress >= this.trackPoints.length - 1) {
this.stopPlayback()
return
}
this.currentProgress++
this.updateMapPoint(this.currentProgress)
}, this.playbackInterval)
},
stopPlayback() {
if (this.playbackTimer) {
clearInterval(this.playbackTimer)
this.playbackTimer = null
}
this.isPlaying = false
},
formatDateTime(date) {
const year = date.getFullYear()
const month = (date.getMonth() + 1).toString().padStart(2, '0')
const day = date.getDate().toString().padStart(2, '0')
const hour = date.getHours().toString().padStart(2, '0')
const minute = date.getMinutes().toString().padStart(2, '0')
const second = '00'
return `${year}-${month}-${day} ${hour}:${minute}:${second}`
},
confirm1(e) {
this.activeTime=0
const selectedTime = new Date(e.year, e.month - 1, e.day, e.hour, e.minute)
this.startTime = this.formatDateTime(selectedTime)
this.updateTrackData()
},
confirm2(e) {
this.activeTime=0
const selectedTime = new Date(e.year, e.month - 1, e.day, e.hour, e.minute)
this.endTime = this.formatDateTime(selectedTime)
this.updateTrackData()
},
selectTime(time) {
this.activeTime = time
const hours = parseInt(time)
const now = new Date()
const startTime = new Date(now - hours * 60 * 60 * 1000)
this.startTime = this.formatDateTime(startTime)
this.endTime = this.formatDateTime(now)
this.updateTrackData()
}
}
}
</script>
<style lang="scss" scoped>
.page {
width: 100%;
height: 100vh;
background-color: #f5f7fa;
display: flex;
flex-direction: column;
position: relative;
}
.map-container {
width: 100%;
height: 45vh;
position: relative;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05);
flex-shrink: 0;
.map {
width: 100%;
height: 100%;
}
.park {
position: absolute;
right: 24rpx;
bottom: 48rpx;
width: 88rpx;
height: 88rpx;
z-index: 999;
transition: transform 0.2s ease;
&:active {
transform: scale(0.95);
}
.img {
width: 100%;
height: 100%;
border-radius: 16rpx;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
}
}
}
.track-panel {
flex: 1;
background: #fff;
border-radius: 32rpx 32rpx 0 0;
margin-top: -30rpx;
position: relative;
z-index: 2;
box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.05);
padding: 24rpx;
display: flex;
flex-direction: column;
}
.time-range {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 16rpx;
gap: 16rpx;
.time-picker {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
background: #f8fafc;
padding: 16rpx;
border-radius: 12rpx;
border: 2rpx solid #e2e8f0;
transition: all 0.3s ease;
&:active {
background: #f1f5f9;
}
text {
margin-right: 12rpx;
font-size: 24rpx;
color: #334155;
}
}
.separator {
padding: 0 16rpx;
color: #64748b;
font-size: 24rpx;
}
}
.time-selector {
display: flex;
justify-content: space-between;
margin-bottom: 16rpx;
gap: 12rpx;
.time-btn {
flex: 1;
padding: 12rpx 20rpx;
border-radius: 32rpx;
font-size: 24rpx;
color: #64748b;
background: #f1f5f9;
transition: all 0.3s ease;
text-align: center;
&.active {
background: #3b82f6;
color: #fff;
box-shadow: 0 4rpx 12rpx rgba(59, 130, 246, 0.3);
}
&:active {
transform: scale(0.98);
}
}
}
.device-info {
padding: 16rpx;
background: #f8fafc;
border-radius: 16rpx;
margin-bottom: 16rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.02);
.device-item {
display: flex;
align-items: center;
margin-bottom: 12rpx;
&:last-child {
margin-bottom: 0;
}
.device-number {
font-size: 28rpx;
font-weight: 600;
color: #1e293b;
}
.device-type {
font-size: 24rpx;
color: #64748b;
margin-left: 12rpx;
}
.label {
width: 100rpx;
font-size: 24rpx;
color: #64748b;
}
.value {
flex: 1;
font-size: 24rpx;
color: #334155;
font-weight: 500;
}
}
}
.playback-panel {
background: #fff;
border-radius: 16rpx;
padding: 16rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
.playback-controls {
display: flex;
flex-direction: column;
gap: 16rpx;
// position: absolute;
// top: 0;
// right: 0;
.progress-row {
padding: 0 80rpx;
margin-bottom: 8rpx;
}
.control-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 8rpx;
.left-controls {
display: flex;
align-items: center;
gap: 20rpx;
}
.play-btn {
width: 72rpx;
height: 72rpx;
display: flex;
align-items: center;
justify-content: center;
background: #3b82f6;
border-radius: 50%;
box-shadow: 0 4rpx 12rpx rgba(59, 130, 246, 0.3);
transition: all 0.2s ease;
&:active {
transform: scale(0.95);
box-shadow: 0 2rpx 8rpx rgba(59, 130, 246, 0.2);
}
text {
color: #fff;
font-size: 24rpx;
font-weight: 500;
}
}
.speed-control {
display: flex;
align-items: center;
background: #f8fafc;
padding: 8rpx 16rpx;
border-radius: 32rpx;
border: 2rpx solid #e2e8f0;
.speed-label {
font-size: 24rpx;
color: #334155;
font-weight: 500;
margin-right: 12rpx;
min-width: 44rpx;
text-align: center;
}
.speed-buttons {
display: flex;
align-items: center;
gap: 8rpx;
.speed-btn {
width: 60rpx;
height: 60rpx;
line-height: 60rpx;
text-align: center;
background: #fff;
border-radius: 50%;
font-size: 28rpx;
color: #334155;
font-weight: 500;
box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.05);
transition: all 0.2s ease;
&:active {
background: #f1f5f9;
transform: scale(0.95);
}
}
}
}
.point-info {
font-size: 24rpx;
color: #64748b;
background: #f8fafc;
padding: 8rpx 16rpx;
border-radius: 24rpx;
}
}
}
}
// 自定义滑块样式
::v-deep .uni-slider {
margin: 0;
}
::v-deep .uni-slider-handle {
width: 28rpx;
height: 28rpx;
background-color: #3b82f6;
box-shadow: 0 2rpx 8rpx rgba(59, 130, 246, 0.3);
transition: transform 0.2s ease;
&:active {
transform: scale(1.1);
}
}
::v-deep .uni-slider-track {
height: 4rpx;
background-color: #3b82f6;
}
</style>