feat(mini-program): 添加 Markdown 渲染组件支持回溯过去页面
- 创建 Markdown.vue 组件,解析并渲染 Markdown 格式内容 - 支持分割线 (---)、四级标题 (####)、列表项 (*-)、段落 - 更新 RecordView.vue 使用 Markdown 组件渲染事件内容和 AI 回复 - 样式采用紫色主题,与整体设计保持一致 解决 issues: 小程序回溯过去页面的 Markdown 内容以纯文本显示的问题
This commit is contained in:
@@ -0,0 +1,134 @@
|
|||||||
|
<template>
|
||||||
|
<view class="markdown-container">
|
||||||
|
<view v-for="(block, index) in parsedBlocks" :key="index" :class="block.type">
|
||||||
|
<!-- 分割线 -->
|
||||||
|
<view v-if="block.type === 'hr'" class="markdown-hr"></view>
|
||||||
|
|
||||||
|
<!-- 标题 -->
|
||||||
|
<text v-else-if="block.type === 'h4'" class="markdown-h4">{{ block.content }}</text>
|
||||||
|
|
||||||
|
<!-- 列表项 -->
|
||||||
|
<view v-else-if="block.type === 'li'" class="markdown-li">
|
||||||
|
<text class="li-bullet">• </text>
|
||||||
|
<text class="li-content">{{ block.content }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 普通段落 -->
|
||||||
|
<text v-else class="markdown-p">{{ block.content }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
content: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 解析 Markdown 内容
|
||||||
|
const parsedBlocks = computed(() => {
|
||||||
|
if (!props.content) return []
|
||||||
|
|
||||||
|
const lines = props.content.split('\n')
|
||||||
|
const blocks = []
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const trimmed = line.trim()
|
||||||
|
|
||||||
|
// 跳过空行
|
||||||
|
if (trimmed === '') continue
|
||||||
|
|
||||||
|
// 分割线
|
||||||
|
if (trimmed === '---' || trimmed === '***' || trimmed === '___') {
|
||||||
|
blocks.push({ type: 'hr', content: '' })
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 四级标题 ####
|
||||||
|
const h4Match = trimmed.match(/^####\s+(.+)/)
|
||||||
|
if (h4Match) {
|
||||||
|
blocks.push({ type: 'h4', content: h4Match[1] })
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 列表项 * 或 -
|
||||||
|
const liMatch = trimmed.match(/^[*\-]\s+(.+)/)
|
||||||
|
if (liMatch) {
|
||||||
|
blocks.push({ type: 'li', content: liMatch[1] })
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 普通段落
|
||||||
|
blocks.push({ type: 'p', content: trimmed })
|
||||||
|
}
|
||||||
|
|
||||||
|
return blocks
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.markdown-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 分割线 */
|
||||||
|
.markdown-hr {
|
||||||
|
height: 2rpx;
|
||||||
|
background: linear-gradient(to right,
|
||||||
|
rgba(168, 85, 247, 0.1) 0%,
|
||||||
|
rgba(168, 85, 247, 0.4) 50%,
|
||||||
|
rgba(168, 85, 247, 0.1) 100%
|
||||||
|
);
|
||||||
|
margin: 24rpx 0;
|
||||||
|
border-radius: 2rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 标题 */
|
||||||
|
.markdown-h4 {
|
||||||
|
display: block;
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: rgba(243, 232, 255, 0.95);
|
||||||
|
margin: 16rpx 0 8rpx 0;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 列表项 */
|
||||||
|
.markdown-li {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12rpx;
|
||||||
|
padding: 8rpx 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.li-bullet {
|
||||||
|
color: #C084FC;
|
||||||
|
font-size: 24rpx;
|
||||||
|
line-height: 1.5;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.li-content {
|
||||||
|
flex: 1;
|
||||||
|
color: rgba(243, 232, 255, 0.8);
|
||||||
|
font-size: 24rpx;
|
||||||
|
line-height: 1.6;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 普通段落 */
|
||||||
|
.markdown-p {
|
||||||
|
display: block;
|
||||||
|
color: rgba(243, 232, 255, 0.75);
|
||||||
|
font-size: 24rpx;
|
||||||
|
line-height: 1.6;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -42,16 +42,16 @@
|
|||||||
<text class="event-date">{{ event.time }}</text>
|
<text class="event-date">{{ event.time }}</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<text class="event-content">{{ event.content }}</text>
|
<view class="event-body">
|
||||||
|
<Markdown class="event-content" :content="event.content" />
|
||||||
|
</view>
|
||||||
|
|
||||||
<view class="ai-reply">
|
<view class="ai-reply">
|
||||||
<view class="ai-header">
|
<view class="ai-header">
|
||||||
<text class="ai-icon">✨</text>
|
<text class="ai-icon">✨</text>
|
||||||
<text class="ai-title">Life Harmony AI</text>
|
<text class="ai-title">Life Harmony AI</text>
|
||||||
</view>
|
</view>
|
||||||
<text class="ai-content" :class="{ typing: event.isNew }">
|
<Markdown class="ai-content" :content="event.aiFeedback" />
|
||||||
{{ event.aiFeedback }}
|
|
||||||
</text>
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -61,6 +61,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
import { useAppStore } from '../../stores/app.js'
|
import { useAppStore } from '../../stores/app.js'
|
||||||
|
import Markdown from '../../components/Markdown.vue'
|
||||||
|
|
||||||
const store = useAppStore()
|
const store = useAppStore()
|
||||||
|
|
||||||
@@ -121,6 +122,7 @@ onMounted(() => {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 32rpx;
|
gap: 32rpx;
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ==================== 输入卡片 - 金色玻璃态 ==================== */
|
/* ==================== 输入卡片 - 金色玻璃态 ==================== */
|
||||||
@@ -285,12 +287,14 @@ onMounted(() => {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.event-body {
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
.event-content {
|
.event-content {
|
||||||
display: block;
|
|
||||||
font-size: 26rpx;
|
font-size: 26rpx;
|
||||||
color: rgba(255, 255, 255, 0.7);
|
color: rgba(255, 255, 255, 0.7);
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
margin-bottom: 24rpx;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ==================== AI 回复区域 - 金色玻璃态核心特征 ==================== */
|
/* ==================== AI 回复区域 - 金色玻璃态核心特征 ==================== */
|
||||||
@@ -336,39 +340,6 @@ onMounted(() => {
|
|||||||
|
|
||||||
.ai-content {
|
.ai-content {
|
||||||
font-size: 24rpx;
|
font-size: 24rpx;
|
||||||
color: rgba(243, 232, 255, 0.8);
|
|
||||||
font-style: italic;
|
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
overflow: hidden;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 打字机动画 - 原型标准 */
|
|
||||||
.ai-content.typing {
|
|
||||||
animation: typing-reveal 2.5s steps(60, end);
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes typing-reveal {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(8rpx);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 光标闪烁效果(增强打字机感) */
|
|
||||||
.ai-content.typing::after {
|
|
||||||
content: '|';
|
|
||||||
animation: cursor-blink 0.8s steps(2) infinite;
|
|
||||||
color: rgba(192, 132, 252, 0.6);
|
|
||||||
margin-left: 4rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes cursor-blink {
|
|
||||||
0%, 50% { opacity: 1; }
|
|
||||||
51%, 100% { opacity: 0; }
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user