Commit 48329f8d authored by 王炽's avatar 王炽

Merge branch 'dev' of http://gitlab2.dui88.com/fh/20250528_FHQ1 into dev

parents 4063e012 c88545da
......@@ -58,10 +58,16 @@ export const getAdd = (data) => api.post('/c/maternityCheckup/add',data);
* @returns
*/
export const getReportList = () => api.get('/c/maternityCheckup/reportList');
export const getReportList = (data) => api.get('/c/maternityCheckup/reportList', data);
/**
* 保存订阅消息
* @returns
*/
export const getWxNotification = (data) => api.post('/c/wxNotification/save',data);
\ No newline at end of file
export const getWxNotification = (data) => api.post('/c/wxNotification/save',data);
/**
* 修改报告单
* @returns
*/
export const getDeleteReportImg = (data) => api.post('/c/maternityCheckup/deleteReportImg',data);
\ No newline at end of file
......@@ -137,6 +137,7 @@ import { onLoad } from '@dcloudio/uni-app'
import {
uploadImage
} from "../../api/common.js";
import {
getAdd,
getExaminationItems
......@@ -144,6 +145,11 @@ import {
// 导入日期选择器组件
import DatePicker from '@/components/DatePicker.vue'
import { useUserStore } from "@/stores/user";
// 获取用户信息
const userStore = useUserStore();
const babyId = ref(userStore.babyInfo?.content?.id)
// 默认产检时间
const time = ref('');
......@@ -334,9 +340,10 @@ const onSave = throttleTap( async () => {
const param = {
checkupDate: time.value,
checkupItems: ids,
reportImages: bgdImgList.value
reportImages: bgdImgList.value,
babyId: babyId.value
}
console.log(param, '参数')
console.log(param, '新增产检保存参数')
showLoading();
const {success, data } = await getAdd(param);
hideLoading();
......@@ -365,6 +372,7 @@ const onSeeBtn = () => {
// 获取产检项目列表
const getList = async () => {
const {success, data, message } = await getExaminationItems()
console.log(success, data, message, '获取产检项目列表')
if (success) {
examinationList.value = data
}
......
......@@ -214,7 +214,7 @@ const onSave = throttleTap(async () => {
id: editId.value,
checkupItems: ids
}
console.log(param, '参数')
console.log(param, '产检项目编辑保存参数')
showLoading();
const {success, data} = await getUpdate(param);
hideLoading();
......@@ -243,6 +243,7 @@ const onSave = throttleTap(async () => {
// 获取产检项目列表
const getList = async () => {
const {success, data, message} = await getExaminationItems()
console.log(success, data, message, '获取产检项目列表')
if (success) {
examinationList.value = data
}
......
......@@ -5,73 +5,67 @@
<!-- 头部导航 -->
<view class="header">
<view class="nav-left" @click="goBack">
<text class="back-btn"></text>
<view class="nav-left">
<image :src="feedingAnalysisRes.icon_return" class="back-btn" @click="goBack" />
</view>
</view>
<!-- 图例 -->
<view class="legend-section">
<view class="legend-item">
<view class="legend-color legend-breastfeeding"></view>
<text class="legend-text">母乳亲喂</text>
<view class="legend-color legend-breastfeeding"></view>
</view>
<view class="legend-item">
<view class="legend-color legend-bottle"></view>
<text class="legend-text">母乳瓶喂</text>
<view class="legend-color legend-bottle"></view>
</view>
<view class="legend-item">
<view class="legend-color legend-formula"></view>
<text class="legend-text">奶粉喂养</text>
<view class="legend-color legend-formula"></view>
</view>
<view class="legend-item">
<view class="legend-color legend-food"></view>
<text class="legend-text">辅食</text>
<view class="legend-color legend-food"></view>
</view>
</view>
<!-- CSS柱状图区域 -->
<view class="chart-section">
<view class="chart-container">
<view class="chart-nav-left" @click="prevWeek">
<view class="nav-arrow"></view>
<image :src="feedingAnalysisRes.icon_l_arrow" class="nav-arrow" />
</view>
<view class="chart-content">
<view class="chart-bars">
<view
v-for="(day, index) in chartData"
:key="index"
class="chart-bar-wrapper"
:class="{ active: day.isActive }"
@click="onBarClick(day)"
>
<view v-for="(day, index) in chartData" :key="index" class="chart-bar-wrapper" @click="onBarClick(day)">
<view class="chart-bar">
<view
v-for="(typeData, typeIndex) in getBarSegments(day)"
:key="typeIndex"
class="bar-segment"
:style="{
height: `${typeData.height}%`,
backgroundColor: typeData.color
}"
></view>
<view v-for="(typeData, typeIndex) in getBarSegments(day)" :key="typeIndex" class="bar-segment" :style="{
height: `${typeData.height}%`,
backgroundColor: typeData.color
}"></view>
</view>
<view class="bar-count" v-if="day.isActive && day.totalCount > 0">
<view class="bar-count" v-if="day.dateString === selectedDate && day.totalCount > 0">
{{ day.totalCount }}
</view>
<view class="bar-date">{{ day.date }}</view>
<!-- 选中背景图 -->
<image v-if="day.dateString === selectedDate" :src="feedingAnalysisRes.icon_sel_tag_bg"
class="selected-bg" />
</view>
</view>
</view>
<view class="chart-nav-right" @click="nextWeek">
<view class="nav-arrow"></view>
<image :src="feedingAnalysisRes.icon_r_arrow" class="nav-arrow" />
</view>
</view>
<!-- 宝宝年龄 -->
<view class="baby-age">
<text>宝宝 {{ calculateBabyAge(babyInfo.birthday) }}</text>
</view>
</view>
<view class="gap-bg"></view>
<!-- 宝宝年龄 -->
<view class="baby-age">
<text>宝宝 {{ calculateBabyAge(babyInfo.birthday) }} </text>
</view>
<!-- 喂养记录列表 -->
......@@ -100,41 +94,41 @@
<view v-if="record.foodDetails" class="content-info">
<view class="content-text">{{ record.foodDetails }}</view>
</view>
<!-- 如果没有 foodDetails,则显示原有字段 -->
<view v-else>
<!-- 母乳亲喂:显示左右时间,时间加粗 -->
<view v-if="record.type === '母乳亲喂' && (record.leftDuration || record.rightDuration)"
class="duration-info">
<view v-if="record.leftDuration" class="duration-text">
<text class="duration-label"></text>
<text class="duration-time">{{ record.leftDuration }}</text>
<!-- 母乳亲喂:显示左右时间,时间加粗 -->
<view v-if="record.type === '母乳亲喂' && (record.leftDuration || record.rightDuration)"
class="duration-info">
<view v-if="record.leftDuration" class="duration-text">
<text class="duration-label"></text>
<text class="duration-time">{{ record.leftDuration }}</text>
</view>
<view v-if="record.rightDuration" class="duration-text">
<text class="duration-label"></text>
<text class="duration-time">{{ record.rightDuration }}</text>
</view>
</view>
<view v-if="record.rightDuration" class="duration-text">
<text class="duration-label"></text>
<text class="duration-time">{{ record.rightDuration }}</text>
</view>
</view>
<!-- 母乳瓶喂:显示总乳量 -->
<view v-if="record.type === '母乳瓶喂' && record.amount" class="amount-info">
<view class="amount-text">
<text class="amount-label">总乳量</text>
<text class="amount-value">{{ record.amount }}</text>
<!-- 母乳瓶喂:显示总乳量 -->
<view v-if="record.type === '母乳瓶喂' && record.amount" class="amount-info">
<view class="amount-text">
<text class="amount-label">总乳量</text>
<text class="amount-value">{{ record.amount }}</text>
</view>
</view>
</view>
<!-- 奶粉喂养:显示总奶量 -->
<view v-if="record.type === '奶粉喂养' && record.amount" class="amount-info">
<view class="amount-text">
<text class="amount-label">总奶量</text>
<text class="amount-value">{{ record.amount }}</text>
<!-- 奶粉喂养:显示总奶量 -->
<view v-if="record.type === '奶粉喂养' && record.amount" class="amount-info">
<view class="amount-text">
<text class="amount-label">总奶量</text>
<text class="amount-value">{{ record.amount }}</text>
</view>
</view>
</view>
<!-- 辅食:显示内容,超出显示... -->
<view v-if="record.type === '辅食' && record.content" class="content-info">
<view class="content-text">{{ record.content }}</view>
<!-- 辅食:显示内容,超出显示... -->
<view v-if="record.type === '辅食' && record.content" class="content-info">
<view class="content-text">{{ record.content }}</view>
</view>
</view>
</view>
......@@ -148,8 +142,8 @@
</view>
<!-- 编辑记录弹窗 -->
<view class="popup-mask" v-if="showEditPopup" @click="closeEditPopup">
<view class="popup-content" @click.stop>
<view class="popup-mask" v-if="showEditPopup" @click="closeEditPopup" @touchmove.prevent>
<view class="popup-content" @click.stop @touchmove.stop>
<view class="popup-title">修改喂养记录</view>
<view class="form-item">
<text class="label">时间:</text>
......@@ -165,8 +159,10 @@
</view>
<view class="form-item">
<text class="label">喂养详情:</text>
<input class="input" v-model="editForm.content" placeholder="请输入具体内容" maxlength="20" />
<text class="char-count">{{ editForm.content.length }}/20</text>
<view class="input-container">
<input class="input" v-model="editForm.content" placeholder="请输入具体内容" maxlength="20" />
<text class="char-count">{{ editForm.content.length }}/20</text>
</view>
</view>
<view class="popup-buttons">
<button class="cancel-btn" @click="closeEditPopup">取消</button>
......@@ -177,7 +173,7 @@
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted, watch, nextTick,getCurrentInstance } from 'vue'
import { ref, computed, onMounted, onUnmounted, watch, nextTick, getCurrentInstance } from 'vue'
import { onLoad, onShow } from '@dcloudio/uni-app'
import { feedingRecordsStatisticsPeriod, feedingRecordsByDate, feedingRecords as feedingRecordsAPI } from '@/api/feeding.js'
import { useFeedStore } from '@/stores/feed.js'
......@@ -215,7 +211,7 @@ const feedingAnalysisRes = {
icon_muru: $baseUrl + `feedingRecord/${version}/icon_muru.png`,
icon_naifen: $baseUrl + `feedingRecord/${version}/icon_naifen.png`,
icon_pingwei: $baseUrl + `feedingRecord/${version}/icon_pingwei.png`,
// 其他可能用到的图标
icon_analysis: $baseUrl + `feedingRecord/${version}/icon_analysis.png`,
icon_arrow_yellow: $baseUrl + `feedingRecord/${version}/icon_arrow_yellow.png`,
......@@ -223,9 +219,9 @@ const feedingAnalysisRes = {
icon_return: $baseUrl + `feedingRecord/${version}/icon_return.png`,
icon_baby_change: $baseUrl + `feedingRecord/${version}/icon_baby_change.png`,
//analysis
icon_l_arrow: $baseUrl + `feedingRecord/${version}/icon_l_arrow.png`,
icon_r_arrow: $baseUrl + `feedingRecord/${version}/icon_r_arrow.png`,
icon_sel_tag_bg: $baseUrl + `feedingRecord/${version}/icon_sel_tag_bg.png`,
icon_l_arrow: $baseUrl + `feedingAnalysis/${version}/icon_l_arrow.png`,
icon_r_arrow: $baseUrl + `feedingAnalysis/${version}/icon_r_arrow.png`,
icon_sel_tag_bg: $baseUrl + `feedingAnalysis/${version}/icon_sel_tag_bg.png`,
}
......@@ -241,18 +237,14 @@ const recordsCache = ref(new Map()) // 记录数据缓存
// 响应式数据
const currentWeek = ref(0) // 当前显示的周数
const selectedDate = ref('2025-07-03') // 当前选中的日期
const selectedDate = ref(formatDateString(new Date())) // 当前选中的日期,默认为今天
const chartData = ref([])
// 全局状态管理
const feedStore = useFeedStore()
// 计算属性 - 获取宝宝信息
const babyInfo = computed(() => feedStore.getCurrentBaby() || {
id: 1,
name: '默认宝宝',
birthday: '2024-01-15'
})
const babyInfo = computed(() => feedStore.getCurrentBaby())
// 修改记录相关状态
const showEditPopup = ref(false)
......@@ -285,12 +277,12 @@ const maxWeekCount = computed(() => {
// 获取记录颜色 - 根据设计稿调整
function getRecordColor(type) {
const colors = {
'母乳亲喂': '#F9D9D9', // 浅粉色
'母乳瓶喂': '#D9D9F9', // 浅紫色
'奶粉喂养': '#F9E9D9', // 浅橙色/米色
'辅食': '#D9F9D9' // 浅绿色
'母乳亲喂': '#d3a358', // 浅粉色
'母乳瓶喂': '#d3a358', // 浅紫色
'奶粉喂养': '#d3a358', // 浅橙色/米色
'辅食': '#d3a358' // 浅绿色
}
return colors[type] || '#CCCCCC'
return colors[type] || '#d3a358'
}
// 获取记录背景色 - 根据设计稿调整
......@@ -339,14 +331,14 @@ function getRecordIconClass(type) {
// API 调用函数
async function loadStatisticsPeriod(startDate, endDate) {
const cacheKey = `${startDate}_${endDate}_${feedStore.getCurrentBabyId()}`
const cacheKey = `${startDate}_${endDate}_${feedStore.getCurrentBabyId()}`
// 避免重复请求
if (loadingStatistics.value.has(cacheKey)) {
console.log('该时间段统计数据正在加载中,跳过重复请求:', cacheKey)
return
}
// 检查缓存
const cachedData = statisticsCache.value.get(cacheKey)
if (cachedData && Date.now() - cachedData.timestamp < 10 * 60 * 1000) { // 10分钟缓存
......@@ -354,7 +346,7 @@ async function loadStatisticsPeriod(startDate, endDate) {
apiStatistics.value[cacheKey] = cachedData.data
return
}
try {
const startTime = Date.now()
loadingStatistics.value.add(cacheKey)
......@@ -362,52 +354,39 @@ async function loadStatisticsPeriod(startDate, endDate) {
if (loadingStatistics.value.size === 1) {
isLoading.value = true
}
// Mock 数据模拟
const mockResponse = {
code: '000000',
data: [
{ date: '2025-07-01', totalCount: 1, breastfeedingCount: 1, bottleCount: 0, formulaCount: 0, foodCount: 0 },
{ date: '2025-07-02', totalCount: 2, breastfeedingCount: 1, bottleCount: 1, formulaCount: 0, foodCount: 0 },
{ date: '2025-07-03', totalCount: 4, breastfeedingCount: 1, bottleCount: 1, formulaCount: 1, foodCount: 1 },
{ date: '2025-07-04', totalCount: 2, breastfeedingCount: 0, bottleCount: 0, formulaCount: 1, foodCount: 1 },
{ date: '2025-07-05', totalCount: 2, breastfeedingCount: 1, bottleCount: 0, formulaCount: 0, foodCount: 1 },
{ date: '2025-07-06', totalCount: 1, breastfeedingCount: 0, bottleCount: 0, formulaCount: 0, foodCount: 1 },
{ date: '2025-07-07', totalCount: 2, breastfeedingCount: 0, bottleCount: 1, formulaCount: 1, foodCount: 0 }
]
}
// 添加超时处理
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error('请求超时')), 10000) // 10秒超时
})
const apiPromise = feedingRecordsStatisticsPeriod({
babyId: feedStore.getCurrentBabyId(),
sdate: startDate,
edate: endDate
})
// 使用 mock 数据或真实 API
const response = await Promise.race([apiPromise, timeoutPromise])
if (DEBUG_API) {
console.log('获取统计数据:', { startDate, endDate }, response)
console.log('统计数据API返回数据结构:', JSON.stringify(response, null, 2))
}
// 检查响应状态
if (response && response.code && response.code !== '000000') {
throw new Error(`API返回错误: ${response.message || '未知错误'}`)
}
if (response && response.data) {
// 验证数据格式
if (!Array.isArray(response.data)) {
console.error('统计数据API返回的数据格式不正确,期望数组:', response.data)
return
}
// 转换API数据格式为页面需要的格式
const statistics = response.data.map(item => {
// 验证项目格式
......@@ -415,35 +394,34 @@ async function loadStatisticsPeriod(startDate, endDate) {
console.warn('跳过无效的统计项目:', item)
return null
}
return {
date: item.date,
dateString: item.date,
isActive: item.date === selectedDate.value,
totalCount: item.totalCount || 0,
date: item.feedDate,
dateString: item.feedDate,
totalCount: (item.breastFeedingCount || 0) + (item.bottleFeedingCount || 0) + (item.milkFeedingCount || 0) + (item.babyFoodCount || 0),
typeCounts: {
'母乳亲喂': item.breastfeedingCount || 0,
'母乳瓶喂': item.bottleCount || 0,
'奶粉喂养': item.formulaCount || 0,
'辅食': item.foodCount || 0
'母乳亲喂': item.breastFeedingCount || 0,
'母乳瓶喂': item.bottleFeedingCount || 0,
'奶粉喂养': item.milkFeedingCount || 0,
'辅食': item.babyFoodCount || 0
}
}
}).filter(item => item !== null) // 过滤掉无效项目
apiStatistics.value[cacheKey] = statistics
// 添加到缓存
statisticsCache.value.set(cacheKey, {
data: statistics,
timestamp: Date.now()
})
// 记录性能
const endTime = Date.now()
console.log(`获取统计数据耗时: ${endTime - startTime}ms`)
} else {
console.log('统计数据API返回空数据')
// 缓存空数据
statisticsCache.value.set(cacheKey, {
data: [],
......@@ -452,7 +430,7 @@ async function loadStatisticsPeriod(startDate, endDate) {
}
} catch (error) {
handleApiError(error, '获取统计数据')
// 简单的重试机制(最多重试1次)
if (!loadingStatistics.value.has(`${cacheKey}_retry`)) {
console.log('尝试重试获取统计数据...')
......@@ -462,7 +440,7 @@ async function loadStatisticsPeriod(startDate, endDate) {
}, 2000) // 2秒后重试
return
}
// 如果API失败,使用本地数据作为fallback
console.warn('API获取统计数据失败,使用本地数据作为fallback')
} finally {
......@@ -480,7 +458,7 @@ async function loadRecordsByDate(date) {
console.log('该日期记录正在加载中,跳过重复请求:', date)
return
}
// 检查缓存
const cacheKey = `${date}_${babyInfo.value.id}`
const cachedData = recordsCache.value.get(cacheKey)
......@@ -489,7 +467,7 @@ async function loadRecordsByDate(date) {
apiRecords.value[date] = cachedData.data
return
}
try {
const startTime = Date.now()
loadingRecords.value.add(date)
......@@ -497,36 +475,36 @@ async function loadRecordsByDate(date) {
if (loadingRecords.value.size === 1) {
isLoading.value = true
}
// Mock 数据模拟
const mockResponse = {
code: '000000',
data: [] // 删除所有 feedingRecords.value[date] || []
}
// 添加超时处理
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error('请求超时')), 10000) // 10秒超时
})
const apiPromise = feedingRecordsByDate({
babyId: feedStore.getCurrentBabyId(),
date: date
})
// 使用 mock 数据或真实 API
const response = await Promise.race([apiPromise, timeoutPromise])
if (DEBUG_API) {
console.log('获取指定日期记录:', date, response)
console.log('记录API返回数据结构:', JSON.stringify(response, null, 2))
}
// 检查响应状态
if (response && response.code && response.code !== '000000') {
throw new Error(`API返回错误: ${response.message || '未知错误'}`)
}
if (response && response.data) {
// 验证数据格式
if (!Array.isArray(response.data)) {
......@@ -534,7 +512,7 @@ async function loadRecordsByDate(date) {
apiRecords.value[date] = []
return
}
// 转换API数据格式为页面需要的格式
const records = response.data.map(record => {
// 验证记录格式
......@@ -542,7 +520,7 @@ async function loadRecordsByDate(date) {
console.warn('跳过无效的记录:', record)
return null
}
return {
id: record.id, // 保留原始记录ID,用于修改时传递
recordId: record.recordId, // 添加recordId字段用于编辑
......@@ -555,25 +533,25 @@ async function loadRecordsByDate(date) {
foodDetails: record.foodDetails || '' // 添加foodDetails字段
}
}).filter(record => record !== null) // 过滤掉无效记录
// 按时间排序
records.sort((a, b) => a.time.localeCompare(b.time))
apiRecords.value[date] = records
// 添加到缓存
recordsCache.value.set(cacheKey, {
data: records,
timestamp: Date.now()
})
// 记录性能
const endTime = Date.now()
console.log(`获取记录数据耗时: ${endTime - startTime}ms`)
} else {
apiRecords.value[date] = []
console.log('记录API返回空数据')
// 缓存空数据
recordsCache.value.set(cacheKey, {
data: [],
......@@ -582,7 +560,7 @@ async function loadRecordsByDate(date) {
}
} catch (error) {
handleApiError(error, '获取指定日期记录')
// 简单的重试机制(最多重试1次)
if (!loadingRecords.value.has(`${date}_retry`)) {
console.log('尝试重试获取记录数据...')
......@@ -592,7 +570,7 @@ async function loadRecordsByDate(date) {
}, 2000) // 2秒后重试
return
}
// 如果API失败,使用本地数据作为fallback
console.warn('API获取记录失败,使用本地数据作为fallback')
} finally {
......@@ -607,7 +585,7 @@ async function loadRecordsByDate(date) {
// 统一错误处理函数
function handleApiError(error, context) {
console.error(`${context} 失败:`, error)
// 检查网络状态
uni.getNetworkType({
success: (res) => {
......@@ -616,7 +594,7 @@ function handleApiError(error, context) {
}
}
})
// 根据错误类型提供不同的处理建议
if (error.message && error.message.includes('超时')) {
console.warn('请求超时,可能是网络问题')
......@@ -630,7 +608,7 @@ function handleApiError(error, context) {
// 工具函数
function formatTimeFromTimestamp(timestamp) {
if (!timestamp) return ''
let date
// 处理不同的时间戳格式
if (typeof timestamp === 'string') {
......@@ -648,13 +626,13 @@ function formatTimeFromTimestamp(timestamp) {
} else {
date = new Date(timestamp)
}
// 检查日期是否有效
if (isNaN(date.getTime())) {
console.error('无效的时间戳:', timestamp)
return ''
}
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
return `${hours}:${minutes}`
......@@ -662,8 +640,14 @@ function formatTimeFromTimestamp(timestamp) {
function formatDuration(seconds) {
if (!seconds || seconds <= 0) return ''
const minutes = Math.floor(seconds / 60)
return `${minutes}min`
const minutes = seconds / 60
if (minutes < 1) {
// 不足1分钟时显示小数,保留2位小数
return `${minutes.toFixed(2)}min`
} else {
// 1分钟以上时显示整数
return `${Math.floor(minutes)}min`
}
}
function getFeedingTypeLabel(type) {
......@@ -708,11 +692,11 @@ function calculateBabyAge(birthday) {
const today = new Date()
const diffTime = Math.abs(today - birthDate)
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24))
const years = Math.floor(diffDays / 365)
const months = Math.floor((diffDays % 365) / 30)
const days = diffDays % 30
if (years > 0) {
return `${years}${months}个月${days}天`
} else if (months > 0) {
......@@ -725,7 +709,7 @@ function calculateBabyAge(birthday) {
// 编辑记录
function editRecord(index) {
console.log('编辑记录:', index)
const record = todayRecords.value[index]
if (!record) {
uni.showToast({
......@@ -734,23 +718,23 @@ function editRecord(index) {
})
return
}
// 设置编辑表单数据
editForm.value = {
time: record.time || '',
time: record.time || getCurrentTime(), // 使用记录的原始时间,如果没有则使用当前时间
type: record.type || '',
content: record.foodDetails || record.content || '', // 优先使用foodDetails
leftDuration: record.leftDuration || '',
rightDuration: record.rightDuration || '',
amount: record.amount || ''
}
// 保存当前编辑的记录
editingRecord.value = {
index,
record
}
// 显示编辑弹窗
showEditPopup.value = true
}
......@@ -761,78 +745,158 @@ function goBack() {
}
// 生成周数据
function generateWeekData(weekOffset = 0) {
function generateWeekData(weekOffset = 0, targetDate = null) {
const data = []
const baseDate = new Date('2025-07-03')
const baseDate = new Date() // 使用当前日期作为基准
baseDate.setDate(baseDate.getDate() + weekOffset * 7)
// 计算当前周的开始和结束日期
const startDate = new Date(baseDate)
startDate.setDate(baseDate.getDate() - baseDate.getDay()) // 设置为周日
const endDate = new Date(startDate)
endDate.setDate(startDate.getDate() + 6) // 设置为周六
const startDateString = startDate.toISOString().split('T')[0]
const endDateString = endDate.toISOString().split('T')[0]
// 加载统计数据
loadStatisticsPeriod(startDateString, endDateString)
const startDateString = formatDateString(startDate)
const endDateString = formatDateString(endDate)
// 获取统计数据的缓存键
const cacheKey = `${startDateString}_${endDateString}_${feedStore.getCurrentBabyId()}`
const statisticsData = apiStatistics.value[cacheKey] || []
// 确定要选中的日期
let activeDate = targetDate
if (!activeDate) {
if (weekOffset === 0) {
// 当前周,选择今天
const today = new Date()
activeDate = formatDateString(today)
} else {
// 其他周,选择该周的第一天
activeDate = startDateString
}
}
for (let i = 0; i < 7; i++) {
const date = new Date(startDate)
date.setDate(startDate.getDate() + i)
const dateString = date.toISOString().split('T')[0]
// 优先使用API数据,如果没有则使用本地数据
const records = apiRecords.value[dateString] || []
// 统计各类型记录数量
const typeCounts = {
'母乳亲喂': 0,
'母乳瓶喂': 0,
'奶粉喂养': 0,
'辅食': 0
const dateString = formatDateString(date)
// 从统计数据中查找对应日期的数据
const statisticsItem = statisticsData.find(item => item.dateString === dateString)
if (statisticsItem) {
// 使用接口返回的统计数据
data.push({
date: `${date.getMonth() + 1}/${date.getDate()}`,
dateString,
totalCount: statisticsItem.totalCount,
typeCounts: statisticsItem.typeCounts
})
} else {
// 如果没有统计数据,使用默认值
data.push({
date: `${date.getMonth() + 1}/${date.getDate()}`,
dateString,
totalCount: 0,
typeCounts: {
'母乳亲喂': 0,
'母乳瓶喂': 0,
'奶粉喂养': 0,
'辅食': 0
}
})
}
records.forEach(record => {
if (typeCounts.hasOwnProperty(record.type)) {
typeCounts[record.type]++
}
})
data.push({
date: `${date.getMonth() + 1}/${date.getDate()}`,
dateString,
isActive: dateString === selectedDate.value,
totalCount: records.length,
typeCounts
})
}
return data
}
// 上一周
function prevWeek() {
async function prevWeek() {
currentWeek.value--
updateChartData()
await updateChartData(false) // 切换周时使用默认选择
}
// 下一周
function nextWeek() {
async function nextWeek() {
currentWeek.value++
updateChartData()
await updateChartData(false) // 切换周时使用默认选择
}
// 更新图表数据
function updateChartData() {
chartData.value = generateWeekData(currentWeek.value)
async function updateChartData(keepCurrentSelection = false) {
// 确保统计数据已加载
const baseDate = new Date()
baseDate.setDate(baseDate.getDate() + currentWeek.value * 7)
const startDate = new Date(baseDate)
startDate.setDate(baseDate.getDate() - baseDate.getDay())
const endDate = new Date(startDate)
endDate.setDate(startDate.getDate() + 6)
const startDateString = formatDateString(startDate)
const endDateString = formatDateString(endDate)
// 等待统计数据加载完成
await loadStatisticsPeriod(startDateString, endDateString)
// 确定要选中的日期
let targetDate = null
if (keepCurrentSelection && selectedDate.value) {
// 如果保持当前选择,检查当前选中的日期是否在当前周内
const currentSelectedDate = new Date(selectedDate.value)
const weekStart = new Date(startDate)
const weekEnd = new Date(endDate)
if (currentSelectedDate >= weekStart && currentSelectedDate <= weekEnd) {
// 当前选中的日期在当前周内,保持选中
targetDate = selectedDate.value
} else {
// 当前选中的日期不在当前周内,使用默认选择
if (currentWeek.value === 0) {
// 当前周,选择今天
const today = new Date()
targetDate = formatDateString(today)
} else {
// 其他周,选择该周的第一天
targetDate = startDateString
}
}
} else {
// 使用默认选择逻辑
if (currentWeek.value === 0) {
// 当前周,选择今天
const today = new Date()
targetDate = formatDateString(today)
} else {
// 其他周,选择该周的第一天
targetDate = startDateString
}
}
// 更新图表数据,传入目标日期
chartData.value = generateWeekData(currentWeek.value, targetDate)
// 更新选中的日期
selectedDate.value = targetDate
// 加载选中日期的记录
if (selectedDate.value) {
await loadRecordsByDate(selectedDate.value)
}
}
// 点击柱状图切换日期
function onBarClick(day) {
if (day.dateString) {
selectDate(day.dateString)
async function onBarClick(day) {
console.log('柱状图点击事件触发:', day)
console.log('当前 chartData:', chartData.value)
console.log('当前 selectedDate:', selectedDate.value)
if (day && day.dateString) {
console.log('切换到日期:', day.dateString)
await selectDate(day.dateString)
} else {
console.log('无效的 day 对象或缺少 dateString:', day)
}
}
......@@ -867,7 +931,7 @@ function saveEditRecord() {
})
return
}
// 验证喂养详情(最多20字)
if (editForm.value.content && editForm.value.content.length > 20) {
uni.showToast({
......@@ -876,7 +940,7 @@ function saveEditRecord() {
})
return
}
if (!editingRecord.value) {
uni.showToast({
title: '编辑记录不存在',
......@@ -884,7 +948,7 @@ function saveEditRecord() {
})
return
}
// 构建API请求数据
const { index, record } = editingRecord.value
const apiData = {
......@@ -893,7 +957,7 @@ function saveEditRecord() {
recordTime: formatDateTimeString(selectedDate.value, editForm.value.time),
feedingType: getFeedingTypeId(editForm.value.type)
}
// 根据喂养类型添加相应字段
if (editForm.value.type === '母乳亲喂') {
apiData.durationLeftSeconds = parseDurationToSeconds(editForm.value.leftDuration)
......@@ -901,12 +965,12 @@ function saveEditRecord() {
} else if (editForm.value.type === '母乳瓶喂' || editForm.value.type === '奶粉喂养') {
apiData.volume = parseVolumeToNumber(editForm.value.amount)
}
// 统一添加 foodDetails 字段
if (editForm.value.content) {
apiData.foodDetails = editForm.value.content
}
// 调用API保存修改
saveRecordToAPI(apiData, index)
}
......@@ -918,32 +982,40 @@ function getCurrentTime() {
return `${hours}:${minutes}`
}
// 格式化日期字符串
function formatDateString(date) {
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
}
async function saveRecordToAPI(apiData, index) {
try {
uni.showLoading({
title: '保存中...'
})
const response = await feedingRecordsAPI(apiData)
if (response && response.code === '000000') {
uni.showToast({
title: '保存成功',
icon: 'success'
})
closeEditPopup()
// 清空缓存,确保获取最新数据
clearCache()
// 重新加载当前日期的记录
if (selectedDate.value) {
await loadRecordsByDate(selectedDate.value)
}
// 更新图表数据
updateChartData()
updateChartData(true) // 保存记录后保持当前选择
} else {
throw new Error(response?.message || '保存失败')
}
......@@ -961,7 +1033,7 @@ async function saveRecordToAPI(apiData, index) {
function updateLocalRecord(index, formData) {
const dateKey = selectedDate.value
if (!dateKey) return
// 更新API数据
if (apiRecords.value[dateKey] && apiRecords.value[dateKey][index]) {
const record = apiRecords.value[dateKey][index]
......@@ -972,19 +1044,19 @@ function updateLocalRecord(index, formData) {
record.rightDuration = formData.rightDuration
record.amount = formData.amount
}
// 更新本地数据
// 本地数据不再需要更新,因为API调用失败时,apiRecords.value[dateKey] 为空
}
// 选择日期
function selectDate(dateString) {
async function selectDate(dateString) {
console.log('selectDate 被调用,日期:', dateString)
selectedDate.value = dateString
// 加载选中日期的记录
loadRecordsByDate(dateString)
updateChartData()
await loadRecordsByDate(dateString)
console.log('selectDate 完成,当前选中日期:', selectedDate.value)
}
// 获取柱状图段数据
......@@ -1022,24 +1094,53 @@ function getBarSegments(day) {
// 页面加载
onLoad(() => {
updateChartData()
updateChartData(false) // 初始化时使用默认选择
// 初始化API数据
initializeApiData()
})
// 初始化图表数据
async function initializeChartData() {
console.log('初始化图表数据...')
// 加载当前周的统计数据
const baseDate = new Date()
const startDate = new Date(baseDate)
startDate.setDate(baseDate.getDate() - baseDate.getDay()) // 设置为周日
const endDate = new Date(startDate)
endDate.setDate(startDate.getDate() + 6) // 设置为周六
const startDateString = formatDateString(startDate)
const endDateString = formatDateString(endDate)
// 等待统计数据加载完成
await loadStatisticsPeriod(startDateString, endDateString)
// 更新图表数据
await updateChartData(false) // 初始化时使用默认选择
console.log('图表数据初始化完成')
}
// 初始化API数据
async function initializeApiData() {
console.log('初始化API数据...')
// 加载当前选中日期的记录
if (selectedDate.value) {
console.log('加载当前选中日期记录:', selectedDate.value)
await loadRecordsByDate(selectedDate.value)
}
console.log('API数据初始化完成')
}
// 页面挂载时初始化数据
onMounted(async () => {
// 先加载统计数据,然后更新图表(updateChartData 中已经包含了加载记录的逻辑)
await initializeChartData()
})
// 页面显示时刷新数据
onShow(() => {
// 如果已经有选中日期,刷新当前日期的记录
......@@ -1048,10 +1149,10 @@ onShow(() => {
}
})
// 监听选中日期变化
watch(selectedDate, () => {
updateChartData()
})
// 移除监听选中日期变化,避免循环调用
// watch(selectedDate, () => {
// updateChartData()
// })
// 格式化月份字符串
function formatMonthString(date) {
......@@ -1082,7 +1183,7 @@ function testApiIntegration() {
console.log('=== 测试API集成 ===')
console.log('当前选中日期:', selectedDate.value)
console.log('当前周数:', currentWeek.value)
console.log('当前宝宝ID:', feedStore.getCurrentBabyId())
console.log('当前宝宝ID:', feedStore.getCurrentBabyId())
console.log('API统计数据:', apiStatistics.value)
console.log('API记录数据:', apiRecords.value)
console.log('缓存状态:', {
......@@ -1113,7 +1214,7 @@ function testApiIntegration() {
<style lang="scss" scoped>
.feeding-analysis-page {
min-height: 100vh;
background: #FDFBF7; // 根据设计稿调整背景色
background: #fef6eb; // 根据设计稿调整背景色
display: flex;
flex-direction: column;
}
......@@ -1121,27 +1222,25 @@ function testApiIntegration() {
// 状态栏占位
.status-bar-placeholder {
height: 75rpx;
background: #FDFBF7;
background: #fef6eb;
}
// 头部导航
.header {
display: flex;
justify-content: flex-start;
justify-content: space-between;
align-items: center;
padding: 20rpx 30rpx;
background: #FDFBF7;
.nav-left {
display: flex;
align-items: center;
padding: 15rpx;
.back-btn {
font-size: 40rpx;
color: #000000; // 黑色箭头
font-weight: bold;
line-height: 1;
width: 20rpx;
height: 32rpx;
margin-right: 20rpx;
}
}
}
......@@ -1149,15 +1248,17 @@ function testApiIntegration() {
// 图例
.legend-section {
display: flex;
justify-content: space-around;
// justify-content: space-around;
justify-content: start;
align-items: center;
padding: 25rpx 40rpx;
background: #FDFBF7;
background: #fef6eb;
.legend-item {
display: flex;
align-items: center;
gap: 12rpx;
margin-right: 20rpx;
.legend-color {
width: 32rpx; // 16px * 2
......@@ -1191,41 +1292,43 @@ function testApiIntegration() {
// CSS柱状图区域
.chart-section {
background: #FDFBF7;
padding: 30rpx 0;
// background: #FDFBF7;
// padding: 30rpx 0;
.chart-container {
display: flex;
align-items: center;
padding: 0 30rpx;
// padding: 0 30rpx;
.chart-nav-left,
.chart-nav-right {
width: 70rpx;
height: 70rpx;
background: #FDFBF7; // 浅米色背景
border-radius: 35rpx;
width: 42rpx;
height: 144rpx;
// background: #FDFBF7; // 浅米色背景
// border-radius: 35rpx;
display: flex;
align-items: center;
justify-content: center;
margin: 0 15rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
// margin: 0 15rpx;
// box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
.nav-arrow {
font-size: 28rpx;
color: #000000; // 黑色箭头
font-weight: bold;
line-height: 1;
width: 42rpx;
height: 144rpx;
// font-size: 28rpx;
// color: #000000; // 黑色箭头
// font-weight: bold;
// line-height: 1;
}
}
.chart-content {
flex: 1;
margin: 0 25rpx;
height: 220rpx;
background: transparent; // 去掉背景色
border-radius: 16rpx;
padding: 20rpx;
// flex: 1;
// margin: 0 25rpx;
height: 500rpx;
// background: transparent; // 去掉背景色
// border-radius: 16rpx;
// padding: 20rpx;
// 去掉阴影
.chart-bars {
......@@ -1233,76 +1336,59 @@ function testApiIntegration() {
align-items: flex-end; // 从底部对齐
justify-content: space-between;
height: 100%;
padding: 20rpx 0 40rpx 0;
// padding: 20rpx 0 40rpx 0;
box-sizing: border-box;
position: relative;
.chart-bar-wrapper {
display: flex;
flex-direction: column;
align-items: center;
flex: 1;
position: relative;
padding: 0 10rpx;
cursor: pointer;
transition: all 0.3s ease;
.chart-bar-wrapper {
display: flex;
flex-direction: column;
align-items: center;
flex: 1;
position: relative;
padding: 0 10rpx;
transition: all 0.3s ease;
&:hover {
transform: scale(1.05);
}
&:active {
transform: scale(0.95);
}
&.active {
.chart-bar {
// 选中状态不需要背景色,保持透明
}
// 选中状态的背景显示在日期文字后面
.bar-date {
background: #F0EFEA;
padding: 4rpx 8rpx;
border-radius: 4rpx;
}
}
.chart-bar {
width: 60rpx;
height: 120rpx;
width: 77rpx;
height: 360rpx;
display: flex;
flex-direction: column;
// 移除容器的border-radius,让内部段自己控制圆角
position: relative;
background: transparent;
justify-content: flex-end; // 确保段从底部开始
margin-bottom: 20rpx;
.bar-segment {
width: 100%;
min-height: 10rpx;
transition: height 0.3s ease;
flex-shrink: 0;
// 如果只有一个段,整个柱子都有圆角
&:only-child {
border-radius: 8rpx !important;
}
// 多个段的情况
&:not(:only-child) {
// 第一个段(底部)有底部圆角
&:first-child {
border-top-left-radius: 8rpx !important;
border-top-right-radius: 8rpx !important;
}
// 最后一个段(顶部)有顶部圆角
&:last-child {
border-bottom-left-radius: 8rpx !important;
border-bottom-right-radius: 8rpx !important;
}
// 中间段没有圆角
&:not(:first-child):not(:last-child) {
border-radius: 0 !important;
......@@ -1313,7 +1399,7 @@ function testApiIntegration() {
.bar-count {
position: absolute;
top: -30rpx; // 调整位置到柱子顶部上方
top: -40rpx; // 调整位置到柱子顶部上方
left: 50%;
transform: translateX(-50%);
font-size: 20rpx;
......@@ -1321,6 +1407,7 @@ function testApiIntegration() {
font-weight: bold;
white-space: nowrap;
z-index: 3;
pointer-events: none; // 确保不拦截点击事件
}
.bar-date {
......@@ -1328,26 +1415,56 @@ function testApiIntegration() {
font-size: 24rpx;
color: #666666;
font-weight: 500;
position: relative;
z-index: 2;
pointer-events: none; // 确保不拦截点击事件
}
&.active .bar-date {
color: #C89F6B;
font-weight: bold;
// 确保点击时没有任何背景色变化
&:active {
background: transparent !important;
}
// 确保hover时没有任何背景色变化
&:hover {
background: transparent !important;
}
.selected-bg {
position: absolute;
bottom: -20rpx;
left: 50%;
transform: translateX(-50%);
width: 156rpx;
height: 62rpx;
z-index: 1;
pointer-events: none; // 确保不拦截点击事件
}
}
}
}
}
.baby-age {
text-align: center;
padding: 25rpx 0;
}
text {
font-size: 26rpx;
color: #000000; // 黑色文字
font-weight: bold; // 加粗
}
.gap-bg {
height: 43rpx;
width: 100%;
margin-top: -43rpx;
border-radius: 30rpx 30rpx 0 0;
background: #ffffff;
}
.baby-age {
text-align: left;
padding: 25rpx 0 0 40rpx;
background: #ffffff;
height: 80rpx;
text {
font-size: 26rpx;
color: #000000; // 黑色文字
font-weight: bold; // 加粗
}
}
......@@ -1559,6 +1676,8 @@ function testApiIntegration() {
z-index: 1000;
display: flex;
align-items: flex-end;
overflow: hidden;
touch-action: none;
}
.popup-content {
......@@ -1589,6 +1708,11 @@ function testApiIntegration() {
display: block;
}
.input-container {
position: relative;
width: 100%;
}
.picker,
.input {
width: 100%;
......@@ -1597,6 +1721,7 @@ function testApiIntegration() {
border-radius: 12rpx;
font-size: 28rpx;
color: #333;
min-height: 88rpx;
border: 1rpx solid #e9ecef;
box-sizing: border-box;
}
......@@ -1609,10 +1734,16 @@ function testApiIntegration() {
.char-count {
position: absolute;
right: 0;
bottom: -30rpx;
right: 20rpx;
top: 50%;
transform: translateY(-50%);
font-size: 24rpx;
color: #999;
pointer-events: none;
z-index: 1;
background: rgba(248, 249, 250, 0.8);
padding: 2rpx 8rpx;
border-radius: 4rpx;
}
}
......@@ -1661,49 +1792,32 @@ function testApiIntegration() {
@media screen and (max-width: 750rpx) {
.legend-section {
padding: 25rpx 30rpx;
.legend-item {
gap: 8rpx;
.legend-color {
width: 20rpx;
height: 20rpx;
}
.legend-text {
font-size: 24rpx;
}
}
}
.chart-section {
.chart-container {
padding: 0 20rpx;
.chart-nav-left,
.chart-nav-right {
width: 60rpx;
height: 60rpx;
margin: 0 10rpx;
}
.chart-content {
margin: 0 20rpx;
height: 200rpx;
}
}
}
.records-container {
.record-list {
padding: 0 20rpx 30rpx 20rpx;
.record-item {
.time-dot {
width: 120rpx;
margin-right: 15rpx;
}
.record-content {
padding: 24rpx;
}
......
......@@ -72,6 +72,8 @@
<view class="bottom-section">
<image class="bottom-bg" :src="feedingIndexRes.bottom_bg" mode="aspectFill" />
<view class="bottom-bg-content"></view>
<!-- 温馨提示 -->
<view class="warm-tip"
v-if="selectedType === 'breastfeeding' && recordMethods[selectedType] === 'manual'">
......@@ -171,7 +173,9 @@
<view class="food-categories">
<text class="category-title">辅食分类</text>
<view class="category-actions">
<text v-if="foodSelectionState.isEditMode" class="complete-btn" @click="exitEditMode">
<text v-if="foodSelectionState.isEditMode" class="complete-btn"
:class="{ disabled: foodSelectionState.isDeletingFood }"
@click="foodSelectionState.isDeletingFood ? null : exitEditMode()">
完成
</text>
<view v-else class="delete-btn" @click="enterEditMode">
......@@ -186,9 +190,13 @@
<view v-for="(category, categoryName) in foodCategories" :key="categoryName"
class="category-item">
<text class="category-name">{{ categoryName }}</text>
<image class="add-btn" :class="{ disabled: foodSelectionState.isEditMode }"
:src="feedingIndexRes.icon_fushi_add" mode="aspectFit"
@click="showAddFoodPopup(categoryName)" />
<view class="add-section">
<image class="add-btn" :class="{
disabled: foodSelectionState.isEditMode || category.customItems.length >= 20 || foodSelectionState.isAddingFood
}" :src="feedingIndexRes.icon_fushi_add" mode="aspectFit"
@click="foodSelectionState.isAddingFood ? null : showAddFoodPopup(categoryName)" />
<text v-if="category.customItems.length >= 20" class="limit-tip">已达上限</text>
</view>
<view class="selected-tags">
<!-- 默认辅食项 -->
<view v-for="(item, index) in category.items" :key="item" class="tag-item" :class="{
......@@ -213,13 +221,13 @@
:src="feedingIndexRes.icon_shanchu_x" mode="aspectFit"
@click.stop="removeFoodItem(categoryName, item)" />
</view>
<!-- 展开/缩起按钮 -->
<view v-if="(category.items.length + category.customItems.length) > 3"
class="expand-btn" @click="toggleCategoryExpansion(categoryName)">
<image class="expand-icon"
:src="category.expanded ? feedingIndexRes.icon_fushi_up : feedingIndexRes.icon_fushi_drop"
mode="aspectFit" />
</view>
</view>
<!-- 展开/缩起按钮 -->
<view v-if="(category.items.length + category.customItems.length) > 3"
class="expand-btn" @click="toggleCategoryExpansion(categoryName)">
<image class="expand-icon"
:src="category.expanded ? feedingIndexRes.icon_fushi_up : feedingIndexRes.icon_fushi_drop"
mode="aspectFit" />
</view>
</view>
</view>
......@@ -303,20 +311,21 @@
</view>
</view>
<!-- 底部完成按钮 -->
<view class="bottom_complete-btn" :class="{ 'disabled': isSubmitting }" @click="completeRecord">
<image class="complete-btn-bg" :src="feedingIndexRes.complete_btn" />
<!-- <text class="complete-btn-text">{{ isSubmitting ? '保存中...' : '完成记录' }}</text> -->
</view>
</view>
</scroll-view>
<!-- 底部完成按钮 -->
<view class="bottom_complete-btn" :class="{ 'disabled': isSubmitting }" @click="completeRecord">
<image class="complete-btn-bg" :src="feedingIndexRes.complete_btn" />
<!-- <text class="complete-btn-text">{{ isSubmitting ? '保存中...' : '完成记录' }}</text> -->
</view>
</view>
<!-- 添加辅食弹窗 -->
<uni-popup ref="addFoodPopup" type="center" :mask-click="false">
<view class="add-food-popup">
<view class="popup-header">
<text class="popup-title">新增辅食名称快捷键</text>
<text class="popup-title">新增辅食名称</text>
</view>
<view class="popup-content">
<view class="input-container">
......@@ -328,8 +337,11 @@
<view class="popup-buttons">
<image class="cancel-btn" :src="feedingIndexRes.icon_fushi_btn_cancel" mode="aspectFit"
@click="cancelAddFood"></image>
<image class="confirm-btn" :src="feedingIndexRes.icon_fushi_btn_add" mode="aspectFit"
@click="confirmAddFood"></image>
<image class="confirm-btn"
:class="{ disabled: foodSelectionState.isAddingFood || !foodSelectionState.newFoodItem.trim() }"
:src="feedingIndexRes.icon_fushi_btn_add" mode="aspectFit"
@click="(foodSelectionState.isAddingFood || !foodSelectionState.newFoodItem.trim()) ? null : confirmAddFood()">
</image>
</view>
</view>
</uni-popup>
......@@ -377,17 +389,16 @@
@change="onBabyChange" v-if="babyList.length > 1" />
<!-- 记录成功弹窗 -->
<uni-popup ref="successPopup" type="center" :mask-click="false" :show="true"
@close="showSuccessPopup = false">
<uni-popup ref="successPopup" type="center" :mask-click="false">
<view class="success-popup">
<image class="success-bg" :src="feedingIndexRes.add_suc_bg" mode="aspectFit" />
<view class="success-content">
<text class="success-title">记录成功</text>
<view class="success-buttons">
<image class="success-btn success-btn-jump" :src="feedingIndexRes.add_suc_btn_jump" mode="aspectFit"
@click="onSuccessJump" />
<image class="success-btn success-btn-close" :src="feedingIndexRes.add_suc_btn_close"
mode="aspectFit" @click="onSuccessClose" />
<image class="success-btn success-btn-jump" :src="feedingIndexRes.add_suc_btn_jump" mode="aspectFit"
@click="onSuccessJump" />
</view>
</view>
</view>
......@@ -403,6 +414,7 @@ import { useFeedStore } from '@/stores/feed.js'
// 弹窗引用
const addFoodPopup = ref(null)
const successPopup = ref(null)
const { proxy } = getCurrentInstance();
const $baseUrl = proxy.$baseUrl;
......@@ -567,11 +579,12 @@ const foodSelectionState = ref({
currentCategory: '', // 当前添加的分类
newFoodItem: '', // 新添加的辅食项
pendingDeletes: [], // 待删除的辅食列表
originalFoodData: null // 保存原始辅食数据,用于恢复
originalFoodData: null, // 保存原始辅食数据,用于恢复
isAddingFood: false, // 添加辅食防连点状态
isDeletingFood: false // 删除辅食防连点状态
})
// 记录成功弹窗状态
const showSuccessPopup = ref(false)
// 记录成功弹窗状态 - 已移除,改用 .open() 和 .close() 方法
// 语音识别状态
const voiceRecognitionState = ref({
......@@ -704,9 +717,9 @@ onUnmounted(() => {
voiceRecognitionState.value.recordingTimer = null
}
// 清理录音管理器事件监听器
recorderManager.offStart()
recorderManager.offStop()
recorderManager.offError()
recorderManager?.offStart()
recorderManager?.offStop()
recorderManager?.offError()
})
// 页面参数接收
......@@ -1312,9 +1325,9 @@ function startLeftTimer() {
}
leftTimerInterval = setInterval(() => {
timerData.value.leftDuration++
// 检查是否达到60分钟上限,达到时自动暂停
if (timerData.value.leftDuration >= 3600) { // 3600秒 = 60分钟
if (timerData.value.leftDuration >= 300) { // 3600秒 = 60分钟
console.log('左侧计时器达到60分钟上限,自动暂停')
stopLeftTimer()
isLeftTimerRunning.value = false
......@@ -1340,9 +1353,9 @@ function startRightTimer() {
}
rightTimerInterval = setInterval(() => {
timerData.value.rightDuration++
// 检查是否达到60分钟上限,达到时自动暂停
if (timerData.value.rightDuration >= 3600) { // 3600秒 = 60分钟
if (timerData.value.rightDuration >= 300) { // 3600秒 = 60分钟
console.log('右侧计时器达到60分钟上限,自动暂停')
stopRightTimer()
isRightTimerRunning.value = false
......@@ -1458,9 +1471,9 @@ async function completeRecord() {
clearFeedingData()
// 显示成功弹窗
showSuccessPopup.value = true
successPopup.value.open()
} catch (error) {
......@@ -1502,19 +1515,19 @@ function validateRecordData() {
const leftDuration = feedingData.value.breastfeeding.leftDuration
const rightDuration = feedingData.value.breastfeeding.rightDuration
if (leftDuration === 0 && rightDuration === 0) {
return { valid: false, message: '请设置喂养时长' }
return { valid: false, message: '还没有输入喂养信息哦~' }
}
} else if (currentMethod === 'timer') {
// 计时器模式:验证计时器状态
const leftDuration = timerData.value.leftDuration
const rightDuration = timerData.value.rightDuration
if (leftDuration === 0 && rightDuration === 0) {
return { valid: false, message: '请先启动计时器' }
return { valid: false, message: '还没有输入喂养信息哦~' }
}
} else if (currentMethod === 'voice') {
// 语音模式:验证语音识别结果
if (!voiceRecognitionState.value.recognizedText.trim()) {
return { valid: false, message: '请先进行语音识别' }
return { valid: false, message: '还没有输入喂养信息哦~' }
}
}
break
......@@ -1527,12 +1540,12 @@ function validateRecordData() {
// 手动模式:验证喂养量
const amount = feedingData.value[selectedType.value].amount
if (amount === 0) {
return { valid: false, message: '请设置喂养量' }
return { valid: false, message: '还没有输入喂养信息哦~' }
}
} else if (currentMethod2 === 'voice') {
// 语音模式:验证语音识别结果
if (!voiceRecognitionState.value.recognizedText.trim()) {
return { valid: false, message: '请先进行语音识别' }
return { valid: false, message: '还没有输入喂养信息哦~' }
}
}
break
......@@ -1541,7 +1554,7 @@ function validateRecordData() {
// 辅食只有手动选择模式
const selectedItems = feedingData.value.food.selectedItems
if (selectedItems.length === 0) {
return { valid: false, message: '请选择辅食' }
return { valid: false, message: '还没有输入喂养信息哦~' }
}
break
}
......@@ -1784,6 +1797,14 @@ function cancelEditMode() {
}
async function exitEditMode() {
// 防连点检查
if (foodSelectionState.value.isDeletingFood) {
console.log('防连点:删除完成按钮被阻止')
return
}
foodSelectionState.value.isDeletingFood = true
console.log('防连点:设置删除完成状态为true')
// 如果有待删除的辅食,调用接口删除
if (foodSelectionState.value.pendingDeletes.length > 0) {
try {
......@@ -1814,6 +1835,24 @@ async function exitEditMode() {
title: '删除失败',
icon: 'none'
})
// 删除失败时恢复数据
console.log('删除失败,恢复原始数据')
if (foodSelectionState.value.originalFoodData) {
// 恢复原始辅食数据
Object.keys(foodCategories.value).forEach(categoryName => {
const originalCategory = foodSelectionState.value.originalFoodData[categoryName]
if (originalCategory) {
foodCategories.value[categoryName].items = [...originalCategory.items]
foodCategories.value[categoryName].customItems = [...originalCategory.customItems]
}
})
// 清空待删除列表
foodSelectionState.value.pendingDeletes = []
console.log('数据恢复完成')
}
}
}
......@@ -1821,15 +1860,39 @@ async function exitEditMode() {
foodSelectionState.value.isEditMode = false
// 清理原始数据
foodSelectionState.value.originalFoodData = null
// 立即重置防连点状态
foodSelectionState.value.isDeletingFood = false
console.log('防连点:重置删除完成状态为false')
}
function showAddFoodPopup(categoryName) {
// 防连点检查
if (foodSelectionState.value.isAddingFood) {
console.log('防连点:添加辅食按钮被阻止')
return
}
foodSelectionState.value.isAddingFood = true
console.log('防连点:设置添加辅食状态为true')
// 在编辑模式下不允许添加
if (foodSelectionState.value.isEditMode) {
uni.showToast({
title: '编辑模式下不能添加辅食',
icon: 'none'
})
foodSelectionState.value.isAddingFood = false
return
}
// 检查分类下的自定义辅食数量限制
const category = foodCategories.value[categoryName]
if (category.customItems.length >= 20) {
uni.showToast({
title: '该分类下最多可添加20个自定义辅食',
icon: 'none'
})
foodSelectionState.value.isAddingFood = false
return
}
......@@ -1837,6 +1900,12 @@ function showAddFoodPopup(categoryName) {
foodSelectionState.value.newFoodItem = ''
// 显示弹窗
addFoodPopup.value.open()
// 立即重置防连点状态(弹窗显示后)
setTimeout(() => {
foodSelectionState.value.isAddingFood = false
console.log('防连点:重置添加辅食状态为false')
}, 100)
}
// 切换分类展开/缩起状态
......@@ -1853,6 +1922,13 @@ function cancelAddFood() {
}
function confirmAddFood() {
if (foodSelectionState.value.isAddingFood) {
console.log('防连点:添加辅食按钮被阻止')
return
}
foodSelectionState.value.isAddingFood = true
console.log('防连点:设置添加辅食状态为true')
const itemName = foodSelectionState.value.newFoodItem.trim()
const categoryName = foodSelectionState.value.currentCategory
......@@ -1861,11 +1937,18 @@ function confirmAddFood() {
title: '请输入辅食名称',
icon: 'none'
})
foodSelectionState.value.isAddingFood = false
return
}
addCustomFoodItem(categoryName, itemName)
addFoodPopup.value.close()
// 2秒后重置防连点状态
setTimeout(() => {
foodSelectionState.value.isAddingFood = false
console.log('防连点:重置添加辅食状态为false')
}, 2000)
}
async function addCustomFoodItem(categoryName, itemName) {
......@@ -1875,6 +1958,7 @@ async function addCustomFoodItem(categoryName, itemName) {
title: '辅食名称不能超过10个字',
icon: 'none'
})
foodSelectionState.value.isAddingFood = false
return
}
......@@ -1885,6 +1969,7 @@ async function addCustomFoodItem(categoryName, itemName) {
title: '该分类下最多可添加20个自定义辅食',
icon: 'none'
})
foodSelectionState.value.isAddingFood = false
return
}
......@@ -1894,6 +1979,7 @@ async function addCustomFoodItem(categoryName, itemName) {
title: '该辅食已存在',
icon: 'none'
})
foodSelectionState.value.isAddingFood = false
return
}
......@@ -1906,6 +1992,7 @@ async function addCustomFoodItem(categoryName, itemName) {
title: '辅食类型错误',
icon: 'none'
})
foodSelectionState.value.isAddingFood = false
return
}
......@@ -1922,7 +2009,7 @@ async function addCustomFoodItem(categoryName, itemName) {
uni.showToast({
title: '添加成功',
icon: 'success'
icon: 'none'
})
} catch (error) {
......@@ -1931,6 +2018,8 @@ async function addCustomFoodItem(categoryName, itemName) {
title: '添加失败',
icon: 'none'
})
// 失败时重置防连点状态
foodSelectionState.value.isAddingFood = false
}
}
......@@ -2188,7 +2277,8 @@ async function processVoiceFile(tempFilePath) {
try {
// 显示加载提示
uni.showLoading({
title: '正在转换并上传语音...'
title: '正在转换并上传语音...',
mask: true
})
// 上传语音文件(转换为base64)
......@@ -2515,7 +2605,7 @@ async function completeVoiceRecord() {
}
// 显示成功弹窗
showSuccessPopup.value = true
successPopup.value.open()
} catch (error) {
console.error('保存语音记录失败:', error)
......@@ -2536,17 +2626,14 @@ async function completeVoiceRecord() {
// 记录成功弹窗相关方法
function onSuccessJump() {
// 跳转到喂养记录页面
uni.navigateTo({
url: '/pages/feedingRecord/feedingRecord'
})
uni.navigateBack()
// 关闭弹窗
showSuccessPopup.value = false
successPopup.value.close()
}
function onSuccessClose() {
// 关闭弹窗
showSuccessPopup.value = false
successPopup.value.close()
}
// 检查是否有未完成的编辑操作
......@@ -2677,7 +2764,7 @@ function loadDefaultFoodsData() {
<style lang="scss" scoped>
/* ===== 页面整体布局 ===== */
.feeding-record-add-page {
background: #FFF8F1;
background: #ffffff;
min-height: 100vh;
padding: 0;
position: relative;
......@@ -2685,32 +2772,42 @@ function loadDefaultFoodsData() {
/* ===== 可滚动内容区域 ===== */
.scrollable-content {
height: calc(100vh);
height: calc(100vh - 218rpx);
background-color: #FFF8F1;
/* 减去底部按钮的高度 */
overflow-y: auto;
}
/* ===== 底部完成按钮 ===== */
.bottom_complete-btn {
padding-top: 20rpx;
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 120rpx;
z-index: 1000;
bottom: 0rpx;
left: 0rpx;
right: 0rpx;
width: 750rpx;
height: 218rpx;
display: flex;
align-items: center;
background: #fff;
justify-content: center;
align-items: center;
z-index: 100;
.complete-btn-bg {
width: 100%;
height: 100%;
width: 686rpx;
height: 94rpx;
display: block;
}
&.disabled {
opacity: 0.6;
pointer-events: none;
}
&:active {
transform: scale(0.95);
}
}
/* ===== 广告横幅 ===== */
......@@ -2744,7 +2841,7 @@ function loadDefaultFoodsData() {
display: flex;
align-items: center;
gap: 15rpx;
cursor: pointer;
// cursor: pointer;
transition: opacity 0.3s ease;
/* 移除点击高亮效果 */
-webkit-tap-highlight-color: transparent;
......@@ -2793,7 +2890,7 @@ function loadDefaultFoodsData() {
.time-display {
display: flex;
align-items: center;
cursor: pointer;
// cursor: pointer;
/* 移除点击高亮效果 */
-webkit-tap-highlight-color: transparent;
-webkit-touch-callout: none;
......@@ -2854,6 +2951,7 @@ function loadDefaultFoodsData() {
left: 0;
right: 0;
display: flex;
background-color: #FFF8F1;
justify-content: space-between;
padding: 0rpx 30rpx;
gap: 15rpx;
......@@ -2938,6 +3036,7 @@ function loadDefaultFoodsData() {
right: 0;
padding: 30rpx;
// margin-top: 30rpx;
background-color: #fff;
// height: 300rpx;
z-index: 2;
}
......@@ -3183,7 +3282,14 @@ function loadDefaultFoodsData() {
.complete-btn {
font-size: 26rpx;
color: #6f6d67;
cursor: pointer;
// cursor: pointer;
transition: all 0.3s ease;
&.disabled {
opacity: 0.5;
cursor: not-allowed;
pointer-events: none;
}
}
.delete-btn {
......@@ -3192,7 +3298,7 @@ function loadDefaultFoodsData() {
gap: 8rpx;
font-size: 26rpx;
color: #6f6d67;
cursor: pointer;
// cursor: pointer;
.delete-icon {
width: 24rpx;
......@@ -3206,51 +3312,74 @@ function loadDefaultFoodsData() {
.category-item {
display: flex;
align-items: flex-start;
gap: 20rpx;
// gap: 20rpx;
margin-bottom: 20rpx;
flex-wrap: wrap;
padding: 0 20rpx;
position: relative;
.category-name {
font-size: 26rpx;
color: #1d1e25;
min-width: 80rpx;
min-width: 60rpx;
margin-top: 8rpx;
flex-shrink: 0;
}
.add-section {
display: flex;
flex-direction: column;
align-items: center;
flex-shrink: 0;
margin-right: 20rpx;
}
.add-btn {
width: 40rpx;
height: 40rpx;
cursor: pointer;
margin-top: 8rpx;
width: 80rpx;
height: 60rpx;
transition: all 0.3s ease;
flex-shrink: 0;
&.disabled {
opacity: 0.5;
cursor: not-allowed;
pointer-events: none;
}
}
.limit-tip {
font-size: 20rpx;
color: #999;
margin-top: 4rpx;
text-align: center;
white-space: nowrap;
}
.selected-tags {
display: flex;
flex-wrap: wrap;
gap: 10rpx;
flex: 1;
align-items: flex-start;
padding-right: 60rpx;
.tag-item {
display: inline-flex;
align-items: center;
background: #f6f8fa;
color: #1d1e25;
padding: 8rpx 16rpx;
border-radius: 20rpx;
font-size: 22rpx;
cursor: pointer;
// padding: 8rpx 16rpx;
padding-left: 32rpx;
border-radius: 30rpx;
height: 60rpx;
font-size: 28rpx;
// cursor: pointer;
position: relative;
transition: all 0.3s ease;
border: none;
/* 为删除按钮预留空间 */
padding-right: 36rpx;
flex-shrink: 0;
&.active {
background: #fef7f2;
......@@ -3278,7 +3407,8 @@ function loadDefaultFoodsData() {
}
.tag {
font-size: 22rpx;
font-size: 28rpx;
text-align: center;
transition: all 0.3s ease;
/* 文字不被截断,根据内容自适应宽度 */
white-space: nowrap;
......@@ -3290,24 +3420,32 @@ function loadDefaultFoodsData() {
top: -5rpx;
width: 28rpx;
height: 28rpx;
cursor: pointer;
// cursor: pointer;
z-index: 1;
}
}
.expand-btn {
display: flex;
align-items: center;
justify-content: center;
width: 40rpx;
height: 40rpx;
cursor: pointer;
margin-left: 10rpx;
.expand-icon {
width: 24rpx;
height: 24rpx;
}
}
.expand-btn {
display: flex;
align-items: center;
justify-content: center;
width: 40rpx;
height: 40rpx;
// cursor: pointer;
margin-left: 10rpx;
flex-shrink: 0;
align-self: flex-start;
position: absolute;
right: 20rpx;
top: 0;
z-index: 10;
.expand-icon {
width: 24rpx;
height: 24rpx;
}
}
}
......@@ -3357,6 +3495,9 @@ function loadDefaultFoodsData() {
color: #1d1e25;
box-sizing: border-box;
background: white;
outline: none;
line-height: 70rpx;
/* 关键:设置为 height - padding*2 的值,或与 height 一致 */
}
.char-count {
......@@ -3380,16 +3521,15 @@ function loadDefaultFoodsData() {
.confirm-btn {
width: 284rpx;
height: 94rpx;
}
.cancel-btn {
// background: #f6f8fa;
// color: #dcb477;
transition: all 0.3s ease;
}
.confirm-btn {
// background: #dcb477;
// color: white;
&.disabled {
opacity: 0.5;
cursor: not-allowed;
pointer-events: none;
}
}
}
}
......@@ -3416,6 +3556,7 @@ function loadDefaultFoodsData() {
}
.success-content {
margin-top: 190rpx;
position: relative;
z-index: 2;
display: flex;
......@@ -3441,17 +3582,9 @@ function loadDefaultFoodsData() {
.success-btn {
width: 284rpx;
height: 94rpx;
cursor: pointer;
pointer-events: auto;
}
.success-btn-jump {
// 跳转按钮样式
}
.success-btn-close {
// 关闭按钮样式
}
}
}
}
......@@ -3687,6 +3820,7 @@ function loadDefaultFoodsData() {
width: 100%;
height: 1008rpx;
z-index: 1;
background-color: #fff;
.bottom-bg {
position: absolute;
......@@ -3698,45 +3832,37 @@ function loadDefaultFoodsData() {
}
.bottom_complete-btn {
position: absolute;
left: 50%;
transform: translateX(-50%);
bottom: 100rpx;
width: 686rpx;
height: 94rpx;
display: flex;
align-items: center;
justify-content: center;
z-index: 2;
cursor: pointer;
transition: all 0.3s ease;
/* 移除点击高亮效果 */
-webkit-tap-highlight-color: transparent;
-webkit-touch-callout: none;
-webkit-user-select: none;
user-select: none;
&.disabled {
opacity: 0.6;
pointer-events: none;
}
}
.complete-btn-bg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.bottom_complete-btn {
position: absolute;
left: 50%;
transform: translateX(-50%);
bottom: 100rpx;
width: 686rpx;
height: 94rpx;
display: flex;
align-items: center;
justify-content: center;
z-index: 2;
// cursor: pointer;
transition: all 0.3s ease;
/* 移除点击高亮效果 */
-webkit-tap-highlight-color: transparent;
-webkit-touch-callout: none;
-webkit-user-select: none;
user-select: none;
.complete-btn-text {
position: relative;
z-index: 1;
font-size: 32rpx;
color: white;
font-weight: bold;
}
&.disabled {
opacity: 0.6;
pointer-events: none;
}
.complete-btn-bg {
position: absolute;
width: 686rpx;
height: 94rpx;
}
}
......@@ -3837,7 +3963,7 @@ function loadDefaultFoodsData() {
padding: 25rpx;
border-radius: 12rpx;
font-size: 28rpx;
cursor: pointer;
// cursor: pointer;
transition: all 0.3s ease;
/* 移除点击高亮效果 */
-webkit-tap-highlight-color: transparent;
......@@ -3882,4 +4008,10 @@ function loadDefaultFoodsData() {
white-space: nowrap;
z-index: 10;
}
.food-input::placeholder {
color: #bbb;
line-height: 70rpx;
/* 或 110rpx,和上面保持一致 */
}
</style>
\ No newline at end of file
......@@ -148,36 +148,10 @@
<image :src="feedingRecordRes.add_btn" class="add-btn-img" />
</view>
<!-- 添加记录弹窗 -->
<view class="popup-mask" v-if="showPopup" @click="closePopup">
<view class="popup-content" @click.stop>
<view class="popup-title">添加喂养记录</view>
<view class="form-item">
<text class="label">时间:</text>
<picker mode="time" :value="newRecord.time" @change="onTimeChange">
<view class="picker">{{ newRecord.time || '请选择时间' }}</view>
</picker>
</view>
<view class="form-item">
<text class="label">类型:</text>
<picker :range="feedingTypes" @change="onTypeChange">
<view class="picker">{{ newRecord.type || '请选择类型' }}</view>
</picker>
</view>
<view class="form-item">
<text class="label">内容:</text>
<input class="input" v-model="newRecord.content" placeholder="请输入具体内容" maxlength="50" />
</view>
<view class="popup-buttons">
<button class="cancel-btn" @click="closePopup">取消</button>
<button class="confirm-btn" @click="saveRecord">保存</button>
</view>
</view>
</view>
<!-- 编辑弹窗 -->
<view class="popup-mask" v-if="showEditPopup" @click="closeEditPopup">
<view class="popup-content" @click.stop>
<view class="popup-mask" v-if="showEditPopup" @click="closeEditPopup" @touchmove.prevent>
<view class="popup-content" @click.stop @touchmove.stop>
<view class="popup-title">修改喂养记录</view>
<view class="form-item">
<text class="label">时间:</text>
......@@ -193,12 +167,14 @@
</view>
<view class="form-item">
<text class="label">喂养详情:</text>
<input class="input" v-model="editForm.content" placeholder="请输入具体内容" maxlength="20" />
<text class="char-count">{{ editForm.content.length }}/20</text>
<view class="input-container">
<input class="input" v-model="editForm.content" placeholder="请输入具体内容" maxlength="20" />
<text class="char-count">{{ editForm.content.length }}/20</text>
</view>
</view>
<view class="popup-buttons">
<button class="cancel-btn" @click="closeEditPopup">取消</button>
<button class="confirm-btn" @click="saveEditRecord">保存</button>
<button class="confirm-btn" @click="saveEditRecord">保存修改</button>
</view>
</view>
</view>
......@@ -392,6 +368,10 @@ const calendarDates = computed(() => {
return dates
})
function goToFeedingIndex() {
uni.navigateBack()
}
// 方法
function createDateObject(date) {
const today = new Date()
......@@ -622,7 +602,7 @@ function editRecord(index) {
const record = todayRecords.value[index]
if (!record) return
editForm.value = {
time: record.time || '',
time: record.time || getCurrentTime(), // 使用记录的原始时间,如果没有则使用当前时间
type: record.type || '',
content: record.foodDetails || record.content || '' // 优先使用foodDetails
}
......@@ -1120,8 +1100,14 @@ function formatTimeFromTimestamp(timestamp) {
function formatDuration(seconds) {
if (!seconds || seconds <= 0) return ''
const minutes = Math.floor(seconds / 60)
return `${minutes}min`
const minutes = seconds / 60
if (minutes < 1) {
// 不足1分钟时显示小数,保留2位小数
return `${minutes.toFixed(2)}min`
} else {
// 1分钟以上时显示整数
return `${Math.floor(minutes)}min`
}
}
function getFeedingTypeLabel(type) {
......@@ -1149,7 +1135,18 @@ function formatDateTimeString(dateString, timeString) {
const [hours, minutes] = timeString.split(':');
date.setHours(parseInt(hours, 10));
date.setMinutes(parseInt(minutes, 10));
return date.toISOString(); // 使用ISO格式的时间戳
date.setSeconds(0);
date.setMilliseconds(0);
// 使用本地时间而不是UTC时间,避免时区问题
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hour = String(date.getHours()).padStart(2, '0');
const minute = String(date.getMinutes()).padStart(2, '0');
const second = String(date.getSeconds()).padStart(2, '0');
return `${year}-${month}-${day}T${hour}:${minute}:${second}`;
}
function loadBabyFeedingRecords(baby) {
......@@ -1910,6 +1907,8 @@ function testApiIntegration() {
z-index: 1000;
display: flex;
align-items: flex-end;
overflow: hidden;
touch-action: none;
}
.popup-content {
......@@ -1939,6 +1938,11 @@ function testApiIntegration() {
display: block;
}
.input-container {
position: relative;
width: 100%;
}
.picker,
.input {
width: 100%;
......@@ -1946,11 +1950,27 @@ function testApiIntegration() {
background: #f8f9fa;
border-radius: 12rpx;
font-size: 28rpx;
min-height: 88rpx;
color: #333;
border: 1rpx solid #e9ecef;
box-sizing: border-box;
}
.char-count {
position: absolute;
right: 20rpx;
top: 50%;
transform: translateY(-50%);
font-size: 24rpx;
color: #999;
pointer-events: none;
z-index: 1;
background: rgba(248, 249, 250, 0.8);
padding: 2rpx 8rpx;
border-radius: 4rpx;
}
.picker {
display: flex;
align-items: center;
......@@ -1966,7 +1986,7 @@ function testApiIntegration() {
button {
flex: 1;
height: 88rpx;
border-radius: 12rpx;
border-radius: 50rpx;
font-size: 32rpx;
border: none;
font-weight: 600;
......
......@@ -95,6 +95,11 @@ import {
getDelete
} from '../../api/obstetric.js';
import md from '../../md';
import { useUserStore } from "@/stores/user";
// 获取用户信息
const userStore = useUserStore();
const babyId = ref(userStore.babyInfo?.content?.id)
// 控制查看更多图片
const showMore = ref(false)
......@@ -166,8 +171,8 @@ const getDeleteFn = async (id) => {
}
// 获取报告单
const getReportListFn = async () => {
console.log('获取报告单')
const { code, message, data, success } = await getReportList()
const { code, message, data, success } = await getReportList({babyId: babyId.value})
console.log('报告单列表', code, message, data, success)
if (success) {
listData.value = data
} else {
......
......@@ -107,6 +107,7 @@ import {
throttleTap
} from '@/utils/index.js';
import md from '../../md';
import { useUserStore } from "@/stores/user";
import {
getInfo,
getUpdate,
......@@ -118,6 +119,10 @@ const {
proxy
} = getCurrentInstance();
const $baseUrl = proxy.$baseUrl;
const userStore = useUserStore();
const babyId = ref(userStore.babyInfo?.content?.id)
const back_btn = ref('')
// 时间弹窗控制
......@@ -165,7 +170,7 @@ const getSrcUrl = (status) => {
const imageMap = {
'pending': `${$baseUrl}chanjianTool/1001/icon4.png`,
'expired': `${$baseUrl}chanjianTool/1001/icon5.png`,
'completed': `${$baseUrl}chanjianTool/1001/icon6.png`
'completed': `${$baseUrl}chanjianTool/1001/icon30.png`
};
return imageMap[status];
......@@ -269,11 +274,12 @@ const onEditTime = async () => {
}
// 获取信息接口
const getInfoFn = async () => {
console.log('获取信息')
// 获取信息
const { code, success, message, data } = await getInfo()
const { code, success, message, data } = await getInfo({babyId: babyId.value})
console.log('产检提醒首页获取信息', code, success, message, data)
if (success) {
homeInfo.value = data
uni.setStorageSync('dueDate', data.dueDate);
} else {
uni.showToast({
......
......@@ -24,11 +24,11 @@
<image @tap="backHandler" class="btnback" :src="`${$baseUrl}chanjianTool/1001/back.png`"></image>
<view class="page_title">
<view class="info-l">
<image :src="$baseUrl + 'common/default_avatar.png'"></image>
<image :src="babyInfo?.content?.babyAvatar || $baseUrl + 'common/default_avatar.png'"></image>
</view>
<view class="info-r">
<view class="info-r-t">
</view>
<view class="info-r-b">
怀孕{{ info.gestationalWeeks }}
......@@ -88,7 +88,7 @@
<text class="empty-text">当日暂无记录</text>
</view>
<view class="con-list-item" v-for="(item, index) in todayRecords" :key="index"
@click="onDetails(item.id)">
@click="onDetails(item)">
<view class="item-time">
<view class="">
第{{ item.index }}次产检
......@@ -122,6 +122,7 @@ import {
onLoad,
onShow
} from '@dcloudio/uni-app'
import { useUserStore } from "@/stores/user";
import {
throttleTap,
dateFormatter
......@@ -130,7 +131,10 @@ import {
import {
getInfo
} from '../../api/obstetric.js';
import { console } from 'inspector';
// 获取用户信息
const userStore = useUserStore();
const babyInfo =ref(userStore?.babyInfo || {})
// 新增日历组件 start-------
const currentSelectedDate = ref('')
const currentDate = ref(new Date())
......@@ -389,7 +393,10 @@ const getProject = (projects) => {
}
// 跳转产检详情页面
const onDetails = (id) => {
const onDetails = (item) => {
const { id, type } = item
// 如果是新增的就不跳转
if(type === '1') return
uni.navigateTo({
url: `/pages/productionDetails/productionDetails?id=${id}`
})
......@@ -397,11 +404,18 @@ const onDetails = (id) => {
// 获取信息接口
const getInfoFn = async () => {
console.log('获取信息')
// 获取信息
const {code,success, message, data } = await getInfo()
const {code,success, message, data } = await getInfo({babyId: babyInfo.value?.content?.id})
console.log('日历页面获取信息', code, success, message, data)
if (success) {
info.value = data
// 保留 data 里面 checkupList 里面 status 等于 completed 的数据
info.value = {
...data,
checkupList: data.checkupList.filter(item => item.status === 'completed')
}
console.log('日历获取数据:', info.value)
} else {
uni.showToast({
title: message,
......
......@@ -27,9 +27,10 @@
<image v-else :src="`${$baseUrl}chanjianTool/1001/icon25.png`"></image>
完成检查
</view>
<view class="r-remind" @click="onRemind">
<!-- 提醒按钮 -->
<!-- <view class="r-remind" @click="onRemind">
<image :src="`${$baseUrl}chanjianTool/1001/icon26.png`"></image>
</view>
</view> -->
</view>
</view>
......@@ -57,8 +58,8 @@
</view>
<view class="project-content">
<rich-text class="desc" :nodes="infoData.content"></rich-text>
<!-- <rich-text class="desc" :nodes="infoData.content"></rich-text> -->
<mp-html :content="infoData.content" ></mp-html>
</view>
</view>
<!-- 产检项目 -->
......@@ -168,7 +169,8 @@ import { uploadImage } from '../../api/common.js'
import {
getDetail,
getUpdate,
getWxNotification
getWxNotification,
getDeleteReportImg
} from '../../api/obstetric.js';
// 提醒选择器相关状态
......@@ -341,9 +343,9 @@ const onImageDel = (id) => {
// 直接保存
const params = {
id: editId.value,
reportImages: bgdImgList.value
keepImages: bgdImgList.value
}
onEditTime(params)
getDeleteReportImgFn(params)
}
// 上传图片
const onUpload = throttleTap(() => {
......@@ -521,11 +523,25 @@ const onEditTime = async (params) => {
})
}
}
// 修改报告单
const getDeleteReportImgFn = async (params) => {
console.log(params)
const { code, success, message } = await getDeleteReportImg(params)
if (success) {
// 重新获取信息
getDetailFn(editId.value)
} else {
uni.showToast({
title: message,
icon: 'none'
})
}
}
// 获取详情接口
const getDetailFn = async (id) => {
console.log('获取信息', id)
// 获取信息
const {code, success, message, data } = await getDetail({id})
console.log('详情页面获取信息', code, success, message, data)
if (success) {
infoData.value = data
bgdImgList.value = data.reportImages == null ? [] : data.reportImages
......
## 为减小组件包的大小,默认组件包中不包含编辑、latex 公式等扩展功能,需要使用扩展功能的请参考下方的 插件扩展 栏的说明
## 功能介绍
- 全端支持(含 `v3、NVUE`
- 支持丰富的标签(包括 `table``video``svg` 等)
- 支持丰富的事件效果(自动预览图片、链接处理等)
- 支持设置占位图(加载中、出错时、预览时)
- 支持锚点跳转、长按复制等丰富功能
- 支持大部分 *html* 实体
- 丰富的插件(关键词搜索、内容编辑、`latex` 公式等)
- 效率高、容错性强且轻量化
查看 [功能介绍](https://jin-yufeng.github.io/mp-html/#/overview/feature) 了解更多
## 使用方法
- `uni_modules` 方式
1. 点击右上角的 `使用 HBuilder X 导入插件` 按钮直接导入项目或点击 `下载插件 ZIP` 按钮下载插件包并解压到项目的 `uni_modules/mp-html` 目录下
2. 在需要使用页面的 `(n)vue` 文件中添加
```html
<!-- 不需要引入,可直接使用 -->
<mp-html :content="html" />
```
```javascript
export default {
data() {
return {
html: '<div>Hello World!</div>'
}
}
}
```
3. 需要更新版本时在 `HBuilder X` 中右键 `uni_modules/mp-html` 目录选择 `从插件市场更新` 即可
- 源码方式
1.[github](https://github.com/jin-yufeng/mp-html/tree/master/dist/uni-app)[gitee](https://gitee.com/jin-yufeng/mp-html/tree/master/dist/uni-app) 下载源码
插件市场的 **非 uni_modules 版本** 无法更新,不建议从插件市场获取
2. 在需要使用页面的 `(n)vue` 文件中添加
```html
<mp-html :content="html" />
```
```javascript
import mpHtml from '@/components/mp-html/mp-html'
export default {
// HBuilderX 2.5.5+ 可以通过 easycom 自动引入
components: {
mpHtml
},
data() {
return {
html: '<div>Hello World!</div>'
}
}
}
```
- npm 方式
1. 在项目根目录下执行
```bash
npm install mp-html
```
2. 在需要使用页面的 `(n)vue` 文件中添加
```html
<mp-html :content="html" />
```
```javascript
import mpHtml from 'mp-html/dist/uni-app/components/mp-html/mp-html'
export default {
// 不可省略
components: {
mpHtml
},
data() {
return {
html: '<div>Hello World!</div>'
}
}
}
```
3. 需要更新版本时执行以下命令即可
```bash
npm update mp-html
```
使用 *cli* 方式运行的项目,通过 *npm* 方式引入时,需要在 *vue.config.js* 中配置 *transpileDependencies*,详情可见 [#330](https://github.com/jin-yufeng/mp-html/issues/330#issuecomment-913617687)
如果在 **nvue** 中使用还要将 `dist/uni-app/static` 目录下的内容拷贝到项目的 `static` 目录下,否则无法运行
查看 [快速开始](https://jin-yufeng.github.io/mp-html/#/overview/quickstart) 了解更多
## 组件属性
| 属性 | 类型 | 默认值 | 说明 |
|:---:|:---:|:---:|---|
| container-style | String | | 容器的样式([2.1.0+](https://jin-yufeng.github.io/mp-html/#/changelog/changelog#v210)) |
| content | String | | 用于渲染的 html 字符串 |
| copy-link | Boolean | true | 是否允许外部链接被点击时自动复制 |
| domain | String | | 主域名(用于链接拼接) |
| error-img | String | | 图片出错时的占位图链接 |
| lazy-load | Boolean | false | 是否开启图片懒加载 |
| loading-img | String | | 图片加载过程中的占位图链接 |
| pause-video | Boolean | true | 是否在播放一个视频时自动暂停其他视频 |
| preview-img | Boolean | true | 是否允许图片被点击时自动预览 |
| scroll-table | Boolean | false | 是否给每个表格添加一个滚动层使其能单独横向滚动 |
| selectable | Boolean | false | 是否开启文本长按复制 |
| set-title | Boolean | true | 是否将 title 标签的内容设置到页面标题 |
| show-img-menu | Boolean | true | 是否允许图片被长按时显示菜单 |
| tag-style | Object | | 设置标签的默认样式 |
| use-anchor | Boolean | false | 是否使用锚点链接 |
查看 [属性](https://jin-yufeng.github.io/mp-html/#/basic/prop) 了解更多
## 组件事件
| 名称 | 触发时机 |
|:---:|---|
| load | dom 树加载完毕时 |
| ready | 图片加载完毕时 |
| error | 发生渲染错误时 |
| imgtap | 图片被点击时 |
| linktap | 链接被点击时 |
| play | 音视频播放时 |
查看 [事件](https://jin-yufeng.github.io/mp-html/#/basic/event) 了解更多
## api
组件实例上提供了一些 `api` 方法可供调用
| 名称 | 作用 |
|:---:|---|
| in | 将锚点跳转的范围限定在一个 scroll-view 内 |
| navigateTo | 锚点跳转 |
| getText | 获取文本内容 |
| getRect | 获取富文本内容的位置和大小 |
| setContent | 设置富文本内容 |
| imgList | 获取所有图片的数组 |
| pauseMedia | 暂停播放音视频([2.2.2+](https://jin-yufeng.github.io/mp-html/#/changelog/changelog#v222)) |
| setPlaybackRate | 设置音视频播放速率([2.4.0+](https://jin-yufeng.github.io/mp-html/#/changelog/changelog#v240)) |
查看 [api](https://jin-yufeng.github.io/mp-html/#/advanced/api) 了解更多
## 插件扩展
除基本功能外,本组件还提供了丰富的扩展,可按照需要选用
| 名称 | 作用 |
|:---:|---|
| audio | 音乐播放器 |
| editable | 富文本 **编辑**[示例项目](https://mp-html.oss-cn-hangzhou.aliyuncs.com/editable.zip)) |
| emoji | 解析 emoji |
| highlight | 代码块高亮显示 |
| markdown | 渲染 markdown |
| search | 关键词搜索 |
| style | 匹配 style 标签中的样式 |
| txv-video | 使用腾讯视频 |
| img-cache | 图片缓存 by [@PentaTea](https://github.com/PentaTea) |
| latex | 渲染 latex 公式 by [@Zeng-J](https://github.com/Zeng-J) |
从插件市场导入的包中 **不含有** 扩展插件,使用插件需通过微信小程序 `富文本插件` 获取或参考以下方法进行打包:
1. 获取完整组件包
```bash
npm install mp-html
```
2. 编辑 `tools/config.js` 中的 `plugins` 项,选择需要的插件
3. 生成新的组件包
`node_modules/mp-html` 目录下执行
```bash
npm install
npm run build:uni-app
```
4. 拷贝 `dist/uni-app` 中的内容到项目根目录
查看 [插件](https://jin-yufeng.github.io/mp-html/#/advanced/plugin) 了解更多
## 关于 nvue
`nvue` 使用原生渲染,不支持部分 `css` 样式,为实现和 `html` 相同的效果,组件内部通过 `web-view` 进行渲染,性能上差于原生,根据 `weex` 官方建议,`web` 标签仅应用在非常规的降级场景。因此,如果通过原生的方式(如 `richtext`)能够满足需要,则不建议使用本组件,如果有较多的富文本内容,则可以直接使用 `vue` 页面
由于渲染方式与其他端不同,有以下限制:
1. 不支持 `lazy-load` 属性
2. 视频不支持全屏播放
3. 如果在 `flex-direction: row` 的容器中使用,需要给组件设置宽度或设置 `flex: 1` 占满剩余宽度
`nvue` 模式下,[此问题](https://ask.dcloud.net.cn/question/119678) 修复前,不支持通过 `uni_modules` 引入,需要本地引入(将 [dist/uni-app](https://github.com/jin-yufeng/mp-html/tree/master/dist/uni-app) 中的内容拷贝到项目根目录下)
## 问题反馈
遇到问题时,请先查阅 [常见问题](https://jin-yufeng.github.io/mp-html/#/question/faq)[issue](https://github.com/jin-yufeng/mp-html/issues) 中是否已有相同的问题
可通过 [issue](https://github.com/jin-yufeng/mp-html/issues/new/choose) 、插件问答或发送邮件到 [mp_html@126.com](mailto:mp_html@126.com) 提问,不建议在评论区提问(不方便回复)
提问请严格按照 [issue 模板](https://github.com/jin-yufeng/mp-html/issues/new/choose) ,描述清楚使用环境、`html` 内容或可复现的 `demo` 项目以及复现方式,对于 **描述不清****无法复现** 或重复的问题将不予回复
欢迎加入 `QQ` 交流群:
群1(已满):`699734691`
群2(已满):`778239129`
群3:`960265313`
查看 [问题反馈](https://jin-yufeng.github.io/mp-html/#/question/feedback) 了解更多
## v2.5.1(2025-04-20)
1. `U` 适配鸿蒙 `APP` [详细](https://github.com/jin-yufeng/mp-html/issues/615)
2. `U` 微信小程序替换废弃 `api` `getSystemInfoSync` [详细](https://github.com/jin-yufeng/mp-html/issues/613)
3. `F` 修复了 `app` 端播放视频可能报错的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/617)
4. `F` 修复了 `latex` 插件可能出现 `xxx can be used only in display mode` 的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/632)
5. `F` 修复了 `uni-app``latex` 公式可能不显示的问题 [#599](https://github.com/jin-yufeng/mp-html/issues/599)[#627](https://github.com/jin-yufeng/mp-html/issues/627)
## v2.5.0(2024-04-22)
1. `U` `play` 事件增加返回 `src` 等信息 [详细](https://github.com/jin-yufeng/mp-html/issues/526)
2. `U` `preview-img` 属性支持设置为 `all` 开启 `base64` 图片预览 [详细](https://github.com/jin-yufeng/mp-html/issues/536)
3. `U` `editable` 插件增加简易模式(点击文字直接编辑)
4. `U` `latex` 插件支持块级公式 [详细](https://github.com/jin-yufeng/mp-html/issues/582)
5. `F` 修复了表格部分情况下背景丢失的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/587)
6. `F` 修复了部分 `svg` 无法显示的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/591)
7. `F` 修复了 `h5``app` 端部分情况下样式无法识别的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/518)
8. `F` 修复了 `latex` 插件部分情况下显示不正确的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/580)
9. `F` 修复了 `editable` 插件表格无法删除的问题
10. `F` 修复了 `editable` 插件 `vue3` `h5` 端点击图片报错的问题
11. `F` 修复了 `editable` 插件点击表格没有菜单栏的问题
## v2.4.3(2024-01-21)
1. `A` 增加 [card](https://jin-yufeng.gitee.io/mp-html/#/advanced/plugin#card) 插件 [详细](https://github.com/jin-yufeng/mp-html/pull/533) by [@whoooami](https://github.com/whoooami)
2. `F` 修复了 `svg` 中包含 `foreignobject` 可能不显示的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/523)
3. `F` 修复了合并单元格的表格部分情况下显示不正确的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/561)
4. `F` 修复了 `img` 标签设置 `object-fit` 无效的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/567)
5. `F` 修复了 `latex` 插件公式会换行的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/540)
6. `F` 修复了 `editable``audio` 插件共用时点击 `audio` 无法编辑的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/529) by [@whoooami](https://github.com/whoooami)
7. `F` 修复了微信小程序部分情况下图片会报错 `replace of undefined` 的问题
8. `F` 修复了快手小程序图片不显示的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/571)
## v2.4.2(2023-05-14)
1. `A` `editable` 插件支持修改文字颜色 [详细](https://github.com/jin-yufeng/mp-html/issues/254)
2. `F` 修复了 `svg` 中有 `style` 不生效的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/505)
3. `F` 修复了使用旧版编译器可能报错 `Bad attr nodes` 的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/472)
4. `F` 修复了 `app` 端可能出现无法读取 `lazyLoad` 的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/513)
5. `F` 修复了 `editable` 插件在点击换图时未拼接 `domain` 的问题 [详细](https://github.com/jin-yufeng/mp-html/pull/497) by [@TwoKe945](https://github.com/TwoKe945)
6. `F` 修复了 `latex` 插件部分情况下不显示的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/515)
7. `F` 修复了 `editable` 插件点击音视频时其他标签框不消失的问题
## v2.4.1(2022-12-25)
1. `F` 修复了没有图片时 `ready` 事件可能不触发的问题
2. `F` 修复了加载过程中可能出现 `Root label not found` 错误的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/470)
3. `F` 修复了 `audio` 插件退出页面可能会报错的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/457)
4. `F` 修复了 `vue3` 运行到 `app``HBuilder X 3.6.10` 以上报错的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/480)
5. `F` 修复了 `nvue` 端链接中包含 `%22` 时可能无法显示的问题
6. `F` 修复了 `vue3` 使用 `highlight` 插件可能报错的问题
## v2.4.0(2022-08-27)
1. `A` 增加了 [setPlaybackRate](https://jin-yufeng.gitee.io/mp-html/#/advanced/api#setPlaybackRate)`api`,可以设置音视频的播放速率 [详细](https://github.com/jin-yufeng/mp-html/issues/452)
2. `A` 示例小程序代码开源 [详细](https://github.com/jin-yufeng/mp-html-demo)
3. `U` 优化 `ready` 事件触发时机,未设置懒加载的情况下基本可以准确触发 [详细](https://github.com/jin-yufeng/mp-html/issues/195)
4. `U` `highlight` 插件在编辑状态下不进行高亮处理,便于编辑
5. `F` 修复了 `flex` 布局下图片大小可能不正确的问题
6. `F` 修复了 `selectable` 属性没有设置 `force` 也可能出现渲染异常的问题
7. `F` 修复了表格中的图片大小可能不正确的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/448)
8. `F` 修复了含有合并单元格的表格可能无法设置竖直对齐的问题
9. `F` 修复了 `editable` 插件在 `scroll-view` 中使用时工具条位置可能不正确的问题
10. `F` 修复了 `vue3` 使用 [search](advanced/plugin#search) 插件可能导致错误换行的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/449)
## v2.3.2(2022-08-13)
1. `A` 增加 [latex](https://jin-yufeng.gitee.io/mp-html/#/advanced/plugin#latex) 插件,可以渲染数学公式 [详细](https://github.com/jin-yufeng/mp-html/pull/447) by [@Zeng-J](https://github.com/Zeng-J)
2. `U` 优化根节点下有很多标签的长内容渲染速度
3. `U` `highlight` 插件适配 `lang-xxx` 格式
4. `F` 修复了 `table` 标签设置 `border` 属性后可能无法修改边框样式的问题 [详细](https://github.com/jin-yufeng/mp-html/pull/439) by [@zouxingjie](https://github.com/zouxingjie)
5. `F` 修复了 `editable` 插件输入连续空格无效的问题
6. `F` 修复了 `vue3` 图片设置 `inline` 会报错的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/438)
7. `F` 修复了 `vue3` 使用 `table` 可能报错的问题
## v2.3.1(2022-05-20)
1. `U` `app` 端支持使用本地图片
2. `U` 优化了微信小程序 `selectable` 属性在 `ios` 端的处理 [详细](https://jin-yufeng.gitee.io/mp-html/#/basic/prop#selectable)
3. `F` 修复了 `editable` 插件不在顶部时 `tooltip` 位置可能错误的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/430)
4. `F` 修复了 `vue3` 运行到微信小程序可能报错丢失内容的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/414)
5. `F` 修复了 `vue3` 部分标签可能被错误换行的问题
6. `F` 修复了 `editable` 插件 `app` 端插入视频无法预览的问题
## v2.3.0(2022-04-01)
1. `A` 增加了 `play` 事件,音视频播放时触发,可用于与页面其他音视频进行互斥播放 [详细](basic/event#play)
2. `U` `show-img-menu` 属性支持控制预览时是否长按弹出菜单
3. `U` 优化 `wxs` 处理,提高渲染性能 [详细](https://developers.weixin.qq.com/community/develop/article/doc/0006cc2b204740f601bd43fa25a413)
4. `U` `video` 标签支持 `object-fit` 属性
5. `U` 增加支持一些常用实体编码 [详细](https://github.com/jin-yufeng/mp-html/issues/418)
6. `F` 修复了图片仅设置高度可能不显示的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/410)
7. `F` 修复了 `video` 标签高度设置为 `auto` 不显示的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/411)
8. `F` 修复了使用 `grid` 布局时可能样式错误的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/413)
9. `F` 修复了含有合并单元格的表格部分情况下显示异常的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/417)
10. `F` 修复了 `editable` 插件连续插入内容时顺序不正确的问题
11. `F` 修复了 `uni-app``vue3` 使用 `audio` 插件报错的问题
12. `F` 修复了 `uni-app``highlight` 插件使用自定义的 `prism.min.js` 报错的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/416)
## v2.2.2(2022-02-26)
1. `A` 增加了 [pauseMedia](https://jin-yufeng.gitee.io/mp-html/#/advanced/api#pauseMedia)`api`,可用于暂停播放音视频 [详细](https://github.com/jin-yufeng/mp-html/issues/317)
2. `U` 优化了长内容的加载速度
3. `U` 适配 `vue3` [#389](https://github.com/jin-yufeng/mp-html/issues/389)[#398](https://github.com/jin-yufeng/mp-html/pull/398) by [@zhouhuafei](https://github.com/zhouhuafei)[#400](https://github.com/jin-yufeng/mp-html/issues/400)
4. `F` 修复了小程序端图片高度设置为百分比时可能不显示的问题
5. `F` 修复了 `highlight` 插件部分情况下可能显示不完整的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/403)
## v2.2.1(2021-12-24)
1. `A` `editable` 插件增加上下移动标签功能
2. `U` `editable` 插件支持在文本中间光标处插入内容
3. `F` 修复了 `nvue` 端设置 `margin` 后可能导致高度不正确的问题
4. `F` 修复了 `highlight` 插件使用压缩版的 `prism.css` 可能导致背景失效的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/367)
5. `F` 修复了编辑状态下使用 `emoji` 插件内容为空时可能报错的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/371)
6. `F` 修复了使用 `editable` 插件后将 `selectable` 属性设置为 `force` 不生效的问题
## v2.2.0(2021-10-12)
1. `A` 增加 `customElements` 配置项,便于添加自定义功能性标签 [详细](https://github.com/jin-yufeng/mp-html/issues/350)
2. `A` `editable` 插件增加切换音视频自动播放状态的功能 [详细](https://github.com/jin-yufeng/mp-html/pull/341) by [@leeseett](https://github.com/leeseett)
3. `A` `editable` 插件删除媒体标签时触发 `remove` 事件,便于删除已上传的文件
4. `U` `editable` 插件 `insertImg` 方法支持同时插入多张图片 [详细](https://github.com/jin-yufeng/mp-html/issues/342)
5. `U` `editable` 插入图片和音视频时支持拼接 `domian` 主域名
6. `F` 修复了内部链接参数中包含 `://` 时被认为是外部链接的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/356)
7. `F` 修复了部分 `svg` 标签名或属性名大小写不正确时不生效的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/351)
8. `F` 修复了 `nvue` 页面运行到非 `app` 平台时可能样式错误的问题
## v2.1.5(2021-08-13)
1. `A` 增加支持标签的 `dir` 属性
2. `F` 修复了 `ruby` 标签文字与拼音没有居中对齐的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/325)
3. `F` 修复了音视频标签内有 `a` 标签时可能无法播放的问题
4. `F` 修复了 `externStyle` 中的 `class` 名包含下划线或数字时可能失效的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/326)
5. `F` 修复了 `h5` 端引入 `externStyle` 可能不生效的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/326)
## v2.1.4(2021-07-14)
1. `F` 修复了 `rt` 标签无法设置样式的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/318)
2. `F` 修复了表格中有单元格同时合并行和列时可能显示不正确的问题
3. `F` 修复了 `app` 端无法关闭图片长按菜单的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/322)
4. `F` 修复了 `editable` 插件只能添加图片链接不能修改的问题 [详细](https://github.com/jin-yufeng/mp-html/pull/312) by [@leeseett](https://github.com/leeseett)
## v2.1.3(2021-06-12)
1. `A` `editable` 插件增加 `insertTable` 方法
2. `U` `editable` 插件支持编辑表格中的空白单元格 [详细](https://github.com/jin-yufeng/mp-html/issues/310)
3. `F` 修复了 `externStyle` 中使用伪类可能失效的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/298)
4. `F` 修复了多个组件同时使用时 `tag-style` 属性时可能互相影响的问题 [详细](https://github.com/jin-yufeng/mp-html/pull/305) by [@woodguoyu](https://github.com/woodguoyu)
5. `F` 修复了包含 `linearGradient``svg` 可能无法显示的问题
6. `F` 修复了编译到头条小程序时可能报错的问题
7. `F` 修复了 `nvue` 端不触发 `click` 事件的问题
8. `F` 修复了 `editable` 插件尾部插入时无法撤销的问题
9. `F` 修复了 `editable` 插件的 `insertHtml` 方法只能在末尾插入的问题
10. `F` 修复了 `editable` 插件插入音频不显示的问题
## v2.1.2(2021-04-24)
1. `A` 增加了 [img-cache](https://jin-yufeng.gitee.io/mp-html/#/advanced/plugin#img-cache) 插件,可以在 `app` 端缓存图片 [详细](https://github.com/jin-yufeng/mp-html/issues/292) by [@PentaTea](https://github.com/PentaTea)
2. `U` 支持通过 `container-style` 属性设置 `white-space` 来保留连续空格和换行符 [详细](https://jin-yufeng.gitee.io/mp-html/#/question/faq#space)
3. `U` 代码风格符合 [standard](https://standardjs.com) 标准
4. `U` `editable` 插件编辑状态下支持预览视频 [详细](https://github.com/jin-yufeng/mp-html/issues/286)
5. `F` 修复了 `svg` 标签内嵌 `svg` 时无法显示的问题
6. `F` 修复了编译到支付宝和头条小程序时部分区域不可复制的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/291)
## v2.1.1(2021-04-09)
1. 修复了对 `p` 标签设置 `tag-style` 可能不生效的问题
2. 修复了 `svg` 标签中的文本无法显示的问题
3. 修复了使用 `editable` 插件编辑表格时可能报错的问题
4. 修复了使用 `highlight` 插件运行到头条小程序时可能没有样式的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/280)
5. 修复了使用 `editable` 插件 `editable` 属性为 `false` 时会报错的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/284)
6. 修复了 `style` 插件连续子选择器失效的问题
7. 修复了 `editable` 插件无法修改图片和字体大小的问题
## v2.1.0.2(2021-03-21)
修复了 `nvue` 端使用可能报错的问题
## v2.1.0(2021-03-20)
1. `A` 增加了 [container-style](https://jin-yufeng.gitee.io/mp-html/#/basic/prop#container-style) 属性 [详细](https://gitee.com/jin-yufeng/mp-html/pulls/1)
2. `A` 增加支持 `strike` 标签
3. `A` `editable` 插件增加 `placeholder` 属性 [详细](https://jin-yufeng.gitee.io/mp-html/#/advanced/plugin#editable)
4. `A` `editable` 插件增加 `insertHtml` 方法 [详细](https://jin-yufeng.gitee.io/mp-html/#/advanced/plugin#editable)
5. `U` 外部样式支持标签名选择器 [详细](https://jin-yufeng.gitee.io/mp-html/#/overview/quickstart#setting)
6. `F` 修复了 `nvue` 端部分情况下可能不显示的问题
## v2.0.5(2021-03-12)
1. `U` [linktap](https://jin-yufeng.gitee.io/mp-html/#/basic/event#linktap) 事件增加返回内部文本内容 `innerText` [详细](https://github.com/jin-yufeng/mp-html/issues/271)
2. `U` [selectable](https://jin-yufeng.gitee.io/mp-html/#/basic/prop#selectable) 属性设置为 `force` 时能够在微信 `iOS` 端生效(文本块会变成 `inline-block`[详细](https://github.com/jin-yufeng/mp-html/issues/267)
3. `F` 修复了部分情况下竖向无法滚动的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/182)
4. `F` 修复了多次修改富文本数据时部分内容可能不显示的问题
5. `F` 修复了 [腾讯视频](https://jin-yufeng.gitee.io/mp-html/#/advanced/plugin#txv-video) 插件可能无法播放的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/265)
6. `F` 修复了 [highlight](https://jin-yufeng.gitee.io/mp-html/#/advanced/plugin#highlight) 插件没有设置高亮语言时没有应用默认样式的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/276) by [@fuzui](https://github.com/fuzui)
<template>
<view id="_root" :class="(selectable?'_select ':'')+'_root'" :style="containerStyle">
<slot v-if="!nodes[0]" />
<!-- #ifndef APP-PLUS-NVUE -->
<node v-else :childs="nodes" :opts="[lazyLoad,loadingImg,errorImg,showImgMenu,selectable]" name="span" />
<!-- #endif -->
<!-- #ifdef APP-PLUS-NVUE -->
<web-view ref="web" src="/uni_modules/mp-html/static/app-plus/mp-html/local.html" :style="'margin-top:-2px;height:' + height + 'px'" @onPostMessage="_onMessage" />
<!-- #endif -->
</view>
</template>
<script>
/**
* mp-html v2.5.1
* @description 富文本组件
* @tutorial https://github.com/jin-yufeng/mp-html
* @property {String} container-style 容器的样式
* @property {String} content 用于渲染的 html 字符串
* @property {Boolean} copy-link 是否允许外部链接被点击时自动复制
* @property {String} domain 主域名,用于拼接链接
* @property {String} error-img 图片出错时的占位图链接
* @property {Boolean} lazy-load 是否开启图片懒加载
* @property {string} loading-img 图片加载过程中的占位图链接
* @property {Boolean} pause-video 是否在播放一个视频时自动暂停其他视频
* @property {Boolean} preview-img 是否允许图片被点击时自动预览
* @property {Boolean} scroll-table 是否给每个表格添加一个滚动层使其能单独横向滚动
* @property {Boolean | String} selectable 是否开启长按复制
* @property {Boolean} set-title 是否将 title 标签的内容设置到页面标题
* @property {Boolean} show-img-menu 是否允许图片被长按时显示菜单
* @property {Object} tag-style 标签的默认样式
* @property {Boolean | Number} use-anchor 是否使用锚点链接
* @event {Function} load dom 结构加载完毕时触发
* @event {Function} ready 所有图片加载完毕时触发
* @event {Function} imgtap 图片被点击时触发
* @event {Function} linktap 链接被点击时触发
* @event {Function} play 音视频播放时触发
* @event {Function} error 媒体加载出错时触发
*/
// #ifndef APP-PLUS-NVUE
import node from './node/node'
// #endif
import Parser from './parser'
const plugins=[]
// #ifdef APP-PLUS-NVUE
const dom = weex.requireModule('dom')
// #endif
export default {
name: 'mp-html',
data () {
return {
nodes: [],
// #ifdef APP-PLUS-NVUE
height: 3
// #endif
}
},
props: {
containerStyle: {
type: String,
default: ''
},
content: {
type: String,
default: ''
},
copyLink: {
type: [Boolean, String],
default: true
},
domain: String,
errorImg: {
type: String,
default: ''
},
lazyLoad: {
type: [Boolean, String],
default: false
},
loadingImg: {
type: String,
default: ''
},
pauseVideo: {
type: [Boolean, String],
default: true
},
previewImg: {
type: [Boolean, String],
default: true
},
scrollTable: [Boolean, String],
selectable: [Boolean, String],
setTitle: {
type: [Boolean, String],
default: true
},
showImgMenu: {
type: [Boolean, String],
default: true
},
tagStyle: Object,
useAnchor: [Boolean, Number]
},
// #ifdef VUE3
emits: ['load', 'ready', 'imgtap', 'linktap', 'play', 'error'],
// #endif
// #ifndef APP-PLUS-NVUE
components: {
node
},
// #endif
watch: {
content (content) {
this.setContent(content)
}
},
created () {
this.plugins = []
for (let i = plugins.length; i--;) {
this.plugins.push(new plugins[i](this))
}
},
mounted () {
if (this.content && !this.nodes.length) {
this.setContent(this.content)
}
},
beforeDestroy () {
this._hook('onDetached')
},
methods: {
/**
* @description 将锚点跳转的范围限定在一个 scroll-view 内
* @param {Object} page scroll-view 所在页面的示例
* @param {String} selector scroll-view 的选择器
* @param {String} scrollTop scroll-view scroll-top 属性绑定的变量名
*/
in (page, selector, scrollTop) {
// #ifndef APP-PLUS-NVUE
if (page && selector && scrollTop) {
this._in = {
page,
selector,
scrollTop
}
}
// #endif
},
/**
* @description 锚点跳转
* @param {String} id 要跳转的锚点 id
* @param {Number} offset 跳转位置的偏移量
* @returns {Promise}
*/
navigateTo (id, offset) {
return new Promise((resolve, reject) => {
if (!this.useAnchor) {
reject(Error('Anchor is disabled'))
return
}
offset = offset || parseInt(this.useAnchor) || 0
// #ifdef APP-PLUS-NVUE
if (!id) {
dom.scrollToElement(this.$refs.web, {
offset
})
resolve()
} else {
this._navigateTo = {
resolve,
reject,
offset
}
this.$refs.web.evalJs('uni.postMessage({data:{action:"getOffset",offset:(document.getElementById(' + id + ')||{}).offsetTop}})')
}
// #endif
// #ifndef APP-PLUS-NVUE
let deep = ' '
// #ifdef MP-WEIXIN || MP-QQ || MP-TOUTIAO
deep = '>>>'
// #endif
const selector = uni.createSelectorQuery()
// #ifndef MP-ALIPAY
.in(this._in ? this._in.page : this)
// #endif
.select((this._in ? this._in.selector : '._root') + (id ? `${deep}#${id}` : '')).boundingClientRect()
if (this._in) {
selector.select(this._in.selector).scrollOffset()
.select(this._in.selector).boundingClientRect()
} else {
// 获取 scroll-view 的位置和滚动距离
selector.selectViewport().scrollOffset() // 获取窗口的滚动距离
}
selector.exec(res => {
if (!res[0]) {
reject(Error('Label not found'))
return
}
const scrollTop = res[1].scrollTop + res[0].top - (res[2] ? res[2].top : 0) + offset
if (this._in) {
// scroll-view 跳转
this._in.page[this._in.scrollTop] = scrollTop
} else {
// 页面跳转
uni.pageScrollTo({
scrollTop,
duration: 300
})
}
resolve()
})
// #endif
})
},
/**
* @description 获取文本内容
* @return {String}
*/
getText (nodes) {
let text = '';
(function traversal (nodes) {
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i]
if (node.type === 'text') {
text += node.text.replace(/&amp;/g, '&')
} else if (node.name === 'br') {
text += '\n'
} else {
// 块级标签前后加换行
const isBlock = node.name === 'p' || node.name === 'div' || node.name === 'tr' || node.name === 'li' || (node.name[0] === 'h' && node.name[1] > '0' && node.name[1] < '7')
if (isBlock && text && text[text.length - 1] !== '\n') {
text += '\n'
}
// 递归获取子节点的文本
if (node.children) {
traversal(node.children)
}
if (isBlock && text[text.length - 1] !== '\n') {
text += '\n'
} else if (node.name === 'td' || node.name === 'th') {
text += '\t'
}
}
}
})(nodes || this.nodes)
return text
},
/**
* @description 获取内容大小和位置
* @return {Promise}
*/
getRect () {
return new Promise((resolve, reject) => {
uni.createSelectorQuery()
// #ifndef MP-ALIPAY
.in(this)
// #endif
.select('#_root').boundingClientRect().exec(res => res[0] ? resolve(res[0]) : reject(Error('Root label not found')))
})
},
/**
* @description 暂停播放媒体
*/
pauseMedia () {
for (let i = (this._videos || []).length; i--;) {
this._videos[i].pause()
}
// #ifdef APP-PLUS
const command = 'for(var e=document.getElementsByTagName("video"),i=e.length;i--;)e[i].pause()'
// #ifndef APP-PLUS-NVUE
let page = this.$parent
while (!page.$scope) page = page.$parent
page.$scope.$getAppWebview().evalJS(command)
// #endif
// #ifdef APP-PLUS-NVUE
this.$refs.web.evalJs(command)
// #endif
// #endif
},
/**
* @description 设置媒体播放速率
* @param {Number} rate 播放速率
*/
setPlaybackRate (rate) {
this.playbackRate = rate
for (let i = (this._videos || []).length; i--;) {
this._videos[i].playbackRate(rate)
}
// #ifdef APP-PLUS
const command = 'for(var e=document.getElementsByTagName("video"),i=e.length;i--;)e[i].playbackRate=' + rate
// #ifndef APP-PLUS-NVUE
let page = this.$parent
while (!page.$scope) page = page.$parent
page.$scope.$getAppWebview().evalJS(command)
// #endif
// #ifdef APP-PLUS-NVUE
this.$refs.web.evalJs(command)
// #endif
// #endif
},
/**
* @description 设置内容
* @param {String} content html 内容
* @param {Boolean} append 是否在尾部追加
*/
setContent (content, append) {
if (!append || !this.imgList) {
this.imgList = []
}
const nodes = new Parser(this).parse(content)
// #ifdef APP-PLUS-NVUE
if (this._ready) {
this._set(nodes, append)
}
// #endif
this.$set(this, 'nodes', append ? (this.nodes || []).concat(nodes) : nodes)
// #ifndef APP-PLUS-NVUE
this._videos = []
this.$nextTick(() => {
this._hook('onLoad')
this.$emit('load')
})
if (this.lazyLoad || this.imgList._unloadimgs < this.imgList.length / 2) {
// 设置懒加载,每 350ms 获取高度,不变则认为加载完毕
let height = 0
const callback = rect => {
if (!rect || !rect.height) rect = {}
// 350ms 总高度无变化就触发 ready 事件
if (rect.height === height) {
this.$emit('ready', rect)
} else {
height = rect.height
setTimeout(() => {
this.getRect().then(callback).catch(callback)
}, 350)
}
}
this.getRect().then(callback).catch(callback)
} else {
// 未设置懒加载,等待所有图片加载完毕
if (!this.imgList._unloadimgs) {
this.getRect().then(rect => {
this.$emit('ready', rect)
}).catch(() => {
this.$emit('ready', {})
})
}
}
// #endif
},
/**
* @description 调用插件钩子函数
*/
_hook (name) {
for (let i = plugins.length; i--;) {
if (this.plugins[i][name]) {
this.plugins[i][name]()
}
}
},
// #ifdef APP-PLUS-NVUE
/**
* @description 设置内容
*/
_set (nodes, append) {
this.$refs.web.evalJs('setContent(' + JSON.stringify(nodes).replace(/%22/g, '') + ',' + JSON.stringify([this.containerStyle.replace(/(?:margin|padding)[^;]+/g, ''), this.errorImg, this.loadingImg, this.pauseVideo, this.scrollTable, this.selectable]) + ',' + append + ')')
},
/**
* @description 接收到 web-view 消息
*/
_onMessage (e) {
const message = e.detail.data[0]
switch (message.action) {
// web-view 初始化完毕
case 'onJSBridgeReady':
this._ready = true
if (this.nodes) {
this._set(this.nodes)
}
break
// 内容 dom 加载完毕
case 'onLoad':
this.height = message.height
this._hook('onLoad')
this.$emit('load')
break
// 所有图片加载完毕
case 'onReady':
this.getRect().then(res => {
this.$emit('ready', res)
}).catch(() => {
this.$emit('ready', {})
})
break
// 总高度发生变化
case 'onHeightChange':
this.height = message.height
break
// 图片点击
case 'onImgTap':
this.$emit('imgtap', message.attrs)
if (this.previewImg) {
uni.previewImage({
current: parseInt(message.attrs.i),
urls: this.imgList
})
}
break
// 链接点击
case 'onLinkTap': {
const href = message.attrs.href
this.$emit('linktap', message.attrs)
if (href) {
// 锚点跳转
if (href[0] === '#') {
if (this.useAnchor) {
dom.scrollToElement(this.$refs.web, {
offset: message.offset
})
}
} else if (href.includes('://')) {
// 打开外链
if (this.copyLink) {
plus.runtime.openWeb(href)
}
} else {
uni.navigateTo({
url: href,
fail () {
uni.switchTab({
url: href
})
}
})
}
}
break
}
case 'onPlay':
this.$emit('play')
break
// 获取到锚点的偏移量
case 'getOffset':
if (typeof message.offset === 'number') {
dom.scrollToElement(this.$refs.web, {
offset: message.offset + this._navigateTo.offset
})
this._navigateTo.resolve()
} else {
this._navigateTo.reject(Error('Label not found'))
}
break
// 点击
case 'onClick':
this.$emit('tap')
this.$emit('click')
break
// 出错
case 'onError':
this.$emit('error', {
source: message.source,
attrs: message.attrs
})
}
}
// #endif
}
}
</script>
<style>
/* #ifndef APP-PLUS-NVUE */
/* 根节点样式 */
._root {
padding: 1px 0;
overflow-x: auto;
overflow-y: hidden;
-webkit-overflow-scrolling: touch;
}
/* 长按复制 */
._select {
user-select: text;
}
/* #endif */
</style>
<template>
<view :id="attrs.id" :class="'_block _'+name+' '+attrs.class" :style="attrs.style">
<block v-for="(n, i) in childs" v-bind:key="i">
<!-- 图片 -->
<!-- 占位图 -->
<image v-if="n.name==='img'&&!n.t&&((opts[1]&&!ctrl[i])||ctrl[i]<0)" class="_img" :style="n.attrs.style" :src="ctrl[i]<0?opts[2]:opts[1]" mode="widthFix" />
<!-- 显示图片 -->
<!-- #ifdef H5 || (APP-PLUS && VUE2) -->
<img v-if="n.name==='img'" :id="n.attrs.id" :class="'_img '+n.attrs.class" :style="(ctrl[i]===-1?'display:none;':'')+n.attrs.style" :src="n.attrs.src||(ctrl.load?n.attrs['data-src']:'')" :data-i="i" @load="imgLoad" @error="mediaError" @tap.stop="imgTap" @longpress="imgLongTap" />
<!-- #endif -->
<!-- #ifndef H5 || (APP-PLUS && VUE2) -->
<!-- 表格中的图片,使用 rich-text 防止大小不正确 -->
<rich-text v-if="n.name==='img'&&n.t" :style="'display:'+n.t" :nodes="[{attrs:{style:n.attrs.style||'',src:n.attrs.src},name:'img'}]" :data-i="i" @tap.stop="imgTap" />
<!-- #endif -->
<!-- #ifdef APP-HARMONY -->
<image v-else-if="n.name==='img'" :id="n.attrs.id" :class="'_img '+n.attrs.class" :style="(ctrl[i]===-1?'display:none;':'')+'width:'+ctrl[i]+'px;'+n.attrs.style" :src="n.attrs.src||(ctrl.load?n.attrs['data-src']:'')" :mode="!n.h?'widthFix':(!n.w?'heightFix':(n.m||'scaleToFill'))" :data-i="i" @load="imgLoad" @error="mediaError" @tap.stop="imgTap" @longpress="imgLongTap" />
<!-- #endif -->
<!-- #ifndef H5 || APP-PLUS || MP-KUAISHOU -->
<image v-else-if="n.name==='img'" :id="n.attrs.id" :class="'_img '+n.attrs.class" :style="(ctrl[i]===-1?'display:none;':'')+'width:'+(ctrl[i]||1)+'px;height:1px;'+n.attrs.style" :src="n.attrs.src" :mode="!n.h?'widthFix':(!n.w?'heightFix':(n.m||'scaleToFill'))" :lazy-load="opts[0]" :webp="n.webp" :show-menu-by-longpress="opts[3]&&!n.attrs.ignore" :image-menu-prevent="!opts[3]||n.attrs.ignore" :data-i="i" @load="imgLoad" @error="mediaError" @tap.stop="imgTap" @longpress="imgLongTap" />
<!-- #endif -->
<!-- #ifdef MP-KUAISHOU -->
<image v-else-if="n.name==='img'" :id="n.attrs.id" :class="'_img '+n.attrs.class" :style="(ctrl[i]===-1?'display:none;':'')+n.attrs.style" :src="n.attrs.src" :lazy-load="opts[0]" :data-i="i" @load="imgLoad" @error="mediaError" @tap.stop="imgTap"></image>
<!-- #endif -->
<!-- #ifdef APP-PLUS && VUE3 -->
<image v-else-if="n.name==='img'" :id="n.attrs.id" :class="'_img '+n.attrs.class" :style="(ctrl[i]===-1?'display:none;':'')+'width:'+(ctrl[i]||1)+'px;'+n.attrs.style" :src="n.attrs.src||(ctrl.load?n.attrs['data-src']:'')" :mode="!n.h?'widthFix':(!n.w?'heightFix':(n.m||''))" :data-i="i" @load="imgLoad" @error="mediaError" @tap.stop="imgTap" @longpress="imgLongTap" />
<!-- #endif -->
<!-- 文本 -->
<!-- #ifdef MP-WEIXIN -->
<text v-else-if="n.text" :user-select="opts[4]=='force'&&isiOS" decode>{{n.text}}</text>
<!-- #endif -->
<!-- #ifndef MP-WEIXIN || MP-BAIDU || MP-ALIPAY || MP-TOUTIAO -->
<text v-else-if="n.text" decode>{{n.text}}</text>
<!-- #endif -->
<text v-else-if="n.name==='br'">\n</text>
<!-- 链接 -->
<view v-else-if="n.name==='a'" :id="n.attrs.id" :class="(n.attrs.href?'_a ':'')+n.attrs.class" hover-class="_hover" :style="'display:inline;'+n.attrs.style" :data-i="i" @tap.stop="linkTap">
<node name="span" :childs="n.children" :opts="opts" style="display:inherit" />
</view>
<!-- 视频 -->
<!-- #ifdef APP-PLUS -->
<view v-else-if="n.html" :id="n.attrs.id" :class="'_video '+n.attrs.class" :style="n.attrs.style" v-html="n.html" :data-i="i" @vplay.stop="play" />
<!-- #endif -->
<!-- #ifndef APP-PLUS -->
<video v-else-if="n.name==='video'" :id="n.attrs.id" :class="n.attrs.class" :style="n.attrs.style" :autoplay="n.attrs.autoplay" :controls="n.attrs.controls" :loop="n.attrs.loop" :muted="n.attrs.muted" :object-fit="n.attrs['object-fit']" :poster="n.attrs.poster" :src="n.src[ctrl[i]||0]" :data-i="i" @play="play" @error="mediaError" />
<!-- #endif -->
<!-- #ifdef H5 || APP-PLUS -->
<iframe v-else-if="n.name==='iframe'" :style="n.attrs.style" :allowfullscreen="n.attrs.allowfullscreen" :frameborder="n.attrs.frameborder" :src="n.attrs.src" />
<embed v-else-if="n.name==='embed'" :style="n.attrs.style" :src="n.attrs.src" />
<!-- #endif -->
<!-- #ifndef MP-TOUTIAO || ((H5 || APP-PLUS) && VUE3) -->
<!-- 音频 -->
<audio v-else-if="n.name==='audio'" :id="n.attrs.id" :class="n.attrs.class" :style="n.attrs.style" :author="n.attrs.author" :controls="n.attrs.controls" :loop="n.attrs.loop" :name="n.attrs.name" :poster="n.attrs.poster" :src="n.src[ctrl[i]||0]" :data-i="i" @play="play" @error="mediaError" />
<!-- #endif -->
<view v-else-if="(n.name==='table'&&n.c)||n.name==='li'" :id="n.attrs.id" :class="'_'+n.name+' '+n.attrs.class" :style="n.attrs.style">
<node v-if="n.name==='li'" :childs="n.children" :opts="opts" />
<view v-else v-for="(tbody, x) in n.children" v-bind:key="x" :class="'_'+tbody.name+' '+tbody.attrs.class" :style="tbody.attrs.style">
<node v-if="tbody.name==='td'||tbody.name==='th'" :childs="tbody.children" :opts="opts" />
<block v-else v-for="(tr, y) in tbody.children" v-bind:key="y">
<view v-if="tr.name==='td'||tr.name==='th'" :class="'_'+tr.name+' '+tr.attrs.class" :style="tr.attrs.style">
<node :childs="tr.children" :opts="opts" />
</view>
<view v-else :class="'_'+tr.name+' '+tr.attrs.class" :style="tr.attrs.style">
<view v-for="(td, z) in tr.children" v-bind:key="z" :class="'_'+td.name+' '+td.attrs.class" :style="td.attrs.style">
<node :childs="td.children" :opts="opts" />
</view>
</view>
</block>
</view>
</view>
<!-- 富文本 -->
<!-- #ifdef H5 || ((MP-WEIXIN || MP-QQ || APP-PLUS || MP-360) && VUE2) -->
<rich-text v-else-if="!n.c&&!handler.isInline(n.name, n.attrs.style)" :id="n.attrs.id" :style="n.f" :user-select="opts[4]" :nodes="[n]" />
<!-- #endif -->
<!-- #ifndef H5 || ((MP-WEIXIN || MP-QQ || APP-PLUS || MP-360) && VUE2) -->
<rich-text v-else-if="!n.c" :id="n.attrs.id" :style="'display:inline;'+n.f" :preview="false" :selectable="opts[4]" :user-select="opts[4]" :nodes="[n]" />
<!-- #endif -->
<!-- 继续递归 -->
<view v-else-if="n.c===2" :id="n.attrs.id" :class="'_block _'+n.name+' '+n.attrs.class" :style="n.f+';'+n.attrs.style">
<node v-for="(n2, j) in n.children" v-bind:key="j" :style="n2.f" :name="n2.name" :attrs="n2.attrs" :childs="n2.children" :opts="opts" />
</view>
<node v-else :style="n.f" :name="n.name" :attrs="n.attrs" :childs="n.children" :opts="opts" />
</block>
</view>
</template>
<script module="handler" lang="wxs">
// 行内标签列表
var inlineTags = {
abbr: true,
b: true,
big: true,
code: true,
del: true,
em: true,
i: true,
ins: true,
label: true,
q: true,
small: true,
span: true,
strong: true,
sub: true,
sup: true
}
/**
* @description 判断是否为行内标签
*/
module.exports = {
isInline: function (tagName, style) {
return inlineTags[tagName] || (style || '').indexOf('display:inline') !== -1
}
}
</script>
<script>
import node from './node'
export default {
name: 'node',
options: {
// #ifdef MP-WEIXIN
virtualHost: true,
// #endif
// #ifdef MP-TOUTIAO
addGlobalClass: false
// #endif
},
data () {
return {
ctrl: {},
// #ifdef MP-WEIXIN
isiOS: uni.getSystemInfoSync().system.includes('iOS')
// #endif
}
},
props: {
name: String,
attrs: {
type: Object,
default () {
return {}
}
},
childs: Array,
opts: Array
},
components: {
// #ifndef ((H5 || APP-PLUS) && VUE3) || APP-HARMONY
node
// #endif
},
mounted () {
this.$nextTick(() => {
for (this.root = this.$parent; this.root.$options.name !== 'mp-html'; this.root = this.root.$parent);
})
// #ifdef H5 || APP-PLUS
if (this.opts[0]) {
let i
for (i = this.childs.length; i--;) {
if (this.childs[i].name === 'img') break
}
if (i !== -1) {
this.observer = uni.createIntersectionObserver(this).relativeToViewport({
top: 500,
bottom: 500
})
this.observer.observe('._img', res => {
if (res.intersectionRatio) {
this.$set(this.ctrl, 'load', 1)
this.observer.disconnect()
}
})
}
}
// #endif
},
beforeDestroy () {
// #ifdef H5 || APP-PLUS
if (this.observer) {
this.observer.disconnect()
}
// #endif
},
methods:{
// #ifdef MP-WEIXIN
toJSON () { return this },
// #endif
/**
* @description 播放视频事件
* @param {Event} e
*/
play (e) {
const i = e.currentTarget.dataset.i
const node = this.childs[i]
this.root.$emit('play', {
source: node.name,
attrs: {
...node.attrs,
src: node.src[this.ctrl[i] || 0]
}
})
// #ifndef APP-PLUS
if (this.root.pauseVideo) {
let flag = false
const id = e.target.id
for (let i = this.root._videos.length; i--;) {
if (this.root._videos[i].id === id) {
flag = true
} else {
this.root._videos[i].pause() // 自动暂停其他视频
}
}
// 将自己加入列表
if (!flag) {
const ctx = uni.createVideoContext(id
// #ifndef MP-BAIDU
, this
// #endif
)
ctx.id = id
if (this.root.playbackRate) {
ctx.playbackRate(this.root.playbackRate)
}
this.root._videos.push(ctx)
}
}
// #endif
},
/**
* @description 图片点击事件
* @param {Event} e
*/
imgTap (e) {
const node = this.childs[e.currentTarget.dataset.i]
if (node.a) {
this.linkTap(node.a)
return
}
if (node.attrs.ignore) return
// #ifdef H5 || APP-PLUS
node.attrs.src = node.attrs.src || node.attrs['data-src']
// #endif
// #ifndef APP-HARMONY
this.root.$emit('imgtap', node.attrs)
// #endif
// #ifdef APP-HARMONY
this.root.$emit('imgtap', {
...node.attrs
})
// #endif
// 自动预览图片
if (this.root.previewImg) {
uni.previewImage({
// #ifdef MP-WEIXIN
showmenu: this.root.showImgMenu,
// #endif
// #ifdef MP-ALIPAY
enablesavephoto: this.root.showImgMenu,
enableShowPhotoDownload: this.root.showImgMenu,
// #endif
current: parseInt(node.attrs.i),
urls: this.root.imgList
})
}
},
/**
* @description 图片长按
*/
imgLongTap (e) {
// #ifdef APP-PLUS
const attrs = this.childs[e.currentTarget.dataset.i].attrs
if (this.opts[3] && !attrs.ignore) {
uni.showActionSheet({
itemList: ['保存图片'],
success: () => {
const save = path => {
uni.saveImageToPhotosAlbum({
filePath: path,
success () {
uni.showToast({
title: '保存成功'
})
}
})
}
if (this.root.imgList[attrs.i].startsWith('http')) {
uni.downloadFile({
url: this.root.imgList[attrs.i],
success: res => save(res.tempFilePath)
})
} else {
save(this.root.imgList[attrs.i])
}
}
})
}
// #endif
},
/**
* @description 图片加载完成事件
* @param {Event} e
*/
imgLoad (e) {
const i = e.currentTarget.dataset.i
/* #ifndef H5 || (APP-PLUS && VUE2) */
if (!this.childs[i].w) {
// 设置原宽度
this.$set(this.ctrl, i, e.detail.width)
} else /* #endif */ if ((this.opts[1] && !this.ctrl[i]) || this.ctrl[i] === -1) {
// 加载完毕,取消加载中占位图
this.$set(this.ctrl, i, 1)
}
this.checkReady()
},
/**
* @description 检查是否所有图片加载完毕
*/
checkReady () {
if (this.root && !this.root.lazyLoad) {
this.root._unloadimgs -= 1
if (!this.root._unloadimgs) {
setTimeout(() => {
this.root.getRect().then(rect => {
this.root.$emit('ready', rect)
}).catch(() => {
this.root.$emit('ready', {})
})
}, 350)
}
}
},
/**
* @description 链接点击事件
* @param {Event} e
*/
linkTap (e) {
const node = e.currentTarget ? this.childs[e.currentTarget.dataset.i] : {}
const attrs = node.attrs || e
const href = attrs.href
this.root.$emit('linktap', Object.assign({
innerText: this.root.getText(node.children || []) // 链接内的文本内容
}, attrs))
if (href) {
if (href[0] === '#') {
// 跳转锚点
this.root.navigateTo(href.substring(1)).catch(() => { })
} else if (href.split('?')[0].includes('://')) {
// 复制外部链接
if (this.root.copyLink) {
// #ifdef H5
window.open(href)
// #endif
// #ifdef MP
uni.setClipboardData({
data: href,
success: () =>
uni.showToast({
title: '链接已复制'
})
})
// #endif
// #ifdef APP-PLUS
plus.runtime.openWeb(href)
// #endif
}
} else {
// 跳转页面
uni.navigateTo({
url: href,
fail () {
uni.switchTab({
url: href,
fail () { }
})
}
})
}
}
},
/**
* @description 错误事件
* @param {Event} e
*/
mediaError (e) {
const i = e.currentTarget.dataset.i
const node = this.childs[i]
// 加载其他源
if (node.name === 'video' || node.name === 'audio') {
let index = (this.ctrl[i] || 0) + 1
if (index > node.src.length) {
index = 0
}
if (index < node.src.length) {
this.$set(this.ctrl, i, index)
return
}
} else if (node.name === 'img') {
// #ifdef H5 && VUE3
if (this.opts[0] && !this.ctrl.load) return
// #endif
// 显示错误占位图
if (this.opts[2]) {
this.$set(this.ctrl, i, -1)
}
this.checkReady()
}
if (this.root) {
this.root.$emit('error', {
source: node.name,
attrs: node.attrs,
// #ifndef H5 && VUE3
errMsg: e.detail.errMsg
// #endif
})
}
}
}
}
</script>
<style>
/* a 标签默认效果 */
._a {
padding: 1.5px 0 1.5px 0;
color: #366092;
word-break: break-all;
}
/* a 标签点击态效果 */
._hover {
text-decoration: underline;
opacity: 0.7;
}
/* 图片默认效果 */
._img {
max-width: 100%;
-webkit-touch-callout: none;
}
/* 内部样式 */
._block {
display: block;
}
._b,
._strong {
font-weight: bold;
}
._code {
font-family: monospace;
}
._del {
text-decoration: line-through;
}
._em,
._i {
font-style: italic;
}
._h1 {
font-size: 2em;
}
._h2 {
font-size: 1.5em;
}
._h3 {
font-size: 1.17em;
}
._h5 {
font-size: 0.83em;
}
._h6 {
font-size: 0.67em;
}
._h1,
._h2,
._h3,
._h4,
._h5,
._h6 {
display: block;
font-weight: bold;
}
._image {
height: 1px;
}
._ins {
text-decoration: underline;
}
._li {
display: list-item;
}
._ol {
list-style-type: decimal;
}
._ol,
._ul {
display: block;
padding-left: 40px;
margin: 1em 0;
}
._q::before {
content: '"';
}
._q::after {
content: '"';
}
._sub {
font-size: smaller;
vertical-align: sub;
}
._sup {
font-size: smaller;
vertical-align: super;
}
._thead,
._tbody,
._tfoot {
display: table-row-group;
}
._tr {
display: table-row;
}
._td,
._th {
display: table-cell;
vertical-align: middle;
}
._th {
font-weight: bold;
text-align: center;
}
._ul {
list-style-type: disc;
}
._ul ._ul {
margin: 0;
list-style-type: circle;
}
._ul ._ul ._ul {
list-style-type: square;
}
._abbr,
._b,
._code,
._del,
._em,
._i,
._ins,
._label,
._q,
._span,
._strong,
._sub,
._sup {
display: inline;
}
/* #ifdef APP-PLUS */
._video {
width: 300px;
height: 225px;
}
/* #endif */
</style>
\ No newline at end of file
/**
* @fileoverview html 解析器
*/
// 配置
const config = {
// 信任的标签(保持标签名不变)
trustTags: makeMap('a,abbr,ad,audio,b,blockquote,br,code,col,colgroup,dd,del,dl,dt,div,em,fieldset,h1,h2,h3,h4,h5,h6,hr,i,img,ins,label,legend,li,ol,p,q,ruby,rt,source,span,strong,sub,sup,table,tbody,td,tfoot,th,thead,tr,title,ul,video'),
// 块级标签(转为 div,其他的非信任标签转为 span)
blockTags: makeMap('address,article,aside,body,caption,center,cite,footer,header,html,nav,pre,section'),
// #ifdef (MP-WEIXIN || MP-QQ || APP-PLUS || MP-360) && VUE3
// 行内标签
inlineTags: makeMap('abbr,b,big,code,del,em,i,ins,label,q,small,span,strong,sub,sup'),
// #endif
// 要移除的标签
ignoreTags: makeMap('area,base,canvas,embed,frame,head,iframe,input,link,map,meta,param,rp,script,source,style,textarea,title,track,wbr'),
// 自闭合的标签
voidTags: makeMap('area,base,br,col,circle,ellipse,embed,frame,hr,img,input,line,link,meta,param,path,polygon,rect,source,track,use,wbr'),
// html 实体
entities: {
lt: '<',
gt: '>',
quot: '"',
apos: "'",
ensp: '\u2002',
emsp: '\u2003',
nbsp: '\xA0',
semi: ';',
ndash: '–',
mdash: '—',
middot: '·',
lsquo: '‘',
rsquo: '’',
ldquo: '“',
rdquo: '”',
bull: '•',
hellip: '…',
larr: '←',
uarr: '↑',
rarr: '→',
darr: '↓'
},
// 默认的标签样式
tagStyle: {
// #ifndef APP-PLUS-NVUE
address: 'font-style:italic',
big: 'display:inline;font-size:1.2em',
caption: 'display:table-caption;text-align:center',
center: 'text-align:center',
cite: 'font-style:italic',
dd: 'margin-left:40px',
mark: 'background-color:yellow',
pre: 'font-family:monospace;white-space:pre',
s: 'text-decoration:line-through',
small: 'display:inline;font-size:0.8em',
strike: 'text-decoration:line-through',
u: 'text-decoration:underline'
// #endif
},
// svg 大小写对照表
svgDict: {
animatetransform: 'animateTransform',
lineargradient: 'linearGradient',
viewbox: 'viewBox',
attributename: 'attributeName',
repeatcount: 'repeatCount',
repeatdur: 'repeatDur',
foreignobject: 'foreignObject'
}
}
const tagSelector={}
let windowWidth, system
// #ifdef MP-WEIXIN
if (uni.canIUse('getWindowInfo')) {
windowWidth = uni.getWindowInfo().windowWidth
system = uni.getDeviceInfo().system
} else {
// #endif
const systemInfo = uni.getSystemInfoSync()
windowWidth = systemInfo.windowWidth
// #ifdef MP-WEIXIN
system = systemInfo.system
}
// #endif
const blankChar = makeMap(' ,\r,\n,\t,\f')
let idIndex = 0
// #ifdef H5 || APP-PLUS
config.ignoreTags.iframe = undefined
config.trustTags.iframe = true
config.ignoreTags.embed = undefined
config.trustTags.embed = true
// #endif
// #ifdef APP-PLUS-NVUE
config.ignoreTags.source = undefined
config.ignoreTags.style = undefined
// #endif
/**
* @description 创建 map
* @param {String} str 逗号分隔
*/
function makeMap (str) {
const map = Object.create(null)
const list = str.split(',')
for (let i = list.length; i--;) {
map[list[i]] = true
}
return map
}
/**
* @description 解码 html 实体
* @param {String} str 要解码的字符串
* @param {Boolean} amp 要不要解码 &amp;
* @returns {String} 解码后的字符串
*/
function decodeEntity (str, amp) {
let i = str.indexOf('&')
while (i !== -1) {
const j = str.indexOf(';', i + 3)
let code
if (j === -1) break
if (str[i + 1] === '#') {
// &#123; 形式的实体
code = parseInt((str[i + 2] === 'x' ? '0' : '') + str.substring(i + 2, j))
if (!isNaN(code)) {
str = str.substr(0, i) + String.fromCharCode(code) + str.substr(j + 1)
}
} else {
// &nbsp; 形式的实体
code = str.substring(i + 1, j)
if (config.entities[code] || (code === 'amp' && amp)) {
str = str.substr(0, i) + (config.entities[code] || '&') + str.substr(j + 1)
}
}
i = str.indexOf('&', i + 1)
}
return str
}
/**
* @description 合并多个块级标签,加快长内容渲染
* @param {Array} nodes 要合并的标签数组
*/
function mergeNodes (nodes) {
let i = nodes.length - 1
for (let j = i; j >= -1; j--) {
if (j === -1 || nodes[j].c || !nodes[j].name || (nodes[j].name !== 'div' && nodes[j].name !== 'p' && nodes[j].name[0] !== 'h') || (nodes[j].attrs.style || '').includes('inline')) {
if (i - j >= 5) {
nodes.splice(j + 1, i - j, {
name: 'div',
attrs: {},
children: nodes.slice(j + 1, i + 1)
})
}
i = j - 1
}
}
}
/**
* @description html 解析器
* @param {Object} vm 组件实例
*/
function Parser (vm) {
this.options = vm || {}
this.tagStyle = Object.assign({}, config.tagStyle, this.options.tagStyle)
this.imgList = vm.imgList || []
this.imgList._unloadimgs = 0
this.plugins = vm.plugins || []
this.attrs = Object.create(null)
this.stack = []
this.nodes = []
this.pre = (this.options.containerStyle || '').includes('white-space') && this.options.containerStyle.includes('pre') ? 2 : 0
}
/**
* @description 执行解析
* @param {String} content 要解析的文本
*/
Parser.prototype.parse = function (content) {
// 插件处理
for (let i = this.plugins.length; i--;) {
if (this.plugins[i].onUpdate) {
content = this.plugins[i].onUpdate(content, config) || content
}
}
new Lexer(this).parse(content)
// 出栈未闭合的标签
while (this.stack.length) {
this.popNode()
}
if (this.nodes.length > 50) {
mergeNodes(this.nodes)
}
return this.nodes
}
/**
* @description 将标签暴露出来(不被 rich-text 包含)
*/
Parser.prototype.expose = function () {
// #ifndef APP-PLUS-NVUE
for (let i = this.stack.length; i--;) {
const item = this.stack[i]
if (item.c || item.name === 'a' || item.name === 'video' || item.name === 'audio') return
item.c = 1
}
// #endif
}
/**
* @description 处理插件
* @param {Object} node 要处理的标签
* @returns {Boolean} 是否要移除此标签
*/
Parser.prototype.hook = function (node) {
for (let i = this.plugins.length; i--;) {
if (this.plugins[i].onParse && this.plugins[i].onParse(node, this) === false) {
return false
}
}
return true
}
/**
* @description 将链接拼接上主域名
* @param {String} url 需要拼接的链接
* @returns {String} 拼接后的链接
*/
Parser.prototype.getUrl = function (url) {
const domain = this.options.domain
if (url[0] === '/') {
if (url[1] === '/') {
// // 开头的补充协议名
url = (domain ? domain.split('://')[0] : 'http') + ':' + url
} else if (domain) {
// 否则补充整个域名
url = domain + url
} /* #ifdef APP-PLUS */ else {
url = plus.io.convertLocalFileSystemURL(url)
} /* #endif */
} else if (!url.includes('data:') && !url.includes('://')) {
if (domain) {
url = domain + '/' + url
} /* #ifdef APP-PLUS */ else {
url = plus.io.convertLocalFileSystemURL(url)
} /* #endif */
}
return url
}
/**
* @description 解析样式表
* @param {Object} node 标签
* @returns {Object}
*/
Parser.prototype.parseStyle = function (node) {
const attrs = node.attrs
const list = (this.tagStyle[node.name] || '').split(';').concat((attrs.style || '').split(';'))
const styleObj = {}
let tmp = ''
if (attrs.id && !this.xml) {
// 暴露锚点
if (this.options.useAnchor) {
this.expose()
} else if (node.name !== 'img' && node.name !== 'a' && node.name !== 'video' && node.name !== 'audio') {
attrs.id = undefined
}
}
// 转换 width 和 height 属性
if (attrs.width) {
styleObj.width = parseFloat(attrs.width) + (attrs.width.includes('%') ? '%' : 'px')
attrs.width = undefined
}
if (attrs.height) {
styleObj.height = parseFloat(attrs.height) + (attrs.height.includes('%') ? '%' : 'px')
attrs.height = undefined
}
for (let i = 0, len = list.length; i < len; i++) {
const info = list[i].split(':')
if (info.length < 2) continue
const key = info.shift().trim().toLowerCase()
let value = info.join(':').trim()
if ((value[0] === '-' && value.lastIndexOf('-') > 0) || value.includes('safe')) {
// 兼容性的 css 不压缩
tmp += `;${key}:${value}`
} else if (!styleObj[key] || value.includes('import') || !styleObj[key].includes('import')) {
// 重复的样式进行覆盖
if (value.includes('url')) {
// 填充链接
let j = value.indexOf('(') + 1
if (j) {
while (value[j] === '"' || value[j] === "'" || blankChar[value[j]]) {
j++
}
value = value.substr(0, j) + this.getUrl(value.substr(j))
}
} else if (value.includes('rpx')) {
// 转换 rpx(rich-text 内部不支持 rpx)
value = value.replace(/[0-9.]+\s*rpx/g, $ => parseFloat($) * windowWidth / 750 + 'px')
}
styleObj[key] = value
}
}
node.attrs.style = tmp
return styleObj
}
/**
* @description 解析到标签名
* @param {String} name 标签名
* @private
*/
Parser.prototype.onTagName = function (name) {
this.tagName = this.xml ? name : name.toLowerCase()
if (this.tagName === 'svg') {
this.xml = (this.xml || 0) + 1 // svg 标签内大小写敏感
config.ignoreTags.style = undefined // svg 标签内 style 可用
}
}
/**
* @description 解析到属性名
* @param {String} name 属性名
* @private
*/
Parser.prototype.onAttrName = function (name) {
name = this.xml ? name : name.toLowerCase()
// #ifdef (VUE3 && (H5 || APP-PLUS)) || APP-PLUS-NVUE
if (name.includes('?') || name.includes(';')) {
this.attrName = undefined
return
}
// #endif
if (name.substr(0, 5) === 'data-') {
if (name === 'data-src' && !this.attrs.src) {
// data-src 自动转为 src
this.attrName = 'src'
} else if (this.tagName === 'img' || this.tagName === 'a') {
// a 和 img 标签保留 data- 的属性,可以在 imgtap 和 linktap 事件中使用
this.attrName = name
} else {
// 剩余的移除以减小大小
this.attrName = undefined
}
} else {
this.attrName = name
this.attrs[name] = 'T' // boolean 型属性缺省设置
}
}
/**
* @description 解析到属性值
* @param {String} val 属性值
* @private
*/
Parser.prototype.onAttrVal = function (val) {
const name = this.attrName || ''
if (name === 'style' || name === 'href') {
// 部分属性进行实体解码
this.attrs[name] = decodeEntity(val, true)
} else if (name.includes('src')) {
// 拼接主域名
this.attrs[name] = this.getUrl(decodeEntity(val, true))
} else if (name) {
this.attrs[name] = val
}
}
/**
* @description 解析到标签开始
* @param {Boolean} selfClose 是否有自闭合标识 />
* @private
*/
Parser.prototype.onOpenTag = function (selfClose) {
// 拼装 node
const node = Object.create(null)
node.name = this.tagName
node.attrs = this.attrs
// 避免因为自动 diff 使得 type 被设置为 null 导致部分内容不显示
if (this.options.nodes.length) {
node.type = 'node'
}
this.attrs = Object.create(null)
const attrs = node.attrs
const parent = this.stack[this.stack.length - 1]
const siblings = parent ? parent.children : this.nodes
const close = this.xml ? selfClose : config.voidTags[node.name]
// 替换标签名选择器
if (tagSelector[node.name]) {
attrs.class = tagSelector[node.name] + (attrs.class ? ' ' + attrs.class : '')
}
// 转换 embed 标签
if (node.name === 'embed') {
// #ifndef H5 || APP-PLUS
const src = attrs.src || ''
// 按照后缀名和 type 将 embed 转为 video 或 audio
if (src.includes('.mp4') || src.includes('.3gp') || src.includes('.m3u8') || (attrs.type || '').includes('video')) {
node.name = 'video'
} else if (src.includes('.mp3') || src.includes('.wav') || src.includes('.aac') || src.includes('.m4a') || (attrs.type || '').includes('audio')) {
node.name = 'audio'
}
if (attrs.autostart) {
attrs.autoplay = 'T'
}
attrs.controls = 'T'
// #endif
// #ifdef H5 || APP-PLUS
this.expose()
// #endif
}
// #ifndef APP-PLUS-NVUE
// 处理音视频
if (node.name === 'video' || node.name === 'audio') {
// 设置 id 以便获取 context
if (node.name === 'video' && !attrs.id) {
attrs.id = 'v' + idIndex++
}
// 没有设置 controls 也没有设置 autoplay 的自动设置 controls
if (!attrs.controls && !attrs.autoplay) {
attrs.controls = 'T'
}
// 用数组存储所有可用的 source
node.src = []
if (attrs.src) {
node.src.push(attrs.src)
attrs.src = undefined
}
this.expose()
}
// #endif
// 处理自闭合标签
if (close) {
if (!this.hook(node) || config.ignoreTags[node.name]) {
// 通过 base 标签设置主域名
if (node.name === 'base' && !this.options.domain) {
this.options.domain = attrs.href
} /* #ifndef APP-PLUS-NVUE */ else if (node.name === 'source' && parent && (parent.name === 'video' || parent.name === 'audio') && attrs.src) {
// 设置 source 标签(仅父节点为 video 或 audio 时有效)
parent.src.push(attrs.src)
} /* #endif */
return
}
// 解析 style
const styleObj = this.parseStyle(node)
// 处理图片
if (node.name === 'img') {
if (attrs.src) {
// 标记 webp
if (attrs.src.includes('webp')) {
node.webp = 'T'
}
// data url 图片如果没有设置 original-src 默认为不可预览的小图片
if (attrs.src.includes('data:') && this.options.previewImg !== 'all' && !attrs['original-src']) {
attrs.ignore = 'T'
}
if (!attrs.ignore || node.webp || attrs.src.includes('cloud://')) {
for (let i = this.stack.length; i--;) {
const item = this.stack[i]
if (item.name === 'a') {
node.a = item.attrs
}
if (item.name === 'table' && !node.webp && !attrs.src.includes('cloud://')) {
if (!styleObj.display || styleObj.display.includes('inline')) {
node.t = 'inline-block'
} else {
node.t = styleObj.display
}
styleObj.display = undefined
}
// #ifndef H5 || APP-PLUS
const style = item.attrs.style || ''
if (style.includes('flex:') && !style.includes('flex:0') && !style.includes('flex: 0') && (!styleObj.width || parseInt(styleObj.width) > 100)) {
styleObj.width = '100% !important'
styleObj.height = ''
for (let j = i + 1; j < this.stack.length; j++) {
this.stack[j].attrs.style = (this.stack[j].attrs.style || '').replace('inline-', '')
}
} else if (style.includes('flex') && styleObj.width === '100%') {
for (let j = i + 1; j < this.stack.length; j++) {
const style = this.stack[j].attrs.style || ''
if (!style.includes(';width') && !style.includes(' width') && style.indexOf('width') !== 0) {
styleObj.width = ''
break
}
}
} else if (style.includes('inline-block')) {
if (styleObj.width && styleObj.width[styleObj.width.length - 1] === '%') {
item.attrs.style += ';max-width:' + styleObj.width
styleObj.width = ''
} else {
item.attrs.style += ';max-width:100%'
}
}
// #endif
item.c = 1
}
attrs.i = this.imgList.length.toString()
let src = attrs['original-src'] || attrs.src
// #ifndef H5 || MP-ALIPAY || APP-PLUS || MP-360
if (this.imgList.includes(src)) {
// 如果有重复的链接则对域名进行随机大小写变换避免预览时错位
let i = src.indexOf('://')
if (i !== -1) {
i += 3
let newSrc = src.substr(0, i)
for (; i < src.length; i++) {
if (src[i] === '/') break
newSrc += Math.random() > 0.5 ? src[i].toUpperCase() : src[i]
}
newSrc += src.substr(i)
src = newSrc
}
}
// #endif
this.imgList.push(src)
if (!node.t) {
this.imgList._unloadimgs += 1
}
// #ifdef H5 || APP-PLUS
if (this.options.lazyLoad) {
attrs['data-src'] = attrs.src
attrs.src = undefined
}
// #endif
}
}
if (styleObj.display === 'inline') {
styleObj.display = ''
}
// #ifndef APP-PLUS-NVUE
if (attrs.ignore) {
styleObj['max-width'] = styleObj['max-width'] || '100%'
attrs.style += ';-webkit-touch-callout:none'
}
// #endif
// 设置的宽度超出屏幕,为避免变形,高度转为自动
if (parseInt(styleObj.width) > windowWidth) {
styleObj.height = undefined
}
// 记录是否设置了宽高
if (!isNaN(parseInt(styleObj.width))) {
node.w = 'T'
}
if (!isNaN(parseInt(styleObj.height)) && (!styleObj.height.includes('%') || (parent && (parent.attrs.style || '').includes('height')))) {
node.h = 'T'
}
if (node.w && node.h && styleObj['object-fit']) {
if (styleObj['object-fit'] === 'contain') {
node.m = 'aspectFit'
} else if (styleObj['object-fit'] === 'cover') {
node.m = 'aspectFill'
}
}
} else if (node.name === 'svg') {
siblings.push(node)
this.stack.push(node)
this.popNode()
return
}
for (const key in styleObj) {
if (styleObj[key]) {
attrs.style += `;${key}:${styleObj[key].replace(' !important', '')}`
}
}
attrs.style = attrs.style.substr(1) || undefined
// #ifdef (MP-WEIXIN || MP-QQ) && VUE3
if (!attrs.style) {
delete attrs.style
}
// #endif
} else {
if ((node.name === 'pre' || ((attrs.style || '').includes('white-space') && attrs.style.includes('pre'))) && this.pre !== 2) {
this.pre = node.pre = 1
}
node.children = []
this.stack.push(node)
}
// 加入节点树
siblings.push(node)
}
/**
* @description 解析到标签结束
* @param {String} name 标签名
* @private
*/
Parser.prototype.onCloseTag = function (name) {
// 依次出栈到匹配为止
name = this.xml ? name : name.toLowerCase()
let i
for (i = this.stack.length; i--;) {
if (this.stack[i].name === name) break
}
if (i !== -1) {
while (this.stack.length > i) {
this.popNode()
}
} else if (name === 'p' || name === 'br') {
const siblings = this.stack.length ? this.stack[this.stack.length - 1].children : this.nodes
siblings.push({
name,
attrs: {
class: tagSelector[name] || '',
style: this.tagStyle[name] || ''
}
})
}
}
/**
* @description 处理标签出栈
* @private
*/
Parser.prototype.popNode = function () {
const node = this.stack.pop()
let attrs = node.attrs
const children = node.children
const parent = this.stack[this.stack.length - 1]
const siblings = parent ? parent.children : this.nodes
if (!this.hook(node) || config.ignoreTags[node.name]) {
// 获取标题
if (node.name === 'title' && children.length && children[0].type === 'text' && this.options.setTitle) {
uni.setNavigationBarTitle({
title: children[0].text
})
}
siblings.pop()
return
}
if (node.pre && this.pre !== 2) {
// 是否合并空白符标识
this.pre = node.pre = undefined
for (let i = this.stack.length; i--;) {
if (this.stack[i].pre) {
this.pre = 1
}
}
}
const styleObj = {}
// 转换 svg
if (node.name === 'svg') {
if (this.xml > 1) {
// 多层 svg 嵌套
this.xml--
return
}
// #ifdef APP-PLUS-NVUE
(function traversal (node) {
if (node.name) {
// 调整 svg 的大小写
node.name = config.svgDict[node.name] || node.name
for (const item in node.attrs) {
if (config.svgDict[item]) {
node.attrs[config.svgDict[item]] = node.attrs[item]
node.attrs[item] = undefined
}
}
for (let i = 0; i < (node.children || []).length; i++) {
traversal(node.children[i])
}
}
})(node)
// #endif
// #ifndef APP-PLUS-NVUE
let src = ''
const style = attrs.style
attrs.style = ''
attrs.xmlns = 'http://www.w3.org/2000/svg';
(function traversal (node) {
if (node.type === 'text') {
src += node.text
return
}
const name = config.svgDict[node.name] || node.name
if (name === 'foreignObject') {
for (const child of (node.children || [])) {
if (child.attrs && !child.attrs.xmlns) {
child.attrs.xmlns = 'http://www.w3.org/1999/xhtml'
break
}
}
}
src += '<' + name
for (const item in node.attrs) {
const val = node.attrs[item]
if (val) {
src += ` ${config.svgDict[item] || item}="${val.replace(/"/g, '')}"`
}
}
if (!node.children) {
src += '/>'
} else {
src += '>'
for (let i = 0; i < node.children.length; i++) {
traversal(node.children[i])
}
src += '</' + name + '>'
}
})(node)
node.name = 'img'
node.attrs = {
src: 'data:image/svg+xml;utf8,' + src.replace(/#/g, '%23'),
style,
ignore: 'T'
}
node.children = undefined
// #endif
this.xml = false
config.ignoreTags.style = true
return
}
// #ifndef APP-PLUS-NVUE
// 转换 align 属性
if (attrs.align) {
if (node.name === 'table') {
if (attrs.align === 'center') {
styleObj['margin-inline-start'] = styleObj['margin-inline-end'] = 'auto'
} else {
styleObj.float = attrs.align
}
} else {
styleObj['text-align'] = attrs.align
}
attrs.align = undefined
}
// 转换 dir 属性
if (attrs.dir) {
styleObj.direction = attrs.dir
attrs.dir = undefined
}
// 转换 font 标签的属性
if (node.name === 'font') {
if (attrs.color) {
styleObj.color = attrs.color
attrs.color = undefined
}
if (attrs.face) {
styleObj['font-family'] = attrs.face
attrs.face = undefined
}
if (attrs.size) {
let size = parseInt(attrs.size)
if (!isNaN(size)) {
if (size < 1) {
size = 1
} else if (size > 7) {
size = 7
}
styleObj['font-size'] = ['x-small', 'small', 'medium', 'large', 'x-large', 'xx-large', 'xxx-large'][size - 1]
}
attrs.size = undefined
}
}
// #endif
// 一些编辑器的自带 class
if ((attrs.class || '').includes('align-center')) {
styleObj['text-align'] = 'center'
}
Object.assign(styleObj, this.parseStyle(node))
if (node.name !== 'table' && parseInt(styleObj.width) > windowWidth) {
styleObj['max-width'] = '100%'
styleObj['box-sizing'] = 'border-box'
}
// #ifndef APP-PLUS-NVUE
if (config.blockTags[node.name]) {
node.name = 'div'
} else if (!config.trustTags[node.name] && !this.xml) {
// 未知标签转为 span,避免无法显示
node.name = 'span'
}
if (node.name === 'a' || node.name === 'ad'
// #ifdef H5 || APP-PLUS
|| node.name === 'iframe' // eslint-disable-line
// #endif
) {
this.expose()
} else if (node.name === 'video') {
if ((styleObj.height || '').includes('auto')) {
styleObj.height = undefined
}
/* #ifdef APP-PLUS */
let str = '<video style="width:100%;height:100%"'
for (const item in attrs) {
if (attrs[item]) {
str += ' ' + item + '="' + attrs[item] + '"'
}
}
if (this.options.pauseVideo) {
str += ' onplay="this.dispatchEvent(new CustomEvent(\'vplay\',{bubbles:!0}));for(var e=document.getElementsByTagName(\'video\'),t=0;t<e.length;t++)e[t]!=this&&e[t].pause()"'
}
str += '>'
for (let i = 0; i < node.src.length; i++) {
str += '<source src="' + node.src[i] + '">'
}
str += '</video>'
node.html = str
/* #endif */
} else if ((node.name === 'ul' || node.name === 'ol') && node.c) {
// 列表处理
const types = {
a: 'lower-alpha',
A: 'upper-alpha',
i: 'lower-roman',
I: 'upper-roman'
}
if (types[attrs.type]) {
attrs.style += ';list-style-type:' + types[attrs.type]
attrs.type = undefined
}
for (let i = children.length; i--;) {
if (children[i].name === 'li') {
children[i].c = 1
}
}
} else if (node.name === 'table') {
// 表格处理
// cellpadding、cellspacing、border 这几个常用表格属性需要通过转换实现
let padding = parseFloat(attrs.cellpadding)
let spacing = parseFloat(attrs.cellspacing)
const border = parseFloat(attrs.border)
const bordercolor = styleObj['border-color']
const borderstyle = styleObj['border-style']
if (node.c) {
// padding 和 spacing 默认 2
if (isNaN(padding)) {
padding = 2
}
if (isNaN(spacing)) {
spacing = 2
}
}
if (border) {
attrs.style += `;border:${border}px ${borderstyle || 'solid'} ${bordercolor || 'gray'}`
}
if (node.flag && node.c) {
// 有 colspan 或 rowspan 且含有链接的表格通过 grid 布局实现
styleObj.display = 'grid'
if (styleObj['border-collapse'] === 'collapse') {
styleObj['border-collapse'] = undefined
spacing = 0
}
if (spacing) {
styleObj['grid-gap'] = spacing + 'px'
styleObj.padding = spacing + 'px'
} else if (border) {
// 无间隔的情况下避免边框重叠
attrs.style += ';border-left:0;border-top:0'
}
const width = [] // 表格的列宽
const trList = [] // tr 列表
const cells = [] // 保存新的单元格
const map = {}; // 被合并单元格占用的格子
(function traversal (nodes) {
for (let i = 0; i < nodes.length; i++) {
if (nodes[i].name === 'tr') {
trList.push(nodes[i])
} else if (nodes[i].name === 'colgroup') {
let colI = 1
for (const col of (nodes[i].children || [])) {
if (col.name === 'col') {
const style = col.attrs.style || ''
const start = style.indexOf('width') ? style.indexOf(';width') : 0
// 提取出宽度
if (start !== -1) {
let end = style.indexOf(';', start + 6)
if (end === -1) {
end = style.length
}
width[colI] = style.substring(start ? start + 7 : 6, end)
}
colI += 1
}
}
} else {
traversal(nodes[i].children || [])
}
}
})(children)
for (let row = 1; row <= trList.length; row++) {
let col = 1
for (let j = 0; j < trList[row - 1].children.length; j++) {
const td = trList[row - 1].children[j]
if (td.name === 'td' || td.name === 'th') {
// 这个格子被上面的单元格占用,则列号++
while (map[row + '.' + col]) {
col++
}
let style = td.attrs.style || ''
let start = style.indexOf('width') ? style.indexOf(';width') : 0
// 提取出 td 的宽度
if (start !== -1) {
let end = style.indexOf(';', start + 6)
if (end === -1) {
end = style.length
}
if (!td.attrs.colspan) {
width[col] = style.substring(start ? start + 7 : 6, end)
}
style = style.substr(0, start) + style.substr(end)
}
// 设置竖直对齐
style += ';display:flex'
start = style.indexOf('vertical-align')
if (start !== -1) {
const val = style.substr(start + 15, 10)
if (val.includes('middle')) {
style += ';align-items:center'
} else if (val.includes('bottom')) {
style += ';align-items:flex-end'
}
} else {
style += ';align-items:center'
}
// 设置水平对齐
start = style.indexOf('text-align')
if (start !== -1) {
const val = style.substr(start + 11, 10)
if (val.includes('center')) {
style += ';justify-content: center'
} else if (val.includes('right')) {
style += ';justify-content: right'
}
}
style = (border ? `;border:${border}px ${borderstyle || 'solid'} ${bordercolor || 'gray'}` + (spacing ? '' : ';border-right:0;border-bottom:0') : '') + (padding ? `;padding:${padding}px` : '') + ';' + style
// 处理列合并
if (td.attrs.colspan) {
style += `;grid-column-start:${col};grid-column-end:${col + parseInt(td.attrs.colspan)}`
if (!td.attrs.rowspan) {
style += `;grid-row-start:${row};grid-row-end:${row + 1}`
}
col += parseInt(td.attrs.colspan) - 1
}
// 处理行合并
if (td.attrs.rowspan) {
style += `;grid-row-start:${row};grid-row-end:${row + parseInt(td.attrs.rowspan)}`
if (!td.attrs.colspan) {
style += `;grid-column-start:${col};grid-column-end:${col + 1}`
}
// 记录下方单元格被占用
for (let rowspan = 1; rowspan < td.attrs.rowspan; rowspan++) {
for (let colspan = 0; colspan < (td.attrs.colspan || 1); colspan++) {
map[(row + rowspan) + '.' + (col - colspan)] = 1
}
}
}
if (style) {
td.attrs.style = style
}
cells.push(td)
col++
}
}
if (row === 1) {
let temp = ''
for (let i = 1; i < col; i++) {
temp += (width[i] ? width[i] : 'auto') + ' '
}
styleObj['grid-template-columns'] = temp
}
}
node.children = cells
} else {
// 没有使用合并单元格的表格通过 table 布局实现
if (node.c) {
styleObj.display = 'table'
}
if (!isNaN(spacing)) {
styleObj['border-spacing'] = spacing + 'px'
}
if (border || padding) {
// 遍历
(function traversal (nodes) {
for (let i = 0; i < nodes.length; i++) {
const td = nodes[i]
if (td.name === 'th' || td.name === 'td') {
if (border) {
td.attrs.style = `border:${border}px ${borderstyle || 'solid'} ${bordercolor || 'gray'};${td.attrs.style || ''}`
}
if (padding) {
td.attrs.style = `padding:${padding}px;${td.attrs.style || ''}`
}
} else if (td.children) {
traversal(td.children)
}
}
})(children)
}
}
// 给表格添加一个单独的横向滚动层
if (this.options.scrollTable && !(attrs.style || '').includes('inline')) {
const table = Object.assign({}, node)
node.name = 'div'
node.attrs = {
style: 'overflow:auto'
}
node.children = [table]
attrs = table.attrs
}
} else if ((node.name === 'tbody' || node.name === 'tr') && node.flag && node.c) {
node.flag = undefined;
(function traversal (nodes) {
for (let i = 0; i < nodes.length; i++) {
if (nodes[i].name === 'td') {
// 颜色样式设置给单元格避免丢失
for (const style of ['color', 'background', 'background-color']) {
if (styleObj[style]) {
nodes[i].attrs.style = style + ':' + styleObj[style] + ';' + (nodes[i].attrs.style || '')
}
}
} else {
traversal(nodes[i].children || [])
}
}
})(children)
} else if ((node.name === 'td' || node.name === 'th') && (attrs.colspan || attrs.rowspan)) {
for (let i = this.stack.length; i--;) {
if (this.stack[i].name === 'table' || this.stack[i].name === 'tbody' || this.stack[i].name === 'tr') {
this.stack[i].flag = 1 // 指示含有合并单元格
}
}
} else if (node.name === 'ruby') {
// 转换 ruby
node.name = 'span'
for (let i = 0; i < children.length - 1; i++) {
if (children[i].type === 'text' && children[i + 1].name === 'rt') {
children[i] = {
name: 'div',
attrs: {
style: 'display:inline-block;text-align:center'
},
children: [{
name: 'div',
attrs: {
style: 'font-size:50%;' + (children[i + 1].attrs.style || '')
},
children: children[i + 1].children
}, children[i]]
}
children.splice(i + 1, 1)
}
}
} else if (node.c) {
(function traversal (node) {
node.c = 2
for (let i = node.children.length; i--;) {
const child = node.children[i]
// #ifdef (MP-WEIXIN || MP-QQ || APP-PLUS || MP-360) && VUE3
if (child.name && (config.inlineTags[child.name] || ((child.attrs.style || '').includes('inline') && child.children)) && !child.c) {
traversal(child)
}
// #endif
if (!child.c || child.name === 'table') {
node.c = 1
}
}
})(node)
}
if ((styleObj.display || '').includes('flex') && !node.c) {
for (let i = children.length; i--;) {
const item = children[i]
if (item.f) {
item.attrs.style = (item.attrs.style || '') + item.f
item.f = undefined
}
}
}
// flex 布局时部分样式需要提取到 rich-text 外层
const flex = parent && ((parent.attrs.style || '').includes('flex') || (parent.attrs.style || '').includes('grid'))
// #ifdef MP-WEIXIN
// 检查基础库版本 virtualHost 是否可用
&& !(node.c && wx.getNFCAdapter) // eslint-disable-line
// #endif
// #ifndef MP-WEIXIN || MP-QQ || MP-BAIDU || MP-TOUTIAO
&& !node.c // eslint-disable-line
// #endif
if (flex) {
node.f = ';max-width:100%'
}
if (children.length >= 50 && node.c && !(styleObj.display || '').includes('flex')) {
mergeNodes(children)
}
// #endif
for (const key in styleObj) {
if (styleObj[key]) {
const val = `;${key}:${styleObj[key].replace(' !important', '')}`
/* #ifndef APP-PLUS-NVUE */
if (flex && ((key.includes('flex') && key !== 'flex-direction') || key === 'align-self' || key.includes('grid') || styleObj[key][0] === '-' || (key.includes('width') && val.includes('%')))) {
node.f += val
if (key === 'width') {
attrs.style += ';width:100%'
}
} else /* #endif */ {
attrs.style += val
}
}
}
attrs.style = attrs.style.substr(1) || undefined
// #ifdef (MP-WEIXIN || MP-QQ) && VUE3
for (const key in attrs) {
if (!attrs[key]) {
delete attrs[key]
}
}
// #endif
}
/**
* @description 解析到文本
* @param {String} text 文本内容
*/
Parser.prototype.onText = function (text) {
if (!this.pre) {
// 合并空白符
let trim = ''
let flag
for (let i = 0, len = text.length; i < len; i++) {
if (!blankChar[text[i]]) {
trim += text[i]
} else {
if (trim[trim.length - 1] !== ' ') {
trim += ' '
}
if (text[i] === '\n' && !flag) {
flag = true
}
}
}
// 去除含有换行符的空串
if (trim === ' ') {
if (flag) return
// #ifdef VUE3
else {
const parent = this.stack[this.stack.length - 1]
if (parent && parent.name[0] === 't') return
}
// #endif
}
text = trim
}
const node = Object.create(null)
node.type = 'text'
// #ifdef (MP-BAIDU || MP-ALIPAY || MP-TOUTIAO) && VUE3
node.attrs = {}
// #endif
node.text = decodeEntity(text)
if (this.hook(node)) {
// #ifdef MP-WEIXIN
if (this.options.selectable === 'force' && system.includes('iOS') && !uni.canIUse('rich-text.user-select')) {
this.expose()
}
// #endif
const siblings = this.stack.length ? this.stack[this.stack.length - 1].children : this.nodes
siblings.push(node)
}
}
/**
* @description html 词法分析器
* @param {Object} handler 高层处理器
*/
function Lexer (handler) {
this.handler = handler
}
/**
* @description 执行解析
* @param {String} content 要解析的文本
*/
Lexer.prototype.parse = function (content) {
this.content = content || ''
this.i = 0 // 标记解析位置
this.start = 0 // 标记一个单词的开始位置
this.state = this.text // 当前状态
for (let len = this.content.length; this.i !== -1 && this.i < len;) {
this.state()
}
}
/**
* @description 检查标签是否闭合
* @param {String} method 如果闭合要进行的操作
* @returns {Boolean} 是否闭合
* @private
*/
Lexer.prototype.checkClose = function (method) {
const selfClose = this.content[this.i] === '/'
if (this.content[this.i] === '>' || (selfClose && this.content[this.i + 1] === '>')) {
if (method) {
this.handler[method](this.content.substring(this.start, this.i))
}
this.i += selfClose ? 2 : 1
this.start = this.i
this.handler.onOpenTag(selfClose)
if (this.handler.tagName === 'script') {
this.i = this.content.indexOf('</', this.i)
if (this.i !== -1) {
this.i += 2
this.start = this.i
}
this.state = this.endTag
} else {
this.state = this.text
}
return true
}
return false
}
/**
* @description 文本状态
* @private
*/
Lexer.prototype.text = function () {
this.i = this.content.indexOf('<', this.i) // 查找最近的标签
if (this.i === -1) {
// 没有标签了
if (this.start < this.content.length) {
this.handler.onText(this.content.substring(this.start, this.content.length))
}
return
}
const c = this.content[this.i + 1]
if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) {
// 标签开头
if (this.start !== this.i) {
this.handler.onText(this.content.substring(this.start, this.i))
}
this.start = ++this.i
this.state = this.tagName
} else if (c === '/' || c === '!' || c === '?') {
if (this.start !== this.i) {
this.handler.onText(this.content.substring(this.start, this.i))
}
const next = this.content[this.i + 2]
if (c === '/' && ((next >= 'a' && next <= 'z') || (next >= 'A' && next <= 'Z'))) {
// 标签结尾
this.i += 2
this.start = this.i
this.state = this.endTag
return
}
// 处理注释
let end = '-->'
if (c !== '!' || this.content[this.i + 2] !== '-' || this.content[this.i + 3] !== '-') {
end = '>'
}
this.i = this.content.indexOf(end, this.i)
if (this.i !== -1) {
this.i += end.length
this.start = this.i
}
} else {
this.i++
}
}
/**
* @description 标签名状态
* @private
*/
Lexer.prototype.tagName = function () {
if (blankChar[this.content[this.i]]) {
// 解析到标签名
this.handler.onTagName(this.content.substring(this.start, this.i))
while (blankChar[this.content[++this.i]]);
if (this.i < this.content.length && !this.checkClose()) {
this.start = this.i
this.state = this.attrName
}
} else if (!this.checkClose('onTagName')) {
this.i++
}
}
/**
* @description 属性名状态
* @private
*/
Lexer.prototype.attrName = function () {
let c = this.content[this.i]
if (blankChar[c] || c === '=') {
// 解析到属性名
this.handler.onAttrName(this.content.substring(this.start, this.i))
let needVal = c === '='
const len = this.content.length
while (++this.i < len) {
c = this.content[this.i]
if (!blankChar[c]) {
if (this.checkClose()) return
if (needVal) {
// 等号后遇到第一个非空字符
this.start = this.i
this.state = this.attrVal
return
}
if (this.content[this.i] === '=') {
needVal = true
} else {
this.start = this.i
this.state = this.attrName
return
}
}
}
} else if (!this.checkClose('onAttrName')) {
this.i++
}
}
/**
* @description 属性值状态
* @private
*/
Lexer.prototype.attrVal = function () {
const c = this.content[this.i]
const len = this.content.length
if (c === '"' || c === "'") {
// 有冒号的属性
this.start = ++this.i
this.i = this.content.indexOf(c, this.i)
if (this.i === -1) return
this.handler.onAttrVal(this.content.substring(this.start, this.i))
} else {
// 没有冒号的属性
for (; this.i < len; this.i++) {
if (blankChar[this.content[this.i]]) {
this.handler.onAttrVal(this.content.substring(this.start, this.i))
break
} else if (this.checkClose('onAttrVal')) return
}
}
while (blankChar[this.content[++this.i]]);
if (this.i < len && !this.checkClose()) {
this.start = this.i
this.state = this.attrName
}
}
/**
* @description 结束标签状态
* @returns {String} 结束的标签名
* @private
*/
Lexer.prototype.endTag = function () {
const c = this.content[this.i]
if (blankChar[c] || c === '>' || c === '/') {
this.handler.onCloseTag(this.content.substring(this.start, this.i))
if (c !== '>') {
this.i = this.content.indexOf('>', this.i)
if (this.i === -1) return
}
this.start = ++this.i
this.state = this.text
} else {
this.i++
}
}
export default Parser
{
"id": "mp-html",
"displayName": "mp-html 富文本组件【全端支持,支持编辑、latex等扩展】",
"version": "v2.5.1",
"description": "一个强大的富文本组件,高效轻量,功能丰富",
"keywords": [
"富文本",
"编辑器",
"html",
"rich-text",
"editor"
],
"repository": "https://github.com/jin-yufeng/mp-html",
"dcloudext": {
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "无",
"permissions": "无"
},
"npmurl": "https://www.npmjs.com/package/mp-html",
"type": "component-vue"
},
"uni_modules": {
"platforms": {
"cloud": {
"tcb": "y",
"aliyun": "y",
"alipay": "n"
},
"client": {
"App": {
"app-vue": "y",
"app-nvue": "y",
"app-harmony": "u",
"app-uvue": "u"
},
"H5-mobile": {
"Safari": "y",
"Android Browser": "y",
"微信浏览器(Android)": "y",
"QQ浏览器(Android)": "y"
},
"H5-pc": {
"Chrome": "y",
"IE": "u",
"Edge": "y",
"Firefox": "y",
"Safari": "y"
},
"小程序": {
"微信": "y",
"阿里": "y",
"百度": "y",
"字节跳动": "y",
"QQ": "y"
},
"快应用": {
"华为": "y",
"联盟": "y"
},
"Vue": {
"vue2": "y",
"vue3": "y"
}
}
}
}
}
\ No newline at end of file
......@@ -270,6 +270,9 @@ const handleToolClick = async (item) => {
buttonName: item.title,
});
// 跳转产检提醒页面判断
let listData = userStore.babyInfo.allBabyBaseInfo || []
if (item.title === "医生问诊") {
if (!cfgStatus.value.isRegister) return;
......@@ -301,8 +304,20 @@ const handleToolClick = async (item) => {
},
},
});
} else if(item.title === "产检提醒"){
if(listData.length > 0){
const hasPregnancy = listData.some(item => item.typeName === "孕中" && item.selected === true )
console.log(hasPregnancy, 'hasPregnancy')
if(hasPregnancy){
jump({ type: item.link.type, url: item.link.url});
} else{
uni.showToast({
title: "需要是孕中状态哦",
icon: "none",
});
}
}
} else {
const extra = item.link.extra;
if(extra && extra.babyId){
jump({ type: item.link.type, url: item.link.url+'?babyId='+extra.babyId});
......@@ -338,7 +353,7 @@ const handleEditProfile = (e) => {
)?.id;
if (type === "edit") {
navigateTo(`/pages/person/person?type=${type}&id=${babyId}`);
navigateTo(`/pages/person/person?type=${type}&id=${babyId.value}`);
} else {
navigateTo(`/pages/person/person?type=${type}`);
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment