From 11963b8b123809547128223d84a6d4d353e02349 Mon Sep 17 00:00:00 2001 From: lvmeng <848755140@qq.com> Date: Mon, 1 Jun 2026 10:19:07 +0800 Subject: [PATCH] =?UTF-8?q?feat(form):=20=E6=B7=BB=E5=8A=A0=E5=8A=A8?= =?UTF-8?q?=E6=80=81=E8=A1=A8=E5=8D=95=E7=BB=84=E4=BB=B6=E5=92=8C=E4=B8=8B?= =?UTF-8?q?=E6=8B=89=E9=80=89=E6=8B=A9=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 FormField 数据模型定义五种字段类型(文本、数字、下拉、日期、固定值) - 实现 FormFieldAdapter 适配器支持动态表单渲染和数据绑定 - 添加 DropdownPopup 弹窗组件和 DropdownAdapter 适配器 - 新增多个圆角背景资源文件(bg_gray_radius10、bg_white_radius10_stroke 等) - 在 Common.kt 中添加 hideKeyboard 扩展函数用于隐藏软键盘 - 创建 DictType 数据模型用于字典类型数据传输 - 添加表单项和下拉项的布局文件(list_item_form_field、list_item_dropdown 等) --- .../dish/match/adapter/DropdownAdapter.kt | 31 +++ .../dish/match/adapter/FormFieldAdapter.kt | 191 ++++++++++++++++++ .../shuwei/dish/match/dialog/DropdownPopup.kt | 78 +++++++ .../com/shuwei/dish/match/model/DictType.kt | 14 ++ .../com/shuwei/dish/match/model/FormField.kt | 45 +++++ .../com/shuwei/dish/match/utils/ext/Common.kt | 7 + .../main/res/drawable/bg_gray_radius10.xml | 6 + .../res/drawable/bg_white_radius10_stroke.xml | 7 + .../drawable/bg_white_radius10_stroke2.xml | 7 + .../res/drawable/bg_white_stroke_blue.xml | 6 + .../main/res/layout/layout_popup_dropdown.xml | 19 ++ .../main/res/layout/list_item_dropdown.xml | 24 +++ .../main/res/layout/list_item_form_field.xml | 105 ++++++++++ .../res/mipmap-xxhdpi/ic_triangle_down.png | Bin 0 -> 763 bytes .../main/res/mipmap-xxhdpi/ic_triangle_up.png | Bin 0 -> 689 bytes 15 files changed, 540 insertions(+) create mode 100644 app/src/main/java/com/shuwei/dish/match/adapter/DropdownAdapter.kt create mode 100644 app/src/main/java/com/shuwei/dish/match/adapter/FormFieldAdapter.kt create mode 100644 app/src/main/java/com/shuwei/dish/match/dialog/DropdownPopup.kt create mode 100644 app/src/main/java/com/shuwei/dish/match/model/DictType.kt create mode 100644 app/src/main/java/com/shuwei/dish/match/model/FormField.kt create mode 100644 app/src/main/res/drawable/bg_gray_radius10.xml create mode 100644 app/src/main/res/drawable/bg_white_radius10_stroke.xml create mode 100644 app/src/main/res/drawable/bg_white_radius10_stroke2.xml create mode 100644 app/src/main/res/drawable/bg_white_stroke_blue.xml create mode 100644 app/src/main/res/layout/layout_popup_dropdown.xml create mode 100644 app/src/main/res/layout/list_item_dropdown.xml create mode 100644 app/src/main/res/layout/list_item_form_field.xml create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_triangle_down.png create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_triangle_up.png diff --git a/app/src/main/java/com/shuwei/dish/match/adapter/DropdownAdapter.kt b/app/src/main/java/com/shuwei/dish/match/adapter/DropdownAdapter.kt new file mode 100644 index 0000000..7992c23 --- /dev/null +++ b/app/src/main/java/com/shuwei/dish/match/adapter/DropdownAdapter.kt @@ -0,0 +1,31 @@ +package com.shuwei.dish.match.adapter + +import android.content.Context +import android.view.LayoutInflater +import android.view.ViewGroup +import com.chad.library.adapter4.BaseQuickAdapter +import com.chad.library.adapter4.viewholder.QuickViewHolder +import com.shuwei.dish.match.databinding.ListItemDropdownBinding +import com.shuwei.dish.match.model.DictType +import com.shuwei.dish.match.utils.ext.gone +import com.shuwei.dish.match.utils.ext.visible + +class DropdownAdapter(var list: MutableList) : + BaseQuickAdapter(list) { + + inner class VH(var binding: ListItemDropdownBinding) : QuickViewHolder(binding.root) + + override fun onCreateViewHolder(context: Context, parent: ViewGroup, viewType: Int): VH { + val inflater = LayoutInflater.from(context) + val binding = ListItemDropdownBinding.inflate(inflater, parent, false) + return VH(binding) + } + + override fun onBindViewHolder(holder: VH, position: Int, item: DictType?) { + holder.binding.tvDropdown.text = item?.value + holder.binding.divider.run { + if (position == list.size - 1) gone() else visible() + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/shuwei/dish/match/adapter/FormFieldAdapter.kt b/app/src/main/java/com/shuwei/dish/match/adapter/FormFieldAdapter.kt new file mode 100644 index 0000000..5b65312 --- /dev/null +++ b/app/src/main/java/com/shuwei/dish/match/adapter/FormFieldAdapter.kt @@ -0,0 +1,191 @@ +package com.shuwei.dish.match.adapter + +import android.app.DatePickerDialog +import android.content.Context +import android.text.Editable +import android.text.InputType +import android.text.Spannable +import android.text.SpannableStringBuilder +import android.text.TextWatcher +import android.text.style.ForegroundColorSpan +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.core.graphics.toColorInt +import com.chad.library.adapter4.BaseQuickAdapter +import com.chad.library.adapter4.viewholder.QuickViewHolder +import com.shuwei.dish.match.R +import com.shuwei.dish.match.databinding.ListItemFormFieldBinding +import com.shuwei.dish.match.dialog.DropdownPopup +import com.shuwei.dish.match.model.FieldType +import com.shuwei.dish.match.model.FormField +import com.shuwei.dish.match.utils.ext.dp +import com.shuwei.dish.match.utils.ext.gone +import com.shuwei.dish.match.utils.ext.hideKeyboard +import com.shuwei.dish.match.utils.ext.visible +import java.text.SimpleDateFormat +import java.util.Calendar +import java.util.Locale +import kotlin.math.min + +/** + * 动态表单字段 Adapter,配合 FlexboxLayoutManager 实现两列自动换行布局 + * 单一 item 布局,通过 visibility 控制 TEXT/NUMBER/FIXED/DROPDOWN/DATE_PICKER 五种类型渲染 + * + * @param activityContext Activity context,用于创建 DatePickerDialog + * @param fields 表单字段列表(hidden=false 的字段,由外部过滤后传入) + */ +class FormFieldAdapter( + private val activityContext: Context, + fields: MutableList +) : BaseQuickAdapter(fields) { + + private val isoFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()) + private val displayDateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()) + + inner class VH(val binding: ListItemFormFieldBinding) : QuickViewHolder(binding.root) + + override fun onCreateViewHolder(context: Context, parent: ViewGroup, viewType: Int): VH { + val binding = ListItemFormFieldBinding.inflate(LayoutInflater.from(context), parent, false) + return VH(binding) + } + + override fun onBindViewHolder(holder: VH, position: Int, item: FormField?) { + item ?: return + val b = holder.binding + + // 设置标签,必填项追加红色星号 + b.tvLabel.text = buildLabelSpannable(item.label, item.required) + + // 先全部隐藏,再按类型显示对应控件 + b.etInput.gone() + b.flDropdown.gone() + b.flDatePicker.gone() + + // 移除旧的 TextWatcher,防止 RecyclerView 复用时错误触发 + (b.etInput.tag as? TextWatcher)?.let { b.etInput.removeTextChangedListener(it) } + b.etInput.tag = null + + when (item.type) { + FieldType.TEXT -> bindEditText(b, item, InputType.TYPE_CLASS_TEXT) + FieldType.NUMBER -> bindEditText( + b, item, + InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_FLAG_DECIMAL or InputType.TYPE_NUMBER_FLAG_SIGNED + ) + FieldType.FIXED -> bindFixed(b, item) + FieldType.DROPDOWN -> bindDropdown(b, item) + FieldType.DATE_PICKER -> bindDatePicker(b, item) + } + } + + /** 绑定可编辑文本框(TEXT / NUMBER) */ + private fun bindEditText(b: ListItemFormFieldBinding, item: FormField, inputType: Int) { + b.etInput.visible() + b.etInput.isEnabled = true + b.etInput.inputType = inputType + b.etInput.hint = item.hint.ifBlank { item.label } + b.etInput.setBackgroundResource(R.drawable.bg_white_radius10_stroke2) + b.etInput.setText(item.value) + b.etInput.setSelection(b.etInput.text.length) + + val watcher = object : TextWatcher { + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} + override fun afterTextChanged(s: Editable?) { + item.value = s?.toString() ?: "" + } + } + b.etInput.addTextChangedListener(watcher) + b.etInput.tag = watcher + } + + /** 绑定固定值(不可编辑,灰色背景区分) */ + private fun bindFixed(b: ListItemFormFieldBinding, item: FormField) { + b.etInput.visible() + b.etInput.isEnabled = false + b.etInput.inputType = InputType.TYPE_CLASS_TEXT + b.etInput.hint = "" + b.etInput.setBackgroundResource(R.drawable.bg_gray_radius10) + b.etInput.setText(item.value) + } + + /** 绑定下拉选择框,点击时创建 DropdownPopup */ + private fun bindDropdown(b: ListItemFormFieldBinding, item: FormField) { + b.flDropdown.visible() + b.tvDropdownValue.hint = item.hint.ifBlank { "请选择${item.label}" } + b.tvDropdownValue.text = item.value.ifBlank { null } + + b.flDropdown.setOnClickListener { v -> + v.hideKeyboard() + val popHeight = min(item.options.size.coerceAtLeast(1), 4) * 71.dp + DropdownPopup( + context = activityContext, + list = item.options.toMutableList(), + popWidth = v.width, + popHeight = popHeight + ) { dictType -> + item.value = dictType.value ?: "" + item.valueId = dictType.id ?: "" + b.tvDropdownValue.text = item.value + // 将 extraApiKeys 映射的 DictType 字段值写入 extraValues + item.extraValues.clear() + item.extraApiKeys.forEach { (apiKey, dictField) -> + val fieldValue = when (dictField) { + "id" -> dictType.id ?: "" + "value" -> dictType.value ?: "" + "type" -> dictType.type + else -> "" + } + item.extraValues[apiKey] = fieldValue + } + }.also { + it.bgLayout = b.flDropdown + it.showAsDropDown(v) + } + } + } + + /** 绑定日期选择器,点击时弹出 DatePickerDialog */ + private fun bindDatePicker(b: ListItemFormFieldBinding, item: FormField) { + b.flDatePicker.visible() + b.tvDateValue.hint = item.hint.ifBlank { "请选择日期" } + b.tvDateValue.text = item.value.ifBlank { null } + + b.flDatePicker.setOnClickListener { v -> + v.hideKeyboard() + val cal = Calendar.getInstance() + DatePickerDialog( + activityContext, + { _, year, month, dayOfMonth -> + val selected = Calendar.getInstance().apply { + set(year, month, dayOfMonth, 0, 0, 0) + set(Calendar.MILLISECOND, 0) + } + // value 存显示格式,valueId 存 ISO 格式供提交使用 + item.value = displayDateFormat.format(selected.time) + item.valueId = isoFormat.format(selected.time) + b.tvDateValue.text = item.value + }, + cal.get(Calendar.YEAR), + cal.get(Calendar.MONTH), + cal.get(Calendar.DAY_OF_MONTH) + ).show() + } + } + + /** 构建带红色星号的必填标签 */ + private fun buildLabelSpannable(label: String, required: Boolean): SpannableStringBuilder { + return SpannableStringBuilder().apply { + append(label) + if (required) { + val start = length + append(" *") + setSpan( + ForegroundColorSpan("#FFCC0000".toColorInt()), + start, length, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE + ) + } + } + } + +} diff --git a/app/src/main/java/com/shuwei/dish/match/dialog/DropdownPopup.kt b/app/src/main/java/com/shuwei/dish/match/dialog/DropdownPopup.kt new file mode 100644 index 0000000..611e3d6 --- /dev/null +++ b/app/src/main/java/com/shuwei/dish/match/dialog/DropdownPopup.kt @@ -0,0 +1,78 @@ +package com.shuwei.dish.match.dialog + +import android.content.Context +import android.graphics.Color +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.PopupWindow +import androidx.core.graphics.drawable.toDrawable +import androidx.core.view.updateLayoutParams +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.shuwei.dish.match.R +import com.shuwei.dish.match.adapter.DropdownAdapter +import com.shuwei.dish.match.databinding.LayoutPopupDropdownBinding +import com.shuwei.dish.match.model.DictType +import com.shuwei.dish.match.utils.ext.dp + +class DropdownPopup( + var context: Context, + var list: MutableList, + var popWidth: Int = 280.dp, + var popHeight: Int = 284.dp, + var onPopupItemClick: (DictType) -> Unit +) : PopupWindow() { + + init { + // 初始化PopupWindow +// val popupView = LayoutInflater.from(context).inflate(R.layout.layout_popup_dropdown, null) + val binding = LayoutPopupDropdownBinding.inflate(LayoutInflater.from(context)) + contentView = binding.root + width = ViewGroup.LayoutParams.WRAP_CONTENT + height = ViewGroup.LayoutParams.WRAP_CONTENT + // 设置透明背景,避免覆盖圆角 + setBackgroundDrawable(Color.TRANSPARENT.toDrawable()) + isOutsideTouchable = true + isFocusable = true + + // 设置动画样式 +// animationStyle = R.style.PopupAnimation + + // 初始化视图和事件 + initViews(binding.rvWarehouse) + } + + private fun initViews(rv: RecyclerView) { + // 获取关闭按钮并设置点击事件 + rv.updateLayoutParams { + width = popWidth + height = popHeight + } + rv.layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false) + rv.adapter = DropdownAdapter(list).setOnItemClickListener { adapter, view, position -> + onPopupItemClick(list[position]) + dismiss() + } + } + + var arrowImage: ImageView? = null + + var bgLayout: ViewGroup? = null + + var defLayoutBgResId = R.drawable.bg_white_radius10_stroke2 + + override fun showAsDropDown(anchor: View?) { + super.showAsDropDown(anchor) + bgLayout?.setBackgroundResource(R.drawable.bg_white_stroke_blue) + arrowImage?.setImageResource(R.mipmap.ic_triangle_up) + } + + override fun dismiss() { + super.dismiss() + bgLayout?.setBackgroundResource(defLayoutBgResId) + arrowImage?.setImageResource(R.mipmap.ic_triangle_down) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/shuwei/dish/match/model/DictType.kt b/app/src/main/java/com/shuwei/dish/match/model/DictType.kt new file mode 100644 index 0000000..faf55c7 --- /dev/null +++ b/app/src/main/java/com/shuwei/dish/match/model/DictType.kt @@ -0,0 +1,14 @@ +package com.shuwei.dish.match.model + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +/** + * 字典类型 如仓库列表 物品类型 存储方式等 + */ +@Parcelize +data class DictType( + val id: String? = null, + val value: String? = null, + val type: String = "" +) : Parcelable diff --git a/app/src/main/java/com/shuwei/dish/match/model/FormField.kt b/app/src/main/java/com/shuwei/dish/match/model/FormField.kt new file mode 100644 index 0000000..5a62e8e --- /dev/null +++ b/app/src/main/java/com/shuwei/dish/match/model/FormField.kt @@ -0,0 +1,45 @@ +package com.shuwei.dish.match.model + +/** 表单字段类型枚举 */ +enum class FieldType { + /** 普通文本输入 */ + TEXT, + /** 数字输入(支持小数和负数) */ + NUMBER, + /** 下拉选择 */ + DROPDOWN, + /** 日期选择弹窗 */ + DATE_PICKER, + /** 固定值,不可编辑 */ + FIXED +} + +/** + * 动态表单字段数据模型 + * @param label 字段标签名称 + * @param type 字段类型,决定渲染哪种输入控件 + * @param apiKey 非下拉字段对应的接口参数名,空字符串表示不提交 + * @param extraApiKeys 下拉字段的多接口参数映射,key=接口参数名,value=DictType 字段名(id/value/type) + * @param required 是否必填,提交时校验 + * @param hidden 是否隐藏,hidden=true 时不渲染该字段 + * @param hint 校验失败提示文案,同时作为输入框 hint + * @param options 下拉框选项列表(DROPDOWN 类型使用) + * @param submitValueId 提交时使用 valueId 而非 value;DATE_PICKER 选 ISO 格式时设为 true + * @param value 当前显示值/输入值;DATE_PICKER 存 yyyy-MM-dd 显示格式 + * @param valueId DATE_PICKER 存 ISO 格式 yyyy-MM-dd'T'HH:mm:ss + * @param extraValues 下拉选中后按 extraApiKeys 映射存储的提交值,key=接口参数名 + */ +data class FormField( + val label: String, + val type: FieldType, + val apiKey: String = "", + val extraApiKeys: Map = emptyMap(), + val required: Boolean = false, + val hidden: Boolean = false, + val hint: String = "", + val options: List = emptyList(), + val submitValueId: Boolean = false, + var value: String = "", + var valueId: String = "", + val extraValues: MutableMap = mutableMapOf() +) diff --git a/app/src/main/java/com/shuwei/dish/match/utils/ext/Common.kt b/app/src/main/java/com/shuwei/dish/match/utils/ext/Common.kt index db97b83..21f0699 100644 --- a/app/src/main/java/com/shuwei/dish/match/utils/ext/Common.kt +++ b/app/src/main/java/com/shuwei/dish/match/utils/ext/Common.kt @@ -10,6 +10,7 @@ import android.os.Bundle import android.util.TypedValue import android.view.View import android.view.inputmethod.EditorInfo +import android.view.inputmethod.InputMethodManager import android.widget.EditText import android.widget.ImageView import android.widget.Toast @@ -160,4 +161,10 @@ fun Double.roundedOneDecimalPlace(): Double { } fun ImageView.load(url: Any?) { GlideUtils.loadImage(context, url, this) +} + +fun View.hideKeyboard() { + val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + imm.hideSoftInputFromWindow(this.windowToken, 0) + this.clearFocus() // 清除焦点避免键盘再次弹出 } \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_gray_radius10.xml b/app/src/main/res/drawable/bg_gray_radius10.xml new file mode 100644 index 0000000..ca49277 --- /dev/null +++ b/app/src/main/res/drawable/bg_gray_radius10.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_white_radius10_stroke.xml b/app/src/main/res/drawable/bg_white_radius10_stroke.xml new file mode 100644 index 0000000..aab7453 --- /dev/null +++ b/app/src/main/res/drawable/bg_white_radius10_stroke.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_white_radius10_stroke2.xml b/app/src/main/res/drawable/bg_white_radius10_stroke2.xml new file mode 100644 index 0000000..c1bd1ec --- /dev/null +++ b/app/src/main/res/drawable/bg_white_radius10_stroke2.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_white_stroke_blue.xml b/app/src/main/res/drawable/bg_white_stroke_blue.xml new file mode 100644 index 0000000..b185704 --- /dev/null +++ b/app/src/main/res/drawable/bg_white_stroke_blue.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/app/src/main/res/layout/layout_popup_dropdown.xml b/app/src/main/res/layout/layout_popup_dropdown.xml new file mode 100644 index 0000000..a7deb27 --- /dev/null +++ b/app/src/main/res/layout/layout_popup_dropdown.xml @@ -0,0 +1,19 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/list_item_dropdown.xml b/app/src/main/res/layout/list_item_dropdown.xml new file mode 100644 index 0000000..95f1d19 --- /dev/null +++ b/app/src/main/res/layout/list_item_dropdown.xml @@ -0,0 +1,24 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/list_item_form_field.xml b/app/src/main/res/layout/list_item_form_field.xml new file mode 100644 index 0000000..5921755 --- /dev/null +++ b/app/src/main/res/layout/list_item_form_field.xml @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/mipmap-xxhdpi/ic_triangle_down.png b/app/src/main/res/mipmap-xxhdpi/ic_triangle_down.png new file mode 100644 index 0000000000000000000000000000000000000000..f035d806d694e10336ac00ba295663b9dbe0397a GIT binary patch literal 763 zcmeAS@N?(olHy`uVBq!ia0vp^5g^RL1|$oo8kjIJFm3R3aSW+od~?gti#bq$?ZMqS zhnzkNsIAOht18ANHc!Q}W$6M17NHG7(fkvhctt)xzD2t;|KIm#y8GX3C^Y;(`)8S# zdvDzeCZ=Vd%z7JGeZ&-wXE$S~YIZp+$`->NwOiWY5L?5WE^guQ1eUO#H+|gv68ABt{~jvCttQ#Y*qla^}Yq8cswO7*>lV$cS)QO6xs4$JTSW&*_-_CoOHgV<)(J zn7NhGk00H0v#~IP@rlxo8Cy9F_|A32@Q4?SF&}#JcG8#H!WW+y@UI*N@KZ*ZPS zEHpf>Z(zr|MtOqs<0cam1F1jEmNt67`1yO(8t;^zJ@o2uUHq-u#`JHNe?nOeMARpj z<&|b{_+ECrzT{Eik5z0Ids6(Hncg*itkOx)KRHi^@%w=aTd@t(Q~diHsuT9eao_OX zq@I4@fUR(Polb($4*Y2Zlzy!94R+qGl}KBzE_IB=Z1qRN@K&ENR| zOk@Lt%z8F%39*L9R}&ZyGZpNS(n*M5=<~mpP;oBnF)NS!T!?{JO)9o#J>M30{q?t- z?}~4zM%IWPX!~`^Br)yvsWQ8*zjMCNtbPBlh<{JJjQj+d_w!{do;+8+exUk*^NzU- zs#CTf_?%ZWiKk!x}=I?`I#yJAlWs3?E z7_N&J1tthw=PfdPz_UYdLN`PD5#7EyvO5^^0-vloz*^X?n9f|*5Y1xAy+QfTn-dlr zobRkTq4B|U$K7wA?|h73ZTNtzV7Ic%M8OAL1%*$Z91wl9=!9pZ_OYWACNq9#5O?%r zx}$vO$%%^xX8TrW2LJuKytF#zscD?v?b|m_E^f?isE*#dhppM5_ilvAhcaoqr1lTL z>Mpmezj1RBP%#gjL@jZH5oBjT9U4G!=zfA6~Ee0}X=3<5$ zw{G|v_p>VNc0pm;wL3TM7Oht~uv?R-GBG>p^`1|qxo2x{ztueI#Wp8thFuzmgk2~j zW=w=Pv(~I?=>L8t;fD+hIBtL