瀏覽代碼

feat(web-admin): add mp plan form anchors and polish print customer pick.

Improve the page-variant plan form for WebView and align print customer selection with the mobile demo.

Co-authored-by: Cursor <cursoragent@cursor.com>
wuzilong 1 月之前
父節點
當前提交
ca0098f6ca
共有 2 個文件被更改,包括 163 次插入 和 23 次删除
  1. 68 7
      web-admin/src/components/business/pl-plan-form-dialog/index.vue
  2. 95 16
      web-admin/src/views/m/pl/plan/print.vue

+ 68 - 7
web-admin/src/components/business/pl-plan-form-dialog/index.vue

@@ -8,8 +8,20 @@
     @update:model-value="onFormShellVisible"
   >
     <h2 v-if="isPageVariant" class="mp-plan-form-title">{{ dialogTitle }}</h2>
+    <nav v-if="isPageVariant" class="mp-plan-anchor-bar">
+      <button
+        v-for="item in mpAnchors"
+        :key="item.id"
+        type="button"
+        class="mp-plan-anchor"
+        :class="{ active: mpActiveAnchor === item.id }"
+        @click="scrollMpAnchor(item.id)"
+      >
+        {{ item.label }}
+      </button>
+    </nav>
     <div class="pl-plan-form-body">
-      <section class="pl-plan-form-section">
+      <section id="sec-basic" class="pl-plan-form-section">
         <h3 class="pl-plan-form-section-title">基础信息</h3>
         <el-form
           ref="basicFormRef"
@@ -66,7 +78,7 @@
         </el-form>
       </section>
 
-      <section class="pl-plan-form-section">
+      <section id="sec-transport" class="pl-plan-form-section">
         <h3 class="pl-plan-form-section-title">运输信息</h3>
         <el-form
           :model="formData"
@@ -190,8 +202,9 @@
         </el-form>
       </section>
 
-      <section class="pl-plan-form-section">
+      <section id="sec-load" class="pl-plan-form-section">
         <h3 class="pl-plan-form-section-title">装送货信息</h3>
+        <div id="sec-delivery" class="mp-plan-anchor-target" aria-hidden="true" />
         <el-form :label-position="formLabelPosition" label-width="100px" class="pl-dialog-form">
           <PlPlanStopsSplitShell
             :plan-total-qty="planTotalQty"
@@ -261,7 +274,7 @@
         </el-form>
       </section>
 
-      <section class="pl-plan-form-section">
+      <section id="sec-remark" class="pl-plan-form-section">
         <h3 class="pl-plan-form-section-title">备注</h3>
         <el-input v-model="formData.remark" type="textarea" :rows="3" placeholder="请输入备注" />
       </section>
@@ -302,7 +315,7 @@
         </el-button>
       </div>
     </template>
-    <div v-else class="mp-plan-form-footer dialog-footer">
+    <div v-if="isPageVariant" class="mp-plan-form-footer dialog-footer">
       <el-button @click="handleCancel">
         <el-icon><Close /></el-icon>取消
       </el-button>
@@ -541,6 +554,19 @@
   const isCopy = computed(() => !props.planId && !!props.copyFromPlanId)
   const isPageVariant = computed(() => props.variant === 'page')
   const formLabelPosition = computed(() => (isPageVariant.value ? 'top' : 'right'))
