feat(system): 新增产品线选择组件

- 重构 CsSelectOptionItemVo 类,改名为 CsSelectOptionVo
- 新增 SystemComponentController 控制器,提供部门下拉选择数据源查询接口
- 新增 SystemProductLineVo 类,用于表示产品线选择框的选项
- 实现查询部门选择数据源的方法,构建产品线选择框的选项列表
- 新增前端 API 接口和组件以支持产品线选择功能
- 更新业务演示页面,添加产品线选择框
This commit is contained in:
2025-07-27 11:55:26 +08:00
parent f245ed17c2
commit 14460b1c9a
10 changed files with 247 additions and 29 deletions
@@ -0,0 +1,8 @@
import request from '@/utils/request'
export function queryDeptSelectDataSource() {
return request({
url: 'api/component/queryDeptSelectDataSource',
method: 'post'
})
}
@@ -0,0 +1,75 @@
<template>
<el-select
v-model="selectedValue"
filterable
clearable
placeholder="请选择产品线"
:disabled="disabled"
@change="handleChange"
>
<el-option
v-for="item in deptOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</template>
<script>
import { queryDeptSelectDataSource } from '@/api/system/component'
import CsMessage from '@/utils/elementui/CsMessage'
export default {
name: 'CuteProductLineSelect',
props: {
value: {
type: [Number, String],
default: null
},
disabled: {
type: Boolean,
default: false
}
},
data() {
return {
deptOptions: [],
selectedValue: this.value
}
},
watch: {
value(newVal) {
this.selectedValue = newVal
}
},
created() {
this.fetchDeptData()
},
methods: {
fetchDeptData() {
queryDeptSelectDataSource().then(data => {
this.deptOptions = data
}).catch(error => {
console.error('获取部门数据失败:', error)
CsMessage.Error('获取部门数据失败')
})
},
handleChange(value) {
this.selectedValue = value
for (const item of this.deptOptions) {
if (item.value === value) {
this.$emit('detail', item)
break
}
}
// 绑定change事件
this.$emit('change', value)
// 绑定form value
this.$emit('input', value)
}
}
}
</script>
@@ -12,9 +12,13 @@
<el-form-item label="部门" prop="dept">
<cute-dept-tree v-model="form.dept" />
</el-form-item>
<el-form-item label="产品线" prop="productLine">
<cute-product-line-select v-model="form.productLine" @detail="onProductLineDetailChange" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onSubmit">立即创建</el-button>
<el-button>取消</el-button>
<el-button type="primary" @click="onSubmit('form')">提 交</el-button>
<el-button type="danger" @click="onReset('form')">重 置</el-button>
<el-button>取 消</el-button>
</el-form-item>
</el-form>
</el-card>
@@ -30,6 +34,12 @@
</div>
<cute-dept-tree @node-click="onDeptTreeNodeClick" @search="onDeptTreeSearch" />
</el-card>
<el-card style="margin-top: 10px">
<div slot="header" class="clearfix">
<span>产品线选择框</span>
</div>
<cute-product-line-select @change="onProductLineChange" @detail="onProductLineDetailChange" />
</el-card>
</div>
</template>
@@ -37,31 +47,49 @@
import CuteUserSelect from '@/views/components/business/CuteUserSelect.vue'
import CuteDeptTree from '@/views/components/business/CuteDeptTree.vue'
import CuteProductLineSelect from '@/views/components/business/CuteProductLineSelect.vue'
export default {
name: 'CuteBusinessDemo',
components: { CuteDeptTree, CuteUserSelect },
components: { CuteProductLineSelect, CuteDeptTree, CuteUserSelect },
data() {
return {
form: {
user: null,
dept: null
dept: null,
productLine: null
}
}
},
methods: {
onUserSelectChange(data) {
console.error('onUserSelectChange=', data)
console.log('onUserSelectChange=', data)
},
onDeptTreeNodeClick(data) {
console.error('onDeptTreeNodeClick=', data)
console.log('onDeptTreeNodeClick=', data)
},
onSubmit() {
console.error('this.form=', this.form)
onSubmit(formName) {
this.$refs[formName].validate((valid) => {
if (valid) {
console.log('this.form=', this.form)
} else {
console.error('error submit!!')
return false
}
})
},
onReset(formName) {
this.$refs[formName].resetFields()
},
onDeptTreeSearch(key) {
console.error('onDeptTreeSearch:key', key)
console.error('onDeptTreeSearch:form.dept', this.form.dept)
console.log('onDeptTreeSearch:key', key)
console.log('onDeptTreeSearch:form.dept', this.form.dept)
},
onProductLineChange(data) {
console.log('onProductLineChange', data)
},
onProductLineDetailChange(data) {
console.log('onProductLineDetailChange', data)
}
}
}
@@ -545,9 +545,9 @@ export default {
handleWebSocketMessage(e) {
const that = this
const data = e.data
// console.error('来自服务器的数据:data', data)
// console.log('来自服务器的数据:data', data)
const callbackData = JSON.parse(data)
// console.error('callbackData', callbackData)
// console.log('callbackData', callbackData)
try {
that.dynamicInstance = JSON.parse(callbackData.data)
// 判断流水线是否结束
@@ -607,9 +607,9 @@ export default {
id: that.dynamicTemplate.id
}
try {
if (that.dynamicHook) {
clearInterval(that.dynamicHook)
}
// if (that.dynamicHook) {
// clearInterval(that.dynamicHook)
// }
that.dynamicStartButtonLoading = true
// 这里的 dynamicStartupStatus状态,需要后期与上下文关联后,初始化的时候带入最近一个流水线实例
// let result
@@ -30,7 +30,7 @@ import java.util.Map;
@AllArgsConstructor
@NoArgsConstructor
@EqualsAndHashCode(callSuper = false)
public class CsSelectOptionItemVo extends CsObject {
public class CsSelectOptionVo extends CsObject {
private String label;
private String value;
private Map<String, Object> ext;
@@ -15,7 +15,7 @@
*/
package cn.odboy.util;
import cn.odboy.base.CsSelectOptionItemVo;
import cn.odboy.base.CsSelectOptionVo;
import com.alibaba.fastjson2.JSON;
import lombok.experimental.UtilityClass;
@@ -197,22 +197,22 @@ public final class CsCollUtil extends cn.hutool.core.collection.CollUtil {
}
public static void main(String[] args) {
List<CsSelectOptionItemVo> testData = new ArrayList<>();
CsSelectOptionItemVo selectOption1 = CsSelectOptionItemVo.builder().label("苹果").value("apple").build();
CsSelectOptionItemVo selectOption2 = CsSelectOptionItemVo.builder().label("苹果").value("apple").build();
CsSelectOptionItemVo selectOption3 = CsSelectOptionItemVo.builder().label("香蕉").value("banana").build();
CsSelectOptionItemVo selectOption4 = CsSelectOptionItemVo.builder().label("猫").value("cat").build();
List<CsSelectOptionVo> testData = new ArrayList<>();
CsSelectOptionVo selectOption1 = CsSelectOptionVo.builder().label("苹果").value("apple").build();
CsSelectOptionVo selectOption2 = CsSelectOptionVo.builder().label("苹果").value("apple").build();
CsSelectOptionVo selectOption3 = CsSelectOptionVo.builder().label("香蕉").value("banana").build();
CsSelectOptionVo selectOption4 = CsSelectOptionVo.builder().label("猫").value("cat").build();
testData.add(selectOption1);
testData.add(selectOption2);
testData.add(selectOption3);
testData.add(selectOption4);
List<CsSelectOptionItemVo> selectOptions1 = CsCollUtil.streamAscSort(testData, CsSelectOptionItemVo::getValue);
List<CsSelectOptionVo> selectOptions1 = CsCollUtil.streamAscSort(testData, CsSelectOptionVo::getValue);
System.out.println(JSON.toJSONString(selectOptions1));
List<CsSelectOptionItemVo> selectOptions2 = CsCollUtil.streamDescSort(testData, CsSelectOptionItemVo::getValue);
List<CsSelectOptionVo> selectOptions2 = CsCollUtil.streamDescSort(testData, CsSelectOptionVo::getValue);
System.out.println(JSON.toJSONString(selectOptions2));
List<CsSelectOptionItemVo> selectOptions3 = CsCollUtil.streamDistinct(testData, CsSelectOptionItemVo::getValue);
List<CsSelectOptionVo> selectOptions3 = CsCollUtil.streamDistinct(testData, CsSelectOptionVo::getValue);
System.out.println(JSON.toJSONString(selectOptions3));
Map<String, String> selectMap1 = CsCollUtil.streamToMap(testData, CsSelectOptionItemVo::getValue, CsSelectOptionItemVo::getLabel);
Map<String, String> selectMap1 = CsCollUtil.streamToMap(testData, CsSelectOptionVo::getValue, CsSelectOptionVo::getLabel);
System.out.println(JSON.toJSONString(selectMap1));
// 这只是一个示范
BigDecimal total1 = CsCollUtil.streamBigDecimalAdd(testData, "value");
@@ -0,0 +1,27 @@
package cn.odboy.system.controller;
import cn.odboy.system.service.SystemDeptService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequiredArgsConstructor
@Api(tags = "系统:组件数据源")
@RequestMapping("/api/component")
public class SystemComponentController {
private final SystemDeptService systemDeptService;
@ApiOperation("查询部门下拉选择数据源")
@PostMapping(value = "/queryDeptSelectDataSource")
@PreAuthorize("@el.check()")
public ResponseEntity<?> queryDeptSelectDataSource() {
return new ResponseEntity<>(systemDeptService.queryDeptSelectDataSource(), HttpStatus.OK);
}
}
@@ -3,7 +3,7 @@ package cn.odboy.system.controller;
import cn.hutool.core.collection.CollectionUtil;
import cn.odboy.base.CsPageArgs;
import cn.odboy.base.CsResultVo;
import cn.odboy.base.CsSelectOptionItemVo;
import cn.odboy.base.CsSelectOptionVo;
import cn.odboy.framework.exception.BadRequestException;
import cn.odboy.framework.properties.AppProperties;
import cn.odboy.system.constant.SystemCaptchaBizEnum;
@@ -190,7 +190,7 @@ public class SystemUserController {
@ApiOperation("查询用户基础数据")
@PostMapping(value = "/queryUserMetadataOptions")
@PreAuthorize("@el.check('user:list')")
public ResponseEntity<List<CsSelectOptionItemVo>> queryUserMetadataOptions(@Validated @RequestBody CsPageArgs<SystemQueryUserArgs> args) {
public ResponseEntity<List<CsSelectOptionVo>> queryUserMetadataOptions(@Validated @RequestBody CsPageArgs<SystemQueryUserArgs> args) {
int maxPageSize = 50;
SystemQueryUserArgs criteria = args.getArgs();
LambdaQueryWrapper<SystemUserTb> wrapper = new LambdaQueryWrapper<>();
@@ -210,7 +210,7 @@ public class SystemUserController {
ext.put("deptId", m.getDeptId());
ext.put("email", m.getEmail());
ext.put("phone", m.getPhone());
return CsSelectOptionItemVo.builder()
return CsSelectOptionVo.builder()
.label(m.getNickName())
.value(m.getUsername())
.ext(ext)
@@ -0,0 +1,22 @@
package cn.odboy.system.dal.model;
import cn.odboy.base.CsObject;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class SystemProductLineVo extends CsObject {
/**
* 部门ID(select的value)
*/
private Long value;
/**
* 产品线,如"研发部/华南分部/devops组"
*/
private String label;
/**
* ID路径,如"1-2-3"
*/
private String idPath;
}
@@ -9,6 +9,7 @@ import cn.odboy.framework.exception.BadRequestException;
import cn.odboy.system.constant.SystemDataScopeEnum;
import cn.odboy.system.dal.dataobject.SystemDeptTb;
import cn.odboy.system.dal.model.SystemCreateDeptArgs;
import cn.odboy.system.dal.model.SystemProductLineVo;
import cn.odboy.system.dal.model.SystemQueryDeptArgs;
import cn.odboy.system.dal.mysql.SystemDeptMapper;
import cn.odboy.system.dal.mysql.SystemRoleMapper;
@@ -17,6 +18,7 @@ import cn.odboy.system.framework.permission.core.CsSecurityHelper;
import cn.odboy.util.ClassUtil;
import cn.odboy.util.FileUtil;
import cn.odboy.util.StringUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -25,6 +27,7 @@ import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
@Service
@@ -339,4 +342,59 @@ public class SystemDeptService {
}
return depts;
}
public List<SystemProductLineVo> queryDeptSelectDataSource() {
List<SystemDeptTb> depts = findEnabledDepts();
return buildDeptSelectOptions(depts);
}
private List<SystemProductLineVo> buildDeptSelectOptions(List<SystemDeptTb> depts) {
// 获取所有部门并按父子关系组织
Map<Long, SystemDeptTb> deptMap = depts.stream().collect(Collectors.toMap(SystemDeptTb::getId, Function.identity()));
List<SystemProductLineVo> options = new ArrayList<>();
for (SystemDeptTb dept : depts) {
// 构建部门ID路径
List<Long> pathIds = new ArrayList<>();
buildDeptIdPath(dept, deptMap, pathIds);
// 反转路径,从顶级部门到当前部门
Collections.reverse(pathIds);
SystemProductLineVo dto = new SystemProductLineVo();
dto.setValue(dept.getId());
dto.setIdPath(pathIds.stream().map(String::valueOf).collect(Collectors.joining("-")));
dto.setLabel(buildNamePath(dept, deptMap)); // 保留名称路径用于显示
options.add(dto);
}
// 按部门名称路径排序
options.sort(Comparator.comparing(SystemProductLineVo::getLabel));
return options;
}
private void buildDeptIdPath(SystemDeptTb dept, Map<Long, SystemDeptTb> deptMap, List<Long> pathIds) {
pathIds.add(dept.getId());
if (dept.getPid() != null && dept.getPid() != 0 && deptMap.containsKey(dept.getPid())) {
buildDeptIdPath(deptMap.get(dept.getPid()), deptMap, pathIds);
}
}
private String buildNamePath(SystemDeptTb dept, Map<Long, SystemDeptTb> deptMap) {
List<String> pathNames = new ArrayList<>();
buildDeptNamePath(dept, deptMap, pathNames);
Collections.reverse(pathNames);
return String.join("/", pathNames);
}
private void buildDeptNamePath(SystemDeptTb dept, Map<Long, SystemDeptTb> deptMap, List<String> pathNames) {
pathNames.add(dept.getName());
if (dept.getPid() != null && dept.getPid() != 0 && deptMap.containsKey(dept.getPid())) {
buildDeptNamePath(deptMap.get(dept.getPid()), deptMap, pathNames);
}
}
private List<SystemDeptTb> findEnabledDepts() {
return systemDeptMapper.selectList(new LambdaQueryWrapper<SystemDeptTb>().eq(SystemDeptTb::getEnabled, 1));
}
}