Skip to content

Commit 8a4f0b2

Browse files
authored
Merge pull request #781 from wjh-1024/QRCodeAndMobile
Add QR code scanning, mobile detail page and batch QR export to Excel
2 parents 71608a4 + 1bcdae7 commit 8a4f0b2

16 files changed

Lines changed: 1225 additions & 17 deletions

File tree

cmdb-api/api/lib/perm/acl/audit.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -389,7 +389,7 @@ def add_login_log(cls, username, is_ok, description, _id=None, logout_at=None, i
389389
logout_at=logout_at,
390390
ip=(ip or request.headers.get('X-Forwarded-For') or
391391
request.headers.get('X-Real-IP') or request.remote_addr or '').split(',')[0],
392-
browser=browser or request.headers.get('User-Agent'),
392+
browser=(browser or request.headers.get('User-Agent') or '')[:255],
393393
channel=request.values.get('channel', 'web'),
394394
)
395395

cmdb-api/api/views/cmdb/ci.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,3 +276,55 @@ def post(self, ci_id):
276276
return self.jsonify(**CIManager().rollback(ci_id, before_date))
277277

278278
return self.get(ci_id)
279+
280+
281+
class CIMobileDetailView(APIView):
282+
url_prefix = "/ci/<int:ci_id>/mobile"
283+
284+
def get(self, ci_id):
285+
ci = CIManager.get_ci_by_id_from_db(ci_id, ret_key=RetKey.NAME, fields=None, valid=True)
286+
287+
ci_type = CITypeCache.get(ci.get("_type", 0)) if ci.get("_type") else None
288+
type_info = {"id": ci_type.id, "name": ci_type.name, "alias": ci_type.alias} if ci_type else {}
289+
290+
attribute_alias_map = {}
291+
if ci_type:
292+
from api.lib.cmdb.ci_type import CITypeAttributeManager
293+
attrs = CITypeAttributeManager.get_attr_names_by_type_id(ci_type.id)
294+
if attrs:
295+
from api.lib.cmdb.cache import AttributeCache
296+
for attr_name in attrs:
297+
attr_obj = AttributeCache.get(attr_name)
298+
if attr_obj:
299+
attribute_alias_map[attr_obj.name] = attr_obj.alias or attr_obj.name
300+
301+
relations = {"parents": [], "children": []}
302+
try:
303+
from api.lib.cmdb.ci import CIRelationManager
304+
children = CIRelationManager.get_children(ci_id, ret_key=RetKey.NAME)
305+
for type_name, cis in children.items():
306+
for c in cis:
307+
c["_type_name"] = CITypeCache.get(type_name).alias if type_name else type_name
308+
relations["children"].append(c)
309+
310+
parent_ids = CIRelationManager.get_parent_ids([ci_id])
311+
if ci_id in parent_ids:
312+
for p_id, p_type_id in parent_ids[ci_id]:
313+
parent_ci = CIManager.get_cis_by_ids([str(p_id)], ret_key=RetKey.NAME)
314+
if parent_ci:
315+
for p in parent_ci:
316+
p_type = CITypeCache.get(p_type_id) if p_type_id else None
317+
p["_type_name"] = p_type.alias if p_type else ""
318+
relations["parents"].append(p)
319+
except Exception:
320+
pass
321+
322+
from api.lib.cmdb.history import AttributeHistoryManger
323+
try:
324+
history = AttributeHistoryManger.get_by_ci_id(ci_id)
325+
history = history[:10]
326+
except Exception:
327+
history = []
328+
329+
return self.jsonify(ci=ci, type=type_info, relations=relations, history=history,
330+
attribute_alias_map=attribute_alias_map)

cmdb-ui/jsconfig.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,8 @@
77
}
88
},
99
"exclude": ["node_modules", "dist"],
10-
"include": ["src/*"]
10+
"include": ["src/*"],
11+
"typeAcquisition": {
12+
"enable": false
13+
}
1114
}

