欢迎大家加入开源鸿蒙跨平台开发者社区,一起共建开源鸿蒙跨平台生态。

在这里插入图片描述

📌 模块概述

添加影片模块是MovieTracker应用中用于创建新影片记录的功能。用户可以通过表单输入影片的各种信息,如标题、导演、演员、年份、评分、分类、标签等。添加影片是应用的核心功能,用户通过这个模块来构建自己的影片库。

该模块的主要功能包括:影片表单输入、数据验证、海报上传、分类和标签选择、影片状态设置等。通过Cordova框架与OpenHarmony原生能力的结合,实现了完整的影片创建流程和文件上传功能。

添加影片需要处理多种数据类型的输入,包括文本、日期、数字、文件等。同时需要进行数据验证,确保输入数据的有效性。

🔗 完整流程

第一步:表单设计与数据输入

添加影片页面需要提供一个完整的表单,包含影片的所有必要信息。表单需要进行分组,将相关的字段组织在一起,提高用户的输入效率。

表单设计需要考虑用户体验,提供清晰的标签、占位符文本、帮助提示等。同时需要支持必填字段的标记,告知用户哪些字段是必须填写的。

第二步:数据验证与错误处理

用户提交表单前需要进行数据验证。验证包括检查必填字段是否为空、检查数据格式是否正确、检查字段值是否在允许的范围内等。

验证失败时需要显示清晰的错误消息,告知用户具体的错误位置和原因,帮助用户快速修正错误。

第三步:海报上传与处理

用户可以为影片上传海报图片。上传过程需要处理文件选择、文件验证、文件上传等步骤。上传的图片需要进行处理,如缩放、压缩等,以节省存储空间。

上传过程需要显示进度提示,告知用户上传的进度。上传完成后需要显示上传的图片预览。

🔧 Web代码实现

添加影片HTML结构

<div id="add-movie-page" class="page">
    <div class="page-header">
        <h2>添加影片</h2>
    </div>
    
    <form id="add-movie-form" class="movie-form">
        <div class="form-section">
            <h3>基本信息</h3>
            
            <div class="form-group">
                <label>影片标题 *</label>
                <input type="text" id="movie-title" placeholder="请输入影片标题" class="form-input" required>
            </div>
            
            <div class="form-group">
                <label>导演 *</label>
                <input type="text" id="movie-director" placeholder="请输入导演名称" class="form-input" required>
            </div>
            
            <div class="form-group">
                <label>演员</label>
                <input type="text" id="movie-actors" placeholder="多个演员用逗号分隔" class="form-input">
            </div>
            
            <div class="form-group">
                <label>年份 *</label>
                <input type="number" id="movie-year" placeholder="请输入年份" class="form-input" required>
            </div>
        </div>
        
        <div class="form-section">
            <h3>分类与标签</h3>
            
            <div class="form-group">
                <label>分类 *</label>
                <select id="movie-category" class="form-select" required>
                    <option value="">请选择分类</option>
                </select>
            </div>
            
            <div class="form-group">
                <label>标签</label>
                <div id="movie-tags" class="tags-select"></div>
            </div>
        </div>
        
        <div class="form-section">
            <h3>评分与描述</h3>
            
            <div class="form-group">
                <label>评分</label>
                <input type="number" id="movie-rating" placeholder="1-10" min="1" max="10" step="0.5" class="form-input">
            </div>
            
            <div class="form-group">
                <label>描述</label>
                <textarea id="movie-description" placeholder="请输入影片描述" class="form-textarea"></textarea>
            </div>
        </div>
        
        <div class="form-section">
            <h3>海报与状态</h3>
            
            <div class="form-group">
                <label>海报</label>
                <input type="file" id="movie-poster" accept="image/*" class="form-input">
                <div id="poster-preview" class="poster-preview"></div>
            </div>
            
            <div class="form-group">
                <label>状态 *</label>
                <select id="movie-status" class="form-select" required>
                    <option value="watchlist">想看</option>
                    <option value="watched">已看</option>
                </select>
            </div>
        </div>
        
        <div class="form-actions">
            <button type="submit" class="btn btn-primary">保存影片</button>
            <button type="button" class="btn btn-secondary" onclick="app.navigateTo('all-movies')">取消</button>
        </div>
    </form>
</div>

这个HTML结构定义了添加影片页面的布局。表单分为多个部分,包括基本信息、分类与标签、评分与描述、海报与状态。每个字段都有清晰的标签和占位符文本。

表单初始化与提交

