Kotlin+Compose实战:趣味校园Android应用从零到可运行
简介这是一份面向Android初学者与校园社交应用开发者的Java实战项目源码以贴近校园生活的移动社交应用为主题围绕信息分享与即时聊天展开适合作为课程设计、毕业设计或自学练手的参考案例。资源包共629个文件包含89个java源文件、105个xml布局与配置、214个class编译文件、185个png图片资源以及11个jar依赖库另有apk安装包与sql数据库脚本压缩包约11.77MB结构完整便于对照学习。项目已实现主页动态发布与赞踩评论、资讯浏览、好友列表与聊天记录、周边的人查找、天气与图书查询以及昵称头像修改等模块服务端需运行MyAppServer工程并导入MySQL驱动。目前已有173人学习下载读者可从中掌握Android界面布局、网络请求、数据库交互与社交功能实现的完整思路并借鉴其模块划分与代码组织方式。1. 趣味校园类 Android 应用从想法到能跑通的完整路径很多开发者第一次接触「趣味校园」这个方向脑子里浮现的是一堆功能堆叠——签到、表白墙、二手市场、失物招领。但真正动手写代码时第一个卡住的地方往往不是功能本身而是数据从哪来没有后端的情况下一个校园趣味应用怎么让用户觉得「有东西可看」FunnyCampus 这类 Android 应用的核心矛盾就在这里——它要轻量、要有趣、要能离线跑同时还得让内容看起来是活的。我见过太多校园项目死在「首页空白」这一步用户打开 App 看到一片白三秒后就卸载了。所以这篇文章不讲虚的直接拆一条可复现的路径用本地数据 轻量网络请求 模块化 UI把趣味校园应用从零跑到能演示、能迭代的状态。适合有 Android 基础、想做一个完整课程设计或练手项目的开发者也适合想了解移动端内容型应用架构的熟手。2. 趣味校园应用的技术选型与最小可行架构2.1 为什么我建议用 Kotlin Jetpack Compose 而不是 Java XML2024 年之后新开的 Android 项目如果还在用 Java XML 写趣味校园类应用后期维护成本会明显偏高。原因不是 Java 不能写而是 Compose 在「内容型页面」上的开发效率差距太大。趣味校园应用的核心页面是什么信息流、卡片列表、详情页、发布表单。这些页面在 Compose 里用LazyColumnCard组合一个文件能写完整个首页用 XML 则要写 adapter、ViewHolder、多个 layout 文件改一个间距要跳三个文件。选型建议如下维度Kotlin ComposeJava XML首页信息流开发单文件 80 行内至少 3 个文件状态管理StateFlow collectAsStateLiveData Observer列表性能LazyColumn 自带复用RecyclerView 需手动优化学习曲线声明式前期陡后期平命令式前期平后期陡我一般会直接上 Kotlin Compose最低 API 定在 26覆盖绝大多数校园机型。依赖注入不急着上 Hilt用简单的单例 手动构造就能撑住一个课程设计级别的项目。2.2 最小可行架构三层拆开但别拆太细趣味校园应用不需要 Clean Architecture 那种五层拆分三层足够数据层本地 Room 数据库 一个轻量网络请求模块Retrofit OkHttp逻辑层ViewModel 持有 UI 状态Repository 统一数据来源UI 层Compose 页面 可复用组件关键决策是本地数据优先。校园应用经常面临网络不稳定、后端接口没写好的情况所以首页内容应该先从 Room 读网络请求作为补充。这样即使后端挂了App 打开也有内容。// 数据层Room 实体定义 Entity(tableName funny_posts) data class FunnyPost( PrimaryKey(autoGenerate true) val id: Long 0, val title: String, // 帖子标题 val content: String, // 正文内容 val author: String, // 发布者昵称 val category: String, // 分类吐槽/求助/分享 val timestamp: Long, // 发布时间戳 val likeCount: Int 0 // 点赞数默认 0 )这段代码定义了一个最简帖子实体。category字段用字符串而不是枚举是为了后续扩展分类时不用改数据库 schema。timestamp用 Long 存毫秒值排序和格式化都方便。likeCount给默认值 0插入时不用手动传。// Repository本地优先网络兜底 class PostRepository( private val dao: PostDao, private val api: CampusApi ) { // 先返回本地缓存再尝试网络刷新 fun getPosts(): FlowListFunnyPost flow { emit(dao.getAll()) // 第一步发本地数据 try { val remote api.fetchPosts() // 第二步请求网络 dao.insertAll(remote) // 写入本地 emit(dao.getAll()) // 再发一次更新后的数据 } catch (e: Exception) { // 网络失败不崩溃本地数据已经发出去了 } } }这个 Repository 的逻辑是先发本地缓存让 UI 立刻有内容再尝试网络请求成功就更新数据库并重新发射。catch块里什么都不做是有意为之——网络失败时用户至少能看到本地数据不会白屏。这是趣味校园类应用最实用的容错模式。2.3 首页信息流的 Compose 实现首页是整个应用的门面必须做到「打开即有内容滑动不卡顿」。用LazyColumn配合collectAsStateWithLifecycle是最稳妥的方案。Composable fun HomeScreen(viewModel: HomeViewModel) { // collectAsStateWithLifecycle 在页面不可见时自动停止收集省电 val posts by viewModel.posts.collectAsStateWithLifecycle() LazyColumn( contentPadding PaddingValues(16.dp), verticalArrangement Arrangement.spacedBy(12.dp) ) { items(posts, key { it.id }) { post - PostCard(post) // 每个帖子一张卡片 } } } Composable fun PostCard(post: FunnyPost) { Card( modifier Modifier.fillMaxWidth(), elevation CardDefaults.cardElevation(defaultElevation 2.dp) ) { Column(modifier Modifier.padding(16.dp)) { Text(text post.title, style MaterialTheme.typography.titleMedium) Spacer(modifier Modifier.height(4.dp)) Text( text post.content, maxLines 2, // 最多两行超出省略 overflow TextOverflow.Ellipsis, style MaterialTheme.typography.bodyMedium ) Spacer(modifier Modifier.height(8.dp)) Text( text ${post.author} · ${post.likeCount} 赞, style MaterialTheme.typography.labelSmall ) } } }key { it.id }这个参数很关键。不加的话列表数据更新时 Compose 会按位置复用组件导致点赞状态错位。加上 key 之后每个 item 和帖子 id 绑定更新时精准刷新。maxLines 2配合TextOverflow.Ellipsis是信息流卡片的标配防止一条长帖把整个列表撑爆。3. 本地数据与网络请求的配合让应用「看起来是活的」3.1 Room 数据库的建表与查询优化趣味校园应用的数据量不大但查询频率高。首页每次打开都要读全部帖子如果每次都是全表扫描帖子多了会明显变慢。加索引是最简单的优化手段。// 在实体上加索引 Entity( tableName funny_posts, indices [Index(value [timestamp]), Index(value [category])] ) data class FunnyPost(...) // DAO 查询按时间倒序限制条数 Dao interface PostDao { Query(SELECT * FROM funny_posts ORDER BY timestamp DESC LIMIT 50) fun getAll(): ListFunnyPost Insert(onConflict OnConflictStrategy.REPLACE) suspend fun insertAll(posts: ListFunnyPost) Query(UPDATE funny_posts SET likeCount likeCount 1 WHERE id :postId) suspend fun likePost(postId: Long) }timestamp索引让排序查询走索引而不是全表扫描。LIMIT 50是防止一次性加载过多数据首页只需要展示最近 50 条。OnConflictStrategy.REPLACE保证网络返回的重复数据不会报错直接覆盖旧记录。点赞用 SQL 原子操作likeCount 1避免读-改-写的并发问题。3.2 Retrofit 接口定义与错误处理网络层不需要复杂一个接口、一个数据类、一个错误封装就够了。关键是错误处理要让 UI 层能区分「没数据」和「请求失败」。// API 接口定义 interface CampusApi { GET(posts) suspend fun fetchPosts(): ListFunnyPost } // 网络结果封装 sealed class NetResultout T { data class SuccessT(val data: T) : NetResultT() data class Error(val code: Int, val message: String) : NetResultNothing() object Loading : NetResultNothing() } // 安全的网络请求 suspend fun T safeApiCall(call: suspend () - T): NetResultT { return try { NetResult.Success(call()) } catch (e: IOException) { NetResult.Error(-1, 网络连接失败) // 本地网络问题 } catch (e: HttpException) { NetResult.Error(e.code(), e.message()) // 服务端返回错误 } catch (e: Exception) { NetResult.Error(-2, 未知错误) // 兜底 } }safeApiCall把三种异常分开处理IOException是本地网络断了HttpException是服务端返回了 4xx/5xx其他异常统一兜底。UI 层拿到NetResult.Error后可以决定是显示「网络不可用」还是「服务器维护中」而不是笼统地弹一个 Toast。3.3 用 WorkManager 做后台刷新趣味校园应用不需要实时推送但可以在合适的时机后台刷新数据。WorkManager 是最合适的选择它能在设备充电、连 WiFi 时执行任务不耗用户电量。// 后台刷新 Worker class RefreshWorker( context: Context, params: WorkerParameters ) : CoroutineWorker(context, params) { override suspend fun doWork(): Result { val dao AppDatabase.getInstance(applicationContext).postDao() val api RetrofitClient.api return try { val remote api.fetchPosts() dao.insertAll(remote) Result.success() } catch (e: Exception) { Result.retry() // 失败后按退避策略重试 } } } // 调度每 6 小时一次仅在联网时执行 val request PeriodicWorkRequestBuilderRefreshWorker(6, TimeUnit.HOURS) .setConstraints( Constraints.Builder() .setRequiredNetworkType(NetworkType.CONNECTED) .build() ) .build() WorkManager.getInstance(context).enqueueUniquePeriodicWork( refresh_posts, ExistingPeriodicWorkPolicy.KEEP, // 已存在则不重复创建 request )Result.retry()会让 WorkManager 按指数退避重试第一次失败等 30 秒第二次等 60 秒避免网络刚断就疯狂重试。ExistingPeriodicWorkPolicy.KEEP保证不会因为重复调用而创建多个定时任务。6 小时的间隔对校园应用足够太频繁反而浪费资源。4. 避坑与排查趣味校园应用最容易翻车的 5 个地方4.1 首页白屏数据没加载完就渲染现象App 打开后首页空白过一两秒才出现内容或者一直空白。原因ViewModel 里用MutableStateFlow(emptyList())初始化为空列表UI 直接渲染空列表没有加载状态区分。解决给 UI 状态加一个Loading态加载中显示骨架屏或进度条而不是空列表。// 正确的 UI 状态定义 sealed class HomeUiState { object Loading : HomeUiState() data class Success(val posts: ListFunnyPost) : HomeUiState() data class Error(val message: String) : HomeUiState() } // ViewModel 中 private val _uiState MutableStateFlowHomeUiState(HomeUiState.Loading) val uiState: StateFlowHomeUiState _uiState.asStateFlow()4.2 列表滑动卡顿在 item 里做耗时操作现象首页滑动时掉帧帖子越多越卡。原因在PostCard里直接做时间格式化、字符串拼接等操作每次重组都重新计算。解决把格式化逻辑放到数据层或者在 Composable 里用remember缓存计算结果。Composable fun PostCard(post: FunnyPost) { // remember 缓存格式化结果只有 post.timestamp 变化时才重新计算 val timeText remember(post.timestamp) { formatTime(post.timestamp) } // ... }4.3 数据库主线程报错Room 查询没切协程现象App 崩溃日志显示Cannot access database on the main thread。原因Room 默认禁止主线程查询但有些开发者忘了在 DAO 方法上加suspend或者没在协程里调用。解决所有 DAO 的读写方法都加suspend在 ViewModel 的viewModelScope里调用。如果必须同步查询用allowMainThreadQueries()但这是下策只适合调试。4.4 网络请求重复触发配置变更导致重建现象旋转屏幕后网络请求发了两次日志里看到重复请求。原因Activity 重建时 ViewModel 虽然保留了但如果请求写在init块或者 Composable 的LaunchedEffect里可能被重复触发。解决网络请求放在 ViewModel 的init块里只执行一次UI 层只负责收集状态。如果必须在 Composable 里触发用LaunchedEffect(Unit)保证只执行一次。4.5 混淆后崩溃Room 和 Retrofit 的反射被裁掉现象Debug 包正常Release 包一打开就崩溃。原因R8 混淆把 Room 生成的实现类和 Retrofit 的接口方法名裁掉了。解决在proguard-rules.pro里保留相关类。# Room 生成的实现类 -keep class * extends androidx.room.RoomDatabase -keep androidx.room.Entity class * # Retrofit 接口 -keep interface com.example.funnycampus.data.CampusApi -keep class com.example.funnycampus.data.model.** { *; }5. 进阶技巧用「伪实时」让趣味校园应用更有生命力趣味校园应用最难的不是技术是「让用户觉得有人在用」。真实后端没搭好之前可以用几个小技巧制造活跃感。第一个是本地时间戳扰动在展示帖子时间时把超过 1 小时的帖子显示为「刚刚」「几分钟前」的模糊时间而不是精确到秒。用户感知上会觉得内容更新更频繁。fun formatTime(timestamp: Long): String { val diff System.currentTimeMillis() - timestamp return when { diff 60_000 - 刚刚 diff 3600_000 - ${diff / 60_000} 分钟前 diff 86400_000 - ${diff / 3600_000} 小时前 else - ${diff / 86400_000} 天前 } }第二个是预置种子数据。首次安装时往 Room 里插入 20 条精心编写的校园趣味内容分类覆盖吐槽、求助、分享。这样用户第一次打开就有东西看而不是面对空白页。种子数据放在assets/seed.json里首次启动时读取并插入。// 首次启动时加载种子数据 suspend fun loadSeedIfEmpty(dao: PostDao, context: Context) { if (dao.getAll().isNotEmpty()) return // 已有数据则跳过 val json context.assets.open(seed.json).bufferedReader().readText() val posts Gson().fromJson(json, ArrayFunnyPost::class.java) dao.insertAll(posts.toList()) }第三个是点赞的即时反馈。用户点赞时先更新本地数据库和 UI再异步发网络请求。即使网络失败本地状态已经变了用户感知是「点赞成功」。下次网络恢复时再同步。这种「乐观更新」模式在校园应用里特别实用因为校园网经常不稳定。fun likePost(postId: Long) { viewModelScope.launch { dao.likePost(postId) // 先更新本地 try { api.likePost(postId) // 再同步网络 } catch (e: Exception) { // 网络失败不回滚下次刷新时以服务端为准 } } }最后一个技巧是分类标签的视觉差异化。吐槽用暖色、求助用蓝色、分享用绿色用户扫一眼就能区分内容类型。这个用 Compose 的when表达式几行就能实现但对「趣味感」的提升很明显。Composable fun CategoryTag(category: String) { val (bgColor, textColor) when (category) { 吐槽 - Color(0xFFFFF3E0) to Color(0xFFE65100) 求助 - Color(0xFFE3F2FD) to Color(0xFF1565C0) 分享 - Color(0xFFE8F5E9) to Color(0xFF2E7D32) else - Color(0xFFF5F5F5) to Color(0xFF616161) } Box( modifier Modifier .background(bgColor, RoundedCornerShape(4.dp)) .padding(horizontal 8.dp, vertical 2.dp) ) { Text(text category, color textColor, style MaterialTheme.typography.labelSmall) } }这几个技巧加起来不到 100 行代码但能让一个课程设计级别的应用在演示时看起来「像那么回事」。我自己的习惯是先把种子数据和乐观更新做进去再去调 UI 细节。因为前者决定用户会不会留下来后者只决定用户觉得好不好看。顺序反了容易在美化上花太多时间最后发现没人用。希望帮到你。本文还有配套的精品资源点击获取

