|
|
@@ -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>
|