chore: initialize qiming workspace repository

This commit is contained in:
Codex
2026-05-29 14:22:48 +08:00
commit bfd67a0f2c
10750 changed files with 1885711 additions and 0 deletions

View File

@@ -0,0 +1,274 @@
# l-cascader 多选级联选择器使用指南
## 概述
l-cascader 组件现已支持单选和多选两种模式,类似 Ant Design 的 Cascader 级联多选组件。多选模式下使用 lime-checkbox 组件作为选择框。
## 基本用法
### 单选模式(默认)
```vue
<template>
<view>
<l-cascader
:visible="showSingle"
:options="options"
v-model="singleValue"
title="单选级联选择"
@change="onSingleChange"
@finish="onSingleFinish"
/>
<button @click="showSingle = true">打开单选级联选择器</button>
</view>
</template>
<script setup lang="uts">
const showSingle = ref(false)
const singleValue = ref('')
const options = [
{
label: '浙江省',
value: 'zhejiang',
children: [
{
label: '杭州市',
value: 'hangzhou',
children: [
{ label: '西湖区', value: 'xihu' },
{ label: '余杭区', value: 'yuhang' }
]
},
{
label: '宁波市',
value: 'ningbo',
children: [
{ label: '海曙区', value: 'haishu' },
{ label: '江北区', value: 'jiangbei' }
]
}
]
},
{
label: '江苏省',
value: 'jiangsu',
children: [
{
label: '南京市',
value: 'nanjing',
children: [
{ label: '玄武区', value: 'xuanwu' },
{ label: '鼓楼区', value: 'gulou' }
]
}
]
}
]
const onSingleChange = (value: string, selectedOptions: any[]) => {
console.log('单选值变化:', value, selectedOptions)
}
const onSingleFinish = () => {
console.log('单选完成:', singleValue.value)
showSingle.value = false
}
</script>
```
### 多选模式
```vue
<template>
<view>
<l-cascader
:visible="showMultiple"
:options="options"
:multiple="true"
v-model:multipleValue="multipleValue"
title="多选级联选择"
:maxTagCount="5"
:showCheckedCount="true"
@change="onMultipleChange"
@finish="onMultipleFinish"
/>
<button @click="showMultiple = true">打开多选级联选择器</button>
<view v-if="multipleValue.length > 0">
<text>已选择: {{ multipleValue.join(', ') }}</text>
</view>
</view>
</template>
<script setup lang="uts">
const showMultiple = ref(false)
const multipleValue = ref<string[]>([])
const onMultipleChange = (values: string[], selectedOptions: any[]) => {
console.log('多选值变化:', values, selectedOptions)
}
const onMultipleFinish = () => {
console.log('多选完成:', multipleValue.value)
showMultiple.value = false
}
</script>
```
## 属性说明
### 多选相关属性
| 属性名 | 类型 | 默认值 | 说明 |
|--------|------|--------|------|
| multiple | boolean | false | 是否启用多选模式 |
| multipleValue | string[] | [] | 多选模式下的选中值数组支持v-model |
| defaultMultipleValue | string[] | [] | 多选模式下的默认选中值数组 |
| maxTagCount | number | -1 | 最大选择数量,-1表示无限制 |
| showCheckedCount | boolean | true | 是否显示选中数量 |
### 其他属性
| 属性名 | 类型 | 默认值 | 说明 |
|--------|------|--------|------|
| visible | boolean | false | 控制组件显示/隐藏 |
| options | UTSJSONObject[] | [] | 数据源 |
| title | string | - | 主标题文本 |
| placeholder | string | '选择选项' | 未选择时的提示文字 |
| checkStrictly | boolean | false | 父子节点选择是否关联 |
| closeable | boolean | true | 是否显示关闭按钮 |
| activeColor | string | '#3283ff' | 选中状态主题色 |
## 事件说明
| 事件名 | 参数 | 说明 |
|--------|------|------|
| change | (value: string \| string[], selectedOptions: any[]) | 选项变化时触发单选返回string多选返回string[] |
| pick | (level: number, index: number, value: string, selectedIndexes: number[]) | 选中时触发 |
| close | - | 关闭时触发 |
| finish | - | 点击完成时触发 |
## 数据格式
### 选项数据结构
```typescript
interface CascaderOption {
label: string; // 显示文本
value: string; // 选项值
children?: CascaderOption[]; // 子选项
disabled?: boolean; // 是否禁用
color?: string; // 自定义颜色
}
```
### 字段别名配置
```vue
<l-cascader
:options="options"
:keys="{ label: 'name', value: 'id', children: 'subItems' }"
/>
```
## 高级用法
### 自定义字段别名
```vue
<template>
<l-cascader
:visible="show"
:options="customOptions"
:keys="{ label: 'name', value: 'code', children: 'subItems' }"
v-model:multipleValue="values"
:multiple="true"
/>
</template>
<script setup lang="uts">
const customOptions = [
{
name: '华东地区',
code: 'east',
subItems: [
{
name: '上海',
code: 'shanghai',
subItems: [
{ name: '浦东新区', code: 'pudong' },
{ name: '黄浦区', code: 'huangpu' }
]
}
]
}
]
</script>
```
### 限制选择数量
```vue
<l-cascader
:visible="show"
:options="options"
:multiple="true"
:maxTagCount="3"
v-model:multipleValue="values"
/>
```
### 禁用某些选项
```vue
<script setup lang="uts">
const options = [
{
label: '浙江省',
value: 'zhejiang',
children: [
{
label: '杭州市',
value: 'hangzhou',
disabled: true, // 禁用此选项
children: [
{ label: '西湖区', value: 'xihu' }
]
}
]
}
]
</script>
```
## 注意事项
1. **单选模式**:使用 `v-model` 绑定单个字符串值
2. **多选模式**:使用 `v-model:multipleValue` 绑定字符串数组
3. **父子关联**`checkStrictly` 为 false 时,选择父节点会自动选择所有子节点
4. **最大选择数**`maxTagCount` 设置为 -1 表示无限制
5. **样式定制**:可以通过 CSS 变量自定义主题色和样式
## 样式定制
```scss
// 自定义主题色
.l-cascader {
--l-cascader-icon-color: #ff6b6b;
--l-cascader-cell-title-color: #333;
}
// 自定义checkbox样式
.l-cascader__checkbox {
.l-checkbox {
--l-checkbox-icon-checked-color: #ff6b6b;
}
}
```
## 兼容性
- 支持 H5、小程序、App 多端
- 基于 uni-app-x 框架开发
- 使用 UTS 语言编写
- 兼容 lime-checkbox 组件

View File

