index.vue 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277
  1. <template>
  2. <div>
  3. <el-dialog
  4. v-model="dialog.visible.value"
  5. :title="dialog.title.value"
  6. width="1200px"
  7. append-to-body
  8. destroy-on-close
  9. >
  10. <div class="tax-code-container">
  11. <!-- 左侧树 -->
  12. <div class="tax-tree-panel">
  13. <el-tree
  14. ref="treeRef"
  15. :data="treeData"
  16. :props="treeProps"
  17. node-key="id"
  18. highlight-current
  19. :expand-on-click-node="false"
  20. @node-click="handleNodeClick"
  21. @node-dblclick="handleTreeDblClick"
  22. />
  23. </div>
  24. <!-- 右侧列表 -->
  25. <div class="tax-list-panel">
  26. <!-- 搜索框 -->
  27. <div class="search-bar">
  28. <el-input
  29. v-model="searchKeyword"
  30. placeholder="请输入名称或编码"
  31. prefix-icon="Search"
  32. clearable
  33. @keyup.enter="handleSearch"
  34. @clear="handleSearch"
  35. style="width: 280px"
  36. />
  37. <el-button icon="Search" @click="handleSearch" style="margin-left:8px" />
  38. </div>
  39. <!-- 表格 -->
  40. <el-table
  41. v-loading="loading"
  42. :data="listData"
  43. border
  44. highlight-current-row
  45. height="360px"
  46. @row-dblclick="handleRowDblClick"
  47. >
  48. <el-table-column label="编码" align="center" prop="taxationNo" min-width="110">
  49. <template #default="{ row }">
  50. <el-link type="primary" :underline="false">{{ row.taxationNo }}</el-link>
  51. </template>
  52. </el-table-column>
  53. <el-table-column label="合并编码" align="center" prop="mergeNo" min-width="130" show-overflow-tooltip>
  54. <template #default="{ row }">
  55. <el-link type="primary" :underline="false">{{ row.mergeNo }}</el-link>
  56. </template>
  57. </el-table-column>
  58. <el-table-column label="名称" align="center" prop="name" min-width="120" show-overflow-tooltip>
  59. <template #default="{ row }">
  60. <el-link type="primary" :underline="false">{{ row.name }}</el-link>
  61. </template>
  62. </el-table-column>
  63. <el-table-column label="简称" align="center" prop="abbreviation" min-width="100" show-overflow-tooltip>
  64. <template #default="{ row }">
  65. <el-link type="primary" :underline="false">{{ row.abbreviation }}</el-link>
  66. </template>
  67. </el-table-column>
  68. <!-- <el-table-column label="说明" align="center" prop="remark" min-width="150" show-overflow-tooltip />
  69. <el-table-column label="税率" align="center" prop="taxRate" width="80" />
  70. <el-table-column label="用户选择比例" align="center" prop="selectRatio" width="110" /> -->
  71. </el-table>
  72. <!-- 分页 -->
  73. <pagination
  74. v-show="total > 0"
  75. :total="total"
  76. v-model:page="queryParams.pageNum"
  77. v-model:limit="queryParams.pageSize"
  78. @pagination="getList"
  79. />
  80. </div>
  81. </div>
  82. </el-dialog>
  83. </div>
  84. </template>
  85. <script setup lang="ts">
  86. import { listTaxCode, getTaxCode, getTaxCodeTree } from '@/api/system/taxCode';
  87. import { TaxCodeVO, TaxCodeQuery } from '@/api/system/taxCode/types';
  88. import useDialog from '@/hooks/useDialog';
  89. const emit = defineEmits<{
  90. (e: 'select', row: TaxCodeVO): void;
  91. }>();
  92. const { proxy } = getCurrentInstance() as ComponentInternalInstance;
  93. const dialog = useDialog({ title: '税收编码选择 请双击选择税收编码' });
  94. const treeRef = ref<ElTreeInstance>();
  95. const treeProps = { label: 'label', children: 'children' };
  96. const currentEchoId = ref<string | number | undefined>();
  97. const treeData = ref<any[]>([]);
  98. const listData = ref<TaxCodeVO[]>([]);
  99. const loading = ref(false);
  100. const total = ref(0);
  101. const searchKeyword = ref('');
  102. const queryParams = ref<TaxCodeQuery>({
  103. pageNum: 1,
  104. pageSize: 10,
  105. parentId: undefined,
  106. name: undefined,
  107. taxationNo: undefined,
  108. params: {}
  109. });
  110. /** 加载树形数据 */
  111. const loadTreeData = async () => {
  112. try {
  113. const res = await getTaxCodeTree();
  114. treeData.value = res.data ?? [];
  115. } catch (error) {
  116. console.error('加载税收编码树失败:', error);
  117. treeData.value = [];
  118. }
  119. };
  120. /** 等待节点出现在树中并展开 */
  121. const expandNodeById = (id: string | number): Promise<void> => {
  122. return new Promise(resolve => {
  123. const tryExpand = (retry = 0) => {
  124. const node = treeRef.value?.getNode(id);
  125. if (node) {
  126. node.expand(() => resolve(), false);
  127. } else if (retry < 30) {
  128. setTimeout(() => tryExpand(retry + 1), 100);
  129. } else {
  130. resolve();
  131. }
  132. };
  133. tryExpand();
  134. });
  135. };
  136. /** 根据 id 回显:展开祖先路径并高亮目标节点 */
  137. const echoById = async (id: string | number) => {
  138. try {
  139. const res = await getTaxCode(id);
  140. const data = res.data;
  141. // ancestors 格式如 "0,100001,100002",按逗号分割并过滤掉根节点 0
  142. const ancestors = (data.ancestors ?? '')
  143. .split(',')
  144. .filter((a: string) => a && a !== '0');
  145. for (const ancId of ancestors) {
  146. await expandNodeById(ancId);
  147. }
  148. await nextTick();
  149. treeRef.value?.setCurrentKey(id);
  150. } catch (e) {
  151. console.warn('taxCode echo failed', e);
  152. }
  153. };
  154. /** 查询右侧列表 */
  155. const getList = async () => {
  156. loading.value = true;
  157. try {
  158. const res = await listTaxCode(queryParams.value);
  159. // 后端已按 isBottom='0' 过滤,仅返回无下级的叶子节点
  160. listData.value = res.rows ?? [];
  161. total.value = res.total ?? 0;
  162. } finally {
  163. loading.value = false;
  164. }
  165. };
  166. /** 单击树节点 — 刷新右侧列表 */
  167. const handleNodeClick = (node: any) => {
  168. queryParams.value.parentId = node.id;
  169. queryParams.value.pageNum = 1;
  170. getList();
  171. };
  172. /** 双击树节点 — 叶子节点直接选中 */
  173. const handleTreeDblClick = (data: any, node: any) => {
  174. if (node.isLeaf) {
  175. emit('select', data as TaxCodeVO);
  176. dialog.closeDialog();
  177. }
  178. };
  179. /** 搜索 */
  180. const handleSearch = () => {
  181. const kw = searchKeyword.value?.trim();
  182. queryParams.value.name = kw || undefined;
  183. queryParams.value.taxationNo = undefined;
  184. queryParams.value.pageNum = 1;
  185. getList();
  186. };
  187. /** 双击行选择 */
  188. const handleRowDblClick = (row: TaxCodeVO) => {
  189. if (row.isBottom !== '0') return;
  190. emit('select', row);
  191. dialog.closeDialog();
  192. };
  193. /** 对外暴露打开方法,可传入 id 用于回显 */
  194. const open = (id?: string | number) => {
  195. currentEchoId.value = id;
  196. searchKeyword.value = '';
  197. queryParams.value = {
  198. pageNum: 1,
  199. pageSize: 10,
  200. parentId: undefined,
  201. name: undefined,
  202. taxationNo: undefined,
  203. isBottom: '0',
  204. params: {}
  205. };
  206. dialog.openDialog();
  207. };
  208. watch(
  209. () => dialog.visible.value,
  210. async (val) => {
  211. if (val) {
  212. await loadTreeData();
  213. await getList();
  214. if (currentEchoId.value) {
  215. await echoById(currentEchoId.value);
  216. }
  217. } else {
  218. listData.value = [];
  219. total.value = 0;
  220. }
  221. }
  222. );
  223. defineExpose({ open, close: dialog.closeDialog });
  224. </script>
  225. <style scoped>
  226. .tax-code-container {
  227. display: flex;
  228. gap: 16px;
  229. min-height: 460px;
  230. }
  231. .tax-tree-panel {
  232. width: 300px;
  233. flex-shrink: 0;
  234. border: 1px solid var(--el-border-color);
  235. border-radius: 4px;
  236. padding: 8px 0;
  237. overflow-y: auto;
  238. max-height: 480px;
  239. }
  240. .tax-list-panel {
  241. flex: 1;
  242. display: flex;
  243. flex-direction: column;
  244. gap: 8px;
  245. min-width: 0;
  246. }
  247. .search-bar {
  248. display: flex;
  249. align-items: center;
  250. }
  251. </style>