Trae Assistant commited on
Commit
e62ad0b
·
1 Parent(s): d80bcd3

Fix: Revert Jinja2 to default, use Vue custom delimiters, add Dark Mode

Browse files
Files changed (2) hide show
  1. app.py +3 -6
  2. templates/index.html +97 -64
app.py CHANGED
@@ -8,12 +8,9 @@ app = Flask(__name__, static_folder='static', template_folder='templates')
8
  app.config['MAX_CONTENT_LENGTH'] = 2 * 1024 * 1024 # Limit upload to 2MB (JSON files shouldn't be large)
9
 
10
  # Jinja2 configuration to avoid conflict with Vue.js
11
- app.jinja_env.variable_start_string = '(('
12
- app.jinja_env.variable_end_string = '))'
13
- app.jinja_env.block_start_string = '((%'
14
- app.jinja_env.block_end_string = '%))'
15
- app.jinja_env.comment_start_string = '((#'
16
- app.jinja_env.comment_end_string = '#))'
17
 
18
  # Project Metadata
19
  PROJECT_INFO = {
 
8
  app.config['MAX_CONTENT_LENGTH'] = 2 * 1024 * 1024 # Limit upload to 2MB (JSON files shouldn't be large)
9
 
10
  # Jinja2 configuration to avoid conflict with Vue.js
11
+ # Using default Jinja2 delimiters {{ }}. Vue.js will be configured to use ['${', '}']
12
+
13
+
 
 
 
14
 
15
  # Project Metadata
16
  PROJECT_INFO = {
templates/index.html CHANGED
@@ -3,12 +3,13 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>(( project.title_cn )) - (( project.name ))</title>
7
  <script src="https://cdn.tailwindcss.com"></script>
8
  <script src="/static/js/vue.global.js"></script>
9
  <script src="/static/js/html2canvas.min.js"></script>
10
  <script>
11
  tailwind.config = {
 
12
  theme: {
13
  extend: {
14
  colors: {
@@ -38,39 +39,46 @@
38
  .quadrant:hover {
39
  background-color: rgba(243, 244, 246, 0.5);
40
  }
 
 
 
41
  [v-cloak] { display: none; }
42
  </style>
43
  </head>
44
- <body class="bg-gray-50 h-screen flex flex-col font-sans text-gray-800">
45
 
46
  <div id="app" v-cloak class="flex-1 flex flex-col h-full">
47
  <!-- Header -->
48
- <header class="bg-white shadow-sm z-10 px-6 py-3 flex justify-between items-center">
49
  <div class="flex items-center gap-3">
50
  <div class="w-10 h-10 rounded-lg bg-gradient-to-br from-indigo-500 to-purple-600 flex items-center justify-center text-white font-bold text-xl shadow-lg">
51
  S
52
  </div>
53
  <div>
54
- <h1 class="text-xl font-bold text-gray-900">(( project.title_cn ))</h1>
55
- <p class="text-xs text-gray-500">(( project.name )) v(( project.version ))</p>
56
  </div>
57
  </div>
58
- <div class="flex gap-3">
59
- <button @click="clearAll" class="px-4 py-2 text-sm text-red-600 hover:text-red-800 transition-colors flex items-center gap-2">
 
 
 
 
60
  <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path></svg>
61
  清空
62
  </button>
63
- <button @click="resetData" class="px-4 py-2 text-sm text-gray-600 hover:text-indigo-600 transition-colors">
64
  重置演示
65
  </button>
66
  <div class="relative">
67
- <button @click="triggerImport" class="px-4 py-2 text-sm text-gray-600 hover:text-indigo-600 transition-colors flex items-center gap-2">
68
  <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12"></path></svg>
69
  导入
70
  </button>
71
  <input type="file" ref="fileInput" @change="handleImport" class="hidden" accept=".json">
72
  </div>
73
- <button @click="exportJSON" class="px-4 py-2 text-sm text-gray-600 hover:text-indigo-600 transition-colors flex items-center gap-2">
74
  <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"></path></svg>
75
  导出JSON
76
  </button>
@@ -84,39 +92,39 @@
84
  <!-- Main Content -->
85
  <div class="flex-1 flex overflow-hidden">
86
  <!-- Sidebar: Controls & List -->
87
- <div class="w-80 bg-white border-r border-gray-200 flex flex-col z-10 shadow-lg">
88
- <div class="p-5 border-b border-gray-100">
89
- <h2 class="font-bold text-gray-800 mb-4 flex items-center gap-2">
90
  <span class="w-1 h-5 bg-indigo-500 rounded-full"></span>
91
  添加相关方
92
  </h2>
93
  <div class="space-y-3">
94
  <div>
95
- <label class="block text-xs font-medium text-gray-500 mb-1">名称</label>
96
- <input v-model="newStakeholder.name" @keyup.enter="addStakeholder" type="text" placeholder="例如:CEO, 核心用户..." class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none transition-all">
97
  </div>
98
  <div>
99
- <label class="block text-xs font-medium text-gray-500 mb-1">角色类型</label>
100
  <div class="grid grid-cols-2 gap-2">
101
  <button v-for="cat in categories" :key="cat.id"
102
  @click="newStakeholder.category = cat.id"
103
  :class="{'ring-2 ring-offset-1': newStakeholder.category === cat.id}"
104
  class="px-2 py-1.5 rounded text-xs font-medium text-white transition-all text-center truncate"
105
  :style="{backgroundColor: cat.color}">
106
- {{ cat.name }}
107
  </button>
108
  </div>
109
  </div>
110
- <button @click="addStakeholder" :disabled="!newStakeholder.name" class="w-full py-2 bg-gray-900 text-white rounded-md text-sm font-medium hover:bg-gray-800 transition-colors disabled:opacity-50 disabled:cursor-not-allowed">
111
  添加至待定区
112
  </button>
113
  </div>
114
  </div>
115
 
116
- <div class="flex-1 overflow-y-auto p-4 bg-gray-50">
117
  <h3 class="text-xs font-bold text-gray-400 uppercase tracking-wider mb-3">待定区 (拖拽至右侧)</h3>
118
  <div
119
- class="min-h-[100px] border-2 border-dashed border-gray-300 rounded-lg p-2 flex flex-col gap-2 transition-colors"
120
  @dragover.prevent
121
  @drop="onDrop($event, 'sidebar')"
122
  >
@@ -128,12 +136,12 @@
128
  :key="s.id"
129
  draggable="true"
130
  @dragstart="onDragStart($event, s)"
131
- class="stakeholder-card bg-white p-3 rounded shadow-sm border-l-4 flex justify-between items-center group"
132
  :style="{borderLeftColor: getCategoryColor(s.category)}"
133
  >
134
  <div>
135
- <div class="font-bold text-gray-800 text-sm">{{ s.name }}</div>
136
- <div class="text-[10px] text-gray-500">{{ getCategoryName(s.category) }}</div>
137
  </div>
138
  <button @click="deleteStakeholder(s.id)" class="text-gray-300 hover:text-red-500 opacity-0 group-hover:opacity-100 transition-opacity">
139
  <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path></svg>
@@ -141,9 +149,9 @@
141
  </div>
142
  </div>
143
 
144
- <div class="mt-6 bg-blue-50 p-3 rounded-lg border border-blue-100">
145
- <h4 class="text-xs font-bold text-blue-800 mb-1">使用说明</h4>
146
- <ul class="text-xs text-blue-700 list-disc list-inside space-y-1">
147
  <li>输入名称并选择类型,添加相关方</li>
148
  <li>将卡片拖拽至右侧矩阵的相应位置</li>
149
  <li><b>Power (权力)</b>: 影响项目的能力</li>
@@ -155,67 +163,67 @@
155
  </div>
156
 
157
  <!-- Canvas Area -->
158
- <div class="flex-1 bg-gray-100 p-8 overflow-auto flex items-center justify-center relative">
159
 
160
- <div id="capture-area" class="bg-white shadow-2xl rounded-xl w-[900px] min-h-[800px] flex flex-col p-6 relative select-none">
161
  <!-- Title for Export -->
162
  <div class="text-center mb-4">
163
- <h2 class="text-2xl font-bold text-gray-800">利益相关者分析矩阵 (Stakeholder Matrix)</h2>
164
- <p class="text-sm text-gray-500">Power-Interest Grid</p>
165
  </div>
166
 
167
  <!-- Matrix Container -->
168
- <div class="flex-1 relative border-l-2 border-b-2 border-gray-800 ml-8 mb-8">
169
 
170
  <!-- Axis Labels -->
171
- <div class="absolute -left-10 top-1/2 -translate-y-1/2 -rotate-90 text-sm font-bold tracking-widest text-gray-600">
172
  权力 (POWER)
173
- <span class="text-xs font-normal block text-center text-gray-400">Low → High</span>
174
  </div>
175
- <div class="absolute bottom-[-40px] left-1/2 -translate-x-1/2 text-sm font-bold tracking-widest text-gray-600">
176
  利益 (INTEREST)
177
- <span class="text-xs font-normal block text-center text-gray-400">Low → High</span>
178
  </div>
179
 
180
  <!-- Grid Background -->
181
  <div class="absolute inset-0 grid grid-cols-2 grid-rows-2">
182
  <!-- Top Left: High Power, Low Interest -->
183
- <div class="quadrant border-r border-b border-gray-200 bg-gray-50/30 p-4 relative"
184
  @dragover.prevent @drop="onDrop($event, 'matrix', {p:'high', i:'low'})">
185
- <div class="absolute top-2 left-2 text-indigo-900 font-bold opacity-20 text-4xl">A</div>
186
  <div class="absolute top-4 left-4">
187
- <h3 class="font-bold text-gray-700">令其满意 (Keep Satisfied)</h3>
188
- <p class="text-xs text-gray-500">High Power, Low Interest</p>
189
  </div>
190
  </div>
191
 
192
  <!-- Top Right: High Power, High Interest -->
193
- <div class="quadrant border-b border-gray-200 bg-indigo-50/30 p-4 relative"
194
  @dragover.prevent @drop="onDrop($event, 'matrix', {p:'high', i:'high'})">
195
- <div class="absolute top-2 right-2 text-indigo-900 font-bold opacity-20 text-4xl">B</div>
196
  <div class="absolute top-4 right-4 text-right">
197
- <h3 class="font-bold text-indigo-700">重点管理 (Manage Closely)</h3>
198
- <p class="text-xs text-indigo-500">High Power, High Interest</p>
199
  </div>
200
  </div>
201
 
202
  <!-- Bottom Left: Low Power, Low Interest -->
203
- <div class="quadrant border-r border-gray-200 bg-white p-4 relative"
204
  @dragover.prevent @drop="onDrop($event, 'matrix', {p:'low', i:'low'})">
205
- <div class="absolute bottom-2 left-2 text-indigo-900 font-bold opacity-20 text-4xl">C</div>
206
  <div class="absolute bottom-4 left-4">
207
- <h3 class="font-bold text-gray-500">最小关注 (Monitor)</h3>
208
- <p class="text-xs text-gray-400">Low Power, Low Interest</p>
209
  </div>
210
  </div>
211
 
212
  <!-- Bottom Right: Low Power, High Interest -->
213
- <div class="quadrant bg-gray-50/30 p-4 relative"
214
  @dragover.prevent @drop="onDrop($event, 'matrix', {p:'low', i:'high'})">
215
- <div class="absolute bottom-2 right-2 text-indigo-900 font-bold opacity-20 text-4xl">D</div>
216
  <div class="absolute bottom-4 right-4 text-right">
217
- <h3 class="font-bold text-gray-700">随时告知 (Keep Informed)</h3>
218
- <p class="text-xs text-gray-500">Low Power, High Interest</p>
219
  </div>
220
  </div>
221
  </div>
@@ -232,15 +240,15 @@
232
  @dblclick="moveToSidebar(s)"
233
  >
234
  <div
235
- class="w-12 h-12 rounded-full shadow-lg border-2 border-white flex items-center justify-center text-white font-bold text-xs relative transition-transform hover:scale-110"
236
  :style="{backgroundColor: getCategoryColor(s.category)}"
237
  :title="s.name + ' (' + getCategoryName(s.category) + ')'"
238
  >
239
- {{ s.name.substring(0, 2) }}
240
 
241
  <!-- Tooltip -->
242
  <div class="absolute -bottom-8 left-1/2 -translate-x-1/2 bg-gray-800 text-white text-[10px] px-2 py-1 rounded opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap z-20 pointer-events-none">
243
- {{ s.name }}
244
  </div>
245
  </div>
246
  </div>
@@ -248,28 +256,28 @@
248
  </div>
249
 
250
  <!-- Legend & Report -->
251
- <div class="mt-2 border-t pt-4 border-gray-100">
252
  <div class="flex justify-center gap-6 mb-4">
253
  <div v-for="cat in categories" :key="cat.id" class="flex items-center gap-2">
254
  <div class="w-3 h-3 rounded-full" :style="{backgroundColor: cat.color}"></div>
255
- <span class="text-xs text-gray-600">{{ cat.name }}</span>
256
  </div>
257
  </div>
258
 
259
  <!-- Analysis Report -->
260
- <div v-if="matrixStakeholders.length > 0" class="bg-indigo-50 rounded-lg p-4 text-sm text-gray-700">
261
- <h3 class="font-bold text-indigo-900 mb-2 flex items-center gap-2">
262
  <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"></path></svg>
263
  智能分析报告
264
  </h3>
265
  <div class="grid grid-cols-4 gap-4">
266
- <div v-for="(count, key) in quadrantCounts" :key="key" class="flex flex-col items-center border-r last:border-0 border-indigo-200">
267
- <span class="text-xs text-gray-500">{{ getQuadrantLabel(key) }}</span>
268
- <span class="font-bold text-lg text-indigo-700">{{ count }}</span>
269
  </div>
270
  </div>
271
- <p class="mt-3 text-xs text-indigo-600 text-center">
272
- 💡 建议:重点关注 <b>{{ quadrantCounts['high-high'] || 0 }}</b> 位“重点管理”对象,需建立紧密沟通机制。
273
  </p>
274
  </div>
275
  </div>
@@ -283,6 +291,7 @@
283
  const { createApp, ref, computed, onMounted, watch } = Vue;
284
 
285
  createApp({
 
286
  setup() {
287
  const categories = [
288
  { id: 'internal', name: '内部人员', color: '#4f46e5' }, // Indigo
@@ -296,8 +305,30 @@
296
  const draggedItem = ref(null);
297
  const fileInput = ref(null);
298
 
 
 
 
 
 
 
 
 
 
 
 
 
 
299
  // Initialize with some data if empty
300
  onMounted(() => {
 
 
 
 
 
 
 
 
 
301
  const saved = localStorage.getItem('stakeholder-map-data');
302
  if (saved) {
303
  stakeholders.value = JSON.parse(saved);
@@ -520,7 +551,9 @@
520
  exportJSON,
521
  quadrantCounts,
522
  getQuadrantLabel,
523
- clearAll
 
 
524
  };
525
  }
526
  }).mount('#app');
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>{{ project.title_cn }} - {{ project.name }}</title>
7
  <script src="https://cdn.tailwindcss.com"></script>
8
  <script src="/static/js/vue.global.js"></script>
9
  <script src="/static/js/html2canvas.min.js"></script>
10
  <script>
11
  tailwind.config = {
12
+ darkMode: 'class',
13
  theme: {
14
  extend: {
15
  colors: {
 
39
  .quadrant:hover {
40
  background-color: rgba(243, 244, 246, 0.5);
41
  }
42
+ .dark .quadrant:hover {
43
+ background-color: rgba(55, 65, 81, 0.5);
44
+ }
45
  [v-cloak] { display: none; }
46
  </style>
47
  </head>
48
+ <body class="bg-gray-50 dark:bg-gray-900 h-screen flex flex-col font-sans text-gray-800 dark:text-gray-100 transition-colors duration-200">
49
 
50
  <div id="app" v-cloak class="flex-1 flex flex-col h-full">
51
  <!-- Header -->
52
+ <header class="bg-white dark:bg-gray-800 shadow-sm z-10 px-6 py-3 flex justify-between items-center transition-colors">
53
  <div class="flex items-center gap-3">
54
  <div class="w-10 h-10 rounded-lg bg-gradient-to-br from-indigo-500 to-purple-600 flex items-center justify-center text-white font-bold text-xl shadow-lg">
55
  S
56
  </div>
57
  <div>
58
+ <h1 class="text-xl font-bold text-gray-900 dark:text-white">{{ project.title_cn }}</h1>
59
+ <p class="text-xs text-gray-500 dark:text-gray-400">{{ project.name }} v{{ project.version }}</p>
60
  </div>
61
  </div>
62
+ <div class="flex gap-3 items-center">
63
+ <button @click="toggleDark" class="p-2 text-gray-500 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white transition-colors rounded-full hover:bg-gray-100 dark:hover:bg-gray-700">
64
+ <span v-if="!isDark">🌙</span>
65
+ <span v-else>☀️</span>
66
+ </button>
67
+ <button @click="clearAll" class="px-4 py-2 text-sm text-red-600 hover:text-red-800 dark:text-red-400 dark:hover:text-red-300 transition-colors flex items-center gap-2">
68
  <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path></svg>
69
  清空
70
  </button>
71
+ <button @click="resetData" class="px-4 py-2 text-sm text-gray-600 hover:text-indigo-600 dark:text-gray-300 dark:hover:text-indigo-400 transition-colors">
72
  重置演示
73
  </button>
74
  <div class="relative">
75
+ <button @click="triggerImport" class="px-4 py-2 text-sm text-gray-600 hover:text-indigo-600 dark:text-gray-300 dark:hover:text-indigo-400 transition-colors flex items-center gap-2">
76
  <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12"></path></svg>
77
  导入
78
  </button>
79
  <input type="file" ref="fileInput" @change="handleImport" class="hidden" accept=".json">
80
  </div>
81
+ <button @click="exportJSON" class="px-4 py-2 text-sm text-gray-600 hover:text-indigo-600 dark:text-gray-300 dark:hover:text-indigo-400 transition-colors flex items-center gap-2">
82
  <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"></path></svg>
83
  导出JSON
84
  </button>
 
92
  <!-- Main Content -->
93
  <div class="flex-1 flex overflow-hidden">
94
  <!-- Sidebar: Controls & List -->
95
+ <div class="w-80 bg-white dark:bg-gray-800 border-r border-gray-200 dark:border-gray-700 flex flex-col z-10 shadow-lg transition-colors">
96
+ <div class="p-5 border-b border-gray-100 dark:border-gray-700">
97
+ <h2 class="font-bold text-gray-800 dark:text-white mb-4 flex items-center gap-2">
98
  <span class="w-1 h-5 bg-indigo-500 rounded-full"></span>
99
  添加相关方
100
  </h2>
101
  <div class="space-y-3">
102
  <div>
103
+ <label class="block text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">名称</label>
104
+ <input v-model="newStakeholder.name" @keyup.enter="addStakeholder" type="text" placeholder="例如:CEO, 核心用户..." class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md text-sm focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none transition-all bg-white dark:bg-gray-700 dark:text-white">
105
  </div>
106
  <div>
107
+ <label class="block text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">角色类型</label>
108
  <div class="grid grid-cols-2 gap-2">
109
  <button v-for="cat in categories" :key="cat.id"
110
  @click="newStakeholder.category = cat.id"
111
  :class="{'ring-2 ring-offset-1': newStakeholder.category === cat.id}"
112
  class="px-2 py-1.5 rounded text-xs font-medium text-white transition-all text-center truncate"
113
  :style="{backgroundColor: cat.color}">
114
+ ${ cat.name }
115
  </button>
116
  </div>
117
  </div>
118
+ <button @click="addStakeholder" :disabled="!newStakeholder.name" class="w-full py-2 bg-gray-900 dark:bg-indigo-600 text-white rounded-md text-sm font-medium hover:bg-gray-800 dark:hover:bg-indigo-500 transition-colors disabled:opacity-50 disabled:cursor-not-allowed">
119
  添加至待定区
120
  </button>
121
  </div>
122
  </div>
123
 
124
+ <div class="flex-1 overflow-y-auto p-4 bg-gray-50 dark:bg-gray-900 transition-colors">
125
  <h3 class="text-xs font-bold text-gray-400 uppercase tracking-wider mb-3">待定区 (拖拽至右侧)</h3>
126
  <div
127
+ class="min-h-[100px] border-2 border-dashed border-gray-300 dark:border-gray-700 rounded-lg p-2 flex flex-col gap-2 transition-colors"
128
  @dragover.prevent
129
  @drop="onDrop($event, 'sidebar')"
130
  >
 
136
  :key="s.id"
137
  draggable="true"
138
  @dragstart="onDragStart($event, s)"
139
+ class="stakeholder-card bg-white dark:bg-gray-800 p-3 rounded shadow-sm border-l-4 flex justify-between items-center group transition-colors"
140
  :style="{borderLeftColor: getCategoryColor(s.category)}"
141
  >
142
  <div>
143
+ <div class="font-bold text-gray-800 dark:text-gray-200 text-sm">${ s.name }</div>
144
+ <div class="text-[10px] text-gray-500 dark:text-gray-400">${ getCategoryName(s.category) }</div>
145
  </div>
146
  <button @click="deleteStakeholder(s.id)" class="text-gray-300 hover:text-red-500 opacity-0 group-hover:opacity-100 transition-opacity">
147
  <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path></svg>
 
149
  </div>
150
  </div>
151
 
152
+ <div class="mt-6 bg-blue-50 dark:bg-blue-900/30 p-3 rounded-lg border border-blue-100 dark:border-blue-900/50">
153
+ <h4 class="text-xs font-bold text-blue-800 dark:text-blue-300 mb-1">使用说明</h4>
154
+ <ul class="text-xs text-blue-700 dark:text-blue-400 list-disc list-inside space-y-1">
155
  <li>输入名称并选择类型,添加相关方</li>
156
  <li>将卡片拖拽至右侧矩阵的相应位置</li>
157
  <li><b>Power (权力)</b>: 影响项目的能力</li>
 
163
  </div>
164
 
165
  <!-- Canvas Area -->
166
+ <div class="flex-1 bg-gray-100 dark:bg-gray-900 p-8 overflow-auto flex items-center justify-center relative transition-colors">
167
 
168
+ <div id="capture-area" class="bg-white dark:bg-gray-800 shadow-2xl rounded-xl w-[900px] min-h-[800px] flex flex-col p-6 relative select-none transition-colors">
169
  <!-- Title for Export -->
170
  <div class="text-center mb-4">
171
+ <h2 class="text-2xl font-bold text-gray-800 dark:text-white">利益相关者分析矩阵 (Stakeholder Matrix)</h2>
172
+ <p class="text-sm text-gray-500 dark:text-gray-400">Power-Interest Grid</p>
173
  </div>
174
 
175
  <!-- Matrix Container -->
176
+ <div class="flex-1 relative border-l-2 border-b-2 border-gray-800 dark:border-gray-400 ml-8 mb-8">
177
 
178
  <!-- Axis Labels -->
179
+ <div class="absolute -left-10 top-1/2 -translate-y-1/2 -rotate-90 text-sm font-bold tracking-widest text-gray-600 dark:text-gray-400">
180
  权力 (POWER)
181
+ <span class="text-xs font-normal block text-center text-gray-400 dark:text-gray-500">Low → High</span>
182
  </div>
183
+ <div class="absolute bottom-[-40px] left-1/2 -translate-x-1/2 text-sm font-bold tracking-widest text-gray-600 dark:text-gray-400">
184
  利益 (INTEREST)
185
+ <span class="text-xs font-normal block text-center text-gray-400 dark:text-gray-500">Low → High</span>
186
  </div>
187
 
188
  <!-- Grid Background -->
189
  <div class="absolute inset-0 grid grid-cols-2 grid-rows-2">
190
  <!-- Top Left: High Power, Low Interest -->
191
+ <div class="quadrant border-r border-b border-gray-200 dark:border-gray-600 bg-gray-50/30 dark:bg-gray-700/30 p-4 relative"
192
  @dragover.prevent @drop="onDrop($event, 'matrix', {p:'high', i:'low'})">
193
+ <div class="absolute top-2 left-2 text-indigo-900 dark:text-indigo-400 font-bold opacity-20 text-4xl">A</div>
194
  <div class="absolute top-4 left-4">
195
+ <h3 class="font-bold text-gray-700 dark:text-gray-300">令其满意 (Keep Satisfied)</h3>
196
+ <p class="text-xs text-gray-500 dark:text-gray-400">High Power, Low Interest</p>
197
  </div>
198
  </div>
199
 
200
  <!-- Top Right: High Power, High Interest -->
201
+ <div class="quadrant border-b border-gray-200 dark:border-gray-600 bg-indigo-50/30 dark:bg-indigo-900/20 p-4 relative"
202
  @dragover.prevent @drop="onDrop($event, 'matrix', {p:'high', i:'high'})">
203
+ <div class="absolute top-2 right-2 text-indigo-900 dark:text-indigo-400 font-bold opacity-20 text-4xl">B</div>
204
  <div class="absolute top-4 right-4 text-right">
205
+ <h3 class="font-bold text-indigo-700 dark:text-indigo-400">重点管理 (Manage Closely)</h3>
206
+ <p class="text-xs text-indigo-500 dark:text-indigo-300">High Power, High Interest</p>
207
  </div>
208
  </div>
209
 
210
  <!-- Bottom Left: Low Power, Low Interest -->
211
+ <div class="quadrant border-r border-gray-200 dark:border-gray-600 bg-white dark:bg-gray-800 p-4 relative"
212
  @dragover.prevent @drop="onDrop($event, 'matrix', {p:'low', i:'low'})">
213
+ <div class="absolute bottom-2 left-2 text-indigo-900 dark:text-indigo-400 font-bold opacity-20 text-4xl">C</div>
214
  <div class="absolute bottom-4 left-4">
215
+ <h3 class="font-bold text-gray-500 dark:text-gray-400">最小关注 (Monitor)</h3>
216
+ <p class="text-xs text-gray-400 dark:text-gray-500">Low Power, Low Interest</p>
217
  </div>
218
  </div>
219
 
220
  <!-- Bottom Right: Low Power, High Interest -->
221
+ <div class="quadrant bg-gray-50/30 dark:bg-gray-700/30 p-4 relative"
222
  @dragover.prevent @drop="onDrop($event, 'matrix', {p:'low', i:'high'})">
223
+ <div class="absolute bottom-2 right-2 text-indigo-900 dark:text-indigo-400 font-bold opacity-20 text-4xl">D</div>
224
  <div class="absolute bottom-4 right-4 text-right">
225
+ <h3 class="font-bold text-gray-700 dark:text-gray-300">随时告知 (Keep Informed)</h3>
226
+ <p class="text-xs text-gray-500 dark:text-gray-400">Low Power, High Interest</p>
227
  </div>
228
  </div>
229
  </div>
 
240
  @dblclick="moveToSidebar(s)"
241
  >
242
  <div
243
+ class="w-12 h-12 rounded-full shadow-lg border-2 border-white dark:border-gray-600 flex items-center justify-center text-white font-bold text-xs relative transition-transform hover:scale-110"
244
  :style="{backgroundColor: getCategoryColor(s.category)}"
245
  :title="s.name + ' (' + getCategoryName(s.category) + ')'"
246
  >
247
+ ${ s.name.substring(0, 2) }
248
 
249
  <!-- Tooltip -->
250
  <div class="absolute -bottom-8 left-1/2 -translate-x-1/2 bg-gray-800 text-white text-[10px] px-2 py-1 rounded opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap z-20 pointer-events-none">
251
+ ${ s.name }
252
  </div>
253
  </div>
254
  </div>
 
256
  </div>
257
 
258
  <!-- Legend & Report -->
259
+ <div class="mt-2 border-t pt-4 border-gray-100 dark:border-gray-700">
260
  <div class="flex justify-center gap-6 mb-4">
261
  <div v-for="cat in categories" :key="cat.id" class="flex items-center gap-2">
262
  <div class="w-3 h-3 rounded-full" :style="{backgroundColor: cat.color}"></div>
263
+ <span class="text-xs text-gray-600 dark:text-gray-400">${ cat.name }</span>
264
  </div>
265
  </div>
266
 
267
  <!-- Analysis Report -->
268
+ <div v-if="matrixStakeholders.length > 0" class="bg-indigo-50 dark:bg-indigo-900/20 rounded-lg p-4 text-sm text-gray-700 dark:text-gray-300">
269
+ <h3 class="font-bold text-indigo-900 dark:text-indigo-300 mb-2 flex items-center gap-2">
270
  <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"></path></svg>
271
  智能分析报告
272
  </h3>
273
  <div class="grid grid-cols-4 gap-4">
274
+ <div v-for="(count, key) in quadrantCounts" :key="key" class="flex flex-col items-center border-r last:border-0 border-indigo-200 dark:border-indigo-800">
275
+ <span class="text-xs text-gray-500 dark:text-gray-400">${ getQuadrantLabel(key) }</span>
276
+ <span class="font-bold text-lg text-indigo-700 dark:text-indigo-400">${ count }</span>
277
  </div>
278
  </div>
279
+ <p class="mt-3 text-xs text-indigo-600 dark:text-indigo-300 text-center">
280
+ 💡 建议:重点关注 <b>${ quadrantCounts['high-high'] || 0 }</b> 位“重点管理”对象,需建立紧密沟通机制。
281
  </p>
282
  </div>
283
  </div>
 
291
  const { createApp, ref, computed, onMounted, watch } = Vue;
292
 
293
  createApp({
294
+ delimiters: ['${', '}'],
295
  setup() {
296
  const categories = [
297
  { id: 'internal', name: '内部人员', color: '#4f46e5' }, // Indigo
 
305
  const draggedItem = ref(null);
306
  const fileInput = ref(null);
307
 
308
+ // Dark Mode Logic
309
+ const isDark = ref(false);
310
+ const toggleDark = () => {
311
+ isDark.value = !isDark.value;
312
+ if (isDark.value) {
313
+ document.documentElement.classList.add('dark');
314
+ localStorage.setItem('theme', 'dark');
315
+ } else {
316
+ document.documentElement.classList.remove('dark');
317
+ localStorage.setItem('theme', 'light');
318
+ }
319
+ };
320
+
321
  // Initialize with some data if empty
322
  onMounted(() => {
323
+ // Check Theme
324
+ if (localStorage.getItem('theme') === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
325
+ isDark.value = true;
326
+ document.documentElement.classList.add('dark');
327
+ } else {
328
+ isDark.value = false;
329
+ document.documentElement.classList.remove('dark');
330
+ }
331
+
332
  const saved = localStorage.getItem('stakeholder-map-data');
333
  if (saved) {
334
  stakeholders.value = JSON.parse(saved);
 
551
  exportJSON,
552
  quadrantCounts,
553
  getQuadrantLabel,
554
+ clearAll,
555
+ isDark,
556
+ toggleDark
557
  };
558
  }
559
  }).mount('#app');