StepsDisplay.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504
  1. <template>
  2. <div class="steps-display-container">
  3. <div v-if="parsedContent" class="steps-content">
  4. <button>删除</button>
  5. <h2 class="steps-title">{{ parsedContent.title }}</h2>
  6. <!-- <p class="steps-id">计划ID: {{ parsedContent.planId }}</p> -->
  7. <el-steps
  8. :active="activeStep"
  9. finish-status="success"
  10. class="custom-steps"
  11. :space="60"
  12. direction="vertical"
  13. >
  14. <el-step
  15. v-for="(step, index) in parsedContent.steps"
  16. :key="index"
  17. :title="'步骤 ' + (index + 1)"
  18. >
  19. <template #icon>
  20. <div class="step-icon-container">
  21. <div v-if="index < activeStep" class="step-icon completed">
  22. <el-icon><Check /></el-icon>
  23. </div>
  24. <div v-else-if="index === activeStep" class="step-icon current">
  25. <el-icon><Loading /></el-icon>
  26. </div>
  27. <div v-else style="border-radius: 50%;">
  28. {{ index + 1 }}
  29. </div>
  30. </div>
  31. </template>
  32. <template #title>
  33. <div class="step-title-container" @click.stop="toggleCollapse(index)">
  34. <span>步骤 {{ index + 1 }}</span>
  35. <el-button
  36. type="text"
  37. class="collapse-button"
  38. >
  39. <el-icon>
  40. <component :is="collapsedSteps[index] ? 'ArrowDown' : 'ArrowUp'"></component>
  41. </el-icon>
  42. </el-button>
  43. </div>
  44. </template>
  45. <template #description>
  46. <div :class="['step-description', { 'collapsed': collapsedSteps[index] }]">
  47. <div class="step-content">
  48. {{ step.stepRequirement }}
  49. <div>22312312</div>
  50. <div>22312312</div>
  51. <div>22312312</div>
  52. <div>22312312</div>
  53. </div>
  54. </div>
  55. </template>
  56. </el-step>
  57. </el-steps>
  58. <!--
  59. <div class="steps-controls">
  60. <el-button
  61. type="primary"
  62. size="small"
  63. @click="prevStep"
  64. :disabled="activeStep <= 0"
  65. >
  66. 上一步
  67. </el-button>
  68. <el-button
  69. type="primary"
  70. size="small"
  71. @click="nextStep"
  72. :disabled="activeStep >= parsedContent.steps.length"
  73. >
  74. 下一步
  75. </el-button>
  76. <el-button
  77. type="success"
  78. size="small"
  79. @click="completeAll"
  80. :disabled="activeStep >= parsedContent.steps.length"
  81. >
  82. 完成所有
  83. </el-button>
  84. </div> -->
  85. </div>
  86. <div v-else class="error-message">
  87. <el-alert
  88. title="内容格式错误"
  89. type="error"
  90. description="无法解析传入的内容,请检查格式是否正确"
  91. show-icon
  92. />
  93. </div>
  94. </div>
  95. </template>
  96. <script lang="ts" setup>
  97. import { ref, computed, onMounted, watch, onBeforeUnmount } from 'vue'
  98. import { ChatDotRound, Check, Loading, ArrowDown, ArrowUp } from '@element-plus/icons-vue'
  99. import { deleteExecutor, executePlanByTemplateId, executorDetail } from '@/api/advance'
  100. // 定义props
  101. const props = defineProps({
  102. content: {
  103. type: [String, Object],
  104. required: true
  105. },
  106. initialStep: {
  107. type: Number,
  108. default: 0
  109. }
  110. })
  111. // 定义事件
  112. const emit = defineEmits(['step-change', 'complete'])
  113. // 当前激活的步骤
  114. const activeStep = ref(props.initialStep)
  115. // 步骤折叠状态
  116. const collapsedSteps = ref([])
  117. const planTemplateId = ref('')
  118. const planId = ref('')
  119. const currentStepIndex = ref()
  120. const response = ref()
  121. const handleExecute = async () => {
  122. const res = await executorDetail(planId)
  123. response.value = res
  124. if (!res.completed) handleExecute()
  125. else deleteExecutor(planId)
  126. }
  127. // 解析内容
  128. const parsedContent = computed(() => {
  129. try {
  130. if (typeof props.content === 'string') {
  131. const obj = JSON.parse(props.content)
  132. console.log(obj);
  133. obj.steps = obj.steps.map(_ => ({ ..._, stepRequirement: _.stepRequirement.split(' ')[1] }))
  134. planTemplateId.value = obj.planTemplateId
  135. currentStepIndex.value = 0
  136. setTimeout(() => {
  137. executePlanByTemplateId({ planTemplateId: planTemplateId.value }).then(res => {
  138. planId.value = res.planId
  139. })
  140. }, 1000);
  141. return obj
  142. } else {
  143. return props.content
  144. }
  145. } catch (error) {
  146. console.error('解析内容失败:', error)
  147. return null
  148. }
  149. })
  150. // 初始化折叠状态
  151. watch(() => parsedContent.value, (newContent) => {
  152. if (newContent && newContent.steps) {
  153. // 默认所有步骤都展开,当前步骤之外的都折叠
  154. collapsedSteps.value = newContent.steps.map((_, index) => index !== activeStep.value)
  155. }
  156. }, { immediate: true })
  157. // 切换步骤折叠状态
  158. const toggleCollapse = (index) => {
  159. collapsedSteps.value[index] = !collapsedSteps.value[index]
  160. }
  161. // 获取步骤类型
  162. const getStepType = (requirement) => {
  163. if (requirement.includes('[BROWSER_AGENT]')) {
  164. return 'BROWSER_AGENT'
  165. } else if (requirement.includes('[DEFAULT_AGENT]')) {
  166. return 'DEFAULT_AGENT'
  167. } else {
  168. return 'UNKNOWN'
  169. }
  170. }
  171. // 开始进度条动画
  172. const startProgressAnimation = () => {
  173. // 重置进度
  174. stepProgress.value = 0
  175. // 清除之前的定时器
  176. if (progressTimer) {
  177. clearInterval(progressTimer)
  178. }
  179. // 创建新的定时器,模拟进度增长
  180. progressTimer = setInterval(() => {
  181. if (stepProgress.value < 100) {
  182. // 使用非线性增长,开始快,接近100%时变慢
  183. const increment = Math.max(1, 5 * Math.pow(1 - stepProgress.value / 100, 1.5))
  184. stepProgress.value = Math.min(100, stepProgress.value + increment)
  185. } else {
  186. clearInterval(progressTimer)
  187. }
  188. }, 200) // 每200毫秒更新一次进度,更加流畅
  189. }
  190. // 上一步
  191. const prevStep = () => {
  192. if (activeStep.value > 0) {
  193. // 折叠当前步骤,展开上一步骤
  194. if (collapsedSteps.value[activeStep.value - 1]) {
  195. collapsedSteps.value[activeStep.value - 1] = false
  196. }
  197. if (!collapsedSteps.value[activeStep.value]) {
  198. collapsedSteps.value[activeStep.value] = true
  199. }
  200. activeStep.value--
  201. emit('step-change', activeStep.value)
  202. startProgressAnimation()
  203. }
  204. }
  205. // 下一步
  206. const nextStep = () => {
  207. if (parsedContent.value && activeStep.value < parsedContent.value.steps.length) {
  208. // 折叠当前步骤,展开下一步骤
  209. if (!collapsedSteps.value[activeStep.value]) {
  210. collapsedSteps.value[activeStep.value] = true
  211. }
  212. if (activeStep.value + 1 < parsedContent.value.steps.length && collapsedSteps.value[activeStep.value + 1]) {
  213. collapsedSteps.value[activeStep.value + 1] = false
  214. }
  215. activeStep.value++
  216. emit('step-change', activeStep.value)
  217. if (activeStep.value < parsedContent.value.steps.length) {
  218. startProgressAnimation()
  219. }
  220. }
  221. }
  222. // 完成所有步骤
  223. const completeAll = () => {
  224. if (parsedContent.value) {
  225. activeStep.value = parsedContent.value.steps.length
  226. emit('complete')
  227. // 清除进度条定时器
  228. if (progressTimer) {
  229. clearInterval(progressTimer)
  230. }
  231. }
  232. }
  233. // 监听content变化,重置步骤
  234. watch(() => props.content, () => {
  235. activeStep.value = props.initialStep
  236. // 重置进度条
  237. stepProgress.value = 0
  238. if (progressTimer) {
  239. clearInterval(progressTimer)
  240. }
  241. // 如果有初始步骤,开始进度动画
  242. if (activeStep.value > 0 && activeStep.value < parsedContent.value?.steps.length) {
  243. startProgressAnimation()
  244. }
  245. }, { deep: true })
  246. // 组件挂载时初始化
  247. onMounted(() => {
  248. if (activeStep.value > 0 && parsedContent.value) {
  249. emit('step-change', activeStep.value)
  250. startProgressAnimation()
  251. }
  252. })
  253. // 组件卸载时清理定时器
  254. onBeforeUnmount(() => {
  255. if (progressTimer) {
  256. clearInterval(progressTimer)
  257. }
  258. })
  259. </script>
  260. <style scoped>
  261. .steps-display-container {
  262. padding: 20px;
  263. background-color: #fff;
  264. border-radius: 8px;
  265. box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
  266. }
  267. .steps-title {
  268. font-size: 18px;
  269. font-weight: bold;
  270. margin-bottom: 8px;
  271. color: #303133;
  272. }
  273. .steps-id {
  274. font-size: 14px;
  275. color: #909399;
  276. margin-bottom: 20px;
  277. }
  278. .custom-steps {
  279. margin: 20px 0;
  280. }
  281. .steps-controls {
  282. display: flex;
  283. gap: 10px;
  284. margin-top: 20px;
  285. justify-content: flex-end;
  286. }
  287. .error-message {
  288. margin: 20px 0;
  289. }
  290. :deep(.el-step__title) {
  291. font-weight: bold;
  292. }
  293. :deep(.el-step__description) {
  294. font-size: 14px;
  295. white-space: pre-wrap;
  296. word-break: break-word;
  297. }
  298. :deep(.el-step__icon) {
  299. background-color: #f5f7fa;
  300. }
  301. :deep(.el-step.is-success .el-step__icon) {
  302. background-color: #67c23a;
  303. color: white;
  304. }
  305. :deep(.el-step.is-process .el-step__icon) {
  306. background-color: #409eff;
  307. color: white;
  308. }
  309. /* 步骤标题容器 */
  310. .step-title-container {
  311. display: flex;
  312. align-items: center;
  313. justify-content: space-between;
  314. width: 100%;
  315. cursor: pointer;
  316. }
  317. /* 折叠按钮 */
  318. .collapse-button {
  319. margin-left: 8px;
  320. padding: 2px;
  321. font-size: 12px;
  322. }
  323. /* 步骤描述 */
  324. .step-description {
  325. transition: all 0.3s ease;
  326. overflow: hidden;
  327. max-height: 500px;
  328. }
  329. .step-description.collapsed {
  330. max-height: 30px;
  331. overflow: hidden;
  332. }
  333. /* 步骤内容 */
  334. .step-content {
  335. padding: 8px 0;
  336. }
  337. /* 步骤进度条样式 */
  338. .step-progress {
  339. margin-top: 10px;
  340. display: flex;
  341. align-items: center;
  342. }
  343. .progress-text {
  344. margin-left: 8px;
  345. font-size: 12px;
  346. color: #409eff;
  347. font-weight: bold;
  348. transition: all 0.3s;
  349. }
  350. /* 自定义步骤图标 */
  351. .step-icon-container {
  352. display: flex;
  353. justify-content: center;
  354. align-items: center;
  355. width: 100%;
  356. height: 100%;
  357. }
  358. .step-icon {
  359. display: flex;
  360. justify-content: center;
  361. align-items: center;
  362. width: 24px;
  363. height: 24px;
  364. border-radius: 50%;
  365. background-color: #f5f7fa;
  366. color: #909399;
  367. }
  368. .step-icon.current {
  369. background-color: #409eff;
  370. color: white;
  371. animation: rotate 2s linear infinite, pulse 1.5s infinite;
  372. }
  373. .step-icon.completed {
  374. background-color: #67c23a;
  375. color: white;
  376. }
  377. /* 自定义进度条 */
  378. .progress-bar-container {
  379. position: relative;
  380. width: 100%;
  381. height: 8px;
  382. background-color: #f5f7fa;
  383. border-radius: 4px;
  384. overflow: hidden;
  385. margin-right: 10px;
  386. flex-grow: 1;
  387. }
  388. .progress-bar-background {
  389. position: absolute;
  390. top: 0;
  391. left: 0;
  392. width: 100%;
  393. height: 100%;
  394. background: linear-gradient(90deg, #f5f7fa 0%, #e4e7ed 100%);
  395. opacity: 0.5;
  396. }
  397. .progress-bar-fill {
  398. position: absolute;
  399. top: 0;
  400. left: 0;
  401. height: 100%;
  402. background: linear-gradient(90deg, #409eff 0%, #95d0ff 100%);
  403. border-radius: 4px;
  404. transition: width 0.2s ease-out;
  405. box-shadow: 0 0 5px rgba(64, 158, 255, 0.5);
  406. }
  407. .progress-bar-glow {
  408. position: absolute;
  409. top: 0;
  410. width: 20px;
  411. height: 100%;
  412. background: linear-gradient(90deg, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.8) 50%, rgba(255, 255, 255, 0) 100%);
  413. transform: translateX(-50%);
  414. animation: shimmer 1.5s infinite;
  415. }
  416. @keyframes pulse {
  417. 0% {
  418. box-shadow: 0 0 0 0 rgba(64, 158, 255, 0.4);
  419. }
  420. 70% {
  421. box-shadow: 0 0 0 10px rgba(64, 158, 255, 0);
  422. }
  423. 100% {
  424. box-shadow: 0 0 0 0 rgba(64, 158, 255, 0);
  425. }
  426. }
  427. @keyframes rotate {
  428. 0% {
  429. transform: rotate(0);
  430. }
  431. 100% {
  432. transform: rotate(360deg);
  433. }
  434. }
  435. @keyframes shimmer {
  436. 0% {
  437. opacity: 0.3;
  438. }
  439. 50% {
  440. opacity: 0.7;
  441. }
  442. 100% {
  443. opacity: 0.3;
  444. }
  445. }
  446. :deep(.el-step__icon) {
  447. border-radius: 50%;
  448. }
  449. </style>