cmdb-ui/package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"axios": "0.18.0",
2424
"babel-eslint": "^8.2.2",
2525
"butterfly-dag": "^4.3.26",
26+
"codemirror": "^5.65.13",
2627
"core-js": "^3.31.0",
2728
"echarts": "^5.3.2",
2829
"element-ui": "^2.15.10",
@@ -37,18 +38,17 @@
3738
"lodash.pick": "^4.4.0",
3839
"md5": "^2.2.1",
3940
"moment": "^2.24.0",
40-
"monaco-editor": "^0.28.1",
41-
"monaco-editor-webpack-plugin": "^4.2.0",
42-
"monaco-vim": "^0.4.4",
4341
"nprogress": "^0.2.0",
42+
"qrcode": "^1.5.4",
4443
"relation-graph": "^2.1.42",
4544
"snabbdom": "^3.5.1",
4645
"sortablejs": "1.9.0",
4746
"style-resources-loader": "^1.5.0",
4847
"viser-vue": "^2.4.8",
4948
"vue": "2.6.11",
50-
"vue-clipboard2": "^0.3.3",
5149
"vue-cli-plugin-style-resources-loader": "^0.1.5",
50+
"vue-clipboard2": "^0.3.3",
51+
"vue-codemirror": "^4.0.6",
5252
"vue-cropper": "^0.6.2",
5353
"vue-grid-layout": "2.3.12",
5454
"vue-i18n": "8.28.2",

cmdb-ui/src/modules/cmdb/api/ci.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,14 @@ export function getCIById(ciId) {
5252
})
5353
}
5454

