2025-03-28 15:19:42 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<view class="chat-container">
|
2026-03-16 14:28:38 +08:00
|
|
|
|
<!-- 选择模式操作栏 -->
|
|
|
|
|
|
<view v-if="isSelectMode" class="select-mode-header">
|
|
|
|
|
|
<view class="select-header-left">
|
|
|
|
|
|
<view class="select-count">{{ selectedMessages.length }} 条已选择</view>
|
|
|
|
|
|
<view class="select-all" @click="selectAllMessages">全选</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view class="select-header-right">
|
|
|
|
|
|
<view class="cancel-btn" @click="exitSelectMode">取消</view>
|
|
|
|
|
|
<view class="delete-btn" @click="deleteSelectedMessages" :disabled="selectedMessages.length === 0">删除</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
2026-02-03 14:50:56 +08:00
|
|
|
|
<!-- Tab切换 -->
|
2026-03-16 14:28:38 +08:00
|
|
|
|
<view v-else class="tab-container">
|
2026-02-03 14:50:56 +08:00
|
|
|
|
<view
|
|
|
|
|
|
class="tab-item"
|
|
|
|
|
|
:class="{ active: activeTab === 'policy' }"
|
|
|
|
|
|
@click="switchTab('policy')"
|
|
|
|
|
|
>
|
|
|
|
|
|
政策查询
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view
|
|
|
|
|
|
class="tab-item"
|
|
|
|
|
|
:class="{ active: activeTab === 'job' }"
|
|
|
|
|
|
@click="switchTab('job')"
|
|
|
|
|
|
>
|
|
|
|
|
|
岗位推荐
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
|
2025-05-13 11:42:17 +08:00
|
|
|
|
<!-- #ifdef MP-WEIXIN -->
|
2025-10-24 16:54:52 +08:00
|
|
|
|
<view class="chat-background" v-if="!messages.length">
|
|
|
|
|
|
<image class="backlogo" src="/static/icon/backAI.png"></image>
|
|
|
|
|
|
<view class="back-rowTitle">嗨!欢迎使用{{ config.appInfo.areaName }}AI智能求职</view>
|
|
|
|
|
|
<view class="back-rowText">
|
|
|
|
|
|
我可以根据您的简历和求职需求,帮你精准匹配{{ config.appInfo.areaName }}互联网招聘信息,对比招聘信息的优缺点,提供面试指导等,请把你的任务交给我吧~
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view class="back-rowh3">猜你所想</view>
|
|
|
|
|
|
<view
|
|
|
|
|
|
class="back-rowmsg button-click"
|
|
|
|
|
|
v-for="(item, index) in queries"
|
|
|
|
|
|
:key="index"
|
|
|
|
|
|
@click="sendMessageGuess(item)"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ item }}
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view class="chat-item self" v-if="isRecording">
|
|
|
|
|
|
<view class="message">{{ recognizedText }} {{ lastFinalText }}</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
2025-05-13 11:42:17 +08:00
|
|
|
|
<!-- #endif -->
|
|
|
|
|
|
<!-- #ifndef MP-WEIXIN -->
|
2025-10-24 16:54:52 +08:00
|
|
|
|
<view class="chat-background" v-if="!messages.length">
|
2025-04-16 14:24:06 +08:00
|
|
|
|
<image class="backlogo" src="/static/icon/backAI.png"></image>
|
2025-09-29 11:53:10 +08:00
|
|
|
|
<view class="back-rowTitle">嗨!欢迎使用{{ config.appInfo.areaName }}AI智能求职</view>
|
2025-04-16 14:24:06 +08:00
|
|
|
|
<view class="back-rowText">
|
2025-09-29 11:53:10 +08:00
|
|
|
|
我可以根据您的简历和求职需求,帮你精准匹配{{ config.appInfo.areaName }}互联网招聘信息,对比招聘信息的优缺点,提供面试指导等,请把你的任务交给我吧~
|
2025-04-16 14:24:06 +08:00
|
|
|
|
</view>
|
|
|
|
|
|
<view class="back-rowh3">猜你所想</view>
|
2025-05-13 11:10:38 +08:00
|
|
|
|
<view
|
|
|
|
|
|
class="back-rowmsg button-click"
|
|
|
|
|
|
v-for="(item, index) in queries"
|
|
|
|
|
|
:key="index"
|
|
|
|
|
|
@click="sendMessageGuess(item)"
|
|
|
|
|
|
>
|
2025-04-16 14:24:06 +08:00
|
|
|
|
{{ item }}
|
2025-03-28 15:19:42 +08:00
|
|
|
|
</view>
|
2025-04-16 14:24:06 +08:00
|
|
|
|
<view class="chat-item self" v-if="isRecording">
|
|
|
|
|
|
<view class="message">{{ recognizedText }} {{ lastFinalText }}</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
2025-10-24 16:54:52 +08:00
|
|
|
|
<!-- #endif -->
|
2025-03-28 15:19:42 +08:00
|
|
|
|
<scroll-view class="chat-list scrollView" :scroll-top="scrollTop" :scroll-y="true" scroll-with-animation>
|
2025-05-13 11:42:17 +08:00
|
|
|
|
<!-- #ifdef MP-WEIXIN -->
|
|
|
|
|
|
<view class="chat-list list-content">
|
|
|
|
|
|
<!-- #endif -->
|
|
|
|
|
|
<!-- #ifndef MP-WEIXIN -->
|
2025-04-16 14:24:06 +08:00
|
|
|
|
<view class="chat-list list-content" v-fade:600="messages.length >= 1">
|
2025-05-13 11:42:17 +08:00
|
|
|
|
<!-- #endif -->
|
2025-04-16 14:24:06 +08:00
|
|
|
|
<view
|
|
|
|
|
|
v-for="(msg, index) in messages"
|
2026-02-03 14:50:56 +08:00
|
|
|
|
v-show="shouldShowMessage(msg)"
|
2025-04-16 14:24:06 +08:00
|
|
|
|
:key="index"
|
|
|
|
|
|
:id="'msg-' + index"
|
|
|
|
|
|
class="chat-item"
|
|
|
|
|
|
:class="{ self: msg.self }"
|
2026-03-16 14:28:38 +08:00
|
|
|
|
@longpress="handleLongPress(msg, index)"
|
|
|
|
|
|
@click="isSelectMode ? toggleSelectMessage(index) : null"
|
2025-04-16 14:24:06 +08:00
|
|
|
|
>
|
2026-03-16 14:28:38 +08:00
|
|
|
|
<!-- 选择框 -->
|
|
|
|
|
|
<view v-if="isSelectMode" class="message-checkbox">
|
|
|
|
|
|
<view class="checkbox" :class="{ checked: selectedMessages.includes(index) }">
|
|
|
|
|
|
<uni-icons v-if="selectedMessages.includes(index)" type="success" size="24" color="#FFFFFF"></uni-icons>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
2025-04-16 14:24:06 +08:00
|
|
|
|
<view class="message" v-if="msg.self">
|
|
|
|
|
|
<view class="msg-filecontent" v-if="msg.files.length">
|
|
|
|
|
|
<view
|
|
|
|
|
|
class="msg-files btn-light"
|
|
|
|
|
|
v-for="(file, vInex) in msg.files"
|
|
|
|
|
|
:key="vInex"
|
|
|
|
|
|
@click="jumpUrl(file)"
|
2026-04-09 21:14:25 +08:00
|
|
|
|
:class="{ 'msg-image-file': isImage(file.type, file.name) }"
|
2025-04-16 14:24:06 +08:00
|
|
|
|
>
|
2026-04-09 21:14:25 +08:00
|
|
|
|
<image
|
|
|
|
|
|
v-if="isImage(file.type, file.name)"
|
|
|
|
|
|
class="msg-image-thumbnail"
|
|
|
|
|
|
:src="file.url"
|
|
|
|
|
|
mode="aspectFill"
|
|
|
|
|
|
></image>
|
|
|
|
|
|
<image
|
|
|
|
|
|
v-else
|
|
|
|
|
|
class="msg-file-icon"
|
|
|
|
|
|
src="/static/icon/Vector2.png"
|
|
|
|
|
|
></image>
|
2025-04-16 14:24:06 +08:00
|
|
|
|
<text class="msg-file-text">{{ file.name || '附件' }}</text>
|
2025-03-28 15:19:42 +08:00
|
|
|
|
</view>
|
2025-03-29 11:51:48 +08:00
|
|
|
|
</view>
|
2025-04-16 14:24:06 +08:00
|
|
|
|
{{ msg.displayText }}
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view class="message" :class="{ messageNull: !msg.displayText }" v-else>
|
|
|
|
|
|
<!-- {{ msg.displayText }} -->
|
|
|
|
|
|
<view class="message-markdown">
|
|
|
|
|
|
<md-render
|
|
|
|
|
|
:content="msg.displayText"
|
|
|
|
|
|
:typing="isTyping && messages.length - 1 === index"
|
|
|
|
|
|
></md-render>
|
2026-03-16 14:28:38 +08:00
|
|
|
|
<view class="message-controll" v-show="showControll(index) && !isSelectMode">
|
2025-04-16 14:24:06 +08:00
|
|
|
|
<view class="controll-left">
|
|
|
|
|
|
<image
|
|
|
|
|
|
class="controll-icon btn-light"
|
|
|
|
|
|
src="/static/icon/copy.png"
|
|
|
|
|
|
@click="copyMarkdown(msg.displayText)"
|
|
|
|
|
|
></image>
|
|
|
|
|
|
<image
|
|
|
|
|
|
class="controll-icon mar_le10 btn-light"
|
|
|
|
|
|
src="/static/icon/feedback.png"
|
|
|
|
|
|
v-if="!msg.userBadFeedback"
|
|
|
|
|
|
@click="userGoodFeedback(msg)"
|
|
|
|
|
|
></image>
|
|
|
|
|
|
<image
|
|
|
|
|
|
v-if="isSpeaking && !isPaused && speechIndex === index"
|
|
|
|
|
|
class="controll-icon mar_le10 btn-light"
|
|
|
|
|
|
src="/static/icon/stop.png"
|
|
|
|
|
|
@click="stopMarkdown(msg.displayText, index)"
|
|
|
|
|
|
></image>
|
|
|
|
|
|
<image
|
|
|
|
|
|
class="controll-icon mar_le10 btn-light"
|
|
|
|
|
|
src="/static/icon/broadcast.png"
|
|
|
|
|
|
@click="readMarkdown(msg.displayText, index)"
|
|
|
|
|
|
v-else
|
|
|
|
|
|
></image>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view class="controll-right">
|
|
|
|
|
|
<image
|
|
|
|
|
|
class="controll-icon mar_ri10 btn-light"
|
|
|
|
|
|
src="/static/icon/refresh.png"
|
|
|
|
|
|
@click="refreshMarkdown(index)"
|
|
|
|
|
|
></image>
|
2025-04-07 09:10:55 +08:00
|
|
|
|
</view>
|
2025-03-29 20:46:23 +08:00
|
|
|
|
</view>
|
2025-04-16 14:24:06 +08:00
|
|
|
|
</view>
|
|
|
|
|
|
<!-- guess -->
|
|
|
|
|
|
<view
|
|
|
|
|
|
class="guess"
|
2026-03-16 14:28:38 +08:00
|
|
|
|
v-if="showGuess && !msg.self && messages.length - 1 === index && msg.displayText && !isSelectMode"
|
2025-04-16 14:24:06 +08:00
|
|
|
|
>
|
|
|
|
|
|
<view class="gulist">
|
|
|
|
|
|
<view
|
|
|
|
|
|
class="guess-list"
|
|
|
|
|
|
@click="sendMessageGuess(item)"
|
|
|
|
|
|
v-for="(item, index) in guessList"
|
|
|
|
|
|
:key="index"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ item }}
|
2025-03-28 15:19:42 +08:00
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
2025-03-29 11:51:48 +08:00
|
|
|
|
</view>
|
2025-03-28 15:19:42 +08:00
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
2025-04-16 14:24:06 +08:00
|
|
|
|
<view class="chat-item self" v-if="isRecording">
|
|
|
|
|
|
<view class="message">{{ recognizedText }} {{ lastFinalText }}</view>
|
|
|
|
|
|
</view>
|
2026-01-23 22:01:38 +08:00
|
|
|
|
<!-- 语音正在识别提示 -->
|
|
|
|
|
|
<!-- <view>{{isRecognizing}}</view> -->
|
|
|
|
|
|
<view class="chat-item self" v-if="isRecognizing">
|
|
|
|
|
|
<view class="message msg-loading">
|
|
|
|
|
|
<view class="loading-content">
|
|
|
|
|
|
<view class="ai-loading">
|
|
|
|
|
|
<view></view>
|
|
|
|
|
|
<view></view>
|
|
|
|
|
|
<view></view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<text class="loading-text">正在识别语音...</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
2025-04-16 14:24:06 +08:00
|
|
|
|
<view v-if="isTyping" class="self">
|
2026-01-22 14:05:22 +08:00
|
|
|
|
<view class="message msg-loading">
|
2026-01-23 13:10:40 +08:00
|
|
|
|
<view class="loading-content">
|
|
|
|
|
|
<view class="ai-loading">
|
|
|
|
|
|
<view></view>
|
|
|
|
|
|
<view></view>
|
|
|
|
|
|
<view></view>
|
|
|
|
|
|
</view>
|
2026-01-22 14:05:22 +08:00
|
|
|
|
<text class="loading-text">AI正在思考中...</text>
|
2026-01-23 13:10:40 +08:00
|
|
|
|
</view>
|
2026-01-22 14:05:22 +08:00
|
|
|
|
</view>
|
2025-04-16 14:24:06 +08:00
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
2025-03-28 15:19:42 +08:00
|
|
|
|
</scroll-view>
|
2025-04-16 14:24:06 +08:00
|
|
|
|
<!-- 使用 v-show 保证不销毁事件 -->
|
|
|
|
|
|
<view class="vio_container" :class="status" v-show="status !== 'idle'">
|
2025-03-28 15:19:42 +08:00
|
|
|
|
<view class="record-tip">{{ statusText }}</view>
|
2025-04-07 09:10:55 +08:00
|
|
|
|
<WaveDisplay
|
|
|
|
|
|
:background="audiowaveStyle"
|
|
|
|
|
|
:isActive="isRecording"
|
|
|
|
|
|
:audioData="audioDataForDisplay"
|
|
|
|
|
|
:showInfo="isRecording"
|
|
|
|
|
|
/>
|
2025-03-28 15:19:42 +08:00
|
|
|
|
</view>
|
2025-04-16 14:24:06 +08:00
|
|
|
|
<view class="input-area" v-show="status === 'idle'">
|
2025-03-28 15:19:42 +08:00
|
|
|
|
<view class="areatext">
|
|
|
|
|
|
<input
|
|
|
|
|
|
v-model="textInput"
|
|
|
|
|
|
placeholder-class="inputplaceholder"
|
|
|
|
|
|
class="input"
|
|
|
|
|
|
@confirm="sendMessage"
|
|
|
|
|
|
:disabled="isTyping"
|
2026-03-16 14:14:51 +08:00
|
|
|
|
:adjust-position="true"
|
2026-04-07 17:51:22 +08:00
|
|
|
|
placeholder="请输入您想找的岗位信息或就政策信息【比如:设计师、10000-12000、广州】【比如:今年喀什地区高校毕业生有什么就业政策】"
|
2025-04-07 09:10:55 +08:00
|
|
|
|
v-show="!isVoice"
|
2025-03-28 15:19:42 +08:00
|
|
|
|
/>
|
|
|
|
|
|
<view
|
|
|
|
|
|
class="input_vio"
|
2025-04-16 14:24:06 +08:00
|
|
|
|
@touchstart.prevent="handleTouchStart"
|
2025-03-28 15:19:42 +08:00
|
|
|
|
@touchmove="handleTouchMove"
|
|
|
|
|
|
@touchend="handleTouchEnd"
|
|
|
|
|
|
@touchcancel="handleTouchCancel"
|
2025-04-07 09:10:55 +08:00
|
|
|
|
v-show="isVoice"
|
2025-03-28 15:19:42 +08:00
|
|
|
|
type="default"
|
|
|
|
|
|
>
|
|
|
|
|
|
按住说话
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<!-- upload -->
|
|
|
|
|
|
<view class="btn-box" @click="changeShowFile">
|
|
|
|
|
|
<image
|
|
|
|
|
|
class="send-btn"
|
|
|
|
|
|
:class="{ 'add-file-btn': showfile }"
|
|
|
|
|
|
src="/static/icon/addGroup.png"
|
|
|
|
|
|
></image>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<!-- sendmessgae Button-->
|
|
|
|
|
|
<view class="btn-box purple" v-if="textInput && !isTyping" @click="sendMessage">
|
|
|
|
|
|
<image class="send-btn" src="/static/icon/send3.png"></image>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view class="btn-box" v-else-if="!isTyping && !isVoice" @click="changeVoice">
|
|
|
|
|
|
<image class="send-btn" src="/static/icon/send2x.png"></image>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view class="btn-box" v-else-if="!isTyping" @click="changeVoice">
|
|
|
|
|
|
<image class="send-btn" src="/static/icon/send4.png"></image>
|
|
|
|
|
|
</view>
|
2025-04-07 09:10:55 +08:00
|
|
|
|
<view class="btn-box purple" v-else>
|
|
|
|
|
|
<view class="btn-box-round"></view>
|
2025-03-28 15:19:42 +08:00
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
2025-12-22 12:14:27 +08:00
|
|
|
|
<view class="ai-tips">本服务为AI生成内容,结果仅供参考</view>
|
2026-04-03 19:22:20 +08:00
|
|
|
|
<!-- 文件上传区域 - 使用绝对定位向上展开,避免被tabbar遮挡 -->
|
|
|
|
|
|
<view class="file-upload-popup" :class="{ show: showfile }">
|
2025-05-15 14:17:51 +08:00
|
|
|
|
<view class="area-tips">
|
|
|
|
|
|
<uni-icons type="info-filled" color="#ADADAD" size="15"></uni-icons>
|
|
|
|
|
|
上传后自动解析简历内容
|
|
|
|
|
|
</view>
|
2025-03-28 15:19:42 +08:00
|
|
|
|
<view class="area-file">
|
2025-03-28 18:19:05 +08:00
|
|
|
|
<view class="file-card" @click="uploadCamera">
|
|
|
|
|
|
<image class="card-img" src="/static/icon/file1.png"></image>
|
|
|
|
|
|
<text>拍照上传</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view class="file-card" @click="uploadCamera('album')">
|
|
|
|
|
|
<image class="card-img" src="/static/icon/file2.png"></image>
|
|
|
|
|
|
<text>相册上传</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view class="file-card" @click="getUploadFile">
|
|
|
|
|
|
<image class="card-img" src="/static/icon/file3.png"></image>
|
|
|
|
|
|
<text>文件上传</text>
|
|
|
|
|
|
</view>
|
2025-03-28 15:19:42 +08:00
|
|
|
|
</view>
|
2026-04-03 19:22:20 +08:00
|
|
|
|
</view>
|
2025-03-28 15:19:42 +08:00
|
|
|
|
|
|
|
|
|
|
<!-- filelist -->
|
|
|
|
|
|
<view class="area-uploadfiles" v-if="filesList.length">
|
|
|
|
|
|
<scroll-view class="uploadfiles-scroll" scroll-x="true">
|
|
|
|
|
|
<view class="uploadfiles-list">
|
2025-03-28 18:19:05 +08:00
|
|
|
|
<view
|
|
|
|
|
|
class="file-uploadsend"
|
2026-04-09 21:14:25 +08:00
|
|
|
|
:class="{ 'file-border': isImage(file.type, file.name) }"
|
2025-03-28 18:19:05 +08:00
|
|
|
|
v-for="(file, index) in filesList"
|
|
|
|
|
|
:key="index"
|
|
|
|
|
|
>
|
2026-04-09 21:14:25 +08:00
|
|
|
|
<view class="file-thumbnail" @click="jumpUrl(file)">
|
|
|
|
|
|
<image
|
|
|
|
|
|
class="file-iconImg"
|
|
|
|
|
|
v-if="isImage(file.type, file.name) && !file.error"
|
|
|
|
|
|
:src="file.tempPath || file.url"
|
|
|
|
|
|
mode="aspectFill"
|
|
|
|
|
|
@load="onImageLoad(file)"
|
|
|
|
|
|
@error="onImageError(file)"
|
|
|
|
|
|
></image>
|
|
|
|
|
|
<view class="file-doc" v-else-if="!isImage(file.type, file.name)">
|
|
|
|
|
|
<FileIcon class="doc-icon" :type="file.type"></FileIcon>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view class="image-loading" v-if="isImage(file.type, file.name) && file.loading">
|
|
|
|
|
|
<uni-icons type="spinner-cycle" size="30" color="#999"></uni-icons>
|
|
|
|
|
|
<text class="loading-text">加载中...</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view class="image-error" v-if="isImage(file.type, file.name) && file.error" @click="retryImageLoad(file)">
|
|
|
|
|
|
<uni-icons type="info" size="30" color="#ff4444"></uni-icons>
|
|
|
|
|
|
<text class="error-text">加载失败</text>
|
|
|
|
|
|
<text class="retry-text">点击重试</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<!-- 图片数量标签 -->
|
|
|
|
|
|
<view class="image-count-badge" v-if="isImage(file.type, file.name) && filesList.filter(f => isImage(f.type, f.name)).length > 1">
|
|
|
|
|
|
{{ filesList.filter(f => isImage(f.type, f.name)).indexOf(file) + 1 }}/{{ filesList.filter(f => isImage(f.type, f.name)).length }}
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view class="file-info">
|
|
|
|
|
|
<view class="filename-text">{{ file.name }}</view>
|
|
|
|
|
|
<view class="filerow" v-if="!isImage(file.type, file.name)">
|
|
|
|
|
|
<FileText :type="file.type"></FileText>
|
|
|
|
|
|
<view class="row-x"></view>
|
|
|
|
|
|
<view class="filename-size">{{ file.size }}</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view class="filerow" v-else>
|
|
|
|
|
|
<text class="file-type-tag">图片</text>
|
|
|
|
|
|
<view class="row-x"></view>
|
|
|
|
|
|
<view class="filename-size">{{ file.size }}</view>
|
2025-03-28 18:19:05 +08:00
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
2025-03-28 15:19:42 +08:00
|
|
|
|
<view class="file-del" catchtouchmove="true" @click="delfile(file)">
|
2025-04-07 09:10:55 +08:00
|
|
|
|
<uni-icons type="closeempty" color="#FFFFFF" size="10"></uni-icons>
|
2025-03-28 15:19:42 +08:00
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</scroll-view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
2025-04-16 14:24:06 +08:00
|
|
|
|
<PopupFeeBack ref="feeback" @onSend="confirmFeeBack"></PopupFeeBack>
|
|
|
|
|
|
<MsgTips ref="feeBackTips" content="已收到反馈,感谢您的关注" title="反馈成功" :icon="successIcon"></MsgTips>
|
2025-03-28 15:19:42 +08:00
|
|
|
|
</view>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup>
|
2025-04-16 14:24:06 +08:00
|
|
|
|
import {
|
|
|
|
|
|
ref,
|
|
|
|
|
|
inject,
|
|
|
|
|
|
nextTick,
|
|
|
|
|
|
onMounted,
|
|
|
|
|
|
onUnmounted,
|
|
|
|
|
|
toRaw,
|
|
|
|
|
|
reactive,
|
|
|
|
|
|
computed,
|
|
|
|
|
|
watch,
|
2025-10-21 22:58:47 +08:00
|
|
|
|
getCurrentInstance,
|
2025-04-16 14:24:06 +08:00
|
|
|
|
} from 'vue';
|
2025-03-28 15:19:42 +08:00
|
|
|
|
import { storeToRefs } from 'pinia';
|
2026-01-23 12:12:51 +08:00
|
|
|
|
// 移除重复导入,使用从globalFunction注入的config
|
2025-03-28 15:19:42 +08:00
|
|
|
|
import useChatGroupDBStore from '@/stores/userChatGroupStore';
|
|
|
|
|
|
import MdRender from '@/components/md-render/md-render.vue';
|
2025-04-16 14:24:06 +08:00
|
|
|
|
import PopupFeeBack from './popupbadFeeback.vue';
|
2025-03-28 15:19:42 +08:00
|
|
|
|
import AudioWave from './AudioWave.vue';
|
2025-04-07 09:10:55 +08:00
|
|
|
|
import WaveDisplay from './WaveDisplay.vue';
|
2025-03-28 18:19:05 +08:00
|
|
|
|
import FileIcon from './fileIcon.vue';
|
2025-04-07 09:10:55 +08:00
|
|
|
|
import FileText from './fileText.vue';
|
2025-03-28 15:19:42 +08:00
|
|
|
|
import { useAudioRecorder } from '@/hook/useRealtimeRecorder.js';
|
2025-04-16 14:24:06 +08:00
|
|
|
|
import { useTTSPlayer } from '@/hook/useTTSPlayer.js';
|
2026-01-23 22:01:38 +08:00
|
|
|
|
import successIcon from '@/static/icon/success.png';
|
2025-04-07 09:10:55 +08:00
|
|
|
|
// 全局
|
2025-09-29 11:53:10 +08:00
|
|
|
|
const { $api, navTo, throttle, config } = inject('globalFunction');
|
2025-04-07 09:10:55 +08:00
|
|
|
|
const emit = defineEmits(['onConfirm']);
|
|
|
|
|
|
const { messages, isTyping, textInput, chatSessionID } = storeToRefs(useChatGroupDBStore());
|
|
|
|
|
|
// hook
|
|
|
|
|
|
const {
|
|
|
|
|
|
isRecording,
|
|
|
|
|
|
startRecording,
|
|
|
|
|
|
stopRecording,
|
|
|
|
|
|
cancelRecording,
|
|
|
|
|
|
audioDataForDisplay,
|
|
|
|
|
|
volumeLevel,
|
|
|
|
|
|
recognizedText,
|
|
|
|
|
|
lastFinalText,
|
2026-01-23 22:01:38 +08:00
|
|
|
|
recordingDuration,
|
|
|
|
|
|
isRecognizing,
|
|
|
|
|
|
reset
|
2025-07-22 15:20:21 +08:00
|
|
|
|
} = useAudioRecorder();
|
2025-03-28 15:19:42 +08:00
|
|
|
|
|
2026-01-23 22:01:38 +08:00
|
|
|
|
// 监听语音识别结果变化,自动发送消息
|
|
|
|
|
|
watch(
|
|
|
|
|
|
() => recognizedText.value,
|
|
|
|
|
|
(newVal) => {
|
|
|
|
|
|
if (newVal && newVal.trim()) {
|
|
|
|
|
|
console.log('监听到语音识别结果变化,自动发送消息:', newVal);
|
|
|
|
|
|
sendMessage(newVal);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// 监听isRecognizing状态,显示提示
|
|
|
|
|
|
watch(
|
|
|
|
|
|
() => isRecognizing.value,
|
|
|
|
|
|
(newVal) => {
|
|
|
|
|
|
if (newVal) {
|
|
|
|
|
|
$api.msg('正在识别语音...');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2025-07-21 14:49:45 +08:00
|
|
|
|
const { speak, pause, resume, isSpeaking, isPaused, cancelAudio } = useTTSPlayer(config.speechSynthesis);
|
2025-03-28 15:19:42 +08:00
|
|
|
|
|
2025-10-21 22:58:47 +08:00
|
|
|
|
// 获取组件实例(用于小程序 SelectorQuery)
|
|
|
|
|
|
const instance = getCurrentInstance();
|
|
|
|
|
|
|
2025-04-07 09:10:55 +08:00
|
|
|
|
// state
|
2026-02-03 14:50:56 +08:00
|
|
|
|
const activeTab = ref('policy'); // 'policy' or 'job'
|
2025-04-07 09:10:55 +08:00
|
|
|
|
const queries = ref([]);
|
2025-03-28 15:19:42 +08:00
|
|
|
|
const guessList = ref([]);
|
|
|
|
|
|
const scrollTop = ref(0);
|
|
|
|
|
|
const showGuess = ref(false);
|
|
|
|
|
|
const showfile = ref(false);
|
|
|
|
|
|
const filesList = ref([]);
|
|
|
|
|
|
const bgText = ref(false);
|
|
|
|
|
|
const isVoice = ref(false);
|
|
|
|
|
|
const status = ref('idle'); // idle | recording | cancel
|
|
|
|
|
|
const startY = ref(0);
|
|
|
|
|
|
const cancelThreshold = 100;
|
2025-04-07 09:10:55 +08:00
|
|
|
|
const speechIndex = ref(0);
|
|
|
|
|
|
const isAudioPermission = ref(false);
|
2025-04-16 14:24:06 +08:00
|
|
|
|
const feebackData = ref(null);
|
2026-03-16 14:28:38 +08:00
|
|
|
|
// 删除功能相关状态
|
|
|
|
|
|
const isSelectMode = ref(false);
|
|
|
|
|
|
const selectedMessages = ref([]);
|
2025-04-16 14:24:06 +08:00
|
|
|
|
// ref for DOM element
|
|
|
|
|
|
const voiceBtn = ref(null);
|
|
|
|
|
|
const feeback = ref(null);
|
|
|
|
|
|
const feeBackTips = ref(null);
|
2025-03-28 15:19:42 +08:00
|
|
|
|
const state = reactive({
|
|
|
|
|
|
uploadFileTips: '请根据以上附件,帮我推荐岗位。',
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-04-07 09:10:55 +08:00
|
|
|
|
const statusText = computed(() => {
|
|
|
|
|
|
switch (status.value) {
|
|
|
|
|
|
case 'recording':
|
|
|
|
|
|
return '松手发送,上划取消';
|
|
|
|
|
|
case 'cancel':
|
|
|
|
|
|
return '松手取消';
|
|
|
|
|
|
default:
|
|
|
|
|
|
return '按住说话';
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const audiowaveStyle = computed(() => {
|
|
|
|
|
|
return status.value === 'cancel'
|
|
|
|
|
|
? '#f54545'
|
|
|
|
|
|
: status.value === 'recording'
|
|
|
|
|
|
? 'linear-gradient(to right, #377dff, #9a60ff)'
|
|
|
|
|
|
: '#f1f1f1';
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
onMounted(async () => {
|
|
|
|
|
|
changeQueries();
|
2025-03-28 15:19:42 +08:00
|
|
|
|
scrollToBottom();
|
2025-04-07 09:10:55 +08:00
|
|
|
|
isAudioPermission.value = await requestMicPermission();
|
2026-01-23 22:01:38 +08:00
|
|
|
|
reset(); // 重置语音识别状态
|
2025-03-28 15:19:42 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
2025-04-07 09:10:55 +08:00
|
|
|
|
const requestMicPermission = async () => {
|
2025-10-21 22:58:47 +08:00
|
|
|
|
// #ifdef H5
|
2025-04-07 09:10:55 +08:00
|
|
|
|
try {
|
2025-10-21 22:58:47 +08:00
|
|
|
|
if (typeof navigator !== 'undefined' && navigator.mediaDevices) {
|
|
|
|
|
|
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
|
|
|
|
console.log('✅ 麦克风权限已授权');
|
2025-04-07 09:10:55 +08:00
|
|
|
|
|
2025-10-21 22:58:47 +08:00
|
|
|
|
// 立刻停止所有音轨,释放麦克风
|
|
|
|
|
|
stream.getTracks().forEach((track) => track.stop());
|
2025-04-07 09:10:55 +08:00
|
|
|
|
|
2025-10-21 22:58:47 +08:00
|
|
|
|
return true;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.warn('❌ 当前环境不支持麦克风');
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
2025-04-07 09:10:55 +08:00
|
|
|
|
} catch (err) {
|
|
|
|
|
|
console.warn('❌ 用户拒绝麦克风权限或不支持:', err);
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
2025-10-21 22:58:47 +08:00
|
|
|
|
// #endif
|
|
|
|
|
|
|
|
|
|
|
|
// #ifdef MP-WEIXIN
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 微信小程序使用 uni.authorize 请求权限
|
|
|
|
|
|
const res = await uni.authorize({
|
|
|
|
|
|
scope: 'scope.record'
|
|
|
|
|
|
});
|
|
|
|
|
|
console.log('✅ 麦克风权限已授权');
|
|
|
|
|
|
return true;
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
console.warn('❌ 用户拒绝麦克风权限:', err);
|
|
|
|
|
|
// 用户拒绝授权,但不影响其他功能
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
// #endif
|
|
|
|
|
|
|
|
|
|
|
|
// #ifndef H5 || MP-WEIXIN
|
|
|
|
|
|
// 其他平台暂不支持
|
|
|
|
|
|
console.warn('❌ 当前平台不支持麦克风权限检测');
|
|
|
|
|
|
return false;
|
|
|
|
|
|
// #endif
|
2025-04-07 09:10:55 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
function showControll(index) {
|
|
|
|
|
|
if (index === messages.value.length - 1 && isTyping.value) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const sendMessage = (text) => {
|
|
|
|
|
|
const values = textInput.value || text;
|
2025-03-28 15:19:42 +08:00
|
|
|
|
showfile.value = false;
|
|
|
|
|
|
showGuess.value = false;
|
|
|
|
|
|
if (values.trim()) {
|
|
|
|
|
|
// 判断是否有对话ID // 没有则创建对话列表
|
|
|
|
|
|
const callback = () => {
|
|
|
|
|
|
const normalArr = toRaw(filesList.value); // 转换为普通数组
|
|
|
|
|
|
filesList.value = [];
|
|
|
|
|
|
useChatGroupDBStore()
|
2025-04-07 09:10:55 +08:00
|
|
|
|
.getStearm(values, normalArr, scrollToBottom, {
|
2025-10-13 16:01:49 +08:00
|
|
|
|
onDataReceived: (data, message, index) => {
|
|
|
|
|
|
// 流式朗读:只在内容足够长且包含完整信息时才开始朗读
|
|
|
|
|
|
if (!message.self && message.displayText && message.displayText.trim()) {
|
|
|
|
|
|
// 检查是否已经开始朗读这条消息
|
|
|
|
|
|
if (speechIndex.value !== index) {
|
|
|
|
|
|
// 延迟TTS,等待内容更完整
|
|
|
|
|
|
// 只有在内容长度超过50个字符或者包含岗位信息时才开始朗读
|
|
|
|
|
|
const hasJobInfo = message.displayText.includes('```job-json') ||
|
|
|
|
|
|
message.displayText.includes('岗位') ||
|
|
|
|
|
|
message.displayText.includes('公司') ||
|
|
|
|
|
|
message.displayText.includes('薪资');
|
|
|
|
|
|
|
|
|
|
|
|
if (message.displayText.length > 50 || hasJobInfo) {
|
|
|
|
|
|
console.log('🎵 Starting streaming TTS for message index:', index);
|
|
|
|
|
|
console.log('📝 Current text length:', message.displayText.length);
|
|
|
|
|
|
console.log('📝 Has job info:', hasJobInfo);
|
|
|
|
|
|
|
|
|
|
|
|
// 开始朗读当前消息
|
2026-01-24 18:29:47 +08:00
|
|
|
|
speechIndex.value = index;
|
|
|
|
|
|
readMarkdown(message.displayText, index, { immediate: false });
|
|
|
|
|
|
// 一旦开始朗读,就设置speechIndex,避免重复调用
|
|
|
|
|
|
speechIndex.value = index;
|
2025-10-13 16:01:49 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
console.log('⏳ Waiting for more content before TTS, current length:', message.displayText.length);
|
|
|
|
|
|
}
|
2026-01-24 18:29:47 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
// 已经开始朗读这条消息,不再重复调用
|
|
|
|
|
|
console.log('⏭️ Already speaking this message, skipping duplicate TTS call');
|
2025-10-13 16:01:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
onComplete: () => {
|
|
|
|
|
|
console.log('🎯 onComplete callback triggered');
|
|
|
|
|
|
console.log('📊 Messages array length:', messages.value.length);
|
|
|
|
|
|
|
|
|
|
|
|
// 确保最后一条AI消息的朗读完成
|
|
|
|
|
|
const lastMessageIndex = messages.value.length - 1;
|
|
|
|
|
|
if (lastMessageIndex >= 0) {
|
|
|
|
|
|
const lastMessage = messages.value[lastMessageIndex];
|
|
|
|
|
|
if (!lastMessage.self && lastMessage.displayText && lastMessage.displayText.trim()) {
|
|
|
|
|
|
console.log('🎵 Final TTS for complete message');
|
|
|
|
|
|
console.log('📝 Final text length:', lastMessage.displayText.length);
|
|
|
|
|
|
console.log('📝 Final text preview:', lastMessage.displayText.substring(0, 100) + '...');
|
|
|
|
|
|
|
|
|
|
|
|
// 停止当前的朗读(如果有的话)
|
|
|
|
|
|
if (isSpeaking.value) {
|
|
|
|
|
|
console.log('🛑 Stopping current TTS to start final complete TTS');
|
|
|
|
|
|
cancelAudio();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 开始朗读完整的内容
|
|
|
|
|
|
speechIndex.value = lastMessageIndex;
|
2026-01-24 18:29:47 +08:00
|
|
|
|
readMarkdown(lastMessage.displayText, lastMessageIndex, { immediate: true });
|
2025-10-13 16:01:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
2025-04-07 09:10:55 +08:00
|
|
|
|
})
|
2025-03-28 15:19:42 +08:00
|
|
|
|
.then(() => {
|
|
|
|
|
|
getGuess();
|
|
|
|
|
|
scrollToBottom();
|
|
|
|
|
|
});
|
|
|
|
|
|
emit('onConfirm', values);
|
|
|
|
|
|
textInput.value = '';
|
|
|
|
|
|
scrollToBottom();
|
|
|
|
|
|
};
|
|
|
|
|
|
// 没有对话列表则创建
|
|
|
|
|
|
if (!chatSessionID.value) {
|
|
|
|
|
|
useChatGroupDBStore()
|
|
|
|
|
|
.addTabel(values)
|
|
|
|
|
|
.then((res) => {
|
|
|
|
|
|
callback();
|
|
|
|
|
|
});
|
|
|
|
|
|
} else {
|
|
|
|
|
|
callback();
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
if (filesList.value.length) {
|
|
|
|
|
|
$api.msg('上传文件请输入想问的问题描述');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
$api.msg('请输入职位信息或描述');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const sendMessageGuess = (item) => {
|
|
|
|
|
|
showGuess.value = false;
|
|
|
|
|
|
textInput.value = item;
|
|
|
|
|
|
sendMessage(item);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const delfile = (file) => {
|
|
|
|
|
|
uni.showModal({
|
|
|
|
|
|
content: '确认删除文件?',
|
2025-03-28 18:19:05 +08:00
|
|
|
|
success(res) {
|
|
|
|
|
|
if (res.confirm) {
|
|
|
|
|
|
filesList.value = filesList.value.filter((item) => item.url !== file.url);
|
|
|
|
|
|
if (!filesList.value.length) {
|
|
|
|
|
|
if (textInput.value === state.uploadFileTips) {
|
|
|
|
|
|
textInput.value = '';
|
|
|
|
|
|
}
|
2025-03-28 15:19:42 +08:00
|
|
|
|
}
|
2025-03-28 18:19:05 +08:00
|
|
|
|
$api.msg('附件删除成功');
|
2025-03-28 15:19:42 +08:00
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const scrollToBottom = throttle(function () {
|
|
|
|
|
|
nextTick(() => {
|
|
|
|
|
|
try {
|
2026-02-03 14:50:56 +08:00
|
|
|
|
let query;
|
2025-10-21 22:58:47 +08:00
|
|
|
|
// #ifdef MP-WEIXIN
|
2026-02-03 14:50:56 +08:00
|
|
|
|
query = uni.createSelectorQuery().in(instance);
|
2025-10-21 22:58:47 +08:00
|
|
|
|
// #endif
|
|
|
|
|
|
// #ifndef MP-WEIXIN
|
2026-02-03 14:50:56 +08:00
|
|
|
|
query = uni.createSelectorQuery();
|
2025-10-21 22:58:47 +08:00
|
|
|
|
// #endif
|
|
|
|
|
|
|
2025-04-07 09:10:55 +08:00
|
|
|
|
query.select('.scrollView').boundingClientRect();
|
|
|
|
|
|
query.select('.list-content').boundingClientRect();
|
|
|
|
|
|
query.exec((res) => {
|
2025-10-21 22:58:47 +08:00
|
|
|
|
if (!res || !res[0] || !res[1]) {
|
|
|
|
|
|
console.warn('scrollToBottom: 元素未找到或尚未渲染');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2025-04-07 09:10:55 +08:00
|
|
|
|
const scrollViewHeight = res[0].height;
|
|
|
|
|
|
const scrollContentHeight = res[1].height;
|
|
|
|
|
|
if (scrollContentHeight > scrollViewHeight) {
|
|
|
|
|
|
const scrolldistance = scrollContentHeight - scrollViewHeight;
|
|
|
|
|
|
scrollTop.value = scrolldistance;
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
2025-03-28 15:19:42 +08:00
|
|
|
|
} catch (err) {
|
|
|
|
|
|
console.warn(err);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}, 500);
|
|
|
|
|
|
|
|
|
|
|
|
function getGuess() {
|
2026-01-22 12:57:01 +08:00
|
|
|
|
// $api.chatRequest('/guest', { sessionId: chatSessionID.value }, 'POST').then((res) => {
|
|
|
|
|
|
$api.chatRequest('/guest', undefined, 'POST').then((res) => {
|
|
|
|
|
|
console.log('getGuess ---- res:', res);
|
2025-03-28 15:19:42 +08:00
|
|
|
|
guessList.value = res.data;
|
|
|
|
|
|
showGuess.value = true;
|
|
|
|
|
|
nextTick(() => {
|
|
|
|
|
|
scrollToBottom();
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-09 21:14:25 +08:00
|
|
|
|
// 从文件路径提取文件名
|
|
|
|
|
|
function getFileNameFromPath(filePath) {
|
|
|
|
|
|
if (!filePath) return '';
|
|
|
|
|
|
// 处理反斜杠和正斜杠
|
|
|
|
|
|
const path = filePath.replace(/\\/g, '/');
|
|
|
|
|
|
const lastSlashIndex = path.lastIndexOf('/');
|
|
|
|
|
|
if (lastSlashIndex !== -1) {
|
|
|
|
|
|
return path.substring(lastSlashIndex + 1);
|
|
|
|
|
|
}
|
|
|
|
|
|
return path;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 从文件名推断文件类型
|
|
|
|
|
|
function getFileTypeFromName(fileName) {
|
|
|
|
|
|
if (!fileName) return '';
|
|
|
|
|
|
const lowerName = fileName.toLowerCase();
|
|
|
|
|
|
if (lowerName.endsWith('.jpg') || lowerName.endsWith('.jpeg')) return 'image/jpeg';
|
|
|
|
|
|
if (lowerName.endsWith('.png')) return 'image/png';
|
|
|
|
|
|
if (lowerName.endsWith('.gif')) return 'image/gif';
|
|
|
|
|
|
if (lowerName.endsWith('.bmp')) return 'image/bmp';
|
|
|
|
|
|
if (lowerName.endsWith('.webp')) return 'image/webp';
|
|
|
|
|
|
if (lowerName.endsWith('.svg')) return 'image/svg+xml';
|
|
|
|
|
|
if (lowerName.endsWith('.pdf')) return 'application/pdf';
|
|
|
|
|
|
if (lowerName.endsWith('.doc')) return 'application/msword';
|
|
|
|
|
|
if (lowerName.endsWith('.docx')) return 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';
|
|
|
|
|
|
if (lowerName.endsWith('.ppt')) return 'application/vnd.ms-powerpoint';
|
|
|
|
|
|
if (lowerName.endsWith('.pptx')) return 'application/vnd.openxmlformats-officedocument.presentationml.presentation';
|
|
|
|
|
|
if (lowerName.endsWith('.xls')) return 'application/vnd.ms-excel';
|
|
|
|
|
|
if (lowerName.endsWith('.xlsx')) return 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
|
|
|
|
|
|
if (lowerName.endsWith('.txt')) return 'text/plain';
|
|
|
|
|
|
if (lowerName.endsWith('.md')) return 'text/markdown';
|
|
|
|
|
|
if (lowerName.endsWith('.html') || lowerName.endsWith('.htm')) return 'text/html';
|
|
|
|
|
|
if (lowerName.endsWith('.csv')) return 'text/csv';
|
|
|
|
|
|
return '';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function isImage(type, name) {
|
|
|
|
|
|
if (!type && !name) return false;
|
|
|
|
|
|
|
|
|
|
|
|
const debug = false;
|
|
|
|
|
|
if (debug) console.log('isImage检查:', { type, name });
|
|
|
|
|
|
|
|
|
|
|
|
if (type) {
|
|
|
|
|
|
const lowerType = type.toLowerCase();
|
|
|
|
|
|
|
|
|
|
|
|
if (lowerType.includes('image/')) {
|
|
|
|
|
|
if (debug) console.log('通过MIME类型识别为图片:', lowerType);
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (lowerType.includes('image')) {
|
|
|
|
|
|
if (debug) console.log('通过"image"关键词识别为图片:', lowerType);
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const imageExtensionsInType = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg', 'heic', 'heif', 'tiff', 'tif'];
|
|
|
|
|
|
for (const ext of imageExtensionsInType) {
|
|
|
|
|
|
if (lowerType.includes(ext)) {
|
|
|
|
|
|
if (debug) console.log('通过类型中的扩展名识别为图片:', ext);
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (name) {
|
|
|
|
|
|
const lowerName = name.toLowerCase();
|
|
|
|
|
|
const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg', '.heic', '.heif', '.tiff', '.tif', '.jfif', '.pjpeg', '.pjp'];
|
|
|
|
|
|
for (const ext of imageExtensions) {
|
|
|
|
|
|
if (lowerName.endsWith(ext)) {
|
|
|
|
|
|
if (debug) console.log('通过文件扩展名识别为图片:', ext);
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const imageExtensionsWithoutDot = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg', 'heic', 'heif', 'tiff', 'tif', 'jfif', 'pjpeg', 'pjp'];
|
|
|
|
|
|
for (const ext of imageExtensionsWithoutDot) {
|
|
|
|
|
|
if (lowerName.endsWith('.' + ext)) {
|
|
|
|
|
|
if (debug) console.log('通过带点的扩展名识别为图片:', ext);
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (debug) console.log('未识别为图片:', lowerName);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (debug) console.log('不是图片文件');
|
|
|
|
|
|
return false;
|
2025-03-28 15:19:42 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-03-28 18:19:05 +08:00
|
|
|
|
function isFile(type) {
|
|
|
|
|
|
const allowedTypes = config.allowedFileTypes || [];
|
|
|
|
|
|
if (!allowedTypes.includes(type)) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
return true;
|
2025-03-28 15:19:42 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function jumpUrl(file) {
|
2026-04-09 21:14:25 +08:00
|
|
|
|
if (!file.url && !file.tempPath) {
|
2025-03-28 15:19:42 +08:00
|
|
|
|
$api.msg('文件地址丢失');
|
2026-04-09 21:14:25 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (isImage(file.type, file.name)) {
|
|
|
|
|
|
const imageUrl = file.tempPath || file.url;
|
|
|
|
|
|
uni.previewImage({ urls: [imageUrl], current: 0 });
|
|
|
|
|
|
} else {
|
|
|
|
|
|
window.open(file.url);
|
2025-03-28 15:19:42 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-09 21:14:25 +08:00
|
|
|
|
function VerifyNumberFiles(additionalCount = 1) {
|
|
|
|
|
|
const currentCount = filesList.value.length;
|
|
|
|
|
|
const maxCount = config.allowedFileNumber || 9;
|
|
|
|
|
|
|
|
|
|
|
|
if (currentCount + additionalCount > maxCount) {
|
|
|
|
|
|
$api.msg(`最多只能上传${maxCount}个文件,当前已有${currentCount}个`);
|
2025-03-28 15:19:42 +08:00
|
|
|
|
return true;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-09 21:14:25 +08:00
|
|
|
|
function onImageLoad(file) {
|
|
|
|
|
|
const index = filesList.value.findIndex(f => f.url === file.url);
|
|
|
|
|
|
if (index !== -1) {
|
|
|
|
|
|
filesList.value[index].loading = false;
|
|
|
|
|
|
filesList.value[index].error = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function onImageError(file) {
|
|
|
|
|
|
const index = filesList.value.findIndex(f => f.url === file.url);
|
|
|
|
|
|
if (index !== -1) {
|
|
|
|
|
|
filesList.value[index].loading = false;
|
|
|
|
|
|
filesList.value[index].error = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function retryImageLoad(file) {
|
|
|
|
|
|
const index = filesList.value.findIndex(f => f.url === file.url);
|
|
|
|
|
|
if (index !== -1) {
|
|
|
|
|
|
filesList.value[index].loading = true;
|
|
|
|
|
|
filesList.value[index].error = false;
|
|
|
|
|
|
// 触发重新加载
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
if (filesList.value[index]) {
|
|
|
|
|
|
filesList.value[index].loading = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}, 100);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-03-28 15:19:42 +08:00
|
|
|
|
function uploadCamera(type = 'camera') {
|
2026-04-09 21:14:25 +08:00
|
|
|
|
// 在微信小程序中检查权限
|
|
|
|
|
|
// #ifdef MP-WEIXIN
|
|
|
|
|
|
const scope = type === 'camera' ? 'scope.camera' : 'scope.writePhotosAlbum';
|
|
|
|
|
|
|
|
|
|
|
|
uni.getSetting({
|
|
|
|
|
|
success: (res) => {
|
|
|
|
|
|
if (!res.authSetting[scope]) {
|
|
|
|
|
|
// 未授权,发起授权请求
|
|
|
|
|
|
uni.authorize({
|
|
|
|
|
|
scope: scope,
|
|
|
|
|
|
success: () => {
|
|
|
|
|
|
// 授权成功,执行选择图片
|
|
|
|
|
|
chooseImageAfterAuth();
|
|
|
|
|
|
},
|
|
|
|
|
|
fail: (err) => {
|
|
|
|
|
|
console.error('授权失败:', err);
|
|
|
|
|
|
if (err.errMsg && err.errMsg.includes('auth deny')) {
|
|
|
|
|
|
uni.showModal({
|
|
|
|
|
|
title: '权限提示',
|
|
|
|
|
|
content: `需要${type === 'camera' ? '相机' : '相册'}权限才能上传图片,请在设置中开启权限`,
|
|
|
|
|
|
showCancel: false,
|
|
|
|
|
|
confirmText: '确定'
|
|
|
|
|
|
});
|
|
|
|
|
|
} else {
|
|
|
|
|
|
$api.msg('授权失败,请重试');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 已授权,直接执行选择图片
|
|
|
|
|
|
chooseImageAfterAuth();
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
fail: (err) => {
|
|
|
|
|
|
console.error('获取设置失败:', err);
|
|
|
|
|
|
chooseImageAfterAuth(); // 失败时也尝试选择图片
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
// #endif
|
|
|
|
|
|
|
|
|
|
|
|
// #ifndef MP-WEIXIN
|
|
|
|
|
|
// 非微信小程序环境直接选择图片
|
|
|
|
|
|
chooseImageAfterAuth();
|
|
|
|
|
|
// #endif
|
|
|
|
|
|
|
|
|
|
|
|
function chooseImageAfterAuth() {
|
|
|
|
|
|
uni.chooseImage({
|
|
|
|
|
|
count: 9, //支持多选,最多9张
|
|
|
|
|
|
sizeType: ['original', 'compressed'], //可以指定是原图还是压缩图,默认二者都有
|
|
|
|
|
|
// sourceType: [type], // 移除 sourceType 参数,解决微信 "unknown scene" 错误
|
|
|
|
|
|
success: function (res) {
|
|
|
|
|
|
const tempFilePaths = res.tempFilePaths;
|
|
|
|
|
|
const files = res.tempFiles;
|
|
|
|
|
|
|
|
|
|
|
|
// 检查文件数量限制
|
|
|
|
|
|
if (VerifyNumberFiles(files.length)) return;
|
|
|
|
|
|
|
|
|
|
|
|
// 显示上传提示
|
|
|
|
|
|
$api.msg(`开始上传${files.length}张图片...`);
|
|
|
|
|
|
|
|
|
|
|
|
// 上传所有图片
|
|
|
|
|
|
const uploadPromises = tempFilePaths.map((filePath, index) => {
|
|
|
|
|
|
return $api.uploadFile(filePath, true).then((resData) => {
|
|
|
|
|
|
resData = JSON.parse(resData);
|
|
|
|
|
|
if (resData.code === 200 && resData.filePath) {
|
|
|
|
|
|
// 从文件对象获取文件信息
|
|
|
|
|
|
const fileInfo = files[index] || {};
|
|
|
|
|
|
const fileName = getFileNameFromPath(fileInfo.name || filePath);
|
|
|
|
|
|
const fileType = fileInfo.type || getFileTypeFromName(fileName);
|
|
|
|
|
|
const fileSize = fileInfo.size || 0;
|
|
|
|
|
|
|
|
|
|
|
|
// 使用统一的文件路径处理函数
|
|
|
|
|
|
const processedFile = processFilePath(
|
|
|
|
|
|
resData.filePath, // 原始服务器路径
|
|
|
|
|
|
fileName,
|
|
|
|
|
|
fileType,
|
|
|
|
|
|
fileSize,
|
|
|
|
|
|
tempFilePaths[index] // 临时路径用于预览
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// 添加图片加载状态字段
|
|
|
|
|
|
processedFile.loading = false;
|
|
|
|
|
|
processedFile.error = false;
|
|
|
|
|
|
|
|
|
|
|
|
return processedFile;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
throw new Error(resData.msg || '上传失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
Promise.all(uploadPromises).then((uploadedFiles) => {
|
|
|
|
|
|
// 将所有上传成功的文件添加到filesList,并设置图片加载状态
|
|
|
|
|
|
uploadedFiles.forEach(file => {
|
|
|
|
|
|
if (isImage(file.type, file.name)) {
|
|
|
|
|
|
file.loading = true; // 图片开始加载
|
|
|
|
|
|
}
|
|
|
|
|
|
filesList.value.push(file);
|
2025-03-28 15:19:42 +08:00
|
|
|
|
});
|
|
|
|
|
|
textInput.value = state.uploadFileTips;
|
2026-04-09 21:14:25 +08:00
|
|
|
|
showfile.value = false; // 关闭上传面板
|
|
|
|
|
|
$api.msg(`成功上传${uploadedFiles.length}张图片`);
|
|
|
|
|
|
}).catch((error) => {
|
|
|
|
|
|
$api.msg(error.message || '图片上传失败');
|
|
|
|
|
|
});
|
|
|
|
|
|
},
|
|
|
|
|
|
fail: function (err) {
|
|
|
|
|
|
console.error('选择图片失败:', err);
|
|
|
|
|
|
// 专门处理微信 "unknown scene" 错误
|
|
|
|
|
|
if (err.errMsg && err.errMsg.includes('unknown scene')) {
|
|
|
|
|
|
$api.msg('选择图片失败,请尝试重新选择或检查权限设置');
|
|
|
|
|
|
} else if (err.errMsg && err.errMsg.includes('auth deny')) {
|
|
|
|
|
|
$api.msg('没有相机或相册权限,请在设置中开启');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
$api.msg('选择图片失败: ' + (err.errMsg || '未知错误'));
|
2025-03-28 15:19:42 +08:00
|
|
|
|
}
|
2026-04-09 21:14:25 +08:00
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2025-03-28 15:19:42 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function getUploadFile(type = 'camera') {
|
2026-04-09 21:14:25 +08:00
|
|
|
|
// 检测是否是微信小程序环境
|
|
|
|
|
|
const isWeixinMiniProgram = typeof wx !== 'undefined' && wx.chooseMessageFile;
|
|
|
|
|
|
|
|
|
|
|
|
console.log('当前环境检测:', {
|
|
|
|
|
|
isWeixinMiniProgram,
|
|
|
|
|
|
wxExists: typeof wx !== 'undefined',
|
|
|
|
|
|
hasChooseMessageFile: typeof wx !== 'undefined' && wx.chooseMessageFile
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (isWeixinMiniProgram) {
|
|
|
|
|
|
console.log('微信小程序环境,使用uni.chooseMessageFile');
|
|
|
|
|
|
// 微信小程序环境:使用uni.chooseMessageFile
|
|
|
|
|
|
uni.chooseMessageFile({
|
|
|
|
|
|
count: 9,
|
|
|
|
|
|
type: 'file',
|
|
|
|
|
|
success: (res) => {
|
|
|
|
|
|
console.log('uni.chooseMessageFile返回的res:', res);
|
|
|
|
|
|
// uni.chooseMessageFile返回的数据结构不同
|
|
|
|
|
|
// tempFiles数组中每个对象有path、name、size等属性
|
|
|
|
|
|
const tempFiles = res.tempFiles || [];
|
|
|
|
|
|
const tempFilePaths = tempFiles.map(file => file.path);
|
|
|
|
|
|
const files = tempFiles;
|
|
|
|
|
|
|
|
|
|
|
|
console.log('tempFilePaths:', tempFilePaths);
|
|
|
|
|
|
console.log('tempFiles:', tempFiles);
|
|
|
|
|
|
if (files && files.length > 0) {
|
|
|
|
|
|
console.log('第一个文件详情:', files[0]);
|
|
|
|
|
|
console.log('第一个文件的所有属性:', Object.keys(files[0]));
|
|
|
|
|
|
// 详细记录每个文件的所有属性
|
|
|
|
|
|
files.forEach((file, index) => {
|
|
|
|
|
|
console.log(`文件 ${index} 完整对象:`, JSON.stringify(file));
|
|
|
|
|
|
console.log(`文件 ${index} 关键属性:`, {
|
|
|
|
|
|
name: file.name,
|
|
|
|
|
|
type: file.type,
|
|
|
|
|
|
path: file.path,
|
|
|
|
|
|
size: file.size,
|
|
|
|
|
|
hasType: 'type' in file,
|
|
|
|
|
|
typeIsString: typeof file.type === 'string',
|
|
|
|
|
|
typeLength: file.type ? file.type.length : 0
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 调用公共的文件处理函数
|
|
|
|
|
|
handleSelectedFiles(tempFilePaths, files);
|
|
|
|
|
|
},
|
|
|
|
|
|
fail: (err) => {
|
|
|
|
|
|
console.error('uni.chooseMessageFile失败:', err);
|
|
|
|
|
|
// 微信小程序特定的错误处理
|
|
|
|
|
|
if (err.errMsg && err.errMsg.includes('cancel')) {
|
|
|
|
|
|
// 用户取消选择,不提示错误
|
|
|
|
|
|
console.log('用户取消选择文件');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
$api.msg('选择文件失败,请重试');
|
|
|
|
|
|
}
|
2025-03-28 15:19:42 +08:00
|
|
|
|
}
|
2026-04-09 21:14:25 +08:00
|
|
|
|
});
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.log('非微信小程序环境,使用uni.chooseFile');
|
|
|
|
|
|
// 其他平台:使用uni.chooseFile
|
|
|
|
|
|
uni.chooseFile({
|
|
|
|
|
|
count: 9,
|
|
|
|
|
|
success: (res) => {
|
|
|
|
|
|
console.log('uni.chooseFile返回的res:', res);
|
|
|
|
|
|
const tempFilePaths = res.tempFilePaths;
|
|
|
|
|
|
const files = res.tempFiles;
|
|
|
|
|
|
|
|
|
|
|
|
console.log('tempFilePaths:', tempFilePaths);
|
|
|
|
|
|
console.log('tempFiles:', files);
|
|
|
|
|
|
if (files && files.length > 0) {
|
|
|
|
|
|
console.log('第一个文件详情:', files[0]);
|
|
|
|
|
|
console.log('第一个文件的所有属性:', Object.keys(files[0]));
|
|
|
|
|
|
// 详细记录每个文件的所有属性
|
|
|
|
|
|
files.forEach((file, index) => {
|
|
|
|
|
|
console.log(`文件 ${index} 完整对象:`, JSON.stringify(file));
|
|
|
|
|
|
console.log(`文件 ${index} 关键属性:`, {
|
|
|
|
|
|
name: file.name,
|
|
|
|
|
|
type: file.type,
|
|
|
|
|
|
path: file.path,
|
|
|
|
|
|
size: file.size,
|
|
|
|
|
|
hasType: 'type' in file,
|
|
|
|
|
|
typeIsString: typeof file.type === 'string',
|
|
|
|
|
|
typeLength: file.type ? file.type.length : 0
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 调用公共的文件处理函数
|
|
|
|
|
|
handleSelectedFiles(tempFilePaths, files);
|
|
|
|
|
|
},
|
|
|
|
|
|
fail: (err) => {
|
|
|
|
|
|
console.error('uni.chooseFile失败:', err);
|
|
|
|
|
|
if (err.errMsg && !err.errMsg.includes('cancel')) {
|
|
|
|
|
|
$api.msg('选择文件失败,请重试');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 处理服务器返回的文件路径,生成正确的显示URL和服务器路径
|
|
|
|
|
|
function processFilePath(originalFilePath, fileName, fileType, fileSize, tempPath) {
|
|
|
|
|
|
let displayUrl = originalFilePath; // 用于缩略图显示的URL(外网域名)
|
|
|
|
|
|
const serverPath = originalFilePath; // 原始服务器路径(内网地址,用于对话接口)
|
|
|
|
|
|
|
|
|
|
|
|
console.log('processFilePath原始fileUrl:', originalFilePath);
|
|
|
|
|
|
|
|
|
|
|
|
// 如果filePath包含IP地址,替换为配置的域名(用于显示)
|
|
|
|
|
|
if (displayUrl && displayUrl.includes('://')) {
|
|
|
|
|
|
// 检查是否是本地临时路径(如http://tmp/),如果是则保持原样
|
|
|
|
|
|
if (displayUrl.startsWith('http://tmp/')) {
|
|
|
|
|
|
// 本地临时路径,保持原样
|
|
|
|
|
|
console.log('本地临时路径,保持原样:', displayUrl);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 方法1:直接替换域名部分(保持路径结构)- 使用兼容的方法
|
|
|
|
|
|
// 例如:http://10.98.80.146/file/xxx.jpg -> https://www.xjksly.cn/file/xxx.jpg
|
|
|
|
|
|
const originalUrl = displayUrl;
|
|
|
|
|
|
|
|
|
|
|
|
// 提取协议、域名和路径部分
|
|
|
|
|
|
let protocol = '';
|
|
|
|
|
|
let domain = '';
|
|
|
|
|
|
let path = '';
|
|
|
|
|
|
|
|
|
|
|
|
// 分离协议和剩余部分
|
|
|
|
|
|
const protocolIndex = originalUrl.indexOf('://');
|
|
|
|
|
|
if (protocolIndex !== -1) {
|
|
|
|
|
|
protocol = originalUrl.substring(0, protocolIndex + 3);
|
|
|
|
|
|
const rest = originalUrl.substring(protocolIndex + 3);
|
|
|
|
|
|
const slashIndex = rest.indexOf('/');
|
|
|
|
|
|
if (slashIndex !== -1) {
|
|
|
|
|
|
domain = rest.substring(0, slashIndex);
|
|
|
|
|
|
path = rest.substring(slashIndex);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
domain = rest;
|
|
|
|
|
|
path = '/';
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
console.log('解析结果:', { protocol, domain, path });
|
|
|
|
|
|
|
|
|
|
|
|
// 使用config.baseUrl或config.imgBaseUrl作为新域名
|
|
|
|
|
|
let baseUrl = config.baseUrl || config.imgBaseUrl;
|
|
|
|
|
|
// 如果baseUrl以/api/ks结尾,去掉这个部分
|
|
|
|
|
|
if (baseUrl.endsWith('/api/ks')) {
|
|
|
|
|
|
baseUrl = baseUrl.substring(0, baseUrl.length - 7);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 构建新URL:使用新域名 + 原路径
|
|
|
|
|
|
// 确保baseUrl不以/结尾,path以/开头
|
|
|
|
|
|
let cleanBaseUrl = baseUrl;
|
|
|
|
|
|
if (cleanBaseUrl.endsWith('/')) {
|
|
|
|
|
|
cleanBaseUrl = cleanBaseUrl.substring(0, cleanBaseUrl.length - 1);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!path.startsWith('/')) {
|
|
|
|
|
|
path = '/' + path;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
displayUrl = cleanBaseUrl + path;
|
|
|
|
|
|
console.log('替换域名后的displayUrl(用于显示):', displayUrl);
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if (displayUrl && !displayUrl.startsWith('http')) {
|
|
|
|
|
|
// 如果filePath不是完整的URL,直接添加图片基础URL前缀
|
|
|
|
|
|
displayUrl = config.imgBaseUrl + displayUrl;
|
|
|
|
|
|
console.log('非完整URL,添加imgBaseUrl前缀:', displayUrl);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
url: displayUrl, // 使用处理后的完整URL(用于缩略图显示)
|
|
|
|
|
|
serverPath: serverPath, // 原始服务器路径(内网地址,用于对话接口)
|
|
|
|
|
|
type: fileType,
|
|
|
|
|
|
name: fileName,
|
|
|
|
|
|
size: fileSize > 0 ? $api.formatFileSize(fileSize) : '',
|
|
|
|
|
|
tempPath: tempPath // 保存临时路径用于预览
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 处理选择的文件(公共逻辑)
|
|
|
|
|
|
function handleSelectedFiles(tempFilePaths, files) {
|
|
|
|
|
|
// 检查文件数量限制
|
|
|
|
|
|
if (VerifyNumberFiles(files.length)) return;
|
|
|
|
|
|
|
|
|
|
|
|
const allowedTypes = config.allowedFileTypes || [];
|
|
|
|
|
|
|
|
|
|
|
|
// 调试:在文件类型检查之前记录详细信息
|
|
|
|
|
|
console.log('=== handleSelectedFiles 文件类型检查开始 ===');
|
|
|
|
|
|
console.log('allowedTypes配置:', allowedTypes);
|
|
|
|
|
|
console.log('files数组长度:', files.length);
|
|
|
|
|
|
files.forEach((file, index) => {
|
|
|
|
|
|
console.log(`文件 ${index} 检查前详情:`, {
|
|
|
|
|
|
name: file.name,
|
|
|
|
|
|
type: file.type,
|
|
|
|
|
|
hasType: 'type' in file,
|
|
|
|
|
|
typeValue: file.type,
|
|
|
|
|
|
typeIsString: typeof file.type === 'string',
|
|
|
|
|
|
typeLength: file.type ? file.type.length : 0,
|
|
|
|
|
|
size: file.size,
|
|
|
|
|
|
path: file.path
|
|
|
|
|
|
});
|
|
|
|
|
|
// 尝试从文件名推断类型
|
|
|
|
|
|
const inferredType = getFileTypeFromName(file.name);
|
|
|
|
|
|
console.log(`文件 ${index} 从文件名推断的类型:`, inferredType);
|
|
|
|
|
|
// 检查是否在allowedTypes中
|
|
|
|
|
|
const isAllowedByType = allowedTypes.includes(file.type);
|
|
|
|
|
|
const isAllowedByInferred = allowedTypes.includes(inferredType);
|
|
|
|
|
|
const isImageFile = isImage(file.type, file.name);
|
|
|
|
|
|
console.log(`文件 ${index} 检查结果:`, {
|
|
|
|
|
|
isAllowedByType,
|
|
|
|
|
|
isAllowedByInferred,
|
|
|
|
|
|
isImageFile,
|
|
|
|
|
|
finalAllowed: isAllowedByType || isAllowedByInferred || isImageFile
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
console.log('=== handleSelectedFiles 文件类型检查结束 ===');
|
|
|
|
|
|
|
|
|
|
|
|
// 检查所有文件类型 - 增强版:优先使用file.type,回退到文件名推断
|
|
|
|
|
|
const invalidFiles = files.filter(file => {
|
|
|
|
|
|
// 如果文件类型存在且在允许的类型列表中,则通过
|
|
|
|
|
|
if (file.type && allowedTypes.includes(file.type)) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 尝试从文件名推断类型
|
|
|
|
|
|
const inferredType = getFileTypeFromName(file.name);
|
|
|
|
|
|
if (inferredType && allowedTypes.includes(inferredType)) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 如果是图片,也通过
|
|
|
|
|
|
if (isImage(file.type, file.name)) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 否则,文件无效
|
|
|
|
|
|
return true;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (invalidFiles.length > 0) {
|
|
|
|
|
|
// 调试:查看不支持的文件类型
|
|
|
|
|
|
console.log('不支持的文件类型:', invalidFiles.map(f => ({name: f.name, type: f.type})));
|
|
|
|
|
|
// 更详细地显示为什么这些文件不被支持
|
|
|
|
|
|
invalidFiles.forEach((file, index) => {
|
|
|
|
|
|
const inferredType = getFileTypeFromName(file.name);
|
|
|
|
|
|
console.log(`无效文件 ${index} 分析:`, {
|
|
|
|
|
|
name: file.name,
|
|
|
|
|
|
type: file.type,
|
|
|
|
|
|
inferredType,
|
|
|
|
|
|
isImage: isImage(file.type, file.name),
|
|
|
|
|
|
isInAllowedTypes: allowedTypes.includes(file.type),
|
|
|
|
|
|
isInferredInAllowedTypes: allowedTypes.includes(inferredType)
|
2025-03-28 15:19:42 +08:00
|
|
|
|
});
|
2026-04-09 21:14:25 +08:00
|
|
|
|
});
|
|
|
|
|
|
return $api.msg('仅支持 txt md html word doc docx pdf ppt pptx csv excel xlsx 和图片格式');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 显示上传提示
|
|
|
|
|
|
const imageCount = files.filter(file => isImage(file.type, file.name)).length;
|
|
|
|
|
|
const otherFileCount = files.length - imageCount;
|
|
|
|
|
|
let tip = '开始上传';
|
|
|
|
|
|
if (imageCount > 0) tip += `${imageCount}张图片`;
|
|
|
|
|
|
if (otherFileCount > 0) {
|
|
|
|
|
|
if (imageCount > 0) tip += '和';
|
|
|
|
|
|
tip += `${otherFileCount}个文件`;
|
|
|
|
|
|
}
|
|
|
|
|
|
$api.msg(tip + '...');
|
|
|
|
|
|
|
|
|
|
|
|
// 上传所有文件
|
|
|
|
|
|
const uploadPromises = tempFilePaths.map((filePath, index) => {
|
|
|
|
|
|
return $api.uploadFile(filePath, true).then((resData) => {
|
|
|
|
|
|
resData = JSON.parse(resData);
|
|
|
|
|
|
console.log('服务器返回的resData:', resData);
|
|
|
|
|
|
if (resData.code === 200 && resData.filePath) {
|
|
|
|
|
|
// 从文件对象或文件路径获取文件信息
|
|
|
|
|
|
const fileInfo = files[index] || {};
|
|
|
|
|
|
const fileName = fileInfo.name || getFileNameFromPath(tempFilePaths[index]);
|
|
|
|
|
|
const fileType = fileInfo.type || getFileTypeFromName(fileName);
|
|
|
|
|
|
const fileSize = fileInfo.size || 0;
|
|
|
|
|
|
|
|
|
|
|
|
// 使用统一的文件路径处理函数
|
|
|
|
|
|
const processedFile = processFilePath(
|
|
|
|
|
|
resData.filePath, // 原始服务器路径
|
|
|
|
|
|
fileName,
|
|
|
|
|
|
fileType,
|
|
|
|
|
|
fileSize,
|
|
|
|
|
|
tempFilePaths[index] // 临时路径用于预览
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// 调试:记录文件信息
|
|
|
|
|
|
console.log('上传文件信息:', {
|
|
|
|
|
|
fileInfo: fileInfo,
|
|
|
|
|
|
type: fileType,
|
|
|
|
|
|
name: fileName,
|
|
|
|
|
|
size: fileSize,
|
|
|
|
|
|
isImage: isImage(fileType, fileName),
|
|
|
|
|
|
originalFilePath: resData.filePath,
|
|
|
|
|
|
displayUrl: processedFile.url,
|
|
|
|
|
|
serverPath: processedFile.serverPath,
|
|
|
|
|
|
imgBaseUrl: config.imgBaseUrl
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return processedFile;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
throw new Error(resData.msg || '上传失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
Promise.all(uploadPromises).then((uploadedFiles) => {
|
|
|
|
|
|
// 将所有上传成功的文件添加到filesList
|
|
|
|
|
|
uploadedFiles.forEach(file => {
|
|
|
|
|
|
if (isImage(file.type, file.name)) {
|
|
|
|
|
|
file.loading = true; // 图片开始加载
|
|
|
|
|
|
file.error = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
filesList.value.push(file);
|
|
|
|
|
|
});
|
|
|
|
|
|
textInput.value = state.uploadFileTips;
|
|
|
|
|
|
showfile.value = false; // 关闭上传面板
|
|
|
|
|
|
$api.msg(`成功上传${uploadedFiles.length}个文件`);
|
|
|
|
|
|
}).catch((error) => {
|
|
|
|
|
|
$api.msg(error.message || '文件上传失败');
|
2025-03-28 15:19:42 +08:00
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-04-07 09:10:55 +08:00
|
|
|
|
const tipsPermisson = () => {
|
|
|
|
|
|
uni.showToast({
|
|
|
|
|
|
title: '需要授权麦克风权限才能使用语音功能',
|
|
|
|
|
|
icon: 'none',
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleTouchStart = async (e) => {
|
|
|
|
|
|
if (!isAudioPermission.value) {
|
|
|
|
|
|
return tipsPermisson();
|
|
|
|
|
|
}
|
2025-04-10 10:59:25 +08:00
|
|
|
|
cancelAudio();
|
2025-03-28 15:19:42 +08:00
|
|
|
|
startY.value = e.touches[0].clientY;
|
|
|
|
|
|
status.value = 'recording';
|
|
|
|
|
|
showfile.value = false;
|
|
|
|
|
|
startRecording();
|
2025-04-07 09:10:55 +08:00
|
|
|
|
$api.sleep(1000).then(() => {
|
|
|
|
|
|
scrollToBottom();
|
|
|
|
|
|
});
|
2025-03-28 15:19:42 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleTouchMove = (e) => {
|
|
|
|
|
|
const moveY = e.touches[0].clientY;
|
|
|
|
|
|
if (startY.value - moveY > cancelThreshold) {
|
|
|
|
|
|
status.value = 'cancel';
|
|
|
|
|
|
} else {
|
|
|
|
|
|
status.value = 'recording';
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleTouchEnd = () => {
|
|
|
|
|
|
if (status.value === 'cancel') {
|
|
|
|
|
|
console.log('取消发送');
|
|
|
|
|
|
cancelRecording();
|
2026-01-23 22:01:38 +08:00
|
|
|
|
status.value = 'idle';
|
2025-03-28 15:19:42 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
stopRecording();
|
2025-04-16 14:24:06 +08:00
|
|
|
|
if (isAudioPermission.value) {
|
2026-01-23 22:01:38 +08:00
|
|
|
|
// 主要根据录音时长判断,而不是完全依赖识别结果
|
|
|
|
|
|
// 由于setInterval是异步的,这里需要考虑计时延迟
|
|
|
|
|
|
const actualDuration = recordingDuration.value > 0 ? recordingDuration.value : (isRecording.value ? 0.5 : 0);
|
|
|
|
|
|
if (actualDuration < 1) {
|
2025-04-16 14:24:06 +08:00
|
|
|
|
$api.msg('说话时长太短');
|
2026-01-23 22:01:38 +08:00
|
|
|
|
status.value = 'idle';
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 状态管理由useAudioRecorder hook内部处理
|
|
|
|
|
|
status.value = 'idle';
|
2025-04-07 09:10:55 +08:00
|
|
|
|
}
|
2025-04-16 14:24:06 +08:00
|
|
|
|
}
|
2025-03-28 15:19:42 +08:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleTouchCancel = () => {
|
|
|
|
|
|
stopRecording();
|
|
|
|
|
|
status.value = 'idle';
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
function closeGuess() {
|
|
|
|
|
|
showGuess.value = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function changeVoice() {
|
|
|
|
|
|
isVoice.value = !isVoice.value;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function changeShowFile() {
|
|
|
|
|
|
showfile.value = !showfile.value;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-21 22:58:47 +08:00
|
|
|
|
function closeFile() {
|
2025-03-28 15:19:42 +08:00
|
|
|
|
showfile.value = false;
|
|
|
|
|
|
}
|
2025-04-07 09:10:55 +08:00
|
|
|
|
|
|
|
|
|
|
function copyMarkdown(value) {
|
|
|
|
|
|
$api.copyText(value);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function userGoodFeedback(msg) {
|
2025-04-16 14:24:06 +08:00
|
|
|
|
// $api.msg('该功能正在开发中,敬请期待后续更新!');
|
|
|
|
|
|
feeback.value?.open();
|
|
|
|
|
|
feebackData.value = msg;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function confirmFeeBack(value) {
|
|
|
|
|
|
useChatGroupDBStore()
|
|
|
|
|
|
.badFeedback(feebackData.value, value)
|
|
|
|
|
|
.then(() => {
|
|
|
|
|
|
feeback.value?.close();
|
|
|
|
|
|
feeBackTips.value?.open();
|
|
|
|
|
|
});
|
2025-04-07 09:10:55 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-13 16:01:49 +08:00
|
|
|
|
// 防抖定时器
|
|
|
|
|
|
let ttsDebounceTimer = null;
|
|
|
|
|
|
|
2026-01-24 18:29:47 +08:00
|
|
|
|
// 保存上一次调用的文本内容,避免重复调用TTS
|
|
|
|
|
|
let lastSpeechText = '';
|
|
|
|
|
|
|
|
|
|
|
|
function readMarkdown(value, index, options = {}) {
|
2025-10-13 16:01:49 +08:00
|
|
|
|
console.log('🎤 readMarkdown called');
|
|
|
|
|
|
console.log('📝 Text to speak:', value ? value.substring(0, 100) + '...' : 'No text');
|
|
|
|
|
|
console.log('🔢 Index:', index);
|
|
|
|
|
|
console.log('🔢 Current speechIndex:', speechIndex.value);
|
|
|
|
|
|
console.log('⏸️ Is paused:', isPaused.value);
|
|
|
|
|
|
console.log('🔊 Is speaking:', isSpeaking.value);
|
|
|
|
|
|
|
|
|
|
|
|
// 清除之前的防抖定时器
|
|
|
|
|
|
if (ttsDebounceTimer) {
|
|
|
|
|
|
clearTimeout(ttsDebounceTimer);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-24 17:57:25 +08:00
|
|
|
|
// 总是先停止当前播放,无论是不是同一消息
|
|
|
|
|
|
console.log('🛑 Always stopping current speech before starting new one');
|
2025-10-13 16:01:49 +08:00
|
|
|
|
speechIndex.value = index;
|
|
|
|
|
|
|
2026-01-24 18:29:47 +08:00
|
|
|
|
// 立即调用speak,不使用防抖延迟
|
|
|
|
|
|
const speakNow = () => {
|
|
|
|
|
|
// 检查文本内容是否发生变化,避免重复调用TTS
|
|
|
|
|
|
if (value !== lastSpeechText) {
|
|
|
|
|
|
console.log('🎵 Starting new speech');
|
|
|
|
|
|
console.log('🎵 Calling speak function with text length:', value ? value.length : 0);
|
|
|
|
|
|
try {
|
|
|
|
|
|
speak(value);
|
|
|
|
|
|
console.log('✅ Speak function called successfully');
|
|
|
|
|
|
// 更新上一次调用的文本内容
|
|
|
|
|
|
lastSpeechText = value;
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('❌ Error calling speak function:', error);
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.log('🔄 Same text as last speech, skipping duplicate TTS call');
|
2025-10-13 16:01:49 +08:00
|
|
|
|
}
|
2026-01-24 18:29:47 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 改进防抖逻辑,确保在短时间内只调用一次
|
|
|
|
|
|
if (options.immediate) {
|
|
|
|
|
|
// 如果是onComplete回调,立即播放
|
|
|
|
|
|
speakNow();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 对于流式数据,总是使用防抖,避免频繁调用
|
|
|
|
|
|
ttsDebounceTimer = setTimeout(speakNow, 500); // 延长防抖时间到500ms
|
|
|
|
|
|
}
|
2025-04-07 09:10:55 +08:00
|
|
|
|
}
|
|
|
|
|
|
function stopMarkdown(value, index) {
|
2025-10-13 16:01:49 +08:00
|
|
|
|
console.log('⏸️ stopMarkdown called for index:', index);
|
|
|
|
|
|
console.log('🔢 Current speechIndex:', speechIndex.value);
|
|
|
|
|
|
console.log('🔊 Is speaking:', isSpeaking.value);
|
|
|
|
|
|
console.log('⏸️ Is paused:', isPaused.value);
|
|
|
|
|
|
|
|
|
|
|
|
// 清除防抖定时器
|
|
|
|
|
|
if (ttsDebounceTimer) {
|
|
|
|
|
|
clearTimeout(ttsDebounceTimer);
|
|
|
|
|
|
ttsDebounceTimer = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-04-07 09:10:55 +08:00
|
|
|
|
speechIndex.value = index;
|
2025-10-13 16:01:49 +08:00
|
|
|
|
pause();
|
2025-04-07 09:10:55 +08:00
|
|
|
|
}
|
|
|
|
|
|
function refreshMarkdown(index) {
|
|
|
|
|
|
if (isTyping.value) {
|
|
|
|
|
|
$api.msg('正在生成');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
const text = messages.value[index - 1].text;
|
|
|
|
|
|
sendMessageGuess(text);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const jobSearchQueries = [
|
2026-01-22 12:57:01 +08:00
|
|
|
|
'喀什地区有哪些薪资 12K 以上的岗位适合我?',
|
|
|
|
|
|
'喀什地区 3 年工作经验能找到哪些 12K 以上的工作?',
|
|
|
|
|
|
'喀什地区哪些公司在招聘,薪资范围在 12K 以上?',
|
|
|
|
|
|
'喀什地区有哪些企业提供 15K 以上的岗位?',
|
|
|
|
|
|
'喀什地区哪些公司在招 3-5 年经验的岗位?',
|
|
|
|
|
|
'我有三年的工作经验,能否推荐一些适合我的喀什地区的国企 岗位?',
|
|
|
|
|
|
'喀什地区国企目前在招聘哪些岗位?',
|
|
|
|
|
|
'喀什地区有哪些适合 3 年经验的国企岗位?',
|
|
|
|
|
|
'喀什地区国企招聘的岗位待遇如何?',
|
|
|
|
|
|
'喀什地区国企岗位的薪资水平是多少?',
|
|
|
|
|
|
'喀什地区哪些国企支持双休 & 五险一金完善?',
|
|
|
|
|
|
'喀什地区有哪些公司支持远程办公?',
|
|
|
|
|
|
'喀什地区有哪些外企的岗位,薪资 12K 以上的多吗?',
|
|
|
|
|
|
'喀什地区哪些企业在招聘 Web3.0 相关岗位?',
|
|
|
|
|
|
'喀什地区哪些岗位支持海外远程?薪资如何?',
|
|
|
|
|
|
'喀什地区招聘 AI/大数据相关岗位的公司有哪些?',
|
2025-04-07 09:10:55 +08:00
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
function changeQueries(value) {
|
|
|
|
|
|
queries.value = getRandomJobQueries(jobSearchQueries);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function getRandomJobQueries(queries, count = 2) {
|
|
|
|
|
|
const shuffled = queries.sort(() => 0.5 - Math.random()); // 随机打乱数组
|
|
|
|
|
|
return shuffled.slice(0, count); // 取前 count 条
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-03 14:50:56 +08:00
|
|
|
|
// 切换tab
|
|
|
|
|
|
function switchTab(tab) {
|
|
|
|
|
|
activeTab.value = tab;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 检查消息是否应该显示在当前tab
|
|
|
|
|
|
function shouldShowMessage(msg) {
|
|
|
|
|
|
if (msg.self) {
|
|
|
|
|
|
return true; // 用户自己的消息总是显示
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (activeTab.value === 'policy') {
|
|
|
|
|
|
// 政策查询tab:显示除了岗位卡片以外的所有内容
|
|
|
|
|
|
// 岗位卡片通常包含特定的标记,如```job-json或岗位推荐等关键词
|
|
|
|
|
|
const isJobCard = msg.displayText && (
|
|
|
|
|
|
msg.displayText.includes('```job-json') ||
|
|
|
|
|
|
msg.displayText.includes('岗位推荐') ||
|
|
|
|
|
|
msg.displayText.includes('推荐岗位') ||
|
|
|
|
|
|
msg.displayText.includes('岗位信息') ||
|
|
|
|
|
|
(msg.displayText.includes('```') && msg.displayText.includes('公司') && msg.displayText.includes('薪资'))
|
|
|
|
|
|
);
|
|
|
|
|
|
return !isJobCard;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 岗位推荐tab:只显示岗位卡片内容
|
|
|
|
|
|
const isJobCard = msg.displayText && (
|
|
|
|
|
|
msg.displayText.includes('```job-json') ||
|
|
|
|
|
|
msg.displayText.includes('岗位推荐') ||
|
|
|
|
|
|
msg.displayText.includes('推荐岗位') ||
|
|
|
|
|
|
msg.displayText.includes('岗位信息') ||
|
|
|
|
|
|
(msg.displayText.includes('```') && msg.displayText.includes('公司') && msg.displayText.includes('薪资'))
|
|
|
|
|
|
);
|
|
|
|
|
|
return isJobCard;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-16 14:28:38 +08:00
|
|
|
|
// 长按事件处理函数
|
|
|
|
|
|
function handleLongPress(msg, index) {
|
|
|
|
|
|
isSelectMode.value = true;
|
|
|
|
|
|
selectedMessages.value = [index];
|
|
|
|
|
|
|
|
|
|
|
|
// 如果长按的是用户消息,同时选中对应的AI回复
|
|
|
|
|
|
if (msg.self) {
|
|
|
|
|
|
const nextIndex = index + 1;
|
|
|
|
|
|
if (nextIndex < messages.value.length && !messages.value[nextIndex].self) {
|
|
|
|
|
|
selectedMessages.value.push(nextIndex);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 选择/取消选择消息
|
|
|
|
|
|
function toggleSelectMessage(index) {
|
|
|
|
|
|
const msg = messages.value[index];
|
|
|
|
|
|
const idx = selectedMessages.value.indexOf(index);
|
|
|
|
|
|
|
|
|
|
|
|
if (idx > -1) {
|
|
|
|
|
|
// 取消选择
|
|
|
|
|
|
selectedMessages.value.splice(idx, 1);
|
|
|
|
|
|
|
|
|
|
|
|
// 如果取消选择的是用户消息,同时取消选择对应的AI回复
|
|
|
|
|
|
if (msg.self) {
|
|
|
|
|
|
const nextIndex = index + 1;
|
|
|
|
|
|
if (nextIndex < messages.value.length && !messages.value[nextIndex].self) {
|
|
|
|
|
|
const aiIdx = selectedMessages.value.indexOf(nextIndex);
|
|
|
|
|
|
if (aiIdx > -1) {
|
|
|
|
|
|
selectedMessages.value.splice(aiIdx, 1);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (selectedMessages.value.length === 0) {
|
|
|
|
|
|
isSelectMode.value = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 选择消息
|
|
|
|
|
|
selectedMessages.value.push(index);
|
|
|
|
|
|
|
|
|
|
|
|
// 如果选择的是用户消息,同时选择对应的AI回复
|
|
|
|
|
|
if (msg.self) {
|
|
|
|
|
|
const nextIndex = index + 1;
|
|
|
|
|
|
if (nextIndex < messages.value.length && !messages.value[nextIndex].self) {
|
|
|
|
|
|
if (!selectedMessages.value.includes(nextIndex)) {
|
|
|
|
|
|
selectedMessages.value.push(nextIndex);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 退出选择模式
|
|
|
|
|
|
function exitSelectMode() {
|
|
|
|
|
|
isSelectMode.value = false;
|
|
|
|
|
|
selectedMessages.value = [];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 全选消息
|
|
|
|
|
|
function selectAllMessages() {
|
|
|
|
|
|
selectedMessages.value = messages.value.map((_, index) => index);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 删除选中消息
|
|
|
|
|
|
function deleteSelectedMessages() {
|
|
|
|
|
|
if (selectedMessages.value.length === 0) return;
|
|
|
|
|
|
|
|
|
|
|
|
uni.showModal({
|
|
|
|
|
|
content: `确定删除${selectedMessages.value.length}条消息吗?`,
|
|
|
|
|
|
success(res) {
|
|
|
|
|
|
if (res.confirm) {
|
|
|
|
|
|
// 调用store中的删除方法
|
|
|
|
|
|
useChatGroupDBStore().deleteMessages(selectedMessages.value);
|
|
|
|
|
|
// 退出选择模式
|
|
|
|
|
|
exitSelectMode();
|
|
|
|
|
|
$api.msg('消息删除成功');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-03 14:50:56 +08:00
|
|
|
|
defineExpose({ scrollToBottom, closeGuess, closeFile, changeQueries, handleTouchCancel, switchTab });
|
2025-03-28 15:19:42 +08:00
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style lang="stylus" scoped>
|
|
|
|
|
|
/* 过渡样式 */
|
|
|
|
|
|
.collapse-enter-active,
|
|
|
|
|
|
.collapse-leave-active {
|
|
|
|
|
|
transition: max-height 0.3s ease, opacity 0.3s ease;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.collapse-enter-from,
|
|
|
|
|
|
.collapse-leave-to {
|
|
|
|
|
|
max-height: 0;
|
|
|
|
|
|
opacity: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.collapse-enter-to,
|
|
|
|
|
|
.collapse-leave-from {
|
|
|
|
|
|
max-height: 400rpx; /* 根据你内容最大高度设定 */
|
|
|
|
|
|
opacity: 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
.msg-filecontent
|
|
|
|
|
|
display: flex
|
|
|
|
|
|
flex-wrap: wrap
|
|
|
|
|
|
.msg-files
|
|
|
|
|
|
overflow: hidden
|
|
|
|
|
|
margin-right: 10rpx
|
|
|
|
|
|
height: 30rpx
|
|
|
|
|
|
max-width: 201rpx
|
|
|
|
|
|
background: #FFFFFF
|
|
|
|
|
|
border-radius: 10rpx
|
|
|
|
|
|
display: flex
|
|
|
|
|
|
align-items: center
|
|
|
|
|
|
justify-content: flex-start
|
|
|
|
|
|
padding: 10rpx
|
|
|
|
|
|
color: #000000
|
|
|
|
|
|
margin-bottom: 10rpx
|
|
|
|
|
|
.msg-file-icon
|
|
|
|
|
|
width: 29rpx
|
|
|
|
|
|
height: 26rpx
|
|
|
|
|
|
padding-right: 10rpx
|
|
|
|
|
|
.msg-file-text
|
|
|
|
|
|
flex: 1
|
|
|
|
|
|
white-space: nowrap
|
|
|
|
|
|
overflow: hidden
|
|
|
|
|
|
text-overflow: ellipsis
|
|
|
|
|
|
color: rgba(96, 96, 96, 1)
|
|
|
|
|
|
font-size: 24rpx
|
|
|
|
|
|
.msg-files:active
|
|
|
|
|
|
background: #e9e9e9
|
2026-04-09 21:14:25 +08:00
|
|
|
|
|
|
|
|
|
|
// 消息中的图片文件样式
|
|
|
|
|
|
.msg-image-file
|
|
|
|
|
|
height: 120rpx
|
|
|
|
|
|
width: 120rpx
|
|
|
|
|
|
padding: 4rpx
|
|
|
|
|
|
flex-direction: column
|
|
|
|
|
|
justify-content: center
|
|
|
|
|
|
.msg-image-thumbnail
|
|
|
|
|
|
width: 100%
|
|
|
|
|
|
height: 80rpx
|
|
|
|
|
|
border-radius: 8rpx
|
|
|
|
|
|
margin-bottom: 8rpx
|
|
|
|
|
|
.msg-file-text
|
|
|
|
|
|
font-size: 20rpx
|
|
|
|
|
|
text-align: center
|
|
|
|
|
|
white-space: nowrap
|
|
|
|
|
|
overflow: hidden
|
|
|
|
|
|
text-overflow: ellipsis
|
2025-03-28 15:19:42 +08:00
|
|
|
|
.guess
|
2025-03-29 20:46:23 +08:00
|
|
|
|
padding: 5rpx 0 10rpx 0
|
2025-03-28 15:19:42 +08:00
|
|
|
|
.guess-list
|
2025-03-29 20:46:23 +08:00
|
|
|
|
padding: 16rpx 24rpx
|
|
|
|
|
|
margin-top: 28rpx
|
2025-03-28 15:19:42 +08:00
|
|
|
|
font-size: 24rpx
|
|
|
|
|
|
color: #8c8c8c
|
2025-03-29 20:46:23 +08:00
|
|
|
|
width: 100%;
|
|
|
|
|
|
border-radius: 20rpx 20rpx 20rpx 20rpx;
|
|
|
|
|
|
border: 2rpx solid #E5E5E5;
|
|
|
|
|
|
font-size: 28rpx;
|
|
|
|
|
|
color: #333333;
|
|
|
|
|
|
line-height: 33rpx;
|
2025-03-28 15:19:42 +08:00
|
|
|
|
.gulist
|
|
|
|
|
|
display: flex
|
|
|
|
|
|
flex-wrap: wrap
|
|
|
|
|
|
position: relative
|
|
|
|
|
|
image-margin-top = 40rpx
|
|
|
|
|
|
.chat-container
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
2025-04-10 10:59:25 +08:00
|
|
|
|
height: 100%;
|
2025-03-28 15:19:42 +08:00
|
|
|
|
position: relative
|
|
|
|
|
|
z-index: 1
|
|
|
|
|
|
background: #FFFFFF
|
|
|
|
|
|
|
|
|
|
|
|
.chat-background
|
|
|
|
|
|
position: absolute
|
|
|
|
|
|
padding: 44rpx;
|
|
|
|
|
|
display: flex
|
|
|
|
|
|
flex-direction: column
|
|
|
|
|
|
justify-content: flex-start
|
|
|
|
|
|
align-items: center
|
2025-04-07 09:10:55 +08:00
|
|
|
|
width: calc(100% - 88rpx)
|
2025-05-13 11:10:38 +08:00
|
|
|
|
position: relative
|
|
|
|
|
|
z-index: 1
|
2025-03-28 15:19:42 +08:00
|
|
|
|
.backlogo
|
|
|
|
|
|
width: 313rpx;
|
|
|
|
|
|
height: 190rpx;
|
|
|
|
|
|
.back-rowTitle
|
|
|
|
|
|
height: 56rpx;
|
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
|
font-size: 40rpx;
|
|
|
|
|
|
color: #333333;
|
|
|
|
|
|
line-height: 47rpx;
|
|
|
|
|
|
margin-top: 40rpx
|
|
|
|
|
|
.back-rowText
|
|
|
|
|
|
margin-top: 28rpx
|
|
|
|
|
|
width: 100%;
|
2025-04-07 09:10:55 +08:00
|
|
|
|
height: 148rpx;
|
2025-03-28 15:19:42 +08:00
|
|
|
|
font-weight: 400;
|
|
|
|
|
|
font-size: 28rpx;
|
|
|
|
|
|
color: #333333;
|
2025-04-07 09:10:55 +08:00
|
|
|
|
line-height: 40rpx
|
|
|
|
|
|
// border-bottom: 2rpx dashed rgba(0, 0, 0, 0.2);
|
|
|
|
|
|
border-bottom: 2rpx solid transparent;
|
|
|
|
|
|
background-image: linear-gradient(to right, rgba(0, 0, 0, 0.2) 50%, transparent 50%);
|
|
|
|
|
|
background-size: 10rpx 2rpx; /* 调整虚线宽度和间距 */
|
|
|
|
|
|
background-repeat: repeat-x;
|
|
|
|
|
|
background-position: 0 148rpx
|
2025-03-28 15:19:42 +08:00
|
|
|
|
.back-rowh3
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
font-weight: 500;
|
2025-04-07 09:10:55 +08:00
|
|
|
|
font-size: 28rpx;
|
2025-03-28 15:19:42 +08:00
|
|
|
|
color: #000000;
|
2025-04-07 09:10:55 +08:00
|
|
|
|
margin-top: 28rpx
|
2025-03-28 15:19:42 +08:00
|
|
|
|
.back-rowmsg
|
2025-04-07 09:10:55 +08:00
|
|
|
|
width: calc(100% - 32rpx)
|
|
|
|
|
|
margin-top: 24rpx
|
2025-03-28 15:19:42 +08:00
|
|
|
|
font-weight: 500;
|
2025-04-07 09:10:55 +08:00
|
|
|
|
color: rgba(51, 51, 51, 1);
|
2025-03-28 15:19:42 +08:00
|
|
|
|
line-height: 28rpx;
|
|
|
|
|
|
font-size: 24rpx
|
2025-04-07 09:10:55 +08:00
|
|
|
|
background: rgba(246, 246, 246, 1);
|
2025-03-28 15:19:42 +08:00
|
|
|
|
border-radius: 8rpx 8rpx 8rpx 8rpx;
|
|
|
|
|
|
padding: 32rpx 18rpx;
|
|
|
|
|
|
|
|
|
|
|
|
.chat-list {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
|
white-space: pre-wrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
.list-content {
|
2025-10-24 16:54:52 +08:00
|
|
|
|
padding: 0 44rpx 10rpx 44rpx;
|
2025-03-28 15:19:42 +08:00
|
|
|
|
}
|
|
|
|
|
|
.chat-item {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: flex-start;
|
2025-10-24 16:54:52 +08:00
|
|
|
|
margin-bottom: 10rpx;
|
2025-04-07 09:10:55 +08:00
|
|
|
|
width: 100%;
|
2025-03-28 15:19:42 +08:00
|
|
|
|
}
|
|
|
|
|
|
.chat-item.self {
|
|
|
|
|
|
justify-content: flex-end;
|
|
|
|
|
|
}
|
2025-03-29 20:46:23 +08:00
|
|
|
|
.message
|
2025-10-24 16:54:52 +08:00
|
|
|
|
margin-top: 0rpx
|
2025-03-28 15:19:42 +08:00
|
|
|
|
// max-width: 80%;
|
2025-03-29 20:46:23 +08:00
|
|
|
|
width: 100%;
|
2025-03-28 15:19:42 +08:00
|
|
|
|
word-break: break-word;
|
|
|
|
|
|
color: #333333;
|
|
|
|
|
|
user-select: text;
|
|
|
|
|
|
-webkit-user-select: text;
|
2025-03-29 20:46:23 +08:00
|
|
|
|
.message-markdown
|
|
|
|
|
|
border-radius: 0 20rpx 20rpx 20rpx;
|
2025-04-16 14:24:06 +08:00
|
|
|
|
padding: 20rpx 20rpx 20rpx 20rpx;
|
2025-03-29 20:46:23 +08:00
|
|
|
|
background: #F6F6F6;
|
2025-04-07 09:10:55 +08:00
|
|
|
|
.message-controll
|
|
|
|
|
|
display: flex
|
|
|
|
|
|
justify-content: space-between
|
|
|
|
|
|
align-items: center
|
|
|
|
|
|
border-top: 2rpx solid #EAEAEA
|
2025-04-16 14:24:06 +08:00
|
|
|
|
padding: 24rpx 0 4rpx 0
|
2025-04-10 10:59:25 +08:00
|
|
|
|
margin-top: 10rpx
|
2025-04-07 09:10:55 +08:00
|
|
|
|
.controll-left
|
|
|
|
|
|
.controll-right
|
|
|
|
|
|
.controll-icon
|
|
|
|
|
|
width: 60rpx;
|
|
|
|
|
|
height: 60rpx;
|
|
|
|
|
|
|
2025-03-28 15:19:42 +08:00
|
|
|
|
.messageNull
|
2025-03-29 20:46:23 +08:00
|
|
|
|
display: none
|
2025-03-28 15:19:42 +08:00
|
|
|
|
.msg-loading{
|
2026-01-22 14:05:22 +08:00
|
|
|
|
background: #F6F6F6;
|
|
|
|
|
|
border-radius: 20rpx 0 20rpx 20rpx;
|
|
|
|
|
|
padding: 20rpx;
|
|
|
|
|
|
width: fit-content;
|
2025-03-28 15:19:42 +08:00
|
|
|
|
display: flex;
|
2026-01-22 14:05:22 +08:00
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
|
|
|
|
|
|
.loading-content{
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
gap: 16rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.loading-text{
|
|
|
|
|
|
font-size: 28rpx;
|
|
|
|
|
|
color: #666666;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
}
|
2025-03-28 15:19:42 +08:00
|
|
|
|
}
|
|
|
|
|
|
.loaded{
|
|
|
|
|
|
padding-left: 20rpx
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat-item.self .message {
|
|
|
|
|
|
background: linear-gradient( 225deg, #DAE2FE 0%, #E9E3FF 100%);
|
|
|
|
|
|
border-radius: 20rpx 0 20rpx 20rpx;
|
|
|
|
|
|
padding: 20rpx;
|
2025-03-29 20:46:23 +08:00
|
|
|
|
width: fit-content;
|
2025-03-28 15:19:42 +08:00
|
|
|
|
}
|
|
|
|
|
|
.input-area {
|
2025-03-28 18:19:05 +08:00
|
|
|
|
padding: 32rpx 28rpx 24rpx 28rpx;
|
2025-12-22 12:14:27 +08:00
|
|
|
|
/* #ifdef H5 */
|
|
|
|
|
|
padding-bottom: calc(24rpx + env(safe-area-inset-bottom) + 70rpx);
|
|
|
|
|
|
/* #endif */
|
|
|
|
|
|
/* #ifdef MP-WEIXIN */
|
|
|
|
|
|
/* 小程序不支持CSS中的本地图片,使用image标签替代 */
|
|
|
|
|
|
padding-bottom: calc(24rpx + env(safe-area-inset-bottom) + 40rpx - 50rpx);
|
|
|
|
|
|
/* #endif */
|
2025-03-28 15:19:42 +08:00
|
|
|
|
position: relative;
|
|
|
|
|
|
background: #FFFFFF;
|
|
|
|
|
|
box-shadow: 0rpx -4rpx 10rpx 0rpx rgba(11,44,112,0.06);
|
|
|
|
|
|
transition: height 2s ease-in-out;
|
2025-10-24 16:54:52 +08:00
|
|
|
|
z-index: 1001;
|
2025-03-28 15:19:42 +08:00
|
|
|
|
}
|
2025-12-22 12:14:27 +08:00
|
|
|
|
.ai-tips{
|
|
|
|
|
|
font-size: 24rpx;
|
|
|
|
|
|
color: #8c8c8c;
|
|
|
|
|
|
line-height: 33rpx;
|
|
|
|
|
|
margin-top: 18rpx;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
}
|
2025-03-28 15:19:42 +08:00
|
|
|
|
.input-area::after
|
|
|
|
|
|
position: absolute
|
|
|
|
|
|
content: ''
|
|
|
|
|
|
top: 0
|
|
|
|
|
|
left: 0
|
|
|
|
|
|
width: 100%
|
|
|
|
|
|
z-index: 1
|
|
|
|
|
|
box-shadow: 0rpx -4rpx 10rpx 0rpx rgba(11,44,112,0.06);
|
|
|
|
|
|
.areatext{
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
}
|
|
|
|
|
|
.input {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
border-radius: 5rpx;
|
|
|
|
|
|
min-height: 63rpx;
|
|
|
|
|
|
line-height: 63rpx;
|
|
|
|
|
|
padding: 4rpx 24rpx;
|
|
|
|
|
|
position: relative
|
|
|
|
|
|
background: #F5F5F5;
|
|
|
|
|
|
border-radius: 60rpx 60rpx 60rpx 60rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
.input_vio
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
border-radius: 5rpx;
|
|
|
|
|
|
min-height: 63rpx;
|
|
|
|
|
|
line-height: 63rpx;
|
|
|
|
|
|
padding: 4rpx 24rpx;
|
|
|
|
|
|
// position: relative
|
|
|
|
|
|
border-radius: 60rpx 60rpx 60rpx 60rpx;
|
|
|
|
|
|
font-size: 28rpx
|
|
|
|
|
|
color: #333333
|
|
|
|
|
|
background: #F5F5F5;
|
|
|
|
|
|
text-align: center
|
|
|
|
|
|
font-size: 28rpx
|
|
|
|
|
|
font-weight: 500
|
2025-04-10 10:59:25 +08:00
|
|
|
|
user-select:none;
|
2025-03-28 15:19:42 +08:00
|
|
|
|
-webkit-touch-callout:none;
|
|
|
|
|
|
-webkit-user-select:none;
|
|
|
|
|
|
-khtml-user-select:none;
|
|
|
|
|
|
-moz-user-select:none;
|
|
|
|
|
|
-ms-user-select:none;
|
2025-04-16 14:24:06 +08:00
|
|
|
|
touch-action: none; /* 禁用默认滚动 */
|
2025-03-28 15:19:42 +08:00
|
|
|
|
.input_vio:active
|
|
|
|
|
|
background: #e8e8e8
|
|
|
|
|
|
.vio_container
|
|
|
|
|
|
background: transparent
|
|
|
|
|
|
padding: 28rpx
|
|
|
|
|
|
text-align: center
|
2025-04-16 14:24:06 +08:00
|
|
|
|
-webkit-touch-callout:none;
|
|
|
|
|
|
-webkit-user-select:none;
|
|
|
|
|
|
-khtml-user-select:none;
|
|
|
|
|
|
-moz-user-select:none;
|
|
|
|
|
|
-ms-user-select:none;
|
|
|
|
|
|
touch-action: none; /* 禁用默认滚动 */
|
2026-01-23 22:01:38 +08:00
|
|
|
|
position: fixed;
|
|
|
|
|
|
left: 0;
|
|
|
|
|
|
right: 0;
|
|
|
|
|
|
bottom: 160rpx; /* 为底部导航栏留出空间 */
|
|
|
|
|
|
z-index: 9999; /* 确保高于其他元素 */
|
2025-03-28 15:19:42 +08:00
|
|
|
|
.record-tip
|
|
|
|
|
|
font-weight: 400;
|
|
|
|
|
|
color: #909090;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
padding-bottom: 16rpx
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.inputplaceholder {
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
font-size: 24rpx;
|
|
|
|
|
|
color: #000000;
|
|
|
|
|
|
line-height: 28rpx;
|
|
|
|
|
|
opacity: 0.4
|
|
|
|
|
|
}
|
|
|
|
|
|
.btn-box
|
|
|
|
|
|
margin-left: 12rpx;
|
|
|
|
|
|
width: 70rpx;
|
|
|
|
|
|
height: 70rpx;
|
|
|
|
|
|
border-radius: 50%
|
|
|
|
|
|
background: #F5F5F5;
|
|
|
|
|
|
display: flex
|
|
|
|
|
|
align-items: center
|
|
|
|
|
|
justify-content: center
|
|
|
|
|
|
.send-btn,
|
|
|
|
|
|
.receive-btn
|
|
|
|
|
|
transition: transform 0.5s ease;
|
|
|
|
|
|
width: 38rpx;
|
|
|
|
|
|
height: 38rpx;
|
2025-04-07 09:10:55 +08:00
|
|
|
|
.btn-box-round
|
|
|
|
|
|
width: 22rpx
|
|
|
|
|
|
height: 22rpx
|
|
|
|
|
|
border-radius: 4rpx
|
|
|
|
|
|
background: #FFFFFF
|
2025-03-28 15:19:42 +08:00
|
|
|
|
.purple
|
|
|
|
|
|
background: linear-gradient( 225deg, #9E74FD 0%, #256BFA 100%);
|
|
|
|
|
|
.add-file-btn{
|
|
|
|
|
|
transform: rotate(45deg)
|
|
|
|
|
|
transition: transform 0.5s ease;
|
|
|
|
|
|
}
|
2025-05-15 14:17:51 +08:00
|
|
|
|
.area-tips{
|
|
|
|
|
|
font-weight: 400;
|
|
|
|
|
|
font-size: 26rpx;
|
|
|
|
|
|
color: #434343;
|
|
|
|
|
|
margin-top: 18rpx
|
|
|
|
|
|
}
|
2026-04-03 19:22:20 +08:00
|
|
|
|
|
|
|
|
|
|
/* 文件上传弹窗 - 向上展开,避免被tabbar遮挡 */
|
|
|
|
|
|
.file-upload-popup {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
bottom: 100%;
|
|
|
|
|
|
left: 0;
|
|
|
|
|
|
right: 0;
|
|
|
|
|
|
background: #FFFFFF;
|
|
|
|
|
|
padding: 0 28rpx 24rpx 28rpx;
|
|
|
|
|
|
box-shadow: 0rpx -4rpx 10rpx 0rpx rgba(11,44,112,0.06);
|
|
|
|
|
|
transform: translateY(20rpx);
|
|
|
|
|
|
opacity: 0;
|
|
|
|
|
|
visibility: hidden;
|
|
|
|
|
|
transition: all 0.3s ease;
|
|
|
|
|
|
z-index: 1002;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.file-upload-popup.show {
|
|
|
|
|
|
transform: translateY(0);
|
|
|
|
|
|
opacity: 1;
|
|
|
|
|
|
visibility: visible;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-03-28 15:19:42 +08:00
|
|
|
|
.area-file
|
|
|
|
|
|
display: grid
|
|
|
|
|
|
width: 100%
|
|
|
|
|
|
grid-template-columns: repeat(3, 1fr)
|
|
|
|
|
|
grid-gap: 20rpx
|
2025-05-15 14:17:51 +08:00
|
|
|
|
padding: 20rpx 0 0 0;
|
2025-03-28 18:19:05 +08:00
|
|
|
|
.file-card
|
|
|
|
|
|
display: flex
|
|
|
|
|
|
flex-direction: column
|
|
|
|
|
|
align-items: center
|
|
|
|
|
|
padding: 24rpx 0
|
|
|
|
|
|
background: #F5F5F5;
|
|
|
|
|
|
border-radius: 20rpx 20rpx 20rpx 20rpx;
|
|
|
|
|
|
text
|
|
|
|
|
|
font-size: 24rpx
|
|
|
|
|
|
font-weight: 500
|
2025-04-07 09:10:55 +08:00
|
|
|
|
color: rgba(0,0,0,.5)
|
2025-03-28 18:19:05 +08:00
|
|
|
|
padding-top: 8rpx
|
|
|
|
|
|
.card-img
|
|
|
|
|
|
height: 56rpx
|
|
|
|
|
|
width: 56rpx
|
|
|
|
|
|
.file-card:active
|
|
|
|
|
|
background: #e8e8e8
|
|
|
|
|
|
|
2025-03-28 15:19:42 +08:00
|
|
|
|
.area-uploadfiles
|
|
|
|
|
|
position: absolute
|
2026-04-09 21:14:25 +08:00
|
|
|
|
bottom: 100%
|
2025-04-07 09:10:55 +08:00
|
|
|
|
width: calc(100% - 30rpx)
|
2025-03-28 15:19:42 +08:00
|
|
|
|
background: #FFFFFF
|
|
|
|
|
|
left: 0
|
2025-04-07 09:10:55 +08:00
|
|
|
|
padding: 10rpx 0 10rpx 30rpx
|
2025-03-28 15:19:42 +08:00
|
|
|
|
box-shadow: 0rpx -4rpx 10rpx 0rpx rgba(11,44,112,0.06);
|
2026-04-09 21:14:25 +08:00
|
|
|
|
z-index: 1003
|
2025-03-28 15:19:42 +08:00
|
|
|
|
.uploadfiles-scroll
|
|
|
|
|
|
height: 100%
|
|
|
|
|
|
.uploadfiles-list
|
|
|
|
|
|
height: 100%
|
|
|
|
|
|
display: flex
|
2025-04-07 09:10:55 +08:00
|
|
|
|
margin-right: 28rpx
|
2025-03-28 15:19:42 +08:00
|
|
|
|
flex-wrap: nowrap
|
2025-03-28 18:19:05 +08:00
|
|
|
|
.file-doc
|
2025-03-28 15:19:42 +08:00
|
|
|
|
display: flex
|
2025-04-07 09:10:55 +08:00
|
|
|
|
align-items: center
|
|
|
|
|
|
justify-content: flex-start
|
2025-03-28 18:19:05 +08:00
|
|
|
|
padding: 16rpx 20rpx 18rpx 20rpx
|
|
|
|
|
|
height: calc(100% - 40rpx)
|
2025-04-07 09:10:55 +08:00
|
|
|
|
.doc-icon
|
|
|
|
|
|
width: 60rpx
|
|
|
|
|
|
height: 76rpx
|
|
|
|
|
|
margin-right: 20rpx
|
|
|
|
|
|
.doc-con
|
|
|
|
|
|
flex: 1
|
|
|
|
|
|
width: 0
|
2025-03-28 18:19:05 +08:00
|
|
|
|
.file-uploadsend
|
2025-04-07 09:10:55 +08:00
|
|
|
|
margin: 10rpx 18rpx 0 0;
|
2025-03-28 15:19:42 +08:00
|
|
|
|
height: 100%
|
|
|
|
|
|
font-size: 24rpx
|
|
|
|
|
|
position: relative
|
2026-04-09 21:14:25 +08:00
|
|
|
|
min-width: 200rpx;
|
|
|
|
|
|
max-width: 200rpx;
|
|
|
|
|
|
height: 200rpx;
|
|
|
|
|
|
border-radius: 12rpx;
|
2025-03-28 15:19:42 +08:00
|
|
|
|
border: 2rpx solid #E2E2E2;
|
2025-04-07 09:10:55 +08:00
|
|
|
|
overflow: hidden
|
2026-04-09 21:14:25 +08:00
|
|
|
|
display: flex
|
|
|
|
|
|
flex-direction: column
|
|
|
|
|
|
&.file-border
|
|
|
|
|
|
border: 2rpx solid #4A90E2;
|
|
|
|
|
|
background: #F5F9FF;
|
2025-03-28 15:19:42 +08:00
|
|
|
|
.file-del
|
|
|
|
|
|
position: absolute
|
2025-04-07 09:10:55 +08:00
|
|
|
|
right: 25rpx
|
|
|
|
|
|
top: 18rpx
|
2025-03-28 15:19:42 +08:00
|
|
|
|
z-index: 9
|
|
|
|
|
|
border-radius: 50%
|
|
|
|
|
|
display: flex
|
|
|
|
|
|
align-items: center
|
|
|
|
|
|
justify-content: center
|
|
|
|
|
|
transform: translate(50%, -10rpx)
|
|
|
|
|
|
height: 40rpx
|
2025-04-07 09:10:55 +08:00
|
|
|
|
width: 37rpx;
|
|
|
|
|
|
height: 37rpx;
|
|
|
|
|
|
background: #4B4B4B;
|
|
|
|
|
|
border: 2rpx solid #FFFFFF;
|
2025-03-28 15:19:42 +08:00
|
|
|
|
.file-del:active
|
|
|
|
|
|
background: #e8e8e8
|
|
|
|
|
|
.filename-text
|
|
|
|
|
|
overflow: hidden
|
|
|
|
|
|
text-overflow: ellipsis
|
|
|
|
|
|
white-space: nowrap
|
|
|
|
|
|
color: #333333
|
2025-03-28 18:19:05 +08:00
|
|
|
|
font-size: 24rpx
|
2025-03-28 15:19:42 +08:00
|
|
|
|
font-weight: 500
|
|
|
|
|
|
max-width: 100%
|
2025-03-28 18:19:05 +08:00
|
|
|
|
.filename-size
|
|
|
|
|
|
overflow: hidden
|
|
|
|
|
|
text-overflow: ellipsis
|
|
|
|
|
|
white-space: nowrap
|
|
|
|
|
|
color: #7B7B7B;
|
|
|
|
|
|
max-width: 100%
|
2026-04-09 21:14:25 +08:00
|
|
|
|
.file-thumbnail
|
|
|
|
|
|
flex: 1
|
|
|
|
|
|
display: flex
|
|
|
|
|
|
align-items: center
|
|
|
|
|
|
justify-content: center
|
|
|
|
|
|
overflow: hidden
|
|
|
|
|
|
border-radius: 10rpx 10rpx 0 0
|
|
|
|
|
|
background: #F8F9FA
|
2025-03-28 18:19:05 +08:00
|
|
|
|
.file-iconImg
|
|
|
|
|
|
height: 100%
|
|
|
|
|
|
width: 100%
|
2026-04-09 21:14:25 +08:00
|
|
|
|
object-fit: cover
|
|
|
|
|
|
.file-info
|
|
|
|
|
|
padding: 12rpx 16rpx
|
|
|
|
|
|
background: white
|
|
|
|
|
|
border-top: 1rpx solid #F0F0F0
|
|
|
|
|
|
.file-type-tag
|
|
|
|
|
|
font-size: 20rpx
|
|
|
|
|
|
color: #4A90E2
|
|
|
|
|
|
background: #F0F7FF
|
|
|
|
|
|
padding: 4rpx 8rpx
|
|
|
|
|
|
border-radius: 4rpx
|
|
|
|
|
|
.image-loading, .image-error
|
|
|
|
|
|
position: absolute
|
|
|
|
|
|
top: 0
|
|
|
|
|
|
left: 0
|
|
|
|
|
|
right: 0
|
|
|
|
|
|
bottom: 0
|
|
|
|
|
|
display: flex
|
|
|
|
|
|
flex-direction: column
|
|
|
|
|
|
align-items: center
|
|
|
|
|
|
justify-content: center
|
|
|
|
|
|
background: rgba(255, 255, 255, 0.9)
|
|
|
|
|
|
.loading-text, .error-text
|
|
|
|
|
|
font-size: 24rpx
|
|
|
|
|
|
margin-top: 10rpx
|
|
|
|
|
|
.loading-text
|
|
|
|
|
|
color: #999
|
|
|
|
|
|
.error-text
|
|
|
|
|
|
color: #ff4444
|
|
|
|
|
|
.retry-text
|
|
|
|
|
|
font-size: 22rpx
|
|
|
|
|
|
color: #4A90E2
|
|
|
|
|
|
margin-top: 5rpx
|
|
|
|
|
|
text-decoration: underline
|
|
|
|
|
|
.image-count-badge
|
|
|
|
|
|
position: absolute
|
|
|
|
|
|
top: 10rpx
|
|
|
|
|
|
right: 10rpx
|
|
|
|
|
|
background: rgba(0, 0, 0, 0.6)
|
|
|
|
|
|
color: white
|
|
|
|
|
|
font-size: 20rpx
|
|
|
|
|
|
padding: 4rpx 8rpx
|
|
|
|
|
|
border-radius: 12rpx
|
|
|
|
|
|
z-index: 2
|
2025-04-07 09:10:55 +08:00
|
|
|
|
.filerow
|
|
|
|
|
|
display: flex
|
|
|
|
|
|
align-items: center
|
|
|
|
|
|
margin-top: 7rpx
|
|
|
|
|
|
.row-x
|
|
|
|
|
|
margin: 0 18rpx
|
|
|
|
|
|
height: 20rpx
|
|
|
|
|
|
width: 2rpx
|
|
|
|
|
|
background: rgba(226, 226, 226, .9)
|
2025-03-28 18:19:05 +08:00
|
|
|
|
.file-border
|
2025-04-07 09:10:55 +08:00
|
|
|
|
width: 160rpx !important;
|
2025-03-28 15:19:42 +08:00
|
|
|
|
|
2026-01-22 14:05:22 +08:00
|
|
|
|
/* 更美观的loading动画 - 兼容H5和小程序 */
|
|
|
|
|
|
@keyframes ai-loading-dots {
|
|
|
|
|
|
0%, 20%, 80%, 100% {
|
|
|
|
|
|
transform: scale(1);
|
|
|
|
|
|
opacity: 0.6;
|
2025-03-28 15:19:42 +08:00
|
|
|
|
}
|
2026-01-22 14:05:22 +08:00
|
|
|
|
40% {
|
|
|
|
|
|
transform: scale(1.2);
|
|
|
|
|
|
opacity: 1;
|
2025-03-28 15:19:42 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-01-22 14:05:22 +08:00
|
|
|
|
|
|
|
|
|
|
/* 重置默认样式 */
|
|
|
|
|
|
.ai-loading {
|
2025-03-28 15:19:42 +08:00
|
|
|
|
display: inline-flex;
|
2026-01-22 14:05:22 +08:00
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
gap: 8rpx;
|
|
|
|
|
|
width: auto;
|
|
|
|
|
|
height: auto;
|
|
|
|
|
|
background: transparent;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
border-radius: 0;
|
|
|
|
|
|
padding: 0;
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 三个点的样式 - 使用标准CSS语法,不使用嵌套 */
|
2026-01-23 13:10:40 +08:00
|
|
|
|
.ai-loading view {
|
2026-01-22 14:05:22 +08:00
|
|
|
|
display: inline-block;
|
|
|
|
|
|
width: 12rpx;
|
|
|
|
|
|
height: 12rpx;
|
2025-03-28 15:19:42 +08:00
|
|
|
|
border-radius: 50%;
|
2026-01-22 14:05:22 +08:00
|
|
|
|
background-color: #256BFA;
|
|
|
|
|
|
animation: ai-loading-dots 1.4s ease-in-out infinite both;
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
padding: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 为每个点设置不同的动画延迟 */
|
2026-01-23 13:10:40 +08:00
|
|
|
|
.ai-loading view:nth-child(1) {
|
2026-01-22 14:05:22 +08:00
|
|
|
|
animation-delay: -0.32s;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-23 13:10:40 +08:00
|
|
|
|
.ai-loading view:nth-child(2) {
|
2026-01-22 14:05:22 +08:00
|
|
|
|
animation-delay: -0.16s;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-23 13:10:40 +08:00
|
|
|
|
.ai-loading view:nth-child(3) {
|
2026-01-22 14:05:22 +08:00
|
|
|
|
animation-delay: 0s;
|
|
|
|
|
|
}
|
2026-02-03 14:50:56 +08:00
|
|
|
|
|
|
|
|
|
|
/* Tab切换样式 */
|
|
|
|
|
|
.tab-container {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
background: #FFFFFF;
|
|
|
|
|
|
border-bottom: 2rpx solid #F4F4F4;
|
|
|
|
|
|
padding: 0 44rpx;
|
|
|
|
|
|
height: 88rpx;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
z-index: 10;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tab-item {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
font-size: 28rpx;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
color: #666666;
|
|
|
|
|
|
line-height: 88rpx;
|
|
|
|
|
|
height: 88rpx;
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
transition: all 0.3s ease;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tab-item.active {
|
|
|
|
|
|
color: #256BFA;
|
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tab-item.active::after {
|
|
|
|
|
|
content: '';
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
bottom: 0;
|
|
|
|
|
|
left: 50%;
|
|
|
|
|
|
transform: translateX(-50%);
|
|
|
|
|
|
width: 60rpx;
|
|
|
|
|
|
height: 4rpx;
|
|
|
|
|
|
background: #256BFA;
|
|
|
|
|
|
border-radius: 2rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tab-item:active {
|
|
|
|
|
|
background-color: #F5F5F5;
|
|
|
|
|
|
}
|
2026-03-16 14:28:38 +08:00
|
|
|
|
|
|
|
|
|
|
/* 选择模式样式 */
|
|
|
|
|
|
.select-mode-header {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
padding: 0 44rpx;
|
|
|
|
|
|
height: 88rpx;
|
|
|
|
|
|
background: #FFFFFF;
|
|
|
|
|
|
border-bottom: 2rpx solid #F4F4F4;
|
|
|
|
|
|
z-index: 10;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.select-header-left {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 24rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.select-count {
|
|
|
|
|
|
font-size: 28rpx;
|
|
|
|
|
|
color: #333333;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.select-all {
|
|
|
|
|
|
font-size: 28rpx;
|
|
|
|
|
|
color: #256BFA;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.select-header-right {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 32rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.cancel-btn {
|
|
|
|
|
|
font-size: 28rpx;
|
|
|
|
|
|
color: #666666;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.delete-btn {
|
|
|
|
|
|
font-size: 28rpx;
|
|
|
|
|
|
color: #FF4444;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.delete-btn:disabled {
|
|
|
|
|
|
color: #CCCCCC;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 消息选择框样式 */
|
|
|
|
|
|
.message-checkbox {
|
|
|
|
|
|
margin-right: 12rpx;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
|
padding-top: 8rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.checkbox {
|
|
|
|
|
|
width: 40rpx;
|
|
|
|
|
|
height: 40rpx;
|
|
|
|
|
|
border: 2rpx solid #CCCCCC;
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.checkbox.checked {
|
|
|
|
|
|
background-color: #256BFA;
|
|
|
|
|
|
border-color: #256BFA;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 调整消息项布局,为选择框留出空间 */
|
|
|
|
|
|
.chat-item {
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.chat-item .message {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
}
|
2025-03-28 15:19:42 +08:00
|
|
|
|
</style>
|