Explorar el Código

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

Co-authored-by: Cursor <cursoragent@cursor.com>
wuzilong hace 2 semanas
padre
commit
b9e44bd697

+ 181 - 69
web-admin/src/views/crm/quote/components/QuoteFormDialog.vue

@@ -1,5 +1,6 @@
 <template>
   <el-dialog
+    v-if="!isPageVariant"
     v-model="visible"
     :title="dialogTitle"
     width="1680px"
@@ -33,81 +34,192 @@
       </div>
     </template>
   </el-dialog>
+
+  <div v-else class="mp-quote-form-page">
+    <QuoteFormPanel
+      ref="panelRef"
+      variant="page"
+      hide-footer
+      :auto-init="false"
+      @success="handleSuccess"
+      @close="emit('close')"
+    />
+    <div class="mp-plan-form-footer">
+      <el-button class="btn-ghost" @click="emit('close')">
+        <el-icon><Close /></el-icon>取消
+      </el-button>
+      <el-button class="btn-ghost" :loading="saveLoading" @click="handleSaveDraft">
+        <el-icon><DocumentCopy /></el-icon>保存草稿
+      </el-button>
+      <el-button class="btn-main" type="primary" @click="handleSubmit">
+        <el-icon><CircleCheck /></el-icon>提交审批
+      </el-button>
+    </div>
+  </div>
 </template>
 
 <script setup lang="ts">
