Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cmdb-api/api/lib/perm/acl/audit.py
Original file line number Diff line number Diff line change
Expand Up @@ -389,7 +389,7 @@ def add_login_log(cls, username, is_ok, description, _id=None, logout_at=None, i
logout_at=logout_at,
ip=(ip or request.headers.get('X-Forwarded-For') or
request.headers.get('X-Real-IP') or request.remote_addr or '').split(',')[0],
browser=browser or request.headers.get('User-Agent'),
browser=(browser or request.headers.get('User-Agent') or '')[:255],
channel=request.values.get('channel', 'web'),
)

Expand Down
52 changes: 52 additions & 0 deletions cmdb-api/api/views/cmdb/ci.py
Original file line number Diff line number Diff line change
Expand Up @@ -276,3 +276,55 @@ def post(self, ci_id):
return self.jsonify(**CIManager().rollback(ci_id, before_date))

return self.get(ci_id)


class CIMobileDetailView(APIView):
url_prefix = "/ci/<int:ci_id>/mobile"

def get(self, ci_id):
ci = CIManager.get_ci_by_id_from_db(ci_id, ret_key=RetKey.NAME, fields=None, valid=True)

ci_type = CITypeCache.get(ci.get("_type", 0)) if ci.get("_type") else None
type_info = {"id": ci_type.id, "name": ci_type.name, "alias": ci_type.alias} if ci_type else {}

attribute_alias_map = {}
if ci_type:
from api.lib.cmdb.ci_type import CITypeAttributeManager
attrs = CITypeAttributeManager.get_attr_names_by_type_id(ci_type.id)
if attrs:
from api.lib.cmdb.cache import AttributeCache
for attr_name in attrs:
attr_obj = AttributeCache.get(attr_name)
if attr_obj:
attribute_alias_map[attr_obj.name] = attr_obj.alias or attr_obj.name

relations = {"parents": [], "children": []}
try:
from api.lib.cmdb.ci import CIRelationManager
children = CIRelationManager.get_children(ci_id, ret_key=RetKey.NAME)
for type_name, cis in children.items():
for c in cis:
c["_type_name"] = CITypeCache.get(type_name).alias if type_name else type_name
relations["children"].append(c)

parent_ids = CIRelationManager.get_parent_ids([ci_id])
if ci_id in parent_ids:
for p_id, p_type_id in parent_ids[ci_id]:
parent_ci = CIManager.get_cis_by_ids([str(p_id)], ret_key=RetKey.NAME)
if parent_ci:
for p in parent_ci:
p_type = CITypeCache.get(p_type_id) if p_type_id else None
p["_type_name"] = p_type.alias if p_type else ""
relations["parents"].append(p)
except Exception:
pass

from api.lib.cmdb.history import AttributeHistoryManger
try:
history = AttributeHistoryManger.get_by_ci_id(ci_id)
history = history[:10]
except Exception:
history = []

return self.jsonify(ci=ci, type=type_info, relations=relations, history=history,
attribute_alias_map=attribute_alias_map)
5 changes: 4 additions & 1 deletion cmdb-ui/jsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,8 @@
}
},
"exclude": ["node_modules", "dist"],
"include": ["src/*"]
"include": ["src/*"],
"typeAcquisition": {
"enable": false
}
}
8 changes: 4 additions & 4 deletions cmdb-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"axios": "0.18.0",
"babel-eslint": "^8.2.2",
"butterfly-dag": "^4.3.26",
"codemirror": "^5.65.13",
"core-js": "^3.31.0",
"echarts": "^5.3.2",
"element-ui": "^2.15.10",
Expand All @@ -37,18 +38,17 @@
"lodash.pick": "^4.4.0",
"md5": "^2.2.1",
"moment": "^2.24.0",
"monaco-editor": "^0.28.1",
"monaco-editor-webpack-plugin": "^4.2.0",
"monaco-vim": "^0.4.4",
"nprogress": "^0.2.0",
"qrcode": "^1.5.4",
"relation-graph": "^2.1.42",
"snabbdom": "^3.5.1",
"sortablejs": "1.9.0",
"style-resources-loader": "^1.5.0",
"viser-vue": "^2.4.8",
"vue": "2.6.11",
"vue-clipboard2": "^0.3.3",
"vue-cli-plugin-style-resources-loader": "^0.1.5",
"vue-clipboard2": "^0.3.3",
"vue-codemirror": "^4.0.6",
"vue-cropper": "^0.6.2",
"vue-grid-layout": "2.3.12",
"vue-i18n": "8.28.2",
Expand Down
8 changes: 8 additions & 0 deletions cmdb-ui/src/modules/cmdb/api/ci.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,14 @@ export function getCIById(ciId) {
})
}

// 获取移动端CI详情
export function getCIMobileDetail(ciId) {
return axios({
url: urlPrefix + `/ci/${ciId}/mobile`,
method: 'GET'
})
}