55+
// 获取移动端CI详情
56+
export function getCIMobileDetail(ciId) {
57+
return axios({
58+
url: urlPrefix + `/ci/${ciId}/mobile`,
59+
method: 'GET'
60+
})
61+
}
62+
5563
// 获取自动发现占比
5664
export function getCIAdcStatistics() {
5765
return axios({
Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
<template>
2+
<a-modal
3+
v-model="visible"
4+
:title="$t('cmdb.ci.qrcodeBatchTitle')"
5+
width="800px"
6+
:footer="null"
7+
:maskClosable="true"
8+
>
9+
<p class="qrcode-batch-tip">{{ $t('cmdb.ci.qrcodeBatchTip') }}</p>
10+
11+
<div v-if="qrcodeList.length === 0 && !generating" class="qrcode-batch-empty">
12+
<a-empty :description="$t('cmdb.ci.qrcodeBatchEmpty')" />
13+
</div>
14+
15+
<div v-if="generating" class="qrcode-batch-generating">
16+
<a-spin />
17+
<span>正在生成 {{ generatedCount }} / {{ totalCount }} ...</span>
18+
</div>
19+
20+
<div v-if="qrcodeList.length" class="qrcode-batch-grid" ref="qrcodeGrid">
21+
<div
22+
v-for="item in qrcodeList"
23+
:key="item.ciId"
24+
class="qrcode-batch-item"
25+
>
26+
<canvas :ref="'qrcode-' + item.ciId"></canvas>
27+
<p class="qrcode-batch-item-label">{{ item.label }}</p>
28+
<p class="qrcode-batch-item-id">CI ID: {{ item.ciId }}</p>
29+
</div>
30+
</div>
31+
32+
<div class="qrcode-batch-actions" v-if="qrcodeList.length">
33+
<a-button type="primary" @click="downloadAll">
34+
<a-icon type="download" /> {{ $t('cmdb.ci.qrcodeDownload') }}
35+
</a-button>
36+
<a-button @click="printAll">
37+
<a-icon type="printer" /> {{ $t('cmdb.ci.printQRCode') }}
38+
</a-button>
39+
</div>
40+
</a-modal>
41+
</template>
42+
43+
<script>
44+
import QRCode from 'qrcode'
45+
46+
export default {
47+
name: 'QRCodeBatchExport',
48+
data() {
49+
return {
50+
visible: false,
51+
ciList: [],
52+
qrcodeList: [],
53+
generating: false,
54+
generatedCount: 0,
55+
totalCount: 0
56+
}
57+
},
58+
methods: {
59+
open(ciList) {
60+
if (!ciList || !ciList.length) {
61+
this.$message.warning(this.$t('cmdb.ci.qrcodeBatchEmpty'))
62+
return
63+
}
64+
this.ciList = ciList
65+
this.qrcodeList = []
66+
this.visible = true
67+
this.$nextTick(() => {
68+
this.generateAll()
69+
})
70+
},
71+
async generateAll() {
72+
this.generating = true
73+
this.generatedCount = 0
74+
this.totalCount = this.ciList.length
75+
76+
const qrcodeList = []
77+
for (const ci of this.ciList) {
78+
const mobileUrl = `${window.location.origin}/cmdb/mobile/${ci.typeId}/${ci.ciId}`
79+
qrcodeList.push({
80+
ciId: ci.ciId,
81+
typeId: ci.typeId,
82+
label: ci.label || ci.name || `CI ${ci.ciId}`,
83+
url: mobileUrl
84+
})
85+
}
86+
87+
this.qrcodeList = qrcodeList
88+
await this.$nextTick()
89+
90+
for (const item of this.qrcodeList) {
91+
const canvasRef = this.$refs['qrcode-' + item.ciId]
92+
const canvas = Array.isArray(canvasRef) ? canvasRef[0] : canvasRef
93+
if (canvas) {
94+
try {
95+
await QRCode.toCanvas(canvas, item.url, {
96+
width: 150,
97+
margin: 1,
98+
color: { dark: '#000000', light: '#ffffff' }
99+
})
100+
} catch (e) {
101+
console.error('QRCode generate failed for CI', item.ciId, e)
102+
}
103+
}
104+
this.generatedCount++
105+
}
106+
107+
this.generating = false
108+
},
109+
downloadAll() {
110+
const grid = this.$refs.qrcodeGrid
111+
if (!grid) return
112+
113+
const link = document.createElement('a')
114+
link.download = 'cmdb-qrcodes-batch.png'
115+
116+
import('html2canvas').then(({ default: html2canvas }) => {
117+
html2canvas(grid, {
118+
backgroundColor: '#ffffff',
119+
scale: 2
120+
}).then((canvas) => {
121+
link.href = canvas.toDataURL('image/png')
122+
link.click()
123+
})
124+
}).catch(() => {
125+
this.$message.warning(this.$t('cmdb.ci.copyFailed'))
126+
})
127+
},
128+
printAll() {
129+
const grid = this.$refs.qrcodeGrid
130+
if (!grid) return
131+
132+
const printWindow = window.open('', '_blank', 'width=800,height=600')
133+
if (!printWindow) {
134+
this.$message.warning(this.$t('cmdb.ci.copyFailed'))
135+
return
136+
}
137+
138+
const content = grid.innerHTML
139+
printWindow.document.write(`
140+
<html>
141+
<head>
142+
<title>CMDB QR Codes</title>
143+
<style>
144+
body { font-family: Arial, sans-serif; padding: 20px; }
145+
.qrcode-batch-grid { display: flex; flex-wrap: wrap; gap: 20px; justify-content: center; }
146+
.qrcode-batch-item { text-align: center; width: 170px; }
147+
.qrcode-batch-item-label { font-size: 12px; margin: 4px 0 2px; word-break: break-all; }
148+
.qrcode-batch-item-id { font-size: 11px; color: #999; margin: 0; }
149+
@media print {
150+
.qrcode-batch-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; }
151+
.qrcode-batch-item { page-break-inside: avoid; }
152+
}
153+
</style>
154+
</head>
155+
<body>
156+
<div class="qrcode-batch-grid">${content}</div>
157+
</body>
158+
</html>
159+
`)
160+
printWindow.document.close()
161+
setTimeout(() => {
162+
printWindow.print()
163+
printWindow.close()
164+
}, 500)
165+
}
166+
}
167+
}
168+
</script>
169+
170+
<style lang="less" scoped>
171+
.qrcode-batch-tip {
172+
color: rgba(0, 0, 0, 0.45);
173+
font-size: 13px;
174+
margin-bottom: 16px;
175+
}
176+
177+
.qrcode-batch-empty {
178+
padding: 20px 0;
179+
}
180+
181+
.qrcode-batch-generating {
182+
display: flex;
183+
align-items: center;
184+
justify-content: center;
185+
gap: 12px;
186+
padding: 40px 0;
187+
color: #999;
188+
}
189+
190+
.qrcode-batch-grid {
191+
display: flex;
192+
flex-wrap: wrap;
193+
gap: 16px;
194+
justify-content: center;
195+
padding: 8px 0;
196+
}
197+
198+
.qrcode-batch-item {
199+
text-align: center;
200+
width: 160px;
201+
padding: 12px 8px;
202+
border: 1px solid #f0f0f0;
203+
border-radius: 8px;
204+
background: #fff;
205+
}
206+
207+
.qrcode-batch-item-label {
208+
font-size: 12px;
209+
color: #333;
210+
margin: 6px 0 2px;
211+
word-break: break-all;
212+
max-width: 140px;
213+
overflow: hidden;
214+
text-overflow: ellipsis;
215+
white-space: nowrap;
216+
}
217+
218+
.qrcode-batch-item-id {
219+
font-size: 11px;
220+
color: #bbb;
221+
margin: 0;
222+
}
223+
224+
.qrcode-batch-actions {
225+
display: flex;
226+
justify-content: center;
227+
gap: 12px;
228+
margin-top: 16px;
229+
padding-top: 16px;
230+
border-top: 1px solid #f0f0f0;
231+
}
232+
</style>

0 commit comments

Comments
 (0)