chore: initialize qiming workspace repository
This commit is contained in:
@@ -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
@@ -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>
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user