| | |
| | | -- 为 tcm_clinic 表新增代理关联字段 |
| | | ALTER TABLE tcm_clinic |
| | | ADD COLUMN agent_id BIGINT COMMENT '负责该诊所的省代理ID,tcm_provincial_agent.id' |
| | | AFTER name; |
| | | AFTER name; |
| | | INSERT INTO sys_menu (id, parent_id, name, menu_code, type, path, component, sort, status, visible, permission_key, remark, deleted) VALUES |
| | | (2119, 2100, '省代理管理', 'member_agent_manage', 'C', '/member/user/agent', 'Member/AgentList', 12, 1, 1, 'member:agent:view', '省代理管理菜单', 0), |
| | | (2120, 2119, '新增省代理', 'member_agent_add', 'B', NULL, NULL, 1, 1, 1, 'member:agent:add', '新增省代理权限', 0), |
| | | (2121, 2119, '编辑省代理', 'member_agent_edit', 'B', NULL, NULL, 2, 1, 1, 'member:agent:edit', '编辑省代理权限', 0), |
| | | (2122, 2119, '删除省代理', 'member_agent_delete', 'B', NULL, NULL, 3, 1, 1, 'member:agent:delete', '删除省代理权限', 0), |
| | | (2123, 2119, '省代理详情', 'member_agent_detail', 'B', NULL, NULL, 4, 1, 1, 'member:agent:detail', '省代理详情权限', 0); |
| | | |
| | | |
| | | -- 养生直播:新增设置直播间密码按钮权限 |
| | | INSERT INTO sys_menu (id, parent_id, name, menu_code, type, path, component, icon, sort, status, visible, permission_key, remark, deleted) |
| | | VALUES (3064, 3060, '设置直播间密码', 'live_course_password', 'B', NULL, NULL, NULL, 4, 1, 1, 'live:course:password', '设置直播间密码按钮', 0) |
| | | ON DUPLICATE KEY UPDATE parent_id = VALUES(parent_id), name = VALUES(name), |
| | | menu_code = VALUES(menu_code), type = VALUES(type), sort = VALUES(sort), status = VALUES(status), |
| | | visible = VALUES(visible), permission_key = VALUES(permission_key), remark = VALUES(remark), deleted = VALUES(deleted); |
| | | |
| | | |
| | | -- 为 tcm_clinic 表新增银行信息字段(全部加在 status 字段后面) |
| | | ALTER TABLE tcm_clinic |
| | | ADD COLUMN bank_name VARCHAR(100) COMMENT '开户银行名称(如:中国建设银行)' AFTER status, |
| | | ADD COLUMN account_number VARCHAR(30) COMMENT '银行账号' AFTER bank_name, |
| | | ADD COLUMN account_holder VARCHAR(50) COMMENT '账户名称/户名(即开户人姓名或公司名称)' AFTER account_number, |
| | | ADD COLUMN branch_name VARCHAR(100) COMMENT '开户支行名称(如:成都高新支行)' AFTER account_holder; |
| New file |
| | |
| | | package com.tcm.training.admin.agent.controller; |
| | | import com.tcm.training.admin.agent.dto.ProvincialAgentSaveRequest; |
| | | import com.tcm.training.admin.agent.service.ProvincialAgentAppService; |
| | | import com.tcm.training.admin.agent.vo.ProvincialAgentDetailVO; |
| | | import com.tcm.training.admin.agent.vo.ProvincialAgentListItemVO; |
| | | import com.tcm.training.admin.common.ApiResponse; |
| | | import com.tcm.training.admin.common.PageResponse; |
| | | import org.springframework.validation.annotation.Validated; |
| | | import org.springframework.web.bind.annotation.*; |
| | | import javax.validation.Valid; |
| | | @Validated @RestController @RequestMapping({"/api/v1","/api"}) |
| | | public class ProvincialAgentController { |
| | | private final ProvincialAgentAppService service; public ProvincialAgentController(ProvincialAgentAppService service){this.service=service;} |
| | | @GetMapping("/provincial-agents") public ApiResponse<PageResponse<ProvincialAgentListItemVO>> page(@RequestParam(defaultValue="1") int page,@RequestParam(defaultValue="10") int pageSize,@RequestParam(required=false) String agentName,@RequestParam(required=false) String province){return ApiResponse.ok(service.page(page,pageSize,agentName,province));} |
| | | @GetMapping("/provincial-agents/{id}") public ApiResponse<ProvincialAgentDetailVO> detail(@PathVariable Long id){return ApiResponse.ok(service.detail(id));} |
| | | @PostMapping("/provincial-agents") public ApiResponse<ProvincialAgentDetailVO> create(@Valid @RequestBody ProvincialAgentSaveRequest r){return ApiResponse.ok(service.create(r));} |
| | | @PutMapping("/provincial-agents/{id}") public ApiResponse<ProvincialAgentDetailVO> update(@PathVariable Long id,@Valid @RequestBody ProvincialAgentSaveRequest r){return ApiResponse.ok(service.update(id,r));} |
| | | @DeleteMapping("/provincial-agents/{id}") public ApiResponse<Void> delete(@PathVariable Long id){service.delete(id);return ApiResponse.ok(null);} |
| | | } |
| New file |
| | |
| | | package com.tcm.training.admin.agent.dto; |
| | | |
| | | import lombok.Getter; |
| | | import lombok.Setter; |
| | | |
| | | import javax.validation.Valid; |
| | | import javax.validation.constraints.NotBlank; |
| | | import javax.validation.constraints.Size; |
| | | import java.util.ArrayList; |
| | | import java.util.List; |
| | | |
| | | @Getter |
| | | @Setter |
| | | public class ProvincialAgentSaveRequest { |
| | | @NotBlank(message = "请输入代理负责人") |
| | | @Size(max = 50, message = "代理负责人不能超过50个字") |
| | | private String agentName; |
| | | @NotBlank(message = "请选择负责省份") |
| | | private String province; |
| | | @Size(max = 100) private String bankName; |
| | | @Size(max = 30) private String accountNumber; |
| | | @Size(max = 50) private String accountHolder; |
| | | @Size(max = 100) private String branchName; |
| | | @Size(max = 200) private String contactInfo; |
| | | @Size(max = 255) private String address; |
| | | @Valid private List<RegionItem> regions = new ArrayList<RegionItem>(); |
| | | |
| | | @Getter @Setter |
| | | public static class RegionItem { |
| | | @NotBlank(message = "区域编码不能为空") private String regionCode; |
| | | @Size(max = 100) private String regionName; |
| | | private Integer regionLevel; |
| | | } |
| | | } |
| New file |
| | |
| | | package com.tcm.training.admin.agent.entity; |
| | | |
| | | |
| | | import com.baomidou.mybatisplus.annotation.IdType; |
| | | import com.baomidou.mybatisplus.annotation.TableField; |
| | | import com.baomidou.mybatisplus.annotation.TableId; |
| | | import com.baomidou.mybatisplus.annotation.TableName; |
| | | import lombok.Getter; |
| | | import lombok.NoArgsConstructor; |
| | | import lombok.Setter; |
| | | |
| | | import java.time.LocalDateTime; |
| | | |
| | | @TableName("tcm_agent_region") |
| | | @Getter |
| | | @Setter |
| | | @NoArgsConstructor |
| | | public class AgentRegion { |
| | | |
| | | @TableId(type = IdType.AUTO) |
| | | private Long id; |
| | | |
| | | @TableField("agent_id") |
| | | private Long agentId; |
| | | |
| | | @TableField("region_code") |
| | | private String regionCode; |
| | | |
| | | @TableField("region_name") |
| | | private String regionName; |
| | | |
| | | @TableField("agent_name") |
| | | private String agentName; |
| | | |
| | | @TableField("region_level") |
| | | private Integer regionLevel; |
| | | |
| | | @TableField("created_at") |
| | | private LocalDateTime createdAt; |
| | | } |
| New file |
| | |
| | | package com.tcm.training.admin.agent.entity; |
| | | |
| | | import com.baomidou.mybatisplus.annotation.*; |
| | | import lombok.Getter; |
| | | import lombok.NoArgsConstructor; |
| | | import lombok.Setter; |
| | | |
| | | import java.time.LocalDateTime; |
| | | |
| | | @TableName("tcm_provincial_agent") |
| | | @Getter |
| | | @Setter |
| | | @NoArgsConstructor |
| | | public class ProvincialAgent { |
| | | |
| | | @TableId(type = IdType.AUTO) |
| | | private Long id; |
| | | |
| | | @TableField("agent_name") |
| | | private String agentName; |
| | | |
| | | @TableField("province") |
| | | private String province; |
| | | |
| | | @TableField("bank_name") |
| | | private String bankName; |
| | | |
| | | @TableField("account_number") |
| | | private String accountNumber; |
| | | |
| | | @TableField("account_holder") |
| | | private String accountHolder; |
| | | |
| | | @TableField("branch_name") |
| | | private String branchName; |
| | | |
| | | @TableField("contact_info") |
| | | private String contactInfo; |
| | | |
| | | @TableField("address") |
| | | private String address; |
| | | |
| | | @TableLogic |
| | | private Integer deleted; |
| | | |
| | | @TableField(value = "created_by", fill = FieldFill.INSERT) |
| | | private String createdBy; |
| | | |
| | | @TableField(value = "updated_by", fill = FieldFill.INSERT_UPDATE) |
| | | private String updatedBy; |
| | | |
| | | @TableField("created_at") |
| | | private LocalDateTime createdAt; |
| | | |
| | | @TableField("updated_at") |
| | | private LocalDateTime updatedAt; |
| | | } |
| New file |
| | |
| | | package com.tcm.training.admin.agent.mapper; |
| | | |
| | | import com.baomidou.mybatisplus.core.mapper.BaseMapper; |
| | | import com.tcm.training.admin.agent.entity.AgentRegion; |
| | | import org.apache.ibatis.annotations.Mapper; |
| | | |
| | | @Mapper |
| | | public interface AgentRegionMapper extends BaseMapper<AgentRegion> { |
| | | } |
| New file |
| | |
| | | package com.tcm.training.admin.agent.mapper; |
| | | |
| | | import com.baomidou.mybatisplus.core.mapper.BaseMapper; |
| | | import com.tcm.training.admin.agent.entity.ProvincialAgent; |
| | | import org.apache.ibatis.annotations.Mapper; |
| | | |
| | | @Mapper |
| | | public interface ProvincialAgentMapper extends BaseMapper<ProvincialAgent> { |
| | | |
| | | } |
| New file |
| | |
| | | package com.tcm.training.admin.agent.service; |
| | | |
| | | import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
| | | import com.baomidou.mybatisplus.core.metadata.IPage; |
| | | import com.baomidou.mybatisplus.extension.plugins.pagination.Page; |
| | | import com.tcm.training.admin.agent.dto.ProvincialAgentSaveRequest; |
| | | import com.tcm.training.admin.agent.entity.AgentRegion; |
| | | import com.tcm.training.admin.agent.entity.ProvincialAgent; |
| | | import com.tcm.training.admin.agent.mapper.AgentRegionMapper; |
| | | import com.tcm.training.admin.agent.mapper.ProvincialAgentMapper; |
| | | import com.tcm.training.admin.agent.vo.ProvincialAgentDetailVO; |
| | | import com.tcm.training.admin.agent.vo.ProvincialAgentListItemVO; |
| | | import com.tcm.training.admin.common.PageResponse; |
| | | import org.springframework.stereotype.Service; |
| | | import org.springframework.transaction.annotation.Transactional; |
| | | import java.util.ArrayList; |
| | | import java.util.List; |
| | | |
| | | @Service |
| | | public class ProvincialAgentAppService { |
| | | private final ProvincialAgentMapper agentMapper; private final AgentRegionMapper regionMapper; |
| | | public ProvincialAgentAppService(ProvincialAgentMapper agentMapper, AgentRegionMapper regionMapper) { this.agentMapper = agentMapper; this.regionMapper = regionMapper; } |
| | | public PageResponse<ProvincialAgentListItemVO> page(int page, int pageSize, String agentName, String province) { |
| | | LambdaQueryWrapper<ProvincialAgent> q = new LambdaQueryWrapper<ProvincialAgent>().eq(ProvincialAgent::getDeleted, 0) |
| | | .like(agentName != null && !agentName.trim().isEmpty(), ProvincialAgent::getAgentName, trim(agentName)) |
| | | .like(province != null && !province.trim().isEmpty(), ProvincialAgent::getProvince, trim(province)) |
| | | .orderByDesc(ProvincialAgent::getCreatedAt).orderByDesc(ProvincialAgent::getId); |
| | | IPage<ProvincialAgent> result = agentMapper.selectPage(new Page<ProvincialAgent>(page, pageSize), q); |
| | | List<ProvincialAgentListItemVO> list = new ArrayList<ProvincialAgentListItemVO>(); |
| | | for (ProvincialAgent item : result.getRecords()) list.add(toList(item)); |
| | | return PageResponse.of(list, result.getTotal(), result.getCurrent(), result.getSize()); |
| | | } |
| | | public ProvincialAgentDetailVO detail(Long id) { return toDetail(require(id)); } |
| | | @Transactional public ProvincialAgentDetailVO create(ProvincialAgentSaveRequest request) { ProvincialAgent a = new ProvincialAgent(); fill(a, request); a.setDeleted(0); agentMapper.insert(a); saveRegions(a, request); return detail(a.getId()); } |
| | | @Transactional public ProvincialAgentDetailVO update(Long id, ProvincialAgentSaveRequest request) { ProvincialAgent a = require(id); fill(a, request); agentMapper.updateById(a); regionMapper.delete(new LambdaQueryWrapper<AgentRegion>().eq(AgentRegion::getAgentId, id)); saveRegions(a, request); return detail(id); } |
| | | @Transactional public void delete(Long id) { require(id); agentMapper.deleteById(id); } |
| | | private void fill(ProvincialAgent a, ProvincialAgentSaveRequest r) { a.setAgentName(trim(r.getAgentName())); a.setProvince(trim(r.getProvince())); a.setBankName(trim(r.getBankName())); a.setAccountNumber(trim(r.getAccountNumber())); a.setAccountHolder(trim(r.getAccountHolder())); a.setBranchName(trim(r.getBranchName())); a.setContactInfo(trim(r.getContactInfo())); a.setAddress(trim(r.getAddress())); } |
| | | private void saveRegions(ProvincialAgent a, ProvincialAgentSaveRequest r) { if (r.getRegions() == null) return; for (ProvincialAgentSaveRequest.RegionItem x : r.getRegions()) { if (x == null || trim(x.getRegionCode()) == null) continue; AgentRegion ar = new AgentRegion(); ar.setAgentId(a.getId()); ar.setAgentName(a.getAgentName()); ar.setRegionCode(trim(x.getRegionCode())); ar.setRegionName(trim(x.getRegionName())); ar.setRegionLevel(x.getRegionLevel()); regionMapper.insert(ar); } } |
| | | private ProvincialAgent require(Long id) { ProvincialAgent a = agentMapper.selectOne(new LambdaQueryWrapper<ProvincialAgent>().eq(ProvincialAgent::getId,id).eq(ProvincialAgent::getDeleted,0).last("limit 1")); if (a == null) throw new IllegalArgumentException("省代理不存在"); return a; } |
| | | private ProvincialAgentListItemVO toList(ProvincialAgent a) { ProvincialAgentListItemVO v = new ProvincialAgentListItemVO(); v.setId(a.getId()); v.setAgentName(a.getAgentName()); v.setProvince(a.getProvince()); v.setContactInfo(a.getContactInfo()); v.setAddress(a.getAddress()); List<AgentRegion> rs = regions(a.getId()); v.setRegionCount(rs.size()); List<String> names = new ArrayList<String>(); for (AgentRegion r:rs) names.add(r.getRegionName() == null ? r.getRegionCode() : r.getRegionName()); v.setRegionNames(names); v.setCreatedAt(a.getCreatedAt()); return v; } |
| | | private ProvincialAgentDetailVO toDetail(ProvincialAgent a) { ProvincialAgentDetailVO v = new ProvincialAgentDetailVO(); ProvincialAgentListItemVO base=toList(a); v.setId(base.getId()); v.setAgentName(base.getAgentName()); v.setProvince(base.getProvince()); v.setContactInfo(base.getContactInfo()); v.setAddress(base.getAddress()); v.setRegionCount(base.getRegionCount()); v.setRegionNames(base.getRegionNames()); v.setCreatedAt(base.getCreatedAt()); v.setBankName(a.getBankName()); v.setAccountNumber(a.getAccountNumber()); v.setAccountHolder(a.getAccountHolder()); v.setBranchName(a.getBranchName()); v.setUpdatedAt(a.getUpdatedAt()); List<ProvincialAgentDetailVO.RegionVO> out=new ArrayList<ProvincialAgentDetailVO.RegionVO>(); for(AgentRegion r:regions(a.getId())) { ProvincialAgentDetailVO.RegionVO x=new ProvincialAgentDetailVO.RegionVO(); x.setRegionCode(r.getRegionCode()); x.setRegionName(r.getRegionName()); x.setRegionLevel(r.getRegionLevel()); out.add(x); } v.setRegions(out); return v; } |
| | | private List<AgentRegion> regions(Long id) { return regionMapper.selectList(new LambdaQueryWrapper<AgentRegion>().eq(AgentRegion::getAgentId,id).orderByAsc(AgentRegion::getId)); } |
| | | private String trim(String s) { if(s==null)return null; String x=s.trim(); return x.isEmpty()?null:x; } |
| | | } |
| New file |
| | |
| | | package com.tcm.training.admin.agent.vo; |
| | | |
| | | import lombok.Getter; |
| | | import lombok.Setter; |
| | | import java.time.LocalDateTime; |
| | | import java.util.List; |
| | | |
| | | @Getter @Setter |
| | | public class ProvincialAgentDetailVO extends ProvincialAgentListItemVO { |
| | | private String bankName; private String accountNumber; private String accountHolder; |
| | | private String branchName; private LocalDateTime updatedAt; private List<RegionVO> regions; |
| | | @Getter @Setter public static class RegionVO { private String regionCode; private String regionName; private Integer regionLevel; } |
| | | } |
| New file |
| | |
| | | package com.tcm.training.admin.agent.vo; |
| | | |
| | | import lombok.Getter; |
| | | import lombok.Setter; |
| | | import java.time.LocalDateTime; |
| | | import java.util.List; |
| | | |
| | | @Getter @Setter |
| | | public class ProvincialAgentListItemVO { |
| | | private Long id; private String agentName; private String province; |
| | | private String contactInfo; private String address; private Integer regionCount; |
| | | private List<String> regionNames; private LocalDateTime createdAt; |
| | | } |
| | |
| | | null, null, null, 6, "member:user:export", "导出用户按钮")); |
| | | menuIds.add(saveMenu(2117L, 2110L, "解绑微信", "member_user_unbind_wechat", "B", |
| | | null, null, null, 7, "member:user:unbind-wechat", "解绑微信按钮")); |
| | | menuIds.add(saveMenu(2119L, 2100L, "省代理管理", "member_agent_manage", "C", |
| | | "/member/user/agent", "Member/AgentList", null, 12, "member:agent:view", "省代理管理菜单")); |
| | | menuIds.add(saveMenu(2120L, 2119L, "新增省代理", "member_agent_add", "B", |
| | | null, null, null, 1, "member:agent:add", "新增省代理按钮")); |
| | | menuIds.add(saveMenu(2121L, 2119L, "编辑省代理", "member_agent_edit", "B", |
| | | null, null, null, 2, "member:agent:edit", "编辑省代理按钮")); |
| | | menuIds.add(saveMenu(2122L, 2119L, "删除省代理", "member_agent_delete", "B", |
| | | null, null, null, 3, "member:agent:delete", "删除省代理按钮")); |
| | | menuIds.add(saveMenu(2123L, 2119L, "省代理详情", "member_agent_detail", "B", |
| | | null, null, null, 4, "member:agent:detail", "省代理详情按钮")); |
| | | menuIds.add(saveMenu(3000L, 0L, "课程与学习", "live_root", "M", |
| | | "/live", null, "read", 300, null, "课程与学习模块")); |
| | | menuIds.add(saveMenu(3050L, 3000L, "课程管理", "live_course_manage_root", "M", |
| | |
| | | null, null, null, 2, "live:course:edit", "养生直播编辑按钮")); |
| | | menuIds.add(saveMenu(3063L, 3060L, "启用禁用直播", "live_course_status", "B", |
| | | null, null, null, 3, "live:course:status", "养生直播状态按钮")); |
| | | menuIds.add(saveMenu(3064L, 3060L, "设置直播间密码", "live_course_password", "B", |
| | | null, null, null, 4, "live:course:password", "设置直播间密码按钮")); |
| | | menuIds.add(saveMenu(3070L, 3060L, "场次管理", "live_session_manage", "B", |
| | | null, null, null, 10, "live:session:detail", "直播场次管理权限")); |
| | | menuIds.add(saveMenu(3071L, 3060L, "新增场次", "live_session_add", "B", |
| | |
| | | /** 状态:1启用 0禁用 */ |
| | | private Integer status; |
| | | |
| | | @TableField("bank_name") |
| | | private String bankName; |
| | | |
| | | @TableField("account_number") |
| | | private String accountNumber; |
| | | |
| | | @TableField("account_holder") |
| | | private String accountHolder; |
| | | |
| | | @TableField("branch_name") |
| | | private String branchName; |
| | | |
| | | @TableLogic |
| | | private Integer deleted; |
| | | |
| | |
| | | /** |
| | | /** |
| | | * @name umi 的路由配置 |
| | | * @description 只支持 path,component,routes,redirect,wrappers,name,icon 的配置 |
| | | * @param path path 只支持两种占位符配置,第一种是动态参数 :id 的形式,第二种是 * 通配符,通配符只能出现路由字符串的最后。 |
| | |
| | | component: './member/clinic', |
| | | }, |
| | | { |
| | | name: '省代理管理', |
| | | path: '/member/user/agent', |
| | | access: 'member:agent:view', |
| | | component: './member/agent', |
| | | }, |
| | | { |
| | | name: '诊所详情', |
| | | path: '/member/user/clinic/:id', |
| | | hideInMenu: true, |
| | |
| | | const canViewMemberUserGroup = |
| | | !!permission?.['member:user:view'] || |
| | | !!permission?.['member:clinic:view'] || |
| | | !!permission?.['member:agent:view'] || |
| | | !!permission?.['member:user:member:view']; |
| | | const canViewMemberVipGroup = |
| | | !!permission?.['member:vip-price:view'] || !!permission?.['member:vip-intro:view']; |
| New file |
| | |
| | | import { request } from '@umijs/max'; |
| | | import { Cascader } from 'antd'; |
| | | import type { CascaderProps } from 'antd/es/cascader'; |
| | | import React, { useCallback, useEffect, useRef, useState } from 'react'; |
| | | |
| | | type RegionRecord = { |
| | | id: number; |
| | | code: string; |
| | | name: string; |
| | | parentId: number; |
| | | level: number; |
| | | }; |
| | | |
| | | export type RegionCascaderOption = { |
| | | label: string; |
| | | value: string; |
| | | level: number; |
| | | isLeaf: boolean; |
| | | loading?: boolean; |
| | | children?: RegionCascaderOption[]; |
| | | }; |
| | | |
| | | type RegionResponse = { |
| | | data: RegionRecord[]; |
| | | }; |
| | | |
| | | export type RegionCascaderProps = Omit< |
| | | CascaderProps<RegionCascaderOption, 'value', false>, |
| | | 'loadData' | 'options' |
| | | >; |
| | | |
| | | const ROOT_CACHE_KEY = '__root__'; |
| | | |
| | | const toOptions = (regions: RegionRecord[]): RegionCascaderOption[] => |
| | | regions.map((region) => ({ |
| | | label: region.name, |
| | | value: region.code, |
| | | level: region.level, |
| | | isLeaf: region.level >= 3, |
| | | })); |
| | | |
| | | const replaceOption = ( |
| | | options: RegionCascaderOption[], |
| | | value: string, |
| | | replacement: RegionCascaderOption, |
| | | ): RegionCascaderOption[] => |
| | | options.map((option) => { |
| | | if (option.value === value) { |
| | | return replacement; |
| | | } |
| | | if (!option.children?.length) { |
| | | return option; |
| | | } |
| | | return { ...option, children: replaceOption(option.children, value, replacement) }; |
| | | }); |
| | | |
| | | const RegionCascader: React.FC<RegionCascaderProps> = ({ value, ...cascaderProps }) => { |
| | | const [options, setOptions] = useState<RegionCascaderOption[]>([]); |
| | | const [loading, setLoading] = useState(true); |
| | | const cacheRef = useRef(new Map<string, RegionCascaderOption[]>()); |
| | | |
| | | const fetchRegions = useCallback(async (parentCode?: string) => { |
| | | const cacheKey = parentCode || ROOT_CACHE_KEY; |
| | | const cached = cacheRef.current.get(cacheKey); |
| | | if (cached) { |
| | | return cached; |
| | | } |
| | | |
| | | const response = await request<RegionResponse>('/app/register/regions', { |
| | | method: 'GET', |
| | | params: parentCode ? { parentCode } : undefined, |
| | | }); |
| | | const nextOptions = toOptions(response.data || []); |
| | | cacheRef.current.set(cacheKey, nextOptions); |
| | | return nextOptions; |
| | | }, []); |
| | | |
| | | useEffect(() => { |
| | | let active = true; |
| | | |
| | | const initialize = async () => { |
| | | setLoading(true); |
| | | try { |
| | | let rootOptions = await fetchRegions(); |
| | | |
| | | // 编辑表单传入完整编码路径时,预加载路径以正确显示各级名称。 |
| | | if (Array.isArray(value)) { |
| | | for (const parentCode of value.slice(0, -1)) { |
| | | const children = await fetchRegions(parentCode); |
| | | const attachChildren = (items: RegionCascaderOption[]): RegionCascaderOption[] => |
| | | items.map((item) => { |
| | | if (item.value === parentCode) { |
| | | return { ...item, children, isLeaf: children.length === 0 }; |
| | | } |
| | | return item.children?.length |
| | | ? { ...item, children: attachChildren(item.children) } |
| | | : item; |
| | | }); |
| | | rootOptions = attachChildren(rootOptions); |
| | | } |
| | | } |
| | | |
| | | if (active) { |
| | | setOptions(rootOptions); |
| | | } |
| | | } finally { |
| | | if (active) { |
| | | setLoading(false); |
| | | } |
| | | } |
| | | }; |
| | | |
| | | void initialize(); |
| | | return () => { |
| | | active = false; |
| | | }; |
| | | }, [fetchRegions, value]); |
| | | |
| | | const loadData = async (selectedOptions: RegionCascaderOption[]) => { |
| | | const target = selectedOptions[selectedOptions.length - 1]; |
| | | if (!target || target.isLeaf) { |
| | | return; |
| | | } |
| | | |
| | | setOptions((current) => replaceOption(current, target.value, { ...target, loading: true })); |
| | | try { |
| | | const children = await fetchRegions(target.value); |
| | | setOptions((current) => |
| | | replaceOption(current, target.value, { |
| | | ...target, |
| | | loading: false, |
| | | isLeaf: children.length === 0, |
| | | children, |
| | | }), |
| | | ); |
| | | } catch (error) { |
| | | setOptions((current) => replaceOption(current, target.value, { ...target, loading: false })); |
| | | throw error; |
| | | } |
| | | }; |
| | | |
| | | return ( |
| | | <Cascader<RegionCascaderOption, 'value'> |
| | | {...cascaderProps} |
| | | value={value} |
| | | options={options} |
| | | loadData={loadData} |
| | | loading={loading || cascaderProps.loading} |
| | | placeholder={cascaderProps.placeholder || '请选择省市区'} |
| | | /> |
| | | ); |
| | | }; |
| | | |
| | | export default RegionCascader; |
| | | |
| | |
| | | const canDetail = access['live:course:detail']; |
| | | const canEdit = access['live:course:edit']; |
| | | const canStatus = access['live:course:status']; |
| | | const canPassword = access['live:course:password']; |
| | | |
| | | const loadList = async () => { |
| | | setLoading(true); |
| | |
| | | {record.status === 1 ? '禁用' : '启用'} |
| | | </button> |
| | | ) : null} |
| | | {index === 0 && canEdit ? ( |
| | | {index === 0 && canPassword ? ( |
| | | <button type="button" onClick={() => openPasswordModal(record)}> |
| | | 直播间密码 |
| | | </button> |
| New file |
| | |
| | | import RegionCascader from '@/components/RegionCascader'; |
| | | import { PlusOutlined } from '@ant-design/icons'; |
| | | import { history, useAccess } from '@umijs/max'; |
| | | import { Button, Descriptions, Form, Input, Modal, Pagination, Space, Table, message } from 'antd'; |
| | | import React, { useEffect, useState } from 'react'; |
| | | import { createAgent, deleteAgent, getAgentDetail, getAgentPage, updateAgent } from './service'; |
| | | |
| | | type Region={regionCode:string|string[];regionName?:string;regionLevel?:number}; type Item={id:number;agentName:string;province:string;contactInfo?:string;address?:string;regionNames?:string[];regionCount:number;createdAt?:string}; |
| | | const AgentPage:React.FC=()=>{const access=useAccess();const [search]=Form.useForm();const [form]=Form.useForm();const [list,setList]=useState<Item[]>([]);const [pagination,setPagination]=useState({page:1,pageSize:10,total:0});const [loading,setLoading]=useState(false);const [open,setOpen]=useState(false);const [detail,setDetail]=useState<any>();const [editing,setEditing]=useState<number>();const canView=access['member:agent:view']; |
| | | const load=async(page=1,pageSize=pagination.pageSize)=>{setLoading(true);try{const r=await getAgentPage({page,pageSize,...search.getFieldsValue()});setList(r.data?.list||[]);setPagination({page:Number(r.data?.page||page),pageSize:Number(r.data?.pageSize||pageSize),total:Number(r.data?.total||0)});}finally{setLoading(false);}};useEffect(()=>{if(!canView){history.replace('/Welcome');return;}load();},[canView]); |
| | | const edit=async(id?:number)=>{setEditing(id);setOpen(true);if(id){const r=await getAgentDetail(id);const d=r.data;setDetail(d);form.setFieldsValue({...d,regions:d.regions||[]});}else{setDetail(undefined);form.resetFields();form.setFieldsValue({regions:[{}]});}}; |
| | | const submit=async()=>{const v=await form.validateFields();const regions=(v.regions||[]).filter((x:Region)=>x?.regionCode).map((x:Region)=>{const path=Array.isArray(x.regionCode)?x.regionCode:[x.regionCode];return {...x,regionCode:path[path.length-1],regionName:x.regionName||path[path.length-1],regionLevel:path.length};});const data={...v,regions};try{if(editing)await updateAgent(editing,data);else await createAgent(data);message.success(editing?'保存成功':'新增成功');setOpen(false);load(editing?pagination.page:1);}catch(e){message.error((e as Error).message||'保存失败');}}; |
| | | const columns=[{title:'序号',width:70,render:(_:any,__ :any,i:number)=>(pagination.page-1)*pagination.pageSize+i+1},{title:'代理负责人',dataIndex:'agentName'},{title:'负责省份',dataIndex:'province'},{title:'负责区域',render:(_:any,r:Item)=>(r.regionNames||[]).join('、')||'-'},{title:'联系信息',dataIndex:'contactInfo',render:(v:string)=>v||'-'},{title:'创建时间',dataIndex:'createdAt',width:180},{title:'操作',render:(_:any,r:Item)=><Space><Button type="link" onClick={()=>{getAgentDetail(r.id).then(x=>{setDetail(x.data);setOpen(true);setEditing(undefined);});}}>详情</Button>{access['member:agent:edit']&&<Button type="link" onClick={()=>edit(r.id)}>编辑</Button>}{access['member:agent:delete']&&<Button type="link" danger onClick={()=>Modal.confirm({title:'确定删除该省代理吗?',onOk:async()=>{await deleteAgent(r.id);message.success('删除成功');load();}})}>删除</Button>}</Space>}]; |
| | | return <div style={{padding:24}}><h2>省代理管理</h2><Form form={search} layout="inline" onFinish={()=>load(1)}><Form.Item name="agentName" label="代理负责人"><Input allowClear placeholder="请输入"/></Form.Item><Form.Item name="province" label="负责省份"><Input allowClear placeholder="请输入"/></Form.Item><Button onClick={()=>{search.resetFields();load(1)}}>重置</Button><Button type="primary" htmlType="submit" style={{marginLeft:8}}>查询</Button></Form><div style={{margin:'16px 0'}}>{access['member:agent:add']&&<Button type="primary" icon={<PlusOutlined/>} onClick={()=>edit()}>新增</Button>}</div><Table rowKey="id" columns={columns as any} dataSource={list} loading={loading} pagination={false}/><Pagination style={{marginTop:16,textAlign:'right'}} current={pagination.page} pageSize={pagination.pageSize} total={pagination.total} showSizeChanger onChange={load}/> |
| | | <Modal open={open} title={detail&&!editing?'省代理详情':editing?'编辑省代理':'新增省代理'} width={760} onCancel={()=>{setOpen(false);setDetail(undefined)}} footer={detail&&!editing?null:<><Button onClick={()=>setOpen(false)}>取消</Button><Button type="primary" onClick={submit}>确定</Button></>}> |
| | | {detail&&!editing?<Descriptions bordered column={2} items={[['代理负责人',detail.agentName],['负责省份',detail.province],['负责区域',(detail.regionNames||[]).join('、')||'-'],['开户银行',detail.bankName],['银行账号',detail.accountNumber],['账户名称',detail.accountHolder],['开户支行',detail.branchName],['联系信息',detail.contactInfo],['详细地址',detail.address]].map(([label,children])=>({label,children:children||'-'}))}/>:<Form form={form} labelCol={{span:5}}><Form.Item name="agentName" label="代理负责人" rules={[{required:true,message:'请输入代理负责人'}]}><Input maxLength={50}/></Form.Item><Form.Item name="province" label="负责省份" rules={[{required:true,message:'请输入负责省份'}]}><Input maxLength={20}/></Form.Item><Form.List name="regions">{(fields,{add,remove})=><>{fields.map(({key,name,...rest})=><Space key={key} align="baseline"><Form.Item {...rest} name={[name,'regionCode']} rules={[{required:true,message:'请选择区域'}]}><RegionCascader/></Form.Item><Form.Item {...rest} name={[name,'regionName']} hidden><Input/></Form.Item><Button onClick={()=>remove(name)}>删除</Button></Space>)}<Button type="dashed" onClick={()=>add({})}>添加区域</Button></>}</Form.List><Form.Item name="bankName" label="开户银行"><Input/></Form.Item><Form.Item name="accountNumber" label="银行账号"><Input/></Form.Item><Form.Item name="accountHolder" label="账户名称"><Input/></Form.Item><Form.Item name="branchName" label="开户支行"><Input/></Form.Item><Form.Item name="contactInfo" label="联系信息"><Input/></Form.Item><Form.Item name="address" label="详细地址"><Input.TextArea rows={3}/></Form.Item></Form>} |
| | | </Modal></div>};export default AgentPage; |
| New file |
| | |
| | | import { request } from '@umijs/max'; |
| | | export const getAgentPage=(params:any)=>request('/provincial-agents',{method:'GET',params}); |
| | | export const getAgentDetail=(id:number|string)=>request(`/provincial-agents/${id}`,{method:'GET'}); |
| | | export const createAgent=(data:any)=>request('/provincial-agents',{method:'POST',data}); |
| | | export const updateAgent=(id:number|string,data:any)=>request(`/provincial-agents/${id}`,{method:'PUT',data}); |
| | | export const deleteAgent=(id:number|string)=>request(`/provincial-agents/${id}`,{method:'DELETE'}); |