|
|
@@ -1,10 +1,35 @@
|
|
|
<template>
|
|
|
- <div class="workflow-add container-height">
|
|
|
+ <div class="workflow-add container-height container">
|
|
|
+ <!-- ══════════════════════════════════════
|
|
|
+ 步骤进度条(sticky sentinel + 步骤条)
|
|
|
+ ══════════════════════════════════════ -->
|
|
|
+ <div ref="stepsSentinelRef" class="wta-steps-sentinel"></div>
|
|
|
+ <div class="wta-steps-wrap" :class="{ 'wta-steps-wrap--sticky': isSticky }">
|
|
|
+ <div class="container">
|
|
|
+ <div class="wta-steps">
|
|
|
+ <div v-for="(step, idx) in steps" :key="idx"
|
|
|
+ class="wta-step"
|
|
|
+ :class="{ 'wta-step--active': idx === currentStep, 'wta-step--done': idx < currentStep }"
|
|
|
+ style="cursor: pointer"
|
|
|
+ @click="scrollToSection(idx)">
|
|
|
+ <div class="wta-step__circle">
|
|
|
+ <svg v-if="idx < currentStep" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"><polyline points="20 6 9 17 4 12"/></svg>
|
|
|
+ <span v-else>{{ idx + 1 }}</span>
|
|
|
+ </div>
|
|
|
+ <span class="wta-step__label">{{ step }}</span>
|
|
|
+ <div v-if="idx < steps.length - 1" class="wta-step__line"
|
|
|
+ :class="{ 'wta-step__line--done': idx < currentStep }"></div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
<Breadcrumb />
|
|
|
+
|
|
|
<div class="flex-between mt10">
|
|
|
<div class="flex_1 mr20">
|
|
|
<el-form :model="ruleForm" :rules="rules" ref="ruleFormRef" label-position="top" class="page-add">
|
|
|
- <div class="padding16 bg_color_fff border_radius_10 box_shadow_card" v-show="!isFullscreen">
|
|
|
+ <div class="padding16 bg_color_fff border_radius_10 box_shadow_card" id="section-upload" v-show="!isFullscreen">
|
|
|
<div class="gap10">
|
|
|
<div class="line_vertical"></div>
|
|
|
<div class="font_size20 bold"><span class="color_required font_size16">*</span>{{$t('workflowTrade.fileUpload')}}</div>
|
|
|
@@ -26,7 +51,7 @@
|
|
|
</el-form-item>
|
|
|
</div>
|
|
|
</div>
|
|
|
- <div class="padding16 bg_color_fff border_radius_10 mt10 box_shadow_card">
|
|
|
+ <div class="padding16 bg_color_fff border_radius_10 mt10 box_shadow_card" id="section-basic">
|
|
|
<div class="gap10">
|
|
|
<div class="line_vertical"></div>
|
|
|
<div class="font_size20 bold">{{$t('common.basicInfo')}}</div>
|
|
|
@@ -129,7 +154,7 @@
|
|
|
</el-form-item>
|
|
|
</div>
|
|
|
</div>
|
|
|
- <div class="padding16 bg_color_fff border_radius_10 mt10 box_shadow_card" v-show="!isFullscreen">
|
|
|
+ <div class="padding16 bg_color_fff border_radius_10 mt10 box_shadow_card" id="section-price" v-show="!isFullscreen">
|
|
|
<div class="gap10 mb20">
|
|
|
<div class="line_vertical"></div>
|
|
|
<div class="font_size20 bold">{{$t('workflowTrade.priceSetting')}}</div>
|
|
|
@@ -189,7 +214,7 @@
|
|
|
</template>
|
|
|
|
|
|
<script setup>
|
|
|
-import { ref, onMounted, reactive, watchEffect, nextTick } from 'vue'
|
|
|
+import { ref, onMounted, reactive, watchEffect, nextTick, onUnmounted } from 'vue'
|
|
|
import { useRoute, useRouter } from 'vue-router'
|
|
|
import FileUploader from '@/components/FileUploader.vue'
|
|
|
import DGTMessage from '@/utils/message'
|
|
|
@@ -201,9 +226,87 @@ import { getCategoryListTree } from '@/api/category.js'
|
|
|
import { getAgreementType } from '@/api/common.js'
|
|
|
import { publishAdd, getPublishDetail,publishEdit } from '@/api/publish.js'
|
|
|
|
|
|
-import { useI18n } from 'vue-i18n'
|
|
|
+import { useI18n } from 'vue-i18n'
|
|
|
const { t } = useI18n()
|
|
|
|
|
|
+// ══════════════════════════════════════
|
|
|
+// 步骤条状态管理
|
|
|
+// ══════════════════════════════════════
|
|
|
+const currentStep = ref(0)
|
|
|
+const isSticky = ref(false)
|
|
|
+const stepsSentinelRef = ref(null)
|
|
|
+
|
|
|
+const sectionRefs = {
|
|
|
+ 0: 'section-upload',
|
|
|
+ 1: 'section-basic',
|
|
|
+ 2: 'section-price'
|
|
|
+}
|
|
|
+
|
|
|
+const steps = [
|
|
|
+ t('workflowTrade.fileUpload'),
|
|
|
+ t('common.basicInfo'),
|
|
|
+ t('workflowTrade.priceSetting')
|
|
|
+]
|
|
|
+
|
|
|
+let scrollObserver = null
|
|
|
+let stickyObserver = null
|
|
|
+
|
|
|
+const scrollToSection = (stepIndex) => {
|
|
|
+ if (stepIndex === currentStep.value) return
|
|
|
+
|
|
|
+ const sectionId = sectionRefs[stepIndex]
|
|
|
+ const element = document.getElementById(sectionId)
|
|
|
+
|
|
|
+ if (element) {
|
|
|
+ const offsetTop = element.offsetTop - 120
|
|
|
+ window.scrollTo({
|
|
|
+ top: offsetTop,
|
|
|
+ behavior: 'smooth'
|
|
|
+ })
|
|
|
+ currentStep.value = stepIndex
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const initScrollObserver = () => {
|
|
|
+ const sections = Object.values(sectionRefs)
|
|
|
+ .map(id => document.getElementById(id))
|
|
|
+ .filter(Boolean)
|
|
|
+
|
|
|
+ if (sections.length === 0) return
|
|
|
+
|
|
|
+ scrollObserver = new IntersectionObserver((entries) => {
|
|
|
+ entries.forEach(entry => {
|
|
|
+ if (entry.isIntersecting) {
|
|
|
+ const sectionId = entry.target.id
|
|
|
+ const stepIndex = Object.values(sectionRefs).indexOf(sectionId)
|
|
|
+
|
|
|
+ if (stepIndex !== -1 && stepIndex !== currentStep.value) {
|
|
|
+ currentStep.value = stepIndex
|
|
|
+ }
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }, {
|
|
|
+ rootMargin: '-100px 0px -50% 0px',
|
|
|
+ threshold: 0.15
|
|
|
+ })
|
|
|
+
|
|
|
+ sections.forEach(section => scrollObserver.observe(section))
|
|
|
+}
|
|
|
+
|
|
|
+const initStickyObserver = () => {
|
|
|
+ const sentinel = stepsSentinelRef.value
|
|
|
+ if (!sentinel) return
|
|
|
+
|
|
|
+ stickyObserver = new IntersectionObserver(([entry]) => {
|
|
|
+ isSticky.value = !entry.isIntersecting
|
|
|
+ }, {
|
|
|
+ threshold: 0,
|
|
|
+ rootMargin: '-60px 0px 0px 0px'
|
|
|
+ })
|
|
|
+
|
|
|
+ stickyObserver.observe(sentinel)
|
|
|
+}
|
|
|
+
|
|
|
// 防止重复提交的加载状态
|
|
|
const isSubmiting = ref(false)
|
|
|
// 发布规则
|
|
|
@@ -295,8 +398,27 @@ onMounted(() => {
|
|
|
getDetail();
|
|
|
}
|
|
|
getAgreementTypeFn();
|
|
|
+
|
|
|
+ nextTick(() => {
|
|
|
+ initScrollObserver()
|
|
|
+ initStickyObserver()
|
|
|
+ })
|
|
|
});
|
|
|
|
|
|
+onUnmounted(() => {
|
|
|
+ window.removeEventListener('keydown', handleKeyDown)
|
|
|
+
|
|
|
+ if (scrollObserver) {
|
|
|
+ scrollObserver.disconnect()
|
|
|
+ scrollObserver = null
|
|
|
+ }
|
|
|
+
|
|
|
+ if (stickyObserver) {
|
|
|
+ stickyObserver.disconnect()
|
|
|
+ stickyObserver = null
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
// 提交表单
|
|
|
const submitForm = async () => {
|
|
|
|
|
|
@@ -424,13 +546,92 @@ const getAgreementTypeFn = () => {
|
|
|
})
|
|
|
};
|
|
|
</script>
|
|
|
-<style lang="scss">
|
|
|
+<style lang="scss" scoped>
|
|
|
+
|
|
|
+/* ══════════════════════════════════════
|
|
|
+ 步骤进度条
|
|
|
+══════════════════════════════════════ */
|
|
|
+.wta-steps-sentinel { height: 1px; }
|
|
|
+.wta-steps-wrap {
|
|
|
+ background: #fff;
|
|
|
+ // border-bottom: 1px solid #e8eaf6;
|
|
|
+ // box-shadow: 0 2px 12px rgba(79,70,229,0.06);
|
|
|
+ transition: box-shadow 0.3s;
|
|
|
+}
|
|
|
+.wta-steps-wrap--sticky {
|
|
|
+ position: fixed;
|
|
|
+ top: 60px;
|
|
|
+ left: 0;
|
|
|
+ right: 0;
|
|
|
+ z-index: 100;
|
|
|
+ box-shadow: 0 4px 24px rgba(79,70,229,0.13);
|
|
|
+ backdrop-filter: blur(16px);
|
|
|
+ background: rgba(255,255,255,0.95);
|
|
|
+}
|
|
|
+.wta-steps {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ padding: 16px 0;
|
|
|
+ overflow-x: auto;
|
|
|
+}
|
|
|
+.wta-step {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+ flex-shrink: 0;
|
|
|
+}
|
|
|
+.wta-step__circle {
|
|
|
+ width: 28px; height: 28px;
|
|
|
+ border-radius: 50%;
|
|
|
+ border: 2px solid #e0e0f0;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ font-size: 12px;
|
|
|
+ font-weight: 700;
|
|
|
+ color: #9ca3af;
|
|
|
+ background: #fff;
|
|
|
+ transition: all 0.3s;
|
|
|
+ flex-shrink: 0;
|
|
|
+}
|
|
|
+.wta-step__label {
|
|
|
+ font-size: 13px;
|
|
|
+ color: #9ca3af;
|
|
|
+ font-weight: 500;
|
|
|
+ white-space: nowrap;
|
|
|
+ transition: color 0.3s;
|
|
|
+}
|
|
|
+.wta-step__line {
|
|
|
+ width: 40px;
|
|
|
+ height: 2px;
|
|
|
+ background: #e0e0f0;
|
|
|
+ margin: 0 4px;
|
|
|
+ border-radius: 2px;
|
|
|
+ transition: background 0.3s;
|
|
|
+}
|
|
|
+.wta-step__line--done { background: linear-gradient(90deg, #6366f1, #a855f7); }
|
|
|
+.wta-step--active .wta-step__circle {
|
|
|
+ border-color: #6366f1;
|
|
|
+ background: linear-gradient(135deg, #6366f1, #a855f7);
|
|
|
+ color: #fff;
|
|
|
+ box-shadow: 0 0 0 4px rgba(99,102,241,0.15);
|
|
|
+}
|
|
|
+.wta-step--active .wta-step__label { color: #4f46e5; font-weight: 700; }
|
|
|
+.wta-step--done .wta-step__circle {
|
|
|
+ border-color: #6366f1;
|
|
|
+ background: linear-gradient(135deg, #6366f1, #a855f7);
|
|
|
+ color: #fff;
|
|
|
+}
|
|
|
+.wta-step--done .wta-step__label { color: #6366f1; }
|
|
|
+
|
|
|
.workflow-add{
|
|
|
.payType{
|
|
|
background: #EAF0FF;
|
|
|
border-radius: 8px 8px 8px 8px;
|
|
|
border: 1px solid transparent;
|
|
|
padding: 10px 16px;
|
|
|
+ cursor: pointer;
|
|
|
&.active{
|
|
|
background: #EAF0FF;
|
|
|
border-color: $primary-color;
|
|
|
@@ -448,7 +649,7 @@ const getAgreementTypeFn = () => {
|
|
|
}
|
|
|
}
|
|
|
</style>
|
|
|
-<style lang="scss">
|
|
|
+<style lang="scss" scoped>
|
|
|
.editor-container {
|
|
|
position: relative;
|
|
|
width: 100%;
|
|
|
@@ -507,4 +708,12 @@ const getAgreementTypeFn = () => {
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+.container{
|
|
|
+ min-width: auto;
|
|
|
+}
|
|
|
+
|
|
|
+.line_vertical{
|
|
|
+ background: linear-gradient(135deg, rgb(0, 85, 254), rgb(200, 50, 250));
|
|
|
+}
|
|
|
</style>
|