This commit is contained in:
zlwu committed 2026-08-21 16:50:47 +08:00
1 parent 6dc8cfb2c1
commit d26fb23fcf
181 files changed
+25727 -24557

No files matched your search

@@ -0,0 +1,152 @@
<template>
<el-select
:model-value="modelValue"
:placeholder="placeholder"
:clearable="clearable"
:filterable="filterable"
:allow-create="allowCreate"
:default-first-option="allowCreate"
:disabled="disabled || loading"
:size="size"
:style="style"
@update:model-value="onSelect"
>
<el-option
v-for="opt in options"
:key="opt.value"
:label="opt.label"
:value="opt.value"
>
<div class="dict-select-option">
<span>{{ opt.label }}</span>
<el-button
v-if="dictStore.isSessionCreated(dictType, opt.value)"
link
type="danger"
size="small"
class="dict-select-option__remove"
@click.stop="onRemove(opt.value)"
>
<el-icon><Delete /></el-icon>
</el-button>
</div>
</el-option>
</el-select>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import { Delete } from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { useDictStore } from '@/store/modules/dict'
const props = withDefaults(
defineProps<{
dictType: string
modelValue?: string | null
allowCreate?: boolean
clearable?: boolean
filterable?: boolean
disabled?: boolean
placeholder?: string
size?: '' | 'default' | 'small' | 'large'
style?: string | Record<string, string>
}>(),
{
modelValue: '',
allowCreate: true,
clearable: true,
filterable: true,
disabled: false,
placeholder: '请选择',
size: 'default'
}
)
const emit = defineEmits<{
'update:modelValue': [value: string]
change: [value: string]
}>()
const dictStore = useDictStore()
const loading = ref(false)
const options = computed(() => dictStore.getOptions(props.dictType))
function isKnown(value: string) {
return options.value.some((o) => o.value === value || o.label === value)
}
async function onSelect(raw: string | null | undefined) {
const next = raw == null ? '' : String(raw).trim()
if (!next) {
emit('update:modelValue', '')
emit('change', '')
return
}
if (!props.allowCreate || isKnown(next)) {
const known = options.value.find((o) => o.value === next || o.label === next)
const value = known?.value ?? next
emit('update:modelValue', value)
emit('change', value)
return
}
try {
await ElMessageBox.confirm(`将「${next}」新增到字典,是否继续?`, '新增字典项', {
type: 'warning',
confirmButtonText: '确定',
cancelButtonText: '取消'
})
} catch {
return
}
loading.value = true
try {
const data = await dictStore.ensure(props.dictType, next)
emit('update:modelValue', data.value)
emit('change', data.value)
} catch (e: any) {
ElMessage.error(e?.message || '写入字典失败')
} finally {
loading.value = false
}
}
async function onRemove(value: string) {
try {
await ElMessageBox.confirm(`确定从字典中删除「${value}」吗?`, '删除字典项', {
type: 'warning',
confirmButtonText: '删除',
cancelButtonText: '取消'
})
} catch {
return
}
loading.value = true
try {
await dictStore.ensureRemove(props.dictType, value)
if (props.modelValue === value) {
emit('update:modelValue', '')
emit('change', '')
}
ElMessage.success('已删除')
} catch (e: any) {
ElMessage.error(e?.message || '删除失败')
} finally {
loading.value = false
}
}
</script>
<style scoped>
.dict-select-option {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.dict-select-option__remove {
flex-shrink: 0;
}
</style>
@@ -4,7 +4,7 @@
v-model:current-page="pageModel"
v-model:page-size="limitModel"
:total="total"
:page-sizes="[10, 20, 50, 100]"
:page-sizes="LIST_PAGE_SIZES"
layout="total, sizes, prev, pager, next, jumper"
@size-change="emitPagination"
@current-change="emitPagination"
@@ -14,7 +14,7 @@
<script setup lang="ts">
import { computed } from 'vue'
import { DEFAULT_LIST_PAGE_SIZE } from '@/utils/table/tableConfig'
import { DEFAULT_LIST_PAGE_SIZE, LIST_PAGE_SIZES } from '@/utils/table/tableConfig'
const props = defineProps<{
total: number
@@ -423,7 +423,7 @@
v-model:current-page="addPagination.page"
v-model:page-size="addPagination.pageSize"
:total="addPagination.total"
:page-sizes="[10, 20, 50]"
:page-sizes="[10, 20, 100, 500]"
layout="total, sizes, prev, pager, next"
@size-change="handleAddSizeChange"
@current-change="handleAddPageChange"
@@ -83,9 +83,20 @@
{{ row.model || row.productModel || '—' }}
</template>
</el-table-column>
<el-table-column label="单位" min-width="80" align="center">
<el-table-column label="单位" min-width="100" align="center">
<template #default="{ row }">
<DictTag dict-type="crm_product_unit" :value="row.unit" />
<DictSelect
v-if="editablePackageDelivery"
v-model="row.unit"
class="pl-plan-product-cell-control"
dict-type="crm_product_unit"
placeholder="请选择单位"
style="width: 100%"
/>
<template v-else>
<DictTag v-if="row.unit" dict-type="crm_product_unit" :value="row.unit" />
<span v-else>—</span>
</template>
</template>
</el-table-column>
<el-table-column label="计划数量" min-width="110" align="right">
@@ -126,25 +137,14 @@
</el-table-column>
<el-table-column prop="packageSpec" label="包装规格" min-width="150" align="center">
<template #default="{ row }">
<el-select
<DictSelect
v-if="editablePackageDelivery"
v-model="row.packageSpec"
class="pl-plan-product-cell-control"
dict-type="crm_product_gg"
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>
@@ -153,21 +153,14 @@
</el-table-column>
<el-table-column label="送货方式" min-width="150" align="center">
<template #default="{ row }">
<el-select
<DictSelect
v-if="editablePackageDelivery"
v-model="row.deliveryMethod"
class="pl-plan-product-cell-control"
dict-type="crm_shfs"
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) }}
@@ -236,6 +229,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 DictSelect from '@/components/DictSelect/index.vue'
import { useDictStore } from '@/store/modules/dict'
const props = defineProps<{
@@ -248,7 +242,7 @@ const props = defineProps<{
editableQty?: boolean
/** 产品信息区:别名 / 品牌可改 */
editableAliasBrand?: boolean
/** 包装规格 / 送货方式可下拉编辑(对齐报价单) */
/** 单位 / 包装规格 / 送货方式可下拉编辑(对齐报价单) */
editablePackageDelivery?: boolean
/** 新增/编辑弹窗默认不展示实际数量 */
showActualQty?: boolean
@@ -269,9 +263,6 @@ 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 || '')
@@ -80,7 +80,14 @@
</section>
<section class="pl-plan-form-section">
<h3 class="pl-plan-form-section-title">供需双方</h3>
<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="handleSwapParties">
<el-icon><Sort /></el-icon>供需方互换
</el-button>
</div>
</div>
<el-form label-position="right" label-width="100px" class="pl-dialog-form">
<div class="pl-plan-party-row">
<div class="pl-plan-party-block">
@@ -377,7 +384,12 @@
</div>
<div class="pl-plan-logistics-block">
<h4 class="pl-plan-subtitle">送货信息</h4>
<div class="pl-plan-logistics-block-head">
<h4 class="pl-plan-subtitle">送货信息</h4>
<el-button text type="primary" @click="handleSwapPickupDelivery">
<el-icon><Sort /></el-icon>取送货互换
</el-button>
</div>
<div class="form-grid-2">
<el-form-item label="送货地址" class="form-col-span-2">
<PlAddressWithLibrary
@@ -472,7 +484,16 @@
</template>
<script setup lang="ts">
import { Check, Close, DocumentCopy, FolderOpened, Plus, Search, Van } from '@element-plus/icons-vue'
import {
Check,
Close,
DocumentCopy,
FolderOpened,
Plus,
Search,
Sort,
Van
} from '@element-plus/icons-vue'
import {
planApi,
transportApi,
@@ -838,6 +859,66 @@ function syncCommittedPartyIds() {
committedCustomerId.value = String(formData.customerId || '')
}
/** 供需方互换:仅交换本区字段,勿走 select change(会清空合同) */
function handleSwapParties() {
const temp = {
supplierId: formData.supplierId,
supplierName: formData.supplierName,
supplierContracts: [...formData.supplierContracts],
customerId: formData.customerId,
customerName: formData.customerName,
demanderContracts: [...formData.demanderContracts]
}
formData.supplierId = temp.customerId
formData.supplierName = temp.customerName
formData.supplierContracts = temp.demanderContracts
formData.customerId = temp.supplierId
formData.customerName = temp.supplierName
formData.demanderContracts = temp.supplierContracts
formData.destinationId = temp.supplierId
formData.destinationName = temp.supplierName
// 联系人仅绑定需方;互换后无供方联系人可对调,清空以免挂在错误客户上
formData.contactName = ''
formData.contactPhone = ''
syncCommittedPartyIds()
ElMessage.success('供需方已互换')
}
/** 取货信息 ↔ 送货信息 */
function handleSwapPickupDelivery() {
suppressAddressMutex.value = true
const temp = {
pickupAddress: formData.pickupAddress,
pickupLng: formData.pickupLng,
pickupLat: formData.pickupLat,
pickupTime: formData.pickupTime,
pickupContact: formData.pickupContact,
pickupNotes: formData.pickupNotes
}
formData.pickupAddress = formData.destinationAddress
formData.pickupLng = formData.destinationLng
formData.pickupLat = formData.destinationLat
formData.pickupTime = formData.deliveryTime
formData.pickupContact = formData.deliveryContact
formData.pickupNotes = formData.destinationNotes
formData.destinationAddress = temp.pickupAddress
formData.destinationLng = temp.pickupLng
formData.destinationLat = temp.pickupLat
formData.deliveryTime = temp.pickupTime
formData.deliveryContact = temp.pickupContact
formData.destinationNotes = temp.pickupNotes
nextTick(() => {
suppressAddressMutex.value = false
})
ElMessage.success('取送货信息已互换')
}
function handleSupplierSelectChange(customerId?: string) {
if (!customerId) {
formData.supplierId = ''
@@ -1711,6 +1792,17 @@ async function handleSubmit(mode: 'save' | 'saveAndArrange' = 'save') {
margin: 0 0 10px;
}
.pl-plan-logistics-block-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.pl-plan-logistics-block-head .pl-plan-subtitle {
margin: 0;
}
.pl-plan-party-select-row {
display: flex;
align-items: flex-start;
@@ -1838,7 +1930,11 @@ async function handleSubmit(mode: 'save' | 'saveAndArrange' = 'save') {
border-radius: 8px;
background: #f8fafc;
.pl-plan-subtitle {
> .pl-plan-subtitle {
margin: 0 0 8px;
}
.pl-plan-logistics-block-head {
margin: 0 0 8px;
}
@@ -17,7 +17,7 @@
<span class="selected-hint">已选 {{ selectedProducts.length }} 项</span>
<ListQuickSearchInput
v-model="quickSearchKeyword"
placeholder="产品名称/拼音首字母"
placeholder="产品名称/拼音首字母/规格型号"
@search="handleQuickSearch"
@clear="handleQuickSearchClear"
/>
@@ -317,6 +317,9 @@
page: queryParams.value.pageNum,
pageSize: queryParams.value.pageSize,
keyword: keyword || undefined,
// 选择器:名称/拼音/规格型号;不匹配产品编号(避免短数字误命中编号)
matchProductNo: false,
matchModel: true,
status: queryParams.value.status,
supplierId:
props.supplierId != null && props.supplierId !== '' ? String(props.supplierId) : undefined
@@ -470,7 +473,7 @@
margin-bottom: 12px;
:deep(.quick-search-input) {
width: 220px;
width: 280px;
flex-shrink: 0;
}
}
@@ -1,5 +1,6 @@
<script setup lang="ts">
import { computed } from 'vue'
import { LIST_PAGE_SIZES } from '@/utils/table/tableConfig'
import DataTablePanel from './DataTablePanel.vue'
import FilterBlock from './FilterBlock.vue'
import ListToolbar from './ListToolbar.vue'
@@ -23,7 +24,7 @@ const props = withDefaults(
tableScrollClass?: string
}>(),
{
pageSizes: () => [10, 20, 30],
pageSizes: () => [...LIST_PAGE_SIZES],
showSummary: false,
}
)
@@ -168,7 +168,7 @@
// 默认分页常量
const DEFAULT_PAGINATION_OPTIONS: PaginationOptions = {
pageSizes: [10, 20, 30, 50, 100],
pageSizes: [10, 20, 100, 500],
align: 'right',
background: true,
layout: layout.value,