-import { computed, nextTick, ref } from 'vue'
-import { CircleCheck, Close, DocumentCopy } from '@element-plus/icons-vue'
-import QuoteFormPanel from './QuoteFormPanel.vue'
-
-const emit = defineEmits<{
-  success: []
-}>()
-
-const visible = ref(false)
-const dialogMode = ref<'add' | 'edit' | 'copy'>('add')
-const editId = ref<string | number>('')
-const copyId = ref<string | number>('')
-const panelRef = ref<InstanceType<typeof QuoteFormPanel> | null>(null)
-const saveLoading = computed(() => Boolean(panelRef.value?.saveLoading))
-const dialogTitle = computed(() => {
-  if (dialogMode.value === 'edit') return '编辑报价单'
-  if (dialogMode.value === 'copy') return '复制新增报价单'
-  return '新增报价单'
-})
-
-const handleOpened = async () => {
-  await nextTick()
-  if (dialogMode.value === 'edit' && editId.value) {
-    await panelRef.value?.initEdit(editId.value)
-    return
+  import { computed, nextTick, onMounted, ref, watch } from 'vue'
+  import { CircleCheck, Close, DocumentCopy } from '@element-plus/icons-vue'
+  import QuoteFormPanel from './QuoteFormPanel.vue'
+
+  const props = withDefaults(
+    defineProps<{
+      modelValue?: boolean
+      variant?: 'dialog' | 'page'
+      pageMode?: 'add' | 'edit' | 'copy'
+      quoteId?: string | number | null
+      copyFromId?: string | number | null
+    }>(),
+    {
+      modelValue: false,
+      variant: 'dialog',
+      pageMode: 'add',
+      quoteId: null,
+      copyFromId: null
+    }
+  )
+
+  const emit = defineEmits<{
+    success: []
+    close: []
+    'update:modelValue': [value: boolean]
+    'loading-change': [loading: boolean]
+  }>()
+
+  const isPageVariant = computed(() => props.variant === 'page')
+  const visible = ref(false)
+  const dialogMode = ref<'add' | 'edit' | 'copy'>('add')
+  const editId = ref<string | number>('')
+  const copyId = ref<string | number>('')
+  const panelRef = ref<InstanceType<typeof QuoteFormPanel> | null>(null)
+  const pageBooting = ref(false)
+
+  const saveLoading = computed(() => Boolean(panelRef.value?.saveLoading))
+  const dialogTitle = computed(() => {
+    if (dialogMode.value === 'edit') return '编辑报价单'
+    if (dialogMode.value === 'copy') return '复制新增报价单'
+    return '新增报价单'
+  })
+
+  const handleOpened = async () => {
+    await nextTick()
+    if (dialogMode.value === 'edit' && editId.value) {
+      await panelRef.value?.initEdit(editId.value)
+      return
+    }
+    if (dialogMode.value === 'copy' && copyId.value) {
+      await panelRef.value?.initCopy(copyId.value)
+      return
+    }
+    await panelRef.value?.initNew()
+  }
+
+  const handleSaveDraft = () => {
+    panelRef.value?.handleSaveDraft()
+  }
+
+  const handleSubmit = () => {
+    panelRef.value?.handleSubmit()
+  }
+
+  const handleSuccess = () => {
+    if (!isPageVariant.value) {
+      visible.value = false
+    }
+    emit('success')
   }
-  if (dialogMode.value === 'copy' && copyId.value) {
-    await panelRef.value?.initCopy(copyId.value)
-    return
+
+  async function bootPageMode() {
+    pageBooting.value = true
+    emit('loading-change', true)
+    await nextTick()
+    try {
+      if (props.pageMode === 'edit' && props.quoteId) {
+        await panelRef.value?.initEdit(props.quoteId)
+      } else if (props.pageMode === 'copy' && props.copyFromId) {
+        await panelRef.value?.initCopy(props.copyFromId)
+      } else {
+        await panelRef.value?.initNew()
+      }
+    } finally {
+      pageBooting.value = false
+      emit('loading-change', false)
+    }
   }
-  await panelRef.value?.initNew()
-}
-
-const handleSaveDraft = () => {
-  panelRef.value?.handleSaveDraft()
-}
-
-const handleSubmit = () => {
-  panelRef.value?.handleSubmit()
-}
-
-const handleSuccess = () => {
-  visible.value = false
-  emit('success')
-}
-
-const open = () => {
-  dialogMode.value = 'add'
-  editId.value = ''
-  copyId.value = ''
-  visible.value = true
-}
-
-const openEdit = (id: string | number) => {
-  dialogMode.value = 'edit'
-  editId.value = id
-  copyId.value = ''
-  visible.value = true
-}
-
-const openCopy = (id: string | number) => {
-  dialogMode.value = 'copy'
-  copyId.value = id
-  editId.value = ''
-  visible.value = true
-}
-
-defineExpose({ open, openEdit, openCopy })
+
+  watch(
+    () =>
+      [
+        isPageVariant.value,
+        props.pageMode,
+        props.quoteId,
+        props.copyFromId
+      ] as const,
+    () => {
+      if (isPageVariant.value) {
+        void bootPageMode()
+      }
+    }
+  )
+
+  onMounted(() => {
+    if (isPageVariant.value) {
+      void bootPageMode()
+    }
+  })
+
+  const open = () => {
+    dialogMode.value = 'add'
+    editId.value = ''
+    copyId.value = ''
+    visible.value = true
+  }
+
+  const openEdit = (id: string | number) => {
+    dialogMode.value = 'edit'
+    editId.value = id
+    copyId.value = ''
+    visible.value = true
+  }
+
+  const openCopy = (id: string | number) => {
+    dialogMode.value = 'copy'
+    copyId.value = id
+    editId.value = ''
+    visible.value = true
+  }
+
+  defineExpose({ open, openEdit, openCopy })
 </script>
 
 <style scoped lang="scss">
-:deep(.quote-form-dialog .el-dialog__body) {
-  padding-top: 8px;
-}
+  :deep(.quote-form-dialog .el-dialog__body) {
+    padding-top: 8px;
+  }
+
+  .mp-quote-form-page {
+    display: flex;
+    flex-direction: column;
+    min-height: calc(100vh - 120px);
+    padding: 0 12px 72px;
+  }
+
+  .mp-plan-form-footer {
+    position: sticky;
+    bottom: 0;
+    z-index: 12;
+    display: flex;
+    flex-wrap: wrap;
+    gap: 8px;
+    margin: 0 -12px;
+    padding: 10px 12px calc(10px + env(safe-area-inset-bottom, 0px));
+    background: #fff;
+    border-top: 1px solid #dcebe4;
+
+    .el-button {
+      flex: 1 1 0;
+      min-width: 0;
+    }
+  }
 </style>

+ 35 - 4
web-admin/src/views/crm/quote/components/QuoteFormPanel.vue

@@ -1,11 +1,11 @@
 <template>
-  <div class="quote-form-panel">
+  <div class="quote-form-panel" :class="{ 'quote-form-panel--page': isPageVariant }">
       <el-form
         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 quote-form-body"
         scroll-to-error
         :scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
@@ -672,13 +672,18 @@ const props = withDefaults(
   defineProps<{
     hideFooter?: boolean
     autoInit?: boolean
+    /** 小程序 WebView 全屏页:单列布局 */
+    variant?: 'dialog' | 'page'
   }>(),
   {
     hideFooter: false,
-    autoInit: true
+    autoInit: true,
+    variant: 'dialog'
   }
 )
 
+const isPageVariant = computed(() => props.variant === 'page')
+
 const emit = defineEmits<{
   success: []
   close: []
@@ -2417,6 +2422,32 @@ onMounted(async () => {
 </script>
 
 <style scoped lang="scss">
+.quote-form-panel--page {
+  .quote-form-body {
+    :deep(.form-grid-4),
+    :deep(.form-grid-2) {
+      display: grid;
+      grid-template-columns: 1fr;
+      gap: 12px;
+    }
+
+    :deep(.form-col-span-2),
+    :deep([class*='form-col-span']) {
+      grid-column: 1 / -1;
+    }
+  }
+
+  :deep(.dialog-list-panel),
+  :deep(.list-body),
+  :deep(.el-table) {
+    max-width: 100%;
+  }
+
+  :deep(.dialog-list-panel) {
+    overflow-x: auto;
+  }
+}
+
 .quote-form-body {
   :deep(.el-form-item__label) {
     white-space: nowrap;

+ 102 - 4
web-admin/src/views/m/crm/quote/form.vue

@@ -2,16 +2,114 @@
   <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"
+      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>
+    <QuoteFormDialog
+      v-show="!contentLoading"
+      :key="formKey"
+      variant="page"
+      :model-value="true"
+      :page-mode="pageMode"
+      :quote-id="quoteId"
+      :copy-from-id="copyFromId"
+      @loading-change="onLoadingChange"
+      @success="onSuccess"
+      @close="goList"
+    />
   </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 QuoteFormDialog from '@/views/crm/quote/components/QuoteFormDialog.vue'
+  import { MP_CRM_QUOTE_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 pageMode = computed<'add' | 'edit' | 'copy'>(() => {
+    if (route.name === 'MpCrmQuoteEdit') 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 quoteId = 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}-${quoteId.value || ''}-${copyFromId.value || ''}`
+  )
+
+  const loadingCopy = computed(() => {
+    if (pageMode.value === 'edit') {
+      return { title: '正在加载报价单', desc: '请稍候,正在获取报价单详情…' }
+    }
+    if (pageMode.value === 'copy') {
+      return { title: '正在复制报价单', desc: '请稍候,正在预填原单数据…' }
+    }
+    return { title: '正在准备表单', desc: '请稍候,正在初始化主数据…' }
+  })
+
+  watch(formKey, () => {
+    contentLoading.value = true
+  })
+
+  function onLoadingChange(loading: boolean) {
+    contentLoading.value = loading
+  }
+
+  function goList() {
+    void router.replace(MP_CRM_QUOTE_LIST_PATH)
+  }
+
+  function onSuccess() {
+    if (pageMode.value === 'edit' && quoteId.value) {
+      void router.replace(`/m/crm/quote/${quoteId.value}`)
+      return
+    }
+    goList()
+  }
 </script>