@@ -0,0 +1,112 @@
# l-cascader 多选功能测试指南
## 测试页面
已创建测试页面:`/pages/test-cascader/test-cascader.uvue`
## 测试内容
### 1. 单选模式测试
- 点击"打开单选级联选择器"按钮
- 选择任意选项,验证单选功能
- 检查选中值是否正确显示
### 2. 多选模式测试
- 点击"打开多选级联选择器"按钮
- 验证 checkbox 是否正常显示
- 选择多个选项,验证多选功能
- 检查选中值和数量是否正确显示
### 3. 限制选择数量测试
- 点击"打开限制3个的多选级联选择器"按钮
- 尝试选择超过3个选项
- 验证是否显示限制提示
- 检查选中数量是否正确限制
## 修复的问题
### 1. new-conversation-set.uvue 中的问题
- **问题**:添加 `:multiple="true"` 后组件无法点击checkbox 不显示
- **原因**
- 使用了错误的 v-model 绑定(应该用 `v-model:multipleValue`
- 数据类型不匹配(单选用 string多选用 string[]
- **修复**
- 修改为 `v-model:multipleValue="cascaderMultipleValue"`
- 添加 `cascaderMultipleValue` 响应式数据
- 修改 `:multiple="isMultiple"` 动态控制模式
### 2. l-cascader 组件中的问题
- **问题**checkbox 组件属性使用错误
- **原因**:使用了不存在的 `checked` 属性
- **修复**:改为使用 `modelValue``@update:modelValue`
## 使用方法
### 单选模式
```vue
<l-cascader
:visible="show"
:options="options"
v-model="singleValue"
title="单选级联选择"
@change="onChange"
/>
```
### 多选模式
```vue
<l-cascader
:visible="show"
:options="options"
:multiple="true"
v-model:multipleValue="multipleValue"
title="多选级联选择"
@change="onChange"
/>
```
### 限制选择数量
```vue
<l-cascader
:visible="show"
:options="options"
:multiple="true"
:maxTagCount="3"
v-model:multipleValue="limitedValue"
title="限制选择数量"
@change="onChange"
/>
```
## 注意事项
1. **数据绑定**
- 单选模式使用 `v-model` 绑定 string 类型
- 多选模式使用 `v-model:multipleValue` 绑定 string[] 类型
2. **事件处理**
- `change` 事件在单选模式下返回 string
- `change` 事件在多选模式下返回 string[]
3. **样式**
- 多选模式下使用 lime-checkbox 组件
- 单选模式下保持原有的图标显示
4. **功能特性**
- 支持父子节点关联选择checkStrictly 属性)
- 支持限制最大选择数量maxTagCount 属性)
- 支持禁用某些选项disabled 属性)
## 测试数据
测试页面使用了三级级联数据:
- 省份(浙江省、江苏省、广东省)
- 城市(杭州、宁波、温州等)
- 区县(西湖区、余杭区等)
## 预期结果
1. **单选模式**:只能选择一个最终选项,显示选中路径
2. **多选模式**:可以选择多个选项,显示所有选中值
3. **限制模式**:最多选择指定数量的选项,超出时显示提示
4. **交互体验**点击流畅checkbox 状态正确,数据绑定准确

View File

