Compare commits
3
Commits
6d9fe1bbf8
...
a1092c02c3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a1092c02c3 | ||
|
|
f48a66b611 | ||
|
|
28cd110dae |
@@ -145,7 +145,7 @@
|
||||
<div class="panel-heading panel-heading--table">
|
||||
<div>
|
||||
<h2>明细数据列表</h2>
|
||||
<p>展开部门可查看人员明细;加粉=区间有效加粉(按员工+客户去重,须会话同意,剔除已删客户);挂号=已支付且实收低于 10 元的订单,预约=有效预约记录;开口率=开口/加粉,挂号率=挂号/加粉,面诊率=面诊/预约,预约接诊率=接诊诊单/面诊,面诊接诊率=面诊/挂号,接诊率=接诊诊单/加粉</p>
|
||||
<p>展开部门可查看人员明细;加粉=总进线=区间有效加粉(按员工+客户去重,须会话同意,剔除已删客户与继承客户);挂号=已支付且实收低于 10 元的订单,预约=有效预约记录;开口率=开口/加粉,挂号率=挂号/加粉,面诊率=面诊/挂号(看挂号后流失),预约率=面诊/预约(看预约后未面诊),面诊接诊率=接诊诊单/面诊,接诊率=接诊诊单/总进线</p>
|
||||
</div>
|
||||
<span>{{ dashboard.rows.length }} 个顶层节点</span>
|
||||
</div>
|
||||
@@ -194,13 +194,13 @@
|
||||
<template #default="{ row }">{{ formatPercent(row.paid_appointment_rate) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="面诊率" min-width="96" align="right">
|
||||
<template #default="{ row }">{{ formatPercent(row.interview_paid_rate) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="预约率" min-width="96" align="right">
|
||||
<template #default="{ row }">{{ formatPercent(row.interview_rate) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="预约接诊率" min-width="116" align="right">
|
||||
<template #default="{ row }">{{ formatPercent(row.interview_receive_rate) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="面诊接诊率" min-width="116" align="right">
|
||||
<template #default="{ row }">{{ formatPercent(row.interview_paid_rate) }}</template>
|
||||
<template #default="{ row }">{{ formatPercent(row.interview_receive_rate) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="接诊率" min-width="96" align="right">
|
||||
<template #default="{ row }">{{ formatPercent(row.receive_rate) }}</template>
|
||||
@@ -330,7 +330,7 @@ const timeOptions = [
|
||||
{ label: '自定义', value: 'custom' }
|
||||
]
|
||||
const metricCards: Array<{ key: string; label: string; type: MetricType; hint: string }> = [
|
||||
{ key: 'add_fans_count', label: '加粉数', type: 'count', hint: '区间有效加粉:去重,须会话同意,剔除已删客户' },
|
||||
{ key: 'add_fans_count', label: '加粉数', type: 'count', hint: '区间有效加粉:去重,须会话同意,剔除已删客户与继承客户' },
|
||||
{ key: 'total_open_count', label: '开口数', type: 'count', hint: '来源于个人业绩录入' },
|
||||
{ key: 'interview_count', label: '面诊', type: 'count', hint: '已完成预约' },
|
||||
{ key: 'completed_order_count', label: '接诊诊单', type: 'count', hint: '业务订单,按创建人归属并过滤无效单' },
|
||||
|
||||
@@ -0,0 +1,556 @@
|
||||
<template>
|
||||
<section class="embedded-panel" v-loading="loading">
|
||||
<div class="panel-toolbar">
|
||||
<div>
|
||||
<h2>医生排班</h2>
|
||||
<p>查看近 7 日出诊医生及可约时段(与医生排班管理同口径)</p>
|
||||
</div>
|
||||
<div class="toolbar-actions">
|
||||
<el-button
|
||||
:icon="Refresh"
|
||||
:loading="refreshing"
|
||||
:disabled="!selectedDoctorId || !form.date"
|
||||
@click="handleRefreshSlots"
|
||||
>
|
||||
刷新时段
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-form :model="form" label-width="88px" class="paiban-form">
|
||||
<el-form-item label="选择医生">
|
||||
<div class="doctor-list">
|
||||
<el-radio-group v-model="selectedDoctorId" @change="handleDoctorChange">
|
||||
<el-radio
|
||||
v-for="doctor in doctorList"
|
||||
:key="doctor.id"
|
||||
:value="doctor.id"
|
||||
class="doctor-radio"
|
||||
>
|
||||
{{ doctor.name }}
|
||||
</el-radio>
|
||||
</el-radio-group>
|
||||
<el-empty
|
||||
v-if="!loading && doctorList.length === 0"
|
||||
description="暂无可用医生"
|
||||
:image-size="64"
|
||||
/>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="排班时间">
|
||||
<el-empty v-if="!selectedDoctorId" description="请先选择医生" :image-size="72" />
|
||||
<el-empty
|
||||
v-else-if="selectedDoctorId && doctorRosterDates.length === 0"
|
||||
description="该医生暂无排班"
|
||||
:image-size="72"
|
||||
/>
|
||||
<div v-else class="paiban-time-container">
|
||||
<div class="date-selector">
|
||||
<el-button
|
||||
v-for="dateOption in dateOptions"
|
||||
:key="dateOption.date"
|
||||
:type="form.date === dateOption.date ? 'primary' : ''"
|
||||
class="date-button"
|
||||
@click="selectDate(dateOption.date)"
|
||||
>
|
||||
{{ dateOption.label }}
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div v-if="form.date" class="time-slots-container">
|
||||
<div class="time-slots-grid">
|
||||
<div
|
||||
v-for="slot in filteredTimeSlots"
|
||||
:key="slot.time"
|
||||
class="time-slot-item"
|
||||
:class="{
|
||||
available: slot.available,
|
||||
unavailable: !slot.available,
|
||||
selected: form.selectedTime === slot.time
|
||||
}"
|
||||
@click="selectTimeSlot(slot)"
|
||||
>
|
||||
<div class="slot-time">{{ slot.time }}</div>
|
||||
<div class="slot-status" :class="{ 'status-available': slot.available }">
|
||||
{{ slot.available ? '可约' : slot.hasAppointment ? '已约' : '空号' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-empty
|
||||
v-if="filteredTimeSlots.length === 0"
|
||||
description="当前日期暂无可展示时段"
|
||||
:image-size="64"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onMounted, onUnmounted, reactive, ref } from 'vue'
|
||||
import { Refresh } from '@element-plus/icons-vue'
|
||||
import dayjs from 'dayjs'
|
||||
import isoWeek from 'dayjs/plugin/isoWeek'
|
||||
import { getDoctors } from '@/api/tcm'
|
||||
import { getAvailableSlots, rosterLists } from '@/api/doctor'
|
||||
import feedback from '@/utils/feedback'
|
||||
|
||||
dayjs.extend(isoWeek)
|
||||
|
||||
interface TimeSlot {
|
||||
time: string
|
||||
available: boolean
|
||||
hasAppointment: boolean
|
||||
quota: number
|
||||
}
|
||||
|
||||
interface DateOption {
|
||||
date: string
|
||||
label: string
|
||||
}
|
||||
|
||||
const loading = ref(false)
|
||||
const refreshing = ref(false)
|
||||
const doctorList = ref<any[]>([])
|
||||
const timeSlots = ref<TimeSlot[]>([])
|
||||
const selectedDoctorId = ref(0)
|
||||
const doctorRosterDates = ref<string[]>([])
|
||||
let autoRefreshTimer: number | null = null
|
||||
let isLoadingSlots = false
|
||||
|
||||
const form = reactive({
|
||||
date: '',
|
||||
selectedTime: ''
|
||||
})
|
||||
|
||||
const dateOptions = computed<DateOption[]>(() => {
|
||||
if (!selectedDoctorId.value || doctorRosterDates.value.length === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
const options: DateOption[] = []
|
||||
const weekDays = ['日', '一', '二', '三', '四', '五', '六']
|
||||
const today = dayjs().startOf('day')
|
||||
|
||||
for (const date of doctorRosterDates.value) {
|
||||
const dateObj = dayjs(date)
|
||||
if (dateObj.isBefore(today)) {
|
||||
continue
|
||||
}
|
||||
options.push({
|
||||
date,
|
||||
label: `${dateObj.format('MM月DD日')} (${weekDays[dateObj.day()]})`
|
||||
})
|
||||
}
|
||||
|
||||
return options
|
||||
})
|
||||
|
||||
const filteredTimeSlots = computed(() => {
|
||||
if (!form.date || timeSlots.value.length === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
const today = dayjs().format('YYYY-MM-DD')
|
||||
if (form.date !== today) {
|
||||
return timeSlots.value
|
||||
}
|
||||
|
||||
const now = dayjs()
|
||||
return timeSlots.value.map((slot) => {
|
||||
const slotDateTime = dayjs(`${form.date} ${slot.time}`)
|
||||
const isPast = slotDateTime.isBefore(now) || slotDateTime.isSame(now, 'minute')
|
||||
if (isPast) {
|
||||
return { ...slot, available: false }
|
||||
}
|
||||
return slot
|
||||
})
|
||||
})
|
||||
|
||||
async function loadDoctors() {
|
||||
try {
|
||||
loading.value = true
|
||||
const res = await getDoctors()
|
||||
doctorList.value = res || []
|
||||
if (doctorList.value.length > 0 && !selectedDoctorId.value) {
|
||||
selectedDoctorId.value = doctorList.value[0].id
|
||||
await handleDoctorChange()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载医生列表失败:', error)
|
||||
feedback.msgError('加载医生列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDoctorChange() {
|
||||
form.selectedTime = ''
|
||||
form.date = ''
|
||||
timeSlots.value = []
|
||||
doctorRosterDates.value = []
|
||||
if (selectedDoctorId.value) {
|
||||
await loadDoctorRoster()
|
||||
}
|
||||
}
|
||||
|
||||
async function loadDoctorRoster() {
|
||||
if (!selectedDoctorId.value) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
loading.value = true
|
||||
const startDate = dayjs().format('YYYY-MM-DD')
|
||||
const endDate = dayjs().add(6, 'day').format('YYYY-MM-DD')
|
||||
const res = await rosterLists({
|
||||
doctor_id: selectedDoctorId.value,
|
||||
start_date: startDate,
|
||||
end_date: endDate,
|
||||
status: 1
|
||||
})
|
||||
|
||||
if (res?.lists && res.lists.length > 0) {
|
||||
doctorRosterDates.value = [...new Set(res.lists.map((item: any) => item.date))] as string[]
|
||||
doctorRosterDates.value.sort()
|
||||
await nextTick()
|
||||
if (doctorRosterDates.value.length > 0) {
|
||||
const today = dayjs().format('YYYY-MM-DD')
|
||||
const defaultDate = doctorRosterDates.value.includes(today)
|
||||
? today
|
||||
: doctorRosterDates.value[0]
|
||||
selectDate(defaultDate)
|
||||
}
|
||||
} else {
|
||||
doctorRosterDates.value = []
|
||||
feedback.msgWarning('该医生暂无排班')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载医生排班失败:', error)
|
||||
feedback.msgError('加载医生排班失败')
|
||||
doctorRosterDates.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function selectDate(date: string) {
|
||||
form.date = date
|
||||
form.selectedTime = ''
|
||||
if (selectedDoctorId.value) {
|
||||
loadTimeSlots()
|
||||
}
|
||||
}
|
||||
|
||||
async function loadTimeSlots(silent = false) {
|
||||
if (!selectedDoctorId.value || !form.date) {
|
||||
return
|
||||
}
|
||||
if (isLoadingSlots) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
isLoadingSlots = true
|
||||
if (!silent) {
|
||||
loading.value = true
|
||||
}
|
||||
|
||||
const response = await getAvailableSlots({
|
||||
doctor_id: selectedDoctorId.value,
|
||||
appointment_date: form.date,
|
||||
period: 'all'
|
||||
})
|
||||
|
||||
timeSlots.value = (response?.slots || []).map((slot: any) => ({
|
||||
time: slot.time,
|
||||
available: slot.available,
|
||||
hasAppointment: Boolean(slot.has_appointment ?? slot.available === false),
|
||||
quota: slot.available ? 1 : 0
|
||||
}))
|
||||
} catch (error) {
|
||||
console.error('加载时间段失败:', error)
|
||||
if (!silent) {
|
||||
feedback.msgError('加载时间段失败')
|
||||
}
|
||||
timeSlots.value = []
|
||||
} finally {
|
||||
isLoadingSlots = false
|
||||
if (!silent) {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function selectTimeSlot(slot: TimeSlot) {
|
||||
if (!slot.available) {
|
||||
feedback.msgWarning('该时间段不可预约')
|
||||
return
|
||||
}
|
||||
form.selectedTime = slot.time
|
||||
}
|
||||
|
||||
async function handleRefreshSlots() {
|
||||
if (!selectedDoctorId.value || !form.date) {
|
||||
return
|
||||
}
|
||||
if (isLoadingSlots) {
|
||||
feedback.msgWarning('正在刷新中,请稍候')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
refreshing.value = true
|
||||
const previousSelection = form.selectedTime
|
||||
form.selectedTime = ''
|
||||
await loadTimeSlots()
|
||||
if (previousSelection) {
|
||||
const slot = timeSlots.value.find((s) => s.time === previousSelection)
|
||||
if (slot?.available) {
|
||||
form.selectedTime = previousSelection
|
||||
}
|
||||
}
|
||||
feedback.msgSuccess('刷新成功')
|
||||
} catch (error) {
|
||||
console.error('刷新失败:', error)
|
||||
feedback.msgError('刷新失败,请重试')
|
||||
} finally {
|
||||
refreshing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function startAutoRefresh() {
|
||||
stopAutoRefresh()
|
||||
autoRefreshTimer = window.setInterval(() => {
|
||||
if (selectedDoctorId.value && form.date) {
|
||||
loadTimeSlots(true)
|
||||
}
|
||||
}, 5000)
|
||||
}
|
||||
|
||||
function stopAutoRefresh() {
|
||||
if (autoRefreshTimer) {
|
||||
clearInterval(autoRefreshTimer)
|
||||
autoRefreshTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
if (selectedDoctorId.value && form.date) {
|
||||
await loadTimeSlots()
|
||||
return
|
||||
}
|
||||
await loadDoctors()
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
refresh,
|
||||
loading
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
loadDoctors()
|
||||
startAutoRefresh()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
stopAutoRefresh()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.embedded-panel {
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
.panel-toolbar {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
margin-bottom: 18px;
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: #1f2a37;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 6px 0 0;
|
||||
color: #667085;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
.toolbar-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.paiban-form {
|
||||
:deep(.el-form-item__label) {
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.doctor-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
|
||||
.doctor-radio {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.paiban-time-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.date-selector {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.date-button {
|
||||
min-width: 130px;
|
||||
height: 40px;
|
||||
font-size: 14px;
|
||||
border-radius: 8px;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.time-slots-container {
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.time-slots-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
|
||||
gap: 10px;
|
||||
max-height: 450px;
|
||||
overflow-y: auto;
|
||||
padding: 2px;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: #dcdfe6;
|
||||
border-radius: 3px;
|
||||
|
||||
&:hover {
|
||||
background-color: #c0c4cc;
|
||||
}
|
||||
}
|
||||
|
||||
.time-slot-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 8px;
|
||||
border: 2px solid #e4e7ed;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
background-color: #fff;
|
||||
min-height: 70px;
|
||||
|
||||
.slot-time {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.slot-status {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
padding: 0 8px;
|
||||
border-radius: 4px;
|
||||
background-color: #f4f4f5;
|
||||
|
||||
&.status-available {
|
||||
color: #67c23a;
|
||||
background-color: #f0f9ff;
|
||||
}
|
||||
}
|
||||
|
||||
&.available {
|
||||
border-color: #e4e7ed;
|
||||
|
||||
&:hover {
|
||||
border-color: #409eff;
|
||||
background-color: #ecf5ff;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
&.unavailable {
|
||||
background-color: #f5f7fa;
|
||||
border-color: #e4e7ed;
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
|
||||
.slot-time {
|
||||
color: #c0c4cc;
|
||||
}
|
||||
|
||||
.slot-status {
|
||||
color: #c0c4cc;
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.selected {
|
||||
border-color: #409eff;
|
||||
background: linear-gradient(135deg, #409eff 0%, #66b1ff 100%);
|
||||
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.3);
|
||||
|
||||
.slot-time {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.slot-status {
|
||||
color: #fff;
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-radio-group) {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
:deep(.el-radio) {
|
||||
margin-right: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -17,6 +17,7 @@
|
||||
<el-tab-pane label="患者列表" name="patients" />
|
||||
<el-tab-pane label="订单管理" name="orders" />
|
||||
<el-tab-pane label="面诊进度" name="progress" />
|
||||
<el-tab-pane label="医生排班" name="paiban" />
|
||||
</el-tabs>
|
||||
|
||||
<div v-show="activeWorkspace === 'patients'">
|
||||
@@ -207,6 +208,10 @@
|
||||
ref="progressPanelRef"
|
||||
@open-diagnosis="openDiagnosis"
|
||||
/>
|
||||
<paiban-panel
|
||||
v-if="activeWorkspace === 'paiban'"
|
||||
ref="paibanPanelRef"
|
||||
/>
|
||||
</el-card>
|
||||
|
||||
<edit-popup ref="editRef" @success="refreshPage" />
|
||||
@@ -342,6 +347,7 @@ const EditPopup = defineAsyncComponent(() => import('@/views/tcm/diagnosis/edit.
|
||||
const AppointmentPopup = defineAsyncComponent(() => import('@/views/tcm/diagnosis/appointment.vue'))
|
||||
const OrderPanel = defineAsyncComponent(() => import('./components/OrderPanel.vue'))
|
||||
const ProgressPanel = defineAsyncComponent(() => import('./components/ProgressPanel.vue'))
|
||||
const PaibanPanel = defineAsyncComponent(() => import('./components/PaibanPanel.vue'))
|
||||
|
||||
type StatusFilter = '' | 'unbooked' | 'pending_interview' | 'completed' | 'missed'
|
||||
type DateType = 'all' | 'today' | 'tomorrow' | 'day_after' | 'last7' | 'last30' | 'custom'
|
||||
@@ -355,6 +361,7 @@ const editRef = ref<any>()
|
||||
const appointmentRef = ref<any>()
|
||||
const orderPanelRef = ref<any>()
|
||||
const progressPanelRef = ref<any>()
|
||||
const paibanPanelRef = ref<any>()
|
||||
const qrcodeDialogVisible = ref(false)
|
||||
const qrcodeLoading = ref(false)
|
||||
const qrcodeUrl = ref('')
|
||||
@@ -423,6 +430,7 @@ const canFillIdCard = computed(() => hasPermission(['tcm.diagnosis/edit']))
|
||||
const workspaceLoading = computed(() => {
|
||||
if (activeWorkspace.value === 'orders') return Boolean(orderPanelRef.value?.loading)
|
||||
if (activeWorkspace.value === 'progress') return Boolean(progressPanelRef.value?.loading)
|
||||
if (activeWorkspace.value === 'paiban') return Boolean(paibanPanelRef.value?.loading)
|
||||
return pager.loading
|
||||
})
|
||||
|
||||
@@ -437,6 +445,7 @@ function handleFilterChange() {
|
||||
function refreshPage() {
|
||||
if (activeWorkspace.value === 'orders') return orderPanelRef.value?.refresh?.()
|
||||
if (activeWorkspace.value === 'progress') return progressPanelRef.value?.refresh?.()
|
||||
if (activeWorkspace.value === 'paiban') return paibanPanelRef.value?.refresh?.()
|
||||
return getLists()
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
import hashlib
|
||||
import struct
|
||||
import zlib
|
||||
from collections.abc import Mapping
|
||||
from collections.abc import Mapping, Sequence
|
||||
from copy import deepcopy
|
||||
from datetime import date, datetime, timedelta
|
||||
from os import PathLike
|
||||
@@ -163,8 +163,27 @@ class DemoDoctorRepository:
|
||||
"room_id": "demo-room-501",
|
||||
"status": 2,
|
||||
"status_text": "已结束",
|
||||
"recording_status_text": "录制完成",
|
||||
"recording_urls_list": ["https://media.example.invalid/demo/diagnosis-501.mp4"],
|
||||
"recording_status_text": "录制完成",
|
||||
"recording_urls_list": ["https://media.example.invalid/demo/diagnosis-501.mp4"],
|
||||
"transcription_status": "completed",
|
||||
"transcription_status_text": "文字已生成",
|
||||
"transcript_text": "患者:最近睡眠比上周好一些。\n医生:继续记录睡眠和空腹血糖。",
|
||||
"transcript_segments": [
|
||||
{
|
||||
"segment_id": "demo-1",
|
||||
"speaker_role": "patient",
|
||||
"speaker_user_id": "patient_301",
|
||||
"timestamp": 1,
|
||||
"text": "最近睡眠比上周好一些。",
|
||||
},
|
||||
{
|
||||
"segment_id": "demo-2",
|
||||
"speaker_role": "doctor",
|
||||
"speaker_user_id": "doctor_1001",
|
||||
"timestamp": 2,
|
||||
"text": "继续记录睡眠和空腹血糖。",
|
||||
},
|
||||
],
|
||||
"start_time_text": f"{self._today.isoformat()} 09:10:00",
|
||||
"end_time_text": f"{self._today.isoformat()} 09:22:00",
|
||||
"duration_text": "12分00秒",
|
||||
@@ -2233,8 +2252,12 @@ class DemoDoctorRepository:
|
||||
"room_id": "manual-upload",
|
||||
"status": 2,
|
||||
"status_text": "已结束",
|
||||
"recording_status_text": "待上传",
|
||||
"recording_urls_list": [],
|
||||
"recording_status_text": "待上传",
|
||||
"recording_urls_list": [],
|
||||
"transcription_status": "not_started",
|
||||
"transcription_status_text": "未生成文字",
|
||||
"transcript_text": "",
|
||||
"transcript_segments": [],
|
||||
"start_time_text": datetime.now().replace(microsecond=0).isoformat(sep=" "),
|
||||
"end_time_text": datetime.now().replace(microsecond=0).isoformat(sep=" "),
|
||||
"duration_text": "0秒",
|
||||
@@ -2531,8 +2554,12 @@ class DemoDoctorRepository:
|
||||
replay = {
|
||||
**record,
|
||||
"status_text": "呼叫中",
|
||||
"recording_status_text": "未录制",
|
||||
"recording_urls_list": [],
|
||||
"recording_status_text": "未录制",
|
||||
"recording_urls_list": [],
|
||||
"transcription_status": "not_started",
|
||||
"transcription_status_text": "未生成文字",
|
||||
"transcript_text": "",
|
||||
"transcript_segments": [],
|
||||
"start_time_text": datetime.now().replace(microsecond=0).isoformat(sep=" "),
|
||||
"end_time_text": "",
|
||||
"duration_text": "—",
|
||||
@@ -2567,7 +2594,7 @@ class DemoDoctorRepository:
|
||||
)
|
||||
return deepcopy(record)
|
||||
|
||||
def bind_call_room(self, diagnosis_id: int, room_id: str) -> dict[str, Any]:
|
||||
def bind_call_room(self, diagnosis_id: int, room_id: str) -> dict[str, Any]:
|
||||
"""Persist the room identifier on the active demo call."""
|
||||
|
||||
if not room_id.strip():
|
||||
@@ -2594,11 +2621,133 @@ class DemoDoctorRepository:
|
||||
"status_text": "通话中",
|
||||
}
|
||||
)
|
||||
return {
|
||||
"diagnosis_id": diagnosis_id,
|
||||
"room_id": room_id.strip(),
|
||||
"cloud_recording": {"started": False, "message": "演示模式不录制"},
|
||||
}
|
||||
return {
|
||||
"diagnosis_id": diagnosis_id,
|
||||
"room_id": room_id.strip(),
|
||||
"cloud_recording": {"started": False, "message": "演示模式不录制"},
|
||||
}
|
||||
|
||||
def _demo_call_record(
|
||||
self, diagnosis_id: int, call_record_id: int | str
|
||||
) -> dict[str, Any]:
|
||||
record = next(
|
||||
(
|
||||
row
|
||||
for row in self._call_records.get(diagnosis_id, [])
|
||||
if str(row.get("id") or row.get("call_record_id") or "")
|
||||
== str(call_record_id)
|
||||
),
|
||||
None,
|
||||
)
|
||||
if record is None:
|
||||
raise RepositoryNotFoundError(f"call record {call_record_id} not found")
|
||||
return record
|
||||
|
||||
def start_call_transcription(
|
||||
self,
|
||||
diagnosis_id: int,
|
||||
call_record_id: int | str,
|
||||
transcription_session_id: str,
|
||||
*,
|
||||
language: str = "zh-CN",
|
||||
) -> dict[str, Any]:
|
||||
"""Start an in-memory transcript attached to one demo call record."""
|
||||
|
||||
clean_session = transcription_session_id.strip()
|
||||
if not clean_session:
|
||||
raise ValueError("transcription_session_id is required")
|
||||
with self._lock:
|
||||
self._find_consultation(diagnosis_id)
|
||||
record = self._demo_call_record(diagnosis_id, call_record_id)
|
||||
existing = str(record.get("transcription_session_id") or "")
|
||||
if existing and existing != clean_session:
|
||||
raise ValueError("another transcription session already exists")
|
||||
record.update(
|
||||
{
|
||||
"transcription_session_id": clean_session,
|
||||
"transcription_language": language.strip() or "zh-CN",
|
||||
"transcription_status": "running",
|
||||
"transcription_status_text": "录音转写中",
|
||||
"transcript_text": "",
|
||||
"transcript_segments": [],
|
||||
}
|
||||
)
|
||||
return deepcopy(record)
|
||||
|
||||
def upsert_call_transcript_segments(
|
||||
self,
|
||||
diagnosis_id: int,
|
||||
call_record_id: int | str,
|
||||
transcription_session_id: str,
|
||||
segments: Sequence[Mapping[str, Any]],
|
||||
) -> dict[str, Any]:
|
||||
"""Idempotently append/update completed segments in demo mode."""
|
||||
|
||||
with self._lock:
|
||||
record = self._demo_call_record(diagnosis_id, call_record_id)
|
||||
if record.get("transcription_session_id") != transcription_session_id:
|
||||
raise ValueError("transcription session does not match the call record")
|
||||
stored = record.setdefault("transcript_segments", [])
|
||||
for incoming in segments:
|
||||
segment_id = str(incoming.get("segment_id") or "").strip()
|
||||
text = str(incoming.get("text") or "").strip()
|
||||
if not segment_id or not text:
|
||||
raise ValueError("transcript segment_id and text are required")
|
||||
normalized = {
|
||||
"segment_id": segment_id,
|
||||
"speaker_role": str(incoming.get("speaker_role") or "unknown"),
|
||||
"speaker_user_id": str(incoming.get("speaker_user_id") or ""),
|
||||
"timestamp": int(incoming.get("timestamp") or 0),
|
||||
"text": text,
|
||||
}
|
||||
current = next(
|
||||
(row for row in stored if row.get("segment_id") == segment_id), None
|
||||
)
|
||||
if current is None:
|
||||
stored.append(normalized)
|
||||
else:
|
||||
current.update(normalized)
|
||||
stored.sort(key=lambda row: (int(row.get("timestamp") or 0), row["segment_id"]))
|
||||
labels = {"doctor": "医生", "patient": "患者", "unknown": "未知说话人"}
|
||||
record["transcript_text"] = "\n".join(
|
||||
f"{labels.get(str(row.get('speaker_role')), '未知说话人')}:{row['text']}"
|
||||
for row in stored
|
||||
)
|
||||
return deepcopy(record)
|
||||
|
||||
def finish_call_transcription(
|
||||
self,
|
||||
diagnosis_id: int,
|
||||
call_record_id: int | str,
|
||||
transcription_session_id: str,
|
||||
expected_segment_count: int,
|
||||
*,
|
||||
status: str = "completed",
|
||||
) -> dict[str, Any]:
|
||||
"""Finalize demo transcript fields on the same call record."""
|
||||
|
||||
clean_status = status.strip().lower()
|
||||
if clean_status not in {"completed", "partial", "failed"}:
|
||||
raise ValueError("transcription status is invalid")
|
||||
with self._lock:
|
||||
record = self._demo_call_record(diagnosis_id, call_record_id)
|
||||
if record.get("transcription_session_id") != transcription_session_id:
|
||||
raise ValueError("transcription session does not match the call record")
|
||||
actual_count = len(record.get("transcript_segments") or [])
|
||||
final_status = clean_status
|
||||
if clean_status == "completed" and actual_count < expected_segment_count:
|
||||
final_status = "partial"
|
||||
record["transcription_status"] = final_status
|
||||
record["transcription_status_text"] = {
|
||||
"completed": "文字已生成",
|
||||
"partial": "文字部分保存",
|
||||
"failed": "文字生成失败",
|
||||
}[final_status]
|
||||
record["transcription_segment_count"] = actual_count
|
||||
record["transcription_finished_at"] = datetime.now().replace(
|
||||
microsecond=0
|
||||
).isoformat(sep=" ")
|
||||
return deepcopy(record)
|
||||
|
||||
def my_self(self) -> Session:
|
||||
"""Compatibility alias for :meth:`get_session`."""
|
||||
|
||||
@@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
import mimetypes
|
||||
import re
|
||||
import time
|
||||
from collections.abc import Mapping
|
||||
from collections.abc import Mapping, Sequence
|
||||
from contextlib import suppress
|
||||
from datetime import date
|
||||
from io import BytesIO
|
||||
@@ -619,8 +619,38 @@ class DoctorRepository(Protocol):
|
||||
def end_call(self, diagnosis_id: int) -> Any:
|
||||
"""End the active diagnosis call."""
|
||||
|
||||
def bind_call_room(self, diagnosis_id: int, room_id: str) -> Any:
|
||||
"""Bind a TRTC room to the active call."""
|
||||
def bind_call_room(self, diagnosis_id: int, room_id: str) -> Any:
|
||||
"""Bind a TRTC room to the active call."""
|
||||
|
||||
def start_call_transcription(
|
||||
self,
|
||||
diagnosis_id: int,
|
||||
call_record_id: int | str,
|
||||
transcription_session_id: str,
|
||||
*,
|
||||
language: str = "zh-CN",
|
||||
) -> Any:
|
||||
"""Start a transcript stored on one exact call record."""
|
||||
|
||||
def upsert_call_transcript_segments(
|
||||
self,
|
||||
diagnosis_id: int,
|
||||
call_record_id: int | str,
|
||||
transcription_session_id: str,
|
||||
segments: Sequence[Mapping[str, Any]],
|
||||
) -> Any:
|
||||
"""Idempotently persist completed transcript segments."""
|
||||
|
||||
def finish_call_transcription(
|
||||
self,
|
||||
diagnosis_id: int,
|
||||
call_record_id: int | str,
|
||||
transcription_session_id: str,
|
||||
expected_segment_count: int,
|
||||
*,
|
||||
status: str = "completed",
|
||||
) -> Any:
|
||||
"""Finalize and materialize the transcript text on a call record."""
|
||||
|
||||
def my_self(self) -> Session:
|
||||
"""Compatibility alias for :meth:`get_session`."""
|
||||
@@ -2171,32 +2201,157 @@ class RemoteDoctorRepository:
|
||||
ticket.diagnosis_id = diagnosis_id
|
||||
return ticket
|
||||
|
||||
def start_call(self, diagnosis_id: int, patient_id: int, *, call_type: int = 2) -> Any:
|
||||
"""Create the server-side call record before ringing participants."""
|
||||
|
||||
return self.client.post(
|
||||
"tcm.diagnosis/startCall",
|
||||
{
|
||||
"diagnosis_id": diagnosis_id,
|
||||
"patient_id": patient_id,
|
||||
"call_type": call_type,
|
||||
},
|
||||
)
|
||||
def start_call(self, diagnosis_id: int, patient_id: int, *, call_type: int = 2) -> Any:
|
||||
"""Create the server-side call record before ringing participants."""
|
||||
|
||||
payload = self.client.post(
|
||||
"tcm.diagnosis/startCall",
|
||||
{
|
||||
"diagnosis_id": diagnosis_id,
|
||||
"patient_id": patient_id,
|
||||
"call_type": call_type,
|
||||
},
|
||||
)
|
||||
if not isinstance(payload, Mapping):
|
||||
raise ApiProtocolError(
|
||||
"tcm.diagnosis/startCall returned no valid call_record_id object", data=payload
|
||||
)
|
||||
raw_id = next(
|
||||
(
|
||||
payload[key]
|
||||
for key in ("call_record_id", "callRecordId", "id")
|
||||
if key in payload
|
||||
),
|
||||
None,
|
||||
)
|
||||
try:
|
||||
if isinstance(raw_id, bool):
|
||||
raise ValueError
|
||||
call_record_id = int(raw_id)
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise ApiProtocolError(
|
||||
"tcm.diagnosis/startCall returned no valid call_record_id",
|
||||
data=dict(payload),
|
||||
) from exc
|
||||
if call_record_id <= 0:
|
||||
raise ApiProtocolError(
|
||||
"tcm.diagnosis/startCall returned no valid call_record_id",
|
||||
data=dict(payload),
|
||||
)
|
||||
return {"call_record_id": call_record_id}
|
||||
|
||||
def end_call(self, diagnosis_id: int) -> Any:
|
||||
"""End the active call/recording associated with a diagnosis."""
|
||||
|
||||
return self.client.post("tcm.diagnosis/endCall", {"diagnosis_id": diagnosis_id})
|
||||
|
||||
def bind_call_room(self, diagnosis_id: int, room_id: str) -> Any:
|
||||
"""Bind the actual TRTC room to the active call record."""
|
||||
def bind_call_room(self, diagnosis_id: int, room_id: str) -> Any:
|
||||
"""Bind the actual TRTC room to the active call record."""
|
||||
|
||||
if not room_id.strip():
|
||||
raise ValueError("room_id is required")
|
||||
return self.client.post(
|
||||
"tcm.diagnosis/bindCallRoom",
|
||||
{"diagnosis_id": diagnosis_id, "room_id": room_id.strip()},
|
||||
)
|
||||
return self.client.post(
|
||||
"tcm.diagnosis/bindCallRoom",
|
||||
{"diagnosis_id": diagnosis_id, "room_id": room_id.strip()},
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _transcription_identity(
|
||||
diagnosis_id: int,
|
||||
call_record_id: int | str,
|
||||
transcription_session_id: str,
|
||||
) -> dict[str, Any]:
|
||||
if diagnosis_id <= 0:
|
||||
raise ValueError("diagnosis_id must be positive")
|
||||
if isinstance(call_record_id, bool) or not str(call_record_id).strip():
|
||||
raise ValueError("call_record_id is required")
|
||||
clean_session = transcription_session_id.strip()
|
||||
if not clean_session or len(clean_session) > 128:
|
||||
raise ValueError("transcription_session_id must contain 1 to 128 characters")
|
||||
return {
|
||||
"diagnosis_id": diagnosis_id,
|
||||
"call_record_id": call_record_id,
|
||||
"transcription_session_id": clean_session,
|
||||
}
|
||||
|
||||
def start_call_transcription(
|
||||
self,
|
||||
diagnosis_id: int,
|
||||
call_record_id: int | str,
|
||||
transcription_session_id: str,
|
||||
*,
|
||||
language: str = "zh-CN",
|
||||
) -> Any:
|
||||
"""Create the server transcript session for one call record."""
|
||||
|
||||
body = self._transcription_identity(
|
||||
diagnosis_id, call_record_id, transcription_session_id
|
||||
)
|
||||
body["language"] = language.strip()[:32] or "zh-CN"
|
||||
return self.client.post("tcm.diagnosis/startCallTranscription", body)
|
||||
|
||||
def upsert_call_transcript_segments(
|
||||
self,
|
||||
diagnosis_id: int,
|
||||
call_record_id: int | str,
|
||||
transcription_session_id: str,
|
||||
segments: Sequence[Mapping[str, Any]],
|
||||
) -> Any:
|
||||
"""Upsert a bounded batch using each segment_id as the idempotency key."""
|
||||
|
||||
body = self._transcription_identity(
|
||||
diagnosis_id, call_record_id, transcription_session_id
|
||||
)
|
||||
normalized: list[dict[str, Any]] = []
|
||||
if len(segments) > 50:
|
||||
raise ValueError("at most 50 transcript segments may be submitted at once")
|
||||
for segment in segments:
|
||||
segment_id = str(segment.get("segment_id") or "").strip()
|
||||
text = str(segment.get("text") or "").strip()
|
||||
if not segment_id or len(segment_id) > 160:
|
||||
raise ValueError("transcript segment_id is invalid")
|
||||
if not text or len(text) > 4_000:
|
||||
raise ValueError("transcript text is invalid")
|
||||
normalized.append(
|
||||
{
|
||||
"segment_id": segment_id,
|
||||
"speaker_user_id": str(segment.get("speaker_user_id") or "")[:160],
|
||||
"speaker_role": str(segment.get("speaker_role") or "unknown")[:20],
|
||||
"timestamp": max(int(segment.get("timestamp") or 0), 0),
|
||||
"text": text,
|
||||
}
|
||||
)
|
||||
if not normalized:
|
||||
raise ValueError("at least one transcript segment is required")
|
||||
body["segments"] = normalized
|
||||
return self.client.post("tcm.diagnosis/upsertCallTranscriptSegments", body)
|
||||
|
||||
def finish_call_transcription(
|
||||
self,
|
||||
diagnosis_id: int,
|
||||
call_record_id: int | str,
|
||||
transcription_session_id: str,
|
||||
expected_segment_count: int,
|
||||
*,
|
||||
status: str = "completed",
|
||||
) -> Any:
|
||||
"""Finalize a transcript; repeated requests use the same session identity."""
|
||||
|
||||
body = self._transcription_identity(
|
||||
diagnosis_id, call_record_id, transcription_session_id
|
||||
)
|
||||
clean_status = status.strip().lower()
|
||||
if clean_status not in {"completed", "partial", "failed"}:
|
||||
raise ValueError("transcription status is invalid")
|
||||
if expected_segment_count < 0:
|
||||
raise ValueError("expected_segment_count must not be negative")
|
||||
body.update(
|
||||
{
|
||||
"expected_segment_count": expected_segment_count,
|
||||
"status": clean_status,
|
||||
}
|
||||
)
|
||||
return self.client.post("tcm.diagnosis/finishCallTranscription", body)
|
||||
|
||||
# Compatibility aliases keep UI naming independent from endpoint history.
|
||||
def my_self(self) -> Session:
|
||||
|
||||
@@ -20,6 +20,7 @@ from PySide6.QtWidgets import (
|
||||
QInputDialog,
|
||||
QLabel,
|
||||
QMessageBox,
|
||||
QPlainTextEdit,
|
||||
QPushButton,
|
||||
QScrollArea,
|
||||
QSizePolicy,
|
||||
@@ -1411,7 +1412,7 @@ class DiagnosisDialog(QDialog):
|
||||
layout.setContentsMargins(16, 14, 16, 18)
|
||||
layout.setSpacing(12)
|
||||
toolbar = QHBoxLayout()
|
||||
hint = QLabel("每条通话记录可包含多个回放地址,并可追加指定记录的视频。")
|
||||
hint = QLabel("每条通话记录可包含多个回放地址;录音转写完成后可查看本次面诊对话文字。")
|
||||
hint.setObjectName("DiagnosisDialogGuidance")
|
||||
hint.setWordWrap(True)
|
||||
toolbar.addWidget(hint, 1)
|
||||
@@ -1553,7 +1554,7 @@ class DiagnosisDialog(QDialog):
|
||||
("房间号", 180),
|
||||
("时长", 110),
|
||||
("状态", 90),
|
||||
("录制", 100),
|
||||
("录制 / 文字", 120),
|
||||
("操作", 130),
|
||||
),
|
||||
"assign": (
|
||||
@@ -3365,11 +3366,71 @@ class DiagnosisDialog(QDialog):
|
||||
cell.stop()
|
||||
self._inline_recording_cells.clear()
|
||||
|
||||
@staticmethod
|
||||
def _call_transcript_text(row: Any) -> str:
|
||||
raw = first_value(
|
||||
row,
|
||||
"transcript_text",
|
||||
"call_transcript",
|
||||
"conversation_text",
|
||||
"transcript",
|
||||
default="",
|
||||
)
|
||||
if isinstance(raw, Mapping):
|
||||
raw = first_value(raw, "text", "full_text", "content", default="")
|
||||
text = str(raw or "").strip()
|
||||
if text:
|
||||
return text
|
||||
segments = first_value(row, "transcript_segments", "segments", default=[]) or []
|
||||
if isinstance(segments, Sequence) and not isinstance(
|
||||
segments, (str, bytes, bytearray)
|
||||
):
|
||||
labels = {"doctor": "医生", "patient": "患者", "unknown": "未知说话人"}
|
||||
lines: list[str] = []
|
||||
for segment in segments:
|
||||
content = str(first_value(segment, "text", "sourceText", default="") or "").strip()
|
||||
if not content:
|
||||
continue
|
||||
role = str(
|
||||
first_value(segment, "speaker_role", "speakerRole", default="unknown")
|
||||
or "unknown"
|
||||
)
|
||||
lines.append(f"{labels.get(role, '未知说话人')}:{content}")
|
||||
return "\n".join(lines)
|
||||
return ""
|
||||
|
||||
def _view_call_transcript(self, call_record_id: int, transcript: str) -> None:
|
||||
dialog = QDialog(self)
|
||||
dialog.setObjectName("DiagnosisCallTranscriptDialog")
|
||||
dialog.setWindowTitle(f"面诊对话文字 · 通话记录 #{call_record_id}")
|
||||
dialog.resize(680, 520)
|
||||
layout = QVBoxLayout(dialog)
|
||||
layout.setContentsMargins(20, 18, 20, 18)
|
||||
layout.setSpacing(12)
|
||||
title = QLabel("录音转写对话")
|
||||
title.setObjectName("DiagnosisDailySectionTitle")
|
||||
layout.addWidget(title)
|
||||
hint = QLabel("以下内容由语音自动转写,仅作为面诊记录辅助,请由医生核对。")
|
||||
hint.setObjectName("DiagnosisDialogGuidance")
|
||||
hint.setWordWrap(True)
|
||||
layout.addWidget(hint)
|
||||
content = QPlainTextEdit(dialog)
|
||||
content.setObjectName("DiagnosisCallTranscriptText")
|
||||
content.setReadOnly(True)
|
||||
content.setPlainText(transcript)
|
||||
layout.addWidget(content, 1)
|
||||
close_button = QPushButton("关闭")
|
||||
close_button.setProperty("variant", "primary")
|
||||
close_button.clicked.connect(dialog.accept)
|
||||
layout.addWidget(close_button, 0, Qt.AlignmentFlag.AlignRight)
|
||||
dialog.exec()
|
||||
|
||||
def _fill_video(self, rows: Sequence[Any]) -> None:
|
||||
self._stop_inline_recordings()
|
||||
matrix: list[tuple[Any, ...]] = []
|
||||
row_urls: list[list[str]] = []
|
||||
record_ids: list[int] = []
|
||||
transcripts: list[str] = []
|
||||
for row in rows:
|
||||
raw_urls = first_value(row, "recording_urls_list", "recording_urls", default=[]) or []
|
||||
if isinstance(raw_urls, str) or not isinstance(raw_urls, Sequence):
|
||||
@@ -3382,6 +3443,8 @@ class DiagnosisDialog(QDialog):
|
||||
normalized.append(url)
|
||||
row_urls.append(normalized)
|
||||
record_ids.append(_int(first_value(row, "id", "call_record_id"), 0))
|
||||
transcript = self._call_transcript_text(row)
|
||||
transcripts.append(transcript)
|
||||
call_type = _raw_value(row, "call_type_text")
|
||||
if call_type is _MISSING:
|
||||
raw_call_type = _raw_value(row, "call_type")
|
||||
@@ -3398,6 +3461,15 @@ class DiagnosisDialog(QDialog):
|
||||
3: "未接听",
|
||||
4: "已取消",
|
||||
}.get(_int(raw_status, -1), "—")
|
||||
recording_status = first_value(
|
||||
row, "recording_status_text", "record_status_text", "record_status"
|
||||
)
|
||||
transcript_status = first_value(
|
||||
row,
|
||||
"transcription_status_text",
|
||||
"transcript_status_text",
|
||||
default="文字已生成" if transcript else "未生成文字",
|
||||
)
|
||||
matrix.append(
|
||||
(
|
||||
"" if normalized else "暂无录制回放",
|
||||
@@ -3407,9 +3479,7 @@ class DiagnosisDialog(QDialog):
|
||||
first_value(row, "room_id", "room_no"),
|
||||
first_value(row, "duration_text", "duration"),
|
||||
status,
|
||||
first_value(
|
||||
row, "recording_status_text", "record_status_text", "record_status"
|
||||
),
|
||||
f"{display_text(recording_status)}\n{display_text(transcript_status)}",
|
||||
"",
|
||||
)
|
||||
)
|
||||
@@ -3434,6 +3504,20 @@ class DiagnosisDialog(QDialog):
|
||||
if item is not None:
|
||||
item.setText("")
|
||||
call_record_id = record_ids[row_index]
|
||||
actions: list[QPushButton] = []
|
||||
transcript = transcripts[row_index]
|
||||
if transcript and call_record_id > 0:
|
||||
view_transcript = self._action_button(
|
||||
"查看文字", f"查看通话记录 #{call_record_id} 的录音转写"
|
||||
)
|
||||
view_transcript.setObjectName("DiagnosisVideoTranscriptView")
|
||||
view_transcript.setProperty("callRecordId", call_record_id)
|
||||
view_transcript.clicked.connect(
|
||||
lambda _checked=False, selected=call_record_id, text=transcript: self._view_call_transcript(
|
||||
selected, text
|
||||
)
|
||||
)
|
||||
actions.append(view_transcript)
|
||||
if self._editable and self._can_video_upload and call_record_id > 0:
|
||||
upload = self._action_button(
|
||||
"追加回放", f"上传并绑定通话记录 #{call_record_id}"
|
||||
@@ -3452,12 +3536,16 @@ class DiagnosisDialog(QDialog):
|
||||
selected
|
||||
)
|
||||
)
|
||||
upload_host = QWidget()
|
||||
upload_host.setObjectName("DiagnosisVideoRowUploadCell")
|
||||
upload_layout = QVBoxLayout(upload_host)
|
||||
upload_layout.setContentsMargins(0, 0, 0, 0)
|
||||
upload_layout.addWidget(upload, 0, Qt.AlignmentFlag.AlignCenter)
|
||||
table.setCellWidget(row_index, 8, upload_host)
|
||||
actions.append(upload)
|
||||
if actions:
|
||||
action_host = QWidget()
|
||||
action_host.setObjectName("DiagnosisVideoRowUploadCell")
|
||||
action_layout = QVBoxLayout(action_host)
|
||||
action_layout.setContentsMargins(0, 0, 0, 0)
|
||||
action_layout.setSpacing(2)
|
||||
for action in actions:
|
||||
action_layout.addWidget(action, 0, Qt.AlignmentFlag.AlignCenter)
|
||||
table.setCellWidget(row_index, 8, action_host)
|
||||
upload_item = table.item(row_index, 8)
|
||||
if upload_item is not None:
|
||||
upload_item.setText("")
|
||||
|
||||
@@ -130,6 +130,7 @@ def _ticket_mapping(ticket: Any) -> Mapping[str, Any]:
|
||||
"patientUserId": "patient_user_id",
|
||||
"diagnosisId": "diagnosis_id",
|
||||
"patientId": "patient_id",
|
||||
"callRecordId": "call_record_id",
|
||||
}
|
||||
adapted = {
|
||||
json_name: getattr(ticket, attribute_name)
|
||||
@@ -196,6 +197,7 @@ class VideoCallRequest:
|
||||
target_user_id: str
|
||||
diagnosis_id: Identifier
|
||||
patient_id: Identifier | None = None
|
||||
call_record_id: Identifier | None = None
|
||||
backend_mode: BackendMode = BackendMode.EMBEDDED
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
@@ -218,6 +220,12 @@ class VideoCallRequest:
|
||||
"patient_id",
|
||||
_identifier(self.patient_id, "patientId"),
|
||||
)
|
||||
if self.call_record_id is not None:
|
||||
object.__setattr__(
|
||||
self,
|
||||
"call_record_id",
|
||||
_identifier(self.call_record_id, "callRecordId"),
|
||||
)
|
||||
object.__setattr__(self, "backend_mode", BackendMode.parse(self.backend_mode))
|
||||
|
||||
@classmethod
|
||||
@@ -257,6 +265,7 @@ class VideoCallRequest:
|
||||
return {
|
||||
"diagnosis_id": self.diagnosis_id,
|
||||
"patient_id": self.patient_id,
|
||||
"call_record_id": self.call_record_id,
|
||||
"backend_mode": self.backend_mode.value,
|
||||
}
|
||||
|
||||
@@ -285,6 +294,13 @@ def normalize_backend_ticket(
|
||||
_identifier,
|
||||
required=False,
|
||||
)
|
||||
payload_call_record = _read_aliases(
|
||||
payload,
|
||||
("callRecordId", "call_record_id"),
|
||||
"callRecordId",
|
||||
_identifier,
|
||||
required=False,
|
||||
)
|
||||
|
||||
normalized_diagnosis = _merge_identifier(
|
||||
payload_diagnosis,
|
||||
@@ -323,6 +339,7 @@ def normalize_backend_ticket(
|
||||
),
|
||||
diagnosis_id=normalized_diagnosis,
|
||||
patient_id=normalized_patient,
|
||||
call_record_id=payload_call_record,
|
||||
backend_mode=BackendMode.parse(backend_mode),
|
||||
)
|
||||
|
||||
|
||||
@@ -74,6 +74,71 @@ def _call_repository_method(method: Callable[..., Any], payload: Mapping[str, An
|
||||
return _resolve_result(result)
|
||||
|
||||
|
||||
def _mapping_candidate(value: Any) -> Mapping[str, Any] | None:
|
||||
if isinstance(value, Mapping):
|
||||
return value
|
||||
for attribute in ("raw", "data"):
|
||||
candidate = getattr(value, attribute, None)
|
||||
if isinstance(candidate, Mapping):
|
||||
return candidate
|
||||
return None
|
||||
|
||||
|
||||
def _extract_call_record_id(result: Any) -> int | str | None:
|
||||
"""Read a positive call-record identity from common backend envelopes."""
|
||||
|
||||
pending = [result]
|
||||
visited: set[int] = set()
|
||||
while pending:
|
||||
candidate = pending.pop(0)
|
||||
mapping = _mapping_candidate(candidate)
|
||||
if mapping is None or id(mapping) in visited:
|
||||
continue
|
||||
visited.add(id(mapping))
|
||||
for key in ("call_record_id", "callRecordId", "id"):
|
||||
value = mapping.get(key)
|
||||
if isinstance(value, bool) or value is None:
|
||||
continue
|
||||
if isinstance(value, int) and value > 0:
|
||||
return value
|
||||
if isinstance(value, str) and value.strip():
|
||||
return value.strip()
|
||||
for key in ("data", "result", "record", "call_record", "callRecord"):
|
||||
nested = mapping.get(key)
|
||||
if isinstance(nested, Mapping):
|
||||
pending.append(nested)
|
||||
return None
|
||||
|
||||
|
||||
def _clean_transcript_segment(segment: Mapping[str, Any], session_id: str) -> dict[str, Any]:
|
||||
segment_id = str(segment.get("segment_id", segment.get("segmentId", ""))).strip()
|
||||
text = str(segment.get("text", segment.get("sourceText", ""))).strip()
|
||||
speaker_user_id = str(
|
||||
segment.get("speaker_user_id", segment.get("speakerUserId", ""))
|
||||
).strip()
|
||||
speaker_role = str(segment.get("speaker_role", segment.get("speakerRole", "unknown"))).strip()
|
||||
if not segment_id or len(segment_id) > 160:
|
||||
raise ValueError("transcript segment_id must contain 1 to 160 characters")
|
||||
if not text or len(text) > 4_000:
|
||||
raise ValueError("transcript text must contain 1 to 4000 characters")
|
||||
if len(speaker_user_id) > 160:
|
||||
raise ValueError("transcript speaker_user_id is too long")
|
||||
if speaker_role not in {"doctor", "patient", "unknown"}:
|
||||
speaker_role = "unknown"
|
||||
try:
|
||||
timestamp = int(segment.get("timestamp") or 0)
|
||||
except (TypeError, ValueError):
|
||||
timestamp = 0
|
||||
return {
|
||||
"segment_id": segment_id,
|
||||
"transcription_session_id": session_id,
|
||||
"speaker_user_id": speaker_user_id,
|
||||
"speaker_role": speaker_role,
|
||||
"timestamp": max(timestamp, 0),
|
||||
"text": text,
|
||||
}
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class _WorkItem:
|
||||
operation: str
|
||||
@@ -169,11 +234,17 @@ class OrderedCallLifecycle:
|
||||
self.logger = logger
|
||||
self.started = False
|
||||
self.ended = False
|
||||
self.call_record_id: int | str | None = request.call_record_id
|
||||
self.bound_room_id: str | None = None
|
||||
self._claimed_room_id: str | None = None
|
||||
self._start_future: Future[bool] | None = None
|
||||
self._bind_future: Future[bool] | None = None
|
||||
self._end_future: Future[bool] | None = None
|
||||
self._transcription_start_future: Future[bool] | None = None
|
||||
self._transcription_finish_future: Future[bool] | None = None
|
||||
self._transcription_session_id: str | None = None
|
||||
self._transcription_active = False
|
||||
self._segment_futures: dict[str, Future[bool]] = {}
|
||||
self._lock = threading.RLock()
|
||||
self._worker = _OrderedDaemonWorker(logger, request.safe_log_context())
|
||||
|
||||
@@ -181,6 +252,10 @@ class OrderedCallLifecycle:
|
||||
def worker_is_daemon(self) -> bool:
|
||||
return self._worker.is_daemon
|
||||
|
||||
@property
|
||||
def transcription_session_id(self) -> str | None:
|
||||
return self._transcription_session_id
|
||||
|
||||
def start(self) -> Future[bool]:
|
||||
with self._lock:
|
||||
if self._start_future is not None:
|
||||
@@ -196,9 +271,13 @@ class OrderedCallLifecycle:
|
||||
payload["patient_id"] = self.request.patient_id
|
||||
|
||||
def operation() -> bool:
|
||||
_call_repository_method(method, payload)
|
||||
result = _call_repository_method(method, payload)
|
||||
record_id = _extract_call_record_id(result)
|
||||
if record_id is None:
|
||||
raise ValueError("startCall response did not include the current call_record_id")
|
||||
with self._lock:
|
||||
self.started = True
|
||||
self.call_record_id = record_id
|
||||
self.logger.info(
|
||||
"video call record started",
|
||||
extra={"video_call": self.request.safe_log_context()},
|
||||
@@ -309,10 +388,167 @@ class OrderedCallLifecycle:
|
||||
|
||||
return self._worker.submit("screenshot", operation)
|
||||
|
||||
def start_transcription(self, session_id: str, *, language: str = "zh-CN") -> Future[bool]:
|
||||
"""Start persisted realtime transcription for this exact call record."""
|
||||
|
||||
clean_session = str(session_id or "").strip()
|
||||
clean_language = str(language or "zh-CN").strip()[:32] or "zh-CN"
|
||||
if not clean_session or len(clean_session) > 128:
|
||||
raise ValueError("transcription session id must contain 1 to 128 characters")
|
||||
with self._lock:
|
||||
if self._end_future is not None:
|
||||
raise RuntimeError("video call has already ended")
|
||||
if self._transcription_start_future is not None:
|
||||
if self._transcription_session_id != clean_session:
|
||||
raise RuntimeError("another transcription session already exists")
|
||||
return self._transcription_start_future
|
||||
if self._start_future is None:
|
||||
self.start()
|
||||
method = getattr(self.repository, "start_call_transcription", None)
|
||||
if not callable(method):
|
||||
raise ValueError("video repository does not implement call transcription storage")
|
||||
self._transcription_session_id = clean_session
|
||||
|
||||
def operation() -> bool:
|
||||
with self._lock:
|
||||
started = self.started
|
||||
record_id = self.call_record_id
|
||||
if not started:
|
||||
return False
|
||||
if record_id is None:
|
||||
raise ValueError("server did not return the current call_record_id")
|
||||
_call_repository_method(
|
||||
method,
|
||||
{
|
||||
"diagnosis_id": self.request.diagnosis_id,
|
||||
"call_record_id": record_id,
|
||||
"transcription_session_id": clean_session,
|
||||
"language": clean_language,
|
||||
},
|
||||
)
|
||||
with self._lock:
|
||||
self._transcription_active = True
|
||||
self.logger.info(
|
||||
"video call transcription started",
|
||||
extra={"video_call": self.request.safe_log_context()},
|
||||
)
|
||||
return True
|
||||
|
||||
self._transcription_start_future = self._worker.submit(
|
||||
"transcription-start", operation
|
||||
)
|
||||
return self._transcription_start_future
|
||||
|
||||
def save_transcript_segment(self, segment: Mapping[str, Any]) -> Future[bool]:
|
||||
"""Upsert one completed, bounded transcript segment without logging its text."""
|
||||
|
||||
with self._lock:
|
||||
session_id = self._transcription_session_id
|
||||
if not session_id or self._transcription_start_future is None:
|
||||
raise RuntimeError("call transcription has not started")
|
||||
if self._transcription_finish_future is not None:
|
||||
raise RuntimeError("call transcription has already stopped")
|
||||
cleaned = _clean_transcript_segment(segment, session_id)
|
||||
segment_id = cleaned["segment_id"]
|
||||
existing = self._segment_futures.get(segment_id)
|
||||
if existing is not None:
|
||||
return existing
|
||||
method = getattr(self.repository, "upsert_call_transcript_segments", None)
|
||||
if not callable(method):
|
||||
raise ValueError("video repository does not implement transcript segment storage")
|
||||
|
||||
def operation() -> bool:
|
||||
with self._lock:
|
||||
active = self._transcription_active
|
||||
record_id = self.call_record_id
|
||||
if not active:
|
||||
return False
|
||||
if record_id is None:
|
||||
raise ValueError("server did not return the current call_record_id")
|
||||
_call_repository_method(
|
||||
method,
|
||||
{
|
||||
"diagnosis_id": self.request.diagnosis_id,
|
||||
"call_record_id": record_id,
|
||||
"transcription_session_id": session_id,
|
||||
"segments": [cleaned],
|
||||
},
|
||||
)
|
||||
return True
|
||||
|
||||
future = self._worker.submit("transcription-segment", operation)
|
||||
self._segment_futures[segment_id] = future
|
||||
|
||||
def release_failed(completed: Future[bool]) -> None:
|
||||
if completed.cancelled() or completed.exception() is not None:
|
||||
with self._lock:
|
||||
if self._segment_futures.get(segment_id) is completed:
|
||||
self._segment_futures.pop(segment_id, None)
|
||||
|
||||
future.add_done_callback(release_failed)
|
||||
return future
|
||||
|
||||
def finish_transcription(self, *, status: str = "completed") -> Future[bool]:
|
||||
"""Flush and finalize the current transcript exactly once."""
|
||||
|
||||
clean_status = str(status or "completed").strip().lower()
|
||||
if clean_status not in {"completed", "partial", "failed"}:
|
||||
raise ValueError("transcription status is invalid")
|
||||
with self._lock:
|
||||
if self._transcription_finish_future is not None:
|
||||
return self._transcription_finish_future
|
||||
if self._transcription_start_future is None or not self._transcription_session_id:
|
||||
return _settled_future(False)
|
||||
method = getattr(self.repository, "finish_call_transcription", None)
|
||||
if not callable(method):
|
||||
raise ValueError("video repository does not implement transcription finalization")
|
||||
session_id = self._transcription_session_id
|
||||
|
||||
def operation() -> bool:
|
||||
with self._lock:
|
||||
active = self._transcription_active
|
||||
record_id = self.call_record_id
|
||||
expected_count = len(self._segment_futures)
|
||||
if not active:
|
||||
return False
|
||||
if record_id is None:
|
||||
raise ValueError("server did not return the current call_record_id")
|
||||
_call_repository_method(
|
||||
method,
|
||||
{
|
||||
"diagnosis_id": self.request.diagnosis_id,
|
||||
"call_record_id": record_id,
|
||||
"transcription_session_id": session_id,
|
||||
"expected_segment_count": expected_count,
|
||||
"status": clean_status,
|
||||
},
|
||||
)
|
||||
with self._lock:
|
||||
self._transcription_active = False
|
||||
self.logger.info(
|
||||
"video call transcription finalized",
|
||||
extra={
|
||||
"video_call": self.request.safe_log_context(),
|
||||
"transcription_status": clean_status,
|
||||
"segment_count": expected_count,
|
||||
},
|
||||
)
|
||||
return True
|
||||
|
||||
self._transcription_finish_future = self._worker.submit(
|
||||
"transcription-finish", operation
|
||||
)
|
||||
return self._transcription_finish_future
|
||||
|
||||
def end(self, reason: str) -> Future[bool]:
|
||||
with self._lock:
|
||||
if self._end_future is not None:
|
||||
return self._end_future
|
||||
if (
|
||||
self._transcription_start_future is not None
|
||||
and self._transcription_finish_future is None
|
||||
):
|
||||
self.finish_transcription(status="partial")
|
||||
method = getattr(self.repository, "end_call", None)
|
||||
|
||||
def operation() -> bool:
|
||||
|
||||
@@ -210,6 +210,7 @@ if WEBENGINE_AVAILABLE: # pragma: no cover - GUI behavior needs an integration
|
||||
call_error = Signal(str) # type: ignore[misc]
|
||||
_start_completed = Signal(bool) # type: ignore[misc]
|
||||
_screenshot_completed = Signal(bool, str) # type: ignore[misc]
|
||||
_transcription_completed = Signal(str, str, str, bool, str) # type: ignore[misc]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -284,6 +285,7 @@ if WEBENGINE_AVAILABLE: # pragma: no cover - GUI behavior needs an integration
|
||||
|
||||
self._start_completed.connect(self._on_lifecycle_started)
|
||||
self._screenshot_completed.connect(self._on_screenshot_completed)
|
||||
self._transcription_completed.connect(self._on_transcription_completed)
|
||||
self.web_view.loadFinished.connect(self._on_load_finished)
|
||||
self.web_view.setUrl(QUrl(self.location.url))
|
||||
|
||||
@@ -426,6 +428,21 @@ if WEBENGINE_AVAILABLE: # pragma: no cover - GUI behavior needs an integration
|
||||
str(message.get("message") or "截屏图片无效。")[:200],
|
||||
)
|
||||
return
|
||||
if event == "transcription-start-request":
|
||||
self._start_transcription(
|
||||
str(message.get("sessionId") or ""),
|
||||
str(message.get("language") or "zh-CN"),
|
||||
)
|
||||
return
|
||||
if event == "transcription-segment":
|
||||
self._save_transcript_segment(message)
|
||||
return
|
||||
if event == "transcription-stop":
|
||||
self._finish_transcription(
|
||||
str(message.get("sessionId") or ""),
|
||||
str(message.get("status") or "completed"),
|
||||
)
|
||||
return
|
||||
room_id = message.get("roomId", message.get("room_id"))
|
||||
if room_id not in (None, ""):
|
||||
self.lifecycle.bind_room(room_id)
|
||||
@@ -434,8 +451,6 @@ if WEBENGINE_AVAILABLE: # pragma: no cover - GUI behavior needs an integration
|
||||
if event == "status":
|
||||
status = str(message.get("status", "unknown"))[:80]
|
||||
self.status_changed.emit(status)
|
||||
if status == "idle" and not self.open_im:
|
||||
self._close_from_companion("remote-idle")
|
||||
elif event == "hangup":
|
||||
status = str(message.get("status", "ended"))[:80]
|
||||
self.call_ended.emit(status)
|
||||
@@ -509,6 +524,109 @@ if WEBENGINE_AVAILABLE: # pragma: no cover - GUI behavior needs an integration
|
||||
f"window.doctorConsultation?.screenshotResult?.({state}, {payload});"
|
||||
)
|
||||
|
||||
def _notify_transcription_completed(
|
||||
self,
|
||||
operation: str,
|
||||
session_id: str,
|
||||
segment_id: str,
|
||||
future: Future[bool],
|
||||
) -> None:
|
||||
try:
|
||||
succeeded = bool(future.result())
|
||||
except Exception as error:
|
||||
succeeded = False
|
||||
message = str(error)[:200] or "录音文字保存失败。"
|
||||
else:
|
||||
message = {
|
||||
"start": "录音文字存储已准备。",
|
||||
"segment": "",
|
||||
"stop": "本次面诊对话文字已保存。",
|
||||
}.get(operation, "")
|
||||
with suppress(RuntimeError):
|
||||
self._transcription_completed.emit(
|
||||
operation, session_id, segment_id, succeeded, message
|
||||
)
|
||||
|
||||
def _on_transcription_completed(
|
||||
self,
|
||||
operation: str,
|
||||
session_id: str,
|
||||
segment_id: str,
|
||||
succeeded: bool,
|
||||
message: str,
|
||||
) -> None:
|
||||
payload = json.dumps(str(message)[:200], ensure_ascii=True)
|
||||
state = "true" if succeeded else "false"
|
||||
self._page.runJavaScript(
|
||||
"window.doctorConsultation?.transcriptionResult?.("
|
||||
f"{json.dumps(operation)}, {json.dumps(session_id)}, "
|
||||
f"{json.dumps(segment_id)}, {state}, {payload});"
|
||||
)
|
||||
|
||||
def _start_transcription(self, session_id: str, language: str) -> None:
|
||||
try:
|
||||
future = self.lifecycle.start_transcription(session_id, language=language)
|
||||
except Exception as error:
|
||||
self._on_transcription_completed(
|
||||
"start", session_id, "", False, str(error)[:200]
|
||||
)
|
||||
return
|
||||
future.add_done_callback(
|
||||
lambda completed: self._notify_transcription_completed(
|
||||
"start", session_id, "", completed
|
||||
)
|
||||
)
|
||||
|
||||
def _save_transcript_segment(self, message: Mapping[str, Any]) -> None:
|
||||
session_id = str(message.get("sessionId") or "").strip()
|
||||
segment = message.get("segment")
|
||||
segment_id = (
|
||||
str(segment.get("segment_id") or segment.get("segmentId") or "").strip()
|
||||
if isinstance(segment, Mapping)
|
||||
else ""
|
||||
)
|
||||
if not isinstance(segment, Mapping):
|
||||
self._on_transcription_completed(
|
||||
"segment", session_id, segment_id, False, "录音文字片段无效。"
|
||||
)
|
||||
return
|
||||
if session_id != str(self.lifecycle.transcription_session_id or ""):
|
||||
self._on_transcription_completed(
|
||||
"segment", session_id, segment_id, False, "录音会话标识不匹配。"
|
||||
)
|
||||
return
|
||||
try:
|
||||
future = self.lifecycle.save_transcript_segment(segment)
|
||||
except Exception as error:
|
||||
self._on_transcription_completed(
|
||||
"segment", session_id, segment_id, False, str(error)[:200]
|
||||
)
|
||||
return
|
||||
future.add_done_callback(
|
||||
lambda completed: self._notify_transcription_completed(
|
||||
"segment", session_id, segment_id, completed
|
||||
)
|
||||
)
|
||||
|
||||
def _finish_transcription(self, session_id: str, status: str) -> None:
|
||||
if session_id.strip() != str(self.lifecycle.transcription_session_id or ""):
|
||||
self._on_transcription_completed(
|
||||
"stop", session_id, "", False, "录音会话标识不匹配。"
|
||||
)
|
||||
return
|
||||
try:
|
||||
future = self.lifecycle.finish_transcription(status=status)
|
||||
except Exception as error:
|
||||
self._on_transcription_completed(
|
||||
"stop", session_id, "", False, str(error)[:200]
|
||||
)
|
||||
return
|
||||
future.add_done_callback(
|
||||
lambda completed: self._notify_transcription_completed(
|
||||
"stop", session_id, "", completed
|
||||
)
|
||||
)
|
||||
|
||||
def _close_from_companion(self, reason: str) -> None:
|
||||
self._companion_ended = True
|
||||
self._close_reason = reason
|
||||
|
||||
@@ -232,6 +232,69 @@ def test_demo_call_lifecycle_mutates_record(repository: DemoDoctorRepository) ->
|
||||
assert ended["room_id"] == "room-501"
|
||||
|
||||
|
||||
def test_demo_transcript_upsert_and_finish_round_trip_in_call_records(
|
||||
repository: DemoDoctorRepository,
|
||||
) -> None:
|
||||
"""Demo replay reads expose one finalized segment for a repeated segment ID."""
|
||||
|
||||
started = repository.start_call(501, 301)
|
||||
call_record_id = started["id"]
|
||||
repository.start_call_transcription(501, call_record_id, "session-1")
|
||||
repository.upsert_call_transcript_segments(
|
||||
501,
|
||||
call_record_id,
|
||||
"session-1",
|
||||
[
|
||||
{
|
||||
"segment_id": "seg-1",
|
||||
"speaker_user_id": "patient_301",
|
||||
"speaker_role": "patient",
|
||||
"timestamp": 1200,
|
||||
"text": "draft words",
|
||||
}
|
||||
],
|
||||
)
|
||||
repository.upsert_call_transcript_segments(
|
||||
501,
|
||||
call_record_id,
|
||||
"session-1",
|
||||
[
|
||||
{
|
||||
"segment_id": "seg-1",
|
||||
"speaker_user_id": "patient_301",
|
||||
"speaker_role": "patient",
|
||||
"timestamp": 1200,
|
||||
"text": "final words",
|
||||
}
|
||||
],
|
||||
)
|
||||
repository.finish_call_transcription(
|
||||
501,
|
||||
call_record_id,
|
||||
"session-1",
|
||||
expected_segment_count=1,
|
||||
status="completed",
|
||||
)
|
||||
repository.end_call(501)
|
||||
|
||||
record = next(
|
||||
row for row in repository.list_call_records(501) if row["id"] == call_record_id
|
||||
)
|
||||
assert record["status"] == 2
|
||||
assert record["transcription_status"] == "completed"
|
||||
assert record["transcription_segment_count"] == 1
|
||||
assert record["transcript_segments"] == [
|
||||
{
|
||||
"segment_id": "seg-1",
|
||||
"speaker_role": "patient",
|
||||
"speaker_user_id": "patient_301",
|
||||
"timestamp": 1200,
|
||||
"text": "final words",
|
||||
}
|
||||
]
|
||||
assert "final words" in record["transcript_text"]
|
||||
|
||||
|
||||
def test_tolerant_page_parsing_accepts_aliases_and_bad_rows() -> None:
|
||||
"""List parsing handles nullable fields, aliases and non-object rows safely."""
|
||||
|
||||
@@ -315,6 +378,8 @@ class _StubApiClient:
|
||||
"userSig": "short-lived",
|
||||
"patientUserId": "patient_2",
|
||||
}
|
||||
if endpoint == "tcm.diagnosis/startCall":
|
||||
return {"call_record_id": 901}
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from doctor_workstation.core.errors import ApiProtocolError
|
||||
from doctor_workstation.core.models import Appointment, Consultation, PageResult, Prescription
|
||||
from doctor_workstation.services.mock_repository import DemoDoctorRepository
|
||||
from doctor_workstation.services.repository import (
|
||||
@@ -76,6 +77,8 @@ class RecordingClient:
|
||||
self.post_calls.append((endpoint, body))
|
||||
if endpoint in {"tcm.prescription/add", "tcm.prescriptionOrder/create"}:
|
||||
return {"id": 88}
|
||||
if endpoint == "tcm.diagnosis/startCall":
|
||||
return {"call_record_id": 901}
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@@ -253,6 +256,106 @@ def test_remote_new_contracts_use_exact_admin_endpoints_and_dtos() -> None:
|
||||
} <= get_endpoints
|
||||
|
||||
|
||||
def test_remote_transcription_endpoints_use_exact_normalized_dtos() -> None:
|
||||
"""Realtime transcript persistence stays within the three audited POST DTOs."""
|
||||
|
||||
client = RecordingClient()
|
||||
repository = RemoteDoctorRepository(client) # type: ignore[arg-type]
|
||||
|
||||
repository.start_call_transcription(501, 901, " session-1 ", language=" zh-CN ")
|
||||
repository.upsert_call_transcript_segments(
|
||||
501,
|
||||
901,
|
||||
"session-1",
|
||||
[
|
||||
{
|
||||
"segment_id": "seg-1",
|
||||
"speaker_user_id": "patient_301",
|
||||
"speaker_role": "patient",
|
||||
"timestamp": "1200",
|
||||
"text": " patient words ",
|
||||
}
|
||||
],
|
||||
)
|
||||
repository.finish_call_transcription(
|
||||
501,
|
||||
901,
|
||||
"session-1",
|
||||
expected_segment_count=1,
|
||||
status="completed",
|
||||
)
|
||||
|
||||
assert client.post_calls == [
|
||||
(
|
||||
"tcm.diagnosis/startCallTranscription",
|
||||
{
|
||||
"diagnosis_id": 501,
|
||||
"call_record_id": 901,
|
||||
"transcription_session_id": "session-1",
|
||||
"language": "zh-CN",
|
||||
},
|
||||
),
|
||||
(
|
||||
"tcm.diagnosis/upsertCallTranscriptSegments",
|
||||
{
|
||||
"diagnosis_id": 501,
|
||||
"call_record_id": 901,
|
||||
"transcription_session_id": "session-1",
|
||||
"segments": [
|
||||
{
|
||||
"segment_id": "seg-1",
|
||||
"speaker_user_id": "patient_301",
|
||||
"speaker_role": "patient",
|
||||
"timestamp": 1200,
|
||||
"text": "patient words",
|
||||
}
|
||||
],
|
||||
},
|
||||
),
|
||||
(
|
||||
"tcm.diagnosis/finishCallTranscription",
|
||||
{
|
||||
"diagnosis_id": 501,
|
||||
"call_record_id": 901,
|
||||
"transcription_session_id": "session-1",
|
||||
"expected_segment_count": 1,
|
||||
"status": "completed",
|
||||
},
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def test_remote_start_call_requires_and_normalizes_current_record_id() -> None:
|
||||
client = RecordingClient()
|
||||
repository = RemoteDoctorRepository(client) # type: ignore[arg-type]
|
||||
|
||||
assert repository.start_call(501, 301) == {"call_record_id": 901}
|
||||
assert client.post_calls == [
|
||||
(
|
||||
"tcm.diagnosis/startCall",
|
||||
{"diagnosis_id": 501, "patient_id": 301, "call_type": 2},
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"response",
|
||||
[None, {}, {"ok": True}, {"call_record_id": 0}, {"callRecordId": -1}, {"id": True}],
|
||||
)
|
||||
def test_remote_start_call_rejects_missing_or_invalid_record_id(response: Any) -> None:
|
||||
class StartCallClient(RecordingClient):
|
||||
def post(self, endpoint: str, payload: dict[str, Any] | None = None) -> Any:
|
||||
if endpoint == "tcm.diagnosis/startCall":
|
||||
self.post_calls.append((endpoint, dict(payload or {})))
|
||||
return response
|
||||
return super().post(endpoint, payload)
|
||||
|
||||
repository = RemoteDoctorRepository(StartCallClient()) # type: ignore[arg-type]
|
||||
|
||||
with pytest.raises(ApiProtocolError, match="call_record"):
|
||||
repository.start_call(501, 301)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"unsafe_reference",
|
||||
[r"C:\records\tongue.jpg", r"\\server\share\report.pdf", "file:///tmp/a.jpg"],
|
||||
|
||||
@@ -188,10 +188,13 @@ def test_call_lifecycle_is_fifo_daemon_and_never_blocks_caller() -> None:
|
||||
release_start = threading.Event()
|
||||
|
||||
class Repository:
|
||||
def start_call(self, diagnosis_id: int, patient_id: int, *, call_type: int) -> None:
|
||||
def start_call(
|
||||
self, diagnosis_id: int, patient_id: int, *, call_type: int
|
||||
) -> dict[str, int]:
|
||||
start_entered.set()
|
||||
assert release_start.wait(2)
|
||||
events.append(("start", diagnosis_id, patient_id, call_type))
|
||||
return {"call_record_id": 900}
|
||||
|
||||
def bind_call_room(self, diagnosis_id: int, room_id: str) -> None:
|
||||
events.append(("bind", diagnosis_id, room_id))
|
||||
@@ -237,6 +240,208 @@ def test_call_lifecycle_is_fifo_daemon_and_never_blocks_caller() -> None:
|
||||
]
|
||||
|
||||
|
||||
def test_transcription_lifecycle_uses_start_record_id_and_remains_fifo() -> None:
|
||||
events: list[tuple[object, ...]] = []
|
||||
|
||||
class Repository:
|
||||
def start_call(
|
||||
self, diagnosis_id: int, patient_id: int, *, call_type: int
|
||||
) -> dict[str, object]:
|
||||
events.append(("start", diagnosis_id, patient_id, call_type))
|
||||
return {"data": {"callRecordId": 901}}
|
||||
|
||||
def start_call_transcription(
|
||||
self,
|
||||
diagnosis_id: int,
|
||||
call_record_id: int,
|
||||
transcription_session_id: str,
|
||||
*,
|
||||
language: str,
|
||||
) -> None:
|
||||
events.append(
|
||||
(
|
||||
"transcription-start",
|
||||
diagnosis_id,
|
||||
call_record_id,
|
||||
transcription_session_id,
|
||||
language,
|
||||
)
|
||||
)
|
||||
|
||||
def upsert_call_transcript_segments(
|
||||
self,
|
||||
diagnosis_id: int,
|
||||
call_record_id: int,
|
||||
transcription_session_id: str,
|
||||
segments: list[dict[str, object]],
|
||||
) -> None:
|
||||
events.append(
|
||||
(
|
||||
"segment",
|
||||
diagnosis_id,
|
||||
call_record_id,
|
||||
transcription_session_id,
|
||||
segments,
|
||||
)
|
||||
)
|
||||
|
||||
def finish_call_transcription(
|
||||
self,
|
||||
diagnosis_id: int,
|
||||
call_record_id: int,
|
||||
transcription_session_id: str,
|
||||
expected_segment_count: int,
|
||||
*,
|
||||
status: str,
|
||||
) -> None:
|
||||
events.append(
|
||||
(
|
||||
"finish",
|
||||
diagnosis_id,
|
||||
call_record_id,
|
||||
transcription_session_id,
|
||||
expected_segment_count,
|
||||
status,
|
||||
)
|
||||
)
|
||||
|
||||
def end_call(self, diagnosis_id: int) -> None:
|
||||
events.append(("end", diagnosis_id))
|
||||
|
||||
request = VideoCallRequest(
|
||||
sdk_app_id=1400123456,
|
||||
user_id="doctor_42",
|
||||
user_sig="short-lived-ticket",
|
||||
target_user_id="patient_8",
|
||||
diagnosis_id=123,
|
||||
patient_id=8,
|
||||
)
|
||||
lifecycle = OrderedCallLifecycle(request, Repository(), logging.getLogger(__name__))
|
||||
|
||||
start = lifecycle.start()
|
||||
transcription_start = lifecycle.start_transcription("session-1")
|
||||
segment = lifecycle.save_transcript_segment(
|
||||
{
|
||||
"segment_id": "seg-1",
|
||||
"speaker_user_id": "patient_8",
|
||||
"speaker_role": "patient",
|
||||
"timestamp": 1200,
|
||||
"text": "patient words",
|
||||
}
|
||||
)
|
||||
duplicate = lifecycle.save_transcript_segment(
|
||||
{"segment_id": "seg-1", "text": "must not produce another write"}
|
||||
)
|
||||
finish = lifecycle.finish_transcription(status="completed")
|
||||
end = lifecycle.end("test")
|
||||
|
||||
assert duplicate is segment
|
||||
assert start.result(timeout=2) is True
|
||||
assert transcription_start.result(timeout=2) is True
|
||||
assert segment.result(timeout=2) is True
|
||||
assert finish.result(timeout=2) is True
|
||||
assert end.result(timeout=2) is True
|
||||
assert lifecycle.wait(1) is True
|
||||
assert lifecycle.call_record_id == 901
|
||||
assert events == [
|
||||
("start", 123, 8, 2),
|
||||
("transcription-start", 123, 901, "session-1", "zh-CN"),
|
||||
(
|
||||
"segment",
|
||||
123,
|
||||
901,
|
||||
"session-1",
|
||||
[
|
||||
{
|
||||
"segment_id": "seg-1",
|
||||
"transcription_session_id": "session-1",
|
||||
"speaker_user_id": "patient_8",
|
||||
"speaker_role": "patient",
|
||||
"timestamp": 1200,
|
||||
"text": "patient words",
|
||||
}
|
||||
],
|
||||
),
|
||||
("finish", 123, 901, "session-1", 1, "completed"),
|
||||
("end", 123),
|
||||
]
|
||||
|
||||
|
||||
def test_end_auto_finishes_active_transcription_as_partial() -> None:
|
||||
events: list[tuple[object, ...]] = []
|
||||
|
||||
class Repository:
|
||||
def start_call(
|
||||
self, diagnosis_id: int, patient_id: int, *, call_type: int
|
||||
) -> dict[str, int]:
|
||||
events.append(("start", diagnosis_id, patient_id, call_type))
|
||||
return {"call_record_id": 902}
|
||||
|
||||
def start_call_transcription(
|
||||
self,
|
||||
diagnosis_id: int,
|
||||
call_record_id: int,
|
||||
transcription_session_id: str,
|
||||
*,
|
||||
language: str,
|
||||
) -> None:
|
||||
events.append(
|
||||
(
|
||||
"transcription-start",
|
||||
diagnosis_id,
|
||||
call_record_id,
|
||||
transcription_session_id,
|
||||
language,
|
||||
)
|
||||
)
|
||||
|
||||
def finish_call_transcription(
|
||||
self,
|
||||
diagnosis_id: int,
|
||||
call_record_id: int,
|
||||
transcription_session_id: str,
|
||||
expected_segment_count: int,
|
||||
*,
|
||||
status: str,
|
||||
) -> None:
|
||||
events.append(
|
||||
(
|
||||
"finish",
|
||||
diagnosis_id,
|
||||
call_record_id,
|
||||
transcription_session_id,
|
||||
expected_segment_count,
|
||||
status,
|
||||
)
|
||||
)
|
||||
|
||||
def end_call(self, diagnosis_id: int) -> None:
|
||||
events.append(("end", diagnosis_id))
|
||||
|
||||
request = VideoCallRequest(
|
||||
sdk_app_id=1400123456,
|
||||
user_id="doctor_42",
|
||||
user_sig="short-lived-ticket",
|
||||
target_user_id="patient_8",
|
||||
diagnosis_id=123,
|
||||
patient_id=8,
|
||||
)
|
||||
lifecycle = OrderedCallLifecycle(request, Repository(), logging.getLogger(__name__))
|
||||
|
||||
lifecycle.start()
|
||||
lifecycle.start_transcription("session-auto-partial")
|
||||
ended = lifecycle.end("window-closed")
|
||||
|
||||
assert ended.result(timeout=2) is True
|
||||
assert lifecycle.wait(1) is True
|
||||
assert events == [
|
||||
("start", 123, 8, 2),
|
||||
("transcription-start", 123, 902, "session-auto-partial", "zh-CN"),
|
||||
("finish", 123, 902, "session-auto-partial", 0, "partial"),
|
||||
("end", 123),
|
||||
]
|
||||
|
||||
|
||||
def test_failed_start_prevents_bind_and_end_writes() -> None:
|
||||
events: list[str] = []
|
||||
|
||||
@@ -278,8 +483,9 @@ def test_video_screenshot_is_uploaded_and_appended_to_patient_tongue_images() ->
|
||||
events: list[tuple[object, ...]] = []
|
||||
|
||||
class Repository:
|
||||
def start_call(self, diagnosis_id: int, *, call_type: int) -> None:
|
||||
def start_call(self, diagnosis_id: int, *, call_type: int) -> dict[str, int]:
|
||||
events.append(("start", diagnosis_id, call_type))
|
||||
return {"call_record_id": 903}
|
||||
|
||||
def upload_material_bytes(
|
||||
self,
|
||||
@@ -325,6 +531,28 @@ def test_video_screenshot_is_uploaded_and_appended_to_patient_tongue_images() ->
|
||||
]
|
||||
|
||||
|
||||
def test_start_rejects_missing_record_id_without_using_ticket_fallback() -> None:
|
||||
class Repository:
|
||||
def start_call(self, diagnosis_id: int, *, call_type: int) -> dict[str, object]:
|
||||
del diagnosis_id, call_type
|
||||
return {}
|
||||
|
||||
request = VideoCallRequest(
|
||||
sdk_app_id=1400123456,
|
||||
user_id="doctor_42",
|
||||
user_sig="short-lived-ticket",
|
||||
target_user_id="patient_8",
|
||||
diagnosis_id=123,
|
||||
call_record_id=77,
|
||||
)
|
||||
lifecycle = OrderedCallLifecycle(request, Repository(), logging.getLogger(__name__))
|
||||
|
||||
with pytest.raises(ValueError, match="startCall response did not include"):
|
||||
lifecycle.start().result(timeout=2)
|
||||
|
||||
assert lifecycle.started is False
|
||||
|
||||
|
||||
def test_https_document_policy_is_exact_and_origin_scoped() -> None:
|
||||
policy = TrustedDocumentPolicy.from_url(
|
||||
"https://RTC.Example.com/doctor-call/index.html?tenant=a#boot",
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+115
-115
File diff suppressed because one or more lines are too long
Vendored
+2
-2
@@ -5,8 +5,8 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="color-scheme" content="light" />
|
||||
<title>视频面诊</title>
|
||||
<script type="module" crossorigin src="./assets/index-DwSVWep6.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-qOBmgxQV.css">
|
||||
<script type="module" crossorigin src="./assets/index-R5GzqA8s.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-CED5X2W4.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
@@ -23,6 +23,7 @@ const props = defineProps<{
|
||||
chatBusy: Readonly<Ref<boolean>>
|
||||
notice: Readonly<Ref<string>>
|
||||
hasMoreMessages: Readonly<Ref<boolean>>
|
||||
transcriptionState: Readonly<Ref<string>>
|
||||
onSendText: (text: string) => Promise<void>
|
||||
onSendAttachment: (file: File) => Promise<void>
|
||||
onLoadMore: () => Promise<void>
|
||||
@@ -42,6 +43,15 @@ const isChat = computed(() => props.mode.value === 'chat')
|
||||
const isCalling = computed(() => ['starting', 'dialing', 'connected'].includes(props.phase.value))
|
||||
const videoVisible = computed(() => !isChat.value || isCalling.value)
|
||||
const canCapture = computed(() => props.phase.value === 'connected')
|
||||
const transcriptionActive = computed(() => props.transcriptionState.value === 'recording')
|
||||
const transcriptionFailed = computed(() => props.transcriptionState.value === 'error')
|
||||
const transcriptionStatusText = computed(() => {
|
||||
if (props.transcriptionState.value === 'starting') return '自动录音启动中…'
|
||||
if (props.transcriptionState.value === 'recording') return '自动录音并转文字中'
|
||||
if (props.transcriptionState.value === 'stopping') return '正在保存录音文字…'
|
||||
if (props.transcriptionState.value === 'error') return '自动录音转文字失败'
|
||||
return '自动录音已结束'
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.messages.value.length,
|
||||
@@ -242,6 +252,19 @@ async function captureScreenshot(): Promise<void> {
|
||||
</div>
|
||||
|
||||
<div v-if="isCalling" class="video-actions">
|
||||
<div
|
||||
v-if="canCapture"
|
||||
class="recording-status"
|
||||
:class="{
|
||||
'recording-status--active': transcriptionActive,
|
||||
'recording-status--error': transcriptionFailed,
|
||||
}"
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
>
|
||||
<span class="recording-indicator" aria-hidden="true" />
|
||||
{{ transcriptionStatusText }}
|
||||
</div>
|
||||
<button
|
||||
class="capture-button"
|
||||
type="button"
|
||||
|
||||
Vendored
+7
@@ -30,6 +30,13 @@ interface DoctorConsultationApi {
|
||||
hangup(): Promise<void>
|
||||
hostCallReady(ok: boolean, message?: string): void
|
||||
screenshotResult(ok: boolean, message: string): void
|
||||
transcriptionResult(
|
||||
operation: 'start' | 'segment' | 'stop',
|
||||
sessionId: string,
|
||||
segmentId: string,
|
||||
ok: boolean,
|
||||
message: string,
|
||||
): void
|
||||
}
|
||||
|
||||
interface QtVideoBridge {
|
||||
|
||||
+493
-17
@@ -15,6 +15,7 @@ import './style.css'
|
||||
type CallPhase = 'ready' | 'starting' | 'dialing' | 'connected' | 'ended' | 'error'
|
||||
type CompanionMode = 'chat' | 'video'
|
||||
type ChatMessageType = 'text' | 'image' | 'file' | 'audio' | 'video' | 'system'
|
||||
type TranscriptionState = 'idle' | 'starting' | 'recording' | 'stopping' | 'error'
|
||||
|
||||
interface NormalizedCallConfig {
|
||||
SDKAppID: number
|
||||
@@ -38,11 +39,74 @@ interface UiChatMessage {
|
||||
|
||||
interface BridgeMessage {
|
||||
source: 'doctor-call'
|
||||
event: 'ready' | 'call-start-request' | 'status' | 'room' | 'hangup' | 'error'
|
||||
event:
|
||||
| 'ready'
|
||||
| 'call-start-request'
|
||||
| 'status'
|
||||
| 'room'
|
||||
| 'hangup'
|
||||
| 'error'
|
||||
| 'transcription-start-request'
|
||||
| 'transcription-segment'
|
||||
| 'transcription-stop'
|
||||
diagnosisId?: number | string
|
||||
status?: string
|
||||
roomId?: string
|
||||
message?: string
|
||||
sessionId?: string
|
||||
language?: string
|
||||
segment?: {
|
||||
segment_id: string
|
||||
speaker_user_id: string
|
||||
speaker_role: 'doctor' | 'patient' | 'unknown'
|
||||
timestamp: number
|
||||
text: string
|
||||
}
|
||||
}
|
||||
|
||||
interface RealtimeTranscriberMessage {
|
||||
segmentId: string
|
||||
speakerUserId: string
|
||||
sourceText: string
|
||||
timestamp: number
|
||||
isCompleted: boolean
|
||||
}
|
||||
|
||||
interface RealtimeTranscriberListener {
|
||||
onReceiveTranscriberMessage: (
|
||||
roomId: string | number,
|
||||
message: RealtimeTranscriberMessage,
|
||||
) => void
|
||||
onRealtimeTranscriberStarted: (
|
||||
roomId: string | number,
|
||||
robotId: string,
|
||||
sourceLanguage: string,
|
||||
) => void
|
||||
onRealtimeTranscriberStopped: (roomId: string | number, robotId: string) => void
|
||||
onRealtimeTranscriberError: (
|
||||
roomId: string | number,
|
||||
robotId: string,
|
||||
error: number,
|
||||
errorMessage: string,
|
||||
) => void
|
||||
}
|
||||
|
||||
interface RealtimeTranscriberManager {
|
||||
addListener(listener: RealtimeTranscriberListener): void
|
||||
removeListener(listener: RealtimeTranscriberListener): void
|
||||
startRealtimeTranscriber(config: { sourceLanguage: string }): Promise<string>
|
||||
stopRealtimeTranscriber(robotId: string): Promise<void>
|
||||
}
|
||||
|
||||
interface PendingTranscriptionReply {
|
||||
sessionId: string
|
||||
resolve: (value: boolean) => void
|
||||
promise: Promise<boolean>
|
||||
}
|
||||
|
||||
interface PendingSegment {
|
||||
message: BridgeMessage
|
||||
attempts: number
|
||||
}
|
||||
|
||||
const phase = ref<CallPhase>('ready')
|
||||
@@ -54,6 +118,7 @@ const chatReady = ref(false)
|
||||
const chatBusy = ref(false)
|
||||
const notice = ref('')
|
||||
const hasMoreMessages = ref(false)
|
||||
const transcriptionState = ref<TranscriptionState>('idle')
|
||||
|
||||
let activeConfig: NormalizedCallConfig | null = null
|
||||
let chat: any = null
|
||||
@@ -65,6 +130,23 @@ let endNotified = true
|
||||
let starting = false
|
||||
let emittedRoomId = ''
|
||||
let resolveHostCallReady: ((value: boolean) => void) | null = null
|
||||
const pendingTranscriptionStarts = new Map<string, PendingTranscriptionReply>()
|
||||
const pendingTranscriptionStops = new Map<string, PendingTranscriptionReply>()
|
||||
let transcriptionSessionId = ''
|
||||
let transcriptionGeneration = 0
|
||||
let transcriberRunning = false
|
||||
let transcriberManager: RealtimeTranscriberManager | null = null
|
||||
let transcriberListener: RealtimeTranscriberListener | null = null
|
||||
let transcriberRobotId = ''
|
||||
let transcriptionStartPromise: Promise<void> | null = null
|
||||
let transcriptionStopPromise: Promise<void> | null = null
|
||||
let hangupNotification: Promise<void> | null = null
|
||||
let callCycleGeneration = 0
|
||||
let autoTranscriptionAttemptedGeneration = -1
|
||||
let lastTranscriberMessageAt = 0
|
||||
let transcriberStoppedAt = 0
|
||||
const acknowledgedSegmentIds = new Set<string>()
|
||||
const pendingSegments = new Map<string, PendingSegment>()
|
||||
|
||||
function initializeQtWebChannel(): void {
|
||||
const transport = window.qt?.webChannelTransport
|
||||
@@ -457,17 +539,372 @@ async function sendAttachment(file: File): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
function notifyHangup(status = 'ended'): void {
|
||||
if (endNotified) return
|
||||
function newTranscriptionSessionId(): string {
|
||||
const random = window.crypto?.randomUUID?.()
|
||||
return random ? `call-${random}` : `call-${Date.now()}-${Math.random().toString(16).slice(2)}`
|
||||
}
|
||||
|
||||
function requestTranscriptionStart(sessionId: string): Promise<boolean> {
|
||||
if (!activeConfig || !window.qtVideoBridge?.notify) return Promise.resolve(false)
|
||||
const existing = pendingTranscriptionStarts.get(sessionId)
|
||||
if (existing) return existing.promise
|
||||
const pending = {} as PendingTranscriptionReply
|
||||
pending.sessionId = sessionId
|
||||
pending.promise = new Promise<boolean>((resolve) => {
|
||||
pending.resolve = resolve
|
||||
})
|
||||
pendingTranscriptionStarts.set(sessionId, pending)
|
||||
emit({
|
||||
source: 'doctor-call',
|
||||
event: 'transcription-start-request',
|
||||
diagnosisId: activeConfig?.diagnosisId,
|
||||
sessionId,
|
||||
language: 'zh',
|
||||
})
|
||||
window.setTimeout(() => {
|
||||
if (pendingTranscriptionStarts.get(sessionId) !== pending) return
|
||||
pendingTranscriptionStarts.delete(sessionId)
|
||||
pending.resolve(false)
|
||||
}, 15000)
|
||||
return pending.promise
|
||||
}
|
||||
|
||||
function requestTranscriptionStop(
|
||||
sessionId: string,
|
||||
status: 'completed' | 'partial' | 'failed',
|
||||
): Promise<boolean> {
|
||||
if (!activeConfig || !sessionId || !window.qtVideoBridge?.notify) {
|
||||
return Promise.resolve(false)
|
||||
}
|
||||
const existing = pendingTranscriptionStops.get(sessionId)
|
||||
if (existing) return existing.promise
|
||||
const pending = {} as PendingTranscriptionReply
|
||||
pending.sessionId = sessionId
|
||||
pending.promise = new Promise<boolean>((resolve) => {
|
||||
pending.resolve = resolve
|
||||
})
|
||||
pendingTranscriptionStops.set(sessionId, pending)
|
||||
emit({
|
||||
source: 'doctor-call',
|
||||
event: 'transcription-stop',
|
||||
diagnosisId: activeConfig?.diagnosisId,
|
||||
sessionId,
|
||||
status,
|
||||
})
|
||||
window.setTimeout(() => {
|
||||
if (pendingTranscriptionStops.get(sessionId) !== pending) return
|
||||
pendingTranscriptionStops.delete(sessionId)
|
||||
pending.resolve(false)
|
||||
}, 15000)
|
||||
return pending.promise
|
||||
}
|
||||
|
||||
function getTranscriberManager(): RealtimeTranscriberManager {
|
||||
const engine = TUICallKitAPI.getTUICallEngineInstance?.()
|
||||
const cloud = engine?.getTRTCCloudInstance?.()
|
||||
const manager = cloud?.getAITranscriberManager?.() as Partial<RealtimeTranscriberManager> | null
|
||||
if (
|
||||
!manager
|
||||
|| typeof manager.addListener !== 'function'
|
||||
|| typeof manager.removeListener !== 'function'
|
||||
|| typeof manager.startRealtimeTranscriber !== 'function'
|
||||
|| typeof manager.stopRealtimeTranscriber !== 'function'
|
||||
) {
|
||||
throw new Error('当前视频服务未开通实时语音转写')
|
||||
}
|
||||
return manager as RealtimeTranscriberManager
|
||||
}
|
||||
|
||||
function handleTranscriberMessage(
|
||||
_roomId: string | number,
|
||||
message: RealtimeTranscriberMessage,
|
||||
): void {
|
||||
if (
|
||||
!activeConfig
|
||||
|| !['recording', 'stopping'].includes(transcriptionState.value)
|
||||
|| !transcriptionSessionId
|
||||
|| message.isCompleted !== true
|
||||
) return
|
||||
lastTranscriberMessageAt = Date.now()
|
||||
const segmentId = String(message.segmentId ?? '').trim()
|
||||
const text = String(message.sourceText ?? '').trim()
|
||||
if (
|
||||
!segmentId
|
||||
|| acknowledgedSegmentIds.has(segmentId)
|
||||
|| pendingSegments.has(segmentId)
|
||||
|| !text
|
||||
) return
|
||||
const speakerUserId = String(message.speakerUserId ?? '').trim()
|
||||
const bridgeMessage: BridgeMessage = {
|
||||
source: 'doctor-call',
|
||||
event: 'transcription-segment',
|
||||
diagnosisId: activeConfig.diagnosisId,
|
||||
sessionId: transcriptionSessionId,
|
||||
segment: {
|
||||
segment_id: segmentId.slice(0, 160),
|
||||
speaker_user_id: speakerUserId.slice(0, 160),
|
||||
speaker_role: speakerUserId === activeConfig.userID
|
||||
? 'doctor'
|
||||
: speakerUserId === activeConfig.targetUserId
|
||||
? 'patient'
|
||||
: 'unknown',
|
||||
timestamp: Math.max(0, Math.trunc(Number(message.timestamp) || 0)),
|
||||
text: text.slice(0, 4000),
|
||||
},
|
||||
}
|
||||
pendingSegments.set(segmentId, { message: bridgeMessage, attempts: 1 })
|
||||
emit(bridgeMessage)
|
||||
}
|
||||
|
||||
function subscribeTranscriber(): {
|
||||
manager: RealtimeTranscriberManager
|
||||
listener: RealtimeTranscriberListener
|
||||
} {
|
||||
if (transcriberManager && transcriberListener) {
|
||||
return { manager: transcriberManager, listener: transcriberListener }
|
||||
}
|
||||
const manager = getTranscriberManager()
|
||||
const listener: RealtimeTranscriberListener = {
|
||||
onReceiveTranscriberMessage: handleTranscriberMessage,
|
||||
onRealtimeTranscriberStarted: () => undefined,
|
||||
onRealtimeTranscriberStopped: (_roomId, robotId) => {
|
||||
if (robotId !== transcriberRobotId) return
|
||||
transcriberRunning = false
|
||||
transcriberStoppedAt = Date.now()
|
||||
if (transcriptionState.value === 'recording') {
|
||||
void stopTranscription('partial', true)
|
||||
}
|
||||
},
|
||||
onRealtimeTranscriberError: (_roomId, robotId, _error, errorMessage) => {
|
||||
if (robotId !== transcriberRobotId || transcriptionState.value === 'stopping') return
|
||||
notice.value = safeErrorMessage(new Error(errorMessage), '实时语音转写发生错误')
|
||||
void stopTranscription('partial', true)
|
||||
},
|
||||
}
|
||||
manager.addListener(listener)
|
||||
transcriberManager = manager
|
||||
transcriberListener = listener
|
||||
return { manager, listener }
|
||||
}
|
||||
|
||||
function unsubscribeTranscriber(
|
||||
manager = transcriberManager,
|
||||
listener = transcriberListener,
|
||||
): void {
|
||||
if (manager && listener) {
|
||||
try {
|
||||
manager.removeListener(listener)
|
||||
} catch {
|
||||
// A destroyed call engine has already released the listener.
|
||||
}
|
||||
}
|
||||
if (transcriberManager === manager && transcriberListener === listener) {
|
||||
transcriberManager = null
|
||||
transcriberListener = null
|
||||
}
|
||||
}
|
||||
|
||||
function transcriptionTokenIsCurrent(token: number, sessionId: string): boolean {
|
||||
return (
|
||||
token === transcriptionGeneration
|
||||
&& sessionId === transcriptionSessionId
|
||||
&& transcriptionState.value === 'starting'
|
||||
&& phase.value === 'connected'
|
||||
&& !endNotified
|
||||
)
|
||||
}
|
||||
|
||||
async function waitForPendingSegments(timeoutMs = 3000, quietMs = 300): Promise<boolean> {
|
||||
const startedAt = Date.now()
|
||||
const deadline = Date.now() + timeoutMs
|
||||
while (Date.now() < deadline) {
|
||||
const stoppedOrSettled = transcriberStoppedAt > 0 || Date.now() - startedAt >= 500
|
||||
const quiet = Date.now() - lastTranscriberMessageAt >= quietMs
|
||||
if (stoppedOrSettled && quiet && pendingSegments.size === 0) return true
|
||||
await new Promise((resolve) => window.setTimeout(resolve, 50))
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
async function performStartTranscription(): Promise<void> {
|
||||
if (!activeConfig || phase.value !== 'connected') throw new Error('视频接通后才能开始录音')
|
||||
if (transcriptionState.value !== 'idle' && transcriptionState.value !== 'error') {
|
||||
throw new Error('录音任务正在处理中')
|
||||
}
|
||||
|
||||
transcriptionState.value = 'starting'
|
||||
const sessionId = newTranscriptionSessionId()
|
||||
const token = ++transcriptionGeneration
|
||||
transcriptionSessionId = sessionId
|
||||
acknowledgedSegmentIds.clear()
|
||||
pendingSegments.clear()
|
||||
lastTranscriberMessageAt = Date.now()
|
||||
transcriberStoppedAt = 0
|
||||
notice.value = '正在准备录音文字存储…'
|
||||
const storageReady = await requestTranscriptionStart(sessionId)
|
||||
if (!storageReady) {
|
||||
if (!transcriptionTokenIsCurrent(token, sessionId)) return
|
||||
transcriptionState.value = 'error'
|
||||
transcriptionSessionId = ''
|
||||
throw new Error(notice.value || '服务端无法保存本次面诊对话文字')
|
||||
}
|
||||
if (!transcriptionTokenIsCurrent(token, sessionId)) {
|
||||
await requestTranscriptionStop(sessionId, 'partial')
|
||||
return
|
||||
}
|
||||
|
||||
let ownedManager: RealtimeTranscriberManager | null = null
|
||||
let ownedListener: RealtimeTranscriberListener | null = null
|
||||
try {
|
||||
const subscribed = subscribeTranscriber()
|
||||
const { manager, listener } = subscribed
|
||||
ownedManager = manager
|
||||
ownedListener = listener
|
||||
const robotId = await manager.startRealtimeTranscriber({
|
||||
sourceLanguage: 'zh',
|
||||
})
|
||||
if (!robotId) throw new Error('当前腾讯云项目未开通实时语音转写')
|
||||
if (!transcriptionTokenIsCurrent(token, sessionId)) {
|
||||
await manager.stopRealtimeTranscriber(robotId)
|
||||
unsubscribeTranscriber(manager, listener)
|
||||
return
|
||||
}
|
||||
transcriberRobotId = robotId
|
||||
transcriberRunning = true
|
||||
transcriptionState.value = 'recording'
|
||||
notice.value = '正在录音并实时转换为对话文字'
|
||||
} catch (error) {
|
||||
unsubscribeTranscriber(ownedManager, ownedListener)
|
||||
await requestTranscriptionStop(sessionId, 'failed')
|
||||
if (sessionId === transcriptionSessionId) transcriptionState.value = 'error'
|
||||
const message = safeErrorMessage(error, '录音转文字启动失败')
|
||||
notice.value = message
|
||||
if (sessionId === transcriptionSessionId) transcriptionSessionId = ''
|
||||
throw new Error(message)
|
||||
}
|
||||
}
|
||||
|
||||
function startTranscription(): Promise<void> {
|
||||
if (transcriptionStartPromise) return transcriptionStartPromise
|
||||
const operation = performStartTranscription()
|
||||
const tracked = operation.finally(() => {
|
||||
if (transcriptionStartPromise === tracked) transcriptionStartPromise = null
|
||||
})
|
||||
transcriptionStartPromise = tracked
|
||||
return transcriptionStartPromise
|
||||
}
|
||||
|
||||
async function stopTranscription(
|
||||
status: 'completed' | 'partial' | 'failed' = 'completed',
|
||||
managerAlreadyStopped = false,
|
||||
): Promise<void> {
|
||||
if (transcriptionStopPromise) return transcriptionStopPromise
|
||||
if (transcriptionState.value === 'idle') return
|
||||
if (!transcriptionSessionId) {
|
||||
transcriptionState.value = 'idle'
|
||||
return
|
||||
}
|
||||
const sessionId = transcriptionSessionId
|
||||
const startInFlight = transcriptionStartPromise
|
||||
transcriptionGeneration += 1
|
||||
transcriptionState.value = 'stopping'
|
||||
transcriptionStopPromise = (async () => {
|
||||
if (startInFlight) {
|
||||
try {
|
||||
await startInFlight
|
||||
} catch {
|
||||
// Its failure is materialized as a failed/partial transcript below.
|
||||
}
|
||||
}
|
||||
let sdkStopped = managerAlreadyStopped
|
||||
const manager = transcriberManager
|
||||
const robotId = transcriberRobotId
|
||||
if (!managerAlreadyStopped && transcriberRunning && manager && robotId) {
|
||||
try {
|
||||
await manager.stopRealtimeTranscriber(robotId)
|
||||
sdkStopped = true
|
||||
} catch (error) {
|
||||
console.warn('[doctor-consultation] 停止实时转写失败', safeErrorMessage(error))
|
||||
}
|
||||
}
|
||||
transcriberRunning = false
|
||||
transcriberRobotId = ''
|
||||
const segmentsComplete = await waitForPendingSegments()
|
||||
unsubscribeTranscriber()
|
||||
const finalStatus = sdkStopped && segmentsComplete ? status : 'partial'
|
||||
const persisted = await requestTranscriptionStop(sessionId, finalStatus)
|
||||
if (sessionId !== transcriptionSessionId) return
|
||||
transcriptionState.value = persisted ? 'idle' : 'error'
|
||||
notice.value = persisted
|
||||
? '本次面诊对话文字已保存到视频记录'
|
||||
: '对话文字未能完整确认保存,请稍后在面诊记录中检查'
|
||||
transcriptionSessionId = ''
|
||||
acknowledgedSegmentIds.clear()
|
||||
pendingSegments.clear()
|
||||
})().finally(() => {
|
||||
transcriptionStopPromise = null
|
||||
})
|
||||
return transcriptionStopPromise
|
||||
}
|
||||
|
||||
function transcriptionResult(
|
||||
operation: 'start' | 'segment' | 'stop',
|
||||
sessionId: string,
|
||||
segmentId: string,
|
||||
ok: boolean,
|
||||
message: string,
|
||||
): void {
|
||||
if (operation === 'start') {
|
||||
const pending = pendingTranscriptionStarts.get(sessionId)
|
||||
if (!pending) return
|
||||
pendingTranscriptionStarts.delete(sessionId)
|
||||
if (message && sessionId === transcriptionSessionId) notice.value = message
|
||||
pending.resolve(Boolean(ok))
|
||||
} else if (operation === 'stop') {
|
||||
const pending = pendingTranscriptionStops.get(sessionId)
|
||||
if (!pending) return
|
||||
pendingTranscriptionStops.delete(sessionId)
|
||||
if (message && sessionId === transcriptionSessionId) notice.value = message
|
||||
pending.resolve(Boolean(ok))
|
||||
} else if (operation === 'segment' && sessionId === transcriptionSessionId) {
|
||||
const pending = pendingSegments.get(segmentId)
|
||||
if (!pending) return
|
||||
if (ok) {
|
||||
pendingSegments.delete(segmentId)
|
||||
acknowledgedSegmentIds.add(segmentId)
|
||||
} else if (pending.attempts < 3) {
|
||||
pending.attempts += 1
|
||||
window.setTimeout(() => {
|
||||
if (pendingSegments.get(segmentId) === pending) emit(pending.message)
|
||||
}, pending.attempts * 250)
|
||||
} else {
|
||||
notice.value = message || '部分对话文字保存失败,本次记录将标记为部分保存'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function notifyHangup(status = 'ended'): Promise<void> {
|
||||
if (hangupNotification) return hangupNotification
|
||||
if (endNotified) return Promise.resolve()
|
||||
endNotified = true
|
||||
phase.value = 'ended'
|
||||
statusText.value = mode.value === 'chat' ? '视频通话已结束,IM 保持连接' : '视频问诊已结束'
|
||||
emit({
|
||||
source: 'doctor-call',
|
||||
event: 'hangup',
|
||||
diagnosisId: activeConfig?.diagnosisId,
|
||||
status,
|
||||
})
|
||||
hangupNotification = (async () => {
|
||||
try {
|
||||
if (transcriptionState.value !== 'idle') await stopTranscription('completed')
|
||||
} catch (error) {
|
||||
notice.value = safeErrorMessage(error, '录音文字收尾失败,请稍后检查面诊记录')
|
||||
console.warn('[doctor-consultation] 录音文字收尾失败', notice.value)
|
||||
} finally {
|
||||
emit({
|
||||
source: 'doctor-call',
|
||||
event: 'hangup',
|
||||
diagnosisId: activeConfig?.diagnosisId,
|
||||
status,
|
||||
})
|
||||
}
|
||||
})()
|
||||
return hangupNotification
|
||||
}
|
||||
|
||||
function readRoomId(): string {
|
||||
@@ -498,21 +935,31 @@ function handleStatusChanged(payload: unknown): void {
|
||||
: payload
|
||||
const status = typeof value === 'string' ? value : 'unknown'
|
||||
if (status === 'connected' || status.startsWith('calling-')) {
|
||||
if (endNotified || !activeConfig) return
|
||||
const cycle = callCycleGeneration
|
||||
phase.value = 'connected'
|
||||
statusText.value = '视频问诊进行中'
|
||||
void pollRoomId()
|
||||
if (autoTranscriptionAttemptedGeneration !== cycle) {
|
||||
autoTranscriptionAttemptedGeneration = cycle
|
||||
void startTranscription().catch((error) => {
|
||||
if (cycle !== callCycleGeneration || endNotified) return
|
||||
notice.value = safeErrorMessage(error, '自动录音转文字启动失败')
|
||||
})
|
||||
}
|
||||
} else if (status === 'calling' || status.startsWith('dialing')) {
|
||||
if (endNotified || !activeConfig) return
|
||||
phase.value = 'dialing'
|
||||
statusText.value = '正在等待患者接听'
|
||||
} else if (status === 'idle' && activeConfig && !starting) {
|
||||
notifyHangup(status)
|
||||
void notifyHangup(status)
|
||||
}
|
||||
emit({ source: 'doctor-call', event: 'status', diagnosisId: activeConfig?.diagnosisId, status })
|
||||
}
|
||||
|
||||
TUICallKitAPI.setCallback({
|
||||
statusChanged: handleStatusChanged,
|
||||
afterCalling: () => notifyHangup('after-calling'),
|
||||
afterCalling: () => { void notifyHangup('after-calling') },
|
||||
})
|
||||
TUICallKitAPI.setLanguage('zh-cn')
|
||||
TUICallKitAPI.enableFloatWindow(false)
|
||||
@@ -543,11 +990,17 @@ async function startVideo(): Promise<void> {
|
||||
if (!activeConfig) throw new Error('问诊配置尚未准备好')
|
||||
if (starting || !endNotified) throw new Error('已有视频通话正在进行')
|
||||
starting = true
|
||||
endNotified = false
|
||||
phase.value = 'starting'
|
||||
statusText.value = '正在创建安全视频通话'
|
||||
notice.value = ''
|
||||
try {
|
||||
if (hangupNotification) await hangupNotification
|
||||
if (!activeConfig || !endNotified) throw new Error('已有视频通话正在进行')
|
||||
hangupNotification = null
|
||||
callCycleGeneration += 1
|
||||
endNotified = false
|
||||
phase.value = 'starting'
|
||||
statusText.value = '正在创建安全视频通话'
|
||||
transcriptionState.value = 'idle'
|
||||
transcriptionSessionId = ''
|
||||
notice.value = ''
|
||||
const allowed = await requestHostCallStart()
|
||||
if (!allowed) throw new Error(notice.value || '服务器未能创建视频通话记录')
|
||||
await TUICallKitAPI.init({
|
||||
@@ -579,10 +1032,14 @@ async function startVideo(): Promise<void> {
|
||||
}
|
||||
|
||||
async function hangup(): Promise<void> {
|
||||
if (!activeConfig || endNotified) return
|
||||
if (!activeConfig) return
|
||||
if (endNotified) {
|
||||
if (hangupNotification) await hangupNotification
|
||||
return
|
||||
}
|
||||
try {
|
||||
await TUICallKitAPI.hangup()
|
||||
notifyHangup('local-hangup')
|
||||
await notifyHangup('local-hangup')
|
||||
} catch (error) {
|
||||
const message = safeErrorMessage(error, '结束视频通话失败')
|
||||
emit({ source: 'doctor-call', event: 'error', diagnosisId: activeConfig.diagnosisId, message })
|
||||
@@ -610,7 +1067,17 @@ async function open(config: DoctorCallConfig): Promise<void> {
|
||||
nextReqMessageID = ''
|
||||
hasMoreMessages.value = false
|
||||
endNotified = true
|
||||
hangupNotification = null
|
||||
callCycleGeneration += 1
|
||||
autoTranscriptionAttemptedGeneration = -1
|
||||
phase.value = 'ready'
|
||||
transcriptionState.value = 'idle'
|
||||
transcriptionSessionId = ''
|
||||
transcriptionGeneration += 1
|
||||
transcriberRunning = false
|
||||
transcriberRobotId = ''
|
||||
acknowledgedSegmentIds.clear()
|
||||
pendingSegments.clear()
|
||||
notice.value = ''
|
||||
if (activeConfig.mode === 'chat') {
|
||||
try {
|
||||
@@ -629,6 +1096,13 @@ async function open(config: DoctorCallConfig): Promise<void> {
|
||||
|
||||
async function close(): Promise<void> {
|
||||
if (!endNotified) await hangup()
|
||||
if (hangupNotification) await hangupNotification
|
||||
unsubscribeTranscriber()
|
||||
transcriptionGeneration += 1
|
||||
for (const pending of pendingTranscriptionStarts.values()) pending.resolve(false)
|
||||
for (const pending of pendingTranscriptionStops.values()) pending.resolve(false)
|
||||
pendingTranscriptionStarts.clear()
|
||||
pendingTranscriptionStops.clear()
|
||||
await logoutChat()
|
||||
activeConfig = null
|
||||
phase.value = 'ended'
|
||||
@@ -641,6 +1115,7 @@ window.doctorConsultation = {
|
||||
hangup,
|
||||
hostCallReady,
|
||||
screenshotResult,
|
||||
transcriptionResult,
|
||||
}
|
||||
window.doctorCall = { start: open, hangup }
|
||||
initializeQtWebChannel()
|
||||
@@ -655,6 +1130,7 @@ createApp(App, {
|
||||
chatBusy: readonly(chatBusy),
|
||||
notice: readonly(notice),
|
||||
hasMoreMessages: readonly(hasMoreMessages),
|
||||
transcriptionState: readonly(transcriptionState),
|
||||
onSendText: sendText,
|
||||
onSendAttachment: sendAttachment,
|
||||
onLoadMore: () => loadMessages(true),
|
||||
|
||||
@@ -294,6 +294,31 @@ button:disabled { cursor: not-allowed; opacity: .55; }
|
||||
gap: 10px;
|
||||
}
|
||||
.video-actions button { padding: 10px 15px; border-radius: 10px; font-weight: 600; }
|
||||
.recording-status {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 15px;
|
||||
border: 1px solid rgba(255, 255, 255, .24);
|
||||
border-radius: 10px;
|
||||
color: #fff;
|
||||
background: rgba(22, 29, 39, .88);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.recording-status--active { border-color: rgba(244, 99, 115, .64); background: rgba(126, 35, 50, .9); }
|
||||
.recording-status--error { border-color: rgba(242, 109, 109, .56); color: #ffe4e7; background: rgba(100, 31, 40, .88); }
|
||||
.recording-indicator {
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
border-radius: 50%;
|
||||
background: #f46373;
|
||||
box-shadow: 0 0 0 4px rgba(244, 99, 115, .16);
|
||||
}
|
||||
.recording-status--active .recording-indicator { animation: recording-pulse 1.25s ease-in-out infinite; }
|
||||
@keyframes recording-pulse {
|
||||
50% { box-shadow: 0 0 0 8px rgba(244, 99, 115, .04); opacity: .72; }
|
||||
}
|
||||
.hangup-button { border: 1px solid #b44755; color: #fff; background: rgba(161, 47, 61, .9); }
|
||||
.hangup-button:hover { background: #be394d; }
|
||||
.video-notice {
|
||||
|
||||
@@ -361,12 +361,48 @@ class DiagnosisController extends BaseAdminController
|
||||
// 传递当前管理员ID
|
||||
$params['admin_id'] = $this->adminId;
|
||||
|
||||
$result = DiagnosisLogic::startCall($params);
|
||||
if ($result) {
|
||||
return $this->success('发起通话成功', [], 1, 1);
|
||||
}
|
||||
return $this->fail(DiagnosisLogic::getError());
|
||||
}
|
||||
$result = DiagnosisLogic::startCall($params, $this->adminInfo);
|
||||
if ($result !== false) {
|
||||
return $this->data($result);
|
||||
}
|
||||
return $this->fail(DiagnosisLogic::getError());
|
||||
}
|
||||
|
||||
/** @notes 为当前医生的指定通话记录启动实时录音转写 */
|
||||
public function startCallTranscription()
|
||||
{
|
||||
$params = $this->request->post();
|
||||
$params['admin_id'] = (int)$this->adminId;
|
||||
$result = DiagnosisLogic::startCallTranscription($params);
|
||||
if ($result === false) {
|
||||
return $this->fail(DiagnosisLogic::getError());
|
||||
}
|
||||
return $this->data($result);
|
||||
}
|
||||
|
||||
/** @notes 幂等写入当前通话的已完成转写分段 */
|
||||
public function upsertCallTranscriptSegments()
|
||||
{
|
||||
$params = $this->request->post();
|
||||
$params['admin_id'] = (int)$this->adminId;
|
||||
$result = DiagnosisLogic::upsertCallTranscriptSegments($params);
|
||||
if ($result === false) {
|
||||
return $this->fail(DiagnosisLogic::getError());
|
||||
}
|
||||
return $this->data($result);
|
||||
}
|
||||
|
||||
/** @notes 完成当前通话转写并固化对话文字 */
|
||||
public function finishCallTranscription()
|
||||
{
|
||||
$params = $this->request->post();
|
||||
$params['admin_id'] = (int)$this->adminId;
|
||||
$result = DiagnosisLogic::finishCallTranscription($params);
|
||||
if ($result === false) {
|
||||
return $this->fail(DiagnosisLogic::getError());
|
||||
}
|
||||
return $this->data($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 结束通话
|
||||
|
||||
@@ -959,11 +959,12 @@ class ConversionLogic
|
||||
/**
|
||||
* 区间有效加粉:按企微员工聚合后,再投影到部门/成员/虚拟桶。
|
||||
*
|
||||
* 口径(对齐企微客户列表,而非原始回调条数):
|
||||
* 口径(对齐企微客户列表 / 官方「新增客户」不含继承,而非原始回调条数):
|
||||
* - 统计区间内 add_external_contact,按 (user_id, external_userid) 去重;
|
||||
* - 加粉之后、统计结束前须有 msg_audit_approved(排除未完成链路的幽灵事件);
|
||||
* - 加粉之后、统计结束前若出现 del_external_contact(客户从企微侧删除)则不计。
|
||||
* 不扣减 del_follow_user:企微客户列表中改名带「删」的客户往往仍在列表中。
|
||||
* 不扣减 del_follow_user:企微客户列表中改名带「删」的客户往往仍在列表中;
|
||||
* - 剔除继承客户:跟进人 add_way∈{201 内部成员共享, 202 管理员/负责人分配}(含在职/离职继承)。
|
||||
*
|
||||
* @param array<string, mixed>|null $mediaChannel
|
||||
* @param int[]|null $adminIds null means all active/unbound WeCom users
|
||||
@@ -986,7 +987,7 @@ class ConversionLogic
|
||||
}
|
||||
}
|
||||
|
||||
$baseKey = self::requestRowsCacheKey('fans-effective-v2', [
|
||||
$baseKey = self::requestRowsCacheKey('fans-effective-v3', [
|
||||
$startTimestamp,
|
||||
$endTimestamp,
|
||||
self::mediaChannelCacheKey($mediaChannel),
|
||||
@@ -1029,6 +1030,8 @@ class ConversionLogic
|
||||
->alias('e')
|
||||
->where('e.change_type', 'add_external_contact')
|
||||
->where('e.event_time', 'between', [$startTimestamp, $endTimestamp])
|
||||
->where('e.user_id', '<>', '')
|
||||
->where('e.external_userid', '<>', '')
|
||||
->whereRaw(
|
||||
'EXISTS (SELECT 1 FROM `' . $eventTable . '` audit_e'
|
||||
. ' WHERE audit_e.user_id = e.user_id'
|
||||
@@ -1047,8 +1050,8 @@ class ConversionLogic
|
||||
. ' AND del_e.event_time <= ?)',
|
||||
['del_external_contact', $endTimestamp]
|
||||
)
|
||||
->fieldRaw('e.user_id, COUNT(DISTINCT e.external_userid) AS add_fans_count')
|
||||
->group('e.user_id');
|
||||
->field(['e.user_id', 'e.external_userid'])
|
||||
->group('e.user_id, e.external_userid');
|
||||
if ($workWechatUserIds !== null) {
|
||||
$query->whereIn('e.user_id', $workWechatUserIds);
|
||||
}
|
||||
@@ -1056,11 +1059,114 @@ class ConversionLogic
|
||||
MediaChannelService::applyExternalUserChannelFilter($query, 'e.external_userid', $mediaChannel);
|
||||
}
|
||||
|
||||
self::$requestRowsCache[$cacheKey] = $query->select()->toArray();
|
||||
$pairs = $query->select()->toArray();
|
||||
$pairs = self::excludeInheritedFanPairs($pairs);
|
||||
|
||||
$countsByUser = [];
|
||||
foreach ($pairs as $pair) {
|
||||
$userId = trim((string) ($pair['user_id'] ?? ''));
|
||||
if ($userId === '') {
|
||||
continue;
|
||||
}
|
||||
$countsByUser[$userId] = ($countsByUser[$userId] ?? 0) + 1;
|
||||
}
|
||||
|
||||
$rows = [];
|
||||
foreach ($countsByUser as $userId => $count) {
|
||||
$rows[] = [
|
||||
'user_id' => $userId,
|
||||
'add_fans_count' => $count,
|
||||
];
|
||||
}
|
||||
|
||||
self::$requestRowsCache[$cacheKey] = $rows;
|
||||
|
||||
return self::$requestRowsCache[$cacheKey];
|
||||
}
|
||||
|
||||
/**
|
||||
* 剔除企微「继承/分配」客户:跟进人 add_way 为 201(内部成员共享)或 202(管理员/负责人分配,含在职/离职继承)。
|
||||
* 无本地客户档案或跟进信息不含该员工时保守保留(无法判定则仍计加粉)。
|
||||
*
|
||||
* @param array<int, array<string, mixed>> $pairs
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
private static function excludeInheritedFanPairs(array $pairs): array
|
||||
{
|
||||
if ($pairs === []) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$externalUserIds = [];
|
||||
foreach ($pairs as $pair) {
|
||||
$extId = trim((string) ($pair['external_userid'] ?? ''));
|
||||
if ($extId !== '') {
|
||||
$externalUserIds[$extId] = true;
|
||||
}
|
||||
}
|
||||
$externalIdList = array_keys($externalUserIds);
|
||||
if ($externalIdList === []) {
|
||||
return $pairs;
|
||||
}
|
||||
|
||||
/** @var array<string, true> $inheritedKeys user_id\0external_userid */
|
||||
$inheritedKeys = [];
|
||||
foreach (array_chunk($externalIdList, 500) as $chunk) {
|
||||
$contactRows = Db::name('qywx_external_contact')
|
||||
->whereIn('external_userid', $chunk)
|
||||
->field(['external_userid', 'follow_users'])
|
||||
->select()
|
||||
->toArray();
|
||||
foreach ($contactRows as $contact) {
|
||||
$extId = trim((string) ($contact['external_userid'] ?? ''));
|
||||
if ($extId === '') {
|
||||
continue;
|
||||
}
|
||||
$followUsers = $contact['follow_users'] ?? null;
|
||||
if (\is_string($followUsers) && $followUsers !== '') {
|
||||
$decoded = json_decode($followUsers, true);
|
||||
$followUsers = \is_array($decoded) ? $decoded : [];
|
||||
}
|
||||
if (!\is_array($followUsers)) {
|
||||
continue;
|
||||
}
|
||||
foreach ($followUsers as $fu) {
|
||||
if (!\is_array($fu)) {
|
||||
continue;
|
||||
}
|
||||
$addWay = (int) ($fu['add_way'] ?? $fu['AddWay'] ?? 0);
|
||||
if ($addWay !== 201 && $addWay !== 202) {
|
||||
continue;
|
||||
}
|
||||
$followUserId = trim((string) ($fu['userid'] ?? $fu['UserId'] ?? ''));
|
||||
if ($followUserId === '') {
|
||||
continue;
|
||||
}
|
||||
$inheritedKeys[$followUserId . "\0" . $extId] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($inheritedKeys === []) {
|
||||
return $pairs;
|
||||
}
|
||||
|
||||
$kept = [];
|
||||
foreach ($pairs as $pair) {
|
||||
$userId = trim((string) ($pair['user_id'] ?? ''));
|
||||
$extId = trim((string) ($pair['external_userid'] ?? ''));
|
||||
if ($userId === '' || $extId === '') {
|
||||
continue;
|
||||
}
|
||||
if (isset($inheritedKeys[$userId . "\0" . $extId])) {
|
||||
continue;
|
||||
}
|
||||
$kept[] = $pair;
|
||||
}
|
||||
|
||||
return $kept;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $userIds
|
||||
* @return array<string, array{id: int|string, name: string}>
|
||||
@@ -1625,10 +1731,13 @@ class ConversionLogic
|
||||
$entity['account_cost'] = $effectiveAccountCost;
|
||||
$entity['paid_appointment_rate'] = self::percent($paidAppointmentCount, $addFansCount);
|
||||
$entity['open_appointment_rate'] = self::percent($paidAppointmentCount, $totalOpenCount);
|
||||
// 预约率:面诊 / 预约(看预约后未到面)
|
||||
$entity['interview_rate'] = self::percent($interviewCount, $appointmentTotalCount);
|
||||
// 接诊率:接诊诊单 / 总进线(加粉);医生维度仍用面诊作分母
|
||||
$entity['receive_rate'] = self::receiveRate($completedOrderCount, $addFansCount, $interviewCount, $dimension);
|
||||
// 面诊接诊率:接诊诊单 / 面诊
|
||||
$entity['interview_receive_rate'] = self::percent($completedOrderCount, $interviewCount);
|
||||
// 面诊接诊率:面诊 / 挂号
|
||||
// 面诊率:面诊 / 挂号(看挂号后流失)
|
||||
$entity['interview_paid_rate'] = self::percent($interviewCount, $paidAppointmentCount);
|
||||
$entity['open_receive_rate'] = self::percent($completedOrderCount, $totalOpenCount);
|
||||
$entity['avg_unit_price'] = self::safeDivideMoney($completedOrderAmount, $completedOrderCount);
|
||||
@@ -2711,6 +2820,10 @@ class ConversionLogic
|
||||
return $charts;
|
||||
}
|
||||
|
||||
/**
|
||||
* 接诊率:接诊诊单 ÷ 总进线(加粉)。
|
||||
* 医生维度无加粉口径时,退化为接诊诊单 ÷ 面诊。
|
||||
*/
|
||||
private static function receiveRate(int $completedOrderCount, int $addFansCount, int $interviewCount, string $dimension): float
|
||||
{
|
||||
$denominator = $dimension === 'doctor' ? $interviewCount : $addFansCount;
|
||||
|
||||
@@ -1572,32 +1572,67 @@ class DiagnosisLogic extends BaseLogic
|
||||
/**
|
||||
* @notes 发起通话
|
||||
* @param array $params
|
||||
* @return bool
|
||||
*/
|
||||
public static function startCall(array $params): bool
|
||||
{
|
||||
try {
|
||||
// 获取当前管理员ID(从参数中获取)
|
||||
$adminId = $params['admin_id'] ?? 0;
|
||||
|
||||
if (!$adminId) {
|
||||
self::setError('获取管理员信息失败');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 创建通话记录
|
||||
\app\common\model\tcm\CallRecord::create([
|
||||
'diagnosis_id' => $params['diagnosis_id'],
|
||||
'caller_id' => $adminId,
|
||||
'caller_type' => 'doctor',
|
||||
'callee_id' => $params['patient_id'] ?? 0,
|
||||
'callee_type' => 'patient',
|
||||
'call_type' => $params['call_type'] ?? 2, // 1-语音 2-视频
|
||||
* @return array{call_record_id:int}|false
|
||||
*/
|
||||
public static function startCall(array $params, array $adminInfo = [])
|
||||
{
|
||||
try {
|
||||
$adminId = (int)($params['admin_id'] ?? 0);
|
||||
$diagnosisId = (int)($params['diagnosis_id'] ?? 0);
|
||||
$patientId = (int)($params['patient_id'] ?? 0);
|
||||
$callType = (int)($params['call_type'] ?? 2);
|
||||
if ($adminId <= 0 || $diagnosisId <= 0 || $patientId <= 0) {
|
||||
self::setError('通话身份参数无效');
|
||||
return false;
|
||||
}
|
||||
if (!in_array($callType, [1, 2], true)) {
|
||||
self::setError('通话类型无效');
|
||||
return false;
|
||||
}
|
||||
$diagnosisQuery = Diagnosis::where('id', $diagnosisId)
|
||||
->where('patient_id', $patientId);
|
||||
$roleIds = array_map(
|
||||
'intval',
|
||||
AdminRole::where('admin_id', $adminId)->column('role_id')
|
||||
);
|
||||
if (in_array(2, $roleIds, true)) {
|
||||
$diagnosisQuery->where('assistant_id', $adminId);
|
||||
}
|
||||
if (DataScopeService::isEnabled()) {
|
||||
$visibleIds = DataScopeService::getVisibleAdminIds($adminId, $adminInfo);
|
||||
if ($visibleIds === []) {
|
||||
self::setError('诊单不存在或无权访问');
|
||||
return false;
|
||||
}
|
||||
if (is_array($visibleIds)) {
|
||||
$diagnosisQuery->whereIn('assistant_id', $visibleIds);
|
||||
}
|
||||
}
|
||||
$diagnosis = $diagnosisQuery->find();
|
||||
if (!$diagnosis) {
|
||||
self::setError('诊单不存在、患者不匹配或无权访问');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 创建通话记录
|
||||
$record = \app\common\model\tcm\CallRecord::create([
|
||||
'diagnosis_id' => $diagnosisId,
|
||||
'caller_id' => $adminId,
|
||||
'caller_type' => 'doctor',
|
||||
'callee_id' => $patientId,
|
||||
'callee_type' => 'patient',
|
||||
'call_type' => $callType,
|
||||
'status' => 1, // 1-进行中
|
||||
'start_time' => time()
|
||||
]);
|
||||
|
||||
return true;
|
||||
'start_time' => time()
|
||||
]);
|
||||
|
||||
$callRecordId = (int)($record['id'] ?? 0);
|
||||
if ($callRecordId <= 0) {
|
||||
self::setError('创建通话记录后未取得记录ID');
|
||||
return false;
|
||||
}
|
||||
|
||||
return ['call_record_id' => $callRecordId];
|
||||
} catch (\Exception $e) {
|
||||
self::setError($e->getMessage());
|
||||
return false;
|
||||
@@ -1807,7 +1842,7 @@ class DiagnosisLogic extends BaseLogic
|
||||
* @param array $params
|
||||
* @return array
|
||||
*/
|
||||
public static function getCallRecords(array $params): array
|
||||
public static function getCallRecords(array $params): array
|
||||
{
|
||||
try {
|
||||
$records = \app\common\model\tcm\CallRecord::where('diagnosis_id', $params['diagnosis_id'])
|
||||
@@ -1826,16 +1861,323 @@ class DiagnosisLogic extends BaseLogic
|
||||
$urls = $decoded;
|
||||
}
|
||||
}
|
||||
$record['recording_urls_list'] = $urls;
|
||||
$record['recording_status_text'] = self::recordingStatusText((int)($record['recording_status'] ?? 0));
|
||||
}
|
||||
$record['recording_urls_list'] = $urls;
|
||||
$record['recording_status_text'] = self::recordingStatusText((int)($record['recording_status'] ?? 0));
|
||||
$record['transcription_status_text'] = self::transcriptionStatusText(
|
||||
(string)($record['transcription_status'] ?? '')
|
||||
);
|
||||
}
|
||||
|
||||
return $records;
|
||||
} catch (\Exception $e) {
|
||||
self::setError($e->getMessage());
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 为指定通话记录创建幂等的实时转写会话
|
||||
* @return array|false
|
||||
*/
|
||||
public static function startCallTranscription(array $params)
|
||||
{
|
||||
try {
|
||||
$sessionId = trim((string)($params['transcription_session_id'] ?? ''));
|
||||
$language = trim((string)($params['language'] ?? 'zh-CN'));
|
||||
if ($sessionId === '' || mb_strlen($sessionId) > 128) {
|
||||
self::setError('转写会话ID无效');
|
||||
return false;
|
||||
}
|
||||
if ($language === '' || mb_strlen($language) > 32) {
|
||||
self::setError('转写语言无效');
|
||||
return false;
|
||||
}
|
||||
return \think\facade\Db::transaction(function () use ($params, $sessionId, $language) {
|
||||
$record = self::resolveCallRecordForTranscription($params, true, true);
|
||||
if (!$record) {
|
||||
return false;
|
||||
}
|
||||
$existingSession = trim((string)($record['transcription_session_id'] ?? ''));
|
||||
if ($existingSession !== '' && $existingSession !== $sessionId) {
|
||||
self::setError('该通话记录已绑定其他转写会话');
|
||||
return false;
|
||||
}
|
||||
if ($existingSession === '') {
|
||||
$now = time();
|
||||
$record->save([
|
||||
'transcription_session_id' => $sessionId,
|
||||
'transcription_language' => $language,
|
||||
'transcription_status' => 'running',
|
||||
'transcription_segment_count' => 0,
|
||||
'transcript_text' => '',
|
||||
'transcription_started_at' => $now,
|
||||
'transcription_finished_at' => 0,
|
||||
'update_time' => $now,
|
||||
]);
|
||||
}
|
||||
|
||||
return [
|
||||
'call_record_id' => (int)$record['id'],
|
||||
'transcription_session_id' => $sessionId,
|
||||
'status' => (string)($record['transcription_status'] ?? 'running'),
|
||||
];
|
||||
});
|
||||
} catch (\Throwable $e) {
|
||||
self::setError($e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 幂等写入已完成的实时转写分段,并刷新通话记录文字字段
|
||||
* @return array|false
|
||||
*/
|
||||
public static function upsertCallTranscriptSegments(array $params)
|
||||
{
|
||||
try {
|
||||
$sessionId = trim((string)($params['transcription_session_id'] ?? ''));
|
||||
$segments = $params['segments'] ?? null;
|
||||
if ($sessionId === '' || mb_strlen($sessionId) > 128) {
|
||||
self::setError('转写会话ID无效');
|
||||
return false;
|
||||
}
|
||||
if (!is_array($segments) || count($segments) < 1 || count($segments) > 50) {
|
||||
self::setError('转写分段数量必须为1到50条');
|
||||
return false;
|
||||
}
|
||||
$normalized = [];
|
||||
foreach ($segments as $segment) {
|
||||
if (!is_array($segment)) {
|
||||
self::setError('转写分段格式无效');
|
||||
return false;
|
||||
}
|
||||
$segmentId = trim((string)($segment['segment_id'] ?? ''));
|
||||
$text = trim((string)($segment['text'] ?? ''));
|
||||
$speakerUserId = trim((string)($segment['speaker_user_id'] ?? ''));
|
||||
$speakerRole = trim((string)($segment['speaker_role'] ?? 'unknown'));
|
||||
if ($segmentId === '' || mb_strlen($segmentId) > 160) {
|
||||
self::setError('转写分段ID无效');
|
||||
return false;
|
||||
}
|
||||
if ($text === '' || mb_strlen($text) > 4000) {
|
||||
self::setError('转写文字长度无效');
|
||||
return false;
|
||||
}
|
||||
if (mb_strlen($speakerUserId) > 160) {
|
||||
self::setError('说话人ID过长');
|
||||
return false;
|
||||
}
|
||||
if (!in_array($speakerRole, ['doctor', 'patient', 'unknown'], true)) {
|
||||
$speakerRole = 'unknown';
|
||||
}
|
||||
$normalized[] = [
|
||||
'segment_id' => $segmentId,
|
||||
'speaker_user_id' => $speakerUserId,
|
||||
'speaker_role' => $speakerRole,
|
||||
'timestamp_ms' => max(0, (int)($segment['timestamp'] ?? 0)),
|
||||
'text' => $text,
|
||||
];
|
||||
}
|
||||
|
||||
$result = \think\facade\Db::transaction(function () use (
|
||||
$params,
|
||||
$sessionId,
|
||||
$normalized
|
||||
) {
|
||||
$record = self::resolveCallRecordForTranscription($params, false, true);
|
||||
if (!$record) {
|
||||
return false;
|
||||
}
|
||||
if ((string)($record['transcription_session_id'] ?? '') !== $sessionId) {
|
||||
self::setError('转写会话与通话记录不匹配');
|
||||
return false;
|
||||
}
|
||||
if ((string)($record['transcription_status'] ?? '') !== 'running') {
|
||||
self::setError('该通话转写已经结束');
|
||||
return false;
|
||||
}
|
||||
$callRecordId = (int)$record['id'];
|
||||
$now = time();
|
||||
foreach ($normalized as $segment) {
|
||||
$values = array_merge($segment, [
|
||||
'call_record_id' => $callRecordId,
|
||||
'transcription_session_id' => $sessionId,
|
||||
'create_time' => $now,
|
||||
'update_time' => $now,
|
||||
]);
|
||||
\think\facade\Db::name('tcm_call_transcript_segment')
|
||||
->duplicate([
|
||||
'speaker_user_id',
|
||||
'speaker_role',
|
||||
'timestamp_ms',
|
||||
'text',
|
||||
'update_time',
|
||||
])
|
||||
->insert($values);
|
||||
}
|
||||
$snapshot = self::buildCallTranscriptSnapshot($callRecordId, $sessionId);
|
||||
$record->save([
|
||||
'transcription_segment_count' => $snapshot['segment_count'],
|
||||
'transcript_text' => $snapshot['transcript_text'],
|
||||
'update_time' => $now,
|
||||
]);
|
||||
return [
|
||||
'call_record_id' => $callRecordId,
|
||||
'segment_count' => (int)$snapshot['segment_count'],
|
||||
];
|
||||
});
|
||||
if ($result === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return [
|
||||
'call_record_id' => (int)$result['call_record_id'],
|
||||
'transcription_session_id' => $sessionId,
|
||||
'stored_segment_count' => (int)$result['segment_count'],
|
||||
];
|
||||
} catch (\Throwable $e) {
|
||||
self::setError($e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 完成实时转写并将最终文字固化在通话记录上
|
||||
* @return array|false
|
||||
*/
|
||||
public static function finishCallTranscription(array $params)
|
||||
{
|
||||
try {
|
||||
$sessionId = trim((string)($params['transcription_session_id'] ?? ''));
|
||||
$requestedStatus = strtolower(trim((string)($params['status'] ?? 'completed')));
|
||||
$expectedCount = (int)($params['expected_segment_count'] ?? 0);
|
||||
if ($sessionId === '' || mb_strlen($sessionId) > 128 || $expectedCount < 0) {
|
||||
self::setError('转写完成参数无效');
|
||||
return false;
|
||||
}
|
||||
if (!in_array($requestedStatus, ['completed', 'partial', 'failed'], true)) {
|
||||
self::setError('转写完成状态无效');
|
||||
return false;
|
||||
}
|
||||
return \think\facade\Db::transaction(function () use (
|
||||
$params,
|
||||
$sessionId,
|
||||
$requestedStatus,
|
||||
$expectedCount
|
||||
) {
|
||||
$record = self::resolveCallRecordForTranscription($params, false, true);
|
||||
if (!$record) {
|
||||
return false;
|
||||
}
|
||||
if ((string)($record['transcription_session_id'] ?? '') !== $sessionId) {
|
||||
self::setError('转写会话与通话记录不匹配');
|
||||
return false;
|
||||
}
|
||||
$existingStatus = (string)($record['transcription_status'] ?? '');
|
||||
if ($existingStatus !== '' && $existingStatus !== 'running') {
|
||||
return [
|
||||
'call_record_id' => (int)$record['id'],
|
||||
'transcription_session_id' => $sessionId,
|
||||
'status' => $existingStatus,
|
||||
'segment_count' => (int)($record['transcription_segment_count'] ?? 0),
|
||||
];
|
||||
}
|
||||
|
||||
$callRecordId = (int)$record['id'];
|
||||
$snapshot = self::buildCallTranscriptSnapshot($callRecordId, $sessionId);
|
||||
$actualCount = (int)$snapshot['segment_count'];
|
||||
$finalStatus = $requestedStatus;
|
||||
if ($requestedStatus === 'completed' && $actualCount < $expectedCount) {
|
||||
$finalStatus = 'partial';
|
||||
}
|
||||
$now = time();
|
||||
$record->save([
|
||||
'transcription_status' => $finalStatus,
|
||||
'transcription_segment_count' => $actualCount,
|
||||
'transcript_text' => $snapshot['transcript_text'],
|
||||
'transcription_finished_at' => $now,
|
||||
'update_time' => $now,
|
||||
]);
|
||||
|
||||
return [
|
||||
'call_record_id' => $callRecordId,
|
||||
'transcription_session_id' => $sessionId,
|
||||
'status' => $finalStatus,
|
||||
'segment_count' => $actualCount,
|
||||
];
|
||||
});
|
||||
} catch (\Throwable $e) {
|
||||
self::setError($e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static function resolveCallRecordForTranscription(
|
||||
array $params,
|
||||
bool $requireRunning,
|
||||
bool $lock = false
|
||||
)
|
||||
{
|
||||
$diagnosisId = (int)($params['diagnosis_id'] ?? 0);
|
||||
$callRecordId = (int)($params['call_record_id'] ?? 0);
|
||||
$adminId = (int)($params['admin_id'] ?? 0);
|
||||
if ($diagnosisId <= 0 || $callRecordId <= 0 || $adminId <= 0) {
|
||||
self::setError('通话转写身份参数无效');
|
||||
return null;
|
||||
}
|
||||
$query = \app\common\model\tcm\CallRecord::where('id', $callRecordId)
|
||||
->where('diagnosis_id', $diagnosisId)
|
||||
->where('caller_id', $adminId)
|
||||
->where('caller_type', 'doctor');
|
||||
if ($lock) {
|
||||
$query->lock(true);
|
||||
}
|
||||
$record = $query->find();
|
||||
if (!$record) {
|
||||
self::setError('通话记录不存在或无权操作');
|
||||
return null;
|
||||
}
|
||||
if ($requireRunning && (int)($record['status'] ?? 0) !== 1) {
|
||||
self::setError('通话记录已经结束');
|
||||
return null;
|
||||
}
|
||||
return $record;
|
||||
}
|
||||
|
||||
/** @return array{segment_count:int,transcript_text:string} */
|
||||
private static function buildCallTranscriptSnapshot(int $callRecordId, string $sessionId): array
|
||||
{
|
||||
$rows = \think\facade\Db::name('tcm_call_transcript_segment')
|
||||
->where('call_record_id', $callRecordId)
|
||||
->where('transcription_session_id', $sessionId)
|
||||
->order('timestamp_ms asc, id asc')
|
||||
->select()
|
||||
->toArray();
|
||||
$labels = ['doctor' => '医生', 'patient' => '患者', 'unknown' => '未知说话人'];
|
||||
$lines = [];
|
||||
foreach ($rows as $row) {
|
||||
$text = trim((string)($row['text'] ?? ''));
|
||||
if ($text === '') {
|
||||
continue;
|
||||
}
|
||||
$role = (string)($row['speaker_role'] ?? 'unknown');
|
||||
$lines[] = ($labels[$role] ?? $labels['unknown']) . ':' . $text;
|
||||
}
|
||||
return [
|
||||
'segment_count' => count($rows),
|
||||
'transcript_text' => implode("\n", $lines),
|
||||
];
|
||||
}
|
||||
|
||||
private static function transcriptionStatusText(string $status): string
|
||||
{
|
||||
return [
|
||||
'running' => '录音转写中',
|
||||
'completed' => '文字已生成',
|
||||
'partial' => '文字部分保存',
|
||||
'failed' => '文字生成失败',
|
||||
][$status] ?? '未生成文字';
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 将 TRTC 房间号写入当前诊单通话记录,并尝试 API 合流云端录制
|
||||
|
||||
@@ -3963,9 +3963,9 @@ class PrescriptionOrderLogic
|
||||
}
|
||||
}
|
||||
|
||||
// 药方名称反查:处方库(开方医师 creator_id + 公开模板),按 formula_type + 药材集合匹配,
|
||||
// 复刻前端 resolveSlipAuxLibraryName 口径(主方/辅方均如此;处方记录 prescription_name 仅做兜底)。
|
||||
[$libByDoctor, $libPublic] = self::buildPrescriptionLibraryNameIndexForExport($rxById);
|
||||
// 药方名称反查:处方库(开方医师 → 公开 → 全库同药材集合兜底),按 formula_type + 药材集合匹配。
|
||||
// 辅方模板额外全量加载,避免「系统代开 / 非模板所属医师」因 creator 范围过窄丢辅方名。
|
||||
[$libByDoctor, $libPublic, $libAny] = self::buildPrescriptionLibraryNameIndexForExport($rxById);
|
||||
|
||||
if ($needGuahaoChannel) {
|
||||
$yejiChannelHighlightByDiag = [];
|
||||
@@ -4174,11 +4174,12 @@ class PrescriptionOrderLogic
|
||||
|
||||
$item['export_medication_form'] = self::formatMedicationFormForExport(\is_array($rx) ? $rx : []);
|
||||
|
||||
// 药方名称:主方/辅方均反查处方库取名称(与处方笺一致),带标签;甘草/无匹配留空
|
||||
// 药方名称:主方/辅方均反查处方库取名称(与处方笺一致),带标签;有辅方药材时即使未匹配到库名也保留「辅方」标记
|
||||
[$mainRxName, $auxRxName] = self::resolvePrescriptionNamesForExport(
|
||||
\is_array($rx) ? $rx : [],
|
||||
$libByDoctor,
|
||||
$libPublic
|
||||
$libPublic,
|
||||
$libAny
|
||||
);
|
||||
$rxNameParts = [];
|
||||
if ($mainRxName !== '') {
|
||||
@@ -4298,19 +4299,24 @@ class PrescriptionOrderLogic
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出药方名称反查:批量加载相关开方医师处方库(creator_id 命中 + 公开模板),
|
||||
* 建立 [creator_id][formula_type][herbKey] => name 与 公开 [formula_type][herbKey] => name 两级索引。
|
||||
* 导出药方名称反查:批量加载相关开方医师处方库(creator_id 命中 + 公开模板 + 全量辅方模板),
|
||||
* 建立三级索引:医师私有 / 公开 / 任意已加载(跨医师同药材集合兜底)。
|
||||
*
|
||||
* @param array<int, array> $rxById 处方 id => 处方行(需含 herbs / creator_id)
|
||||
*
|
||||
* @return array{0: array<int, array<string, array<string, string>>>, 1: array<string, array<string, string>>}
|
||||
* @return array{
|
||||
* 0: array<int, array<string, array<string, string>>>,
|
||||
* 1: array<string, array<string, string>>,
|
||||
* 2: array<string, array<string, string>>
|
||||
* }
|
||||
*/
|
||||
private static function buildPrescriptionLibraryNameIndexForExport(array $rxById): array
|
||||
{
|
||||
$libByDoctor = [];
|
||||
$libPublic = [];
|
||||
$libAny = [];
|
||||
if ($rxById === []) {
|
||||
return [$libByDoctor, $libPublic];
|
||||
return [$libByDoctor, $libPublic, $libAny];
|
||||
}
|
||||
|
||||
$doctorIds = [];
|
||||
@@ -4323,12 +4329,15 @@ class PrescriptionOrderLogic
|
||||
$doctorIdList = array_keys($doctorIds);
|
||||
|
||||
try {
|
||||
// 开方医师私有 + 公开模板 + 全量辅方模板(辅方常被非模板所有者的「系统代开」处方复用)
|
||||
$libRows = PrescriptionLibrary::whereNull('delete_time')
|
||||
->where(function ($q) use ($doctorIdList) {
|
||||
if ($doctorIdList !== []) {
|
||||
$q->whereIn('creator_id', $doctorIdList)->whereOr('is_public', 1);
|
||||
$q->whereIn('creator_id', $doctorIdList)
|
||||
->whereOr('is_public', 1)
|
||||
->whereOr('formula_type', '辅方');
|
||||
} else {
|
||||
$q->where('is_public', 1);
|
||||
$q->where('is_public', 1)->whereOr('formula_type', '辅方');
|
||||
}
|
||||
})
|
||||
->field(['prescription_name', 'formula_type', 'herbs', 'creator_id', 'is_public'])
|
||||
@@ -4337,7 +4346,7 @@ class PrescriptionOrderLogic
|
||||
} catch (\Throwable $e) {
|
||||
Log::warning('buildPrescriptionLibraryNameIndexForExport failed: ' . $e->getMessage());
|
||||
|
||||
return [$libByDoctor, $libPublic];
|
||||
return [$libByDoctor, $libPublic, $libAny];
|
||||
}
|
||||
|
||||
foreach ($libRows as $lib) {
|
||||
@@ -4359,6 +4368,9 @@ class PrescriptionOrderLogic
|
||||
}
|
||||
$ft = ((string) ($lib['formula_type'] ?? '')) === '辅方' ? '辅方' : '主方';
|
||||
|
||||
// 跨医师兜底:同药材集合优先保留先写入的名称(稳定、可预期)
|
||||
$libAny[$ft][$key] ??= $name;
|
||||
|
||||
if ((int) ($lib['is_public'] ?? 0) === 1) {
|
||||
$libPublic[$ft][$key] ??= $name;
|
||||
}
|
||||
@@ -4368,23 +4380,28 @@ class PrescriptionOrderLogic
|
||||
}
|
||||
}
|
||||
|
||||
return [$libByDoctor, $libPublic];
|
||||
return [$libByDoctor, $libPublic, $libAny];
|
||||
}
|
||||
|
||||
/**
|
||||
* 反查单张处方的主方 / 辅方名称(与处方笺展示一致):
|
||||
* - 主方:主方药材集合匹配开方医师/公开处方库(formula_type=主方);兜底取处方 prescription_name。
|
||||
* - 主方:主方药材集合匹配 开方医师 → 公开 → 任意已加载;兜底取处方 prescription_name。
|
||||
* - 辅方:仅在实际存在辅方药材时,优先 aux_usage.prescription_name / library_name,
|
||||
* 否则按辅方药材集合匹配(formula_type=辅方)。
|
||||
* 否则按辅方药材集合匹配;仍无匹配时回落为「辅方」(保证导出名含辅方标记)。
|
||||
*
|
||||
* @param array $rx 处方行(含 herbs / creator_id / prescription_name / aux_usage)
|
||||
* @param array<int, array<string, array<string, string>>> $libByDoctor
|
||||
* @param array<string, array<string, string>> $libPublic
|
||||
* @param array<string, array<string, string>> $libAny
|
||||
*
|
||||
* @return array{0: string, 1: string} [主方名称, 辅方名称]
|
||||
*/
|
||||
private static function resolvePrescriptionNamesForExport(array $rx, array $libByDoctor, array $libPublic): array
|
||||
{
|
||||
private static function resolvePrescriptionNamesForExport(
|
||||
array $rx,
|
||||
array $libByDoctor,
|
||||
array $libPublic,
|
||||
array $libAny = []
|
||||
): array {
|
||||
if ($rx === []) {
|
||||
return ['', ''];
|
||||
}
|
||||
@@ -4393,7 +4410,7 @@ class PrescriptionOrderLogic
|
||||
|
||||
[$mainHerbs, $auxHerbs] = self::splitPrescriptionHerbsFromRx($rx);
|
||||
|
||||
$lookup = static function (string $ft, array $hs) use ($doctorId, $libByDoctor, $libPublic): string {
|
||||
$lookup = static function (string $ft, array $hs) use ($doctorId, $libByDoctor, $libPublic, $libAny): string {
|
||||
if ($hs === []) {
|
||||
return '';
|
||||
}
|
||||
@@ -4404,8 +4421,11 @@ class PrescriptionOrderLogic
|
||||
if ($doctorId > 0 && isset($libByDoctor[$doctorId][$ft][$key])) {
|
||||
return $libByDoctor[$doctorId][$ft][$key];
|
||||
}
|
||||
if (isset($libPublic[$ft][$key])) {
|
||||
return $libPublic[$ft][$key];
|
||||
}
|
||||
|
||||
return (string) ($libPublic[$ft][$key] ?? '');
|
||||
return (string) ($libAny[$ft][$key] ?? '');
|
||||
};
|
||||
|
||||
// 主方
|
||||
@@ -4432,6 +4452,10 @@ class PrescriptionOrderLogic
|
||||
if ($auxName === '') {
|
||||
$auxName = $lookup('辅方', $auxHerbs);
|
||||
}
|
||||
// 有辅方药材但未匹配到库名/持久化名时,仍输出标记,避免导出名静默丢掉辅方
|
||||
if ($auxName === '') {
|
||||
$auxName = '辅方';
|
||||
}
|
||||
|
||||
return [$mainName, $auxName];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
-- 医生工作站视频接通后的实时录音转写。
|
||||
-- 转写摘要固化到每条通话记录,分段表用唯一键保证 WebChannel 重试幂等。
|
||||
|
||||
ALTER TABLE `zyt_tcm_call_record`
|
||||
ADD COLUMN `transcription_session_id` varchar(128) NOT NULL DEFAULT '' COMMENT '实时转写会话ID',
|
||||
ADD COLUMN `transcription_language` varchar(32) NOT NULL DEFAULT '' COMMENT '转写源语言',
|
||||
ADD COLUMN `transcription_status` varchar(16) NOT NULL DEFAULT '' COMMENT 'running/completed/partial/failed',
|
||||
ADD COLUMN `transcription_segment_count` int unsigned NOT NULL DEFAULT 0 COMMENT '已持久化完成分段数',
|
||||
ADD COLUMN `transcript_text` longtext NULL COMMENT '按时间和说话人生成的面诊对话文字',
|
||||
ADD COLUMN `transcription_started_at` int unsigned NOT NULL DEFAULT 0 COMMENT '转写开始Unix时间',
|
||||
ADD COLUMN `transcription_finished_at` int unsigned NOT NULL DEFAULT 0 COMMENT '转写完成Unix时间';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `zyt_tcm_call_transcript_segment` (
|
||||
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '分段ID',
|
||||
`call_record_id` int unsigned NOT NULL COMMENT '通话记录ID',
|
||||
`transcription_session_id` varchar(128) NOT NULL COMMENT '实时转写会话ID',
|
||||
`segment_id` varchar(160) NOT NULL COMMENT '腾讯云完成分段ID',
|
||||
`speaker_user_id` varchar(160) NOT NULL DEFAULT '' COMMENT '说话人TRTC用户ID',
|
||||
`speaker_role` varchar(20) NOT NULL DEFAULT 'unknown' COMMENT 'doctor/patient/unknown',
|
||||
`timestamp_ms` bigint unsigned NOT NULL DEFAULT 0 COMMENT '分段时间戳(毫秒)',
|
||||
`text` text NOT NULL COMMENT '完成分段文字,最多4000字符',
|
||||
`create_time` int unsigned NOT NULL DEFAULT 0,
|
||||
`update_time` int unsigned NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_call_session_segment` (`call_record_id`, `transcription_session_id`, `segment_id`),
|
||||
KEY `idx_call_session_time` (`call_record_id`, `transcription_session_id`, `timestamp_ms`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='视频面诊实时转写完成分段';
|
||||
@@ -0,0 +1,89 @@
|
||||
-- 实时转写接口继承现有“1v1通话”权限,避免未注册 action 被默认放行。
|
||||
|
||||
START TRANSACTION;
|
||||
|
||||
SET @video_call_menu_id := (
|
||||
SELECT `id`
|
||||
FROM `zyt_system_menu`
|
||||
WHERE `perms` = 'tcm.diagnosis/video-call'
|
||||
ORDER BY `id`
|
||||
LIMIT 1
|
||||
);
|
||||
|
||||
INSERT INTO `zyt_system_menu` (
|
||||
`pid`, `type`, `name`, `icon`, `sort`, `perms`, `paths`, `component`,
|
||||
`selected`, `params`, `is_cache`, `is_show`, `is_disable`, `create_time`, `update_time`
|
||||
)
|
||||
SELECT @video_call_menu_id, 'A', '启动通话转写', '', 1,
|
||||
'tcm.diagnosis/startCallTranscription', '', '', '', '', 0, 1, 0,
|
||||
UNIX_TIMESTAMP(), UNIX_TIMESTAMP()
|
||||
FROM DUAL
|
||||
WHERE @video_call_menu_id IS NOT NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM `zyt_system_menu`
|
||||
WHERE `perms` = 'tcm.diagnosis/startCallTranscription'
|
||||
);
|
||||
|
||||
INSERT INTO `zyt_system_menu` (
|
||||
`pid`, `type`, `name`, `icon`, `sort`, `perms`, `paths`, `component`,
|
||||
`selected`, `params`, `is_cache`, `is_show`, `is_disable`, `create_time`, `update_time`
|
||||
)
|
||||
SELECT @video_call_menu_id, 'A', '保存通话转写分段', '', 2,
|
||||
'tcm.diagnosis/upsertCallTranscriptSegments', '', '', '', '', 0, 1, 0,
|
||||
UNIX_TIMESTAMP(), UNIX_TIMESTAMP()
|
||||
FROM DUAL
|
||||
WHERE @video_call_menu_id IS NOT NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM `zyt_system_menu`
|
||||
WHERE `perms` = 'tcm.diagnosis/upsertCallTranscriptSegments'
|
||||
);
|
||||
|
||||
INSERT INTO `zyt_system_menu` (
|
||||
`pid`, `type`, `name`, `icon`, `sort`, `perms`, `paths`, `component`,
|
||||
`selected`, `params`, `is_cache`, `is_show`, `is_disable`, `create_time`, `update_time`
|
||||
)
|
||||
SELECT @video_call_menu_id, 'A', '完成通话转写', '', 3,
|
||||
'tcm.diagnosis/finishCallTranscription', '', '', '', '', 0, 1, 0,
|
||||
UNIX_TIMESTAMP(), UNIX_TIMESTAMP()
|
||||
FROM DUAL
|
||||
WHERE @video_call_menu_id IS NOT NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM `zyt_system_menu`
|
||||
WHERE `perms` = 'tcm.diagnosis/finishCallTranscription'
|
||||
);
|
||||
|
||||
SET @transcription_start_menu_id := (
|
||||
SELECT `id` FROM `zyt_system_menu`
|
||||
WHERE `perms` = 'tcm.diagnosis/startCallTranscription'
|
||||
ORDER BY `id` LIMIT 1
|
||||
);
|
||||
SET @transcription_segment_menu_id := (
|
||||
SELECT `id` FROM `zyt_system_menu`
|
||||
WHERE `perms` = 'tcm.diagnosis/upsertCallTranscriptSegments'
|
||||
ORDER BY `id` LIMIT 1
|
||||
);
|
||||
SET @transcription_finish_menu_id := (
|
||||
SELECT `id` FROM `zyt_system_menu`
|
||||
WHERE `perms` = 'tcm.diagnosis/finishCallTranscription'
|
||||
ORDER BY `id` LIMIT 1
|
||||
);
|
||||
|
||||
INSERT IGNORE INTO `zyt_system_role_menu` (`role_id`, `menu_id`)
|
||||
SELECT `role_id`, @transcription_start_menu_id
|
||||
FROM `zyt_system_role_menu`
|
||||
WHERE `menu_id` = @video_call_menu_id
|
||||
AND @transcription_start_menu_id IS NOT NULL;
|
||||
|
||||
INSERT IGNORE INTO `zyt_system_role_menu` (`role_id`, `menu_id`)
|
||||
SELECT `role_id`, @transcription_segment_menu_id
|
||||
FROM `zyt_system_role_menu`
|
||||
WHERE `menu_id` = @video_call_menu_id
|
||||
AND @transcription_segment_menu_id IS NOT NULL;
|
||||
|
||||
INSERT IGNORE INTO `zyt_system_role_menu` (`role_id`, `menu_id`)
|
||||
SELECT `role_id`, @transcription_finish_menu_id
|
||||
FROM `zyt_system_role_menu`
|
||||
WHERE `menu_id` = @video_call_menu_id
|
||||
AND @transcription_finish_menu_id IS NOT NULL;
|
||||
|
||||
COMMIT;
|
||||
+1
-1
@@ -1 +1 @@
|
||||
import t from"./error-Db3RUzsa.js";import{o,q as r,r as a,v as n,D as c,s}from"./.pnpm-C1KTa5cB.js";import"./index-Cxx1IoGl.js";const p="/admin/assets/no_perms-jDxcYpYC.png",i={class:"error404"},x=o({__name:"403",setup(m){return(_,e)=>(r(),a("div",i,[n(t,{code:"403",title:"您的账号权限不足,请联系管理员添加权限!","show-btn":!1},{content:c(()=>[...e[0]||(e[0]=[s("div",{class:"flex justify-center"},[s("img",{class:"w-[150px] h-[150px]",src:p,alt:""})],-1)])]),_:1})]))}});export{x as default};
|
||||
import t from"./error-AoNr5fax.js";import{o,q as r,r as a,v as n,D as c,s}from"./.pnpm-C1KTa5cB.js";import"./index-BzVi6Aa3.js";const p="/admin/assets/no_perms-jDxcYpYC.png",i={class:"error404"},x=o({__name:"403",setup(m){return(_,e)=>(r(),a("div",i,[n(t,{code:"403",title:"您的账号权限不足,请联系管理员添加权限!","show-btn":!1},{content:c(()=>[...e[0]||(e[0]=[s("div",{class:"flex justify-center"},[s("img",{class:"w-[150px] h-[150px]",src:p,alt:""})],-1)])]),_:1})]))}});export{x as default};
|
||||
+1
-1
@@ -1 +1 @@
|
||||
import e from"./error-Db3RUzsa.js";import{o,q as r,r as t,v as s}from"./.pnpm-C1KTa5cB.js";import"./index-Cxx1IoGl.js";const a={class:"error404"},d=o({__name:"404",setup(c){return(n,_)=>(r(),t("div",a,[s(e,{code:"404",title:"哎呀,出错了!您访问的页面不存在…"})]))}});export{d as default};
|
||||
import e from"./error-AoNr5fax.js";import{o,q as r,r as t,v as s}from"./.pnpm-C1KTa5cB.js";import"./index-BzVi6Aa3.js";const a={class:"error404"},d=o({__name:"404",setup(c){return(n,_)=>(r(),t("div",a,[s(e,{code:"404",title:"哎呀,出错了!您访问的页面不存在…"})]))}});export{d as default};
|
||||
+1
-1
@@ -1 +1 @@
|
||||
import{o as C,R as D,q as d,r as I,ac as N,O as u,bj as B,D as o,v as l,bk as E,br as L,L as i,T as r,s as p,bi as R,M as w}from"./.pnpm-C1KTa5cB.js";import{a as V}from"./doctor-fOWXzRlL.js";import{m as A,_ as M}from"./index-Cxx1IoGl.js";const P={class:"appointment-record-panel"},$={class:"cell-stack"},j={class:"font-medium"},q={class:"text-gray-500 text-sm"},O={class:"cell-stack"},F={class:"text-gray-500 text-sm"},G=C({__name:"AppointmentRecordPanel",props:{diagnosisId:{}},setup(v,{expose:y}){const f=v,m=w(!1),g=w([]),h=w({});function x(t){return t?String(t).length>=8?String(t).slice(0,5):t:""}function k(t){return t==="morning"?"上午":t==="afternoon"?"下午":t==="all"?"全天":t||"—"}function S(t){const n=t.channel_source??t.channels??"";if(n===""||n===null||n===void 0)return"—";const a=String(n),s=h.value[a]||a,c=String(t.channel_source_detail??"").trim();return c!==""?`${s}(${c})`:s}function T(t){return[...t].sort((n,a)=>{const s=String(n.appointment_date||""),c=String(a.appointment_date||"");if(s!==c)return c.localeCompare(s);const _=String(n.appointment_time||""),e=String(a.appointment_time||"");return _!==e?e.localeCompare(_):Number(a.id||0)-Number(n.id||0)})}const z=async()=>{try{const t=await A({type:"channels"}),n=((t==null?void 0:t.channels)||[]).filter(s=>s.status!==0),a={};for(const s of n)s.value!=null&&(a[String(s.value)]=s.name||String(s.value));h.value=a}catch{h.value={}}},b=async()=>{if(f.diagnosisId){m.value=!0;try{await z();const t=await V({patient_id:f.diagnosisId,diag_scope_relax:1,page_no:1,page_size:500}),n=(t==null?void 0:t.lists)||[];g.value=T(n)}catch(t){console.error(t),g.value=[]}finally{m.value=!1}}};return D(()=>f.diagnosisId,()=>{b()},{immediate:!0}),y({refresh:b}),(t,n)=>{const a=E,s=L,c=B,_=R;return d(),I("div",P,[N((d(),u(c,{data:g.value,border:"",stripe:"","empty-text":"暂无挂号记录"},{default:o(()=>[l(a,{label:"ID",prop:"id",width:"72",align:"center"}),l(a,{label:"状态",width:"100",align:"center"},{default:o(({row:e})=>[l(s,{type:e.status===1?"success":e.status===2?"info":e.status===3?"primary":"danger",size:"small",effect:"light"},{default:o(()=>[i(r(e.status_desc||"—"),1)]),_:2},1032,["type"])]),_:1}),l(a,{label:"患者(挂号人)","min-width":"150"},{default:o(({row:e})=>[p("div",$,[p("span",j,r(e.patient_name||"—"),1),p("span",q,r(e.patient_phone||""),1)])]),_:1}),l(a,{label:"挂号医生",prop:"doctor_name",width:"110","show-overflow-tooltip":""}),l(a,{label:"挂号助理",width:"110","show-overflow-tooltip":""},{default:o(({row:e})=>[i(r(e.assistant_name||"—"),1)]),_:1}),l(a,{label:"预约时间","min-width":"130"},{default:o(({row:e})=>[p("div",O,[p("span",null,r(e.appointment_date||"—"),1),p("span",F,r(x(e.appointment_time)),1)])]),_:1}),l(a,{label:"时段",width:"80",align:"center"},{default:o(({row:e})=>[i(r(k(e.period)),1)]),_:1}),l(a,{label:"类型",width:"100","show-overflow-tooltip":""},{default:o(({row:e})=>[i(r(e.appointment_type_desc||"—"),1)]),_:1}),l(a,{label:"渠道",width:"110","show-overflow-tooltip":""},{default:o(({row:e})=>[i(r(S(e)),1)]),_:1}),l(a,{label:"确认诊单",width:"92",align:"center"},{default:o(({row:e})=>[e.diagnosis_confirmed?(d(),u(s,{key:0,type:"success",size:"small",effect:"plain"},{default:o(()=>[...n[0]||(n[0]=[i("已确认",-1)])]),_:1})):(d(),u(s,{key:1,type:"warning",size:"small",effect:"plain"},{default:o(()=>[...n[1]||(n[1]=[i("未确认",-1)])]),_:1}))]),_:1}),l(a,{label:"开方",width:"80",align:"center"},{default:o(({row:e})=>[e.has_prescription?(d(),u(s,{key:0,type:"success",size:"small",effect:"plain"},{default:o(()=>[...n[2]||(n[2]=[i("已开方",-1)])]),_:1})):(d(),u(s,{key:1,type:"info",size:"small",effect:"plain"},{default:o(()=>[...n[3]||(n[3]=[i("未开方",-1)])]),_:1}))]),_:1}),l(a,{label:"备注",prop:"remark","min-width":"100","show-overflow-tooltip":""}),l(a,{label:"创建时间",width:"165",prop:"create_time"})]),_:1},8,["data"])),[[_,m.value]])])}}}),Q=M(G,[["__scopeId","data-v-4de87dfa"]]);export{Q as default};
|
||||
import{o as C,R as D,q as d,r as I,ac as N,O as u,bj as B,D as o,v as l,bk as E,br as L,L as i,T as r,s as p,bi as R,M as w}from"./.pnpm-C1KTa5cB.js";import{a as V}from"./doctor-HO-u4a0b.js";import{m as A,_ as M}from"./index-BzVi6Aa3.js";const P={class:"appointment-record-panel"},$={class:"cell-stack"},j={class:"font-medium"},q={class:"text-gray-500 text-sm"},O={class:"cell-stack"},F={class:"text-gray-500 text-sm"},G=C({__name:"AppointmentRecordPanel",props:{diagnosisId:{}},setup(v,{expose:y}){const f=v,m=w(!1),g=w([]),h=w({});function x(t){return t?String(t).length>=8?String(t).slice(0,5):t:""}function k(t){return t==="morning"?"上午":t==="afternoon"?"下午":t==="all"?"全天":t||"—"}function S(t){const n=t.channel_source??t.channels??"";if(n===""||n===null||n===void 0)return"—";const a=String(n),s=h.value[a]||a,c=String(t.channel_source_detail??"").trim();return c!==""?`${s}(${c})`:s}function T(t){return[...t].sort((n,a)=>{const s=String(n.appointment_date||""),c=String(a.appointment_date||"");if(s!==c)return c.localeCompare(s);const _=String(n.appointment_time||""),e=String(a.appointment_time||"");return _!==e?e.localeCompare(_):Number(a.id||0)-Number(n.id||0)})}const z=async()=>{try{const t=await A({type:"channels"}),n=((t==null?void 0:t.channels)||[]).filter(s=>s.status!==0),a={};for(const s of n)s.value!=null&&(a[String(s.value)]=s.name||String(s.value));h.value=a}catch{h.value={}}},b=async()=>{if(f.diagnosisId){m.value=!0;try{await z();const t=await V({patient_id:f.diagnosisId,diag_scope_relax:1,page_no:1,page_size:500}),n=(t==null?void 0:t.lists)||[];g.value=T(n)}catch(t){console.error(t),g.value=[]}finally{m.value=!1}}};return D(()=>f.diagnosisId,()=>{b()},{immediate:!0}),y({refresh:b}),(t,n)=>{const a=E,s=L,c=B,_=R;return d(),I("div",P,[N((d(),u(c,{data:g.value,border:"",stripe:"","empty-text":"暂无挂号记录"},{default:o(()=>[l(a,{label:"ID",prop:"id",width:"72",align:"center"}),l(a,{label:"状态",width:"100",align:"center"},{default:o(({row:e})=>[l(s,{type:e.status===1?"success":e.status===2?"info":e.status===3?"primary":"danger",size:"small",effect:"light"},{default:o(()=>[i(r(e.status_desc||"—"),1)]),_:2},1032,["type"])]),_:1}),l(a,{label:"患者(挂号人)","min-width":"150"},{default:o(({row:e})=>[p("div",$,[p("span",j,r(e.patient_name||"—"),1),p("span",q,r(e.patient_phone||""),1)])]),_:1}),l(a,{label:"挂号医生",prop:"doctor_name",width:"110","show-overflow-tooltip":""}),l(a,{label:"挂号助理",width:"110","show-overflow-tooltip":""},{default:o(({row:e})=>[i(r(e.assistant_name||"—"),1)]),_:1}),l(a,{label:"预约时间","min-width":"130"},{default:o(({row:e})=>[p("div",O,[p("span",null,r(e.appointment_date||"—"),1),p("span",F,r(x(e.appointment_time)),1)])]),_:1}),l(a,{label:"时段",width:"80",align:"center"},{default:o(({row:e})=>[i(r(k(e.period)),1)]),_:1}),l(a,{label:"类型",width:"100","show-overflow-tooltip":""},{default:o(({row:e})=>[i(r(e.appointment_type_desc||"—"),1)]),_:1}),l(a,{label:"渠道",width:"110","show-overflow-tooltip":""},{default:o(({row:e})=>[i(r(S(e)),1)]),_:1}),l(a,{label:"确认诊单",width:"92",align:"center"},{default:o(({row:e})=>[e.diagnosis_confirmed?(d(),u(s,{key:0,type:"success",size:"small",effect:"plain"},{default:o(()=>[...n[0]||(n[0]=[i("已确认",-1)])]),_:1})):(d(),u(s,{key:1,type:"warning",size:"small",effect:"plain"},{default:o(()=>[...n[1]||(n[1]=[i("未确认",-1)])]),_:1}))]),_:1}),l(a,{label:"开方",width:"80",align:"center"},{default:o(({row:e})=>[e.has_prescription?(d(),u(s,{key:0,type:"success",size:"small",effect:"plain"},{default:o(()=>[...n[2]||(n[2]=[i("已开方",-1)])]),_:1})):(d(),u(s,{key:1,type:"info",size:"small",effect:"plain"},{default:o(()=>[...n[3]||(n[3]=[i("未开方",-1)])]),_:1}))]),_:1}),l(a,{label:"备注",prop:"remark","min-width":"100","show-overflow-tooltip":""}),l(a,{label:"创建时间",width:"165",prop:"create_time"})]),_:1},8,["data"])),[[_,m.value]])])}}}),Q=M(G,[["__scopeId","data-v-4de87dfa"]]);export{Q as default};
|
||||
+1
-1
@@ -1 +1 @@
|
||||
import{o as T,R as C,q as _,r as b,ac as D,O as h,bj as $,D as i,v as n,bk as L,L as c,T as d,br as k,s as E,a1 as A,w as P,u as B,ci as F,bi as M,M as w}from"./.pnpm-C1KTa5cB.js";import{ae as V}from"./tcm-DJJMkqKG.js";import{_ as q}from"./index-Cxx1IoGl.js";const K={class:"assign-log-panel"},j={key:1,class:"text-gray-400"},z=T({__name:"AssignLogPanel",props:{diagnosisId:{}},setup(v,{expose:y}){const u=v,m=w(!1),p=w([]);function N(a){const e=a.related_po_creator_name;if(e!=null&&String(e).trim()!=="")return String(e);const t=Number(a.related_po_creator_id);return Number.isFinite(t)&&t>0?`ID:${t}`:"—"}function x(a){const e=a.related_po_create_time_text;if(e!=null&&String(e).trim()!=="")return String(e);const t=Number(a.related_po_create_time);if(!Number.isFinite(t)||t<=0)return"—";const s=new Date(t*1e3);if(Number.isNaN(s.getTime()))return"—";const o=l=>String(l).padStart(2,"0");return`${s.getFullYear()}-${o(s.getMonth()+1)}-${o(s.getDate())} ${o(s.getHours())}:${o(s.getMinutes())}:${o(s.getSeconds())}`}function f(a,e){const t=e==="from"?"from_assistant_name":"to_assistant_name",s=e==="from"?"from_assistant_id":"to_assistant_id",o=a[t];if(o!=null&&String(o).trim()!==""&&String(o)!=="—")return String(o);const l=Number(a[s]);return Number.isFinite(l)&&l>0?`ID:${l}`:"—"}const g=async()=>{if(u.diagnosisId){m.value=!0;try{const a=await V({id:u.diagnosisId}),e=Array.isArray(a)?a:[];p.value=e}catch(a){console.error(a),p.value=[]}finally{m.value=!1}}};return C(()=>u.diagnosisId,()=>{g()},{immediate:!0}),y({refresh:g}),(a,e)=>{const t=L,s=k,o=P,l=A,S=$,I=M;return _(),b("div",K,[D((_(),h(S,{data:p.value,border:"",stripe:"","empty-text":"暂无指派记录"},{default:i(()=>[n(t,{label:"操作时间",width:"175",prop:"create_time_text"}),n(t,{label:"原医助","min-width":"120"},{default:i(({row:r})=>[c(d(f(r,"from")),1)]),_:1}),n(t,{label:"新医助","min-width":"120"},{default:i(({row:r})=>[c(d(f(r,"to")),1)]),_:1}),n(t,{label:"继承",width:"72",align:"center"},{default:i(({row:r})=>[Number(r.is_inherit)===1?(_(),h(s,{key:0,type:"success",size:"small"},{default:i(()=>[...e[0]||(e[0]=[c("是",-1)])]),_:1})):(_(),b("span",j,"否"))]),_:1}),n(t,{label:"快照·业务单创建人","min-width":"130","show-overflow-tooltip":""},{default:i(({row:r})=>[c(d(N(r)),1)]),_:1}),n(t,{label:"快照·业务单创建时间",width:"190"},{header:i(()=>[e[1]||(e[1]=E("span",null,"快照·业务单创建时间",-1)),n(l,{placement:"top",content:"related_po_create_time:触发本次操作的处方业务订单 create_time;与原文助一致时表示医助创建订单时间"},{default:i(()=>[n(o,{class:"assign-log-col-hint"},{default:i(()=>[n(B(F))]),_:1})]),_:1})]),default:i(({row:r})=>[c(d(x(r)),1)]),_:1}),n(t,{label:"操作人",width:"110",prop:"operator_name"}),n(t,{label:"操作账号",width:"120",prop:"operator_account","show-overflow-tooltip":""}),n(t,{label:"IP",width:"130",prop:"ip","show-overflow-tooltip":""})]),_:1},8,["data"])),[[I,m.value]])])}}}),Y=q(z,[["__scopeId","data-v-f670e3e6"]]);export{Y as default};
|
||||
import{o as T,R as C,q as _,r as b,ac as D,O as h,bj as $,D as i,v as n,bk as L,L as c,T as d,br as k,s as E,a1 as A,w as P,u as B,ci as F,bi as M,M as w}from"./.pnpm-C1KTa5cB.js";import{ae as V}from"./tcm-BlzCcc0R.js";import{_ as q}from"./index-BzVi6Aa3.js";const K={class:"assign-log-panel"},j={key:1,class:"text-gray-400"},z=T({__name:"AssignLogPanel",props:{diagnosisId:{}},setup(v,{expose:y}){const u=v,m=w(!1),p=w([]);function N(a){const e=a.related_po_creator_name;if(e!=null&&String(e).trim()!=="")return String(e);const t=Number(a.related_po_creator_id);return Number.isFinite(t)&&t>0?`ID:${t}`:"—"}function x(a){const e=a.related_po_create_time_text;if(e!=null&&String(e).trim()!=="")return String(e);const t=Number(a.related_po_create_time);if(!Number.isFinite(t)||t<=0)return"—";const s=new Date(t*1e3);if(Number.isNaN(s.getTime()))return"—";const o=l=>String(l).padStart(2,"0");return`${s.getFullYear()}-${o(s.getMonth()+1)}-${o(s.getDate())} ${o(s.getHours())}:${o(s.getMinutes())}:${o(s.getSeconds())}`}function f(a,e){const t=e==="from"?"from_assistant_name":"to_assistant_name",s=e==="from"?"from_assistant_id":"to_assistant_id",o=a[t];if(o!=null&&String(o).trim()!==""&&String(o)!=="—")return String(o);const l=Number(a[s]);return Number.isFinite(l)&&l>0?`ID:${l}`:"—"}const g=async()=>{if(u.diagnosisId){m.value=!0;try{const a=await V({id:u.diagnosisId}),e=Array.isArray(a)?a:[];p.value=e}catch(a){console.error(a),p.value=[]}finally{m.value=!1}}};return C(()=>u.diagnosisId,()=>{g()},{immediate:!0}),y({refresh:g}),(a,e)=>{const t=L,s=k,o=P,l=A,S=$,I=M;return _(),b("div",K,[D((_(),h(S,{data:p.value,border:"",stripe:"","empty-text":"暂无指派记录"},{default:i(()=>[n(t,{label:"操作时间",width:"175",prop:"create_time_text"}),n(t,{label:"原医助","min-width":"120"},{default:i(({row:r})=>[c(d(f(r,"from")),1)]),_:1}),n(t,{label:"新医助","min-width":"120"},{default:i(({row:r})=>[c(d(f(r,"to")),1)]),_:1}),n(t,{label:"继承",width:"72",align:"center"},{default:i(({row:r})=>[Number(r.is_inherit)===1?(_(),h(s,{key:0,type:"success",size:"small"},{default:i(()=>[...e[0]||(e[0]=[c("是",-1)])]),_:1})):(_(),b("span",j,"否"))]),_:1}),n(t,{label:"快照·业务单创建人","min-width":"130","show-overflow-tooltip":""},{default:i(({row:r})=>[c(d(N(r)),1)]),_:1}),n(t,{label:"快照·业务单创建时间",width:"190"},{header:i(()=>[e[1]||(e[1]=E("span",null,"快照·业务单创建时间",-1)),n(l,{placement:"top",content:"related_po_create_time:触发本次操作的处方业务订单 create_time;与原文助一致时表示医助创建订单时间"},{default:i(()=>[n(o,{class:"assign-log-col-hint"},{default:i(()=>[n(B(F))]),_:1})]),_:1})]),default:i(({row:r})=>[c(d(x(r)),1)]),_:1}),n(t,{label:"操作人",width:"110",prop:"operator_name"}),n(t,{label:"操作账号",width:"120",prop:"operator_account","show-overflow-tooltip":""}),n(t,{label:"IP",width:"130",prop:"ip","show-overflow-tooltip":""})]),_:1},8,["data"])),[[I,m.value]])])}}}),Y=q(z,[["__scopeId","data-v-f670e3e6"]]);export{Y as default};
|
||||
+1
-1
@@ -1 +1 @@
|
||||
import{o as B,R as D,q as E,O,D as I,r as _,T as S,P as x,ac as W,s as h,ad as P,v as U,K as $,L as j,bt as H,p as K,M as v,dg as c}from"./.pnpm-C1KTa5cB.js";import{af as Y}from"./tcm-DJJMkqKG.js";import{_ as q}from"./index-Cxx1IoGl.js";const z={key:0,class:"watch-state"},F={key:1,class:"watch-state watch-error"},G=B({__name:"AssistantWatchCallDialog",props:{modelValue:{type:Boolean},diagnosisId:{}},emits:["update:modelValue","closed"],setup(R,{emit:A}){const u=R,g=A,y=K({get:()=>u.modelValue,set:t=>g("update:modelValue",t)}),r=v(null),l=v(!1),i=v(""),f=v("旁观视频通话"),n=new Map;let a=null,p=0;function w(t,e){return`${t}\0${String(e)}`}function N(t){return t.startsWith("patient_")?"患者":t.startsWith("doctor_")?"医护":t}async function T(t){if(!a||!r.value||t.streamType!==c.TYPE.STREAM_TYPE_MAIN)return;const e=w(t.userId,t.streamType);if(n.has(e))return;const o=document.createElement("div");o.className="watch-tile";const s=document.createElement("div");s.className="watch-tile-cap",s.textContent=N(t.userId);const d=document.createElement("div");d.className="watch-tile-view",o.appendChild(s),o.appendChild(d),r.value.appendChild(o),n.set(e,{wrap:o,userId:t.userId,streamType:t.streamType});try{await a.startRemoteVideo({userId:t.userId,streamType:t.streamType,view:d})}catch(b){console.warn("[AssistantWatchCall] startRemoteVideo",b)}}async function V(t){if(!a)return;const e=w(t.userId,t.streamType),o=n.get(e);if(o){try{await a.stopRemoteVideo({userId:t.userId,streamType:t.streamType})}catch{}o.wrap.remove(),n.delete(e)}}function C(){a&&(a.on(c.EVENT.REMOTE_VIDEO_AVAILABLE,T),a.on(c.EVENT.REMOTE_VIDEO_UNAVAILABLE,V))}function k(){a&&(a.off(c.EVENT.REMOTE_VIDEO_AVAILABLE,T),a.off(c.EVENT.REMOTE_VIDEO_UNAVAILABLE,V))}async function m(){if(k(),a){for(const[,t]of n){try{await a.stopRemoteVideo({userId:t.userId,streamType:t.streamType})}catch{}t.wrap.remove()}n.clear(),r.value&&(r.value.innerHTML="");try{await a.exitRoom()}catch{}try{a.destroy()}catch{}a=null}else n.clear(),r.value&&(r.value.innerHTML="")}async function L(){const t=++p;if(await m(),!u.diagnosisId){i.value="诊单无效";return}l.value=!0,i.value="",f.value="旁观视频通话";try{const e=await Y({diagnosis_id:u.diagnosisId});if(t!==p)return;e.patientName&&(f.value=`旁观视频通话 · ${e.patientName}`),a=c.create(),C();const o={sdkAppId:e.sdkAppId,userId:e.userId,userSig:e.userSig,autoReceiveAudio:!0,autoReceiveVideo:!0,...e.roomId!=null&&e.roomId>0?{roomId:e.roomId}:{strRoomId:e.strRoomId}};if(!(e.roomId!=null&&e.roomId>0)&&!e.strRoomId)throw new Error("缺少房间号");if(await a.enterRoom(o),t!==p){await m();return}l.value=!1}catch(e){l.value=!1;let o="进入房间失败";if(typeof e=="string")o=e;else if(e&&typeof e=="object"){const s=e;s.msg?o=String(s.msg):s.message&&(o=String(s.message))}i.value=o,await m()}}function M(){m(),l.value=!1,i.value="",f.value="旁观视频通话",g("closed")}return D(()=>[u.modelValue,u.diagnosisId],([t,e])=>{if(!t){p++,m();return}e>0&&L()}),(t,e)=>{const o=$,s=H;return E(),O(s,{modelValue:y.value,"onUpdate:modelValue":e[1]||(e[1]=d=>y.value=d),title:f.value,width:"760px","destroy-on-close":"","append-to-body":"","close-on-click-modal":!1,class:"assistant-watch-call-dialog",onClosed:M},{footer:I(()=>[e[3]||(e[3]=h("span",{class:"watch-hint"},"仅观看,不会开启摄像头与麦克风",-1)),U(o,{type:"primary",onClick:e[0]||(e[0]=d=>y.value=!1)},{default:I(()=>[...e[2]||(e[2]=[j("离开",-1)])]),_:1})]),default:I(()=>[l.value?(E(),_("div",z,"正在连接房间…")):i.value?(E(),_("div",F,S(i.value),1)):x("",!0),W(h("div",{ref_key:"gridRef",ref:r,class:"watch-grid"},null,512),[[P,!l.value&&!i.value]])]),_:1},8,["modelValue","title"])}}}),Z=q(G,[["__scopeId","data-v-7aff665d"]]);export{Z as default};
|
||||
import{o as B,R as D,q as E,O,D as I,r as _,T as S,P as x,ac as W,s as h,ad as P,v as U,K as $,L as j,bt as H,p as K,M as v,dg as c}from"./.pnpm-C1KTa5cB.js";import{af as Y}from"./tcm-BlzCcc0R.js";import{_ as q}from"./index-BzVi6Aa3.js";const z={key:0,class:"watch-state"},F={key:1,class:"watch-state watch-error"},G=B({__name:"AssistantWatchCallDialog",props:{modelValue:{type:Boolean},diagnosisId:{}},emits:["update:modelValue","closed"],setup(R,{emit:A}){const u=R,g=A,y=K({get:()=>u.modelValue,set:t=>g("update:modelValue",t)}),r=v(null),l=v(!1),i=v(""),f=v("旁观视频通话"),n=new Map;let a=null,p=0;function w(t,e){return`${t}\0${String(e)}`}function N(t){return t.startsWith("patient_")?"患者":t.startsWith("doctor_")?"医护":t}async function T(t){if(!a||!r.value||t.streamType!==c.TYPE.STREAM_TYPE_MAIN)return;const e=w(t.userId,t.streamType);if(n.has(e))return;const o=document.createElement("div");o.className="watch-tile";const s=document.createElement("div");s.className="watch-tile-cap",s.textContent=N(t.userId);const d=document.createElement("div");d.className="watch-tile-view",o.appendChild(s),o.appendChild(d),r.value.appendChild(o),n.set(e,{wrap:o,userId:t.userId,streamType:t.streamType});try{await a.startRemoteVideo({userId:t.userId,streamType:t.streamType,view:d})}catch(b){console.warn("[AssistantWatchCall] startRemoteVideo",b)}}async function V(t){if(!a)return;const e=w(t.userId,t.streamType),o=n.get(e);if(o){try{await a.stopRemoteVideo({userId:t.userId,streamType:t.streamType})}catch{}o.wrap.remove(),n.delete(e)}}function C(){a&&(a.on(c.EVENT.REMOTE_VIDEO_AVAILABLE,T),a.on(c.EVENT.REMOTE_VIDEO_UNAVAILABLE,V))}function k(){a&&(a.off(c.EVENT.REMOTE_VIDEO_AVAILABLE,T),a.off(c.EVENT.REMOTE_VIDEO_UNAVAILABLE,V))}async function m(){if(k(),a){for(const[,t]of n){try{await a.stopRemoteVideo({userId:t.userId,streamType:t.streamType})}catch{}t.wrap.remove()}n.clear(),r.value&&(r.value.innerHTML="");try{await a.exitRoom()}catch{}try{a.destroy()}catch{}a=null}else n.clear(),r.value&&(r.value.innerHTML="")}async function L(){const t=++p;if(await m(),!u.diagnosisId){i.value="诊单无效";return}l.value=!0,i.value="",f.value="旁观视频通话";try{const e=await Y({diagnosis_id:u.diagnosisId});if(t!==p)return;e.patientName&&(f.value=`旁观视频通话 · ${e.patientName}`),a=c.create(),C();const o={sdkAppId:e.sdkAppId,userId:e.userId,userSig:e.userSig,autoReceiveAudio:!0,autoReceiveVideo:!0,...e.roomId!=null&&e.roomId>0?{roomId:e.roomId}:{strRoomId:e.strRoomId}};if(!(e.roomId!=null&&e.roomId>0)&&!e.strRoomId)throw new Error("缺少房间号");if(await a.enterRoom(o),t!==p){await m();return}l.value=!1}catch(e){l.value=!1;let o="进入房间失败";if(typeof e=="string")o=e;else if(e&&typeof e=="object"){const s=e;s.msg?o=String(s.msg):s.message&&(o=String(s.message))}i.value=o,await m()}}function M(){m(),l.value=!1,i.value="",f.value="旁观视频通话",g("closed")}return D(()=>[u.modelValue,u.diagnosisId],([t,e])=>{if(!t){p++,m();return}e>0&&L()}),(t,e)=>{const o=$,s=H;return E(),O(s,{modelValue:y.value,"onUpdate:modelValue":e[1]||(e[1]=d=>y.value=d),title:f.value,width:"760px","destroy-on-close":"","append-to-body":"","close-on-click-modal":!1,class:"assistant-watch-call-dialog",onClosed:M},{footer:I(()=>[e[3]||(e[3]=h("span",{class:"watch-hint"},"仅观看,不会开启摄像头与麦克风",-1)),U(o,{type:"primary",onClick:e[0]||(e[0]=d=>y.value=!1)},{default:I(()=>[...e[2]||(e[2]=[j("离开",-1)])]),_:1})]),default:I(()=>[l.value?(E(),_("div",z,"正在连接房间…")):i.value?(E(),_("div",F,S(i.value),1)):x("",!0),W(h("div",{ref_key:"gridRef",ref:r,class:"watch-grid"},null,512),[[P,!l.value&&!i.value]])]),_:1},8,["modelValue","title"])}}}),Z=q(G,[["__scopeId","data-v-7aff665d"]]);export{Z as default};
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
@@ -1 +1 @@
|
||||
import{o as N,di as O,R as P,q as d,r as f,v as o,D as i,K as V,L as n,P as h,ac as D,O as w,bj as L,bk as j,T as u,s as y,bi as z,M as v}from"./.pnpm-C1KTa5cB.js";import M from"./RecordingPlaybackBlock-BLiZhM1Y.js";import{U as k}from"./index-CZyPOnHt.js";import{i as c,_ as q}from"./index-Cxx1IoGl.js";import{aj as K,ak as x,al as A}from"./tcm-DJJMkqKG.js";import"./RecordingVideoPlayer-ByTd0pZe.js";import"./file-D2AwthSZ.js";const F={class:"call-record-panel"},G={key:0,class:"call-record-toolbar"},H={class:"call-record-empty"},J={class:"call-record-empty__desc"},Q={key:0,class:"text-primary"},W={key:1,class:"text-gray-400"},X=N({__name:"CallRecordPanel",props:{diagnosisId:{},readOnly:{type:Boolean,default:!1}},setup(_,{expose:R}){const r=_,p=v(!1),g=v([]),U=O("toolbarUploadRef"),m=async()=>{if(r.diagnosisId){p.value=!0;try{g.value=await K({diagnosis_id:r.diagnosisId})||[]}catch(e){console.error(e),g.value=[]}finally{p.value=!1}}};P(()=>r.diagnosisId,()=>{m()},{immediate:!0}),R({refresh:m});function C(e){return{1:"进行中",2:"已结束",3:"未接听",4:"已取消"}[e]??"—"}async function S(e){const t=b(e);if(!t){c.msgError("上传成功但未返回视频地址");return}try{const a=await A({diagnosis_id:r.diagnosisId});await x({diagnosis_id:r.diagnosisId,call_record_id:Number((a==null?void 0:a.id)||0),file_url:t}),c.msgSuccess("视频回放上传成功"),await m()}catch(a){c.msgError((a==null?void 0:a.message)||"写入回放失败")}}async function E(e,t){const a=b(t);if(!a){c.msgError("上传成功但未返回视频地址");return}try{await x({diagnosis_id:r.diagnosisId,call_record_id:Number(e.id||0),file_url:a}),c.msgSuccess("视频回放上传成功"),await m()}catch(l){c.msgError((l==null?void 0:l.message)||"写入回放失败")}}function b(e){var t,a;return String(((t=e==null?void 0:e.data)==null?void 0:t.uri)||((a=e==null?void 0:e.data)==null?void 0:a.url)||"").trim()}return(e,t)=>{const a=V,l=j,I=L,B=z;return d(),f("div",F,[_.readOnly?h("",!0):(d(),f("div",G,[o(k,{ref_key:"toolbarUploadRef",ref:U,type:"video",direct:"",multiple:!1,limit:1,"show-progress":!0,onSuccess:S},{default:i(()=>[o(a,{type:"primary"},{default:i(()=>[...t[0]||(t[0]=[n("上传视频",-1)])]),_:1})]),_:1},512)])),D((d(),w(I,{data:g.value,border:"",stripe:""},{empty:i(()=>[y("div",H,[t[1]||(t[1]=y("div",{class:"call-record-empty__title"},"暂无通话记录",-1)),y("div",J,u(_.readOnly?"暂无录制回放数据。":"现在可以直接点击上方“上传视频”。系统会自动生成一条默认通话记录来承载回放。"),1)])]),default:i(()=>[o(l,{label:"录制回放","min-width":"320"},{default:i(({row:s})=>[o(M,{"record-id":s.id,urls:s.recording_urls_list},null,8,["record-id","urls"])]),_:1}),o(l,{label:"开始时间",width:"170",prop:"start_time_text"}),o(l,{label:"结束时间",width:"170",prop:"end_time_text"}),o(l,{label:"通话类型",width:"100"},{default:i(({row:s})=>[n(u(s.call_type===1?"语音":"视频"),1)]),_:1}),o(l,{label:"房间号",width:"180"},{default:i(({row:s})=>[s.room_id?(d(),f("span",Q,u(s.room_id),1)):(d(),f("span",W,"—"))]),_:1}),o(l,{label:"时长",width:"110",prop:"duration_text"}),o(l,{label:"状态",width:"90"},{default:i(({row:s})=>[n(u(C(s.status)),1)]),_:1}),o(l,{label:"录制",width:"100"},{default:i(({row:s})=>[n(u(s.recording_status_text||"—"),1)]),_:1}),_.readOnly?h("",!0):(d(),w(l,{key:0,label:"上传回放",width:"180"},{default:i(({row:s})=>[o(k,{type:"video",direct:"",multiple:!1,limit:1,"show-progress":!0,onSuccess:T=>E(s,T)},{default:i(()=>[o(a,{type:"primary",plain:"",size:"small"},{default:i(()=>[...t[2]||(t[2]=[n("上传视频",-1)])]),_:1})]),_:1},8,["onSuccess"])]),_:1}))]),_:1},8,["data"])),[[B,p.value]])])}}}),sa=q(X,[["__scopeId","data-v-78d5c9e4"]]);export{sa as default};
|
||||
import{o as N,di as O,R as P,q as d,r as f,v as o,D as i,K as V,L as n,P as h,ac as D,O as w,bj as L,bk as j,T as u,s as y,bi as z,M as v}from"./.pnpm-C1KTa5cB.js";import M from"./RecordingPlaybackBlock-D_npN3pU.js";import{U as k}from"./index-CEYf_4x5.js";import{i as c,_ as q}from"./index-BzVi6Aa3.js";import{aj as K,ak as x,al as A}from"./tcm-BlzCcc0R.js";import"./RecordingVideoPlayer-Dgsrn-1d.js";import"./file-DqKLtehk.js";const F={class:"call-record-panel"},G={key:0,class:"call-record-toolbar"},H={class:"call-record-empty"},J={class:"call-record-empty__desc"},Q={key:0,class:"text-primary"},W={key:1,class:"text-gray-400"},X=N({__name:"CallRecordPanel",props:{diagnosisId:{},readOnly:{type:Boolean,default:!1}},setup(_,{expose:R}){const r=_,p=v(!1),g=v([]),U=O("toolbarUploadRef"),m=async()=>{if(r.diagnosisId){p.value=!0;try{g.value=await K({diagnosis_id:r.diagnosisId})||[]}catch(e){console.error(e),g.value=[]}finally{p.value=!1}}};P(()=>r.diagnosisId,()=>{m()},{immediate:!0}),R({refresh:m});function C(e){return{1:"进行中",2:"已结束",3:"未接听",4:"已取消"}[e]??"—"}async function S(e){const t=b(e);if(!t){c.msgError("上传成功但未返回视频地址");return}try{const a=await A({diagnosis_id:r.diagnosisId});await x({diagnosis_id:r.diagnosisId,call_record_id:Number((a==null?void 0:a.id)||0),file_url:t}),c.msgSuccess("视频回放上传成功"),await m()}catch(a){c.msgError((a==null?void 0:a.message)||"写入回放失败")}}async function E(e,t){const a=b(t);if(!a){c.msgError("上传成功但未返回视频地址");return}try{await x({diagnosis_id:r.diagnosisId,call_record_id:Number(e.id||0),file_url:a}),c.msgSuccess("视频回放上传成功"),await m()}catch(l){c.msgError((l==null?void 0:l.message)||"写入回放失败")}}function b(e){var t,a;return String(((t=e==null?void 0:e.data)==null?void 0:t.uri)||((a=e==null?void 0:e.data)==null?void 0:a.url)||"").trim()}return(e,t)=>{const a=V,l=j,I=L,B=z;return d(),f("div",F,[_.readOnly?h("",!0):(d(),f("div",G,[o(k,{ref_key:"toolbarUploadRef",ref:U,type:"video",direct:"",multiple:!1,limit:1,"show-progress":!0,onSuccess:S},{default:i(()=>[o(a,{type:"primary"},{default:i(()=>[...t[0]||(t[0]=[n("上传视频",-1)])]),_:1})]),_:1},512)])),D((d(),w(I,{data:g.value,border:"",stripe:""},{empty:i(()=>[y("div",H,[t[1]||(t[1]=y("div",{class:"call-record-empty__title"},"暂无通话记录",-1)),y("div",J,u(_.readOnly?"暂无录制回放数据。":"现在可以直接点击上方“上传视频”。系统会自动生成一条默认通话记录来承载回放。"),1)])]),default:i(()=>[o(l,{label:"录制回放","min-width":"320"},{default:i(({row:s})=>[o(M,{"record-id":s.id,urls:s.recording_urls_list},null,8,["record-id","urls"])]),_:1}),o(l,{label:"开始时间",width:"170",prop:"start_time_text"}),o(l,{label:"结束时间",width:"170",prop:"end_time_text"}),o(l,{label:"通话类型",width:"100"},{default:i(({row:s})=>[n(u(s.call_type===1?"语音":"视频"),1)]),_:1}),o(l,{label:"房间号",width:"180"},{default:i(({row:s})=>[s.room_id?(d(),f("span",Q,u(s.room_id),1)):(d(),f("span",W,"—"))]),_:1}),o(l,{label:"时长",width:"110",prop:"duration_text"}),o(l,{label:"状态",width:"90"},{default:i(({row:s})=>[n(u(C(s.status)),1)]),_:1}),o(l,{label:"录制",width:"100"},{default:i(({row:s})=>[n(u(s.recording_status_text||"—"),1)]),_:1}),_.readOnly?h("",!0):(d(),w(l,{key:0,label:"上传回放",width:"180"},{default:i(({row:s})=>[o(k,{type:"video",direct:"",multiple:!1,limit:1,"show-progress":!0,onSuccess:T=>E(s,T)},{default:i(()=>[o(a,{type:"primary",plain:"",size:"small"},{default:i(()=>[...t[2]||(t[2]=[n("上传视频",-1)])]),_:1})]),_:1},8,["onSuccess"])]),_:1}))]),_:1},8,["data"])),[[B,p.value]])])}}}),sa=q(X,[["__scopeId","data-v-78d5c9e4"]]);export{sa as default};
|
||||
+1
-1
@@ -1 +1 @@
|
||||
import{o as T,ap as V,R as I,q as o,r as l,v as a,K as N,D as i,L as c,P as k,ac as z,bi as M,u as p,O as f,bk as O,T as _,F as P,br as j,s as F,bj as R,bB as A,M as w}from"./.pnpm-C1KTa5cB.js";import{am as q}from"./tcm-DJJMkqKG.js";import{_ as H}from"./index-Cxx1IoGl.js";const K={class:"case-record-list"},Y={key:0,class:"mb-3 flex justify-end"},G={key:0},J={key:1,class:"text-gray-400"},Q={class:"void-detail text-xs text-gray-500 mt-1"},U=T({__name:"CaseRecordList",props:{diagnosisId:{type:Number,default:0},readOnly:{type:Boolean,default:!1}},emits:["view","openPrescription"],setup(y,{expose:x,emit:C}){const m=y,b=C,r=w([]),d=w(!1),u=async()=>{if(m.diagnosisId){d.value=!0;try{const t=await q({diagnosis_id:m.diagnosisId});r.value=Array.isArray(t)?t:[]}catch(t){console.error("获取病历记录失败:",t),r.value=[]}finally{d.value=!1}}},S=t=>{if(!t)return"";const e=new Date(t*1e3);return`${e.getFullYear()}-${String(e.getMonth()+1).padStart(2,"0")}-${String(e.getDate()).padStart(2,"0")} ${String(e.getHours()).padStart(2,"0")}:${String(e.getMinutes()).padStart(2,"0")}`},B=t=>{b("view",t)},$=()=>{b("openPrescription")};return V(()=>{u()}),I(()=>m.diagnosisId,()=>{u()}),x({refresh:u}),(t,e)=>{const h=N,n=O,v=j,E=R,D=A,L=M;return o(),l("div",K,[y.readOnly?k("",!0):(o(),l("div",Y,[a(h,{type:"primary",size:"small",onClick:$},{default:i(()=>[...e[0]||(e[0]=[c("开方",-1)])]),_:1})])),z((o(),f(E,{data:p(r),border:""},{default:i(()=>[a(n,{prop:"prescription_date",label:"就诊日期",width:"120"}),a(n,{prop:"visit_no",label:"门诊号",width:"120"}),a(n,{prop:"clinical_diagnosis",label:"临床诊断","min-width":"160","show-overflow-tooltip":""}),a(n,{label:"处方摘要","min-width":"180"},{default:i(({row:s})=>[s.herbs&&s.herbs.length?(o(),l("span",G,_(s.herbs.slice(0,3).map(g=>`${g.name}${g.dosage}克`).join("、"))+_(s.herbs.length>3?"...":""),1)):(o(),l("span",J,"—"))]),_:1}),a(n,{prop:"doctor_name",label:"医师",width:"90","show-overflow-tooltip":""}),a(n,{label:"状态",width:"140",align:"center"},{default:i(({row:s})=>[s.void_status===1?(o(),l(P,{key:0},[a(v,{type:"danger",size:"small"},{default:i(()=>[...e[1]||(e[1]=[c("已作废",-1)])]),_:1}),F("div",Q,_(s.void_by_name||"—")+" "+_(S(s.void_time)),1)],64)):(o(),f(v,{key:1,type:"success",size:"small"},{default:i(()=>[...e[2]||(e[2]=[c("正常",-1)])]),_:1}))]),_:1}),a(n,{label:"操作",width:"120",fixed:"right"},{default:i(({row:s})=>[a(h,{link:"",type:"primary",size:"small",onClick:g=>B(s)},{default:i(()=>[...e[3]||(e[3]=[c(" 查看 ",-1)])]),_:1},8,["onClick"])]),_:1})]),_:1},8,["data"])),[[L,p(d)]]),!p(d)&&p(r).length===0?(o(),f(D,{key:1,description:"暂无病历记录,开方后会自动显示",class:"mt-4"})):k("",!0)])}}}),ee=H(U,[["__scopeId","data-v-043d2738"]]);export{ee as default};
|
||||
import{o as T,ap as V,R as I,q as o,r as l,v as a,K as N,D as i,L as c,P as k,ac as z,bi as M,u as p,O as f,bk as O,T as _,F as P,br as j,s as F,bj as R,bB as A,M as w}from"./.pnpm-C1KTa5cB.js";import{am as q}from"./tcm-BlzCcc0R.js";import{_ as H}from"./index-BzVi6Aa3.js";const K={class:"case-record-list"},Y={key:0,class:"mb-3 flex justify-end"},G={key:0},J={key:1,class:"text-gray-400"},Q={class:"void-detail text-xs text-gray-500 mt-1"},U=T({__name:"CaseRecordList",props:{diagnosisId:{type:Number,default:0},readOnly:{type:Boolean,default:!1}},emits:["view","openPrescription"],setup(y,{expose:x,emit:C}){const m=y,b=C,r=w([]),d=w(!1),u=async()=>{if(m.diagnosisId){d.value=!0;try{const t=await q({diagnosis_id:m.diagnosisId});r.value=Array.isArray(t)?t:[]}catch(t){console.error("获取病历记录失败:",t),r.value=[]}finally{d.value=!1}}},S=t=>{if(!t)return"";const e=new Date(t*1e3);return`${e.getFullYear()}-${String(e.getMonth()+1).padStart(2,"0")}-${String(e.getDate()).padStart(2,"0")} ${String(e.getHours()).padStart(2,"0")}:${String(e.getMinutes()).padStart(2,"0")}`},B=t=>{b("view",t)},$=()=>{b("openPrescription")};return V(()=>{u()}),I(()=>m.diagnosisId,()=>{u()}),x({refresh:u}),(t,e)=>{const h=N,n=O,v=j,E=R,D=A,L=M;return o(),l("div",K,[y.readOnly?k("",!0):(o(),l("div",Y,[a(h,{type:"primary",size:"small",onClick:$},{default:i(()=>[...e[0]||(e[0]=[c("开方",-1)])]),_:1})])),z((o(),f(E,{data:p(r),border:""},{default:i(()=>[a(n,{prop:"prescription_date",label:"就诊日期",width:"120"}),a(n,{prop:"visit_no",label:"门诊号",width:"120"}),a(n,{prop:"clinical_diagnosis",label:"临床诊断","min-width":"160","show-overflow-tooltip":""}),a(n,{label:"处方摘要","min-width":"180"},{default:i(({row:s})=>[s.herbs&&s.herbs.length?(o(),l("span",G,_(s.herbs.slice(0,3).map(g=>`${g.name}${g.dosage}克`).join("、"))+_(s.herbs.length>3?"...":""),1)):(o(),l("span",J,"—"))]),_:1}),a(n,{prop:"doctor_name",label:"医师",width:"90","show-overflow-tooltip":""}),a(n,{label:"状态",width:"140",align:"center"},{default:i(({row:s})=>[s.void_status===1?(o(),l(P,{key:0},[a(v,{type:"danger",size:"small"},{default:i(()=>[...e[1]||(e[1]=[c("已作废",-1)])]),_:1}),F("div",Q,_(s.void_by_name||"—")+" "+_(S(s.void_time)),1)],64)):(o(),f(v,{key:1,type:"success",size:"small"},{default:i(()=>[...e[2]||(e[2]=[c("正常",-1)])]),_:1}))]),_:1}),a(n,{label:"操作",width:"120",fixed:"right"},{default:i(({row:s})=>[a(h,{link:"",type:"primary",size:"small",onClick:g=>B(s)},{default:i(()=>[...e[3]||(e[3]=[c(" 查看 ",-1)])]),_:1},8,["onClick"])]),_:1})]),_:1},8,["data"])),[[L,p(d)]]),!p(d)&&p(r).length===0?(o(),f(D,{key:1,description:"暂无病历记录,开方后会自动显示",class:"mt-4"})):k("",!0)])}}}),ee=H(U,[["__scopeId","data-v-043d2738"]]);export{ee as default};
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
import{_ as o}from"./GancaoSubmissionReconcileButton.vue_vue_type_script_setup_true_lang-DSizL7xy.js";import"./.pnpm-C1KTa5cB.js";import"./tcm-BlzCcc0R.js";import"./index-BzVi6Aa3.js";export{o as default};
|
||||
@@ -1 +0,0 @@
|
||||
import{_ as o}from"./GancaoSubmissionReconcileButton.vue_vue_type_script_setup_true_lang-CTGxGmsz.js";import"./.pnpm-C1KTa5cB.js";import"./tcm-DJJMkqKG.js";import"./index-Cxx1IoGl.js";export{o as default};
|
||||
+1
-1
@@ -1 +1 @@
|
||||
import{o as U,be as F,q as f,r as k,F as M,ac as D,O as E,D as t,L as u,K as G,v as r,bt as B,bh as q,b9 as P,aw as A,b6 as L,bc as T,bf as K,b7 as z,P as g,p as W,M as v,ba as $}from"./.pnpm-C1KTa5cB.js";import{p as j}from"./tcm-DJJMkqKG.js";import{i as C}from"./index-Cxx1IoGl.js";const X=U({__name:"GancaoSubmissionReconcileButton",props:{order:{}},emits:["resolved"],setup(N,{emit:w}){const d=N,y=w,V=W(()=>{var m,p,a;const i=String(((m=d.order)==null?void 0:m.pharmacy_claim_target)||"").trim().toLowerCase(),e=String(((p=d.order)==null?void 0:p.pharmacy_claim_status)||"").trim().toUpperCase(),n=Number(((a=d.order)==null?void 0:a.pharmacy_claim_lease_expires_at)||0),c=e==="PENDING"&&n>0&&n<=Math.floor(Date.now()/1e3);return i==="gancao"&&(c||["UNKNOWN","PENDING_RECONCILE"].includes(e))}),s=v(!1),_=v(!1),o=$({resolution:"CONFIRM_SUCCESS",remote_order_no:"",note:""});function O(){o.resolution="CONFIRM_SUCCESS",o.remote_order_no="",o.note="",s.value=!0}async function b(){var e;const i=Number((e=d.order)==null?void 0:e.id);if(i){if(o.resolution==="CONFIRM_SUCCESS"&&!o.remote_order_no.trim()){C.msgError("请填写甘草药方单号");return}if(!o.note.trim()){C.msgError("请填写甘草后台核对依据");return}_.value=!0;try{await j({id:i,resolution:o.resolution,remote_order_no:o.resolution==="CONFIRM_SUCCESS"?o.remote_order_no.trim():"",note:o.note.trim()}),C.msgSuccess("甘草提交核对已记录"),s.value=!1,y("resolved")}catch{}finally{_.value=!1}}}return(i,e)=>{const n=G,c=q,m=K,p=T,a=L,S=z,R=P,x=B,I=F("perms");return V.value?(f(),k(M,{key:0},[D((f(),E(n,{type:"danger",size:"small",plain:"",onClick:O},{default:t(()=>[...e[5]||(e[5]=[u("核对甘草提交",-1)])]),_:1})),[[I,["tcm.prescriptionOrder/confirmGancaoSubmission"]]]),r(x,{modelValue:s.value,"onUpdate:modelValue":e[4]||(e[4]=l=>s.value=l),title:"人工核对甘草提交",width:"min(92vw, 520px)","append-to-body":"","destroy-on-close":"","close-on-click-modal":!1},{footer:t(()=>[r(n,{onClick:e[3]||(e[3]=l=>s.value=!1)},{default:t(()=>[...e[8]||(e[8]=[u("取消",-1)])]),_:1}),r(n,{type:"primary",loading:_.value,onClick:b},{default:t(()=>[...e[9]||(e[9]=[u("确认并记录",-1)])]),_:1},8,["loading"])]),default:t(()=>[r(c,{title:"请先在甘草后台核对。本操作会写入不可变更的审计记录。",type:"warning",closable:!1,"show-icon":"",class:"mb-4"}),r(R,{"label-width":"100px",onSubmit:A(b,["prevent"])},{default:t(()=>[r(a,{label:"核对结果",required:""},{default:t(()=>[r(p,{modelValue:o.resolution,"onUpdate:modelValue":e[0]||(e[0]=l=>o.resolution=l)},{default:t(()=>[r(m,{label:"CONFIRM_SUCCESS"},{default:t(()=>[...e[6]||(e[6]=[u("确认已创建",-1)])]),_:1}),r(m,{label:"CONFIRM_NOT_CREATED"},{default:t(()=>[...e[7]||(e[7]=[u("确认未创建",-1)])]),_:1})]),_:1},8,["modelValue"])]),_:1}),o.resolution==="CONFIRM_SUCCESS"?(f(),E(a,{key:0,label:"甘草单号",required:""},{default:t(()=>[r(S,{modelValue:o.remote_order_no,"onUpdate:modelValue":e[1]||(e[1]=l=>o.remote_order_no=l),maxlength:"64",clearable:""},null,8,["modelValue"])]),_:1})):g("",!0),r(a,{label:"核对依据",required:""},{default:t(()=>[r(S,{modelValue:o.note,"onUpdate:modelValue":e[2]||(e[2]=l=>o.note=l),type:"textarea",rows:3,maxlength:"1000","show-word-limit":"",placeholder:"例如:核对时间、甘草后台查询条件及结果"},null,8,["modelValue"])]),_:1})]),_:1})]),_:1},8,["modelValue"])],64)):g("",!0)}}});export{X as _};
|
||||
import{o as U,be as F,q as f,r as k,F as M,ac as D,O as E,D as t,L as u,K as G,v as r,bt as B,bh as q,b9 as P,aw as A,b6 as L,bc as T,bf as K,b7 as z,P as g,p as W,M as v,ba as $}from"./.pnpm-C1KTa5cB.js";import{p as j}from"./tcm-BlzCcc0R.js";import{i as C}from"./index-BzVi6Aa3.js";const X=U({__name:"GancaoSubmissionReconcileButton",props:{order:{}},emits:["resolved"],setup(N,{emit:w}){const d=N,y=w,V=W(()=>{var m,p,a;const i=String(((m=d.order)==null?void 0:m.pharmacy_claim_target)||"").trim().toLowerCase(),e=String(((p=d.order)==null?void 0:p.pharmacy_claim_status)||"").trim().toUpperCase(),n=Number(((a=d.order)==null?void 0:a.pharmacy_claim_lease_expires_at)||0),c=e==="PENDING"&&n>0&&n<=Math.floor(Date.now()/1e3);return i==="gancao"&&(c||["UNKNOWN","PENDING_RECONCILE"].includes(e))}),s=v(!1),_=v(!1),o=$({resolution:"CONFIRM_SUCCESS",remote_order_no:"",note:""});function O(){o.resolution="CONFIRM_SUCCESS",o.remote_order_no="",o.note="",s.value=!0}async function b(){var e;const i=Number((e=d.order)==null?void 0:e.id);if(i){if(o.resolution==="CONFIRM_SUCCESS"&&!o.remote_order_no.trim()){C.msgError("请填写甘草药方单号");return}if(!o.note.trim()){C.msgError("请填写甘草后台核对依据");return}_.value=!0;try{await j({id:i,resolution:o.resolution,remote_order_no:o.resolution==="CONFIRM_SUCCESS"?o.remote_order_no.trim():"",note:o.note.trim()}),C.msgSuccess("甘草提交核对已记录"),s.value=!1,y("resolved")}catch{}finally{_.value=!1}}}return(i,e)=>{const n=G,c=q,m=K,p=T,a=L,S=z,R=P,x=B,I=F("perms");return V.value?(f(),k(M,{key:0},[D((f(),E(n,{type:"danger",size:"small",plain:"",onClick:O},{default:t(()=>[...e[5]||(e[5]=[u("核对甘草提交",-1)])]),_:1})),[[I,["tcm.prescriptionOrder/confirmGancaoSubmission"]]]),r(x,{modelValue:s.value,"onUpdate:modelValue":e[4]||(e[4]=l=>s.value=l),title:"人工核对甘草提交",width:"min(92vw, 520px)","append-to-body":"","destroy-on-close":"","close-on-click-modal":!1},{footer:t(()=>[r(n,{onClick:e[3]||(e[3]=l=>s.value=!1)},{default:t(()=>[...e[8]||(e[8]=[u("取消",-1)])]),_:1}),r(n,{type:"primary",loading:_.value,onClick:b},{default:t(()=>[...e[9]||(e[9]=[u("确认并记录",-1)])]),_:1},8,["loading"])]),default:t(()=>[r(c,{title:"请先在甘草后台核对。本操作会写入不可变更的审计记录。",type:"warning",closable:!1,"show-icon":"",class:"mb-4"}),r(R,{"label-width":"100px",onSubmit:A(b,["prevent"])},{default:t(()=>[r(a,{label:"核对结果",required:""},{default:t(()=>[r(p,{modelValue:o.resolution,"onUpdate:modelValue":e[0]||(e[0]=l=>o.resolution=l)},{default:t(()=>[r(m,{label:"CONFIRM_SUCCESS"},{default:t(()=>[...e[6]||(e[6]=[u("确认已创建",-1)])]),_:1}),r(m,{label:"CONFIRM_NOT_CREATED"},{default:t(()=>[...e[7]||(e[7]=[u("确认未创建",-1)])]),_:1})]),_:1},8,["modelValue"])]),_:1}),o.resolution==="CONFIRM_SUCCESS"?(f(),E(a,{key:0,label:"甘草单号",required:""},{default:t(()=>[r(S,{modelValue:o.remote_order_no,"onUpdate:modelValue":e[1]||(e[1]=l=>o.remote_order_no=l),maxlength:"64",clearable:""},null,8,["modelValue"])]),_:1})):g("",!0),r(a,{label:"核对依据",required:""},{default:t(()=>[r(S,{modelValue:o.note,"onUpdate:modelValue":e[2]||(e[2]=l=>o.note=l),type:"textarea",rows:3,maxlength:"1000","show-word-limit":"",placeholder:"例如:核对时间、甘草后台查询条件及结果"},null,8,["modelValue"])]),_:1})]),_:1})]),_:1},8,["modelValue"])],64)):g("",!0)}}});export{X as _};
|
||||
+1
-1
@@ -1 +1 @@
|
||||
import{o as F,R as q,q as t,r as n,v as i,D as c,s as o,L as _,bh as G,w as H,u as x,cV as $,K as j,by as J,ac as K,F as E,G as O,O as f,bB as W,P as v,bi as Q,M as m,p as U,ae as X,T as r,br as Z,aa as ee,bq as se,a8 as ae,E as C}from"./.pnpm-C1KTa5cB.js";import{d as te}from"./dayjs-DznSeB-q.js";import{ar as ne,as as oe}from"./tcm-DJJMkqKG.js";import{p as re}from"./im-business-message-parse-DXRL3M5q.js";import{_ as le}from"./index-Cxx1IoGl.js";const ie={class:"im-chat-record-panel"},ce={class:"toolbar mb-3"},de={class:"chat-wrap"},_e={key:0,class:"chat-list"},ue={class:"meta"},fe={class:"name"},me={class:"time"},pe={class:"bubble"},ye={key:0,class:"friendly-text"},ge={class:"friendly-main"},ve={key:0,class:"friendly-sub"},he={key:1,class:"text-content"},we={key:2,class:"text-content"},be={key:3,class:"muted"},ke=F({__name:"ImChatRecordPanel",props:{diagnosisId:{}},setup(B,{expose:M}){const u=B,d=m(!1),p=m(!1),y=ae([]),L=m(""),g=m(""),D={text:"",image:"图片",file:"文件",sound:"语音",video:"视频",location:"位置",custom:"自定义",face:"表情",other:""},h=U(()=>y.value.map(e=>{const a=P(e);let l="";return a!=null&&a.tag?l=a.tag:l=D[e.msg_type]||(e.msg_type!=="other"?e.msg_type:""),{raw:e,friendly:a,tag:l}}));function N(e){if(e==null||!e)return"—";const a=e>1e12?Math.floor(e/1e3):e;return te.unix(a).format("YYYY-MM-DD HH:mm:ss")}function P(e){const a=(e.text||"").trim();if(!a)return null;const l=a.startsWith("{")&&(/\bbusinessID\b/.test(a)||/\bcmd\b/.test(a));return e.msg_type==="custom"||l?re(a):null}function R(e){return e.is_from_doctor?e.from_staff_name?`医生(${e.from_staff_name})`:"医生/员工":g.value?`患者(${g.value})`:"患者"}async function w(){if(u.diagnosisId){d.value=!0;try{const e=await ne({diagnosis_id:u.diagnosisId,only_archived:1});y.value=(e==null?void 0:e.lists)||[],L.value=(e==null?void 0:e.patient_im_id)||"",g.value=(e==null?void 0:e.patient_name)||""}catch(e){console.error(e),y.value=[]}finally{d.value=!1}}}function b(){w()}async function S(){if(u.diagnosisId){p.value=!0;try{await oe({diagnosis_id:u.diagnosisId}),C.success('已发起后台同步,几秒后请点击"重新加载已归档"查看新消息')}catch(e){console.error(e),C.error("发起同步失败")}finally{p.value=!1}}}return q(()=>u.diagnosisId,()=>{w()},{immediate:!0}),M({refresh:b}),(e,a)=>{const l=G,k=H,I=j,T=Z,V=ee,Y=se,z=W,A=Q;return t(),n("div",ie,[i(l,{type:"info","show-icon":"",closable:!1,class:"mb-4",title:"说明"},{default:c(()=>[...a[0]||(a[0]=[o("p",{class:"panel-tip"},[_(" 展示单聊记录:已合并患者 与 "),o("strong",null,"所有医生 / 医助账号"),_("分别产生的会话,按时间排序。 数据由后台定时任务从腾讯云 IM 漫游消息同步至本地归档;点击下方按钮可立即触发后台同步。 ")],-1)])]),_:1}),o("div",ce,[i(I,{type:"primary",link:"",loading:p.value,onClick:S},{default:c(()=>[i(k,{class:"mr-1"},{default:c(()=>[i(x($))]),_:1}),a[1]||(a[1]=_(" 同步最新(后台异步) ",-1))]),_:1},8,["loading"]),i(I,{type:"primary",link:"",loading:d.value,onClick:b},{default:c(()=>[i(k,{class:"mr-1"},{default:c(()=>[i(x(J))]),_:1}),a[2]||(a[2]=_(" 重新加载已归档 ",-1))]),_:1},8,["loading"])]),K((t(),n("div",de,[!d.value&&h.value.length?(t(),n("div",_e,[(t(!0),n(E,null,O(h.value,s=>(t(),n("div",{key:s.raw.msg_id,class:X(["chat-row",s.raw.is_from_doctor?"from-doctor":"from-patient"])},[o("div",ue,[o("span",fe,r(R(s.raw)),1),o("span",me,r(N(s.raw.time)),1),s.tag?(t(),f(T,{key:0,size:"small",type:"info",class:"ml-2"},{default:c(()=>[_(r(s.tag),1)]),_:2},1024)):v("",!0)]),o("div",pe,[s.raw.msg_type==="image"&&s.raw.image_url?(t(),f(V,{key:0,src:s.raw.image_url,"preview-src-list":[s.raw.image_url],fit:"contain",class:"chat-img"},null,8,["src","preview-src-list"])):(s.raw.msg_type==="file"||s.raw.msg_type==="sound"||s.raw.msg_type==="video")&&s.raw.file_url?(t(),f(Y,{key:1,href:s.raw.file_url,target:"_blank",type:"primary"},{default:c(()=>[_(r(s.raw.file_name||"打开文件"),1)]),_:2},1032,["href"])):(t(),n(E,{key:2},[s.friendly?(t(),n("div",ye,[o("div",ge,r(s.friendly.main),1),s.friendly.sub?(t(),n("div",ve,r(s.friendly.sub),1)):v("",!0)])):s.raw.msg_type==="text"&&s.raw.text?(t(),n("div",he,r(s.raw.text),1)):s.raw.text?(t(),n("div",we,r(s.raw.text),1)):(t(),n("span",be,"(无法展示该消息类型)"))],64))])],2))),128))])):d.value?v("",!0):(t(),f(z,{key:1,description:"暂无 IM 聊天记录"}))])),[[A,d.value]])])}}}),Me=le(ke,[["__scopeId","data-v-58b699dd"]]);export{Me as default};
|
||||
import{o as F,R as q,q as t,r as n,v as i,D as c,s as o,L as _,bh as G,w as H,u as x,cV as $,K as j,by as J,ac as K,F as E,G as O,O as f,bB as W,P as v,bi as Q,M as m,p as U,ae as X,T as r,br as Z,aa as ee,bq as se,a8 as ae,E as C}from"./.pnpm-C1KTa5cB.js";import{d as te}from"./dayjs-DznSeB-q.js";import{ar as ne,as as oe}from"./tcm-BlzCcc0R.js";import{p as re}from"./im-business-message-parse-DXRL3M5q.js";import{_ as le}from"./index-BzVi6Aa3.js";const ie={class:"im-chat-record-panel"},ce={class:"toolbar mb-3"},de={class:"chat-wrap"},_e={key:0,class:"chat-list"},ue={class:"meta"},fe={class:"name"},me={class:"time"},pe={class:"bubble"},ye={key:0,class:"friendly-text"},ge={class:"friendly-main"},ve={key:0,class:"friendly-sub"},he={key:1,class:"text-content"},we={key:2,class:"text-content"},be={key:3,class:"muted"},ke=F({__name:"ImChatRecordPanel",props:{diagnosisId:{}},setup(B,{expose:M}){const u=B,d=m(!1),p=m(!1),y=ae([]),L=m(""),g=m(""),D={text:"",image:"图片",file:"文件",sound:"语音",video:"视频",location:"位置",custom:"自定义",face:"表情",other:""},h=U(()=>y.value.map(e=>{const a=P(e);let l="";return a!=null&&a.tag?l=a.tag:l=D[e.msg_type]||(e.msg_type!=="other"?e.msg_type:""),{raw:e,friendly:a,tag:l}}));function N(e){if(e==null||!e)return"—";const a=e>1e12?Math.floor(e/1e3):e;return te.unix(a).format("YYYY-MM-DD HH:mm:ss")}function P(e){const a=(e.text||"").trim();if(!a)return null;const l=a.startsWith("{")&&(/\bbusinessID\b/.test(a)||/\bcmd\b/.test(a));return e.msg_type==="custom"||l?re(a):null}function R(e){return e.is_from_doctor?e.from_staff_name?`医生(${e.from_staff_name})`:"医生/员工":g.value?`患者(${g.value})`:"患者"}async function w(){if(u.diagnosisId){d.value=!0;try{const e=await ne({diagnosis_id:u.diagnosisId,only_archived:1});y.value=(e==null?void 0:e.lists)||[],L.value=(e==null?void 0:e.patient_im_id)||"",g.value=(e==null?void 0:e.patient_name)||""}catch(e){console.error(e),y.value=[]}finally{d.value=!1}}}function b(){w()}async function S(){if(u.diagnosisId){p.value=!0;try{await oe({diagnosis_id:u.diagnosisId}),C.success('已发起后台同步,几秒后请点击"重新加载已归档"查看新消息')}catch(e){console.error(e),C.error("发起同步失败")}finally{p.value=!1}}}return q(()=>u.diagnosisId,()=>{w()},{immediate:!0}),M({refresh:b}),(e,a)=>{const l=G,k=H,I=j,T=Z,V=ee,Y=se,z=W,A=Q;return t(),n("div",ie,[i(l,{type:"info","show-icon":"",closable:!1,class:"mb-4",title:"说明"},{default:c(()=>[...a[0]||(a[0]=[o("p",{class:"panel-tip"},[_(" 展示单聊记录:已合并患者 与 "),o("strong",null,"所有医生 / 医助账号"),_("分别产生的会话,按时间排序。 数据由后台定时任务从腾讯云 IM 漫游消息同步至本地归档;点击下方按钮可立即触发后台同步。 ")],-1)])]),_:1}),o("div",ce,[i(I,{type:"primary",link:"",loading:p.value,onClick:S},{default:c(()=>[i(k,{class:"mr-1"},{default:c(()=>[i(x($))]),_:1}),a[1]||(a[1]=_(" 同步最新(后台异步) ",-1))]),_:1},8,["loading"]),i(I,{type:"primary",link:"",loading:d.value,onClick:b},{default:c(()=>[i(k,{class:"mr-1"},{default:c(()=>[i(x(J))]),_:1}),a[2]||(a[2]=_(" 重新加载已归档 ",-1))]),_:1},8,["loading"])]),K((t(),n("div",de,[!d.value&&h.value.length?(t(),n("div",_e,[(t(!0),n(E,null,O(h.value,s=>(t(),n("div",{key:s.raw.msg_id,class:X(["chat-row",s.raw.is_from_doctor?"from-doctor":"from-patient"])},[o("div",ue,[o("span",fe,r(R(s.raw)),1),o("span",me,r(N(s.raw.time)),1),s.tag?(t(),f(T,{key:0,size:"small",type:"info",class:"ml-2"},{default:c(()=>[_(r(s.tag),1)]),_:2},1024)):v("",!0)]),o("div",pe,[s.raw.msg_type==="image"&&s.raw.image_url?(t(),f(V,{key:0,src:s.raw.image_url,"preview-src-list":[s.raw.image_url],fit:"contain",class:"chat-img"},null,8,["src","preview-src-list"])):(s.raw.msg_type==="file"||s.raw.msg_type==="sound"||s.raw.msg_type==="video")&&s.raw.file_url?(t(),f(Y,{key:1,href:s.raw.file_url,target:"_blank",type:"primary"},{default:c(()=>[_(r(s.raw.file_name||"打开文件"),1)]),_:2},1032,["href"])):(t(),n(E,{key:2},[s.friendly?(t(),n("div",ye,[o("div",ge,r(s.friendly.main),1),s.friendly.sub?(t(),n("div",ve,r(s.friendly.sub),1)):v("",!0)])):s.raw.msg_type==="text"&&s.raw.text?(t(),n("div",he,r(s.raw.text),1)):s.raw.text?(t(),n("div",we,r(s.raw.text),1)):(t(),n("span",be,"(无法展示该消息类型)"))],64))])],2))),128))])):d.value?v("",!0):(t(),f(z,{key:1,description:"暂无 IM 聊天记录"}))])),[[A,d.value]])])}}}),Me=le(ke,[["__scopeId","data-v-58b699dd"]]);export{Me as default};
|
||||
+1
-1
@@ -1 +1 @@
|
||||
import{o as S,q as a,r as l,ae as x,v as r,D as m,L as d,T as i,a0 as D,s as n,O as g,br as G,P as B,aa as K,u as v,bE as L,w as q,bF as A,bG as H,bH as O,F as P,K as U,p as u}from"./.pnpm-C1KTa5cB.js";import{t as j,_ as J}from"./index-Cxx1IoGl.js";const Q={class:"msg-body"},R={class:"msg-meta"},W={class:"msg-sender"},X={class:"msg-time"},Y={key:0,class:"content-text"},Z={key:2,class:"content-pending"},ee=["src"],se={key:4,class:"content-pending"},te=["src"],ae={key:6,class:"content-pending"},ne={key:7,class:"content-file"},le={class:"file-meta"},ie=["title"],oe={class:"file-info"},ce={key:8,class:"content-fallback"},re={class:"fallback-title"},me={class:"fallback-desc"},ue=S({__name:"MessageBubble",props:{message:{}},setup(s){const o=s,w=u(()=>o.message.from_is_staff),c=u(()=>(o.message.media||[]).find(t=>t.status===1)??null),_=u(()=>{const e=o.message.from_profile;return e&&e.name||o.message.from_user}),z=u(()=>{const e=o.message.from_profile;return(e==null?void 0:e.avatar)||""}),F=u(()=>(_.value||"").slice(-2)||"?"),E=u(()=>{const e=o.message.msgtype;return e==="image"||e==="video"||e==="voice"?"bubble-media":e==="file"?"bubble-file":"bubble-text"}),C=u(()=>{switch(o.message.msgtype){case"link":return"[链接]";case"weapp":return"[小程序]";case"chatrecord":return"[聊天记录]";case"location":return"[位置]";case"card":return"[名片]";case"meeting":return"[会议]";case"docmsg":return"[文档]";case"mixed":return"[混合消息]";case"emotion":return"[表情]";default:return`[${o.message.msgtype}]`}});function M(e){return e?j(e*1e3,"mm-dd hh:MM"):""}function N(e){return e?e<1024?`${e} B`:e<1024*1024?`${(e/1024).toFixed(1)} KB`:e<1024*1024*1024?`${(e/1024/1024).toFixed(2)} MB`:`${(e/1024/1024/1024).toFixed(2)} GB`:""}function $(e){window.open(e,"_blank","noopener")}return(e,t)=>{var k,y,p,h;const T=D,b=G,V=K,f=q,I=U;return a(),l("div",{class:x(["msg-bubble",{"is-staff":w.value}])},[r(T,{class:"msg-avatar",size:36,src:z.value},{default:m(()=>[d(i(F.value),1)]),_:1},8,["src"]),n("div",Q,[n("div",R,[n("span",W,i(_.value),1),n("span",X,i(M(s.message.send_time)),1),s.message.action==="recall"?(a(),g(b,{key:0,size:"small",type:"info"},{default:m(()=>[...t[1]||(t[1]=[d(" 已撤回 ",-1)])]),_:1})):B("",!0)]),n("div",{class:x(["msg-content",E.value])},[s.message.msgtype==="text"||s.message.msgtype==="markdown"?(a(),l("div",Y,i(s.message.content),1)):s.message.msgtype==="image"&&((k=c.value)!=null&&k.file_url)?(a(),g(V,{key:1,src:c.value.file_url,"preview-src-list":[c.value.file_url],fit:"contain",class:"content-image","hide-on-click-modal":""},null,8,["src","preview-src-list"])):s.message.msgtype==="image"?(a(),l("div",Z,[r(f,null,{default:m(()=>[r(v(L))]),_:1}),t[2]||(t[2]=n("span",null,"图片下载中…",-1))])):s.message.msgtype==="video"&&((y=c.value)!=null&&y.file_url)?(a(),l("video",{key:3,src:c.value.file_url,controls:"",class:"content-video"},null,8,ee)):s.message.msgtype==="video"?(a(),l("div",se,[r(f,null,{default:m(()=>[r(v(A))]),_:1}),t[3]||(t[3]=n("span",null,"视频下载中…",-1))])):s.message.msgtype==="voice"&&((p=c.value)!=null&&p.file_url)?(a(),l("audio",{key:5,src:c.value.file_url,controls:"",class:"content-audio"},null,8,te)):s.message.msgtype==="voice"?(a(),l("div",ae,[r(f,null,{default:m(()=>[r(v(H))]),_:1}),n("span",null,"语音下载中…("+i(s.message.play_length)+"s)",1)])):s.message.msgtype==="file"?(a(),l("div",ne,[r(f,{size:24},{default:m(()=>[r(v(O))]),_:1}),n("div",le,[n("div",{class:"file-name",title:s.message.file_name},i(s.message.file_name||"未命名文件"),9,ie),n("div",oe,[d(i(N(s.message.file_size))+" ",1),s.message.file_ext?(a(),l(P,{key:0},[d("· "+i(s.message.file_ext),1)],64)):B("",!0)])]),(h=c.value)!=null&&h.file_url?(a(),g(I,{key:0,size:"small",type:"primary",link:"",onClick:t[0]||(t[0]=de=>$(c.value.file_url))},{default:m(()=>[...t[4]||(t[4]=[d(" 下载 ",-1)])]),_:1})):(a(),g(b,{key:1,size:"small",type:"info"},{default:m(()=>[...t[5]||(t[5]=[d("下载中…",-1)])]),_:1}))])):(a(),l("div",ce,[n("div",re,i(C.value),1),n("div",me,i(s.message.content),1)]))],2)])],2)}}}),ve=J(ue,[["__scopeId","data-v-b5ff1168"]]);export{ve as default};
|
||||
import{o as S,q as a,r as l,ae as x,v as r,D as m,L as d,T as i,a0 as D,s as n,O as g,br as G,P as B,aa as K,u as v,bE as L,w as q,bF as A,bG as H,bH as O,F as P,K as U,p as u}from"./.pnpm-C1KTa5cB.js";import{t as j,_ as J}from"./index-BzVi6Aa3.js";const Q={class:"msg-body"},R={class:"msg-meta"},W={class:"msg-sender"},X={class:"msg-time"},Y={key:0,class:"content-text"},Z={key:2,class:"content-pending"},ee=["src"],se={key:4,class:"content-pending"},te=["src"],ae={key:6,class:"content-pending"},ne={key:7,class:"content-file"},le={class:"file-meta"},ie=["title"],oe={class:"file-info"},ce={key:8,class:"content-fallback"},re={class:"fallback-title"},me={class:"fallback-desc"},ue=S({__name:"MessageBubble",props:{message:{}},setup(s){const o=s,w=u(()=>o.message.from_is_staff),c=u(()=>(o.message.media||[]).find(t=>t.status===1)??null),_=u(()=>{const e=o.message.from_profile;return e&&e.name||o.message.from_user}),z=u(()=>{const e=o.message.from_profile;return(e==null?void 0:e.avatar)||""}),F=u(()=>(_.value||"").slice(-2)||"?"),E=u(()=>{const e=o.message.msgtype;return e==="image"||e==="video"||e==="voice"?"bubble-media":e==="file"?"bubble-file":"bubble-text"}),C=u(()=>{switch(o.message.msgtype){case"link":return"[链接]";case"weapp":return"[小程序]";case"chatrecord":return"[聊天记录]";case"location":return"[位置]";case"card":return"[名片]";case"meeting":return"[会议]";case"docmsg":return"[文档]";case"mixed":return"[混合消息]";case"emotion":return"[表情]";default:return`[${o.message.msgtype}]`}});function M(e){return e?j(e*1e3,"mm-dd hh:MM"):""}function N(e){return e?e<1024?`${e} B`:e<1024*1024?`${(e/1024).toFixed(1)} KB`:e<1024*1024*1024?`${(e/1024/1024).toFixed(2)} MB`:`${(e/1024/1024/1024).toFixed(2)} GB`:""}function $(e){window.open(e,"_blank","noopener")}return(e,t)=>{var k,y,p,h;const T=D,b=G,V=K,f=q,I=U;return a(),l("div",{class:x(["msg-bubble",{"is-staff":w.value}])},[r(T,{class:"msg-avatar",size:36,src:z.value},{default:m(()=>[d(i(F.value),1)]),_:1},8,["src"]),n("div",Q,[n("div",R,[n("span",W,i(_.value),1),n("span",X,i(M(s.message.send_time)),1),s.message.action==="recall"?(a(),g(b,{key:0,size:"small",type:"info"},{default:m(()=>[...t[1]||(t[1]=[d(" 已撤回 ",-1)])]),_:1})):B("",!0)]),n("div",{class:x(["msg-content",E.value])},[s.message.msgtype==="text"||s.message.msgtype==="markdown"?(a(),l("div",Y,i(s.message.content),1)):s.message.msgtype==="image"&&((k=c.value)!=null&&k.file_url)?(a(),g(V,{key:1,src:c.value.file_url,"preview-src-list":[c.value.file_url],fit:"contain",class:"content-image","hide-on-click-modal":""},null,8,["src","preview-src-list"])):s.message.msgtype==="image"?(a(),l("div",Z,[r(f,null,{default:m(()=>[r(v(L))]),_:1}),t[2]||(t[2]=n("span",null,"图片下载中…",-1))])):s.message.msgtype==="video"&&((y=c.value)!=null&&y.file_url)?(a(),l("video",{key:3,src:c.value.file_url,controls:"",class:"content-video"},null,8,ee)):s.message.msgtype==="video"?(a(),l("div",se,[r(f,null,{default:m(()=>[r(v(A))]),_:1}),t[3]||(t[3]=n("span",null,"视频下载中…",-1))])):s.message.msgtype==="voice"&&((p=c.value)!=null&&p.file_url)?(a(),l("audio",{key:5,src:c.value.file_url,controls:"",class:"content-audio"},null,8,te)):s.message.msgtype==="voice"?(a(),l("div",ae,[r(f,null,{default:m(()=>[r(v(H))]),_:1}),n("span",null,"语音下载中…("+i(s.message.play_length)+"s)",1)])):s.message.msgtype==="file"?(a(),l("div",ne,[r(f,{size:24},{default:m(()=>[r(v(O))]),_:1}),n("div",le,[n("div",{class:"file-name",title:s.message.file_name},i(s.message.file_name||"未命名文件"),9,ie),n("div",oe,[d(i(N(s.message.file_size))+" ",1),s.message.file_ext?(a(),l(P,{key:0},[d("· "+i(s.message.file_ext),1)],64)):B("",!0)])]),(h=c.value)!=null&&h.file_url?(a(),g(I,{key:0,size:"small",type:"primary",link:"",onClick:t[0]||(t[0]=de=>$(c.value.file_url))},{default:m(()=>[...t[4]||(t[4]=[d(" 下载 ",-1)])]),_:1})):(a(),g(b,{key:1,size:"small",type:"info"},{default:m(()=>[...t[5]||(t[5]=[d("下载中…",-1)])]),_:1}))])):(a(),l("div",ce,[n("div",re,i(C.value),1),n("div",me,i(s.message.content),1)]))],2)])],2)}}}),ve=J(ue,[["__scopeId","data-v-b5ff1168"]]);export{ve as default};
|
||||
+1
-1
@@ -1,2 +1,2 @@
|
||||
import{o as ie,R as oe,q as n,r as o,O as k,K as le,D as r,L as P,P as m,v as l,s as d,F as w,G as E,bB as ae,b7 as re,bt as de,p as ue,M as g,T as U,aa as me,u as V,d7 as z,aw as M,w as ce,bH as pe,e as ge}from"./.pnpm-C1KTa5cB.js";import{_ as fe}from"./picker-CNX2EN3u.js";import{e as ve,c as _e,i as f,_ as ye}from"./index-Cxx1IoGl.js";import{a as T,d as he}from"./patient-BpamRx-E.js";import{h as ke}from"./perm-CZ2bIVzs.js";import"./index-B3Fu5tec.js";import"./index-BhA3CyxZ.js";import"./index.vue_vue_type_script_setup_true_lang-CerDQAzd.js";import"./index-B-rCQZ_o.js";import"./index-CZyPOnHt.js";import"./file-D2AwthSZ.js";import"./index.vue_vue_type_script_setup_true_lang-0C3mdvgv.js";import"./usePaging--aAVZOna.js";const we={class:"note-timeline-wrap"},Ie={key:0,class:"timeline-actions"},Ce={class:"upload-trigger"},be={class:"upload-trigger"},xe={key:1,class:"note-timeline"},Ee={class:"timeline-date"},Ve={class:"timeline-body"},Ne={key:0,class:"timeline-content"},Se={key:1,class:"timeline-images"},De={key:2,class:"timeline-images"},Be={key:0,class:"thumb-wrap"},Ae={key:1,class:"file-wrap"},Pe=["href","title"],Ue={class:"file-name"},W=8e3,ze=ie({__name:"NoteTimeline",props:{notes:{},diagnosisId:{},readonly:{type:Boolean}},emits:["refresh"],setup(u,{emit:j}){const c=u,I=j,X=ue(()=>ke(["doctor.appointment/addDoctorNote"])),v=g(!1),C=g(""),N=g(!1),q=ve(),_=t=>q.getImageUrl(t),S=g([]),D=g([]),y=g(0),h=g(0),H=["jpg","jpeg","png","gif","bmp","webp","svg"],B=t=>{var i;const e=((i=t.split(".").pop())==null?void 0:i.toLowerCase().split("?")[0])||"";return H.includes(e)},$=t=>{var i;const e=t.split("/");return decodeURIComponent(((i=e[e.length-1])==null?void 0:i.split("?")[0])||"文件")},K=t=>t.filter(B).map(_),Z=(t,e)=>{const i=t.filter(B),b=t[e];return i.indexOf(b)},J=t=>t?t.split(`
|
||||
import{o as ie,R as oe,q as n,r as o,O as k,K as le,D as r,L as P,P as m,v as l,s as d,F as w,G as E,bB as ae,b7 as re,bt as de,p as ue,M as g,T as U,aa as me,u as V,d7 as z,aw as M,w as ce,bH as pe,e as ge}from"./.pnpm-C1KTa5cB.js";import{_ as fe}from"./picker-DFb6DRYi.js";import{e as ve,c as _e,i as f,_ as ye}from"./index-BzVi6Aa3.js";import{a as T,d as he}from"./patient-DfMQG9Ny.js";import{h as ke}from"./perm-CTEIwbku.js";import"./index-Da4XqmJd.js";import"./index-BjEe2GZP.js";import"./index.vue_vue_type_script_setup_true_lang-CerDQAzd.js";import"./index-CQoyCdiY.js";import"./index-CEYf_4x5.js";import"./file-DqKLtehk.js";import"./index.vue_vue_type_script_setup_true_lang-0C3mdvgv.js";import"./usePaging--aAVZOna.js";const we={class:"note-timeline-wrap"},Ie={key:0,class:"timeline-actions"},Ce={class:"upload-trigger"},be={class:"upload-trigger"},xe={key:1,class:"note-timeline"},Ee={class:"timeline-date"},Ve={class:"timeline-body"},Ne={key:0,class:"timeline-content"},Se={key:1,class:"timeline-images"},De={key:2,class:"timeline-images"},Be={key:0,class:"thumb-wrap"},Ae={key:1,class:"file-wrap"},Pe=["href","title"],Ue={class:"file-name"},W=8e3,ze=ie({__name:"NoteTimeline",props:{notes:{},diagnosisId:{},readonly:{type:Boolean}},emits:["refresh"],setup(u,{emit:j}){const c=u,I=j,X=ue(()=>ke(["doctor.appointment/addDoctorNote"])),v=g(!1),C=g(""),N=g(!1),q=ve(),_=t=>q.getImageUrl(t),S=g([]),D=g([]),y=g(0),h=g(0),H=["jpg","jpeg","png","gif","bmp","webp","svg"],B=t=>{var i;const e=((i=t.split(".").pop())==null?void 0:i.toLowerCase().split("?")[0])||"";return H.includes(e)},$=t=>{var i;const e=t.split("/");return decodeURIComponent(((i=e[e.length-1])==null?void 0:i.split("?")[0])||"文件")},K=t=>t.filter(B).map(_),Z=(t,e)=>{const i=t.filter(B),b=t[e];return i.indexOf(b)},J=t=>t?t.split(`
|
||||
`).filter(Boolean):[],Q=t=>{if(!c.diagnosisId)return;const e=Array.isArray(t)?t:[t];if(e.length<=y.value){y.value=e.length;return}const i=e.slice(y.value);y.value=e.length,i.length>0&&T({diagnosis_id:c.diagnosisId,tongue_images:i}).then(()=>{f.msgSuccess("舌苔照片已添加"),I("refresh")})},Y=t=>{if(!c.diagnosisId)return;const e=Array.isArray(t)?t:[t];if(e.length<=h.value){h.value=e.length;return}const i=e.slice(h.value);h.value=e.length,i.length>0&&T({diagnosis_id:c.diagnosisId,report_files:i}).then(()=>{f.msgSuccess("检查报告已添加"),I("refresh")})};oe(()=>c.notes,()=>{S.value=[],D.value=[],y.value=0,h.value=0});const ee=async()=>{if(!c.diagnosisId){f.msgWarning("当前无诊单,无法添加备注");return}const t=C.value.trim();if(!t){f.msgWarning("请输入备注内容");return}N.value=!0;try{await T({diagnosis_id:c.diagnosisId,content:t}),f.msgSuccess("备注保存成功"),C.value="",v.value=!1,I("refresh")}catch(e){f.msgError((e==null?void 0:e.message)||"保存失败")}finally{N.value=!1}},A=async(t,e,i)=>{try{await ge.confirm("确认删除?","提示",{type:"warning"})}catch{return}await he({note_id:t,image_type:e,image_path:i}),f.msgSuccess("已删除"),I("refresh")};return(t,e)=>{const i=le,b=_e,F=fe,L=me,x=ce,te=ae,se=re,ne=de;return n(),o("div",we,[!u.readonly&&u.diagnosisId?(n(),o("div",Ie,[X.value?(n(),k(i,{key:0,type:"primary",plain:"",size:"small",onClick:e[0]||(e[0]=s=>v.value=!0)},{default:r(()=>[...e[6]||(e[6]=[P(" 添加备注 ",-1)])]),_:1})):m("",!0),l(F,{modelValue:S.value,"onUpdate:modelValue":e[1]||(e[1]=s=>S.value=s),limit:99,type:"image","exclude-domain":!0,onChange:Q},{upload:r(()=>[d("div",Ce,[l(b,{size:20,name:"el-icon-Plus"}),e[7]||(e[7]=d("span",null,"舌苔照片",-1))])]),_:1},8,["modelValue"]),l(F,{modelValue:D.value,"onUpdate:modelValue":e[2]||(e[2]=s=>D.value=s),limit:99,type:"file","exclude-domain":!0,onChange:Y},{upload:r(()=>[d("div",be,[l(b,{size:20,name:"el-icon-Plus"}),e[8]||(e[8]=d("span",null,"检查报告",-1))])]),_:1},8,["modelValue"])])):m("",!0),u.notes.length?(n(),o("div",xe,[(n(!0),o(w,null,E(u.notes,s=>{var R,G;return n(),o("div",{key:s.id,class:"timeline-node"},[e[11]||(e[11]=d("div",{class:"timeline-dot"},null,-1)),d("div",Ee,U(s.note_date),1),d("div",Ve,[s.content?(n(),o("div",Ne,[(n(!0),o(w,null,E(J(s.content),(a,p)=>(n(),o("div",{key:p,class:"content-line"},U(a),1))),128))])):m("",!0),(R=s.tongue_images)!=null&&R.length?(n(),o("div",Se,[e[9]||(e[9]=d("span",{class:"images-label"},"舌苔照片",-1)),(n(!0),o(w,null,E(s.tongue_images,(a,p)=>(n(),o("div",{key:p,class:"thumb-wrap"},[l(L,{src:_(a),"preview-src-list":s.tongue_images.map(_),"initial-index":p,"z-index":W,fit:"cover",class:"timeline-thumb","preview-teleported":""},null,8,["src","preview-src-list","initial-index"]),u.readonly?m("",!0):(n(),k(x,{key:0,class:"thumb-delete",onClick:M(O=>A(s.id,"tongue_images",a),["stop"])},{default:r(()=>[l(V(z))]),_:1},8,["onClick"]))]))),128))])):m("",!0),(G=s.report_files)!=null&&G.length?(n(),o("div",De,[e[10]||(e[10]=d("span",{class:"images-label"},"检查报告",-1)),(n(!0),o(w,null,E(s.report_files,(a,p)=>(n(),o(w,{key:p},[B(a)?(n(),o("div",Be,[l(L,{src:_(a),"preview-src-list":K(s.report_files),"initial-index":Z(s.report_files,p),"z-index":W,fit:"cover",class:"timeline-thumb","preview-teleported":""},null,8,["src","preview-src-list","initial-index"]),u.readonly?m("",!0):(n(),k(x,{key:0,class:"thumb-delete",onClick:M(O=>A(s.id,"report_files",a),["stop"])},{default:r(()=>[l(V(z))]),_:1},8,["onClick"]))])):(n(),o("div",Ae,[d("a",{href:_(a),target:"_blank",class:"file-link",title:$(a)},[l(x,{size:20},{default:r(()=>[l(V(pe))]),_:1}),d("span",Ue,U($(a)),1)],8,Pe),u.readonly?m("",!0):(n(),k(x,{key:0,class:"file-delete",onClick:M(O=>A(s.id,"report_files",a),["stop"])},{default:r(()=>[l(V(z))]),_:1},8,["onClick"]))]))],64))),128))])):m("",!0)])])}),128))])):m("",!0),!u.notes.length&&u.readonly?(n(),k(te,{key:2,description:"暂无备注","image-size":48})):m("",!0),l(ne,{modelValue:v.value,"onUpdate:modelValue":e[5]||(e[5]=s=>v.value=s),title:"添加备注",width:"480px","append-to-body":""},{footer:r(()=>[l(i,{onClick:e[4]||(e[4]=s=>v.value=!1)},{default:r(()=>[...e[12]||(e[12]=[P("取消",-1)])]),_:1}),l(i,{type:"primary",loading:N.value,onClick:ee},{default:r(()=>[...e[13]||(e[13]=[P("保存",-1)])]),_:1},8,["loading"])]),default:r(()=>[l(se,{modelValue:C.value,"onUpdate:modelValue":e[3]||(e[3]=s=>C.value=s),type:"textarea",rows:4,placeholder:"输入今日备注...",maxlength:"500","show-word-limit":""},null,8,["modelValue"])]),_:1},8,["modelValue"])])}}}),Ke=ye(ze,[["__scopeId","data-v-530ad386"]]);export{Ke as default};
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
.embedded-panel[data-v-e7899ed0]{padding-top:4px}.panel-toolbar[data-v-e7899ed0]{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;margin-bottom:18px}.panel-toolbar h2[data-v-e7899ed0]{margin:0;font-size:18px;font-weight:700;color:#1f2a37}.panel-toolbar p[data-v-e7899ed0]{margin:6px 0 0;color:#667085;font-size:13px}.toolbar-actions[data-v-e7899ed0]{display:flex;align-items:center;gap:10px;flex-shrink:0}.paiban-form[data-v-e7899ed0] .el-form-item__label{font-weight:500}.doctor-list[data-v-e7899ed0]{display:flex;flex-wrap:wrap;gap:12px;width:100%}.doctor-list .doctor-radio[data-v-e7899ed0]{margin-right:0}.paiban-time-container[data-v-e7899ed0]{width:100%}.date-selector[data-v-e7899ed0]{display:flex;gap:10px;flex-wrap:wrap;margin-bottom:16px}.date-selector .date-button[data-v-e7899ed0]{min-width:130px;height:40px;font-size:14px;border-radius:8px;transition:all .2s}.date-selector .date-button[data-v-e7899ed0]:hover{transform:translateY(-2px);box-shadow:0 2px 8px #0000001a}.time-slots-container[data-v-e7899ed0]{background-color:#f8f9fa;border-radius:8px;padding:16px}.time-slots-grid[data-v-e7899ed0]{display:grid;grid-template-columns:repeat(auto-fill,minmax(110px,1fr));gap:10px;max-height:450px;overflow-y:auto;padding:2px}.time-slots-grid[data-v-e7899ed0]::-webkit-scrollbar{width:6px}.time-slots-grid[data-v-e7899ed0]::-webkit-scrollbar-thumb{background-color:#dcdfe6;border-radius:3px}.time-slots-grid[data-v-e7899ed0]::-webkit-scrollbar-thumb:hover{background-color:#c0c4cc}.time-slots-grid .time-slot-item[data-v-e7899ed0]{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:0 8px;border:2px solid #e4e7ed;border-radius:8px;cursor:pointer;transition:all .2s;background-color:#fff;min-height:70px}.time-slots-grid .time-slot-item .slot-time[data-v-e7899ed0]{font-size:15px;font-weight:600;color:#303133;margin-bottom:6px}.time-slots-grid .time-slot-item .slot-status[data-v-e7899ed0]{font-size:12px;color:#909399;padding:0 8px;border-radius:4px;background-color:#f4f4f5}.time-slots-grid .time-slot-item .slot-status.status-available[data-v-e7899ed0]{color:#67c23a;background-color:#f0f9ff}.time-slots-grid .time-slot-item.available[data-v-e7899ed0]{border-color:#e4e7ed}.time-slots-grid .time-slot-item.available[data-v-e7899ed0]:hover{border-color:#409eff;background-color:#ecf5ff;transform:translateY(-2px);box-shadow:0 4px 12px #409eff26}.time-slots-grid .time-slot-item.unavailable[data-v-e7899ed0]{background-color:#f5f7fa;border-color:#e4e7ed;cursor:not-allowed;opacity:.6}.time-slots-grid .time-slot-item.unavailable .slot-time[data-v-e7899ed0]{color:#c0c4cc}.time-slots-grid .time-slot-item.unavailable .slot-status[data-v-e7899ed0]{color:#c0c4cc;background-color:#f5f7fa}.time-slots-grid .time-slot-item.unavailable[data-v-e7899ed0]:hover{transform:none;box-shadow:none}.time-slots-grid .time-slot-item.selected[data-v-e7899ed0]{border-color:#409eff;background:linear-gradient(135deg,#409eff,#66b1ff);box-shadow:0 4px 12px #409eff4d}.time-slots-grid .time-slot-item.selected .slot-time[data-v-e7899ed0]{color:#fff}.time-slots-grid .time-slot-item.selected .slot-status[data-v-e7899ed0]{color:#fff;background-color:#fff3}[data-v-e7899ed0] .el-radio-group{display:flex;flex-wrap:wrap;gap:12px}[data-v-e7899ed0] .el-radio{margin-right:0}
|
||||
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
@@ -1 +1 @@
|
||||
import{m as I,f as p,a as w}from"./diag-display-DCz_VAqj.js";import{o as y,q as x,r as B,s as e,T as a,u as i,P as N}from"./.pnpm-C1KTa5cB.js";import{_ as V}from"./index-Cxx1IoGl.js";const b={class:"card patient-card"},q={class:"patient-hero"},D={class:"patient-name"},E={class:"patient-meta"},G={key:0},S=y({__name:"PatientInfoCard",props:{apt:{default:()=>({})},diag:{default:()=>({})}},setup(t){return(T,n)=>{var d,s,o,c,m,l,r,g,u,f,h,v,k,P,C;return x(),B("div",b,[n[0]||(n[0]=e("div",{class:"card-title"},"患者信息",-1)),e("div",q,[e("div",D,a(((d=t.apt)==null?void 0:d.patient_name)||"—"),1),e("div",E,[e("div",null,a(i(I)((s=t.apt)==null?void 0:s.patient_phone))+" · "+a(i(p)((o=t.diag)==null?void 0:o.gender))+" · "+a(((c=t.diag)==null?void 0:c.age)!=null?t.diag.age+"岁":"—"),1),e("div",null,a((m=t.diag)!=null&&m.height?t.diag.height+"cm":"—")+" / "+a((l=t.diag)!=null&&l.weight?t.diag.weight+"kg":"—")+" · "+a(((r=t.diag)==null?void 0:r.region)||"—"),1),e("div",null," 预约:"+a((g=t.apt)==null?void 0:g.appointment_date)+" "+a((u=t.apt)==null?void 0:u.appointment_time)+" · "+a(i(w)((f=t.apt)==null?void 0:f.period)),1),e("div",null,"医生:"+a(((h=t.apt)==null?void 0:h.doctor_name)||"—")+" 客服:"+a(((v=t.apt)==null?void 0:v.assistant_name)||"—"),1),e("div",null," 状态:"+a(((k=t.apt)==null?void 0:k.status_desc)||"—")+" · "+a((P=t.apt)!=null&&P.has_prescription?"已开方":"未开方"),1),(C=t.apt)!=null&&C.remark?(x(),B("div",G,"备注:"+a(t.apt.remark),1)):N("",!0)])])])}}}),F=V(S,[["__scopeId","data-v-1b0de09c"]]);export{F as default};
|
||||
import{m as I,f as p,a as w}from"./diag-display-DCz_VAqj.js";import{o as y,q as x,r as B,s as e,T as a,u as i,P as N}from"./.pnpm-C1KTa5cB.js";import{_ as V}from"./index-BzVi6Aa3.js";const b={class:"card patient-card"},q={class:"patient-hero"},D={class:"patient-name"},E={class:"patient-meta"},G={key:0},S=y({__name:"PatientInfoCard",props:{apt:{default:()=>({})},diag:{default:()=>({})}},setup(t){return(T,n)=>{var d,s,o,c,m,l,r,g,u,f,h,v,k,P,C;return x(),B("div",b,[n[0]||(n[0]=e("div",{class:"card-title"},"患者信息",-1)),e("div",q,[e("div",D,a(((d=t.apt)==null?void 0:d.patient_name)||"—"),1),e("div",E,[e("div",null,a(i(I)((s=t.apt)==null?void 0:s.patient_phone))+" · "+a(i(p)((o=t.diag)==null?void 0:o.gender))+" · "+a(((c=t.diag)==null?void 0:c.age)!=null?t.diag.age+"岁":"—"),1),e("div",null,a((m=t.diag)!=null&&m.height?t.diag.height+"cm":"—")+" / "+a((l=t.diag)!=null&&l.weight?t.diag.weight+"kg":"—")+" · "+a(((r=t.diag)==null?void 0:r.region)||"—"),1),e("div",null," 预约:"+a((g=t.apt)==null?void 0:g.appointment_date)+" "+a((u=t.apt)==null?void 0:u.appointment_time)+" · "+a(i(w)((f=t.apt)==null?void 0:f.period)),1),e("div",null,"医生:"+a(((h=t.apt)==null?void 0:h.doctor_name)||"—")+" 客服:"+a(((v=t.apt)==null?void 0:v.assistant_name)||"—"),1),e("div",null," 状态:"+a(((k=t.apt)==null?void 0:k.status_desc)||"—")+" · "+a((P=t.apt)!=null&&P.has_prescription?"已开方":"未开方"),1),(C=t.apt)!=null&&C.remark?(x(),B("div",G,"备注:"+a(t.apt.remark),1)):N("",!0)])])])}}}),F=V(S,[["__scopeId","data-v-1b0de09c"]]);export{F as default};
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
@@ -1 +1 @@
|
||||
import{o as L,q as i,r as c,s as d,F as v,O as m,bq as q,D as _,L as k,P as y,G as B,p as f,T as w}from"./.pnpm-C1KTa5cB.js";import H from"./RecordingVideoPlayer-ByTd0pZe.js";import{e as I,_ as P}from"./index-Cxx1IoGl.js";const R={key:0,class:"recording-list"},C={class:"recording-item"},V={key:1,class:"recording-alternates"},N={class:"recording-alternates__links"},D={key:1,class:"text-gray-400"},E=L({__name:"RecordingPlaybackBlock",props:{recordId:{},urls:{}},setup(l){const $=l,h=I(),u=f(()=>{const e=$.urls||[],t=new Set,r=[];for(const o of e){const s=String(o??"").trim();!s||t.has(s)||(t.add(s),r.push(s))}return r}),a=f(()=>{const e=u.value;if(!e.length)return null;const t=e.find(n=>/\.mp4(\?|#|$)/i.test(n));if(t)return t;const r=e.find(n=>/\.m3u8(\?|#|$)/i.test(n)&&/vod-qcloud\.com/i.test(n));if(r)return r;const o=e.find(n=>/\.m3u8(\?|#|$)/i.test(n)&&/\.cos\.[^/]+\.myqcloud\.com/i.test(n));if(o)return o;const s=e.find(n=>/\.m3u8(\?|#|$)/i.test(n));return s||e[0]}),p=f(()=>{const e=u.value,t=a.value;return t?e.filter(r=>r!==t):e.slice(1)});function b(e){if(!e||typeof e!="string")return!1;const t=e.trim();return/^https?:\/\//i.test(t)?/\.(mp4|webm|ogg|mov|mkv|m4v|m3u8)(\?|#|$)/i.test(t):/\.(mp4|webm|ogg|mov|mkv|m4v|m3u8)(\?|#|$)/i.test(t)||t.startsWith("/uploads/")}function g(e){return h.getImageUrl(String(e||"").trim())}function S(e,t){const r=e.trim();return/\.mp4(\?|#|$)/i.test(r)?`MP4 ${t+1}`:/vod-qcloud\.com/i.test(r)&&/\.m3u8/i.test(r)?`点播 ${t+1}`:/\.cos\.[^/]+\.myqcloud\.com/i.test(r)&&/\.m3u8/i.test(r)?`COS HLS ${t+1}`:/\.m3u8/i.test(r)?`HLS ${t+1}`:`链接 ${t+1}`}return(e,t)=>{const r=q;return u.value.length?(i(),c("div",R,[d("div",C,[a.value?(i(),c(v,{key:0},[b(a.value)?(i(),m(H,{key:`${l.recordId}-${a.value}`,src:a.value},null,8,["src"])):(i(),m(r,{key:1,href:g(a.value),target:"_blank",type:"primary"},{default:_(()=>[...t[0]||(t[0]=[k(" 打开回放 ",-1)])]),_:1},8,["href"]))],64)):y("",!0),p.value.length?(i(),c("div",V,[t[1]||(t[1]=d("span",{class:"recording-alternates__label"},"备用地址",-1)),d("div",N,[(i(!0),c(v,null,B(p.value,(o,s)=>(i(),m(r,{key:`${l.recordId}-alt-${s}-${o.slice(-32)}`,href:g(o),target:"_blank",type:"primary",class:"recording-alternates__link"},{default:_(()=>[k(w(S(o,s)),1)]),_:2},1032,["href"]))),128))])])):y("",!0)])])):(i(),c("span",D,"暂无"))}}}),x=P(E,[["__scopeId","data-v-d67b2e91"]]);export{x as default};
|
||||
import{o as L,q as i,r as c,s as d,F as v,O as m,bq as q,D as _,L as k,P as y,G as B,p as f,T as w}from"./.pnpm-C1KTa5cB.js";import H from"./RecordingVideoPlayer-Dgsrn-1d.js";import{e as I,_ as P}from"./index-BzVi6Aa3.js";const R={key:0,class:"recording-list"},C={class:"recording-item"},V={key:1,class:"recording-alternates"},N={class:"recording-alternates__links"},D={key:1,class:"text-gray-400"},E=L({__name:"RecordingPlaybackBlock",props:{recordId:{},urls:{}},setup(l){const $=l,h=I(),u=f(()=>{const e=$.urls||[],t=new Set,r=[];for(const o of e){const s=String(o??"").trim();!s||t.has(s)||(t.add(s),r.push(s))}return r}),a=f(()=>{const e=u.value;if(!e.length)return null;const t=e.find(n=>/\.mp4(\?|#|$)/i.test(n));if(t)return t;const r=e.find(n=>/\.m3u8(\?|#|$)/i.test(n)&&/vod-qcloud\.com/i.test(n));if(r)return r;const o=e.find(n=>/\.m3u8(\?|#|$)/i.test(n)&&/\.cos\.[^/]+\.myqcloud\.com/i.test(n));if(o)return o;const s=e.find(n=>/\.m3u8(\?|#|$)/i.test(n));return s||e[0]}),p=f(()=>{const e=u.value,t=a.value;return t?e.filter(r=>r!==t):e.slice(1)});function b(e){if(!e||typeof e!="string")return!1;const t=e.trim();return/^https?:\/\//i.test(t)?/\.(mp4|webm|ogg|mov|mkv|m4v|m3u8)(\?|#|$)/i.test(t):/\.(mp4|webm|ogg|mov|mkv|m4v|m3u8)(\?|#|$)/i.test(t)||t.startsWith("/uploads/")}function g(e){return h.getImageUrl(String(e||"").trim())}function S(e,t){const r=e.trim();return/\.mp4(\?|#|$)/i.test(r)?`MP4 ${t+1}`:/vod-qcloud\.com/i.test(r)&&/\.m3u8/i.test(r)?`点播 ${t+1}`:/\.cos\.[^/]+\.myqcloud\.com/i.test(r)&&/\.m3u8/i.test(r)?`COS HLS ${t+1}`:/\.m3u8/i.test(r)?`HLS ${t+1}`:`链接 ${t+1}`}return(e,t)=>{const r=q;return u.value.length?(i(),c("div",R,[d("div",C,[a.value?(i(),c(v,{key:0},[b(a.value)?(i(),m(H,{key:`${l.recordId}-${a.value}`,src:a.value},null,8,["src"])):(i(),m(r,{key:1,href:g(a.value),target:"_blank",type:"primary"},{default:_(()=>[...t[0]||(t[0]=[k(" 打开回放 ",-1)])]),_:1},8,["href"]))],64)):y("",!0),p.value.length?(i(),c("div",V,[t[1]||(t[1]=d("span",{class:"recording-alternates__label"},"备用地址",-1)),d("div",N,[(i(!0),c(v,null,B(p.value,(o,s)=>(i(),m(r,{key:`${l.recordId}-alt-${s}-${o.slice(-32)}`,href:g(o),target:"_blank",type:"primary",class:"recording-alternates__link"},{default:_(()=>[k(w(S(o,s)),1)]),_:2},1032,["href"]))),128))])])):y("",!0)])])):(i(),c("span",D,"暂无"))}}}),x=P(E,[["__scopeId","data-v-d67b2e91"]]);export{x as default};
|
||||
+1
-1
@@ -1,2 +1,2 @@
|
||||
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/.pnpm-C1KTa5cB.js","assets/.pnpm-B3v8nGpq.css"])))=>i.map(i=>d[i]);
|
||||
import{o as K,ap as z,n as S,R as H,aq as G,q as v,r as p,s as f,ac as O,ad as W,b8 as $,aw as j,F as w,v as k,D as I,u as J,b5 as Q,w as X,P,L as Y,bq as Z,M as _,p as E,al as ee}from"./.pnpm-C1KTa5cB.js";import{e as ae,_ as ne}from"./index-Cxx1IoGl.js";const te={class:"recording-playback__shell"},re=["preload"],oe=["onKeydown"],le=K({__name:"RecordingVideoPlayer",props:{src:{}},setup(B){const V=B,A=ae(),b=_(null),m=_(null),c=_(!1),s=_(!1),t=_(!1);let d=null,r=null,l=0,u=null;function i(){u!==null&&(clearTimeout(u),u=null)}const y=E(()=>{const e=String(V.src||"").trim();return e?N(A.getImageUrl(e)):""}),D=E(()=>{if(!s.value)return"none";const e=y.value;return e&&!L(e)?"metadata":"none"}),q=E(()=>!s.value||t.value);function N(e){if(!e||!/^https?:\/\//i.test(e))return e;try{const a=new URL(e),n=a.hostname.toLowerCase();if(n.includes("myqcloud.com")||n.includes("vod-qcloud.com")||n.includes("vod-qcloud"))return a.pathname.includes("+")&&(a.pathname=a.pathname.replace(/\+/g,"%2B")),a.href}catch{}return e}function L(e){return/\.m3u8(\?|#|$)/i.test(e)}function U(){var n;t.value=!1,i();const e=m.value,a=(n=e==null?void 0:e.error)==null?void 0:n.code;a===MediaError.MEDIA_ERR_ABORTED||a===1||(c.value=!0)}function h(){d&&(d.destroy(),d=null);const e=m.value;e&&(e.pause(),e.removeAttribute("src"),e.load())}function R(e,a){let n=!1;const o=()=>{n||a!==l||(n=!0,i(),t.value=!1)};e.addEventListener("loadedmetadata",()=>o(),{once:!0}),e.addEventListener("loadeddata",()=>o(),{once:!0}),e.addEventListener("canplay",()=>o(),{once:!0}),e.addEventListener("playing",()=>o(),{once:!0}),e.addEventListener("progress",()=>{try{e.buffered.length>0&&e.buffered.end(0)>0&&o("progress-buffered")}catch{}},{passive:!0})}async function g(){if(!s.value)return;await S();const e=m.value,a=y.value;if(!e)return;if(!a){t.value=!1,i();return}const n=++l;if(t.value=!0,i(),h(),c.value=!1,L(a)){try{const o=await ee(()=>import("./.pnpm-C1KTa5cB.js").then(M=>M.dN),__vite__mapDeps([0,1])),x=o.default;if(x.isSupported()){d=new x({enableWorker:!0,lowLatencyMode:!1,maxBufferLength:20,maxMaxBufferLength:120}),d.on(o.Events.MANIFEST_PARSED,()=>{n===l&&(t.value=!1,i())}),d.on(o.Events.ERROR,(M,F)=>{if(F.fatal){if(n!==l)return;t.value=!1,i(),c.value=!0,h()}}),d.loadSource(a),d.attachMedia(e),u=setTimeout(()=>{u=null,n===l&&t.value&&(t.value=!1)},45e3);return}if(e.canPlayType("application/vnd.apple.mpegurl")){R(e,n),e.src=a,u=setTimeout(()=>{u=null,n===l&&t.value&&(t.value=!1)},45e3);return}}catch{if(n!==l)return;t.value=!1,i(),c.value=!0;return}if(n!==l)return;t.value=!1,i(),c.value=!0;return}R(e,n),e.src=a,u=setTimeout(()=>{u=null,n===l&&t.value&&(t.value=!1)},25e3)}function C(){const e=b.value;if(!e||typeof IntersectionObserver>"u"){s.value=!0,g();return}r=new IntersectionObserver(a=>{for(const n of a)if(n.isIntersecting){r==null||r.disconnect(),r=null,s.value=!0,g();break}},{root:null,rootMargin:"240px 0px",threshold:0}),r.observe(e)}function T(){r==null||r.disconnect(),r=null,s.value||(s.value=!0),g()}return z(()=>{S(()=>{C()})}),H(y,()=>{s.value&&g()}),G(()=>{r==null||r.disconnect(),r=null,i(),l++,h()}),(e,a)=>{const n=X,o=Z;return v(),p("div",{ref_key:"wrapRef",ref:b,class:"recording-playback"},[f("div",te,[O(f("video",{ref_key:"videoRef",ref:m,class:"recording-video",controls:"",playsinline:"","webkit-playsinline":"",preload:D.value,onError:U},null,40,re),[[W,!c.value]]),!c.value&&q.value?(v(),p("div",{key:0,class:"recording-playback__overlay",role:"button",tabindex:"0",onClick:T,onKeydown:$(j(T,["prevent"]),["enter"])},[s.value?(v(),p(w,{key:1},[k(n,{class:"recording-playback__spin"},{default:I(()=>[k(J(Q))]),_:1}),a[2]||(a[2]=f("span",null,"正在加载…",-1))],64)):(v(),p(w,{key:0},[a[0]||(a[0]=f("span",{class:"recording-playback__overlay-title"},"预览待加载",-1)),a[1]||(a[1]=f("span",{class:"recording-playback__overlay-desc"},"滚动至此处将自动加载(减轻并发卡顿),也可点击立即加载",-1))],64))],40,oe)):P("",!0)]),c.value?(v(),p(w,{key:0},[k(o,{href:y.value||"#",target:"_blank",type:"primary"},{default:I(()=>[...a[3]||(a[3]=[Y("在新窗口打开",-1)])]),_:1},8,["href"]),a[4]||(a[4]=f("div",{class:"recording-playback__hint"}," 内嵌播放失败(编码不支持、跨域或私有存储等)时可尝试在新窗口打开。 ",-1))],64)):P("",!0)],512)}}}),ie=ne(le,[["__scopeId","data-v-749b0199"]]);export{ie as default};
|
||||
import{o as K,ap as z,n as S,R as H,aq as G,q as v,r as p,s as f,ac as O,ad as W,b8 as $,aw as j,F as w,v as k,D as I,u as J,b5 as Q,w as X,P,L as Y,bq as Z,M as _,p as E,al as ee}from"./.pnpm-C1KTa5cB.js";import{e as ae,_ as ne}from"./index-BzVi6Aa3.js";const te={class:"recording-playback__shell"},re=["preload"],oe=["onKeydown"],le=K({__name:"RecordingVideoPlayer",props:{src:{}},setup(B){const V=B,A=ae(),b=_(null),m=_(null),c=_(!1),s=_(!1),t=_(!1);let d=null,r=null,l=0,u=null;function i(){u!==null&&(clearTimeout(u),u=null)}const y=E(()=>{const e=String(V.src||"").trim();return e?N(A.getImageUrl(e)):""}),D=E(()=>{if(!s.value)return"none";const e=y.value;return e&&!L(e)?"metadata":"none"}),q=E(()=>!s.value||t.value);function N(e){if(!e||!/^https?:\/\//i.test(e))return e;try{const a=new URL(e),n=a.hostname.toLowerCase();if(n.includes("myqcloud.com")||n.includes("vod-qcloud.com")||n.includes("vod-qcloud"))return a.pathname.includes("+")&&(a.pathname=a.pathname.replace(/\+/g,"%2B")),a.href}catch{}return e}function L(e){return/\.m3u8(\?|#|$)/i.test(e)}function U(){var n;t.value=!1,i();const e=m.value,a=(n=e==null?void 0:e.error)==null?void 0:n.code;a===MediaError.MEDIA_ERR_ABORTED||a===1||(c.value=!0)}function h(){d&&(d.destroy(),d=null);const e=m.value;e&&(e.pause(),e.removeAttribute("src"),e.load())}function R(e,a){let n=!1;const o=()=>{n||a!==l||(n=!0,i(),t.value=!1)};e.addEventListener("loadedmetadata",()=>o(),{once:!0}),e.addEventListener("loadeddata",()=>o(),{once:!0}),e.addEventListener("canplay",()=>o(),{once:!0}),e.addEventListener("playing",()=>o(),{once:!0}),e.addEventListener("progress",()=>{try{e.buffered.length>0&&e.buffered.end(0)>0&&o("progress-buffered")}catch{}},{passive:!0})}async function g(){if(!s.value)return;await S();const e=m.value,a=y.value;if(!e)return;if(!a){t.value=!1,i();return}const n=++l;if(t.value=!0,i(),h(),c.value=!1,L(a)){try{const o=await ee(()=>import("./.pnpm-C1KTa5cB.js").then(M=>M.dN),__vite__mapDeps([0,1])),x=o.default;if(x.isSupported()){d=new x({enableWorker:!0,lowLatencyMode:!1,maxBufferLength:20,maxMaxBufferLength:120}),d.on(o.Events.MANIFEST_PARSED,()=>{n===l&&(t.value=!1,i())}),d.on(o.Events.ERROR,(M,F)=>{if(F.fatal){if(n!==l)return;t.value=!1,i(),c.value=!0,h()}}),d.loadSource(a),d.attachMedia(e),u=setTimeout(()=>{u=null,n===l&&t.value&&(t.value=!1)},45e3);return}if(e.canPlayType("application/vnd.apple.mpegurl")){R(e,n),e.src=a,u=setTimeout(()=>{u=null,n===l&&t.value&&(t.value=!1)},45e3);return}}catch{if(n!==l)return;t.value=!1,i(),c.value=!0;return}if(n!==l)return;t.value=!1,i(),c.value=!0;return}R(e,n),e.src=a,u=setTimeout(()=>{u=null,n===l&&t.value&&(t.value=!1)},25e3)}function C(){const e=b.value;if(!e||typeof IntersectionObserver>"u"){s.value=!0,g();return}r=new IntersectionObserver(a=>{for(const n of a)if(n.isIntersecting){r==null||r.disconnect(),r=null,s.value=!0,g();break}},{root:null,rootMargin:"240px 0px",threshold:0}),r.observe(e)}function T(){r==null||r.disconnect(),r=null,s.value||(s.value=!0),g()}return z(()=>{S(()=>{C()})}),H(y,()=>{s.value&&g()}),G(()=>{r==null||r.disconnect(),r=null,i(),l++,h()}),(e,a)=>{const n=X,o=Z;return v(),p("div",{ref_key:"wrapRef",ref:b,class:"recording-playback"},[f("div",te,[O(f("video",{ref_key:"videoRef",ref:m,class:"recording-video",controls:"",playsinline:"","webkit-playsinline":"",preload:D.value,onError:U},null,40,re),[[W,!c.value]]),!c.value&&q.value?(v(),p("div",{key:0,class:"recording-playback__overlay",role:"button",tabindex:"0",onClick:T,onKeydown:$(j(T,["prevent"]),["enter"])},[s.value?(v(),p(w,{key:1},[k(n,{class:"recording-playback__spin"},{default:I(()=>[k(J(Q))]),_:1}),a[2]||(a[2]=f("span",null,"正在加载…",-1))],64)):(v(),p(w,{key:0},[a[0]||(a[0]=f("span",{class:"recording-playback__overlay-title"},"预览待加载",-1)),a[1]||(a[1]=f("span",{class:"recording-playback__overlay-desc"},"滚动至此处将自动加载(减轻并发卡顿),也可点击立即加载",-1))],64))],40,oe)):P("",!0)]),c.value?(v(),p(w,{key:0},[k(o,{href:y.value||"#",target:"_blank",type:"primary"},{default:I(()=>[...a[3]||(a[3]=[Y("在新窗口打开",-1)])]),_:1},8,["href"]),a[4]||(a[4]=f("div",{class:"recording-playback__hint"}," 内嵌播放失败(编码不支持、跨域或私有存储等)时可尝试在新窗口打开。 ",-1))],64)):P("",!0)],512)}}}),ie=ne(le,[["__scopeId","data-v-749b0199"]]);export{ie as default};
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
@@ -1,2 +1,2 @@
|
||||
import{o as T,q as e,r as t,v as m,b7 as V,s as d,D as w,L as I,K as E,P as r,F as u,G as v,T as g,O as C,bB as z,M as p}from"./.pnpm-C1KTa5cB.js";import{a4 as L}from"./tcm-DJJMkqKG.js";import{i as M,_ as S}from"./index-Cxx1IoGl.js";/* empty css */const D={class:"tracking-timeline-wrap"},F={key:0,class:"timeline-input"},H={class:"input-actions"},q={key:1,class:"tracking-timeline"},A={class:"timeline-date"},G={class:"timeline-body"},K={key:0,class:"timeline-content"},O=T({__name:"TrackingNoteTimeline",props:{notes:{},diagnosisId:{},readonly:{type:Boolean}},emits:["refresh"],setup(n,{emit:f}){const c=n,y=f,o=p(""),l=p(!1),_=s=>s?s.split(`
|
||||
import{o as T,q as e,r as t,v as m,b7 as V,s as d,D as w,L as I,K as E,P as r,F as u,G as v,T as g,O as C,bB as z,M as p}from"./.pnpm-C1KTa5cB.js";import{a4 as L}from"./tcm-BlzCcc0R.js";import{i as M,_ as S}from"./index-BzVi6Aa3.js";/* empty css */const D={class:"tracking-timeline-wrap"},F={key:0,class:"timeline-input"},H={class:"input-actions"},q={key:1,class:"tracking-timeline"},A={class:"timeline-date"},G={class:"timeline-body"},K={key:0,class:"timeline-content"},O=T({__name:"TrackingNoteTimeline",props:{notes:{},diagnosisId:{},readonly:{type:Boolean}},emits:["refresh"],setup(n,{emit:f}){const c=n,y=f,o=p(""),l=p(!1),_=s=>s?s.split(`
|
||||
`).filter(Boolean):[],k=async()=>{if(!c.diagnosisId)return;const s=o.value.trim();if(s){l.value=!0;try{await L({diagnosis_id:c.diagnosisId,tracking_content:s}),M.msgSuccess("已添加"),o.value="",y("refresh")}finally{l.value=!1}}};return(s,i)=>{const b=V,h=E,B=z;return e(),t("div",D,[!n.readonly&&n.diagnosisId?(e(),t("div",F,[m(b,{modelValue:o.value,"onUpdate:modelValue":i[0]||(i[0]=a=>o.value=a),type:"textarea",rows:2,placeholder:"输入跟踪备注,回车换行;保存后将以「[HH:MM] 内容」追加到当天记录",maxlength:"1000","show-word-limit":"",resize:"none",disabled:l.value},null,8,["modelValue","disabled"]),d("div",H,[m(h,{type:"primary",size:"small",loading:l.value,disabled:!o.value.trim(),onClick:k},{default:w(()=>[...i[1]||(i[1]=[I(" 添加 ",-1)])]),_:1},8,["loading","disabled"])])])):r("",!0),n.notes.length?(e(),t("div",q,[(e(!0),t(u,null,v(n.notes,a=>(e(),t("div",{key:a.id,class:"timeline-node"},[i[2]||(i[2]=d("div",{class:"timeline-dot"},null,-1)),d("div",A,g(a.note_date),1),d("div",G,[a.content?(e(),t("div",K,[(e(!0),t(u,null,v(_(a.content),(x,N)=>(e(),t("div",{key:N,class:"content-line"},g(x),1))),128))])):r("",!0)])]))),128))])):r("",!0),!n.notes.length&&n.readonly?(e(),C(B,{key:2,description:"暂无跟踪备注","image-size":48})):r("",!0)])}}}),Q=S(O,[["__scopeId","data-v-82b635bd"]]);export{Q as default};
|
||||
+1
-1
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
import{_ as o}from"./account-adjust.vue_vue_type_script_setup_true_lang-Cr9oMM11.js";import"./.pnpm-C1KTa5cB.js";import"./index-Da4XqmJd.js";import"./index-BzVi6Aa3.js";export{o as default};
|
||||
@@ -1 +0,0 @@
|
||||
import{_ as o}from"./account-adjust.vue_vue_type_script_setup_true_lang-B-w5z28y.js";import"./.pnpm-C1KTa5cB.js";import"./index-B3Fu5tec.js";import"./index-Cxx1IoGl.js";export{o as default};
|
||||
+1
-1
@@ -1 +1 @@
|
||||
import{o as h,R as v,q,O as B,D as l,s as D,v as t,b9 as F,u as n,b6 as I,L as u,T as w,bc as j,bf as S,b7 as T,a8 as y,ba as U,p as G}from"./.pnpm-C1KTa5cB.js";import{_ as L}from"./index-B3Fu5tec.js";import{i as V}from"./index-Cxx1IoGl.js";const M={class:"pr-8"},H=h({__name:"account-adjust",props:{show:{type:Boolean,required:!0},value:{type:[Number,String],required:!0}},emits:["update:show","confirm"],setup(d,{emit:k}){const s=y(),i=d,f=k,o=U({action:1,num:"",remark:""}),m=y(),c=G(()=>Number(i.value)+Number(o.num)*(o.action==1?1:-1)),R={num:[{required:!0,message:"请输入调整的金额"}]},g=e=>{if(e.includes("-"))return V.msgError("请输入正整数");o.num=e},x=async()=>{var e;await((e=s.value)==null?void 0:e.validate()),f("confirm",o)},E=()=>{var e;f("update:show",!1),(e=s.value)==null||e.resetFields()};return v(()=>i.show,e=>{var a,r;e?(a=m.value)==null||a.open():(r=m.value)==null||r.close()}),v(c,e=>{e<0&&(V.msgError("调整后余额需大于0"),o.num="")}),(e,a)=>{const r=I,_=S,C=j,b=T,N=F;return q(),B(L,{ref_key:"popupRef",ref:m,title:"余额调整",width:"500px",onConfirm:x,async:!0,onClose:E},{default:l(()=>[D("div",M,[t(N,{ref_key:"formRef",ref:s,model:n(o),"label-width":"120px",rules:R},{default:l(()=>[t(r,{label:"当前余额"},{default:l(()=>[u("¥ "+w(d.value),1)]),_:1}),t(r,{label:"余额增减",required:"",prop:"action"},{default:l(()=>[t(C,{modelValue:n(o).action,"onUpdate:modelValue":a[0]||(a[0]=p=>n(o).action=p)},{default:l(()=>[t(_,{value:1},{default:l(()=>[...a[2]||(a[2]=[u("增加余额",-1)])]),_:1}),t(_,{value:2},{default:l(()=>[...a[3]||(a[3]=[u("扣减余额",-1)])]),_:1})]),_:1},8,["modelValue"])]),_:1}),t(r,{label:"调整余额",prop:"num"},{default:l(()=>[t(b,{"model-value":n(o).num,placeholder:"请输入调整的金额",type:"number",onInput:g},null,8,["model-value"])]),_:1}),t(r,{label:"调整后余额"},{default:l(()=>[u(" ¥ "+w(n(c)),1)]),_:1}),t(r,{label:"备注",prop:"remark"},{default:l(()=>[t(b,{modelValue:n(o).remark,"onUpdate:modelValue":a[1]||(a[1]=p=>n(o).remark=p),type:"textarea",rows:4},null,8,["modelValue"])]),_:1})]),_:1},8,["model"])])]),_:1},512)}}});export{H as _};
|
||||
import{o as h,R as v,q,O as B,D as l,s as D,v as t,b9 as F,u as n,b6 as I,L as u,T as w,bc as j,bf as S,b7 as T,a8 as y,ba as U,p as G}from"./.pnpm-C1KTa5cB.js";import{_ as L}from"./index-Da4XqmJd.js";import{i as V}from"./index-BzVi6Aa3.js";const M={class:"pr-8"},H=h({__name:"account-adjust",props:{show:{type:Boolean,required:!0},value:{type:[Number,String],required:!0}},emits:["update:show","confirm"],setup(d,{emit:k}){const s=y(),i=d,f=k,o=U({action:1,num:"",remark:""}),m=y(),c=G(()=>Number(i.value)+Number(o.num)*(o.action==1?1:-1)),R={num:[{required:!0,message:"请输入调整的金额"}]},g=e=>{if(e.includes("-"))return V.msgError("请输入正整数");o.num=e},x=async()=>{var e;await((e=s.value)==null?void 0:e.validate()),f("confirm",o)},E=()=>{var e;f("update:show",!1),(e=s.value)==null||e.resetFields()};return v(()=>i.show,e=>{var a,r;e?(a=m.value)==null||a.open():(r=m.value)==null||r.close()}),v(c,e=>{e<0&&(V.msgError("调整后余额需大于0"),o.num="")}),(e,a)=>{const r=I,_=S,C=j,b=T,N=F;return q(),B(L,{ref_key:"popupRef",ref:m,title:"余额调整",width:"500px",onConfirm:x,async:!0,onClose:E},{default:l(()=>[D("div",M,[t(N,{ref_key:"formRef",ref:s,model:n(o),"label-width":"120px",rules:R},{default:l(()=>[t(r,{label:"当前余额"},{default:l(()=>[u("¥ "+w(d.value),1)]),_:1}),t(r,{label:"余额增减",required:"",prop:"action"},{default:l(()=>[t(C,{modelValue:n(o).action,"onUpdate:modelValue":a[0]||(a[0]=p=>n(o).action=p)},{default:l(()=>[t(_,{value:1},{default:l(()=>[...a[2]||(a[2]=[u("增加余额",-1)])]),_:1}),t(_,{value:2},{default:l(()=>[...a[3]||(a[3]=[u("扣减余额",-1)])]),_:1})]),_:1},8,["modelValue"])]),_:1}),t(r,{label:"调整余额",prop:"num"},{default:l(()=>[t(b,{"model-value":n(o).num,placeholder:"请输入调整的金额",type:"number",onInput:g},null,8,["model-value"])]),_:1}),t(r,{label:"调整后余额"},{default:l(()=>[u(" ¥ "+w(n(c)),1)]),_:1}),t(r,{label:"备注",prop:"remark"},{default:l(()=>[t(b,{modelValue:n(o).remark,"onUpdate:modelValue":a[1]||(a[1]=p=>n(o).remark=p),type:"textarea",rows:4},null,8,["modelValue"])]),_:1})]),_:1},8,["model"])])]),_:1},512)}}});export{H as _};
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
import{_ as o}from"./add-nav.vue_vue_type_script_setup_true_lang-DTFBmDjB.js";import"./.pnpm-C1KTa5cB.js";import"./index-CQoyCdiY.js";import"./index-BzVi6Aa3.js";import"./picker-CoG_3YpY.js";import"./index-Da4XqmJd.js";import"./index.vue_vue_type_script_setup_true_lang-CerDQAzd.js";import"./article-BB6kpo3D.js";import"./usePaging--aAVZOna.js";import"./picker-DFb6DRYi.js";import"./index-BjEe2GZP.js";import"./index-CEYf_4x5.js";import"./file-DqKLtehk.js";import"./index.vue_vue_type_script_setup_true_lang-0C3mdvgv.js";export{o as default};
|
||||
@@ -1 +0,0 @@
|
||||
import{_ as o}from"./add-nav.vue_vue_type_script_setup_true_lang-DW1znYtt.js";import"./.pnpm-C1KTa5cB.js";import"./index-B-rCQZ_o.js";import"./index-Cxx1IoGl.js";import"./picker-BaeJGhM3.js";import"./index-B3Fu5tec.js";import"./index.vue_vue_type_script_setup_true_lang-CerDQAzd.js";import"./article-BaVF8uYW.js";import"./usePaging--aAVZOna.js";import"./picker-CNX2EN3u.js";import"./index-BhA3CyxZ.js";import"./index-CZyPOnHt.js";import"./file-D2AwthSZ.js";import"./index.vue_vue_type_script_setup_true_lang-0C3mdvgv.js";export{o as default};
|
||||
+1
-1
@@ -1 +1 @@
|
||||
import{o as E,q as p,r as C,s as l,v as a,u as c,bQ as B,C as N,D as d,O as $,b7 as z,b6 as D,I,K as A,L,p as R}from"./.pnpm-C1KTa5cB.js";import{_ as q}from"./index-B-rCQZ_o.js";import{_ as F}from"./picker-BaeJGhM3.js";import{_ as K}from"./picker-CNX2EN3u.js";import{c as O,i as r}from"./index-Cxx1IoGl.js";const P={class:"bg-fill-light flex items-center w-full p-4 mb-4"},Q={class:"upload-btn w-[60px] h-[60px]"},S={class:"ml-3 flex-1"},T={class:"flex items-center"},j={class:"flex items-center mt-[18px]"},G={class:"flex-1 flex items-center"},H={class:"drag-move cursor-move ml-auto"},Z=E({__name:"add-nav",props:{modelValue:{type:Array,default:()=>[]},max:{type:Number,default:100},min:{type:Number,default:1}},emits:["update:modelValue"],setup(_,{emit:f}){const t=_,V=f,m=R({get(){return t.modelValue},set(s){V("update:modelValue",s)}}),x=()=>{var s;((s=t.modelValue)==null?void 0:s.length)<t.max?m.value.push({image:"",name:"导航名称",link:{},is_show:"1"}):r.msgError(`最多添加${t.max}个`)},v=s=>{var e;if(((e=t.modelValue)==null?void 0:e.length)<=t.min)return r.msgError(`最少保留${t.min}个`);m.value.splice(s,1)};return(s,e)=>{const u=O,g=K,b=z,h=F,k=I,w=D,y=q,U=A;return p(),C("div",null,[l("div",null,[a(c(B),{class:"draggable",modelValue:c(m),"onUpdate:modelValue":e[0]||(e[0]=o=>N(m)?m.value=o:null),animation:"300",handle:".drag-move","item-key":"index"},{item:d(({element:o,index:i})=>[(p(),$(y,{class:"w-[467px]",key:i,onClose:n=>v(i)},{default:d(()=>[l("div",P,[a(g,{modelValue:o.image,"onUpdate:modelValue":n=>o.image=n,"upload-class":"bg-body",size:"60px","exclude-domain":""},{upload:d(()=>[l("div",Q,[a(u,{name:"el-icon-Plus",size:20})])]),_:1},8,["modelValue","onUpdate:modelValue"]),l("div",S,[l("div",T,[e[1]||(e[1]=l("span",{class:"text-tx-regular flex-none mr-3"},"名称",-1)),a(b,{modelValue:o.name,"onUpdate:modelValue":n=>o.name=n,placeholder:"请输入名称"},null,8,["modelValue","onUpdate:modelValue"])]),l("div",j,[e[2]||(e[2]=l("span",{class:"text-tx-regular flex-none mr-3"},"链接",-1)),a(h,{modelValue:o.link,"onUpdate:modelValue":n=>o.link=n},null,8,["modelValue","onUpdate:modelValue"])]),a(w,{label:"是否显示",class:"mt-[18px]"},{default:d(()=>[l("div",G,[a(k,{modelValue:o.is_show,"onUpdate:modelValue":n=>o.is_show=n,"active-value":"1","inactive-value":"0"},null,8,["modelValue","onUpdate:modelValue"]),l("div",H,[a(u,{name:"el-icon-Rank",size:"18"})])])]),_:2},1024)])])]),_:2},1032,["onClose"]))]),_:1},8,["modelValue"])]),l("div",null,[a(U,{type:"primary",onClick:x},{default:d(()=>[...e[3]||(e[3]=[L("添加",-1)])]),_:1})])])}}});export{Z as _};
|
||||
import{o as E,q as p,r as C,s as l,v as a,u as c,bQ as B,C as N,D as d,O as $,b7 as z,b6 as D,I,K as A,L,p as R}from"./.pnpm-C1KTa5cB.js";import{_ as q}from"./index-CQoyCdiY.js";import{_ as F}from"./picker-CoG_3YpY.js";import{_ as K}from"./picker-DFb6DRYi.js";import{c as O,i as r}from"./index-BzVi6Aa3.js";const P={class:"bg-fill-light flex items-center w-full p-4 mb-4"},Q={class:"upload-btn w-[60px] h-[60px]"},S={class:"ml-3 flex-1"},T={class:"flex items-center"},j={class:"flex items-center mt-[18px]"},G={class:"flex-1 flex items-center"},H={class:"drag-move cursor-move ml-auto"},Z=E({__name:"add-nav",props:{modelValue:{type:Array,default:()=>[]},max:{type:Number,default:100},min:{type:Number,default:1}},emits:["update:modelValue"],setup(_,{emit:f}){const t=_,V=f,m=R({get(){return t.modelValue},set(s){V("update:modelValue",s)}}),x=()=>{var s;((s=t.modelValue)==null?void 0:s.length)<t.max?m.value.push({image:"",name:"导航名称",link:{},is_show:"1"}):r.msgError(`最多添加${t.max}个`)},v=s=>{var e;if(((e=t.modelValue)==null?void 0:e.length)<=t.min)return r.msgError(`最少保留${t.min}个`);m.value.splice(s,1)};return(s,e)=>{const u=O,g=K,b=z,h=F,k=I,w=D,y=q,U=A;return p(),C("div",null,[l("div",null,[a(c(B),{class:"draggable",modelValue:c(m),"onUpdate:modelValue":e[0]||(e[0]=o=>N(m)?m.value=o:null),animation:"300",handle:".drag-move","item-key":"index"},{item:d(({element:o,index:i})=>[(p(),$(y,{class:"w-[467px]",key:i,onClose:n=>v(i)},{default:d(()=>[l("div",P,[a(g,{modelValue:o.image,"onUpdate:modelValue":n=>o.image=n,"upload-class":"bg-body",size:"60px","exclude-domain":""},{upload:d(()=>[l("div",Q,[a(u,{name:"el-icon-Plus",size:20})])]),_:1},8,["modelValue","onUpdate:modelValue"]),l("div",S,[l("div",T,[e[1]||(e[1]=l("span",{class:"text-tx-regular flex-none mr-3"},"名称",-1)),a(b,{modelValue:o.name,"onUpdate:modelValue":n=>o.name=n,placeholder:"请输入名称"},null,8,["modelValue","onUpdate:modelValue"])]),l("div",j,[e[2]||(e[2]=l("span",{class:"text-tx-regular flex-none mr-3"},"链接",-1)),a(h,{modelValue:o.link,"onUpdate:modelValue":n=>o.link=n},null,8,["modelValue","onUpdate:modelValue"])]),a(w,{label:"是否显示",class:"mt-[18px]"},{default:d(()=>[l("div",G,[a(k,{modelValue:o.is_show,"onUpdate:modelValue":n=>o.is_show=n,"active-value":"1","inactive-value":"0"},null,8,["modelValue","onUpdate:modelValue"]),l("div",H,[a(u,{name:"el-icon-Rank",size:"18"})])])]),_:2},1024)])])]),_:2},1032,["onClose"]))]),_:1},8,["modelValue"])]),l("div",null,[a(U,{type:"primary",onClick:x},{default:d(()=>[...e[3]||(e[3]=[L("添加",-1)])]),_:1})])])}}});export{Z as _};
|
||||
+1
-1
@@ -1 +1 @@
|
||||
import{r as n}from"./index-Cxx1IoGl.js";function e(t){return n.get({url:"/auth.admin/lists",params:t},{ignoreCancelToken:!0})}function i(t){return n.post({url:"/auth.admin/add",params:t})}function r(t){return n.post({url:"/auth.admin/edit",params:t})}function u(t){return n.post({url:"/auth.admin/delete",params:t})}function d(t){return n.get({url:"/auth.admin/detail",params:t})}export{e as a,r as b,u as c,i as d,d as e};
|
||||
import{r as n}from"./index-BzVi6Aa3.js";function e(t){return n.get({url:"/auth.admin/lists",params:t},{ignoreCancelToken:!0})}function i(t){return n.post({url:"/auth.admin/add",params:t})}function r(t){return n.post({url:"/auth.admin/edit",params:t})}function u(t){return n.post({url:"/auth.admin/delete",params:t})}function d(t){return n.get({url:"/auth.admin/detail",params:t})}export{e as a,r as b,u as c,i as d,d as e};
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
@@ -1 +1 @@
|
||||
import{r as e}from"./index-Cxx1IoGl.js";function a(t){return e.get({url:"/article.articleCate/lists",params:t})}function l(t){return e.get({url:"/article.articleCate/all",params:t})}function i(t){return e.post({url:"/article.articleCate/add",params:t})}function c(t){return e.post({url:"/article.articleCate/edit",params:t})}function u(t){return e.post({url:"/article.articleCate/delete",params:t})}function n(t){return e.get({url:"/article.articleCate/detail",params:t})}function s(t){return e.post({url:"/article.articleCate/updateStatus",params:t})}function o(t){return e.get({url:"/article.article/lists",params:t})}function d(t){return e.post({url:"/article.article/add",params:t})}function f(t){return e.post({url:"/article.article/edit",params:t})}function C(t){return e.post({url:"/article.article/delete",params:t})}function p(t){return e.get({url:"/article.article/detail",params:t})}function g(t){return e.post({url:"/article.article/updateStatus",params:t})}export{a,u as b,s as c,c as d,i as e,n as f,p as g,f as h,d as i,l as j,o as k,g as l,C as m};
|
||||
import{r as e}from"./index-BzVi6Aa3.js";function a(t){return e.get({url:"/article.articleCate/lists",params:t})}function l(t){return e.get({url:"/article.articleCate/all",params:t})}function i(t){return e.post({url:"/article.articleCate/add",params:t})}function c(t){return e.post({url:"/article.articleCate/edit",params:t})}function u(t){return e.post({url:"/article.articleCate/delete",params:t})}function n(t){return e.get({url:"/article.articleCate/detail",params:t})}function s(t){return e.post({url:"/article.articleCate/updateStatus",params:t})}function o(t){return e.get({url:"/article.article/lists",params:t})}function d(t){return e.post({url:"/article.article/add",params:t})}function f(t){return e.post({url:"/article.article/edit",params:t})}function C(t){return e.post({url:"/article.article/delete",params:t})}function p(t){return e.get({url:"/article.article/detail",params:t})}function g(t){return e.post({url:"/article.article/updateStatus",params:t})}export{a,u as b,s as c,c as d,i as e,n as f,p as g,f as h,d as i,l as j,o as k,g as l,C as m};
|
||||
+1
-1
@@ -1 +1 @@
|
||||
import{r as e}from"./index-Cxx1IoGl.js";function r(s){return e.get({url:"/asset.AssetUser/lists",params:s})}function u(s){return e.post({url:"/asset.AssetUser/add",params:s})}function a(s){return e.post({url:"/asset.AssetUser/edit",params:s})}function i(s){return e.post({url:"/asset.AssetUser/delete",params:s})}function o(s){return e.get({url:"/asset.AssetResource/lists",params:s})}function n(s){return e.post({url:"/asset.AssetResource/add",params:s})}function A(s){return e.post({url:"/asset.AssetResource/edit",params:s})}function c(s){return e.post({url:"/asset.AssetResource/delete",params:s})}export{o as a,c as b,A as c,n as d,r as e,a as f,i as g,u as h};
|
||||
import{r as e}from"./index-BzVi6Aa3.js";function r(s){return e.get({url:"/asset.AssetUser/lists",params:s})}function u(s){return e.post({url:"/asset.AssetUser/add",params:s})}function a(s){return e.post({url:"/asset.AssetUser/edit",params:s})}function i(s){return e.post({url:"/asset.AssetUser/delete",params:s})}function o(s){return e.get({url:"/asset.AssetResource/lists",params:s})}function n(s){return e.post({url:"/asset.AssetResource/add",params:s})}function A(s){return e.post({url:"/asset.AssetResource/edit",params:s})}function c(s){return e.post({url:"/asset.AssetResource/delete",params:s})}export{o as a,c as b,A as c,n as d,r as e,a as f,i as g,u as h};
|
||||
@@ -0,0 +1 @@
|
||||
import{_ as o}from"./attr.vue_vue_type_script_setup_true_lang-gUeVRrBW.js";import"./.pnpm-C1KTa5cB.js";import"./index-CQoyCdiY.js";import"./index-BzVi6Aa3.js";import"./picker-CoG_3YpY.js";import"./index-Da4XqmJd.js";import"./index.vue_vue_type_script_setup_true_lang-CerDQAzd.js";import"./article-BB6kpo3D.js";import"./usePaging--aAVZOna.js";import"./picker-DFb6DRYi.js";import"./index-BjEe2GZP.js";import"./index-CEYf_4x5.js";import"./file-DqKLtehk.js";import"./index.vue_vue_type_script_setup_true_lang-0C3mdvgv.js";export{o as default};
|
||||
@@ -1 +0,0 @@
|
||||
import{_ as o}from"./attr.vue_vue_type_script_setup_true_lang-DMX38Vjl.js";import"./.pnpm-C1KTa5cB.js";import"./index-B-rCQZ_o.js";import"./index-Cxx1IoGl.js";import"./picker-BaeJGhM3.js";import"./index-B3Fu5tec.js";import"./index.vue_vue_type_script_setup_true_lang-CerDQAzd.js";import"./article-BaVF8uYW.js";import"./usePaging--aAVZOna.js";import"./picker-CNX2EN3u.js";import"./index-BhA3CyxZ.js";import"./index-CZyPOnHt.js";import"./file-D2AwthSZ.js";import"./index.vue_vue_type_script_setup_true_lang-0C3mdvgv.js";import"./index.vue_vue_type_script_setup_true_lang-CScz2Hxp.js";export{o as default};
|
||||
@@ -1 +0,0 @@
|
||||
import{_ as o}from"./attr.vue_vue_type_script_setup_true_lang-D_pPXHcq.js";import"./.pnpm-C1KTa5cB.js";import"./add-nav.vue_vue_type_script_setup_true_lang-DW1znYtt.js";import"./index-B-rCQZ_o.js";import"./index-Cxx1IoGl.js";import"./picker-BaeJGhM3.js";import"./index-B3Fu5tec.js";import"./index.vue_vue_type_script_setup_true_lang-CerDQAzd.js";import"./article-BaVF8uYW.js";import"./usePaging--aAVZOna.js";import"./picker-CNX2EN3u.js";import"./index-BhA3CyxZ.js";import"./index-CZyPOnHt.js";import"./file-D2AwthSZ.js";import"./index.vue_vue_type_script_setup_true_lang-0C3mdvgv.js";export{o as default};
|
||||
@@ -0,0 +1 @@
|
||||
import{_ as o}from"./attr.vue_vue_type_script_setup_true_lang-DQQ5tkNK.js";import"./.pnpm-C1KTa5cB.js";import"./picker-DFb6DRYi.js";import"./index-Da4XqmJd.js";import"./index-BzVi6Aa3.js";import"./index-BjEe2GZP.js";import"./index.vue_vue_type_script_setup_true_lang-CerDQAzd.js";import"./index-CQoyCdiY.js";import"./index-CEYf_4x5.js";import"./file-DqKLtehk.js";import"./index.vue_vue_type_script_setup_true_lang-0C3mdvgv.js";import"./usePaging--aAVZOna.js";export{o as default};
|
||||
@@ -0,0 +1 @@
|
||||
import{_ as o}from"./attr.vue_vue_type_script_setup_true_lang-C1sD3H0v.js";import"./.pnpm-C1KTa5cB.js";import"./add-nav.vue_vue_type_script_setup_true_lang-DTFBmDjB.js";import"./index-CQoyCdiY.js";import"./index-BzVi6Aa3.js";import"./picker-CoG_3YpY.js";import"./index-Da4XqmJd.js";import"./index.vue_vue_type_script_setup_true_lang-CerDQAzd.js";import"./article-BB6kpo3D.js";import"./usePaging--aAVZOna.js";import"./picker-DFb6DRYi.js";import"./index-BjEe2GZP.js";import"./index-CEYf_4x5.js";import"./file-DqKLtehk.js";import"./index.vue_vue_type_script_setup_true_lang-0C3mdvgv.js";export{o as default};
|
||||
@@ -1 +0,0 @@
|
||||
import{_ as o}from"./attr.vue_vue_type_script_setup_true_lang-keaJgqFX.js";import"./.pnpm-C1KTa5cB.js";import"./add-nav.vue_vue_type_script_setup_true_lang-DW1znYtt.js";import"./index-B-rCQZ_o.js";import"./index-Cxx1IoGl.js";import"./picker-BaeJGhM3.js";import"./index-B3Fu5tec.js";import"./index.vue_vue_type_script_setup_true_lang-CerDQAzd.js";import"./article-BaVF8uYW.js";import"./usePaging--aAVZOna.js";import"./picker-CNX2EN3u.js";import"./index-BhA3CyxZ.js";import"./index-CZyPOnHt.js";import"./file-D2AwthSZ.js";import"./index.vue_vue_type_script_setup_true_lang-0C3mdvgv.js";export{o as default};
|
||||
@@ -0,0 +1 @@
|
||||
import{_ as o}from"./attr.vue_vue_type_script_setup_true_lang-DLUgRb_6.js";import"./.pnpm-C1KTa5cB.js";import"./index-CQoyCdiY.js";import"./index-BzVi6Aa3.js";import"./picker-CoG_3YpY.js";import"./index-Da4XqmJd.js";import"./index.vue_vue_type_script_setup_true_lang-CerDQAzd.js";import"./article-BB6kpo3D.js";import"./usePaging--aAVZOna.js";import"./picker-DFb6DRYi.js";import"./index-BjEe2GZP.js";import"./index-CEYf_4x5.js";import"./file-DqKLtehk.js";import"./index.vue_vue_type_script_setup_true_lang-0C3mdvgv.js";import"./index.vue_vue_type_script_setup_true_lang-CScz2Hxp.js";export{o as default};
|
||||
@@ -1 +0,0 @@
|
||||
import{_ as o}from"./attr.vue_vue_type_script_setup_true_lang-Dot-5pr7.js";import"./.pnpm-C1KTa5cB.js";import"./index.vue_vue_type_script_setup_true_lang-CScz2Hxp.js";import"./picker-CNX2EN3u.js";import"./index-B3Fu5tec.js";import"./index-Cxx1IoGl.js";import"./index-BhA3CyxZ.js";import"./index.vue_vue_type_script_setup_true_lang-CerDQAzd.js";import"./index-B-rCQZ_o.js";import"./index-CZyPOnHt.js";import"./file-D2AwthSZ.js";import"./index.vue_vue_type_script_setup_true_lang-0C3mdvgv.js";import"./usePaging--aAVZOna.js";export{o as default};
|
||||
@@ -1 +0,0 @@
|
||||
import{_ as o}from"./attr.vue_vue_type_script_setup_true_lang-BSBOWQnL.js";import"./.pnpm-C1KTa5cB.js";import"./index-B-rCQZ_o.js";import"./index-Cxx1IoGl.js";import"./picker-BaeJGhM3.js";import"./index-B3Fu5tec.js";import"./index.vue_vue_type_script_setup_true_lang-CerDQAzd.js";import"./article-BaVF8uYW.js";import"./usePaging--aAVZOna.js";import"./picker-CNX2EN3u.js";import"./index-BhA3CyxZ.js";import"./index-CZyPOnHt.js";import"./file-D2AwthSZ.js";import"./index.vue_vue_type_script_setup_true_lang-0C3mdvgv.js";export{o as default};
|
||||
@@ -1 +0,0 @@
|
||||
import{_ as o}from"./attr.vue_vue_type_script_setup_true_lang-DneoXzdE.js";import"./.pnpm-C1KTa5cB.js";import"./picker-CNX2EN3u.js";import"./index-B3Fu5tec.js";import"./index-Cxx1IoGl.js";import"./index-BhA3CyxZ.js";import"./index.vue_vue_type_script_setup_true_lang-CerDQAzd.js";import"./index-B-rCQZ_o.js";import"./index-CZyPOnHt.js";import"./file-D2AwthSZ.js";import"./index.vue_vue_type_script_setup_true_lang-0C3mdvgv.js";import"./usePaging--aAVZOna.js";export{o as default};
|
||||
+1
-1
@@ -1 +1 @@
|
||||
import{o as b,q as c,r as V,s as v,v as t,D as a,Y as x,X as E,u as l,b9 as g,F as k,p as w}from"./.pnpm-C1KTa5cB.js";import{_ as p}from"./menu-set.vue_vue_type_script_setup_true_lang-DBOQoi51.js";import"./index-B-rCQZ_o.js";import"./index-Cxx1IoGl.js";import"./picker-BaeJGhM3.js";import"./index-B3Fu5tec.js";import"./index.vue_vue_type_script_setup_true_lang-CerDQAzd.js";import"./article-BaVF8uYW.js";import"./usePaging--aAVZOna.js";import"./picker-CNX2EN3u.js";import"./index-BhA3CyxZ.js";import"./index-CZyPOnHt.js";import"./file-D2AwthSZ.js";import"./index.vue_vue_type_script_setup_true_lang-0C3mdvgv.js";const $=b({__name:"attr",props:{modelValue:{type:Object,default:()=>({nav:[],menu:{}})}},emits:["update:modelValue"],setup(s,{emit:u}){const d=s,i=u,o=w({get(){return d.modelValue},set(n){i("update:modelValue",n)}});return(n,e)=>{const r=E,f=x,_=g;return c(),V(k,null,[e[2]||(e[2]=v("div",{class:"title flex items-center before:w-[3px] before:h-[14px] before:block before:bg-primary before:mr-2"}," pc导航设置 ",-1)),t(_,{class:"mt-4","label-width":"70px"},{default:a(()=>[t(f,{"model-value":"nav"},{default:a(()=>[t(r,{label:"主导航设置",name:"nav"},{default:a(()=>[t(p,{modelValue:l(o).nav,"onUpdate:modelValue":e[0]||(e[0]=m=>l(o).nav=m)},null,8,["modelValue"])]),_:1}),t(r,{label:"菜单设置",name:"menu"},{default:a(()=>[t(p,{modelValue:l(o).menu,"onUpdate:modelValue":e[1]||(e[1]=m=>l(o).menu=m)},null,8,["modelValue"])]),_:1})]),_:1})]),_:1})],64)}}});export{$ as default};
|
||||
import{o as b,q as c,r as V,s as v,v as t,D as a,Y as x,X as E,u as l,b9 as g,F as k,p as w}from"./.pnpm-C1KTa5cB.js";import{_ as p}from"./menu-set.vue_vue_type_script_setup_true_lang-hLXLJaTV.js";import"./index-CQoyCdiY.js";import"./index-BzVi6Aa3.js";import"./picker-CoG_3YpY.js";import"./index-Da4XqmJd.js";import"./index.vue_vue_type_script_setup_true_lang-CerDQAzd.js";import"./article-BB6kpo3D.js";import"./usePaging--aAVZOna.js";import"./picker-DFb6DRYi.js";import"./index-BjEe2GZP.js";import"./index-CEYf_4x5.js";import"./file-DqKLtehk.js";import"./index.vue_vue_type_script_setup_true_lang-0C3mdvgv.js";const $=b({__name:"attr",props:{modelValue:{type:Object,default:()=>({nav:[],menu:{}})}},emits:["update:modelValue"],setup(s,{emit:u}){const d=s,i=u,o=w({get(){return d.modelValue},set(n){i("update:modelValue",n)}});return(n,e)=>{const r=E,f=x,_=g;return c(),V(k,null,[e[2]||(e[2]=v("div",{class:"title flex items-center before:w-[3px] before:h-[14px] before:block before:bg-primary before:mr-2"}," pc导航设置 ",-1)),t(_,{class:"mt-4","label-width":"70px"},{default:a(()=>[t(f,{"model-value":"nav"},{default:a(()=>[t(r,{label:"主导航设置",name:"nav"},{default:a(()=>[t(p,{modelValue:l(o).nav,"onUpdate:modelValue":e[0]||(e[0]=m=>l(o).nav=m)},null,8,["modelValue"])]),_:1}),t(r,{label:"菜单设置",name:"menu"},{default:a(()=>[t(p,{modelValue:l(o).menu,"onUpdate:modelValue":e[1]||(e[1]=m=>l(o).menu=m)},null,8,["modelValue"])]),_:1})]),_:1})]),_:1})],64)}}});export{$ as default};
|
||||
@@ -0,0 +1 @@
|
||||
import{_ as o}from"./attr.vue_vue_type_script_setup_true_lang-BfTE-a6a.js";import"./.pnpm-C1KTa5cB.js";import"./index-CQoyCdiY.js";import"./index-BzVi6Aa3.js";import"./picker-CoG_3YpY.js";import"./index-Da4XqmJd.js";import"./index.vue_vue_type_script_setup_true_lang-CerDQAzd.js";import"./article-BB6kpo3D.js";import"./usePaging--aAVZOna.js";import"./picker-DFb6DRYi.js";import"./index-BjEe2GZP.js";import"./index-CEYf_4x5.js";import"./file-DqKLtehk.js";import"./index.vue_vue_type_script_setup_true_lang-0C3mdvgv.js";export{o as default};
|
||||
@@ -1 +0,0 @@
|
||||
import{_ as o}from"./attr.vue_vue_type_script_setup_true_lang-n-HgGvnU.js";import"./.pnpm-C1KTa5cB.js";import"./index-B-rCQZ_o.js";import"./index-Cxx1IoGl.js";import"./picker-BaeJGhM3.js";import"./index-B3Fu5tec.js";import"./index.vue_vue_type_script_setup_true_lang-CerDQAzd.js";import"./article-BaVF8uYW.js";import"./usePaging--aAVZOna.js";import"./picker-CNX2EN3u.js";import"./index-BhA3CyxZ.js";import"./index-CZyPOnHt.js";import"./file-D2AwthSZ.js";import"./index.vue_vue_type_script_setup_true_lang-0C3mdvgv.js";export{o as default};
|
||||
@@ -0,0 +1 @@
|
||||
import{_ as o}from"./attr.vue_vue_type_script_setup_true_lang-56KPKvp8.js";import"./.pnpm-C1KTa5cB.js";import"./index.vue_vue_type_script_setup_true_lang-CScz2Hxp.js";import"./picker-DFb6DRYi.js";import"./index-Da4XqmJd.js";import"./index-BzVi6Aa3.js";import"./index-BjEe2GZP.js";import"./index.vue_vue_type_script_setup_true_lang-CerDQAzd.js";import"./index-CQoyCdiY.js";import"./index-CEYf_4x5.js";import"./file-DqKLtehk.js";import"./index.vue_vue_type_script_setup_true_lang-0C3mdvgv.js";import"./usePaging--aAVZOna.js";export{o as default};
|
||||
@@ -0,0 +1 @@
|
||||
import{_ as o}from"./attr.vue_vue_type_script_setup_true_lang-D2Ubhe0_.js";import"./.pnpm-C1KTa5cB.js";import"./add-nav.vue_vue_type_script_setup_true_lang-DTFBmDjB.js";import"./index-CQoyCdiY.js";import"./index-BzVi6Aa3.js";import"./picker-CoG_3YpY.js";import"./index-Da4XqmJd.js";import"./index.vue_vue_type_script_setup_true_lang-CerDQAzd.js";import"./article-BB6kpo3D.js";import"./usePaging--aAVZOna.js";import"./picker-DFb6DRYi.js";import"./index-BjEe2GZP.js";import"./index-CEYf_4x5.js";import"./file-DqKLtehk.js";import"./index.vue_vue_type_script_setup_true_lang-0C3mdvgv.js";export{o as default};
|
||||
@@ -0,0 +1 @@
|
||||
import{_ as o}from"./attr-setting.vue_vue_type_script_setup_true_lang-CXpzw1sE.js";import"./.pnpm-C1KTa5cB.js";import"./index-g2w7I1jI.js";import"./attr-wS7V6FVE.js";import"./index-CQoyCdiY.js";import"./index-BzVi6Aa3.js";import"./picker-CoG_3YpY.js";import"./index-Da4XqmJd.js";import"./index.vue_vue_type_script_setup_true_lang-CerDQAzd.js";import"./article-BB6kpo3D.js";import"./usePaging--aAVZOna.js";import"./picker-DFb6DRYi.js";import"./index-BjEe2GZP.js";import"./index-CEYf_4x5.js";import"./file-DqKLtehk.js";import"./index.vue_vue_type_script_setup_true_lang-0C3mdvgv.js";import"./content.vue_vue_type_script_setup_true_lang-CuMBK0Wd.js";import"./decoration-img-jcdhU0Sa.js";import"./attr.vue_vue_type_script_setup_true_lang-DQQ5tkNK.js";import"./content-C7C5LyHs.js";import"./attr.vue_vue_type_script_setup_true_lang-BfTE-a6a.js";import"./content.vue_vue_type_script_setup_true_lang-4sJOmI20.js";import"./attr.vue_vue_type_script_setup_true_lang-D2Ubhe0_.js";import"./add-nav.vue_vue_type_script_setup_true_lang-DTFBmDjB.js";import"./content-BOknc4i-.js";import"./attr.vue_vue_type_script_setup_true_lang-C1sD3H0v.js";import"./content.vue_vue_type_script_setup_true_lang-hFJ8S6KZ.js";import"./attr.vue_vue_type_script_setup_true_lang-B-dt5wFe.js";import"./content-cYMitSY-.js";import"./decoration-BwegRllU.js";import"./attr.vue_vue_type_script_setup_true_lang-56KPKvp8.js";import"./index.vue_vue_type_script_setup_true_lang-CScz2Hxp.js";import"./content-CeEM48xO.js";import"./content.vue_vue_type_script_setup_true_lang-SmDZWVN1.js";import"./attr.vue_vue_type_script_setup_true_lang-C6DHyy0n.js";import"./content-J-T98bs1.js";import"./attr.vue_vue_type_script_setup_true_lang-gUeVRrBW.js";import"./content.vue_vue_type_script_setup_true_lang-B0dLhOs8.js";import"./attr.vue_vue_type_script_setup_true_lang-Crc8ECqw.js";import"./content-D_axG_V_.js";export{o as default};
|
||||
@@ -1 +0,0 @@
|
||||
import{_ as o}from"./attr-setting.vue_vue_type_script_setup_true_lang-DPqpk4ZS.js";import"./.pnpm-C1KTa5cB.js";import"./index-B8QxRZ_R.js";import"./attr-DJCmS0fQ.js";import"./index-B-rCQZ_o.js";import"./index-Cxx1IoGl.js";import"./picker-BaeJGhM3.js";import"./index-B3Fu5tec.js";import"./index.vue_vue_type_script_setup_true_lang-CerDQAzd.js";import"./article-BaVF8uYW.js";import"./usePaging--aAVZOna.js";import"./picker-CNX2EN3u.js";import"./index-BhA3CyxZ.js";import"./index-CZyPOnHt.js";import"./file-D2AwthSZ.js";import"./index.vue_vue_type_script_setup_true_lang-0C3mdvgv.js";import"./content.vue_vue_type_script_setup_true_lang-DPajVJbA.js";import"./decoration-img-Vzh-KZHw.js";import"./attr.vue_vue_type_script_setup_true_lang-DneoXzdE.js";import"./content-Dt1esG11.js";import"./attr.vue_vue_type_script_setup_true_lang-BSBOWQnL.js";import"./content.vue_vue_type_script_setup_true_lang-B6Am_aBA.js";import"./attr.vue_vue_type_script_setup_true_lang-D_pPXHcq.js";import"./add-nav.vue_vue_type_script_setup_true_lang-DW1znYtt.js";import"./content-aJRGigxP.js";import"./attr.vue_vue_type_script_setup_true_lang-keaJgqFX.js";import"./content.vue_vue_type_script_setup_true_lang-DHWW-fo2.js";import"./attr.vue_vue_type_script_setup_true_lang-B-dt5wFe.js";import"./content-gcCdV-yA.js";import"./decoration-BF4ut0tN.js";import"./attr.vue_vue_type_script_setup_true_lang-Dot-5pr7.js";import"./index.vue_vue_type_script_setup_true_lang-CScz2Hxp.js";import"./content-CCuj-Ob7.js";import"./content.vue_vue_type_script_setup_true_lang-Cw1yD5Dq.js";import"./attr.vue_vue_type_script_setup_true_lang-C6DHyy0n.js";import"./content-DG3FcGDA.js";import"./attr.vue_vue_type_script_setup_true_lang-n-HgGvnU.js";import"./content.vue_vue_type_script_setup_true_lang-PyGFHp9z.js";import"./attr.vue_vue_type_script_setup_true_lang-Crc8ECqw.js";import"./content-D3p-zkka.js";export{o as default};
|
||||
+1
-1
@@ -1 +1 @@
|
||||
import{o as g,q as a,r as b,v as c,bg as y,D as r,s as x,T as _,a2 as h,O as i,a3 as w,a4 as v,u as C}from"./.pnpm-C1KTa5cB.js";import{e as k}from"./index-B8QxRZ_R.js";const B={class:"pages-setting"},D={class:"title flex items-center before:w-[3px] before:h-[14px] before:block before:bg-primary before:mr-2 text-xl font-medium"},O=g({__name:"attr-setting",props:{widget:{type:Object,default:()=>({})},type:{type:String,default:"mobile"}},emits:["update:content"],setup(e,{emit:d}){const m=d,p=n=>{m("update:content",n)};return(n,E)=>{const f=y,u=h;return a(),b("div",B,[c(f,{shadow:"never",class:"!border-none flex"},{default:r(()=>{var t;return[x("div",D,_((t=e.widget)==null?void 0:t.title),1)]}),_:1}),c(u,{class:"w-full",style:{height:"calc(100% - 60px)"}},{default:r(()=>{var t,s,o,l;return[(a(),i(w,null,[(a(),i(v((s=C(k)[(t=e.widget)==null?void 0:t.name])==null?void 0:s.attr),{content:(o=e.widget)==null?void 0:o.content,styles:(l=e.widget)==null?void 0:l.styles,type:e.type,"onUpdate:content":p},null,40,["content","styles","type"]))],1024))]}),_:1})])}}});export{O as _};
|
||||
import{o as g,q as a,r as b,v as c,bg as y,D as r,s as x,T as _,a2 as h,O as i,a3 as w,a4 as v,u as C}from"./.pnpm-C1KTa5cB.js";import{e as k}from"./index-g2w7I1jI.js";const B={class:"pages-setting"},D={class:"title flex items-center before:w-[3px] before:h-[14px] before:block before:bg-primary before:mr-2 text-xl font-medium"},O=g({__name:"attr-setting",props:{widget:{type:Object,default:()=>({})},type:{type:String,default:"mobile"}},emits:["update:content"],setup(e,{emit:d}){const m=d,p=n=>{m("update:content",n)};return(n,E)=>{const f=y,u=h;return a(),b("div",B,[c(f,{shadow:"never",class:"!border-none flex"},{default:r(()=>{var t;return[x("div",D,_((t=e.widget)==null?void 0:t.title),1)]}),_:1}),c(u,{class:"w-full",style:{height:"calc(100% - 60px)"}},{default:r(()=>{var t,s,o,l;return[(a(),i(w,null,[(a(),i(v((s=C(k)[(t=e.widget)==null?void 0:t.name])==null?void 0:s.attr),{content:(o=e.widget)==null?void 0:o.content,styles:(l=e.widget)==null?void 0:l.styles,type:e.type,"onUpdate:content":p},null,40,["content","styles","type"]))],1024))]}),_:1})])}}});export{O as _};
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
@@ -1 +1 @@
|
||||
import{o as E,q as s,O as r,D as t,v as l,bg as F,b6 as C,bc as N,u as a,bf as z,L as p,b7 as B,P as i,s as b,b9 as O,p as j}from"./.pnpm-C1KTa5cB.js";import{_ as D}from"./index.vue_vue_type_script_setup_true_lang-CScz2Hxp.js";import{_ as I}from"./picker-CNX2EN3u.js";const G=E({__name:"attr",props:{content:{type:Object,default:()=>({})},styles:{type:Object,default:()=>({})}},emits:["update:content"],setup(d,{emit:g}){const y=g,x=d,o=j({get:()=>x.content,set:f=>{y("update:content",f)}});return(f,e)=>{const m=z,_=N,u=C,v=B,V=I,k=D,U=F,w=O;return s(),r(w,{ref:"form","label-width":"80px",size:"large"},{default:t(()=>[l(U,{shadow:"never",class:"!border-none flex mt-2"},{default:t(()=>[l(u,{label:"页面标题"},{default:t(()=>[l(_,{modelValue:a(o).title_type,"onUpdate:modelValue":e[0]||(e[0]=n=>a(o).title_type=n)},{default:t(()=>[l(m,{value:"1"},{default:t(()=>[...e[7]||(e[7]=[p("文字",-1)])]),_:1}),l(m,{value:"2"},{default:t(()=>[...e[8]||(e[8]=[p("图片",-1)])]),_:1})]),_:1},8,["modelValue"])]),_:1}),d.content.title_type==1?(s(),r(u,{key:0},{default:t(()=>[l(v,{modelValue:a(o).title,"onUpdate:modelValue":e[1]||(e[1]=n=>a(o).title=n),maxlength:"8","show-word-limit":"",class:"w-[300px]",placeholder:"请输入页面标题"},null,8,["modelValue"])]),_:1})):i("",!0),d.content.title_type==2?(s(),r(u,{key:1},{default:t(()=>[l(V,{modelValue:a(o).title_img,"onUpdate:modelValue":e[2]||(e[2]=n=>a(o).title_img=n),limit:1,size:"100px"},null,8,["modelValue"]),e[9]||(e[9]=b("div",{class:"form-tips"},"建议图片尺寸:300px*40px",-1))]),_:1})):i("",!0),d.content.title_type==1?(s(),r(u,{key:2,label:"文字颜色"},{default:t(()=>[l(_,{modelValue:a(o).text_color,"onUpdate:modelValue":e[3]||(e[3]=n=>a(o).text_color=n)},{default:t(()=>[l(m,{value:"1"},{default:t(()=>[...e[10]||(e[10]=[p("白色",-1)])]),_:1}),l(m,{value:"2"},{default:t(()=>[...e[11]||(e[11]=[p("黑色",-1)])]),_:1})]),_:1},8,["modelValue"])]),_:1})):i("",!0),l(u,{label:"页面背景"},{default:t(()=>[l(_,{modelValue:a(o).bg_type,"onUpdate:modelValue":e[4]||(e[4]=n=>a(o).bg_type=n)},{default:t(()=>[l(m,{value:"1"},{default:t(()=>[...e[12]||(e[12]=[p("背景颜色",-1)])]),_:1}),l(m,{value:"2"},{default:t(()=>[...e[13]||(e[13]=[p("背景图片",-1)])]),_:1})]),_:1},8,["modelValue"])]),_:1}),d.content.bg_type==1?(s(),r(u,{key:3},{default:t(()=>[l(k,{modelValue:a(o).bg_color,"onUpdate:modelValue":e[5]||(e[5]=n=>a(o).bg_color=n),"reset-color":"#F5F5F5"},null,8,["modelValue"])]),_:1})):i("",!0),d.content.bg_type==2?(s(),r(u,{key:4},{default:t(()=>[l(V,{modelValue:a(o).bg_image,"onUpdate:modelValue":e[6]||(e[6]=n=>a(o).bg_image=n),limit:1,size:"100px"},null,8,["modelValue"]),e[14]||(e[14]=b("div",{class:"form-tips"},"建议图片尺寸:750px*高度不限",-1))]),_:1})):i("",!0)]),_:1})]),_:1},512)}}});export{G as _};
|
||||
import{o as E,q as s,O as r,D as t,v as l,bg as F,b6 as C,bc as N,u as a,bf as z,L as p,b7 as B,P as i,s as b,b9 as O,p as j}from"./.pnpm-C1KTa5cB.js";import{_ as D}from"./index.vue_vue_type_script_setup_true_lang-CScz2Hxp.js";import{_ as I}from"./picker-DFb6DRYi.js";const G=E({__name:"attr",props:{content:{type:Object,default:()=>({})},styles:{type:Object,default:()=>({})}},emits:["update:content"],setup(d,{emit:g}){const y=g,x=d,o=j({get:()=>x.content,set:f=>{y("update:content",f)}});return(f,e)=>{const m=z,_=N,u=C,v=B,V=I,k=D,U=F,w=O;return s(),r(w,{ref:"form","label-width":"80px",size:"large"},{default:t(()=>[l(U,{shadow:"never",class:"!border-none flex mt-2"},{default:t(()=>[l(u,{label:"页面标题"},{default:t(()=>[l(_,{modelValue:a(o).title_type,"onUpdate:modelValue":e[0]||(e[0]=n=>a(o).title_type=n)},{default:t(()=>[l(m,{value:"1"},{default:t(()=>[...e[7]||(e[7]=[p("文字",-1)])]),_:1}),l(m,{value:"2"},{default:t(()=>[...e[8]||(e[8]=[p("图片",-1)])]),_:1})]),_:1},8,["modelValue"])]),_:1}),d.content.title_type==1?(s(),r(u,{key:0},{default:t(()=>[l(v,{modelValue:a(o).title,"onUpdate:modelValue":e[1]||(e[1]=n=>a(o).title=n),maxlength:"8","show-word-limit":"",class:"w-[300px]",placeholder:"请输入页面标题"},null,8,["modelValue"])]),_:1})):i("",!0),d.content.title_type==2?(s(),r(u,{key:1},{default:t(()=>[l(V,{modelValue:a(o).title_img,"onUpdate:modelValue":e[2]||(e[2]=n=>a(o).title_img=n),limit:1,size:"100px"},null,8,["modelValue"]),e[9]||(e[9]=b("div",{class:"form-tips"},"建议图片尺寸:300px*40px",-1))]),_:1})):i("",!0),d.content.title_type==1?(s(),r(u,{key:2,label:"文字颜色"},{default:t(()=>[l(_,{modelValue:a(o).text_color,"onUpdate:modelValue":e[3]||(e[3]=n=>a(o).text_color=n)},{default:t(()=>[l(m,{value:"1"},{default:t(()=>[...e[10]||(e[10]=[p("白色",-1)])]),_:1}),l(m,{value:"2"},{default:t(()=>[...e[11]||(e[11]=[p("黑色",-1)])]),_:1})]),_:1},8,["modelValue"])]),_:1})):i("",!0),l(u,{label:"页面背景"},{default:t(()=>[l(_,{modelValue:a(o).bg_type,"onUpdate:modelValue":e[4]||(e[4]=n=>a(o).bg_type=n)},{default:t(()=>[l(m,{value:"1"},{default:t(()=>[...e[12]||(e[12]=[p("背景颜色",-1)])]),_:1}),l(m,{value:"2"},{default:t(()=>[...e[13]||(e[13]=[p("背景图片",-1)])]),_:1})]),_:1},8,["modelValue"])]),_:1}),d.content.bg_type==1?(s(),r(u,{key:3},{default:t(()=>[l(k,{modelValue:a(o).bg_color,"onUpdate:modelValue":e[5]||(e[5]=n=>a(o).bg_color=n),"reset-color":"#F5F5F5"},null,8,["modelValue"])]),_:1})):i("",!0),d.content.bg_type==2?(s(),r(u,{key:4},{default:t(()=>[l(V,{modelValue:a(o).bg_image,"onUpdate:modelValue":e[6]||(e[6]=n=>a(o).bg_image=n),limit:1,size:"100px"},null,8,["modelValue"]),e[14]||(e[14]=b("div",{class:"form-tips"},"建议图片尺寸:750px*高度不限",-1))]),_:1})):i("",!0)]),_:1})]),_:1},512)}}});export{G as _};
|
||||
+1
-1
@@ -1 +1 @@
|
||||
import{o as j,q as d,r as b,v as o,D as s,bg as F,s as l,u as p,bQ as S,O as _,b6 as q,P as r,b7 as z,I as A,K,L,b9 as P,p as Q,cn as v}from"./.pnpm-C1KTa5cB.js";import{_ as R}from"./index-B-rCQZ_o.js";import{c as T,i as k}from"./index-Cxx1IoGl.js";import{_ as G}from"./picker-BaeJGhM3.js";import{_ as H}from"./picker-CNX2EN3u.js";const J={class:"flex-1"},M={class:"bg-fill-light w-full p-4 mt-4"},W={class:"flex-1"},X={class:"flex-1 flex items-center"},Y={class:"drag-move cursor-move ml-auto"},Z={key:0,class:"mt-4"},f=5,ae=j({__name:"attr",props:{content:{type:Object,default:()=>({})},styles:{type:Object,default:()=>({})},type:{type:String,default:"mobile"}},emits:["update:content"],setup(m,{emit:y}){const u=y,c=m,g=Q({get:()=>c.content,set:a=>{u("update:content",a)}}),w=()=>{var a;if(((a=c.content.data)==null?void 0:a.length)<f){const e=v(c.content);e.data.push({is_show:"1",image:"",name:"",link:{}}),u("update:content",e)}else k.msgError(`最多添加${f}张图片`)},E=a=>{var i;if(((i=c.content.data)==null?void 0:i.length)<=1)return k.msgError("最少保留一张图片");const e=v(c.content);e.data.splice(a,1),u("update:content",e)};return(a,e)=>{const i=H,U=G,C=z,h=q,B=A,D=T,N=R,$=K,I=F,O=P;return d(),b("div",null,[o(O,{"label-width":"70px"},{default:s(()=>[o(I,{shadow:"never",class:"!border-none flex mt-2"},{default:s(()=>{var x;return[e[2]||(e[2]=l("div",{class:"flex items-end"},[l("div",{class:"text-base text-[#101010] font-medium"},"图片设置"),l("div",{class:"text-xs text-tx-secondary ml-2"}," 最多添加5张,建议图片尺寸:750px*200px ")],-1)),l("div",J,[o(p(S),{class:"draggable",modelValue:p(g).data,"onUpdate:modelValue":e[0]||(e[0]=t=>p(g).data=t),animation:"300",handle:".drag-move"},{item:s(({element:t,index:V})=>[(d(),_(N,{key:V,onClose:n=>E(V),class:"w-full"},{default:s(()=>[l("div",M,[o(i,{width:"396px",height:"196px",modelValue:t.image,"onUpdate:modelValue":n=>t.image=n,"upload-class":"bg-body","exclude-domain":""},null,8,["modelValue","onUpdate:modelValue"]),l("div",W,[o(h,{class:"mt-[18px]",label:"图片链接"},{default:s(()=>[m.type=="mobile"?(d(),_(U,{key:0,modelValue:t.link,"onUpdate:modelValue":n=>t.link=n},null,8,["modelValue","onUpdate:modelValue"])):r("",!0),m.type=="pc"?(d(),_(C,{key:1,placeholder:"请输入链接",modelValue:t.link.path,"onUpdate:modelValue":n=>t.link.path=n},null,8,["modelValue","onUpdate:modelValue"])):r("",!0)]),_:2},1024),o(h,{label:"是否显示",class:"mt-[18px] !mb-0"},{default:s(()=>[l("div",X,[o(B,{modelValue:t.is_show,"onUpdate:modelValue":n=>t.is_show=n,"active-value":"1","inactive-value":"0"},null,8,["modelValue","onUpdate:modelValue"]),l("div",Y,[o(D,{name:"el-icon-Rank",size:"18"})])])]),_:2},1024)])])]),_:2},1032,["onClose"]))]),_:1},8,["modelValue"])]),((x=m.content.data)==null?void 0:x.length)<f?(d(),b("div",Z,[o($,{class:"w-full",type:"primary",onClick:w},{default:s(()=>[...e[1]||(e[1]=[L("添加图片",-1)])]),_:1})])):r("",!0)]}),_:1})]),_:1})])}}});export{ae as _};
|
||||
import{o as j,q as d,r as b,v as o,D as s,bg as F,s as l,u as p,bQ as S,O as _,b6 as q,P as r,b7 as z,I as A,K,L,b9 as P,p as Q,cn as v}from"./.pnpm-C1KTa5cB.js";import{_ as R}from"./index-CQoyCdiY.js";import{c as T,i as k}from"./index-BzVi6Aa3.js";import{_ as G}from"./picker-CoG_3YpY.js";import{_ as H}from"./picker-DFb6DRYi.js";const J={class:"flex-1"},M={class:"bg-fill-light w-full p-4 mt-4"},W={class:"flex-1"},X={class:"flex-1 flex items-center"},Y={class:"drag-move cursor-move ml-auto"},Z={key:0,class:"mt-4"},f=5,ae=j({__name:"attr",props:{content:{type:Object,default:()=>({})},styles:{type:Object,default:()=>({})},type:{type:String,default:"mobile"}},emits:["update:content"],setup(m,{emit:y}){const u=y,c=m,g=Q({get:()=>c.content,set:a=>{u("update:content",a)}}),w=()=>{var a;if(((a=c.content.data)==null?void 0:a.length)<f){const e=v(c.content);e.data.push({is_show:"1",image:"",name:"",link:{}}),u("update:content",e)}else k.msgError(`最多添加${f}张图片`)},E=a=>{var i;if(((i=c.content.data)==null?void 0:i.length)<=1)return k.msgError("最少保留一张图片");const e=v(c.content);e.data.splice(a,1),u("update:content",e)};return(a,e)=>{const i=H,U=G,C=z,h=q,B=A,D=T,N=R,$=K,I=F,O=P;return d(),b("div",null,[o(O,{"label-width":"70px"},{default:s(()=>[o(I,{shadow:"never",class:"!border-none flex mt-2"},{default:s(()=>{var x;return[e[2]||(e[2]=l("div",{class:"flex items-end"},[l("div",{class:"text-base text-[#101010] font-medium"},"图片设置"),l("div",{class:"text-xs text-tx-secondary ml-2"}," 最多添加5张,建议图片尺寸:750px*200px ")],-1)),l("div",J,[o(p(S),{class:"draggable",modelValue:p(g).data,"onUpdate:modelValue":e[0]||(e[0]=t=>p(g).data=t),animation:"300",handle:".drag-move"},{item:s(({element:t,index:V})=>[(d(),_(N,{key:V,onClose:n=>E(V),class:"w-full"},{default:s(()=>[l("div",M,[o(i,{width:"396px",height:"196px",modelValue:t.image,"onUpdate:modelValue":n=>t.image=n,"upload-class":"bg-body","exclude-domain":""},null,8,["modelValue","onUpdate:modelValue"]),l("div",W,[o(h,{class:"mt-[18px]",label:"图片链接"},{default:s(()=>[m.type=="mobile"?(d(),_(U,{key:0,modelValue:t.link,"onUpdate:modelValue":n=>t.link=n},null,8,["modelValue","onUpdate:modelValue"])):r("",!0),m.type=="pc"?(d(),_(C,{key:1,placeholder:"请输入链接",modelValue:t.link.path,"onUpdate:modelValue":n=>t.link.path=n},null,8,["modelValue","onUpdate:modelValue"])):r("",!0)]),_:2},1024),o(h,{label:"是否显示",class:"mt-[18px] !mb-0"},{default:s(()=>[l("div",X,[o(B,{modelValue:t.is_show,"onUpdate:modelValue":n=>t.is_show=n,"active-value":"1","inactive-value":"0"},null,8,["modelValue","onUpdate:modelValue"]),l("div",Y,[o(D,{name:"el-icon-Rank",size:"18"})])])]),_:2},1024)])])]),_:2},1032,["onClose"]))]),_:1},8,["modelValue"])]),((x=m.content.data)==null?void 0:x.length)<f?(d(),b("div",Z,[o($,{class:"w-full",type:"primary",onClick:w},{default:s(()=>[...e[1]||(e[1]=[L("添加图片",-1)])]),_:1})])):r("",!0)]}),_:1})]),_:1})])}}});export{ae as _};
|
||||
+1
-1
@@ -1 +1 @@
|
||||
import{o as k,q as d,r as u,v as l,D as o,bg as F,s,bc as U,u as n,bf as B,L as x,b6 as C,bm as N,F as b,G as c,bn as O,b9 as j,p as D}from"./.pnpm-C1KTa5cB.js";import{_ as G}from"./add-nav.vue_vue_type_script_setup_true_lang-DW1znYtt.js";const L={class:"flex-1 mt-4"},I=k({__name:"attr",props:{content:{type:Object,default:()=>({})},styles:{type:Object,default:()=>({})}},emits:["update:content"],setup(v,{emit:V}){const y=V,w=v,a=D({get:()=>w.content,set:m=>{y("update:content",m)}});return(m,e)=>{const r=B,E=U,p=O,_=N,i=C,f=F,g=j;return d(),u("div",null,[l(g,{"label-width":"70px"},{default:o(()=>[l(f,{shadow:"never",class:"!border-none flex mt-2"},{default:o(()=>[e[6]||(e[6]=s("div",{class:"flex items-end mb-4"},[s("div",{class:"text-base text-[#101010] font-medium"},"展示样式")],-1)),l(E,{modelValue:n(a).style,"onUpdate:modelValue":e[0]||(e[0]=t=>n(a).style=t)},{default:o(()=>[l(r,{value:1},{default:o(()=>[...e[4]||(e[4]=[x("固定显示",-1)])]),_:1}),l(r,{value:2},{default:o(()=>[...e[5]||(e[5]=[x("分页滑动",-1)])]),_:1})]),_:1},8,["modelValue"]),l(i,{label:"每行数量",class:"mt-4"},{default:o(()=>[l(_,{modelValue:n(a).per_line,"onUpdate:modelValue":e[1]||(e[1]=t=>n(a).per_line=t),style:{width:"300px"}},{default:o(()=>[(d(),u(b,null,c(5,t=>l(p,{key:t,label:t+"个",value:t},null,8,["label","value"])),64))]),_:1},8,["modelValue"])]),_:1}),l(i,{label:"显示行数"},{default:o(()=>[l(_,{modelValue:n(a).show_line,"onUpdate:modelValue":e[2]||(e[2]=t=>n(a).show_line=t),style:{width:"300px"}},{default:o(()=>[(d(),u(b,null,c(2,t=>l(p,{key:t,label:t+"行",value:t},null,8,["label","value"])),64))]),_:1},8,["modelValue"])]),_:1})]),_:1}),l(f,{shadow:"never",class:"!border-none flex mt-2"},{default:o(()=>[e[7]||(e[7]=s("div",{class:"flex items-end"},[s("div",{class:"text-base text-[#101010] font-medium"},"菜单设置"),s("div",{class:"text-xs text-tx-secondary ml-2"},"建议图片尺寸:100px*100px")],-1)),s("div",L,[l(G,{modelValue:n(a).data,"onUpdate:modelValue":e[3]||(e[3]=t=>n(a).data=t)},null,8,["modelValue"])])]),_:1})]),_:1})])}}});export{I as _};
|
||||
import{o as k,q as d,r as u,v as l,D as o,bg as F,s,bc as U,u as n,bf as B,L as x,b6 as C,bm as N,F as b,G as c,bn as O,b9 as j,p as D}from"./.pnpm-C1KTa5cB.js";import{_ as G}from"./add-nav.vue_vue_type_script_setup_true_lang-DTFBmDjB.js";const L={class:"flex-1 mt-4"},I=k({__name:"attr",props:{content:{type:Object,default:()=>({})},styles:{type:Object,default:()=>({})}},emits:["update:content"],setup(v,{emit:V}){const y=V,w=v,a=D({get:()=>w.content,set:m=>{y("update:content",m)}});return(m,e)=>{const r=B,E=U,p=O,_=N,i=C,f=F,g=j;return d(),u("div",null,[l(g,{"label-width":"70px"},{default:o(()=>[l(f,{shadow:"never",class:"!border-none flex mt-2"},{default:o(()=>[e[6]||(e[6]=s("div",{class:"flex items-end mb-4"},[s("div",{class:"text-base text-[#101010] font-medium"},"展示样式")],-1)),l(E,{modelValue:n(a).style,"onUpdate:modelValue":e[0]||(e[0]=t=>n(a).style=t)},{default:o(()=>[l(r,{value:1},{default:o(()=>[...e[4]||(e[4]=[x("固定显示",-1)])]),_:1}),l(r,{value:2},{default:o(()=>[...e[5]||(e[5]=[x("分页滑动",-1)])]),_:1})]),_:1},8,["modelValue"]),l(i,{label:"每行数量",class:"mt-4"},{default:o(()=>[l(_,{modelValue:n(a).per_line,"onUpdate:modelValue":e[1]||(e[1]=t=>n(a).per_line=t),style:{width:"300px"}},{default:o(()=>[(d(),u(b,null,c(5,t=>l(p,{key:t,label:t+"个",value:t},null,8,["label","value"])),64))]),_:1},8,["modelValue"])]),_:1}),l(i,{label:"显示行数"},{default:o(()=>[l(_,{modelValue:n(a).show_line,"onUpdate:modelValue":e[2]||(e[2]=t=>n(a).show_line=t),style:{width:"300px"}},{default:o(()=>[(d(),u(b,null,c(2,t=>l(p,{key:t,label:t+"行",value:t},null,8,["label","value"])),64))]),_:1},8,["modelValue"])]),_:1})]),_:1}),l(f,{shadow:"never",class:"!border-none flex mt-2"},{default:o(()=>[e[7]||(e[7]=s("div",{class:"flex items-end"},[s("div",{class:"text-base text-[#101010] font-medium"},"菜单设置"),s("div",{class:"text-xs text-tx-secondary ml-2"},"建议图片尺寸:100px*100px")],-1)),s("div",L,[l(G,{modelValue:n(a).data,"onUpdate:modelValue":e[3]||(e[3]=t=>n(a).data=t)},null,8,["modelValue"])])]),_:1})]),_:1})])}}});export{I as _};
|
||||
+1
-1
@@ -1 +1 @@
|
||||
import{o as V,q as E,r as w,v as t,D as o,bg as y,b6 as g,b7 as B,u as s,s as a,bc as C,bf as N,L as u,b9 as U,p as j}from"./.pnpm-C1KTa5cB.js";import{_ as k}from"./add-nav.vue_vue_type_script_setup_true_lang-DW1znYtt.js";const D={class:"flex-1"},O=V({__name:"attr",props:{content:{type:Object,default:()=>({})},styles:{type:Object,default:()=>({})}},emits:["update:content"],setup(p,{emit:i}){const f=i,_=p,l=j({get:()=>_.content,set:m=>{f("update:content",m)}});return(m,e)=>{const x=B,c=g,d=y,r=N,b=C,v=U;return E(),w("div",null,[t(v,{"label-width":"70px"},{default:o(()=>[t(d,{shadow:"never",class:"!border-none flex mt-2"},{default:o(()=>[t(c,{label:"标题"},{default:o(()=>[t(x,{class:"w-[396px]",modelValue:s(l).title,"onUpdate:modelValue":e[0]||(e[0]=n=>s(l).title=n)},null,8,["modelValue"])]),_:1})]),_:1}),t(d,{shadow:"never",class:"!border-none flex mt-2"},{default:o(()=>[e[5]||(e[5]=a("div",{class:"flex items-end mb-4"},[a("div",{class:"text-base text-[#101010] font-medium"},"展示样式")],-1)),t(b,{modelValue:s(l).style,"onUpdate:modelValue":e[1]||(e[1]=n=>s(l).style=n)},{default:o(()=>[t(r,{value:1},{default:o(()=>[...e[3]||(e[3]=[u("横排",-1)])]),_:1}),t(r,{value:2},{default:o(()=>[...e[4]||(e[4]=[u("竖排",-1)])]),_:1})]),_:1},8,["modelValue"])]),_:1}),t(d,{shadow:"never",class:"!border-none flex mt-2"},{default:o(()=>[e[6]||(e[6]=a("div",{class:"flex items-end mb-4"},[a("div",{class:"text-base text-[#101010] font-medium"},"菜单"),a("div",{class:"text-xs text-tx-secondary ml-2"},"建议图片尺寸:100px*100px")],-1)),a("div",D,[t(k,{modelValue:s(l).data,"onUpdate:modelValue":e[2]||(e[2]=n=>s(l).data=n)},null,8,["modelValue"])])]),_:1})]),_:1})])}}});export{O as _};
|
||||
import{o as V,q as E,r as w,v as t,D as o,bg as y,b6 as g,b7 as B,u as s,s as a,bc as C,bf as N,L as u,b9 as U,p as j}from"./.pnpm-C1KTa5cB.js";import{_ as k}from"./add-nav.vue_vue_type_script_setup_true_lang-DTFBmDjB.js";const D={class:"flex-1"},O=V({__name:"attr",props:{content:{type:Object,default:()=>({})},styles:{type:Object,default:()=>({})}},emits:["update:content"],setup(p,{emit:i}){const f=i,_=p,l=j({get:()=>_.content,set:m=>{f("update:content",m)}});return(m,e)=>{const x=B,c=g,d=y,r=N,b=C,v=U;return E(),w("div",null,[t(v,{"label-width":"70px"},{default:o(()=>[t(d,{shadow:"never",class:"!border-none flex mt-2"},{default:o(()=>[t(c,{label:"标题"},{default:o(()=>[t(x,{class:"w-[396px]",modelValue:s(l).title,"onUpdate:modelValue":e[0]||(e[0]=n=>s(l).title=n)},null,8,["modelValue"])]),_:1})]),_:1}),t(d,{shadow:"never",class:"!border-none flex mt-2"},{default:o(()=>[e[5]||(e[5]=a("div",{class:"flex items-end mb-4"},[a("div",{class:"text-base text-[#101010] font-medium"},"展示样式")],-1)),t(b,{modelValue:s(l).style,"onUpdate:modelValue":e[1]||(e[1]=n=>s(l).style=n)},{default:o(()=>[t(r,{value:1},{default:o(()=>[...e[3]||(e[3]=[u("横排",-1)])]),_:1}),t(r,{value:2},{default:o(()=>[...e[4]||(e[4]=[u("竖排",-1)])]),_:1})]),_:1},8,["modelValue"])]),_:1}),t(d,{shadow:"never",class:"!border-none flex mt-2"},{default:o(()=>[e[6]||(e[6]=a("div",{class:"flex items-end mb-4"},[a("div",{class:"text-base text-[#101010] font-medium"},"菜单"),a("div",{class:"text-xs text-tx-secondary ml-2"},"建议图片尺寸:100px*100px")],-1)),a("div",D,[t(k,{modelValue:s(l).data,"onUpdate:modelValue":e[2]||(e[2]=n=>s(l).data=n)},null,8,["modelValue"])])]),_:1})]),_:1})])}}});export{O as _};
|
||||
+1
-1
@@ -1 +1 @@
|
||||
import{o as j,q as V,r as v,v as e,D as t,s,L as h,bg as q,b6 as A,u as m,bQ as K,ae as M,b7 as O,I as Q,K as R,T as w,P as G,b9 as H,F as J,p as y}from"./.pnpm-C1KTa5cB.js";import{_ as W}from"./index-B-rCQZ_o.js";import{_ as X}from"./picker-BaeJGhM3.js";import{_ as Y}from"./picker-CNX2EN3u.js";import{c as Z,i as b}from"./index-Cxx1IoGl.js";import{_ as ee}from"./index.vue_vue_type_script_setup_true_lang-CScz2Hxp.js";const le={class:"mb-[18px] max-w-[400px]"},oe={class:"bg-fill-light w-full p-4 mt-4"},te={class:"upload-btn w-[60px] h-[60px]"},se={class:"upload-btn w-[60px] h-[60px]"},ae={class:"flex-1 flex items-center"},ne={class:"drag-move cursor-move ml-auto"},de={key:0,class:"mt-4"},c=5,p=2,_e=j({__name:"attr",props:{modelValue:{type:Object,default:()=>({list:[],style:{}})}},emits:["update:modelValue"],setup(k,{emit:U}){const C=k,E=U,n=y({get(){return C.modelValue},set(a){E("update:modelValue",a)}}),$=y(()=>{var a;return((a=n.value.list)==null?void 0:a.filter(l=>l.is_show=="1"))||[]}),z=()=>{var a;((a=n.value.list)==null?void 0:a.length)<c?n.value.list.push({name:"",selected:"",unselected:"",is_show:1,link:{}}):b.msgError(`最多添加${c}个`)},B=a=>{var l;if(((l=n.value.list)==null?void 0:l.length)<=p)return b.msgError(`最少保留${p}个`);n.value.list.splice(a,1)},D=a=>a.relatedContext.index!=0,F=a=>{if($.value.length<p)return a.is_show=1,b.msgError(`最少显示${p}个`)};return(a,l)=>{const _=q,x=ee,i=A,f=Z,g=Y,N=O,I=X,P=Q,S=W,T=R,L=H;return V(),v(J,null,[e(_,{shadow:"never",class:"!border-none flex"},{default:t(()=>[...l[3]||(l[3]=[s("div",{class:"title flex items-center before:w-[3px] before:h-[14px] before:block before:bg-primary before:mr-2 text-xl font-medium"},[h(" 底部导航设置 "),s("span",{class:"form-tips ml-[10px] !mt-0"}," 至少添加2个导航,最多添加5个导航 ")],-1)])]),_:1}),e(L,{"label-width":"70px"},{default:t(()=>[e(_,{shadow:"never",class:"!border-none flex mt-2"},{default:t(()=>[l[4]||(l[4]=s("div",{class:"flex items-end mb-4"},[s("div",{class:"text-base text-[#101010] font-medium"},"展示样式")],-1)),e(i,{label:"默认颜色"},{default:t(()=>[e(x,{class:"max-w-[400px]",modelValue:m(n).style.default_color,"onUpdate:modelValue":l[0]||(l[0]=u=>m(n).style.default_color=u),"default-color":"#999999"},null,8,["modelValue"])]),_:1}),e(i,{label:"选中颜色",style:{"margin-bottom":"0"}},{default:t(()=>[e(x,{class:"max-w-[400px]",modelValue:m(n).style.selected_color,"onUpdate:modelValue":l[1]||(l[1]=u=>m(n).style.selected_color=u),"default-color":"#4173ff"},null,8,["modelValue"])]),_:1})]),_:1}),e(_,{shadow:"never",class:"!border-none flex mt-2"},{default:t(()=>{var u;return[l[7]||(l[7]=s("div",{class:"flex items-end mb-4"},[s("div",{class:"text-base text-[#101010] font-medium"},"菜单设置"),s("div",{class:"text-xs text-tx-secondary ml-2"},"建议图片尺寸:100px*100px")],-1)),s("div",le,[e(m(K),{class:"draggable",modelValue:m(n).list,"onUpdate:modelValue":l[2]||(l[2]=o=>m(n).list=o),animation:"300",draggable:".draggable",handle:".drag-move",move:D,"item-key":"index"},{item:t(({element:o,index:r})=>[e(S,{onClose:d=>B(r),class:M(["max-w-[400px]",{draggable:r!=0}]),"show-close":r!==0},{default:t(()=>[s("div",oe,[e(i,{label:"导航图标"},{default:t(()=>[e(g,{modelValue:o.unselected,"onUpdate:modelValue":d=>o.unselected=d,"upload-class":"bg-body","exclude-domain":"",size:"60px"},{upload:t(()=>[s("div",te,[e(f,{name:"el-icon-Plus",size:16}),l[5]||(l[5]=s("span",{class:"text-xs leading-5"}," 未选中 ",-1))])]),_:1},8,["modelValue","onUpdate:modelValue"]),e(g,{modelValue:o.selected,"onUpdate:modelValue":d=>o.selected=d,"exclude-domain":"","upload-class":"bg-body",size:"60px"},{upload:t(()=>[s("div",se,[e(f,{name:"el-icon-Plus",size:16}),l[6]||(l[6]=s("span",{class:"text-xs leading-5"}," 选中 ",-1))])]),_:1},8,["modelValue","onUpdate:modelValue"])]),_:2},1024),e(i,{label:"导航名称"},{default:t(()=>[e(N,{modelValue:o.name,"onUpdate:modelValue":d=>o.name=d,placeholder:"请输入名称"},null,8,["modelValue","onUpdate:modelValue"])]),_:2},1024),e(i,{label:"链接地址"},{default:t(()=>[e(I,{"is-tab":!0,disabled:r===0,modelValue:o.link,"onUpdate:modelValue":d=>o.link=d},null,8,["disabled","modelValue","onUpdate:modelValue"])]),_:2},1024),e(i,{label:"是否显示"},{default:t(()=>[s("div",ae,[e(P,{disabled:r==0,modelValue:o.is_show,"onUpdate:modelValue":d=>o.is_show=d,"active-value":1,"inactive-value":0,onChange:d=>F(o)},null,8,["disabled","modelValue","onUpdate:modelValue","onChange"]),s("div",ne,[e(f,{name:"el-icon-Rank",size:"18"})])])]),_:2},1024)])]),_:2},1032,["onClose","show-close","class"])]),_:1},8,["modelValue"])]),((u=m(n).list)==null?void 0:u.length)<c?(V(),v("div",de,[e(T,{class:"w-full",type:"primary",onClick:z},{default:t(()=>{var o;return[h(" 添加导航 "+w((o=m(n).list)==null?void 0:o.length)+" / "+w(c),1)]}),_:1})])):G("",!0)]}),_:1})]),_:1})],64)}}});export{_e as _};
|
||||
import{o as j,q as V,r as v,v as e,D as t,s,L as h,bg as q,b6 as A,u as m,bQ as K,ae as M,b7 as O,I as Q,K as R,T as w,P as G,b9 as H,F as J,p as y}from"./.pnpm-C1KTa5cB.js";import{_ as W}from"./index-CQoyCdiY.js";import{_ as X}from"./picker-CoG_3YpY.js";import{_ as Y}from"./picker-DFb6DRYi.js";import{c as Z,i as b}from"./index-BzVi6Aa3.js";import{_ as ee}from"./index.vue_vue_type_script_setup_true_lang-CScz2Hxp.js";const le={class:"mb-[18px] max-w-[400px]"},oe={class:"bg-fill-light w-full p-4 mt-4"},te={class:"upload-btn w-[60px] h-[60px]"},se={class:"upload-btn w-[60px] h-[60px]"},ae={class:"flex-1 flex items-center"},ne={class:"drag-move cursor-move ml-auto"},de={key:0,class:"mt-4"},c=5,p=2,_e=j({__name:"attr",props:{modelValue:{type:Object,default:()=>({list:[],style:{}})}},emits:["update:modelValue"],setup(k,{emit:U}){const C=k,E=U,n=y({get(){return C.modelValue},set(a){E("update:modelValue",a)}}),$=y(()=>{var a;return((a=n.value.list)==null?void 0:a.filter(l=>l.is_show=="1"))||[]}),z=()=>{var a;((a=n.value.list)==null?void 0:a.length)<c?n.value.list.push({name:"",selected:"",unselected:"",is_show:1,link:{}}):b.msgError(`最多添加${c}个`)},B=a=>{var l;if(((l=n.value.list)==null?void 0:l.length)<=p)return b.msgError(`最少保留${p}个`);n.value.list.splice(a,1)},D=a=>a.relatedContext.index!=0,F=a=>{if($.value.length<p)return a.is_show=1,b.msgError(`最少显示${p}个`)};return(a,l)=>{const _=q,x=ee,i=A,f=Z,g=Y,N=O,I=X,P=Q,S=W,T=R,L=H;return V(),v(J,null,[e(_,{shadow:"never",class:"!border-none flex"},{default:t(()=>[...l[3]||(l[3]=[s("div",{class:"title flex items-center before:w-[3px] before:h-[14px] before:block before:bg-primary before:mr-2 text-xl font-medium"},[h(" 底部导航设置 "),s("span",{class:"form-tips ml-[10px] !mt-0"}," 至少添加2个导航,最多添加5个导航 ")],-1)])]),_:1}),e(L,{"label-width":"70px"},{default:t(()=>[e(_,{shadow:"never",class:"!border-none flex mt-2"},{default:t(()=>[l[4]||(l[4]=s("div",{class:"flex items-end mb-4"},[s("div",{class:"text-base text-[#101010] font-medium"},"展示样式")],-1)),e(i,{label:"默认颜色"},{default:t(()=>[e(x,{class:"max-w-[400px]",modelValue:m(n).style.default_color,"onUpdate:modelValue":l[0]||(l[0]=u=>m(n).style.default_color=u),"default-color":"#999999"},null,8,["modelValue"])]),_:1}),e(i,{label:"选中颜色",style:{"margin-bottom":"0"}},{default:t(()=>[e(x,{class:"max-w-[400px]",modelValue:m(n).style.selected_color,"onUpdate:modelValue":l[1]||(l[1]=u=>m(n).style.selected_color=u),"default-color":"#4173ff"},null,8,["modelValue"])]),_:1})]),_:1}),e(_,{shadow:"never",class:"!border-none flex mt-2"},{default:t(()=>{var u;return[l[7]||(l[7]=s("div",{class:"flex items-end mb-4"},[s("div",{class:"text-base text-[#101010] font-medium"},"菜单设置"),s("div",{class:"text-xs text-tx-secondary ml-2"},"建议图片尺寸:100px*100px")],-1)),s("div",le,[e(m(K),{class:"draggable",modelValue:m(n).list,"onUpdate:modelValue":l[2]||(l[2]=o=>m(n).list=o),animation:"300",draggable:".draggable",handle:".drag-move",move:D,"item-key":"index"},{item:t(({element:o,index:r})=>[e(S,{onClose:d=>B(r),class:M(["max-w-[400px]",{draggable:r!=0}]),"show-close":r!==0},{default:t(()=>[s("div",oe,[e(i,{label:"导航图标"},{default:t(()=>[e(g,{modelValue:o.unselected,"onUpdate:modelValue":d=>o.unselected=d,"upload-class":"bg-body","exclude-domain":"",size:"60px"},{upload:t(()=>[s("div",te,[e(f,{name:"el-icon-Plus",size:16}),l[5]||(l[5]=s("span",{class:"text-xs leading-5"}," 未选中 ",-1))])]),_:1},8,["modelValue","onUpdate:modelValue"]),e(g,{modelValue:o.selected,"onUpdate:modelValue":d=>o.selected=d,"exclude-domain":"","upload-class":"bg-body",size:"60px"},{upload:t(()=>[s("div",se,[e(f,{name:"el-icon-Plus",size:16}),l[6]||(l[6]=s("span",{class:"text-xs leading-5"}," 选中 ",-1))])]),_:1},8,["modelValue","onUpdate:modelValue"])]),_:2},1024),e(i,{label:"导航名称"},{default:t(()=>[e(N,{modelValue:o.name,"onUpdate:modelValue":d=>o.name=d,placeholder:"请输入名称"},null,8,["modelValue","onUpdate:modelValue"])]),_:2},1024),e(i,{label:"链接地址"},{default:t(()=>[e(I,{"is-tab":!0,disabled:r===0,modelValue:o.link,"onUpdate:modelValue":d=>o.link=d},null,8,["disabled","modelValue","onUpdate:modelValue"])]),_:2},1024),e(i,{label:"是否显示"},{default:t(()=>[s("div",ae,[e(P,{disabled:r==0,modelValue:o.is_show,"onUpdate:modelValue":d=>o.is_show=d,"active-value":1,"inactive-value":0,onChange:d=>F(o)},null,8,["disabled","modelValue","onUpdate:modelValue","onChange"]),s("div",ne,[e(f,{name:"el-icon-Rank",size:"18"})])])]),_:2},1024)])]),_:2},1032,["onClose","show-close","class"])]),_:1},8,["modelValue"])]),((u=m(n).list)==null?void 0:u.length)<c?(V(),v("div",de,[e(T,{class:"w-full",type:"primary",onClick:z},{default:t(()=>{var o;return[h(" 添加导航 "+w((o=m(n).list)==null?void 0:o.length)+" / "+w(c),1)]}),_:1})])):G("",!0)]}),_:1})]),_:1})],64)}}});export{_e as _};
|
||||
+1
-1
@@ -1 +1 @@
|
||||
import{o as V,q as b,r as w,v as e,D as n,bg as x,b6 as g,b7 as k,u as o,s as E,b9 as U,p as v}from"./.pnpm-C1KTa5cB.js";import{_ as q}from"./picker-CNX2EN3u.js";const C=V({__name:"attr",props:{content:{type:Object,default:()=>({})},styles:{type:Object,default:()=>({})}},emits:["update:content"],setup(u,{emit:p}){const r=p,i=u,l=v({get:()=>i.content,set:d=>{r("update:content",d)}});return(d,t)=>{const s=k,m=g,_=q,c=x,f=U;return b(),w("div",null,[e(f,{"label-width":"90px",size:"large","label-position":"top"},{default:n(()=>[e(c,{shadow:"never",class:"!border-none flex mt-2"},{default:n(()=>[e(m,{label:"平台名称"},{default:n(()=>[e(s,{class:"w-[400px]","show-word-limit":"",maxlength:"20",modelValue:o(l).title,"onUpdate:modelValue":t[0]||(t[0]=a=>o(l).title=a)},null,8,["modelValue"])]),_:1}),e(m,{label:"客服二维码"},{default:n(()=>[E("div",null,[e(_,{modelValue:o(l).qrcode,"onUpdate:modelValue":t[1]||(t[1]=a=>o(l).qrcode=a),"exclude-domain":""},null,8,["modelValue"])])]),_:1}),e(m,{label:"备注"},{default:n(()=>[e(s,{class:"w-[400px]","show-word-limit":"",maxlength:"20",modelValue:o(l).remark,"onUpdate:modelValue":t[2]||(t[2]=a=>o(l).remark=a)},null,8,["modelValue"])]),_:1}),e(m,{label:"联系电话"},{default:n(()=>[e(s,{class:"w-[400px]",modelValue:o(l).mobile,"onUpdate:modelValue":t[3]||(t[3]=a=>o(l).mobile=a)},null,8,["modelValue"])]),_:1}),e(m,{label:"服务时间"},{default:n(()=>[e(s,{class:"w-[400px]","show-word-limit":"",maxlength:"20",modelValue:o(l).time,"onUpdate:modelValue":t[4]||(t[4]=a=>o(l).time=a)},null,8,["modelValue"])]),_:1})]),_:1})]),_:1})])}}});export{C as _};
|
||||
import{o as V,q as b,r as w,v as e,D as n,bg as x,b6 as g,b7 as k,u as o,s as E,b9 as U,p as v}from"./.pnpm-C1KTa5cB.js";import{_ as q}from"./picker-DFb6DRYi.js";const C=V({__name:"attr",props:{content:{type:Object,default:()=>({})},styles:{type:Object,default:()=>({})}},emits:["update:content"],setup(u,{emit:p}){const r=p,i=u,l=v({get:()=>i.content,set:d=>{r("update:content",d)}});return(d,t)=>{const s=k,m=g,_=q,c=x,f=U;return b(),w("div",null,[e(f,{"label-width":"90px",size:"large","label-position":"top"},{default:n(()=>[e(c,{shadow:"never",class:"!border-none flex mt-2"},{default:n(()=>[e(m,{label:"平台名称"},{default:n(()=>[e(s,{class:"w-[400px]","show-word-limit":"",maxlength:"20",modelValue:o(l).title,"onUpdate:modelValue":t[0]||(t[0]=a=>o(l).title=a)},null,8,["modelValue"])]),_:1}),e(m,{label:"客服二维码"},{default:n(()=>[E("div",null,[e(_,{modelValue:o(l).qrcode,"onUpdate:modelValue":t[1]||(t[1]=a=>o(l).qrcode=a),"exclude-domain":""},null,8,["modelValue"])])]),_:1}),e(m,{label:"备注"},{default:n(()=>[e(s,{class:"w-[400px]","show-word-limit":"",maxlength:"20",modelValue:o(l).remark,"onUpdate:modelValue":t[2]||(t[2]=a=>o(l).remark=a)},null,8,["modelValue"])]),_:1}),e(m,{label:"联系电话"},{default:n(()=>[e(s,{class:"w-[400px]",modelValue:o(l).mobile,"onUpdate:modelValue":t[3]||(t[3]=a=>o(l).mobile=a)},null,8,["modelValue"])]),_:1}),e(m,{label:"服务时间"},{default:n(()=>[e(s,{class:"w-[400px]","show-word-limit":"",maxlength:"20",modelValue:o(l).time,"onUpdate:modelValue":t[4]||(t[4]=a=>o(l).time=a)},null,8,["modelValue"])]),_:1})]),_:1})]),_:1})])}}});export{C as _};
|
||||
+1
-1
@@ -1 +1 @@
|
||||
import{o as I,q as i,r as g,v as t,D as l,bg as O,s,u,bQ as j,O as F,b6 as q,b7 as z,I as A,K,L,P,b9 as Q,p as R,cn as b}from"./.pnpm-C1KTa5cB.js";import{_ as S}from"./index-B-rCQZ_o.js";import{c as T,i as v}from"./index-Cxx1IoGl.js";import{_ as G}from"./picker-BaeJGhM3.js";import{_ as H}from"./picker-CNX2EN3u.js";const J={class:"bg-fill-light flex items-center w-full p-4 mt-4"},M={class:"ml-3 flex-1"},W={class:"flex-1 flex items-center"},X={class:"drag-move cursor-move ml-auto"},Y={key:0,class:"mt-4"},p=5,le=I({__name:"attr",props:{content:{type:Object,default:()=>({})},styles:{type:Object,default:()=>({})}},emits:["update:content"],setup(r,{emit:h}){const m=h,c=r,f=R({get:()=>c.content,set:a=>{m("update:content",a)}}),k=()=>{var a;if(((a=c.content.data)==null?void 0:a.length)<p){const e=b(c.content);e.data.push({is_show:"1",image:"",name:"",link:{}}),m("update:content",e)}else v.msgError(`最多添加${p}张图片`)},w=a=>{var d;if(((d=c.content.data)==null?void 0:d.length)<=1)return v.msgError("最少保留一张图片");const e=b(c.content);e.data.splice(a,1),m("update:content",e)};return(a,e)=>{const d=H,y=z,_=q,E=G,U=A,C=T,B=S,D=K,N=O,$=Q;return i(),g("div",null,[t($,{"label-width":"70px"},{default:l(()=>[t(N,{shadow:"never",class:"!border-none flex mt-2"},{default:l(()=>{var x;return[e[2]||(e[2]=s("div",{class:"flex items-end mb-4"},[s("div",{class:"text-base text-[#101010] font-medium"},"菜单"),s("div",{class:"text-xs text-tx-secondary ml-2"}," 最多添加5张,建议图片尺寸:750px*200px ")],-1)),t(u(j),{class:"draggable",modelValue:u(f).data,"onUpdate:modelValue":e[0]||(e[0]=o=>u(f).data=o),animation:"300",handle:".drag-move","item-key":"index"},{item:l(({element:o,index:V})=>[(i(),F(B,{key:V,onClose:n=>w(V),class:"w-[467px]"},{default:l(()=>[s("div",J,[t(d,{modelValue:o.image,"onUpdate:modelValue":n=>o.image=n,"upload-class":"bg-body","exclude-domain":""},null,8,["modelValue","onUpdate:modelValue"]),s("div",M,[t(_,{label:"图片名称"},{default:l(()=>[t(y,{modelValue:o.name,"onUpdate:modelValue":n=>o.name=n,placeholder:"请输入名称"},null,8,["modelValue","onUpdate:modelValue"])]),_:2},1024),t(_,{class:"mt-[18px]",label:"图片链接"},{default:l(()=>[t(E,{modelValue:o.link,"onUpdate:modelValue":n=>o.link=n},null,8,["modelValue","onUpdate:modelValue"])]),_:2},1024),t(_,{label:"是否显示",class:"mt-[18px]"},{default:l(()=>[s("div",W,[t(U,{modelValue:o.is_show,"onUpdate:modelValue":n=>o.is_show=n,"active-value":"1","inactive-value":"0"},null,8,["modelValue","onUpdate:modelValue"]),s("div",X,[t(C,{name:"el-icon-Rank",size:"18"})])])]),_:2},1024)])])]),_:2},1032,["onClose"]))]),_:1},8,["modelValue"]),((x=r.content.data)==null?void 0:x.length)<p?(i(),g("div",Y,[t(D,{class:"w-full",type:"primary",onClick:k},{default:l(()=>[...e[1]||(e[1]=[L("添加图片",-1)])]),_:1})])):P("",!0)]}),_:1})]),_:1})])}}});export{le as _};
|
||||
import{o as I,q as i,r as g,v as t,D as l,bg as O,s,u,bQ as j,O as F,b6 as q,b7 as z,I as A,K,L,P,b9 as Q,p as R,cn as b}from"./.pnpm-C1KTa5cB.js";import{_ as S}from"./index-CQoyCdiY.js";import{c as T,i as v}from"./index-BzVi6Aa3.js";import{_ as G}from"./picker-CoG_3YpY.js";import{_ as H}from"./picker-DFb6DRYi.js";const J={class:"bg-fill-light flex items-center w-full p-4 mt-4"},M={class:"ml-3 flex-1"},W={class:"flex-1 flex items-center"},X={class:"drag-move cursor-move ml-auto"},Y={key:0,class:"mt-4"},p=5,le=I({__name:"attr",props:{content:{type:Object,default:()=>({})},styles:{type:Object,default:()=>({})}},emits:["update:content"],setup(r,{emit:h}){const m=h,c=r,f=R({get:()=>c.content,set:a=>{m("update:content",a)}}),k=()=>{var a;if(((a=c.content.data)==null?void 0:a.length)<p){const e=b(c.content);e.data.push({is_show:"1",image:"",name:"",link:{}}),m("update:content",e)}else v.msgError(`最多添加${p}张图片`)},w=a=>{var d;if(((d=c.content.data)==null?void 0:d.length)<=1)return v.msgError("最少保留一张图片");const e=b(c.content);e.data.splice(a,1),m("update:content",e)};return(a,e)=>{const d=H,y=z,_=q,E=G,U=A,C=T,B=S,D=K,N=O,$=Q;return i(),g("div",null,[t($,{"label-width":"70px"},{default:l(()=>[t(N,{shadow:"never",class:"!border-none flex mt-2"},{default:l(()=>{var x;return[e[2]||(e[2]=s("div",{class:"flex items-end mb-4"},[s("div",{class:"text-base text-[#101010] font-medium"},"菜单"),s("div",{class:"text-xs text-tx-secondary ml-2"}," 最多添加5张,建议图片尺寸:750px*200px ")],-1)),t(u(j),{class:"draggable",modelValue:u(f).data,"onUpdate:modelValue":e[0]||(e[0]=o=>u(f).data=o),animation:"300",handle:".drag-move","item-key":"index"},{item:l(({element:o,index:V})=>[(i(),F(B,{key:V,onClose:n=>w(V),class:"w-[467px]"},{default:l(()=>[s("div",J,[t(d,{modelValue:o.image,"onUpdate:modelValue":n=>o.image=n,"upload-class":"bg-body","exclude-domain":""},null,8,["modelValue","onUpdate:modelValue"]),s("div",M,[t(_,{label:"图片名称"},{default:l(()=>[t(y,{modelValue:o.name,"onUpdate:modelValue":n=>o.name=n,placeholder:"请输入名称"},null,8,["modelValue","onUpdate:modelValue"])]),_:2},1024),t(_,{class:"mt-[18px]",label:"图片链接"},{default:l(()=>[t(E,{modelValue:o.link,"onUpdate:modelValue":n=>o.link=n},null,8,["modelValue","onUpdate:modelValue"])]),_:2},1024),t(_,{label:"是否显示",class:"mt-[18px]"},{default:l(()=>[s("div",W,[t(U,{modelValue:o.is_show,"onUpdate:modelValue":n=>o.is_show=n,"active-value":"1","inactive-value":"0"},null,8,["modelValue","onUpdate:modelValue"]),s("div",X,[t(C,{name:"el-icon-Rank",size:"18"})])])]),_:2},1024)])])]),_:2},1032,["onClose"]))]),_:1},8,["modelValue"]),((x=r.content.data)==null?void 0:x.length)<p?(i(),g("div",Y,[t(D,{class:"w-full",type:"primary",onClick:k},{default:l(()=>[...e[1]||(e[1]=[L("添加图片",-1)])]),_:1})])):P("",!0)]}),_:1})]),_:1})])}}});export{le as _};
|
||||
@@ -0,0 +1 @@
|
||||
import{_ as o}from"./auth.vue_vue_type_script_setup_true_lang-BIdlBWpp.js";import"./.pnpm-C1KTa5cB.js";import"./menu-BEp8tD9Y.js";import"./index-BzVi6Aa3.js";import"./role-BQzGowK6.js";import"./index-Da4XqmJd.js";export{o as default};
|
||||
@@ -1 +0,0 @@
|
||||
import{_ as o}from"./auth.vue_vue_type_script_setup_true_lang-D7L7W7OR.js";import"./.pnpm-C1KTa5cB.js";import"./menu-D0pxhNrw.js";import"./index-Cxx1IoGl.js";import"./role-Ckwc7kKZ.js";import"./index-B3Fu5tec.js";export{o as default};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user