+  const mpAnchors = [
+    { id: 'sec-basic', label: '基础' },
+    { id: 'sec-transport', label: '运输' },
+    { id: 'sec-load', label: '装货' },
+    { id: 'sec-delivery', label: '送货' },
+    { id: 'sec-remark', label: '备注' }
+  ]
+  const mpActiveAnchor = ref('sec-basic')
+
+  function scrollMpAnchor(id: string) {
+    mpActiveAnchor.value = id
+    document.getElementById(id)?.scrollIntoView({ behavior: 'smooth', block: 'start' })
+  }
   const dialogTitle = computed(() => (isEdit.value ? '编辑运输计划' : '新增运输计划'))
 
   const innerVisible = computed({
@@ -2406,6 +2432,39 @@
     color: #087f57;
   }
 
+  .mp-plan-anchor-bar {
+    display: flex;
+    gap: 6px;
+    overflow-x: auto;
+    margin: 0 0 12px;
+    padding-bottom: 2px;
+    -webkit-overflow-scrolling: touch;
+  }
+
+  .mp-plan-anchor {
+    flex: 0 0 auto;
+    height: 30px;
+    padding: 0 12px;
+    border: 1px solid #dcebe4;
+    border-radius: 999px;
+    background: #fff;
+    font-size: 12px;
+    color: #6b7b74;
+    cursor: pointer;
+  }
+
+  .mp-plan-anchor.active {
+    color: #fff;
+    background: #0a9f6d;
+    border-color: #0a9f6d;
+    font-weight: 600;
+  }
+
+  .mp-plan-anchor-target {
+    height: 0;
+    overflow: hidden;
+  }
+
   .mp-plan-form-page {
     :deep(.form-grid-5),
     :deep(.form-grid-4),
@@ -2440,8 +2499,10 @@
     display: flex;
     flex-wrap: wrap;
     gap: 8px;
-    padding: 12px 0 calc(8px + env(safe-area-inset-bottom));
-    background: #f5f8f7;
+    margin: 0 -4px;
+    padding: 10px 4px calc(10px + env(safe-area-inset-bottom));
+    background: #fff;
+    border-top: 1px solid #dcebe4;
   }
 
   .pl-plan-transport-row {

+ 95 - 16
web-admin/src/views/m/pl/plan/print.vue

@@ -8,9 +8,10 @@
           <button class="mp-wv-header__back" type="button" @click="goBack">← 详情</button>
         </div>
         <h1>打印单据</h1>
+        <div class="mp-wv-header__sub">长按预览区可存图(WebView 不支持系统打印)</div>
       </header>
 
-      <p class="mp-print-hint">长按预览区可保存图片;WebView 不支持系统打印。</p>
+      <p class="mp-print-hint">WebView 不调用系统打印机,引导长按存图。</p>
 
       <div class="mp-wv-chips">
         <button
@@ -25,33 +26,50 @@
         </button>
       </div>
 
-      <div v-if="kind === 'delivery' && customers.length > 1" class="mp-wv-toolbar">
-        <el-select
-          v-model="customerId"
-          placeholder="选择送货客户"
-          style="width: 100%"
-          @change="reloadPreview"
-        >
-          <el-option
-            v-for="item in customers"
-            :key="item.customerId"
-            :label="item.customerName"
-            :value="item.customerId"
-          />
-        </el-select>
+      <div v-if="needCustomer" class="mp-wv-section" style="margin-top: 0">
+        <h3>送货客户</h3>
+        <button class="mp-wv-picker" type="button" @click="customerSheetOpen = true">
+          <span :class="{ 'is-ph': !customerLabel }">{{ customerLabel || '请选择送货客户' }}</span>
+          <span class="mp-wv-picker__chev">选择</span>
+        </button>
       </div>
 
       <div v-if="loading" class="mp-wv-empty">正在生成单据…</div>
       <div v-else-if="blockReason" class="mp-wv-empty">{{ blockReason }}</div>
       <div v-else class="mp-print-preview" v-html="html"></div>
+
+      <div class="mp-wv-fab-bar">
+        <button class="mp-wv-btn" type="button" @click="goBack">返回详情</button>
+        <button class="mp-wv-btn mp-wv-btn--primary" type="button" @click="hintSave">
+          保存预览
+        </button>
+      </div>
+
+      <MpBottomSheet v-model="customerSheetOpen" title="选择送货客户" searchable search-placeholder="搜索客户">
+        <template #default="{ keyword }">
+          <button
+            v-for="item in filteredCustomers(keyword)"
+            :key="item.customerId"
+            class="mp-wv-filter-opt"
+            type="button"
+            @click="pickCustomer(item)"
+          >
+            <span>{{ item.customerName }}</span>
+            <span v-if="item.customerId === customerId">✓</span>
+          </button>
+          <div v-if="!filteredCustomers(keyword).length" class="mp-wv-empty">无匹配客户</div>
+        </template>
+      </MpBottomSheet>
     </template>
   </MpWebviewShell>
 </template>
 
 <script setup lang="ts">
-  import { onMounted, ref } from 'vue'
+  import { computed, onMounted, ref } from 'vue'
   import { useRoute, useRouter } from 'vue-router'
+  import { ElMessage } from 'element-plus'
   import MpWebviewShell from '@/layouts/MpWebviewShell.vue'
+  import MpBottomSheet from '@/components/mp/MpBottomSheet.vue'
   import { planApi, type Plan } from '@/api/pl'
   import { canBatchPrintSlip } from '@/utils/pl-batch-print'
   import { listDeliveryPrintCustomers } from '@/utils/pl-delivery-slip-print'
@@ -80,6 +98,20 @@
   const blockReason = ref('')
   const customerId = ref('')
   const customers = ref<{ customerId: string; customerName: string }[]>([])
+  const customerSheetOpen = ref(false)
+
+  const needCustomer = computed(
+    () => kind.value === 'delivery' || kind.value === 'blank-delivery'
+  )
+  const customerLabel = computed(
+    () => customers.value.find((c) => c.customerId === customerId.value)?.customerName || ''
+  )
+
+  function filteredCustomers(keyword: string) {
+    const kw = (keyword || '').trim().toLowerCase()
+    if (!kw) return customers.value
+    return customers.value.filter((c) => c.customerName.toLowerCase().includes(kw))
+  }
 
   function goBack() {
     if (window.history.length > 1) {
@@ -90,6 +122,10 @@
     void router.replace(id ? `/m/pl/plan/${id}` : '/m/pl/plan')
   }
 
+  function hintSave() {
+    ElMessage.info('请长按预览区保存图片')
+  }
+
   function slipCommand(current: MpPlanPrintKind) {
     if (current === 'inbound' || current === 'delivery' || current === 'subpack') return current
     return null
@@ -97,6 +133,11 @@
 
   async function reloadPreview() {
     const current = kind.value
+    if (needCustomer.value && !customerId.value) {
+      html.value = ''
+      blockReason.value = '请先选择送货客户'
+      return
+    }
     const command = slipCommand(current)
     if (command && plan.value) {
       const check = canBatchPrintSlip([plan.value], command)
@@ -125,6 +166,12 @@
     void reloadPreview()
   }
 
+  function pickCustomer(item: { customerId: string; customerName: string }) {
+    customerId.value = item.customerId
+    customerSheetOpen.value = false
+    void reloadPreview()
+  }
+
   onMounted(async () => {
     const id = String(route.params.id || '')
     if (!id) {
@@ -146,3 +193,35 @@
     }
   })
 </script>
+
+<style scoped lang="scss">
+  .mp-wv-header__sub {
+    margin-top: 4px;
+    font-size: 12px;
+    color: rgba(255, 255, 255, 0.9);
+  }
+
+  .mp-wv-picker {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    width: 100%;
+    height: 40px;
+    padding: 0 12px;
+    border: 1px solid #dcebe4;
+    border-radius: 8px;
+    background: #fff;
+    font-size: 14px;
+    color: #1f2a26;
+    cursor: pointer;
+  }
+
+  .mp-wv-picker .is-ph {
+    color: #6b7b74;
+  }
+
+  .mp-wv-picker__chev {
+    color: #0a9f6d;
+    font-size: 12px;
+  }
+</style>