Преглед изворни кода

✨ feat(mini): 添加地区选择器和富文本组件

- 新增地区选择器组件 AreaPicker,支持省市区三级联动选择
- 集成 mp-html 富文本组件,支持 HTML 内容渲染
- 在 app.config.ts 中注册 mp-html 组件
yourname пре 1 месец
родитељ
комит
30253d6071

+ 3 - 1
mini/src/app.config.ts

@@ -47,5 +47,7 @@ export default defineAppConfig({
       }
     ]
   },
-  usingComponents: {}
+  usingComponents: {
+    'mp-html': './components/mp-html'
+  }
 })

+ 272 - 0
mini/src/components/AreaPicker.tsx

@@ -0,0 +1,272 @@
+import React, { useState, useEffect } from 'react'
+import { View, Text, Picker, Button } from '@tarojs/components'
+import { useQuery } from '@tanstack/react-query'
+import { areaClient } from '../api'
+
+interface AreaInfo {
+  id: number
+  name: string
+  type: 'province' | 'city' | 'district'
+}
+
+interface AreaPickerProps {
+  visible: boolean
+  onClose: () => void
+  onConfirm: (areaIds: number[], areaInfos: AreaInfo[]) => void
+  value?: number[]
+  title?: string
+}
+
+export const AreaPicker: React.FC<AreaPickerProps> = ({
+  visible,
+  onClose,
+  onConfirm,
+  value = [],
+  title = '选择地区'
+}) => {
+  const [selectedProvince, setSelectedProvince] = useState<number | undefined>(value[0])
+  const [selectedCity, setSelectedCity] = useState<number | undefined>(value[1])
+  const [selectedDistrict, setSelectedDistrict] = useState<number | undefined>(value[2])
+
+  // 获取省份列表
+  const { data: provincesResponse } = useQuery({
+    queryKey: ['areas', 'provinces'],
+    queryFn: async () => {
+      const res = await areaClient.provinces.$get({
+        query: { page: 1, pageSize: 50 }
+      })
+      if (res.status !== 200) throw new Error('获取省份列表失败')
+      return await res.json()
+    }
+  })
+
+  // 获取城市列表
+  const { data: citiesResponse } = useQuery({
+    queryKey: ['areas', 'cities', selectedProvince],
+    queryFn: async () => {
+      if (!selectedProvince) return { success: true, data: { cities: [] }, message: '' }
+      const res = await areaClient.cities.$get({
+        query: { provinceId: selectedProvince, page: 1, pageSize: 50 }
+      })
+      if (res.status !== 200) throw new Error('获取城市列表失败')
+      return await res.json()
+    },
+    enabled: !!selectedProvince
+  })
+
+  // 获取区县列表
+  const { data: districtsResponse } = useQuery({
+    queryKey: ['areas', 'districts', selectedCity],
+    queryFn: async () => {
+      if (!selectedCity) return { success: true, data: { districts: [] }, message: '' }
+      const res = await areaClient.districts.$get({
+        query: { cityId: selectedCity, page: 1, pageSize: 50 }
+      })
+      if (res.status !== 200) throw new Error('获取区县列表失败')
+      return await res.json()
+    },
+    enabled: !!selectedCity
+  })
+
+  // 提取数据
+  const provinces = provincesResponse?.data?.provinces || []
+  const cities = citiesResponse?.data?.cities || []
+  const districts = districtsResponse?.data?.districts || []
+
+  // 初始化选择值 - 只在组件首次显示时初始化
+  const [hasInitialized, setHasInitialized] = useState(false)
+
+  useEffect(() => {
+    if (visible && !hasInitialized) {
+      setSelectedProvince(value[0])
+      setSelectedCity(value[1])
+      setSelectedDistrict(value[2])
+      setHasInitialized(true)
+    } else if (!visible) {
+      setHasInitialized(false)
+    }
+  }, [visible, value, hasInitialized])
+
+  // 处理省份选择
+  const handleProvinceChange = (e: any) => {
+    const provinceIndex = Number(e.detail.value)
+    const selectedProvinceObj = provinces[provinceIndex]
+    if (selectedProvinceObj) {
+      setSelectedProvince(selectedProvinceObj.id)
+      setSelectedCity(undefined)
+      setSelectedDistrict(undefined)
+    }
+  }
+
+  // 处理城市选择
+  const handleCityChange = (e: any) => {
+    const cityIndex = Number(e.detail.value)
+    const selectedCityObj = cities[cityIndex]
+    if (selectedCityObj) {
+      setSelectedCity(selectedCityObj.id)
+      setSelectedDistrict(undefined)
+    }
+  }
+
+  // 处理区县选择
+  const handleDistrictChange = (e: any) => {
+    const districtIndex = Number(e.detail.value)
+    const selectedDistrictObj = districts[districtIndex]
+    if (selectedDistrictObj) {
+      setSelectedDistrict(selectedDistrictObj.id)
+    }
+  }
+
+  // 确认选择
+  const handleConfirm = () => {
+    const areaIds: number[] = []
+    const areaInfos: AreaInfo[] = []
+
+    if (selectedProvince) {
+      const province = provinces.find(p => p.id === selectedProvince)
+      if (province) {
+        areaIds.push(selectedProvince)
+        areaInfos.push({ id: province.id, name: province.name, type: 'province' })
+      }
+    }
+
+    if (selectedCity) {
+      const city = cities.find(c => c.id === selectedCity)
+      if (city) {
+        areaIds.push(selectedCity)
+        areaInfos.push({ id: city.id, name: city.name, type: 'city' })
+      }
+    }
+
+    if (selectedDistrict) {
+      const district = districts.find(d => d.id === selectedDistrict)
+      if (district) {
+        areaIds.push(selectedDistrict)
+        areaInfos.push({ id: district.id, name: district.name, type: 'district' })
+      }
+    }
+
+    onConfirm(areaIds, areaInfos)
+    onClose()
+  }
+
+  // 取消选择
+  const handleCancel = () => {
+    onClose()
+  }
+
+  // 获取显示文本
+  const getDisplayText = () => {
+    if (!selectedProvince) return '请选择省市区'
+
+    const province = provinces.find(p => p.id === selectedProvince)
+    const city = cities.find(c => c.id === selectedCity)
+    const district = districts.find(d => d.id === selectedDistrict)
+
+    if (district && city && province) {
+      return `${province.name} ${city.name} ${district.name}`
+    } else if (city && province) {
+      return `${province.name} ${city.name}`
+    } else if (province) {
+      return province.name
+    }
+
+    return '请选择省市区'
+  }
+
+  if (!visible) return null
+
+  return (
+    <View className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
+      <View className="bg-white rounded-lg w-4/5 max-w-md">
+        {/* 标题栏 */}
+        <View className="flex justify-between items-center p-4 border-b border-gray-200">
+          <Text className="text-lg font-bold text-gray-800">{title}</Text>
+        </View>
+
+        {/* 选择器区域 */}
+        <View className="p-4">
+          {/* 当前选择显示 */}
+          <View className="mb-4 p-3 bg-gray-50 rounded-lg">
+            <Text className="text-sm text-gray-600">已选择:</Text>
+            <Text className="text-sm font-medium text-blue-600">{getDisplayText()}</Text>
+          </View>
+
+          {/* 三级选择器 */}
+          <View className="flex space-x-2 mb-4">
+            {/* 省份选择器 */}
+            <View className="flex-1">
+              <Text className="text-sm text-gray-600 mb-1 block">省份</Text>
+              <Picker
+                mode="selector"
+                range={provinces}
+                rangeKey="name"
+                value={selectedProvince ? provinces.findIndex(p => p.id === selectedProvince) : -1}
+                onChange={handleProvinceChange}
+              >
+                <View className="border border-gray-300 rounded px-3 py-2 text-sm bg-white">
+                  {selectedProvince ? provinces.find(p => p.id === selectedProvince)?.name : '请选择省份'}
+                </View>
+              </Picker>
+            </View>
+
+            {/* 城市选择器 */}
+            <View className="flex-1">
+              <Text className="text-sm text-gray-600 mb-1 block">城市</Text>
+              <Picker
+                mode="selector"
+                range={cities}
+                rangeKey="name"
+                value={selectedCity ? cities.findIndex(c => c.id === selectedCity) : -1}
+                onChange={handleCityChange}
+                disabled={!selectedProvince}
+              >
+                <View className={`border border-gray-300 rounded px-3 py-2 text-sm ${
+                  !selectedProvince ? 'bg-gray-100 text-gray-400' : 'bg-white'
+                }`}>
+                  {selectedCity ? cities.find(c => c.id === selectedCity)?.name : '请选择城市'}
+                </View>
+              </Picker>
+            </View>
+
+            {/* 区县选择器 */}
+            <View className="flex-1">
+              <Text className="text-sm text-gray-600 mb-1 block">区县</Text>
+              <Picker
+                mode="selector"
+                range={districts}
+                rangeKey="name"
+                value={selectedDistrict ? districts.findIndex(d => d.id === selectedDistrict) : -1}
+                onChange={handleDistrictChange}
+                disabled={!selectedCity}
+              >
+                <View className={`border border-gray-300 rounded px-3 py-2 text-sm ${
+                  !selectedCity ? 'bg-gray-100 text-gray-400' : 'bg-white'
+                }`}>
+                  {selectedDistrict ? districts.find(d => d.id === selectedDistrict)?.name : '请选择区县'}
+                </View>
+              </Picker>
+            </View>
+          </View>
+        </View>
+
+        {/* 按钮区域 */}
+        <View className="flex border-t border-gray-200">
+          <Button
+            className="flex-1 py-3 text-gray-600 bg-white border-none rounded-none"
+            onClick={handleCancel}
+          >
+            取消
+          </Button>
+          <View className="w-px bg-gray-200"></View>
+          <Button
+            className="flex-1 py-3 text-blue-500 bg-white border-none rounded-none font-bold"
+            onClick={handleConfirm}
+          >
+            确定
+          </Button>
+        </View>
+      </View>
+    </View>
+  )
+}

