尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
Android城市选择器:三级联动、拼音索引与定位降级实践
简介这是一份面向Android开发者的城市选择器组件包界面风格仿照美团适合电商、旅游、社交及生活服务类应用快速接入城市选择功能。包内以164个class文件、31个java源码和40个xml布局资源为主另有22张png界面素材、3个jar依赖以及json配置、db数据等辅助文件合计285个文件压缩包约908KB体量轻巧便于集成和二次定制。组件结合高德地图定位能力采用模块化设计与简洁接口开发者可引入模块、设置回调并弹出选择器也能按项目需要调整城市数据和界面样式。目前已有931人学习下载适合希望减少重复开发、快速实现美观城市选择交互的Android开发者参考复用也可作为学习组件封装与界面组织的实践样例。1. 城市选择器为什么值得单独做一遍从一次下单流程的卡顿说起Android 城市选择器这个组件很多团队最初都是随便塞一个对话框等地址填错率上来才回头重做。我碰到的触发点更具体结算页的城市字段让用户手打一个三级地址被拆成三个输入框用户在城市名里混进省份和区县后端校验直接报错客服工单里一半是地址不规范。这时候再讨论「要不要做一个正经的城市选择器」已经不是体验问题而是数据质量问题。这套方案要解决三件事把全国城市数据以可维护的方式塞进安装包、让用户在两秒内从字母索引列表里点到目标城市、把定位城市和最近访问城市放在手指够得着的位置。适合正在做电商、外卖、出行、本地生活方向课程设计或毕设的同学也适合手上有个老项目、想把地址选择这块重做一遍的工程师。下面按数据结构、索引列表、搜索定位、避坑排查、验收的顺序走一遍每一步都能直接落到代码。2. 数据结构与总体架构从一份 JSON 到三层联动的落地路径城市选择器看着简单真正决定后期好不好维护的是数据怎么组织。我见过太多实现把省市区写死在三个静态数组里加一个区县就要改代码发版也见过每次打开选择器都去请求一次城市接口的弱网下转圈转半天。这两条路都不该走。2.1 数据从哪来assets 内置 JSON 与字段约定城市数据的变更频率极低一年也就几次行政区划调整。基于这个前提合理的做法是把一份约 3400 条记录的 JSON 打进assets首次启动时解析并写入本地数据库之后所有界面都读数据库。这样做的收益很直接离线可用、打开即渲染、不占一次网络往返服务端也不用为这个低频接口单独扛流量。代价是安装包会大一两百 KB压缩后其实可以接受。字段约定要在动手前定死后面搜索、排序、回显全靠它。下面这张表是我一般会写进设计文档的字段清单字段类型说明idString全局唯一建议用行政区划码不要用自增parentIdString顶级为 0用于建父子关系nameString展示名不带省市区后缀需谨慎建议保留全称levelInt1 省 / 2 市 / 3 区县pinyinString全拼小写预处理阶段生成initialString首字母大写单字符用于分组hotInt热门权重0 表示不进热门区versionInt数据版本号用于触发本地库重建这里有两个容易忽略的细节。第一id一定要能跨版本稳定用户存的最近访问城市是存 id 的如果 id 变了历史记录全部失效。第二initial不要留空遇到多音字或生僻字统一落到#分组界面上的#分组就是给这类数据兜底的。2.2 三个核心模型与一张扁平搜索表模型层分两套一套是给三级联动用的树形结构一套是给搜索用的扁平结构。树形结构保持父子关系扁平结构把三级全部拍平并冗余上完整路径搜索时直接展示「云岭省 临江市 青川县」这样的完整结果用户不用先选省再选市。// 树形节点省、市、区县共用一套结构用 level 区分层级 data class RegionNode( val id: String, val parentId: String, val name: String, val level: Int, val pinyin: String, val initial: String, val hot: Int 0 ) // 扁平节点搜索用fullName 冗余完整路径避免搜索时再回溯父级 data class RegionFlat( val id: String, val name: String, val fullName: String, // 云岭省 临江市 青川县 val pinyin: String, // yunlinglinjiangqingchuan val initials: String, // ylljqc用于首字母连打匹配 val initial: String, // Y分组用 val level: Int, val hot: Int )RegionFlat是这套方案里最值钱的一张表。以 3400 条数据算冗余存储的字符串大概几十 KB换来的是搜索时零次父级查询、结果直接可展示。initials字段把每个字的拼音首字母拼起来用户输入llj就能命中「临江市」这是国内用户最习惯的输入方式比全拼命中率高得多。对应的 JSON 片段长这样预处理脚本会把它转成上面两个模型[ { id: 530000, parentId: 0, name: 云岭省, level: 1, pinyin: yunling, initial: Y, hot: 0 }, { id: 530100, parentId: 530000, name: 临江市, level: 2, pinyin: linjiang, initial: L, hot: 1 } ]提示JSON 里的id、parentId只是示例形态实际项目里用你们后台已有的编码体系别为了这套组件去改后台。2.3 模块划分五个模块各管一段组件再小也要有边界否则三个月后没人敢动。我一般按下面五个模块切模块职责不做什么>// 对外只暴露一个结果对象和三段式选择器调用方不需要知道内部结构 data class RegionResult( val provinceId: String, val provinceName: String, val cityId: String, val cityName: String, val districtId: String, val districtName: String ) class CityPickerFragment : BottomSheetDialogFragment() { private var provinceId: String? null private var cityId: String? null private var districtId: String? null // 传入已选 id 做回显先按 id 反查扁平表拿到省市再逐级定位 fun setSelected(result: RegionResult?) { result ?: return provinceId result.provinceId cityId result.cityId districtId result.districtId // 注意顺序先省后市再区父级列表得先加载完才能定位子项 loadProvinces { scrollToProvince(provinceId) } } }回显这段是很多人第一次写会翻车的地方先定位省、再异步加载市、再定位市回调一层套一层。稳妥的写法是在repository里提供一次性的resolvePath(districtId)同步返回省市区三级节点界面拿到后一次性把三个变量都设好再做滚动定位避免中间态渲染出错误的高亮。3. 字母索引列表分组列表、粘性头与拼音排序的实现列表是城市选择器的主界面也是用户停留最久的地方。这一块的目标很明确右侧手指按住能连续滑动、左侧列表跟着跳、顶部永远显示当前分组的字母。三件事对应三处实现。3.1 拼音预处理在构建期算好不在运行时算一个反直觉的选型拼音转换不要放在运行时。很多实现会在列表绑定时调一次汉字转拼音3400 条数据在低端机上首次渲染能卡出明显白屏。正确做法是在构建期用脚本把拼音算好直接写进 JSONApp 里只做字符串比较。预处理脚本用 Python 写跑一次几分钟结果提交到仓库# tools/gen_pinyin.py # 作用读取原始行政区划数据补全 pinyin / initials / initial 三个字段 # 运行python tools/gen_pinyin.py assets/region_raw.json assets/region.json import json import sys PINYIN_TABLE json.load(open(tools/pinyin_table.json, encodingutf-8)) def to_pinyin(text): 逐字查表查不到的字落到占位符由人工校对补齐 out [] for ch in text: if ch in PINYIN_TABLE: out.append(PINYIN_TABLE[ch]) elif ch.isascii(): out.append(ch.lower()) else: out.append(?) # ? 会在下面统一归到 # 分组 return out def enrich(node): py to_pinyin(node[name]) node[pinyin] .join(py) node[initials] .join(p[0] if p ! ? else ? for p in py) # 首字符不是 a-z 的统一归到 # head node[pinyin][:1] node[initial] head.upper() if a head z else # return node if __name__ __main__: src, dst sys.argv[1], sys.argv[2] data [enrich(n) for n in json.load(open(src, encodingutf-8))] json.dump(data, open(dst, w, encodingutf-8), ensure_asciiFalse, indent2)脚本本身不长关键在最后两步输出initials供搜索用输出initial供分组用任何非 a-z 开头的城市一律进#。多音字是这里唯一的玄学机器查表几乎必错几个我一般会把?的数量打印出来人工过一遍再合并宁可多花二十分钟也别让用户搜不到自己所在的城市。3.2 分组数据组装与 DiffUtil 更新列表数据不是简单的ListRegionFlat而是「头部 内容」混排的扁平列表这样RecyclerView只需要一种 Item 类型加两种 viewType。分组头本身的字母也占一行滚动到这一组时顶部粘性头显示同样的字母。// 把有序的城市列表转成 [头A][城市][城市][头B][城市] 这样的混排列表 sealed class PickerItem { data class Header(val letter: String) : PickerItem() data class City(val data: RegionFlat) : PickerItem() } fun buildPickerItems(list: ListRegionFlat): ListPickerItem { // 排序规则先按首字母字母相同按全拼全拼相同按名称保证每次结果稳定 val sorted list.sortedWith( compareBy({ it.initial }, { it.pinyin }, { it.name }) ) val result ArrayListPickerItem(sorted.size 27) var lastLetter for (item in sorted) { if (item.initial ! lastLetter) { lastLetter item.initial result.add(PickerItem.Header(lastLetter)) } result.add(PickerItem.City(item)) } return result }排序必须稳定否则同一个城市在不同次打开时位置会变用户会怀疑自己点错了。用compareBy三级排序能保证结果可复现。更新时走DiffUtil只刷变化项而不是notifyDataSetChanged()全量重绘——后者在搜索框逐字输入时会让整个列表闪一下。3.3 右侧索引栏触摸事件的拦截与字母定位索引栏是一个自定义 View核心就两件事算出手指落在哪个字母上、把这个字母回调出去。难点在事件竞争上——外面的RecyclerView会抢触摸事件必须在ACTION_DOWN时就申请父级不拦截否则手指一滑动事件就被列表拿走了索引栏直接失效。class IndexBarView JvmOverloads constructor( context: Context, attrs: AttributeSet? null ) : View(context, attrs) { private val letters: ListString listOf(#) (A..Z).map { it.toString() } private var itemHeight 0f private var lastIndex -1 var onLetterChanged: ((String) - Unit)? null override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { super.onMeasure(widthMeasureSpec, heightMeasureSpec) // 每个字母固定高度总高可能超出控件高度超出时压缩 val available measuredHeight - paddingTop - paddingBottom itemHeight available.toFloat() / letters.size } override fun onTouchEvent(event: MotionEvent): Boolean { when (event.action) { MotionEvent.ACTION_DOWN, MotionEvent.ACTION_MOVE - { // 关键阻止外层 RecyclerView 在滑动时抢走事件 parent?.requestDisallowInterceptTouchEvent(true) val index ((event.y - paddingTop) / itemHeight) .toInt().coerceIn(0, letters.lastIndex) if (index ! lastIndex) { lastIndex index onLetterChanged?.invoke(letters[index]) } } MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL - { parent?.requestDisallowInterceptTouchEvent(false) lastIndex -1 } } return true } }itemHeight在onMeasure里算而不是写死是为了适配不同屏高小屏上 27 个字母挤在一起写死 20dp 会超出控件高度最后几个字母点不到。coerceIn也不能省手指滑出控件边界时index会变成负数或越界直接崩在letters[index]上。回调到列表这边用LinearLayoutManager.scrollToPositionWithOffset而不是smoothScrollToPosition后者在长列表里会一段一段滚用户按住索引栏快速滑动时体验很差。定位到分组头时 offset 传 0让头刚好贴在顶部。3.4 粘性头部ItemDecoration 的绘制与偏移粘性头用ItemDecoration.onDrawOver实现不需要额外 View也不会影响列表本身的测量。绘制分两步先取当前第一个可见项所属的分组字母画一条再看第二个可见项如果它的分组字母和当前不同就把当前这条往上顶出去。class StickyHeaderDecoration( private val adapter: PickerAdapter, private val headerHeight: Int ) : RecyclerView.ItemDecoration() { override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) { val topChild parent.getChildAt(0) ?: return val topPosition parent.getChildAdapterPosition(topChild) if (topPosition RecyclerView.NO_POSITION) return val header adapter.headerOf(topPosition) ?: return val offset if (topChild.top 0) topChild.top else 0 // 保存画布状态画完必须还原否则后续所有绘制都会带着这个位移 val save c.save() c.translate(0f, offset.toFloat()) adapter.drawHeader(c, header, parent.width, headerHeight) c.restoreToCount(save) } }save()/restoreToCount()这一对是必须的漏掉restore会导致后面的子 View 整体位移表现为「列表滚着滚着整屏向上飘」属于典型的血泪经验。另外headerHeight要和普通 Item 里的分组头高度完全一致差 1dp 都能看出抖动建议两边引用同一个dimen资源。4. 搜索与定位拼音首字母匹配、模糊查询与定位兜底搜索框是城市选择器里使用频次第二高的入口也是唯一一个可以直接跳过字母索引的捷径。定位城市则决定了「用户打开就能用」的体验但它的失败率远高于我们的想象。4.1 搜索索引三路命中与打分搜索结果要按相关度排序不能查到什么返回什么。这套实现里三路命中分别是名称完全相等、拼音或首字母前缀命中、名称或拼音包含命中。给每种命中一个分数最后按分数降序排列同级再按热门权重排。// 权重越高越靠前数字本身没有含义只要保持相对大小关系即可 private const val SCORE_EXACT_NAME 100 private const val SCORE_EXACT_PINYIN 90 private const val SCORE_PREFIX_INITIALS 80 private const val SCORE_PREFIX_PINYIN 70 private const val SCORE_PREFIX_NAME 60 private const val SCORE_CONTAINS 30 fun search(all: ListRegionFlat, rawInput: String): ListRegionFlat { val input rawInput.trim().lowercase() if (input.isEmpty()) return emptyList() return all.asSequence() .mapNotNull { city - val score when { city.name rawInput - SCORE_EXACT_NAME city.pinyin input - SCORE_EXACT_PINYIN city.initials.startsWith(input) - SCORE_PREFIX_INITIALS city.pinyin.startsWith(input) - SCORE_PREFIX_PINYIN city.name.startsWith(rawInput) - SCORE_PREFIX_NAME city.name.contains(rawInput) || city.pinyin.contains(input) - SCORE_CONTAINS else - returnmapNotNull null } // 同分时热门城市优先且层级越低越优先用户多半在找区县或市 city to (score city.hot * 5 city.level) } .sortedByDescending { it.second } .map { it.first } .take(50) // 结果截断避免极端输入下渲染几千条 .toList() }这里要说清一个选型判断很多人第一反应是上 Trie 树或者倒排索引。3400 条数据一次全量线性扫描在中等机型上实测 3~8ms加一层索引结构带来的内存和复杂度完全不划算。什么时候该上索引数据量过万、或者搜索框要做实时联想且加上了服务端结果混排那时候再考虑。别过度设计是这套方案里我最想强调的一句话。take(50)的截断也很有必要。用户输入一个a可能命中上千条RecyclerView一次性绑定这么多项草稿纸上算着没事真机上直接掉帧。截断后配合「输入更精确的关键词」的提示体验反而更好。4.2 定位城市超时、降级、可见的失败定位这块必须先说失败率。室内、地下室、权限拒绝、系统定位服务被关掉任何一种情况都能让定位一直挂着不返回。我的做法是给它加一个硬性超时3 秒拿不到就降级降级后用最后一个缓存定位或热门城市第一位顶上并且这张卡片上明确显示城市名而不是一直转圈。fun locateCity(onResult: (RegionFlat?) - Unit) { // 先看缓存24 小时内的定位结果直接复用避免每次打开都请求 val cached repository.lastLocated() if (cached ! null System.currentTimeMillis() - cached.time 24 * 3600_000L) { onResult(repository.findByCityName(cached.cityName)) return } val finished AtomicBoolean(false) // 超时兜底保证回调只触发一次 handler.postDelayed({ if (finished.compareAndSet(false, true)) onResult(null) }, 3000) locationClient.requestOnce( onSuccess { name - if (finished.compareAndSet(false, true)) { repository.saveLocated(name) onResult(repository.findByCityName(name)) } }, onFailure { if (finished.compareAndSet(false, true)) onResult(null) } ) }AtomicBoolean这个锁不能省。超时回调、成功回调、失败回调三者是竞态的没有它就会出现「先显示了定位城市3 秒后又被降级成热门城市」的诡异现象查起来非常费劲。onResult(null)之后界面做的事是定位卡片显示「定位失败点击重试」同时热门城市照常展示用户永远有一条路能走下去。还有一个容易被忽略的点拿到了经纬度不等于拿到了城市名。逆地理编码这一步同样可能超时或返回空必须有两段超时而不是一个总超时。4.3 最近访问与热门城市落库与去重最近访问城市用Room存一张表三列就够城市 id、展示名、访问时间。关键是 upsert 语义——同一个城市重复访问只更新时间不新增记录并且只保留最近 6 条。Entity(tableName recent_city) data class RecentCity( PrimaryKey val cityId: String, val name: String, val visitedAt: Long ) Dao interface RecentCityDao { Query(SELECT * FROM recent_city ORDER BY visitedAt DESC LIMIT 6) fun query(): ListRecentCity // REPLACE 冲突策略直接实现 upsert重复访问只会刷新时间戳 Insert(onConflict OnConflictStrategy.REPLACE) fun insert(city: RecentCity) // 只留最近 6 条超出部分删掉避免表无限增长 Query(DELETE FROM recent_city WHERE cityId NOT IN (SELECT cityId FROM recent_city ORDER BY visitedAt DESC LIMIT 6)) fun trim() }热门城市不落库直接从RegionFlat里筛hot 0按权重排因为这份数据是跟着 JSON 版本走的用户本地改不了没必要多存一份。真正需要落库的只有最近访问和定位缓存两样。注意trim()要在insert()之后调用顺序反了会先删掉刚插进去的那条。这种顺序问题在测试环境数据少的时候完全看不出来上线后用户点了七八个城市才暴露。5. 避坑与排查城市选择器最容易翻车的五件事这一章是整篇里最像「后悔药」的部分。下面五条都是我在真实项目里遇到过、并且排查了不少时间的每条按现象、原因、解决三段写遇到同类问题可以对着看。5.1 索引栏点一下跳错位置现象点右侧字母L列表跳到K组和L组之间再点一次又跳对了。滚动列表后偏差会变化越往后越明显。原因scrollToPositionWithOffset(position, 0)的position是混排列表里的下标而索引栏拿到的是字母。如果中间用了indexOfFirst在过滤后的列表里找和实际 Adapter 的下标就对不上——过滤列表少了一层分组头。另一种常见原因是粘性头高度占了位但没有在offset里扣掉视觉上就会差一个头的高度。解决在 Adapter 内部维护一张letterToPosition: MapString, Int在submitList之后一次性算好索引栏回调直接查这张表。粘性头如果覆盖在列表顶部offset传-headerHeight或者给RecyclerView加paddingTop并设clipToPaddingfalse两种方式选一种不要混用。改完拿最长的分组通常是#或S反复点十次确认每次都落在分组头正下方。5.2 搜索框输入后列表错乱甚至崩溃现象在搜索框里输两个字列表项的文字和头像对不上再删掉某个位置直接抛IndexOutOfBoundsException。原因索引列表和搜索结果共用一个 Adapter靠viewType分支处理。索引列表的 position 包含分组头搜索结果不包含两边对 position 的理解不一致。加上搜索是异步的结果回来时submitList传的可能是上一轮的旧数据。解决拆成两个 Adapter各管各的数据源用两个RecyclerView或者ViewFlipper切换别在一个 Adapter 里做分支。异步结果回来时用一个请求序号做校验只有最新一次请求的结果才允许submitList。搜索框加 200ms 防抖逐字输入时不会每次都触发查询。5.3 定位一直转圈用户以为卡死现象进入选择器定位卡片一直是加载态等十几秒才变成「定位失败」期间用户不知道能不能手动选。原因只处理了成功回调没处理失败和超时。系统定位服务被关闭时很多定位实现既不回调成功也不回调失败就这么挂着。另外逆地理编码是第二次网络请求这一步的超时被忽略了。解决加双层超时定位 3 秒、逆地理 2 秒任何一层超时都走降级。降级后卡片文案要明确比如「未能获取定位已为你展示热门城市」让用户知道下一步该做什么。定位期间列表本身必须可操作不要把整个界面盖一层 loading。5.4 数据更新了用户手机上还是老城市现象新版本加了几个区县测试机上正常已经装过老版本的用户升级后搜不到新城市。原因本地库是按「表是否为空」判断要不要初始化的老用户表里已经有数据新 JSON 根本不会被读取。version字段写了但没人用。解决启动时比对assets里的version和SharedPreferences里存的版本号不一致就清表重建并且保留最近访问记录按 id 匹不上的直接丢弃。重建过程放在子线程主线程先读旧数据渲染避免白屏。这一步必须写进版本发布的检查清单不然每次发版都可能漏。5.5 编辑地址回显时高亮错位现象从地址列表点「编辑」选择器打开后停在了正确城市但高亮的省和市对不上或者滚动位置偏了几十项。原因回显时先设了provinceId触发了一次列表刷新此时市列表还没加载完scrollToPosition拿到的是空列表的位置 0等市列表加载完界面已经滚到顶部了而后来的定位逻辑又被一次用户手动滑动取消。解决在repository里提供同步的resolvePath(districtId)一次性拿到省市区三级节点三个状态变量都设好再触发一次渲染和定位把「多次异步串联」变成「一次同步取数 一次定位」。scrollToPositionWithOffset用post包一层等布局完成再滚否则在onViewCreated里直接调用会因为列表还没测量而无效。6. 验收与答辩一份能照着跑的检查清单到这一步功能基本齐了剩下的问题是怎么证明它真的能用。我给这套组件准备了一份 12 项的验收清单答辩前或者提测前跑一遍比临时翻代码有效得多。类别检查项通过标准数据首次启动入库耗时中端机冷启动 800ms期间有加载态数据版本升级重建老版本升级后能搜到新增区县最近访问不丢列表索引栏连续滑动26 个字母 # 全部可点跳跃无越界崩溃列表粘性头切换分组交界处顶出动画连续无闪烁无错位列表空分组#组只含归底数据不出现空头搜索首字母连打输入llj能命中对应城市搜索全拼与汉字三种输入方式结果一致排序合理搜索无结果显示明确空态不是白屏定位权限拒绝3 秒内降级到热门城市文案清晰定位定位成功结果卡片可点击点击后正确选中回显编辑已有地址三级高亮和滚动位置都正确兼容小屏与大字体系统字体放大后索引栏不重叠、文字不截断表格里最容易糊弄过去的是「小屏与大字体」这一项。把系统字体调到最大27 个字母的索引栏几乎必然重叠这个时候要么让索引栏跟随字体缩放要么固定textSize不跟随系统二选一但要写进设计说明里答辩时被问到能答上来。另外冷启动那 800ms 的指标是靠「首屏先渲染热门城市、后台继续入库」做到的不是靠优化解析速度硬扛这个取舍要讲清楚。答辩场上最常被追问的两个问题一个是「为什么拼音不在运行时算」一个是「为什么搜索不上索引结构」。前者的答案在 3.1运行时转换会让首屏多出几百毫秒的白屏构建期算好只是一次性成本。后者的答案在 4.13400 条数据线性扫描 8ms 内解决加 Trie 树的收益抵不上维护成本。这两个回答其实指向同一个判断标准——先量数据规模再决定要不要上复杂结构而不是反过来。分享一个我自己的习惯。这套组件第一次交付时我以为列表和搜索都测过了就没问题结果上线第二天收到反馈说「编辑地址后回到的是上一个城市」。查了半天才发现是回显时三次异步回调的顺序问题测试用例里完全没有覆盖「从编辑入口进入」这个路径。从那以后我每次交付这类带状态回显的组件都会强制走一遍「首次进入、选择、确认、再次进入编辑、再改一次」这条完整链路一条不漏地手点不管自动化测试覆盖率写到了多少。这套检查清单也是那次之后补出来的希望帮到你。本文还有配套的精品资源点击获取
RELATED