@@ -0,0 +1,149 @@
@import '~@/uni_modules/lime-style/index.scss';
// @import './icon';
@import '../../../lime-icon/components/l-icon/icon.scss';
// @import '~@/uni_modules/lime-icon/components/l-icon/icon.scss';
// @import '~@/uni_modules/lime-style/mixins/hairline.scss';
/* #ifdef uniVersion >= 4.75 */
$use-css-var: true;
/* #endif */
$cascader: #{$prefix}-cascader;
$cascader-title-color: create-var(cascader-title-color, $text-color-1);
$cascader-icon-color: create-var(cascader-icon-color, $primary-color);
$cascader-icon-size: create-var(cascader-icon-size, 24px);
$cascader-bg-color: create-var(cascader-bg-color, $bg-color-container);
$cascader-border-radius: create-var(cascader-border-radius, $border-radius-lg);
$cascader-height: create-var(cascader-height, 320px);
$cascader-cell-height: create-var(cascader-cell-height, 50px);
$cascader-cell-padding-x: create-var(cascader-cell-cell-padding, $spacer);
$cascader-cell-padding-y: create-var(cascader-cell-cell-padding, $spacer-sm);
$cascader-cell-title-color: create-var(cascader-cell-title-color, $text-color-1);
$cascader-cell-title-font-size: create-var(cascader-cell-title-font-size, $font-size-md);
$cascader-disabled-color: create-var(cascader-disabled-color, $text-color-3);
$cascader-title-height: create-var(cascader-title-height, 48px);
$cascader-title-padding-top: create-var(cascader-title-padding-top, $spacer);
$cascader-title-padding-bottom: create-var(cascader-title-padding-bottom, $spacer-xs);
$cascader-title-font-size: create-var(cascader-title-font-size, 18px);
$cascader-options-title-color: create-var(cascader-options-title-color, $text-color-3);
$cascader-close-icon-color: create-var(cascader-close-icon-color, $text-color-2);
.#{$cascader} {
background-color: $cascader-bg-color;
/* #ifndef APP-ANDROID || APP-IOS || APP-HARMONY */
color: $cascader-title-color;
/* #endif */
// border-radius: 12px 12px 0 0;
@include border-radius($cascader-border-radius $cascader-border-radius 0 0);
&__title {
/* #ifndef UNI-APP-X */
display: flex;
// width: 100%;
align-items: center;
justify-content: center;
/* #endif */
position: relative;
font-weight: 700;
text-align: center;
color: $cascader-title-color;
// line-height: $cascader-title-height;
// height: $cascader-title-height;
// padding: 14px 0;
@include padding($cascader-title-padding-top 0 $cascader-title-padding-bottom);
font-size: $cascader-title-font-size;
}
&__close {
&-btn {
right: $cascader-cell-padding-x;
top: 12px;
position: absolute;
/* #ifndef APP-ANDROID || APP-IOS || APP-HARMONY */
color: $cascader-close-icon-color !important;
/* #endif */
}
&-icon {
font-family: $prefix;
font-size: $cascader-icon-size;
color: $cascader-close-icon-color !important;
}
}
&__content {
// height: $cascader-content-height;
}
&__options {
// flex: 1;
/* #ifndef APP-ANDROID || APP-IOS || APP-HARMONY */
width: 100vw;
/* #endif */
/* #ifdef APP-ANDROID || APP-IOS || APP-HARMONY */
width: 750rpx;
/* #endif */
height: $cascader-height;
&-container {
/* #ifndef UNI-APP-X */
display: flex;
/* #endif */
flex-direction: row;
transition-property: transform;
transition-timing-function: ease;
transition-duration: 300ms;
}
&-title {
color: $cascader-options-title-color;
font-size: $font-size;
line-height: 22px;
padding-top: 16px;
padding-left: 16px;
box-sizing: border-box;
}
}
&__cell {
/* #ifndef UNI-APP-X */
display: flex;
box-sizing: border-box;
/* #endif */
// padding: $cascader-cell-padding;
@include padding($cascader-cell-padding-y $cascader-cell-padding-x);
flex-direction: row;
justify-content: space-between;
align-items: center;
height: $cascader-cell-height;
&--disabled {
color: $cascader-disabled-color;
}
&-title {
font-size: $cascader-cell-title-font-size;
color: $cascader-cell-title-color;
}
&-icon {
font-size: $cascader-icon-size;
color: $cascader-icon-color;
}
}
// 多选模式下的checkbox样式
&__checkbox {
width: 100%;
/* #ifndef UNI-APP-X */
display: flex;
/* #endif */
align-items: center;
// justify-content: space-between;
.l-checkbox {
width: 100%;
.l-checkbox__label {
flex: 1;
margin-left: 8px;
}
}
}
&__footer {
padding: 16px;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,462 @@
<template>
<l-popup :visible="show" position="bottom" @click-overlay="onClose">
<view class="l-cascader" :style="[styles]">
<text class="l-cascader__title">{{title}}</text>
<view class="l-cascader__close-btn" @click="onCloseBtn" v-if="closeable">
<view class="l-cascader__close-icon" >
<l-icon name="tdesign:close" size="24px"></l-icon>
</view>
</view>
<view class="l-cascader__content">
<l-tabs
:list="steps"
:value="stepIndex"
:space-evenly="false" @change="onTabChange" size="large"
:color="color"
:visible="show"
:activeColor="activeColor"
:lineColor="activeColor"
:bgColor="bgColor">
</l-tabs>
<template v-if="subTitles.length > 0 ">
<text v-show="subTitles.length > stepIndex" class="l-cascader__options-title">
{{subTitles.length > stepIndex ? subTitles[stepIndex]: ''}}
</text>
</template>
<view class="l-cascader__options-container" :style="[containerStyles]">
<scroll-view
class="l-cascader__options"
v-for="(_options, index) in items"
:key="index"
:scroll-y="true">
<view class="l-cascader__cell" v-for="(item, _index) in _options" :key="_index" @click="handleSelect(item[fieldKeys.value], index, true)">
<text
class="l-cascader__cell-title"
:class="{'l-cascader__cell--disabled': item['disabled'] == true}"
:style="[color != null ?'color:' + color: '']">{{item[fieldKeys.label]}}</text>
<view class="l-cascader__cell-icon"
v-if="selectedValue.length > index && selectedValue[index] == item[fieldKeys.value]">
<l-icon
:size="iconSize"
:color="activeColor"
name="check">
</l-icon>
</view>
</view>
</scroll-view>
</view>
</view>
<view class="l-cascader__footer" v-if="innerConfirmBtn">
<l-button
@click="handleConfirm"
:color="innerConfirmBtn.color || props.color"
:shape="innerConfirmBtn.shape || 'round'"
:type="innerConfirmBtn.type || 'primary'">
{{
innerConfirmBtn.content
}}
</l-button>
</view>
</view>
</l-popup>
</template>
<script lang="ts">
// @ts-nocheck
/**
* Cascader 级联选择器组件
* @description 支持多级联动的选择器,适用于地区选择、分类选择等场景
* @tutorial https://ext.dcloud.net.cn/plugin?name=lime-cascader
*
* @property {boolean} visible 控制组件显示/隐藏(必填)
* @property {boolean} checkStrictly 父子节点选择是否关联默认false联动选择
* @property {object[]} options 数据源必填符合CascaderOption结构
* @property {string} title 主标题文本
* @property {object} keys 字段别名配置(例:{label:'name',value:'id'}
* @property {string} value 当前选中值支持v-model
* @property {string} defaultValue 默认选中值
* @property {string} placeholder 未选择时的提示文字
* @property {string[]} subTitles 各级副标题(如:['省份','城市','区县']
* @property {boolean} closeable 是否显示关闭按钮
* @property {boolean} uniCloud 是否使用uniCloud数据源
* @property {boolean} swipeable 是否支持手势滑动切换层级
* @property {string} iconSize 图标尺寸支持CSS单位
* @property {string} activeColor 选中状态主题色
* @property {string} fontSize 文字字号支持CSS单位
* @property {string} color 文字颜色
* @property {string} bgColor 背景颜色
* @event {Function} change 选项变化时触发(返回当前选中路径)
* @event {Function} pick 选中时触发
* @event {Function} close 关闭时触发
* @event {Function} finish 点击完成时触发
*/
import { defineComponent, ref, computed, watch, reactive, onMounted ,onUnmounted, toRaw, watchEffect } from '@/uni_modules/lime-shared/vue';
import cascaderProps from './props';
import { CascaderOption, KeysType, ChildrenInfoType } from './type';
import { getIndexesByValue, parseOptions, parseKeys, splitEveryTwo, getUniCloudArea, pickUniCloudArea, getIndexByValue, updateChildren } from './utils';
export default defineComponent({
name: 'l-cascader',
emits: ['pick', 'change', 'close', 'finish', 'update:modelValue', 'input', 'update:visible'],
props: cascaderProps,
setup(props, {emit}) {
const childrenInfo: ChildrenInfoType = {
value: '',
level: 0,
};
const fieldKeys = computed(():KeysType => parseKeys(props.keys, props.uniCloud))
const cascaderValue = computed({
set(value: string){
// modelValue.value = value
emit('update:modelValue', value)
// #ifdef VUE2
emit('input', value)
// #endif
},
get():string {
return props.value ?? props.modelValue
}
})
const show = computed({
set(value: boolean){
emit('update:visible', value)
},
get():boolean {
return props.visible
}
} as WritableComputedOptions<string>)
const cache = new Map<string, Record<string,any>[]>();
const stepIndex = ref(0)
const selectedIndexes = reactive<number[]>([]);
const selectedValue = reactive<string[]>([]);
const uniCloudColumns = ref<UTSJSONObject[]>([]);
const realColumns = computed(():UTSJSONObject[] =>{
if(props.uniCloud) {
return uniCloudColumns.value
}
return props.options
})
const items = reactive<CascaderOption[][]>([realColumns.value]);
const steps = reactive<CascaderOption[]>([{ label: props.placeholder }])
const innerConfirmBtn = computed(():UTSJSONObject|null=> {
if(props.confirmBtn == null) return null
if(typeof props.confirmBtn == 'string') {
return {
content: props.confirmBtn as string
}
}
return props.confirmBtn as UTSJSONObject
})
const styles = computed(() =>{
const style:Record<string, any> = {}
if(props.bgColor) {
style['background'] = props.bgColor!
}
return style
})
const containerStyles = computed(()=>{
const style:Record<string, any> = {}
style['width'] = (items.length + 1) + '00%';
style['transform'] = `translateX(${-stepIndex.value}00vw)`;
return style
})
const onTabChange = (index : number) => {
stepIndex.value = index
}
// 监听选中索引变化
const watchSelectedIndexes = () => {
if (realColumns.value.length > 0) {
items.splice(0, items.length, ...[parseOptions(realColumns.value, fieldKeys.value)]);
let current = realColumns.value;
for (let i = 0, size = selectedIndexes.length; i < size; i += 1) {
const index = selectedIndexes[i];
const next = current[index]
const value = next?.[fieldKeys.value.value]
const label = next?.[fieldKeys.value.label]
const children = next?.[fieldKeys.value.children]
if(value) {
selectedValue.push(value);
}
if(label && steps[i]?.['label'] != label) {
steps.push({label});
}
if(children) {
current = children
items.push(parseOptions(children, fieldKeys.value));
}
}
}
if (steps.length < items.length) {
steps.push({ label: props.placeholder });
}
// stepIndex.value = items.length - 1;
stepIndex.value = Math.max(Math.min(selectedIndexes.length - 1, items.length - 1), 0);
}
let timer = 0
const initWithValue = () => {
clearTimeout(timer)
timer = setTimeout(()=>{
if (cascaderValue.value) {
steps.pop()
const path = getIndexesByValue(realColumns.value, cascaderValue.value, fieldKeys.value)
path?.forEach((e : number, index) => {
// @ts-ignore
if(selectedIndexes.length > index) {
selectedIndexes[index] = e
} else {
// @ts-ignore
selectedIndexes.push(e);
}
});
watchSelectedIndexes();
} else {
selectedIndexes.length = 0;
selectedValue.length = 0;
steps.length = 1;
steps[0] = { label: props.placeholder }
items.length = 1;
items[0] = parseOptions(realColumns.value, fieldKeys.value)
stepIndex.value = 0;
}
},50)
}
// 设置级联选择器值
const setCascaderValue = (value: string, selectedOptions: CascaderOption[]) => {
cascaderValue.value = value;
emit('change', value, [...selectedOptions])
}
const setSteps = (index: number, label: string) => {
steps[index] = {label}
}
// 取消选择
const cancelSelect = (value: string, level: number, index: number, item:CascaderOption) => {
selectedIndexes[level] = index;
selectedIndexes.length = level;
selectedValue.length = level;
// steps[level] = String(props.placeholder);
// steps[level + 1] = props.placeholder;
setSteps(level, props.placeholder)
setSteps(level + 1, props.placeholder)
steps.length = level + 1;
const children = item[fieldKeys.value.children] as CascaderOption[]|null
if (children != null && children.length > 0) {
items[level + 1] = item[fieldKeys.value.children];
} else if (children != null && children.length == 0) {
childrenInfo.value = value;
childrenInfo.level = level;
}
}
// 选择
const chooseSelect = (value: string, level: number, index: number, item:CascaderOption) => {
selectedIndexes[level] = index;
selectedIndexes.length = level + 1;
selectedValue[level] = String(value);
selectedValue.length = level + 1;
setSteps(level, `${item[fieldKeys.value.label] ?? props.placeholder}`)
const children = item[fieldKeys.value.children] as CascaderOption[]|null
if (children != null && children.length > 0) {
// setItems(level + 1, children)
items[level + 1] = children;
items.length = level + 2;
stepIndex.value += 1;
setSteps(level + 1, props.placeholder)
steps.length = level + 2;
} else if (children != null && children.length == 0) {
childrenInfo.value = value;
childrenInfo.level = level;
} else {
// 如果存在确确按钮,则需要点按钮关闭
if(props.confirmBtn != null) return
items.length = level + 1
steps.length = level + 1;
setCascaderValue(
`${item[fieldKeys.value.value] ?? ''}`,
items.map((item, index):CascaderOption => toRaw(item[selectedIndexes[index]]))
)
emit('finish');
show.value = false
}
}
const handleSelect = (value: string, level: number, shouldEmit: boolean) => {
const _value = value
if(items.length <= level) return
const index = items[level].findIndex((item: CascaderOption):boolean => item[fieldKeys.value.value] == _value);
let item = selectedIndexes.slice(0, level).reduce((acc:CascaderOption[], item:number, index:number):CascaderOption[] => {
if (index == 0) {
return [acc[item]] as CascaderOption[];
}
const children = acc[0][fieldKeys.value.children] as CascaderOption[]|null
return [children?.[item] ?? {}] as CascaderOption[];
}, realColumns.value);
let cursor :CascaderOption|null
if (level == 0) {
cursor = item[index];
} else {
const children = item[0][fieldKeys.value.children] as CascaderOption[]|null
cursor = children?.[index];
}
const disabled = cursor?.['disabled'] == true
if (disabled || cursor == null) {
return;
}
// 反选条件1有确认按钮并且没有下级 2允许各自取消checkStrictly
const hasChildren = Boolean(cursor[fieldKeys.value.children]);
const isSelected = selectedValue.includes(_value);
const canCancel =
(props.confirmBtn && !hasChildren) ||
(props.checkStrictly);
if (canCancel && isSelected) {
// if (props.checkStrictly && selectedValue.includes(_value)) {
cancelSelect(_value, level, index, cursor);
} else {
chooseSelect(_value, level, index, cursor);
}
if(shouldEmit){
const code = cursor[fieldKeys.value.value] as string
emit('pick', level, index, code, toRaw([...selectedIndexes]))
if(!props.uniCloud) return
pickUniCloudArea(uniCloudColumns.value, level, code, toRaw([...selectedIndexes]), cache)
}
}
// 更新级联选择器值
const updateCascaderValue = () => {
setCascaderValue(
selectedValue[selectedValue.length - 1],
items
.filter((item, index):boolean => selectedIndexes.length > index)
.map((item, index):CascaderOption => toRaw(item[selectedIndexes[index]]))
);
};
const onClose = () => {
emit('close');
show.value = false
}
const onCloseBtn = () => {
if (props.checkStrictly) {
updateCascaderValue();
onClose()
} else {
onClose()
}
}
const handleConfirm = () => {
// 1. 如果没有选中任何选项,直接关闭
if (selectedValue.length === 0) {
show.value = false;
return;
}
// 2. 获取当前选中的完整路径(各级选项)
const selectedOptions = items
.filter((_, index) => selectedIndexes.length > index)
.map((item, index) => toRaw(item[selectedIndexes[index]]));
// 3. 更新级联选择器的值(如果是联动模式)
if (!props.checkStrictly) {
setCascaderValue(
selectedValue[selectedValue.length - 1],
selectedOptions
);
}
// 4. 触发 finish 事件,返回选中值和选项路径
emit('finish');
// 5. 关闭弹窗
show.value = false;
}
const optionsWatch = watch(():CascaderOption[] => realColumns.value, (_:CascaderOption[]) => {
watchSelectedIndexes()
if(selectedIndexes.length == 0) {
initWithValue()
}
if (show.value) {
handleSelect(childrenInfo.value, childrenInfo.level, false);
}
},{ deep: true});
const placeholderWatch = watch(():string=> props.placeholder, (newValue: string, oldValue: string) => {
const index = steps.indexOf({label : oldValue} as CascaderOption);
if (index != -1) {
setSteps(index, newValue)
}
});
onMounted(() => {
if(props.uniCloud) {
const update = async () => {
const provinces = await getUniCloudArea({type: 0}, cache)
uniCloudColumns.value = provinces
if(cascaderValue.value != '' && /^\d{6}$/.test(cascaderValue.value)) {
const [province, citie, countie] = splitEveryTwo(cascaderValue.value );
const provinceCode = province.padEnd(6, '0')
const provinceIndex = getIndexByValue(uniCloudColumns.value, provinceCode)
const cities = await getUniCloudArea({type: 1, parent_code: provinceCode}, cache)
const citieCode = (province + citie).padEnd(6, '0')
const citiesIndex = getIndexByValue(uniCloudColumns.value, citieCode)
const counties = await getUniCloudArea({type: 2, parent_code: citieCode}, cache)
updateChildren(uniCloudColumns.value, cities, [provinceIndex])
updateChildren(uniCloudColumns.value[provinceIndex]['children'] as UTSJSONObject[], counties, [citiesIndex])
}
initWithValue()
}
update()
return
}
initWithValue()
})
watchEffect(()=>{
const value = cascaderValue.value
if(realColumns.value.length > 0 && selectedIndexes.length == 0) {
initWithValue()
}
})
// 监听 cascaderValue 变化,当值被清空时重置状态
const cascaderValueWatch = watch(():string => cascaderValue.value, (newValue: string, oldValue: string) => {
if (newValue == '' && oldValue != '') {
initWithValue()
}
})
onUnmounted(()=>{
optionsWatch()
placeholderWatch()
cascaderValueWatch()
})
return {
show,
items,
steps,
stepIndex,
selectedValue,
styles,
containerStyles,
fieldKeys,
handleSelect,
onCloseBtn,
onClose,
onTabChange,
handleConfirm,
innerConfirmBtn
}
}
})
</script>
<style lang="scss">
@import './index';
</style>

View File

@@ -0,0 +1,102 @@
export default {
visible: {
type: Boolean,
default: false,
},
/**
* 父子节点选中状态不再关联,可各自选中或取消
*/
checkStrictly: {
type: Boolean,
default: false,
},
uniCloud: {
type: Boolean,
default: false,
},
options: {
type: Array,
default: () => [{ }]
},
/**
* 标题
*/
title: {
type: String,
default: null
},
keys: {
type: Object,
default: () => { }
},
/**
* 选项值
*/
value: {
type: String,
default: null
},
/**
* 选项值
*/
modelValue: {
type: String,
default: null
},
/**
* 选项值,非受控属性
*/
defaultValue: {
type: String,
default: null
},
/**
* 未选中时的提示文案
*/
placeholder: {
type: String,
default: '选择选项'
},
/**
* 每级展示的次标题
*/
subTitles: {
type: Array,
default: () => []
},
/**
* 关闭按钮
*/
closeable: {
type: Boolean,
default: true
},
swipeable: {
type: Boolean,
default: false
},
fontSize: {
type: String,
default: null
},
color: {
type: String,
default: null
},
bgColor: {
type: String,
default: null
},
iconSize: {
type: String,
default: null
},
activeColor: {
type: String,
default: null
},
confirmBtn: {
type: [String, Object],
default: null
}
}

View File

@@ -0,0 +1,91 @@
// @ts-nocheck
// #ifndef UNI-APP-X
type UTSJSONObject = Record<string, any>
// #endif
export type ChildrenInfoType = {
value: string;
level: number;
}
export type KeysType = {
label : string,
value : string,
children : string
}
export type CascaderOption = {
children ?: Array<CascaderOption>;
/** option label content */
label ?: string;
/** option search text */
text ?: string;
/** option value */
value ?: string;
/** option node content */
content ?: string;
color?: string;
disabled: boolean;
}
export interface CascaderProps {
visible: boolean;
/**
* 父子节点选中状态不再关联,可各自选中或取消
*/
checkStrictly: boolean;
/**
* 是否支持多选
*/
multiple?: boolean;
options : UTSJSONObject[];
/**
* 标题
*/
title ?: string;
keys?: UTSJSONObject;
/**
* 选项值
*/
value ?: string;
/**
* 选项值,非受控属性
*/
defaultValue ?: string;
/**
* 未选中时的提示文案
*/
placeholder : string;
/**
* 每级展示的次标题
*/
subTitles : string[];
/**
* 关闭按钮
*/
closeable: boolean;
uniCloud: boolean;
swipeable ?: boolean;
fontSize?: string;
color?: string;
bgColor?: string;
// #ifdef APP
iconSize: string;
activeColor: string;
// #endif
// #ifndef APP
iconSize?: string;
activeColor?: string;
// #endif
/**
* 确认按钮。值为 null 则不显示确认按钮。值类型为字符串,则表示自定义按钮文本,值类型为 Object 则表示透传 Button 组件属性
* @default ''
*/
confirmBtn ?: string | UTSJSONObject // string | ButtonProps | null
/**
* 确认按钮处于禁用状态时的文字
*/
confirmDisabledText ?: string;
}

View File

@@ -0,0 +1,175 @@
// @ts-nocheck
import type { CascaderOption, KeysType } from './type';
// #ifndef UNI-APP-X
type UTSJSONObject = Record<string, any>
// #endif
// #ifdef UNI-APP-X
const Object = UTSJSONObject
// #endif
import { t } from '@/utils/i18n'
export function parseKeys(keys : UTSJSONObject | null, uniCloud:boolean) : KeysType {
const _labelKey = uniCloud ? 'name' : `${keys?.['label'] ?? 'label'}`
const _valueKey = uniCloud ? 'code' : `${keys?.['value'] ?? 'value'}`
const _childrenKey = `${keys?.['children'] ?? 'children'}`
return {
label: _labelKey,
value: _valueKey,
children: _childrenKey,
} as KeysType
}
export function parseOptions(options : UTSJSONObject[], keys : KeysType) : UTSJSONObject[] {
return options.map((item) : UTSJSONObject => {
// #ifndef APP-ANDROID
const obj = {
[keys.label]: item[keys.label],
[keys.value]: item[keys.value],
[keys.children]: item[keys.children],
}
// #endif
// #ifdef APP-ANDROID
const obj = {}
item.toMap().forEach((v, key) => {
if (key == keys.label || key == keys.value || key == keys.children) {
obj[key] = v
}
})
// #endif
return obj;
});
}
export function getIndexesByValue(options : UTSJSONObject[], value : string | null, keys : KeysType) : number[] | null {
if (value == null) return null
for (let i = 0, size = options.length; i < size; i += 1) {
const opt = options[i];
if (opt[keys.value] == value) {
return [i];
}
const children = opt[keys.children] as UTSJSONObject[] | null
if (children != null) {
const res = getIndexesByValue(children, value, keys)
if (res != null) {
return [i, ...res]
}
}
}
return null
}
/**
* 在数组的指定位置插入或更新值。
* 如果指定的索引小于数组的长度,则更新该位置的值。
* 如果指定的索引大于或等于数组的长度,则将值添加到数组的末尾。
*
* @param {number[]} arr - 要操作的数字数组。
* @param {number} index - 要插入或更新值的索引位置。
* @param {number} value - 要插入或更新的值。
*/
export function pushAt<T>(arr : T[], index : number, value : T) {
// #ifdef APP-ANDROID
if (index < arr.length) {
arr[index] = value;
} else {
arr.push(value);
}
// #endif
// #ifndef APP-ANDROID
arr[index] = value;
// #endif
};
/**
* 将给定的字符串按照每两个字符进行分割,返回分割后的字符串数组。
*
* @param {string} str - 需要被分割的原始字符串。
* @returns {string[]} 返回一个数组,其中每个元素都是原始字符串中连续的两个字符组成的子串。
*/
export function splitEveryTwo(str : string) : string[] {
const result : string[] = [];
for (let i = 0; i < str.length; i += 2) {
result.push(str.substring(i, i + 2));
}
return result;
}
export function getIndexByValue(options : UTSJSONObject[], value : string) : number {
return Math.max(options.findIndex(item => item['code'] == value), 0)
}
export function updateChildren(parent : UTSJSONObject[], child : UTSJSONObject[], selectedIndexes : number[]) {
if (selectedIndexes.length == 0 && parent.length == 0) {
parent.concat(child)
// parent = child
} else if (selectedIndexes.length > 1) {
const i = selectedIndexes.shift()!;
updateChildren(parent[i]['children'] as UTSJSONObject[], child, selectedIndexes)
} else if (selectedIndexes.length == 1) {
const i = selectedIndexes.shift()!;
// #ifdef APP-ANDROID || APP-IOS
parent[i].set('children', child)
// #endif
// #ifndef APP-ANDROID || APP-IOS
parent[i]['children'] = child
// #endif
}
}
export function getUniCloudArea(where : UTSJSONObject, cache : Map<string, UTSJSONObject[]>) : Promise<UTSJSONObject[]> {
return new Promise((resolve) => {
const db = uniCloud.databaseForJQL()
const collection = db.collection('opendb-city-china')
const type = (where['type'] ?? 4) as number;
const code = where['parent_code'] as string|null
if (code != null && cache.has(code)) {
resolve(cache.get(code)!)
return
}
uni.showLoading({
title: t('Mobile.Page.loading')
})
collection.where(where).get().then(res => {
uni.hideLoading()
// 省市 就加上children 县就直接返回
const cursor = type > 1 ? res.data : res.data.map((item) : UTSJSONObject => {
return Object.assign(item, { children: [] as UTSJSONObject[] })
})
if (code != null) {
cache.set(code, cursor)
}
resolve(cursor)
}).catch(err => {
uni.hideLoading()
uni.showToast({
icon: 'error',
title: t('Mobile.Common.networkRetry')
})
})
})
}
export function pickUniCloudArea(columns : UTSJSONObject[], level : number, value : string, selectedIndexes : number[], cache:Map<string, UTSJSONObject[]>) {
const first = selectedIndexes[0]
// 获取当前项
const item = selectedIndexes.reduce((p : UTSJSONObject | null, c : number, i : number) : UTSJSONObject | null => {
// #ifdef APP-ANDROID || APP-IOS
return i == 0 ? p : p?.getArray<UTSJSONObject>('children')?.[c]
// #endif
// #ifndef APP-ANDROID || APP-IOS
return i == 0 ? p : p?.['children']?.[c]
// #endif
}, columns[first])
const code = item?.[`code`] as string | null
const children = item?.[`children`] as UTSJSONObject[] | null
if (item != null && code == value && children != null && children.length == 0) {
getUniCloudArea({ type: level + 1, parent_code: code }, cache).then(res => {
updateChildren(columns, res, selectedIndexes)
})
}
}

View File

@@ -0,0 +1,192 @@
export const areaList = [
{
label: '北京市',
value: '110000',
children: [
{
value: '110100',
label: '北京市',
children: [
{ value: '110101', label: '东城区' },
{ value: '110102', label: '西城区' },
{ value: '110105', label: '朝阳区' },
{ value: '110106', label: '丰台区' },
{ value: '110107', label: '石景山区' },
{ value: '110108', label: '海淀区' },
{ value: '110109', label: '门头沟区' },
{ value: '110111', label: '房山区' },
{ value: '110112', label: '通州区' },
{ value: '110113', label: '顺义区' },
{ value: '110114', label: '昌平区' },
{ value: '110115', label: '大兴区' },
{ value: '110116', label: '怀柔区' },
{ value: '110117', label: '平谷区' },
{ value: '110118', label: '密云区' },
{ value: '110119', label: '延庆区' },
],
},
],
},
{
label: '天津市',
value: '120000',
children: [
{
value: '120100',
label: '天津市',
children: [
{ value: '120101', label: '和平区' },
{ value: '120102', label: '河东区' },
{ value: '120103', label: '河西区' },
{ value: '120104', label: '南开区' },
{ value: '120105', label: '河北区' },
{ value: '120106', label: '红桥区' },
{ value: '120110', label: '东丽区' },
{ value: '120111', label: '西青区' },
{ value: '120112', label: '津南区' },
{ value: '120113', label: '北辰区' },
{ value: '120114', label: '武清区' },
{ value: '120115', label: '宝坻区' },
{ value: '120116', label: '滨海新区' },
{ value: '120117', label: '宁河区' },
{ value: '120118', label: '静海区' },
{ value: '120119', label: '蓟州区' },
],
},
],
},
]
export const areaList2 = [
{
name: '北京市',
code: '110000',
items: [
{
code: '110100',
name: '北京市',
items: [
{ code: '110101', name: '东城区' },
{ code: '110102', name: '西城区' },
{ code: '110105', name: '朝阳区' },
{ code: '110106', name: '丰台区' },
{ code: '110107', name: '石景山区' },
{ code: '110108', name: '海淀区' },
{ code: '110109', name: '门头沟区' },
{ code: '110111', name: '房山区' },
{ code: '110112', name: '通州区' },
{ code: '110113', name: '顺义区' },
{ code: '110114', name: '昌平区' },
{ code: '110115', name: '大兴区' },
{ code: '110116', name: '怀柔区' },
{ code: '110117', name: '平谷区' },
{ code: '110118', name: '密云区' },
{ code: '110119', name: '延庆区' },
],
},
],
},
{
name: '天津市',
code: '120000',
items: [
{
code: '120100',
name: '天津市',
items: [
{ code: '120101', name: '和平区' },
{ code: '120102', name: '河东区' },
{ code: '120103', name: '河西区' },
{ code: '120104', name: '南开区' },
{ code: '120105', name: '河北区' },
{ code: '120106', name: '红桥区' },
{ code: '120110', name: '东丽区' },
{ code: '120111', name: '西青区' },
{ code: '120112', name: '津南区' },
{ code: '120113', name: '北辰区' },
{ code: '120114', name: '武清区' },
{ code: '120115', name: '宝坻区' },
{ code: '120116', name: '滨海新区' },
{ code: '120117', name: '宁河区' },
{ code: '120118', name: '静海区' },
{ code: '120119', name: '蓟州区' },
],
},
],
},
]
const makeOption = (
label : string,
value : string,
children ?: UTSJSONObject[],
) : UTSJSONObject => ({
label,
value,
children,
});
export function useCascaderAreaData() : Promise<UTSJSONObject[]> {
return new Promise((resolve, reject) => {
// #ifdef APP
const manager = uni.getFileSystemManager();
manager.readFile({
filePath: 'static/city/city-china.json',
encoding: 'utf-8',
success: (res) => {
const areaList = JSON.parse<UTSJSONObject>(res.data as string)
if(areaList == null) {
reject('加载失败')
}
const city = areaList!['city_list'] as UTSJSONObject
const county = areaList!['county_list'] as UTSJSONObject
const province = areaList!['province_list'] as UTSJSONObject
const provinceMap = new Map<string, UTSJSONObject>();
UTSJSONObject.keys(province).forEach((code) => {
provinceMap.set(code.slice(0, 2), makeOption(`${province[code]}`, code, []));
});
const cityMap = new Map<string, UTSJSONObject>();
UTSJSONObject.keys(city).forEach((code) => {
const option = makeOption(`${city[code]}`, code, []);
cityMap.set(code.slice(0, 4), option);
const _province = provinceMap.get(code.slice(0, 2));
if (_province != null) {
(_province['children'] as UTSJSONObject[]).push(option)
}
});
UTSJSONObject.keys(county).forEach((code) => {
const _city = cityMap.get(code.slice(0, 4));
if (_city != null) {
(_city['children'] as UTSJSONObject[]).push(makeOption(`${county[code]}`, code, null));
}
});
// #ifndef APP-ANDROID || APP-IOS || APP-HARMONY
resolve(Array.from(provinceMap.values()))
// #endif
// #ifdef APP-ANDROID || APP-IOS || APP-HARMONY
const obj : UTSJSONObject[] = []
provinceMap.forEach((value, code) => {
obj.push(value)
})
resolve(obj)
// #endif
}
} as ReadFileOptions);
// #endif
})
}

View File

@@ -0,0 +1,112 @@
{
"id": "lime-cascader",
"displayName": "lime-cascader 级联选择",
"version": "0.1.3",
"description": "lime-cascader 级联选择 用于多层级数据选择省市区等选择主要为树形结构可展示更多的数据。兼容uniapp/uniappx",
"keywords": [
"lime-cascader",
"cascader",
"省市区",
"级联选择"
],
"repository": "",
"engines": {
"HBuilderX": "^4.28",
"uni-app": "^4.75",
"uni-app-x": "^4.75"
},
"dcloudext": {
"type": "component-vue",
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "无",
"permissions": "无"
},
"npmurl": "",
"darkmode": "√",
"i18n": "x",
"widescreen": "x"
},
"uni_modules": {
"dependencies": [
"lime-popup",
"lime-style",
"lime-icon",
"lime-shared",
"lime-tabs"
],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "√",
"aliyun": "√",
"alipay": "√"
},
"client": {
"uni-app": {
"vue": {
"vue2": "√",
"vue3": "√"
},
"web": {
"safari": "√",
"chrome": "√"
},
"app": {
"vue": "√",
"nvue": "-",
"android": {
"extVersion": "",
"minVersion": "21"
},
"ios": "√",
"harmony": "√"
},
"mp": {
"weixin": "√",
"alipay": "-",
"toutiao": "-",
"baidu": "-",
"kuaishou": "-",
"jd": "-",
"harmony": "-",
"qq": "-",
"lark": "-"
},
"quickapp": {
"huawei": "-",
"union": "-"
}
},
"uni-app-x": {
"web": {
"safari": "√",
"chrome": "√"
},
"app": {
"android": {
"extVersion": "",
"minVersion": "21"
},
"ios": "√",
"harmony": "√"
},
"mp": {
"weixin": "√"
}
}
}
}
}
}

