前端 实现文字打字效果(仿AI)
DOM结构
<scroll-view class="scroll-view" scroll-y="true" :scroll-top="scrollTop" :style="{height: contentHeight + 'px'}"scroll-with-animation show-scrollbar="false" id="report-scroll-view"><view class="report-container"><!-- 内容 --><view class="report-content" v-if="reportData && reportData.aiAnalysis"><!-- 使用解析后的多个文本分段显示 --><block v-for="(item, index) in parsedContent" :key="index"><!-- 标题 --><view v-if="item.type === 'title'" class="title-section"><text class="title-text">{{item.content}}</text></view><!-- 普通段落 --><view v-if="item.type === 'paragraph'" class="paragraph"><text>{{item.content}}</text></view><!-- 缩进列表项 --><view v-if="item.type === 'list-item'" class="list-item"><text>{{item.content}}</text></view></block></view><!-- 没有内容 --><view class="report-content no-content" v-else-if="!isLoading"><text class="report-text">暂无此内容</text></view><!-- 加载中状态 --><view class="loading-container" v-if="isLoading"><view class="loading-spinner"></view></view><!-- 底部AI解读提示 - 仅在解读完成后显示 --><view class="ai-disclaimer" v-if="!isLoading && reportData"><text class="disclaimer-text">此内容由人工智能DeepSeek进行解读</text></view></view></scroll-view>
data值
scrollTop: 0,
contentHeight: 0, //内容区高度,动态计算
reportData: null,
parsedContent: [], // 解析后的内容数组
isLoading: false, //是否正在加载数据
displayText: '', // 当前显示的文本
typingSpeed: 10, // 打字效果速度(毫秒)
isTyping: false, // 是否正在展示打字效果
fullText: '', // 完整的报告文本
parsedContent: [], // 解析后的内容数组
statusBarHeight: 0, //状态栏高度,用于动态调整布局
监听displayText变化,解析文本结构
watch: {// 监听displayText变化,解析文本结构displayText(newVal) {this.parseContent(newVal);}},
onLoad方法
onLoad(option) {// 获取系统信息设置顶部状态栏高度try {const systemInfo = uni.getSystemInfoSync();this.statusBarHeight = systemInfo.statusBarHeight || 20;// 计算内容区域高度(屏幕高度减去状态栏、标题栏和额外的顶部间距)this.contentHeight = systemInfo.windowHeight - this.statusBarHeight - 90 - 15;} catch (e) {this.statusBarHeight = 20;this.contentHeight = 500;}// 获取数据this.getReportInterpretation();},
methods
// 解析内容为标题、段落和列表项parseContent(text) {if (!text) {this.parsedContent = [];return;}const lines = text.split('\n');const result = [];for (let i = 0; i < lines.length; i++) {const line = lines[i].trim();if (!line) continue;// 处理标题行(数字+点开头,后面可能有冒号结尾)if (/^\d+\..*?[::]?$/.test(line)) {result.push({type: 'title',content: line});}// 处理列表项(短横线开头)else if (line.startsWith('- ') || line.startsWith('• ')) {result.push({type: 'list-item',content: line});}// 处理普通段落else {result.push({type: 'paragraph',content: line});}}this.parsedContent = result;},
打字效果
// 打字效果实现startTypeEffect(text) {if (!text) return;this.fullText = text;this.displayText = '';this.isTyping = true;let currentIndex = 0;const typeNextChar = () => {if (currentIndex < this.fullText.length && this.isTyping) {this.displayText = this.fullText.substring(0, currentIndex + 1);currentIndex++;// 滚动到底部this.$nextTick(() => {this.scrollToBottom();});// 使用setTimeout实现打字效果setTimeout(typeNextChar, this.typingSpeed);} else {this.isTyping = false;}};typeNextChar();},
滚动方法
// 滚动到底部scrollToBottom() {const query = uni.createSelectorQuery().in(this);query.select('#report-scroll-view').boundingClientRect();query.select('.report-container').boundingClientRect();query.exec(res => {if (res && res[0] && res[1]) {const scrollViewHeight = res[0].height;const containerHeight = res[1].height;if (containerHeight > scrollViewHeight) {this.scrollTop = containerHeight - scrollViewHeight;}}});},
发送请求获取数据
// 获取报告解读getReportInterpretation() {// 设置加载状态this.isLoading = true;// 发送请求到接口getListByPageDeepseek(params).then(res => {// 处理返回数据if (res && res.data) {// 保存报告数据this.reportData = res.data;// 启动打字效果if (res.data.aiAnalysis) {this.startTypeEffect(res.data.aiAnalysis);} else {const defaultText = '暂无此报告的解读内容';this.startTypeEffect(defaultText);}} else {// 没有报告数据console.warn('接口返回数据中无data字段');const defaultAnalysis ='由于您提供的报告内容显示为"null"(空值),我无法获取具体信息进行分析。;this.reportData = {aiAnalysis: defaultAnalysis};this.startTypeEffect(defaultAnalysis);}// 关闭加载状态this.isLoading = false;}).catch(error => {console.error('获取报告解读失败:', error);let errorMsg = '系统内部异常,请稍后再试';if (typeof error === 'string') {errorMsg = error;} else if (error && error.message) {errorMsg = error.message;}this.reportData = {aiAnalysis: errorMsg};this.startTypeEffect(errorMsg);// 关闭加载状态this.isLoading = false;});}
css,有些是多余的,自己删吧
.container {display: flex;flex-direction: column;height: 100vh;background-color: #f5f5f5;position: relative;.header {padding-bottom: 10rpx;background-color: #fff;border-bottom: 1px solid #eee;position: sticky;top: 0;left: 0;right: 0;z-index: 100;.header-content {position: relative;display: flex;align-items: center;height: 80rpx;padding: 0 30rpx;.back-button {position: absolute;left: 30rpx;width: 60rpx;height: 60rpx;display: flex;align-items: center;justify-content: center;z-index: 1;.bacl-icon {font-size: 28rpx;}}.title-container {position: absolute;left: 0;right: 0;top: 0;bottom: 0;display: flex;justify-content: center;align-items: center;.title {font-size: 36rpx;font-weight: bold;text-align: center;}}}}.scroll-view {flex: 1;position: relative;overflow: hidden;.report-container {display: flex;flex-direction: column;.report-content {background-color: #fff;padding: 30rpx;font-size: 28rpx;line-height: 1.6;color: #333;.title-section {margin-top: 20rpx;margin-bottom: 10rpx;.title-text {font-size: 32rpx;font-weight: bold;color: #333;display: block;}}.paragraph {text-indent: 2em;margin-bottom: 15rpx;}.list-item {padding-left: 2em;margin-bottom: 10rpx;}&.no-content {color: #999;text-align: center;padding: 60rpx 30rpx;}}.loading-container {display: flex;justify-content: center;align-items: center;padding: 100rpx 0;.loading-spinner {width: 60rpx;height: 60rpx;border: 6rpx solid rgba(0, 0, 0, 0.1);border-left-color: #4e8ef7;border-radius: 50%;animation: spin 1s linear infinite;}}.ai-disclaimer {background-color: #FFF9E6;padding: 20rpx 30rpx;.disclaimer-text {font-size: 24rpx;color: #8F7846;line-height: 1.4;}}}}}@keyframes spin {0% {transform: rotate(0deg);}100% {transform: rotate(360deg);}}