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

feat(web-admin): add page variant for contract form in mp webview.

Co-authored-by: Cursor <cursoragent@cursor.com>
wuzilong пре 2 недеља
родитељ
комит
a33c2d1f2e

+ 81 - 14
web-admin/src/views/crm/contract/components/ContractFormDialog.vue

@@ -1,13 +1,20 @@
 <template>
   <el-dialog
     v-model="dialogVisible"
-    :title="formTitle"
-    width="calc(100vw - 48px)"
+    :title="isPageVariant ? undefined : formTitle"
+    :width="isPageVariant ? '100%' : 'calc(100vw - 48px)'"
+    :fullscreen="isPageVariant"
+    :show-close="!isPageVariant"
+    :modal="!isPageVariant"
+    :append-to-body="!isPageVariant"
     align-center
-    class="dialog-form-layout contract-form-dialog"
+    :class="[
+      'dialog-form-layout',
+      'contract-form-dialog',
+      { 'contract-form-dialog--page': isPageVariant }
+    ]"
     destroy-on-close
-    append-to-body
-    modal-class="order-module-dialog-mask"
+    :modal-class="isPageVariant ? undefined : 'order-module-dialog-mask'"
     :close-on-click-modal="false"
     @opened="handleOpened"
   >
@@ -20,9 +27,10 @@
         ref="formRef"
         :model="form"
         :rules="rules"
-        label-position="right"
-        label-width="120px"
+        :label-position="isPageVariant ? 'top' : 'right'"
+        :label-width="isPageVariant ? undefined : '120px'"
         class="crm-dialog-form"
+        :class="{ 'crm-dialog-form--page': isPageVariant }"
         :disabled="formLoading"
       >
         <!-- 合同基础信息(一行 4 列) -->
@@ -946,18 +954,29 @@ type ExtendedContractProductForm = ContractProductForm & {
   priceTiers?: PriceTierRow[]
 }
 
-const props = defineProps<{
-  visible: boolean
-  editId: string | null
-  /** 复制来源合同(列表选中行,含 products) */
-  copySource?: ContractVO | null
-}>()
+const props = withDefaults(
+  defineProps<{
+    visible: boolean
+    editId: string | null
+    /** 复制来源合同(列表选中行,含 products) */
+    copySource?: ContractVO | null
+    /** dialog=桌面弹窗;page=小程序 WebView 全屏页 */
+    variant?: 'dialog' | 'page'
+  }>(),
+  {
+    copySource: null,
+    variant: 'dialog'
+  }
+)
 
 const emit = defineEmits<{
   'update:visible': [value: boolean]
   success: []
+  'loading-change': [loading: boolean]
 }>()
 
