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

View File

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

View File

@ -125,9 +125,6 @@
<!-- 右栏交互控制台 --> <!-- 右栏交互控制台 -->
<div class="right-panel"> <div class="right-panel">
<div class="mode-selector-wrapper">
<ModeSelector @mode-selected="handleModeSelected" />
</div>
<div class="console-box"> <div class="console-box">
<!-- 上传区域 --> <!-- 上传区域 -->
<div class="console-section"> <div class="console-section">
@ -135,8 +132,8 @@
<span class="console-label">{{ $t('home.realitySeed') }}</span> <span class="console-label">{{ $t('home.realitySeed') }}</span>
<span class="console-meta">{{ $t('home.supportedFormats') }}</span> <span class="console-meta">{{ $t('home.supportedFormats') }}</span>
</div> </div>
<div <div
class="upload-zone" class="upload-zone"
:class="{ 'drag-over': isDragOver, 'has-files': files.length > 0 }" :class="{ 'drag-over': isDragOver, 'has-files': files.length > 0 }"
@dragover.prevent="handleDragOver" @dragover.prevent="handleDragOver"
@ -153,13 +150,13 @@
style="display: none" style="display: none"
:disabled="loading" :disabled="loading"
/> />
<div v-if="files.length === 0" class="upload-placeholder"> <div v-if="files.length === 0" class="upload-placeholder">
<div class="upload-icon"></div> <div class="upload-icon"></div>
<div class="upload-title">{{ $t('home.dragToUpload') }}</div> <div class="upload-title">{{ $t('home.dragToUpload') }}</div>
<div class="upload-hint">{{ $t('home.orBrowse') }}</div> <div class="upload-hint">{{ $t('home.orBrowse') }}</div>
</div> </div>
<div v-else class="file-list"> <div v-else class="file-list">
<div v-for="(file, index) in files" :key="index" class="file-item"> <div v-for="(file, index) in files" :key="index" class="file-item">
<span class="file-icon">📄</span> <span class="file-icon">📄</span>
@ -192,17 +189,13 @@
</div> </div>
</div> </div>
<!-- 启动按钮 --> <!-- Mode selector (CTA) -->
<div class="console-section btn-section"> <div class="console-section mode-selector-section">
<button <ModeSelector
class="start-engine-btn" projectId="new"
@click="startSimulation"
:disabled="!canSubmit || loading" :disabled="!canSubmit || loading"
> @mode-selected="handleModeSelected"
<span v-if="!loading">{{ $t('home.startEngine') }}</span> />
<span v-else>{{ $t('home.initializing') }}</span>
<span class="btn-arrow"></span>
</button>
</div> </div>
</div> </div>
</div> </div>
@ -216,19 +209,10 @@
<script setup> <script setup>
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
import { useRouter } from 'vue-router'
import HistoryDatabase from '../components/HistoryDatabase.vue' import HistoryDatabase from '../components/HistoryDatabase.vue'
import LanguageSwitcher from '../components/LanguageSwitcher.vue' import LanguageSwitcher from '../components/LanguageSwitcher.vue'
import ModeSelector from '../components/ModeSelector.vue' import ModeSelector from '../components/ModeSelector.vue'
import { setPendingUpload } from '../store/pendingUpload.js'
const router = useRouter()
// Mode sélectionné (public | private)
const selectedMode = ref(null)
const handleModeSelected = (mode) => {
selectedMode.value = mode
}
// //
const formData = ref({ const formData = ref({
@ -240,7 +224,6 @@ const files = ref([])
// //
const loading = ref(false) const loading = ref(false)
const error = ref('')
const isDragOver = ref(false) const isDragOver = ref(false)
// //
@ -251,6 +234,14 @@ const canSubmit = computed(() => {
return formData.value.simulationRequirement.trim() !== '' && files.value.length > 0 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 = () => { const triggerFileInput = () => {
if (!loading.value) { 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> </script>
<style scoped> <style scoped>
@ -684,10 +659,6 @@ const startSimulation = () => {
flex: 1.2; flex: 1.2;
} }
.mode-selector-wrapper {
margin-bottom: 20px;
}
.console-box { .console-box {
border: 1px solid #CCC; /* 外部实线 */ border: 1px solid #CCC; /* 外部实线 */
padding: 8px; /* 内边距形成双重边框感 */ padding: 8px; /* 内边距形成双重边框感 */
@ -697,7 +668,7 @@ const startSimulation = () => {
padding: 20px; padding: 20px;
} }
.console-section.btn-section { .console-section.mode-selector-section {
padding-top: 0; padding-top: 0;
} }
@ -839,57 +810,6 @@ const startSimulation = () => {
color: #AAA; 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) { @media (max-width: 1024px) {
.dashboard-section { .dashboard-section {

File diff suppressed because it is too large Load Diff