feat(aigc): add animated progress and completion redirect

enhance GeneratingProgressPanel with smooth width transition and animated progress increments
add active/complete step styling for progress panel items
emit complete event when generation finishes, add navigation to AIGC detail page on completion
clean up MineSetting component: format menu-item v-for and comment out unused phone row
This commit is contained in:
duanshuwen
2026-07-07 11:06:34 +08:00
parent a6e369e1d5
commit 409559e885
4 changed files with 123 additions and 11 deletions

View File

@@ -12,6 +12,7 @@
v-for="(step, index) in steps" v-for="(step, index) in steps"
:key="`${step}-${index}`" :key="`${step}-${index}`"
class="generating-step-item" class="generating-step-item"
:class="getStepClass(index)"
> >
<view class="generating-step-dot" /> <view class="generating-step-dot" />
<text class="generating-step-label">{{ step }}</text> <text class="generating-step-label">{{ step }}</text>
@@ -25,7 +26,7 @@
</template> </template>
<script setup> <script setup>
import { computed } from "vue"; import { computed, onMounted, onUnmounted, ref, watch } from "vue";
const props = defineProps({ const props = defineProps({
progress: { progress: {
@@ -42,13 +43,97 @@ const props = defineProps({
}, },
}); });
const normalizedProgress = computed(() => { const emit = defineEmits(["complete"]);
const value = Number(props.progress);
const displayProgress = ref(0);
let progressTimer = null;
let hasCompleted = false;
const normalizeProgress = (progress) => {
const value = Number(progress);
if (!Number.isFinite(value)) return 0; if (!Number.isFinite(value)) return 0;
return Math.max(0, Math.min(100, Math.round(value))); return Math.max(0, Math.min(100, Math.round(value)));
};
const normalizedProgress = computed(() => {
return normalizeProgress(displayProgress.value);
}); });
const progressWidth = computed(() => `${normalizedProgress.value}%`); const progressWidth = computed(() => `${normalizedProgress.value}%`);
const currentStepIndex = computed(() => {
const progress = normalizedProgress.value;
if (progress <= 24) return 0;
if (progress <= 49) return 1;
if (progress <= 79) return 2;
return 3;
});
const clearProgressTimer = () => {
if (progressTimer) {
clearInterval(progressTimer);
progressTimer = null;
}
};
const getNextProgress = (progress) => {
if (progress < 35) return Math.min(progress + 7, 35);
if (progress < 70) return Math.min(progress + 5, 70);
if (progress < 92) return Math.min(progress + 3, 92);
if (progress < 99) return Math.min(progress + 1, 99);
return 100;
};
const completeProgress = () => {
clearProgressTimer();
if (hasCompleted) return;
hasCompleted = true;
emit("complete");
};
const startProgressTimer = () => {
clearProgressTimer();
if (normalizedProgress.value >= 100) {
completeProgress();
return;
}
progressTimer = setInterval(() => {
displayProgress.value = getNextProgress(normalizedProgress.value);
if (normalizedProgress.value >= 100) {
completeProgress();
}
}, 320);
};
const resetProgress = () => {
hasCompleted = false;
displayProgress.value = normalizeProgress(props.progress);
startProgressTimer();
};
const getStepClass = (index) => ({
"is-complete": normalizedProgress.value >= 100 || index < currentStepIndex.value,
"is-active": normalizedProgress.value < 100 && index === currentStepIndex.value,
});
watch(
() => props.progress,
() => {
resetProgress();
}
);
onMounted(() => {
resetProgress();
});
onUnmounted(() => {
clearProgressTimer();
});
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">

View File

@@ -23,6 +23,7 @@
min-width: 24px; min-width: 24px;
border-radius: 999px; border-radius: 999px;
background: linear-gradient(90deg, #20cf75 0%, #f2d646 100%); background: linear-gradient(90deg, #20cf75 0%, #f2d646 100%);
transition: width 0.28s ease;
} }
.generating-progress-text { .generating-progress-text {
@@ -52,6 +53,14 @@
box-sizing: border-box; box-sizing: border-box;
} }
.generating-step-item.is-active {
background: rgba(255, 255, 255, 0.86);
}
.generating-step-item.is-complete {
background: rgba(236, 255, 244, 0.72);
}
.generating-step-dot { .generating-step-dot {
flex: 0 0 18px; flex: 0 0 18px;
width: 18px; width: 18px;
@@ -62,6 +71,17 @@
box-sizing: border-box; box-sizing: border-box;
} }
.generating-step-item.is-active .generating-step-dot {
border-color: #20cf75;
background: #20cf75;
box-shadow: 0 0 0 4px rgba(32, 207, 117, 0.12);
}
.generating-step-item.is-complete .generating-step-dot {
border-color: rgba(32, 207, 117, 0.42);
background: rgba(32, 207, 117, 0.22);
}
.generating-step-label { .generating-step-label {
min-width: 0; min-width: 0;
overflow: hidden; overflow: hidden;
@@ -73,6 +93,11 @@
white-space: nowrap; white-space: nowrap;
} }
.generating-step-item.is-active .generating-step-label,
.generating-step-item.is-complete .generating-step-label {
color: #172033;
}
.generating-freeze-text { .generating-freeze-text {
display: block; display: block;
overflow: hidden; overflow: hidden;

View File

@@ -23,6 +23,7 @@
v-else-if="currentStep === 'generating'" v-else-if="currentStep === 'generating'"
:cost="currentVersion.cost || 100" :cost="currentVersion.cost || 100"
:progress="8" :progress="8"
@complete="handleGenerateComplete"
/> />
<template v-else> <template v-else>
@@ -176,6 +177,12 @@ const handleGenerate = () => {
currentStep.value = "generating"; currentStep.value = "generating";
}; };
const handleGenerateComplete = () => {
uni.navigateTo({
url: "/pages-aigc/detail/detail",
});
};
const handleClosePointDialog = () => { const handleClosePointDialog = () => {
pointDialogVisible.value = false; pointDialogVisible.value = false;
}; };

View File

@@ -11,20 +11,15 @@
<text class="label">昵称</text> <text class="label">昵称</text>
<text class="value">{{ userInfo.nickname }}</text> <text class="value">{{ userInfo.nickname }}</text>
</view> </view>
<view class="row"> <!-- <view class="row">
<text class="label">手机号</text> <text class="label">手机号</text>
<text class="value">{{ userInfo.phone }}</text> <text class="value">{{ userInfo.phone }}</text>
</view> </view> -->
</view> </view>
<!-- 中间功能入口循环渲染 --> <!-- 中间功能入口循环渲染 -->
<view class="menu-card"> <view class="menu-card">
<view <view class="menu-item" v-for="(item, index) in menuList" :key="index" @click="handleMenuClick(item)">
class="menu-item"
v-for="(item, index) in menuList"
:key="index"
@click="handleMenuClick(item)"
>
<text class="label">{{ item.label }}</text> <text class="label">{{ item.label }}</text>
<uni-icons type="right" size="14" color="#ccc"></uni-icons> <uni-icons type="right" size="14" color="#ccc"></uni-icons>
</view> </view>