相关新闻

ShardingSphere分库分表实战:从数据膨胀事故到核心原理与踩坑复盘

ShardingSphere分库分表实战:从数据膨胀事故到核心原理与踩坑复盘

1. 从一次数据膨胀事故说起:为什么我要啃ShardingSphere去年下半年,我负责维护的一套业务系统出了一次不大不小的事故。单表数据量在三个月内从两千万飙到一亿两千万,慢查询告警一天响几十次,最夸张的一条统计SQL跑了四十多秒&…

2026/10/13 12:33:36 阅读更多 →
订单状态机实战:通用表驱动状态流转引擎的设计与落地

订单状态机实战:通用表驱动状态流转引擎的设计与落地

先把状态机这事的来龙去脉讲清楚。 干电商后台这几年,订单状态这堆逻辑是我见过最容易写成一坨翔的地方。一个订单从待支付到已支付,再到已发货、已完成,听起来就是一条直线,可真到了代码里,你会看到支付回调里判断状…

2026/10/13 12:33:36 阅读更多 →
图像分析的数据结构:从四叉树到区域邻接图的工程实践

图像分析的数据结构:从四叉树到区域邻接图的工程实践

图像分析的数据结构,是《图像处理、分析与机器视觉》第四版里最容易被低估的一章。我第一次翻这本书时,注意力全在滤波、边缘检测和分割算法上,觉得数据结构无非就是把像素存进数组。直到动手实现分割结果的区域关系分析时才发现,…

