Commit 677dd111 authored by 法拉51246's avatar 法拉51246

增加每个页面批量删除

增加查看详情
增加多选合并打印和全选合并打印
增加过滤角色权限分配,低权限不能分配高权限
parent 5a69a12f
......@@ -204,7 +204,7 @@ public class OAuth2TokenServiceImpl implements OAuth2TokenService {
private Map<String, String> buildUserInfo(Long userId, Integer userType) {
if (userType.equals(UserTypeEnum.ADMIN.getValue())) {
AdminUserDO user = adminUserService.getUser(userId);
Long branchDeptId = findBranchDeptId(user.getDeptId(), 100L);//找到分公司Id,这里的100L是总公司的ID,未来某一天如果有变,就改这里
Long branchDeptId = findBranchDeptId(user.getDeptId(), 100L);//找到分公司Id,这里的100L是总公司的ID,未来某一天如果有变,就改这里 锚点
return MapUtil.builder(LoginUser.INFO_KEY_NICKNAME, user.getNickname())
.put(LoginUser.INFO_KEY_DEPT_ID, StrUtil.toStringOrNull(branchDeptId)).build();
} else if (userType.equals(UserTypeEnum.MEMBER.getValue())) {
......
......@@ -9,10 +9,13 @@ import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
import cn.iocoder.yudao.module.system.controller.admin.permission.vo.role.RolePageReqVO;
import cn.iocoder.yudao.module.system.controller.admin.permission.vo.role.RoleSaveReqVO;
import cn.iocoder.yudao.module.system.dal.dataobject.permission.RoleDO;
import cn.iocoder.yudao.module.system.dal.dataobject.permission.UserRoleDO;
import cn.iocoder.yudao.module.system.dal.mysql.permission.RoleMapper;
import cn.iocoder.yudao.module.system.dal.mysql.permission.UserRoleMapper;
import cn.iocoder.yudao.module.system.dal.redis.RedisKeyConstants;
import cn.iocoder.yudao.module.system.enums.permission.DataScopeEnum;
import cn.iocoder.yudao.module.system.enums.permission.RoleCodeEnum;
......@@ -30,6 +33,7 @@ import org.springframework.util.StringUtils;
import javax.annotation.Resource;
import java.util.*;
import java.util.stream.Collectors;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
......@@ -50,6 +54,8 @@ public class RoleServiceImpl implements RoleService {
@Resource
private RoleMapper roleMapper;
@Resource
private UserRoleMapper userRoleMapper;
@Override
@Transactional(rollbackFor = Exception.class)
......@@ -187,7 +193,14 @@ public class RoleServiceImpl implements RoleService {
@Override
public List<RoleDO> getRoleListByStatus(Collection<Integer> statuses) {
return roleMapper.selectListByStatus(statuses);
List<RoleDO> roleDOS = roleMapper.selectListByStatus(statuses);
//判断当前用户是不是超级管理员,如果不是,需要过滤掉超级管理员
List<UserRoleDO> userRoleDOS = userRoleMapper.selectListByUserId(SecurityFrameworkUtils.getLoginUserId());
// 如果userRoleDOS中没有roleId=1的记录,则过滤掉roleDOS中id=1的记录
roleDOS = userRoleDOS.stream().noneMatch(userRole -> userRole.getRoleId() == 1L)
? roleDOS.stream().filter(role -> role.getId() != 1L).collect(Collectors.toList())
: roleDOS;
return roleDOS;
}
@Override
......
......@@ -76,6 +76,27 @@ public class CustomerInfoController {
return success(true);
}
@DeleteMapping("/deleteByIds")
@Operation(summary = "批量删除客户信息")
@Parameter(name = "ids", description = "编号", required = true)
@PreAuthorize("@ss.hasPermission('visit:customer-info:delete')")
@Idempotent(timeout = 10, message = "重复请求,请稍后重试")
public CommonResult<Boolean> deleteCustomerInfoByIds(@RequestParam("ids") String ids) {
//如果ids是空的,返回异常提示,请先选择需要打印的记录
if (StringUtil.isEmpty(ids)) {
throw new RuntimeException("请先选择需要删除的记录");
}
String[] split = ids.split(",");
//转为List
List<Long> idList = new ArrayList<>();
for (String s : split) {
idList.add(Long.parseLong(s));
}
customerInfoService.deleteCustomerInfoByIds(idList);
return success(true);
}
@GetMapping("/get")
@Operation(summary = "获得客户信息")
@Parameter(name = "id", description = "编号", required = true, example = "1024")
......
......@@ -60,12 +60,15 @@ public class CustomerInfoSaveReqVO {
private String locationImage;
@Schema(description = "产品信息")
@NotEmpty(message = "产品信息不能为空")
private String productIds;
@Schema(description = "产品信息名称")
@NotEmpty(message = "产品信息名称不能为空")
private String productNames;
@Schema(description = "客户部门")
@NotEmpty(message = "客户部门不能为空")
private String department;
}
\ No newline at end of file
......@@ -67,6 +67,26 @@ public class InfoController {
return success(true);
}
@DeleteMapping("/deleteByIds")
@Operation(summary = "删除客户拜访记录")
@Parameter(name = "ids", description = "编号", required = true)
@PreAuthorize("@ss.hasPermission('visit:info:delete')")
@Idempotent(timeout = 10, message = "重复请求,请稍后重试")
public CommonResult<Boolean> deleteInfoByIds(@RequestParam("ids") String ids) {
//如果ids是空的,返回异常提示,请先选择需要打印的记录
if (StringUtil.isEmpty(ids)) {
throw new RuntimeException("请先选择需要删除的记录");
}
String[] split = ids.split(",");
//转为List
List<Long> idList = new ArrayList<>();
for (String s : split) {
idList.add(Long.parseLong(s));
}
infoService.deleteInfoByIds(idList);
return success(true);
}
@GetMapping("/get")
@Operation(summary = "获得客户拜访记录")
@Parameter(name = "id", description = "编号", required = true, example = "1024")
......@@ -78,6 +98,7 @@ public class InfoController {
@GetMapping("/getPrintListByIds")
@Operation(summary = "获得客户拜访打印信息根据Ids")
@PreAuthorize("@ss.hasPermission('visit:info:print')")
public CommonResult<List<InfoPrintVO>> getPrintListByIds(@RequestParam("ids") String ids) {
//如果ids是空的,返回异常提示,请先选择需要打印的记录
if (StringUtil.isEmpty(ids)) {
......@@ -93,8 +114,41 @@ public class InfoController {
return success(info);
}
@GetMapping("/getUnionPrintListByIds")
@Operation(summary = "获得合并客户拜访打印信息根据Ids")
@PreAuthorize("@ss.hasPermission('visit:info:print')")
public CommonResult<List<InfoPrintVO>> getUnionPrintListByIds(@RequestParam("ids") String ids) {
//如果ids是空的,返回异常提示,请先选择需要打印的记录
if (StringUtil.isEmpty(ids)) {
throw new RuntimeException("请先选择需要合并的打印的记录");
}
String[] split = ids.split(",");
//转为List
List<Long> idList = new ArrayList<>();
for (String s : split) {
idList.add(Long.parseLong(s));
}
List<InfoPrintVO> info = infoService.getUnionInfoByIds(idList);
return success(info);
}
@GetMapping("/getUnionAllPrintListByIds")
@Operation(summary = "获得合并客户拜访打印信息根据Ids")
@PreAuthorize("@ss.hasPermission('visit:info:print')")
public CommonResult<List<InfoPrintVO>> getUnionAllPrintListByIds(@Valid InfoPageReqVO pageReqVO) {
PageResult<InfoDO> pageResult = infoService.getInfoPage(pageReqVO);
if (pageResult.getTotal()==0){
throw new RuntimeException("当前不存在可打印数据");
}
List<InfoDO> list = pageResult.getList();
List<InfoPrintVO> info = infoService.getUnionAllInfoByIds(list);
return success(info);
}
@GetMapping("/getPrintListByCompanyName")
@Operation(summary = "获得客户拜访打印信息根据公司名称")
@PreAuthorize("@ss.hasPermission('visit:customer-info:print')")
public CommonResult<List<InfoPrintVO>> getPrintListByCompanyName(@RequestParam("companyName") String companyName) {
InfoPrintVO info = infoService.getInfoByCompanyName(companyName);
List<InfoPrintVO> infoPrintVO = new ArrayList<>();
......
......@@ -10,6 +10,7 @@ import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
......@@ -32,7 +33,7 @@ public class InfoPrintVO {
private String locationImage;
@Schema(description = "最近四次拜访日期", requiredMode = Schema.RequiredMode.REQUIRED)
private List<LocalDateTime> visitDate;
private List<LocalDate> visitDate;
@Schema(description = "拜访品种")
private String visitProductNames;
......
......@@ -17,7 +17,6 @@ public class InfoSaveReqVO {
private Long id;
@Schema(description = "客户姓名", requiredMode = Schema.RequiredMode.REQUIRED)
@NotEmpty(message = "客户姓名不能为空")
private String customerName;
@Schema(description = "联系方式")
......@@ -68,27 +67,35 @@ public class InfoSaveReqVO {
private Integer customerStatus;
@Schema(description = "拜访品种(多选,逗号分隔)")
@NotEmpty(message = "拜访品种不能为空")
private String visitProductIds;
@Schema(description = "拜访品种名称(多选,逗号分隔)")
@NotEmpty(message = "拜访品种名称不能为空")
private String visitProductNames;
@Schema(description = "拜访方式(字典)")
@NotNull(message = "拜访方式不能为空")
private Integer visitMethod;
@Schema(description = "拜访类型(字典)", example = "2")
@NotNull(message = "拜访类型不能为空")
private Integer visitType;
@Schema(description = "客户所属部门(字典)")
@NotEmpty(message = "客户所属部门不能为空")
private String department;
@Schema(description = "服务内容")
@NotEmpty(message = "服务内容不能为空")
private String serviceContent;
@Schema(description = "客户反馈")
@NotEmpty(message = "客户反馈不能为空")
private String customerFeedback;
@Schema(description = "服务记录图片URL列表(JSON数组)")
@Schema(description = "服务记录图片")
@NotEmpty(message = "服务记录图片不能为空")
private String serviceImages;
}
\ No newline at end of file
......@@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.visit.controller.admin.product;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.idempotent.core.annotation.Idempotent;
import cn.iocoder.yudao.module.system.enums.common.SexEnum;
import com.fhs.common.utils.StringUtil;
import io.swagger.v3.oas.annotations.Parameters;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
......@@ -71,6 +72,26 @@ public class ProductController {
return success(true);
}
@DeleteMapping("/deleteByIds")
@Operation(summary = "删除产品")
@Parameter(name = "ids", description = "编号", required = true)
@PreAuthorize("@ss.hasPermission('visit:product:delete')")
@Idempotent(timeout = 10, message = "重复请求,请稍后重试")
public CommonResult<Boolean> deleteProductByIds(@RequestParam("ids") String ids) {
//如果ids是空的,返回异常提示,请先选择需要打印的记录
if (StringUtil.isEmpty(ids)) {
throw new RuntimeException("请先选择需要删除的记录");
}
String[] split = ids.split(",");
//转为List
List<Long> idList = new ArrayList<>();
for (String s : split) {
idList.add(Long.parseLong(s));
}
productService.deleteProductByIds(idList);
return success(true);
}
@GetMapping("/get")
@Operation(summary = "获得产品")
@Parameter(name = "id", description = "编号", required = true, example = "1024")
......
......@@ -13,14 +13,17 @@ public interface ErrorCodeConstants {
ErrorCode PRODUCT_NOT_EXISTS = new ErrorCode(1001000001, "产品不存在");
ErrorCode PRODUCT_PRODUCT_EXISTS = new ErrorCode(1001000002, "导入产品已存在!");
ErrorCode PRODUCT_IMPORT_LIST_IS_EMPTY = new ErrorCode(1001000003, "导入产品数据不能为空!");
ErrorCode PRODUCT_LIST_NOT_EXISTS = new ErrorCode(1001000004, "所选产品中含有不存在的数据");
// ========== 客户信息 ==========
ErrorCode CUSTOMER_INFO_NOT_EXISTS = new ErrorCode(1002000001, "客户信息不存在");
ErrorCode CUSTOMER_INFO_COMPANY_NAME_DUPLICATE = new ErrorCode(1002000002, "该公司名称已存在");
ErrorCode CUSTOMER_INFO_IMPORT_LIST_IS_EMPTY = new ErrorCode(1002000003, "导入客户数据不能为空!");
ErrorCode CUSTOMER_INFO_EXISTS = new ErrorCode(1002000004, "导入客户数据已存在!");
ErrorCode CUSTOMER_INFO_LIST_NOT_EXISTS = new ErrorCode(1002000005, "所选客户信息中含有不存在的数据");
// ========== 客户拜访记录 ==========
ErrorCode INFO_NOT_EXISTS = new ErrorCode(1003000001, "客户拜访记录不存在");
ErrorCode INFO_LIST_NOT_EXISTS = new ErrorCode(1003000002, "所选客户拜访记录中含有不存在的数据");
}
......@@ -59,4 +59,6 @@ public interface CustomerInfoService {
List<CustomerInfoDO> getAllCustomerInfo();
CustomerImportRespVO importCustomerList(List<CustomerImportExcelVO> list, Boolean updateSupport);
void deleteCustomerInfoByIds(List<Long> idList);
}
\ No newline at end of file
......@@ -106,6 +106,16 @@ public class CustomerInfoServiceImpl implements CustomerInfoService {
customerInfoMapper.deleteById(id);
}
@Override
public void deleteCustomerInfoByIds(List<Long> idList) {
// 校验存在
if(customerInfoMapper.selectByIds(idList).size() != idList.size()){
throw exception(INFO_LIST_NOT_EXISTS);
}
// 删除
customerInfoMapper.deleteByIds(idList);
}
private void validateCustomerInfoExists(Long id) {
if (customerInfoMapper.selectById(id) == null) {
throw exception(CUSTOMER_INFO_NOT_EXISTS);
......
......@@ -65,4 +65,10 @@ public interface InfoService {
* @return 客户拜访记录
*/
InfoPrintVO getInfoByCompanyName(String companyName);
List<InfoPrintVO> getUnionInfoByIds(List<Long> idList);
void deleteInfoByIds(List<Long> idList);
List<InfoPrintVO> getUnionAllInfoByIds(List<InfoDO> list);
}
\ No newline at end of file
......@@ -61,4 +61,6 @@ public interface ProductService {
ProductImportRespVO importProductList(List<ProductImportExcelVO> list, Boolean updateSupport);
List<ProductSimpleReqVO> getProductByIds(String productIds);
void deleteProductByIds(List<Long> idList);
}
\ No newline at end of file
......@@ -65,6 +65,18 @@ public class ProductServiceImpl implements ProductService {
productMapper.deleteById(id);
}
@Override
public void deleteProductByIds(List<Long> idList) {
// 校验存在
if(productMapper.selectByIds(idList).size() != idList.size()){
throw exception(PRODUCT_LIST_NOT_EXISTS);
}
// 删除
productMapper.deleteByIds(idList);
}
private void validateProductExists(Long id) {
if (productMapper.selectById(id) == null) {
throw exception(PRODUCT_NOT_EXISTS);
......
......@@ -65,6 +65,11 @@ export const CustomerInfoApi = {
return await request.delete({ url: `/visit/customer-info/delete?id=` + id })
},
// 批量删除客户信息
deleteCustomerInfoByIds: async (ids: String) => {
return await request.delete({ url: `/visit/customer-info/deleteByIds?ids=` + ids })
},
// 导出客户信息 Excel
exportCustomerInfo: async (params) => {
return await request.download({ url: `/visit/customer-info/export-excel`, params })
......
......@@ -67,6 +67,15 @@ export const InfoApi = {
return await request.get({ url: `/visit/info/get?id=` + id })
},
//查询合并打印客户拜访记录
getUnionPrintListByIds: async (ids: string) => {
return await request.get({ url: `/visit/info/getUnionPrintListByIds`, params: { ids } })
},
//查询合并打印客户拜访记录(筛选出的所有数据)
getUnionAllPrintListByIds: async (params: any) => {
return await request.get({ url: `/visit/info/getUnionAllPrintListByIds`, params })
},
//查询打印客户拜访记录
getPrintListByIds: async (ids: string) => {
return await request.get({ url: `/visit/info/getPrintListByIds`, params: { ids } })
......@@ -93,6 +102,12 @@ export const InfoApi = {
return await request.delete({ url: `/visit/info/delete?id=` + id })
},
// 删除客户拜访记录
deleteInfoByIds: async (ids: String) => {
return await request.delete({ url: `/visit/info/deleteByIds?ids=` + ids })
},
// 导出客户拜访记录 Excel
exportInfo: async (params) => {
return await request.download({ url: `/visit/info/export-excel`, params })
......
......@@ -41,6 +41,12 @@ export const ProductApi = {
return await request.delete({ url: `/visit/product/delete?id=` + id })
},
// 批量删除产品
deleteProductByIds: async (ids: String) => {
return await request.delete({ url: `/visit/product/deleteByIds?ids=` + ids })
},
// 导出产品 Excel
exportProduct: async (params) => {
return await request.download({ url: `/visit/product/export-excel`, params })
......
......@@ -16,10 +16,9 @@
:on-success="uploadSuccess"
list-type="picture-card"
>
<div class="upload-empty">
<div class="upload-empty" >
<slot name="empty">
<Icon icon="ep:plus" />
<!-- <span>请上传图片</span> -->
</slot>
</div>
<template #file="{ file }">
......@@ -116,6 +115,7 @@ const beforeUpload: UploadProps['beforeUpload'] = (rawFile) => {
// 图片上传成功
interface UploadEmits {
(e: 'update:modelValue', value: string[]): void
(e: 'change', value: string[]): void
}
const emit = defineEmits<UploadEmits>()
......@@ -159,16 +159,16 @@ watch(
const emitUpdateModelValue = () => {
let result: string[] = fileList.value.map((file) => file.url!)
emit('update:modelValue', result)
emit('change', result)
}
// 删除图片
const handleRemove = (uploadFile: UploadFile) => {
fileList.value = fileList.value.filter(
(item) => item.url !== uploadFile.url || item.name !== uploadFile.name
)
emit(
'update:modelValue',
fileList.value.map((file) => file.url!)
)
const result = fileList.value.map((file) => file.url!)
emit('update:modelValue', result)
emit('change', result)
}
// 图片上传错误提示
......
......@@ -6,6 +6,7 @@
:rules="formRules"
label-width="100px"
v-loading="formLoading"
:disabled="isDetail"
>
<el-form-item label="客户姓名" prop="customerName">
<el-input v-model="formData.customerName" placeholder="请输入客户姓名" />
......@@ -51,7 +52,7 @@
placeholder="请点击右侧选择"
readonly
>
<template #append>
<template #append v-if="formType !== 'detail'">
<el-link type="primary" @click="openMapPicker">选择地址</el-link>
</template>
</el-input>
......@@ -69,13 +70,13 @@
<!-- <el-form-item label="定位静态图 URL" prop="locationImage">-->
<!-- <UploadImg v-model="formData.locationImage" />-->
<!-- </el-form-item>-->
<el-form-item label="产品信息" prop="productIds">
<el-form-item label="产品信息" prop="productNames">
<el-input
v-model="formData.productNames"
placeholder="请点击右侧选择"
readonly
>
<template #append>
<template #append v-if="formType !== 'detail'">
<el-link type="primary" @click="openProduct">选择产品</el-link>
</template>
</el-input>
......@@ -92,7 +93,7 @@
</el-form-item>
</el-form>
<template #footer>
<el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
<el-button @click="submitForm" type="primary" :disabled="formLoading" v-if="formType !== 'detail'">确 定</el-button>
<el-button @click="dialogVisible = false">取 消</el-button>
</template>
</Dialog>
......@@ -111,6 +112,7 @@ defineOptions({ name: 'CustomerInfoForm' })
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
const isDetail = computed(() => formType.value === 'detail')//detail说明是查看详情,要只读
const dialogVisible = ref(false) // 弹窗的是否展示
const dialogTitle = ref('') // 弹窗的标题
const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
......@@ -159,11 +161,13 @@ const formRules = reactive({
provinceName: [{ required: true, message: '省名称不能为空', trigger: 'blur' }],
cityName: [{ required: true, message: '市名称不能为空', trigger: 'blur' }],
areaName: [{ required: true, message: '区名称不能为空', trigger: 'blur' }],
regionFullName: [{ required: true, message: '所在地区不能为空', trigger: 'blur' }],
regionFullName: [{ required: true, message: '所在地区不能为空', trigger: 'change' }],
locationText: [{ required: true, message: '详细地址不能为空', trigger: 'change' }],
longitude: [{ required: true, message: '经度不能为空', trigger: 'blur' }],
latitude: [{ required: true, message: '纬度不能为空', trigger: 'blur' }],
locationImage: [{ required: true, message: '定位静态图不能为空', trigger: 'blur' }],
productNames: [{ required: true, message: '产品信息名称不能为空', trigger: 'blur|change' }],
productIds: [{ required: true, message: '产品信息不能为空', trigger: 'blur|change' }],
})
const formRef = ref() // 表单 Ref
......@@ -179,6 +183,7 @@ const handleUpdateProduct = (ids: string,names: string) => {
//存入表单
formData.value.productIds = ids;
formData.value.productNames = names;
formRef.value?.validateField('productNames')
};
//===========================================地图相关操作=================================
const mapPickerRef = ref()
......@@ -221,7 +226,7 @@ const handleLocation = ({ lat, lng, address }) => {
city = '市辖区'
}
selectedOptions.value = [province, city, area]//专门回显用的
formRef.value?.validateField('regionFullName')
} else {
console.warn('逆地址解析失败:', res.data.message)
}
......
......@@ -72,6 +72,14 @@
>
<Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button>
<el-button
type="danger"
plain
@click="handleDeleteRows()"
v-hasPermi="['visit:customer-info:delete']"
>
<Icon icon="ep:delete" class="mr-5px" /> 删除
</el-button>
<el-button
type="warning"
plain
......@@ -100,7 +108,7 @@
<el-table-column label="编号" align="center" prop="id" />
<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="companyName" min-width="240px"/>
<el-table-column label="性质等级" align="center" prop="customerType">
<template #default="scope">
<dict-tag :type="DICT_TYPE.CUSTOMER_TYPE" :value="scope.row.customerType" />
......@@ -119,7 +127,7 @@
<ProductList :ids=scope.row.productIds />
</template>
</el-table-column>
<el-table-column label="操作" align="center" min-width="320px" fixed="right">
<el-table-column label="操作" align="center" min-width="380px" fixed="right">
<template #default="scope">
<el-button
link
......@@ -132,9 +140,17 @@
link
type="primary"
@click="handlePrint(scope.row.companyName)"
v-hasPermi="['visit:customer-info:print']"
>
打印拜访记录
</el-button>
<el-button
link
type="primary"
@click="openForm('detail', scope.row.id)"
>
查看详情
</el-button>
<el-button
link
type="primary"
......@@ -166,7 +182,6 @@
<!-- 表单弹窗:添加/修改 -->
<CustomerInfoForm ref="formRef" @success="getList" />
<!-- 打印弹窗 -->
<!-- <VisitPrint ref="visitPrintRef" :data="selectedData" />-->
<Teleport to="body">
<div v-show="false">
<PrintContent ref="printComp" :dataList="printData" />
......@@ -300,6 +315,22 @@ const handleDelete = async (id: number) => {
} catch {}
}
/** 删除按钮操作 */
const handleDeleteRows = async () => {
try {
if (multipleSelection.value.length === 0) {
return message.warning('请先选择需要删除的记录')
}
// 删除的二次确认
await message.delConfirm()
// 发起删除
await CustomerInfoApi.deleteCustomerInfoByIds(multipleSelection.value.join(','))
message.success(t('common.delSuccess'))
// 刷新列表
await getList()
} catch {}
}
/** 导出按钮操作 */
const handleExport = async () => {
try {
......
......@@ -6,8 +6,9 @@
:rules="formRules"
label-width="100px"
v-loading="formLoading"
:disabled="isDetail"
>
<el-form-item label="拜访人姓名" prop="customerName">
<el-form-item label="客户姓名" prop="customerName">
<el-input v-model="formData.customerName" placeholder="请输入拜访人姓名" />
</el-form-item>
<el-form-item label="联系方式" prop="contact">
......@@ -38,7 +39,7 @@
placeholder="请点击右侧选择"
readonly
>
<template #append>
<template #append v-if="formType !== 'detail'" >
<el-link type="primary" @click="openMapPicker">选择地址</el-link>
</template>
</el-input>
......@@ -78,7 +79,7 @@
placeholder="请点击右侧选择"
readonly
>
<template #append>
<template #append v-if="formType !== 'detail'" >
<el-link type="primary" @click="openProduct">选择产品</el-link>
</template>
</el-input>
......@@ -124,38 +125,40 @@
/>
</el-form-item>
<el-form-item label="服务记录图片" prop="serviceImages">
<UploadImgs v-model="formData.serviceImages" :limit="9"/>
<UploadImgs :class="{hide_box: isDetail}" v-model="formData.serviceImages" :limit="9" @change="onServiceImageChange" :disabled="isDetail" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
<el-button @click="submitForm" type="primary" :disabled="formLoading" v-if="formType !== 'detail'" >确 定</el-button>
<el-button @click="dialogVisible = false">取 消</el-button>
</template>
</Dialog>
<productTable ref="productListRef" @updateProduct="handleUpdateProduct" :ids="formData.visitProductIds"/>
</template>
<script setup lang="ts">
import {getIntDictOptions, DICT_TYPE, getStrDictOptions} from '@/utils/dict'
import { InfoApi, InfoVO } from '@/api/visit/info'
import { CustomerInfoApi } from '@/api/visit/customerinfo'
import {DICT_TYPE, getIntDictOptions, 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";
import { pcaTextArr } from 'element-china-area-data'
import {pcaTextArr} from 'element-china-area-data'
import MapPicker from '../util/MapPicker.vue'
import { onMounted } from 'vue'
import request from "@/config/axios";
import {onMounted} from 'vue'
import {homesApi} from "@/api/visit/home";
/** 客户拜访记录 表单 */
defineOptions({ name: 'InfoForm' })
const upload_btn = ref(true)//定义图片上传展示隐藏
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
const dialogVisible = ref(false) // 弹窗的是否展示
const dialogTitle = ref('') // 弹窗的标题
const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
const formType = ref('') // 表单的类型:create - 新增;update - 修改
const formType = ref('') // 表单的类型:create - 新增;update - 修改;detail - 查看
const selectedOptions = ref<string[]>([])
const isDetail = computed(() => formType.value === 'detail')//detail说明是查看详情,要只读
const formData = ref({
id: undefined,
customerName: undefined,
......@@ -181,21 +184,25 @@ const formData = ref({
serviceImages: [],
})
const formRules = reactive({
customerName: [{ required: true, message: '拜访人姓名不能为空', trigger: 'blur' }],
companyName: [{ required: true, message: '客户公司名称不能为空', trigger: 'blur|change' }],
provinceName: [{ required: true, message: '省名称不能为空', trigger: 'blur' }],
cityName: [{ required: true, message: '市名称不能为空', trigger: 'blur' }],
areaName: [{ required: true, message: '区名称不能为空', trigger: 'blur' }],
regionFullName: [{ required: true, message: '所在地区不能为空', trigger: 'blur' }],
regionFullName: [{ required: true, message: '所在地区不能为空', trigger: 'change' }],
locationText: [{ required: true, message: '详细地址不能为空', trigger: 'change' }],
longitude: [{ required: true, message: '经度不能为空', trigger: 'blur' }],
latitude: [{ required: true, message: '纬度不能为空', trigger: 'blur' }],
locationImage: [{ required: true, message: '定位静态图不能为空', trigger: 'blur' }],
visitDate: [{ required: true, message: '拜访日期不能为空', trigger: 'blur' }],
customerStatus: [{ required: true, message: '性质等级不能为空', trigger: 'blur' }],
department: [{ required: true, message: '客户部门不能为空', trigger: 'blur' }],
customerStatus: [{ required: true, message: '性质等级不能为空', trigger: 'change' }],
department: [{ required: true, message: '客户部门不能为空', trigger: 'change' }],
visitMethod: [{ required: true, message: '拜访方式不能为空', trigger: 'change' }],
visitType: [{ required: true, message: '拜访类型不能为空', trigger: 'change' }],
visitProductNames: [{ required: true, message: '拜访品种名称不能为空', trigger: 'blur|change' }],
visitProductIds: [{ required: true, message: '拜访品种不能为空', trigger: 'blur|change' }],
serviceContent: [{ required: true, message: '请输入服务内容', trigger: 'blur' }, { max: 500, message: '最多输入 500 个字符', trigger: 'blur' }],
customerFeedback: [{ required: true, message: '请输入客户反馈', trigger: 'blur' }, { max: 500, message: '最多输入 500 个字符', trigger: 'blur' }],
serviceImages: [{ required: true, message: '请上传服务记录图片', trigger: 'blur|change' }]
})
const formRef = ref() // 表单 Ref
//===========================================下拉搜索相关====================================
......@@ -268,6 +275,11 @@ const handleSelect = (item: Record<string, any>) => {
formData.value.customerStatus = item.customerStatus;
formData.value.department = item.department;
restoreSelectedOptionsFromForm();
formRef.value?.validateField('companyName')
formRef.value?.validateField('regionFullName')
formRef.value?.validateField('locationText')
formRef.value?.validateField('customerStatus')
formRef.value?.validateField('department')
}
//===========================================选择产品相关操作=================================
const productListRef = ref<InstanceType<typeof productTable> | null>(null); // 产品列表 Ref
......@@ -282,6 +294,7 @@ const handleUpdateProduct = (ids: string,names: string) => {
formData.value.visitProductIds = ids;
console.log(names)
formData.value.visitProductNames = names;
formRef.value?.validateField('visitProductNames')
};
//===========================================地图相关操作=================================
const mapPickerRef = ref()
......@@ -323,7 +336,7 @@ const handleLocation = ({ lat, lng, address }) => {
city = '市辖区'
}
selectedOptions.value = [province, city, area]//专门回显用的
formRef.value?.validateField('regionFullName')
} else {
console.warn('逆地址解析失败:', res.data.message)
}
......@@ -442,8 +455,16 @@ const resetForm = () => {
formRef.value?.resetFields()
selectedOptions.value = [];
}
const onServiceImageChange = () => {
// 强制触发校验
formRef.value?.validateField('serviceImages')
}
onMounted(() => {
loadAll()
})
</script>
<style lang="scss" scoped>
.hide_box ::v-deep .el-upload--picture-card {
display: none;
}
</style>
......@@ -117,6 +117,14 @@
>
<Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button>
<el-button
type="danger"
plain
@click="handleDeleteRows()"
v-hasPermi="['visit:info:delete']"
>
<Icon icon="ep:delete" class="mr-5px" /> 删除
</el-button>
<el-button
type="success"
plain
......@@ -131,9 +139,28 @@
plain
@click="handlePrint"
:loading="exportLoading"
v-hasPermi="['visit:info:print']"
>
<Icon icon="ep:printer" class="mr-5px" /> 批量打印
</el-button>
<el-button
type="warning"
plain
@click="handleUnionPrint"
:loading="exportLoading"
v-hasPermi="['visit:info:print']"
>
<Icon icon="ep:printer" class="mr-5px" /> 多选合并打印
</el-button>
<el-button
type="primary"
plain
@click="handleUnionAllPrint"
:loading="exportLoading"
v-hasPermi="['visit:info:print']"
>
<Icon icon="ep:printer" class="mr-5px" /> 全选合并打印
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
......@@ -145,7 +172,7 @@
<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="companyName" width="240px" />
<el-table-column label="所在地区" align="center" prop="regionFullName" width="160px" />
<el-table-column
label="拜访日期"
......@@ -181,8 +208,15 @@
:formatter="dateFormatter"
width="180px"
/>
<el-table-column label="操作" align="center" min-width="120px" fixed="right">
<el-table-column label="操作" align="center" min-width="180px" fixed="right">
<template #default="scope">
<el-button
link
type="primary"
@click="openForm('detail', scope.row.id)"
>
查看详情
</el-button>
<el-button
link
type="primary"
......@@ -214,8 +248,6 @@
<!-- 表单弹窗:添加/修改 -->
<InfoForm ref="formRef" @success="getList" />
<!-- 打印弹窗 -->
<!-- <VisitPrint ref="visitPrintRef" :data="selectedData" />-->
<Teleport to="body">
<div v-show="false">
<PrintContent ref="printComp" :dataList="printData" />
......@@ -228,20 +260,17 @@
import {DICT_TYPE, getIntDictOptions} from '@/utils/dict'
import {dateFormatter} from '@/utils/formatTime'
import download from '@/utils/download'
import {InfoApi, InfoPrintVO, InfoVO} from '@/api/visit/info'
import {InfoApi, InfoVO} from '@/api/visit/info'
import InfoForm from './InfoForm.vue'
import ProductList from "@/views/visit/util/ProductList.vue";
import {useRoute} from 'vue-router'
import { usePrint } from '../util/print'
import {usePrint} from '../util/print'
import PrintContent from '../util/PrintContent.vue'
//==========
const printData = ref([
{ name: '张三', date: '2025-06-01', remark: '拜访说明', image: 'https://image.xnszz.com//20250604/visit_1749022959344.png' },
{ name: '李四', date: '2025-06-03', remark: '再次拜访', image: 'https://image.xnszz.com//20250604/visit_1749022959344.png' },
])
const printData = ref([])
const printComp = ref()
......@@ -250,7 +279,24 @@ const handlePrint = async () => {
return message.warning('请先选择需要打印的记录')
}
printData.value = await InfoApi.getPrintListByIds(multipleSelection.value.join(','))
console.log(printData.value)
await nextTick()
const dom = printComp.value.printRef
if (dom) usePrint(dom)
}
const handleUnionPrint = async () => {
if (multipleSelection.value.length === 0) {
return message.warning('请先选择需要合并打印的记录')
}
printData.value = await InfoApi.getUnionPrintListByIds(multipleSelection.value.join(','))
await nextTick()
const dom = printComp.value.printRef
if (dom) usePrint(dom)
}
const handleUnionAllPrint = async () => {
if (list.value.length === 0) {
return message.warning('当前不存在可打印数据')
}
printData.value = await InfoApi.getUnionAllPrintListByIds(queryParams)
await nextTick()
const dom = printComp.value.printRef
if (dom) usePrint(dom)
......@@ -315,7 +361,7 @@ const resetQuery = () => {
handleQuery()
}
/** 添加/修改操作 */
/** 添加/修改/查看操作 */
const formRef = ref()
const openForm = (type: string, id?: number) => {
formRef.value.open(type, id)
......@@ -334,6 +380,22 @@ const handleDelete = async (id: number) => {
} catch {}
}
/** 批量删除按钮操作 */
const handleDeleteRows = async () => {
try {
if (multipleSelection.value.length === 0) {
return message.warning('请先选择需要删除的记录')
}
// 删除的二次确认
await message.delConfirm()
// 发起删除
await InfoApi.deleteInfoByIds(multipleSelection.value.join(','))
message.success(t('common.delSuccess'))
// 刷新列表
await getList()
} catch {}
}
/** 导出按钮操作 */
const handleExport = async () => {
try {
......
......@@ -72,6 +72,14 @@
>
<Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button>
<el-button
type="danger"
plain
@click="handleDeleteRows()"
v-hasPermi="['visit:product:delete']"
>
<Icon icon="ep:delete" class="mr-5px" /> 删除
</el-button>
<el-button
type="warning"
plain
......@@ -233,6 +241,22 @@ const handleDelete = async (id: number) => {
} catch {}
}
/** 批量删除按钮操作 */
const handleDeleteRows = async () => {
try {
if (multipleSelection.value.length === 0) {
return message.warning('请先选择需要删除的记录')
}
// 删除的二次确认
await message.delConfirm()
// 发起删除
await ProductApi.deleteProductByIds(multipleSelection.value.join(','))
message.success(t('common.delSuccess'))
// 刷新列表
await getList()
} catch {}
}
/** 导出按钮操作 */
const handleExport = async () => {
try {
......
......@@ -92,7 +92,7 @@
justify-content: center; /* 水平居中 */
height: 40px; /* 与其他 div 高度一致 */
width: 100%; /* 需要设定宽度 */"
>{{ formatDate(time) }}</li>
>{{ time[0]+"" + time[1]+""+time[2]+"" }}</li>
</ul>
<div
style="display: flex;
......@@ -170,7 +170,7 @@ const printRef = ref(null)
const formatDate = (date: string | Date) => {
const d = new Date(date)
return d.toLocaleString()
return d.toLocaleDateString()
}
const getSafeImages = (val?: string) => {
if (!val||val==='') return []
......
<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