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

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zlwuandCursor committed 2026-08-07 12:50:32 +08:00
1 parent 0dbd2da703
commit a80a01b37d
1 file changed
+129 -72
@@ -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>