feat: 添加图片附件功能到常用语,支持上传和预览

This commit is contained in:
Jafeng 2026-05-20 17:06:38 +08:00
parent 957ccf8b47
commit b95f57f1c1
2 changed files with 157 additions and 7 deletions

View File

@ -82,6 +82,16 @@
已收藏
</view>
</view>
<view v-if="phrase.files && phrase.files.length" class="phrase-images">
<image
v-for="(file, fileIndex) in phrase.files"
:key="file.url || fileIndex"
class="phrase-image"
:src="file.url"
mode="aspectFill"
@click.stop="previewFiles(phrase.files, fileIndex)"
/>
</view>
<view class="phrase-actions">
<view class="action-btn send" @click.stop="sendPhrase(phrase)">
<text class="action-icon"></text>
@ -168,6 +178,23 @@
:auto-height="false"
></textarea>
<view class="char-count">{{ phraseForm.content.length }}/500</view>
<view class="form-label content-label">图片附件</view>
<view class="image-grid">
<view
v-for="(file, index) in phraseForm.files"
:key="file.url || index"
class="image-item"
@click="previewFiles(phraseForm.files, index)"
>
<image class="image-thumb" :src="file.url" mode="aspectFill" />
<view class="image-remove" @click.stop="removePhraseImage(index)">×</view>
</view>
<view v-if="phraseForm.files.length < 9" class="image-add" @click="addPhraseImage">
<text class="image-add-icon">+</text>
</view>
</view>
<view class="popup-actions">
<button class="cancel-btn" @click="closePopup">取消</button>
<button class="confirm-btn" @click="savePhrase">保存</button>
@ -200,6 +227,16 @@
<view class="form-hint">收藏后将添加到所选目录</view>
<view class="collect-preview">{{ collectingPhrase?.content || "" }}</view>
<view v-if="collectingPhrase?.files && collectingPhrase.files.length" class="phrase-images collect-images">
<image
v-for="(file, fileIndex) in collectingPhrase.files"
:key="file.url || fileIndex"
class="phrase-image"
:src="file.url"
mode="aspectFill"
@click.stop="previewFiles(collectingPhrase.files, fileIndex)"
/>
</view>
<view class="popup-actions">
<button class="cancel-btn" @click="closeCollectPopup">取消</button>
@ -239,6 +276,7 @@ import { ref, computed, onMounted } from "vue";
import { storeToRefs } from "pinia";
import api from "@/utils/api";
import useAccountStore from "@/store/account";
import { chooseAndUploadImage, normalizeFileUrl } from "@/utils/file";
const { doctorInfo } = storeToRefs(useAccountStore());
@ -262,6 +300,7 @@ const collectingPhrase = ref(null);
const phraseForm = ref({
content: "",
cateId: "",
files: [],
});
const collectForm = ref({
@ -348,12 +387,29 @@ const normalizeCategory = (item, categoryType) => ({
deletable: categoryType === "user",
});
const normalizeFiles = (files) => {
if (!Array.isArray(files)) return [];
return files
.map((item) => {
if (typeof item === "string") return { type: "image", url: normalizeFileUrl(item) };
const url = item?.url || item?.URL || item?.download_url;
if (!url) return null;
return {
type: item.type || "image",
url: normalizeFileUrl(url),
name: item.name || item.fileName || "",
};
})
.filter(Boolean);
};
const normalizePhrase = (item, phraseType) => ({
id: item._id || item.id,
cateId: item.cateId || item.categoryId,
content: item.content || "",
createTime: item.createTime,
updateTime: item.updateTime,
files: normalizeFiles(item.files),
sourceType: item.sourceType,
sourceCommonWordId: item.sourceCommonWordId,
sourceCateId: item.sourceCateId,
@ -398,7 +454,7 @@ const sendPhrase = (phrase) => {
const prevPage = pages[pages.length - 2];
if (prevPage) {
prevPage.$vm.sendCommonPhrase(phrase.content);
prevPage.$vm.sendCommonPhrase(phrase);
}
uni.navigateBack();
@ -409,6 +465,7 @@ const showAddPhraseDialog = () => {
phraseForm.value = {
content: "",
cateId: currentCategory.value || firstMyCategory.value?.id || "",
files: [],
};
showPhrasePopup.value = true;
};
@ -418,6 +475,7 @@ const editPhrase = (phrase) => {
phraseForm.value = {
content: phrase.content,
cateId: phrase.cateId || firstMyCategory.value?.id || "",
files: normalizeFiles(phrase.files),
};
showPhrasePopup.value = true;
};
@ -427,6 +485,26 @@ const handlePhraseCategoryChange = (event) => {
phraseForm.value.cateId = myCategories.value[index]?.id || "";
};
const previewFiles = (files, index = 0) => {
const urls = normalizeFiles(files).map((item) => item.url).filter(Boolean);
if (!urls.length) return;
uni.previewImage({ urls, current: urls[index] || urls[0] });
};
const addPhraseImage = async () => {
if (phraseForm.value.files.length >= 9) {
uni.showToast({ title: "最多上传9张图片", icon: "none" });
return;
}
const url = await chooseAndUploadImage({ count: 1 });
if (!url) return;
phraseForm.value.files.push({ type: "image", url: normalizeFileUrl(url), name: "图片" });
};
const removePhraseImage = (index) => {
phraseForm.value.files.splice(index, 1);
};
const savePhrase = async () => {
if (!phraseForm.value.cateId) {
uni.showToast({ title: "请选择目录", icon: "none" });
@ -448,6 +526,7 @@ const savePhrase = async () => {
id: editingPhrase.value?.id,
cateId: phraseForm.value.cateId,
content: phraseForm.value.content,
files: normalizeFiles(phraseForm.value.files),
corpId,
userId,
});
@ -459,6 +538,7 @@ const savePhrase = async () => {
_id: editingPhrase.value?.id || result.data?._id,
cateId: phraseForm.value.cateId,
content: phraseForm.value.content.trim(),
files: normalizeFiles(phraseForm.value.files),
}, "user");
if (editingPhrase.value) {
@ -583,6 +663,7 @@ const confirmFavorite = async () => {
sourceCommonWordId: phrase.id,
sourceCateId: phrase.cateId,
collectClient: "wxapp",
files: normalizeFiles(phrase.files),
});
if (result.success && result.data) {
@ -1071,6 +1152,20 @@ $border-color: #edf0f5;
}
}
.phrase-images {
display: flex;
flex-wrap: wrap;
gap: 12rpx;
margin-top: 18rpx;
}
.phrase-image {
width: 96rpx;
height: 96rpx;
border-radius: 10rpx;
background: #f2f4f7;
}
.collect-tip {
padding: 8rpx 0 28rpx;
color: #98a2b3;
@ -1235,6 +1330,60 @@ $border-color: #edf0f5;
border: 1px solid $border-color;
}
.collect-images {
margin: -14rpx 0 32rpx;
}
.image-grid {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
margin-bottom: 30rpx;
}
.image-item,
.image-add {
position: relative;
width: 142rpx;
height: 142rpx;
border-radius: 12rpx;
overflow: hidden;
background: #f7f8fb;
}
.image-thumb {
width: 100%;
height: 100%;
}
.image-remove {
position: absolute;
top: 6rpx;
right: 6rpx;
width: 34rpx;
height: 34rpx;
line-height: 34rpx;
text-align: center;
color: #fff;
font-size: 26rpx;
border-radius: 50%;
background: rgba(0, 0, 0, 0.55);
}
.image-add {
display: flex;
align-items: center;
justify-content: center;
color: #98a2b3;
border: 1px dashed #cfd3dc;
box-sizing: border-box;
}
.image-add-icon {
font-size: 52rpx;
line-height: 52rpx;
}
.phrase-textarea {
width: 100%;
height: 220rpx;

View File

@ -992,12 +992,13 @@ onHide(() => {
console.log("✓ 页面隐藏已清空当前会话ID");
});
const sendCommonPhrase = (content) => {
if (chatInputRef.value) {
//
chatInputRef.value.setInputText(content);
}
};
const sendCommonPhrase = (phraseOrContent) => {
const content = typeof phraseOrContent === "string" ? phraseOrContent : phraseOrContent?.content || "";
if (chatInputRef.value) {
//
chatInputRef.value.setInputText(content);
}
};
//
const handleStreamText = (char) => {