2026-01-18 10:24:35 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<div class="rule-learning-demo">
|
2026-02-01 23:42:12 +08:00
|
|
|
|
<el-card shadow="hover">
|
|
|
|
|
|
<template #header>
|
|
|
|
|
|
<div class="card-header">
|
|
|
|
|
|
<h4>规则 vs 学习</h4>
|
|
|
|
|
|
<p class="subtitle">
|
|
|
|
|
|
对比:你写阈值 (规则) vs 让模型从数据里"推断"阈值 (学习)
|
|
|
|
|
|
</p>
|
2026-01-18 10:24:35 +08:00
|
|
|
|
</div>
|
2026-02-01 23:42:12 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<el-row :gutter="20">
|
|
|
|
|
|
<!-- Rule Based -->
|
2026-02-18 17:38:10 +08:00
|
|
|
|
<el-col
|
|
|
|
|
|
:xs="24"
|
|
|
|
|
|
:md="12"
|
|
|
|
|
|
class="mb-4-xs"
|
|
|
|
|
|
>
|
|
|
|
|
|
<el-card
|
|
|
|
|
|
shadow="never"
|
|
|
|
|
|
class="panel-card"
|
|
|
|
|
|
>
|
2026-02-01 23:42:12 +08:00
|
|
|
|
<template #header>
|
2026-02-18 17:38:10 +08:00
|
|
|
|
<div class="panel-title">
|
|
|
|
|
|
规则系统(手写 If/Else)
|
|
|
|
|
|
</div>
|
2026-02-01 23:42:12 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
<div class="panel-content">
|
|
|
|
|
|
<div class="control-row">
|
|
|
|
|
|
<span class="label">阈值 size ></span>
|
|
|
|
|
|
<el-input-number
|
|
|
|
|
|
v-model="ruleThreshold"
|
|
|
|
|
|
:min="1"
|
|
|
|
|
|
:max="10"
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<span class="text-xs text-gray">(必须明确写出)</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="control-row mt-4">
|
|
|
|
|
|
<span class="label">测试输入 size</span>
|
|
|
|
|
|
<el-slider
|
|
|
|
|
|
v-model="testInput"
|
|
|
|
|
|
:min="1"
|
|
|
|
|
|
:max="10"
|
|
|
|
|
|
show-input
|
|
|
|
|
|
input-size="small"
|
|
|
|
|
|
class="flex-1"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
|
class="result-box mt-4"
|
|
|
|
|
|
:class="{
|
|
|
|
|
|
good: ruleResult.label === '🍎',
|
|
|
|
|
|
bad: ruleResult.label === '🍒'
|
|
|
|
|
|
}"
|
|
|
|
|
|
>
|
2026-02-18 17:38:10 +08:00
|
|
|
|
<div class="result-title">
|
|
|
|
|
|
输出
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="result-value">
|
|
|
|
|
|
{{ ruleResult.text }}
|
|
|
|
|
|
</div>
|
2026-02-01 23:42:12 +08:00
|
|
|
|
<div class="result-code">
|
|
|
|
|
|
if (size > {{ ruleThreshold }}) return 🍎 else return 🍒
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<el-alert
|
|
|
|
|
|
title="当环境变化(比如'苹果平均变小了'),你需要手动改规则;规则越多,维护成本越高。"
|
|
|
|
|
|
type="warning"
|
|
|
|
|
|
:closable="false"
|
|
|
|
|
|
class="mt-4"
|
|
|
|
|
|
/>
|
2026-01-18 10:24:35 +08:00
|
|
|
|
</div>
|
2026-02-01 23:42:12 +08:00
|
|
|
|
</el-card>
|
|
|
|
|
|
</el-col>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Machine Learning -->
|
2026-02-18 17:38:10 +08:00
|
|
|
|
<el-col
|
|
|
|
|
|
:xs="24"
|
|
|
|
|
|
:md="12"
|
|
|
|
|
|
>
|
|
|
|
|
|
<el-card
|
|
|
|
|
|
shadow="never"
|
|
|
|
|
|
class="panel-card"
|
|
|
|
|
|
>
|
2026-02-01 23:42:12 +08:00
|
|
|
|
<template #header>
|
2026-02-18 17:38:10 +08:00
|
|
|
|
<div class="panel-title">
|
|
|
|
|
|
机器学习(从样本推断边界)
|
|
|
|
|
|
</div>
|
2026-02-01 23:42:12 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
<div class="panel-content">
|
|
|
|
|
|
<div class="control-row">
|
|
|
|
|
|
<el-input-number
|
|
|
|
|
|
v-model="newSize"
|
|
|
|
|
|
:min="1"
|
|
|
|
|
|
:max="10"
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
placeholder="Size"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<el-select
|
|
|
|
|
|
v-model="newLabel"
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
placeholder="Label"
|
|
|
|
|
|
style="width: 120px"
|
|
|
|
|
|
>
|
2026-02-18 17:38:10 +08:00
|
|
|
|
<el-option
|
|
|
|
|
|
label="🍒 樱桃"
|
|
|
|
|
|
value="🍒"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<el-option
|
|
|
|
|
|
label="🍎 苹果"
|
|
|
|
|
|
value="🍎"
|
|
|
|
|
|
/>
|
2026-02-01 23:42:12 +08:00
|
|
|
|
</el-select>
|
2026-02-18 17:38:10 +08:00
|
|
|
|
<el-button
|
|
|
|
|
|
type="primary"
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
@click="addSample"
|
2026-02-01 23:42:12 +08:00
|
|
|
|
>
|
2026-02-18 17:38:10 +08:00
|
|
|
|
添加样本
|
|
|
|
|
|
</el-button>
|
2026-02-01 23:42:12 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="samples-area mt-4">
|
|
|
|
|
|
<el-empty
|
|
|
|
|
|
v-if="trainingData.length === 0"
|
|
|
|
|
|
description="还没有样本:先添加 2-4 个样本再训练"
|
|
|
|
|
|
:image-size="40"
|
|
|
|
|
|
/>
|
2026-02-18 17:38:10 +08:00
|
|
|
|
<div
|
|
|
|
|
|
v-else
|
|
|
|
|
|
class="sample-chips"
|
|
|
|
|
|
>
|
2026-02-01 23:42:12 +08:00
|
|
|
|
<el-tag
|
|
|
|
|
|
v-for="(p, i) in trainingData"
|
|
|
|
|
|
:key="p.id"
|
|
|
|
|
|
closable
|
|
|
|
|
|
:type="p.label === '🍎' ? 'danger' : 'info'"
|
|
|
|
|
|
effect="plain"
|
2026-02-18 17:38:10 +08:00
|
|
|
|
@close="removeSample(i)"
|
2026-02-01 23:42:12 +08:00
|
|
|
|
>
|
|
|
|
|
|
{{ p.size }} → {{ p.label }}
|
|
|
|
|
|
</el-tag>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="actions mt-4 flex gap-2">
|
|
|
|
|
|
<el-button
|
|
|
|
|
|
type="success"
|
|
|
|
|
|
:disabled="trainingData.length < 2"
|
2026-02-18 17:38:10 +08:00
|
|
|
|
@click="train"
|
2026-02-01 23:42:12 +08:00
|
|
|
|
>
|
|
|
|
|
|
训练(推断阈值)
|
|
|
|
|
|
</el-button>
|
2026-02-18 17:38:10 +08:00
|
|
|
|
<el-button @click="resetLearning">
|
|
|
|
|
|
重置
|
|
|
|
|
|
</el-button>
|
2026-02-01 23:42:12 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-02-18 17:38:10 +08:00
|
|
|
|
<div
|
|
|
|
|
|
v-if="learnedThreshold !== null"
|
|
|
|
|
|
class="learned-result mt-4"
|
|
|
|
|
|
>
|
2026-02-01 23:42:12 +08:00
|
|
|
|
<el-alert
|
|
|
|
|
|
type="success"
|
|
|
|
|
|
:closable="false"
|
|
|
|
|
|
show-icon
|
|
|
|
|
|
title="学习完成!"
|
|
|
|
|
|
>
|
|
|
|
|
|
<p>
|
|
|
|
|
|
模型推断出阈值应为: <strong>{{ learnedThreshold }}</strong>
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<p class="text-xs">
|
|
|
|
|
|
(大于 {{ learnedThreshold }} 是苹果,否则是樱桃)
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</el-alert>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-card>
|
|
|
|
|
|
</el-col>
|
|
|
|
|
|
</el-row>
|
|
|
|
|
|
</el-card>
|
2026-01-18 10:24:35 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup>
|
2026-02-01 23:42:12 +08:00
|
|
|
|
import { ref, computed } from 'vue'
|
2026-01-18 10:24:35 +08:00
|
|
|
|
|
2026-02-01 23:42:12 +08:00
|
|
|
|
// Rule Based Logic
|
|
|
|
|
|
const ruleThreshold = ref(5)
|
|
|
|
|
|
const testInput = ref(6)
|
2026-01-18 10:24:35 +08:00
|
|
|
|
|
|
|
|
|
|
const ruleResult = computed(() => {
|
2026-02-01 23:42:12 +08:00
|
|
|
|
if (testInput.value > ruleThreshold.value) {
|
|
|
|
|
|
return { label: '🍎', text: '🍎 苹果' }
|
|
|
|
|
|
} else {
|
|
|
|
|
|
return { label: '🍒', text: '🍒 樱桃' }
|
2026-01-19 12:22:02 +08:00
|
|
|
|
}
|
2026-01-18 10:24:35 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
2026-02-01 23:42:12 +08:00
|
|
|
|
// ML Logic
|
|
|
|
|
|
const newSize = ref(5)
|
|
|
|
|
|
const newLabel = ref('🍎')
|
2026-01-19 12:22:02 +08:00
|
|
|
|
const trainingData = ref([
|
2026-02-01 23:42:12 +08:00
|
|
|
|
{ id: 1, size: 2, label: '🍒' },
|
|
|
|
|
|
{ id: 2, size: 8, label: '🍎' }
|
2026-01-19 12:22:02 +08:00
|
|
|
|
])
|
2026-02-01 23:42:12 +08:00
|
|
|
|
const learnedThreshold = ref(null)
|
2026-01-19 12:22:02 +08:00
|
|
|
|
|
|
|
|
|
|
const addSample = () => {
|
2026-02-01 23:42:12 +08:00
|
|
|
|
trainingData.value.push({
|
|
|
|
|
|
id: Date.now(),
|
|
|
|
|
|
size: newSize.value,
|
|
|
|
|
|
label: newLabel.value
|
|
|
|
|
|
})
|
2026-01-19 12:22:02 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const removeSample = (index) => {
|
|
|
|
|
|
trainingData.value.splice(index, 1)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const resetLearning = () => {
|
|
|
|
|
|
trainingData.value = []
|
2026-02-01 23:42:12 +08:00
|
|
|
|
learnedThreshold.value = null
|
2026-01-19 12:22:02 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-01 23:42:12 +08:00
|
|
|
|
const train = () => {
|
|
|
|
|
|
// Simple "training": find the boundary between cherry and apple
|
|
|
|
|
|
// Sort data by size
|
|
|
|
|
|
const sorted = [...trainingData.value].sort((a, b) => a.size - b.size)
|
|
|
|
|
|
|
|
|
|
|
|
// Find the first Apple
|
|
|
|
|
|
const firstAppleIndex = sorted.findIndex((item) => item.label === '🍎')
|
|
|
|
|
|
|
|
|
|
|
|
if (firstAppleIndex === -1) {
|
|
|
|
|
|
// All cherries
|
|
|
|
|
|
learnedThreshold.value = 10
|
|
|
|
|
|
} else if (firstAppleIndex === 0) {
|
|
|
|
|
|
// All apples
|
|
|
|
|
|
learnedThreshold.value = 0
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Boundary is between last cherry and first apple
|
|
|
|
|
|
const lastCherry = sorted[firstAppleIndex - 1]
|
|
|
|
|
|
const firstApple = sorted[firstAppleIndex]
|
|
|
|
|
|
learnedThreshold.value = (lastCherry.size + firstApple.size) / 2
|
2026-01-19 12:22:02 +08:00
|
|
|
|
}
|
2026-02-01 23:42:12 +08:00
|
|
|
|
}
|
2026-01-18 10:24:35 +08:00
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
|
.rule-learning-demo {
|
2026-02-01 23:42:12 +08:00
|
|
|
|
margin: 20px 0;
|
2026-01-19 12:22:02 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-01 23:42:12 +08:00
|
|
|
|
.card-header h4 {
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
font-weight: 600;
|
2026-01-18 10:24:35 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-19 12:22:02 +08:00
|
|
|
|
.subtitle {
|
2026-02-01 23:42:12 +08:00
|
|
|
|
font-size: 13px;
|
2026-01-19 12:22:02 +08:00
|
|
|
|
color: var(--vp-c-text-2);
|
2026-02-01 23:42:12 +08:00
|
|
|
|
margin: 4px 0 0;
|
2026-01-19 12:22:02 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-01 23:42:12 +08:00
|
|
|
|
.panel-title {
|
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
|
font-size: 14px;
|
2026-01-18 10:24:35 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-01 23:42:12 +08:00
|
|
|
|
.control-row {
|
2026-01-18 10:24:35 +08:00
|
|
|
|
display: flex;
|
2026-01-19 12:22:02 +08:00
|
|
|
|
align-items: center;
|
2026-02-01 23:42:12 +08:00
|
|
|
|
gap: 8px;
|
2026-01-19 12:22:02 +08:00
|
|
|
|
flex-wrap: wrap;
|
2026-01-18 10:24:35 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-19 12:22:02 +08:00
|
|
|
|
.label {
|
2026-02-01 23:42:12 +08:00
|
|
|
|
font-size: 14px;
|
2026-01-18 10:24:35 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-01 23:42:12 +08:00
|
|
|
|
.text-xs {
|
|
|
|
|
|
font-size: 12px;
|
2026-01-19 12:22:02 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-01 23:42:12 +08:00
|
|
|
|
.text-gray {
|
|
|
|
|
|
color: var(--vp-c-text-2);
|
2026-01-19 12:22:02 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-01 23:42:12 +08:00
|
|
|
|
.flex-1 {
|
|
|
|
|
|
flex: 1;
|
2026-01-19 12:22:02 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-01 23:42:12 +08:00
|
|
|
|
.mt-4 {
|
|
|
|
|
|
margin-top: 16px;
|
2026-01-19 12:22:02 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-01 23:42:12 +08:00
|
|
|
|
.mb-4-xs {
|
|
|
|
|
|
margin-bottom: 20px;
|
2026-01-18 10:24:35 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-01 23:42:12 +08:00
|
|
|
|
@media (min-width: 992px) {
|
|
|
|
|
|
.mb-4-xs {
|
|
|
|
|
|
margin-bottom: 0;
|
|
|
|
|
|
}
|
2026-01-19 12:22:02 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-01 23:42:12 +08:00
|
|
|
|
.result-box {
|
|
|
|
|
|
background-color: var(--vp-c-bg-alt);
|
|
|
|
|
|
padding: 12px;
|
2026-02-14 20:23:34 +08:00
|
|
|
|
border-radius: 6px;
|
2026-01-19 11:25:10 +08:00
|
|
|
|
border: 1px solid var(--vp-c-divider);
|
2026-02-01 23:42:12 +08:00
|
|
|
|
text-align: center;
|
2026-01-18 10:24:35 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-01 23:42:12 +08:00
|
|
|
|
.result-box.good {
|
|
|
|
|
|
border-color: var(--el-color-danger);
|
|
|
|
|
|
background-color: var(--el-color-danger-light-9);
|
2026-01-18 10:24:35 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-01 23:42:12 +08:00
|
|
|
|
.result-box.bad {
|
|
|
|
|
|
border-color: var(--el-color-primary);
|
|
|
|
|
|
background-color: var(--el-color-primary-light-9);
|
2026-01-18 10:24:35 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-01 23:42:12 +08:00
|
|
|
|
.result-title {
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
color: var(--vp-c-text-2);
|
|
|
|
|
|
text-transform: uppercase;
|
2026-01-19 12:22:02 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-01 23:42:12 +08:00
|
|
|
|
.result-value {
|
|
|
|
|
|
font-size: 24px;
|
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
|
margin: 8px 0;
|
2026-01-18 12:21:49 +08:00
|
|
|
|
}
|
2026-01-18 10:24:35 +08:00
|
|
|
|
|
2026-02-01 23:42:12 +08:00
|
|
|
|
.result-code {
|
|
|
|
|
|
font-family: monospace;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
background-color: rgba(0, 0, 0, 0.05);
|
|
|
|
|
|
padding: 4px;
|
|
|
|
|
|
border-radius: 4px;
|
2026-01-18 10:24:35 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-01 23:42:12 +08:00
|
|
|
|
.sample-chips {
|
2026-01-18 10:24:35 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-wrap: wrap;
|
2026-02-01 23:42:12 +08:00
|
|
|
|
gap: 8px;
|
|
|
|
|
|
min-height: 40px;
|
2026-01-18 10:24:35 +08:00
|
|
|
|
}
|
2026-01-19 12:22:02 +08:00
|
|
|
|
|
2026-02-01 23:42:12 +08:00
|
|
|
|
.gap-2 {
|
|
|
|
|
|
gap: 8px;
|
2026-01-18 10:24:35 +08:00
|
|
|
|
}
|
2026-01-18 12:21:49 +08:00
|
|
|
|
</style>
|