2026/10/13 12:33:36 阅读更多 →

最新新闻

React Native鸿蒙适配实战:RN页面与鸿蒙提醒服务的系统能力桥接

React Native鸿蒙适配实战:RN页面与鸿蒙提醒服务的系统能力桥接

做鸿蒙适配做到第七个页面,CareTipsScreen算是我这轮项目里最头疼、也最有收获的一个。先说清楚它是什么:这是一款健康管理应用里的“每日护理建议”聚合页,用户每天打开应用,能看到系统按时间段推荐的护理建议,比如下…

2026/10/13 13:15:36 阅读更多 →
驱动工具收费怎么办?显卡驱动修复免费替代方案与卸载清理指南

驱动工具收费怎么办?显卡驱动修复免费替代方案与卸载清理指南

前两年我装完系统,第一件事就是找一款驱动管理工具,跑一遍自动检测,网卡、显卡、声卡一次性装齐,整个过程免费,体验也算顺畅。直到前两天,我打开这款老牌驱动工具的新版本,界面确实变干净了&…

2026/10/13 13:15:36 阅读更多 →
DeepSeek Harness:Agent框架的全插件化设计与可回放日志实践

DeepSeek Harness:Agent框架的全插件化设计与可回放日志实践

这两年做 Agent 工程化,我最大的感受是:光有一个能跑通 demo 的工作流远远不够,真正让人头疼的是框架耦合、排查困难、扩展两难。团队内部折腾了大半年,最终沉淀出一套叫 DeepSeek Harness 的 Agent 运行框架。名字里带 Harness&a…

