更新 2026-04-18 18:32
This commit is contained in:
parent
582092f467
commit
e56e54561c
@ -92,7 +92,7 @@ export const cancelAppointment = (id) => put(`/appointment/status?id=${id}&statu
|
|||||||
// 提交报告
|
// 提交报告
|
||||||
export const createReport = (data) => post('/report/create', data)
|
export const createReport = (data) => post('/report/create', data)
|
||||||
|
|
||||||
/** 触发服务回顾短片生成(异步;reportId + 操作者)durationSec: 15 | 30 */
|
/** 触发服务回顾短片生成(异步)。composeMode: preset | interleave,时长由后端按素材计算 */
|
||||||
export const postReportHighlightStart = (data) => post('/report/highlight/start', data)
|
export const postReportHighlightStart = (data) => post('/report/highlight/start', data)
|
||||||
|
|
||||||
// 获取报告(通过token)
|
// 获取报告(通过token)
|
||||||
|
|||||||
@ -47,7 +47,7 @@
|
|||||||
<view v-if="errorMsg" class="rc-error">{{ errorMsg }}</view>
|
<view v-if="errorMsg" class="rc-error">{{ errorMsg }}</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 成功状态 -->
|
<!-- 成功状态:留资 + 微信绑定闭环(S3→S4) -->
|
||||||
<view v-else class="rc-success">
|
<view v-else class="rc-success">
|
||||||
<view class="rc-success-icon">✓</view>
|
<view class="rc-success-icon">✓</view>
|
||||||
<view class="rc-success-title">已为您设置提醒</view>
|
<view class="rc-success-title">已为您设置提醒</view>
|
||||||
@ -57,6 +57,25 @@
|
|||||||
<view class="rc-success-desc">
|
<view class="rc-success-desc">
|
||||||
到点我们会通过微信/短信告诉你,无需惦记。
|
到点我们会通过微信/短信告诉你,无需惦记。
|
||||||
</view>
|
</view>
|
||||||
|
<!-- #ifdef MP-WEIXIN -->
|
||||||
|
<view v-if="wechatBound" class="rc-bind rc-bind--ok">
|
||||||
|
<text class="rc-bind-title">微信已关联</text>
|
||||||
|
<text class="rc-bind-text">已用当前微信绑定该手机号。使用同一手机号登录本小程序,可在「我的」查看预约与宠物档案。</text>
|
||||||
|
</view>
|
||||||
|
<view v-else class="rc-bind rc-bind--hint">
|
||||||
|
<text class="rc-bind-title">接收微信提醒</text>
|
||||||
|
<text class="rc-bind-text">本次未拿到微信授权,提醒可能仅通过短信送达。可重新进入本报告页再次提交,或在本小程序用该手机号登录后使用。</text>
|
||||||
|
</view>
|
||||||
|
<!-- #endif -->
|
||||||
|
<!-- #ifdef H5 -->
|
||||||
|
<view class="rc-bind rc-bind--hint">
|
||||||
|
<text class="rc-bind-title">微信内打开体验更佳</text>
|
||||||
|
<text class="rc-bind-text">在微信中打开本链接,留资时可关联微信,便于接收服务提醒与后续报告。</text>
|
||||||
|
</view>
|
||||||
|
<!-- #endif -->
|
||||||
|
|
||||||
|
<button class="rc-login-btn" @click="goLoginWithPhone">用该手机号登录</button>
|
||||||
|
<view class="rc-login-hint">登录后可在「我的」查看宠物档案与预约</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
@ -77,6 +96,7 @@ const consent = ref(false)
|
|||||||
const submitting = ref(false)
|
const submitting = ref(false)
|
||||||
const submitted = ref(false)
|
const submitted = ref(false)
|
||||||
const savedDate = ref('')
|
const savedDate = ref('')
|
||||||
|
const wechatBound = ref(false)
|
||||||
const errorMsg = ref('')
|
const errorMsg = ref('')
|
||||||
|
|
||||||
const titleText = computed(() => {
|
const titleText = computed(() => {
|
||||||
@ -139,6 +159,17 @@ const fetchMiniLoginCode = () =>
|
|||||||
// #endif
|
// #endif
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function goLoginWithPhone() {
|
||||||
|
const p = phone.value
|
||||||
|
if (!/^1[3-9]\d{9}$/.test(p)) {
|
||||||
|
uni.showToast({ title: '请先填写有效手机号', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uni.navigateTo({
|
||||||
|
url: '/pages/login/Login?phone=' + encodeURIComponent(p)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
async function onSubmit() {
|
async function onSubmit() {
|
||||||
if (!canSubmit.value) return
|
if (!canSubmit.value) return
|
||||||
errorMsg.value = ''
|
errorMsg.value = ''
|
||||||
@ -148,6 +179,7 @@ async function onSubmit() {
|
|||||||
const res = await submitReportReminder(props.token, phone.value, loginCode)
|
const res = await submitReportReminder(props.token, phone.value, loginCode)
|
||||||
if (res && res.code === 200 && res.data && res.data.ok) {
|
if (res && res.code === 200 && res.data && res.data.ok) {
|
||||||
savedDate.value = res.data.remindDate || suggestion.value.remindDate
|
savedDate.value = res.data.remindDate || suggestion.value.remindDate
|
||||||
|
wechatBound.value = !!res.data.wechatBound
|
||||||
submitted.value = true
|
submitted.value = true
|
||||||
} else {
|
} else {
|
||||||
errorMsg.value = (res && res.message) || '提交失败,请稍后再试'
|
errorMsg.value = (res && res.message) || '提交失败,请稍后再试'
|
||||||
@ -317,4 +349,53 @@ onMounted(loadSuggestion)
|
|||||||
color: #4b5563;
|
color: #4b5563;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
.rc-bind {
|
||||||
|
margin-top: 14px;
|
||||||
|
padding: 12px 12px;
|
||||||
|
border-radius: 12px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
.rc-bind--ok {
|
||||||
|
background: #ecfdf5;
|
||||||
|
border: 1px solid #86efac;
|
||||||
|
}
|
||||||
|
.rc-bind--hint {
|
||||||
|
background: #f8fafc;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
}
|
||||||
|
.rc-bind-title {
|
||||||
|
display: block;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #166534;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
.rc-bind--hint .rc-bind-title {
|
||||||
|
color: #334155;
|
||||||
|
}
|
||||||
|
.rc-bind-text {
|
||||||
|
display: block;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #475569;
|
||||||
|
line-height: 1.55;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rc-login-btn {
|
||||||
|
margin-top: 16px;
|
||||||
|
width: 100%;
|
||||||
|
height: 42px;
|
||||||
|
line-height: 42px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid #16a34a;
|
||||||
|
background: #ffffff;
|
||||||
|
color: #15803d;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.rc-login-hint {
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #94a3b8;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -154,7 +154,14 @@ const safeLine = (s, fallback = '—') => {
|
|||||||
|
|
||||||
const isHttpOk = (res) => res && (res.code === 200 || res.code === '200')
|
const isHttpOk = (res) => res && (res.code === 200 || res.code === '200')
|
||||||
|
|
||||||
onLoad(() => { view.value = 'login' })
|
onLoad((options) => {
|
||||||
|
view.value = 'login'
|
||||||
|
const raw = options && (options.phone ?? options.prefillPhone)
|
||||||
|
const p = raw == null ? '' : String(raw).trim()
|
||||||
|
if (/^1[3-9]\d{9}$/.test(p)) {
|
||||||
|
loginForm.phone = p
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const backToLoginForm = () => { view.value = 'login' }
|
const backToLoginForm = () => { view.value = 'login' }
|
||||||
|
|
||||||
|
|||||||
@ -61,6 +61,11 @@
|
|||||||
<text class="menu-text">门店日程</text>
|
<text class="menu-text">门店日程</text>
|
||||||
<text class="menu-arrow">›</text>
|
<text class="menu-arrow">›</text>
|
||||||
</view>
|
</view>
|
||||||
|
<view class="menu-item" @click="goLeadsPage">
|
||||||
|
<view class="menu-icon-wrap"><AppIcon name="orders" :size="16" color="#666660" /></view>
|
||||||
|
<text class="menu-text">回访池</text>
|
||||||
|
<text class="menu-arrow">›</text>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
|||||||
@ -149,6 +149,13 @@
|
|||||||
<text class="remark-text">{{ reportData.remark }}</text>
|
<text class="remark-text">{{ reportData.remark }}</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
<!-- 留资 + 微信绑定闭环(S3→S4):下次服务提醒,仅宠主可见 -->
|
||||||
|
<ReminderCard
|
||||||
|
v-if="reportData && !isStaff && reminderToken"
|
||||||
|
:token="reminderToken"
|
||||||
|
:pet-name="reportData.petName || ''"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- 服务回顾短片:素材智能剪辑(后端可扩展接入生成式 AI 视频) -->
|
<!-- 服务回顾短片:素材智能剪辑(后端可扩展接入生成式 AI 视频) -->
|
||||||
<view v-if="hasHighlightSource" class="highlight-card">
|
<view v-if="hasHighlightSource" class="highlight-card">
|
||||||
<view class="highlight-head">
|
<view class="highlight-head">
|
||||||
@ -156,13 +163,21 @@
|
|||||||
<text class="highlight-badge">智能剪辑</text>
|
<text class="highlight-badge">智能剪辑</text>
|
||||||
</view>
|
</view>
|
||||||
<text class="highlight-desc">
|
<text class="highlight-desc">
|
||||||
先按「服务前 → 过程 → 服务后」整理素材,再将照片与小视频穿插编排(类相册成片),图片带轻微推拉镜头,竖屏 9:16,可选 BGM。总时长 15 秒或 30 秒;服务器需 ffmpeg(建议安装 ffprobe)。配乐请将商用授权文件配置在后端 HIGHLIGHT_BGM_PATH。
|
将服务前、过程、服务后素材自动剪成竖屏 9:16 短片。成片时长随素材量变化,并受门店服务端配置的总时长上限约束,不承诺固定秒数。请选择「预设顺序」或「穿插成片」后,再点下方「生成短片」。
|
||||||
|
</text>
|
||||||
|
<text v-if="canTriggerHighlight" class="highlight-desc-ops">
|
||||||
|
预设顺序含对比封面与收尾镜头;穿插成片在阶段间均匀穿插照片与小视频。若生成失败并提示服务器环境,一般由未安装 ffmpeg/ffprobe 或未加入 PATH,需运维安装后重启应用;可选配乐由门店在服务端配置。
|
||||||
</text>
|
</text>
|
||||||
<view v-if="highlightStatus === 'processing'" class="highlight-status highlight-status--busy">
|
<view v-if="highlightStatus === 'processing'" class="highlight-status highlight-status--busy">
|
||||||
<text>短片生成中,请稍候…</text>
|
<text>短片生成中,请稍候…</text>
|
||||||
|
<text class="highlight-processing-hint">可离开本页,稍后重新进入即可查看结果。</text>
|
||||||
</view>
|
</view>
|
||||||
<view v-else-if="highlightStatus === 'failed'" class="highlight-status highlight-status--err">
|
<view v-else-if="highlightStatus === 'failed'" class="highlight-status highlight-status--err">
|
||||||
<text>{{ reportData.highlightVideoError || '生成失败' }}</text>
|
<text v-if="reportData.highlightFailReasonLabel" class="highlight-err-type">{{
|
||||||
|
reportData.highlightFailReasonLabel
|
||||||
|
}}</text>
|
||||||
|
<text class="highlight-err-msg">{{ highlightErrorUserText }}</text>
|
||||||
|
<text v-if="canTriggerHighlight" class="highlight-fail-hint">可调整编排方式后重试;若仍失败请联系门店或管理员。</text>
|
||||||
</view>
|
</view>
|
||||||
<video
|
<video
|
||||||
v-if="highlightReady"
|
v-if="highlightReady"
|
||||||
@ -172,20 +187,60 @@
|
|||||||
:show-center-play-btn="true"
|
:show-center-play-btn="true"
|
||||||
objectFit="contain"
|
objectFit="contain"
|
||||||
/>
|
/>
|
||||||
<view v-if="canTriggerHighlight" class="highlight-actions">
|
<text v-if="highlightReady && reportData.highlightDurationSec" class="highlight-meta">
|
||||||
|
约 {{ reportData.highlightDurationSec }} 秒
|
||||||
|
<text v-if="highlightComposeLabel"> · {{ highlightComposeLabel }}</text>
|
||||||
|
</text>
|
||||||
|
<!-- #ifdef MP-WEIXIN -->
|
||||||
|
<view v-if="highlightReady" class="highlight-mp-bar">
|
||||||
<button
|
<button
|
||||||
class="highlight-btn"
|
class="highlight-btn highlight-btn--album highlight-btn--block"
|
||||||
:disabled="highlightStatus === 'processing'"
|
:disabled="savingHighlightVideo"
|
||||||
@click="startHighlight(15)"
|
@click="saveHighlightVideoToAlbum"
|
||||||
>
|
>
|
||||||
生成 15 秒
|
{{ savingHighlightVideo ? '保存中…' : '保存视频' }}
|
||||||
</button>
|
</button>
|
||||||
|
<text class="highlight-mp-hint">
|
||||||
|
保存后可在系统相册中转发到微信好友或其它应用;分享本报告小程序卡片请用页面底部「转发给宠主」。
|
||||||
|
</text>
|
||||||
|
<text class="highlight-mp-tip">也可点右上角「···」将本页转发给好友。</text>
|
||||||
|
</view>
|
||||||
|
<!-- #endif -->
|
||||||
|
<!-- #ifdef H5 -->
|
||||||
|
<view v-if="highlightReady" class="highlight-h5-bar">
|
||||||
<button
|
<button
|
||||||
class="highlight-btn highlight-btn--secondary"
|
class="highlight-btn highlight-btn--album highlight-btn--block"
|
||||||
:disabled="highlightStatus === 'processing'"
|
:disabled="savingHighlightVideo"
|
||||||
@click="startHighlight(30)"
|
@click="downloadHighlightVideoH5"
|
||||||
>
|
>
|
||||||
生成 30 秒
|
{{ savingHighlightVideo ? '处理中…' : '下载短片' }}
|
||||||
|
</button>
|
||||||
|
<text class="highlight-mp-hint">
|
||||||
|
将回顾短片保存到本机下载目录;若浏览器拦截,将尝试复制链接。也可使用浏览器菜单分享本页。
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
<!-- #endif -->
|
||||||
|
<view v-if="canTriggerHighlight" class="highlight-actions">
|
||||||
|
<view class="highlight-mode">
|
||||||
|
<view
|
||||||
|
:class="['highlight-mode-item', highlightComposeMode === 'preset' && 'is-on']"
|
||||||
|
@click="highlightComposeMode = 'preset'"
|
||||||
|
>
|
||||||
|
预设顺序
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
:class="['highlight-mode-item', highlightComposeMode === 'interleave' && 'is-on']"
|
||||||
|
@click="highlightComposeMode = 'interleave'"
|
||||||
|
>
|
||||||
|
穿插成片
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<button
|
||||||
|
class="highlight-btn highlight-btn--block"
|
||||||
|
:disabled="highlightStatus === 'processing'"
|
||||||
|
@click="startHighlight"
|
||||||
|
>
|
||||||
|
生成短片
|
||||||
</button>
|
</button>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@ -251,6 +306,7 @@ import { messageFromApi, NETWORK_ERROR_MSG } from '../../utils/apiResult.js'
|
|||||||
import BeforeAfterSlider from '../../components/report/BeforeAfterSlider.vue'
|
import BeforeAfterSlider from '../../components/report/BeforeAfterSlider.vue'
|
||||||
import DuringMediaGrid from '../../components/report/DuringMediaGrid.vue'
|
import DuringMediaGrid from '../../components/report/DuringMediaGrid.vue'
|
||||||
import TestimonialModal from '../../components/report/TestimonialModal.vue'
|
import TestimonialModal from '../../components/report/TestimonialModal.vue'
|
||||||
|
import ReminderCard from '../../components/report/ReminderCard.vue'
|
||||||
import { getUserSession, isLoggedIn } from '../../utils/session.js'
|
import { getUserSession, isLoggedIn } from '../../utils/session.js'
|
||||||
|
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
@ -260,6 +316,8 @@ const reportData = ref(null)
|
|||||||
const posterCanvas = ref(null)
|
const posterCanvas = ref(null)
|
||||||
const posterModalOpen = ref(false)
|
const posterModalOpen = ref(false)
|
||||||
const routeToken = ref('')
|
const routeToken = ref('')
|
||||||
|
/** 小程序:保存成片视频到相册 */
|
||||||
|
const savingHighlightVideo = ref(false)
|
||||||
|
|
||||||
const isStaff = computed(() => isLoggedIn())
|
const isStaff = computed(() => isLoggedIn())
|
||||||
|
|
||||||
@ -278,6 +336,30 @@ const highlightReady = computed(
|
|||||||
() => highlightStatus.value === 'done' && !!reportData.value?.highlightVideoUrl
|
() => highlightStatus.value === 'done' && !!reportData.value?.highlightVideoUrl
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const highlightComposeMode = ref('preset')
|
||||||
|
|
||||||
|
const highlightComposeLabel = computed(() => {
|
||||||
|
const m = reportData.value?.highlightComposeMode
|
||||||
|
if (m === 'interleave') return '穿插成片'
|
||||||
|
if (m === 'preset') return '预设顺序'
|
||||||
|
return ''
|
||||||
|
})
|
||||||
|
|
||||||
|
/** 宠主可读说明:新数据由后端归类中文;旧数据若含技术英文则降级为通用句 */
|
||||||
|
const highlightErrorUserText = computed(() => {
|
||||||
|
const raw = reportData.value?.highlightVideoError
|
||||||
|
const hasCode = !!reportData.value?.highlightFailReason
|
||||||
|
if (!raw || !String(raw).trim()) {
|
||||||
|
return '生成失败,请稍后重试'
|
||||||
|
}
|
||||||
|
const s = String(raw).trim()
|
||||||
|
if (hasCode) return s
|
||||||
|
if (/ffmpeg|exit\s*code|IllegalState|Exception|stack|0x|at\s+com\./i.test(s)) {
|
||||||
|
return '短片生成遇到问题,请稍后重试或联系门店。'
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
})
|
||||||
|
|
||||||
/** 仅店长/员工在登录态下可发起合成(宠主仅查看已生成短片) */
|
/** 仅店长/员工在登录态下可发起合成(宠主仅查看已生成短片) */
|
||||||
const canTriggerHighlight = computed(() => {
|
const canTriggerHighlight = computed(() => {
|
||||||
const u = getUserSession()
|
const u = getUserSession()
|
||||||
@ -302,6 +384,9 @@ const getRouteToken = () => {
|
|||||||
return token || ''
|
return token || ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 报告 token:接口字段优先,兼容 H5 地址栏 */
|
||||||
|
const reminderToken = computed(() => reportData.value?.reportToken || getRouteToken())
|
||||||
|
|
||||||
const maybeStartHighlightPoll = () => {
|
const maybeStartHighlightPoll = () => {
|
||||||
stopHighlightPoll()
|
stopHighlightPoll()
|
||||||
if (reportData.value?.highlightVideoStatus !== 'processing') return
|
if (reportData.value?.highlightVideoStatus !== 'processing') return
|
||||||
@ -314,6 +399,10 @@ const maybeStartHighlightPoll = () => {
|
|||||||
const res = await getReportByToken(t)
|
const res = await getReportByToken(t)
|
||||||
if (res.code === 200 && res.data) {
|
if (res.code === 200 && res.data) {
|
||||||
reportData.value = res.data
|
reportData.value = res.data
|
||||||
|
const m = res.data.highlightComposeMode
|
||||||
|
if (m === 'preset' || m === 'interleave') {
|
||||||
|
highlightComposeMode.value = m
|
||||||
|
}
|
||||||
if (res.data.highlightVideoStatus !== 'processing') {
|
if (res.data.highlightVideoStatus !== 'processing') {
|
||||||
stopHighlightPoll()
|
stopHighlightPoll()
|
||||||
if (res.data.highlightVideoStatus === 'done') {
|
if (res.data.highlightVideoStatus === 'done') {
|
||||||
@ -324,7 +413,7 @@ const maybeStartHighlightPoll = () => {
|
|||||||
}, 2500)
|
}, 2500)
|
||||||
}
|
}
|
||||||
|
|
||||||
const startHighlight = async (durationSec) => {
|
const startHighlight = async () => {
|
||||||
const u = getUserSession()
|
const u = getUserSession()
|
||||||
if (!reportData.value?.id || !u?.id) {
|
if (!reportData.value?.id || !u?.id) {
|
||||||
uni.showToast({ title: '请先登录门店账号', icon: 'none' })
|
uni.showToast({ title: '请先登录门店账号', icon: 'none' })
|
||||||
@ -333,7 +422,7 @@ const startHighlight = async (durationSec) => {
|
|||||||
uni.showLoading({ title: '提交中' })
|
uni.showLoading({ title: '提交中' })
|
||||||
const res = await postReportHighlightStart({
|
const res = await postReportHighlightStart({
|
||||||
reportId: reportData.value.id,
|
reportId: reportData.value.id,
|
||||||
durationSec,
|
composeMode: highlightComposeMode.value,
|
||||||
operatorUserId: u.id,
|
operatorUserId: u.id,
|
||||||
role: u.role || 'staff'
|
role: u.role || 'staff'
|
||||||
})
|
})
|
||||||
@ -345,7 +434,9 @@ const startHighlight = async (durationSec) => {
|
|||||||
} else if (res.code === 503) {
|
} else if (res.code === 503) {
|
||||||
uni.showModal({
|
uni.showModal({
|
||||||
title: '暂无法生成',
|
title: '暂无法生成',
|
||||||
content: res.message || '服务器未配置 ffmpeg',
|
content:
|
||||||
|
(res.message || '服务器未安装或未配置 ffmpeg') +
|
||||||
|
'\n\n请在主机安装 ffmpeg(及 ffprobe),保证运行后端的用户能执行「ffmpeg -version」;Docker 需在镜像内安装或挂载二进制,必要时在配置中指定 ffmpeg 绝对路径并重启服务。',
|
||||||
showCancel: false
|
showCancel: false
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
@ -353,6 +444,119 @@ const startHighlight = async (durationSec) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const saveHighlightVideoToAlbum = async () => {
|
||||||
|
// #ifdef MP-WEIXIN
|
||||||
|
const path = reportData.value?.highlightVideoUrl
|
||||||
|
if (!path) return
|
||||||
|
const fullUrl = imgUrl(path)
|
||||||
|
savingHighlightVideo.value = true
|
||||||
|
uni.showLoading({ title: '下载中', mask: true })
|
||||||
|
try {
|
||||||
|
const tempPath = await new Promise((resolve, reject) => {
|
||||||
|
uni.downloadFile({
|
||||||
|
url: fullUrl,
|
||||||
|
success: (res) => {
|
||||||
|
if (res.statusCode === 200 && res.tempFilePath) {
|
||||||
|
resolve(res.tempFilePath)
|
||||||
|
} else {
|
||||||
|
reject(new Error(res.errMsg || `下载失败(${res.statusCode})`))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fail: (err) => reject(err || new Error('downloadFile fail'))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
uni.saveVideoToPhotosAlbum({
|
||||||
|
filePath: tempPath,
|
||||||
|
success: () => resolve(),
|
||||||
|
fail: (err) => reject(err || new Error('saveVideo fail'))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
uni.showToast({ title: '已保存到相册', icon: 'success' })
|
||||||
|
} catch (e) {
|
||||||
|
const msg = e && (e.errMsg || e.message) ? String(e.errMsg || e.message) : ''
|
||||||
|
if (msg.includes('auth deny') || msg.includes('authorize') || msg.includes('auth')) {
|
||||||
|
uni.showModal({
|
||||||
|
title: '需要相册权限',
|
||||||
|
content: '保存视频需授权写入系统相册(用于留存服务回顾短片),请在设置中开启。',
|
||||||
|
confirmText: '去设置',
|
||||||
|
success: (m) => {
|
||||||
|
if (m.confirm) uni.openSetting({})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else if (
|
||||||
|
msg.includes('downloadFile:fail') ||
|
||||||
|
msg.includes('domain') ||
|
||||||
|
msg.includes('不在以下 downloadFile')
|
||||||
|
) {
|
||||||
|
uni.showModal({
|
||||||
|
title: '无法下载短片',
|
||||||
|
content:
|
||||||
|
'请登录微信公众平台 → 小程序 → 开发管理 → 开发设置,将视频文件所在域名(与接口域名一致,如 https://api.xxx.com)加入「downloadFile 合法域名」。',
|
||||||
|
showCancel: false
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
uni.showToast({ title: msg.length > 0 && msg.length < 52 ? msg : '保存失败', icon: 'none' })
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
savingHighlightVideo.value = false
|
||||||
|
uni.hideLoading()
|
||||||
|
}
|
||||||
|
// #endif
|
||||||
|
}
|
||||||
|
|
||||||
|
const downloadHighlightVideoH5 = async () => {
|
||||||
|
// #ifdef H5
|
||||||
|
const path = reportData.value?.highlightVideoUrl
|
||||||
|
if (!path) return
|
||||||
|
const fullUrl = imgUrl(path)
|
||||||
|
const rawName = (reportData.value?.petName || '宠物').trim() || '宠物'
|
||||||
|
const safeName = rawName.replace(/[\\/:*?"<>|]/g, '_').slice(0, 32)
|
||||||
|
savingHighlightVideo.value = true
|
||||||
|
uni.showLoading({ title: '准备下载', mask: true })
|
||||||
|
try {
|
||||||
|
const res = await fetch(fullUrl, { mode: 'cors', credentials: 'omit' })
|
||||||
|
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||||
|
const blob = await res.blob()
|
||||||
|
const objectUrl = URL.createObjectURL(blob)
|
||||||
|
const a = document.createElement('a')
|
||||||
|
a.href = objectUrl
|
||||||
|
a.download = `服务回顾_${safeName}.mp4`
|
||||||
|
a.rel = 'noopener'
|
||||||
|
document.body.appendChild(a)
|
||||||
|
a.click()
|
||||||
|
document.body.removeChild(a)
|
||||||
|
URL.revokeObjectURL(objectUrl)
|
||||||
|
uni.showToast({ title: '已开始下载', icon: 'success' })
|
||||||
|
} catch {
|
||||||
|
try {
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
uni.setClipboardData({
|
||||||
|
data: fullUrl,
|
||||||
|
success: () => resolve(),
|
||||||
|
fail: (err) => reject(err)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
uni.showModal({
|
||||||
|
title: '无法直接下载',
|
||||||
|
content:
|
||||||
|
'可能受跨域或浏览器策略限制。已将视频链接复制到剪贴板,可粘贴到地址栏打开;或使用右上角菜单分享本页。',
|
||||||
|
showCancel: false
|
||||||
|
})
|
||||||
|
} catch {
|
||||||
|
uni.showModal({
|
||||||
|
title: '下载失败',
|
||||||
|
content: '请稍后重试,或复制报告链接在系统浏览器中打开。',
|
||||||
|
showCancel: false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
savingHighlightVideo.value = false
|
||||||
|
uni.hideLoading()
|
||||||
|
}
|
||||||
|
// #endif
|
||||||
|
}
|
||||||
|
|
||||||
const beforePhotos = computed(() => {
|
const beforePhotos = computed(() => {
|
||||||
if (!reportData.value?.beforePhotos) return []
|
if (!reportData.value?.beforePhotos) return []
|
||||||
// 支持新旧两种格式:旧版是 string[],新版是 {url, mediaType}[]
|
// 支持新旧两种格式:旧版是 string[],新版是 {url, mediaType}[]
|
||||||
@ -410,6 +614,9 @@ const shareTitle = computed(() => {
|
|||||||
if (!r) return '洗护前后对比'
|
if (!r) return '洗护前后对比'
|
||||||
const store = r.store?.name || '宠伴生活馆'
|
const store = r.store?.name || '宠伴生活馆'
|
||||||
const pet = r.petName || '爱宠'
|
const pet = r.petName || '爱宠'
|
||||||
|
if (r.highlightVideoStatus === 'done' && r.highlightVideoUrl) {
|
||||||
|
return `${pet}的服务回顾短片|${store}`
|
||||||
|
}
|
||||||
return `${pet}的洗护蜕变|${store}`
|
return `${pet}的洗护蜕变|${store}`
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -441,6 +648,10 @@ const loadReport = async () => {
|
|||||||
const res = await getReportByToken(token)
|
const res = await getReportByToken(token)
|
||||||
if (res && res.code === 200 && res.data) {
|
if (res && res.code === 200 && res.data) {
|
||||||
reportData.value = res.data
|
reportData.value = res.data
|
||||||
|
const m = res.data.highlightComposeMode
|
||||||
|
if (m === 'preset' || m === 'interleave') {
|
||||||
|
highlightComposeMode.value = m
|
||||||
|
}
|
||||||
maybeStartHighlightPoll()
|
maybeStartHighlightPoll()
|
||||||
} else if (res && res.code === 404) {
|
} else if (res && res.code === 404) {
|
||||||
reportData.value = null
|
reportData.value = null
|
||||||
@ -1144,6 +1355,20 @@ onUnmounted(() => stopHighlightPoll())
|
|||||||
line-height: 1.55;
|
line-height: 1.55;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
.highlight-desc-ops {
|
||||||
|
display: block;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #94a3b8;
|
||||||
|
line-height: 1.5;
|
||||||
|
margin: -4px 0 12px;
|
||||||
|
}
|
||||||
|
.highlight-fail-hint {
|
||||||
|
display: block;
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #b45309;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
.highlight-status {
|
.highlight-status {
|
||||||
padding: 10px 12px;
|
padding: 10px 12px;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
@ -1154,10 +1379,88 @@ onUnmounted(() => stopHighlightPoll())
|
|||||||
background: rgba(45, 185, 109, 0.12);
|
background: rgba(45, 185, 109, 0.12);
|
||||||
color: #166534;
|
color: #166534;
|
||||||
}
|
}
|
||||||
|
.highlight-processing-hint {
|
||||||
|
display: block;
|
||||||
|
margin-top: 12rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #64748b;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
.highlight-status--err {
|
.highlight-status--err {
|
||||||
background: rgba(239, 68, 68, 0.1);
|
background: rgba(239, 68, 68, 0.1);
|
||||||
color: #b91c1c;
|
color: #b91c1c;
|
||||||
}
|
}
|
||||||
|
.highlight-err-type {
|
||||||
|
display: block;
|
||||||
|
font-size: 26rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
color: #991b1b;
|
||||||
|
}
|
||||||
|
.highlight-err-msg {
|
||||||
|
display: block;
|
||||||
|
font-size: 24rpx;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: #7f1d1d;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
.highlight-meta {
|
||||||
|
display: block;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #888;
|
||||||
|
margin-top: 12rpx;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.highlight-mp-bar {
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
.highlight-btn--album {
|
||||||
|
background: linear-gradient(135deg, #0d9488 0%, #0f766e 100%);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.highlight-mp-hint {
|
||||||
|
display: block;
|
||||||
|
margin-top: 16rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #94a3b8;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
.highlight-mp-tip {
|
||||||
|
display: block;
|
||||||
|
margin-top: 12rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #64748b;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
.highlight-h5-bar {
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.highlight-mode {
|
||||||
|
display: flex;
|
||||||
|
gap: 16rpx;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.highlight-mode-item {
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
padding: 20rpx 16rpx;
|
||||||
|
font-size: 26rpx;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
background: #f3f4f6;
|
||||||
|
color: #6b7280;
|
||||||
|
border: 2rpx solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.highlight-mode-item.is-on {
|
||||||
|
background: #ecfdf5;
|
||||||
|
color: #047857;
|
||||||
|
border-color: #6ee7b7;
|
||||||
|
}
|
||||||
|
|
||||||
.highlight-player {
|
.highlight-player {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-height: 420px;
|
max-height: 420px;
|
||||||
@ -1169,6 +1472,10 @@ onUnmounted(() => stopHighlightPoll())
|
|||||||
display: flex;
|
display: flex;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
.highlight-btn--block {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.highlight-btn {
|
.highlight-btn {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user