瀏覽代碼

feat:【infra】移动端 admin uniapp 的代码生成的初始化

YunaiV 6 月之前
父節點
當前提交
33fb112dfe

+ 2 - 0
yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/enums/codegen/CodegenFrontTypeEnum.java

@@ -23,6 +23,8 @@ public enum CodegenFrontTypeEnum {
 
     VUE3_VBEN5_EP_SCHEMA(50), // Vue3 VBEN5 + EP + schema 模版
     VUE3_VBEN5_EP_GENERAL(51), // Vue3 VBEN5 + EP 标准模版
+
+    VUE3_ADMIN_UNIAPP_WOT(60), // Vue3 Admin + Uniapp + WOT 标准模版
     ;
 
     /**

+ 19 - 0
yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenEngine.java

@@ -137,6 +137,16 @@ public class CodegenEngine {
                     vue3FilePath("views/${table.moduleName}/${table.businessName}/components/${subSimpleClassName}List.vue"))
             .put(CodegenFrontTypeEnum.VUE3_ELEMENT_PLUS.getType(), vue3TemplatePath("api/api.ts"),
                     vue3FilePath("api/${table.moduleName}/${table.businessName}/index.ts"))
+            .put(CodegenFrontTypeEnum.VUE3_ADMIN_UNIAPP_WOT.getType(), vue3AdminUniappTemplatePath("api/api.ts"),
+                    vue3UniappFilePath("api/${table.moduleName}/${table.businessName}/index.ts"))
+            .put(CodegenFrontTypeEnum.VUE3_ADMIN_UNIAPP_WOT.getType(), vue3AdminUniappTemplatePath("views/index.vue"),
+                    vue3UniappFilePath("pages-${table.moduleName}/${table.businessName}/index.vue"))
+            .put(CodegenFrontTypeEnum.VUE3_ADMIN_UNIAPP_WOT.getType(), vue3AdminUniappTemplatePath("components/search-form.vue"),
+                    vue3UniappFilePath("pages-${table.moduleName}/${table.businessName}/components/search-form.vue"))
+            .put(CodegenFrontTypeEnum.VUE3_ADMIN_UNIAPP_WOT.getType(), vue3AdminUniappTemplatePath("views/form/index.vue"),
+                    vue3UniappFilePath("pages-${table.moduleName}/${table.businessName}/form/index.vue"))
+            .put(CodegenFrontTypeEnum.VUE3_ADMIN_UNIAPP_WOT.getType(), vue3AdminUniappTemplatePath("views/detail/index.vue"),
+                    vue3UniappFilePath("pages-${table.moduleName}/${table.businessName}/detail/index.vue"))
             // VUE3_VBEN2_ANTD_SCHEMA
             .put(CodegenFrontTypeEnum.VUE3_VBEN2_ANTD_SCHEMA.getType(), vue3VbenTemplatePath("views/data.ts"),
                     vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/${classNameVar}.data.ts"))
@@ -617,6 +627,15 @@ public class CodegenEngine {
                 "src/" + path;
     }
 
+    private static String vue3AdminUniappTemplatePath(String path) {
+        return "codegen/vue3_admin_uniapp/" + path + ".vm";
+    }
+
+    private static String vue3UniappFilePath(String path) {
+        return "yudao-ui-${sceneEnum.basePackage}-uniapp/" + // 顶级目录
+                "src/" + path;
+    }
+
     private static String vue3VbenFilePath(String path) {
         return "yudao-ui-${sceneEnum.basePackage}-vben/" + // 顶级目录
                 "src/" + path;

+ 55 - 0
yudao-module-infra/src/main/resources/codegen/vue3_admin_uniapp/api/api.ts.vm

@@ -0,0 +1,55 @@
+import type { PageParam, PageResult } from '@/http/types'
+import { http } from '@/http/http'
+
+// TODO @AI:不使用 baseUrl,而是参考之前的,直接写在每个方法里。
+#set ($baseURL = "/${table.moduleName}/${simpleClassName_strikeCase}")
+const baseUrl = '${baseURL}'
+
+#set ($primaryJavaType = $primaryColumn.javaType.toLowerCase())
+#if(${primaryJavaType} == "long" || ${primaryJavaType} == "integer" || ${primaryJavaType} == "short" || ${primaryJavaType} == "double" || ${primaryJavaType} == "bigdecimal" || ${primaryJavaType} == "byte")
+#set ($primaryTsType = "number")
+#else
+#set ($primaryTsType = "string")
+#end
+
+/** ${table.classComment}信息 */
+export interface ${simpleClassName} {
+#foreach ($column in $columns)
+  #if ($column.primaryKey || $column.createOperation || $column.updateOperation || $column.listOperationResult)
+    #set ($javaType = $column.javaType.toLowerCase())
+    #set ($optional = $column.nullable || $column.primaryKey)
+    #if(${javaType} == "long" || ${javaType} == "integer" || ${javaType} == "short" || ${javaType} == "double" || ${javaType} == "bigdecimal" || ${javaType} == "byte")
+  ${column.javaField}#if($optional)?#end: number
+    #elseif(${javaType} == "boolean")
+  ${column.javaField}#if($optional)?#end: boolean
+    #else
+  ${column.javaField}#if($optional)?#end: string
+    #end
+  #end
+#end
+}
+
+/** 获取${table.classComment}分页列表 */
+export function get${simpleClassName}Page(params: PageParam) {
+  return http.get<PageResult<${simpleClassName}>>(baseUrl + '/page', params)
+}
+
+/** 获取${table.classComment}详情 */
+export function get${simpleClassName}(id: ${primaryTsType}) {
+  return http.get<${simpleClassName}>(baseUrl + '/get?id=' + id)
+}
+
+/** 创建${table.classComment} */
+export function create${simpleClassName}(data: ${simpleClassName}) {
+  return http.post<number>(baseUrl + '/create', data)
+}
+
+/** 更新${table.classComment} */
+export function update${simpleClassName}(data: ${simpleClassName}) {
+  return http.put<boolean>(baseUrl + '/update', data)
+}
+
+/** 删除${table.classComment} */
+export function delete${simpleClassName}(id: ${primaryTsType}) {
+  return http.delete<boolean>(baseUrl + '/delete?id=' + id)
+}

+ 257 - 0
yudao-module-infra/src/main/resources/codegen/vue3_admin_uniapp/components/search-form.vue.vm

@@ -0,0 +1,257 @@
+<template>
+  <wd-search
+    :placeholder="searchPlaceholder"
+    :hide-cancel="true"
+    disabled
+    @click="visible = true"
+  />
+
+  <wd-popup
+    v-model="visible"
+    position="top"
+    custom-style="border-radius: 0 0 24rpx 24rpx;"
+    safe-area-inset-top
+    @close="visible = false"
+  >
+    <view class="p-32rpx">
+      <view class="mb-24rpx text-32rpx text-[#333] font-semibold">
+        搜索${table.classComment}
+      </view>
+
+#set ($hasDict = 0)
+#foreach ($column in $columns)
+  #if ($hasDict == 0 && $column.listOperation && $column.dictType && "" != $column.dictType)
+    #set ($hasDict = 1)
+  #end
+#end
+
+#foreach($column in $columns)
+  #if ($column.listOperation)
+    #set ($dictType = $column.dictType)
+    #set ($javaField = $column.javaField)
+    #set ($javaType = $column.javaType)
+    #set ($comment = $column.columnComment)
+    #set ($dictMethod = "getDictOptions")
+    #if ($javaType == "Integer" || $javaType == "Long" || $javaType == "Byte" || $javaType == "Short")
+      #set ($dictMethod = "getIntDictOptions")
+    #elseif ($javaType == "String")
+      #set ($dictMethod = "getStrDictOptions")
+    #elseif ($javaType == "Boolean")
+      #set ($dictMethod = "getBoolDictOptions")
+    #end
+
+    #if ($column.htmlType == "input")
+      <view class="mb-24rpx">
+        <view class="mb-12rpx text-28rpx text-[#666]">
+          ${comment}
+        </view>
+        <wd-input
+          v-model="formData.${javaField}"
+          placeholder="请输入${comment}"
+          clearable
+        />
+      </view>
+    #elseif (($column.htmlType == "select" || $column.htmlType == "radio") && $dictType && "" != $dictType)
+      <view class="mb-32rpx">
+        <view class="mb-12rpx text-28rpx text-[#666]">
+          ${comment}
+        </view>
+        <wd-radio-group v-model="formData.${javaField}" shape="button" size="medium">
+          <wd-radio :value="-1">
+            全部
+          </wd-radio>
+          <wd-radio
+            v-for="dict in $dictMethod(DICT_TYPE.$dictType.toUpperCase())"
+            :key="dict.value"
+            :value="dict.value"
+          >
+            {{ dict.label }}
+          </wd-radio>
+        </wd-radio-group>
+      </view>
+    #else
+      <view class="mb-24rpx">
+        <view class="mb-12rpx text-28rpx text-[#666]">
+          ${comment}
+        </view>
+        <wd-input
+          v-model="formData.${javaField}"
+          placeholder="请输入${comment}"
+          clearable
+        />
+      </view>
+    #end
+  #end
+#end
+
+      <view class="w-full flex justify-center gap-24rpx">
+        <wd-button class="flex-1" plain @click="handleReset">
+          重置
+        </wd-button>
+        <wd-button class="flex-1" type="primary" @click="handleSearch">
+          搜索
+        </wd-button>
+      </view>
+    </view>
+  </wd-popup>
+</template>
+
+<script lang="ts" setup>
+import { computed, reactive, ref, watch } from 'vue'
+
+#set ($hasDict = 0)
+#set ($hasGetDictOptions = 0)
+#set ($hasGetIntDictOptions = 0)
+#set ($hasGetStrDictOptions = 0)
+#set ($hasGetBoolDictOptions = 0)
+#foreach($column in $columns)
+  #if ($column.listOperation && $column.dictType && "" != $column.dictType)
+    #set ($hasDict = 1)
+    #if ($column.htmlType == "select" || $column.htmlType == "radio")
+      #if ($column.javaType == "Integer" || $column.javaType == "Long" || $column.javaType == "Byte" || $column.javaType == "Short")
+        #set ($hasGetIntDictOptions = 1)
+      #elseif ($column.javaType == "String")
+        #set ($hasGetStrDictOptions = 1)
+      #elseif ($column.javaType == "Boolean")
+        #set ($hasGetBoolDictOptions = 1)
+      #else
+        #set ($hasGetDictOptions = 1)
+      #end
+    #end
+  #end
+#end
+#if ($hasDict == 1)
+import { DICT_TYPE } from '@/utils/constants'
+import {
+  #if ($hasGetDictOptions == 1)
+  getDictOptions,
+  #end
+  #if ($hasGetIntDictOptions == 1)
+  getIntDictOptions,
+  #end
+  #if ($hasGetStrDictOptions == 1)
+  getStrDictOptions,
+  #end
+  #if ($hasGetBoolDictOptions == 1)
+  getBoolDictOptions,
+  #end
+  getDictLabel,
+} from '@/hooks/useDict'
+#end
+
+/** 搜索表单数据 */
+export interface SearchFormData {
+#foreach($column in $columns)
+  #if ($column.listOperation)
+    #set ($javaType = $column.javaType.toLowerCase())
+    #if ($column.dictType && "" != $column.dictType)
+      #if(${javaType} == "string")
+  ${column.javaField}?: string | number
+      #elseif(${javaType} == "boolean")
+  ${column.javaField}?: boolean | number
+      #else
+  ${column.javaField}?: number
+      #end
+    #elseif(${javaType} == "long" || ${javaType} == "integer" || ${javaType} == "short" || ${javaType} == "double" || ${javaType} == "bigdecimal" || ${javaType} == "byte")
+  ${column.javaField}?: number
+    #elseif(${javaType} == "boolean")
+  ${column.javaField}?: boolean
+    #else
+  ${column.javaField}?: string
+    #end
+  #end
+#end
+}
+
+const props = defineProps<{
+  searchParams?: Partial<SearchFormData>
+}>()
+
+const emit = defineEmits<{
+  search: [data: SearchFormData]
+  reset: []
+}>()
+
+const visible = ref(false)
+
+/** 搜索条件 placeholder 拼接 */
+const searchPlaceholder = computed(() => {
+  const conditions: string[] = []
+#foreach($column in $columns)
+  #if ($column.listOperation)
+    #set ($dictType = $column.dictType)
+    #set ($javaField = $column.javaField)
+    #set ($javaType = $column.javaType.toLowerCase())
+    #set ($comment = $column.columnComment)
+    #if ($dictType && "" != $dictType)
+  if (props.searchParams?.${javaField} !== undefined && props.searchParams.${javaField} !== -1) {
+    conditions.push('${comment}:' + getDictLabel(DICT_TYPE.$dictType.toUpperCase(), props.searchParams.${javaField}))
+  }
+    #else
+  #if(${javaType} == "long" || ${javaType} == "integer" || ${javaType} == "short" || ${javaType} == "double" || ${javaType} == "bigdecimal" || ${javaType} == "byte" || ${javaType} == "boolean")
+  if (props.searchParams?.${javaField} !== undefined) {
+    conditions.push('${comment}:' + props.searchParams.${javaField})
+  }
+  #else
+  if (props.searchParams?.${javaField}) {
+    conditions.push('${comment}:' + props.searchParams.${javaField})
+  }
+  #end
+    #end
+  #end
+#end
+  return conditions.length > 0 ? conditions.join(' | ') : '搜索${table.classComment}'
+})
+
+const formData = reactive<SearchFormData>({
+#foreach($column in $columns)
+  #if ($column.listOperation)
+    #if ($column.dictType && "" != $column.dictType)
+      #set ($javaType = $column.javaType.toLowerCase())
+      #if(${javaType} == "string")
+  ${column.javaField}: -1 as number | string,
+      #elseif(${javaType} == "boolean")
+  ${column.javaField}: -1 as number | boolean,
+      #else
+  ${column.javaField}: -1 as number,
+      #end
+    #else
+  ${column.javaField}: undefined,
+    #end
+  #end
+#end
+})
+
+watch(visible, (val) => {
+  if (val && props.searchParams) {
+#foreach($column in $columns)
+  #if ($column.listOperation)
+    #if ($column.dictType && "" != $column.dictType)
+    formData.${column.javaField} = props.searchParams.${column.javaField} ?? -1
+    #else
+    formData.${column.javaField} = props.searchParams.${column.javaField}
+    #end
+  #end
+#end
+  }
+})
+
+function handleSearch() {
+  visible.value = false
+  emit('search', { ...formData } as SearchFormData)
+}
+
+function handleReset() {
+#foreach($column in $columns)
+  #if ($column.listOperation)
+    #if ($column.dictType && "" != $column.dictType)
+  formData.${column.javaField} = -1
+    #else
+  formData.${column.javaField} = undefined
+    #end
+  #end
+#end
+  visible.value = false
+  emit('reset')
+}
+</script>

+ 143 - 0
yudao-module-infra/src/main/resources/codegen/vue3_admin_uniapp/views/detail/index.vue.vm

@@ -0,0 +1,143 @@
+<template>
+  <view class="page-container">
+    <wd-navbar
+      title="${table.classComment}详情"
+      left-arrow placeholder safe-area-inset-top fixed
+      @click-left="handleBack"
+    />
+
+    <view>
+      <wd-cell-group border>
+#foreach($column in $columns)
+  #if ($column.primaryKey || $column.listOperationResult || $column.createOperation || $column.updateOperation)
+    #set ($javaField = $column.javaField)
+    #set ($comment = $column.columnComment)
+    #if ($column.dictType && "" != $column.dictType)
+        <wd-cell title="${comment}">
+          <dict-tag :type="DICT_TYPE.${column.dictType.toUpperCase()}" :value="formData?.${javaField}" />
+        </wd-cell>
+    #elseif ($column.javaType == "LocalDateTime")
+        <wd-cell title="${comment}" :value="formatDateTime(formData?.${javaField}) || '-'" />
+    #else
+        <wd-cell title="${comment}" :value="String(formData?.${javaField} ?? '-')" />
+    #end
+  #end
+#end
+      </wd-cell-group>
+    </view>
+
+    <view class="fixed bottom-0 left-0 right-0 bg-white p-24rpx">
+      <view class="w-full flex gap-24rpx">
+        <wd-button
+          v-if="hasAccessByCodes(['${permissionPrefix}:update'])"
+          class="flex-1" type="warning" @click="handleEdit"
+        >
+          编辑
+        </wd-button>
+        <wd-button
+          v-if="hasAccessByCodes(['${permissionPrefix}:delete'])"
+          class="flex-1" type="error" :loading="deleting" @click="handleDelete"
+        >
+          删除
+        </wd-button>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script lang="ts" setup>
+import type { ${simpleClassName} } from '@/api/${table.moduleName}/${table.businessName}'
+import { onMounted, ref } from 'vue'
+import { useToast } from 'wot-design-uni'
+import { delete${simpleClassName}, get${simpleClassName} } from '@/api/${table.moduleName}/${table.businessName}'
+import { useAccess } from '@/hooks/useAccess'
+import { navigateBackPlus } from '@/utils'
+#set ($hasDict = 0)
+#foreach($column in $columns)
+  #if ($hasDict == 0 && $column.dictType && "" != $column.dictType)
+    #set ($hasDict = 1)
+  #end
+#end
+#if ($hasDict == 1)
+import { DICT_TYPE } from '@/utils/constants'
+#end
+#set ($hasDateTime = 0)
+#foreach($column in $columns)
+  #if ($hasDateTime == 0 && $column.javaType == "LocalDateTime")
+    #set ($hasDateTime = 1)
+  #end
+#end
+#if ($hasDateTime == 1)
+import { formatDateTime } from '@/utils/date'
+#end
+
+const props = defineProps<{
+  id?: number | any
+}>()
+
+definePage({
+  style: {
+    navigationBarTitleText: '',
+    navigationStyle: 'custom',
+  },
+})
+
+const { hasAccessByCodes } = useAccess()
+const toast = useToast()
+const formData = ref<${simpleClassName}>()
+const deleting = ref(false)
+
+function handleBack() {
+  navigateBackPlus('/pages-${table.moduleName}/${table.businessName}/index')
+}
+
+async function getDetail() {
+  if (!props.id) {
+    return
+  }
+  try {
+    toast.loading('加载中...')
+    formData.value = await get${simpleClassName}(props.id)
+  } finally {
+    toast.close()
+  }
+}
+
+function handleEdit() {
+  uni.navigateTo({
+    url: '/pages-${table.moduleName}/${table.businessName}/form/index?id=' + props.id,
+  })
+}
+
+function handleDelete() {
+  if (!props.id) {
+    return
+  }
+  uni.showModal({
+    title: '提示',
+    content: '确定要删除该${table.classComment}吗?',
+    success: async (res) => {
+      if (!res.confirm) {
+        return
+      }
+      deleting.value = true
+      try {
+        await delete${simpleClassName}(props.id)
+        toast.success('删除成功')
+        setTimeout(() => {
+          handleBack()
+        }, 500)
+      } finally {
+        deleting.value = false
+      }
+    },
+  })
+}
+
+onMounted(() => {
+  getDetail()
+})
+</script>
+
+<style lang="scss" scoped>
+</style>

+ 225 - 0
yudao-module-infra/src/main/resources/codegen/vue3_admin_uniapp/views/form/index.vue.vm

@@ -0,0 +1,225 @@
+<template>
+  <view class="page-container">
+    <wd-navbar
+      :title="getTitle"
+      left-arrow placeholder safe-area-inset-top fixed
+      @click-left="handleBack"
+    />
+
+    <view>
+      <wd-form ref="formRef" :model="formData" :rules="formRules">
+        <wd-cell-group border>
+#foreach($column in $columns)
+  #if (($column.createOperation || $column.updateOperation) && !$column.primaryKey)
+    #set ($dictType = $column.dictType)
+    #set ($javaField = $column.javaField)
+    #set ($javaType = $column.javaType)
+    #set ($comment = $column.columnComment)
+    #set ($dictMethod = "getDictOptions")
+    #if ($javaType == "Integer" || $javaType == "Long" || $javaType == "Byte" || $javaType == "Short")
+      #set ($dictMethod = "getIntDictOptions")
+    #elseif ($javaType == "String")
+      #set ($dictMethod = "getStrDictOptions")
+    #elseif ($javaType == "Boolean")
+      #set ($dictMethod = "getBoolDictOptions")
+    #end
+
+    #if (${javaType.toLowerCase()} == "long" || ${javaType.toLowerCase()} == "integer" || ${javaType.toLowerCase()} == "short" || ${javaType.toLowerCase()} == "double" || ${javaType.toLowerCase()} == "bigdecimal" || ${javaType.toLowerCase()} == "byte")
+          <wd-cell title="${comment}" title-width="180rpx" prop="${javaField}" center>
+            <wd-input-number
+              v-model="formData.${javaField}"
+              :min="0"
+            />
+          </wd-cell>
+    #elseif (${javaType.toLowerCase()} == "boolean")
+          <wd-cell title="${comment}" title-width="180rpx" prop="${javaField}" center>
+            <wd-switch v-model="formData.${javaField}" />
+          </wd-cell>
+    #elseif ($column.htmlType == "textarea")
+          <wd-textarea
+            v-model="formData.${javaField}"
+            label="${comment}"
+            label-width="180rpx"
+            placeholder="请输入${comment}"
+            :maxlength="200"
+            show-word-limit
+            clearable
+          />
+    #elseif (($column.htmlType == "select" || $column.htmlType == "radio") && $dictType && "" != $dictType)
+          <wd-cell title="${comment}" title-width="180rpx" prop="${javaField}" center>
+            <wd-radio-group v-model="formData.${javaField}" shape="button" size="medium">
+              <wd-radio
+                v-for="dict in $dictMethod(DICT_TYPE.$dictType.toUpperCase())"
+                :key="dict.value"
+                :value="dict.value"
+              >
+                {{ dict.label }}
+              </wd-radio>
+            </wd-radio-group>
+          </wd-cell>
+    #else
+          <wd-input
+            v-model="formData.${javaField}"
+            label="${comment}"
+            label-width="180rpx"
+            prop="${javaField}"
+            clearable
+            placeholder="请输入${comment}"
+          />
+    #end
+  #end
+#end
+        </wd-cell-group>
+      </wd-form>
+    </view>
+
+    <view class="fixed bottom-0 left-0 right-0 bg-white p-24rpx">
+      <wd-button
+        type="primary"
+        block
+        :loading="formLoading"
+        @click="handleSubmit"
+      >
+        保存
+      </wd-button>
+    </view>
+  </view>
+</template>
+
+<script lang="ts" setup>
+import type { ${simpleClassName} } from '@/api/${table.moduleName}/${table.businessName}'
+import { computed, onMounted, ref } from 'vue'
+import { useToast } from 'wot-design-uni'
+import { create${simpleClassName}, get${simpleClassName}, update${simpleClassName} } from '@/api/${table.moduleName}/${table.businessName}'
+import { navigateBackPlus } from '@/utils'
+
+#set ($primaryJavaType = $primaryColumn.javaType.toLowerCase())
+#if(${primaryJavaType} == "long" || ${primaryJavaType} == "integer" || ${primaryJavaType} == "short" || ${primaryJavaType} == "double" || ${primaryJavaType} == "bigdecimal" || ${primaryJavaType} == "byte")
+#set ($primaryTsType = "number")
+#else
+#set ($primaryTsType = "string")
+#end
+
+#set ($hasDict = 0)
+#set ($hasGetDictOptions = 0)
+#set ($hasGetIntDictOptions = 0)
+#set ($hasGetStrDictOptions = 0)
+#set ($hasGetBoolDictOptions = 0)
+#foreach ($column in $columns)
+  #if (($column.createOperation || $column.updateOperation) && !$column.primaryKey
+      && ($column.htmlType == "select" || $column.htmlType == "radio")
+      && $column.dictType && "" != $column.dictType)
+    #set ($hasDict = 1)
+    #if ($column.javaType == "Integer" || $column.javaType == "Long" || $column.javaType == "Byte" || $column.javaType == "Short")
+      #set ($hasGetIntDictOptions = 1)
+    #elseif ($column.javaType == "String")
+      #set ($hasGetStrDictOptions = 1)
+    #elseif ($column.javaType == "Boolean")
+      #set ($hasGetBoolDictOptions = 1)
+    #else
+      #set ($hasGetDictOptions = 1)
+    #end
+  #end
+#end
+#if ($hasDict == 1)
+import { DICT_TYPE } from '@/utils/constants'
+import {
+  #if ($hasGetDictOptions == 1)
+  getDictOptions,
+  #end
+  #if ($hasGetIntDictOptions == 1)
+  getIntDictOptions,
+  #end
+  #if ($hasGetStrDictOptions == 1)
+  getStrDictOptions,
+  #end
+  #if ($hasGetBoolDictOptions == 1)
+  getBoolDictOptions,
+  #end
+} from '@/hooks/useDict'
+#end
+
+const props = defineProps<{
+  id?: ${primaryTsType} | any
+}>()
+
+definePage({
+  style: {
+    navigationBarTitleText: '',
+    navigationStyle: 'custom',
+  },
+})
+
+const toast = useToast()
+const getTitle = computed(() => props.id ? '编辑${table.classComment}' : '新增${table.classComment}')
+const formLoading = ref(false)
+
+// TODO @AI:date + 时间范围,需要特殊处理;参考 /Users/yunai/Java/yudao-ui-admin-uniapp-next-v4/src/pages/message/components/search-form.vue 里的开始结束时间的处理;注意,index.vue.vm 也要调整;
+const formData = ref<${simpleClassName}>({
+#foreach($column in $columns)
+  #if (($column.createOperation || $column.updateOperation) || $column.primaryKey)
+    #set ($javaType = $column.javaType.toLowerCase())
+    #if ($column.primaryKey)
+  ${column.javaField}: undefined,
+    #elseif(${javaType} == "long" || ${javaType} == "integer" || ${javaType} == "short" || ${javaType} == "double" || ${javaType} == "bigdecimal" || ${javaType} == "byte")
+  ${column.javaField}: 0,
+    #elseif(${javaType} == "boolean")
+  ${column.javaField}: false,
+    #else
+  ${column.javaField}: '',
+    #end
+  #end
+#end
+})
+
+const formRules = {
+#foreach($column in $columns)
+  #if (($column.createOperation || $column.updateOperation) && !$column.nullable && !$column.primaryKey)
+  ${column.javaField}: [{ required: true, message: '${column.columnComment}不能为空' }],
+  #end
+#end
+}
+
+const formRef = ref()
+
+function handleBack() {
+  navigateBackPlus('/pages-${table.moduleName}/${table.businessName}/index')
+}
+
+async function getDetail() {
+  if (!props.id) {
+    return
+  }
+  formData.value = await get${simpleClassName}(props.id)
+}
+
+async function handleSubmit() {
+  const { valid } = await formRef.value.validate()
+  if (!valid) {
+    return
+  }
+
+  formLoading.value = true
+  try {
+    if (props.id) {
+      await update${simpleClassName}(formData.value)
+      toast.success('修改成功')
+    } else {
+      await create${simpleClassName}(formData.value)
+      toast.success('新增成功')
+    }
+    setTimeout(() => {
+      handleBack()
+    }, 500)
+  } finally {
+    formLoading.value = false
+  }
+}
+
+onMounted(() => {
+  getDetail()
+})
+</script>
+
+<style lang="scss" scoped>
+</style>