2026/10/13 13:15:36 阅读更多 →
构建帧率无关的时钟可视化内核:requestAnimationFrame与时间流映射

构建帧率无关的时钟可视化内核:requestAnimationFrame与时间流映射

1. 项目概述:这不是一个普通时钟,而是一次视觉逻辑的重新校准“炫酷的时钟效果”——这五个字在前端开发、数字艺术、交互设计甚至新媒体运营的日常沟通中,出现频率远超表面看起来的那么简单。它从来不是指“把时间显示出来”,而是…

2026/10/13 13:15:36 阅读更多 →
免费搭建本地AI知识库:用Cherry Studio和开源模型实现私有文档智能检索

免费搭建本地AI知识库:用Cherry Studio和开源模型实现私有文档智能检索

最近整理资料时我一直在想一个问题:手头文档越堆越多,光是项目方案、会议纪要、行业报告、个人笔记就占了几个文件夹,想快速找出某个结论时经常得翻半天。后来我尝试把常用的云上AI助手当检索工具,但每次都要上传文档、担心隐私、…

2026/10/13 13:15:36 阅读更多 →
个人主页置顶帖与投稿汇总实操指南:打造内容导航体系

个人主页置顶帖与投稿汇总实操指南:打造内容导航体系

1. 先搞清楚:置顶帖和投稿汇总到底在解决什么问题1.1 置顶帖是主页的第一屏,也是你的数字名片我见过太多创作者陷入同一个困境:内容发了几百条,主页却像一个乱糟糟的仓库,新访客点进来根本不知道你擅长什么、写过哪些代…

