This commit is contained in:
zlwu committed 2026-08-09 14:51:41 +08:00
1 parent ca6aca2b14
commit f70dc26206
63 files changed
+589912 -9127

No files matched your search

@@ -15,51 +15,22 @@
>
<p v-if="selectableStatusHint" class="contract-selector-hint">{{ selectableStatusHint }}</p>
<div class="filter-block contract-selector-filter">
<el-form ref="queryFormRef" :model="queryParams" class="filter-form" @submit.prevent>
<div class="filter-grid">
<el-form-item label="合同编号" prop="contractNo">
<el-input
v-model="queryParams.contractNo"
placeholder="搜索合同编号"
clearable
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="合同名称" prop="contractName">
<el-input
v-model="queryParams.contractName"
placeholder="搜索合同名称"
clearable
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="客户名称" prop="customerName">
<el-input
v-model="queryParams.customerName"
placeholder="搜索客户名称"
clearable
@keyup.enter="handleQuery"
/>
</el-form-item>
</div>
<div class="filter-actions">
<div class="filter-actions-start" />
<div class="filter-actions-end">
<el-button class="btn-ghost" :loading="loading" @click="resetQuery">
<el-icon><RefreshLeft /></el-icon>重置
</el-button>
<el-button class="btn-main" type="primary" :loading="loading" @click="handleQuery">
<el-icon><Search /></el-icon>搜索
</el-button>
</div>
</div>
</el-form>
</div>
<div class="panel dialog-list-panel contract-selector-panel">
<div class="contract-selector-quick-search">
<el-input
v-model="queryParams.keyword"
clearable
placeholder="合同编号 / 产品名称 / 产品拼音首字母"
>
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
</div>
<div class="panel dialog-list-panel">
<div class="list-body list-wrap dialog-list-body-only">
<ListTable table-key="business-contract-selector"
<ListTable
table-key="business-contract-selector"
ref="tableRef"
v-loading="loading"
:data="contractList"
@@ -74,7 +45,15 @@
>
<el-table-column label="" min-width="48" align="center" header-align="center">
<template #default="{ row }">
<el-checkbox
v-if="multiple"
:model-value="selectedIdSet.has(String(row.id))"
:disabled="!isContractSelectable(row)"
@click.stop
@change="checked => handleCheckboxChange(row, !!checked)"
/>
<el-radio
v-else
v-model="selectedId"
:label="String(row.id)"
:disabled="!isContractSelectable(row)"
@@ -89,7 +68,7 @@
<el-table-column
prop="contractNo"
label="合同编号"
min-width="150"
min-width="140"
align="center"
header-align="center"
show-overflow-tooltip
@@ -106,15 +85,7 @@
<span v-else>—</span>
</template>
</el-table-column>
<el-table-column
prop="customerName"
label="客户名称"
min-width="150"
align="center"
header-align="center"
show-overflow-tooltip
/>
<el-table-column label="产品信息" min-width="280" align="left" header-align="center">
<el-table-column label="产品信息" min-width="260" align="left" header-align="center">
<template #default="{ row }">
<div v-if="row.products?.length" class="contract-selector-products">
<div
@@ -128,34 +99,36 @@
<span v-else class="contract-selector-products__empty">—</span>
</template>
</el-table-column>
<el-table-column prop="contractType" label="合同类型" min-width="110" align="center" header-align="center">
<el-table-column
prop="contractType"
label="合同类型"
min-width="110"
align="center"
header-align="center"
>
<template #default="{ row }">
<DictTag dict-type="crm_contract_type" :value="row.contractType" />
</template>
</el-table-column>
<el-table-column label="已关联的运输计划数" min-width="140" align="center" header-align="center">
<template #default="{ row }">
{{ planCountMap[String(row.id)] ?? 0 }}
</template>
</el-table-column>
<el-table-column prop="status" label="状态" min-width="100" align="center" header-align="center">
<template #default="{ row }">
<DictTag dict-type="crm_contract_status" :value="row.status" />
</template>
</el-table-column>
<el-table-column
v-if="!hideAmount"
prop="amount"
label="合同金额"
min-width="120"
align="right"
prop="customerName"
label="客户名称"
min-width="150"
align="center"
header-align="center"
>
<template #default="{ row }">
<span class="money-display">{{ formatMoneyDisplay(row.amount) }}</span>
</template>
</el-table-column>
show-overflow-tooltip
/>
<el-table-column prop="signDate" label="签订日期" min-width="120" align="center" header-align="center" />
<el-table-column label="已关联的运输计划数" min-width="140" align="center" header-align="center">
<template #default="{ row }">
{{ planCountMap[String(row.id)] ?? 0 }}
</template>
</el-table-column>
</ListTable>
<ListPagination
@@ -176,7 +149,7 @@
<el-button
class="btn-main"
type="primary"
:disabled="!currentRow"
:disabled="multiple ? selectedIdSet.size < 1 : !currentRow"
:loading="detailLoading"
@click="handleConfirm"
>
@@ -190,15 +163,14 @@
</template>
<script setup lang="ts">
import { Check, Close, RefreshLeft, Search } from '@element-plus/icons-vue'
import type { FormInstance, TableInstance } from 'element-plus'
import { Check, Close, Search } from '@element-plus/icons-vue'
import type { TableInstance } from 'element-plus'
import { getContract, listContract } from '@/api/crm/contract'
import type { ContractProductForm, ContractQuery, ContractVO } from '@/api/crm/contract/types'
import { planApi } from '@/api/pl'
import DictTag from '@/components/DictTag/index.vue'
import ListPagination from '@/components/ListPagination/index.vue'
import { useDictStore } from '@/store/modules/dict'
import { formatMoneyDisplay } from '@/utils/money-format'
import { formatPlanProductListText } from '@/utils/pl-display'
const ContractDetailDialog = defineAsyncComponent(
@@ -211,10 +183,19 @@ const props = withDefaults(
hideAmount?: boolean
/** 仅这些状态的合同可被选中;未配置时列表内均可选 */
selectableStatuses?: string[]
/** 限定客户:只展示该客户合同 */
customerId?: string
/** 多选模式(运输计划供方/需方) */
multiple?: boolean
/** 多选模式下已选合同 ID(打开时预勾选) */
selectedIds?: string[]
}>(),
{
hideAmount: false,
selectableStatuses: undefined
selectableStatuses: undefined,
customerId: undefined,
multiple: false,
selectedIds: () => []
}
)
@@ -223,6 +204,7 @@ const dictStore = useDictStore()
const emit = defineEmits<{
'update:visible': [value: boolean]
confirm: [contract: ContractVO]
confirmMultiple: [contracts: ContractVO[]]
}>()
const innerVisible = computed({
@@ -230,7 +212,6 @@ const innerVisible = computed({
set: value => emit('update:visible', value)
})
const queryFormRef = ref<FormInstance>()
const tableRef = ref<TableInstance>()
const loading = ref(false)
const detailLoading = ref(false)
@@ -245,20 +226,30 @@ const contractDetailRef = ref<InstanceType<typeof ContractDetailDialog>>()
const queryParams = reactive<ContractQuery>({
pageNum: 1,
pageSize: 20,
contractNo: '',
contractName: '',
customerName: '',
keyword: '',
customerId: undefined,
excludeStatus: 'invalid',
includeProducts: true
})
const selectedIdSet = ref<Set<string>>(new Set())
const selectableStatusHint = computed(() => {
if (!props.selectableStatuses?.length) return ''
const labels = props.selectableStatuses
.map(status => dictStore.getLabel('crm_contract_status', status))
.filter(Boolean)
if (!labels.length) return '仅部分状态的合同可被选择'
return `仅「${labels.join('」「')}」状态的合同可被选择,其他状态仅展示不可选`
const parts: string[] = []
if (props.customerId) {
parts.push('仅展示当前客户下的合同')
}
if (props.selectableStatuses?.length) {
const labels = props.selectableStatuses
.map(status => dictStore.getLabel('crm_contract_status', status))
.filter(Boolean)
if (labels.length) {
parts.push(`仅「${labels.join('」「')}」状态可被选择,其他状态仅展示不可选`)
} else {
parts.push('仅部分状态的合同可被选择')
}
}
return parts.join(';')
})
function formatContractProductLine(product: ContractProductForm) {
@@ -357,6 +348,15 @@ function selectContractRow(row: ContractVO | null | undefined, options?: { silen
}
function handleRowClick(row: ContractVO) {
if (props.multiple) {
if (!isContractSelectable(row)) {
warnContractNotSelectable(row)
return
}
const id = String(row.id)
handleCheckboxChange(row, !selectedIdSet.value.has(id))
return
}
selectContractRow(row)
}
@@ -364,6 +364,19 @@ function handleRadioChange(row: ContractVO) {
selectContractRow(row)
}
function handleCheckboxChange(row: ContractVO, checked: boolean) {
if (!row?.id) return
if (!isContractSelectable(row)) {
warnContractNotSelectable(row)
return
}
const id = String(row.id)
const next = new Set(selectedIdSet.value)
if (checked) next.add(id)
else next.delete(id)
selectedIdSet.value = next
}
async function loadPlanCounts() {
const ids = contractList.value.map(item => String(item.id)).filter(Boolean)
if (!ids.length) {
@@ -379,28 +392,13 @@ async function loadPlanCounts() {
}
}
function handleQuery() {
queryParams.pageNum = 1
getList()
}
function resetQuery() {
queryFormRef.value?.resetFields()
queryParams.pageNum = 1
getList()
}
watchDebounced(
() => ({
contractNo: queryParams.contractNo,
contractName: queryParams.contractName,
customerName: queryParams.customerName
}),
() => queryParams.keyword,
() => {
queryParams.pageNum = 1
getList()
},
{ debounce: 300, deep: true }
{ debounce: 300 }
)
function handleCurrentChange(row: ContractVO | undefined) {
@@ -422,8 +420,11 @@ function handleCurrentChange(row: ContractVO | undefined) {
function handleOpen() {
selectedId.value = ''
currentRow.value = null
selectedIdSet.value = new Set((props.selectedIds || []).map(String).filter(Boolean))
planCountMap.value = {}
queryParams.pageNum = 1
queryParams.keyword = ''
queryParams.customerId = props.customerId || undefined
nextTick(() => {
tableRef.value?.setCurrentRow(undefined)
})
@@ -431,6 +432,26 @@ function handleOpen() {
}
async function handleConfirm() {
if (props.multiple) {
const ids = Array.from(selectedIdSet.value)
if (!ids.length) {
ElMessage.warning('请至少选择一份合同')
return
}
detailLoading.value = true
try {
const details: ContractVO[] = []
for (const id of ids) {
const res = await getContract(id)
details.push((res.data ?? res) as ContractVO)
}
emit('confirmMultiple', details)
innerVisible.value = false
} finally {
detailLoading.value = false
}
return
}
if (!currentRow.value?.id) return
if (!isContractSelectable(currentRow.value)) {
warnContractNotSelectable(currentRow.value)
@@ -460,48 +481,18 @@ async function handleConfirm() {
border-radius: 8px;
}
.contract-selector-filter {
padding: 12px 16px 10px;
background: var(--mint-surface);
border: 1px solid var(--mint-border);
border-radius: var(--mint-radius);
margin-bottom: 12px;
.filter-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 10px 14px;
}
.filter-actions {
margin-top: 10px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
}
.filter-actions-end {
display: flex;
align-items: center;
gap: 8px;
margin-left: auto;
}
.filter-actions-end .el-button + .el-button {
margin-left: 0 !important;
.contract-selector-panel {
> .list-body.list-wrap {
border-top: none;
}
}
@media (max-width: 1100px) {
.contract-selector-filter .filter-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
.contract-selector-quick-search {
margin: 0 0 12px;
@media (max-width: 560px) {
.contract-selector-filter .filter-grid {
grid-template-columns: 1fr;
:deep(.el-input) {
width: 100%;
max-width: 420px;
}
}
@@ -19,7 +19,6 @@
<el-tab-pane label="归档填写" name="archive" />
<el-tab-pane label="计划概况" name="overview" />
<el-tab-pane label="运输信息" name="transport" />
<el-tab-pane label="合同信息" name="contract" />
</el-tabs>
<div v-show="activeTab === 'archive'" class="pl-archive-tab-panel pl-arrange-form-scroll">
@@ -189,7 +188,7 @@
const loading = ref(false)
const submitLoading = ref(false)
const activeTab = ref<'archive' | 'overview' | 'transport' | 'contract'>('archive')
const activeTab = ref<'archive' | 'overview' | 'transport'>('archive')
const tabsRef = ref<InstanceType<typeof PlPlanViewTabs> | null>(null)
const plan = ref<Partial<Plan>>({})
const feedbackMirrorEl = ref<HTMLElement | null>(null)
@@ -0,0 +1,418 @@
<!-- eslint-disable vue/no-v-model-argument -->
<template>
<div class="pl-plan-contract-panel" v-loading="loadingDetails">
<slot
name="toolbar"
:open="openSelector"
:disabled="triggerDisabled"
:hint="hintText"
:hint-warn="hintWarn"
:count="selectedIds.length"
>
<div v-if="!readonly" class="pl-plan-contract-panel__toolbar">
<el-button class="btn-ghost" :disabled="triggerDisabled" @click="openSelector">
<el-icon><Search /></el-icon>选择合同
</el-button>
<span v-if="hintText" class="pl-plan-contract-panel__hint" :class="{ 'is-warn': hintWarn }">
{{ hintText }}
</span>
</div>
</slot>
<div v-if="detailRows.length" class="pl-plan-contract-cards">
<article v-for="row in detailRows" :key="row.id" class="pl-plan-contract-card">
<div class="pl-plan-contract-card__head">
<div class="pl-plan-contract-card__meta">
<span class="pl-plan-contract-card__no">{{ row.contractNo || row.id || '—' }}</span>
<DictTag dict-type="crm_contract_type" :value="row.contractType" />
<span class="pl-plan-contract-card__count">
{{ row.products?.length || 0 }} 个产品
</span>
</div>
<div class="pl-plan-contract-card__actions">
<el-button link type="primary" @click="handleViewContract(String(row.id))">
<el-icon><View /></el-icon>查看
</el-button>
<el-button v-if="!readonly" link type="danger" @click="removeContract(String(row.id))">
<el-icon><Delete /></el-icon>移除
</el-button>
</div>
</div>
<div v-if="row.products?.length" class="pl-plan-contract-card__body">
<div class="pl-plan-contract-card__table-scroll">
<el-table
:data="row.products"
class="list-table pl-plan-contract-card__table"
size="small"
stripe
>
<el-table-column label="产品名称" min-width="110" align="left" show-overflow-tooltip>
<template #default="{ row: product }">
{{ product.standardProductName || product.productName || '—' }}
</template>
</el-table-column>
<el-table-column label="别名" min-width="80" align="center" show-overflow-tooltip>
<template #default="{ row: product }">
{{ product.aliasName || '—' }}
</template>
</el-table-column>
<el-table-column label="品牌" min-width="72" align="center" show-overflow-tooltip>
<template #default="{ row: product }">
{{ formatBrandConstraintLabel(product.brandMode, product.brandName) }}
</template>
</el-table-column>
<el-table-column label="型号/规格" min-width="90" align="center" show-overflow-tooltip>
<template #default="{ row: product }">
{{ product.model || product.productModel || '—' }}
</template>
</el-table-column>
<el-table-column label="数量" min-width="64" align="right" show-overflow-tooltip>
<template #default="{ row: product }">
{{ product.quantity ?? '—' }}
</template>
</el-table-column>
<el-table-column label="单位" min-width="56" align="center">
<template #default="{ row: product }">
<DictTag dict-type="crm_product_unit" :value="product.unit" />
</template>
</el-table-column>
<el-table-column label="包装规格" min-width="90" align="center" show-overflow-tooltip>
<template #default="{ row: product }">
{{ product.packageSpec || '—' }}
</template>
</el-table-column>
<el-table-column label="送货方式" min-width="80" align="center" show-overflow-tooltip>
<template #default="{ row: product }">
{{ product.deliveryMethod || '—' }}
</template>
</el-table-column>
<el-table-column label="备注" min-width="90" align="left" show-overflow-tooltip>
<template #default="{ row: product }">
{{ product.remark || '—' }}
</template>
</el-table-column>
</el-table>
</div>
</div>
<p v-else class="pl-plan-contract-card__empty">暂无产品明细</p>
</article>
</div>
<p v-else-if="readonly || customerId" class="pl-plan-contract-panel__empty-hint">
{{ readonly ? '暂无关联合同' : '暂未选择合同' }}
</p>
<ContractSelector
v-if="!readonly"
v-model:visible="selectorVisible"
:customer-id="customerId"
:selectable-statuses="selectableStatuses"
multiple
:selected-ids="selectedIds"
hide-amount
@confirm-multiple="handleConfirmMultiple"
/>
<ContractDetailDialog
v-if="contractDetailDialogActive"
ref="contractDetailRef"
hide-product-price
/>
</div>
</template>
<script setup lang="ts">
import { Delete, Search, View } from '@element-plus/icons-vue'
import { getContract } from '@/api/crm/contract'
import type { ContractProductForm, ContractVO } from '@/api/crm/contract/types'
import { fetchProductDetail } from '@/api/crm/product'
import ContractSelector from '@/components/business/contract-selector/index.vue'
import DictTag from '@/components/DictTag/index.vue'
import { applyAliasDisplayMeta, formatBrandConstraintLabel } from '@/utils/crm-product-brand'
const ContractDetailDialog = defineAsyncComponent(
() => import('@/views/crm/contract/components/ContractDetailDialog.vue')
)
export interface PlanContractLinkForm {
contractId: string
contractNo?: string
}
const props = withDefaults(
defineProps<{
modelValue: PlanContractLinkForm[]
customerId?: string
disabled?: boolean
required?: boolean
/** 只读:展示合同卡片,标题栏提供「查看」 */
readonly?: boolean
selectableStatuses?: string[]
}>(),
{
disabled: false,
required: false,
readonly: false,
selectableStatuses: () => ['approved', 'executing']
}
)
const emit = defineEmits<{
'update:modelValue': [value: PlanContractLinkForm[]]
view: [contractId: string]
}>()
const selectorVisible = ref(false)
const detailRows = ref<ContractVO[]>([])
const loadingDetails = ref(false)
const contractDetailDialogActive = ref(false)
const contractDetailRef = ref<InstanceType<typeof ContractDetailDialog>>()
const selectedIds = computed(() =>
(props.modelValue || []).map(item => String(item.contractId)).filter(Boolean)
)
const triggerDisabled = computed(() => props.disabled || !props.customerId)
const hintWarn = computed(
() => !!props.customerId && props.required && !selectedIds.value.length
)
const hintText = computed(() => {
if (!props.customerId) return '请先选择客户'
if (hintWarn.value) return '请至少选择一份合同'
if (selectedIds.value.length) return `已选 ${selectedIds.value.length} 份`
return ''
})
function openSelector() {
if (props.readonly || triggerDisabled.value) return
selectorVisible.value = true
}
async function enrichContractProducts(contract: ContractVO): Promise<ContractVO> {
const products = (contract.products || []) as ContractProductForm[]
if (!products.length) return contract
await Promise.all(
products.map(async line => {
if (!line.productId) {
line.standardProductName = line.standardProductName || line.productName
return
}
if (line.standardProductName || line.aliasName) return
try {
const res: any = await fetchProductDetail({ id: String(line.productId) })
const data = res?.data ?? res
if (data) applyAliasDisplayMeta(line, data)
} catch {
line.standardProductName = line.productName
line.aliasName = undefined
}
})
)
return { ...contract, products }
}
function emitLinksFromDetails(rows: ContractVO[]) {
emit(
'update:modelValue',
rows.map(row => ({
contractId: String(row.id),
contractNo: row.contractNo || undefined
}))
)
}
async function loadDetails(ids: string[]) {
if (!ids.length) {
detailRows.value = []
return
}
loadingDetails.value = true
try {
const rows: ContractVO[] = []
for (const id of ids) {
const existing = detailRows.value.find(item => String(item.id) === id)
if (existing?.products?.length) {
rows.push(await enrichContractProducts(existing))
continue
}
try {
const res = await getContract(id)
const detail = (res.data ?? res) as ContractVO
rows.push(await enrichContractProducts(detail))
} catch {
const link = (props.modelValue || []).find(item => String(item.contractId) === id)
rows.push({
id,
contractNo: link?.contractNo || id,
products: []
} as ContractVO)
}
}
detailRows.value = rows
} finally {
loadingDetails.value = false
}
}
async function handleConfirmMultiple(contracts: ContractVO[]) {
const enriched = await Promise.all((contracts || []).map(item => enrichContractProducts(item)))
detailRows.value = enriched
emitLinksFromDetails(detailRows.value)
}
function removeContract(id: string) {
const next = detailRows.value.filter(item => String(item.id) !== id)
detailRows.value = next
emitLinksFromDetails(next)
}
async function handleViewContract(contractId: string) {
const id = String(contractId || '').trim()
if (!id) {
ElMessage.warning('暂无关联合同')
return
}
emit('view', id)
contractDetailDialogActive.value = true
await nextTick()
contractDetailRef.value?.open(id)
}
defineExpose({
openSelector
})
watch(
() => props.customerId,
(id, prev) => {
if (props.readonly) return
if (prev && id !== prev) {
detailRows.value = []
emit('update:modelValue', [])
}
}
)
watch(
selectedIds,
ids => {
const current = detailRows.value.map(item => String(item.id)).join(',')
const next = ids.join(',')
if (current !== next) {
loadDetails(ids)
}
},
{ immediate: true }
)
</script>
<style scoped lang="scss">
.pl-plan-contract-panel {
width: 100%;
min-width: 0;
}
.pl-plan-contract-panel__toolbar {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 8px;
}
.pl-plan-contract-panel__hint {
font-size: 12px;
color: #64748b;
&.is-warn {
color: #d97706;
}
}
.pl-plan-contract-cards {
display: flex;
flex-direction: column;
gap: 10px;
min-width: 0;
}
.pl-plan-contract-card {
min-width: 0;
overflow: hidden;
border: 1px solid var(--mint-border, #d9e8e2);
border-radius: 8px;
background: #fff;
}
.pl-plan-contract-card__head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
padding: 8px 12px;
background: var(--primary-bg, #ecfdf5);
border-bottom: 1px solid #a7f3d0;
}
.pl-plan-contract-card__meta {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px 10px;
min-width: 0;
}
.pl-plan-contract-card__actions {
display: inline-flex;
align-items: center;
flex-shrink: 0;
gap: 4px;
}
.pl-plan-contract-card__no {
font-size: 13px;
font-weight: 700;
color: var(--primary-dark, #059669);
word-break: break-all;
}
.pl-plan-contract-card__count {
font-size: 12px;
color: #64748b;
}
.pl-plan-contract-card__body {
min-width: 0;
}
.pl-plan-contract-card__table-scroll {
width: 100%;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
.pl-plan-contract-card__table {
width: max-content;
min-width: 100%;
:deep(.el-table__header th) {
padding: 6px 0;
font-size: 12px;
background: #f8fafc;
}
:deep(.el-table__body td) {
padding: 6px 0;
font-size: 12px;
}
}
.pl-plan-contract-card__empty,
.pl-plan-contract-panel__empty-hint {
margin: 0;
padding: 10px 12px;
font-size: 12px;
color: #94a3b8;
}
</style>
@@ -124,10 +124,56 @@
{{ formatBrandQtySummary(row.brandQtyRows) || '—' }}
</template>
</el-table-column>
<el-table-column prop="packageSpec" label="包装规格" min-width="110" show-overflow-tooltip />
<el-table-column label="送货方式" min-width="100" align="center">
<el-table-column prop="packageSpec" label="包装规格" min-width="150" align="center">
<template #default="{ row }">
<el-tag size="small" type="info">{{ getDeliveryMethodLabel(row.deliveryMethod) }}</el-tag>
<el-select
v-if="editablePackageDelivery"
v-model="row.packageSpec"
class="pl-plan-product-cell-control"
placeholder="请选择包装规格"
filterable
allow-create
default-first-option
clearable
style="width: 100%"
@change="val => handlePackageSpecChange(row, val)"
>
<el-option
v-for="dict in dictStore.getOptions('crm_product_gg')"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
<template v-else>
<DictTag v-if="row.packageSpec" dict-type="crm_product_gg" :value="row.packageSpec" />
<span v-else>—</span>
</template>
</template>
</el-table-column>
<el-table-column label="送货方式" min-width="150" align="center">
<template #default="{ row }">
<el-select
v-if="editablePackageDelivery"
v-model="row.deliveryMethod"
class="pl-plan-product-cell-control"
placeholder="请选择送货方式"
clearable
style="width: 100%"
>
<el-option
v-for="dict in dictStore.getOptions('crm_shfs')"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
<template v-else>
<el-tag v-if="row.deliveryMethod" size="small" type="info">
{{ getDeliveryMethodDisplay(row.deliveryMethod) }}
</el-tag>
<span v-else>—</span>
</template>
</template>
</el-table-column>
<el-table-column v-if="editableRemark" label="备注" min-width="140">
@@ -190,6 +236,7 @@ import { formatBrandQtySummary } from '@/utils/pl-display'
import PlProductNameHazardCell from '@/components/business/pl-product-name-hazard-cell/index.vue'
import ProductAliasBrandDialog from '@/components/business/product-alias-brand-dialog/index.vue'
import DictTag from '@/components/DictTag/index.vue'
import { useDictStore } from '@/store/modules/dict'
const props = defineProps<{
products: PlanFormProductRow[]
@@ -201,6 +248,8 @@ const props = defineProps<{
editableQty?: boolean
/** 产品信息区:别名 / 品牌可改 */
editableAliasBrand?: boolean
/** 包装规格 / 送货方式可下拉编辑(对齐报价单) */
editablePackageDelivery?: boolean
/** 新增/编辑弹窗默认不展示实际数量 */
showActualQty?: boolean
/** 展示归档品牌数量摘要(如:君正 12 / 红四方 13) */
@@ -214,10 +263,22 @@ const emit = defineEmits<{
select: [row: PlanFormProductRow, index: number]
}>()
const dictStore = useDictStore()
const selectedProductIdSet = computed(
() => new Set((props.selectedProductIds || []).map(id => String(id)))
)
function handlePackageSpecChange(row: PlanFormProductRow, value: string | number | boolean | undefined) {
row.packageSpec = value == null || value === '' ? '' : String(value)
}
function getDeliveryMethodDisplay(method?: string) {
const label = dictStore.getLabel('crm_shfs', method || '')
if (label && label !== method) return label
return getDeliveryMethodLabel(method)
}
function isProductSelected(row: PlanFormProductRow) {
return selectedProductIdSet.value.has(String(row.productId))
}
File diff suppressed because it is too large. Load diff
@@ -1,68 +1,15 @@
<!-- eslint-disable vue/no-v-model-argument -->
<template>
<div class="pl-plan-transport-feedback-blocks">
<section class="pl-feedback-shared-products">
<section class="pl-plan-form-section pl-plan-form-section--plain">
<h3 class="pl-plan-form-section-title">产品信息</h3>
<p v-if="!sharedProductTableRows.length && !loading" class="pl-plan-form-empty-hint"
>暂无产品</p
>
<div v-else class="view-subtable-wrap">
<ListTable
table-key="business-pl-plan-transport-feedback-shared-products"
:data="sharedProductTableRows"
class="view-subtable list-table"
stripe
>
<el-table-column type="index" label="序号" min-width="68" align="center" />
<el-table-column
prop="productNo"
label="产品编码"
min-width="110"
show-overflow-tooltip
>
<template #default="{ row }">{{ row.productNo || '—' }}</template>
</el-table-column>
<el-table-column
prop="productName"
label="产品名称"
min-width="120"
show-overflow-tooltip
>
<template #default="{ row }">{{ row.productName || '—' }}</template>
</el-table-column>
<el-table-column prop="aliasName" label="别名" min-width="110" show-overflow-tooltip>
<template #default="{ row }">{{ row.aliasName || '—' }}</template>
</el-table-column>
<el-table-column prop="brandName" label="品牌" min-width="100" show-overflow-tooltip>
<template #default="{ row }">{{ row.brandName || '—' }}</template>
</el-table-column>
<el-table-column
prop="specification"
label="型号/规格"
min-width="100"
show-overflow-tooltip
>
<template #default="{ row }">{{ row.specification || '—' }}</template>
</el-table-column>
<el-table-column prop="unitLabel" label="单位" min-width="72" align="center">
<template #default="{ row }">{{ row.unitLabel || '—' }}</template>
</el-table-column>
<el-table-column prop="planQty" label="计划数量" min-width="100" align="right">
<template #default="{ row }">{{ row.planQty ?? '—' }}</template>
</el-table-column>
<el-table-column
prop="packageSpec"
label="包装规格"
min-width="110"
show-overflow-tooltip
>
<template #default="{ row }">{{ row.packageSpec || '—' }}</template>
</el-table-column>
<el-table-column prop="remark" label="备注" min-width="120" show-overflow-tooltip>
<template #default="{ row }">{{ row.remark || '—' }}</template>
</el-table-column>
</ListTable>
</div>
<p v-if="!planProductRows.length && !loading" class="pl-plan-form-empty-hint">暂无产品明细</p>
<PlPlanFormProductTable
v-else
:products="planProductRows"
show-actual-qty
show-brand-qty
/>
</section>
<div class="pl-feedback-cards-row">
@@ -377,6 +324,7 @@
import { computed } from 'vue'
import { CopyDocument, Edit } from '@element-plus/icons-vue'
import AttachmentUploadField from '@/components/business/attachment-upload-field/index.vue'
import PlPlanFormProductTable from '@/components/business/pl-plan-form-dialog/PlPlanFormProductTable.vue'
import {
businessTypeTagType,
formatKgToTon
@@ -422,19 +370,6 @@
siteFeedbackImages: string
}
type SharedProductTableRow = {
key: string
productNo?: string
productName: string
aliasName?: string
brandName?: string
specification?: string
unitLabel?: string
planQty?: number | null
packageSpec?: string
remark?: string
}
type TicketQtyRow = {
key: string
refLabel: string
@@ -599,63 +534,19 @@
return props.ioOrder.kind === 'inbound' ? '入库单回传' : '出库单回传'
})
const sharedProductTableRows = computed<SharedProductTableRow[]>(() => {
if (props.planProductRows?.length) {
return props.planProductRows.map((row, index) => {
const unitLabel =
dictStore.getLabel('crm_product_unit', row.unit) || row.unit || ''
const brandLabel = resolveDisplayBrandLabel({
brandMode: row.brandMode,
brandName: row.brandName
})
return {
key: `plan-${row.id || row.productId || index}`,
productNo: row.productNo,
productName: row.standardProductName || row.productName || '',
aliasName: row.aliasName,
brandName: brandLabel,
specification: row.model,
unitLabel,
planQty: row.quantity,
packageSpec: row.packageSpec,
remark: row.remark
}
})
const feedbackQtyUnit = computed(() => {
const planRow = props.planProductRows?.[0]
if (planRow?.unit) {
return dictStore.getLabel('crm_product_unit', planRow.unit) || planRow.unit || '吨'
}
if (props.driverFeedbackTableRows?.length) {
return props.driverFeedbackTableRows.map((row, index) => ({
key: `driver-${index}-${row.productNo || row.productName}`,
productNo: row.productNo,
productName: row.productName || '',
aliasName: row.aliasName,
brandName: row.brandName,
specification: row.specification || row.model,
unitLabel: row.unitLabel || row.unit,
planQty: row.planQty,
packageSpec: row.packageSpec,
remark: row.remark
}))
const driverRow = props.driverFeedbackTableRows?.[0]
if (driverRow?.unitLabel || driverRow?.unit) {
return driverRow.unitLabel || driverRow.unit || '吨'
}
return resolvedIoOrderItems.value.map((row, index) => ({
key: `io-${index}-${row.productId || row.productName}`,
productNo: row.productNo,
productName: row.productName || '',
aliasName: row.aliasName,
brandName: row.brandName,
specification: row.specification,
unitLabel: row.unitLabel || row.unit,
planQty: row.planQty,
packageSpec: row.packageSpec,
remark: row.remark
}))
const ioRow = resolvedIoOrderItems.value[0]
return ioRow?.unitLabel || ioRow?.unit || '吨'
})
const feedbackQtyUnit = computed(
() => sharedProductTableRows.value[0]?.unitLabel || '吨'
)
function formatTicketQty(value: unknown) {
if (value == null || value === '') return '—'
return String(value)
@@ -780,42 +671,6 @@
flex-shrink: 0;
}
.pl-feedback-shared-products {
padding: 16px;
border: 1px solid #e2e8f0;
border-radius: 10px;
background: #fff;
}
.view-subtable-wrap {
border: 1px solid #e2e8f0;
border-radius: 8px;
overflow: hidden;
background: #fff;
}
.view-subtable-wrap :deep(.el-table.view-subtable) {
--el-table-header-bg-color: #f8fafc;
--el-table-header-text-color: #1e293b;
--el-table-border-color: #e2e8f0;
width: 100%;
.el-table__header th.el-table__cell,
.el-table__body td.el-table__cell {
font-size: 14px;
padding: 8px 0;
}
.el-table__header th.el-table__cell .cell {
font-weight: 500;
color: #1e293b;
}
.el-table__body td.el-table__cell .cell {
color: #1e293b;
}
}
.pl-feedback-cards-row {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
@@ -9,7 +9,7 @@
<el-tab-pane label="计划概况" name="overview">
<section class="pl-plan-form-section pl-plan-form-section--plain">
<h3 class="pl-plan-form-section-title">基础信息</h3>
<ViewKvTable :fields="overviewBasicViewFields" mode="quint" />
<ViewKvTable :fields="overviewBasicViewFields" mode="sept" />
</section>
<section class="pl-plan-form-section pl-plan-form-section--plain">
@@ -18,6 +18,82 @@
<PlPlanFormProductTable v-else :products="productRows" show-actual-qty show-brand-qty />
</section>
<section class="pl-plan-form-section pl-plan-form-section--plain">
<h3 class="pl-plan-form-section-title">供需双方</h3>
<div class="pl-plan-party-row">
<div class="pl-plan-party-block">
<h4 class="pl-plan-subtitle">供方信息</h4>
<PlPlanContractMultiSelect
:model-value="plan?.supplierContracts || []"
:customer-id="plan?.supplierId"
readonly
>
<template #toolbar>
<div class="pl-plan-party-select-row">
<div class="pl-plan-view-party-field pl-plan-party-select-row__customer">
<span class="pl-plan-view-party-field__value">{{
plan?.supplierName || '—'
}}</span>
<span
v-if="supplierContactText"
class="pl-plan-view-party-field__contact"
>
{{ supplierContactText }}
</span>
</div>
</div>
</template>
</PlPlanContractMultiSelect>
</div>
<div class="pl-plan-flow-bridge" aria-hidden="true">
<span class="pl-plan-flow-bridge__label">供货</span>
<span class="pl-plan-flow-bridge__arrow">
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<path
d="M5 12h11.5"
stroke="currentColor"
stroke-width="2.75"
stroke-linecap="round"
/>
<path
d="M13.5 8.5 18.5 12l-5 3.5"
stroke="currentColor"
stroke-width="2.75"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</span>
</div>
<div class="pl-plan-party-block">
<h4 class="pl-plan-subtitle">需方信息</h4>
<PlPlanContractMultiSelect
:model-value="plan?.demanderContracts || []"
:customer-id="plan?.customerId"
readonly
>
<template #toolbar>
<div class="pl-plan-party-select-row">
<div class="pl-plan-view-party-field pl-plan-party-select-row__customer">
<span class="pl-plan-view-party-field__value">{{
plan?.customerName || '—'
}}</span>
<span
v-if="demanderContactText"
class="pl-plan-view-party-field__contact"
>
{{ demanderContactText }}
</span>
</div>
</div>
</template>
</PlPlanContractMultiSelect>
</div>
</div>
</section>
<section class="pl-plan-form-section pl-plan-form-section--plain">
<h3 class="pl-plan-form-section-title">危化品证书信息</h3>
<PlPlanHazardCertViewTable :products="hazardProducts" />
@@ -127,59 +203,6 @@
/>
</el-tab-pane>
<el-tab-pane label="合同信息" name="contract">
<section class="pl-plan-form-section pl-plan-form-section--plain">
<p v-if="!contractSnapshot" class="pl-plan-form-empty-hint">暂无关联合同信息</p>
<template v-else>
<div class="pl-plan-form-section-head">
<h3 class="pl-plan-form-section-title">基本信息</h3>
<div class="pl-plan-form-section-actions">
<el-button class="btn-ghost" @click="handleViewContract">
<el-icon><View /></el-icon>查看合同
</el-button>
</div>
</div>
<ViewKvTable :fields="contractBasicViewFields" mode="triple" />
<h4 class="pl-plan-subtitle">供需双方</h4>
<div class="parties-row">
<div class="party-panel">
<div class="party-panel__title">供方信息</div>
<ViewKvTable :fields="contractSupplierViewFields" mode="dual" />
</div>
<div class="party-flow-bridge" aria-hidden="true">
<span class="party-flow-bridge__label">供货</span>
<span class="party-flow-bridge__arrow">
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<path
d="M5 12h11.5"
stroke="currentColor"
stroke-width="2.75"
stroke-linecap="round"
/>
<path
d="M13.5 8.5 18.5 12l-5 3.5"
stroke="currentColor"
stroke-width="2.75"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</span>
</div>
<div class="party-panel">
<div class="party-panel__title">需方信息</div>
<ViewKvTable :fields="contractBuyerViewFields" mode="dual" />
</div>
</div>
<h4 class="pl-plan-subtitle">合同产品预览</h4>
<PlPlanFormProductTable :products="contractProductRows" />
</template>
</section>
</el-tab-pane>
</el-tabs>
<!-- defer:目标在同次渲染的兄弟节点(归档左侧)时,等 flush 后再解析,避免 emitsOptions 空引用 -->
@@ -187,7 +210,6 @@
<PlPlanTransportFeedbackBlocks v-bind="feedbackBlocksBind" v-on="feedbackBlocksOn" />
</Teleport>
<ContractDetailDialog v-if="contractDetailDialogActive" ref="contractDetailRef" hide-product-price />
<PlPlanDriverFeedbackDialog
v-model:visible="driverFeedbackDialogVisible"
:plan-id="planId"
@@ -335,8 +357,9 @@
</template>
<script setup lang="ts">
import { Check, Close, CopyDocument, View } from '@element-plus/icons-vue'
import { Check, Close, CopyDocument } from '@element-plus/icons-vue'
import AttachmentUploadField from '@/components/business/attachment-upload-field/index.vue'
import PlPlanContractMultiSelect from '@/components/business/pl-plan-contract-multi-select/index.vue'
import BrandQtyChipsField, {
type BrandQtyRow
} from '@/components/business/brand-qty-chips-field/index.vue'
@@ -352,6 +375,8 @@
import ViewKvTable from '@/components/business/system-view-dialog/ViewKvTable.vue'
import type { ViewField } from '@/components/business/system-view-dialog/types'
import type { ContractVO } from '@/api/crm/contract/types'
import { getContactByCustomerId } from '@/api/crm/customer'
import type { ContactVO } from '@/api/crm/customerTypes'
import { inventoryApi } from '@/api/db/inventory'
import {
inboundOrderApi,
@@ -362,7 +387,6 @@
import {
enrichPlanProductLinesAlias,
getDeliveryMethodLabel,
mapContractProducts,
type PlanFormProductRow
} from '@/composables/usePlContractToPlan'
import { mapPlanProductToFormRow, usePlPlanViewDetail, type PlPlanTransportSiteRow } from '@/composables/usePlPlanViewDetail'
@@ -387,10 +411,6 @@
import { formatMoneyDisplay } from '@/utils/money-format'
import type { Transport, TransportDriverProductFeedback } from '@/api/pl'
const ContractDetailDialog = defineAsyncComponent(
() => import('@/views/crm/contract/components/ContractDetailDialog.vue')
)
const dictStore = useDictStore()
const props = withDefaults(
@@ -405,7 +425,7 @@
feedbackMirrorTo?: string | HTMLElement | null
/** 隐藏内部 Tab 导航(由外层统一控制 Tab) */
hideNav?: boolean
/** 受控当前 Tab:overview / transport / contract */
/** 受控当前 Tab:overview / transport */
tab?: string
}>(),
{
@@ -441,8 +461,6 @@
}
}
)
const contractDetailDialogActive = ref(false)
const contractDetailRef = ref<InstanceType<typeof ContractDetailDialog>>()
const vehicleViewVisible = ref(false)
const driverFeedbackDialogVisible = ref(false)
const siteFeedbackDialogVisible = ref(false)
@@ -469,6 +487,80 @@
resetDetail
} = usePlPlanViewDetail()
type PartyContactDisplay = { contactName?: string; phone?: string }
const supplierContact = ref<PartyContactDisplay>({})
const demanderContact = ref<PartyContactDisplay>({})
let partyContactLoadSeq = 0
function pickPrimaryContact(list: ContactVO[]): PartyContactDisplay {
if (!list.length) return {}
const primary =
list.find(item => Number(item.isPrimary) === 1) ||
list.find(item => Number(item.isKey) === 1) ||
list[0]
const contactName = String(primary?.contactName || '').trim()
const phone = String(primary?.mobile || primary?.telephone || '').trim()
return {
contactName: contactName || undefined,
phone: phone || undefined
}
}
function formatPartyContactText(contact: PartyContactDisplay): string {
const parts: string[] = []
if (contact.contactName) parts.push(contact.contactName)
if (contact.phone) parts.push(contact.phone)
return parts.join(' ')
}
const supplierContactText = computed(() => formatPartyContactText(supplierContact.value))
const demanderContactText = computed(() => formatPartyContactText(demanderContact.value))
async function loadPartyContact(customerId?: string | null): Promise<PartyContactDisplay> {
const id = String(customerId || '').trim()
if (!id) return {}
try {
const res = await getContactByCustomerId(id)
const list = (res?.data || []) as ContactVO[]
return pickPrimaryContact(Array.isArray(list) ? list : [])
} catch {
return {}
}
}
async function refreshPartyContacts() {
const seq = ++partyContactLoadSeq
const supplierId = plan.value?.supplierId
const customerId = plan.value?.customerId
const [supplier, demanderFromCrm] = await Promise.all([
loadPartyContact(supplierId),
loadPartyContact(customerId)
])
if (seq !== partyContactLoadSeq) return
supplierContact.value = supplier
const planContactName = String(plan.value?.contactName || '').trim()
const planContactPhone = String(plan.value?.contactPhone || '').trim()
demanderContact.value = {
contactName: demanderFromCrm.contactName || planContactName || undefined,
phone: demanderFromCrm.phone || planContactPhone || undefined
}
}
watch(
() => [plan.value?.id, plan.value?.supplierId, plan.value?.customerId] as const,
() => {
if (!plan.value?.id) {
supplierContact.value = {}
demanderContact.value = {}
return
}
void refreshPartyContacts()
},
{ immediate: true }
)
const ioOrderEditVisible = ref(false)
const ioOrderSaving = ref(false)
const ioOrderEditForm = ref<PlIoOrder | null>(null)
@@ -1123,7 +1215,9 @@
{ label: '计划日期', value: plan.value?.planDate },
{ label: '归属公司', value: getPlPlanCompanyLabel(plan.value?.belongsCompany) },
{ label: '单据类型', value: getPlPlanTypeLabel(plan.value?.planType || '') },
{ label: '跟进人', value: plan.value?.followUserName }
{ label: '跟进人', value: plan.value?.followUserName },
{ label: '供方', value: plan.value?.supplierName },
{ label: '需方', value: plan.value?.customerName }
])
const summaryCarrierName = computed(() => displayCarrierName.value)
@@ -1181,57 +1275,6 @@
]
})
const contractBasicViewFields = computed<ViewField[]>(() => {
const c = contractSnapshot.value
if (!c) return []
const settlementLabel = c.settlementMethod
? dictStore.getLabel('crm_settlement_method', c.settlementMethod)
: '—'
return [
{ label: '合同编号', value: c.contractNo, long: false },
{ label: '合同名称', value: c.contractName, long: false },
{ label: '签订日期', value: c.signDate },
{
label: '合同有效期',
value: c.startDate && c.endDate ? `${c.startDate} ~ ${c.endDate}` : '—',
long: false
},
{ label: '结算方式', value: settlementLabel }
]
})
const contractSupplierViewFields = computed<ViewField[]>(() => {
const c = contractSnapshot.value
if (!c) return []
return [
{ label: '单位名称(章)', value: c.partyA },
{ label: '单位地址', value: c.partyAAddress },
{ label: '法定代表人', value: c.partyALegalPerson },
{ label: '代理人', value: c.partyAAgent },
{ label: '电话/传真', value: c.partyAPhoneFax },
{ label: '开户银行', value: c.partyABank },
{ label: '账号', value: c.partyAAccount }
]
})
const contractBuyerViewFields = computed<ViewField[]>(() => {
const c = contractSnapshot.value
if (!c) return []
return [
{ label: '单位名称(章)', value: c.partyB },
{ label: '单位地址', value: c.partyBAddress },
{ label: '法定代表人', value: c.partyBLegalPerson },
{ label: '代理人', value: c.partyBAgent },
{ label: '电话/传真', value: c.partyBPhoneFax },
{ label: '开户银行', value: c.partyBBank },
{ label: '账号', value: c.partyBAccount }
]
})
const contractProductRows = computed<PlanFormProductRow[]>(() =>
mapContractProducts(contractSnapshot.value?.products)
)
/** 危化证区域展示 CRM 标准名(productRows 已回填 standardProductName;库内 productName 可能是别名) */
const hazardProducts = computed(() =>
products.value
@@ -1244,18 +1287,6 @@
})
)
async function handleViewContract() {
const contractId = contractSnapshot.value?.id || plan.value?.contractId
if (!contractId) {
ElMessage.warning('暂无关联合同')
return
}
contractDetailDialogActive.value = true
await nextTick()
contractDetailRef.value?.open(contractId)
}
watch(
() => props.planId,
async (planId) => {
@@ -1443,7 +1474,8 @@
grid-column: 1 / -1;
}
.parties-row {
.parties-row,
.pl-plan-party-row {
display: flex;
align-items: stretch;
gap: 0;
@@ -1451,6 +1483,100 @@
min-height: 0;
}
.pl-plan-party-block {
flex: 1 1 0;
min-width: 0;
padding: 12px;
border: 1px solid #e2e8f0;
border-radius: 8px;
background: #f8fafc;
overflow: hidden;
}
.pl-plan-subtitle {
margin: 0 0 10px;
font-size: 13px;
font-weight: 600;
color: #1e293b;
}
.pl-plan-party-select-row {
display: flex;
align-items: flex-start;
gap: 12px;
margin-bottom: 8px;
}
.pl-plan-party-select-row__customer {
flex: 1.4 1 0;
min-width: 0;
}
.pl-plan-view-party-field {
display: flex;
flex-wrap: wrap;
align-items: baseline;
gap: 0 12px;
min-width: 0;
}
.pl-plan-view-party-field__value {
font-size: 14px;
font-weight: 600;
color: #1e293b;
word-break: break-word;
line-height: 1.5;
}
.pl-plan-view-party-field__contact {
font-size: 14px;
font-weight: 400;
color: var(--mint-text-muted, #64748b);
white-space: nowrap;
line-height: 1.5;
}
.pl-plan-flow-bridge {
flex: 0 0 88px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 10px;
padding: 0 8px;
}
.pl-plan-flow-bridge__label {
font-size: 12px;
font-weight: 600;
color: var(--primary-dark, #168f45);
letter-spacing: 0.06em;
padding: 4px 10px;
border-radius: 999px;
background: #e6f8ed;
border: 1px solid #a8e0b8;
line-height: 1.2;
}
.pl-plan-flow-bridge__arrow {
display: flex;
align-items: center;
justify-content: center;
width: 44px;
height: 44px;
border-radius: 50%;
color: var(--primary-dark, #168f45);
background: linear-gradient(145deg, #e6f8ed 0%, #d4f0de 100%);
border: 1px solid #a8e0b8;
box-shadow: 0 2px 8px rgba(28, 168, 81, 0.14);
svg {
width: 22px;
height: 22px;
display: block;
}
}
.party-panel {
flex: 1 1 0;
min-width: 0;
@@ -1536,19 +1662,22 @@
}
@media (max-width: 1100px) {
.parties-row {
.parties-row,
.pl-plan-party-row {
flex-direction: column;
min-height: 0;
}
.party-flow-bridge {
.party-flow-bridge,
.pl-plan-flow-bridge {
flex: 0 0 auto;
flex-direction: row;
gap: 12px;
padding: 12px 0;
}
.party-flow-bridge__arrow {
.party-flow-bridge__arrow,
.pl-plan-flow-bridge__arrow {
transform: rotate(90deg);
}
}