产品列表配置附加费

This commit is contained in:
wuweihua
2026-09-08 11:55:31 +08:00
parent 38796d8683
commit 520fc32a4e
12 changed files with 569 additions and 3 deletions
@@ -18,6 +18,8 @@ import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.page.TableDataInfo; import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.enums.BusinessType; import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.price.domain.PrcSurcharge; import com.ruoyi.price.domain.PrcSurcharge;
import com.ruoyi.price.domain.PrcSurchargeRel;
import com.ruoyi.price.service.IPrcSurchargeRelService;
import com.ruoyi.price.service.IPrcSurchargeService; import com.ruoyi.price.service.IPrcSurchargeService;
/** /**
@@ -32,6 +34,9 @@ public class SurchargeController extends BaseController
@Autowired @Autowired
private IPrcSurchargeService surchargeService; private IPrcSurchargeService surchargeService;
@Autowired
private IPrcSurchargeRelService surchargeRelService;
@PreAuthorize("@ss.hasPermi('price:surcharge:list')") @PreAuthorize("@ss.hasPermi('price:surcharge:list')")
@GetMapping("/list") @GetMapping("/list")
public TableDataInfo list(PrcSurcharge surcharge) public TableDataInfo list(PrcSurcharge surcharge)
@@ -48,6 +53,39 @@ public class SurchargeController extends BaseController
return success(surchargeService.selectPrcSurchargeById(scId)); return success(surchargeService.selectPrcSurchargeById(scId));
} }
/**
* 查询指定产品已关联的附加费(非分页,供产品列表抽屉使用)
*/
@PreAuthorize("@ss.hasPermi('price:surcharge:query')")
@GetMapping("/byProduct/{productCode}")
public AjaxResult listByProduct(@PathVariable String productCode)
{
return success(surchargeRelService.selectRelByProductCode(productCode));
}
/**
* 绑定附加费到产品/明细(body: scId, productId, productCode, detailId;detailId 空=产品级)
*/
@PreAuthorize("@ss.hasPermi('price:surcharge:add')")
@Log(title = "附加费关联", businessType = BusinessType.INSERT)
@PostMapping("/rel")
public AjaxResult addRel(@RequestBody PrcSurchargeRel rel)
{
rel.setCreateBy(getUsername());
return toAjax(surchargeRelService.insertPrcSurchargeRel(rel));
}
/**
* 解绑(删除关联记录,不删除附加费本身)
*/
@PreAuthorize("@ss.hasPermi('price:surcharge:remove')")
@Log(title = "附加费关联", businessType = BusinessType.DELETE)
@DeleteMapping("/rel/{relIds}")
public AjaxResult removeRel(@PathVariable Long[] relIds)
{
return toAjax(surchargeRelService.deletePrcSurchargeRelByIds(relIds));
}
@PreAuthorize("@ss.hasPermi('price:surcharge:add')") @PreAuthorize("@ss.hasPermi('price:surcharge:add')")
@Log(title = "附加费配置", businessType = BusinessType.INSERT) @Log(title = "附加费配置", businessType = BusinessType.INSERT)
@PostMapping @PostMapping
@@ -30,6 +30,7 @@ public class BmsProduct extends BaseEntity
private String sourceSheet; private String sourceSheet;
private Long tmsId; private Long tmsId;
private Integer countryCount; private Integer countryCount;
private Integer surchargeCount;
private List<BmsProductCountry> countries; private List<BmsProductCountry> countries;
/** /**
* 查询用:ACTIVE已生效 / PENDING待生效 / EXPIRED已失效 * 查询用:ACTIVE已生效 / PENDING待生效 / EXPIRED已失效
@@ -0,0 +1,44 @@
package com.ruoyi.price.domain;
import java.math.BigDecimal;
import com.ruoyi.common.core.domain.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 附加费与产品/明细关联 prc_surcharge_rel
* 同时携带附加费展示字段(JOIN prc_surcharge)与明细展示字段(LEFT JOIN bms_product_country),
* 便于产品列表抽屉直接渲染。
*
* 语义:
* detail_id 为空 = 产品级附加费(作用于该产品全部明细)
* detail_id 有值 = 明细级附加费(作用于该产品某个 国家×重量段 明细)
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class PrcSurchargeRel extends BaseEntity
{
private static final long serialVersionUID = 1L;
private Long relId;
private Long scId;
private Long productId;
private String productCode;
private Long detailId;
/** 附加费冗余展示字段 */
private String scName;
private String feeType;
private String chargeType;
private BigDecimal chargeValue;
private String currency;
private String roundMode;
private String ssCode;
private String countryCode;
private String scNote;
/** 明细展示字段 */
private String countryName;
private BigDecimal weightMin;
private BigDecimal weightMax;
}
@@ -0,0 +1,24 @@
package com.ruoyi.price.mapper;
import java.util.List;
import com.ruoyi.price.domain.PrcSurchargeRel;
/**
* 附加费与产品/明细关联 数据层
*
* @author ruoyi
*/
public interface PrcSurchargeRelMapper
{
/**
* 查询关联列表(JOIN 附加费 + 明细),支持按 productCode / scId 过滤
*/
public List<PrcSurchargeRel> selectPrcSurchargeRelList(PrcSurchargeRel rel);
public int insertPrcSurchargeRel(PrcSurchargeRel rel);
public int deletePrcSurchargeRelByIds(Long[] relIds);
/** 删除产品时清理其关联 */
public int deleteRelByProductCodes(String[] productCodes);
}
@@ -0,0 +1,24 @@
package com.ruoyi.price.service;
import java.util.List;
import com.ruoyi.price.domain.PrcSurchargeRel;
/**
* 附加费与产品/明细关联 服务层
*
* @author ruoyi
*/
public interface IPrcSurchargeRelService
{
/** 查询某产品已关联的附加费(含附加费与明细展示字段) */
public List<PrcSurchargeRel> selectRelByProductCode(String productCode);
public List<PrcSurchargeRel> selectPrcSurchargeRelList(PrcSurchargeRel rel);
public int insertPrcSurchargeRel(PrcSurchargeRel rel);
public int deletePrcSurchargeRelByIds(Long[] relIds);
/** 删除产品时清理其关联 */
public int deleteRelByProductCodes(String[] productCodes);
}
@@ -32,6 +32,7 @@ import com.ruoyi.price.domain.BmsProductCountry;
import com.ruoyi.price.mapper.BmsProductCountryMapper; import com.ruoyi.price.mapper.BmsProductCountryMapper;
import com.ruoyi.price.mapper.BmsProductMapper; import com.ruoyi.price.mapper.BmsProductMapper;
import com.ruoyi.price.service.IBmsProductService; import com.ruoyi.price.service.IBmsProductService;
import com.ruoyi.price.service.IPrcSurchargeRelService;
import com.ruoyi.system.domain.BmsRegion; import com.ruoyi.system.domain.BmsRegion;
import com.ruoyi.system.mapper.BmsRegionMapper; import com.ruoyi.system.mapper.BmsRegionMapper;
@@ -64,6 +65,9 @@ public class BmsProductServiceImpl implements IBmsProductService
@Autowired @Autowired
private BmsRegionMapper regionMapper; private BmsRegionMapper regionMapper;
@Autowired
private IPrcSurchargeRelService surchargeRelService;
@Override @Override
public BmsProduct selectBmsProductById(Long productId) public BmsProduct selectBmsProductById(Long productId)
{ {
@@ -116,6 +120,7 @@ public class BmsProductServiceImpl implements IBmsProductService
if (!codes.isEmpty()) if (!codes.isEmpty())
{ {
countryMapper.deleteByProductCodes(codes.toArray(new String[0])); countryMapper.deleteByProductCodes(codes.toArray(new String[0]));
surchargeRelService.deleteRelByProductCodes(codes.toArray(new String[0]));
} }
return productMapper.deleteBmsProductByIds(productIds); return productMapper.deleteBmsProductByIds(productIds);
} }
@@ -0,0 +1,79 @@
package com.ruoyi.price.service.impl;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.price.domain.PrcSurchargeRel;
import com.ruoyi.price.mapper.PrcSurchargeRelMapper;
import com.ruoyi.price.service.IPrcSurchargeRelService;
/**
* 附加费与产品/明细关联 服务层实现
*
* @author ruoyi
*/
@Service
public class PrcSurchargeRelServiceImpl implements IPrcSurchargeRelService
{
@Autowired
private PrcSurchargeRelMapper relMapper;
@Override
public List<PrcSurchargeRel> selectRelByProductCode(String productCode)
{
PrcSurchargeRel query = new PrcSurchargeRel();
query.setProductCode(productCode);
return relMapper.selectPrcSurchargeRelList(query);
}
@Override
public List<PrcSurchargeRel> selectPrcSurchargeRelList(PrcSurchargeRel rel)
{
return relMapper.selectPrcSurchargeRelList(rel);
}
@Override
public int insertPrcSurchargeRel(PrcSurchargeRel rel)
{
validate(rel);
return relMapper.insertPrcSurchargeRel(rel);
}
@Override
public int deletePrcSurchargeRelByIds(Long[] relIds)
{
if (relIds == null || relIds.length == 0)
{
return 0;
}
return relMapper.deletePrcSurchargeRelByIds(relIds);
}
@Override
public int deleteRelByProductCodes(String[] productCodes)
{
if (productCodes == null || productCodes.length == 0)
{
return 0;
}
return relMapper.deleteRelByProductCodes(productCodes);
}
private void validate(PrcSurchargeRel rel)
{
if (rel.getScId() == null)
{
throw new ServiceException("附加费ID不能为空");
}
if (rel.getProductId() == null)
{
throw new ServiceException("产品ID不能为空");
}
if (StringUtils.isEmpty(rel.getProductCode()))
{
throw new ServiceException("产品代码不能为空");
}
}
}
@@ -23,13 +23,15 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<result property="updateBy" column="updated_by" /> <result property="updateBy" column="updated_by" />
<result property="updateTime" column="updated_at" /> <result property="updateTime" column="updated_at" />
<result property="countryCount" column="country_count" /> <result property="countryCount" column="country_count" />
<result property="surchargeCount" column="surcharge_count" />
</resultMap> </resultMap>
<sql id="selectBmsProductVo"> <sql id="selectBmsProductVo">
select p.product_id, p.product_code, p.product_name, p.service_level, p.cargo_attr, select p.product_id, p.product_code, p.product_name, p.service_level, p.cargo_attr,
p.dest_region, p.update_note, p.tax_mode, p.effective_date, p.status, p.dest_region, p.update_note, p.tax_mode, p.effective_date, p.status,
p.source_sheet, p.remark, p.tms_id, p.created_by, p.created_at, p.updated_by, p.updated_at, p.source_sheet, p.remark, p.tms_id, p.created_by, p.created_at, p.updated_by, p.updated_at,
(select count(distinct c.country_name) from bms_product_country c where c.product_code = p.product_code) as country_count (select count(distinct c.country_name) from bms_product_country c where c.product_code = p.product_code) as country_count,
(select count(*) from prc_surcharge_rel r where r.product_id = p.product_id) as surcharge_count
from bms_product p from bms_product p
</sql> </sql>
@@ -0,0 +1,84 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.price.mapper.PrcSurchargeRelMapper">
<resultMap type="PrcSurchargeRel" id="PrcSurchargeRelResult">
<id property="relId" column="rel_id" />
<result property="scId" column="sc_id" />
<result property="productId" column="product_id" />
<result property="productCode" column="product_code" />
<result property="detailId" column="detail_id" />
<result property="scName" column="sc_name" />
<result property="feeType" column="fee_type" />
<result property="chargeType" column="charge_type" />
<result property="chargeValue" column="charge_value" />
<result property="currency" column="currency" />
<result property="roundMode" column="round_mode" />
<result property="ssCode" column="ss_code" />
<result property="countryCode" column="country_code" />
<result property="scNote" column="sc_note" />
<result property="countryName" column="country_name" />
<result property="weightMin" column="weight_min" />
<result property="weightMax" column="weight_max" />
<result property="createBy" column="created_by" />
<result property="createTime" column="created_at" />
<result property="updateBy" column="updated_by" />
<result property="updateTime" column="updated_at" />
</resultMap>
<sql id="selectPrcSurchargeRelVo">
select r.rel_id, r.sc_id, r.product_id, r.product_code, r.detail_id,
r.created_by, r.created_at, r.updated_by, r.updated_at,
s.sc_name, s.fee_type, s.charge_type, s.charge_value, s.currency,
s.round_mode, s.ss_code, s.country_code, s.sc_note,
c.country_name, c.weight_min, c.weight_max
from prc_surcharge_rel r
join prc_surcharge s on s.sc_id = r.sc_id
left join bms_product_country c on c.detail_id = r.detail_id
</sql>
<select id="selectPrcSurchargeRelList" parameterType="PrcSurchargeRel" resultMap="PrcSurchargeRelResult">
<include refid="selectPrcSurchargeRelVo"/>
<where>
<if test="productId != null">
AND r.product_id = #{productId}
</if>
<if test="productCode != null and productCode != ''">
AND r.product_code = #{productCode}
</if>
<if test="scId != null">
AND r.sc_id = #{scId}
</if>
</where>
order by r.detail_id is not null, r.rel_id asc
</select>
<insert id="insertPrcSurchargeRel" parameterType="PrcSurchargeRel" useGeneratedKeys="true" keyProperty="relId">
insert into prc_surcharge_rel (
sc_id, product_id, product_code, detail_id,
<if test="createBy != null and createBy != ''">created_by,</if>
created_at
) values (
#{scId}, #{productId}, #{productCode}, #{detailId},
<if test="createBy != null and createBy != ''">#{createBy},</if>
sysdate()
)
</insert>
<delete id="deletePrcSurchargeRelByIds" parameterType="Long">
delete from prc_surcharge_rel where rel_id in
<foreach item="relId" collection="array" open="(" separator="," close=")">
#{relId}
</foreach>
</delete>
<delete id="deleteRelByProductCodes">
delete from prc_surcharge_rel where product_code in
<foreach item="code" collection="array" open="(" separator="," close=")">
#{code}
</foreach>
</delete>
</mapper>
+39
View File
@@ -0,0 +1,39 @@
-- 附加费关联表:维护 prc_surcharge 与 bms_product(产品表) / bms_product_country(明细表) 的关系
-- 语义:
-- product_id 产品主键(外键关联 bms_product.product_id,与 product_code 一一对应)
-- product_code 产品对接代码(便于与明细表 bms_product_country 及前端交互)
-- detail_id 为空 = 产品级附加费(作用于该产品全部明细)
-- 有值 = 明细级附加费(作用于该产品某个 国家×重量段 明细)
-- 一个附加费可关联多个产品/明细;一个产品/明细可挂多个附加费(多对多)
-- 执行后即可在「产品列表」按产品/明细维护附加费关联
SET NAMES utf8mb4;
CREATE TABLE IF NOT EXISTS prc_surcharge_rel (
rel_id BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '关联ID',
sc_id BIGINT(20) NOT NULL COMMENT '附加费配置ID prc_surcharge.sc_id',
product_id BIGINT(20) NOT NULL COMMENT '产品ID bms_product.product_id',
product_code VARCHAR(20) NOT NULL COMMENT '产品对接代码 bms_product.product_code',
detail_id BIGINT(20) DEFAULT NULL COMMENT '产品明细ID bms_product_country.detail_id(空=作用于产品全部明细)',
created_by VARCHAR(64) DEFAULT '' COMMENT '创建人',
created_at DATETIME DEFAULT NULL COMMENT '创建时间',
updated_by VARCHAR(64) DEFAULT '' COMMENT '更新人',
updated_at DATETIME DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (rel_id),
UNIQUE KEY uk_rel_scope (sc_id, product_id, detail_id),
INDEX idx_rel_sc (sc_id),
INDEX idx_rel_product (product_id),
INDEX idx_rel_product_code (product_code),
INDEX idx_rel_detail (detail_id),
CONSTRAINT fk_rel_sc FOREIGN KEY (sc_id) REFERENCES prc_surcharge (sc_id) ON DELETE CASCADE,
CONSTRAINT fk_rel_product FOREIGN KEY (product_id) REFERENCES bms_product (product_id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='附加费与产品/明细关联表';
-- 若已按旧版(无 product_id)建表且已有数据,升级步骤:
-- ALTER TABLE prc_surcharge_rel ADD COLUMN product_id BIGINT(20) DEFAULT NULL COMMENT '产品ID' AFTER sc_id;
-- UPDATE prc_surcharge_rel r JOIN bms_product p ON p.product_code = r.product_code SET r.product_id = p.product_id;
-- ALTER TABLE prc_surcharge_rel MODIFY COLUMN product_id BIGINT(20) NOT NULL COMMENT '产品ID bms_product.product_id';
-- ALTER TABLE prc_surcharge_rel ADD INDEX idx_rel_product (product_id);
-- ALTER TABLE prc_surcharge_rel ADD CONSTRAINT fk_rel_product FOREIGN KEY (product_id) REFERENCES bms_product (product_id) ON DELETE CASCADE;
-- 若上一版方案(prc_surcharge 加 product_code 字段)已执行,可手动回退(MySQL 5.7 不支持 DROP COLUMN IF EXISTS):
-- ALTER TABLE prc_surcharge DROP COLUMN product_code;
+25
View File
@@ -19,6 +19,31 @@ export function getSurcharge(scId) {
}) })
} }
/** 查询指定产品的附加费关联(非分页,含附加费与明细展示字段) */
export function listSurchargeByProduct(productCode) {
return request({
url: '/price/surcharge/byProduct/' + productCode,
method: 'get'
})
}
/** 绑定附加费到产品/明细(scId + productCode + detailId) */
export function addSurchargeRel(data) {
return request({
url: '/price/surcharge/rel',
method: 'post',
data: data
})
}
/** 解绑附加费关联 */
export function delSurchargeRel(relIds) {
return request({
url: '/price/surcharge/rel/' + relIds,
method: 'delete'
})
}
/** 新增附加费配置 */ /** 新增附加费配置 */
export function addSurcharge(data) { export function addSurcharge(data) {
return request({ return request({
@@ -40,6 +40,7 @@
<template #col-action="{ row }"> <template #col-action="{ row }">
<el-button link type="primary" icon="View" @click="openCountries(row)" v-hasPermi="['price:product:query']">产品明细</el-button> <el-button link type="primary" icon="View" @click="openCountries(row)" v-hasPermi="['price:product:query']">产品明细</el-button>
<el-button link type="primary" icon="Money" @click="openSurcharges(row)" v-hasPermi="['price:surcharge:query']">附加费</el-button>
<el-button :disabled="isProductActive(row)" :title="isProductActive(row) ? '已生效不可编辑' : ''" link type="primary" icon="Edit" @click="handleUpdate(row)" v-hasPermi="['price:product:edit']">编辑</el-button> <el-button :disabled="isProductActive(row)" :title="isProductActive(row) ? '已生效不可编辑' : ''" link type="primary" icon="Edit" @click="handleUpdate(row)" v-hasPermi="['price:product:edit']">编辑</el-button>
<el-button :disabled="isProductActive(row)" :title="isProductActive(row) ? '已生效不可删除' : ''" link type="primary" icon="Delete" @click="handleDelete(row.productId)" v-hasPermi="['price:product:remove']">删除</el-button> <el-button :disabled="isProductActive(row)" :title="isProductActive(row) ? '已生效不可删除' : ''" link type="primary" icon="Delete" @click="handleDelete(row.productId)" v-hasPermi="['price:product:remove']">删除</el-button>
</template> </template>
@@ -149,6 +150,75 @@
</el-table> </el-table>
</el-drawer> </el-drawer>
<el-drawer v-model="surchargeOpen" size="920px" :title="surchargeTitle" append-to-body>
<div class="mb8" style="display: flex; justify-content: space-between">
<el-button type="primary" plain icon="Plus" @click="handleRelAdd" v-hasPermi="['price:surcharge:add']">新增关联</el-button>
</div>
<el-table v-loading="surchargeLoading" :data="surchargeList" border max-height="calc(100vh - 160px)" empty-text="该产品暂无关联附加费">
<el-table-column label="附加费名称" prop="scName" min-width="140" show-overflow-tooltip />
<el-table-column label="计费方式" width="110" align="center">
<template #default="{ row }">{{ chargeTypeLabel(row.chargeType) }}</template>
</el-table-column>
<el-table-column label="计费数值" width="140" align="right">
<template #default="{ row }">
<span class="charge-value">{{ formatChargeValue(row) }}</span>
</template>
</el-table-column>
<el-table-column label="取值方式" width="100" align="center">
<template #default="{ row }">{{ roundModeLabel(row.roundMode) }}</template>
</el-table-column>
<el-table-column label="适用国家" width="110" align="center">
<template #default="{ row }">{{ countryLabel(row.countryCode) }}</template>
</el-table-column>
<el-table-column label="作用范围" width="170" align="center">
<template #default="{ row }">
<el-tag v-if="row.detailId == null" type="primary" size="small" effect="plain">产品级 · 全部明细</el-tag>
<el-tag v-else type="warning" size="small" effect="plain">{{ row.countryName || '-' }} · {{ row.weightMin }}-{{ row.weightMax }}kg</el-tag>
</template>
</el-table-column>
<el-table-column label="状态" width="80" align="center">
<template #default="{ row }">
<el-tag :type="String(row.ssCode).toLowerCase() === 'on' ? 'success' : 'info'" size="small">
{{ String(row.ssCode).toLowerCase() === 'on' ? '有效' : '停用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="备注" prop="scNote" min-width="120" show-overflow-tooltip />
<el-table-column label="操作" width="90" align="center" fixed="right">
<template #default="{ row }">
<el-button link type="danger" icon="Delete" @click="handleRelDelete(row.relId)" v-hasPermi="['price:surcharge:remove']">解绑</el-button>
</template>
</el-table-column>
</el-table>
</el-drawer>
<el-dialog :title="relTitle" v-model="relOpen" width="560px" append-to-body>
<el-form ref="relFormRef" :model="relForm" :rules="relRules" label-width="100px">
<el-form-item label="附加费" prop="scId">
<el-select v-model="relForm.scId" placeholder="请选择附加费" filterable style="width: 100%">
<el-option v-for="s in surchargePool" :key="s.scId" :label="s.scName" :value="s.scId" />
</el-select>
</el-form-item>
<el-form-item label="作用范围" prop="scope">
<el-radio-group v-model="relForm.scope">
<el-radio value="PRODUCT">产品级(全部明细)</el-radio>
<el-radio value="DETAIL">明细级(指定国家×重量段)</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item v-if="relForm.scope === 'DETAIL'" label="明细" prop="detailId">
<el-select v-model="relForm.detailId" placeholder="请选择明细" filterable style="width: 100%">
<el-option v-for="d in countryList" :key="d.detailId" :label="detailLabel(d)" :value="d.detailId" />
</el-select>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="submitRel">确 定</el-button>
<el-button @click="relOpen = false">取 消</el-button>
</div>
</template>
</el-dialog>
<excel-import-dialog <excel-import-dialog
ref="importRef" ref="importRef"
title="产品 Excel 导入" title="产品 Excel 导入"
@@ -163,9 +233,12 @@
<script setup name="PriceProduct"> <script setup name="PriceProduct">
import ExcelImportDialog from '@/components/ExcelImportDialog' import ExcelImportDialog from '@/components/ExcelImportDialog'
import { listProduct, getProduct, addProduct, updateProduct, delProduct, listProductCountries } from '@/api/price/product' import { listProduct, getProduct, addProduct, updateProduct, delProduct, listProductCountries } from '@/api/price/product'
import { listSurchargeByProduct, listSurcharge, addSurchargeRel, delSurchargeRel } from '@/api/price/surcharge'
import { listRegionOptions } from '@/api/data/region'
import { onMounted } from 'vue'
const { proxy } = getCurrentInstance() const { proxy } = getCurrentInstance()
const { bms_product_effect_status, bms_tax_mode } = useDict('bms_product_effect_status', 'bms_tax_mode') const { bms_product_effect_status, bms_tax_mode, bms_round_mode, bms_charge_type } = useDict('bms_product_effect_status', 'bms_tax_mode', 'bms_round_mode', 'bms_charge_type')
const tableRef = ref() const tableRef = ref()
const importRef = ref() const importRef = ref()
const formRef = ref() const formRef = ref()
@@ -176,6 +249,18 @@ const countryTitle = ref('')
const countryLoading = ref(false) const countryLoading = ref(false)
const countryList = ref([]) const countryList = ref([])
const countryKeyword = ref('') const countryKeyword = ref('')
const surchargeOpen = ref(false)
const surchargeTitle = ref('')
const surchargeLoading = ref(false)
const surchargeList = ref([])
const currentProduct = ref(null)
const regionOptions = ref([])
const surchargePool = ref([])
const relOpen = ref(false)
const relTitle = ref('')
const relFormRef = ref()
const filteredCountryList = computed(() => { const filteredCountryList = computed(() => {
const kw = countryKeyword.value.trim().toLowerCase() const kw = countryKeyword.value.trim().toLowerCase()
if (!kw) return countryList.value if (!kw) return countryList.value
@@ -194,9 +279,10 @@ const columns = [
{ prop: 'taxMode', label: '税模式', width: 90, align: 'center', dict: 'bms_tax_mode' }, { prop: 'taxMode', label: '税模式', width: 90, align: 'center', dict: 'bms_tax_mode' },
{ prop: 'effectiveDate', label: '生效日期', width: 120, align: 'center' }, { prop: 'effectiveDate', label: '生效日期', width: 120, align: 'center' },
{ prop: 'countryCount', label: '国家数', width: 80, align: 'center' }, { prop: 'countryCount', label: '国家数', width: 80, align: 'center' },
{ prop: 'surchargeCount', label: '附加费数', width: 90, align: 'center' },
{ prop: 'status', label: '状态', width: 90, align: 'center', slot: 'status' }, { prop: 'status', label: '状态', width: 90, align: 'center', slot: 'status' },
{ prop: 'updateNote', label: '更新内容', minWidth: 160, showOverflowTooltip: true }, { prop: 'updateNote', label: '更新内容', minWidth: 160, showOverflowTooltip: true },
{ prop: 'action', label: '操作', width: 240, fixed: 'right', slot: 'action' } { prop: 'action', label: '操作', width: 300, fixed: 'right', slot: 'action' }
] ]
const searchColumns = [ const searchColumns = [
@@ -343,9 +429,124 @@ function countrySpanMethod({ column, rowIndex }) {
while (rowIndex + count < list.length && list[rowIndex + count]?.countryCode === cur) count++ while (rowIndex + count < list.length && list[rowIndex + count]?.countryCode === cur) count++
return { rowspan: count, colspan: 1 } return { rowspan: count, colspan: 1 }
} }
// ===== 产品附加费关联 =====
const relData = reactive({
relForm: {},
relRules: {
scId: [{ required: true, message: '请选择附加费', trigger: 'change' }]
}
})
const { relForm, relRules } = toRefs(relData)
function openSurcharges(row) {
currentProduct.value = row
surchargeTitle.value = `${row.productCode} · ${row.productName} · 附加费关联`
surchargeOpen.value = true
loadSurcharges()
}
function loadSurcharges() {
if (!currentProduct.value) return
surchargeLoading.value = true
listSurchargeByProduct(currentProduct.value.productCode).then(res => {
surchargeList.value = res.data || []
}).finally(() => {
surchargeLoading.value = false
})
}
function loadSurchargePool() {
return listSurcharge({ ssCode: 'on', pageNum: 1, pageSize: 1000 }).then(res => {
surchargePool.value = res.rows || []
})
}
function resetRelForm() {
relForm.value = {
scId: undefined,
scope: 'PRODUCT',
detailId: undefined
}
relFormRef.value && relFormRef.value.resetFields()
}
function handleRelAdd() {
resetRelForm()
relTitle.value = '新增附加费关联'
relOpen.value = true
loadSurchargePool()
}
function submitRel() {
relFormRef.value.validate(valid => {
if (!valid) return
if (relForm.value.scope === 'DETAIL' && !relForm.value.detailId) {
proxy.$modal.msgError('请选择明细')
return
}
const payload = {
scId: relForm.value.scId,
productId: currentProduct.value ? currentProduct.value.productId : undefined,
productCode: currentProduct.value ? currentProduct.value.productCode : undefined,
detailId: relForm.value.scope === 'DETAIL' ? relForm.value.detailId : null
}
addSurchargeRel(payload).then(() => {
proxy.$modal.msgSuccess('关联成功')
relOpen.value = false
loadSurcharges()
tableRef.value.refresh()
})
})
}
function handleRelDelete(relId) {
proxy.$modal.confirm('是否确认解绑该附加费?').then(() => {
return delSurchargeRel(relId)
}).then(() => {
proxy.$modal.msgSuccess('解绑成功')
loadSurcharges()
tableRef.value.refresh()
}).catch(() => {})
}
function detailLabel(d) {
const w = d.weightMin != null && d.weightMax != null ? `${d.weightMin}-${d.weightMax}kg` : '-'
return `${d.countryName || d.countryCode || '-'} · ${w}`
}
function chargeTypeLabel(val) {
const found = bms_charge_type.value.find(c => c.value === val)
return found ? found.label : (val || '-')
}
function roundModeLabel(val) {
const found = bms_round_mode.value.find(d => d.value === val)
return found ? found.label : (val || '-')
}
function formatChargeValue(row) {
if (row.chargeValue == null) return '-'
const v = Number(row.chargeValue)
if (row.chargeType === 'RATE') return (v * 100).toFixed(2) + '%'
return v + ' ' + (row.currency || 'CNY')
}
function countryLabel(code) {
if (!code) return '全部'
const found = regionOptions.value.find(r => r.countryCode2 === code)
return found ? (found.countryNameCn || found.countryNameEn || code) : code
}
onMounted(() => {
listRegionOptions().then(res => {
regionOptions.value = res.data || []
})
})
</script> </script>
<style scoped> <style scoped>
.mb16 { margin-bottom: 16px; } .mb16 { margin-bottom: 16px; }
.pre-wrap { white-space: pre-wrap; font-size: 12px; line-height: 1.4; } .pre-wrap { white-space: pre-wrap; font-size: 12px; line-height: 1.4; }
.charge-value { font-weight: 600; color: #d63031; }
</style> </style>