Skip to content

Commit 23f4a7a

Browse files
authored
Merge pull request #14 from hsdt/feat/xml-viewer
feat(xml-viewer)
2 parents 682dbd6 + e574435 commit 23f4a7a

4 files changed

Lines changed: 262 additions & 0 deletions

File tree

projects/template-editor/src/components/preview/Preview.vue

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ import SimpleContextMenu from '../ContextMenu.vue';
117117
import { installContextMenuDirective } from '../../directives/context-menu';
118118
import IcdGroupItem from '../forms/IcdGroupItem.vue';
119119
import IcdList from '../forms/IcdList.vue';
120+
import XmlViewer from './XmlViewer.vue';
120121
121122
export default {
122123
name: 'Preview',
@@ -237,6 +238,7 @@ export default {
237238
.component('Checkbox', Checkbox)
238239
.component('DatePicker', DatePicker)
239240
.component('Paint', Paint)
241+
.component('XmlViewer', XmlViewer)
240242
.component('ContextMenu', SimpleContextMenu)
241243
.component('ImContextMenu', ImContextMenu)
242244
.component('ImContextMenuItem', ImContextMenuItem)

projects/template-editor/src/components/preview/PreviewWrapper.vue

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,9 @@ export default {
9393
<Checkbox
9494
size="md" v-model="hsBenhAn.NguyenNhanTuVong" value="DO_BENH" />
9595
</PageA4>
96+
<PageA4 style="padding:5mm">
97+
<XmlViewer url="http://localhost:5000/Content/Upload/2026/04/13/BA26000003_du_lieu.xml" />
98+
</PageA4>
9699
`,
97100
context: {
98101
moment: moment,
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
<template>
2+
<ul class="tree-view">
3+
<template v-for="(value, key) in data">
4+
<li v-if="key === '#text'" :key="key + '-text'">
5+
<span
6+
v-if="typeof value === 'string' && value.length > 80"
7+
class="tree-value-block"
8+
>{{ value }}</span
9+
>
10+
<span v-else class="tree-value">{{ value }}</span>
11+
</li>
12+
<template v-else-if="Array.isArray(value)">
13+
<li v-for="(item, idx) in value" :key="key + '-' + idx">
14+
<span @click="toggle(key + '-' + idx)" class="tree-key">
15+
<span class="arrow" :class="{ open: isOpen(key + '-' + idx) }"
16+
>▶</span
17+
>
18+
&lt;{{ key }}&gt;
19+
</span>
20+
<template v-if="isOpen(key + '-' + idx)">
21+
<TreeView :data="item" />
22+
<span class="tree-key tree-close">&lt;/{{ key }}&gt;</span>
23+
</template>
24+
<template v-else>
25+
<div class="tree-collapsed">
26+
...<br />
27+
<span class="tree-key tree-close">&lt;/{{ key }}&gt;</span>
28+
</div>
29+
</template>
30+
</li>
31+
</template>
32+
<li v-else-if="isObject(value)" :key="key + '-obj'">
33+
<template v-if="Object.keys(value).length === 0">
34+
<span class="tree-key">&lt;{{ key }}/&gt;</span>
35+
</template>
36+
<template v-else-if="Object.keys(value).length === 1 && value['#text']">
37+
<span class="tree-key">&lt;{{ key }}&gt;</span>
38+
<span class="tree-value">{{ value['#text'] }}</span>
39+
<span class="tree-key tree-close">&lt;/{{ key }}&gt;</span>
40+
</template>
41+
<template v-else>
42+
<span @click="toggle(key)" class="tree-key">
43+
<span class="arrow" :class="{ open: isOpen(key) }">▶</span>
44+
&lt;{{ key }}&gt;
45+
</span>
46+
<template v-if="isOpen(key)">
47+
<TreeView :data="value" />
48+
<span class="tree-key tree-close">&lt;/{{ key }}&gt;</span>
49+
</template>
50+
<template v-else>
51+
<div class="tree-collapsed">
52+
...<br />
53+
<span class="tree-key tree-close">&lt;/{{ key }}&gt;</span>
54+
</div>
55+
</template>
56+
</template>
57+
</li>
58+
<li v-else :key="key + '-val'">
59+
<span class="tree-key">&lt;{{ key }}&gt;:</span>
60+
<span
61+
v-if="typeof value === 'string' && value.length > 80"
62+
class="tree-value-block"
63+
>{{ value }}</span
64+
>
65+
<span v-else class="tree-value">{{ value }}</span>
66+
</li>
67+
</template>
68+
</ul>
69+
</template>
70+
71+
<script lang="ts">
72+
import { defineComponent, ref } from "vue";
73+
74+
export default defineComponent({
75+
name: "TreeView",
76+
props: {
77+
data: { type: [Object, Array], required: true },
78+
},
79+
setup(props) {
80+
const openKeys = ref<Record<string, boolean>>({});
81+
const isObject = (val: any) =>
82+
val && typeof val === "object" && !Array.isArray(val);
83+
const isOpen = (key: string | number) => openKeys.value[String(key)];
84+
const toggle = (key: string | number) => {
85+
const k = String(key);
86+
openKeys.value[k] = !openKeys.value[k];
87+
};
88+
return { isObject, isOpen, toggle };
89+
},
90+
});
91+
</script>
92+
93+
<style scoped>
94+
.tree-view {
95+
list-style: none;
96+
padding-left: 18px;
97+
font-family: "Fira Mono", "Consolas", "Menlo", "Monaco", monospace;
98+
font-size: 16px;
99+
}
100+
.tree-key {
101+
cursor: pointer;
102+
color: #8e24aa;
103+
user-select: none;
104+
font-weight: 500;
105+
transition: color 0.2s;
106+
}
107+
.tree-value {
108+
color: #333;
109+
}
110+
.arrow {
111+
display: inline-block;
112+
width: 1em;
113+
color: #888;
114+
transition:
115+
transform 0.2s,
116+
color 0.2s;
117+
vertical-align: middle;
118+
margin-right: 2px;
119+
font-size: 1em;
120+
}
121+
.arrow.open {
122+
transform: rotate(90deg);
123+
color: #8e24aa;
124+
}
125+
.tree-key {
126+
display: inline-flex;
127+
align-items: center;
128+
}
129+
.tree-value-block {
130+
display: block;
131+
background: #f8f8f8;
132+
color: #444;
133+
border-radius: 4px;
134+
padding: 8px;
135+
margin: 4px 0 4px 16px;
136+
font-family: inherit;
137+
font-size: 15px;
138+
white-space: pre-wrap;
139+
overflow-x: auto;
140+
max-width: 100%;
141+
}
142+
</style>
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
<template>
2+
<div v-if="loading">Đang tải...</div>
3+
<div v-else class="xml-viewer-container">
4+
<TreeView v-if="parsedObject" :data="parsedObject" />
5+
</div>
6+
</template>
7+
8+
<style scoped>
9+
.xml-viewer-container {
10+
overflow-y: auto;
11+
max-height: 100%;
12+
border-radius: 6px;
13+
font-size: 15px;
14+
height: 100%;
15+
box-sizing: border-box;
16+
scrollbar-width: thin;
17+
width: 100%;
18+
max-width: 100%;
19+
}
20+
</style>
21+
22+
23+
24+
<script lang="ts">
25+
import { defineComponent, ref, onMounted } from 'vue';
26+
import TreeView from './TreeView.vue';
27+
28+
export default defineComponent({
29+
name: 'XmlViewer',
30+
components: { TreeView },
31+
props: {
32+
url: { type: String, required: true }
33+
},
34+
setup(props) {
35+
const xmlContent = ref('');
36+
const parsedObject = ref<any>(null);
37+
const loading = ref(true);
38+
const error = ref('');
39+
40+
41+
42+
function domToObj(node: Node): any {
43+
// Nếu là text node
44+
if (node.nodeType === 3) {
45+
const text = node.nodeValue?.trim();
46+
return text ? text : undefined;
47+
}
48+
// Nếu là element node
49+
if (node.nodeType === 1) {
50+
const obj: any = {};
51+
const el = node as Element;
52+
// Thuộc tính
53+
if (el.attributes && el.attributes.length > 0) {
54+
obj['@attributes'] = {};
55+
for (let attr of Array.from(el.attributes)) {
56+
obj['@attributes'][attr.name] = attr.value;
57+
}
58+
}
59+
// Child nodes
60+
for (let child of Array.from(node.childNodes)) {
61+
const childObj = domToObj(child);
62+
if (childObj === undefined) continue;
63+
if (child.nodeType === 3) {
64+
// text node
65+
if (!obj['#text']) obj['#text'] = '';
66+
obj['#text'] += childObj;
67+
} else if (child.nodeType === 1) {
68+
// element node
69+
const childEl = child as Element;
70+
const tag = childEl.tagName;
71+
if (!obj[tag]) obj[tag] = [];
72+
obj[tag].push(childObj);
73+
}
74+
}
75+
// Nếu chỉ có 1 phần tử trong mảng thì trả về object thay vì array
76+
for (let key in obj) {
77+
if (Array.isArray(obj[key]) && obj[key].length === 1) {
78+
obj[key] = obj[key][0];
79+
}
80+
}
81+
return obj;
82+
}
83+
return undefined;
84+
}
85+
86+
const fetchXml = async () => {
87+
loading.value = true;
88+
error.value = '';
89+
try {
90+
const res = await fetch(props.url, {
91+
headers: {
92+
'Content-Type': 'text/xml'
93+
}
94+
});
95+
if (!res.ok) throw new Error('Không thể tải XML');
96+
xmlContent.value = await res.text();
97+
const parser = new DOMParser();
98+
const xmlDoc = parser.parseFromString(xmlContent.value, 'text/xml');
99+
// Lấy node gốc
100+
const root = xmlDoc.documentElement;
101+
parsedObject.value = { [root.tagName]: domToObj(root) };
102+
} catch (e: any) {
103+
error.value = e.message || 'Lỗi không xác định';
104+
} finally {
105+
loading.value = false;
106+
}
107+
};
108+
109+
onMounted(fetchXml);
110+
111+
112+
return { xmlContent, parsedObject, loading, error };
113+
}
114+
});
115+
</script>

0 commit comments

Comments
 (0)