区域选择组件(PC)

news/2025/12/1 12:29:24/文章来源:https://www.cnblogs.com/zx529/p/19292341

从零到一:开发高性能的PC端多选区域选择组件

在招聘需求发布场景中,常常需要选择多个地址,但组件库自带的级联选择往往存在局限性:只能选择到具体城市、无法多选父级地区、展示不够直观等。今天我将带大家从零开始,开发一个支持平铺多级展示、多选、搜索功能的PC端区域选择组件,并分享一些优化思路。

一、需求分析与组件设计

1. 为什么需要自定义区域选择组件?

在招聘系统中,HR发布职位时经常需要选择多个工作地点,比如"北京-海淀区"、"上海-浦东新区"等组合。现有组件存在以下问题:

  • 原生级联选择器只能单选,无法满足多选需求
  • 无法选择父级地区(如只选择"北京市"而不选择具体区)
  • 展示方式为树形结构,不够直观,操作效率低
  • 缺少已选地区的集中管理

原生级联选择组件功能展示:
image

优化后的组件:

image

2. 核心功能需求梳理

基于实际业务场景,我们需要实现以下功能:

  • 支持多选地区,包括父级和子级地区
  • 采用平铺方式展示地区数据,提高选择效率
  • 通过面包屑导航实现层级切换
  • 右侧面板展示已选地区,支持单独删除和一键清空
  • 支持国内/国外地区切换
  • 提供搜索功能快速定位地区

二、基础版本实现

如何实现地区数据的获取与处理?

首先,我们需要从接口获取地区数据并进行扁平化处理,以便后续操作:

