refactor(routing): ModeSelector via query param + suppression routes /private

- ModeSelector.vue : nouveaux props projectId (default 'new') + disabled
  navigue via router.push({ path: '/process/:id', query: { mode } })
  plus de sessionStorage, plus de ref locale de mode hors URL
- Home.vue : suppression bouton Start Engine et selectedMode ref
  ModeSelector devient la CTA (déplacé dans console-box)
  handleModeSelected appelle setPendingUpload synchrone avant navigation
  import statique de setPendingUpload (warning dynamic import supprimé)
  CSS obsolètes supprimées (.start-engine-btn*, pulse-border, wrapper)
- router/index.js : suppression import PrivateImpactView + routes /private et /private/:projectId
- frontend/src/views/PrivateImpactView.vue : supprimé (passthrough obsolète)

Grep final : 0 URL /private, 0 PrivateImpactView, 0 sessionStorage côté frontend.

Prompt N°25 — Roadmap refactoring wizard

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Cyril 2026-04-17 21:11:00 +02:00
parent 75d5a9bf64
commit 9899afe920
4 changed files with 50 additions and 2205 deletions

View File

@ -8,9 +8,11 @@
<div class="mode-cards">
<!-- Public Mode -->
<button
type="button"
class="mode-card"
:class="{ 'is-selected': selected === 'public' }"
@click="select('public')"
:class="{ 'is-selected': selected === 'public', 'is-disabled': disabled }"
:disabled="disabled"
@click="selectMode('public')"
>
<div class="card-icon">
<svg viewBox="0 0 24 24" width="32" height="32" fill="none" stroke="currentColor" stroke-width="1.5">
@ -38,9 +40,11 @@
<!-- Private Impact Mode -->
<button
type="button"
class="mode-card mode-card--private"
:class="{ 'is-selected': selected === 'private' }"
@click="select('private')"
:class="{ 'is-selected': selected === 'private', 'is-disabled': disabled }"
:disabled="disabled"
@click="selectMode('private')"
>
<div class="card-icon">
<svg viewBox="0 0 24 24" width="32" height="32" fill="none" stroke="currentColor" stroke-width="1.5">
@ -70,14 +74,26 @@
<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
const props = defineProps({
projectId: { type: String, default: 'new' },
disabled: { type: Boolean, default: false },
})
const emit = defineEmits(['mode-selected'])
const selected = ref(null)
const router = useRouter()
const select = (mode) => {
const selectMode = (mode) => {
if (props.disabled) return
selected.value = mode
emit('mode-selected', mode)
router.push({
path: `/process/${props.projectId}`,
query: { mode },
})
}
</script>
@ -121,12 +137,13 @@ const select = (mode) => {
border-radius: 4px;
background: #fff;
cursor: pointer;
transition: border-color 0.18s, box-shadow 0.18s, background 0.18s;
transition: border-color 0.18s, box-shadow 0.18s, background 0.18s, opacity 0.18s;
width: 100%;
gap: 14px;
font-family: inherit;
}
.mode-card:hover {
.mode-card:hover:not(.is-disabled) {
border-color: #000;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
}
@ -136,12 +153,17 @@ const select = (mode) => {
background: #FAFAFA;
}
.mode-card--private:hover,
.mode-card--private:hover:not(.is-disabled),
.mode-card--private.is-selected {
border-color: #1A1A1A;
background: #F8F8F8;
}
.mode-card.is-disabled {
cursor: not-allowed;
opacity: 0.45;
}
.card-icon {
color: #333;
flex-shrink: 0;

View File

@ -5,7 +5,6 @@ import SimulationView from '../views/SimulationView.vue'
import SimulationRunView from '../views/SimulationRunView.vue'
import ReportView from '../views/ReportView.vue'
import InteractionView from '../views/InteractionView.vue'
import PrivateImpactView from '../views/PrivateImpactView.vue'
const routes = [
{
@ -42,12 +41,6 @@ const routes = [
name: 'Interaction',
component: InteractionView,
props: true
},
{
path: '/private/:projectId',
name: 'PrivateImpact',
component: PrivateImpactView,
props: true
}
]

View File

@ -125,9 +125,6 @@
<!-- 右栏交互控制台 -->
<div class="right-panel">
<div class="mode-selector-wrapper">
<ModeSelector @mode-selected="handleModeSelected" />
</div>
<div class="console-box">
<!-- 上传区域 -->
<div class="console-section">
@ -135,8 +132,8 @@
<span class="console-label">{{ $t('home.realitySeed') }}</span>
<span class="console-meta">{{ $t('home.supportedFormats') }}</span>
</div>
<div
<div
class="upload-zone"
:class="{ 'drag-over': isDragOver, 'has-files': files.length > 0 }"
@dragover.prevent="handleDragOver"
@ -153,13 +150,13 @@
style="display: none"
:disabled="loading"
/>
<div v-if="files.length === 0" class="upload-placeholder">
<div class="upload-icon"></div>
<div class="upload-title">{{ $t('home.dragToUpload') }}</div>
<div class="upload-hint">{{ $t('home.orBrowse') }}</div>
</div>
<div v-else class="file-list">
<div v-for="(file, index) in files" :key="index" class="file-item">
<span class="file-icon">📄</span>
@ -192,17 +189,13 @@
</div>
</div>
<!-- 启动按钮 -->
<div class="console-section btn-section">
<button
class="start-engine-btn"
@click="startSimulation"
<!-- Mode selector (CTA) -->
<div class="console-section mode-selector-section">
<ModeSelector
projectId="new"
:disabled="!canSubmit || loading"
>
<span v-if="!loading">{{ $t('home.startEngine') }}</span>
<span v-else>{{ $t('home.initializing') }}</span>
<span class="btn-arrow"></span>
</button>
@mode-selected="handleModeSelected"
/>
</div>
</div>
</div>
@ -216,19 +209,10 @@
<script setup>
import { ref, computed } from 'vue'
import { useRouter } from 'vue-router'
import HistoryDatabase from '../components/HistoryDatabase.vue'
import LanguageSwitcher from '../components/LanguageSwitcher.vue'
import ModeSelector from '../components/ModeSelector.vue'
const router = useRouter()
// Mode sélectionné (public | private)
const selectedMode = ref(null)
const handleModeSelected = (mode) => {
selectedMode.value = mode
}
import { setPendingUpload } from '../store/pendingUpload.js'
//
const formData = ref({
@ -240,7 +224,6 @@ const files = ref([])
//
const loading = ref(false)
const error = ref('')
const isDragOver = ref(false)
//
@ -251,6 +234,14 @@ const canSubmit = computed(() => {
return formData.value.simulationRequirement.trim() !== '' && files.value.length > 0
})
// ModeSelector prend la main sur la navigation. Son emit `mode-selected` est
// synchrone et arrive AVANT son `router.push`, ce qui permet de stocker la
// pending upload avant que MainView ne la lise via getPendingUpload().
const handleModeSelected = () => {
if (!canSubmit.value || loading.value) return
setPendingUpload(files.value, formData.value.simulationRequirement)
}
//
const triggerFileInput = () => {
if (!loading.value) {
@ -305,22 +296,6 @@ const scrollToBottom = () => {
})
}
// - APIProcess
const startSimulation = () => {
if (!canSubmit.value || loading.value) return
//
import('../store/pendingUpload.js').then(({ setPendingUpload }) => {
setPendingUpload(files.value, formData.value.simulationRequirement)
// Process使
router.push({
name: 'Process',
params: { projectId: 'new' },
query: selectedMode.value === 'private' ? { mode: 'private' } : {},
})
})
}
</script>
<style scoped>
@ -684,10 +659,6 @@ const startSimulation = () => {
flex: 1.2;
}
.mode-selector-wrapper {
margin-bottom: 20px;
}
.console-box {
border: 1px solid #CCC; /* 外部实线 */
padding: 8px; /* 内边距形成双重边框感 */
@ -697,7 +668,7 @@ const startSimulation = () => {
padding: 20px;
}
.console-section.btn-section {
.console-section.mode-selector-section {
padding-top: 0;
}
@ -839,57 +810,6 @@ const startSimulation = () => {
color: #AAA;
}
.start-engine-btn {
width: 100%;
background: var(--black);
color: var(--white);
border: none;
padding: 20px;
font-family: var(--font-mono);
font-weight: 700;
font-size: 1.1rem;
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
transition: all 0.3s ease;
letter-spacing: 1px;
position: relative;
overflow: hidden;
}
/* 可点击状态(非禁用) */
.start-engine-btn:not(:disabled) {
background: var(--black);
border: 1px solid var(--black);
animation: pulse-border 2s infinite;
}
.start-engine-btn:hover:not(:disabled) {
background: var(--orange);
border-color: var(--orange);
transform: translateY(-2px);
}
.start-engine-btn:active:not(:disabled) {
transform: translateY(0);
}
.start-engine-btn:disabled {
background: #E5E5E5;
color: #999;
cursor: not-allowed;
transform: none;
border: 1px solid #E5E5E5;
}
/* 引导动画:微妙的边框脉冲 */
@keyframes pulse-border {
0% { box-shadow: 0 0 0 0 rgba(0, 0, 0, 0.2); }
70% { box-shadow: 0 0 0 6px rgba(0, 0, 0, 0); }
100% { box-shadow: 0 0 0 0 rgba(0, 0, 0, 0); }
}
/* 响应式适配 */
@media (max-width: 1024px) {
.dashboard-section {

File diff suppressed because it is too large Load Diff