PlanDetail.vue 67 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769
  1. <template>
  2. <div class="plan-detail page-container">
  3. <!-- Page Header -->
  4. <PageHeader :title="plan.name || '方案详情'" :subtitle="subtitleText">
  5. <template #actions>
  6. <el-button @click="goBack">
  7. <el-icon><ArrowLeft /></el-icon> 返回
  8. </el-button>
  9. <el-button v-if="!isRunning" type="primary" @click="startSimulation" :loading="starting || preflightLoading">
  10. <el-icon><VideoPlay /></el-icon> 启动仿真
  11. </el-button>
  12. <el-button v-else type="danger" @click="stopSimulation" :loading="stopping">
  13. <el-icon><VideoPause /></el-icon> 停止仿真
  14. </el-button>
  15. <el-button @click="showUpload = true">
  16. <el-icon><Upload /></el-icon> 上传结果
  17. </el-button>
  18. </template>
  19. </PageHeader>
  20. <!-- Running Progress Banner -->
  21. <div v-if="isRunning" class="progress-banner">
  22. <div class="progress-info">
  23. <span class="status-dot running" />
  24. <span class="progress-title">仿真运行中</span>
  25. <span class="progress-detail">
  26. {{ activeTask?.completed_points || 0 }} / {{ activeTask?.total_points || 0 }} 点
  27. · {{ activeTask?.progress || 0 }}%
  28. · 预计剩余 {{ estimatedRemaining }}
  29. </span>
  30. </div>
  31. <el-progress
  32. :percentage="activeTask?.progress || 0"
  33. :stroke-width="6"
  34. class="progress-bar"
  35. />
  36. </div>
  37. <!-- Main Tabs -->
  38. <el-tabs v-model="activeTab" class="plan-tabs">
  39. <!-- ========== Tab 1: Overview ========== -->
  40. <el-tab-pane label="概览" name="overview">
  41. <!-- Stat Cards -->
  42. <el-row :gutter="16" class="stat-row">
  43. <el-col :span="6">
  44. <StatCard label="扫描变量数" :value="scanVariables.length" unit="个" />
  45. </el-col>
  46. <el-col :span="6">
  47. <StatCard label="固定参数" :value="fixedParams.length" unit="项" />
  48. </el-col>
  49. <el-col :span="6">
  50. <StatCard label="预计数据点" :value="estimatedPoints" unit="点" />
  51. </el-col>
  52. <el-col :span="6">
  53. <StatCard
  54. label="预计耗时"
  55. :value="estimatedTimeMin"
  56. unit="分钟"
  57. :valueColor="estimatedTimeMin > 120 ? 'var(--color-warning)' : ''"
  58. />
  59. <div class="est-basis">{{ estimatedTimeBasis }}</div>
  60. </el-col>
  61. </el-row>
  62. <!-- AI Design Rationale -->
  63. <SectionCard title="AI 设计思路" v-if="plan.ai_reasoning">
  64. <div class="ai-reasoning">{{ plan.ai_reasoning }}</div>
  65. </SectionCard>
  66. <!-- Acceptance Criteria -->
  67. <SectionCard title="验收标准">
  68. <div v-if="acceptanceItems.length" class="criteria-list">
  69. <div v-for="(c, idx) in acceptanceItems" :key="idx" class="criteria-item">
  70. <el-icon class="criteria-icon"><CircleCheck /></el-icon>
  71. <span class="criteria-text">{{ c.text }}</span>
  72. <el-tag size="small" effect="plain" :type="c.origin === 'AI 设定' ? 'primary' : 'warning'" class="criteria-origin">{{ c.origin }}</el-tag>
  73. </div>
  74. </div>
  75. <el-empty v-else description="暂无验收标准,可在边界条件中设置目标转矩/效率/损耗等" :image-size="60" />
  76. </SectionCard>
  77. <!-- Latest Results Summary -->
  78. <SectionCard
  79. v-if="results.length"
  80. title="最新结果摘要"
  81. :subtitle="`共 ${results.length} 个数据点`"
  82. >
  83. <template #actions>
  84. <el-button size="small" text type="primary" @click="activeTab = 'results'">
  85. 查看全部 <el-icon><ArrowRight /></el-icon>
  86. </el-button>
  87. </template>
  88. <el-table :data="results.slice(0, 5)" size="small" border>
  89. <el-table-column label="#" type="index" width="50" />
  90. <el-table-column v-for="col in coreResultColumns" :key="col.key" :prop="col.key" :label="col.label" min-width="110" align="right">
  91. <template #default="{ row }">
  92. <el-tooltip
  93. v-if="col.key === 'status' && isFailed(row) && row.error_message"
  94. :content="row.error_message"
  95. placement="top"
  96. :show-after="100"
  97. raw-content
  98. >
  99. <span class="text-danger status-failed-cell">{{ formatCell(row[col.key]) }}</span>
  100. </el-tooltip>
  101. <span v-else :class="{ 'text-danger': isFailed(row) }">{{ formatCell(row[col.key]) }}</span>
  102. </template>
  103. </el-table-column>
  104. </el-table>
  105. </SectionCard>
  106. <!-- Quick Actions -->
  107. <SectionCard title="快捷操作">
  108. <div class="quick-actions">
  109. <div class="quick-action-card" @click="activeTab = 'parameters'">
  110. <el-icon :size="24" color="var(--color-primary)"><Setting /></el-icon>
  111. <div class="qa-title">编辑参数</div>
  112. <div class="qa-desc">扫描变量、边界条件、固定参数</div>
  113. </div>
  114. <div class="quick-action-card" @click="activeTab = 'results'">
  115. <el-icon :size="24" color="var(--color-success)"><DataAnalysis /></el-icon>
  116. <div class="qa-title">查看结果</div>
  117. <div class="qa-desc">{{ results.length }} 个数据点</div>
  118. </div>
  119. <div class="quick-action-card" @click="activeTab = 'ai'">
  120. <el-icon :size="24" color="#7c3aed"><MagicStick /></el-icon>
  121. <div class="qa-title">AI 闭环</div>
  122. <div class="qa-desc">分析、迭代、经验提取</div>
  123. </div>
  124. <div class="quick-action-card" @click="downloadResults">
  125. <el-icon :size="24" color="var(--color-warning)"><Download /></el-icon>
  126. <div class="qa-title">下载结果</div>
  127. <div class="qa-desc">CSV / JSON 格式</div>
  128. </div>
  129. </div>
  130. </SectionCard>
  131. </el-tab-pane>
  132. <!-- ========== Tab 2: Parameters ========== -->
  133. <el-tab-pane label="方案参数" name="parameters">
  134. <!-- Scan Variables -->
  135. <SectionCard title="扫描变量">
  136. <template #actions>
  137. <el-button v-if="scanVarsDirty" size="small" type="warning" :loading="savingVars" @click="saveScanVariables">
  138. 保存修改
  139. </el-button>
  140. <el-button size="small" type="primary" @click="showAddVar = true">
  141. <el-icon><Plus /></el-icon> 添加变量
  142. </el-button>
  143. </template>
  144. <el-table :data="scanVariables" size="small" border>
  145. <el-table-column prop="name" label="变量名" min-width="140" />
  146. <el-table-column prop="name_cn" label="中文名" min-width="120" />
  147. <el-table-column label="取值" min-width="200">
  148. <template #default="{ row }">
  149. <span v-if="Array.isArray(row.values) && row.values.length" class="var-values">{{ row.values.join(' / ') }}</span>
  150. <span v-else>{{ row.min_value }} ~ {{ row.max_value }} (步长 {{ row.step }})</span>
  151. </template>
  152. </el-table-column>
  153. <el-table-column prop="unit" label="单位" width="80" align="center" />
  154. <el-table-column label="点数" width="80" align="center">
  155. <template #default="{ row }">{{ calcPoints(row) }}</template>
  156. </el-table-column>
  157. <el-table-column label="操作" width="100" fixed="right">
  158. <template #default="{ row, $index }">
  159. <el-button size="small" link type="danger" @click="removeVariable($index)">删除</el-button>
  160. </template>
  161. </el-table-column>
  162. </el-table>
  163. <el-empty v-if="!scanVariables.length" description="暂无扫描变量,点击添加" :image-size="60" />
  164. </SectionCard>
  165. <!-- Boundary Conditions -->
  166. <SectionCard title="边界条件">
  167. <template #actions>
  168. <template v-if="!bcEditing">
  169. <el-button size="small" @click="startBcEdit">
  170. <el-icon><Edit /></el-icon> 编辑
  171. </el-button>
  172. </template>
  173. <template v-else>
  174. <el-button size="small" @click="bcEditing = false">取消</el-button>
  175. <el-button size="small" type="primary" :loading="bcSaving" @click="saveBcEdit">保存</el-button>
  176. </template>
  177. </template>
  178. <el-descriptions :column="3" border size="small">
  179. <el-descriptions-item
  180. v-for="bc in boundaryConditions"
  181. :key="bc.key"
  182. :label="bc.label"
  183. >
  184. <template #label>
  185. <span>{{ bc.label }}</span>
  186. <el-tag
  187. v-if="bc.source === 'user'"
  188. size="small" effect="plain" type="success" class="bc-source-tag"
  189. >用户指定</el-tag>
  190. <el-tag
  191. v-else-if="bc.source === 'ai'"
  192. size="small" effect="plain" type="primary" class="bc-source-tag"
  193. >AI 补充</el-tag>
  194. <el-tag
  195. v-else-if="bc.source === 'edited'"
  196. size="small" effect="plain" type="warning" class="bc-source-tag"
  197. >已修改</el-tag>
  198. </template>
  199. <template v-if="bcEditing">
  200. <el-select
  201. v-if="bc.type === 'enum'"
  202. v-model="bcEditForm[bc.key]"
  203. clearable
  204. placeholder="未设置"
  205. size="small"
  206. style="width: 100%"
  207. >
  208. <el-option v-for="opt in bc.options" :key="opt" :label="opt" :value="opt" />
  209. </el-select>
  210. <el-input-number
  211. v-else
  212. v-model="bcEditForm[bc.key]"
  213. :precision="bc.type === 'int' ? 0 : 2"
  214. :placeholder="'未设置'"
  215. controls-position="right"
  216. size="small"
  217. style="width: 100%"
  218. />
  219. </template>
  220. <template v-else>
  221. {{ bc.value !== undefined && bc.value !== null && bc.value !== '' ? bc.value + (bc.unit ? ' ' + bc.unit : '') : '未设置' }}
  222. </template>
  223. </el-descriptions-item>
  224. </el-descriptions>
  225. </SectionCard>
  226. <!-- Fixed Parameters (collapsible) -->
  227. <SectionCard title="固定参数">
  228. <template #actions>
  229. <span class="fixed-summary">
  230. {{ modifiedFixedParams.length }} 项已修改 / 共 {{ fixedParams.length }} 项
  231. </span>
  232. <el-button size="small" @click="showAllFixed = !showAllFixed">
  233. {{ showAllFixed ? '只看修改项' : '全部展开' }}
  234. </el-button>
  235. <template v-if="!fixedEditing">
  236. <el-button size="small" @click="startFixedEdit">
  237. <el-icon><Edit /></el-icon> 编辑
  238. </el-button>
  239. </template>
  240. <template v-else>
  241. <el-button size="small" @click="fixedEditing = false">取消</el-button>
  242. <el-button size="small" type="primary" :loading="fixedSaving" @click="saveFixedParams">保存</el-button>
  243. </template>
  244. </template>
  245. <!-- Modified params always visible -->
  246. <div v-if="modifiedFixedParams.length" class="fixed-modified">
  247. <div class="fixed-group-title">
  248. <el-icon><Star /></el-icon> 已修改参数
  249. </div>
  250. <el-table :data="modifiedFixedParams" size="small" border>
  251. <el-table-column prop="name_cn" label="参数" min-width="140" />
  252. <el-table-column prop="name" label="Motor-CAD 变量" min-width="160">
  253. <template #default="{ row }">
  254. <code class="var-code">{{ row.motorcad_var || row.name }}</code>
  255. </template>
  256. </el-table-column>
  257. <el-table-column label="当前值" width="120" align="right">
  258. <template #default="{ row }">
  259. <span class="modified-value">{{ row.value }}</span>
  260. </template>
  261. </el-table-column>
  262. <el-table-column label="默认值" width="100" align="center">
  263. <template #default="{ row }">
  264. <span class="text-muted">{{ getDefaultValue(row.name) }}</span>
  265. </template>
  266. </el-table-column>
  267. <el-table-column prop="unit" label="单位" width="70" align="center" />
  268. <el-table-column prop="category_cn" label="分类" width="100" align="center" />
  269. </el-table>
  270. </div>
  271. <!-- All params (collapsible by category) -->
  272. <div v-if="showAllFixed" class="fixed-all">
  273. <div v-for="cat in fixedCategories" :key="cat" class="fixed-category">
  274. <div class="fixed-category-title" @click="toggleCategory(cat)">
  275. <el-icon class="cat-arrow">
  276. <ArrowDown v-if="expandedCategories[cat] !== false" />
  277. <ArrowRight v-else />
  278. </el-icon>
  279. {{ cat }}
  280. <span class="cat-count">({{ fixedParamsByCategory(cat).length }})</span>
  281. </div>
  282. <el-table
  283. v-if="expandedCategories[cat] !== false"
  284. :data="fixedParamsByCategory(cat)"
  285. size="small"
  286. border
  287. >
  288. <el-table-column prop="name_cn" label="参数" min-width="130" />
  289. <el-table-column prop="name" label="变量名" min-width="150">
  290. <template #default="{ row }">
  291. <code class="var-code">{{ row.motorcad_var || row.name }}</code>
  292. <el-tag v-if="row.description_cn?.includes('需确认')" size="small" type="warning" effect="plain">需确认</el-tag>
  293. </template>
  294. </el-table-column>
  295. <el-table-column label="值" width="140" align="right">
  296. <template #default="{ row }">
  297. <template v-if="fixedEditing">
  298. <el-select
  299. v-if="typeof row.value === 'boolean' || row.value === 'True' || row.value === 'False'"
  300. v-model="fixedEditValues[row.name]"
  301. size="small"
  302. style="width: 110px"
  303. >
  304. <el-option label="True" value="True" />
  305. <el-option label="False" value="False" />
  306. </el-select>
  307. <el-input-number
  308. v-else-if="!isNaN(Number(row.value)) && String(row.value).trim() !== ''"
  309. v-model="fixedEditValues[row.name]"
  310. :precision="Number.isInteger(Number(row.value)) ? 0 : 4"
  311. controls-position="right"
  312. size="small"
  313. style="width: 130px"
  314. />
  315. <el-input v-else v-model="fixedEditValues[row.name]" size="small" style="width: 130px" />
  316. </template>
  317. <span v-else :class="{ 'modified-value': isFixedModified(row) }">{{ row.value }}</span>
  318. </template>
  319. </el-table-column>
  320. <el-table-column prop="unit" label="单位" width="70" align="center" />
  321. <el-table-column prop="description_cn" label="说明" min-width="140" show-overflow-tooltip />
  322. </el-table>
  323. </div>
  324. </div>
  325. <el-empty
  326. v-if="!modifiedFixedParams.length && !showAllFixed"
  327. description="所有参数均为默认值"
  328. :image-size="60"
  329. />
  330. </SectionCard>
  331. </el-tab-pane>
  332. <!-- ========== Tab 3: Results ========== -->
  333. <el-tab-pane :label="`仿真结果 (${results.length})`" name="results">
  334. <SectionCard title="仿真结果" no-padding>
  335. <template #actions>
  336. <el-button size="small" @click="openColumnSettings" :disabled="!results.length">
  337. <el-icon><Setting /></el-icon> 列设置
  338. </el-button>
  339. <el-button size="small" @click="downloadResults" :disabled="!results.length">
  340. <el-icon><Download /></el-icon> 下载 CSV
  341. </el-button>
  342. <el-button size="small" type="primary" plain @click="exportExcel" :disabled="!results.length">
  343. <el-icon><Download /></el-icon> 导出 Excel
  344. </el-button>
  345. </template>
  346. <el-table :data="results" size="small" border max-height="600">
  347. <el-table-column label="#" type="index" width="50" fixed="left" />
  348. <el-table-column
  349. v-for="col in displayColumns"
  350. :key="col.key"
  351. :prop="col.key"
  352. :label="col.label"
  353. :min-width="col.width || 110"
  354. align="right"
  355. >
  356. <template #default="{ row }">
  357. <el-tooltip
  358. v-if="col.key === 'status' && isFailed(row) && row.error_message"
  359. :content="row.error_message"
  360. placement="top"
  361. :show-after="100"
  362. raw-content
  363. >
  364. <span class="text-danger status-failed-cell">{{ formatCell(row[col.key]) }}</span>
  365. </el-tooltip>
  366. <span v-else :class="{ 'text-danger': isFailed(row) }">{{ formatCell(row[col.key]) }}</span>
  367. </template>
  368. </el-table-column>
  369. </el-table>
  370. <el-empty v-if="!results.length" description="暂无仿真结果" :image-size="80" />
  371. </SectionCard>
  372. </el-tab-pane>
  373. <!-- ========== Tab 4: AI Loop ========== -->
  374. <el-tab-pane label="AI 闭环" name="ai">
  375. <!-- Step indicator -->
  376. <el-steps :active="aiStep" finish-status="success" align-center class="ai-steps">
  377. <el-step title="AI 分析" description="分析当前结果" />
  378. <el-step title="生成迭代" description="生成下一轮方案" />
  379. <el-step title="提取经验" description="沉淀到经验库" />
  380. </el-steps>
  381. <!-- Step 1: AI Analysis -->
  382. <SectionCard title="1. AI 结果分析">
  383. <template #actions>
  384. <el-button
  385. size="small"
  386. type="primary"
  387. :loading="analyzing"
  388. @click="runAIAnalysis"
  389. :disabled="!results.length"
  390. >
  391. <el-icon><MagicStick /></el-icon> 运行分析
  392. </el-button>
  393. </template>
  394. <div v-if="aiAnalysis" class="ai-analysis-content">
  395. <div class="ai-section">
  396. <div class="ai-section-title">关键发现</div>
  397. <div class="ai-section-body">{{ aiAnalysis.findings || '暂无' }}</div>
  398. </div>
  399. <div class="ai-section">
  400. <div class="ai-section-title">优化建议</div>
  401. <div class="ai-section-body">{{ aiAnalysis.suggestions || '暂无' }}</div>
  402. </div>
  403. </div>
  404. <el-empty v-else description="点击运行 AI 分析当前仿真结果" :image-size="60" />
  405. </SectionCard>
  406. <!-- Step 2: Generate Iteration -->
  407. <SectionCard title="2. 生成下一轮迭代方案">
  408. <template #actions>
  409. <el-button
  410. size="small"
  411. type="primary"
  412. :loading="generating"
  413. @click="generateIteration"
  414. :disabled="!aiAnalysis"
  415. >
  416. <el-icon><RefreshRight /></el-icon> 生成迭代方案
  417. </el-button>
  418. </template>
  419. <div v-if="iterationPlan" class="iteration-content">
  420. <el-alert
  421. :title="`迭代方案已生成:${iterationPlan.name || '新方案'}`"
  422. type="success"
  423. :closable="false"
  424. show-icon
  425. />
  426. <div class="mt-3">
  427. <el-button type="primary" @click="viewIteration">查看迭代方案</el-button>
  428. </div>
  429. </div>
  430. <el-empty v-else description="先运行 AI 分析,再生成迭代方案" :image-size="60" />
  431. </SectionCard>
  432. <!-- Step 3: Extract Experience -->
  433. <SectionCard title="3. 提取经验入库">
  434. <template #actions>
  435. <el-button
  436. size="small"
  437. type="primary"
  438. :loading="extracting"
  439. @click="extractExperience"
  440. :disabled="!results.length"
  441. >
  442. <el-icon><Collection /></el-icon> 提取经验
  443. </el-button>
  444. </template>
  445. <div v-if="experienceExtracted" class="experience-content">
  446. <el-alert title="经验已成功提取到经验库" type="success" :closable="false" show-icon />
  447. <div class="mt-3">
  448. <el-button @click="$router.push('/experience')">查看经验库</el-button>
  449. </div>
  450. </div>
  451. <el-empty v-else description="将本次仿真的关键参数和结论提取为经验" :image-size="60" />
  452. </SectionCard>
  453. </el-tab-pane>
  454. </el-tabs>
  455. <!-- Add Variable Dialog -->
  456. <el-dialog v-model="showAddVar" title="添加扫描变量" width="500px">
  457. <el-form :model="newVar" label-width="100px">
  458. <el-form-item label="变量名">
  459. <el-select v-model="newVar.name" filterable placeholder="选择 Motor-CAD 变量" @change="onVarSelect">
  460. <el-option
  461. v-for="v in availableScanParams"
  462. :key="v.name"
  463. :label="`${v.name_cn} (${v.name}) -> ${v.motorcad_var}`"
  464. :value="v.name"
  465. />
  466. </el-select>
  467. </el-form-item>
  468. <el-form-item label="最小值">
  469. <el-input-number v-model="newVar.min_value" :precision="4" controls-position="right" style="width: 100%" />
  470. </el-form-item>
  471. <el-form-item label="最大值">
  472. <el-input-number v-model="newVar.max_value" :precision="4" controls-position="right" style="width: 100%" />
  473. </el-form-item>
  474. <el-form-item label="步长">
  475. <el-input-number v-model="newVar.step" :precision="4" :min="0.0001" controls-position="right" style="width: 100%" />
  476. </el-form-item>
  477. </el-form>
  478. <template #footer>
  479. <el-button @click="showAddVar = false">取消</el-button>
  480. <el-button type="primary" @click="confirmAddVar">确认添加</el-button>
  481. </template>
  482. </el-dialog>
  483. <!-- Upload Results Dialog -->
  484. <el-dialog v-model="showUpload" title="上传仿真结果" width="500px">
  485. <el-upload
  486. drag
  487. action="#"
  488. :auto-upload="false"
  489. :on-change="onFileChange"
  490. accept=".csv,.json"
  491. >
  492. <el-icon class="el-icon--upload"><UploadFilled /></el-icon>
  493. <div class="el-upload__text">拖拽文件到此处,或<em>点击上传</em></div>
  494. <template #tip>
  495. <div class="el-upload__tip">支持 CSV / JSON 格式的仿真结果文件</div>
  496. </template>
  497. </el-upload>
  498. <template #footer>
  499. <el-button @click="showUpload = false">取消</el-button>
  500. <el-button type="primary" :loading="uploading" @click="confirmUpload" :disabled="!uploadFile">
  501. 上传
  502. </el-button>
  503. </template>
  504. </el-dialog>
  505. <!-- Pre-flight Checklist Dialog -->
  506. <el-dialog v-model="showPreflight" title="仿真前检查" width="560px" :close-on-click-modal="false">
  507. <div v-if="preflight" class="preflight-body">
  508. <div v-for="c in preflight.checks" :key="c.key" class="preflight-item" :class="`is-${c.status}`">
  509. <el-icon class="preflight-icon">
  510. <CircleCheckFilled v-if="c.status === 'pass'" />
  511. <WarningFilled v-else-if="c.status === 'warn'" />
  512. <CircleCloseFilled v-else />
  513. </el-icon>
  514. <div class="preflight-content">
  515. <div class="preflight-label">{{ PREFLIGHT_LABELS[c.key] || c.key }}</div>
  516. <div class="preflight-message">{{ preflightMessage(c) }}</div>
  517. <el-button
  518. v-if="c.key === 'model_path' && c.status === 'fail' && c.fixable"
  519. size="small"
  520. type="primary"
  521. class="preflight-fix"
  522. :loading="applyingDefaultModel"
  523. @click="applyDefaultModel(c.default_model)"
  524. >
  525. 使用拓扑默认模型({{ c.default_model }})
  526. </el-button>
  527. </div>
  528. </div>
  529. <el-divider />
  530. <div class="thermal-mode-row">
  531. <span class="thermal-mode-label">热仿真模式</span>
  532. <el-radio-group v-model="thermalMode" size="small">
  533. <el-radio value="off">仅电磁(最快)</el-radio>
  534. <el-radio value="steady">电磁+稳态热(默认)</el-radio>
  535. <el-radio value="coupled">磁热耦合(精算)</el-radio>
  536. </el-radio-group>
  537. </div>
  538. <el-alert v-if="!preflight.ok" type="error" :closable="false" style="margin-top: 12px">
  539. 存在未通过的检查项,请先处理后再启动仿真。
  540. </el-alert>
  541. </div>
  542. <template #footer>
  543. <el-button @click="showPreflight = false">取消</el-button>
  544. <el-button type="primary" :disabled="!preflight?.ok" :loading="starting" @click="confirmStartSimulation">
  545. 确认启动
  546. </el-button>
  547. </template>
  548. </el-dialog>
  549. <!-- Result Column Settings Dialog -->
  550. <el-dialog v-model="showColumnSettings" title="结果列设置" width="480px">
  551. <div class="column-settings-tip">勾选要在结果表中显示的列(默认显示核心指标与扫描变量)</div>
  552. <el-checkbox-group v-model="visibleColumnKeys" class="column-settings-group">
  553. <el-checkbox v-for="col in allResultColumns" :key="col.key" :label="col.key" class="column-settings-item">
  554. {{ col.label }}
  555. </el-checkbox>
  556. </el-checkbox-group>
  557. <template #footer>
  558. <el-button @click="resetColumns">恢复默认</el-button>
  559. <el-button type="primary" @click="confirmColumnSettings">确定</el-button>
  560. </template>
  561. </el-dialog>
  562. </div>
  563. </template>
  564. <script setup lang="ts">
  565. import { ref, reactive, computed, onMounted, onUnmounted, watch } from 'vue'
  566. import { useRoute, useRouter } from 'vue-router'
  567. import { ElMessage, ElMessageBox } from 'element-plus'
  568. import {
  569. ArrowLeft, VideoPlay, VideoPause, Upload, Plus, Edit,
  570. Download, DataAnalysis, MagicStick, Setting, CircleCheck,
  571. Star, ArrowDown, ArrowRight, RefreshRight, Collection, UploadFilled,
  572. CircleCheckFilled, WarningFilled, CircleCloseFilled,
  573. } from '@element-plus/icons-vue'
  574. import api from '@/api'
  575. import StatCard from '@/components/StatCard.vue'
  576. import SectionCard from '@/components/SectionCard.vue'
  577. import PageHeader from '@/components/PageHeader.vue'
  578. const route = useRoute()
  579. const router = useRouter()
  580. const planId = computed(() => Number(route.params.id))
  581. // State
  582. const plan = reactive<any>({})
  583. const results = ref<any[]>([])
  584. const activeTask = ref<any>(null)
  585. const activeTab = ref('overview')
  586. const isRunning = ref(false)
  587. const starting = ref(false)
  588. const stopping = ref(false)
  589. const analyzing = ref(false)
  590. const generating = ref(false)
  591. const extracting = ref(false)
  592. const uploading = ref(false)
  593. const uploadFile = ref<File | null>(null)
  594. const showAddVar = ref(false)
  595. const scanVarsDirty = ref(false)
  596. const savingVars = ref(false)
  597. const showUpload = ref(false)
  598. const showAllFixed = ref(true) // default fully expanded (user requirement 2026-09-03)
  599. const showPreflight = ref(false)
  600. const preflight = ref<any>(null)
  601. const preflightLoading = ref(false)
  602. const applyingDefaultModel = ref(false)
  603. const solveTimeStats = ref<any>(null)
  604. const aiStep = ref(0)
  605. const aiAnalysis = ref<any>(null)
  606. const iterationPlan = ref<any>(null)
  607. const experienceExtracted = ref(false)
  608. const newVar = reactive({
  609. name: '',
  610. name_cn: '',
  611. min_value: 0,
  612. max_value: 1,
  613. step: 0.1,
  614. unit: '',
  615. })
  616. const expandedCategories = reactive<Record<string, boolean>>({})
  617. // Scan parameter catalog (frontend copy; B3 will unify with backend)
  618. const SCAN_PARAM_CN: Record<string, string> = {
  619. Airgap: '气隙长度',
  620. Magnet_Length: '磁钢轴向长度',
  621. Magnet_Thickness: '磁钢径向厚度',
  622. 'Magnet_Arc_[ED]': '磁钢极弧角',
  623. RMSCurrent: '额定电流',
  624. Shaft_Speed: '额定转速',
  625. Slot_Depth: '槽深',
  626. Slot_Width: '槽宽',
  627. Tooth_Width: '齿宽',
  628. Turns_per_Coil: '每线圈匝数',
  629. Stator_Outer_Diameter: '定子外径',
  630. Stator_Inner_Diameter: '定子内径',
  631. Stator_Axial_Length: '定子轴向长度',
  632. Wire_Diameter: '线径',
  633. }
  634. const categoryCnMap: Record<string, string> = {
  635. Geometry: '几何尺寸',
  636. Stator: '定子参数',
  637. Rotor: '转子参数',
  638. Performance: '性能规格',
  639. Winding: '绕组参数',
  640. Material: '材料属性',
  641. Thermal: '热与冷却',
  642. Simulation: '仿真设置',
  643. System: '仿真设置',
  644. Mechanical: '其他',
  645. Electrical: '性能规格',
  646. General: '其他',
  647. }
  648. // BC field catalog loaded from the backend single-source (P1-1). The plan
  649. // boundary display renders from this canonical catalog instead of a local
  650. // hard-coded template that had drifted to a third key naming.
  651. const bcFieldCatalog = ref<any[]>([])
  652. // Fixed parameter default template (36 params)
  653. const ALL_FIXED_PARAM_TEMPLATE = [
  654. { name: 'Outer_Rotor_Diameter', motorcad_var: null, name_cn: '转子外径', unit: 'mm', value: 200.0, category: 'Geometry', description_cn: '转子背铁外径' },
  655. { name: 'Inner_Rotor_Diameter', motorcad_var: null, name_cn: '转子内径', unit: 'mm', value: 120.0, category: 'Geometry', description_cn: '转子背铁内径' },
  656. { name: 'Stator_Outer_Diameter', motorcad_var: null, name_cn: '定子外径', unit: 'mm', value: 198.0, category: 'Geometry', description_cn: '定子冲片外径' },
  657. { name: 'Stator_Inner_Diameter', motorcad_var: null, name_cn: '定子内径', unit: 'mm', value: 122.0, category: 'Geometry', description_cn: '定子冲片内径' },
  658. { name: 'Airgap', motorcad_var: 'Airgap', name_cn: '气隙长度', unit: 'mm', value: 1.0, category: 'Geometry', description_cn: '定转子之间气隙' },
  659. { name: 'Stator_Yoke_Thickness', motorcad_var: null, name_cn: '定子轭厚', unit: 'mm', value: 10.0, category: 'Stator', description_cn: '定子轭部径向厚度' },
  660. { name: 'Slot_Depth', motorcad_var: 'Slot_Depth', name_cn: '槽深', unit: 'mm', value: 7.0, category: 'Stator', description_cn: '定子槽深度' },
  661. { name: 'Slot_Width', motorcad_var: 'Slot_Width', name_cn: '槽宽', unit: 'mm', value: 8.5, category: 'Stator', description_cn: '定子槽口宽度' },
  662. { name: 'Tooth_Width', motorcad_var: 'Tooth_Width', name_cn: '齿宽', unit: 'mm', value: 7.0, category: 'Stator', description_cn: '定子齿宽度' },
  663. { name: 'Number_of_Slots', motorcad_var: 'Slot_Number', name_cn: '槽数', unit: '', value: 12, category: 'Stator', description_cn: '定子总槽数' },
  664. { name: 'Rotor_Back_Iron_Thickness', motorcad_var: null, name_cn: '转子背铁厚', unit: 'mm', value: 8.0, category: 'Rotor', description_cn: '转子背铁径向厚度' },
  665. { name: 'Magnet_Length', motorcad_var: 'Magnet_Length', name_cn: '磁钢轴向长度', unit: 'mm', value: 3.0, category: 'Rotor', description_cn: '永磁体轴向厚度' },
  666. { name: 'Magnet_Thickness', motorcad_var: 'Magnet_Thickness', name_cn: '磁钢径向厚度', unit: 'mm', value: 13.0, category: 'Rotor', description_cn: '磁钢环径向厚度' },
  667. { name: 'Magnet_Arc_[ED]', motorcad_var: 'Magnet_Arc_[ED]', name_cn: '磁钢极弧角', unit: 'deg', value: 121.0, category: 'Rotor', description_cn: '磁钢极弧角度(电角度)' },
  668. { name: 'Number_of_Poles', motorcad_var: 'Pole_Number', name_cn: '极数', unit: '', value: 10, category: 'Rotor', description_cn: '电机总极数' },
  669. { name: 'Current_Advance_Angle', motorcad_var: null, name_cn: '电流超前角', unit: 'deg', value: 0.0, category: 'Performance', description_cn: '弱磁控制电流超前角' },
  670. { name: 'DC_Link_Voltage', motorcad_var: 'DCBusVoltage', name_cn: '母线电压', unit: 'V', value: 13.5, category: 'Performance', description_cn: '直流母线电压' },
  671. { name: 'RMSCurrent', motorcad_var: 'RMSCurrent', name_cn: '额定电流', unit: 'A', value: 21.0, category: 'Performance', description_cn: '额定相电流(RMS)' },
  672. { name: 'Shaft_Speed', motorcad_var: 'Shaft_Speed', name_cn: '额定转速', unit: 'rpm', value: 5000.0, category: 'Performance', description_cn: '电机额定转速' },
  673. { name: 'Max_Speed', motorcad_var: 'WindageGraph_MaxSpeed', name_cn: '最高转速', unit: 'rpm', value: 7500.0, category: 'Performance', description_cn: '电机最高转速(变量名需确认)' },
  674. { name: 'Turns_per_Coil', motorcad_var: 'ConductorsPerSlot', name_cn: '每线圈匝数', unit: '', value: 20, category: 'Winding', description_cn: '每个线圈的匝数' },
  675. { name: 'Parallel_Paths', motorcad_var: 'ParallelPaths', name_cn: '并联支路数', unit: '', value: 1, category: 'Winding', description_cn: '绕组并联支路数' },
  676. { name: 'Copper_Fill_Factor', motorcad_var: 'Slot_Fill', name_cn: '槽满率', unit: '', value: 0.45, category: 'Winding', description_cn: '铜线面积与槽面积之比' },
  677. { name: 'Wire_Diameter', motorcad_var: 'Wire_Diameter', name_cn: '线径', unit: 'mm', value: 1.63, category: 'Winding', description_cn: '漆包线直径' },
  678. { name: 'Winding_Connection', motorcad_var: 'WindingConnection', name_cn: '绕组连接方式', unit: '', value: 'Star', category: 'Winding', description_cn: '绕组连接方式(星/三角)(变量名需确认)' },
  679. { name: 'Current_Density', motorcad_var: null, name_cn: '电流密度', unit: 'A/mm2', value: 6.0, category: 'Winding', description_cn: '绕组电流密度(变量名需确认)' },
  680. { name: 'Magnet_Material', motorcad_var: 'Material_Magnet', name_cn: '磁钢材料', unit: '', value: 'NdFeB_N42SH', category: 'Material', description_cn: '永磁体材料牌号' },
  681. { name: 'Steel_Grade', motorcad_var: 'Material_Stator_Lam_Yoke', name_cn: '硅钢片牌号', unit: '', value: 'M19_24G', category: 'Material', description_cn: '电工硅钢片牌号' },
  682. { name: 'Magnet_Temperature', motorcad_var: 'InitialMagnetTemperature', name_cn: '磁钢温度', unit: 'C', value: 40.0, category: 'Material', description_cn: '磁钢工作温度' },
  683. { name: 'Magnet_Remanence', motorcad_var: 'Magnet_Br_at_RefTemp', name_cn: '剩磁', unit: 'T', value: 1.31, category: 'Material', description_cn: '永磁体剩磁密度(变量名需确认)' },
  684. { name: 'Ambient_Temperature', motorcad_var: 'Ambient_Temperature', name_cn: '环境温度', unit: 'C', value: 40.0, category: 'Thermal', description_cn: '环境空气温度' },
  685. { name: 'Cooling_Method', motorcad_var: 'Cooling_Type', name_cn: '冷却方式', unit: '', value: 'Natural', category: 'Thermal', description_cn: '冷却方式(自然/强迫风冷/水冷)' },
  686. { name: 'Insulation_Class', motorcad_var: null, name_cn: '绝缘等级', unit: '', value: 'F', category: 'Thermal', description_cn: '绝缘等级(A/E/B/F/H)(变量名需确认)' },
  687. { name: 'TorquePointsPerCycle', motorcad_var: 'TorquePointsPerCycle', name_cn: '每周期转矩采样点', unit: '', value: 30, category: 'Simulation', description_cn: '每电周期转矩计算采样点数' },
  688. { name: 'CurrentDefinition', motorcad_var: null, name_cn: '电流定义方式', unit: '', value: 1.0, category: 'Simulation', description_cn: '1=有效值, 2=峰值电流定义' },
  689. { name: 'AirgapMeshPoints_mesh', motorcad_var: 'AirgapMeshPoints_mesh', name_cn: '气隙网格点数', unit: '', value: 600.0, category: 'Simulation', description_cn: '气隙网格点数(显式求解网格加密)' },
  690. { name: 'MessageDisplayState', motorcad_var: 'MessageDisplayState', name_cn: '消息显示', unit: '', value: 2.0, category: 'Simulation', description_cn: '脚本运行时抑制弹窗' },
  691. ]
  692. // Computed
  693. const subtitleText = computed(() => {
  694. const parts: string[] = []
  695. if (plan.topology) parts.push(plan.topology)
  696. if (plan.created_at) parts.push(`创建于 ${new Date(plan.created_at).toLocaleDateString('zh-CN')}`)
  697. return parts.join(' · ')
  698. })
  699. // Scan variables live under plan_data.variables in the plan payload (P0 fix:
  700. // previously read the non-existent plan.scan_variables, so the overview stat
  701. // cards and the parameters tab always showed 0/empty for every plan).
  702. const scanVariables = computed(() => plan.plan_data?.variables || plan.scan_variables || [])
  703. const fixedParams = computed(() => {
  704. const fps = plan.fixed_parameters || []
  705. // Backfill from template if missing
  706. if (fps.length === 0) {
  707. return ALL_FIXED_PARAM_TEMPLATE.map(p => ({ ...p, category_cn: categoryCnMap[p.category] || p.category }))
  708. }
  709. return fps.map((p: any) => ({
  710. ...p,
  711. category_cn: p.category_cn || categoryCnMap[p.category] || p.category,
  712. name_cn: p.name_cn || SCAN_PARAM_CN[p.name] || p.name,
  713. }))
  714. })
  715. // BC lives under plan_data (not top-level). Show ALL catalog fields (unset
  716. // ones included) so the full 25-field boundary picture is visible; tag each
  717. // with its source (user-specified / AI-supplemented / edited).
  718. const bcMeta = computed(() => plan.plan_data?.bc_meta || {})
  719. const boundaryConditions = computed(() => {
  720. const bc = plan.plan_data?.boundary_conditions || plan.boundary_conditions || {}
  721. return bcFieldCatalog.value.map(t => ({
  722. ...t,
  723. value: bc[t.key],
  724. source: bcMeta.value[t.key]?.source || (bc[t.key] != null && bc[t.key] !== '' ? 'user' : ''),
  725. }))
  726. })
  727. // Acceptance criteria: AI-provided criteria first, then derived from BC target
  728. // fields so the overview always shows the plan's goals at a glance.
  729. const ACCEPTANCE_FROM_BC: { key: string; metric: string; label: string; op: string; unit: string }[] = [
  730. { key: 'target_torque_nm', metric: 'tavg_nm', label: '平均转矩', op: '>=', unit: 'Nm' },
  731. { key: 'target_efficiency_pct', metric: 'efficiency_pct', label: '效率', op: '>=', unit: '%' },
  732. { key: 'max_losses_w', metric: 'total_losses_w', label: '总损耗', op: '<=', unit: 'W' },
  733. { key: 'target_ripple_pct', metric: 'ripple_pct', label: '转矩脉动', op: '<=', unit: '%' },
  734. { key: 'max_axial_force_n', metric: 'axial_force_n', label: '轴向力', op: '<=', unit: 'N' },
  735. { key: 'outer_diameter_mm', metric: 'outer_diameter', label: '外径', op: '<=', unit: 'mm' },
  736. { key: 'axial_length_mm', metric: 'axial_length', label: '轴向长度', op: '<=', unit: 'mm' },
  737. ]
  738. const acceptanceItems = computed(() => {
  739. const ac = plan.plan_data?.acceptance_criteria || plan.acceptance_criteria
  740. const items: { text: string; origin: string }[] = []
  741. if (ac && Array.isArray(ac.hard_constraints)) {
  742. ac.hard_constraints.forEach((c: string) => items.push({ text: c, origin: 'AI 设定' }))
  743. } else if (typeof ac === 'string' && ac) {
  744. items.push({ text: ac, origin: 'AI 设定' })
  745. }
  746. const bc = plan.plan_data?.boundary_conditions || {}
  747. ACCEPTANCE_FROM_BC.forEach(m => {
  748. const v = bc[m.key]
  749. if (v !== undefined && v !== null && v !== '') {
  750. items.push({ text: `${m.label} ${m.op} ${v} ${m.unit}`.trim(), origin: '边界条件' })
  751. }
  752. })
  753. return items
  754. })
  755. const estimatedPoints = computed(() => {
  756. if (!scanVariables.value.length) return 0
  757. return scanVariables.value.reduce((acc: number, v: any) => acc * calcPoints(v), 1)
  758. })
  759. // Per-point duration calibrated from measured solve times (P2-1); falls back
  760. // to the 2.5 min/point heuristic when no measurement exists yet.
  761. const perPointMin = computed(() => {
  762. const avg = solveTimeStats.value?.avg_s
  763. return avg ? avg / 60 : 2.5
  764. })
  765. const estimatedTimeMin = computed(() => {
  766. return Math.round(estimatedPoints.value * perPointMin.value)
  767. })
  768. const estimatedTimeBasis = computed(() => {
  769. const s = solveTimeStats.value
  770. if (s && s.count) return `基于最近 ${s.count} 次实测(~${Math.round(s.avg_s)}s/点)`
  771. return '按经验值 2.5 分钟/点估算'
  772. })
  773. const estimatedRemaining = computed(() => {
  774. if (!activeTask.value || !activeTask.value.total_points) return '-'
  775. const remaining = activeTask.value.total_points - (activeTask.value.completed_points || 0)
  776. const mins = Math.round(remaining * perPointMin.value)
  777. if (mins < 60) return `${mins} 分钟`
  778. return `${(mins / 60).toFixed(1)} 小时`
  779. })
  780. const modifiedFixedParams = computed(() => {
  781. return fixedParams.value.filter((p: any) => {
  782. const def = ALL_FIXED_PARAM_TEMPLATE.find(t => t.name === p.name)
  783. if (!def) return true // unknown param, treat as modified
  784. return String(p.value) !== String(def.value)
  785. })
  786. })
  787. const fixedCategories = computed<string[]>(() => {
  788. const fps = fixedParams.value as any[]
  789. const cats = [...new Set(fps.map((p: any) => String(p.category_cn || p.category || '其他')))]
  790. return cats
  791. })
  792. // Topology-aware variable catalog fetched from backend
  793. const variableCatalog = ref<any>(null)
  794. async function loadVariableCatalog() {
  795. try {
  796. const topo = (plan.topology as string) || 'SSSR'
  797. const res: any = await api.get('/plans/variable-catalog', { params: { topology: topo } })
  798. variableCatalog.value = res
  799. } catch (e) {
  800. console.error('Failed to load variable catalog:', e)
  801. }
  802. }
  803. const availableScanParams = computed(() => {
  804. const usedNames = new Set(scanVariables.value.map((v: any) => v.name))
  805. // Prefer backend template (topology-aware with motorcad_var), fallback to local
  806. const template = variableCatalog.value?.template || ALL_FIXED_PARAM_TEMPLATE
  807. return template
  808. .filter((p: any) => !usedNames.has(p.name))
  809. .map((p: any) => ({
  810. name: p.name,
  811. name_cn: p.name_cn || p.name,
  812. unit: p.unit,
  813. motorcad_var: p.motorcad_var || p.name,
  814. }))
  815. })
  816. const coreResultColumns = [
  817. { key: 'tavg_nm', label: '平均转矩(Nm)', width: 120 },
  818. { key: 'ripple_pct', label: '转矩脉动(%)', width: 110 },
  819. { key: 'efficiency_pct', label: '效率(%)', width: 90 },
  820. { key: 'total_losses_w', label: '总损耗(W)', width: 110 },
  821. { key: 'status', label: '状态', width: 80 },
  822. ]
  823. const allResultColumns = computed(() => {
  824. if (!results.value.length) return coreResultColumns
  825. const keys = Object.keys(results.value[0]) as string[]
  826. const labelMap: Record<string, string> = {
  827. point_id: '点ID', index: '序号', Airgap: '气隙(mm)', Magnet_Length: '磁钢长(mm)',
  828. Magnet_Thickness: '磁钢厚(mm)', 'Magnet_Arc_[ED]': '极弧角(deg)',
  829. RMSCurrent: '电流(A)', Shaft_Speed: '转速(rpm)',
  830. tavg_nm: '平均转矩(Nm)', ripple_pct: '转矩脉动(%)',
  831. average_torque_nm: '平均转矩(Nm)', torque_ripple_pct: '转矩脉动(%)',
  832. cogging_torque_nm: '齿槽转矩(Nm)', efficiency_pct: '效率(%)',
  833. total_losses_w: '总损耗(W)', copper_loss_w: '铜耗(W)',
  834. iron_loss_w: '铁耗(W)', magnet_loss_w: '磁钢耗(W)',
  835. max_flux_density_t: '最大磁密(T)', status: '状态',
  836. solve_time_s: '求解时间(s)', error: '错误信息', error_message: '错误信息',
  837. output_power_w: '输出功率(W)', input_power_w: '输入功率(W)',
  838. em_power_w: '电磁功率(W)', power_factor: '功率因数',
  839. back_emf_v: '反电动势(V)', back_emf_thd_pct: '反电动势谐波(%)',
  840. line_current_rms_a: '线电流(A)', phase_current_peak_a: '相电流峰值(A)',
  841. shaft_speed_rpm: '转速(rpm)', no_load_speed_rpm: '空载转速(rpm)',
  842. torque_constant: '转矩常数(Nm/A)', shaft_torque_nm: '轴转矩(Nm)',
  843. stall_torque_nm: '堵转转矩(Nm)', tmax_nm: '最大转矩(Nm)', tmin_nm: '最小转矩(Nm)',
  844. winding_temp_c: '绕组温度(°C)', winding_hotspot_temp_c: '绕组热点(°C)',
  845. magnet_temp_c: '磁钢温度(°C)', stator_temp_c: '定子温度(°C)',
  846. bearing_temp_c: '轴承温度(°C)', temp_rise_c: '温升(°C)',
  847. thermal_resistance_k_w: '热阻(K/W)',
  848. axial_force_n: '轴向力(N)', radial_force_n: '径向力(N)',
  849. max_stress_mpa: '最大应力(MPa)', deformation_mm: '最大变形(mm)',
  850. }
  851. return keys.map(k => ({
  852. key: k,
  853. label: labelMap[k] || k,
  854. width: k === 'error' ? 200 : 110,
  855. }))
  856. })
  857. // --- Result column settings (P2-4) ---
  858. // Default: core metric columns + the plan's scan-variable input columns. All
  859. // 35+ metrics remain available on demand via the column-settings dialog
  860. // instead of being laid out unconditionally.
  861. const showColumnSettings = ref(false)
  862. const visibleColumnKeys = ref<string[] | null>(loadSavedColumns()) // null = use defaults
  863. // Persist the column preference across sessions (localStorage). The choice is
  864. // global (shared by all plans) since metric columns are plan-independent.
  865. const COLUMN_STORAGE_KEY = 'afm:plan-result-columns'
  866. function loadSavedColumns(): string[] | null {
  867. try {
  868. const raw = localStorage.getItem(COLUMN_STORAGE_KEY)
  869. if (!raw) return null
  870. const parsed = JSON.parse(raw)
  871. return Array.isArray(parsed) && parsed.length ? parsed : null
  872. } catch {
  873. return null
  874. }
  875. }
  876. function persistColumns() {
  877. try {
  878. if (visibleColumnKeys.value === null) {
  879. localStorage.removeItem(COLUMN_STORAGE_KEY)
  880. } else {
  881. localStorage.setItem(COLUMN_STORAGE_KEY, JSON.stringify(visibleColumnKeys.value))
  882. }
  883. } catch { /* storage may be unavailable; non-fatal */ }
  884. }
  885. const defaultColumnKeys = computed(() => {
  886. const keys = coreResultColumns.map(c => c.key)
  887. scanVariables.value.forEach((v: any) => {
  888. if (v?.name && !keys.includes(v.name)) keys.push(v.name)
  889. })
  890. // Thermal columns join the default view when the task ran with thermal
  891. // analysis (thermal_mode steady/coupled) and the results carry them.
  892. const thermalCore = [
  893. 'winding_temp_c', 'winding_hotspot_temp_c', 'magnet_temp_c',
  894. 'stator_temp_c', 'temp_rise_c',
  895. ]
  896. if (results.value.length) {
  897. const present = new Set(Object.keys(results.value[0]))
  898. thermalCore.forEach(k => {
  899. if (present.has(k) && !keys.includes(k)) keys.push(k)
  900. })
  901. }
  902. return keys
  903. })
  904. const displayColumns = computed(() => {
  905. const all = allResultColumns.value
  906. const sel = new Set(visibleColumnKeys.value ?? defaultColumnKeys.value)
  907. return all.filter(c => sel.has(c.key))
  908. })
  909. const openColumnSettings = () => {
  910. if (visibleColumnKeys.value === null) {
  911. visibleColumnKeys.value = [...defaultColumnKeys.value]
  912. }
  913. showColumnSettings.value = true
  914. }
  915. const confirmColumnSettings = () => {
  916. if (visibleColumnKeys.value !== null && !visibleColumnKeys.value.length) {
  917. ElMessage.warning('至少保留一列')
  918. return
  919. }
  920. persistColumns()
  921. showColumnSettings.value = false
  922. }
  923. const resetColumns = () => {
  924. visibleColumnKeys.value = null
  925. persistColumns()
  926. showColumnSettings.value = false
  927. }
  928. // Methods
  929. const calcPoints = (v: any) => {
  930. // Prefer the explicit values list (plan_data.variables / AI-generated plans
  931. // store values arrays); fall back to a min/max/step range otherwise so both
  932. // legacy and current plan shapes compute a non-zero point count (P0-1).
  933. if (Array.isArray(v.values) && v.values.length) return v.values.length
  934. const step = v.step ?? v.step_size
  935. if (!step || step <= 0) return 0
  936. const min = v.min_value ?? v.start
  937. const max = v.max_value ?? v.stop
  938. if (min == null || max == null) return 0
  939. return Math.floor((max - min) / step + 1e-9) + 1
  940. }
  941. const getDefaultValue = (name: string) => {
  942. const def = ALL_FIXED_PARAM_TEMPLATE.find(t => t.name === name)
  943. return def ? def.value : '-'
  944. }
  945. const fixedParamsByCategory = (cat: string) => {
  946. return fixedParams.value.filter((p: any) => String(p.category_cn || p.category) === cat)
  947. }
  948. const toggleCategory = (cat: string) => {
  949. // Categories are expanded by default; the map only records explicit collapse.
  950. expandedCategories[cat] = expandedCategories[cat] === false
  951. }
  952. const isFailed = (row: any) => row.status === 'FAILED' || row.status === 'failed'
  953. const formatCell = (val: any) => {
  954. if (val === null || val === undefined) return '-'
  955. if (typeof val === 'number') return Number.isInteger(val) ? val : val.toFixed(4)
  956. return String(val)
  957. }
  958. const goBack = () => router.back()
  959. const onVarSelect = (name: string) => {
  960. const template = variableCatalog.value?.template || ALL_FIXED_PARAM_TEMPLATE
  961. const p = template.find((t: any) => t.name === name)
  962. if (p) {
  963. newVar.name_cn = p.name_cn || p.name
  964. newVar.unit = p.unit
  965. const baseVal = Number(p.value) || 1
  966. newVar.min_value = baseVal * 0.8
  967. newVar.max_value = baseVal * 1.2
  968. newVar.step = Math.abs(baseVal * 0.05) || 0.1
  969. }
  970. }
  971. // Ensure plan_data.variables exists and return it (scan variables are stored
  972. // under plan_data.variables in the plan payload).
  973. const ensureVariables = () => {
  974. if (!plan.plan_data) plan.plan_data = {}
  975. if (!Array.isArray(plan.plan_data.variables)) plan.plan_data.variables = []
  976. return plan.plan_data.variables
  977. }
  978. const confirmAddVar = () => {
  979. if (!newVar.name) { ElMessage.warning('请选择变量名'); return }
  980. ensureVariables().push({ ...newVar })
  981. scanVarsDirty.value = true
  982. showAddVar.value = false
  983. Object.assign(newVar, { name: '', name_cn: '', min_value: 0, max_value: 1, step: 0.1, unit: '' })
  984. ElMessage.success('变量已添加,点击"保存修改"后生效')
  985. }
  986. const removeVariable = (idx: number) => {
  987. ensureVariables().splice(idx, 1)
  988. scanVarsDirty.value = true
  989. }
  990. // Persist scan-variable edits to the backend (full plan_data replacement,
  991. // validated server-side by the single-source plan schema).
  992. const saveScanVariables = async () => {
  993. savingVars.value = true
  994. try {
  995. const planData = { ...(plan.plan_data || {}), variables: ensureVariables() }
  996. await api.put(`/plans/${planId.value}`, { plan_data: planData })
  997. scanVarsDirty.value = false
  998. ElMessage.success('方案参数已保存')
  999. await loadPlan()
  1000. } catch (e: any) {
  1001. ElMessage.error('保存失败: ' + (e.message || e))
  1002. } finally {
  1003. savingVars.value = false
  1004. }
  1005. }
  1006. // Boundary-condition editing: inline edit mode on the plan's BC. Values the
  1007. // user changes are tagged "edited" in bc_meta so the source stays traceable.
  1008. const bcEditing = ref(false)
  1009. const bcSaving = ref(false)
  1010. const bcEditForm = reactive<Record<string, any>>({})
  1011. const startBcEdit = () => {
  1012. const bc = plan.plan_data?.boundary_conditions || {}
  1013. bcFieldCatalog.value.forEach(f => { bcEditForm[f.key] = bc[f.key] ?? null })
  1014. bcEditing.value = true
  1015. }
  1016. const saveBcEdit = async () => {
  1017. bcSaving.value = true
  1018. try {
  1019. const oldBc = plan.plan_data?.boundary_conditions || {}
  1020. const oldMeta = { ...(plan.plan_data?.bc_meta || {}) }
  1021. const newBc: Record<string, any> = {}
  1022. for (const [k, v] of Object.entries(bcEditForm)) {
  1023. if (v !== null && v !== undefined && v !== '') newBc[k] = v
  1024. }
  1025. for (const k of Object.keys(newBc)) {
  1026. const changed = String(newBc[k]) !== String(oldBc[k] ?? '')
  1027. if (changed || !oldMeta[k]) oldMeta[k] = { source: 'edited' }
  1028. }
  1029. const planData = {
  1030. ...(plan.plan_data || {}),
  1031. boundary_conditions: newBc,
  1032. bc_meta: oldMeta,
  1033. }
  1034. await api.put(`/plans/${planId.value}`, { plan_data: planData })
  1035. bcEditing.value = false
  1036. ElMessage.success('边界条件已保存')
  1037. await loadPlan()
  1038. } catch (e: any) {
  1039. ElMessage.error('保存失败: ' + (e.message || e))
  1040. } finally {
  1041. bcSaving.value = false
  1042. }
  1043. }
  1044. // Fixed-parameter inline editing: enter edit mode -> all categories expand,
  1045. // value cells become inputs; save writes the full fixed_params list back via
  1046. // PUT plan_data (server-side schema validation applies).
  1047. const fixedEditing = ref(false)
  1048. const fixedSaving = ref(false)
  1049. const fixedEditValues = reactive<Record<string, any>>({})
  1050. const startFixedEdit = () => {
  1051. ;(fixedParams.value as any[]).forEach((p: any) => { fixedEditValues[p.name] = p.value })
  1052. fixedEditing.value = true
  1053. }
  1054. const isFixedModified = (row: any) => {
  1055. const def = ALL_FIXED_PARAM_TEMPLATE.find(t => t.name === row.name)
  1056. if (!def) return true
  1057. return String(row.value) !== String(def.value)
  1058. }
  1059. const saveFixedParams = async () => {
  1060. fixedSaving.value = true
  1061. try {
  1062. const newFixed = (fixedParams.value as any[]).map((p: any) => ({
  1063. ...p,
  1064. value: fixedEditValues[p.name] !== undefined ? fixedEditValues[p.name] : p.value,
  1065. }))
  1066. const planData = { ...(plan.plan_data || {}), fixed_params: newFixed }
  1067. await api.put(`/plans/${planId.value}`, { plan_data: planData })
  1068. fixedEditing.value = false
  1069. ElMessage.success('固定参数已保存')
  1070. await loadPlan()
  1071. } catch (e: any) {
  1072. ElMessage.error('保存失败: ' + (e.message || e))
  1073. } finally {
  1074. fixedSaving.value = false
  1075. }
  1076. }
  1077. // Pre-flight checklist: key -> localized label (backend returns keys only).
  1078. const PREFLIGHT_LABELS: Record<string, string> = {
  1079. model_path: '模型文件',
  1080. unverified_vars: '变量名确认',
  1081. scan_vars: '扫描变量',
  1082. point_count: '数据点规模',
  1083. executor: '本地执行器',
  1084. }
  1085. function preflightMessage(c: any): string {
  1086. switch (c.key) {
  1087. case 'model_path':
  1088. return c.status === 'pass' ? `已就绪:${c.value}` : (c.value ? `模型文件不存在:${c.value}` : '未设置模型路径(model_path 为空)')
  1089. case 'unverified_vars':
  1090. return c.status === 'pass' ? '全部固定参数变量名已映射' : `${(c.items || []).length} 个变量名未经 Motor-CAD 验证:${(c.items || []).join('、')}`
  1091. case 'scan_vars':
  1092. return c.status === 'pass' ? `${c.count} 个扫描变量` : '未定义扫描变量(至少 1 个)'
  1093. case 'point_count':
  1094. return c.status === 'warn' ? `${c.count} 个数据点,规模较大,请确认` : `${c.count} 个数据点`
  1095. case 'executor':
  1096. return c.online > 0 ? `${c.online} 个执行器在线` : '未检测到在线执行器(任务将排队等待)'
  1097. default:
  1098. return ''
  1099. }
  1100. }
  1101. // P6: task-level thermal mode (off/steady/coupled), chosen at one-click start.
  1102. const thermalMode = ref('steady')
  1103. const startSimulation = async () => {
  1104. // Run the pre-flight checklist first; the start button in the dialog is
  1105. // disabled while any check has status 'fail'.
  1106. preflightLoading.value = true
  1107. try {
  1108. const res: any = await api.get(`/plans/${planId.value}/preflight`)
  1109. preflight.value = res
  1110. showPreflight.value = true
  1111. } catch (e: any) {
  1112. ElMessage.error('仿真前检查失败: ' + (e.message || e))
  1113. } finally {
  1114. preflightLoading.value = false
  1115. }
  1116. }
  1117. const confirmStartSimulation = async () => {
  1118. showPreflight.value = false
  1119. starting.value = true
  1120. try {
  1121. await api.post(`/plans/${planId.value}/start-simulation`, {
  1122. thermal_mode: thermalMode.value,
  1123. })
  1124. ElMessage.success('仿真已启动')
  1125. isRunning.value = true
  1126. activeTab.value = 'overview'
  1127. } catch (e: any) {
  1128. ElMessage.error('启动失败: ' + (e.message || e))
  1129. } finally {
  1130. starting.value = false
  1131. }
  1132. }
  1133. // One-click repair for a failed model_path check: write the topology's
  1134. // default base model into the plan, then re-run preflight so the checklist
  1135. // flips green in place.
  1136. const applyDefaultModel = async (defaultModel: string) => {
  1137. applyingDefaultModel.value = true
  1138. try {
  1139. const planData = { ...(plan.plan_data || {}), model_path: defaultModel }
  1140. await api.put(`/plans/${planId.value}`, { plan_data: planData })
  1141. ElMessage.success('已应用拓扑默认模型')
  1142. await loadPlan()
  1143. const res: any = await api.get(`/plans/${planId.value}/preflight`)
  1144. preflight.value = res
  1145. } catch (e: any) {
  1146. ElMessage.error('应用默认模型失败: ' + (e.message || e))
  1147. } finally {
  1148. applyingDefaultModel.value = false
  1149. }
  1150. }
  1151. const stopSimulation = async () => {
  1152. try {
  1153. await ElMessageBox.confirm(
  1154. '确认停止当前仿真?已完成的点会保留,执行器将在当前点结束后中断。',
  1155. '停止仿真',
  1156. { type: 'warning' },
  1157. )
  1158. } catch {
  1159. return // user dismissed the confirm dialog
  1160. }
  1161. stopping.value = true
  1162. try {
  1163. const tid = activeTask.value?.task_id
  1164. if (tid) {
  1165. await api.post(`/tasks/${tid}/cancel`)
  1166. }
  1167. // Refresh from backend: cancel flips the task status, so the progress
  1168. // banner (driven by activeTask.total_points snapshot) disappears.
  1169. await loadActiveTask()
  1170. ElMessage.success('已发送停止指令,执行器将在当前点结束后中断')
  1171. } catch (e: any) {
  1172. ElMessage.error('停止失败: ' + (e.message || e))
  1173. } finally {
  1174. stopping.value = false
  1175. }
  1176. }
  1177. const onFileChange = (file: any) => {
  1178. uploadFile.value = file.raw
  1179. }
  1180. const confirmUpload = async () => {
  1181. if (!uploadFile.value) return
  1182. uploading.value = true
  1183. try {
  1184. const formData = new FormData()
  1185. formData.append('file', uploadFile.value)
  1186. await api.post(`/plans/${planId.value}/upload-results`, formData, {
  1187. headers: { 'Content-Type': 'multipart/form-data' },
  1188. })
  1189. ElMessage.success('结果上传成功')
  1190. showUpload.value = false
  1191. loadResults()
  1192. } catch (e: any) {
  1193. ElMessage.error('上传失败: ' + (e.message || e))
  1194. } finally {
  1195. uploading.value = false
  1196. }
  1197. }
  1198. // Download the result set as CSV (same columns as the Excel export).
  1199. // NOTE: /api/plans/{id}/download returns the *plan definition* JSON for the
  1200. // executor, not results — do not point this button at that endpoint.
  1201. const downloadResults = () => {
  1202. window.open(`/api/plans/${planId.value}/export-csv`, '_blank')
  1203. }
  1204. // Export the full result set (scan info + EM + thermal columns, two-row
  1205. // header format matching the torqrippswap scan_results.xlsx convention).
  1206. const exportExcel = () => {
  1207. window.open(`/api/plans/${planId.value}/export-xlsx`, '_blank')
  1208. }
  1209. const runAIAnalysis = async () => {
  1210. analyzing.value = true
  1211. try {
  1212. const res: any = await api.post(`/plans/${planId.value}/ai-analyze`, {}, { timeout: 60000 })
  1213. aiAnalysis.value = res
  1214. aiStep.value = 1
  1215. ElMessage.success('AI 分析完成')
  1216. } catch (e: any) {
  1217. ElMessage.error('分析失败: ' + (e.message || e))
  1218. } finally {
  1219. analyzing.value = false
  1220. }
  1221. }
  1222. const generateIteration = async () => {
  1223. generating.value = true
  1224. try {
  1225. const res: any = await api.post(`/plans/${planId.value}/generate-iteration`, {}, { timeout: 60000 })
  1226. iterationPlan.value = res
  1227. aiStep.value = 2
  1228. ElMessage.success('迭代方案已生成')
  1229. } catch (e: any) {
  1230. ElMessage.error('生成失败: ' + (e.message || e))
  1231. } finally {
  1232. generating.value = false
  1233. }
  1234. }
  1235. const viewIteration = () => {
  1236. if (iterationPlan.value?.id) {
  1237. router.push(`/plans/${iterationPlan.value.id}`)
  1238. }
  1239. }
  1240. const extractExperience = async () => {
  1241. extracting.value = true
  1242. try {
  1243. await api.post(`/experience/from-plan/${planId.value}/smart-extract`, {})
  1244. experienceExtracted.value = true
  1245. aiStep.value = 3
  1246. ElMessage.success('经验已提取')
  1247. } catch (e: any) {
  1248. ElMessage.error('提取失败: ' + (e.message || e))
  1249. } finally {
  1250. extracting.value = false
  1251. }
  1252. }
  1253. // Data loading
  1254. const loadPlan = async () => {
  1255. try {
  1256. const res: any = await api.get(`/plans/${planId.value}`)
  1257. Object.assign(plan, res)
  1258. } catch (e: any) {
  1259. ElMessage.error('加载方案失败: ' + (e.message || e))
  1260. }
  1261. }
  1262. const loadResults = async () => {
  1263. try {
  1264. const res: any = await api.get(`/plans/${planId.value}/results`)
  1265. const raw = res.items || res.results || res.data || []
  1266. // Flatten nested params/metrics into a flat object for table display.
  1267. // Alias legacy metric names to the canonical afmcore keys (tavg_nm /
  1268. // ripple_pct) so older results still populate the columns.
  1269. results.value = raw.map((r: any) => {
  1270. const flat = { ...r, ...(r.params || {}), ...(r.metrics || {}) }
  1271. if (flat.tavg_nm == null && flat.average_torque_nm != null) flat.tavg_nm = flat.average_torque_nm
  1272. if (flat.ripple_pct == null && flat.torque_ripple_pct != null) flat.ripple_pct = flat.torque_ripple_pct
  1273. return flat
  1274. })
  1275. } catch {
  1276. results.value = []
  1277. }
  1278. }
  1279. const loadActiveTask = async () => {
  1280. try {
  1281. const res: any = await api.get(`/plans/${planId.value}/active-task`)
  1282. activeTask.value = res.task || res
  1283. isRunning.value = activeTask.value && ['running', 'dispatched'].includes(activeTask.value.status)
  1284. } catch {
  1285. activeTask.value = null
  1286. isRunning.value = false
  1287. }
  1288. }
  1289. const loadBcFieldCatalog = async () => {
  1290. try {
  1291. const res: any = await api.get('/bc-fields')
  1292. bcFieldCatalog.value = res.fields || []
  1293. } catch { /* optional: boundary display stays empty */ }
  1294. }
  1295. const loadSolveTimeStats = async () => {
  1296. try {
  1297. const res: any = await api.get('/analytics/solve-time-stats')
  1298. solveTimeStats.value = res
  1299. } catch { /* optional: fall back to heuristic estimate */ }
  1300. }
  1301. let pollTimer: any = null
  1302. onMounted(() => {
  1303. loadPlan()
  1304. loadResults()
  1305. loadActiveTask()
  1306. loadVariableCatalog()
  1307. loadBcFieldCatalog()
  1308. loadSolveTimeStats()
  1309. pollTimer = setInterval(() => {
  1310. if (isRunning.value) {
  1311. loadActiveTask()
  1312. loadResults()
  1313. }
  1314. }, 5000)
  1315. })
  1316. onUnmounted(() => {
  1317. if (pollTimer) clearInterval(pollTimer)
  1318. })
  1319. watch(activeTab, (tab) => {
  1320. if (tab === 'results') loadResults()
  1321. })
  1322. </script>
  1323. <style scoped>
  1324. .plan-detail {
  1325. padding: var(--space-6);
  1326. max-width: 1600px;
  1327. margin: 0 auto;
  1328. }
  1329. /* Progress Banner */
  1330. .progress-banner {
  1331. background: linear-gradient(135deg, var(--color-primary-bg), #e0e7ff);
  1332. border: 1px solid var(--color-primary-light);
  1333. border-radius: var(--radius-lg);
  1334. padding: var(--space-4) var(--space-5);
  1335. margin-bottom: var(--space-5);
  1336. }
  1337. .progress-info {
  1338. display: flex;
  1339. align-items: center;
  1340. gap: var(--space-3);
  1341. margin-bottom: var(--space-2);
  1342. }
  1343. .progress-title {
  1344. font-weight: 600;
  1345. color: var(--color-primary-dark);
  1346. }
  1347. .progress-detail {
  1348. font-size: var(--font-size-sm);
  1349. color: var(--color-text-secondary);
  1350. }
  1351. .progress-bar {
  1352. margin: 0;
  1353. }
  1354. /* Tabs */
  1355. .plan-tabs {
  1356. --el-tabs-header-height: 48px;
  1357. }
  1358. .plan-tabs :deep(.el-tabs__header) {
  1359. margin-bottom: var(--space-5);
  1360. border-bottom: 2px solid var(--color-border-light);
  1361. }
  1362. .plan-tabs :deep(.el-tabs__nav-wrap::after) {
  1363. display: none;
  1364. }
  1365. .plan-tabs :deep(.el-tabs__item) {
  1366. font-size: var(--font-size-base);
  1367. font-weight: 500;
  1368. height: 48px;
  1369. line-height: 48px;
  1370. }
  1371. /* Stat row */
  1372. .stat-row {
  1373. margin-bottom: var(--space-5);
  1374. }
  1375. /* AI reasoning */
  1376. .ai-reasoning {
  1377. font-size: var(--font-size-sm);
  1378. line-height: 1.8;
  1379. color: var(--color-text-secondary);
  1380. white-space: pre-wrap;
  1381. background: var(--color-border-light);
  1382. padding: var(--space-4);
  1383. border-radius: var(--radius-md);
  1384. border-left: 3px solid var(--color-primary);
  1385. }
  1386. /* Criteria */
  1387. .criteria-list {
  1388. display: flex;
  1389. flex-direction: column;
  1390. gap: var(--space-2);
  1391. }
  1392. .criteria-item {
  1393. display: flex;
  1394. align-items: center;
  1395. gap: var(--space-2);
  1396. font-size: var(--font-size-sm);
  1397. color: var(--color-text-secondary);
  1398. }
  1399. .criteria-icon {
  1400. color: var(--color-success);
  1401. flex-shrink: 0;
  1402. }
  1403. /* Quick actions */
  1404. .quick-actions {
  1405. display: grid;
  1406. grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  1407. gap: var(--space-4);
  1408. }
  1409. .quick-action-card {
  1410. border: 1px solid var(--color-border);
  1411. border-radius: var(--radius-lg);
  1412. padding: var(--space-5);
  1413. cursor: pointer;
  1414. transition: all var(--transition-fast);
  1415. text-align: center;
  1416. }
  1417. .quick-action-card:hover {
  1418. border-color: var(--color-primary);
  1419. box-shadow: var(--shadow-md);
  1420. transform: translateY(-2px);
  1421. }
  1422. .qa-title {
  1423. font-weight: 600;
  1424. font-size: var(--font-size-base);
  1425. color: var(--color-text-primary);
  1426. margin-top: var(--space-2);
  1427. }
  1428. .qa-desc {
  1429. font-size: var(--font-size-xs);
  1430. color: var(--color-text-muted);
  1431. margin-top: var(--space-1);
  1432. }
  1433. /* Fixed params */
  1434. .fixed-summary {
  1435. font-size: var(--font-size-sm);
  1436. color: var(--color-text-muted);
  1437. margin-right: var(--space-3);
  1438. }
  1439. .fixed-modified {
  1440. margin-bottom: var(--space-5);
  1441. }
  1442. .fixed-group-title {
  1443. display: flex;
  1444. align-items: center;
  1445. gap: var(--space-2);
  1446. font-size: var(--font-size-sm);
  1447. font-weight: 600;
  1448. color: var(--color-warning);
  1449. margin-bottom: var(--space-3);
  1450. }
  1451. .modified-value {
  1452. font-weight: 600;
  1453. color: var(--color-primary);
  1454. }
  1455. .var-code {
  1456. font-family: var(--font-mono);
  1457. font-size: 12px;
  1458. background: var(--color-border-light);
  1459. padding: 2px 6px;
  1460. border-radius: var(--radius-sm);
  1461. color: var(--color-text-secondary);
  1462. }
  1463. .fixed-category {
  1464. margin-bottom: var(--space-3);
  1465. }
  1466. .fixed-category-title {
  1467. display: flex;
  1468. align-items: center;
  1469. gap: var(--space-2);
  1470. padding: var(--space-2) var(--space-3);
  1471. background: var(--color-border-light);
  1472. border-radius: var(--radius-md);
  1473. cursor: pointer;
  1474. font-size: var(--font-size-sm);
  1475. font-weight: 600;
  1476. color: var(--color-text-secondary);
  1477. transition: background var(--transition-fast);
  1478. }
  1479. .fixed-category-title:hover {
  1480. background: var(--color-primary-light);
  1481. }
  1482. .cat-arrow {
  1483. font-size: 12px;
  1484. transition: transform var(--transition-fast);
  1485. }
  1486. .cat-count {
  1487. font-weight: 400;
  1488. color: var(--color-text-muted);
  1489. font-size: var(--font-size-xs);
  1490. }
  1491. /* AI steps */
  1492. .ai-steps {
  1493. margin-bottom: var(--space-6);
  1494. padding: var(--space-5);
  1495. background: var(--color-surface);
  1496. border-radius: var(--radius-lg);
  1497. border: 1px solid var(--color-border);
  1498. }
  1499. .ai-analysis-content {
  1500. display: flex;
  1501. flex-direction: column;
  1502. gap: var(--space-4);
  1503. }
  1504. .ai-section {
  1505. border: 1px solid var(--color-border-light);
  1506. border-radius: var(--radius-md);
  1507. overflow: hidden;
  1508. }
  1509. .ai-section-title {
  1510. background: var(--color-border-light);
  1511. padding: var(--space-2) var(--space-4);
  1512. font-size: var(--font-size-sm);
  1513. font-weight: 600;
  1514. color: var(--color-text-secondary);
  1515. }
  1516. .ai-section-body {
  1517. padding: var(--space-4);
  1518. font-size: var(--font-size-sm);
  1519. line-height: 1.7;
  1520. color: var(--color-text-secondary);
  1521. white-space: pre-wrap;
  1522. }
  1523. .iteration-content,
  1524. .experience-content {
  1525. padding: var(--space-2) 0;
  1526. }
  1527. /* Upload */
  1528. :deep(.el-upload-dragger) {
  1529. border-radius: var(--radius-lg);
  1530. }
  1531. /* Pre-flight checklist */
  1532. .preflight-body {
  1533. display: flex;
  1534. flex-direction: column;
  1535. gap: var(--space-2);
  1536. }
  1537. .thermal-mode-row {
  1538. display: flex;
  1539. align-items: center;
  1540. gap: var(--space-3);
  1541. }
  1542. .thermal-mode-label {
  1543. font-size: 13px;
  1544. color: var(--color-text-secondary);
  1545. font-weight: 500;
  1546. }
  1547. .preflight-item {
  1548. display: flex;
  1549. align-items: flex-start;
  1550. gap: var(--space-3);
  1551. padding: var(--space-3);
  1552. border: 1px solid var(--color-border);
  1553. border-radius: var(--radius-md);
  1554. }
  1555. .preflight-icon {
  1556. font-size: 20px;
  1557. margin-top: 2px;
  1558. }
  1559. .preflight-item.is-pass .preflight-icon { color: var(--color-success); }
  1560. .preflight-item.is-warn .preflight-icon { color: var(--color-warning); }
  1561. .preflight-item.is-fail .preflight-icon { color: var(--color-danger); }
  1562. .preflight-item.is-fail { border-color: var(--color-danger); background: #fef2f2; }
  1563. .preflight-label {
  1564. font-weight: 600;
  1565. font-size: var(--font-size-sm);
  1566. color: var(--color-text-primary);
  1567. }
  1568. .preflight-message {
  1569. font-size: var(--font-size-sm);
  1570. color: var(--color-text-secondary);
  1571. margin-top: 2px;
  1572. word-break: break-all;
  1573. }
  1574. .preflight-fix {
  1575. margin-top: 6px;
  1576. }
  1577. .est-basis {
  1578. margin-top: 4px;
  1579. font-size: 11px;
  1580. color: var(--color-text-muted);
  1581. text-align: center;
  1582. }
  1583. /* Result column settings */
  1584. .column-settings-tip {
  1585. font-size: var(--font-size-sm);
  1586. color: var(--color-text-muted);
  1587. margin-bottom: var(--space-3);
  1588. }
  1589. .column-settings-group {
  1590. display: grid;
  1591. grid-template-columns: 1fr 1fr;
  1592. gap: 4px 16px;
  1593. max-height: 400px;
  1594. overflow-y: auto;
  1595. }
  1596. .column-settings-item {
  1597. margin-right: 0;
  1598. }
  1599. /* Failed-status cell with error tooltip */
  1600. .status-failed-cell {
  1601. cursor: help;
  1602. border-bottom: 1px dashed var(--color-danger);
  1603. }
  1604. .bc-source-tag {
  1605. margin-left: 6px;
  1606. font-size: 10px;
  1607. height: 18px;
  1608. line-height: 16px;
  1609. padding: 0 4px;
  1610. }
  1611. .criteria-text {
  1612. flex: 1;
  1613. }
  1614. .criteria-origin {
  1615. margin-left: 8px;
  1616. flex-shrink: 0;
  1617. }
  1618. .var-values {
  1619. font-family: var(--font-mono, monospace);
  1620. font-size: 12px;
  1621. }
  1622. </style>