“益康养老”HarmonyOS APP门禁设备绑定与配网流程详解,及用户信息管理功能实现
·
第一部分:门禁设备绑定与配网全流程解析
1.1 业务背景与核心价值
在益康养老场景中,门禁设备的智能化管理是保障老人安全、提升护理效率的关键环节。通过HarmonyOS APP实现蓝牙门禁设备的快速绑定与WiFi配网,使护理员能够:
- 一键式设备管理:简化传统繁琐的硬件配置流程
- 实时状态监控:随时掌握门禁设备在线状态
- 远程控制能力:实现跨设备、跨场景的智能门禁控制
- 安全认证机制:确保只有授权人员可操作特定门禁
1.2 整体架构与交互流程
1.3 详细实现方案
1.3.1 蓝牙设备发现与连接
// features/device/src/main/ets/bluetooth/AccessControlManager.ets
import { ble, connection, wifi } from '@ohos.bluetooth';
import { BusinessError } from '@ohos.base';
import { Logger } from '@elderly/basic';
/**
* 门禁设备管理器
* 负责蓝牙设备的扫描、连接、绑定和配网
*/
export class AccessControlManager {
private static instance: AccessControlManager;
private isScanning: boolean = false;
private discoveredDevices: Array<BluetoothDevice> = [];
private connectedDevice: BluetoothDevice | null = null;
// 门禁设备名称前缀约定
private static readonly DEVICE_NAME_PREFIX: string = 'JL-ITHEIMA';
// 门禁设备服务UUID约定
private static readonly SERVICE_UUID: string = '0000AE30-0000-1000-8000-00805F9B34FB';
// 写入特征值UUID
private static readonly WRITE_CHAR_UUID: string = '0000AE10-0000-1000-8000-00805F9B34FB';
// 通知特征值UUID
private static readonly NOTIFY_CHAR_UUID: string = '0000AE04-0000-1000-8000-00805F9B34FB';
public static getInstance(): AccessControlManager {
if (!AccessControlManager.instance) {
AccessControlManager.instance = new AccessControlManager();
}
return AccessControlManager.instance;
}
/**
* 开始扫描门禁设备
* @param duration 扫描持续时间(毫秒),默认10秒
*/
public async startDeviceScan(duration: number = 10000): Promise<Array<BluetoothDevice>> {
if (this.isScanning) {
Logger.warn('AccessControlManager', '设备扫描已在进行中');
return this.discoveredDevices;
}
try {
this.isScanning = true;
this.discoveredDevices = [];
Logger.info('AccessControlManager', '开始扫描门禁设备...');
// 注册蓝牙扫描回调
ble.on('BLEDeviceFind', (device: ble.ScanResult) => {
this.handleDiscoveredDevice(device);
});
// 开始BLE扫描
await ble.startBLEScan({
interval: 0,
dutyMode: ble.ScanDuty.SCAN_DUTY_LOW_POWER,
matchMode: ble.MatchMode.MATCH_MODE_AGGRESSIVE
});
// 设置扫描超时
setTimeout(() => {
this.stopDeviceScan();
Logger.info('AccessControlManager', `扫描完成,共发现${this.discoveredDevices.length}个设备`);
}, duration);
} catch (error) {
const err: BusinessError = error as BusinessError;
Logger.error('AccessControlManager', `设备扫描失败: ${err.code}, ${err.message}`);
this.isScanning = false;
throw new Error(`蓝牙扫描启动失败: ${err.message}`);
}
return this.discoveredDevices;
}
/**
* 处理发现的设备
*/
private handleDiscoveredDevice(scanResult: ble.ScanResult): void {
// 过滤门禁设备:名称以特定前缀开头
if (scanResult.deviceName && scanResult.deviceName.startsWith(AccessControlManager.DEVICE_NAME_PREFIX)) {
const device: BluetoothDevice = {
deviceId: scanResult.deviceId,
deviceName: scanResult.deviceName,
rssi: scanResult.rssi,
isConnectable: scanResult.isConnectable
};
// 去重检查
const existingIndex = this.discoveredDevices.findIndex(d => d.deviceId === device.deviceId);
if (existingIndex === -1) {
this.discoveredDevices.push(device);
Logger.debug('AccessControlManager', `发现门禁设备: ${device.deviceName} (${device.rssi}dBm)`);
// 通知UI更新
emitter.emit({
eventId: EventConstants.DEVICE_DISCOVERED
}, { device });
}
}
}
/**
* 连接门禁设备
*/
public async connectDevice(deviceId: string): Promise<boolean> {
try {
Logger.info('AccessControlManager', `开始连接设备: ${deviceId}`);
// 停止扫描
if (this.isScanning) {
this.stopDeviceScan();
}
// 建立蓝牙连接
await connection.connect({
deviceId,
isAutoConnect: false
});
// 发现服务
const services = await ble.getBLEDeviceServices(deviceId);
const targetService = services.find(service =>
service.serviceUuid === AccessControlManager.SERVICE_UUID
);
if (!targetService) {
throw new Error('未找到门禁设备服务');
}
// 获取特征值
const characteristics = await ble.getBLEDeviceCharacteristics(deviceId, targetService.serviceUuid);
const writeChar = characteristics.find(char =>
char.characteristicUuid === AccessControlManager.WRITE_CHAR_UUID
);
const notifyChar = characteristics.find(char =>
char.characteristicUuid === AccessControlManager.NOTIFY_CHAR_UUID
);
if (!writeChar || !notifyChar) {
throw new Error('未找到必要的特征值');
}
// 开启通知
await ble.setBLECharacteristicChangeNotification(deviceId, {
serviceUuid: targetService.serviceUuid,
characteristicUuid: notifyChar.characteristicUuid,
enable: true
});
// 监听通知
ble.on('BLECharacteristicChange', (data: ble.NotifyCharacteristic) => {
this.handleDeviceNotification(data);
});
this.connectedDevice = this.discoveredDevices.find(d => d.deviceId === deviceId) || null;
Logger.info('AccessControlManager', '门禁设备连接成功');
return true;
} catch (error) {
const err: BusinessError = error as BusinessError;
Logger.error('AccessControlManager', `设备连接失败: ${err.code}, ${err.message}`);
return false;
}
}
/**
* 处理设备通知
*/
private handleDeviceNotification(data: ble.NotifyCharacteristic): void {
Logger.debug('AccessControlManager', `收到设备通知: ${Array.from(data.characteristicValue).join(',')}`);
// 根据协议解析设备状态
const value = new Uint8Array(data.characteristicValue);
// 示例:根据数据长度判断消息类型
switch (value.length) {
case 1: // 连接状态
if (value[0] === 0x01) {
emitter.emit({
eventId: EventConstants.DEVICE_CONNECTED
}, { status: 'connected' });
}
break;
case 2: // WiFi连接状态
const wifiStatus = value[0];
const signalStrength = value[1];
emitter.emit({
eventId: EventConstants.WIFI_STATUS_UPDATE
}, { wifiStatus, signalStrength });
break;
default:
Logger.debug('AccessControlManager', `未知通知数据长度: ${value.length}`);
}
}
/**
* 停止设备扫描
*/
public stopDeviceScan(): void {
if (!this.isScanning) {
return;
}
try {
ble.off('BLEDeviceFind');
ble.stopBLEScan();
Logger.info('AccessControlManager', '设备扫描已停止');
} catch (error) {
Logger.error('AccessControlManager', '停止扫描失败');
} finally {
this.isScanning = false;
}
}
}
// 设备数据类型定义
export interface BluetoothDevice {
deviceId: string;
deviceName: string;
rssi: number;
isConnectable: boolean;
}
1.3.2 WiFi配网模块实现
// features/device/src/main/ets/wifi/DeviceNetworkingManager.ets
import { wifi, connection } from '@ohos.wifi';
import { BusinessError } from '@ohos.base';
import { Logger } from '@elderly/basic';
import { AccessControlManager } from '../bluetooth/AccessControlManager';
/**
* 设备配网管理器
* 负责WiFi网络扫描、密码配置和网络连接状态管理
*/
export class DeviceNetworkingManager {
private static instance: DeviceNetworkingManager;
private accessControlManager: AccessControlManager;
private isScanningWifi: boolean = false;
private wifiList: Array<WifiScanInfo> = [];
private constructor() {
this.accessControlManager = AccessControlManager.getInstance();
}
public static getInstance(): DeviceNetworkingManager {
if (!DeviceNetworkingManager.instance) {
DeviceNetworkingManager.instance = new DeviceNetworkingManager();
}
return DeviceNetworkingManager.instance;
}
/**
* 检查WiFi状态
*/
public async checkWifiStatus(): Promise<WifiStatus> {
try {
const isActive = await wifi.isWifiActive();
if (!isActive) {
return {
isEnabled: false,
message: 'WiFi未开启,请先打开WiFi'
};
}
const connectionInfo = await wifi.getLinkedInfo();
return {
isEnabled: true,
isConnected: connectionInfo !== null,
ssid: connectionInfo?.ssid,
signalLevel: this.calculateSignalLevel(connectionInfo?.rssi || -100)
};
} catch (error) {
const err: BusinessError = error as BusinessError;
Logger.error('DeviceNetworkingManager', `检查WiFi状态失败: ${err.message}`);
return {
isEnabled: false,
message: '无法获取WiFi状态'
};
}
}
/**
* 扫描周边WiFi网络
*/
public async scanWifiNetworks(): Promise<Array<WifiScanInfo>> {
if (this.isScanningWifi) {
Logger.warn('DeviceNetworkingManager', 'WiFi扫描已在进行中');
return this.wifiList;
}
try {
this.isScanningWifi = true;
this.wifiList = [];
Logger.info('DeviceNetworkingManager', '开始扫描WiFi网络...');
// 请求WiFi扫描权限(需在module.json5中声明)
await this.requestWifiPermissions();
// 开始扫描
await wifi.scan();
// 获取扫描结果
const scanResults = await wifi.getScanInfoList();
// 过滤和排序:按信号强度降序排列
this.wifiList = scanResults
.filter(wifi => !wifi.ssid.includes('<unknown ssid>') && wifi.ssid.trim() !== '')
.sort((a, b) => (b.rssi || -100) - (a.rssi || -100))
.map(wifi => ({
ssid: wifi.ssid,
bssid: wifi.bssid,
securityType: wifi.securityType,
rssi: wifi.rssi,
band: wifi.band,
frequency: wifi.frequency,
timestamp: wifi.timestamp
}));
Logger.info('DeviceNetworkingManager', `扫描完成,发现${this.wifiList.length}个WiFi网络`);
return this.wifiList;
} catch (error) {
const err: BusinessError = error as BusinessError;
Logger.error('DeviceNetworkingManager', `WiFi扫描失败: ${err.message}`);
throw new Error(`WiFi扫描失败: ${err.message}`);
} finally {
this.isScanningWifi = false;
}
}
/**
* 为设备配置WiFi网络
*/
public async configureDeviceWifi(ssid: string, password: string): Promise<NetworkConfigResult> {
try {
// 验证输入
if (!ssid || ssid.trim().length === 0) {
return {
success: false,
message: 'SSID不能为空'
};
}
if (!password || password.length < 8) {
return {
success: false,
message: '密码至少需要8个字符'
};
}
Logger.info('DeviceNetworkingManager', `开始为设备配置WiFi: ${ssid}`);
// 构建配网指令数据
const configData = this.buildWifiConfigData(ssid, password);
// 通过蓝牙发送配网指令
const sendResult = await this.sendWifiConfigToDevice(configData);
if (!sendResult) {
return {
success: false,
message: '发送配网指令失败'
};
}
// 等待设备连接WiFi(轮询状态)
const connectionResult = await this.waitForDeviceConnection(ssid, 30000);
if (connectionResult.success) {
Logger.info('DeviceNetworkingManager', `设备成功连接到WiFi: ${ssid}`);
// 上报设备网络状态到服务器
await this.reportDeviceNetworkStatus(ssid, 'connected');
return {
success: true,
message: '配网成功',
ssid,
ipAddress: connectionResult.ipAddress
};
} else {
return {
success: false,
message: `设备连接WiFi失败: ${connectionResult.message}`
};
}
} catch (error) {
const err: BusinessError = error as BusinessError;
Logger.error('DeviceNetworkingManager', `配网过程出错: ${err.message}`);
return {
success: false,
message: `配网失败: ${err.message}`
};
}
}
/**
* 构建WiFi配置数据
*/
private buildWifiConfigData(ssid: string, password: string): Uint8Array {
// 协议示例:数据头(0xAA) + 指令(0x01) + SSID长度 + SSID + 密码长度 + 密码
const encoder = new TextEncoder();
const ssidBytes = encoder.encode(ssid);
const passwordBytes = encoder.encode(password);
const data = new Uint8Array(4 + ssidBytes.length + 1 + passwordBytes.length);
let offset = 0;
// 数据头
data[offset++] = 0xAA;
// 指令:WiFi配置
data[offset++] = 0x01;
// SSID长度
data[offset++] = ssidBytes.length;
// SSID内容
data.set(ssidBytes, offset);
offset += ssidBytes.length;
// 密码长度
data[offset++] = passwordBytes.length;
// 密码内容
data.set(passwordBytes, offset);
return data;
}
/**
* 等待设备连接WiFi
*/
private async waitForDeviceConnection(ssid: string, timeout: number): Promise<DeviceConnectionResult> {
return new Promise((resolve) => {
const startTime = Date.now();
let connectionChecked = false;
// 监听设备WiFi状态通知
const handler = emitter.on(EventConstants.WIFI_STATUS_UPDATE, (data: any) => {
if (data.wifiStatus === 0x01 && !connectionChecked) { // 0x01表示连接成功
connectionChecked = true;
emitter.off(handler);
// 模拟获取设备IP地址(实际应从设备获取)
const ipAddress = this.generateRandomIP();
resolve({
success: true,
ssid,
ipAddress,
message: '连接成功'
});
}
});
// 超时检查
const checkTimeout = () => {
if (!connectionChecked && (Date.now() - startTime) > timeout) {
emitter.off(handler);
resolve({
success: false,
ssid,
message: '连接超时'
});
} else if (!connectionChecked) {
setTimeout(checkTimeout, 1000);
}
};
checkTimeout();
});
}
/**
* 上报设备网络状态
*/
private async reportDeviceNetworkStatus(ssid: string, status: string): Promise<void> {
try {
// 这里调用后端API上报设备状态
// 示例代码,实际应使用HttpService
Logger.info('DeviceNetworkingManager', `上报设备网络状态: ${ssid} - ${status}`);
// 模拟API调用
await new Promise(resolve => setTimeout(resolve, 500));
} catch (error) {
Logger.warn('DeviceNetworkingManager', '上报设备状态失败,将在下次同步时重试');
}
}
}
// 类型定义
export interface WifiStatus {
isEnabled: boolean;
isConnected?: boolean;
ssid?: string;
signalLevel?: number;
message?: string;
}
export interface WifiScanInfo {
ssid: string;
bssid: string;
securityType: number;
rssi: number;
band: number;
frequency: number;
timestamp: number;
}
export interface NetworkConfigResult {
success: boolean;
message: string;
ssid?: string;
ipAddress?: string;
}
export interface DeviceConnectionResult {
success: boolean;
ssid?: string;
ipAddress?: string;
message: string;
}
1.3.3 设备绑定页面实现
// features/device/src/main/ets/pages/DeviceBindingPage.ets
import { AccessControlManager } from '../bluetooth/AccessControlManager';
import { DeviceNetworkingManager } from '../wifi/DeviceNetworkingManager';
import { Logger } from '@elderly/basic';
import { Router } from '@elderly/basic';
import { HttpService } from '@elderly/basic';
@Component
export struct DeviceBindingPage {
@State deviceList: Array<BluetoothDevice> = [];
@State selectedDevice: BluetoothDevice | null = null;
@State currentStep: number = 1; // 1: 扫描设备, 2: 绑定设备, 3: 配置WiFi, 4: 完成
@State isLoading: boolean = false;
@State statusMessage: string = '点击开始扫描设备';
private accessControlManager: AccessControlManager = AccessControlManager.getInstance();
private networkingManager: DeviceNetworkingManager = DeviceNetworkingManager.getInstance();
private httpService: HttpService = new HttpService('https://api.elderlycare.com');
aboutToAppear(): void {
// 监听设备发现事件
emitter.on(EventConstants.DEVICE_DISCOVERED, this.handleDeviceDiscovered.bind(this));
// 监听设备连接事件
emitter.on(EventConstants.DEVICE_CONNECTED, this.handleDeviceConnected.bind(this));
}
aboutToDisappear(): void {
// 清理事件监听
emitter.off(EventConstants.DEVICE_DISCOVERED);
emitter.off(EventConstants.DEVICE_CONNECTED);
// 停止扫描
this.accessControlManager.stopDeviceScan();
}
/**
* 开始扫描设备
*/
private async startScanning(): Promise<void> {
if (this.isLoading) return;
this.isLoading = true;
this.statusMessage = '正在扫描附近的设备...';
this.deviceList = [];
try {
const devices = await this.accessControlManager.startDeviceScan(15000);
this.deviceList = devices;
if (devices.length === 0) {
this.statusMessage = '未发现门禁设备,请确保设备已开启并在范围内';
} else {
this.statusMessage = `发现 ${devices.length} 个设备,请选择要绑定的设备`;
}
} catch (error) {
this.statusMessage = '扫描失败,请检查蓝牙权限和开关';
Logger.error('DeviceBindingPage', `扫描失败: ${error.message}`);
} finally {
this.isLoading = false;
}
}
/**
* 选择设备
*/
private selectDevice(device: BluetoothDevice): void {
this.selectedDevice = device;
this.currentStep = 2;
this.statusMessage = `已选择设备: ${device.deviceName}`;
}
/**
* 绑定设备
*/
private async bindDevice(): Promise<void> {
if (!this.selectedDevice) {
return;
}
this.isLoading = true;
this.statusMessage = '正在连接并绑定设备...';
try {
// 1. 连接设备
const connected = await this.accessControlManager.connectDevice(this.selectedDevice.deviceId);
if (!connected) {
this.statusMessage = '设备连接失败';
return;
}
// 2. 调用绑定API
const bindResult = await this.httpService.post('/api/v1/devices/bind', {
deviceId: this.selectedDevice.deviceId,
deviceName: this.selectedDevice.deviceName,
deviceType: 'access_control'
});
if (bindResult.code === 200) {
Logger.info('DeviceBindingPage', '设备绑定成功');
this.currentStep = 3; // 进入配网步骤
this.statusMessage = '设备绑定成功,请配置WiFi网络';
} else {
this.statusMessage = `绑定失败: ${bindResult.message}`;
}
} catch (error) {
this.statusMessage = `绑定过程出错: ${error.message}`;
Logger.error('DeviceBindingPage', `绑定失败: ${error.message}`);
} finally {
this.isLoading = false;
}
}
/**
* 处理设备发现事件
*/
private handleDeviceDiscovered(data: any): void {
const device: BluetoothDevice = data.device;
// 更新设备列表
const existingIndex = this.deviceList.findIndex(d => d.deviceId === device.deviceId);
if (existingIndex === -1) {
this.deviceList = [...this.deviceList, device];
}
}
/**
* 处理设备连接事件
*/
private handleDeviceConnected(data: any): void {
Logger.debug('DeviceBindingPage', '设备连接状态更新');
}
/**
* 渲染扫描步骤
*/
@Builder
private renderScanStep() {
Column({ space: 20 }) {
// 扫描状态显示
Text(this.statusMessage)
.fontSize(18)
.fontColor(this.isLoading ? Color.Blue : Color.Gray)
.textAlign(TextAlign.Center)
.width('100%')
// 扫描按钮
Button(this.isLoading ? '扫描中...' : '开始扫描')
.width('80%')
.height(50)
.backgroundColor(this.isLoading ? Color.Gray : Color.Blue)
.enabled(!this.isLoading)
.onClick(() => this.startScanning())
// 设备列表
if (this.deviceList.length > 0) {
List({ space: 10 }) {
ForEach(this.deviceList, (device: BluetoothDevice) => {
ListItem() {
this.renderDeviceItem(device);
}
})
}
.height(300)
.width('100%')
}
}
.width('100%')
.height('100%')
.padding(20)
}
/**
* 渲染设备项
*/
@Builder
private renderDeviceItem(device: BluetoothDevice) {
Row({ space: 15 }) {
// 设备图标
Image($r('app.media.ic_device_lock'))
.width(40)
.height(40)
Column({ space: 5 }) {
Text(device.deviceName)
.fontSize(16)
.fontColor(Color.Black)
Text(`信号强度: ${device.rssi}dBm`)
.fontSize(12)
.fontColor(Color.Gray)
}
.layoutWeight(1)
// 选择按钮
Button('选择')
.width(60)
.height(30)
.onClick(() => this.selectDevice(device))
}
.width('100%')
.padding(10)
.backgroundColor(Color.White)
.borderRadius(8)
.shadow({ radius: 2, color: Color.Gray })
}
build() {
Column({ space: 20 }) {
// 进度指示器
this.renderProgressIndicator()
// 步骤内容
if (this.currentStep === 1) {
this.renderScanStep();
} else if (this.currentStep === 2) {
this.renderBindStep();
} else if (this.currentStep === 3) {
this.renderNetworkStep();
} else {
this.renderCompleteStep();
}
}
.width('100%')
.height('100%')
.backgroundColor('#F5F7FA')
}
}
第二部分:头像上传与昵称修改功能实现
2.1 用户信息管理架构设计
2.2 头像上传功能完整实现
2.2.1 图片选择与处理服务
// common/basic/src/main/ets/services/ImagePickerService.ets
import { picker, photoAccessHelper } from '@ohos.file.picker';
import { image } from '@ohos.multimedia.image';
import { fileIo } from '@ohos.file.fs';
import { BusinessError } from '@ohos.base';
import { Logger } from '../utils/Logger';
/**
* 图片选择与处理服务
* 支持拍照、相册选择、图片压缩和格式转换
*/
export class ImagePickerService {
private static instance: ImagePickerService;
private maxFileSize: number = 5 * 1024 * 1024; // 5MB限制
private supportedFormats: Array<string> = ['image/jpeg', 'image/png', 'image/webp'];
public static getInstance(): ImagePickerService {
if (!ImagePickerService.instance) {
ImagePickerService.instance = new ImagePickerService();
}
return ImagePickerService.instance;
}
/**
* 显示图片选择器
*/
public async showImagePicker(options: ImagePickerOptions = {}): Promise<ImagePickerResult> {
const defaultOptions: ImagePickerOptions = {
sourceType: 'both', // both, camera, gallery
maxSelectCount: 1,
enableCrop: true,
cropAspectRatio: [1, 1], // 正方形
maxWidth: 1024,
maxHeight: 1024,
quality: 85
};
const finalOptions = { ...defaultOptions, ...options };
try {
let result: ImagePickerResult;
if (finalOptions.sourceType === 'camera') {
result = await this.pickFromCamera(finalOptions);
} else if (finalOptions.sourceType === 'gallery') {
result = await this.pickFromGallery(finalOptions);
} else {
// 显示选择对话框
const source = await this.showSourceSelector();
if (source === 'camera') {
result = await this.pickFromCamera(finalOptions);
} else if (source === 'gallery') {
result = await this.pickFromGallery(finalOptions);
} else {
return {
success: false,
message: '用户取消了选择'
};
}
}
return result;
} catch (error) {
const err: BusinessError = error as BusinessError;
Logger.error('ImagePickerService', `图片选择失败: ${err.message}`);
return {
success: false,
message: `图片选择失败: ${err.message}`
};
}
}
/**
* 从相机获取图片
*/
private async pickFromCamera(options: ImagePickerOptions): Promise<ImagePickerResult> {
try {
const cameraPicker = new picker.CameraPicker();
const pickerOptions: picker.CameraSelectOptions = {
maxSelectNumber: options.maxSelectCount || 1,
MIMEType: picker.PhotoViewMIMETypes.IMAGE_TYPE
};
const result = await cameraPicker.select(pickerOptions);
if (!result || result.photoUris.length === 0) {
return {
success: false,
message: '未选择图片'
};
}
const imageUri = result.photoUris[0];
// 验证图片
const validation = await this.validateImage(imageUri);
if (!validation.valid) {
return {
success: false,
message: validation.message
};
}
// 处理图片(压缩、裁剪等)
const processedImage = await this.processImage(imageUri, options);
return {
success: true,
uri: processedImage.uri,
base64: processedImage.base64,
width: processedImage.width,
height: processedImage.height,
size: processedImage.size,
mimeType: processedImage.mimeType
};
} catch (error) {
const err: BusinessError = error as BusinessError;
throw new Error(`相机选择失败: ${err.message}`);
}
}
/**
* 从相册获取图片
*/
private async pickFromGallery(options: ImagePickerOptions): Promise<ImagePickerResult> {
try {
const photoPicker = new picker.PhotoViewPicker();
const pickerOptions: picker.PhotoSelectOptions = {
maxSelectNumber: options.maxSelectCount || 1,
MIMEType: picker.PhotoViewMIMETypes.IMAGE_TYPE,
isPreview: true
};
const result = await photoPicker.select(pickerOptions);
if (!result || result.photoUris.length === 0) {
return {
success: false,
message: '未选择图片'
};
}
const imageUri = result.photoUris[0];
// 验证图片
const validation = await this.validateImage(imageUri);
if (!validation.valid) {
return {
success: false,
message: validation.message
};
}
// 处理图片
const processedImage = await this.processImage(imageUri, options);
return {
success: true,
uri: processedImage.uri,
base64: processedImage.base64,
width: processedImage.width,
height: processedImage.height,
size: processedImage.size,
mimeType: processedImage.mimeType
};
} catch (error) {
const err: BusinessError = error as BusinessError;
throw new Error(`相册选择失败: ${err.message}`);
}
}
/**
* 处理图片(压缩、裁剪等)
*/
private async processImage(uri: string, options: ImagePickerOptions): Promise<ProcessedImage> {
try {
// 创建图片源
const imageSource = image.createImageSource(uri);
// 获取图片信息
const imageInfo = await imageSource.getImageInfo();
// 计算目标尺寸
const targetSize = this.calculateTargetSize(
{ width: imageInfo.size.width, height: imageInfo.size.height },
options
);
// 创建PixelMap
const decodingOptions: image.DecodingOptions = {
desiredSize: targetSize,
editable: true
};
const pixelMap = await imageSource.createPixelMap(decodingOptions);
// 如果需要裁剪
let croppedPixelMap = pixelMap;
if (options.enableCrop && options.cropAspectRatio) {
croppedPixelMap = await this.cropImage(pixelMap, options.cropAspectRatio);
}
// 保存处理后的图片到临时文件
const tempUri = await this.saveToTempFile(croppedPixelMap, options);
// 转换为base64
const base64Data = await this.convertToBase64(tempUri);
// 释放资源
imageSource.release();
pixelMap.release();
if (croppedPixelMap !== pixelMap) {
croppedPixelMap.release();
}
return {
uri: tempUri,
base64: base64Data,
width: croppedPixelMap.getImageInfo().size.width,
height: croppedPixelMap.getImageInfo().size.height,
size: await this.getFileSize(tempUri),
mimeType: 'image/jpeg'
};
} catch (error) {
const err: BusinessError = error as BusinessError;
throw new Error(`图片处理失败: ${err.message}`);
}
}
/**
* 计算目标尺寸
*/
private calculateTargetSize(
originalSize: Size,
options: ImagePickerOptions
): Size {
const maxWidth = options.maxWidth || 1024;
const maxHeight = options.maxHeight || 1024;
let width = originalSize.width;
let height = originalSize.height;
// 如果图片尺寸大于最大限制,则进行缩放
if (width > maxWidth || height > maxHeight) {
const widthRatio = maxWidth / width;
const heightRatio = maxHeight / height;
const ratio = Math.min(widthRatio, heightRatio);
width = Math.floor(width * ratio);
height = Math.floor(height * ratio);
}
return { width, height };
}
/**
* 裁剪图片
*/
private async cropImage(
pixelMap: image.PixelMap,
aspectRatio: [number, number]
): Promise<image.PixelMap> {
const imageInfo = pixelMap.getImageInfo();
const width = imageInfo.size.width;
const height = imageInfo.size.height;
// 计算裁剪区域
const targetAspect = aspectRatio[0] / aspectRatio[1];
const currentAspect = width / height;
let cropWidth, cropHeight, cropX, cropY;
if (currentAspect > targetAspect) {
// 图片过宽,裁剪宽度
cropHeight = height;
cropWidth = Math.floor(height * targetAspect);
cropX = Math.floor((width - cropWidth) / 2);
cropY = 0;
} else {
// 图片过高,裁剪高度
cropWidth = width;
cropHeight = Math.floor(width / targetAspect);
cropX = 0;
cropY = Math.floor((height - cropHeight) / 2);
}
const cropOptions: image.InitializationOptions = {
size: {
height: cropHeight,
width: cropWidth
},
editable: true
};
return pixelMap.crop(cropOptions);
}
/**
* 保存到临时文件
*/
private async saveToTempFile(
pixelMap: image.PixelMap,
options: ImagePickerOptions
): Promise<string> {
const tempDir = globalThis.cacheDir + '/temp_images/';
const fileName = `avatar_${Date.now()}.jpg`;
const filePath = tempDir + fileName;
// 创建目录
try {
await fileIo.mkdir(tempDir);
} catch (error) {
// 目录可能已存在
}
// 创建图片打包器
const imagePacker = image.createImagePacker();
// 打包选项
const packOptions: image.PackingOption = {
format: 'image/jpeg',
quality: options.quality || 85
};
// 打包图片
const arrayBuffer = await imagePacker.packing(pixelMap, packOptions);
// 写入文件
const file = await fileIo.open(filePath, fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.CREATE);
await fileIo.write(file.fd, arrayBuffer);
await fileIo.close(file.fd);
return filePath;
}
/**
* 转换为base64
*/
private async convertToBase64(filePath: string): Promise<string> {
const file = await fileIo.open(filePath, fileIo.OpenMode.READ_ONLY);
const stat = await fileIo.stat(filePath);
const buffer = new ArrayBuffer(stat.size);
await fileIo.read(file.fd, buffer);
await fileIo.close(file.fd);
// ArrayBuffer转base64
const bytes = new Uint8Array(buffer);
let binary = '';
for (let i = 0; i < bytes.length; i++) {
binary += String.fromCharCode(bytes[i]);
}
return btoa(binary);
}
/**
* 获取文件大小
*/
private async getFileSize(filePath: string): Promise<number> {
const stat = await fileIo.stat(filePath);
return stat.size;
}
}
// 类型定义
export interface ImagePickerOptions {
sourceType?: 'camera' | 'gallery' | 'both';
maxSelectCount?: number;
enableCrop?: boolean;
cropAspectRatio?: [number, number];
maxWidth?: number;
maxHeight?: number;
quality?: number;
}
export interface ImagePickerResult {
success: boolean;
message?: string;
uri?: string;
base64?: string;
width?: number;
height?: number;
size?: number;
mimeType?: string;
}
export interface ProcessedImage {
uri: string;
base64: string;
width: number;
height: number;
size: number;
mimeType: string;
}
export interface Size {
width: number;
height: number;
}
2.2.2 头像上传页面组件
// features/mine/src/main/ets/pages/AvatarUploadPage.ets
import { ImagePickerService, ImagePickerResult } from '@elderly/basic';
import { UserViewModel } from '../viewmodel/UserViewModel';
import { Logger } from '@elderly/basic';
import { Router } from '@elderly/basic';
@Component
export struct AvatarUploadPage {
@State currentAvatar: string = $r('app.media.default_avatar');
@State selectedImage: string = '';
@State isLoading: boolean = false;
@State uploadProgress: number = 0;
@State errorMessage: string = '';
private imagePicker: ImagePickerService = ImagePickerService.getInstance();
private userViewModel: UserViewModel = new UserViewModel();
aboutToAppear(): void {
this.loadCurrentAvatar();
}
/**
* 加载当前头像
*/
private async loadCurrentAvatar(): Promise<void> {
try {
const userInfo = this.userViewModel.getUserInfo();
if (userInfo.avatarUrl) {
this.currentAvatar = userInfo.avatarUrl;
}
} catch (error) {
Logger.warn('AvatarUploadPage', '加载当前头像失败,使用默认头像');
}
}
/**
* 显示图片选择器
*/
private async showImagePicker(): Promise<void> {
if (this.isLoading) {
return;
}
this.isLoading = true;
this.errorMessage = '';
try {
const result: ImagePickerResult = await this.imagePicker.showImagePicker({
sourceType: 'both',
enableCrop: true,
cropAspectRatio: [1, 1],
maxWidth: 800,
maxHeight: 800,
quality: 90
});
if (result.success && result.uri) {
this.selectedImage = result.uri;
this.errorMessage = '';
} else {
this.errorMessage = result.message || '选择图片失败';
}
} catch (error) {
this.errorMessage = `选择图片时出错: ${error.message}`;
Logger.error('AvatarUploadPage', `图片选择失败: ${error.message}`);
} finally {
this.isLoading = false;
}
}
/**
* 上传头像
*/
private async uploadAvatar(): Promise<void> {
if (!this.selectedImage) {
this.errorMessage = '请先选择图片';
return;
}
this.isLoading = true;
this.uploadProgress = 0;
this.errorMessage = '';
try {
// 模拟上传进度(实际应使用分块上传和进度回调)
const progressInterval = setInterval(() => {
if (this.uploadProgress < 90) {
this.uploadProgress += 10;
}
}, 200);
// 实际上传逻辑
const result = await this.userViewModel.updateAvatar(this.selectedImage);
clearInterval(progressInterval);
this.uploadProgress = 100;
if (result.success) {
// 更新本地显示
this.currentAvatar = this.selectedImage;
// 延迟返回,让用户看到上传成功
setTimeout(() => {
Router.back();
}, 1000);
} else {
this.errorMessage = result.message;
this.uploadProgress = 0;
}
} catch (error) {
this.errorMessage = `上传失败: ${error.message}`;
this.uploadProgress = 0;
Logger.error('AvatarUploadPage', `头像上传失败: ${error.message}`);
} finally {
this.isLoading = false;
}
}
/**
* 取消上传
*/
private cancelUpload(): void {
this.selectedImage = '';
this.uploadProgress = 0;
this.errorMessage = '';
}
build() {
Column({ space: 30 }) {
// 页面标题
Text('修改头像')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.fontColor(Color.Black)
.width('100%')
.textAlign(TextAlign.Center)
// 头像显示区域
Column({ space: 15 }) {
Stack({ alignContent: Alignment.Center }) {
// 当前头像
Image(this.selectedImage || this.currentAvatar)
.width(200)
.height(200)
.borderRadius(100)
.border({ width: 4, color: Color.White })
.shadow({ radius: 10, color: '#00000020' })
// 上传进度指示器
if (this.isLoading && this.uploadProgress > 0) {
LoadingProgress()
.width(60)
.height(60)
.color(Color.Blue)
Text(`${this.uploadProgress}%`)
.fontSize(14)
.fontColor(Color.Blue)
.margin({ top: 70 })
}
}
.width(200)
.height(200)
// 选择图片按钮
if (!this.selectedImage) {
Button('选择图片')
.width(150)
.height(40)
.backgroundColor(Color.Blue)
.fontColor(Color.White)
.enabled(!this.isLoading)
.onClick(() => this.showImagePicker())
}
}
.alignItems(HorizontalAlign.Center)
.width('100%')
// 错误信息
if (this.errorMessage) {
Text(this.errorMessage)
.fontSize(14)
.fontColor(Color.Red)
.textAlign(TextAlign.Center)
.width('90%')
.padding(10)
.backgroundColor('#FFF5F5')
.borderRadius(8)
}
// 操作按钮
if (this.selectedImage) {
Row({ space: 20 }) {
Button('取消')
.width(120)
.height(45)
.backgroundColor('#F0F0F0')
.fontColor(Color.Black)
.enabled(!this.isLoading)
.onClick(() => this.cancelUpload())
Button(this.isLoading ? '上传中...' : '确认上传')
.width(120)
.height(45)
.backgroundColor(Color.Blue)
.fontColor(Color.White)
.enabled(!this.isLoading)
.onClick(() => this.uploadAvatar())
}
.margin({ top: 20 })
}
// 使用说明
Column({ space: 10 }) {
Text('温馨提示')
.fontSize(16)
.fontColor(Color.Gray)
.fontWeight(FontWeight.Medium)
Text('• 建议使用正方形图片')
.fontSize(14)
.fontColor(Color.Gray)
Text('• 图片大小不超过5MB')
.fontSize(14)
.fontColor(Color.Gray)
Text('• 支持JPG、PNG、WebP格式')
.fontSize(14)
.fontColor(Color.Gray)
}
.width('90%')
.padding(15)
.backgroundColor('#F8F9FA')
.borderRadius(10)
.margin({ top: 30 })
}
.width('100%')
.height('100%')
.padding(20)
.backgroundColor(Color.White)
}
}
2.3 昵称修改功能实现
2.3.1 昵称编辑组件
// features/mine/src/main/ets/components/NicknameEditor.ets
import { UserViewModel } from '../viewmodel/UserViewModel';
import { Logger } from '@elderly/basic';
import { Prompt } from '@ohos.prompt';
@Component
export struct NicknameEditor {
@State currentNickname: string = '';
@State editingNickname: string = '';
@State isEditing: boolean = false;
@State isLoading: boolean = false;
@State validationError: string = '';
private userViewModel: UserViewModel = new UserViewModel();
private readonly NICKNAME_MIN_LENGTH: number = 2;
private readonly NICKNAME_MAX_LENGTH: number = 20;
aboutToAppear(): void {
this.loadCurrentNickname();
}
/**
* 加载当前昵称
*/
private async loadCurrentNickname(): Promise<void> {
try {
const userInfo = this.userViewModel.getUserInfo();
this.currentNickname = userInfo.nickname || '';
this.editingNickname = this.currentNickname;
} catch (error) {
Logger.error('NicknameEditor', `加载昵称失败: ${error.message}`);
}
}
/**
* 开始编辑
*/
private startEditing(): void {
this.isEditing = true;
this.validationError = '';
this.editingNickname = this.currentNickname;
}
/**
* 取消编辑
*/
private cancelEditing(): void {
this.isEditing = false;
this.validationError = '';
this.editingNickname = this.currentNickname;
}
/**
* 验证昵称
*/
private validateNickname(nickname: string): ValidationResult {
const trimmedNickname = nickname.trim();
// 检查空值
if (trimmedNickname.length === 0) {
return {
valid: false,
message: '昵称不能为空'
};
}
// 检查长度
if (trimmedNickname.length < this.NICKNAME_MIN_LENGTH) {
return {
valid: false,
message: `昵称至少需要${this.NICKNAME_MIN_LENGTH}个字符`
};
}
if (trimmedNickname.length > this.NICKNAME_MAX_LENGTH) {
return {
valid: false,
message: `昵称不能超过${this.NICKNAME_MAX_LENGTH}个字符`
};
}
// 检查字符范围(中文、英文、数字、下划线、减号)
const nicknameRegex = /^[\u4e00-\u9fa5a-zA-Z0-9_\-]+$/;
if (!nicknameRegex.test(trimmedNickname)) {
return {
valid: false,
message: '昵称只能包含中文、英文、数字、下划线和减号'
};
}
// 检查敏感词(养老场景特殊要求)
if (this.containsSensitiveWords(trimmedNickname)) {
return {
valid: false,
message: '昵称包含不适当的词汇'
};
}
return {
valid: true,
message: ''
};
}
/**
* 保存昵称
*/
private async saveNickname(): Promise<void> {
// 验证昵称
const validation = this.validateNickname(this.editingNickname);
if (!validation.valid) {
this.validationError = validation.message;
return;
}
// 检查是否与当前昵称相同
if (this.editingNickname === this.currentNickname) {
this.isEditing = false;
return;
}
this.isLoading = true;
this.validationError = '';
try {
Logger.info('NicknameEditor', `开始保存昵称: ${this.editingNickname}`);
// 调用ViewModel更新昵称
const result = await this.userViewModel.updateNickname(this.editingNickname);
if (result.success) {
this.currentNickname = this.editingNickname;
// 显示成功提示
Prompt.showToast({
message: '昵称修改成功',
duration: 2000,
bottom: '50vp'
});
this.isEditing = false;
} else {
this.validationError = result.message || '保存失败';
}
} catch (error) {
Logger.error('NicknameEditor', `保存昵称失败: ${error.message}`);
this.validationError = '保存失败,请检查网络连接';
} finally {
this.isLoading = false;
}
}
/**
* 渲染显示模式
*/
@Builder
private renderDisplayMode() {
Row({ space: 15 }) {
Column({ space: 5 }) {
Text('昵称')
.fontSize(16)
.fontColor('#666666')
Text(this.currentNickname || '未设置')
.fontSize(18)
.fontColor(this.currentNickname ? Color.Black : '#999999')
}
.layoutWeight(1)
Button('修改')
.width(70)
.height(32)
.fontSize(14)
.backgroundColor(Color.Transparent)
.borderColor(Color.Blue)
.borderWidth(1)
.fontColor(Color.Blue)
.onClick(() => this.startEditing())
}
.width('100%')
.padding({ top: 15, bottom: 15 })
.border({ bottom: { width: 1, color: '#EEEEEE' } })
}
/**
* 渲染编辑模式
*/
@Builder
private renderEditMode() {
Column({ space: 15 }) {
// 输入框
TextInput({
text: this.editingNickname,
placeholder: `请输入${this.NICKNAME_MIN_LENGTH}-${this.NICKNAME_MAX_LENGTH}位字符`
})
.width('100%')
.height(50)
.fontSize(16)
.padding({ left: 15, right: 15 })
.backgroundColor('#F8F9FA')
.borderRadius(8)
.onChange((value: string) => {
this.editingNickname = value;
this.validationError = ''; // 清空错误信息
})
// 字符计数
Row() {
Text(`${this.editingNickname.length}/${this.NICKNAME_MAX_LENGTH}`)
.fontSize(12)
.fontColor(
this.editingNickname.length > this.NICKNAME_MAX_LENGTH ?
Color.Red : '#999999'
)
}
.width('100%')
.justifyContent(FlexAlign.End)
// 错误提示
if (this.validationError) {
Text(this.validationError)
.fontSize(12)
.fontColor(Color.Red)
.width('100%')
}
// 操作按钮
Row({ space: 20 }) {
Button('取消')
.width(100)
.height(40)
.backgroundColor('#F0F0F0')
.fontColor(Color.Black)
.enabled(!this.isLoading)
.onClick(() => this.cancelEditing())
Button(this.isLoading ? '保存中...' : '保存')
.width(100)
.height(40)
.backgroundColor(
this.editingNickname === this.currentNickname || this.isLoading ?
'#CCCCCC' : Color.Blue
)
.fontColor(Color.White)
.enabled(
this.editingNickname !== this.currentNickname &&
!this.isLoading
)
.onClick(() => this.saveNickname())
}
.width('100%')
.justifyContent(FlexAlign.Center)
.margin({ top: 10 })
}
.width('100%')
.padding({ top: 15, bottom: 15 })
.border({ bottom: { width: 1, color: '#EEEEEE' } })
}
build() {
Column() {
if (this.isEditing) {
this.renderEditMode();
} else {
this.renderDisplayMode();
}
}
.width('100%')
}
}
// 类型定义
interface ValidationResult {
valid: boolean;
message: string;
}
2.4 用户信息整合页面
// features/mine/src/main/ets/pages/UserProfilePage.ets
import { UserViewModel } from '../viewmodel/UserViewModel';
import { NicknameEditor } from '../components/NicknameEditor';
import { Router } from '@elderly/basic';
import { Logger } from '@elderly/basic';
@Component
export struct UserProfilePage {
@State userInfo: UserInfo = {
id: '',
nickname: '',
avatarUrl: '',
phone: '',
email: '',
role: '',
joinDate: ''
};
@State isLoading: boolean = true;
private userViewModel: UserViewModel = new UserViewModel();
aboutToAppear(): void {
this.loadUserProfile();
// 订阅用户信息更新
this.userViewModel.subscribeToUpdates((updatedInfo: UserInfo) => {
this.userInfo = updatedInfo;
});
}
/**
* 加载用户信息
*/
private async loadUserProfile(): Promise<void> {
this.isLoading = true;
try {
await this.userViewModel.loadUserInfo();
this.userInfo = this.userViewModel.getUserInfo();
} catch (error) {
Logger.error('UserProfilePage', `加载用户信息失败: ${error.message}`);
} finally {
this.isLoading = false;
}
}
/**
* 跳转到头像上传页面
*/
private navigateToAvatarUpload(): void {
Router.pushUrl({
url: 'pages/AvatarUploadPage'
});
}
/**
* 渲染用户信息项
*/
@Builder
private renderInfoItem(label: string, value: string, editable: boolean = false) {
Row({ space: 15 }) {
Text(label)
.fontSize(16)
.fontColor('#666666')
.width(80)
Text(value || '未设置')
.fontSize(16)
.fontColor(value ? Color.Black : '#999999')
.layoutWeight(1)
if (editable) {
Image($r('app.media.ic_arrow_right'))
.width(16)
.height(16)
}
}
.width('100%')
.padding({ top: 18, bottom: 18 })
.border({ bottom: { width: 0.5, color: '#F0F0F0' } })
.onClick(() => {
if (editable && label === '头像') {
this.navigateToAvatarUpload();
}
})
}
/**
* 渲染头像项(特殊处理)
*/
@Builder
private renderAvatarItem() {
Row({ space: 15 }) {
Text('头像')
.fontSize(16)
.fontColor('#666666')
.width(80)
Image(this.userInfo.avatarUrl || $r('app.media.default_avatar'))
.width(60)
.height(60)
.borderRadius(30)
.border({ width: 2, color: Color.White })
.shadow({ radius: 2, color: '#00000010' })
.layoutWeight(1)
Image($r('app.media.ic_arrow_right'))
.width(16)
.height(16)
}
.width('100%')
.padding({ top: 18, bottom: 18 })
.border({ bottom: { width: 0.5, color: '#F0F0F0' } })
.onClick(() => {
this.navigateToAvatarUpload();
})
}
build() {
Column({ space: 0 }) {
// 页面标题
Text('个人信息')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor(Color.Black)
.width('100%')
.textAlign(TextAlign.Center)
.padding({ top: 20, bottom: 30 })
// 加载状态
if (this.isLoading) {
Column() {
LoadingProgress()
.color(Color.Blue)
.width(40)
.height(40)
Text('加载中...')
.fontSize(14)
.fontColor('#666666')
.margin({ top: 15 })
}
.width('100%')
.height(300)
.justifyContent(FlexAlign.Center)
} else {
// 用户信息列表
List() {
// 头像
ListItem() {
this.renderAvatarItem()
}
// 昵称(使用自定义编辑器)
ListItem() {
Column() {
Text('昵称')
.fontSize(16)
.fontColor('#666666')
.margin({ bottom: 10 })
NicknameEditor()
}
.width('100%')
.padding({ top: 18, bottom: 18 })
.border({ bottom: { width: 0.5, color: '#F0F0F0' } })
}
// 手机号
ListItem() {
this.renderInfoItem('手机号', this.userInfo.phone)
}
// 邮箱
ListItem() {
this.renderInfoItem('邮箱', this.userInfo.email)
}
// 角色
ListItem() {
this.renderInfoItem('角色', this.userInfo.role)
}
// 加入日期
ListItem() {
this.renderInfoItem('加入日期', this.userInfo.joinDate)
}
}
.width('100%')
.layoutWeight(1)
.divider({
strokeWidth: 0.5,
color: '#F0F0F0',
startMargin: 20,
endMargin: 20
})
}
}
.width('100%')
.height('100%')
.backgroundColor(Color.White)
}
}
// 类型定义
interface UserInfo {
id: string;
nickname: string;
avatarUrl: string;
phone: string;
email: string;
role: string;
joinDate: string;
}
总结
本文详细阐述了益康养老HarmonyOS APP中两个核心功能模块的实现方案:
-
门禁设备绑定与配网流程:通过蓝牙发现、连接、绑定,再通过WiFi配网,实现了物联网设备的智能化管理。该方案充分利用了HarmonyOS的分布式能力和跨设备协同特性。
-
用户信息管理功能:实现了完整的头像上传(支持拍照、相册选择、图片处理)和昵称修改功能,提供了良好的用户体验和健壮的错误处理机制。
这两个功能模块都严格遵循了鸿蒙官方开发规范,采用了三层架构设计,确保了代码的可维护性、可扩展性和性能优化。通过本文的实现方案,可以为养老行业提供一套稳定、易用、安全的智能设备管理和用户信息管理解决方案。
更多推荐
所有评论(0)