ReportParserImport.vue 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211
  1. <template>
  2. <div class="report-import admin-page">
  3. <el-card>
  4. <div slot="header">
  5. <span>解析器导入管理</span>
  6. <div style="float: right; display: flex; gap: 10px;">
  7. <el-button type="primary" size="small" icon="el-icon-upload2" @click="uploadDialogVisible = true">上传导入包</el-button>
  8. </div>
  9. </div>
  10. <div class="adm-table-wrap">
  11. <el-table :data="imports" v-loading="loading" border stripe height="100%">
  12. <el-table-column prop="id" label="ID" width="60">
  13. <template slot="header">
  14. <span class="sort-header" @click.stop="onSortClick('id', $event)">ID{{ sortIcon('id') }}</span>
  15. </template>
  16. </el-table-column>
  17. <el-table-column prop="packageName" label="导入包名称" min-width="200">
  18. <template slot="header">
  19. <span class="sort-header" @click.stop="onSortClick('packageName', $event)">导入包名称<span v-if="sortIndex('packageName') !== null" class="sort-index" :class="sortClass('packageName') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('packageName') }}</span>{{ sortIcon('packageName') }}</span>
  20. </template>
  21. </el-table-column>
  22. <el-table-column prop="packageFileUrl" label="文件" width="150">
  23. <template slot="header">
  24. <span class="sort-header" @click.stop="onSortClick('packageFileUrl', $event)">文件<span v-if="sortIndex('packageFileUrl') !== null" class="sort-index" :class="sortClass('packageFileUrl') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('packageFileUrl') }}</span>{{ sortIcon('packageFileUrl') }}</span>
  25. </template>
  26. </el-table-column>
  27. <el-table-column label="状态" width="100">
  28. <template slot-scope="{ row }">
  29. <el-tag :type="statusType(row.status)" size="small">{{ statusText(row.status) }}</el-tag>
  30. </template>
  31. </el-table-column>
  32. <el-table-column prop="createdAt" label="导入时间" width="180">
  33. <template slot="header">
  34. <span class="sort-header" @click.stop="onSortClick('createdAt', $event)">导入时间{{ sortIcon('createdAt') }}</span>
  35. </template>
  36. </el-table-column>
  37. <el-table-column label="操作" width="160" fixed="right">
  38. <template slot-scope="{ row }">
  39. <template v-if="row.status === 'pending'">
  40. <el-button size="mini" type="success" @click="handleApprove(row)">批准</el-button>
  41. <el-button size="mini" type="danger" @click="handleReject(row)">驳回</el-button>
  42. </template>
  43. <el-button size="mini" type="primary" @click="handlePreview(row)">预览</el-button>
  44. </template>
  45. </el-table-column>
  46. </el-table>
  47. </div>
  48. </el-card>
  49. <!-- 上传导入包对话框 -->
  50. <el-dialog :visible.sync="uploadDialogVisible" title="上传导入包" width="600px" :close-on-click-modal="false">
  51. <el-upload
  52. drag
  53. :auto-upload="false"
  54. :limit="1"
  55. accept=".json"
  56. :on-change="handleFileChange">
  57. <i class="el-icon-upload"></i>
  58. <div class="el-upload__text">将 json 文件拖到此处,或<em>点击上传</em></div>
  59. <div class="el-upload__tip" slot="tip">导入包 JSON 格式:{ packageName, typeId, displayName, family, description, parserScript, fingerprints: [{pattern, weight, isExclusion}] }</div>
  60. </el-upload>
  61. <div slot="footer">
  62. <el-button @click="uploadDialogVisible = false">取消</el-button>
  63. <el-button type="primary" @click="handleImport" :loading="submitting">确认导入</el-button>
  64. </div>
  65. </el-dialog>
  66. <!-- 预览导入包对话框 -->
  67. <el-dialog :visible.sync="previewDialogVisible" title="导入包预览" width="700px">
  68. <div v-if="previewData">
  69. <el-descriptions :column="2" border size="small">
  70. <el-descriptions-item label="名称">{{ previewData.packageName }}</el-descriptions-item>
  71. <el-descriptions-item label="类型ID">{{ previewData.typeId }}</el-descriptions-item>
  72. <el-descriptions-item label="显示名称">{{ previewData.displayName }}</el-descriptions-item>
  73. <el-descriptions-item label="家族">{{ previewData.family }}</el-descriptions-item>
  74. <el-descriptions-item label="描述" :span="2">{{ previewData.description }}</el-descriptions-item>
  75. </el-descriptions>
  76. <h4>指纹规则</h4>
  77. <el-table :data="previewData.fingerprints || []" border size="small">
  78. <el-table-column prop="pattern" label="文本特征" min-width="200" />
  79. <el-table-column prop="weight" label="权重" width="70" />
  80. <el-table-column label="类型" width="80">
  81. <template slot-scope="{ row }">
  82. <el-tag :type="row.isExclusion ? 'danger' : 'success'" size="mini">{{ row.isExclusion ? '排除' : '正向' }}</el-tag>
  83. </template>
  84. </el-table-column>
  85. <h4>解析脚本</h4>
  86. <pre class="script-preview">{{ previewData.parserScript }}</pre>
  87. </el-table>
  88. </div>
  89. </el-dialog>
  90. </div>
  91. </template>
  92. <script>
  93. import { getParserImports, approveParserImport, rejectParserImport, importParserPackage } from '@/api/reportParser'
  94. import SortMixin from '@/mixins/SortMixin'
  95. export default {
  96. name: 'ReportParserImport',
  97. mixins: [SortMixin],
  98. data() {
  99. return {
  100. loading: false, submitting: false,
  101. imports: [],
  102. sortableColumns: { id: 'ID', createdAt: '导入时间', packageName: '导入包名称', packageFileUrl: '文件' },
  103. uploadDialogVisible: false,
  104. previewDialogVisible: false,
  105. selectedFile: null,
  106. previewData: null,
  107. }
  108. },
  109. mounted() { this.fetchImports() },
  110. methods: {
  111. loadList() {
  112. this.fetchImports()
  113. },
  114. async fetchImports() {
  115. this.loading = true
  116. try {
  117. var params = { page: 1, size: 50 }
  118. if (this.sortState.length > 0) params.sort = this.sortState
  119. const res = await getParserImports(params)
  120. this.imports = res.data?.records || res.data || []
  121. } catch (e) { this.$message.error('加载失败') }
  122. this.loading = false
  123. },
  124. statusType(status) {
  125. return { pending: 'warning', approved: 'success', rejected: 'danger' }[status] || 'info'
  126. },
  127. statusText(status) {
  128. return { pending: '待审核', approved: '已批准', rejected: '已驳回' }[status] || status
  129. },
  130. handleFileChange(file) {
  131. this.selectedFile = file.raw
  132. },
  133. async handleImport() {
  134. if (!this.selectedFile) {
  135. this.$message.warning('请先选择文件')
  136. return
  137. }
  138. this.submitting = true
  139. const reader = new FileReader()
  140. reader.onload = async (e) => {
  141. try {
  142. const jsonContent = e.target.result
  143. const res = await importParserPackage({ jsonContent, fileName: this.selectedFile.name })
  144. this.$message.success('导入成功,待审核')
  145. this.uploadDialogVisible = false
  146. this.selectedFile = null
  147. this.fetchImports()
  148. } catch (err) { this.$message.error('导入失败') }
  149. this.submitting = false
  150. }
  151. reader.readAsText(this.selectedFile)
  152. },
  153. handlePreview(row) {
  154. try {
  155. const jsonContent = row.fingerprintJson
  156. this.previewData = JSON.parse(jsonContent)
  157. this.previewDialogVisible = true
  158. } catch (e) { this.$message.error('预览失败:JSON 解析错误') }
  159. },
  160. handleApprove(row) {
  161. this.$confirm('确认批准此导入?将注册报告类型和指纹规则。', '提示', { type: 'warning' })
  162. .then(async () => {
  163. try {
  164. await approveParserImport({ id: row.id })
  165. this.$message.success('已批准')
  166. this.fetchImports()
  167. } catch (e) { this.$message.error('操作失败') }
  168. }).catch(() => {})
  169. },
  170. handleReject(row) {
  171. this.$prompt('请输入驳回原因', '驳回导入', { inputPlaceholder: '驳回原因', inputValidator: (v) => !!v || '请输入原因' })
  172. .then(async ({ value }) => {
  173. try {
  174. await rejectParserImport({ id: row.id, note: value })
  175. this.$message.success('已驳回')
  176. this.fetchImports()
  177. } catch (e) { this.$message.error('操作失败') }
  178. }).catch(() => {})
  179. },
  180. }
  181. }
  182. </script>
  183. <style scoped>
  184. .script-preview {
  185. background: #f5f7fa;
  186. border: 1px solid #e4e7ed;
  187. border-radius: 4px;
  188. padding: 10px;
  189. max-height: 300px;
  190. overflow: auto;
  191. font-size: 12px;
  192. white-space: pre-wrap;
  193. word-break: break-all;
  194. }
  195. .sort-header {
  196. cursor: pointer;
  197. user-select: none;
  198. font-weight: 500;
  199. font-size: 12px;
  200. }
  201. .sort-header:hover {
  202. color: #409eff;
  203. }
  204. </style>