Files
attractor/attractor-ui/pages/devices/master.vue
2026-04-07 11:18:02 +08:00

431 lines
9.4 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="master-page">
<view class="hero-card">
<view>
<text class="hero-title">设备管理</text>
<text class="hero-sub">统一管理你的 Attractor 设备</text>
</view>
<button class="hero-btn" @click="openAddDeviceModal">新增设备</button>
</view>
<view class="filter-row">
<view
v-for="(tab, index) in deviceTabs"
:key="tab.type || index"
class="filter-pill"
:class="{ active: activeTab === index }"
@click="switchTab(index)"
>
{{ tab.name }}
</view>
</view>
<scroll-view class="device-list" scroll-y>
<view v-if="filteredDevices.length === 0" class="empty-card">
<text class="empty-title">当前分类暂无设备</text>
<text class="empty-sub">你可以点击右上角新增设备进行添加</text>
</view>
<view
class="device-card"
v-for="device in filteredDevices"
:key="device.id"
@click="toDeviceDetail(device.id)"
>
<image class="device-icon" :src="device.image" mode="aspectFit" />
<view class="device-main">
<text class="device-name">{{ device.name }}</text>
<text class="device-sn">SN: {{ device.sn }}</text>
<view class="meta-row">
<text class="meta-tag">{{ typeText(device.type) }}</text>
<text class="meta-text" v-if="device.user">使用者{{ device.user }}</text>
</view>
</view>
<button class="delete-btn" @click.stop="deleteDevice(device.id)">
<uni-icons type="trash" size="13" color="#64748b"></uni-icons>
<text>删除</text>
</button>
</view>
</scroll-view>
<uni-popup ref="addDevicePopup" type="center">
<view class="popup-card">
<text class="popup-title">新增设备</text>
<button class="pair-btn" @click="bluetoothPair">蓝牙配对设备</button>
<view class="form-item">
<text class="form-label">设备名称</text>
<input class="form-input" v-model="newDevice.name" placeholder="请输入设备名称" />
</view>
<view class="form-item">
<text class="form-label">SN </text>
<input class="form-input" v-model="newDevice.sn" placeholder="配对后自动填充/手动输入" />
</view>
<view class="form-item">
<text class="form-label">设备类型</text>
<picker :range="pickerTypes" :value="newDevice.type" @change="onTypeChange">
<view class="picker-view">{{ pickerTypes[newDevice.type] }}</view>
</picker>
</view>
<view class="popup-actions">
<button class="ghost" @click="closeAddDeviceModal">取消</button>
<button class="primary" @click="confirmAddDevice">确认添加</button>
</view>
</view>
</uni-popup>
</view>
</template>
<script>
export default {
data() {
return {
deviceTabs: [
{ name: '全部', type: '' },
{ name: '智能硬件', type: 'hardware' },
{ name: '传感器', type: 'sensor' },
{ name: '控制器', type: 'controller' }
],
activeTab: 0,
deviceList: [
{
id: 1,
name: 'Attractor 主控器',
sn: 'ATR-20260327001',
type: 'controller',
image: '/static/images/app_icon_pack_schemeA_tealLogo/master_1024.png',
user: 'Attractor_001'
},
{
id: 2,
name: '环境传感器 A1',
sn: 'ATR-20260327002',
type: 'sensor',
image: '/static/images/app_icon_pack_schemeA_tealLogo/master_1024.png',
user: ''
}
],
newDevice: {
name: '',
sn: '',
type: 0
},
pickerTypes: ['智能硬件', '传感器', '控制器']
}
},
computed: {
filteredDevices() {
if (this.activeTab === 0) return this.deviceList
const targetType = this.deviceTabs[this.activeTab].type
return this.deviceList.filter(item => item.type === targetType)
}
},
methods: {
typeText(type) {
const map = {
hardware: '智能硬件',
sensor: '传感器',
controller: '控制器'
}
return map[type] || '未知类型'
},
switchTab(index) {
this.activeTab = index
},
openAddDeviceModal() {
this.newDevice = { name: '', sn: '', type: 0 }
this.$refs.addDevicePopup.open()
},
closeAddDeviceModal() {
this.$refs.addDevicePopup.close()
},
onTypeChange(e) {
this.newDevice.type = Number(e.detail.value)
},
bluetoothPair() {
this.newDevice.sn = `ATR-${Date.now().toString().slice(-8)}`
uni.showToast({ title: '配对成功SN 已填充', icon: 'none' })
},
confirmAddDevice() {
if (!this.newDevice.name || !this.newDevice.sn) {
uni.showToast({ title: '设备名称和 SN 不能为空', icon: 'none' })
return
}
const typeMap = ['hardware', 'sensor', 'controller']
const id = Math.max(...this.deviceList.map(d => d.id), 0) + 1
this.deviceList.unshift({
id,
name: this.newDevice.name,
sn: this.newDevice.sn,
type: typeMap[this.newDevice.type],
image: '/static/images/app_icon_pack_schemeA_tealLogo/master_1024.png',
user: ''
})
this.closeAddDeviceModal()
uni.showToast({ title: '设备已添加', icon: 'success' })
},
deleteDevice(id) {
uni.showModal({
title: '删除设备',
content: '确认删除该设备吗?',
success: res => {
if (res.confirm) {
this.deviceList = this.deviceList.filter(item => item.id !== id)
uni.showToast({ title: '删除成功', icon: 'none' })
}
}
})
},
toDeviceDetail(id) {
uni.navigateTo({
url: '/pages/devices/detail/detail?id=' + id
})
}
}
}
</script>
<style scoped>
.master-page {
min-height: 100vh;
background: #f4f7f8;
padding: 20rpx;
box-sizing: border-box;
}
.hero-card {
background: #ffffff;
border: 1rpx solid #e7eeef;
border-radius: 18rpx;
padding: 22rpx;
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 14rpx;
}
.hero-title {
display: block;
font-size: 34rpx;
font-weight: 700;
color: #0f172a;
}
.hero-sub {
display: block;
margin-top: 4rpx;
font-size: 22rpx;
color: #94a3b8;
}
.hero-btn {
height: 62rpx;
line-height: 62rpx;
padding: 0 20rpx;
border-radius: 14rpx;
border: 1rpx solid #bfe9e7;
background: #e6f5f4;
color: #0f7f7a;
font-size: 24rpx;
}
.filter-row {
display: flex;
gap: 10rpx;
overflow-x: auto;
padding-bottom: 8rpx;
margin-bottom: 10rpx;
}
.filter-pill {
flex-shrink: 0;
padding: 8rpx 18rpx;
border-radius: 999rpx;
border: 1rpx solid #e2e8f0;
background: #ffffff;
color: #64748b;
font-size: 23rpx;
}
.filter-pill.active {
border-color: #bfe9e7;
background: #e6f5f4;
color: #0f7f7a;
font-weight: 600;
}
.device-list {
height: calc(100vh - 240rpx);
}
.empty-card {
background: #ffffff;
border: 1rpx dashed #d7dee5;
border-radius: 16rpx;
padding: 40rpx 20rpx;
text-align: center;
}
.empty-title {
display: block;
font-size: 26rpx;
color: #64748b;
}
.empty-sub {
display: block;
margin-top: 8rpx;
font-size: 22rpx;
color: #94a3b8;
}
.device-card {
background: #ffffff;
border: 1rpx solid #e7eeef;
border-radius: 16rpx;
padding: 16rpx;
display: flex;
align-items: center;
margin-bottom: 10rpx;
}
.device-icon {
width: 72rpx;
height: 72rpx;
border-radius: 14rpx;
margin-right: 14rpx;
background: #f0fdfa;
}
.device-main {
flex: 1;
display: flex;
flex-direction: column;
gap: 4rpx;
}
.device-name {
font-size: 26rpx;
color: #0f172a;
font-weight: 600;
}
.device-sn,
.meta-text {
font-size: 22rpx;
color: #94a3b8;
}
.meta-row {
display: flex;
gap: 10rpx;
align-items: center;
}
.meta-tag {
font-size: 21rpx;
color: #0f7f7a;
background: #e6f5f4;
border-radius: 8rpx;
padding: 2rpx 10rpx;
}
.delete-btn {
display: inline-flex;
align-items: center;
gap: 6rpx;
height: 46rpx;
padding: 0 12rpx;
border-radius: 12rpx;
border: 1rpx solid #e2e8f0;
background: #f8fafc;
color: #64748b;
font-size: 20rpx;
}
.popup-card {
width: 620rpx;
background: #ffffff;
border-radius: 18rpx;
padding: 24rpx;
box-sizing: border-box;
}
.popup-title {
display: block;
text-align: center;
font-size: 30rpx;
color: #0f172a;
font-weight: 700;
margin-bottom: 16rpx;
}
.pair-btn {
height: 68rpx;
line-height: 68rpx;
border-radius: 14rpx;
border: 1rpx solid #bfe9e7;
background: #e6f5f4;
color: #0f7f7a;
font-size: 24rpx;
margin-bottom: 16rpx;
}
.form-item {
margin-bottom: 12rpx;
}
.form-label {
display: block;
font-size: 23rpx;
color: #64748b;
margin-bottom: 6rpx;
}
.form-input,
.picker-view {
height: 64rpx;
line-height: 64rpx;
border-radius: 12rpx;
border: 1rpx solid #e2e8f0;
background: #f8fafc;
padding: 0 14rpx;
font-size: 24rpx;
color: #0f172a;
}
.popup-actions {
margin-top: 16rpx;
display: flex;
gap: 10rpx;
}
.popup-actions button {
flex: 1;
height: 68rpx;
line-height: 68rpx;
border-radius: 12rpx;
font-size: 24rpx;
}
.popup-actions .ghost {
border: 1rpx solid #e2e8f0;
background: #f8fafc;
color: #64748b;
}
.popup-actions .primary {
border: 1rpx solid #bfe9e7;
background: #e6f5f4;
color: #0f7f7a;
}
</style>