[Tip] TAB - SPACE

本文介绍了如何在SourceInsight中设置TAB显示、将输入的TAB转换为空格以及将已存在的TAB符转换为空格的方法。

在很多大公司的编码规范中一般建议不使用TAB符,全部用四个SPACE(空格)代替,另外由于有些代码并不是自己编写,难免存在TAB符,所以需要进行替换。

- source insight中显示TAB符:
Options->Document Options 将 Visible tabs 打勾

- source insight中将输入的TAB符转换为空格:  
1. Options->Document Options 将 Expand Tabs 打勾
2. TAB符宽度设置, 在TAB width中填入期望数值,一般为4个空格,即填4

- source insight中将已有的TAB符转换为空格:
选择需要替换的文件,Edit - Special Edit - Tabs to Spaces

 


``` <template> <view class="container"> <!-- 日期筛选 --> <view class="filter-box"> <picker mode="date" @change="startDateChange"> <view class="date-picker">开始日期:{{ startDate || '请选择' }}</view> </picker> <picker mode="date" @change="endDateChange"> <view class="date-picker">结束日期:{{ endDate || '请选择' }}</view> </picker> <button @tap="filterData" class="filter-btn">筛选</button> </view> <!-- 考核进度 --> <view class="progress-card"> <text class="title">个人考核进度</text> <progress :percent="progress" stroke-width="6" activeColor="#4cd964"/> <text class="progress-text">{{ progress }}%(当前阶段:{{ currentStage }})</text> </view> <!-- 选项卡切换 --> <view class="tabs"> <text :class="['tab-item', activeTab === 0 ? 'active' : '']" @tap="switchTab(0)">订单记录</text> <text :class="['tab-item', activeTab === 1 ? 'active' : '']" @tap="switchTab(1)">消费记录</text> </view> <!-- 记录列表 --> <scroll-view scroll-y class="list-container" @scrolltolower="loadMore"> <view v-if="activeTab === 0" class="list"> <view v-for="(item, index) in filteredOrders" :key="index" class="list-item"> <text class="order-no">订单号:{{ item.orderNo }}</text> <text class="order-date">{{ item.date }}</text> <text class="order-amount">金额:¥{{ item.amount }}</text> </view> <view v-if="!filteredOrders.length" class="empty-tip">暂无订单记录</view> </view> <view v-if="activeTab === 1" class="list"> <view v-for="(item, index) in filteredConsumptions" :key="index" class="list-item"> <text class="consume-type">{{ item.type }}</text> <text class="consume-date">{{ item.date }}</text> <text class="consume-amount">¥{{ item.amount }}</text> </view> <view v-if="!filteredConsumptions.length" class="empty-tip">暂无消费记录</view> </view> <view v-if="loading" class="loading">加载中...</view> </scroll-view> </view> </template> <style lang="scss"> .container { padding: 20rpx; } .filter-box { display: flex; align-items: center; margin-bottom: 20rpx; .date-picker { flex: 1; padding: 15rpx; border: 1rpx solid #eee; margin-right: 10rpx; border-radius: 8rpx; } .filter-btn { width: 150rpx; height: 70rpx; line-height: 70rpx; } } .progress-card { background: #fff; padding: 30rpx; border-radius: 16rpx; margin-bottom: 30rpx; box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.1); .title { display: block; font-size: 34rpx; margin-bottom: 20rpx; } .progress-text { display: block; margin-top: 15rpx; color: #666; } } .tabs { display: flex; border-bottom: 1rpx solid #eee; .tab-item { flex: 1; text-align: center; padding: 25rpx; color: #666; &.active { color: #007AFF; border-bottom: 4rpx solid #007AFF; } } } .list-container { height: calc(100vh - 400rpx); .list-item { padding: 30rpx; background: #fff; margin-bottom: 20rpx; border-radius: 12rpx; display: flex; justify-content: space-between; box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.05); .order-amount, .consume-amount { color: #e64340; font-weight: bold; } } .empty-tip { text-align: center; color: #999; padding: 50rpx; } .loading { text-align: center; padding: 30rpx; color: #999; } } </style>```我需要消费记录样式和订单记录样式帮我在我这个代码写一下
03-29
<template> <div class="login-container"> <!-- 顶部标题区 --> <div class="login-header"> < img src="https://qzonestyle.gtimg.cn/qzone/v8/img/Qzone.svg" alt="QQ空间图标" class="header-icon" /> <h1 class="header-title">QQ账号登录</h1> </div> <!-- 登录主体区 --> <div class="login-main"> <!-- 登录选项卡:账号密码登录 / 扫码登录 --> <div class="login-tabs"> <button class="tab-btn" :class="{ active: isPwdLogin }" @click="isPwdLogin = true" > 账号密码登录 </button> <button class="tab-btn" :class="{ active: !isPwdLogin }" @click="isPwdLogin = false" > 扫码登录 </button> </div> <!-- 账号密码登录面板 --> <div class="login-panel" v-if="isPwdLogin"> <div class="input-group"> <label class="input-label">QQ账号</label> <input type="text" class="input-control" placeholder="请输入QQ号或邮箱" v-model="loginForm.qq" /> </div> <div class="input-group"> <label class="input-label">密码</label> <input type="password" class="input-control" placeholder="请输入密码" v-model="loginForm.password" /> </div> <!-- 记住账号 & 忘记密码 --> <div class="login-option"> <label class="remember-label"> <input type="checkbox" v-model="loginForm.remember" /> 记住账号 </label> <a href=" " class="forget-link">忘记密码?</a > </div> <!-- 登录按钮 --> <button class="login-btn" @click="handleLogin" :disabled="!loginForm.qq || !loginForm.password" > 登录 </button> <!-- 其他登录方式 --> <div class="other-login"> <span class="other-title">其他登录方式</span> <div class="other-icons"> < img src="https://qzonestyle.gtimg.cn/qzone/v8/img/weixin.svg" alt="微信登录" class="other-icon" /> < img src="https://qzonestyle.gtimg.cn/qzone/v8/img/weibo.svg" alt="微博登录" class="other-icon" /> < img src="https://qzonestyle.gtimg.cn/qzone/v8/img/qqmail.svg" alt="邮箱登录" class="other-icon" /> </div> </div> </div> <!-- 扫码登录面板 --> <div class="login-panel qrcode-panel" v-else> <div class="qrcode-box"> <!-- 二维码占位图(实际项目需替换为真实二维码接口) --> < img src="https://qzonestyle.gtimg.cn/qzone/v8/img/qrcode_login.png" alt="登录二维码" class="qrcode-img" /> <p class="qrcode-tip">请使用QQ手机版扫码登录</p > </div> <button class="refresh-qrcode" @click="handleRefreshQrcode"> 刷新二维码 </button> </div> </div> <!-- 底部版权区 --> <div class="login-footer"> <a href="#" class="footer-link">服务条款</a > <span class="footer-split">|</span> <a href="#" class="footer-link">隐私政策</a > <span class="footer-split">|</span> <a href="#" class="footer-link">帮助中心</a > <p class="copyright">Copyright © 1998 - 2024 Tencent. All Rights Reserved.</p > </div> </div> </template> <script setup> import { ref } from 'vue'; // 登录状态与表单数据 const isPwdLogin = ref(true); // true:账号密码登录,false:扫码登录 const loginForm = ref({ qq: '', password: '', remember: false }); // 登录按钮点击事件(实际项目需对接后端接口) const handleLogin = () => { alert(`登录信息:QQ=${loginForm.value.qq},记住账号=${loginForm.value.remember}`); // 真实场景下此处应写接口请求逻辑,如: // axios.post('/api/login', loginForm.value).then(res => { ... }) }; // 刷新二维码事件 const handleRefreshQrcode = () => { alert('二维码已刷新(实际项目需重新请求二维码接口)'); }; </script> <style scoped> /* 全局样式重置与基础配置 */ * { margin: 0; padding: 0; box-sizing: border-box; font-family: "Microsoft YaHei", Arial, sans-serif; } body { background-color: #f5f5f5; padding-top: 50px; } /* 登录容器 */ .login-container { width: 800px; margin: 0 auto; background-color: #fff; border-radius: 8px; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); padding: 30px; } /* 顶部标题 */ .login-header { display: flex; align-items: center; margin-bottom: 30px; } .header-icon { width: 40px; height: 40px; margin-right: 12px; } .header-title { font-size: 24px; color: #333; font-weight: normal; } /* 登录选项卡 */ .login-tabs { display: flex; border-bottom: 1px solid #eee; margin-bottom: 25px; } .tab-btn { padding: 10px 20px; background: transparent; border: none; font-size: 16px; color: #666; cursor: pointer; position: relative; } .tab-btn.active { color: #1E9FFF; } .tab-btn.active::after { content: ''; position: absolute; bottom: -1px; left: 0; width: 100%; height: 2px; background-color: #1E9FFF; } /* 登录面板通用样式 */ .login-panel { width: 500px; margin: 0 auto; } /* 输入框组 */ .input-group { margin-bottom: 20px; } .input-label { display: block; margin-bottom: 8px; font-size: 14px; color: #333; } .input-control { width: 100%; height: 44px; padding: 0 15px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; transition: border-color 0.3s; } .input-control:focus { outline: none; border-color: #1E9FFF; box-shadow: 0 0 0 2px rgba(30, 159, 255, 0.2); } /* 登录选项(记住账号/忘记密码) */ .login-option { display: flex; justify-content: space-between; align-items: center; margin-bottom: 25px; font-size: 14px; } .remember-label { color: #666; cursor: pointer; } .forget-link { color: #1E9FFF; text-decoration: none; } .forget-link:hover { text-decoration: underline; } /* 登录按钮 */ .login-btn { width: 100%; height: 46px; background-color: #1E9FFF; color: #fff; border: none; border-radius: 4px; font-size: 16px; cursor: pointer; transition: background-color 0.3s; } .login-btn:disabled { background-color: #87CEEB; cursor: not-allowed; } .login-btn:hover:not(:disabled) { background-color: #0C85D0; } /* 其他登录方式 */ .other-login { margin-top: 30px; text-align: center; } .other-title { font-size: 14px; color: #999; display: inline-block; padding: 0 15px; position: relative; } .other-title::before, .other-title::after { content: ''; position: absolute; top: 50%; width: 80px; height: 1px; background-color: #eee; } .other-title::before { left: -80px; } .other-title::after { right: -80px; } .other-icons { display: flex; justify-content: center; gap: 30px; margin-top: 20px; } .other-icon { width: 40px; height: 40px; cursor: pointer; transition: transform 0.3s; } .other-icon:hover { transform: scale(1.1); } /* 扫码登录面板 */ .qrcode-panel { text-align: center; } .qrcode-box { display: inline-block; padding: 15px; background-color: #fff; border: 1px solid #eee; border-radius: 4px; margin-bottom: 20px; } .qrcode-img { width: 200px; height: 200px; } .qrcode-tip { margin-top: 10px; font-size: 14px; color: #666; } .refresh-qrcode { padding: 8px 16px; border: 1px solid #1E9FFF; color: #1E9FFF; background: transparent; border-radius: 4px; font-size: 14px; cursor: pointer; transition: background-color 0.3s; } .refresh-qrcode:hover { background-color: rgba(30, 159, 255, 0.1); } /* 底部版权区 */ .login-footer { margin-top: 40px; text-align: center; font-size: 12px; color: #999; } .footer-link { color: #999; text-decoration: none; margin: 0 5px; } .footer-link:hover { color: #1E9FFF; text-decoration: underline; } .footer-split { margin: 0 5px; } .copyright { margin-top: 10px; } /* 响应式适配(手机端) */ @media (max-width: 768px) { .login-container { width: 100%; margin: 0 15px; padding: 20px 15px; } .login-panel { width: 100%; } .other-title::before, .other-title::after { width: 50px; } .other-title::before { left: -50px; } .other-title::after { right: -50px; } } </style>
09-17
<template> <view class="container"> <view class="banner" style="background:url('https://app.txcaapp.cc/static/images/bg1.png');background-size: 100% 300px;z-index: -1;background-repeat:no-repeat;"> </view> <!-- 头部导航 --> <view class="header" :style="{paddingTop: navHeight + 'px'}"> <view class="back-btn" @click="goBack"> <image src="https://app.txcaapp.cc/static/images/goback1.png" mode="aspectFit"></image> </view> <view class="title">我的团队</view> </view> <!-- 业绩展示 --> <view class="performance-card"> <text class="label">{{ st === 0 ? '一部业绩:' : '二部业绩:' }}</text> <text class="value">{{userinfo.team_yeji_total || 0}}</text> </view> <!-- 部门切换 --> <view class="department-tabs"> <view :class="['tab', {active: st === 0}]" @click="switchDepartment(0)"> 一部 ({{userinfo.myteamCount1 || 0}}) </view> <view :class="['tab', {active: st === 1}]" @click="switchDepartment(1)"> 二部 ({{userinfo.myteamCount2 || 0}}) </view> </view> <!-- 搜索框 --> <view class="search-box"> <image class="search-icon" src="/static/img/search_ico.png"></image> <input v-model="keyword" placeholder="输入昵称/姓名/手机号搜索" placeholder-style="color:#C2C2C2;font-size:24rpx" @confirm="handleSearch" /> </view> <!-- 成员列表 --> <view class="member-list"> <view v-if="loading" class="loading-tip">加载中...</view> <view v-else-if="currentList.length === 0" class="empty-tip">暂无数据</view> <view v-for="member in currentList" :key="member.id" class="member-item" @click="toggleMember(member)"> <view class="member-info"> <image class="avatar" :src="member.headimg || '/static/img/touxiang.png'"></image> <view class="details"> <view class="name-line"> <text class="name">{{member.nickname || member.tel}}</text> <text class="id">(ID:{{member.id}})</text> <text class="level">{{member.levelname || '无'}}</text> </view> <view class="info-line"> <text>手机号: {{member.tel || '未设置'}}</text> <text>贡献: {{member.teamyeji || 0}}</text> </view> </view> <view class="arrow" v-if="hasChildren(member)"> {{expandedMembers.has(member.id) ? '▼' : '▶'}} </view> </view> <!-- 子成员列表 --> <view class="children-list" v-if="expandedMembers.has(member.id) && getChildren(member.id).length"> <view v-for="child in getChildren(member.id)" :key="child.id" class="child-item" @click.stop="toggleMember(child)"> <view class="member-info"> <image class="avatar" :src="child.headimg || '/static/img/touxiang.png'"></image> <view class="details"> <view class="name-line"> <text class="name">{{child.nickname || child.tel}}</text> <text class="id">(ID:{{child.id}})</text> </view> <view class="info-line"> <text>手机号: {{child.tel || '未设置'}}</text> </view> </view> <view class="arrow" v-if="hasChildren(child)"> {{expandedMembers.has(child.id) ? '▼' : '▶'}} </view> </view> </view> </view> </view> </view> </view> </template> <script> var app = getApp(); export default { data() { return { loading: true, st: 0, keyword: '', userinfo: {}, datalist: [], currentList: [], expandedMembers: new Set(), childrenMap: new Map(), navHeight: 0 }; }, onLoad(options) { this.initPage(options); }, methods: { initPage: function(options) { this.getStatusBarHeight(); this.mid = options.mid || 0; this.loadData(); }, getStatusBarHeight: function() { var systemInfo = uni.getSystemInfoSync(); this.navHeight = systemInfo.statusBarHeight + 8; }, goBack: function() { uni.navigateBack(); }, loadData: async function() { this.loading = true; try { var res = await this.fetchTeamData(); this.processResponse(res); } catch (error) { console.error('加载数据失败:', error); uni.showToast({ title: '加载失败', icon: 'none' }); } finally { this.loading = false; } }, fetchTeamData: function() { return new Promise((resolve, reject) => { app.post('ApiAgent/team', { st: this.st, pagenum: 1, keyword: this.keyword, mid: this.mid, version: '2.6.3' }, (res) => { if (res && res.datalist) { resolve(res); } else { reject(new Error('无效数据')); } }); }); }, processResponse: function(res) { this.userinfo = res.userinfo || {}; this.datalist = res.datalist || []; this.initCurrentList(); }, initCurrentList: function() { var rootId = this.st === 0 ? this.userinfo.leftid : this.userinfo.rightid; var rootMember = this.datalist.find(m => m.id === rootId); this.currentList = rootMember ? [rootMember] : []; this.expandedMembers.clear(); this.childrenMap.clear(); }, hasChildren: function(member) { return (member.leftid && member.leftid !== 0) || (member.rightid && member.rightid !== 0); }, getChildren: function(memberId) { if (this.childrenMap.has(memberId)) { return this.childrenMap.get(memberId); } var member = this.datalist.find(m => m.id === memberId); if (!member) return []; var children = []; if (member.leftid && member.leftid !== 0) { var leftChild = this.datalist.find(m => m.id === member.leftid); if (leftChild) children.push(leftChild); } if (member.rightid && member.rightid !== 0) { var rightChild = this.datalist.find(m => m.id === member.rightid); if (rightChild) children.push(rightChild); } this.childrenMap.set(memberId, children); return children; }, toggleMember: function(member) { if (!this.hasChildren(member)) return; if (this.expandedMembers.has(member.id)) { this.expandedMembers.delete(member.id); } else { this.expandedMembers.add(member.id); this.getChildren(member.id); // 预加载子成员 } }, switchDepartment: function(st) { if (this.st === st) return; this.st = st; uni.pageScrollTo({ scrollTop: 0, duration: 0 }); this.loadData(); }, handleSearch: function() { this.loadData(); }, onPullDownRefresh: function() { this.loadData().finally(() => { uni.stopPullDownRefresh(); }); } } }; </script> <style> .container { background-color: #f7f7f7; min-height: 100vh; padding-bottom: 20px; } .banner { position: absolute; width: 100%; height: 300px; z-index: -1; } .header { position: relative; padding: 15px; color: #fff; display: flex; align-items: center; } .back-btn { width: 24px; height: 24px; } .back-btn image { width: 100%; height: 100%; } .title { flex: 1; text-align: center; font-size: 18px; font-weight: bold; } .performance-card { background: #fff; border-radius: 8px; margin: 15px; padding: 15px; display: flex; align-items: center; } .performance-card .label { font-size: 16px; font-weight: bold; } .performance-card .value { font-size: 18px; color: #f56c6c; margin-left: 10px; } .department-tabs { display: flex; justify-content: center; background: #fff; margin: 0 15px; border-radius: 8px; } .tab { padding: 10px 20px; font-size: 16px; color: #666; } .tab.active { color: #007aff; font-weight: bold; border-bottom: 2px solid #007aff; } .search-box { display: flex; align-items: center; background: #fff; margin: 15px; padding: 8px 15px; border-radius: 20px; } .search-icon { width: 16px; height: 16px; margin-right: 8px; } .search-box input { flex: 1; height: 30px; font-size: 14px; } .member-list { margin: 15px; } .loading-tip, .empty-tip { text-align: center; padding: 30px; color: #999; } .member-item { background-color: #fff; border-radius: 8px; margin-bottom: 10px; overflow: hidden; } .member-info { display: flex; align-items: center; padding: 12px; position: relative; } .avatar { width: 50px; height: 50px; border-radius: 50%; margin-right: 12px; background-color: #f0f0f0; } .details { flex: 1; } .name-line { display: flex; align-items: center; margin-bottom: 5px; } .name { font-size: 15px; color: #333; margin-right: 8px; max-width: 120px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .id { font-size: 12px; color: #999; margin-right: 8px; } .level { font-size: 12px; color: #fff; background-color: #67c23a; padding: 1px 5px; border-radius: 3px; } .info-line { display: flex; justify-content: space-between; font-size: 12px; color: #999; } .arrow { padding: 0 10px; color: #999; font-size: 14px; } .children-list { padding-left: 30px; background-color: #f9f9f9; overflow: hidden; } .child-item { border-top: 1px dashed #eee; } .child-item .avatar { width: 40px; height: 40px; } </style>注意:代码中有两个接口ApiAgent/get_team_yeji和ApiAgent/team,将这段代码进行修复,且优化;多余代码可以直接删除或者重构,给出完整代码
06-09
<template> <div class="sImgEditorBox"> <div class="top" ref="viewDomP"> <a-spin :spinning="isShowLoading" class="fullBox"> <div class="fd-img-contain fd-clearfix" ref="imageContainer"> <div class="fd-img-panel" id="jsimgpanel" :style="panelStyle" @mousedown="imgbigsmalldown($event)" @mousemove="imgbigsmallmove($event)" @mouseup="imgbigsmallup($event)"> <img :src="currentImageUrl" :style="{width: imgWidth1 + 'px', height: imgHeight1 + 'px'}" class="fd-img"> <div :style="{width: imgWidth1 + 'px', height: imgHeight1 + 'px'}" class="fd-painting-drawing" id="jsdrawpaint" @mousedown="drawmaskdown($event)" @mousemove="drawmaskmove($event)" @mouseup="drawmaskup($event)"> <!-- 已确认的遮挡矩形 --> <div v-for="(rect, index) in rectData" :key="rect.id" class="fdDiv" :id="'jspzDiv' + rect.id" :style="{ left: rect.x + 'px', top: rect.y + 'px', width: rect.width + 'px', height: rect.height + 'px' }"> </div> <!-- 当前正在绘制的矩形 --> <div v-if="currentRect" class="fdDiv" :style="{ left: currentRect.x + 'px', top: currentRect.y + 'px', width: currentRect.width + 'px', height: currentRect.height + 'px' }"> </div> <!-- 确认操作面板 --> <ul class="fd-pzselect" :style="{ display: showConfirmPanel ? 'block' : 'none', left: confirmPosition.x + 'px', top: confirmPosition.y + 'px' }"> <li class="fd-li" @click="clickPzSuredelete('y',$event)" @mousedown="mousedownSureDelete($event)" @mouseup="mouseupSureDelete($event)">确定</li> <li class="fd-li" @click="clickPzSuredelete('n',$event)" @mousedown="mousedownSureDelete($event)" @mouseup="mouseupSureDelete($event)">取消</li> </ul> </div> </div> <canvas id="myCanvas" ref="myCanvas" v-show="active" :style="{width: imgWidth1, height: imgHeight1}"></canvas> </div> </a-spin> <canvas class="fd-canvas-act" id="jsCanvasAct"></canvas> </div> <div class="bottom"> <div class="actionBox"> <span class="tip">{{isXsCpd?'提示:单击图片放大!':'提示:单击图片放大后,进行遮挡操作!'}}</span> <div class="actionBox1" v-show="status!='WAIT_MAKE_DOCUMENT'&&status!='LEADER_APPROVED'"> <a v-for="item in action" :key="item.action" :disabled="(!active && item.action !== 'upload') || item.disabled || isShowLoading" :class="`btn ${ ((!active && item.action !== 'upload') || item.disabled) && 'disabledBox' }`" @click="onClickAction(item)" > <a-tooltip placement="bottom"> <template slot="title"> <span>{{ item.label }}</span> </template> <a-icon :type="item.icon" /> </a-tooltip> </a> </div> </div> <div class="viewList flex"> <a-tabs :activeKey="active" @change="onChangeTab"> <!-- <a-tab-pane key="upload"> <span slot="tab"> <div class="btnUpload" @click="onUpload"> <a-icon type="plus-circle" /> <input type="file" class="hideDom" ref="upload" /> </div> </span> </a-tab-pane> --> <a-tab-pane v-for="item in imgs" :key="item.fileId"> <span slot="tab"> <img :src="item.src" class="viewListImg" /> </span> </a-tab-pane> </a-tabs> </div> </div> <div class="fullscreen-container" v-show="isFullscreen"> <!-- 关闭按钮,右上角 --> <a-button class="close-fullscreen-btn" type="danger" icon="close" @click="toggleFullScreen" /> <a-carousel ref="carousel" :dots="false" :arrows="true" v-model="carouselIndex" class="image-carousel" @change="handleCarouselChange" > <!-- 上一张箭头(左侧) --> <div slot="prevArrow" slot-scope="props" class="custom-slick-arrow" style="left: 10px"> <a-icon type="left-circle" /> </div> <div v-for="(item, index) in imgs" :key="item.fileId" class="image-wrapper"> <img :src="item.src" /> </div> <!-- 下一张箭头(右侧) --> <div slot="nextArrow" slot-scope="props" class="custom-slick-arrow" style="right: 10px"> <a-icon type="right-circle" /> </div> </a-carousel> </div> </div> </template> <script> import _ from "lodash"; export default { components: { }, props: { dataSource: { type: Array, default: () => [], }, disabled: { type: Boolean, default: false, }, isXsCpd: { // 是否线上呈批单 type: Boolean, default: false } }, data() { this.onClickAction = _.debounce(this.onClickAction, 300); return { isShowLoading: false /* 是否显示loading */, canvas: null /* canvas对象 */, active: "" /* 当前编辑图片key */, imgs: [ /* 图片list */ // { // id: "1", // src: "/files/1.png", // }, // { // id: "2", // src: "/files/2.png", // }, ], currentImageUrl: null, currentImageName:null, status: '', // 状态 width: "", height: "", rotation: 0, // 当前旋转角度(0、90、180、270) canvas: null /* canvas对象 */, ctx: null /* 2d画布对象 */, cache: [] /* 缓存步骤数据 */, cacheNum: 10 /* 缓存步骤数量 */, cacheDoubleBack: false /* 添加完缓存之后点击返回要返回两次 */, /**拖拽需要的参数 */ isMove: false, // 是否可移动 currntMoveX: 0, // 当前X轴偏移量 currntMoveY: 0, // 当前Y轴偏移量 scaleX: 0, // 拖拽起始X位置 scaleY: 0, // 拖拽起始Y位置 /**缩放需要的参数 */ scale: 1, // 当前缩放比例 minScale: 0.1, // 最小缩放比例 maxScale: 4, // 最大缩放比例 /**全屏参数 */ isFullscreen: false, // 是否全屏显示 carouselIndex: 0, /**---------------------- */ // 预览的时候图片的大小 imgWidth1: 0, imgHeight1: 0, canvasDoc: null, //canvas的dom对象 /**遮挡功能参数--------------------------------------------- */ isClickZd: false, // 是否点击了遮挡按钮(进入遮挡模式) isDown: false, // 鼠标是否按下(开始绘制矩形) drawLeft: 0, // 鼠标按下的起始坐标(相对于绘制区域) drawTop: 0, // 绘制起点Y坐标 drawIndex: 1, // 矩形索引计数器(每次绘制新矩形时递增,生成矩形的唯一ID) rectData: [], // 存储已确认的遮挡矩形(每个矩形包含id, x, y, width, height) currentRect: null, // 当前正在绘制的矩形(临时对象,包含x, y, width, height) showConfirmPanel: false, // 是否显示确认面板 confirmPosition: { x: 0, y: 0 }, // 确认面板位置 zdbtnClass: false, // 控制遮挡按钮样式(表示激活状态) }; }, computed: { action() { return [ /* 当前视图的操作 */ { label: "全屏", action: "fullscreen", icon: "fullscreen", disabled: this.disabled, }, { label: "上一个", action: "prev", icon: "left", disabled: this.disabled, }, { label: "下一个", action: "next", icon: "right", disabled: this.disabled, }, { label: "放大", action: "zoomIn", icon: "zoom-in", disabled: this.disabled, }, { label: "缩小", action: "zoomOut", icon: "zoom-out", disabled: this.disabled, }, { label: "逆时针旋转", action: "rotateL", icon: "undo", disabled: this.disabled, }, { label: "顺时针旋转", action: "rotateR", icon: "redo", disabled: this.disabled, }, { label: "撤回", action: "back", icon: "left", disabled: this.disabled, }, { label: "重置", action: "reset", icon: "retweet", disabled: this.disabled, }, { label: "下载", action: "download", icon: "download", disabled: this.disabled, }, { label: "保存", action: "save", icon: "save", disabled: this.disabled, }, { label: "遮挡", action: "cover", icon: "copy", disabled: this.disabled, }, { label: "删除", action: "delete", icon: "delete", disabled: this.disabled, }, { label: "上传", action: "upload", icon: "upload", disabled: this.disabled, }, ]; }, panelStyle() { return { transform: ` translate(${this.currntMoveX}px, ${this.currntMoveY}px) scale(${this.scale}) rotate(${this.rotation}deg) `, transformOrigin: 'center', position: 'relative', // cursor: this.isMove ? 'grabbing' : 'default', }; } }, watch: { dataSource: { handler(val) { this.imgs = val || []; let hasActive = _.find(this.imgs, { fileId: this.active }); if (hasActive) { this.onloadImg(this.getItem(this.active)?.src, true, true); return; } if (this.imgs.length > 0) { this.onChangeTab(this.imgs[0]?.fileId); } else { this.active = null; } }, deep: true, // 深度监听 immediate: true, // 立即执行 }, }, created() { this.status = this.$utils.getSearchKeys('status') }, methods: { onUpload() { /* 点击上传摁扭 */ // let dom = this.$refs.upload; // dom.click(); this.$emit("onUpload"); }, onChangeTab(e) { /* 切换图片 */ if (e === this.active) return; this.active = e; // this.onloadImg(this.getItem(this.active)?.src, true, true); this.resetImg() this.resetData() }, onClickAction(item) { if (this.isShowLoading) return; /* 点击当前视图操作 */ const { action } = item; switch (action) { case "fullscreen": this.toggleFullScreen(); break; case "prev": this.prev(); break; case "next": this.next(); break; case "zoomIn": this.zoomIn(); break; case "zoomOut": this.zoomOut(); break; case "rotateL": this.rotateImage(-90); break; case "rotateR": this.rotateImage(90); break; case "back": this.onBackFromCache(); break; case "reset": this.onReset(); break; case "download": this.onSave("download"); break; case "save": this.onSave(); break; case "cover": this.onClickImgAction(item); break; case "upload": this.$emit("onUpload"); break; case "delete": this.onDelete(item); break; } }, onDelete() { this.$emit("onDelete", this.getItem(this.active)); }, onReset() { this.resetData(); this.$emit("onReset", this.getItem(this.active)); }, onSave(type) { /* 保存、下载 */ const _this = this; _this.isClickZd = false; // 获取隐藏Canvas const canvasAct = document.getElementById('jsCanvasAct'); if (!canvasAct) return; const ctxAct = canvasAct.getContext('2d'); // 设置Canvas尺寸为原始图片尺寸 canvasAct.width = _this.width; canvasAct.height = _this.height; // 清空画布 ctxAct.fillStyle = "#fff"; ctxAct.fillRect(0, 0, canvasAct.width, canvasAct.height); // 绘制原始图片 const img = new Image(); img.crossOrigin = 'anonymous'; // 如果图片跨域,需要设置 img.src = _this.currentImageUrl; img.onload = function () { ctxAct.drawImage(img, 0, 0, canvasAct.width, canvasAct.height); // 绘制所有遮挡矩形 ctxAct.fillStyle = 'rgba(198,198,198,0.9)'; // 灰色半透明 _this.rectData.forEach(rect => { // 将预览区域的矩形坐标转换到原始图片坐标 const ratioW = _this.width / _this.imgWidth1; const ratioH = _this.height / _this.imgHeight1; const ratio = Math.max(ratioW, ratioH); const x = rect.x * ratio; const y = rect.y * ratio; const width = rect.width * ratio; const height = rect.height * ratio; ctxAct.fillRect(x, y, width, height); }); // 生成Base64 var data = canvasAct.toDataURL('image/png'); if (type === "download") { if(window.navigator.msSaveOrOpenBlob){ var bstr = atob(data.split(',')[1]); var n = bstr.length; var u8arr = new Uint8Array(n); while(n--) { u8arr[n] = bstr.charCodeAt(n); } var blob = new Blob([u8arr]); window.navigator.msSaveOrOpenBlob(blob,_this.currentImageName) }else { let a = document.createElement("a"); a.download = _this.currentImageName; a.href = data; a.dispatchEvent(new MouseEvent("click")); } } _this.$emit("onSave", { data, ..._this.getItem(_this.active) }); _this.resetData(); }; }, getItem(fileId) { let item = _.find(this.imgs, { fileId }); return item; }, // 获取合适的比例(要展示完全) getImageRightProportion (imageWidth, imageHeight) { const viewDomP = this.$refs.viewDomP; const padding = 16; const containerWidth = viewDomP.clientWidth - padding; const containerHeight = viewDomP.clientHeight - padding; const widthProportion = containerWidth / imageWidth; const heightProportion = containerHeight / imageHeight; const proportion = Math.min(widthProportion, heightProportion) return Math.floor(proportion * 100) / 100; }, onloadImg(path) { this.currentImageUrl = path; console.log('=============================path', path); //创建图片 let image = new Image(); //设置图片地址 image.src = path + `?uuid=${this.$utils.getId()}`; this.isShowLoading = true; image.onload = (e) => { const {width, height} = image; this.width = image.width; // 原始宽高,用于重置比例计算,避免遮挡后重置比例失效问题 this.height = image.height; const proportion = this.getImageRightProportion(width, height); this.imgWidth1 = width * proportion; this.imgHeight1 = height * proportion; // 初始化Canvas,绘制图片 // this.initCanvas(); this.isShowLoading = false; }; }, /** * 遮挡相关-​​双Canvas策略 * 显示层:#myCanvas 用于实时预览 * 操作层:#jsCanvasAct 离屏处理实际遮挡合成 * 避免了频繁的服务器交互,所有遮挡处理均在客户端完成,只在最终保存时上传合成结果,大大提升了用户体验 * 进入遮挡模式 → 2. 绘制矩形区域 → 3. 确认保留矩形 → 4. 合成遮挡图 → 5. 上传服务器 → 6. 更新视图 * @param item */ onClickImgAction(item) { if (this.isShowLoading) return; this.zdbtnClass = !this.zdbtnClass; this.isClickZd = !this.isClickZd; }, drawmaskdown(event) { if (this.isClickZd === false) return; this.isDown = true; // 获取目标元素的边界信息 const rect = event.currentTarget.getBoundingClientRect(); // 获取鼠标点击的相对位置 let x = event.clientX - rect.left; let y = event.clientY - rect.top; // 计算缩放后的实际位置 this.drawLeft = x / this.scale; this.drawTop = y / this.scale; // 初始化当前矩形 this.currentRect = { id: this.drawIndex, x: this.drawLeft, y: this.drawTop, width: 0, height: 0 }; this.showConfirmPanel = false; }, drawmaskmove(event) { if (this.isClickZd === false || !this.isDown) return; const rect = event.currentTarget.getBoundingClientRect(); // 获取鼠标当前位置 let offsetX = event.clientX - rect.left; let offsetY = event.clientY - rect.top; // 更新当前矩形的尺寸 this.currentRect.width = (offsetX / this.scale) - this.drawLeft; this.currentRect.height = (offsetY / this.scale) - this.drawTop; }, drawmaskup(event) { if (this.isClickZd === false || !this.isDown) return; this.isDown = false; const rect = event.currentTarget.getBoundingClientRect(); // 获取鼠标抬起位置 let x = event.clientX - rect.left; let y = event.clientY - rect.top; // 显示确认面板(位置计算) this.confirmPosition = { x: x / this.scale + 20, y: y / this.scale - 10 }; this.showConfirmPanel = true; this.drawIndex++; }, // 点击确定取消事件 clickPzSuredelete(yorn, event) { // 保留原始功能 if (yorn === 'n') { // 取消 - 丢弃当前矩形 this.currentRect = null; this.drawIndex = this.drawIndex - 1; } else { // 确定 - 保存当前矩形 this.rectData.push({...this.currentRect}); this.currentRect = null; } // 隐藏确认面板 this.showConfirmPanel = false; }, // 阻止确认面板上的鼠标事件冒泡 mousedownSureDelete:function (event) { event.stopPropagation() }, mouseupSureDelete:function (event) { event.stopPropagation() }, /** * 移除旧的 Canvas 元素 * 创建新的 Canvas 元素并设置尺寸 * 加载并绘制新图片到 Canvas * 清除之前绘制的遮挡元素 */ initCanvas() { // 绘制图片到 Canvas this.canvasDoc = this.$refs.myCanvas; if (this.canvasDoc) { this.ctx = this.canvasDoc.getContext('2d'); this.canvasimg = new Image(); this.canvasimg.onload = () => { // 清除旧内容 this.ctx.clearRect(0, 0, this.canvasDoc.width, this.canvasDoc.height); // 绘制新图片 this.ctx.drawImage(this.canvasimg, 0, 0, this.imgWidth1, this.imgHeight1); }; this.canvasimg.src = this.currentImageUrl; } // 清空数据 this.drawIndex = 1; this.rectData = []; this.isClickZd = false; this.currentRect = null; this.showConfirmPanel = false; // 放大缩小操作数据还原 this.isMove = false; // 是否可移动 this.scale = 1; // 放大缩小倍数 this.currntMoveX = 0; // 移动left this.currntMoveY = 0; // 移动top this.scaleX = 0; // this.scaleY = 0; // this.rotation = 0; // 当前缩放 }, addCache() { /* 添加缓存 */ let imgCache = this.ctx?.getImageData( 0, 0, this.canvas.width, this.canvas.height ); this.cache.push(imgCache); if (this.cache.length > this.cacheNum) { this.cache.shift(); } this.cacheDoubleBack = true; }, /* 获取最新缓存 */ getCache() { let imgCache; if (this.cache.length === 1) { imgCache = this.cache[0]; } else { imgCache = this.cache.pop(); } return imgCache; }, onBackFromCache() { /* 返回操作 */ let imgCache = this.getCache(); if (this.cacheDoubleBack) { /* 如果需要返回两次,再拿一遍 */ imgCache = this.getCache(); this.cacheDoubleBack = false; } if (!imgCache) return; this.ctx.putImageData(imgCache, 0, 0); }, /** * 旋转图片方法 * 重置缩放比例和位置偏移,清空遮挡 */ rotateImage(degrees) { if (this.isShowLoading) return; // 退出遮挡模式 if (this.isClickZd) { this.isClickZd = false; this.zdbtnClass = false; this.showConfirmPanel = false; this.currentRect = null; } // 重置位置偏移量 this.currntMoveX = 0; this.currntMoveY = 0; // 更新旋转角度(确保在0-360度范围内) this.rotation = (this.rotation + degrees + 360) % 360; }, /** * 负责将图片预览区域恢复到初始状态,并为新图片准备预览环境。 * 清除之前的遮挡区域 * 重置缩放和旋转状态 * 重新初始化 Canvas * 设置新图片 */ resetImg() { this.rectData = []; // 清空之前绘制的矩形区域数据 this.isClickZd = false; // 关闭遮挡模式 // 加载并计算新图片尺寸 this.onloadImg(this.getItem(this.active)?.src, true, true); this.$nextTick(() => { // 初始化Canvas,绘制图片 this.initCanvas(); // 获取图片面板元素 const jsimgpanel = this.$refs.jsimgpanel; if (jsimgpanel) { // 重置位置 jsimgpanel.style.left = '0px'; jsimgpanel.style.top = '0px'; // 重置变换 jsimgpanel.style.transform = `rotate(0deg) scale(1)`; } }); }, resetData() { // this.ctx?.clearRect(0, 0, this.canvas.width, this.canvas.height); this.rotation = 0; this.cache = []; this.canvas = null; this.ctx = null; // 重置拖拽状态 this.isMove = false; this.currntMoveX = 0; this.currntMoveY = 0; // 重置缩放 this.scale = 1; // 重置遮挡状态 this.rectData = []; // 清空已保存的矩形 this.isClickZd = false; // 退出遮挡模式 this.zdbtnClass = false; // 样式恢复 }, /**放大缩小相关方法 */ zoomIn() { this.scale += 0.2; if (this.scale > 5) this.scale = 5; }, zoomOut() { this.scale -= 0.2; if (this.scale <= 0) this.scale = 0; }, /**上一张下一张所需方法 */ prev(){ let index = this.imgs.findIndex(item => item.fileId == this.active) if (index-1 >= 0) { this.onChangeTab(this.imgs[index-1].fileId) }else{ return } }, next(){ let index = this.imgs.findIndex(item => item.fileId == this.active) if (index+1 < this.imgs.length) { this.onChangeTab(this.imgs[index+1].fileId) }else{ return } }, /**全屏所需方法 */ toggleFullScreen() { if (!this.isFullscreen) { // 打开全屏前设置当前索引 this.carouselIndex = this.imgs.findIndex( img => img.fileId === this.active ); // 如果找不到,默认显示第一张 if (this.carouselIndex === -1) { this.carouselIndex = 0; } // 使用 $nextTick 确保 DOM 更新 this.$nextTick(() => { // 手动设置轮播位置 if (this.$refs.carousel && this.$refs.carousel.goTo) { this.$refs.carousel.goTo(this.carouselIndex); } }); } this.isFullscreen = !this.isFullscreen; }, handleCarouselChange(current) { this.carouselIndex = current; // 更新激活的图片 if (this.imgs[current]) { this.active = this.imgs[current].fileId; this.currentImageUrl = this.imgs[current].src; this.currentImageName = this.imgs[current].downloadFileName; } }, /** * 拖拽相关操作 */ imgbigsmalldown(event) { // 遮挡模式下禁用拖拽 if (this.isClickZd) return; event.preventDefault(); this.isMove = true; this.scaleX = event.clientX; this.scaleY = event.clientY; }, imgbigsmallmove(event) { // 遮挡模式下禁用拖拽 if (this.isClickZd) return; if (!this.isMove) return; event.preventDefault(); // 计算移动距离 const deltaX = event.clientX - this.scaleX; const deltaY = event.clientY - this.scaleY; // 更新位置 - 使用响应式数据而非直接操作DOM this.currntMoveX += deltaX; this.currntMoveY += deltaY; // 更新起始位置 this.scaleX = event.clientX; this.scaleY = event.clientY; }, imgbigsmallup() { // 遮挡模式下禁用拖拽 if (this.isClickZd) return; if (!this.isMove) return; this.isMove = false; }, }, }; </script> <style lang="less" scoped> .sImgEditorBox { width: 100%; height: 100%; } .top { height: calc(100% - 120px); padding-top: 16px; width: 100%; text-align: center; overflow: auto; } .bottom { position: relative; height: 120px; background: #fff; z-index: 1; .actionBox { height: 40px; border-top: 1px solid #dadada; border-bottom: 1px solid #dadada; display: flex; justify-content: space-between; } .tip { display: flex; align-items: center; padding-left: 8px; color: red; } .actionBox1 { text-align: right; display: flex; align-items: center; a, div { display: inline-block; margin-right: 16px; font-size: 20px; } } .viewList { height: calc(100% - 40px); padding: 0 16px; .viewListImg, .btnUpload { display: inline-block; width: 40px; height: 60px; } .btnUpload { border: 1px dashed #2160b8; display: flex; justify-content: center; align-items: center; } } } /deep/ .anticon { margin: 0 !important; } /deep/ .ant-tabs-tab { padding: 14px 5px !important; } /deep/ .ant-tabs, /deep/.ant-tabs-bar, /deep/ .ant-tabs-nav-container, /deep/.ant-tabs-nav-wrap, /deep/ .ant-tabs-nav-scroll, /deep/.ant-tabs-nav { height: 100%; } .hideDom { display: inline-block; visibility: hidden; width: 0px; height: 0px; } .btn { color: #555; } .btn:hover { color: #1890ff; } .selBtn { color: #1890ff; } .disabledBox { color: #ddd; } /**全屏样式================================ */ // 全屏容器 .fullscreen-container { position: fixed; top: 0; left: 0; z-index: 1000; width: 100%; height: 100vh; display: flex; flex-direction: column; justify-content: center; align-items: center; background: rgba(0, 0, 0, 0.6); // 轮播图容器 .image-carousel { width: 100%; height: 100%; .slick-slide { display: flex !important; justify-content: center; align-items: center; height: 100%; } // 自定义左右箭头样式 .custom-slick-arrow { position: absolute; z-index: 1000; top: 50%; transform: translateY(-50%); width: 60px; height: 100px; display: flex !important; align-items: center; justify-content: center; background: rgba(0, 0, 0, 0.4); border-radius: 5px; font-size: 32px; color: white; transition: all 0.3s ease; &:before { display: none; } &:hover { background: rgba(0, 0, 0, 0.7); transform: translateY(-50%) scale(1.1); } } // 左侧箭头 [slot="prevArrow"] { left: 10px; } // 右侧箭头 [slot="nextArrow"] { right: 10px; } } } // 图片包装 .image-wrapper { // max-width: 90%; // max-height: 90%; overflow: auto; display: flex !important; justify-content: center; align-items: center; height: 100%; img { max-width: 100%; max-height: 100vh; object-fit: contain; box-shadow: 0 0 20px rgba(0, 0, 0, 0.3); } } // 关闭按钮样式(右上角) .close-fullscreen-btn { position: absolute; top: 20px; right: 20px; z-index: 1001; // 确保在最上层 background: rgba(255, 255, 255, 0.3); border: none; color: white; font-size: 24px; width: 48px; height: 48px; display: flex; align-items: center; justify-content: center; border-radius: 50%; transition: all 0.3s ease; &:hover { background: rgba(255, 0, 0, 0.7); transform: scale(1.1); } } /*********图片展示部分********/ .fd-img-contain{ position: relative; z-index: 1; margin: 0 auto; padding: 15px 0; width: 515px; height: 100%; } .fd-img-panel { position: absolute; left: 0; top: 0; z-index: 5; width: 100%; height: 100%; display: flex; align-items: center; /*定义body的元素垂直居中*/ justify-content: center; /*定义body的里的元素水平居中*/ } .fd-img-panel .fd-painting-drawing { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 2; width: 100%; height: 100%; } // 遮挡矩形 .fdDiv { width: 1px; height: 1px; border: 1px solid #333333; position: absolute; background: rgba(198,198,198,0.9); } /***********批注选择样式**********/ .fd-pzselect { display: none; position: absolute; top: 9px; left: 99px; z-index: 8; width: 84px; border: 1px solid rgb(167, 171, 176); background: rgb(245, 245, 245); } /**canvas样式============== */ #myCanvas { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 2; opacity: 0; } .fd-canvas-act { display: none; } </style> 这个是我修改完的,旋转后再遮挡,正在绘制的遮挡矩形开始位置与我鼠标按下位置不一样,并且正在绘制的遮挡矩形跟确认面板是不需要跟着旋转的,用户看到的是旋转后的图片,那么就在旋转后的图片上进行遮挡操作,我理解旋转不应该影响到遮挡操作
06-16
训练数据保存为deep_convnet_params.pkl,UI使用wxPython编写。卷积神经网络(CNN)是一种专门针对图像、视频等结构化数据设计的深度学习模型,在计算机视觉、语音识别、自然语言处理等多个领域有广泛应用。其核心设计理念源于对生物视觉系统的模拟,主要特点包括局部感知、权重共享、多层级抽象以及空间不变性。 **1. 局部感知与卷积操作** 卷积层是CNN的基本构建块,使用一组可学习的滤波器对输入图像进行扫描。每个滤波器在图像上滑动,以局部区域内的像素与滤波器权重进行逐元素乘法后求和,生成输出。这一过程能够捕获图像中的边缘、纹理等局部特征。 **2. 权重共享** 同一滤波器在整个输入图像上保持相同的权重。这显著减少了模型参数数量,增强了泛化能力,并体现了对图像平移不变性的内在假设。 **3. 池化操作** 池化层通常紧随卷积层之后,用于降低数据维度并引入空间不变性。常见方法有最大池化和平均池化,它们可以减少模型对微小位置变化的敏感度,同时保留重要特征。 **4. 多层级抽象** CNN通常包含多个卷积和池化层堆叠在一起。随着网络深度增加,每一层逐渐提取更复杂、更抽象的特征,从底层识别边缘、角点,到高层识别整个对象或场景,使得CNN能够从原始像素数据中自动学习到丰富的表示。 **5. 激活函数与正则化** CNN中使用非线性激活函数来引入非线性表达能力。为防止过拟合,常采用正则化技术,如L2正则化和Dropout,以增强模型的泛化性能。 **6. 应用场景** CNN在诸多领域展现出强大应用价,包括图像分类、目标检测、语义分割、人脸识别、图像生成、医学影像分析以及自然语言处理等任务。 **7. 发展与演变** CNN的概念起源于20世纪80年代,其影响力在硬件加速和大规模数据集出现后真正显现。经典模型如LeNet-5用于手写数字识别,而AlexNet、VGG、GoogLeNet、ResNet等现代架构推动了CNN技术的快速发展。如今,CNN已成为深度学习图像处理领域的基石,并持续创新。 资源来源于网络分享,仅用于学习交流使用,请勿用于商业,如有侵权请联系我删除!
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值