View File

@@ -0,0 +1,406 @@
# lime-cascader 级联选择
- 级联选择 用于多层级数据选择主要为树形结构可展示更多的数据。兼容uniapp/uniappx
- 插件依赖`lime-popup`,`lime-style`,`lime-shared`,`lime-tabs`,`lime-icon`,`lime-svg`,不喜勿下。
## 文档
[cascader](https://limex.qcoon.cn/components/cascader.html)
## 安装
插件市场导入即可,首次导入可能需要重新编译
**注意**
* 🔔 本插件依赖的[lime-svg](https://ext.dcloud.net.cn/plugin?id=18519)在 uniapp x app中是原生插件如果购买(收费为5元)则需要自定义基座才能使用uniapp可忽略。
* 🔔 如果不需要[lime-svg](https://ext.dcloud.net.cn/plugin?id=18519)在lime-icon代码中注释掉即可
```html
// lime-icon/components/l-icon.uvue 第4行 注释掉即可。
<!-- <l-svg class="l-icon" :class="classes" :style="styles" :color="color" :src="iconUrl" v-else :web="web" @error="imageError" @load="imageload" @click="$emit('click')"></l-svg> -->
```
## 代码演示
示例使用了`uts``vue3 setup``uniapp`可以把数据类型去掉即可。
### 基础使用
级联选择组件通过设置`visible`弹出选择器。
```html
<view class="cell" @click="visible = true">
<text>地址</text>
<text v-show="fieldValue == null" style="color: #999;">请选择地址 ></text>
<text v-show="fieldValue != null">{{fieldValue}}</text>
</view>
<l-cascader
v-model:visible="visible"
v-model="cascaderValue"
:options="options"
title="请选择所在地区"
@change="onChange"/>
```
```js
const visible = ref(false)
// 设置默认值,如 110000
const cascaderValue = ref('');
const fieldValue = ref<string|null>(null);
// 选项列表children 代表子选项,支持多级嵌套
const options = [
{
label: '北京市',
value: '110000',
children: [
{
value: '110100',
label: '北京市',
children: [
{ value: '110101', label: '东城区' },
{ value: '110102', label: '西城区' },
{ value: '110105', label: '朝阳区' },
{ value: '110106', label: '丰台区' },
{ value: '110107', label: '石景山区' },
{ value: '110108', label: '海淀区' },
{ value: '110109', label: '门头沟区' },
{ value: '110111', label: '房山区' },
{ value: '110112', label: '通州区' },
{ value: '110113', label: '顺义区' },
{ value: '110114', label: '昌平区' },
{ value: '110115', label: '大兴区' },
{ value: '110116', label: '怀柔区' },
{ value: '110117', label: '平谷区' },
{ value: '110118', label: '密云区' },
{ value: '110119', label: '延庆区' },
],
},
],
},
{
label: '天津市',
value: '120000',
children: [
{
value: '120100',
label: '天津市',
children: [
{ value: '120101', label: '和平区' },
{ value: '120102', label: '河东区' },
{ value: '120103', label: '河西区' },
{ value: '120104', label: '南开区' },
{ value: '120105', label: '河北区' },
{ value: '120106', label: '红桥区' },
{ value: '120110', label: '东丽区' },
{ value: '120111', label: '西青区' },
{ value: '120112', label: '津南区' },
{ value: '120113', label: '北辰区' },
{ value: '120114', label: '武清区' },
{ value: '120115', label: '宝坻区' },
{ value: '120116', label: '滨海新区' },
{ value: '120117', label: '宁河区' },
{ value: '120118', label: '静海区' },
{ value: '120119', label: '蓟州区' },
],
},
],
},
];
// 全部选项选择后,会触发 change 事件
const onChange = (value: string, options: UTSJSONObject[]) => {
fieldValue.value = options.map((item: UTSJSONObject):any|null => (item['label'])).join('/');
};
```
### 带初始值
通过设置`value``v-model`设置默认值,选择项的值
```html
<view class="cell" @click="visible2 = true">
<text>地址</text>
<text v-show="fieldValue2 == null" style="color: #999;">请选择地址 ></text>
<text v-show="fieldValue2 != null">{{fieldValue2}}</text>
</view>
<l-cascader
v-model:visible="visible2"
v-model="cascaderValue2"
:options="options2"
title="请选择所在地区"
@change="onChange2"/>
```
```js
const visible2 = ref(false)
// 设置默认值
const cascaderValue2 = ref('120119');
const fieldValue2 = ref<string|null>(null);
const options2 = areaList;
const onChange2 = (value: string, options: UTSJSONObject[]) => {
fieldValue2.value = options.map((item: UTSJSONObject):any|null => (item['label'])).join('/');
};
```
### 自定义字段名
通过设置`keys`属性可以自定义 `options` 里的字段名称
```html
<view class="cell" @click="visible3 = true">
<text>地址</text>
<text v-show="fieldValue3 == null" style="color: #999;">请选择地址 ></text>
<text v-show="fieldValue3 != null">{{fieldValue3}}</text>
</view>
<l-cascader
v-model:visible="visible3"
v-model="cascaderValue3"
:options="options3"
:keys="keys"
title="请选择所在地区"
@change="onChange3"/>
```
```js
const visible3 = ref(false)
const cascaderValue3 = ref('');
const fieldValue3 = ref<string|null>(null);
const options3 = [
{
name: '北京市',
code: '110000',
items: [
{
code: '110100',
name: '北京市',
items: [
{ code: '110101', name: '东城区' },
{ code: '110102', name: '西城区' },
{ code: '110105', name: '朝阳区' },
{ code: '110106', name: '丰台区' },
{ code: '110107', name: '石景山区' },
{ code: '110108', name: '海淀区' },
{ code: '110109', name: '门头沟区' },
{ code: '110111', name: '房山区' },
{ code: '110112', name: '通州区' },
{ code: '110113', name: '顺义区' },
{ code: '110114', name: '昌平区' },
{ code: '110115', name: '大兴区' },
{ code: '110116', name: '怀柔区' },
{ code: '110117', name: '平谷区' },
{ code: '110118', name: '密云区' },
{ code: '110119', name: '延庆区' },
],
},
],
},
{
name: '天津市',
code: '120000',
items: [
{
code: '120100',
name: '天津市',
items: [
{ code: '120101', name: '和平区' },
{ code: '120102', name: '河东区' },
{ code: '120103', name: '河西区' },
{ code: '120104', name: '南开区' },
{ code: '120105', name: '河北区' },
{ code: '120106', name: '红桥区' },
{ code: '120110', name: '东丽区' },
{ code: '120111', name: '西青区' },
{ code: '120112', name: '津南区' },
{ code: '120113', name: '北辰区' },
{ code: '120114', name: '武清区' },
{ code: '120115', name: '宝坻区' },
{ code: '120116', name: '滨海新区' },
{ code: '120117', name: '宁河区' },
{ code: '120118', name: '静海区' },
{ code: '120119', name: '蓟州区' },
],
},
],
},
];
const keys = {label: 'name', value: 'code', children: 'items'}
const onChange3 = (value: string, options: UTSJSONObject[]) => {
fieldValue3.value = options.map((item: UTSJSONObject):any|null => (item['name'])).join('/');
};
```
### 自定义选项上方内容
通过设置`subTitles`属性每级展示的次标题
```html
<view class="cell" @click="visible4 = true">
<text>地址</text>
<text v-show="fieldValue4 == null" style="color: #999;">请选择地址 ></text>
<text v-show="fieldValue4 != null">{{fieldValue4}}</text>
</view>
<l-cascader
v-model:visible="visible4"
v-model="cascaderValue4"
:options="options4"
:subTitles="subTitles"
title="请选择所在地区"
@change="onChange4"/>
```
```js
const visible4 = ref(false)
const cascaderValue4 = ref('');
const fieldValue4 = ref<string|null>(null);
const options4 = areaList;
const subTitles = ['请选择省份', '请选择城市', '请选择区/县'];
const onChange4 = (value: string, options: UTSJSONObject[]) => {
fieldValue4.value = options.map((item: UTSJSONObject):any|null => (item['label'])).join('/');
};
```
### 自定义颜色
通过设置`active-color`属性来设置选中状态的高亮颜色。
```html
<view class="cell" @click="visible5 = true">
<text>地址</text>
<text v-show="fieldValue5 == null" style="color: #999;">请选择地址 ></text>
<text v-show="fieldValue5 != null">{{fieldValue5}}</text>
</view>
<l-cascader
v-model:visible="visible5"
v-model="cascaderValue5"
:options="options5"
active-color="#34c471"
title="请选择所在地区"
@change="onChange5"/>
```
### 中国省市区数据
`lime-shared/areaData`提供了一份中国省市区数据, 该数据来源于 `Vant`
```js
import { useCascaderAreaData } from '@/uni_modules/lime-shared/areaData'
const options7 = useCascaderAreaData();
const subTitles = ['请选择省份', '请选择城市', '请选择区/县'];
const visible7 = ref(false)
const cascaderValue7 = ref('');
const fieldValue7 = ref<string|null>(null);
const onChange7 = (value: string, options: UTSJSONObject[]) => {
fieldValue7.value = options.map((item: UTSJSONObject):any|null => (item['label'])).join('/');
};
```
```html
<view class="cell" @click="visible7 = true">
<text>地址</text>
<text v-show="fieldValue7 == null" style="color: #999;">请选择地址 ></text>
<text v-show="fieldValue7 != null">{{fieldValue7}}</text>
</view>
<l-cascader
v-model:visible="visible7"
v-model="cascaderValue7"
:options="options7"
:subTitles="subTitles"
title="请选择所在地区"
@change="onChange7"/>
```
### uniCloud
除了加载本地数据,还可以设置`uniCloud`,会加载`opendb-city-china`(中国城市省市区数据,含港澳台)表, 在[uniCloud](https://unicloud.dcloud.net.cn/)控制台使用opendb创建
```html
<view class="cell" @click="visible6 = true">
<text>地址</text>
<text v-show="fieldValue6 == null" style="color: #999;">请选择地址 ></text>
<text v-show="fieldValue6 != null">{{fieldValue6}}</text>
</view>
<l-cascader
v-model:visible="visible6"
v-model="cascaderValue6"
:options="options6"
:subTitles="subTitles"
:keys="{label: 'name', value: 'code'}"
title="请选择所在地区"
@pick="onPick6"
@change="onChange6"/>
```
```js
const visible6 = ref(false)
const cascaderValue6 = ref('');
const fieldValue6 = ref<string|null>(null);
const onChange6 = (value: string, options: UTSJSONObject[]) => {
fieldValue6.value = options.map((item: UTSJSONObject):any|null => (item['name'])).join('/');
};
```
### 查看示例
- 导入后直接使用这个标签查看演示效果
```html
<!-- // 代码位于 uni_modules/lime-cascader/compoents/lime-cascader -->
<lime-cascader />
```
### 插件标签
- 默认 l-cascader 为 component
- 默认 lime-cascader 为 demo
### 关于vue2的使用方式
- 插件使用了`composition-api`, 如果你希望在vue2中使用请按官方的教程[vue-composition-api](https://uniapp.dcloud.net.cn/tutorial/vue-composition-api.html)配置
- 关键代码是: 在main.js中 在vue2部分加上这一段即可
```js
// vue2
import Vue from 'vue'
import VueCompositionAPI from '@vue/composition-api'
Vue.use(VueCompositionAPI)
```
## API
### Props
| 参数 | 说明 | 类型 | 默认值 |
| --- | --- | --- | --- |
| v-model:visible | 是否显示级联选择器 | _boolean_ | `false` |
| v-model | 值 | _string_ | `-` |
| subTitles | 每级展示的次标题,`Array<string>` | _string[]_ | `-` |
| placeholder | 未选中时的提示文案 | _string_ | `选择选项` |
| keys | 用来定义 `value / label``options` 中对应的字段别名。 | _{label,value,children}_ | `{}` |
| title | 标题 | _string_ | `-` |
| options | 可选项数据源 | _[]_ | `-` |
| closeable | 关闭按钮 | _boolean_ | `-` |
| bgColor | 背景色 | _string_ | `-` |
| activeColor | 激活色 | _string_ | `-` |
| iconSize | 图标尺寸 | _string_ | `-` |
| color | 文本色 | _string_ | `-` |
| fontSize | 字体大小 | _string_ | `-` |
### Events
| 事件名 | 说明 | 回调参数 |
| ---------------- | -------------------------- | ------------------- |
| pick | 选择后触发 | `(level: number, index:number, value: string, selectedIndexes:number[])` |
| change | 值发生变更时触发 | `(value: string, options: UTSJSONObject[])` |
| close | 关闭时触发 | `` |
| finish | 选择后触发 | `` |
## 主题定制
### 样式变量
组件提供了下列 CSS 变量可用于自定义样式uvue app无效。
| 名称 | 默认值 | 描述 |
| ------------------------------ | ------------------------------------ | ---- |
| --l-cascader-title-color | _$text-color-1_ | - |
| --l-cascader-icon-color | _$primary-color_ | - |
| --l-cascader-icon-size | _24px_ | - |
| --l-cascader-bg-color | _$bg-color-container_ | - |
| --l-cascader-height | _320px_ | - |
| --l-cascader-cell-height | _50px_ | - |
| --l-cascader-cell-cell-padding | _14px 16px_ | - |
| --l-cascader-cell-title-color | _$text-color-1_ | - |
| --l-cascader-cell-title-font-size | _$font-size-md_ | - |
| --l-cascader-disabled-color | _$text-color-3_ | - |
| --l-cascader-title-height | _48px_ | - |
| --l-cascader-title-font-size | _18px_ | - |
| --l-cascader-options-title-color | _$text-color-3_ | - |
## 打赏
如果你觉得本插件,解决了你的问题,赠人玫瑰,手留余香。
![](https://testingcf.jsdelivr.net/gh/liangei/image@1.9/alipay.png)
![](https://testingcf.jsdelivr.net/gh/liangei/image@1.9/wpay.png)