35 changed files with 4170 additions and 992 deletions
-
6app.json
-
77pages/home/home.js
-
23pages/home/home.wxml
-
16pages/home/home.wxss
-
2pages/personal/personal.wxml
-
8pages/personal/personal.wxss
-
BINpagesA/images/1.png
-
BINpagesA/images/2.png
-
BINpagesA/images/3.png
-
BINpagesA/images/5.png
-
BINpagesA/images/6.jpg
-
286pagesB/pages/administrativeDivision/administrativeDivision.js
-
4pagesB/pages/administrativeDivision/administrativeDivision.json
-
95pagesB/pages/administrativeDivision/administrativeDivision.wxml
-
316pagesB/pages/administrativeDivision/administrativeDivision.wxss
-
305pagesB/pages/onlineAsk/onlineAsk.js
-
4pagesB/pages/onlineAsk/onlineAsk.json
-
130pagesB/pages/onlineAsk/onlineAsk.wxml
-
386pagesB/pages/onlineAsk/onlineAsk.wxss
-
484pagesB/pages/policyElucidation/policyElucidation.js
-
61pagesB/pages/policyElucidation/policyElucidation.wxml
-
112pagesB/pages/policyElucidation/policyElucidation.wxss
-
381pagesB/pages/spDetails/spDetails.js
-
4pagesB/pages/spDetails/spDetails.json
-
174pagesB/pages/spDetails/spDetails.wxml
-
664pagesB/pages/spDetails/spDetails.wxss
-
746pagesB/pages/training/training.js
-
110pagesB/pages/training/training.wxml
-
240pagesB/pages/training/training.wxss
-
99pagesB/pages/wzDetails/wzDetails.js
-
4pagesB/pages/wzDetails/wzDetails.json
-
62pagesB/pages/wzDetails/wzDetails.wxml
-
322pagesB/pages/wzDetails/wzDetails.wxss
-
2project.private.config.json
-
39utils/api.js
|
Before Width: 556 | Height: 674 | Size: 368 KiB |
|
Before Width: 466 | Height: 678 | Size: 374 KiB |
|
Before Width: 459 | Height: 682 | Size: 285 KiB |
|
Before Width: 300 | Height: 200 | Size: 21 KiB |
|
Before Width: 600 | Height: 394 | Size: 37 KiB |
@ -0,0 +1,286 @@ |
|||
import http from '../../../utils/api' |
|||
|
|||
Page({ |
|||
/** |
|||
* 页面的初始数据 |
|||
*/ |
|||
data: { |
|||
regionList: [], // 当前显示的区域列表
|
|||
selectedRegions: [], // 已选择的区域路径
|
|||
currentParentCode: '', // 当前父级code
|
|||
loading: false, |
|||
showPicker: false, |
|||
regionTitle: '请选择区域', |
|||
autoShowPicker: false // 新增:控制是否自动弹出选择器
|
|||
}, |
|||
|
|||
/** |
|||
* 生命周期函数--监听页面加载 |
|||
*/ |
|||
onLoad(options) { |
|||
// 首次加载,获取第一级数据
|
|||
this.getRegionData('', () => { |
|||
// 首次加载完成后自动弹出选择器
|
|||
this.setData({ |
|||
showPicker: true, |
|||
regionTitle: '请选择区域' |
|||
}) |
|||
}) |
|||
}, |
|||
|
|||
// 获取区域数据
|
|||
getRegionData(parentCode, callback) { |
|||
this.setData({ |
|||
loading: true |
|||
}) |
|||
|
|||
http.areaChildren({ |
|||
data: { |
|||
parentCode: parentCode || '' |
|||
}, |
|||
success: res => { |
|||
console.log('区域数据响应:', res) |
|||
if (res.code === 200 && res.data && res.data.length > 0) { |
|||
this.setData({ |
|||
regionList: res.data, |
|||
currentParentCode: parentCode, |
|||
loading: false |
|||
}) |
|||
} else { |
|||
// 没有更多数据了,说明已经是最后一级
|
|||
this.setData({ |
|||
loading: false, |
|||
regionList: [] |
|||
}) |
|||
|
|||
// 如果是最后一级,关闭选择器
|
|||
if (this.data.showPicker) { |
|||
this.setData({ |
|||
showPicker: false |
|||
}) |
|||
wx.showToast({ |
|||
title: '已选择到最后一级', |
|||
icon: 'none' |
|||
}) |
|||
} |
|||
} |
|||
|
|||
// 执行回调
|
|||
if (callback) { |
|||
callback() |
|||
} |
|||
}, |
|||
fail: err => { |
|||
console.error('请求失败:', err) |
|||
this.setData({ |
|||
loading: false |
|||
}) |
|||
wx.showToast({ |
|||
title: '加载失败', |
|||
icon: 'none' |
|||
}) |
|||
|
|||
if (callback) { |
|||
callback() |
|||
} |
|||
} |
|||
}) |
|||
}, |
|||
|
|||
// 开始选择(点击按钮时调用)
|
|||
startSelection() { |
|||
// 如果还没有选择任何区域,重新加载第一级
|
|||
if (this.data.selectedRegions.length === 0) { |
|||
this.getRegionData('', () => { |
|||
this.setData({ |
|||
showPicker: true, |
|||
regionTitle: '请选择区域' |
|||
}) |
|||
}) |
|||
} else { |
|||
// 如果有已选择的区域,加载下一级
|
|||
const lastRegion = this.data.selectedRegions[this.data.selectedRegions.length - 1] |
|||
this.loadAndShowNextLevel(lastRegion.code) |
|||
} |
|||
}, |
|||
|
|||
// 加载并显示下一级区域选择器
|
|||
loadAndShowNextLevel(parentCode) { |
|||
this.getRegionData(parentCode, () => { |
|||
if (this.data.regionList.length > 0) { |
|||
// 有下一级数据,自动弹出选择器
|
|||
this.setData({ |
|||
showPicker: true, |
|||
regionTitle: `请选择${this.data.selectedRegions[this.data.selectedRegions.length - 1].name}的下一级区域` |
|||
}) |
|||
} else { |
|||
// 没有下一级数据,提示用户
|
|||
wx.showToast({ |
|||
title: '已经是最后一级,无法继续选择', |
|||
icon: 'none' |
|||
}) |
|||
} |
|||
}) |
|||
}, |
|||
|
|||
// 关闭选择器
|
|||
closePicker() { |
|||
this.setData({ |
|||
showPicker: false |
|||
}) |
|||
}, |
|||
|
|||
// 选择区域
|
|||
selectRegion(e) { |
|||
const index = e.currentTarget.dataset.index |
|||
const region = this.data.regionList[index] |
|||
|
|||
console.log('选择的区域:', region) |
|||
|
|||
// 获取当前已选择的最后一级
|
|||
const lastSelectedRegion = this.data.selectedRegions.length > 0 ? |
|||
this.data.selectedRegions[this.data.selectedRegions.length - 1] : |
|||
null |
|||
|
|||
// 检查是否是同级选择(替换最后一级)
|
|||
const isSameLevel = lastSelectedRegion && |
|||
lastSelectedRegion.parentCode === region.parentCode |
|||
|
|||
let selectedRegions = [...this.data.selectedRegions] |
|||
|
|||
if (isSameLevel) { |
|||
// 同级选择,替换最后一级
|
|||
selectedRegions[selectedRegions.length - 1] = { |
|||
code: region.code, |
|||
name: region.name, |
|||
parentCode: region.parentCode |
|||
} |
|||
} else { |
|||
// 选择下一级,添加到路径
|
|||
selectedRegions.push({ |
|||
code: region.code, |
|||
name: region.name, |
|||
parentCode: region.parentCode |
|||
}) |
|||
} |
|||
|
|||
this.setData({ |
|||
selectedRegions |
|||
}) |
|||
|
|||
// 关闭当前选择器
|
|||
this.setData({ |
|||
showPicker: false |
|||
}) |
|||
|
|||
// 延迟一段时间后自动加载并显示下一级选择器
|
|||
setTimeout(() => { |
|||
this.loadAndShowNextLevel(region.code) |
|||
}, 300) |
|||
}, |
|||
|
|||
// 重新选择(点击已选择的任意层级)
|
|||
reSelectRegion(e) { |
|||
const index = e.currentTarget.dataset.index |
|||
|
|||
// 截断到指定级别(包括点击的层级)
|
|||
const selectedRegions = this.data.selectedRegions.slice(0, index + 1) |
|||
|
|||
this.setData({ |
|||
selectedRegions |
|||
}) |
|||
|
|||
// 获取该层级的数据
|
|||
const targetRegion = selectedRegions[selectedRegions.length - 1] |
|||
|
|||
// 获取点击层级的同级数据并显示选择器
|
|||
this.getRegionData(targetRegion.parentCode, () => { |
|||
this.setData({ |
|||
showPicker: true, |
|||
regionTitle: index === 0 ? |
|||
'请选择区域' : |
|||
`请选择${this.data.selectedRegions[index - 1]?.name || '区域'}的下一级` |
|||
}) |
|||
}) |
|||
}, |
|||
|
|||
// 完成选择
|
|||
completeSelection() { |
|||
if (this.data.selectedRegions.length === 0) { |
|||
wx.showToast({ |
|||
title: '请先选择区域', |
|||
icon: 'none' |
|||
}) |
|||
return |
|||
} |
|||
|
|||
const lastRegion = this.data.selectedRegions[this.data.selectedRegions.length - 1] |
|||
|
|||
// 这里可以调用你的业务接口,传递parentCode
|
|||
this.submitRegion(lastRegion.code) |
|||
}, |
|||
|
|||
// 提交选择的区域(示例)
|
|||
submitRegion(parentCode) { |
|||
console.log('提交的parentCode:', parentCode) |
|||
console.log('完整选择路径:', this.data.selectedRegions) |
|||
http.userCode({ |
|||
data: { |
|||
areaCode: parentCode |
|||
}, |
|||
success: res => { |
|||
console.log(11111, res); |
|||
if (res.code == 200) { |
|||
wx.showModal({ |
|||
title: '选择完成', |
|||
content: `已选择到: ${this.data.selectedRegions.map(r => r.name).join(' > ')}`, |
|||
showCancel: false, |
|||
success: (res) => { |
|||
if (res.confirm) { |
|||
wx.switchTab({ |
|||
url: '/pages/home/home', |
|||
}) |
|||
} |
|||
} |
|||
}) |
|||
} |
|||
} |
|||
}) |
|||
|
|||
|
|||
}, |
|||
|
|||
// 重置选择
|
|||
resetSelection() { |
|||
wx.showModal({ |
|||
title: '确认重置', |
|||
content: '确定要重置所有选择吗?', |
|||
success: (res) => { |
|||
if (res.confirm) { |
|||
this.setData({ |
|||
selectedRegions: [], |
|||
regionList: [], |
|||
currentParentCode: '', |
|||
showPicker: false |
|||
}) |
|||
|
|||
// 重新获取第一级数据
|
|||
this.getRegionData('', () => { |
|||
// 重置后自动弹出第一级选择器
|
|||
this.setData({ |
|||
showPicker: true, |
|||
regionTitle: '请选择区域' |
|||
}) |
|||
}) |
|||
} |
|||
} |
|||
}) |
|||
}, |
|||
|
|||
/** |
|||
* 生命周期函数--监听页面显示 |
|||
*/ |
|||
onShow() { |
|||
|
|||
} |
|||
}) |
|||
@ -0,0 +1,4 @@ |
|||
{ |
|||
"navigationBarTitleText":"区域选择", |
|||
"usingComponents": {} |
|||
} |
|||
@ -0,0 +1,95 @@ |
|||
<view class="container"> |
|||
<!-- 已选择区域路径显示 --> |
|||
<view class="selected-path" wx:if="{{selectedRegions.length > 0}}"> |
|||
<text class="path-title">已选择:</text> |
|||
<view class="path-items"> |
|||
<view |
|||
wx:for="{{selectedRegions}}" |
|||
wx:key="index" |
|||
class="path-item {{index === selectedRegions.length - 1 ? 'last' : ''}}" |
|||
data-index="{{index}}" |
|||
bindtap="reSelectRegion" |
|||
> |
|||
<text>{{item.name}}</text> |
|||
<text class="separator" wx:if="{{index < selectedRegions.length - 1}}">></text> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
|
|||
<!-- 选择按钮 --> |
|||
<view class="select-btn-container"> |
|||
<button |
|||
class="select-btn" |
|||
bindtap="startSelection" |
|||
loading="{{loading}}" |
|||
> |
|||
{{selectedRegions.length === 0 ? '开始选择区域' : '继续选择下一级'}} |
|||
</button> |
|||
</view> |
|||
|
|||
<!-- 操作按钮 --> |
|||
<view class="action-buttons"> |
|||
<button |
|||
class="btn complete-btn" |
|||
bindtap="completeSelection" |
|||
disabled="{{selectedRegions.length === 0}}" |
|||
> |
|||
完成选择 |
|||
</button> |
|||
<button |
|||
class="btn reset-btn" |
|||
bindtap="resetSelection" |
|||
wx:if="{{selectedRegions.length > 0}}" |
|||
> |
|||
重置 |
|||
</button> |
|||
</view> |
|||
|
|||
<!-- 选择器弹出层 --> |
|||
<view class="picker-modal" wx:if="{{showPicker}}"> |
|||
<view class="picker-mask" bindtap="closePicker"></view> |
|||
<view class="picker-content"> |
|||
<view class="picker-header"> |
|||
<text class="picker-title">{{regionTitle}}</text> |
|||
<text class="picker-close" bindtap="closePicker">×</text> |
|||
</view> |
|||
|
|||
<view class="picker-body"> |
|||
<!-- 加载状态 --> |
|||
<view class="loading-container" wx:if="{{loading}}"> |
|||
<view class="loading-spinner"></view> |
|||
<text class="loading-text">加载中...</text> |
|||
</view> |
|||
|
|||
<!-- 区域列表 --> |
|||
<scroll-view scroll-y class="region-list" wx:else> |
|||
<view |
|||
wx:for="{{regionList}}" |
|||
wx:key="id" |
|||
class="region-item" |
|||
data-index="{{index}}" |
|||
bindtap="selectRegion" |
|||
> |
|||
<view class="region-info"> |
|||
<text class="region-name">{{item.name}}</text> |
|||
</view> |
|||
<text class="arrow">›</text> |
|||
</view> |
|||
|
|||
<!-- 空状态 --> |
|||
<view class="empty-state" wx:if="{{regionList.length === 0}}"> |
|||
<text class="empty-text"> |
|||
{{selectedRegions.length > 0 ? '已选择到最后一级' : '暂无区域数据'}} |
|||
</text> |
|||
</view> |
|||
</scroll-view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
|
|||
<!-- 使用说明 --> |
|||
<view class="instruction"> |
|||
<text class="instruction-title">使用说明:</text> |
|||
<text class="instruction-text">1. 点击"开始选择区域"开始选择\n2. 选择后会加载下一级区域\n3. 点击已选择区域的任意级别可以重新选择\n4. 选择最后一级时会直接替换\n5. 完成选择后点击"完成选择"按钮</text> |
|||
</view> |
|||
</view> |
|||
@ -0,0 +1,316 @@ |
|||
.container { |
|||
padding: 30rpx; |
|||
min-height: 100vh; |
|||
background: #f5f5f5; |
|||
} |
|||
|
|||
/* 已选择路径 */ |
|||
.selected-path { |
|||
background: white; |
|||
border-radius: 16rpx; |
|||
padding: 30rpx; |
|||
margin-bottom: 30rpx; |
|||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05); |
|||
} |
|||
|
|||
.path-title { |
|||
font-size: 28rpx; |
|||
color: #666; |
|||
display: block; |
|||
margin-bottom: 20rpx; |
|||
} |
|||
|
|||
.path-items { |
|||
display: flex; |
|||
flex-wrap: wrap; |
|||
align-items: center; |
|||
margin-bottom: 20rpx; |
|||
} |
|||
|
|||
.path-item { |
|||
display: flex; |
|||
align-items: center; |
|||
padding: 12rpx 20rpx; |
|||
background: #f0f8ff; |
|||
border-radius: 8rpx; |
|||
margin-right: 10rpx; |
|||
margin-bottom: 10rpx; |
|||
cursor: pointer; |
|||
} |
|||
|
|||
.path-item:active { |
|||
background: #e1f0ff; |
|||
} |
|||
|
|||
.path-item.last { |
|||
background: #e6f7ff; |
|||
border: 1rpx solid #1890ff; |
|||
} |
|||
|
|||
.path-item text:first-child { |
|||
font-size: 28rpx; |
|||
color: #1890ff; |
|||
font-weight: 500; |
|||
} |
|||
|
|||
.separator { |
|||
margin-left: 10rpx; |
|||
color: #999; |
|||
} |
|||
|
|||
|
|||
/* 选择按钮 */ |
|||
.select-btn-container { |
|||
margin: 40rpx 0; |
|||
} |
|||
|
|||
.select-btn { |
|||
width: 100%; |
|||
background: linear-gradient(135deg, #1890ff, #096dd9); |
|||
color: white; |
|||
border-radius: 12rpx; |
|||
font-size: 32rpx; |
|||
height: 88rpx; |
|||
line-height: 88rpx; |
|||
border: none; |
|||
} |
|||
|
|||
.select-btn::after { |
|||
border: none; |
|||
} |
|||
|
|||
.select-btn[loading] { |
|||
opacity: 0.8; |
|||
} |
|||
|
|||
.select-btn:active { |
|||
opacity: 0.9; |
|||
} |
|||
|
|||
/* 操作按钮 */ |
|||
.action-buttons { |
|||
display: flex; |
|||
gap: 20rpx; |
|||
margin-top: 50rpx; |
|||
} |
|||
|
|||
.btn { |
|||
flex: 1; |
|||
border-radius: 12rpx; |
|||
font-size: 28rpx; |
|||
height: 80rpx; |
|||
line-height: 80rpx; |
|||
border: none; |
|||
} |
|||
|
|||
.btn::after { |
|||
border: none; |
|||
} |
|||
|
|||
.complete-btn { |
|||
background: #07c160; |
|||
color: white; |
|||
} |
|||
|
|||
.complete-btn[disabled] { |
|||
background: #ccc; |
|||
color: #999; |
|||
} |
|||
|
|||
.complete-btn:active:not([disabled]) { |
|||
background: #06ad56; |
|||
} |
|||
|
|||
.reset-btn { |
|||
background: #fff; |
|||
color: #ff4d4f; |
|||
border: 1rpx solid #ff4d4f !important; |
|||
} |
|||
|
|||
.reset-btn:active { |
|||
background: #fff5f5; |
|||
} |
|||
|
|||
/* 选择器模态框 */ |
|||
.picker-modal { |
|||
position: fixed; |
|||
top: 0; |
|||
left: 0; |
|||
right: 0; |
|||
bottom: 0; |
|||
z-index: 1000; |
|||
} |
|||
|
|||
.picker-mask { |
|||
position: absolute; |
|||
top: 0; |
|||
left: 0; |
|||
right: 0; |
|||
bottom: 0; |
|||
background: rgba(0, 0, 0, 0.5); |
|||
animation: fadeIn 0.3s ease; |
|||
} |
|||
|
|||
.picker-content { |
|||
position: absolute; |
|||
bottom: 0; |
|||
left: 0; |
|||
right: 0; |
|||
background: white; |
|||
border-radius: 32rpx 32rpx 0 0; |
|||
max-height: 70vh; |
|||
display: flex; |
|||
flex-direction: column; |
|||
animation: slideUp 0.3s ease; |
|||
} |
|||
|
|||
.picker-header { |
|||
padding: 32rpx 40rpx; |
|||
border-bottom: 1rpx solid #f0f0f0; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: space-between; |
|||
position: relative; |
|||
} |
|||
|
|||
.picker-title { |
|||
font-size: 32rpx; |
|||
color: #333; |
|||
font-weight: 600; |
|||
flex: 1; |
|||
text-align: center; |
|||
} |
|||
|
|||
.picker-close { |
|||
font-size: 48rpx; |
|||
color: #999; |
|||
position: absolute; |
|||
right: 30rpx; |
|||
top: 50%; |
|||
transform: translateY(-50%); |
|||
width: 60rpx; |
|||
height: 60rpx; |
|||
text-align: center; |
|||
line-height: 60rpx; |
|||
} |
|||
|
|||
.picker-close:active { |
|||
background: #f5f5f5; |
|||
border-radius: 50%; |
|||
} |
|||
|
|||
.picker-body { |
|||
flex: 1; |
|||
overflow: hidden; |
|||
} |
|||
|
|||
/* 加载状态 */ |
|||
.loading-container { |
|||
display: flex; |
|||
flex-direction: column; |
|||
align-items: center; |
|||
justify-content: center; |
|||
height: 300rpx; |
|||
} |
|||
|
|||
.loading-spinner { |
|||
width: 60rpx; |
|||
height: 60rpx; |
|||
border: 4rpx solid #f0f0f0; |
|||
border-top-color: #1890ff; |
|||
border-radius: 50%; |
|||
animation: spin 1s linear infinite; |
|||
margin-bottom: 20rpx; |
|||
} |
|||
|
|||
.loading-text { |
|||
font-size: 28rpx; |
|||
color: #999; |
|||
} |
|||
|
|||
/* 区域列表 */ |
|||
.region-list { |
|||
height: 60vh; |
|||
} |
|||
|
|||
.region-item { |
|||
padding: 32rpx 40rpx; |
|||
border-bottom: 1rpx solid #f0f0f0; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: space-between; |
|||
} |
|||
|
|||
.region-item:active { |
|||
background: #f5f5f5; |
|||
} |
|||
|
|||
.region-info { |
|||
flex: 1; |
|||
} |
|||
|
|||
.region-name { |
|||
font-size: 30rpx; |
|||
color: #333; |
|||
display: block; |
|||
margin-bottom: 8rpx; |
|||
} |
|||
|
|||
.arrow { |
|||
color: #ccc; |
|||
font-size: 36rpx; |
|||
margin-left: 20rpx; |
|||
} |
|||
|
|||
/* 空状态 */ |
|||
.empty-state { |
|||
display: flex; |
|||
justify-content: center; |
|||
align-items: center; |
|||
height: 200rpx; |
|||
} |
|||
|
|||
.empty-text { |
|||
font-size: 28rpx; |
|||
color: #999; |
|||
} |
|||
|
|||
/* 使用说明 */ |
|||
.instruction { |
|||
margin-top: 60rpx; |
|||
padding: 24rpx; |
|||
background: white; |
|||
border-radius: 12rpx; |
|||
border-left: 6rpx solid #1890ff; |
|||
} |
|||
|
|||
.instruction-title { |
|||
font-size: 28rpx; |
|||
color: #1890ff; |
|||
font-weight: 600; |
|||
display: block; |
|||
margin-bottom: 16rpx; |
|||
} |
|||
|
|||
.instruction-text { |
|||
font-size: 26rpx; |
|||
color: #666; |
|||
line-height: 1.6; |
|||
white-space: pre-line; |
|||
} |
|||
|
|||
/* 动画 */ |
|||
@keyframes fadeIn { |
|||
from { opacity: 0; } |
|||
to { opacity: 1; } |
|||
} |
|||
|
|||
@keyframes slideUp { |
|||
from { transform: translateY(100%); } |
|||
to { transform: translateY(0); } |
|||
} |
|||
|
|||
@keyframes spin { |
|||
from { transform: rotate(0deg); } |
|||
to { transform: rotate(360deg); } |
|||
} |
|||
@ -0,0 +1,305 @@ |
|||
// pages/forum/forum.js
|
|||
Page({ |
|||
data: { |
|||
// 帖子列表数据
|
|||
posts: [], |
|||
// 是否显示发帖弹窗
|
|||
showPostModal: false, |
|||
// 发帖标题
|
|||
postTitle: '', |
|||
// 发帖内容
|
|||
postContent: '', |
|||
// 标签输入
|
|||
tagInput: '', |
|||
// 已选标签
|
|||
selectedTags: [], |
|||
// 当前回复的帖子索引
|
|||
activeReplyIndex: -1, |
|||
// 回复内容
|
|||
replyContent: '', |
|||
// 加载状态
|
|||
loading: false, |
|||
}, |
|||
|
|||
onLoad: function() { |
|||
// 页面加载时获取帖子数据
|
|||
this.loadPosts(); |
|||
}, |
|||
|
|||
onPullDownRefresh: function() { |
|||
// 下拉刷新
|
|||
this.loadPosts(); |
|||
wx.stopPullDownRefresh(); |
|||
}, |
|||
|
|||
// 加载帖子数据
|
|||
loadPosts: function() { |
|||
this.setData({ loading: true }); |
|||
|
|||
// 模拟网络请求
|
|||
setTimeout(() => { |
|||
// 模拟数据
|
|||
const mockPosts = [ |
|||
{ |
|||
id: 1, |
|||
username: '技术爱好者', |
|||
avatar: 'https://img.yzcdn.cn/vant/cat.jpeg', |
|||
time: '2小时前', |
|||
title: '微信小程序如何实现图片上传和预览功能?', |
|||
content: '我正在开发一个微信小程序,需要实现图片上传功能,并且能够在上传前预览图片。请问有什么好的实现方案吗?上传的图片大小限制和格式有什么建议?', |
|||
tags: ['微信小程序', '图片上传', '前端开发'], |
|||
likeCount: 12, |
|||
replyCount: 5, |
|||
viewCount: 156, |
|||
liked: false, |
|||
solved: false, |
|||
showAllReplies: false, |
|||
replies: [ |
|||
{ |
|||
username: '前端开发工程师', |
|||
avatar: 'https://img.yzcdn.cn/vant/cat.jpeg', |
|||
time: '1小时前', |
|||
content: '可以使用wx.chooseImage选择图片,然后使用wx.uploadFile上传到服务器。预览功能可以使用wx.previewImage实现。' |
|||
}, |
|||
{ |
|||
username: '小程序开发者', |
|||
avatar: 'https://img.yzcdn.cn/vant/cat.jpeg', |
|||
time: '45分钟前', |
|||
content: '建议将图片大小限制在2MB以内,支持JPG、PNG格式。可以使用云开发存储功能简化上传流程。' |
|||
} |
|||
] |
|||
}, |
|||
{ |
|||
id: 2, |
|||
username: '产品经理小王', |
|||
avatar: 'https://img.yzcdn.cn/vant/cat.jpeg', |
|||
time: '昨天 14:30', |
|||
title: '如何设计一个用户友好的注册流程?', |
|||
content: '我们正在设计一个新产品的注册流程,希望既保证安全性又尽量简化步骤。大家有什么好的设计建议或参考案例吗?', |
|||
tags: ['产品设计', '用户体验', '注册流程'], |
|||
likeCount: 8, |
|||
replyCount: 3, |
|||
viewCount: 89, |
|||
liked: true, |
|||
solved: true, |
|||
showAllReplies: false, |
|||
replies: [ |
|||
{ |
|||
username: 'UX设计师', |
|||
avatar: 'https://img.yzcdn.cn/vant/cat.jpeg', |
|||
time: '昨天 16:45', |
|||
content: '建议采用手机号验证码注册,配合第三方登录选项。关键是将必填信息减到最少,其他信息可以后续引导补充。' |
|||
} |
|||
] |
|||
}, |
|||
{ |
|||
id: 3, |
|||
username: '后端开发', |
|||
avatar: 'https://img.yzcdn.cn/vant/cat.jpeg', |
|||
time: '3天前', |
|||
title: 'RESTful API设计的最佳实践有哪些?', |
|||
content: '我正在设计一组RESTful API,想了解一些最佳实践,比如URL命名规范、状态码使用、版本控制等方面有什么推荐的做法?', |
|||
tags: ['后端开发', 'API设计', 'RESTful'], |
|||
likeCount: 15, |
|||
replyCount: 7, |
|||
viewCount: 234, |
|||
liked: false, |
|||
solved: false, |
|||
showAllReplies: false, |
|||
replies: [ |
|||
{ |
|||
username: '架构师', |
|||
avatar: 'https://img.yzcdn.cn/vant/cat.jpeg', |
|||
time: '2天前', |
|||
content: '建议使用名词复数形式作为资源端点,合理使用HTTP状态码,API版本可以通过URL路径或请求头来管理。' |
|||
}, |
|||
{ |
|||
username: '全栈工程师', |
|||
avatar: 'https://img.yzcdn.cn/vant/cat.jpeg', |
|||
time: '1天前', |
|||
content: '不要忘记实现合适的错误处理机制,返回清晰的错误信息。同时考虑API限流和身份验证机制。' |
|||
}, |
|||
{ |
|||
username: '资深开发者', |
|||
avatar: 'https://img.yzcdn.cn/vant/cat.jpeg', |
|||
time: '1天前', |
|||
content: '推荐使用Swagger或OpenAPI规范来文档化你的API,这对前后端协作非常有帮助。' |
|||
} |
|||
] |
|||
} |
|||
]; |
|||
|
|||
this.setData({ |
|||
posts: mockPosts, |
|||
loading: false |
|||
}); |
|||
}, 800); |
|||
}, |
|||
|
|||
// 显示发帖弹窗
|
|||
showPostModal: function() { |
|||
this.setData({ |
|||
showPostModal: true |
|||
}); |
|||
}, |
|||
|
|||
// 隐藏发帖弹窗
|
|||
hidePostModal: function() { |
|||
this.setData({ |
|||
showPostModal: false, |
|||
postTitle: '', |
|||
postContent: '', |
|||
selectedTags: [], |
|||
tagInput: '' |
|||
}); |
|||
}, |
|||
|
|||
// 防止弹窗内点击事件冒泡
|
|||
preventTap: function() { |
|||
// 阻止事件冒泡
|
|||
}, |
|||
|
|||
// 发帖标题输入
|
|||
onPostTitleInput: function(e) { |
|||
this.setData({ |
|||
postTitle: e.detail.value |
|||
}); |
|||
}, |
|||
|
|||
// 发帖内容输入
|
|||
onPostContentInput: function(e) { |
|||
this.setData({ |
|||
postContent: e.detail.value |
|||
}); |
|||
}, |
|||
|
|||
|
|||
|
|||
|
|||
// 提交帖子
|
|||
submitPost: function() { |
|||
const { postTitle, postContent, selectedTags } = this.data; |
|||
|
|||
if (!postTitle.trim()) { |
|||
wx.showToast({ |
|||
title: '请输入标题', |
|||
icon: 'none' |
|||
}); |
|||
return; |
|||
} |
|||
|
|||
if (!postContent.trim()) { |
|||
wx.showToast({ |
|||
title: '请输入内容', |
|||
icon: 'none' |
|||
}); |
|||
return; |
|||
} |
|||
|
|||
// 创建新帖子
|
|||
const newPost = { |
|||
id: Date.now(), |
|||
username: '当前用户', |
|||
avatar: 'https://img.yzcdn.cn/vant/cat.jpeg', |
|||
time: '刚刚', |
|||
title: postTitle, |
|||
content: postContent, |
|||
tags: selectedTags, |
|||
likeCount: 0, |
|||
replyCount: 0, |
|||
viewCount: 0, |
|||
liked: false, |
|||
solved: false, |
|||
showAllReplies: false, |
|||
replies: [] |
|||
}; |
|||
|
|||
// 添加到帖子列表顶部
|
|||
const posts = this.data.posts; |
|||
posts.unshift(newPost); |
|||
|
|||
this.setData({ |
|||
posts: posts, |
|||
showPostModal: false, |
|||
postTitle: '', |
|||
postContent: '', |
|||
selectedTags: [], |
|||
tagInput: '' |
|||
}); |
|||
|
|||
wx.showToast({ |
|||
title: '发布成功', |
|||
icon: 'success' |
|||
}); |
|||
}, |
|||
|
|||
|
|||
|
|||
// 显示回复输入框
|
|||
showReplyInput: function(e) { |
|||
const index = e.currentTarget.dataset.index; |
|||
this.setData({ |
|||
activeReplyIndex: index, |
|||
replyContent: '' |
|||
}); |
|||
}, |
|||
|
|||
// 回复内容输入
|
|||
onReplyInput: function(e) { |
|||
this.setData({ |
|||
replyContent: e.detail.value |
|||
}); |
|||
}, |
|||
|
|||
// 提交回复
|
|||
submitReply: function(e) { |
|||
const index = e.currentTarget.dataset.index; |
|||
const replyContent = this.data.replyContent.trim(); |
|||
|
|||
if (!replyContent) { |
|||
wx.showToast({ |
|||
title: '请输入回复内容', |
|||
icon: 'none' |
|||
}); |
|||
return; |
|||
} |
|||
|
|||
const posts = this.data.posts; |
|||
const post = posts[index]; |
|||
|
|||
// 创建新回复
|
|||
const newReply = { |
|||
username: '当前用户', |
|||
avatar: 'https://img.yzcdn.cn/vant/cat.jpeg', |
|||
time: '刚刚', |
|||
content: replyContent |
|||
}; |
|||
|
|||
post.replies.push(newReply); |
|||
post.replyCount += 1; |
|||
|
|||
this.setData({ |
|||
posts: posts, |
|||
activeReplyIndex: -1, |
|||
replyContent: '' |
|||
}); |
|||
|
|||
wx.showToast({ |
|||
title: '回复成功', |
|||
icon: 'success' |
|||
}); |
|||
}, |
|||
|
|||
// 切换显示全部回复
|
|||
toggleReplies: function(e) { |
|||
const index = e.currentTarget.dataset.index; |
|||
const posts = this.data.posts; |
|||
const post = posts[index]; |
|||
|
|||
post.showAllReplies = !post.showAllReplies; |
|||
|
|||
this.setData({ |
|||
posts: posts |
|||
}); |
|||
} |
|||
}); |
|||
@ -0,0 +1,4 @@ |
|||
{ |
|||
"navigationBarTitleText": "问答论坛", |
|||
"usingComponents": {} |
|||
} |
|||
@ -0,0 +1,130 @@ |
|||
<view class="forum-page"> |
|||
|
|||
<!-- 顶部栏 --> |
|||
<view class="header"> |
|||
<view class="title">问答论坛</view> |
|||
<button class="add-btn" bindtap="showPostModal">发帖提问</button> |
|||
</view> |
|||
|
|||
<!-- 帖子列表 --> |
|||
<view class="post-list" wx:if="{{posts.length > 0}}"> |
|||
<block wx:for="{{posts}}" wx:key="id"> |
|||
<view class="post-item" data-index="{{index}}"> |
|||
<!-- 帖子头部 --> |
|||
<view class="post-header"> |
|||
<view class="user-info"> |
|||
<image class="avatar" src="{{item.avatar}}" mode="aspectFill"></image> |
|||
<view class="user-detail"> |
|||
<view class="username">{{item.username}}</view> |
|||
<view class="post-time">{{item.time}}</view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
|
|||
<!-- 帖子内容 --> |
|||
<view class="post-content"> |
|||
<view class="post-title">{{item.title}}</view> |
|||
<view class="post-desc">{{item.content}}</view> |
|||
|
|||
</view> |
|||
|
|||
|
|||
|
|||
<!-- 回复列表 --> |
|||
<view class="reply-list" wx:if="{{item.replies.length > 0}}"> |
|||
<view class="reply-title">共{{item.replies.length}}条回复</view> |
|||
<block wx:for="{{item.replies}}" wx:key="index"> |
|||
<view class="reply-item" wx:if="{{index < 2 || item.showAllReplies}}"> |
|||
<view class="reply-user"> |
|||
<image class="reply-avatar" src="{{item.avatar}}" mode="aspectFill"></image> |
|||
<view class="reply-user-info"> |
|||
<view class="reply-username">{{item.username}}</view> |
|||
<view class="reply-time">{{item.time}}</view> |
|||
</view> |
|||
</view> |
|||
<view class="reply-content">{{item.content}}</view> |
|||
</view> |
|||
</block> |
|||
|
|||
<!-- 查看更多回复 --> |
|||
<view class="view-more-replies" wx:if="{{item.replies.length > 2 && !item.showAllReplies}}" bindtap="toggleReplies" data-index="{{index}}"> |
|||
查看全部{{item.replies.length}}条回复 |
|||
</view> |
|||
|
|||
<!-- 收起回复 --> |
|||
<view class="view-more-replies" wx:if="{{item.replies.length > 2 && item.showAllReplies}}" bindtap="toggleReplies" data-index="{{index}}"> |
|||
收起回复 |
|||
</view> |
|||
</view> |
|||
|
|||
<!-- 回复输入区域(当前激活的帖子) --> |
|||
<view class="reply-input-container" wx:if="{{activeReplyIndex === index}}"> |
|||
<input |
|||
class="reply-input" |
|||
placeholder="输入您的回复..." |
|||
value="{{replyContent}}" |
|||
bindinput="onReplyInput" |
|||
focus="{{activeReplyIndex === index}}" |
|||
/> |
|||
<button class="send-reply-btn" bindtap="submitReply" data-index="{{index}}">发送</button> |
|||
</view> |
|||
</view> |
|||
</block> |
|||
</view> |
|||
|
|||
<!-- 空状态 --> |
|||
<view class="empty-state" wx:else> |
|||
<image class="empty-image" src="/images/empty.png" mode="aspectFit"></image> |
|||
<view class="empty-text">暂无帖子,快来发布第一个问题吧!</view> |
|||
<button class="empty-btn" bindtap="showPostModal">发布问题</button> |
|||
</view> |
|||
|
|||
<!-- 发帖弹窗 --> |
|||
<view class="modal-overlay" wx:if="{{showPostModal}}" bindtap="hidePostModal"> |
|||
<view class="modal-content" catchtap="preventTap"> |
|||
<view class="modal-header">发布新问题</view> |
|||
|
|||
<view class="form-item"> |
|||
<input |
|||
class="form-input" |
|||
placeholder="请输入问题标题" |
|||
value="{{postTitle}}" |
|||
bindinput="onPostTitleInput" |
|||
/> |
|||
</view> |
|||
|
|||
<view class="form-item"> |
|||
<textarea |
|||
class="form-textarea" |
|||
placeholder="请详细描述您的问题..." |
|||
value="{{postContent}}" |
|||
bindinput="onPostContentInput" |
|||
maxlength="500" |
|||
/> |
|||
<view class="textarea-counter">{{postContent.length}}/500</view> |
|||
</view> |
|||
|
|||
<view class="form-item"> |
|||
<view class="selected-tags" wx:if="{{selectedTags.length > 0}}"> |
|||
<block wx:for="{{selectedTags}}" wx:key="index"> |
|||
<view class="selected-tag"> |
|||
{{item}} |
|||
<text class="remove-tag" data-index="{{index}}" bindtap="removeTag">×</text> |
|||
</view> |
|||
</block> |
|||
</view> |
|||
</view> |
|||
|
|||
<view class="modal-buttons"> |
|||
<button class="modal-btn cancel" bindtap="hidePostModal">取消</button> |
|||
<button class="modal-btn submit" bindtap="submitPost" disabled="{{!postTitle || !postContent}}">发布</button> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
|
|||
<!-- 加载提示 --> |
|||
<view class="loading" wx:if="{{loading}}"> |
|||
<image class="loading-icon" src="/images/loading.png" mode="aspectFit"></image> |
|||
加载中... |
|||
</view> |
|||
</view> |
|||
@ -0,0 +1,386 @@ |
|||
.forum-page { |
|||
min-height: 100vh; |
|||
padding-bottom: 40rpx; |
|||
} |
|||
|
|||
/* 顶部栏 */ |
|||
.header { |
|||
display: flex; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
padding: 30rpx 30rpx 20rpx; |
|||
background-color: #fff; |
|||
border-bottom: 1rpx solid #eee; |
|||
position: sticky; |
|||
top: 0; |
|||
z-index: 10; |
|||
} |
|||
|
|||
.title { |
|||
font-size: 36rpx; |
|||
font-weight: bold; |
|||
color: #333; |
|||
} |
|||
|
|||
.add-btn { |
|||
background-color: #007AFF; |
|||
color: white; |
|||
font-size: 26rpx; |
|||
padding: 10rpx 20rpx; |
|||
line-height: normal; |
|||
border-radius: 30rpx; |
|||
margin: 0; |
|||
} |
|||
|
|||
.add-btn:after { |
|||
border: none; |
|||
} |
|||
|
|||
/* 帖子项 */ |
|||
.post-list { |
|||
padding: 20rpx 30rpx; |
|||
} |
|||
|
|||
.post-item { |
|||
background-color: white; |
|||
border-radius: 16rpx; |
|||
padding: 30rpx; |
|||
margin-bottom: 24rpx; |
|||
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05); |
|||
} |
|||
|
|||
.post-header { |
|||
display: flex; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
margin-bottom: 24rpx; |
|||
} |
|||
|
|||
.user-info { |
|||
display: flex; |
|||
align-items: center; |
|||
} |
|||
|
|||
.avatar { |
|||
width: 70rpx; |
|||
height: 70rpx; |
|||
border-radius: 50%; |
|||
margin-right: 20rpx; |
|||
} |
|||
|
|||
.user-detail { |
|||
display: flex; |
|||
flex-direction: column; |
|||
} |
|||
|
|||
.username { |
|||
font-size: 30rpx; |
|||
font-weight: bold; |
|||
color: #333; |
|||
margin-bottom: 6rpx; |
|||
} |
|||
|
|||
.post-time { |
|||
font-size: 24rpx; |
|||
color: #999; |
|||
} |
|||
|
|||
|
|||
|
|||
/* 帖子内容 */ |
|||
.post-content { |
|||
margin-bottom: 24rpx; |
|||
} |
|||
|
|||
.post-title { |
|||
font-size: 34rpx; |
|||
font-weight: bold; |
|||
color: #333; |
|||
margin-bottom: 16rpx; |
|||
line-height: 1.4; |
|||
} |
|||
|
|||
.post-desc { |
|||
font-size: 28rpx; |
|||
color: #555; |
|||
line-height: 1.6; |
|||
margin-bottom: 20rpx; |
|||
} |
|||
|
|||
|
|||
|
|||
|
|||
/* 回复列表 */ |
|||
.reply-list { |
|||
margin-top: 24rpx; |
|||
} |
|||
|
|||
.reply-title { |
|||
font-size: 26rpx; |
|||
color: #666; |
|||
margin-bottom: 20rpx; |
|||
font-weight: bold; |
|||
} |
|||
|
|||
.reply-item { |
|||
padding: 24rpx 0; |
|||
border-bottom: 1rpx solid #f8f8f8; |
|||
} |
|||
|
|||
.reply-item:last-child { |
|||
border-bottom: none; |
|||
} |
|||
|
|||
.reply-user { |
|||
display: flex; |
|||
align-items: center; |
|||
margin-bottom: 16rpx; |
|||
} |
|||
|
|||
.reply-avatar { |
|||
width: 56rpx; |
|||
height: 56rpx; |
|||
border-radius: 50%; |
|||
margin-right: 16rpx; |
|||
} |
|||
|
|||
.reply-user-info { |
|||
display: flex; |
|||
flex-direction: column; |
|||
} |
|||
|
|||
.reply-username { |
|||
font-size: 26rpx; |
|||
color: #333; |
|||
margin-bottom: 4rpx; |
|||
} |
|||
|
|||
.reply-time { |
|||
font-size: 22rpx; |
|||
color: #999; |
|||
} |
|||
|
|||
.reply-content { |
|||
font-size: 28rpx; |
|||
color: #444; |
|||
line-height: 1.5; |
|||
margin-left: 72rpx; |
|||
} |
|||
|
|||
.view-more-replies { |
|||
font-size: 26rpx; |
|||
color: #007AFF; |
|||
text-align: center; |
|||
padding: 20rpx 0; |
|||
margin-top: 10rpx; |
|||
} |
|||
|
|||
/* 回复输入区域 */ |
|||
.reply-input-container { |
|||
display: flex; |
|||
align-items: center; |
|||
margin-top: 20rpx; |
|||
padding-top: 20rpx; |
|||
border-top: 1rpx solid #f0f0f0; |
|||
} |
|||
|
|||
.reply-input { |
|||
flex: 1; |
|||
background-color: #f8f8f8; |
|||
border-radius: 30rpx; |
|||
padding: 16rpx 24rpx; |
|||
font-size: 28rpx; |
|||
margin-right: 16rpx; |
|||
} |
|||
|
|||
.send-reply-btn { |
|||
background-color: #007AFF; |
|||
color: white; |
|||
font-size: 26rpx; |
|||
padding: 12rpx 24rpx; |
|||
line-height: normal; |
|||
border-radius: 30rpx; |
|||
margin: 0; |
|||
} |
|||
|
|||
.send-reply-btn:after { |
|||
border: none; |
|||
} |
|||
|
|||
/* 空状态 */ |
|||
.empty-state { |
|||
display: flex; |
|||
flex-direction: column; |
|||
align-items: center; |
|||
justify-content: center; |
|||
padding: 100rpx 30rpx; |
|||
text-align: center; |
|||
} |
|||
|
|||
.empty-image { |
|||
width: 300rpx; |
|||
height: 300rpx; |
|||
margin-bottom: 40rpx; |
|||
opacity: 0.6; |
|||
} |
|||
|
|||
.empty-text { |
|||
font-size: 30rpx; |
|||
color: #999; |
|||
margin-bottom: 40rpx; |
|||
} |
|||
|
|||
.empty-btn { |
|||
background-color: #007AFF; |
|||
color: white; |
|||
font-size: 30rpx; |
|||
padding: 20rpx 40rpx; |
|||
line-height: normal; |
|||
border-radius: 40rpx; |
|||
margin: 0; |
|||
} |
|||
|
|||
.empty-btn:after { |
|||
border: none; |
|||
} |
|||
|
|||
/* 弹窗样式 */ |
|||
.modal-overlay { |
|||
position: fixed; |
|||
top: 0; |
|||
left: 0; |
|||
right: 0; |
|||
bottom: 0; |
|||
background-color: rgba(0, 0, 0, 0.5); |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
z-index: 100; |
|||
} |
|||
|
|||
.modal-content { |
|||
background-color: white; |
|||
border-radius: 20rpx; |
|||
width: 85%; |
|||
max-height: 80vh; |
|||
overflow-y: auto; |
|||
padding: 40rpx; |
|||
} |
|||
|
|||
.modal-header { |
|||
font-size: 36rpx; |
|||
font-weight: bold; |
|||
color: #333; |
|||
margin-bottom: 40rpx; |
|||
text-align: center; |
|||
} |
|||
|
|||
.form-item { |
|||
margin-bottom: 40rpx; |
|||
} |
|||
|
|||
.form-input { |
|||
background-color: #f8f8f8; |
|||
border-radius: 12rpx; |
|||
padding: 24rpx; |
|||
font-size: 28rpx; |
|||
} |
|||
|
|||
.form-textarea { |
|||
background-color: #f8f8f8; |
|||
border-radius: 12rpx; |
|||
padding: 24rpx; |
|||
font-size: 28rpx; |
|||
height: 200rpx; |
|||
width: 100%; |
|||
box-sizing: border-box; |
|||
} |
|||
|
|||
.textarea-counter { |
|||
font-size: 24rpx; |
|||
color: #999; |
|||
text-align: right; |
|||
margin-top: 10rpx; |
|||
} |
|||
|
|||
.selected-tags { |
|||
display: flex; |
|||
flex-wrap: wrap; |
|||
} |
|||
|
|||
.selected-tag { |
|||
font-size: 24rpx; |
|||
color: #007AFF; |
|||
background-color: #e6f2ff; |
|||
padding: 8rpx 20rpx; |
|||
border-radius: 20rpx; |
|||
margin-right: 12rpx; |
|||
margin-bottom: 12rpx; |
|||
display: flex; |
|||
align-items: center; |
|||
} |
|||
|
|||
.remove-tag { |
|||
font-size: 30rpx; |
|||
margin-left: 8rpx; |
|||
color: #999; |
|||
} |
|||
|
|||
.modal-buttons { |
|||
display: flex; |
|||
justify-content: space-between; |
|||
margin-top: 40rpx; |
|||
} |
|||
|
|||
.modal-btn { |
|||
flex: 1; |
|||
font-size: 30rpx; |
|||
border-radius: 50px; |
|||
margin: 0 10rpx; |
|||
} |
|||
|
|||
.modal-btn::after{ |
|||
border: none; |
|||
} |
|||
|
|||
.modal-btn.cancel { |
|||
background-color: #f0f0f0; |
|||
color: #666; |
|||
} |
|||
|
|||
.modal-btn.submit { |
|||
background-color: #007AFF; |
|||
color: white; |
|||
} |
|||
|
|||
.modal-btn.submit[disabled] { |
|||
background-color: #cccccc; |
|||
color: white; |
|||
} |
|||
|
|||
/* 加载提示 */ |
|||
.loading { |
|||
display: flex; |
|||
flex-direction: column; |
|||
align-items: center; |
|||
justify-content: center; |
|||
padding: 60rpx 0; |
|||
color: #999; |
|||
font-size: 28rpx; |
|||
} |
|||
|
|||
.loading-icon { |
|||
width: 60rpx; |
|||
height: 60rpx; |
|||
margin-bottom: 20rpx; |
|||
animation: rotate 1s linear infinite; |
|||
} |
|||
|
|||
@keyframes rotate { |
|||
0% { |
|||
transform: rotate(0deg); |
|||
} |
|||
100% { |
|||
transform: rotate(360deg); |
|||
} |
|||
} |
|||
@ -1,330 +1,248 @@ |
|||
import http from '../../../utils/api' |
|||
|
|||
// policy-interpretation.js
|
|||
Page({ |
|||
data: { |
|||
currentDate: '2023年11月15日', |
|||
searchKeyword: '', |
|||
activeFilter: 'all', |
|||
activeFilter: '全部', |
|||
activeFilterValue: '', |
|||
showPolicyDetail: false, |
|||
currentPolicy: {}, |
|||
filteredPolicies: [], |
|||
dictType: [], |
|||
|
|||
// 所有政策数据
|
|||
allPolicies: [ |
|||
{ |
|||
id: 1, |
|||
title: '2023年农业补贴资金申报指南', |
|||
summary: '明确了今年农业补贴的申请条件、标准和流程,重点支持粮食生产和特色农业', |
|||
date: '2023-10-20', |
|||
source: '农业农村部', |
|||
type: 'financial', |
|||
typeText: '财政补贴', |
|||
keyPoints: [ |
|||
'提高粮食生产者补贴标准,每亩增加10-20元', |
|||
'新增特色农产品种植补贴,最高每亩补贴500元', |
|||
'简化申报流程,推行线上申报系统', |
|||
'强化补贴资金监管,确保专款专用' |
|||
], |
|||
interpretation: '此次农业补贴政策调整体现了国家保障粮食安全和促进农业高质量发展的决心。通过提高补贴标准、扩大补贴范围,调动农民种粮积极性,同时支持特色农业发展,优化农业产业结构。', |
|||
applicableObjects: [ |
|||
'种粮大户', |
|||
'家庭农场', |
|||
'农民专业合作社', |
|||
'农业企业' |
|||
], |
|||
applicationGuide: [ |
|||
'登录农业农村部政务服务平台', |
|||
'填写申报信息并上传证明材料', |
|||
'乡镇农业部门初审', |
|||
'县级农业农村部门审核', |
|||
'公示7个工作日', |
|||
'拨付补贴资金' |
|||
], |
|||
expertAdvice: '建议符合条件的农业经营主体尽早准备申报材料,重点关注今年新增的特色农产品补贴项目。同时注意申报时间节点,避免错过申报期限。', |
|||
relatedLinks: [ |
|||
{ name: '政策原文链接', url: 'https://example.com/policy1' }, |
|||
{ name: '申报平台入口', url: 'https://example.com/apply1' } |
|||
] |
|||
}, |
|||
{ |
|||
id: 2, |
|||
title: '畜牧业高质量发展支持政策', |
|||
summary: '针对畜牧业规模化、标准化发展提出具体扶持措施,加强动物疫病防控', |
|||
date: '2023-09-15', |
|||
source: '农业农村部', |
|||
type: 'financial', |
|||
typeText: '财政补贴', |
|||
keyPoints: [ |
|||
'对标准化养殖场建设给予30%资金补贴', |
|||
'引进优良种畜每头补贴500-2000元', |
|||
'支持畜禽粪污资源化利用项目', |
|||
'加强动物疫病防控体系建设' |
|||
], |
|||
interpretation: '本政策旨在推动畜牧业转型升级,提高产业竞争力。通过支持标准化养殖和粪污资源化利用,促进畜牧业绿色发展,保障畜产品有效供给和质量安全。', |
|||
applicableObjects: [ |
|||
'规模化养殖场', |
|||
'畜牧专业合作社', |
|||
'畜禽育种企业' |
|||
], |
|||
applicationGuide: [ |
|||
'准备养殖场备案证明等相关材料', |
|||
'向县级畜牧部门提交申请', |
|||
'现场审核验收', |
|||
'审批公示', |
|||
'拨付补贴资金' |
|||
], |
|||
expertAdvice: '建议养殖场重点考虑粪污资源化利用项目申报,这既是政策支持重点,也符合环保要求。同时加强疫病防控,确保养殖安全。', |
|||
relatedLinks: [ |
|||
{ name: '政策详细解读', url: 'https://example.com/policy2' } |
|||
] |
|||
}, |
|||
{ |
|||
id: 3, |
|||
title: '智慧农业技术推广实施方案', |
|||
summary: '推动物联网、大数据等现代信息技术在农业生产中的应用', |
|||
date: '2023-11-05', |
|||
source: '农业农村部、工信部', |
|||
type: 'technology', |
|||
typeText: '技术推广', |
|||
keyPoints: [ |
|||
'建设智慧农业示范基地,每个补贴50-100万元', |
|||
'推广农业物联网设备,补贴比例达40%', |
|||
'开展智慧农业技术培训,每年培训10万人次', |
|||
'支持农业大数据平台建设' |
|||
], |
|||
interpretation: '智慧农业是农业现代化的重要方向。本方案通过示范基地建设、设备补贴和技术培训,加快现代信息技术与农业深度融合,提升农业生产智能化水平。', |
|||
applicableObjects: [ |
|||
'农业科技示范园', |
|||
'现代农业园区', |
|||
'农业科技企业', |
|||
'新型农业经营主体' |
|||
], |
|||
applicationGuide: [ |
|||
'制定智慧农业建设方案', |
|||
'县级农业农村部门推荐', |
|||
'市级部门审核', |
|||
'省级部门审批立项', |
|||
'组织项目实施', |
|||
'验收评估' |
|||
], |
|||
expertAdvice: '建议有条件的农业经营主体积极申报智慧农业项目,重点关注物联网设备应用和数据平台建设。同时加强技术人员培训,确保技术有效应用。', |
|||
relatedLinks: [ |
|||
{ name: '技术标准手册', url: 'https://example.com/policy3' } |
|||
] |
|||
}, |
|||
{ |
|||
id: 4, |
|||
title: '草原生态保护补助奖励政策', |
|||
summary: '实施草原禁牧、草畜平衡奖励,促进草原生态恢复', |
|||
date: '2023-08-30', |
|||
source: '国家林草局', |
|||
type: 'environment', |
|||
typeText: '生态保护', |
|||
keyPoints: [ |
|||
'禁牧草原每亩每年补贴7.5元', |
|||
'草畜平衡草原每亩每年补贴2.5元', |
|||
'实施草原生态修复工程', |
|||
'支持草原畜牧业转型发展' |
|||
], |
|||
interpretation: '草原生态保护补助奖励政策是生态文明建设的重要内容。通过实施禁牧和草畜平衡,减轻草原生态压力,促进草原植被恢复,实现生态保护和牧民增收双赢。', |
|||
applicableObjects: [ |
|||
'草原牧户', |
|||
'草原生态保护合作社', |
|||
'草原管护员' |
|||
], |
|||
applicationGuide: [ |
|||
'牧户申报草原承包面积', |
|||
'村级公示', |
|||
'乡镇审核', |
|||
'县级审定', |
|||
'资金发放' |
|||
], |
|||
expertAdvice: '建议牧户严格按照核定的载畜量放牧,避免超载过牧。同时可考虑发展草原生态旅游等替代产业,增加收入来源。', |
|||
relatedLinks: [ |
|||
{ name: '政策实施细则', url: 'https://example.com/policy4' } |
|||
] |
|||
}, |
|||
{ |
|||
id: 5, |
|||
title: '农业保险保费补贴管理办法', |
|||
summary: '扩大农业保险覆盖面,提高保障水平,优化理赔服务', |
|||
date: '2023-10-10', |
|||
source: '财政部、农业农村部', |
|||
type: 'insurance', |
|||
typeText: '农业保险', |
|||
keyPoints: [ |
|||
'三大粮食作物保险保费补贴比例提高至70%', |
|||
'扩大特色农产品保险试点范围', |
|||
'简化理赔流程,缩短理赔时间', |
|||
'建立农业保险信息共享平台' |
|||
], |
|||
interpretation: '农业保险是防范农业风险的重要工具。新办法通过提高补贴比例、扩大保险范围,增强农业抗风险能力,为农民提供更全面的风险保障。', |
|||
applicableObjects: [ |
|||
'所有从事农业生产的农户', |
|||
'农业经营主体', |
|||
'农业保险公司' |
|||
], |
|||
applicationGuide: [ |
|||
'选择符合条件的农业保险产品', |
|||
'与保险公司签订保险合同', |
|||
'缴纳自付部分保费', |
|||
'出险后及时报案', |
|||
'保险公司查勘定损', |
|||
'理赔款支付' |
|||
], |
|||
expertAdvice: '建议农户积极投保农业保险,特别是特色农产品保险。注意了解保险责任范围、免赔条款等重要内容,确保自身权益。', |
|||
relatedLinks: [ |
|||
{ name: '农业保险产品目录', url: 'https://example.com/policy5' } |
|||
] |
|||
} |
|||
], |
|||
// 分页相关
|
|||
pageNum: 1, |
|||
pageSize: 10, |
|||
total: 0, |
|||
hasMore: true, |
|||
isLoading: false, |
|||
isLoadingMore: false, |
|||
|
|||
// 筛选后的政策
|
|||
filteredPolicies: [] |
|||
// 滚动区域高度
|
|||
scrollHeight: 0, |
|||
|
|||
// 搜索定时器
|
|||
searchTimer: null |
|||
}, |
|||
|
|||
onLoad: function(options) { |
|||
|
|||
this.getpolicyeZd() |
|||
this.getpolicyelucidation() |
|||
|
|||
// 计算滚动区域高度
|
|||
this.calculateScrollHeight() |
|||
}, |
|||
|
|||
// 政策解读列表
|
|||
getpolicyelucidation(){ |
|||
http.policyelucidation({ |
|||
data:{}, |
|||
success:res=>{ |
|||
console.log(111111,res); |
|||
this.setData({ |
|||
filteredPolicies:res.rows |
|||
}) |
|||
} |
|||
}) |
|||
onReady: function() { |
|||
// 确保获取正确的高度
|
|||
this.calculateScrollHeight() |
|||
}, |
|||
|
|||
// 搜索输入处理
|
|||
onSearchInput: function(e) { |
|||
this.setData({ |
|||
searchKeyword: e.detail.value |
|||
}); |
|||
// 计算滚动区域高度
|
|||
calculateScrollHeight: function() { |
|||
const systemInfo = wx.getSystemInfoSync() |
|||
const query = wx.createSelectorQuery() |
|||
query.select('.search-section').boundingClientRect() |
|||
query.select('.bottom-tip').boundingClientRect() |
|||
query.exec((res) => { |
|||
if (res[0] && res[1]) { |
|||
const searchHeight = res[0].height |
|||
const bottomHeight = res[1].height |
|||
const windowHeight = systemInfo.windowHeight |
|||
const scrollHeight = windowHeight - searchHeight - bottomHeight - 40 |
|||
this.setData({ |
|||
scrollHeight: Math.max(scrollHeight, 400) // 设置最小高度
|
|||
}) |
|||
} |
|||
}) |
|||
}, |
|||
|
|||
// 搜索按钮点击
|
|||
onSearch: function() { |
|||
this.filterPolicies(); |
|||
// 政策解读列表(支持分页和筛选)
|
|||
getpolicyelucidation: function(isLoadMore = false) { |
|||
if (isLoadMore && (!this.data.hasMore || this.data.isLoadingMore)) { |
|||
return |
|||
} |
|||
|
|||
if (!isLoadMore) { |
|||
this.setData({ isLoading: true }) |
|||
} else { |
|||
this.setData({ isLoadingMore: true }) |
|||
} |
|||
|
|||
const params = { |
|||
pageNum: isLoadMore ? this.data.pageNum : 1, |
|||
pageSize: this.data.pageSize, |
|||
searchKey: this.data.searchKeyword || undefined |
|||
} |
|||
|
|||
// 如果选择了分类(不是"全部"),则添加分类参数
|
|||
if (this.data.activeFilterValue && this.data.activeFilter !== '全部') { |
|||
params.policyCategory = this.data.activeFilterValue |
|||
} |
|||
|
|||
http.policyelucidation({ |
|||
data: params, |
|||
success: res => { |
|||
console.log('政策列表:', res) |
|||
|
|||
const newList = isLoadMore |
|||
? [...this.data.filteredPolicies, ...res.rows] |
|||
: res.rows |
|||
|
|||
const total = res.total || 0 |
|||
const currentTotal = newList.length |
|||
const hasMore = currentTotal < total |
|||
|
|||
this.setData({ |
|||
filteredPolicies: newList, |
|||
total: total, |
|||
hasMore: hasMore, |
|||
pageNum: isLoadMore ? this.data.pageNum + 1 : 2, |
|||
isLoading: false, |
|||
isLoadingMore: false |
|||
}) |
|||
}, |
|||
fail: (err) => { |
|||
console.error('获取政策列表失败:', err) |
|||
this.setData({ |
|||
isLoading: false, |
|||
isLoadingMore: false |
|||
}) |
|||
|
|||
if (!isLoadMore) { |
|||
wx.showToast({ |
|||
title: '加载失败', |
|||
icon: 'none' |
|||
}) |
|||
} |
|||
} |
|||
}) |
|||
}, |
|||
|
|||
// 筛选标签点击
|
|||
onFilterTap: function(e) { |
|||
const filter = e.currentTarget.dataset.filter; |
|||
this.setData({ |
|||
activeFilter: filter |
|||
}, () => { |
|||
this.filterPolicies(); |
|||
}); |
|||
// 类型
|
|||
getpolicyeZd: function() { |
|||
http.policyeZd({ |
|||
data: { |
|||
dictType: 'policy_category' |
|||
}, |
|||
success: res => { |
|||
console.log('分类数据:', res) |
|||
this.setData({ |
|||
dictType: res.rows |
|||
}) |
|||
}, |
|||
fail: (err) => { |
|||
console.error('获取分类失败:', err) |
|||
wx.showToast({ |
|||
title: '加载分类失败', |
|||
icon: 'none' |
|||
}) |
|||
} |
|||
}) |
|||
}, |
|||
|
|||
// 筛选政策
|
|||
filterPolicies: function() { |
|||
const { allPolicies, activeFilter, searchKeyword } = this.data; |
|||
// 搜索输入处理(防抖)
|
|||
onSearchInput: function(e) { |
|||
const searchKeyword = e.detail.value.trim() |
|||
|
|||
let filtered = allPolicies; |
|||
this.setData({ |
|||
searchKeyword: searchKeyword |
|||
}) |
|||
|
|||
// 按类型筛选
|
|||
if (activeFilter !== 'all') { |
|||
if (activeFilter === 'latest') { |
|||
// 最新政策:最近30天内发布的
|
|||
const thirtyDaysAgo = new Date(); |
|||
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30); |
|||
|
|||
filtered = filtered.filter(policy => { |
|||
const policyDate = new Date(policy.date); |
|||
return policyDate >= thirtyDaysAgo; |
|||
}); |
|||
} else { |
|||
filtered = filtered.filter(policy => policy.type === activeFilter); |
|||
} |
|||
// 清除之前的定时器
|
|||
if (this.data.searchTimer) { |
|||
clearTimeout(this.data.searchTimer) |
|||
} |
|||
|
|||
// 按关键词搜索
|
|||
if (searchKeyword.trim() !== '') { |
|||
const keyword = searchKeyword.toLowerCase(); |
|||
filtered = filtered.filter(policy => |
|||
policy.title.toLowerCase().includes(keyword) || |
|||
policy.summary.toLowerCase().includes(keyword) || |
|||
(policy.keyPoints && policy.keyPoints.some(point => point.toLowerCase().includes(keyword))) |
|||
); |
|||
} |
|||
// 设置新的定时器(500ms防抖)
|
|||
this.data.searchTimer = setTimeout(() => { |
|||
// 重置分页
|
|||
this.setData({ |
|||
pageNum: 1, |
|||
hasMore: true |
|||
}, () => { |
|||
this.getpolicyelucidation() |
|||
}) |
|||
}, 500) |
|||
}, |
|||
|
|||
// 清空搜索
|
|||
onClearSearch: function() { |
|||
this.setData({ |
|||
searchKeyword: '', |
|||
pageNum: 1, |
|||
hasMore: true |
|||
}, () => { |
|||
this.getpolicyelucidation() |
|||
}) |
|||
}, |
|||
|
|||
// 搜索按钮点击(键盘搜索键)
|
|||
onSearch: function() { |
|||
this.setData({ |
|||
pageNum: 1, |
|||
hasMore: true |
|||
}, () => { |
|||
this.getpolicyelucidation() |
|||
}) |
|||
}, |
|||
|
|||
// 筛选标签点击
|
|||
onFilterTap: function(e) { |
|||
const filter = e.currentTarget.dataset.filter |
|||
const value = e.currentTarget.dataset.value |
|||
|
|||
this.setData({ |
|||
filteredPolicies: filtered |
|||
}); |
|||
activeFilter: filter, |
|||
activeFilterValue: value, |
|||
pageNum: 1, |
|||
hasMore: true |
|||
}, () => { |
|||
this.getpolicyelucidation() |
|||
}) |
|||
}, |
|||
|
|||
// 政策卡片点击
|
|||
onPolicyTap: function(e) { |
|||
const policyId = e.currentTarget.dataset.id; |
|||
http.policyeDetails({ |
|||
data:{ |
|||
id:policyId |
|||
}, |
|||
success:res=>{ |
|||
console.log(333,res); |
|||
this.setData({ |
|||
showPolicyDetail: true, |
|||
currentPolicy: res.data |
|||
}) |
|||
} |
|||
}) |
|||
const policyId = e.currentTarget.dataset.id |
|||
http.policyeDetails({ |
|||
data: { |
|||
id: policyId |
|||
}, |
|||
success: res => { |
|||
console.log('政策详情:', res) |
|||
this.setData({ |
|||
showPolicyDetail: true, |
|||
currentPolicy: res.data |
|||
}) |
|||
}, |
|||
fail: (err) => { |
|||
console.error('获取政策详情失败:', err) |
|||
wx.showToast({ |
|||
title: '加载详情失败', |
|||
icon: 'none' |
|||
}) |
|||
} |
|||
}) |
|||
}, |
|||
|
|||
// 上拉加载更多
|
|||
onReachBottom: function() { |
|||
if (this.data.hasMore && !this.data.isLoadingMore) { |
|||
this.getpolicyelucidation(true) |
|||
} |
|||
}, |
|||
|
|||
// 隐藏政策详情
|
|||
hidePolicyDetail: function() { |
|||
this.setData({ |
|||
showPolicyDetail: false |
|||
}); |
|||
}) |
|||
}, |
|||
|
|||
// 阻止事件冒泡
|
|||
stopPropagation: function() { |
|||
// 空函数,仅用于阻止事件冒泡
|
|||
}, |
|||
|
|||
// 链接点击
|
|||
onLinkTap: function(e) { |
|||
const url = e.currentTarget.dataset.url; |
|||
wx.showModal({ |
|||
title: '提示', |
|||
content: '即将打开外部链接: ' + url, |
|||
success: function(res) { |
|||
if (res.confirm) { |
|||
// 在实际小程序中,这里需要使用web-view或复制链接
|
|||
wx.setClipboardData({ |
|||
data: url, |
|||
success: function() { |
|||
wx.showToast({ |
|||
title: '链接已复制', |
|||
icon: 'success' |
|||
}); |
|||
} |
|||
}); |
|||
} |
|||
} |
|||
}); |
|||
}, |
|||
|
|||
// 收藏政策
|
|||
onSavePolicy: function() { |
|||
const policyTitle = this.data.currentPolicy.title; |
|||
wx.showToast({ |
|||
title: `"${policyTitle}"已收藏`, |
|||
icon: 'success', |
|||
duration: 2000 |
|||
}); |
|||
|
|||
// 在实际应用中,这里可以将政策保存到本地或发送到服务器
|
|||
setTimeout(() => { |
|||
this.hidePolicyDetail(); |
|||
}, 1500); |
|||
} |
|||
}); |
|||
}) |
|||
@ -0,0 +1,381 @@ |
|||
// pages/training/videoDetail/videoDetail.js
|
|||
import http from '../../../utils/api' |
|||
const baseUrl = require('../../../utils/baseUrl') |
|||
|
|||
Page({ |
|||
data: { |
|||
baseUrl: baseUrl, |
|||
video: {}, |
|||
videoUrl: '', |
|||
loading: true, |
|||
fullScreen: false, |
|||
isPlaying: false, |
|||
isMuted: false, |
|||
loop: false, |
|||
playbackRate: 1, |
|||
currentTime: 0, |
|||
duration: 0, |
|||
videoTags: [], |
|||
videoError: false, |
|||
autoplay: false, |
|||
controlsVisible: true, |
|||
controlsTimer: null, |
|||
lastTouchTime: 0, |
|||
}, |
|||
|
|||
onLoad(options) { |
|||
console.log('页面参数:', options) |
|||
this.getVideoDetails(options.id) |
|||
}, |
|||
|
|||
onReady() { |
|||
this.videoContext = wx.createVideoContext('videoPlayer', this) |
|||
console.log('视频上下文创建成功') |
|||
}, |
|||
|
|||
onUnload() { |
|||
// 清除定时器
|
|||
if (this.data.controlsTimer) { |
|||
clearTimeout(this.data.controlsTimer) |
|||
} |
|||
// 页面卸载时停止播放
|
|||
if (this.videoContext) { |
|||
this.videoContext.pause() |
|||
} |
|||
}, |
|||
|
|||
// 获取视频详情
|
|||
getVideoDetails(id) { |
|||
this.setData({ |
|||
loading: true, |
|||
videoError: false |
|||
}) |
|||
|
|||
http.videoDetails({ |
|||
data: { id }, |
|||
success: res => { |
|||
console.log('视频详情响应:', res) |
|||
if (res.code === 200 && res.data) { |
|||
const video = res.data |
|||
console.log('视频数据:', video) |
|||
|
|||
// 处理视频URL
|
|||
let videoUrl = '' |
|||
if (video.videoUrl) { |
|||
videoUrl = this.data.baseUrl + video.videoUrl |
|||
console.log('视频完整URL:', videoUrl) |
|||
} |
|||
|
|||
// 处理视频标签
|
|||
const tags = video.tags ? video.tags.split(',').filter(tag => tag.trim()) : [] |
|||
|
|||
this.setData({ |
|||
video: video, |
|||
videoUrl: videoUrl, |
|||
videoTags: tags, |
|||
loading: false |
|||
}) |
|||
|
|||
// 设置页面标题
|
|||
wx.setNavigationBarTitle({ |
|||
title: video.title.substring(0, 12) + (video.title.length > 12 ? '...' : '') |
|||
}) |
|||
|
|||
} else { |
|||
wx.showToast({ |
|||
title: res.msg || '视频不存在', |
|||
icon: 'none', |
|||
duration: 2000 |
|||
}) |
|||
setTimeout(() => { |
|||
wx.navigateBack() |
|||
}, 2000) |
|||
} |
|||
}, |
|||
fail: err => { |
|||
console.error('获取视频详情失败:', err) |
|||
this.setData({ |
|||
loading: false, |
|||
videoError: true |
|||
}) |
|||
wx.showToast({ |
|||
title: '加载失败,请重试', |
|||
icon: 'none', |
|||
duration: 2000 |
|||
}) |
|||
} |
|||
}) |
|||
}, |
|||
|
|||
// 返回上一页
|
|||
goBack() { |
|||
wx.navigateBack() |
|||
}, |
|||
|
|||
// 视频加载完成
|
|||
onVideoLoaded(e) { |
|||
console.log('视频元数据加载完成:', e.detail) |
|||
this.setData({ |
|||
duration: e.detail.duration || 0, |
|||
videoError: false |
|||
}) |
|||
}, |
|||
|
|||
// 视频播放错误
|
|||
onVideoError(e) { |
|||
console.error('视频播放错误详情:', e.detail) |
|||
this.setData({ |
|||
videoError: true, |
|||
isPlaying: false |
|||
}) |
|||
|
|||
wx.showModal({ |
|||
title: '播放错误', |
|||
content: '视频加载失败,请检查网络或视频链接', |
|||
showCancel: true, |
|||
confirmText: '重试', |
|||
success: (res) => { |
|||
if (res.confirm) { |
|||
this.retryVideo() |
|||
} |
|||
} |
|||
}) |
|||
}, |
|||
|
|||
// 视频播放事件
|
|||
onVideoPlay(e) { |
|||
console.log('视频开始播放') |
|||
this.setData({ |
|||
isPlaying: true, |
|||
videoError: false |
|||
}) |
|||
|
|||
// 播放时隐藏控制栏
|
|||
if (this.data.fullScreen) { |
|||
this.hideControls() |
|||
} |
|||
}, |
|||
|
|||
// 视频暂停事件
|
|||
onVideoPause(e) { |
|||
console.log('视频暂停') |
|||
this.setData({ |
|||
isPlaying: false |
|||
}) |
|||
|
|||
// 暂停时显示控制栏
|
|||
if (this.data.fullScreen) { |
|||
this.showControls() |
|||
} |
|||
}, |
|||
|
|||
// 视频播放结束
|
|||
onVideoEnded(e) { |
|||
console.log('视频播放结束') |
|||
this.setData({ |
|||
isPlaying: false, |
|||
currentTime: 0 |
|||
}) |
|||
|
|||
// 结束播放时显示控制栏
|
|||
if (this.data.fullScreen) { |
|||
this.showControls() |
|||
} |
|||
}, |
|||
|
|||
// 时间更新事件
|
|||
onTimeUpdate(e) { |
|||
const currentTime = e.detail.currentTime |
|||
const duration = e.detail.duration |
|||
|
|||
// 更新当前时间和总时长
|
|||
this.setData({ |
|||
currentTime: currentTime, |
|||
duration: duration > 0 ? duration : this.data.duration |
|||
}) |
|||
}, |
|||
|
|||
// 全屏切换事件
|
|||
onFullScreenChange(e) { |
|||
const fullScreen = e.detail.fullScreen |
|||
console.log('全屏状态变化:', fullScreen) |
|||
|
|||
this.setData({ |
|||
fullScreen: fullScreen, |
|||
controlsVisible: !fullScreen |
|||
}) |
|||
|
|||
if (fullScreen) { |
|||
wx.setKeepScreenOn({ keepScreenOn: true }) |
|||
// 进入全屏后自动播放
|
|||
setTimeout(() => { |
|||
this.videoContext.play() |
|||
}, 300) |
|||
} else { |
|||
wx.setKeepScreenOn({ keepScreenOn: false }) |
|||
// 退出全屏时暂停
|
|||
this.videoContext.pause() |
|||
} |
|||
}, |
|||
|
|||
// 进入全屏
|
|||
enterFullScreen() { |
|||
this.videoContext.requestFullScreen({ direction: 90 }) |
|||
}, |
|||
|
|||
// 退出全屏
|
|||
exitFullScreen() { |
|||
this.videoContext.exitFullScreen() |
|||
}, |
|||
|
|||
// 切换播放状态
|
|||
togglePlay() { |
|||
console.log('切换播放状态,当前状态:', this.data.isPlaying) |
|||
if (this.data.videoError) { |
|||
this.retryVideo() |
|||
return |
|||
} |
|||
|
|||
if (this.data.isPlaying) { |
|||
this.videoContext.pause() |
|||
} else { |
|||
this.videoContext.play() |
|||
} |
|||
}, |
|||
|
|||
// 切换静音
|
|||
toggleMute() { |
|||
const isMuted = !this.data.isMuted |
|||
this.setData({ isMuted: isMuted }) |
|||
this.videoContext.muted(isMuted) |
|||
|
|||
wx.showToast({ |
|||
title: isMuted ? '已静音' : '已取消静音', |
|||
icon: 'none', |
|||
duration: 800 |
|||
}) |
|||
}, |
|||
|
|||
// 切换循环
|
|||
toggleLoop() { |
|||
const loop = !this.data.loop |
|||
this.setData({ loop: loop }) |
|||
this.videoContext.loop(loop) |
|||
|
|||
wx.showToast({ |
|||
title: loop ? '开启循环播放' : '关闭循环播放', |
|||
icon: 'none', |
|||
duration: 800 |
|||
}) |
|||
}, |
|||
|
|||
// 切换播放速度
|
|||
toggleSpeed() { |
|||
const speeds = [0.5, 0.75, 1, 1.25, 1.5, 2] |
|||
const currentIndex = speeds.indexOf(this.data.playbackRate) |
|||
const nextIndex = (currentIndex + 1) % speeds.length |
|||
const nextSpeed = speeds[nextIndex] |
|||
|
|||
this.setData({ playbackRate: nextSpeed }) |
|||
this.videoContext.playbackRate(nextSpeed) |
|||
|
|||
wx.showToast({ |
|||
title: `播放速度 ${nextSpeed}x`, |
|||
icon: 'none', |
|||
duration: 800 |
|||
}) |
|||
}, |
|||
|
|||
// 重试播放
|
|||
retryVideo() { |
|||
console.log('重试播放视频') |
|||
this.setData({ |
|||
videoError: false, |
|||
loading: true |
|||
}) |
|||
|
|||
// 重新加载视频
|
|||
setTimeout(() => { |
|||
this.setData({ loading: false }) |
|||
if (this.videoContext) { |
|||
this.videoContext.seek(0) |
|||
this.videoContext.play() |
|||
} |
|||
}, 500) |
|||
}, |
|||
|
|||
// 触摸控制
|
|||
onTouchControl(e) { |
|||
const type = e.currentTarget.dataset.type |
|||
const currentTime = this.data.currentTime |
|||
const duration = this.data.duration |
|||
|
|||
if (type === 'backward') { |
|||
const newTime = Math.max(0, currentTime - 10) |
|||
this.setData({ currentTime: newTime }) |
|||
this.videoContext.seek(newTime) |
|||
|
|||
wx.showToast({ |
|||
title: '-10秒', |
|||
icon: 'none', |
|||
duration: 500 |
|||
}) |
|||
} else if (type === 'forward') { |
|||
const newTime = Math.min(duration, currentTime + 10) |
|||
this.setData({ currentTime: newTime }) |
|||
this.videoContext.seek(newTime) |
|||
|
|||
wx.showToast({ |
|||
title: '+10秒', |
|||
icon: 'none', |
|||
duration: 500 |
|||
}) |
|||
} |
|||
}, |
|||
|
|||
// 触摸移动
|
|||
onTouchMove() { |
|||
if (this.data.fullScreen) { |
|||
this.showControls() |
|||
this.hideControls() |
|||
} |
|||
}, |
|||
|
|||
// 显示控制栏
|
|||
showControls() { |
|||
this.setData({ controlsVisible: true }) |
|||
|
|||
// 清除之前的定时器
|
|||
if (this.data.controlsTimer) { |
|||
clearTimeout(this.data.controlsTimer) |
|||
} |
|||
|
|||
// 3秒后自动隐藏控制栏
|
|||
const timer = setTimeout(() => { |
|||
if (this.data.isPlaying && this.data.fullScreen) { |
|||
this.setData({ controlsVisible: false }) |
|||
} |
|||
}, 3000) |
|||
|
|||
this.setData({ controlsTimer: timer }) |
|||
}, |
|||
|
|||
// 隐藏控制栏
|
|||
hideControls() { |
|||
if (this.data.controlsTimer) { |
|||
clearTimeout(this.data.controlsTimer) |
|||
} |
|||
|
|||
const timer = setTimeout(() => { |
|||
if (this.data.isPlaying && this.data.fullScreen) { |
|||
this.setData({ controlsVisible: false }) |
|||
} |
|||
}, 3000) |
|||
|
|||
this.setData({ controlsTimer: timer }) |
|||
}, |
|||
|
|||
|
|||
|
|||
|
|||
}) |
|||
@ -0,0 +1,4 @@ |
|||
{ |
|||
"navigationBarTitleText":"视频详情", |
|||
"usingComponents": {} |
|||
} |
|||
@ -0,0 +1,174 @@ |
|||
<view class="video-detail-container"> |
|||
<!-- 导航栏 --> |
|||
<view class="nav-bar" wx:if="{{!fullScreen}}"> |
|||
<view class="nav-title">{{video.title}}</view> |
|||
</view> |
|||
|
|||
<!-- 视频播放器区域 --> |
|||
<view class="video-player-section" style="{{fullScreen ? 'height: 100vh;' : ''}}"> |
|||
<!-- 视频播放器 --> |
|||
<video |
|||
id="videoPlayer" |
|||
src="{{videoUrl}}" |
|||
poster="{{video.coverImage ? baseUrl + video.coverImage : ''}}" |
|||
initial-time="0" |
|||
autoplay="{{autoplay}}" |
|||
loop="{{loop}}" |
|||
muted="{{isMuted}}" |
|||
controls="{{!fullScreen}}" |
|||
show-fullscreen-btn="{{!fullScreen}}" |
|||
show-play-btn="true" |
|||
show-center-play-btn="true" |
|||
enable-progress-gesture="true" |
|||
enable-play-gesture="{{fullScreen}}" |
|||
object-fit="contain" |
|||
direction="0" |
|||
bindplay="onVideoPlay" |
|||
bindpause="onVideoPause" |
|||
bindended="onVideoEnded" |
|||
bindtimeupdate="onTimeUpdate" |
|||
bindfullscreenchange="onFullScreenChange" |
|||
bindloadedmetadata="onVideoLoaded" |
|||
binderror="onVideoError" |
|||
class="video-player" |
|||
></video> |
|||
|
|||
<!-- 视频封面 --> |
|||
<view class="video-cover" wx:if="{{video.coverImage && !isPlaying && !fullScreen}}"> |
|||
<image |
|||
src="{{baseUrl + video.coverImage}}" |
|||
mode="aspectFill" |
|||
class="cover-image" |
|||
></image> |
|||
<view class="cover-play-btn" catchtap="togglePlay"> |
|||
<image class="play-icon" src="/pagesB/images/bo.png"></image> |
|||
</view> |
|||
</view> |
|||
|
|||
<!-- 全屏时的自定义控制栏 --> |
|||
<view class="fullscreen-controls" wx:if="{{fullScreen}}" catchtouchmove="onTouchMove"> |
|||
<!-- 顶部控制栏 --> |
|||
<view class="fullscreen-top-bar"> |
|||
<view class="top-bar-left" catchtap="exitFullScreen"> |
|||
<image class="back-icon" src="/pagesB/images/left.png"></image> |
|||
<text class="back-text">返回</text> |
|||
</view> |
|||
<view class="top-bar-title">{{video.title}}</view> |
|||
<view class="top-bar-right"></view> |
|||
</view> |
|||
|
|||
<!-- 播放按钮(中间) --> |
|||
<view class="center-play-btn" wx:if="{{!isPlaying}}" catchtap="togglePlay"> |
|||
<image class="play-large-icon" src="/pagesB/images/play.png"></image> |
|||
<view class="play-ripple"></view> |
|||
</view> |
|||
|
|||
<!-- 底部控制栏 --> |
|||
<view class="fullscreen-bottom-bar"> |
|||
<!-- 进度条 --> |
|||
<view class="fullscreen-progress"> |
|||
<view class="progress-time">{{formatTime(currentTime)}}</view> |
|||
<view class="progress-slider-container"> |
|||
<view class="progress-bg"> |
|||
<view class="progress-current" style="width: {{duration ? (currentTime / duration * 100) + '%' : '0%'}}"></view> |
|||
</view> |
|||
<view |
|||
class="progress-thumb" |
|||
style="left: {{duration ? (currentTime / duration * 100) + '%' : '0%'}}" |
|||
></view> |
|||
</view> |
|||
<view class="progress-time">{{formatTime(duration)}}</view> |
|||
</view> |
|||
|
|||
<view class="bottom-controls"> |
|||
<view class="control-item" catchtap="togglePlay"> |
|||
<image class="control-icon" src="{{isPlaying ? '/pagesB/images/pause.png' : '/pagesB/images/play.png'}}"></image> |
|||
</view> |
|||
<view class="control-item" catchtap="toggleMute"> |
|||
<image class="control-icon" src="{{isMuted ? '/pagesB/images/volume_mute.png' : '/pagesB/images/volume.png'}}"></image> |
|||
</view> |
|||
<view class="control-item" catchtap="toggleLoop"> |
|||
<image class="control-icon" src="{{loop ? '/pagesB/images/loop_on.png' : '/pagesB/images/loop.png'}}"></image> |
|||
</view> |
|||
<view class="control-item" catchtap="toggleSpeed"> |
|||
<text class="speed-text">{{playbackRate}}x</text> |
|||
</view> |
|||
<view class="control-item" catchtap="enterFullScreen"> |
|||
<image class="control-icon" src="/pagesB/images/fullscreen.png"></image> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
|
|||
<!-- 触摸控制区域 --> |
|||
<view class="touch-control-left" catchtap="onTouchControl" data-type="backward"></view> |
|||
<view class="touch-control-center" catchtap="togglePlay"></view> |
|||
<view class="touch-control-right" catchtap="onTouchControl" data-type="forward"></view> |
|||
</view> |
|||
|
|||
<!-- 非全屏播放按钮 --> |
|||
<view class="normal-play-btn" wx:if="{{!fullScreen && !isPlaying && !video.coverImage}}" catchtap="togglePlay"> |
|||
<image class="play-icon" src="/pagesB/images/bo.png"></image> |
|||
</view> |
|||
</view> |
|||
|
|||
<!-- 视频信息区域 --> |
|||
<scroll-view |
|||
class="video-info-section" |
|||
scroll-y |
|||
wx:if="{{!fullScreen}}" |
|||
scroll-with-animation |
|||
> |
|||
<!-- 视频标题和描述 --> |
|||
<view class="video-header"> |
|||
<view class="video-title">{{video.title}}</view> |
|||
<view class="video-description">{{video.description}}</view> |
|||
|
|||
<!-- 视频统计数据 --> |
|||
<view class="video-stats"> |
|||
<view class="stat-item"> |
|||
<text>{{video.viewCount}} 播放</text> |
|||
</view> |
|||
<view class="stat-item"> |
|||
<text>{{video.publishTime}}</text> |
|||
</view> |
|||
<view class="stat-item"> |
|||
<text>{{video.category}}</text> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
|
|||
<!-- 发布者信息 --> |
|||
<view class="publisher-section" wx:if="{{video.publisherName}}"> |
|||
<image class="publisher-avatar" src="{{video.publisherAvatar ? baseUrl + video.publisherAvatar : '/pagesB/images/default_avatar.png'}}"></image> |
|||
<view class="publisher-info"> |
|||
<view class="publisher-name">{{video.publisherName}}</view> |
|||
<view class="publisher-desc">{{video.publisherDesc || '视频发布者'}}</view> |
|||
</view> |
|||
</view> |
|||
|
|||
<!-- 视频标签 --> |
|||
<view class="tags-section" wx:if="{{videoTags.length > 0}}"> |
|||
<view class="section-title">标签</view> |
|||
<view class="tags-container"> |
|||
<view class="tag-item" wx:for="{{videoTags}}" wx:key="index"> |
|||
{{item}} |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</scroll-view> |
|||
|
|||
<!-- 加载中 --> |
|||
<view class="loading-container" wx:if="{{loading}}"> |
|||
<view class="loading-spinner"> |
|||
<view class="spinner-circle"></view> |
|||
</view> |
|||
<text class="loading-text">加载中...</text> |
|||
</view> |
|||
|
|||
<!-- 错误提示 --> |
|||
<view class="error-container" wx:if="{{videoError}}"> |
|||
<image class="error-icon" src="/pagesB/images/error.png"></image> |
|||
<text class="error-text">视频加载失败</text> |
|||
<view class="retry-btn" catchtap="retryVideo">重试</view> |
|||
</view> |
|||
</view> |
|||
@ -0,0 +1,664 @@ |
|||
.video-detail-container { |
|||
min-height: 100vh; |
|||
background: linear-gradient(135deg, #0f172a 0%, #1a1e2c 100%); |
|||
position: relative; |
|||
} |
|||
|
|||
/* 导航栏 */ |
|||
.nav-bar { |
|||
position: fixed; |
|||
top: 0; |
|||
left: 0; |
|||
right: 0; |
|||
height: 88rpx; |
|||
padding: 0 30rpx; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: space-between; |
|||
background: linear-gradient(180deg, rgba(15, 23, 42, 0.98) 0%, rgba(15, 23, 42, 0.9) 100%); |
|||
backdrop-filter: blur(20rpx); |
|||
z-index: 1000; |
|||
border-bottom: 1rpx solid rgba(255, 255, 255, 0.08); |
|||
} |
|||
|
|||
.nav-title { |
|||
font-size: 34rpx; |
|||
font-weight: 700; |
|||
color: #fff; |
|||
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.3); |
|||
} |
|||
|
|||
|
|||
|
|||
|
|||
/* 视频播放器区域 */ |
|||
.video-player-section { |
|||
width: 100%; |
|||
height: 500rpx; |
|||
position: relative; |
|||
background: #000; |
|||
overflow: hidden; |
|||
} |
|||
|
|||
.video-player { |
|||
width: 100%; |
|||
height: 100%; |
|||
background: #000; |
|||
} |
|||
|
|||
/* 视频封面 */ |
|||
.video-cover { |
|||
position: absolute; |
|||
top: 0; |
|||
left: 0; |
|||
right: 0; |
|||
bottom: 0; |
|||
background: #000; |
|||
z-index: 2; |
|||
} |
|||
|
|||
.cover-image { |
|||
width: 100%; |
|||
height: 100%; |
|||
opacity: 0.9; |
|||
} |
|||
|
|||
.cover-play-btn { |
|||
position: absolute; |
|||
top: 50%; |
|||
left: 50%; |
|||
transform: translate(-50%, -50%); |
|||
width: 120rpx; |
|||
height: 120rpx; |
|||
background: rgba(0, 0, 0, 0.6); |
|||
backdrop-filter: blur(20rpx); |
|||
border-radius: 50%; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
border: 3rpx solid rgba(255, 255, 255, 0.3); |
|||
box-shadow: 0 8rpx 40rpx rgba(0, 0, 0, 0.5); |
|||
} |
|||
|
|||
.cover-play-btn .play-icon { |
|||
width: 50rpx; |
|||
height: 50rpx; |
|||
margin-left: 8rpx; |
|||
filter: brightness(2); |
|||
} |
|||
|
|||
/* 全屏控制栏 */ |
|||
.fullscreen-controls { |
|||
position: absolute; |
|||
top: 0; |
|||
left: 0; |
|||
right: 0; |
|||
bottom: 0; |
|||
z-index: 10; |
|||
opacity: 1; |
|||
transition: opacity 0.3s; |
|||
} |
|||
|
|||
.fullscreen-controls.hidden { |
|||
opacity: 0; |
|||
pointer-events: none; |
|||
} |
|||
|
|||
.fullscreen-top-bar { |
|||
padding: 80rpx 40rpx 0; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: space-between; |
|||
background: linear-gradient(to bottom, rgba(0, 0, 0, 0.9) 0%, rgba(0, 0, 0, 0) 100%); |
|||
height: 120rpx; |
|||
} |
|||
|
|||
.top-bar-left { |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 16rpx; |
|||
padding: 16rpx 28rpx; |
|||
background: rgba(0, 0, 0, 0.6); |
|||
border-radius: 40rpx; |
|||
backdrop-filter: blur(20rpx); |
|||
border: 1rpx solid rgba(255, 255, 255, 0.1); |
|||
} |
|||
|
|||
.back-text { |
|||
color: #fff; |
|||
font-size: 28rpx; |
|||
font-weight: 500; |
|||
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.3); |
|||
} |
|||
|
|||
.top-bar-title { |
|||
font-size: 32rpx; |
|||
color: #fff; |
|||
max-width: 400rpx; |
|||
overflow: hidden; |
|||
text-overflow: ellipsis; |
|||
white-space: nowrap; |
|||
font-weight: 600; |
|||
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.5); |
|||
padding: 12rpx 24rpx; |
|||
background: rgba(0, 0, 0, 0.5); |
|||
border-radius: 20rpx; |
|||
backdrop-filter: blur(10rpx); |
|||
} |
|||
|
|||
/* 中间播放按钮 */ |
|||
.center-play-btn { |
|||
position: absolute; |
|||
top: 50%; |
|||
left: 50%; |
|||
transform: translate(-50%, -50%); |
|||
width: 160rpx; |
|||
height: 160rpx; |
|||
background: rgba(0, 0, 0, 0.7); |
|||
backdrop-filter: blur(30rpx); |
|||
border-radius: 50%; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
border: 4rpx solid rgba(255, 255, 255, 0.3); |
|||
z-index: 20; |
|||
box-shadow: |
|||
0 12rpx 60rpx rgba(0, 0, 0, 0.5), |
|||
inset 0 0 0 1rpx rgba(255, 255, 255, 0.1); |
|||
animation: pulse 2s infinite; |
|||
} |
|||
|
|||
@keyframes pulse { |
|||
0% { |
|||
box-shadow: |
|||
0 12rpx 60rpx rgba(0, 0, 0, 0.5), |
|||
inset 0 0 0 1rpx rgba(255, 255, 255, 0.1); |
|||
} |
|||
50% { |
|||
box-shadow: |
|||
0 12rpx 80rpx rgba(52, 152, 219, 0.4), |
|||
inset 0 0 0 1rpx rgba(52, 152, 219, 0.3); |
|||
} |
|||
100% { |
|||
box-shadow: |
|||
0 12rpx 60rpx rgba(0, 0, 0, 0.5), |
|||
inset 0 0 0 1rpx rgba(255, 255, 255, 0.1); |
|||
} |
|||
} |
|||
|
|||
.play-large-icon { |
|||
width: 70rpx; |
|||
height: 70rpx; |
|||
margin-left: 10rpx; |
|||
filter: brightness(2) drop-shadow(0 4rpx 8rpx rgba(0, 0, 0, 0.5)); |
|||
} |
|||
|
|||
.play-ripple { |
|||
position: absolute; |
|||
top: 50%; |
|||
left: 50%; |
|||
transform: translate(-50%, -50%); |
|||
width: 0; |
|||
height: 0; |
|||
border-radius: 50%; |
|||
border: 2rpx solid rgba(255, 255, 255, 0.4); |
|||
animation: ripple 2s infinite; |
|||
} |
|||
|
|||
@keyframes ripple { |
|||
0% { |
|||
width: 0; |
|||
height: 0; |
|||
opacity: 1; |
|||
} |
|||
100% { |
|||
width: 240rpx; |
|||
height: 240rpx; |
|||
opacity: 0; |
|||
} |
|||
} |
|||
|
|||
/* 底部控制栏 */ |
|||
.fullscreen-bottom-bar { |
|||
position: absolute; |
|||
bottom: 0; |
|||
left: 0; |
|||
right: 0; |
|||
padding: 0 40rpx 80rpx; |
|||
background: linear-gradient(to top, rgba(0, 0, 0, 0.95) 0%, rgba(0, 0, 0, 0) 100%); |
|||
} |
|||
|
|||
.fullscreen-progress { |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 24rpx; |
|||
margin-bottom: 60rpx; |
|||
padding: 0 20rpx; |
|||
} |
|||
|
|||
.progress-time { |
|||
font-size: 26rpx; |
|||
color: rgba(255, 255, 255, 0.95); |
|||
min-width: 90rpx; |
|||
text-align: center; |
|||
font-weight: 500; |
|||
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.5); |
|||
} |
|||
|
|||
.progress-slider-container { |
|||
flex: 1; |
|||
position: relative; |
|||
height: 8rpx; |
|||
background: rgba(255, 255, 255, 0.2); |
|||
border-radius: 4rpx; |
|||
overflow: hidden; |
|||
} |
|||
|
|||
.progress-bg { |
|||
width: 100%; |
|||
height: 100%; |
|||
background: rgba(255, 255, 255, 0.1); |
|||
border-radius: 4rpx; |
|||
} |
|||
|
|||
.progress-current { |
|||
height: 100%; |
|||
background: linear-gradient(90deg, #3498db, #9b59b6); |
|||
border-radius: 4rpx; |
|||
transition: width 0.1s; |
|||
} |
|||
|
|||
.progress-thumb { |
|||
position: absolute; |
|||
top: 50%; |
|||
transform: translate(-50%, -50%); |
|||
width: 28rpx; |
|||
height: 28rpx; |
|||
background: #fff; |
|||
border-radius: 50%; |
|||
box-shadow: |
|||
0 4rpx 12rpx rgba(0, 0, 0, 0.5), |
|||
0 0 0 2rpx #3498db; |
|||
} |
|||
|
|||
.bottom-controls { |
|||
display: flex; |
|||
justify-content: center; |
|||
align-items: center; |
|||
gap: 80rpx; |
|||
padding: 30rpx 50rpx; |
|||
background: rgba(0, 0, 0, 0.7); |
|||
border-radius: 80rpx; |
|||
backdrop-filter: blur(30rpx); |
|||
margin: 0 auto; |
|||
max-width: 700rpx; |
|||
border: 1rpx solid rgba(255, 255, 255, 0.1); |
|||
} |
|||
|
|||
.control-item { |
|||
width: 90rpx; |
|||
height: 90rpx; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
border-radius: 50%; |
|||
background: rgba(255, 255, 255, 0.1); |
|||
transition: all 0.2s; |
|||
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.3); |
|||
} |
|||
|
|||
.control-item:active { |
|||
background: rgba(255, 255, 255, 0.2); |
|||
transform: scale(0.9); |
|||
} |
|||
|
|||
.control-icon { |
|||
width: 44rpx; |
|||
height: 44rpx; |
|||
filter: brightness(2) drop-shadow(0 2rpx 4rpx rgba(0, 0, 0, 0.3)); |
|||
} |
|||
|
|||
.speed-text { |
|||
color: #fff; |
|||
font-size: 30rpx; |
|||
font-weight: 700; |
|||
background: linear-gradient(135deg, #3498db, #9b59b6); |
|||
padding: 12rpx 28rpx; |
|||
border-radius: 40rpx; |
|||
box-shadow: 0 6rpx 24rpx rgba(52, 152, 219, 0.4); |
|||
} |
|||
|
|||
/* 触摸控制区域 */ |
|||
.touch-control-left, |
|||
.touch-control-center, |
|||
.touch-control-right { |
|||
position: absolute; |
|||
top: 120rpx; |
|||
bottom: 200rpx; |
|||
z-index: 5; |
|||
} |
|||
|
|||
.touch-control-left { |
|||
left: 0; |
|||
width: 30%; |
|||
} |
|||
|
|||
.touch-control-center { |
|||
left: 30%; |
|||
width: 40%; |
|||
} |
|||
|
|||
.touch-control-right { |
|||
left: 70%; |
|||
width: 30%; |
|||
} |
|||
|
|||
/* 非全屏播放按钮 */ |
|||
.normal-play-btn { |
|||
position: absolute; |
|||
top: 50%; |
|||
left: 50%; |
|||
transform: translate(-50%, -50%); |
|||
width: 120rpx; |
|||
height: 120rpx; |
|||
background: rgba(0, 0, 0, 0.7); |
|||
border-radius: 50%; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
z-index: 3; |
|||
border: 3rpx solid rgba(255, 255, 255, 0.3); |
|||
backdrop-filter: blur(10rpx); |
|||
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.5); |
|||
} |
|||
|
|||
.normal-play-btn .play-icon { |
|||
width: 50rpx; |
|||
height: 50rpx; |
|||
margin-left: 8rpx; |
|||
filter: brightness(2); |
|||
} |
|||
|
|||
/* 视频信息区域 */ |
|||
.video-info-section { |
|||
height: calc(100vh - 500rpx); |
|||
background: linear-gradient(180deg, #1a1e2c 0%, #0f172a 100%); |
|||
} |
|||
|
|||
.video-header { |
|||
padding: 50rpx 40rpx 40rpx; |
|||
border-bottom: 1rpx solid rgba(255, 255, 255, 0.08); |
|||
} |
|||
|
|||
.video-title { |
|||
font-size: 40rpx; |
|||
font-weight: 800; |
|||
color: #fff; |
|||
line-height: 1.3; |
|||
margin-bottom: 30rpx; |
|||
letter-spacing: 0.5rpx; |
|||
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.3); |
|||
} |
|||
|
|||
.video-description { |
|||
font-size: 30rpx; |
|||
color: rgba(255, 255, 255, 0.85); |
|||
line-height: 1.6; |
|||
margin-bottom: 40rpx; |
|||
} |
|||
|
|||
.video-stats { |
|||
display: flex; |
|||
gap: 30rpx; |
|||
flex-wrap: wrap; |
|||
} |
|||
|
|||
.stat-item { |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 14rpx; |
|||
font-size: 26rpx; |
|||
color: rgba(255, 255, 255, 0.7); |
|||
padding: 14rpx 28rpx; |
|||
background: rgba(255, 255, 255, 0.08); |
|||
border-radius: 40rpx; |
|||
backdrop-filter: blur(10rpx); |
|||
} |
|||
|
|||
|
|||
|
|||
/* 发布者信息 */ |
|||
.publisher-section { |
|||
padding: 40rpx; |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 30rpx; |
|||
border-bottom: 1rpx solid rgba(255, 255, 255, 0.08); |
|||
background: rgba(255, 255, 255, 0.03); |
|||
margin: 0 20rpx; |
|||
border-radius: 24rpx; |
|||
margin-top: 20rpx; |
|||
} |
|||
|
|||
.publisher-avatar { |
|||
width: 120rpx; |
|||
height: 120rpx; |
|||
border-radius: 50%; |
|||
border: 4rpx solid #3498db; |
|||
box-shadow: |
|||
0 12rpx 40rpx rgba(52, 152, 219, 0.5), |
|||
0 0 0 1rpx rgba(255, 255, 255, 0.1); |
|||
background: #1e293b; |
|||
} |
|||
|
|||
.publisher-info { |
|||
flex: 1; |
|||
} |
|||
|
|||
.publisher-name { |
|||
font-size: 36rpx; |
|||
font-weight: 700; |
|||
color: #fff; |
|||
margin-bottom: 12rpx; |
|||
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.3); |
|||
} |
|||
|
|||
.publisher-desc { |
|||
font-size: 26rpx; |
|||
color: rgba(255, 255, 255, 0.6); |
|||
line-height: 1.4; |
|||
} |
|||
|
|||
/* 标签区域 */ |
|||
.tags-section { |
|||
padding: 40rpx; |
|||
background: rgba(255, 255, 255, 0.03); |
|||
margin: 20rpx; |
|||
border-radius: 24rpx; |
|||
} |
|||
|
|||
.section-title { |
|||
font-size: 32rpx; |
|||
font-weight: 700; |
|||
color: #fff; |
|||
margin-bottom: 30rpx; |
|||
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.3); |
|||
} |
|||
|
|||
.tags-container { |
|||
display: flex; |
|||
flex-wrap: wrap; |
|||
gap: 20rpx; |
|||
} |
|||
|
|||
.tag-item { |
|||
padding: 16rpx 36rpx; |
|||
background: linear-gradient(135deg, #3498db, #2ecc71); |
|||
color: white; |
|||
border-radius: 40rpx; |
|||
font-size: 26rpx; |
|||
font-weight: 600; |
|||
box-shadow: 0 6rpx 24rpx rgba(52, 152, 219, 0.4); |
|||
transition: all 0.2s; |
|||
} |
|||
|
|||
.tag-item:active { |
|||
transform: scale(0.95); |
|||
box-shadow: 0 4rpx 16rpx rgba(52, 152, 219, 0.3); |
|||
} |
|||
|
|||
/* 加载中 */ |
|||
.loading-container { |
|||
position: fixed; |
|||
top: 0; |
|||
left: 0; |
|||
right: 0; |
|||
bottom: 0; |
|||
background: linear-gradient(135deg, #0f172a 0%, #1a1e2c 100%); |
|||
display: flex; |
|||
flex-direction: column; |
|||
align-items: center; |
|||
justify-content: center; |
|||
z-index: 2000; |
|||
} |
|||
|
|||
.loading-spinner { |
|||
position: relative; |
|||
width: 140rpx; |
|||
height: 140rpx; |
|||
margin-bottom: 50rpx; |
|||
} |
|||
|
|||
.spinner-circle { |
|||
position: absolute; |
|||
top: 50%; |
|||
left: 50%; |
|||
transform: translate(-50%, -50%); |
|||
width: 100%; |
|||
height: 100%; |
|||
border: 10rpx solid transparent; |
|||
border-top-color: #3498db; |
|||
border-right-color: #3498db; |
|||
border-radius: 50%; |
|||
animation: spinnerRotate 1.2s linear infinite; |
|||
box-shadow: 0 0 20rpx rgba(52, 152, 219, 0.3); |
|||
} |
|||
|
|||
@keyframes spinnerRotate { |
|||
0% { |
|||
transform: translate(-50%, -50%) rotate(0deg); |
|||
} |
|||
100% { |
|||
transform: translate(-50%, -50%) rotate(360deg); |
|||
} |
|||
} |
|||
|
|||
.loading-text { |
|||
font-size: 32rpx; |
|||
color: rgba(255, 255, 255, 0.8); |
|||
letter-spacing: 3rpx; |
|||
font-weight: 500; |
|||
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.3); |
|||
} |
|||
|
|||
/* 错误提示 */ |
|||
.error-container { |
|||
position: fixed; |
|||
top: 50%; |
|||
left: 50%; |
|||
transform: translate(-50%, -50%); |
|||
display: flex; |
|||
flex-direction: column; |
|||
align-items: center; |
|||
justify-content: center; |
|||
z-index: 2000; |
|||
background: linear-gradient(135deg, rgba(15, 23, 42, 0.95), rgba(26, 30, 44, 0.95)); |
|||
padding: 80rpx 60rpx; |
|||
border-radius: 40rpx; |
|||
backdrop-filter: blur(40rpx); |
|||
border: 1rpx solid rgba(255, 255, 255, 0.1); |
|||
min-width: 500rpx; |
|||
box-shadow: |
|||
0 20rpx 80rpx rgba(0, 0, 0, 0.5), |
|||
inset 0 1rpx 0 rgba(255, 255, 255, 0.1); |
|||
} |
|||
|
|||
.error-icon { |
|||
width: 140rpx; |
|||
height: 140rpx; |
|||
margin-bottom: 40rpx; |
|||
filter: brightness(1.5) drop-shadow(0 4rpx 12rpx rgba(0, 0, 0, 0.3)); |
|||
} |
|||
|
|||
.error-text { |
|||
font-size: 36rpx; |
|||
color: #fff; |
|||
margin-bottom: 50rpx; |
|||
font-weight: 700; |
|||
text-align: center; |
|||
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.3); |
|||
} |
|||
|
|||
.retry-btn { |
|||
padding: 24rpx 80rpx; |
|||
background: linear-gradient(135deg, #3498db, #2980b9); |
|||
color: white; |
|||
border-radius: 50rpx; |
|||
font-size: 32rpx; |
|||
font-weight: 700; |
|||
box-shadow: |
|||
0 12rpx 40rpx rgba(52, 152, 219, 0.5), |
|||
inset 0 1rpx 0 rgba(255, 255, 255, 0.3); |
|||
transition: all 0.2s; |
|||
letter-spacing: 2rpx; |
|||
} |
|||
|
|||
.retry-btn:active { |
|||
transform: scale(0.95); |
|||
box-shadow: |
|||
0 6rpx 24rpx rgba(52, 152, 219, 0.4), |
|||
inset 0 1rpx 0 rgba(255, 255, 255, 0.2); |
|||
} |
|||
|
|||
/* 响应式调整 */ |
|||
@media screen and (max-width: 750rpx) { |
|||
.video-player-section { |
|||
height: 450rpx; |
|||
} |
|||
|
|||
.video-info-section { |
|||
height: calc(100vh - 450rpx); |
|||
} |
|||
|
|||
.video-title { |
|||
font-size: 36rpx; |
|||
} |
|||
|
|||
.video-description { |
|||
font-size: 28rpx; |
|||
} |
|||
|
|||
.bottom-controls { |
|||
gap: 60rpx; |
|||
padding: 25rpx 40rpx; |
|||
max-width: 650rpx; |
|||
} |
|||
|
|||
.center-play-btn { |
|||
width: 140rpx; |
|||
height: 140rpx; |
|||
} |
|||
|
|||
.play-large-icon { |
|||
width: 60rpx; |
|||
height: 60rpx; |
|||
} |
|||
|
|||
.fullscreen-top-bar { |
|||
padding: 60rpx 30rpx 0; |
|||
} |
|||
|
|||
.fullscreen-bottom-bar { |
|||
padding: 0 30rpx 60rpx; |
|||
} |
|||
} |
|||
@ -1,489 +1,393 @@ |
|||
import http from '../../../utils/api' |
|||
const baseUrl = require('../../../utils/baseUrl') |
|||
|
|||
Page({ |
|||
data: { |
|||
currentTab: 0, |
|||
searchKeyword: '', |
|||
articleActiveCategory: 0, |
|||
videoActiveCategory: 'all', |
|||
baseUrl:baseUrl, |
|||
// 文章数据
|
|||
articles: [ |
|||
{ |
|||
id: 1, |
|||
title: '微信小程序开发最佳实践', |
|||
description: '深度解析小程序性能优化方案,助你打造流畅用户体验', |
|||
cover: 'https://via.placeholder.com/400x300/3498db/ffffff?text=小程序', |
|||
category: '技术干货', |
|||
author: '张技术', |
|||
readTime: 15, |
|||
date: '2024-01-15', |
|||
views: 2345, |
|||
likes: 189, |
|||
categoryId: 'tech' |
|||
}, |
|||
{ |
|||
id: 2, |
|||
title: '企业数字化转型的关键路径', |
|||
description: '传统企业如何通过数字化实现业务增长与效率提升', |
|||
cover: 'https://via.placeholder.com/400x300/2ecc71/ffffff?text=数字化', |
|||
category: '商业思维', |
|||
author: '李商业', |
|||
readTime: 20, |
|||
date: '2024-01-12', |
|||
views: 1567, |
|||
likes: 234, |
|||
categoryId: 'business' |
|||
}, |
|||
{ |
|||
id: 3, |
|||
title: '高效团队管理的7个秘诀', |
|||
description: '打造高绩效团队的实用方法与技巧', |
|||
cover: 'https://via.placeholder.com/400x300/e74c3c/ffffff?text=管理', |
|||
category: '管理技能', |
|||
author: '王经理', |
|||
readTime: 12, |
|||
date: '2024-01-10', |
|||
views: 3210, |
|||
likes: 456, |
|||
categoryId: 'management' |
|||
}, |
|||
{ |
|||
id: 4, |
|||
title: '前端工程师的职业发展路径', |
|||
description: '从初级到专家,前端工程师的成长路线图', |
|||
cover: 'https://via.placeholder.com/400x300/9b59b6/ffffff?text=前端', |
|||
category: '职业发展', |
|||
author: '赵前端', |
|||
readTime: 18, |
|||
date: '2024-01-08', |
|||
views: 1890, |
|||
likes: 345, |
|||
categoryId: 'career' |
|||
}, |
|||
{ |
|||
id: 5, |
|||
title: '云原生架构设计与实践', |
|||
description: '基于云原生技术的现代应用架构方案', |
|||
cover: 'https://via.placeholder.com/400x300/1abc9c/ffffff?text=云原生', |
|||
category: '技术干货', |
|||
author: '钱架构', |
|||
readTime: 25, |
|||
date: '2024-01-05', |
|||
views: 2789, |
|||
likes: 512, |
|||
categoryId: 'tech' |
|||
}, |
|||
{ |
|||
id: 6, |
|||
title: '人工智能在商业中的应用', |
|||
description: 'AI技术如何赋能传统行业实现智能化升级', |
|||
cover: 'https://via.placeholder.com/400x300/f39c12/ffffff?text=AI', |
|||
category: '商业思维', |
|||
author: '孙智能', |
|||
readTime: 22, |
|||
date: '2024-01-03', |
|||
views: 4321, |
|||
likes: 678, |
|||
categoryId: 'business' |
|||
} |
|||
], |
|||
videoActiveCategory: '全部', |
|||
baseUrl: baseUrl, |
|||
|
|||
// 视频数据
|
|||
videos: [ |
|||
{ |
|||
id: 1, |
|||
title: 'Vue.js 3.0 核心源码解析', |
|||
cover: 'https://via.placeholder.com/400x300/3498db/ffffff?text=Vue3', |
|||
duration: '45:23', |
|||
instructor: { |
|||
name: '前端大神', |
|||
avatar: 'https://via.placeholder.com/40/3498db/ffffff?text=FS' |
|||
}, |
|||
level: '高级', |
|||
views: '12.3k', |
|||
tags: ['Vue', '前端', '源码'], |
|||
category: 'tech', |
|||
isRecommended: true |
|||
}, |
|||
{ |
|||
id: 2, |
|||
title: 'React Hooks 深度解析', |
|||
cover: 'https://via.placeholder.com/400x300/2ecc71/ffffff?text=React', |
|||
duration: '38:45', |
|||
instructor: { |
|||
name: 'React专家', |
|||
avatar: 'https://via.placeholder.com/40/2ecc71/ffffff?text=RE' |
|||
}, |
|||
level: '中级', |
|||
views: '8.7k', |
|||
tags: ['React', 'Hooks', '前端'], |
|||
category: 'tech', |
|||
isRecommended: true |
|||
}, |
|||
{ |
|||
id: 3, |
|||
title: '产品经理必备的7个思维模型', |
|||
cover: 'https://via.placeholder.com/400x300/e74c3c/ffffff?text=产品', |
|||
duration: '52:12', |
|||
instructor: { |
|||
name: '产品专家', |
|||
avatar: 'https://via.placeholder.com/40/e74c3c/ffffff?text=PM' |
|||
}, |
|||
level: '中级', |
|||
views: '15.2k', |
|||
tags: ['产品', '思维', '方法论'], |
|||
category: 'business', |
|||
isRecommended: false |
|||
}, |
|||
{ |
|||
id: 4, |
|||
title: 'Python 数据分析实战', |
|||
cover: 'https://via.placeholder.com/400x300/9b59b6/ffffff?text=Python', |
|||
duration: '41:36', |
|||
instructor: { |
|||
name: '数据科学家', |
|||
avatar: 'https://via.placeholder.com/40/9b59b6/ffffff?text=DS' |
|||
}, |
|||
level: '初级', |
|||
views: '23.4k', |
|||
tags: ['Python', '数据分析', '实战'], |
|||
category: 'tech', |
|||
isRecommended: true |
|||
}, |
|||
{ |
|||
id: 5, |
|||
title: '高效沟通的艺术与技巧', |
|||
cover: 'https://via.placeholder.com/400x300/1abc9c/ffffff?text=沟通', |
|||
duration: '36:58', |
|||
instructor: { |
|||
name: '沟通专家', |
|||
avatar: 'https://via.placeholder.com/40/1abc9c/ffffff?text=CM' |
|||
}, |
|||
level: '初级', |
|||
views: '9.8k', |
|||
tags: ['沟通', '职场', '技巧'], |
|||
category: 'management', |
|||
isRecommended: false |
|||
}, |
|||
{ |
|||
id: 6, |
|||
title: 'Node.js 高性能服务开发', |
|||
cover: 'https://via.placeholder.com/400x300/f39c12/ffffff?text=Node', |
|||
duration: '55:42', |
|||
instructor: { |
|||
name: '后端架构师', |
|||
avatar: 'https://via.placeholder.com/40/f39c12/ffffff?text=BE' |
|||
}, |
|||
level: '高级', |
|||
views: '6.5k', |
|||
tags: ['Node.js', '后端', '性能'], |
|||
category: 'tech', |
|||
isRecommended: false |
|||
}, |
|||
{ |
|||
id: 7, |
|||
title: '职场晋升的底层逻辑', |
|||
cover: 'https://via.placeholder.com/400x300/34495e/ffffff?text=晋升', |
|||
duration: '33:27', |
|||
instructor: { |
|||
name: '职业规划师', |
|||
avatar: 'https://via.placeholder.com/40/34495e/ffffff?text=CD' |
|||
}, |
|||
level: '中级', |
|||
views: '11.2k', |
|||
tags: ['职场', '晋升', '规划'], |
|||
category: 'career', |
|||
isRecommended: true |
|||
}, |
|||
{ |
|||
id: 8, |
|||
title: 'JavaScript 入门到精通', |
|||
cover: 'https://via.placeholder.com/400x300/e74c3c/ffffff?text=JS', |
|||
duration: '1:15:30', |
|||
instructor: { |
|||
name: '前端导师', |
|||
avatar: 'https://via.placeholder.com/40/e74c3c/ffffff?text=JS' |
|||
}, |
|||
level: '初级', |
|||
views: '45.6k', |
|||
tags: ['JavaScript', '入门', '教程'], |
|||
category: 'tech', |
|||
isRecommended: true |
|||
// 文章相关
|
|||
allArticles: [], |
|||
filteredArticles: [], |
|||
articlePageNo: 1, |
|||
articlePageSize: 10, |
|||
articleTotal: 0, |
|||
articleHasMore: true, |
|||
articleLoading: false, |
|||
articleRequested: false, // 标记文章数据是否已请求
|
|||
|
|||
// 视频相关
|
|||
allVideos: [], |
|||
filteredVideos: [], |
|||
videoPageNo: 1, |
|||
videoPageSize: 10, |
|||
videoTotal: 0, |
|||
videoHasMore: true, |
|||
videoLoading: false, |
|||
videoRequested: false, // 标记视频数据是否已请求
|
|||
|
|||
// 字典数据
|
|||
wzzd: [], |
|||
videoType: [] |
|||
}, |
|||
|
|||
onLoad() { |
|||
this.getarticleZd() |
|||
this.getvideoZd() |
|||
this.loadInitialData() |
|||
}, |
|||
|
|||
// 加载初始数据
|
|||
loadInitialData() { |
|||
if (this.data.currentTab === 0) { |
|||
this.resetArticleParams() |
|||
this.getArticleList(true) |
|||
} else { |
|||
this.resetVideoParams() |
|||
this.getVideoList(true) |
|||
} |
|||
}, |
|||
|
|||
// 文章字典
|
|||
getarticleZd() { |
|||
http.articleZd({ |
|||
data: { |
|||
dictType: 'article_category' |
|||
}, |
|||
{ |
|||
id: 9, |
|||
title: '商业计划书撰写指南', |
|||
cover: 'https://via.placeholder.com/400x300/1abc9c/ffffff?text=商业', |
|||
duration: '28:45', |
|||
instructor: { |
|||
name: '商业顾问', |
|||
avatar: 'https://via.placeholder.com/40/1abc9c/ffffff?text=BC' |
|||
}, |
|||
level: '中级', |
|||
views: '7.3k', |
|||
tags: ['商业', '创业', '计划书'], |
|||
category: 'business', |
|||
isRecommended: false |
|||
success: res => { |
|||
this.setData({ |
|||
wzzd: res.rows |
|||
}) |
|||
} |
|||
}) |
|||
}, |
|||
|
|||
// 视频字典
|
|||
getvideoZd() { |
|||
http.videoZd({ |
|||
data: { |
|||
dictType: 'video_category' |
|||
}, |
|||
{ |
|||
id: 10, |
|||
title: '团队领导力提升训练', |
|||
cover: 'https://via.placeholder.com/400x300/9b59b6/ffffff?text=领导力', |
|||
duration: '49:18', |
|||
instructor: { |
|||
name: '领导力教练', |
|||
avatar: 'https://via.placeholder.com/40/9b59b6/ffffff?text=LC' |
|||
}, |
|||
level: '高级', |
|||
views: '5.9k', |
|||
tags: ['领导力', '管理', '团队'], |
|||
category: 'management', |
|||
isRecommended: true |
|||
success: res => { |
|||
this.setData({ |
|||
videoType: res.rows |
|||
}) |
|||
} |
|||
], |
|||
|
|||
filteredArticles: [], |
|||
filteredVideos: [] |
|||
}) |
|||
}, |
|||
|
|||
onLoad() { |
|||
// 初始化数据
|
|||
// 获取文章列表(支持搜索和分类)
|
|||
getArticleList(isRefresh = false) { |
|||
if (this.data.articleLoading && !isRefresh) return |
|||
|
|||
this.setData({ |
|||
// filteredArticles: this.data.articles,
|
|||
filteredVideos: this.data.videos |
|||
}); |
|||
this.getarticle() |
|||
this.getarticleZd() |
|||
articleLoading: true |
|||
}) |
|||
|
|||
const params = { |
|||
pageNo: this.data.articlePageNo, |
|||
pageSize: this.data.articlePageSize |
|||
} |
|||
|
|||
// 添加搜索关键词
|
|||
if (this.data.searchKeyword && this.data.searchKeyword.trim()) { |
|||
params.searchKey = this.data.searchKeyword.trim() |
|||
} |
|||
|
|||
// 添加分类筛选
|
|||
if (this.data.articleActiveCategory !== 0) { |
|||
const categoryDict = this.data.wzzd.find(item => item.dictSort === this.data.articleActiveCategory) |
|||
if (categoryDict) { |
|||
params.category = categoryDict.dictLabel |
|||
} |
|||
} |
|||
|
|||
http.article({ |
|||
data: params, |
|||
success: res => { |
|||
const newArticles = res.rows || [] |
|||
const total = res.total || 0 |
|||
|
|||
let allArticles, filteredArticles |
|||
if (isRefresh) { |
|||
allArticles = newArticles |
|||
filteredArticles = newArticles |
|||
} else { |
|||
allArticles = [...this.data.allArticles, ...newArticles] |
|||
filteredArticles = [...this.data.filteredArticles, ...newArticles] |
|||
} |
|||
|
|||
const hasMore = this.data.articlePageNo * this.data.articlePageSize < total |
|||
|
|||
this.setData({ |
|||
allArticles, |
|||
filteredArticles, |
|||
articleTotal: total, |
|||
articleHasMore: hasMore, |
|||
articleLoading: false, |
|||
articleRequested: true |
|||
}) |
|||
|
|||
// 如果是下拉刷新,停止刷新动画
|
|||
if (isRefresh) { |
|||
wx.stopPullDownRefresh() |
|||
} |
|||
}, |
|||
fail: () => { |
|||
this.setData({ |
|||
articleLoading: false |
|||
}) |
|||
wx.showToast({ |
|||
title: '加载失败', |
|||
icon: 'error', |
|||
duration: 2000 |
|||
}) |
|||
} |
|||
}) |
|||
}, |
|||
|
|||
// 文章列表
|
|||
getarticle(){ |
|||
http.article({ |
|||
data:{}, |
|||
success:res=>{ |
|||
console.log(1111,res); |
|||
this.setData({ |
|||
filteredArticles:res.rows |
|||
}) |
|||
} |
|||
// 获取视频列表(支持搜索和分类)
|
|||
getVideoList(isRefresh = false) { |
|||
if (this.data.videoLoading && !isRefresh) return |
|||
|
|||
this.setData({ |
|||
videoLoading: true |
|||
}) |
|||
|
|||
const params = { |
|||
pageNo: this.data.videoPageNo, |
|||
pageSize: this.data.videoPageSize |
|||
} |
|||
|
|||
// 添加搜索关键词
|
|||
if (this.data.searchKeyword && this.data.searchKeyword.trim()) { |
|||
params.searchKey = this.data.searchKeyword.trim() |
|||
} |
|||
|
|||
// 添加分类筛选
|
|||
if (this.data.videoActiveCategory !== '全部') { |
|||
params.category = this.data.videoActiveCategory |
|||
} |
|||
|
|||
http.videoList({ |
|||
data: params, |
|||
success: res => { |
|||
const newVideos = res.rows || [] |
|||
const total = res.total || 0 |
|||
|
|||
let allVideos, filteredVideos |
|||
if (isRefresh) { |
|||
allVideos = newVideos |
|||
filteredVideos = newVideos |
|||
} else { |
|||
allVideos = [...this.data.allVideos, ...newVideos] |
|||
filteredVideos = [...this.data.filteredVideos, ...newVideos] |
|||
} |
|||
|
|||
const hasMore = this.data.videoPageNo * this.data.videoPageSize < total |
|||
|
|||
this.setData({ |
|||
allVideos, |
|||
filteredVideos, |
|||
videoTotal: total, |
|||
videoHasMore: hasMore, |
|||
videoLoading: false, |
|||
videoRequested: true |
|||
}) |
|||
|
|||
// 如果是下拉刷新,停止刷新动画
|
|||
if (isRefresh) { |
|||
wx.stopPullDownRefresh() |
|||
} |
|||
}, |
|||
fail: () => { |
|||
this.setData({ |
|||
videoLoading: false |
|||
}) |
|||
wx.showToast({ |
|||
title: '加载失败', |
|||
icon: 'error', |
|||
duration: 2000 |
|||
}) |
|||
}, |
|||
} |
|||
}) |
|||
}, |
|||
|
|||
// 文章字典
|
|||
getarticleZd(){ |
|||
http.articleZd({ |
|||
data:{ |
|||
dictType:'article_category' |
|||
}, |
|||
success:res=>{ |
|||
console.log(2222,res); |
|||
this.setData({ |
|||
wzzd:res.rows |
|||
}) |
|||
} |
|||
}) |
|||
}, |
|||
// 重置文章参数
|
|||
resetArticleParams() { |
|||
this.setData({ |
|||
articlePageNo: 1, |
|||
articleTotal: 0, |
|||
articleHasMore: true, |
|||
allArticles: [], |
|||
filteredArticles: [] |
|||
}) |
|||
}, |
|||
|
|||
// 重置视频参数
|
|||
resetVideoParams() { |
|||
this.setData({ |
|||
videoPageNo: 1, |
|||
videoTotal: 0, |
|||
videoHasMore: true, |
|||
allVideos: [], |
|||
filteredVideos: [] |
|||
}) |
|||
}, |
|||
|
|||
// 切换主选项卡
|
|||
switchTab(e) { |
|||
const tab = parseInt(e.currentTarget.dataset.tab); |
|||
const tab = parseInt(e.currentTarget.dataset.tab) |
|||
if (tab === this.data.currentTab) return |
|||
|
|||
this.setData({ |
|||
currentTab: tab, |
|||
searchKeyword: '' |
|||
}); |
|||
}) |
|||
|
|||
// 重置筛选结果
|
|||
if (tab === 0) { |
|||
this.setData({ |
|||
filteredArticles: this.filterArticles('', this.data.articleActiveCategory) |
|||
}); |
|||
} else { |
|||
this.setData({ |
|||
filteredVideos: this.filterVideos('', this.data.videoActiveCategory) |
|||
}); |
|||
} |
|||
// 延迟加载新tab的数据,确保切换动画完成
|
|||
setTimeout(() => { |
|||
if (tab === 0) { |
|||
if (!this.data.articleRequested) { |
|||
this.resetArticleParams() |
|||
this.getArticleList(true) |
|||
} |
|||
} else { |
|||
if (!this.data.videoRequested) { |
|||
this.resetVideoParams() |
|||
this.getVideoList(true) |
|||
} |
|||
} |
|||
}, 300) |
|||
}, |
|||
|
|||
// 搜索输入
|
|||
onSearchInput(e) { |
|||
const keyword = e.detail.value; |
|||
this.setData({ searchKeyword: keyword }); |
|||
const keyword = e.detail.value |
|||
this.setData({ |
|||
searchKeyword: keyword |
|||
}) |
|||
|
|||
// 防抖处理,500ms后执行搜索
|
|||
clearTimeout(this.searchTimer) |
|||
this.searchTimer = setTimeout(() => { |
|||
this.performSearch() |
|||
}, 500) |
|||
}, |
|||
|
|||
// 执行搜索
|
|||
performSearch() { |
|||
if (this.data.currentTab === 0) { |
|||
this.setData({ |
|||
filteredArticles: this.filterArticles(keyword, this.data.articleActiveCategory) |
|||
}); |
|||
this.resetArticleParams() |
|||
this.getArticleList(true) |
|||
} else { |
|||
this.setData({ |
|||
filteredVideos: this.filterVideos(keyword, this.data.videoActiveCategory) |
|||
}); |
|||
this.resetVideoParams() |
|||
this.getVideoList(true) |
|||
} |
|||
}, |
|||
|
|||
// 选择文章分类
|
|||
selectArticleCategory(e) { |
|||
console.log(3333,e); |
|||
const category = Number(e.currentTarget.dataset.category); |
|||
this.setData({ |
|||
const category = Number(e.currentTarget.dataset.category) |
|||
if (category === this.data.articleActiveCategory) return |
|||
|
|||
this.setData({ |
|||
articleActiveCategory: category |
|||
}); |
|||
}) |
|||
|
|||
// 重置并重新加载数据
|
|||
this.resetArticleParams() |
|||
this.getArticleList(true) |
|||
}, |
|||
|
|||
// 选择视频分类
|
|||
selectVideoCategory(e) { |
|||
const category = e.currentTarget.dataset.category; |
|||
this.setData({ videoActiveCategory: category }); |
|||
this.setData({ |
|||
filteredVideos: this.filterVideos(this.data.searchKeyword, category) |
|||
}); |
|||
}, |
|||
|
|||
// 筛选文章函数
|
|||
filterArticles(keyword, category) { |
|||
let filtered = this.data.articles; |
|||
|
|||
// 按分类筛选
|
|||
if (category !== 'all') { |
|||
const categoryMap = { |
|||
'tech': '技术干货', |
|||
'business': '商业思维', |
|||
'management': '管理技能', |
|||
'career': '职业发展' |
|||
}; |
|||
|
|||
filtered = filtered.filter(item => item.category === categoryMap[category]); |
|||
} |
|||
|
|||
// 按关键词筛选
|
|||
if (keyword) { |
|||
const lowerKeyword = keyword.toLowerCase(); |
|||
filtered = filtered.filter(item => |
|||
item.title.toLowerCase().includes(lowerKeyword) || |
|||
item.description.toLowerCase().includes(lowerKeyword) || |
|||
item.author.toLowerCase().includes(lowerKeyword) || |
|||
item.category.toLowerCase().includes(lowerKeyword) |
|||
); |
|||
} |
|||
|
|||
return filtered; |
|||
}, |
|||
|
|||
// 筛选视频函数
|
|||
filterVideos(keyword, category) { |
|||
let filtered = this.data.videos; |
|||
|
|||
// 按分类筛选
|
|||
if (category !== 'all') { |
|||
switch (category) { |
|||
case 'recommend': |
|||
filtered = filtered.filter(item => item.isRecommended); |
|||
break; |
|||
case 'tech': |
|||
filtered = filtered.filter(item => item.category === 'tech'); |
|||
break; |
|||
case 'business': |
|||
filtered = filtered.filter(item => item.category === 'business'); |
|||
break; |
|||
case 'management': |
|||
filtered = filtered.filter(item => item.category === 'management'); |
|||
break; |
|||
case 'career': |
|||
filtered = filtered.filter(item => item.category === 'career'); |
|||
break; |
|||
case 'beginner': |
|||
filtered = filtered.filter(item => item.level === '初级'); |
|||
break; |
|||
case 'advanced': |
|||
filtered = filtered.filter(item => item.level === '中级' || item.level === '高级'); |
|||
break; |
|||
} |
|||
} |
|||
const category = e.currentTarget.dataset.category |
|||
if (category === this.data.videoActiveCategory) return |
|||
|
|||
// 按关键词筛选
|
|||
if (keyword) { |
|||
const lowerKeyword = keyword.toLowerCase(); |
|||
filtered = filtered.filter(item => |
|||
item.title.toLowerCase().includes(lowerKeyword) || |
|||
item.tags.some(tag => tag.toLowerCase().includes(lowerKeyword)) || |
|||
item.instructor.name.toLowerCase().includes(lowerKeyword) || |
|||
item.level.toLowerCase().includes(lowerKeyword) |
|||
); |
|||
} |
|||
this.setData({ |
|||
videoActiveCategory: category |
|||
}) |
|||
|
|||
return filtered; |
|||
// 重置并重新加载数据
|
|||
this.resetVideoParams() |
|||
this.getVideoList(true) |
|||
}, |
|||
|
|||
// 查看文章详情
|
|||
viewArticleDetail(e) { |
|||
const id = e.currentTarget.dataset.id; |
|||
|
|||
const id = e.currentTarget.dataset.id |
|||
console.log('查看文章详情', id) |
|||
wx.navigateTo({ |
|||
url: `/pagesB/pages/wzDetails/wzDetails?id=${id}` |
|||
}) |
|||
}, |
|||
|
|||
// 播放视频
|
|||
playVideo(e) { |
|||
const id = e.currentTarget.dataset.id; |
|||
|
|||
|
|||
const id = e.currentTarget.dataset.id |
|||
console.log('播放视频', id) |
|||
wx.navigateTo({ |
|||
url: `/pagesB/pages/spDetails/spDetails?id=${id}` |
|||
}) |
|||
}, |
|||
|
|||
// 下拉刷新
|
|||
onPullDownRefresh() { |
|||
wx.showLoading({ |
|||
title: '刷新中...' |
|||
}); |
|||
wx.showNavigationBarLoading() |
|||
|
|||
// 模拟网络请求
|
|||
setTimeout(() => { |
|||
// 重置数据
|
|||
this.setData({ |
|||
filteredArticles: this.data.articles, |
|||
filteredVideos: this.data.videos, |
|||
articleActiveCategory: 'all', |
|||
videoActiveCategory: 'all', |
|||
searchKeyword: '' |
|||
}); |
|||
|
|||
wx.hideLoading(); |
|||
wx.stopPullDownRefresh(); |
|||
wx.showToast({ |
|||
title: '刷新成功', |
|||
icon: 'success' |
|||
}); |
|||
}, 1000); |
|||
if (this.data.currentTab === 0) { |
|||
this.resetArticleParams() |
|||
this.getArticleList(true) |
|||
} else { |
|||
this.resetVideoParams() |
|||
this.getVideoList(true) |
|||
} |
|||
|
|||
// 重置搜索关键词
|
|||
this.setData({ |
|||
searchKeyword: '' |
|||
}) |
|||
}, |
|||
|
|||
// 上拉加载更多
|
|||
onReachBottom() { |
|||
if (this.data.currentTab === 0) { |
|||
// 文章加载更多
|
|||
wx.showLoading({ |
|||
title: '加载更多文章...' |
|||
}); |
|||
|
|||
setTimeout(() => { |
|||
// 这里可以添加更多文章数据
|
|||
wx.hideLoading(); |
|||
if (!this.data.articleHasMore || this.data.articleLoading) { |
|||
wx.showToast({ |
|||
title: '没有更多了', |
|||
icon: 'none' |
|||
}); |
|||
}, 1000); |
|||
} else { |
|||
// 视频加载更多
|
|||
wx.showLoading({ |
|||
title: '加载更多视频...' |
|||
}); |
|||
title: '已加载全部', |
|||
icon: 'none', |
|||
duration: 1000 |
|||
}) |
|||
return |
|||
} |
|||
|
|||
setTimeout(() => { |
|||
// 这里可以添加更多视频数据
|
|||
wx.hideLoading(); |
|||
this.setData({ |
|||
articlePageNo: this.data.articlePageNo + 1 |
|||
}) |
|||
this.getArticleList(false) |
|||
} else { |
|||
if (!this.data.videoHasMore || this.data.videoLoading) { |
|||
wx.showToast({ |
|||
title: '没有更多了', |
|||
icon: 'none' |
|||
}); |
|||
}, 1000); |
|||
title: '已加载全部', |
|||
icon: 'none', |
|||
duration: 1000 |
|||
}) |
|||
return |
|||
} |
|||
|
|||
this.setData({ |
|||
videoPageNo: this.data.videoPageNo + 1 |
|||
}) |
|||
this.getVideoList(false) |
|||
} |
|||
}, |
|||
|
|||
// 页面显示时刷新数据
|
|||
onShow() { |
|||
// 如果需要返回时刷新数据,可以在这里调用
|
|||
} |
|||
}); |
|||
}) |
|||
@ -0,0 +1,99 @@ |
|||
import http from '../../../utils/api' |
|||
const baseUrl = require('../../../utils/baseUrl') |
|||
|
|||
Page({ |
|||
data: { |
|||
baseUrl: baseUrl, |
|||
article: {}, // 文章详情
|
|||
loading: true, // 加载状态
|
|||
}, |
|||
|
|||
onLoad(options) { |
|||
this.getArticleDetails(options.id) |
|||
}, |
|||
|
|||
// 获取文章详情
|
|||
getArticleDetails(id) { |
|||
this.setData({ |
|||
loading: true |
|||
}) |
|||
|
|||
http.articleDetails({ |
|||
data: { id }, |
|||
success: res => { |
|||
console.log('文章详情:', res) |
|||
if (res.code === 200 && res.data) { |
|||
// 解析富文本内容
|
|||
const article = res.data |
|||
// 这里可以处理富文本内容的样式
|
|||
article.content = this.formatRichContent(article.content) |
|||
|
|||
this.setData({ |
|||
article: article, |
|||
loading: false |
|||
}) |
|||
|
|||
// 设置页面标题
|
|||
wx.setNavigationBarTitle({ |
|||
title: article.title.substring(0, 10) + (article.title.length > 10 ? '...' : '') |
|||
}) |
|||
} else { |
|||
wx.showToast({ |
|||
title: '文章不存在', |
|||
icon: 'error' |
|||
}) |
|||
setTimeout(() => { |
|||
wx.navigateBack() |
|||
}, 2000) |
|||
} |
|||
}, |
|||
fail: err => { |
|||
console.error('获取文章详情失败:', err) |
|||
this.setData({ |
|||
loading: false |
|||
}) |
|||
wx.showToast({ |
|||
title: '加载失败', |
|||
icon: 'error' |
|||
}) |
|||
} |
|||
}) |
|||
}, |
|||
|
|||
|
|||
// 格式化富文本内容
|
|||
formatRichContent(content) { |
|||
// 这里可以添加自定义样式,比如给图片添加样式
|
|||
return content.replace(/<img/g, '<img style="max-width:100%;height:auto;border-radius:15rpx;margin:20rpx 0;"') |
|||
}, |
|||
|
|||
|
|||
|
|||
// 滚动事件
|
|||
onPageScroll(e) { |
|||
this.setData({ |
|||
scrollTop: e.scrollTop |
|||
}) |
|||
}, |
|||
|
|||
// 回到顶部
|
|||
scrollToTop() { |
|||
wx.pageScrollTo({ |
|||
scrollTop: 0, |
|||
duration: 300 |
|||
}) |
|||
}, |
|||
|
|||
// 下拉刷新
|
|||
onPullDownRefresh() { |
|||
|
|||
}, |
|||
|
|||
|
|||
|
|||
|
|||
// 页面卸载
|
|||
onUnload() { |
|||
|
|||
} |
|||
}) |
|||
@ -0,0 +1,4 @@ |
|||
{ |
|||
"navigationBarTitleText":"文章详情", |
|||
"usingComponents": {} |
|||
} |
|||
@ -0,0 +1,62 @@ |
|||
<view class="article-detail-container"> |
|||
|
|||
<!-- 文章内容 --> |
|||
<scroll-view class="article-content" scroll-y> |
|||
<!-- 文章封面 --> |
|||
<view class="article-cover"> |
|||
<image |
|||
class="cover-image" |
|||
src="{{baseUrl + article.coverImage}}" |
|||
mode="widthFix" |
|||
lazy-load |
|||
></image> |
|||
<view class="cover-overlay"></view> |
|||
<view class="cover-gradient"></view> |
|||
<view class="cover-category">{{article.category}}</view> |
|||
</view> |
|||
|
|||
<!-- 文章主体 --> |
|||
<view class="article-body"> |
|||
<!-- 标题区域 --> |
|||
<view class="title-section"> |
|||
<view class="article-title">{{article.title}}</view> |
|||
<view class="article-subtitle">{{article.subtitle}}</view> |
|||
|
|||
<!-- 专家信息 --> |
|||
<view class="expert-info"> |
|||
<image class="expert-avatar" src="{{baseUrl + article.expertAvatar}}"></image> |
|||
<view class="expert-detail"> |
|||
<view class="expert-name">{{article.expertName}}</view> |
|||
<view class="publish-time">{{article.publishTime}}</view> |
|||
</view> |
|||
<view class="view-count"> |
|||
<image class="view-icon" src="/pagesB/images/lll.png"></image> |
|||
<text>{{article.viewCount}} 阅读</text> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
|
|||
<!-- 文章内容(富文本) --> |
|||
<view class="rich-content"> |
|||
<rich-text nodes="{{article.content}}"></rich-text> |
|||
</view> |
|||
|
|||
</view> |
|||
</scroll-view> |
|||
|
|||
|
|||
<!-- 加载中 --> |
|||
<view class="loading-container" wx:if="{{loading}}"> |
|||
<view class="loading-spinner"> |
|||
<view class="spinner-circle"></view> |
|||
<view class="spinner-circle circle-2"></view> |
|||
<view class="spinner-circle circle-3"></view> |
|||
</view> |
|||
<text class="loading-text">加载中...</text> |
|||
</view> |
|||
|
|||
<!-- 回到顶部按钮 --> |
|||
<view class="back-to-top" wx:if="{{scrollTop > 400}}" catchtap="scrollToTop"> |
|||
<image class="top-icon" src="/pagesB/images/top.png"></image> |
|||
</view> |
|||
</view> |
|||
@ -0,0 +1,322 @@ |
|||
.article-detail-container { |
|||
min-height: 100vh; |
|||
background: #f8fafc; |
|||
position: relative; |
|||
} |
|||
|
|||
|
|||
/* 文章内容容器 */ |
|||
.article-content { |
|||
height: 100vh; |
|||
position: relative; |
|||
} |
|||
|
|||
/* 文章封面 */ |
|||
.article-cover { |
|||
position: relative; |
|||
height: 500rpx; |
|||
overflow: hidden; |
|||
} |
|||
|
|||
.cover-image { |
|||
width: 100%; |
|||
height: 100%; |
|||
object-fit: cover; |
|||
} |
|||
|
|||
.cover-overlay { |
|||
position: absolute; |
|||
top: 0; |
|||
left: 0; |
|||
right: 0; |
|||
bottom: 0; |
|||
background: linear-gradient(to bottom, rgba(0, 0, 0, 0.3), rgba(0, 0, 0, 0.1)); |
|||
} |
|||
|
|||
.cover-gradient { |
|||
position: absolute; |
|||
bottom: 0; |
|||
left: 0; |
|||
right: 0; |
|||
height: 200rpx; |
|||
background: linear-gradient(to top, rgba(0, 0, 0, 0.6), transparent); |
|||
} |
|||
|
|||
.cover-category { |
|||
position: absolute; |
|||
top: 40rpx; |
|||
right: 40rpx; |
|||
background: rgba(52, 152, 219, 0.95); |
|||
backdrop-filter: blur(10rpx); |
|||
color: white; |
|||
padding: 12rpx 28rpx; |
|||
border-radius: 25rpx; |
|||
font-size: 24rpx; |
|||
font-weight: 600; |
|||
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.2); |
|||
z-index: 2; |
|||
} |
|||
|
|||
/* 文章主体 */ |
|||
.article-body { |
|||
background: white; |
|||
border-radius: 40rpx 40rpx 0 0; |
|||
margin-top: -40rpx; |
|||
position: relative; |
|||
z-index: 10; |
|||
padding-bottom: 50rpx; |
|||
} |
|||
|
|||
.title-section { |
|||
padding: 50rpx 40rpx 40rpx; |
|||
border-bottom: 1rpx solid #f1f5f9; |
|||
} |
|||
|
|||
.article-title { |
|||
font-size: 44rpx; |
|||
font-weight: 800; |
|||
color: #1e293b; |
|||
line-height: 1.4; |
|||
margin-bottom: 20rpx; |
|||
letter-spacing: 0.5rpx; |
|||
} |
|||
|
|||
.article-subtitle { |
|||
font-size: 30rpx; |
|||
color: #64748b; |
|||
line-height: 1.6; |
|||
margin-bottom: 40rpx; |
|||
padding-bottom: 30rpx; |
|||
border-bottom: 1rpx dashed #e2e8f0; |
|||
} |
|||
|
|||
/* 专家信息 */ |
|||
.expert-info { |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 20rpx; |
|||
margin-top: 30rpx; |
|||
} |
|||
|
|||
.expert-avatar { |
|||
width: 80rpx; |
|||
height: 80rpx; |
|||
border-radius: 50%; |
|||
border: 3rpx solid white; |
|||
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1); |
|||
} |
|||
|
|||
.expert-detail { |
|||
flex: 1; |
|||
} |
|||
|
|||
.expert-name { |
|||
font-size: 28rpx; |
|||
font-weight: 600; |
|||
color: #1e293b; |
|||
margin-bottom: 8rpx; |
|||
} |
|||
|
|||
.publish-time { |
|||
font-size: 24rpx; |
|||
color: #94a3b8; |
|||
} |
|||
|
|||
.view-count { |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 10rpx; |
|||
background: rgba(226, 232, 240, 0.3); |
|||
padding: 12rpx 20rpx; |
|||
border-radius: 25rpx; |
|||
font-size: 24rpx; |
|||
color: #64748b; |
|||
} |
|||
|
|||
.view-icon { |
|||
width: 28rpx; |
|||
height: 28rpx; |
|||
opacity: 0.7; |
|||
} |
|||
|
|||
/* 富文本内容 */ |
|||
.rich-content { |
|||
padding: 40rpx; |
|||
font-size: 32rpx; |
|||
line-height: 1.8; |
|||
color: #334155; |
|||
} |
|||
|
|||
.rich-content h1, |
|||
.rich-content h2, |
|||
.rich-content h3 { |
|||
color: #1e293b; |
|||
margin: 40rpx 0 20rpx; |
|||
font-weight: 700; |
|||
} |
|||
|
|||
.rich-content h1 { |
|||
font-size: 40rpx; |
|||
} |
|||
|
|||
.rich-content h2 { |
|||
font-size: 36rpx; |
|||
} |
|||
|
|||
.rich-content h3 { |
|||
font-size: 32rpx; |
|||
} |
|||
|
|||
.rich-content p { |
|||
margin-bottom: 30rpx; |
|||
} |
|||
|
|||
.rich-content ul, |
|||
.rich-content ol { |
|||
margin: 20rpx 0 20rpx 40rpx; |
|||
} |
|||
|
|||
.rich-content li { |
|||
margin-bottom: 15rpx; |
|||
position: relative; |
|||
} |
|||
|
|||
.rich-content ul li:before { |
|||
content: '•'; |
|||
color: #3498db; |
|||
font-weight: bold; |
|||
position: absolute; |
|||
left: -25rpx; |
|||
} |
|||
|
|||
.rich-content strong { |
|||
color: #1e293b; |
|||
font-weight: 700; |
|||
} |
|||
|
|||
.rich-content a { |
|||
color: #3498db; |
|||
text-decoration: underline; |
|||
} |
|||
|
|||
|
|||
|
|||
|
|||
/* 加载中 */ |
|||
.loading-container { |
|||
position: fixed; |
|||
top: 0; |
|||
left: 0; |
|||
right: 0; |
|||
bottom: 0; |
|||
background: rgba(255, 255, 255, 0.95); |
|||
display: flex; |
|||
flex-direction: column; |
|||
align-items: center; |
|||
justify-content: center; |
|||
z-index: 2000; |
|||
} |
|||
|
|||
.loading-spinner { |
|||
position: relative; |
|||
width: 120rpx; |
|||
height: 120rpx; |
|||
margin-bottom: 40rpx; |
|||
} |
|||
|
|||
.spinner-circle { |
|||
position: absolute; |
|||
top: 50%; |
|||
left: 50%; |
|||
transform: translate(-50%, -50%); |
|||
width: 100%; |
|||
height: 100%; |
|||
border: 8rpx solid rgba(52, 152, 219, 0.1); |
|||
border-radius: 50%; |
|||
animation: spinnerRotate 2s linear infinite; |
|||
} |
|||
|
|||
.spinner-circle.circle-2 { |
|||
width: 80%; |
|||
height: 80%; |
|||
border-width: 6rpx; |
|||
animation-delay: 0.2s; |
|||
} |
|||
|
|||
.spinner-circle.circle-3 { |
|||
width: 60%; |
|||
height: 60%; |
|||
border-width: 4rpx; |
|||
animation-delay: 0.4s; |
|||
} |
|||
|
|||
@keyframes spinnerRotate { |
|||
0% { |
|||
transform: translate(-50%, -50%) rotate(0deg); |
|||
} |
|||
100% { |
|||
transform: translate(-50%, -50%) rotate(360deg); |
|||
} |
|||
} |
|||
|
|||
.loading-text { |
|||
font-size: 28rpx; |
|||
color: #64748b; |
|||
letter-spacing: 1rpx; |
|||
} |
|||
|
|||
/* 回到顶部 */ |
|||
.back-to-top { |
|||
position: fixed; |
|||
right: 40rpx; |
|||
bottom: 160rpx; |
|||
width: 80rpx; |
|||
height: 80rpx; |
|||
background: rgba(255, 255, 255, 0.95); |
|||
backdrop-filter: blur(10rpx); |
|||
border-radius: 50%; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
box-shadow: 0 8rpx 30rpx rgba(0, 0, 0, 0.15); |
|||
border: 1rpx solid rgba(226, 232, 240, 0.8); |
|||
transition: all 0.3s ease; |
|||
z-index: 90; |
|||
} |
|||
|
|||
.back-to-top:active { |
|||
transform: scale(0.95); |
|||
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.2); |
|||
} |
|||
|
|||
.top-icon { |
|||
width: 36rpx; |
|||
height: 36rpx; |
|||
} |
|||
|
|||
/* 响应式调整 */ |
|||
@media screen and (max-width: 750rpx) { |
|||
.article-cover { |
|||
height: 400rpx; |
|||
} |
|||
|
|||
.article-title { |
|||
font-size: 38rpx; |
|||
} |
|||
|
|||
.article-subtitle { |
|||
font-size: 28rpx; |
|||
} |
|||
|
|||
.rich-content { |
|||
font-size: 30rpx; |
|||
} |
|||
|
|||
.action-bar { |
|||
padding: 0 30rpx; |
|||
} |
|||
|
|||
.comment-btn { |
|||
padding: 18rpx 30rpx; |
|||
} |
|||
} |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue