提交
This commit is contained in:
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,
|
||||
|
||||
Reference in new issue
Block a user