相关推荐

SAP ABAP工作进程采样数据分析与性能优化

SAP ABAP工作进程采样数据分析与性能优化

1. 理解Sampled Work Process Data的本质在SAP ABAP系统中,工作进程(Work Process)是执行应用程序代码的实际载体。当系统运行时,每个工作进程都在处理不同类型的任务 - 可能是处理用户的交互请求,也可能是执行后台作业。Sampled Work Proces…

📅 2026/11/2 8:08:16
AI模型工程落地评估指南:可信度三角与热词因果链

AI模型工程落地评估指南:可信度三角与热词因果链

1. 这份“智能周报”不是新闻简报,而是模型落地的观测窗口“本周发布了哪些重要模型和应用?|智能周报(上)”——这个标题乍看像一份科技媒体快讯,但如果你真把它当新闻速读来处理,反而会错过它最…

📅 2026/11/2 8:08:16
Vue3+Element Plus实现24小时时间范围选择器

Vue3+Element Plus实现24小时时间范围选择器

1. 需求分析与技术选型在Vue3项目中使用Element Plus的el-date-picker组件实现时间范围筛选时,经常需要限制开始时间和结束时间的可选范围。本次要实现的业务需求包含以下几个关键点:时间范围限制在24小时内开始时间和结束时间需要联动控制时间格式需要精…

