| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482 |
- <template>
- <div class="page-container" v-loading="loading">
- <div class="page-header">
- <div>
- <el-button text @click="$router.push('/projects')" style="padding: 0; margin-bottom: 8px">
- <el-icon><ArrowLeft /></el-icon> 返回项目列表
- </el-button>
- <span class="page-title">{{ project?.name }}</span>
- <el-tag size="small" style="margin-left: 12px">{{ project?.topology }}</el-tag>
- <el-tag size="small" type="success" style="margin-left: 8px">{{ project?.status }}</el-tag>
- </div>
- <el-button type="primary" @click="showCreatePlan = true">
- <el-icon><Plus /></el-icon> 新建方案
- </el-button>
- </div>
- <el-row :gutter="16" v-if="project">
- <el-col :span="8">
- <div class="card">
- <h3 style="margin-bottom: 12px">项目信息</h3>
- <el-descriptions :column="1" border size="small">
- <el-descriptions-item label="ID">{{ project.id }}</el-descriptions-item>
- <el-descriptions-item label="拓扑结构">{{ project.topology }}</el-descriptions-item>
- <el-descriptions-item label="模型">{{ project.model_path || '-' }}</el-descriptions-item>
- <el-descriptions-item label="状态">{{ project.status }}</el-descriptions-item>
- <el-descriptions-item label="方案数">{{ project.plan_count }}</el-descriptions-item>
- <el-descriptions-item label="创建时间">{{ formatDate(project.created_at) }}</el-descriptions-item>
- <el-descriptions-item label="更新时间">{{ formatDate(project.updated_at) }}</el-descriptions-item>
- </el-descriptions>
- <div v-if="project.description" style="margin-top: 12px">
- <div style="color: #6b7280; font-size: 12px; margin-bottom: 4px">项目描述</div>
- <div style="font-size: 13px">{{ project.description }}</div>
- </div>
- </div>
- </el-col>
- <el-col :span="16">
- <div class="card">
- <h3 style="margin-bottom: 12px">边界条件</h3>
- <pre v-if="Object.keys(project.boundary_conditions).length" style="background: #f8fafc; padding: 12px; border-radius: 4px; font-size: 12px">{{ JSON.stringify(project.boundary_conditions, null, 2) }}</pre>
- <el-empty v-else description="未设置边界条件" :image-size="60" />
- </div>
- </el-col>
- </el-row>
- <div class="card">
- <h3 style="margin-bottom: 12px">仿真方案</h3>
- <el-table :data="plans" stripe @row-click="goToPlan">
- <el-table-column prop="id" label="ID" width="60" />
- <el-table-column prop="name" label="方案名称" min-width="200">
- <template #default="{ row }">
- <span style="color: #3b82f6; cursor: pointer">{{ row.name }}</span>
- </template>
- </el-table-column>
- <el-table-column prop="plan_id" label="方案UUID" width="180" />
- <el-table-column prop="status" label="状态" width="100">
- <template #default="{ row }">
- <el-tag size="small" :type="planStatusType(row.status)">{{ row.status }}</el-tag>
- </template>
- </el-table-column>
- <el-table-column prop="estimated_points" label="数据点" width="80" />
- <el-table-column prop="estimated_time_min" label="预估(分钟)" width="100" />
- <el-table-column prop="result_count" label="结果数" width="80" />
- <el-table-column prop="updated_at" label="更新时间" width="160">
- <template #default="{ row }">{{ formatDate(row.updated_at) }}</template>
- </el-table-column>
- </el-table>
- </div>
- <!-- Create Plan Dialog with Visual Variable Editor -->
- <el-dialog v-model="showCreatePlan" title="新建仿真方案" width="900px" top="5vh">
- <el-form :model="planForm" label-width="100px">
- <el-row :gutter="12">
- <el-col :span="14">
- <el-form-item label="方案名称">
- <el-input v-model="planForm.name" placeholder="例如:气隙扫描 0.6-1.5mm" />
- </el-form-item>
- </el-col>
- <el-col :span="10">
- <el-form-item label="模型路径">
- <el-input v-model="planForm.model_path" :placeholder="project?.model_path || 'models/xxx.mot'" />
- </el-form-item>
- </el-col>
- </el-row>
- <el-form-item label="备注">
- <el-input v-model="planForm.notes" type="textarea" :rows="1" />
- </el-form-item>
- </el-form>
- <el-divider content-position="left">扫描变量</el-divider>
- <div class="plan-editor-toolbar">
- <el-button type="primary" size="small" @click="addVariable">
- <el-icon><Plus /></el-icon> 添加变量
- </el-button>
- <el-button type="success" size="small" @click="generateFromRules" :loading="generating">
- <el-icon><MagicStick /></el-icon> 规则生成
- </el-button>
- <el-button size="small" @click="clearVariables">清空全部</el-button>
- <div class="plan-estimate">
- <el-tag type="info">总数据点:<b>{{ estimatedPoints }}</b></el-tag>
- <el-tag type="warning" style="margin-left: 8px">预估时间:<b>{{ estimatedTimeMin }} 分钟</b></el-tag>
- </div>
- </div>
- <el-table :data="planVariables" size="small" border class="variable-table" max-height="320">
- <el-table-column label="#" type="index" width="40" align="center" />
- <el-table-column label="参数" width="200">
- <template #default="{ row }">
- <el-select v-model="row.name" size="small" filterable @change="onParamChange(row)" style="width: 100%">
- <el-option
- v-for="p in availableParams(row.name)"
- :key="p.name"
- :label="`${p.display_name} (${p.unit})`"
- :value="p.name"
- >
- <span style="float: left">{{ p.display_name }}</span>
- <span style="float: right; color: #8492a6; font-size: 12px">{{ p.category }} / {{ p.unit }}</span>
- </el-option>
- </el-select>
- </template>
- </el-table-column>
- <el-table-column label="模式" width="90" align="center">
- <template #default="{ row }">
- <el-radio-group v-model="row.mode" size="small" @change="onModeChange(row)">
- <el-radio-button label="range">范围</el-radio-button>
- <el-radio-button label="values">枚举</el-radio-button>
- </el-radio-group>
- </template>
- </el-table-column>
- <el-table-column label="起始" width="90">
- <template #default="{ row }">
- <el-input-number v-if="row.mode === 'range'" v-model="row.start" :step="row.step || 0.1" :precision="4" controls-position="right" size="small" style="width: 100%" @change="updateValues(row)" />
- <span v-else style="color: #c0c4cc">-</span>
- </template>
- </el-table-column>
- <el-table-column label="终止" width="90">
- <template #default="{ row }">
- <el-input-number v-if="row.mode === 'range'" v-model="row.stop" :step="row.step || 0.1" :precision="4" controls-position="right" size="small" style="width: 100%" @change="updateValues(row)" />
- <span v-else style="color: #c0c4cc">-</span>
- </template>
- </el-table-column>
- <el-table-column label="步长" width="80">
- <template #default="{ row }">
- <el-input-number v-if="row.mode === 'range'" v-model="row.step" :min="0.001" :precision="4" controls-position="right" size="small" style="width: 100%" @change="updateValues(row)" />
- <span v-else style="color: #c0c4cc">-</span>
- </template>
- </el-table-column>
- <el-table-column label="枚举值">
- <template #default="{ row }">
- <el-input
- v-if="row.mode === 'values'"
- v-model="row.values_text"
- size="small"
- placeholder="例如:0.6, 1.0, 1.5"
- @change="parseValues(row)"
- />
- <span v-else style="font-size: 12px; color: #606266">{{ row.values?.join(', ') || '' }}</span>
- </template>
- </el-table-column>
- <el-table-column label="数量" width="60" align="center">
- <template #default="{ row }">
- <el-tag size="small" :type="row.values?.length > 0 ? 'success' : 'info'">{{ row.values?.length || 0 }}</el-tag>
- </template>
- </el-table-column>
- <el-table-column label="操作" width="60" align="center" fixed="right">
- <template #default="{ $index }">
- <el-button size="small" text type="danger" @click="removeVariable($index)">
- <el-icon><Delete /></el-icon>
- </el-button>
- </template>
- </el-table-column>
- </el-table>
- <div v-if="planVariables.length === 0" style="text-align: center; padding: 24px; color: #909399">
- 暂无扫描变量。点击"添加变量"或"规则生成"开始配置。
- </div>
- <template #footer>
- <el-button @click="showCreatePlan = false">取消</el-button>
- <el-button type="primary" @click="createPlan" :loading="creating" :disabled="planVariables.length === 0">
- Create Plan ({{ estimatedPoints }} points)
- </el-button>
- </template>
- </el-dialog>
- </div>
- </template>
- <script setup lang="ts">
- import { ref, computed, onMounted } from 'vue'
- import { useRoute, useRouter } from 'vue-router'
- import { ElMessage } from 'element-plus'
- import { ArrowLeft, Plus, Delete, MagicStick } from '@element-plus/icons-vue'
- import { projectApi, planApi, generationApi } from '@/api'
- const route = useRoute()
- const router = useRouter()
- const loading = ref(false)
- const creating = ref(false)
- const generating = ref(false)
- const project = ref<any>(null)
- const plans = ref<any[]>([])
- const showCreatePlan = ref(false)
- // Plan form
- const planForm = ref({ name: '', model_path: '', notes: '' })
- // Scan parameter registry (loaded from API)
- const scanParams = ref<any[]>([])
- // Plan variables (editable table rows)
- interface PlanVariable {
- name: string
- display_name: string
- unit: string
- mode: 'range' | 'values'
- start: number
- stop: number
- step: number
- values: number[]
- values_text: string
- }
- const planVariables = ref<PlanVariable[]>([])
- // Load scan parameter registry
- async function loadScanParams() {
- try {
- const res = await generationApi.listScanParameters()
- scanParams.value = res.data.parameters
- } catch (e: any) {
- ElMessage.error('加载扫描参数失败:' + e.message)
- }
- }
- // Filter available params (exclude already used, except current row)
- function availableParams(currentName: string) {
- const used = new Set(planVariables.value.filter(v => v.name !== currentName).map(v => v.name))
- return scanParams.value.filter(p => !used.has(p.name))
- }
- // Add a new variable row
- function addVariable() {
- if (scanParams.value.length === 0) {
- ElMessage.warning('扫描参数尚未加载')
- return
- }
- const available = availableParams('')
- if (available.length === 0) {
- ElMessage.warning('所有参数已添加')
- return
- }
- const p = available[0]
- planVariables.value.push({
- name: p.name,
- display_name: p.display_name,
- unit: p.unit,
- mode: 'range',
- start: p.default_start,
- stop: p.default_stop,
- step: p.default_step,
- values: generateValues(p.default_start, p.default_stop, p.default_step),
- values_text: '',
- })
- }
- // Remove a variable
- function removeVariable(index: number) {
- planVariables.value.splice(index, 1)
- }
- // Clear all variables
- function clearVariables() {
- planVariables.value = []
- }
- // On parameter change: update metadata and defaults
- function onParamChange(row: PlanVariable) {
- const p = scanParams.value.find(sp => sp.name === row.name)
- if (p) {
- row.display_name = p.display_name
- row.unit = p.unit
- row.start = p.default_start
- row.stop = p.default_stop
- row.step = p.default_step
- if (row.mode === 'range') {
- row.values = generateValues(row.start, row.stop, row.step)
- }
- }
- }
- // On mode change
- function onModeChange(row: PlanVariable) {
- if (row.mode === 'range') {
- row.values = generateValues(row.start, row.stop, row.step)
- row.values_text = ''
- } else {
- row.values_text = row.values.join(', ')
- }
- }
- // Update values from range inputs
- function updateValues(row: PlanVariable) {
- if (row.mode === 'range') {
- row.values = generateValues(row.start, row.stop, row.step)
- }
- }
- // Parse explicit values from text
- function parseValues(row: PlanVariable) {
- if (row.mode !== 'values') return
- const parts = row.values_text.split(/[,;\s]+/).filter(s => s.trim() !== '')
- const vals: number[] = []
- for (const p of parts) {
- const n = parseFloat(p)
- if (!isNaN(n)) vals.push(n)
- }
- row.values = vals
- }
- // Generate evenly spaced values
- function generateValues(start: number, stop: number, step: number): number[] {
- if (!step || step <= 0 || start === undefined || stop === undefined) return []
- const count = Math.floor((stop - start) / step + 1e-9) + 1
- if (count <= 0 || count > 1000) return []
- const vals = []
- for (let i = 0; i < count; i++) {
- vals.push(parseFloat((start + i * step).toFixed(6)))
- }
- // Ensure stop is included
- if (vals.length > 0 && Math.abs(vals[vals.length - 1] - stop) > 1e-9) {
- vals.push(parseFloat(stop.toFixed(6)))
- }
- return vals
- }
- // Estimate total points (Cartesian product)
- const estimatedPoints = computed(() => {
- let total = 1
- for (const v of planVariables.value) {
- const count = v.values?.length || 0
- total *= count > 0 ? count : 1
- }
- return planVariables.value.length > 0 ? total : 0
- })
- const estimatedTimeMin = computed(() => estimatedPoints.value * 3)
- // Generate plan from rules (backend rule engine)
- async function generateFromRules() {
- if (!project.value) {
- ElMessage.warning('项目未加载')
- return
- }
- generating.value = true
- try {
- const res = await generationApi.generatePlanForProject(
- Number(route.params.id),
- { model_path: project.value.model_path || '' }
- )
- const generated = res.data
- planForm.value.name = generated.name
- planForm.value.model_path = generated.model_path || project.value.model_path || ''
- // Convert generated variables to editor format
- planVariables.value = (generated.variables || []).map((v: any) => ({
- name: v.name,
- display_name: v.display_name || v.name,
- unit: v.unit || '',
- mode: 'range',
- start: v.start ?? v.values?.[0] ?? 0,
- stop: v.stop ?? v.values?.[v.values.length - 1] ?? 0,
- step: v.step ?? 1,
- values: v.values || [],
- values_text: '',
- }))
- ElMessage.success(`已生成方案,共 ${generated.estimated_points} 个数据点`)
- } catch (e: any) {
- ElMessage.error('Failed to generate plan: ' + e.message)
- } finally {
- generating.value = false
- }
- }
- async function loadProject() {
- loading.value = true
- try {
- const id = Number(route.params.id)
- const res = await projectApi.get(id)
- project.value = res.data
- const plansRes = await planApi.list({ project_id: id, limit: 100 })
- plans.value = plansRes.data.items
- } catch (e: any) {
- ElMessage.error('加载项目失败:' + e.message)
- } finally {
- loading.value = false
- }
- }
- async function createPlan() {
- if (!planForm.value.name.trim()) {
- ElMessage.warning('请输入方案名称')
- return
- }
- if (planVariables.value.length === 0) {
- ElMessage.warning('请至少添加一个扫描变量')
- return
- }
- // Validate all variables have values
- for (const v of planVariables.value) {
- if (!v.values || v.values.length === 0) {
- ElMessage.warning(`变量 "${v.name}" 没有有效值`)
- return
- }
- }
- creating.value = true
- try {
- // Build plan_data compatible with src/plan_schema.py
- const variables = planVariables.value.map(v => ({
- name: v.name,
- display_name: v.display_name,
- unit: v.unit,
- values: v.values,
- }))
- const planData = {
- model_path: planForm.value.model_path || project.value?.model_path || '',
- topology: project.value?.topology || 'SSSR',
- variables,
- cases: [],
- }
- await planApi.create({
- project_id: Number(route.params.id),
- name: planForm.value.name,
- plan_data: planData,
- notes: planForm.value.notes,
- })
- ElMessage.success(`方案已创建,共 ${estimatedPoints.value} 个数据点`)
- showCreatePlan.value = false
- resetPlanForm()
- await loadProject()
- } catch (e: any) {
- ElMessage.error('创建方案失败:' + e.message)
- } finally {
- creating.value = false
- }
- }
- function resetPlanForm() {
- planForm.value = { name: '', model_path: '', notes: '' }
- planVariables.value = []
- }
- function goToPlan(row: any) {
- router.push(`/plans/${row.id}`)
- }
- function planStatusType(status: string) {
- const map: Record<string, string> = { draft: 'info', confirmed: 'warning', executing: 'primary', completed: 'success', failed: 'danger' }
- return map[status] || 'info'
- }
- function formatDate(d: string) {
- return new Date(d).toLocaleString()
- }
- onMounted(() => {
- loadProject()
- loadScanParams()
- })
- </script>
- <style scoped>
- .plan-editor-toolbar {
- display: flex;
- align-items: center;
- margin-bottom: 12px;
- gap: 8px;
- }
- .plan-estimate {
- margin-left: auto;
- }
- .variable-table {
- margin-bottom: 12px;
- }
- </style>
|