瀏覽代碼

feat(crm): wire alias/brand manage dialog into quote product lines.

Co-authored-by: Cursor <cursoragent@cursor.com>
zlwu 2 月之前
父節點
當前提交
a80a01b37d
共有 1 個文件被更改,包括 129 次插入 和 72 次删除
  1. 129 72
      web-admin/src/views/crm/quote/components/QuoteFormPanel.vue

+ 129 - 72
web-admin/src/views/crm/quote/components/QuoteFormPanel.vue

@@ -76,7 +76,12 @@
               style="width: 100%"
               @change="handleOwnerChange"
             >
-              <el-option v-for="user in userList" :key="user.userId" :label="user.nickName" :value="user.userId" />
+              <el-option
+                v-for="user in ownerUserList"
+                :key="user.userId"
+                :label="user.nickName"
+                :value="user.userId"
+              />
             </el-select>
           </el-form-item>
           <!-- 第三行:运费、询价/报价日期、有效期(天) -->
@@ -139,47 +144,69 @@
                 />
               </template>
             </el-table-column>
-            <el-table-column label="别名" min-width="100">
+            <el-table-column label="别名" min-width="140">
               <template #default="scope">
-                <el-select
-                  v-if="(scope.row.aliasOptions || []).length"
-                  v-model="scope.row.aliasName"
-                  placeholder="不选则用产品名称"
-                  clearable
-                  filterable
-                  size="small"
-                  style="width: 100%"
-                >
-                  <el-option
-                    v-for="name in scope.row.aliasOptions"
-                    :key="name"
-                    :label="name"
-                    :value="name"
-                  />
-                </el-select>
-                <span v-else class="text-muted">—</span>
+                <div class="line-meta-cell">
+                  <el-select
+                    v-if="(scope.row.aliasOptions || []).length"
+                    v-model="scope.row.aliasName"
+                    placeholder="不选则用产品名称"
+                    clearable
+                    filterable
+                    size="small"
+                    class="line-meta-cell__control"
+                  >
+                    <el-option
+                      v-for="name in scope.row.aliasOptions"
+                      :key="name"
+                      :label="name"
+                      :value="name"
+                    />
+                  </el-select>
+                  <span v-else class="text-muted line-meta-cell__empty">—</span>
+                  <el-button
+                    v-if="scope.row.productId"
+                    link
+                    type="primary"
+                    class="line-meta-cell__btn"
+                    @click="openAliasBrandManage(scope.row, 'alias')"
+                  >
+                    <el-icon><Setting /></el-icon>
+                  </el-button>
+                </div>
               </template>
             </el-table-column>
-            <el-table-column label="品牌" min-width="100">
+            <el-table-column label="品牌" min-width="140">
               <template #default="scope">
-                <el-select
-                  v-if="(scope.row.brandOptions || []).length"
-                  v-model="scope.row.brandId"
-                  placeholder="不限品牌"
-                  clearable
-                  filterable
-                  size="small"
-                  style="width: 100%"
-                  @change="(val) => handleBrandIdChange(scope.row, val)"
-                >
-                  <el-option
-                    v-for="b in scope.row.brandOptions"
-                    :key="b.id"
-                    :label="b.brandName"
-                    :value="b.id"
-                  />
-                </el-select>
-                <span v-else class="text-muted">—</span>
+                <div class="line-meta-cell">
+                  <el-select
+                    v-if="(scope.row.brandOptions || []).length"
+                    v-model="scope.row.brandId"
+                    placeholder="不限品牌"
+                    clearable
+                    filterable
+                    size="small"
+                    class="line-meta-cell__control"
+                    @change="(val) => handleBrandIdChange(scope.row, val)"
+                  >
+                    <el-option
+                      v-for="b in scope.row.brandOptions"
+                      :key="b.id"
+                      :label="b.brandName"
+                      :value="b.id"
+                    />
+                  </el-select>
+                  <span v-else class="text-muted line-meta-cell__empty">—</span>
+                  <el-button
+                    v-if="scope.row.productId"
+                    link
+                    type="primary"
+                    class="line-meta-cell__btn"
+                    @click="openAliasBrandManage(scope.row, 'brand')"
+                  >
+                    <el-icon><Setting /></el-icon>
+                  </el-button>
+                </div>
               </template>
             </el-table-column>
             <el-table-column label="型号/规格" prop="model" min-width="100" :show-overflow-tooltip="true">
@@ -421,6 +448,14 @@
       @confirm="handleProductConfirm"
     />
 
+    <ProductAliasBrandDialog
+      v-model="aliasBrandManageVisible"
+      :product-id="aliasBrandManageProductId"
+      :product-name="aliasBrandManageProductName"
+      :mode="aliasBrandManageMode"
+      @saved="handleAliasBrandManaged"
+    />
+
     <!-- 客户选择器 -->
     <CustomerSelector :visible="customerSelectorVisible" @update:visible="customerSelectorVisible = $event" @confirm="handleCustomerConfirm" />
 
@@ -513,19 +548,23 @@ import {
   Loading,
   Plus,
   Search,
+  Setting,
   UploadFilled
 } from '@element-plus/icons-vue'
 import ImagePreview from '@/components/ImagePreview/index.vue';