2026/10/13 13:14:25 阅读更多 →

日新闻

物联网系统集成避坑指南:从选型到验收的关键要点

物联网系统集成避坑指南:从选型到验收的关键要点

做了这么多年物联网系统集成的项目,我最大的感受是,大部分坑不是出在技术前沿,而是出在一些看起来很平常的地方。设备选了便宜的、协议没问清楚、现场没摸透、验收标准写得含糊——这些细枝末节,最后都会变成延期、超支和扯皮的大…

2026/10/13 0:00:57 阅读更多 →
ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

如果你玩ESP32有一阵子了,很难不碰到舵机控制这道坎。做智能小车要转方向,做机械臂要抬关节,做云台要偏航俯仰,背后都是同一个东西在干活:舵机。而我见过太多人卡在“代码能编译、舵机就是不听话”这个阶段&#xff0c…

2026/10/13 0:00:57 阅读更多 →
CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

1. 为什么CAN总线至今仍是工业现场的“硬通货”?——从汽车底盘到智能温室的真实需求你有没有拆过一辆老款燃油车的中控台?或者调试过某高校实验室里那台跑着Modbus协议却频频丢帧的PLC控制柜?又或者,在某个农业物联网项目里&…

2026/10/13 0:00:57 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/13 2:54:56 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/12 0:16:38 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/13 1:58:08 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 21:05:09 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 14:48:49 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 18:11:17 阅读更多 →