feat: 添加图片附件功能到常用语,支持上传和预览
This commit is contained in:
parent
957ccf8b47
commit
b95f57f1c1
@ -82,6 +82,16 @@
|
|||||||
已收藏
|
已收藏
|
||||||
</view>
|
</view>
|
||||||
</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="phrase-actions">
|
||||||
<view class="action-btn send" @click.stop="sendPhrase(phrase)">
|
<view class="action-btn send" @click.stop="sendPhrase(phrase)">
|
||||||
<text class="action-icon">✈</text>
|
<text class="action-icon">✈</text>
|
||||||
@ -168,6 +178,23 @@
|
|||||||
:auto-height="false"
|
:auto-height="false"
|
||||||
></textarea>
|
></textarea>
|
||||||
<view class="char-count">{{ phraseForm.content.length }}/500</view>
|
<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">
|
<view class="popup-actions">
|
||||||
<button class="cancel-btn" @click="closePopup">取消</button>
|
<button class="cancel-btn" @click="closePopup">取消</button>
|
||||||
<button class="confirm-btn" @click="savePhrase">保存</button>
|
<button class="confirm-btn" @click="savePhrase">保存</button>
|
||||||
@ -200,6 +227,16 @@
|
|||||||
<view class="form-hint">收藏后将添加到所选目录</view>
|
<view class="form-hint">收藏后将添加到所选目录</view>
|
||||||
|
|
||||||
<view class="collect-preview">{{ collectingPhrase?.content || "" }}</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">
|
<view class="popup-actions">
|
||||||
<button class="cancel-btn" @click="closeCollectPopup">取消</button>
|
<button class="cancel-btn" @click="closeCollectPopup">取消</button>
|
||||||
@ -239,6 +276,7 @@ import { ref, computed, onMounted } from "vue";
|
|||||||
import { storeToRefs } from "pinia";
|
import { storeToRefs } from "pinia";
|
||||||
import api from "@/utils/api";
|
import api from "@/utils/api";
|
||||||
import useAccountStore from "@/store/account";
|
import useAccountStore from "@/store/account";
|
||||||
|
import { chooseAndUploadImage, normalizeFileUrl } from "@/utils/file";
|
||||||
|
|
||||||
const { doctorInfo } = storeToRefs(useAccountStore());
|
const { doctorInfo } = storeToRefs(useAccountStore());
|
||||||
|
|
||||||
@ -262,6 +300,7 @@ const collectingPhrase = ref(null);
|
|||||||
const phraseForm = ref({
|
const phraseForm = ref({
|
||||||
content: "",
|
content: "",
|
||||||
cateId: "",
|
cateId: "",
|
||||||
|
files: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
const collectForm = ref({
|
const collectForm = ref({
|
||||||
@ -348,12 +387,29 @@ const normalizeCategory = (item, categoryType) => ({
|
|||||||
deletable: categoryType === "user",
|
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) => ({
|
const normalizePhrase = (item, phraseType) => ({
|
||||||
id: item._id || item.id,
|
id: item._id || item.id,
|
||||||
cateId: item.cateId || item.categoryId,
|
cateId: item.cateId || item.categoryId,
|
||||||
content: item.content || "",
|
content: item.content || "",
|
||||||
createTime: item.createTime,
|
createTime: item.createTime,
|
||||||
updateTime: item.updateTime,
|
updateTime: item.updateTime,
|
||||||
|
files: normalizeFiles(item.files),
|
||||||
sourceType: item.sourceType,
|
sourceType: item.sourceType,
|
||||||
sourceCommonWordId: item.sourceCommonWordId,
|
sourceCommonWordId: item.sourceCommonWordId,
|
||||||
sourceCateId: item.sourceCateId,
|
sourceCateId: item.sourceCateId,
|
||||||
@ -398,7 +454,7 @@ const sendPhrase = (phrase) => {
|
|||||||
const prevPage = pages[pages.length - 2];
|
const prevPage = pages[pages.length - 2];
|
||||||
|
|
||||||
if (prevPage) {
|
if (prevPage) {
|
||||||
prevPage.$vm.sendCommonPhrase(phrase.content);
|
prevPage.$vm.sendCommonPhrase(phrase);
|
||||||
}
|
}
|
||||||
|
|
||||||
uni.navigateBack();
|
uni.navigateBack();
|
||||||
@ -409,6 +465,7 @@ const showAddPhraseDialog = () => {
|
|||||||
phraseForm.value = {
|
phraseForm.value = {
|
||||||
content: "",
|
content: "",
|
||||||
cateId: currentCategory.value || firstMyCategory.value?.id || "",
|
cateId: currentCategory.value || firstMyCategory.value?.id || "",
|
||||||
|
files: [],
|
||||||
};
|
};
|
||||||
showPhrasePopup.value = true;
|
showPhrasePopup.value = true;
|
||||||
};
|
};
|
||||||
@ -418,6 +475,7 @@ const editPhrase = (phrase) => {
|
|||||||
phraseForm.value = {
|
phraseForm.value = {
|
||||||
content: phrase.content,
|
content: phrase.content,
|
||||||
cateId: phrase.cateId || firstMyCategory.value?.id || "",
|
cateId: phrase.cateId || firstMyCategory.value?.id || "",
|
||||||
|
files: normalizeFiles(phrase.files),
|
||||||
};
|
};
|
||||||
showPhrasePopup.value = true;
|
showPhrasePopup.value = true;
|
||||||
};
|
};
|
||||||
@ -427,6 +485,26 @@ const handlePhraseCategoryChange = (event) => {
|
|||||||
phraseForm.value.cateId = myCategories.value[index]?.id || "";
|
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 () => {
|
const savePhrase = async () => {
|
||||||
if (!phraseForm.value.cateId) {
|
if (!phraseForm.value.cateId) {
|
||||||
uni.showToast({ title: "请选择目录", icon: "none" });
|
uni.showToast({ title: "请选择目录", icon: "none" });
|
||||||
@ -448,6 +526,7 @@ const savePhrase = async () => {
|
|||||||
id: editingPhrase.value?.id,
|
id: editingPhrase.value?.id,
|
||||||
cateId: phraseForm.value.cateId,
|
cateId: phraseForm.value.cateId,
|
||||||
content: phraseForm.value.content,
|
content: phraseForm.value.content,
|
||||||
|
files: normalizeFiles(phraseForm.value.files),
|
||||||
corpId,
|
corpId,
|
||||||
userId,
|
userId,
|
||||||
});
|
});
|
||||||
@ -459,6 +538,7 @@ const savePhrase = async () => {
|
|||||||
_id: editingPhrase.value?.id || result.data?._id,
|
_id: editingPhrase.value?.id || result.data?._id,
|
||||||
cateId: phraseForm.value.cateId,
|
cateId: phraseForm.value.cateId,
|
||||||
content: phraseForm.value.content.trim(),
|
content: phraseForm.value.content.trim(),
|
||||||
|
files: normalizeFiles(phraseForm.value.files),
|
||||||
}, "user");
|
}, "user");
|
||||||
|
|
||||||
if (editingPhrase.value) {
|
if (editingPhrase.value) {
|
||||||
@ -583,6 +663,7 @@ const confirmFavorite = async () => {
|
|||||||
sourceCommonWordId: phrase.id,
|
sourceCommonWordId: phrase.id,
|
||||||
sourceCateId: phrase.cateId,
|
sourceCateId: phrase.cateId,
|
||||||
collectClient: "wxapp",
|
collectClient: "wxapp",
|
||||||
|
files: normalizeFiles(phrase.files),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result.success && result.data) {
|
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 {
|
.collect-tip {
|
||||||
padding: 8rpx 0 28rpx;
|
padding: 8rpx 0 28rpx;
|
||||||
color: #98a2b3;
|
color: #98a2b3;
|
||||||
@ -1235,6 +1330,60 @@ $border-color: #edf0f5;
|
|||||||
border: 1px solid $border-color;
|
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 {
|
.phrase-textarea {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 220rpx;
|
height: 220rpx;
|
||||||
|
|||||||
@ -992,12 +992,13 @@ onHide(() => {
|
|||||||
console.log("✓ 页面隐藏,已清空当前会话ID");
|
console.log("✓ 页面隐藏,已清空当前会话ID");
|
||||||
});
|
});
|
||||||
|
|
||||||
const sendCommonPhrase = (content) => {
|
const sendCommonPhrase = (phraseOrContent) => {
|
||||||
if (chatInputRef.value) {
|
const content = typeof phraseOrContent === "string" ? phraseOrContent : phraseOrContent?.content || "";
|
||||||
// 覆盖输入框内容,而不是直接发送
|
if (chatInputRef.value) {
|
||||||
chatInputRef.value.setInputText(content);
|
// 覆盖输入框内容,而不是直接发送
|
||||||
}
|
chatInputRef.value.setInputText(content);
|
||||||
};
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 处理流式文本输入
|
// 处理流式文本输入
|
||||||
const handleStreamText = (char) => {
|
const handleStreamText = (char) => {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user