一、为什么你写的textarea总在摸鱼?
还记得被表单支配的恐惧吗?刚学Vue时,我天真地以为所有输入框都是input的天下。直到产品经理微笑着说出那句经典台词:“这里能不能让用户多输点文字?比如500字?”
然后我写下了这段祖传代码:
<input type="text" v-model="message" placeholder="快来吐槽吧!">
结果?用户反馈:“你们这个输入框是给蚂蚁用的吗?打两行字就没了!”
这时候,textarea就像救世主一样登场了!这货简直就是输入框里的加长林肯——空间大、坐着舒服,专门对付话痨用户。
但问题是,很多新手拿到textarea后,依然在用input的思维去操作它。最常见的翻车现场:
<!-- 错误示范:这货根本不是这么玩的! -->
<textarea v-model="message" value="初始值"></textarea>
兄弟,textarea是自闭标签啊!它不像input那样用value属性,内容得写在标签中间:
<!-- 正确姿势 -->
<textarea v-model="message">这里是初始值</textarea>
不过,在Vue里我们更推荐用v-model统一管理,这才是真·高效做法。
二、v-model和textarea的“相亲”现场
v-model遇见textarea,就像西红柿遇见了鸡蛋——天生一对。来看看它们怎么擦出火花的:
基础绑定(小白版):
<template>
<div>
<h3>吐槽箱(当前字数:{{ message.length }})</h3>
<textarea
v-model="message"
placeholder="有什么不爽的,尽管吐出来吧!"
rows="4"
cols="50"
></textarea>
<p>你的吐槽预览:{{ message }}</p>
</div>
</template>
<script>
export default {
data() {
return {
message: ''
}
}
}
</script>
就这?就这?你以为这就完了?图样图森破!真正的战场才刚刚开始。
三、textarea的“装备升级”之路
1. 自动增高——拒绝滚动条噩梦
用户哐哐哐输入小作文,结果还要在狭小的文本框里滚来滚去?不存在的!
<template>
<div>
<textarea
v-model="message"
@input="autoResize"
ref="myTextarea"
placeholder="随便写,这框会自己变大..."
style="resize: none; overflow: hidden;"
></textarea>
</div>
</template>
<script>
export default {
methods: {
autoResize() {
const textarea = this.$refs.myTextarea;
textarea.style.height = 'auto';
textarea.style.height = textarea.scrollHeight + 'px';
}
}
}
</script>
这招简直绝了!textarea会根据内容自动调整高度,用户体验直接拉满。
2. 字数统计——防患于未然
<template>
<div>
<textarea
v-model="message"
:maxlength="maxLength"
placeholder="最多输入{{ maxLength }}字"
></textarea>
<p :class="{'warning': isNearLimit}">
字数:{{ message.length }} / {{ maxLength }}
<span v-if="isNearLimit" style="color: orange;">快超了!手下留情!</span>
</p>
</div>
</template>
<script>
export default {
data() {
return {
message: '',
maxLength: 200
}
},
computed: {
isNearLimit() {
return this.message.length > this.maxLength * 0.8;
}
}
}
</script>
3. 防手抖提交——给用户反悔的机会
<template>
<div>
<textarea v-model="draftMessage" placeholder="写下你的心声..."></textarea>
<button @click="saveDraft">存草稿</button>
<button @click="submit" :disabled="!draftMessage">提交</button>
<!-- 草稿列表 -->
<div v-if="drafts.length">
<h4>草稿箱(点击继续编辑)</h4>
<div
v-for="draft in drafts"
:key="draft.id"
@click="loadDraft(draft)"
class="draft-item"
>
{{ draft.content.substring(0, 30) }}...
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
draftMessage: '',
drafts: [],
nextId: 1
}
},
methods: {
saveDraft() {
if (!this.draftMessage.trim()) return;
this.drafts.push({
id: this.nextId++,
content: this.draftMessage,
time: new Date().toLocaleString()
});
alert('草稿保存成功!');
},
loadDraft(draft) {
this.draftMessage = draft.content;
},
submit() {
if (!this.draftMessage.trim()) return;
// 这里写提交逻辑
console.log('提交内容:', this.draftMessage);
alert('提交成功!');
this.draftMessage = '';
}
}
}
</script>
四、进阶玩法——让你的textarea秀起来
1. Markdown实时预览
<template>
<div class="markdown-editor">
<div class="editor-panel">
<textarea
v-model="markdownText"
placeholder="输入Markdown语法..."
@input="updatePreview"
></textarea>
</div>
<div class="preview-panel">
<div v-html="compiledMarkdown"></div>
</div>
</div>
</template>
<script>
// 简单Markdown解析(实际项目建议使用marked.js等库)
export default {
data() {
return {
markdownText: '# 标题\n\n写点什么吧...',
}
},
computed: {
compiledMarkdown() {
return this.markdownText
.replace(/# (.*)/g, '<h1>$1</h1>')
.replace(/\*\*(.*)\*\*/g, '<strong>$1</strong>')
.replace(/\n/g, '<br>');
}
}
}
</script>
<style scoped>
.markdown-editor {
display: flex;
gap: 20px;
}
.editor-panel, .preview-panel {
flex: 1;
border: 1px solid #ccc;
padding: 10px;
min-height: 300px;
}
</style>
2. @提及功能
<template>
<div>
<textarea
v-model="commentText"
@input="checkMention"
@keydown.tab.prevent="completeMention"
ref="commentArea"
placeholder="输入@提及用户..."
></textarea>
<div v-if="showUserList" class="user-list">
<div
v-for="user in filteredUsers"
:key="user.id"
@click="selectUser(user)"
class="user-item"
>
{{ user.name }}
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
commentText: '',
showUserList: false,
users: [
{ id: 1, name: '张三' },
{ id: 2, name: '李四' },
{ id: 3, name: '王五' }
],
currentMentionStart: -1
}
},
computed: {
filteredUsers() {
if (this.currentMentionStart === -1) return [];
const currentText = this.commentText.substring(this.currentMentionStart + 1);
return this.users.filter(user =>
user.name.includes(currentText)
);
}
},
methods: {
checkMention() {
const cursorPos = this.$refs.commentArea.selectionStart;
const textBeforeCursor = this.commentText.substring(0, cursorPos);
const lastAtPos = textBeforeCursor.lastIndexOf('@');
if (lastAtPos > -1 && /[\s]@[\w]*$/.test(textBeforeCursor)) {
this.currentMentionStart = lastAtPos;
this.showUserList = true;
} else {
this.showUserList = false;
this.currentMentionStart = -1;
}
},
selectUser(user) {
const textBefore = this.commentText.substring(0, this.currentMentionStart);
const textAfter = this.commentText.substring(this.$refs.commentArea.selectionStart);
this.commentText = textBefore + '@' + user.name + ' ' + textAfter;
this.showUserList = false;
this.currentMentionStart = -1;
this.$nextTick(() => {
this.$refs.commentArea.focus();
});
},
completeMention() {
if (this.filteredUsers.length > 0) {
this.selectUser(this.filteredUsers[0]);
}
}
}
}
</script>
五、避坑指南——那些年我们踩过的textarea坑
坑1:v-model和value属性混用
<!-- 大坑!Vue会发出警告 -->
<textarea v-model="message" value="初始值"></textarea>
<!-- 正确姿势 -->
<textarea v-model="message">{{ message }}</textarea>
坑2:忘记处理空白字符
// 用户可能输入一堆空格
submit() {
// 错误:直接提交
// axios.post('/api', { content: this.message })
// 正确:先trim一下
const content = this.message.trim();
if (!content) {
alert('请输入有效内容!');
return;
}
// 再提交
}
坑3:移动端适配问题
<textarea
v-model="message"
:rows="isMobile ? 3 : 5"
:class="{ 'mobile-textarea': isMobile }"
@focus="handleFocus"
></textarea>
六、完整实战:打造一个智能评论框
是时候展示真正的技术了!我们来搞一个功能齐全的评论框:
<template>
<div class="smart-comment">
<!-- 工具栏 -->
<div class="toolbar">
<button @click="insertText('**粗体**')">粗体</button>
<button @click="insertText('*斜体*')">斜体</button>
<button @click="insertText('`代码`')">代码</button>
<button @click="showEmoji = !showEmoji">😊</button>
</div>
<!-- 表情选择 -->
<div v-if="showEmoji" class="emoji-picker">
<span
v-for="emoji in emojis"
:key="emoji"
@click="insertText(emoji)"
class="emoji"
>{{ emoji }}</span>
</div>
<!-- 文本输入区 -->
<textarea
v-model="comment"
ref="textarea"
:placeholder="placeholder"
@input="handleInput"
@paste="handlePaste"
:maxlength="maxLength"
class="comment-textarea"
></textarea>
<!-- 底部信息 -->
<div class="footer">
<span :class="{
'normal': lengthRatio < 0.8,
'warning': lengthRatio >= 0.8,
'danger': lengthRatio >= 0.95
}">
{{ comment.length }} / {{ maxLength }}
</span>
<button
@click="submitComment"
:disabled="!canSubmit"
class="submit-btn"
>
{{ submitting ? '提交中...' : '发表评论' }}
</button>
</div>
<!-- 实时预览 -->
<div v-if="comment" class="preview">
<h4>预览:</h4>
<div class="preview-content">{{ comment }}</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
comment: '',
showEmoji: false,
submitting: false,
maxLength: 1000,
emojis: ['😊', '😂', '🤔', '👍', '❤️', '🔥', '👏', '🎉']
}
},
computed: {
lengthRatio() {
return this.comment.length / this.maxLength;
},
canSubmit() {
return this.comment.trim().length > 0 &&
this.comment.length <= this.maxLength &&
!this.submitting;
},
placeholder() {
const placeholders = [
'说点什么吧...',
'分享你的想法...',
'这里可以畅所欲言...',
'友好的讨论是交流的第一步...'
];
return placeholders[Math.floor(Math.random() * placeholders.length)];
}
},
methods: {
handleInput() {
// 实时保存到本地存储
localStorage.setItem('commentDraft', this.comment);
},
insertText(text) {
const textarea = this.$refs.textarea;
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
this.comment = this.comment.substring(0, start) +
text +
this.comment.substring(end);
// 移动光标到插入内容后
this.$nextTick(() => {
textarea.focus();
textarea.setSelectionRange(start + text.length, start + text.length);
});
},
handlePaste(event) {
// 处理粘贴文本,比如清理格式等
const pastedText = event.clipboardData.getData('text');
event.preventDefault();
// 这里可以添加文本处理逻辑
document.execCommand('insertText', false, pastedText);
},
async submitComment() {
this.submitting = true;
try {
// 模拟API调用
await new Promise(resolve => setTimeout(resolve, 1000));
console.log('提交评论:', this.comment);
alert('评论发表成功!');
// 清空内容
this.comment = '';
localStorage.removeItem('commentDraft');
} catch (error) {
console.error('提交失败:', error);
alert('提交失败,请重试!');
} finally {
this.submitting = false;
}
}
},
mounted() {
// 加载草稿
const draft = localStorage.getItem('commentDraft');
if (draft) {
this.comment = draft;
}
}
}
</script>
<style scoped>
.smart-comment {
border: 1px solid #e1e1e1;
border-radius: 8px;
padding: 16px;
background: white;
}
.toolbar {
margin-bottom: 10px;
}
.toolbar button {
margin-right: 8px;
padding: 4px 8px;
border: 1px solid #ddd;
background: white;
border-radius: 4px;
cursor: pointer;
}
.emoji-picker {
margin-bottom: 10px;
padding: 8px;
border: 1px solid #e1e1e1;
border-radius: 4px;
}
.emoji {
cursor: pointer;
margin-right: 4px;
font-size: 1.2em;
}
.comment-textarea {
width: 100%;
min-height: 100px;
padding: 12px;
border: 1px solid #e1e1e1;
border-radius: 4px;
font-family: inherit;
resize: vertical;
}
.footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 10px;
}
.normal { color: #666; }
.warning { color: orange; }
.danger { color: red; }
.submit-btn {
padding: 8px 16px;
background: #007cba;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.submit-btn:disabled {
background: #ccc;
cursor: not-allowed;
}
.preview {
margin-top: 16px;
padding: 12px;
border: 1px dashed #e1e1e1;
border-radius: 4px;
}
.preview-content {
white-space: pre-wrap;
}
</style>
结语
看到这里,你是不是对textarea刮目相看了?这货根本不是什么简单的多行输入框,而是一个可以玩出各种花样的瑞士军刀!
从最基本的v-model绑定,到自动增高、字数统计、Markdown预览、@提及功能,再到完整的智能评论框——textarea的潜力远超你的想象。
记住,好的表单体验不是让用户去适应你的代码,而是让你的代码去适应用户的习惯。下次遇到多文本输入场景时,别再只会用input了,拿出你的textarea绝活,让用户体验直接起飞!
6909

被折叠的 条评论
为什么被折叠?