// 获取自动发现占比
export function getCIAdcStatistics() {
return axios({
Expand Down
232 changes: 232 additions & 0 deletions cmdb-ui/src/modules/cmdb/components/QRCodeBatchExport.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
<template>
<a-modal
v-model="visible"
:title="$t('cmdb.ci.qrcodeBatchTitle')"
width="800px"
:footer="null"
:maskClosable="true"
>
<p class="qrcode-batch-tip">{{ $t('cmdb.ci.qrcodeBatchTip') }}</p>

<div v-if="qrcodeList.length === 0 && !generating" class="qrcode-batch-empty">
<a-empty :description="$t('cmdb.ci.qrcodeBatchEmpty')" />
</div>

<div v-if="generating" class="qrcode-batch-generating">
<a-spin />
<span>正在生成 {{ generatedCount }} / {{ totalCount }} ...</span>
</div>

<div v-if="qrcodeList.length" class="qrcode-batch-grid" ref="qrcodeGrid">
<div
v-for="item in qrcodeList"
:key="item.ciId"
class="qrcode-batch-item"
>
<canvas :ref="'qrcode-' + item.ciId"></canvas>
<p class="qrcode-batch-item-label">{{ item.label }}</p>
<p class="qrcode-batch-item-id">CI ID: {{ item.ciId }}</p>
</div>
</div>

<div class="qrcode-batch-actions" v-if="qrcodeList.length">
<a-button type="primary" @click="downloadAll">
<a-icon type="download" /> {{ $t('cmdb.ci.qrcodeDownload') }}
</a-button>
<a-button @click="printAll">
<a-icon type="printer" /> {{ $t('cmdb.ci.printQRCode') }}
</a-button>
</div>
</a-modal>
</template>

<script>
import QRCode from 'qrcode'

export default {
name: 'QRCodeBatchExport',
data() {
return {
visible: false,
ciList: [],
qrcodeList: [],
generating: false,
generatedCount: 0,
totalCount: 0
}
},
methods: {
open(ciList) {
if (!ciList || !ciList.length) {
this.$message.warning(this.$t('cmdb.ci.qrcodeBatchEmpty'))
return
}
this.ciList = ciList
this.qrcodeList = []
this.visible = true
this.$nextTick(() => {
this.generateAll()
})
},
async generateAll() {
this.generating = true
this.generatedCount = 0
this.totalCount = this.ciList.length

const qrcodeList = []
for (const ci of this.ciList) {
const mobileUrl = `${window.location.origin}/cmdb/mobile/${ci.typeId}/${ci.ciId}`
qrcodeList.push({
ciId: ci.ciId,
typeId: ci.typeId,
label: ci.label || ci.name || `CI ${ci.ciId}`,
url: mobileUrl
})
}

this.qrcodeList = qrcodeList
await this.$nextTick()

for (const item of this.qrcodeList) {
const canvasRef = this.$refs['qrcode-' + item.ciId]
const canvas = Array.isArray(canvasRef) ? canvasRef[0] : canvasRef
if (canvas) {
try {
await QRCode.toCanvas(canvas, item.url, {
width: 150,
margin: 1,
color: { dark: '#000000', light: '#ffffff' }
})
} catch (e) {
console.error('QRCode generate failed for CI', item.ciId, e)
}
}
this.generatedCount++
}

this.generating = false
},
downloadAll() {
const grid = this.$refs.qrcodeGrid
if (!grid) return

const link = document.createElement('a')
link.download = 'cmdb-qrcodes-batch.png'

import('html2canvas').then(({ default: html2canvas }) => {
html2canvas(grid, {
backgroundColor: '#ffffff',
scale: 2
}).then((canvas) => {
link.href = canvas.toDataURL('image/png')
link.click()
})
}).catch(() => {
this.$message.warning(this.$t('cmdb.ci.copyFailed'))
})
},
printAll() {
const grid = this.$refs.qrcodeGrid
if (!grid) return

const printWindow = window.open('', '_blank', 'width=800,height=600')
if (!printWindow) {
this.$message.warning(this.$t('cmdb.ci.copyFailed'))
return
}

const content = grid.innerHTML
printWindow.document.write(`
<html>
<head>
<title>CMDB QR Codes</title>
<style>
body { font-family: Arial, sans-serif; padding: 20px; }
.qrcode-batch-grid { display: flex; flex-wrap: wrap; gap: 20px; justify-content: center; }
.qrcode-batch-item { text-align: center; width: 170px; }
.qrcode-batch-item-label { font-size: 12px; margin: 4px 0 2px; word-break: break-all; }
.qrcode-batch-item-id { font-size: 11px; color: #999; margin: 0; }
@media print {
.qrcode-batch-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; }
.qrcode-batch-item { page-break-inside: avoid; }
}
</style>
</head>
<body>
<div class="qrcode-batch-grid">${content}</div>
</body>
</html>
`)
printWindow.document.close()
setTimeout(() => {
printWindow.print()
printWindow.close()
}, 500)
}
}
}
</script>

<style lang="less" scoped>
.qrcode-batch-tip {
color: rgba(0, 0, 0, 0.45);
font-size: 13px;
margin-bottom: 16px;
}

.qrcode-batch-empty {
padding: 20px 0;
}

.qrcode-batch-generating {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
padding: 40px 0;
color: #999;
}

.qrcode-batch-grid {
display: flex;
flex-wrap: wrap;
gap: 16px;
justify-content: center;
padding: 8px 0;
}

.qrcode-batch-item {
text-align: center;
width: 160px;
padding: 12px 8px;
border: 1px solid #f0f0f0;
border-radius: 8px;
background: #fff;
}

.qrcode-batch-item-label {
font-size: 12px;
color: #333;
margin: 6px 0 2px;
word-break: break-all;
max-width: 140px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

.qrcode-batch-item-id {
font-size: 11px;
color: #bbb;
margin: 0;
}

.qrcode-batch-actions {
display: flex;
justify-content: center;
gap: 12px;
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid #f0f0f0;
}
</style>
Loading