feat: product form and search UI for pinyin initials.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zlwuandCursor committed 2026-08-07 17:30:53 +08:00
1 parent 5575b08437
commit b151ac6499
5 files changed
+1457 -847

No files matched your search

+1
View File
@@ -16,6 +16,7 @@ export interface ProductAliasItem {
export interface ProductFormData {
id?: string
productName: string
namePinyinAbbr?: string
productNo?: string
categoryId?: string
model?: string
@@ -44,7 +44,7 @@
placeholder?: string
}>(),
{
placeholder: '请搜索并选择 CRM 产品'
placeholder: '请搜索 CRM 产品(名称/拼音)'
}
)
@@ -0,0 +1,514 @@
<!-- eslint-disable vue/no-v-model-argument -->
<template>
<el-dialog
v-model="visible"
:title="isEdit ? '编辑产品' : '新增产品'"
width="960px"
class="dialog-form-layout"
align-center
append-to-body
destroy-on-close
modal-class="order-module-dialog-mask"
style="backdrop-filter: none"
:close-on-click-modal="false"
@open="handleOpen"
@close="handleClose"
>
<el-form
ref="formRef"
v-loading="dialogLoading"
:model="formData"
:rules="formRules"
label-position="top"
class="crm-dialog-form"
scroll-to-error
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
>
<div class="form-grid-3">
<el-form-item label="产品编号">
<el-input v-model="formData.productNo" placeholder="留空自动生成(如 SXWL000001)" />
</el-form-item>
<el-form-item label="产品名称" prop="productName">
<el-input v-model="formData.productName" placeholder="请输入产品名称" />
</el-form-item>
<el-form-item label="拼音首字母" class="form-col-span-2">
<div class="pinyin-abbr-row">
<el-input
v-model="formData.namePinyinAbbr"
placeholder="默认按名称自动生成,可手改"
maxlength="100"
clearable
/>
<el-button class="btn-ghost" @click="regeneratePinyinAbbr">
<el-icon><Refresh /></el-icon>
重新生成
</el-button>
</div>
</el-form-item>
<el-form-item label="产品分类">
<el-tree-select
v-model="formData.categoryId"
:data="categoryOptions"
:props="{ value: 'id', label: 'categoryName', children: 'children' }"
placeholder="请选择产品分类"
check-strictly
clearable
style="width: 100%"
/>
</el-form-item>
<el-form-item label="规格型号">
<el-input v-model="formData.model" placeholder="请输入规格型号" />
</el-form-item>
<el-form-item label="条码" prop="barcode">
<el-input
v-model="formData.barcode"
:placeholder="isEdit ? '请输入条码' : '留空自动生成'"
/>
</el-form-item>
<el-form-item label="税率(%)">
<el-input-number
v-model="formData.taxRate"
:precision="0"
:min="0"
:max="100"
controls-position="right"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="初始库存">
<el-input-number
v-model="formData.stock"
:min="0"
controls-position="right"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="库存预警值">
<el-input-number
v-model="formData.lowStock"
:min="0"
controls-position="right"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="状态">
<el-select v-model="formData.status" placeholder="请选择状态" style="width: 100%">
<el-option
v-for="opt in dictStore.getOptions('crm_product_status')"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</el-select>
</el-form-item>
<el-form-item label="是否易制毒">
<el-switch v-model="formData.isToxic" :active-value="1" :inactive-value="0" />
</el-form-item>
<el-form-item label="是否易制爆">
<el-switch v-model="formData.isExplosive" :active-value="1" :inactive-value="0" />
</el-form-item>
<el-form-item label="产品图片" class="form-col-span-2">
<AttachmentUploadField
ref="productImagesUploadRef"
v-model="formData.images"
:limit="5"
accept="image/*"
folder="crm/product"
hint="支持 JPG、PNG、WebP 等图片,最多 5 张"
/>
</el-form-item>
<el-form-item label="产品描述" style="grid-column: 1 / -1">
<el-input
v-model="formData.description"
type="textarea"
:rows="3"
placeholder="请输入产品描述"
/>
</el-form-item>
<el-form-item label="备注" style="grid-column: 1 / -1">
<el-input v-model="formData.remark" type="textarea" :rows="2" placeholder="请输入备注" />
</el-form-item>
</div>
<div class="sub-card-section">
<div class="sub-card-header">
<span class="sub-card-title">品牌</span>
<el-button type="primary" size="small" @click="handleAddBrand">
<el-icon><Plus /></el-icon>新增品牌
</el-button>
</div>
<div v-if="formData.brands.length" class="sub-card-grid">
<div v-for="(row, index) in formData.brands" :key="`brand-${index}`" class="sub-card">
<el-input v-model="row.brandName" placeholder="品牌名称" class="sub-card-name" />
<el-select v-model="row.status" placeholder="状态" class="sub-card-status">
<el-option
v-for="opt in dictStore.getOptions('crm_product_status')"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</el-select>
<el-button link type="danger" class="sub-card-remove" @click="handleRemoveBrand(index)">
<el-icon><Delete /></el-icon>
</el-button>
</div>
</div>
<div v-else class="sub-card-empty">暂无品牌,点击上方新增</div>
</div>
<div class="sub-card-section">
<div class="sub-card-header">
<span class="sub-card-title">别名</span>
<el-button type="primary" size="small" @click="handleAddAlias">
<el-icon><Plus /></el-icon>新增别名
</el-button>
</div>
<div v-if="formData.aliases.length" class="sub-card-grid">
<div
v-for="(row, index) in formData.aliases"
:key="`alias-${index}`"
class="sub-card sub-card--alias"
>
<el-input v-model="row.aliasName" placeholder="别名" class="sub-card-name" />
<el-button link type="danger" class="sub-card-remove" @click="handleRemoveAlias(index)">
<el-icon><Delete /></el-icon>
</el-button>
</div>
</div>
<div v-else class="sub-card-empty">暂无别名,点击上方新增</div>
</div>
</el-form>
<template #footer>
<el-button class="btn-ghost" @click="visible = false">
<el-icon><Close /></el-icon>取消
</el-button>
<el-button class="btn-main" type="primary" :loading="submitLoading" @click="submitForm">
<el-icon><Check /></el-icon>确定
</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { Check, Close, Delete, Plus, Refresh } from '@element-plus/icons-vue'
import type { FormInstance } from 'element-plus'
import { ElMessage } from 'element-plus'
import AttachmentUploadField from '@/components/business/attachment-upload-field/index.vue'
import {
fetchCategoryTree,
fetchCreateProduct,
fetchProductDetail,
fetchUpdateProduct,
type ProductAliasItem,
type ProductBrandItem
} from '@/api/crm/product'
import { useDictStore } from '@/store/modules/dict'
defineOptions({ name: 'ProductFormDialog' })
const props = defineProps<{
modelValue: boolean
/** 有值则为编辑,无值则为新增 */
productId?: string | null
}>()
const emit = defineEmits<{
'update:modelValue': [value: boolean]
success: [product: Record<string, any>]
}>()
const dictStore = useDictStore()
const visible = computed({
get: () => props.modelValue,
set: (value: boolean) => emit('update:modelValue', value)
})
const isEdit = computed(() => !!props.productId)
const formRef = ref<FormInstance>()
const productImagesUploadRef = ref<InstanceType<typeof AttachmentUploadField>>()
const dialogLoading = ref(false)
const submitLoading = ref(false)
const categoryOptions = ref<any[]>([])
const formData = reactive({
id: '',
productName: '',
namePinyinAbbr: '',
productNo: '',
categoryId: '',
model: '',
barcode: '',
stock: 0,
lowStock: 10,
taxRate: 13,
status: 'on',
isToxic: 0,
isExplosive: 0,
description: '',
images: '',
remark: '',
brands: [] as ProductBrandItem[],
aliases: [] as ProductAliasItem[]
})
const formRules = {
productName: [{ required: true, message: '产品名称不能为空', trigger: 'blur' }],
barcode: [
{
validator: (_rule: any, value: string, callback: (err?: Error) => void) => {
if (!value) {
callback()
return
}
if (!/^\d+$/.test(value)) {
callback(new Error('条码必须为数字'))
return
}
callback()
},
trigger: 'blur'
}
]
}
function resetFormData() {
formData.id = ''
formData.productName = ''
formData.namePinyinAbbr = ''
formData.productNo = ''
formData.categoryId = ''
formData.model = ''
formData.barcode = ''
formData.stock = 0
formData.lowStock = 10
formData.taxRate = 13
formData.status = 'on'
formData.isToxic = 0
formData.isExplosive = 0
formData.description = ''
formData.images = ''
formData.remark = ''
formData.brands = []
formData.aliases = []
}
function regeneratePinyinAbbr() {
formData.namePinyinAbbr = ''
ElMessage.info('已清空,保存时将按产品名称重新生成拼音首字母')
}
async function loadCategoryOptions() {
try {
const res = await fetchCategoryTree()
categoryOptions.value = (res as any)?.data || res || []
} catch {
categoryOptions.value = []
}
}
async function handleOpen() {
resetFormData()
await loadCategoryOptions()
if (!props.productId) {
return
}
dialogLoading.value = true
try {
const res = await fetchProductDetail({ id: String(props.productId) })
const data = (res as any)?.data || res || {}
Object.assign(formData, {
id: data.id || '',
productName: data.productName || '',
namePinyinAbbr: data.namePinyinAbbr || '',
productNo: data.productNo || '',
categoryId: data.categoryId || '',
model: data.model || '',
barcode: data.barcode || '',
stock: data.stock ?? 0,
lowStock: data.lowStock ?? 10,
taxRate: data.taxRate ?? 13,
status: data.status || 'on',
isToxic: data.isToxic ?? 0,
isExplosive: data.isExplosive ?? 0,
description: data.description || '',
images: data.images || '',
remark: data.remark || '',
brands: (data.brands || []).map((item: ProductBrandItem) => ({
id: item.id,
brandName: item.brandName || '',
status: item.status || 'on',
orderNum: item.orderNum
})),
aliases: (data.aliases || []).map((item: ProductAliasItem) => ({
id: item.id,
aliasName: item.aliasName || '',
orderNum: item.orderNum
}))
})
} catch {
ElMessage.error('加载产品详情失败')
visible.value = false
} finally {
dialogLoading.value = false
}
}
function handleClose() {
resetFormData()
formRef.value?.resetFields()
}
function handleAddBrand() {
formData.brands.push({ brandName: '', status: 'on' })
}
function handleRemoveBrand(index: number) {
formData.brands.splice(index, 1)
}
function handleAddAlias() {
formData.aliases.push({ aliasName: '' })
}
function handleRemoveAlias(index: number) {
formData.aliases.splice(index, 1)
}
async function submitForm() {
if (productImagesUploadRef.value?.hasUploading?.()) {
ElMessage.warning('附件上传中,请稍候')
return
}
const valid = await formRef.value?.validate().catch(() => false)
if (!valid) return
submitLoading.value = true
try {
const data: Record<string, any> = {
productName: formData.productName,
// 显式传空串:后端按「重新生成」处理;有值则手改入库
namePinyinAbbr: (formData.namePinyinAbbr || '').trim(),
productNo: formData.productNo || undefined,
categoryId: formData.categoryId || undefined,
model: formData.model || undefined,
barcode: formData.barcode || undefined,
stock: formData.stock ?? 0,
lowStock: formData.lowStock ?? 10,
taxRate: formData.taxRate,
status: formData.status,
isToxic: formData.isToxic,
isExplosive: formData.isExplosive,
description: formData.description || undefined,
images: formData.images || undefined,
remark: formData.remark || undefined,
brands: formData.brands
.filter((item) => item.brandName?.trim())
.map((item, index) => ({
...(item.id ? { id: item.id } : {}),
brandName: item.brandName.trim(),
status: item.status || 'on',
orderNum: item.orderNum ?? index + 1
})),
aliases: formData.aliases
.filter((item) => item.aliasName?.trim())
.map((item, index) => ({
...(item.id ? { id: item.id } : {}),
aliasName: item.aliasName.trim(),
orderNum: item.orderNum ?? index + 1
}))
}
let product: Record<string, any> = {}
if (isEdit.value) {
const res = await fetchUpdateProduct({ id: formData.id, ...data })
product = (res as any)?.data || res || { id: formData.id, ...data }
ElMessage.success('修改成功')
} else {
const res = await fetchCreateProduct(data)
product = (res as any)?.data || res || data
ElMessage.success('新增成功')
}
visible.value = false
emit('success', product)
} finally {
submitLoading.value = false
}
}
</script>
<style scoped lang="scss">
.pinyin-abbr-row {
display: flex;
gap: 8px;
align-items: center;
width: 100%;
.el-input {
flex: 1;
min-width: 0;
}
}
.sub-card-section {
margin-top: 16px;
}
.sub-card-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
}
.sub-card-title {
font-size: 14px;
font-weight: 500;
color: var(--text, #1e293b);
}
.sub-card-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
}
.sub-card {
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
padding: 10px 12px;
background: var(--mint-surface, #f8fafc);
border-radius: 8px;
}
.sub-card-name {
flex: 1;
min-width: 0;
}
.sub-card-status {
flex: 0 0 96px;
width: 96px;
}
.sub-card-remove {
flex-shrink: 0;
padding: 4px;
}
.sub-card-empty {
padding: 16px 12px;
font-size: 13px;
color: var(--mint-text-muted, #94a3b8);
text-align: center;
background: var(--mint-surface, #f8fafc);
border-radius: 8px;
}
@media (max-width: 720px) {
.sub-card-grid {
grid-template-columns: minmax(0, 1fr);
}
}
</style>
@@ -0,0 +1,508 @@
<!-- eslint-disable vue/no-v-model-argument -->
<template>
<div class="product-selector-root">
<el-dialog
title="选择产品"
v-model="dialogVisible"
width="80%"
align-center
append-to-body
class="dialog-form-layout"
modal-class="order-module-dialog-mask"
style="backdrop-filter: none"
:close-on-click-modal="false"
>
<div class="panel dialog-list-panel">
<div class="list-toolbar product-selector-toolbar">
<span class="selected-hint">已选 {{ selectedProducts.length }} 项</span>
<ListQuickSearchInput
v-model="quickSearchKeyword"
placeholder="产品名称/拼音首字母"
@search="handleQuickSearch"
@clear="handleQuickSearchClear"
/>
<el-button class="btn-main" type="primary" @click="handleAddProduct">
<el-icon><Plus /></el-icon>新增
</el-button>
<el-button
class="btn-ghost"
:disabled="selectedProducts.length !== 1"
@click="handleToolbarEdit"
>
<el-icon><Edit /></el-icon>编辑
</el-button>
</div>
<div class="list-body list-wrap dialog-list-body-only">
<ListTable
table-key="business-product-selector"
ref="tableRef"
v-loading="loading"
:data="productList"
class="list-table"
stripe
row-key="id"
@selection-change="handleSelectionChange"
:max-height="400"
>
<el-table-column type="selection" min-width="55" align="center" header-align="center" />
<el-table-column
type="index"
label="序号"
min-width="60"
align="center"
header-align="center"
/>
<el-table-column
label="产品编号"
prop="productNo"
min-width="140"
align="center"
header-align="center"
:show-overflow-tooltip="true"
/>
<el-table-column
label="产品名称"
prop="productName"
min-width="200"
align="left"
header-align="left"
class-name="col-name-wrap"
label-class-name="col-name-wrap"
/>
<el-table-column
label="品牌"
min-width="140"
align="left"
header-align="left"
class-name="col-name-wrap product-selector-wrap-col"
label-class-name="col-name-wrap"
>
<template #default="{ row }">
{{ formatProductBrands(row.brands) }}
</template>
</el-table-column>
<el-table-column
label="别名"
min-width="140"
align="left"
header-align="left"
class-name="col-name-wrap product-selector-wrap-col"
label-class-name="col-name-wrap"
>
<template #default="{ row }">
{{ formatProductAliases(row.aliases) }}
</template>
</el-table-column>
<el-table-column
label="产品分类"
prop="categoryName"
min-width="120"
align="center"
header-align="center"
/>
<el-table-column
label="规格型号"
prop="model"
min-width="120"
align="center"
header-align="center"
:show-overflow-tooltip="true"
/>
<el-table-column
label="计量单位"
prop="unit"
min-width="100"
align="center"
header-align="center"
>
<template #default="scope">
<DictTag dict-type="crm_product_unit" :value="scope.row.unit" />
</template>
</el-table-column>
<el-table-column
label="税率(%)"
prop="taxRate"
min-width="90"
align="center"
header-align="center"
>
<template #default="scope">
<span>{{ scope.row.taxRate || 0 }}%</span>
</template>
</el-table-column>
<el-table-column
label="状态"
prop="status"
min-width="90"
align="center"
header-align="center"
>
<template #default="scope">
<el-tag
:type="dictStore.getTagType('crm_product_status', scope.row.status)"
size="small"
>
{{ dictStore.getLabel('crm_product_status', scope.row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column
label="操作"
min-width="100"
align="center"
header-align="center"
fixed="right"
>
<template #default="{ row }">
<el-button link type="primary" @click="handleRowEdit(row)">
<el-icon><Edit /></el-icon>编辑
</el-button>
</template>
</el-table-column>
</ListTable>
<ListPagination
v-show="total > 0"
:total="total"
v-model:page="queryParams.pageNum"
v-model:limit="queryParams.pageSize"
@pagination="getList"
class="pagination list-pager-bar"
/>
</div>
</div>
<el-divider content-position="left">已选产品 ({{ selectedProducts.length }})</el-divider>
<div class="selected-products">
<el-tag
v-for="product in selectedProducts"
:key="product.id"
closable
@close="handleRemoveSelection(product)"
class="mr-2 mb-2"
>
{{ product.productName }}
</el-tag>
<span v-if="selectedProducts.length === 0" class="text-muted">暂无选择</span>
</div>
<template #footer>
<div class="dialog-footer">
<el-button class="btn-ghost" @click="handleClose">
<el-icon><Close /></el-icon>取消
</el-button>
<el-button
class="btn-main"
type="primary"
@click="handleConfirm"
:disabled="selectedProducts.length === 0"
>
<el-icon><Check /></el-icon>确定 ({{ selectedProducts.length }})
</el-button>
</div>
</template>
</el-dialog>
<ProductFormDialog
v-model="formDialogVisible"
:product-id="formProductId"
@success="handleFormSuccess"
/>
</div>
</template>
<script setup lang="ts">
import { Check, Close, Edit, Plus } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import DictTag from '@/components/DictTag/index.vue'
import ListPagination from '@/components/ListPagination/index.vue'
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
import ProductFormDialog from '@/components/business/product-form-dialog/index.vue'
import {
fetchCategoryTree,
fetchProductList,
type ProductAliasItem,
type ProductBrandItem
} from '@/api/crm/product'
import { useDictStore } from '@/store/modules/dict'
defineOptions({ name: 'ProductSelector' })
interface ProductVO {
id: string | number
productNo?: string
productName?: string
categoryId?: string | number
categoryName?: string
model?: string
unit?: string
taxRate?: number
status?: string
brands?: ProductBrandItem[]
aliases?: ProductAliasItem[]
}
interface ProductCategoryVO {
id: string | number
categoryName: string
children?: ProductCategoryVO[]
}
const props = defineProps<{
visible: boolean
supplierId?: string | number
}>()
const emit = defineEmits<{
'update:visible': [value: boolean]
confirm: [products: ProductVO[]]
}>()
const dictStore = useDictStore()
const dialogVisible = ref(false)
const formDialogVisible = ref(false)
const formProductId = ref<string | null>(null)
const pendingSelectId = ref<string | null>(null)
watch(
() => props.visible,
(val) => {
dialogVisible.value = val
if (val) {
quickSearchKeyword.value = ''
queryParams.value.pageNum = 1
getList()
loadCategoryTree()
}
}
)
watch(dialogVisible, (val) => {
emit('update:visible', val)
})
const quickSearchKeyword = ref('')
const queryParams = ref({
pageNum: 1,
pageSize: 20,
status: 'on'
})
const productList = ref<ProductVO[]>([])
const loading = ref(false)
const total = ref(0)
const categoryTree = ref<any[]>([])
const selectedProducts = ref<ProductVO[]>([])
const tableRef = ref<any>()
function formatProductBrands(brands?: ProductBrandItem[]) {
if (!brands?.length) return '-'
const names = brands.map((b) => b.brandName).filter(Boolean)
return names.length ? names.join('、') : '-'
}
function formatProductAliases(aliases?: ProductAliasItem[]) {
if (!aliases?.length) return '-'
const names = aliases.map((a) => a.aliasName).filter(Boolean)
return names.length ? names.join('、') : '-'
}
const getList = async () => {
loading.value = true
try {
const keyword = quickSearchKeyword.value?.trim()
const res = await fetchProductList({
page: queryParams.value.pageNum,
pageSize: queryParams.value.pageSize,
keyword: keyword || undefined,
status: queryParams.value.status,
supplierId:
props.supplierId != null && props.supplierId !== '' ? String(props.supplierId) : undefined
})
const data = (res as any)?.data || res || {}
productList.value = data.list || data.rows || []
total.value = data.total ?? 0
if (categoryTree.value.length > 0) {
resolveCategoryNames()
}
await nextTick()
await selectPendingRow()
} catch (error) {
console.error('获取产品列表失败:', error)
} finally {
loading.value = false
}
}
const loadCategoryTree = async () => {
try {
const res = await fetchCategoryTree()
const tree = (res as any)?.data || res || []
categoryTree.value = convertTreeData(tree)
resolveCategoryNames()
} catch (error) {
console.error('获取产品分类失败:', error)
}
}
const convertTreeData = (data: ProductCategoryVO[]): any[] => {
return data.map((item) => ({
value: item.id,
label: item.categoryName,
children: item.children ? convertTreeData(item.children) : undefined
}))
}
const resolveCategoryName = (categoryId: any, tree: any[]): string => {
for (const node of tree) {
if (node.value === categoryId || node.id === categoryId) return node.label || ''
if (node.children) {
const found = resolveCategoryName(categoryId, node.children)
if (found) return found
}
}
return ''
}
const resolveCategoryNames = () => {
if (!categoryTree.value.length) return
productList.value.forEach((product) => {
if (!product.categoryName && product.categoryId != null) {
product.categoryName = resolveCategoryName(product.categoryId, categoryTree.value)
}
})
}
async function selectPendingRow() {
const id = pendingSelectId.value
if (!id) return
pendingSelectId.value = null
const row = productList.value.find((p) => String(p.id) === id)
const table = tableRef.value
if (!row || !table?.toggleRowSelection) return
table.toggleRowSelection(row, true)
}
function handleQuickSearch() {
queryParams.value.pageNum = 1
getList()
}
function handleQuickSearchClear() {
quickSearchKeyword.value = ''
queryParams.value.pageNum = 1
getList()
}
const handleSelectionChange = (selection: ProductVO[]) => {
selectedProducts.value = selection
}
const handleRemoveSelection = (product: ProductVO) => {
const index = selectedProducts.value.findIndex((p) => p.id === product.id)
if (index > -1) {
selectedProducts.value.splice(index, 1)
}
const table = tableRef.value
const row = productList.value.find((p) => String(p.id) === String(product.id))
if (table?.toggleRowSelection && row) {
table.toggleRowSelection(row, false)
}
}
function handleAddProduct() {
formProductId.value = null
formDialogVisible.value = true
}
function handleToolbarEdit() {
if (selectedProducts.value.length !== 1) {
ElMessage.warning('请选择一条数据进行编辑')
return
}
handleRowEdit(selectedProducts.value[0])
}
function handleRowEdit(row: ProductVO) {
formProductId.value = row?.id != null ? String(row.id) : null
formDialogVisible.value = true
}
async function handleFormSuccess(product: Record<string, any>) {
const wasCreate = !formProductId.value
const id = product?.id != null ? String(product.id) : null
if (wasCreate && id) {
pendingSelectId.value = id
} else if (id) {
selectedProducts.value = selectedProducts.value.map((p) =>
String(p.id) === id
? { ...p, ...product, productName: product.productName || p.productName }
: p
)
}
await getList()
}
const handleConfirm = () => {
emit('confirm', selectedProducts.value)
handleClose()
}
const handleClose = () => {
selectedProducts.value = []
pendingSelectId.value = null
quickSearchKeyword.value = ''
formDialogVisible.value = false
formProductId.value = null
dialogVisible.value = false
}
</script>
<style scoped lang="scss">
.product-selector-toolbar {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;
padding: 10px 12px 12px;
margin-bottom: 12px;
:deep(.quick-search-input) {
width: 220px;
flex-shrink: 0;
}
}
.selected-hint {
margin-right: 4px;
font-size: 13px;
color: var(--mint-text-muted, #64748b);
}
.selected-products {
min-height: 40px;
padding: 10px;
background-color: #f5f7fa;
border-radius: 4px;
}
.text-muted {
color: #1e293b;
}
.mr-2 {
margin-right: 8px;
}
.mb-2 {
margin-bottom: 8px;
}
:deep(.product-selector-wrap-col .cell) {
white-space: normal;
word-break: break-word;
line-height: 1.45;
}
</style>
File diff suppressed because it is too large. Load diff