Browse Source

feat(web): 指标关联配置管理页(列表+新增/编辑+单对/一键校准)

iwt 2 days ago
parent
commit
082a1f46c7
1 changed files with 267 additions and 0 deletions
  1. 267 0
      cfc-web/src/views/admin/IndicatorCorrelations.vue

+ 267 - 0
cfc-web/src/views/admin/IndicatorCorrelations.vue

@@ -0,0 +1,267 @@
+<template>
+  <div class="indicator-correlations admin-page">
+    <div class="header">
+      <h2>指标关联配置</h2>
+      <el-button type="warning" icon="el-icon-refresh" @click="calibrateAll">一键校准</el-button>
+    </div>
+
+    <div class="adm-table-wrap">
+      <el-table height="100%" :data="list" v-loading="loading" border stripe>
+        <el-table-column prop="id" label="ID" width="60" />
+        <el-table-column label="指标A" min-width="150" show-overflow-tooltip>
+          <template slot-scope="{ row }">{{ defName(row.definitionIdA) }}</template>
+        </el-table-column>
+        <el-table-column label="指标B" min-width="150" show-overflow-tooltip>
+          <template slot-scope="{ row }">{{ defName(row.definitionIdB) }}</template>
+        </el-table-column>
+        <el-table-column label="方向" width="80">
+          <template slot-scope="{ row }">
+            <el-tag v-if="row.direction === 'positive'" type="success" size="mini">正向</el-tag>
+            <el-tag v-else-if="row.direction === 'negative'" type="danger" size="mini">负向</el-tag>
+            <span v-else>-</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="人工强度" width="90">
+          <template slot-scope="{ row }">{{ row.manualStrength !== null && row.manualStrength !== undefined ? row.manualStrength : '-' }}</template>
+        </el-table-column>
+        <el-table-column label="计算强度" width="90">
+          <template slot-scope="{ row }">{{ row.computedStrength !== null && row.computedStrength !== undefined ? row.computedStrength : '-' }}</template>
+        </el-table-column>
+        <el-table-column label="生效强度" width="90">
+          <template slot-scope="{ row }">{{ row.effectiveStrength !== null && row.effectiveStrength !== undefined ? row.effectiveStrength : '-' }}</template>
+        </el-table-column>
+        <el-table-column label="样本量" width="80">
+          <template slot-scope="{ row }">{{ row.sampleSize || 0 }}</template>
+        </el-table-column>
+        <el-table-column label="置信度" width="80">
+          <template slot-scope="{ row }">{{ row.confidence !== null && row.confidence !== undefined ? row.confidence : '-' }}</template>
+        </el-table-column>
+        <el-table-column label="最近校准时间" width="150">
+          <template slot-scope="{ row }">{{ formatTime(row.computedAt) }}</template>
+        </el-table-column>
+        <el-table-column label="人工锁定" width="90">
+          <template slot-scope="{ row }">
+            <el-tag v-if="row.manualLocked" type="warning" size="mini">锁定</el-tag>
+            <span v-else>-</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="状态" width="80">
+          <template slot-scope="{ row }">
+            <el-tag :type="row.status === 1 ? 'success' : 'info'" size="mini">{{ row.status === 1 ? '生效' : '停用' }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" width="160" fixed="right">
+          <template slot-scope="{ row }">
+            <el-button size="mini" type="primary" @click="openEdit(row)">编辑</el-button>
+            <el-button size="mini" type="warning" @click="calibrateOne(row)">单对校准</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+    </div>
+
+    <el-pagination
+      @size-change="onSizeChange"
+      @current-change="onPageChange"
+      :current-page="page"
+      :page-size="size"
+      :total="total"
+      layout="total, prev, pager, next"
+      style="margin-top: 20px; text-align: right;"
+    />
+
+    <el-dialog :title="editingId ? '编辑关联对' : '新增关联对'" :visible.sync="dialogVisible" width="600px">
+      <el-form :model="form" label-width="100px" size="small">
+        <el-form-item label="指标A">
+          <el-select v-model="form.definitionIdA" filterable clearable :filter-method="filterDefsA" placeholder="搜索并选择指标A" style="width:100%;">
+            <el-option v-for="d in defOptionsA" :key="d.id" :label="d.name + '(' + d.code + ')'" :value="d.id" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="指标B">
+          <el-select v-model="form.definitionIdB" filterable clearable :filter-method="filterDefsB" placeholder="搜索并选择指标B" style="width:100%;">
+            <el-option v-for="d in defOptionsB" :key="d.id" :label="d.name + '(' + d.code + ')'" :value="d.id" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="方向">
+          <el-select v-model="form.direction" style="width:100%;">
+            <el-option label="正向" value="positive" />
+            <el-option label="负向" value="negative" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="人工强度">
+          <el-input-number v-model="form.manualStrength" :min="0" :max="1" :step="0.05" :precision="2" />
+        </el-form-item>
+        <el-form-item label="说明">
+          <el-input v-model="form.description" type="textarea" :rows="2" placeholder="关联说明(可空)" />
+        </el-form-item>
+        <el-form-item label="人工锁定">
+          <el-switch v-model="form.manualLocked" />
+          <span class="tip-text">开启后后续校准不覆盖人工值</span>
+        </el-form-item>
+        <el-form-item label="来源备注">
+          <el-input v-model="form.sourceNote" placeholder="来源备注(可空)" />
+        </el-form-item>
+      </el-form>
+      <span slot="footer">
+        <el-button @click="dialogVisible = false">取消</el-button>
+        <el-button type="primary" @click="submitCorrelation" :loading="saving">保存</el-button>
+      </span>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import SortMixin from '@/mixins/SortMixin'
+import { listIndicatorDefinitions, listIndicatorCorrelations, saveIndicatorCorrelation, calibrateIndicatorCorrelations } from '@/api/indicator'
+
+export default {
+  mixins: [SortMixin],
+  name: 'IndicatorCorrelations',
+  data() {
+    return {
+      list: [],
+      total: 0,
+      page: 1,
+      size: 15,
+      loading: false,
+      dialogVisible: false,
+      saving: false,
+      editingId: null,
+      form: this.getEmptyForm(),
+      definitions: [],
+      defMap: {},
+      defOptionsA: [],
+      defOptionsB: [],
+      sortableColumns: { id: 'ID', createdAt: '创建时间', updatedAt: '更新时间' }
+    }
+  },
+  created() {
+    this.loadDefinitions()
+    this.listCorrelations()
+  },
+  methods: {
+    getEmptyForm() {
+      return { definitionIdA: null, definitionIdB: null, direction: 'positive', manualStrength: 0.5, description: '', manualLocked: false, sourceNote: '' }
+    },
+    async loadDefinitions() {
+      try {
+        const res = await listIndicatorDefinitions({ status: 1 })
+        if (res.code === 200) {
+          this.definitions = res.data || []
+          const map = {}
+          this.definitions.forEach(d => { map[d.id] = d.name })
+          this.defMap = map
+        }
+      } catch (e) { console.error(e) }
+    },
+    async listCorrelations() {
+      this.loading = true
+      try {
+        const res = await listIndicatorCorrelations({ page: this.page, size: this.size })
+        if (res.code === 200 && res.data) {
+          this.list = res.data.records || []
+          this.total = res.data.total || 0
+        }
+      } catch (e) { console.error(e) }
+      this.loading = false
+    },
+    onSizeChange(val) {
+      this.size = val
+      this.page = 1
+      this.listCorrelations()
+    },
+    onPageChange(p) {
+      this.page = typeof p === 'number' ? p : 1
+      this.listCorrelations()
+    },
+    openCreate() {
+      this.editingId = null
+      this.form = this.getEmptyForm()
+      this.defOptionsA = this.definitions
+      this.defOptionsB = this.definitions
+      this.dialogVisible = true
+    },
+    openEdit(row) {
+      this.editingId = row.id
+      this.form = {
+        definitionIdA: row.definitionIdA,
+        definitionIdB: row.definitionIdB,
+        direction: row.direction || 'positive',
+        manualStrength: row.manualStrength !== null && row.manualStrength !== undefined ? row.manualStrength : 0.5,
+        description: row.description || '',
+        manualLocked: !!row.manualLocked,
+        sourceNote: row.sourceNote || ''
+      }
+      this.defOptionsA = this.definitions
+      this.defOptionsB = this.definitions
+      this.dialogVisible = true
+    },
+    filterDefsA(keyword) {
+      this.defOptionsA = this.filterDefs(keyword)
+    },
+    filterDefsB(keyword) {
+      this.defOptionsB = this.filterDefs(keyword)
+    },
+    filterDefs(keyword) {
+      const kw = (keyword || '').trim().toLowerCase()
+      if (!kw) return this.definitions
+      return this.definitions.filter(d => (d.name || '').toLowerCase().indexOf(kw) > -1 || (d.code || '').toLowerCase().indexOf(kw) > -1)
+    },
+    async submitCorrelation() {
+      if (!this.form.definitionIdA || !this.form.definitionIdB) {
+        this.$message.error('请选择指标A和指标B')
+        return
+      }
+      if (this.form.definitionIdA === this.form.definitionIdB) {
+        this.$message.error('指标A与指标B不能相同')
+        return
+      }
+      this.saving = true
+      try {
+        const res = await saveIndicatorCorrelation({ ...this.form })
+        if (res.code === 200) {
+          this.$message.success(this.editingId ? '更新成功' : '创建成功')
+          this.dialogVisible = false
+          this.listCorrelations()
+        } else {
+          this.$message.error(res.message || '操作失败')
+        }
+      } catch (e) { this.$message.error('操作失败') }
+      this.saving = false
+    },
+    calibrateOne(row) {
+      this.$confirm('确定对该关联对执行校准?', '提示', { type: 'warning' }).then(async () => {
+        try {
+          const res = await calibrateIndicatorCorrelations({ pairId: row.id })
+          if (res.code === 200) {
+            this.$message.success(res.message || '校准完成')
+            this.listCorrelations()
+          }
+        } catch (e) { this.$message.error('校准失败') }
+      }).catch(() => {})
+    },
+    calibrateAll() {
+      this.$confirm('将基于样本数据重新计算全部关联对的有效强度,确定?', '提示', { type: 'warning' }).then(async () => {
+        try {
+          const res = await calibrateIndicatorCorrelations({})
+          if (res.code === 200) {
+            this.$message.success(res.message || '校准完成')
+            this.listCorrelations()
+          }
+        } catch (e) { this.$message.error('校准失败') }
+      }).catch(() => {})
+    },
+    defName(id) {
+      return (this.defMap[id] || '#' + id)
+    },
+    formatTime(t) {
+      return t ? t.replace('T', ' ').substring(0, 19) : '-'
+    }
+  }
+}
+</script>
+
+<style scoped>
+.header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; }
+.header h2 { margin: 0; }
+.tip-text { margin-left: 8px; font-size: 12px; color: #999; }
+</style>