Commit 882f447d authored by 法拉51246's avatar 法拉51246

打印烂的

大屏没写
腾讯地图key替换为客户的
parent 29054b48
......@@ -45,6 +45,12 @@
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-spring-boot-starter-biz-data-permission</artifactId>
</dependency>
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-module-infra-biz</artifactId>
<version>2.4.2-jdk8-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>
......@@ -129,4 +129,6 @@ public class CustomerInfoController {
BeanUtils.toBean(list, CustomerInfoRespVO.class));
}
}
\ No newline at end of file
......@@ -24,8 +24,8 @@ public class CustomerInfoRespVO {
@ExcelProperty("客户姓名")
private String customerName;
@Schema(description = "联系方式", requiredMode = Schema.RequiredMode.REQUIRED)
@ExcelProperty("联系方式")
@Schema(description = "联系方式(客户手机号)", requiredMode = Schema.RequiredMode.REQUIRED)
@ExcelProperty("联系方式(客户手机号)")
private String contact;
@Schema(description = "公司名称", requiredMode = Schema.RequiredMode.REQUIRED)
......@@ -69,12 +69,11 @@ public class CustomerInfoRespVO {
private String productNames;
@Schema(description = "客户所属部门", requiredMode = Schema.RequiredMode.REQUIRED)
@ExcelProperty("客户所属部门")
@ExcelProperty(value = "客户所属部门", converter = DictConvert.class)
@DictFormat("customer_dept")
private String department;
@Schema(description = "静态图", requiredMode = Schema.RequiredMode.REQUIRED)
@ExcelProperty("静态图")
private String locationImage;
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
......@@ -84,4 +83,8 @@ public class CustomerInfoRespVO {
@Schema(description = "产品信息List对象")
private List<ProductSimpleReqVO> productList;
@Schema(description = "业务经理")
@ExcelProperty("业务经理")
private String creator;
}
\ No newline at end of file
......@@ -17,8 +17,8 @@ public class CustomerInfoSaveReqVO {
@NotEmpty(message = "客户姓名不能为空")
private String customerName;
@Schema(description = "联系方式", requiredMode = Schema.RequiredMode.REQUIRED)
@NotEmpty(message = "联系方式不能为空")
@Schema(description = "联系方式")
@Pattern(regexp = "^$|^\\d{11}$", message = "联系方式必须是11位数字")
private String contact;
@Schema(description = "公司名称", requiredMode = Schema.RequiredMode.REQUIRED)
......
......@@ -71,6 +71,26 @@ public class InfoController {
return success(BeanUtils.toBean(info, InfoRespVO.class));
}
@GetMapping("/getPrintListByIds")
@Operation(summary = "获得客户拜访打印信息根据Ids")
public CommonResult<List<InfoPrintVO>> getPrintListByIds(@RequestParam("ids") String ids) {
String[] split = ids.split(",");
//转为List
List<Long> idList = new ArrayList<>();
for (String s : split) {
idList.add(Long.parseLong(s));
}
List<InfoPrintVO> info = infoService.getInfoByIds(idList);
return success(info);
}
@GetMapping("/getPrintListByCompanyName")
@Operation(summary = "获得客户拜访打印信息根据公司名称")
public CommonResult<InfoPrintVO> getPrintListByCompanyName(@RequestParam("companyName") String companyName) {
InfoPrintVO info = infoService.getInfoByCompanyName(companyName);
return success(info);
}
@GetMapping("/page")
@Operation(summary = "获得客户拜访记录分页")
@PreAuthorize("@ss.hasPermission('visit:info:query')")
......
package cn.iocoder.yudao.module.visit.controller.admin.info.vo;
import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
import com.alibaba.excel.annotation.ExcelProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
@Schema(description = "管理后台 - 客户拜访记录 Print VO")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class InfoPrintVO {
@Schema(description = "联系方式", requiredMode = Schema.RequiredMode.REQUIRED)
private String contact;
@Schema(description = "客户公司名称", requiredMode = Schema.RequiredMode.REQUIRED)
private String companyName;
@Schema(description = "客户部门")
private String department;
@Schema(description = "定位静态图", requiredMode = Schema.RequiredMode.REQUIRED)
private String locationImage;
@Schema(description = "最近四次拜访日期", requiredMode = Schema.RequiredMode.REQUIRED)
private List<LocalDateTime> visitDate;
@Schema(description = "拜访品种")
private String visitProductNames;
@Schema(description = "最近四次服务内容")
private List<String> serviceContent;
@Schema(description = "服务记录图片URL列表(JSON数组)")
private String serviceImages;
@Schema(description = "业务员")
private String salesman;
@Schema(description = "服务数量")
private Integer serviceCount;
}
\ No newline at end of file
......@@ -37,6 +37,11 @@ public class InfoRespVO {
@ExcelProperty("客户公司名称")
private String companyName;
@Schema(description = "客户所属部门", requiredMode = Schema.RequiredMode.REQUIRED)
@ExcelProperty(value = "客户所属部门", converter = DictConvert.class)
@DictFormat("customer_dept")
private String department;
@Schema(description = "所在地区", requiredMode = Schema.RequiredMode.REQUIRED)
@ExcelProperty("所在地区")
private String regionFullName;
......@@ -50,7 +55,8 @@ public class InfoRespVO {
@Schema(description = "区名称", requiredMode = Schema.RequiredMode.REQUIRED)
private String areaName;
@Schema(description = "定位地址文字描述", requiredMode = Schema.RequiredMode.REQUIRED)
@Schema(description = "详细地址", requiredMode = Schema.RequiredMode.REQUIRED)
@ExcelProperty("详细地址")
private String locationText;
@Schema(description = "经度", requiredMode = Schema.RequiredMode.REQUIRED)
......@@ -59,7 +65,7 @@ public class InfoRespVO {
@Schema(description = "纬度", requiredMode = Schema.RequiredMode.REQUIRED)
private BigDecimal latitude;
@Schema(description = "定位静态图URL", requiredMode = Schema.RequiredMode.REQUIRED)
@Schema(description = "定位静态图", requiredMode = Schema.RequiredMode.REQUIRED)
private String locationImage;
@Schema(description = "拜访日期", requiredMode = Schema.RequiredMode.REQUIRED)
......@@ -101,7 +107,6 @@ public class InfoRespVO {
private String customerFeedback;
@Schema(description = "服务记录图片URL列表(JSON数组)")
@ExcelProperty("服务记录图片URL列表(JSON数组)")
private String serviceImages;
......
......@@ -20,8 +20,8 @@ public class InfoSaveReqVO {
@NotEmpty(message = "客户姓名不能为空")
private String customerName;
@Schema(description = "联系方式(客户手机号)", requiredMode = Schema.RequiredMode.REQUIRED)
@NotEmpty(message = "联系方式(客户手机号)不能为空")
@Schema(description = "联系方式(客户手机号)")
@Pattern(regexp = "^$|^\\d{11}$", message = "联系方式必须是11位数字")
private String contact;
@Schema(description = "客户公司名称", requiredMode = Schema.RequiredMode.REQUIRED)
......@@ -80,6 +80,9 @@ public class InfoSaveReqVO {
@Schema(description = "拜访类型(字典)", example = "2")
private Integer visitType;
@Schema(description = "客户所属部门(字典)")
private String department;
@Schema(description = "服务内容")
private String serviceContent;
......
......@@ -115,6 +115,12 @@ public class InfoDO extends BaseDO {
* 枚举 {@link TODO visit_type 对应的类}
*/
private Integer visitType;
/**
* 客户部门
*
* 枚举 {@link TODO customer_dept 对应的类}
*/
private String department;
/**
* 服务内容
*/
......
......@@ -31,4 +31,8 @@ public interface InfoMapper extends BaseMapperX<InfoDO> {
.orderByDesc(InfoDO::getId));
}
default List<InfoDO> selectByCompanyName(String companyName) {
return selectList(new LambdaQueryWrapperX<InfoDO>()
.eq(InfoDO::getCompanyName, companyName));
}
}
\ No newline at end of file
......@@ -16,6 +16,8 @@ public interface ErrorCodeConstants {
// ========== 客户信息 ==========
ErrorCode CUSTOMER_INFO_NOT_EXISTS = new ErrorCode(1002000001, "客户信息不存在");
ErrorCode CUSTOMER_INFO_COMPANY_NAME_DUPLICATE = new ErrorCode(1002000002, "该公司名称已存在");
// ========== 客户拜访记录 ==========
ErrorCode INFO_NOT_EXISTS = new ErrorCode(1003000001, "客户拜访记录不存在");
......
package cn.iocoder.yudao.module.visit.service.customerinfo;
import cn.iocoder.yudao.module.infra.service.file.FileService;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.entity.ContentType;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import org.springframework.validation.annotation.Validated;
import org.springframework.transaction.annotation.Transactional;
import java.io.IOException;
import java.util.*;
import cn.iocoder.yudao.module.visit.controller.admin.customerinfo.vo.*;
import cn.iocoder.yudao.module.visit.dal.dataobject.customerinfo.CustomerInfoDO;
......@@ -29,21 +38,58 @@ public class CustomerInfoServiceImpl implements CustomerInfoService {
@Resource
private CustomerInfoMapper customerInfoMapper;
@Resource
private FileService fileService;
@Override
public Long createCustomerInfo(CustomerInfoSaveReqVO createReqVO) {
// 插入
CustomerInfoDO customerInfo = BeanUtils.toBean(createReqVO, CustomerInfoDO.class);
//校验companyName是否重复
CustomerInfoDO customerInfoDO = customerInfoMapper.selectByCompanyName(customerInfo.getCompanyName());
if (customerInfoDO != null) {
throw exception(CUSTOMER_INFO_COMPANY_NAME_DUPLICATE);
}
String url = uploadMapImageByUrl(customerInfo.getLocationImage(), null);
customerInfo.setLocationImage(url);
customerInfoMapper.insert(customerInfo);
// 返回
return customerInfo.getId();
}
public String uploadMapImageByUrl(String imageUrl, String directory) {
try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
String fixedUrl = imageUrl.replace("|", "%7C");
HttpGet request = new HttpGet(fixedUrl);
try (CloseableHttpResponse response = httpClient.execute(request)) {
HttpEntity entity = response.getEntity();
byte[] content = EntityUtils.toByteArray(entity);
// 文件名:你可以从 URL 中提取或自定义
String name = "map_" + System.currentTimeMillis() + ".jpg";
String type = ContentType.getOrDefault(entity).getMimeType();
// 调用你已有的 createFile 方法
return fileService.createFile(content, name, directory, type);
}
} catch (IOException e) {
throw new RuntimeException("下载地图图片失败:" + imageUrl, e);
}
}
@Override
public void updateCustomerInfo(CustomerInfoSaveReqVO updateReqVO) {
// 校验存在
validateCustomerInfoExists(updateReqVO.getId());
// 更新
CustomerInfoDO updateObj = BeanUtils.toBean(updateReqVO, CustomerInfoDO.class);
CustomerInfoDO customerInfoDO = customerInfoMapper.selectByCompanyName(updateObj.getCompanyName());
if (customerInfoDO != null) {
throw exception(CUSTOMER_INFO_COMPANY_NAME_DUPLICATE);
}
if (updateObj.getLocationImage() != null&& updateObj.getLocationImage().startsWith("https://apis.map.qq.com/ws/staticmap/v2/")){
String url = uploadMapImageByUrl(updateObj.getLocationImage(), null);
updateObj.setLocationImage(url);
}
customerInfoMapper.updateById(updateObj);
}
......
......@@ -51,5 +51,18 @@ public interface InfoService {
* @return 客户拜访记录分页
*/
PageResult<InfoDO> getInfoPage(InfoPageReqVO pageReqVO);
/**
* 获得客户拜访记录打印数据 根据ids
*
* @param ids 查询
* @return 客户拜访记录
*/
List<InfoPrintVO> getInfoByIds(List<Long> ids);
/**
* 获得客户拜访记录打印数据 根据客户名称
*
* @param companyName 查询
* @return 客户拜访记录
*/
InfoPrintVO getInfoByCompanyName(String companyName);
}
\ No newline at end of file
......@@ -2,21 +2,37 @@ package cn.iocoder.yudao.module.visit.service.info;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.infra.service.file.FileService;
import cn.iocoder.yudao.module.visit.controller.admin.customerinfo.vo.CustomerInfoSaveReqVO;
import cn.iocoder.yudao.module.visit.controller.admin.info.vo.InfoPageReqVO;
import cn.iocoder.yudao.module.visit.controller.admin.info.vo.InfoPrintVO;
import cn.iocoder.yudao.module.visit.controller.admin.info.vo.InfoSaveReqVO;
import cn.iocoder.yudao.module.visit.dal.dataobject.customerinfo.CustomerInfoDO;
import cn.iocoder.yudao.module.visit.dal.dataobject.info.InfoDO;
import cn.iocoder.yudao.module.visit.dal.mysql.customerinfo.CustomerInfoMapper;
import cn.iocoder.yudao.module.visit.dal.mysql.info.InfoMapper;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.entity.ContentType;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import javax.annotation.Resource;
import java.io.IOException;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.visit.enums.ErrorCodeConstants.INFO_NOT_EXISTS;
import static java.nio.file.Files.createFile;
/**
* 客户拜访记录 Service 实现类
......@@ -33,11 +49,16 @@ public class InfoServiceImpl implements InfoService {
@Resource
private CustomerInfoMapper customerInfoMapper;
@Resource
private FileService fileService;
@Override
@Transactional(rollbackFor = Exception.class)
public Long createInfo(InfoSaveReqVO createReqVO) {
// 插入
InfoDO info = BeanUtils.toBean(createReqVO, InfoDO.class);
String url = uploadMapImageByUrl(info.getLocationImage(), null);
info.setLocationImage(url);
int insert = infoMapper.insert(info);
if (insert >0){
//拜访记录插入成功,根据【公司名称】判断该客户是否记录在案,如果没有,则插入一条客户信息
......@@ -49,6 +70,7 @@ public class InfoServiceImpl implements InfoService {
customerInfoSaveReqVO.setContact(info.getContact());
customerInfoSaveReqVO.setCompanyName(info.getCompanyName());
customerInfoSaveReqVO.setCustomerType(info.getCustomerStatus());
customerInfoSaveReqVO.setDepartment(info.getDepartment());
customerInfoSaveReqVO.setProvinceName(info.getProvinceName());
customerInfoSaveReqVO.setCityName(info.getCityName());
customerInfoSaveReqVO.setAreaName(info.getAreaName());
......@@ -67,12 +89,36 @@ public class InfoServiceImpl implements InfoService {
return info.getId();
}
public String uploadMapImageByUrl(String imageUrl, String directory) {
try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
String fixedUrl = imageUrl.replace("|", "%7C");
HttpGet request = new HttpGet(fixedUrl);
try (CloseableHttpResponse response = httpClient.execute(request)) {
HttpEntity entity = response.getEntity();
byte[] content = EntityUtils.toByteArray(entity);
// 文件名:你可以从 URL 中提取或自定义
String name = "map_" + System.currentTimeMillis() + ".jpg";
String type = ContentType.getOrDefault(entity).getMimeType();
// 调用你已有的 createFile 方法
return fileService.createFile(content, name, directory, type);
}
} catch (IOException e) {
throw new RuntimeException("下载地图图片失败:" + imageUrl, e);
}
}
@Override
public void updateInfo(InfoSaveReqVO updateReqVO) {
// 校验存在
validateInfoExists(updateReqVO.getId());
// 更新
InfoDO updateObj = BeanUtils.toBean(updateReqVO, InfoDO.class);
if (updateObj.getLocationImage() != null&& updateObj.getLocationImage().startsWith("https://apis.map.qq.com/ws/staticmap/v2/")){
String url = uploadMapImageByUrl(updateObj.getLocationImage(), null);
updateObj.setLocationImage(url);
}
infoMapper.updateById(updateObj);
}
......@@ -100,4 +146,92 @@ public class InfoServiceImpl implements InfoService {
return infoMapper.selectPage(pageReqVO);
}
@Override
public List<InfoPrintVO> getInfoByIds(List<Long> ids) {
List<InfoPrintVO> vos = new ArrayList<>();
//先获取客户拜访记录
List<InfoDO> info = infoMapper.selectByIds(ids);
if (info != null && !info.isEmpty()){
//获取客户拜访记录成功,获取客户信息
info.forEach(infoDO -> {
InfoPrintVO infoPrintVO = new InfoPrintVO();
infoPrintVO.setServiceContent(Collections.singletonList(infoDO.getServiceContent()));//本次服务内容
//服务图片(最多四张)
String imagesStr = infoDO.getServiceImages();
if (StringUtils.isNotBlank(imagesStr)) {
String limitedImagesStr = Arrays.stream(imagesStr.split(","))
.limit(4)
.collect(Collectors.joining(","));
infoPrintVO.setServiceImages(limitedImagesStr);
} else {
infoPrintVO.setServiceImages("");
}
infoPrintVO.setVisitProductNames(infoDO.getVisitProductNames());//本次拜访品种
infoPrintVO.setCompanyName(infoDO.getCompanyName());//公司名称
infoPrintVO.setDepartment(infoDO.getDepartment());//部门
infoPrintVO.setContact(infoDO.getContact());//联系方式
infoPrintVO.setSalesman(infoDO.getCreator());//拜访记录的创建者就是该记录的业务员
infoPrintVO.setLocationImage(infoDO.getLocationImage());
infoPrintVO.setVisitDate(Collections.singletonList(infoDO.getVisitDate()));
//拜访次数按拜访id打印都是1
infoPrintVO.setServiceCount(1);
vos.add(infoPrintVO);
});
}
return vos;
}
@Override
public InfoPrintVO getInfoByCompanyName(String companyName) {
InfoPrintVO infoPrintVO = new InfoPrintVO();
//服务内容(最近四次)
List<String> serviceContent = new ArrayList<>();
//服务图片(最多四张)
List<String> serviceImages = new ArrayList<>();
//拜访时间记录
List<LocalDateTime> visitDateList = new ArrayList<>();
//现根据客户名称获取历史拜访记录
List<InfoDO> info = infoMapper.selectByCompanyName(companyName);
if (info != null && !info.isEmpty()){
//取最近四次
info = info.stream().sorted(Comparator.comparing(InfoDO::getVisitDate).reversed()).limit(4).collect(Collectors.toList());
//取最多4张服务图片
int maxImages = 4;
for (InfoDO infoDO : info) {
if (serviceImages.size() >= maxImages) break;
String imagesStr = infoDO.getServiceImages();
if (StringUtils.isNotBlank(imagesStr)) {
List<String> imageList = Arrays.asList(imagesStr.split(","));
int remaining = maxImages - serviceImages.size();
serviceImages.addAll(imageList.stream().limit(remaining).collect(Collectors.toList()));
}
}
//获取客户拜访记录成功,获取客户信息
info.forEach(infoDO -> {
//服务内容记录
serviceContent.add(infoDO.getServiceContent());
//拜访时间记录
visitDateList.add(infoDO.getVisitDate());
});
//下面获取客户信息
CustomerInfoDO customerInfoDO = customerInfoMapper.selectByCompanyName(companyName);
infoPrintVO.setVisitProductNames(customerInfoDO.getProductNames());//本次拜访品种
infoPrintVO.setCompanyName(customerInfoDO.getCompanyName());//公司名称
infoPrintVO.setSalesman(customerInfoDO.getCreator());//拜访记录的创建者就是该记录的业务员
infoPrintVO.setLocationImage(customerInfoDO.getLocationImage());//公司定位图
infoPrintVO.setContact(customerInfoDO.getContact());
infoPrintVO.setDepartment(customerInfoDO.getDepartment());
//添加多次的信息
infoPrintVO.setServiceContent(serviceContent);
infoPrintVO.setVisitDate(visitDateList);
infoPrintVO.setServiceImages(String.join(",", serviceImages));
//拜访次数
infoPrintVO.setServiceCount(info.size());
}else {
return null;
}
return infoPrintVO;
}
}
\ No newline at end of file
......@@ -15,7 +15,7 @@
"@iconify/iconify": "^3.1.1",
"@microsoft/fetch-event-source": "^2.0.1",
"@videojs-player/vue": "^1.0.0",
"@vueuse/core": "^10.9.0",
"@vueuse/core": "^10.11.1",
"@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-vue": "^5.1.10",
"@zxcvbn-ts/core": "^3.0.4",
......@@ -59,6 +59,7 @@
"vue-i18n": "9.10.2",
"vue-router": "4.4.5",
"vue-types": "^5.1.1",
"vue3-print-nb": "^0.1.4",
"vue3-signature": "^0.2.4",
"vuedraggable": "^4.1.0",
"web-storage-cache": "^1.1.1",
......@@ -6908,7 +6909,6 @@
"version": "10.11.1",
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.11.1.tgz",
"integrity": "sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==",
"license": "MIT",
"dependencies": {
"@types/web-bluetooth": "^0.0.20",
"@vueuse/metadata": "10.11.1",
......@@ -17077,6 +17077,14 @@
"integrity": "sha512-L2RPSAwUFbgZH20etwrXyVyCBu9OxRSi8T/38QsvnkJyvq2LufW2lDCOzm7t/U9C1mkhJGWYfCuFBCmIuNivrg==",
"license": "MIT"
},
"node_modules/vue3-print-nb": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/vue3-print-nb/-/vue3-print-nb-0.1.4.tgz",
"integrity": "sha512-LExI7viEzplR6ZKQ2b+V4U0cwGYbVD4fut/XHvk3UPGlT5CcvIGs6VlwGp107aKgk6P8Pgx4rco3Rehv2lti3A==",
"dependencies": {
"vue": "^3.0.5"
}
},
"node_modules/vue3-signature": {
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/vue3-signature/-/vue3-signature-0.2.4.tgz",
......
......@@ -31,7 +31,7 @@
"@iconify/iconify": "^3.1.1",
"@microsoft/fetch-event-source": "^2.0.1",
"@videojs-player/vue": "^1.0.0",
"@vueuse/core": "^10.9.0",
"@vueuse/core": "^10.11.1",
"@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-vue": "^5.1.10",
"@zxcvbn-ts/core": "^3.0.4",
......@@ -75,6 +75,7 @@
"vue-i18n": "9.10.2",
"vue-router": "4.4.5",
"vue-types": "^5.1.1",
"vue3-print-nb": "^0.1.4",
"vue3-signature": "^0.2.4",
"vuedraggable": "^4.1.0",
"web-storage-cache": "^1.1.1",
......
......@@ -21,8 +21,40 @@ export interface InfoVO {
serviceContent: string // 服务内容
customerFeedback: string // 客户反馈
serviceImages: string // 服务记录图片URL列表(JSON数组)
department: string // 客户部门
}
export interface InfoPrintVO {
/** 联系方式 */
contact: string;
/** 客户公司名称 */
companyName: string;
/** 客户部门 */
department?: string;
/** 定位静态图(URL) */
locationImage: string;
/** 最近四次拜访日期 */
visitDate: string[]; // ISO 格式字符串,或可用 Date[] 视后端返回而定
/** 拜访品种 */
visitProductNames?: string;
/** 最近四次服务内容 */
serviceContent: string[];
/** 服务记录图片URL列表(逗号分隔的字符串) */
serviceImages: string;
/** 业务员 */
salesman: string;
/** 服务数量 */
serviceCount: number;
}
// 客户拜访记录 API
export const InfoApi = {
// 查询客户拜访记录分页
......@@ -35,6 +67,17 @@ export const InfoApi = {
return await request.get({ url: `/visit/info/get?id=` + id })
},
//查询打印客户拜访记录
getPrintListByIds: async (ids: string) => {
return await request.get({ url: `/visit/info/getPrintListByIds`, params: { ids } })
},
//查询打印客户拜访记录
getPrintListByCompanyName: async (companyName: string) => {
return await request.get({ url: `/visit/info/getPrintListByCompanyName`, params: { companyName } })
},
// 新增客户拜访记录
createInfo: async (data: InfoVO) => {
return await request.post({ url: `/visit/info/create`, data })
......
......@@ -96,7 +96,6 @@
</Dialog>
</template>
<script lang="ts" setup>
import {DICT_TYPE, getIntDictOptions} from '@/utils/dict'
import {CommonStatusEnum} from '@/utils/constants'
import {defaultProps, handleTree} from '@/utils/tree'
import * as DeptApi from '@/api/system/dept'
......
......@@ -11,7 +11,7 @@
<el-input v-model="formData.customerName" placeholder="请输入客户姓名" />
</el-form-item>
<el-form-item label="联系方式" prop="contact">
<el-input v-model="formData.contact" placeholder="请输入联系方式" />
<el-input v-model="formData.contact" maxlength="11" placeholder="请输入联系方式" />
</el-form-item>
<el-form-item label="公司名称" prop="companyName">
<el-input v-model="formData.companyName" placeholder="请输入公司名称" />
......@@ -153,9 +153,9 @@ const formData = ref<CustomerFormData>({
})
const formRules = reactive({
customerName: [{ required: true, message: '客户姓名不能为空', trigger: 'blur' }],
contact: [{ required: true, message: '联系方式不能为空', trigger: 'blur' }],
companyName: [{ required: true, message: '公司名称不能为空', trigger: 'blur' }],
customerType: [{ required: true, message: '性质等级不能为空', trigger: 'change' }],
department: [{ required: true, message: '客户部门不能为空', trigger: 'change' }],
provinceName: [{ required: true, message: '省名称不能为空', trigger: 'blur' }],
cityName: [{ required: true, message: '市名称不能为空', trigger: 'blur' }],
areaName: [{ required: true, message: '区名称不能为空', trigger: 'blur' }],
......@@ -194,7 +194,7 @@ const handleLocation = ({ lat, lng, address }) => {
formData.value.longitude = lng
formData.value.locationText = address
//静态图
const mapKey = 'KHXBZ-OVYYZ-N4NXF-7JCZ2-PR4FT-RYF4E'
const mapKey = '2OZBZ-WUCE7-SLKXP-HJVOW-3P6RF-WVB7H'
const mapUrl = `https://apis.map.qq.com/ws/staticmap/v2/?center=${lat},${lng}&zoom=13&size=600*300&maptype=roadmap&markers=size:large|color:0xFFCCFF|label:k|${lat},${lng}&key=${mapKey}`
formData.value.locationImage = mapUrl
......
......@@ -87,17 +87,18 @@
<!-- 列表 -->
<ContentWrap>
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" />
<el-table-column label="编号" align="center" prop="id" />
<el-table-column label="客户姓名" align="center" prop="customerName" />
<el-table-column label="联系方式" align="center" prop="contact" />
<el-table-column label="公司名称" align="center" prop="companyName" />
<el-table-column label="客户姓名" align="center" prop="customerName" min-width="100px"/>
<el-table-column label="联系方式" align="center" prop="contact" min-width="120px"/>
<el-table-column label="公司名称" align="center" prop="companyName" min-width="160px"/>
<el-table-column label="性质等级" align="center" prop="customerType">
<template #default="scope">
<dict-tag :type="DICT_TYPE.CUSTOMER_TYPE" :value="scope.row.customerType" />
</template>
</el-table-column>
<el-table-column label="所在地区" align="center" prop="regionFullName" />
<el-table-column label="所在地区" align="center" prop="regionFullName" min-width="160px"/>
<el-table-column
label="创建时间"
align="center"
......@@ -105,13 +106,27 @@
:formatter="dateFormatter"
width="180px"
/>
<el-table-column label="产品信息" prop="productIds">
<el-table-column label="产品信息" prop="productIds" min-width="100px">
<template #default="scope">
<ProductList :ids=scope.row.productIds />
</template>
</el-table-column>
<el-table-column label="操作" align="center" min-width="120px">
<el-table-column label="操作" align="center" min-width="320px" fixed="right">
<template #default="scope">
<el-button
link
type="primary"
@click="visitInfo(scope.row.companyName)"
>
拜访记录
</el-button>
<el-button
link
type="primary"
@click="handlePrint(scope.row.companyName)"
>
打印拜访记录
</el-button>
<el-button
link
type="primary"
......@@ -142,6 +157,8 @@
<!-- 表单弹窗:添加/修改 -->
<CustomerInfoForm ref="formRef" @success="getList" />
<!-- 打印弹窗 -->
<VisitPrint ref="visitPrintRef" :data="selectedData" />
</template>
<script setup lang="ts">
......@@ -151,10 +168,13 @@ import download from '@/utils/download'
import { CustomerInfoApi, CustomerInfoVO } from '@/api/visit/customerinfo'
import CustomerInfoForm from './CustomerInfoForm.vue'
import ProductList from "@/views/visit/util/ProductList.vue";
import { useRouter } from 'vue-router'
import {InfoApi, InfoPrintVO} from "@/api/visit/info";
/** 客户信息 列表 */
defineOptions({ name: 'CustomerInfo' })
const router = useRouter()
const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化
......@@ -173,6 +193,30 @@ const queryParams = reactive({
})
const queryFormRef = ref() // 搜索的表单
const exportLoading = ref(false) // 导出的加载中
const multipleSelection = ref([]) //存储多选内容
const visitPrintRef = ref()
// 模拟获取详情数据的方法(你应该调 InfoApi.getInfoListByIds 或类似接口)
const selectedData = ref<InfoPrintVO[]>([])
const handlePrint = async (companyName: string) => {
selectedData.value = await InfoApi.getPrintListByCompanyName(companyName)
//判断是否有数据
if (!selectedData.value) {
message.warning('该客户暂无拜访记录')
return
}
console.log(selectedData.value)
await nextTick()
visitPrintRef.value?.print()
}
/** 多选操作**/
const handleSelectionChange = (val) => {
multipleSelection.value = val.map(item=>item.id)
//console.log(val,multipleSelection)
}
/** 查询列表 */
const getList = async () => {
......@@ -191,7 +235,9 @@ const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
const visitInfo=(data)=>{
router.push({ path: '/visit/info', query: { companyName: data } })
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
......
......@@ -11,7 +11,7 @@
<el-input v-model="formData.customerName" placeholder="请输入拜访人姓名" />
</el-form-item>
<el-form-item label="联系方式" prop="contact">
<el-input v-model="formData.contact" placeholder="请输入联系方式" />
<el-input v-model="formData.contact" maxlength="11" placeholder="请输入联系方式" />
</el-form-item>
<el-form-item label="公司名称" prop="companyName">
<el-autocomplete
......@@ -62,6 +62,16 @@
/>
</el-select>
</el-form-item>
<el-form-item label="客户部门" prop="department">
<el-select v-model="formData.department" placeholder="请选择客户部门">
<el-option
v-for="dict in getStrDictOptions(DICT_TYPE.CUSTOMER_DEPT)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="拜访品种" prop="visitProductNames">
<el-input
v-model="formData.visitProductNames"
......@@ -125,7 +135,7 @@
<productTable ref="productListRef" @updateProduct="handleUpdateProduct" :ids="formData.visitProductIds"/>
</template>
<script setup lang="ts">
import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
import {getIntDictOptions, DICT_TYPE, getStrDictOptions} from '@/utils/dict'
import { InfoApi, InfoVO } from '@/api/visit/info'
import { CustomerInfoApi } from '@/api/visit/customerinfo'
import productTable from "@/views/visit/customerinfo/productTable.vue";
......@@ -159,6 +169,7 @@ const formData = ref({
locationImage: undefined,
visitDate: Date.now(),
customerStatus: undefined,
department: undefined,
visitProductIds: undefined,
visitProductNames: undefined,
visitMethod: undefined,
......@@ -169,7 +180,6 @@ const formData = ref({
})
const formRules = reactive({
customerName: [{ required: true, message: '拜访人姓名不能为空', trigger: 'blur' }],
contact: [{ required: true, message: '联系方式不能为空', trigger: 'blur' }],
companyName: [{ required: true, message: '客户公司名称不能为空', trigger: 'blur' }],
provinceName: [{ required: true, message: '省名称不能为空', trigger: 'blur' }],
cityName: [{ required: true, message: '市名称不能为空', trigger: 'blur' }],
......@@ -181,6 +191,7 @@ const formRules = reactive({
locationImage: [{ required: true, message: '定位静态图URL不能为空', trigger: 'blur' }],
visitDate: [{ required: true, message: '拜访日期不能为空', trigger: 'blur' }],
customerStatus: [{ required: true, message: '性质等级不能为空', trigger: 'blur' }],
department: [{ required: true, message: '客户部门不能为空', trigger: 'blur' }],
serviceContent: [{ required: true, message: '请输入服务内容', trigger: 'blur' }, { max: 500, message: '最多输入 500 个字符', trigger: 'blur' }],
customerFeedback: [{ required: true, message: '请输入客户反馈', trigger: 'blur' }, { max: 500, message: '最多输入 500 个字符', trigger: 'blur' }],
})
......@@ -282,7 +293,7 @@ const handleLocation = ({ lat, lng, address }) => {
formData.value.longitude = lng
formData.value.locationText = address
//静态图
const mapKey = 'KHXBZ-OVYYZ-N4NXF-7JCZ2-PR4FT-RYF4E'
const mapKey = '2OZBZ-WUCE7-SLKXP-HJVOW-3P6RF-WVB7H'
const mapUrl = `https://apis.map.qq.com/ws/staticmap/v2/?center=${lat},${lng}&zoom=13&size=600*300&maptype=roadmap&markers=size:large|color:0xFFCCFF|label:k|${lat},${lng}&key=${mapKey}`
formData.value.locationImage = mapUrl
......@@ -386,6 +397,7 @@ const resetForm = () => {
locationImage: undefined,
visitDate: Date.now(),
customerStatus: undefined,
department: undefined,
visitProductIds: undefined,
visitProductNames: undefined,
visitMethod: undefined,
......
......@@ -126,18 +126,27 @@
>
<Icon icon="ep:download" class="mr-5px" /> 导出
</el-button>
<el-button
type="success"
plain
@click="handlePrint"
:loading="exportLoading"
>
<Icon icon="ep:printer" class="mr-5px" /> 批量打印
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" />
<el-table-column label="主键ID" align="center" prop="id" />
<el-table-column label="拜访人姓名" align="center" prop="customerName" width="100px"/>
<el-table-column label="联系方式" align="center" prop="contact" width="120px"/>
<el-table-column label="客户公司名称" align="center" prop="companyName" width="160px" />
<el-table-column label="所在省市区" align="center" prop="regionFullName" width="160px" />
<el-table-column label="所在区" align="center" prop="regionFullName" width="160px" />
<el-table-column
label="拜访日期"
align="center"
......@@ -204,15 +213,34 @@
<!-- 表单弹窗:添加/修改 -->
<InfoForm ref="formRef" @success="getList" />
<!-- 打印弹窗 -->
<VisitPrint ref="visitPrintRef" :data="selectedData" />
</template>
<script setup lang="ts">
import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
import { dateFormatter } from '@/utils/formatTime'
import {DICT_TYPE, getIntDictOptions} from '@/utils/dict'
import {dateFormatter} from '@/utils/formatTime'
import download from '@/utils/download'
import { InfoApi, InfoVO } from '@/api/visit/info'
import {InfoApi, InfoPrintVO, InfoVO} from '@/api/visit/info'
import InfoForm from './InfoForm.vue'
import ProductList from "@/views/visit/util/ProductList.vue";
import {useRoute} from 'vue-router'
const visitPrintRef = ref()
// 模拟获取详情数据的方法(你应该调 InfoApi.getInfoListByIds 或类似接口)
const selectedData = ref<InfoPrintVO[]>([])
const handlePrint = async () => {
if (multipleSelection.value.length === 0) {
return message.warning('请先选择需要打印的记录')
}
selectedData.value = await InfoApi.getPrintListByIds(multipleSelection.value.join(','))
console.log(selectedData.value)
await nextTick()
visitPrintRef.value?.print()
}
const route = useRoute()
/** 客户拜访记录 列表 */
defineOptions({ name: 'Info' })
......@@ -238,7 +266,13 @@ const queryParams = reactive({
})
const queryFormRef = ref() // 搜索的表单
const exportLoading = ref(false) // 导出的加载中
const multipleSelection = ref([]) //存储多选内容
/** 多选操作**/
const handleSelectionChange = (val) => {
multipleSelection.value = val.map(item=>item.id)
//console.log(val,multipleSelection)
}
/** 查询列表 */
const getList = async () => {
loading.value = true
......@@ -299,6 +333,10 @@ const handleExport = async () => {
/** 初始化 **/
onMounted(() => {
const companyName = route.query.companyName
if (companyName){
queryParams.companyName = companyName
}
getList()
})
</script>
......@@ -182,7 +182,7 @@ const multipleSelection = ref([]) //存储多选内容
/** 多选操作**/
const handleSelectionChange = (val) => {
multipleSelection.value = val.map(item=>item.id)
console.log(val,multipleSelection)
//console.log(val,multipleSelection)
}
/** 查询列表 */
const getList = async () => {
......
......@@ -26,7 +26,7 @@ import { ref, onMounted, onUnmounted } from 'vue'
const visible = ref(false)
// ✅ 腾讯地图 locationPicker 地址,替换成你的 key & 应用名
const pickerUrl = `https://apis.map.qq.com/tools/locpicker?search=1&type=1&key=KHXBZ-OVYYZ-N4NXF-7JCZ2-PR4FT-RYF4E&referer=VISITKEY`
const pickerUrl = `https://apis.map.qq.com/tools/locpicker?search=1&type=1&key=2OZBZ-WUCE7-SLKXP-HJVOW-3P6RF-WVB7H&referer=VISITKEY`
// ✅ 定义暴露 open() 方法,父组件通过 ref 调用
const open = () => {
......
<template>
<div ref="printArea" style="padding: 20px; font-size: 14px;" v-show="visible">
<div v-for="(item, index) in data" :key="index" class="print-page">
<h2 style="text-align: center; margin-bottom: 16px;">渠道服务记录</h2>
<div style="display: flex;">
<!-- 左侧 1/3 -->
<div style="width: 33%;">
<img crossorigin="anonymous" :src="item.locationImage" style="width: 100%; height: 120px;" alt=""/>
<p>业务员:{{ item.salesman }}</p>
<p>服务数量:{{ item.serviceCount }}</p>
<h4>拜访时间记录</h4>
<ul>
<li v-for="(time, i) in item.visitDate" :key="i">{{ formatDate(time) }}</li>
</ul>
<h4>主要服务内容</h4>
<ul>
<li v-for="(content, i) in item.serviceContent" :key="i">内容{{ i + 1 }}{{ content }}</li>
</ul>
</div>
<!-- 右侧 2/3 -->
<div style="width: 67%; padding-left: 20px;">
<p>客户名称:{{ item.companyName }}</p>
<p>客户部门:{{ getDictLabel(DICT_TYPE.CUSTOMER_DEPT, item.department) }}</p>
<p>推广产品:{{ item.visitProductNames }}</p>
<div style="border: 1px solid #ccc; padding: 10px; margin-bottom: 10px;">拜访签到实景图</div>
<div style="display: flex; flex-wrap: wrap; gap: 10px;">
<img
crossorigin="anonymous"
v-for="(url, i) in getSafeImages(item.serviceImages)"
:key="i"
:src="url"
style="width: calc(50% - 5px); height: 100px; object-fit: cover; border: 1px solid #ccc;"
/>
</div>
</div>
</div>
<div style="page-break-after: always;"></div>
</div>
</div>
</template>
<script setup lang="ts">
import {nextTick, ref} from 'vue'
import { getDictLabel, DICT_TYPE } from '@/utils/dict'
const props = defineProps<{ data: any[] }>()
const printArea = ref<HTMLElement | null>(null)
const visible = ref(false)
const formatDate = (date: string | Date) => {
const d = new Date(date)
return d.toLocaleString()
}
const getSafeImages = (val?: string) => {
if (!val) return []
return val.split(',').slice(0, 4)
}
const print = async () => {
const printWindow = window.open('', '_blank')
if (!printWindow) return
let html = ''
for (const item of props.data) {
html += `
<div class="print-page">
<h2>渠道服务记录</h2>
<div style="display: flex;">
<div style="width: 33%;">
<img src="${item.locationImage}" crossorigin="anonymous" style="width: 100%; height: 120px;" />
<p>业务员:${item.salesman || ''}</p>
<p>服务数量:${item.serviceCount || ''}</p>
<h4>拜访时间记录</h4>
<ul>
${(item.visitDate || []).map((t: string) => `<li>${formatDate(t)}</li>`).join('')}
</ul>
<h4>主要服务内容</h4>
<ul>
${(item.serviceContent || []).map((c: string, i: number) => `<li>内容${i + 1}${c}</li>`).join('')}
</ul>
</div>
<div style="width: 67%; padding-left: 20px;">
<p>客户名称:${item.companyName || ''}</p>
<p>客户部门:${getDictLabel(DICT_TYPE.CUSTOMER_DEPT, item.department) || ''}</p>
<p>推广产品:${item.visitProductNames || ''}</p>
<div style="border: 1px solid #ccc; padding: 10px; margin-bottom: 10px;">拜访签到实景图</div>
<div style="display: flex; flex-wrap: wrap; gap: 10px;">
${(item.serviceImages || '')
.split(',')
.slice(0, 4)
.map(url => `<img src="${url}" crossorigin="anonymous" style="width: calc(50% - 5px); height: 100px; object-fit: cover; border: 1px solid #ccc;" />`)
.join('')}
</div>
</div>
</div>
<div style="page-break-after: always;"></div>
</div>
`
}
printWindow.document.write(`
<html>
<head>
<title>打印</title>
<meta charset="UTF-8">
<style>
body, html {
margin: 0;
padding: 0;
width: 210mm;
height: auto;
font-size: 14px;
-webkit-print-color-adjust: exact;
}
.print-page {
width: 210mm;
height: 297mm;
padding: 20mm;
box-sizing: border-box;
page-break-after: always;
overflow: hidden;
}
h2 {
text-align: center;
margin-bottom: 16px;
}
img {
max-width: 100%;
object-fit: cover;
}
</style>
</head>
<body>${html}</body>
</html>
`)
printWindow.document.close()
printWindow.focus()
printWindow.print()
printWindow.close()
}
defineExpose({ print })
</script>
<style scoped>
/* 普通预览样式 */
#printWrapper {
width: 210mm;
margin: 0 auto;
font-size: 14px;
}
.print-page {
width: 210mm;
height: 297mm;
padding: 20mm;
box-sizing: border-box;
page-break-after: always;
overflow: hidden;
position: relative;
display: flex;
flex-direction: column;
justify-content: space-between;
border: 1px dashed transparent;
}
.print-page h2 {
text-align: center;
margin-bottom: 16px;
}
.print-page img {
max-width: 100%;
object-fit: cover;
}
.print-page .images-wrapper {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.print-page .images-wrapper img {
width: calc(50% - 5px);
height: 100px;
border: 1px solid #ccc;
}
/* 打印样式 */
@media print {
body, html {
margin: 0;
padding: 0;
width: 210mm;
height: auto;
-webkit-print-color-adjust: exact;
}
#printWrapper {
width: 210mm;
margin: 0 auto;
}
.print-page {
width: 210mm;
height: 297mm;
page-break-after: always;
overflow: hidden;
box-sizing: border-box;
padding: 20mm;
}
.no-print {
display: none !important;
}
}
</style>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment