index.vue 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314
  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-class-name="rowClassName"
  47. @row-dblclick="handleRowDblClick"
  48. >
  49. <el-table-column label="编码" align="center" prop="taxationNo" min-width="110">
  50. <template #default="{ row }">
  51. <el-link :type="isRowDisabled(row) ? 'info' : 'primary'" :disabled="isRowDisabled(row)" :underline="false">{{ row.taxationNo }}</el-link>
  52. </template>
  53. </el-table-column>
  54. <el-table-column label="合并编码" align="center" prop="mergeNo" min-width="130" show-overflow-tooltip>
  55. <template #default="{ row }">
  56. <el-link :type="isRowDisabled(row) ? 'info' : 'primary'" :disabled="isRowDisabled(row)" :underline="false">{{ row.mergeNo }}</el-link>
  57. </template>
  58. </el-table-column>
  59. <el-table-column label="名称" align="center" prop="name" min-width="120" show-overflow-tooltip>
  60. <template #default="{ row }">
  61. <el-link :type="isRowDisabled(row) ? 'info' : 'primary'" :disabled="isRowDisabled(row)" :underline="false">{{ row.name }}</el-link>
  62. </template>
  63. </el-table-column>
  64. <el-table-column label="简称" align="center" prop="abbreviation" min-width="100" show-overflow-tooltip>
  65. <template #default="{ row }">
  66. <el-link :type="isRowDisabled(row) ? 'info' : 'primary'" :disabled="isRowDisabled(row)" :underline="false">{{ row.abbreviation }}</el-link>
  67. </template>
  68. </el-table-column>
  69. <el-table-column label="说明" align="center" prop="remark" min-width="150" show-overflow-tooltip>
  70. <template #default="{ row }">
  71. <el-link :type="isRowDisabled(row) ? 'info' : 'primary'" :disabled="isRowDisabled(row)" :underline="false">{{ row.remark }}</el-link>
  72. </template>
  73. </el-table-column>
  74. <el-table-column label="税率" align="center" prop="explain" width="90">
  75. <template #default="{ row }">
  76. <el-link :type="isRowDisabled(row) ? 'info' : 'primary'" :disabled="isRowDisabled(row)" :underline="false">{{ formatTaxRate(row.taxrate) }}</el-link>
  77. </template>
  78. </el-table-column>
  79. </el-table>
  80. <!-- 分页 -->
  81. <pagination
  82. v-show="total > 0"
  83. :total="total"
  84. v-model:page="queryParams.pageNum"
  85. v-model:limit="queryParams.pageSize"
  86. @pagination="getList"
  87. />
  88. </div>
  89. </div>
  90. </el-dialog>
  91. </div>
  92. </template>
  93. <script setup lang="ts">
  94. import { listTaxCode, getTaxCode, getTaxCodeTree } from '@/api/system/taxCode';
  95. import { TaxCodeVO, TaxCodeQuery } from '@/api/system/taxCode/types';
  96. import useDialog from '@/hooks/useDialog';
  97. const emit = defineEmits<{
  98. (e: 'select', row: TaxCodeVO): void;
  99. }>();
  100. const { proxy } = getCurrentInstance() as ComponentInternalInstance;
  101. const dialog = useDialog({ title: '税收编码选择 请双击选择税收编码' });
  102. const treeRef = ref<ElTreeInstance>();
  103. const treeProps = { label: 'label', children: 'children' };
  104. const currentEchoId = ref<string | number | undefined>();
  105. const treeData = ref<any[]>([]);
  106. const listData = ref<TaxCodeVO[]>([]);
  107. const loading = ref(false);
  108. const total = ref(0);
  109. const searchKeyword = ref('');
  110. const queryParams = ref<TaxCodeQuery>({
  111. pageNum: 1,
  112. pageSize: 10,
  113. parentId: undefined,
  114. name: undefined,
  115. taxationNo: undefined,
  116. params: {}
  117. });
  118. /** 加载树形数据 */
  119. const loadTreeData = async () => {
  120. try {
  121. const res = await getTaxCodeTree();
  122. treeData.value = res.data ?? [];
  123. } catch (error) {
  124. console.error('加载税收编码树失败:', error);
  125. treeData.value = [];
  126. }
  127. };
  128. /** 等待节点出现在树中并展开 */
  129. const expandNodeById = (id: string | number): Promise<void> => {
  130. return new Promise(resolve => {
  131. const tryExpand = (retry = 0) => {
  132. const node = treeRef.value?.getNode(id);
  133. if (node) {
  134. node.expand(() => resolve(), false);
  135. } else if (retry < 30) {
  136. setTimeout(() => tryExpand(retry + 1), 100);
  137. } else {
  138. resolve();
  139. }
  140. };
  141. tryExpand();
  142. });
  143. };
  144. /** 根据 id 回显:展开祖先路径并高亮目标节点 */
  145. const echoById = async (id: string | number) => {
  146. try {
  147. const res = await getTaxCode(id);
  148. const data = res.data;
  149. // ancestors 格式如 "0,100001,100002",按逗号分割并过滤掉根节点 0
  150. const ancestors = (data.ancestors ?? '')
  151. .split(',')
  152. .filter((a: string) => a && a !== '0');
  153. for (const ancId of ancestors) {
  154. await expandNodeById(ancId);
  155. }
  156. await nextTick();
  157. treeRef.value?.setCurrentKey(id);
  158. } catch (e) {
  159. console.warn('taxCode echo failed', e);
  160. }
  161. };
  162. /** 查询右侧列表 */
  163. const getList = async () => {
  164. loading.value = true;
  165. try {
  166. const res = await listTaxCode(queryParams.value);
  167. listData.value = res.rows ?? [];
  168. total.value = res.total ?? 0;
  169. } finally {
  170. loading.value = false;
  171. }
  172. };
  173. /** 单击树节点 — 刷新右侧列表 */
  174. const handleNodeClick = (node: any) => {
  175. queryParams.value.parentId = node.id;
  176. queryParams.value.pageNum = 1;
  177. getList();
  178. };
  179. /** 双击树节点 — 叶子节点直接选中 */
  180. const handleTreeDblClick = (data: any, node: any) => {
  181. if (node.isLeaf) {
  182. emit('select', data as TaxCodeVO);
  183. dialog.closeDialog();
  184. }
  185. };
  186. /** 搜索 */
  187. const handleSearch = () => {
  188. const kw = searchKeyword.value?.trim();
  189. queryParams.value.name = kw || undefined;
  190. queryParams.value.taxationNo = undefined;
  191. queryParams.value.pageNum = 1;
  192. getList();
  193. };
  194. /** 格式化税率为百分制显示 */
  195. const formatTaxRate = (val: any): string => {
  196. if (val === null || val === undefined || val === '') return '';
  197. const num = Number(val);
  198. if (isNaN(num)) return String(val);
  199. return `${Math.round(num * 100)}%`;
  200. };
  201. /** 判断行是否禁用:非叶子节点(isBottom === 0)不可选 */
  202. const isRowDisabled = (row: TaxCodeVO): boolean => {
  203. return Number((row as any)?.isBottom) === 0;
  204. };
  205. /** 行样式类名:禁用行显示为灰色 */
  206. const rowClassName = ({ row }: { row: TaxCodeVO }): string => {
  207. return isRowDisabled(row) ? 'tax-code-row-disabled' : '';
  208. };
  209. /** 双击行选择 */
  210. const handleRowDblClick = (row: TaxCodeVO) => {
  211. if (isRowDisabled(row)) {
  212. proxy?.$modal.msgWarning('该节点不是叶子节点,不可选择');
  213. return;
  214. }
  215. emit('select', row);
  216. dialog.closeDialog();
  217. };
  218. /** 对外暴露打开方法,可传入 id 用于回显 */
  219. const open = (id?: string | number) => {
  220. currentEchoId.value = id;
  221. searchKeyword.value = '';
  222. queryParams.value = {
  223. pageNum: 1,
  224. pageSize: 10,
  225. parentId: undefined,
  226. name: undefined,
  227. taxationNo: undefined,
  228. params: {}
  229. };
  230. dialog.openDialog();
  231. };
  232. watch(
  233. () => dialog.visible.value,
  234. async (val) => {
  235. if (val) {
  236. await loadTreeData();
  237. await getList();
  238. if (currentEchoId.value) {
  239. await echoById(currentEchoId.value);
  240. }
  241. } else {
  242. listData.value = [];
  243. total.value = 0;
  244. }
  245. }
  246. );
  247. defineExpose({ open, close: dialog.closeDialog });
  248. </script>
  249. <style scoped>
  250. .tax-code-container {
  251. display: flex;
  252. gap: 16px;
  253. min-height: 460px;
  254. }
  255. .tax-tree-panel {
  256. width: 300px;
  257. flex-shrink: 0;
  258. border: 1px solid var(--el-border-color);
  259. border-radius: 4px;
  260. padding: 8px 0;
  261. overflow-y: auto;
  262. max-height: 480px;
  263. }
  264. .tax-list-panel {
  265. flex: 1;
  266. display: flex;
  267. flex-direction: column;
  268. gap: 8px;
  269. min-width: 0;
  270. }
  271. .search-bar {
  272. display: flex;
  273. align-items: center;
  274. }
  275. :deep(.tax-code-row-disabled) {
  276. background-color: var(--el-fill-color-light) !important;
  277. color: var(--el-text-color-disabled);
  278. cursor: not-allowed;
  279. }
  280. :deep(.tax-code-row-disabled:hover > td.el-table__cell) {
  281. background-color: var(--el-fill-color-light) !important;
  282. }
  283. </style>