Разлика између датотеке није приказан због своје велике величине
+ 7 - 0
mini/src/components/mp-html/index.js


+ 1 - 0
mini/src/components/mp-html/index.json

@@ -0,0 +1 @@
+{"component":true,"usingComponents":{"node":"./node/node"}}

+ 1 - 0
mini/src/components/mp-html/index.wxml

@@ -0,0 +1 @@
+<view class="_root {{selectable?'_select':''}}" style="{{containerStyle}}"><slot wx:if="{{!nodes[0]}}"/><node id="_root" childs="{{nodes}}" opts="{{[lazyLoad,loadingImg,errorImg,showImgMenu,selectable]}}" catchadd="_add"/></view>

+ 1 - 0
mini/src/components/mp-html/index.wxss

@@ -0,0 +1 @@
+._root{padding:1px 0;overflow-x:auto;overflow-y:hidden;-webkit-overflow-scrolling:touch}._select{-webkit-user-select:text;user-select:text}

Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
mini/src/components/mp-html/node/node.js


+ 1 - 0
mini/src/components/mp-html/node/node.json

@@ -0,0 +1 @@
+{"component":true,"usingComponents":{"node":"./node"}}

Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
mini/src/components/mp-html/node/node.wxml


+ 1 - 0
mini/src/components/mp-html/node/node.wxss

