index.vue 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278
  1. <template>
  2. <div class="auth-select-wrapper">
  3. <a-tree-select
  4. :model-value="modelValue"
  5. @update:model-value="onUpdate"
  6. :data="authOptions"
  7. tree-checked-strategy="child"
  8. :tree-checkable="multiple"
  9. :max-tag-count="2"
  10. :fieldNames="{ title: 'name', key: 'id' }"
  11. allow-search
  12. allow-clear
  13. :filter-tree-node="filterTreeNode"
  14. :treeProps="{
  15. defaultExpandedKeys: [],
  16. expandedKeys: expandedKeys,
  17. onExpand: expandTreeNode,
  18. }"
  19. placeholder="负责人"
  20. >
  21. <template #header v-if="multiple && showFooter">
  22. <div class="tree-select-header">
  23. <a-space>
  24. <a-button type="primary" size="mini" @click="handleSelectAll">
  25. 全选
  26. </a-button>
  27. <a-button type="outline" size="mini" @click="handleInvertSelect">
  28. 反选
  29. </a-button>
  30. <a-button type="dashed" size="mini" @click="handleClear">
  31. 清空
  32. </a-button>
  33. </a-space>
  34. </div>
  35. </template>
  36. </a-tree-select>
  37. </div>
  38. </template>
  39. <script setup>
  40. import { ref, onMounted, defineProps, defineEmits } from "vue";
  41. import advertCommonApi from "@/views/v1/api/advert/common";
  42. const props = defineProps({
  43. modelValue: {
  44. type: [String, Number, Array],
  45. default: "",
  46. },
  47. multiple: {
  48. type: Boolean,
  49. default: false,
  50. },
  51. showFooter: {
  52. type: Boolean,
  53. default: true,
  54. },
  55. });
  56. const expandedKeys = ref([]);
  57. const searchText = ref(""); // 存储当前搜索文本
  58. const emit = defineEmits(["update:modelValue", "change"]);
  59. const onUpdate = (val) => {
  60. emit("update:modelValue", val);
  61. emit("change", val);
  62. };
  63. const authOptions = ref([]);
  64. // 获取节点的所有祖先节点ID
  65. const getAncestorIds = (nodeId) => {
  66. const ancestorIds = [];
  67. const findNode = (nodes, targetId) => {
  68. for (const node of nodes) {
  69. if (node.id === targetId) {
  70. return node;
  71. }
  72. if (node.children && node.children.length > 0) {
  73. const found = findNode(node.children, targetId);
  74. if (found) return found;
  75. }
  76. }
  77. return null;
  78. };
  79. let currentId = nodeId;
  80. while (currentId) {
  81. const node = findNode(authOptions.value, currentId);
  82. if (node && node.parent_id) {
  83. ancestorIds.push(node.parent_id);
  84. currentId = node.parent_id;
  85. } else {
  86. break;
  87. }
  88. }
  89. return ancestorIds;
  90. };
  91. // 搜索
  92. const filterTreeNode = (inputText, node) => {
  93. // 更新搜索文本
  94. searchText.value = inputText || "";
  95. if (inputText) {
  96. const filterData =
  97. node.name.toLowerCase().indexOf(inputText.toLowerCase()) > -1;
  98. if (filterData) {
  99. // 获取所有祖先节点并展开
  100. const ancestorIds = getAncestorIds(node.id);
  101. ancestorIds.forEach((ancestorId) => {
  102. if (!expandedKeys.value.includes(ancestorId)) {
  103. expandedKeys.value.push(ancestorId);
  104. }
  105. });
  106. }
  107. return filterData;
  108. }
  109. };
  110. // 展开/收起节点
  111. const expandTreeNode = (expandKeys) => {
  112. expandedKeys.value = expandKeys;
  113. };
  114. // 获取所有子节点的ID(支持搜索过滤)
  115. const getAllChildIds = (tree, filterText = "") => {
  116. const ids = [];
  117. // 收集指定节点下的所有叶子节点ID
  118. const collectLeafIds = (nodes) => {
  119. nodes.forEach((node) => {
  120. if (node.children && node.children.length > 0) {
  121. collectLeafIds(node.children);
  122. } else if (!String(node.id).includes("dept_id_")) {
  123. ids.push(node.id);
  124. }
  125. });
  126. };
  127. const traverse = (nodes) => {
  128. nodes.forEach((node) => {
  129. // 检查节点是否匹配搜索条件
  130. const matchFilter =
  131. !filterText ||
  132. node.name.toLowerCase().indexOf(filterText.toLowerCase()) > -1;
  133. if (matchFilter) {
  134. // 如果节点匹配,收集该节点下所有叶子节点
  135. if (node.children && node.children.length > 0) {
  136. collectLeafIds(node.children);
  137. } else if (!String(node.id).includes("dept_id_")) {
  138. ids.push(node.id);
  139. }
  140. } else if (node.children && node.children.length > 0) {
  141. // 如果不匹配但有子节点,继续在子节点中搜索
  142. traverse(node.children);
  143. }
  144. });
  145. };
  146. traverse(tree);
  147. return [...new Set(ids)]; // 去重
  148. };
  149. // 全选
  150. const handleSelectAll = () => {
  151. if (!props.multiple) return;
  152. const currentSelected = Array.isArray(props.modelValue)
  153. ? props.modelValue
  154. : [];
  155. const filterIds = getAllChildIds(authOptions.value, searchText.value);
  156. if (searchText.value) {
  157. // 有搜索条件:保留原有选中项,添加搜索结果
  158. const newIds = [...new Set([...currentSelected, ...filterIds])];
  159. onUpdate(newIds);
  160. } else {
  161. // 无搜索条件:全选所有
  162. onUpdate(filterIds);
  163. }
  164. };
  165. // 反选
  166. const handleInvertSelect = () => {
  167. if (!props.multiple) return;
  168. const currentSelected = Array.isArray(props.modelValue)
  169. ? props.modelValue
  170. : [];
  171. const filterIds = getAllChildIds(authOptions.value, searchText.value);
  172. if (searchText.value) {
  173. // 有搜索条件:只在搜索结果范围内反选,保留其他已选项
  174. const otherSelected = currentSelected.filter(
  175. (id) => !filterIds.includes(id)
  176. );
  177. const invertedInFilter = filterIds.filter(
  178. (id) => !currentSelected.includes(id)
  179. );
  180. onUpdate([...otherSelected, ...invertedInFilter]);
  181. } else {
  182. // 无搜索条件:全局反选
  183. const invertedIds = filterIds.filter((id) => !currentSelected.includes(id));
  184. onUpdate(invertedIds);
  185. }
  186. };
  187. // 清空
  188. const handleClear = () => {
  189. if (!props.multiple) return;
  190. onUpdate([]);
  191. };
  192. // 获取后台归属人列表
  193. const getAuthList = async () => {
  194. const res = await advertCommonApi.getAuthOptionsApi();
  195. if (res.code == 200) {
  196. // 添加 parent_id 字段
  197. const processedData = addParentId(res.data);
  198. if (props.multiple) {
  199. authOptions.value = processedData;
  200. } else {
  201. const data = await handleData(processedData);
  202. authOptions.value = data;
  203. }
  204. }
  205. };
  206. // 添加 parent_id 字段到树形数据
  207. const addParentId = (data, parentId = null) => {
  208. return data.map((item) => {
  209. const newItem = {
  210. ...item,
  211. parent_id: parentId,
  212. };
  213. if (item.children && item.children.length > 0) {
  214. newItem.children = addParentId(item.children, item.id);
  215. }
  216. return newItem;
  217. });
  218. };
  219. // 处理数据
  220. const handleData = (data) => {
  221. return new Promise((resolve) => {
  222. for (const item of data) {
  223. if (String(item.id).includes("dept_id_")) {
  224. item.disabled = true;
  225. }
  226. if (item.children) {
  227. handleData(item.children);
  228. }
  229. }
  230. resolve(data);
  231. });
  232. };
  233. onMounted(() => {
  234. getAuthList();
  235. });
  236. </script>
  237. <style scoped>
  238. .auth-select-wrapper {
  239. width: 100%;
  240. }
  241. .tree-select-header {
  242. padding: 8px 12px;
  243. border-bottom: 1px solid var(--color-border-2);
  244. }
  245. .tree-select-header :deep(.arco-btn-text) {
  246. color: var(--color-text-2);
  247. }
  248. .tree-select-header :deep(.arco-btn-text:hover) {
  249. color: rgb(var(--primary-6));
  250. background-color: transparent;
  251. }
  252. </style>