📅 2026/11/2 8:03:16
MORE NEWS

更多资讯

📰

React Server Components RCE漏洞分析与防御实践

1. React Server Components RCE漏洞深度解析CVE-2025-55182是React生态中近年来最严重的安全漏洞之一,它影响了React Server Components(RSC)的核心实现机制。这个漏洞本质上源于服务端组件序列化过程中的代码注入缺陷,攻击者可以…

📰

Flink实时处理淘客订单架构设计与实践

1. 淘客订单实时处理的业务挑战在传统淘客系统中,订单处理通常采用批处理模式,这种架构存在几个致命缺陷。以我们团队运营的微赚淘客系统为例,日均处理80万笔淘宝联盟订单回调时,批处理模式会导致用户从下单到看到返利状态更新平均…

📰

Shapefile文件族解析与shp地图显示实操指南

前阵子有个做区域规划的朋友丢过来一个压缩包,说里面是某片区的边界数据,让我帮忙“打开看看长什么样”。我解压出来一看,十几个同名不同后缀的文件摊在文件夹里,他以为只需要双击那个扩展名是 shp 的就能出图,结果双击…

📰

ICLR 2026预讲会:DAMO开发者矩阵与AI前沿技术

1. 项目概述ICLR 2026预讲会|DAMO开发者矩阵专场是一个面向人工智能研究者和开发者的前沿技术分享活动。作为机器学习领域的顶级会议ICLR(International Conference on Learning Representations)的预热活动,这个专场聚焦于DAMO开…

📰

Hindsight:工程师的技术复盘方法论与工程化实践

1. “Hindsight”不是工具名,而是开发者对技术决策的反思视角“Hindsight”这个词在当前技术社区里被高频提及,但它不是某个开源库、CLI 工具或 npm 包的正式名称——至少在 PyPI、npm registry 和 Docker Hub 的官方索引中,不存在一个叫hind…

📰

图论中的偏序关系与DAG拓扑排序实战解析

1. 问题背景与核心概念解析Codeforces 2195F这道题目出现在编程竞赛平台上,考察的是图论中偏序关系与DAG拓扑排序的综合应用。这类问题在实际工程中有着广泛的应用场景,比如任务调度、依赖管理、编译顺序确定等。偏序关系(Partial Order&…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