# harmony-sino-traveller **Repository Path**: dev-edu/harmony-sino-traveller ## Basic Information - **Project Name**: harmony-sino-traveller - **Description**: 鸿蒙课程项目《华夏旅行者》课程资料 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 10 - **Created**: 2024-06-14 - **Last Updated**: 2025-05-29 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 一阶段案例 SinoTraveller ## 介绍 通过这个项目主要是想达到下面几个目的: - 巩固、熟悉ArkUI相关知识点 - 熟练使用ArkTS语法 - 通过自己实现一些ArkUI效果,大家可以慢慢的自己灵活玩转ArkUI ## 效果截图 | 首页 | 必游榜 | 目的地 | | :------------------------------------------------: | :---------------------------------------------: | :--------------------------------------------------: | | ![首页](./screenshots/image-20240606113839256.png) | ![å](./screenshots/image-20240606113945295.png) | ![目的地](./screenshots/image-20240606114034025.png) | ## 动图演示 | 首页 | 必游榜 | 目的地 | | :--------------------------: | :----------------------------: | :----------------------------: | | ![首页](./screenshots/1.gif) | ![必游榜](./screenshots/2.gif) | ![目的地](./screenshots/3.gif) | ## 主要功能 - Tabs组件导航与Swiper轮播 - LazyForEach实现数据懒加载 - 自定义实现带活动条的Tabs效果 - 自定义实现二级导航 - 自定义实现一镜切换效果 ## 运行条件 * DevEco 5.0.3.200 * SDK:API12以上 * HarmonyOS NEXT * Preview与模拟器(一镜切换效果需要) ## 数据与类型 ### 资源文件 **color.json** ```json { "color": [ { "name": "start_window_background", "value": "#FFFFFF" }, { "name": "basic_background", "value": "#F1F3F5" }, { "name": "color_bar_background", "value": "#3366FF" }, { "name": "color_svg_like", "value": "#f40" }, { "name": "color_svg_icon", "value": "#999" }, { "name": "color_tag_item_bg", "value": "#eee" }, { "name": "font_color_selected", "value": "#111111" }, { "name": "font_color_unselected", "value": "#999999" }, { "name": "list_tags_color", "value": "#BF8230" }, { "name": "list_tags_bg_color", "value": "#F9F4EC" }, { "name": "list_item_bg_color", "value": "#FDFFFC" }, { "name": "list_item_text_color", "value": "#717376" } ] } ``` **float.json** ```typescript { "float": [ { "name": "text_font_size_small", "value": "12fp" }, { "name": "text_font_size_normal", "value": "14fp" }, { "name": "text_font_size_medium", "value": "16fp" }, { "name": "text_font_size_large", "value": "18fp" }, { "name": "title_font_size_small", "value": "16fp" }, { "name": "title_font_size_normal", "value": "18fp" }, { "name": "title_font_size_medium", "value": "20fp" }, { "name": "title_font_size_large", "value": "24fp" }, { "name": "nav_font_size_small", "value": "14fp" }, { "name": "nav_font_size_normal", "value": "16fp" }, { "name": "nav_font_size_medium", "value": "18fp" }, { "name": "border_radius_small", "value": "4vp" }, { "name": "border_radius_normal", "value": "6vp" }, { "name": "border_radius_medium", "value": "8vp" }, { "name": "border_radius_large", "value": "12vp" }, { "name": "padding_size_small", "value": "4vp" }, { "name": "padding_size_normal", "value": "8vp" }, { "name": "padding_size_medium", "value": "12vp" }, { "name": "padding_size_large", "value": "16vp" }, { "name": "margin_size_small", "value": "4vp" }, { "name": "margin_size_normal", "value": "8vp" }, { "name": "margin_size_medium", "value": "12vp" }, { "name": "margin_size_large", "value": "16vp" }, { "name": "icon_size_small", "value": "14vp" }, { "name": "icon_size_normal", "value": "18vp" }, { "name": "icon_size_medium", "value": "20vp" }, { "name": "icon_size_large", "value": "24vp" }, { "name": "icon_size_huge", "value": "32vp" } ] } ``` **常量类CommonConstants.ets** ```typescript export default class CommonConstants{ static readonly FULL_LENGTH:string = '100%' static readonly LOGIN_BUTTON_WIDTH:string = '90%' static readonly COLUMN_SPACE:string = '20vp' static readonly LIKELY_LENGTH:string = '100vp' //percent static readonly PERCENT_8:string = '8%'; static readonly PERCENT_10:string = '10%'; static readonly PERCENT_20:string = '20%'; static readonly PERCENT_30:string = '30%'; static readonly PERCENT_40:string = '40%'; static readonly PERCENT_50:string = '50%'; static readonly PERCENT_60:string = '60%'; static readonly PERCENT_70:string = '70%'; static readonly PERCENT_80:string = '80%'; static readonly PERCENT_90:string = '90%'; static readonly PERCENT_94:string = '94%'; static readonly PERCENT_100:string = '100%'; // default_value static readonly DEFAULT_0:number = 0; static readonly DEFAULT_1:number = 1; static readonly DEFAULT_2:number = 2; static readonly DEFAULT_4:number = 4; static readonly DEFAULT_8:number = 8; static readonly DEFAULT_10:number = 10; static readonly DEFAULT_12:number = 12; static readonly DEFAULT_14:number = 14; static readonly DEFAULT_16:number = 16; static readonly DEFAULT_18:number = 18; static readonly DEFAULT_20:number = 20; static readonly DEFAULT_24:number = 24; static readonly DEFAULT_28:number = 28; static readonly DEFAULT_32:number = 32; static readonly DEFAULT_40:number = 40; static readonly DEFAULT_48:number = 48; static readonly DEFAULT_56:number = 56; static readonly DEFAULT_60:number = 60; static readonly DEFAULT_70:number = 70; static readonly DEFAULT_80:number = 80; static readonly DEFAULT_100:number = 100; static readonly DEFAULT_150:number = 150; static readonly DEFAULT_200:number = 200; static readonly DEFAULT_222:number = 222; static readonly DEFAULT_300:number = 300; } ``` ### tabs类型与数据 **类型** ```typescript export interface ITabBarDataType{ icon: ResourceStr iconSelected:ResourceStr name:ResourceStr text:ResourceStr } ``` **数据** ```typescript export const tabBarData: ITabBarDataType[] = [ { icon: $r("app.media.ic_tabs_index"), iconSelected: $r("app.media.ic_tabs_index_select"), name: "index", text: "首页" }, { icon: $r("app.media.ic_tabs_discover"), iconSelected: $r("app.media.ic_tabs_discover_select"), name: "must_visit_list", text: "必游榜" }, { icon: $r("app.media.ic_tabs_location"), iconSelected: $r("app.media.ic_tabs_location_select"), name: "destination", text: "目的地" }, { icon: $r("app.media.ic_tabs_collect"), iconSelected: $r("app.media.ic_tabs_collect_select"), name: "collect", text: "收藏" }, { icon: $r("app.media.ic_tabs_user"), iconSelected: $r("app.media.ic_tabs_user_select"), name: "user", text: "我的" }, ] ``` ### swiper数据 ```typescript export const swiperImages: ResourceStr[] = [ $r("app.media.ic_swiper_01"), $r("app.media.ic_swiper_02"), $r("app.media.ic_swiper_03"), $r("app.media.ic_swiper_04"), $r("app.media.ic_swiper_05"), $r("app.media.ic_swiper_06"), ] ``` ### travelNotes类型与数据 **类型** ```typescript @Observed export class TravelNotes{ id:number // 游记id avatar: ResourceStr // 游记作者头像 author: string // 游记作者名称 title: string // 游记标题 content: string // 游记内容 cover:ResourceStr // 游记封面 time: string // 发表时间 location: string // 游记地点 likeNum: number // 点赞数量 watchNum: number // 阅读数量 likeFlag: boolean | null // 当前用户是否点过赞 constructor(id: number // 游记id , avatar: ResourceStr // 游记作者头像 , author: string // 游记作者名称 , title: string // 游记标题 , content: string // 游记内容 , cover: ResourceStr // 游记封面 , time: string // 发表时间 , location: string // 游记地点 , likeNum: number // 点赞数量 , watchNum: number // 阅读数量 , likeFlag: boolean | null // 当前用户是否点过赞 ) { this.id = id this.avatar = avatar this.author = author this.title = title this.content = content this.cover = cover this.time = time this.location = location this.likeNum = likeNum this.watchNum = watchNum this.likeFlag = likeFlag } } ``` **数据** ```typescript export const travelNotesList: TravelNotes[] = [ new TravelNotes(1, "/assets/travels/avatar1.jpeg", "假发子", "绍兴、苏州 | 穿过冬日的烟雨江南", "对 江南 一直很向往,在一个不怎么合适的季节突然有了时间,决定去看看。 一、绍兴 2024年1月28日上午八点半我从 厦门 站出发。 这次是一个人的瞎溜达,带上了新买的镜头,外加相机无人机等...", "/assets/travels/cover1.jpeg", "2024-01-11", "绍兴", 135, 3134, false), new TravelNotes(2, "/assets/travels/avatar2.jpeg", "水杉", "热辣五一,绝美江西", "1.为什么选江西?我对 江西 并不熟悉,了解八一起义,但不知道发生在 江西 ;熟读《滕王阁序》,但不知道描绘是 江西 ;憧憬瓷都 景德镇 ,但不知道隶属于 江西", "/assets/travels/cover2.jpeg", "2024-05-01", "江西", 200, 5657, false), new TravelNotes(3, "/assets/travels/avatar3.jpeg", "庞尼西", "自驾川南的五天四晚,寻找遗落大山的小众宝藏", "虽然同在 四川 ,但是川南之于 成都 来说,一直都是挺遥远的一个存在,尤其是 泸州 和 宜宾 ,距离上虽然好于 川西 ,但在高铁还没有通车的年代,去一次 泸州 和 宜宾 ,坐车少说也要4小时+...", "/assets/travels/cover3.jpeg", "2024-05-11", "四川", 210, 3504, false), new TravelNotes(4, "/assets/travels/avatar4.jpeg", "淡紫四叶草", "一个人的昆明五日City walk之旅", "又到了一年蓝花楹盛开的季节,对于一位浪漫主义的紫色狂热者,去看一场蓝花楹的欲望越发强烈。考虑国内最佳追花地-- 云南 昆明 和 四川 西昌 的花期,五一假期应该是 昆明 最合适。时隔十年...", "/assets/travels/cover4.jpeg", "2024-05-11", "昆明", 110, 2916, false), new TravelNotes(5, "/assets/travels/avatar5.jpeg", "菲菲小肥肥", "写我的夏天,十五天在宝岛画个圈", "【序】<< 为什么十五天独自环岛 2024年了,现在我才着手开始正式开始写下六年前前往 台湾 的游记,旅途中一直有一些随笔,但我想把记忆和照片整理一下,回看旅途也能给未来的自己力量,于...", "/assets/travels/cover5.jpeg", "2024-05-18", "台北", 120, 3310, false), new TravelNotes(6, "/assets/travels/avatar6.jpeg", "阿KEN", "五天四夜,寻找被遗忘的山西色", "- 山西 视频版攻略和 山西 旅行短片可上B站搜同名账号观看- 一、前言 提起 山西 ,不知道大家会想起什么?是灰蒙蒙的天空?黑漆漆的煤矿?还是富得漏油的煤老板?其实这些都是带有误解色彩...", "/assets/travels/cover6.jpeg", "2024-05-20", "平遥", 320, 4356, false), new TravelNotes(7, "/assets/travels/avatar7.jpeg", "LuC Photo", "五一自驾|邯郸-安阳-长治-阳泉", "三省四城访古之旅 2024年的五一假期, 北京 出发自驾,走了 邯郸 - 安阳 - 长治 - 阳泉 的5日访古之旅。一路上都是石窟、博物馆、古建筑、彩塑一类的景点,历史爱好者逛的非常满足。 邯郸 ...", "/assets/travels/cover7.jpeg", "2024-05-20", "邯郸", 220, 4100, false), new TravelNotes(8, "/assets/travels/avatar8.jpeg", "古彤随影", "我在格聂,很想你", "序 人的情绪是一件很奇妙的事情,有时候一见钟情,有时候,时隔多年依然偶尔想起。 即便这许多年不曾见面,即便中间有几年断了联系,即便每每想起,印象中还是当年青涩的模样。 有这样一...", "/assets/travels/cover8.jpeg", "2024-03-24", "理塘", 151, 1120, false), new TravelNotes(9, "/assets/travels/avatar9.jpeg", "幸存者格蕾丝", "大理、沙溪淡季六日行", "再续前缘 《去有风的地方》热播后, 大理 这个初代顶流旅游城市再次爆火。这次去 大理 倒不是跟风,而是冲着 腾冲 银杏和无量山冬樱花去的。11月底报了个摄影团,在 大理 集散,就这样再次...", "/assets/travels/cover9.jpeg", "2024-05-27", "大理", 199, 3420, false), new TravelNotes(10, "/assets/travels/avatar10.jpeg", "小宇", "不留遗憾——在藏东南看尽冰川", "再次出发去 拉萨 了!上一回还是去年国庆,一路看喜马拉雅山脉。意犹未尽,于是今年春节便决定,再进藏区。 √ 出行日期:2024年2月9日-2024年2月21日 √ 设备:5D4+70-200+100-400,大疆ai...", "/assets/travels/cover10.jpeg", "2024-01-11", "林芝", 529, 6420, false), ] ``` ### 数据懒加载的泛型类 ```typescript export default abstract class BaseDataSource implements IDataSource{ private dataArray: T[] | null = [] private listeners:DataChangeListener[] = [] constructor(dataArray: T[] | null) { this.dataArray = dataArray; } totalCount(): number { return this.dataArray === null ? 0 : this.dataArray.length } getData(index: number): T | null { return this.dataArray !== null && index >= 0 && index < this.totalCount() ? this.dataArray[index] : null } registerDataChangeListener(listener: DataChangeListener): void { if(this.listeners.indexOf(listener) < 0){ this.listeners.push(listener) } } unregisterDataChangeListener(listener: DataChangeListener): void { const pos = this.listeners.indexOf(listener); if(pos >= 0){ this.listeners.splice(pos, 1) } } notifyDataReload(){ this.listeners.forEach(listener=>{ listener.onDataReloaded() }) } notifyDataAdd(index:number){ this.listeners.forEach(listener=>{ listener.onDataAdd(index) }) } notifyDataDelete(index:number){ this.listeners.forEach(listener=>{ listener.onDataDelete(index) }) } notifyDataChange(index:number){ this.listeners.forEach(listener=>{ listener.onDataChange(index) }) } notifyDataMove(from:number, to:number){ this.listeners.forEach(listener=>{ listener.onDataMove(from,to) }) } //下面是对自己数据的操作 public setDataArray(dataArray:T[]){ this.dataArray = dataArray } // 插入数据 public addData(index:number, data:T){ if(this.dataArray !== null){ this.dataArray.splice(index,0,data) this.notifyDataAdd(index) } } // 在数组最后添加数据 public pushData(data:T | T[]){ if(this.dataArray !== null){ if (Array.isArray(data)) { this.dataArray.push(...data); } else { this.dataArray.push(data); } this.notifyDataAdd(this.dataArray.length - 1) } } // 删除数据 public deleteData(index:number){ if(this.dataArray !== null){ const delArr = this.dataArray.splice(index, 1); if(delArr.length > 0){ this.notifyDataDelete(index) } } } } ``` ### 旅行日志数据懒加载 ```typescript export default class TravelNotesDataSource extends BaseDataSource{ constructor(dataArray:TravelNotes[] | null){ super(dataArray) } } ``` ### 必游榜类型与数据 **类型** ```typescript export interface IMustVisitListTag{ id: number tag: string count: number desc?: string } export interface IMustVisitDataType { id: number, name: string, tags: string[], cover: ResourceStr, location?: string, desc?: string, score?: number } export interface ListContentColor{ tagColor: ResourceColor, linearBgColor:Array<[ResourceColor, number]> } ``` **数据** ```typescript export const mustVisitListTags: IMustVisitListTag[] = [ {id: 1, tag: "乐园必打卡", count: 6, desc: "乐园度假区Tag"}, {id: 2, tag: "必游博物馆", count: 6, desc: "博物馆Tag"}, {id: 3, tag: "名胜古迹榜", count: 6, desc: "名胜古迹Tag"}, {id: 4, tag: "经典自驾线", count: 5, desc: "经典自驾Tag"} ] export const mustVisitListContent: IMustVisitDataType[] = [ {id: 1, name: "上海迪士尼度假区", tags:["走进奇幻的童话世界"], cover: "/assets/must_visit/must_visit_1.JPG", location:"上海", desc:"", score: 4.7}, {id: 2, name: "北京环球度假区", tags:["亮相中国的魔法世界"], cover: "/assets/must_visit/must_visit_2.JPG", location:"北京", desc:"", score: 4.9}, {id: 3, name: "珠海长隆海洋公园", tags:["世界最大的海洋馆之一"], cover: "/assets/must_visit/must_visit_3.JPG", location:"横琴", desc:"", score: 4.7}, {id: 4, name: "大唐芙蓉园", tags:["绝美夜色带你梦回大唐"], cover: "/assets/must_visit/must_visit_4.JPG", location:"西安", desc:"", score: 4.7}, {id: 5, name: "杭州宋城", tags:["宋文华主题公园"], cover: "/assets/must_visit/must_visit_5.JPG", location:"杭州", desc:"", score: 4.7}, {id: 6, name: "深圳世界之窗", tags:["不出国门环游世界"], cover: "/assets/must_visit/must_visit_6.JPG", location:"深圳", desc:"", score: 4.6}, {id: 7, name: "故宫", tags:["千里江山图","清明上河图"], cover: "/assets/must_visit/must_visit_7.JPG", location:"北京", desc:"", score: 4.7}, {id: 8, name: "中国国家博物馆", tags:["司母戊鼎","四羊方尊"], cover: "/assets/must_visit/must_visit_8.JPG", location:"北京", desc:"", score: 4.8}, {id: 9, name: "陕西历史博物馆", tags:["感受最兴盛朝代的历史文化"], cover: "/assets/must_visit/must_visit_9.JPG", location:"西安", desc:"", score: 4.7}, {id: 10, name: "三星堆博物馆", tags:["感受三千年前的古蜀之光"], cover: "/assets/must_visit/must_visit_10.JPG", location:"广汉", desc:"", score: 4.7}, {id: 11, name: "秦始皇帝陵博物馆", tags:["世界第八大奇迹"], cover: "/assets/must_visit/must_visit_11.JPG", location:"临潼", desc:"", score: 4.8}, {id: 12, name: "湖北省博物馆", tags:["越王勾践剑", "曾侯乙编钟"], cover: "/assets/must_visit/must_visit_12.PNG", location:"武汉", desc:"", score: 4.7}, {id: 13, name: "八达岭长城", tags:["中国十大名胜古迹"], cover: "/assets/must_visit/must_visit_13.JPG", location:"延庆", desc:"", score: 4.7}, {id: 14, name: "龙门石窟", tags:["中国石刻艺术的最高峰"], cover: "/assets/must_visit/must_visit_14.JPG", location:"洛阳", desc:"", score: 4.7}, {id: 15, name: "布达拉宫", tags:["佛教圣地"], cover: "/assets/must_visit/must_visit_15.JPG", location:"拉萨", desc:"", score: 4.7}, {id: 16, name: "莫高窟", tags:["人类艺术殿堂"], cover: "/assets/must_visit/must_visit_16.JPG", location:"敦煌", desc:"", score: 4.7}, {id: 17, name: "滕王阁", tags:["江南三大名楼之一"], cover: "/assets/must_visit/must_visit_17.JPG", location:"南昌", desc:"", score: 4.6}, {id: 18, name: "承德避暑山庄", tags:["现存最大古典皇家园林"], cover: "/assets/must_visit/must_visit_18.JPG", location:"承德", desc:"", score: 4.5}, ] export const listContentColors: ListContentColor[] = [ {tagColor:"#6DAB75", linearBgColor:[['#E4F2E4',0.0],['#FEF9EF',0.7],['#FFFFFF',1.0]]}, {tagColor:"#A496DB", linearBgColor:[['#F3EFFD',0.0],['#FEF9FF',0.7],['#FFFFFF',1.0]]}, {tagColor:"#CCA487", linearBgColor:[['#FFE9D9',0.0],['#FEF9EF',0.7],['#FFFFFF',1.0]]}, ] ``` ### 必游榜数据懒加载 ```typescript export class MustVisitDataSource extends BaseDataSource{ constructor(dataArray:IMustVisitDataType[] | null){ super(dataArray) } } ``` ### TravelNotes添加位置属性信息 ```typescript @Observed export class TravelNotes { ......省略 cardArea: Area constructor(......) { ......省略 this.cardArea = { width:0, height:0, position:{ x:0, y:0 }, globalPosition:{ x:0, y:0 } } } } ```