uniapp 仿企微手机版审批,底部输入框默认一行,聚焦变高,并且输入框可以点击上传图片和文件文件
时间: 2025-07-21 18:55:20 浏览: 6
<think>我们要求实现一个仿企业微信手机版审批的底部输入框,具有以下特点:
1. 默认高度为一行(类似于企业微信的回复框)
2. 当聚焦时,输入框变高(可以输入多行)
3. 支持上传图片和文件
4. 具有提交按钮
我们将创建一个自定义组件,包含以下部分:
- 一个文本输入区域(textarea),初始高度为一行,聚焦时变为多行(比如3-5行)
- 一个上传按钮(可以触发选择图片或文件)
- 一个提交按钮
由于企业微信中,底部输入框通常还包含其他功能(如表情、拍照等),但我们这里主要实现上传图片和文件。
注意:在uniapp中,我们需要使用uni.chooseImage和uni.chooseMessageFile来实现图片和文件的选择。
设计思路:
1. 使用textarea作为输入框,通过动态设置height来控制高度(默认一行,聚焦时变为多行)
2. 使用一个图标作为上传按钮,点击后弹出选择菜单(图片或文件)
3. 显示已选择的图片和文件列表(可预览,可删除)
4. 提交按钮用于提交输入的文本和上传的文件
我们将组件命名为 `approval-reply-input`
由于uniapp对H5和小程序的支持,我们需要考虑不同平台的上传API:
- 图片上传:uni.chooseImage
- 文件上传:uni.chooseMessageFile(主要用于微信小程序,其他平台可以使用uni.chooseFile)
但是,为了兼容性,我们可以这样做:
- 在微信小程序中使用uni.chooseMessageFile
- 在其他平台(H5、App)使用uni.chooseFile(注意:uni.chooseFile在H5和App中可用)
不过,在H5中,我们可以使用input type="file"来实现文件选择,但为了统一,我们使用uni的API。
步骤:
1. 布局:底部固定定位,包含输入框、上传按钮、提交按钮
2. 输入框高度控制:使用一个变量`inputHeight`,默认高度为一行(比如40px),当聚焦时,将高度设置为多行(比如120px)
3. 上传按钮:点击后弹出actionSheet选择图片或文件
4. 已选文件列表:在输入框上方显示已选的文件和图片,可以删除
5. 提交:将输入内容和文件列表提交
注意:在微信小程序中,底部输入框要避免被键盘遮挡,可以使用adjust-position属性,但这里我们使用固定定位,并动态调整位置。
由于企业微信风格,我们采用蓝色主题。
下面开始实现:</think>下面我将为您实现一个高度仿企业微信手机版审批的底部输入框组件,支持输入框高度自适应、图片和文件上传功能。这个组件将完美还原企业微信的交互体验,包括默认单行显示、聚焦时自动增高、支持多类型文件上传等功能。
### 完整组件实现 (wechat-style-input.vue)
```vue
<template>
<!-- 底部输入框容器 -->
<view
class="input-container"
:style="{ bottom: keyboardHeight + 'px' }"
>
<!-- 输入区域 -->
<view class="input-area">
<!-- 文本输入框 -->
<textarea
ref="textarea"
v-model="inputValue"
class="input-text"
:class="{ 'expanded': isExpanded }"
placeholder="说点什么..."
placeholder-class="placeholder"
:focus="autoFocus"
:adjust-position="false"
:show-confirm-bar="false"
:cursor-spacing="20"
:auto-height="true"
@focus="handleFocus"
@blur="handleBlur"
@input="handleInput"
@linechange="handleLineChange"
></textarea>
<!-- 附件上传按钮 -->
<view class="attachment-btn" @click="showAttachmentMenu">
<uni-icons type="plus" size="24" color="#666"></uni-icons>
</view>
</view>
<!-- 发送按钮 -->
<view class="send-btn-container">
<button
class="send-btn"
:class="{ 'active': canSend }"
:disabled="!canSend"
@click="handleSend"
>
<uni-icons type="paperplane" size="18" color="#fff"></uni-icons>
</button>
</view>
<!-- 附件预览区域 -->
<scroll-view
v-if="attachments.length > 0"
class="attachment-preview"
scroll-x
:show-scrollbar="false"
>
<view class="attachment-list">
<view
v-for="(item, index) in attachments"
:key="index"
class="attachment-item"
:class="item.type"
>
<image
v-if="item.type === 'image'"
:src="item.path"
class="attachment-thumb"
mode="aspectFill"
@click="previewImage(index)"
/>
<view v-else-if="item.type === 'file'" class="file-icon">
<uni-icons type="folder" size="32" color="#2a6ae9"></uni-icons>
<text class="file-ext">{{ item.extension }}</text>
</view>
<view v-else-if="item.type === 'video'" class="video-icon">
<uni-icons type="videocam" size="32" color="#ff5500"></uni-icons>
</view>
<text class="attachment-name" v-if="item.name">{{ getShortName(item.name) }}</text>
<uni-icons
type="close"
size="14"
color="#fff"
class="delete-icon"
@click="removeAttachment(index)"
></uni-icons>
</view>
</view>
</scroll-view>
<!-- 附件选择菜单 -->
<uni-popup ref="attachmentMenu" type="bottom">
<view class="attachment-menu">
<view class="menu-header">
<text class="title">添加附件</text>
<uni-icons type="close" size="20" color="#999" @click="closeMenu"></uni-icons>
</view>
<view class="menu-options">
<view class="option" @click="chooseImage">
<uni-icons type="image" size="36" color="#2a6ae9"></uni-icons>
<text>图片</text>
</view>
<view class="option" @click="chooseFile">
<uni-icons type="folder" size="36" color="#2a6ae9"></uni-icons>
<text>文件</text>
</view>
<view class="option" @click="chooseVideo">
<uni-icons type="videocam" size="36" color="#2a6ae9"></uni-icons>
<text>视频</text>
</view>
<view class="option" @click="takePhoto">
<uni-icons type="camera" size="36" color="#2a6ae9"></uni-icons>
<text>拍照</text>
</view>
</view>
</view>
</uni-popup>
</view>
</template>
<script>
export default {
props: {
// 是否自动聚焦
autoFocus: {
type: Boolean,
default: false
},
// 最大附件数量
maxAttachments: {
type: Number,
default: 9
}
},
data() {
return {
inputValue: "", // 输入框内容
attachments: [], // 附件列表
isExpanded: false, // 是否展开状态
keyboardHeight: 0, // 键盘高度
isFocused: false, // 输入框是否聚焦
minHeight: 44, // 最小高度 (px)
maxHeight: 150, // 最大高度 (px)
currentHeight: 44 // 当前高度 (px)
};
},
computed: {
// 是否可以发送
canSend() {
return this.inputValue.trim() || this.attachments.length > 0;
},
// 获取文件扩展名
getFileExtension() {
return (name) => {
const parts = name.split('.');
return parts.length > 1 ? parts.pop().toUpperCase() : 'FILE';
};
},
// 缩短文件名
getShortName() {
return (name, maxLength = 8) => {
if (name.length <= maxLength) return name;
return name.substring(0, maxLength - 3) + '...';
};
}
},
methods: {
// 输入框聚焦
handleFocus(e) {
this.isFocused = true;
this.keyboardHeight = e.detail.height;
this.isExpanded = true;
},
// 输入框失焦
handleBlur() {
this.isFocused = false;
this.keyboardHeight = 0;
// 没有内容时恢复单行高度
if (!this.inputValue.trim() && this.attachments.length === 0) {
setTimeout(() => {
this.isExpanded = false;
this.currentHeight = this.minHeight;
}, 300);
}
},
// 输入内容变化
handleInput(e) {
// 内容变化处理
},
// 行数变化时调整高度
handleLineChange(e) {
const lineHeight = 24; // 每行高度 (根据实际字体大小调整)
const newHeight = Math.min(
Math.max(e.detail.height * lineHeight, this.minHeight),
this.maxHeight
);
this.currentHeight = newHeight;
},
// 显示附件菜单
showAttachmentMenu() {
this.$refs.attachmentMenu.open();
},
// 关闭附件菜单
closeMenu() {
this.$refs.attachmentMenu.close();
},
// 选择图片
chooseImage() {
this.closeMenu();
if (this.attachments.length >= this.maxAttachments) {
this.showLimitToast();
return;
}
uni.chooseImage({
count: this.maxAttachments - this.attachments.length,
sizeType: ['compressed'],
sourceType: ['album'],
success: (res) => {
this.processAttachments(res.tempFiles, 'image');
}
});
},
// 选择文件
chooseFile() {
this.closeMenu();
if (this.attachments.length >= this.maxAttachments) {
this.showLimitToast();
return;
}
uni.chooseMessageFile({
count: this.maxAttachments - this.attachments.length,
type: 'file',
success: (res) => {
this.processAttachments(res.tempFiles, 'file');
}
});
},
// 选择视频
chooseVideo() {
this.closeMenu();
if (this.attachments.length >= this.maxAttachments) {
this.showLimitToast();
return;
}
uni.chooseVideo({
sourceType: ['album'],
success: (res) => {
this.processAttachments([{
path: res.tempFilePath,
name: res.name || 'video.mp4',
size: res.size
}], 'video');
}
});
},
// 拍照
takePhoto() {
this.closeMenu();
if (this.attachments.length >= this.maxAttachments) {
this.showLimitToast();
return;
}
uni.chooseImage({
count: 1,
sizeType: ['compressed'],
sourceType: ['camera'],
success: (res) => {
this.processAttachments(res.tempFiles, 'image');
}
});
},
// 处理附件数据
processAttachments(files, type) {
uni.showLoading({ title: '加载中...', mask: true });
const newAttachments = files.map(file => ({
type,
path: file.path || file.tempFilePath,
name: file.name || this.getFileNameFromPath(file.path),
size: file.size,
extension: this.getFileExtension(file.name || '')
}));
// 模拟处理延迟
setTimeout(() => {
this.attachments = [...this.attachments, ...newAttachments];
uni.hideLoading();
// 自动聚焦输入框
if (!this.isFocused) {
this.$refs.textarea.focus();
}
}, 300);
},
// 从路径获取文件名
getFileNameFromPath(path) {
return path.split('/').pop() || 'file';
},
// 预览图片
previewImage(index) {
const images = this.attachments
.filter(item => item.type === 'image')
.map(item => item.path);
if (images.length > 0) {
uni.previewImage({
current: images.indexOf(this.attachments[index].path),
urls: images
});
}
},
// 删除附件
removeAttachment(index) {
this.attachments.splice(index, 1);
},
// 显示限制提示
showLimitToast() {
uni.showToast({
title: `最多添加${this.maxAttachments}个附件`,
icon: 'none',
duration: 2000
});
},
// 发送内容
handleSend() {
if (!this.canSend) return;
const payload = {
content: this.inputValue.trim(),
attachments: [...this.attachments]
};
this.$emit('send', payload);
// 重置状态
this.inputValue = '';
this.attachments = [];
this.isExpanded = false;
this.currentHeight = this.minHeight;
},
// 手动聚焦输入框
focusInput() {
this.$refs.textarea.focus();
}
}
};
</script>
<style scoped>
/* 底部输入容器 */
.input-container {
position: fixed;
left: 0;
right: 0;
background: #f5f5f5;
border-top: 1rpx solid #e0e0e0;
padding: 16rpx 24rpx;
padding-bottom: calc(16rpx + env(safe-area-inset-bottom));
z-index: 1000;
transition: bottom 0.3s ease;
}
/* 输入区域 */
.input-area {
display: flex;
align-items: flex-end;
background: #fff;
border-radius: 24rpx;
padding: 12rpx 16rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
}
/* 文本输入框 */
.input-text {
flex: 1;
min-height: 44px;
max-height: 150px;
font-size: 32rpx;
line-height: 1.5;
padding: 8rpx 16rpx;
background: #f8f8f8;
border-radius: 16rpx;
transition: height 0.2s ease;
}
/* 展开状态 */
.input-text.expanded {
background: #fff;
box-shadow: inset 0 0 0 1rpx #e0e0e0;
}
.placeholder {
color: #999;
}
/* 附件按钮 */
.attachment-btn {
width: 72rpx;
height: 72rpx;
border-radius: 50%;
background: #f0f0f0;
display: flex;
justify-content: center;
align-items: center;
margin-left: 16rpx;
flex-shrink: 0;
}
/* 发送按钮 */
.send-btn-container {
margin-left: 16rpx;
}
.send-btn {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
background: #c0c0c0;
display: flex;
justify-content: center;
align-items: center;
padding: 0;
margin: 0;
}
.send-btn.active {
background: #2a6ae9;
}
/* 附件预览区域 */
.attachment-preview {
margin-top: 20rpx;
height: 160rpx;
}
.attachment-list {
display: flex;
gap: 20rpx;
padding: 0 10rpx;
}
.attachment-item {
position: relative;
width: 140rpx;
height: 140rpx;
border-radius: 12rpx;
overflow: hidden;
background: #f8f8f8;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.attachment-thumb {
width: 100%;
height: 100%;
}
.file-icon, .video-icon {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
}
.file-ext {
font-size: 20rpx;
color: #666;
margin-top: 8rpx;
}
.attachment-name {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: rgba(0, 0, 0, 0.6);
color: white;
font-size: 20rpx;
padding: 4rpx 8rpx;
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.delete-icon {
position: absolute;
top: 6rpx;
right: 6rpx;
background: rgba(0, 0, 0, 0.5);
border-radius: 50%;
padding: 4rpx;
}
/* 附件菜单 */
.attachment-menu {
background: #fff;
border-radius: 24rpx 24rpx 0 0;
padding: 32rpx;
}
.menu-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 40rpx;
}
.title {
font-size: 36rpx;
font-weight: 500;
color: #333;
}
.menu-options {
display: flex;
justify-content: space-around;
}
.option {
display: flex;
flex-direction: column;
align-items: center;
width: 120rpx;
}
.option text {
font-size: 26rpx;
color: #333;
margin-top: 16rpx;
}
</style>
```
### 核心功能实现说明
#### 1. 输入框高度自适应
```javascript
handleLineChange(e) {
const lineHeight = 24; // 每行高度
const newHeight = Math.min(
Math.max(e.detail.height * lineHeight, this.minHeight),
this.maxHeight
);
this.currentHeight = newHeight;
}
```
- 默认高度44px(单行显示)
- 聚焦时自动展开(`isExpanded: true`)
- 监听`@linechange`事件动态调整高度
- 设置最小高度44px和最大高度150px(约6行)
- 失焦时若无内容则恢复单行状态
#### 2. 多类型附件上传
```javascript
// 图片选择
chooseImage() {
uni.chooseImage({...});
}
// 文件选择
chooseFile() {
uni.chooseMessageFile({...});
}
// 视频选择
chooseVideo() {
uni.chooseVideo({...});
}
// 拍照
takePhoto() {
uni.chooseImage({ sourceType: ['camera'] });
}
```
- 支持4种附件类型:图片、文件、视频、拍照
- 使用统一入口菜单(底部弹出)
- 每种类型使用不同的图标标识
- 限制最大附件数量(默认9个)
#### 3. 附件预览与处理
```vue
<view class="attachment-item" :class="item.type">
<!-- 图片显示缩略图 -->
<image v-if="item.type === 'image'" :src="item.path" />
<!-- 文件显示图标 -->
<view v-else-if="item.type === 'file'" class="file-icon">
<uni-icons type="folder" />
<text class="file-ext">{{ item.extension }}</text>
</view>
<!-- 视频显示图标 -->
<view v-else-if="item.type === 'video'" class="video-icon">
<uni-icons type="videocam" />
</view>
<!-- 文件名显示 -->
<text class="attachment-name">{{ getShortName(item.name) }}</text>
<!-- 删除按钮 -->
<uni-icons type="close" class="delete-icon" />
</view>
```
- 图片显示缩略图(支持预览)
- 文件显示文件夹图标+扩展名
- 视频显示摄像机图标
- 文件名智能截断显示
- 右上角删除按钮可移除附件
### 企业微信风格实现细节
1. **视觉设计**:
- 底部输入框圆角设计(24rpx)
- 发送按钮圆形设计(激活状态蓝色)
- 附件图标使用企业微信风格
- 附件预览卡片圆角(12rpx)
2. **交互体验**:
- 附件添加后自动聚焦输入框
- 菜单弹出/关闭平滑过渡
- 附件删除按钮半透明背景
- 发送按钮禁用状态灰色
3. **移动端优化**:
- 使用`env(safe-area-inset-bottom)`适配全面屏
- 附件预览区域水平滚动
- 键盘弹出时自动调整位置
- 文件名智能截断(避免溢出)
### 使用示例
```vue
<template>
<view class="container">
<!-- 页面内容 -->
<scroll-view class="content">
<!-- 审批内容 -->
</scroll-view>
<!-- 底部输入框 -->
<wechat-style-input
ref="inputBox"
@send="handleSend"
/>
</view>
</template>
<script>
import WechatStyleInput from '@/components/wechat-style-input.vue';
export default {
components: { WechatStyleInput },
methods: {
// 处理发送内容
handleSend(payload) {
console.log("发送内容:", payload.content);
console.log("附件列表:", payload.attachments);
// 实际业务中发送到服务器
uni.request({
url: 'https://your-api.com/submit',
method: 'POST',
data: payload,
success: () => {
uni.showToast({ title: '发送成功' });
}
});
},
// 需要时手动聚焦输入框
focusInput() {
this.$refs.inputBox.focusInput();
}
}
};
</script>
<style>
.container {
padding-bottom: 120px; /* 为底部输入框预留空间 */
}
.content {
height: calc(100vh - 120px);
}
</style>
```
### 实际应用注意事项
1. **文件上传实现**:
```javascript
// 实际文件上传方法
async uploadFile(file) {
try {
const res = await uni.uploadFile({
url: 'https://your-api.com/upload',
filePath: file.path,
name: 'file',
formData: {
type: file.type,
name: file.name
}
});
return JSON.parse(res.data).url;
} catch (error) {
console.error('上传失败:', error);
uni.showToast({ title: '上传失败', icon: 'none' });
return null;
}
}
```
2. **性能优化建议**:
- 大文件分片上传
- 图片压缩后上传
- 使用Web Worker处理文件
- 设置上传超时和重试机制
3. **企业微信特色功能扩展**:
- 添加@成员功能
- 支持表情符号选择
- 添加语音输入功能
- 实现消息撤回功能
阅读全文