-import ProductSelector from './ProductSelector.vue';
+import ProductSelector from '@/components/business/product-selector/index.vue';
+import ProductAliasBrandDialog from '@/components/business/product-alias-brand-dialog/index.vue'
 import CustomerSelector from '@/components/CustomerSelector.vue';
 import ContactSelector from '@/components/ContactSelector.vue';
 import OpportunitySelector from '@/components/OpportunitySelector.vue';
 import { fetchUserOptions } from '@/api/admin';
 import { fetchContactsByCustomerId } from '@/api/crm/contact';
 import { listAllSupplierInfo, type SupplierInfoVO } from '@/api/crm/supplierInfo';
+import { filterOwnerCandidates, findDefaultOwnerUser } from '@/utils/crm-owner-candidates';
 import { removeProductsNotAllowedByHazard } from '@/utils/crm-supplier-hazard'
 import {
   applyBrandSelectChange,
+  applyManagedProductMetaToLines,
   initLineBrandMetaFromProduct,
   prepareLineAliasAndBrandForSubmit,
   resolveAliasFromSavedName
@@ -690,8 +729,9 @@ const submitRules = ref({
 });
 const submitFormRef = ref<FormInstance>();
 
-// 用户列表(负责人 + 审批人候选)
+// 用户列表(审批人候选用全量;负责人仅方金强 / 吴九华)
 const userList = ref<any[]>([]);
+const ownerUserList = computed(() => filterOwnerCandidates(userList.value));
 
 // 供方信息列表
 const supplierList = ref<SupplierInfoVO[]>([]);
@@ -1035,43 +1075,18 @@ const handleOpportunityConfirm = (opportunity: OpportunityVO) => {
   }
 };
 
-/** 打开用户选择器 */
+/** 负责人变化 */
 const handleOwnerChange = (userId: string | number | undefined) => {
-  const user = userList.value.find((item) => String(item.userId) === String(userId))
+  const user = ownerUserList.value.find((item) => String(item.userId) === String(userId))
   form.value.ownerUserName = user?.nickName || ''
 }
 
-/** 当前登录用户 ID(后端 current-user 返回 id,兼容历史 userId 字段) */
-const resolveCurrentUserId = (): string => {
-  const info = userStore.info || ({} as Api.Auth.UserInfo & Record<string, unknown>)
-  const fromInfo = String(info.id || info.userId || '')
-  if (fromInfo) return fromInfo
-  const token = userStore.accessToken
-  if (!token || token.split('.').length < 2) return ''
-  try {
-    const payload = token.split('.')[1]
-    const b64 = payload.replace(/-/g, '+').replace(/_/g, '/')
-    const pad = '='.repeat((4 - (b64.length % 4)) % 4)
-    const obj = JSON.parse(atob(b64 + pad))
-    return String(obj?.sub || '')
-  } catch {
-    return ''
-  }
-}
-
-/** 新增时默认负责人为当前登录人 */
+/** 新增时默认负责人为方金强 */
 const applyDefaultOwnerUser = () => {
-  const currentUserId = resolveCurrentUserId()
-  if (!currentUserId) return
-  const matched = userList.value.find((item) => String(item.userId) === currentUserId)
-  if (matched) {
-    form.value.ownerUserId = matched.userId as QuoteForm['ownerUserId']
-    form.value.ownerUserName = matched.nickName || ''
-    return
-  }
-  form.value.ownerUserId = currentUserId as QuoteForm['ownerUserId']
-  const info = userStore.info as Record<string, string | undefined>
-  form.value.ownerUserName = info?.realName || info?.userName || info?.username || ''
+  const matched = findDefaultOwnerUser(ownerUserList.value)
+  if (!matched) return
+  form.value.ownerUserId = matched.userId as QuoteForm['ownerUserId']
+  form.value.ownerUserName = matched.nickName || ''
 }
 
 const formatApproverOptionLabel = (user: { nickName?: string; userName?: string }) => {
@@ -1266,6 +1281,29 @@ const handleBrandIdChange = (row: QuoteProductForm, val: string | undefined | nu
   applyBrandSelectChange(row, val, row.brandOptions || [])
 }
 
+const aliasBrandManageVisible = ref(false)
+const aliasBrandManageProductId = ref('')
+const aliasBrandManageProductName = ref('')
+const aliasBrandManageMode = ref<'alias' | 'brand'>('alias')
+
+function openAliasBrandManage(row: QuoteProductForm, mode: 'alias' | 'brand') {
+  if (!row.productId) return
+  aliasBrandManageProductId.value = String(row.productId)
+  aliasBrandManageProductName.value = String(
+    row.standardProductName || row.productName || ''
+  )
+  aliasBrandManageMode.value = mode
+  aliasBrandManageVisible.value = true
+}
+
+function handleAliasBrandManaged(product: Record<string, any>) {
+  applyManagedProductMetaToLines(
+    form.value.products || [],
+    aliasBrandManageProductId.value,
+    product
+  )
+}
+
 async function enrichQuoteProductBrandMeta(lines: QuoteProductForm[]) {
   await Promise.all(
     (lines || []).map(async (line) => {
@@ -2266,4 +2304,23 @@ onMounted(async () => {
   background: transparent !important;
   border: none !important;
 }
+
+.line-meta-cell {
+  display: flex;
+  align-items: center;
+  gap: 4px;
+  min-width: 0;
+}
+.line-meta-cell__control {
+  flex: 1;
+  min-width: 0;
+}
+.line-meta-cell__empty {
+  flex: 1;
+  min-width: 0;
+}
+.line-meta-cell__btn {
+  flex-shrink: 0;
+  padding: 4px;
+}
 </style>