9.5下午提交
This commit is contained in:
parent
ad39805b83
commit
c711c53959
21
api/api.js
21
api/api.js
@ -401,6 +401,27 @@ const api = {
|
||||
polularScienceArticleListIndexNew(data){
|
||||
return request('/expertAPI/polularScienceArticleListIndexNew', data, 'post', false);
|
||||
},
|
||||
meetingListBySearch(data){
|
||||
return request('/expertAPI/meetingListBySearch', data, 'post', false);
|
||||
},
|
||||
getExpertByUuid(data){
|
||||
return request('/expertAPI/getExpertByUuid', data, 'post', false);
|
||||
},
|
||||
getUserIcon(data){
|
||||
return request('/expertAPI/getUserIcon', data, 'post', false);
|
||||
},
|
||||
addUserIcon(data){
|
||||
return request('/expertAPI/addUserIcon', data, 'post', false);
|
||||
},
|
||||
getDiseaseList(data){
|
||||
return request('/expertAPI/disease', data, 'post', false);
|
||||
},
|
||||
getPositionList(data){
|
||||
return request('/expertAPI/positionList', data, 'post', false);
|
||||
},
|
||||
getOfficeList(data){
|
||||
return request('/expertAPI/officeList', data, 'post', false);
|
||||
},
|
||||
}
|
||||
|
||||
export default api
|
||||
22
pages.json
22
pages.json
@ -208,6 +208,28 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "writeInfo/writeInfo",
|
||||
"style": {
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarRightButton":{ "hide": true},
|
||||
"navigationBarTitleText": "uni-app分页",
|
||||
"app": {
|
||||
"bounce": "none"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "myApplication/myApplication",
|
||||
"style": {
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarRightButton":{ "hide": true},
|
||||
"navigationBarTitleText": "uni-app分页",
|
||||
"app": {
|
||||
"bounce": "none"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "searchVideo/searchVideo",
|
||||
"style": {
|
||||
|
||||
@ -112,7 +112,7 @@
|
||||
switch(courseType) {
|
||||
case 'course':
|
||||
navTo({
|
||||
url: '/pages_course/index/index'
|
||||
url: '/pages_course/course/course'
|
||||
})
|
||||
break;
|
||||
case 'video':
|
||||
|
||||
@ -18,7 +18,7 @@
|
||||
placeholderColor="#999999"
|
||||
></uni-search-bar>
|
||||
</view>
|
||||
<view class="header-right">
|
||||
<view class="header-right" @click="goMessage">
|
||||
<view class="message-icon">
|
||||
<uni-icons type="email" size="32" color="#fff"></uni-icons>
|
||||
<view v-if="hasUnread" class="red-dot"></view>
|
||||
@ -142,8 +142,9 @@
|
||||
<view class="replay-section">
|
||||
<view class="section-header">
|
||||
<text class="section-title-text">精彩回放</text>
|
||||
<view class="more-link">
|
||||
<text class="more-text">更多<uni-icons type="right" size="32rpx" color="#999"></uni-icons></text>
|
||||
<view class="more-link" @click="onReplayMore">
|
||||
<text class="more-text">更多</text>
|
||||
<uni-icons type="right" size="32rpx" color="#999"></uni-icons>
|
||||
</view>
|
||||
</view>
|
||||
<view class="replay-grid">
|
||||
@ -280,6 +281,11 @@
|
||||
url:'/pages_app/search/search'
|
||||
})
|
||||
}
|
||||
const onReplayMore=()=>{
|
||||
navTo({
|
||||
url:'/pages_app/video/video'
|
||||
})
|
||||
}
|
||||
// 计算头部背景色
|
||||
const headerBgColor = computed(() => {
|
||||
// 获取屏幕宽度来计算rpx到px的转换比例
|
||||
@ -439,28 +445,9 @@
|
||||
// 精彩回放点击事件
|
||||
const onReplayClick = (item) => {
|
||||
console.log('点击精彩回放:', item);
|
||||
uni.showToast({
|
||||
title: `查看${item.content}`,
|
||||
icon: 'none'
|
||||
});
|
||||
|
||||
// 根据类型跳转到不同页面
|
||||
switch(item.type) {
|
||||
case 'case_discussion':
|
||||
uni.navigateTo({
|
||||
url: '/pages/discussion/detail?id=' + item.id
|
||||
});
|
||||
break;
|
||||
case 'new_topic':
|
||||
uni.navigateTo({
|
||||
url: '/pages/topic/detail?id=' + item.id
|
||||
});
|
||||
break;
|
||||
default:
|
||||
uni.navigateTo({
|
||||
url: '/pages/replay/detail?id=' + item.id
|
||||
});
|
||||
}
|
||||
navTo({
|
||||
url: '/pages_course/course_detail/course_detail?id=' + item.id
|
||||
})
|
||||
};
|
||||
|
||||
// 网格点击事件
|
||||
@ -480,9 +467,9 @@
|
||||
}else if(index==5){
|
||||
url='/pages_app/ppt/ppt'
|
||||
}else if(index==6){
|
||||
url='/pages_course/index/index'
|
||||
url='/pages_course/course/course'
|
||||
}else{
|
||||
url='/pages_app/search/search'
|
||||
url='/pages_app/myApplication/myApplication'
|
||||
}
|
||||
navTo({
|
||||
url:url
|
||||
@ -512,10 +499,15 @@
|
||||
|
||||
// 查看更多实用指南
|
||||
const onGuideMore = () => {
|
||||
console.log('查看更多实用指南');
|
||||
uni.navigateTo({
|
||||
url: '/pages/guide/list'
|
||||
});
|
||||
if(currentGuideTab.value==0){
|
||||
navTo({
|
||||
url: '/pages_app/zhinan/zhinan'
|
||||
});
|
||||
}else{
|
||||
navTo({
|
||||
url: '/pages_app/ppt/ppt'
|
||||
});
|
||||
}
|
||||
};
|
||||
const goNews=()=>{
|
||||
let url=docUrl+signInfo.news.path;
|
||||
@ -538,7 +530,9 @@
|
||||
const onBannerClick = (item,index) => {
|
||||
if (!item) return;
|
||||
if(index==0){
|
||||
|
||||
navTo({
|
||||
url:'/pages_app/personInfo/personInfo'
|
||||
})
|
||||
}else{
|
||||
// 如果存在外链path,优先按平台打开
|
||||
if (item.path) {
|
||||
@ -600,6 +594,11 @@
|
||||
})
|
||||
// #endif
|
||||
};
|
||||
const goMessage=()=>{
|
||||
navTo({
|
||||
url: '/pages_app/msg/msg'
|
||||
})
|
||||
}
|
||||
|
||||
// 实用指南点击事件
|
||||
const onGuideClick = (item) => {
|
||||
|
||||
@ -33,49 +33,54 @@
|
||||
:enable-back-to-top="true"
|
||||
>
|
||||
<!-- 时间标题 -->
|
||||
<view class="time-header">2025年08月</view>
|
||||
|
||||
|
||||
<!-- 会议列表 -->
|
||||
<view class="meeting-list" v-if="meetingList.length > 0">
|
||||
<view class="meeting-item" v-for="(item, index) in meetingList" :key="item.id || index">
|
||||
<!-- 左侧日期标识 -->
|
||||
<view class="date-tag" :style="{backgroundColor: item.tagColor}">
|
||||
<text class="date-text">{{ item.date }}</text>
|
||||
</view>
|
||||
<template v-for="(group, groupIndex) in groupedMeetings" :key="groupIndex">
|
||||
<view class="time-header">{{ group.monthYear }}</view>
|
||||
<view class="meetcell" v-for="(item, index) in group.items" :key="item.id || index">
|
||||
<view class="meeting-item">
|
||||
<!-- 左侧日期标识 -->
|
||||
<view class="date-tag" :style="{backgroundColor: item.tagColor}">
|
||||
<text class="date-text">{{ formatDay(item.begin_date) }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 会议内容 -->
|
||||
<view class="meeting-content">
|
||||
<view class="meeting-title">{{ item.title }}</view>
|
||||
<view class="meeting-poster" @click="playVideo(item)">
|
||||
<image
|
||||
:src="docUrl+item.liveimg"
|
||||
class="poster-image"
|
||||
mode="aspectFill"
|
||||
@error="onImageError"
|
||||
@load="onImageLoad"
|
||||
:data-item-id="item.id"
|
||||
></image>
|
||||
<view class="play-btn">
|
||||
<up-image :src="playImg" width="108rpx" height="108rpx" ></up-image>
|
||||
</view>
|
||||
<view class="preview-tag" v-if="item.status === 'upcoming'">预告</view>
|
||||
<view class="live-tag" v-else-if="item.status === 'live'">直播中</view>
|
||||
<view class="replay-tag" v-else-if="item.status === 'replay'">回放</view>
|
||||
</view>
|
||||
<view class="meeting-info">
|
||||
<view class="info-item">
|
||||
<view class="timebox">
|
||||
<up-image :src="timeImg" width="24rpx" height="24rpx" ></up-image>
|
||||
<!-- 会议内容 -->
|
||||
<view class="meeting-content">
|
||||
<view class="meeting-title">{{ item.title }}</view>
|
||||
<view class="meeting-poster" @click="playVideo(item)">
|
||||
<image
|
||||
:src="docUrl+item.liveimg"
|
||||
class="poster-image"
|
||||
mode="aspectFill"
|
||||
@error="onImageError"
|
||||
@load="onImageLoad"
|
||||
:data-item-id="item.id"
|
||||
></image>
|
||||
<view class="play-btn">
|
||||
<up-image :src="playImg" width="108rpx" height="108rpx" ></up-image>
|
||||
</view>
|
||||
<view class="status-tag" :class="getStatusClass(item.status_code)">
|
||||
{{ item.status }}
|
||||
</view>
|
||||
</view>
|
||||
<view class="meeting-info">
|
||||
<view class="info-item">
|
||||
<view class="timebox">
|
||||
<up-image :src="timeImg" width="24rpx" height="24rpx" ></up-image>
|
||||
</view>
|
||||
<text class="info-text">{{ formatDate(item.begin_date,'YYYY.MM.DD')+'-'+formatDate(item.end_date,'YYYY.MM.DD') }}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<uni-icons type="location" size="14" color="#999"></uni-icons>
|
||||
<text class="info-text">{{ item.location }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="info-text">{{ item.time }}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<uni-icons type="location" size="14" color="#999"></uni-icons>
|
||||
<text class="info-text">{{ item.location }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
@ -84,7 +89,6 @@
|
||||
<uni-icons type="calendar" size="80" color="#ccc"></uni-icons>
|
||||
</view>
|
||||
<text class="empty-text">暂无会议数据</text>
|
||||
<text class="empty-subtext">请稍后再试或调整筛选条件</text>
|
||||
</view>
|
||||
|
||||
<!-- 加载更多提示 -->
|
||||
@ -93,10 +97,10 @@
|
||||
<uni-icons type="spinner-cycle" size="20" color="#999"></uni-icons>
|
||||
<text class="load-more-text">加载中...</text>
|
||||
</view>
|
||||
<view class="load-more-content" v-else-if="hasMoreData">
|
||||
<view class="load-more-content" v-else-if="hasMoreData && meetingList.length > 0">
|
||||
<text class="load-more-text">上拉加载更多</text>
|
||||
</view>
|
||||
<view class="load-more-content" v-else>
|
||||
<view class="load-more-content" v-else-if="meetingList.length > 0 && !isLoadingMore">
|
||||
<text class="load-more-text">没有更多数据了</text>
|
||||
</view>
|
||||
</view>
|
||||
@ -153,7 +157,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref,nextTick} from 'vue';
|
||||
import { ref, nextTick, computed } from 'vue';
|
||||
import { onShow } from "@dcloudio/uni-app";
|
||||
import CustomTabbar from '@/components/tabBar/tabBar.vue';
|
||||
import api from '@/api/api.js';
|
||||
@ -162,11 +166,12 @@
|
||||
import playImg from "@/static/bofang.png"
|
||||
import timeImg from "@/static/play_long.png"
|
||||
import docUrl from "@/utils/docUrl"
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
// 弹窗状态
|
||||
const isTimePopupShow = ref(false);
|
||||
const isLocationPopupShow = ref(false);
|
||||
const selectedMonth = ref('all');
|
||||
const selectedMonth = ref('');
|
||||
const selectedProvince = ref('');
|
||||
|
||||
// 下拉刷新和上拉加载状态
|
||||
@ -179,81 +184,241 @@
|
||||
const scrollTop = ref(0);
|
||||
|
||||
// 月份数据
|
||||
const monthList = ref([
|
||||
{ value: 'all', label: '所有' },
|
||||
{ value: '8', label: '8月' },
|
||||
{ value: '9', label: '9月' },
|
||||
{ value: '10', label: '10月' },
|
||||
{ value: '11', label: '11月' },
|
||||
{ value: '12', label: '12月' },
|
||||
{ value: '1', label: '1月' },
|
||||
{ value: '2', label: '2月' },
|
||||
{ value: '3', label: '3月' },
|
||||
{ value: '4', label: '4月' },
|
||||
{ value: '5', label: '5月' },
|
||||
{ value: '6', label: '6月' },
|
||||
{ value: '7', label: '7月' }
|
||||
]);
|
||||
// 生成月份列表
|
||||
const monthList = ref([]);
|
||||
const generateMonthList = () => {
|
||||
const months = [];
|
||||
let index=-1;
|
||||
const currentDate = new Date();
|
||||
const currentMonth = currentDate.getMonth() + 1; // 当前月份 (1-12)
|
||||
const currentYear = currentDate.getFullYear();
|
||||
|
||||
// 添加"所有"选项
|
||||
months.push({ value: '', label: '所有' });
|
||||
|
||||
// 从当前月份到12月
|
||||
for (let month = currentMonth; month <= 12; month++) {
|
||||
index++;
|
||||
let monthLabel = '';
|
||||
if (month === currentMonth) {
|
||||
monthLabel = `${month}月`;
|
||||
} else if (month === currentMonth + 1) {
|
||||
monthLabel = `${month}月`;
|
||||
} else {
|
||||
monthLabel = `${month}月`;
|
||||
}
|
||||
|
||||
months.push({
|
||||
value: index,
|
||||
label: monthLabel,
|
||||
year: currentYear,
|
||||
month: month
|
||||
});
|
||||
}
|
||||
|
||||
// 从1月到当前月份(不包括当前月份,避免重复)
|
||||
for (let month = 1; month < currentMonth; month++) {
|
||||
index++;
|
||||
months.push({
|
||||
value: index,
|
||||
label: `${month}月`,
|
||||
year: currentYear + 1, // 下一年的月份
|
||||
month: month
|
||||
});
|
||||
}
|
||||
|
||||
monthList.value = months;
|
||||
console.log('生成的月份列表:', monthList.value);
|
||||
};
|
||||
// 省份数据
|
||||
const provinceList = ref([
|
||||
{ code: 'all', name: '全国' },
|
||||
{ code: 'beijing', name: '北京市' },
|
||||
{ code: 'tianjin', name: '天津市' },
|
||||
{ code: 'hebei', name: '河北省' },
|
||||
{ code: 'shanxi', name: '山西省' },
|
||||
{ code: 'neimenggu', name: '内蒙古...' },
|
||||
{ code: 'liaoning', name: '辽宁省' },
|
||||
{ code: 'jilin', name: '吉林省' },
|
||||
{ code: 'heilongjiang', name: '黑龙江省' },
|
||||
{ code: 'shanghai', name: '上海市' },
|
||||
{ code: 'jiangsu', name: '江苏省' },
|
||||
{ code: 'zhejiang', name: '浙江省' },
|
||||
{ code: 'anhui', name: '安徽省' },
|
||||
{ code: 'fujian', name: '福建省' },
|
||||
{ code: 'jiangxi', name: '江西省' },
|
||||
{ code: 'shandong', name: '山东省' },
|
||||
{ code: 'henan', name: '河南省' },
|
||||
{ code: 'hubei', name: '湖北省' },
|
||||
{ code: 'hunan', name: '湖南省' },
|
||||
{ code: 'guangdong', name: '广东省' },
|
||||
{ code: 'guangxi', name: '广西壮...' },
|
||||
{ code: 'hainan', name: '海南省' },
|
||||
{ code: 'chongqing', name: '重庆市' },
|
||||
{ code: 'sichuan', name: '四川省' },
|
||||
{ code: 'guizhou', name: '贵州省' },
|
||||
{ code: 'yunnan', name: '云南省' },
|
||||
{ code: 'xizang', name: '西藏自...' },
|
||||
{ code: 'shaanxi', name: '陕西省' },
|
||||
{ code: 'gansu', name: '甘肃省' },
|
||||
{ code: 'qinghai', name: '青海省' },
|
||||
{ code: 'ningxia', name: '宁夏回...' },
|
||||
{ code: 'xinjiang', name: '新疆维...' },
|
||||
{ code: 'taiwan', name: '台湾省' },
|
||||
{ code: 'hongkong', name: '香港特...' },
|
||||
{ code: 'macao', name: '澳门特...' }
|
||||
{ code: '', name: '全国' },
|
||||
{ code: '北京', name: '北京市' },
|
||||
{ code: '天津', name: '天津市' },
|
||||
{ code: '河北', name: '河北省' },
|
||||
{ code: '山西', name: '山西省' },
|
||||
{ code: '内蒙古', name: '内蒙古自治区' },
|
||||
{ code: '辽宁', name: '辽宁省' },
|
||||
{ code: '吉林', name: '吉林省' },
|
||||
{ code: '黑龙江', name: '黑龙江省' },
|
||||
{ code: '上海', name: '上海市' },
|
||||
{ code: '江苏', name: '江苏省' },
|
||||
{ code: '浙江', name: '浙江省' },
|
||||
{ code: '安徽', name: '安徽省' },
|
||||
{ code: '福建', name: '福建省' },
|
||||
{ code: '江西', name: '江西省' },
|
||||
{ code: '山东', name: '山东省' },
|
||||
{ code: '河南', name: '河南省' },
|
||||
{ code: '湖北', name: '湖北省' },
|
||||
{ code: '湖南', name: '湖南省' },
|
||||
{ code: '广东', name: '广东省' },
|
||||
{ code: '广西', name: '广西壮族自治区' },
|
||||
{ code: '海南', name: '海南省' },
|
||||
{ code: '重庆', name: '重庆市' },
|
||||
{ code: '四川', name: '四川省' },
|
||||
{ code: '贵州', name: '贵州省' },
|
||||
{ code: '云南', name: '云南省' },
|
||||
{ code: '西藏', name: '西藏自治区' },
|
||||
{ code: '陕西', name: '陕西省' },
|
||||
{ code: '甘肃', name: '甘肃省' },
|
||||
{ code: '青海', name: '青海省' },
|
||||
{ code: '宁夏', name: '宁夏回族自治区' },
|
||||
{ code: '新疆', name: '新疆维吾尔自治区' },
|
||||
{ code: '台湾', name: '台湾省' },
|
||||
{ code: '香港', name: '香港特别行政区' },
|
||||
{ code: '澳门', name: '澳门特别行政区' }
|
||||
]);
|
||||
|
||||
// 会议列表数据
|
||||
const meetingList = ref([]);
|
||||
|
||||
// 按年月分组的会议列表
|
||||
const groupedMeetings = computed(() => {
|
||||
if (!meetingList.value || meetingList.value.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const groups = {};
|
||||
|
||||
meetingList.value.forEach(item => {
|
||||
const monthYear = formatDate(item.begin_date, 'YYYY年MM月');
|
||||
|
||||
if (!groups[monthYear]) {
|
||||
groups[monthYear] = {
|
||||
monthYear: monthYear,
|
||||
items: []
|
||||
};
|
||||
}
|
||||
|
||||
groups[monthYear].items.push(item);
|
||||
});
|
||||
|
||||
// 转换为数组并按时间排序
|
||||
return Object.values(groups).sort((a, b) => {
|
||||
// 按年月排序
|
||||
const dateA = dayjs(a.items[0].begin_date);
|
||||
const dateB = dayjs(b.items[0].begin_date);
|
||||
return dateA.isBefore(dateB) ? -1 : 1;
|
||||
});
|
||||
});
|
||||
|
||||
// 页面显示时获取会议列表数据
|
||||
onShow(() => {
|
||||
generateMonthList();
|
||||
getMeetingList(true);
|
||||
});
|
||||
|
||||
// 获取会议列表数据的函数
|
||||
const getMeetingList = async (isRefresh = false) => {
|
||||
const meetingListBySearch = async (isRefresh = false) => {
|
||||
if (isRefresh) {
|
||||
currentPage.value = 1;
|
||||
hasMoreData.value = true;
|
||||
meetingList.value = [];
|
||||
}
|
||||
|
||||
const params = {
|
||||
page: currentPage.value,
|
||||
pageSize: pageSize.value,
|
||||
month: selectedMonth.value !== 'all' ? selectedMonth.value : '',
|
||||
province: selectedProvince.value !== 'all' ? selectedProvince.value : ''
|
||||
month: selectedMonth.value,
|
||||
location: selectedProvince.value,
|
||||
status:''
|
||||
};
|
||||
|
||||
try {
|
||||
console.log('获取会议列表参数:', params);
|
||||
const response = await api.meetingListBySearch(params);
|
||||
console.log('会议列表API响应:', response);
|
||||
|
||||
if (response && response.code ==1 && response.data) {
|
||||
const { list, totalPage, pageNumber, totalRow} = response.data;
|
||||
|
||||
console.log('解析后的数据:', { list, totalPage, pageNumber, totalRow});
|
||||
|
||||
if (Array.isArray(list) && list.length > 0) {
|
||||
// 处理会议数据,根据实际API数据结构映射字段
|
||||
const processedItems = list.map(item => {
|
||||
// 解析开始日期,提取日期部分用于显示
|
||||
const beginDate = dayjs(item.begin_date);
|
||||
const day = beginDate.format('DD');
|
||||
|
||||
// 根据状态确定标签颜色
|
||||
const tagColor = getTagColor(item.status);
|
||||
|
||||
// 格式化时间显示
|
||||
const timeStr = formatDate(item.begin_date, 'YYYY.MM.DD');
|
||||
|
||||
return {
|
||||
id: item.id,
|
||||
begin_date: day, // 用于左侧日期标识显示
|
||||
tagColor: tagColor,
|
||||
title: item.title,
|
||||
liveimg: item.liveimg,
|
||||
poster: item.liveimg, // 使用 liveimg 作为海报
|
||||
time: timeStr,
|
||||
location: item.location || '线上',
|
||||
status: getStatusText(item.status),
|
||||
// 保留原始数据用于其他功能
|
||||
originalData: item,
|
||||
begin_date_timestamp: item.begin_date_timestamp,
|
||||
end_date_timestamp: item.end_date_timestamp,
|
||||
begin_date: item.begin_date,
|
||||
end_date: item.end_date,
|
||||
liveurl: item.liveurl,
|
||||
spareurl: item.spareurl,
|
||||
path: item.path,
|
||||
status_code: item.status
|
||||
};
|
||||
});
|
||||
|
||||
if (isRefresh) {
|
||||
meetingList.value = processedItems;
|
||||
} else {
|
||||
meetingList.value.push(...processedItems);
|
||||
}
|
||||
|
||||
// 更新分页状态
|
||||
hasMoreData.value = totalPage>pageNumber;
|
||||
showLoadMore.value = totalRow > 0;
|
||||
|
||||
console.log('会议列表更新成功,当前总数:', meetingList.value.length);
|
||||
console.log('分页信息:', { currentPage: currentPage.value, totalPage, pageNumber, totalRow, hasMore: hasMoreData.value });
|
||||
} else {
|
||||
console.log('API返回的数据为空');
|
||||
if (isRefresh) {
|
||||
meetingList.value = [];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.log('API响应格式不正确:', response);
|
||||
if (isRefresh) {
|
||||
meetingList.value = [];
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取会议列表失败:', error);
|
||||
if (isRefresh) {
|
||||
meetingList.value = [];
|
||||
}
|
||||
uni.showToast({
|
||||
title: '获取会议列表失败',
|
||||
icon: 'error',
|
||||
duration: 2000
|
||||
});
|
||||
}
|
||||
|
||||
};
|
||||
// 获取会议列表数据的函数
|
||||
const getMeetingList = async (isRefresh = false) => {
|
||||
if (isRefresh) {
|
||||
currentPage.value = 1;
|
||||
hasMoreData.value = true;
|
||||
meetingList.value = [];
|
||||
}
|
||||
|
||||
const params = {
|
||||
page: currentPage.value,
|
||||
pageSize: pageSize.value,
|
||||
month: selectedMonth.value,
|
||||
location: selectedProvince.value
|
||||
};
|
||||
|
||||
try {
|
||||
@ -262,46 +427,45 @@
|
||||
console.log('会议列表API响应:', response);
|
||||
|
||||
if (response && response.code === 200 && response.data) {
|
||||
let newItems = [];
|
||||
let totalCount = 0;
|
||||
const { list, total, pageNum, pages, isLastPage } = response.data;
|
||||
|
||||
// 处理不同的数据结构
|
||||
if (response.data.list && Array.isArray(response.data.list)) {
|
||||
newItems = response.data.list;
|
||||
totalCount = response.data.total || response.data.totalRow || 0;
|
||||
console.log('使用 res.data.list 结构');
|
||||
} else if (response.data && Array.isArray(response.data)) {
|
||||
newItems = response.data;
|
||||
totalCount = response.total || response.totalRow || newItems.length;
|
||||
console.log('使用 res.data 结构');
|
||||
} else if (Array.isArray(response)) {
|
||||
newItems = response;
|
||||
totalCount = newItems.length;
|
||||
console.log('使用 res 数组结构');
|
||||
}
|
||||
console.log('解析后的数据:', { list, total, pageNum, pages, isLastPage });
|
||||
|
||||
console.log('解析后的数据:', { newItems, totalCount });
|
||||
console.log('图片字段映射示例:', newItems.slice(0, 2).map(item => ({
|
||||
liveimg: item.liveimg || item.live_image || item.live_img,
|
||||
poster: item.poster || item.cover_image || item.image
|
||||
})));
|
||||
if (Array.isArray(list) && list.length > 0) {
|
||||
// 处理会议数据,根据实际API数据结构映射字段
|
||||
const processedItems = list.map(item => {
|
||||
// 解析开始日期,提取日期部分用于显示
|
||||
const beginDate = dayjs(item.begin_date);
|
||||
const day = beginDate.format('DD');
|
||||
|
||||
if (Array.isArray(newItems) && newItems.length > 0) {
|
||||
// 处理会议数据,添加必要的字段
|
||||
const processedItems = newItems.map(item => ({
|
||||
id: item.id || item.meeting_id || Math.random().toString(36).substr(2, 9),
|
||||
date: item.date || item.meeting_date || item.start_time || '13',
|
||||
tagColor: getTagColor(item.status || item.meeting_status || 'upcoming'),
|
||||
title: item.title || item.meeting_title || item.name || '会议标题',
|
||||
liveimg: item.liveimg || item.live_image || item.live_img || '',
|
||||
poster: item.poster || item.cover_image || item.image || '/static/meeting-poster-1.jpg',
|
||||
time: formatMeetingTime(item.start_time || item.meeting_time || item.time),
|
||||
location: item.location || item.address || item.venue || '线上',
|
||||
status: item.status || item.meeting_status || 'upcoming',
|
||||
description: item.description || item.content || '',
|
||||
organizer: item.organizer || item.host || '',
|
||||
speakers: item.speakers || item.experts || []
|
||||
}));
|
||||
// 根据状态确定标签颜色
|
||||
const tagColor = getTagColor(item.status);
|
||||
|
||||
// 格式化时间显示
|
||||
const timeStr = formatDate(item.begin_date, 'YYYY.MM.DD');
|
||||
|
||||
return {
|
||||
id: item.id,
|
||||
begin_date: day, // 用于左侧日期标识显示
|
||||
tagColor: tagColor,
|
||||
title: item.title,
|
||||
liveimg: item.liveimg,
|
||||
poster: item.liveimg, // 使用 liveimg 作为海报
|
||||
time: timeStr,
|
||||
location: item.location || '线上',
|
||||
status: getStatusText(item.status),
|
||||
// 保留原始数据用于其他功能
|
||||
originalData: item,
|
||||
begin_date_timestamp: item.begin_date_timestamp,
|
||||
end_date_timestamp: item.end_date_timestamp,
|
||||
begin_date: item.begin_date,
|
||||
end_date: item.end_date,
|
||||
liveurl: item.liveurl,
|
||||
spareurl: item.spareurl,
|
||||
path: item.path,
|
||||
status_code: item.status
|
||||
};
|
||||
});
|
||||
|
||||
if (isRefresh) {
|
||||
meetingList.value = processedItems;
|
||||
@ -309,18 +473,12 @@
|
||||
meetingList.value.push(...processedItems);
|
||||
}
|
||||
|
||||
// 检查是否还有更多数据
|
||||
if (meetingList.value.length >= totalCount) {
|
||||
hasMoreData.value = false;
|
||||
}
|
||||
// 更新分页状态
|
||||
hasMoreData.value = !isLastPage;
|
||||
showLoadMore.value = total > 0;
|
||||
|
||||
console.log('会议列表更新成功,当前总数:', meetingList.value.length);
|
||||
console.log('图片字段详情:', processedItems.slice(0, 2).map(item => ({
|
||||
id: item.id,
|
||||
liveimg: item.liveimg,
|
||||
poster: item.poster,
|
||||
finalImage: item.liveimg || item.poster
|
||||
})));
|
||||
console.log('分页信息:', { currentPage: currentPage.value, total, pages, hasMore: hasMoreData.value });
|
||||
} else {
|
||||
console.log('API返回的数据为空');
|
||||
if (isRefresh) {
|
||||
@ -349,40 +507,105 @@
|
||||
// 根据会议状态获取标签颜色
|
||||
const getTagColor = (status) => {
|
||||
const colorMap = {
|
||||
'upcoming': '#FF4444', // 预告
|
||||
'live': '#00BCD4', // 直播中
|
||||
'replay': '#9C27B0', // 回放
|
||||
'finished': '#4CAF50', // 已结束
|
||||
'cancelled': '#FF9800' // 已取消
|
||||
1: '#FF4444', // 预告
|
||||
2: '#00BCD4', // 直播中
|
||||
3: '#9C27B0', // 已结束/回放
|
||||
4: '#4CAF50', // 已完成
|
||||
5: '#FF9800' // 已取消
|
||||
};
|
||||
return colorMap[status] || '#FF4444';
|
||||
};
|
||||
|
||||
// 格式化会议时间
|
||||
const formatMeetingTime = (timeStr) => {
|
||||
if (!timeStr) return '2025.08.13';
|
||||
// 获取状态文本
|
||||
const getStatusText = (status) => {
|
||||
const statusMap = {
|
||||
1: '预告',
|
||||
2: '直播中',
|
||||
3: '已结束',
|
||||
4: '已完成',
|
||||
5: '已取消'
|
||||
};
|
||||
return statusMap[status] || '预告';
|
||||
};
|
||||
|
||||
// 获取状态样式类
|
||||
const getStatusClass = (status) => {
|
||||
const classMap = {
|
||||
1: 'status-preview',
|
||||
2: 'status-live',
|
||||
3: 'status-ended',
|
||||
4: 'status-completed',
|
||||
5: 'status-cancelled'
|
||||
};
|
||||
return classMap[status] || 'status-preview';
|
||||
};
|
||||
|
||||
// 使用 dayjs 格式化日期函数,包含年月
|
||||
const formatDate = (date, format = 'YYYY-MM') => {
|
||||
if (!date) return '';
|
||||
|
||||
try {
|
||||
// 如果是时间戳
|
||||
if (typeof timeStr === 'number') {
|
||||
const date = new Date(timeStr);
|
||||
return `${date.getFullYear()}.${String(date.getMonth() + 1).padStart(2, '0')}.${String(date.getDate()).padStart(2, '0')}`;
|
||||
// 使用 dayjs 解析日期
|
||||
const dayjsDate = dayjs(date);
|
||||
|
||||
// 检查日期是否有效
|
||||
if (!dayjsDate.isValid()) {
|
||||
console.error('无效的日期:', date);
|
||||
return '';
|
||||
}
|
||||
|
||||
// 如果是字符串,尝试解析
|
||||
const date = new Date(timeStr);
|
||||
if (!isNaN(date.getTime())) {
|
||||
return `${date.getFullYear()}.${String(date.getMonth() + 1).padStart(2, '0')}.${String(date.getDate()).padStart(2, '0')}`;
|
||||
// 根据格式参数返回不同格式
|
||||
switch (format) {
|
||||
case 'YYYY-MM':
|
||||
return dayjsDate.format('YYYY-MM');
|
||||
case 'YYYY-MM-DD':
|
||||
return dayjsDate.format('YYYY-MM-DD');
|
||||
case 'YYYY-MM-DD HH:mm':
|
||||
return dayjsDate.format('YYYY-MM-DD HH:mm');
|
||||
case 'YYYY-MM-DD HH:mm:ss':
|
||||
return dayjsDate.format('YYYY-MM-DD HH:mm:ss');
|
||||
case 'YYYY年MM月':
|
||||
return dayjsDate.format('YYYY年MM月');
|
||||
case 'YYYY年MM月DD日':
|
||||
return dayjsDate.format('YYYY年MM月DD日');
|
||||
case 'MM月':
|
||||
return dayjsDate.format('MM月');
|
||||
case 'MM月DD日':
|
||||
return dayjsDate.format('MM月DD日');
|
||||
case 'YYYY.MM.DD':
|
||||
return dayjsDate.format('YYYY.MM.DD');
|
||||
case 'MM-DD':
|
||||
return dayjsDate.format('MM-DD');
|
||||
case 'MM/DD':
|
||||
return dayjsDate.format('MM/DD');
|
||||
case 'YYYY/MM/DD':
|
||||
return dayjsDate.format('YYYY/MM/DD');
|
||||
default:
|
||||
return dayjsDate.format(format);
|
||||
}
|
||||
|
||||
// 如果解析失败,返回原字符串
|
||||
return timeStr;
|
||||
} catch (error) {
|
||||
console.error('时间格式化失败:', error);
|
||||
return timeStr;
|
||||
console.error('日期格式化失败:', error);
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
// 格式化会议时间
|
||||
const formatMeetingTime = (timeStr) => {
|
||||
if (!timeStr) return '';
|
||||
|
||||
try {
|
||||
// 使用 dayjs 格式化时间
|
||||
const formattedDate = formatDate(timeStr, 'YYYY年MM月');
|
||||
return formattedDate || '';
|
||||
} catch (error) {
|
||||
console.error('时间格式化失败:', error);
|
||||
return '';
|
||||
}
|
||||
};
|
||||
const formatDay=(timeStr)=>{
|
||||
if (!timeStr) return '';
|
||||
return formatDate(timeStr, 'DD');
|
||||
}
|
||||
// 显示时间选择弹窗
|
||||
const showTimePopup = () => {
|
||||
isTimePopupShow.value = !isTimePopupShow.value;
|
||||
@ -398,7 +621,7 @@
|
||||
selectedMonth.value = month.value;
|
||||
console.log('选择月份:', month.label);
|
||||
// 选择月份后重新加载数据
|
||||
getMeetingList(true);
|
||||
meetingListBySearch(true);
|
||||
hideTimePopup();
|
||||
};
|
||||
|
||||
@ -417,7 +640,7 @@
|
||||
selectedProvince.value = province.code;
|
||||
console.log('选择省份:', province.name);
|
||||
// 选择省份后重新加载数据
|
||||
getMeetingList(true);
|
||||
meetingListBySearch(true);
|
||||
hideLocationPopup();
|
||||
};
|
||||
|
||||
@ -803,37 +1026,37 @@ $shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.preview-tag {
|
||||
.status-tag {
|
||||
position: absolute;
|
||||
top: 16rpx;
|
||||
right: 16rpx;
|
||||
background-color: #FF4444;
|
||||
color: $white;
|
||||
font-size: 24rpx;
|
||||
padding: 4rpx 16rpx;
|
||||
border-radius: 20rpx;
|
||||
}
|
||||
color: $white;
|
||||
font-weight: 500;
|
||||
border: 2rpx solid #fff;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.2);
|
||||
|
||||
.live-tag {
|
||||
position: absolute;
|
||||
top: 16rpx;
|
||||
right: 16rpx;
|
||||
background-color: #00BCD4;
|
||||
color: $white;
|
||||
font-size: 24rpx;
|
||||
padding: 4rpx 16rpx;
|
||||
border-radius: 20rpx;
|
||||
}
|
||||
&.status-preview {
|
||||
background-color: #FF4444;
|
||||
}
|
||||
|
||||
.replay-tag {
|
||||
position: absolute;
|
||||
top: 16rpx;
|
||||
right: 16rpx;
|
||||
background-color: #9C27B0;
|
||||
color: $white;
|
||||
font-size: 24rpx;
|
||||
padding: 4rpx 16rpx;
|
||||
border-radius: 20rpx;
|
||||
&.status-live {
|
||||
background-color: #00BCD4;
|
||||
}
|
||||
|
||||
&.status-ended {
|
||||
background-color: #9C27B0;
|
||||
}
|
||||
|
||||
&.status-completed {
|
||||
background-color: #4CAF50;
|
||||
}
|
||||
|
||||
&.status-cancelled {
|
||||
background-color: #FF9800;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -168,30 +168,37 @@
|
||||
const generateMonthList = () => {
|
||||
const months = [];
|
||||
const currentDate = new Date();
|
||||
const currentMonth = currentDate.getMonth() + 1; // 当前月份 (1-12)
|
||||
const currentYear = currentDate.getFullYear();
|
||||
|
||||
// 添加"所有"选项
|
||||
months.push({ value: 'all', label: '所有' });
|
||||
|
||||
// 从当前月份开始,往后生成12个月
|
||||
for (let i = 0; i < 12; i++) {
|
||||
const targetDate = new Date(currentDate.getFullYear(), currentDate.getMonth() + i, 1);
|
||||
const month = targetDate.getMonth() + 1;
|
||||
const year = targetDate.getFullYear();
|
||||
|
||||
// 格式化月份标签
|
||||
// 从当前月份到12月
|
||||
for (let month = currentMonth; month <= 12; month++) {
|
||||
let monthLabel = '';
|
||||
if (i === 0) {
|
||||
if (month === currentMonth) {
|
||||
monthLabel = `${month}月(本月)`;
|
||||
} else if (i === 1) {
|
||||
} else if (month === currentMonth + 1) {
|
||||
monthLabel = `${month}月(下月)`;
|
||||
} else {
|
||||
monthLabel = `${month}月`;
|
||||
}
|
||||
|
||||
months.push({
|
||||
value: month.toString(), // 只传月份数字
|
||||
value: month.toString(),
|
||||
label: monthLabel,
|
||||
year: year,
|
||||
year: currentYear,
|
||||
month: month
|
||||
});
|
||||
}
|
||||
|
||||
// 从1月到当前月份(不包括当前月份,避免重复)
|
||||
for (let month = 1; month < currentMonth; month++) {
|
||||
months.push({
|
||||
value: month.toString(),
|
||||
label: `${month}月`,
|
||||
year: currentYear + 1, // 下一年的月份
|
||||
month: month
|
||||
});
|
||||
}
|
||||
@ -754,7 +761,50 @@
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
// 格式化日期函数,包含年月
|
||||
const formatDate = (date, format = 'YYYY-MM') => {
|
||||
if (!date) return '';
|
||||
|
||||
// 如果传入的是字符串,转换为Date对象
|
||||
const dateObj = typeof date === 'string' ? new Date(date) : date;
|
||||
|
||||
// 检查日期是否有效
|
||||
if (isNaN(dateObj.getTime())) {
|
||||
console.error('无效的日期:', date);
|
||||
return '';
|
||||
}
|
||||
|
||||
const year = dateObj.getFullYear();
|
||||
const month = (dateObj.getMonth() + 1).toString().padStart(2, '0');
|
||||
const day = dateObj.getDate().toString().padStart(2, '0');
|
||||
const hours = dateObj.getHours().toString().padStart(2, '0');
|
||||
const minutes = dateObj.getMinutes().toString().padStart(2, '0');
|
||||
const seconds = dateObj.getSeconds().toString().padStart(2, '0');
|
||||
|
||||
// 根据格式参数返回不同格式
|
||||
switch (format) {
|
||||
case 'YYYY-MM':
|
||||
return `${year}-${month}`;
|
||||
case 'YYYY-MM-DD':
|
||||
return `${year}-${month}-${day}`;
|
||||
case 'YYYY-MM-DD HH:mm':
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}`;
|
||||
case 'YYYY-MM-DD HH:mm:ss':
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
||||
case 'YYYY年MM月':
|
||||
return `${year}年${month}月`;
|
||||
case 'YYYY年MM月DD日':
|
||||
return `${year}年${month}月${day}日`;
|
||||
case 'MM月':
|
||||
return `${month}月`;
|
||||
case 'MM月DD日':
|
||||
return `${month}月${day}日`;
|
||||
default:
|
||||
return `${year}-${month}`;
|
||||
}
|
||||
};
|
||||
|
||||
onShow(() => {
|
||||
// 生成月份列表
|
||||
generateMonthList();
|
||||
// 加载会议列表
|
||||
|
||||
422
pages_app/myApplication/myApplication.vue
Normal file
422
pages_app/myApplication/myApplication.vue
Normal file
@ -0,0 +1,422 @@
|
||||
<template>
|
||||
<view class="my-application-page">
|
||||
<!-- 顶部导航栏 -->
|
||||
<uni-nav-bar
|
||||
left-icon="left"
|
||||
title="我的应用"
|
||||
@clickLeft="goBack"
|
||||
fixed
|
||||
color="#8B2316"
|
||||
height="140rpx"
|
||||
:border="false"
|
||||
backgroundColor="#eeeeee"
|
||||
>
|
||||
<template #right>
|
||||
<text class="edit-btn" @click="toggleEdit">{{
|
||||
isEditMode ? "完成" : "编辑"
|
||||
}}</text>
|
||||
</template>
|
||||
</uni-nav-bar>
|
||||
|
||||
<scroll-view class="content" scroll-y>
|
||||
<!-- 我的应用部分 -->
|
||||
<view class="section">
|
||||
<view class="section-title">我的应用</view>
|
||||
<view class="app-grid">
|
||||
<view
|
||||
class="app-item"
|
||||
v-for="(app, index) in myApps"
|
||||
:key="app.id"
|
||||
@click="openApp(app)"
|
||||
>
|
||||
<view
|
||||
class="iconbox"
|
||||
v-if="isEditMode"
|
||||
@click.stop="removeFromMyApps(index)"
|
||||
>
|
||||
<up-icon
|
||||
name="minus-circle"
|
||||
color="#fe0001"
|
||||
size="40rpx"
|
||||
></up-icon>
|
||||
</view>
|
||||
<view class="app-icon">
|
||||
<image :src="app.icon" mode="aspectFit" class="icon-image" />
|
||||
</view>
|
||||
<text class="app-name">{{ app.name }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 常用功能部分 -->
|
||||
<view class="section">
|
||||
<view class="section-title">常用功能</view>
|
||||
<view class="app-grid">
|
||||
<view
|
||||
class="app-item"
|
||||
v-for="(app, index) in commonApps"
|
||||
:key="app.id"
|
||||
@click="openApp(app)"
|
||||
>
|
||||
<view
|
||||
class="iconbox"
|
||||
@click.stop="addToMyApps(app)"
|
||||
v-if="isEditMode"
|
||||
>
|
||||
<up-icon
|
||||
name="plus-circle"
|
||||
color="#067fe6"
|
||||
size="40rpx"
|
||||
></up-icon>
|
||||
</view>
|
||||
<view class="app-icon">
|
||||
<image :src="app.icon" mode="aspectFit" class="icon-image" />
|
||||
</view>
|
||||
<text class="app-name">{{ app.name }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { onShow } from "@dcloudio/uni-app";
|
||||
import api from "@/api/api";
|
||||
import docUrl from '@/utils/docUrl';
|
||||
|
||||
// 编辑模式状态
|
||||
const isEditMode = ref(false);
|
||||
|
||||
// 我的应用列表
|
||||
const myApps = ref([]);
|
||||
|
||||
// 常用功能列表
|
||||
const commonApps = ref([]);
|
||||
|
||||
|
||||
const blackList=['临床研究','精品课程','名医名科','肝病学院','病例分享','科研项目','肝病检测','我的下载','病例讨论','临床研究']
|
||||
// 页面路由映射
|
||||
|
||||
// 获取用户图标数据
|
||||
const addUserIcon = () => {
|
||||
api.addUserIcon({
|
||||
idStr:myApps.value.map(item => item.id).join(',')
|
||||
}).then(res => {
|
||||
console.log('addUserIcon API响应:', res);
|
||||
if(res.code === '200'){
|
||||
uni.showToast({
|
||||
title: '保存成功',
|
||||
icon: 'none',
|
||||
duration: 1500
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
const getUserIcon = () => {
|
||||
api.getUserIcon({}).then(res => {
|
||||
console.log('getUserIcon API响应:', res);
|
||||
|
||||
if (res.code === '200' && res.data && Array.isArray(res.data)) {
|
||||
// 处理API数据(先过滤黑名单)
|
||||
const sourceList = res.data.filter(item => !blackList.includes(item.name));
|
||||
const allApps = sourceList.map(item => ({
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
icon: docUrl + item.img, // 使用docUrl拼接图片路径
|
||||
bgColor: bgColorMap[item.id] || '#999999', // 使用预设颜色
|
||||
url: urlMap[item.id] || '', // 使用预设路由
|
||||
selected: item.selected
|
||||
}));
|
||||
|
||||
// 根据selected字段分离我的应用和常用功能
|
||||
myApps.value = allApps.filter(app =>
|
||||
app.selected !== 'false' && app.selected !== false
|
||||
);
|
||||
|
||||
commonApps.value = allApps.filter(app =>
|
||||
app.selected === 'false' || app.selected === false
|
||||
);
|
||||
|
||||
console.log('我的应用:', myApps.value);
|
||||
console.log('常用功能:', commonApps.value);
|
||||
} else {
|
||||
console.error('获取应用图标失败:', res.message || '未知错误');
|
||||
uni.showToast({
|
||||
title: res.message || '获取应用列表失败',
|
||||
icon: 'error',
|
||||
duration: 2000
|
||||
});
|
||||
}
|
||||
}).catch(error => {
|
||||
console.error('获取应用图标异常:', error);
|
||||
uni.showToast({
|
||||
title: '网络请求失败',
|
||||
icon: 'error',
|
||||
duration: 2000
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// 返回上一页
|
||||
const goBack = () => {
|
||||
uni.navigateBack();
|
||||
};
|
||||
|
||||
// 切换编辑模式
|
||||
const toggleEdit = () => {
|
||||
isEditMode.value = !isEditMode.value;
|
||||
if(!isEditMode.value){
|
||||
addUserIcon();
|
||||
}
|
||||
};
|
||||
|
||||
// 打开应用
|
||||
const openApp = (app) => {
|
||||
if (isEditMode.value) return;
|
||||
let url='';
|
||||
if(app.name=="肝胆课件"){
|
||||
url='/pages_app/ppt/ppt'
|
||||
navTo({
|
||||
url:url
|
||||
})
|
||||
}else if(app.name=="精品课"){
|
||||
url='/pages_course/course/course'
|
||||
navTo({
|
||||
url:url
|
||||
})
|
||||
}else if(app.name=="积分商城"){
|
||||
url='/pages_goods/pointMall/pointMall'
|
||||
navTo({
|
||||
url:url
|
||||
})
|
||||
}else if(app.name=="肝病新闻"){
|
||||
url='/pages_app/news/news'
|
||||
navTo({
|
||||
url:url
|
||||
})
|
||||
}else if(app.name=="我的福利"){
|
||||
url='/pages_app/myWelfare/myWelfare'
|
||||
navTo({
|
||||
url:url
|
||||
})
|
||||
}else if(app.name=="专题e站"){
|
||||
url='https://wx.igandan.com/Esite/index.htm#/home?fromtype=doctor'
|
||||
// H5 端
|
||||
// #ifdef H5
|
||||
window.open(url, '_blank');
|
||||
// #endif
|
||||
|
||||
// App 端
|
||||
// #ifdef APP-PLUS
|
||||
plus.runtime.openURL(url);
|
||||
// #endif
|
||||
|
||||
// 小程序端:使用内嵌 webview 页面打开
|
||||
// #ifdef MP
|
||||
const encoded = encodeURIComponent(url)
|
||||
uni.navigateTo({
|
||||
url: `/pages_app/webview/webview?url=${encoded}`
|
||||
})
|
||||
// #endif
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
// 添加到我的应用
|
||||
const addToMyApps = (app) => {
|
||||
// 检查是否已存在
|
||||
const exists = myApps.value.some(item => item.id === app.id);
|
||||
if (exists) {
|
||||
uni.showToast({
|
||||
title: '已在我的应用中',
|
||||
icon: 'none',
|
||||
duration: 1500
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否超过最大数量
|
||||
if (myApps.value.length >= 3) {
|
||||
uni.showToast({
|
||||
title: '最多添加3个应用',
|
||||
icon: 'none',
|
||||
duration: 1500
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 更新应用状态为已选中
|
||||
app.selected = '1';
|
||||
|
||||
// 从常用功能中移除
|
||||
const commonIndex = commonApps.value.findIndex(item => item.id === app.id);
|
||||
if (commonIndex > -1) {
|
||||
commonApps.value.splice(commonIndex, 1);
|
||||
}
|
||||
|
||||
// 添加到我的应用
|
||||
myApps.value.push(app);
|
||||
|
||||
|
||||
};
|
||||
|
||||
// 从我的应用中移除
|
||||
const removeFromMyApps = (index) => {
|
||||
const app = myApps.value[index];
|
||||
|
||||
// 更新应用状态为未选中
|
||||
app.selected = 'false';
|
||||
|
||||
// 从我的应用中移除
|
||||
myApps.value.splice(index, 1);
|
||||
|
||||
// 添加到常用功能
|
||||
commonApps.value.push(app);
|
||||
|
||||
|
||||
};
|
||||
|
||||
onShow(() => {
|
||||
// 页面显示时的逻辑
|
||||
getUserIcon();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.my-application-page {
|
||||
background-color: #ffffff;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.content {
|
||||
position: fixed;
|
||||
top: calc(var(--status-bar-height) + 64px);
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
right: 0;
|
||||
box-sizing: border-box;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.edit-btn {
|
||||
color: #8b2316;
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
color: #666666;
|
||||
font-size: 28rpx;
|
||||
margin-bottom: 20rpx;
|
||||
padding: 0 30rpx;
|
||||
}
|
||||
|
||||
.app-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
border: 1rpx solid #e0e0e0;
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.app-item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 20rpx 10rpx;
|
||||
border-right: 1rpx solid #e0e0e0;
|
||||
border-bottom: 1rpx solid #e0e0e0;
|
||||
transition: all 0.3s ease;
|
||||
.iconbox {
|
||||
position: absolute;
|
||||
top: 8rpx;
|
||||
right: 8rpx;
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
// 右边框处理
|
||||
&:nth-child(3n) {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
// 下边框处理(最后一行)
|
||||
&:nth-last-child(-n + 3) {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.app-icon {
|
||||
width: 100rpx;
|
||||
height: 100rpx;
|
||||
border-radius: 20rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.icon-image {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
}
|
||||
|
||||
.app-name {
|
||||
font-size: 24rpx;
|
||||
color: #000;
|
||||
text-align: center;
|
||||
line-height: 1.2;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.delete-btn {
|
||||
position: absolute;
|
||||
top: -8rpx;
|
||||
right: -8rpx;
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
background-color: #ff4757;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 2rpx 8rpx rgba(255, 71, 87, 0.3);
|
||||
}
|
||||
|
||||
.add-btn {
|
||||
position: absolute;
|
||||
top: -8rpx;
|
||||
right: -8rpx;
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
background-color: #ffffff;
|
||||
border: 2rpx solid #8b2316;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 2rpx 8rpx rgba(139, 35, 22, 0.2);
|
||||
}
|
||||
|
||||
// 编辑模式下的样式调整
|
||||
.app-item:has(.delete-btn) {
|
||||
border-right-color: #ff4757;
|
||||
border-bottom-color: #ff4757;
|
||||
}
|
||||
|
||||
.app-item:has(.add-btn) {
|
||||
border-right-color: #8b2316;
|
||||
border-bottom-color: #8b2316;
|
||||
}
|
||||
</style>
|
||||
@ -20,7 +20,7 @@
|
||||
<view class="row" @click="onChooseAvatar">
|
||||
<view class="label"><text>头像</text><text class="req">*</text></view>
|
||||
<view class="value value-avatar">
|
||||
<image :src="form.avatar" mode="aspectFill" class="avatar" />
|
||||
<image :src="docUrl+form.photo" mode="aspectFill" class="avatar" />
|
||||
<uni-icons type="right" size="18" color="#bbb" />
|
||||
</view>
|
||||
</view>
|
||||
@ -28,14 +28,14 @@
|
||||
<!-- 姓名 -->
|
||||
<view class="row">
|
||||
<view class="label"><text>姓名</text><text class="req">*</text></view>
|
||||
<view class="value">{{ form.name }}</view>
|
||||
<view class="value">{{ form.realName }}</view>
|
||||
</view>
|
||||
|
||||
<!-- 性别 -->
|
||||
<view class="row" @click="onPickGender">
|
||||
<view class="label"><text>性别</text><text class="req">*</text></view>
|
||||
<view class="value with-arrow">
|
||||
<text>{{ form.gender }}</text>
|
||||
<text>{{ getGenderText(form.sex) }}</text>
|
||||
<uni-icons type="right" size="18" color="#bbb" />
|
||||
</view>
|
||||
</view>
|
||||
@ -44,7 +44,7 @@
|
||||
<view class="row" @click="onPickBirthday">
|
||||
<view class="label"><text>出生日期</text></view>
|
||||
<view class="value with-arrow">
|
||||
<text>{{ form.birthday || '' }}</text>
|
||||
<text>{{ form.birthDate || '' }}</text>
|
||||
<uni-icons type="right" size="18" color="#bbb" />
|
||||
</view>
|
||||
</view>
|
||||
@ -70,11 +70,12 @@
|
||||
<!-- 专业资料分组 -->
|
||||
<view class="section-header">专业资料</view>
|
||||
|
||||
<!-- 医院 -->
|
||||
<view class="row" @click="onPickHospital">
|
||||
<!-- 医院地址 -->
|
||||
<view class="row" @click="openAreaPicker">
|
||||
<view class="label"><text>医院</text><text class="req">*</text></view>
|
||||
<view class="value with-arrow">
|
||||
<text>{{ form.hospital }}</text>
|
||||
<text v-if="!regionText" class="placeholder">请选择医院</text>
|
||||
<text v-else>{{ regionText }}</text>
|
||||
<uni-icons type="right" size="18" color="#bbb" />
|
||||
</view>
|
||||
</view>
|
||||
@ -83,7 +84,7 @@
|
||||
<view class="row" @click="onPickDept">
|
||||
<view class="label"><text>科室</text><text class="req">*</text></view>
|
||||
<view class="value with-arrow">
|
||||
<text>{{ form.department }}</text>
|
||||
<text>{{ form.officeName }}</text>
|
||||
<uni-icons type="right" size="18" color="#bbb" />
|
||||
</view>
|
||||
</view>
|
||||
@ -92,7 +93,7 @@
|
||||
<view class="row" @click="onEditDeptPhone">
|
||||
<view class="label"><text>科室电话</text><text class="req">*</text></view>
|
||||
<view class="value with-arrow">
|
||||
<text>{{ form.departmentPhone }}</text>
|
||||
<text>{{ form.officePhone }}</text>
|
||||
<uni-icons type="right" size="18" color="#bbb" />
|
||||
</view>
|
||||
</view>
|
||||
@ -101,7 +102,7 @@
|
||||
<view class="row" @click="onPickTitle">
|
||||
<view class="label"><text>职称</text></view>
|
||||
<view class="value with-arrow">
|
||||
<text>{{ form.title }}</text>
|
||||
<text>{{ form.positionName }}</text>
|
||||
<uni-icons type="right" size="18" color="#bbb" />
|
||||
</view>
|
||||
</view>
|
||||
@ -110,7 +111,7 @@
|
||||
<view class="row" @click="onEditLicenseNo">
|
||||
<view class="label"><text>执业医师证编号</text><text class="req">*</text></view>
|
||||
<view class="value with-arrow">
|
||||
<text>{{ form.licenseNo }}</text>
|
||||
<text>{{ form.certificate }}</text>
|
||||
<uni-icons type="right" size="18" color="#bbb" />
|
||||
</view>
|
||||
</view>
|
||||
@ -119,7 +120,7 @@
|
||||
<view class="row" @click="onChooseLicenseImg">
|
||||
<view class="label"><text>执业医师证图片或胸牌</text><text class="req">*</text></view>
|
||||
<view class="value value-license">
|
||||
<image :src="form.licenseImg" mode="aspectFill" class="license-thumb" />
|
||||
<image :src="docUrl+form.certificateImg" mode="aspectFill" class="license-thumb" />
|
||||
<uni-icons type="right" size="18" color="#bbb" />
|
||||
</view>
|
||||
</view>
|
||||
@ -128,7 +129,7 @@
|
||||
<view class="row" @click="onEditSpecialty">
|
||||
<view class="label"><text>专长</text></view>
|
||||
<view class="value with-arrow">
|
||||
<text>{{ form.specialty }}</text>
|
||||
<text>{{ getSpecialtyText() }}</text>
|
||||
<uni-icons type="right" size="18" color="#bbb" />
|
||||
</view>
|
||||
</view>
|
||||
@ -137,35 +138,250 @@
|
||||
<view class="row" @click="onEditIntro">
|
||||
<view class="label"><text>个人简介</text></view>
|
||||
<view class="value with-arrow">
|
||||
<text>{{ form.intro || '暂无简介' }}</text>
|
||||
<uni-icons type="right" size="18" color="#bbb" />
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 省市区选择器 -->
|
||||
<view v-if="showAreaPicker" class="picker-mask" @click="closeAreaPicker"></view>
|
||||
<view v-if="showAreaPicker" class="picker-panel">
|
||||
<view class="picker-header">
|
||||
<text class="picker-btn" @click="closeAreaPicker">取消</text>
|
||||
<text class="picker-title">选择地区</text>
|
||||
<text class="picker-btn ok" @click="confirmArea">确定</text>
|
||||
</view>
|
||||
<picker-view v-if="provinces.length && cities.length && areas.length" class="picker-view" :indicator-style="indicatorStyle" :value="pickerIndex" @change="onAreaChange">
|
||||
<picker-view-column>
|
||||
<view v-for="(p,pi) in provinces" :key="pi" class="picker-item">{{ p.name }}</view>
|
||||
</picker-view-column>
|
||||
<picker-view-column>
|
||||
<view v-for="(c,ci) in cities" :key="ci" class="picker-item">{{ c.name }}</view>
|
||||
</picker-view-column>
|
||||
<picker-view-column>
|
||||
<view v-for="(a,ai) in areas" :key="ai" class="picker-item">{{ a.name }}</view>
|
||||
</picker-view-column>
|
||||
</picker-view>
|
||||
<view v-else class="picker-empty">地区数据加载中...</view>
|
||||
</view>
|
||||
|
||||
<!-- 日期选择器 -->
|
||||
<view v-if="showDatePicker" class="picker-mask" @click="closeDatePicker"></view>
|
||||
<view v-if="showDatePicker" class="picker-panel">
|
||||
<view class="picker-header">
|
||||
<text class="picker-btn" @click="closeDatePicker">取消</text>
|
||||
<text class="picker-title">选择出生日期</text>
|
||||
<text class="picker-btn ok" @click="confirmDate">确定</text>
|
||||
</view>
|
||||
<picker-view class="picker-view" :indicator-style="indicatorStyle" :value="datePickerIndex" @change="onDateChange">
|
||||
<picker-view-column>
|
||||
<view v-for="(year, index) in years" :key="index" class="picker-item">{{ year }}年</view>
|
||||
</picker-view-column>
|
||||
<picker-view-column>
|
||||
<view v-for="(month, index) in months" :key="index" class="picker-item">{{ month }}月</view>
|
||||
</picker-view-column>
|
||||
<picker-view-column>
|
||||
<view v-for="(day, index) in days" :key="index" class="picker-item">{{ day }}日</view>
|
||||
</picker-view-column>
|
||||
</picker-view>
|
||||
</view>
|
||||
|
||||
<!-- 科室选择器 -->
|
||||
<view v-if="showDeptPicker" class="picker-mask" @click="closeDeptPicker"></view>
|
||||
<view v-if="showDeptPicker" class="picker-panel">
|
||||
<view class="picker-header">
|
||||
<text class="picker-btn" @click="closeDeptPicker">取消</text>
|
||||
<text class="picker-title">选择科室</text>
|
||||
<text class="picker-btn ok" @click="confirmDept">确定</text>
|
||||
</view>
|
||||
<picker-view class="picker-view" :indicator-style="indicatorStyle" :value="deptPickerIndex" @change="onDeptChange">
|
||||
<picker-view-column>
|
||||
<view v-for="(dept, index) in deptList" :key="index" class="picker-item">{{ dept }}</view>
|
||||
</picker-view-column>
|
||||
</picker-view>
|
||||
</view>
|
||||
|
||||
<!-- 职称选择器 -->
|
||||
<view v-if="showTitlePicker" class="picker-mask" @click="closeTitlePicker"></view>
|
||||
<view v-if="showTitlePicker" class="picker-panel">
|
||||
<view class="picker-header">
|
||||
<text class="picker-btn" @click="closeTitlePicker">取消</text>
|
||||
<text class="picker-title">选择职称</text>
|
||||
<text class="picker-btn ok" @click="confirmTitle">确定</text>
|
||||
</view>
|
||||
<picker-view class="picker-view" :indicator-style="indicatorStyle" :value="titlePickerIndex" @change="onTitleChange">
|
||||
<picker-view-column>
|
||||
<view v-for="(title, index) in titleList" :key="index" class="picker-item">{{ title }}</view>
|
||||
</picker-view-column>
|
||||
</picker-view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { onShow } from "@dcloudio/uni-app";
|
||||
|
||||
import api from '@/api/api';
|
||||
import areaList from '@/utils/areaList.js';
|
||||
import docUrl from '@/utils/docUrl';
|
||||
const form = ref({
|
||||
avatar: '/static/big_background_my.png',
|
||||
name: '邱建东',
|
||||
gender: '男',
|
||||
birthday: '',
|
||||
mobile: '17600668628',
|
||||
// 基本资料字段
|
||||
photo: '',
|
||||
realName: '',
|
||||
sex: 0,
|
||||
birthDate: '',
|
||||
mobile: '',
|
||||
email: '',
|
||||
hospital: '北京博爱医院',
|
||||
department: '肝病科',
|
||||
departmentPhone: '1234567890',
|
||||
title: '主任中医师',
|
||||
licenseNo: '12345678',
|
||||
licenseImg: '/static/big_background_my.png',
|
||||
specialty: '肝炎、肝硬化'
|
||||
// 专业资料字段
|
||||
hospitalName: '',
|
||||
officeName: '',
|
||||
officePhone: '',
|
||||
positionName: '',
|
||||
certificate: '',
|
||||
certificateImg: '',
|
||||
intro: '',
|
||||
// 地址相关字段
|
||||
provId: 0,
|
||||
cityId: 0,
|
||||
countyId: 0,
|
||||
// 专长列表
|
||||
specialityList: []
|
||||
});
|
||||
|
||||
// 地址选择相关变量
|
||||
const regionText = ref('');
|
||||
const provinces = ref([]);
|
||||
const cities = ref([]);
|
||||
const areas = ref([]);
|
||||
const pickerIndex = ref([0,0,0]);
|
||||
const showAreaPicker = ref(false);
|
||||
const indicatorStyle = `height: 80rpx;`;
|
||||
|
||||
// 日期选择相关变量
|
||||
const showDatePicker = ref(false);
|
||||
const years = ref([]);
|
||||
const months = ref([]);
|
||||
const days = ref([]);
|
||||
const datePickerIndex = ref([0, 0, 0]);
|
||||
|
||||
// 科室选择相关变量
|
||||
const showDeptPicker = ref(false);
|
||||
const deptList = ref([
|
||||
'内科', '外科', '妇产科', '儿科', '眼科', '耳鼻喉科', '口腔科', '皮肤科',
|
||||
'神经科', '精神科', '肿瘤科', '康复科', '急诊科', '麻醉科', '病理科',
|
||||
'检验科', '影像科', '药剂科', '护理部', '肝病科', '消化科', '心内科',
|
||||
'呼吸科', '肾内科', '内分泌科', '血液科', '风湿科', '感染科', 'ICU',
|
||||
'手术室', '门诊部', '住院部', '体检中心', '其他'
|
||||
]);
|
||||
const deptPickerIndex = ref([0]);
|
||||
|
||||
// 职称选择相关变量
|
||||
const showTitlePicker = ref(false);
|
||||
const titleList = ref([
|
||||
'住院医师', '主治医师', '副主任医师', '主任医师',
|
||||
'住院中医师', '主治中医师', '副主任中医师', '主任中医师',
|
||||
'住院药师', '主管药师', '副主任药师', '主任药师',
|
||||
'住院护师', '主管护师', '副主任护师', '主任护师',
|
||||
'住院技师', '主管技师', '副主任技师', '主任技师',
|
||||
'助理医师', '执业医师', '执业助理医师',
|
||||
'医士', '医师', '主管医师', '副主任医师', '主任医师',
|
||||
'护士', '护师', '主管护师', '副主任护师', '主任护师',
|
||||
'技师', '主管技师', '副主任技师', '主任技师',
|
||||
'其他'
|
||||
]);
|
||||
const titlePickerIndex = ref([0]);
|
||||
|
||||
const goBack = () => uni.navigateBack();
|
||||
|
||||
// 获取性别文本
|
||||
const getGenderText = (sex) => {
|
||||
const genderMap = { 0: '女', 1: '男' };
|
||||
return genderMap[sex] || '';
|
||||
};
|
||||
const getDiseaseList = async () => {
|
||||
const res = await api.getDiseaseList({});
|
||||
console.log('疾病列表API响应:', res);
|
||||
if (res.code === 200) {
|
||||
deptList.value = res.data.diseaseList.map(item => item.diseaseName);
|
||||
}
|
||||
};
|
||||
const getPositionList = async () => {
|
||||
const res = await api.getPositionList({});
|
||||
console.log('职称列表API响应:', res);
|
||||
if (res.code === 200 ) {
|
||||
titleList.value = res.data.positionList.map(item => item.positionName);
|
||||
}
|
||||
};
|
||||
const getOfficeList = async () => {
|
||||
const res = await api.getOfficeList({});
|
||||
console.log('科室列表API响应:', res);
|
||||
if (res.code === 200 ) {
|
||||
deptList.value = res.data.officeList.map(item => item.officeName);
|
||||
}
|
||||
};
|
||||
|
||||
// 获取专长文本
|
||||
const getSpecialtyText = () => {
|
||||
if (!form.value.specialityList || form.value.specialityList.length === 0) {
|
||||
return '暂无专长';
|
||||
}
|
||||
return form.value.specialityList.map(item => item.diseaseName).join('、');
|
||||
};
|
||||
const getExpertByUuid = async () => {
|
||||
try {
|
||||
const res = await api.getExpertByUuid({});
|
||||
console.log('专家信息API响应:', res);
|
||||
|
||||
if (res.code === 200 && res.data && res.data.expert) {
|
||||
const expert = res.data.expert;
|
||||
const specialityList = res.data.specialityList || [];
|
||||
|
||||
// 直接使用API返回的字段结构
|
||||
form.value = {
|
||||
photo: expert.photo || '',
|
||||
realName: expert.realName || '',
|
||||
sex: expert.sex || 0,
|
||||
birthDate: expert.birthDate || '',
|
||||
mobile: expert.mobile || '',
|
||||
email: expert.email || '',
|
||||
hospitalName: expert.hospitalName || '',
|
||||
officeName: expert.officeName || '',
|
||||
officePhone: expert.officePhone || '',
|
||||
positionName: expert.positionName || '',
|
||||
certificate: expert.certificate || '',
|
||||
certificateImg: expert.certificateImg || '',
|
||||
intro: expert.intro || '',
|
||||
provId: expert.provId || 0,
|
||||
cityId: expert.cityId || 0,
|
||||
countyId: expert.countyId || 0,
|
||||
specialityList: specialityList
|
||||
};
|
||||
|
||||
// 设置地址信息(如果有省市区ID)
|
||||
if (expert.provId && expert.cityId && expert.countyId) {
|
||||
// 这里可以根据ID设置地址,暂时先设置医院名称
|
||||
regionText.value = expert.hospitalName || '';
|
||||
}
|
||||
|
||||
console.log('解析后的表单数据:', form.value);
|
||||
} else {
|
||||
console.error('获取专家信息失败:', res.msg || '未知错误');
|
||||
uni.showToast({
|
||||
title: res.msg || '获取个人信息失败',
|
||||
icon: 'error',
|
||||
duration: 2000
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取专家信息异常:', error);
|
||||
uni.showToast({
|
||||
title: '网络请求失败',
|
||||
icon: 'error',
|
||||
duration: 2000
|
||||
});
|
||||
}
|
||||
};
|
||||
// 通用打开裁剪器
|
||||
const openCropper = (callback, opts = {}) => {
|
||||
const { destWidth = 600, rectWidth = 300, fileType = 'jpg' } = opts;
|
||||
@ -192,20 +408,242 @@
|
||||
}, { destWidth: 1000, rectWidth: 500 });
|
||||
};
|
||||
|
||||
// 地址选择相关函数
|
||||
const normalizeNode = (node) => ({
|
||||
code: node?.code || '',
|
||||
name: node?.label || node?.value || node?.name || '',
|
||||
children: Array.isArray(node?.children) ? node.children : []
|
||||
});
|
||||
|
||||
const getAreaTree = () => {
|
||||
const raw = areaList && (areaList.default || areaList);
|
||||
return Array.isArray(raw) ? raw.map(normalizeNode) : [];
|
||||
};
|
||||
|
||||
const buildData = () => {
|
||||
const tree = getAreaTree();
|
||||
provinces.value = tree.length ? tree : [{ code: '', name: '', children: [] }];
|
||||
const pIdx = Math.min(pickerIndex.value[0], Math.max(provinces.value.length - 1, 0));
|
||||
const pNode = provinces.value[pIdx];
|
||||
cities.value = (pNode?.children || []).map(normalizeNode);
|
||||
if (!cities.value.length) cities.value = [{ code: '', name: '', children: [] }];
|
||||
const cIdx = Math.min(pickerIndex.value[1], Math.max(cities.value.length - 1, 0));
|
||||
const cNode = cities.value[cIdx];
|
||||
areas.value = (cNode?.children || []).map(normalizeNode);
|
||||
if (!areas.value.length) areas.value = [{ code: '', name: '' }];
|
||||
};
|
||||
|
||||
const openAreaPicker = () => {
|
||||
showAreaPicker.value = true;
|
||||
pickerIndex.value = [0,0,0];
|
||||
buildData();
|
||||
};
|
||||
|
||||
const closeAreaPicker = () => {
|
||||
showAreaPicker.value = false;
|
||||
};
|
||||
|
||||
const onAreaChange = (e) => {
|
||||
const val = (e && e.detail && e.detail.value) ? e.detail.value : [0,0,0];
|
||||
const [pi, ci, ai] = val;
|
||||
if (pi !== pickerIndex.value[0]) {
|
||||
pickerIndex.value = [pi, 0, 0];
|
||||
buildData();
|
||||
return;
|
||||
}
|
||||
if (ci !== pickerIndex.value[1]) {
|
||||
pickerIndex.value = [pi, ci, 0];
|
||||
buildData();
|
||||
return;
|
||||
}
|
||||
pickerIndex.value = [pi, ci, ai];
|
||||
};
|
||||
|
||||
const confirmArea = () => {
|
||||
const p = provinces.value[pickerIndex.value[0]];
|
||||
const c = cities.value[pickerIndex.value[1]];
|
||||
const a = areas.value[pickerIndex.value[2]];
|
||||
regionText.value = [p?.name, c?.name, a?.name].filter(Boolean).join(' ');
|
||||
// 更新表单中的医院地址
|
||||
form.value.hospitalName = regionText.value;
|
||||
closeAreaPicker();
|
||||
};
|
||||
|
||||
// 日期选择相关函数
|
||||
const initDatePicker = () => {
|
||||
const currentDate = new Date();
|
||||
const currentYear = currentDate.getFullYear();
|
||||
const currentMonth = currentDate.getMonth() + 1;
|
||||
const currentDay = currentDate.getDate();
|
||||
|
||||
// 生成年份列表(从1900年到今年)
|
||||
years.value = [];
|
||||
for (let year = currentYear; year >= 1900; year--) {
|
||||
years.value.push(year);
|
||||
}
|
||||
|
||||
// 生成月份列表
|
||||
months.value = [];
|
||||
for (let month = 1; month <= 12; month++) {
|
||||
months.value.push(month);
|
||||
}
|
||||
|
||||
// 生成日期列表
|
||||
updateDays(currentYear, currentMonth);
|
||||
|
||||
// 设置默认选中当前日期
|
||||
datePickerIndex.value = [0, currentMonth - 1, currentDay - 1];
|
||||
};
|
||||
|
||||
const updateDays = (year, month) => {
|
||||
const daysInMonth = new Date(year, month, 0).getDate();
|
||||
days.value = [];
|
||||
for (let day = 1; day <= daysInMonth; day++) {
|
||||
days.value.push(day);
|
||||
}
|
||||
};
|
||||
|
||||
const openDatePicker = () => {
|
||||
showDatePicker.value = true;
|
||||
initDatePicker();
|
||||
};
|
||||
|
||||
const closeDatePicker = () => {
|
||||
showDatePicker.value = false;
|
||||
};
|
||||
|
||||
const onDateChange = (e) => {
|
||||
const val = e.detail.value;
|
||||
const [yearIndex, monthIndex, dayIndex] = val;
|
||||
|
||||
const selectedYear = years.value[yearIndex];
|
||||
const selectedMonth = months.value[monthIndex];
|
||||
|
||||
// 更新日期列表
|
||||
updateDays(selectedYear, selectedMonth);
|
||||
|
||||
// 检查选择的日期是否晚于今天
|
||||
const selectedDate = new Date(selectedYear, selectedMonth - 1, days.value[dayIndex] || 1);
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
|
||||
if (selectedDate > today) {
|
||||
// 如果选择的日期晚于今天,重置为今天
|
||||
const currentDate = new Date();
|
||||
const currentYear = currentDate.getFullYear();
|
||||
const currentMonth = currentDate.getMonth() + 1;
|
||||
const currentDay = currentDate.getDate();
|
||||
|
||||
const yearIdx = years.value.findIndex(y => y === currentYear);
|
||||
const monthIdx = months.value.findIndex(m => m === currentMonth);
|
||||
const dayIdx = days.value.findIndex(d => d === currentDay);
|
||||
|
||||
if (yearIdx !== -1 && monthIdx !== -1 && dayIdx !== -1) {
|
||||
datePickerIndex.value = [yearIdx, monthIdx, dayIdx];
|
||||
}
|
||||
} else {
|
||||
datePickerIndex.value = val;
|
||||
}
|
||||
};
|
||||
|
||||
const confirmDate = () => {
|
||||
const [yearIndex, monthIndex, dayIndex] = datePickerIndex.value;
|
||||
const selectedYear = years.value[yearIndex];
|
||||
const selectedMonth = months.value[monthIndex];
|
||||
const selectedDay = days.value[dayIndex];
|
||||
|
||||
// 格式化日期
|
||||
const formattedDate = `${selectedYear}-${String(selectedMonth).padStart(2, '0')}-${String(selectedDay).padStart(2, '0')}`;
|
||||
|
||||
// 更新表单数据
|
||||
form.value.birthDate = formattedDate;
|
||||
|
||||
closeDatePicker();
|
||||
};
|
||||
|
||||
// 科室选择相关函数
|
||||
const openDeptPicker = () => {
|
||||
showDeptPicker.value = true;
|
||||
// 如果当前有选中的科室,设置默认选中项
|
||||
if (form.value.officeName) {
|
||||
const index = deptList.value.findIndex(dept => dept === form.value.officeName);
|
||||
if (index !== -1) {
|
||||
deptPickerIndex.value = [index];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const closeDeptPicker = () => {
|
||||
showDeptPicker.value = false;
|
||||
};
|
||||
|
||||
const onDeptChange = (e) => {
|
||||
const val = e.detail.value;
|
||||
deptPickerIndex.value = val;
|
||||
};
|
||||
|
||||
const confirmDept = () => {
|
||||
const selectedIndex = deptPickerIndex.value[0];
|
||||
const selectedDept = deptList.value[selectedIndex];
|
||||
|
||||
// 更新表单数据
|
||||
form.value.officeName = selectedDept;
|
||||
|
||||
closeDeptPicker();
|
||||
};
|
||||
|
||||
// 职称选择相关函数
|
||||
const openTitlePicker = () => {
|
||||
showTitlePicker.value = true;
|
||||
// 如果当前有选中的职称,设置默认选中项
|
||||
if (form.value.positionName) {
|
||||
const index = titleList.value.findIndex(title => title === form.value.positionName);
|
||||
if (index !== -1) {
|
||||
titlePickerIndex.value = [index];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const closeTitlePicker = () => {
|
||||
showTitlePicker.value = false;
|
||||
};
|
||||
|
||||
const onTitleChange = (e) => {
|
||||
const val = e.detail.value;
|
||||
titlePickerIndex.value = val;
|
||||
};
|
||||
|
||||
const confirmTitle = () => {
|
||||
const selectedIndex = titlePickerIndex.value[0];
|
||||
const selectedTitle = titleList.value[selectedIndex];
|
||||
|
||||
// 更新表单数据
|
||||
form.value.positionName = selectedTitle;
|
||||
|
||||
closeTitlePicker();
|
||||
};
|
||||
|
||||
const onPickGender = () => {};
|
||||
const onPickBirthday = () => {};
|
||||
const onPickBirthday = () => {
|
||||
openDatePicker();
|
||||
};
|
||||
const onEditPhone = () => {};
|
||||
const onEditEmail = () => {};
|
||||
const onPickHospital = () => {};
|
||||
const onPickDept = () => {};
|
||||
const onPickDept = () => {
|
||||
openDeptPicker();
|
||||
};
|
||||
const onEditDeptPhone = () => {};
|
||||
const onPickTitle = () => {};
|
||||
const onPickTitle = () => {
|
||||
openTitlePicker();
|
||||
};
|
||||
const onEditLicenseNo = () => {};
|
||||
const onEditSpecialty = () => {};
|
||||
const onEditIntro = () => {};
|
||||
|
||||
onShow(() => {
|
||||
// 可在此处拉取并回填个人资料
|
||||
getExpertByUuid();
|
||||
getPositionList();
|
||||
});
|
||||
</script>
|
||||
|
||||
@ -277,4 +715,67 @@
|
||||
border-radius: 8rpx;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
color: #bbb;
|
||||
}
|
||||
|
||||
/* 地区选择器样式 */
|
||||
.picker-mask {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0,0,0,0.4);
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.picker-panel {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: #fff;
|
||||
z-index: 1001;
|
||||
}
|
||||
|
||||
.picker-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 20rpx 24rpx;
|
||||
border-bottom: 1rpx solid #eee;
|
||||
}
|
||||
|
||||
.picker-title {
|
||||
font-size: 30rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.picker-btn {
|
||||
color: #666;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.picker-btn.ok {
|
||||
color: #38c1b1;
|
||||
}
|
||||
|
||||
.picker-view {
|
||||
height: 480rpx;
|
||||
}
|
||||
|
||||
.picker-item {
|
||||
height: 80rpx;
|
||||
line-height: 80rpx;
|
||||
text-align: center;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.picker-empty {
|
||||
padding: 40rpx;
|
||||
text-align: center;
|
||||
color: #999;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -165,7 +165,7 @@
|
||||
})
|
||||
}else if(type.value=='course'){
|
||||
uni.navigateTo({
|
||||
url: `/pages_course/index/index?keywords=${searchKeyword.value}`
|
||||
url: `/pages_course/course/course?keywords=${searchKeyword.value}`
|
||||
})
|
||||
}else if(type.value=='ppt'){
|
||||
uni.navigateTo({
|
||||
@ -173,7 +173,7 @@
|
||||
})
|
||||
}else if(type.value=='mall'){
|
||||
uni.navigateTo({
|
||||
url: `/pages_app/pointMall/pointMall?keywords=${searchKeyword.value}`
|
||||
url: `/pages_goods/pointMall/pointMall?keywords=${searchKeyword.value}`
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
123
pages_app/writeInfo/writeInfo.vue
Normal file
123
pages_app/writeInfo/writeInfo.vue
Normal file
@ -0,0 +1,123 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<!-- 顶部导航栏 -->
|
||||
<uni-nav-bar
|
||||
left-icon="left"
|
||||
title="修改邮箱"
|
||||
@clickLeft="goBack"
|
||||
fixed
|
||||
color="#8B2316"
|
||||
height="140rpx"
|
||||
:border="false"
|
||||
backgroundColor="#eeeeee"
|
||||
/>
|
||||
|
||||
<view class="content">
|
||||
<!-- 邮箱输入框 -->
|
||||
<view class="input-wrapper">
|
||||
<input
|
||||
class="input"
|
||||
type="text"
|
||||
placeholder="请输入您的邮箱"
|
||||
placeholder-class="ph"
|
||||
v-model.trim="email"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 提示文案 -->
|
||||
<view class="hint">请填写您的真实邮箱,以便联系上您</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部保存按钮 -->
|
||||
<view class="footer">
|
||||
<button class="save-btn" @click="onSave">保存</button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const email = ref('')
|
||||
|
||||
const goBack = () => {
|
||||
uni.navigateBack()
|
||||
}
|
||||
|
||||
const isValidEmail = (val) => {
|
||||
if (!val) return false
|
||||
const re = /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/
|
||||
return re.test(val)
|
||||
}
|
||||
|
||||
const onSave = () => {
|
||||
if (!isValidEmail(email.value)) {
|
||||
uni.showToast({ title: '请输入有效的邮箱', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
// 这里调用保存接口(占位)
|
||||
// await api.updateEmail({ email: email.value })
|
||||
uni.showToast({ title: '保存成功', icon: 'success' })
|
||||
setTimeout(() => goBack(), 800)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page {
|
||||
background-color: #fff;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 24rpx 30rpx;
|
||||
}
|
||||
|
||||
.input-wrapper {
|
||||
background: #fff;
|
||||
border: 2rpx solid #e6e6e6;
|
||||
border-radius: 16rpx;
|
||||
padding: 16rpx 20rpx;
|
||||
}
|
||||
|
||||
.input {
|
||||
height: 72rpx;
|
||||
line-height: 72rpx;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.ph {
|
||||
color: #c7c7c7;
|
||||
}
|
||||
|
||||
.hint {
|
||||
margin-top: 24rpx;
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.footer {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: env(safe-area-inset-bottom);
|
||||
padding: 24rpx 30rpx calc(24rpx + env(safe-area-inset-bottom));
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.save-btn {
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
line-height: 88rpx;
|
||||
text-align: center;
|
||||
color: #8b2316;
|
||||
font-size: 30rpx;
|
||||
border: 2rpx solid #8b2316;
|
||||
border-radius: 16rpx;
|
||||
background: #fff;
|
||||
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
132
pages_app/writeInfo/wtiteInfo.vue
Normal file
132
pages_app/writeInfo/wtiteInfo.vue
Normal file
@ -0,0 +1,132 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<!-- 顶部导航栏 -->
|
||||
<uni-nav-bar
|
||||
left-icon="left"
|
||||
title="修改邮箱"
|
||||
@clickLeft="goBack"
|
||||
fixed
|
||||
color="#8B2316"
|
||||
height="140rpx"
|
||||
:border="false"
|
||||
backgroundColor="#eeeeee"
|
||||
/>
|
||||
|
||||
<view class="content">
|
||||
{{ eitType === 'email' }}
|
||||
<!-- 邮箱输入框 -->
|
||||
<view class="input-wrapper">
|
||||
<input
|
||||
v-if="eitType === 'email'"
|
||||
class="input"
|
||||
type="text"
|
||||
:placeholder="placeholder"
|
||||
placeholder-class="ph"
|
||||
v-model.trim="content"
|
||||
|
||||
>
|
||||
</input>
|
||||
<textarea v-model.trim="content" placeholder="请输入您的个人简介(500字以内)" placeholder-class="ph" auto-height v-else > </textarea>
|
||||
</view>
|
||||
|
||||
|
||||
|
||||
<!-- 提示文案 -->
|
||||
<view class="hint">请填写您的真实邮箱,以便联系上您</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部保存按钮 -->
|
||||
<view class="footer">
|
||||
<button class="save-btn" @click="onSave">保存</button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const content = ref('')
|
||||
const eitType = ref('1ww');
|
||||
const placeholder = ref('请输入您的邮箱');
|
||||
|
||||
const goBack = () => {
|
||||
uni.navigateBack()
|
||||
}
|
||||
|
||||
const isValidEmail = (val) => {
|
||||
if (!val) return false
|
||||
const re = /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/
|
||||
return re.test(val)
|
||||
}
|
||||
|
||||
const onSave = () => {
|
||||
if (!isValidEmail(email.value)) {
|
||||
uni.showToast({ title: '请输入有效的邮箱', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
// 这里调用保存接口(占位)
|
||||
// await api.updateEmail({ email: email.value })
|
||||
uni.showToast({ title: '保存成功', icon: 'success' })
|
||||
setTimeout(() => goBack(), 800)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page {
|
||||
background-color: #fff;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 24rpx 30rpx;
|
||||
}
|
||||
|
||||
.input-wrapper {
|
||||
background: #fff;
|
||||
border: 2rpx solid #e6e6e6;
|
||||
border-radius: 16rpx;
|
||||
padding: 16rpx 20rpx;
|
||||
}
|
||||
|
||||
.input {
|
||||
height: 60rpx;
|
||||
line-height:60rpx;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.ph {
|
||||
color: #c7c7c7;
|
||||
}
|
||||
|
||||
.hint {
|
||||
margin-top: 24rpx;
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.footer {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: env(safe-area-inset-bottom);
|
||||
padding: 24rpx 30rpx calc(24rpx + env(safe-area-inset-bottom));
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.save-btn {
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
line-height: 88rpx;
|
||||
text-align: center;
|
||||
color: #8b2316;
|
||||
font-size: 30rpx;
|
||||
border: 2rpx solid #8b2316;
|
||||
border-radius: 16rpx;
|
||||
background: #fff;
|
||||
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
72
uni_modules/qf-image-cropper/changelog.md
Normal file
72
uni_modules/qf-image-cropper/changelog.md
Normal file
@ -0,0 +1,72 @@
|
||||
## 2.2.5(2024-07-30)
|
||||
* 修复 当 checkRange=true 时,拖动四个伸缩角放大图片时还可能会超出或未到边界的问题
|
||||
* 修复 当 checkRange=false 时,图片旋转时会放大图片适应裁剪尺寸的问题
|
||||
* 修复 当 checkRange=true 时,图片旋转 90° 或 270° 进行缩放可能会无法拖动图片的问题
|
||||
## 2.2.4(2024-06-21)
|
||||
* 新增 reverseRotatable 属性,是否支持逆向翻转
|
||||
* 修复 `2.1.7` 版本导致旋转后图片没有自动适配裁剪框的问题
|
||||
|
||||
## 2.2.3(2024-06-21)
|
||||
* 新增 gpu 属性,是否开启硬件加速,图片缩放过程中如果出现元素的“留影”或“重影”效果,可通过该方式解决或减轻这一问题
|
||||
* 修复 组件使用 `v-if` 并设置 `src` 属性时可能会出现图片渲染位置存在偏差的问题
|
||||
|
||||
## 2.2.2(2024-06-21)
|
||||
* 优化 组件实例 chooseImage 方法支持传参
|
||||
* 修复 组件使用 `v-if` 时组件无非正常渲染的问题
|
||||
|
||||
## 2.2.1(2024-06-15)
|
||||
* 修复 H5平台不支持手势拖动图片的问题
|
||||
|
||||
## 2.2.0(2024-05-31)
|
||||
* 修复 APP平台 `vue2` 项目因 `2.1.9` 版本修复 `vue3` 项目bug而引发的问题
|
||||
|
||||
## 2.1.9(2024-05-29)
|
||||
* 修复 APP平台 `vue3` 项目因 uniapp `renderjs` 中未支持条件编译,导致运行了H5平台代码报错的问题
|
||||
|
||||
## 2.1.8(2024-05-29)
|
||||
* 新增 zIndex 属性,调整组件层级
|
||||
* 新增 组件内容插槽
|
||||
* 优化 微信小程序平台动态修改元素style时的多余内容
|
||||
|
||||
## 2.1.7(2024-05-28)
|
||||
* 新增 checkRange 属性,当 checkRange=false 时允许图片位置超出裁剪边界
|
||||
* 新增 minScale 属性,图片最小缩放倍数,当 minScale<0 时可使图片宽高不再受裁剪区域宽高限制
|
||||
* 新增 backgroundColor 属性,生成图片背景色,如果裁剪区域没有完全包含在图片中时,不设置该属性生成图片存在一定的透明块
|
||||
* 优化 动态修改图片宽高但没有传入src时,尺寸适应问题
|
||||
* 修复 APP平台通过 `this.$ownerInstance` 获取组件实例时机过早,其值为 `undefined` 导致报错界面没有正常渲染的问题
|
||||
|
||||
## 2.1.6(2023-04-16)
|
||||
* 修复 组件使用 v-show 指令会导致选择图片后初始位置严重偏位的问题
|
||||
|
||||
## 2.1.5(2023-04-15)
|
||||
* 新增 兼容APP平台
|
||||
|
||||
## 2.1.4(2023-03-13)
|
||||
* 新增 fileType 属性,用于指定生成文件的类型,只支持 'jpg' 或 'png',默认为 'png'
|
||||
* 新增 delay 属性,微信小程序平台使用 `Canvas 2D` 绘制时控制图片从绘制到生成所需时间
|
||||
* 优化 当生成图片的尺寸宽/高超过 Canvas 2D 最大限制(1365*1365)则将画布尺寸缩放在限制范围内绘制完成后输出目标尺寸
|
||||
* 优化 旋转图标指示方向与实际旋转方向不符
|
||||
|
||||
## 2.1.3(2023-02-06)
|
||||
* 优化 vue3支持
|
||||
|
||||
## 2.1.2(2023-02-03)
|
||||
* 新增 navigation 属性,H5平台当 showAngle 为 true 时,使用插件的页面在 `page.json` 中配置了 "navigationStyle": "custom" 时,必须将此值设为 false ,否则四个可拉伸角的触发位置会有偏差
|
||||
* 修复 H5平台部分设备(已知iPhone11以下机型)拍照的图片缩放时会闪动的问题
|
||||
|
||||
## 2.1.1(2022-12-06)
|
||||
* 修复 横屏适配问题
|
||||
|
||||
## 2.1.0(2022-12-06)
|
||||
* 新增 兼容H5平台,使用 renderjs 响应手势事件
|
||||
|
||||
## 2.0.0(2022-12-05)
|
||||
* 重构 插件,使用 WXS 响应手势事件
|
||||
* 新增 图片翻转
|
||||
* 新增 拉伸裁剪框放大图片
|
||||
* 新增 监听PC鼠标滚轮触发缩放
|
||||
* 新增 圆形、圆角矩形的图片裁剪
|
||||
* 优化 图片缩放,移动端以双指触摸中心点为缩放中心点,PC端以鼠标所在点为缩放中心点
|
||||
* 优化 裁剪框样式
|
||||
* 优化 图片位置拖动 支持边界回弹效果(滑动时可滑出边界,释放时回弹到边界)
|
||||
* 优化 生成图片使用新版 Canvas 2D 接口
|
||||
@ -0,0 +1,855 @@
|
||||
/**
|
||||
* 图片编辑器-手势监听
|
||||
* 1. 支持编译到app-vue(uni-app 2.5.5及以上版本)、H5上
|
||||
*/
|
||||
/** 图片偏移量 */
|
||||
var offset = { x: 0, y: 0 };
|
||||
/** 图片缩放比例 */
|
||||
var scale = 1;
|
||||
/** 图片最小缩放比例 */
|
||||
var minScale = 1;
|
||||
/** 图片旋转角度 */
|
||||
var rotate = 0;
|
||||
/** 触摸点 */
|
||||
var touches = [];
|
||||
/** 图片布局信息 */
|
||||
var img = {};
|
||||
/** 系统信息 */
|
||||
var sys = {};
|
||||
/** 裁剪区域布局信息 */
|
||||
var area = {};
|
||||
/** 触摸行为类型 */
|
||||
var touchType = '';
|
||||
/** 操作角的位置 */
|
||||
var activeAngle = 0;
|
||||
/** 裁剪区域布局信息偏移量 */
|
||||
var areaOffset = { left: 0, right: 0, top: 0, bottom: 0 };
|
||||
/** 元素ID */
|
||||
var elIds = {
|
||||
'imageStyles': 'crop-image',
|
||||
'maskStylesList': 'crop-mask-block',
|
||||
'borderStyles': 'crop-border',
|
||||
'circleBoxStyles': 'crop-circle-box',
|
||||
'circleStyles': 'crop-circle',
|
||||
'gridStylesList': 'crop-grid',
|
||||
'angleStylesList': 'crop-angle',
|
||||
}
|
||||
/** 记录上次初始化时间戳,排除APP重复更新 */
|
||||
var timestamp = 0;
|
||||
/** vue3 renderjs 条件编译无效,以此方式区别 APP 和 H5 */
|
||||
// #ifdef H5
|
||||
var platform = 'H5';
|
||||
// #endif
|
||||
// #ifdef APP
|
||||
var platform = 'APP';
|
||||
// #endif
|
||||
/** 容错值 */
|
||||
var fault = 0.000001;
|
||||
/**
|
||||
* 获取a、b两数中的最小正数
|
||||
* @param a
|
||||
* @param b
|
||||
*/
|
||||
function minimum(a, b) {
|
||||
if (a > 0 && b < 0) return a;
|
||||
if (a < 0 && b > 0) return b;
|
||||
if (a > 0 && b > 0) return Math.min(a, b);
|
||||
return 0;
|
||||
}
|
||||
/**
|
||||
* 在容错访问内获取n近似值
|
||||
* @param n
|
||||
*/
|
||||
function num(n) {
|
||||
var m = parseFloat((n).toFixed(6));
|
||||
return m === fault || m === -fault ? 0 : m;
|
||||
}
|
||||
/**
|
||||
* 比较a值在容错值范围内是否等于b值
|
||||
* @param a
|
||||
* @param b
|
||||
*/
|
||||
function equalsByFault(a, b) {
|
||||
return Math.abs(a - b) <= fault;
|
||||
}
|
||||
/**
|
||||
* 比较a值在容错值范围内是否小于b值
|
||||
* @param a
|
||||
* @param b
|
||||
*/
|
||||
function lessThanByFault(a, b) {
|
||||
var c = a - b;
|
||||
return c < 0 ? c < -fault : c < fault;
|
||||
}
|
||||
/**
|
||||
* 验证并获取有效最大值
|
||||
* @param v
|
||||
* @param max
|
||||
* @param isInclude
|
||||
* @param x
|
||||
* @param y
|
||||
* @param rate
|
||||
* @returns
|
||||
*/
|
||||
function validMax(v, max, isInclude, x, y, rate) {
|
||||
if(typeof max === 'number') {
|
||||
if(isInclude && equalsByFault(max, y)) { // 宽高不等时,x轴用y轴值要做等比例转换
|
||||
var n = num(max * rate);
|
||||
if (n <= x) return n; // 转化后值在x轴最大值范围内
|
||||
return x; // 转化后值超出x轴最大值范围则用最大值
|
||||
}
|
||||
return max;
|
||||
}
|
||||
return v;
|
||||
}
|
||||
/**
|
||||
* 样式对象转字符串
|
||||
* @param {Object} style 样式对象
|
||||
*/
|
||||
function styleToString(style) {
|
||||
if(typeof style === 'string') return style;
|
||||
var str = '';
|
||||
for (let k in style) {
|
||||
str += k + ':' + style[k] + ';';
|
||||
}
|
||||
return str;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @param {Object} instance 页面实例对象
|
||||
* @param {Object} key 要修改样式的key
|
||||
* @param {Object|Array} style 样式
|
||||
*/
|
||||
function setStyle(instance, key, style) {
|
||||
// console.log('setStyle', instance, key, JSON.stringify(style))
|
||||
// #ifdef APP-PLUS
|
||||
if(platform === 'APP') {
|
||||
if(Object.prototype.toString.call(style) === '[object Array]') {
|
||||
for (var i = 0, len = style.length; i < len; i++) {
|
||||
var el = window.document.getElementById(elIds[key] + '-' + (i + 1));
|
||||
el && (el.style = styleToString(style[i]));
|
||||
}
|
||||
} else {
|
||||
var el = window.document.getElementById(elIds[key]);
|
||||
el && (el.style = styleToString(style));
|
||||
}
|
||||
}
|
||||
// #endif
|
||||
// #ifdef H5
|
||||
if(platform === 'H5') instance[key] = style;
|
||||
// #endif
|
||||
}
|
||||
/**
|
||||
* 触发页面实例指定方法
|
||||
* @param {Object} instance 页面实例对象
|
||||
* @param {Object} name 方法名称
|
||||
* @param {Object} obj 传递参数
|
||||
*/
|
||||
function callMethod(instance, name, obj) {
|
||||
// #ifdef APP-PLUS
|
||||
if(platform === 'APP') instance.callMethod(name, obj);
|
||||
// #endif
|
||||
// #ifdef H5
|
||||
if(platform === 'H5') instance[name](obj);
|
||||
// #endif
|
||||
}
|
||||
/**
|
||||
* 计算两点间距
|
||||
* @param {Object} touches 触摸点信息
|
||||
*/
|
||||
function getDistanceByTouches(touches) {
|
||||
// 根据勾股定理求两点间距离
|
||||
var a = touches[1].pageX - touches[0].pageX;
|
||||
var b = touches[1].pageY - touches[0].pageY;
|
||||
var c = Math.sqrt(Math.pow(a, 2) + Math.pow(b, 2));
|
||||
// 求两点间的中点坐标
|
||||
// 1. a、b可能为负值
|
||||
// 2. 在求a、b时,如用touches[1]减touches[0],则求中点坐标也得用touches[1]减a/2、b/2
|
||||
// 3. 同理,在求a、b时,也可用touches[0]减touches[1],则求中点坐标也得用touches[0]减a/2、b/2
|
||||
var x = touches[1].pageX - a / 2;
|
||||
var y = touches[1].pageY - b / 2;
|
||||
return { c, x, y };
|
||||
};
|
||||
|
||||
/**
|
||||
* 修正取值
|
||||
* @param {Object} a
|
||||
* @param {Object} b
|
||||
* @param {Object} c
|
||||
* @param {Object} reverse 是否反向
|
||||
*/
|
||||
function correctValue(a, b, c, reverse) {
|
||||
return num(reverse ? Math.max(Math.min(a, b), c) : Math.min(Math.max(a, b), c));
|
||||
}
|
||||
|
||||
/**
|
||||
* 旋转90°或270°时检查边界:限制 x、y 拖动范围,禁止滑出边界
|
||||
* @param {Object} e 点坐标
|
||||
* @param {Object} xReverse x是否反向
|
||||
* @param {Object} yReverse y是否反向
|
||||
*/
|
||||
function checkRotateRange(e, xReverse, yReverse) {
|
||||
var o = num((img.height - img.width) / 2); // 宽高差值一半
|
||||
return {
|
||||
x: correctValue(e.x, -img.height + o + area.width + area.left, area.left + o, xReverse),
|
||||
y: correctValue(e.y, -img.width - o + area.height + area.top, area.top - o, yReverse)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查边界:限制 x、y 拖动范围,禁止滑出边界
|
||||
* @param {Object} e 点坐标
|
||||
*/
|
||||
function checkRange(e) {
|
||||
var r = rotate / 90 % 2;
|
||||
if(r === 1) { // 因图片宽高可能不等,翻转 90° 或 270° 后图片宽高需反着计算,且左右和上下边界要根据差值做偏移
|
||||
if (area.width === area.height) {
|
||||
return checkRotateRange(e, img.height < area.height, img.width < area.width);
|
||||
}
|
||||
var isInclude = img.height < area.width && img.width < area.height; // 图片是否包含在裁剪区域内
|
||||
if (img.width < area.height || img.height < area.width) {
|
||||
if (area.width < area.height && img.width < img.height) {
|
||||
return isInclude
|
||||
? checkRotateRange(e, area.width < area.height, area.width < area.height)
|
||||
: checkRotateRange(e, false, true);
|
||||
}
|
||||
if (area.height < area.width && img.height < img.width) {
|
||||
return isInclude
|
||||
? checkRotateRange(e, area.height < area.width, area.height < area.width)
|
||||
: checkRotateRange(e, true, false);
|
||||
}
|
||||
}
|
||||
if (img.height >= area.width && img.width >= area.height) {
|
||||
return checkRotateRange(e, false, false);
|
||||
}
|
||||
if (isInclude) {
|
||||
return area.height < area.width
|
||||
? checkRotateRange(e, true, true)
|
||||
: checkRotateRange(e, area.width < area.height, area.width < area.height);
|
||||
}
|
||||
if (img.height < area.width && !img.width < area.height) {
|
||||
return checkRotateRange(e, true, false);
|
||||
}
|
||||
if (!img.height < area.width && img.width < area.height) {
|
||||
return checkRotateRange(e, false, true);
|
||||
}
|
||||
return checkRotateRange(e, img.height < area.height, img.width < area.width);
|
||||
}
|
||||
return {
|
||||
x: correctValue(e.x, -img.width + area.width + area.left, area.left, img.width < area.width),
|
||||
y: correctValue(e.y, -img.height + area.height + area.top, area.top, img.height < area.height)
|
||||
}
|
||||
};
|
||||
/**
|
||||
* 变更图片布局信息
|
||||
* @param {Object} e 布局信息
|
||||
*/
|
||||
function changeImageRect(e) {
|
||||
// console.log('changeImageRect', e)
|
||||
offset.x += e.x || 0;
|
||||
offset.y += e.y || 0;
|
||||
if(e.check && area.checkRange) { // 检查边界
|
||||
var point = checkRange(offset);
|
||||
if(offset.x !== point.x || offset.y !== point.y) {
|
||||
offset = point;
|
||||
}
|
||||
}
|
||||
|
||||
// 因频繁修改 width/height 会造成大量的内存消耗,改为scale
|
||||
// e.instance.imageStyles = {
|
||||
// width: img.width + 'px',
|
||||
// height: img.height + 'px',
|
||||
// transform: 'translate(' + (offset.x + ox) + 'px, ' + (offset.y + ox) + 'px) rotate(' + rotate +'deg)'
|
||||
// };
|
||||
var ox = (img.width - img.oldWidth) / 2;
|
||||
var oy = (img.height - img.oldHeight) / 2;
|
||||
// e.instance.imageStyles = {
|
||||
// width: img.oldWidth + 'px',
|
||||
// height: img.oldHeight + 'px',
|
||||
// transform: 'translate(' + (offset.x + ox) + 'px, ' + (offset.y + oy) + 'px) rotate(' + rotate +'deg) scale(' + scale + ')'
|
||||
// };
|
||||
setStyle(e.instance, 'imageStyles', {
|
||||
width: img.oldWidth + 'px',
|
||||
height: img.oldHeight + 'px',
|
||||
transform: (img.gpu ? 'translateZ(0) ' : '') + 'translate(' + (offset.x + ox) + 'px, ' + (offset.y + oy) + 'px' + ') rotate(' + rotate +'deg) scale(' + scale + ')'
|
||||
});
|
||||
callMethod(e.instance, 'dataChange', {
|
||||
width: img.width,
|
||||
height: img.height,
|
||||
x: offset.x,
|
||||
y: offset.y,
|
||||
rotate: rotate
|
||||
});
|
||||
};
|
||||
/**
|
||||
* 变更裁剪区域布局信息
|
||||
* @param {Object} e 布局信息
|
||||
*/
|
||||
function changeAreaRect(e) {
|
||||
// console.log('changeAreaRect', e)
|
||||
// 变更蒙版样式
|
||||
setStyle(e.instance, 'maskStylesList', [
|
||||
{
|
||||
left: 0,
|
||||
width: (area.left + areaOffset.left) + 'px',
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
'z-index': area.zIndex + 2
|
||||
},
|
||||
{
|
||||
left: (area.right + areaOffset.right) + 'px',
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
'z-index': area.zIndex + 2
|
||||
},
|
||||
{
|
||||
left: (area.left + areaOffset.left) + 'px',
|
||||
width: (area.width + areaOffset.right - areaOffset.left) + 'px',
|
||||
top: 0,
|
||||
height: (area.top + areaOffset.top) + 'px',
|
||||
'z-index': area.zIndex + 2
|
||||
},
|
||||
{
|
||||
left: (area.left + areaOffset.left) + 'px',
|
||||
width: (area.width + areaOffset.right - areaOffset.left) + 'px',
|
||||
top: (area.bottom + areaOffset.bottom) + 'px',
|
||||
// height: (area.top - areaOffset.bottom + sys.offsetBottom) + 'px',
|
||||
bottom: 0,
|
||||
'z-index': area.zIndex + 2
|
||||
}
|
||||
]);
|
||||
// 变更边框样式
|
||||
if(area.showBorder) {
|
||||
setStyle(e.instance, 'borderStyles', {
|
||||
left: (area.left + areaOffset.left) + 'px',
|
||||
top: (area.top + areaOffset.top) + 'px',
|
||||
width: (area.width + areaOffset.right - areaOffset.left) + 'px',
|
||||
height: (area.height + areaOffset.bottom - areaOffset.top) + 'px',
|
||||
'z-index': area.zIndex + 3
|
||||
});
|
||||
}
|
||||
|
||||
// 变更参考线样式
|
||||
if(area.showGrid) {
|
||||
setStyle(e.instance, 'gridStylesList', [
|
||||
{
|
||||
'border-width': '1px 0 0 0',
|
||||
left: (area.left + areaOffset.left) + 'px',
|
||||
right: (area.right + areaOffset.right) + 'px',
|
||||
top: (area.top + areaOffset.top + (area.height + areaOffset.bottom - areaOffset.top) / 3 - 0.5) + 'px',
|
||||
width: (area.width + areaOffset.right - areaOffset.left) + 'px',
|
||||
'z-index': area.zIndex + 3
|
||||
},
|
||||
{
|
||||
'border-width': '1px 0 0 0',
|
||||
left: (area.left + areaOffset.left) + 'px',
|
||||
right: (area.right + areaOffset.right) + 'px',
|
||||
top: (area.top + areaOffset.top + (area.height + areaOffset.bottom - areaOffset.top) * 2 / 3 - 0.5) + 'px',
|
||||
width: (area.width + areaOffset.right - areaOffset.left) + 'px',
|
||||
'z-index': area.zIndex + 3
|
||||
},
|
||||
{
|
||||
'border-width': '0 1px 0 0',
|
||||
top: (area.top + areaOffset.top) + 'px',
|
||||
bottom: (area.bottom + areaOffset.bottom) + 'px',
|
||||
left: (area.left + areaOffset.left + (area.width + areaOffset.right - areaOffset.left) / 3 - 0.5) + 'px',
|
||||
height: (area.height + areaOffset.bottom - areaOffset.top) + 'px',
|
||||
'z-index': area.zIndex + 3
|
||||
},
|
||||
{
|
||||
'border-width': '0 1px 0 0',
|
||||
top: (area.top + areaOffset.top) + 'px',
|
||||
bottom: (area.bottom + areaOffset.bottom) + 'px',
|
||||
left: (area.left + areaOffset.left + (area.width + areaOffset.right - areaOffset.left) * 2 / 3 - 0.5) + 'px',
|
||||
height: (area.height + areaOffset.bottom - areaOffset.top) + 'px',
|
||||
'z-index': area.zIndex + 3
|
||||
}
|
||||
]);
|
||||
}
|
||||
|
||||
// 变更四个伸缩角样式
|
||||
if(area.showAngle) {
|
||||
setStyle(e.instance, 'angleStylesList', [
|
||||
{
|
||||
'border-width': area.angleBorderWidth + 'px 0 0 ' + area.angleBorderWidth + 'px',
|
||||
left: (area.left + areaOffset.left - area.angleBorderWidth) + 'px',
|
||||
top: (area.top + areaOffset.top - area.angleBorderWidth) + 'px',
|
||||
'z-index': area.zIndex + 3
|
||||
},
|
||||
{
|
||||
'border-width': area.angleBorderWidth + 'px ' + area.angleBorderWidth + 'px 0 0',
|
||||
left: (area.right + areaOffset.right - area.angleSize) + 'px',
|
||||
top: (area.top + areaOffset.top - area.angleBorderWidth) + 'px',
|
||||
'z-index': area.zIndex + 3
|
||||
},
|
||||
{
|
||||
'border-width': '0 0 ' + area.angleBorderWidth + 'px ' + area.angleBorderWidth + 'px',
|
||||
left: (area.left + areaOffset.left - area.angleBorderWidth) + 'px',
|
||||
top: (area.bottom + areaOffset.bottom - area.angleSize) + 'px',
|
||||
'z-index': area.zIndex + 3
|
||||
},
|
||||
{
|
||||
'border-width': '0 ' + area.angleBorderWidth + 'px ' + area.angleBorderWidth + 'px 0',
|
||||
left: (area.right + areaOffset.right - area.angleSize) + 'px',
|
||||
top: (area.bottom + areaOffset.bottom - area.angleSize) + 'px',
|
||||
'z-index': area.zIndex + 3
|
||||
}
|
||||
]);
|
||||
}
|
||||
|
||||
// 变更圆角样式
|
||||
if(area.radius > 0) {
|
||||
var radius = area.radius;
|
||||
if(area.width === area.height && area.radius >= area.width / 2) { // 圆形
|
||||
radius = (area.width / 2);
|
||||
} else { // 圆角矩形
|
||||
if(area.width !== area.height) { // 限制圆角半径不能超过短边的一半
|
||||
radius = Math.min(area.width / 2, area.height / 2, radius);
|
||||
}
|
||||
}
|
||||
setStyle(e.instance, 'circleBoxStyles', {
|
||||
left: (area.left + areaOffset.left) + 'px',
|
||||
top: (area.top + areaOffset.top) + 'px',
|
||||
width: (area.width + areaOffset.right - areaOffset.left) + 'px',
|
||||
height: (area.height + areaOffset.bottom - areaOffset.top) + 'px',
|
||||
'z-index': area.zIndex + 2
|
||||
});
|
||||
setStyle(e.instance, 'circleStyles', {
|
||||
'box-shadow': '0 0 0 ' + Math.max(area.width, area.height) + 'px rgba(51, 51, 51, 0.8)',
|
||||
'border-radius': radius + 'px'
|
||||
});
|
||||
}
|
||||
};
|
||||
/**
|
||||
* 缩放图片
|
||||
* @param {Object} e 布局信息
|
||||
*/
|
||||
function scaleImage(e) {
|
||||
// console.log('scaleImage', e)
|
||||
var last = scale;
|
||||
scale = Math.min(Math.max(e.scale + scale, minScale), img.maxScale);
|
||||
if(last !== scale) {
|
||||
img.width = num(img.oldWidth * scale);
|
||||
img.height = num(img.oldHeight * scale);
|
||||
// 参考问题:有一个长4000px、宽4000px的四方形ABCD,A点的坐标固定在(-2000,-2000),
|
||||
// 该四边形上有一个点E,坐标为(-100,-300),将该四方形复制一份并缩小到90%后,
|
||||
// 新四边形的A点坐标为多少时可使新四边形的E点与原四边形的E点重合?
|
||||
// 预期效果:从图中选取某点(参照物)为中心点进行缩放,缩放时无论图像怎么变化,该点位置始终固定不变
|
||||
// 计算方法:以相同起点先计算缩放前后两点间的距离,再加上原图像偏移量即可
|
||||
e.x = num((e.x - offset.x) * (1 - scale / last));
|
||||
e.y = num((e.y - offset.y) * (1 - scale / last));
|
||||
changeImageRect(e);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
/**
|
||||
* 获取触摸点在哪个角
|
||||
* @param {number} x 触摸点x轴坐标
|
||||
* @param {number} y 触摸点y轴坐标
|
||||
* @return {number} 角的位置:0=无;1=左上;2=右上;3=左下;4=右下;
|
||||
*/
|
||||
function getToucheAngle(x, y) {
|
||||
// console.log('getToucheAngle', x, y, JSON.stringify(area))
|
||||
var o = area.angleBorderWidth; // 需扩大触发范围则把 o 值加大即可
|
||||
var oy = sys.navigation ? 0 : sys.windowTop;
|
||||
if(y >= area.top - o + oy && y <= area.top + area.angleSize + o + oy) {
|
||||
if(x >= area.left - o && x <= area.left + area.angleSize + o) {
|
||||
return 1; // 左上角
|
||||
} else if(x >= area.right - area.angleSize - o && x <= area.right + o) {
|
||||
return 2; // 右上角
|
||||
}
|
||||
} else if(y >= area.bottom - area.angleSize - o + oy && y <= area.bottom + o + oy) {
|
||||
if(x >= area.left - o && x <= area.left + area.angleSize + o) {
|
||||
return 3; // 左下角
|
||||
} else if(x >= area.right - area.angleSize - o && x <= area.right + o) {
|
||||
return 4; // 右下角
|
||||
}
|
||||
}
|
||||
return 0; // 无触摸到角
|
||||
};
|
||||
/**
|
||||
* 重置数据
|
||||
*/
|
||||
function resetData() {
|
||||
offset = { x: 0, y: 0 };
|
||||
scale = 1;
|
||||
minScale = img.minScale;
|
||||
rotate = 0;
|
||||
};
|
||||
function getTouchs(touches) {
|
||||
var result = [];
|
||||
var len = touches ? touches.length : 0
|
||||
for (var i = 0; i < len; i++) {
|
||||
result[i] = {
|
||||
pageX: touches[i].pageX,
|
||||
// h5无标题栏时,窗口顶部距离仍为标题栏高度,且触摸点y轴坐标还是有标题栏的值,即减去标题栏高度的值
|
||||
pageY: touches[i].pageY + sys.windowTop
|
||||
};
|
||||
}
|
||||
return result;
|
||||
};
|
||||
var mouseEvent = false;
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
imageStyles: {},
|
||||
maskStylesList: [{}, {}, {}, {}],
|
||||
borderStyles: {},
|
||||
gridStylesList: [{}, {}, {}, {}],
|
||||
angleStylesList: [{}, {}, {}, {}],
|
||||
circleBoxStyles: {},
|
||||
circleStyles: {}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
// 监听 PC 端鼠标滚轮
|
||||
// #ifdef H5
|
||||
platform === 'H5' && window.addEventListener('mousewheel', async (e) => {
|
||||
var touchs = getTouchs([e])
|
||||
img.src && scaleImage({
|
||||
instance: await this.getInstance(),
|
||||
check: true,
|
||||
// 鼠标向上滚动时,deltaY 固定 -100,鼠标向下滚动时,deltaY 固定 100
|
||||
scale: e.deltaY > 0 ? -0.05 : 0.05,
|
||||
x: touchs[0].pageX,
|
||||
y: touchs[0].pageY
|
||||
});
|
||||
});
|
||||
// #endif
|
||||
},
|
||||
// #ifdef H5
|
||||
mounted() {
|
||||
platform === 'H5' && this.initH5Events();
|
||||
},
|
||||
// #endif
|
||||
setPlatform(p) {
|
||||
platform = p;
|
||||
},
|
||||
methods: {
|
||||
// #ifdef H5
|
||||
getTouchEvent(e) {
|
||||
e.touches = [
|
||||
{ pageX: e.pageX, pageY: e.pageY }
|
||||
];
|
||||
return e;
|
||||
},
|
||||
initH5Events() {
|
||||
const preview = document.getElementById('pic-preview');
|
||||
preview?.addEventListener('mousedown', (e, ev) => {
|
||||
mouseEvent = true;
|
||||
this.touchstart(this.getTouchEvent(e));
|
||||
});
|
||||
preview?.addEventListener('mousemove', (e) => {
|
||||
if (!mouseEvent) return;
|
||||
this.touchmove(this.getTouchEvent(e));
|
||||
});
|
||||
preview?.addEventListener('mouseup', (e) => {
|
||||
mouseEvent = false;
|
||||
this.touchend(this.getTouchEvent(e))
|
||||
});
|
||||
preview?.addEventListener('mouseleave', (e) => {
|
||||
mouseEvent = false;
|
||||
this.touchend(this.getTouchEvent(e))
|
||||
});
|
||||
},
|
||||
// #endif
|
||||
async getInstance() {
|
||||
// #ifdef APP-PLUS
|
||||
if(platform === 'APP')
|
||||
return this.$ownerInstance
|
||||
? Promise.resolve(this.$ownerInstance)
|
||||
: new Promise((resolve) => {
|
||||
setTimeout(async () => {
|
||||
resolve(await this.getInstance());
|
||||
});
|
||||
});
|
||||
// #endif
|
||||
// #ifdef H5
|
||||
if(platform === 'H5')
|
||||
return Promise.resolve(this);
|
||||
// #endif
|
||||
},
|
||||
/**
|
||||
* 初始化:观察数据变更
|
||||
* @param {Object} newVal 新数据
|
||||
* @param {Object} oldVal 旧数据
|
||||
* @param {Object} o 组件实例对象
|
||||
*/
|
||||
initObserver: async function(newVal, oldVal, o, i) {
|
||||
// console.log('initObserver', newVal, oldVal, o, i)
|
||||
if(newVal && (!img.src || timestamp !== newVal.timestamp)) {
|
||||
timestamp = newVal.timestamp;
|
||||
img = newVal.img;
|
||||
sys = newVal.sys;
|
||||
area = newVal.area;
|
||||
minScale = img.minScale;
|
||||
resetData();
|
||||
const instance = await this.getInstance()
|
||||
img.src && changeImageRect({
|
||||
instance,
|
||||
x: (sys.windowWidth - img.width) / 2,
|
||||
y: (sys.windowHeight + sys.windowTop - sys.offsetBottom - img.height) / 2
|
||||
});
|
||||
changeAreaRect({
|
||||
instance
|
||||
});
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 鼠标滚轮滚动
|
||||
* @param {Object} e 事件对象
|
||||
* @param {Object} o 组件实例对象
|
||||
*/
|
||||
mousewheel: function(e, o) {
|
||||
// h5平台 wheel 事件无法判断滚轮滑动方向,需使用 mousewheel
|
||||
},
|
||||
/**
|
||||
* 触摸开始
|
||||
* @param {Object} e 事件对象
|
||||
* @param {Object} o 组件实例对象
|
||||
*/
|
||||
touchstart: function(e, o) {
|
||||
if(!img.src) return;
|
||||
touches = getTouchs(e.touches);
|
||||
activeAngle = area.showAngle ? getToucheAngle(touches[0].pageX, touches[0].pageY) : 0;
|
||||
if(touches.length === 1 && activeAngle !== 0) {
|
||||
touchType = 'stretch'; // 伸缩裁剪区域
|
||||
} else {
|
||||
touchType = '';
|
||||
}
|
||||
// console.log('touchstart', e, activeAngle)
|
||||
},
|
||||
/**
|
||||
* 触摸移动
|
||||
* @param {Object} e 事件对象
|
||||
* @param {Object} o 组件实例对象
|
||||
*/
|
||||
touchmove: async function(e, o) {
|
||||
if(!img.src) return;
|
||||
// console.log('touchmove', e, o)
|
||||
e.touches = getTouchs(e.touches);
|
||||
if(touchType === 'stretch') { // 触摸四个角进行拉伸
|
||||
var point = e.touches[0];
|
||||
var start = touches[0];
|
||||
var x = point.pageX - start.pageX;
|
||||
var y = point.pageY - start.pageY;
|
||||
if(x !== 0 || y !== 0) {
|
||||
var maxX = num(area.width * (1 - area.minScale));
|
||||
var maxY = num(area.height * (1 - area.minScale));
|
||||
// console.log(x, y, maxX, maxY, offset, area)
|
||||
touches[0] = point;
|
||||
var r = rotate / 90 % 2;
|
||||
var m = r === 1 ? num((img.height - img.width) / 2) : 0; // 宽高差值一半
|
||||
var xCompare = r === 1 ? lessThanByFault(img.height, area.width) : lessThanByFault(img.width, area.width);
|
||||
var yCompare = r === 1 ? lessThanByFault(img.width, area.height) : lessThanByFault(img.height, area.height)
|
||||
var isInclude = xCompare && yCompare;
|
||||
var isIntersect = area.checkRange && (xCompare || yCompare); // 图片是否包含在裁剪区域内
|
||||
var isReverse = !isInclude || num((offset.x - area.left) / area.width) <= num((offset.y - area.top) / area.height) || (area.width > area.height && img.width < img.height && r === 1);
|
||||
switch(activeAngle) {
|
||||
case 1: // 左上角
|
||||
x = num(x + areaOffset.left);
|
||||
y = num(y + areaOffset.top);
|
||||
if(x >= 0 && y >= 0) { // 有效滑动
|
||||
var t = num(offset.y + m - area.top);
|
||||
var l = num(offset.x - m - area.left);
|
||||
// && (offset.x + img.width < area.right || offset.y + img.height < area.bottom)
|
||||
var max = isIntersect && ((l >= 0) || (t >= 0))
|
||||
? minimum(t, l)
|
||||
: false;
|
||||
if(x > y && isReverse) { // 以x轴滑动距离为缩放基准
|
||||
maxX = validMax(maxX, max, isInclude, l, t, area.width / area.height);
|
||||
if(x > maxX) x = maxX;
|
||||
y = num(x * area.height / area.width);
|
||||
} else { // 以y轴滑动距离为缩放基准
|
||||
maxY = validMax(maxY, max, isInclude, t, l, area.height / area.width);
|
||||
if(y > maxY) y = maxY;
|
||||
x = num(y * area.width / area.height);
|
||||
}
|
||||
areaOffset.left = x;
|
||||
areaOffset.top = y;
|
||||
}
|
||||
break;
|
||||
case 2: // 右上角
|
||||
x = num(x + areaOffset.right);
|
||||
y = num(y + areaOffset.top);
|
||||
if(x <= 0 && y >= 0) { // 有效滑动
|
||||
var w = (r === 1 ? img.height : img.width);
|
||||
var t = num(offset.y + m - area.top);
|
||||
var l = num(area.right + m - offset.x - w);
|
||||
var max = isIntersect && ((t >= 0) || (l >= 0))
|
||||
? minimum(t, l)
|
||||
: false;
|
||||
if(-x > y && isReverse) { // 以x轴滑动距离为缩放基准
|
||||
maxX = validMax(maxX, max, isInclude, l, t, area.width / area.height);
|
||||
if(-x > maxX) x = -maxX;
|
||||
y = num(-x * area.height / area.width);
|
||||
} else { // 以y轴滑动距离为缩放基准
|
||||
maxY = validMax(maxY, max, isInclude, t, l, area.height / area.width);
|
||||
if(y > maxY) y = maxY;
|
||||
x = num(-y * area.width / area.height);
|
||||
}
|
||||
areaOffset.right = x;
|
||||
areaOffset.top = y;
|
||||
}
|
||||
break;
|
||||
case 3: // 左下角
|
||||
x += num(x + areaOffset.left);
|
||||
y += num(y + areaOffset.bottom);
|
||||
if(x >= 0 && y <= 0) { // 有效滑动
|
||||
var w = (r === 1 ? img.width : img.height);
|
||||
var t = num(area.bottom - m - offset.y - w);
|
||||
var l = num(offset.x - m - area.left);
|
||||
var max = isIntersect && ((l >= 0) || (t >= 0))
|
||||
? minimum(t, l)
|
||||
: false;
|
||||
if(x > -y && isReverse) { // 以x轴滑动距离为缩放基准
|
||||
maxX = validMax(maxX, max, isInclude, l, t, area.width / area.height);
|
||||
if(x > maxX) x = maxX;
|
||||
y = num(-x * area.height / area.width);
|
||||
} else { // 以y轴滑动距离为缩放基准
|
||||
maxY = validMax(maxY, max, isInclude, t, l, area.height / area.width);
|
||||
if(-y > maxY) y = -maxY;
|
||||
x = num(-y * area.width / area.height);
|
||||
}
|
||||
areaOffset.left = x;
|
||||
areaOffset.bottom = y;
|
||||
}
|
||||
break;
|
||||
case 4: // 右下角
|
||||
x = num(x + areaOffset.right);
|
||||
y = num(y + areaOffset.bottom);
|
||||
if(x <= 0 && y <= 0) { // 有效滑动
|
||||
var w = (r === 1 ? img.height : img.width);
|
||||
var h = (r === 1 ? img.width : img.height);
|
||||
var t = num(area.bottom - offset.y - h - m);
|
||||
var l = num(area.right + m - offset.x - w);
|
||||
var max = isIntersect && ((l >= 0) || (t >= 0))
|
||||
? minimum(t, l)
|
||||
: false;
|
||||
if(-x > -y && isReverse) { // 以x轴滑动距离为缩放基准
|
||||
maxX = validMax(maxX, max, isInclude, l, t, area.width / area.height);
|
||||
if(-x > maxX) x = -maxX;
|
||||
y = num(x * area.height / area.width);
|
||||
} else { // 以y轴滑动距离为缩放基准
|
||||
maxY = validMax(maxY, max, isInclude, t, l, area.height / area.width);
|
||||
if(-y > maxY) y = -maxY;
|
||||
x = num(y * area.width / area.height);
|
||||
}
|
||||
areaOffset.right = x;
|
||||
areaOffset.bottom = y;
|
||||
}
|
||||
break;
|
||||
}
|
||||
// console.log(x, y, JSON.stringify(areaOffset))
|
||||
changeAreaRect({
|
||||
instance: await this.getInstance(),
|
||||
});
|
||||
// this.draw();
|
||||
}
|
||||
} else if (e.touches.length == 2) { // 双点触摸缩放
|
||||
var start = getDistanceByTouches(touches);
|
||||
var end = getDistanceByTouches(e.touches);
|
||||
scaleImage({
|
||||
instance: await this.getInstance(),
|
||||
check: !area.bounce,
|
||||
scale: (end.c - start.c) / 100,
|
||||
x: end.x,
|
||||
y: end.y
|
||||
});
|
||||
touchType = 'scale';
|
||||
} else if(touchType === 'scale') {// 从双点触摸变成单点触摸 / 从缩放变成拖动
|
||||
touchType = 'move';
|
||||
} else {
|
||||
changeImageRect({
|
||||
instance: await this.getInstance(),
|
||||
check: !area.bounce,
|
||||
x: e.touches[0].pageX - touches[0].pageX,
|
||||
y: e.touches[0].pageY - touches[0].pageY
|
||||
});
|
||||
touchType = 'move';
|
||||
}
|
||||
touches = e.touches;
|
||||
},
|
||||
/**
|
||||
* 触摸结束
|
||||
* @param {Object} e 事件对象
|
||||
* @param {Object} o 组件实例对象
|
||||
*/
|
||||
touchend: async function(e, o) {
|
||||
if(!img.src) return;
|
||||
if(touchType === 'stretch') { // 拉伸裁剪区域的四个角缩放
|
||||
// 裁剪区域宽度被缩放到多少
|
||||
var left = areaOffset.left;
|
||||
var right = areaOffset.right;
|
||||
var top = areaOffset.top;
|
||||
var bottom = areaOffset.bottom;
|
||||
var w = area.width + right - left;
|
||||
var h = area.height + bottom - top;
|
||||
// 图像放大倍数
|
||||
var p = scale * (area.width / w) - scale;
|
||||
// 复原裁剪区域
|
||||
areaOffset = { left: 0, right: 0, top: 0, bottom: 0 };
|
||||
changeAreaRect({
|
||||
instance: await this.getInstance(),
|
||||
});
|
||||
scaleImage({
|
||||
instance: await this.getInstance(),
|
||||
scale: p,
|
||||
x: area.left + left + (1 === activeAngle || 3 === activeAngle ? w : 0),
|
||||
y: area.top + top + (1 === activeAngle || 2 === activeAngle ? h : 0)
|
||||
});
|
||||
} else if (area.bounce) { // 检查边界并矫正,实现拖动到边界时有回弹效果
|
||||
changeImageRect({
|
||||
instance: await this.getInstance(),
|
||||
check: true
|
||||
});
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 顺时针翻转图片90°
|
||||
* @param {Object} e 事件对象
|
||||
* @param {Object} o 组件实例对象
|
||||
*/
|
||||
rotateImage: async function(r) {
|
||||
rotate = (rotate + (r || 90)) % 360;
|
||||
|
||||
if(img.minScale >= 1 && area.checkRange) {
|
||||
// 因图片宽高可能不等,翻转后图片宽高需足够填满裁剪区域
|
||||
minScale = 1;
|
||||
if(img.width < area.height) {
|
||||
minScale = area.height / img.oldWidth;
|
||||
} else if(img.height < area.width) {
|
||||
minScale = area.width / img.oldHeight;
|
||||
}
|
||||
if(minScale !== 1) {
|
||||
scaleImage({
|
||||
instance: await this.getInstance(),
|
||||
scale: minScale - scale,
|
||||
x: sys.windowWidth / 2,
|
||||
y: (sys.windowHeight - sys.offsetBottom) / 2
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 由于拖动画布后会导致图片位置偏移,翻转时的旋转中心点需是图片区域+偏移区域的中心点
|
||||
// 翻转x轴中心点 = (超出裁剪区域右侧的图片宽度 - 超出裁剪区域左侧的图片宽度) / 2
|
||||
// 翻转y轴中心点 = (超出裁剪区域下方的图片宽度 - 超出裁剪区域上方的图片宽度) / 2
|
||||
var ox = ((offset.x + img.width - area.right) - (area.left - offset.x)) / 2;
|
||||
var oy = ((offset.y + img.height - area.bottom) - (area.top - offset.y)) / 2;
|
||||
changeImageRect({
|
||||
instance: await this.getInstance(),
|
||||
check: true,
|
||||
x: -ox - oy,
|
||||
y: -oy + ox
|
||||
});
|
||||
},
|
||||
rotateImage90: function() {
|
||||
this.rotateImage(90)
|
||||
},
|
||||
rotateImage270: function() {
|
||||
this.rotateImage(270)
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,743 @@
|
||||
<template>
|
||||
<view class="image-cropper" :style="{ zIndex }" @wheel="cropper.mousewheel">
|
||||
<canvas v-if="use2d" type="2d" id="imgCanvas" class="img-canvas" :style="{
|
||||
width: `${canvansWidth}px`,
|
||||
height: `${canvansHeight}px`
|
||||
}"></canvas>
|
||||
<canvas v-else id="imgCanvas" canvas-id="imgCanvas" class="img-canvas" :style="{
|
||||
width: `${canvansWidth}px`,
|
||||
height: `${canvansHeight}px`
|
||||
}"></canvas>
|
||||
<view id="pic-preview" class="pic-preview" :change:init="cropper.initObserver" :init="initData" @touchstart="cropper.touchstart" @touchmove="cropper.touchmove" @touchend="cropper.touchend">
|
||||
<image v-if="imgSrc" id="crop-image" class="crop-image" :style="cropper.imageStyles" :src="imgSrc" webp></image>
|
||||
<view v-for="(item, index) in maskList" :key="item.id" :id="item.id" class="crop-mask-block" :style="cropper.maskStylesList[index]"></view>
|
||||
<view v-if="showBorder" id="crop-border" class="crop-border" :style="cropper.borderStyles"></view>
|
||||
<view v-if="radius > 0" id="crop-circle-box" class="crop-circle-box" :style="cropper.circleBoxStyles">
|
||||
<view class="crop-circle" id="crop-circle" :style="cropper.circleStyles"></view>
|
||||
</view>
|
||||
<block v-if="showGrid">
|
||||
<view v-for="(item, index) in gridList" :key="item.id" :id="item.id" class="crop-grid" :style="cropper.gridStylesList[index]"></view>
|
||||
</block>
|
||||
<block v-if="showAngle">
|
||||
<view v-for="(item, index) in angleList" :key="item.id" :id="item.id" class="crop-angle" :style="cropper.angleStylesList[index]">
|
||||
<view :style="[{
|
||||
width: `${angleSize}px`,
|
||||
height: `${angleSize}px`
|
||||
}]"></view>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
<slot />
|
||||
<view class="fixed-bottom safe-area-inset-bottom" :style="{ zIndex: initData.area.zIndex + 99 }">
|
||||
<view v-if="(rotatable || reverseRotatable) && !!imgSrc" class="action-bar">
|
||||
<view v-if="reverseRotatable" class="rotate-icon" @click="cropper.rotateImage270"></view>
|
||||
<view v-if="rotatable" class="rotate-icon is-reverse" @click="cropper.rotateImage90"></view>
|
||||
</view>
|
||||
<view v-if="!choosable" class="choose-btn" @click="cropClick">确定</view>
|
||||
<block v-else-if="!!imgSrc">
|
||||
<view class="rechoose" @click="chooseImage">重选</view>
|
||||
<button class="button" size="mini" @click="cropClick">确定</button>
|
||||
</block>
|
||||
<view v-else class="choose-btn" @click="chooseImage">选择图片</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<!-- #ifdef APP-VUE -->
|
||||
<script module="cropper" lang="renderjs">
|
||||
import cropper from './qf-image-cropper.render.js';
|
||||
// vue3 app renderjs中条件编译无效
|
||||
cropper.setPlatform('APP');
|
||||
export default {
|
||||
mixins: [ cropper ]
|
||||
}
|
||||
</script>
|
||||
<!-- #endif -->
|
||||
<!-- #ifdef H5 -->
|
||||
<script module="cropper" lang="renderjs">
|
||||
import cropper from './qf-image-cropper.render.js';
|
||||
export default {
|
||||
mixins: [ cropper ]
|
||||
}
|
||||
</script>
|
||||
<!-- #endif -->
|
||||
<!-- #ifdef MP-WEIXIN || MP-QQ -->
|
||||
<script module="cropper" lang="wxs" src="./qf-image-cropper.wxs"></script>
|
||||
<!-- #endif -->
|
||||
<script>
|
||||
/** 裁剪区域最大宽高所占屏幕宽度百分比 */
|
||||
const AREA_SIZE = 75;
|
||||
/** 图片默认宽高 */
|
||||
const IMG_SIZE = 300;
|
||||
|
||||
export default {
|
||||
name:"qf-image-cropper",
|
||||
// #ifdef MP-WEIXIN
|
||||
options: {
|
||||
// 表示启用样式隔离,在自定义组件内外,使用 class 指定的样式将不会相互影响
|
||||
styleIsolation: "isolated"
|
||||
},
|
||||
// #endif
|
||||
props: {
|
||||
/** 图片资源地址 */
|
||||
src: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
/** 裁剪宽度,有些平台或设备对于canvas的尺寸有限制,过大可能会导致无法正常绘制 */
|
||||
width: {
|
||||
type: Number,
|
||||
default: IMG_SIZE
|
||||
},
|
||||
/** 裁剪高度,有些平台或设备对于canvas的尺寸有限制,过大可能会导致无法正常绘制 */
|
||||
height: {
|
||||
type: Number,
|
||||
default: IMG_SIZE
|
||||
},
|
||||
/** 是否绘制裁剪区域边框 */
|
||||
showBorder: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
/** 是否绘制裁剪区域网格参考线 */
|
||||
showGrid: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
/** 是否展示四个支持伸缩的角 */
|
||||
showAngle: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
/** 裁剪区域最小缩放倍数 */
|
||||
areaScale: {
|
||||
type: Number,
|
||||
default: 0.3
|
||||
},
|
||||
/** 图片最小缩放倍数 */
|
||||
minScale: {
|
||||
type: Number,
|
||||
default: 1
|
||||
},
|
||||
/** 图片最大缩放倍数 */
|
||||
maxScale: {
|
||||
type: Number,
|
||||
default: 5
|
||||
},
|
||||
/** 检查图片位置是否超出裁剪边界,如果超出则会矫正位置 */
|
||||
checkRange: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
/** 生成图片背景色:如果裁剪区域没有完全包含在图片中时,不设置该属性生成图片存在一定的透明块 */
|
||||
backgroundColor: {
|
||||
type: String
|
||||
},
|
||||
/** 是否有回弹效果:当 checkRange 为 true 时有效,拖动时可以拖出边界,释放时会弹回边界 */
|
||||
bounce: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
/** 是否支持翻转 */
|
||||
rotatable: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
/** 是否支持逆向翻转 */
|
||||
reverseRotatable: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/** 是否支持从本地选择素材 */
|
||||
choosable: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
/** 是否开启硬件加速,图片缩放过程中如果出现元素的“留影”或“重影”效果,可通过该方式解决或减轻这一问题 */
|
||||
gpu: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/** 四个角尺寸,单位px */
|
||||
angleSize: {
|
||||
type: Number,
|
||||
default: 20
|
||||
},
|
||||
/** 四个角边框宽度,单位px */
|
||||
angleBorderWidth: {
|
||||
type: Number,
|
||||
default: 2
|
||||
},
|
||||
zIndex: {
|
||||
type: [Number, String]
|
||||
},
|
||||
/** 裁剪图片圆角半径,单位px */
|
||||
radius: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
/** 生成文件的类型,只支持 'jpg' 或 'png'。默认为 'png' */
|
||||
fileType: {
|
||||
type: String,
|
||||
default: 'png'
|
||||
},
|
||||
/**
|
||||
* 图片从绘制到生成所需时间,单位ms
|
||||
* 微信小程序平台使用 `Canvas 2D` 绘制时有效
|
||||
* 如绘制大图或出现裁剪图片空白等情况应适当调大该值,因 `Canvas 2d` 采用同步绘制,需自己把控绘制完成时间
|
||||
*/
|
||||
delay: {
|
||||
type: Number,
|
||||
default: 1000
|
||||
},
|
||||
// #ifdef H5
|
||||
/**
|
||||
* 页面是否是原生标题栏
|
||||
* H5平台当 showAngle 为 true 时,使用插件的页面在 `page.json` 中配置了 "navigationStyle": "custom" 时,必须将此值设为 false ,否则四个可拉伸角的触发位置会有偏差。
|
||||
* 注:因H5平台的窗口高度是包含标题栏的,而屏幕触摸点的坐标是不包含的
|
||||
*/
|
||||
navigation: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
// #endif
|
||||
},
|
||||
emits: ["crop"],
|
||||
data() {
|
||||
return {
|
||||
// 用不同 id 使 v-for key 不重复
|
||||
maskList: [
|
||||
{ id: 'crop-mask-block-1' },
|
||||
{ id: 'crop-mask-block-2' },
|
||||
{ id: 'crop-mask-block-3' },
|
||||
{ id: 'crop-mask-block-4' },
|
||||
],
|
||||
gridList: [
|
||||
{ id: 'crop-grid-1' },
|
||||
{ id: 'crop-grid-2' },
|
||||
{ id: 'crop-grid-3' },
|
||||
{ id: 'crop-grid-4' },
|
||||
],
|
||||
angleList: [
|
||||
{ id: 'crop-angle-1' },
|
||||
{ id: 'crop-angle-2' },
|
||||
{ id: 'crop-angle-3' },
|
||||
{ id: 'crop-angle-4' },
|
||||
],
|
||||
/** 本地缓存的图片路径 */
|
||||
imgSrc: '',
|
||||
/** 图片的裁剪宽度 */
|
||||
imgWidth: IMG_SIZE,
|
||||
/** 图片的裁剪高度 */
|
||||
imgHeight: IMG_SIZE,
|
||||
/** 裁剪区域最大宽度所占屏幕宽度百分比 */
|
||||
widthPercent: AREA_SIZE,
|
||||
/** 裁剪区域最大高度所占屏幕宽度百分比 */
|
||||
heightPercent: AREA_SIZE,
|
||||
/** 裁剪区域布局信息 */
|
||||
area: {},
|
||||
/** 未被缩放过的图片宽 */
|
||||
oldWidth: 0,
|
||||
/** 未被缩放过的图片高 */
|
||||
oldHeight: 0,
|
||||
/** 系统信息 */
|
||||
sys: uni.getSystemInfoSync(),
|
||||
scaleWidth: 0,
|
||||
scaleHeight: 0,
|
||||
rotate: 0,
|
||||
offsetX: 0,
|
||||
offsetY: 0,
|
||||
use2d: false,
|
||||
canvansWidth: 0,
|
||||
canvansHeight: 0,
|
||||
// imageStyles: {},
|
||||
// maskStylesList: [{}, {}, {}, {}],
|
||||
// borderStyles: {},
|
||||
// gridStylesList: [{}, {}, {}, {}],
|
||||
// angleStylesList: [{}, {}, {}, {}],
|
||||
// circleBoxStyles: {},
|
||||
// circleStyles: {},
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
initData() {
|
||||
// console.log('initData')
|
||||
return {
|
||||
timestamp: new Date().getTime(),
|
||||
area: {
|
||||
...this.area,
|
||||
bounce: this.bounce,
|
||||
showBorder: this.showBorder,
|
||||
showGrid: this.showGrid,
|
||||
showAngle: this.showAngle,
|
||||
angleSize: this.angleSize,
|
||||
angleBorderWidth: this.angleBorderWidth,
|
||||
minScale: this.areaScale,
|
||||
widthPercent: this.widthPercent,
|
||||
heightPercent: this.heightPercent,
|
||||
radius: this.radius,
|
||||
checkRange: this.checkRange,
|
||||
zIndex: +this.zIndex || 0,
|
||||
},
|
||||
sys: this.sys,
|
||||
img: {
|
||||
minScale: this.minScale,
|
||||
maxScale: this.maxScale,
|
||||
src: this.imgSrc,
|
||||
width: this.oldWidth,
|
||||
height: this.oldHeight,
|
||||
oldWidth: this.oldWidth,
|
||||
oldHeight: this.oldHeight,
|
||||
gpu: this.gpu,
|
||||
}
|
||||
}
|
||||
},
|
||||
imgProps() {
|
||||
return {
|
||||
width: this.width,
|
||||
height: this.height,
|
||||
src: this.src,
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
imgProps: {
|
||||
handler(val, oldVal) {
|
||||
// 自定义裁剪尺,示例如下:
|
||||
this.imgWidth = Number(val.width) || IMG_SIZE;
|
||||
this.imgHeight = Number(val.height) || IMG_SIZE;
|
||||
let use2d = true;
|
||||
// #ifndef MP-WEIXIN
|
||||
use2d = false;
|
||||
// #endif
|
||||
// if(use2d && (this.imgWidth > 1365 || this.imgHeight > 1365)) {
|
||||
// use2d = false;
|
||||
// }
|
||||
let canvansWidth = this.imgWidth;
|
||||
let canvansHeight = this.imgHeight;
|
||||
let size = Math.max(canvansWidth, canvansHeight)
|
||||
let scalc = 1;
|
||||
if(size > 1365) {
|
||||
scalc = 1365 / size;
|
||||
}
|
||||
this.canvansWidth = canvansWidth * scalc;
|
||||
this.canvansHeight = canvansHeight * scalc;
|
||||
this.use2d = use2d;
|
||||
this.initArea();
|
||||
const src = val.src || this.imgSrc;
|
||||
src && this.initImage(src, oldVal === undefined);
|
||||
},
|
||||
immediate: true
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
/** 提供给wxs调用,用来接收图片变更数据 */
|
||||
dataChange(e) {
|
||||
// console.log('dataChange', e)
|
||||
this.scaleWidth = e.width;
|
||||
this.scaleHeight = e.height;
|
||||
this.rotate = e.rotate;
|
||||
this.offsetX = e.x;
|
||||
this.offsetY = e.y;
|
||||
},
|
||||
/** 初始化裁剪区域布局信息 */
|
||||
initArea() {
|
||||
// 底部操作栏高度 = 底部底部操作栏内容高度 + 设备底部安全区域高度
|
||||
this.sys.offsetBottom = uni.upx2px(100) + this.sys.safeAreaInsets.bottom;
|
||||
// #ifndef H5
|
||||
this.sys.windowTop = 0;
|
||||
this.sys.navigation = true;
|
||||
// #endif
|
||||
// #ifdef H5
|
||||
// h5平台的窗口高度是包含标题栏的
|
||||
this.sys.windowTop = this.sys.windowTop || 44;
|
||||
this.sys.navigation = this.navigation;
|
||||
// #endif
|
||||
let wp = this.widthPercent;
|
||||
let hp = this.heightPercent;
|
||||
if (this.imgWidth > this.imgHeight) {
|
||||
hp = hp * this.imgHeight / this.imgWidth;
|
||||
} else if (this.imgWidth < this.imgHeight) {
|
||||
wp = wp * this.imgWidth / this.imgHeight;
|
||||
}
|
||||
const size = this.sys.windowWidth > this.sys.windowHeight ? this.sys.windowHeight : this.sys.windowWidth;
|
||||
const width = size * wp / 100;
|
||||
const height = size * hp / 100;
|
||||
const left = (this.sys.windowWidth - width) / 2;
|
||||
const right = left + width;
|
||||
const top = (this.sys.windowHeight + this.sys.windowTop - this.sys.offsetBottom - height) / 2;
|
||||
const bottom = this.sys.windowHeight + this.sys.windowTop - this.sys.offsetBottom - top;
|
||||
this.area = { width, height, left, right, top, bottom };
|
||||
this.scaleWidth = width;
|
||||
this.scaleHeight = height;
|
||||
},
|
||||
/** 从本地选取图片 */
|
||||
chooseImage(options) {
|
||||
// #ifdef MP-WEIXIN || MP-JD
|
||||
if(uni.chooseMedia) {
|
||||
uni.chooseMedia({
|
||||
...options,
|
||||
count: 1,
|
||||
mediaType: ['image'],
|
||||
success: (res) => {
|
||||
this.resetData();
|
||||
this.initImage(res.tempFiles[0].tempFilePath);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
// #endif
|
||||
uni.chooseImage({
|
||||
...options,
|
||||
count: 1,
|
||||
success: (res) => {
|
||||
this.resetData();
|
||||
this.initImage(res.tempFiles[0].path);
|
||||
}
|
||||
});
|
||||
},
|
||||
/** 重置数据 */
|
||||
resetData() {
|
||||
this.imgSrc = '';
|
||||
this.rotate = 0;
|
||||
this.offsetX = 0;
|
||||
this.offsetY = 0;
|
||||
this.initArea();
|
||||
},
|
||||
/**
|
||||
* 初始化图片信息
|
||||
* @param {String} url 图片链接
|
||||
*/
|
||||
initImage(url, isFirst) {
|
||||
uni.getImageInfo({
|
||||
src: url,
|
||||
success: async (res) => {
|
||||
if (isFirst && this.src === url) await (new Promise((resolve) => setTimeout(resolve, 50)));
|
||||
this.imgSrc = res.path;
|
||||
let scale = res.width / res.height;
|
||||
let areaScale = this.area.width / this.area.height;
|
||||
if (scale > 1) { // 横向图片
|
||||
if (scale >= areaScale) { // 图片宽不小于目标宽,则高固定,宽自适应
|
||||
this.scaleWidth = (this.scaleHeight / res.height) * this.scaleWidth * (res.width / this.scaleWidth);
|
||||
} else { // 否则宽固定、高自适应
|
||||
this.scaleHeight = res.height * this.scaleWidth / res.width;
|
||||
}
|
||||
} else { // 纵向图片
|
||||
if (scale <= areaScale) { // 图片高不小于目标高,宽固定,高自适应
|
||||
this.scaleHeight = (this.scaleWidth / res.width) * this.scaleHeight / (this.scaleHeight / res.height);
|
||||
} else { // 否则高固定,宽自适应
|
||||
this.scaleWidth = res.width * this.scaleHeight / res.height;
|
||||
}
|
||||
}
|
||||
// 记录原始宽高,为缩放比列做限制
|
||||
this.oldWidth = +this.scaleWidth.toFixed(2);
|
||||
this.oldHeight = +this.scaleHeight.toFixed(2);
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error(err)
|
||||
}
|
||||
});
|
||||
},
|
||||
/**
|
||||
* 剪切图片圆角
|
||||
* @param {Object} ctx canvas 的绘图上下文对象
|
||||
* @param {Number} radius 圆角半径
|
||||
* @param {Number} scale 生成图片的实际尺寸与截取区域比
|
||||
* @param {Function} drawImage 执行剪切时所调用的绘图方法,入参为是否执行了剪切
|
||||
*/
|
||||
drawClipImage(ctx, radius, scale, drawImage) {
|
||||
if(radius > 0) {
|
||||
ctx.save();
|
||||
ctx.beginPath();
|
||||
const w = this.canvansWidth;
|
||||
const h = this.canvansHeight;
|
||||
if(w === h && radius >= w / 2) { // 圆形
|
||||
ctx.arc(w / 2, h / 2, w / 2, 0, 2 * Math.PI);
|
||||
} else { // 圆角矩形
|
||||
if(w !== h) { // 限制圆角半径不能超过短边的一半
|
||||
radius = Math.min(w / 2, h / 2, radius);
|
||||
// radius = Math.min(Math.max(w, h) / 2, radius);
|
||||
}
|
||||
ctx.moveTo(radius, 0);
|
||||
ctx.arcTo(w, 0, w, h, radius);
|
||||
ctx.arcTo(w, h, 0, h, radius);
|
||||
ctx.arcTo(0, h, 0, 0, radius);
|
||||
ctx.arcTo(0, 0, w, 0, radius);
|
||||
ctx.closePath();
|
||||
}
|
||||
ctx.clip();
|
||||
drawImage && drawImage(true);
|
||||
ctx.restore();
|
||||
} else {
|
||||
drawImage && drawImage(false);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 旋转图片
|
||||
* @param {Object} ctx canvas 的绘图上下文对象
|
||||
* @param {Number} rotate 旋转角度
|
||||
* @param {Number} scale 生成图片的实际尺寸与截取区域比
|
||||
*/
|
||||
drawRotateImage(ctx, rotate, scale) {
|
||||
if(rotate !== 0) {
|
||||
// 1. 以图片中心点为旋转中心点
|
||||
const x = this.scaleWidth * scale / 2;
|
||||
const y = this.scaleHeight * scale / 2;
|
||||
ctx.translate(x, y);
|
||||
// 2. 旋转画布
|
||||
ctx.rotate(rotate * Math.PI / 180);
|
||||
// 3. 旋转完画布后恢复设置旋转中心时所做的偏移
|
||||
ctx.translate(-x, -y);
|
||||
}
|
||||
},
|
||||
drawImage(ctx, image, callback) {
|
||||
// 生成图片的实际尺寸与截取区域比
|
||||
const scale = this.canvansWidth / this.area.width;
|
||||
if(this.backgroundColor) {
|
||||
if(ctx.setFillStyle) ctx.setFillStyle(this.backgroundColor);
|
||||
else ctx.fillStyle = this.backgroundColor;
|
||||
ctx.fillRect(0, 0, this.canvansWidth, this.canvansHeight);
|
||||
}
|
||||
this.drawClipImage(ctx, this.radius, scale, () => {
|
||||
this.drawRotateImage(ctx, this.rotate, scale);
|
||||
const r = this.rotate / 90;
|
||||
ctx.drawImage(
|
||||
image,
|
||||
[
|
||||
(this.offsetX - this.area.left),
|
||||
(this.offsetY - this.area.top),
|
||||
-(this.offsetX - this.area.left),
|
||||
-(this.offsetY - this.area.top)
|
||||
][r] * scale,
|
||||
[
|
||||
(this.offsetY - this.area.top),
|
||||
-(this.offsetX - this.area.left),
|
||||
-(this.offsetY - this.area.top),
|
||||
(this.offsetX - this.area.left)
|
||||
][r] * scale,
|
||||
this.scaleWidth * scale,
|
||||
this.scaleHeight * scale
|
||||
);
|
||||
});
|
||||
},
|
||||
/**
|
||||
* 绘图
|
||||
* @param {Object} canvas
|
||||
* @param {Object} ctx canvas 的绘图上下文对象
|
||||
* @param {String} src 图片路径
|
||||
* @param {Function} callback 开始绘制时回调
|
||||
*/
|
||||
draw2DImage(canvas, ctx, src, callback) {
|
||||
// console.log('draw2DImage', canvas, ctx, src, callback)
|
||||
if(canvas) {
|
||||
const image = canvas.createImage();
|
||||
image.onload = () => {
|
||||
this.drawImage(ctx, image);
|
||||
// 如果觉得`生成时间过长`或`出现生成图片空白`可尝试调整延迟时间
|
||||
callback && setTimeout(callback, this.delay);
|
||||
};
|
||||
image.onerror = (err) => {
|
||||
console.error(err)
|
||||
uni.hideLoading();
|
||||
};
|
||||
image.src = src;
|
||||
} else {
|
||||
this.drawImage(ctx, src);
|
||||
setTimeout(() => {
|
||||
ctx.draw(false, callback);
|
||||
}, 200);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 画布转图片到本地缓存
|
||||
* @param {Object} canvas
|
||||
* @param {String} canvasId
|
||||
*/
|
||||
canvasToTempFilePath(canvas, canvasId) {
|
||||
// console.log('canvasToTempFilePath', canvas, canvasId)
|
||||
uni.canvasToTempFilePath({
|
||||
canvas,
|
||||
canvasId,
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: this.canvansWidth,
|
||||
height: this.canvansHeight,
|
||||
destWidth: this.imgWidth, // 必要,保证生成图片宽度不受设备分辨率影响
|
||||
destHeight: this.imgHeight, // 必要,保证生成图片高度不受设备分辨率影响
|
||||
fileType: this.fileType, // 目标文件的类型,默认png
|
||||
success: (res) => {
|
||||
// 生成的图片临时文件路径
|
||||
this.handleImage(res.tempFilePath);
|
||||
},
|
||||
fail: (err) => {
|
||||
uni.hideLoading();
|
||||
uni.showToast({ title: '裁剪失败,生成图片异常!', icon: 'none' });
|
||||
}
|
||||
}, this);
|
||||
},
|
||||
/** 确认裁剪 */
|
||||
cropClick() {
|
||||
uni.showLoading({ title: '裁剪中...', mask: true });
|
||||
if(!this.use2d) {
|
||||
const ctx = uni.createCanvasContext('imgCanvas', this);
|
||||
ctx.clearRect(0, 0, this.canvansWidth, this.canvansHeight);
|
||||
this.draw2DImage(null, ctx, this.imgSrc, () => {
|
||||
this.canvasToTempFilePath(null, 'imgCanvas');
|
||||
});
|
||||
return;
|
||||
}
|
||||
// #ifdef MP-WEIXIN
|
||||
const query = uni.createSelectorQuery().in(this);
|
||||
query.select('#imgCanvas')
|
||||
.fields({ node: true, size: true })
|
||||
.exec((res) => {
|
||||
const canvas = res[0].node;
|
||||
|
||||
const dpr = uni.getSystemInfoSync().pixelRatio;
|
||||
canvas.width = res[0].width * dpr;
|
||||
canvas.height = res[0].height * dpr;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.scale(dpr, dpr);
|
||||
ctx.clearRect(0, 0, this.canvansWidth, this.canvansHeight);
|
||||
|
||||
this.draw2DImage(canvas, ctx, this.imgSrc, () => {
|
||||
this.canvasToTempFilePath(canvas);
|
||||
});
|
||||
});
|
||||
// #endif
|
||||
},
|
||||
handleImage(tempFilePath){
|
||||
// 在H5平台下,tempFilePath 为 base64
|
||||
// console.log(tempFilePath)
|
||||
uni.hideLoading();
|
||||
this.$emit('crop', { tempFilePath });
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.image-cropper {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: #000;
|
||||
.img-canvas {
|
||||
position: absolute !important;
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
.pic-preview {
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
position: relative;
|
||||
|
||||
.crop-mask-block {
|
||||
background-color: rgba(51, 51, 51, 0.8);
|
||||
z-index: 2;
|
||||
position: fixed;
|
||||
box-sizing: border-box;
|
||||
pointer-events: none;
|
||||
}
|
||||
.crop-circle-box {
|
||||
position: fixed;
|
||||
box-sizing: border-box;
|
||||
z-index: 2;
|
||||
pointer-events: none;
|
||||
overflow: hidden;
|
||||
.crop-circle {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
.crop-image {
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
border-radius: 0 !important;
|
||||
display: block !important;
|
||||
backface-visibility: hidden;
|
||||
}
|
||||
.crop-border {
|
||||
position: fixed;
|
||||
border: 1px solid #fff;
|
||||
box-sizing: border-box;
|
||||
z-index: 3;
|
||||
pointer-events: none;
|
||||
}
|
||||
.crop-grid {
|
||||
position: fixed;
|
||||
z-index: 3;
|
||||
border-style: dashed;
|
||||
border-color: #fff;
|
||||
pointer-events: none;
|
||||
opacity: 0.5;
|
||||
}
|
||||
.crop-angle {
|
||||
position: fixed;
|
||||
z-index: 3;
|
||||
border-style: solid;
|
||||
border-color: #fff;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.fixed-bottom {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 99;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
background-color: $uni-bg-color-grey;
|
||||
|
||||
.action-bar {
|
||||
position: absolute;
|
||||
top: -90rpx;
|
||||
left: 10rpx;
|
||||
display: flex;
|
||||
.rotate-icon {
|
||||
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAAAXNSR0IArs4c6QAABCFJREFUaEPtml3IpVMUx3//ko/ChTIyiGFSMyhllI8bc4F85yuNC2FCqLmQC1+FZORiEkUMNW7UjKjJULgxV+NzSkxDhEkZgwsyigv119J63p7zvOc8z37OmXdOb51dz82711r7/99r7bXXXucVi3xokeNnRqCvB20fDmwAlgK/5bcD+FTSr33tHXQP2H4MeHQE0A+B5yRtLiUyDQJrgVc6AAaBpyV93kXkoBMIQLbfBS5NcK8BRwDXNcD+AdwnaVMbiWkRCPBBohpxHuK7M7865sclRdgNHVMhkF6IMIpwirFEUhzo8M7lwIvASTXEqyVtH8ZgagQSbOzsDknv18HZXpHn5IL8+94IOUm7miSmSqAttjPdbgGuTrnNktYsGgLpoYuAD2qg1zRTbG8P2D4SOC6/Q7vSHPALsE/S7wWy80RsPw/ckxMfSTq/LtRJwPbxwF3ASiCUTxwHCPAnEBfVF8AWSTtL7Ng+LfWOTfmlkn6udFsJ5K15R6a4kvX6yGyUFBvTOWzHXXFzCt4g6c1OArYj9iIGh43YgR+BvztXh1PSa4cMkd0jaVmXDduPAE+k3HpJD7cSGFKvfAc8FQUX8IOk/V2L1udtB/hTgdOBW4Aba/M7Ja1qs2f7euCNlHlZUlx4/495IWQ7Jl+qGbxX0gt9AHfJ2o6zFBVoNVrDKe+F3Sm8VdK1bQQ+A85JgXckXdkFaJx527cC9TpnVdvBtl3h2iapuhsGPdBw1b9xnUvaNw7AEh3bnwDnpuwGSfeP0rN9NvAMELXRXFkxEEK2nwQeSiOtRVQJwC4Z29cAW1Nuu6TVXTrN+SaBt4ErUug2Sa/2NdhH3vZy4NvU2S/p6D768w5xI3WOrAD7LtISFpGdIhVXKfaYvjd20wP13L9M0p4DBbaFRKToSLExVkr6qs+aIwlI6iwz+izUQqC+ab29PiMwqRcmPXczD8w8MFj1zg7xXEqbpdHCw7FgWSjafZL+KcQxtpjteCeflwYulFR/J3TabSslVkj6utPChAK2f6q9uZdLitKieLQRuExSvX9ZbLRUMFs09efpUZL+KtUfVo1GW/umNHC3pOhRLtiwfSbwZS6wV9IJfRdreuBBYH0a2STp9r4G+8jbXgc8mzoDT8VSO00ClwDv1ZR7XyylC4ec7ejaLUmdsV6Aw7oSbwFXpdFdks7qA6pU1na0aR6owgeIR/1cx63UzjAC0YXYVjMQHlkn6ZtSo21ytuPZGKFagQ/xsXZ/3iGuFrYdjafXG0DiQMeBi47c9/GV3BO247UV38n5o0UAP6xmu7jFOGxjRr66On5NPBDOCBsDTapxjHY1dyOcolNXnYlx1himE53p2PmNkxosevfavhg4Izt2k7TXPwZ2S6p6QZPin/2rwcQ7OKmBohCadJGF1P8PG6aaQBKVX/8AAAAASUVORK5CYII=');
|
||||
background-size: 60% 60%;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
&.is-reverse {
|
||||
transform: rotateY(180deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.rechoose {
|
||||
color: $uni-color-primary;
|
||||
padding: 0 $uni-spacing-row-lg;
|
||||
line-height: 100rpx;
|
||||
}
|
||||
|
||||
.choose-btn {
|
||||
color: $uni-color-primary;
|
||||
text-align: center;
|
||||
line-height: 100rpx;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.button {
|
||||
margin: auto $uni-spacing-row-lg auto auto;
|
||||
background-color: $uni-color-primary;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.safe-area-inset-bottom {
|
||||
padding-bottom: 0;
|
||||
padding-bottom: constant(safe-area-inset-bottom); // 兼容 IOS<11.2
|
||||
padding-bottom: env(safe-area-inset-bottom); // 兼容 IOS>=11.2
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,727 @@
|
||||
/**
|
||||
* 图片编辑器-手势监听
|
||||
* 1. wxs 暂不支持 es6 语法
|
||||
* 2. 支持编译到微信小程序、QQ小程序、app-vue、H5上(uni-app 2.2.5及以上版本)
|
||||
*/
|
||||
/** 图片偏移量 */
|
||||
var offset = { x: 0, y: 0 };
|
||||
/** 图片缩放比例 */
|
||||
var scale = 1;
|
||||
/** 图片最小缩放比例 */
|
||||
var minScale = 1;
|
||||
/** 图片旋转角度 */
|
||||
var rotate = 0;
|
||||
/** 触摸点 */
|
||||
var touches = [];
|
||||
/** 图片布局信息 */
|
||||
var img = {};
|
||||
/** 系统信息 */
|
||||
var sys = {};
|
||||
/** 裁剪区域布局信息 */
|
||||
var area = {};
|
||||
/** 触摸行为类型 */
|
||||
var touchType = '';
|
||||
/** 操作角的位置 */
|
||||
var activeAngle = 0;
|
||||
/** 裁剪区域布局信息偏移量 */
|
||||
var areaOffset = { left: 0, right: 0, top: 0, bottom: 0 };
|
||||
/** 容错值 */
|
||||
var fault = 0.000001;
|
||||
/**
|
||||
* 获取a、b两数中的最小正数
|
||||
* @param a
|
||||
* @param b
|
||||
*/
|
||||
function minimum(a, b) {
|
||||
if (a > 0 && b < 0) return a;
|
||||
if (a < 0 && b > 0) return b;
|
||||
if (a > 0 && b > 0) return Math.min(a, b);
|
||||
return 0;
|
||||
}
|
||||
/**
|
||||
* 在容错访问内获取n近似值
|
||||
* @param n
|
||||
*/
|
||||
function num(n) {
|
||||
var m = parseFloat((n).toFixed(6));
|
||||
return m === fault || m === -fault ? 0 : m;
|
||||
}
|
||||
/**
|
||||
* 比较a值在容错值范围内是否等于b值
|
||||
* @param a
|
||||
* @param b
|
||||
*/
|
||||
function equalsByFault(a, b) {
|
||||
return Math.abs(a - b) <= fault;
|
||||
}
|
||||
/**
|
||||
* 比较a值在容错值范围内是否小于b值
|
||||
* @param a
|
||||
* @param b
|
||||
*/
|
||||
function lessThanByFault(a, b) {
|
||||
var c = a - b;
|
||||
return c < 0 ? c < -fault : c < fault;
|
||||
}
|
||||
/**
|
||||
* 验证并获取有效最大值
|
||||
* @param v
|
||||
* @param max
|
||||
* @param isInclude
|
||||
* @param x
|
||||
* @param y
|
||||
* @param rate
|
||||
* @returns
|
||||
*/
|
||||
function validMax(v, max, isInclude, x, y, rate) {
|
||||
if(typeof max === 'number') {
|
||||
if(isInclude && equalsByFault(max, y)) { // 宽高不等时,x轴用y轴值要做等比例转换
|
||||
var n = num(max * rate);
|
||||
if (n <= x) return n; // 转化后值在x轴最大值范围内
|
||||
return x; // 转化后值超出x轴最大值范围则用最大值
|
||||
}
|
||||
return max;
|
||||
}
|
||||
return v;
|
||||
}
|
||||
/**
|
||||
* 计算两点间距
|
||||
* @param {Object} touches 触摸点信息
|
||||
*/
|
||||
function getDistanceByTouches(touches) {
|
||||
// 根据勾股定理求两点间距离
|
||||
var a = touches[1].pageX - touches[0].pageX;
|
||||
var b = touches[1].pageY - touches[0].pageY;
|
||||
var c = Math.sqrt(Math.pow(a, 2) + Math.pow(b, 2));
|
||||
// 求两点间的中点坐标
|
||||
// 1. a、b可能为负值
|
||||
// 2. 在求a、b时,如用touches[1]减touches[0],则求中点坐标也得用touches[1]减a/2、b/2
|
||||
// 3. 同理,在求a、b时,也可用touches[0]减touches[1],则求中点坐标也得用touches[0]减a/2、b/2
|
||||
var x = touches[1].pageX - a / 2;
|
||||
var y = touches[1].pageY - b / 2;
|
||||
return { c, x, y };
|
||||
};
|
||||
/**
|
||||
* 修正取值
|
||||
* @param {Object} a
|
||||
* @param {Object} b
|
||||
* @param {Object} c
|
||||
* @param {Object} reverse 是否反向
|
||||
*/
|
||||
function correctValue(a, b, c, reverse) {
|
||||
return num(reverse ? Math.max(Math.min(a, b), c) : Math.min(Math.max(a, b), c));
|
||||
}
|
||||
|
||||
/**
|
||||
* 旋转90°或270°时检查边界:限制 x、y 拖动范围,禁止滑出边界
|
||||
* @param {Object} e 点坐标
|
||||
* @param {Object} xReverse x是否反向
|
||||
* @param {Object} yReverse y是否反向
|
||||
*/
|
||||
function checkRotateRange(e, xReverse, yReverse) {
|
||||
var o = num((img.height - img.width) / 2); // 宽高差值一半
|
||||
return {
|
||||
x: correctValue(e.x, -img.height + o + area.width + area.left, area.left + o, xReverse),
|
||||
y: correctValue(e.y, -img.width - o + area.height + area.top, area.top - o, yReverse)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查边界:限制 x、y 拖动范围,禁止滑出边界
|
||||
* @param {Object} e 点坐标
|
||||
*/
|
||||
function checkRange(e) {
|
||||
var r = rotate / 90 % 2;
|
||||
if(r === 1) { // 因图片宽高可能不等,翻转 90° 或 270° 后图片宽高需反着计算,且左右和上下边界要根据差值做偏移
|
||||
if (area.width === area.height) {
|
||||
return checkRotateRange(e, img.height < area.height, img.width < area.width);
|
||||
}
|
||||
var isInclude = img.height < area.width && img.width < area.height; // 图片是否包含在裁剪区域内
|
||||
if (img.width < area.height || img.height < area.width) {
|
||||
if (area.width < area.height && img.width < img.height) {
|
||||
return isInclude
|
||||
? checkRotateRange(e, area.width < area.height, area.width < area.height)
|
||||
: checkRotateRange(e, false, true);
|
||||
}
|
||||
if (area.height < area.width && img.height < img.width) {
|
||||
return isInclude
|
||||
? checkRotateRange(e, area.height < area.width, area.height < area.width)
|
||||
: checkRotateRange(e, true, false);
|
||||
}
|
||||
}
|
||||
if (img.height >= area.width && img.width >= area.height) {
|
||||
return checkRotateRange(e, false, false);
|
||||
}
|
||||
if (isInclude) {
|
||||
return area.height < area.width
|
||||
? checkRotateRange(e, true, true)
|
||||
: checkRotateRange(e, area.width < area.height, area.width < area.height);
|
||||
}
|
||||
if (img.height < area.width && !img.width < area.height) {
|
||||
return checkRotateRange(e, true, false);
|
||||
}
|
||||
if (!img.height < area.width && img.width < area.height) {
|
||||
return checkRotateRange(e, false, true);
|
||||
}
|
||||
return checkRotateRange(e, img.height < area.height, img.width < area.width);
|
||||
}
|
||||
return {
|
||||
x: correctValue(e.x, -img.width + area.width + area.left, area.left, img.width < area.width),
|
||||
y: correctValue(e.y, -img.height + area.height + area.top, area.top, img.height < area.height)
|
||||
};
|
||||
};
|
||||
/**
|
||||
* 变更图片布局信息
|
||||
* @param {Object} e 布局信息
|
||||
*/
|
||||
function changeImageRect(e) {
|
||||
offset.x += e.x || 0;
|
||||
offset.y += e.y || 0;
|
||||
var image = e.instance.selectComponent('.crop-image');
|
||||
if(e.check && area.checkRange) { // 检查边界
|
||||
var point = checkRange(offset);
|
||||
if(offset.x !== point.x || offset.y !== point.y) {
|
||||
offset = point;
|
||||
}
|
||||
}
|
||||
// image.setStyle({
|
||||
// width: img.width + 'px',
|
||||
// height: img.height + 'px',
|
||||
// transform: 'translate(' + offset.x + 'px, ' + offset.y + 'px) rotate(' + rotate +'deg)'
|
||||
// });
|
||||
var ox = (img.width - img.oldWidth) / 2;
|
||||
var oy = (img.height - img.oldHeight) / 2;
|
||||
image.setStyle({
|
||||
width: img.oldWidth + 'px',
|
||||
height: img.oldHeight + 'px',
|
||||
transform: (img.gpu ? 'translateZ(0) ' : '') + 'translate(' + (offset.x + ox) + 'px, ' + (offset.y + oy) + 'px) rotate(' + rotate +'deg) scale(' + scale + ')'
|
||||
});
|
||||
|
||||
e.instance.callMethod('dataChange', {
|
||||
width: img.width,
|
||||
height: img.height,
|
||||
x: offset.x,
|
||||
y: offset.y,
|
||||
rotate: rotate
|
||||
});
|
||||
};
|
||||
/**
|
||||
* 变更裁剪区域布局信息
|
||||
* @param {Object} e 布局信息
|
||||
*/
|
||||
function changeAreaRect(e) {
|
||||
// 变更蒙版样式
|
||||
var masks = e.instance.selectAllComponents('.crop-mask-block');
|
||||
var maskStyles = [
|
||||
{
|
||||
left: 0,
|
||||
width: (area.left + areaOffset.left) + 'px',
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
'z-index': area.zIndex + 2
|
||||
},
|
||||
{
|
||||
left: (area.right + areaOffset.right) + 'px',
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
'z-index': area.zIndex + 2
|
||||
},
|
||||
{
|
||||
left: (area.left + areaOffset.left) + 'px',
|
||||
width: (area.width + areaOffset.right - areaOffset.left) + 'px',
|
||||
top: 0,
|
||||
height: (area.top + areaOffset.top) + 'px',
|
||||
'z-index': area.zIndex + 2
|
||||
},
|
||||
{
|
||||
left: (area.left + areaOffset.left) + 'px',
|
||||
width: (area.width + areaOffset.right - areaOffset.left) + 'px',
|
||||
top: (area.bottom + areaOffset.bottom) + 'px',
|
||||
// height: (area.top - areaOffset.bottom + sys.offsetBottom) + 'px',
|
||||
bottom: 0,
|
||||
'z-index': area.zIndex + 2
|
||||
}
|
||||
];
|
||||
var len = masks.length;
|
||||
for (var i = 0; i < len; i++) {
|
||||
masks[i].setStyle(maskStyles[i]);
|
||||
}
|
||||
|
||||
// 变更边框样式
|
||||
if(area.showBorder) {
|
||||
var border = e.instance.selectComponent('.crop-border');
|
||||
border.setStyle({
|
||||
left: (area.left + areaOffset.left) + 'px',
|
||||
top: (area.top + areaOffset.top) + 'px',
|
||||
width: (area.width + areaOffset.right - areaOffset.left) + 'px',
|
||||
height: (area.height + areaOffset.bottom - areaOffset.top) + 'px',
|
||||
'z-index': area.zIndex + 3
|
||||
});
|
||||
}
|
||||
|
||||
// 变更参考线样式
|
||||
if(area.showGrid) {
|
||||
var grids = e.instance.selectAllComponents('.crop-grid');
|
||||
var gridStyles = [
|
||||
{
|
||||
'border-width': '1px 0 0 0',
|
||||
left: (area.left + areaOffset.left) + 'px',
|
||||
right: (area.right + areaOffset.right) + 'px',
|
||||
top: (area.top + areaOffset.top + (area.height + areaOffset.bottom - areaOffset.top) / 3 - 0.5) + 'px',
|
||||
width: (area.width + areaOffset.right - areaOffset.left) + 'px',
|
||||
'z-index': area.zIndex + 3
|
||||
},
|
||||
{
|
||||
'border-width': '1px 0 0 0',
|
||||
left: (area.left + areaOffset.left) + 'px',
|
||||
right: (area.right + areaOffset.right) + 'px',
|
||||
top: (area.top + areaOffset.top + (area.height + areaOffset.bottom - areaOffset.top) * 2 / 3 - 0.5) + 'px',
|
||||
width: (area.width + areaOffset.right - areaOffset.left) + 'px',
|
||||
'z-index': area.zIndex + 3
|
||||
},
|
||||
{
|
||||
'border-width': '0 1px 0 0',
|
||||
top: (area.top + areaOffset.top) + 'px',
|
||||
bottom: (area.bottom + areaOffset.bottom) + 'px',
|
||||
left: (area.left + areaOffset.left + (area.width + areaOffset.right - areaOffset.left) / 3 - 0.5) + 'px',
|
||||
height: (area.height + areaOffset.bottom - areaOffset.top) + 'px',
|
||||
'z-index': area.zIndex + 3
|
||||
},
|
||||
{
|
||||
'border-width': '0 1px 0 0',
|
||||
top: (area.top + areaOffset.top) + 'px',
|
||||
bottom: (area.bottom + areaOffset.bottom) + 'px',
|
||||
left: (area.left + areaOffset.left + (area.width + areaOffset.right - areaOffset.left) * 2 / 3 - 0.5) + 'px',
|
||||
height: (area.height + areaOffset.bottom - areaOffset.top) + 'px',
|
||||
'z-index': area.zIndex + 3
|
||||
}
|
||||
];
|
||||
var len = grids.length;
|
||||
for (var i = 0; i < len; i++) {
|
||||
grids[i].setStyle(gridStyles[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// 变更四个伸缩角样式
|
||||
if(area.showAngle) {
|
||||
var angles = e.instance.selectAllComponents('.crop-angle');
|
||||
var angleStyles = [
|
||||
{
|
||||
'border-width': area.angleBorderWidth + 'px 0 0 ' + area.angleBorderWidth + 'px',
|
||||
left: (area.left + areaOffset.left - area.angleBorderWidth) + 'px',
|
||||
top: (area.top + areaOffset.top - area.angleBorderWidth) + 'px',
|
||||
'z-index': area.zIndex + 3
|
||||
},
|
||||
{
|
||||
'border-width': area.angleBorderWidth + 'px ' + area.angleBorderWidth + 'px 0 0',
|
||||
left: (area.right + areaOffset.right - area.angleSize) + 'px',
|
||||
top: (area.top + areaOffset.top - area.angleBorderWidth) + 'px',
|
||||
'z-index': area.zIndex + 3
|
||||
},
|
||||
{
|
||||
'border-width': '0 0 ' + area.angleBorderWidth + 'px ' + area.angleBorderWidth + 'px',
|
||||
left: (area.left + areaOffset.left - area.angleBorderWidth) + 'px',
|
||||
top: (area.bottom + areaOffset.bottom - area.angleSize) + 'px',
|
||||
'z-index': area.zIndex + 3
|
||||
},
|
||||
{
|
||||
'border-width': '0 ' + area.angleBorderWidth + 'px ' + area.angleBorderWidth + 'px 0',
|
||||
left: (area.right + areaOffset.right - area.angleSize) + 'px',
|
||||
top: (area.bottom + areaOffset.bottom - area.angleSize) + 'px',
|
||||
'z-index': area.zIndex + 3
|
||||
}
|
||||
];
|
||||
var len = angles.length;
|
||||
for (var i = 0; i < len; i++) {
|
||||
angles[i].setStyle(angleStyles[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// 变更圆角样式
|
||||
if(area.radius > 0) {
|
||||
var circleBox = e.instance.selectComponent('.crop-circle-box');
|
||||
var circle = e.instance.selectComponent('.crop-circle');
|
||||
var radius = area.radius;
|
||||
if(area.width === area.height && area.radius >= area.width / 2) { // 圆形
|
||||
radius = (area.width / 2);
|
||||
} else { // 圆角矩形
|
||||
if(area.width !== area.height) { // 限制圆角半径不能超过短边的一半
|
||||
radius = Math.min(area.width / 2, area.height / 2, radius);
|
||||
}
|
||||
}
|
||||
circleBox.setStyle({
|
||||
left: (area.left + areaOffset.left) + 'px',
|
||||
top: (area.top + areaOffset.top) + 'px',
|
||||
width: (area.width + areaOffset.right - areaOffset.left) + 'px',
|
||||
height: (area.height + areaOffset.bottom - areaOffset.top) + 'px',
|
||||
'z-index': area.zIndex + 2
|
||||
});
|
||||
circle.setStyle({
|
||||
'box-shadow': '0 0 0 ' + Math.max(area.width, area.height) + 'px rgba(51, 51, 51, 0.8)',
|
||||
'border-radius': radius + 'px'
|
||||
});
|
||||
}
|
||||
};
|
||||
/**
|
||||
* 缩放图片
|
||||
* @param {Object} e 布局信息
|
||||
*/
|
||||
function scaleImage(e) {
|
||||
var last = scale;
|
||||
scale = Math.min(Math.max(e.scale + scale, minScale), img.maxScale);
|
||||
if(last !== scale) {
|
||||
img.width = num(img.oldWidth * scale);
|
||||
img.height = num(img.oldHeight * scale);
|
||||
// 参考问题:有一个长4000px、宽4000px的四方形ABCD,A点的坐标固定在(-2000,-2000),
|
||||
// 该四边形上有一个点E,坐标为(-100,-300),将该四方形复制一份并缩小到90%后,
|
||||
// 新四边形的A点坐标为多少时可使新四边形的E点与原四边形的E点重合?
|
||||
// 预期效果:从图中选取某点(参照物)为中心点进行缩放,缩放时无论图像怎么变化,该点位置始终固定不变
|
||||
// 计算方法:以相同起点先计算缩放前后两点间的距离,再加上原图像偏移量即可
|
||||
e.x = num((e.x - offset.x) * (1 - scale / last));
|
||||
e.y = num((e.y - offset.y) * (1 - scale / last));
|
||||
changeImageRect(e);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
/**
|
||||
* 获取触摸点在哪个角
|
||||
* @param {number} x 触摸点x轴坐标
|
||||
* @param {number} y 触摸点y轴坐标
|
||||
* @return {number} 角的位置:0=无;1=左上;2=右上;3=左下;4=右下;
|
||||
*/
|
||||
function getToucheAngle(x, y) {
|
||||
// console.log('getToucheAngle', x, y, JSON.stringify(area))
|
||||
var o = area.angleBorderWidth; // 需扩大触发范围则把 o 值加大即可
|
||||
if(y >= area.top - o && y <= area.top + area.angleSize + o) {
|
||||
if(x >= area.left - o && x <= area.left + area.angleSize + o) {
|
||||
return 1; // 左上角
|
||||
} else if(x >= area.right - area.angleSize - o && x <= area.right + o) {
|
||||
return 2; // 右上角
|
||||
}
|
||||
} else if(y >= area.bottom - area.angleSize - o && y <= area.bottom + o) {
|
||||
if(x >= area.left - o && x <= area.left + area.angleSize + o) {
|
||||
return 3; // 左下角
|
||||
} else if(x >= area.right - area.angleSize - o && x <= area.right + o) {
|
||||
return 4; // 右下角
|
||||
}
|
||||
}
|
||||
return 0; // 无触摸到角
|
||||
};
|
||||
/**
|
||||
* 重置数据
|
||||
*/
|
||||
function resetData() {
|
||||
offset = { x: 0, y: 0 };
|
||||
scale = 1;
|
||||
minScale = img.minScale;
|
||||
rotate = 0;
|
||||
};
|
||||
/**
|
||||
* 顺时针翻转图片90°
|
||||
* @param {Object} e 事件对象
|
||||
* @param {Object} o 组件实例对象
|
||||
*/
|
||||
function rotateImage(e, o, r) {
|
||||
rotate = (rotate + r) % 360;
|
||||
if(img.minScale >= 1 && area.checkRange) {
|
||||
// 因图片宽高可能不等,翻转后图片宽高需足够填满裁剪区域
|
||||
minScale = 1;
|
||||
if(img.width < area.height) {
|
||||
minScale = area.height / img.oldWidth;
|
||||
} else if(img.height < area.width) {
|
||||
minScale = area.width / img.oldHeight;
|
||||
}
|
||||
if(minScale !== 1) {
|
||||
scaleImage({
|
||||
instance: o,
|
||||
scale: minScale - scale,
|
||||
x: sys.windowWidth / 2,
|
||||
y: (sys.windowHeight - sys.offsetBottom) / 2
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 由于拖动画布后会导致图片位置偏移,翻转时的旋转中心点需是图片区域+偏移区域的中心点
|
||||
// 翻转x轴中心点 = (超出裁剪区域右侧的图片宽度 - 超出裁剪区域左侧的图片宽度) / 2
|
||||
// 翻转y轴中心点 = (超出裁剪区域下方的图片宽度 - 超出裁剪区域上方的图片宽度) / 2
|
||||
var ox = ((offset.x + img.width - area.right) - (area.left - offset.x)) / 2;
|
||||
var oy = ((offset.y + img.height - area.bottom) - (area.top - offset.y)) / 2;
|
||||
changeImageRect({
|
||||
instance: o,
|
||||
check: true,
|
||||
x: -ox - oy,
|
||||
y: -oy + ox
|
||||
});
|
||||
};
|
||||
module.exports = {
|
||||
/**
|
||||
* 初始化:观察数据变更
|
||||
* @param {Object} newVal 新数据
|
||||
* @param {Object} oldVal 旧数据
|
||||
* @param {Object} o 组件实例对象
|
||||
*/
|
||||
initObserver: function(newVal, oldVal, o, i) {
|
||||
if(newVal) {
|
||||
img = newVal.img;
|
||||
sys = newVal.sys;
|
||||
area = newVal.area;
|
||||
minScale = img.minScale;
|
||||
resetData();
|
||||
img.src && changeImageRect({
|
||||
instance: o,
|
||||
x: (sys.windowWidth - img.width) / 2,
|
||||
y: (sys.windowHeight - sys.offsetBottom - img.height) / 2
|
||||
});
|
||||
changeAreaRect({
|
||||
instance: o
|
||||
});
|
||||
// console.log('initRect', JSON.stringify(newVal))
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 鼠标滚轮滚动
|
||||
* @param {Object} e 事件对象
|
||||
* @param {Object} o 组件实例对象
|
||||
*/
|
||||
mousewheel: function(e, o) {
|
||||
if(!img.src) return;
|
||||
scaleImage({
|
||||
instance: o,
|
||||
check: true,
|
||||
// 鼠标向上滚动时,deltaY 固定 -100,鼠标向下滚动时,deltaY 固定 100
|
||||
scale: e.detail.deltaY > 0 ? -0.05 : 0.05,
|
||||
x: e.touches[0].pageX,
|
||||
y: e.touches[0].pageY
|
||||
});
|
||||
},
|
||||
/**
|
||||
* 触摸开始
|
||||
* @param {Object} e 事件对象
|
||||
* @param {Object} o 组件实例对象
|
||||
*/
|
||||
touchstart: function(e, o) {
|
||||
if(!img.src) return;
|
||||
touches = e.touches;
|
||||
activeAngle = area.showAngle ? getToucheAngle(touches[0].pageX, touches[0].pageY) : 0;
|
||||
if(touches.length === 1 && activeAngle !== 0) {
|
||||
touchType = 'stretch'; // 伸缩裁剪区域
|
||||
} else {
|
||||
touchType = '';
|
||||
}
|
||||
// console.log('touchstart', JSON.stringify(e), activeAngle)
|
||||
},
|
||||
/**
|
||||
* 触摸移动
|
||||
* @param {Object} e 事件对象
|
||||
* @param {Object} o 组件实例对象
|
||||
*/
|
||||
touchmove: function(e, o) {
|
||||
if(!img.src) return;
|
||||
// console.log('touchmove', JSON.stringify(e), JSON.stringify(o))
|
||||
if(touchType === 'stretch') { // 触摸四个角进行拉伸
|
||||
var point = e.touches[0];
|
||||
var start = touches[0];
|
||||
var x = point.pageX - start.pageX;
|
||||
var y = point.pageY - start.pageY;
|
||||
if(x !== 0 || y !== 0) {
|
||||
var maxX = num(area.width * (1 - area.minScale));
|
||||
var maxY = num(area.height * (1 - area.minScale));
|
||||
// console.log(x, y, maxX, maxY, offset, area)
|
||||
touches[0] = point;
|
||||
var r = rotate / 90 % 2;
|
||||
var m = r === 1 ? num((img.height - img.width) / 2) : 0; // 宽高差值一半
|
||||
var xCompare = r === 1 ? lessThanByFault(img.height, area.width) : lessThanByFault(img.width, area.width);
|
||||
var yCompare = r === 1 ? lessThanByFault(img.width, area.height) : lessThanByFault(img.height, area.height)
|
||||
var isInclude = xCompare && yCompare;
|
||||
var isIntersect = area.checkRange && (xCompare || yCompare); // 图片是否包含在裁剪区域内
|
||||
var isReverse = !isInclude || num((offset.x - area.left) / area.width) <= num((offset.y - area.top) / area.height) || (area.width > area.height && img.width < img.height && r === 1);
|
||||
switch(activeAngle) {
|
||||
case 1: // 左上角
|
||||
x = num(x + areaOffset.left);
|
||||
y = num(y + areaOffset.top);
|
||||
if(x >= 0 && y >= 0) { // 有效滑动
|
||||
var t = num(offset.y + m - area.top);
|
||||
var l = num(offset.x - m - area.left);
|
||||
// && (offset.x + img.width < area.right || offset.y + img.height < area.bottom)
|
||||
var max = isIntersect && ((l >= 0) || (t >= 0))
|
||||
? minimum(t, l)
|
||||
: false;
|
||||
if(x > y && isReverse) { // 以x轴滑动距离为缩放基准
|
||||
maxX = validMax(maxX, max, isInclude, l, t, area.width / area.height);
|
||||
if(x > maxX) x = maxX;
|
||||
y = num(x * area.height / area.width);
|
||||
} else { // 以y轴滑动距离为缩放基准
|
||||
maxY = validMax(maxY, max, isInclude, t, l, area.height / area.width);
|
||||
if(y > maxY) y = maxY;
|
||||
x = num(y * area.width / area.height);
|
||||
}
|
||||
areaOffset.left = x;
|
||||
areaOffset.top = y;
|
||||
}
|
||||
break;
|
||||
case 2: // 右上角
|
||||
x = num(x + areaOffset.right);
|
||||
y = num(y + areaOffset.top);
|
||||
if(x <= 0 && y >= 0) { // 有效滑动
|
||||
var w = (r === 1 ? img.height : img.width);
|
||||
var t = num(offset.y + m - area.top);
|
||||
var l = num(area.right + m - offset.x - w);
|
||||
var max = isIntersect && ((t >= 0) || (l >= 0))
|
||||
? minimum(t, l)
|
||||
: false;
|
||||
// var max = isInclude && ((offset.x > 0 && offset.x + img.width <= area.right) || (offset.y > 0 && offset.y >= area.top))
|
||||
// ? minimum(offset.y - area.top, area.right - offset.x - img.width)
|
||||
// : false;
|
||||
// console.log(offset.x, offset.y, img.width, img.height, area.top, area.right, m, max)
|
||||
// console.log(offset.y + m - area.top, area.right + m - offset.x - w)
|
||||
if(-x > y && isReverse) { // 以x轴滑动距离为缩放基准
|
||||
maxX = validMax(maxX, max, isInclude, l, t, area.width / area.height);
|
||||
if(-x > maxX) x = -maxX;
|
||||
y = num(-x * area.height / area.width);
|
||||
} else { // 以y轴滑动距离为缩放基准
|
||||
maxY = validMax(maxY, max, isInclude, t, l, area.height / area.width);
|
||||
if(y > maxY) y = maxY;
|
||||
x = num(-y * area.width / area.height);
|
||||
}
|
||||
areaOffset.right = x;
|
||||
areaOffset.top = y;
|
||||
}
|
||||
break;
|
||||
case 3: // 左下角
|
||||
x += num(x + areaOffset.left);
|
||||
y += num(y + areaOffset.bottom);
|
||||
if(x >= 0 && y <= 0) { // 有效滑动
|
||||
var w = (r === 1 ? img.width : img.height);
|
||||
var t = num(area.bottom - m - offset.y - w);
|
||||
var l = num(offset.x - m - area.left);
|
||||
var max = isIntersect && ((l >= 0) || (t >= 0))
|
||||
? minimum(t, l)
|
||||
: false;
|
||||
if(x > -y && isReverse) { // 以x轴滑动距离为缩放基准
|
||||
maxX = validMax(maxX, max, isInclude, l, t, area.width / area.height);
|
||||
if(x > maxX) x = maxX;
|
||||
y = num(-x * area.height / area.width);
|
||||
} else { // 以y轴滑动距离为缩放基准
|
||||
maxY = validMax(maxY, max, isInclude, t, l, area.height / area.width);
|
||||
if(-y > maxY) y = -maxY;
|
||||
x = num(-y * area.width / area.height);
|
||||
}
|
||||
areaOffset.left = x;
|
||||
areaOffset.bottom = y;
|
||||
}
|
||||
break;
|
||||
case 4: // 右下角
|
||||
x = num(x + areaOffset.right);
|
||||
y = num(y + areaOffset.bottom);
|
||||
if(x <= 0 && y <= 0) { // 有效滑动
|
||||
var w = (r === 1 ? img.height : img.width);
|
||||
var h = (r === 1 ? img.width : img.height);
|
||||
var t = num(area.bottom - offset.y - h - m);
|
||||
var l = num(area.right + m - offset.x - w);
|
||||
var max = isIntersect && ((l >= 0) || (t >= 0))
|
||||
? minimum(t, l)
|
||||
: false;
|
||||
if(-x > -y && isReverse) { // 以x轴滑动距离为缩放基准
|
||||
maxX = validMax(maxX, max, isInclude, l, t, area.width / area.height);
|
||||
if(-x > maxX) x = -maxX;
|
||||
y = num(x * area.height / area.width);
|
||||
} else { // 以y轴滑动距离为缩放基准
|
||||
maxY = validMax(maxY, max, isInclude, t, l, area.height / area.width);
|
||||
if(-y > maxY) y = -maxY;
|
||||
x = num(y * area.width / area.height);
|
||||
}
|
||||
areaOffset.right = x;
|
||||
areaOffset.bottom = y;
|
||||
}
|
||||
break;
|
||||
}
|
||||
// console.log(x, y, JSON.stringify(areaOffset))
|
||||
changeAreaRect({
|
||||
instance: o,
|
||||
});
|
||||
// this.draw();
|
||||
}
|
||||
} else if (e.touches.length == 2) { // 双点触摸缩放
|
||||
var start = getDistanceByTouches(touches);
|
||||
var end = getDistanceByTouches(e.touches);
|
||||
scaleImage({
|
||||
instance: o,
|
||||
check: !area.bounce,
|
||||
scale: (end.c - start.c) / 100,
|
||||
x: end.x,
|
||||
y: end.y
|
||||
});
|
||||
touchType = 'scale';
|
||||
} else if(touchType === 'scale') {// 从双点触摸变成单点触摸 / 从缩放变成拖动
|
||||
touchType = 'move';
|
||||
} else {
|
||||
changeImageRect({
|
||||
instance: o,
|
||||
check: !area.bounce,
|
||||
x: e.touches[0].pageX - touches[0].pageX,
|
||||
y: e.touches[0].pageY - touches[0].pageY
|
||||
});
|
||||
touchType = 'move';
|
||||
}
|
||||
touches = e.touches;
|
||||
},
|
||||
/**
|
||||
* 触摸结束
|
||||
* @param {Object} e 事件对象
|
||||
* @param {Object} o 组件实例对象
|
||||
*/
|
||||
touchend: function(e, o) {
|
||||
if(!img.src) return;
|
||||
if(touchType === 'stretch') { // 拉伸裁剪区域的四个角缩放
|
||||
// 裁剪区域宽度被缩放到多少
|
||||
var left = areaOffset.left;
|
||||
var right = areaOffset.right;
|
||||
var top = areaOffset.top;
|
||||
var bottom = areaOffset.bottom;
|
||||
var w = area.width + right - left;
|
||||
var h = area.height + bottom - top;
|
||||
// 图像放大倍数
|
||||
var p = scale * (area.width / w) - scale;
|
||||
// 复原裁剪区域
|
||||
areaOffset = { left: 0, right: 0, top: 0, bottom: 0 };
|
||||
changeAreaRect({
|
||||
instance: o,
|
||||
});
|
||||
scaleImage({
|
||||
instance: o,
|
||||
scale: p,
|
||||
x: area.left + left + (1 === activeAngle || 3 === activeAngle ? w : 0),
|
||||
y: area.top + top + (1 === activeAngle || 2 === activeAngle ? h : 0)
|
||||
});
|
||||
} else if (area.bounce) { // 检查边界并矫正,实现拖动到边界时有回弹效果
|
||||
changeImageRect({
|
||||
instance: o,
|
||||
check: true
|
||||
});
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 顺时针翻转图片90°
|
||||
* @param {Object} e 事件对象
|
||||
* @param {Object} o 组件实例对象
|
||||
*/
|
||||
rotateImage: function(e, o) {
|
||||
rotateImage(e, o, 90);
|
||||
},
|
||||
rotateImage90: function(e, o) {
|
||||
rotateImage(e, o, 90)
|
||||
},
|
||||
rotateImage270: function(e, o) {
|
||||
rotateImage(e, o, 270)
|
||||
},
|
||||
// 此处只用于对齐其他平台端的样式参数,防止异常,无作用
|
||||
imageStyles: '',
|
||||
maskStylesList: ['', '', '', ''],
|
||||
borderStyles: '',
|
||||
gridStylesList: ['', '', '', ''],
|
||||
angleStylesList: ['', '', '', ''],
|
||||
circleBoxStyles: '',
|
||||
circleStyles: '',
|
||||
}
|
||||
81
uni_modules/qf-image-cropper/package.json
Normal file
81
uni_modules/qf-image-cropper/package.json
Normal file
@ -0,0 +1,81 @@
|
||||
{
|
||||
"id": "qf-image-cropper",
|
||||
"displayName": "图片裁剪插件",
|
||||
"version": "2.2.5",
|
||||
"description": "图片裁剪插件,支持自定义尺寸、定点等比例缩放、拖动、图片翻转、剪切圆形/圆角图片、定制样式,功能多性能高体验好注释全。",
|
||||
"keywords": [
|
||||
"qf-image-cropper",
|
||||
"图片裁剪",
|
||||
"图片编辑",
|
||||
"头像裁剪",
|
||||
"小程序"
|
||||
],
|
||||
"repository": "",
|
||||
"engines": {
|
||||
"HBuilderX": "^3.1.0"
|
||||
},
|
||||
"dcloudext": {
|
||||
"type": "component-vue",
|
||||
"sale": {
|
||||
"regular": {
|
||||
"price": "0.00"
|
||||
},
|
||||
"sourcecode": {
|
||||
"price": "0.00"
|
||||
}
|
||||
},
|
||||
"contact": {
|
||||
"qq": ""
|
||||
},
|
||||
"declaration": {
|
||||
"ads": "无",
|
||||
"data": "插件不采集任何数据",
|
||||
"permissions": "无"
|
||||
},
|
||||
"npmurl": ""
|
||||
},
|
||||
"uni_modules": {
|
||||
"dependencies": [],
|
||||
"encrypt": [],
|
||||
"platforms": {
|
||||
"client": {
|
||||
"Vue": {
|
||||
"vue2": "y",
|
||||
"vue3": "y"
|
||||
},
|
||||
"App": {
|
||||
"app-vue": "y",
|
||||
"app-nvue": "n"
|
||||
},
|
||||
"H5-mobile": {
|
||||
"Safari": "y",
|
||||
"Android Browser": "y",
|
||||
"微信浏览器(Android)": "y",
|
||||
"QQ浏览器(Android)": "u"
|
||||
},
|
||||
"H5-pc": {
|
||||
"Chrome": "u",
|
||||
"IE": "u",
|
||||
"Edge": "u",
|
||||
"Firefox": "u",
|
||||
"Safari": "u"
|
||||
},
|
||||
"小程序": {
|
||||
"微信": "y",
|
||||
"阿里": "n",
|
||||
"百度": "n",
|
||||
"字节跳动": "n",
|
||||
"QQ": "u",
|
||||
"钉钉": "n",
|
||||
"快手": "n",
|
||||
"飞书": "n",
|
||||
"京东": "n"
|
||||
},
|
||||
"快应用": {
|
||||
"华为": "n",
|
||||
"联盟": "n"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
97
uni_modules/qf-image-cropper/readme.md
Normal file
97
uni_modules/qf-image-cropper/readme.md
Normal file
@ -0,0 +1,97 @@
|
||||
# qf-image-cropper
|
||||
## 图片裁剪插件
|
||||
uniapp微信小程序图片裁剪插件,支持自定义尺寸、定点等比例缩放、拖动、图片翻转、剪切圆形/圆角图片、定制样式,功能多性能高体验好注释全。
|
||||
|
||||
### 平台支持:
|
||||
1. 支持微信小程序:移动端、PC端、开发者工具
|
||||
2. 支持H5平台(2.1.0版本起)
|
||||
3. 支持APP平台(2.1.5版本起):Android、IOS
|
||||
4. 其他平台暂未测试兼容性未知
|
||||
|
||||
### 支持功能:
|
||||
1. 自定义裁剪尺寸
|
||||
2. 定点等比例缩放:移动端以双指触摸中心点为缩放中心点,PC端以鼠标所在点为缩放中心点
|
||||
3. 自由拖动:支持限制滑出边界,也支持回弹效果(滑动时可滑出边界,释放时回弹到边界)
|
||||
4. 图片翻转:在裁剪尺寸非 1:1 的情况下,翻转时宽高无法铺满裁剪区域时,图片会自动放大到合适尺寸
|
||||
5. 裁剪生成新图片
|
||||
6. 本地选择图片
|
||||
7. 可定制样式:可自由选择是否渲染裁剪边框、可伸缩裁剪顶角、参考线
|
||||
8. 裁剪圆角图片:圆形、圆角矩形
|
||||
|
||||
### 属性说明
|
||||
| 属性名 | 类型 | 默认值 | 说明 |
|
||||
|:---|:---|:---|:---|
|
||||
| src | String | | 图片资源地址 |
|
||||
| width | Number | 300 | 裁剪宽度 |
|
||||
| height | Number | 300 | 裁剪高度 |
|
||||
| showBorder | Boolean | true | 是否绘制裁剪区域边框 |
|
||||
| showGrid | Boolean | true | 是否绘制裁剪区域网格参考线 |
|
||||
| showAngle | Boolean | true | 是否展示四个支持伸缩的角 |
|
||||
| areaScale | Number | 0.3 | 裁剪区域最小缩放倍数 |
|
||||
| minScale | Number | 1 | 图片最小缩放倍数 |
|
||||
| maxScale | Number | 5 | 图片最大缩放倍数 |
|
||||
| checkRange | Boolean | true | 检查图片位置是否超出裁剪边界,如果超出则会矫正位置 |
|
||||
| backgroundColor | String | | 生成图片背景色:如果裁剪区域没有完全包含在图片中时,不设置该属性则生成图片存在一定的透明块 |
|
||||
| bounce | Boolean | true | 是否有回弹效果:当 checkRange 为 true 时有效,拖动时可以拖出边界,释放时会弹回边界 |
|
||||
| rotatable | Boolean | true | 是否支持翻转 |
|
||||
| reverseRotatable | Boolean | false | 是否支持逆向翻转 |
|
||||
| choosable | Boolean | true | 是否支持从本地选择素材 |
|
||||
| gpu | Boolean | false | 是否开启硬件加速,图片缩放过程中如果出现元素的“留影”或“重影”效果,可通过该方式解决或减轻这一问题 |
|
||||
| angleSize | Number | 20 | 四个角尺寸,单位px |
|
||||
| angleBorderWidth | Number | 2 | 四个角边框宽度,单位px |
|
||||
| zIndex | Number/String | | 调整组件层级 |
|
||||
| radius | Number | | 裁剪图片圆角半径,单位px |
|
||||
| fileType | String | png | 生成文件的类型,只支持 'jpg' 或 'png'。默认为 'png' |
|
||||
| delay | Number | 1000 | 图片从绘制到生成所需时间,单位ms<br>微信小程序平台使用 `Canvas 2D` 绘制时有效<br>如绘制大图或出现裁剪图片空白等情况应适当调大该值,因 `Canvas 2d` 采用同步绘制,需自己把控绘制完成时间 |
|
||||
| navigation | Boolean | true | 页面是否是原生标题栏:<br>H5平台当 showAngle 为 true 时,使用插件的页面在 `page.json` 中配置了 `"navigationStyle": "custom"` 时,必须将此值设为 false ,否则四个可拉伸角的触发位置会有偏差。<br>注:因H5平台的窗口高度是包含标题栏的,而屏幕触摸点的坐标是不包含的 |
|
||||
| @crop | EventHandle | | 剪裁完成后触发,event = { tempFilePath }。在H5平台下,tempFilePath 为 base64 |
|
||||
|
||||
### 基本用法
|
||||
```
|
||||
<template>
|
||||
<div>
|
||||
<qf-image-cropper :width="500" :height="500" :radius="30" @crop="handleCrop"></qf-image-cropper>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import QfImageCropper from '@/components/qf-image-cropper/qf-image-cropper.vue';
|
||||
export default {
|
||||
components: {
|
||||
QfImageCropper
|
||||
},
|
||||
methods: {
|
||||
handleCrop(e) {
|
||||
uni.previewImage({
|
||||
urls: [e.tempFilePath],
|
||||
current: 0
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
通过ref组件实例可在进入页面后直接打开相册选择图片
|
||||
```
|
||||
mounted() {
|
||||
this.$refs.qfImageCropper.chooseImage({ sourceType: ['album'] });
|
||||
}
|
||||
```
|
||||
### 使用说明
|
||||
1.建议在`pages.json`中将引用插件的页面添加一下配置禁止下拉刷新和禁止页面滑动,防止出现性能或页面抖动等问题。
|
||||
```
|
||||
{
|
||||
"enablePullDownRefresh": false,
|
||||
"disableScroll": true
|
||||
}
|
||||
```
|
||||
2.建议使用本插件不要设置过大宽高的目标图片尺寸,建议1365x1365以内,否则可能会导致如下问题:
|
||||
```
|
||||
1.界面卡顿,内存占用过高
|
||||
2.生成图片失真(模糊)
|
||||
3.确定裁剪后一直显示 `裁剪中...`,该问题是由 `uni.canvasToTempFilePath` 无法回调导致,不同平台不同设备限制可能有所不同。
|
||||
```
|
||||
3.如裁剪后的图片存在偏移的问题,请检查是否受自己项目中父组件或全局样式影响。
|
||||
4.src属性设置网络图片时,图片资源必须是能触发 `getImageInfo` API 的 success 回调才可用于插件裁剪。因此小程序平台获取网络图片信息需先配置download域名白名单才能生效。
|
||||
5.如果组件无法正常渲染且使用了 `v-if` 时,可尝试将 `v-if` 替换为 `v-show`
|
||||
6.如果App端导入组件后无法正常渲染,请尝试重新运行
|
||||
Loading…
x
Reference in New Issue
Block a user