b5c67988a6
- 在商品模型中添加所属专区字段 sectionIds,支持逗号分隔字符串 - 商品编辑界面增加所属专区多选下拉框,联动专区数据 - 新增首页专区相关接口,包括分页查询、增删改查及权限校验等 - 实现首页专区列表页面,支持专区的增删改查及状态切换 - 新增专区白名单用户管理弹窗,支持多选用户设置白名单 - 实现专区商品列表抽屉,支持分页查看专区内商品 - 首页专区编辑弹窗支持标题、副标题、banner等多字段编辑及上传功能 - 完善专区管理界面交互和表格展示,支持搜索和状态筛选功能
2140 lines
70 KiB
Vue
2140 lines
70 KiB
Vue
<!-- 编辑弹窗 -->
|
||
<template>
|
||
<a-drawer
|
||
width="70%"
|
||
:visible="visible"
|
||
:maskClosable="false"
|
||
:maxable="maxable"
|
||
:title="isUpdate ? '编辑商品' : '添加商品'"
|
||
:body-style="{ paddingBottom: '28px' }"
|
||
@update:visible="updateVisible"
|
||
@ok="save"
|
||
>
|
||
<template #extra>
|
||
<a-button type="primary" style="margin-right: 8px" @click="save">保存</a-button>
|
||
</template>
|
||
<a-form
|
||
ref="formRef"
|
||
:model="form"
|
||
:rules="rules"
|
||
:label-col="styleResponsive ? { md: 3, sm: 5, xs: 24 } : { flex: '90px' }"
|
||
:wrapper-col="
|
||
styleResponsive ? { md: 24, sm: 24, xs: 24 } : { flex: '1' }
|
||
"
|
||
>
|
||
<a-tabs type="card" v-model:active-key="active" @change="onChange">
|
||
<a-tab-pane tab="基本信息" key="base">
|
||
<a-form-item label="商品名称" name="name">
|
||
<a-input
|
||
allow-clear
|
||
style="width: 558px"
|
||
placeholder="请输入商品名称"
|
||
v-model:value="form.name"
|
||
/>
|
||
</a-form-item>
|
||
<a-form-item label="商品分类" name="categoryIds">
|
||
<a-tree-select
|
||
allow-clear
|
||
multiple
|
||
:max-tag-count="3"
|
||
:tree-data="navigationList"
|
||
tree-default-expand-all
|
||
style="width: 320px"
|
||
placeholder="请选择分类(可多选)"
|
||
:value="form.categoryIds && form.categoryIds.length ? form.categoryIds : undefined"
|
||
:listHeight="700"
|
||
:dropdown-style="{ overflow: 'auto' }"
|
||
@update:value="(value?: number[]) => (form.categoryIds = value || [])"
|
||
@change="onCategoryIds"
|
||
/>
|
||
</a-form-item>
|
||
<a-form-item label="所属专区" name="sectionIds">
|
||
<a-select
|
||
v-model:value="sectionIdList"
|
||
mode="multiple"
|
||
:max-tag-count="3"
|
||
:options="zoneOptions"
|
||
style="width: 320px"
|
||
placeholder="请选择所属专区(可多选)"
|
||
allow-clear
|
||
/>
|
||
</a-form-item>
|
||
<a-form-item label="商品编码" name="code">
|
||
<a-input
|
||
allow-clear
|
||
style="width: 558px"
|
||
placeholder="请输入商品编码"
|
||
v-model:value="form.code"
|
||
/>
|
||
</a-form-item>
|
||
<a-form-item label="商品卖点" name="comments">
|
||
<a-textarea
|
||
:rows="1"
|
||
:maxlength="100"
|
||
style="width: 558px"
|
||
show-count
|
||
placeholder="此款商品美观大方 性价比较高 不容错过"
|
||
v-model:value="form.comments"
|
||
/>
|
||
</a-form-item>
|
||
<a-form-item label="单位名称" name="unitName">
|
||
<a-input
|
||
allow-clear
|
||
style="width: 240px"
|
||
placeholder="单位名称,如(个)"
|
||
v-model:value="form.unitName"
|
||
/>
|
||
</a-form-item>
|
||
<a-form-item label="商城价" name="price">
|
||
<a-space>
|
||
<a-input-number
|
||
:placeholder="`商城价`"
|
||
style="width: 240px"
|
||
:min="0.01"
|
||
v-model:value="form.price"
|
||
/>
|
||
</a-space>
|
||
</a-form-item>
|
||
<a-form-item label="市场价" name="salePrice">
|
||
<a-input-number
|
||
:placeholder="`市场价`"
|
||
style="width: 240px"
|
||
:min="0.01"
|
||
v-model:value="form.salePrice"
|
||
/>
|
||
</a-form-item>
|
||
<a-form-item label="会员价" name="originPrice">
|
||
<a-input-number
|
||
:placeholder="`会员价`"
|
||
style="width: 240px"
|
||
:min="0.01"
|
||
v-model:value="form.dealerPrice"
|
||
/>
|
||
</a-form-item>
|
||
<a-form-item label="进货价格" name="originPrice">
|
||
<a-input-number
|
||
:placeholder="`进货价格`"
|
||
style="width: 240px"
|
||
:min="0.01"
|
||
v-model:value="form.buyingPrice"
|
||
/>
|
||
</a-form-item>
|
||
<a-form-item :label="form.canExpress === 1 ? '运费模板' : '运费模板'" v-if="!merchantId" :required="form.canExpress === 1">
|
||
<a-select
|
||
v-model:value="form.expressTemplateId"
|
||
style="width: 240px"
|
||
:placeholder="form.canExpress === 1 ? '请选择运费模板' : '请选择运费模板'"
|
||
>
|
||
<a-select-option
|
||
v-for="(item, index) in expressTemplateList"
|
||
:key="index"
|
||
:value="item.id"
|
||
>{{ item.title }}
|
||
</a-select-option>
|
||
</a-select>
|
||
</a-form-item>
|
||
<a-form-item label="商品图片(400x400)" name="image">
|
||
<!-- <SelectFile-->
|
||
<!-- :placeholder="`请选择视频文件`"-->
|
||
<!-- :limit="1"-->
|
||
<!-- :data="images"-->
|
||
<!-- @done="chooseImage"-->
|
||
<!-- @del="onDeleteItem"-->
|
||
<!-- />-->
|
||
<!-- 视频预览 -->
|
||
<div
|
||
v-if="form.image && isVideo(form.image)"
|
||
class="relative"
|
||
style="width: 100px; height: 100px; margin-right: 10px; border-radius: 6px; overflow: hidden; border: 1px solid #434343; background: #1a1a1a;"
|
||
>
|
||
<video
|
||
:src="form.image"
|
||
style="width: 100%; height: 100%; object-fit: cover;"
|
||
preload="metadata"
|
||
muted
|
||
/>
|
||
<!-- 播放图标覆盖层 -->
|
||
<div
|
||
class="absolute inset-0 flex items-center justify-center"
|
||
style="background: rgba(0,0,0,0.25);"
|
||
>
|
||
<PlayCircleOutlined style="font-size: 28px; color: rgba(255,255,255,0.85);" />
|
||
</div>
|
||
</div>
|
||
<!-- 图片预览 -->
|
||
<a-image
|
||
v-else-if="form.image"
|
||
width="100px"
|
||
height="100px"
|
||
:src="form.image"
|
||
style="margin-right: 10px"
|
||
/>
|
||
<a-upload
|
||
:show-upload-list="false"
|
||
:customRequest="onUploadImage"
|
||
ref="imageRef"
|
||
>
|
||
<a-button class="ele-btn-icon">
|
||
<template #icon>
|
||
<UploadOutlined/>
|
||
</template>
|
||
<span>上传</span>
|
||
</a-button>
|
||
</a-upload>
|
||
<div class="ele-text-placeholder">
|
||
支持上传视频(mp4格式),视频时长不超过60秒,视频大小不超过200M。
|
||
</div>
|
||
</a-form-item>
|
||
<a-form-item label="轮播图" name="files">
|
||
<div class="flex flex-wrap justify-start items-start">
|
||
<div
|
||
v-for="(item, index) in files"
|
||
:key="index"
|
||
style="margin-right: 10px; margin-bottom: 10px"
|
||
class="relative"
|
||
>
|
||
<img style="width: 100px; height: 100px" :src="item.url"/>
|
||
<div
|
||
class="absolute right-0 top-0 w-6 h-6 bg-red-400 flex justify-center items-center rounded-bl-lg"
|
||
@click="onDeleteFile(index)"
|
||
>
|
||
<delete-outlined style="color: white"/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<a-upload
|
||
:show-upload-list="false"
|
||
:customRequest="onUploadSwiper"
|
||
multiple
|
||
:max-count="9"
|
||
ref="swiperRef"
|
||
>
|
||
<a-button class="ele-btn-icon">
|
||
<template #icon>
|
||
<UploadOutlined/>
|
||
</template>
|
||
<span>上传轮播图</span>
|
||
</a-button>
|
||
</a-upload>
|
||
</a-form-item>
|
||
<a-form-item label="状态" name="status">
|
||
<a-radio-group v-model:value="form.status">
|
||
<a-radio :value="0">上架</a-radio>
|
||
<a-radio :value="1">下架</a-radio>
|
||
</a-radio-group>
|
||
</a-form-item>
|
||
</a-tab-pane>
|
||
<a-tab-pane tab="商品详情" key="content">
|
||
<RichTextEditor
|
||
v-if="active === 'content'"
|
||
v-model="content"
|
||
:height="520"
|
||
placeholder="请输入商品详情,支持图文混排..."
|
||
/>
|
||
</a-tab-pane>
|
||
<a-tab-pane tab="商品规格" key="spec">
|
||
<a-form-item label="规格类型" name="specs">
|
||
<a-space>
|
||
<a-radio-group v-model:value="form.specs">
|
||
<a-radio :value="0">单规格</a-radio>
|
||
<a-radio :value="1">多规格</a-radio>
|
||
</a-radio-group>
|
||
</a-space>
|
||
</a-form-item>
|
||
<a-form-item name="multipleSpec" v-if="form.specs == 1">
|
||
<div class="w-[300px] ml-10 flex items-center">
|
||
<SelectSpec
|
||
placeholder="选择规格"
|
||
:width="130"
|
||
v-model:value="form.specName"
|
||
@done="onSpec"
|
||
/>
|
||
<a-button type="link" size="small" @click="openSpecManage">
|
||
<template #icon><SettingOutlined /></template>
|
||
规格管理
|
||
</a-button>
|
||
</div>
|
||
</a-form-item>
|
||
<a-form-item name="specValue" v-if="form.specs == 1">
|
||
<a-space direction="vertical" class="ml-[40px]">
|
||
<template v-for="(item, index) in spec" :key="index">
|
||
<div
|
||
class="text-left flex items-center leading-10 text-gray-400"
|
||
>
|
||
<div class="mr-2">{{ item.value }} :</div>
|
||
<CloseCircleOutlined
|
||
class="cursor-pointer"
|
||
@click="onClose(index)"
|
||
/>
|
||
</div>
|
||
<ele-edit-tag
|
||
v-model:data="item.detail"
|
||
size="middle"
|
||
shape="round"
|
||
/>
|
||
</template>
|
||
<a-card class="ml-[40px]" v-if="showSpecForm">
|
||
<a-form-item name="name">
|
||
<a-input
|
||
allow-clear
|
||
placeholder="请输入规格,如:颜色"
|
||
v-model:value="name"
|
||
/>
|
||
</a-form-item>
|
||
<a-form-item name="value">
|
||
<a-input
|
||
allow-clear
|
||
placeholder="请输入规格值,如:红色"
|
||
v-model:value="value"
|
||
/>
|
||
</a-form-item>
|
||
<a-space>
|
||
<a-button type="primary" @click="addSpecValue">确定</a-button>
|
||
<a-button @click="openSpecForm">取消</a-button>
|
||
</a-space>
|
||
</a-card>
|
||
<a-space>
|
||
<a-button type="primary" class="mt-5" @click="openSpecForm"
|
||
>添加新规格
|
||
</a-button>
|
||
<a-button type="primary" class="mt-5" @click="generateSku" v-if="spec.length > 0"
|
||
>生成SKU
|
||
</a-button>
|
||
<a-tooltip title="方案1(推荐):编辑旧商品时,重新点击此按钮即可自动将规格数据转换为新格式显示">
|
||
<InfoCircleOutlined class="mt-5 ml-1 text-gray-400 cursor-help" />
|
||
</a-tooltip>
|
||
</a-space>
|
||
</a-space>
|
||
</a-form-item>
|
||
<a-form-item name="oneSpec" v-if="form.specs == 1">
|
||
<div class="w-full">
|
||
<!-- 批量设置 -->
|
||
<div class="batch-set-bar flex items-center gap-3 mb-4 p-3 bg-gray-50 rounded">
|
||
<span class="text-sm font-medium text-gray-600 whitespace-nowrap">批量设置:</span>
|
||
<a-input-number
|
||
v-model:value="batchPrice"
|
||
:precision="2"
|
||
:min="0"
|
||
style="width: 110px"
|
||
placeholder="商城价"
|
||
/>
|
||
<a-input-number
|
||
v-model:value="batchSalePrice"
|
||
:precision="2"
|
||
:min="0"
|
||
style="width: 110px"
|
||
placeholder="划线价格"
|
||
/>
|
||
<a-input-number
|
||
v-model:value="batchBuyingPrice"
|
||
:precision="2"
|
||
:min="0"
|
||
style="width: 110px"
|
||
placeholder="会员价"
|
||
/>
|
||
<a-input-number
|
||
v-model:value="batchStock"
|
||
:min="0"
|
||
style="width: 100px"
|
||
placeholder="库存"
|
||
/>
|
||
<a-input
|
||
v-model:value="batchSkuNo"
|
||
style="width: 120px"
|
||
placeholder="SKU编码"
|
||
/>
|
||
<a-button type="primary" @click="batchApply">立即设置</a-button>
|
||
</div>
|
||
<div class="sku-table">
|
||
<a-table
|
||
:pagination="false"
|
||
:dataSource="skuList"
|
||
:columns="columns"
|
||
:scroll="{ y: 500 }"
|
||
>
|
||
<template #bodyCell="{ record, column, index }">
|
||
<template v-if="column.key === 'line'">
|
||
{{ index + 1 }}
|
||
</template>
|
||
<template v-if="column.key === 'image'">
|
||
<SelectFile
|
||
:placeholder="`请选择商品图片`"
|
||
:limit="1"
|
||
:data="record.images || []"
|
||
:index="index"
|
||
@done="chooseSkuImage"
|
||
@del="() => onDeleteSkuItem(index)"
|
||
/>
|
||
</template>
|
||
<template v-if="column.key === 'price'">
|
||
<a-input
|
||
:placeholder="`成本价`"
|
||
v-model:value="skuList[index].price"
|
||
/>
|
||
</template>
|
||
<template v-if="column.key === 'salePrice'">
|
||
<a-input
|
||
:placeholder="`市场价`"
|
||
v-model:value="record.salePrice"
|
||
/>
|
||
</template>
|
||
<template v-if="column.key === 'buyingPrice'">
|
||
<a-input
|
||
:placeholder="`会员价`"
|
||
v-model:value="record.buyingPrice"
|
||
/>
|
||
</template>
|
||
<template v-if="column.key === 'stock'">
|
||
<a-input
|
||
:placeholder="`库存`"
|
||
v-model:value="record.stock"
|
||
/>
|
||
</template>
|
||
<template v-if="column.key === 'skuNo'">
|
||
<a-input
|
||
:placeholder="`编码`"
|
||
v-model:value="record.skuNo"
|
||
/>
|
||
</template>
|
||
</template>
|
||
</a-table>
|
||
</div>
|
||
</div>
|
||
</a-form-item>
|
||
</a-tab-pane>
|
||
<a-tab-pane tab="营销设置" key="coupon">
|
||
<a-form-item label="商品重量" name="goodsWeight">
|
||
<a-input
|
||
allow-clear
|
||
style="width: 250px"
|
||
placeholder="请输入商品重量"
|
||
v-model:value="form.goodsWeight"
|
||
/>
|
||
</a-form-item>
|
||
<a-form-item label="销量" name="sales">
|
||
<a-input-number
|
||
allow-clear
|
||
style="width: 250px"
|
||
placeholder="请输入销量"
|
||
v-model:value="form.sales"
|
||
/>
|
||
</a-form-item>
|
||
<a-form-item label="库存" name="stock">
|
||
<a-input-number
|
||
allow-clear
|
||
style="width: 250px"
|
||
placeholder="请输入库存"
|
||
v-model:value="form.stock"
|
||
/>
|
||
</a-form-item>
|
||
<a-form-item label="获取积分" name="gainIntegral">
|
||
<a-input-number
|
||
:placeholder="`消费获取的积分`"
|
||
style="width: 250px"
|
||
v-model:value="form.gainIntegral"
|
||
/>
|
||
</a-form-item>
|
||
<a-form-item label="库存计算方式" name="deductStockType">
|
||
<a-radio-group v-model:value="form.deductStockType">
|
||
<a-radio :value="20">付款减库存</a-radio>
|
||
<a-radio :value="10">下单减库存</a-radio>
|
||
</a-radio-group>
|
||
</a-form-item>
|
||
<a-form-item label="货架" name="position">
|
||
<a-input
|
||
allow-clear
|
||
style="width: 250px"
|
||
placeholder="请输入货架"
|
||
v-model:value="form.position"
|
||
/>
|
||
</a-form-item>
|
||
<a-form-item label="排序号" name="sortNumber">
|
||
<a-input-number
|
||
:min="0"
|
||
:max="9999"
|
||
style="width: 250px"
|
||
placeholder="请输入排序号"
|
||
v-model:value="form.sortNumber"
|
||
/>
|
||
</a-form-item>
|
||
<a-form-item label="是否可以快递配送">
|
||
<a-switch
|
||
size="small"
|
||
v-model:checked="form.canExpress"
|
||
:checked-value="1"
|
||
:un-checked-value="0"
|
||
/>
|
||
</a-form-item>
|
||
<a-form-item label="是否新品">
|
||
<a-switch
|
||
size="small"
|
||
v-model:checked="form.isNew"
|
||
:checked-value="1"
|
||
:un-checked-value="0"
|
||
/>
|
||
</a-form-item>
|
||
<a-form-item label="活动方式" name="activityType">
|
||
<a-radio-group v-model:value="form.activityType">
|
||
<a-radio :value="0">全平台(新老用户均可)</a-radio>
|
||
<a-radio :value="1">新用户专享(仅新用户可购)</a-radio>
|
||
</a-radio-group>
|
||
</a-form-item>
|
||
<a-form-item label="配送方式" name="deliveryMode">
|
||
<a-radio-group v-model:value="form.deliveryMode">
|
||
<a-radio :value="0">快递配送</a-radio>
|
||
<a-radio :value="1">限自提</a-radio>
|
||
</a-radio-group>
|
||
</a-form-item>
|
||
<!-- <a-form-item label="是否开启分红功能" v-if="!merchantId">-->
|
||
<!-- <a-switch-->
|
||
<!-- size="small"-->
|
||
<!-- v-model:checked="form.commissionRole"-->
|
||
<!-- :checked-value="1"-->
|
||
<!-- :un-checked-value="0"-->
|
||
<!-- />-->
|
||
<!-- </a-form-item>-->
|
||
<!-- <a-form-item label="角色分红配置" v-if="form.commissionRole === 1">-->
|
||
<!-- <a-space>-->
|
||
<!-- <a-input-->
|
||
<!-- v-model:value="form.goodsRoleCommission[index].amount"-->
|
||
<!-- v-for="(item, index) in form.goodsRoleCommission"-->
|
||
<!-- :key="index"-->
|
||
<!-- >-->
|
||
<!-- <template #addonBefore>{{ item.roleName }}</template>-->
|
||
<!-- <template #addonAfter>元</template>-->
|
||
<!-- </a-input>-->
|
||
<!-- </a-space>-->
|
||
<!-- </a-form-item>-->
|
||
|
||
<a-divider orientation="left">分销设置</a-divider>
|
||
<a-form-item label="是否开启分销" name="isOpenCommission">
|
||
<a-switch
|
||
size="small"
|
||
v-model:checked="form.isOpenCommission"
|
||
:checked-value="1"
|
||
:un-checked-value="0"
|
||
/>
|
||
</a-form-item>
|
||
<template v-if="form.isOpenCommission === 1">
|
||
<a-form-item label="分佣类型" name="commissionType">
|
||
<a-radio-group v-model:value="form.commissionType">
|
||
<a-radio :value="10">固定金额</a-radio>
|
||
<a-radio :value="20">百分比</a-radio>
|
||
</a-radio-group>
|
||
</a-form-item>
|
||
<a-form-item
|
||
:label="form.commissionType === 20 ? '一级佣金(%)' : '一级佣金(元)'"
|
||
name="firstMoney"
|
||
>
|
||
<a-input-number
|
||
v-model:value="form.firstMoney"
|
||
:min="0"
|
||
:max="form.commissionType === 20 ? 100 : undefined"
|
||
:precision="2"
|
||
style="width: 250px"
|
||
:placeholder="
|
||
form.commissionType === 20
|
||
? '请输入一级佣金百分比'
|
||
: '请输入一级佣金金额'
|
||
"
|
||
>
|
||
<template #addonAfter>{{
|
||
form.commissionType === 20 ? '%' : '元'
|
||
}}
|
||
</template>
|
||
</a-input-number>
|
||
</a-form-item>
|
||
<a-form-item
|
||
:label="form.commissionType === 20 ? '二级佣金(%)' : '二级佣金(元)'"
|
||
name="secondMoney"
|
||
>
|
||
<a-input-number
|
||
v-model:value="form.secondMoney"
|
||
:min="0"
|
||
:max="form.commissionType === 20 ? 100 : undefined"
|
||
:precision="2"
|
||
style="width: 250px"
|
||
:placeholder="
|
||
form.commissionType === 20
|
||
? '请输入二级佣金百分比'
|
||
: '请输入二级佣金金额'
|
||
"
|
||
>
|
||
<template #addonAfter>{{
|
||
form.commissionType === 20 ? '%' : '元'
|
||
}}
|
||
</template>
|
||
</a-input-number>
|
||
</a-form-item>
|
||
<!-- <a-form-item-->
|
||
<!-- :label="form.commissionType === 20 ? '三级佣金(%)' : '三级佣金(元)'"-->
|
||
<!-- name="thirdMoney"-->
|
||
<!-- >-->
|
||
<!-- <a-input-number-->
|
||
<!-- v-model:value="form.thirdMoney"-->
|
||
<!-- :min="0"-->
|
||
<!-- :max="form.commissionType === 20 ? 100 : undefined"-->
|
||
<!-- :precision="2"-->
|
||
<!-- style="width: 250px"-->
|
||
<!-- :placeholder="-->
|
||
<!-- form.commissionType === 20-->
|
||
<!-- ? '请输入三级佣金百分比'-->
|
||
<!-- : '请输入三级佣金金额'-->
|
||
<!-- "-->
|
||
<!-- >-->
|
||
<!-- <template #addonAfter>{{-->
|
||
<!-- form.commissionType === 20 ? '%' : '元'-->
|
||
<!-- }}</template>-->
|
||
<!-- </a-input-number>-->
|
||
<!-- </a-form-item>-->
|
||
<a-form-item label="一级分润" name="firstDividend">
|
||
<a-input-number
|
||
v-model:value="form.firstDividend"
|
||
:min="0"
|
||
:max="form.commissionType === 20 ? 100 : undefined"
|
||
:precision="2"
|
||
style="width: 250px"
|
||
placeholder="请输入一级分润"
|
||
>
|
||
<template #addonAfter>{{
|
||
form.commissionType === 20 ? '%' : '元'
|
||
}}
|
||
</template>
|
||
</a-input-number>
|
||
</a-form-item>
|
||
<a-form-item label="二级分润" name="secondDividend">
|
||
<a-input-number
|
||
v-model:value="form.secondDividend"
|
||
:min="0"
|
||
:max="form.commissionType === 20 ? 100 : undefined"
|
||
:precision="2"
|
||
style="width: 250px"
|
||
placeholder="请输入二级分润"
|
||
>
|
||
<template #addonAfter>{{
|
||
form.commissionType === 20 ? '%' : '元'
|
||
}}
|
||
</template>
|
||
</a-input-number>
|
||
</a-form-item>
|
||
<a-form-item label="配送费奖金" name="deliveryMoney">
|
||
<a-input-number
|
||
v-model:value="form.deliveryMoney"
|
||
:min="0"
|
||
:max="20"
|
||
:precision="2"
|
||
:step="0.01"
|
||
style="width: 250px"
|
||
placeholder="请输入配送费奖金"
|
||
>
|
||
<template #addonAfter>元</template>
|
||
</a-input-number>
|
||
</a-form-item>
|
||
</template>
|
||
<template v-if="form.type === 1 || merchantId">
|
||
<a-form-item label="可用日期">
|
||
<a-select v-model:value="canUseDate" mode="multiple">
|
||
<a-select-option
|
||
v-for="(item, index) in dayList"
|
||
:key="index"
|
||
:value="index"
|
||
>{{ item }}
|
||
</a-select-option>
|
||
</a-select>
|
||
</a-form-item>
|
||
<a-form-item label="服务保障">
|
||
<div>
|
||
<a-space>
|
||
<a-tag
|
||
v-for="item in ensureTag"
|
||
:key="item"
|
||
closable
|
||
@close="onDeleteEnsureTag(item)"
|
||
>{{ item }}
|
||
</a-tag>
|
||
<a-input v-model:value="ensureTagItem"></a-input>
|
||
<a-button @click="addEnsureTag()" :disabled="!ensureTagItem"
|
||
>添加
|
||
</a-button
|
||
>
|
||
</a-space>
|
||
</div>
|
||
<div class="mt-2">
|
||
<a-space>
|
||
<span @click="addEnsureTag('免预约')">免预约</span>
|
||
<span @click="addEnsureTag('随时退')">随时退</span>
|
||
<span @click="addEnsureTag('过期自动退')">过期自动退</span>
|
||
</a-space>
|
||
</div>
|
||
</a-form-item>
|
||
<a-form-item label="有效期限">
|
||
<a-input-number
|
||
:min="0"
|
||
:max="9999"
|
||
style="width: 250px"
|
||
placeholder="请输入有效期限"
|
||
v-model:value="form.expiredDay"
|
||
/>
|
||
</a-form-item>
|
||
</template>
|
||
</a-tab-pane>
|
||
</a-tabs>
|
||
</a-form>
|
||
</a-drawer>
|
||
|
||
</template>
|
||
|
||
<script lang="ts" setup>
|
||
import {
|
||
CloseCircleOutlined,
|
||
DeleteOutlined,
|
||
InfoCircleOutlined,
|
||
PlayCircleOutlined,
|
||
SettingOutlined,
|
||
UploadOutlined
|
||
} from '@ant-design/icons-vue';
|
||
import {ref, reactive, watch} from 'vue';
|
||
import {useRouter} from 'vue-router';
|
||
import {Form, message} from 'ant-design-vue';
|
||
import {assignObject, messageLoading, uuid} from 'ele-admin-pro';
|
||
import {addShopGoods, updateShopGoods} from '@/api/shop/shopGoods';
|
||
import {ShopGoods} from '@/api/shop/shopGoods/model';
|
||
import {useThemeStore} from '@/store/modules/theme';
|
||
import {storeToRefs} from 'pinia';
|
||
import {ItemType} from 'ele-admin-pro/es/ele-image-upload/types';
|
||
import {FormInstance, RuleObject} from 'ant-design-vue/es/form';
|
||
import {ShopMerchant} from '@/api/shop/shopMerchant/model';
|
||
import {uploadFile, uploadOss} from '@/api/system/file';
|
||
import {FileRecord} from '@/api/system/file/model';
|
||
import RichTextEditor from '@/components/RichTextEditor.vue';
|
||
import markdownit from 'markdown-it';
|
||
import {ShopSpec} from '@/api/shop/shopSpec/model';
|
||
import {ShopGoodsSku} from '@/api/shop/shopGoodsSku/model';
|
||
import {ShopGoodsSpec} from '@/api/shop/shopGoodsSpec/model';
|
||
import {listShopGoodsSku} from '@/api/shop/shopGoodsSku';
|
||
import {listShopGoodsSpec} from '@/api/shop/shopGoodsSpec';
|
||
import {getShopSpec} from '@/api/shop/shopSpec';
|
||
import {ShopGoodsCategory} from '@/api/shop/shopGoodsCategory/model';
|
||
import {getMerchantId} from '@/utils/merchant';
|
||
import {ShopExpressTemplate} from '@/api/shop/shopExpressTemplate/model';
|
||
import {listShopExpressTemplate} from '@/api/shop/shopExpressTemplate';
|
||
import {listShopCommissionRole} from '@/api/shop/shopCommissionRole';
|
||
import {listShopGoodsRoleCommission} from '@/api/shop/shopGoodsRoleCommission';
|
||
import {listHomeSections} from '@/api/shop/shopZone';
|
||
|
||
// 是否是修改
|
||
const isUpdate = ref(false);
|
||
const useForm = Form.useForm;
|
||
// 是否开启响应式布局
|
||
const themeStore = useThemeStore();
|
||
const {styleResponsive} = storeToRefs(themeStore);
|
||
|
||
const props = defineProps<{
|
||
// 弹窗是否打开
|
||
visible: boolean;
|
||
// 修改回显的数据
|
||
data?: ShopGoods | null;
|
||
// 分类数据
|
||
navigationList?: ShopGoodsCategory[];
|
||
}>();
|
||
|
||
const emit = defineEmits<{
|
||
(e: 'done'): void;
|
||
(e: 'update:visible', visible: boolean): void;
|
||
}>();
|
||
|
||
// 提交状态
|
||
const loading = ref(false);
|
||
// 是否显示最大化切换按钮
|
||
const maxable = ref(true);
|
||
// 表格选中数据
|
||
const formRef = ref<FormInstance | null>(null);
|
||
const images = ref<ItemType[]>([]);
|
||
const content = ref('');
|
||
|
||
// Markdown -> HTML 转换(编辑回显旧数据时,把 Markdown 内容转成富文本可识别的 HTML)
|
||
const md = markdownit();
|
||
/**
|
||
* 把后端 content 转为富文本编辑器可用的 HTML:
|
||
* - 已是 HTML(含标签)则原样返回,避免二次转义;
|
||
* - 否则按 Markdown 渲染为 HTML。
|
||
*/
|
||
const toEditorContent = (raw?: string): string => {
|
||
if (!raw) return '';
|
||
const s = raw.trim();
|
||
if (/<[a-z][\s\S]*>/i.test(s)) return raw;
|
||
return md.render(s);
|
||
};
|
||
|
||
// 当前选项卡
|
||
const active = ref('base');
|
||
|
||
// 规格数据结构:{ value: 规格名, detail: 规格值数组 }
|
||
interface SpecItem {
|
||
value: string;
|
||
detail: string[];
|
||
}
|
||
|
||
/**
|
||
* 统一规范化 detail 为数组
|
||
*/
|
||
const normalizeDetail = (detail: any): string[] => {
|
||
if (Array.isArray(detail)) {
|
||
return detail.map(String);
|
||
}
|
||
if (typeof detail === 'string') {
|
||
return detail.split(',').map(s => s.trim()).filter(Boolean);
|
||
}
|
||
return [];
|
||
};
|
||
|
||
/**
|
||
* 统一解析规格值,支持多种存储格式
|
||
* @param specValue 规格值(JSON字符串或已解析对象)
|
||
* @returns 统一格式的 SpecItem[]
|
||
*/
|
||
const parseSpecValue = (specValue: string | any): SpecItem[] => {
|
||
if (!specValue) return [];
|
||
try {
|
||
let parsed: any;
|
||
if (typeof specValue === 'string') {
|
||
parsed = JSON.parse(specValue);
|
||
// 处理双层编码:parse 后仍是字符串(说明原始值是 "=\"...\" 格式)
|
||
if (typeof parsed === 'string') {
|
||
try { parsed = JSON.parse(parsed); } catch (_) { /* ignore */ }
|
||
}
|
||
} else {
|
||
parsed = specValue;
|
||
}
|
||
|
||
// 标准数组格式: [{value: "颜色", detail: ["红色"]}]
|
||
if (Array.isArray(parsed)) {
|
||
return parsed
|
||
.filter((item: any) => item && (item.value !== undefined || typeof item === 'string') && (Array.isArray(item.detail) || typeof item.detail === 'string'))
|
||
.map((item: any) => {
|
||
if (typeof item === 'string') {
|
||
return { value: '', detail: [item] };
|
||
}
|
||
return {
|
||
value: item.value !== undefined ? String(item.value) : '',
|
||
detail: normalizeDetail(item.detail)
|
||
};
|
||
})
|
||
.filter((item: SpecItem) => item.detail.length > 0);
|
||
}
|
||
|
||
// 对象格式: {"颜色": ["红色", "蓝色"], "尺码": ["S", "M"]}
|
||
if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
|
||
return Object.entries(parsed).map(([key, value]: [string, any]) => ({
|
||
value: key,
|
||
detail: normalizeDetail(value)
|
||
}));
|
||
}
|
||
|
||
// 单个字符串规格值(旧格式):specValue = "\"红色\""
|
||
if (typeof parsed === 'string' && parsed.trim()) {
|
||
return [{ value: '', detail: [parsed.trim()] }];
|
||
}
|
||
|
||
return [];
|
||
} catch (e) {
|
||
console.error('解析规格值失败:', e, '原始值:', specValue);
|
||
return [];
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 将后端多条 ShopGoodsSpec 记录合并为前端 spec.value 格式
|
||
* 每条记录 specValue 为简单字符串,按 specName 分组
|
||
*/
|
||
const mergeGoodsSpecs = (goodsSpecs: ShopGoodsSpec[]): SpecItem[] => {
|
||
if (!goodsSpecs || goodsSpecs.length === 0) return [];
|
||
const map = new Map<string, string[]>();
|
||
goodsSpecs.forEach((gs) => {
|
||
const name = gs.specName || '';
|
||
let val = gs.specValue || '';
|
||
// 去掉 JSON 引号:数据库存的是 "\"红色\"",需要解析成 红色
|
||
try { val = JSON.parse(val); } catch (_) { /* 不是 JSON 格式,保持原样 */ }
|
||
if (!map.has(name)) {
|
||
map.set(name, []);
|
||
}
|
||
if (val) map.get(name)!.push(val);
|
||
});
|
||
return Array.from(map.entries()).map(([value, detail]) => ({ value, detail }));
|
||
};
|
||
|
||
const spec = ref<SpecItem[]>([]);
|
||
const showSpecForm = ref(false);
|
||
const name = ref();
|
||
const value = ref();
|
||
const skuList = ref<ShopGoodsSku[]>([]);
|
||
const files = ref<ItemType[]>([]);
|
||
const goodsSpec = ref<ShopGoodsSpec>();
|
||
const category = ref<string[]>([]);
|
||
// 所属专区(多选)
|
||
const sectionIdList = ref<number[]>([]);
|
||
const zoneOptions = ref<{ label: string; value: number }[]>([]);
|
||
|
||
// 批量设置
|
||
const batchPrice = ref<number | undefined>(undefined);
|
||
const batchSalePrice = ref<number | undefined>(undefined);
|
||
const batchBuyingPrice = ref<number | undefined>(undefined);
|
||
const batchStock = ref<number | undefined>(undefined);
|
||
const batchSkuNo = ref<string>('');
|
||
const takeaway = ref<ShopGoodsCategory[]>([]);
|
||
const merchantId = getMerchantId();
|
||
|
||
const router = useRouter();
|
||
const openSpecManage = () => {
|
||
router.push('/shop/shopSpec');
|
||
};
|
||
|
||
const columns = [
|
||
{
|
||
title: '图片',
|
||
dataIndex: 'image',
|
||
key: 'image',
|
||
align: 'center'
|
||
},
|
||
{
|
||
title: 'SKU',
|
||
dataIndex: 'sku',
|
||
align: 'center'
|
||
},
|
||
{
|
||
title: '商城价',
|
||
dataIndex: 'price',
|
||
key: 'price',
|
||
align: 'center'
|
||
},
|
||
{
|
||
title: '划线价',
|
||
dataIndex: 'salePrice',
|
||
key: 'salePrice',
|
||
align: 'center'
|
||
},
|
||
{
|
||
title: '会员价',
|
||
dataIndex: 'buyingPrice',
|
||
key: 'buyingPrice',
|
||
align: 'center'
|
||
},
|
||
{
|
||
title: '库存',
|
||
dataIndex: 'stock',
|
||
key: 'stock',
|
||
align: 'center'
|
||
},
|
||
{
|
||
title: '商品编号',
|
||
dataIndex: 'skuNo',
|
||
key: 'skuNo',
|
||
align: 'center'
|
||
}
|
||
];
|
||
|
||
const dayList = ['周一', '周二', '周三', '周四', '周五', '周六', '周日'];
|
||
const canUseDate = ref([]);
|
||
const ensureTag = ref<string[]>([]);
|
||
const ensureTagItem = ref('');
|
||
const addEnsureTag = (item = '') => {
|
||
const tag = item || ensureTagItem.value;
|
||
if (ensureTag.value.findIndex((item) => item === tag) !== -1)
|
||
return message.error('请勿重复添加');
|
||
ensureTag.value.push(tag);
|
||
ensureTagItem.value = '';
|
||
};
|
||
|
||
// 选择栏目(方案B:支持多选,主分类取列表首个,向后兼容旧字段)
|
||
const onCategoryIds = (ids: number[]) => {
|
||
form.categoryIds = ids || [];
|
||
form.categoryId = ids && ids.length ? ids[0] : undefined;
|
||
// 💾 在新增模式下,用户手动选择栏目时也保存到本地存储
|
||
if (!isUpdate.value && ids && ids.length) {
|
||
saveLastCategory(ids[0]);
|
||
}
|
||
};
|
||
|
||
// �💾 保存和恢复分类选择的功能
|
||
const LAST_CATEGORY_KEY = 'shop_goods_last_category';
|
||
|
||
// 保存最后选择的分类到本地存储
|
||
const saveLastCategory = (categoryId: number | undefined) => {
|
||
if (categoryId) {
|
||
localStorage.setItem(LAST_CATEGORY_KEY, categoryId.toString());
|
||
}
|
||
};
|
||
|
||
// 从本地存储获取最后选择的分类
|
||
const getLastCategory = (): number | undefined => {
|
||
const saved = localStorage.getItem(LAST_CATEGORY_KEY);
|
||
return saved ? parseInt(saved) : undefined;
|
||
};
|
||
|
||
const onDeleteEnsureTag = (tag: string) => {
|
||
const index = ensureTag.value.findIndex((item) => item === tag);
|
||
ensureTag.value.splice(index, 1);
|
||
};
|
||
|
||
// 🎨 智能一键排版 - 人性化设计
|
||
const handleAutoFormat = (editor: any) => {
|
||
try {
|
||
// 1. 检查内容
|
||
const content = editor.getContent();
|
||
if (!content || content.trim() === '' || content === '<p><br></p>' || content === '<p></p>') {
|
||
message.warning({
|
||
content: '📝 请先输入一些内容,然后再使用一键排版功能',
|
||
duration: 3
|
||
});
|
||
return;
|
||
}
|
||
|
||
// 2. 显示友好的加载提示
|
||
const loadingMsg = message.loading({
|
||
content: '✨ 正在为您的文章进行智能排版优化...',
|
||
duration: 0
|
||
});
|
||
|
||
// 3. 延迟执行,让用户看到加载效果
|
||
setTimeout(() => {
|
||
try {
|
||
const optimizedContent = smartFormatContent(content);
|
||
editor.setContent(optimizedContent);
|
||
|
||
loadingMsg();
|
||
|
||
// 4. 显示成功提示
|
||
message.success({
|
||
content: '🎉 排版优化完成!您的文章现在看起来更专业了',
|
||
duration: 4
|
||
});
|
||
|
||
// 5. 可选:显示优化统计
|
||
showOptimizationStats(content, optimizedContent);
|
||
|
||
} catch (error) {
|
||
loadingMsg();
|
||
console.error('排版优化失败:', error);
|
||
message.error({
|
||
content: '😅 排版优化遇到了问题,请检查文章内容后重试',
|
||
duration: 4
|
||
});
|
||
}
|
||
}, 800); // 给用户一个良好的反馈体验
|
||
|
||
} catch (error) {
|
||
console.error('一键排版功能错误:', error);
|
||
message.error({
|
||
content: '🔧 功能暂时不可用,请刷新页面后重试',
|
||
duration: 4
|
||
});
|
||
}
|
||
};
|
||
|
||
// 📊 显示优化统计信息
|
||
const showOptimizationStats = (originalContent: string, optimizedContent: string) => {
|
||
const stats = analyzeOptimization(originalContent, optimizedContent);
|
||
|
||
if (stats.optimizations.length > 0) {
|
||
message.info({
|
||
content: `📈 本次优化: ${stats.optimizations.join('、')}`,
|
||
duration: 6
|
||
});
|
||
}
|
||
};
|
||
|
||
// 🔍 分析优化效果
|
||
const analyzeOptimization = (original: string, optimized: string) => {
|
||
const optimizations: string[] = [];
|
||
|
||
// 检查各种优化项目
|
||
if ((optimized.match(/<h[1-6][^>]*style/g) || []).length > (original.match(/<h[1-6][^>]*style/g) || []).length) {
|
||
optimizations.push('标题样式');
|
||
}
|
||
|
||
if ((optimized.match(/<p[^>]*style/g) || []).length > (original.match(/<p[^>]*style/g) || []).length) {
|
||
optimizations.push('段落格式');
|
||
}
|
||
|
||
if ((optimized.match(/<img[^>]*style/g) || []).length > (original.match(/<img[^>]*style/g) || []).length) {
|
||
optimizations.push('图片布局');
|
||
}
|
||
|
||
if ((optimized.match(/<ul[^>]*style|<ol[^>]*style/g) || []).length > (original.match(/<ul[^>]*style|<ol[^>]*style/g) || []).length) {
|
||
optimizations.push('列表格式');
|
||
}
|
||
|
||
return {optimizations};
|
||
};
|
||
|
||
// 🎨 智能排版核心函数 - 简单而强大
|
||
const smartFormatContent = (content: string): string => {
|
||
let optimized = content;
|
||
|
||
// 1. 🏷️ 标题优化 - 让标题更有层次感
|
||
optimized = optimized.replace(/<h1([^>]*)>/g, '<h1$1 style="font-size: 28px; font-weight: 700; margin: 24px 0 16px 0; line-height: 1.3; color: #1a1a1a; border-bottom: 2px solid #e8e8e8; padding-bottom: 10px;">');
|
||
optimized = optimized.replace(/<h2([^>]*)>/g, '<h2$1 style="font-size: 24px; font-weight: 600; margin: 20px 0 14px 0; line-height: 1.4; color: #2c2c2c;">');
|
||
optimized = optimized.replace(/<h3([^>]*)>/g, '<h3$1 style="font-size: 20px; font-weight: 600; margin: 18px 0 12px 0; line-height: 1.4; color: #3c3c3c;">');
|
||
optimized = optimized.replace(/<h4([^>]*)>/g, '<h4$1 style="font-size: 16px; font-weight: 600; margin: 14px 0 8px 0; line-height: 1.4; color: #4c4c4c;">');
|
||
optimized = optimized.replace(/<h5([^>]*)>/g, '<h5$1 style="font-size: 14px; font-weight: 600; margin: 12px 0 6px 0; line-height: 1.4; color: #5c5c5c;">');
|
||
optimized = optimized.replace(/<h6([^>]*)>/g, '<h6$1 style="font-size: 13px; font-weight: 600; margin: 10px 0 5px 0; line-height: 1.4; color: #6c6c6c;">');
|
||
|
||
// 2. 📝 段落优化 - 让阅读更舒适
|
||
optimized = optimized.replace(/<p([^>]*)>/g, (match, attrs) => {
|
||
if (!attrs.includes('style=')) {
|
||
return `<p${attrs} style="line-height: 1.8; margin: 16px 0; text-indent: 2em; color: #333;">`;
|
||
}
|
||
return match;
|
||
});
|
||
|
||
// 3. 🖼️ 图片优化 - 让图片更美观
|
||
optimized = optimized.replace(/<img([^>]*?)>/g, (match, attrs) => {
|
||
if (!attrs.includes('style=')) {
|
||
const hasAlt = attrs.includes('alt=');
|
||
return `<img${attrs} style="max-width: 100%; height: auto; margin: 20px auto; display: block; border-radius: 8px; box-shadow: 0 4px 16px rgba(0,0,0,0.1);"${!hasAlt ? ' alt="图片"' : ''}>`;
|
||
}
|
||
return match;
|
||
});
|
||
|
||
// 4. 📋 列表优化 - 让列表更清晰
|
||
optimized = optimized.replace(/<ul([^>]*)>/g, '<ul$1 style="margin: 16px 0; padding-left: 24px; line-height: 1.6;">');
|
||
optimized = optimized.replace(/<ol([^>]*)>/g, '<ol$1 style="margin: 16px 0; padding-left: 24px; line-height: 1.6;">');
|
||
optimized = optimized.replace(/<li([^>]*)>/g, '<li$1 style="margin: 8px 0; color: #333;">');
|
||
|
||
// 5. 💬 引用优化 - 让引用更突出
|
||
optimized = optimized.replace(/<blockquote([^>]*)>/g, '<blockquote$1 style="margin: 20px 0; padding: 16px 20px; border-left: 4px solid #1890ff; background: linear-gradient(90deg, #f6f8fa 0%, #ffffff 100%); font-style: italic; color: #555;">');
|
||
|
||
// 6. 💻 代码优化 - 让代码更专业
|
||
optimized = optimized.replace(/<code([^>]*)>/g, '<code$1 style="background-color: #f1f3f4; padding: 2px 6px; border-radius: 4px; font-family: \'Fira Code\', Consolas, Monaco, monospace; font-size: 0.9em; color: #d73a49;">');
|
||
optimized = optimized.replace(/<pre([^>]*)>/g, '<pre$1 style="margin: 20px 0; padding: 20px; background-color: #f8f9fa; border: 1px solid #e9ecef; border-radius: 8px; overflow-x: auto; font-family: \'Fira Code\', Consolas, Monaco, monospace; font-size: 14px; line-height: 1.5;">');
|
||
|
||
// 7. 📊 表格优化 - 让表格更美观
|
||
optimized = optimized.replace(/<table([^>]*)>/g, '<table$1 style="width: 100%; border-collapse: collapse; margin: 20px 0; box-shadow: 0 2px 8px rgba(0,0,0,0.1); border-radius: 8px; overflow: hidden;">');
|
||
optimized = optimized.replace(/<th([^>]*)>/g, '<th$1 style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 12px; text-align: left; font-weight: 600;">');
|
||
optimized = optimized.replace(/<td([^>]*)>/g, '<td$1 style="padding: 12px; border-bottom: 1px solid #eee; color: #333;">');
|
||
|
||
// 8. 🔗 链接优化 - 让链接更友好
|
||
optimized = optimized.replace(/<a([^>]*)>/g, '<a$1 style="color: #1890ff; text-decoration: none; border-bottom: 1px solid transparent; transition: border-bottom 0.2s ease;" onmouseover="this.style.borderBottom=\'1px solid #1890ff\'" onmouseout="this.style.borderBottom=\'1px solid transparent\'">');
|
||
|
||
// 9. ➖ 分隔线优化 - 让分隔线更优雅
|
||
optimized = optimized.replace(/<hr([^>]*)>/g, '<hr$1 style="border: none; height: 2px; background: linear-gradient(90deg, transparent, #e8e8e8, transparent); margin: 30px 0;">');
|
||
|
||
// 10. 🧹 清理多余空白
|
||
optimized = optimized.replace(/\s+/g, ' '); // 清理多余空格
|
||
optimized = optimized.replace(/<p[^>]*>\s*<\/p>/g, ''); // 清理空段落
|
||
optimized = optimized.replace(/(<\/[^>]+>)\s+(<[^>]+>)/g, '$1$2'); // 清理标签间空白
|
||
|
||
return optimized;
|
||
};
|
||
|
||
// 🔄 段落首行缩进切换功能
|
||
const toggleParagraphIndent = (editor: any) => {
|
||
try {
|
||
const content = editor.getContent();
|
||
|
||
if (!content || content.trim() === '' || content === '<p><br></p>' || content === '<p></p>') {
|
||
message.warning({
|
||
content: '📝 请先输入一些段落内容,然后再切换首行缩进',
|
||
duration: 3
|
||
});
|
||
return;
|
||
}
|
||
|
||
// 检查当前是否有首行缩进
|
||
const hasIndent = content.includes('text-indent: 2em') || content.includes('text-indent:2em');
|
||
|
||
let newContent: string;
|
||
let actionText: string;
|
||
|
||
if (hasIndent) {
|
||
// 移除首行缩进
|
||
newContent = removeIndentFromParagraphs(content);
|
||
actionText = '已移除段落首行缩进';
|
||
} else {
|
||
// 添加首行缩进
|
||
newContent = addIndentToParagraphs(content);
|
||
actionText = '已添加段落首行缩进';
|
||
}
|
||
|
||
editor.setContent(newContent);
|
||
|
||
message.success({
|
||
content: `📐 ${actionText}`,
|
||
duration: 3
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('首行缩进切换失败:', error);
|
||
message.error({
|
||
content: '🔧 首行缩进切换失败,请重试',
|
||
duration: 3
|
||
});
|
||
}
|
||
};
|
||
|
||
// 为段落添加首行缩进
|
||
const addIndentToParagraphs = (content: string): string => {
|
||
return content.replace(/<p([^>]*)>/g, (match, attrs) => {
|
||
// 如果已经有 style 属性
|
||
if (attrs.includes('style=')) {
|
||
// 检查是否已经有 text-indent
|
||
if (attrs.includes('text-indent')) {
|
||
// 更新现有的 text-indent
|
||
return match.replace(/text-indent:\s*[^;]+;?/g, 'text-indent: 2em;');
|
||
} else {
|
||
// 在现有 style 中添加 text-indent
|
||
return match.replace(/style="([^"]*)"/, 'style="$1 text-indent: 2em;"');
|
||
}
|
||
} else {
|
||
// 添加新的 style 属性
|
||
return `<p${attrs} style="text-indent: 2em;">`;
|
||
}
|
||
});
|
||
};
|
||
|
||
// 从段落移除首行缩进
|
||
const removeIndentFromParagraphs = (content: string): string => {
|
||
return content.replace(/<p([^>]*)>/g, (match, attrs) => {
|
||
if (attrs.includes('text-indent')) {
|
||
// 移除 text-indent 属性
|
||
let newAttrs = attrs.replace(/text-indent:\s*[^;]+;?\s*/g, '');
|
||
|
||
// 如果 style 属性变空了,移除整个 style 属性
|
||
newAttrs = newAttrs.replace(/style="\s*"/g, '');
|
||
newAttrs = newAttrs.replace(/style=''\s*/g, '');
|
||
|
||
return `<p${newAttrs}>`;
|
||
}
|
||
return match;
|
||
});
|
||
};
|
||
|
||
// 用户信息
|
||
const form = reactive<ShopGoods>({
|
||
goodsId: undefined,
|
||
type: 1,
|
||
code: undefined,
|
||
name: undefined,
|
||
goodsName: undefined,
|
||
image: undefined,
|
||
video: undefined,
|
||
content: undefined,
|
||
canExpress: 1,
|
||
unitName: '',
|
||
categoryId: undefined,
|
||
categoryIds: [],
|
||
parentName: undefined,
|
||
categoryName: undefined,
|
||
specs: 0,
|
||
commissionRole: 0,
|
||
// 分销佣金(新字段,后端保持 snake_case)
|
||
isOpenCommission: 0,
|
||
commissionType: 10,
|
||
firstMoney: 0,
|
||
secondMoney: 0,
|
||
thirdMoney: 0,
|
||
firstDividend: 0,
|
||
secondDividend: 0,
|
||
deliveryMoney: 0,
|
||
position: undefined,
|
||
price: undefined,
|
||
originPrice: undefined,
|
||
salePrice: undefined,
|
||
buyingPrice: undefined,
|
||
dealerPrice: undefined,
|
||
priceGift: undefined,
|
||
dealerGift: undefined,
|
||
priceGiftNum: undefined,
|
||
priceGiftName: undefined,
|
||
dealerGiftNum: undefined,
|
||
files: undefined,
|
||
sales: 0,
|
||
isNew: 0,
|
||
activityType: 0,
|
||
deliveryMode: 0,
|
||
gainIntegral: 0,
|
||
goodsWeight: undefined,
|
||
recommend: undefined,
|
||
merchantId: undefined,
|
||
merchantName: undefined,
|
||
supplierMerchantId: undefined,
|
||
supplierName: undefined,
|
||
chainStorePrice: undefined,
|
||
memberStorePrice: undefined,
|
||
memberMarketPrice: undefined,
|
||
chainStoreRate: undefined,
|
||
memberStoreRate: undefined,
|
||
memberMarketRate: undefined,
|
||
memberStoreCommission: undefined,
|
||
supplierCommission: undefined,
|
||
coopCommission: undefined,
|
||
expressTemplateId: undefined,
|
||
canUseDate: undefined,
|
||
ensureTag: undefined,
|
||
expiredDay: undefined,
|
||
stock: 1000,
|
||
deductStockType: 20,
|
||
isShow: undefined,
|
||
status: 0,
|
||
comments: '',
|
||
sortNumber: 100,
|
||
specName: '',
|
||
goodsRoleCommission: []
|
||
});
|
||
|
||
/* 更新visible */
|
||
const updateVisible = (value: boolean) => {
|
||
emit('update:visible', value);
|
||
};
|
||
|
||
// 表单验证规则
|
||
const rules = reactive({
|
||
type: [
|
||
{
|
||
required: true,
|
||
message: '请选择商品类型',
|
||
type: 'number',
|
||
trigger: 'blur'
|
||
}
|
||
],
|
||
image: [
|
||
{
|
||
required: true,
|
||
message: '请上传图片',
|
||
type: 'string',
|
||
trigger: 'blur'
|
||
}
|
||
],
|
||
files: [
|
||
{
|
||
required: true,
|
||
message: '请上传轮播图',
|
||
type: 'string',
|
||
trigger: 'blur',
|
||
validator: async (_rule: RuleObject, value: string) => {
|
||
if (form.files?.length == 0) {
|
||
return Promise.reject('选择上传轮播图');
|
||
}
|
||
return Promise.resolve();
|
||
}
|
||
}
|
||
],
|
||
specs: [
|
||
{
|
||
required: true,
|
||
message: '请选择规格类型',
|
||
type: 'number',
|
||
trigger: 'blur'
|
||
}
|
||
],
|
||
price: [
|
||
{
|
||
required: true,
|
||
message: '请填写商城价',
|
||
type: 'number',
|
||
trigger: 'blur'
|
||
}
|
||
],
|
||
salePrice: [
|
||
{
|
||
required: true,
|
||
message: '请填写市场价',
|
||
type: 'number',
|
||
trigger: 'blur'
|
||
}
|
||
],
|
||
buyingPrice: [
|
||
{
|
||
required: true,
|
||
message: '请填写进货价',
|
||
type: 'number',
|
||
trigger: 'blur'
|
||
}
|
||
],
|
||
stock: [
|
||
{
|
||
required: true,
|
||
message: '请填写商品库存',
|
||
type: 'number',
|
||
trigger: 'blur'
|
||
}
|
||
],
|
||
// supplierMerchantId: [
|
||
// {
|
||
// required: true,
|
||
// message: '请选择供应商',
|
||
// type: 'number',
|
||
// trigger: 'blur'
|
||
// }
|
||
// ],
|
||
// categoryId: [
|
||
// {
|
||
// required: true,
|
||
// type: 'string',
|
||
// message: '选择商品分类',
|
||
// trigger: 'blur',
|
||
// validator: async (_rule: RuleObject, value: string) => {
|
||
// if (!form.categoryId) {
|
||
// return Promise.reject('选择商品分类');
|
||
// }
|
||
// return Promise.resolve();
|
||
// }
|
||
// }
|
||
// ],
|
||
name: [
|
||
{
|
||
required: true,
|
||
message: '请选择商品名称',
|
||
type: 'string',
|
||
trigger: 'blur'
|
||
}
|
||
],
|
||
goodsName: [
|
||
{
|
||
required: true,
|
||
message: '请选择商品名称',
|
||
type: 'string',
|
||
trigger: 'blur'
|
||
}
|
||
],
|
||
sortNumber: [
|
||
{
|
||
required: true,
|
||
message: '请输入排序号',
|
||
type: 'number',
|
||
trigger: 'blur'
|
||
}
|
||
],
|
||
chainStorePrice: [
|
||
{
|
||
required: true,
|
||
message: '请输入供应价',
|
||
type: 'number',
|
||
trigger: 'blur'
|
||
}
|
||
],
|
||
memberStorePrice: [
|
||
{
|
||
required: true,
|
||
message: '请输入会员店价格',
|
||
type: 'number',
|
||
trigger: 'blur'
|
||
}
|
||
],
|
||
memberMarketPrice: [
|
||
{
|
||
required: true,
|
||
message: '请输入会员超市价格',
|
||
type: 'number',
|
||
trigger: 'blur'
|
||
}
|
||
],
|
||
memberStoreCommission: [
|
||
{
|
||
required: true,
|
||
message: '请输入直推收益分配',
|
||
type: 'number',
|
||
trigger: 'blur'
|
||
}
|
||
],
|
||
supplierCommission: [
|
||
{
|
||
required: true,
|
||
message: '请输入仓储费',
|
||
type: 'number',
|
||
trigger: 'blur'
|
||
}
|
||
]
|
||
});
|
||
|
||
const onType = (index: number) => {
|
||
form.type = index;
|
||
};
|
||
|
||
/* 搜索 */
|
||
const chooseMerchantId = (item: ShopMerchant) => {
|
||
form.merchantName = item.merchantName;
|
||
form.merchantId = item.merchantId;
|
||
};
|
||
|
||
const clearMerchant = () => {
|
||
form.merchantName = '';
|
||
form.merchantId = 0;
|
||
};
|
||
|
||
const chooseSupplier = (item: ShopMerchant) => {
|
||
form.supplierName = item.merchantName;
|
||
form.supplierMerchantId = item.merchantId;
|
||
};
|
||
|
||
const chooseGoodsCategory = (item: ShopGoodsCategory, value: any) => {
|
||
const cid = value[1].value as number;
|
||
form.categoryId = cid;
|
||
form.categoryIds = [cid];
|
||
form.parentName = value[0].label;
|
||
form.categoryName = value[1].label;
|
||
};
|
||
const chooseTakeawayCategory = (item: ShopGoodsCategory, value: any) => {
|
||
const cid = item[0] as number;
|
||
form.categoryParent = '店铺分类';
|
||
form.categoryChildren = value[0].label;
|
||
form.categoryId = cid;
|
||
form.categoryIds = [cid];
|
||
};
|
||
|
||
const chooseImage = (data: FileRecord) => {
|
||
images.value.push({
|
||
uid: data.id,
|
||
url: data.path,
|
||
status: 'done'
|
||
});
|
||
form.image = data.path;
|
||
};
|
||
|
||
const chooseSkuImage = (data: FileRecord) => {
|
||
const index = data?.index;
|
||
skuList.value[index].images?.push({
|
||
uid: uuid(),
|
||
url: data.path,
|
||
status: 'done'
|
||
});
|
||
skuList.value[index].image = data.path;
|
||
};
|
||
|
||
const onDeleteSkuItem = (index: number) => {
|
||
skuList.value[index].images = [];
|
||
skuList.value[index].image = '';
|
||
};
|
||
|
||
const videos = ref<ItemType[]>([]);
|
||
const chooseVideo = (data: FileRecord) => {
|
||
videos.value = [
|
||
{
|
||
uid: uuid(),
|
||
url: data.path,
|
||
status: 'done'
|
||
}
|
||
];
|
||
form.video = data.path;
|
||
};
|
||
|
||
const onDeleteVideo = (index: number) => {
|
||
images.value.splice(index, 1);
|
||
form.video = undefined;
|
||
};
|
||
|
||
const onChange = (text: string) => {
|
||
// 加载商品多规格
|
||
if (text == 'spec') {
|
||
const goodsId = props.data?.goodsId;
|
||
if (goodsId) {
|
||
listShopGoodsSpec({goodsId}).then((data) => {
|
||
if (data.length > 0) {
|
||
// 多条记录合并为 spec.value;兼容旧格式
|
||
const merged = mergeGoodsSpecs(data);
|
||
spec.value = merged.length > 0 ? merged : parseSpecValue(data[0].specValue);
|
||
goodsSpec.value = {
|
||
specId: data[0].specId,
|
||
specName: data[0].specName,
|
||
specValue: JSON.stringify(spec.value)
|
||
};
|
||
form.specName = data[0].specName || '';
|
||
}
|
||
});
|
||
listShopGoodsSku({goodsId}).then((data) => {
|
||
skuList.value = data.map((d) => ({
|
||
...d,
|
||
images: d.image ? [{uid: uuid(), url: d.image, status: 'done' as const}] : []
|
||
}));
|
||
});
|
||
}
|
||
}
|
||
};
|
||
|
||
const onDeleteItem = (index: number) => {
|
||
images.value.splice(index, 1);
|
||
form.image = '';
|
||
};
|
||
|
||
const onClose = (index: number) => {
|
||
spec.value.splice(index, 1);
|
||
// 同步更新 goodsSpec 的 specValue
|
||
if (goodsSpec.value) {
|
||
goodsSpec.value.specValue = JSON.stringify(spec.value);
|
||
}
|
||
};
|
||
|
||
const openSpecForm = () => {
|
||
showSpecForm.value = !showSpecForm.value;
|
||
};
|
||
|
||
const onSpec = (row: any) => {
|
||
form.specName = row.specName;
|
||
const rawSpecValue = row.specValue || row.value;
|
||
// 如果 specValue 为空,则从后端重新拉取完整规格数据
|
||
if (!rawSpecValue && row.specId) {
|
||
getShopSpec(row.specId).then((fullSpec: any) => {
|
||
const v = fullSpec.specValue || fullSpec.value;
|
||
goodsSpec.value = {
|
||
specId: row.specId,
|
||
specName: row.specName,
|
||
specValue: v
|
||
};
|
||
spec.value = parseSpecValue(v);
|
||
console.log('[onSpec] 重新拉取规格值:', v, '解析结果:', spec.value);
|
||
}).catch((e: any) => {
|
||
message.error('获取规格值失败: ' + (e.message || e));
|
||
});
|
||
} else {
|
||
goodsSpec.value = {
|
||
specId: row.specId,
|
||
specName: row.specName,
|
||
specValue: rawSpecValue
|
||
};
|
||
spec.value = parseSpecValue(rawSpecValue);
|
||
}
|
||
console.log('[onSpec] 选择规格模板:', row.specName, '原始值:', rawSpecValue, '解析结果:', spec.value);
|
||
skuList.value = [];
|
||
};
|
||
|
||
// 新增规格
|
||
const addSpecValue = () => {
|
||
if (!name.value || !value.value) {
|
||
message.error(`请输入规格和规格值`);
|
||
return false;
|
||
}
|
||
const findIndex = spec.value.findIndex((d) => d.value == name.value);
|
||
if (findIndex !== -1) {
|
||
message.error(`${name.value}已存在`);
|
||
return false;
|
||
}
|
||
spec.value.push({
|
||
value: name.value,
|
||
detail: [value.value]
|
||
});
|
||
// 同步更新 goodsSpec 的 specValue
|
||
if (goodsSpec.value) {
|
||
goodsSpec.value.specValue = JSON.stringify(spec.value);
|
||
}
|
||
name.value = '';
|
||
value.value = '';
|
||
openSpecForm();
|
||
};
|
||
|
||
const chooseFile = (data: FileRecord) => {
|
||
files.value.push({
|
||
uid: data.id,
|
||
url: data.path,
|
||
status: 'done'
|
||
});
|
||
};
|
||
|
||
const onDeleteFile = (index: number) => {
|
||
files.value.splice(index, 1);
|
||
};
|
||
|
||
/**
|
||
* 生成商品SKU列表
|
||
*/
|
||
const generateSku = () => {
|
||
if (spec.value.length === 0) {
|
||
message.warning('请先添加规格');
|
||
return;
|
||
}
|
||
// 过滤掉没有规格值的项
|
||
const specs = spec.value.filter(s => s.detail.length > 0);
|
||
if (specs.length === 0) {
|
||
message.warning('请先添加规格值');
|
||
return;
|
||
}
|
||
|
||
// 前端笛卡尔积生成 SKU 组合
|
||
const cartesian = (lists: string[][]): string[][] => {
|
||
if (lists.length === 0) return [[]];
|
||
if (lists.length === 1) return lists[0].map(v => [v]);
|
||
const [first, ...rest] = lists;
|
||
const sub = cartesian(rest);
|
||
return first.flatMap(v => sub.map(s => [v, ...s]));
|
||
};
|
||
|
||
const detailLists = specs.map(s => s.detail);
|
||
const names = specs.map(s => s.value);
|
||
const combos = cartesian(detailLists);
|
||
|
||
skuList.value = combos.map((combo, i) => {
|
||
const skuStr = combo.join('|');
|
||
// 保留已有行的价格等数据,避免重复生成时丢失
|
||
const existing = skuList.value[i] || {};
|
||
return {
|
||
sku: skuStr,
|
||
price: existing.price || 0,
|
||
salePrice: existing.salePrice || 0,
|
||
buyingPrice: existing.buyingPrice || 0,
|
||
stock: existing.stock || 0,
|
||
skuNo: existing.skuNo || '',
|
||
images: existing.images || []
|
||
};
|
||
});
|
||
// 同步更新 goodsSpec 的 specValue(每条记录一个值,JSON 格式)
|
||
if (goodsSpec.value) {
|
||
// 把 spec.value 拆成多条记录对应的 specValue 格式
|
||
const firstSpec = spec.value[0];
|
||
goodsSpec.value.specName = firstSpec?.value || '';
|
||
// specValue 在 save 时会被拆成多条记录,这里仅同步 specName
|
||
}
|
||
message.success(`已生成 ${combos.length} 条SKU`);
|
||
};
|
||
|
||
/**
|
||
* 批量设置SKU字段值
|
||
*/
|
||
const batchApply = () => {
|
||
if (skuList.value.length === 0) {
|
||
message.warning('请先生成SKU');
|
||
return;
|
||
}
|
||
let changed = 0;
|
||
skuList.value.forEach((item) => {
|
||
if (batchPrice.value !== undefined && batchPrice.value !== null) {
|
||
item.price = batchPrice.value;
|
||
changed++;
|
||
}
|
||
if (batchSalePrice.value !== undefined && batchSalePrice.value !== null) {
|
||
item.salePrice = batchSalePrice.value;
|
||
changed++;
|
||
}
|
||
if (batchBuyingPrice.value !== undefined && batchBuyingPrice.value !== null) {
|
||
item.buyingPrice = batchBuyingPrice.value;
|
||
changed++;
|
||
}
|
||
if (batchStock.value !== undefined && batchStock.value !== null) {
|
||
item.stock = batchStock.value;
|
||
changed++;
|
||
}
|
||
if (batchSkuNo.value) {
|
||
item.skuNo = batchSkuNo.value;
|
||
changed++;
|
||
}
|
||
});
|
||
if (changed > 0) {
|
||
message.success(`已批量设置 ${skuList.value.length} 条SKU`);
|
||
} else {
|
||
message.warning('请至少填写一个批量设置值');
|
||
}
|
||
};
|
||
|
||
|
||
const {resetFields} = useForm(form, rules);
|
||
|
||
const COMMISSION_PERCENT_FIELDS = [
|
||
'firstMoney',
|
||
'secondMoney',
|
||
'thirdMoney',
|
||
'firstDividend',
|
||
'secondDividend'
|
||
] as const;
|
||
|
||
type CommissionPercentField = (typeof COMMISSION_PERCENT_FIELDS)[number];
|
||
|
||
const toDbPercent = (val: unknown) => {
|
||
if (val === null || val === undefined) return val as any;
|
||
const num = Number(val);
|
||
if (!Number.isFinite(num)) return val as any;
|
||
return num / 100;
|
||
};
|
||
|
||
// Back-end stores percent as a fraction (e.g. 0.1 = 10%); UI inputs percent (e.g. 10 = 10%).
|
||
const toUiPercent = (val: unknown) => {
|
||
if (val === null || val === undefined) return val as any;
|
||
const num = Number(val);
|
||
if (!Number.isFinite(num)) return val as any;
|
||
// If historical data is already stored as 0~1, convert to 0~100 for display.
|
||
if (num <= 1) return Number((num * 100).toFixed(2));
|
||
return num;
|
||
};
|
||
|
||
/* 保存编辑 */
|
||
const save = () => {
|
||
if (!formRef.value) {
|
||
return;
|
||
}
|
||
formRef.value
|
||
.validate()
|
||
.then(() => {
|
||
loading.value = true;
|
||
// 快递配送时运费模板必选(后端也有此校验,前端提前拦截给出友好提示)
|
||
if (form.canExpress === 1 && !form.expressTemplateId && !merchantId) {
|
||
loading.value = false;
|
||
return message.error('请选择运费模板');
|
||
}
|
||
if (form.priceGift && !form.priceGiftNum)
|
||
return message.error('请输入会员赠品数量');
|
||
if (ensureTag.value.length) form.ensureTag = ensureTag.value.join();
|
||
if (canUseDate.value.length) form.canUseDate = canUseDate.value.join();
|
||
|
||
// 分销关闭时,避免把历史值一并保存到后端
|
||
if (form.isOpenCommission !== 1) {
|
||
form.commissionType = 10;
|
||
form.firstMoney = 0;
|
||
form.secondMoney = 0;
|
||
form.thirdMoney = 0;
|
||
form.firstDividend = 0;
|
||
form.secondDividend = 0;
|
||
form.deliveryMoney = 0;
|
||
}
|
||
if (form.isOpenCommission === 1 && form.commissionType === 20) {
|
||
// UI 输入:0~100;保存到后端:0~1(例如 10% => 0.1)
|
||
const invalidPercent = COMMISSION_PERCENT_FIELDS.some((k) => {
|
||
const v = (form as any)[k] ?? 0;
|
||
return v < 0 || v > 100;
|
||
});
|
||
if (invalidPercent) {
|
||
return message.error('佣金百分比需在 0~100 之间');
|
||
}
|
||
}
|
||
|
||
// if (form.dealerGift && !form.dealerGiftNum) return message.error('请输入经销商赠品数量');
|
||
if (form.commissionRole === 1) {
|
||
for (let i = 0; i < form.goodsRoleCommission.length; i++) {
|
||
if (
|
||
form.goodsRoleCommission[i].amount === undefined ||
|
||
form.goodsRoleCommission[i].amount === '' ||
|
||
form.goodsRoleCommission[i].amount === null
|
||
) {
|
||
return message.error(
|
||
'请输入' + form.goodsRoleCommission[i].roleName + '的分红金额'
|
||
);
|
||
}
|
||
}
|
||
}
|
||
// 多规格模式下,保存前确保 goodsSpec.specValue 与当前 spec 数组同步
|
||
if (form.specs === 1 && goodsSpec.value && spec.value.length) {
|
||
goodsSpec.value.specValue = JSON.stringify(spec.value);
|
||
}
|
||
// 方案B:主分类兜底取多分类第一个,保证旧字段/旧逻辑兼容
|
||
if (form.categoryIds && form.categoryIds.length && !form.categoryId) {
|
||
form.categoryId = form.categoryIds[0];
|
||
}
|
||
|
||
const formData: any = {
|
||
...form,
|
||
// 所属专区:数字数组 -> 逗号分隔字符串
|
||
sectionIds: sectionIdList.value.length
|
||
? sectionIdList.value.join(',')
|
||
: '',
|
||
content: content.value,
|
||
category: JSON.stringify(category.value),
|
||
files: JSON.stringify(files.value),
|
||
type: getMerchantId() ? 1 : 0
|
||
};
|
||
|
||
// 多规格数据: goodsSpecs 为数组(每条记录一个规格值),goodsSkus 为数组
|
||
if (form.specs === 1) {
|
||
// 把 spec.value 拆成多条记录,每条 specValue 存单个字符串值(JSON 格式)
|
||
const goodsSpecs: ShopGoodsSpec[] = [];
|
||
spec.value.forEach((s) => {
|
||
s.detail.forEach((d) => {
|
||
goodsSpecs.push({
|
||
specId: goodsSpec.value?.specId,
|
||
specName: s.value,
|
||
specValue: JSON.stringify(d) // JSON 格式存单个字符串,如 "\"红色\""
|
||
});
|
||
});
|
||
});
|
||
formData.goodsSpecs = goodsSpecs;
|
||
formData.goodsSkus = skuList.value.map((item) => ({
|
||
...item,
|
||
// 提取图片URL(images 是前端上传组件格式,转为单图片URL)
|
||
image: item.images?.[0]?.url || ''
|
||
}));
|
||
} else {
|
||
formData.goodsSpecs = [];
|
||
formData.goodsSkus = [];
|
||
}
|
||
if (isUpdate.value) {
|
||
formData.type = props.data.type;
|
||
}
|
||
if (formData.isOpenCommission === 1 && formData.commissionType === 20) {
|
||
// Convert percent fields for persistence: 10 => 0.1
|
||
COMMISSION_PERCENT_FIELDS.forEach((k) => {
|
||
(formData as any)[k as CommissionPercentField] = toDbPercent(
|
||
(formData as any)[k]
|
||
);
|
||
});
|
||
}
|
||
const saveOrUpdate = isUpdate.value ? updateShopGoods : addShopGoods;
|
||
saveOrUpdate(formData)
|
||
.then((msg) => {
|
||
loading.value = false;
|
||
message.success(msg);
|
||
updateVisible(false);
|
||
emit('done');
|
||
})
|
||
.catch((e) => {
|
||
loading.value = false;
|
||
message.error(e.message);
|
||
});
|
||
})
|
||
.catch(() => {
|
||
});
|
||
};
|
||
|
||
const expressTemplateList = ref<ShopExpressTemplate[]>([]);
|
||
const getExpressTemplateList = async () => {
|
||
expressTemplateList.value = await listShopExpressTemplate();
|
||
};
|
||
|
||
const getRoleList = async () => {
|
||
const roleList = await listShopCommissionRole();
|
||
form.goodsRoleCommission = [];
|
||
roleList.forEach((d) => {
|
||
form.goodsRoleCommission.push({
|
||
roleId: d.id,
|
||
roleName: d.title,
|
||
amount: 0
|
||
});
|
||
});
|
||
if (props?.data?.goodsId) {
|
||
const commissionList = await listShopGoodsRoleCommission({
|
||
goodsId: props?.data?.goodsId
|
||
});
|
||
form.goodsRoleCommission = form.goodsRoleCommission.map((d) => {
|
||
const find = commissionList.find((c) => c.roleId == d.roleId);
|
||
return {
|
||
...d,
|
||
amount: find?.amount || 0
|
||
};
|
||
});
|
||
}
|
||
};
|
||
|
||
// 判断是否为视频文件(支持 mp4/webm/mov/avi 等常见格式)
|
||
const isVideo = (url: string): boolean => {
|
||
if (!url) return false;
|
||
const videoExts = ['.mp4', '.webm', '.mov', '.avi', '.m3u8', '.ogg'];
|
||
const lowerUrl = url.toLowerCase().split('?')[0]; // 去掉查询参数
|
||
return videoExts.some((ext) => lowerUrl.endsWith(ext));
|
||
};
|
||
|
||
const onUploadImage = (item) => {
|
||
const {file} = item;
|
||
if (file.size / 1024 / 1024 > 10) {
|
||
message.error('大小不能超过 10MB');
|
||
return;
|
||
}
|
||
const hide = messageLoading({
|
||
content: '上传中..',
|
||
duration: 0,
|
||
mask: true
|
||
});
|
||
uploadOss(file)
|
||
.then((res) => {
|
||
hide();
|
||
form.image = res.path;
|
||
})
|
||
.catch((e) => {
|
||
message.error(e.message);
|
||
hide();
|
||
});
|
||
};
|
||
const onUploadSwiper = (item) => {
|
||
const {file} = item;
|
||
if (file.size / 1024 / 1024 > 10) {
|
||
message.error('大小不能超过 10MB');
|
||
return;
|
||
}
|
||
const hide = messageLoading({
|
||
content: '上传中..',
|
||
duration: 0,
|
||
mask: true
|
||
});
|
||
uploadOss(file)
|
||
.then((data) => {
|
||
hide();
|
||
files.value.push({
|
||
uid: data.id,
|
||
url: data.path,
|
||
status: 'done'
|
||
});
|
||
setPicker();
|
||
})
|
||
.catch((e) => {
|
||
message.error(e.message);
|
||
hide();
|
||
});
|
||
};
|
||
|
||
const swiperRef = ref();
|
||
const imageRef = ref();
|
||
const setPicker = () => {
|
||
const swiperEl = swiperRef.value?.$el?.querySelector('input[type="file"]');
|
||
if (swiperEl) {
|
||
swiperEl.removeAttribute('accept'); // 移除 accept 属性
|
||
swiperEl.removeAttribute('capture'); // 移除 capture 属性
|
||
swiperEl.setAttribute('name', 'file'); // 添加 name 属性
|
||
}
|
||
const imageEl = imageRef.value?.$el?.querySelector('input[type="file"]');
|
||
if (imageEl) {
|
||
imageEl.removeAttribute('accept'); // 移除 accept 属性
|
||
imageEl.removeAttribute('capture'); // 移除 capture 属性
|
||
imageEl.setAttribute('name', 'file'); // 添加 name 属性
|
||
}
|
||
};
|
||
watch(
|
||
() => props.visible,
|
||
async (visible) => {
|
||
if (visible) {
|
||
await getExpressTemplateList();
|
||
// 加载所属专区选项
|
||
listHomeSections({})
|
||
.then((list) => {
|
||
zoneOptions.value = (list || []).map((z) => ({
|
||
label: z.title || '',
|
||
value: z.sectionId as number
|
||
}));
|
||
})
|
||
.catch(() => {
|
||
zoneOptions.value = [];
|
||
});
|
||
|
||
images.value = [];
|
||
sectionIdList.value = [];
|
||
category.value = [];
|
||
files.value = [];
|
||
videos.value = [];
|
||
canUseDate.value = [];
|
||
ensureTag.value = [];
|
||
ensureTagItem.value = '';
|
||
if (props.data) {
|
||
assignObject(form, props.data);
|
||
// 所属专区:逗号分隔字符串 -> 数字数组
|
||
sectionIdList.value = form.sectionIds
|
||
? form.sectionIds
|
||
.split(',')
|
||
.map((s) => Number(s.trim()))
|
||
.filter((n) => !Number.isNaN(n))
|
||
: [];
|
||
if (form.commissionType === undefined || form.commissionType === null) {
|
||
form.commissionType = 10;
|
||
}
|
||
if (form.isOpenCommission === 1 && form.commissionType === 20) {
|
||
// Convert historical DB values (0~1) to UI percent (0~100).
|
||
COMMISSION_PERCENT_FIELDS.forEach((k) => {
|
||
(form as any)[k as CommissionPercentField] = toUiPercent(
|
||
(form as any)[k]
|
||
);
|
||
});
|
||
}
|
||
if (props.data.image) {
|
||
images.value.push({
|
||
uid: uuid(),
|
||
url: props.data.image,
|
||
status: 'done'
|
||
});
|
||
}
|
||
if (props.data.files) {
|
||
files.value = JSON.parse(props.data.files);
|
||
}
|
||
if (props.data.goodsSpecs && props.data.goodsSpecs.length > 0) {
|
||
if (props.data.specs == 1) {
|
||
// 多条记录合并为 spec.value;兼容旧格式(单条存 JSON 数组)
|
||
const merged = mergeGoodsSpecs(props.data.goodsSpecs);
|
||
spec.value = merged.length > 0 ? merged : parseSpecValue(props.data.goodsSpecs[0].specValue);
|
||
goodsSpec.value = {
|
||
specId: props.data.goodsSpecs[0].specId,
|
||
specName: props.data.goodsSpecs[0].specName,
|
||
specValue: JSON.stringify(spec.value)
|
||
};
|
||
form.specName = props.data.goodsSpecs[0].specName || '';
|
||
}
|
||
}
|
||
if (props.data.goodsSkus && props.data.goodsSkus.length > 0) {
|
||
skuList.value = props.data.goodsSkus.map((d) => ({
|
||
...d,
|
||
images: d.image ? [{uid: uuid(), url: d.image, status: 'done' as const}] : []
|
||
}));
|
||
}
|
||
// 多规格商品(specs == 1)从 API 加载规格和 SKU 数据
|
||
// 注意:列表 API 通常不返回 goodsSpecs/goodsSkus,需要单独查询
|
||
if (props.data.specs == 1 && props.data.goodsId) {
|
||
listShopGoodsSpec({goodsId: props.data.goodsId}).then((data) => {
|
||
if (data.length > 0) {
|
||
// 多条记录合并为 spec.value;兼容旧格式
|
||
const merged = mergeGoodsSpecs(data);
|
||
spec.value = merged.length > 0 ? merged : parseSpecValue(data[0].specValue);
|
||
goodsSpec.value = {
|
||
specId: data[0].specId,
|
||
specName: data[0].specName,
|
||
specValue: JSON.stringify(spec.value)
|
||
};
|
||
form.specName = data[0].specName || '';
|
||
}
|
||
});
|
||
listShopGoodsSku({goodsId: props.data.goodsId}).then((data) => {
|
||
skuList.value = data.map((d) => ({
|
||
...d,
|
||
images: d.image ? [{uid: uuid(), url: d.image, status: 'done' as const}] : []
|
||
}));
|
||
});
|
||
}
|
||
if (props.data.video) {
|
||
videos.value = [
|
||
{
|
||
uid: uuid(),
|
||
url: props.data.video,
|
||
status: 'done'
|
||
}
|
||
];
|
||
}
|
||
// 商品分类
|
||
if (props.data.parentName) {
|
||
category.value.push(props.data.parentName);
|
||
}
|
||
if (props.data.category) {
|
||
category.value = JSON.parse(props.data.category);
|
||
}
|
||
if (props.data.content) {
|
||
content.value = toEditorContent(props.data.content);
|
||
}
|
||
|
||
isUpdate.value = true;
|
||
if (form.ensureTag) {
|
||
ensureTag.value = form.ensureTag.split(',');
|
||
}
|
||
// 🎯 优先级设置栏目(方案B:支持多分类回填):
|
||
// 1. 如果传入了 categoryIds,使用多分类
|
||
// 2. 否则如果传入了 categoryId,兼容单分类
|
||
// 3. 否则使用上次保存的栏目
|
||
if (props.data.categoryIds && props.data.categoryIds.length) {
|
||
form.categoryIds = props.data.categoryIds;
|
||
form.categoryId = props.data.categoryId ?? props.data.categoryIds[0];
|
||
} else if (props.data.categoryId) {
|
||
form.categoryId = props.data?.categoryId;
|
||
form.categoryIds = [props.data.categoryId];
|
||
} else {
|
||
const lastCategory = getLastCategory();
|
||
if (lastCategory) {
|
||
form.categoryId = lastCategory;
|
||
form.categoryIds = [lastCategory];
|
||
}
|
||
}
|
||
await getRoleList();
|
||
} else {
|
||
await getRoleList();
|
||
spec.value = [];
|
||
goodsSpec.value = undefined;
|
||
skuList.value = [];
|
||
isUpdate.value = false;
|
||
}
|
||
setPicker();
|
||
} else {
|
||
resetFields();
|
||
}
|
||
},
|
||
{immediate: true}
|
||
);
|
||
</script>
|
||
|
||
<style scoped>
|
||
</style>
|