Compare commits

..
Author SHA1 Message Date
Your Name e76f16193b 更新 2026-07-06 15:00:10 +08:00
428 changed files with 2742 additions and 38684 deletions
+1 -1
View File
@@ -29,7 +29,7 @@
stroke-dasharray: 90, 150;
stroke-dashoffset: 0;
stroke-width: 2;
stroke: #4073fa;
stroke: #06b6d4;
stroke-linecap: round;
}
-7
View File
@@ -11,7 +11,6 @@
"dependencies": {
"@element-plus/icons-vue": "^2.3.1",
"@highlightjs/vue-plugin": "^2.1.0",
"@mediapipe/tasks-vision": "^0.10.35",
"@tencentcloud/call-uikit-vue": "^4.0.12",
"@tencentcloud/chat-uikit-vue3": "^4.5.4",
"@trtc/calls-uikit-vue": "^4.4.6",
@@ -2485,12 +2484,6 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@mediapipe/tasks-vision": {
"version": "0.10.35",
"resolved": "https://registry.npmjs.org/@mediapipe/tasks-vision/-/tasks-vision-0.10.35.tgz",
"integrity": "sha512-HOvadwVRE6JC+45nyYhmnywnr5h/J8KZvOeUNVOG9q/0875pZgItznFB9bRTvLc264YSJqiZ1NsIpCStJw/egg==",
"license": "Apache-2.0"
},
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
-1
View File
@@ -13,7 +13,6 @@
"dependencies": {
"@element-plus/icons-vue": "^2.3.1",
"@highlightjs/vue-plugin": "^2.1.0",
"@mediapipe/tasks-vision": "^0.10.35",
"@tencentcloud/call-uikit-vue": "^4.0.12",
"@tencentcloud/chat-uikit-vue3": "^4.5.4",
"@trtc/calls-uikit-vue": "^4.4.6",
Binary file not shown.
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+158
View File
@@ -0,0 +1,158 @@
/**
* 将标准双 el-card 列表页迁移为 admin-page 架构组件
* 用法: node scripts/migrate-admin-pages.mjs [--dry-run] [glob...]
*/
import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const viewsRoot = path.join(__dirname, '../src/views')
const dryRun = process.argv.includes('--dry-run')
const extraPaths = process.argv.slice(2).filter((a) => !a.startsWith('--'))
const defaultTargets = [
'article/lists/index.vue',
'article/column/index.vue',
'permission/role/index.vue',
'permission/menu/index.vue',
'dev_tools/code/index.vue',
'setting/dict/type/index.vue',
'setting/dict/data/index.vue',
'message/notice/index.vue',
'message/short_letter/index.vue',
'fans/index.vue',
'order/index.vue',
'asset/user/index.vue',
'asset/resource/index.vue',
'finance/balance_details.vue',
'finance/recharge_record.vue',
'finance/refund_record.vue'
]
function migrate(content) {
if (content.includes('admin-page-filter-panel')) return null
if (!content.includes('el-card class="!border-none"')) return null
if (!content.includes('<el-table')) return null
let out = content
// 根容器 class 清理
out = out.replace(
/<div class="[^"]*">\s*\n\s*<el-card class="!border-none" shadow="never">/,
'<div>\n <admin-page-filter-panel>'
)
if (!out.includes('admin-page-filter-panel')) {
out = out.replace(
/<el-card class="!border-none" shadow="never">/,
'<admin-page-filter-panel>'
)
}
// 第一个 filter card 结束
out = out.replace(
/<\/el-form>\s*\n\s*<\/el-card>/,
'</el-form>\n </admin-page-filter-panel>'
)
// 第二个 data card 开始 - 提取 v-loading
const loadingMatch = out.match(
/<el-card([^>]*?)class="[^"]*mt-4[^"]*"[^>]*shadow="never"[^>]*>/
)
const altLoadingMatch = out.match(
/<el-card v-loading="([^"]+)"([^>]*)class="[^"]*mt-4[^"]*"([^>]*)shadow="never"([^>]*)>/
)
if (altLoadingMatch) {
out = out.replace(
altLoadingMatch[0],
`<admin-page-data-panel v-loading="${altLoadingMatch[1]}">`
)
} else if (loadingMatch) {
out = out.replace(loadingMatch[0], '<admin-page-data-panel>')
} else {
out = out.replace(
/<el-card class="!border-none mt-4" shadow="never">/,
'<admin-page-data-panel>'
)
out = out.replace(
/<el-card v-loading="([^"]+)" class="mt-4 !border-none" shadow="never">/,
'<admin-page-data-panel v-loading="$1">'
)
out = out.replace(
/<el-card v-loading="([^"]+)" class="!border-none mt-4" shadow="never">/,
'<admin-page-data-panel v-loading="$1">'
)
}
// toolbar: 紧跟 data panel 后的首个 div 包裹按钮
out = out.replace(
/(<admin-page-data-panel[^>]*>\s*)<div>\s*\n(\s*<el-button[\s\S]*?<\/div>\s*\n)/,
'$1<template #toolbar>\n$2</template>\n'
)
out = out.replace(
/(<admin-page-data-panel[^>]*>\s*)<div>\s*\n(\s*<router-link[\s\S]*?<\/div>\s*\n)/,
'$1<template #toolbar>\n$2</template>\n'
)
// 移除 table 前的 mt-4 class
out = out.replace(/<el-table class="mt-4"/g, '<el-table')
out = out.replace(/<el-table\s+class="mt-4"\s+/g, '<el-table ')
// pagination footer
out = out.replace(
/<div class="flex(?: mt-4)? justify-end(?: mt-4)?">\s*\n\s*<pagination([^/]*)\/>\s*\n\s*<\/div>\s*\n\s*<\/el-card>/,
'<template #footer>\n <pagination$1/>\n </template>\n </admin-page-data-panel>'
)
out = out.replace(
/<div class="flex mt-4 justify-end">\s*\n\s*<pagination([^/]*)\/>\s*\n\s*<\/div>\s*\n\s*<\/el-card>/,
'<template #footer>\n <pagination$1/>\n </template>\n </admin-page-data-panel>'
)
// 无 pagination 的 data card 闭合
if (out.includes('<admin-page-data-panel') && out.includes('</el-card>')) {
out = out.replace(/<\/el-table>\s*\n\s*<\/el-card>/, '</el-table>\n </admin-page-data-panel>')
}
// table 上的 v-loading 移到 panel(若 panel 还没有)
out = out.replace(
/<admin-page-data-panel>\s*\n(\s*)<el-table([^>]*?)v-loading="([^"]+)"([^>]*)>/,
'<admin-page-data-panel v-loading="$3">\n$1<el-table$2$4>'
)
out = out.replace(/<el-table([^>]*?)v-loading="([^"]+)"([^>]*)>/, '<el-table$1$3>')
if (out === content || !out.includes('admin-page-data-panel')) return null
return out
}
const targets = extraPaths.length
? extraPaths.map((p) => path.resolve(p))
: defaultTargets.map((p) => path.join(viewsRoot, p))
let migrated = 0
let skipped = 0
for (const file of targets) {
if (!fs.existsSync(file)) {
console.warn('skip (missing):', path.relative(viewsRoot, file))
skipped++
continue
}
const original = fs.readFileSync(file, 'utf8')
const result = migrate(original)
if (!result) {
console.log('skip (no match):', path.relative(viewsRoot, file))
skipped++
continue
}
if (dryRun) {
console.log('would migrate:', path.relative(viewsRoot, file))
} else {
fs.writeFileSync(file, result, 'utf8')
console.log('migrated:', path.relative(viewsRoot, file))
}
migrated++
}
console.log(`\nDone. migrated=${migrated} skipped=${skipped}${dryRun ? ' (dry-run)' : ''}`)
@@ -0,0 +1,24 @@
<template>
<section class="admin-data-panel" v-loading="loading">
<div v-if="$slots.toolbar" class="admin-data-panel__toolbar">
<slot name="toolbar" />
</div>
<div class="admin-data-panel__body">
<slot />
</div>
<div v-if="$slots.footer" class="admin-data-panel__footer">
<slot name="footer" />
</div>
</section>
</template>
<script setup lang="ts">
withDefaults(
defineProps<{
loading?: boolean
}>(),
{
loading: false
}
)
</script>
@@ -0,0 +1,60 @@
<template>
<section class="admin-filter-panel" :class="{ 'is-collapsed': collapsed }">
<div v-if="collapsible" class="admin-filter-panel__head">
<span class="admin-filter-panel__label">{{ title }}</span>
<el-button class="admin-filter-panel__toggle" link type="primary" @click="collapsed = !collapsed">
{{ collapsed ? '展开筛选' : '收起筛选' }}
</el-button>
</div>
<div class="admin-filter-panel__body">
<slot />
</div>
</section>
</template>
<script setup lang="ts">
withDefaults(
defineProps<{
/** 是否显示收起/展开 */
collapsible?: boolean
/** 筛选区标题 */
title?: string
}>(),
{
collapsible: true,
title: '筛选条件'
}
)
const collapsed = ref(false)
</script>
@@ -0,0 +1,24 @@
<template>
<section class="admin-form-panel">
<header v-if="title || $slots.header" class="admin-form-panel__header">
<slot name="header">
<h2 v-if="title" class="admin-form-panel__title">{{ title }}</h2>
</slot>
</header>
<div class="admin-form-panel__body">
<slot />
</div>
<footer v-if="$slots.footer" class="admin-form-panel__footer">
<slot name="footer" />
</footer>
</section>
</template>
<script setup lang="ts">
defineProps({
title: {
type: String,
default: ''
}
})
</script>
@@ -0,0 +1,5 @@
<template>
<div class="admin-page-actions">
<slot />
</div>
</template>
@@ -0,0 +1,16 @@
<template>
<div class="admin-stat-grid" :class="[`admin-stat-grid--cols-${columns}`]">
<slot />
</div>
</template>
<script setup lang="ts">
withDefaults(
defineProps<{
columns?: 2 | 3 | 4 | 5
}>(),
{
columns: 4
}
)
</script>
@@ -0,0 +1,27 @@
<template>
<article class="admin-stat-item" :class="[`admin-stat-item--${tone}`]">
<div class="admin-stat-item__label">{{ label }}</div>
<div class="admin-stat-item__value" :class="{ 'is-money': money }">
<slot>{{ value }}</slot>
</div>
<p v-if="hint" class="admin-stat-item__hint">{{ hint }}</p>
</article>
</template>
<script setup lang="ts">
withDefaults(
defineProps<{
label: string
value?: string | number
hint?: string
money?: boolean
tone?: 'default' | 'primary' | 'success' | 'warning'
}>(),
{
value: '',
hint: '',
money: false,
tone: 'default'
}
)
</script>
@@ -1,812 +0,0 @@
<template>
<span class="beauty-trigger" @click="openPanel">
<slot name="reference">
<el-button type="primary" link size="small">
<el-icon><MagicStick /></el-icon>
<span class="beauty-btn-text">美颜</span>
</el-button>
</slot>
</span>
<Teleport to="body">
<div
v-if="visible"
class="beauty-window"
:style="windowStyle"
@mousedown.stop
>
<div class="beauty-window-header" @mousedown="onDragStart">
<span class="beauty-window-title">
<el-icon><MagicStick /></el-icon>
美颜设置
</span>
<el-button type="danger" link class="beauty-window-close" @mousedown.stop @click="closePanel">
<el-icon><Close /></el-icon>
</el-button>
</div>
<div class="beauty-window-body">
<!-- 左侧大预览 -->
<div class="beauty-preview-col">
<div ref="previewStageRef" class="beauty-preview-stage">
<video
v-show="previewing"
ref="previewVideoRef"
class="beauty-preview-video"
autoplay
muted
playsinline
></video>
<div v-if="!previewing" class="beauty-preview-placeholder">
<el-icon :size="36"><VideoCamera /></el-icon>
<span v-if="previewLoading">正在打开摄像头...</span>
<span v-else-if="previewError" class="beauty-preview-error-text">{{ previewError }}</span>
<span v-else>摄像头预览未开启</span>
</div>
<span v-if="previewing" class="beauty-preview-badge">对方看到的效果镜像显示</span>
<el-button
v-if="previewing"
class="beauty-fullscreen-btn"
circle
size="small"
title="全屏预览(按 Esc 退出)"
@click="toggleFullscreen"
>
<el-icon><FullScreen /></el-icon>
</el-button>
</div>
<div class="beauty-preview-toolbar">
<el-button
size="small"
:loading="previewLoading"
@click="togglePreview"
>
{{ previewing ? '关闭预览' : '打开预览' }}
</el-button>
<el-button
v-if="previewing"
size="small"
@click="toggleFullscreen"
>
<el-icon><FullScreen /></el-icon>
<span style="margin-left: 2px">全屏</span>
</el-button>
<span class="beauty-preview-note">预览效果即对方所见画面镜像显示</span>
</div>
</div>
<!-- 右侧调节项 -->
<div class="beauty-control-col">
<div class="beauty-section-header">
<span class="beauty-section-title">基础美颜</span>
<el-switch v-model="settings.enabled" size="small" />
</div>
<div class="beauty-section-body" :class="{ 'is-disabled': !settings.enabled }">
<div class="beauty-item">
<span class="beauty-label">磨皮算法</span>
<el-radio-group
v-model="settings.style"
size="small"
:disabled="!settings.enabled"
>
<el-radio-button :value="BEAUTY_STYLE_NATURE">自然</el-radio-button>
<el-radio-button :value="BEAUTY_STYLE_SMOOTH">光滑</el-radio-button>
</el-radio-group>
</div>
<div class="beauty-item">
<el-tooltip content="磨皮仅作用于人脸皮肤,眉眼/背景/衣物不受影响" placement="left">
<span class="beauty-label has-tip">磨皮</span>
</el-tooltip>
<el-slider
v-model="settings.beautyLevel"
:min="0"
:max="BEAUTY_LEVEL_MAX"
:step="1"
:disabled="!settings.enabled"
/>
</div>
<div class="beauty-item">
<span class="beauty-label">美白</span>
<el-slider
v-model="settings.whitenessLevel"
:min="0"
:max="BEAUTY_LEVEL_MAX"
:step="1"
:disabled="!settings.enabled"
/>
</div>
<div class="beauty-item">
<span class="beauty-label">红润</span>
<el-slider
v-model="settings.ruddinessLevel"
:min="0"
:max="BEAUTY_LEVEL_MAX"
:step="1"
:disabled="!settings.enabled"
/>
</div>
<div class="beauty-item">
<el-tooltip content="淡化脸上色斑 / 黑色素沉着,仅作用于人脸皮肤" placement="left">
<span class="beauty-label has-tip">祛斑</span>
</el-tooltip>
<el-slider
v-model="settings.spotLevel"
:min="0"
:max="BEAUTY_LEVEL_MAX"
:step="1"
:disabled="!settings.enabled"
/>
</div>
<div class="beauty-item">
<el-tooltip content="淡化脸上小面积深色痣点,仅作用于人脸皮肤" placement="left">
<span class="beauty-label has-tip">去痣</span>
</el-tooltip>
<el-slider
v-model="settings.moleLevel"
:min="0"
:max="BEAUTY_LEVEL_MAX"
:step="1"
:disabled="!settings.enabled"
/>
</div>
<div class="beauty-item">
<el-tooltip content="提亮眼下区域,减轻黑眼圈" placement="left">
<span class="beauty-label has-tip">去黑眼圈</span>
</el-tooltip>
<el-slider
v-model="settings.darkCircleLevel"
:min="0"
:max="BEAUTY_LEVEL_MAX"
:step="1"
:disabled="!settings.enabled"
/>
</div>
<div class="beauty-item">
<el-tooltip content="提升画面整体亮度,适合光线偏暗环境" placement="left">
<span class="beauty-label has-tip">亮度</span>
</el-tooltip>
<el-slider
v-model="settings.brightnessLevel"
:min="0"
:max="BEAUTY_LEVEL_MAX"
:step="1"
:disabled="!settings.enabled"
/>
</div>
<div class="beauty-item">
<el-tooltip content="锐化细节,让画面更清晰" placement="left">
<span class="beauty-label has-tip">清晰度</span>
</el-tooltip>
<el-slider
v-model="settings.clarityLevel"
:min="0"
:max="BEAUTY_LEVEL_MAX"
:step="1"
:disabled="!settings.enabled"
/>
</div>
</div>
<el-divider class="beauty-divider" />
<div class="beauty-section-header">
<span class="beauty-section-title">
高级美颜
<el-tooltip
placement="top"
content="瘦脸/口红/腮红由本机 AI 实时处理(免费,无需腾讯付费特效)。开关在下次开启摄像头时生效,通话中可关闭摄像头再打开。"
>
<el-icon class="beauty-help-icon"><QuestionFilled /></el-icon>
</el-tooltip>
</span>
<el-switch
v-model="settings.advanced.enabled"
size="small"
:disabled="!advancedSupported"
/>
</div>
<div v-if="!advancedSupported" class="beauty-unsupported">
当前浏览器不支持高级美颜需要 WebGL请使用新版 Chrome / Edge
</div>
<div
v-else
class="beauty-section-body"
:class="{ 'is-disabled': !settings.advanced.enabled }"
>
<div class="beauty-item">
<span class="beauty-label">瘦脸</span>
<el-slider
v-model="settings.advanced.slimStrength"
:min="0"
:max="100"
:step="1"
:disabled="!settings.advanced.enabled"
/>
</div>
<div class="beauty-item">
<span class="beauty-label">口红</span>
<el-slider
v-model="settings.advanced.lipstickStrength"
:min="0"
:max="100"
:step="1"
:disabled="!settings.advanced.enabled"
/>
<el-color-picker
v-model="settings.advanced.lipstickColor"
size="small"
:predefine="LIPSTICK_PRESETS"
:disabled="!settings.advanced.enabled"
/>
</div>
<div class="beauty-item">
<span class="beauty-label">腮红</span>
<el-slider
v-model="settings.advanced.blushStrength"
:min="0"
:max="100"
:step="1"
:disabled="!settings.advanced.enabled"
/>
<el-color-picker
v-model="settings.advanced.blushColor"
size="small"
:predefine="BLUSH_PRESETS"
:disabled="!settings.advanced.enabled"
/>
</div>
</div>
<div v-if="needCameraRestartHint" class="beauty-restart-hint">
通话中如果摄像头已按原始画面开启需关闭摄像头再打开才能生效
</div>
<div class="beauty-window-footer">
<span class="beauty-tip">全部为免费能力通话中实时生效</span>
<el-button link type="primary" size="small" @click="handleReset">
恢复默认
</el-button>
</div>
</div>
</div>
</div>
</Teleport>
</template>
<script setup lang="ts">
import { reactive, ref, watch, computed, nextTick, onBeforeUnmount } from 'vue'
import { MagicStick, QuestionFilled, Close, VideoCamera, FullScreen } from '@element-plus/icons-vue'
import {
BEAUTY_LEVEL_MAX,
BEAUTY_STYLE_NATURE,
BEAUTY_STYLE_SMOOTH,
applyBeautyToEngine,
defaultBeautySettings,
hasAnyBeautyEffect,
loadBeautySettings,
saveBeautySettings
} from '@/utils/call-beauty'
import {
getRawUserMedia,
installBeautyMediaInterceptor,
preloadBeautyModel
} from '@/utils/beauty/beauty-media-interceptor'
import {
FaceBeautyPipeline,
hasActiveBeautyPipeline,
isAdvancedBeautySupported,
updateActiveBeautyPipelines
} from '@/utils/beauty/face-beauty-pipeline'
const LIPSTICK_PRESETS = ['#c94f5e', '#b03a48', '#d96a76', '#a52a3c', '#e0788a', '#8f2436']
const BLUSH_PRESETS = ['#e88193', '#f0a3b0', '#dd6b7f', '#f2b8c0', '#e5949e']
const WINDOW_WIDTH = 780
const props = withDefaults(
defineProps<{
/** 获取当前 TUICallEngine 实例(未初始化时返回 null 即可) */
getEngine: () => any
/** 面板 z-index,需高于承载视频通话的浮窗 */
popperZIndex?: number
}>(),
{
popperZIndex: 300001
}
)
// 拦截器必须先于 SDK 申请摄像头安装;组件被通话相关页面引入时即安装
installBeautyMediaInterceptor()
const settings = reactive(loadBeautySettings())
const advancedSupported = isAdvancedBeautySupported()
// 面板挂载即预热模型,避免首次打开预览时同步加载 WASM/模型卡死
if (advancedSupported) preloadBeautyModel()
/** 美颜已开启但当前没有活跃通话管线 → 摄像头是在开启前用原始画面打开的 */
const needCameraRestartHint = computed(() => {
return hasAnyBeautyEffect(settings) && !hasActiveBeautyPipeline()
})
// ---------------------------------------------------------------------------
// 浮动窗口(可拖拽)
// ---------------------------------------------------------------------------
const visible = ref(false)
const windowPos = reactive({ x: 0, y: 0 })
const windowStyle = computed(() => ({
left: `${windowPos.x}px`,
top: `${windowPos.y}px`,
width: `${WINDOW_WIDTH}px`,
zIndex: props.popperZIndex
}))
const dragState = { active: false, offsetX: 0, offsetY: 0 }
const onDragMove = (e: MouseEvent) => {
if (!dragState.active) return
windowPos.x = Math.max(0, Math.min(window.innerWidth - 120, e.clientX - dragState.offsetX))
windowPos.y = Math.max(0, Math.min(window.innerHeight - 60, e.clientY - dragState.offsetY))
}
const onDragEnd = () => {
dragState.active = false
document.removeEventListener('mousemove', onDragMove)
document.removeEventListener('mouseup', onDragEnd)
}
const onDragStart = (e: MouseEvent) => {
dragState.active = true
dragState.offsetX = e.clientX - windowPos.x
dragState.offsetY = e.clientY - windowPos.y
document.addEventListener('mousemove', onDragMove)
document.addEventListener('mouseup', onDragEnd)
e.preventDefault()
}
const openPanel = async () => {
if (visible.value) return
windowPos.x = Math.max(16, Math.floor((window.innerWidth - WINDOW_WIDTH) / 2))
windowPos.y = Math.max(16, Math.floor(window.innerHeight * 0.12))
visible.value = true
// 先预热模型,并等面板先画出「正在打开摄像头」再启预览,避免首开卡死
if (advancedSupported) {
preloadBeautyModel()
await nextTick()
await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()))
void startPreview()
}
}
const closePanel = () => {
stopPreview()
visible.value = false
}
// ---------------------------------------------------------------------------
// 基础美颜:下发到 TUICallEngine
// ---------------------------------------------------------------------------
let applyTimer: ReturnType<typeof setTimeout> | null = null
/**
* 基础美颜下发到 TUICallEngineTRTC BasicBeauty 插件)。
* 仅作为 WebGL 不可用时的回退——正常情况下全部效果走本地管线。
* WebGL 可用时始终向引擎下发全 0,确保引擎侧插件保持关闭,
* 不会与本地管线叠加,也保证「关闭美颜」时引擎侧残留效果被清掉。
*/
const applyNow = async () => {
if (advancedSupported) {
await applyBeautyToEngine(props.getEngine(), { ...settings, enabled: false })
return
}
await applyBeautyToEngine(props.getEngine(), settings)
}
/** 通话建立后调用:仅在启用了美颜时下发,避免无意义地拉起插件 */
const applyIfEnabled = async () => {
if (!settings.enabled) return
await applyNow()
}
watch(
settings,
() => {
saveBeautySettings(settings)
// 全部美颜参数对活跃管线(含预览)实时生效
updateActiveBeautyPipelines({ ...settings, advanced: { ...settings.advanced } })
if (settings.advanced.enabled) preloadBeautyModel()
if (applyTimer) clearTimeout(applyTimer)
// 滑杆拖动会高频触发,防抖后再下发到引擎(仅 WebGL 回退场景实际生效)
applyTimer = setTimeout(() => {
applyTimer = null
void applyNow()
}, 200)
},
{ deep: true }
)
// ---------------------------------------------------------------------------
// 效果预览:本地摄像头 → 同一条美颜管线 → 大画面播放,不发起任何通话
// ---------------------------------------------------------------------------
const previewing = ref(false)
const previewLoading = ref(false)
const previewError = ref('')
const previewVideoRef = ref<HTMLVideoElement | null>(null)
const previewStageRef = ref<HTMLElement | null>(null)
let previewPipeline: FaceBeautyPipeline | null = null
const toggleFullscreen = () => {
if (document.fullscreenElement) {
void document.exitFullscreen().catch(() => {})
} else {
void previewStageRef.value?.requestFullscreen().catch(() => {})
}
}
const startPreview = async () => {
if (previewing.value || previewLoading.value) return
previewError.value = ''
previewLoading.value = true
try {
// 720p 采集保证预览清晰;管线内部会做适度降采样控制处理开销
const raw = await getRawUserMedia({
video: {
width: { ideal: 1280 },
height: { ideal: 720 },
frameRate: { ideal: 30 }
},
audio: false
})
const track = raw.getVideoTracks()[0]
if (!track) throw new Error('未获取到摄像头画面')
// 让出一帧,保证 loading 文案先渲染
await nextTick()
await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()))
previewPipeline = new FaceBeautyPipeline(
track,
{ ...settings, advanced: { ...settings.advanced } },
{ isPreview: true }
)
const processedTrack = await previewPipeline.start()
previewing.value = true
await nextTick()
if (previewVideoRef.value) {
previewVideoRef.value.srcObject = new MediaStream([processedTrack])
await previewVideoRef.value.play().catch(() => {})
}
} catch (err: any) {
console.warn('[call-beauty] 预览启动失败', err)
previewPipeline?.destroy()
previewPipeline = null
previewing.value = false
previewError.value =
err?.name === 'NotAllowedError'
? '摄像头权限被拒绝,请在浏览器地址栏允许摄像头访问'
: err?.name === 'NotReadableError'
? '摄像头被其他程序占用,无法打开预览'
: '预览启动失败,请检查摄像头设备'
} finally {
previewLoading.value = false
}
}
const stopPreview = () => {
if (document.fullscreenElement && document.fullscreenElement === previewStageRef.value) {
void document.exitFullscreen().catch(() => {})
}
if (previewVideoRef.value) previewVideoRef.value.srcObject = null
// destroy 会同时停掉输出轨和源摄像头轨,释放摄像头占用
previewPipeline?.destroy()
previewPipeline = null
previewing.value = false
previewLoading.value = false
}
const togglePreview = () => {
if (previewing.value) {
stopPreview()
} else {
void startPreview()
}
}
const handleReset = () => {
const defaults = defaultBeautySettings()
Object.assign(settings.advanced, defaults.advanced, { enabled: settings.advanced.enabled })
Object.assign(settings, {
style: defaults.style,
beautyLevel: defaults.beautyLevel,
whitenessLevel: defaults.whitenessLevel,
ruddinessLevel: defaults.ruddinessLevel,
spotLevel: defaults.spotLevel,
moleLevel: defaults.moleLevel,
darkCircleLevel: defaults.darkCircleLevel,
brightnessLevel: defaults.brightnessLevel,
clarityLevel: defaults.clarityLevel
})
}
onBeforeUnmount(() => {
if (applyTimer) clearTimeout(applyTimer)
stopPreview()
onDragEnd()
})
defineExpose({
applyNow,
applyIfEnabled,
openPanel
})
</script>
<style lang="scss" scoped>
.beauty-trigger {
display: inline-flex;
align-items: center;
}
.beauty-btn-text {
margin-left: 2px;
font-size: 12px;
}
</style>
<style lang="scss">
.beauty-window {
position: fixed;
max-width: calc(100vw - 24px);
max-height: calc(100vh - 24px);
background: #fff;
border-radius: 10px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.28);
overflow: hidden;
display: flex;
flex-direction: column;
.beauty-window-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 16px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
cursor: move;
user-select: none;
.beauty-window-title {
display: inline-flex;
align-items: center;
gap: 6px;
color: #fff;
font-size: 14px;
font-weight: 500;
}
.beauty-window-close {
color: #fff;
&:hover {
color: #ffd4d4;
}
}
}
.beauty-window-body {
display: flex;
gap: 16px;
padding: 16px;
flex: 1;
min-height: 0;
overflow: hidden;
@media (max-width: 820px) {
flex-direction: column;
overflow-y: auto;
}
}
.beauty-preview-col {
flex: 1 1 440px;
min-width: 0;
.beauty-preview-stage {
position: relative;
border-radius: 8px;
overflow: hidden;
background: #111;
aspect-ratio: 16 / 9;
.beauty-preview-video {
width: 100%;
height: 100%;
/* contain 完整显示画面,与实际发送给对方的内容一致,不裁切 */
object-fit: contain;
/* 本地自拍习惯镜像显示;实际发送给对方的画面不镜像 */
transform: scaleX(-1);
}
.beauty-preview-placeholder {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 10px;
color: #8a8f99;
font-size: 13px;
padding: 0 24px;
text-align: center;
.beauty-preview-error-text {
color: #f56c6c;
}
}
.beauty-preview-badge {
position: absolute;
left: 10px;
bottom: 10px;
padding: 3px 10px;
border-radius: 12px;
background: rgba(0, 0, 0, 0.55);
font-size: 12px;
color: #fff;
line-height: 1.6;
}
.beauty-fullscreen-btn {
position: absolute;
right: 10px;
top: 10px;
background: rgba(0, 0, 0, 0.45);
border-color: transparent;
color: #fff;
&:hover {
background: rgba(0, 0, 0, 0.65);
color: #fff;
}
}
/* 全屏时铺满屏幕 */
&:fullscreen {
aspect-ratio: auto;
width: 100vw;
height: 100vh;
}
}
.beauty-preview-toolbar {
display: flex;
align-items: center;
gap: 10px;
margin-top: 10px;
.beauty-preview-note {
font-size: 12px;
color: #909399;
line-height: 1.5;
}
}
}
.beauty-control-col {
flex: 0 0 300px;
min-width: 260px;
display: flex;
flex-direction: column;
min-height: 0;
overflow-y: auto;
padding-right: 6px;
scrollbar-width: thin;
}
.beauty-section-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
.beauty-section-title {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 14px;
font-weight: 500;
color: #303133;
}
.beauty-help-icon {
color: #a8abb2;
cursor: help;
font-size: 14px;
}
}
.beauty-divider {
margin: 10px 0;
}
.beauty-section-body {
&.is-disabled {
opacity: 0.75;
}
.beauty-item {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 4px;
.beauty-label {
flex-shrink: 0;
width: 64px;
font-size: 13px;
color: #606266;
&.has-tip {
cursor: help;
text-decoration: underline dotted #c0c4cc;
text-underline-offset: 3px;
}
}
.el-slider {
flex: 1;
margin-right: 8px;
}
.el-color-picker {
flex-shrink: 0;
}
&.beauty-item-hint {
margin-top: -2px;
margin-bottom: 6px;
.beauty-hint {
font-size: 12px;
color: #909399;
line-height: 1.4;
}
}
}
}
.beauty-unsupported {
padding: 4px 0 8px;
font-size: 12px;
color: #e6a23c;
line-height: 1.5;
}
.beauty-restart-hint {
margin-top: 4px;
padding: 4px 8px;
border-radius: 4px;
background: #fdf6ec;
font-size: 12px;
color: #e6a23c;
line-height: 1.5;
}
.beauty-window-footer {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: auto;
padding-top: 8px;
.beauty-tip {
font-size: 12px;
color: #909399;
}
}
}
</style>
+1 -33
View File
@@ -64,20 +64,6 @@
>
群视频
</el-button>
<!-- 美颜设置免费基础美颜 + 本地 AI 瘦脸/口红/腮红带大窗口实时预览仅对开启了美颜功能的医生显示 -->
<CallBeautyPanel
v-if="isCallReady && beautyAllowed"
ref="beautyPanelRef"
:get-engine="getCallEngine"
:popper-z-index="300002"
>
<template #reference>
<el-button size="small">
<el-icon><MagicStick /></el-icon>
<span style="margin-left: 2px">美颜</span>
</el-button>
</template>
</CallBeautyPanel>
</div>
</template>
</MessageInput>
@@ -141,8 +127,7 @@
<script setup lang="ts">
import { ref, nextTick, watch, computed, onMounted, onUnmounted } from 'vue'
import { Loading, Close, Rank, Camera, Fold, Expand, MagicStick } from '@element-plus/icons-vue'
import CallBeautyPanel from '@/components/call-beauty-panel/index.vue'
import { Loading, Close, Rank, Camera, Fold, Expand } from '@element-plus/icons-vue'
import { uploadImageBlob, uploadVideoBlob } from '@/api/file'
import { addDoctorNote } from '@/api/patient'
import {
@@ -202,17 +187,6 @@ const patientUserId = ref('')
const conversationId = ref('')
const isCallReady = ref(false)
const showCallKitWindow = ref(false) // 仅在通话中显示视频窗口
/** 美颜面板(本地 AI 管线 + 引擎兜底,非付费特效 SDK) */
const beautyPanelRef = ref<InstanceType<typeof CallBeautyPanel> | null>(null)
/** 医生编辑页「美颜功能」开关:关闭时不显示美颜入口 */
const beautyAllowed = computed(() => userStore.userInfo?.enable_beauty !== 0)
const getCallEngine = () => {
try {
return TUICallKitServer.getTUICallEngineInstance()
} catch {
return null
}
}
const { login, logout } = useLoginState()
const { setActiveConversation, createC2CConversation, activeConversation } = useConversationListState()
const userStore = useUserStore()
@@ -749,8 +723,6 @@ function setupCallRoomBinding() {
}
if (next === CALL_STATUS_CONNECTED) {
void tryBindRoomIfReady()
// 接通后重新下发已保存的美颜设置(摄像头此时已开启,插件可正常启动)
void beautyPanelRef.value?.applyIfEnabled()
clearLocalRecordingStartTimer()
if (!isLochostVodEnabled.value) {
previousCallStatus = next
@@ -1119,10 +1091,6 @@ const open = async (data: { patientId: number; patientName: string; diagnosisId?
showCallKitWindow.value = true
pendingCallRecordStart = ensureCallRecordStarted()
void pendingCallRecordStart
// 呼叫阶段本地摄像头预览已开启,延迟下发美颜让医生在预览中即可看到效果
setTimeout(() => {
void beautyPanelRef.value?.applyIfEnabled()
}, 1500)
},
// 须先 endCallDeleteCloudRecording),再 await 本地上传;否则上传 WebM 耗时数分钟会拖住云端停录,控制台房间长期「尚未结束」
afterCalling: () => {
+7 -3
View File
@@ -17,14 +17,18 @@ defineProps({
<style scoped lang="scss">
.footer-btns {
height: 60px;
height: 64px;
&__content {
bottom: 0;
height: 60px;
height: 64px;
right: 0;
left: 0;
z-index: 99;
@apply flex justify-center items-center shadow bg-body;
padding: 0 20px;
border-top: 1px solid var(--el-border-color-lighter);
box-shadow: 0 -4px 16px rgba(15, 23, 42, 0.04);
@apply flex justify-center items-center gap-3 bg-body;
}
}
</style>
+228
View File
@@ -0,0 +1,228 @@
<template>
<div class="page-stage admin-page" :class="{ 'page-stage--flat': hideChrome }">
<header v-if="showHeader" class="page-stage__hero">
<div class="page-stage__hero-glow" aria-hidden="true"></div>
<div class="page-stage__hero-inner">
<div class="page-stage__intro">
<h1 class="page-stage__title">{{ pageTitle }}</h1>
<p v-if="pageDesc" class="page-stage__desc">{{ pageDesc }}</p>
</div>
<div v-if="$slots.actions" class="page-stage__actions">
<slot name="actions" />
</div>
</div>
</header>
<div class="page-stage__body">
<slot />
</div>
</div>
</template>
<script setup lang="ts">
import { PageEnum } from '@/enums/pageEnum'
const route = useRoute()
const hideChrome = computed(() => {
if (route.meta?.hidePageHeader) return true
if (route.path === PageEnum.INDEX || route.path === `${PageEnum.INDEX}/`) return true
const title = (route.meta?.title as string) || ''
return title.includes('工作台')
})
const pageTitle = computed(() => {
const matched = route.matched.filter((item) => item.meta?.title)
const last = matched[matched.length - 1]
return (last?.meta?.title as string) || ''
})
const pageDesc = computed(() => (route.meta?.pageDesc as string) || '')
const showHeader = computed(() => !hideChrome.value && Boolean(pageTitle.value))
</script>
<style lang="scss" scoped>
.page-stage {
min-height: 100%;
}
.page-stage__hero {
position: relative;
margin-bottom: 20px;
padding: 22px 24px;
border-radius: var(--admin-radius-xl);
border: 1px solid var(--el-border-color-lighter);
background: var(--admin-surface-glass);
backdrop-filter: blur(16px) saturate(140%);
box-shadow: var(--el-box-shadow-light);
overflow: hidden;
}
.page-stage__hero-glow {
position: absolute;
inset: 0;
background:
linear-gradient(120deg, rgba(6, 182, 212, 0.12) 0%, transparent 42%),
linear-gradient(300deg, rgba(16, 185, 129, 0.1) 0%, transparent 38%);
pointer-events: none;
}
.page-stage__hero-inner {
position: relative;
display: flex;
flex-wrap: wrap;
align-items: flex-start;
justify-content: space-between;
gap: 12px 20px;
}
.page-stage__title {
margin: 0;
font-size: 24px;
font-weight: 800;
line-height: 1.2;
letter-spacing: -0.03em;
color: var(--el-text-color-primary);
background: var(--admin-brand-gradient);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.page-stage__desc {
margin: 8px 0 0;
font-size: 14px;
line-height: 1.5;
color: var(--el-text-color-secondary);
max-width: 62ch;
}
.page-stage__actions {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
}
.page-stage__body {
min-width: 0;
}
.page-stage--flat .page-stage__hero {
display: none;
}
</style>
+1 -35
View File
@@ -55,20 +55,6 @@
<!-- 窗口底部操作栏 -->
<div class="window-footer">
<!-- 美颜设置本地 AI 管线免费仅对开启了美颜功能的医生显示 -->
<CallBeautyPanel
v-if="isInitialized && !callRejected && beautyAllowed"
ref="beautyPanelRef"
:get-engine="getCallEngine"
:popper-z-index="2100"
>
<template #reference>
<el-button size="small">
<el-icon><MagicStick /></el-icon>
<span style="margin-left: 2px">美颜</span>
</el-button>
</template>
</CallBeautyPanel>
<el-button
@click="handleClose"
:disabled="calling && !callRejected"
@@ -106,13 +92,11 @@
<script setup lang="ts">
import { ref, onUnmounted, nextTick, computed } from 'vue'
import { VideoCamera, CircleClose, Phone, MagicStick } from '@element-plus/icons-vue'
import { VideoCamera, CircleClose, Phone } from '@element-plus/icons-vue'
import { getCallSignature, endCall } from '@/api/tcm'
import feedback from '@/utils/feedback'
import { formatTUICallUserError } from '@/utils/tuicall-error'
import { TUICallKitAPI, TUICallKit, TUICallType, STATUS } from '@trtc/calls-uikit-vue'
import CallBeautyPanel from '@/components/call-beauty-panel/index.vue'
import useUserStore from '@/stores/modules/user'
interface CallInfo {
diagnosisId: number
@@ -140,18 +124,6 @@ const callInfo = ref<CallInfo>({
isGroup: false
})
/** 美颜面板(本地 AI 管线 + 引擎兜底,非付费特效 SDK) */
const beautyPanelRef = ref<InstanceType<typeof CallBeautyPanel> | null>(null)
/** 医生编辑页「美颜功能」开关:关闭时不显示美颜入口 */
const beautyAllowed = computed(() => useUserStore().userInfo?.enable_beauty !== 0)
const getCallEngine = () => {
try {
return TUICallKitAPI.getTUICallEngineInstance()
} catch {
return null
}
}
// 窗口位置和大小
const windowRef = ref<HTMLElement>()
const windowPosition = ref({ x: 100, y: 100 })
@@ -261,8 +233,6 @@ const handleStatusChange = ({ oldStatus, newStatus }: any) => {
shouldAutoClose.value = false // 通话接通时,不自动关闭
statusText.value = '通话中...'
feedback.msgSuccess('通话已接通')
// 接通后重新下发已保存的美颜设置(摄像头此时已开启,插件可正常启动)
void beautyPanelRef.value?.applyIfEnabled()
// 如果是多人通话且还有其他用户需要邀请
if (callInfo.value.isGroup && callInfo.value.userIds && callInfo.value.userIds.length > 1) {
@@ -277,10 +247,6 @@ const handleStatusChange = ({ oldStatus, newStatus }: any) => {
calling.value = true
shouldAutoClose.value = false
statusText.value = '等待对方接听...'
// 呼叫阶段本地摄像头预览已开启,延迟下发美颜让医生在预览中即可看到效果
setTimeout(() => {
void beautyPanelRef.value?.applyIfEnabled()
}, 1500)
}
// 通话结束,回到空闲状态
+20 -20
View File
@@ -1,28 +1,28 @@
const defaultSetting = {
showCrumb: true, // 是否显示面包屑
showLogo: false, // 是否显示logo
isUniqueOpened: true, //只展开一个一级菜单
sideWidth: 183, //侧边栏宽度
sideTheme: 'dark', //侧边栏主题
sideDarkColor: '#1d2124', //侧边栏深色主题颜色
openMultipleTabs: true, // 是否开启多标签tab栏
theme: '#4A5DFF', //主题色
successTheme: '#67c23a', //成功主题色
warningTheme: '#e6a23c', //警告主题色
dangerTheme: '#f56c6c', //危险主题色
errorTheme: '#f56c6c', //错误主题色
infoTheme: '#909399' //信息主题色
showCrumb: false,
showLogo: true,
isUniqueOpened: true,
sideWidth: 248,
sideTheme: 'dark',
sideDarkColor: '#060d18',
openMultipleTabs: true,
theme: '#06b6d4',
successTheme: '#10b981',
warningTheme: '#f59e0b',
dangerTheme: '#ef4444',
errorTheme: '#ef4444',
infoTheme: '#6366f1'
}
/** 本地 setting 缓存结构版本。提升后仅对低于该版本的老缓存执行 SETTING_SCHEMA_MIGRATIONS */
export const SETTING_SCHEMA_VERSION = 1
export const SETTING_SCHEMA_VERSION = 6
/**
* 按版本写入 defaultSetting 中的键(老用户 localStorage 会长期盖住 config 默认值)。
* 以后若要再推一批新默认值:把 SETTING_SCHEMA_VERSION +1,并为本版本追加一条迁移键列表。
*/
export const SETTING_SCHEMA_MIGRATIONS: Record<number, (keyof typeof defaultSetting)[]> = {
1: ['sideTheme', 'sideDarkColor']
1: ['sideTheme', 'sideDarkColor'],
2: ['theme', 'successTheme', 'warningTheme', 'dangerTheme', 'errorTheme', 'infoTheme', 'sideDarkColor', 'sideWidth', 'showLogo'],
3: ['theme', 'sideDarkColor', 'sideWidth', 'showCrumb', 'showLogo'],
4: ['theme', 'successTheme', 'warningTheme', 'dangerTheme', 'errorTheme', 'infoTheme', 'sideDarkColor'],
5: ['theme', 'successTheme', 'warningTheme', 'dangerTheme', 'errorTheme', 'infoTheme', 'sideDarkColor'],
6: ['theme', 'successTheme', 'warningTheme', 'dangerTheme', 'errorTheme', 'infoTheme', 'sideDarkColor', 'sideWidth']
}
export default defaultSetting
@@ -1,5 +1,5 @@
<template>
<el-breadcrumb class="app-breadcrumb">
<el-breadcrumb class="app-breadcrumb" separator="/">
<el-breadcrumb-item v-for="item in breadcrumbs" :key="item.path">
{{ item.meta.title }}
</el-breadcrumb-item>
@@ -21,22 +21,24 @@ useWatchRoute((route) => {
})
</script>
<style scoped lang="scss">
.app-breadcrumb {
:deep(.el-breadcrumb__item) {
.el-breadcrumb__inner {
color: #303133;
color: var(--el-text-color-secondary);
font-weight: 500;
font-size: var(--el-font-size-small);
}
&:last-child .el-breadcrumb__inner {
color: #303133;
color: var(--el-text-color-primary);
font-weight: 600;
}
}
:deep(.el-breadcrumb__separator) {
color: #606266;
color: var(--el-text-color-placeholder);
font-weight: 400;
}
}
</style>
@@ -1,7 +1,7 @@
<template>
<header class="header">
<div class="navbar">
<div class="flex-1 flex">
<div class="flex-1 flex items-center gap-1 min-w-0">
<div class="navbar-item">
<el-tooltip
class="box-item"
@@ -17,11 +17,14 @@
<refresh />
</el-tooltip>
</div>
<div class="flex items-center px-2" v-if="!isMobile && settingStore.showCrumb">
<div
class="hidden md:flex items-center min-w-0 px-2"
v-if="settingStore.showCrumb && breadcrumbs.length"
>
<breadcrumb />
</div>
</div>
<div class="flex">
<div class="flex items-center gap-1">
<div class="navbar-item" v-if="!isMobile">
<el-tooltip
class="box-item"
@@ -36,12 +39,7 @@
<user-drop-down />
</div>
<div class="navbar-item">
<el-tooltip
class="box-item"
effect="dark"
content="主题设置"
placement="bottom"
>
<el-tooltip class="box-item" effect="dark" content="主题设置" placement="bottom">
<setting />
</el-tooltip>
</div>
@@ -52,8 +50,10 @@
</template>
<script setup lang="ts">
import type { RouteLocationMatched } from 'vue-router'
import { useFullscreen } from '@vueuse/core'
import { useWatchRoute } from '@/hooks/useWatchRoute'
import useAppStore from '@/stores/modules/app'
import useSettingStore from '@/stores/modules/setting'
@@ -70,14 +70,20 @@ const isMobile = computed(() => appStore.isMobile)
const isCollapsed = computed(() => appStore.isCollapsed)
const settingStore = useSettingStore()
const { isFullscreen } = useFullscreen()
const breadcrumbs = ref<RouteLocationMatched[]>([])
useWatchRoute((route) => {
breadcrumbs.value = route.matched.filter((item) => item.meta && item.meta.title)
})
</script>
<style lang="scss">
.navbar {
height: var(--navbar-height);
@apply flex px-2 bg-body;
@apply flex px-3 bg-body;
.navbar-item {
@apply h-full flex justify-center items-center hover:bg-page;
@apply h-full flex justify-center items-center;
}
}
</style>
@@ -1,6 +1,6 @@
<template>
<div class="app-tabs pl-4 flex bg-body">
<div class="flex-1 min-w-0">
<div class="app-tabs flex bg-body">
<div class="flex-1 min-w-0 pl-3">
<el-tabs
:model-value="currentTab"
:closable="tabsLists.length > 1"
@@ -13,7 +13,7 @@
</el-tabs>
</div>
<el-dropdown @command="handleCommand">
<span class="flex items-center px-3">
<span class="tabs-more-btn flex items-center px-3">
<icon :size="16" name="el-icon-arrow-down" />
</span>
<template #dropdown>
@@ -60,61 +60,84 @@ const handleCommand = (command: any) => {
</script>
<style lang="scss" scoped>
.app-tabs {
@apply border-t border-br;
height: var(--tabs-height);
border-top: 1px solid var(--admin-header-border);
background: var(--admin-surface-glass);
backdrop-filter: blur(12px);
.tabs-more-btn {
height: var(--tabs-height);
color: var(--el-text-color-secondary);
cursor: pointer;
transition: color 0.2s ease, background-color 0.2s ease;
&:hover {
color: var(--admin-brand-primary-dark);
background: var(--admin-brand-gradient-soft);
}
}
:deep(.el-tabs) {
height: 40px;
height: var(--tabs-height);
.el-tabs {
&__header {
margin-bottom: 0;
}
&__content {
display: none;
}
&__nav-next,
&__nav-prev {
@apply text-xl;
@apply text-lg;
}
&__nav-wrap::after {
height: 0;
}
&__item {
font-weight: normal;
padding: 0 15px !important;
font-weight: 600;
font-size: var(--el-font-size-small);
padding: 0 16px !important;
height: calc(var(--tabs-height) - 8px);
margin-top: 4px;
border-radius: var(--admin-radius-md) var(--admin-radius-md) 0 0;
box-sizing: border-box;
color: var(--el-text-color-secondary);
transition: color 0.2s ease, background-color 0.2s ease, box-shadow 0.2s ease;
&:hover {
color: var(--admin-brand-primary-dark);
}
&.is-active {
color: var(--el-text-color-primary);
background-color: var(--el-color-primary-light-9);
&::before {
content: '';
display: inline-block;
width: 6px;
height: 6px;
background-color: var(--el-color-primary);
margin-right: 6px;
border-radius: 50%;
vertical-align: 2px;
}
color: var(--admin-brand-primary-dark);
background: var(--el-bg-color);
border: 1px solid var(--el-border-color-lighter);
border-bottom-color: transparent;
box-shadow: var(--el-box-shadow-lighter);
&::before,
&::after {
position: absolute;
content: '';
display: block;
top: 0;
height: 2px;
left: 0;
width: 100%;
background-color: var(--el-color-primary);
display: none;
}
}
.is-icon-close {
color: var(--el-text-color-regular);
color: var(--el-text-color-placeholder);
vertical-align: -2px;
border-radius: var(--admin-radius-sm);
&:hover {
color: var(--color-white);
background-color: var(--el-color-danger);
}
}
}
&__active-bar {
display: none;
}
@@ -1,9 +1,11 @@
<template>
<el-dropdown class="px-2" @command="handleCommand">
<div class="flex items-center">
<el-avatar :size="34" :src="userInfo.avatar" />
<div class="ml-3 mr-1">{{ userInfo.name }}</div>
<icon name="el-icon-ArrowDown" />
<el-dropdown class="user-dropdown px-1" @command="handleCommand">
<div class="user-trigger flex items-center gap-2.5 px-2 py-1 rounded-md cursor-pointer">
<el-avatar :size="32" :src="userInfo.avatar" />
<span class="user-name max-w-[120px] truncate text-sm font-medium text-tx-primary">{{
userInfo.name
}}</span>
<icon name="el-icon-ArrowDown" :size="14" />
</div>
<template #dropdown>
@@ -41,3 +43,13 @@ const handleCommand = async (command: string) => {
}
}
</script>
<style scoped lang="scss">
.user-trigger {
transition: background-color 0.2s ease;
&:hover {
background: var(--el-fill-color-light);
}
}
</style>
+37 -8
View File
@@ -1,27 +1,56 @@
<template>
<main class="main-wrap h-full bg-page">
<main class="main-wrap h-full">
<el-scrollbar>
<div class="px-2 py-4">
<router-view v-if="isRouteShow" v-slot="{ Component, route }">
<keep-alive :include="includeList" :max="20">
<component :is="Component" :key="route.fullPath" />
</keep-alive>
</router-view>
<div class="main-stage">
<page-shell v-if="isRouteShow">
<router-view v-slot="{ Component, route }">
<keep-alive :include="includeList" :max="20">
<component :is="Component" :key="route.fullPath" />
</keep-alive>
</router-view>
</page-shell>
</div>
</el-scrollbar>
</main>
</template>
<script setup lang="ts">
import PageShell from '@/components/page-shell/index.vue'
import useAppStore from '@/stores/modules/app'
import useTabsStore from '@/stores/modules/multipleTabs'
import useSettingStore from '@/stores/modules/setting'
const appStore = useAppStore()
const tabsStore = useTabsStore()
const settingStore = useSettingStore()
const isRouteShow = computed(() => appStore.isRouteShow)
const includeList = computed(() => (settingStore.openMultipleTabs ? tabsStore.getCacheTabList : []))
</script>
<style></style>
@@ -100,7 +100,7 @@ import theme_light from '@/assets/images/theme_white.png'
import useSettingStore from '@/stores/modules/setting'
const settingStore = useSettingStore()
const predefineColors = ref(['#409EFF', '#28C76F', '#EA5455', '#FF9F43', '#01CFE8', '#4A5DFF'])
const predefineColors = ref(['#06b6d4', '#10b981', '#0891b2', '#6366f1', '#f59e0b', '#ef4444', '#64748b'])
const sideThemeList = [
{
type: 'dark',
@@ -68,32 +68,42 @@ const themeClass = computed(() => `theme-${props.theme}`)
.el-menu {
:deep(.el-menu-item) {
&.is-active {
@apply bg-primary border-primary;
@apply bg-primary;
box-shadow: inset 3px 0 0 var(--el-color-primary-light-3);
}
}
}
:deep(.el-menu--collapse) {
.el-sub-menu.is-active .el-sub-menu__title {
@apply bg-primary #{!important};
}
}
}
&.theme-light {
:deep(.el-menu) {
.el-menu-item {
border-color: transparent;
&.is-active {
@apply bg-primary-light-9 border-r-2 border-primary;
@apply bg-primary-light-9;
color: var(--el-color-primary);
font-weight: 600;
box-shadow: inset 3px 0 0 var(--el-color-primary);
}
}
.el-menu-item:hover,
.el-sub-menu__title:hover {
color: var(--el-color-primary);
}
}
}
.el-menu {
border-right: none;
&:not(.el-menu--collapse) {
width: var(--aside-width);
}
@@ -1,5 +1,15 @@
<template>
<div class="side" :style="sideStyle">
<div v-if="showBrandStrip" class="sidebar-brand">
<div class="sidebar-brand__mark">ZY</div>
<overflow-tooltip
class="sidebar-brand__name"
:content="config.web_name"
:teleported="true"
placement="bottom"
overflo-type="unset"
/>
</div>
<side-logo v-if="settingStore.showLogo" :show-title="!isCollapsed" :theme="sideTheme" />
<side-menu
:routes="routes"
@@ -25,17 +35,19 @@ const appStore = useAppStore()
const isCollapsed = computed(() => {
if (appStore.isMobile) {
return false
} else {
return appStore.isCollapsed
}
return appStore.isCollapsed
})
const settingStore = useSettingStore()
const sideTheme = computed(() => settingStore.sideTheme)
const userStore = useUserStore()
const config = computed(() => appStore.config)
const routes = computed(() => userStore.routes)
const showBrandStrip = computed(() => !settingStore.showLogo && !isCollapsed.value)
const sideStyle = computed(() => {
return sideTheme.value == 'dark'
? {
@@ -43,6 +55,7 @@ const sideStyle = computed(() => {
}
: ''
})
const menuProp = computed(() => {
return {
backgroundColor: sideTheme.value == 'dark' ? settingStore.sideDarkColor : '',
@@ -50,6 +63,7 @@ const menuProp = computed(() => {
activeTextColor: sideTheme.value == 'dark' ? 'var(--el-color-white)' : ''
}
})
const handleSelect = () => {
if (appStore.isMobile) {
appStore.toggleCollapsed(true)
@@ -61,7 +75,8 @@ const handleSelect = () => {
.side {
position: relative;
z-index: 999;
@apply border-r border-br-light h-full flex flex-col;
background-color: var(--side-dark-color, var(--el-bg-color));
@apply h-full flex flex-col;
border-right: 1px solid var(--admin-sidebar-border);
background-color: var(--side-dark-color, var(--sidebar-dark-bg));
}
</style>
+298
View File
@@ -0,0 +1,298 @@
/**
* Admin 页面架构 v2 - 大改版面板系统
*/
.admin-page .page-stage__body {
display: flex;
flex-direction: column;
gap: 16px;
}
/* ---------- FilterPanel ---------- */
.admin-filter-panel {
position: relative;
border-radius: var(--admin-radius-xl);
border: 1px solid var(--el-border-color-lighter);
background: var(--admin-surface-glass);
backdrop-filter: blur(12px);
box-shadow: var(--el-box-shadow-lighter);
overflow: hidden;
&::before {
content: '';
position: absolute;
inset: 0 0 auto;
height: 3px;
background: var(--admin-brand-gradient);
}
}
.admin-filter-panel__head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 14px 22px 0;
}
.admin-filter-panel__label {
font-size: 14px;
font-weight: 700;
color: var(--el-text-color-primary);
}
.admin-filter-panel__toggle {
padding: 0;
font-size: 13px;
font-weight: 600;
}
.admin-filter-panel__body {
padding: 14px 22px 12px;
}
.admin-filter-panel.is-collapsed .admin-filter-panel__body {
display: none;
}
.admin-filter-panel .el-form--inline {
margin-bottom: 0;
.el-form-item {
margin-bottom: 12px;
margin-right: 18px;
}
.el-form-item__label {
font-weight: 600;
color: var(--el-text-color-regular);
}
}
/* ---------- DataPanel ---------- */
.admin-data-panel {
border-radius: var(--admin-radius-xl);
border: 1px solid var(--el-border-color-lighter);
background: var(--admin-surface-elevated);
box-shadow: var(--el-box-shadow);
overflow: hidden;
}
.admin-data-panel__toolbar {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 10px;
padding: 16px 22px;
border-bottom: 1px solid var(--el-border-color-extra-light);
background: var(--admin-brand-gradient-soft);
}
.admin-data-panel__body {
padding: 0 22px 18px;
}
.admin-data-panel__toolbar + .admin-data-panel__body {
padding-top: 16px;
}
.admin-data-panel__footer {
display: flex;
justify-content: flex-end;
padding: 0 22px 18px;
}
/* ---------- FormPanel ---------- */
.admin-form-panel {
border-radius: var(--admin-radius-xl);
border: 1px solid var(--el-border-color-lighter);
background: var(--admin-surface-elevated);
box-shadow: var(--el-box-shadow-light);
overflow: hidden;
}
.admin-form-panel__header {
padding: 16px 22px;
border-bottom: 1px solid var(--el-border-color-extra-light);
background: var(--admin-brand-gradient-soft);
}
.admin-form-panel__title {
margin: 0;
font-size: 17px;
font-weight: 700;
color: var(--el-text-color-primary);
}
.admin-form-panel__body {
padding: 22px;
}
.admin-form-panel__footer {
display: flex;
justify-content: center;
gap: 12px;
padding: 16px 22px 22px;
border-top: 1px solid var(--el-border-color-extra-light);
background: var(--el-fill-color-lighter);
}
/* ---------- StatGrid ---------- */
.admin-stat-grid {
display: grid;
gap: 14px;
}
.admin-stat-grid--cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.admin-stat-grid--cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.admin-stat-grid--cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.admin-stat-grid--cols-5 { grid-template-columns: repeat(5, minmax(0, 1fr)); }
.admin-stat-item {
position: relative;
padding: 18px 20px;
border-radius: var(--admin-radius-lg);
border: 1px solid var(--el-border-color-lighter);
background: var(--admin-surface-elevated);
box-shadow: var(--el-box-shadow-lighter);
overflow: hidden;
transition: transform 0.2s ease, box-shadow 0.2s ease;
&::before {
content: '';
position: absolute;
inset: 0 auto 0 0;
width: 4px;
background: var(--el-border-color);
}
&:hover {
transform: translateY(-2px);
box-shadow: var(--el-box-shadow-light);
}
}
.admin-stat-item--primary {
&::before { background: var(--admin-brand-gradient); }
border-color: rgba(6, 182, 212, 0.2);
background: linear-gradient(145deg, rgba(6, 182, 212, 0.08), rgba(16, 185, 129, 0.05));
}
.admin-stat-item--success {
&::before { background: #10b981; }
border-color: rgba(16, 185, 129, 0.22);
}
.admin-stat-item--warning {
&::before { background: #f59e0b; }
border-color: rgba(245, 158, 11, 0.22);
}
.admin-stat-item__label {
font-size: 13px;
font-weight: 500;
color: var(--el-text-color-secondary);
}
.admin-stat-item__value {
margin-top: 10px;
font-size: 30px;
font-weight: 800;
line-height: 1.05;
letter-spacing: -0.03em;
color: var(--el-text-color-primary);
font-variant-numeric: tabular-nums;
}
.admin-stat-item__value.is-money {
font-size: 26px;
}
.admin-stat-item__hint {
margin: 8px 0 0;
font-size: 12px;
color: var(--el-text-color-placeholder);
}
/* ---------- Tables ---------- */
.admin-data-panel .el-table,
.admin-page .el-card .el-table {
--el-table-border-color: transparent;
border-radius: var(--admin-radius-md);
thead th.el-table__cell {
position: sticky;
top: 0;
z-index: 2;
background: var(--table-header-bg-color) !important;
border-bottom: 1px solid var(--el-border-color-lighter);
font-weight: 700;
font-size: var(--el-font-size-small);
}
td.el-table__cell {
border-bottom: 1px solid var(--el-border-color-extra-light);
}
.el-table__row:hover > td.el-table__cell {
background-color: rgba(6, 182, 212, 0.04) !important;
}
.el-table__row.current-row > td.el-table__cell {
background-color: rgba(6, 182, 212, 0.08) !important;
}
}
/* ---------- Legacy cards ---------- */
.admin-page .page-stage__body {
> div > .el-card:first-child:has(.el-form),
> .el-card:first-child:has(.el-form) {
border-radius: var(--admin-radius-xl);
border: 1px solid var(--el-border-color-lighter);
box-shadow: var(--el-box-shadow-lighter);
}
> div > .el-card + .el-card,
> .el-card + .el-card {
border-radius: var(--admin-radius-xl);
box-shadow: var(--el-box-shadow);
}
}
.workbench-page {
margin: 0;
}
@media (max-width: 1200px) {
.admin-stat-grid--cols-4,
.admin-stat-grid--cols-5 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 768px) {
.admin-filter-panel__body,
.admin-data-panel__body,
.admin-data-panel__toolbar,
.admin-data-panel__footer {
padding-left: 16px;
padding-right: 16px;
}
.admin-stat-grid--cols-2,
.admin-stat-grid--cols-3,
.admin-stat-grid--cols-4,
.admin-stat-grid--cols-5 {
grid-template-columns: 1fr;
}
.admin-stat-item:hover {
transform: none;
}
}
@media (prefers-reduced-motion: reduce) {
.admin-stat-item {
transition: none;
}
}
+190
View File
@@ -0,0 +1,190 @@
/* Admin Shell v2 - 互联网医院工作台大改版 */
.layout-default {
background: var(--sidebar-dark-bg);
}
/* ---------- Sidebar ---------- */
.app-aside .side {
position: relative;
&::before {
content: '';
position: absolute;
inset: 0 auto 0 0;
width: var(--sidebar-rail-width);
background: var(--admin-brand-gradient);
z-index: 2;
pointer-events: none;
}
&::after {
content: '';
position: absolute;
inset: 0;
background:
radial-gradient(circle at 0% 0%, var(--admin-brand-mesh-a), transparent 45%),
radial-gradient(circle at 100% 100%, var(--admin-brand-mesh-b), transparent 40%);
pointer-events: none;
}
}
.sidebar-brand {
position: relative;
z-index: 1;
display: flex;
align-items: center;
gap: 12px;
min-height: calc(var(--navbar-height) + 4px);
padding: 0 18px 0 20px;
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
}
.sidebar-brand__mark {
width: 38px;
height: 38px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 11px;
font-weight: 800;
letter-spacing: 0.1em;
color: #ffffff;
background: var(--admin-brand-gradient);
box-shadow: var(--admin-brand-glow);
flex-shrink: 0;
}
.sidebar-brand__name {
font-size: 15px;
font-weight: 700;
color: rgba(255, 255, 255, 0.95);
line-height: 1.25;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.menu.theme-dark .el-menu {
--el-menu-bg-color: transparent;
--el-menu-hover-bg-color: var(--sidebar-dark-hover);
--el-menu-active-color: #ffffff;
--el-menu-text-color: rgba(255, 255, 255, 0.62);
padding: 12px 10px 16px;
position: relative;
z-index: 1;
}
.menu.theme-dark .el-menu .el-menu-item,
.menu.theme-dark .el-menu .el-sub-menu__title {
margin: 3px 8px;
border-radius: var(--admin-radius-md);
transition: background-color 0.2s ease, color 0.2s ease, box-shadow 0.2s ease, transform 0.12s ease;
}
.menu.theme-dark .el-menu .el-menu-item.is-active {
background: var(--sidebar-dark-active) !important;
color: #ffffff;
font-weight: 600;
box-shadow:
inset 0 0 0 1px rgba(6, 182, 212, 0.28),
0 8px 24px rgba(6, 182, 212, 0.15);
}
.menu.theme-dark .el-menu .el-menu-item:hover,
.menu.theme-dark .el-menu .el-sub-menu__title:hover {
background: var(--sidebar-dark-hover);
color: #ffffff;
}
.menu.theme-light .el-menu .el-menu-item.is-active {
font-weight: 600;
color: var(--admin-brand-primary-dark);
background: var(--admin-brand-gradient-soft);
box-shadow: inset 3px 0 0 var(--admin-brand-primary);
}
/* ---------- Header ---------- */
.app-header {
position: relative;
z-index: 20;
background: var(--admin-surface-glass);
backdrop-filter: blur(20px) saturate(160%);
border-bottom: 1px solid var(--admin-header-border);
&::after {
content: '';
position: absolute;
inset: auto 0 0;
height: 2px;
background: var(--admin-brand-gradient);
opacity: 0.75;
pointer-events: none;
}
}
:root.dark .app-header {
background: rgba(15, 23, 42, 0.88);
}
.navbar-tool {
width: 38px;
height: 38px;
border-radius: var(--admin-radius-md);
display: inline-flex;
align-items: center;
justify-content: center;
color: var(--el-text-color-regular);
cursor: pointer;
transition: background-color 0.2s ease, color 0.2s ease, transform 0.12s ease, box-shadow 0.2s ease;
&:hover {
background: var(--admin-brand-gradient-soft);
color: var(--admin-brand-primary-dark);
box-shadow: var(--el-box-shadow-lighter);
}
&:active {
transform: scale(0.96);
}
}
/* ---------- Main stage (去掉双层白盒) ---------- */
.main-wrap {
position: relative;
background:
radial-gradient(circle at 0% 0%, var(--admin-brand-mesh-a), transparent 34%),
radial-gradient(circle at 100% 0%, var(--admin-brand-mesh-c), transparent 30%),
radial-gradient(circle at 50% 100%, var(--admin-brand-mesh-b), transparent 38%),
var(--el-bg-color-page);
}
.main-stage {
max-width: var(--content-max-width);
margin: 0 auto;
padding: var(--stage-padding-y) var(--stage-padding-x);
min-height: calc(100vh - var(--navbar-height) - var(--tabs-height, 0px));
}
@media (max-width: 768px) {
:root {
--stage-padding-x: 14px;
--stage-padding-y: 14px;
}
}
@media (prefers-reduced-motion: reduce) {
.navbar-tool,
.menu.theme-dark .el-menu .el-menu-item,
.menu.theme-dark .el-menu .el-sub-menu__title {
transition: none;
}
}
@media (prefers-reduced-transparency: reduce) {
.app-header {
backdrop-filter: none;
background: var(--el-bg-color);
}
}
+46 -31
View File
@@ -1,43 +1,58 @@
:root.dark {
color-scheme: dark;
--table-header-bg-color: var(--el-bg-color);
--el-bg-color-page: #0a0a0a;
--el-bg-color: #1d2124;
--el-bg-color-overlay: #1d1e1f;
--el-text-color-primary: #e5eaf3;
--el-text-color-regular: #cfd3dc;
--el-text-color-secondary: #a3a6ad;
--el-text-color-placeholder: #8d9095;
--el-text-color-disabled: #6c6e72;
--el-border-color-darker: #636466;
--el-border-color-dark: #58585b;
--el-border-color: #4c4d4f;
--el-border-color-light: #414243;
--el-border-color-lighter: #363637;
--el-border-color-extra-light: #2b2b2c;
--el-fill-color-darker: #424243;
--el-fill-color-dark: #39393a;
--el-fill-color: #303030;
--el-fill-color-light: #262727;
--el-fill-color-lighter: #1d1d1d;
--el-fill-color-extra-light: #191919;
--table-header-bg-color: rgba(6, 182, 212, 0.12);
--sidebar-dark-bg: #030712;
--sidebar-dark-hover: rgba(255, 255, 255, 0.05);
--sidebar-dark-active: rgba(6, 182, 212, 0.2);
--admin-surface-elevated: #111827;
--admin-surface-muted: rgba(17, 24, 39, 0.88);
--admin-surface-glass: rgba(17, 24, 39, 0.86);
--el-bg-color-page: #030712;
--el-bg-color: #0f172a;
--el-bg-color-overlay: #1e293b;
--el-text-color-primary: #f1f5f9;
--el-text-color-regular: #cbd5e1;
--el-text-color-secondary: #94a3b8;
--el-text-color-placeholder: #64748b;
--el-text-color-disabled: #475569;
--el-border-color-darker: #64748b;
--el-border-color-dark: #475569;
--el-border-color: rgba(6, 182, 212, 0.18);
--el-border-color-light: rgba(6, 182, 212, 0.12);
--el-border-color-lighter: rgba(255, 255, 255, 0.06);
--el-border-color-extra-light: rgba(255, 255, 255, 0.04);
--el-fill-color-darker: #334155;
--el-fill-color-dark: #1e293b;
--el-fill-color: #1e293b;
--el-fill-color-light: #172033;
--el-fill-color-lighter: #131c2e;
--el-fill-color-extra-light: #0f172a;
--el-fill-color-blank: var(--el-bg-color);
--el-mask-color: rgba(0, 0, 0, 0.8);
--el-mask-color-extra-light: rgba(0, 0, 0, 0.3);
--el-box-shadow: 0px 12px 32px 4px rgba(0, 0, 0, 0.36), 0px 8px 20px rgba(0, 0, 0, 0.72);
--el-box-shadow-light: 0px 0px 12px rgba(0, 0, 0, 0.72);
--el-box-shadow-lighter: 0px 0px 6px rgba(0, 0, 0, 0.72);
--el-box-shadow-dark: 0px 16px 48px 16px rgba(0, 0, 0, 0.72), 0px 12px 32px #000000,
0px 8px 16px -8px #000000 !important;
/* wangeditor主题 */
--el-mask-color: rgba(2, 6, 23, 0.78);
--el-mask-color-extra-light: rgba(2, 6, 23, 0.42);
--el-box-shadow: 0 4px 24px rgba(0, 0, 0, 0.28);
--el-box-shadow-light: 0 2px 16px rgba(0, 0, 0, 0.22);
--el-box-shadow-lighter: 0 1px 4px rgba(0, 0, 0, 0.16);
--el-box-shadow-dark: 0 16px 48px rgba(0, 0, 0, 0.36);
--admin-header-border: rgba(6, 182, 212, 0.14);
--admin-sidebar-border: rgba(255, 255, 255, 0.04);
--admin-brand-mesh-a: rgba(6, 182, 212, 0.12);
--admin-brand-mesh-b: rgba(16, 185, 129, 0.08);
--admin-brand-mesh-c: rgba(99, 102, 241, 0.06);
--w-e-textarea-bg-color: var(--el-bg-color);
--w-e-textarea-color: var(--el-text-color-primary);
--w-e-textarea-border-color: var(--el-border-color);
--w-e-textarea-slight-border-color: var(--el-border-color-light);
--w-e-textarea-slight-color: var(--el-border-color);
--w-e-textarea-slight-bg-color: var(--el-bg-color-page);
/* --w-e-textarea-selected-border-color: #b4d5ff;
--w-e-textarea-handler-bg-color: #4290f7; */
--w-e-toolbar-color: var(--el-text-color-primary);
--w-e-toolbar-bg-color: var(--el-bg-color);
--w-e-toolbar-active-color: var(--el-text-color-primary);
+246 -26
View File
@@ -1,14 +1,20 @@
:root {
// 确保消息提示在最上层
/* Messages & notifications */
.el-message {
z-index: 9999 !important;
border-radius: var(--admin-radius-md);
border: 1px solid var(--el-border-color-lighter);
box-shadow: var(--el-box-shadow);
}
.el-notification {
z-index: 9999 !important;
border-radius: var(--admin-radius-lg);
border: 1px solid var(--el-border-color-lighter);
box-shadow: var(--el-box-shadow);
}
// 弹窗居中
/* Dialog */
.el-overlay-dialog {
display: flex;
justify-content: center;
@@ -19,11 +25,14 @@
.el-dialog {
--el-dialog-content-font-size: var(--el-font-size-base);
--el-dialog-margin-top: 50px;
max-width: calc(100vw - 30px);
--el-dialog-border-radius: var(--admin-radius-lg);
max-width: calc(100vw - 32px);
flex: none;
display: flex;
flex-direction: column;
border-radius: 5px;
border-radius: var(--admin-radius-lg);
border: 1px solid var(--el-border-color-lighter);
box-shadow: var(--el-box-shadow-dark);
&.body-padding .el-dialog__body {
padding: 0;
@@ -31,50 +40,158 @@
.el-dialog__body {
flex: 1;
padding: 15px 20px;
padding: 16px 20px 20px;
}
.el-dialog__header {
font-size: var(--el-font-size-large);
font-weight: 600;
padding: 16px 20px 12px;
margin-right: 0;
border-bottom: 1px solid var(--el-border-color-extra-light);
}
.el-dialog__footer {
padding: 12px 20px 16px;
border-top: 1px solid var(--el-border-color-extra-light);
}
}
}
/* Cards - applies to all list pages */
.el-card {
--el-card-border-radius: var(--admin-radius-lg);
border: 1px solid var(--el-border-color-lighter);
box-shadow: none;
background: var(--admin-surface-elevated);
transition: border-color 0.2s ease, box-shadow 0.2s ease;
&.is-always-shadow,
&.is-hover-shadow:hover {
box-shadow: var(--el-box-shadow-light);
}
.el-card__header {
padding: 14px 18px;
border-bottom: 1px solid var(--el-border-color-extra-light);
font-weight: 600;
color: var(--el-text-color-primary);
}
.el-card__body {
padding: 18px;
}
}
/* Menu */
.el-menu {
user-select: none;
}
/* Drawer */
.el-drawer {
--el-drawer-padding-primary: 16px;
&__header {
margin-bottom: 0;
padding: 13px 16px;
padding: 14px 18px;
border-bottom: 1px solid var(--el-border-color-lighter);
font-weight: 600;
}
&__title {
@apply text-tx-primary;
}
}
.el-table {
--el-table-header-text-color: var(--el-text-color-primary);
--el-table-header-bg-color: var(--table-header-bg-color);
font-size: var(--el-font-size-base);
thead {
th {
font-weight: 400;
}
&__body {
padding: 16px 18px;
}
}
/* Table */
.el-table {
--el-table-header-text-color: var(--el-text-color-primary);
--el-table-header-bg-color: var(--table-header-bg-color);
--el-table-border-color: var(--el-border-color-extra-light);
--el-table-row-hover-bg-color: var(--el-fill-color-lighter);
font-size: var(--el-font-size-base);
border-radius: var(--admin-radius-md);
overflow: hidden;
thead {
th {
font-weight: 600;
font-size: var(--el-font-size-small);
letter-spacing: 0.01em;
}
th.el-table__cell {
background-color: var(--table-header-bg-color);
}
}
.el-table__cell {
padding: 12px 0;
}
}
/* Form controls */
.el-input-group__prepend {
background-color: var(--el-fill-color-blank);
background-color: var(--el-fill-color-light);
border-color: var(--el-border-color);
}
.el-checkbox {
--el-checkbox-font-size: var(--el-font-size-base);
}
.el-button {
--el-border-radius-base: var(--admin-radius-md);
font-weight: 500;
transition: transform 0.12s ease, box-shadow 0.2s ease, background-color 0.2s ease;
&:active:not(.is-disabled) {
transform: scale(0.98);
}
}
.el-button--primary:not(.is-link):not(.is-text):not(.is-plain) {
background: var(--admin-brand-gradient);
border: none;
box-shadow: var(--admin-brand-glow);
--el-button-bg-color: transparent;
--el-button-border-color: transparent;
--el-button-hover-bg-color: transparent;
--el-button-hover-border-color: transparent;
&:hover {
filter: brightness(1.05);
box-shadow: 0 12px 32px rgba(6, 182, 212, 0.32);
}
}
.el-button--primary.is-link,
.el-button--primary.is-text {
--el-button-hover-link-text-color: var(--admin-brand-primary-dark);
}
.el-button--primary.is-plain {
--el-button-bg-color: rgba(6, 182, 212, 0.1);
--el-button-border-color: rgba(6, 182, 212, 0.35);
--el-button-text-color: var(--admin-brand-primary-dark);
--el-button-hover-bg-color: rgba(6, 182, 212, 0.16);
--el-button-hover-border-color: var(--admin-brand-primary);
}
.el-button--large {
--el-border-radius-base: var(--admin-radius-md);
}
.el-button.is-round {
--el-border-radius-base: var(--admin-radius-round);
}
/* Popup menus */
.el-menu--popup-container {
&.theme-light {
.el-menu {
@@ -83,12 +200,14 @@
@apply bg-primary-light-9 border-primary border-r-2;
}
}
.el-menu-item:hover,
.el-sub-menu__title:hover {
color: var(--el-color-primary);
}
}
}
&.theme-dark {
.el-menu {
.el-menu-item {
@@ -101,52 +220,120 @@
}
.el-message-box {
--el-messagebox-width: 350px;
--el-messagebox-width: 380px;
--el-messagebox-border-radius: var(--admin-radius-lg);
}
.el-date-editor {
--el-date-editor-datetimerange-width: 380px;
.el-range-input {
font-size: var(--el-font-size-small);
}
}
.el-button--primary {
--el-button-hover-link-text-color: var(--el-color-primary-light-3);
}
.el-button--success {
--el-button-hover-link-text-color: var(--el-color-success-light-3);
}
.el-button--info {
--el-button-hover-link-text-color: var(--el-color-info-light-3);
}
.el-button--warning {
--el-button-hover-link-text-color: var(--el-color-warning-light-3);
}
.el-button--danger {
--el-button-hover-link-text-color: var(--el-color-danger-light-3);
}
.el-image__error {
font-size: 12px;
}
.el-tabs__nav-wrap::after {
height: 1px;
background: var(--el-border-color-extra-light);
}
.el-tabs__item {
font-weight: 500;
transition: color 0.2s ease;
&.is-active {
color: var(--el-color-primary);
font-weight: 600;
}
&:hover {
color: var(--el-color-primary-light-3);
}
}
.el-tabs__active-bar {
height: 3px;
border-radius: 999px;
background: var(--admin-brand-gradient);
}
.el-page-header {
&__breadcrumb {
margin-bottom: 0;
}
}
/* Tags */
.el-tag {
--el-tag-border-radius: var(--admin-radius-sm);
border: none;
font-weight: 500;
}
/* Pagination */
.el-pagination {
.el-pager li {
border-radius: var(--admin-radius-sm);
font-weight: 500;
}
.btn-prev,
.btn-next {
border-radius: var(--admin-radius-sm);
}
}
/* Alert */
.el-alert {
border-radius: var(--admin-radius-md);
border: 1px solid var(--el-border-color-extra-light);
}
/* Empty state */
.el-empty {
padding: 32px 0;
}
/* Descriptions */
.el-descriptions {
.el-descriptions__label {
font-weight: 500;
color: var(--el-text-color-secondary);
}
}
/* Focus rings */
.el-input,
.el-select,
.el-textarea {
@apply shadow-primary-light-8;
box-shadow: 0 0 0 0 var(--tw-shadow-color);
&:focus-within {
box-shadow: 0 0 0 2px var(--tw-shadow-color);
border-radius: var(--el-input-border-radius, var(--el-border-radius-base));
transition: box-shadow ease 0.1s;
transition: box-shadow ease 0.15s;
}
}
@@ -156,6 +343,7 @@
border-radius: var(--el-checkbox-border-radius);
box-shadow: 0 0 0 0 var(--tw-shadow-color);
&:active {
box-shadow: 0 0 0 2px var(--tw-shadow-color);
transition: box-shadow ease 0s;
@@ -173,29 +361,61 @@
.el-form-item.is-error .el-checkbox {
@apply shadow-danger-light-8;
}
/* Dropdown */
.el-dropdown-menu {
border-radius: var(--admin-radius-md);
border: 1px solid var(--el-border-color-lighter);
box-shadow: var(--el-box-shadow);
padding: 4px;
}
.el-dropdown-menu__item {
border-radius: var(--admin-radius-sm);
}
/* Popover / tooltip polish */
.el-popover.el-popper {
border-radius: var(--admin-radius-md);
border: 1px solid var(--el-border-color-lighter);
box-shadow: var(--el-box-shadow);
}
}
@media (max-width: 768px) {
.el-pagination > .el-pagination__jump {
display: none !important;
}
.el-pagination > .el-pagination__sizes {
display: none !important;
}
}
.el-button {
// 防止被tailwindcss默认样式覆盖
background-color: var(--el-button-bg-color, var(--el-color-white));
//覆盖el-button的点击样式
&:focus {
color: var(--el-button-text-color);
border-color: var(--el-button-border-color);
background-color: var(--el-button-bg-color);
}
&:hover {
color: var(--el-button-hover-text-color);
border-color: var(--el-button-hover-border-color);
background-color: var(--el-button-hover-bg-color);
}
}
/* Inline form filter blocks on list pages */
.el-card .el-form--inline {
.el-form-item {
margin-bottom: 12px;
}
}
/* Page section spacing between stacked cards */
.el-card + .el-card {
margin-top: 16px;
}
+4 -2
View File
@@ -1,5 +1,7 @@
@import 'element.scss';
@import 'dark.css';
@import 'var.css';
@import 'dark.css';
@import 'tailwind.css';
@import 'element.scss';
@import 'admin-shell.scss';
@import 'admin-pages.scss';
@import 'public.scss';
+48
View File
@@ -1,6 +1,10 @@
body {
@apply text-base text-tx-primary overflow-hidden min-w-[375px];
font-feature-settings: 'kern' 1;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.form-tips {
@apply text-tx-secondary text-xs leading-6 mt-1;
}
@@ -12,7 +16,51 @@ body {
visibility: hidden;
}
/* Scrollbar */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: var(--el-border-color);
border-radius: 999px;
border: 2px solid transparent;
background-clip: content-box;
}
::-webkit-scrollbar-thumb:hover {
background: var(--el-border-color-dark);
background-clip: content-box;
}
/* NProgress */
#nprogress .bar {
@apply bg-primary #{!important};
height: 2px !important;
}
#nprogress .peg {
box-shadow: 0 0 8px var(--el-color-primary), 0 0 4px var(--el-color-primary) !important;
}
/* Shared page utilities */
.admin-page-title {
@apply text-xl font-semibold text-tx-primary tracking-tight;
}
.admin-page-desc {
@apply text-sm text-tx-secondary mt-1;
}
.admin-stat-value {
@apply text-3xl font-semibold text-tx-primary tabular-nums;
}
.admin-stat-label {
@apply text-sm text-tx-secondary;
}
+85 -35
View File
@@ -1,49 +1,99 @@
:root {
/* Typography */
--el-font-family: theme(fontFamily.sans);
--el-font-weight-primary: 400;
--el-menu-item-height: 46px;
--el-menu-sub-item-height: var(--el-menu-item-height);
--el-menu-icon-width: 18px;
--aside-width: 200px;
--navbar-height: 50px;
--color-white: #ffffff;
--table-header-bg-color: #f8f8f8;
--el-font-size-extra-large: 18px;
--el-menu-base-level-padding: 16px;
--el-menu-level-padding: 26px;
--el-font-size-large: 16px;
--el-font-size-medium: 15px;
--el-font-size-base: 14px;
--el-font-size-small: 13px;
--el-font-size-extra-small: 12px;
/* Brand - MedTech 绚丽临床 */
--admin-brand-primary: #06b6d4;
--admin-brand-primary-dark: #0891b2;
--admin-brand-accent: #10b981;
--admin-brand-secondary: #6366f1;
--admin-brand-success: #10b981;
--admin-brand-gradient: linear-gradient(120deg, #06b6d4 0%, #10b981 48%, #0891b2 100%);
--admin-brand-gradient-soft: linear-gradient(
120deg,
rgba(6, 182, 212, 0.16) 0%,
rgba(16, 185, 129, 0.1) 50%,
rgba(8, 145, 178, 0.08) 100%
);
--admin-brand-glow: 0 0 24px rgba(6, 182, 212, 0.35);
--admin-brand-mesh-a: rgba(6, 182, 212, 0.18);
--admin-brand-mesh-b: rgba(16, 185, 129, 0.14);
--admin-brand-mesh-c: rgba(99, 102, 241, 0.1);
/* Layout shell v2 */
--aside-width: 248px;
--navbar-height: 60px;
--tabs-height: 42px;
--stage-padding-x: 28px;
--stage-padding-y: 24px;
--content-max-width: 1680px;
--sidebar-rail-width: 3px;
/* Shape */
--admin-radius-sm: 8px;
--admin-radius-md: 12px;
--admin-radius-lg: 16px;
--admin-radius-xl: 22px;
--admin-radius-2xl: 28px;
--el-border-radius-base: var(--admin-radius-md);
--el-border-radius-small: var(--admin-radius-sm);
--el-border-radius-round: 999px;
/* Menu */
--el-menu-item-height: 46px;
--el-menu-sub-item-height: var(--el-menu-item-height);
--el-menu-icon-width: 20px;
--el-menu-base-level-padding: 14px;
--el-menu-level-padding: 22px;
/* Surfaces */
--color-white: #ffffff;
--table-header-bg-color: rgba(6, 182, 212, 0.06);
--sidebar-dark-bg: #060d18;
--sidebar-dark-hover: rgba(255, 255, 255, 0.05);
--sidebar-dark-active: rgba(6, 182, 212, 0.14);
--admin-surface-elevated: #ffffff;
--admin-surface-muted: rgba(255, 255, 255, 0.72);
--admin-surface-glass: rgba(255, 255, 255, 0.82);
--el-bg-color: var(--color-white);
--el-bg-color-page: #f6f6f6;
--el-bg-color-page: #eef6fb;
--el-bg-color-overlay: #ffffff;
--el-text-color-primary: #333333;
--el-text-color-regular: #666666;
--el-text-color-secondary: #999999;
--el-text-color-placeholder: #a8abb2;
--el-text-color-disabled: #c0c4cc;
--el-border-color: #dcdfe6;
--el-border-color-light: #e4e7ed;
--el-border-color-lighter: #ebeef5;
--el-border-color-extra-light: #f2f2f2;
--el-border-color-dark: #d4d7de;
--el-border-color-darker: #cdd0d6;
--el-fill-color: #f0f2f5;
--el-fill-color-light: #f8f8f8;
--el-fill-color-lighter: #fafafa;
--el-fill-color-extra-light: #fafcff;
--el-fill-color-dark: #ebedf0;
--el-fill-color-darker: #e6e8eb;
--el-text-color-primary: #0b1220;
--el-text-color-regular: #334155;
--el-text-color-secondary: #64748b;
--el-text-color-placeholder: #94a3b8;
--el-text-color-disabled: #cbd5e1;
--el-border-color: rgba(6, 182, 212, 0.12);
--el-border-color-light: rgba(6, 182, 212, 0.08);
--el-border-color-lighter: rgba(15, 23, 42, 0.06);
--el-border-color-extra-light: rgba(15, 23, 42, 0.04);
--el-border-color-dark: #cbd5e1;
--el-border-color-darker: #94a3b8;
--el-fill-color: #f1f5f9;
--el-fill-color-light: #f8fafc;
--el-fill-color-lighter: #fafbfc;
--el-fill-color-extra-light: #fcfdfe;
--el-fill-color-dark: #e2e8f0;
--el-fill-color-darker: #cbd5e1;
--el-fill-color-blank: #ffffff;
/* 过亮会盖住抽屉/弹窗下的内容;Element Loading 与部分蒙层共用此变量 */
--el-mask-color: rgba(255, 255, 255, 0.5);
--el-mask-color-extra-light: rgba(255, 255, 255, 0.22);
-el-box-shadow: 0px 12px 32px 4px rgba(0, 0, 0, 0.04), 0px 8px 20px rgba(0, 0, 0, 0.08);
--el-box-shadow-light: 0px 0px 12px rgba(0, 0, 0, 0.12);
--el-box-shadow-lighter: 0px 0px 6px rgba(0, 0, 0, 0.12);
--el-box-shadow-dark: 0px 16px 48px 16px rgba(0, 0, 0, 0.08), 0px 12px 32px rgba(0, 0, 0, 0.12),
0px 8px 16px -8px rgba(0, 0, 0, 0.16);
--el-mask-color: rgba(6, 13, 24, 0.55);
--el-mask-color-extra-light: rgba(6, 13, 24, 0.1);
--el-box-shadow: 0 4px 24px rgba(6, 182, 212, 0.08), 0 12px 40px rgba(15, 23, 42, 0.06);
--el-box-shadow-light: 0 2px 16px rgba(6, 182, 212, 0.1);
--el-box-shadow-lighter: 0 1px 4px rgba(15, 23, 42, 0.04);
--el-box-shadow-dark: 0 16px 48px rgba(6, 182, 212, 0.14), 0 32px 64px rgba(15, 23, 42, 0.1);
--admin-header-border: rgba(6, 182, 212, 0.1);
--admin-sidebar-border: rgba(255, 255, 255, 0.06);
--admin-stage-bg: transparent;
}
@@ -1,104 +0,0 @@
/**
* getUserMedia 拦截器:给通话视频流透明注入本地 AI 高级美颜
*
* TUICallKit / TRTC SDK 申请摄像头时走 navigator.mediaDevices.getUserMedia
* 这里在其外层包一层:拿到真实摄像头流后,若高级美颜已开启,则把视频轨送入
* FaceBeautyPipeline 处理,返回处理后的流。SDK 对此无感知,无需改动任何 SDK 代码。
*
* 仅处理"摄像头视频"请求;纯音频、屏幕共享(getDisplayMedia)不受影响。
* 管线创建失败时始终回退原始流,不会因美颜故障导致通话不可用。
*/
import { hasAnyBeautyEffect, loadBeautySettings } from '@/utils/call-beauty'
import useUserStore from '@/stores/modules/user'
import {
FaceBeautyPipeline,
getFaceLandmarker,
isAdvancedBeautySupported
} from './face-beauty-pipeline'
/**
* 当前登录医生是否被允许使用美颜(la_admin.enable_beauty,医生编辑页配置)。
* 字段缺失(旧数据/旧接口)时视为允许,与后端默认值保持一致。
*/
export function isBeautyAllowedForCurrentUser(): boolean {
try {
const userStore = useUserStore()
return userStore.userInfo?.enable_beauty !== 0
} catch {
return true
}
}
let installed = false
let originalGetUserMedia: ((constraints?: MediaStreamConstraints) => Promise<MediaStream>) | null =
null
/**
* 绕过拦截器直接获取原始摄像头流。
* 供美颜预览使用:预览自己管理管线,避免被拦截器再包一层造成双重处理。
*/
export function getRawUserMedia(constraints?: MediaStreamConstraints): Promise<MediaStream> {
if (originalGetUserMedia) return originalGetUserMedia(constraints)
return navigator.mediaDevices.getUserMedia(constraints)
}
async function wrapStreamWithBeauty(stream: MediaStream): Promise<MediaStream> {
const videoTrack = stream.getVideoTracks()[0]
if (!videoTrack) return stream
const settings = loadBeautySettings()
const pipeline = new FaceBeautyPipeline(videoTrack, settings)
const processedTrack = await pipeline.start()
// SDK 停用处理后的轨道时,同步销毁管线并释放真实摄像头
const originalStop = processedTrack.stop.bind(processedTrack)
processedTrack.stop = () => {
originalStop()
pipeline.destroy()
stream.getTracks().forEach((t) => t.stop())
}
return new MediaStream([processedTrack, ...stream.getAudioTracks()])
}
/**
* 安装拦截器(幂等)。需在 SDK 发起摄像头请求前调用——
* 目前由美颜面板组件挂载时触发,早于任何通话开始。
*/
export function installBeautyMediaInterceptor() {
if (installed) return
if (!navigator.mediaDevices?.getUserMedia) return
installed = true
const original = navigator.mediaDevices.getUserMedia.bind(navigator.mediaDevices)
originalGetUserMedia = original
navigator.mediaDevices.getUserMedia = async (constraints?: MediaStreamConstraints) => {
const stream = await original(constraints)
if (!constraints?.video) return stream
const settings = loadBeautySettings()
if (
!isBeautyAllowedForCurrentUser() ||
!hasAnyBeautyEffect(settings) ||
!isAdvancedBeautySupported()
) {
return stream
}
try {
return await wrapStreamWithBeauty(stream)
} catch (err) {
console.warn('[face-beauty] 美颜管线创建失败,使用原始摄像头画面', err)
return stream
}
}
}
/** 高级美颜开启时提前预热模型(约 4MB,加载一次全局复用),避免首帧卡顿 */
export function preloadBeautyModel() {
if (!isAdvancedBeautySupported()) return
getFaceLandmarker().catch(() => {
// 预热失败不打扰用户,实际使用时还会重试并有兜底
})
}
File diff suppressed because it is too large Load Diff
-191
View File
@@ -1,191 +0,0 @@
/**
* 视频通话美颜设置
*
* 全部效果(磨皮/美白/红润/祛斑/去痣/去黑眼圈/亮度/清晰度/瘦脸/口红/腮红)默认由本地管线处理:
* MediaPipe Face Landmarker(开源免费)识别人脸关键点 + WebGL/Canvas 逐帧渲染,
* 经 getUserMedia 拦截注入通话视频流(见 utils/beauty/ 目录),预览所见即所得。
*
* 仅当浏览器不支持 WebGL 时,磨皮/美白/红润回退到腾讯云 TRTC 免费 BasicBeauty
* 插件(TUICallEngine.setBeautyLevel)。不依赖付费的腾讯特效 SDK。
*/
/** 高级美颜(本地 AI 处理)设置 */
export interface AdvancedBeautySettings {
/** 总开关;通话中途开启需重新开关摄像头才能接管视频流 */
enabled: boolean
/** 瘦脸强度 0-100 */
slimStrength: number
/** 口红浓度 0-1000 为关闭) */
lipstickStrength: number
/** 口红颜色(hex */
lipstickColor: string
/** 腮红浓度 0-1000 为关闭) */
blushStrength: number
/** 腮红颜色(hex */
blushColor: string
}
export interface CallBeautySettings {
/** 是否启用基础美颜(关闭时以全 0 参数下发,引擎会停用 BasicBeauty 插件) */
enabled: boolean
/** 磨皮算法:0 光滑(TRTCBeautyStyleSmooth/ 1 自然(TRTCBeautyStyleNature */
style: number
/** 磨皮级别 0-9 */
beautyLevel: number
/** 美白级别 0-9 */
whitenessLevel: number
/** 红润级别 0-9 */
ruddinessLevel: number
/** 祛斑 / 淡化黑色素 0-9(仅人脸,压暗斑点向周围肤色靠拢) */
spotLevel: number
/** 去痣 0-9(仅人脸,强力淡化小面积深色痣点) */
moleLevel: number
/** 去黑眼圈 0-9(仅眼下区域提亮淡化) */
darkCircleLevel: number
/** 摄像头亮度 0-9(整帧提亮,0 为原亮度) */
brightnessLevel: number
/** 清晰度 0-9(锐化细节,0 为关闭) */
clarityLevel: number
/** 高级美颜(瘦脸/口红/腮红,本地 AI) */
advanced: AdvancedBeautySettings
}
export const BEAUTY_STYLE_SMOOTH = 0
export const BEAUTY_STYLE_NATURE = 1
export const BEAUTY_LEVEL_MAX = 9
const STORAGE_KEY = 'tcm_call_beauty_settings'
export const defaultAdvancedBeautySettings = (): AdvancedBeautySettings => ({
enabled: false,
slimStrength: 30,
lipstickStrength: 0,
lipstickColor: '#c94f5e',
blushStrength: 0,
blushColor: '#e88193'
})
export const defaultBeautySettings = (): CallBeautySettings => ({
enabled: false,
style: BEAUTY_STYLE_NATURE,
beautyLevel: 5,
whitenessLevel: 3,
ruddinessLevel: 2,
spotLevel: 0,
moleLevel: 0,
darkCircleLevel: 0,
brightnessLevel: 0,
clarityLevel: 0,
advanced: defaultAdvancedBeautySettings()
})
const clampLevel = (value: unknown): number => {
const num = Math.round(Number(value))
if (!Number.isFinite(num)) return 0
return Math.min(BEAUTY_LEVEL_MAX, Math.max(0, num))
}
const clampPercent = (value: unknown, fallback: number): number => {
const num = Math.round(Number(value))
if (!Number.isFinite(num)) return fallback
return Math.min(100, Math.max(0, num))
}
const normalizeHexColor = (value: unknown, fallback: string): string => {
if (typeof value === 'string' && /^#[0-9a-fA-F]{6}$/.test(value)) return value
return fallback
}
/** 高级美颜是否有任一效果实际开启 */
export function hasAnyAdvancedEffect(advanced: AdvancedBeautySettings): boolean {
return (
advanced.enabled &&
(advanced.slimStrength > 0 || advanced.lipstickStrength > 0 || advanced.blushStrength > 0)
)
}
/** 全部美颜(基础 + 高级)是否有任一效果实际开启(决定是否接管摄像头流) */
export function hasAnyBeautyEffect(settings: CallBeautySettings): boolean {
const basicOn =
settings.enabled &&
(settings.beautyLevel > 0 ||
settings.whitenessLevel > 0 ||
settings.ruddinessLevel > 0 ||
settings.spotLevel > 0 ||
settings.moleLevel > 0 ||
settings.darkCircleLevel > 0 ||
settings.brightnessLevel > 0 ||
settings.clarityLevel > 0)
return basicOn || hasAnyAdvancedEffect(settings.advanced)
}
export function loadBeautySettings(): CallBeautySettings {
const defaults = defaultBeautySettings()
try {
const raw = localStorage.getItem(STORAGE_KEY)
if (!raw) return defaults
const parsed = JSON.parse(raw)
const advDefaults = defaults.advanced
const adv = parsed?.advanced ?? {}
return {
enabled: Boolean(parsed?.enabled),
style:
parsed?.style === BEAUTY_STYLE_SMOOTH
? BEAUTY_STYLE_SMOOTH
: BEAUTY_STYLE_NATURE,
beautyLevel: clampLevel(parsed?.beautyLevel ?? defaults.beautyLevel),
whitenessLevel: clampLevel(parsed?.whitenessLevel ?? defaults.whitenessLevel),
ruddinessLevel: clampLevel(parsed?.ruddinessLevel ?? defaults.ruddinessLevel),
spotLevel: clampLevel(parsed?.spotLevel ?? defaults.spotLevel),
moleLevel: clampLevel(parsed?.moleLevel ?? defaults.moleLevel),
darkCircleLevel: clampLevel(parsed?.darkCircleLevel ?? defaults.darkCircleLevel),
brightnessLevel: clampLevel(parsed?.brightnessLevel ?? defaults.brightnessLevel),
clarityLevel: clampLevel(parsed?.clarityLevel ?? defaults.clarityLevel),
advanced: {
enabled: Boolean(adv?.enabled),
slimStrength: clampPercent(adv?.slimStrength, advDefaults.slimStrength),
lipstickStrength: clampPercent(adv?.lipstickStrength, advDefaults.lipstickStrength),
lipstickColor: normalizeHexColor(adv?.lipstickColor, advDefaults.lipstickColor),
blushStrength: clampPercent(adv?.blushStrength, advDefaults.blushStrength),
blushColor: normalizeHexColor(adv?.blushColor, advDefaults.blushColor)
}
}
} catch {
return defaults
}
}
export function saveBeautySettings(settings: CallBeautySettings) {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(settings))
} catch {
// localStorage 不可用时静默忽略,仅本次会话生效
}
}
/**
* 将美颜设置应用到 TUICallEngine 实例。
* 引擎内部对 setBeautyLevel 自带 try-catch(摄像头未开启等场景不会抛错),
* 这里只兜底引擎不存在 / 接口缺失的情况。
*/
export async function applyBeautyToEngine(
engine: any,
settings: CallBeautySettings
): Promise<boolean> {
if (!engine || typeof engine.setBeautyLevel !== 'function') {
return false
}
const enabled = settings.enabled
try {
await engine.setBeautyLevel({
style: settings.style === BEAUTY_STYLE_SMOOTH ? BEAUTY_STYLE_SMOOTH : BEAUTY_STYLE_NATURE,
beautyLevel: enabled ? clampLevel(settings.beautyLevel) : 0,
whitenessLevel: enabled ? clampLevel(settings.whitenessLevel) : 0,
ruddinessLevel: enabled ? clampLevel(settings.ruddinessLevel) : 0
})
return true
} catch (error) {
console.warn('[call-beauty] 应用美颜设置失败', error)
return false
}
}
+25 -6
View File
@@ -1,9 +1,10 @@
<template>
<div class="change-password flex flex-col">
<div class="flex-1 flex items-center justify-center">
<div class="change-password-card bg-body rounded-md px-10 py-10 w-[480px]">
<div class="text-center text-2xl font-medium mb-2">首次登录</div>
<div class="text-center text-gray-500 text-sm mb-8">为了您的账号安全请修改初始密码</div>
<div class="change-password-backdrop" aria-hidden="true"></div>
<div class="flex-1 flex items-center justify-center relative z-[1] px-4">
<div class="change-password-card">
<div class="text-center text-2xl font-semibold mb-2 text-tx-primary">首次登录</div>
<div class="text-center text-tx-secondary text-sm mb-8">为了您的账号安全请修改初始密码</div>
<el-form ref="formRef" :model="formData" size="large" :rules="rules">
<el-form-item prop="password">
@@ -117,7 +118,25 @@ const { isLock, lockFn: lockSubmit } = useLockFn(handleSubmit)
<style lang="scss" scoped>
.change-password {
background-image: url('./images/login_bg.png');
@apply min-h-screen bg-no-repeat bg-center bg-cover;
position: relative;
min-height: 100vh;
background: #0f172a;
}
.change-password-backdrop {
position: absolute;
inset: 0;
background:
radial-gradient(circle at 15% 20%, rgba(15, 118, 110, 0.35), transparent 42%),
linear-gradient(160deg, #0f172a 0%, #111827 48%, #0b1120 100%);
}
.change-password-card {
width: min(480px, 100%);
padding: 40px 36px;
border-radius: var(--admin-radius-xl);
border: 1px solid rgba(255, 255, 255, 0.08);
box-shadow: 0 24px 64px rgba(2, 6, 23, 0.45);
background: var(--el-bg-color);
}
</style>
+192 -32
View File
@@ -1,33 +1,41 @@
<template>
<div class="login flex flex-col">
<div class="flex-1 flex items-center justify-center">
<div class="login-card flex rounded-md overflow-hidden">
<div class="flex-1 h-full hidden md:inline-block">
<image-contain :src="config.login_image" :width="400" height="100%" />
<div class="login-v2">
<div class="login-v2__mesh" aria-hidden="true"></div>
<div class="login-v2__layout">
<aside class="login-v2__brand">
<div class="login-v2__brand-inner">
<div class="login-v2__mark">ZYT</div>
<p class="login-v2__kicker">互联网医院</p>
<h1 class="login-v2__headline">医生与医助<br />智慧工作台</h1>
<p class="login-v2__lead">问诊处方订单与运营数据一屏协同</p>
<div v-if="config.login_image" class="login-v2__visual hidden xl:block">
<image-contain :src="config.login_image" :width="420" height="280" />
</div>
</div>
<div
class="login-form bg-body flex flex-col justify-center px-10 py-10 md:w-[420px] w-[380px] flex-none mx-auto"
>
<div class="text-center text-3xl font-medium mb-8">{{ config.web_name }}</div>
</aside>
<!-- 企业微信自动授权中 -->
<div v-if="wxWorkAutoLogin" class="text-center py-10">
<section class="login-v2__panel">
<div class="login-v2__glass">
<div class="login-v2__form-head">
<h2 class="login-v2__form-title">{{ config.web_name }}</h2>
<p class="login-v2__form-sub">登录后继续你的接诊与管理工作</p>
</div>
<div v-if="wxWorkAutoLogin" class="login-v2__loading">
<el-icon class="is-loading mb-4" :size="40" color="var(--el-color-primary)">
<Loading />
</el-icon>
<div class="text-gray-500">企业微信授权登录中...</div>
<div class="text-tx-secondary">企业微信授权登录中...</div>
</div>
<template v-else>
<!-- 登录方式切换标签 -->
<div v-if="wxWorkEnabled" class="flex justify-center mb-6">
<div v-if="wxWorkEnabled" class="login-v2__mode">
<el-radio-group v-model="loginMode" size="large">
<el-radio-button value="account">账号登录</el-radio-button>
<el-radio-button value="wxwork">企业微信</el-radio-button>
</el-radio-group>
</div>
<!-- 账号密码登录 -->
<template v-if="loginMode === 'account'">
<el-form ref="formRef" :model="formData" size="large" :rules="rules">
<el-form-item prop="account">
@@ -58,29 +66,34 @@
<div class="mb-5">
<el-checkbox v-model="remAccount" label="记住账号"></el-checkbox>
</div>
<el-button type="primary" size="large" :loading="isLock" @click="lockLogin">
登录
<el-button
class="login-v2__submit"
type="primary"
size="large"
:loading="isLock"
@click="lockLogin"
>
进入工作台
</el-button>
</template>
<!-- 企业微信扫码登录非企业微信内浏览器 -->
<template v-if="loginMode === 'wxwork'">
<div class="wxwork-qrcode-wrap">
<div v-if="wxWorkLoading" class="text-center py-10">
<el-icon class="is-loading" :size="32" color="var(--el-color-primary)">
<Loading />
</el-icon>
<div class="mt-2 text-gray-400 text-sm">加载企业微信扫码...</div>
<div class="mt-2 text-tx-secondary text-sm">加载企业微信扫码...</div>
</div>
<div v-else id="wxwork_qrcode_container" class="wxwork-qrcode"></div>
</div>
<div class="text-center text-sm text-gray-400 mt-4">
<div class="text-center text-sm text-tx-secondary mt-4">
请使用企业微信扫描二维码登录
</div>
</template>
</template>
</div>
</div>
</section>
</div>
<layout-footer />
</div>
@@ -286,31 +299,178 @@ onMounted(async () => {
</script>
<style lang="scss" scoped>
.login {
background-image: url('./images/login_bg.png');
@apply min-h-screen bg-no-repeat bg-center bg-cover;
.login-card {
height: auto;
min-height: 400px;
.login-v2 {
position: relative;
min-height: 100vh;
display: flex;
flex-direction: column;
background: #030712;
overflow: hidden;
}
.login-v2__mesh {
position: absolute;
inset: 0;
background:
radial-gradient(circle at 12% 18%, rgba(6, 182, 212, 0.35), transparent 42%),
radial-gradient(circle at 88% 12%, rgba(16, 185, 129, 0.28), transparent 38%),
radial-gradient(circle at 70% 88%, rgba(99, 102, 241, 0.18), transparent 40%),
linear-gradient(155deg, #030712 0%, #0b1220 45%, #060d18 100%);
}
.login-v2__layout {
position: relative;
z-index: 1;
flex: 1;
display: grid;
grid-template-columns: 1fr;
min-height: calc(100vh - 48px);
}
@media (min-width: 1024px) {
.login-v2__layout {
grid-template-columns: minmax(0, 1.05fr) minmax(420px, 520px);
}
}
.login-v2__brand {
display: none;
padding: 48px 56px;
color: #ffffff;
}
@media (min-width: 1024px) {
.login-v2__brand {
display: flex;
align-items: center;
}
}
.login-v2__brand-inner {
max-width: 520px;
}
.login-v2__mark {
width: 48px;
height: 48px;
border-radius: 14px;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: 800;
letter-spacing: 0.1em;
background: var(--admin-brand-gradient);
box-shadow: var(--admin-brand-glow);
margin-bottom: 28px;
}
.login-v2__kicker {
margin: 0 0 12px;
font-size: 13px;
font-weight: 600;
letter-spacing: 0.14em;
text-transform: uppercase;
color: rgba(255, 255, 255, 0.72);
}
.login-v2__headline {
margin: 0;
font-size: clamp(32px, 4vw, 44px);
font-weight: 800;
line-height: 1.15;
letter-spacing: -0.03em;
}
.login-v2__lead {
margin: 16px 0 0;
font-size: 16px;
line-height: 1.6;
color: rgba(255, 255, 255, 0.72);
max-width: 36ch;
}
.login-v2__visual {
margin-top: 36px;
border-radius: var(--admin-radius-xl);
overflow: hidden;
border: 1px solid rgba(255, 255, 255, 0.12);
box-shadow: 0 24px 64px rgba(0, 0, 0, 0.35);
}
.login-v2__panel {
display: flex;
align-items: center;
justify-content: center;
padding: 32px 20px;
}
.login-v2__glass {
width: min(440px, 100%);
padding: 36px 32px;
border-radius: var(--admin-radius-2xl);
border: 1px solid rgba(255, 255, 255, 0.14);
background: rgba(255, 255, 255, 0.92);
backdrop-filter: blur(24px) saturate(160%);
box-shadow: var(--el-box-shadow-dark);
}
.login-v2__form-head {
margin-bottom: 28px;
}
.login-v2__form-title {
margin: 0;
font-size: 26px;
font-weight: 800;
letter-spacing: -0.02em;
color: var(--el-text-color-primary);
}
.login-v2__form-sub {
margin: 8px 0 0;
font-size: 14px;
color: var(--el-text-color-secondary);
}
.login-v2__mode {
display: flex;
justify-content: center;
margin-bottom: 24px;
}
.login-v2__submit {
width: 100%;
}
.login-v2__loading {
text-align: center;
padding: 40px 0;
}
.wxwork-qrcode-wrap {
display: flex;
justify-content: center;
align-items: center;
min-height: 400px;
min-height: 360px;
}
.wxwork-qrcode {
width: 340px;
height: 400px;
width: 320px;
height: 360px;
overflow: hidden;
:deep(iframe) {
width: 340px !important;
height: 400px !important;
width: 320px !important;
height: 360px !important;
border: none;
}
}
@media (prefers-reduced-transparency: reduce) {
.login-v2__glass {
background: #ffffff;
backdrop-filter: none;
}
}
</style>
+9 -9
View File
@@ -1,17 +1,17 @@
<template>
<div>
<el-card class="!border-none" shadow="never">
<admin-page-filter-panel>
<el-alert
type="warning"
title="温馨提示:用于管理网站的分类,只可添加到一级"
:closable="false"
show-icon
/>
</el-card>
<el-card class="!border-none mt-4" shadow="never" v-loading="pager.loading">
<div>
</admin-page-filter-panel>
<admin-page-data-panel v-loading="pager.loading">
<template #toolbar>
<el-button
class="mb-4"
v-perms="['article.articleCate/add']"
type="primary"
@click="handleAdd()"
@@ -21,7 +21,7 @@
</template>
新增
</el-button>
</div>
</template>
<el-table size="large" :data="pager.lists">
<el-table-column label="栏目名称" prop="name" min-width="120" />
<el-table-column label="文章数" prop="article_count" min-width="120" />
@@ -58,10 +58,10 @@
</template>
</el-table-column>
</el-table>
<div class="flex justify-end mt-4">
<template #footer>
<pagination v-model="pager" @change="getLists" />
</div>
</el-card>
</template>
</admin-page-data-panel>
<edit-popup v-if="showEdit" ref="editRef" @success="getLists" @close="showEdit = false" />
</div>
</template>
+12 -11
View File
@@ -1,6 +1,6 @@
<template>
<div class="article-lists">
<el-card class="!border-none" shadow="never">
<div>
<admin-page-filter-panel>
<el-form ref="formRef" class="mb-[-16px]" :model="queryParams" :inline="true">
<el-form-item class="w-[280px]" label="文章标题">
<el-input
@@ -33,24 +33,25 @@
<el-button @click="resetParams">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<el-card class="!border-none mt-4" shadow="never">
<div>
</admin-page-filter-panel>
<admin-page-data-panel v-loading="pager.loading">
<template #toolbar>
<router-link
v-perms="['article.article/add', 'article.article/add:edit']"
:to="{
path: getRoutePath('article.article/add:edit')
}"
>
<el-button type="primary" class="mb-4">
<el-button type="primary">
<template #icon>
<icon name="el-icon-Plus" />
</template>
发布文章
</el-button>
</router-link>
</div>
<el-table size="large" v-loading="pager.loading" :data="pager.lists">
</template>
<el-table size="large" :data="pager.lists">
<el-table-column label="ID" prop="id" min-width="80" />
<el-table-column label="封面" min-width="100">
<template #default="{ row }">
@@ -116,10 +117,10 @@
</template>
</el-table-column>
</el-table>
<div class="flex justify-end mt-4">
<template #footer>
<pagination v-model="pager" @change="getLists" />
</div>
</el-card>
</template>
</admin-page-data-panel>
</div>
</template>
<script lang="ts" setup name="articleLists">
+10 -10
View File
@@ -1,7 +1,7 @@
<template>
<div class="asset-resource-container">
<!-- 搜索区域 -->
<el-card class="!border-none" shadow="never">
<admin-page-filter-panel>
<el-form class="ls-form" :model="searchData" inline>
<el-form-item class="w-[280px]" label="标题">
<el-input
@@ -28,25 +28,25 @@
<el-button @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</el-card>
</admin-page-filter-panel>
<!-- 列表区域 -->
<el-card class="!border-none mt-4" shadow="never">
<div class="mb-4 flex items-center gap-2">
<admin-page-data-panel v-loading="loading">
<template #toolbar>
<el-button type="primary" @click="handleAdd">上传资源</el-button>
<el-button type="danger" :disabled="selectedIds.length === 0" @click="handleBatchDelete">
批量删除
</el-button>
<span v-if="selectedIds.length > 0" class="text-sm text-gray-500">已选 {{ selectedIds.length }} </span>
</div>
</template>
<el-tabs v-model="queryParams.type" @tab-change="handleTabChange">
<el-tab-pane label="图片" name="1"></el-tab-pane>
<el-tab-pane label="视频" name="2"></el-tab-pane>
<el-tab-pane label="语音" name="3"></el-tab-pane>
</el-tabs>
<el-table :data="tableData" v-loading="loading" @selection-change="handleSelectionChange">
<el-table :data="tableData" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" />
<!-- <el-table-column prop="id" label="ID" width="80" /> -->
<el-table-column prop="title" label="标题" min-width="80" />
@@ -71,7 +71,7 @@
</el-table-column>
</el-table>
<div class="mt-4 flex justify-end">
<template #footer>
<el-pagination
v-model:current-page="queryParams.page_no"
v-model:page-size="queryParams.page_size"
@@ -80,8 +80,8 @@
@size-change="getList"
@current-change="getList"
/>
</div>
</el-card>
</template>
</admin-page-data-panel>
<!-- 新增/编辑资源弹窗 -->
<el-dialog v-model="dialogVisible" :title="isEdit ? '编辑资源' : '新增分发资源'" width="600px" destroy-on-close>
+10 -10
View File
@@ -1,7 +1,7 @@
<template>
<div class="asset-user-container">
<!-- 搜索区域 -->
<el-card class="!border-none" shadow="never">
<admin-page-filter-panel>
<el-form class="ls-form" :model="searchData" inline>
<el-form-item class="w-[280px]" label="手机号">
<el-input
@@ -16,15 +16,15 @@
<el-button @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</el-card>
</admin-page-filter-panel>
<!-- 列表区域 -->
<el-card class="!border-none mt-4" shadow="never">
<div class="mb-4">
<admin-page-data-panel v-loading="loading">
<template #toolbar>
<el-button type="primary" @click="handleAdd">新增账号</el-button>
</div>
<el-table :data="tableData" v-loading="loading">
</template>
<el-table :data="tableData">
<!-- <el-table-column prop="id" label="ID" width="80" /> -->
<el-table-column prop="phone" label="手机号" />
<el-table-column label="备注" min-width="200">
@@ -63,7 +63,7 @@
</el-table-column>
</el-table>
<div class="mt-4 flex justify-end">
<template #footer>
<el-pagination
v-model:current-page="queryParams.page_no"
v-model:page-size="queryParams.page_size"
@@ -72,8 +72,8 @@
@size-change="getList"
@current-change="getList"
/>
</div>
</el-card>
</template>
</admin-page-data-panel>
<!-- 编辑/新增弹窗 -->
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="500px" destroy-on-close>
-15
View File
@@ -146,19 +146,6 @@
<span class="ml-2 text-gray-500">{{ formData.enable_charge === 1 ? '已开启' : '已关闭' }}</span>
</el-form-item>
<!-- 美颜功能 -->
<el-form-item label="美颜功能">
<div>
<el-switch
v-model="formData.enable_beauty"
:active-value="1"
:inactive-value="0"
/>
<span class="ml-2 text-gray-500">{{ formData.enable_beauty === 1 ? '已开启' : '已关闭' }}</span>
<div class="form-tips">开启后该医生视频通话时可配置美颜磨皮/美白/瘦脸/口红等关闭则不显示美颜入口</div>
</div>
</el-form-item>
<!-- 所属科室 -->
<el-form-item label="所属科室" prop="department">
<el-select v-model="formData.department" placeholder="请选择所属科室" clearable class="flex-1">
@@ -308,7 +295,6 @@ const formData = reactive({
enable_image_consult: 1, // 是否开启图文问诊
enable_video_consult: 1, // 是否开启视频问诊
enable_charge: 0, // 是否开启收费
enable_beauty: 1, // 是否开启美颜功能
department: '', // 所属科室
specialty: '', // 擅长领域
education: '', // 教育背景
@@ -450,7 +436,6 @@ const open = (type = 'add') => {
enable_image_consult: 1,
enable_video_consult: 1,
enable_charge: 0,
enable_beauty: 1,
department: '',
specialty: '',
education: '',
+7 -6
View File
@@ -1,6 +1,6 @@
<template>
<div>
<el-card class="!border-none" shadow="never">
<admin-page-filter-panel>
<el-form ref="formRef" class="mb-[-16px]" :model="queryParams" :inline="true">
<el-form-item class="w-[280px]" label="用户信息">
<el-input
@@ -37,8 +37,9 @@
/>
</el-form-item>
</el-form>
</el-card>
<el-card class="!border-none mt-4" shadow="never">
</admin-page-filter-panel>
<admin-page-data-panel>
<el-table size="large" v-loading="pager.loading" :data="pager.lists">
<el-table-column label="头像" min-width="100">
<template #default="{ row }">
@@ -67,10 +68,10 @@
</template>
</el-table-column>
</el-table>
<div class="flex justify-end mt-4">
<template #footer>
<pagination v-model="pager" @change="getLists" />
</div>
</el-card>
</template>
</admin-page-data-panel>
</div>
</template>
<script lang="ts" setup name="consumerLists">
+7 -7
View File
@@ -1,6 +1,6 @@
<template>
<div class="code-generation">
<el-card class="!border-none" shadow="never">
<div>
<admin-page-filter-panel>
<el-form class="mb-[-16px]" :model="formData" inline>
<el-form-item class="w-[280px]" label="表名称">
<el-input v-model="formData.table_name" clearable @keyup.enter="resetPage" />
@@ -13,8 +13,8 @@
<el-button @click="resetParams">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<el-card class="!border-none mt-4" shadow="never" v-loading="pager.loading">
</admin-page-filter-panel>
<admin-page-data-panel>
<div class="flex">
<data-table
v-perms="['tools.generator/selectTable']"
@@ -126,10 +126,10 @@
</el-table-column>
</el-table>
</div>
<div class="flex justify-end mt-4">
<template #footer>
<pagination v-model="pager" @change="getLists" />
</div>
</el-card>
</template>
</admin-page-data-panel>
<code-preview
v-if="previewState.show"
v-model="previewState.show"
+1 -1
View File
@@ -866,7 +866,7 @@ onUnmounted(() => {
font-size: 15px;
font-weight: 700;
color: #fff;
background: linear-gradient(145deg, #6366f1 0%, #8b5cf6 100%);
background: linear-gradient(145deg, #0d9488 0%, #0891b2 100%);
flex-shrink: 0;
}
+33 -11
View File
@@ -1,11 +1,11 @@
<template>
<div class="error">
<div>
<div class="error-panel">
<slot name="content">
<div class="error-code">{{ code }}</div>
</slot>
<div class="text-lg text-tx-secondary mt-7 mb-7">{{ title }}</div>
<el-button v-if="showBtn" type="primary" @click="router.go(-1)">
<div class="error-title">{{ title }}</div>
<el-button v-if="showBtn" type="primary" size="large" @click="router.go(-1)">
{{ second }} 秒后返回上一页
</el-button>
</div>
@@ -43,16 +43,38 @@ onUnmounted(() => {
<style lang="scss" scoped>
.error {
text-align: center;
height: 100vh;
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
.error-code {
@apply text-primary;
font-size: 150px;
}
.el-button {
width: 176px;
}
background: var(--el-bg-color-page);
padding: 24px;
}
.error-panel {
width: min(480px, 100%);
padding: 48px 32px;
border-radius: var(--admin-radius-xl);
border: 1px solid var(--el-border-color-lighter);
background: var(--el-bg-color);
box-shadow: var(--el-box-shadow);
}
.error-code {
@apply text-primary;
font-size: 96px;
font-weight: 700;
line-height: 1;
letter-spacing: -0.04em;
}
.error-title {
@apply text-tx-secondary;
font-size: 18px;
margin: 20px 0 28px;
}
.el-button {
min-width: 176px;
}
</style>
+9 -9
View File
@@ -1,7 +1,7 @@
<template>
<div class="fans-management">
<!-- 搜索区域 -->
<el-card class="!border-none" shadow="never">
<admin-page-filter-panel>
<el-form class="ls-form" :model="formData" inline>
<el-form-item class="w-[280px]" label="姓名">
<el-input
@@ -36,14 +36,14 @@
<el-button @click="resetParams">重置</el-button>
</el-form-item>
</el-form>
</el-card>
</admin-page-filter-panel>
<!-- 列表区域 -->
<el-card class="!border-none mt-4" shadow="never">
<div class="mb-4">
<admin-page-data-panel v-loading="pager.loading">
<template #toolbar>
<el-button type="primary" @click="handleAdd">新增粉丝</el-button>
</div>
<el-table :data="pager.lists" size="large" v-loading="pager.loading">
</template>
<el-table :data="pager.lists" size="large">
<el-table-column label="ID" prop="id" width="70" />
<el-table-column label="姓名" prop="name" min-width="100" />
<el-table-column label="手机号" prop="phone" min-width="130" />
@@ -81,10 +81,10 @@
</template>
</el-table-column>
</el-table>
<div class="flex mt-4 justify-end">
<template #footer>
<pagination v-model="pager" @change="getLists" />
</div>
</el-card>
</template>
</admin-page-data-panel>
<!-- 新增/编辑粉丝弹窗 -->
<el-dialog
+3 -3
View File
@@ -3704,7 +3704,7 @@ function getYejiDeptComboOption(tb: YejiTable) {
const cats = rows.map(r =>
r.dept_name.length > 10 ? `${r.dept_name.slice(0, 10)}…` : r.dept_name
)
const palette = ['#64748b', '#38bdf8', '#6366f1']
const palette = ['#64748b', '#38bdf8', '#0d9488']
const series: any[] = [
{
name: '进线',
@@ -4029,8 +4029,8 @@ onMounted(async () => {
.yeji-page {
min-width: 0;
--yj-brand: #6366f1;
--yj-brand-soft: #eef2ff;
--yj-brand: #0d9488;
--yj-brand-soft: #f0fdfa;
--yj-teal: #0ea5e9;
--yj-teal-soft: #f0f9ff;
--yj-accent: #f43f5e;
+7 -7
View File
@@ -1,6 +1,6 @@
<template>
<div>
<el-card class="!border-none" shadow="never">
<admin-page-filter-panel>
<el-alert
type="warning"
title="温馨提示:用户账户变动记录"
@@ -38,9 +38,9 @@
<el-button @click="resetParams">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<el-card class="!border-none mt-4" shadow="never">
<el-table size="large" v-loading="pager.loading" :data="pager.lists">
</admin-page-filter-panel>
<admin-page-data-panel v-loading="pager.loading">
<el-table size="large" :data="pager.lists">
<el-table-column label="用户账号" prop="account" min-width="100" />
<el-table-column label="用户昵称" min-width="160">
<template #default="{ row }">
@@ -71,10 +71,10 @@
<el-table-column label="来源单号" prop="source_sn" min-width="100" />
<el-table-column label="记录时间" prop="create_time" min-width="120" />
</el-table>
<div class="flex justify-end mt-4">
<template #footer>
<pagination v-model="pager" @change="getLists" />
</div>
</el-card>
</template>
</admin-page-data-panel>
</div>
</template>
<script lang="ts" setup name="balanceDetail">
+7 -7
View File
@@ -1,6 +1,6 @@
<template>
<div>
<el-card class="!border-none" shadow="never">
<admin-page-filter-panel>
<el-alert
type="warning"
title="温馨提示:用户充值记录"
@@ -54,9 +54,9 @@
/>
</el-form-item>
</el-form>
</el-card>
<el-card class="!border-none mt-4" shadow="never">
<el-table size="large" v-loading="pager.loading" :data="pager.lists">
</admin-page-filter-panel>
<admin-page-data-panel v-loading="pager.loading">
<el-table size="large" :data="pager.lists">
<el-table-column label="用户信息" min-width="160">
<template #default="{ row }">
<div class="flex items-center">
@@ -104,10 +104,10 @@
</template>
</el-table-column>
</el-table>
<div class="flex justify-end mt-4">
<template #footer>
<pagination v-model="pager" @change="getLists" />
</div>
</el-card>
</template>
</admin-page-data-panel>
</div>
</template>
<script lang="ts" setup name="rechargeRecord">
+7 -7
View File
@@ -20,7 +20,7 @@
</div>
</div>
</el-card>
<el-card class="!border-none" shadow="never">
<admin-page-filter-panel>
<el-form ref="formRef" class="mb-[-16px] mt-[16px]" :model="queryParams" :inline="true">
<el-form-item class="w-[280px]" label="退款单号">
<el-input
@@ -69,8 +69,8 @@
/> -->
</el-form-item>
</el-form>
</el-card>
<el-card class="!border-none mt-4" shadow="never">
</admin-page-filter-panel>
<admin-page-data-panel>
<el-tabs v-model="activeTab" @tab-change="handleTabChange">
<el-tab-pane
v-for="(item, index) in tabLists"
@@ -78,7 +78,7 @@
:name="index"
:key="index"
>
<el-table size="large" v-loading="pager.loading" :data="pager.lists">
<el-table size="large" :data="pager.lists">
<el-table-column label="退款单号" prop="sn" min-width="190" />
<el-table-column label="用户信息" min-width="160">
<template #default="{ row }">
@@ -140,10 +140,10 @@
</el-tab-pane>
</el-tabs>
<div class="flex justify-end mt-4">
<template #footer>
<pagination v-model="pager" @change="getLists" />
</div>
</el-card>
</template>
</admin-page-data-panel>
<refund-log v-model="showRefundLog" :refund-id="selectRefundId" />
</div>
</template>
+5 -5
View File
@@ -1,14 +1,14 @@
<template>
<div>
<el-card class="!border-none" shadow="never">
<admin-page-filter-panel>
<el-alert
type="warning"
title="温馨提示:平台配置在各个场景下的通知发送方式和内容模板"
:closable="false"
show-icon
></el-alert>
</el-card>
<el-card class="!border-none mt-4" shadow="never">
</admin-page-filter-panel>
<admin-page-data-panel v-loading="pager.loading">
<el-tabs v-model="tabsActive" @tab-change="getLists">
<el-tab-pane
v-for="(item, index) in tabsMap"
@@ -18,7 +18,7 @@
lazy
></el-tab-pane>
</el-tabs>
<el-table size="large" :data="pager.lists" v-loading="pager.loading">
<el-table size="large" :data="pager.lists" >
<el-table-column label="通知场景" prop="scene_name" min-width="120" />
<el-table-column label="通知类型" prop="type_desc" min-width="160" />
<el-table-column label="短信通知" min-width="80">
@@ -44,7 +44,7 @@
</template>
</el-table-column>
</el-table>
</el-card>
</admin-page-data-panel>
</div>
</template>
<script lang="ts" setup name="notice">
@@ -1,6 +1,6 @@
<template>
<div>
<el-card class="!border-none" shadow="never" v-loading="state.loading">
<admin-page-data-panel v-loading="state.loading">
<el-table size="large" :data="state.lists">
<el-table-column label="短信渠道" prop="name" min-width="120" />
<el-table-column label="状态" min-width="120">
@@ -22,7 +22,7 @@
</template>
</el-table-column>
</el-table>
</el-card>
</admin-page-data-panel>
<edit-popup ref="editRef" @success="getLists" />
</div>
</template>
@@ -33,13 +33,11 @@ import EditPopup from './edit.vue'
const editRef = shallowRef<InstanceType<typeof EditPopup>>()
// 列表数据
const state = reactive({
loading: false,
lists: []
})
// 获取存储引擎列表数据
const getLists = async () => {
try {
state.loading = true
+8 -12
View File
@@ -1,7 +1,7 @@
<!-- 订单列表 -->
<template>
<div class="order-list">
<el-card class="!border-none" shadow="never">
<div>
<admin-page-filter-panel>
<!-- 搜索表单 -->
<el-form class="ls-form" :model="queryParams" inline>
<el-form-item class="w-[280px]" label="订单号">
@@ -106,10 +106,10 @@
</el-button>
</el-form-item>
</el-form>
</el-card>
</admin-page-filter-panel>
<!-- Tab 筛选 + 今日收益 -->
<el-card class="!border-none mt-4" shadow="never">
<!-- Tab 筛选 + 今日收益 + 数据表格 -->
<admin-page-data-panel v-loading="pager.loading">
<div class="flex items-center justify-between mb-4">
<el-tabs v-model="patientAssociationTab" @tab-change="handleTabChange">
<el-tab-pane label="全部" name="" />
@@ -122,10 +122,7 @@
<span class="text-gray-400">{{ todayRevenue.count }} </span>
</div>
</div>
</el-card>
<!-- 数据表格 -->
<el-card class="!border-none mt-4" shadow="never">
<div v-perms="['order.order/zhipai']" class="mb-3 flex items-center gap-3">
<el-button type="primary" :disabled="!selectedOrderIds.length" @click="openAssignDialog">
将创建人指给医助
@@ -136,7 +133,6 @@
</div>
<el-table
ref="orderTableRef"
v-loading="pager.loading"
:data="pager.lists"
row-key="id"
size="large"
@@ -283,10 +279,10 @@
</el-table-column>
</el-table>
<div class="flex justify-end mt-4">
<template #footer>
<pagination v-model="pager" @change="getLists" />
</div>
</el-card>
</template>
</admin-page-data-panel>
<!-- 详情弹窗 -->
<el-dialog
@@ -1,6 +1,6 @@
<template>
<div class="department">
<el-card class="!border-none" shadow="never">
<div>
<admin-page-filter-panel>
<el-form ref="formRef" class="mb-[-16px]" :model="queryParams" :inline="true">
<el-form-item class="w-[280px]" label="部门名称" prop="name">
<el-input
@@ -22,22 +22,21 @@
<el-button @click="resetParams">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<el-card class="!border-none mt-4" shadow="never">
<div>
</admin-page-filter-panel>
<admin-page-data-panel v-loading="loading">
<template #toolbar>
<el-button v-perms="['dept.dept/add']" type="primary" @click="handleAdd()">
<template #icon>
<icon name="el-icon-Plus" />
</template>
新增
</el-button>
<el-button @click="handleExpand"> 展开/折叠 </el-button>
</div>
<el-button @click="handleExpand">展开/折叠</el-button>
</template>
<el-table
ref="tableRef"
class="mt-4"
size="large"
v-loading="loading"
:data="lists"
row-key="id"
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
@@ -68,7 +67,6 @@
}}</el-tag>
</template>
</el-table-column>
<el-table-column label="排序" prop="sort" min-width="100" />
<el-table-column label="更新时间" prop="update_time" min-width="180" />
<el-table-column label="操作" width="160" fixed="right">
@@ -101,7 +99,7 @@
</template>
</el-table-column>
</el-table>
</el-card>
</admin-page-data-panel>
<edit-popup v-if="showEdit" ref="editRef" @success="getLists" @close="showEdit = false" />
</div>
</template>
@@ -120,14 +118,18 @@ let isExpand = false
const loading = ref(false)
const lists = ref<any[]>([])
const queryParams = reactive({
status: '',
name: ''
name: '',
status: ''
})
const showEdit = ref(false)
const getLists = async () => {
loading.value = true
lists.value = await deptLists(queryParams)
loading.value = false
try {
lists.value = await deptLists(queryParams)
} finally {
loading.value = false
}
}
const resetParams = () => {
+11 -10
View File
@@ -1,6 +1,6 @@
<template>
<div class="post-lists">
<el-card class="!border-none" shadow="never">
<div>
<admin-page-filter-panel>
<el-form ref="formRef" class="mb-[-16px]" :model="queryParams" :inline="true">
<el-form-item class="w-[280px]" label="岗位编码">
<el-input
@@ -31,17 +31,18 @@
/>
</el-form-item>
</el-form>
</el-card>
<el-card class="!border-none mt-4" shadow="never">
<div>
</admin-page-filter-panel>
<admin-page-data-panel v-loading="pager.loading">
<template #toolbar>
<el-button v-perms="['dept.jobs/add']" type="primary" @click="handleAdd()">
<template #icon>
<icon name="el-icon-Plus" />
</template>
新增
</el-button>
</div>
<el-table class="mt-4" size="large" v-loading="pager.loading" :data="pager.lists">
</template>
<el-table size="large" :data="pager.lists">
<el-table-column label="岗位编码" prop="code" min-width="100" />
<el-table-column label="岗位名称" prop="name" min-width="100" />
<el-table-column label="排序" prop="sort" min-width="100" />
@@ -75,10 +76,10 @@
</template>
</el-table-column>
</el-table>
<div class="flex justify-end mt-4">
<template #footer>
<pagination v-model="pager" @change="getLists" />
</div>
</el-card>
</template>
</admin-page-data-panel>
<edit-popup v-if="showEdit" ref="editRef" @success="getLists" @close="showEdit = false" />
</div>
</template>
+1 -1
View File
@@ -1275,7 +1275,7 @@ onUnmounted(() => {
.float-action.edit {
color: #fff;
background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%);
background: linear-gradient(135deg, #0d9488 0%, #0f766e 100%);
}
.float-action.edit:hover,
+73 -73
View File
@@ -1,6 +1,6 @@
<template>
<div class="admin">
<el-card class="!border-none" shadow="never">
<div>
<admin-page-filter-panel>
<el-form class="mb-[-16px]" :model="formData" inline>
<el-form-item class="w-[280px]" label="管理员账号">
<el-input
@@ -40,77 +40,78 @@
/>
</el-form-item>
</el-form>
</el-card>
<el-card v-loading="pager.loading" class="mt-4 !border-none" shadow="never">
<el-button v-perms="['auth.admin/add']" type="primary" @click="handleAdd">
<template #icon>
<icon name="el-icon-Plus" />
</template>
新增
</el-button>
<div class="mt-4">
<el-table :data="pager.lists" size="large">
<el-table-column label="ID" prop="id" min-width="60" />>
<el-table-column label="头像" min-width="100">
<template #default="{ row }">
<el-avatar :size="50" :src="row.avatar"></el-avatar>
</template>
</el-table-column>
<el-table-column label="账号" prop="account" min-width="100" />
<el-table-column label="名称" prop="name" min-width="100" />
<el-table-column
label="角色"
prop="role_name"
min-width="100"
show-tooltip-when-overflow
/>
<el-table-column
label="部门"
prop="dept_name"
min-width="100"
show-tooltip-when-overflow
/>
<el-table-column label="创建时间" prop="create_time" min-width="180" />
<el-table-column label="最近登录时间" prop="login_time" min-width="180" />
<el-table-column label="最近登录IP" prop="login_ip" min-width="120" />
<el-table-column label="状态" min-width="100" v-perms="['auth.admin/edit']">
<template #default="{ row }">
<el-switch
v-if="row.root != 1"
v-model="row.disable"
:active-value="0"
:inactive-value="1"
@change="changeStatus(row)"
/>
</template>
</el-table-column>
<el-table-column label="操作" width="120" fixed="right">
<template #default="{ row }">
<el-button
v-perms="['auth.admin/edit']"
type="primary"
link
@click="handleEdit(row)"
>
编辑
</el-button>
<el-button
v-if="row.root != 1"
v-perms="['auth.admin/delete']"
type="danger"
link
@click="handleDelete(row.id)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
</div>
<div class="flex mt-4 justify-end">
</admin-page-filter-panel>
<admin-page-data-panel v-loading="pager.loading">
<template #toolbar>
<el-button v-perms="['auth.admin/add']" type="primary" @click="handleAdd">
<template #icon>
<icon name="el-icon-Plus" />
</template>
新增
</el-button>
</template>
<el-table :data="pager.lists" size="large">
<el-table-column label="ID" prop="id" min-width="60" />
<el-table-column label="头像" min-width="100">
<template #default="{ row }">
<el-avatar :size="50" :src="row.avatar"></el-avatar>
</template>
</el-table-column>
<el-table-column label="账号" prop="account" min-width="100" />
<el-table-column label="名称" prop="name" min-width="100" />
<el-table-column
label="角色"
prop="role_name"
min-width="100"
show-tooltip-when-overflow
/>
<el-table-column
label="部门"
prop="dept_name"
min-width="100"
show-tooltip-when-overflow
/>
<el-table-column label="创建时间" prop="create_time" min-width="180" />
<el-table-column label="最近登录时间" prop="login_time" min-width="180" />
<el-table-column label="最近登录IP" prop="login_ip" min-width="120" />
<el-table-column label="状态" min-width="100" v-perms="['auth.admin/edit']">
<template #default="{ row }">
<el-switch
v-if="row.root != 1"
v-model="row.disable"
:active-value="0"
:inactive-value="1"
@change="changeStatus(row)"
/>
</template>
</el-table-column>
<el-table-column label="操作" width="120" fixed="right">
<template #default="{ row }">
<el-button
v-perms="['auth.admin/edit']"
type="primary"
link
@click="handleEdit(row)"
>
编辑
</el-button>
<el-button
v-if="row.root != 1"
v-perms="['auth.admin/delete']"
type="danger"
link
@click="handleDelete(row.id)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<template #footer>
<pagination v-model="pager" @change="getLists" />
</div>
</el-card>
</template>
</admin-page-data-panel>
<edit-popup v-if="showEdit" ref="editRef" @success="getLists" @close="showEdit = false" />
</div>
</template>
@@ -125,7 +126,6 @@ import feedback from '@/utils/feedback'
import EditPopup from './edit.vue'
const editRef = shallowRef<InstanceType<typeof EditPopup>>()
//
const formData = reactive({
account: '',
name: '',
+6 -8
View File
@@ -1,20 +1,18 @@
<template>
<div class="menu-lists">
<el-card class="!border-none" shadow="never">
<div>
<div>
<admin-page-data-panel v-loading="pager.loading">
<template #toolbar>
<el-button v-perms="['auth.menu/add']" type="primary" @click="handleAdd()">
<template #icon>
<icon name="el-icon-Plus" />
</template>
新增
</el-button>
<el-button @click="handleExpand"> 展开/折叠 </el-button>
</div>
<el-button @click="handleExpand">展开/折叠</el-button>
</template>
<el-table
ref="tableRef"
class="mt-4"
size="large"
v-loading="pager.loading"
:data="pager.lists"
row-key="id"
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
@@ -87,7 +85,7 @@
</template>
</el-table-column>
</el-table>
</el-card>
</admin-page-data-panel>
<edit-popup v-if="showEdit" ref="editRef" @success="getLists" @close="showEdit = false" />
</div>
</template>
+54 -59
View File
@@ -1,68 +1,64 @@
<template>
<div class="role-lists">
<el-card class="!border-none" shadow="never">
<div>
<div>
<admin-page-data-panel v-loading="pager.loading">
<template #toolbar>
<el-button v-perms="['auth.role/add']" type="primary" @click="handleAdd">
<template #icon>
<icon name="el-icon-Plus" />
</template>
新增
</el-button>
</div>
<div class="mt-4" v-loading="pager.loading">
<div>
<el-table :data="pager.lists" size="large">
<el-table-column prop="id" label="ID" min-width="100" />
<el-table-column prop="name" label="名称" min-width="150" />
<el-table-column
prop="desc"
label="备注"
min-width="150"
show-overflow-tooltip
/>
<el-table-column prop="sort" label="排序" min-width="100" />
<el-table-column label="数据范围" min-width="140">
<template #default="{ row }">
{{ dataScopeLabel(row.data_scope) }}
</template>
</el-table-column>
<el-table-column prop="num" label="管理员人数" min-width="100" />
<el-table-column prop="create_time" label="创建时间" min-width="180" />
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }">
<el-button
link
type="primary"
v-perms="['auth.role/edit']"
@click="handleEdit(row)"
>
编辑
</el-button>
<el-button
link
type="primary"
v-perms="['auth.role/edit']"
@click="handleAuth(row)"
>
分配权限
</el-button>
<el-button
v-perms="['auth.role/delete']"
link
type="danger"
@click="handleDelete(row.id)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
</div>
<div class="flex justify-end mt-4">
<pagination v-model="pager" @change="getLists" />
</div>
</div>
</el-card>
</template>
<el-table :data="pager.lists" size="large">
<el-table-column prop="id" label="ID" min-width="100" />
<el-table-column prop="name" label="名称" min-width="150" />
<el-table-column
prop="desc"
label="备注"
min-width="150"
show-overflow-tooltip
/>
<el-table-column prop="sort" label="排序" min-width="100" />
<el-table-column label="数据范围" min-width="140">
<template #default="{ row }">
{{ dataScopeLabel(row.data_scope) }}
</template>
</el-table-column>
<el-table-column prop="num" label="管理员人数" min-width="100" />
<el-table-column prop="create_time" label="创建时间" min-width="180" />
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }">
<el-button
link
type="primary"
v-perms="['auth.role/edit']"
@click="handleEdit(row)"
>
编辑
</el-button>
<el-button
link
type="primary"
v-perms="['auth.role/edit']"
@click="handleAuth(row)"
>
分配权限
</el-button>
<el-button
v-perms="['auth.role/delete']"
link
type="danger"
@click="handleDelete(row.id)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<template #footer>
<pagination v-model="pager" @change="getLists" />
</template>
</admin-page-data-panel>
<edit-popup v-if="showEdit" ref="editRef" @success="getLists" @close="showEdit = false" />
<auth-popup v-if="showAuth" ref="authRef" @success="getLists" @close="showAuth = false" />
</div>
@@ -115,7 +111,6 @@ const dataScopeLabel = (scope: number | string | null | undefined) => {
)
}
//
const handleDelete = async (id: number) => {
await feedback.confirm('确定要删除?')
await roleDelete({ id })
+54 -57
View File
@@ -1,6 +1,6 @@
<template>
<div class="dict-type">
<el-card class="!border-none" shadow="never">
<div>
<admin-page-filter-panel>
<el-page-header class="mb-4" content="数据管理" @back="$router.back()" />
<el-form ref="formRef" class="mb-[-16px]" :model="queryParams" inline>
<el-form-item class="w-[280px]" label="字典名称">
@@ -28,9 +28,10 @@
<el-button @click="resetParams">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<el-card class="!border-none mt-4" shadow="never">
<div>
</admin-page-filter-panel>
<admin-page-data-panel v-loading="pager.loading">
<template #toolbar>
<el-button
v-perms="['setting.dict.dict_data/add']"
type="primary"
@@ -52,58 +53,54 @@
</template>
删除
</el-button>
</div>
<div class="mt-4" v-loading="pager.loading">
<div>
<el-table
:data="pager.lists"
size="large"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" />
<el-table-column label="ID" prop="id" />
<el-table-column label="数据名称" prop="name" min-width="120" />
<el-table-column label="数据值" prop="value" min-width="120" />
<el-table-column label="状态">
<template v-slot="{ row }">
<el-tag v-if="row.status == 1">正常</el-tag>
<el-tag v-else type="danger">停用</el-tag>
</template>
</el-table-column>
<el-table-column
label="备注"
prop="remark"
min-width="120"
show-tooltip-when-overflow
/>
<el-table-column label="排序" prop="sort" />
<el-table-column label="操作" width="120" fixed="right">
<template #default="{ row }">
<el-button
v-perms="['setting.dict.dict_data/edit']"
link
type="primary"
@click="handleEdit(row)"
>
编辑
</el-button>
<el-button
v-perms="['setting.dict.dict_data/delete']"
link
type="danger"
@click="handleDelete(row.id)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
</div>
<div class="flex justify-end mt-4">
<pagination v-model="pager" @change="getLists" />
</div>
</div>
</el-card>
</template>
<el-table
:data="pager.lists"
size="large"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" />
<el-table-column label="ID" prop="id" />
<el-table-column label="数据名称" prop="name" min-width="120" />
<el-table-column label="数据值" prop="value" min-width="120" />
<el-table-column label="状态">
<template v-slot="{ row }">
<el-tag v-if="row.status == 1">正常</el-tag>
<el-tag v-else type="danger">停用</el-tag>
</template>
</el-table-column>
<el-table-column
label="备注"
prop="remark"
min-width="120"
show-tooltip-when-overflow
/>
<el-table-column label="排序" prop="sort" />
<el-table-column label="操作" width="120" fixed="right">
<template #default="{ row }">
<el-button
v-perms="['setting.dict.dict_data/edit']"
link
type="primary"
@click="handleEdit(row)"
>
编辑
</el-button>
<el-button
v-perms="['setting.dict.dict_data/delete']"
link
type="danger"
@click="handleDelete(row.id)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<template #footer>
<pagination v-model="pager" @change="getLists" />
</template>
</admin-page-data-panel>
<edit-popup v-if="showEdit" ref="editRef" @success="getLists" @close="showEdit = false" />
</div>
</template>
+65 -69
View File
@@ -1,6 +1,6 @@
<template>
<div class="dict-type">
<el-card class="!border-none" shadow="never">
<div>
<admin-page-filter-panel>
<el-form ref="formRef" class="mb-[-16px]" :model="queryParams" inline>
<el-form-item class="w-[280px]" label="字典名称">
<el-input v-model="queryParams.name" clearable @keyup.enter="resetPage" />
@@ -20,9 +20,10 @@
<el-button @click="resetParams">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<el-card class="!border-none mt-4" shadow="never">
<div>
</admin-page-filter-panel>
<admin-page-data-panel v-loading="pager.loading">
<template #toolbar>
<el-button
v-perms="['setting.dict.dict_type/add']"
type="primary"
@@ -44,69 +45,65 @@
</template>
删除
</el-button>
</div>
<div class="mt-4" v-loading="pager.loading">
<div>
<el-table
:data="pager.lists"
size="large"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" />
<el-table-column label="ID" prop="id" />
<el-table-column label="字典名称" prop="name" min-width="120" />
<el-table-column label="字典类型" prop="type" min-width="120" />
<el-table-column label="状态">
<template v-slot="{ row }">
<el-tag v-if="row.status == 1">正常</el-tag>
<el-tag v-else type="danger">停用</el-tag>
</template>
</el-table-column>
<el-table-column label="备注" prop="remark" show-tooltip-when-overflow />
<el-table-column label="创建时间" prop="create_time" min-width="180" />
<el-table-column label="操作" width="190" fixed="right">
<template #default="{ row }">
<el-button
v-perms="['setting.dict.dict_type/edit']"
link
type="primary"
@click="handleEdit(row)"
>
编辑
</el-button>
<el-button
v-perms="['setting.dict.dict_data/lists']"
type="primary"
link
>
<router-link
:to="{
path: getRoutePath('setting.dict.dict_data/lists'),
query: {
id: row.id
}
}"
>
数据管理
</router-link>
</el-button>
<el-button
v-perms="['setting.dict.dict_type/delete']"
link
type="danger"
@click="handleDelete(row.id)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
</div>
<div class="flex justify-end mt-4">
<pagination v-model="pager" @change="getLists" />
</div>
</div>
</el-card>
</template>
<el-table
:data="pager.lists"
size="large"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" />
<el-table-column label="ID" prop="id" />
<el-table-column label="字典名称" prop="name" min-width="120" />
<el-table-column label="字典类型" prop="type" min-width="120" />
<el-table-column label="状态">
<template v-slot="{ row }">
<el-tag v-if="row.status == 1">正常</el-tag>
<el-tag v-else type="danger">停用</el-tag>
</template>
</el-table-column>
<el-table-column label="备注" prop="remark" show-tooltip-when-overflow />
<el-table-column label="创建时间" prop="create_time" min-width="180" />
<el-table-column label="操作" width="190" fixed="right">
<template #default="{ row }">
<el-button
v-perms="['setting.dict.dict_type/edit']"
link
type="primary"
@click="handleEdit(row)"
>
编辑
</el-button>
<el-button
v-perms="['setting.dict.dict_data/lists']"
type="primary"
link
>
<router-link
:to="{
path: getRoutePath('setting.dict.dict_data/lists'),
query: {
id: row.id
}
}"
>
数据管理
</router-link>
</el-button>
<el-button
v-perms="['setting.dict.dict_type/delete']"
link
type="danger"
@click="handleDelete(row.id)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<template #footer>
<pagination v-model="pager" @change="getLists" />
</template>
</admin-page-data-panel>
<edit-popup v-if="showEdit" ref="editRef" @success="getLists" @close="showEdit = false" />
</div>
</template>
@@ -151,7 +148,6 @@ const handleEdit = async (data: any) => {
editRef.value?.setFormData(data)
}
//
const handleDelete = async (id: any[] | number) => {
await feedback.confirm('确定要删除?')
await dictTypeDelete({ id })
+2 -2
View File
@@ -1,6 +1,6 @@
<template>
<div class="conversion-stats-page">
<el-card class="!border-none" shadow="never">
<admin-page-filter-panel>
<el-form :inline="true" :model="queryParams" class="stats-filter-form">
<el-form-item label="统计维度">
<el-segmented
@@ -89,7 +89,7 @@
<el-button @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</el-card>
</admin-page-filter-panel>
<div class="stats-kpi-grid">
<div
+103 -68
View File
@@ -18,7 +18,7 @@
</span>
<div>
<div class="wb-section-name">订单统计</div>
<div class="wb-section-sub">{{ orderStatsDateRangeText }} · {{ orderStatsData.order_type_name || '' }}</div>
<div class="wb-section-sub">{{ orderStatsDateRangeText }} · {{ orderStatsData.order_type_name || '-' }}</div>
</div>
</div>
<div class="wb-toolbar">
@@ -821,8 +821,8 @@ onMounted(() => {
<style lang="scss" scoped>
.workbench-page {
min-height: 100%;
padding: 20px 20px 40px;
background: linear-gradient(160deg, #eef2ff 0%, #f8fafc 38%, #f1f5f9 100%);
padding: 4px 0 24px;
background: transparent;
}
.wb-hero {
@@ -830,56 +830,86 @@ onMounted(() => {
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 16px;
margin-bottom: 22px;
padding: 22px 26px;
border-radius: 20px;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.92) 0%, rgba(255, 255, 255, 0.65) 100%);
border: 1px solid rgba(255, 255, 255, 0.95);
box-shadow: 0 12px 40px rgba(15, 23, 42, 0.06);
gap: 20px;
margin-bottom: 24px;
padding: 28px 28px;
border-radius: var(--admin-radius-xl);
border: 1px solid var(--el-border-color-lighter);
background: var(--admin-surface-glass);
backdrop-filter: blur(16px);
box-shadow: var(--el-box-shadow-light);
position: relative;
overflow: hidden;
&::before {
content: '';
position: absolute;
inset: 0;
background:
linear-gradient(120deg, rgba(6, 182, 212, 0.14) 0%, transparent 45%),
linear-gradient(300deg, rgba(16, 185, 129, 0.1) 0%, transparent 40%);
pointer-events: none;
}
&::after {
content: '';
position: absolute;
inset: 0 0 auto;
height: 3px;
background: var(--admin-brand-gradient);
}
}
.wb-hero-text {
position: relative;
z-index: 1;
}
.wb-hero-title {
margin: 0 0 6px;
font-size: 26px;
font-weight: 700;
letter-spacing: 0.02em;
color: #0f172a;
margin: 0 0 8px;
font-size: 28px;
font-weight: 800;
letter-spacing: -0.03em;
background: var(--admin-brand-gradient);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.wb-hero-desc {
margin: 0;
font-size: 14px;
color: #64748b;
font-size: 15px;
color: var(--el-text-color-secondary);
}
.wb-refresh-btn {
box-shadow: 0 8px 20px rgba(59, 130, 246, 0.25);
position: relative;
z-index: 1;
box-shadow: var(--admin-brand-glow);
}
.wb-section {
margin-bottom: 20px;
border-radius: 20px;
background: rgba(255, 255, 255, 0.88);
border: 1px solid rgba(255, 255, 255, 0.9);
box-shadow: 0 8px 32px rgba(15, 23, 42, 0.05);
border-radius: var(--admin-radius-xl);
background: var(--admin-surface-elevated);
border: 1px solid var(--el-border-color-lighter);
box-shadow: var(--el-box-shadow);
overflow: hidden;
backdrop-filter: blur(10px);
}
.wb-section--diagnosis {
border-top: 3px solid transparent;
border-image: linear-gradient(90deg, #3b82f6, #60a5fa) 1;
border-image: linear-gradient(90deg, #06b6d4, #10b981) 1;
}
.wb-section--order {
border-top: 3px solid transparent;
border-image: linear-gradient(90deg, #8b5cf6, #a78bfa) 1;
border-image: linear-gradient(90deg, #0891b2, #6366f1) 1;
}
.wb-section--trend {
border-top: 3px solid transparent;
border-image: linear-gradient(90deg, #14b8a6, #2dd4bf) 1;
border-image: linear-gradient(90deg, #10b981, #06b6d4) 1;
}
.wb-section-head {
@@ -889,8 +919,8 @@ onMounted(() => {
justify-content: space-between;
gap: 12px;
padding: 18px 22px;
border-bottom: 1px solid rgba(226, 232, 240, 0.9);
background: linear-gradient(180deg, rgba(248, 250, 252, 0.9) 0%, rgba(255, 255, 255, 0) 100%);
border-bottom: 1px solid var(--el-border-color-extra-light);
background: var(--admin-brand-gradient-soft);
}
.wb-section-title {
@@ -903,38 +933,68 @@ onMounted(() => {
display: inline-flex;
align-items: center;
justify-content: center;
width: 44px;
height: 44px;
width: 46px;
height: 46px;
border-radius: 14px;
font-size: 22px;
color: #fff;
}
.wb-section-icon--blue {
background: linear-gradient(135deg, #3b82f6, #2563eb);
box-shadow: 0 8px 20px rgba(37, 99, 235, 0.35);
background: var(--admin-brand-gradient);
box-shadow: var(--admin-brand-glow);
}
.wb-section-icon--violet {
background: linear-gradient(135deg, #8b5cf6, #7c3aed);
box-shadow: 0 8px 20px rgba(124, 58, 237, 0.3);
background: linear-gradient(135deg, #0891b2, #6366f1);
box-shadow: 0 8px 24px rgba(99, 102, 241, 0.25);
}
.wb-section-icon--teal {
background: linear-gradient(135deg, #14b8a6, #0d9488);
box-shadow: 0 8px 20px rgba(13, 148, 136, 0.3);
background: linear-gradient(135deg, #10b981, #06b6d4);
box-shadow: 0 8px 24px rgba(16, 185, 129, 0.25);
}
.wb-kpi--blue {
background: linear-gradient(145deg, rgba(6, 182, 212, 0.12), rgba(16, 185, 129, 0.06));
border: 1px solid rgba(6, 182, 212, 0.22);
box-shadow: var(--el-box-shadow-lighter);
}
.wb-kpi--violet {
background: linear-gradient(145deg, rgba(99, 102, 241, 0.1), rgba(6, 182, 212, 0.06));
border: 1px solid rgba(99, 102, 241, 0.2);
box-shadow: var(--el-box-shadow-lighter);
}
.wb-kpi--amber {
background: linear-gradient(145deg, rgba(245, 158, 11, 0.12), rgba(251, 191, 36, 0.06));
border: 1px solid rgba(245, 158, 11, 0.24);
box-shadow: var(--el-box-shadow-lighter);
}
.wb-rank-strip {
display: flex;
flex-direction: column;
justify-content: center;
min-height: 112px;
padding: 14px 18px;
border-radius: var(--admin-radius-lg);
background: var(--admin-brand-gradient-soft);
border: 1px solid var(--el-border-color-lighter);
box-shadow: var(--el-box-shadow-lighter);
}
.wb-section-name {
font-size: 17px;
font-size: 16px;
font-weight: 600;
color: #0f172a;
color: var(--el-text-color-primary);
}
.wb-section-sub {
margin-top: 2px;
font-size: 12px;
color: #94a3b8;
color: var(--el-text-color-secondary);
}
.wb-toolbar {
@@ -968,21 +1028,6 @@ onMounted(() => {
box-sizing: border-box;
}
.wb-kpi--blue {
background: linear-gradient(145deg, #eff6ff 0%, #dbeafe 100%);
border: 1px solid rgba(59, 130, 246, 0.2);
}
.wb-kpi--violet {
background: linear-gradient(145deg, #f5f3ff 0%, #ede9fe 100%);
border: 1px solid rgba(139, 92, 246, 0.22);
}
.wb-kpi--amber {
background: linear-gradient(145deg, #fffbeb 0%, #fef3c7 100%);
border: 1px solid rgba(245, 158, 11, 0.25);
}
.wb-kpi-label {
font-size: 13px;
color: #64748b;
@@ -1007,17 +1052,6 @@ onMounted(() => {
color: #94a3b8;
}
.wb-rank-strip {
display: flex;
flex-direction: column;
justify-content: center;
min-height: 112px;
padding: 14px 18px;
border-radius: 16px;
background: #f8fafc;
border: 1px solid #e2e8f0;
}
.wb-rank-strip--compact {
min-height: 112px;
}
@@ -1080,9 +1114,10 @@ onMounted(() => {
.wb-chart-card {
height: 100%;
padding: 14px 16px 8px;
border-radius: 16px;
background: #fafbfc;
border: 1px solid #eef0f4;
border-radius: var(--admin-radius-lg);
background: var(--admin-surface-elevated);
border: 1px solid var(--el-border-color-lighter);
box-shadow: var(--el-box-shadow-lighter);
}
.wb-chart-card-head {
+16 -1
View File
@@ -77,7 +77,22 @@ module.exports = {
mask: 'var(--el-mask-color)'
},
fontFamily: {
sans: ['PingFang SC', 'Arial', 'Hiragino Sans GB', 'Microsoft YaHei', 'sans-serif']
sans: [
'PingFang SC',
'SF Pro Text',
'Segoe UI',
'Arial',
'Hiragino Sans GB',
'Microsoft YaHei',
'sans-serif'
]
},
borderRadius: {
sm: 'var(--admin-radius-sm)',
DEFAULT: 'var(--admin-radius-md)',
md: 'var(--admin-radius-md)',
lg: 'var(--admin-radius-lg)',
xl: 'var(--admin-radius-xl)'
},
boxShadow: {
DEFAULT: 'var(--el-box-shadow)',
File diff suppressed because one or more lines are too long
@@ -86,7 +86,6 @@ class AdminLogic extends BaseLogic
'enable_image_consult' => $params['enable_image_consult'] ?? 1,
'enable_video_consult' => $params['enable_video_consult'] ?? 1,
'enable_charge' => $params['enable_charge'] ?? 0,
'enable_beauty' => $params['enable_beauty'] ?? 1,
]);
// 角色
@@ -152,7 +151,6 @@ class AdminLogic extends BaseLogic
'enable_image_consult' => $params['enable_image_consult'] ?? 1,
'enable_video_consult' => $params['enable_video_consult'] ?? 1,
'enable_charge' => $params['enable_charge'] ?? 0,
'enable_beauty' => $params['enable_beauty'] ?? 1,
];
// 头像
@@ -288,7 +286,7 @@ class AdminLogic extends BaseLogic
'gender', 'age', 'phone', 'title', 'department',
'specialty', 'education', 'experience', 'honors',
'license_no', 'qualification_images', 'enable_image_consult', 'enable_video_consult', 'enable_charge',
'enable_beauty', 'work_wechat_userid'
'work_wechat_userid'
])->findOrEmpty($params['id'])->toArray();
// 将资质图片JSON字符串转换为数组,供前端组件使用
@@ -1 +1 @@
import r from"./error-BT3t0AXw.js";import{f as p,ak as i,I as m,a as e,aN as s,J as o}from"./@vue/runtime-core-C6bnekPw.js";import"./element-plus-CXrrycLn.js";import"./@vue/runtime-dom-DDAG46FW.js";import"./lodash-D3kF6u-c.js";import"./@vue/shared-mAAVTE9n.js";import"./@vue/reactivity-DiY1c2vO.js";import"./@element-plus/icons-vue-BAghejlR.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./vue-router-QlpZ4wdW.js";import"./index-CO3hhC4Q.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./@vueuse/core-1S4fJlii.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";const a="/admin/assets/no_perms-jDxcYpYC.png",n={class:"error404"},W=p({__name:"403",setup(c){return(_,t)=>(i(),m("div",n,[e(r,{code:"403",title:"您的账号权限不足,请联系管理员添加权限!","show-btn":!1},{content:s(()=>[...t[0]||(t[0]=[o("div",{class:"flex justify-center"},[o("img",{class:"w-[150px] h-[150px]",src:a,alt:""})],-1)])]),_:1})]))}});export{W as default};
import r from"./error-BUBKvVs4.js";import{f as p,ak as i,I as m,a as e,aN as s,J as o}from"./@vue/runtime-core-C6bnekPw.js";import"./element-plus-Bolc0EfP.js";import"./@vue/runtime-dom-DDAG46FW.js";import"./lodash-D3kF6u-c.js";import"./@vue/shared-mAAVTE9n.js";import"./@vue/reactivity-DiY1c2vO.js";import"./@element-plus/icons-vue-B0jSCQ-G.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./vue-router-QlpZ4wdW.js";import"./index-B6p-ZV3k.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./@vueuse/core-1S4fJlii.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";const a="/admin/assets/no_perms-jDxcYpYC.png",n={class:"error404"},W=p({__name:"403",setup(c){return(_,t)=>(i(),m("div",n,[e(r,{code:"403",title:"您的账号权限不足,请联系管理员添加权限!","show-btn":!1},{content:s(()=>[...t[0]||(t[0]=[o("div",{class:"flex justify-center"},[o("img",{class:"w-[150px] h-[150px]",src:a,alt:""})],-1)])]),_:1})]))}});export{W as default};
@@ -1 +1 @@
import o from"./error-BT3t0AXw.js";import{f as r,ak as t,I as m,a as p}from"./@vue/runtime-core-C6bnekPw.js";import"./element-plus-CXrrycLn.js";import"./@vue/runtime-dom-DDAG46FW.js";import"./lodash-D3kF6u-c.js";import"./@vue/shared-mAAVTE9n.js";import"./@vue/reactivity-DiY1c2vO.js";import"./@element-plus/icons-vue-BAghejlR.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./vue-router-QlpZ4wdW.js";import"./index-CO3hhC4Q.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./@vueuse/core-1S4fJlii.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";const i={class:"error404"},T=r({__name:"404",setup(e){return(a,s)=>(t(),m("div",i,[p(o,{code:"404",title:"哎呀,出错了!您访问的页面不存在…"})]))}});export{T as default};
import o from"./error-BUBKvVs4.js";import{f as r,ak as t,I as m,a as p}from"./@vue/runtime-core-C6bnekPw.js";import"./element-plus-Bolc0EfP.js";import"./@vue/runtime-dom-DDAG46FW.js";import"./lodash-D3kF6u-c.js";import"./@vue/shared-mAAVTE9n.js";import"./@vue/reactivity-DiY1c2vO.js";import"./@element-plus/icons-vue-B0jSCQ-G.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./vue-router-QlpZ4wdW.js";import"./index-B6p-ZV3k.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./@vueuse/core-1S4fJlii.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";const i={class:"error404"},T=r({__name:"404",setup(e){return(a,s)=>(t(),m("div",i,[p(o,{code:"404",title:"哎呀,出错了!您访问的页面不存在…"})]))}});export{T as default};
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
import{M as T,N as C,T as $,r as D,d as L,L as k}from"./element-plus-CXrrycLn.js";import{Y as E}from"./@element-plus/icons-vue-BAghejlR.js";import{a6 as P}from"./tcm-ujpoSvdA.js";import{f as A,w as B,ak as p,I as b,aP as F,G as h,aN as n,a as i,O as m,J as M}from"./@vue/runtime-core-C6bnekPw.js";import{Q as c}from"./@vue/shared-mAAVTE9n.js";import{y as V,n as w}from"./@vue/reactivity-DiY1c2vO.js";import{_ as K}from"./index-CO3hhC4Q.js";import"./@vue/runtime-dom-DDAG46FW.js";import"./lodash-D3kF6u-c.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./vue-router-QlpZ4wdW.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./@vueuse/core-1S4fJlii.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";const Y={class:"assign-log-panel"},q={key:1,class:"text-gray-400"},z=A({__name:"AssignLogPanel",props:{diagnosisId:{}},setup(N,{expose:v}){const _=N,d=w(!1),u=w([]);function y(o){const e=o.related_po_creator_name;if(e!=null&&String(e).trim()!=="")return String(e);const t=Number(o.related_po_creator_id);return Number.isFinite(t)&&t>0?`ID:${t}`:"—"}function x(o){const e=o.related_po_create_time_text;if(e!=null&&String(e).trim()!=="")return String(e);const t=Number(o.related_po_create_time);if(!Number.isFinite(t)||t<=0)return"—";const a=new Date(t*1e3);if(Number.isNaN(a.getTime()))return"—";const r=l=>String(l).padStart(2,"0");return`${a.getFullYear()}-${r(a.getMonth()+1)}-${r(a.getDate())} ${r(a.getHours())}:${r(a.getMinutes())}:${r(a.getSeconds())}`}function f(o,e){const t=e==="from"?"from_assistant_name":"to_assistant_name",a=e==="from"?"from_assistant_id":"to_assistant_id",r=o[t];if(r!=null&&String(r).trim()!==""&&String(r)!=="—")return String(r);const l=Number(o[a]);return Number.isFinite(l)&&l>0?`ID:${l}`:"—"}const g=async()=>{if(_.diagnosisId){d.value=!0;try{const o=await P({id:_.diagnosisId}),e=Array.isArray(o)?o:[];u.value=e}catch(o){console.error(o),u.value=[]}finally{d.value=!1}}};return B(()=>_.diagnosisId,()=>{g()},{immediate:!0}),v({refresh:g}),(o,e)=>{const t=C,a=$,r=L,l=D,S=T,I=k;return p(),b("div",Y,[F((p(),h(S,{data:u.value,border:"",stripe:"","empty-text":"暂无指派记录"},{default:n(()=>[i(t,{label:"操作时间",width:"175",prop:"create_time_text"}),i(t,{label:"原医助","min-width":"120"},{default:n(({row:s})=>[m(c(f(s,"from")),1)]),_:1}),i(t,{label:"新医助","min-width":"120"},{default:n(({row:s})=>[m(c(f(s,"to")),1)]),_:1}),i(t,{label:"继承",width:"72",align:"center"},{default:n(({row:s})=>[Number(s.is_inherit)===1?(p(),h(a,{key:0,type:"success",size:"small"},{default:n(()=>[...e[0]||(e[0]=[m("是",-1)])]),_:1})):(p(),b("span",q,"否"))]),_:1}),i(t,{label:"快照·业务单创建人","min-width":"130","show-overflow-tooltip":""},{default:n(({row:s})=>[m(c(y(s)),1)]),_:1}),i(t,{label:"快照·业务单创建时间",width:"190"},{header:n(()=>[e[1]||(e[1]=M("span",null,"快照·业务单创建时间",-1)),i(l,{placement:"top",content:"related_po_create_time:触发本次操作的处方业务订单 create_time;与原文助一致时表示医助创建订单时间"},{default:n(()=>[i(r,{class:"assign-log-col-hint"},{default:n(()=>[i(V(E))]),_:1})]),_:1})]),default:n(({row:s})=>[m(c(x(s)),1)]),_:1}),i(t,{label:"操作人",width:"110",prop:"operator_name"}),i(t,{label:"操作账号",width:"120",prop:"operator_account","show-overflow-tooltip":""}),i(t,{label:"IP",width:"130",prop:"ip","show-overflow-tooltip":""})]),_:1},8,["data"])),[[I,d.value]])])}}}),Ct=K(z,[["__scopeId","data-v-f670e3e6"]]);export{Ct as default};
import{M as T,N as C,T as $,r as D,d as L,L as k}from"./element-plus-Bolc0EfP.js";import{Y as E}from"./@element-plus/icons-vue-B0jSCQ-G.js";import{a6 as P}from"./tcm-Ba4j6pRH.js";import{f as A,w as B,ak as p,I as b,aP as F,G as h,aN as n,a as i,O as m,J as M}from"./@vue/runtime-core-C6bnekPw.js";import{Q as c}from"./@vue/shared-mAAVTE9n.js";import{y as V,n as w}from"./@vue/reactivity-DiY1c2vO.js";import{_ as K}from"./index-B6p-ZV3k.js";import"./@vue/runtime-dom-DDAG46FW.js";import"./lodash-D3kF6u-c.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./vue-router-QlpZ4wdW.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./@vueuse/core-1S4fJlii.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";const Y={class:"assign-log-panel"},q={key:1,class:"text-gray-400"},z=A({__name:"AssignLogPanel",props:{diagnosisId:{}},setup(N,{expose:v}){const _=N,d=w(!1),u=w([]);function y(o){const e=o.related_po_creator_name;if(e!=null&&String(e).trim()!=="")return String(e);const t=Number(o.related_po_creator_id);return Number.isFinite(t)&&t>0?`ID:${t}`:"—"}function x(o){const e=o.related_po_create_time_text;if(e!=null&&String(e).trim()!=="")return String(e);const t=Number(o.related_po_create_time);if(!Number.isFinite(t)||t<=0)return"—";const a=new Date(t*1e3);if(Number.isNaN(a.getTime()))return"—";const r=l=>String(l).padStart(2,"0");return`${a.getFullYear()}-${r(a.getMonth()+1)}-${r(a.getDate())} ${r(a.getHours())}:${r(a.getMinutes())}:${r(a.getSeconds())}`}function f(o,e){const t=e==="from"?"from_assistant_name":"to_assistant_name",a=e==="from"?"from_assistant_id":"to_assistant_id",r=o[t];if(r!=null&&String(r).trim()!==""&&String(r)!=="—")return String(r);const l=Number(o[a]);return Number.isFinite(l)&&l>0?`ID:${l}`:"—"}const g=async()=>{if(_.diagnosisId){d.value=!0;try{const o=await P({id:_.diagnosisId}),e=Array.isArray(o)?o:[];u.value=e}catch(o){console.error(o),u.value=[]}finally{d.value=!1}}};return B(()=>_.diagnosisId,()=>{g()},{immediate:!0}),v({refresh:g}),(o,e)=>{const t=C,a=$,r=L,l=D,S=T,I=k;return p(),b("div",Y,[F((p(),h(S,{data:u.value,border:"",stripe:"","empty-text":"暂无指派记录"},{default:n(()=>[i(t,{label:"操作时间",width:"175",prop:"create_time_text"}),i(t,{label:"原医助","min-width":"120"},{default:n(({row:s})=>[m(c(f(s,"from")),1)]),_:1}),i(t,{label:"新医助","min-width":"120"},{default:n(({row:s})=>[m(c(f(s,"to")),1)]),_:1}),i(t,{label:"继承",width:"72",align:"center"},{default:n(({row:s})=>[Number(s.is_inherit)===1?(p(),h(a,{key:0,type:"success",size:"small"},{default:n(()=>[...e[0]||(e[0]=[m("是",-1)])]),_:1})):(p(),b("span",q,"否"))]),_:1}),i(t,{label:"快照·业务单创建人","min-width":"130","show-overflow-tooltip":""},{default:n(({row:s})=>[m(c(y(s)),1)]),_:1}),i(t,{label:"快照·业务单创建时间",width:"190"},{header:n(()=>[e[1]||(e[1]=M("span",null,"快照·业务单创建时间",-1)),i(l,{placement:"top",content:"related_po_create_time:触发本次操作的处方业务订单 create_time;与原文助一致时表示医助创建订单时间"},{default:n(()=>[i(r,{class:"assign-log-col-hint"},{default:n(()=>[i(V(E))]),_:1})]),_:1})]),default:n(({row:s})=>[m(c(x(s)),1)]),_:1}),i(t,{label:"操作人",width:"110",prop:"operator_name"}),i(t,{label:"操作账号",width:"120",prop:"operator_account","show-overflow-tooltip":""}),i(t,{label:"IP",width:"130",prop:"ip","show-overflow-tooltip":""})]),_:1},8,["data"])),[[I,d.value]])])}}}),Ct=K(z,[["__scopeId","data-v-f670e3e6"]]);export{Ct as default};
@@ -1 +1 @@
import{i as L,L as T,N as V,T as D,M as z,W as M}from"./element-plus-CXrrycLn.js";import{ae as O}from"./tcm-ujpoSvdA.js";import{f as P,b as F,w as j,ak as a,I as n,a as i,aN as s,O as m,H as w,aP as A,G as g,F as H,J as R}from"./@vue/runtime-core-C6bnekPw.js";import{y as d,n as k}from"./@vue/reactivity-DiY1c2vO.js";import{Q as c}from"./@vue/shared-mAAVTE9n.js";import{_ as G}from"./index-CO3hhC4Q.js";import"./@vue/runtime-dom-DDAG46FW.js";import"./lodash-D3kF6u-c.js";import"./@element-plus/icons-vue-BAghejlR.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./vue-router-QlpZ4wdW.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./@vueuse/core-1S4fJlii.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";const J={class:"case-record-list"},Q={key:0,class:"mb-3 flex justify-end"},W={key:0},Y={key:1,class:"text-gray-400"},q={class:"void-detail text-xs text-gray-500 mt-1"},K=P({__name:"CaseRecordList",props:{diagnosisId:{type:Number,default:0},readOnly:{type:Boolean,default:!1}},emits:["view","openPrescription"],setup(y,{expose:x,emit:C}){const _=y,h=C,l=k([]),p=k(!1),u=async()=>{if(_.diagnosisId){p.value=!0;try{const e=await O({diagnosis_id:_.diagnosisId});l.value=Array.isArray(e)?e:[]}catch(e){console.error("获取病历记录失败:",e),l.value=[]}finally{p.value=!1}}},S=e=>{if(!e)return"";const t=new Date(e*1e3);return`${t.getFullYear()}-${String(t.getMonth()+1).padStart(2,"0")}-${String(t.getDate()).padStart(2,"0")} ${String(t.getHours()).padStart(2,"0")}:${String(t.getMinutes()).padStart(2,"0")}`},$=e=>{h("view",e)},B=()=>{h("openPrescription")};return F(()=>{u()}),j(()=>_.diagnosisId,()=>{u()}),x({refresh:u}),(e,t)=>{const b=L,r=V,v=D,E=z,N=M,I=T;return a(),n("div",J,[y.readOnly?w("",!0):(a(),n("div",Q,[i(b,{type:"primary",size:"small",onClick:B},{default:s(()=>[...t[0]||(t[0]=[m("开方",-1)])]),_:1})])),A((a(),g(E,{data:d(l),border:""},{default:s(()=>[i(r,{prop:"prescription_date",label:"就诊日期",width:"120"}),i(r,{prop:"visit_no",label:"门诊号",width:"120"}),i(r,{prop:"clinical_diagnosis",label:"临床诊断","min-width":"160","show-overflow-tooltip":""}),i(r,{label:"处方摘要","min-width":"180"},{default:s(({row:o})=>[o.herbs&&o.herbs.length?(a(),n("span",W,c(o.herbs.slice(0,3).map(f=>`${f.name}${f.dosage}`).join("、"))+c(o.herbs.length>3?"...":""),1)):(a(),n("span",Y,"—"))]),_:1}),i(r,{prop:"doctor_name",label:"医师",width:"90","show-overflow-tooltip":""}),i(r,{label:"状态",width:"140",align:"center"},{default:s(({row:o})=>[o.void_status===1?(a(),n(H,{key:0},[i(v,{type:"danger",size:"small"},{default:s(()=>[...t[1]||(t[1]=[m("已作废",-1)])]),_:1}),R("div",q,c(o.void_by_name||"—")+" "+c(S(o.void_time)),1)],64)):(a(),g(v,{key:1,type:"success",size:"small"},{default:s(()=>[...t[2]||(t[2]=[m("正常",-1)])]),_:1}))]),_:1}),i(r,{label:"操作",width:"120",fixed:"right"},{default:s(({row:o})=>[i(b,{link:"",type:"primary",size:"small",onClick:f=>$(o)},{default:s(()=>[...t[3]||(t[3]=[m(" 查看 ",-1)])]),_:1},8,["onClick"])]),_:1})]),_:1},8,["data"])),[[I,d(p)]]),!d(p)&&d(l).length===0?(a(),g(N,{key:1,description:"暂无病历记录,开方后会自动显示",class:"mt-4"})):w("",!0)])}}}),zt=G(K,[["__scopeId","data-v-043d2738"]]);export{zt as default};
import{i as L,L as T,N as V,T as D,M as z,W as M}from"./element-plus-Bolc0EfP.js";import{ae as O}from"./tcm-Ba4j6pRH.js";import{f as P,b as F,w as j,ak as a,I as n,a as i,aN as s,O as m,H as w,aP as A,G as g,F as H,J as R}from"./@vue/runtime-core-C6bnekPw.js";import{y as d,n as k}from"./@vue/reactivity-DiY1c2vO.js";import{Q as c}from"./@vue/shared-mAAVTE9n.js";import{_ as G}from"./index-B6p-ZV3k.js";import"./@vue/runtime-dom-DDAG46FW.js";import"./lodash-D3kF6u-c.js";import"./@element-plus/icons-vue-B0jSCQ-G.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./vue-router-QlpZ4wdW.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./@vueuse/core-1S4fJlii.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";const J={class:"case-record-list"},Q={key:0,class:"mb-3 flex justify-end"},W={key:0},Y={key:1,class:"text-gray-400"},q={class:"void-detail text-xs text-gray-500 mt-1"},K=P({__name:"CaseRecordList",props:{diagnosisId:{type:Number,default:0},readOnly:{type:Boolean,default:!1}},emits:["view","openPrescription"],setup(y,{expose:x,emit:C}){const _=y,h=C,l=k([]),p=k(!1),u=async()=>{if(_.diagnosisId){p.value=!0;try{const e=await O({diagnosis_id:_.diagnosisId});l.value=Array.isArray(e)?e:[]}catch(e){console.error("获取病历记录失败:",e),l.value=[]}finally{p.value=!1}}},S=e=>{if(!e)return"";const t=new Date(e*1e3);return`${t.getFullYear()}-${String(t.getMonth()+1).padStart(2,"0")}-${String(t.getDate()).padStart(2,"0")} ${String(t.getHours()).padStart(2,"0")}:${String(t.getMinutes()).padStart(2,"0")}`},$=e=>{h("view",e)},B=()=>{h("openPrescription")};return F(()=>{u()}),j(()=>_.diagnosisId,()=>{u()}),x({refresh:u}),(e,t)=>{const b=L,r=V,v=D,E=z,N=M,I=T;return a(),n("div",J,[y.readOnly?w("",!0):(a(),n("div",Q,[i(b,{type:"primary",size:"small",onClick:B},{default:s(()=>[...t[0]||(t[0]=[m("开方",-1)])]),_:1})])),A((a(),g(E,{data:d(l),border:""},{default:s(()=>[i(r,{prop:"prescription_date",label:"就诊日期",width:"120"}),i(r,{prop:"visit_no",label:"门诊号",width:"120"}),i(r,{prop:"clinical_diagnosis",label:"临床诊断","min-width":"160","show-overflow-tooltip":""}),i(r,{label:"处方摘要","min-width":"180"},{default:s(({row:o})=>[o.herbs&&o.herbs.length?(a(),n("span",W,c(o.herbs.slice(0,3).map(f=>`${f.name}${f.dosage}`).join("、"))+c(o.herbs.length>3?"...":""),1)):(a(),n("span",Y,"—"))]),_:1}),i(r,{prop:"doctor_name",label:"医师",width:"90","show-overflow-tooltip":""}),i(r,{label:"状态",width:"140",align:"center"},{default:s(({row:o})=>[o.void_status===1?(a(),n(H,{key:0},[i(v,{type:"danger",size:"small"},{default:s(()=>[...t[1]||(t[1]=[m("已作废",-1)])]),_:1}),R("div",q,c(o.void_by_name||"—")+" "+c(S(o.void_time)),1)],64)):(a(),g(v,{key:1,type:"success",size:"small"},{default:s(()=>[...t[2]||(t[2]=[m("正常",-1)])]),_:1}))]),_:1}),i(r,{label:"操作",width:"120",fixed:"right"},{default:s(({row:o})=>[i(b,{link:"",type:"primary",size:"small",onClick:f=>$(o)},{default:s(()=>[...t[3]||(t[3]=[m(" 查看 ",-1)])]),_:1},8,["onClick"])]),_:1})]),_:1},8,["data"])),[[I,d(p)]]),!d(p)&&d(l).length===0?(a(),g(N,{key:1,description:"暂无病历记录,开方后会自动显示",class:"mt-4"})):w("",!0)])}}}),zt=G(K,[["__scopeId","data-v-043d2738"]]);export{zt as default};
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
import{L as d}from"./element-plus-Bolc0EfP.js";import{f as i,aP as r,ak as e,I as o,aq as s,H as t,J as _}from"./@vue/runtime-core-C6bnekPw.js";const c={class:"admin-data-panel"},m={key:0,class:"admin-data-panel__toolbar"},p={class:"admin-data-panel__body"},f={key:1,class:"admin-data-panel__footer"},k=i({__name:"DataPanel",props:{loading:{type:Boolean,default:!1}},setup(n){return(a,u)=>{const l=d;return r((e(),o("section",c,[a.$slots.toolbar?(e(),o("div",m,[s(a.$slots,"toolbar")])):t("",!0),_("div",p,[s(a.$slots,"default")]),a.$slots.footer?(e(),o("div",f,[s(a.$slots,"footer")])):t("",!0)])),[[l,n.loading]])}}});export{k as _};
@@ -0,0 +1 @@
import{i as d}from"./element-plus-Bolc0EfP.js";import{f as p,ak as s,I as o,J as n,a as m,aN as _,O as f,H as u,aq as y}from"./@vue/runtime-core-C6bnekPw.js";import{o as k,Q as i}from"./@vue/shared-mAAVTE9n.js";import{y as a,n as b}from"./@vue/reactivity-DiY1c2vO.js";const B={key:0,class:"admin-filter-panel__head"},C={class:"admin-filter-panel__label"},N={class:"admin-filter-panel__body"},E=p({__name:"FilterPanel",props:{collapsible:{type:Boolean,default:!0},title:{default:"筛选条件"}},setup(t){const e=b(!1);return(r,l)=>{const c=d;return s(),o("section",{class:k(["admin-filter-panel",{"is-collapsed":a(e)}])},[t.collapsible?(s(),o("div",B,[n("span",C,i(t.title),1),m(c,{class:"admin-filter-panel__toggle",link:"",type:"primary",onClick:l[0]||(l[0]=h=>e.value=!a(e))},{default:_(()=>[f(i(a(e)?"展开筛选":"收起筛选"),1)]),_:1})])):u("",!0),n("div",N,[y(r.$slots,"default")])],2)}}});export{E as _};
@@ -1 +1 @@
import{_ as o}from"./MediaSourceSelect.vue_vue_type_script_setup_true_lang-CMN0ydu1.js";import"./element-plus-CXrrycLn.js";import"./@vue/runtime-dom-DDAG46FW.js";import"./lodash-D3kF6u-c.js";import"./@vue/runtime-core-C6bnekPw.js";import"./@vue/reactivity-DiY1c2vO.js";import"./@vue/shared-mAAVTE9n.js";import"./@element-plus/icons-vue-BAghejlR.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";export{o as default};
import{_ as o}from"./MediaSourceSelect.vue_vue_type_script_setup_true_lang-DBM4IcXw.js";import"./element-plus-Bolc0EfP.js";import"./@vue/runtime-dom-DDAG46FW.js";import"./lodash-D3kF6u-c.js";import"./@vue/runtime-core-C6bnekPw.js";import"./@vue/reactivity-DiY1c2vO.js";import"./@vue/shared-mAAVTE9n.js";import"./@element-plus/icons-vue-B0jSCQ-G.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";export{o as default};
@@ -1 +1 @@
import{Q as g,P as V}from"./element-plus-CXrrycLn.js";import{f as C,ak as o,G as s,aN as v,I as p,F as h,ap as B,H as k,A as m}from"./@vue/runtime-core-C6bnekPw.js";import{y as c}from"./@vue/reactivity-DiY1c2vO.js";import{p as w,o as S}from"./@vue/shared-mAAVTE9n.js";const N=C({__name:"MediaSourceSelect",props:{modelValue:{default:""},options:{},loading:{type:Boolean,default:!1},placeholder:{default:"请选择自媒体来源"},clearable:{type:Boolean,default:!0},filterable:{type:Boolean,default:!0},allowCreate:{type:Boolean,default:!1},disabled:{type:Boolean,default:!1},selectClass:{},selectStyle:{}},emits:["update:modelValue","change","visible-change"],setup(e,{emit:i}){const u=e,n=i,d=m(()=>(u.options||[]).map(l=>typeof l=="string"?{name:l}:{name:l.name,value:l.value})),f=m(()=>d.value.some(l=>l.name===u.modelValue)),b=l=>{n("visible-change",l)};return(l,t)=>{const r=g,y=V;return o(),s(y,{"model-value":e.modelValue,placeholder:e.placeholder,clearable:e.clearable,filterable:e.filterable,"allow-create":e.allowCreate,"default-first-option":e.allowCreate,disabled:e.disabled,loading:e.loading,class:S(e.selectClass),style:w(e.selectStyle),"onUpdate:modelValue":t[0]||(t[0]=a=>n("update:modelValue",a)),onChange:t[1]||(t[1]=a=>n("change",a)),onVisibleChange:b},{default:v(()=>[(o(!0),p(h,null,B(c(d),a=>(o(),s(r,{key:a.name,label:a.name,value:a.name},null,8,["label","value"]))),128)),e.modelValue&&!c(f)?(o(),s(r,{key:`__legacy_${e.modelValue}`,label:`${e.modelValue}(已停用)`,value:e.modelValue},null,8,["label","value"])):k("",!0)]),_:1},8,["model-value","placeholder","clearable","filterable","allow-create","default-first-option","disabled","loading","class","style"])}}});export{N as _};
import{Q as g,P as V}from"./element-plus-Bolc0EfP.js";import{f as C,ak as o,G as s,aN as v,I as p,F as h,ap as B,H as k,A as m}from"./@vue/runtime-core-C6bnekPw.js";import{y as c}from"./@vue/reactivity-DiY1c2vO.js";import{p as w,o as S}from"./@vue/shared-mAAVTE9n.js";const N=C({__name:"MediaSourceSelect",props:{modelValue:{default:""},options:{},loading:{type:Boolean,default:!1},placeholder:{default:"请选择自媒体来源"},clearable:{type:Boolean,default:!0},filterable:{type:Boolean,default:!0},allowCreate:{type:Boolean,default:!1},disabled:{type:Boolean,default:!1},selectClass:{},selectStyle:{}},emits:["update:modelValue","change","visible-change"],setup(e,{emit:i}){const u=e,n=i,d=m(()=>(u.options||[]).map(l=>typeof l=="string"?{name:l}:{name:l.name,value:l.value})),f=m(()=>d.value.some(l=>l.name===u.modelValue)),b=l=>{n("visible-change",l)};return(l,t)=>{const r=g,y=V;return o(),s(y,{"model-value":e.modelValue,placeholder:e.placeholder,clearable:e.clearable,filterable:e.filterable,"allow-create":e.allowCreate,"default-first-option":e.allowCreate,disabled:e.disabled,loading:e.loading,class:S(e.selectClass),style:w(e.selectStyle),"onUpdate:modelValue":t[0]||(t[0]=a=>n("update:modelValue",a)),onChange:t[1]||(t[1]=a=>n("change",a)),onVisibleChange:b},{default:v(()=>[(o(!0),p(h,null,B(c(d),a=>(o(),s(r,{key:a.name,label:a.name,value:a.name},null,8,["label","value"]))),128)),e.modelValue&&!c(f)?(o(),s(r,{key:`__legacy_${e.modelValue}`,label:`${e.modelValue}(已停用)`,value:e.modelValue},null,8,["label","value"])):k("",!0)]),_:1},8,["model-value","placeholder","clearable","filterable","allow-create","default-first-option","disabled","loading","class","style"])}}});export{N as _};
File diff suppressed because one or more lines are too long
@@ -1,2 +1,2 @@
import{i as se,W as ne,C as le,V as ae,v as re,d as me,a as de}from"./element-plus-CXrrycLn.js";import{_ as pe}from"./picker-DJROU0jC.js";import{e as ue,c as ce,i as g,_ as ge}from"./index-CO3hhC4Q.js";import{s as U}from"./@vue/runtime-dom-DDAG46FW.js";import{b as z,G as fe}from"./@element-plus/icons-vue-BAghejlR.js";import{a as P,d as ve}from"./patient-CXHiwyhn.js";import{h as _e}from"./perm-WF2yqSYy.js";import{f as ye,w as he,ak as o,I as n,G as k,aN as r,O as M,H as p,a as l,J as m,F as w,ap as E,A as ke}from"./@vue/runtime-core-C6bnekPw.js";import{n as f,y as N}from"./@vue/reactivity-DiY1c2vO.js";import{Q as T}from"./@vue/shared-mAAVTE9n.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./lodash-D3kF6u-c.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./index-f0T-c8zR.js";import"./index-x_mVhNP1.js";import"./index.vue_vue_type_script_setup_true_lang-iW1NfatD.js";import"./index-DjxDai3I.js";import"./index-CkJ0T1tI.js";import"./cos-js-sdk-v5-DSMN0LUT.js";import"./file-BdBRZBUu.js";import"./index.vue_vue_type_script_setup_true_lang-C6MkXIUg.js";import"./@vueuse/core-1S4fJlii.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./usePaging-VsbTxSU0.js";import"./vuedraggable-5bKFmC7X.js";import"./vue-BVs68q8v.js";import"./@vue/compiler-dom-CqayqxS7.js";import"./@vue/compiler-core-CvbQOXIO.js";import"./sortablejs-dsEAUXlE.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./vue-router-QlpZ4wdW.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";const we={class:"note-timeline-wrap"},Ie={key:0,class:"timeline-actions"},Ce={class:"upload-trigger"},xe={class:"upload-trigger"},Ve={key:1,class:"note-timeline"},Ee={class:"timeline-date"},Ne={class:"timeline-body"},be={key:0,class:"timeline-content"},Se={key:1,class:"timeline-images"},Ae={key:2,class:"timeline-images"},De={key:0,class:"thumb-wrap"},Be={key:1,class:"file-wrap"},Ue=["href","title"],ze={class:"file-name"},O=8e3,Pe=ye({__name:"NoteTimeline",props:{notes:{},diagnosisId:{},readonly:{type:Boolean}},emits:["refresh"],setup(d,{emit:j}){const u=d,I=j,X=ke(()=>_e(["doctor.appointment/addDoctorNote"])),v=f(!1),C=f(""),b=f(!1),H=ue(),_=t=>H.getImageUrl(t),S=f([]),A=f([]),y=f(0),h=f(0),J=["jpg","jpeg","png","gif","bmp","webp","svg"],D=t=>{var s;const e=((s=t.split(".").pop())==null?void 0:s.toLowerCase().split("?")[0])||"";return J.includes(e)},$=t=>{var s;const e=t.split("/");return decodeURIComponent(((s=e[e.length-1])==null?void 0:s.split("?")[0])||"文件")},Q=t=>t.filter(D).map(_),Z=(t,e)=>{const s=t.filter(D),x=t[e];return s.indexOf(x)},q=t=>t?t.split(`
import{i as se,W as ne,C as le,V as ae,v as re,d as me,a as de}from"./element-plus-Bolc0EfP.js";import{_ as pe}from"./picker-CB1VezxD.js";import{e as ue,c as ce,i as g,_ as ge}from"./index-B6p-ZV3k.js";import{s as U}from"./@vue/runtime-dom-DDAG46FW.js";import{b as z,G as fe}from"./@element-plus/icons-vue-B0jSCQ-G.js";import{a as P,d as ve}from"./patient-DcVrNNOW.js";import{h as _e}from"./perm-BkbQ-MqJ.js";import{f as ye,w as he,ak as o,I as n,G as k,aN as r,O as M,H as p,a as l,J as m,F as w,ap as E,A as ke}from"./@vue/runtime-core-C6bnekPw.js";import{n as f,y as N}from"./@vue/reactivity-DiY1c2vO.js";import{Q as T}from"./@vue/shared-mAAVTE9n.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./lodash-D3kF6u-c.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./index-BEsrPt83.js";import"./index-BidU9ngF.js";import"./index.vue_vue_type_script_setup_true_lang-EJPdzUeK.js";import"./index-CGJON9HS.js";import"./index-BK2tOS24.js";import"./cos-js-sdk-v5-DSMN0LUT.js";import"./file-BE-zRsES.js";import"./index.vue_vue_type_script_setup_true_lang-Lcfr_gGB.js";import"./@vueuse/core-1S4fJlii.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./usePaging-VsbTxSU0.js";import"./vuedraggable-5bKFmC7X.js";import"./vue-BVs68q8v.js";import"./@vue/compiler-dom-CqayqxS7.js";import"./@vue/compiler-core-CvbQOXIO.js";import"./sortablejs-dsEAUXlE.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./vue-router-QlpZ4wdW.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";const we={class:"note-timeline-wrap"},Ie={key:0,class:"timeline-actions"},Ce={class:"upload-trigger"},xe={class:"upload-trigger"},Ve={key:1,class:"note-timeline"},Ee={class:"timeline-date"},Ne={class:"timeline-body"},be={key:0,class:"timeline-content"},Se={key:1,class:"timeline-images"},Ae={key:2,class:"timeline-images"},De={key:0,class:"thumb-wrap"},Be={key:1,class:"file-wrap"},Ue=["href","title"],ze={class:"file-name"},O=8e3,Pe=ye({__name:"NoteTimeline",props:{notes:{},diagnosisId:{},readonly:{type:Boolean}},emits:["refresh"],setup(d,{emit:j}){const u=d,I=j,X=ke(()=>_e(["doctor.appointment/addDoctorNote"])),v=f(!1),C=f(""),b=f(!1),H=ue(),_=t=>H.getImageUrl(t),S=f([]),A=f([]),y=f(0),h=f(0),J=["jpg","jpeg","png","gif","bmp","webp","svg"],D=t=>{var s;const e=((s=t.split(".").pop())==null?void 0:s.toLowerCase().split("?")[0])||"";return J.includes(e)},$=t=>{var s;const e=t.split("/");return decodeURIComponent(((s=e[e.length-1])==null?void 0:s.split("?")[0])||"文件")},Q=t=>t.filter(D).map(_),Z=(t,e)=>{const s=t.filter(D),x=t[e];return s.indexOf(x)},q=t=>t?t.split(`
`).filter(Boolean):[],K=t=>{if(!u.diagnosisId)return;const e=Array.isArray(t)?t:[t];if(e.length<=y.value){y.value=e.length;return}const s=e.slice(y.value);y.value=e.length,s.length>0&&P({diagnosis_id:u.diagnosisId,tongue_images:s}).then(()=>{g.msgSuccess("舌苔照片已添加"),I("refresh")})},Y=t=>{if(!u.diagnosisId)return;const e=Array.isArray(t)?t:[t];if(e.length<=h.value){h.value=e.length;return}const s=e.slice(h.value);h.value=e.length,s.length>0&&P({diagnosis_id:u.diagnosisId,report_files:s}).then(()=>{g.msgSuccess("检查报告已添加"),I("refresh")})};he(()=>u.notes,()=>{S.value=[],A.value=[],y.value=0,h.value=0});const ee=async()=>{if(!u.diagnosisId){g.msgWarning("当前无诊单,无法添加备注");return}const t=C.value.trim();if(!t){g.msgWarning("请输入备注内容");return}b.value=!0;try{await P({diagnosis_id:u.diagnosisId,content:t}),g.msgSuccess("备注保存成功"),C.value="",v.value=!1,I("refresh")}catch(e){g.msgError((e==null?void 0:e.message)||"保存失败")}finally{b.value=!1}},B=async(t,e,s)=>{try{await de.confirm("确认删除?","提示",{type:"warning"})}catch{return}await ve({note_id:t,image_type:e,image_path:s}),g.msgSuccess("已删除"),I("refresh")};return(t,e)=>{const s=se,x=ce,F=pe,G=re,V=me,te=ne,ie=le,oe=ae;return o(),n("div",we,[!d.readonly&&d.diagnosisId?(o(),n("div",Ie,[X.value?(o(),k(s,{key:0,type:"primary",plain:"",size:"small",onClick:e[0]||(e[0]=i=>v.value=!0)},{default:r(()=>[...e[6]||(e[6]=[M(" 添加备注 ",-1)])]),_:1})):p("",!0),l(F,{modelValue:S.value,"onUpdate:modelValue":e[1]||(e[1]=i=>S.value=i),limit:99,type:"image","exclude-domain":!0,onChange:K},{upload:r(()=>[m("div",Ce,[l(x,{size:20,name:"el-icon-Plus"}),e[7]||(e[7]=m("span",null,"舌苔照片",-1))])]),_:1},8,["modelValue"]),l(F,{modelValue:A.value,"onUpdate:modelValue":e[2]||(e[2]=i=>A.value=i),limit:99,type:"file","exclude-domain":!0,onChange:Y},{upload:r(()=>[m("div",xe,[l(x,{size:20,name:"el-icon-Plus"}),e[8]||(e[8]=m("span",null,"检查报告",-1))])]),_:1},8,["modelValue"])])):p("",!0),d.notes.length?(o(),n("div",Ve,[(o(!0),n(w,null,E(d.notes,i=>{var L,R;return o(),n("div",{key:i.id,class:"timeline-node"},[e[11]||(e[11]=m("div",{class:"timeline-dot"},null,-1)),m("div",Ee,T(i.note_date),1),m("div",Ne,[i.content?(o(),n("div",be,[(o(!0),n(w,null,E(q(i.content),(a,c)=>(o(),n("div",{key:c,class:"content-line"},T(a),1))),128))])):p("",!0),(L=i.tongue_images)!=null&&L.length?(o(),n("div",Se,[e[9]||(e[9]=m("span",{class:"images-label"},"舌苔照片",-1)),(o(!0),n(w,null,E(i.tongue_images,(a,c)=>(o(),n("div",{key:c,class:"thumb-wrap"},[l(G,{src:_(a),"preview-src-list":i.tongue_images.map(_),"initial-index":c,"z-index":O,fit:"cover",class:"timeline-thumb","preview-teleported":""},null,8,["src","preview-src-list","initial-index"]),d.readonly?p("",!0):(o(),k(V,{key:0,class:"thumb-delete",onClick:U(W=>B(i.id,"tongue_images",a),["stop"])},{default:r(()=>[l(N(z))]),_:1},8,["onClick"]))]))),128))])):p("",!0),(R=i.report_files)!=null&&R.length?(o(),n("div",Ae,[e[10]||(e[10]=m("span",{class:"images-label"},"检查报告",-1)),(o(!0),n(w,null,E(i.report_files,(a,c)=>(o(),n(w,{key:c},[D(a)?(o(),n("div",De,[l(G,{src:_(a),"preview-src-list":Q(i.report_files),"initial-index":Z(i.report_files,c),"z-index":O,fit:"cover",class:"timeline-thumb","preview-teleported":""},null,8,["src","preview-src-list","initial-index"]),d.readonly?p("",!0):(o(),k(V,{key:0,class:"thumb-delete",onClick:U(W=>B(i.id,"report_files",a),["stop"])},{default:r(()=>[l(N(z))]),_:1},8,["onClick"]))])):(o(),n("div",Be,[m("a",{href:_(a),target:"_blank",class:"file-link",title:$(a)},[l(V,{size:20},{default:r(()=>[l(N(fe))]),_:1}),m("span",ze,T($(a)),1)],8,Ue),d.readonly?p("",!0):(o(),k(V,{key:0,class:"file-delete",onClick:U(W=>B(i.id,"report_files",a),["stop"])},{default:r(()=>[l(N(z))]),_:1},8,["onClick"]))]))],64))),128))])):p("",!0)])])}),128))])):p("",!0),!d.notes.length&&d.readonly?(o(),k(te,{key:2,description:"暂无备注","image-size":48})):p("",!0),l(oe,{modelValue:v.value,"onUpdate:modelValue":e[5]||(e[5]=i=>v.value=i),title:"添加备注",width:"480px","append-to-body":""},{footer:r(()=>[l(s,{onClick:e[4]||(e[4]=i=>v.value=!1)},{default:r(()=>[...e[12]||(e[12]=[M("取消",-1)])]),_:1}),l(s,{type:"primary",loading:b.value,onClick:ee},{default:r(()=>[...e[13]||(e[13]=[M("保存",-1)])]),_:1},8,["loading"])]),default:r(()=>[l(ie,{modelValue:C.value,"onUpdate:modelValue":e[3]||(e[3]=i=>C.value=i),type:"textarea",rows:4,placeholder:"输入今日备注...",maxlength:"500","show-word-limit":""},null,8,["modelValue"])]),_:1},8,["modelValue"])])}}}),Mt=ge(Pe,[["__scopeId","data-v-530ad386"]]);export{Mt as default};
@@ -1 +1 @@
import{m as x,f as y,a as B}from"./diag-display-DCz_VAqj.js";import{f as w,ak as I,I as P,J as a,H as N}from"./@vue/runtime-core-C6bnekPw.js";import{Q as i}from"./@vue/shared-mAAVTE9n.js";import{y as e}from"./@vue/reactivity-DiY1c2vO.js";import{_ as V}from"./index-CO3hhC4Q.js";import"./lodash-D3kF6u-c.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./vue-router-QlpZ4wdW.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./element-plus-CXrrycLn.js";import"./@vue/runtime-dom-DDAG46FW.js";import"./@element-plus/icons-vue-BAghejlR.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./@vueuse/core-1S4fJlii.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";const b={class:"card patient-card"},D={class:"patient-hero"},E={class:"patient-name"},G={class:"patient-meta"},H={key:0},J=w({__name:"PatientInfoCard",props:{apt:{default:()=>({})},diag:{default:()=>({})}},setup(t){return(Q,o)=>{var m,r,n,d,p,s,c,l,g,f,u,h,v,k,C;return I(),P("div",b,[o[0]||(o[0]=a("div",{class:"card-title"},"患者信息",-1)),a("div",D,[a("div",E,i(((m=t.apt)==null?void 0:m.patient_name)||"—"),1),a("div",G,[a("div",null,i(e(x)((r=t.apt)==null?void 0:r.patient_phone))+" · "+i(e(y)((n=t.diag)==null?void 0:n.gender))+" · "+i(((d=t.diag)==null?void 0:d.age)!=null?t.diag.age+"岁":"—"),1),a("div",null,i((p=t.diag)!=null&&p.height?t.diag.height+"cm":"—")+" / "+i((s=t.diag)!=null&&s.weight?t.diag.weight+"kg":"—")+" · "+i(((c=t.diag)==null?void 0:c.region)||"—"),1),a("div",null," 预约:"+i((l=t.apt)==null?void 0:l.appointment_date)+" "+i((g=t.apt)==null?void 0:g.appointment_time)+" · "+i(e(B)((f=t.apt)==null?void 0:f.period)),1),a("div",null,"医生:"+i(((u=t.apt)==null?void 0:u.doctor_name)||"—")+" 客服:"+i(((h=t.apt)==null?void 0:h.assistant_name)||"—"),1),a("div",null," 状态:"+i(((v=t.apt)==null?void 0:v.status_desc)||"—")+" · "+i((k=t.apt)!=null&&k.has_prescription?"已开方":"未开方"),1),(C=t.apt)!=null&&C.remark?(I(),P("div",H,"备注:"+i(t.apt.remark),1)):N("",!0)])])])}}}),Ct=V(J,[["__scopeId","data-v-1b0de09c"]]);export{Ct as default};
import{m as x,f as y,a as B}from"./diag-display-DCz_VAqj.js";import{f as w,ak as I,I as P,J as a,H as N}from"./@vue/runtime-core-C6bnekPw.js";import{Q as i}from"./@vue/shared-mAAVTE9n.js";import{y as e}from"./@vue/reactivity-DiY1c2vO.js";import{_ as V}from"./index-B6p-ZV3k.js";import"./lodash-D3kF6u-c.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./vue-router-QlpZ4wdW.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./element-plus-Bolc0EfP.js";import"./@vue/runtime-dom-DDAG46FW.js";import"./@element-plus/icons-vue-B0jSCQ-G.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./@vueuse/core-1S4fJlii.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";const b={class:"card patient-card"},D={class:"patient-hero"},E={class:"patient-name"},G={class:"patient-meta"},H={key:0},J=w({__name:"PatientInfoCard",props:{apt:{default:()=>({})},diag:{default:()=>({})}},setup(t){return(Q,o)=>{var m,r,n,d,p,s,c,l,g,f,u,h,v,k,C;return I(),P("div",b,[o[0]||(o[0]=a("div",{class:"card-title"},"患者信息",-1)),a("div",D,[a("div",E,i(((m=t.apt)==null?void 0:m.patient_name)||"—"),1),a("div",G,[a("div",null,i(e(x)((r=t.apt)==null?void 0:r.patient_phone))+" · "+i(e(y)((n=t.diag)==null?void 0:n.gender))+" · "+i(((d=t.diag)==null?void 0:d.age)!=null?t.diag.age+"岁":"—"),1),a("div",null,i((p=t.diag)!=null&&p.height?t.diag.height+"cm":"—")+" / "+i((s=t.diag)!=null&&s.weight?t.diag.weight+"kg":"—")+" · "+i(((c=t.diag)==null?void 0:c.region)||"—"),1),a("div",null," 预约:"+i((l=t.apt)==null?void 0:l.appointment_date)+" "+i((g=t.apt)==null?void 0:g.appointment_time)+" · "+i(e(B)((f=t.apt)==null?void 0:f.period)),1),a("div",null,"医生:"+i(((u=t.apt)==null?void 0:u.doctor_name)||"—")+" 客服:"+i(((h=t.apt)==null?void 0:h.assistant_name)||"—"),1),a("div",null," 状态:"+i(((v=t.apt)==null?void 0:v.status_desc)||"—")+" · "+i((k=t.apt)!=null&&k.has_prescription?"已开方":"未开方"),1),(C=t.apt)!=null&&C.remark?(I(),P("div",H,"备注:"+i(t.apt.remark),1)):N("",!0)])])])}}}),Ct=V(J,[["__scopeId","data-v-1b0de09c"]]);export{Ct as default};
@@ -1 +1 @@
import{W as B,M as A,N as R,T as j,i as q,L as z}from"./element-plus-CXrrycLn.js";import{_ as $}from"./index.vue_vue_type_script_setup_true_lang-iW1NfatD.js";import{u as G}from"./usePaging-VsbTxSU0.js";import{q as J}from"./tcm-ujpoSvdA.js";import M from"./PrescriptionOrderDetailDrawer-CIHvcvfO.js";import{w as Q,a as S,f as U}from"./prescription-order-utils-70xOmaF9.js";import{f as W,w as H,as as K,ak as m,I as u,a as e,F as X,aP as b,G as h,aN as a,J as w,O as p,A as y}from"./@vue/runtime-core-C6bnekPw.js";import{y as n,a as Y,n as Z,r as tt}from"./@vue/reactivity-DiY1c2vO.js";import{Q as d}from"./@vue/shared-mAAVTE9n.js";import{_ as et}from"./index-CO3hhC4Q.js";import"./@vue/runtime-dom-DDAG46FW.js";import"./lodash-D3kF6u-c.js";import"./@element-plus/icons-vue-BAghejlR.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./vue-router-QlpZ4wdW.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./@vueuse/core-1S4fJlii.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";const ot={class:"patient-order-list"},it={key:0,class:"po-empty-tip"},at={class:"text-red-500 font-semibold"},rt={class:"flex justify-end mt-3"},st=W({__name:"PatientOrderList",props:{diagnosisId:{},patientId:{}},setup(x,{expose:k}){const c=x,_=y(()=>{const o=Number(c.patientId);return Number.isFinite(o)&&o>0?o:0}),f=y(()=>_.value>0),l=tt({}),{pager:r,getLists:g,resetPage:I}=G({fetchFun:J,params:l,size:10}),N=()=>{Object.keys(l).forEach(o=>delete l[o]),c.diagnosisId>0&&(l.context_diagnosis_id=c.diagnosisId),f.value&&(l.patient_id=_.value),l.scene="diagnosis_edit"},v=Z();function D(o){var i;(i=v.value)==null||i.open(o)}const T=o=>{const i=Number(o);return Number.isFinite(i)?i.toFixed(2):"0.00"};return H(()=>[c.diagnosisId,_.value],()=>{if(!f.value){r.lists=[],r.count=0;return}N(),I()},{immediate:!0}),k({refresh:()=>g()}),(o,i)=>{const E=B,s=R,P=j,C=q,O=A,F=$,L=K("perms"),V=z;return m(),u("div",ot,[f.value?(m(),u(X,{key:1},[b((m(),h(O,{data:n(r).lists,border:"",stripe:"","empty-text":"暂无业务订单"},{default:a(()=>[e(s,{label:"订单编号",prop:"order_no","min-width":"200","show-overflow-tooltip":""}),e(s,{label:"金额",width:"120",align:"right"},{default:a(({row:t})=>[w("span",at,"¥"+d(T(t.amount)),1)]),_:1}),e(s,{label:"医生",prop:"doctor_name",width:"110","show-overflow-tooltip":""},{default:a(({row:t})=>[p(d(t.doctor_name||"—"),1)]),_:1}),e(s,{label:"医助",prop:"assistant_name",width:"110","show-overflow-tooltip":""},{default:a(({row:t})=>[p(d(t.assistant_name||"—"),1)]),_:1}),e(s,{label:"履约状态",width:"110"},{default:a(({row:t})=>[e(P,{type:n(Q)(t.fulfillment_status),size:"small",effect:"light"},{default:a(()=>[p(d(n(S)(t.fulfillment_status)),1)]),_:2},1032,["type"])]),_:1}),e(s,{label:"创建时间","min-width":"170"},{default:a(({row:t})=>[p(d(n(U)(t.create_time)),1)]),_:1}),e(s,{label:"操作",width:"100",fixed:"right"},{default:a(({row:t})=>[b((m(),h(C,{type:"primary",link:"",onClick:nt=>D(t.id)},{default:a(()=>[...i[1]||(i[1]=[p("查看详情",-1)])]),_:1},8,["onClick"])),[[L,["tcm.prescriptionOrder/detail"]]])]),_:1})]),_:1},8,["data"])),[[V,n(r).loading]]),w("div",rt,[e(F,{modelValue:n(r),"onUpdate:modelValue":i[0]||(i[0]=t=>Y(r)?r.value=t:null),onChange:n(g)},null,8,["modelValue","onChange"])])],64)):(m(),u("div",it,[e(E,{description:"当前诊单未携带患者ID,无法列出业务订单"})])),e(M,{ref_key:"detailDrawerRef",ref:v,readonly:"","append-to-body":""},null,512)])}}}),Kt=et(st,[["__scopeId","data-v-4a0ef613"]]);export{Kt as default};
import{W as B,M as A,N as R,T as j,i as q,L as z}from"./element-plus-Bolc0EfP.js";import{_ as $}from"./index.vue_vue_type_script_setup_true_lang-EJPdzUeK.js";import{u as G}from"./usePaging-VsbTxSU0.js";import{q as J}from"./tcm-Ba4j6pRH.js";import M from"./PrescriptionOrderDetailDrawer-CqEoe-Ic.js";import{w as Q,a as S,f as U}from"./prescription-order-utils-70xOmaF9.js";import{f as W,w as H,as as K,ak as m,I as u,a as e,F as X,aP as b,G as h,aN as a,J as w,O as p,A as y}from"./@vue/runtime-core-C6bnekPw.js";import{y as n,a as Y,n as Z,r as tt}from"./@vue/reactivity-DiY1c2vO.js";import{Q as d}from"./@vue/shared-mAAVTE9n.js";import{_ as et}from"./index-B6p-ZV3k.js";import"./@vue/runtime-dom-DDAG46FW.js";import"./lodash-D3kF6u-c.js";import"./@element-plus/icons-vue-B0jSCQ-G.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./vue-router-QlpZ4wdW.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./@vueuse/core-1S4fJlii.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";const ot={class:"patient-order-list"},it={key:0,class:"po-empty-tip"},at={class:"text-red-500 font-semibold"},rt={class:"flex justify-end mt-3"},st=W({__name:"PatientOrderList",props:{diagnosisId:{},patientId:{}},setup(x,{expose:k}){const c=x,_=y(()=>{const o=Number(c.patientId);return Number.isFinite(o)&&o>0?o:0}),f=y(()=>_.value>0),l=tt({}),{pager:r,getLists:g,resetPage:I}=G({fetchFun:J,params:l,size:10}),N=()=>{Object.keys(l).forEach(o=>delete l[o]),c.diagnosisId>0&&(l.context_diagnosis_id=c.diagnosisId),f.value&&(l.patient_id=_.value),l.scene="diagnosis_edit"},v=Z();function D(o){var i;(i=v.value)==null||i.open(o)}const T=o=>{const i=Number(o);return Number.isFinite(i)?i.toFixed(2):"0.00"};return H(()=>[c.diagnosisId,_.value],()=>{if(!f.value){r.lists=[],r.count=0;return}N(),I()},{immediate:!0}),k({refresh:()=>g()}),(o,i)=>{const E=B,s=R,P=j,C=q,O=A,F=$,L=K("perms"),V=z;return m(),u("div",ot,[f.value?(m(),u(X,{key:1},[b((m(),h(O,{data:n(r).lists,border:"",stripe:"","empty-text":"暂无业务订单"},{default:a(()=>[e(s,{label:"订单编号",prop:"order_no","min-width":"200","show-overflow-tooltip":""}),e(s,{label:"金额",width:"120",align:"right"},{default:a(({row:t})=>[w("span",at,"¥"+d(T(t.amount)),1)]),_:1}),e(s,{label:"医生",prop:"doctor_name",width:"110","show-overflow-tooltip":""},{default:a(({row:t})=>[p(d(t.doctor_name||"—"),1)]),_:1}),e(s,{label:"医助",prop:"assistant_name",width:"110","show-overflow-tooltip":""},{default:a(({row:t})=>[p(d(t.assistant_name||"—"),1)]),_:1}),e(s,{label:"履约状态",width:"110"},{default:a(({row:t})=>[e(P,{type:n(Q)(t.fulfillment_status),size:"small",effect:"light"},{default:a(()=>[p(d(n(S)(t.fulfillment_status)),1)]),_:2},1032,["type"])]),_:1}),e(s,{label:"创建时间","min-width":"170"},{default:a(({row:t})=>[p(d(n(U)(t.create_time)),1)]),_:1}),e(s,{label:"操作",width:"100",fixed:"right"},{default:a(({row:t})=>[b((m(),h(C,{type:"primary",link:"",onClick:nt=>D(t.id)},{default:a(()=>[...i[1]||(i[1]=[p("查看详情",-1)])]),_:1},8,["onClick"])),[[L,["tcm.prescriptionOrder/detail"]]])]),_:1})]),_:1},8,["data"])),[[V,n(r).loading]]),w("div",rt,[e(F,{modelValue:n(r),"onUpdate:modelValue":i[0]||(i[0]=t=>Y(r)?r.value=t:null),onChange:n(g)},null,8,["modelValue","onChange"])])],64)):(m(),u("div",it,[e(E,{description:"当前诊单未携带患者ID,无法列出业务订单"})])),e(M,{ref_key:"detailDrawerRef",ref:v,readonly:"","append-to-body":""},null,512)])}}}),Kt=et(st,[["__scopeId","data-v-4a0ef613"]]);export{Kt as default};
@@ -1 +1 @@
import{S as L}from"./element-plus-CXrrycLn.js";import B from"./RecordingVideoPlayer-BZKhGYiH.js";import{e as H,_ as I}from"./index-CO3hhC4Q.js";import{f as w,ak as n,I as m,J as p,F as v,G as u,aN as _,O as k,H as y,ap as R,A as d}from"./@vue/runtime-core-C6bnekPw.js";import{Q as q}from"./@vue/shared-mAAVTE9n.js";import"./@vue/runtime-dom-DDAG46FW.js";import"./lodash-D3kF6u-c.js";import"./@vue/reactivity-DiY1c2vO.js";import"./@element-plus/icons-vue-BAghejlR.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./vue-router-QlpZ4wdW.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./@vueuse/core-1S4fJlii.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";const C={key:0,class:"recording-list"},N={class:"recording-item"},P={key:1,class:"recording-alternates"},V={class:"recording-alternates__links"},A={key:1,class:"text-gray-400"},E=w({__name:"RecordingPlaybackBlock",props:{recordId:{},urls:{}},setup(c){const $=c,h=H(),l=d(()=>{const e=$.urls||[],t=new Set,r=[];for(const s of e){const o=String(s??"").trim();!o||t.has(o)||(t.add(o),r.push(o))}return r}),a=d(()=>{const e=l.value;if(!e.length)return null;const t=e.find(i=>/\.mp4(\?|#|$)/i.test(i));if(t)return t;const r=e.find(i=>/\.m3u8(\?|#|$)/i.test(i)&&/vod-qcloud\.com/i.test(i));if(r)return r;const s=e.find(i=>/\.m3u8(\?|#|$)/i.test(i)&&/\.cos\.[^/]+\.myqcloud\.com/i.test(i));if(s)return s;const o=e.find(i=>/\.m3u8(\?|#|$)/i.test(i));return o||e[0]}),f=d(()=>{const e=l.value,t=a.value;return t?e.filter(r=>r!==t):e.slice(1)});function S(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 b(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=L;return l.value.length?(n(),m("div",C,[p("div",N,[a.value?(n(),m(v,{key:0},[S(a.value)?(n(),u(B,{key:`${c.recordId}-${a.value}`,src:a.value},null,8,["src"])):(n(),u(r,{key:1,href:g(a.value),target:"_blank",type:"primary"},{default:_(()=>[...t[0]||(t[0]=[k(" 打开回放 ",-1)])]),_:1},8,["href"]))],64)):y("",!0),f.value.length?(n(),m("div",P,[t[1]||(t[1]=p("span",{class:"recording-alternates__label"},"备用地址",-1)),p("div",V,[(n(!0),m(v,null,R(f.value,(s,o)=>(n(),u(r,{key:`${c.recordId}-alt-${o}-${s.slice(-32)}`,href:g(s),target:"_blank",type:"primary",class:"recording-alternates__link"},{default:_(()=>[k(q(b(s,o)),1)]),_:2},1032,["href"]))),128))])])):y("",!0)])])):(n(),m("span",A,"暂无"))}}}),ht=I(E,[["__scopeId","data-v-d67b2e91"]]);export{ht as default};
import{S as L}from"./element-plus-Bolc0EfP.js";import B from"./RecordingVideoPlayer-BCuXkpfJ.js";import{e as H,_ as I}from"./index-B6p-ZV3k.js";import{f as w,ak as n,I as m,J as p,F as v,G as u,aN as _,O as k,H as y,ap as R,A as d}from"./@vue/runtime-core-C6bnekPw.js";import{Q as q}from"./@vue/shared-mAAVTE9n.js";import"./@vue/runtime-dom-DDAG46FW.js";import"./lodash-D3kF6u-c.js";import"./@vue/reactivity-DiY1c2vO.js";import"./@element-plus/icons-vue-B0jSCQ-G.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./vue-router-QlpZ4wdW.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./@vueuse/core-1S4fJlii.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";const C={key:0,class:"recording-list"},N={class:"recording-item"},P={key:1,class:"recording-alternates"},V={class:"recording-alternates__links"},A={key:1,class:"text-gray-400"},E=w({__name:"RecordingPlaybackBlock",props:{recordId:{},urls:{}},setup(c){const $=c,h=H(),l=d(()=>{const e=$.urls||[],t=new Set,r=[];for(const s of e){const o=String(s??"").trim();!o||t.has(o)||(t.add(o),r.push(o))}return r}),a=d(()=>{const e=l.value;if(!e.length)return null;const t=e.find(i=>/\.mp4(\?|#|$)/i.test(i));if(t)return t;const r=e.find(i=>/\.m3u8(\?|#|$)/i.test(i)&&/vod-qcloud\.com/i.test(i));if(r)return r;const s=e.find(i=>/\.m3u8(\?|#|$)/i.test(i)&&/\.cos\.[^/]+\.myqcloud\.com/i.test(i));if(s)return s;const o=e.find(i=>/\.m3u8(\?|#|$)/i.test(i));return o||e[0]}),f=d(()=>{const e=l.value,t=a.value;return t?e.filter(r=>r!==t):e.slice(1)});function S(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 b(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=L;return l.value.length?(n(),m("div",C,[p("div",N,[a.value?(n(),m(v,{key:0},[S(a.value)?(n(),u(B,{key:`${c.recordId}-${a.value}`,src:a.value},null,8,["src"])):(n(),u(r,{key:1,href:g(a.value),target:"_blank",type:"primary"},{default:_(()=>[...t[0]||(t[0]=[k(" 打开回放 ",-1)])]),_:1},8,["href"]))],64)):y("",!0),f.value.length?(n(),m("div",P,[t[1]||(t[1]=p("span",{class:"recording-alternates__label"},"备用地址",-1)),p("div",V,[(n(!0),m(v,null,R(f.value,(s,o)=>(n(),u(r,{key:`${c.recordId}-alt-${o}-${s.slice(-32)}`,href:g(s),target:"_blank",type:"primary",class:"recording-alternates__link"},{default:_(()=>[k(q(b(s,o)),1)]),_:2},1032,["href"]))),128))])])):y("",!0)])])):(n(),m("span",A,"暂无"))}}}),ht=I(E,[["__scopeId","data-v-d67b2e91"]]);export{ht as default};
File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More