async function initAddMovieForm() {
    try {
        // 加载分类
        const categories = await db.getAllCategories();
        const categorySelect = document.getElementById('movie-category');
        categories.forEach(cat => {
            const option = document.createElement('option');
            option.value = cat.id;
            option.textContent = cat.name;
            categorySelect.appendChild(option);
        });
        
        // 加载标签
        const tags = await db.getAllTags();
        const tagsContainer = document.getElementById('movie-tags');
        tags.forEach(tag => {
            const label = document.createElement('label');
            label.className = 'tag-checkbox-label';
            label.innerHTML = `
                <input type="checkbox" class="tag-checkbox" value="${tag.id}">
                <span>${tag.name}</span>
            `;
            tagsContainer.appendChild(label);
        });
        
        // 绑定表单提交事件
        document.getElementById('add-movie-form').addEventListener('submit', handleAddMovieSubmit);
        
        // 绑定海报上传事件
        document.getElementById('movie-poster').addEventListener('change', handlePosterUpload);
    } catch (error) {
        console.error('初始化表单失败:', error);
        showError('初始化表单失败');
    }
}

async function handleAddMovieSubmit(event) {
    event.preventDefault();
    
    // 验证表单
    const errors = validateMovieForm();
    if (errors.length > 0) {
        showError(errors.join('\n'));
        return;
    }
    
    try {
        const movie = {
            title: document.getElementById('movie-title').value,
            director: document.getElementById('movie-director').value,
            actors: document.getElementById('movie-actors').value,
            year: parseInt(document.getElementById('movie-year').value),
            categoryId: parseInt(document.getElementById('movie-category').value),
            rating: parseFloat(document.getElementById('movie-rating').value) || null,
            description: document.getElementById('movie-description').value,
            status: document.getElementById('movie-status').value,
            poster: document.getElementById('movie-poster').dataset.url || null,
            tags: getSelectedTags(),
            createdDate: new Date().toISOString()
        };
        
        await db.addMovie(movie);
        showSuccess('影片已添加');
        
        // 清空表单
        document.getElementById('add-movie-form').reset();
        
        // 返回影片列表
        setTimeout(() => {
            app.navigateTo('all-movies');
        }, 1000);
    } catch (error) {
        console.error('添加影片失败:', error);
        showError('添加影片失败');
    }
}

function validateMovieForm() {
    const errors = [];
    
    const title = document.getElementById('movie-title').value.trim();
    if (!title) {
        errors.push('影片标题不能为空');
    }
    
    const director = document.getElementById('movie-director').value.trim();
    if (!director) {
        errors.push('导演不能为空');
    }
    
    const year = parseInt(document.getElementById('movie-year').value);
    if (!year || year < 1900 || year > new Date().getFullYear() + 5) {
        errors.push('年份无效');
    }
    
    const category = document.getElementById('movie-category').value;
    if (!category) {
        errors.push('请选择分类');
    }
    
    const rating = parseFloat(document.getElementById('movie-rating').value);
    if (rating && (rating < 1 || rating > 10)) {
        errors.push('评分必须在1-10之间');
    }
    
    return errors;
}

function getSelectedTags() {
    const checkboxes = document.querySelectorAll('.tag-checkbox:checked');
    return Array.from(checkboxes).map(cb => parseInt(cb.value));
}

这个函数实现了表单的初始化和提交处理。initAddMovieForm()加载分类和标签,绑定事件处理器。handleAddMovieSubmit()处理表单提交,进行数据验证和保存。validateMovieForm()验证表单数据的有效性。

海报上传处理

function handlePosterUpload(event) {
    const file = event.target.files[0];
    if (!file) return;
    
    // 验证文件类型
    if (!file.type.startsWith('image/')) {
        showError('请选择图片文件');
        return;
    }
    
    // 验证文件大小(限制为5MB)
    if (file.size > 5 * 1024 * 1024) {
        showError('图片大小不能超过5MB');
        return;
    }
    
    // 读取文件并显示预览
    const reader = new FileReader();
    reader.onload = function(e) {
        const preview = document.getElementById('poster-preview');
        preview.innerHTML = `<img src="${e.target.result}" alt="海报预览">`;
        
        // 保存文件URL
        document.getElementById('movie-poster').dataset.url = e.target.result;
    };
    reader.readAsDataURL(file);
}

这个函数实现了海报上传和预览功能。验证文件类型和大小,然后读取文件并显示预览。

🔌 OpenHarmony原生代码

添加影片插件

// AddMoviePlugin.ets
import { webview } from '@kit.ArkWeb';
import { common } from '@kit.AbilityKit';
import { fileIo } from '@kit.CoreFileKit';

export class AddMoviePlugin {
    private context: common.UIAbilityContext;
    
    constructor(context: common.UIAbilityContext) {
        this.context = context;
    }
    