@@ -0,0 +1 @@
+._a{padding:1.5px 0 1.5px 0;color:#366092;word-break:break-all}._hover{text-decoration:underline;opacity:.7}._img{max-width:100%;-webkit-touch-callout:none}._b,._strong{font-weight:700}._code{font-family:monospace}._del{text-decoration:line-through}._em,._i{font-style:italic}._h1{font-size:2em}._h2{font-size:1.5em}._h3{font-size:1.17em}._h5{font-size:.83em}._h6{font-size:.67em}._h1,._h2,._h3,._h4,._h5,._h6{display:block;font-weight:700}._ins{text-decoration:underline}._li{display:list-item}._ol{list-style-type:decimal}._ol,._ul{display:block;padding-left:40px;margin:1em 0}._q::before{content:'"'}._q::after{content:'"'}._sub{font-size:smaller;vertical-align:sub}._sup{font-size:smaller;vertical-align:super}._tbody,._tfoot,._thead{display:table-row-group}._tr{display:table-row}._td,._th{display:table-cell;vertical-align:middle}._th{font-weight:700;text-align:center}._ul{list-style-type:disc}._ul ._ul{margin:0;list-style-type:circle}._ul ._ul ._ul{list-style-type:square}._abbr,._b,._code,._del,._em,._i,._ins,._label,._q,._span,._strong,._sub,._sup{display:inline}._blockquote,._div,._p{display:block}

Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
mini/src/components/mp-html/parser.js


Неке датотеке нису приказане због велике количине промена