async getTreeData() {try {const res = await request.get(`${service.atsService}/base/atsArea/queryAreaTree`,{homeAbroad: '1', // 1:国内,2:国外,这是接口约定的参数值})const { flatArr, idMap } = this.getFlattenTree(res.result)this.treeData = flatArrthis.treeIdObj = idMap// 100000是根节点ID,表示全国,这里获取所有省份数据this.showCity = this.treeData.filter((item) => item.parentId === '100000')if (this.value) {this.getCurrent()}} catch (e) {console.log('获取地区数据失败->', e)}
}// 树结构平铺,将嵌套的树形结构转换为一维数组,便于后续查找和操作
getFlattenTree(tree) {const flatArr = []const idMap = {} // 创建ID到节点的映射,实现O(1)复杂度查找tree.forEach((itemNode) => {flattenTree(itemNode)})function flattenTree(node) {const newNode = {title: node.title, // 地区名称id: node.id, // 地区IDkey: node.key, // 用于React/Vue列表渲染的唯一keychecked: false, // 选中状态parentId: node.parentId, // 父级IDleaf: node.leaf, // 是否为叶子节点(是否还有子级)}flatArr.push(newNode)idMap[node.id] = newNode// 递归处理子节点if (node.children && node.children.length > 0) {node.children.forEach((child) => flattenTree(child))}}return { flatArr, idMap }
}

这段代码的关键在于将树形结构的数据扁平化为一维数组,并创建一个以ID为键的映射表,大大提高了后续查找的效率。

如何实现地区多选功能?

多选是本组件的核心功能之一,实现方式如下:

// 点击了省市区的checkbox
onCheckboxChange(checked, checkItem) {if (checked) {// 将完整的地区路径保存(从省份到当前选择项)this.alreadyCheckedCity.push([...this.stackProvince, checkItem])}
}// 处理保存操作
handleSave() {this.selectCityCode = []this.selectOption = this.alreadyCheckedCity.map((item) => {if (Array.isArray(item) && item.length > 0) {// 拼接地区名称(如:北京/海淀区)const label = item.map((v) => v.title).join('/')// 拼接地区编码(如:110000,110108)const value = item.map((v) => v.key).join(',')this.selectCityCode.push(value)return { label, value }}return})// 使用'/'分隔多个地区选择(如:110000,110108/310000,310115)this.$emit('input', this.selectCityCode.join('/'))this.$emit('change', this.selectCityCode.join('/'))this.dropOpen = false
}

我们使用一个二维数组 alreadyCheckedCity 来存储已选地区,每条记录包含从省份到选择项的完整路径,这样既可以保存完整的地区层级信息,又能方便地拼接展示文本。

如何实现层级导航和展示?

为了实现直观的层级导航,我们使用面包屑组件并结合动态渲染:

// 点击了省市区的文字,进入下一级
childCheck(child) {if (child.leaf) return // 如果是叶子节点(如:北京市的区),则不继续深入this.stackProvince.push(child) // 将当前地区添加到路径栈中// 过滤出当前地区的所有子地区this.showCity = this.treeData.filter((item) => item.parentId === child.id)
}// 面包屑点击事件,返回上一级
onBreadClick(item, index) {// 显示对应层级的子地区this.showCity = this.treeData.filter(child => child.parentId === item.id)// 截断路径栈,保留到点击的层级this.stackProvince = this.stackProvince.slice(0, index)
}// 渲染地区列表
renderContent() {return (<div class="city-box"><div class="provice-title"><a-breadcrumb separator=">"><a-breadcrumb-item><span style="cursor:pointer" onClick={() => {// 点击"全部省份"返回根层级this.showCity = this.treeData.filter(item => item.parentId === '100000')this.stackProvince = []}}>全部省份</span></a-breadcrumb-item>{this.stackProvince.map((item, index) => {return (!item.leaf && (<a-breadcrumb-item href="javascript:void(0);"><span onClick={() => this.onBreadClick(item, index)}>{item.title}</span></a-breadcrumb-item>))})}</a-breadcrumb></div>{this.showCity.length > 0 &&this.showCity.map((item) => (<div class="city-item"><span class="check-content"><a-checkboxv-model={item.checked}onChange={(e) => this.onCheckboxChange(e.target.checked, item)}></a-checkbox></span><div style="display:flex;align-items:center;" onClick={() => this.childCheck(item)}><a-tooltip title={item.title.length > 4 ? item.title : null}><span class={item.leaf ? 'city-name' : 'city-name city-name--hover'}>{item.title}</span></a-tooltip>{!item.leaf && <span class="arrow">{this.rightArrowSvg()}</span>}</div></div>))}</div>)
}

这种实现方式让用户可以像浏览文件夹一样逐层深入选择地区,同时保持了清晰的层级关系。

[展示层级导航效果图]

如何实现已选地区的管理?

为了方便用户管理已选地区,我们在右侧面板展示所有已选项,并提供删除功能:

// 清空已选或删除单个选项
onClear(index) {if (index === undefined) {// 清空所有选中项this.alreadyCheckedCity.forEach(item => {if (Array.isArray(item) && item.length > 0) {const lastChild = item[item.length - 1]lastChild.checked = false}})this.alreadyCheckedCity = []} else {// 删除单个选中项const item = this.alreadyCheckedCity[index]if (Array.isArray(item) && item.length > 0) {const lastChild = item[item.length - 1]lastChild.checked = false}this.alreadyCheckedCity.splice(index, 1)}
}// 渲染右侧已选面板
renderRightShow() {return (<div class="right-show" ref="rightShowRef"><div><div class="check-header"><div><span class="show__title">已选地区</span><span class="show__count"><span class="cur__count">{this.alreadyCheckedCity.length}</span>/99</span></div><span class="clear_check" onClick={() => this.onClear()}>清空已选</span></div><ul class="selected-box">{this.alreadyCheckedCity.map((item, index) => (<li class="show-item"><span class="show__name">{Array.isArray(item) && item.map((item) => item.title).join('/')}</span><span class="show__close" onClick={() => this.onClear(index)}><a-icon type="close-circle" style="color:#bfc3c7;width: 8px;" /></span></li>))}</ul></div></div>)
}

三、组件优化方案

1. 性能优化:如何避免重复渲染和无效计算?

问题分析
当前实现中,每次数据变化都可能导致组件的重新渲染,影响性能。

优化方案

// 优化前:直接在模板中使用方法
renderContent() {// ... 渲染逻辑
}// 优化后:使用计算属性缓存结果
computed: {displayCityList() {// 只在依赖数据变化时重新计算return this.treeData.filter(item => item.parentId === this.currentParentId)}
}// 优化前:频繁操作DOM
mounted() {document.addEventListener('mousedown', this.handleOutsideClick)
}beforeDestroy() {document.removeEventListener('mousedown', this.handleOutsideClick)
}// 优化后:使用Vue指令或ref
// 在组件内部使用自定义指令或ref来处理点击外部关闭的逻辑

2. 用户体验优化:如何实现搜索功能?

问题分析
当前组件缺少搜索功能,当地区数量较多时,用户查找困难。

优化方案

// 添加搜索功能
data() {return {// ... 原有数据searchKeyword: ''}
},computed: {filteredCities() {if (!this.searchKeyword) {return this.showCity}return this.showCity.filter(item => item.title.includes(this.searchKeyword))}
},methods: {// 搜索方法handleSearch(e) {this.searchKeyword = e.target.value}
}// 在模板中添加搜索框
// <input type="text" v-model="searchKeyword" placeholder="搜索地区" />

[展示搜索功能效果图]

3. 代码健壮性:如何处理异步加载和错误状态?

问题分析
当前组件在数据加载过程中没有状态提示,错误处理也比较简单。

优化方案

data() {return {// ... 原有数据loading: false,error: null}
},async getTreeData() {this.loading = truethis.error = nulltry {// ... 原有数据获取逻辑} catch (e) {console.error('获取地区数据失败:', e)this.error = '获取地区数据失败,请稍后重试'} finally {this.loading = false}
}// 在模板中添加加载和错误状态展示
// {this.loading && <div class="loading">加载中...</div>}
// {this.error && <div class="error">{this.error}</div>}

4. 代码结构优化:如何分离关注点?

问题分析
当前组件既负责数据获取又负责展示,不符合单一职责原则。

优化方案

// 1. 将数据获取逻辑抽离为单独的服务
// src/services/areaService.js
import request from '@/utils/http'
import service from '@/api/service'export const getAreaTree = async (type = '1') => {try {// 调用地区树接口,参数type=1表示国内,type=2表示国外const res = await request.get(`${service.atsService}/base/atsArea/queryAreaTree`, {homeAbroad: type})return res.result} catch (error) {console.error('获取地区树失败:', error)throw error}
}export const flattenAreaTree = (tree) => {// 扁平化处理逻辑...
}// 2. 在组件中使用
import { getAreaTree, flattenAreaTree } from '@/services/areaService'// 组件内使用
async getTreeData() {this.loading = truetry {const treeData = await getAreaTree('1') // 获取国内地区数据const { flatArr, idMap } = flattenAreaTree(treeData)// ... 后续处理} catch (error) {// 错误处理} finally {this.loading = false}
}

5. 功能扩展:如何支持更多自定义配置?

问题分析
当前组件的配置项较少,难以适应不同的业务场景。

优化方案

props: {// ... 原有属性maxSelectCount: {type: Number,default: 99 // 默认最大选择数量},showSearch: {type: Boolean,default: true // 是否显示搜索框},showForeign: {type: Boolean,default: true // 是否显示国外选项},placeholder: {type: String,default: '请选择地区'},// 支持自定义地区数据,适用于没有接口的情况customAreaData: {type: Array,default: () => []}
},// 在数据获取时优先使用自定义数据
async getTreeData() {if (this.customAreaData && this.customAreaData.length > 0) {const { flatArr, idMap } = this.getFlattenTree(this.customAreaData)this.treeData = flatArrthis.treeIdObj = idMapthis.showCity = this.treeData.filter(item => item.parentId === '100000')if (this.value) {this.getCurrent()}return}// 原有接口获取逻辑...
}

四、最佳实践与总结

  1. 性能优先:合理使用计算属性、缓存数据、避免不必要的DOM操作
  2. 用户体验:提供搜索、加载状态、错误提示等增强功能
  3. 代码健壮性:完善错误处理、边界情况检查
  4. 可维护性:遵循单一职责原则,分离数据获取和展示逻辑
  5. 扩展性:提供足够的配置项,支持不同业务场景

通过以上优化,我们的地区选择组件不仅功能完善,而且性能优良,能够为用户提供流畅直观的操作体验。在实际项目中,你还可以根据具体需求进一步定制和扩展。

五、完整优化版本代码

下面是整合了以上优化点的完整组件代码:

import request from '@/utils/http'
import service from '@/api/service'export default {name: 'AreaSelect',props: {value: {type: String,default: '',// 父组件传递的已选地区值,格式为:110000,110108/310000,310115},disabled: {type: Boolean,default: false},placeholder: {type: String,default: '请选择'},maxSelectCount: {type: Number,default: 99 // 最大选择数量限制},showSearch: {type: Boolean,default: true // 是否显示搜索框},showForeign: {type: Boolean,default: true // 是否显示国外选项},customAreaData: {type: Array,default: () => [] // 自定义地区数据,优先级高于接口数据}},data() {return {treeData: [], // 扁平化后的完整地区数据treeIdObj: {}, // 以ID为键的地区数据映射,用于快速查找showCity: [], // 当前页面展示的地区数据dropOpen: false, // 下拉面板是否展开tabKey: '1', // 当前选中的标签页(1:国内,2:国外)stackProvince: [], // 当前面包屑路径栈alreadyCheckedCity: [], // 已选地区列表,二维数组保存完整路径selectOption: [], // 已选地区的选项格式,用于select组件显示selectCityCode: [], // 已选地区的编码列表searchKeyword: '', // 搜索关键词loading: false, // 数据加载状态error: null // 错误信息}},computed: {// 过滤后的地区列表,支持搜索功能filteredCities() {if (!this.searchKeyword) {return this.showCity}return this.showCity.filter(item => item.title.toLowerCase().includes(this.searchKeyword.toLowerCase()))},// 是否可以继续添加地区canAddMore() {return this.alreadyCheckedCity.length < this.maxSelectCount},// 当前父级ID,用于计算属性依赖追踪currentParentId() {return this.stackProvince.length > 0 ? this.stackProvince[this.stackProvince.length - 1].id : '100000'}},watch: {// 监听value变化,同步选中状态value: {immediate: true,handler(val) {if (val && this.treeData.length > 0) {this.getCurrent()}}}},created() {this.getTreeData()},mounted() {// 添加点击外部关闭下拉面板的事件监听document.addEventListener('mousedown', this.handleOutsideClick)},beforeDestroy() {// 组件销毁前移除事件监听document.removeEventListener('mousedown', this.handleOutsideClick)},methods: {// 点击外部关闭下拉面板handleOutsideClick(e) {const dropRenderBox = document.querySelector('.drop-render-box')if (dropRenderBox && !dropRenderBox.contains(e.target)) {this.dropOpen = false}},// 获取地区树数据async getTreeData() {this.loading = truethis.error = nulltry {// 优先使用自定义数据if (this.customAreaData && this.customAreaData.length > 0) {const { flatArr, idMap } = this.getFlattenTree(this.customAreaData)this.treeData = flatArrthis.treeIdObj = idMap} else {// 调用接口获取数据,1表示国内地区const res = await request.get(`${service.atsService}/base/atsArea/queryAreaTree`, {homeAbroad: '1',})const { flatArr, idMap } = this.getFlattenTree(res.result)this.treeData = flatArrthis.treeIdObj = idMap}// 默认显示全国所有省份(parentId='100000')this.showCity = this.treeData.filter(item => item.parentId === '100000')// 如果有初始值,同步选中状态if (this.value) {this.getCurrent()}} catch (e) {console.error('获取地区数据失败:', e)this.error = '获取地区数据失败,请稍后重试'} finally {this.loading = false}},// 根据传入的value同步选中状态getCurrent() {const val = this.value// 按'/'分割多个地区选择const selectedAllCode = val.split('/')this.selectCityCode = [...selectedAllCode]// 处理每个地区选择this.selectCityCode.forEach((item) => {const codes = item.split(',') // 按','分割地区编码路径// 查找每个编码对应的地区信息const arr = codes.reduce((pre, cur) => {pre.push(this.treeIdObj[cur])return pre}, [])this.alreadyCheckedCity.push(arr)})// 合并所有选中的地区编码并去重const selectedCode = [...new Set(selectedAllCode.reduce((pre, cur) => {pre.push(...cur.split(','))return pre}, [])),]// 生成select组件的选项格式this.selectOption = selectedAllCode.map((value) => {const label = value.split(',').map((c) => this.treeIdObj[c].title).join('/')return { label, value }})// 更新选中状态selectedCode.forEach((code) => {const item = this.treeIdObj[code]if (item) {item.checked = true}})},// 树结构平铺处理getFlattenTree(tree) {const flatArr = []const idMap = {}// 递归处理树节点function flattenTree(node) {const newNode = {title: node.title,id: node.id,key: node.key,checked: false,parentId: node.parentId,leaf: node.leaf,}flatArr.push(newNode)idMap[node.id] = newNode// 递归处理子节点if (node.children && node.children.length > 0) {node.children.forEach((child) => flattenTree(child))}}// 处理顶层节点tree.forEach((itemNode) => {flattenTree(itemNode)})return { flatArr, idMap }},// 渲染右侧箭头图标rightArrowSvg() {return (<svgwidth="10px"height="10px"viewBox="0 0 18 18"color="#146aff"class="area-icon-right"><g fill="none" fill-rule="evenodd"><path d="M0 0h18v18H0z"></path><pathd="M10.5 9L5.304 3.04a1.32 1.32 0 0 1 .003-1.691.956.956 0 0 1 1.473-.004l5.914 6.786c.21.24.31.555.305.869.006.314-.096.63-.305.87l-5.914 6.785a.956.956 0 0 1-1.473-.004 1.32 1.32 0 0 1-.003-1.69L10.499 9z"fill="currentColor"></path></g></svg>)},// 处理checkbox选择事件onCheckboxChange(checked, checkItem) {if (checked) {// 添加到已选列表,保存完整路径this.alreadyCheckedCity.push([...this.stackProvince, checkItem])} else {// 取消选择时,从已选列表中移除const index = this.alreadyCheckedCity.findIndex(item => item.length > 0 && item[item.length - 1].id === checkItem.id)if (index > -1) {this.alreadyCheckedCity.splice(index, 1)}}},// 点击地区名称进入下一级childCheck(child) {if (child.leaf) return // 叶子节点不能继续深入this.stackProvince.push(child)// 过滤出当前地区的子地区this.showCity = this.treeData.filter((item) => item.parentId === child.id)},// 面包屑点击事件onBreadClick(item, index) {// 显示对应层级的子地区this.showCity = this.treeData.filter((child) => child.parentId === item.id)// 截断路径栈this.stackProvince = this.stackProvince.slice(0, index)},// 搜索处理handleSearch(e) {this.searchKeyword = e.target.value},// 清空已选或删除单个选项onClear(index) {if (index === undefined) {// 清空所有选中项this.alreadyCheckedCity.forEach(item => {if (Array.isArray(item) && item.length > 0) {const lastChild = item[item.length - 1]lastChild.checked = false}})this.alreadyCheckedCity = []} else {// 删除单个选中项const item = this.alreadyCheckedCity[index]if (Array.isArray(item) && item.length > 0) {const lastChild = item[item.length - 1]lastChild.checked = false}this.alreadyCheckedCity.splice(index, 1)}},// 处理取消操作handleCancel() {this.dropOpen = false},// 处理保存操作handleSave() {this.selectCityCode = []this.selectOption = this.alreadyCheckedCity.map((item) => {if (Array.isArray(item) && item.length > 0) {const label = item.map((v) => v.title).join('/')const value = item.map((v) => v.key).join(',')this.selectCityCode.push(value)return { label, value }}return}).filter(Boolean)// 触发input和change事件,将选中的地区编码通过'/'分隔的字符串形式传递给父组件this.$emit('input', this.selectCityCode.join('/'))this.$emit('change', this.selectCityCode.join('/'))this.dropOpen = false},// 渲染下拉内容dropdownRender() {const tabOptions = [{ title: '国内', key: '1' },{ title: '国外', key: '2' }]// 如果不显示国外选项,过滤掉国外标签const displayTabs = this.showForeign ? tabOptions : tabOptions.filter(tab => tab.key === '1')return (<div class="drop-render-box"><div class="content-box"><div class="left-check"><a-tabs animated={false} v-model={this.tabKey}>{displayTabs.map((item) => (<a-tab-pane key={item.key} tab={item.title}></a-tab-pane>))}</a-tabs>{/* 加载状态 */}{this.loading && <div class="loading">加载中...</div>}{/* 错误状态 */}{this.error && <div class="error">{this.error}</div>}{/* 国内地区内容 */}{!this.loading && !this.error && this.tabKey === '1' && this.renderContent()}{/* 国外地区内容 */}{!this.loading && !this.error && this.tabKey === '2' && this.renderForeignContent()}</div>{/* 右侧已选地区面板 */}{this.renderRightShow()}</div>{/* 底部按钮 */}<div class="drop-footer-btn"><a-buttonsize="small"style="margin-right: 12px;"onClick={this.handleCancel}>取消</a-button><a-buttonsize="small"type="primary"onClick={() => this.handleSave()}>确定</a-button></div></div>)},// 渲染国内地区内容renderContent() {return (<div class="city-box">{/* 面包屑导航 */}<div class="provice-title"><a-breadcrumb separator=">"><a-breadcrumb-item><spanstyle="cursor:pointer"onClick={() => {this.showCity = this.treeData.filter((item) => item.parentId === '100000')this.stackProvince = []}}>全部省份</span></a-breadcrumb-item>{this.stackProvince.map((item, index) => {return (!item.leaf && (<a-breadcrumb-item href="javascript:void(0);"><spanonClick={() => {this.onBreadClick(item, index)}}>{item.title}</span></a-breadcrumb-item>))})}</a-breadcrumb></div>{/* 搜索框 */}{this.showSearch && (<div class="search-container"><inputtype="text"placeholder="搜索地区"value={this.searchKeyword}onChange={this.handleSearch}/></div>)}{/* 地区列表 */}{this.filteredCities.length > 0 &&this.filteredCities.map((item) => (<div class="city-item"><span class="check-content"><a-checkboxv-model={item.checked}disabled={!this.canAddMore && !item.checked}onChange={(e) => {this.onCheckboxChange(e.target.checked, item)}}></a-checkbox></span><divstyle="display:flex;align-items:center;"onClick={() => this.childCheck(item)}><a-tooltip title={item.title.length > 4 ? item.title : null}><spanclass={item.leaf ? 'city-name' : 'city-name city-name--hover'}>{item.title}</span></a-tooltip>{!item.leaf && (<span class="arrow">{this.rightArrowSvg()}</span>)}</div></div>))}{/* 无搜索结果提示 */}{this.searchKeyword && this.filteredCities.length === 0 && (<div class="no-result">暂无匹配结果</div>)}</div>)},// 渲染国外地区内容renderForeignContent() {return <div class="city-box">暂无数据</div>},// 渲染右侧已选面板renderRightShow() {return (<div class="right-show" ref="rightShowRef"><div><div class="check-header"><div><span class="show__title">已选地区</span><span class="show__count"><span class="cur__count">{this.alreadyCheckedCity.length}</span>/{this.maxSelectCount}</span></div><span class="clear_check" onClick={() => this.onClear()} style={{cursor: this.alreadyCheckedCity.length > 0 ? 'pointer' : 'not-allowed'}}>清空已选</span></div><ul class="selected-box">{this.alreadyCheckedCity.map((item, index) => {return (<li class="show-item"><span class="show__name">{Array.isArray(item) &&item.map((item) => item.title).join('/')}</span><spanclass="show__close"onClick={() => {this.onClear(index)}}><a-icontype="close-circle"style="color:#bfc3c7;width: 8px;"/></span></li>)})}</ul></div></div>)}},// 组件渲染render() {return (<div class="area-select"><a-selectstyle="width:100%;max-width:300px"placeholder={this.placeholder}open={this.dropOpen}maxTagCount={1}autoClearSearchValue={true}mode="multiple"disabled={this.disabled}v-model={this.selectCityCode}dropdownRender={this.dropdownRender}onFocus={() => {this.dropOpen = true}}options={this.selectOption}></a-select></div>)},
}
</script>
<style lang="less" scoped>
.drop-render-box {position: relative;width: 580px;height: 350px;background: #fff;padding: 10px 16px 16px;border-radius: 8px;border: 1px solid #ccc;font-size: 12px;color: #0e1114;.search-container {padding: 0px 10px 10px;/deep/ .ant-input {border-radius: 8px;height: 30px;}}.content-box {width: 100%;display: flex;border-bottom: 1px solid #f0f2f5;.left-check {width: 80%;box-sizing: border-box;border-right: 1px solid #f0f2f5;}.right-show {width: 40%;.check-header {height: 40px;line-height: 40px;display: flex;justify-content: space-between;align-items: center;padding-left: 8px;.show__title {margin-right: 6px;}.show__count {color: #bfc3c7;.cur__count {color: #565e66;}}.clear_check {cursor: pointer;}}.selected-box {overflow-y: auto;max-height: 250px;&::-webkit-scrollbar {width: 4px;}&::-webkit-scrollbar-thumb {background: #888;border-radius: 4px;}}.show-item {display: inline-block;margin-left: 10px;padding: 4px 0;.show__name {margin-right: 4px;}.show__close {cursor: pointer;}}}.city-box {height: 230px;overflow-y: auto;display: flex;align-items: center;align-content: flex-start;flex-wrap: wrap;/deep/ .ant-checkbox-inner {width: 12px;height: 12px;&::after {top: 4px;left: 1px;width: 5px;height: 10px;}}row-gap: 8px;&::-webkit-scrollbar {width: 4px;}&::-webkit-scrollbar-track {background: #f1f1f1;}&::-webkit-scrollbar-thumb {background: #888;border-radius: 4px;}}.provice-title {width: 100%;padding-bottom: 2px;background: #fff;z-index: 99;position: sticky;top: 0;}.city-item {width: 110px;margin-left: 4px;display: flex;align-items: center;.check-content {margin-right: 6px;}.city-name {display: inline-block;max-width: 60px;font-size: 12px;cursor: pointer;text-overflow: ellipsis;overflow: hidden;white-space: nowrap;}.city-name--hover {&:hover {color: #1890ff;+ .arrow {display: inline;}}}.arrow {display: none;}}.loading,.error,.no-result {width: 100%;height: 230px;display: flex;align-items: center;justify-content: center;color: #999;}.error {color: #ff4d4f;}}.drop-footer-btn {position: absolute;bottom: 0;right: 0;display: flex;justify-content: flex-end;padding: 8px 10px;/deep/ .ant-btn {display: inline-block;width: 60px;height: 28px;text-align: center;line-height: 28px;}}
}
</style>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/982837.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Wyrm + MNE 工具链完整介绍

🧠 Wyrm + MNE 工具链完整介绍 🧩 一句话总结 MNE = 科研级 EEG 工具(可视化、预处理、ICA) Wyrm = 工程级 BCI 工具箱(Epoch、CSP、快速特征处理) 两者组合:MNE 负责“干净数据”,Wyrm 负责“工程特征 + 训…

CF767E-Change-free

CF767E-Change-free 题目大意 你接下来 \(n\) 天回去食堂吃饭,而且现在你已经决定好了吃什么,所以你在接下来的第 \(i\) 天,花费 \(c_i\) 元。 交易时只允许使用 \(1\) 元的硬币和 \(100\) 元的纸币,你初始有 \(m\…

哈尔滨装修安装工程企业TOP5权威推荐:助力洁净空间品质升级

在医疗与工业领域的专业化建设进程中,企业对合规化、高洁净度的装修安装工程需求持续攀升。2024年黑龙江省医疗洁净工程市场规模突破20亿元,年增速达32%,但行业调研显示,35%的客户投诉集中在洁净度不达标、空间布局…

炼油设备厂家TOP5权威推荐:甄选靠谱大型厂家,赋能工业固废

工业固废资源化利用需求攀升,2024年相关设备市场规模突破360亿元,年增速达32%,但中小企业采购常遇坑:初期成本高致资金压力大、设备工况适配性差效率骤降、操作复杂缺运维团队、与生产衔接不畅影响产能。本榜单基于…

WMS 仓库管理系统怎么选择?

推荐排名①:首选 — 北京鸿链科技有限公司(鸿链科技) 公司介绍 北京鸿链科技有限公司(简称「鸿链科技」)成立于2016年9月,由招商局集团与中国外运联合孵化,是国内领先的仓储物流软件SaaS平台服务商。其核心产品…

VW/Audi MQB All Keys Lost: Hassle-Free SYNC Data Calculations with Xhorse VVDI Autel

Solving the MQB All Keys Lost Dilemma: SYNC Data Solutions for VW & Audi The Challenge: All Keys Lost for VW/Audi MQB Models For European and American automotive repair shops and car owners, a frus…

快捷键和Dos命令

Ctrl+c负责 Ctrl+v粘贴 Ctrl+a全选 Ctrl+x剪切 Ctrl+z撤销 Ctrl+s保存 Ctrl+y复原 win+r 运行 win+e 打开我的电脑 Ctrl+shift+esc任务管理器 shift+delelte永久删除 Alt+F4 关闭窗口 打开cmd方式开始-系统-命令提示符…

2025年哈尔滨十大有名的装修安装工程公司推荐,口碑不错的装

在医疗与工业领域,洁净空间的装修安装工程是保障安全运营与高效生产的核心基石。哈尔滨净朗净化科技有限公司作为本地洁净工程领域的标杆企业,始终以科创之力破解行业痛点。面对市场上良莠不齐的装修安装工程服务,如…

2025年度全屋定制品牌生产厂哪家更值得选?5大实力厂商排行

为帮助装修业主、家居经销商精准锁定适配需求的全屋定制生产厂,避开报价虚高、板材以次充好、落地效果偏差等坑,本文从生产实力(工业4.0标准、供应链管控)、产品品质(环保等级、工艺细节)、服务保障(质保政策、…

列表弹窗实现方案整理

列表弹窗实现方案整理 在Web应用开发中,列表页的编辑和新增功能是非常常见的交互场景。通常我们会通过点击"新增"或"编辑"按钮,打开一个弹窗(Modal/Dialog)来展示表单信息,用户填写或修改数据…

硬盘检测修复工具!实时监测硬盘健康度、温度、还能修复扇区!

如何检测SSD(固态硬盘)是不是二手(翻新)的?有检测固态硬盘是否损坏的工具吗?坏道检测对硬盘伤害大吗?安装系统时检测不到固态硬盘怎么办?有检测固态硬盘是否损坏的工具吗?如何检测SSD(固态硬盘)是不是二手(…

【日记】第一次约拍别人呢(1165 字)

正文早上被行里安装卷帘门的声音吵醒。我也不知道那个是不是,反正听起来很像。非常吵。起床看了一下时间,九十点吧好像。之后就去帮 Lulu 找钻石了。MC 版本更新之后,钻石好像比以前要好找一些了。坐标也有大的改动…

02.Class对象的理解

嘿嘿,接下来一步步拆解吧!!! 1.在标准 Java 编译过程中(通过javac编译器编译.java文件),所有类都会生成对应的.class文件那么要是当前程序没有用到某个类,那个类也会生成对应的.class文件吗?答案是:会! 原因…

2025年12月楼梯厂家最新十大品牌推荐,技术实力与市场口碑深度解析,家装定制品牌榜及选择指南,更能一站式搞定木门/衣柜/橱柜/护墙板

随着人们对家居生活品质要求的不断提升,楼梯行业迎来了快速发展的机遇。本榜单基于产品品质、设计创新、服务体系、行业影响力四大维度,结合行业权威数据及消费者反馈,对2025年十大楼梯品牌的综合实力进行深度解析,…

2025哈尔滨净化改造工程TOP5权威推荐:甄选企业守护洁净

在医疗、工业及商业场景中,洁净空间改造工程是保障环境安全、提升运营效率的核心环节。2024年黑龙江省洁净工程市场规模突破12亿元,年增速达32%,但行业投诉数据显示,35%的纠纷集中在洁净度不达标、空间布局不合理、…

全屋定制制造厂TOP5权威推荐:售后与品质双优之选,破解行业

在消费升级与家居个性化需求激增的背景下,全屋定制市场规模持续扩张,但行业乱象也随之凸显:85%的消费者曾遭遇增项加价设计不符售后推诿等问题,而全屋定制制造厂哪家售后好、全屋定制品牌制造厂哪个值得选、全屋定…

Windows 11网络共享文件夹无法访问

Windows 11网络共享文件夹无法访问 Created: 12/1/2025 11:22:42 Updated: 12/1/2025 11:23:00 Exported: 12/1/2025 12:09:13 Link: https://claude.ai/chat/6e09c84d-d44c-4c56-868a-27e3d8a3cac9 Prompt: 2025/12/1…

2025 TOPDON ArtiDiag 900 Lite 8 Scan Tool: Full System Diagnostics 8 Resets for EU/US Cars

The 2025 TOPDON ArtiDiag 900 Lite: Your All-in-One Diagnostic Solution for European & American Vehicles Problem: Diagnosing Modern Vehicles Isn’t What It Used To Be Today’s European and American …

2025年黑龙江十大改造工程专业公司推荐:改造工程公司

本榜单依托黑龙江地区洁净工程行业调研与真实客户口碑,聚焦医疗、工业改造工程核心需求,筛选出五家标杆企业,为机构选型提供客观依据,助力精准匹配专业改造伙伴。 TOP1 推荐:哈尔滨净朗净化科技有限公司 推荐指…