    public registerAddMovie(controller: webview.WebviewController): void {
        controller.registerJavaScriptProxy({
            object: new AddMovieBridge(this.context),
            name: 'addMovieNative',
            methodList: ['uploadPoster', 'validateMovieData']
        });
    }
}

这个OpenHarmony原生插件为添加影片提供了海报上传和数据验证功能。

海报上传实现

export class AddMovieBridge {
    private context: common.UIAbilityContext;
    
    constructor(context: common.UIAbilityContext) {
        this.context = context;
    }
    
    public uploadPoster(posterBase64: string, movieTitle: string): string {
        try {
            // 将Base64转换为文件
            const buffer = this.base64ToBuffer(posterBase64);
            
            // 生成文件名
            const timestamp = Date.now();
            const fileName = `poster_${movieTitle}_${timestamp}.jpg`;
            
            // 保存到应用缓存目录
            const cacheDir = this.context.cacheDir;
            const filePath = `${cacheDir}/${fileName}`;
            
            const file = fileIo.openSync(filePath, fileIo.OpenMode.CREATE | fileIo.OpenMode.WRITE);
            fileIo.writeSync(file.fd, buffer);
            fileIo.closeSync(file.fd);
            
            return JSON.stringify({
                success: true,
                filePath: filePath,
                fileName: fileName
            });
        } catch (error) {
            return JSON.stringify({
                success: false,
                error: error.message
            });
        }
    }
    
    public validateMovieData(movieJson: string): string {
        try {
            const movie = JSON.parse(movieJson);
            
            const errors: string[] = [];
            
            if (!movie.title || movie.title.trim().length === 0) {
                errors.push('影片标题不能为空');
            }
            
            if (!movie.director || movie.director.trim().length === 0) {
                errors.push('导演不能为空');
            }
            
            if (!movie.year || movie.year < 1900) {
                errors.push('年份无效');
            }
            
            if (movie.rating && (movie.rating < 1 || movie.rating > 10)) {
                errors.push('评分必须在1-10之间');
            }
            
            return JSON.stringify({
                valid: errors.length === 0,
                errors: errors
            });
        } catch (error) {
            return JSON.stringify({
                valid: false,
                error: error.message
            });
        }
    }
    
    private base64ToBuffer(base64: string): ArrayBuffer {
        const binaryString = atob(base64.split(',')[1]);
        const bytes = new Uint8Array(binaryString.length);
        for (let i = 0; i < binaryString.length; i++) {
            bytes[i] = binaryString.charCodeAt(i);
        }
        return bytes.buffer;
    }
}

这个类实现了海报上传和数据验证功能。uploadPoster()将Base64编码的图片保存到文件系统,validateMovieData()验证影片数据的有效性。

Web-Native通信

调用原生上传功能

async function uploadPosterToNative(posterBase64) {
    try {
        const movieTitle = document.getElementById('movie-title').value;
        
        if (window.addMovieNative) {
            const uploadResult = window.addMovieNative.uploadPoster(
                posterBase64,
                movieTitle
            );
            const result = JSON.parse(uploadResult);
            
            if (result.success) {
                document.getElementById('movie-poster').dataset.url = result.filePath;
                showSuccess('海报已上传');
            } else {
                showError(`上传失败: ${result.error}`);
            }
        }
    } catch (error) {
        console.error('上传海报失败:', error);
        showError('上传海报失败');
    }
}

async function validateBeforeSave() {
    try {
        const movie = {
            title: document.getElementById('movie-title').value,
            director: document.getElementById('movie-director').value,
            year: parseInt(document.getElementById('movie-year').value),
            rating: parseFloat(document.getElementById('movie-rating').value)
        };
        
        if (window.addMovieNative) {
            const validationResult = window.addMovieNative.validateMovieData(
                JSON.stringify(movie)
            );
            const result = JSON.parse(validationResult);
            
            if (!result.valid) {
                showError(result.errors.join('\n'));
                return false;
            }
        }
        
        return true;
    } catch (error) {
        console.error('验证失败:', error);
        return false;
    }
}

这个函数展示了如何调用OpenHarmony原生的上传和验证功能。在上传海报和保存影片前进行验证和上传处理。

📝 总结

添加影片模块展示了Cordova与OpenHarmony混合开发中的表单处理和文件上传功能。通过Web层提供完整的表单界面和用户交互,同时利用OpenHarmony原生能力进行文件上传和数据验证。

在实现这个模块时,需要注意表单数据的验证、文件上传的处理、以及用户体验的流畅性。通过合理的架构设计,可以构建出高效、易用的影片添加功能。

Logo

社区规范:仅讨论OpenHarmony相关问题。

更多推荐