2025-02-14
前言:这一篇日志中包含了我实现录入书籍这一功能的过程。当时第一次写这个功能的时候很折腾,写了好几天。这一次一个下午就搞定了。
要实现这个功能,就是要将用户输入的数据传入到页面本地的变量中,然后再向云数据库上传表单中的数据。
不过我没有使用 <form>
这个组件。<form>
绑定的提交函数按理来说会传入子组件中表单组件的 value 值,但我的提交函数传入的内容是空的,仿佛里面没有组件一样。难道我自定义的组件不能作为表单组件被识别?
添加录入书籍功能
之前我在个人页面中添加了“录入书籍”的功能图标,点击这个图标需要能够跳转到“书籍录入”的页面:
我使用 <navigator>
包裹 <uni-grid-item>
,实现了点击图标跳转到相应页面的功能。
<navigator url="/pages/admin/add/add" hover-class="none"> <!-- 将 hover-class 设置为none,避免了点击 navigator 的样式与点击 uni-grid-item 的样式重合 -->
<uni-grid-item>
<view class="cm-grid-item-box">
<uni-icons type="plusempty" :size="30"></uni-icons>
<text class="text">录入书籍</text>
</view>
</uni-grid-item>
</navigator>
解决了一个关于 v-model 的报错
跳转到书籍录入页面之后发现有一个报错:
[Vue warn]: Property "imageValue" was accessed during render but is not defined on instance. \n at <CmFilePicker title="封面" > \n at <CmInput >
看起来和之前添加的“图片上传器”有关:
<view class="cm-form-input">
<view class="title">{{title}}</view>
<uni-file-picker
v-model="imageValue"
fileMediatype="image"
mode="grid"
/>
</view>
我之前从官网复制了 uni-file-picker 的示例过来,还没细想 v-model 的作用……
v-model
是 Vue.js 中的一个指令,用于在表单控件元素(如<input>
、<textarea>
、<select>
)和 Vue 实例的数据之间创建双向数据绑定。这意味着当表单控件的值发生变化时,Vue 实例中的数据也会相应地更新,反之亦然。
原来是用于数据绑定的。我并没有定义 imageValue 这个变量,不报错就怪了。
我接下来要给表单添加功能了,也许 v-model 很快就会用到。
添加扫码功能
参考官网的这篇文档:https://uniapp.dcloud.net.cn/api/system/barcode.html
给 cm-input 组件补充了右侧图标的点击事件 scan。
scan: function() {
console.log('启动扫码...');
uni.scanCode({
scanType: ["barCode"],
success: function(res) {
console.log("扫码结果:", res.result);
}
})
}
在 scanCode 函数内,this 不会指向在页面的 data() 中定义的变量。因此要对本地的变量进行操作,就要使用 var that = this
的写法:
scan: function() {
console.log('启动扫码...');
var that = this; // 1
uni.scanCode({
scanType: ["barCode"],
success: function(res) {
console.log("扫码结果:", res.result);
that.isbn = res.result; // 2
}
})
}
扫码之后,会将得到的书籍 ISBN 号码填写到表单中的 ISBN 这一项。
要注意的是,显示在表单上的数据其实是自定义组件 <cm-input>
中的 <input>
组件中的参数—— value
。
<cm-input title="ISBN" placeholder="扫描或输入ISBN" :value="isbn" r_icon="scan" icon_color="#A00000" :on_icon_click="bindScanIconClicked"></cm-input>
我使用 v-bind,将 value
绑定到了页面中的变量 isbn
上。这个绑定是单向的,也就是 isbn
的值的修改会同步改变 value
值,但是修改 value
值不会影响 isbn
。
信息的传递路径如下:
- 扫码得到的数据 =>
isbn
=>value
=> 显示在页面上的数据
但是会有这样一种情况
- 用户手动输入 ISBN 数据 =>
value
=> 不会更新isbn
=> 点击提交表单的按钮 => 向数据库中的 ISBN 一项添加或修改数据
这种情况下, isbn
的数据不会更新。
这样看来,页面中的局部变量 isbn
只是修改数据 value
的一个接口。
关于通过 ISBN 查询书籍信息的问题
用户在扫描书籍条码成功,或者手动输入 ISBN 并按确定键之后,会自动触发 ISBN 的查询。
// 绑定扫码图标
bindScanIconClicked: function() {
console.log('启动扫码...');
var that = this;
uni.scanCode({
scanType: ["barCode"],
success: (res) => {
console.log("扫码结果:", res.result);
// 通过更新 isbn 来更新表单所显示的数据
that.isbn = res.result;
// 在扫码成功后自动进行 ISBN 搜索
that.searchISBN(that.isbn);
}
})
},
对于 <input>
组件,点击确定键后会触发的事件可以在 @confirm
中设置:
// 绑定在输入 ISBN 后点击确定键
bindISBNInputConfirmed: function(e) {
this.isbn = e.detail.value
console.log('用户输入了 ISBN:', this.isbn)
// 在用户按确认键后自动进行 ISBN 搜索
this.searchISBN(this.isbn)
},
进行 ISBN 搜索后,如果成功了,就更新页面显示的数据;如果失败了,就弹出提示,不更新数据。
关于图片文件上传
我之前自定义了一个组件 cm-file-picker
,这个组件其实就是给 uni-file-picker
添加了一点格式。 uni-file-picker
可以使用 @success
来设置在文件上传成功时执行的事件。uni-file-picker
会在文件上传成功时返回文件的地址。而在开通 uni-app
服务空间的情况下,这些文件会默认上传到服务空间中。
因此,我只需要获取所有图片文件的地址,并在随后将这些地址上传到数据库就行了。
这些地址,我使用一个本地变量 images
数组进行存放。
// 使用文件选择器上传图片成功后触发
onImageUploadSuccess: function(res) {
console.log('图片上传成功:', res.tempFilePaths);
this.images.push(...res.tempFilePaths); // 每次图片上传后,将 uni-file-picker 返回的图片地址数组拼接到 images 数组之后
console.log("当前图片组:", this.images);
}
同时,通过 ISBN 搜索到的书籍图片也可以添加到 images
数组中,预备上传到数据库。
// 通过isbn数据库查找当前isbn对应的书籍数据
searchISBN: function(isbn) {
console.log("查找当前isbn对应的书籍数据...")
var that = this;
uni.request({
url: "https://api.tanshuapi.com/api/isbn_base/v1/index",
data: {
key: that.apikey,
isbn: isbn
},
success: (res) => {
var bookdata = res.data.data;
console.log("请求成功:", bookdata);
that.title = bookdata.title
that.author = bookdata.author
that.publisher = bookdata.publisher
that.images.push(bookdata.img) // 通过网络搜索到图片之后,同样将其地址添加到 images 数组中
},
fail: () => {
console.log("请求失败!");
}
})
},
关于通过 ISBN 搜索到的书籍图片的显示
我们通过 ISBN 搜索到的书籍图片应该显示在哪里呢?我觉得还是“封面”这一栏里面好。我在自定义的 cm-file-picker 组件里面添加了一个 slot
,slot
中的组件仍然会受到父组件 display: flex
的影响,从而自动横向排列。
<template>
<view class="cm-form-input">
<view class="title">{{title}}</view>
<slot></slot>
<uni-file-picker
fileMediatype="image"
mode="grid"
@success="on_upload_success"
/>
</view>
</template>
效果是这样的:
然后我在 cm-file-picker
组件中添加了一个 image
组件用于显示网络图片。我新建了一个本地变量 image_search_res
作为图片资源地址,这个变量的默认值是默认显示的书籍图片。之后我稍微写了一下 image
组件的 CSS 来调整大小和排版。最后我添加了一个绑定图片点击行为的事件 onImageTapped()
,用来设置图片的预览。
<cm-file-picker title="封面" :on_upload_success="onImageUploadSuccess">
<image @tap="onImageTapped(image_search_res)" style="height: 132rpx; width: 132rpx; margin-right: 1rem;" :src="image_search_res">
</image>
</cm-file-picker>
onImageTapped()
函数可以接受当前图片的资源地址,并调用 uni.previewImage()
进行预览:
// 点击单张图片触发预览
onImageTapped: function(src) {
console.log("图片被点击:", src);
uni.previewImage({
current: 0,
urls: [src]
})
}
扩大了 cm-input 组件的右侧图标范围以防止误触
在手机上操作时发现点击扫码时会误触到旁边的输入框,所以我决定把 cm-input 的图标范围扩大一点:
<view class="r-icon">
<uni-icons :type="r_icon" :size="icon_size" :color="icon_color" @click="on_icon_click"></uni-icons>
</view>
...
<style scoped>
.r-icon {
/* background-color: yellow; */
display: flex;
justify-content: center;
width: 20%;
}
</style>
将表单中的数据上传到数据库
我没有使用 form
组件来获取表单数据,因为我发现用不了,目前还不清楚原因……
不过我将所有需要添加到数据库的数据包装成了一个 JS 对象,在点击”录入“按钮之后,这个对象会作为上传函数的参数:
// 点击“录入”按钮提交书籍数据
onFormSubmit: function() {
console.log("提交表单")
var bookdata = {
storage: this.addition,
isbn: this.isbn,
// ...
}
console.log("表单数据:", bookdata);
uniCloud.callFunction({
name: 'addBookData',
data: bookdata
}).then(res => {
console.log('addBookData 云函数返回信息:', res);
if (res.result.code === 200) {
console.log('添加成功');
} else {
console.error('添加失败:', res.result.msg);
}
}).catch(err => {
console.log('调用云函数失败:', err);
})
}
我使用云函数作为上传数据的函数。在项目的 uniCloud/cloudfunctions
文件夹图标上右键,可以选择”新建云函数/云对象“。
然后写好云函数。这个函数只包含一个向 library 数据库中添加一条数据的功能:
'use strict';
exports.main = async (event, context) => {
console.log('调用云函数 addBookData 传入参数:', event)
const db = uniCloud.database();
try {
const res = await db.collection('library').add(event);
return {
code: 200,
msg: '添加成功',
data: res
};
} catch (err) {
return {
code: 500,
msg: '添加失败',
error: err
};
}
};
写好之后记得上传云函数。
到此,录入书籍(而且是新书)的功能就基本实现了。之后我还会完善一些细节。