Compare commits

..
16 Commits
Author SHA1 Message Date
Your Name e8f68101c4 更新 2026-07-02 16:13:38 +08:00
Your Name cea9392239 Merge branch 'master' into 20260623 2026-07-02 16:08:14 +08:00
Your Name 3ea3c97c42 更新 2026-07-02 14:47:53 +08:00
Your Name efd058bce5 更新 2026-07-02 14:35:20 +08:00
long e538dd03a1 build 2026-07-02 11:57:54 +08:00
long 4a17fe8a0c 调整综合转化统计医生维度口径 2026-07-02 11:55:15 +08:00
Your Name e2a1e62867 更新 2026-06-29 09:28:00 +08:00
Your Name b6cdd20c56 新增 2026-06-29 09:25:29 +08:00
Your Name c1be0aa3e5 更新 2026-06-24 10:07:39 +08:00
Your Name 6c87b72ce4 更新 2026-06-23 11:31:49 +08:00
Your Name e28480af32 更新 2026-06-16 16:16:31 +08:00
Your Name 37b2945c44 更新 2026-06-16 15:47:44 +08:00
Your Name c9c2b4608f 更新 2026-06-16 15:23:59 +08:00
Your Name e61e7f97fa 更新 2026-06-15 10:37:10 +08:00
Your Name 14e5bba9a2 更新 2026-06-15 09:18:59 +08:00
Your Name 6b713a2d6c 更新 2026-06-13 15:00:43 +08:00
1079 changed files with 6255 additions and 1133 deletions
@@ -1,11 +1,14 @@
<template>
<view class="food-tile-icon" :class="[`food-tile-icon--${size}`]">
<text class="food-tile-icon__emoji">{{ fallbackIcon }}</text>
<view
class="food-tile-icon"
:class="[`food-tile-icon--${size}`, { 'is-custom-img': hasCustomImg }]"
>
<text v-if="showEmojiFallback" class="food-tile-icon__emoji">{{ fallbackIcon }}</text>
<image
v-if="src && !imageFailed"
class="food-tile-icon__img"
:src="src"
mode="aspectFit"
:mode="imageMode"
@error="onImageError"
/>
</view>
@@ -27,12 +30,20 @@ const fallbackIcon = computed(() => {
return '🍽'
})
const hasCustomImg = computed(() => typeof props.food === 'object' && !!props.food?.img)
const src = computed(() => {
if (typeof props.food === 'object' && props.food?.img) return props.food.img
if (typeof props.food === 'object' && props.food?.icon) return getFoodImgUrl(props.food.icon)
return ''
})
/** 有可用图片时不渲染 emoji,避免 PNG 透明区域露出底层符号 */
const showEmojiFallback = computed(() => !src.value || imageFailed.value)
/** 自定义素材用 aspectFill 铺满,减少边缘露底 */
const imageMode = computed(() => (hasCustomImg.value ? 'aspectFill' : 'aspectFit'))
watch(
() => src.value,
() => {
@@ -71,6 +82,11 @@ function onImageError() {
z-index: 2;
}
.food-tile-icon--tile.is-custom-img .food-tile-icon__img {
width: 82%;
height: 82%;
}
/* 玻璃格上的食材图片:投影增强立体感(参考稿) */
.food-tile-icon--tile .food-tile-icon__img {
filter: drop-shadow(0 6rpx 10rpx rgba(0, 0, 0, 0.16));
@@ -109,6 +125,11 @@ function onImageError() {
font-size: 34rpx;
}
.food-tile-icon--goal.is-custom-img .food-tile-icon__img,
.food-tile-icon--tooltip.is-custom-img .food-tile-icon__img {
border-radius: 8rpx;
}
.food-tile-icon__img {
position: absolute;
left: 0;
@@ -0,0 +1,189 @@
<template>
<view class="vsm" :class="[`vsm--${size}`, { 'vsm--active': active, 'vsm--on-dark': onDark }]">
<view class="vsm-body">
<view class="vsm-leaf" aria-hidden="true" />
<view class="vsm-face">
<view class="vsm-eye vsm-eye--l" />
<view class="vsm-eye vsm-eye--r" />
<view class="vsm-blush vsm-blush--l" />
<view class="vsm-blush vsm-blush--r" />
<view class="vsm-mouth" />
</view>
<view v-if="active" class="vsm-waves" aria-hidden="true">
<view class="vsm-wave vsm-wave--1" />
<view class="vsm-wave vsm-wave--2" />
<view class="vsm-wave vsm-wave--3" />
</view>
</view>
</view>
</template>
<script setup>
defineProps({
/** 正在按住说话 / 录音中 */
active: { type: Boolean, default: false },
/** 深色条背景上使用浅色卡通 */
onDark: { type: Boolean, default: false },
size: { type: String, default: 'md' }
})
</script>
<style scoped lang="scss">
.vsm {
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.vsm--md { width: 72rpx; height: 72rpx; }
.vsm--sm { width: 64rpx; height: 64rpx; }
.vsm-body {
position: relative;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.vsm-leaf {
position: absolute;
top: 2rpx;
left: 50%;
width: 16rpx;
height: 16rpx;
margin-left: -8rpx;
border-radius: 0 100% 0 100%;
background: #34d399;
transform: rotate(-18deg);
z-index: 2;
}
.vsm--on-dark .vsm-leaf {
background: #a7f3d0;
}
.vsm-face {
position: relative;
width: 52rpx;
height: 52rpx;
border-radius: 50%;
background: #fef9c3;
border: 3rpx solid #047857;
box-shadow: 0 4rpx 10rpx rgba(4, 120, 87, 0.18);
z-index: 1;
}
.vsm--sm .vsm-face {
width: 46rpx;
height: 46rpx;
}
.vsm--on-dark .vsm-face {
background: #fffbeb;
border-color: rgba(255, 255, 255, 0.85);
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.12);
}
.vsm-eye {
position: absolute;
top: 16rpx;
width: 6rpx;
height: 8rpx;
border-radius: 50%;
background: #064e3b;
}
.vsm--sm .vsm-eye { top: 14rpx; width: 5rpx; height: 7rpx; }
.vsm--on-dark .vsm-eye { background: #134e4a; }
.vsm-eye--l { left: 12rpx; }
.vsm-eye--r { right: 12rpx; }
.vsm--sm .vsm-eye--l { left: 10rpx; }
.vsm--sm .vsm-eye--r { right: 10rpx; }
.vsm-blush {
position: absolute;
top: 24rpx;
width: 10rpx;
height: 6rpx;
border-radius: 50%;
background: rgba(251, 113, 133, 0.45);
opacity: 0.85;
}
.vsm--sm .vsm-blush { top: 21rpx; width: 8rpx; height: 5rpx; }
.vsm-blush--l { left: 6rpx; }
.vsm-blush--r { right: 6rpx; }
.vsm--on-dark .vsm-blush { background: rgba(255, 255, 255, 0.35); }
.vsm-mouth {
position: absolute;
left: 50%;
bottom: 10rpx;
width: 18rpx;
height: 8rpx;
margin-left: -9rpx;
border-radius: 0 0 18rpx 18rpx;
background: #047857;
transform-origin: center top;
}
.vsm--sm .vsm-mouth {
bottom: 9rpx;
width: 16rpx;
height: 7rpx;
margin-left: -8rpx;
}
.vsm--on-dark .vsm-mouth { background: #ecfdf5; }
.vsm-waves {
position: absolute;
right: -2rpx;
top: 50%;
display: flex;
align-items: flex-end;
gap: 4rpx;
height: 28rpx;
margin-top: -14rpx;
z-index: 0;
}
.vsm-wave {
width: 5rpx;
border-radius: 4rpx;
background: #047857;
animation: vsm-wave-jump 0.72s ease-in-out infinite;
}
.vsm--on-dark .vsm-wave { background: rgba(255, 255, 255, 0.9); }
.vsm-wave--1 { height: 10rpx; animation-delay: 0s; }
.vsm-wave--2 { height: 18rpx; animation-delay: 0.12s; }
.vsm-wave--3 { height: 12rpx; animation-delay: 0.24s; }
.vsm--active .vsm-body {
animation: vsm-bob 0.9s ease-in-out infinite;
}
.vsm--active .vsm-mouth {
animation: vsm-talk 0.32s ease-in-out infinite alternate;
}
.vsm--active .vsm-eye {
animation: vsm-blink 2.4s ease-in-out infinite;
}
@keyframes vsm-bob {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-4rpx); }
}
@keyframes vsm-talk {
0% {
transform: scaleY(0.45);
border-radius: 0 0 18rpx 18rpx;
}
100% {
transform: scaleY(1.15);
height: 12rpx;
border-radius: 50%;
}
}
@keyframes vsm-blink {
0%, 42%, 46%, 100% { transform: scaleY(1); }
44% { transform: scaleY(0.15); }
}
@keyframes vsm-wave-jump {
0%, 100% { transform: scaleY(0.45); opacity: 0.55; }
50% { transform: scaleY(1); opacity: 1; }
}
</style>
@@ -2,6 +2,27 @@ import { ref, onUnmounted } from 'vue'
const MIN_HOLD_MS = 280
/** 短按/过早松手时插件常见错误,不应打扰用户 */
function isBenignSttError(msg, retcode) {
const m = String(msg || '').toLowerCase()
if (/internal voice data failed/i.test(m)) return true
if (/voice data failed/i.test(m)) return true
if (/recordfailed|record failed|record manager/i.test(m)) return true
if (/please stop after start/i.test(m)) return true
if (/no recognition|not start|未开始|无识别/i.test(m)) return true
const code = Number(retcode)
if (code === -30002 || code === -30003 || code === -30012) return true
return false
}
function friendlySttError(msg) {
const m = String(msg || '').trim()
if (!m || isBenignSttError(m)) return ''
if (/not allowed|auth|permission|权限|麦克风/i.test(m)) return '请允许使用麦克风'
if (/network|网络|timeout/i.test(m)) return '网络异常,请稍后再试'
return '语音识别失败,请重试'
}
// #ifdef MP-WEIXIN
// WechatSI 录音管理器是全局单例:回调只在模块级绑定一次,
// 再分发给「当前活跃实例」。否则跨页面(如 more → weekly)后,
@@ -112,7 +133,8 @@ export function useSpeechToText({ onResult, onSettle, onPartial, onError, showTo
function stopListening() {
// #ifdef MP-WEIXIN
const ownsRecording = sharedRecording && activeCtl === controller
if (wxRecordManager && (sttListening.value || startRequested || ownsRecording)) {
// 仅在识别已真正开始后再 stop,避免 -30012 / internal voice data failed
if (wxRecordManager && (sttListening.value || ownsRecording)) {
try {
wxRecordManager.stop()
} catch (e) {}
@@ -128,6 +150,7 @@ export function useSpeechToText({ onResult, onSettle, onPartial, onError, showTo
// #endif
startRequested = false
sttHolding.value = false
sttListening.value = false
}
@@ -171,7 +194,6 @@ export function useSpeechToText({ onResult, onSettle, onPartial, onError, showTo
}
try {
startRequested = true
sharedRecording = true
wxRecordManager.start({
duration: 60000,
lang: 'zh_CN'
@@ -251,22 +273,28 @@ export function useSpeechToText({ onResult, onSettle, onPartial, onError, showTo
// #endif
}
/** 松手结束:手指抬起 */
function endHoldSpeech() {
/** 松手结束:手指抬起force=true 时强制取消尚未真正开始的按住会话 */
function endHoldSpeech(force = false) {
let recording = false
// #ifdef MP-WEIXIN
recording = sharedRecording && activeCtl === controller
// #endif
if (!sttHolding.value && !sttListening.value && !startRequested && !recording) {
if (!force && !sttHolding.value && !sttListening.value && !startRequested && !recording) {
return
}
sttHolding.value = false
// 松手后不再补开排队的录音
pendingStartSession = 0
if (sttListening.value || startRequested || recording) {
if (sttListening.value || recording) {
stopListening()
return
}
if (startRequested) {
// start 已发出但 onStart 未到:取消会话,勿调 stop()
startRequested = false
holdSessionId += 1
return
}
holdSessionId += 1
}
@@ -276,9 +304,9 @@ export function useSpeechToText({ onResult, onSettle, onPartial, onError, showTo
const controller = {
handleStart() {
if (!sttHolding.value) {
try {
wxRecordManager.stop()
} catch (e) {}
// 用户已松手,忽略迟到的 onStart,不再 stop() 以免触发插件报错
startRequested = false
sttListening.value = false
return
}
startRequested = false
@@ -306,6 +334,7 @@ export function useSpeechToText({ onResult, onSettle, onPartial, onError, showTo
sttListening.value = false
startRequested = false
const msg = (res && res.msg) || ''
const retcode = res && (res.retcode ?? res.errCode ?? res.code)
// 上一段未停止就再次 start 触发:停止后若仍按住则自动重试,不打扰用户
if (/please stop after start/i.test(msg)) {
try {
@@ -316,12 +345,18 @@ export function useSpeechToText({ onResult, onSettle, onPartial, onError, showTo
}
pendingStartSession = 0
sttHolding.value = false
const heldMs = Date.now() - holdStartTs
if (isBenignSttError(msg, retcode) || heldMs < MIN_HOLD_MS) {
if (typeof onError === 'function') onError(msg || '')
return
}
// 上层(如一问一答流程)可接管错误并自行重试,返回 true 时不再弹默认提示
if (typeof onError === 'function') {
const handled = onError(msg || '')
if (handled === true) return
}
notify(msg || '语音识别失败')
const tip = friendlySttError(msg)
if (tip) notify(tip)
}
}
@@ -411,3 +446,5 @@ export function useSpeechToText({ onResult, onSettle, onPartial, onError, showTo
stopSpeechToText: endHoldSpeech
}
}
export { isBenignSttError, friendlySttError }
+20 -4
View File
@@ -63,7 +63,15 @@ const FOOD_TIP_OVERRIDE = {
grainMantou: '杂粮做的,比白馒头稳,仍要控量',
riceNoodleSoup: '米粉升糖快,汤粉要少吃',
friedNoodles: '油多又是精面,升糖快',
centuryEggCongee: '白粥熬得软烂,升糖很快,糖友要少喝'
centuryEggCongee: '白粥熬得软烂,升糖很快,糖友要少喝',
sandwich: '夹心面包精制碳多,升糖快',
eightTreasureCongee: '八宝粥甜糯,升糖快,要少喝',
milletCongee: '小米粥熬软升糖快,糖友控量',
shandongPancake: '煎饼加油面,升糖中等偏快',
cake: '蛋糕又甜又油,升糖快',
biscuit: '饼干又甜又碎,升糖快',
wonton: '馄饨面皮精白,升糖中等,别多吃',
blackCoffee: '不加糖的黑咖啡,升糖很低'
}
/** 含糖等级通用科普文案 */
@@ -109,7 +117,7 @@ const RAW_FOODS = [
// 中 GI
{ key: 'brownRice', icon: '🍙', name: '糙米饭', gi: 'mid', val: 4, color: '#C9A26B' },
{ key: 'sweetPotato', icon: '🍠', name: '番薯', gi: 'mid', val: 5, color: '#C75B39' },
{ key: 'sweetPotato', icon: '🍠', name: '番薯', gi: 'mid', val: 5, color: '#C75B39', img: `${GAMES_IMG_BASE}/%E7%95%AA%E8%96%AF.png` },
{ key: 'taro', icon: '🥔', name: '芋头', gi: 'mid', val: 5, color: '#B39DDB' },
{ key: 'fish', icon: '🐟', name: '鱼肉', gi: 'mid', val: 2, color: '#4FA3C7' },
{ key: 'chicken', icon: '🍗', name: '鸡肉', gi: 'mid', val: 3, color: '#D6A15A' },
@@ -126,16 +134,24 @@ const RAW_FOODS = [
{ key: 'wine', icon: '🍷', name: '红酒', gi: 'mid', val: 4, color: '#9B2C3B' },
{ key: 'beer', icon: '🍺', name: '啤酒', gi: 'mid', val: 5, color: '#E0A83E' },
{ key: 'coffee', icon: '☕', name: '咖啡', gi: 'mid', val: 2, color: '#6F4E37' },
{ key: 'blackCoffee', icon: '☕', name: '黑咖啡', gi: 'low', val: -1, color: '#4E342E', img: `${GAMES_IMG_BASE}/%E9%BB%91%E5%92%96%E5%95%A1.png` },
{ key: 'wonton', icon: '🥟', name: '馄饨', gi: 'mid', val: 5, color: '#E8DCC8', img: `${GAMES_IMG_BASE}/%E9%A6%84%E9%A5%A8.png` },
{ key: 'milletCongee', icon: '🥣', name: '小米粥', gi: 'mid', val: 5, color: '#F5E6B8', img: `${GAMES_IMG_BASE}/%E5%B0%8F%E7%B1%B3%E7%B2%A5.png` },
{ key: 'shandongPancake', icon: '🥞', name: '山东煎饼', gi: 'high', val: 16, color: '#D4A574', img: `${GAMES_IMG_BASE}/%E5%B1%B1%E4%B8%9C%E7%85%8E%E9%A5%BC.png` },
{ key: 'corn', icon: '🌽', name: '玉米', gi: 'mid', val: 5, color: '#F9C513' },
{ key: 'udon', icon: '🍲', name: '乌冬面', gi: 'mid', val: 5, color: '#D9B88F' },
{ key: 'milkOats', icon: '🥣', name: '奶冲麦片', gi: 'mid', val: 6, color: '#E8D9B5', img: `${GAMES_IMG_BASE}/%E5%A5%B6%E5%86%B2%E9%BA%A6%E7%89%87.png` },
{ key: 'grainMantou', icon: '🫓', name: '杂粮馒头', gi: 'mid', val: 5, color: '#C8A878', img: `${GAMES_IMG_BASE}/%E6%9D%82%E7%B2%AE%E9%A6%92%E5%A4%B4.png` },
// 高 GI
{ key: 'whiteRice', icon: '🍚', name: '白米饭', gi: 'high', val: 18, color: '#E0E0E0' },
{ key: 'whiteRice', icon: '🍚', name: '白米饭', gi: 'high', val: 18, color: '#E0E0E0', img: `${GAMES_IMG_BASE}/%E7%B1%B3%E9%A5%AD.png` },
{ key: 'whiteBread', icon: '🍞', name: '白面包', gi: 'high', val: 16, color: '#D9A85C' },
{ key: 'mantou', icon: '🥯', name: '馒头', gi: 'high', val: 17, color: '#ECE0C8' },
{ key: 'youtiao', icon: '🥖', name: '油条', gi: 'high', val: 20, color: '#D4943F' },
{ key: 'youtiao', icon: '🥖', name: '油条', gi: 'high', val: 20, color: '#D4943F', img: `${GAMES_IMG_BASE}/%E6%B2%B9%E6%9D%A1.png` },
{ key: 'sandwich', icon: '🥪', name: '三明治', gi: 'high', val: 16, color: '#D9B88F', img: `${GAMES_IMG_BASE}/%E4%B8%89%E6%98%8E%E6%B2%BB.png` },
{ key: 'biscuit', icon: '🍪', name: '饼干', gi: 'high', val: 18, color: '#C9A063', img: `${GAMES_IMG_BASE}/%E9%A5%BC%E5%B9%B2.png` },
{ key: 'cake', icon: '🎂', name: '蛋糕', gi: 'high', val: 22, color: '#F48FB1', img: `${GAMES_IMG_BASE}/%E8%9B%8B%E7%B3%95.png` },
{ key: 'eightTreasureCongee', icon: '🥣', name: '八宝粥', gi: 'high', val: 17, color: '#E8C4A0', img: `${GAMES_IMG_BASE}/%E5%85%AB%E5%AE%9D%E7%B2%A5.png` },
{ key: 'donut', icon: '🍩', name: '甜甜圈', gi: 'high', val: 22, color: '#E87FB0' },
{ key: 'popcorn', icon: '🍿', name: '爆米花', gi: 'high', val: 18, color: '#F5E6B3' },
{ key: 'ramen', icon: '🍜', name: '拉面', gi: 'high', val: 17, color: '#E0A96D' },
+19 -2
View File
@@ -4,10 +4,27 @@
*/
/**
* 指定关卡固定食材(按 key)。未配置的关卡走随机抽取逻辑。
* 第一关固定为这 5 种带真实图片素材的食物。
* 第一关固定为这 16 种带真实图片素材的食物。
*/
const FIXED_LEVEL_FOODS = {
1: ['milkOats', 'grainMantou', 'riceNoodleSoup', 'friedNoodles', 'centuryEggCongee']
1: [
'sandwich',
'eightTreasureCongee',
'milkOats',
'milletCongee',
'shandongPancake',
'grainMantou',
'riceNoodleSoup',
'youtiao',
'friedNoodles',
'sweetPotato',
'centuryEggCongee',
'whiteRice',
'cake',
'biscuit',
'wonton',
'blackCoffee'
]
}
export function getLevelConfig(levelId) {
File diff suppressed because it is too large Load Diff
+258 -1
View File
@@ -639,7 +639,8 @@
}
.more-page .mc-tasks-section {
margin-top: 40rpx;
margin-top: 0;
margin-bottom: 32rpx;
}
.more-page .mc-tasks-grid {
@@ -1167,3 +1168,259 @@
font-size: 22rpx;
color: rgba(108, 122, 113, 0.6);
}
.more-page .mc-tree-species-btn {
display: inline-flex;
align-items: center;
gap: 4rpx;
margin-left: 8rpx;
padding: 4rpx 12rpx;
border-radius: 999rpx;
background: rgba(0, 108, 73, 0.08);
font-size: 20rpx;
font-weight: 700;
color: var(--primary);
}
.more-page .mc-water-panel,
.more-page .mc-species-panel {
width: calc(100% - 48rpx);
max-width: 680rpx;
margin: 0 auto;
position: absolute;
left: 0;
right: 0;
bottom: 0;
max-height: 78vh;
background: var(--surface);
border-radius: 32rpx 32rpx 0 0;
padding: 32rpx 28rpx calc(32rpx + env(safe-area-inset-bottom));
box-shadow: 0 -12rpx 40rpx rgba(15, 23, 42, 0.12);
}
.more-page .mc-water-panel-head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20rpx;
}
.more-page .mc-water-panel-title {
font-size: 34rpx;
font-weight: 800;
color: var(--on-surface);
}
.more-page .mc-water-panel-close {
width: 56rpx;
height: 56rpx;
border-radius: 50%;
background: var(--surface-container-low);
display: flex;
align-items: center;
justify-content: center;
font-size: 36rpx;
color: var(--outline);
}
.more-page .mc-water-panel-summary {
display: block;
font-size: 26rpx;
line-height: 1.5;
color: var(--on-surface-variant);
margin-bottom: 24rpx;
}
.more-page .mc-water-panel-list {
display: flex;
flex-direction: column;
gap: 12rpx;
margin-bottom: 20rpx;
}
.more-page .mc-water-panel-row {
display: flex;
align-items: center;
gap: 16rpx;
padding: 18rpx 16rpx;
border-radius: 20rpx;
background: var(--surface-container-low);
border: 1rpx solid rgba(0, 108, 73, 0.08);
}
.more-page .mc-water-panel-row.is-water {
border-color: rgba(0, 108, 73, 0.22);
background: rgba(0, 108, 73, 0.06);
}
.more-page .mc-water-panel-row.is-claimed {
opacity: 0.72;
}
.more-page .mc-water-panel-row-main {
flex: 1;
min-width: 0;
}
.more-page .mc-water-panel-row-name {
display: block;
font-size: 28rpx;
font-weight: 700;
color: var(--on-surface);
}
.more-page .mc-water-panel-row-sub {
display: block;
margin-top: 4rpx;
font-size: 22rpx;
color: var(--outline);
}
.more-page .mc-water-panel-row-tag {
flex-shrink: 0;
font-size: 22rpx;
font-weight: 700;
color: var(--primary);
}
.more-page .mc-water-panel-row.is-pending .mc-water-panel-row-tag {
color: #b45309;
}
.more-page .mc-water-panel-growth {
padding: 16rpx 18rpx;
border-radius: 16rpx;
background: rgba(0, 108, 73, 0.06);
margin-bottom: 20rpx;
font-size: 24rpx;
line-height: 1.45;
color: var(--primary);
}
.more-page .mc-water-panel-actions {
display: flex;
flex-direction: column;
gap: 12rpx;
}
.more-page .mc-water-panel-btn {
width: 100%;
height: 88rpx;
line-height: 88rpx;
border-radius: 999rpx;
font-size: 28rpx;
font-weight: 700;
background: var(--on-surface);
color: var(--surface);
border: none;
}
.more-page .mc-water-panel-btn.primary {
background: var(--primary);
color: var(--on-primary);
}
.more-page .mc-water-panel-btn.ghost {
background: var(--surface-container-low);
color: var(--on-surface);
}
.more-page .mc-species-panel-tip {
display: block;
font-size: 22rpx;
line-height: 1.45;
color: var(--outline);
margin-bottom: 20rpx;
}
.more-page .mc-species-scroll {
max-height: 56vh;
}
.more-page .mc-species-card {
padding: 20rpx;
border-radius: 24rpx;
border: 2rpx solid rgba(0, 108, 73, 0.1);
background: var(--surface-container-low);
margin-bottom: 16rpx;
}
.more-page .mc-species-card.active {
border-color: var(--primary);
background: rgba(0, 108, 73, 0.06);
}
.more-page .mc-species-card-head {
display: flex;
align-items: center;
gap: 16rpx;
margin-bottom: 16rpx;
}
.more-page .mc-species-card-emoji {
font-size: 56rpx;
line-height: 1;
}
.more-page .mc-species-card-name {
display: block;
font-size: 30rpx;
font-weight: 800;
color: var(--on-surface);
}
.more-page .mc-species-card-tagline {
display: block;
margin-top: 4rpx;
font-size: 22rpx;
color: var(--outline);
}
.more-page .mc-species-stages {
display: flex;
flex-wrap: wrap;
gap: 10rpx;
}
.more-page .mc-species-stage {
width: calc(20% - 8rpx);
min-width: 96rpx;
padding: 10rpx 6rpx;
border-radius: 16rpx;
background: rgba(255, 255, 255, 0.7);
text-align: center;
opacity: 0.45;
}
.more-page .mc-species-stage.reached {
opacity: 1;
}
.more-page .mc-species-stage.current {
box-shadow: 0 0 0 2rpx var(--primary);
background: rgba(0, 108, 73, 0.1);
}
.more-page .mc-species-stage-emoji {
display: block;
font-size: 32rpx;
line-height: 1.1;
}
.more-page .mc-species-stage-lv {
display: block;
font-size: 18rpx;
font-weight: 700;
color: var(--primary);
margin-top: 4rpx;
}
.more-page .mc-species-stage-name {
display: block;
font-size: 18rpx;
color: var(--outline);
margin-top: 2rpx;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
@@ -0,0 +1,75 @@
import { TREE_LEVELS, TREE_MAX_LEVEL, TREE_XP_PER_LEVEL } from './treeLevels.js'
/** 可选树种:每级 emoji 与 treeLevels.js Lv.09 一一对应 */
export const TREE_SPECIES_LIST = [
{
id: 'classic',
name: '稳糖灵树',
tagline: '经典路线,从种子到圆满',
stages: TREE_LEVELS.map((l) => l.emoji)
},
{
id: 'bonsai',
name: '雅韵盆景',
tagline: '小巧精致,书桌旁的温柔陪伴',
stages: ['🫘', '🌱', '🪴', '🎋', '🪴', '🌳', '🎍', '🌸', '🏵️', '🏆']
},
{
id: 'pine',
name: '苍劲青松',
tagline: '挺拔向上,越记越稳',
stages: ['🫘', '🌱', '🌿', '🌲', '🌲', '🌲', '⛰️', '🌲', '🏔️', '🏆']
},
{
id: 'sakura',
name: '樱花小树',
tagline: '坚持打卡,枝头渐开',
stages: ['🫘', '🌱', '🌿', '🌳', '🌸', '🌸', '🌸', '🌸', '🌺', '🏆']
},
{
id: 'ginkgo',
name: '金秋银杏',
tagline: '叶色渐变,见证每一天',
stages: ['🫘', '🌱', '🍃', '🌳', '🍂', '🍂', '🌳', '🍁', '🌟', '🏆']
}
]
export const TREE_SPECIES_STORAGE_KEY = 'tongji_tree_species_v1'
/** 四项任务全勤时每日最多可领积分(与后端 taskDefs 一致) */
export const DAILY_MAX_TASK_POINTS = 40
export function getTreeSpecies(id) {
return TREE_SPECIES_LIST.find((s) => s.id === id) || TREE_SPECIES_LIST[0]
}
export function speciesEmojiAtLevel(speciesId, level) {
const sp = getTreeSpecies(speciesId)
const lv = Math.min(TREE_MAX_LEVEL, Math.max(0, Number(level) || 0))
return sp.stages[lv] || sp.stages[0] || '🌱'
}
/** 当前树种成长路线图(供选择面板展示) */
export function speciesGrowthRoadmap(speciesId) {
const sp = getTreeSpecies(speciesId)
return TREE_LEVELS.map((meta, i) => {
const xpTotal = i * TREE_XP_PER_LEVEL
const daysHint = i === 0
? 0
: Math.max(1, Math.ceil(xpTotal / DAILY_MAX_TASK_POINTS))
return {
level: i,
name: meta.name,
emoji: sp.stages[i] || meta.emoji,
desc: meta.desc,
xpTotal,
daysHint
}
})
}
/** 全勤打卡大约多少天满级 */
export function estimateDaysToMaxLevel() {
const totalXp = TREE_MAX_LEVEL * TREE_XP_PER_LEVEL
return Math.max(1, Math.ceil(totalXp / DAILY_MAX_TASK_POINTS))
}
+2 -2
View File
@@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>中医问诊管理后台</title>
<title>后台管理系统</title>
<style>
* {
margin: 0;
@@ -29,7 +29,7 @@
stroke-dasharray: 90, 150;
stroke-dashoffset: 0;
stroke-width: 2;
stroke: #0EA5E9;
stroke: #4073fa;
stroke-linecap: round;
}
+15
View File
@@ -96,6 +96,21 @@ export function yejiStatsRevisitBreakdown(params: {
return request.get({ url: '/stats.yejiStats/revisitBreakdown', params })
}
/** 被指派数明细:与看板「被指派数」同口径;部门行传 dept_id,医助排行榜传 assistant_id */
export function yejiStatsAssignLines(params: {
start_date: string
end_date: string
dept_id?: number
assistant_id?: number
dept_ids?: string
channel_code?: string
tag_id?: string
page?: number
page_size?: number
}) {
return request.get({ url: '/stats.yejiStats/assignLines', params })
}
export function doctorDailyStatsOverview(params: {
start_date?: string
end_date?: string
+5
View File
@@ -446,6 +446,11 @@ export function prescriptionOrderRevokeRxAudit(params: { id: number }) {
return request.post({ url: '/tcm.prescriptionOrder/revokeRxAudit', params })
}
/** 批量将处方业务订单(创建人/医助归属)改派给其他医助 */
export function prescriptionOrderBatchAssignAssistant(params: { order_ids: number[]; assistant_id: number }) {
return request.post({ url: '/tcm.prescriptionOrder/batchAssignAssistant', params })
}
/** 撤回支付单审核 */
export function prescriptionOrderRevokePayAudit(params: { id: number }) {
return request.post({ url: '/tcm.prescriptionOrder/revokePayAudit', params })
+12 -15
View File
@@ -1,31 +1,28 @@
const defaultSetting = {
showCrumb: true, // 是否显示面包屑
showLogo: true, // 是否显示logo
showLogo: false, // 是否显示logo
isUniqueOpened: true, //只展开一个一级菜单
sideWidth: 200, //侧边栏宽度
sideTheme: 'light', //侧边栏主题
sideDarkColor: '#0369A1', //侧边栏深色主题颜色(深天蓝)
sideWidth: 183, //侧边栏宽度
sideTheme: 'dark', //侧边栏主题
sideDarkColor: '#1d2124', //侧边栏深色主题颜色
openMultipleTabs: true, // 是否开启多标签tab栏
theme: '#0EA5E9', //主题色(天蓝)
successTheme: '#16A34A', //成功主题色
warningTheme: '#D97706', //警告主题色
dangerTheme: '#DC2626', //危险主题色
errorTheme: '#DC2626', //错误主题色
infoTheme: '#64748B' //信息主题色
theme: '#4A5DFF', //主题色
successTheme: '#67c23a', //成功主题色
warningTheme: '#e6a23c', //警告主题色
dangerTheme: '#f56c6c', //危险主题色
errorTheme: '#f56c6c', //错误主题色
infoTheme: '#909399' //信息主题色
}
/** 本地 setting 缓存结构版本。提升后仅对低于该版本的老缓存执行 SETTING_SCHEMA_MIGRATIONS */
export const SETTING_SCHEMA_VERSION = 4
export const SETTING_SCHEMA_VERSION = 1
/**
* 按版本写入 defaultSetting 中的键(老用户 localStorage 会长期盖住 config 默认值)。
* 以后若要再推一批新默认值:把 SETTING_SCHEMA_VERSION +1,并为本版本追加一条迁移键列表。
*/
export const SETTING_SCHEMA_MIGRATIONS: Record<number, (keyof typeof defaultSetting)[]> = {
1: ['sideTheme', 'sideDarkColor'],
2: ['theme', 'successTheme', 'warningTheme', 'dangerTheme', 'errorTheme', 'infoTheme', 'sideTheme', 'sideDarkColor', 'showLogo', 'sideWidth'],
3: ['theme', 'sideTheme', 'sideDarkColor', 'sideWidth'],
4: ['theme', 'sideDarkColor']
1: ['sideTheme', 'sideDarkColor']
}
export default defaultSetting
@@ -26,19 +26,17 @@ useWatchRoute((route) => {
.app-breadcrumb {
:deep(.el-breadcrumb__item) {
.el-breadcrumb__inner {
color: var(--el-text-color-regular);
font-weight: 400;
transition: color 0.2s ease;
}
&:last-child .el-breadcrumb__inner {
color: var(--health-primary-dark, var(--el-color-primary));
color: #303133;
font-weight: 500;
}
&:last-child .el-breadcrumb__inner {
color: #303133;
}
}
:deep(.el-breadcrumb__separator) {
color: var(--el-text-color-secondary);
color: #606266;
}
}
</style>
@@ -1,14 +1,8 @@
<template>
<header class="header">
<div class="navbar">
<div class="navbar-left flex-1 flex items-center min-w-0">
<side-logo
v-if="settingStore.showLogo"
class="navbar-logo shrink-0"
:show-title="!isCollapsed || isMobile"
theme="light"
/>
<div class="navbar-item shrink-0">
<div class="flex-1 flex">
<div class="navbar-item">
<el-tooltip
class="box-item"
effect="dark"
@@ -18,19 +12,16 @@
<fold />
</el-tooltip>
</div>
<div class="navbar-item shrink-0">
<div class="navbar-item">
<el-tooltip class="box-item" effect="dark" content="刷新" placement="bottom">
<refresh />
</el-tooltip>
</div>
<div
class="flex items-center px-2 min-w-0"
v-if="!isMobile && settingStore.showCrumb"
>
<div class="flex items-center px-2" v-if="!isMobile && settingStore.showCrumb">
<breadcrumb />
</div>
</div>
<div class="navbar-right flex shrink-0">
<div class="flex">
<div class="navbar-item" v-if="!isMobile">
<el-tooltip
class="box-item"
@@ -56,6 +47,7 @@
</div>
</div>
</div>
<multiple-tabs v-if="settingStore.openMultipleTabs" />
</header>
</template>
@@ -65,11 +57,11 @@ import { useFullscreen } from '@vueuse/core'
import useAppStore from '@/stores/modules/app'
import useSettingStore from '@/stores/modules/setting'
import SideLogo from '../sidebar/logo.vue'
import Setting from '../setting/index.vue'
import Breadcrumb from './breadcrumb.vue'
import Fold from './fold.vue'
import FullScreen from './full-screen.vue'
import MultipleTabs from './multiple-tabs.vue'
import Refresh from './refresh.vue'
import UserDropDown from './user-drop-down.vue'
@@ -81,31 +73,11 @@ const { isFullscreen } = useFullscreen()
</script>
<style lang="scss">
.header {
background: var(--el-bg-color);
border-bottom: 1px solid var(--health-border, var(--el-border-color-lighter));
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.03);
}
.navbar {
height: var(--navbar-height);
@apply flex px-4;
.navbar-logo {
margin-right: 4px;
border-bottom: none !important;
padding-right: 8px !important;
}
@apply flex px-2 bg-body;
.navbar-item {
@apply h-10 flex justify-center items-center cursor-pointer;
min-width: 40px;
border-radius: 8px;
transition: background-color 0.2s ease;
&:hover {
background-color: var(--el-fill-color);
}
@apply h-full flex justify-center items-center hover:bg-page;
}
}
</style>
@@ -1,5 +1,5 @@
<template>
<div class="app-tabs flex bg-body">
<div class="app-tabs pl-4 flex bg-body">
<div class="flex-1 min-w-0">
<el-tabs
:model-value="currentTab"
@@ -13,7 +13,7 @@
</el-tabs>
</div>
<el-dropdown @command="handleCommand">
<span class="flex items-center px-3 cursor-pointer tab-dropdown">
<span class="flex items-center px-3">
<icon :size="16" name="el-icon-arrow-down" />
</span>
<template #dropdown>
@@ -58,22 +58,9 @@ const handleCommand = (command: any) => {
}
}
</script>
<style lang="scss" scoped>
.app-tabs {
padding: 0 16px;
background: var(--el-bg-color);
border-bottom: 1px solid var(--health-border, var(--el-border-color-lighter));
.tab-dropdown {
height: 40px;
border-radius: 6px;
transition: background-color 0.2s ease;
&:hover {
background-color: var(--el-fill-color);
}
}
@apply border-t border-br;
:deep(.el-tabs) {
height: 40px;
.el-tabs {
@@ -93,19 +80,35 @@ const handleCommand = (command: any) => {
}
&__item {
font-weight: normal;
padding: 0 16px !important;
padding: 0 15px !important;
box-sizing: border-box;
color: var(--el-text-color-secondary);
transition: color 0.2s ease;
&.is-active {
color: var(--health-primary-dark, var(--el-color-primary));
font-weight: 500;
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;
}
&::after {
position: absolute;
content: '';
display: block;
top: 0;
height: 2px;
left: 0;
width: 100%;
background-color: var(--el-color-primary);
}
}
.is-icon-close {
color: var(--el-text-color-secondary);
color: var(--el-text-color-regular);
vertical-align: -2px;
border-radius: 50%;
transition: all 0.2s ease;
&:hover {
color: var(--color-white);
background-color: var(--el-color-danger);
@@ -113,9 +116,7 @@ const handleCommand = (command: any) => {
}
}
&__active-bar {
height: 2px;
border-radius: 2px;
background-color: var(--health-primary, var(--el-color-primary));
display: none;
}
}
}
+3 -13
View File
@@ -1,7 +1,7 @@
<template>
<main class="main-wrap h-full">
<main class="main-wrap h-full bg-page">
<el-scrollbar>
<div class="main-content">
<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" />
@@ -24,14 +24,4 @@ const isRouteShow = computed(() => appStore.isRouteShow)
const includeList = computed(() => (settingStore.openMultipleTabs ? tabsStore.getCacheTabList : []))
</script>
<style lang="scss" scoped>
.main-wrap {
height: 100%;
background: var(--el-bg-color);
}
.main-content {
padding: 16px 20px 24px;
min-height: 100%;
}
</style>
<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(['#0EA5E9', '#38BDF8', '#0284C7', '#16A34A', '#DC2626', '#0369A1'])
const predefineColors = ref(['#409EFF', '#28C76F', '#EA5455', '#FF9F43', '#01CFE8', '#4A5DFF'])
const sideThemeList = [
{
type: 'dark',
@@ -44,13 +44,11 @@ const handleClick = () => {
overflow: hidden;
cursor: pointer;
user-select: none;
@apply flex items-center px-3 relative;
@apply flex items-center p-2 relative;
.logo-title {
width: 70%;
position: absolute;
@apply text-base font-medium;
color: var(--el-text-color-primary);
@apply text-xl;
}
.title-width-enter-active {
@@ -67,53 +67,33 @@ const themeClass = computed(() => `theme-${props.theme}`)
&.theme-dark {
.el-menu {
:deep(.el-menu-item) {
margin: 2px 10px;
border-radius: 6px;
&.is-active {
background-color: rgba(255, 255, 255, 0.12);
color: #fff;
font-weight: 500;
@apply bg-primary border-primary;
}
}
:deep(.el-sub-menu__title) {
margin: 2px 10px;
border-radius: 6px;
}
}
:deep(.el-menu--collapse) {
.el-sub-menu.is-active .el-sub-menu__title {
background-color: rgba(255, 255, 255, 0.12) !important;
@apply bg-primary #{!important};
}
}
}
&.theme-light {
:deep(.el-menu) {
.el-menu-item {
margin: 2px 10px;
border-radius: 6px;
color: var(--el-text-color-regular);
border-color: transparent;
&.is-active {
background-color: var(--health-primary-light, var(--el-color-primary-light-9));
color: var(--health-primary-dark, var(--el-color-primary));
font-weight: 500;
@apply bg-primary-light-9 border-r-2 border-primary;
}
}
.el-sub-menu__title {
margin: 2px 10px;
border-radius: 6px;
color: var(--el-text-color-regular);
}
.el-menu-item:hover,
.el-sub-menu__title:hover {
background-color: var(--el-fill-color);
color: var(--el-text-color-primary);
color: var(--el-color-primary);
}
}
}
.el-menu {
border-right: none;
background: transparent;
padding: 10px 0;
&:not(.el-menu--collapse) {
width: var(--aside-width);
}
@@ -1,10 +1,6 @@
<template>
<div class="side" :style="sideStyle">
<side-logo
v-if="settingStore.showLogo && appStore.isMobile"
:show-title="!isCollapsed"
:theme="sideTheme"
/>
<side-logo v-if="settingStore.showLogo" :show-title="!isCollapsed" :theme="sideTheme" />
<side-menu
:routes="routes"
:is-collapsed="isCollapsed"
@@ -49,9 +45,9 @@ const sideStyle = computed(() => {
})
const menuProp = computed(() => {
return {
backgroundColor: sideTheme.value == 'dark' ? settingStore.sideDarkColor : 'transparent',
textColor: sideTheme.value == 'dark' ? 'rgba(255,255,255,0.85)' : '',
activeTextColor: sideTheme.value == 'dark' ? '#ffffff' : ''
backgroundColor: sideTheme.value == 'dark' ? settingStore.sideDarkColor : '',
textColor: sideTheme.value == 'dark' ? 'var(--el-color-white)' : '',
activeTextColor: sideTheme.value == 'dark' ? 'var(--el-color-white)' : ''
}
})
const handleSelect = () => {
@@ -64,8 +60,8 @@ const handleSelect = () => {
<style lang="scss" scoped>
.side {
position: relative;
z-index: 10;
@apply h-full flex flex-col;
background-color: var(--side-dark-color, var(--health-bg-sidebar, var(--el-bg-color)));
z-index: 999;
@apply border-r border-br-light h-full flex flex-col;
background-color: var(--side-dark-color, var(--el-bg-color));
}
</style>
+9 -28
View File
@@ -1,42 +1,23 @@
<template>
<div class="layout-default flex flex-col h-screen w-full bg-page">
<div class="app-header shrink-0">
<layout-header />
<div class="layout-default flex h-screen w-full">
<!-- shrink-0避免主内容区宽表格/图表把侧栏压缩到折叠宽度 -->
<div class="app-aside shrink-0">
<layout-sidebar />
</div>
<div class="layout-body flex flex-1 min-h-0">
<div class="app-aside shrink-0">
<layout-sidebar />
<div class="flex-1 flex flex-col min-w-0">
<div class="app-header">
<layout-header />
</div>
<div class="app-content flex flex-1 flex-col min-w-0 bg-body">
<multiple-tabs v-if="settingStore.openMultipleTabs" />
<div class="app-main flex-1 min-h-0">
<layout-main />
</div>
<div class="app-main flex-1 min-h-0">
<layout-main />
</div>
</div>
</div>
</template>
<script setup lang="ts">
import useSettingStore from '@/stores/modules/setting'
import LayoutHeader from './components/header/index.vue'
import MultipleTabs from './components/header/multiple-tabs.vue'
import LayoutMain from './components/main.vue'
import LayoutSidebar from './components/sidebar/index.vue'
const settingStore = useSettingStore()
</script>
<style lang="scss" scoped>
.layout-body {
flex: 1;
min-height: 0;
}
.app-content {
border-left: 1px solid var(--health-border, var(--el-border-color-lighter));
}
</style>
+31 -34
View File
@@ -1,46 +1,43 @@
:root.dark {
color-scheme: dark;
--table-header-bg-color: #1E293B;
--health-bg-page: #0F1419;
--health-bg-sidebar: #151B23;
--health-text-primary: #F1F5F9;
--health-text-secondary: #94A3B8;
--health-border: #334155;
--health-border-light: #1E293B;
--health-primary-light: rgba(14, 165, 233, 0.15);
--el-bg-color-page: var(--health-bg-page);
--el-bg-color: #151B23;
--el-bg-color-overlay: #1E293B;
--el-text-color-primary: var(--health-text-primary);
--el-text-color-regular: #CBD5E1;
--el-text-color-secondary: #94A3B8;
--el-text-color-placeholder: #64748B;
--el-text-color-disabled: #475569;
--el-border-color-darker: #475569;
--el-border-color-dark: #334155;
--el-border-color: #334155;
--el-border-color-light: #334155;
--el-border-color-lighter: #1E293B;
--el-border-color-extra-light: #151B23;
--el-fill-color-darker: #334155;
--el-fill-color-dark: #1E293B;
--el-fill-color: #1E293B;
--el-fill-color-light: #151B23;
--el-fill-color-lighter: #0F1419;
--el-fill-color-extra-light: #1E293B;
--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;
--el-fill-color-blank: var(--el-bg-color);
--el-mask-color: rgba(0, 0, 0, 0.72);
--el-mask-color-extra-light: rgba(0, 0, 0, 0.36);
--el-box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
--el-box-shadow-light: 0 1px 4px rgba(0, 0, 0, 0.25);
--el-box-shadow-lighter: 0 1px 2px rgba(0, 0, 0, 0.2);
--el-box-shadow-dark: 0 4px 16px rgba(0, 0, 0, 0.4);
--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主题 */
--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);
+6 -30
View File
@@ -23,7 +23,7 @@
flex: none;
display: flex;
flex-direction: column;
border-radius: 8px;
border-radius: 5px;
&.body-padding .el-dialog__body {
padding: 0;
@@ -56,27 +56,17 @@
}
.el-table {
--el-table-header-text-color: var(--el-text-color-regular);
--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(--health-border, var(--el-border-color-lighter));
--el-table-row-hover-bg-color: var(--el-fill-color-light);
font-size: var(--el-font-size-base);
border-radius: 8px;
overflow: hidden;
thead {
th {
font-weight: 500;
font-weight: 400;
}
}
}
.el-card {
border-radius: 8px;
border-color: var(--health-border, var(--el-border-color-lighter));
box-shadow: none;
}
.el-input-group__prepend {
background-color: var(--el-fill-color-blank);
}
@@ -88,30 +78,22 @@
.el-menu--popup-container {
&.theme-light {
.el-menu {
border-radius: 8px;
padding: 4px;
.el-menu-item {
border-radius: 6px;
&.is-active {
background-color: var(--health-primary-light, var(--el-color-primary-light-9));
color: var(--health-primary-dark, var(--el-color-primary));
font-weight: 500;
@apply bg-primary-light-9 border-primary border-r-2;
}
}
.el-menu-item:hover,
.el-sub-menu__title:hover {
background-color: var(--el-fill-color-light);
color: var(--el-color-primary);
}
}
}
&.theme-dark {
.el-menu {
border-radius: 8px;
padding: 4px;
.el-menu-item {
border-radius: 6px;
&.is-active {
background-color: rgba(255, 255, 255, 0.12);
@apply bg-primary;
}
}
}
@@ -128,12 +110,6 @@
}
}
.el-button {
border-radius: 6px;
font-weight: 500;
transition: all 0.2s ease;
}
.el-button--primary {
--el-button-hover-link-text-color: var(--el-color-primary-light-3);
}
-11
View File
@@ -1,12 +1,6 @@
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;600;700&display=swap');
body {
@apply text-base text-tx-primary overflow-hidden min-w-[375px];
font-feature-settings: 'tnum' 1;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.form-tips {
@apply text-tx-secondary text-xs leading-6 mt-1;
}
@@ -21,9 +15,4 @@ body {
/* NProgress */
#nprogress .bar {
@apply bg-primary #{!important};
height: 3px !important;
}
#nprogress .peg {
box-shadow: 0 0 8px var(--el-color-primary), 0 0 4px var(--el-color-primary) !important;
}
+31 -40
View File
@@ -1,58 +1,49 @@
:root {
--el-font-family: theme(fontFamily.sans);
--el-font-weight-primary: 400;
--el-menu-item-height: 42px;
--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: 52px;
--navbar-height: 50px;
--color-white: #ffffff;
--table-header-bg-color: #F8FAFC;
--table-header-bg-color: #f8f8f8;
--el-font-size-extra-large: 18px;
--el-menu-base-level-padding: 14px;
--el-menu-level-padding: 24px;
--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;
/* 天蓝点缀 + 中性底色(天蓝仅用于主色/激活态,不铺满全局) */
--health-primary: #0EA5E9;
--health-primary-light: #E0F2FE;
--health-primary-dark: #0284C7;
--health-bg-page: #F1F5F9;
--health-bg-sidebar: #FFFFFF;
--health-text-primary: #1E293B;
--health-text-secondary: #64748B;
--health-border: #E2E8F0;
--health-border-light: #EEF2F6;
--el-bg-color: var(--color-white);
--el-bg-color-page: var(--health-bg-page);
--el-bg-color-page: #f6f6f6;
--el-bg-color-overlay: #ffffff;
--el-text-color-primary: var(--health-text-primary);
--el-text-color-regular: var(--health-text-secondary);
--el-text-color-secondary: #94A3B8;
--el-text-color-placeholder: #94A3B8;
--el-text-color-disabled: #CBD5E1;
--el-border-color: var(--health-border);
--el-border-color-light: var(--health-border);
--el-border-color-lighter: var(--health-border-light);
--el-border-color-extra-light: #F8FAFC;
--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: #F8FAFC;
--el-fill-color-dark: #E2E8F0;
--el-fill-color-darker: #CBD5E1;
--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-fill-color-blank: #ffffff;
--el-mask-color: rgba(255, 255, 255, 0.65);
--el-mask-color-extra-light: rgba(255, 255, 255, 0.28);
--el-box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
--el-box-shadow-light: 0 1px 3px rgba(15, 23, 42, 0.06);
--el-box-shadow-lighter: 0 1px 2px rgba(15, 23, 42, 0.04);
--el-box-shadow-dark: 0 4px 12px rgba(15, 23, 42, 0.08);
/* 过亮会盖住抽屉/弹窗下的内容;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);
}
+8 -78
View File
@@ -1,27 +1,14 @@
<template>
<div class="login flex flex-col">
<div class="login-bg-decoration"></div>
<div class="flex-1 flex items-center justify-center relative z-10">
<div class="login-card flex rounded-xl overflow-hidden shadow-lg">
<div class="login-visual hidden md:flex flex-col justify-center items-center relative">
<div class="login-visual-content">
<div class="login-visual-icon">
<svg viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="20" y="8" width="8" height="32" rx="2" fill="currentColor" opacity="0.9"/>
<rect x="8" y="20" width="32" height="8" rx="2" fill="currentColor" opacity="0.9"/>
</svg>
</div>
<h2 class="login-visual-title">中医在线问诊平台</h2>
<p class="login-visual-desc">专业 · 安全 · 可信赖的医疗管理系统</p>
</div>
<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>
<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 mb-8">
<div class="login-form-badge">管理后台</div>
<div class="text-2xl font-semibold text-tx-primary mt-3">{{ config.web_name }}</div>
</div>
<div class="text-center text-3xl font-medium mb-8">{{ config.web_name }}</div>
<!-- 企业微信自动授权中 -->
<div v-if="wxWorkAutoLogin" class="text-center py-10">
@@ -300,68 +287,11 @@ onMounted(async () => {
<style lang="scss" scoped>
.login {
position: relative;
@apply min-h-screen overflow-hidden;
background: var(--health-bg-page, #F1F5F9);
.login-bg-decoration {
display: none;
}
background-image: url('./images/login_bg.png');
@apply min-h-screen bg-no-repeat bg-center bg-cover;
.login-card {
height: auto;
min-height: 440px;
border: 1px solid var(--health-border, var(--el-border-color-lighter));
box-shadow: var(--el-box-shadow-light);
background: var(--el-bg-color);
}
.login-visual {
width: 400px;
background: var(--el-bg-color);
color: var(--el-text-color-primary);
padding: 48px 40px;
border-right: 1px solid var(--health-border, var(--el-border-color-lighter));
.login-visual-icon {
width: 56px;
height: 56px;
border-radius: 14px;
background: var(--el-color-primary-light-9);
color: var(--el-color-primary);
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 24px;
svg {
width: 28px;
height: 28px;
}
}
.login-visual-title {
font-size: 20px;
font-weight: 600;
margin-bottom: 10px;
color: var(--el-text-color-primary);
}
.login-visual-desc {
font-size: 14px;
color: var(--el-text-color-secondary);
line-height: 1.7;
}
}
.login-form-badge {
display: inline-block;
padding: 4px 12px;
border-radius: 6px;
font-size: 12px;
font-weight: 500;
color: var(--el-color-primary);
background: var(--el-color-primary-light-9);
min-height: 400px;
}
}
@@ -378,6 +378,9 @@
</div>
</div>
</el-descriptions-item>
<el-descriptions-item v-if="detailDietaryText" label="忌口" :span="2">
{{ detailDietaryText }}
</el-descriptions-item>
<el-descriptions-item label="状态" :span="2">
<el-tag :type="Number(detailPrescription.void_status) === 1 ? 'danger' : 'success'" size="small">
{{ Number(detailPrescription.void_status) === 1 ? '已作废' : '正常' }}
@@ -837,7 +840,8 @@ import {
logisticsTraceLineUrgent,
analyzeLogisticsPayloadUrgent,
parseLogisticsTracePayload,
canUpdateAmount
canUpdateAmount,
formatDietaryTaboo
} from './prescription-order-utils'
const props = withDefaults(
@@ -896,6 +900,8 @@ const detailPrescription = computed(() => {
return p && typeof p === 'object' ? p : null
})
const detailDietaryText = computed(() => formatDietaryTaboo(detailPrescription.value?.dietary_taboo))
const detailLinkedPayOrders = computed(() => {
const d = detailData.value
if (!d) return []
@@ -142,17 +142,19 @@ export function logActionText(act: string) {
return m[act] || act
}
/** 支付单来源/方式:企微对外收款、付呗等创建链路 + 支付方式回退 */
/** 支付单来源/方式:企微对外收款、付呗、快递代收等创建链路 + 支付方式回退 */
export function formatPayOrderSource(row: { payment_method?: unknown; create_type?: unknown }) {
const createType = String(row?.create_type || '')
if (createType === 'wechat_work') return '企业微信对外收款'
if (createType === 'fubei') return '付呗'
if (createType === 'express_cod') return '快递代收'
const paymentMethod = String(row?.payment_method || '')
const methodMap: Record<string, string> = {
alipay: '支付宝',
wechat: '微信',
wechat_work: '企业微信',
fubei: '付呗',
express_cod: '快递代收',
manual: '手动确认到账'
}
if (paymentMethod && methodMap[paymentMethod]) {
@@ -340,3 +342,18 @@ export function canUpdateAmount(row: { id?: number; fulfillment_status?: number
const fs = Number(row.fulfillment_status)
return fs !== 3 && fs !== 4
}
/** 处方忌口:库内逗号分隔字符串或前端多选数组,统一为「、」连接展示 */
export function formatDietaryTaboo(raw: unknown): string {
if (Array.isArray(raw)) {
return raw.map((v) => String(v ?? '').trim()).filter(Boolean).join('、')
}
if (typeof raw === 'string' && raw.trim()) {
return raw
.split(',')
.map((s) => s.trim())
.filter(Boolean)
.join('、')
}
return ''
}
@@ -39,7 +39,7 @@
<el-radio-button label="rejected">驳回</el-radio-button>
</el-radio-group>
<span class="pl-toolbar__sep" aria-hidden="true" />
<span class="pl-toolbar__k" title="与列表「来源」列一致:系统代开 / 手工">来源</span>
<span class="pl-toolbar__k" title="与列表「来源」列一致:空白处方 / 手工">来源</span>
<el-radio-group
v-model="formData.source_filter"
size="small"
@@ -48,7 +48,7 @@
>
<el-radio-button label="all">全部</el-radio-button>
<el-radio-button label="manual">手工</el-radio-button>
<el-radio-button label="system">系统代开</el-radio-button>
<el-radio-button label="system">空白处方</el-radio-button>
</el-radio-group>
</div>
<div class="pl-toolbar__line pl-toolbar__line--second">
@@ -146,7 +146,7 @@
size="small"
effect="plain"
>
系统代开
空白处方
</el-tag>
<span v-else class="text-gray-400 text-sm">手工</span>
</template>
@@ -290,7 +290,7 @@
type="warning"
size="small"
effect="plain"
>系统代开</el-tag>
>空白处方</el-tag>
<el-tag v-if="slipView && Number(slipView.void_status) === 1" type="danger" size="small">已作废</el-tag>
<el-tag
v-else-if="slipView && Number(slipView.business_prescription_audit_rejected) === 1"
@@ -437,6 +437,7 @@
<p>主方服法{{ rxUsageText }}</p>
<p v-if="rxAuxUsageText">辅方服法{{ rxAuxUsageText }}</p>
<p v-if="rxAdviceText">医嘱{{ rxAdviceText }}</p>
<p v-if="slipDietaryText">忌口{{ slipDietaryText }}</p>
<p v-if="rxRemarkText">备注{{ rxRemarkText }}</p>
<p v-if="rxPharmacyRemarkText" class="rx-text-warn">
药房备注{{ rxPharmacyRemarkText }}
@@ -1856,7 +1857,7 @@ const formData = reactive({
creator_ids: [] as number[],
/** all | passed | not_passed | pending | rejectedall 表示不限,接口传参会转为空) */
audit_filter: 'all' as string,
/** all | manual | system — 与 is_system_auto:手工(0) / 系统代开(1) */
/** all | manual | system — 与 is_system_auto:手工(0) / 空白处方(1) */
source_filter: 'all' as string,
start_time: '',
end_time: ''
@@ -283,6 +283,12 @@
<el-option label="未指派" value="0" />
</el-select>
</el-form-item>
<el-form-item class="w-[140px]" label="辅方">
<el-select v-model="queryParams.has_aux_formula" clearable placeholder="全部" class="!w-full">
<el-option label="含辅方" value="1" />
<el-option label="不含辅方" value="0" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="resetPage">查询</el-button>
<el-button @click="handleReset">重置</el-button>
@@ -292,7 +298,7 @@
:fetch-fun="prescriptionOrderExport"
:params="prescriptionOrderExportParams"
:page-size="pager.size"
export-hint="导出范围与上方筛选一致履约状态创建时间及其他条件均会生效自媒体渠道挂号渠道来源优先取该单关联处方登记的挂号无则诊单下同患者挂号取 id 最大的一条与前台挂号选择的记录一致业绩侧栏带渠道筛选导出时与同页列表高亮挂号同源服务套餐按字典 server_order 解析展示签收日期与详情/业绩看板同源仅读物流库轨迹/签收时间导出不再实时查快递100速度只取决于数据库签收时间由 `tcm:backfill-sign-time` 命令与物流自动更新定时任务落库刚发货尚未同步的单子会暂时为空待下次回填/定时任务刷新后显示"
export-hint="导出范围与上方筛选一致履约状态创建时间及其他条件均会生效自媒体渠道挂号渠道来源优先取该单关联处方登记的挂号无则诊单下同患者挂号取 id 最大的一条与前台挂号选择的记录一致业绩侧栏带渠道筛选导出时与同页列表高亮挂号同源服务套餐按字典 server_order 解析展示关联收款记录与详情侧栏同源已支付/已退款/待审核每笔两行展示摘要行+明细行多笔空行分隔单元格自动换行签收日期与详情/业绩看板同源仅读物流库轨迹/签收时间导出不再实时查快递100速度只取决于数据库签收时间由 `tcm:backfill-sign-time` 命令与物流自动更新定时任务落库刚发货尚未同步的单子会暂时为空待下次回填/定时任务刷新后显示"
/>
</el-form-item>
</el-form>
@@ -303,6 +309,17 @@
<div class="po-table-toolbar__title-wrap">
<span class="po-table-title text-gray-800">订单列表</span>
<span class="po-table-hint text-gray-400"> {{ pager.count }} </span>
<el-button
v-if="canBatchReassign"
type="primary"
plain
size="small"
class="ml-3"
:disabled="selectedOrders.length === 0"
@click="openReassignDialog"
>
批量改派医助{{ selectedOrders.length ? `${selectedOrders.length}` : '' }}
</el-button>
</div>
<div class="po-focus-board">
<div
@@ -339,7 +356,15 @@
</div>
</div>
</div>
<el-table :data="pager.lists" size="default" stripe class="po-data-table" :row-class-name="orderRowClassName">
<el-table
:data="pager.lists"
size="default"
stripe
class="po-data-table"
:row-class-name="orderRowClassName"
@selection-change="onOrderSelectionChange"
>
<el-table-column v-if="canBatchReassign" type="selection" width="44" :selectable="() => true" />
<el-table-column label="订单号" min-width="178" show-overflow-tooltip>
<template #default="{ row }">
<div class="po-order-no-cell">
@@ -406,16 +431,41 @@
</div>
<div v-if="canViewFinanceFields() && row.internal_cost != null && row.internal_cost !== ''" class="flex justify-between">
<span class="text-gray-500">内部成本</span>
<span class="font-medium text-orange-600">¥{{ formatMoney(row.internal_cost) }}</span>
<span
class="font-medium cursor-pointer select-none"
:class="internalCostVisible ? 'text-orange-600' : 'text-gray-400'"
title="点击显示/隐藏"
@click.stop="internalCostVisible = !internalCostVisible"
>
{{ internalCostVisible ? `¥${formatMoney(row.internal_cost)}` : '****' }}
</span>
</div>
</div>
</el-popover>
</div>
</template>
</el-table-column>
<el-table-column v-if="canViewFinanceFields()" label="内部成本" width="92">
<el-table-column v-if="canViewFinanceFields()" width="100">
<template #header>
<span
class="cursor-pointer select-none hover:text-primary"
title="点击显示/隐藏内部成本"
@click.stop="internalCostVisible = !internalCostVisible"
>
内部成本
</span>
</template>
<template #default="{ row }">
{{ row.internal_cost != null && row.internal_cost !== '' ? `¥${row.internal_cost}` : '—' }}
<span
v-if="row.internal_cost != null && row.internal_cost !== ''"
class="cursor-pointer select-none"
:class="internalCostVisible ? 'text-orange-600 font-medium' : 'text-gray-400'"
title="点击显示/隐藏"
@click.stop="internalCostVisible = !internalCostVisible"
>
{{ internalCostVisible ? `¥${row.internal_cost}` : '****' }}
</span>
<span v-else></span>
</template>
</el-table-column>
<el-table-column label="处方审核" width="110">
@@ -704,7 +754,7 @@
<el-form-item label="新金额" prop="amount">
<el-input-number
v-model="updateAmountForm.amount"
:min="0.01"
:min="0"
:step="0.01"
:precision="2"
controls-position="right"
@@ -1898,6 +1948,7 @@
<p v-if="rxAuxUsageText">辅服法{{ rxAuxUsageText }}</p>
</template>
<p v-if="rxAdviceText">医嘱{{ rxAdviceText }}</p>
<p v-if="slipDietaryText">忌口{{ slipDietaryText }}</p>
<p v-if="prescriptionTabType === 'internal' && rxRemarkText">备注{{ rxRemarkText }}</p>
<p v-if="rxPharmacyRemarkText" class="rx-text-warn">
药房备注{{ rxPharmacyRemarkText }}
@@ -2113,6 +2164,34 @@
</div>
</el-drawer>
<!-- 批量改派医助 -->
<el-dialog v-model="reassignVisible" title="批量改派医助" width="440px" append-to-body>
<div class="text-sm text-gray-600 mb-3">
已选 <strong class="text-primary">{{ selectedOrders.length }}</strong> 将订单创建人(医助归属)改派为
</div>
<el-select
v-model="reassignAssistantId"
clearable
filterable
placeholder="选择目标医助"
class="!w-full"
>
<el-option
v-for="a in assistantOptions"
:key="a.id"
:label="a.name"
:value="a.id"
/>
</el-select>
<p class="text-xs text-gray-400 mt-2 leading-relaxed">
仅变更订单的业务归属医助筛选 / 业绩口径并写入操作日志不影响处方支付单及审核状态已是该医助的订单会自动跳过
</p>
<template #footer>
<el-button @click="reassignVisible = false">取消</el-button>
<el-button type="primary" :loading="reassignSubmitting" @click="submitReassign">确定改派</el-button>
</template>
</el-dialog>
<!-- consumer/prescription/index诊单 edit 同一套界面只读诊单与全部分页签 -->
<TcmDiagnosisEditView ref="diagnosisViewRef" />
</div>
@@ -2144,6 +2223,7 @@ import {
analyzeLogisticsPayloadUrgent,
parseLogisticsTracePayload,
canUpdateAmount,
formatDietaryTaboo,
type SlipFormulaType,
type SlipAuxUsageForm
} from './components/prescription-order-utils'
@@ -2167,6 +2247,7 @@ import {
prescriptionOrderRefund,
prescriptionOrderRevokeRxAudit,
prescriptionOrderRevokePayAudit,
prescriptionOrderBatchAssignAssistant,
prescriptionOrderPatchPrescriptionPatient,
prescriptionOrderLinkPayOrder,
prescriptionOrderSubmitGancaoRecipel,
@@ -2266,6 +2347,70 @@ const canViewFinanceFields = () => {
return FINANCE_ROLE_IDS.some((rid) => ids.includes(rid))
}
/** 内部成本默认隐藏,点击表头或单元格切换显示 */
const internalCostVisible = ref(false)
/** 是否可批量改派医助(与后端 canSeeAllPrescriptionOrders 一致:超管 / 全量编辑角色) */
const canBatchReassign = computed(() => {
const u = userStore.userInfo
if (!u) return false
if (Number(u.root) === 1) return true
const ids = Array.isArray(u.role_ids) ? u.role_ids.map((n: unknown) => Number(n)) : []
return ORDER_EDIT_ALL_ROLE_IDS.some((rid) => ids.includes(rid))
})
/** 批量改派:表格多选 */
const selectedOrders = ref<Array<{ id: number; order_no?: string; creator_id?: number }>>([])
function onOrderSelectionChange(rows: Array<{ id: number; order_no?: string; creator_id?: number }>) {
selectedOrders.value = Array.isArray(rows) ? rows : []
}
/** 批量改派弹窗 */
const reassignVisible = ref(false)
const reassignAssistantId = ref<number | ''>('')
const reassignSubmitting = ref(false)
function openReassignDialog() {
if (selectedOrders.value.length === 0) {
feedback.msgWarning('请先勾选要改派的订单')
return
}
reassignAssistantId.value = ''
reassignVisible.value = true
}
async function submitReassign() {
const assistantId = Number(reassignAssistantId.value)
if (!assistantId) {
feedback.msgWarning('请选择目标医助')
return
}
const orderIds = selectedOrders.value.map((r) => Number(r.id)).filter((id) => id > 0)
if (orderIds.length === 0) {
feedback.msgWarning('请先勾选要改派的订单')
return
}
reassignSubmitting.value = true
try {
const res: any = await prescriptionOrderBatchAssignAssistant({
order_ids: orderIds,
assistant_id: assistantId
})
const ok = Number(res?.success ?? 0)
const errs: string[] = Array.isArray(res?.errors) ? res.errors : []
let msg = `已改派 ${ok}`
if (errs.length) msg += `${errs.join('')}`
feedback.msgSuccess(msg)
reassignVisible.value = false
selectedOrders.value = []
getLists()
} catch {
// request 拦截器已弹错误提示
} finally {
reassignSubmitting.value = false
}
}
// 省市区数据
const regionOptions = ref([])
@@ -2469,6 +2614,8 @@ const queryParams = reactive({
supply_mode: '' as '' | 'gancao' | 'self',
/** 服务渠道:'' 不限;'0' 未指派(库内 '' 或 '0' */
service_channel: '' as '' | '0',
/** 是否含辅方:'' 不限;'1' 含辅方;'0' 不含辅方 */
has_aux_formula: '' as '' | '0' | '1',
/** 列表排除履约已取消(4):重点看板「待处方审核」等用 */
exclude_fulfillment_cancelled: 0 as number,
/** 下单人(关联操作日志 audit_rx_* / audit_pay_* */
@@ -2616,6 +2763,11 @@ function normalizePrescriptionOrderListQuery(params: Record<string, unknown>): R
p.service_channel = String(p.service_channel).trim()
if (p.service_channel === '') delete p.service_channel
}
if (p.has_aux_formula === '' || p.has_aux_formula === undefined || p.has_aux_formula === null) {
delete p.has_aux_formula
} else {
p.has_aux_formula = Number(p.has_aux_formula)
}
if (Number(p.exclude_fulfillment_cancelled) !== 1) delete p.exclude_fulfillment_cancelled
if (!String(p.start_time || '').trim() || !String(p.end_time || '').trim()) {
delete p.start_time
@@ -2854,6 +3006,7 @@ function handleReset() {
queryParams.payment_slip_audit_status = ''
queryParams.supply_mode = ''
queryParams.service_channel = ''
queryParams.has_aux_formula = ''
queryParams.audit_admin_id = ''
queryParams.exclude_fulfillment_cancelled = 0
resetParams()
@@ -3104,8 +3257,8 @@ const updateAmountRules: FormRules = {
{
validator: (_rule, value, callback) => {
const num = Number(value)
if (!Number.isFinite(num) || num <= 0) {
callback(new Error('订单金额必须大于0'))
if (!Number.isFinite(num) || num < 0) {
callback(new Error('订单金额不能为负数'))
return
}
callback()
@@ -3162,7 +3315,8 @@ function openUpdateAmount() {
return
}
updateAmountForm.id = Number(row?.id) || 0
updateAmountForm.amount = Number(row?.amount) || undefined
const amt = Number(row?.amount)
updateAmountForm.amount = Number.isFinite(amt) ? amt : undefined
updateAmountVisible.value = true
nextTick(() => updateAmountFormRef.value?.clearValidate())
}
@@ -4544,12 +4698,7 @@ const slipAuxHerbs = computed(() =>
slipHerbsList.value.filter((h: any) => normalizeSlipFormulaType(h?.formula_type) === '辅方')
)
const slipDietaryText = computed(() => {
const d = prescriptionViewData.value?.dietary_taboo
if (Array.isArray(d)) return d.filter(Boolean).join('、')
if (typeof d === 'string' && d.trim()) return d.trim()
return ''
})
const slipDietaryText = computed(() => formatDietaryTaboo(prescriptionViewData.value?.dietary_taboo))
const slipChuwanVisible = computed(() => {
const d = prescriptionViewData.value
@@ -894,6 +894,9 @@
</div>
</div>
</el-descriptions-item>
<el-descriptions-item v-if="detailDietaryText" label="忌口" :span="2">
{{ detailDietaryText }}
</el-descriptions-item>
<el-descriptions-item label="状态" :span="2">
<el-tag :type="Number(detailPrescription.void_status) === 1 ? 'danger' : 'success'" size="small">
{{ Number(detailPrescription.void_status) === 1 ? '已作废' : '正常' }}
@@ -1295,7 +1298,7 @@
<el-form-item label="新金额" prop="amount">
<el-input-number
v-model="updateAmountForm.amount"
:min="0.01"
:min="0"
:step="0.01"
:precision="2"
controls-position="right"
@@ -2311,6 +2314,7 @@
<div class="rx-text">
<p>服法{{ rxUsageText }}</p>
<p v-if="rxAdviceText">医嘱{{ rxAdviceText }}</p>
<p v-if="slipDietaryText">忌口{{ slipDietaryText }}</p>
<p v-if="rxRemarkText">备注{{ rxRemarkText }}</p>
<p v-if="rxPharmacyRemarkText" class="rx-text-warn">
药房备注{{ rxPharmacyRemarkText }}
@@ -2569,6 +2573,7 @@ import {
getDoctors,
getAssistants
} from '@/api/tcm'
import { formatDietaryTaboo } from './components/prescription-order-utils'
import html2canvas from 'html2canvas'
import { jsPDF } from 'jspdf'
import { getDictData } from '@/api/app'
@@ -3579,6 +3584,8 @@ const detailPrescription = computed(() => {
return p && typeof p === 'object' ? p : null
})
const detailDietaryText = computed(() => formatDietaryTaboo(detailPrescription.value?.dietary_taboo))
function normalizeBizPhone(v: unknown): string {
if (v === null || v === undefined) return ''
return String(v).replace(/\s/g, '').trim()
@@ -3726,8 +3733,8 @@ const updateAmountRules: FormRules = {
{
validator: (_rule, value, callback) => {
const num = Number(value)
if (!Number.isFinite(num) || num <= 0) {
callback(new Error('订单金额必须大于0'))
if (!Number.isFinite(num) || num < 0) {
callback(new Error('订单金额不能为负数'))
return
}
callback()
@@ -3879,7 +3886,8 @@ function openUpdateAmount() {
return
}
updateAmountForm.id = Number(row?.id) || 0
updateAmountForm.amount = Number(row?.amount) || undefined
const amt = Number(row?.amount)
updateAmountForm.amount = Number.isFinite(amt) ? amt : undefined
updateAmountVisible.value = true
nextTick(() => updateAmountFormRef.value?.clearValidate())
}
@@ -4097,12 +4105,14 @@ function formatPayOrderSource(row: { payment_method?: unknown; create_type?: unk
const createType = String(row?.create_type || '')
if (createType === 'wechat_work') return '企业微信对外收款'
if (createType === 'fubei') return '付呗'
if (createType === 'express_cod') return '快递代收'
const paymentMethod = String(row?.payment_method || '')
const methodMap: Record<string, string> = {
alipay: '支付宝',
wechat: '微信',
wechat_work: '企业微信',
fubei: '付呗',
express_cod: '快递代收',
manual: '手动确认到账'
}
if (paymentMethod && methodMap[paymentMethod]) {
@@ -5185,12 +5195,7 @@ const slipHerbsList = computed(() => {
return Array.isArray(h) ? h : []
})
const slipDietaryText = computed(() => {
const d = prescriptionViewData.value?.dietary_taboo
if (Array.isArray(d)) return d.filter(Boolean).join('、')
if (typeof d === 'string' && d.trim()) return d.trim()
return ''
})
const slipDietaryText = computed(() => formatDietaryTaboo(prescriptionViewData.value?.dietary_taboo))
const slipChuwanVisible = computed(() => {
const d = prescriptionViewData.value
+3 -5
View File
@@ -231,8 +231,7 @@ import { ref, computed, onMounted } from 'vue'
import dayjs from 'dayjs'
import isoWeek from 'dayjs/plugin/isoWeek'
import feedback from '@/utils/feedback'
import { adminLists } from '@/api/perms/admin'
import { rosterLists, rosterSave, rosterDelete, rosterBatchSave } from '@/api/doctor'
import { doctorLists, rosterLists, rosterSave, rosterDelete, rosterBatchSave } from '@/api/doctor'
import { ElMessageBox } from 'element-plus'
dayjs.extend(isoWeek)
@@ -491,10 +490,9 @@ async function removeSegment(row: RosterSegment) {
const loadDoctors = async () => {
try {
const res = await adminLists({
const res = await doctorLists({
page_no: 1,
page_size: 1000,
role_id: 1
page_size: 1000
})
tableData.value = (res?.lists || []).map((doctor: any) => ({
doctorId: doctor.id,
+224 -2
View File
@@ -283,7 +283,15 @@
<td class="col-name">{{ r.name }}</td>
<td class="col-consult">{{ formatLeaderboardInt(r.consult_count) }}</td>
<td class="col-deal">{{ formatLeaderboardInt(r.deal_order_count) }}</td>
<td class="col-assign">{{ formatLeaderboardInt(r.assign_count ?? 0) }}</td>
<td class="col-assign" @click.stop="onLeaderboardAssignCellClick(r)">
<span
v-if="Number(r.assign_count ?? 0) > 0"
class="yeji-lead-cell--link"
>{{ formatLeaderboardInt(r.assign_count ?? 0) }}</span>
<template v-else>{{
formatLeaderboardInt(r.assign_count ?? 0)
}}</template>
</td>
<td class="col-appointment" @click.stop="onLeaderboardAppointmentCellClick(r)">
<span
v-if="Number(r.appointment_count ?? 0) > 0"
@@ -561,7 +569,13 @@
>{{ formatInt(r.lead_count) }}</span>
<template v-else>{{ formatInt(r.lead_count) }}</template>
</td>
<td>{{ formatInt(r.assign_count ?? 0) }}</td>
<td @click.stop="onAssignCountCellClick($event, tb, r)">
<span
v-if="yejiAssignCountCellClickable(r)"
class="yeji-lead-cell--link"
>{{ formatInt(r.assign_count ?? 0) }}</span>
<template v-else>{{ formatInt(r.assign_count ?? 0) }}</template>
</td>
<td @click.stop="onYejiRevisitTotalCellClick(tb, r)">
<span
v-if="r.dept_id > 0 && Number(r.revisit_count ?? 0) > 0"
@@ -1356,6 +1370,59 @@
/>
</div>
</el-dialog>
<el-dialog
v-model="assignLinesDialogVisible"
width="min(960px, 96vw)"
destroy-on-close
class="yeji-leadlines-dialog"
>
<template #header>
<div class="yeji-unassigned-dialog__head">
<span class="yeji-unassigned-dialog__title">被指派明细</span>
<p class="yeji-unassigned-dialog__sub">{{ assignLinesSubtitle }}</p>
</div>
</template>
<p v-if="assignLinesApiNote" class="yeji-unassigned-dialog__note">{{ assignLinesApiNote }}</p>
<div v-loading="assignLinesLoading" class="yeji-unassigned-dialog__body">
<el-table
v-if="assignLinesRows.length > 0 || assignLinesLoading"
:data="assignLinesRows"
size="small"
stripe
border
max-height="440"
class="yeji-unassigned-dialog__table"
:empty-text="assignLinesLoading ? '加载中…' : '暂无数据'"
>
<el-table-column type="index" label="#" width="46" :index="assignLinesIndexMethod" />
<el-table-column prop="diagnosis_id" label="诊单ID" width="88" />
<el-table-column prop="patient_name" label="患者" min-width="100" show-overflow-tooltip />
<el-table-column prop="patient_phone" label="联系电话" min-width="120" show-overflow-tooltip />
<el-table-column prop="assistant_name" label="被指派医助" min-width="108" show-overflow-tooltip />
<el-table-column prop="assign_count" label="指派次数" width="88" align="right" />
<el-table-column prop="last_assign_time_text" label="最近指派时间" min-width="156" show-overflow-tooltip />
</el-table>
<el-empty
v-if="!assignLinesLoading && assignLinesRows.length === 0"
description="暂无被指派明细"
/>
<div v-if="assignLinesCount > 0" class="yeji-leadlines-dialog__pager">
<el-pagination
:current-page="assignLinesPage"
:page-size="assignLinesPageSize"
background
layout="total, sizes, prev, pager, next, jumper"
:total="assignLinesCount"
:page-sizes="[10, 20, 50, 100]"
:disabled="assignLinesLoading"
:hide-on-single-page="false"
@current-change="onAssignLinesPageChange"
@size-change="onAssignLinesPageSizeChange"
/>
</div>
</div>
</el-dialog>
</div>
</template>
@@ -1376,6 +1443,7 @@ import {
yejiStatsLeadLines,
yejiStatsAppointmentLines,
yejiStatsRevisitBreakdown,
yejiStatsAssignLines,
} from '@/api/stats'
import { deptPerformanceTargetMonthMatrix } from '@/api/finance'
import { prescriptionOrderLists } from '@/api/tcm'
@@ -1491,6 +1559,17 @@ interface YejiLeadLineRow {
external_contact_name: string
}
interface YejiAssignLineRow {
diagnosis_id: number
patient_name: string
patient_phone: string
assistant_id: number
assistant_name: string
assign_count: number
last_assign_time: number
last_assign_time_text: string
}
interface YejiRow {
dept_id: number
dept_name: string
@@ -1709,6 +1788,31 @@ type AppointmentLinesCtx =
}
const appointmentLinesContext = ref<AppointmentLinesCtx | null>(null)
const assignLinesDialogVisible = ref(false)
const assignLinesLoading = ref(false)
const assignLinesSubtitle = ref('')
const assignLinesApiNote = ref('')
const assignLinesRows = ref<YejiAssignLineRow[]>([])
const assignLinesCount = ref(0)
const assignLinesPage = ref(1)
const assignLinesPageSize = ref(20)
type AssignLinesCtx =
| {
mode: 'assistant'
assistant_id: number
name: string
start_date: string
end_date: string
}
| {
mode: 'dept'
dept_id: number
dept_name: string
start_date: string
end_date: string
}
const assignLinesContext = ref<AssignLinesCtx | null>(null)
const appointmentLinesShowChannelColumn = computed(() =>
appointmentLinesRows.value.some(r => String(r.channel_source ?? '') !== '')
)
@@ -3193,6 +3297,124 @@ function onLeadCountCellClick(ev: MouseEvent, tb: YejiTable, row: YejiRow) {
void openLeadLinesDialog(tb, row)
}
function yejiAssignCountCellClickable(r: YejiRow): boolean {
return r.dept_id > 0 && Number(r.assign_count ?? 0) > 0
}
function onAssignCountCellClick(ev: MouseEvent, tb: YejiTable, row: YejiRow) {
if (!yejiAssignCountCellClickable(row)) {
return
}
ev.stopPropagation()
void openAssignLinesDialogForDept(tb, row)
}
function assignLinesIndexMethod(index: number) {
return (assignLinesPage.value - 1) * assignLinesPageSize.value + index + 1
}
function buildAssignLinesRequestParams(page: number, pageSize: number): Record<string, string | number> | null {
const ctx = assignLinesContext.value
if (!ctx) {
return null
}
const p: Record<string, string | number> = {
start_date: ctx.start_date,
end_date: ctx.end_date,
page,
page_size: pageSize,
}
if (ctx.mode === 'dept') {
p.dept_id = ctx.dept_id
} else {
p.assistant_id = ctx.assistant_id
}
if (selectedDeptIds.value.length > 0) {
p.dept_ids = selectedDeptIds.value.join(',')
}
if (selectedChannel.value) {
p.channel_code = selectedChannel.value
}
return p
}
async function fetchAssignLinesPage() {
const params = buildAssignLinesRequestParams(assignLinesPage.value, assignLinesPageSize.value)
if (!params) {
return
}
assignLinesLoading.value = true
try {
const res: any = await yejiStatsAssignLines(params as any)
assignLinesRows.value = Array.isArray(res?.lists) ? res.lists : []
assignLinesCount.value = Number(res?.count ?? 0)
assignLinesApiNote.value = typeof res?.note === 'string' ? res.note : ''
} catch (e: unknown) {
if (axios.isCancel(e)) return
const any = e as any
ElMessage.error(any?.msg || any?.message || '加载被指派明细失败')
assignLinesRows.value = []
assignLinesCount.value = 0
assignLinesApiNote.value = ''
} finally {
assignLinesLoading.value = false
}
}
async function openAssignLinesDialogForDept(tb: YejiTable, row: YejiRow) {
assignLinesContext.value = {
mode: 'dept',
dept_id: row.dept_id,
dept_name: row.dept_name,
start_date: tb.start_date,
end_date: tb.end_date,
}
assignLinesSubtitle.value = `${row.dept_name} · ${tb.start_date} ${tb.end_date}`
assignLinesApiNote.value = ''
assignLinesRows.value = []
assignLinesCount.value = 0
assignLinesPage.value = 1
assignLinesPageSize.value = 20
assignLinesDialogVisible.value = true
await fetchAssignLinesPage()
}
function onLeaderboardAssignCellClick(r: LeaderboardPack['leaderboards'][0]['rows'][0]) {
if (!r || r.admin_id <= 0 || !leaderboardBlock.value) {
return
}
if (Number(r.assign_count ?? 0) <= 0) {
return
}
const lb = leaderboardBlock.value
assignLinesContext.value = {
mode: 'assistant',
assistant_id: r.admin_id,
name: r.name,
start_date: lb.start_date,
end_date: lb.end_date,
}
assignLinesSubtitle.value = `${r.name} · 医助 · ${lb.start_date} ${lb.end_date}`
assignLinesApiNote.value = ''
assignLinesRows.value = []
assignLinesCount.value = 0
assignLinesPage.value = 1
assignLinesPageSize.value = 20
assignLinesDialogVisible.value = true
void fetchAssignLinesPage()
}
function onAssignLinesPageChange(p: number) {
assignLinesPage.value = p
void fetchAssignLinesPage()
}
function onAssignLinesPageSizeChange(size: number) {
assignLinesPageSize.value = size
assignLinesPage.value = 1
void fetchAssignLinesPage()
}
const LEAD_LINES_EXPORT_COLUMNS: { key: keyof YejiLeadLineRow; label: string }[] = [
{ key: 'event_time_text', label: '进线时间' },
{ key: 'reception_admin_name', label: '接待' },
+142 -58
View File
@@ -129,7 +129,7 @@
<el-card class="!border-none" shadow="never">
<template #header>
<div class="card-header">
<span>诊单金额占比</span>
<span>{{ amountPieTitle }}</span>
<span class="card-hint">TOP 10</span>
</div>
</template>
@@ -146,17 +146,17 @@
<el-card class="!border-none" shadow="never">
<template #header>
<div class="card-header">
<span>加粉占比</span>
<span>{{ secondaryPieTitle }}</span>
<span class="card-hint">TOP 10</span>
</div>
</template>
<v-charts
v-if="fanPieHasData"
v-if="secondaryPieHasData"
class="stats-chart"
:option="fanPieOption"
:option="secondaryPieOption"
autoresize
/>
<el-empty v-else description="暂无加粉数据" />
<el-empty v-else :description="secondaryPieEmptyText" />
</el-card>
</el-col>
</el-row>
@@ -311,10 +311,13 @@ const overview = reactive<Record<string, any>>({
amounts: [],
order_counts: [],
fan_counts: [],
rois: []
rois: [],
appointment_counts: [],
interview_counts: []
},
amount_share: [],
fan_share: []
fan_share: [],
order_share: []
}
})
@@ -392,7 +395,7 @@ const currentEntityId = computed({
}
})
const summaryCards: MetricCard[] = [
const defaultSummaryCards: MetricCard[] = [
{ key: 'add_fans_count', label: '加粉数', type: 'count' },
{ key: 'paid_appointment_count', label: '付费挂号', type: 'count' },
{ key: 'free_appointment_count', label: '免费挂号', type: 'count' },
@@ -410,7 +413,15 @@ const summaryCards: MetricCard[] = [
{ key: 'roi', label: 'ROI', type: 'ratio' }
]
const tableColumns: MetricColumn[] = [
const doctorSummaryCards: MetricCard[] = [
{ key: 'appointment_total_count', label: '挂号数', type: 'count' },
{ key: 'interview_count', label: '面诊数', type: 'count' },
{ key: 'completed_order_count', label: '接诊单数', type: 'count' },
{ key: 'completed_order_amount', label: '开药金额', type: 'money' },
{ key: 'receive_rate', label: '接诊率', type: 'percent' }
]
const defaultTableColumns: MetricColumn[] = [
{ key: 'add_fans_count', label: '加粉数', type: 'count' },
{ key: 'total_open_count', label: '总开口', type: 'count', placeholder: true },
{ key: 'unreplied_count', label: '未回复', type: 'count', placeholder: true },
@@ -431,6 +442,18 @@ const tableColumns: MetricColumn[] = [
{ key: 'roi', label: 'ROI', type: 'ratio', minWidth: 90 }
]
const doctorTableColumns: MetricColumn[] = [
{ key: 'appointment_total_count', label: '挂号数', type: 'count' },
{ key: 'interview_count', label: '面诊数', type: 'count' },
{ key: 'completed_order_count', label: '接诊单数', type: 'count' },
{ key: 'completed_order_amount', label: '开药金额', type: 'money', minWidth: 120 },
{ key: 'receive_rate', label: '接诊率', type: 'percent', minWidth: 100 }
]
const isDoctorDimension = computed(() => queryParams.dimension === 'doctor')
const summaryCards = computed(() => isDoctorDimension.value ? doctorSummaryCards : defaultSummaryCards)
const tableColumns = computed(() => isDoctorDimension.value ? doctorTableColumns : defaultTableColumns)
const dateRangeText = computed(() => {
if (!overview.date_range?.length) return '未选择'
return `${overview.date_range[0]}${overview.date_range[1]}`
@@ -438,7 +461,14 @@ const dateRangeText = computed(() => {
const rankingChartHasData = computed(() => overview.charts.ranking.names.length > 0)
const amountPieHasData = computed(() => overview.charts.amount_share.length > 0)
const fanPieHasData = computed(() => overview.charts.fan_share.length > 0)
const secondaryPieHasData = computed(() =>
isDoctorDimension.value
? (overview.charts.order_share?.length || 0) > 0
: (overview.charts.fan_share?.length || 0) > 0
)
const amountPieTitle = computed(() => isDoctorDimension.value ? '开药金额占比' : '诊单金额占比')
const secondaryPieTitle = computed(() => isDoctorDimension.value ? '接诊单数占比' : '加粉占比')
const secondaryPieEmptyText = computed(() => isDoctorDimension.value ? '暂无接诊数据' : '暂无加粉数据')
const rankingChartData = computed(() => {
const ranking = overview.charts?.ranking || {}
@@ -448,56 +478,110 @@ const rankingChartData = computed(() => {
return {
names,
amounts: normalize(Array.isArray(ranking.amounts) ? ranking.amounts : []),
appointmentCounts: normalize(Array.isArray(ranking.appointment_counts) ? ranking.appointment_counts : []),
interviewCounts: normalize(Array.isArray(ranking.interview_counts) ? ranking.interview_counts : []),
orderCounts: normalize(Array.isArray(ranking.order_counts) ? ranking.order_counts : []),
fanCounts: normalize(Array.isArray(ranking.fan_counts) ? ranking.fan_counts : []),
fanCounts: normalize(Array.isArray(ranking.fan_counts) ? ranking.fan_counts : [])
}
})
const rankingChartOption = computed(() => ({
tooltip: { trigger: 'axis' },
legend: { data: ['诊单金额', '接诊诊单', '加粉数'] },
grid: { left: 48, right: 24, top: 48, bottom: 48 },
xAxis: {
type: 'category',
data: rankingChartData.value.names,
axisLabel: {
interval: 0,
rotate: rankingChartData.value.names.length > 6 ? 25 : 0
}
},
yAxis: [
{ type: 'value', name: '金额 / 数量' }
],
series: [
{
name: '诊单金额',
type: 'bar',
barMaxWidth: 36,
data: rankingChartData.value.amounts,
itemStyle: { color: '#4a78ff' }
const rankingChartOption = computed(() => {
const common = {
tooltip: { trigger: 'axis' },
grid: { left: 48, right: 24, top: 48, bottom: 48 },
xAxis: {
type: 'category',
data: rankingChartData.value.names,
axisLabel: {
interval: 0,
rotate: rankingChartData.value.names.length > 6 ? 25 : 0
}
},
{
name: '接诊诊单',
type: 'line',
smooth: true,
showSymbol: true,
symbolSize: 8,
lineStyle: { width: 3 },
data: rankingChartData.value.orderCounts,
itemStyle: { color: '#15b8a6' }
},
{
name: '加粉数',
type: 'line',
smooth: true,
showSymbol: true,
symbolSize: 8,
lineStyle: { width: 3 },
data: rankingChartData.value.fanCounts,
itemStyle: { color: '#ff9f43' }
yAxis: [
{ type: 'value', name: '金额 / 数量' }
]
}
if (!isDoctorDimension.value) {
return {
...common,
legend: { data: ['诊单金额', '接诊诊单', '加粉数'] },
series: [
{
name: '诊单金额',
type: 'bar',
barMaxWidth: 36,
data: rankingChartData.value.amounts,
itemStyle: { color: '#4a78ff' }
},
{
name: '接诊诊单',
type: 'line',
smooth: true,
showSymbol: true,
symbolSize: 8,
lineStyle: { width: 3 },
data: rankingChartData.value.orderCounts,
itemStyle: { color: '#15b8a6' }
},
{
name: '加粉数',
type: 'line',
smooth: true,
showSymbol: true,
symbolSize: 8,
lineStyle: { width: 3 },
data: rankingChartData.value.fanCounts,
itemStyle: { color: '#ff9f43' }
}
]
}
]
}))
}
return {
...common,
legend: { data: ['开药金额', '挂号数', '面诊数', '接诊单数'] },
series: [
{
name: '开药金额',
type: 'bar',
barMaxWidth: 36,
data: rankingChartData.value.amounts,
itemStyle: { color: '#4a78ff' }
},
{
name: '挂号数',
type: 'line',
smooth: true,
showSymbol: true,
symbolSize: 8,
lineStyle: { width: 3 },
data: rankingChartData.value.appointmentCounts,
itemStyle: { color: '#15b8a6' }
},
{
name: '面诊数',
type: 'line',
smooth: true,
showSymbol: true,
symbolSize: 8,
lineStyle: { width: 3 },
data: rankingChartData.value.interviewCounts,
itemStyle: { color: '#ff9f43' }
},
{
name: '接诊单数',
type: 'line',
smooth: true,
showSymbol: true,
symbolSize: 8,
lineStyle: { width: 3 },
data: rankingChartData.value.orderCounts,
itemStyle: { color: '#7c3aed' }
}
]
}
})
const amountPieOption = computed(() => ({
tooltip: { trigger: 'item' },
@@ -511,7 +595,7 @@ const amountPieOption = computed(() => ({
},
series: [
{
name: '诊单金额',
name: amountPieTitle.value,
type: 'pie',
radius: ['42%', '70%'],
center: ['50%', '42%'],
@@ -531,7 +615,7 @@ const amountPieOption = computed(() => ({
]
}))
const fanPieOption = computed(() => ({
const secondaryPieOption = computed(() => ({
tooltip: { trigger: 'item' },
legend: {
bottom: 0,
@@ -543,7 +627,7 @@ const fanPieOption = computed(() => ({
},
series: [
{
name: '加粉数',
name: secondaryPieTitle.value,
type: 'pie',
radius: ['42%', '70%'],
center: ['50%', '42%'],
@@ -558,7 +642,7 @@ const fanPieOption = computed(() => ({
labelLayout: {
hideOverlap: true,
},
data: overview.charts.fan_share
data: isDoctorDimension.value ? overview.charts.order_share : overview.charts.fan_share
}
]
}))
+6 -1
View File
@@ -556,7 +556,11 @@ const validatePhone = (rule: any, value: any, callback: any) => {
}
const validateIdCard = (rule: any, value: any, callback: any) => {
if (value && !/^[1-9]\d{5}(18|19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dXx]$/.test(value)) {
if (!value) {
callback()
return
}
if (!/^[1-9]\d{5}(18|19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dXx]$/.test(value)) {
callback(new Error('身份证号格式不正确'))
return
}
@@ -572,6 +576,7 @@ const formRules = {
diagnosis_type: [{ required: true, message: '请选择诊断类型', trigger: 'change' }],
syndrome_type: [{ required: true, message: '请选择证型', trigger: 'change' }],
diabetes_type: [{ required: true, message: '请选择糖尿病期数', trigger: 'change' }],
local_hospital_name: [{ required: true, message: '请输入当地就诊医院名称', trigger: 'blur' }],
diabetes_discovery_year: [{ max: 50, message: '最多50个字符', trigger: 'blur' }]
}
+146 -27
View File
@@ -67,26 +67,66 @@
>
<!-- 基本信息 -->
<el-divider content-position="left">基本信息</el-divider>
<el-alert
v-if="mode === 'edit' && patientBasicLocked && !canEditPatientBasicFields"
type="warning"
:closable="false"
show-icon
class="mb-3"
>
<template v-if="latestPrescriptionOrder">
最近业务订单 #{{ latestPrescriptionOrder.id }} 状态为{{ latestPrescriptionOrder.fulfillment_status_text }}患者基本信息不可修改
</template>
<template v-else>
存在未完成的业务订单患者基本信息不可修改
</template>
</el-alert>
<el-form-item label="诊单ID">
<el-input v-model="formData.patient_id" disabled placeholder="系统自动生成" />
</el-form-item>
<fieldset
:disabled="!canEditPatientBasicFields"
class="diagnosis-patient-basic-fieldset border-0 min-w-0 p-0 m-0"
>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="姓名" prop="patient_name">
<el-input
v-model="formData.patient_name"
placeholder="请输入姓名"
:disabled="!canEditPatientBasicFields"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="身份证号" prop="id_card">
<template v-if="showIdCardMaskedEdit">
<el-input
:model-value="
idCardRevealUnlocked ? originalIdCard || '' : maskIdCard(originalIdCard || '')
"
readonly
placeholder="点击可查看完整身份证号"
maxlength="18"
class="cursor-pointer phone-mask-toggle"
@click="toggleIdCardReveal"
/>
<p
v-if="originalIdCard && !idCardRevealUnlocked"
class="text-xs text-gray-400 mt-1"
>
当前为脱敏显示点击输入框可查看完整号码
</p>
</template>
<el-input
v-else
v-model="formData.id_card"
placeholder="请输入身份证号"
maxlength="18"
:disabled="!canEditPatientBasicFields"
@focus="handleIdCardFocus"
@blur="handleIdCardBlur"
/>
@@ -120,6 +160,7 @@
v-model="formData.phone"
placeholder="请输入手机号"
maxlength="11"
:disabled="!canEditPatientBasicFields"
@focus="handlePhoneFocus"
@blur="handlePhoneBlur"
/>
@@ -127,7 +168,7 @@
</el-col>
<el-col :span="12">
<el-form-item label="性别" prop="gender">
<el-radio-group v-model="formData.gender">
<el-radio-group v-model="formData.gender" :disabled="!canEditPatientBasicFields">
<el-radio :label="1"></el-radio>
<el-radio :label="0"></el-radio>
</el-radio-group>
@@ -144,11 +185,14 @@
:max="150"
placeholder="请输入年龄"
class="w-full"
:disabled="!canEditPatientBasicFields"
:controls="canEditPatientBasicFields"
/>
</el-form-item>
</el-col>
</el-row>
</fieldset>
<!-- 生命体征 -->
@@ -778,9 +822,32 @@ const submitting = ref(false)
/** 拥有后手机号可正常编辑,失焦不再强制脱敏 */
const hasPhonePlainPermission = computed(() => hasPermission(['tcm.diagnosis/phonePlain']))
const hasDailyRecordPermission = computed(() => hasPermission(['tcm.diagnosis/dailyRecord']))
/** 编辑且无明文权限:只读脱敏,点击切换查看完整号 */
const showPhoneMaskedEdit = computed(() => mode.value === 'edit' && !hasPhonePlainPermission.value)
const patientBasicLocked = ref(false)
const canEditPatientBasicFromApi = ref(true)
const latestPrescriptionOrder = ref<{
id: number
fulfillment_status: number
fulfillment_status_text: string
} | null>(null)
const canEditPatientBasicFields = computed(() => {
if (viewOnly.value) return false
if (mode.value === 'add') return true
return canEditPatientBasicFromApi.value
})
/** 编辑且无明文权限或基本信息锁定:只读脱敏,点击切换查看完整号 */
const showPhoneMaskedEdit = computed(() => {
if (mode.value !== 'edit') return false
if (!canEditPatientBasicFields.value) return true
return !hasPhonePlainPermission.value
})
const phoneRevealUnlocked = ref(false)
/** 编辑且无明文权限或基本信息锁定:只读脱敏,点击切换查看完整身份证号 */
const showIdCardMaskedEdit = computed(() => {
if (mode.value !== 'edit') return false
if (!canEditPatientBasicFields.value) return true
return !hasPhonePlainPermission.value
})
const idCardRevealUnlocked = ref(false)
const activeTab = ref('basic')
const drawerTitle = computed(() => {
if (viewOnly.value) return '诊单详情'
@@ -920,6 +987,11 @@ const togglePhoneReveal = () => {
phoneRevealUnlocked.value = !phoneRevealUnlocked.value
}
const toggleIdCardReveal = () => {
if (!originalIdCard.value) return
idCardRevealUnlocked.value = !idCardRevealUnlocked.value
}
const maskIdCard = (idCard: string) => {
if (!idCard) return idCard
if (idCard.length === 15) {
@@ -990,43 +1062,40 @@ const handlePhoneBlur = async () => {
}
}
// -
// -
const handleIdCardFocus = () => {
isIdCardFocused.value = true
if (originalIdCard.value) {
if (!originalIdCard.value) return
if (hasPhonePlainPermission.value) {
formData.value.id_card = originalIdCard.value
return
}
if (mode.value === 'add') {
formData.value.id_card = originalIdCard.value
}
}
// -
//
const handleIdCardBlur = async () => {
isIdCardFocused.value = false
//
if (formData.value.id_card && (formData.value.id_card.length === 15 || formData.value.id_card.length === 18)) {
originalIdCard.value = formData.value.id_card
//
if (/^[1-9]\d{5}(18|19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dXx]$/.test(formData.value.id_card)) {
//
const id = formData.value.id_card
const birthYear = parseInt(id.substring(6, 10))
const birthMonth = parseInt(id.substring(10, 12))
const birthDay = parseInt(id.substring(12, 14))
const processIdCard = async (idCard: string) => {
if (/^[1-9]\d{5}(18|19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dXx]$/.test(idCard)) {
const birthYear = parseInt(idCard.substring(6, 10))
const birthMonth = parseInt(idCard.substring(10, 12))
const birthDay = parseInt(idCard.substring(12, 14))
const today = new Date()
let age = today.getFullYear() - birthYear
if (today.getMonth() + 1 < birthMonth || (today.getMonth() + 1 === birthMonth && today.getDate() < birthDay)) {
age--
}
formData.value.age = age
//
try {
const result = await checkIdCard({
id_card: formData.value.id_card,
id_card: idCard,
id: formData.value.id || ''
})
if (result.exists) {
ElMessage.warning(result.message)
}
@@ -1034,8 +1103,23 @@ const handleIdCardBlur = async () => {
console.error('检查身份证号失败:', error)
}
}
//
}
if (hasPhonePlainPermission.value) {
if (formData.value.id_card && (formData.value.id_card.length === 15 || formData.value.id_card.length === 18)) {
originalIdCard.value = formData.value.id_card
await processIdCard(formData.value.id_card)
}
return
}
if (mode.value !== 'add') {
return
}
if (formData.value.id_card && (formData.value.id_card.length === 15 || formData.value.id_card.length === 18)) {
originalIdCard.value = formData.value.id_card
await processIdCard(formData.value.id_card)
formData.value.id_card = maskIdCard(formData.value.id_card)
}
}
@@ -1058,7 +1142,11 @@ const validatePhone = (rule: any, value: any, callback: any) => {
const validateIdCard = (rule: any, value: any, callback: any) => {
// 使
const idCardToValidate = originalIdCard.value || value
if (idCardToValidate && !/^[1-9]\d{5}(18|19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dXx]$/.test(idCardToValidate)) {
if (!idCardToValidate) {
callback()
return
}
if (!/^[1-9]\d{5}(18|19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dXx]$/.test(idCardToValidate)) {
callback(new Error('身份证号格式不正确'))
return
}
@@ -1073,6 +1161,7 @@ const formRules = {
age: [{ required: true, message: '请输入年龄', trigger: 'blur' }],
fasting_blood_sugar: [{ required: true, message: '请输入空腹血糖', trigger: 'blur' }],
diagnosis_type: [{ required: true, message: '请选择诊断类型', trigger: 'change' }],
local_hospital_name: [{ required: true, message: '请输入当地就诊医院名称', trigger: 'blur' }],
diabetes_discovery_year: [{ max: 50, message: '最多50个字符', trigger: 'blur' }],
}
@@ -1167,9 +1256,12 @@ async function loadDiagnosisDetailIntoForm(id: number): Promise<void> {
originalPhone.value = data.phone || ''
originalIdCard.value = data.id_card || ''
patientBasicLocked.value = !!data.patient_basic_locked
canEditPatientBasicFromApi.value = data.can_edit_patient_basic !== false
latestPrescriptionOrder.value = data.latest_prescription_order ?? null
data.phone = hasPhonePlainPermission.value ? data.phone || '' : maskPhone(data.phone || '')
data.id_card = maskIdCard(data.id_card || '')
data.id_card = hasPhonePlainPermission.value ? data.id_card || '' : maskIdCard(data.id_card || '')
formData.value = data
formData.value.create_source = data.create_source ?? ''
@@ -1178,12 +1270,20 @@ async function loadDiagnosisDetailIntoForm(id: number): Promise<void> {
formData.value.diabetes_discovery_year = y == null || y === '' ? '' : String(y)
}
const resetPatientBasicLockState = () => {
patientBasicLocked.value = false
canEditPatientBasicFromApi.value = true
latestPrescriptionOrder.value = null
}
const open = async (type: string, id?: number) => {
viewOnly.value = false
mode.value = type
visible.value = true
activeTab.value = 'basic' //
phoneRevealUnlocked.value = false
idCardRevealUnlocked.value = false
resetPatientBasicLockState()
//
await getDictOptions()
@@ -1206,6 +1306,8 @@ const openViewOnly = async (id: number) => {
visible.value = true
activeTab.value = 'basic'
phoneRevealUnlocked.value = false
idCardRevealUnlocked.value = false
resetPatientBasicLockState()
await getDictOptions()
await loadDiagnosisDetailIntoForm(id)
}
@@ -1230,6 +1332,7 @@ const handleSubmit = async () => {
if (result && result.id) {
mode.value = 'edit'
phoneRevealUnlocked.value = false
idCardRevealUnlocked.value = false
formData.value.id = result.id
// patient_id
@@ -1244,7 +1347,9 @@ const handleSubmit = async () => {
formData.value.phone = hasPhonePlainPermission.value
? detail.phone || ''
: maskPhone(detail.phone || '')
formData.value.id_card = maskIdCard(detail.id_card || '')
formData.value.id_card = hasPhonePlainPermission.value
? detail.id_card || ''
: maskIdCard(detail.id_card || '')
} catch (error) {
console.error('获取详情失败:', error)
}
@@ -1346,6 +1451,7 @@ const handleClose = () => {
isPhoneFocused.value = false
isIdCardFocused.value = false
phoneRevealUnlocked.value = false
idCardRevealUnlocked.value = false
viewOnly.value = false
visible.value = false
@@ -1370,6 +1476,19 @@ defineExpose({
}
}
.diagnosis-patient-basic-fieldset:disabled {
opacity: 1;
:deep(.el-input__wrapper),
:deep(.el-input-number),
:deep(.el-input-number__decrease),
:deep(.el-input-number__increase),
:deep(.el-radio),
:deep(.el-radio__input) {
cursor: not-allowed;
pointer-events: none;
}
}
:deep(.el-form) {
padding-bottom: 20px;
font-size: 12px;
+1 -8
View File
@@ -77,14 +77,7 @@ module.exports = {
mask: 'var(--el-mask-color)'
},
fontFamily: {
sans: [
'Noto Sans SC',
'PingFang SC',
'Microsoft YaHei',
'Hiragino Sans GB',
'Arial',
'sans-serif'
]
sans: ['PingFang SC', 'Arial', 'Hiragino Sans GB', 'Microsoft YaHei', 'sans-serif']
},
boxShadow: {
DEFAULT: 'var(--el-box-shadow)',
@@ -17,6 +17,7 @@ use app\adminapi\logic\stats\YejiStatsLogic;
* - GET stats.yejiStats/leadLines 进线数据明细(add_external_contact 逐条)
* - GET stats.yejiStats/appointmentLines 医助排行榜「预约诊单」挂号逐条明细
* - GET stats.yejiStats/revisitBreakdown 二中心复诊下钻:医助 × 业务订单笔数
* - GET stats.yejiStats/assignLines 被指派数明细(医助×诊单去重,与看板同口径)
*/
class YejiStatsController extends BaseAdminController
{
@@ -141,4 +142,13 @@ class YejiStatsController extends BaseAdminController
return $this->data(YejiStatsLogic::revisitDeptAssistantBreakdown($params, $this->adminId, $this->adminInfo));
}
/** 被指派数明细:与看板「被指派数」同口径(区间内非继承指派,医助×诊单去重) */
public function assignLines()
{
@set_time_limit(120);
$params = $this->request->get();
return $this->data(YejiStatsLogic::assignLines($params, $this->adminId, $this->adminInfo));
}
}
@@ -87,7 +87,7 @@ class DiagnosisController extends BaseAdminController
public function edit()
{
$params = (new DiagnosisValidate())->post()->goCheck('edit');
$result = DiagnosisLogic::edit($params);
$result = DiagnosisLogic::edit($params, $this->adminInfo);
if ($result) {
return $this->success('编辑成功', [], 1, 1);
}
@@ -116,7 +116,7 @@ class DiagnosisController extends BaseAdminController
{
$params = (new DiagnosisValidate())->goCheck('id');
$result = DiagnosisLogic::detail($params);
$result = DiagnosisLogic::detail($params, $this->adminInfo);
DiagnosisLogic::markAssignRead((int) ($params['id'] ?? 0), $this->adminId);
return $this->data($result);
}
@@ -286,6 +286,32 @@ class PrescriptionOrderController extends BaseAdminController
return $this->success('已撤回', $result);
}
/**
* @notes 批量将处方业务订单改派给其他医助(写入 creator_id 并逐单记操作日志)
*/
public function batchAssignAssistant()
{
if (!PrescriptionOrderLogic::canSeeAllPrescriptionOrders($this->adminInfo)) {
return $this->fail('无权限批量改派订单');
}
$params = $this->request->post();
$rawIds = $params['order_ids'] ?? [];
if (!\is_array($rawIds) || $rawIds === []) {
return $this->fail('请选择订单');
}
$assistantId = (int) ($params['assistant_id'] ?? 0);
$result = PrescriptionOrderLogic::batchAssignAssistant($rawIds, $assistantId, $this->adminId, $this->adminInfo);
if ($result === false) {
return $this->fail(PrescriptionOrderLogic::getError());
}
$msg = '已改派 ' . (int) $result['success'] . ' 单';
if (!empty($result['errors'])) {
$msg .= '' . implode('', $result['errors']);
}
return $this->success($msg, $result);
}
/**
* 获取业务订单操作日志
*/
@@ -9,6 +9,7 @@ use app\adminapi\logic\dept\DeptLogic;
use app\adminapi\logic\stats\YejiStatsLogic;
use app\adminapi\logic\tcm\PrescriptionOrderLogic;
use app\common\enum\ExportEnum;
use app\common\cache\ExportCache;
use app\common\lists\ListsExcelInterface;
use app\common\lists\ListsExtendInterface;
use app\common\lists\ListsSearchInterface;
@@ -26,6 +27,12 @@ use app\common\service\gancao\GancaoScmRecipelService;
use think\facade\Config;
use think\facade\Db;
use think\db\Query;
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
use PhpOffice\PhpSpreadsheet\IOFactory;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Style\Alignment;
use PhpOffice\PhpSpreadsheet\Style\Border;
use PhpOffice\PhpSpreadsheet\Style\Fill;
class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchInterface, ListsExtendInterface, ListsExcelInterface
{
@@ -108,6 +115,7 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
$this->applyExpressKeywordFilter($query);
$this->applyServiceChannelFilter($query);
$this->applySupplyModeFilter($query);
$this->applyHasAuxFormulaFilter($query);
$this->applyAuditAdminFilter($query);
if (!$this->shouldBypassListVisibilityForDiagnosisEdit()) {
// 业绩看板按部门点「合计业绩」/复诊下钻:部门或数据域内医助筛选已收口,勿再叠「仅本人订单」
@@ -347,6 +355,39 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
}
}
/**
* 是否含辅方:关联处方 herbs 中是否存在 formula_type=辅方(与 PrescriptionOrderLogic::detail 口径一致)
*
* 入参 has_aux_formula1 含辅方 | 0 不含辅方,空表示不限
*/
private function applyHasAuxFormulaFilter($query): void
{
$raw = $this->params['has_aux_formula'] ?? '';
if ($raw === '' || $raw === null) {
return;
}
$wantAux = (int) $raw === 1;
$poTbl = (new PrescriptionOrder())->getTable();
$rxTbl = (new Prescription())->getTable();
// herbs 为 JSON 字段,ThinkPHP 写入时中文按 Unicode 转义存储(辅方 => \u8f85\u65b9)。
// 用 ESCAPE '~' 让反斜杠按字面匹配;同时兼容极少数原文「辅方」写法。
$auxEsc = '%"formula_type":"\\\\u8f85\\\\u65b9"%';
$auxRaw = '%"formula_type":"辅方"%';
$herbsHasAux = "(IFNULL(rx.`herbs`,'') LIKE '{$auxEsc}' ESCAPE '~'"
. " OR IFNULL(rx.`herbs`,'') LIKE '{$auxRaw}')";
$existsSql = "SELECT 1 FROM `{$rxTbl}` rx WHERE rx.`id` = `{$poTbl}`.`prescription_id`"
. " AND rx.`delete_time` IS NULL AND {$herbsHasAux}";
if ($wantAux) {
$query->whereExists($existsSql);
return;
}
$query->whereExists(
"SELECT 1 FROM `{$rxTbl}` rx WHERE rx.`id` = `{$poTbl}`.`prescription_id`"
. " AND rx.`delete_time` IS NULL AND NOT ({$herbsHasAux})"
);
}
/**
* 非「业务订单全量」角色:默认仅本人创建;若持有 viewOrdersForOwnPrescription 则额外包含关联处方开方人为本人的订单。
* 显式筛选 assistant_id 时:若该医助落在当前账号数据域内,则允许按订单创建人命中(与 applyDoctorAssistantFilters 一致)。
@@ -783,6 +824,7 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
'export_medication_days' => '天数',
'export_amount' => '总金额',
'export_paid_amount' => '已付金额',
'export_linked_pay_records' => '关联收款记录',
'export_refund_amount' => '退款金额',
'export_agency_collect' => '代收金额',
'export_tracking_number' => '快递单号',
@@ -795,6 +837,133 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
];
}
/**
* 处方业务订单导出:深色表头、斑马纹、长文本自动换行(覆盖默认亮蓝表头)
*
* @param array<string, string> $excelFields
* @param array<int, array<string, mixed>> $lists
*/
public function createExcel($excelFields, $lists): string
{
$title = array_values($excelFields);
$fieldKeys = array_keys($excelFields);
$data = [];
foreach ($lists as $row) {
$temp = [];
foreach ($excelFields as $key => $excelField) {
$fieldData = $row[$key] ?? '';
if (is_numeric($fieldData) && strlen((string) $fieldData) >= 12) {
$fieldData .= "\t";
}
$temp[$key] = $fieldData;
}
$data[] = $temp;
}
$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
$sheet->setTitle('处方业务订单');
foreach ($title as $key => $value) {
$sheet->setCellValueByColumnAndRow($key + 1, 1, $value);
}
$rowNum = 2;
foreach ($data as $item) {
$column = 1;
foreach ($item as $value) {
$sheet->setCellValueByColumnAndRow($column, $rowNum, $value);
++$column;
}
++$rowNum;
}
$highest = $sheet->getHighestRowAndColumn();
$highestRow = (int) $highest['row'];
$lastColumn = (string) $highest['column'];
$titleScope = 'A1:' . $lastColumn . '1';
$allScope = 'A1:' . $lastColumn . $highestRow;
$bodyScope = 'A2:' . $lastColumn . $highestRow;
$sheet->freezePane('A2');
$sheet->getRowDimension(1)->setRowHeight(26);
$sheet->getStyle($titleScope)->getFill()
->setFillType(Fill::FILL_SOLID)
->getStartColor()->setARGB('FF334155');
$sheet->getStyle($titleScope)->getFont()->getColor()->setARGB('FFFFFFFF');
$sheet->getStyle($titleScope)->getFont()->setBold(true);
$sheet->getStyle($titleScope)->getAlignment()
->setHorizontal(Alignment::HORIZONTAL_CENTER)
->setVertical(Alignment::VERTICAL_CENTER);
$sheet->getStyle($allScope)->getBorders()->getAllBorders()
->setBorderStyle(Border::BORDER_THIN)
->getColor()->setARGB('FFE2E8F0');
$sheet->getStyle($bodyScope)->getAlignment()
->setVertical(Alignment::VERTICAL_TOP)
->setWrapText(true);
$wrapWideKeys = [
'export_linked_pay_records',
'export_prescription_name',
'export_guahao_channel_source',
'export_assistant_dept',
'export_service_package',
];
$fixedWidths = [
'export_fulfillment_status_text' => 12,
'export_order_time' => 18,
'export_patient_gender' => 6,
'export_patient_age' => 6,
'export_medication_days' => 6,
'export_amount' => 10,
'export_paid_amount' => 10,
'export_refund_amount' => 10,
'export_agency_collect' => 10,
'export_sign_time' => 12,
'export_supply_mode' => 10,
'export_linked_pay_records' => 52,
'export_prescription_name' => 34,
'export_guahao_channel_source' => 22,
'export_assistant_dept' => 24,
'export_service_package' => 18,
'export_tracking_number' => 18,
];
foreach ($fieldKeys as $idx => $fieldKey) {
$colLetter = Coordinate::stringFromColumnIndex($idx + 1);
if (isset($fixedWidths[$fieldKey])) {
$sheet->getColumnDimension($colLetter)->setWidth($fixedWidths[$fieldKey]);
} elseif (!in_array($fieldKey, $wrapWideKeys, true)) {
$sheet->getColumnDimension($colLetter)->setAutoSize(true);
} else {
$sheet->getColumnDimension($colLetter)->setWidth(28);
}
}
for ($r = 2; $r <= $highestRow; ++$r) {
if ($r % 2 === 0) {
$sheet->getStyle('A' . $r . ':' . $lastColumn . $r)->getFill()
->setFillType(Fill::FILL_SOLID)
->getStartColor()->setARGB('FFF8FAFC');
}
}
$writer = IOFactory::createWriter($spreadsheet, 'Xlsx');
$exportCache = new ExportCache();
$src = $exportCache->getSrc();
if (!file_exists($src)) {
mkdir($src, 0775, true);
}
$writer->save($src . $this->fileName);
$vars = ['file' => $exportCache->setFile($this->fileName)];
return (string) url('adminapi/download/export', $vars, true, true);
}
/**
* 业绩看板侧栏专用:约诊/接诊条数,与 YejiStatsLogic::applyConsultFiltersAlignedWithAppointmentLists 同口径
* doctor_appointment.status=3appointment_date 落入区间,有效医助 COALESCE(挂号.assistant_id,诊单.assistant_id))。
@@ -48,8 +48,8 @@ class ConversionLogic
$emptyResult = [
'dimension' => $dimension,
'date_range' => [$startDate, $endDate],
'summary' => self::buildSummary([]),
'charts' => self::buildCharts([]),
'summary' => self::buildSummary([], $dimension),
'charts' => self::buildCharts([], $dimension),
'lists' => [],
'count' => 0,
'page_no' => $pageNo,
@@ -57,8 +57,8 @@ class ConversionLogic
'extend' => [
'dimension' => $dimension,
'date_range' => [$startDate, $endDate],
'summary' => self::buildSummary([]),
'charts' => self::buildCharts([]),
'summary' => self::buildSummary([], $dimension),
'charts' => self::buildCharts([], $dimension),
],
];
if ($includeFilters) {
@@ -75,8 +75,8 @@ class ConversionLogic
$result = [
'dimension' => $dimension,
'date_range' => [$startDate, $endDate],
'summary' => self::buildSummary([]),
'charts' => self::buildCharts([]),
'summary' => self::buildSummary([], $dimension),
'charts' => self::buildCharts([], $dimension),
'lists' => [],
'count' => 0,
'page_no' => $pageNo,
@@ -84,8 +84,8 @@ class ConversionLogic
'extend' => [
'dimension' => $dimension,
'date_range' => [$startDate, $endDate],
'summary' => self::buildSummary([]),
'charts' => self::buildCharts([]),
'summary' => self::buildSummary([], $dimension),
'charts' => self::buildCharts([], $dimension),
],
];
if ($includeFilters) {
@@ -124,8 +124,8 @@ class ConversionLogic
$result = [
'dimension' => $dimension,
'date_range' => [$startDate, $endDate],
'summary' => self::buildSummary([]),
'charts' => self::buildCharts([]),
'summary' => self::buildSummary([], $dimension),
'charts' => self::buildCharts([], $dimension),
'lists' => [],
'count' => 0,
'page_no' => $pageNo,
@@ -133,8 +133,8 @@ class ConversionLogic
'extend' => [
'dimension' => $dimension,
'date_range' => [$startDate, $endDate],
'summary' => self::buildSummary([]),
'charts' => self::buildCharts([]),
'summary' => self::buildSummary([], $dimension),
'charts' => self::buildCharts([], $dimension),
],
];
if ($includeFilters) {
@@ -191,8 +191,8 @@ class ConversionLogic
$result = [
'dimension' => $dimension,
'date_range' => [$startDate, $endDate],
'summary' => self::buildSummary($allRows),
'charts' => self::buildCharts($chartRows),
'summary' => self::buildSummary($allRows, $dimension),
'charts' => self::buildCharts($chartRows, $dimension),
'lists' => $pagedRows,
'count' => count($allRows),
'page_no' => $pageNo,
@@ -200,8 +200,8 @@ class ConversionLogic
'extend' => [
'dimension' => $dimension,
'date_range' => [$startDate, $endDate],
'summary' => self::buildSummary($allRows),
'charts' => self::buildCharts($chartRows),
'summary' => self::buildSummary($allRows, $dimension),
'charts' => self::buildCharts($chartRows, $dimension),
],
];
if ($includeFilters) {
@@ -226,8 +226,8 @@ class ConversionLogic
$result = [
'dimension' => $dimension,
'date_range' => [$startDate, $endDate],
'summary' => self::buildSummary($rows),
'charts' => self::buildCharts($rows),
'summary' => self::buildSummary($rows, $dimension),
'charts' => self::buildCharts($rows, $dimension),
'lists' => array_slice($rows, $offset, $pageSize),
'count' => $count,
'page_no' => $pageNo,
@@ -235,8 +235,8 @@ class ConversionLogic
'extend' => [
'dimension' => $dimension,
'date_range' => [$startDate, $endDate],
'summary' => self::buildSummary($rows),
'charts' => self::buildCharts($rows),
'summary' => self::buildSummary($rows, $dimension),
'charts' => self::buildCharts($rows, $dimension),
],
];
if ($includeFilters) {
@@ -1226,7 +1226,7 @@ class ConversionLogic
$entity['paid_appointment_rate'] = self::percent($paidAppointmentCount, $addFansCount);
$entity['open_appointment_rate'] = self::percent($paidAppointmentCount, $totalOpenCount);
$entity['interview_rate'] = self::percent($interviewCount, $appointmentTotalCount);
$entity['receive_rate'] = self::percent($completedOrderCount, $addFansCount);
$entity['receive_rate'] = self::receiveRate($completedOrderCount, $addFansCount, $interviewCount, $dimension);
$entity['interview_receive_rate'] = self::percent($completedOrderCount, $interviewCount);
$entity['open_receive_rate'] = self::percent($completedOrderCount, $totalOpenCount);
$entity['avg_unit_price'] = self::safeDivideMoney($completedOrderAmount, $completedOrderCount);
@@ -1459,7 +1459,7 @@ class ConversionLogic
$node['paid_appointment_rate'] = self::percent($paidAppointmentCount, $addFansCount);
$node['open_appointment_rate'] = self::percent($paidAppointmentCount, $totalOpenCount);
$node['interview_rate'] = self::percent($interviewCount, $appointmentTotalCount);
$node['receive_rate'] = self::percent($completedOrderCount, $addFansCount);
$node['receive_rate'] = self::receiveRate($completedOrderCount, $addFansCount, $interviewCount, 'dept');
$node['interview_receive_rate'] = self::percent($completedOrderCount, $interviewCount);
$node['open_receive_rate'] = self::percent($completedOrderCount, $totalOpenCount);
$node['avg_unit_price'] = self::safeDivideMoney($completedOrderAmount, $completedOrderCount);
@@ -2010,7 +2010,12 @@ class ConversionLogic
'paid_appointment_rate' => self::percent($paidAppointmentCount, $addFansCount),
'open_appointment_rate' => self::percent($paidAppointmentCount, $totalOpenCount),
'interview_rate' => self::percent($interviewCount, $appointmentTotalCount),
'receive_rate' => self::percent($completedOrderCount, $addFansCount),
'receive_rate' => self::receiveRate(
$completedOrderCount,
$addFansCount,
$interviewCount,
'member'
),
'interview_receive_rate' => self::percent($completedOrderCount, $interviewCount),
'open_receive_rate' => self::percent($completedOrderCount, $totalOpenCount),
'avg_unit_price' => self::safeDivideMoney($completedOrderAmount, $completedOrderCount),
@@ -2149,7 +2154,7 @@ class ConversionLogic
* @param array<int, array<string, mixed>> $rows
* @return array<string, mixed>
*/
private static function buildSummary(array $rows): array
private static function buildSummary(array $rows, string $dimension = 'dept'): array
{
$summary = [
'add_fans_count' => 0,
@@ -2181,7 +2186,12 @@ class ConversionLogic
$summary['paid_appointment_rate'] = self::percent($summary['paid_appointment_count'], $summary['add_fans_count']);
$summary['open_appointment_rate'] = self::percent($summary['paid_appointment_count'], $summary['total_open_count']);
$summary['interview_rate'] = self::percent($summary['interview_count'], $summary['appointment_total_count']);
$summary['receive_rate'] = self::percent($summary['completed_order_count'], $summary['add_fans_count']);
$summary['receive_rate'] = self::receiveRate(
$summary['completed_order_count'],
$summary['add_fans_count'],
$summary['interview_count'],
$dimension
);
$summary['interview_receive_rate'] = self::percent($summary['completed_order_count'], $summary['interview_count']);
$summary['open_receive_rate'] = self::percent($summary['completed_order_count'], $summary['total_open_count']);
$summary['avg_unit_price'] = self::safeDivideMoney($summary['completed_order_amount'], $summary['completed_order_count']);
@@ -2310,7 +2320,7 @@ class ConversionLogic
* @param array<int, array<string, mixed>> $rows
* @return array<string, mixed>
*/
private static function buildCharts(array $rows): array
private static function buildCharts(array $rows, string $dimension = 'dept'): array
{
$chartableRows = array_values(array_filter($rows, static fn (array $row): bool => !((bool)($row['_virtual_bucket'] ?? false))));
$topRows = array_slice($chartableRows, 0, 10);
@@ -2319,29 +2329,56 @@ class ConversionLogic
'names' => [],
'amounts' => [],
'order_counts' => [],
'fan_counts' => [],
'rois' => [],
];
if ($dimension === 'doctor') {
$ranking['appointment_counts'] = [];
$ranking['interview_counts'] = [];
} else {
$ranking['fan_counts'] = [];
$ranking['rois'] = [];
}
foreach ($topRows as $row) {
$ranking['names'][] = $row['name'];
$ranking['amounts'][] = $row['completed_order_amount'];
$ranking['order_counts'][] = $row['completed_order_count'];
$ranking['fan_counts'][] = $row['add_fans_count'];
$ranking['rois'][] = $row['roi'];
if ($dimension === 'doctor') {
$ranking['appointment_counts'][] = $row['appointment_total_count'];
$ranking['interview_counts'][] = $row['interview_count'];
} else {
$ranking['fan_counts'][] = $row['add_fans_count'];
$ranking['rois'][] = $row['roi'];
}
}
return [
$charts = [
'ranking' => $ranking,
'amount_share' => array_map(
static fn (array $row): array => ['name' => $row['name'], 'value' => $row['completed_order_amount']],
array_filter($topRows, static fn (array $row): bool => (float)$row['completed_order_amount'] > 0)
),
'fan_share' => array_map(
];
if ($dimension === 'doctor') {
$charts['order_share'] = array_map(
static fn (array $row): array => ['name' => $row['name'], 'value' => $row['completed_order_count']],
array_filter($topRows, static fn (array $row): bool => (int)$row['completed_order_count'] > 0)
);
} else {
$charts['fan_share'] = array_map(
static fn (array $row): array => ['name' => $row['name'], 'value' => $row['add_fans_count']],
array_filter($topRows, static fn (array $row): bool => (int)$row['add_fans_count'] > 0)
),
];
);
}
return $charts;
}
private static function receiveRate(int $completedOrderCount, int $addFansCount, int $interviewCount, string $dimension): float
{
$denominator = $dimension === 'doctor' ? $interviewCount : $addFansCount;
return self::percent($completedOrderCount, $denominator);
}
private static function percent(int $numerator, int $denominator): float
@@ -2616,6 +2616,420 @@ class YejiStatsLogic
];
}
/**
* 被指派明细:与看板「被指派数」同口径(区间内非继承指派,医助×诊单去重)。
*
* @param array{
* start_date?:string,end_date?:string,dept_ids?:int[]|string,channel_code?:string,tag_id?:string,
* assistant_id?:int|string,admin_id?:int|string,dept_id?:int|string,
* page?:int|string,page_size?:int|string
* } $params
*
* @return array<string, mixed>
*/
public static function assignLines(array $params, int $viewerAdminId = 0, array $viewerAdminInfo = []): array
{
$c = self::resolveYejiContext($params);
if ($viewerAdminId > 0) {
self::applyYejiDataScope($c, $viewerAdminId, $viewerAdminInfo);
}
$assistantId = (int) ($params['assistant_id'] ?? $params['admin_id'] ?? 0);
if ($assistantId <= 0 && array_key_exists('dept_id', $params)) {
return self::yejiDeptAssignLinesInner($params, $c);
}
return self::yejiAssistantAssignLinesInner($params, $c, $assistantId);
}
/**
* @param array<string, mixed> $params
* @param array<string, mixed> $c
*
* @return array<string, mixed>
*/
private static function yejiDeptAssignLinesInner(array $params, array $c): array
{
$deptIdParam = (int) ($params['dept_id'] ?? 0);
$channelCode = (string) $c['channelCode'];
$deptName = $deptIdParam === 0
? '未归属中心'
: self::formatYejiDeptRowDisplayName($deptIdParam, $c['deptById']);
$empty = static function (string $note) use ($c, $channelCode, $deptIdParam, $deptName): array {
return self::yejiAssignLinesEmptyPayload($c, $note, 0, '', $deptIdParam, $deptName, $channelCode);
};
if ($deptIdParam <= 0) {
return $empty('未归属中心无被指派明细(看板该行为 0)。');
}
if (!in_array($deptIdParam, $c['tableRowDeptIds'], true)) {
return $empty('该部门不在当前展示部门筛选或数据权限范围内。');
}
$erCenterDeptSet = DeptLogic::getErCenterSubtreeDeptIdSet();
if ($channelCode !== '' && isset($erCenterDeptSet[$deptIdParam])) {
return $empty('与看板一致:二中心及其组织下级在选定渠道下被指派数计 0,无明细。');
}
$targetAssistantIds = [];
foreach ($c['adminToPrimary'] as $aid => $primary) {
if ((int) $primary === $deptIdParam) {
$targetAssistantIds[] = (int) $aid;
}
}
if ($targetAssistantIds === []) {
return $empty('当前部门下没有可映射的被指派医助,无明细。');
}
return self::yejiAssignLinesFetch($c, $params, $targetAssistantIds, 0, '', $deptIdParam, $deptName);
}
/**
* @param array<string, mixed> $params
* @param array<string, mixed> $c
*
* @return array<string, mixed>
*/
private static function yejiAssistantAssignLinesInner(array $params, array $c, int $assistantId): array
{
$channelCode = (string) $c['channelCode'];
$assistantName = '';
if ($assistantId > 0) {
$assistantName = (string) (Db::name('admin')->where('id', $assistantId)->whereNull('delete_time')->value('name') ?: '');
}
$empty = static function (string $note) use ($c, $assistantId, $assistantName, $channelCode): array {
return self::yejiAssignLinesEmptyPayload($c, $note, $assistantId, $assistantName, 0, '', $channelCode);
};
if ($assistantId <= 0) {
return $empty('请指定有效医助,或传入 dept_id 查看部门汇总明细。');
}
$dataScopeRestricted = !empty($c['dataScopeRestricted']);
$dataScopeAdminIds = $dataScopeRestricted && isset($c['dataScopeVisibleAdminIds'])
? $c['dataScopeVisibleAdminIds']
: null;
if ($dataScopeRestricted && $dataScopeAdminIds !== null) {
$flip = array_flip($dataScopeAdminIds);
if (!isset($flip[$assistantId])) {
return $empty('当前账号数据权限下不可查看该医助被指派明细。');
}
}
if (!isset($c['adminToPrimary'][$assistantId])) {
return $empty('该医助不在当前业绩看板展示范围内。');
}
$tagDiagIds = $c['tagDiagIds'];
$tagAssistantIds = $c['tagAssistantIds'];
$tagFallback = $c['tagFallback'];
$scopedAssistants = $tagFallback ? $tagAssistantIds : null;
if ($tagDiagIds !== null && $tagDiagIds === []) {
return $empty('当前渠道标签下无关联诊单,无被指派明细。');
}
if ($scopedAssistants !== null && $scopedAssistants === []) {
return $empty('当前渠道标签下无关联医助,无被指派明细。');
}
if ($scopedAssistants !== null && !isset($scopedAssistants[$assistantId])) {
return $empty('当前渠道标签口径下该医助无被指派记录(与排行榜「被指派数」一致)。');
}
$erCenterDeptSet = DeptLogic::getErCenterSubtreeDeptIdSet();
$primaryDept = (int) ($c['adminToPrimary'][$assistantId] ?? 0);
if ($channelCode !== '' && $primaryDept > 0 && isset($erCenterDeptSet[$primaryDept])) {
return $empty('与看板一致:二中心及其组织下级医助在选定渠道下被指派数计 0,无明细。');
}
return self::yejiAssignLinesFetch(
$c,
$params,
[$assistantId],
$assistantId,
$assistantName,
0,
''
);
}
/**
* @param array<string, mixed> $c
* @param array<string, mixed> $params
* @param list<int> $targetAssistantIds
*
* @return array<string, mixed>
*/
private static function yejiAssignLinesFetch(
array $c,
array $params,
array $targetAssistantIds,
int $assistantIdOut,
string $assistantNameOut,
int $deptIdOut,
string $deptNameOut
): array {
$channelCode = (string) $c['channelCode'];
$startTs = (int) $c['startTs'];
$endTs = (int) $c['endTs'];
$page = max(1, (int) ($params['page'] ?? 1));
$limit = min(100, max(1, (int) ($params['page_size'] ?? 20)));
$offset = ($page - 1) * $limit;
$dataScopeRestricted = !empty($c['dataScopeRestricted']);
$dataScopeAdminIds = $dataScopeRestricted && isset($c['dataScopeVisibleAdminIds'])
? $c['dataScopeVisibleAdminIds']
: null;
if ($dataScopeAdminIds !== null && $dataScopeAdminIds !== []) {
$visFlip = array_flip($dataScopeAdminIds);
$targetAssistantIds = array_values(array_filter(
$targetAssistantIds,
static fn (int $aid): bool => isset($visFlip[$aid])
));
}
if ($targetAssistantIds === []) {
return self::yejiAssignLinesEmptyPayload(
$c,
'当前数据权限下无可查看的被指派医助。',
$assistantIdOut,
$assistantNameOut,
$deptIdOut,
$deptNameOut,
$channelCode
);
}
$tagDiagIds = $c['tagDiagIds'];
$tagAssistantIds = $c['tagAssistantIds'];
$tagFallback = $c['tagFallback'];
$scopedAssistants = $tagFallback ? $tagAssistantIds : null;
if ($tagDiagIds !== null && $tagDiagIds === []) {
return self::yejiAssignLinesEmptyPayload(
$c,
'当前渠道标签下无关联诊单,无被指派明细。',
$assistantIdOut,
$assistantNameOut,
$deptIdOut,
$deptNameOut,
$channelCode
);
}
if ($scopedAssistants !== null && $scopedAssistants === []) {
return self::yejiAssignLinesEmptyPayload(
$c,
'当前渠道标签下无关联医助,无被指派明细。',
$assistantIdOut,
$assistantNameOut,
$deptIdOut,
$deptNameOut,
$channelCode
);
}
$buildBaseQuery = static function () use (
$c,
$targetAssistantIds,
$startTs,
$endTs,
$tagDiagIds,
$scopedAssistants
) {
$diagTable = self::tableWithPrefix('tcm_diagnosis');
$q = Db::name('tcm_diagnosis_assign_log')
->alias('lg')
->join("{$diagTable} dg", 'dg.id = lg.diagnosis_id AND dg.delete_time IS NULL', 'INNER')
->where('lg.create_time', 'between', [$startTs, $endTs])
->where('lg.to_assistant_id', '>', 0)
->where('lg.is_inherit', 0)
->whereIn('lg.to_assistant_id', $targetAssistantIds);
self::applyYejiAssignLogChannelFilters(
$q,
$tagDiagIds,
$scopedAssistants,
$c['appointmentChannelValues'],
$c['channelFilterActive'],
$c['channelInfo']
);
return $q;
};
$countRows = $buildBaseQuery()
->field(['lg.to_assistant_id', 'lg.diagnosis_id'])
->group('lg.to_assistant_id,lg.diagnosis_id')
->select()
->toArray();
$count = \count($countRows);
$rawList = $buildBaseQuery()
->field([
'lg.to_assistant_id',
'lg.diagnosis_id',
Db::raw('COUNT(*) AS assign_count'),
Db::raw('MAX(lg.create_time) AS last_assign_time'),
])
->group('lg.to_assistant_id,lg.diagnosis_id')
->order('last_assign_time', 'desc')
->order('lg.diagnosis_id', 'desc')
->limit($offset, $limit)
->select()
->toArray();
$diagIds = [];
$assistantIds = [];
foreach ($rawList as $rw) {
$diagIds[(int) ($rw['diagnosis_id'] ?? 0)] = true;
$assistantIds[(int) ($rw['to_assistant_id'] ?? 0)] = true;
}
$diagInfo = self::fetchYejiAssignDiagnosisInfo(array_keys($diagIds));
$nameMap = $assistantIds !== []
? Db::name('admin')->whereIn('id', array_keys($assistantIds))->column('name', 'id')
: [];
$lists = [];
foreach ($rawList as $rw) {
$did = (int) ($rw['diagnosis_id'] ?? 0);
$aid = (int) ($rw['to_assistant_id'] ?? 0);
$lastTs = (int) ($rw['last_assign_time'] ?? 0);
$lists[] = [
'diagnosis_id' => $did,
'patient_name' => (string) ($diagInfo[$did]['patient_name'] ?? ''),
'patient_phone' => (string) ($diagInfo[$did]['phone'] ?? ''),
'assistant_id' => $aid,
'assistant_name' => $aid > 0 ? (string) ($nameMap[$aid] ?? ('#' . $aid)) : '—',
'assign_count' => (int) ($rw['assign_count'] ?? 0),
'last_assign_time' => $lastTs,
'last_assign_time_text' => $lastTs > 0 ? date('Y-m-d H:i:s', $lastTs) : '',
];
}
$note = '与看板「被指派数」同口径:区间内 `tcm_diagnosis_assign_log` 成功指派(to_assistant_id>0,剔除勾选「继承」),'
. '按指派操作时间落区间,「医助 × 诊单」去重、剔除已删诊单。';
return [
'start_date' => (string) $c['startDate'],
'end_date' => (string) $c['endDate'],
'assistant_id' => $assistantIdOut,
'assistant_name' => $assistantNameOut,
'dept_id' => $deptIdOut,
'dept_name' => $deptNameOut,
'channel_code' => $channelCode,
'count' => $count,
'lists' => $lists,
'note' => $note,
];
}
/**
* @param array<string, mixed> $c
*
* @return array<string, mixed>
*/
private static function yejiAssignLinesEmptyPayload(
array $c,
string $note,
int $assistantId,
string $assistantName,
int $deptId,
string $deptName,
string $channelCode
): array {
return [
'start_date' => (string) $c['startDate'],
'end_date' => (string) $c['endDate'],
'assistant_id' => $assistantId,
'assistant_name' => $assistantName,
'dept_id' => $deptId,
'dept_name' => $deptName,
'channel_code' => $channelCode,
'count' => 0,
'lists' => [],
'note' => $note,
];
}
/**
* @param \think\db\Query $query
*/
private static function applyYejiAssignLogChannelFilters(
$query,
?array $tagDiagIds,
?array $tagAssistantIds,
array $appointmentChannelValues,
bool $channelFilterActive,
?array $channelInfo
): void {
if (!$channelFilterActive) {
return;
}
if ($appointmentChannelValues !== []) {
$norm = self::normalizeAppointmentChannelValues($appointmentChannelValues);
if ($norm === []) {
$query->whereRaw('0 = 1');
return;
}
$pack = self::buildChannelScopedAppointmentExistsSqlAndBindings('dg.id', $norm, $channelInfo);
if ($pack === null) {
$query->whereRaw('0 = 1');
return;
}
$query->whereRaw($pack[0], $pack[1]);
} elseif ($tagDiagIds !== null || $tagAssistantIds !== null) {
if ($tagDiagIds !== null && $tagDiagIds === []) {
$query->whereRaw('0 = 1');
return;
}
if ($tagAssistantIds !== null && $tagAssistantIds === []) {
$query->whereRaw('0 = 1');
return;
}
if ($tagDiagIds !== null) {
$query->whereIn('lg.diagnosis_id', $tagDiagIds);
}
if ($tagAssistantIds !== null) {
$query->whereIn('lg.to_assistant_id', array_map('intval', array_keys($tagAssistantIds)));
}
} else {
$query->whereRaw('0 = 1');
}
}
/**
* @param list<int> $diagIds
*
* @return array<int, array{patient_name:string,phone:string}>
*/
private static function fetchYejiAssignDiagnosisInfo(array $diagIds): array
{
if ($diagIds === []) {
return [];
}
$rows = Db::name('tcm_diagnosis')
->whereIn('id', $diagIds)
->field(['id', 'patient_name', 'phone'])
->select()
->toArray();
$out = [];
foreach ($rows as $r) {
$out[(int) $r['id']] = [
'patient_name' => trim((string) ($r['patient_name'] ?? '')),
'phone' => trim((string) ($r['phone'] ?? '')),
];
}
return $out;
}
/**
* admin 展示「中心」映射:用于挂号明细按部门筛选(与 aggregateConsults 映射同源)。
*/
+123 -20
View File
@@ -28,6 +28,7 @@ use app\common\model\DiagnosisViewRecord;
use app\common\model\doctor\Appointment;
use app\common\model\auth\Admin;
use app\common\model\auth\AdminRole;
use app\adminapi\logic\auth\AuthLogic;
use app\adminapi\logic\doctor\DoctorNoteLogic;
use app\adminapi\logic\doctor\AppointmentLogic;
use app\adminapi\logic\tcm\TrackingNoteLogic;
@@ -73,8 +74,8 @@ class DiagnosisLogic extends BaseLogic
}
}
// 生成患者ID
$params['patient_id'] = self::generatePatientId();
// 新患者先占位 0,写入后 patient_id 与自增 id 对齐
$params['patient_id'] = 0;
// 处理既往史数组
if (isset($params['past_history']) && is_array($params['past_history'])) {
@@ -119,6 +120,7 @@ class DiagnosisLogic extends BaseLogic
}
$model = Diagnosis::create($params);
$model = self::syncPatientIdWithDiagnosisId($model);
// 图片写入 doctor_note 表
if (!empty($newTongueImages) || !empty($newReportFiles)) {
@@ -130,7 +132,7 @@ class DiagnosisLogic extends BaseLogic
}
// 自动为患者创建 TRTC 账号
self::createPatientTrtcAccount($model->id);
self::createPatientTrtcAccount($model->patient_id);
return $model->id;
} catch (\Exception $e) {
@@ -140,16 +142,20 @@ class DiagnosisLogic extends BaseLogic
}
/**
* @notes 生成患者ID
* @return int
* @notes 新患者首张诊单:patient_id 与自增 id 对齐
* @param Diagnosis $model
* @return Diagnosis
*/
private static function generatePatientId(): int
private static function syncPatientIdWithDiagnosisId(Diagnosis $model): Diagnosis
{
// 获取当前最大的患者ID
$maxPatientId = Diagnosis::max('id') ?? 10000000;
// 返回下一个患者ID
return $maxPatientId + 1;
if ((int) $model->patient_id === (int) $model->id) {
return $model;
}
Diagnosis::where('id', $model->id)->update(['patient_id' => $model->id]);
$model->patient_id = (int) $model->id;
return $model;
}
/**
@@ -157,9 +163,23 @@ class DiagnosisLogic extends BaseLogic
* @param array $params
* @return bool
*/
public static function edit(array $params): bool
/** 诊单编辑:患者基本信息字段(姓名/身份证/手机/性别/年龄) */
private const PATIENT_BASIC_FIELDS = ['patient_name', 'id_card', 'phone', 'gender', 'age'];
public static function edit(array $params, array $adminInfo = []): bool
{
try {
if (!empty($params['id']) && $adminInfo !== []) {
$existing = Diagnosis::find((int) $params['id']);
if ($existing && !self::canEditPatientBasicInfo((int) $params['id'], $adminInfo)) {
if (self::patientBasicFieldsChanged($existing, $params)) {
self::setError('最近业务订单未完成,无法修改患者基本信息');
return false;
}
}
}
// 检查手机号是否重复(排除当前记录)
if (!empty($params['phone'])) {
$exists = Diagnosis::where('phone', $params['phone'])
@@ -250,7 +270,7 @@ class DiagnosisLogic extends BaseLogic
* @param $params
* @return array
*/
public static function detail($params): array
public static function detail($params, array $adminInfo = []): array
{
$diagnosis = Diagnosis::findOrEmpty($params['id'])->toArray();
@@ -317,7 +337,17 @@ class DiagnosisLogic extends BaseLogic
$diagnosis['local_hospital_visit_date'] = $diagnosis['diagnosis_date'];
}
$diagnosis['is_view'] =DiagnosisViewRecord::where(['diagnosis_id'=> $diagnosis['id'], 'user_id' =>$params['user_id']??0])->find() ? 1 : 0;
$latestPo = self::getLatestPrescriptionOrderRowForDiagnosis((int) ($diagnosis['id'] ?? 0));
$patientBasicLocked = $latestPo !== null && (int) ($latestPo['fulfillment_status'] ?? 0) !== 3;
$diagnosis['patient_basic_locked'] = $patientBasicLocked;
$diagnosis['can_edit_patient_basic'] = self::canEditPatientBasicInfo((int) ($diagnosis['id'] ?? 0), $adminInfo);
$diagnosis['latest_prescription_order'] = $latestPo !== null ? [
'id' => (int) ($latestPo['id'] ?? 0),
'fulfillment_status' => (int) ($latestPo['fulfillment_status'] ?? 0),
'fulfillment_status_text'=> PrescriptionOrderLogic::fulfillmentStatusLabel((int) ($latestPo['fulfillment_status'] ?? 0)),
] : null;
return $diagnosis;
}
@@ -486,11 +516,11 @@ class DiagnosisLogic extends BaseLogic
}
/**
* 诊单下用于指派日志快照的「最新」业务单:按 create_time 最新,其次 id(避免误用更早下单但 id 更大的记录)。
* 诊单下「最新」业务单:按 create_time 最新,其次 id(避免误用更早下单但 id 更大的记录)。
*
* @return array{creator_id: int, create_time: int}|null
* @return array<string, mixed>|null
*/
private static function getLatestPrescriptionOrderSnapshotForDiagnosis(int $diagnosisId): ?array
private static function getLatestPrescriptionOrderRowForDiagnosis(int $diagnosisId): ?array
{
if ($diagnosisId <= 0) {
return null;
@@ -500,19 +530,88 @@ class DiagnosisLogic extends BaseLogic
->whereNull('delete_time')
->order('create_time', 'desc')
->order('id', 'desc')
->field(['creator_id', 'create_time'])
->field(['id', 'creator_id', 'create_time', 'fulfillment_status'])
->find();
if ($row === null || $row === []) {
return null;
}
return $row;
}
/**
* 诊单下用于指派日志快照的「最新」业务单:按 create_time 最新,其次 id(避免误用更早下单但 id 更大的记录)。
*
* @return array{creator_id: int, create_time: int}|null
*/
private static function getLatestPrescriptionOrderSnapshotForDiagnosis(int $diagnosisId): ?array
{
$row = self::getLatestPrescriptionOrderRowForDiagnosis($diagnosisId);
if ($row === null) {
return null;
}
return [
'creator_id' => (int) ($row['creator_id'] ?? 0),
'create_time' => (int) ($row['create_time'] ?? 0),
];
}
/**
* 菜单权限 tcm.diagnosis/editPatientBasic:最近业务订单未完成时仍可改患者基本信息
*/
public static function hasEditPatientBasicPermission(array $adminInfo): bool
{
if (!empty($adminInfo['root']) && (int) $adminInfo['root'] === 1) {
return true;
}
$perms = AuthLogic::getAuthByAdminId((int) ($adminInfo['admin_id'] ?? 0));
return in_array('tcm.diagnosis/editPatientBasic', $perms, true);
}
/**
* 是否允许编辑诊单患者基本信息(无业务单 / 最近一单已完成 / 具备 editPatientBasic 权限)
*/
public static function canEditPatientBasicInfo(int $diagnosisId, array $adminInfo): bool
{
$latest = self::getLatestPrescriptionOrderRowForDiagnosis($diagnosisId);
if ($latest === null) {
return true;
}
if ((int) ($latest['fulfillment_status'] ?? 0) === 3) {
return true;
}
return self::hasEditPatientBasicPermission($adminInfo);
}
/**
* @param Diagnosis $existing
*/
private static function patientBasicFieldsChanged($existing, array $params): bool
{
foreach (self::PATIENT_BASIC_FIELDS as $field) {
if (!array_key_exists($field, $params)) {
continue;
}
$old = $existing->{$field};
$new = $params[$field];
if ($field === 'age' || $field === 'gender') {
if ((int) $old !== (int) $new) {
return true;
}
continue;
}
if ((string) $old !== (string) $new) {
return true;
}
}
return false;
}
/**
* 诊单 current assistant_id 0 时,从最近一条指派日志推断「原医助」(如发货释放后:上一条多为 from=X、to=0)。
* - 最近一条 to_assistant_id > 0:视为上一任持有人(库未同步时的兜底)
@@ -3092,8 +3191,9 @@ class DiagnosisLogic extends BaseLogic
unset($params['user_id']); // 诊单表无此字段,仅用于创建 view_record
$patientId = $params['patient_id'] ?? 0;
if (!$patientId) {
$params['patient_id'] = self::generatePatientId();
$isNewPatient = !$patientId;
if ($isNewPatient) {
$params['patient_id'] = 0;
}
$params['status'] = 1;
@@ -3123,6 +3223,9 @@ class DiagnosisLogic extends BaseLogic
}
$model = Diagnosis::create($params);
if ($isNewPatient) {
$model = self::syncPatientIdWithDiagnosisId($model);
}
self::createPatientTrtcAccount($model->patient_id);
// 图片写入 doctor_note
@@ -3161,6 +3161,197 @@ class PrescriptionOrderLogic
return $out;
}
/**
* 导出用:统一解析时间(支持 Unix /毫秒与 Y-m-d H:i:s 字符串,与前端 formatOrderTime 同口径)
*
* @param mixed $value
*/
private static function formatDateTimeForExport($value, string $format = 'Y-m-d H:i'): string
{
if ($value === null || $value === '' || $value === false) {
return '';
}
if (\is_int($value) || \is_float($value)) {
$ts = (int) $value;
if ($ts > 9999999999) {
$ts = (int) floor($ts / 1000);
}
return $ts > 0 ? date($format, $ts) : '';
}
$str = trim((string) $value);
if ($str === '') {
return '';
}
if (preg_match('/^\d+$/', $str)) {
$ts = (int) $str;
if ($ts > 9999999999) {
$ts = (int) floor($ts / 1000);
}
return $ts > 0 ? date($format, $ts) : '';
}
if (str_contains($str, '-') || str_contains($str, '/')) {
$ts = strtotime($str);
return $ts !== false ? date($format, $ts) : $str;
}
return $str;
}
/**
* 导出用:支付单来源/方式(与前端 formatPayOrderSource 同口径)
*
* @param array<string, mixed> $row
*/
private static function formatPayOrderSourceForExport(array $row): string
{
$createType = trim((string) ($row['create_type'] ?? ''));
if ($createType === 'wechat_work') {
return '企业微信对外收款';
}
if ($createType === 'fubei') {
return '付呗';
}
if ($createType === 'express_cod') {
return '快递代收';
}
$paymentMethod = trim((string) ($row['payment_method'] ?? ''));
$methodMap = [
'alipay' => '支付宝',
'wechat' => '微信',
'wechat_work' => '企业微信',
'fubei' => '付呗',
'express_cod' => '快递代收',
'manual' => '手动确认到账',
];
if ($paymentMethod !== '' && isset($methodMap[$paymentMethod])) {
return $methodMap[$paymentMethod];
}
return '普通订单';
}
/**
* 导出用:单条关联收款可读文本(两行结构,便于 Excel 自动换行阅读)
*
* @param array<string, mixed> $pay
* @param array<int|string, string> $creatorNames
*/
private static function formatLinkedPayRecordLineForExport(array $pay, array $creatorNames, int $index = 1): string
{
$typeMap = [
1 => '挂号费',
2 => '问诊费',
3 => '药品费用',
4 => '首付费用',
5 => '尾款费用',
6 => '其他费用',
7 => '全部费用',
8 => '驼奶费用',
];
$statusMap = [
1 => '待支付',
2 => '已支付',
3 => '已取消',
4 => '已退款',
5 => '待审核',
];
$orderNo = trim((string) ($pay['order_no'] ?? ''));
$amount = number_format(round((float) ($pay['amount'] ?? 0), 2), 2, '.', '');
$typeDesc = $typeMap[(int) ($pay['order_type'] ?? 0)] ?? '—';
$statusDesc = $statusMap[(int) ($pay['status'] ?? 0)] ?? '—';
$source = self::formatPayOrderSourceForExport($pay);
$cid = (int) ($pay['creator_id'] ?? 0);
$creator = $cid > 0 ? (string) ($creatorNames[$cid] ?? '') : '—';
$timeStr = self::formatDateTimeForExport($pay['create_time'] ?? '') ?: '—';
$line1 = sprintf('[%d] %s ¥%s %s %s', $index, $orderNo, $amount, $typeDesc, $statusDesc);
$line2 = sprintf(' %s %s %s', $source, $creator, $timeStr);
return $line1 . "\n" . $line2;
}
/**
* 导出用:批量解析业务订单关联收款记录(与详情 linked_pay_orders 同口径:status {2,4,5}
*
* @param int[] $poIds
*
* @return array<int, string> prescription_order_id => 多笔以空行分隔的可读文本
*/
private static function batchLinkedPayRecordsExportTextByPoIds(array $poIds): array
{
$poIds = array_values(array_filter(array_unique(array_map('intval', $poIds)), static fn (int $id): bool => $id > 0));
if ($poIds === []) {
return [];
}
$linksByPo = [];
$allPayIds = [];
$linkRows = PrescriptionOrderPayOrder::whereIn('prescription_order_id', $poIds)
->field(['prescription_order_id', 'pay_order_id'])
->order('id', 'asc')
->select()
->toArray();
foreach ($linkRows as $l) {
$poid = (int) ($l['prescription_order_id'] ?? 0);
$payId = (int) ($l['pay_order_id'] ?? 0);
if ($poid <= 0 || $payId <= 0) {
continue;
}
$linksByPo[$poid][] = $payId;
$allPayIds[$payId] = true;
}
$out = [];
foreach ($poIds as $poid) {
$out[$poid] = '';
}
if ($allPayIds === []) {
return $out;
}
$payRows = Order::whereIn('id', array_keys($allPayIds))
->whereNull('delete_time')
->whereIn('status', [2, 4, 5])
->field(['id', 'order_no', 'order_type', 'amount', 'status', 'create_time', 'creator_id', 'payment_method', 'create_type'])
->select()
->toArray();
$payById = [];
$creatorIds = [];
foreach ($payRows as $r) {
$id = (int) ($r['id'] ?? 0);
if ($id <= 0) {
continue;
}
$payById[$id] = $r;
$cid = (int) ($r['creator_id'] ?? 0);
if ($cid > 0) {
$creatorIds[$cid] = true;
}
}
$creatorNames = $creatorIds !== []
? Admin::whereIn('id', array_keys($creatorIds))->column('name', 'id')
: [];
foreach ($poIds as $poid) {
$lines = [];
$seq = 0;
foreach ($linksByPo[$poid] ?? [] as $payId) {
$pay = $payById[$payId] ?? null;
if (!\is_array($pay)) {
continue;
}
++$seq;
$lines[] = self::formatLinkedPayRecordLineForExport($pay, $creatorNames, $seq);
}
$out[$poid] = implode("\n\n", $lines);
}
return $out;
}
/**
* 业务订单列表导出:写入与 PrescriptionOrderLists::setExcelFields 对应的字段(export=2 时由列表类调用)
* 「挂号渠道来源」取值与详情/列表解析一致:处方 appointment_id 否则诊单下 MAX(挂号.id);业绩抽屉带渠道时与列表同源高亮。
@@ -3392,6 +3583,7 @@ class PrescriptionOrderLogic
$firstVisitAssistantByDiag = self::batchFirstVisitAssistantNameByDiagnosis($diagIdList, $diagById);
$assistantDeptCache = [];
$linkedPayExportByPo = self::batchLinkedPayRecordsExportTextByPoIds($poIds);
foreach ($lists as &$item) {
$poId = (int) ($item['id'] ?? 0);
@@ -3464,6 +3656,7 @@ class PrescriptionOrderLogic
$paid = round((float) ($item['linked_pay_paid_total'] ?? 0), 2);
$item['export_amount'] = number_format($amt, 2, '.', '');
$item['export_paid_amount'] = number_format($paid, 2, '.', '');
$item['export_linked_pay_records'] = $linkedPayExportByPo[$poId] ?? '';
$refundStored = round((float) ($item['refund_amount'] ?? 0), 2);
$item['export_refund_amount'] = $refundStored > 0
? number_format($refundStored, 2, '.', '')
@@ -4216,6 +4409,112 @@ class PrescriptionOrderLogic
return true;
}
/**
* 批量将处方业务订单「创建人(医助归属)」改派给其他医助。
* 仅改写 creator_id(业务归属/医助筛选口径),逐单记操作日志;与支付单批量改派口径一致。
*
* @param int[] $orderIds
* @param int $assistantAdminId 目标医助 admin_id(须含医助角色)
* @return array{success:int, fail:int, errors:string[], per_log:array<int,array{order_id:int,summary:string}>}|false
*/
public static function batchAssignAssistant(array $orderIds, int $assistantAdminId, int $adminId, array $adminInfo)
{
self::$error = '';
$assistantAdminId = (int) $assistantAdminId;
if ($assistantAdminId <= 0) {
self::$error = '请选择医助';
return false;
}
// 目标须为医助角色(与支付单批量改派一致)
$assistantRoleId = (int) Config::get('project.prescription_order_stats_assistant_role_id', 2);
if (
\app\common\model\auth\AdminRole::where('admin_id', $assistantAdminId)
->where('role_id', $assistantRoleId)
->count() === 0
) {
self::$error = '目标账号不是医助角色';
return false;
}
$ids = array_values(array_unique(array_filter(
array_map('intval', $orderIds),
static fn (int $id) => $id > 0
)));
if ($ids === []) {
self::$error = '请选择订单';
return false;
}
if (count($ids) > 200) {
self::$error = '单次最多改派200单';
return false;
}
$targetName = (string) Admin::where('id', $assistantAdminId)->whereNull('delete_time')->value('name');
if ($targetName === '') {
$targetName = (string) $assistantAdminId;
}
$perLog = [];
$errors = [];
$success = 0;
foreach ($ids as $oid) {
$order = PrescriptionOrder::where('id', $oid)->whereNull('delete_time')->find();
if (! $order) {
$errors[] = "订单{$oid}不存在或已删除";
continue;
}
$label = (string) ($order->order_no ?: $oid);
if (! self::canAccessOrder($order, $adminId, $adminInfo)) {
$errors[] = "订单{$label}无操作权限";
continue;
}
$oldCreatorId = (int) $order->creator_id;
if ($oldCreatorId === $assistantAdminId) {
$errors[] = "订单{$label}创建人已是该医助,已跳过";
continue;
}
$oldName = $oldCreatorId > 0
? (string) Admin::where('id', $oldCreatorId)->whereNull('delete_time')->value('name')
: '';
if ($oldName === '' && $oldCreatorId > 0) {
$oldName = (string) $oldCreatorId;
} elseif ($oldName === '') {
$oldName = '—';
}
$order->creator_id = $assistantAdminId;
try {
$order->save();
} catch (\Throwable $e) {
$errors[] = "订单{$label}保存失败";
continue;
}
$summary = '创建人(医助)由「' . $oldName . '」改派为「' . $targetName . '」';
self::writeLog($oid, $adminId, $adminInfo, 'assign_assistant', $summary);
$perLog[] = ['order_id' => $oid, 'summary' => $summary];
$success++;
}
if ($success === 0) {
self::$error = $errors !== [] ? implode('', $errors) : '未能改派任何订单';
return false;
}
return [
'success' => $success,
'fail' => count($errors),
'errors' => $errors,
'per_log' => $perLog,
];
}
private static function writeLog(int $orderId, int $adminId, array $adminInfo, string $action, string $summary): void
{
$adminName = $adminInfo['name'] ?? '';
@@ -32,6 +32,7 @@ class DiagnosisValidate extends BaseValidate
'gender' => 'require|in:0,1',
'age' => 'require|number|between:0,150',
'diagnosis_type' => 'require',
'local_hospital_name' => 'require|max:255',
'status' => 'in:0,1',
'show_card' => 'in:0,1',
'create_source' => 'max:32',
@@ -50,6 +51,7 @@ class DiagnosisValidate extends BaseValidate
'patient_name.require' => '请输入患者姓名',
'patient_name.length' => '患者姓名长度须在1-50位字符',
'id_card.length' => '身份证号长度不正确',
'id_card.require' => '请输入身份证号',
'phone.require' => '请输入手机号',
'phone.mobile' => '手机号格式不正确',
'gender.require' => '请选择性别',
@@ -58,6 +60,8 @@ class DiagnosisValidate extends BaseValidate
'age.number' => '年龄必须为数字',
'age.between' => '年龄范围0-150',
'diagnosis_type.require' => '请选择诊断类型',
'local_hospital_name.require' => '请输入当地就诊医院名称',
'local_hospital_name.max' => '当地就诊医院名称最多255个字符',
'status.in' => '状态参数错误',
'create_source.max' => '渠道来源长度不能超过32个字符',
'current_medications.max' => '在用药物最多2000个字符',
@@ -91,6 +91,6 @@ class PrescriptionOrderValidate extends BaseValidate
{
return $this->only(['id', 'amount'])
->append('id', 'require|integer|gt:0')
->append('amount', 'require|float|gt:0');
->append('amount', 'require|float|egt:0');
}
}
@@ -0,0 +1,5 @@
<<<<<<<< HEAD:server/public/admin/assets/403-JaaH0nJc.js
import r from"./error-BRndE98h.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-Q-68fWIq.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-DRvJpHjl.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-BvW4CeSo.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};
>>>>>>>> master:server/public/admin/assets/403-ClXIpSXk.js
@@ -0,0 +1,5 @@
<<<<<<<< HEAD:server/public/admin/assets/403-JaaH0nJc.js
import r from"./error-BRndE98h.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-Q-68fWIq.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-DRvJpHjl.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-BvW4CeSo.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};
>>>>>>>> master:server/public/admin/assets/403-ClXIpSXk.js
@@ -1 +0,0 @@
import r from"./error-DzAM_8sh.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-DM-pfG5J.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-BQ5Mf_ti.js";import"./index-CaVLWczr.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.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"./@vueuse/core-DfvfIixE.js";import"./@vueuse/shared-Cl3lVqjz.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};
@@ -0,0 +1,5 @@
<<<<<<<< HEAD:server/public/admin/assets/404-BGr9qqKo.js
import o from"./error-BRndE98h.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-Q-68fWIq.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-DRvJpHjl.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-BvW4CeSo.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};
>>>>>>>> master:server/public/admin/assets/404-BYCn5IoB.js
@@ -0,0 +1,5 @@
<<<<<<<< HEAD:server/public/admin/assets/404-BGr9qqKo.js
import o from"./error-BRndE98h.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-Q-68fWIq.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-DRvJpHjl.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-BvW4CeSo.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};
>>>>>>>> master:server/public/admin/assets/404-BYCn5IoB.js
@@ -1 +0,0 @@
import o from"./error-DzAM_8sh.js";import{f as r,ak as t,I as m,a as p}from"./@vue/runtime-core-C6bnekPw.js";import"./element-plus-DM-pfG5J.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-BQ5Mf_ti.js";import"./index-CaVLWczr.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.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"./@vueuse/core-DfvfIixE.js";import"./@vueuse/shared-Cl3lVqjz.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};
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
File diff suppressed because one or more lines are too long
View File
View File
View File
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,5 @@
<<<<<<<< HEAD:server/public/admin/assets/AssignLogPanel-DOu-eQ-v.js
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-DPqenJl7.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-Q-68fWIq.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{a5 as P}from"./tcm-By1d7VLW.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-BvW4CeSo.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};
>>>>>>>> master:server/public/admin/assets/AssignLogPanel-0Z3rSwGx.js
@@ -0,0 +1,5 @@
<<<<<<<< HEAD:server/public/admin/assets/AssignLogPanel-DOu-eQ-v.js
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-DPqenJl7.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-Q-68fWIq.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{a5 as P}from"./tcm-By1d7VLW.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-BvW4CeSo.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};
>>>>>>>> master:server/public/admin/assets/AssignLogPanel-0Z3rSwGx.js
@@ -1 +0,0 @@
import{M as T,N as C,T as $,d as D,f as L,L as k}from"./element-plus-DM-pfG5J.js";import{Y as E}from"./@element-plus/icons-vue-B0jSCQ-G.js";import{a5 as P}from"./tcm-DREvQype.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-CaVLWczr.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-BQ5Mf_ti.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"./@vueuse/core-DfvfIixE.js";import"./@vueuse/shared-Cl3lVqjz.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};
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
View File

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