|
|
@@ -0,0 +1,299 @@
|
|
|
+<!-- eslint-disable vue/no-v-model-argument -->
|
|
|
+<template>
|
|
|
+ <el-dialog
|
|
|
+ v-model="visible"
|
|
|
+ :title="dialogTitle"
|
|
|
+ width="600px"
|
|
|
+ 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"
|
|
|
+ >
|
|
|
+ <div v-loading="dialogLoading">
|
|
|
+ <div v-if="props.mode === 'alias'" 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="aliases.length" class="sub-card-grid">
|
|
|
+ <div
|
|
|
+ v-for="(row, index) in 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>
|
|
|
+
|
|
|
+ <div v-else 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="brands.length" class="sub-card-grid">
|
|
|
+ <div v-for="(row, index) in 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>
|
|
|
+ <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 } from '@element-plus/icons-vue'
|
|
|
+ import { ElMessage } from 'element-plus'
|
|
|
+ import {
|
|
|
+ fetchProductDetail,
|
|
|
+ fetchUpdateProduct,
|
|
|
+ type ProductAliasItem,
|
|
|
+ type ProductBrandItem
|
|
|
+ } from '@/api/crm/product'
|
|
|
+ import { useDictStore } from '@/store/modules/dict'
|
|
|
+
|
|
|
+ defineOptions({ name: 'ProductAliasBrandDialog' })
|
|
|
+
|
|
|
+ const props = defineProps<{
|
|
|
+ modelValue: boolean
|
|
|
+ productId: string
|
|
|
+ mode: 'alias' | 'brand'
|
|
|
+ productName?: string
|
|
|
+ }>()
|
|
|
+
|
|
|
+ const emit = defineEmits<{
|
|
|
+ 'update:modelValue': [boolean]
|
|
|
+ saved: [product: Record<string, any>]
|
|
|
+ }>()
|
|
|
+
|
|
|
+ const dictStore = useDictStore()
|
|
|
+
|
|
|
+ const visible = computed({
|
|
|
+ get: () => props.modelValue,
|
|
|
+ set: (value: boolean) => emit('update:modelValue', value)
|
|
|
+ })
|
|
|
+
|
|
|
+ const dialogLoading = ref(false)
|
|
|
+ const submitLoading = ref(false)
|
|
|
+ const loadedProductName = ref('')
|
|
|
+ const aliases = ref<ProductAliasItem[]>([])
|
|
|
+ const brands = ref<ProductBrandItem[]>([])
|
|
|
+
|
|
|
+ const dialogTitle = computed(() => {
|
|
|
+ const name = props.productName || loadedProductName.value || ''
|
|
|
+ const prefix = props.mode === 'alias' ? '维护别名' : '维护品牌'
|
|
|
+ return name ? `${prefix} - ${name}` : prefix
|
|
|
+ })
|
|
|
+
|
|
|
+ function resetLists() {
|
|
|
+ aliases.value = []
|
|
|
+ brands.value = []
|
|
|
+ loadedProductName.value = ''
|
|
|
+ }
|
|
|
+
|
|
|
+ async function handleOpen() {
|
|
|
+ resetLists()
|
|
|
+ if (!props.productId) {
|
|
|
+ ElMessage.error('加载产品详情失败')
|
|
|
+ visible.value = false
|
|
|
+ return
|
|
|
+ }
|
|
|
+ dialogLoading.value = true
|
|
|
+ try {
|
|
|
+ const res = await fetchProductDetail({ id: String(props.productId) })
|
|
|
+ const data = (res as any)?.data || res || {}
|
|
|
+ loadedProductName.value = data.productName || ''
|
|
|
+ aliases.value = (data.aliases || []).map((item: ProductAliasItem) => ({
|
|
|
+ id: item.id,
|
|
|
+ aliasName: item.aliasName || '',
|
|
|
+ orderNum: item.orderNum
|
|
|
+ }))
|
|
|
+ brands.value = (data.brands || []).map((item: ProductBrandItem) => ({
|
|
|
+ id: item.id,
|
|
|
+ brandName: item.brandName || '',
|
|
|
+ status: item.status || 'on',
|
|
|
+ orderNum: item.orderNum
|
|
|
+ }))
|
|
|
+ } catch {
|
|
|
+ ElMessage.error('加载产品详情失败')
|
|
|
+ visible.value = false
|
|
|
+ } finally {
|
|
|
+ dialogLoading.value = false
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ function handleClose() {
|
|
|
+ resetLists()
|
|
|
+ }
|
|
|
+
|
|
|
+ function handleAddAlias() {
|
|
|
+ aliases.value.push({ aliasName: '' })
|
|
|
+ }
|
|
|
+
|
|
|
+ function handleRemoveAlias(index: number) {
|
|
|
+ aliases.value.splice(index, 1)
|
|
|
+ }
|
|
|
+
|
|
|
+ function handleAddBrand() {
|
|
|
+ brands.value.push({ brandName: '', status: 'on' })
|
|
|
+ }
|
|
|
+
|
|
|
+ function handleRemoveBrand(index: number) {
|
|
|
+ brands.value.splice(index, 1)
|
|
|
+ }
|
|
|
+
|
|
|
+ function hasDuplicateNames(names: string[]) {
|
|
|
+ const seen = new Set<string>()
|
|
|
+ for (const name of names) {
|
|
|
+ if (seen.has(name)) return true
|
|
|
+ seen.add(name)
|
|
|
+ }
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ async function submitForm() {
|
|
|
+ if (!props.productId) return
|
|
|
+
|
|
|
+ let payload: Record<string, any>
|
|
|
+ if (props.mode === 'alias') {
|
|
|
+ const nextAliases = aliases.value
|
|
|
+ .filter((item) => item.aliasName?.trim())
|
|
|
+ .map((item, index) => ({
|
|
|
+ ...(item.id ? { id: item.id } : {}),
|
|
|
+ aliasName: item.aliasName.trim(),
|
|
|
+ orderNum: item.orderNum ?? index + 1
|
|
|
+ }))
|
|
|
+ if (hasDuplicateNames(nextAliases.map((item) => item.aliasName))) {
|
|
|
+ ElMessage.warning('别名不能重复')
|
|
|
+ return
|
|
|
+ }
|
|
|
+ payload = { id: props.productId, aliases: nextAliases }
|
|
|
+ } else {
|
|
|
+ const nextBrands = brands.value
|
|
|
+ .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
|
|
|
+ }))
|
|
|
+ if (hasDuplicateNames(nextBrands.map((item) => item.brandName))) {
|
|
|
+ ElMessage.warning('品牌不能重复')
|
|
|
+ return
|
|
|
+ }
|
|
|
+ payload = { id: props.productId, brands: nextBrands }
|
|
|
+ }
|
|
|
+
|
|
|
+ submitLoading.value = true
|
|
|
+ try {
|
|
|
+ await fetchUpdateProduct(payload)
|
|
|
+ const detailRes = await fetchProductDetail({ id: String(props.productId) })
|
|
|
+ const product = (detailRes as any)?.data || detailRes || {}
|
|
|
+ ElMessage.success('保存成功')
|
|
|
+ visible.value = false
|
|
|
+ emit('saved', product)
|
|
|
+ } catch {
|
|
|
+ // keep dialog open on API error
|
|
|
+ } finally {
|
|
|
+ submitLoading.value = false
|
|
|
+ }
|
|
|
+ }
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped lang="scss">
|
|
|
+ .sub-card-section {
|
|
|
+ margin-top: 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ .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>
|