+const isPageVariant = computed(() => props.variant === 'page')
+
 const dialogVisible = computed({
   get: () => props.visible,
   set: (val) => emit('update:visible', val)
@@ -1098,12 +1117,18 @@ watch(
   (visible) => {
     if (visible && (props.editId || props.copySource)) {
       formLoading.value = true
+      emit('loading-change', true)
     } else if (!visible) {
       formLoading.value = false
+      emit('loading-change', false)
     }
   }
 )
 
+watch(formLoading, (loading) => {
+  if (props.visible) emit('loading-change', loading)
+})
+
 // 组件引用
 const formRef = ref<FormInstance>()
 const approvalDialogRef = ref()
@@ -1368,7 +1393,7 @@ const handleOpened = async () => {
   resetForm()
   const isEdit = !!props.editId
   const isCopy = !!props.copySource
-  if (isEdit || isCopy) {
+  if (isEdit || isCopy || isPageVariant.value) {
     formLoading.value = true
   }
   try {
@@ -3024,6 +3049,32 @@ const loadSupplierList = async () => {
     flex-direction: column;
   }
 
+  &.contract-form-dialog--page {
+    :deep(.el-dialog__wrapper) {
+      top: 0 !important;
+      height: 100% !important;
+    }
+
+    :deep(.el-dialog) {
+      max-height: 100%;
+      border-radius: 0;
+      box-shadow: none;
+    }
+
+    :deep(.el-dialog__header) {
+      display: none;
+    }
+
+    :deep(.el-dialog__body) {
+      padding: 8px 12px 16px;
+    }
+
+    :deep(.el-dialog__footer) {
+      padding: 10px 12px calc(10px + env(safe-area-inset-bottom));
+      border-top: 1px solid #e2e8f0;
+    }
+  }
+
   :deep(.dialog-footer) {
     display: flex;
     justify-content: center;
@@ -3053,6 +3104,22 @@ const loadSupplierList = async () => {
   padding: 0 4px;
 }
 
+.crm-dialog-form--page {
+  .contract-basic-info {
+    grid-template-columns: 1fr;
+  }
+
+  :deep(.el-row) {
+    display: flex;
+    flex-direction: column;
+  }
+
+  :deep(.el-col) {
+    width: 100% !important;
+    max-width: 100% !important;
+  }
+}
+
 .contract-basic-info {
   display: grid;
   grid-template-columns: repeat(4, minmax(0, 1fr));

+ 147 - 4
web-admin/src/views/m/crm/contract/form.vue

@@ -2,16 +2,159 @@
   <MpWebviewShell>
     <header class="mp-wv-header">
       <div class="mp-wv-header__row">
-        <h1 class="mp-wv-header__title">{{ title }}</h1>
+        <button class="mp-wv-header__back" type="button" aria-label="返回列表" @click="goList">
+          ←
+        </button>
+        <h1 class="mp-wv-header__title">{{ pageTitle }}</h1>
       </div>
     </header>
+    <div
+      v-if="contentLoading || bootLoading"
+      class="mp-plan-form-loading mp-plan-form-loading--page"
+      role="status"
+      aria-live="polite"
+      aria-busy="true"
+    >
+      <div class="mp-plan-form-loading__orb" aria-hidden="true">
+        <span class="mp-plan-form-loading__dot" />
+      </div>
+      <p class="mp-plan-form-loading__title">{{ loadingCopy.title }}</p>
+      <p class="mp-plan-form-loading__desc">{{ loadingCopy.desc }}</p>
+      <div class="mp-plan-form-loading__skel" aria-hidden="true">
+        <div class="mp-wv-skel mp-wv-skel--block" />
+        <div class="mp-wv-skel mp-wv-skel--mid" />
+        <div class="mp-wv-skel mp-wv-skel--line" />
+        <div class="mp-wv-skel mp-wv-skel--short" />
+      </div>
+    </div>
+    <div v-else-if="bootError" class="mp-wv-empty">{{ bootError }}</div>
+    <ContractFormDialog
+      v-if="formReady"
+      v-show="!contentLoading"
+      :key="formKey"
+      variant="page"
+      :visible="true"
+      :edit-id="editId"
+      :copy-source="copySource"
+      @loading-change="onLoadingChange"
+      @success="onSuccess"
+      @update:visible="onVisibleChange"
+    />
   </MpWebviewShell>
 </template>
+
 <script setup lang="ts">
-  import { computed } from 'vue'
-  import { useRoute } from 'vue-router'
+  import { computed, ref, watch } from 'vue'
+  import { useRoute, useRouter } from 'vue-router'
   import MpWebviewShell from '@/layouts/MpWebviewShell.vue'
+  import ContractFormDialog from '@/views/crm/contract/components/ContractFormDialog.vue'
+  import { getContract, type ContractVO } from '@/api/crm/contract'
+  import { MP_CRM_CONTRACT_LIST_PATH } from '@/utils/mp-webview'
   import '@/styles/mp-webview.scss'
+
   const route = useRoute()
-  const title = computed(() => String(route.meta.title || ''))
+  const router = useRouter()
+  const contentLoading = ref(true)
+  const bootLoading = ref(false)
+  const bootError = ref('')
+  const formReady = ref(false)
+  const copySource = ref<ContractVO | null>(null)
+
+  const pageMode = computed<'add' | 'edit' | 'copy'>(() => {
+    if (route.name === 'MpCrmContractEdit') return 'edit'
+    const copyFrom = String(route.query.copyFrom || '').trim()
+    if (copyFrom) return 'copy'
+    return 'add'
+  })
+
+  const pageTitle = computed(() => {
+    if (pageMode.value === 'copy') return '复制合同'
+    if (pageMode.value === 'edit') return '编辑合同'
+    return String(route.meta.title || '新增合同')
+  })
+
+  const editId = computed(() => {
+    if (pageMode.value !== 'edit') return null
+    const id = String(route.params.id || '').trim()
+    return id || null
+  })
+
+  const copyFromId = computed(() => {
+    if (pageMode.value !== 'copy') return null
+    const id = String(route.query.copyFrom || '').trim()
+    return id || null
+  })
+
+  const formKey = computed(
+    () =>
+      `${String(route.name || '')}-${pageMode.value}-${editId.value || ''}-${copyFromId.value || ''}`
+  )
+
+  const loadingCopy = computed(() => {
+    if (pageMode.value === 'edit') {
+      return { title: '正在加载合同', desc: '请稍候,正在获取合同详情…' }
+    }
+    if (pageMode.value === 'copy') {
+      return { title: '正在复制合同', desc: '请稍候,正在预填原单数据…' }
+    }
+    return { title: '正在准备表单', desc: '请稍候,正在初始化主数据…' }
+  })
+
+  async function bootForm() {
+    formReady.value = false
+    bootError.value = ''
+    copySource.value = null
+    contentLoading.value = true
+
+    if (pageMode.value === 'copy' && copyFromId.value) {
+      bootLoading.value = true
+      try {
+        const res = await getContract(copyFromId.value)
+        copySource.value = ((res as any)?.data ?? res) as ContractVO
+        formReady.value = true
+      } catch (error) {
+        bootError.value = error instanceof Error ? error.message : '加载复制来源失败'
+      } finally {
+        bootLoading.value = false
+      }
+      return
+    }
+
+    formReady.value = true
+  }
+
+  watch(
+    formKey,
+    () => {
+      void bootForm()
+    },
+    { immediate: true }
+  )
+
+  function onLoadingChange(loading: boolean) {
+    contentLoading.value = loading
+  }
+
+  function goList() {
+    void router.replace(MP_CRM_CONTRACT_LIST_PATH)
+  }
+
+  function onVisibleChange(visible: boolean) {
+    if (!visible) goList()
+  }
+
+  function onSuccess() {
+    if (pageMode.value === 'edit' && editId.value) {
+      void router.replace(`/m/crm/contract/${editId.value}`)
+      return
+    }
+    goList()
+  }
 </script>
+
+<style scoped lang="scss">
+  :deep(.contract-form-dialog--page) {
+    position: relative;
+    z-index: 1;
+  }
+</style>