Explorar o código

feat:【system】地区管理:100%

YunaiV hai 4 meses
pai
achega
2d16080f03

+ 63 - 0
src/pages-system/area/components/area-tree-item.vue

@@ -0,0 +1,63 @@
+<template>
+  <view class="mb-16rpx overflow-hidden rounded-12rpx bg-white shadow-sm">
+    <view
+      class="flex items-center justify-between p-24rpx"
+      @click="handleToggle"
+    >
+      <view class="flex items-center">
+        <!-- 展开/收起图标 -->
+        <view class="mr-16rpx w-40rpx">
+          <wd-icon
+            v-if="hasChildren"
+            :name="expanded ? 'arrow-down' : 'arrow-right'"
+            size="16px"
+            color="#999"
+          />
+        </view>
+        <!-- 地区信息 -->
+        <view class="text-28rpx text-[#333]">
+          {{ item.name }}
+        </view>
+      </view>
+      <!-- 编码 -->
+      <view class="text-24rpx text-[#999]">
+        编码:{{ item.id }}
+      </view>
+    </view>
+
+    <!-- 子节点 -->
+    <view v-if="expanded && hasChildren" class="border-t border-[#f5f5f5] pl-56rpx">
+      <AreaTreeItem
+        v-for="child in item.children"
+        :key="child.id"
+        :item="child"
+        :level="level + 1"
+      />
+    </view>
+  </view>
+</template>
+
+<script lang="ts" setup>
+import type { Area } from '@/api/system/area'
+import { computed, ref } from 'vue'
+
+const props = withDefaults(defineProps<{
+  item: Area
+  level?: number
+}>(), {
+  level: 0,
+})
+
+const expanded = ref(false)
+const hasChildren = computed(() => props.item.children && props.item.children.length > 0)
+
+/** 切换展开/收起 */
+function handleToggle() {
+  if (hasChildren.value) {
+    expanded.value = !expanded.value
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+</style>

+ 74 - 0
src/pages-system/area/components/ip-query-form.vue

@@ -0,0 +1,74 @@
+<template>
+  <wd-popup v-model="visible" position="bottom" closable safe-area-inset-bottom>
+    <view class="p-32rpx">
+      <view class="mb-24rpx text-32rpx font-semibold">
+        IP 查询
+      </view>
+      <wd-input
+        v-model="ipAddress"
+        label="IP 地址"
+        label-width="160rpx"
+        placeholder="请输入 IP 地址"
+        clearable
+      />
+      <wd-input
+        v-model="ipResult"
+        label="地址"
+        label-width="160rpx"
+        placeholder="展示查询 IP 结果"
+        readonly
+        class="mt-24rpx"
+      />
+      <wd-button type="primary" block class="mt-32rpx" @click="handleQueryIp">
+        查询
+      </wd-button>
+    </view>
+  </wd-popup>
+</template>
+
+<script lang="ts" setup>
+import { ref, watch } from 'vue'
+import { getAreaByIp } from '@/api/system/area'
+import { isIp } from '@/utils/validator'
+
+const props = defineProps<{
+  modelValue: boolean
+}>()
+
+const emit = defineEmits<{
+  'update:modelValue': [value: boolean]
+}>()
+
+const visible = ref(false)
+const ipAddress = ref('')
+const ipResult = ref('')
+
+watch(() => props.modelValue, (val) => {
+  visible.value = val
+  if (val) {
+    ipAddress.value = ''
+    ipResult.value = ''
+  }
+})
+
+watch(visible, (val) => {
+  emit('update:modelValue', val)
+})
+
+/** 查询 IP */
+async function handleQueryIp() {
+  if (!ipAddress.value) {
+    uni.showToast({ title: '请输入 IP 地址', icon: 'none' })
+    return
+  }
+  if (!isIp(ipAddress.value)) {
+    uni.showToast({ title: '请输入正确的 IP 地址', icon: 'none' })
+    return
+  }
+  ipResult.value = await getAreaByIp(ipAddress.value)
+  uni.showToast({ title: '查询成功', icon: 'success' })
+}
+</script>
+
+<style lang="scss" scoped>
+</style>

+ 91 - 0
src/pages-system/area/index.vue

@@ -0,0 +1,91 @@
+<template>
+  <view class="yd-page-container">
+    <!-- 顶部导航栏 -->
+    <wd-navbar
+      title="地区管理"
+      left-arrow placeholder safe-area-inset-top fixed
+      @click-left="handleBack"
+    />
+
+    <!-- 地区树列表 -->
+    <view class="p-24rpx">
+      <!-- 加载中 -->
+      <view v-if="loading" class="py-100rpx text-center">
+        <wd-loading />
+      </view>
+      <!-- 地区树 -->
+      <view v-else-if="areaList.length > 0">
+        <AreaTreeItem
+          v-for="item in areaList"
+          :key="item.id"
+          :item="item"
+        />
+      </view>
+      <!-- 空状态 -->
+      <view v-else class="py-100rpx text-center">
+        <wd-status-tip image="content" tip="暂无地区数据" />
+      </view>
+    </view>
+
+    <!-- 搜索按钮 -->
+    <wd-fab
+      position="right-bottom"
+      type="primary"
+      :expandable="false"
+      icon="search"
+      @click="handleOpenIpQuery"
+    />
+
+    <!-- IP 查询弹窗 -->
+    <IpQueryForm v-model="showIpQuery" />
+  </view>
+</template>
+
+<script lang="ts" setup>
+import type { Area } from '@/api/system/area'
+import { ref } from 'vue'
+import { getAreaTree } from '@/api/system/area'
+import { navigateBackPlus } from '@/utils'
+import AreaTreeItem from './components/area-tree-item.vue'
+import IpQueryForm from './components/ip-query-form.vue'
+
+definePage({
+  style: {
+    navigationBarTitleText: '',
+    navigationStyle: 'custom',
+  },
+})
+
+const loading = ref(false)
+const areaList = ref<Area[]>([])
+
+const showIpQuery = ref(false) // 是否显示 IP 查询弹窗
+
+/** 获取地区树 */
+async function getList() {
+  loading.value = true
+  try {
+    areaList.value = await getAreaTree()
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 打开 IP 查询弹窗 */
+function handleOpenIpQuery() {
+  showIpQuery.value = true
+}
+
+/** 返回上一页 */
+function handleBack() {
+  navigateBackPlus()
+}
+
+/** 初始化 */
+onMounted(() => {
+  getList()
+})
+</script>
+
+<style lang="scss" scoped>
+</style>

+ 7 - 0
src/pages/index/index.ts

@@ -149,6 +149,13 @@ const menuGroupsData: MenuGroup[] = [
         iconColor: '#faad14',
         permission: 'system:dict:query',
       },
+      {
+        key: 'area',
+        name: '地区管理',
+        icon: 'location',
+        url: '/pages-system/area/index',
+        iconColor: '#d50f0f',
+      },
     ],
   },
   {

+ 17 - 0
src/utils/validator.ts

@@ -4,6 +4,10 @@ const MOBILE_REGEX = /^1[3-9]\d{9}$/
 /** 邮箱正则表达式 */
 const EMAIL_REGEX = /^[\w-]+(?:\.[\w-]+)*@[\w-]+(?:\.[\w-]+)+$/
 
+/** IP 地址正则表达式(IPv4) */
+// eslint-disable-next-line regexp/no-unused-capturing-group
+const IP_REGEX = /^(\d{1,3}\.){3}\d{1,3}$/
+
 /**
  * 判断字符串是否为空白(null、undefined、空字符串或仅包含空白字符)
  *
@@ -39,3 +43,16 @@ export function isEmail(value?: null | string): boolean {
   }
   return EMAIL_REGEX.test(value)
 }
+
+/**
+ * 验证是否为 IP 地址(IPv4)
+ *
+ * @param value 值
+ * @returns 是否为 IP 地址
+ */
+export function isIp(value?: null | string): boolean {
+  if (!value) {
+    return false
+  }
+  return IP_REGEX.test(value)
+}