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:
parent
75d5a9bf64
commit
9899afe920
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
@ -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 = () => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 开始模拟 - 立即跳转,API调用在Process页面进行
|
|
||||||
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
Loading…
Reference in New Issue