congming_huose-apk/common/components/CustomHeader.vue

595 lines
15 KiB
Vue
Raw Normal View History

2025-11-08 11:30:06 +08:00
<template>
<view class="custom-header">
<view class="header-left">
<text class="menu-icon" @click="toggleSideMenu"></text>
</view>
<view class="header-center">
<!-- 仅在有空间数据时渲染头像与标题无数据时不展示任何占位 -->
<image v-if="hasSpaces && currentSpacePicture" @click="toggleDropdown" class="avatar" :src="currentSpacePicture" mode="aspectFill"></image>
<view v-if="hasSpaces && currentSpaceName" class="title-group" @click="toggleDropdown">
<text class="title">{{currentSpaceName}}</text>
<view class="subtitle-row">
<view class="subtitle selectable">
<text style="color: #F15A04;" v-if="isArmedStatus">{{ getCurrentStatusText() }}</text>
<text style="color: #39986D;" v-if="isDisarmedStatus">{{ getCurrentStatusText() }}</text>
<text style="color: #AA57AC;" v-if="isNightStatus">{{ getCurrentStatusText() }}</text>
<text style="color: red;" v-if="isEmergencyStatus">{{ getCurrentStatusText() }}</text>
<text class="chevron" :class="{ open: spaceSheetVisible }"></text>
2025-11-08 11:30:06 +08:00
</view>
</view>
</view>
</view>
<!-- 小程序上 <transition> 常不生效 v-if 挂载 + 类名切换触发 CSS transition -->
<view
v-if="spaceSheetOpen"
class="dropdown-overlay"
:class="{ 'dropdown-overlay--show': spaceSheetVisible }"
@click="closeDropdown"></view>
<view
v-if="spaceSheetOpen"
class="dropdown-panel"
:class="{ 'dropdown-panel--show': spaceSheetVisible }">
<view class="dropdown-header">
<text class="close-btn" @click="closeDropdown"></text>
<text class="dropdown-title">{{ getYourSpacesText() }}</text>
</view>
<scroll-view scroll-y class="dropdown-list">
<view class="" style="display: flex;justify-content: space-between;flex-wrap: wrap;">
<view
v-for="(space, idx) in spaces"
:key="idx"
class="dropdown-item"
@click.stop="selectSpace(idx)">
<image class="daimg" :class="{ active: idx === selectedIndex }" :src="getSpaceIcon(space, idx)" mode="aspectFill"></image>
<view class="xiao-wrap">
<image class="xiao" :src="getSpaceName(space)" mode="aspectFit"></image>
</view>
<view class="nametxt">
{{ space.name }}
</view>
2025-11-08 11:30:06 +08:00
</view>
</view>
</scroll-view>
<view class="dropdown-footer">
<view class="add-space-btn" @tap.stop="onAddSpaceTap">{{ getAddSpaceText() }}</view>
2025-11-08 11:30:06 +08:00
</view>
</view>
</view>
</template>
<script>
export default {
name: 'CustomHeader',
props: {
title: {
type: String,
default: ''
},
subtitle: {
type: String,
default: ''
},
// 空间数组,支持字符串或包含 name/title/label 字段的对象
spaces: {
type: Array,
default: () => []
},
// 当前选中的空间索引
selectedIndex: {
type: Number,
default: 0
}
},
data() {
return {
spaceSheetOpen: false,
spaceSheetVisible: false,
sheetCloseTimer: null,
sheetAnimMs: 400,
2025-11-08 11:30:06 +08:00
}
},
computed: {
// 计算当前语言,用于触发更新
currentLanguage() {
return this.$i18n && this.$i18n.getCurrentLanguage ? this.$i18n.getCurrentLanguage() : 'zh';
},
// 响应式的标题显示
displayTitle() {
// 添加语言依赖,确保语言变化时重新计算
this.currentLanguage;
return this.title;
},
// 响应式的副标题显示
displaySubtitle() {
// 添加语言依赖,确保语言变化时重新计算
this.currentLanguage;
return this.subtitle;
},
// 规范化当前空间的状态armed/disarmed/night/emergency
currentNormalizedStatus() {
this.currentLanguage; // 依赖语言变化,便于触发重算显示
// 优先使用空间的 status
if (this.hasSpaces) {
const spaces = this.spaces || [];
const index = Math.min(Math.max(0, this.selectedIndex || 0), spaces.length - 1);
const item = spaces[index];
let rawStatus = 'disarmed';
if (item && item.status !== undefined && item.status !== null) {
rawStatus = String(item.status).toLowerCase();
}
switch (rawStatus) {
case '1':
case 'armed':
return 'armed';
case '2':
case 'disarmed':
return 'disarmed';
case '3':
case 'night':
case 'armed_night':
return 'night';
case '4':
case 'emergency':
case 'alarm':
return 'emergency';
default:
return 'disarmed';
}
}
// 无空间数据时,兼容旧逻辑:从标题文案回推状态
const title = this.displayTitle;
if (title === '布防' || title === 'Armed' || title === '警備中' || title === 'Вооружен') return 'armed';
if (title === '撤防' || title === 'Disarmed' || title === '解除済み' || title === 'Разоружен') return 'disarmed';
if (title === '夜间' || title === 'Night' || title === '夜間' || title === 'Ночь') return 'night';
if (title === '紧急' || title === 'Emergency' || title === '緊急' || title === 'Экстренный') return 'emergency';
return 'disarmed';
},
// 状态判断的computed属性
isArmedStatus() {
this.currentLanguage; // 添加语言依赖
return this.currentNormalizedStatus === 'armed';
},
isDisarmedStatus() {
this.currentLanguage; // 添加语言依赖
return this.currentNormalizedStatus === 'disarmed';
},
isNightStatus() {
this.currentLanguage; // 添加语言依赖
return this.currentNormalizedStatus === 'night';
},
isEmergencyStatus() {
this.currentLanguage; // 添加语言依赖
return this.currentNormalizedStatus === 'emergency';
},
// 是否有空间可选
hasSpaces() {
return Array.isArray(this.spaces) && this.spaces.length > 0;
},
// 仅用于 下拉面板 的显示名称数组
spaceNames() {
if (!this.hasSpaces) return [];
return this.spaces.map(item => typeof item === 'string' ? item : (item.name || item.title || item.label || ''))
},
currentSpaceName() {
if (!this.hasSpaces) return this.displaySubtitle;
const index = Math.min(Math.max(0, this.selectedIndex || 0), this.spaceNames.length - 1);
return this.spaceNames[index] || this.displaySubtitle;
},
currentSpacePicture() {
if (!this.hasSpaces) return '';
const spaces = this.spaces || [];
const index = Math.min(Math.max(0, this.selectedIndex || 0), spaces.length - 1);
const item = spaces[index];
return item && (item.picture || item.avatar || item.icon || '');
}
},
watch: {
// 监听语言变化
currentLanguage() {
console.log('CustomHeader语言变化:', this.currentLanguage);
this.$forceUpdate();
}
},
mounted() {
// 监听语言变化事件
uni.$on('languageChanged', this.handleLanguageChange);
},
beforeDestroy() {
if (this.sheetCloseTimer) {
clearTimeout(this.sheetCloseTimer);
this.sheetCloseTimer = null;
}
2025-11-08 11:30:06 +08:00
// 移除事件监听
uni.$off('languageChanged', this.handleLanguageChange);
},
methods: {
handleLanguageChange(lang) {
console.log('CustomHeader语言切换事件:', lang);
this.$forceUpdate();
},
toggleSideMenu() {
console.log('CustomHeader: 点击了菜单按钮');
this.$emit('toggle-side-menu');
},
toggleDropdown() {
if (this.spaceSheetOpen) {
this.closeDropdown();
} else {
this.openSpaceSheet();
}
},
openSpaceSheet() {
this.spaceSheetOpen = true;
this.spaceSheetVisible = false;
// 小程序无 requestAnimationFrame用 nextTick + 短延时保证先渲染「未展开」再过渡到展开
this.$nextTick(() => {
setTimeout(() => {
this.spaceSheetVisible = true;
}, 50);
});
2025-11-08 11:30:06 +08:00
},
closeDropdown() {
if (!this.spaceSheetOpen) return;
this.spaceSheetVisible = false;
if (this.sheetCloseTimer) {
clearTimeout(this.sheetCloseTimer);
}
this.sheetCloseTimer = setTimeout(() => {
this.spaceSheetOpen = false;
this.sheetCloseTimer = null;
}, this.sheetAnimMs);
2025-11-08 11:30:06 +08:00
},
selectSpace(index) {
const rawItem = this.spaces[index];
const name = typeof rawItem === 'string' ? rawItem : (rawItem && (rawItem.name || rawItem.title || rawItem.label));
this.$emit('space-change', index, rawItem, name);
this.closeDropdown();
},
// 判断状态赋值图标
getSpaceName(space) {
if (space.status == 1) {
return 'https://api.ccttiot.com/smartmeter/img/static/uzZLS6VfQDFA1jTNaZVq'
2025-11-08 11:30:06 +08:00
} else if (space.status == 2) {
return 'https://api.ccttiot.com/smartmeter/img/static/uuGVupItvaHcUcDfB8Dw'
2025-11-08 11:30:06 +08:00
} else if (space.status == 3) {
return 'https://api.ccttiot.com/smartmeter/img/static/u69cK2hDbhRQ9BQAEysP'
2025-11-08 11:30:06 +08:00
} else if (space.status == 4) {
return 'https://api.ccttiot.com/smartmeter/img/static/uvdoPqJibC01UQZ2S0bN'
}
},
// 标题多语言化
getYourSpacesText() {
// 添加容错处理
const t = this.$i18n && this.$i18n.t ? this.$i18n.t.bind(this.$i18n) : (key) => key;
return t('yourSpaces') || '您的空间';
},
getAddSpaceText() {
const t = this.$i18n && this.$i18n.t ? this.$i18n.t.bind(this.$i18n) : (key) => key;
return t('addSpace') || '添加空间';
},
// 使用 @tap + $emit避免小程序端无参 @click.stop 父级破坏 __e 分发;跳转由页面处理更稳
onAddSpaceTap() {
this.closeDropdown();
this.$emit('add-space');
},
2025-11-08 11:30:06 +08:00
getCurrentStatusText() {
// 添加容错处理
const t = this.$i18n && this.$i18n.t ? this.$i18n.t.bind(this.$i18n) : (key) => key;
// 根据当前状态返回对应的多语言文本
if (this.isArmedStatus) {
return t('statusArmed') || '布防';
} else if (this.isDisarmedStatus) {
return t('statusDisarmed') || '撤防';
} else if (this.isNightStatus) {
return t('statusNight') || '夜间';
} else if (this.isEmergencyStatus) {
return t('statusEmergency') || '紧急';
}
// 默认返回原始标题
return this.displayTitle;
},
getSpaceIcon(space, index) {
// 如果有自定义图标,使用自定义图标
if (space && (space.picture || space.avatar || space.icon)) {
return space.picture || space.avatar || space.icon
}
// 返回空字符串让CSS处理默认样式
return ''
},
}
}
</script>
<style lang="scss" scoped>
.xiao-wrap {
2025-11-08 11:30:06 +08:00
position: absolute;
left: 40rpx;
bottom: 80rpx;
width: 84rpx;
height: 84rpx;
padding: 6rpx;
box-sizing: border-box;
background-color: #fff;
// border: 2rpx solid #b8c0cc;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.06);
}
.xiao {
width: 100%;
height: 100%;
2025-11-08 11:30:06 +08:00
}
.daimg{
width: 260rpx;
height: 260rpx;
border-radius: 50%;
}
.custom-header {
display: flex;
align-items: center;
padding: 20rpx 24rpx;
background: #fff;
border-bottom: 1rpx solid #f0f0f0;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
padding-top: 100rpx;
box-sizing: border-box;
}
.header-left {
margin-right: 220rpx;
}
.menu-icon {
font-size: 36rpx;
color: #333;
font-weight: bold;
}
.header-center {
display: flex;
align-items: center;
flex: 1;
}
.avatar {
width: 60rpx;
height: 60rpx;
background: #e0e0e0;
border-radius: 50%;
margin-right: 20rpx;
}
.title-group {
display: flex;
flex-direction: column;
}
.title {
font-size: 32rpx;
color: #333;
font-weight: 500;
line-height: 1.2;
}
.subtitle {
font-size: 24rpx;
color: #4caf50;
line-height: 1.2;
margin-top: 4rpx;
}
.subtitle-row {
display: flex;
align-items: center;
}
.selectable {
display: inline-flex;
align-items: center;
color: #4caf50;
}
.chevron {
margin-left: 8rpx;
font-size: 22rpx;
color: #999;
transition: transform 0.2s ease;
}
.chevron.open {
transform: rotate(180deg);
}
/* 下拉面板与遮罩(类名驱动 transition兼容小程序 */
2025-11-08 11:30:06 +08:00
.dropdown-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.35);
2025-11-08 11:30:06 +08:00
z-index: 999;
opacity: 0;
transition: opacity 0.35s ease;
2025-11-08 11:30:06 +08:00
}
.dropdown-overlay--show {
opacity: 1;
}
2025-11-08 11:30:06 +08:00
.dropdown-panel {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: #fff;
border-radius: 24rpx 24rpx 0 0;
box-shadow: 0 -8rpx 32rpx rgba(0, 0, 0, 0.15);
2025-11-08 11:30:06 +08:00
z-index: 1000;
height: 84vh;
box-sizing: border-box;
display: flex;
flex-direction: column;
overflow: hidden;
will-change: transform;
transform: translate3d(0, 100%, 0);
transition: transform 0.4s cubic-bezier(0.25, 0.82, 0.35, 1);
2025-11-08 11:30:06 +08:00
}
.dropdown-panel--show {
transform: translate3d(0, 0, 0);
2025-11-08 11:30:06 +08:00
}
.dropdown-header {
flex-shrink: 0;
2025-11-08 11:30:06 +08:00
display: flex;
align-items: center;
justify-content: space-between;
padding: 32rpx 32rpx 24rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.close-btn {
font-size: 32rpx;
color: #666;
font-weight: bold;
line-height: 1;
}
.dropdown-title {
font-size: 32rpx;
color: #000;
font-weight: 600;
}
.dropdown-list {
flex: 1;
height: 0;
min-height: 200rpx;
2025-11-08 11:30:06 +08:00
padding: 0;
padding-bottom: 24rpx;
overflow: hidden;
2025-11-08 11:30:06 +08:00
box-sizing: border-box;
width: 100%;
}
.dropdown-footer {
flex-shrink: 0;
padding: 20rpx 32rpx;
padding-bottom: 40rpx;
border-top: 1rpx solid #f0f0f0;
background: #fff;
}
.add-space-btn {
height: 88rpx;
line-height: 88rpx;
text-align: center;
font-size: 30rpx;
font-weight: 500;
color: #fff;
background: #000;
border-radius: 44rpx;
}
2025-11-08 11:30:06 +08:00
.dropdown-item {
padding: 32rpx;
margin: 16rpx 24rpx 16rpx;
background: #fff;
border-radius: 16rpx;
transition: all 0.2s ease;
width: 260rpx;
height: 320rpx;
position: relative;
text-align: center;
}
.nametxt{
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-size: 32rpx;
color: #3D3D3D;
}
.dropdown-item:last-child {
margin-bottom: 32rpx;
}
.daimg.active {
border: 6rpx solid #000;
}
.item-left {
margin-right: 24rpx;
}
.space-icon {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
background: #e8e8e8;
display: flex;
align-items: center;
justify-content: center;
}
.default-icon {
background: #f0f0f0;
}
.default-icon-text {
font-size: 32rpx;
color: #999;
}
.item-content {
flex: 1;
display: flex;
flex-direction: column;
}
.item-text {
font-size: 32rpx;
color: #000;
font-weight: 500;
margin-bottom: 8rpx;
}
.item-status {
display: flex;
align-items: center;
}
.status-text {
font-size: 24rpx;
margin-right: 8rpx;
}
.status-disarmed {
color: #4caf50; /* 绿色 - 撤防 */
}
.status-armed {
color: #ff9800; /* 橙色 - 布防 */
}
.status-night {
color: #2196f3; /* 蓝色 - 夜间 */
}
.status-emergency {
color: #f44336; /* 红色 - 紧急 */
}
.status-icon {
font-size: 20rpx;
color: #4caf50;
}
.check {
font-size: 32rpx;
color: #000;
font-weight: bold;
}
</style>