Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
飞
飞鹤小程序
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Commits
Issue Boards
Open sidebar
FH
飞鹤小程序
Commits
48329f8d
Commit
48329f8d
authored
Jul 29, 2025
by
王炽
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch 'dev' of
http://gitlab2.dui88.com/fh/20250528_FHQ1
into dev
parents
4063e012
c88545da
Changes
17
Hide whitespace changes
Inline
Side-by-side
Showing
17 changed files
with
3791 additions
and
532 deletions
+3791
-532
obstetric.js
api/obstetric.js
+8
-2
addPostnatal.vue
pages/addPostnatal/addPostnatal.vue
+10
-2
editPostnatal.vue
pages/editPostnatal/editPostnatal.vue
+2
-1
feedingAnalysis.vue
pages/feedingAnalysis/feedingAnalysis.vue
+439
-325
feedingIndex.vue
pages/feedingIndex/feedingIndex.vue
+275
-143
feedingRecord.vue
pages/feedingRecord/feedingRecord.vue
+56
-36
myReportCard.vue
pages/myReportCard/myReportCard.vue
+7
-2
postnatalCheckUp.vue
pages/postnatalCheckUp/postnatalCheckUp.vue
+9
-3
productionCalendar.vue
pages/productionCalendar/productionCalendar.vue
+22
-8
productionDetails.vue
pages/productionDetails/productionDetails.vue
+24
-8
README.md
uni_modules/mp-html/README.md
+192
-0
changelog.md
uni_modules/mp-html/changelog.md
+156
-0
mp-html.vue
uni_modules/mp-html/components/mp-html/mp-html.vue
+498
-0
node.vue
uni_modules/mp-html/components/mp-html/node/node.vue
+597
-0
parser.js
uni_modules/mp-html/components/mp-html/parser.js
+1400
-0
package.json
uni_modules/mp-html/package.json
+79
-0
My.vue
views/My.vue
+17
-2
No files found.
api/obstetric.js
View file @
48329f8d
...
...
@@ -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
pages/addPostnatal/addPostnatal.vue
View file @
48329f8d
...
...
@@ -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
}
...
...
pages/editPostnatal/editPostnatal.vue
View file @
48329f8d
...
...
@@ -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
}
...
...
pages/feedingAnalysis/feedingAnalysis.vue
View file @
48329f8d
...
...
@@ -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.
isActiv
e && day.totalCount > 0"
>
<view
class=
"bar-count"
v-if=
"day.
dateString === selectedDat
e && 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
+
`feeding
Record
/
${
version
}
/icon_l_arrow.png`
,
icon_r_arrow
:
$baseUrl
+
`feeding
Record
/
${
version
}
/icon_r_arrow.png`
,
icon_sel_tag_bg
:
$baseUrl
+
`feeding
Record
/
${
version
}
/icon_sel_tag_bg.png`
,
icon_l_arrow
:
$baseUrl
+
`feeding
Analysis
/
${
version
}
/icon_l_arrow.png`
,
icon_r_arrow
:
$baseUrl
+
`feeding
Analysis
/
${
version
}
/icon_r_arrow.png`
,
icon_sel_tag_bg
:
$baseUrl
+
`feeding
Analysis
/
${
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
.
breast
f
eedingCount
||
0
,
'母乳瓶喂'
:
item
.
bottleCount
||
0
,
'奶粉喂养'
:
item
.
formula
Count
||
0
,
'辅食'
:
item
.
f
oodCount
||
0
'母乳亲喂'
:
item
.
breast
F
eedingCount
||
0
,
'母乳瓶喂'
:
item
.
bottle
Feeding
Count
||
0
,
'奶粉喂养'
:
item
.
milkFeeding
Count
||
0
,
'辅食'
:
item
.
babyF
oodCount
||
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
:
4
0rpx
;
color
:
#000000
;
// 黑色箭头
font-weight
:
bold
;
line-height
:
1
;
width
:
2
0rpx
;
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
:
70
rpx
;
height
:
70
rpx
;
background
:
#FDFBF7
;
// 浅米色背景
border-radius
:
35rpx
;
width
:
42
rpx
;
height
:
144
rpx
;
//
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
:
22
0rpx
;
background
:
transparent
;
// 去掉背景色
border-radius
:
16rpx
;
padding
:
20rpx
;
//
flex: 1;
//
margin: 0 25rpx;
height
:
50
0rpx
;
//
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
:
60
rpx
;
height
:
12
0rpx
;
width
:
77
rpx
;
height
:
36
0rpx
;
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
:
-
3
0rpx
;
// 调整位置到柱子顶部上方
top
:
-
4
0rpx
;
// 调整位置到柱子顶部上方
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
;
}
...
...
pages/feedingIndex/feedingIndex.vue
View file @
48329f8d
...
...
@@ -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
>=
3
6
00
)
{
// 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
>=
3
6
00
)
{
// 3600秒 = 60分钟
if
(
timerData
.
value
.
rightDuration
>=
300
)
{
// 3600秒 = 60分钟
console
.
log
(
'右侧计时器达到60分钟上限,自动暂停'
)
stopRightTimer
()
isRightTimerRunning
.
value
=
false
...
...
@@ -1458,9 +1471,9 @@ async function completeRecord() {
clearFeedingData
()
// 显示成功弹窗
s
howSuccessPopup
.
value
=
true
s
uccessPopup
.
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() {
}
// 显示成功弹窗
s
howSuccessPopup
.
value
=
true
s
uccessPopup
.
value
.
open
()
}
catch
(
error
)
{
console
.
error
(
'保存语音记录失败:'
,
error
)
...
...
@@ -2536,17 +2626,14 @@ async function completeVoiceRecord() {
// 记录成功弹窗相关方法
function
onSuccessJump
()
{
// 跳转到喂养记录页面
uni
.
navigateTo
({
url
:
'/pages/feedingRecord/feedingRecord'
})
uni
.
navigateBack
()
// 关闭弹窗
s
howSuccessPopup
.
value
=
false
s
uccessPopup
.
value
.
close
()
}
function
onSuccessClose
()
{
// 关闭弹窗
s
howSuccessPopup
.
value
=
false
s
uccessPopup
.
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
:
12
0rpx
;
z-index
:
1000
;
bottom
:
0
rpx
;
left
:
0
rpx
;
right
:
0
rpx
;
width
:
75
0rpx
;
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
:
8
0rpx
;
min-width
:
6
0rpx
;
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
pages/feedingRecord/feedingRecord.vue
View file @
48329f8d
...
...
@@ -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
:
12
rpx
;
border-radius
:
50
rpx
;
font-size
:
32rpx
;
border
:
none
;
font-weight
:
600
;
...
...
pages/myReportCard/myReportCard.vue
View file @
48329f8d
...
...
@@ -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
()
=>
{
cons
ole
.
log
(
'获取报告单'
)
cons
t
{
code
,
message
,
data
,
success
}
=
await
getReportList
(
)
cons
t
{
code
,
message
,
data
,
success
}
=
await
getReportList
({
babyId
:
babyId
.
value
}
)
cons
ole
.
log
(
'报告单列表'
,
code
,
message
,
data
,
success
)
if
(
success
)
{
listData
.
value
=
data
}
else
{
...
...
pages/postnatalCheckUp/postnatalCheckUp.vue
View file @
48329f8d
...
...
@@ -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/icon
6
.png`
'completed'
:
`
${
$baseUrl
}
chanjianTool/1001/icon
30
.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
({
...
...
pages/productionCalendar/productionCalendar.vue
View file @
48329f8d
...
...
@@ -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
,
...
...
pages/productionDetails/productionDetails.vue
View file @
48329f8d
...
...
@@ -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
,
report
Images
:
bgdImgList
.
value
keep
Images
:
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
...
...
uni_modules/mp-html/README.md
0 → 100644
View file @
48329f8d
## 为减小组件包的大小,默认组件包中不包含编辑、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
)
了解更多
uni_modules/mp-html/changelog.md
0 → 100644
View file @
48329f8d
## 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
)
uni_modules/mp-html/components/mp-html/mp-html.vue
0 → 100644
View file @
48329f8d
<
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
(
/&/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
>
uni_modules/mp-html/components/mp-html/node/node.vue
0 → 100644
View file @
48329f8d
<
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
uni_modules/mp-html/components/mp-html/parser.js
0 → 100644
View file @
48329f8d
/**
* @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 要不要解码 &
* @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
]
===
'#'
)
{
// { 形式的实体
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
{
// 形式的实体
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
uni_modules/mp-html/package.json
0 → 100644
View file @
48329f8d
{
"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
views/My.vue
View file @
48329f8d
...
...
@@ -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
}
`
);
}
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment