HBuilderX

HBuilderX

极客开发工具
uni-app

uni-app

开发一次,多端覆盖
uniCloud

uniCloud

云开发平台
HTML5+

HTML5+

增强HTML5的功能体验
MUI

MUI

上万Star的前端框架

从痛点到产品:uni-app x + HarmonyOS打造房产投资管理系统全记录

鸿蒙next 鸿蒙征文

📖 目录

项目背景:从房东的痛点说起 {#项目背景}

故事的开始

2024年初,我在苏州购入了第二套投资房产,准备出租。作为一个程序员房东,我自然而然地想用科技手段来管理我的资产。然而现实很骨感:

痛点一:信息管理混乱

  • Excel表格记录房产信息,版本管理困难
  • 租户资料散落在微信聊天记录里
  • 合同文档放在电脑、手机、云盘多个地方
  • 想查个租客电话号码都要翻半天

痛点二:财务统计困难

  • 每月收租日期记不住,经常漏收
  • 水电费、物业费、维修费记录混乱
  • 年底算投资回报率要拿出计算器算半天
  • 不知道哪套房子最赚钱,哪套在亏损

痛点三:决策缺乏数据支撑

  • 出租率是多少?不清楚
  • 平均租金水平?凭感觉
  • 租客流失率?没统计过
  • 要不要继续投资?拍脑袋决定

市场调研

我研究了市面上的房产管理软件:

结论:没有一款软件能完美满足我的需求!

需求分析

作为开发者,我决定自己动手。经过两周的需求梳理,我列出了核心功能:

必备功能(MVP):

  • ✅ 房产档案管理(地址、面积、价格)
  • ✅ 房间状态管理(空置/已租/维修)
  • ✅ 租户信息管理(联系方式、合同期限)
  • ✅ 收租记录管理(租金、水电费)
  • ✅ 财务收支记录(收入、支出分类)
  • ✅ 数据统计分析(出租率、投资回报率)

进阶功能(Nice to have):

  • 🔄 收租日提醒
  • 📊 数据可视化图表
  • 📱 移动端随时查看
  • 🔒 数据本地存储(隐私安全)
  • 🎨 专业的商务风格UI

目标用户画像

张先生,38岁,IT行业,苏州

  • 持有3套房产用于投资
  • 工作繁忙,希望高效管理
  • 重视数据隐私,不愿上传云端
  • 需要专业的数据支撑投资决策
  • 🛠️ 为什么选择uni-app x + HarmonyOS {#技术选型}

技术选型的纠结过程

作为一个全栈开发者,我面临着技术选型的"幸福烦恼"。让我列出当时的思考过程:

方案一:原生开发(HarmonyOS + Swift/Kotlin)

优势:

  • ✅ 性能最佳,用户体验极致
  • ✅ 可以使用平台最新特性
  • ✅ 没有框架限制

劣势:

  • ❌ 开发成本高(三端分别开发)
  • ❌ 维护困难(代码量 x3)
  • ❌ 学习成本高(多种语言)
  • ❌ 时间成本无法接受(个人项目)

结论: 🚫 虽然性能最好,但对个人开发者不现实

方案二:React Native

优势:

  • ✅ 生态成熟,社区活跃
  • ✅ 组件库丰富
  • ✅ 跨平台能力强

劣势:

  • ❌ 对HarmonyOS支持有限
  • ❌ 包体积较大(基础包10MB+)
  • ❌ 性能相对较差
  • ❌ 需要学习React生态

结论: 🤔 可行,但HarmonyOS支持是短板

方案三:Flutter

优势:

  • ✅ 性能优秀(接近原生)
  • ✅ UI组件精美
  • ✅ 热重载提升效率

劣势:

  • ❌ Dart语言学习成本
  • ❌ 包体积大(基础包15MB+)
  • ❌ HarmonyOS支持需要额外适配
  • ❌ 对前端开发者不够友好

结论: 🤔 性能好,但学习成本高

方案四:uni-app x(最终选择)✨

优势:

  • 原生性能:基于原生渲染,性能接近原生
  • TypeScript支持:类型安全,开发体验好
  • 一次开发,三端运行:iOS、Android、HarmonyOS
  • Vue生态:前端开发者无缝上手
  • 官方HarmonyOS支持:DCloud官方适配
  • 包体积小:基础包仅5MB
  • 开发效率高:热重载+组件化

劣势:

  • ⚠️ 生态相对较新(但在快速完善)
  • ⚠️ 部分高级特性需要等待

结论: 🎉 综合考虑,uni-app x是最佳选择!

为什么HarmonyOS是未来趋势?

作为开发者,我深信HarmonyOS代表着移动生态的未来

  1. 国产化浪潮:信创政策推动,政企市场潜力巨大
  2. 技术创新:分布式能力、原子化服务让人眼前一亮
  3. 生态建设:华为全力推进,开发者红利期
  4. 市场份额:2024年国内份额已突破20%,增长迅猛
  5. 开发者友好:完善的文档、活跃的社区

我的判断:提前布局HarmonyOS,就是抓住下一个技术红利!

最终技术栈

经过一周的调研和demo测试,我确定了技术栈:

核心技术明细:

  • 前端框架:uni-app x (基于Vue 3)
  • 编程语言:TypeScript (主要) + UTS (平台特性)
  • 样式方案:SCSS + CSS Variables(主题系统)
  • 数据库:HarmonyOS relationalStore (SQLite)
  • 状态管理:Composition API + Reactive
  • 数据请求:uni.request + Promise
  • 本地存储:uni.storage (配置) + relationalStore (业务数据)
  • UI组件:uni-ui + 自定义组件库

    🏗️ 系统架构设计:四大模块协同作战 {#架构设计}

整体架构图

系统采用分层架构 + 模块化设计

核心数据模型(ER图)

数据关联关系是整个系统的灵魂! 我花了三天时间设计数据模型,确保数据关联的合理性和扩展性。

设计亮点:

  1. property_id + room_id 双重关联:既支持房产级统计,也支持房间级追踪
  2. 自动同步机制:收租记录→财务记录的自动创建
  3. 状态联动:租户状态→房间状态的自动更新
  4. 数据溯源:通过description字段记录数据来源

项目目录结构

房产投资管理系统/  
│  
├── pages/                          # 页面目录(18个页面)  
│   ├── index/                      # 首页模块  
│   │   └── index.vue              # 首页(数据总览)  
│   │  
│   ├── property/                   # 房产管理模块  
│   │   ├── list.vue               # 房产列表  
│   │   ├── add.vue                # 添加/编辑房产  
│   │   └── detail.vue             # 房产详情  
│   │  
│   ├── rental/                     # 租赁管理模块  
│   │   ├── room-list.vue          # 房间列表  
│   │   ├── room-add.vue           # 添加/编辑房间  
│   │   ├── room-detail.vue        # 房间详情  
│   │   ├── tenant-list.vue        # 租户列表  
│   │   ├── tenant-add.vue         # 添加/编辑租户  
│   │   ├── tenant-detail.vue      # 租户详情  
│   │   ├── rent-collection.vue    # 收租记录列表  
│   │   └── rent-add.vue           # 添加收租记录  
│   │  
│   ├── finance/                    # 财务管理模块  
│   │   ├── list.vue               # 财务记录列表  
│   │   └── add.vue                # 添加财务记录  
│   │  
│   └── stats/                      # 统计分析模块  
│       └── index.vue              # 数据统计页  
│  
├── uni_modules/                    # 插件模块  
│   └── test-relationalStore/      # 数据库封装  
│       └── utssdk/  
│           └── app-harmony/  
│               └── index.uts      # HarmonyOS数据库适配  
│  
├── static/                         # 静态资源  
│   ├── logo.png                   # 应用图标  
│   ├── 我的资产.png                # Tab图标  
│   ├── 房产.png  
│   ├── 财务.png  
│   └── 统计.png  
│  
├── uni.scss                        # 全局样式变量  
├── App.vue                         # 应用入口  
├── pages.json                      # 页面配置  
├── manifest.json                   # 应用配置  
└── project_analysis.md             # 项目开发文档

设计原则:

  • 模块化:按功能模块划分目录
  • 复用性:list-add-detail的标准页面结构
  • 可维护性:清晰的命名和目录层级

💎 核心功能实现:18个页面的血泪史 {#核心功能}

功能模块一:房产管理 - 资产的数字化档案

1.1 房产列表页(pages/property/list.vue)

功能需求:

  • 展示所有房产列表
  • 显示总资产价值
  • 支持房产的增删改查

核心代码:总资产价值计算

// 计算总资产价值  
computed: {  
    totalValue() {  
        const total = this.propertyList.reduce((sum, item) => {  
            // 优先使用当前估值,否则使用购买价格  
            const currentValue = parseFloat(item.current_value) || 0;  
            const purchasePrice = parseFloat(item.purchase_price) || 0;  
            const propertyValue = currentValue || purchasePrice;  

            console.log(`房产 ${item.address}:   
                current_value=${item.current_value}(${typeof item.current_value}),   
                purchase_price=${item.purchase_price}(${typeof item.purchase_price}),   
                使用价值=${propertyValue}`);  

            return sum + propertyValue;  
        }, 0);  

        console.log('总资产价值计算结果:', total);  
        return total;  
    }  
}

踩坑记录1:资产价值计算错误

问题现象:
总资产显示为"100200300"而不是"600"(三套房各200万)

原因分析:
数据库查询时用getString获取价格字段,导致数值以字符串形式返回。JavaScript的+操作符对字符串执行拼接而非数学加法。

解决方案:
在数据库转换函数中,数值类型字段使用getDouble而非getString

// ❌ 错误写法  
function convertResultSetToPropertyArray(resultSet) {  
    record.purchase_price = resultSet.getString(  
        resultSet.getColumnIndex('purchase_price')  
    ); // 返回字符串  
}  

// ✅ 正确写法  
function convertResultSetToPropertyArray(resultSet) {  
    record.purchase_price = resultSet.getDouble(  
        resultSet.getColumnIndex('purchase_price')  
    ); // 返回数值  
}

经验总结:

  • ✅ 整数/小数字段:用getDouble()
  • ✅ 文本/ID字段:用getString()
  • ✅ 在computed中加console.log调试
  • ✅ 前端再用parseFloat兜底确保类型正确

1.2 添加房产页(pages/property/add.vue)

功能需求:

  • 录入房产基本信息
  • 表单验证
  • 数据持久化

核心代码:表单验证

validateForm() {  
    // 地址验证  
    if (!this.formData.address || this.formData.address.trim() === '') {  
        uni.showToast({  
            title: '请输入房产地址',  
            icon: 'none'  
        });  
        return false;  
    }  

    // 面积验证  
    if (!this.formData.area || parseFloat(this.formData.area) <= 0) {  
        uni.showToast({  
            title: '请输入有效的房产面积',  
            icon: 'none'  
        });  
        return false;  
    }  

    // 价格验证  
    if (!this.formData.purchase_price ||   
        parseFloat(this.formData.purchase_price) <= 0) {  
        uni.showToast({  
            title: '请输入有效的购买价格',  
            icon: 'none'  
        });  
        return false;  
    }  

    return true;  
}

设计亮点:

  • 实时验证:输入时即时反馈
  • 友好提示:明确告知用户错误原因
  • 数据类型转换:字符串→数值的安全转换

功能模块二:租赁管理 - 最复杂的业务逻辑

2.1 房间管理:7种房型 x 4种付款方式 = 28种组合

功能需求:

  • 支持7种房型(单间、一室一厅、两室一厅...)
  • 支持4种付款方式(押一付一、押一付三...)
  • 自定义每月收租日(1-31号)
  • 房间状态管理(空置/已租/维修中)

核心代码:房间类型映射

// 房型映射  
getRoomTypeText(type) {  
    const typeMap = {  
        'single': '单间',  
        '1b1b': '一室一厅',  
        '2b1b': '两室一厅',  
        '2b2b': '两室两厅',  
        '3b1b': '三室一厅',  
        '3b2b': '三室两厅',  
        '4b2b': '四室两厅'  
    };  
    return typeMap[type] || type;  
}  

// 付款方式映射  
getPaymentModeText(mode) {  
    const modeMap = {  
        'monthly': '押一付一',  
        'quarterly': '押一付三',  
        'half_yearly': '半年付',  
        'yearly': '年付'  
    };  
    return modeMap[mode] || mode;  
}

踩坑记录2:收租日选择器无法显示全部31天

问题现象:
HarmonyOS的<picker>组件只能显示有限的选项,无法展示完整的31天选择。

解决方案:自定义Grid布局选择器

<template>  
    <!-- 自定义模态框 -->  
    <view class="rent-day-modal" v-if="showRentDayModal" @click="cancelRentDay">  
        <view class="modal-content" @click.stop>  
            <view class="modal-header">  
                <text class="modal-title">选择每月收租日</text>  
            </view>  

            <!-- 7x5 Grid布局显示31天 -->  
            <view class="day-grid">  
                <view class="day-item"   
                      v-for="day in 31"   
                      :key="day"  
                      :class="{ selected: selectedRentDay === day }"  
                      @click="selectRentDay(day)">  
                    <text class="day-text">{{ day }}</text>  
                </view>  
            </view>  

            <view class="modal-footer">  
                <button class="cancel-btn" @click="cancelRentDay">取消</button>  
                <button class="confirm-btn" @click="confirmRentDay">确定</button>  
            </view>  
        </view>  
    </view>  
</template>  

<style lang="scss">  
.day-grid {  
    display: grid;  
    grid-template-columns: repeat(7, 1fr); // 7列布局,展示31天  
    gap: 12rpx;  
    padding: 20rpx;  
}  

.day-item {  
    aspect-ratio: 1; // 保持正方形  
    display: flex;  
    align-items: center;  
    justify-content: center;  
    border-radius: 8rpx;  
    background: #F5F5F5;  
    transition: all 0.2s;  

    &.selected {  
        background: $primary-blue;  
        color: #FFFFFF;  
        transform: scale(1.1);  
    }  

    &:active {  
        opacity: 0.7;  
    }  
}  
</style>

经验总结:
当原生组件无法满足需求时,果断自定义实现。Grid布局非常适合日历、选择器等场景。

2.2 租户管理:隐私保护与数据联动

功能需求:

  • 租户信息录入(姓名、电话、身份证)
  • 身份证号脱敏显示
  • 租户状态与房间状态联动
  • 合同期限管理

核心代码:身份证号脱敏

// 身份证号脱敏处理  
maskIdCard(idCard) {  
    if (!idCard || idCard.length < 8) return idCard;  
    // 显示前4位和后4位,中间用*替代  
    const start = idCard.substring(0, 4);  
    const end = idCard.substring(idCard.length - 4);  
    const middle = '*'.repeat(idCard.length - 8);  
    return `${start}${middle}${end}`;  
}  

// 使用示例  
computed: {  
    maskedIdCard() {  
        return this.maskIdCard(this.tenant.id_card);  
        // 输入:510123199001011234  
        // 输出:5101**********1234  
    }  
}

踩坑记录3:租户状态判断不准确

问题现象:
主页快捷操作"添加租户",明明房间没有租户,却提示"房间已有租户"。

错误逻辑:依赖房间status字段

// ❌ 不准确的判断  
if (room.status === 'rented') {  
    uni.showToast({ title: '房间已有租户' });  
}

根本原因:
房间状态字段可能因为各种原因(删除租户时未更新、数据异常等)导致状态不准确。

正确方案:查询实际租户数据

// ✅ 准确的判断:查询数据库  
checkRoomTenants(room) {  
    uni.queryWithConditions('tenants',   
        `room_id = ${room.id} AND status = 'active'`)  
        .then(tenants => {  
            console.log(`房间 ${room.room_number} 的租户查询结果:`,   
                tenants?.length || 0, '个活跃租户');  

            if (tenants && tenants.length > 0) {  
                // 确实有租户  
                uni.showModal({  
                    title: '房间已有租户',  
                    content: `该房间已有${tenants.length}位租户,是否查看?`,  
                    success: (res) => {  
                        if (res.confirm) {  
                            uni.navigateTo({  
                                url: `/pages/rental/tenant-list?roomId=${room.id}`  
                            });  
                        }  
                    }  
                });  
            } else {  
                // 房间确实空置,可以添加租户  
                uni.navigateTo({  
                    url: `/pages/rental/tenant-add?roomId=${room.id}`  
                });  
            }  
        })  
        .catch(err => {  
            console.error('查询房间租户失败:', err);  
            uni.showToast({  
                title: '查询租户信息失败',  
                icon: 'error'  
            });  
        });  
}

设计原则:

关键业务逻辑,永远基于实际数据查询,而非缓存状态!

租户删除后的房间状态联动

// 删除租户时,自动更新房间状态  
performDelete() {  
    const roomId = this.tenant.room_id;  

    uni.deleteData('tenants', `id = ${this.tenantId}`)  
        .then(() => {  
            // 删除成功后,检查房间是否还有其他租户  
            return this.updateRoomStatus(roomId);  
        })  
        .then(() => {  
            uni.showToast({  
                title: '删除成功,即将返回',  
                icon: 'success'  
            });  
            setTimeout(() => {  
                uni.navigateBack();  
            }, 800);  
        });  
}  

// 智能更新房间状态  
updateRoomStatus(roomId) {  
    if (!roomId) return Promise.resolve();  

    // 查询该房间是否还有活跃租户  
    return uni.queryWithConditions('tenants', `room_id = ${roomId}`)  
        .then(tenants => {  
            const hasActiveTenant = tenants &&   
                tenants.some(tenant => tenant.status === 'active');  

            // 根据实际情况更新房间状态  
            const newStatus = hasActiveTenant ? 'rented' : 'vacant';  

            return uni.updateData('rooms', {  
                status: newStatus,  
                updated_at: new Date().toISOString()  
            }, `id = ${roomId}`);  
        })  
        .then(() => {  
            console.log('房间状态更新成功');  
        })  
        .catch(err => {  
            console.error('房间状态更新失败:', err);  
        });  
}

2.3 收租记录管理:自动化是关键

功能需求:

  • 记录每次收租情况
  • 区分租金和水电费
  • 支持多种支付方式
  • 自动创建财务记录

核心代码:收租记录保存与财务同步

saveRentCollection() {  
    if (!this.validateForm()) {  
        return;  
    }  

    const collectionData = {  
        tenant_id: parseInt(this.formData.tenant_id),  
        collection_date: this.formData.collection_date,  
        rent_amount: parseFloat(this.formData.rent_amount) || 0,  
        utility_amount: parseFloat(this.formData.utility_amount) || 0,  
        total_amount: this.totalAmount,  
        payment_method: this.formData.payment_method || 'cash',  
        status: this.formData.status || 'pending',  
        notes: this.formData.notes || null,  
        updated_at: new Date().toISOString()  
    };  

    // 保存收租记录  
    uni.insertData('rent_collections', collectionData)  
        .then(() => {  
            // 🔥 核心:如果收租状态为已收租,自动创建财务记录  
            if (collectionData.status === 'collected') {  
                return this.createFinancialRecord(collectionData);  
            }  
            return Promise.resolve();  
        })  
        .then(() => {  
            uni.showToast({  
                title: '收租记录添加成功',  
                icon: 'success'  
            });  
            setTimeout(() => {  
                uni.navigateBack();  
            }, 800);  
        })  
        .catch(err => {  
            console.error('收租记录保存失败:', err);  
            uni.showToast({  
                title: '保存失败,请重试',  
                icon: 'error'  
            });  
        });  
}  

// 创建对应的财务记录(核心同步逻辑)  
createFinancialRecord(collectionData) {  
    return new Promise((resolve, reject) => {  
        // 1. 查询租户信息  
        uni.queryWithConditions('tenants', `id = ${collectionData.tenant_id}`)  
            .then(tenantResults => {  
                if (!tenantResults || tenantResults.length === 0) {  
                    console.warn('未找到租户信息,跳过财务记录创建');  
                    return resolve();  
                }  

                const tenant = tenantResults[0];  

                // 2. 查询房间信息(获取property_id)  
                return uni.queryWithConditions('rooms', `id = ${tenant.room_id}`)  
                    .then(roomResults => {  
                        if (!roomResults || roomResults.length === 0) {  
                            console.warn('未找到房间信息,跳过财务记录创建');  
                            return resolve();  
                        }  

                        const room = roomResults[0];  

                        // 3. 创建财务记录  
                        const financialData = {  
                            property_id: room.property_id,  
                            room_id: room.id,  // 🔥 关键:关联房间  
                            type: 'income',  
                            category: 'rent',  
                            amount: collectionData.total_amount,  
                            description: `${tenant.name}租金收入 - ${room.room_number}号房`,  
                            record_date: collectionData.collection_date,  
                            created_at: new Date().toISOString()  
                        };  

                        return uni.insertData('financial_records', financialData);  
                    });  
            })  
            .then(() => {  
                console.log('财务记录创建成功');  
                resolve();  
            })  
            .catch(err => {  
                console.error('财务记录创建失败:', err);  
                resolve(); // 不阻断主流程  
            });  
    });  
}

设计亮点:

  1. 自动化同步:收租后自动创建财务记录,无需手动二次录入
  2. 数据溯源:通过description字段清晰标识收入来源
  3. 三级关联:租户→房间→房产,确保数据完整性
  4. 容错机制:财务记录失败不影响收租记录保存

功能模块三:财务管理 - 数据的统一中心

3.1 财务记录:收支一目了然

功能需求:

  • 记录所有收入支出
  • 支持房间级别关联
  • 分类管理(租金、押金、维修等)
  • 数据统计分析

踩坑记录4:财务记录与租金管理数据不同步

问题现象:

  • 在财务页面添加租金收入,租金管理中看不到
  • 在租金管理添加收入,有时财务记录中不显示

根本原因:
财务记录表缺少room_id字段,无法建立房间级别的数据关联。

解决方案:增强数据库结构

// 1. 修改financial_records表结构,添加room_id字段  
let columnList = [  
    { 'name': 'id', 'type': 'integer', 'nullable': false, 'primary': true },  
    { 'name': 'property_id', 'type': 'integer', 'nullable': true },  
    { 'name': 'room_id', 'type': 'integer', 'nullable': true },  // 🔥 新增  
    { 'name': 'type', 'type': 'text', 'nullable': false },  
    { 'name': 'category', 'type': 'text', 'nullable': false },  
    { 'name': 'amount', 'type': 'real', 'nullable': false },  
    { 'name': 'description', 'type': 'text', 'nullable': true },  
    { 'name': 'record_date', 'type': 'text', 'nullable': false },  
    { 'name': 'created_at', 'type': 'text', 'nullable': true }  
];  

// 2. 修改数据转换函数  
function convertResultSetToFinancialArray(resultSet) {  
    // ...  
    record.property_id = resultSet.getString(resultSet.getColumnIndex('property_id'));  
    record.room_id = resultSet.getString(resultSet.getColumnIndex('room_id')); // 🔥 新增  
    record.amount = resultSet.getDouble(resultSet.getColumnIndex('amount')); // 🔥 改为getDouble  
    // ...  
}

功能增强:财务记录添加房间选择

<template>  
    <!-- 房产选择 -->  
    <view class="field-group">  
        <text class="field-label">所属房产 *</text>  
        <view @click="showPropertyPicker">  
            <text>{{ formData.property_address || '请选择房产' }}</text>  
        </view>  
    </view>  

    <!-- 🔥 新增:房间选择(选择房产后动态显示) -->  
    <view class="field-group"   
          v-if="formData.property_id && roomList.length > 0">  
        <text class="field-label">所属房间</text>  
        <view @click="showRoomPicker">  
            <text>{{ formData.room_number ?   
                `${formData.room_number}号房` : '请选择房间(可选)' }}</text>  
        </view>  
    </view>  
</template>  

<script>  
export default {  
    data() {  
        return {  
            formData: {  
                property_id: null,  
                room_id: null,      // 🔥 新增  
                room_number: '',    // 🔥 新增  
                // ...  
            },  
            roomList: []           // 🔥 新增  
        }  
    },  

    methods: {  
        // 房产选择后,自动加载房间列表  
        handlePropertySelected(property) {  
            this.formData.property_id = property.id;  
            this.formData.property_address = property.address;  

            // 清空之前的房间选择  
            this.formData.room_id = null;  
            this.formData.room_number = '';  

            // 🔥 加载该房产的房间列表  
            this.loadRoomList(property.id);  
        },  

        loadRoomList(propertyId) {  
            if (!propertyId) {  
                this.roomList = [];  
                return;  
            }  

            uni.queryWithConditions('rooms', `property_id = ${propertyId}`)  
                .then(results => {  
                    console.log('房间列表查询成功:', results?.length || 0, '个房间');  
                    this.roomList = results || [];  
                })  
                .catch(err => {  
                    console.error('房间列表查询失败:', err);  
                    this.roomList = [];  
                });  
        },  

        showRoomPicker() {  
            if (this.roomList.length === 0) {  
                uni.showToast({  
                    title: '该房产暂无房间',  
                    icon: 'none'  
                });  
                return;  
            }  

            const roomNameList = this.roomList.map(room =>   
                `${room.room_number}号房 (${room.monthly_rent}元/月)`);  

            uni.showActionSheet({  
                itemList: roomNameList,  
                success: (res) => {  
                    const selectedRoom = this.roomList[res.tapIndex];  
                    this.formData.room_id = selectedRoom.id;  
                    this.formData.room_number = selectedRoom.room_number;  
                }  
            });  
        }  
    }  
}  
</script>

功能模块四:数据统计 - 让数字说话

4.1 首页数据总览:一眼看懂经营状况

功能需求:

  • 总资产价值展示
  • 本月收益分析
  • 租赁管理概览
  • 快捷操作入口

核心代码:营业收入计算

踩坑记录5:主页营业收入不包含财务记录

问题现象:
在财务页面添加的收入,主页营业收入不显示。

错误逻辑:分离计算

// ❌ 错误:分离计算导致遗漏  
monthlyTotalIncome() {  
    return this.monthlyRentIncome + this.monthlyNonRentIncome;  
}  

monthlyRentIncome() {  
    // 只从rent_collections表计算  
    return this.rentCollections  
        .filter(r => r.status === 'collected')  
        .reduce((sum, r) => sum + parseFloat(r.total_amount || 0), 0);  
}  

monthlyNonRentIncome() {  
    // 从financial_records表计算,但排除了租金  
    return this.getMonthlyRecords('income')  
        .filter(r => r.category !== 'rent')  
        .reduce((sum, r) => sum + parseFloat(r.amount || 0), 0);  
}

问题分析:

  • 直接在财务页面添加的租金收入被排除了
  • 数据来源不统一,导致统计不完整

正确方案:统一数据源

// ✅ 正确:所有收入统一从financial_records计算  
monthlyTotalIncome() {  
    return this.monthlyIncome; // 统一数据源  
}  

monthlyIncome() {  
    // 财务记录中当月所有收入之和  
    return this.getMonthlyRecords('income').reduce((sum, record) => {  
        return sum + parseFloat(record.amount || 0);  
    }, 0);  
}  

getMonthlyRecords(type) {  
    const now = new Date();  
    const currentMonth = now.toISOString().slice(0, 7); // YYYY-MM  

    return this.financialRecords.filter(record =>  
        record.type === type &&  
        record.record_date &&  
        record.record_date.startsWith(currentMonth)  
    );  
}

架构优化总结:

  • ✅ 财务记录表是唯一的收入统计数据源
  • ✅ 收租记录表仅用于收租流程管理
  • ✅ 收租记录自动创建财务记录,实现数据同步
  • ✅ 统计计算全部基于financial_records表,避免遗漏

4.2 统计分析页:深度数据洞察

功能需求:

  • 房产收益排行
  • 支出分析
  • 租赁分析(出租率、租金收入占比)
  • 租户分析

核心代码:出租率计算

computed: {  
    // 总房间数  
    totalRooms() {  
        return this.roomList.length;  
    },  

    // 已出租房间数  
    rentedRooms() {  
        return this.roomList.filter(room => room.status === 'rented').length;  
    },  

    // 房间出租率  
    roomOccupancyRate() {  
        if (this.totalRooms === 0) return 0;  
        return ((this.rentedRooms / this.totalRooms) * 100).toFixed(1);  
    },  

    // 租金收入占总收入比例  
    rentIncomePercentage() {  
        if (this.totalIncomeAmount === 0) return 0;  
        return ((this.rentIncomeAmount / this.totalIncomeAmount) * 100).toFixed(1);  
    },  

    // 平均租金水平  
    averageMonthlyRent() {  
        if (this.roomList.length === 0) return 0;  
        const totalRent = this.roomList.reduce((sum, room) =>   
            sum + parseFloat(room.monthly_rent || 0), 0);  
        return (totalRent / this.roomList.length).toFixed(0);  
    }  
}

🔧 HarmonyOS适配的三大挑战与解决方案 {#harmonyos适配}

挑战一:Picker组件的兼容性问题

问题场景:
在租金记录页面,选择租户时使用<picker>组件,当有多个租户时点击无响应。

问题代码:

<!-- ❌ 在HarmonyOS上有问题 -->  
<picker :range="tenantList"   
        range-key="name"  
        @change="onTenantChange">  
    <view>{{ selectedTenant?.name || '请选择租户' }}</view>  
</picker>

根本原因:
HarmonyOS的picker组件在动态数据绑定和复杂对象数组处理上存在兼容性问题。

解决方案:使用uni.showActionSheet替代

// ✅ 完美兼容的方案  
showTenantPicker() {  
    if (this.tenantList.length === 0) {  
        uni.showToast({  
            title: '暂无租户',  
            icon: 'none'  
        });  
        return;  
    }  

    const tenantNames = this.tenantList.map(t => t.name);  

    uni.showActionSheet({  
        itemList: tenantNames,  
        success: (res) => {  
            if (!res.cancel) {  
                const selectedTenant = this.tenantList[res.tapIndex];  
                this.formData.tenant_id = selectedTenant.id;  
                this.selectedTenantText = selectedTenant.name;  
                this.selectedTenant = selectedTenant;  

                uni.showToast({  
                    title: `已选择: ${selectedTenant.name}`,  
                    icon: 'success'  
                });  
            }  
        },  
        fail: (err) => {  
            console.log('用户取消选择或出错:', err);  
        }  
    });  
}

HTML改造:

<!-- ✅ 使用点击事件触发 -->  
<view class="picker-wrapper" @click="showTenantPicker">  
    <text class="picker-value">  
        {{ selectedTenantText || '请选择租户' }}  
    </text>  
    <text class="picker-arrow">👤</text>  
</view>
优势对比: 特性 Picker组件 ActionSheet
兼容性 ⚠️ 有问题 ✅ 完美
用户体验 一般 ✅ 原生感强
自定义 受限 ✅ 灵活
调试难度 较难 简单

经验总结:

在HarmonyOS开发中,涉及复杂数据的选择器,优先考虑uni.showActionSheet或自定义组件。

挑战二:ResultSet数据类型转换陷阱

问题场景:
总资产计算时,100万+200万+300万 = "100200300"

问题代码:

// ❌ 错误:所有字段都用getString  
function convertResultSetToFinancialArray(resultSet) {  
    const result = [];  
    resultSet.goToFirstRow();  

    for (let i = 0; i < count; i++) {  
        let record = {};  
        record.id = resultSet.getString(resultSet.getColumnIndex('id'));  
        record.property_id = resultSet.getString(resultSet.getColumnIndex('property_id'));  
        record.amount = resultSet.getString(resultSet.getColumnIndex('amount')); // ❌ 金额变成字符串  
        // ...  
        result[i] = record;  
        resultSet.goToNextRow();  
    }  
    return result;  
}

问题分析:

// JavaScript的+操作符行为  
"100" + "200" + "300" = "100200300"  // ❌ 字符串拼接  
100 + 200 + 300 = 600                 // ✅ 数值相加

正确方案:严格区分数据类型

// ✅ 正确:根据字段类型选择方法  
function convertResultSetToFinancialArray(resultSet) {  
    const result = [];  
    resultSet.goToFirstRow();  

    for (let i = 0; i < count; i++) {  
        let record = {};  
        record.id = resultSet.getString(resultSet.getColumnIndex('id'));          // 文本  
        record.property_id = resultSet.getString(resultSet.getColumnIndex('property_id')); // ID  
        record.room_id = resultSet.getString(resultSet.getColumnIndex('room_id'));         // ID  
        record.type = resultSet.getString(resultSet.getColumnIndex('type'));              // 文本  
        record.category = resultSet.getString(resultSet.getColumnIndex('category'));      // 文本  
        record.amount = resultSet.getDouble(resultSet.getColumnIndex('amount'));          // ✅ 数值  
        record.description = resultSet.getString(resultSet.getColumnIndex('description'));// 文本  
        record.record_date = resultSet.getString(resultSet.getColumnIndex('record_date'));// 日期  
        record.created_at = resultSet.getString(resultSet.getColumnIndex('created_at'));  // 日期  

        result[i] = record;  
        resultSet.goToNextRow();  
    }  
    return result;  
}

最佳实践规范:

// ResultSet数据类型选择指南  
const TYPE_MAPPING = {  
    // 文本类型  
    'text': 'getString',  
    'varchar': 'getString',  

    // 数值类型  
    'integer': 'getLong',     // 整数  
    'real': 'getDouble',      // 小数  
    'numeric': 'getDouble',   // 数值  

    // 特殊类型  
    'blob': 'getBlob',        // 二进制  
    'boolean': 'getLong',     // 布尔值(0/1)  

    // 日期类型(通常存为text)  
    'datetime': 'getString',  // 日期时间  
    'date': 'getString'       // 日期  
};

调试技巧:

// 添加类型检查日志  
computed: {  
    totalValue() {  
        const total = this.propertyList.reduce((sum, item) => {  
            const value = parseFloat(item.current_value) || 0;  

            // 🔥 调试日志  
            console.log(`房产: ${item.address}`);  
            console.log(`  current_value: ${item.current_value} (${typeof item.current_value})`);  
            console.log(`  转换后: ${value} (${typeof value})`);  

            return sum + value;  
        }, 0);  

        console.log(`总资产: ${total} (${typeof total})`);  
        return total;  
    }  
}

挑战三:生命周期与数据初始化

问题场景:
收租记录列表页面不停打印"收租记录表创建成功",数据加载重复执行。

问题代码:

// ❌ 错误:onLoad和onShow都初始化表  
export default {  
    onLoad(options) {  
        this.initTable();  // 创建表  
        this.loadData();   // 加载数据  
    },  

    onShow() {  
        this.initTable();  // 又创建一次表!  
        this.loadData();   // 又加载一次数据!  
    }  
}

问题分析:

  • 每次页面显示都重新初始化表
  • 导致重复的数据库操作
  • 日志输出混乱,影响调试

正确方案:明确生命周期职责

// ✅ 正确:职责分离  
export default {  
    data() {  
        return {  
            isTableInit: false,  // 表初始化标记  
            propertyId: null,  
            dataList: []  
        }  
    },  

    onLoad(options) {  
        // 1. 获取路由参数  
        this.propertyId = options.propertyId;  

        // 2. 首次加载:初始化表 + 加载数据  
        this.initializeOnce();  
    },  

    onShow() {  
        // 3. 后续显示:仅刷新数据  
        if (this.isTableInit) {  
            this.refreshData();  
        }  
    },  

    methods: {  
        // 首次初始化(只执行一次)  
        async initializeOnce() {  
            if (this.isTableInit) return;  

            try {  
                // 初始化表结构  
                await this.initTable();  
                this.isTableInit = true;  
                console.log('表初始化完成');  

                // 加载初始数据  
                await this.loadData();  
            } catch (err) {  
                console.error('初始化失败:', err);  
            }  
        },  

        // 刷新数据(不重新初始化表)  
        async refreshData() {  
            try {  
                await this.loadDataList();  
                console.log('数据刷新完成');  
            } catch (err) {  
                console.error('数据刷新失败:', err);  
            }  
        },  

        // 初始化表结构  
        initTable() {  
            return uni.createTable('rent_collections', columnList)  
                .then(() => {  
                    console.log('收租记录表创建成功');  
                })  
                .catch(err => {  
                    // createTable会在表已存在时报错,这是正常的  
                    console.log('表已存在或创建失败:', err);  
                });  
        },  

        // 加载完整数据  
        async loadData() {  
            await Promise.all([  
                this.loadTenantMap(),    // 先加载依赖数据  
                this.loadRoomMap()  
            ]);  
            await this.loadDataList();   // 再加载主数据  
        },  

        // 仅加载列表数据  
        loadDataList() {  
            return uni.queryWithConditions('rent_collections', '')  
                .then(results => {  
                    this.dataList = results || [];  
                })  
                .catch(err => {  
                    console.error('数据加载失败:', err);  
                    this.dataList = [];  
                });  
        }  
    }  
}

生命周期最佳实践:

页面首次加载 → onLoad  
    ├─ 初始化表结构 (once)  
    ├─ 加载依赖数据 (once)  
    └─ 加载主数据  

页面再次显示 → onShow    
    └─ 仅刷新数据(不重新初始化)

🔄 数据同步机制:让数据流动起来 {#数据同步}

数据同步是整个系统的核心挑战。我设计了三层同步机制:

同步层级一:收租记录 → 财务记录

触发条件: 收租状态为"已收租"

// 自动同步流程  
收租记录保存 (status='collected')  
    ↓  
查询租户信息 (获取租户名称)  
    ↓  
查询房间信息 (获取房间号、房产ID)  
    ↓    
创建财务记录 (income, category='rent')  
    ↓  
完成同步

同步层级二:租户状态 → 房间状态

触发条件: 添加/删除租户,租户状态变更

// 状态联动逻辑  
租户操作完成  
    ↓  
查询该房间所有租户  
    ↓  
检查是否存在活跃租户 (status='active')  
    ↓  
更新房间状态  
    ├─ 有活跃租户 → status='rented'  
    └─ 无活跃租户 → status='vacant'

同步层级三:房间选择 → 数据联动

触发条件: 财务记录选择房产

// 级联加载流程  
选择房产  
    ↓  
清空之前的房间选择  
    ↓  
查询该房产的所有房间  
    ↓  
展示房间选择器(可选)  
    ↓  
选择房间后关联room_id

⚡ 性能优化:从卡顿到丝滑 {#性能优化}

优化一:Computed Property 缓存计算

问题: 在模板中直接调用方法导致重复计算

<!-- ❌ 错误:每次渲染都重新计算 -->  
<text>{{ getTotalAssets() }}</text>  
<text>{{ getTotalAssets() }}</text>  <!-- 又计算一次 -->

优化: 使用计算属性缓存结果

// ✅ 优化:使用computed缓存  
computed: {  
    totalAssets() {  
        return this.propertyList.reduce((sum, p) =>   
            sum + (parseFloat(p.current_value) || 0), 0);  
    }  
}
<!-- ✅ 多次使用,只计算一次 -->  
<text>{{ totalAssets }}</text>  
<text>{{ totalAssets }}</text>  <!-- 使用缓存值 -->

优化二:数据加载策略

问题: 首页加载慢,等待时间长

原因分析:

// ❌ 串行加载,总耗时 = 每个接口耗时之和  
loadData() {  
    this.loadProperties();      // 100ms  
    this.loadFinancials();      // 150ms  
    this.loadRooms();           // 120ms  
    this.loadTenants();         // 100ms  
    // 总耗时: 470ms  
}

优化方案: 并行加载

// ✅ 并行加载,总耗时 = 最慢的接口耗时  
async loadData() {  
    try {  
        await Promise.all([  
            this.loadProperties(),   // ┐  
            this.loadFinancials(),   // ├─ 并行执行  
            this.loadRooms(),        // ┤  
            this.loadTenants()       // ┘  
        ]);  
        // 总耗时: 150ms(最慢的接口)  

        // 依赖数据的后续处理  
        this.processData();  
    } catch (err) {  
        console.error('数据加载失败:', err);  
    }  
}

性能对比:

  • 串行加载:470ms
  • 并行加载:150ms
  • 性能提升:68% 🚀

优化三:避免不必要的数据库操作

问题: onShow时重复初始化表

优化: 使用标志位控制

data() {  
    return {  
        isTableInit: false  // 🔥 标志位  
    }  
},  

methods: {  
    initTable() {  
        if (this.isTableInit) {  
            console.log('表已初始化,跳过');  
            return Promise.resolve();  
        }  

        return uni.createTable(...)  
            .then(() => {  
                this.isTableInit = true;  
                console.log('表初始化完成');  
            });  
    }  
}

优化四:列表渲染优化

使用v-for时添加唯一key

<!-- ❌ 没有key,Vue无法高效更新 -->  
<view v-for="item in list">  
    {{ item.name }}  
</view>  

<!-- ✅ 有key,Vue可以精确复用 -->  
<view v-for="item in list" :key="item.id">  
    {{ item.name }}  
</view>

避免在v-for中使用v-if

<!-- ❌ 性能差:先渲染再过滤 -->  
<view v-for="item in list" :key="item.id" v-if="item.status === 'active'">  
    {{ item.name }}  
</view>  

<!-- ✅ 性能好:先过滤再渲染 -->  
<view v-for="item in activeList" :key="item.id">  
    {{ item.name }}  
</view>  

<script>  
computed: {  
    activeList() {  
        return this.list.filter(item => item.status === 'active');  
    }  
}  
</script>

🎨 UI/UX设计:商务风格的视觉语言 {#uiux设计}

设计理念

作为一款专业的房产管理工具,UI设计遵循以下原则:

  1. 商务专业:渐变、阴影、卡片化设计
  2. 信息清晰:合理的视觉层级和间距
  3. 操作便捷:大按钮、明确反馈
  4. 数据可视:数字大、图标辅助

SCSS变量系统

// uni.scss - 全局设计token  

// ===== 颜色系统 =====  
// 主色  
$primary-blue: #3B82F6;  
$primary-dark: #2563EB;  

// 功能色  
$income-text: #10B981;      // 收入绿  
$income-bg: rgba(16, 185, 129, 0.1);  
$expense-text: #EF4444;     // 支出红  
$expense-bg: rgba(239, 68, 68, 0.1);  

// 文本色  
$text-primary: #1E293B;     // 主要文本  
$text-secondary: #64748B;   // 次要文本  
$text-tertiary: #94A3B8;    // 辅助文本  

// 背景色  
$bg-primary: #FFFFFF;  
$bg-secondary: #F8FAFC;  

// ===== 间距系统 =====  
$spacing-xs: 8rpx;  
$spacing-sm: 12rpx;  
$spacing-md: 16rpx;  
$spacing-lg: 24rpx;  
$spacing-xl: 32rpx;  
$spacing-xxl: 48rpx;  

// ===== 圆角系统 =====  
$radius-small: 8rpx;  
$radius-medium: 12rpx;  
$radius-large: 16rpx;  
$radius-pill: 999rpx;  

// ===== 阴影系统 =====  
$shadow-card: 0 8rpx 32rpx rgba(15, 23, 42, 0.08),   
              0 4rpx 16rpx rgba(15, 23, 42, 0.04);  

// ===== 字体系统 =====  
$font-size-caption: 22rpx;    // 说明文字  
$font-size-body: 28rpx;       // 正文  
$font-size-subhead: 30rpx;    // 副标题  
$font-size-headline: 32rpx;   // 标题  
$font-size-title: 40rpx;      // 大标题

卡片设计模式

// 统一的卡片样式  
.info-card {  
    background: linear-gradient(135deg, #FFFFFF 0%, #F8F9FA 100%);  
    border-radius: $radius-large;  
    padding: $spacing-xl;  
    box-shadow: $shadow-card;  
    border: 1rpx solid rgba(255, 255, 255, 0.8);  
    transition: all 0.3s ease;  

    &:active {  
        transform: translateY(2rpx);  
        box-shadow: 0 4rpx 16rpx rgba(15, 23, 42, 0.06);  
    }  
}

数据展示设计

大数字设计:

<view class="data-showcase">  
    <text class="data-number">¥{{ formatLargeNumber(2380000) }}</text>  
    <text class="data-label">总资产价值</text>  
</view>  

<style lang="scss">  
.data-number {  
    font-size: 56rpx;      // 大号字体  
    font-weight: 700;      // 粗体  
    color: $text-primary;  
    line-height: 1.2;  
    letter-spacing: -0.5rpx; // 紧凑间距  
}  

.data-label {  
    font-size: $font-size-body;  
    color: $text-secondary;  
    margin-top: $spacing-xs;  
}  
</style>

收支颜色区分:

.income-color {  
    color: $income-text; // 绿色  
}  

.expense-color {  
    color: $expense-text; // 红色  
}

用户体验细节

1. 操作反馈优化

// 成功反馈  
uni.showToast({  
    title: '添加成功,即将返回',  // 明确告知用户  
    icon: 'success'  
});  

setTimeout(() => {  
    uni.navigateBack();  
}, 800);  // 800ms刚好够看到提示

2. 空状态设计

<view class="empty-state" v-if="dataList.length === 0">  
    <text class="empty-icon">🏠</text>  
    <text class="empty-title">暂无数据</text>  
    <text class="empty-subtitle">点击右上角按钮添加</text>  
</view>

3. 加载状态

<view class="loading-state" v-if="isLoading">  
    <text class="loading-icon">⏳</text>  
    <text class="loading-text">加载中...</text>  
</view>

🐛 踩坑经验:那些让我头秃的Bug {#踩坑经验}

Bug 1: 字符串拼接陷阱

现象: 100 + 200 + 300 = "100200300"

原因: 数据库返回字符串类型

解决: getDouble + parseFloat双保险

耗时: 2小时

Bug 2: Picker组件失效

现象: 选择租户时点击无反应

原因: HarmonyOS picker兼容问题

解决: 改用uni.showActionSheet

耗时: 3小时

Bug 3: 数据重复加载

现象: 不停打印"表创建成功"

原因: onLoad和onShow都初始化表

解决: 生命周期职责分离

耗时: 1小时

Bug 4: 房间状态误判

现象: 明明没租户却提示"已有租户"

原因: 依赖status字段而非实际数据

解决: 查询数据库确认

耗时: 1.5小时

Bug 5: 收入统计遗漏

现象: 财务页面添加的收入不计入统计

原因: 分离计算逻辑导致遗漏

解决: 统一数据源(financial_records)

耗时: 2小时

Bug 6: 收租日选择不全

现象: 只能选择部分日期

原因: Picker组件限制

解决: Grid布局自定义选择器

耗时: 4小时

总结:调试时间约占开发时间的30% 😅


🔨 开发工具与调试技巧 {#开发工具}

HBuilder X 实用功能

  1. 真机调试:实时预览,快速定位问题
  2. 代码提示:TypeScript类型提示
  3. 条件编译:处理平台差异
// #ifdef APP-HARMONY  
// HarmonyOS专属代码  
// #endif  

// #ifdef APP-IOS  
// iOS专属代码  
// #endif

调试技巧总结

1. Console.log大法

// 关键位置添加日志  
console.log('=== 数据加载开始 ===');  
console.log('查询条件:', condition);  
console.log('查询结果:', results);  
console.log('=== 数据加载完成 ===');

2. 类型检查

console.log(`金额: ${amount}, 类型: ${typeof amount}`);

3. 生命周期追踪

onLoad() { console.log('⬇️ onLoad'); }  
onShow() { console.log('👁️ onShow'); }  
onHide() { console.log('🙈 onHide'); }

4. 数据快照

console.log('数据快照:', JSON.stringify(data, null, 2));

🚀 未来规划与展望 {#未来规划}

v1.1.0 - 功能增强(开发中)

  • [ ] 合同到期提醒功能
  • [ ] 收租日提醒(推送通知)
  • [ ] 数据导出(Excel格式)
  • [ ] 云同步备份

v1.2.0 - 数据可视化(规划中)

  • [ ] 收益趋势图表(折线图)
  • [ ] 房产价值变化(柱状图)
  • [ ] 租客分布分析(饼图)
  • [ ] 年度投资报告

v1.3.0 - 智能化(未来愿景)

  • [ ] AI租金定价建议
  • [ ] 空置期预测
  • [ ] 智能收租提醒
  • [ ] 租客画像分析

v2.0.0 - 多平台扩展

  • [ ] iOS版本发布
  • [ ] Android版本发布
  • [ ] Web管理后台
  • [ ] 数据云同步

💭 总结与感悟 {#总结}

开发历程回顾

开发阶段:

阶段一:需求分析 + 技术选型  
阶段二:数据库设计 + 架构搭建  
阶段三:核心功能开发(房产、租赁)  
阶段四:财务统计模块  
阶段五:HarmonyOS适配调试  
阶段六:UI优化 + Bug修复

代码统计:

  • 总代码量:10000+ 行
  • 页面数量:18个
  • 组件数量:30+个
  • 数据表:5个核心表

技术收获

1. 深入理解HarmonyOS生态

从零开始学习HarmonyOS的relationalStore数据库,从陌生到熟悉,从踩坑到总结最佳实践。

2. 掌握uni-app x开发

  • Composition API的使用
  • 生命周期管理
  • 数据响应式原理
  • 跨平台适配技巧

3. 数据库设计能力

学会设计合理的数据关联关系,理解数据一致性的重要性。

4. 性能优化思维

从用户体验出发,优化加载速度、减少重复计算、提升交互流畅度。

思维成长

1. 用户思维

不仅是开发者,更是用户。每个功能都从实际需求出发,而非为了技术而技术。

2. 架构思维

先设计数据模型,再设计功能模块。好的架构是成功的一半。

3. 工程思维

代码质量、可维护性、可扩展性同样重要。写代码不仅是为了实现功能,更是为了长期维护。

4. 问题解决思维

遇到问题不慌张,系统分析、逐步排查、总结经验。

给开发者的建议

1. 技术选型要慎重

选择适合自己的技术栈,不要盲目追新。uni-app x + HarmonyOS对我来说是最佳选择。

2. 数据库设计要花时间

数据模型设计好了,后续开发事半功倍。不要急于写代码。

3. 用户体验无小事

800ms的延迟优化、身份证脱敏、空状态提示...这些细节决定产品质量。

4. 持续迭代比一次完美更重要

先实现MVP,再逐步完善。不要追求一次性做到完美。

5. 记录踩坑经验

每个Bug都是成长的机会。记录下来,帮助自己也帮助别人。

写在最后

这个项目从构思到完成,经历了无数次的推翻重来、代码重构、问题调试。但当看到一个功能完整、数据准确、体验流畅的应用在HarmonyOS上运行时,所有的努力都值得了。

uni-app x + HarmonyOS Next 的组合,让我既能享受跨平台开发的便利,又能获得原生应用的性能。虽然过程中遇到了很多适配问题,但每个问题的解决都让我对HarmonyOS生态有了更深的理解。

HarmonyOS代表着未来,提前布局就是抓住机遇。感谢DCloud提供的强大框架,感谢HarmonyOS提供的优秀平台。

让我们一起,星光不负,码向未来! 🚀

继续阅读 »

📖 目录

项目背景:从房东的痛点说起 {#项目背景}

故事的开始

2024年初,我在苏州购入了第二套投资房产,准备出租。作为一个程序员房东,我自然而然地想用科技手段来管理我的资产。然而现实很骨感:

痛点一:信息管理混乱

  • Excel表格记录房产信息,版本管理困难
  • 租户资料散落在微信聊天记录里
  • 合同文档放在电脑、手机、云盘多个地方
  • 想查个租客电话号码都要翻半天

痛点二:财务统计困难

  • 每月收租日期记不住,经常漏收
  • 水电费、物业费、维修费记录混乱
  • 年底算投资回报率要拿出计算器算半天
  • 不知道哪套房子最赚钱,哪套在亏损

痛点三:决策缺乏数据支撑

  • 出租率是多少?不清楚
  • 平均租金水平?凭感觉
  • 租客流失率?没统计过
  • 要不要继续投资?拍脑袋决定

市场调研

我研究了市面上的房产管理软件:

结论:没有一款软件能完美满足我的需求!

需求分析

作为开发者,我决定自己动手。经过两周的需求梳理,我列出了核心功能:

必备功能(MVP):

  • ✅ 房产档案管理(地址、面积、价格)
  • ✅ 房间状态管理(空置/已租/维修)
  • ✅ 租户信息管理(联系方式、合同期限)
  • ✅ 收租记录管理(租金、水电费)
  • ✅ 财务收支记录(收入、支出分类)
  • ✅ 数据统计分析(出租率、投资回报率)

进阶功能(Nice to have):

  • 🔄 收租日提醒
  • 📊 数据可视化图表
  • 📱 移动端随时查看
  • 🔒 数据本地存储(隐私安全)
  • 🎨 专业的商务风格UI

目标用户画像

张先生,38岁,IT行业,苏州

  • 持有3套房产用于投资
  • 工作繁忙,希望高效管理
  • 重视数据隐私,不愿上传云端
  • 需要专业的数据支撑投资决策
  • 🛠️ 为什么选择uni-app x + HarmonyOS {#技术选型}

技术选型的纠结过程

作为一个全栈开发者,我面临着技术选型的"幸福烦恼"。让我列出当时的思考过程:

方案一:原生开发(HarmonyOS + Swift/Kotlin)

优势:

  • ✅ 性能最佳,用户体验极致
  • ✅ 可以使用平台最新特性
  • ✅ 没有框架限制

劣势:

  • ❌ 开发成本高(三端分别开发)
  • ❌ 维护困难(代码量 x3)
  • ❌ 学习成本高(多种语言)
  • ❌ 时间成本无法接受(个人项目)

结论: 🚫 虽然性能最好,但对个人开发者不现实

方案二:React Native

优势:

  • ✅ 生态成熟,社区活跃
  • ✅ 组件库丰富
  • ✅ 跨平台能力强

劣势:

  • ❌ 对HarmonyOS支持有限
  • ❌ 包体积较大(基础包10MB+)
  • ❌ 性能相对较差
  • ❌ 需要学习React生态

结论: 🤔 可行,但HarmonyOS支持是短板

方案三:Flutter

优势:

  • ✅ 性能优秀(接近原生)
  • ✅ UI组件精美
  • ✅ 热重载提升效率

劣势:

  • ❌ Dart语言学习成本
  • ❌ 包体积大(基础包15MB+)
  • ❌ HarmonyOS支持需要额外适配
  • ❌ 对前端开发者不够友好

结论: 🤔 性能好,但学习成本高

方案四:uni-app x(最终选择)✨

优势:

  • 原生性能:基于原生渲染,性能接近原生
  • TypeScript支持:类型安全,开发体验好
  • 一次开发,三端运行:iOS、Android、HarmonyOS
  • Vue生态:前端开发者无缝上手
  • 官方HarmonyOS支持:DCloud官方适配
  • 包体积小:基础包仅5MB
  • 开发效率高:热重载+组件化

劣势:

  • ⚠️ 生态相对较新(但在快速完善)
  • ⚠️ 部分高级特性需要等待

结论: 🎉 综合考虑,uni-app x是最佳选择!

为什么HarmonyOS是未来趋势?

作为开发者,我深信HarmonyOS代表着移动生态的未来

  1. 国产化浪潮:信创政策推动,政企市场潜力巨大
  2. 技术创新:分布式能力、原子化服务让人眼前一亮
  3. 生态建设:华为全力推进,开发者红利期
  4. 市场份额:2024年国内份额已突破20%,增长迅猛
  5. 开发者友好:完善的文档、活跃的社区

我的判断:提前布局HarmonyOS,就是抓住下一个技术红利!

最终技术栈

经过一周的调研和demo测试,我确定了技术栈:

核心技术明细:

  • 前端框架:uni-app x (基于Vue 3)
  • 编程语言:TypeScript (主要) + UTS (平台特性)
  • 样式方案:SCSS + CSS Variables(主题系统)
  • 数据库:HarmonyOS relationalStore (SQLite)
  • 状态管理:Composition API + Reactive
  • 数据请求:uni.request + Promise
  • 本地存储:uni.storage (配置) + relationalStore (业务数据)
  • UI组件:uni-ui + 自定义组件库

    🏗️ 系统架构设计:四大模块协同作战 {#架构设计}

整体架构图

系统采用分层架构 + 模块化设计

核心数据模型(ER图)

数据关联关系是整个系统的灵魂! 我花了三天时间设计数据模型,确保数据关联的合理性和扩展性。

设计亮点:

  1. property_id + room_id 双重关联:既支持房产级统计,也支持房间级追踪
  2. 自动同步机制:收租记录→财务记录的自动创建
  3. 状态联动:租户状态→房间状态的自动更新
  4. 数据溯源:通过description字段记录数据来源

项目目录结构

房产投资管理系统/  
│  
├── pages/                          # 页面目录(18个页面)  
│   ├── index/                      # 首页模块  
│   │   └── index.vue              # 首页(数据总览)  
│   │  
│   ├── property/                   # 房产管理模块  
│   │   ├── list.vue               # 房产列表  
│   │   ├── add.vue                # 添加/编辑房产  
│   │   └── detail.vue             # 房产详情  
│   │  
│   ├── rental/                     # 租赁管理模块  
│   │   ├── room-list.vue          # 房间列表  
│   │   ├── room-add.vue           # 添加/编辑房间  
│   │   ├── room-detail.vue        # 房间详情  
│   │   ├── tenant-list.vue        # 租户列表  
│   │   ├── tenant-add.vue         # 添加/编辑租户  
│   │   ├── tenant-detail.vue      # 租户详情  
│   │   ├── rent-collection.vue    # 收租记录列表  
│   │   └── rent-add.vue           # 添加收租记录  
│   │  
│   ├── finance/                    # 财务管理模块  
│   │   ├── list.vue               # 财务记录列表  
│   │   └── add.vue                # 添加财务记录  
│   │  
│   └── stats/                      # 统计分析模块  
│       └── index.vue              # 数据统计页  
│  
├── uni_modules/                    # 插件模块  
│   └── test-relationalStore/      # 数据库封装  
│       └── utssdk/  
│           └── app-harmony/  
│               └── index.uts      # HarmonyOS数据库适配  
│  
├── static/                         # 静态资源  
│   ├── logo.png                   # 应用图标  
│   ├── 我的资产.png                # Tab图标  
│   ├── 房产.png  
│   ├── 财务.png  
│   └── 统计.png  
│  
├── uni.scss                        # 全局样式变量  
├── App.vue                         # 应用入口  
├── pages.json                      # 页面配置  
├── manifest.json                   # 应用配置  
└── project_analysis.md             # 项目开发文档

设计原则:

  • 模块化:按功能模块划分目录
  • 复用性:list-add-detail的标准页面结构
  • 可维护性:清晰的命名和目录层级

💎 核心功能实现:18个页面的血泪史 {#核心功能}

功能模块一:房产管理 - 资产的数字化档案

1.1 房产列表页(pages/property/list.vue)

功能需求:

  • 展示所有房产列表
  • 显示总资产价值
  • 支持房产的增删改查

核心代码:总资产价值计算

// 计算总资产价值  
computed: {  
    totalValue() {  
        const total = this.propertyList.reduce((sum, item) => {  
            // 优先使用当前估值,否则使用购买价格  
            const currentValue = parseFloat(item.current_value) || 0;  
            const purchasePrice = parseFloat(item.purchase_price) || 0;  
            const propertyValue = currentValue || purchasePrice;  

            console.log(`房产 ${item.address}:   
                current_value=${item.current_value}(${typeof item.current_value}),   
                purchase_price=${item.purchase_price}(${typeof item.purchase_price}),   
                使用价值=${propertyValue}`);  

            return sum + propertyValue;  
        }, 0);  

        console.log('总资产价值计算结果:', total);  
        return total;  
    }  
}

踩坑记录1:资产价值计算错误

问题现象:
总资产显示为"100200300"而不是"600"(三套房各200万)

原因分析:
数据库查询时用getString获取价格字段,导致数值以字符串形式返回。JavaScript的+操作符对字符串执行拼接而非数学加法。

解决方案:
在数据库转换函数中,数值类型字段使用getDouble而非getString

// ❌ 错误写法  
function convertResultSetToPropertyArray(resultSet) {  
    record.purchase_price = resultSet.getString(  
        resultSet.getColumnIndex('purchase_price')  
    ); // 返回字符串  
}  

// ✅ 正确写法  
function convertResultSetToPropertyArray(resultSet) {  
    record.purchase_price = resultSet.getDouble(  
        resultSet.getColumnIndex('purchase_price')  
    ); // 返回数值  
}

经验总结:

  • ✅ 整数/小数字段:用getDouble()
  • ✅ 文本/ID字段:用getString()
  • ✅ 在computed中加console.log调试
  • ✅ 前端再用parseFloat兜底确保类型正确

1.2 添加房产页(pages/property/add.vue)

功能需求:

  • 录入房产基本信息
  • 表单验证
  • 数据持久化

核心代码:表单验证

validateForm() {  
    // 地址验证  
    if (!this.formData.address || this.formData.address.trim() === '') {  
        uni.showToast({  
            title: '请输入房产地址',  
            icon: 'none'  
        });  
        return false;  
    }  

    // 面积验证  
    if (!this.formData.area || parseFloat(this.formData.area) <= 0) {  
        uni.showToast({  
            title: '请输入有效的房产面积',  
            icon: 'none'  
        });  
        return false;  
    }  

    // 价格验证  
    if (!this.formData.purchase_price ||   
        parseFloat(this.formData.purchase_price) <= 0) {  
        uni.showToast({  
            title: '请输入有效的购买价格',  
            icon: 'none'  
        });  
        return false;  
    }  

    return true;  
}

设计亮点:

  • 实时验证:输入时即时反馈
  • 友好提示:明确告知用户错误原因
  • 数据类型转换:字符串→数值的安全转换

功能模块二:租赁管理 - 最复杂的业务逻辑

2.1 房间管理:7种房型 x 4种付款方式 = 28种组合

功能需求:

  • 支持7种房型(单间、一室一厅、两室一厅...)
  • 支持4种付款方式(押一付一、押一付三...)
  • 自定义每月收租日(1-31号)
  • 房间状态管理(空置/已租/维修中)

核心代码:房间类型映射

// 房型映射  
getRoomTypeText(type) {  
    const typeMap = {  
        'single': '单间',  
        '1b1b': '一室一厅',  
        '2b1b': '两室一厅',  
        '2b2b': '两室两厅',  
        '3b1b': '三室一厅',  
        '3b2b': '三室两厅',  
        '4b2b': '四室两厅'  
    };  
    return typeMap[type] || type;  
}  

// 付款方式映射  
getPaymentModeText(mode) {  
    const modeMap = {  
        'monthly': '押一付一',  
        'quarterly': '押一付三',  
        'half_yearly': '半年付',  
        'yearly': '年付'  
    };  
    return modeMap[mode] || mode;  
}

踩坑记录2:收租日选择器无法显示全部31天

问题现象:
HarmonyOS的<picker>组件只能显示有限的选项,无法展示完整的31天选择。

解决方案:自定义Grid布局选择器

<template>  
    <!-- 自定义模态框 -->  
    <view class="rent-day-modal" v-if="showRentDayModal" @click="cancelRentDay">  
        <view class="modal-content" @click.stop>  
            <view class="modal-header">  
                <text class="modal-title">选择每月收租日</text>  
            </view>  

            <!-- 7x5 Grid布局显示31天 -->  
            <view class="day-grid">  
                <view class="day-item"   
                      v-for="day in 31"   
                      :key="day"  
                      :class="{ selected: selectedRentDay === day }"  
                      @click="selectRentDay(day)">  
                    <text class="day-text">{{ day }}</text>  
                </view>  
            </view>  

            <view class="modal-footer">  
                <button class="cancel-btn" @click="cancelRentDay">取消</button>  
                <button class="confirm-btn" @click="confirmRentDay">确定</button>  
            </view>  
        </view>  
    </view>  
</template>  

<style lang="scss">  
.day-grid {  
    display: grid;  
    grid-template-columns: repeat(7, 1fr); // 7列布局,展示31天  
    gap: 12rpx;  
    padding: 20rpx;  
}  

.day-item {  
    aspect-ratio: 1; // 保持正方形  
    display: flex;  
    align-items: center;  
    justify-content: center;  
    border-radius: 8rpx;  
    background: #F5F5F5;  
    transition: all 0.2s;  

    &.selected {  
        background: $primary-blue;  
        color: #FFFFFF;  
        transform: scale(1.1);  
    }  

    &:active {  
        opacity: 0.7;  
    }  
}  
</style>

经验总结:
当原生组件无法满足需求时,果断自定义实现。Grid布局非常适合日历、选择器等场景。

2.2 租户管理:隐私保护与数据联动

功能需求:

  • 租户信息录入(姓名、电话、身份证)
  • 身份证号脱敏显示
  • 租户状态与房间状态联动
  • 合同期限管理

核心代码:身份证号脱敏

// 身份证号脱敏处理  
maskIdCard(idCard) {  
    if (!idCard || idCard.length < 8) return idCard;  
    // 显示前4位和后4位,中间用*替代  
    const start = idCard.substring(0, 4);  
    const end = idCard.substring(idCard.length - 4);  
    const middle = '*'.repeat(idCard.length - 8);  
    return `${start}${middle}${end}`;  
}  

// 使用示例  
computed: {  
    maskedIdCard() {  
        return this.maskIdCard(this.tenant.id_card);  
        // 输入:510123199001011234  
        // 输出:5101**********1234  
    }  
}

踩坑记录3:租户状态判断不准确

问题现象:
主页快捷操作"添加租户",明明房间没有租户,却提示"房间已有租户"。

错误逻辑:依赖房间status字段

// ❌ 不准确的判断  
if (room.status === 'rented') {  
    uni.showToast({ title: '房间已有租户' });  
}

根本原因:
房间状态字段可能因为各种原因(删除租户时未更新、数据异常等)导致状态不准确。

正确方案:查询实际租户数据

// ✅ 准确的判断:查询数据库  
checkRoomTenants(room) {  
    uni.queryWithConditions('tenants',   
        `room_id = ${room.id} AND status = 'active'`)  
        .then(tenants => {  
            console.log(`房间 ${room.room_number} 的租户查询结果:`,   
                tenants?.length || 0, '个活跃租户');  

            if (tenants && tenants.length > 0) {  
                // 确实有租户  
                uni.showModal({  
                    title: '房间已有租户',  
                    content: `该房间已有${tenants.length}位租户,是否查看?`,  
                    success: (res) => {  
                        if (res.confirm) {  
                            uni.navigateTo({  
                                url: `/pages/rental/tenant-list?roomId=${room.id}`  
                            });  
                        }  
                    }  
                });  
            } else {  
                // 房间确实空置,可以添加租户  
                uni.navigateTo({  
                    url: `/pages/rental/tenant-add?roomId=${room.id}`  
                });  
            }  
        })  
        .catch(err => {  
            console.error('查询房间租户失败:', err);  
            uni.showToast({  
                title: '查询租户信息失败',  
                icon: 'error'  
            });  
        });  
}

设计原则:

关键业务逻辑,永远基于实际数据查询,而非缓存状态!

租户删除后的房间状态联动

// 删除租户时,自动更新房间状态  
performDelete() {  
    const roomId = this.tenant.room_id;  

    uni.deleteData('tenants', `id = ${this.tenantId}`)  
        .then(() => {  
            // 删除成功后,检查房间是否还有其他租户  
            return this.updateRoomStatus(roomId);  
        })  
        .then(() => {  
            uni.showToast({  
                title: '删除成功,即将返回',  
                icon: 'success'  
            });  
            setTimeout(() => {  
                uni.navigateBack();  
            }, 800);  
        });  
}  

// 智能更新房间状态  
updateRoomStatus(roomId) {  
    if (!roomId) return Promise.resolve();  

    // 查询该房间是否还有活跃租户  
    return uni.queryWithConditions('tenants', `room_id = ${roomId}`)  
        .then(tenants => {  
            const hasActiveTenant = tenants &&   
                tenants.some(tenant => tenant.status === 'active');  

            // 根据实际情况更新房间状态  
            const newStatus = hasActiveTenant ? 'rented' : 'vacant';  

            return uni.updateData('rooms', {  
                status: newStatus,  
                updated_at: new Date().toISOString()  
            }, `id = ${roomId}`);  
        })  
        .then(() => {  
            console.log('房间状态更新成功');  
        })  
        .catch(err => {  
            console.error('房间状态更新失败:', err);  
        });  
}

2.3 收租记录管理:自动化是关键

功能需求:

  • 记录每次收租情况
  • 区分租金和水电费
  • 支持多种支付方式
  • 自动创建财务记录

核心代码:收租记录保存与财务同步

saveRentCollection() {  
    if (!this.validateForm()) {  
        return;  
    }  

    const collectionData = {  
        tenant_id: parseInt(this.formData.tenant_id),  
        collection_date: this.formData.collection_date,  
        rent_amount: parseFloat(this.formData.rent_amount) || 0,  
        utility_amount: parseFloat(this.formData.utility_amount) || 0,  
        total_amount: this.totalAmount,  
        payment_method: this.formData.payment_method || 'cash',  
        status: this.formData.status || 'pending',  
        notes: this.formData.notes || null,  
        updated_at: new Date().toISOString()  
    };  

    // 保存收租记录  
    uni.insertData('rent_collections', collectionData)  
        .then(() => {  
            // 🔥 核心:如果收租状态为已收租,自动创建财务记录  
            if (collectionData.status === 'collected') {  
                return this.createFinancialRecord(collectionData);  
            }  
            return Promise.resolve();  
        })  
        .then(() => {  
            uni.showToast({  
                title: '收租记录添加成功',  
                icon: 'success'  
            });  
            setTimeout(() => {  
                uni.navigateBack();  
            }, 800);  
        })  
        .catch(err => {  
            console.error('收租记录保存失败:', err);  
            uni.showToast({  
                title: '保存失败,请重试',  
                icon: 'error'  
            });  
        });  
}  

// 创建对应的财务记录(核心同步逻辑)  
createFinancialRecord(collectionData) {  
    return new Promise((resolve, reject) => {  
        // 1. 查询租户信息  
        uni.queryWithConditions('tenants', `id = ${collectionData.tenant_id}`)  
            .then(tenantResults => {  
                if (!tenantResults || tenantResults.length === 0) {  
                    console.warn('未找到租户信息,跳过财务记录创建');  
                    return resolve();  
                }  

                const tenant = tenantResults[0];  

                // 2. 查询房间信息(获取property_id)  
                return uni.queryWithConditions('rooms', `id = ${tenant.room_id}`)  
                    .then(roomResults => {  
                        if (!roomResults || roomResults.length === 0) {  
                            console.warn('未找到房间信息,跳过财务记录创建');  
                            return resolve();  
                        }  

                        const room = roomResults[0];  

                        // 3. 创建财务记录  
                        const financialData = {  
                            property_id: room.property_id,  
                            room_id: room.id,  // 🔥 关键:关联房间  
                            type: 'income',  
                            category: 'rent',  
                            amount: collectionData.total_amount,  
                            description: `${tenant.name}租金收入 - ${room.room_number}号房`,  
                            record_date: collectionData.collection_date,  
                            created_at: new Date().toISOString()  
                        };  

                        return uni.insertData('financial_records', financialData);  
                    });  
            })  
            .then(() => {  
                console.log('财务记录创建成功');  
                resolve();  
            })  
            .catch(err => {  
                console.error('财务记录创建失败:', err);  
                resolve(); // 不阻断主流程  
            });  
    });  
}

设计亮点:

  1. 自动化同步:收租后自动创建财务记录,无需手动二次录入
  2. 数据溯源:通过description字段清晰标识收入来源
  3. 三级关联:租户→房间→房产,确保数据完整性
  4. 容错机制:财务记录失败不影响收租记录保存

功能模块三:财务管理 - 数据的统一中心

3.1 财务记录:收支一目了然

功能需求:

  • 记录所有收入支出
  • 支持房间级别关联
  • 分类管理(租金、押金、维修等)
  • 数据统计分析

踩坑记录4:财务记录与租金管理数据不同步

问题现象:

  • 在财务页面添加租金收入,租金管理中看不到
  • 在租金管理添加收入,有时财务记录中不显示

根本原因:
财务记录表缺少room_id字段,无法建立房间级别的数据关联。

解决方案:增强数据库结构

// 1. 修改financial_records表结构,添加room_id字段  
let columnList = [  
    { 'name': 'id', 'type': 'integer', 'nullable': false, 'primary': true },  
    { 'name': 'property_id', 'type': 'integer', 'nullable': true },  
    { 'name': 'room_id', 'type': 'integer', 'nullable': true },  // 🔥 新增  
    { 'name': 'type', 'type': 'text', 'nullable': false },  
    { 'name': 'category', 'type': 'text', 'nullable': false },  
    { 'name': 'amount', 'type': 'real', 'nullable': false },  
    { 'name': 'description', 'type': 'text', 'nullable': true },  
    { 'name': 'record_date', 'type': 'text', 'nullable': false },  
    { 'name': 'created_at', 'type': 'text', 'nullable': true }  
];  

// 2. 修改数据转换函数  
function convertResultSetToFinancialArray(resultSet) {  
    // ...  
    record.property_id = resultSet.getString(resultSet.getColumnIndex('property_id'));  
    record.room_id = resultSet.getString(resultSet.getColumnIndex('room_id')); // 🔥 新增  
    record.amount = resultSet.getDouble(resultSet.getColumnIndex('amount')); // 🔥 改为getDouble  
    // ...  
}

功能增强:财务记录添加房间选择

<template>  
    <!-- 房产选择 -->  
    <view class="field-group">  
        <text class="field-label">所属房产 *</text>  
        <view @click="showPropertyPicker">  
            <text>{{ formData.property_address || '请选择房产' }}</text>  
        </view>  
    </view>  

    <!-- 🔥 新增:房间选择(选择房产后动态显示) -->  
    <view class="field-group"   
          v-if="formData.property_id && roomList.length > 0">  
        <text class="field-label">所属房间</text>  
        <view @click="showRoomPicker">  
            <text>{{ formData.room_number ?   
                `${formData.room_number}号房` : '请选择房间(可选)' }}</text>  
        </view>  
    </view>  
</template>  

<script>  
export default {  
    data() {  
        return {  
            formData: {  
                property_id: null,  
                room_id: null,      // 🔥 新增  
                room_number: '',    // 🔥 新增  
                // ...  
            },  
            roomList: []           // 🔥 新增  
        }  
    },  

    methods: {  
        // 房产选择后,自动加载房间列表  
        handlePropertySelected(property) {  
            this.formData.property_id = property.id;  
            this.formData.property_address = property.address;  

            // 清空之前的房间选择  
            this.formData.room_id = null;  
            this.formData.room_number = '';  

            // 🔥 加载该房产的房间列表  
            this.loadRoomList(property.id);  
        },  

        loadRoomList(propertyId) {  
            if (!propertyId) {  
                this.roomList = [];  
                return;  
            }  

            uni.queryWithConditions('rooms', `property_id = ${propertyId}`)  
                .then(results => {  
                    console.log('房间列表查询成功:', results?.length || 0, '个房间');  
                    this.roomList = results || [];  
                })  
                .catch(err => {  
                    console.error('房间列表查询失败:', err);  
                    this.roomList = [];  
                });  
        },  

        showRoomPicker() {  
            if (this.roomList.length === 0) {  
                uni.showToast({  
                    title: '该房产暂无房间',  
                    icon: 'none'  
                });  
                return;  
            }  

            const roomNameList = this.roomList.map(room =>   
                `${room.room_number}号房 (${room.monthly_rent}元/月)`);  

            uni.showActionSheet({  
                itemList: roomNameList,  
                success: (res) => {  
                    const selectedRoom = this.roomList[res.tapIndex];  
                    this.formData.room_id = selectedRoom.id;  
                    this.formData.room_number = selectedRoom.room_number;  
                }  
            });  
        }  
    }  
}  
</script>

功能模块四:数据统计 - 让数字说话

4.1 首页数据总览:一眼看懂经营状况

功能需求:

  • 总资产价值展示
  • 本月收益分析
  • 租赁管理概览
  • 快捷操作入口

核心代码:营业收入计算

踩坑记录5:主页营业收入不包含财务记录

问题现象:
在财务页面添加的收入,主页营业收入不显示。

错误逻辑:分离计算

// ❌ 错误:分离计算导致遗漏  
monthlyTotalIncome() {  
    return this.monthlyRentIncome + this.monthlyNonRentIncome;  
}  

monthlyRentIncome() {  
    // 只从rent_collections表计算  
    return this.rentCollections  
        .filter(r => r.status === 'collected')  
        .reduce((sum, r) => sum + parseFloat(r.total_amount || 0), 0);  
}  

monthlyNonRentIncome() {  
    // 从financial_records表计算,但排除了租金  
    return this.getMonthlyRecords('income')  
        .filter(r => r.category !== 'rent')  
        .reduce((sum, r) => sum + parseFloat(r.amount || 0), 0);  
}

问题分析:

  • 直接在财务页面添加的租金收入被排除了
  • 数据来源不统一,导致统计不完整

正确方案:统一数据源

// ✅ 正确:所有收入统一从financial_records计算  
monthlyTotalIncome() {  
    return this.monthlyIncome; // 统一数据源  
}  

monthlyIncome() {  
    // 财务记录中当月所有收入之和  
    return this.getMonthlyRecords('income').reduce((sum, record) => {  
        return sum + parseFloat(record.amount || 0);  
    }, 0);  
}  

getMonthlyRecords(type) {  
    const now = new Date();  
    const currentMonth = now.toISOString().slice(0, 7); // YYYY-MM  

    return this.financialRecords.filter(record =>  
        record.type === type &&  
        record.record_date &&  
        record.record_date.startsWith(currentMonth)  
    );  
}

架构优化总结:

  • ✅ 财务记录表是唯一的收入统计数据源
  • ✅ 收租记录表仅用于收租流程管理
  • ✅ 收租记录自动创建财务记录,实现数据同步
  • ✅ 统计计算全部基于financial_records表,避免遗漏

4.2 统计分析页:深度数据洞察

功能需求:

  • 房产收益排行
  • 支出分析
  • 租赁分析(出租率、租金收入占比)
  • 租户分析

核心代码:出租率计算

computed: {  
    // 总房间数  
    totalRooms() {  
        return this.roomList.length;  
    },  

    // 已出租房间数  
    rentedRooms() {  
        return this.roomList.filter(room => room.status === 'rented').length;  
    },  

    // 房间出租率  
    roomOccupancyRate() {  
        if (this.totalRooms === 0) return 0;  
        return ((this.rentedRooms / this.totalRooms) * 100).toFixed(1);  
    },  

    // 租金收入占总收入比例  
    rentIncomePercentage() {  
        if (this.totalIncomeAmount === 0) return 0;  
        return ((this.rentIncomeAmount / this.totalIncomeAmount) * 100).toFixed(1);  
    },  

    // 平均租金水平  
    averageMonthlyRent() {  
        if (this.roomList.length === 0) return 0;  
        const totalRent = this.roomList.reduce((sum, room) =>   
            sum + parseFloat(room.monthly_rent || 0), 0);  
        return (totalRent / this.roomList.length).toFixed(0);  
    }  
}

🔧 HarmonyOS适配的三大挑战与解决方案 {#harmonyos适配}

挑战一:Picker组件的兼容性问题

问题场景:
在租金记录页面,选择租户时使用<picker>组件,当有多个租户时点击无响应。

问题代码:

<!-- ❌ 在HarmonyOS上有问题 -->  
<picker :range="tenantList"   
        range-key="name"  
        @change="onTenantChange">  
    <view>{{ selectedTenant?.name || '请选择租户' }}</view>  
</picker>

根本原因:
HarmonyOS的picker组件在动态数据绑定和复杂对象数组处理上存在兼容性问题。

解决方案:使用uni.showActionSheet替代

// ✅ 完美兼容的方案  
showTenantPicker() {  
    if (this.tenantList.length === 0) {  
        uni.showToast({  
            title: '暂无租户',  
            icon: 'none'  
        });  
        return;  
    }  

    const tenantNames = this.tenantList.map(t => t.name);  

    uni.showActionSheet({  
        itemList: tenantNames,  
        success: (res) => {  
            if (!res.cancel) {  
                const selectedTenant = this.tenantList[res.tapIndex];  
                this.formData.tenant_id = selectedTenant.id;  
                this.selectedTenantText = selectedTenant.name;  
                this.selectedTenant = selectedTenant;  

                uni.showToast({  
                    title: `已选择: ${selectedTenant.name}`,  
                    icon: 'success'  
                });  
            }  
        },  
        fail: (err) => {  
            console.log('用户取消选择或出错:', err);  
        }  
    });  
}

HTML改造:

<!-- ✅ 使用点击事件触发 -->  
<view class="picker-wrapper" @click="showTenantPicker">  
    <text class="picker-value">  
        {{ selectedTenantText || '请选择租户' }}  
    </text>  
    <text class="picker-arrow">👤</text>  
</view>
优势对比: 特性 Picker组件 ActionSheet
兼容性 ⚠️ 有问题 ✅ 完美
用户体验 一般 ✅ 原生感强
自定义 受限 ✅ 灵活
调试难度 较难 简单

经验总结:

在HarmonyOS开发中,涉及复杂数据的选择器,优先考虑uni.showActionSheet或自定义组件。

挑战二:ResultSet数据类型转换陷阱

问题场景:
总资产计算时,100万+200万+300万 = "100200300"

问题代码:

// ❌ 错误:所有字段都用getString  
function convertResultSetToFinancialArray(resultSet) {  
    const result = [];  
    resultSet.goToFirstRow();  

    for (let i = 0; i < count; i++) {  
        let record = {};  
        record.id = resultSet.getString(resultSet.getColumnIndex('id'));  
        record.property_id = resultSet.getString(resultSet.getColumnIndex('property_id'));  
        record.amount = resultSet.getString(resultSet.getColumnIndex('amount')); // ❌ 金额变成字符串  
        // ...  
        result[i] = record;  
        resultSet.goToNextRow();  
    }  
    return result;  
}

问题分析:

// JavaScript的+操作符行为  
"100" + "200" + "300" = "100200300"  // ❌ 字符串拼接  
100 + 200 + 300 = 600                 // ✅ 数值相加

正确方案:严格区分数据类型

// ✅ 正确:根据字段类型选择方法  
function convertResultSetToFinancialArray(resultSet) {  
    const result = [];  
    resultSet.goToFirstRow();  

    for (let i = 0; i < count; i++) {  
        let record = {};  
        record.id = resultSet.getString(resultSet.getColumnIndex('id'));          // 文本  
        record.property_id = resultSet.getString(resultSet.getColumnIndex('property_id')); // ID  
        record.room_id = resultSet.getString(resultSet.getColumnIndex('room_id'));         // ID  
        record.type = resultSet.getString(resultSet.getColumnIndex('type'));              // 文本  
        record.category = resultSet.getString(resultSet.getColumnIndex('category'));      // 文本  
        record.amount = resultSet.getDouble(resultSet.getColumnIndex('amount'));          // ✅ 数值  
        record.description = resultSet.getString(resultSet.getColumnIndex('description'));// 文本  
        record.record_date = resultSet.getString(resultSet.getColumnIndex('record_date'));// 日期  
        record.created_at = resultSet.getString(resultSet.getColumnIndex('created_at'));  // 日期  

        result[i] = record;  
        resultSet.goToNextRow();  
    }  
    return result;  
}

最佳实践规范:

// ResultSet数据类型选择指南  
const TYPE_MAPPING = {  
    // 文本类型  
    'text': 'getString',  
    'varchar': 'getString',  

    // 数值类型  
    'integer': 'getLong',     // 整数  
    'real': 'getDouble',      // 小数  
    'numeric': 'getDouble',   // 数值  

    // 特殊类型  
    'blob': 'getBlob',        // 二进制  
    'boolean': 'getLong',     // 布尔值(0/1)  

    // 日期类型(通常存为text)  
    'datetime': 'getString',  // 日期时间  
    'date': 'getString'       // 日期  
};

调试技巧:

// 添加类型检查日志  
computed: {  
    totalValue() {  
        const total = this.propertyList.reduce((sum, item) => {  
            const value = parseFloat(item.current_value) || 0;  

            // 🔥 调试日志  
            console.log(`房产: ${item.address}`);  
            console.log(`  current_value: ${item.current_value} (${typeof item.current_value})`);  
            console.log(`  转换后: ${value} (${typeof value})`);  

            return sum + value;  
        }, 0);  

        console.log(`总资产: ${total} (${typeof total})`);  
        return total;  
    }  
}

挑战三:生命周期与数据初始化

问题场景:
收租记录列表页面不停打印"收租记录表创建成功",数据加载重复执行。

问题代码:

// ❌ 错误:onLoad和onShow都初始化表  
export default {  
    onLoad(options) {  
        this.initTable();  // 创建表  
        this.loadData();   // 加载数据  
    },  

    onShow() {  
        this.initTable();  // 又创建一次表!  
        this.loadData();   // 又加载一次数据!  
    }  
}

问题分析:

  • 每次页面显示都重新初始化表
  • 导致重复的数据库操作
  • 日志输出混乱,影响调试

正确方案:明确生命周期职责

// ✅ 正确:职责分离  
export default {  
    data() {  
        return {  
            isTableInit: false,  // 表初始化标记  
            propertyId: null,  
            dataList: []  
        }  
    },  

    onLoad(options) {  
        // 1. 获取路由参数  
        this.propertyId = options.propertyId;  

        // 2. 首次加载:初始化表 + 加载数据  
        this.initializeOnce();  
    },  

    onShow() {  
        // 3. 后续显示:仅刷新数据  
        if (this.isTableInit) {  
            this.refreshData();  
        }  
    },  

    methods: {  
        // 首次初始化(只执行一次)  
        async initializeOnce() {  
            if (this.isTableInit) return;  

            try {  
                // 初始化表结构  
                await this.initTable();  
                this.isTableInit = true;  
                console.log('表初始化完成');  

                // 加载初始数据  
                await this.loadData();  
            } catch (err) {  
                console.error('初始化失败:', err);  
            }  
        },  

        // 刷新数据(不重新初始化表)  
        async refreshData() {  
            try {  
                await this.loadDataList();  
                console.log('数据刷新完成');  
            } catch (err) {  
                console.error('数据刷新失败:', err);  
            }  
        },  

        // 初始化表结构  
        initTable() {  
            return uni.createTable('rent_collections', columnList)  
                .then(() => {  
                    console.log('收租记录表创建成功');  
                })  
                .catch(err => {  
                    // createTable会在表已存在时报错,这是正常的  
                    console.log('表已存在或创建失败:', err);  
                });  
        },  

        // 加载完整数据  
        async loadData() {  
            await Promise.all([  
                this.loadTenantMap(),    // 先加载依赖数据  
                this.loadRoomMap()  
            ]);  
            await this.loadDataList();   // 再加载主数据  
        },  

        // 仅加载列表数据  
        loadDataList() {  
            return uni.queryWithConditions('rent_collections', '')  
                .then(results => {  
                    this.dataList = results || [];  
                })  
                .catch(err => {  
                    console.error('数据加载失败:', err);  
                    this.dataList = [];  
                });  
        }  
    }  
}

生命周期最佳实践:

页面首次加载 → onLoad  
    ├─ 初始化表结构 (once)  
    ├─ 加载依赖数据 (once)  
    └─ 加载主数据  

页面再次显示 → onShow    
    └─ 仅刷新数据(不重新初始化)

🔄 数据同步机制:让数据流动起来 {#数据同步}

数据同步是整个系统的核心挑战。我设计了三层同步机制:

同步层级一:收租记录 → 财务记录

触发条件: 收租状态为"已收租"

// 自动同步流程  
收租记录保存 (status='collected')  
    ↓  
查询租户信息 (获取租户名称)  
    ↓  
查询房间信息 (获取房间号、房产ID)  
    ↓    
创建财务记录 (income, category='rent')  
    ↓  
完成同步

同步层级二:租户状态 → 房间状态

触发条件: 添加/删除租户,租户状态变更

// 状态联动逻辑  
租户操作完成  
    ↓  
查询该房间所有租户  
    ↓  
检查是否存在活跃租户 (status='active')  
    ↓  
更新房间状态  
    ├─ 有活跃租户 → status='rented'  
    └─ 无活跃租户 → status='vacant'

同步层级三:房间选择 → 数据联动

触发条件: 财务记录选择房产

// 级联加载流程  
选择房产  
    ↓  
清空之前的房间选择  
    ↓  
查询该房产的所有房间  
    ↓  
展示房间选择器(可选)  
    ↓  
选择房间后关联room_id

⚡ 性能优化:从卡顿到丝滑 {#性能优化}

优化一:Computed Property 缓存计算

问题: 在模板中直接调用方法导致重复计算

<!-- ❌ 错误:每次渲染都重新计算 -->  
<text>{{ getTotalAssets() }}</text>  
<text>{{ getTotalAssets() }}</text>  <!-- 又计算一次 -->

优化: 使用计算属性缓存结果

// ✅ 优化:使用computed缓存  
computed: {  
    totalAssets() {  
        return this.propertyList.reduce((sum, p) =>   
            sum + (parseFloat(p.current_value) || 0), 0);  
    }  
}
<!-- ✅ 多次使用,只计算一次 -->  
<text>{{ totalAssets }}</text>  
<text>{{ totalAssets }}</text>  <!-- 使用缓存值 -->

优化二:数据加载策略

问题: 首页加载慢,等待时间长

原因分析:

// ❌ 串行加载,总耗时 = 每个接口耗时之和  
loadData() {  
    this.loadProperties();      // 100ms  
    this.loadFinancials();      // 150ms  
    this.loadRooms();           // 120ms  
    this.loadTenants();         // 100ms  
    // 总耗时: 470ms  
}

优化方案: 并行加载

// ✅ 并行加载,总耗时 = 最慢的接口耗时  
async loadData() {  
    try {  
        await Promise.all([  
            this.loadProperties(),   // ┐  
            this.loadFinancials(),   // ├─ 并行执行  
            this.loadRooms(),        // ┤  
            this.loadTenants()       // ┘  
        ]);  
        // 总耗时: 150ms(最慢的接口)  

        // 依赖数据的后续处理  
        this.processData();  
    } catch (err) {  
        console.error('数据加载失败:', err);  
    }  
}

性能对比:

  • 串行加载:470ms
  • 并行加载:150ms
  • 性能提升:68% 🚀

优化三:避免不必要的数据库操作

问题: onShow时重复初始化表

优化: 使用标志位控制

data() {  
    return {  
        isTableInit: false  // 🔥 标志位  
    }  
},  

methods: {  
    initTable() {  
        if (this.isTableInit) {  
            console.log('表已初始化,跳过');  
            return Promise.resolve();  
        }  

        return uni.createTable(...)  
            .then(() => {  
                this.isTableInit = true;  
                console.log('表初始化完成');  
            });  
    }  
}

优化四:列表渲染优化

使用v-for时添加唯一key

<!-- ❌ 没有key,Vue无法高效更新 -->  
<view v-for="item in list">  
    {{ item.name }}  
</view>  

<!-- ✅ 有key,Vue可以精确复用 -->  
<view v-for="item in list" :key="item.id">  
    {{ item.name }}  
</view>

避免在v-for中使用v-if

<!-- ❌ 性能差:先渲染再过滤 -->  
<view v-for="item in list" :key="item.id" v-if="item.status === 'active'">  
    {{ item.name }}  
</view>  

<!-- ✅ 性能好:先过滤再渲染 -->  
<view v-for="item in activeList" :key="item.id">  
    {{ item.name }}  
</view>  

<script>  
computed: {  
    activeList() {  
        return this.list.filter(item => item.status === 'active');  
    }  
}  
</script>

🎨 UI/UX设计:商务风格的视觉语言 {#uiux设计}

设计理念

作为一款专业的房产管理工具,UI设计遵循以下原则:

  1. 商务专业:渐变、阴影、卡片化设计
  2. 信息清晰:合理的视觉层级和间距
  3. 操作便捷:大按钮、明确反馈
  4. 数据可视:数字大、图标辅助

SCSS变量系统

// uni.scss - 全局设计token  

// ===== 颜色系统 =====  
// 主色  
$primary-blue: #3B82F6;  
$primary-dark: #2563EB;  

// 功能色  
$income-text: #10B981;      // 收入绿  
$income-bg: rgba(16, 185, 129, 0.1);  
$expense-text: #EF4444;     // 支出红  
$expense-bg: rgba(239, 68, 68, 0.1);  

// 文本色  
$text-primary: #1E293B;     // 主要文本  
$text-secondary: #64748B;   // 次要文本  
$text-tertiary: #94A3B8;    // 辅助文本  

// 背景色  
$bg-primary: #FFFFFF;  
$bg-secondary: #F8FAFC;  

// ===== 间距系统 =====  
$spacing-xs: 8rpx;  
$spacing-sm: 12rpx;  
$spacing-md: 16rpx;  
$spacing-lg: 24rpx;  
$spacing-xl: 32rpx;  
$spacing-xxl: 48rpx;  

// ===== 圆角系统 =====  
$radius-small: 8rpx;  
$radius-medium: 12rpx;  
$radius-large: 16rpx;  
$radius-pill: 999rpx;  

// ===== 阴影系统 =====  
$shadow-card: 0 8rpx 32rpx rgba(15, 23, 42, 0.08),   
              0 4rpx 16rpx rgba(15, 23, 42, 0.04);  

// ===== 字体系统 =====  
$font-size-caption: 22rpx;    // 说明文字  
$font-size-body: 28rpx;       // 正文  
$font-size-subhead: 30rpx;    // 副标题  
$font-size-headline: 32rpx;   // 标题  
$font-size-title: 40rpx;      // 大标题

卡片设计模式

// 统一的卡片样式  
.info-card {  
    background: linear-gradient(135deg, #FFFFFF 0%, #F8F9FA 100%);  
    border-radius: $radius-large;  
    padding: $spacing-xl;  
    box-shadow: $shadow-card;  
    border: 1rpx solid rgba(255, 255, 255, 0.8);  
    transition: all 0.3s ease;  

    &:active {  
        transform: translateY(2rpx);  
        box-shadow: 0 4rpx 16rpx rgba(15, 23, 42, 0.06);  
    }  
}

数据展示设计

大数字设计:

<view class="data-showcase">  
    <text class="data-number">¥{{ formatLargeNumber(2380000) }}</text>  
    <text class="data-label">总资产价值</text>  
</view>  

<style lang="scss">  
.data-number {  
    font-size: 56rpx;      // 大号字体  
    font-weight: 700;      // 粗体  
    color: $text-primary;  
    line-height: 1.2;  
    letter-spacing: -0.5rpx; // 紧凑间距  
}  

.data-label {  
    font-size: $font-size-body;  
    color: $text-secondary;  
    margin-top: $spacing-xs;  
}  
</style>

收支颜色区分:

.income-color {  
    color: $income-text; // 绿色  
}  

.expense-color {  
    color: $expense-text; // 红色  
}

用户体验细节

1. 操作反馈优化

// 成功反馈  
uni.showToast({  
    title: '添加成功,即将返回',  // 明确告知用户  
    icon: 'success'  
});  

setTimeout(() => {  
    uni.navigateBack();  
}, 800);  // 800ms刚好够看到提示

2. 空状态设计

<view class="empty-state" v-if="dataList.length === 0">  
    <text class="empty-icon">🏠</text>  
    <text class="empty-title">暂无数据</text>  
    <text class="empty-subtitle">点击右上角按钮添加</text>  
</view>

3. 加载状态

<view class="loading-state" v-if="isLoading">  
    <text class="loading-icon">⏳</text>  
    <text class="loading-text">加载中...</text>  
</view>

🐛 踩坑经验:那些让我头秃的Bug {#踩坑经验}

Bug 1: 字符串拼接陷阱

现象: 100 + 200 + 300 = "100200300"

原因: 数据库返回字符串类型

解决: getDouble + parseFloat双保险

耗时: 2小时

Bug 2: Picker组件失效

现象: 选择租户时点击无反应

原因: HarmonyOS picker兼容问题

解决: 改用uni.showActionSheet

耗时: 3小时

Bug 3: 数据重复加载

现象: 不停打印"表创建成功"

原因: onLoad和onShow都初始化表

解决: 生命周期职责分离

耗时: 1小时

Bug 4: 房间状态误判

现象: 明明没租户却提示"已有租户"

原因: 依赖status字段而非实际数据

解决: 查询数据库确认

耗时: 1.5小时

Bug 5: 收入统计遗漏

现象: 财务页面添加的收入不计入统计

原因: 分离计算逻辑导致遗漏

解决: 统一数据源(financial_records)

耗时: 2小时

Bug 6: 收租日选择不全

现象: 只能选择部分日期

原因: Picker组件限制

解决: Grid布局自定义选择器

耗时: 4小时

总结:调试时间约占开发时间的30% 😅


🔨 开发工具与调试技巧 {#开发工具}

HBuilder X 实用功能

  1. 真机调试:实时预览,快速定位问题
  2. 代码提示:TypeScript类型提示
  3. 条件编译:处理平台差异
// #ifdef APP-HARMONY  
// HarmonyOS专属代码  
// #endif  

// #ifdef APP-IOS  
// iOS专属代码  
// #endif

调试技巧总结

1. Console.log大法

// 关键位置添加日志  
console.log('=== 数据加载开始 ===');  
console.log('查询条件:', condition);  
console.log('查询结果:', results);  
console.log('=== 数据加载完成 ===');

2. 类型检查

console.log(`金额: ${amount}, 类型: ${typeof amount}`);

3. 生命周期追踪

onLoad() { console.log('⬇️ onLoad'); }  
onShow() { console.log('👁️ onShow'); }  
onHide() { console.log('🙈 onHide'); }

4. 数据快照

console.log('数据快照:', JSON.stringify(data, null, 2));

🚀 未来规划与展望 {#未来规划}

v1.1.0 - 功能增强(开发中)

  • [ ] 合同到期提醒功能
  • [ ] 收租日提醒(推送通知)
  • [ ] 数据导出(Excel格式)
  • [ ] 云同步备份

v1.2.0 - 数据可视化(规划中)

  • [ ] 收益趋势图表(折线图)
  • [ ] 房产价值变化(柱状图)
  • [ ] 租客分布分析(饼图)
  • [ ] 年度投资报告

v1.3.0 - 智能化(未来愿景)

  • [ ] AI租金定价建议
  • [ ] 空置期预测
  • [ ] 智能收租提醒
  • [ ] 租客画像分析

v2.0.0 - 多平台扩展

  • [ ] iOS版本发布
  • [ ] Android版本发布
  • [ ] Web管理后台
  • [ ] 数据云同步

💭 总结与感悟 {#总结}

开发历程回顾

开发阶段:

阶段一:需求分析 + 技术选型  
阶段二:数据库设计 + 架构搭建  
阶段三:核心功能开发(房产、租赁)  
阶段四:财务统计模块  
阶段五:HarmonyOS适配调试  
阶段六:UI优化 + Bug修复

代码统计:

  • 总代码量:10000+ 行
  • 页面数量:18个
  • 组件数量:30+个
  • 数据表:5个核心表

技术收获

1. 深入理解HarmonyOS生态

从零开始学习HarmonyOS的relationalStore数据库,从陌生到熟悉,从踩坑到总结最佳实践。

2. 掌握uni-app x开发

  • Composition API的使用
  • 生命周期管理
  • 数据响应式原理
  • 跨平台适配技巧

3. 数据库设计能力

学会设计合理的数据关联关系,理解数据一致性的重要性。

4. 性能优化思维

从用户体验出发,优化加载速度、减少重复计算、提升交互流畅度。

思维成长

1. 用户思维

不仅是开发者,更是用户。每个功能都从实际需求出发,而非为了技术而技术。

2. 架构思维

先设计数据模型,再设计功能模块。好的架构是成功的一半。

3. 工程思维

代码质量、可维护性、可扩展性同样重要。写代码不仅是为了实现功能,更是为了长期维护。

4. 问题解决思维

遇到问题不慌张,系统分析、逐步排查、总结经验。

给开发者的建议

1. 技术选型要慎重

选择适合自己的技术栈,不要盲目追新。uni-app x + HarmonyOS对我来说是最佳选择。

2. 数据库设计要花时间

数据模型设计好了,后续开发事半功倍。不要急于写代码。

3. 用户体验无小事

800ms的延迟优化、身份证脱敏、空状态提示...这些细节决定产品质量。

4. 持续迭代比一次完美更重要

先实现MVP,再逐步完善。不要追求一次性做到完美。

5. 记录踩坑经验

每个Bug都是成长的机会。记录下来,帮助自己也帮助别人。

写在最后

这个项目从构思到完成,经历了无数次的推翻重来、代码重构、问题调试。但当看到一个功能完整、数据准确、体验流畅的应用在HarmonyOS上运行时,所有的努力都值得了。

uni-app x + HarmonyOS Next 的组合,让我既能享受跨平台开发的便利,又能获得原生应用的性能。虽然过程中遇到了很多适配问题,但每个问题的解决都让我对HarmonyOS生态有了更深的理解。

HarmonyOS代表着未来,提前布局就是抓住机遇。感谢DCloud提供的强大框架,感谢HarmonyOS提供的优秀平台。

让我们一起,星光不负,码向未来! 🚀

收起阅读 »

香港开发者无法完成手机号验证

开发者账号

尊敬的 DCloud 技术支持团队,

你们好!

我是一位来自香港的开发者,正在使用你们的 uni-app 框架进行开发,非常感谢你们提供优秀的工具。
目前,我在进行开发者账户的 手机号验证 时遇到了一个阻碍。在“验证手机号”的页面中,系统只允许输入11位数字的手机号码,并且 没有提供国家/地区代码的下拉选择框。
我的香港手机号码格式为 +852 XXXX XXXX(共8位),无法在目前仅支持+86号码的输入框中完成验证。当我尝试输入号码时,系统提示“您填写的手机号码不正确”。

作为一名国际开发者,我非常希望完成手机验证以使用账户的全部功能并保障账户安全。
因此,我想请问:

  1. 是否有针对香港或国际开发者的特殊验证流程?
  2. 能否在验证页面添加国家代码(例如+852)的选择功能?
  3. 或者,能否请你们协助手动为我的账户完成手机号验证?
    感谢你们的时间和帮助!期待你们的回复。
    祝好,
继续阅读 »

尊敬的 DCloud 技术支持团队,

你们好!

我是一位来自香港的开发者,正在使用你们的 uni-app 框架进行开发,非常感谢你们提供优秀的工具。
目前,我在进行开发者账户的 手机号验证 时遇到了一个阻碍。在“验证手机号”的页面中,系统只允许输入11位数字的手机号码,并且 没有提供国家/地区代码的下拉选择框。
我的香港手机号码格式为 +852 XXXX XXXX(共8位),无法在目前仅支持+86号码的输入框中完成验证。当我尝试输入号码时,系统提示“您填写的手机号码不正确”。

作为一名国际开发者,我非常希望完成手机验证以使用账户的全部功能并保障账户安全。
因此,我想请问:

  1. 是否有针对香港或国际开发者的特殊验证流程?
  2. 能否在验证页面添加国家代码(例如+852)的选择功能?
  3. 或者,能否请你们协助手动为我的账户完成手机号验证?
    感谢你们的时间和帮助!期待你们的回复。
    祝好,
收起阅读 »

1024星光不负,码向未来——以 uni-app 筑梦鸿蒙生态我的uni-app鸿蒙开发之旅

鸿蒙6 uni-app鸿蒙开发实践 鸿蒙征文

写在前面的话
当 1024 程序员节的代码星光点亮行业夜空,DCloud 发起的 “星光不负,码向未来” 鸿蒙主题征文活动,恰似一座连接开发者与生态未来的桥梁。作为一名深耕跨端开发五年的技术人,我始终感恩 DCloud 为我们搭建了如此优质的交流舞台,更庆幸能借 uni-app 技术栈,深度参与到鸿蒙生态的建设浪潮中。今天,我想分享一款基于 uni-app 开发的鸿蒙元服务项目实战经历,既是对这段开发旅程的复盘,也是对鸿蒙生态璀璨前景的致敬。

一、项目缘起:从痛点出发的技术选型

我们团队今年承接了一款本地生活类鸿蒙元服务的开发需求,核心目标是实现 “即时触达、轻量化交互、跨设备兼容”—— 用户无需安装 App,通过鸿蒙桌面卡片即可快速查看周边商户、领取优惠券并完成预约。在技术选型阶段,我们对比了多种开发方案:原生开发虽能深度适配鸿蒙特性,但跨设备兼容成本高;其他跨端框架对鸿蒙元服务的支持尚不完善。最终,uni-app 凭借其对鸿蒙生态的深度适配、与 Vue 语法的无缝衔接,以及 “一次开发、多端部署” 的核心优势,成为了我们的最优解。
更让我们惊喜的是,DCloud 提供的 uni-app 鸿蒙插件市场(很多鸿蒙插件给我们提供极大便利,开发中也得到鸿蒙插件作者@An_king 帮助)、官方文档与社区支持,为项目启动扫清了初期障碍。从基础配置到进阶功能,这让我们对项目落地充满了信心。

二、核心实践:鸿蒙能力与 uni-app 的深度融合

2.1 鸿蒙云开发的集成与落地

项目初期,我们面临着 “数据实时同步” 与 “服务器部署成本” 的双重挑战。鸿蒙云开发提供的云数据库、云函数等能力,恰好契合我们的需求。通过 uni-app 的扩展 API,我们实现了三大关键突破:
数据双向同步优化:利用鸿蒙云数据库的实时监听能力,结合 uni-app 的响应式数据绑定,实现了商户库存、优惠券余量与用户端的毫秒级同步。此前我们曾遭遇 “本地缓存与云端数据不一致” 的问题,通过自定义云函数,在数据更新时触发云端校验与本地缓存刷新,成功解决了这一痛点。
云函数的轻量化部署:将用户登录验证、优惠券核销等核心业务逻辑部署在鸿蒙云函数中,通过 uni-app 的云对象方法配合调用。这一方案不仅减少了客户端代码体积,更通过云端扩容能力,全面提升了App的性能。
权限申请的优雅处理:鸿蒙系统对用户权限的管控极为严格,我们借助 uni-app 的权限申请封装 API,结合鸿蒙元服务的特性,设计了 “按需申请、分步授权” 的交互流程。例如,在用户首次点击 “获取周边商户” 时才申请定位权限,并通过弹窗清晰说明权限用途,将授权通过率提升了 40%。

2.2 预加载能力的创新应用

元服务的 “秒开体验” 是项目的核心竞争力之一,而uniapp的预加载能力成为了关键突破口。我们通过 uni-app 的app.vue生命周期钩子,结合封装鸿蒙的原生组件,实现了精准化对接预加载策略:
针对高频访问的 “商户列表页”,在元服务启动前预加载核心数据与页面组件,将首屏加载时间从 1.2秒压缩至 0.4秒;
基于用户行为分析,对 “优惠券中心” 等次级页面实现 “智能预加载”—— 当用户浏览商户详情超过 3 秒时,后台异步预加载优惠券数据,既保证了响应速度,又避免了资源浪费。
这一过程中,我们曾遇到 “预加载资源占用过高导致卡顿” 的问题。通过 DCloud 社区的技术交流,我们借鉴了其他开发者分享的 “超级下拉列表“和“资源优先级排序” 方案,对预加载内容按重要性分级,优先加载文本数据,延迟加载图片资源,最终完美解决了性能瓶颈。

2.3 全流程落地:从开发到完成的实践复盘

需求与适配阶段:我们充分利用 uni-app 的多端适配能力,针对安卓、苹果、各家小程序、鸿蒙手机、平板、智慧屏等不同设备,通过uni.getSystemInfoSync()获取设备参数,实现了页面布局的自适应调整。特别是针对鸿蒙元服务的卡片尺寸限制,设计了 “核心信息精简展示、点击展开详情” 的交互模式。
调试与测试阶段:借助鸿蒙云测试平台与 uni-app 的真机调试功能,我们完成了多机型兼容性测试。DCloud 提供的 uni-app 调试工具能够精准定位鸿蒙特有的语法兼容问题,让我们在短时间内修复了 “组件生命周期触发异常”“API 调用权限不足” 等关键 Bug。

三、生态感悟:感恩同行,共赴未来

回望这段开发旅程,我们既惊叹于鸿蒙生态的高速发展,更感激 DCloud 为开发者提供的坚实支撑。从最初对鸿蒙技术的懵懂探索,到如今能熟练运用 uni-app 实现复杂业务场景,每一步成长都离不开 DCloud 官方的技术赋能与社区伙伴的经验分享。uni-app 就像一座桥梁,让我们这些跨端开发者能够低门槛地融入鸿蒙生态,用熟悉的技术栈创造出符合新时代需求的应用产品。
如今,HarmonyOS 6 的到来开启了生态发展的新篇章,流畅的性能、AI 原生能力等创新特性,为开发者提供了更广阔的创新空间。作为生态建设的参与者,我们深刻意识到,每一行代码都是生态的基石,每一次分享都是技术的传承。DCloud 发起的本次征文活动,不仅为我们提供了展示技术成果的舞台,更促进了鸿蒙生态开发者之间的思想碰撞,这种技术共享的氛围,正是行业进步的核心动力。

四、展望未来:以代码为笔,书写生态新篇

星光不负赶路人,时光不负奋斗者。在未来的开发道路上,我们团队将继续深耕 uni-app 与鸿蒙生态的融合创新,计划基于鸿蒙近场能力开发 “设备间优惠券共享” 功能,进一步拓展元服务的应用场景。同时,我们也将积极参与 DCloud 社区的技术分享,把项目中积累的经验转化为帮助他人的力量,就像曾经那些帮助过我们的开发者一样。
我们坚信,随着鸿蒙生态的持续完善与 DCloud 的不断赋能,会有更多开发者加入这场技术革新的浪潮。当无数开发者的代码星光汇聚,必将照亮鸿蒙生态的未来之路。最后,再次感谢 DCloud 举办本次征文活动,感谢每一位为生态建设默默付出的技术人。愿我们以代码为翼,与鸿蒙共成长,在数字时代的浪潮中,书写属于开发者的璀璨篇章!

继续阅读 »

写在前面的话
当 1024 程序员节的代码星光点亮行业夜空,DCloud 发起的 “星光不负,码向未来” 鸿蒙主题征文活动,恰似一座连接开发者与生态未来的桥梁。作为一名深耕跨端开发五年的技术人,我始终感恩 DCloud 为我们搭建了如此优质的交流舞台,更庆幸能借 uni-app 技术栈,深度参与到鸿蒙生态的建设浪潮中。今天,我想分享一款基于 uni-app 开发的鸿蒙元服务项目实战经历,既是对这段开发旅程的复盘,也是对鸿蒙生态璀璨前景的致敬。

一、项目缘起:从痛点出发的技术选型

我们团队今年承接了一款本地生活类鸿蒙元服务的开发需求,核心目标是实现 “即时触达、轻量化交互、跨设备兼容”—— 用户无需安装 App,通过鸿蒙桌面卡片即可快速查看周边商户、领取优惠券并完成预约。在技术选型阶段,我们对比了多种开发方案:原生开发虽能深度适配鸿蒙特性,但跨设备兼容成本高;其他跨端框架对鸿蒙元服务的支持尚不完善。最终,uni-app 凭借其对鸿蒙生态的深度适配、与 Vue 语法的无缝衔接,以及 “一次开发、多端部署” 的核心优势,成为了我们的最优解。
更让我们惊喜的是,DCloud 提供的 uni-app 鸿蒙插件市场(很多鸿蒙插件给我们提供极大便利,开发中也得到鸿蒙插件作者@An_king 帮助)、官方文档与社区支持,为项目启动扫清了初期障碍。从基础配置到进阶功能,这让我们对项目落地充满了信心。

二、核心实践:鸿蒙能力与 uni-app 的深度融合

2.1 鸿蒙云开发的集成与落地

项目初期,我们面临着 “数据实时同步” 与 “服务器部署成本” 的双重挑战。鸿蒙云开发提供的云数据库、云函数等能力,恰好契合我们的需求。通过 uni-app 的扩展 API,我们实现了三大关键突破:
数据双向同步优化:利用鸿蒙云数据库的实时监听能力,结合 uni-app 的响应式数据绑定,实现了商户库存、优惠券余量与用户端的毫秒级同步。此前我们曾遭遇 “本地缓存与云端数据不一致” 的问题,通过自定义云函数,在数据更新时触发云端校验与本地缓存刷新,成功解决了这一痛点。
云函数的轻量化部署:将用户登录验证、优惠券核销等核心业务逻辑部署在鸿蒙云函数中,通过 uni-app 的云对象方法配合调用。这一方案不仅减少了客户端代码体积,更通过云端扩容能力,全面提升了App的性能。
权限申请的优雅处理:鸿蒙系统对用户权限的管控极为严格,我们借助 uni-app 的权限申请封装 API,结合鸿蒙元服务的特性,设计了 “按需申请、分步授权” 的交互流程。例如,在用户首次点击 “获取周边商户” 时才申请定位权限,并通过弹窗清晰说明权限用途,将授权通过率提升了 40%。

2.2 预加载能力的创新应用

元服务的 “秒开体验” 是项目的核心竞争力之一,而uniapp的预加载能力成为了关键突破口。我们通过 uni-app 的app.vue生命周期钩子,结合封装鸿蒙的原生组件,实现了精准化对接预加载策略:
针对高频访问的 “商户列表页”,在元服务启动前预加载核心数据与页面组件,将首屏加载时间从 1.2秒压缩至 0.4秒;
基于用户行为分析,对 “优惠券中心” 等次级页面实现 “智能预加载”—— 当用户浏览商户详情超过 3 秒时,后台异步预加载优惠券数据,既保证了响应速度,又避免了资源浪费。
这一过程中,我们曾遇到 “预加载资源占用过高导致卡顿” 的问题。通过 DCloud 社区的技术交流,我们借鉴了其他开发者分享的 “超级下拉列表“和“资源优先级排序” 方案,对预加载内容按重要性分级,优先加载文本数据,延迟加载图片资源,最终完美解决了性能瓶颈。

2.3 全流程落地:从开发到完成的实践复盘

需求与适配阶段:我们充分利用 uni-app 的多端适配能力,针对安卓、苹果、各家小程序、鸿蒙手机、平板、智慧屏等不同设备,通过uni.getSystemInfoSync()获取设备参数,实现了页面布局的自适应调整。特别是针对鸿蒙元服务的卡片尺寸限制,设计了 “核心信息精简展示、点击展开详情” 的交互模式。
调试与测试阶段:借助鸿蒙云测试平台与 uni-app 的真机调试功能,我们完成了多机型兼容性测试。DCloud 提供的 uni-app 调试工具能够精准定位鸿蒙特有的语法兼容问题,让我们在短时间内修复了 “组件生命周期触发异常”“API 调用权限不足” 等关键 Bug。

三、生态感悟:感恩同行,共赴未来

回望这段开发旅程,我们既惊叹于鸿蒙生态的高速发展,更感激 DCloud 为开发者提供的坚实支撑。从最初对鸿蒙技术的懵懂探索,到如今能熟练运用 uni-app 实现复杂业务场景,每一步成长都离不开 DCloud 官方的技术赋能与社区伙伴的经验分享。uni-app 就像一座桥梁,让我们这些跨端开发者能够低门槛地融入鸿蒙生态,用熟悉的技术栈创造出符合新时代需求的应用产品。
如今,HarmonyOS 6 的到来开启了生态发展的新篇章,流畅的性能、AI 原生能力等创新特性,为开发者提供了更广阔的创新空间。作为生态建设的参与者,我们深刻意识到,每一行代码都是生态的基石,每一次分享都是技术的传承。DCloud 发起的本次征文活动,不仅为我们提供了展示技术成果的舞台,更促进了鸿蒙生态开发者之间的思想碰撞,这种技术共享的氛围,正是行业进步的核心动力。

四、展望未来:以代码为笔,书写生态新篇

星光不负赶路人,时光不负奋斗者。在未来的开发道路上,我们团队将继续深耕 uni-app 与鸿蒙生态的融合创新,计划基于鸿蒙近场能力开发 “设备间优惠券共享” 功能,进一步拓展元服务的应用场景。同时,我们也将积极参与 DCloud 社区的技术分享,把项目中积累的经验转化为帮助他人的力量,就像曾经那些帮助过我们的开发者一样。
我们坚信,随着鸿蒙生态的持续完善与 DCloud 的不断赋能,会有更多开发者加入这场技术革新的浪潮。当无数开发者的代码星光汇聚,必将照亮鸿蒙生态的未来之路。最后,再次感谢 DCloud 举办本次征文活动,感谢每一位为生态建设默默付出的技术人。愿我们以代码为翼,与鸿蒙共成长,在数字时代的浪潮中,书写属于开发者的璀璨篇章!

收起阅读 »

Uniapp 的鸿蒙 next 应用中隐藏和显示系统状态栏

鸿蒙征文 鸿蒙next

本示例至少需要在 HbuilderX 4.61 运行

在 uniapp 开发鸿蒙应用中,通过 UTS 插件,可以调用许多系统原生的 API,这里给出一个小功能:隐藏和显示系统状态栏

原始的界面效果:

隐藏系统状态栏之后的效果:

这个示例的参考文档有:

  • 鸿蒙官方文档: https://developer.huawei.com/consumer/cn/doc/harmonyos-faqs/faqs-arkui-193
  • UTS 插件: https://doc.dcloud.net.cn/uni-app-x/plugin/uts-plugin.html
  • UTSHarmony 的使用: https://doc.dcloud.net.cn/uni-app-x/uts/utsharmony.html

核心页面代码:

<template>  
    <view>  
        <button @click="show">显示原生状态栏</button>  
        <button @click="hide">隐藏原生状态栏</button>  
    </view>  
</template>  
<script>  
    import { showStatusBar, hideStatusBar } from '@/uni_modules/harmony-statusbar';  
    export default {  
        data() {  
            return {  
                message: 'Hello, World!'  
            }  
        },  
        methods: {  
            show() {  
                showStatusBar()  
            },  
            hide() {  
                hideStatusBar()  
            }  
        }  
    }  
</script>  
<style scoped>  
</style>

核心UTS插件代码:

import window from '@ohos.window';  

let _window : window.Window;  

UTSHarmony.onAppAbilityWindowStageCreate((windowStage : window.WindowStage) => {  
    _window = windowStage.getMainWindowSync()  
})  

export const hideStatusBar = () => {  
    _window.setWindowSystemBarEnable([])  
}  

export const showStatusBar = () => {  
    _window.setWindowSystemBarEnable(['status', 'navigation'])  
}

示例工程:

继续阅读 »

本示例至少需要在 HbuilderX 4.61 运行

在 uniapp 开发鸿蒙应用中,通过 UTS 插件,可以调用许多系统原生的 API,这里给出一个小功能:隐藏和显示系统状态栏

原始的界面效果:

隐藏系统状态栏之后的效果:

这个示例的参考文档有:

  • 鸿蒙官方文档: https://developer.huawei.com/consumer/cn/doc/harmonyos-faqs/faqs-arkui-193
  • UTS 插件: https://doc.dcloud.net.cn/uni-app-x/plugin/uts-plugin.html
  • UTSHarmony 的使用: https://doc.dcloud.net.cn/uni-app-x/uts/utsharmony.html

核心页面代码:

<template>  
    <view>  
        <button @click="show">显示原生状态栏</button>  
        <button @click="hide">隐藏原生状态栏</button>  
    </view>  
</template>  
<script>  
    import { showStatusBar, hideStatusBar } from '@/uni_modules/harmony-statusbar';  
    export default {  
        data() {  
            return {  
                message: 'Hello, World!'  
            }  
        },  
        methods: {  
            show() {  
                showStatusBar()  
            },  
            hide() {  
                hideStatusBar()  
            }  
        }  
    }  
</script>  
<style scoped>  
</style>

核心UTS插件代码:

import window from '@ohos.window';  

let _window : window.Window;  

UTSHarmony.onAppAbilityWindowStageCreate((windowStage : window.WindowStage) => {  
    _window = windowStage.getMainWindowSync()  
})  

export const hideStatusBar = () => {  
    _window.setWindowSystemBarEnable([])  
}  

export const showStatusBar = () => {  
    _window.setWindowSystemBarEnable(['status', 'navigation'])  
}

示例工程:

收起阅读 »

经验分享 鸿蒙里的权限设置,如何获取、查询权限

鸿蒙next 鸿蒙征文

鸿蒙里的权限

鸿蒙的权限可以分成三类:

开放权限:system_grant, 比如 INTERNET网络权限、VIBRATE 手机震动权限等。无需用户同意。具体可见 开放权限(系统授权)
用户授权:user_grant,弹窗询问用户是否允许位置定位、发送通知等。具体可见 开放权限(用户授权)
敏感权限:需要在华为后台单独填写表格申请获得,比如修改用户公共目录文件、API 读取剪切板等。具体可见 受限开放权限
还有一些针对特定企业管理的权限,场景比较特殊,这里不做进一步描述。

细节可以看文档 《鸿蒙权限配置指南

如何定义权限

举例定位中用到的模糊定位、精准定位。需要参考文档,在 requestPermissions

如何查询权限是否授权?

const auth = () => {  
    const res = uni.getAppAuthorizeSetting()  
    console.log(res)  
  }

如何主动申请用户授权特定的权限?

先见 uts-api 鸿蒙插件,填写下面代码, uni_modules/harmony-harmony/utssdk/app-harmony/index.uts

import { abilityAccessCtrl, Permissions } from '@kit.AbilityKit';  

export const requestSystemPermission = () => {  

  const permissionList : Array<Permissions> = ['ohos.permission.APPROXIMATELY_LOCATION']  
  UTSHarmony.requestSystemPermission(permissionList, (allRight : boolean, grantedList : Array<string>) => {  
    console.log('res', allRight, grantedList);  
  }, (doNotAskAgain : boolean, grantedList : Array<string>) => {  
    console.log('fail', doNotAskAgain, grantedList);  
  })  
}

在 vue 代码中这样使用

<script setup lang="uts">  
  import { requestSystemPermission } from '@/uni_modules/harmony-harmony'  

  const permisson = () => {  
    requestSystemPermission()  
  }  
</script>

如何打开系统设置?

可引导用户打开设置重新授权。

uni.openAppAuthorizeSetting()  

https://uniapp.dcloud.net.cn/api/system/openappauthorizesetting.html

继续阅读 »

鸿蒙里的权限

鸿蒙的权限可以分成三类:

开放权限:system_grant, 比如 INTERNET网络权限、VIBRATE 手机震动权限等。无需用户同意。具体可见 开放权限(系统授权)
用户授权:user_grant,弹窗询问用户是否允许位置定位、发送通知等。具体可见 开放权限(用户授权)
敏感权限:需要在华为后台单独填写表格申请获得,比如修改用户公共目录文件、API 读取剪切板等。具体可见 受限开放权限
还有一些针对特定企业管理的权限,场景比较特殊,这里不做进一步描述。

细节可以看文档 《鸿蒙权限配置指南

如何定义权限

举例定位中用到的模糊定位、精准定位。需要参考文档,在 requestPermissions

如何查询权限是否授权?

const auth = () => {  
    const res = uni.getAppAuthorizeSetting()  
    console.log(res)  
  }

如何主动申请用户授权特定的权限?

先见 uts-api 鸿蒙插件,填写下面代码, uni_modules/harmony-harmony/utssdk/app-harmony/index.uts

import { abilityAccessCtrl, Permissions } from '@kit.AbilityKit';  

export const requestSystemPermission = () => {  

  const permissionList : Array<Permissions> = ['ohos.permission.APPROXIMATELY_LOCATION']  
  UTSHarmony.requestSystemPermission(permissionList, (allRight : boolean, grantedList : Array<string>) => {  
    console.log('res', allRight, grantedList);  
  }, (doNotAskAgain : boolean, grantedList : Array<string>) => {  
    console.log('fail', doNotAskAgain, grantedList);  
  })  
}

在 vue 代码中这样使用

<script setup lang="uts">  
  import { requestSystemPermission } from '@/uni_modules/harmony-harmony'  

  const permisson = () => {  
    requestSystemPermission()  
  }  
</script>

如何打开系统设置?

可引导用户打开设置重新授权。

uni.openAppAuthorizeSetting()  

https://uniapp.dcloud.net.cn/api/system/openappauthorizesetting.html

收起阅读 »

经验分享 鸿蒙中如何隐藏底部触控小白条?

鸿蒙next 鸿蒙征文

在鸿蒙底部有触控小白条,用来响应系统级用户手势。在应用开发时候,有些业务场景需要隐藏底部触控小白条,鸿蒙提供了响应 API,代码比较简单,使用 UTS 几行代码轻松切换展示。

在 HBuilderX 中新建 uni_modules 文件夹,在 uni_modules 文件夹右键选择创建 UTS-API 插件,创建并编辑 app-harmony/index.uts 文件夹,如果没有就新建该文件。

在文件中填写下面代码:


/**  
 * 展示底部小白条  
 */  
export const showNavigationIndicator = () => {  
  const window = UTSHarmony.getCurrentWindow()  
  window.setSpecificSystemBarEnabled('navigationIndicator', true)  
}  

/**  
 * 隐藏底部小白条  
 */  
export const hideNavigationIndicator = () => {  
  const window = UTSHarmony.getCurrentWindow()  
  window.setSpecificSystemBarEnabled('navigationIndicator', false)  
}  

在 Vue 页面中导入并使用即可。

<template>  
  <view>  
    <button @click="showNavigationIndicator">showNavigationIndicator</button>  
    <button @click="hideNavigationIndicator">hideNavigationIndicator</button>  
  </view>  
</template>  
<script setup lang="uts">  
  import {  
    showNavigationIndicator,  
    hideNavigationIndicator,  
  } from '@/uni_modules/harmony-toggle-navigation-indicator'  
</script>  

当用户点击 hideNavigationIndicator 按钮之后,系统大概一秒后会隐藏小白条。点击 showNavigationIndicator 系统会展示小白条。

继续阅读 »

在鸿蒙底部有触控小白条,用来响应系统级用户手势。在应用开发时候,有些业务场景需要隐藏底部触控小白条,鸿蒙提供了响应 API,代码比较简单,使用 UTS 几行代码轻松切换展示。

在 HBuilderX 中新建 uni_modules 文件夹,在 uni_modules 文件夹右键选择创建 UTS-API 插件,创建并编辑 app-harmony/index.uts 文件夹,如果没有就新建该文件。

在文件中填写下面代码:


/**  
 * 展示底部小白条  
 */  
export const showNavigationIndicator = () => {  
  const window = UTSHarmony.getCurrentWindow()  
  window.setSpecificSystemBarEnabled('navigationIndicator', true)  
}  

/**  
 * 隐藏底部小白条  
 */  
export const hideNavigationIndicator = () => {  
  const window = UTSHarmony.getCurrentWindow()  
  window.setSpecificSystemBarEnabled('navigationIndicator', false)  
}  

在 Vue 页面中导入并使用即可。

<template>  
  <view>  
    <button @click="showNavigationIndicator">showNavigationIndicator</button>  
    <button @click="hideNavigationIndicator">hideNavigationIndicator</button>  
  </view>  
</template>  
<script setup lang="uts">  
  import {  
    showNavigationIndicator,  
    hideNavigationIndicator,  
  } from '@/uni_modules/harmony-toggle-navigation-indicator'  
</script>  

当用户点击 hideNavigationIndicator 按钮之后,系统大概一秒后会隐藏小白条。点击 showNavigationIndicator 系统会展示小白条。

收起阅读 »

关于如何查看apk 的 so 是否支持16 KB 内存页面大小

命令行执行

sh /Users/xxx/Desktop/check_elf_alignment.sh /Users/xxx/Desktop/2.apk 

-e /var/folders/ww/xcthf5jn7gq5_p_v4hkmrm580000gn/T/5_out_XXXXX.xK6hDLJKG2/lib/arm64-v8a/libweexjst.so: \e[32mALIGNED\e[0m (214)
-e /var/folders/ww/xcthf5jn7gq5_p_v4hkmrm580000gn/T/5_out_XXXXX.xK6hDLJKG2/lib/armeabi-v7a/libweexjsb.so: \e[31mUNALIGNED\e[0m (2
12)

UNALIGNED - 支持

ALIGNED - 不支持

继续阅读 »

命令行执行

sh /Users/xxx/Desktop/check_elf_alignment.sh /Users/xxx/Desktop/2.apk 

-e /var/folders/ww/xcthf5jn7gq5_p_v4hkmrm580000gn/T/5_out_XXXXX.xK6hDLJKG2/lib/arm64-v8a/libweexjst.so: \e[32mALIGNED\e[0m (214)
-e /var/folders/ww/xcthf5jn7gq5_p_v4hkmrm580000gn/T/5_out_XXXXX.xK6hDLJKG2/lib/armeabi-v7a/libweexjsb.so: \e[31mUNALIGNED\e[0m (2
12)

UNALIGNED - 支持

ALIGNED - 不支持

收起阅读 »

打开apkApp应用灰屏

bug都是自己写出来的,仔细检查下代码吧

bug都是自己写出来的,仔细检查下代码吧

app端uniapp分片上传文件

思路如下

1.获取大文件的临时目录tempFilePath

  1. tempFilePath转应用的安全路径safePath
  2. 根据大文件的size和每片大小进行分片,得到每个分片的base64
    4.直接把每片的base64上传到后端,让后端处理
    5.删除安全路径的文件

具体方法如下
2.tempFilePath转应用的安全路径safePath

let rootDir = await this.getDbFolder();  

      let safePath = rootDir.fullPath + filename;  
      let existFlag = await this.checkFileExists(filename);  
      if (!existFlag){  
        //复制到安全路径下  
        safePath = await this.copyToSafePath(tempFilePath, rootDir,filename);  
      }  

    async getDbFolder() { // 返回doc的应用私有目录对象,用于文件copy接口使用  
      return new Promise((resolve, reject) => {  
        plus.io.requestFileSystem(plus.io.PRIVATE_DOC, function(fs) {  
          resolve(fs.root)  
        });  
      });  
    },  

    async checkFileExists(fileName) {  
      return new Promise((resolve) => {  
        plus.io.requestFileSystem(plus.io.PRIVATE_DOC, (fs) => {  
          fs.root.getFile(fileName, { create: false },  
              () => resolve(true),  // 文件存在  
              () => resolve(false)  // 文件不存在  
          );  
        });  
      });  
    },  

async copyToSafePath(tempFilePath, rootDir,filename) {  
      return new Promise((resolve, reject) => {  
        // 使用plus.io读取文件  
        plus.io.resolveLocalFileSystemURL(tempFilePath, (entry) => {  
          entry.copyTo(rootDir,filename,function (res){  
            console.log("视频复制成功:" + res.fullPath);  
            resolve(res.fullPath);  
          },function (e){  
            console.log("视频复制成功:" ,e);  
            reject(e);  
          })  
        });  
      });  
    },  
  1. 根据大文件的size和每片大小进行分片,得到每个分片的base64
for (let i = 0; i < totalChunks; i++) {  
          const start = i * chunkSize;  
          const length = Math.min(size - start, chunkSize); // 实际读取长度  

          // 读取分片base64  
          const chunkBase64 = await this.readFileChunk(safePath, start, length);  
....  

/**  
     * 获取分片的base64  
     */  
     readFileChunk(safePath, start, length) {  
      return new Promise((resolve, reject) => {  
        // 使用plus.io读取文件  
        plus.io.resolveLocalFileSystemURL(safePath, (entry) => {  
          entry.file((file) => {  
            const reader = new plus.io.FileReader();  
            // 设置读取成功回调  
            reader.onloadend  = (e)=> {  
              let base64 = e.target.result;  
              resolve(base64);  
            };  

            // 设置读取错误回调  
            reader.onerror = function(error) {  
              console.error('读取文件分片失败:', error);  
              reject(error);  
            };  

            // 截取文件片段并读取  slice是左右都会取到的,所以要减1  
            const fileSlice = file.slice(start, start + length - 1);  
            reader.readAsDataURL(fileSlice);  
          }, (error) => {  
            console.error('获取文件对象失败:', error);  
            reject(error);  
          });  
        }, (error) => {  
          console.error('解析文件路径失败:', error);  
          reject(error);  
        });  
      });  
    },

5.删除安全路径的文件

removeSafePath(safePath) {  
      uni.removeSavedFile({  
        filePath: safePath,  
        success: (res) => {  
          console.log('文件删除成功', safePath);  
        },  
        fail: (err) => {  
          console.error('文件删除失败', err);  
        }  
      });  
    },  
继续阅读 »

思路如下

1.获取大文件的临时目录tempFilePath

  1. tempFilePath转应用的安全路径safePath
  2. 根据大文件的size和每片大小进行分片,得到每个分片的base64
    4.直接把每片的base64上传到后端,让后端处理
    5.删除安全路径的文件

具体方法如下
2.tempFilePath转应用的安全路径safePath

let rootDir = await this.getDbFolder();  

      let safePath = rootDir.fullPath + filename;  
      let existFlag = await this.checkFileExists(filename);  
      if (!existFlag){  
        //复制到安全路径下  
        safePath = await this.copyToSafePath(tempFilePath, rootDir,filename);  
      }  

    async getDbFolder() { // 返回doc的应用私有目录对象,用于文件copy接口使用  
      return new Promise((resolve, reject) => {  
        plus.io.requestFileSystem(plus.io.PRIVATE_DOC, function(fs) {  
          resolve(fs.root)  
        });  
      });  
    },  

    async checkFileExists(fileName) {  
      return new Promise((resolve) => {  
        plus.io.requestFileSystem(plus.io.PRIVATE_DOC, (fs) => {  
          fs.root.getFile(fileName, { create: false },  
              () => resolve(true),  // 文件存在  
              () => resolve(false)  // 文件不存在  
          );  
        });  
      });  
    },  

async copyToSafePath(tempFilePath, rootDir,filename) {  
      return new Promise((resolve, reject) => {  
        // 使用plus.io读取文件  
        plus.io.resolveLocalFileSystemURL(tempFilePath, (entry) => {  
          entry.copyTo(rootDir,filename,function (res){  
            console.log("视频复制成功:" + res.fullPath);  
            resolve(res.fullPath);  
          },function (e){  
            console.log("视频复制成功:" ,e);  
            reject(e);  
          })  
        });  
      });  
    },  
  1. 根据大文件的size和每片大小进行分片,得到每个分片的base64
for (let i = 0; i < totalChunks; i++) {  
          const start = i * chunkSize;  
          const length = Math.min(size - start, chunkSize); // 实际读取长度  

          // 读取分片base64  
          const chunkBase64 = await this.readFileChunk(safePath, start, length);  
....  

/**  
     * 获取分片的base64  
     */  
     readFileChunk(safePath, start, length) {  
      return new Promise((resolve, reject) => {  
        // 使用plus.io读取文件  
        plus.io.resolveLocalFileSystemURL(safePath, (entry) => {  
          entry.file((file) => {  
            const reader = new plus.io.FileReader();  
            // 设置读取成功回调  
            reader.onloadend  = (e)=> {  
              let base64 = e.target.result;  
              resolve(base64);  
            };  

            // 设置读取错误回调  
            reader.onerror = function(error) {  
              console.error('读取文件分片失败:', error);  
              reject(error);  
            };  

            // 截取文件片段并读取  slice是左右都会取到的,所以要减1  
            const fileSlice = file.slice(start, start + length - 1);  
            reader.readAsDataURL(fileSlice);  
          }, (error) => {  
            console.error('获取文件对象失败:', error);  
            reject(error);  
          });  
        }, (error) => {  
          console.error('解析文件路径失败:', error);  
          reject(error);  
        });  
      });  
    },

5.删除安全路径的文件

removeSafePath(safePath) {  
      uni.removeSavedFile({  
        filePath: safePath,  
        success: (res) => {  
          console.log('文件删除成功', safePath);  
        },  
        fail: (err) => {  
          console.error('文件删除失败', err);  
        }  
      });  
    },  
收起阅读 »

摩尔斯电码转换器

鸿蒙征文

摩尔斯电码转换器 📡

一个基于 uni-app 开发的摩尔斯电码编解码应用,支持文本与摩尔斯电码的双向转换。

License
UniApp

✨ 功能特性

image-20251022144208411

核心功能

  • 🔤 文本转摩尔斯电码:将英文文本编码为摩尔斯电码
  • 🔡 摩尔斯电码转文本:将摩尔斯电码解码为可读文本
  • 🔄 双向转换:一键切换编码/解码模式
  • 📋 对照表:内置完整的摩尔斯电码对照表,可随时查阅

支持字符

  • ✅ 26个英文字母(A-Z)
  • ✅ 10个数字(0-9)
  • ✅ 常用标点符号:. , ? ' ! / ( ) & : ; = + - _ " $ @

界面特色

  • 🎨 现代化渐变设计
  • 📱 响应式布局,适配多种屏幕尺寸
  • 💫 流畅的动画过渡效果
  • 🌈 直观的视觉反馈
  • 📝 可选中复制输出结果

📸 预览

编码模式

将文本转换为摩尔斯电码:

输入:HELLO WORLD  
输出:.... . .-.. .-.. --- / .-- --- .-. .-.. -..

解码模式

将摩尔斯电码转换为文本:

输入:.... . .-.. .-.. --- / .-- --- .-. .-.. -..  
输出:HELLO WORLD

🚀 快速开始

环境要求

  • HBuilderX 3.0+(运行到 HarmonyOS 需要 5.0+ 版本)
  • uni-app 框架
  • 支持 uni-app 的运行环境:
    • 微信小程序:微信开发者工具
    • H5:现代浏览器(Chrome、Firefox、Safari 等)
    • Android/iOS:Android Studio / Xcode
    • HarmonyOS:HarmonyOS 5.0+ 设备或模拟器

安装步骤

  1. 克隆项目
git clone [your-repository-url]  
cd Morse
  1. 使用 HBuilderX 打开项目

    • 启动 HBuilderX
    • 文件 → 打开目录 → 选择项目文件夹
  2. 运行项目

    • 运行 → 运行到浏览器 → Chrome(H5)
    • 或运行到微信开发者工具(小程序)
    • 或运行到手机模拟器(App)
    • 或运行到 HarmonyOS(鸿蒙应用)

运行到 HarmonyOS

前置要求

  • 安装 HBuilderX 4.0+ 版本
  • 配置 HarmonyOS 开发环境
  • 安装 DevEco Studio(可选,用于更高级的调试)

运行步骤

  1. 在 HBuilderX 中打开项目
  2. 点击菜单栏:运行 → 运行到手机或模拟器 → 运行到 HarmonyOS
  3. 选择设备
    • 连接 HarmonyOS 真机(需开启开发者模式和 USB 调试)
    • 或使用 HarmonyOS 模拟器
  4. 等待编译:首次运行会自动下载依赖并编译
  5. 查看效果:应用会自动安装并启动到设备上

注意事项

  • 确保设备系统版本为 HarmonyOS5.0 或更高版本
  • 真机调试需要在设置中开启"开发者选项"和"USB调试"
  • 如遇到编译问题,请检查 HBuilderX 的 HarmonyOS 插件是否已安装

📖 使用说明

编码(文本 → 摩尔斯电码)

  1. 点击顶部的「文本 → 摩尔斯」按钮切换到编码模式
  2. 在输入框中输入要编码的文本(支持英文字母、数字和常用符号)
  3. 点击「编码」按钮
  4. 编码结果将显示在输出区域

注意事项

  • 字母之间用空格分隔
  • 单词之间用 / 分隔
  • 不支持的字符会被自动忽略

解码(摩尔斯电码 → 文本)

  1. 点击顶部的「摩尔斯 → 文本」按钮切换到解码模式
  2. 在输入框中输入摩尔斯电码
    • 使用空格分隔不同的字母
    • 使用 / 分隔不同的单词
  3. 点击「解码」按钮
  4. 解码结果将显示在输出区域

示例输入

.... . .-.. .-.. --- / .-- --- .-. .-.. -..

查看对照表

点击底部的「▶ 摩尔斯电码对照表」可展开完整的字符对照表,方便学习和参考。

🛠️ 技术栈

  • 框架:uni-app
  • 语言:TypeScript/UTS
  • UI:uni-app 组件库
  • 样式:CSS3(渐变、阴影、动画)

📂 项目结构

Morse/  
├── pages/  
│   └── index/  
│       └── index.uvue          # 主页面(摩尔斯转换器)  
├── static/  
│   └── logo.png                # 应用图标  
├── App.uvue                    # 应用配置  
├── main.uts                    # 入口文件  
├── manifest.json               # 应用配置清单  
├── pages.json                  # 页面路由配置  
├── uni.scss                    # 全局样式变量  
├── LICENSE                     # MIT 许可证  
└── README.md                   # 项目说明文档

🎯 核心代码说明

摩尔斯电码映射表

项目内置完整的摩尔斯电码映射表,包含:

  • 26个字母
  • 10个数字
  • 24个常用符号
morseCode: {  
    'A': '.-',    'B': '-...',  'C': '-.-.',  // ...  
    '0': '-----', '1': '.----', // ...  
    '.': '.-.-.-', ',': '--..--', // ...  
}

编码算法

将文本转换为摩尔斯电码的核心逻辑:

  1. 将输入文本转为大写
  2. 按空格分割成单词
  3. 遍历每个单词的字符,查找对应的摩尔斯电码
  4. 字母间用空格连接,单词间用 / 连接

解码算法

将摩尔斯电码转换为文本的核心逻辑:

  1. 创建反向映射表(摩尔斯 → 字符)
  2. / 分割成单词
  3. 每个单词按空格分割成字母
  4. 查找每个摩尔斯码对应的字符
  5. 无法识别的码用 ? 表示

🌟 特色亮点

  1. 智能容错:解码时遇到无法识别的码会用 ? 标记,不会中断整个转换过程
  2. 实时反馈:输入为空时会友好提示用户
  3. 一键清空:快速清除输入和输出内容
  4. 学习工具:内置对照表,既是工具也是学习资源
  5. 视觉设计:渐变背景、卡片阴影、动画效果,提供优秀的视觉体验

📱 平台支持

  • ✅ HarmonyOS App(鸿蒙原生应用)
    • 支持 HarmonyOS 5.0+
    • 完整的原生性能体验
    • 适配鸿蒙设计规范
  • ✅ H5(网页版)
  • ✅ 微信小程序
  • ✅ Android App
  • ✅ iOS App
  • ✅ 快应用
  • ✅ 其他 uni-app 支持的平台

🤝 贡献指南

欢迎提交 Issue 和 Pull Request!

  1. Fork 本项目
  2. 创建特性分支 (git checkout -b feature/AmazingFeature)
  3. 提交更改 (git commit -m 'Add some AmazingFeature')
  4. 推送到分支 (git push origin feature/AmazingFeature)
  5. 开启 Pull Request

📝 更新日志

v1.0.0 (2025-10-22)

  • ✨ 初始版本发布
  • 🎉 实现文本转摩尔斯电码功能
  • 🎉 实现摩尔斯电码转文本功能
  • 🎨 设计现代化 UI 界面
  • 📋 添加摩尔斯电码对照表
  • 🚀 支持 HarmonyOS 平台(鸿蒙原生应用)
  • 📱 多平台适配(H5、小程序、App 等)

🔮 未来计划

  • [ ] 添加音频播放功能(播放摩尔斯电码声音)
  • [ ] 支持闪光灯模式(用手机闪光灯展示摩尔斯电码)
  • [ ] 添加振动反馈(鸿蒙设备支持)
  • [ ] 添加历史记录功能
  • [ ] 支持更多语言(中文电码等)
  • [ ] 添加学习模式(摩尔斯电码训练)
  • [ ] 支持语音输入
  • [ ] 鸿蒙卡片服务(快速转换)
  • [ ] 适配鸿蒙折叠屏设备

❓ 常见问题

Q: 为什么有些字符无法转换?

A: 目前只支持英文字母、数字和常用标点符号。中文字符需要另外的电码系统(如中文电码)。

Q: 解码时出现问号是什么意思?

A: 表示该摩尔斯电码无法识别,可能是输入格式错误或不在支持的字符范围内。

Q: 如何正确输入摩尔斯电码?

A: 使用点 . 和横 - 组成字符,字符间用空格分隔,单词间用 / 分隔。

Q: 如何在 HarmonyOS 设备上安装?

A: 使用 HBuilderX 连接 HarmonyOS 设备,选择"运行到 HarmonyOS"即可自动编译并安装。确保设备已开启开发者模式和 USB 调试。

Q: 支持哪些 HarmonyOS 版本?

A: 支持 HarmonyOS 5.0 及以上版本,建议使用 HarmonyOS 5.0+ 以获得最佳体验。

📄 许可证

本项目采用 MIT 许可证 - 查看 LICENSE 文件了解详情

👨‍💻 作者

坚果

🙏 致谢

感谢 uni-app 团队提供的优秀跨平台框架!


⭐ 如果这个项目对你有帮助,请给它一个星标!

继续阅读 »

摩尔斯电码转换器 📡

一个基于 uni-app 开发的摩尔斯电码编解码应用,支持文本与摩尔斯电码的双向转换。

License
UniApp

✨ 功能特性

image-20251022144208411

核心功能

  • 🔤 文本转摩尔斯电码:将英文文本编码为摩尔斯电码
  • 🔡 摩尔斯电码转文本:将摩尔斯电码解码为可读文本
  • 🔄 双向转换:一键切换编码/解码模式
  • 📋 对照表:内置完整的摩尔斯电码对照表,可随时查阅

支持字符

  • ✅ 26个英文字母(A-Z)
  • ✅ 10个数字(0-9)
  • ✅ 常用标点符号:. , ? ' ! / ( ) & : ; = + - _ " $ @

界面特色

  • 🎨 现代化渐变设计
  • 📱 响应式布局,适配多种屏幕尺寸
  • 💫 流畅的动画过渡效果
  • 🌈 直观的视觉反馈
  • 📝 可选中复制输出结果

📸 预览

编码模式

将文本转换为摩尔斯电码:

输入:HELLO WORLD  
输出:.... . .-.. .-.. --- / .-- --- .-. .-.. -..

解码模式

将摩尔斯电码转换为文本:

输入:.... . .-.. .-.. --- / .-- --- .-. .-.. -..  
输出:HELLO WORLD

🚀 快速开始

环境要求

  • HBuilderX 3.0+(运行到 HarmonyOS 需要 5.0+ 版本)
  • uni-app 框架
  • 支持 uni-app 的运行环境:
    • 微信小程序:微信开发者工具
    • H5:现代浏览器(Chrome、Firefox、Safari 等)
    • Android/iOS:Android Studio / Xcode
    • HarmonyOS:HarmonyOS 5.0+ 设备或模拟器

安装步骤

  1. 克隆项目
git clone [your-repository-url]  
cd Morse
  1. 使用 HBuilderX 打开项目

    • 启动 HBuilderX
    • 文件 → 打开目录 → 选择项目文件夹
  2. 运行项目

    • 运行 → 运行到浏览器 → Chrome(H5)
    • 或运行到微信开发者工具(小程序)
    • 或运行到手机模拟器(App)
    • 或运行到 HarmonyOS(鸿蒙应用)

运行到 HarmonyOS

前置要求

  • 安装 HBuilderX 4.0+ 版本
  • 配置 HarmonyOS 开发环境
  • 安装 DevEco Studio(可选,用于更高级的调试)

运行步骤

  1. 在 HBuilderX 中打开项目
  2. 点击菜单栏:运行 → 运行到手机或模拟器 → 运行到 HarmonyOS
  3. 选择设备
    • 连接 HarmonyOS 真机(需开启开发者模式和 USB 调试)
    • 或使用 HarmonyOS 模拟器
  4. 等待编译:首次运行会自动下载依赖并编译
  5. 查看效果:应用会自动安装并启动到设备上

注意事项

  • 确保设备系统版本为 HarmonyOS5.0 或更高版本
  • 真机调试需要在设置中开启"开发者选项"和"USB调试"
  • 如遇到编译问题,请检查 HBuilderX 的 HarmonyOS 插件是否已安装

📖 使用说明

编码(文本 → 摩尔斯电码)

  1. 点击顶部的「文本 → 摩尔斯」按钮切换到编码模式
  2. 在输入框中输入要编码的文本(支持英文字母、数字和常用符号)
  3. 点击「编码」按钮
  4. 编码结果将显示在输出区域

注意事项

  • 字母之间用空格分隔
  • 单词之间用 / 分隔
  • 不支持的字符会被自动忽略

解码(摩尔斯电码 → 文本)

  1. 点击顶部的「摩尔斯 → 文本」按钮切换到解码模式
  2. 在输入框中输入摩尔斯电码
    • 使用空格分隔不同的字母
    • 使用 / 分隔不同的单词
  3. 点击「解码」按钮
  4. 解码结果将显示在输出区域

示例输入

.... . .-.. .-.. --- / .-- --- .-. .-.. -..

查看对照表

点击底部的「▶ 摩尔斯电码对照表」可展开完整的字符对照表,方便学习和参考。

🛠️ 技术栈

  • 框架:uni-app
  • 语言:TypeScript/UTS
  • UI:uni-app 组件库
  • 样式:CSS3(渐变、阴影、动画)

📂 项目结构

Morse/  
├── pages/  
│   └── index/  
│       └── index.uvue          # 主页面(摩尔斯转换器)  
├── static/  
│   └── logo.png                # 应用图标  
├── App.uvue                    # 应用配置  
├── main.uts                    # 入口文件  
├── manifest.json               # 应用配置清单  
├── pages.json                  # 页面路由配置  
├── uni.scss                    # 全局样式变量  
├── LICENSE                     # MIT 许可证  
└── README.md                   # 项目说明文档

🎯 核心代码说明

摩尔斯电码映射表

项目内置完整的摩尔斯电码映射表,包含:

  • 26个字母
  • 10个数字
  • 24个常用符号
morseCode: {  
    'A': '.-',    'B': '-...',  'C': '-.-.',  // ...  
    '0': '-----', '1': '.----', // ...  
    '.': '.-.-.-', ',': '--..--', // ...  
}

编码算法

将文本转换为摩尔斯电码的核心逻辑:

  1. 将输入文本转为大写
  2. 按空格分割成单词
  3. 遍历每个单词的字符,查找对应的摩尔斯电码
  4. 字母间用空格连接,单词间用 / 连接

解码算法

将摩尔斯电码转换为文本的核心逻辑:

  1. 创建反向映射表(摩尔斯 → 字符)
  2. / 分割成单词
  3. 每个单词按空格分割成字母
  4. 查找每个摩尔斯码对应的字符
  5. 无法识别的码用 ? 表示

🌟 特色亮点

  1. 智能容错:解码时遇到无法识别的码会用 ? 标记,不会中断整个转换过程
  2. 实时反馈:输入为空时会友好提示用户
  3. 一键清空:快速清除输入和输出内容
  4. 学习工具:内置对照表,既是工具也是学习资源
  5. 视觉设计:渐变背景、卡片阴影、动画效果,提供优秀的视觉体验

📱 平台支持

  • ✅ HarmonyOS App(鸿蒙原生应用)
    • 支持 HarmonyOS 5.0+
    • 完整的原生性能体验
    • 适配鸿蒙设计规范
  • ✅ H5(网页版)
  • ✅ 微信小程序
  • ✅ Android App
  • ✅ iOS App
  • ✅ 快应用
  • ✅ 其他 uni-app 支持的平台

🤝 贡献指南

欢迎提交 Issue 和 Pull Request!

  1. Fork 本项目
  2. 创建特性分支 (git checkout -b feature/AmazingFeature)
  3. 提交更改 (git commit -m 'Add some AmazingFeature')
  4. 推送到分支 (git push origin feature/AmazingFeature)
  5. 开启 Pull Request

📝 更新日志

v1.0.0 (2025-10-22)

  • ✨ 初始版本发布
  • 🎉 实现文本转摩尔斯电码功能
  • 🎉 实现摩尔斯电码转文本功能
  • 🎨 设计现代化 UI 界面
  • 📋 添加摩尔斯电码对照表
  • 🚀 支持 HarmonyOS 平台(鸿蒙原生应用)
  • 📱 多平台适配(H5、小程序、App 等)

🔮 未来计划

  • [ ] 添加音频播放功能(播放摩尔斯电码声音)
  • [ ] 支持闪光灯模式(用手机闪光灯展示摩尔斯电码)
  • [ ] 添加振动反馈(鸿蒙设备支持)
  • [ ] 添加历史记录功能
  • [ ] 支持更多语言(中文电码等)
  • [ ] 添加学习模式(摩尔斯电码训练)
  • [ ] 支持语音输入
  • [ ] 鸿蒙卡片服务(快速转换)
  • [ ] 适配鸿蒙折叠屏设备

❓ 常见问题

Q: 为什么有些字符无法转换?

A: 目前只支持英文字母、数字和常用标点符号。中文字符需要另外的电码系统(如中文电码)。

Q: 解码时出现问号是什么意思?

A: 表示该摩尔斯电码无法识别,可能是输入格式错误或不在支持的字符范围内。

Q: 如何正确输入摩尔斯电码?

A: 使用点 . 和横 - 组成字符,字符间用空格分隔,单词间用 / 分隔。

Q: 如何在 HarmonyOS 设备上安装?

A: 使用 HBuilderX 连接 HarmonyOS 设备,选择"运行到 HarmonyOS"即可自动编译并安装。确保设备已开启开发者模式和 USB 调试。

Q: 支持哪些 HarmonyOS 版本?

A: 支持 HarmonyOS 5.0 及以上版本,建议使用 HarmonyOS 5.0+ 以获得最佳体验。

📄 许可证

本项目采用 MIT 许可证 - 查看 LICENSE 文件了解详情

👨‍💻 作者

坚果

🙏 致谢

感谢 uni-app 团队提供的优秀跨平台框架!


⭐ 如果这个项目对你有帮助,请给它一个星标!

收起阅读 »

经验分享 鸿蒙通过 WebView 打开页面渲染成桌面 pc 模式怎么办?

鸿蒙next 鸿蒙征文

鸿蒙开发时候可使用 WebView 组件加载网页,展示网页内容并通信。

历史改动

在 HBuilderX 4.81 之后, uniapp 使用 WebView 展示在线网页时候,会默认添加 metaViwe=true,读取并启用 meta viewport 字段。

userAgent 适配

还有一部分网页是读取的 useragent 属性,通过特征判断再渲染展示网页,有的 isMobile 的判断里缺少鸿蒙的判断,只判断了 iphone/ipad/android 等字段,没有判断 OpenHarmony AkWeb 字段,如果是这种响应式展示移动端的方案,一方面可以更新 isMobile 的判断,添加对 harmony 的解析。另一方面可以在 HBuilderX 的 mainfest.json 中主动设置 UserAgent 来规避这个问题。

按照下面操作步骤:
打开 mianfest.json 切换到源码模式,找到 app-harmony 字段,追加下面字段

{"useragent":{"value":"Android","concatenate" : true}}

这下面在系统默认的 UserAgent 之后追加 Android 字段,通过这种方式主动适配网页。

uniapp 默认的 userAgent

Mozilla/5.0 (Phone; OpenHarmony 5.1)  
AppleWebKit/537.36 (KHTML, like Gecko)  
Chrome/114.0.0.0 Safari/537.36 ArkWeb/5.1.0.211  
Mobile uni-app
继续阅读 »

鸿蒙开发时候可使用 WebView 组件加载网页,展示网页内容并通信。

历史改动

在 HBuilderX 4.81 之后, uniapp 使用 WebView 展示在线网页时候,会默认添加 metaViwe=true,读取并启用 meta viewport 字段。

userAgent 适配

还有一部分网页是读取的 useragent 属性,通过特征判断再渲染展示网页,有的 isMobile 的判断里缺少鸿蒙的判断,只判断了 iphone/ipad/android 等字段,没有判断 OpenHarmony AkWeb 字段,如果是这种响应式展示移动端的方案,一方面可以更新 isMobile 的判断,添加对 harmony 的解析。另一方面可以在 HBuilderX 的 mainfest.json 中主动设置 UserAgent 来规避这个问题。

按照下面操作步骤:
打开 mianfest.json 切换到源码模式,找到 app-harmony 字段,追加下面字段

{"useragent":{"value":"Android","concatenate" : true}}

这下面在系统默认的 UserAgent 之后追加 Android 字段,通过这种方式主动适配网页。

uniapp 默认的 userAgent

Mozilla/5.0 (Phone; OpenHarmony 5.1)  
AppleWebKit/537.36 (KHTML, like Gecko)  
Chrome/114.0.0.0 Safari/537.36 ArkWeb/5.1.0.211  
Mobile uni-app
收起阅读 »

uniappx插件nutpi-idcard 开发与使用指南(适配鸿蒙)

鸿蒙征文

uniappx插件nutpi-idcard 开发与使用指南(适配鸿蒙)

前言

nutpi-idcard 是一个基于 UTS (uni-app TypeScript Syntax) 开发的 uni-app 插件适配鸿蒙,主要用于解析身份证号码,提取其中的关键信息,如地区、出生日期、性别等。本插件支持中国居民身份证、港澳台居民居住证以及外国人永久居留身份证。

本文将详细介绍 nutpi-idcard 插件的开发过程和使用方法,希望能为其他开发者提供一些参考。

插件功能

  • 身份证号码解析:能够从身份证号码中提取省市区(或国家/地区)、出生日期、性别等信息。
  • 支持多种证件类型
    • 中国居民身份证
    • 港澳台居民居住证
    • 外国人永久居留身份证
  • 纯 UTS 实现:确保了插件在 uni-app x 及其他支持 UTS 的环境中的兼容性和性能。
  • 跨平台支持:理论上支持所有 uni-app 支持的平台,特别是针对 App (Android, iOS, HarmonyOS) 进行了适配。

开发过程

1. 项目初始化与环境搭建

插件的开发基于 HBuilderX,利用其对 uni-app 和 UTS 的良好支持。

  • 创建 uni-app 项目:首先,创建一个标准的 uni-app 项目(如果还没有的话)。
  • 创建 uni_module:在项目根目录下创建 uni_modules 文件夹(如果不存在),然后在其中创建 nutpi-idcard 文件夹作为插件的根目录。
  • 配置文件 package.json:在 nutpi-idcard 目录下创建 package.json 文件,用于定义插件的基本信息、依赖、平台支持等。关键配置项包括:
    • id: 插件的唯一标识。
    • displayName: 插件在 HBuilderX 中显示的名称。
    • version: 插件版本号。
    • description: 插件描述。
    • author: 作者信息-坚果派。
    • contact: 联系方式。
    • repository: 代码仓库地址。
    • engines: HBuilderX 版本要求。
    • dcloudext: DCloud 扩展配置,如插件类型 (uts)、销售信息等。
    • uni_modules: uni-app 模块配置,如依赖、加密、平台支持等。

2. 核心逻辑实现 (utssdk)

插件的核心代码位于 utssdk 目录下,针对不同平台可以有不同的实现,但本项目中主要关注通用的 UTS 实现,特别是针对 HarmonyOS 的适配。

  • 目录结构

    nutpi-idcard/  
    ├── utssdk/  
    │   ├── app-harmony/         # HarmonyOS 平台特定代码  
    │   │   ├── index.uts        # HarmonyOS 入口及核心逻辑  
    │   │   ├── interfaces.uts   # TypeScript 接口定义  
    │   │   └── module/  
    │   │       └── data/        # 数据文件 (行政区划、国家代码)  
    │   │           ├── china.uts  
    │   │           └── international.uts  
    │   ├── app-android/       # Android 平台 (如果需要特定实现)  
    │   ├── app-ios/           # iOS 平台 (如果需要特定实现)  
    │   ├── index.uts          # 插件主入口 (通常导出各平台实现)  
    │   └── interfaces.uts     # 通用接口定义  
    ├── package.json  
    ├── readme.md  
    └── changelog.md  
  • 数据准备 (module/data/)

    • china.uts: 存储中国行政区划代码与名称的映射。
    • international.uts: 存储 ISO 3166-1 国家代码与名称的映射。
  • 接口定义 (interfaces.uts)
    定义了身份证解析结果的数据结构 IDResult

    export interface IDResult {  
      type?: string;       // 证件类型  
      sign?: string;       // 签发机关或地区  
      country?: string;    // 国家或地区  
      birthday?: string;   // 出生日期 (YYYY-MM-DD)  
      sex?: string;        // 性别 ('男' 或 '女')  
      isValid?: boolean;   // 校验结果 (当前版本简单返回 true)  
    }  
  • 核心解析逻辑 (app-harmony/index.uts)
    这是插件的核心,包含了主要的解析函数。

    • parseID(id: string): IDResult: 公开的 API 函数,根据身份证号码的格式(通过正则表达式判断)调用相应的内部解析函数。
    • parserChina(id: string): IDResult: 解析中国居民身份证和港澳台居民居住证。
    • 通过身份证号码的前6位确定省市区。
    • 通过第7到14位确定出生日期。
    • 通过第17位(顺序码的最后一位)确定性别。
    • parserInternational(id: string): IDResult: 解析外国人永久居留身份证。
    • 通过第1到3位(国家或地区代码)和 international.uts 数据确定国家。
    • 通过第7到14位确定出生日期。
    • 通过第17位确定性别。
    • isIdCardValidInternal(id: string): boolean: 身份证号码有效性校验函数。目前简单返回 true,未来可以根据国家标准实现更复杂的校验逻辑(如校验码计算)。
    // idcard/uni_modules/nutpi-idcard/utssdk/app-harmony/index.uts  
    import { chinaData as _china } from './module/data/china.uts';  
    import { internationalData as _international } from './module/data/international.uts';  
    import type { IDResult } from './interfaces.uts';  
    
    function parserInternational(id: string): IDResult { /* ... */ }  
    function parserChina(id: string): IDResult { /* ... */ }  
    function isIdCardValidInternal(id: string): boolean { /* ... */ }  
    
    export function parseID(id: string): IDResult {  
      if(id.match(/^9\d{16}[0-9xX]$/)){ // 外国人永久居留身份证特征 (假设以9开头)  
          return parserInternational(id);  
      }else if(id.match(/^\d{17}[0-9xX]$/)){ // 中国居民身份证特征  
          return parserChina(id);  
      }else{  
          return { type: '未知类型' };  
      }  
    }  

3. 插件入口 (index.uts)

nutpi-idcard 根目录下的 index.uts 文件通常作为插件的统一入口,它会根据当前运行平台导出相应平台的 parseID 函数。

// idcard/uni_modules/nutpi-idcard/index.uts  
// #ifdef APP-HARMONY  
export * from './utssdk/app-harmony/index.uts';  
// #endif  

// #ifdef APP-PLUS || APP-VUE  
// 假设 Android 和 iOS 使用相同的 UTS 逻辑,或者有单独的 app-android/index.uts 和 app-ios/index.uts  
// 如果 utssdk/index.uts 包含了 Android 和 iOS 的通用逻辑,可以这样导出:  
// export * from './utssdk/index.uts';   
// 或者分别导出  
// #ifdef APP-ANDROID  
// export * from './utssdk/app-android/index.uts';  
// #endif  
// #ifdef APP-IOS  
// export * from './utssdk/app-ios/index.uts';  
// #endif  
// #endif  

// 默认导出 (如果需要在非特定App平台使用,或者作为H5等平台的兜底)  
// export * from './utssdk/index.uts'; // 假设 utssdk/index.uts 包含通用或web实现

注意:上述 index.uts 的条件编译部分需要根据实际支持的平台和代码组织来编写。如果主要目标是 HarmonyOS,则 APP-HARMONY 部分是关键。

4. 文档编写

  • readme.md: 提供插件的详细说明,包括功能特性、安装方法、API 文档、使用示例、作者信息等。
  • changelog.md: 记录插件的版本更新历史和主要变更。

5. 测试与调试

  • 在 HBuilderX 中创建测试页面,引入插件并调用 parseID 函数,传入不同的身份证号码进行测试。
  • 关注控制台输出,确保解析结果的准确性。
  • 针对不同平台(特别是 HarmonyOS)进行真机或模拟器测试。

遇到的问题与解决

  • UTS 模块导入路径:UTS 中模块导入路径需要精确。最初可能因为 method.utsindex.uts 的拆分导致函数重复声明或找不到定义的问题。通过将 method.uts 的内容合并到 index.uts 中解决了此问题。
  • Git 推送标签失败:在版本发布时,如果本地没有对应的 Git 标签,git push origin <tagname> 会失败。通过先执行 git tag <tagname> 创建本地标签,然后再推送解决。
  • 函数未定义错误:在页面中调用插件函数时,如果导入路径不正确或插件未正确导出函数,会导致 xxx is not defined 错误。仔细检查插件的 index.uts 导出逻辑和页面中的导入路径,确保一致。

如何使用 nutpi-idcard 插件

  1. 安装插件

    • 从 DCloud 插件市场安装。插件地址:https://ext.dcloud.net.cn/plugin?id=23728
    • 或者,如果手动引入,将 nutpi-idcard 整个文件夹复制到你的 uni-app 项目的 uni_modules 目录下。
  2. 引入插件:在需要使用的页面或组件的 <script setup lang="uts"><script lang="uts"> 中引入插件。

    // 示例:在页面的 <script setup lang="uts"> 中  
    import { parseID } from '@/uni_modules/nutpi-idcard'; // HBuilderX 会自动处理路径映射  
    // 如果在 uni-app x 项目的 .uvue 文件中,路径可能需要更明确,或者依赖 HBuilderX 的智能提示  
  3. 调用解析函数:使用 parseID 函数解析身份证号码。

    const idNumber = '110101199003070978'; // 替换为实际的身份证号码  
    const idInfo = parseID(idNumber);  
    
    if (idInfo) {  
       console.log('证件类型:', idInfo.type);  
       console.log('签发地/国家:', idInfo.sign ?? idInfo.country);  
       console.log('出生日期:', idInfo.birthday);  
       console.log('性别:', idInfo.sex);  
       console.log('是否有效:', idInfo.isValid);  
    }  

API 参考

parseID(id: string): IDResult

解析身份证号码并返回包含详细信息的对象。

  • 参数

    • id: string - 需要解析的身份证号码(18位中国居民身份证,或外国人永久居留身份证等)。
  • 返回值IDResult 对象,其结构如下:

    interface IDResult {  
      type?: string;       // 证件类型 (例如:'居民身份证', '外国人永久居留身份证', '港澳台居民居住证', '未知类型')  
      sign?: string;       // 签发机关或地区信息 (例如:'北京市市辖区', '北京市朝阳区')  
      country?: string;    // 国家或地区 (例如:'中国', '无国籍' 或其他国家名称,主要用于外国人身份证)  
      birthday?: string;   // 出生日期,格式为 'YYYY-MM-DD'  
      sex?: string;        // 性别 ('男' 或 '女')  
      isValid?: boolean;   // 身份证号码是否有效 (当前版本简单返回true,待实现详细校验逻辑)  
    }  

未来展望

  • 完善校验逻辑:实现更严格的身份证号码校验,包括校验码的计算与验证。
  • 更广泛的证件类型支持:考虑支持更多国家或地区的身份证件类型。
  • 性能优化:对数据查找和字符串处理进行优化,提高解析效率。
  • 更详细的错误提示:当输入格式错误或无法解析时,提供更具体的错误信息。
  • 单元测试:为插件编写完善的单元测试,确保代码质量和稳定性。

作者与联系方式

希望这个插件能对您有所帮助!如果您有任何问题或建议,欢迎联系。

相关链接

继续阅读 »

uniappx插件nutpi-idcard 开发与使用指南(适配鸿蒙)

前言

nutpi-idcard 是一个基于 UTS (uni-app TypeScript Syntax) 开发的 uni-app 插件适配鸿蒙,主要用于解析身份证号码,提取其中的关键信息,如地区、出生日期、性别等。本插件支持中国居民身份证、港澳台居民居住证以及外国人永久居留身份证。

本文将详细介绍 nutpi-idcard 插件的开发过程和使用方法,希望能为其他开发者提供一些参考。

插件功能

  • 身份证号码解析:能够从身份证号码中提取省市区(或国家/地区)、出生日期、性别等信息。
  • 支持多种证件类型
    • 中国居民身份证
    • 港澳台居民居住证
    • 外国人永久居留身份证
  • 纯 UTS 实现:确保了插件在 uni-app x 及其他支持 UTS 的环境中的兼容性和性能。
  • 跨平台支持:理论上支持所有 uni-app 支持的平台,特别是针对 App (Android, iOS, HarmonyOS) 进行了适配。

开发过程

1. 项目初始化与环境搭建

插件的开发基于 HBuilderX,利用其对 uni-app 和 UTS 的良好支持。

  • 创建 uni-app 项目:首先,创建一个标准的 uni-app 项目(如果还没有的话)。
  • 创建 uni_module:在项目根目录下创建 uni_modules 文件夹(如果不存在),然后在其中创建 nutpi-idcard 文件夹作为插件的根目录。
  • 配置文件 package.json:在 nutpi-idcard 目录下创建 package.json 文件,用于定义插件的基本信息、依赖、平台支持等。关键配置项包括:
    • id: 插件的唯一标识。
    • displayName: 插件在 HBuilderX 中显示的名称。
    • version: 插件版本号。
    • description: 插件描述。
    • author: 作者信息-坚果派。
    • contact: 联系方式。
    • repository: 代码仓库地址。
    • engines: HBuilderX 版本要求。
    • dcloudext: DCloud 扩展配置,如插件类型 (uts)、销售信息等。
    • uni_modules: uni-app 模块配置,如依赖、加密、平台支持等。

2. 核心逻辑实现 (utssdk)

插件的核心代码位于 utssdk 目录下,针对不同平台可以有不同的实现,但本项目中主要关注通用的 UTS 实现,特别是针对 HarmonyOS 的适配。

  • 目录结构

    nutpi-idcard/  
    ├── utssdk/  
    │   ├── app-harmony/         # HarmonyOS 平台特定代码  
    │   │   ├── index.uts        # HarmonyOS 入口及核心逻辑  
    │   │   ├── interfaces.uts   # TypeScript 接口定义  
    │   │   └── module/  
    │   │       └── data/        # 数据文件 (行政区划、国家代码)  
    │   │           ├── china.uts  
    │   │           └── international.uts  
    │   ├── app-android/       # Android 平台 (如果需要特定实现)  
    │   ├── app-ios/           # iOS 平台 (如果需要特定实现)  
    │   ├── index.uts          # 插件主入口 (通常导出各平台实现)  
    │   └── interfaces.uts     # 通用接口定义  
    ├── package.json  
    ├── readme.md  
    └── changelog.md  
  • 数据准备 (module/data/)

    • china.uts: 存储中国行政区划代码与名称的映射。
    • international.uts: 存储 ISO 3166-1 国家代码与名称的映射。
  • 接口定义 (interfaces.uts)
    定义了身份证解析结果的数据结构 IDResult

    export interface IDResult {  
      type?: string;       // 证件类型  
      sign?: string;       // 签发机关或地区  
      country?: string;    // 国家或地区  
      birthday?: string;   // 出生日期 (YYYY-MM-DD)  
      sex?: string;        // 性别 ('男' 或 '女')  
      isValid?: boolean;   // 校验结果 (当前版本简单返回 true)  
    }  
  • 核心解析逻辑 (app-harmony/index.uts)
    这是插件的核心,包含了主要的解析函数。

    • parseID(id: string): IDResult: 公开的 API 函数,根据身份证号码的格式(通过正则表达式判断)调用相应的内部解析函数。
    • parserChina(id: string): IDResult: 解析中国居民身份证和港澳台居民居住证。
    • 通过身份证号码的前6位确定省市区。
    • 通过第7到14位确定出生日期。
    • 通过第17位(顺序码的最后一位)确定性别。
    • parserInternational(id: string): IDResult: 解析外国人永久居留身份证。
    • 通过第1到3位(国家或地区代码)和 international.uts 数据确定国家。
    • 通过第7到14位确定出生日期。
    • 通过第17位确定性别。
    • isIdCardValidInternal(id: string): boolean: 身份证号码有效性校验函数。目前简单返回 true,未来可以根据国家标准实现更复杂的校验逻辑(如校验码计算)。
    // idcard/uni_modules/nutpi-idcard/utssdk/app-harmony/index.uts  
    import { chinaData as _china } from './module/data/china.uts';  
    import { internationalData as _international } from './module/data/international.uts';  
    import type { IDResult } from './interfaces.uts';  
    
    function parserInternational(id: string): IDResult { /* ... */ }  
    function parserChina(id: string): IDResult { /* ... */ }  
    function isIdCardValidInternal(id: string): boolean { /* ... */ }  
    
    export function parseID(id: string): IDResult {  
      if(id.match(/^9\d{16}[0-9xX]$/)){ // 外国人永久居留身份证特征 (假设以9开头)  
          return parserInternational(id);  
      }else if(id.match(/^\d{17}[0-9xX]$/)){ // 中国居民身份证特征  
          return parserChina(id);  
      }else{  
          return { type: '未知类型' };  
      }  
    }  

3. 插件入口 (index.uts)

nutpi-idcard 根目录下的 index.uts 文件通常作为插件的统一入口,它会根据当前运行平台导出相应平台的 parseID 函数。

// idcard/uni_modules/nutpi-idcard/index.uts  
// #ifdef APP-HARMONY  
export * from './utssdk/app-harmony/index.uts';  
// #endif  

// #ifdef APP-PLUS || APP-VUE  
// 假设 Android 和 iOS 使用相同的 UTS 逻辑,或者有单独的 app-android/index.uts 和 app-ios/index.uts  
// 如果 utssdk/index.uts 包含了 Android 和 iOS 的通用逻辑,可以这样导出:  
// export * from './utssdk/index.uts';   
// 或者分别导出  
// #ifdef APP-ANDROID  
// export * from './utssdk/app-android/index.uts';  
// #endif  
// #ifdef APP-IOS  
// export * from './utssdk/app-ios/index.uts';  
// #endif  
// #endif  

// 默认导出 (如果需要在非特定App平台使用,或者作为H5等平台的兜底)  
// export * from './utssdk/index.uts'; // 假设 utssdk/index.uts 包含通用或web实现

注意:上述 index.uts 的条件编译部分需要根据实际支持的平台和代码组织来编写。如果主要目标是 HarmonyOS,则 APP-HARMONY 部分是关键。

4. 文档编写

  • readme.md: 提供插件的详细说明,包括功能特性、安装方法、API 文档、使用示例、作者信息等。
  • changelog.md: 记录插件的版本更新历史和主要变更。

5. 测试与调试

  • 在 HBuilderX 中创建测试页面,引入插件并调用 parseID 函数,传入不同的身份证号码进行测试。
  • 关注控制台输出,确保解析结果的准确性。
  • 针对不同平台(特别是 HarmonyOS)进行真机或模拟器测试。

遇到的问题与解决

  • UTS 模块导入路径:UTS 中模块导入路径需要精确。最初可能因为 method.utsindex.uts 的拆分导致函数重复声明或找不到定义的问题。通过将 method.uts 的内容合并到 index.uts 中解决了此问题。
  • Git 推送标签失败:在版本发布时,如果本地没有对应的 Git 标签,git push origin <tagname> 会失败。通过先执行 git tag <tagname> 创建本地标签,然后再推送解决。
  • 函数未定义错误:在页面中调用插件函数时,如果导入路径不正确或插件未正确导出函数,会导致 xxx is not defined 错误。仔细检查插件的 index.uts 导出逻辑和页面中的导入路径,确保一致。

如何使用 nutpi-idcard 插件

  1. 安装插件

    • 从 DCloud 插件市场安装。插件地址:https://ext.dcloud.net.cn/plugin?id=23728
    • 或者,如果手动引入,将 nutpi-idcard 整个文件夹复制到你的 uni-app 项目的 uni_modules 目录下。
  2. 引入插件:在需要使用的页面或组件的 <script setup lang="uts"><script lang="uts"> 中引入插件。

    // 示例:在页面的 <script setup lang="uts"> 中  
    import { parseID } from '@/uni_modules/nutpi-idcard'; // HBuilderX 会自动处理路径映射  
    // 如果在 uni-app x 项目的 .uvue 文件中,路径可能需要更明确,或者依赖 HBuilderX 的智能提示  
  3. 调用解析函数:使用 parseID 函数解析身份证号码。

    const idNumber = '110101199003070978'; // 替换为实际的身份证号码  
    const idInfo = parseID(idNumber);  
    
    if (idInfo) {  
       console.log('证件类型:', idInfo.type);  
       console.log('签发地/国家:', idInfo.sign ?? idInfo.country);  
       console.log('出生日期:', idInfo.birthday);  
       console.log('性别:', idInfo.sex);  
       console.log('是否有效:', idInfo.isValid);  
    }  

API 参考

parseID(id: string): IDResult

解析身份证号码并返回包含详细信息的对象。

  • 参数

    • id: string - 需要解析的身份证号码(18位中国居民身份证,或外国人永久居留身份证等)。
  • 返回值IDResult 对象,其结构如下:

    interface IDResult {  
      type?: string;       // 证件类型 (例如:'居民身份证', '外国人永久居留身份证', '港澳台居民居住证', '未知类型')  
      sign?: string;       // 签发机关或地区信息 (例如:'北京市市辖区', '北京市朝阳区')  
      country?: string;    // 国家或地区 (例如:'中国', '无国籍' 或其他国家名称,主要用于外国人身份证)  
      birthday?: string;   // 出生日期,格式为 'YYYY-MM-DD'  
      sex?: string;        // 性别 ('男' 或 '女')  
      isValid?: boolean;   // 身份证号码是否有效 (当前版本简单返回true,待实现详细校验逻辑)  
    }  

未来展望

  • 完善校验逻辑:实现更严格的身份证号码校验,包括校验码的计算与验证。
  • 更广泛的证件类型支持:考虑支持更多国家或地区的身份证件类型。
  • 性能优化:对数据查找和字符串处理进行优化,提高解析效率。
  • 更详细的错误提示:当输入格式错误或无法解析时,提供更具体的错误信息。
  • 单元测试:为插件编写完善的单元测试,确保代码质量和稳定性。

作者与联系方式

希望这个插件能对您有所帮助!如果您有任何问题或建议,欢迎联系。

相关链接

收起阅读 »