+ 220 - 0
yudao-module-infra/src/main/resources/codegen/vue3_admin_uniapp/views/index.vue.vm

@@ -0,0 +1,220 @@
+<template>
+  <view class="page-container">
+    <!-- 顶部导航栏 -->
+    <wd-navbar
+      title="${table.classComment}管理"
+      left-arrow placeholder safe-area-inset-top fixed
+      @click-left="handleBack"
+    />
+
+    <!-- 搜索组件 -->
+    <SearchForm
+      :search-params="queryParams"
+      @search="handleQuery"
+      @reset="handleReset"
+    />
+
+    <!-- ${table.classComment}列表 -->
+    <view class="p-24rpx">
+      <view
+        v-for="item in list"
+        :key="item.${primaryColumn.javaField}"
+        class="mb-24rpx overflow-hidden rounded-12rpx bg-white shadow-sm"
+        @click="handleDetail(item)"
+      >
+        <!-- TODO @AI:下面的 view 部分代码,缩进可能不太对; -->
+        <view class="p-24rpx">
+#set ($titleField = "")
+#set ($statusField = "")
+#set ($statusDictType = "")
+#foreach($column in $columns)
+  #if ($titleField == "" && !$column.primaryKey && $column.listOperationResult)
+    #set ($titleField = $column.javaField)
+    #set ($titleComment = $column.columnComment)
+  #end
+  #if ($statusField == "" && $column.listOperationResult && $column.dictType && "" != $column.dictType)
+    #set ($statusField = $column.javaField)
+    #set ($statusDictType = $column.dictType)
+  #end
+#end
+          <view class="mb-16rpx flex items-center justify-between">
+            <view class="text-32rpx text-[#333] font-semibold">
+              {{ item.#if($titleField != "")${titleField}#else ${primaryColumn.javaField}#end }}
+            </view>
+#if($statusField != "")
+            <dict-tag :type="DICT_TYPE.$statusDictType.toUpperCase()" :value="item.$statusField" />
+#end
+          </view>
+#set ($infoCount = 0)
+#foreach($column in $columns)
+  #if ($column.listOperationResult && !$column.primaryKey && $column.javaField != $titleField && $column.javaField != $statusField)
+    #if ($infoCount < 2)
+      <view class="mb-12rpx flex items-center text-28rpx text-[#666]">
+        <text class="mr-8rpx text-[#999]">${column.columnComment}:</text>
+        <text class="line-clamp-1">{{ item.${column.javaField} }}</text>
+      </view>
+      #set ($infoCount = $infoCount + 1)
+    #end
+  #end
+#end
+        </view>
+      </view>
+
+      <!-- 加载更多 -->
+      <view v-if="loadMoreState !== 'loading' && list.length === 0" class="py-100rpx text-center">
+        <wd-status-tip image="content" tip="暂无${table.classComment}数据" />
+      </view>
+      <wd-loadmore
+        v-if="list.length > 0"
+        :state="loadMoreState"
+        @reload="loadMore"
+      />
+    </view>
+
+    <!-- 新增按钮 -->
+    <wd-fab
+      v-if="hasAccessByCodes(['${permissionPrefix}:create'])"
+      position="right-bottom"
+      type="primary"
+      :expandable="false"
+      @click="handleAdd"
+    />
+  </view>
+</template>
+
+<script lang="ts" setup>
+import type { SearchFormData } from './components/search-form.vue'
+import type { ${simpleClassName} } from '@/api/${table.moduleName}/${table.businessName}'
+import type { LoadMoreState } from '@/http/types'
+import { onReachBottom } from '@dcloudio/uni-app'
+import { onMounted, reactive, ref } from 'vue'
+import { get${simpleClassName}Page } from '@/api/${table.moduleName}/${table.businessName}'
+import { useAccess } from '@/hooks/useAccess'
+import { navigateBackPlus } from '@/utils'
+import SearchForm from './components/search-form.vue'
+#set ($hasDict = 0)
+#foreach($column in $columns)
+  #if ($hasDict == 0 && $column.listOperationResult && $column.dictType && "" != $column.dictType)
+    #set ($hasDict = 1)
+  #end
+#end
+#if ($hasDict == 1)
+import { DICT_TYPE } from '@/utils/constants'
+#end
+
+definePage({
+  style: {
+    navigationBarTitleText: '',
+    navigationStyle: 'custom',
+  },
+})
+
+const { hasAccessByCodes } = useAccess()
+const total = ref(0)
+const list = ref<${simpleClassName}[]>([])
+const loadMoreState = ref<LoadMoreState>('loading') // 加载更多状态
+
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+#foreach($column in $columns)
+  #if ($column.listOperation)
+    #set ($javaType = $column.javaType.toLowerCase())
+    #if ($column.dictType && "" != $column.dictType)
+      #if(${javaType} == "string")
+  ${column.javaField}: -1 as number | string, // -1 表示全部
+      #elseif(${javaType} == "boolean")
+  ${column.javaField}: -1 as number | boolean, // -1 表示全部
+      #else
+  ${column.javaField}: -1 as number, // -1 表示全部
+      #end
+    #elseif(${javaType} == "long" || ${javaType} == "integer" || ${javaType} == "short" || ${javaType} == "double" || ${javaType} == "bigdecimal" || ${javaType} == "byte")
+  ${column.javaField}: undefined as number | undefined,
+    #elseif(${javaType} == "boolean")
+  ${column.javaField}: undefined as boolean | undefined,
+    #else
+  ${column.javaField}: undefined as string | undefined,
+    #end
+  #end
+#end
+})
+
+/** 返回上一页 */
+function handleBack() {
+  navigateBackPlus()
+}
+
+/** 查询${table.classComment}列表 */
+async function getList() {
+  loadMoreState.value = 'loading'
+  try {
+    const data = await get${simpleClassName}Page({
+      ...queryParams,
+#foreach($column in $columns)
+  #if ($column.listOperation && $column.dictType && "" != $column.dictType)
+      ${column.javaField}: queryParams.${column.javaField} === -1 ? undefined : queryParams.${column.javaField},
+  #end
+#end
+    })
+    list.value = [...list.value, ...data.list]
+    total.value = data.total
+    loadMoreState.value = list.value.length >= total.value ? 'finished' : 'loading'
+  } catch {
+    queryParams.pageNo = queryParams.pageNo > 1 ? queryParams.pageNo - 1 : 1
+    loadMoreState.value = 'error'
+  }
+}
+
+/** 搜索按钮操作 */
+function handleQuery(data?: SearchFormData) {
+#foreach($column in $columns)
+  #if ($column.listOperation)
+  queryParams.${column.javaField} = data?.${column.javaField}#if ($column.dictType && "" != $column.dictType) ?? -1#end
+  #end
+#end
+  queryParams.pageNo = 1
+  list.value = []
+  getList()
+}
+
+/** 重置按钮操作 */
+function handleReset() {
+  handleQuery()
+}
+
+/** 加载更多 */
+function loadMore() {
+  if (loadMoreState.value === 'finished') {
+    return
+  }
+  queryParams.pageNo++
+  getList()
+}
+
+/** 新增${table.classComment} */
+function handleAdd() {
+  uni.navigateTo({
+    url: '/pages-${table.moduleName}/${table.businessName}/form/index',
+  })
+}
+
+/** 查看详情 */
+function handleDetail(item: ${simpleClassName}) {
+  uni.navigateTo({
+    url: '/pages-${table.moduleName}/${table.businessName}/detail/index?id=' + item.${primaryColumn.javaField},
+  })
+}
+
+/** 触底加载更多 */
+onReachBottom(() => {
+  loadMore()
+})
+
+/** 初始化 */
+onMounted(() => {
+  getList()
+})
+</script>
+
+<style lang="scss" scoped>
+</style>