Compare commits

..
34 Commits
Author SHA1 Message Date
Your Name b02c340348 更新 2026-07-17 14:47:00 +08:00
Your Name 0f21dd4451 更新 2026-07-17 14:12:15 +08:00
Your Name 1ed3e06fdf 更新 2026-07-17 12:50:11 +08:00
Your Name c608145023 i
Merge branch 'bug-0717'
2026-07-17 12:13:30 +08:00
Your Name 4f95d9182e 更新 2026-07-17 12:13:02 +08:00
long f662723a25 debug 2026-07-17 11:37:05 +08:00
long 32370df7fe feat(tongji): 新增控糖消消乐小游戏 2026-07-17 10:24:42 +08:00
大哥大哥的大哥哥 62467dc01c feat(tongji): 新增控糖消消乐小游戏 2026-07-17 09:30:22 +08:00
Your Name 97fefbae8f 更新 2026-07-16 09:43:07 +08:00
Your Name 541fe59ad1 Merge branch 'xuhao-20260715' 2026-07-16 09:37:30 +08:00
longandCursor b6c5b422c2 test: 空提交验证 Gitea SSH 推送
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-16 09:25:10 +08:00
Your Name 500038601c 更新 2026-07-16 09:04:40 +08:00
Your Name f6688a740d 更新 2026-07-10 14:19:01 +08:00
Your Name a347e0642c Merge branch 'AI-20260710' 2026-07-10 14:16:51 +08:00
Your Name 13adbac3fd 更新 2026-07-10 14:16:07 +08:00
Your Name 333a18c37e 更新 2026-07-10 14:10:48 +08:00
Your Name e3a91a91f7 i
Merge branch 'AI-20260710'
2026-07-10 14:08:47 +08:00
Your Name 5ca5f06255 更新 2026-07-10 14:08:27 +08:00
Your Name e01f3e7072 更新 2026-07-10 10:58:14 +08:00
Your Name ed78803015 更新 2026-07-10 10:52:36 +08:00
Your Name 546138d60e 更新 2026-07-10 10:28:21 +08:00
Your Name fdddfdb3dd AI 客服 2026-07-10 10:25:51 +08:00
Your Name f5a89b4310 更新 2026-07-09 17:58:02 +08:00
Your Name a650747fd3 更新 2026-07-09 11:27:20 +08:00
Your Name 46b62229a6 i
Merge branch '20260708'
2026-07-09 11:25:25 +08:00
Your Name 0f33f2ad28 更新 2026-07-09 11:24:18 +08:00
Your Name a677b69e97 更新 2026-07-08 15:00:28 +08:00
Your Name 6432e9990c 更新 2026-07-08 14:53:41 +08:00
Your Name 4d6125268a 更新 2026-07-08 14:46:39 +08:00
Your Name 7f93cf5480 更新 2026-07-07 11:06:19 +08:00
Your Name f8f1953a2a Merge branch 'bug-20260706' 2026-07-07 11:04:43 +08:00
Your Name 99eca8b2e5 更新 2026-07-07 09:24:40 +08:00
Your Name bde396189a Please enter the commit message for your changes. Lines starting 2026-07-06 16:58:01 +08:00
Your Name 55e3b5d9e0 更新 2026-07-06 16:34:42 +08:00
400 changed files with 7216 additions and 38200 deletions
+9
View File
@@ -223,6 +223,15 @@
"backgroundColor": "#c7d2fe", "backgroundColor": "#c7d2fe",
"disableScroll": true "disableScroll": true
} }
},
{
"path": "endless-game/index",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "控糖消消乐",
"backgroundColor": "#eefbf4",
"disableScroll": false
}
} }
] ]
} }
@@ -0,0 +1,39 @@
# 控糖消消乐独立功能包
此目录包含“控糖消消乐”无尽三消版的页面与运行代码。食品图片由远端 COS 提供,图标组件复用 `tongji` 分包的公共组件。
## 目录内容
- `index.vue`:页面、棋盘算法、关卡主题、任务、道具、三/四/五连奖励和适老化交互。
- `game-endless.scss`:完整页面与动画样式。
- `composables/useGameSfx.js`:滑动、掉落、消除、连击和大奖音效。
## 迁移步骤
1. 复制 `endless-game` 文件夹到目标项目的 `tongji/` 目录,并确保目标项目同时提供 `tongji/components/TongjiIcon.vue``tongji/utils/svgDataUrl.js`
2. 在目标项目 `pages.json``tongji` 分包 `pages` 数组中加入:
```json
{
"path": "endless-game/index",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "控糖消消乐",
"backgroundColor": "#eefbf4",
"disableScroll": false
}
}
```
3. 使用 `/tongji/endless-game/index` 打开游戏。
4. 微信小程序后台需要将 `https://gz-1349751149.cos.ap-guangzhou.myqcloud.com` 配置为合法的音频与图片下载域名。
## 说明
- 当前项目另有 `/tongji/pages/game` 页面,不属于本独立功能包,迁移时无需复制。
- 食品图片固定使用 `https://gz-1349751149.cos.ap-guangzhou.myqcloud.com/games/food/` 前缀,文件名必须与 `FOODS` 配置 key 的大小写保持一致。
- 连消小目标按“连消×2”累计 2 次;连续 6 次普通消除没有连消时,下一次掉落会提供连消机会。“连消×3”保留为额外积分、驼乳粉与撒花惊喜,不阻挡主线任务。
- 横向 3 个与竖向 3 个相交形成 L/T 形五消时,会在交点生成带高对比 L 标记的范围爆破棋子;该棋子再次被消除时清除周围九格。
- 进入游戏时展示“本周 7 人同行榜”界面原型,包含前后名次、差距提示和可切换的同行/亲友鼓励;顶部“本周同行”可再次打开。当前榜单明确标注为预览数据,正式用户关系和周榜成绩需要后端接口接入。
- 本功能包依赖 uni-app Vue 3、`@dcloudio/uni-app` 以及 `tongji` 分包内的公共 `TongjiIcon` 组件。
- 食物风险等级和科普文案集中在 `index.vue``FOODS` 配置中,正式上线前仍需由医院医生审核。
@@ -0,0 +1,350 @@
/**
* 糖分突袭 · 游戏音效(InnerAudioContext 池化,H5/小程序通用)
* 每种操作独立音轨 + playbackRate 变调,避免“全是同一个声”
*/
import { ref, onUnmounted } from 'vue'
const STORAGE_KEY = 'tongji_game_sfx_enabled'
const COS = 'https://gz-1349751149.cos.ap-guangzhou.myqcloud.com/uploads/file'
/** 9 条基础素材(已上传 COS,小程序域名白名单内) */
const SRC = {
tap: `${COS}/20260526/20260526105128557ef8669.mp3`,
swoosh: `${COS}/20260527/202605271539371ebe13672.mp3`,
pop: `${COS}/20260526/202605261051282a0a94508.mp3`,
buzz: `${COS}/20260527/202605271539376ff628472.mp3`,
chime: `${COS}/20260528/202605280937077bad76716.mp3`,
fanfare: `${COS}/20260528/20260528093707b021f5794.mp3`,
blast: `${COS}/20260528/20260528093707ebd4d5733.mp3`,
sparkle: `${COS}/20260528/20260528093707016f07427.mp3`,
stinger: `${COS}/20260528/20260528093709c669f4659.mp3`
}
/**
* 音效表:src / volume / rate(playbackRate) / pool
* rate 不同可在同一素材上听出明显差异
*/
const SFX = {
// —— 基础素材别名(供分层音效直接调用)——
tap: { src: SRC.tap, volume: 0.32, rate: 1, pool: 3 },
pop: { src: SRC.pop, volume: 0.3, rate: 1, pool: 4 },
fanfare: { src: SRC.fanfare, volume: 0.56, rate: 1, pool: 2 },
blast: { src: SRC.blast, volume: 0.62, rate: 1, pool: 2 },
sparkle: { src: SRC.sparkle, volume: 0.5, rate: 1, pool: 2 },
// —— 交互 ——
select: { src: SRC.tap, volume: 0.32, rate: 1.05, pool: 3 },
deselect: { src: SRC.tap, volume: 0.22, rate: 0.82, pool: 2 },
hint: { src: SRC.tap, volume: 0.26, rate: 1.28, pool: 2 },
swap: { src: SRC.swoosh, volume: 0.38, rate: 1.0, pool: 3 },
swapFail: { src: SRC.buzz, volume: 0.36, rate: 1.18, pool: 2 },
drop: { src: SRC.pop, volume: 0.3, rate: 0.88, pool: 4 },
dropLight: { src: SRC.pop, volume: 0.22, rate: 1.22, pool: 2 },
// —— 消除(按连数)——
match3: { src: SRC.chime, volume: 0.48, rate: 1.0, pool: 4 },
match4: { src: SRC.fanfare, volume: 0.54, rate: 1.06, pool: 3 },
match5: { src: SRC.blast, volume: 0.62, rate: 1.0, pool: 3 },
// —— 消除(按 GI 主色)——
matchLowGi: { src: SRC.chime, volume: 0.42, rate: 0.86, pool: 2 },
matchMidGi: { src: SRC.chime, volume: 0.46, rate: 1.02, pool: 2 },
matchHighGi: { src: SRC.buzz, volume: 0.44, rate: 0.92, pool: 2 },
// —— 连消 ——
combo2: { src: SRC.fanfare, volume: 0.56, rate: 1.12, pool: 2 },
combo3: { src: SRC.blast, volume: 0.64, rate: 1.08, pool: 2 },
comboMega: { src: SRC.blast, volume: 0.72, rate: 1.22, pool: 2 },
// —— 道具 ——
insulin: { src: SRC.sparkle, volume: 0.5, rate: 1.15, pool: 2 },
fiber: { src: SRC.swoosh, volume: 0.48, rate: 1.32, pool: 2 },
meal: { src: SRC.stinger, volume: 0.42, rate: 1.35, pool: 1 },
reshuffle: { src: SRC.swoosh, volume: 0.46, rate: 0.72, pool: 2 },
// —— 反馈 ——
score: { src: SRC.tap, volume: 0.28, rate: 1.38, pool: 2 },
goalTick: { src: SRC.chime, volume: 0.36, rate: 1.45, pool: 2 },
meterWarn: { src: SRC.buzz, volume: 0.5, rate: 1.0, pool: 2 },
meterDanger: { src: SRC.buzz, volume: 0.58, rate: 0.78, pool: 2 },
meterStable: { src: SRC.pop, volume: 0.24, rate: 1.05, pool: 1 },
win: { src: SRC.fanfare, volume: 0.66, rate: 1.0, pool: 1 },
winStinger: { src: SRC.stinger, volume: 0.38, rate: 1.5, pool: 1 },
lose: { src: SRC.buzz, volume: 0.52, rate: 0.68, pool: 1 },
// 兼容旧名
match: { src: SRC.chime, volume: 0.48, rate: 1.0, pool: 4 },
combo: { src: SRC.fanfare, volume: 0.56, rate: 1.1, pool: 3 },
heal: { src: SRC.pop, volume: 0.44, rate: 1.18, pool: 2 },
boost: { src: SRC.sparkle, volume: 0.52, rate: 1.0, pool: 2 },
invalid: { src: SRC.buzz, volume: 0.4, rate: 1.15, pool: 2 },
danger: { src: SRC.blast, volume: 0.58, rate: 0.95, pool: 2 }
}
const WARMUP_SFX = [
'select', 'swap', 'swapFail', 'drop', 'dropLight',
'match3', 'match4', 'match5', 'combo2', 'combo3', 'comboMega',
'insulin', 'sparkle', 'reshuffle', 'lose'
]
function createAudioContext() {
if (typeof Audio !== 'undefined') {
const audio = new Audio()
audio.preload = 'auto'
return {
get src() { return audio.src },
set src(value) { audio.src = value },
get volume() { return audio.volume },
set volume(value) { audio.volume = value },
get playbackRate() { return audio.playbackRate },
set playbackRate(value) { audio.playbackRate = value },
play() {
const promise = audio.play()
if (promise?.catch) promise.catch(() => {})
},
stop() {
audio.pause()
try { audio.currentTime = 0 } catch (_) {}
},
seek(time) {
try { audio.currentTime = time } catch (_) {}
},
destroy() {
audio.pause()
audio.removeAttribute('src')
}
}
}
return uni.createInnerAudioContext()
}
class SfxPool {
constructor(src, size, volume, rate = 1) {
this.src = src
this.size = size
this.volume = volume
this.rate = rate
this.list = []
this.cursor = 0
this.warmedUp = false
}
createOne() {
const ctx = createAudioContext()
ctx.src = this.src
ctx.obeyMuteSwitch = false
ctx.autoplay = false
ctx.volume = this.volume
try {
ctx.playbackRate = this.rate
} catch (_) {}
this.list.push(ctx)
return ctx
}
create() {
while (this.list.length < this.size) this.createOne()
}
warmUp() {
if (this.warmedUp) return
const ctx = this.list[0] || this.createOne()
try {
ctx.volume = 0
ctx.play()
setTimeout(() => {
try {
ctx.stop()
ctx.volume = this.volume
} catch (_) {}
}, 60)
} catch (_) {}
if (this.size > 1) this.cursor = 1
this.warmedUp = true
}
play(scale = 1, rateMul = 1) {
if (this.list.length < this.size) this.create()
const ctx = this.list[this.cursor % this.list.length]
this.cursor += 1
const vol = Math.min(1, this.volume * scale)
const rate = Math.min(2, Math.max(0.5, this.rate * rateMul))
try {
ctx.volume = vol
try {
ctx.playbackRate = rate
} catch (_) {}
ctx.stop()
ctx.seek(0)
ctx.play()
} catch (_) {
try {
ctx.play()
} catch (e) {}
}
}
destroy() {
this.list.forEach((ctx) => {
try {
ctx.stop()
ctx.destroy()
} catch (_) {}
})
this.list = []
this.warmedUp = false
}
}
function dominantGi(tiles = []) {
const cnt = { low: 0, mid: 0, high: 0 }
tiles.forEach((t) => {
const g = t?.type?.gi
if (g && cnt[g] !== undefined) cnt[g] += 1
})
let best = 'mid'
let max = 0
Object.entries(cnt).forEach(([k, v]) => {
if (v > max) {
max = v
best = k
}
})
return best
}
export function useGameSfx() {
const enabled = ref(true)
const pools = {}
try {
const stored = uni.getStorageSync(STORAGE_KEY)
if (stored === false || stored === '0' || stored === 0) {
enabled.value = false
}
} catch (_) {}
function ensureAudioOption() {
// #ifdef MP-WEIXIN
try {
uni.setInnerAudioOption({
obeyMuteSwitch: false,
mixWithOther: true
})
} catch (_) {}
// #endif
}
function getPool(name) {
const cfg = SFX[name]
if (!cfg) return null
if (!pools[name]) {
pools[name] = new SfxPool(cfg.src, cfg.pool, cfg.volume, cfg.rate ?? 1)
}
return pools[name]
}
function warmUp() {
if (!enabled.value) return
ensureAudioOption()
const names = typeof Audio !== 'undefined' ? ['select'] : WARMUP_SFX
names.forEach((name) => {
getPool(name)?.warmUp()
})
}
function play(name, scale = 1, rateMul = 1) {
if (!enabled.value) return
getPool(name)?.play(scale, rateMul)
}
function playLayer(primary, secondary, delayMs = 70, secScale = 0.6) {
play(primary)
if (secondary) {
setTimeout(() => play(secondary, secScale), delayMs)
}
}
function playMatch({ matchCount = 3, chain = 0, tiles = [] } = {}) {
if (!enabled.value) return
const giTrack = (() => {
const gi = dominantGi(tiles)
if (gi === 'low') return 'matchLowGi'
if (gi === 'high') return 'matchHighGi'
return 'matchMidGi'
})()
if (chain >= 3) {
playLayer('comboMega', giTrack, 90, 0.55)
return
}
if (chain === 2) {
playLayer('combo3', giTrack, 80, 0.5)
return
}
if (chain === 1) {
playLayer('combo2', giTrack, 70, 0.45)
return
}
if (matchCount >= 5) {
playLayer('match5', 'matchHighGi', 85, 0.5)
} else if (matchCount >= 4) {
playLayer('match4', giTrack, 65, 0.48)
} else {
play(giTrack, 1, 1)
setTimeout(() => play('match3', 0.85), 40)
}
}
function playDrop(count = 1) {
if (!enabled.value || count <= 0) return
const name = count >= 4 ? 'drop' : 'dropLight'
play(name, Math.min(1.2, 0.85 + count * 0.04), count >= 6 ? 0.9 : 1)
}
function playMeter(delta, level) {
if (!enabled.value) return
if (level > 70) play('meterDanger', 1 + (level - 70) * 0.02)
else if (level < 30) play('meterWarn', 0.9, 0.95)
else if (delta > 8) play('meterWarn', 0.75)
else if (delta < -5) play('meterStable', 1.1)
}
function toggleEnabled() {
enabled.value = !enabled.value
try {
uni.setStorageSync(STORAGE_KEY, enabled.value ? '1' : '0')
} catch (_) {}
if (enabled.value) {
warmUp()
play('select')
}
}
function destroyAll() {
Object.keys(pools).forEach((key) => {
pools[key]?.destroy()
delete pools[key]
})
}
onUnmounted(() => {
destroyAll()
})
return {
enabled,
play,
playLayer,
playMatch,
playDrop,
playMeter,
warmUp,
toggleEnabled,
destroyAll
}
}
@@ -0,0 +1,564 @@
.eg-page {
min-height: 100vh;
color: #173b32;
background: linear-gradient(180deg, #eefbf4 0%, #f9f3dd 56%, #fffaf1 100%);
font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Microsoft YaHei', sans-serif;
}
.eg-confetti-layer { position: fixed; z-index: 80; inset: 0; overflow: hidden; pointer-events: none; }
.eg-confetti-piece {
position: absolute;
top: -70rpx;
display: block;
border-radius: 3rpx;
box-shadow: 0 2rpx 4rpx rgba(0,0,0,.12);
animation-name: egConfettiFall;
animation-timing-function: cubic-bezier(.18,.72,.35,1);
animation-fill-mode: both;
}
.eg-confetti-piece.is-emoji { width: auto !important; height: auto !important; background: transparent !important; box-shadow: none; font-size: 42rpx; }
.eg-nav {
position: relative;
display: grid;
box-sizing: border-box;
grid-template-columns: 156rpx minmax(0, 1fr) 156rpx;
align-items: center;
column-gap: 12rpx;
padding-right: 28rpx;
padding-bottom: 16rpx;
padding-left: 28rpx;
}
.eg-nav-leading { display: flex; width: 156rpx; justify-content: flex-start; }
.eg-nav-btn {
display: flex;
width: 72rpx;
height: 72rpx;
align-items: center;
justify-content: center;
border: 2rpx solid rgba(21, 94, 75, .12);
border-radius: 24rpx;
background: rgba(255,255,255,.82);
}
.eg-nav-actions { display: flex; width: 156rpx; flex-shrink: 0; justify-content: flex-end; gap: 10rpx; }
.eg-nav-btn--small { width: 68rpx; height: 68rpx; border-radius: 22rpx; }
.eg-title-wrap { display: flex; min-width: 0; flex-direction: column; align-items: center; }
.eg-title, .eg-subtitle { overflow: hidden; max-width: 100%; white-space: nowrap; text-overflow: ellipsis; }
.eg-title { color: #155e4b; font-size: 40rpx; font-weight: 800; }
.eg-subtitle { margin-top: 2rpx; color: #648278; font-size: 24rpx; }
.eg-content { padding: 0 16rpx 48rpx; }
.eg-score-card {
display: flex;
align-items: center;
margin-bottom: 16rpx;
padding: 18rpx 24rpx;
border: 2rpx solid rgba(21, 94, 75, .1);
border-radius: 28rpx;
background: rgba(255,255,255,.9);
box-shadow: 0 10rpx 28rpx rgba(32, 78, 43, .08);
}
.eg-stat { display: flex; flex: 1; flex-direction: column; align-items: center; }
.eg-stat--main { border-right: 2rpx solid #e2eee8; border-left: 2rpx solid #e2eee8; }
.eg-stat-label { color: #71847d; font-size: 24rpx; }
.eg-stat-value { margin-top: 4rpx; color: #204e2b; font-size: 38rpx; font-weight: 800; }
.eg-stat-score { margin-top: 2rpx; color: #e16f24; font-size: 46rpx; font-weight: 900; }
.eg-stat--rank { position: relative; cursor: pointer; }
.eg-stat--rank.is-locked { opacity: .52; }
.eg-stat-rank-value { margin-top: 1rpx; color: #176b52; font-size: 35rpx; font-weight: 1000; line-height: 1.08; }
.eg-stat-rank-hint { margin-top: 2rpx; color: #df7427; font-size: 18rpx; font-weight: 800; }
.eg-task-card {
margin-bottom: 16rpx;
padding: 18rpx 22rpx;
border-radius: 24rpx;
background: rgba(255, 249, 223, .94);
box-shadow: 0 8rpx 22rpx rgba(118, 89, 25, .08);
}
.eg-task-card { position: relative; overflow: visible; }
.eg-task-reward-flight {
position: absolute;
z-index: 72;
top: 4rpx;
left: 42%;
display: flex;
width: 112rpx;
height: 92rpx;
align-items: center;
justify-content: center;
border: 4rpx solid rgba(255,255,255,.96);
border-radius: 24rpx;
background: #fff7e8;
box-shadow: 0 10rpx 24rpx rgba(159,94,23,.28);
animation: egTaskRewardFlight 1.12s cubic-bezier(.18,.78,.22,1) both;
pointer-events: none;
}
.eg-task-reward-flight > image { position: relative; z-index: 2; width: 78rpx; height: 66rpx; }
.eg-task-reward-flight > text { position: absolute; z-index: 3; top: -14rpx; right: -16rpx; display: flex; width: 48rpx; height: 48rpx; align-items: center; justify-content: center; border: 4rpx solid #fff; border-radius: 50%; color: #fff; background: #ed7625; box-shadow: 0 5rpx 12rpx rgba(191,75,19,.3); font-size: 25rpx; font-weight: 1000; }
.eg-task-reward-glow { position: absolute; z-index: 1; inset: -15rpx; border-radius: 32rpx; background: radial-gradient(circle, rgba(255,210,67,.52), rgba(255,210,67,0) 68%); animation: egRewardGlow .42s ease-in-out infinite alternate; }
.eg-task-head { display: flex; align-items: center; justify-content: space-between; color: #7c5b1b; font-size: 27rpx; font-weight: 700; }
.eg-task-name { display: flex; align-items: center; gap: 10rpx; }
.eg-task-count { color: #9a6718; font-size: 29rpx; }
.eg-progress { height: 14rpx; margin-top: 12rpx; overflow: hidden; border-radius: 999rpx; background: #eadfbd; }
.eg-progress-fill { height: 100%; border-radius: inherit; background: linear-gradient(90deg, #f0a128, #f4c34f); transition: width .25s; }
.eg-board-shell {
position: relative;
padding: 14rpx;
border: 2rpx solid rgba(34, 103, 79, .16);
border-radius: 34rpx;
background: rgba(255,255,255,.92);
box-shadow: 0 18rpx 44rpx rgba(29, 78, 59, .12);
}
.eg-board-top { display: flex; align-items: center; justify-content: space-between; margin: 0 4rpx 14rpx; }
.eg-board-bonuses { display: flex; align-items: center; gap: 8rpx; }
.eg-combo { padding: 8rpx 16rpx; border-radius: 999rpx; color: #45685d; background: #e8f3ee; font-size: 25rpx; font-weight: 700; }
.eg-combo.is-hot { color: #fff; background: linear-gradient(135deg, #f59e0b, #ea580c); }
.eg-double-state { padding: 8rpx 14rpx; border: 2rpx solid rgba(255,255,255,.9); border-radius: 999rpx; color: #fff; background: linear-gradient(135deg, #7c3aed, #d946ef); box-shadow: 0 4rpx 12rpx rgba(124,58,237,.24); font-size: 23rpx; font-weight: 900; white-space: nowrap; }
.eg-risk { display: flex; align-items: center; gap: 10rpx; color: #8d512c; font-size: 24rpx; }
.eg-risk-dots { display: flex; gap: 5rpx; }
.eg-risk-dot { width: 12rpx; height: 12rpx; border-radius: 50%; background: #ead8c9; }
.eg-risk-dot.on { background: #e96b3b; box-shadow: 0 0 0 3rpx rgba(233,107,59,.12); }
.eg-board { display: flex; flex-direction: column; gap: 8rpx; opacity: 1; transition: opacity .15s; }
.eg-board.is-busy { opacity: .82; }
.eg-row { display: flex; gap: 8rpx; }
.eg-cell {
position: relative;
display: flex;
height: 166rpx;
flex: 1;
flex-direction: column;
align-items: center;
justify-content: center;
overflow: hidden;
border: 5rpx solid transparent;
border-radius: 26rpx;
box-shadow: inset 0 -5rpx 0 rgba(0,0,0,.05), 0 5rpx 12rpx rgba(31, 66, 53, .08);
transition: transform .16s cubic-bezier(.2,.8,.2,1), border-color .15s, opacity .16s, filter .16s;
will-change: transform;
}
.eg-cell.is-dragging { transition: transform .055s linear, border-color .15s; }
.eg-cell.is-selected { z-index: 2; border-color: #167d61; transform: scale(1.06); box-shadow: 0 0 0 6rpx rgba(22,125,97,.14); }
.eg-cell.is-camel-target { border-color: #f59e0b; animation: egPulse 1s infinite; }
.eg-replace-target { position: absolute; z-index: 7; right: 7rpx; bottom: 6rpx; padding: 4rpx 10rpx; border: 2rpx solid #fff; border-radius: 999rpx; color: #fff; background: #1570a6; box-shadow: 0 3rpx 9rpx rgba(20,91,135,.28); font-size: 20rpx; font-weight: 900; }
.eg-cell.is-clearing {
z-index: 8;
border-color: #fff;
animation: egHit .22s cubic-bezier(.2,.9,.3,1) forwards;
filter: brightness(1.28) saturate(1.25);
}
.eg-cell.is-yellow,
.eg-cell.is-cream,
.eg-cell.is-red,
.eg-cell.is-purple { background: #fffaf0; }
.eg-cell.is-level-low { background: #f4faf6; }
.eg-cell.is-level-mid { background: #fff9eb; }
.eg-cell.is-orange,
.eg-cell.is-level-high { background: #fff0e4; }
.eg-cell.is-milk { background: #edf7ff; }
.eg-cell.is-high { border-color: #ef7b45; }
.eg-food-image { width: 132rpx; height: 92rpx; margin-top: 7rpx; border-radius: 17rpx; filter: saturate(1.06) contrast(1.03); }
.eg-food-name { margin-top: 8rpx; color: #203d34; font-size: 29rpx; font-weight: 900; letter-spacing: 1rpx; line-height: 1.1; }
.eg-sugar-tag { position: absolute; top: 5rpx; right: 5rpx; min-width: 52rpx; padding: 4rpx 10rpx; border: 2rpx solid #fff; border-radius: 999rpx; color: #fff; box-shadow: 0 3rpx 8rpx rgba(25,55,45,.22); font-size: 23rpx; font-weight: 900; line-height: 1.25; text-align: center; }
.eg-sugar-tag.is-low { color: #316a54; border-color: rgba(255,255,255,.82); background: #dcefe6; box-shadow: 0 2rpx 5rpx rgba(32,101,75,.1); }
.eg-sugar-tag.is-mid { color: #735a15; border-color: rgba(255,255,255,.9); background: #f7e7a8; box-shadow: 0 2rpx 6rpx rgba(137,91,0,.15); }
.eg-sugar-tag.is-high { background: #d9431f; box-shadow: 0 4rpx 10rpx rgba(163,45,21,.32); }
.eg-sugar-tag.is-prop { background: #2563a8; }
.eg-special-mark { position: absolute; z-index: 12; bottom: 5rpx; left: 6rpx; display: flex; width: 40rpx; height: 40rpx; align-items: center; justify-content: center; border: 3rpx solid rgba(255,255,255,.96); border-radius: 50%; color: #fff; background: #6d28d9; box-shadow: 0 4rpx 11rpx rgba(73,31,130,.3); font-size: 23rpx; font-weight: 900; }
.eg-special-mark.is-row { width: 58rpx; height: 40rpx; border-radius: 999rpx; background: linear-gradient(135deg, #ffd64d, #ff8a18 48%, #e83b22); box-shadow: 0 0 0 4rpx rgba(255,172,30,.2), 0 4rpx 15rpx rgba(203,59,26,.45); animation: egFlameReady .7s ease-in-out infinite alternate; }
.eg-special-mark.is-col { width: 40rpx; height: 58rpx; border-radius: 999rpx; background: linear-gradient(180deg, #ffd64d, #ff8a18 48%, #e83b22); box-shadow: 0 0 0 4rpx rgba(255,172,30,.2), 0 4rpx 15rpx rgba(203,59,26,.45); animation: egFlameReady .7s ease-in-out infinite alternate; }
.eg-special-mark.is-burst { width: 48rpx; height: 48rpx; border-color: #fff3a8; border-radius: 15rpx 50% 50%; background: linear-gradient(145deg, #7c3aed 5%, #dd3b70 48%, #ff8a18 100%); box-shadow: 0 4rpx 13rpx rgba(108,35,155,.38), 0 0 12rpx rgba(255,179,38,.3); animation: egLReady 1.25s ease-in-out infinite alternate; }
.eg-mini-flame { position: relative; width: 25rpx; height: 30rpx; border-radius: 70% 32% 68% 40%; background: linear-gradient(135deg, #fff8a8 8%, #ffc21f 42%, #f04420 84%); box-shadow: 0 0 14rpx rgba(255,230,92,.95); transform: rotate(43deg); }
.eg-mini-flame-core { position: absolute; right: 4rpx; bottom: 3rpx; width: 10rpx; height: 15rpx; border-radius: 70% 35% 70% 42%; background: #fffbd1; box-shadow: 0 0 7rpx rgba(255,255,210,.95); }
.eg-l-special { position: relative; width: 29rpx; height: 29rpx; filter: drop-shadow(0 1rpx 2rpx rgba(73,22,105,.3)); }
.eg-l-arm { position: absolute; left: 3rpx; bottom: 3rpx; border-radius: 999rpx; background: #fff9b8; box-shadow: 0 0 6rpx rgba(255,249,184,.92); }
.eg-l-arm.is-vertical { width: 8rpx; height: 25rpx; }
.eg-l-arm.is-horizontal { width: 25rpx; height: 8rpx; }
.eg-l-core { position: absolute; left: 1rpx; bottom: 1rpx; width: 12rpx; height: 12rpx; border: 2rpx solid #fff; border-radius: 50%; background: #ff5b25; }
.eg-cell.is-fire-clearing { border-color: #ff7a1a; box-shadow: inset 0 0 25rpx rgba(255,102,18,.36), 0 0 18rpx rgba(255,105,20,.48); filter: brightness(1.35) saturate(1.42); }
.eg-cell.is-clearing.is-fire-clearing { animation: egFireCellHit .36s cubic-bezier(.2,.78,.22,1) forwards; }
.eg-fire-clear { position: absolute; z-index: 14; inset: 2rpx; overflow: hidden; border: 3rpx solid rgba(255,210,71,.92); border-radius: 22rpx; opacity: .96; background: linear-gradient(180deg, rgba(255,230,86,.12), rgba(239,63,27,.22)); pointer-events: none; }
.eg-fire-glow { position: absolute; inset: 6%; border-radius: 18rpx; background: radial-gradient(circle, rgba(255,246,177,.74), rgba(255,131,22,.32) 47%, rgba(222,47,23,0) 75%); animation: egFireGlow .34s ease-out both; }
.eg-fire-sweep { position: absolute; top: 23%; left: -48%; width: 168%; height: 55%; border-radius: 60% 45% 55% 42%; background: linear-gradient(90deg, rgba(255,184,38,0), rgba(255,238,124,.94) 28%, rgba(255,133,22,.92) 53%, rgba(226,54,25,.82) 72%, rgba(255,184,38,0)); box-shadow: 0 0 27rpx rgba(255,91,17,.72); animation: egFireSweep .34s cubic-bezier(.2,.78,.22,1) both; }
.eg-fire-flame { position: absolute; bottom: 13%; width: 25rpx; height: 39rpx; border-radius: 72% 35% 68% 42%; background: linear-gradient(145deg, #fff7a6 4%, #ffc329 39%, #ff7419 66%, #dc321f 100%); box-shadow: 0 0 15rpx rgba(255,124,21,.88); transform: rotate(42deg); animation: egFireFlame .34s ease-out both; }
.eg-fire-flame > view { position: absolute; right: 5rpx; bottom: 4rpx; width: 10rpx; height: 18rpx; border-radius: 70% 35% 70% 42%; background: #fffbd6; }
.eg-fire-flame.is-left { left: 16%; animation-delay: .01s; }
.eg-fire-flame.is-center { left: 43%; bottom: 19%; transform: rotate(42deg) scale(1.18); animation-delay: .035s; }
.eg-fire-flame.is-right { right: 15%; animation-delay: .065s; }
.eg-fire-spark { position: absolute; width: 10rpx; height: 17rpx; border-radius: 70% 35% 70% 42%; background: #ffe45e; box-shadow: 0 0 11rpx rgba(255,139,28,.9); animation: egFireSpark .34s ease-out both; }
.eg-fire-spark.is-one { top: 46%; left: 25%; }
.eg-fire-spark.is-two { top: 34%; left: 62%; animation-delay: .035s; }
.eg-fire-spark.is-three { top: 52%; left: 78%; animation-delay: .07s; }
.eg-board-tip { display: flex; align-items: center; justify-content: center; gap: 8rpx; min-height: 58rpx; margin-top: 12rpx; color: #684817; font-size: 26rpx; font-weight: 700; text-align: center; }
.eg-impact-text {
position: absolute;
z-index: 20;
top: 46%;
left: 50%;
padding: 10rpx 24rpx;
border: 4rpx solid rgba(255,255,255,.9);
border-radius: 999rpx;
color: #fff;
background: linear-gradient(135deg, #f59e0b, #e4572e);
box-shadow: 0 10rpx 28rpx rgba(190, 70, 25, .35);
font-size: 34rpx;
font-weight: 900;
transform: translate(-50%, -50%);
animation: egImpact .52s cubic-bezier(.16,.82,.3,1) forwards;
pointer-events: none;
}
.eg-impact-text.is-level-2 { font-size: 40rpx; background: linear-gradient(135deg, #8b5cf6, #ec4899); }
.eg-impact-text.is-level-3 { font-size: 46rpx; background: linear-gradient(135deg, #ef4444, #f59e0b); box-shadow: 0 12rpx 36rpx rgba(239,68,68,.42); }
.eg-jackpot {
position: absolute;
z-index: 30;
top: 48%;
left: 50%;
width: 540rpx;
padding: 24rpx 22rpx 20rpx;
overflow: hidden;
border: 7rpx solid #ffd83d;
border-radius: 36rpx;
color: #fff;
background: radial-gradient(circle at 50% 0%, #ef4444, #9f1239 65%, #64102a);
box-shadow: 0 0 0 7rpx #fff2a8, 0 22rpx 52rpx rgba(100,16,42,.5);
text-align: center;
transform: translate(-50%, -50%);
animation: egJackpotIn 1.35s cubic-bezier(.16,.85,.25,1) forwards;
pointer-events: none;
}
.eg-jackpot.is-match-4 { background: radial-gradient(circle at 50% 0%, #8b5cf6, #5b21b6 68%, #35106d); }
.eg-jackpot.is-match-5 { border-color: #fff36a; background: radial-gradient(circle at 50% 0%, #ff6b24, #d7193f 58%, #760c2a); animation-duration: 1.9s; }
.eg-jackpot.is-match-3 {
width: 414rpx;
padding: 20rpx 22rpx 18rpx;
border-width: 4rpx;
border-color: #f1c663;
color: #31574b;
background: linear-gradient(155deg, #f7fff9, #fff5d9);
box-shadow: 0 0 0 4rpx rgba(255,255,255,.84), 0 14rpx 34rpx rgba(98,82,39,.2);
animation: egTripleIn 1.12s cubic-bezier(.2,.82,.28,1) forwards;
}
.eg-jackpot.is-match-3 .eg-jackpot-lights { display: none; }
.eg-jackpot.is-match-3 .eg-jackpot-kicker { color: #5b8c73; font-size: 21rpx; letter-spacing: 3rpx; text-shadow: none; }
.eg-jackpot.is-match-3 .eg-jackpot-title { color: #365b4e; font-size: 31rpx; text-shadow: none; }
.eg-jackpot.is-match-3 .eg-jackpot-reward { color: #ad7021; font-size: 25rpx; }
.eg-jackpot.is-match-3 .eg-jackpot-note { color: #829087; }
.eg-triple-cans { position: relative; display: flex; justify-content: center; gap: 12rpx; margin: 12rpx 0 10rpx; }
.eg-triple-can { display: flex; width: 78rpx; height: 76rpx; align-items: center; justify-content: center; border: 3rpx solid #f4d996; border-radius: 19rpx; background: rgba(255,255,255,.9); box-shadow: 0 5rpx 12rpx rgba(128,92,30,.12); animation: egTripleCan .42s cubic-bezier(.2,.86,.26,1) both; }
.eg-triple-can:nth-child(2) { animation-delay: .07s; }
.eg-triple-can:nth-child(3) { animation-delay: .14s; }
.eg-triple-can > image { width: 62rpx; height: 58rpx; }
.eg-jackpot.is-match-4 {
width: 548rpx;
padding: 25rpx 22rpx 22rpx;
border-color: #ffd95b;
background: radial-gradient(circle at 50% -10%, #a97cff, #6031b5 58%, #341066);
box-shadow: 0 0 0 6rpx #fff0a9, 0 0 36rpx rgba(255,210,76,.5), 0 24rpx 58rpx rgba(53,16,109,.48);
animation: egFourIn 1.88s cubic-bezier(.16,.84,.24,1) forwards;
}
.eg-jackpot.is-match-4 .eg-jackpot-kicker { color: #ffef8d; font-size: 24rpx; letter-spacing: 3rpx; }
.eg-jackpot.is-match-4 .eg-jackpot-title { margin-top: 9rpx; color: #fff; font-size: 39rpx; }
.eg-jackpot.is-match-4 .eg-jackpot-reward { color: #fff2a8; }
.eg-four-teaser { position: relative; margin-top: 13rpx; }
.eg-four-cans { display: flex; align-items: center; justify-content: center; gap: 8rpx; }
.eg-four-can { display: flex; width: 76rpx; height: 92rpx; align-items: center; justify-content: center; border: 4rpx solid #ffe078; border-radius: 18rpx; background: linear-gradient(180deg, #fffef7, #ffe9a5); box-shadow: inset 0 -5rpx 0 rgba(171,106,17,.12), 0 7rpx 13rpx rgba(34,12,67,.28); animation: egFourCan .48s cubic-bezier(.18,.9,.25,1.2) both; }
.eg-four-can:nth-child(2) { animation-delay: .07s; }
.eg-four-can:nth-child(3) { animation-delay: .14s; }
.eg-four-can:nth-child(4) { animation-delay: .21s; }
.eg-four-can > image { width: 64rpx; height: 70rpx; }
.eg-four-can.is-locked { border-style: dashed; border-color: rgba(255,241,161,.86); color: #fff6ae; background: rgba(48,18,91,.54); box-shadow: inset 0 0 16rpx rgba(255,225,91,.18), 0 0 18rpx rgba(255,222,91,.36); font-size: 48rpx; font-weight: 1000; animation: egLockedCan .68s .34s ease-in-out infinite alternate; }
.eg-four-promise { display: inline-flex; margin-top: 15rpx; padding: 8rpx 18rpx; border: 2rpx solid rgba(255,247,184,.72); border-radius: 999rpx; color: #3d176f; background: linear-gradient(90deg, #fff3a3, #ffd85b); box-shadow: 0 5rpx 15rpx rgba(28,7,62,.22); font-size: 25rpx; font-weight: 1000; animation: egPromisePulse .72s ease-in-out infinite alternate; }
.eg-five-celebration { position: relative; margin: 15rpx 0 10rpx; }
.eg-five-cans { display: flex; align-items: center; justify-content: center; gap: 8rpx; }
.eg-five-can { display: flex; width: 78rpx; height: 93rpx; align-items: center; justify-content: center; border: 4rpx solid #fff17b; border-radius: 18rpx; background: linear-gradient(180deg, #fff, #fff2ac); box-shadow: inset 0 -5rpx 0 rgba(184,91,12,.13), 0 7rpx 0 #bd5b13, 0 0 16rpx rgba(255,240,101,.5); animation: egFiveCan .56s cubic-bezier(.16,.92,.24,1.2) both; }
.eg-five-can:nth-child(2) { animation-delay: .07s; }
.eg-five-can:nth-child(3) { animation-delay: .14s; }
.eg-five-can:nth-child(4) { animation-delay: .21s; }
.eg-five-can:nth-child(5) { animation-delay: .28s; }
.eg-five-can > image { width: 65rpx; height: 72rpx; }
.eg-five-sevens { display: flex; justify-content: center; gap: 8rpx; margin-top: 13rpx; }
.eg-five-sevens > text { display: flex; width: 58rpx; height: 55rpx; align-items: center; justify-content: center; border: 3rpx solid #fff5a7; border-radius: 13rpx; color: #fff36a; background: linear-gradient(155deg, #d7193f, #9f1239); box-shadow: 0 5rpx 0 #70102b, 0 0 13rpx rgba(255,243,106,.45); font-size: 43rpx; font-weight: 1000; line-height: 1; text-shadow: 0 3rpx 0 #8f1733; }
.eg-jackpot-lights { position: absolute; inset: 8rpx; border: 4rpx dotted rgba(255,255,255,.88); border-radius: 25rpx; animation: egLights .24s steps(2) infinite; }
.eg-jackpot-kicker { position: relative; display: block; color: #fff36a; font-size: 28rpx; font-weight: 900; letter-spacing: 6rpx; text-shadow: 0 3rpx 0 rgba(83,19,25,.45); }
.eg-jackpot-reels { position: relative; display: flex; justify-content: center; gap: 12rpx; margin: 13rpx 0; }
.eg-jackpot-reel { display: flex; width: 112rpx; height: 126rpx; flex-direction: column; align-items: center; justify-content: center; border: 6rpx solid #ffcf31; border-radius: 20rpx; background: linear-gradient(180deg, #fff 0%, #fff7cf 48%, #ffd55e 50%, #fff 53%, #fff8dc 100%); box-shadow: inset 0 0 18rpx rgba(137,78,10,.25), 0 7rpx 0 #a85a0a; animation: egReelStop .52s cubic-bezier(.2,.9,.25,1) both; }
.eg-jackpot-reel:nth-child(2) { animation-delay: .09s; }
.eg-jackpot-reel:nth-child(3) { animation-delay: .18s; }
.eg-jackpot-seven { color: #e11d48; font-size: 61rpx; font-weight: 1000; line-height: .85; text-shadow: 0 3rpx 0 #ffd1d8; }
.eg-jackpot-milk { width: 54rpx; height: 36rpx; margin-top: 3rpx; border-radius: 7rpx; }
.eg-jackpot-title { position: relative; display: block; font-size: 37rpx; font-weight: 900; text-shadow: 0 4rpx 0 rgba(73,10,30,.5); }
.eg-jackpot-points { position: relative; display: block; margin-top: 2rpx; color: #fff36a; font-size: 66rpx; font-weight: 1000; line-height: 1.05; letter-spacing: 2rpx; text-shadow: 0 5rpx 0 #9e2813, 0 0 18rpx rgba(255,243,106,.85); animation: egPointsPop .62s .42s cubic-bezier(.16,.9,.25,1.25) both; }
.eg-jackpot-reward { position: relative; display: block; margin-top: 7rpx; color: #fff5a5; font-size: 27rpx; font-weight: 800; line-height: 1.35; }
.eg-jackpot-note { position: relative; display: block; margin-top: 8rpx; color: rgba(255,255,255,.8); font-size: 22rpx; }
.eg-tools { margin-top: 16rpx; }
.eg-tool { display: flex; align-items: center; padding: 16rpx 18rpx; border: 3rpx solid #c9e0d6; border-radius: 26rpx; background: rgba(255,255,255,.92); box-shadow: 0 7rpx 18rpx rgba(24, 84, 63, .07); }
.eg-tool.active { border-color: #f59e0b; background: #fffbeb; }
.eg-tool.disabled { opacity: .45; }
.eg-tool-icon { position: relative; display: flex; width: 88rpx; height: 88rpx; align-items: center; justify-content: center; overflow: visible; border-radius: 24rpx; font-size: 46rpx; background: linear-gradient(145deg, #e8f7ff, #d4ecff); box-shadow: inset 0 -4rpx 0 rgba(33,104,151,.08); }
.eg-tool-food-image { width: 76rpx; height: 58rpx; border-radius: 14rpx; }
.eg-replace-badge { position: absolute; right: -12rpx; bottom: -8rpx; display: flex; min-width: 78rpx; height: 38rpx; align-items: center; justify-content: center; gap: 2rpx; padding: 0 8rpx; border: 3rpx solid #fff; border-radius: 999rpx; color: #fff; background: linear-gradient(135deg, #2389b9, #145d91); box-shadow: 0 5rpx 12rpx rgba(20,93,145,.3); }
.eg-replace-badge .tj-icon-wrap { width: 22rpx; height: 22rpx; }
.eg-replace-badge > text { font-size: 20rpx; font-weight: 900; line-height: 1; }
.eg-tool-copy { display: flex; flex: 1; flex-direction: column; margin-left: 16rpx; }
.eg-tool-name { color: #244a3d; font-size: 29rpx; font-weight: 800; }
.eg-tool-desc { margin-top: 5rpx; color: #6c8179; font-size: 25rpx; }
.eg-tool-count { color: #155e4b; font-size: 32rpx; font-weight: 900; }
.eg-legend { display: flex; justify-content: center; gap: 18rpx; margin-top: 18rpx; color: #53685f; font-size: 25rpx; font-weight: 700; }
.eg-dot--safe { color: #4cae7b; }
.eg-dot--mid { color: #e2aa19; }
.eg-dot--high { color: #e4572e; }
.eg-weekly-overlay { position: fixed; z-index: 90; inset: 0; display: flex; align-items: center; justify-content: center; padding: 28rpx; background: rgba(16, 55, 43, .66); backdrop-filter: blur(5px); }
.eg-weekly-card { width: 100%; max-width: 664rpx; max-height: calc(100vh - 56rpx); overflow-y: auto; padding: 28rpx 26rpx 22rpx; border: 5rpx solid rgba(255,255,255,.96); border-radius: 38rpx; background: linear-gradient(160deg, #f5fff9 0%, #fffdf3 56%, #fff4db 100%); box-shadow: 0 30rpx 80rpx rgba(5,45,32,.38); animation: egWeeklyIn .36s cubic-bezier(.18,.88,.27,1.08) both; }
.eg-weekly-head { display: flex; align-items: center; justify-content: space-between; }
.eg-weekly-title-line { display: flex; align-items: center; gap: 10rpx; }
.eg-weekly-title { color: #155e4b; font-size: 37rpx; font-weight: 1000; }
.eg-weekly-preview { padding: 5rpx 10rpx; border-radius: 999rpx; color: #9a5b14; background: #fff0bd; font-size: 19rpx; font-weight: 900; }
.eg-weekly-sub { display: block; margin-top: 5rpx; color: #6b837a; font-size: 23rpx; font-weight: 700; }
.eg-weekly-medal { display: flex; width: 76rpx; height: 76rpx; align-items: center; justify-content: center; border: 5rpx solid #fff3b2; border-radius: 50%; color: #fff; background: linear-gradient(145deg, #f5ad24, #e26925); box-shadow: 0 8rpx 0 #b85421, 0 12rpx 24rpx rgba(181,84,33,.24); font-size: 39rpx; font-weight: 1000; }
.eg-weekly-progress-card { display: flex; align-items: center; justify-content: space-between; margin-top: 20rpx; padding: 16rpx 18rpx; border: 3rpx solid #d7ebe2; border-radius: 24rpx; background: rgba(255,255,255,.9); }
.eg-weekly-progress-label { display: block; color: #657b73; font-size: 22rpx; font-weight: 700; }
.eg-weekly-progress-value { display: block; margin-top: -2rpx; color: #e06b24; font-size: 43rpx; font-weight: 1000; }
.eg-weekly-progress-copy { display: flex; flex-direction: column; align-items: flex-end; color: #235b49; font-size: 24rpx; font-weight: 900; line-height: 1.55; }
.eg-weekly-progress-copy text:last-child { color: #b86820; }
.eg-weekly-list { display: flex; flex-direction: column; gap: 7rpx; margin-top: 15rpx; }
.eg-weekly-row { display: flex; min-height: 68rpx; align-items: center; padding: 7rpx 14rpx; border: 2rpx solid transparent; border-radius: 19rpx; color: #345c50; background: rgba(255,255,255,.72); }
.eg-weekly-row.is-me { border-color: #f2bd48; color: #174f3e; background: linear-gradient(90deg, #fff2bb, #fff9e3); box-shadow: 0 5rpx 14rpx rgba(153,102,17,.12); transform: scale(1.015); }
.eg-weekly-place { width: 43rpx; color: #6b7d76; font-size: 27rpx; font-weight: 1000; text-align: center; }
.eg-weekly-row:nth-child(1) .eg-weekly-place { color: #dd7b17; font-size: 31rpx; }
.eg-weekly-avatar { display: flex; width: 52rpx; height: 52rpx; flex: 0 0 52rpx; align-items: center; justify-content: center; margin-left: 6rpx; border: 3rpx solid rgba(255,255,255,.9); border-radius: 50%; color: #fff; background: #5d9b84; box-shadow: 0 3rpx 9rpx rgba(30,78,61,.16); font-size: 24rpx; font-weight: 1000; }
.eg-weekly-avatar.is-tone-2 { background: #e39932; }
.eg-weekly-avatar.is-tone-3 { background: #6c83c8; }
.eg-weekly-avatar.is-tone-4 { background: #e5683e; }
.eg-weekly-avatar.is-tone-5 { background: #a673bd; }
.eg-weekly-avatar.is-tone-6 { background: #448e9c; }
.eg-weekly-avatar.is-tone-7 { background: #c27b49; }
.eg-weekly-name { flex: 1; margin-left: 13rpx; overflow: hidden; font-size: 27rpx; font-weight: 850; text-overflow: ellipsis; white-space: nowrap; }
.eg-weekly-me { margin-right: 8rpx; padding: 3rpx 9rpx; border-radius: 999rpx; color: #fff; background: #dd6b27; font-size: 18rpx; font-weight: 900; }
.eg-weekly-count { min-width: 70rpx; color: #315b4d; font-size: 29rpx; font-weight: 1000; text-align: right; }
.eg-cheer-card { margin-top: 16rpx; padding: 15rpx 17rpx 14rpx; border: 3rpx solid #f0d38d; border-radius: 23rpx; background: linear-gradient(135deg, #fff9d9, #fff1c2); }
.eg-cheer-card.is-family { border-color: #d9c8ef; background: linear-gradient(135deg, #f8f2ff, #efe7ff); }
.eg-cheer-card.is-bright { border-color: #f4c77b; background: linear-gradient(135deg, #fff8dc, #ffeec7); }
.eg-cheer-top { display: flex; align-items: center; justify-content: space-between; }
.eg-cheer-source { color: #8e5c18; font-size: 21rpx; font-weight: 900; }
.eg-cheer-card.is-family .eg-cheer-source { color: #77539c; }
.eg-cheer-change { padding: 5rpx 12rpx; border-radius: 999rpx; color: #176b52; background: rgba(255,255,255,.75); font-size: 19rpx; font-weight: 900; }
.eg-cheer-copy { display: block; margin-top: 7rpx; color: #55462d; font-size: 28rpx; font-weight: 900; line-height: 1.42; }
.eg-cheer-note { display: block; margin-top: 5rpx; color: #8d7c77; font-size: 18rpx; }
.eg-weekly-start { display: flex; height: 84rpx; align-items: center; justify-content: center; margin-top: 18rpx; border-radius: 24rpx; color: #fff; background: linear-gradient(135deg, #24906c, #12654a); box-shadow: 0 9rpx 20rpx rgba(18,101,74,.24); font-size: 30rpx; font-weight: 1000; }
.eg-weekly-footnote { display: block; margin-top: 10rpx; color: #8b8b7d; font-size: 18rpx; text-align: center; }
.eg-stage-overlay { position: fixed; z-index: 58; inset: 0; display: flex; align-items: center; justify-content: center; padding: 40rpx; background: rgba(22,62,50,.42); pointer-events: none; }
.eg-stage-card { width: 100%; max-width: 620rpx; padding: 34rpx 28rpx 30rpx; border: 6rpx solid #fff; border-radius: 38rpx; background: linear-gradient(155deg, #f5fff9, #fff8da); box-shadow: 0 26rpx 70rpx rgba(10,56,41,.3); text-align: center; animation: egStageIn 1.8s cubic-bezier(.16,.86,.28,1) both; }
.eg-stage-kicker { display: block; color: #16815f; font-size: 23rpx; font-weight: 900; letter-spacing: 3rpx; }
.eg-stage-number { display: block; margin-top: 5rpx; color: #e47722; font-size: 48rpx; font-weight: 1000; line-height: 1.1; }
.eg-stage-title { display: block; margin-top: 7rpx; color: #194e3d; font-size: 38rpx; font-weight: 900; }
.eg-stage-foods { display: flex; justify-content: center; gap: 9rpx; margin-top: 22rpx; }
.eg-stage-food { display: flex; width: 92rpx; min-height: 110rpx; flex-direction: column; align-items: center; justify-content: center; border: 3rpx solid #dbece4; border-radius: 20rpx; color: #315b4e; background: #fff; font-size: 20rpx; font-weight: 800; }
.eg-stage-food > image { width: 72rpx; height: 52rpx; border-radius: 12rpx; }
.eg-stage-food > text { margin-top: 8rpx; }
.eg-stage-food.is-danger { border-color: #ef8b52; color: #a43c20; background: #fff1e6; box-shadow: 0 0 0 4rpx rgba(239,139,82,.12); }
.eg-stage-tip { display: block; margin-top: 20rpx; color: #6e776d; font-size: 21rpx; font-weight: 700; }
.eg-reshuffle-overlay { position: fixed; z-index: 65; inset: 0; display: flex; align-items: center; justify-content: center; padding: 46rpx; background: rgba(19,55,45,.5); }
.eg-reshuffle-card { width: 100%; max-width: 540rpx; padding: 38rpx 30rpx; border: 5rpx solid rgba(255,255,255,.95); border-radius: 36rpx; background: linear-gradient(155deg, #f4fff9, #e6f6ef); box-shadow: 0 24rpx 64rpx rgba(10,48,37,.3); text-align: center; }
.eg-reshuffle-icon { display: flex; width: 94rpx; height: 94rpx; align-items: center; justify-content: center; margin: 0 auto 18rpx; border-radius: 50%; background: linear-gradient(135deg, #2aa87b, #126a50); box-shadow: 0 10rpx 24rpx rgba(18,106,80,.28); animation: egReshuffleSpin .85s linear infinite; }
.eg-reshuffle-title { display: block; color: #174e3d; font-size: 36rpx; font-weight: 900; }
.eg-reshuffle-desc { display: block; margin-top: 10rpx; color: #5b746b; font-size: 24rpx; font-weight: 700; }
.eg-overlay { position: fixed; z-index: 50; top: 0; right: 0; bottom: 0; left: 0; display: flex; align-items: center; justify-content: center; padding: 36rpx; background: rgba(20, 48, 40, .62); }
.eg-modal { width: 100%; max-width: 650rpx; padding: 34rpx 28rpx 28rpx; border-radius: 38rpx; background: #fffdf8; box-shadow: 0 28rpx 70rpx rgba(0,0,0,.22); }
.eg-modal-icon { display: block; font-size: 68rpx; text-align: center; }
.eg-modal-title { display: block; margin-top: 6rpx; color: #204e3d; font-size: 38rpx; font-weight: 900; text-align: center; }
.eg-modal-sub { display: block; margin: 8rpx 0 22rpx; color: #718078; font-size: 24rpx; text-align: center; }
.eg-result-warning { display: flex; width: 92rpx; height: 92rpx; align-items: center; justify-content: center; margin: 0 auto; border-radius: 50%; color: #fff; background: #e4572e; font-size: 60rpx; font-weight: 900; }
.eg-result-food { display: block; margin: 12rpx 0 18rpx; color: #c34b27; font-size: 28rpx; font-weight: 800; text-align: center; }
.eg-knowledge-card { padding: 22rpx; border-radius: 24rpx; background: #fff3e7; }
.eg-knowledge-title { display: block; color: #8d4427; font-size: 26rpx; font-weight: 800; }
.eg-knowledge-copy { display: block; margin-top: 9rpx; color: #5f514a; font-size: 23rpx; line-height: 1.65; }
.eg-knowledge-review { display: block; margin-top: 12rpx; color: #9b7768; font-size: 19rpx; }
.eg-result-score { display: flex; align-items: center; justify-content: space-between; margin: 20rpx 6rpx; color: #466258; font-size: 25rpx; }
.eg-result-score text:last-child { color: #e16f24; font-size: 36rpx; font-weight: 900; }
.eg-result-best { display: flex; align-items: center; justify-content: space-between; margin: -12rpx 6rpx 20rpx; color: #75867f; font-size: 22rpx; }
.eg-result-best text:last-child { color: #315d4e; font-size: 27rpx; font-weight: 900; }
.eg-primary-btn, .eg-secondary-btn { display: flex; height: 88rpx; align-items: center; justify-content: center; border-radius: 24rpx; font-size: 28rpx; font-weight: 800; }
.eg-primary-btn { color: #fff; background: linear-gradient(135deg, #238b68, #126649); box-shadow: 0 10rpx 22rpx rgba(18,102,73,.23); }
.eg-secondary-btn { margin-top: 12rpx; color: #45665b; background: #edf4f1; }
@keyframes egPulse {
0%, 100% { box-shadow: 0 0 0 0 rgba(245,158,11,.2); }
50% { box-shadow: 0 0 0 8rpx rgba(245,158,11,.12); }
}
@keyframes egWeeklyIn {
0% { opacity: 0; transform: translateY(34rpx) scale(.91); }
100% { opacity: 1; transform: translateY(0) scale(1); }
}
@keyframes egHit {
0% { opacity: 1; transform: scale(1); }
42% { opacity: 1; transform: scale(1.18) rotate(-3deg); }
100% { opacity: 0; transform: scale(.32) rotate(7deg); }
}
@keyframes egTaskRewardFlight {
0% { opacity: 0; transform: translate3d(-20rpx, 0, 0) scale(.62) rotate(-8deg); }
16% { opacity: 1; transform: translate3d(0, -24rpx, 0) scale(1.1) rotate(3deg); }
72% { opacity: 1; transform: translate3d(270rpx, 900rpx, 0) scale(.68) rotate(8deg); }
100% { opacity: 0; transform: translate3d(350rpx, 1205rpx, 0) scale(.3) rotate(2deg); }
}
@keyframes egRewardGlow {
from { opacity: .4; transform: scale(.86); }
to { opacity: .9; transform: scale(1.12); }
}
@keyframes egFireSweep {
0% { opacity: 0; transform: translate3d(-38%, 20rpx, 0) scaleX(.6); }
28% { opacity: 1; }
72% { opacity: .96; }
100% { opacity: 0; transform: translate3d(58%, -15rpx, 0) scaleX(1.08); }
}
@keyframes egFireSpark {
0% { opacity: 0; transform: translateY(14rpx) scale(.5) rotate(35deg); }
38% { opacity: 1; }
100% { opacity: 0; transform: translateY(-38rpx) scale(1.05) rotate(58deg); }
}
@keyframes egFireGlow {
0% { opacity: 0; transform: scale(.72); }
38% { opacity: 1; transform: scale(1.08); }
100% { opacity: 0; transform: scale(1.28); }
}
@keyframes egFireCellHit {
0% { opacity: 1; transform: scale(1); }
24% { opacity: 1; transform: scale(1.09); }
72% { opacity: 1; transform: scale(1.02); }
100% { opacity: 0; transform: scale(.48); }
}
@keyframes egFireFlame {
0% { opacity: 0; transform: translateY(20rpx) rotate(42deg) scale(.55); }
38% { opacity: 1; transform: translateY(0) rotate(42deg) scale(1.05); }
100% { opacity: 0; transform: translateY(-31rpx) rotate(48deg) scale(.86); }
}
@keyframes egFlameReady {
from { transform: scale(.94); filter: brightness(.96); }
to { transform: scale(1.08); filter: brightness(1.18); }
}
@keyframes egLReady {
from { transform: scale(.94) rotate(-2deg); filter: brightness(.96); }
to { transform: scale(1.06) rotate(2deg); filter: brightness(1.12); }
}
@keyframes egImpact {
0% { opacity: 0; transform: translate(-50%, -42%) scale(.55); }
35% { opacity: 1; transform: translate(-50%, -50%) scale(1.16); }
72% { opacity: 1; transform: translate(-50%, -54%) scale(1); }
100% { opacity: 0; transform: translate(-50%, -76%) scale(.92); }
}
@keyframes egJackpotIn {
0% { opacity: 0; transform: translate(-50%, -45%) scale(.48) rotate(-4deg); }
20% { opacity: 1; transform: translate(-50%, -50%) scale(1.09) rotate(2deg); }
31% { transform: translate(-50%, -50%) scale(.98) rotate(0); }
78% { opacity: 1; transform: translate(-50%, -53%) scale(1); }
100% { opacity: 0; transform: translate(-50%, -68%) scale(.92); }
}
@keyframes egTripleIn {
0% { opacity: 0; transform: translate(-50%, -44%) scale(.82); }
20% { opacity: 1; transform: translate(-50%, -50%) scale(1.03); }
74% { opacity: 1; transform: translate(-50%, -52%) scale(1); }
100% { opacity: 0; transform: translate(-50%, -60%) scale(.96); }
}
@keyframes egTripleCan {
0% { opacity: 0; transform: translateY(18rpx) scale(.72); }
100% { opacity: 1; transform: translateY(0) scale(1); }
}
@keyframes egFourIn {
0% { opacity: 0; transform: translate(-50%, -44%) scale(.62) rotate(-3deg); }
18% { opacity: 1; transform: translate(-50%, -50%) scale(1.08) rotate(1deg); }
29% { transform: translate(-50%, -50%) scale(.98) rotate(0); }
78% { opacity: 1; transform: translate(-50%, -52%) scale(1); }
100% { opacity: 0; transform: translate(-50%, -65%) scale(.94); }
}
@keyframes egFourCan {
0% { opacity: 0; transform: translateY(-22rpx) rotate(-7deg) scale(.65); }
75% { opacity: 1; transform: translateY(5rpx) rotate(2deg) scale(1.05); }
100% { opacity: 1; transform: translateY(0) rotate(0) scale(1); }
}
@keyframes egFiveCan {
0% { opacity: 0; transform: translateY(-40rpx) scale(.52) rotate(-8deg); filter: brightness(1.5); }
70% { opacity: 1; transform: translateY(7rpx) scale(1.08) rotate(2deg); }
100% { opacity: 1; transform: translateY(0) scale(1) rotate(0); filter: brightness(1); }
}
@keyframes egLockedCan {
from { opacity: .62; transform: scale(.92); filter: brightness(.9); }
to { opacity: 1; transform: scale(1.06); filter: brightness(1.2); }
}
@keyframes egPromisePulse {
from { transform: scale(.97); box-shadow: 0 5rpx 15rpx rgba(28,7,62,.22); }
to { transform: scale(1.03); box-shadow: 0 7rpx 21rpx rgba(255,221,89,.38); }
}
@keyframes egReelStop {
0% { opacity: .2; transform: translateY(-48rpx) scaleY(1.4); filter: blur(5rpx); }
70% { opacity: 1; transform: translateY(7rpx) scaleY(.94); filter: blur(0); }
100% { transform: translateY(0) scaleY(1); }
}
@keyframes egLights {
0% { opacity: .35; }
100% { opacity: 1; }
}
@keyframes egConfettiFall {
0% { opacity: 0; transform: translate3d(0, -8vh, 0) rotate(0deg) scale(.7); }
9% { opacity: 1; }
48% { transform: translate3d(42rpx, 48vh, 0) rotate(420deg) scale(1); }
100% { opacity: .92; transform: translate3d(-28rpx, 112vh, 0) rotate(920deg) scale(.86); }
}
@keyframes egPointsPop {
0% { opacity: 0; transform: scale(.35) rotate(-7deg); }
55% { opacity: 1; transform: scale(1.28) rotate(3deg); }
78% { transform: scale(.92) rotate(0); }
100% { opacity: 1; transform: scale(1); }
}
@keyframes egStageIn {
0% { opacity: 0; transform: scale(.7) translateY(34rpx); }
18% { opacity: 1; transform: scale(1.06) translateY(0); }
30%, 76% { opacity: 1; transform: scale(1); }
100% { opacity: 0; transform: scale(.96) translateY(-22rpx); }
}
@keyframes egReshuffleSpin {
from { transform: rotate(0); }
to { transform: rotate(360deg); }
}
@media screen and (max-height: 760px) {
.eg-task-card { padding-top: 13rpx; padding-bottom: 13rpx; }
}
File diff suppressed because it is too large Load Diff
+29 -9
View File
@@ -98,15 +98,27 @@
<view v-if="chartUseMockData" class="st-chart-foot"> <view v-if="chartUseMockData" class="st-chart-foot">
<text class="st-chart-mock-hint">示例数据累计记录满 7 天后显示您的真实趋势</text> <text class="st-chart-mock-hint">示例数据累计记录满 7 天后显示您的真实趋势</text>
</view> </view>
<view class="st-game-entry st-game-entry--in-card" @click="goGamePage" style="margin: 0 20rpx 0 20rpx;"> <view class="st-game-entry-list">
<view class="st-game-entry-icon"> <view class="st-game-entry st-game-entry--in-card" @click="goSugarRaidGamePage">
<TongjiIcon name="activity" size="sm" color="#006c49" /> <view class="st-game-entry-icon">
<TongjiIcon name="activity" size="sm" color="#006c49" />
</view>
<view class="st-game-entry-body">
<text class="st-game-entry-title">识糖小课堂</text>
<text class="st-game-entry-sub">认一认哪些食物含糖高</text>
</view>
<TongjiIcon name="chevron-right" size="sm" color="#64748b" />
</view> </view>
<view class="st-game-entry-body"> <view class="st-game-entry st-game-entry--in-card" @click="goEndlessGamePage">
<text class="st-game-entry-title">识糖小课堂</text> <view class="st-game-entry-icon">
<text class="st-game-entry-sub">认一认哪些食物含糖高</text> <TongjiIcon name="sparkles" size="sm" color="#006c49" />
</view>
<view class="st-game-entry-body">
<text class="st-game-entry-title">控糖消消乐</text>
<text class="st-game-entry-sub">无尽消除挑战控糖高分</text>
</view>
<TongjiIcon name="chevron-right" size="sm" color="#64748b" />
</view> </view>
<TongjiIcon name="chevron-right" size="sm" color="#64748b" />
</view> </view>
</view> </view>
<view class="st-training-section"> <view class="st-training-section">
@@ -1512,15 +1524,23 @@ async function goMorePage() {
uni.navigateTo({ url: buildMorePageUrl() }) uni.navigateTo({ url: buildMorePageUrl() })
} }
function goGamePage() { function openGamePage(url) {
uni.navigateTo({ uni.navigateTo({
url: '/tongji/pages/game', url,
fail() { fail() {
uni.showToast({ title: '暂时无法打开游戏', icon: 'none' }) uni.showToast({ title: '暂时无法打开游戏', icon: 'none' })
} }
}) })
} }
function goSugarRaidGamePage() {
openGamePage('/tongji/pages/game')
}
function goEndlessGamePage() {
openGamePage('/tongji/endless-game/index')
}
function navToUser() { function navToUser() {
uni.redirectTo({ url: '/pages/user/user' }) uni.redirectTo({ url: '/pages/user/user' })
} }
@@ -177,7 +177,7 @@
transform: scale(0.98); transform: scale(0.98);
} }
/* 糖分突袭游戏入口 */ /* 游戏入口 */
.weekly-page .st-game-entry { .weekly-page .st-game-entry {
margin-top: 24rpx; margin-top: 24rpx;
@@ -388,8 +388,15 @@
} }
/* 识糖入口嵌在趋势卡内:轻量分隔,避免大块留白 */ /* 识糖入口嵌在趋势卡内:轻量分隔,避免大块留白 */
.weekly-page .st-chart-card .st-game-entry-list {
display: flex;
flex-direction: column;
gap: 12rpx;
margin: 12rpx 20rpx 0;
}
.weekly-page .st-chart-card .st-game-entry--in-card { .weekly-page .st-chart-card .st-game-entry--in-card {
margin-top: 12rpx; margin-top: 0;
padding: 16rpx 20rpx; padding: 16rpx 20rpx;
gap: 16rpx; gap: 16rpx;
border-radius: 24rpx; border-radius: 24rpx;
+283
View File
File diff suppressed because one or more lines are too long
-7
View File
@@ -11,7 +11,6 @@
"dependencies": { "dependencies": {
"@element-plus/icons-vue": "^2.3.1", "@element-plus/icons-vue": "^2.3.1",
"@highlightjs/vue-plugin": "^2.1.0", "@highlightjs/vue-plugin": "^2.1.0",
"@mediapipe/tasks-vision": "^0.10.35",
"@tencentcloud/call-uikit-vue": "^4.0.12", "@tencentcloud/call-uikit-vue": "^4.0.12",
"@tencentcloud/chat-uikit-vue3": "^4.5.4", "@tencentcloud/chat-uikit-vue3": "^4.5.4",
"@trtc/calls-uikit-vue": "^4.4.6", "@trtc/calls-uikit-vue": "^4.4.6",
@@ -2485,12 +2484,6 @@
"@jridgewell/sourcemap-codec": "^1.4.14" "@jridgewell/sourcemap-codec": "^1.4.14"
} }
}, },
"node_modules/@mediapipe/tasks-vision": {
"version": "0.10.35",
"resolved": "https://registry.npmjs.org/@mediapipe/tasks-vision/-/tasks-vision-0.10.35.tgz",
"integrity": "sha512-HOvadwVRE6JC+45nyYhmnywnr5h/J8KZvOeUNVOG9q/0875pZgItznFB9bRTvLc264YSJqiZ1NsIpCStJw/egg==",
"license": "Apache-2.0"
},
"node_modules/@nodelib/fs.scandir": { "node_modules/@nodelib/fs.scandir": {
"version": "2.1.5", "version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
-1
View File
@@ -13,7 +13,6 @@
"dependencies": { "dependencies": {
"@element-plus/icons-vue": "^2.3.1", "@element-plus/icons-vue": "^2.3.1",
"@highlightjs/vue-plugin": "^2.1.0", "@highlightjs/vue-plugin": "^2.1.0",
"@mediapipe/tasks-vision": "^0.10.35",
"@tencentcloud/call-uikit-vue": "^4.0.12", "@tencentcloud/call-uikit-vue": "^4.0.12",
"@tencentcloud/chat-uikit-vue3": "^4.5.4", "@tencentcloud/chat-uikit-vue3": "^4.5.4",
"@trtc/calls-uikit-vue": "^4.4.6", "@trtc/calls-uikit-vue": "^4.4.6",
Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+10
View File
@@ -227,6 +227,16 @@ export function revisitRateVisitOrderLines(params: {
return request.get({ url: '/stats.revisitRate/visitOrderLines', params }) return request.get({ url: '/stats.revisitRate/visitOrderLines', params })
} }
/** 待分配诊单自动指派日志列表(定时命令 tcm:auto-assign-pending 写入,含分配/未分配原因) */
export function autoAssignLogLists(params: Record<string, any>) {
return request.get({ url: '/stats.autoAssignLog/lists', params })
}
/** 批量回退自动分配:将诊单医助撤回到自动分配前的原医助 */
export function autoAssignLogRollback(data: { ids: number[] }) {
return request.post({ url: '/stats.autoAssignLog/rollback', data })
}
/** 医助个人业绩概览 */ /** 医助个人业绩概览 */
export function assistantPerformanceOverview(params: { export function assistantPerformanceOverview(params: {
time_type?: string time_type?: string
+31
View File
@@ -61,6 +61,14 @@ export function tcmDiagnosisDetail(params: any) {
return request.get({ url: '/tcm.diagnosis/detail', params }) return request.get({ url: '/tcm.diagnosis/detail', params })
} }
/** 设置复诊接诊率统计起始偏移(统计诊次=实单序号+偏移;1=二诊起,2=三诊起) */
export function tcmDiagnosisSetRevisitSlotStartOffset(params: {
id: number
revisit_slot_start_offset: number
}) {
return request.post({ url: '/tcm.diagnosis/setRevisitSlotStartOffset', params })
}
/** 诊单挂号 / 取消挂号 操作日志 */ /** 诊单挂号 / 取消挂号 操作日志 */
export function tcmDiagnosisGuahaoLogList(params: { id: number }) { export function tcmDiagnosisGuahaoLogList(params: { id: number }) {
return request.get({ url: '/tcm.diagnosis/guahaoLogList', params }) return request.get({ url: '/tcm.diagnosis/guahaoLogList', params })
@@ -425,6 +433,17 @@ export function prescriptionOrderPatchPrescriptionPatient(params: {
return request.post({ url: '/tcm.prescriptionOrder/patchPrescriptionPatient', params }) return request.post({ url: '/tcm.prescriptionOrder/patchPrescriptionPatient', params })
} }
export function prescriptionOrderPatchPrescriptionUsage(params: {
id: number
times_per_day: number
usage_days: number
medication_days: number
aux_times_per_day?: number
aux_usage_days?: number
}) {
return request.post({ url: '/tcm.prescriptionOrder/patchPrescriptionUsage', params })
}
export function prescriptionOrderAuditPrescription(params: { export function prescriptionOrderAuditPrescription(params: {
id: number id: number
action: 'approve' | 'reject' action: 'approve' | 'reject'
@@ -523,6 +542,18 @@ export function prescriptionOrderLogs(params: { id: number }) {
return request.get({ url: '/tcm.prescriptionOrder/logs', params }) return request.get({ url: '/tcm.prescriptionOrder/logs', params })
} }
/** 手工新增操作日志(可选调整处方/支付单审核状态) */
export function prescriptionOrderAddLog(params: {
id: number
summary: string
prescription_audit_status?: number | ''
payment_slip_audit_status?: number | ''
prescription_audit_remark?: string
payment_slip_audit_remark?: string
}) {
return request.post({ url: '/tcm.prescriptionOrder/addLog', params })
}
/** 修改订单金额 */ /** 修改订单金额 */
export function prescriptionOrderUpdateAmount(params: { id: number; amount: number }) { export function prescriptionOrderUpdateAmount(params: { id: number; amount: number }) {
return request.post({ url: '/tcm.prescriptionOrder/updateAmount', params }) return request.post({ url: '/tcm.prescriptionOrder/updateAmount', params })
@@ -1,812 +0,0 @@
<template>
<span class="beauty-trigger" @click="openPanel">
<slot name="reference">
<el-button type="primary" link size="small">
<el-icon><MagicStick /></el-icon>
<span class="beauty-btn-text">美颜</span>
</el-button>
</slot>
</span>
<Teleport to="body">
<div
v-if="visible"
class="beauty-window"
:style="windowStyle"
@mousedown.stop
>
<div class="beauty-window-header" @mousedown="onDragStart">
<span class="beauty-window-title">
<el-icon><MagicStick /></el-icon>
美颜设置
</span>
<el-button type="danger" link class="beauty-window-close" @mousedown.stop @click="closePanel">
<el-icon><Close /></el-icon>
</el-button>
</div>
<div class="beauty-window-body">
<!-- 左侧大预览 -->
<div class="beauty-preview-col">
<div ref="previewStageRef" class="beauty-preview-stage">
<video
v-show="previewing"
ref="previewVideoRef"
class="beauty-preview-video"
autoplay
muted
playsinline
></video>
<div v-if="!previewing" class="beauty-preview-placeholder">
<el-icon :size="36"><VideoCamera /></el-icon>
<span v-if="previewLoading">正在打开摄像头...</span>
<span v-else-if="previewError" class="beauty-preview-error-text">{{ previewError }}</span>
<span v-else>摄像头预览未开启</span>
</div>
<span v-if="previewing" class="beauty-preview-badge">对方看到的效果镜像显示</span>
<el-button
v-if="previewing"
class="beauty-fullscreen-btn"
circle
size="small"
title="全屏预览(按 Esc 退出)"
@click="toggleFullscreen"
>
<el-icon><FullScreen /></el-icon>
</el-button>
</div>
<div class="beauty-preview-toolbar">
<el-button
size="small"
:loading="previewLoading"
@click="togglePreview"
>
{{ previewing ? '关闭预览' : '打开预览' }}
</el-button>
<el-button
v-if="previewing"
size="small"
@click="toggleFullscreen"
>
<el-icon><FullScreen /></el-icon>
<span style="margin-left: 2px">全屏</span>
</el-button>
<span class="beauty-preview-note">预览效果即对方所见画面镜像显示</span>
</div>
</div>
<!-- 右侧调节项 -->
<div class="beauty-control-col">
<div class="beauty-section-header">
<span class="beauty-section-title">基础美颜</span>
<el-switch v-model="settings.enabled" size="small" />
</div>
<div class="beauty-section-body" :class="{ 'is-disabled': !settings.enabled }">
<div class="beauty-item">
<span class="beauty-label">磨皮算法</span>
<el-radio-group
v-model="settings.style"
size="small"
:disabled="!settings.enabled"
>
<el-radio-button :value="BEAUTY_STYLE_NATURE">自然</el-radio-button>
<el-radio-button :value="BEAUTY_STYLE_SMOOTH">光滑</el-radio-button>
</el-radio-group>
</div>
<div class="beauty-item">
<el-tooltip content="磨皮仅作用于人脸皮肤,眉眼/背景/衣物不受影响" placement="left">
<span class="beauty-label has-tip">磨皮</span>
</el-tooltip>
<el-slider
v-model="settings.beautyLevel"
:min="0"
:max="BEAUTY_LEVEL_MAX"
:step="1"
:disabled="!settings.enabled"
/>
</div>
<div class="beauty-item">
<span class="beauty-label">美白</span>
<el-slider
v-model="settings.whitenessLevel"
:min="0"
:max="BEAUTY_LEVEL_MAX"
:step="1"
:disabled="!settings.enabled"
/>
</div>
<div class="beauty-item">
<span class="beauty-label">红润</span>
<el-slider
v-model="settings.ruddinessLevel"
:min="0"
:max="BEAUTY_LEVEL_MAX"
:step="1"
:disabled="!settings.enabled"
/>
</div>
<div class="beauty-item">
<el-tooltip content="淡化脸上色斑 / 黑色素沉着,仅作用于人脸皮肤" placement="left">
<span class="beauty-label has-tip">祛斑</span>
</el-tooltip>
<el-slider
v-model="settings.spotLevel"
:min="0"
:max="BEAUTY_LEVEL_MAX"
:step="1"
:disabled="!settings.enabled"
/>
</div>
<div class="beauty-item">
<el-tooltip content="淡化脸上小面积深色痣点,仅作用于人脸皮肤" placement="left">
<span class="beauty-label has-tip">去痣</span>
</el-tooltip>
<el-slider
v-model="settings.moleLevel"
:min="0"
:max="BEAUTY_LEVEL_MAX"
:step="1"
:disabled="!settings.enabled"
/>
</div>
<div class="beauty-item">
<el-tooltip content="提亮眼下区域,减轻黑眼圈" placement="left">
<span class="beauty-label has-tip">去黑眼圈</span>
</el-tooltip>
<el-slider
v-model="settings.darkCircleLevel"
:min="0"
:max="BEAUTY_LEVEL_MAX"
:step="1"
:disabled="!settings.enabled"
/>
</div>
<div class="beauty-item">
<el-tooltip content="提升画面整体亮度,适合光线偏暗环境" placement="left">
<span class="beauty-label has-tip">亮度</span>
</el-tooltip>
<el-slider
v-model="settings.brightnessLevel"
:min="0"
:max="BEAUTY_LEVEL_MAX"
:step="1"
:disabled="!settings.enabled"
/>
</div>
<div class="beauty-item">
<el-tooltip content="锐化细节,让画面更清晰" placement="left">
<span class="beauty-label has-tip">清晰度</span>
</el-tooltip>
<el-slider
v-model="settings.clarityLevel"
:min="0"
:max="BEAUTY_LEVEL_MAX"
:step="1"
:disabled="!settings.enabled"
/>
</div>
</div>
<el-divider class="beauty-divider" />
<div class="beauty-section-header">
<span class="beauty-section-title">
高级美颜
<el-tooltip
placement="top"
content="瘦脸/口红/腮红由本机 AI 实时处理(免费,无需腾讯付费特效)。开关在下次开启摄像头时生效,通话中可关闭摄像头再打开。"
>
<el-icon class="beauty-help-icon"><QuestionFilled /></el-icon>
</el-tooltip>
</span>
<el-switch
v-model="settings.advanced.enabled"
size="small"
:disabled="!advancedSupported"
/>
</div>
<div v-if="!advancedSupported" class="beauty-unsupported">
当前浏览器不支持高级美颜需要 WebGL请使用新版 Chrome / Edge
</div>
<div
v-else
class="beauty-section-body"
:class="{ 'is-disabled': !settings.advanced.enabled }"
>
<div class="beauty-item">
<span class="beauty-label">瘦脸</span>
<el-slider
v-model="settings.advanced.slimStrength"
:min="0"
:max="100"
:step="1"
:disabled="!settings.advanced.enabled"
/>
</div>
<div class="beauty-item">
<span class="beauty-label">口红</span>
<el-slider
v-model="settings.advanced.lipstickStrength"
:min="0"
:max="100"
:step="1"
:disabled="!settings.advanced.enabled"
/>
<el-color-picker
v-model="settings.advanced.lipstickColor"
size="small"
:predefine="LIPSTICK_PRESETS"
:disabled="!settings.advanced.enabled"
/>
</div>
<div class="beauty-item">
<span class="beauty-label">腮红</span>
<el-slider
v-model="settings.advanced.blushStrength"
:min="0"
:max="100"
:step="1"
:disabled="!settings.advanced.enabled"
/>
<el-color-picker
v-model="settings.advanced.blushColor"
size="small"
:predefine="BLUSH_PRESETS"
:disabled="!settings.advanced.enabled"
/>
</div>
</div>
<div v-if="needCameraRestartHint" class="beauty-restart-hint">
通话中如果摄像头已按原始画面开启需关闭摄像头再打开才能生效
</div>
<div class="beauty-window-footer">
<span class="beauty-tip">全部为免费能力通话中实时生效</span>
<el-button link type="primary" size="small" @click="handleReset">
恢复默认
</el-button>
</div>
</div>
</div>
</div>
</Teleport>
</template>
<script setup lang="ts">
import { reactive, ref, watch, computed, nextTick, onBeforeUnmount } from 'vue'
import { MagicStick, QuestionFilled, Close, VideoCamera, FullScreen } from '@element-plus/icons-vue'
import {
BEAUTY_LEVEL_MAX,
BEAUTY_STYLE_NATURE,
BEAUTY_STYLE_SMOOTH,
applyBeautyToEngine,
defaultBeautySettings,
hasAnyBeautyEffect,
loadBeautySettings,
saveBeautySettings
} from '@/utils/call-beauty'
import {
getRawUserMedia,
installBeautyMediaInterceptor,
preloadBeautyModel
} from '@/utils/beauty/beauty-media-interceptor'
import {
FaceBeautyPipeline,
hasActiveBeautyPipeline,
isAdvancedBeautySupported,
updateActiveBeautyPipelines
} from '@/utils/beauty/face-beauty-pipeline'
const LIPSTICK_PRESETS = ['#c94f5e', '#b03a48', '#d96a76', '#a52a3c', '#e0788a', '#8f2436']
const BLUSH_PRESETS = ['#e88193', '#f0a3b0', '#dd6b7f', '#f2b8c0', '#e5949e']
const WINDOW_WIDTH = 780
const props = withDefaults(
defineProps<{
/** 获取当前 TUICallEngine 实例(未初始化时返回 null 即可) */
getEngine: () => any
/** 面板 z-index,需高于承载视频通话的浮窗 */
popperZIndex?: number
}>(),
{
popperZIndex: 300001
}
)
// 拦截器必须先于 SDK 申请摄像头安装;组件被通话相关页面引入时即安装
installBeautyMediaInterceptor()
const settings = reactive(loadBeautySettings())
const advancedSupported = isAdvancedBeautySupported()
// 面板挂载即预热模型,避免首次打开预览时同步加载 WASM/模型卡死
if (advancedSupported) preloadBeautyModel()
/** 美颜已开启但当前没有活跃通话管线 → 摄像头是在开启前用原始画面打开的 */
const needCameraRestartHint = computed(() => {
return hasAnyBeautyEffect(settings) && !hasActiveBeautyPipeline()
})
// ---------------------------------------------------------------------------
// 浮动窗口(可拖拽)
// ---------------------------------------------------------------------------
const visible = ref(false)
const windowPos = reactive({ x: 0, y: 0 })
const windowStyle = computed(() => ({
left: `${windowPos.x}px`,
top: `${windowPos.y}px`,
width: `${WINDOW_WIDTH}px`,
zIndex: props.popperZIndex
}))
const dragState = { active: false, offsetX: 0, offsetY: 0 }
const onDragMove = (e: MouseEvent) => {
if (!dragState.active) return
windowPos.x = Math.max(0, Math.min(window.innerWidth - 120, e.clientX - dragState.offsetX))
windowPos.y = Math.max(0, Math.min(window.innerHeight - 60, e.clientY - dragState.offsetY))
}
const onDragEnd = () => {
dragState.active = false
document.removeEventListener('mousemove', onDragMove)
document.removeEventListener('mouseup', onDragEnd)
}
const onDragStart = (e: MouseEvent) => {
dragState.active = true
dragState.offsetX = e.clientX - windowPos.x
dragState.offsetY = e.clientY - windowPos.y
document.addEventListener('mousemove', onDragMove)
document.addEventListener('mouseup', onDragEnd)
e.preventDefault()
}
const openPanel = async () => {
if (visible.value) return
windowPos.x = Math.max(16, Math.floor((window.innerWidth - WINDOW_WIDTH) / 2))
windowPos.y = Math.max(16, Math.floor(window.innerHeight * 0.12))
visible.value = true
// 先预热模型,并等面板先画出「正在打开摄像头」再启预览,避免首开卡死
if (advancedSupported) {
preloadBeautyModel()
await nextTick()
await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()))
void startPreview()
}
}
const closePanel = () => {
stopPreview()
visible.value = false
}
// ---------------------------------------------------------------------------
// 基础美颜:下发到 TUICallEngine
// ---------------------------------------------------------------------------
let applyTimer: ReturnType<typeof setTimeout> | null = null
/**
* 基础美颜下发到 TUICallEngineTRTC BasicBeauty 插件)。
* 仅作为 WebGL 不可用时的回退——正常情况下全部效果走本地管线。
* WebGL 可用时始终向引擎下发全 0,确保引擎侧插件保持关闭,
* 不会与本地管线叠加,也保证「关闭美颜」时引擎侧残留效果被清掉。
*/
const applyNow = async () => {
if (advancedSupported) {
await applyBeautyToEngine(props.getEngine(), { ...settings, enabled: false })
return
}
await applyBeautyToEngine(props.getEngine(), settings)
}
/** 通话建立后调用:仅在启用了美颜时下发,避免无意义地拉起插件 */
const applyIfEnabled = async () => {
if (!settings.enabled) return
await applyNow()
}
watch(
settings,
() => {
saveBeautySettings(settings)
// 全部美颜参数对活跃管线(含预览)实时生效
updateActiveBeautyPipelines({ ...settings, advanced: { ...settings.advanced } })
if (settings.advanced.enabled) preloadBeautyModel()
if (applyTimer) clearTimeout(applyTimer)
// 滑杆拖动会高频触发,防抖后再下发到引擎(仅 WebGL 回退场景实际生效)
applyTimer = setTimeout(() => {
applyTimer = null
void applyNow()
}, 200)
},
{ deep: true }
)
// ---------------------------------------------------------------------------
// 效果预览:本地摄像头 → 同一条美颜管线 → 大画面播放,不发起任何通话
// ---------------------------------------------------------------------------
const previewing = ref(false)
const previewLoading = ref(false)
const previewError = ref('')
const previewVideoRef = ref<HTMLVideoElement | null>(null)
const previewStageRef = ref<HTMLElement | null>(null)
let previewPipeline: FaceBeautyPipeline | null = null
const toggleFullscreen = () => {
if (document.fullscreenElement) {
void document.exitFullscreen().catch(() => {})
} else {
void previewStageRef.value?.requestFullscreen().catch(() => {})
}
}
const startPreview = async () => {
if (previewing.value || previewLoading.value) return
previewError.value = ''
previewLoading.value = true
try {
// 720p 采集保证预览清晰;管线内部会做适度降采样控制处理开销
const raw = await getRawUserMedia({
video: {
width: { ideal: 1280 },
height: { ideal: 720 },
frameRate: { ideal: 30 }
},
audio: false
})
const track = raw.getVideoTracks()[0]
if (!track) throw new Error('未获取到摄像头画面')
// 让出一帧,保证 loading 文案先渲染
await nextTick()
await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()))
previewPipeline = new FaceBeautyPipeline(
track,
{ ...settings, advanced: { ...settings.advanced } },
{ isPreview: true }
)
const processedTrack = await previewPipeline.start()
previewing.value = true
await nextTick()
if (previewVideoRef.value) {
previewVideoRef.value.srcObject = new MediaStream([processedTrack])
await previewVideoRef.value.play().catch(() => {})
}
} catch (err: any) {
console.warn('[call-beauty] 预览启动失败', err)
previewPipeline?.destroy()
previewPipeline = null
previewing.value = false
previewError.value =
err?.name === 'NotAllowedError'
? '摄像头权限被拒绝,请在浏览器地址栏允许摄像头访问'
: err?.name === 'NotReadableError'
? '摄像头被其他程序占用,无法打开预览'
: '预览启动失败,请检查摄像头设备'
} finally {
previewLoading.value = false
}
}
const stopPreview = () => {
if (document.fullscreenElement && document.fullscreenElement === previewStageRef.value) {
void document.exitFullscreen().catch(() => {})
}
if (previewVideoRef.value) previewVideoRef.value.srcObject = null
// destroy 会同时停掉输出轨和源摄像头轨,释放摄像头占用
previewPipeline?.destroy()
previewPipeline = null
previewing.value = false
previewLoading.value = false
}
const togglePreview = () => {
if (previewing.value) {
stopPreview()
} else {
void startPreview()
}
}
const handleReset = () => {
const defaults = defaultBeautySettings()
Object.assign(settings.advanced, defaults.advanced, { enabled: settings.advanced.enabled })
Object.assign(settings, {
style: defaults.style,
beautyLevel: defaults.beautyLevel,
whitenessLevel: defaults.whitenessLevel,
ruddinessLevel: defaults.ruddinessLevel,
spotLevel: defaults.spotLevel,
moleLevel: defaults.moleLevel,
darkCircleLevel: defaults.darkCircleLevel,
brightnessLevel: defaults.brightnessLevel,
clarityLevel: defaults.clarityLevel
})
}
onBeforeUnmount(() => {
if (applyTimer) clearTimeout(applyTimer)
stopPreview()
onDragEnd()
})
defineExpose({
applyNow,
applyIfEnabled,
openPanel
})
</script>
<style lang="scss" scoped>
.beauty-trigger {
display: inline-flex;
align-items: center;
}
.beauty-btn-text {
margin-left: 2px;
font-size: 12px;
}
</style>
<style lang="scss">
.beauty-window {
position: fixed;
max-width: calc(100vw - 24px);
max-height: calc(100vh - 24px);
background: #fff;
border-radius: 10px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.28);
overflow: hidden;
display: flex;
flex-direction: column;
.beauty-window-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 16px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
cursor: move;
user-select: none;
.beauty-window-title {
display: inline-flex;
align-items: center;
gap: 6px;
color: #fff;
font-size: 14px;
font-weight: 500;
}
.beauty-window-close {
color: #fff;
&:hover {
color: #ffd4d4;
}
}
}
.beauty-window-body {
display: flex;
gap: 16px;
padding: 16px;
flex: 1;
min-height: 0;
overflow: hidden;
@media (max-width: 820px) {
flex-direction: column;
overflow-y: auto;
}
}
.beauty-preview-col {
flex: 1 1 440px;
min-width: 0;
.beauty-preview-stage {
position: relative;
border-radius: 8px;
overflow: hidden;
background: #111;
aspect-ratio: 16 / 9;
.beauty-preview-video {
width: 100%;
height: 100%;
/* contain 完整显示画面,与实际发送给对方的内容一致,不裁切 */
object-fit: contain;
/* 本地自拍习惯镜像显示;实际发送给对方的画面不镜像 */
transform: scaleX(-1);
}
.beauty-preview-placeholder {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 10px;
color: #8a8f99;
font-size: 13px;
padding: 0 24px;
text-align: center;
.beauty-preview-error-text {
color: #f56c6c;
}
}
.beauty-preview-badge {
position: absolute;
left: 10px;
bottom: 10px;
padding: 3px 10px;
border-radius: 12px;
background: rgba(0, 0, 0, 0.55);
font-size: 12px;
color: #fff;
line-height: 1.6;
}
.beauty-fullscreen-btn {
position: absolute;
right: 10px;
top: 10px;
background: rgba(0, 0, 0, 0.45);
border-color: transparent;
color: #fff;
&:hover {
background: rgba(0, 0, 0, 0.65);
color: #fff;
}
}
/* 全屏时铺满屏幕 */
&:fullscreen {
aspect-ratio: auto;
width: 100vw;
height: 100vh;
}
}
.beauty-preview-toolbar {
display: flex;
align-items: center;
gap: 10px;
margin-top: 10px;
.beauty-preview-note {
font-size: 12px;
color: #909399;
line-height: 1.5;
}
}
}
.beauty-control-col {
flex: 0 0 300px;
min-width: 260px;
display: flex;
flex-direction: column;
min-height: 0;
overflow-y: auto;
padding-right: 6px;
scrollbar-width: thin;
}
.beauty-section-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
.beauty-section-title {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 14px;
font-weight: 500;
color: #303133;
}
.beauty-help-icon {
color: #a8abb2;
cursor: help;
font-size: 14px;
}
}
.beauty-divider {
margin: 10px 0;
}
.beauty-section-body {
&.is-disabled {
opacity: 0.75;
}
.beauty-item {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 4px;
.beauty-label {
flex-shrink: 0;
width: 64px;
font-size: 13px;
color: #606266;
&.has-tip {
cursor: help;
text-decoration: underline dotted #c0c4cc;
text-underline-offset: 3px;
}
}
.el-slider {
flex: 1;
margin-right: 8px;
}
.el-color-picker {
flex-shrink: 0;
}
&.beauty-item-hint {
margin-top: -2px;
margin-bottom: 6px;
.beauty-hint {
font-size: 12px;
color: #909399;
line-height: 1.4;
}
}
}
}
.beauty-unsupported {
padding: 4px 0 8px;
font-size: 12px;
color: #e6a23c;
line-height: 1.5;
}
.beauty-restart-hint {
margin-top: 4px;
padding: 4px 8px;
border-radius: 4px;
background: #fdf6ec;
font-size: 12px;
color: #e6a23c;
line-height: 1.5;
}
.beauty-window-footer {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: auto;
padding-top: 8px;
.beauty-tip {
font-size: 12px;
color: #909399;
}
}
}
</style>
+1 -33
View File
@@ -64,20 +64,6 @@
> >
群视频 群视频
</el-button> </el-button>
<!-- 美颜设置免费基础美颜 + 本地 AI 瘦脸/口红/腮红带大窗口实时预览仅对开启了美颜功能的医生显示 -->
<CallBeautyPanel
v-if="isCallReady && beautyAllowed"
ref="beautyPanelRef"
:get-engine="getCallEngine"
:popper-z-index="300002"
>
<template #reference>
<el-button size="small">
<el-icon><MagicStick /></el-icon>
<span style="margin-left: 2px">美颜</span>
</el-button>
</template>
</CallBeautyPanel>
</div> </div>
</template> </template>
</MessageInput> </MessageInput>
@@ -141,8 +127,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, nextTick, watch, computed, onMounted, onUnmounted } from 'vue' import { ref, nextTick, watch, computed, onMounted, onUnmounted } from 'vue'
import { Loading, Close, Rank, Camera, Fold, Expand, MagicStick } from '@element-plus/icons-vue' import { Loading, Close, Rank, Camera, Fold, Expand } from '@element-plus/icons-vue'
import CallBeautyPanel from '@/components/call-beauty-panel/index.vue'
import { uploadImageBlob, uploadVideoBlob } from '@/api/file' import { uploadImageBlob, uploadVideoBlob } from '@/api/file'
import { addDoctorNote } from '@/api/patient' import { addDoctorNote } from '@/api/patient'
import { import {
@@ -202,17 +187,6 @@ const patientUserId = ref('')
const conversationId = ref('') const conversationId = ref('')
const isCallReady = ref(false) const isCallReady = ref(false)
const showCallKitWindow = ref(false) // 仅在通话中显示视频窗口 const showCallKitWindow = ref(false) // 仅在通话中显示视频窗口
/** 美颜面板(本地 AI 管线 + 引擎兜底,非付费特效 SDK) */
const beautyPanelRef = ref<InstanceType<typeof CallBeautyPanel> | null>(null)
/** 医生编辑页「美颜功能」开关:关闭时不显示美颜入口 */
const beautyAllowed = computed(() => userStore.userInfo?.enable_beauty !== 0)
const getCallEngine = () => {
try {
return TUICallKitServer.getTUICallEngineInstance()
} catch {
return null
}
}
const { login, logout } = useLoginState() const { login, logout } = useLoginState()
const { setActiveConversation, createC2CConversation, activeConversation } = useConversationListState() const { setActiveConversation, createC2CConversation, activeConversation } = useConversationListState()
const userStore = useUserStore() const userStore = useUserStore()
@@ -749,8 +723,6 @@ function setupCallRoomBinding() {
} }
if (next === CALL_STATUS_CONNECTED) { if (next === CALL_STATUS_CONNECTED) {
void tryBindRoomIfReady() void tryBindRoomIfReady()
// 接通后重新下发已保存的美颜设置(摄像头此时已开启,插件可正常启动)
void beautyPanelRef.value?.applyIfEnabled()
clearLocalRecordingStartTimer() clearLocalRecordingStartTimer()
if (!isLochostVodEnabled.value) { if (!isLochostVodEnabled.value) {
previousCallStatus = next previousCallStatus = next
@@ -1119,10 +1091,6 @@ const open = async (data: { patientId: number; patientName: string; diagnosisId?
showCallKitWindow.value = true showCallKitWindow.value = true
pendingCallRecordStart = ensureCallRecordStarted() pendingCallRecordStart = ensureCallRecordStarted()
void pendingCallRecordStart void pendingCallRecordStart
// 呼叫阶段本地摄像头预览已开启,延迟下发美颜让医生在预览中即可看到效果
setTimeout(() => {
void beautyPanelRef.value?.applyIfEnabled()
}, 1500)
}, },
// 须先 endCallDeleteCloudRecording),再 await 本地上传;否则上传 WebM 耗时数分钟会拖住云端停录,控制台房间长期「尚未结束」 // 须先 endCallDeleteCloudRecording),再 await 本地上传;否则上传 WebM 耗时数分钟会拖住云端停录,控制台房间长期「尚未结束」
afterCalling: () => { afterCalling: () => {
+1 -35
View File
@@ -55,20 +55,6 @@
<!-- 窗口底部操作栏 --> <!-- 窗口底部操作栏 -->
<div class="window-footer"> <div class="window-footer">
<!-- 美颜设置本地 AI 管线免费仅对开启了美颜功能的医生显示 -->
<CallBeautyPanel
v-if="isInitialized && !callRejected && beautyAllowed"
ref="beautyPanelRef"
:get-engine="getCallEngine"
:popper-z-index="2100"
>
<template #reference>
<el-button size="small">
<el-icon><MagicStick /></el-icon>
<span style="margin-left: 2px">美颜</span>
</el-button>
</template>
</CallBeautyPanel>
<el-button <el-button
@click="handleClose" @click="handleClose"
:disabled="calling && !callRejected" :disabled="calling && !callRejected"
@@ -106,13 +92,11 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onUnmounted, nextTick, computed } from 'vue' import { ref, onUnmounted, nextTick, computed } from 'vue'
import { VideoCamera, CircleClose, Phone, MagicStick } from '@element-plus/icons-vue' import { VideoCamera, CircleClose, Phone } from '@element-plus/icons-vue'
import { getCallSignature, endCall } from '@/api/tcm' import { getCallSignature, endCall } from '@/api/tcm'
import feedback from '@/utils/feedback' import feedback from '@/utils/feedback'
import { formatTUICallUserError } from '@/utils/tuicall-error' import { formatTUICallUserError } from '@/utils/tuicall-error'
import { TUICallKitAPI, TUICallKit, TUICallType, STATUS } from '@trtc/calls-uikit-vue' import { TUICallKitAPI, TUICallKit, TUICallType, STATUS } from '@trtc/calls-uikit-vue'
import CallBeautyPanel from '@/components/call-beauty-panel/index.vue'
import useUserStore from '@/stores/modules/user'
interface CallInfo { interface CallInfo {
diagnosisId: number diagnosisId: number
@@ -140,18 +124,6 @@ const callInfo = ref<CallInfo>({
isGroup: false isGroup: false
}) })
/** 美颜面板(本地 AI 管线 + 引擎兜底,非付费特效 SDK) */
const beautyPanelRef = ref<InstanceType<typeof CallBeautyPanel> | null>(null)
/** 医生编辑页「美颜功能」开关:关闭时不显示美颜入口 */
const beautyAllowed = computed(() => useUserStore().userInfo?.enable_beauty !== 0)
const getCallEngine = () => {
try {
return TUICallKitAPI.getTUICallEngineInstance()
} catch {
return null
}
}
// 窗口位置和大小 // 窗口位置和大小
const windowRef = ref<HTMLElement>() const windowRef = ref<HTMLElement>()
const windowPosition = ref({ x: 100, y: 100 }) const windowPosition = ref({ x: 100, y: 100 })
@@ -261,8 +233,6 @@ const handleStatusChange = ({ oldStatus, newStatus }: any) => {
shouldAutoClose.value = false // 通话接通时,不自动关闭 shouldAutoClose.value = false // 通话接通时,不自动关闭
statusText.value = '通话中...' statusText.value = '通话中...'
feedback.msgSuccess('通话已接通') feedback.msgSuccess('通话已接通')
// 接通后重新下发已保存的美颜设置(摄像头此时已开启,插件可正常启动)
void beautyPanelRef.value?.applyIfEnabled()
// 如果是多人通话且还有其他用户需要邀请 // 如果是多人通话且还有其他用户需要邀请
if (callInfo.value.isGroup && callInfo.value.userIds && callInfo.value.userIds.length > 1) { if (callInfo.value.isGroup && callInfo.value.userIds && callInfo.value.userIds.length > 1) {
@@ -277,10 +247,6 @@ const handleStatusChange = ({ oldStatus, newStatus }: any) => {
calling.value = true calling.value = true
shouldAutoClose.value = false shouldAutoClose.value = false
statusText.value = '等待对方接听...' statusText.value = '等待对方接听...'
// 呼叫阶段本地摄像头预览已开启,延迟下发美颜让医生在预览中即可看到效果
setTimeout(() => {
void beautyPanelRef.value?.applyIfEnabled()
}, 1500)
} }
// 通话结束,回到空闲状态 // 通话结束,回到空闲状态
@@ -1,104 +0,0 @@
/**
* getUserMedia 拦截器:给通话视频流透明注入本地 AI 高级美颜
*
* TUICallKit / TRTC SDK 申请摄像头时走 navigator.mediaDevices.getUserMedia
* 这里在其外层包一层:拿到真实摄像头流后,若高级美颜已开启,则把视频轨送入
* FaceBeautyPipeline 处理,返回处理后的流。SDK 对此无感知,无需改动任何 SDK 代码。
*
* 仅处理"摄像头视频"请求;纯音频、屏幕共享(getDisplayMedia)不受影响。
* 管线创建失败时始终回退原始流,不会因美颜故障导致通话不可用。
*/
import { hasAnyBeautyEffect, loadBeautySettings } from '@/utils/call-beauty'
import useUserStore from '@/stores/modules/user'
import {
FaceBeautyPipeline,
getFaceLandmarker,
isAdvancedBeautySupported
} from './face-beauty-pipeline'
/**
* 当前登录医生是否被允许使用美颜(la_admin.enable_beauty,医生编辑页配置)。
* 字段缺失(旧数据/旧接口)时视为允许,与后端默认值保持一致。
*/
export function isBeautyAllowedForCurrentUser(): boolean {
try {
const userStore = useUserStore()
return userStore.userInfo?.enable_beauty !== 0
} catch {
return true
}
}
let installed = false
let originalGetUserMedia: ((constraints?: MediaStreamConstraints) => Promise<MediaStream>) | null =
null
/**
* 绕过拦截器直接获取原始摄像头流。
* 供美颜预览使用:预览自己管理管线,避免被拦截器再包一层造成双重处理。
*/
export function getRawUserMedia(constraints?: MediaStreamConstraints): Promise<MediaStream> {
if (originalGetUserMedia) return originalGetUserMedia(constraints)
return navigator.mediaDevices.getUserMedia(constraints)
}
async function wrapStreamWithBeauty(stream: MediaStream): Promise<MediaStream> {
const videoTrack = stream.getVideoTracks()[0]
if (!videoTrack) return stream
const settings = loadBeautySettings()
const pipeline = new FaceBeautyPipeline(videoTrack, settings)
const processedTrack = await pipeline.start()
// SDK 停用处理后的轨道时,同步销毁管线并释放真实摄像头
const originalStop = processedTrack.stop.bind(processedTrack)
processedTrack.stop = () => {
originalStop()
pipeline.destroy()
stream.getTracks().forEach((t) => t.stop())
}
return new MediaStream([processedTrack, ...stream.getAudioTracks()])
}
/**
* 安装拦截器(幂等)。需在 SDK 发起摄像头请求前调用——
* 目前由美颜面板组件挂载时触发,早于任何通话开始。
*/
export function installBeautyMediaInterceptor() {
if (installed) return
if (!navigator.mediaDevices?.getUserMedia) return
installed = true
const original = navigator.mediaDevices.getUserMedia.bind(navigator.mediaDevices)
originalGetUserMedia = original
navigator.mediaDevices.getUserMedia = async (constraints?: MediaStreamConstraints) => {
const stream = await original(constraints)
if (!constraints?.video) return stream
const settings = loadBeautySettings()
if (
!isBeautyAllowedForCurrentUser() ||
!hasAnyBeautyEffect(settings) ||
!isAdvancedBeautySupported()
) {
return stream
}
try {
return await wrapStreamWithBeauty(stream)
} catch (err) {
console.warn('[face-beauty] 美颜管线创建失败,使用原始摄像头画面', err)
return stream
}
}
}
/** 高级美颜开启时提前预热模型(约 4MB,加载一次全局复用),避免首帧卡顿 */
export function preloadBeautyModel() {
if (!isAdvancedBeautySupported()) return
getFaceLandmarker().catch(() => {
// 预热失败不打扰用户,实际使用时还会重试并有兜底
})
}
File diff suppressed because it is too large Load Diff
-191
View File
@@ -1,191 +0,0 @@
/**
* 视频通话美颜设置
*
* 全部效果(磨皮/美白/红润/祛斑/去痣/去黑眼圈/亮度/清晰度/瘦脸/口红/腮红)默认由本地管线处理:
* MediaPipe Face Landmarker(开源免费)识别人脸关键点 + WebGL/Canvas 逐帧渲染,
* 经 getUserMedia 拦截注入通话视频流(见 utils/beauty/ 目录),预览所见即所得。
*
* 仅当浏览器不支持 WebGL 时,磨皮/美白/红润回退到腾讯云 TRTC 免费 BasicBeauty
* 插件(TUICallEngine.setBeautyLevel)。不依赖付费的腾讯特效 SDK。
*/
/** 高级美颜(本地 AI 处理)设置 */
export interface AdvancedBeautySettings {
/** 总开关;通话中途开启需重新开关摄像头才能接管视频流 */
enabled: boolean
/** 瘦脸强度 0-100 */
slimStrength: number
/** 口红浓度 0-1000 为关闭) */
lipstickStrength: number
/** 口红颜色(hex */
lipstickColor: string
/** 腮红浓度 0-1000 为关闭) */
blushStrength: number
/** 腮红颜色(hex */
blushColor: string
}
export interface CallBeautySettings {
/** 是否启用基础美颜(关闭时以全 0 参数下发,引擎会停用 BasicBeauty 插件) */
enabled: boolean
/** 磨皮算法:0 光滑(TRTCBeautyStyleSmooth/ 1 自然(TRTCBeautyStyleNature */
style: number
/** 磨皮级别 0-9 */
beautyLevel: number
/** 美白级别 0-9 */
whitenessLevel: number
/** 红润级别 0-9 */
ruddinessLevel: number
/** 祛斑 / 淡化黑色素 0-9(仅人脸,压暗斑点向周围肤色靠拢) */
spotLevel: number
/** 去痣 0-9(仅人脸,强力淡化小面积深色痣点) */
moleLevel: number
/** 去黑眼圈 0-9(仅眼下区域提亮淡化) */
darkCircleLevel: number
/** 摄像头亮度 0-9(整帧提亮,0 为原亮度) */
brightnessLevel: number
/** 清晰度 0-9(锐化细节,0 为关闭) */
clarityLevel: number
/** 高级美颜(瘦脸/口红/腮红,本地 AI) */
advanced: AdvancedBeautySettings
}
export const BEAUTY_STYLE_SMOOTH = 0
export const BEAUTY_STYLE_NATURE = 1
export const BEAUTY_LEVEL_MAX = 9
const STORAGE_KEY = 'tcm_call_beauty_settings'
export const defaultAdvancedBeautySettings = (): AdvancedBeautySettings => ({
enabled: false,
slimStrength: 30,
lipstickStrength: 0,
lipstickColor: '#c94f5e',
blushStrength: 0,
blushColor: '#e88193'
})
export const defaultBeautySettings = (): CallBeautySettings => ({
enabled: false,
style: BEAUTY_STYLE_NATURE,
beautyLevel: 5,
whitenessLevel: 3,
ruddinessLevel: 2,
spotLevel: 0,
moleLevel: 0,
darkCircleLevel: 0,
brightnessLevel: 0,
clarityLevel: 0,
advanced: defaultAdvancedBeautySettings()
})
const clampLevel = (value: unknown): number => {
const num = Math.round(Number(value))
if (!Number.isFinite(num)) return 0
return Math.min(BEAUTY_LEVEL_MAX, Math.max(0, num))
}
const clampPercent = (value: unknown, fallback: number): number => {
const num = Math.round(Number(value))
if (!Number.isFinite(num)) return fallback
return Math.min(100, Math.max(0, num))
}
const normalizeHexColor = (value: unknown, fallback: string): string => {
if (typeof value === 'string' && /^#[0-9a-fA-F]{6}$/.test(value)) return value
return fallback
}
/** 高级美颜是否有任一效果实际开启 */
export function hasAnyAdvancedEffect(advanced: AdvancedBeautySettings): boolean {
return (
advanced.enabled &&
(advanced.slimStrength > 0 || advanced.lipstickStrength > 0 || advanced.blushStrength > 0)
)
}
/** 全部美颜(基础 + 高级)是否有任一效果实际开启(决定是否接管摄像头流) */
export function hasAnyBeautyEffect(settings: CallBeautySettings): boolean {
const basicOn =
settings.enabled &&
(settings.beautyLevel > 0 ||
settings.whitenessLevel > 0 ||
settings.ruddinessLevel > 0 ||
settings.spotLevel > 0 ||
settings.moleLevel > 0 ||
settings.darkCircleLevel > 0 ||
settings.brightnessLevel > 0 ||
settings.clarityLevel > 0)
return basicOn || hasAnyAdvancedEffect(settings.advanced)
}
export function loadBeautySettings(): CallBeautySettings {
const defaults = defaultBeautySettings()
try {
const raw = localStorage.getItem(STORAGE_KEY)
if (!raw) return defaults
const parsed = JSON.parse(raw)
const advDefaults = defaults.advanced
const adv = parsed?.advanced ?? {}
return {
enabled: Boolean(parsed?.enabled),
style:
parsed?.style === BEAUTY_STYLE_SMOOTH
? BEAUTY_STYLE_SMOOTH
: BEAUTY_STYLE_NATURE,
beautyLevel: clampLevel(parsed?.beautyLevel ?? defaults.beautyLevel),
whitenessLevel: clampLevel(parsed?.whitenessLevel ?? defaults.whitenessLevel),
ruddinessLevel: clampLevel(parsed?.ruddinessLevel ?? defaults.ruddinessLevel),
spotLevel: clampLevel(parsed?.spotLevel ?? defaults.spotLevel),
moleLevel: clampLevel(parsed?.moleLevel ?? defaults.moleLevel),
darkCircleLevel: clampLevel(parsed?.darkCircleLevel ?? defaults.darkCircleLevel),
brightnessLevel: clampLevel(parsed?.brightnessLevel ?? defaults.brightnessLevel),
clarityLevel: clampLevel(parsed?.clarityLevel ?? defaults.clarityLevel),
advanced: {
enabled: Boolean(adv?.enabled),
slimStrength: clampPercent(adv?.slimStrength, advDefaults.slimStrength),
lipstickStrength: clampPercent(adv?.lipstickStrength, advDefaults.lipstickStrength),
lipstickColor: normalizeHexColor(adv?.lipstickColor, advDefaults.lipstickColor),
blushStrength: clampPercent(adv?.blushStrength, advDefaults.blushStrength),
blushColor: normalizeHexColor(adv?.blushColor, advDefaults.blushColor)
}
}
} catch {
return defaults
}
}
export function saveBeautySettings(settings: CallBeautySettings) {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(settings))
} catch {
// localStorage 不可用时静默忽略,仅本次会话生效
}
}
/**
* 将美颜设置应用到 TUICallEngine 实例。
* 引擎内部对 setBeautyLevel 自带 try-catch(摄像头未开启等场景不会抛错),
* 这里只兜底引擎不存在 / 接口缺失的情况。
*/
export async function applyBeautyToEngine(
engine: any,
settings: CallBeautySettings
): Promise<boolean> {
if (!engine || typeof engine.setBeautyLevel !== 'function') {
return false
}
const enabled = settings.enabled
try {
await engine.setBeautyLevel({
style: settings.style === BEAUTY_STYLE_SMOOTH ? BEAUTY_STYLE_SMOOTH : BEAUTY_STYLE_NATURE,
beautyLevel: enabled ? clampLevel(settings.beautyLevel) : 0,
whitenessLevel: enabled ? clampLevel(settings.whitenessLevel) : 0,
ruddinessLevel: enabled ? clampLevel(settings.ruddinessLevel) : 0
})
return true
} catch (error) {
console.warn('[call-beauty] 应用美颜设置失败', error)
return false
}
}
-15
View File
@@ -146,19 +146,6 @@
<span class="ml-2 text-gray-500">{{ formData.enable_charge === 1 ? '已开启' : '已关闭' }}</span> <span class="ml-2 text-gray-500">{{ formData.enable_charge === 1 ? '已开启' : '已关闭' }}</span>
</el-form-item> </el-form-item>
<!-- 美颜功能 -->
<el-form-item label="美颜功能">
<div>
<el-switch
v-model="formData.enable_beauty"
:active-value="1"
:inactive-value="0"
/>
<span class="ml-2 text-gray-500">{{ formData.enable_beauty === 1 ? '已开启' : '已关闭' }}</span>
<div class="form-tips">开启后该医生视频通话时可配置美颜磨皮/美白/瘦脸/口红等关闭则不显示美颜入口</div>
</div>
</el-form-item>
<!-- 所属科室 --> <!-- 所属科室 -->
<el-form-item label="所属科室" prop="department"> <el-form-item label="所属科室" prop="department">
<el-select v-model="formData.department" placeholder="请选择所属科室" clearable class="flex-1"> <el-select v-model="formData.department" placeholder="请选择所属科室" clearable class="flex-1">
@@ -308,7 +295,6 @@ const formData = reactive({
enable_image_consult: 1, // 是否开启图文问诊 enable_image_consult: 1, // 是否开启图文问诊
enable_video_consult: 1, // 是否开启视频问诊 enable_video_consult: 1, // 是否开启视频问诊
enable_charge: 0, // 是否开启收费 enable_charge: 0, // 是否开启收费
enable_beauty: 1, // 是否开启美颜功能
department: '', // 所属科室 department: '', // 所属科室
specialty: '', // 擅长领域 specialty: '', // 擅长领域
education: '', // 教育背景 education: '', // 教育背景
@@ -450,7 +436,6 @@ const open = (type = 'add') => {
enable_image_consult: 1, enable_image_consult: 1,
enable_video_consult: 1, enable_video_consult: 1,
enable_charge: 0, enable_charge: 0,
enable_beauty: 1,
department: '', department: '',
specialty: '', specialty: '',
education: '', education: '',
@@ -327,7 +327,27 @@
</div> </div>
</div> </div>
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="服用方式"> <el-descriptions-item>
<template #label>
<div class="flex items-center gap-1">
<span>服用方式</span>
<el-button
v-if="
!readonly &&
detailData.prescription_id &&
detailPrescription &&
!String(detailData.prescription_detail_error || '').trim()
"
v-perms="['tcm.prescriptionOrder/patchPrescriptionUsage']"
type="primary"
size="small"
link
@click="openPatchUsageDialog"
>
修改
</el-button>
</div>
</template>
<div class="flex flex-col gap-1.5 text-sm leading-relaxed"> <div class="flex flex-col gap-1.5 text-sm leading-relaxed">
<template v-if="detailHasAuxHerbs"> <template v-if="detailHasAuxHerbs">
<div> <div>
@@ -631,7 +651,7 @@
<el-descriptions-item label="上次医护">{{ detailData.prev_staff || '—' }}</el-descriptions-item> <el-descriptions-item label="上次医护">{{ detailData.prev_staff || '—' }}</el-descriptions-item>
<el-descriptions-item label="服务渠道">{{ detailData.service_channel || '—' }}</el-descriptions-item> <el-descriptions-item label="服务渠道">{{ detailData.service_channel || '—' }}</el-descriptions-item>
<el-descriptions-item label="服务套餐">{{ formatServicePackage(detailData.service_package) }}</el-descriptions-item> <el-descriptions-item label="服务套餐">{{ detailServicePackageText }}</el-descriptions-item>
<el-descriptions-item label="费用类别">{{ feeTypeText(detailData.fee_type) }}</el-descriptions-item> <el-descriptions-item label="费用类别">{{ feeTypeText(detailData.fee_type) }}</el-descriptions-item>
<el-descriptions-item v-if="showInternalCost" label="内部成本"> <el-descriptions-item v-if="showInternalCost" label="内部成本">
@@ -780,7 +800,18 @@
class="po-panel border-gray-100 mt-4" class="po-panel border-gray-100 mt-4"
> >
<template #header> <template #header>
<span class="font-medium text-[15px]">操作日志</span> <div class="flex items-center justify-between gap-2">
<span class="font-medium text-[15px]">操作日志</span>
<el-button
v-if="canAddPrescriptionOrderLog()"
type="primary"
link
size="small"
@click="openAddLogDialog"
>
新增日志
</el-button>
</div>
</template> </template>
<el-timeline v-if="detailLogs.length" class="mt-2 pl-2"> <el-timeline v-if="detailLogs.length" class="mt-2 pl-2">
<el-timeline-item <el-timeline-item
@@ -800,19 +831,182 @@
</el-timeline> </el-timeline>
<el-empty v-else description="暂无操作日志" :image-size="64" /> <el-empty v-else description="暂无操作日志" :image-size="64" />
</el-card> </el-card>
<!-- 新增操作日志 -->
<el-dialog
v-model="addLogVisible"
title="新增操作日志"
width="520px"
:close-on-click-modal="false"
destroy-on-close
append-to-body
@closed="resetAddLogForm"
>
<el-form ref="addLogFormRef" :model="addLogForm" :rules="addLogRules" label-width="108px">
<el-form-item label="日志内容" prop="summary">
<el-input
v-model="addLogForm.summary"
type="textarea"
:rows="4"
maxlength="500"
show-word-limit
placeholder="记录本次操作说明、沟通结果等"
/>
</el-form-item>
<el-form-item v-if="canSetRxAuditOnAddLog" label="处方审核">
<el-select v-model="addLogForm.prescription_audit_status" class="w-full" clearable placeholder="不修改">
<el-option label="待审核" :value="0" />
<el-option label="已通过" :value="1" />
<el-option label="已驳回" :value="2" />
</el-select>
<div v-if="detailData" class="text-xs text-gray-400 mt-1">
当前:{{ auditStatusText(detailData.prescription_audit_status) }}
</div>
</el-form-item>
<el-form-item
v-if="canSetRxAuditOnAddLog && addLogForm.prescription_audit_status !== '' && addLogForm.prescription_audit_status !== null && addLogForm.prescription_audit_status !== undefined"
label="处方审核意见"
>
<el-input
v-model="addLogForm.prescription_audit_remark"
type="textarea"
:rows="2"
maxlength="500"
show-word-limit
placeholder="选填"
/>
</el-form-item>
<el-form-item v-if="canSetPayAuditOnAddLog" label="支付单审核">
<el-select v-model="addLogForm.payment_slip_audit_status" class="w-full" clearable placeholder="不修改">
<el-option label="待审核" :value="0" />
<el-option label="已通过" :value="1" />
<el-option label="已驳回" :value="2" />
</el-select>
<div v-if="detailData" class="text-xs text-gray-400 mt-1">
当前:{{ auditStatusText(detailData.payment_slip_audit_status) }}
</div>
</el-form-item>
<el-form-item
v-if="canSetPayAuditOnAddLog && addLogForm.payment_slip_audit_status !== '' && addLogForm.payment_slip_audit_status !== null && addLogForm.payment_slip_audit_status !== undefined"
label="支付审核意见"
>
<el-input
v-model="addLogForm.payment_slip_audit_remark"
type="textarea"
:rows="2"
maxlength="500"
show-word-limit
placeholder="选填"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="addLogVisible = false">取消</el-button>
<el-button type="primary" :loading="addLogSaving" @click="submitAddLog">保存</el-button>
</template>
</el-dialog>
<!-- 修改服用参数:主方 / 辅方 / 订单设置 -->
<el-dialog
v-model="patchUsageVisible"
title="修改服用参数"
width="480px"
:close-on-click-modal="false"
destroy-on-close
@closed="resetPatchUsageForm"
>
<el-form
ref="patchUsageFormRef"
:model="patchUsageForm"
:rules="patchUsageRules"
label-width="108px"
>
<div v-if="detailHasAuxHerbs" class="text-xs font-medium text-gray-500 mb-3">主方</div>
<el-form-item label="每天次数" prop="times_per_day">
<el-input-number
v-model="patchUsageForm.times_per_day"
:min="1"
:max="6"
:step="1"
controls-position="right"
class="w-full"
/>
</el-form-item>
<el-form-item label="处方开立" prop="usage_days">
<div class="flex items-center gap-1 w-full">
<el-input-number
v-model="patchUsageForm.usage_days"
:min="1"
:max="999"
:step="1"
controls-position="right"
class="flex-1 min-w-0"
/>
<span class="text-gray-500 shrink-0">天</span>
</div>
</el-form-item>
<template v-if="detailHasAuxHerbs">
<div class="text-xs font-medium text-gray-500 mb-3 mt-2 pt-2 border-t border-gray-100">辅方</div>
<el-form-item label="每天次数" prop="aux_times_per_day">
<el-input-number
v-model="patchUsageForm.aux_times_per_day"
:min="1"
:max="6"
:step="1"
controls-position="right"
class="w-full"
/>
</el-form-item>
<el-form-item label="处方开立" prop="aux_usage_days">
<div class="flex items-center gap-1 w-full">
<el-input-number
v-model="patchUsageForm.aux_usage_days"
:min="1"
:max="999"
:step="1"
controls-position="right"
class="flex-1 min-w-0"
/>
<span class="text-gray-500 shrink-0">天</span>
</div>
</el-form-item>
</template>
<div class="text-xs font-medium text-gray-500 mb-3 mt-2 pt-2 border-t border-gray-100">订单设置</div>
<el-form-item label="服用天数" prop="medication_days">
<div class="flex items-center gap-1 w-full">
<el-input-number
v-model="patchUsageForm.medication_days"
:min="1"
:max="999"
:step="1"
controls-position="right"
class="flex-1 min-w-0"
/>
<span class="text-gray-500 shrink-0">天</span>
</div>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="patchUsageVisible = false">取消</el-button>
<el-button type="primary" :loading="patchUsageSaving" @click="submitPatchUsage">保存</el-button>
</template>
</el-dialog>
</div> </div>
</el-drawer> </el-drawer>
</template> </template>
<script lang="ts" setup name="PrescriptionOrderDetailDrawer"> <script lang="ts" setup name="PrescriptionOrderDetailDrawer">
import { computed, onMounted, ref } from 'vue' import { computed, nextTick, onMounted, reactive, ref } from 'vue'
import type { FormInstance, FormRules } from 'element-plus'
import { Refresh, Loading, Search, Van } from '@element-plus/icons-vue' import { Refresh, Loading, Search, Van } from '@element-plus/icons-vue'
import { import {
prescriptionOrderDetail, prescriptionOrderDetail,
prescriptionOrderLogs, prescriptionOrderLogs,
prescriptionOrderAddLog,
prescriptionOrderLogisticsTrace, prescriptionOrderLogisticsTrace,
prescriptionOrderLogisticsJdUpdate, prescriptionOrderLogisticsJdUpdate,
prescriptionOrderPaidPayOrders prescriptionOrderPaidPayOrders,
prescriptionOrderPatchPrescriptionUsage
} from '@/api/tcm' } from '@/api/tcm'
import { getDictData } from '@/api/app' import { getDictData } from '@/api/app'
import feedback from '@/utils/feedback' import feedback from '@/utils/feedback'
@@ -831,6 +1025,7 @@ import {
consumerRxAuditTag, consumerRxAuditTag,
expressCompanyLabel, expressCompanyLabel,
logActionText, logActionText,
auditStatusText,
formatPayOrderSource, formatPayOrderSource,
normalizeBizPhone, normalizeBizPhone,
recipientVsPrescriptionPhoneMismatch, recipientVsPrescriptionPhoneMismatch,
@@ -841,7 +1036,10 @@ import {
analyzeLogisticsPayloadUrgent, analyzeLogisticsPayloadUrgent,
parseLogisticsTracePayload, parseLogisticsTracePayload,
canUpdateAmount, canUpdateAmount,
formatDietaryTaboo formatDietaryTaboo,
type ServicePackageOption,
normalizeServicePackageOptions,
formatServicePackageLabels
} from './prescription-order-utils' } from './prescription-order-utils'
const props = withDefaults( const props = withDefaults(
@@ -873,6 +1071,7 @@ const emit = defineEmits<{
(e: 'view-prescription'): void (e: 'view-prescription'): void
(e: 'test-gancao-preview'): void (e: 'test-gancao-preview'): void
(e: 'view-patient'): void (e: 'view-patient'): void
(e: 'detail-changed'): void
}>() }>()
const userStore = useUserStore() const userStore = useUserStore()
@@ -1088,36 +1287,24 @@ const detailFullAddress = computed(() => {
}) })
// ─── 服务套餐字典 ─── // ─── 服务套餐字典 ───
const servicePackageOptions = ref<Array<{ name: string; value: string }>>([]) const servicePackageOptions = ref<ServicePackageOption[]>([])
async function loadServicePackageOptions() { async function loadServicePackageOptions() {
if (servicePackageOptions.value.length > 0) return
try { try {
const data: any = await getDictData({ type: 'server_order' }) const data: any = await getDictData({ type: 'server_order' })
servicePackageOptions.value = (data?.server_order || []).filter((item: any) => item.status !== 0) const opts = normalizeServicePackageOptions(data?.server_order)
if (opts.length > 0) {
servicePackageOptions.value = opts
}
} catch { } catch {
servicePackageOptions.value = [] /* 请求被同参数请求取消或失败时保留现值,open() 时会重试 */
} }
} }
function formatServicePackage(value: any): string { const detailServicePackageText = computed(() =>
if (!value) return '—' formatServicePackageLabels(detailData.value?.service_package, servicePackageOptions.value)
)
let packages: string[] = []
if (Array.isArray(value)) {
packages = value
} else if (typeof value === 'string') {
packages = value.split(',').filter((v) => v.trim() !== '')
}
if (packages.length === 0) return '—'
const names = packages.map((val) => {
const option = servicePackageOptions.value.find((opt) => opt.value === val)
return option ? option.name : val
})
return names.join('、')
}
onMounted(() => { onMounted(() => {
loadServicePackageOptions() loadServicePackageOptions()
@@ -1142,6 +1329,196 @@ async function fetchLogs(id: number) {
} }
} }
function hasPerm(perm: string) {
const p = userStore.perms || []
return p.includes('*') || p.includes(perm)
}
function canAddPrescriptionOrderLog() {
return hasPerm('tcm.prescriptionOrder/addLog')
}
/** 与后端 canAuditPrescriptionOrder 同档:超管或 prescription_audit_roles */
const canSetRxAuditOnAddLog = computed(() => {
const u = userStore.userInfo
if (!u || Number(u.root) === 1) return true
const ids = Array.isArray(u.role_ids) ? u.role_ids.map((n: unknown) => Number(n)) : []
return ids.some((id) => PRESCRIPTION_AUDIT_ROLE_IDS.includes(id))
})
/** 与后端 canAuditPaymentSlipOrder 同档:超管或 prescription_order_payment_audit_roles 默认 0,3 */
const canSetPayAuditOnAddLog = computed(() => {
const u = userStore.userInfo
if (!u || Number(u.root) === 1) return true
const ids = Array.isArray(u.role_ids) ? u.role_ids.map((n: unknown) => Number(n)) : []
return ids.some((id) => [0, 3].includes(id))
})
const addLogVisible = ref(false)
const addLogSaving = ref(false)
const addLogFormRef = ref<FormInstance>()
const addLogForm = reactive({
summary: '',
prescription_audit_status: '' as number | '',
payment_slip_audit_status: '' as number | '',
prescription_audit_remark: '',
payment_slip_audit_remark: ''
})
const addLogRules: FormRules = {
summary: [{ required: true, message: '请填写日志内容', trigger: 'blur' }]
}
const patchUsageVisible = ref(false)
const patchUsageSaving = ref(false)
const patchUsageFormRef = ref<FormInstance>()
const patchUsageForm = reactive({
times_per_day: 3 as number | undefined,
usage_days: 7 as number | undefined,
aux_times_per_day: 3 as number | undefined,
aux_usage_days: 7 as number | undefined,
medication_days: undefined as number | undefined
})
const patchUsageRules = computed<FormRules>(() => {
const rules: FormRules = {
times_per_day: [{ required: true, message: '请填写主方每天次数', trigger: 'change' }],
usage_days: [{ required: true, message: '请填写主方开立天数', trigger: 'change' }],
medication_days: [{ required: true, message: '请填写订单服用天数', trigger: 'change' }]
}
if (detailHasAuxHerbs.value) {
rules.aux_times_per_day = [{ required: true, message: '请填写辅方每天次数', trigger: 'change' }]
rules.aux_usage_days = [{ required: true, message: '请填写辅方开立天数', trigger: 'change' }]
}
return rules
})
function openPatchUsageDialog() {
const rx = detailPrescription.value
const ord = detailData.value
if (!rx || !ord?.id || !ord.prescription_id) {
feedback.msgWarning('无处方数据')
return
}
const aux = detailAuxUsage.value
patchUsageForm.times_per_day =
Number(rx.times_per_day) > 0 ? Number(rx.times_per_day) : 3
patchUsageForm.usage_days =
Number(rx.usage_days) > 0 ? Number(rx.usage_days) : 7
patchUsageForm.aux_times_per_day =
aux && Number(aux.times_per_day) > 0 ? Number(aux.times_per_day) : 3
patchUsageForm.aux_usage_days =
aux && Number(aux.usage_days) > 0 ? Number(aux.usage_days) : 7
const md = Number(ord.medication_days)
patchUsageForm.medication_days = md > 0 ? md : Number(rx.usage_days) > 0 ? Number(rx.usage_days) : 7
patchUsageVisible.value = true
nextTick(() => patchUsageFormRef.value?.clearValidate())
}
function resetPatchUsageForm() {
patchUsageForm.times_per_day = 3
patchUsageForm.usage_days = 7
patchUsageForm.aux_times_per_day = 3
patchUsageForm.aux_usage_days = 7
patchUsageForm.medication_days = undefined
}
async function submitPatchUsage() {
const form = patchUsageFormRef.value
if (!form) return
try {
await form.validate()
} catch {
return
}
const ordId = detailData.value?.id
if (!ordId) return
patchUsageSaving.value = true
try {
const payload: {
id: number
times_per_day: number
usage_days: number
medication_days: number
aux_times_per_day?: number
aux_usage_days?: number
} = {
id: ordId,
times_per_day: Number(patchUsageForm.times_per_day),
usage_days: Number(patchUsageForm.usage_days),
medication_days: Number(patchUsageForm.medication_days)
}
if (detailHasAuxHerbs.value) {
payload.aux_times_per_day = Number(patchUsageForm.aux_times_per_day)
payload.aux_usage_days = Number(patchUsageForm.aux_usage_days)
}
await prescriptionOrderPatchPrescriptionUsage(payload)
feedback.msgSuccess('保存成功')
patchUsageVisible.value = false
await refresh()
emit('detail-changed')
} catch {
/* 拦截器已提示 */
} finally {
patchUsageSaving.value = false
}
}
function resetAddLogForm() {
addLogForm.summary = ''
addLogForm.prescription_audit_status = ''
addLogForm.payment_slip_audit_status = ''
addLogForm.prescription_audit_remark = ''
addLogForm.payment_slip_audit_remark = ''
addLogFormRef.value?.clearValidate()
}
function openAddLogDialog() {
if (!detailData.value?.id) return
resetAddLogForm()
const d = detailData.value
addLogForm.prescription_audit_remark = String(d.prescription_audit_remark || '')
addLogForm.payment_slip_audit_remark = String(d.payment_slip_audit_remark || '')
addLogVisible.value = true
}
async function submitAddLog() {
if (!addLogFormRef.value || !detailData.value?.id) return
await addLogFormRef.value.validate()
addLogSaving.value = true
try {
const payload: Record<string, unknown> = {
id: detailData.value.id,
summary: addLogForm.summary.trim()
}
if (
canSetRxAuditOnAddLog.value &&
addLogForm.prescription_audit_status !== '' &&
addLogForm.prescription_audit_status !== null &&
addLogForm.prescription_audit_status !== undefined
) {
payload.prescription_audit_status = addLogForm.prescription_audit_status
payload.prescription_audit_remark = addLogForm.prescription_audit_remark
}
if (
canSetPayAuditOnAddLog.value &&
addLogForm.payment_slip_audit_status !== '' &&
addLogForm.payment_slip_audit_status !== null &&
addLogForm.payment_slip_audit_status !== undefined
) {
payload.payment_slip_audit_status = addLogForm.payment_slip_audit_status
payload.payment_slip_audit_remark = addLogForm.payment_slip_audit_remark
}
await prescriptionOrderAddLog(payload as any)
feedback.msgSuccess('日志已添加')
addLogVisible.value = false
await refresh()
emit('detail-changed')
} catch {
/* 拦截器已提示 */
} finally {
addLogSaving.value = false
}
}
// ─── 未关联支付单 ─── // ─── 未关联支付单 ───
async function loadDetailUnlinkedPayOrders(diagnosisId: number, prescriptionOrderId: number, linkedIds: number[]) { async function loadDetailUnlinkedPayOrders(diagnosisId: number, prescriptionOrderId: number, linkedIds: number[]) {
if (!diagnosisId) { if (!diagnosisId) {
@@ -1266,6 +1643,8 @@ async function updateJdLogistics() {
// ─── 打开 / 刷新 ─── // ─── 打开 / 刷新 ───
async function open(id: number) { async function open(id: number) {
// 页面级同参数字典请求会取消抽屉挂载时的那次(axios 去重取消),打开时兜底重试
void loadServicePackageOptions()
// 显式彻底清空缓存,防止前一次弹窗的数据残留 // 显式彻底清空缓存,防止前一次弹窗的数据残留
detailData.value = null detailData.value = null
detailUnlinkedPayOrders.value = [] detailUnlinkedPayOrders.value = []
@@ -135,13 +135,25 @@ export function logActionText(act: string) {
revoke_pay_audit: '撤回支付审核', revoke_pay_audit: '撤回支付审核',
gancao_submit: '甘草下单', gancao_submit: '甘草下单',
patch_rx_patient: '处方患者信息', patch_rx_patient: '处方患者信息',
patch_rx_usage: '服用参数',
update_amount: '修改订单金额', update_amount: '修改订单金额',
complete: '完成订单', complete: '完成订单',
refund: '退款' refund: '退款',
manual_log: '手工备注',
assign_assistant: '改派医助',
add_pay_order: '补齐支付单',
set_ship_mode: '发货类型'
} }
return m[act] || act return m[act] || act
} }
/** 处方/支付单审核状态文案(0 待审核 / 1 已通过 / 2 已驳回) */
export function auditStatusText(s: number | undefined) {
if (s === 1) return '已通过'
if (s === 2) return '已驳回'
return '待审核'
}
/** 支付单来源/方式:企微对外收款、付呗、快递代收等创建链路 + 支付方式回退 */ /** 支付单来源/方式:企微对外收款、付呗、快递代收等创建链路 + 支付方式回退 */
export function formatPayOrderSource(row: { payment_method?: unknown; create_type?: unknown }) { export function formatPayOrderSource(row: { payment_method?: unknown; create_type?: unknown }) {
const createType = String(row?.create_type || '') const createType = String(row?.create_type || '')
@@ -357,3 +369,85 @@ export function formatDietaryTaboo(raw: unknown): string {
} }
return '' return ''
} }
/** 服务套餐 dictserver_order */
export type ServicePackageOption = { name: string; value: string; status?: number }
export function normalizeServicePackageValue(v: unknown): string {
return String(v ?? '').trim()
}
/** 解析订单 service_package(逗号串 / 数组 / 单值数字) */
export function parseServicePackageValues(raw: unknown): string[] {
if (raw == null || raw === '') return []
if (Array.isArray(raw)) {
return raw.map(normalizeServicePackageValue).filter(Boolean)
}
if (typeof raw === 'string') {
return raw.split(',').map((v) => v.trim()).filter(Boolean)
}
const one = normalizeServicePackageValue(raw)
return one ? [one] : []
}
export function servicePackageValueEquals(a: unknown, b: unknown): boolean {
const sa = normalizeServicePackageValue(a)
const sb = normalizeServicePackageValue(b)
if (!sa || !sb) return false
if (sa === sb) return true
const na = Number(sa)
const nb = Number(sb)
return Number.isFinite(na) && Number.isFinite(nb) && na === nb
}
export function normalizeServicePackageOptions(raw: unknown): ServicePackageOption[] {
if (!Array.isArray(raw)) return []
return raw
.map((item: any) => ({
name: String(item?.name ?? '').trim() || normalizeServicePackageValue(item?.value),
value: normalizeServicePackageValue(item?.value),
status: Number(item?.status ?? 1)
}))
.filter((item) => item.value !== '')
}
export function findServicePackageOption(
options: ServicePackageOption[],
value: unknown
): ServicePackageOption | undefined {
const key = normalizeServicePackageValue(value)
if (!key) return undefined
return options.find((opt) => servicePackageValueEquals(opt.value, key))
}
/** 展示用:value → 字典 name,多选用「、」连接 */
export function formatServicePackageLabels(
value: unknown,
options: ServicePackageOption[],
emptyText = '—'
): string {
const packages = parseServicePackageValues(value)
if (packages.length === 0) return emptyText
const names = packages.map((val) => {
const option = findServicePackageOption(options, val)
return option?.name || val
})
return names.join('、')
}
/** 编辑下拉:字典项 + 当前已选但字典缺失的兜底项 */
export function mergeServicePackageSelectOptions(
options: ServicePackageOption[],
selected: unknown[]
): ServicePackageOption[] {
const known = new Set(options.map((o) => o.value))
const extras: ServicePackageOption[] = []
for (const raw of selected) {
const val = normalizeServicePackageValue(raw)
if (!val || known.has(val)) continue
const matched = findServicePackageOption(options, val)
extras.push(matched ?? { name: val, value: val, status: 0 })
known.add(val)
}
return extras.length ? [...options, ...extras] : options
}
@@ -298,7 +298,7 @@
:fetch-fun="prescriptionOrderExport" :fetch-fun="prescriptionOrderExport"
:params="prescriptionOrderExportParams" :params="prescriptionOrderExportParams"
:page-size="pager.size" :page-size="pager.size"
export-hint="导出范围与上方筛选一致履约状态创建时间及其他条件均会生效自媒体渠道挂号渠道来源优先取该单关联处方登记的挂号无则诊单下同患者挂号取 id 最大的一条与前台挂号选择的记录一致业绩侧栏带渠道筛选导出时与同页列表高亮挂号同源服务套餐按字典 server_order 解析展示关联收款记录与详情侧栏同源已支付/已退款/待审核每笔两行展示摘要行+明细行多笔空行分隔单元格自动换行签收日期与详情/业绩看板同源仅读物流库轨迹/签收时间导出不再实时查快递100速度只取决于数据库签收时间由 `tcm:backfill-sign-time` 命令与物流自动更新定时任务落库刚发货尚未同步的单子会暂时为空待下次回填/定时任务刷新后显示" export-hint="导出范围与上方筛选一致履约状态创建时间及其他条件均会生效自媒体渠道挂号渠道来源优先取该单关联处方登记的挂号无则诊单下同患者挂号取 id 最大的一条与前台挂号选择的记录一致业绩侧栏带渠道筛选导出时与同页列表高亮挂号同源服务套餐按字典 server_order 解析展示处方导出主方/辅方药材明细主方/辅方服用方式天数与详情侧栏同口径主方/辅方天数分别取处方 usage_days辅方 aux_usage.usage_days天数列为订单 medication_days关联收款记录与详情侧栏同源已支付/已退款/待审核每笔两行展示摘要行+明细行多笔空行分隔单元格自动换行签收日期与详情/业绩看板同源仅读物流库轨迹/签收时间导出不再实时查快递100速度只取决于数据库签收时间由 `tcm:backfill-sign-time` 命令与物流自动更新定时任务落库刚发货尚未同步的单子会暂时为空待下次回填/定时任务刷新后显示"
/> />
</el-form-item> </el-form-item>
</el-form> </el-form>
@@ -623,6 +623,7 @@
@view-prescription="detailData && openPrescriptionView(detailData)" @view-prescription="detailData && openPrescriptionView(detailData)"
@test-gancao-preview="testGancaoPreviewFromDetail" @test-gancao-preview="testGancaoPreviewFromDetail"
@view-patient="openDiagnosisPatientDetailFromOrder" @view-patient="openDiagnosisPatientDetailFromOrder"
@detail-changed="getLists"
> >
<template #header-extra="{ detail }"> <template #header-extra="{ detail }">
<div class="flex items-center gap-2 ml-4 shrink-0"> <div class="flex items-center gap-2 ml-4 shrink-0">
@@ -1024,10 +1025,11 @@
class="w-full" class="w-full"
> >
<el-option <el-option
v-for="item in servicePackageOptions" v-for="item in editServicePackageSelectOptions"
:key="item.value" :key="item.value"
:label="item.name" :label="item.name"
:value="item.value" :value="item.value"
:disabled="item.status === 0"
/> />
</el-select> </el-select>
</el-form-item> </el-form-item>
@@ -2225,7 +2227,11 @@ import {
canUpdateAmount, canUpdateAmount,
formatDietaryTaboo, formatDietaryTaboo,
type SlipFormulaType, type SlipFormulaType,
type SlipAuxUsageForm type SlipAuxUsageForm,
type ServicePackageOption,
normalizeServicePackageOptions,
parseServicePackageValues,
mergeServicePackageSelectOptions
} from './components/prescription-order-utils' } from './components/prescription-order-utils'
import { useListTimeFilter } from '@/hooks/useListTimeFilter' import { useListTimeFilter } from '@/hooks/useListTimeFilter'
import { import {
@@ -2414,8 +2420,8 @@ async function submitReassign() {
// 省市区数据 // 省市区数据
const regionOptions = ref([]) const regionOptions = ref([])
// 服务套餐选项 // 服务套餐选项(含已停用项,便于编辑时回显历史值)
const servicePackageOptions = ref<Array<{ name: string; value: string }>>([]) const servicePackageOptions = ref<ServicePackageOption[]>([])
/** 筛选:开方医生、诊单医助(与列表接口 doctor_id / assistant_id 一致) */ /** 筛选:开方医生、诊单医助(与列表接口 doctor_id / assistant_id 一致) */
const doctorOptions = ref<Array<{ id: number; name: string }>>([]) const doctorOptions = ref<Array<{ id: number; name: string }>>([])
@@ -2536,8 +2542,7 @@ const loadRegionData = async () => {
const loadServicePackageOptions = async () => { const loadServicePackageOptions = async () => {
try { try {
const data = await getDictData({ type: 'server_order' }) const data = await getDictData({ type: 'server_order' })
servicePackageOptions.value = (data?.server_order || []).filter((item: any) => item.status !== 0) servicePackageOptions.value = normalizeServicePackageOptions(data?.server_order)
console.log('服务套餐选项已加载:', servicePackageOptions.value.length)
} catch (error) { } catch (error) {
console.error('加载服务套餐选项失败:', error) console.error('加载服务套餐选项失败:', error)
servicePackageOptions.value = [] servicePackageOptions.value = []
@@ -3195,10 +3200,17 @@ async function onDetailShipModeChange(mode: string | number | boolean | undefine
} }
} }
function canAddPayOrderRow(row: { fulfillment_status?: number }) { function canAddPayOrderRow(row: {
// 已发货(5) / 已签收(6) 状态可补齐支付单 fulfillment_status?: number
amount?: number | string
linked_pay_paid_total?: number | string
}) {
// 已发货(5) / 已签收(6) 状态可补齐支付单;总金额已付清则不允许
const fs = Number(row.fulfillment_status) const fs = Number(row.fulfillment_status)
return fs === 5 || fs === 6 if (fs !== 5 && fs !== 6) return false
const orderAmount = Math.round((Number(row.amount) || 0) * 100) / 100
const paidTotal = Math.round((Number(row.linked_pay_paid_total) || 0) * 100) / 100
return paidTotal < orderAmount
} }
function canCompleteRow(row: { fulfillment_status?: number; payment_slip_audit_status?: number }) { function canCompleteRow(row: { fulfillment_status?: number; payment_slip_audit_status?: number }) {
@@ -3449,6 +3461,11 @@ const editForm = reactive({
diagnosis_creator_dept_path: '' diagnosis_creator_dept_path: ''
}) })
/** 编辑弹窗下拉:字典项 + 当前已选但字典中缺失的兜底项 */
const editServicePackageSelectOptions = computed(() =>
mergeServicePackageSelectOptions(servicePackageOptions.value, editForm.service_package)
)
/** 诊单创建人部门:拆成面包屑。多部门为「;」分隔;路径内为「 / 」含父级(与后端 buildDeptPath 一致) */ /** 诊单创建人部门:拆成面包屑。多部门为「;」分隔;路径内为「 / 」含父级(与后端 buildDeptPath 一致) */
const editDiagnosisCreatorDeptBreadcrumbs = computed(() => { const editDiagnosisCreatorDeptBreadcrumbs = computed(() => {
const raw = String(editForm.diagnosis_creator_dept_path || '').trim() const raw = String(editForm.diagnosis_creator_dept_path || '').trim()
@@ -3681,18 +3698,7 @@ async function openEdit(row: {
editForm.dose_unit = d.dose_unit || '剂' editForm.dose_unit = d.dose_unit || '剂'
editForm.prev_staff = d.prev_staff || '' editForm.prev_staff = d.prev_staff || ''
editForm.service_channel = d.service_channel || '' editForm.service_channel = d.service_channel || ''
// 处理服务套餐:如果是字符串,转换为数组 editForm.service_package = parseServicePackageValues(d.service_package)
if (d.service_package) {
if (Array.isArray(d.service_package)) {
editForm.service_package = d.service_package
} else if (typeof d.service_package === 'string') {
editForm.service_package = d.service_package.split(',').filter(v => v.trim() !== '')
} else {
editForm.service_package = []
}
} else {
editForm.service_package = []
}
editForm.express_company = String(d.express_company || 'auto') || 'auto' editForm.express_company = String(d.express_company || 'auto') || 'auto'
editForm.tracking_number = d.tracking_number || '' editForm.tracking_number = d.tracking_number || ''
editForm.fee_type = Number(d.fee_type) || 3 editForm.fee_type = Number(d.fee_type) || 3
@@ -4509,7 +4515,18 @@ async function loadAddPayOrderAvailable(diagnosisId: number, currentLinkedIds: n
} }
} }
function openAddPayOrder(row: { id: number; diagnosis_id?: number; pay_order_ids?: number[] }) { function openAddPayOrder(row: {
id: number
diagnosis_id?: number
pay_order_ids?: number[]
fulfillment_status?: number
amount?: number | string
linked_pay_paid_total?: number | string
}) {
if (!canAddPayOrderRow(row)) {
feedback.msgWarning('订单总金额与已付金额一致,无需补齐支付单')
return
}
addPayOrderRowId.value = row.id addPayOrderRowId.value = row.id
addPayOrderForm.add_mode = 'create' addPayOrderForm.add_mode = 'create'
addPayOrderForm.order_type = 3 addPayOrderForm.order_type = 3
@@ -858,31 +858,94 @@
</el-tag> </el-tag>
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="用量"> <el-descriptions-item label="用量">
<template v-if="detailPrescription.dosage_amount"> <div class="flex flex-col gap-1 text-sm leading-relaxed">
{{ detailPrescription.dosage_amount }}{{ detailPrescription.dosage_unit || 'g' }} <div>
<template v-if="detailPrescription.prescription_type === '浓缩水丸'"> <span v-if="detailHasAuxHerbs" class="text-gray-500 mr-1">主方</span>
· {{ Number(detailPrescription.dosage_bag_count) > 0 ? Number(detailPrescription.dosage_bag_count) : 1 }} <template v-if="detailPrescription.dosage_amount">
</template> {{ detailPrescription.dosage_amount }}{{ detailPrescription.dosage_unit || 'g' }}
<span v-if="detailPrescription.prescription_type === '饮片' && detailPrescription.need_decoction !== null" class="ml-2 text-gray-500"> <template v-if="detailPrescription.prescription_type === '浓缩水丸'">
({{ detailPrescription.need_decoction ? '代煎' : '不代煎' }}) · {{ Number(detailPrescription.dosage_bag_count) > 0 ? Number(detailPrescription.dosage_bag_count) : 1 }}
</span> </template>
</template> <span v-if="detailPrescription.prescription_type === '饮片' && detailPrescription.need_decoction !== null" class="ml-2 text-gray-500">
<template v-else></template> ({{ detailPrescription.need_decoction ? '代煎' : '不代煎' }})
</span>
</template>
<template v-else></template>
</div>
<div v-if="detailHasAuxHerbs && detailAuxUsage">
<span class="text-gray-500 mr-1">辅方</span>
<template v-if="detailAuxUsage.dosage_amount != null && detailAuxUsage.dosage_amount !== 0">
{{ detailAuxUsage.dosage_amount }}{{ detailPrescription.dosage_unit || 'g' }}
<template v-if="detailPrescription.prescription_type === '浓缩水丸'">
· {{ Number(detailAuxUsage.dosage_bag_count) > 0 ? Number(detailAuxUsage.dosage_bag_count) : 1 }}
</template>
<span v-if="detailPrescription.prescription_type === '饮片'" class="ml-2 text-gray-500">
({{ detailAuxUsage.need_decoction ? '代煎' : '不代煎' }})
</span>
</template>
<template v-else></template>
</div>
</div>
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="服用方式"> <el-descriptions-item>
<template #label>
<div class="flex items-center gap-1 flex-wrap">
<span>服用方式</span>
<el-button
v-if="
detailData.prescription_id &&
detailPrescription &&
!String(detailData.prescription_detail_error || '').trim()
"
v-perms="['tcm.prescriptionOrder/patchPrescriptionUsage']"
type="primary"
size="small"
link
@click="openPatchUsageDialog"
>
修改
</el-button>
</div>
</template>
<div class="flex flex-col gap-1.5 text-sm leading-relaxed"> <div class="flex flex-col gap-1.5 text-sm leading-relaxed">
<div> <template v-if="detailHasAuxHerbs">
<span class="text-gray-500">每天次数</span> <div>
{{ detailPrescription.times_per_day ? detailPrescription.times_per_day + ' 次' : '—' }} <span class="text-gray-500 mr-1">主方</span>
</div> 每天
<div> {{ detailPrescription.times_per_day ? detailPrescription.times_per_day + ' 次' : '— 次' }}
<span class="text-gray-500">处方开立</span> · 处方开立
{{ {{
detailPrescription.usage_days != null && detailPrescription.usage_days !== '' detailPrescription.usage_days != null && detailPrescription.usage_days !== ''
? detailPrescription.usage_days + ' 天' ? detailPrescription.usage_days + ' 天'
: '—' : '—'
}} }}
</div> </div>
<div v-if="detailAuxUsage">
<span class="text-gray-500 mr-1">辅方</span>
每天
{{ detailAuxUsage.times_per_day ? detailAuxUsage.times_per_day + ' 次' : '— 次' }}
· 处方开立
{{
detailAuxUsage.usage_days != null && Number(detailAuxUsage.usage_days) > 0
? detailAuxUsage.usage_days + ' 天'
: '— 天'
}}
</div>
</template>
<template v-else>
<div>
<span class="text-gray-500">每天次数</span>
{{ detailPrescription.times_per_day ? detailPrescription.times_per_day + ' 次' : '—' }}
</div>
<div>
<span class="text-gray-500">处方开立</span>
{{
detailPrescription.usage_days != null && detailPrescription.usage_days !== ''
? detailPrescription.usage_days + ' 天'
: '—'
}}
</div>
</template>
<div> <div>
<span class="text-gray-500">订单设置</span> <span class="text-gray-500">订单设置</span>
{{ {{
@@ -1139,7 +1202,7 @@
<el-descriptions-item label="上次医护">{{ detailData.prev_staff || '—' }}</el-descriptions-item> <el-descriptions-item label="上次医护">{{ detailData.prev_staff || '—' }}</el-descriptions-item>
<el-descriptions-item label="服务渠道">{{ detailData.service_channel || '—' }}</el-descriptions-item> <el-descriptions-item label="服务渠道">{{ detailData.service_channel || '—' }}</el-descriptions-item>
<el-descriptions-item label="服务套餐">{{ formatServicePackage(detailData.service_package) }}</el-descriptions-item> <el-descriptions-item label="服务套餐">{{ detailServicePackageText }}</el-descriptions-item>
<el-descriptions-item label="费用类别">{{ feeTypeText(detailData.fee_type) }}</el-descriptions-item> <el-descriptions-item label="费用类别">{{ feeTypeText(detailData.fee_type) }}</el-descriptions-item>
<el-descriptions-item v-if="detailData.internal_cost != null && detailData.internal_cost !== ''" label="内部成本"> <el-descriptions-item v-if="detailData.internal_cost != null && detailData.internal_cost !== ''" label="内部成本">
@@ -1525,10 +1588,11 @@
class="w-full" class="w-full"
> >
<el-option <el-option
v-for="item in servicePackageOptions" v-for="item in editServicePackageSelectOptions"
:key="item.value" :key="item.value"
:label="item.name" :label="item.name"
:value="item.value" :value="item.value"
:disabled="item.status === 0"
/> />
</el-select> </el-select>
</el-form-item> </el-form-item>
@@ -2151,6 +2215,93 @@
</template> </template>
</el-dialog> </el-dialog>
<!-- 修改服用参数主方 / 辅方 / 订单设置 -->
<el-dialog
v-model="patchUsageVisible"
title="修改服用参数"
width="92%"
:close-on-click-modal="false"
destroy-on-close
class="po-h5-dialog"
@closed="resetPatchUsageForm"
>
<el-form
ref="patchUsageFormRef"
:model="patchUsageForm"
:rules="patchUsageRules"
label-width="96px"
>
<div v-if="detailHasAuxHerbs" class="text-xs font-medium text-gray-500 mb-3">主方</div>
<el-form-item label="每天次数" prop="times_per_day">
<el-input-number
v-model="patchUsageForm.times_per_day"
:min="1"
:max="6"
:step="1"
controls-position="right"
class="w-full"
/>
</el-form-item>
<el-form-item label="处方开立" prop="usage_days">
<div class="flex items-center gap-1 w-full">
<el-input-number
v-model="patchUsageForm.usage_days"
:min="1"
:max="999"
:step="1"
controls-position="right"
class="flex-1 min-w-0"
/>
<span class="text-gray-500 shrink-0"></span>
</div>
</el-form-item>
<template v-if="detailHasAuxHerbs">
<div class="text-xs font-medium text-gray-500 mb-3 mt-2 pt-2 border-t border-gray-100">辅方</div>
<el-form-item label="每天次数" prop="aux_times_per_day">
<el-input-number
v-model="patchUsageForm.aux_times_per_day"
:min="1"
:max="6"
:step="1"
controls-position="right"
class="w-full"
/>
</el-form-item>
<el-form-item label="处方开立" prop="aux_usage_days">
<div class="flex items-center gap-1 w-full">
<el-input-number
v-model="patchUsageForm.aux_usage_days"
:min="1"
:max="999"
:step="1"
controls-position="right"
class="flex-1 min-w-0"
/>
<span class="text-gray-500 shrink-0"></span>
</div>
</el-form-item>
</template>
<div class="text-xs font-medium text-gray-500 mb-3 mt-2 pt-2 border-t border-gray-100">订单设置</div>
<el-form-item label="服用天数" prop="medication_days">
<div class="flex items-center gap-1 w-full">
<el-input-number
v-model="patchUsageForm.medication_days"
:min="1"
:max="999"
:step="1"
controls-position="right"
class="flex-1 min-w-0"
/>
<span class="text-gray-500 shrink-0"></span>
</div>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="patchUsageVisible = false">取消</el-button>
<el-button type="primary" :loading="patchUsageSaving" @click="submitPatchUsage">保存</el-button>
</template>
</el-dialog>
<!-- 处方详情查看处方单样式 --> <!-- 处方详情查看处方单样式 -->
<el-drawer <el-drawer
v-model="prescriptionViewVisible" v-model="prescriptionViewVisible"
@@ -2565,6 +2716,7 @@ import {
prescriptionOrderRevokeRxAudit, prescriptionOrderRevokeRxAudit,
prescriptionOrderRevokePayAudit, prescriptionOrderRevokePayAudit,
prescriptionOrderPatchPrescriptionPatient, prescriptionOrderPatchPrescriptionPatient,
prescriptionOrderPatchPrescriptionUsage,
prescriptionOrderLinkPayOrder, prescriptionOrderLinkPayOrder,
prescriptionOrderRequestCompletion, prescriptionOrderRequestCompletion,
prescriptionOrderSubmitGancaoRecipel, prescriptionOrderSubmitGancaoRecipel,
@@ -2573,7 +2725,16 @@ import {
getDoctors, getDoctors,
getAssistants getAssistants
} from '@/api/tcm' } from '@/api/tcm'
import { formatDietaryTaboo } from './components/prescription-order-utils' import {
formatDietaryTaboo,
type ServicePackageOption,
normalizeServicePackageOptions,
parseServicePackageValues,
mergeServicePackageSelectOptions,
formatServicePackageLabels,
normalizeSlipAuxUsageForm,
prescriptionHasAuxFormula
} from './components/prescription-order-utils'
import html2canvas from 'html2canvas' import html2canvas from 'html2canvas'
import { jsPDF } from 'jspdf' import { jsPDF } from 'jspdf'
import { getDictData } from '@/api/app' import { getDictData } from '@/api/app'
@@ -2669,7 +2830,7 @@ const canViewFinanceFields = () => {
const regionOptions = ref([]) const regionOptions = ref([])
// //
const servicePackageOptions = ref<Array<{ name: string; value: string }>>([]) const servicePackageOptions = ref<ServicePackageOption[]>([])
/** 筛选:开方医生、诊单医助(与列表接口 doctor_id / assistant_id 一致) */ /** 筛选:开方医生、诊单医助(与列表接口 doctor_id / assistant_id 一致) */
const doctorOptions = ref<Array<{ id: number; name: string }>>([]) const doctorOptions = ref<Array<{ id: number; name: string }>>([])
@@ -2790,8 +2951,7 @@ const loadRegionData = async () => {
const loadServicePackageOptions = async () => { const loadServicePackageOptions = async () => {
try { try {
const data = await getDictData({ type: 'server_order' }) const data = await getDictData({ type: 'server_order' })
servicePackageOptions.value = (data?.server_order || []).filter((item: any) => item.status !== 0) servicePackageOptions.value = normalizeServicePackageOptions(data?.server_order)
console.log('服务套餐选项已加载:', servicePackageOptions.value.length)
} catch (error) { } catch (error) {
console.error('加载服务套餐选项失败:', error) console.error('加载服务套餐选项失败:', error)
servicePackageOptions.value = [] servicePackageOptions.value = []
@@ -3199,28 +3359,6 @@ function feeTypeText(t: number | undefined) {
return m[Number(t)] ?? '—' return m[Number(t)] ?? '—'
} }
//
function formatServicePackage(value: any): string {
if (!value) return '—'
let packages: string[] = []
if (Array.isArray(value)) {
packages = value
} else if (typeof value === 'string') {
packages = value.split(',').filter(v => v.trim() !== '')
}
if (packages.length === 0) return '—'
//
const names = packages.map(val => {
const option = servicePackageOptions.value.find(opt => opt.value === val)
return option ? option.name : val
})
return names.join('、')
}
function auditStatusText(s: number | undefined) { function auditStatusText(s: number | undefined) {
if (s === 1) return '已通过' if (s === 1) return '已通过'
if (s === 2) return '已驳回' if (s === 2) return '已驳回'
@@ -3473,6 +3611,10 @@ const detailVisible = ref(false)
const detailLoading = ref(false) const detailLoading = ref(false)
const detailData = ref<Record<string, any> | null>(null) const detailData = ref<Record<string, any> | null>(null)
const detailServicePackageText = computed(() =>
formatServicePackageLabels(detailData.value?.service_package, servicePackageOptions.value)
)
// --- --- // --- ---
const workflowActiveStep = computed(() => { const workflowActiveStep = computed(() => {
if (!detailData.value) return 0 if (!detailData.value) return 0
@@ -3629,6 +3771,16 @@ const detailLinkedAppointmentResolvedFromTag = computed(() => {
const detailRxHerbs = computed(() => normalizeSlipHerbs(detailPrescription.value?.herbs)) const detailRxHerbs = computed(() => normalizeSlipHerbs(detailPrescription.value?.herbs))
const detailHasAuxHerbs = computed(() => prescriptionHasAuxFormula(detailPrescription.value as any))
const detailAuxUsage = computed(() => {
const rx = detailPrescription.value as any
if (!rx || !prescriptionHasAuxFormula(rx)) return null
const raw = rx.aux_usage
if (raw == null || raw === '' || (Array.isArray(raw) && raw.length === 0)) return null
return normalizeSlipAuxUsageForm(raw, rx.prescription_type || '浓缩水丸')
})
/** false=无权限;true/缺省兼容旧接口(旧版未下发该字段时仍展示药材) */ /** false=无权限;true/缺省兼容旧接口(旧版未下发该字段时仍展示药材) */
const detailHerbsVisible = computed(() => detailData.value?.prescription_detail_herbs_visible !== false) const detailHerbsVisible = computed(() => detailData.value?.prescription_detail_herbs_visible !== false)
@@ -3855,6 +4007,101 @@ const patchRxPatientRules: FormRules = {
phone: [{ required: true, message: '请输入手机号', trigger: 'blur' }] phone: [{ required: true, message: '请输入手机号', trigger: 'blur' }]
} }
const patchUsageVisible = ref(false)
const patchUsageSaving = ref(false)
const patchUsageFormRef = ref<FormInstance>()
const patchUsageForm = reactive({
times_per_day: 3 as number | undefined,
usage_days: 7 as number | undefined,
aux_times_per_day: 3 as number | undefined,
aux_usage_days: 7 as number | undefined,
medication_days: undefined as number | undefined
})
const patchUsageRules = computed<FormRules>(() => {
const rules: FormRules = {
times_per_day: [{ required: true, message: '请填写主方每天次数', trigger: 'change' }],
usage_days: [{ required: true, message: '请填写主方开立天数', trigger: 'change' }],
medication_days: [{ required: true, message: '请填写订单服用天数', trigger: 'change' }]
}
if (detailHasAuxHerbs.value) {
rules.aux_times_per_day = [{ required: true, message: '请填写辅方每天次数', trigger: 'change' }]
rules.aux_usage_days = [{ required: true, message: '请填写辅方开立天数', trigger: 'change' }]
}
return rules
})
function openPatchUsageDialog() {
const rx = detailPrescription.value
const ord = detailData.value
if (!rx || !ord?.id || !ord.prescription_id) {
feedback.msgWarning('无处方数据')
return
}
const aux = detailAuxUsage.value
patchUsageForm.times_per_day =
Number(rx.times_per_day) > 0 ? Number(rx.times_per_day) : 3
patchUsageForm.usage_days =
Number(rx.usage_days) > 0 ? Number(rx.usage_days) : 7
patchUsageForm.aux_times_per_day =
aux && Number(aux.times_per_day) > 0 ? Number(aux.times_per_day) : 3
patchUsageForm.aux_usage_days =
aux && Number(aux.usage_days) > 0 ? Number(aux.usage_days) : 7
const md = Number(ord.medication_days)
patchUsageForm.medication_days = md > 0 ? md : Number(rx.usage_days) > 0 ? Number(rx.usage_days) : 7
patchUsageVisible.value = true
nextTick(() => patchUsageFormRef.value?.clearValidate())
}
function resetPatchUsageForm() {
patchUsageForm.times_per_day = 3
patchUsageForm.usage_days = 7
patchUsageForm.aux_times_per_day = 3
patchUsageForm.aux_usage_days = 7
patchUsageForm.medication_days = undefined
}
async function submitPatchUsage() {
const form = patchUsageFormRef.value
if (!form) return
try {
await form.validate()
} catch {
return
}
const ordId = detailData.value?.id
if (!ordId) return
patchUsageSaving.value = true
try {
const payload: {
id: number
times_per_day: number
usage_days: number
medication_days: number
aux_times_per_day?: number
aux_usage_days?: number
} = {
id: ordId,
times_per_day: Number(patchUsageForm.times_per_day),
usage_days: Number(patchUsageForm.usage_days),
medication_days: Number(patchUsageForm.medication_days)
}
if (detailHasAuxHerbs.value) {
payload.aux_times_per_day = Number(patchUsageForm.aux_times_per_day)
payload.aux_usage_days = Number(patchUsageForm.aux_usage_days)
}
await prescriptionOrderPatchPrescriptionUsage(payload)
feedback.msgSuccess('保存成功')
patchUsageVisible.value = false
await refreshCurrentPrescriptionOrderDetail()
await fetchLogs(ordId)
getLists()
} catch {
/* 拦截器已提示 */
} finally {
patchUsageSaving.value = false
}
}
function openPatchRxPatientDialog() { function openPatchRxPatientDialog() {
const rx = detailPrescription.value const rx = detailPrescription.value
const ord = detailData.value const ord = detailData.value
@@ -4060,6 +4307,10 @@ const editForm = reactive({
diagnosis_creator_dept_path: '' diagnosis_creator_dept_path: ''
}) })
const editServicePackageSelectOptions = computed(() =>
mergeServicePackageSelectOptions(servicePackageOptions.value, editForm.service_package)
)
/** 诊单创建人部门:拆成面包屑。多部门为「;」分隔;路径内为「 / 」含父级(与后端 buildDeptPath 一致) */ /** 诊单创建人部门:拆成面包屑。多部门为「;」分隔;路径内为「 / 」含父级(与后端 buildDeptPath 一致) */
const editDiagnosisCreatorDeptBreadcrumbs = computed(() => { const editDiagnosisCreatorDeptBreadcrumbs = computed(() => {
const raw = String(editForm.diagnosis_creator_dept_path || '').trim() const raw = String(editForm.diagnosis_creator_dept_path || '').trim()
@@ -4310,18 +4561,7 @@ async function openEdit(row: {
editForm.dose_unit = d.dose_unit || '剂' editForm.dose_unit = d.dose_unit || '剂'
editForm.prev_staff = d.prev_staff || '' editForm.prev_staff = d.prev_staff || ''
editForm.service_channel = d.service_channel || '' editForm.service_channel = d.service_channel || ''
// editForm.service_package = parseServicePackageValues(d.service_package)
if (d.service_package) {
if (Array.isArray(d.service_package)) {
editForm.service_package = d.service_package
} else if (typeof d.service_package === 'string') {
editForm.service_package = d.service_package.split(',').filter(v => v.trim() !== '')
} else {
editForm.service_package = []
}
} else {
editForm.service_package = []
}
editForm.express_company = String(d.express_company || 'auto') || 'auto' editForm.express_company = String(d.express_company || 'auto') || 'auto'
editForm.tracking_number = d.tracking_number || '' editForm.tracking_number = d.tracking_number || ''
editForm.fee_type = Number(d.fee_type) || 3 editForm.fee_type = Number(d.fee_type) || 3
@@ -0,0 +1,319 @@
<template>
<div class="auto-assign-log-page">
<el-card class="!border-none" shadow="never">
<el-form :inline="true" class="log-filter-form">
<el-form-item label="记录时间">
<el-date-picker
v-model="timeRange"
type="datetimerange"
range-separator="至"
start-placeholder="开始时间"
end-placeholder="结束时间"
value-format="YYYY-MM-DD HH:mm:ss"
clearable
@change="doSearch"
/>
</el-form-item>
<el-form-item label="执行日期">
<el-date-picker
v-model="dateRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
clearable
@change="doSearch"
/>
</el-form-item>
<el-form-item label="结果">
<el-select
v-model="queryParams.action"
clearable
placeholder="全部"
class="w-[120px]"
@change="doSearch"
>
<el-option label="已分配" value="1" />
<el-option label="未分配" value="0" />
</el-select>
</el-form-item>
<el-form-item label="回退">
<el-select
v-model="queryParams.is_rollback"
clearable
placeholder="全部"
class="w-[120px]"
@change="doSearch"
>
<el-option label="未回退" value="0" />
<el-option label="已回退" value="1" />
</el-select>
</el-form-item>
<el-form-item label="关键词">
<el-input
v-model="queryParams.keyword"
placeholder="患者/手机号/医助/诊单ID"
clearable
class="w-[220px]"
@keyup.enter="doSearch"
@clear="doSearch"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="doSearch">查询</el-button>
<el-button @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
<div class="log-hint">
由每日定时任务待分配诊单自动指派写入按上月二诊复诊接诊率分档轮询分配&gt;70% 每人3条/60%~70%
每人2条/50%~60% 每人1条/&lt;50% 不分回退可将已分配医助撤回到自动分配前的原医助通常为一中心并写入指派日志
</div>
</el-card>
<el-card class="!border-none mt-3" shadow="never">
<div class="toolbar mb-3">
<el-button
v-perms="['stats.autoAssignLog/rollback']"
type="warning"
:disabled="selectedRows.length === 0"
:loading="rollbackLoading"
@click="handleBatchRollback"
>
回退选中{{ selectedRows.length }}
</el-button>
<span v-if="selectedRows.length > 0" class="toolbar-tip">
已选 {{ selectedRows.length }} 条可回退记录
</span>
</div>
<el-table
ref="tableRef"
:data="pager.lists"
v-loading="pager.loading"
size="default"
stripe
row-key="id"
@selection-change="onSelectionChange"
>
<el-table-column
type="selection"
width="48"
:selectable="rowSelectable"
reserve-selection
/>
<el-table-column label="记录时间" width="160">
<template #default="{ row }">{{ row.create_time_text || '—' }}</template>
</el-table-column>
<el-table-column label="诊单" width="90" prop="diagnosis_id" />
<el-table-column label="患者" min-width="110">
<template #default="{ row }">
<div>{{ row.patient_name || '—' }}</div>
<div class="cell-sub">{{ row.patient_phone || '' }}</div>
</template>
</el-table-column>
<el-table-column label="结果" width="90" align="center">
<template #default="{ row }">
<el-tag :type="Number(row.action) === 1 ? 'success' : 'info'" size="small">
{{ row.action_text }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="回退状态" width="140">
<template #default="{ row }">
<template v-if="Number(row.is_rollback) === 1">
<el-tag type="warning" size="small">已回退</el-tag>
<div class="cell-sub">{{ row.rollback_time_text }}</div>
<div v-if="row.rollback_admin_name" class="cell-sub">
{{ row.rollback_admin_name }}
</div>
</template>
<el-tag v-else-if="Number(row.can_rollback) === 1" type="success" size="small">
可回退
</el-tag>
<span v-else></span>
</template>
</el-table-column>
<el-table-column label="医助" min-width="110">
<template #default="{ row }">
<template v-if="Number(row.assistant_id) > 0">
<div>{{ row.assistant_name || '#' + row.assistant_id }}</div>
<div class="cell-sub">上月二诊接诊率 {{ formatRate(row.visit2_rate) }}</div>
</template>
<span v-else></span>
</template>
</el-table-column>
<el-table-column label="档位" width="100" align="center">
<template #default="{ row }">{{ row.tier_text || '—' }}</template>
</el-table-column>
<el-table-column label="轮次" width="70" align="center">
<template #default="{ row }">
{{ Number(row.round_no) > 0 ? '第' + row.round_no + '轮' : '—' }}
</template>
</el-table-column>
<el-table-column label="原因" min-width="280" show-overflow-tooltip>
<template #default="{ row }">{{ row.reason }}</template>
</el-table-column>
<el-table-column label="统计月" width="90" prop="stat_month" align="center" />
<el-table-column label="批次" width="160">
<template #default="{ row }">
<span class="cell-sub">{{ row.batch_no }}</span>
</template>
</el-table-column>
<el-table-column label="操作" width="90" fixed="right" align="center">
<template #default="{ row }">
<el-button
v-if="Number(row.can_rollback) === 1"
v-perms="['stats.autoAssignLog/rollback']"
link
type="warning"
:loading="rollbackLoading"
@click="handleSingleRollback(row)"
>
回退
</el-button>
<span v-else class="cell-sub"></span>
</template>
</el-table-column>
</el-table>
<div class="flex justify-end mt-4">
<pagination v-model="pager" @change="getLists" />
</div>
</el-card>
</div>
</template>
<script setup lang="ts" name="statsAutoAssignLog">
import { autoAssignLogLists, autoAssignLogRollback } from '@/api/stats'
import { usePaging } from '@/hooks/usePaging'
import feedback from '@/utils/feedback'
import { onMounted, reactive, ref, watch } from 'vue'
const dateRange = ref<[string, string] | null>(null)
const timeRange = ref<[string, string] | null>(null)
const tableRef = ref()
const selectedRows = ref<any[]>([])
const rollbackLoading = ref(false)
const queryParams = reactive({
start_date: '',
end_date: '',
start_time: '',
end_time: '',
action: '' as '' | '0' | '1',
is_rollback: '' as '' | '0' | '1',
keyword: ''
})
watch(dateRange, (val) => {
queryParams.start_date = val?.[0] ?? ''
queryParams.end_date = val?.[1] ?? ''
})
watch(timeRange, (val) => {
queryParams.start_time = val?.[0] ?? ''
queryParams.end_time = val?.[1] ?? ''
})
const { pager, getLists, resetPage } = usePaging({
fetchFun: autoAssignLogLists,
params: queryParams
})
const doSearch = () => {
selectedRows.value = []
tableRef.value?.clearSelection?.()
resetPage()
}
const handleReset = () => {
dateRange.value = null
timeRange.value = null
queryParams.start_date = ''
queryParams.end_date = ''
queryParams.start_time = ''
queryParams.end_time = ''
queryParams.action = ''
queryParams.is_rollback = ''
queryParams.keyword = ''
doSearch()
}
const formatRate = (rate: number | null | undefined) =>
rate === null || rate === undefined ? '—' : `${rate}%`
const rowSelectable = (row: any) => Number(row.can_rollback) === 1
const onSelectionChange = (rows: any[]) => {
selectedRows.value = rows.filter((r) => Number(r.can_rollback) === 1)
}
const runRollback = async (ids: number[]) => {
if (ids.length === 0) return
await feedback.confirm(
`确认回退选中的 ${ids.length} 条自动分配?将把诊单医助撤回到自动分配前的原医助,并写入指派日志。`
)
rollbackLoading.value = true
try {
const res: any = await autoAssignLogRollback({ ids })
const success = Number(res?.success ?? 0)
const failed = Number(res?.failed ?? 0)
if (failed > 0 && Array.isArray(res?.messages) && res.messages.length) {
feedback.msgWarning(
`回退成功 ${success} 条,跳过/失败 ${failed} 条:${res.messages.slice(0, 3).join('')}`
)
} else {
feedback.msgSuccess(`回退成功 ${success}`)
}
selectedRows.value = []
tableRef.value?.clearSelection?.()
await getLists()
} finally {
rollbackLoading.value = false
}
}
const handleBatchRollback = () => {
const ids = selectedRows.value.map((r) => Number(r.id)).filter((id) => id > 0)
return runRollback(ids)
}
const handleSingleRollback = (row: any) => {
const id = Number(row?.id ?? 0)
if (id <= 0) return
return runRollback([id])
}
onMounted(() => getLists())
</script>
<style lang="scss" scoped>
.auto-assign-log-page {
.log-filter-form {
:deep(.el-form-item) {
margin-bottom: 8px;
}
}
.log-hint {
font-size: 12px;
color: var(--el-text-color-secondary);
line-height: 1.6;
}
.toolbar {
display: flex;
align-items: center;
gap: 12px;
}
.toolbar-tip {
font-size: 12px;
color: var(--el-text-color-secondary);
}
.cell-sub {
font-size: 12px;
color: var(--el-text-color-secondary);
}
}
</style>
+4 -4
View File
@@ -18,7 +18,7 @@
<el-tree-select <el-tree-select
v-model="deptId" v-model="deptId"
:data="deptTreeOptions" :data="deptTreeOptions"
placeholder="全部部门" placeholder="二中心(全部)"
clearable clearable
filterable filterable
check-strictly check-strictly
@@ -43,10 +43,10 @@
</template> </template>
<div class="rate-caliber"> <div class="rate-caliber">
<p> <p>
<b>当月被指派总数</b>当月内诊单被指派给医助按指派操作时间落月<b>剔除勾选继承的指派</b>)的诊单数医助 × 诊单去重部门行 / 合计行按诊单去重 <b>当月被指派总数</b>当月内诊单被指派给医助按指派操作时间落月<b>剔除勾选继承的指派</b>)的诊单数医助 × 诊单去重部门行 / 合计行按诊单去重<b>再剔除</b>名下存在履约拒收 / 退款业务订单的诊单
</p> </p>
<p> <p>
<b>诊次 N 次下单</b>患者诊单名下计入业绩的业务订单剔除已取消 / 拒收 / 退款按下单时间升序的全局序号<b>跨月累计不重置</b> 5 月指派后旗下成交 4 单为二诊~五诊下月再成交即为六诊 <b>诊次 N 次下单</b>患者诊单名下计入业绩的业务订单剔除已取消 / 拒收 / 退款按下单时间升序编号为实单序号<b>统计诊次 = 实单序号 + 诊单偏移</b>默认偏移 0 1 笔实单为一诊偏移 1 1 笔实单为二诊偏移 2 1 笔实单为三诊5 笔实单等价七诊诊次<b>跨月累计不重置</b>诊单可在业务订单tab 配置偏移量
</p> </p>
<p> <p>
<b>当月 N 诊单数</b>当月内下单且诊次为 N 的订单数归属下单时点<b>持有该患者的医助</b>指派可在往月释放后不再归属继承指派会转移持有人但不计被指派数 <b>当月 N 诊单数</b>当月内下单且诊次为 N 的订单数归属下单时点<b>持有该患者的医助</b>指派可在往月释放后不再归属继承指派会转移持有人但不计被指派数
@@ -55,7 +55,7 @@
<b>当月 N 诊接诊率</b> = 当月 N 诊单数 ÷ 当月被指派总数往月指派当月成交会推高分子比率可能超过 100%医助当月无新指派但旗下有成交时被指派数为 0比率显示 <b>当月 N 诊接诊率</b> = 当月 N 诊单数 ÷ 当月被指派总数往月指派当月成交会推高分子比率可能超过 100%医助当月无新指派但旗下有成交时被指派数为 0比率显示
</p> </p>
<p> <p>
医助按人事部门归组选定部门时含其组织下级 医助按人事部门归组<b>仅统计二中心及其组织下级</b>部门下拉与未选时的默认范围均限定在该子树选定部门时含其组织下级
</p> </p>
</div> </div>
</el-popover> </el-popover>
+45 -1
View File
@@ -83,6 +83,23 @@
<el-radio-button value="0">未确认</el-radio-button> <el-radio-button value="0">未确认</el-radio-button>
</el-radio-group> </el-radio-group>
</div> </div>
<div class="filter-group">
<span class="filter-label">部门</span>
<el-tree-select
v-model="formData.assistant_dept_id"
:data="departmentTreeRaw"
class="filter-dept-select"
clearable
filterable
check-strictly
:default-expand-all="true"
node-key="id"
size="small"
:props="assistantDeptTreeProps"
placeholder="选父级含子级"
@change="handleAssistantDeptChange"
/>
</div>
</div> </div>
<el-dialog <el-dialog
@@ -542,6 +559,7 @@
import { usePaging } from '@/hooks/usePaging' import { usePaging } from '@/hooks/usePaging'
import { defineAsyncComponent, onMounted, onUnmounted, watch } from 'vue' import { defineAsyncComponent, onMounted, onUnmounted, watch } from 'vue'
import { appointmentLists, cancelAppointment, completeAppointment, appointmentDetail } from '@/api/doctor' import { appointmentLists, cancelAppointment, completeAppointment, appointmentDetail } from '@/api/doctor'
import { deptAll } from '@/api/org/department'
import { getCallSignature, generateMiniProgramQrcode, tcmDiagnosisDetail, prescriptionGetByAppointment } from '@/api/tcm' import { getCallSignature, generateMiniProgramQrcode, tcmDiagnosisDetail, prescriptionGetByAppointment } from '@/api/tcm'
import { getDictData } from '@/api/app' import { getDictData } from '@/api/app'
import { addDoctorNote } from '@/api/patient' import { addDoctorNote } from '@/api/patient'
@@ -603,10 +621,19 @@ const formData = reactive({
end_date: '', end_date: '',
date_preset: 'today' as '' | 'yesterday' | 'day_before' | 'today' | 'tomorrow' | 'day_after', date_preset: 'today' as '' | 'yesterday' | 'day_before' | 'today' | 'tomorrow' | 'day_after',
diagnosis_confirmed: '' as '' | '0' | '1', // ''= 1= 0= diagnosis_confirmed: '' as '' | '0' | '1', // ''= 1= 0=
/** 接诊医生 / 诊单医助 / 挂号医助所属部门(选父级含子级) */
assistant_dept_id: '' as number | '',
/** 为 1 时后端 extend 返回各状态数量,避免额外 4 次列表请求 */ /** 为 1 时后端 extend 返回各状态数量,避免额外 4 次列表请求 */
include_status_counts: 0 as 0 | 1 include_status_counts: 0 as 0 | 1
}) })
const departmentTreeRaw = ref<unknown[]>([])
const assistantDeptTreeProps = {
value: 'id',
label: 'name',
children: 'children'
}
const activeTab = ref('1') const activeTab = ref('1')
const dateCustomVisible = ref(false) const dateCustomVisible = ref(false)
const statusCount = ref<Record<number, number>>({ const statusCount = ref<Record<number, number>>({
@@ -721,6 +748,12 @@ const handleDiagnosisConfirmedChange = () => {
loadData() loadData()
} }
//
const handleAssistantDeptChange = () => {
pager.page = 1
loadData()
}
// //
const handleDatePresetChange = (val: string | number | boolean | undefined) => { const handleDatePresetChange = (val: string | number | boolean | undefined) => {
const v = String(val || '') const v = String(val || '')
@@ -770,6 +803,7 @@ const handleReset = () => {
formData.doctor_name = '' formData.doctor_name = ''
formData.date_preset = 'today' formData.date_preset = 'today'
formData.diagnosis_confirmed = '' formData.diagnosis_confirmed = ''
formData.assistant_dept_id = ''
const t = new Date() const t = new Date()
const p = (n: number) => String(n).padStart(2, '0') const p = (n: number) => String(n).padStart(2, '0')
formData.start_date = `${t.getFullYear()}-${p(t.getMonth() + 1)}-${p(t.getDate())}` formData.start_date = `${t.getFullYear()}-${p(t.getMonth() + 1)}-${p(t.getDate())}`
@@ -1099,7 +1133,13 @@ formData.start_date = `${_today.getFullYear()}-${_pad(_today.getMonth() + 1)}-${
formData.end_date = formData.start_date formData.end_date = formData.start_date
formData.status = 1 formData.status = 1
onMounted(() => { onMounted(async () => {
try {
const deptTree = await deptAll()
departmentTreeRaw.value = Array.isArray(deptTree) ? deptTree : []
} catch {
departmentTreeRaw.value = []
}
loadData() loadData()
listPollTimer = setInterval(() => { listPollTimer = setInterval(() => {
loadData({ silent: true }) loadData({ silent: true })
@@ -1228,6 +1268,10 @@ onUnmounted(() => {
padding: 6px 14px; padding: 6px 14px;
} }
} }
.filter-dept-select {
width: 200px;
}
} }
} }
@@ -4,6 +4,43 @@
<el-empty description="当前诊单未携带患者ID,无法列出业务订单" /> <el-empty description="当前诊单未携带患者ID,无法列出业务订单" />
</div> </div>
<template v-else> <template v-else>
<div v-if="diagnosisId > 0" class="po-revisit-offset-bar mb-3">
<div class="po-revisit-offset-bar__main">
<span class="text-sm text-gray-600">复诊统计起始偏移</span>
<el-tooltip placement="top">
<template #content>
<div class="max-w-xs leading-relaxed">
在实单诊次序号上叠加偏移量设为 0默认 1 笔实单计为一诊设为 1 1 笔实单计为二诊设为 2 1 笔实单计为三诊若有 5 笔实单且偏移 2则统计上相当于计至七诊5+2
</div>
</template>
<el-icon class="text-gray-400 align-middle ml-1"><QuestionFilled /></el-icon>
</el-tooltip>
<el-input-number
v-model="revisitSlotStartOffset"
v-perms="['tcm.diagnosis/setRevisitSlotStartOffset']"
:min="0"
:max="20"
:step="1"
controls-position="right"
class="w-[120px] ml-3"
:disabled="offsetSaving"
/>
<span class="text-xs text-gray-500 ml-2">
1 笔实单计为{{ visitSlotStartLabel }}
</span>
</div>
<el-button
v-perms="['tcm.diagnosis/setRevisitSlotStartOffset']"
type="primary"
size="small"
:loading="offsetSaving"
:disabled="!offsetDirty"
@click="saveRevisitSlotStartOffset"
>
保存
</el-button>
</div>
<el-table <el-table
v-loading="pager.loading" v-loading="pager.loading"
:data="pager.lists" :data="pager.lists"
@@ -12,6 +49,23 @@
empty-text="暂无业务订单" empty-text="暂无业务订单"
> >
<el-table-column label="订单编号" prop="order_no" min-width="200" show-overflow-tooltip /> <el-table-column label="订单编号" prop="order_no" min-width="200" show-overflow-tooltip />
<el-table-column label="全局诊次" width="96" align="center">
<template #default="{ row }">
<span v-if="row.global_visit_seq">{{ row.global_visit_seq }}</span>
<span v-else class="text-gray-400"></span>
</template>
</el-table-column>
<el-table-column label="计入统计" width="96" align="center">
<template #default="{ row }">
<el-tag
v-if="row.counts_for_revisit_rate"
type="success"
size="small"
effect="plain"
></el-tag>
<el-tag v-else type="info" size="small" effect="plain"></el-tag>
</template>
</el-table-column>
<el-table-column label="金额" width="120" align="right"> <el-table-column label="金额" width="120" align="right">
<template #default="{ row }"> <template #default="{ row }">
<span class="text-red-500 font-semibold">¥{{ formatAmount(row.amount) }}</span> <span class="text-red-500 font-semibold">¥{{ formatAmount(row.amount) }}</span>
@@ -63,9 +117,11 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, reactive, ref, watch } from 'vue' import { computed, reactive, ref, watch } from 'vue'
import { QuestionFilled } from '@element-plus/icons-vue'
import { usePaging } from '@/hooks/usePaging' import { usePaging } from '@/hooks/usePaging'
import { prescriptionOrderLists } from '@/api/tcm' import { prescriptionOrderLists, tcmDiagnosisDetail, tcmDiagnosisSetRevisitSlotStartOffset } from '@/api/tcm'
import PrescriptionOrderDetailDrawer from '@/views/consumer/prescription/components/PrescriptionOrderDetailDrawer.vue' import PrescriptionOrderDetailDrawer from '@/views/consumer/prescription/components/PrescriptionOrderDetailDrawer.vue'
import feedback from '@/utils/feedback'
import { import {
formatTime, formatTime,
fulfillmentText, fulfillmentText,
@@ -91,6 +147,26 @@ const { pager, getLists, resetPage } = usePaging({
size: 10 size: 10
}) })
const revisitSlotStartOffset = ref(0)
const savedRevisitSlotStartOffset = ref(0)
const offsetSaving = ref(false)
const offsetLoading = ref(false)
const offsetDirty = computed(
() => Number(revisitSlotStartOffset.value) !== Number(savedRevisitSlotStartOffset.value)
)
const visitSlotStartLabel = computed(() => {
const raw = Number(revisitSlotStartOffset.value)
const offset = Number.isFinite(raw) ? raw : 0
const slot = offset + 1
const cn = ['', '一', '二', '三', '四', '五', '六', '七', '八', '九', '十']
if (slot >= 1 && slot <= 10) {
return cn[slot] + '诊'
}
return `${slot}`
})
const buildParams = () => { const buildParams = () => {
Object.keys(queryParams).forEach((k) => delete queryParams[k]) Object.keys(queryParams).forEach((k) => delete queryParams[k])
if (props.diagnosisId > 0) { if (props.diagnosisId > 0) {
@@ -102,6 +178,47 @@ const buildParams = () => {
queryParams.scene = 'diagnosis_edit' queryParams.scene = 'diagnosis_edit'
} }
async function loadRevisitSlotStartOffset() {
if (props.diagnosisId <= 0) return
offsetLoading.value = true
try {
const res: any = await tcmDiagnosisDetail({ id: props.diagnosisId })
const d = res?.data ?? res ?? {}
const offset = Number(d.revisit_slot_start_offset)
const val = Number.isFinite(offset) && offset >= 0 && offset <= 20 ? offset : 0
revisitSlotStartOffset.value = val
savedRevisitSlotStartOffset.value = val
} catch {
revisitSlotStartOffset.value = 0
savedRevisitSlotStartOffset.value = 0
} finally {
offsetLoading.value = false
}
}
async function saveRevisitSlotStartOffset() {
if (props.diagnosisId <= 0) return
const offset = Number(revisitSlotStartOffset.value)
if (!Number.isFinite(offset) || offset < 0 || offset > 20) {
feedback.msgWarning('起始偏移须在 020 之间')
return
}
offsetSaving.value = true
try {
await tcmDiagnosisSetRevisitSlotStartOffset({
id: props.diagnosisId,
revisit_slot_start_offset: offset
})
savedRevisitSlotStartOffset.value = offset
feedback.msgSuccess('保存成功')
getLists()
} catch {
/* 拦截器已提示 */
} finally {
offsetSaving.value = false
}
}
// / // /
const detailDrawerRef = ref<InstanceType<typeof PrescriptionOrderDetailDrawer>>() const detailDrawerRef = ref<InstanceType<typeof PrescriptionOrderDetailDrawer>>()
@@ -117,8 +234,13 @@ const formatAmount = (value: unknown) => {
watch( watch(
() => [props.diagnosisId, patientIdNum.value] as const, () => [props.diagnosisId, patientIdNum.value] as const,
() => { () => {
if (!patientIdAvailable.value) { pager.lists = []; pager.count = 0; return } if (!patientIdAvailable.value) {
pager.lists = []
pager.count = 0
return
}
buildParams() buildParams()
void loadRevisitSlotStartOffset()
resetPage() resetPage()
}, },
{ immediate: true } { immediate: true }
@@ -134,4 +256,21 @@ defineExpose({ refresh: () => getLists() })
.po-empty-tip { .po-empty-tip {
padding: 24px 0; padding: 24px 0;
} }
.po-revisit-offset-bar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
flex-wrap: wrap;
padding: 12px 14px;
border: 1px solid var(--el-border-color-lighter);
border-radius: 8px;
background: var(--el-fill-color-lighter);
}
.po-revisit-offset-bar__main {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 4px;
}
</style> </style>
+122 -20
View File
@@ -121,6 +121,16 @@
end-placeholder="最近挂号结束" end-placeholder="最近挂号结束"
@change="handleLatestAppointmentFilterChange" @change="handleLatestAppointmentFilterChange"
/> />
<daterange-picker
class="latest-assign-range"
v-model:startTime="formData.latest_assign_start_date"
v-model:endTime="formData.latest_assign_end_date"
picker-type="daterange"
value-format="YYYY-MM-DD"
start-placeholder="最近指派开始"
end-placeholder="最近指派结束"
@change="handleLatestAssignFilterChange"
/>
<el-select <el-select
v-model="formData.latest_appointment_channel_source" v-model="formData.latest_appointment_channel_source"
placeholder="最近挂号渠道" placeholder="最近挂号渠道"
@@ -173,6 +183,7 @@
v-loading="pager.loading" v-loading="pager.loading"
@selection-change="handleSelectionChange" @selection-change="handleSelectionChange"
@row-dblclick="goReadonly" @row-dblclick="goReadonly"
@sort-change="handleTableSortChange"
:row-class-name="getRowClassName" :row-class-name="getRowClassName"
class="diagnosis-table" class="diagnosis-table"
stripe stripe
@@ -283,7 +294,14 @@
<span v-else class="status-unprescribed">未开方</span> <span v-else class="status-unprescribed">未开方</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="未服务天数" width="110" align="center"> <el-table-column
label="未服务天数"
prop="unserved_days"
width="110"
align="center"
sortable="custom"
:sort-orders="['descending', 'ascending']"
>
<template #default="{ row }"> <template #default="{ row }">
<el-tooltip <el-tooltip
v-if="row.last_blood_record_at" v-if="row.last_blood_record_at"
@@ -762,6 +780,10 @@ const formData = reactive({
latest_appointment_start_date: '' as string, latest_appointment_start_date: '' as string,
latest_appointment_end_date: '' as string, latest_appointment_end_date: '' as string,
latest_appointment_channel_source: '' as string, latest_appointment_channel_source: '' as string,
latest_assign_start_date: '' as string,
latest_assign_end_date: '' as string,
/** 未服务天数排序:desc=天数多到少 asc=少到多 */
sort_unserved_days: '' as '' | 'asc' | 'desc',
diagnosis_confirmed: '' as '' | '0' | '1', diagnosis_confirmed: '' as '' | '0' | '1',
appointment_date: '' as string, appointment_date: '' as string,
has_appointment: '' as '' | '0' | '1', has_appointment: '' as '' | '0' | '1',
@@ -849,22 +871,50 @@ function resolvePendingAssignOrderMonthForRequest(): string {
return dayjs().format('YYYY-MM') return dayjs().format('YYYY-MM')
} }
/** 待分配角标 count 请求:与列表同条件,且去掉其它顶部 Tab 残留(如默认「当天挂号」 */ /** 除顶部 Tab 专属条件外,与主列表共用的「更多筛选」参数(角标 count 需同步 */
function buildPendingAssignCountPayload(): Record<string, unknown> { function buildSharedDiagnosisFilterPayload(): Record<string, unknown> {
return buildTcmDiagnosisListRequestPayload({ return {
...formData, keyword: formData.keyword,
diagnosis_type: formData.diagnosis_type,
syndrome_type: formData.syndrome_type,
assistant_id: formData.assistant_id,
diagnosis_confirmed: formData.diagnosis_confirmed,
has_appointment: formData.has_appointment,
latest_appointment_start_date: formData.latest_appointment_start_date,
latest_appointment_end_date: formData.latest_appointment_end_date,
latest_appointment_channel_source: formData.latest_appointment_channel_source,
latest_assign_start_date: formData.latest_assign_start_date,
latest_assign_end_date: formData.latest_assign_end_date
}
}
/** 顶部 Tab 角标 count 请求:带上共用筛选,再叠加各 Tab 专属条件 */
function buildDateCountRequestPayload(overrides: Record<string, unknown> = {}): Record<string, unknown> {
return {
page_no: 1, page_no: 1,
page_size: 1, page_size: 1,
pending_assign: 1, ...buildSharedDiagnosisFilterPayload(),
appointment_date: '', appointment_date: '',
has_appointment: '',
pending_booking: '', pending_booking: '',
completed_appointment: '', completed_appointment: '',
latest_appointment_start_date: '', pending_assign: '',
latest_appointment_end_date: '', pending_assign_order_month: '',
latest_appointment_channel_source: '', pending_assign_keyword: '',
pending_assign_order_month: resolvePendingAssignOrderMonthForRequest() sort_unserved_days: '',
} as Record<string, unknown>) as Record<string, unknown> ...overrides
}
}
/** 待分配角标 count 请求:与列表同条件,且去掉其它顶部 Tab 残留(如默认「当天挂号」) */
function buildPendingAssignCountPayload(): Record<string, unknown> {
return buildTcmDiagnosisListRequestPayload(
buildDateCountRequestPayload({
pending_assign: 1,
has_appointment: '',
pending_assign_order_month: resolvePendingAssignOrderMonthForRequest(),
pending_assign_keyword: formData.pending_assign_keyword
}) as Record<string, unknown>
) as Record<string, unknown>
} }
const fetchTcmDiagnosisListsForPaging = (req: Record<string, unknown>) => const fetchTcmDiagnosisListsForPaging = (req: Record<string, unknown>) =>
@@ -893,6 +943,9 @@ function clearSecondaryFiltersWhenPendingAssignWideSearch() {
formData.latest_appointment_start_date = '' formData.latest_appointment_start_date = ''
formData.latest_appointment_end_date = '' formData.latest_appointment_end_date = ''
formData.latest_appointment_channel_source = '' formData.latest_appointment_channel_source = ''
formData.latest_assign_start_date = ''
formData.latest_assign_end_date = ''
formData.sort_unserved_days = ''
formData.pending_assign_order_month = '' formData.pending_assign_order_month = ''
activeTab.value = 'all' activeTab.value = 'all'
if (kw1 !== '') { if (kw1 !== '') {
@@ -994,14 +1047,26 @@ const onPendingAssignOrderMonthChange = async (val: string | null) => {
const fetchDateCounts = async () => { const fetchDateCounts = async () => {
try { try {
const [yesterday, dayBefore, today, tomorrow, dayAfter, all, noApt, doneVisit, pending] = await Promise.all([ const [yesterday, dayBefore, today, tomorrow, dayAfter, all, noApt, doneVisit, pending] = await Promise.all([
tcmDiagnosisLists({ appointment_date: yesterdayStr.value, page_no: 1, page_size: 1 }), tcmDiagnosisLists(
tcmDiagnosisLists({ appointment_date: dayBeforeStr.value, page_no: 1, page_size: 1 }), buildDateCountRequestPayload({ appointment_date: yesterdayStr.value, has_appointment: '' }) as any
tcmDiagnosisLists({ appointment_date: todayStr.value, page_no: 1, page_size: 1 }), ),
tcmDiagnosisLists({ appointment_date: tomorrowStr.value, page_no: 1, page_size: 1 }), tcmDiagnosisLists(
tcmDiagnosisLists({ appointment_date: dayAfterStr.value, page_no: 1, page_size: 1 }), buildDateCountRequestPayload({ appointment_date: dayBeforeStr.value, has_appointment: '' }) as any
tcmDiagnosisLists({ page_no: 1, page_size: 1 }), ),
tcmDiagnosisLists({ has_appointment: 0, page_no: 1, page_size: 1 }), tcmDiagnosisLists(
tcmDiagnosisLists({ completed_appointment: 1, page_no: 1, page_size: 1 }), buildDateCountRequestPayload({ appointment_date: todayStr.value, has_appointment: '' }) as any
),
tcmDiagnosisLists(
buildDateCountRequestPayload({ appointment_date: tomorrowStr.value, has_appointment: '' }) as any
),
tcmDiagnosisLists(
buildDateCountRequestPayload({ appointment_date: dayAfterStr.value, has_appointment: '' }) as any
),
tcmDiagnosisLists(buildDateCountRequestPayload() as any),
tcmDiagnosisLists(buildDateCountRequestPayload({ has_appointment: 0 }) as any),
tcmDiagnosisLists(
buildDateCountRequestPayload({ completed_appointment: 1, has_appointment: '' }) as any
),
tcmDiagnosisLists(buildPendingAssignCountPayload() as any) tcmDiagnosisLists(buildPendingAssignCountPayload() as any)
]) ])
dateCounts.value = { dateCounts.value = {
@@ -1135,6 +1200,11 @@ const clearLatestAppointmentFilters = () => {
formData.latest_appointment_channel_source = '' formData.latest_appointment_channel_source = ''
} }
const clearLatestAssignFilters = () => {
formData.latest_assign_start_date = ''
formData.latest_assign_end_date = ''
}
const hasLatestAppointmentFilter = () => const hasLatestAppointmentFilter = () =>
!!( !!(
formData.latest_appointment_start_date || formData.latest_appointment_start_date ||
@@ -1142,6 +1212,9 @@ const hasLatestAppointmentFilter = () =>
formData.latest_appointment_channel_source formData.latest_appointment_channel_source
) )
const hasLatestAssignFilter = () =>
!!(formData.latest_assign_start_date || formData.latest_assign_end_date)
const handleLatestAppointmentFilterChange = () => { const handleLatestAppointmentFilterChange = () => {
if (hasLatestAppointmentFilter()) { if (hasLatestAppointmentFilter()) {
formData.appointment_date = '' formData.appointment_date = ''
@@ -1154,6 +1227,27 @@ const handleLatestAppointmentFilterChange = () => {
doSearch() doSearch()
} }
const handleLatestAssignFilterChange = () => {
doSearch()
}
const handleTableSortChange = ({
prop,
order
}: {
prop: string
order: 'ascending' | 'descending' | null
}) => {
if (prop === 'unserved_days') {
formData.sort_unserved_days =
order === 'ascending' ? 'asc' : order === 'descending' ? 'desc' : ''
} else {
formData.sort_unserved_days = ''
}
pager.page = 1
getLists()
}
const latestAppointmentChannelText = (row: any) => { const latestAppointmentChannelText = (row: any) => {
const desc = String(row?.latest_appointment_channel_source_desc || '').trim() const desc = String(row?.latest_appointment_channel_source_desc || '').trim()
const raw = String(row?.latest_appointment_channel_source || '').trim() const raw = String(row?.latest_appointment_channel_source || '').trim()
@@ -1200,6 +1294,9 @@ const handleReset = () => {
formData.latest_appointment_start_date = '' formData.latest_appointment_start_date = ''
formData.latest_appointment_end_date = '' formData.latest_appointment_end_date = ''
formData.latest_appointment_channel_source = '' formData.latest_appointment_channel_source = ''
formData.latest_assign_start_date = ''
formData.latest_assign_end_date = ''
formData.sort_unserved_days = ''
formData.diagnosis_confirmed = '' formData.diagnosis_confirmed = ''
formData.appointment_date = '' formData.appointment_date = ''
formData.has_appointment = '' formData.has_appointment = ''
@@ -2298,6 +2395,11 @@ onUnmounted(() => {
max-width: 100%; max-width: 100%;
} }
.latest-assign-range {
width: 260px;
max-width: 100%;
}
.latest-appointment-channel { .latest-appointment-channel {
width: 170px; width: 170px;
} }
File diff suppressed because one or more lines are too long
@@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
namespace app\adminapi\controller\stats;
use app\adminapi\controller\BaseAdminController;
use app\adminapi\lists\stats\AutoAssignLogLists;
use app\adminapi\logic\stats\AutoAssignLogLogic;
/**
* 待分配诊单自动指派日志
*
* - GET stats.autoAssignLog/lists 日志列表
* - POST stats.autoAssignLog/rollback 批量回退已自动分配的医助
*/
class AutoAssignLogController extends BaseAdminController
{
public function lists()
{
return $this->dataLists(new AutoAssignLogLists());
}
public function rollback()
{
$ids = $this->request->post('ids', []);
if (!is_array($ids)) {
$ids = [$ids];
}
$result = AutoAssignLogLogic::rollback($ids, $this->adminId, $this->adminInfo);
if ($result === false) {
return $this->fail(AutoAssignLogLogic::getError());
}
$msg = sprintf('回退成功 %d 条', (int) ($result['success'] ?? 0));
$failed = (int) ($result['failed'] ?? 0);
if ($failed > 0) {
$msg .= sprintf(',跳过/失败 %d 条', $failed);
}
// show=0:由前端按 success/failed 明细提示,避免与部分失败警告重复
return $this->success($msg, $result, 1, 0);
}
}
@@ -94,6 +94,24 @@ class DiagnosisController extends BaseAdminController
return $this->fail(DiagnosisLogic::getError()); return $this->fail(DiagnosisLogic::getError());
} }
/**
* @notes 设置复诊接诊率统计起始偏移(业务订单 tab
*/
public function setRevisitSlotStartOffset()
{
$params = (new DiagnosisValidate())->post()->goCheck('setRevisitSlotStartOffset');
$ok = DiagnosisLogic::setRevisitSlotStartOffset(
(int) $params['id'],
(int) $params['revisit_slot_start_offset'],
$this->adminInfo
);
if (!$ok) {
return $this->fail(DiagnosisLogic::getError());
}
return $this->success('保存成功');
}
/** /**
* @notes 删除诊单 * @notes 删除诊单
* @return \think\response\Json * @return \think\response\Json
@@ -181,6 +181,20 @@ class PrescriptionOrderController extends BaseAdminController
return $this->success('保存成功'); return $this->success('保存成功');
} }
/**
* 修改关联处方服用参数(主方/辅方次数与开立天数)及订单服用天数
*/
public function patchPrescriptionUsage()
{
$params = (new PrescriptionOrderValidate())->post()->goCheck('patchPrescriptionUsage');
$ok = PrescriptionOrderLogic::patchPrescriptionUsage($params, $this->adminId, $this->adminInfo);
if (!$ok) {
return $this->fail(PrescriptionOrderLogic::getError());
}
return $this->success('保存成功');
}
public function auditPrescription() public function auditPrescription()
{ {
$params = (new PrescriptionOrderValidate())->post()->goCheck('auditPrescription'); $params = (new PrescriptionOrderValidate())->post()->goCheck('auditPrescription');
@@ -328,6 +342,20 @@ class PrescriptionOrderController extends BaseAdminController
return $this->success('', $result); return $this->success('', $result);
} }
/**
* 手工新增操作日志(可选同步调整处方/支付单审核状态)
*/
public function addLog()
{
$params = (new PrescriptionOrderValidate())->post()->goCheck('addLog');
$result = PrescriptionOrderLogic::addLog($params, $this->adminId, $this->adminInfo);
if ($result === false) {
return $this->fail(PrescriptionOrderLogic::getError());
}
return $this->success('日志已添加', $result);
}
/** /**
* 为「已发货」订单新增一条关联支付单,并重置支付单审核状态为待审核 * 为「已发货」订单新增一条关联支付单,并重置支付单审核状态为待审核
*/ */
@@ -3,6 +3,8 @@
namespace app\adminapi\lists\doctor; namespace app\adminapi\lists\doctor;
use app\adminapi\lists\BaseAdminDataLists; use app\adminapi\lists\BaseAdminDataLists;
use app\adminapi\logic\dept\DeptLogic;
use app\common\model\auth\AdminDept;
use app\common\model\DiagnosisViewRecord; use app\common\model\DiagnosisViewRecord;
use app\common\model\doctor\Appointment; use app\common\model\doctor\Appointment;
use app\common\model\tcm\Prescription; use app\common\model\tcm\Prescription;
@@ -75,6 +77,35 @@ class AppointmentLists extends BaseAdminDataLists implements ListsSearchInterfac
}); });
} }
/**
* 按部门筛选:接诊医生、诊单医助或挂号医助所属部门命中子树即可(选父级含子级)
*
* @param mixed $query
*/
private function applyAssistantDeptIdFilter($query): void
{
if (!isset($this->params['assistant_dept_id']) || $this->params['assistant_dept_id'] === '' || (int) $this->params['assistant_dept_id'] <= 0) {
return;
}
$rootDeptId = (int) $this->params['assistant_dept_id'];
$deptIds = DeptLogic::getSelfAndDescendantIds($rootDeptId);
$deptIds = array_values(array_filter(array_map('intval', $deptIds), static function (int $id): bool {
return $id > 0;
}));
if ($deptIds === []) {
$query->whereRaw('0 = 1');
return;
}
$inList = implode(',', $deptIds);
$adTbl = (new AdminDept())->getTable();
$query->whereRaw(
"(EXISTS (SELECT 1 FROM `{$adTbl}` ad WHERE ad.`admin_id` = a.`doctor_id` AND ad.`dept_id` IN ({$inList}))"
. " OR EXISTS (SELECT 1 FROM `{$adTbl}` ad WHERE ad.`admin_id` = u.`assistant_id` AND ad.`dept_id` IN ({$inList}))"
. " OR EXISTS (SELECT 1 FROM `{$adTbl}` ad WHERE ad.`admin_id` = a.`assistant_id` AND ad.`dept_id` IN ({$inList})))"
);
}
/** /**
* 渠道筛选:与 AppointmentLogic 一致,兼容仅有 channel_source、仅有 channels、或两者皆有的表结构 * 渠道筛选:与 AppointmentLogic 一致,兼容仅有 channel_source、仅有 channels、或两者皆有的表结构
* *
@@ -191,6 +222,8 @@ class AppointmentLists extends BaseAdminDataLists implements ListsSearchInterfac
$this->applyAssistantIdFilter($query); $this->applyAssistantIdFilter($query);
$this->applyAssistantDeptIdFilter($query);
$this->applyChannelSourceFilter($query, $chFilter); $this->applyChannelSourceFilter($query, $chFilter);
// 是否确认诊单:1=已确认 0=未确认 // 是否确认诊单:1=已确认 0=未确认
@@ -373,6 +406,8 @@ class AppointmentLists extends BaseAdminDataLists implements ListsSearchInterfac
$this->applyAssistantIdFilter($query); $this->applyAssistantIdFilter($query);
$this->applyAssistantDeptIdFilter($query);
$this->applyChannelSourceFilter($query, $chFilter); $this->applyChannelSourceFilter($query, $chFilter);
if ((int) ($this->params['exclude_cancelled'] ?? 0) === 1) { if ((int) ($this->params['exclude_cancelled'] ?? 0) === 1) {
@@ -0,0 +1,147 @@
<?php
declare(strict_types=1);
namespace app\adminapi\lists\stats;
use app\adminapi\lists\BaseAdminDataLists;
use app\common\lists\ListsSearchInterface;
use think\facade\Db;
/**
* 待分配诊单自动指派日志列表
*
* 数据来源:tcm_diagnosis_auto_assign_log(定时命令 tcm:auto-assign-pending 写入)
* 筛选:run_date / create_time 区间 / action / keyword / rollback 状态
*/
class AutoAssignLogLists extends BaseAdminDataLists implements ListsSearchInterface
{
private const TIER_LABELS = [
'gt70' => '>70%',
'60_70' => '60%~70%',
'50_60' => '50%~60%',
];
/**
* @notes 设置搜索条件
*/
public function setSearch(): array
{
return [
'=' => ['run_date', 'action', 'assistant_id', 'batch_no', 'stat_month'],
];
}
/**
* @notes 获取列表
*/
public function lists(): array
{
$rows = $this->buildQuery()
->order(['id' => 'desc'])
->limit($this->limitOffset, $this->limitLength)
->select()
->toArray();
foreach ($rows as &$row) {
$ct = (int) ($row['create_time'] ?? 0);
$row['create_time_text'] = $ct > 0 ? date('Y-m-d H:i:s', $ct) : '';
$row['action_text'] = (int) ($row['action'] ?? 0) === 1 ? '已分配' : '未分配';
$row['tier_text'] = self::TIER_LABELS[(string) ($row['tier'] ?? '')] ?? '';
$row['visit2_rate'] = $row['visit2_rate'] !== null ? (float) $row['visit2_rate'] : null;
$rbTs = (int) ($row['rollback_time'] ?? 0);
$row['rollback_time_text'] = $rbTs > 0 ? date('Y-m-d H:i:s', $rbTs) : '';
$row['is_rollback'] = $rbTs > 0 ? 1 : 0;
$row['can_rollback'] = ((int) ($row['action'] ?? 0) === 1 && $rbTs === 0) ? 1 : 0;
if ($rbTs > 0) {
$row['rollback_status_text'] = '已回退'
. (($row['rollback_admin_name'] ?? '') !== '' ? (' · ' . $row['rollback_admin_name']) : '');
} else {
$row['rollback_status_text'] = ((int) ($row['action'] ?? 0) === 1) ? '可回退' : '—';
}
}
unset($row);
return $rows;
}
/**
* @notes 获取数量
*/
public function count(): int
{
return $this->buildQuery()->count();
}
/**
* @return \think\db\Query
*/
private function buildQuery()
{
$query = Db::name('tcm_diagnosis_auto_assign_log')->where($this->searchWhere);
$keyword = trim((string) ($this->params['keyword'] ?? ''));
if ($keyword !== '') {
$query->where(function ($q) use ($keyword) {
$q->whereLike('patient_name', '%' . $keyword . '%')
->whereOr('patient_phone', 'like', '%' . $keyword . '%')
->whereOr('assistant_name', 'like', '%' . $keyword . '%');
if (preg_match('/^\d+$/', $keyword) === 1 && (int) $keyword > 0) {
$q->whereOr('diagnosis_id', '=', (int) $keyword);
}
});
}
$startDate = trim((string) ($this->params['start_date'] ?? ''));
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $startDate) === 1) {
$query->where('run_date', '>=', $startDate);
}
$endDate = trim((string) ($this->params['end_date'] ?? ''));
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $endDate) === 1) {
$query->where('run_date', '<=', $endDate);
}
// 记录时间(create_time)区间:支持 Y-m-d 或 Y-m-d H:i:s
$startTs = $this->parseTimeParam((string) ($this->params['start_time'] ?? ''), false);
if ($startTs > 0) {
$query->where('create_time', '>=', $startTs);
}
$endTs = $this->parseTimeParam((string) ($this->params['end_time'] ?? ''), true);
if ($endTs > 0) {
$query->where('create_time', '<=', $endTs);
}
$rollback = trim((string) ($this->params['is_rollback'] ?? ''));
if ($rollback === '1') {
$query->where('rollback_time', '>', 0);
} elseif ($rollback === '0') {
$query->where('rollback_time', '=', 0);
}
return $query;
}
/**
* @param bool $endOfDay 仅日期时是否取当天结束
*/
private function parseTimeParam(string $raw, bool $endOfDay): int
{
$raw = trim($raw);
if ($raw === '') {
return 0;
}
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $raw) === 1) {
return (int) strtotime($raw . ($endOfDay ? ' 23:59:59' : ' 00:00:00'));
}
if (preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}(:\d{2})?$/', $raw) === 1) {
if (preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/', $raw) === 1) {
$raw .= $endOfDay ? ':59' : ':00';
}
return (int) strtotime($raw);
}
return 0;
}
}
@@ -140,6 +140,7 @@ class DiagnosisLists extends BaseAdminDataLists implements ListsSearchInterface
} }
$this->applyLatestAppointmentFilters($query, $pendingWideSearch); $this->applyLatestAppointmentFilters($query, $pendingWideSearch);
$this->applyLatestAssignFilters($query, $pendingWideSearch);
$this->applyPendingAssignBusinessOrderMonthFilter($query); $this->applyPendingAssignBusinessOrderMonthFilter($query);
@@ -167,30 +168,7 @@ class DiagnosisLists extends BaseAdminDataLists implements ListsSearchInterface
} }
} }
// 按挂号状态优先级排序:已过号(4) > 已预约(1) > 已完成(3),然后按挂号日期+时间升序 $orderRaw = $this->resolveListOrderRaw($pendingWideSearch);
// 若传了 appointment_date(当天/明天等筛选),只按「该日」的挂号排序与展示
$diagTbl = (new Diagnosis())->getTable();
$aptTbl = (new Appointment())->getTable();
$minAptDateCond = '';
if (!$pendingWideSearch && !empty($this->params['appointment_date'])) {
$sortAptDate = addslashes((string) $this->params['appointment_date']);
$minAptDateCond = " AND apt.appointment_date = '{$sortAptDate}'";
}
// 获取最早的挂号状态(用于排序优先级)
$minAptStatusExpr = '(SELECT apt.status FROM ' . $aptTbl . ' apt WHERE apt.patient_id = ' . $diagTbl . '.id AND apt.status IN (1,3,4)' . $minAptDateCond . ' ORDER BY CASE apt.status WHEN 4 THEN 1 WHEN 1 THEN 2 WHEN 3 THEN 3 ELSE 4 END, apt.appointment_date ASC, apt.appointment_time ASC LIMIT 1)';
// 获取最早的挂号时间(用于同状态内排序)
$minAptExpr = '(SELECT MIN(CONCAT(apt.appointment_date, \' \', IFNULL(NULLIF(TRIM(apt.appointment_time), \'\'), \'00:00:00\'))) FROM ' . $aptTbl . ' apt WHERE apt.patient_id = ' . $diagTbl . '.id AND apt.status IN (1,3,4)' . $minAptDateCond . ')';
// 「已完成」Tab:按最近一条「已完成」(status=3) 挂号日期+时间降序…
$isCompletedTab = !$pendingWideSearch && isset($this->params['completed_appointment']) && (string) $this->params['completed_appointment'] === '1';
if ($isCompletedTab) {
$maxCompletedAptExpr = '(SELECT MAX(CONCAT(apt.appointment_date, \' \', IFNULL(NULLIF(TRIM(apt.appointment_time), \'\'), \'00:00:00\'))) FROM ' . $aptTbl . ' apt WHERE apt.patient_id = ' . $diagTbl . '.id AND apt.status = 3' . $minAptDateCond . ')';
$orderRaw = $diagTbl . '.assign_read_at IS NULL DESC, IFNULL(' . $maxCompletedAptExpr . ", '1970-01-01 00:00:00') DESC, {$diagTbl}.id DESC";
} else {
$orderRaw = $diagTbl . '.assign_read_at IS NULL DESC, CASE IFNULL(' . $minAptStatusExpr . ', 999) WHEN 4 THEN 1 WHEN 1 THEN 2 WHEN 3 THEN 3 ELSE 4 END ASC, IFNULL(' . $minAptExpr . ", '9999-12-31 23:59:59') ASC, {$diagTbl}.id DESC";
}
$lists = $query $lists = $query
->with(['DiagnosisViewRecord']) ->with(['DiagnosisViewRecord'])
@@ -571,6 +549,7 @@ class DiagnosisLists extends BaseAdminDataLists implements ListsSearchInterface
} }
$this->applyLatestAppointmentFilters($query, $pendingWideSearch); $this->applyLatestAppointmentFilters($query, $pendingWideSearch);
$this->applyLatestAssignFilters($query, $pendingWideSearch);
// 仅已开方(待分配+关键词检索时不限制) // 仅已开方(待分配+关键词检索时不限制)
if (!$pendingWideSearch && isset($this->params['only_has_prescription']) && (string) $this->params['only_has_prescription'] === '1') { if (!$pendingWideSearch && isset($this->params['only_has_prescription']) && (string) $this->params['only_has_prescription'] === '1') {
@@ -644,6 +623,99 @@ class DiagnosisLists extends BaseAdminDataLists implements ListsSearchInterface
$query->whereExists("SELECT 1 FROM {$aptTbl} latest_apt WHERE " . implode(' AND ', $conditions)); $query->whereExists("SELECT 1 FROM {$aptTbl} latest_apt WHERE " . implode(' AND ', $conditions));
} }
/**
* 最近一次成功指派过滤:按 create_time DESC, id DESC to_assistant_id>0 的一条。
*
* @param mixed $query
*/
private function applyLatestAssignFilters($query, bool $pendingWideSearch): void
{
if ($pendingWideSearch) {
return;
}
$startDate = $this->normalizeYmd($this->params['latest_assign_start_date'] ?? '');
$endDate = $this->normalizeYmd($this->params['latest_assign_end_date'] ?? '');
if ($startDate === '' && $endDate === '') {
return;
}
$logTbl = Db::name('tcm_diagnosis_assign_log')->getTable();
$diagTbl = (new Diagnosis())->getTable();
$latestIdSql = $this->latestAssignLogIdSubSql($logTbl, $diagTbl);
$conditions = ["latest_lg.id = ({$latestIdSql})"];
if ($startDate !== '') {
$startTs = (int) strtotime($startDate . ' 00:00:00');
$conditions[] = "latest_lg.create_time >= {$startTs}";
}
if ($endDate !== '') {
$endTs = (int) strtotime($endDate . ' 23:59:59');
$conditions[] = "latest_lg.create_time <= {$endTs}";
}
$query->whereExists("SELECT 1 FROM {$logTbl} latest_lg WHERE " . implode(' AND ', $conditions));
}
private function latestAssignLogIdSubSql(string $logTbl, string $diagTbl): string
{
return "SELECT lg_latest.id FROM {$logTbl} lg_latest "
. "WHERE lg_latest.diagnosis_id = {$diagTbl}.id "
. 'AND lg_latest.to_assistant_id > 0 '
. 'ORDER BY lg_latest.create_time DESC, lg_latest.id DESC LIMIT 1';
}
/**
* 列表默认排序;支持 sort_unserved_days=asc|desc 按未服务天数排序。
*/
private function resolveListOrderRaw(bool $pendingWideSearch): string
{
$diagTbl = (new Diagnosis())->getTable();
$sortUnserved = strtolower(trim((string) ($this->params['sort_unserved_days'] ?? '')));
if (in_array($sortUnserved, ['asc', 'desc'], true)) {
$anchorExpr = $this->unservedAnchorExpr($diagTbl);
$nullLast = "CASE WHEN IFNULL({$anchorExpr}, 0) = 0 THEN 1 ELSE 0 END ASC";
if ($sortUnserved === 'desc') {
return "{$nullLast}, IFNULL({$anchorExpr}, 0) ASC, {$diagTbl}.id DESC";
}
return "{$nullLast}, IFNULL({$anchorExpr}, 0) DESC, {$diagTbl}.id DESC";
}
$aptTbl = (new Appointment())->getTable();
$minAptDateCond = '';
if (!$pendingWideSearch && !empty($this->params['appointment_date'])) {
$sortAptDate = addslashes((string) $this->params['appointment_date']);
$minAptDateCond = " AND apt.appointment_date = '{$sortAptDate}'";
}
$minAptStatusExpr = '(SELECT apt.status FROM ' . $aptTbl . ' apt WHERE apt.patient_id = ' . $diagTbl . '.id AND apt.status IN (1,3,4)' . $minAptDateCond . ' ORDER BY CASE apt.status WHEN 4 THEN 1 WHEN 1 THEN 2 WHEN 3 THEN 3 ELSE 4 END, apt.appointment_date ASC, apt.appointment_time ASC LIMIT 1)';
$minAptExpr = '(SELECT MIN(CONCAT(apt.appointment_date, \' \', IFNULL(NULLIF(TRIM(apt.appointment_time), \'\'), \'00:00:00\'))) FROM ' . $aptTbl . ' apt WHERE apt.patient_id = ' . $diagTbl . '.id AND apt.status IN (1,3,4)' . $minAptDateCond . ')';
$isCompletedTab = !$pendingWideSearch && isset($this->params['completed_appointment']) && (string) $this->params['completed_appointment'] === '1';
if ($isCompletedTab) {
$maxCompletedAptExpr = '(SELECT MAX(CONCAT(apt.appointment_date, \' \', IFNULL(NULLIF(TRIM(apt.appointment_time), \'\'), \'00:00:00\'))) FROM ' . $aptTbl . ' apt WHERE apt.patient_id = ' . $diagTbl . '.id AND apt.status = 3' . $minAptDateCond . ')';
return $diagTbl . '.assign_read_at IS NULL DESC, IFNULL(' . $maxCompletedAptExpr . ", '1970-01-01 00:00:00') DESC, {$diagTbl}.id DESC";
}
return $diagTbl . '.assign_read_at IS NULL DESC, CASE IFNULL(' . $minAptStatusExpr . ', 999) WHEN 4 THEN 1 WHEN 1 THEN 2 WHEN 3 THEN 3 ELSE 4 END ASC, IFNULL(' . $minAptExpr . ", '9999-12-31 23:59:59') ASC, {$diagTbl}.id DESC";
}
/** 未服务天数锚点:血糖/饮食/运动记录最近 record_date 的最大值 */
private function unservedAnchorExpr(string $diagTbl): string
{
$bloodTbl = (new BloodRecord())->getTable();
$dietTbl = (new DietRecord())->getTable();
$exerciseTbl = (new ExerciseRecord())->getTable();
return 'GREATEST('
. "COALESCE((SELECT MAX(br.record_date) FROM {$bloodTbl} br WHERE br.diagnosis_id = {$diagTbl}.id AND br.delete_time IS NULL), 0), "
. "COALESCE((SELECT MAX(dr.record_date) FROM {$dietTbl} dr WHERE dr.diagnosis_id = {$diagTbl}.id AND dr.delete_time IS NULL), 0), "
. "COALESCE((SELECT MAX(er.record_date) FROM {$exerciseTbl} er WHERE er.diagnosis_id = {$diagTbl}.id AND er.delete_time IS NULL), 0)"
. ')';
}
private function latestAppointmentIdSubSql(string $aptTbl, string $diagTbl): string private function latestAppointmentIdSubSql(string $aptTbl, string $diagTbl): string
{ {
$statuses = implode(',', self::EFFECTIVE_APPOINTMENT_STATUSES); $statuses = implode(',', self::EFFECTIVE_APPOINTMENT_STATUSES);
@@ -741,6 +741,10 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
} }
unset($item); unset($item);
if ($this->shouldBypassListVisibilityForDiagnosisEdit()) {
$this->appendDiagnosisEditVisitSeqFields($lists);
}
$this->appendPrescriptionOrderAssignSnapshotErCenterFlags($lists); $this->appendPrescriptionOrderAssignSnapshotErCenterFlags($lists);
if ((int) ($this->params['yeji_order_drawer'] ?? 0) === 1) { if ((int) ($this->params['yeji_order_drawer'] ?? 0) === 1) {
@@ -820,6 +824,11 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
'export_guahao_channel_source' => '自媒体渠道(挂号渠道来源)', 'export_guahao_channel_source' => '自媒体渠道(挂号渠道来源)',
'export_medication_form' => '药品形态', 'export_medication_form' => '药品形态',
'export_prescription_name' => '药方名称', 'export_prescription_name' => '药方名称',
'export_prescription_herbs' => '处方',
'export_main_usage' => '主方服用方式',
'export_main_usage_days' => '主方天数',
'export_aux_usage' => '辅方服用方式',
'export_aux_usage_days' => '辅方天数',
'export_service_package' => '服务套餐', 'export_service_package' => '服务套餐',
'export_medication_days' => '天数', 'export_medication_days' => '天数',
'export_amount' => '总金额', 'export_amount' => '总金额',
@@ -909,6 +918,9 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
$wrapWideKeys = [ $wrapWideKeys = [
'export_linked_pay_records', 'export_linked_pay_records',
'export_prescription_name', 'export_prescription_name',
'export_prescription_herbs',
'export_main_usage',
'export_aux_usage',
'export_guahao_channel_source', 'export_guahao_channel_source',
'export_assistant_dept', 'export_assistant_dept',
'export_service_package', 'export_service_package',
@@ -919,6 +931,8 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
'export_patient_gender' => 6, 'export_patient_gender' => 6,
'export_patient_age' => 6, 'export_patient_age' => 6,
'export_medication_days' => 6, 'export_medication_days' => 6,
'export_main_usage_days' => 8,
'export_aux_usage_days' => 8,
'export_amount' => 10, 'export_amount' => 10,
'export_paid_amount' => 10, 'export_paid_amount' => 10,
'export_refund_amount' => 10, 'export_refund_amount' => 10,
@@ -927,6 +941,9 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
'export_supply_mode' => 10, 'export_supply_mode' => 10,
'export_linked_pay_records' => 52, 'export_linked_pay_records' => 52,
'export_prescription_name' => 34, 'export_prescription_name' => 34,
'export_prescription_herbs' => 36,
'export_main_usage' => 28,
'export_aux_usage' => 28,
'export_guahao_channel_source' => 22, 'export_guahao_channel_source' => 22,
'export_assistant_dept' => 24, 'export_assistant_dept' => 24,
'export_service_package' => 18, 'export_service_package' => 18,
@@ -1404,6 +1421,69 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
); );
} }
/**
* 诊单编辑-业务订单 tab:标注全局诊次及是否计入复诊接诊率(与 RevisitRateLogic 同口径)
*
* @param array<int, array<string, mixed>> $lists
*/
private function appendDiagnosisEditVisitSeqFields(array &$lists): void
{
if ($lists === []) {
return;
}
$diagIds = [];
foreach ($lists as $row) {
$d = (int) ($row['diagnosis_id'] ?? 0);
if ($d > 0) {
$diagIds[$d] = true;
}
}
$contextDid = (int) ($this->params['context_diagnosis_id'] ?? 0);
if ($contextDid > 0) {
$diagIds[$contextDid] = true;
}
$diagIdList = array_keys($diagIds);
if ($diagIdList === []) {
return;
}
$offsetRows = Diagnosis::whereIn('id', $diagIdList)
->whereNull('delete_time')
->column('revisit_slot_start_offset', 'id');
$offsetMap = [];
foreach ($offsetRows as $id => $offset) {
$offsetMap[(int) $id] = max(0, min(20, (int) $offset));
}
/** @var array<int, int> $seqByOrderId order_id => global seq within diagnosis */
$seqByOrderId = [];
foreach ($diagIdList as $did) {
$q = PrescriptionOrder::where('diagnosis_id', $did)->whereNull('delete_time');
YejiStatsLogic::applyPrescriptionOrderNotCancelledForPerformanceQuery($q, '');
$orderIds = $q
->order(['create_time' => 'asc', 'id' => 'asc'])
->column('id');
$seq = 0;
foreach ($orderIds as $oid) {
$seq++;
$seqByOrderId[(int) $oid] = $seq;
}
}
foreach ($lists as &$row) {
$oid = (int) ($row['id'] ?? 0);
$did = (int) ($row['diagnosis_id'] ?? 0);
$seq = (int) ($seqByOrderId[$oid] ?? 0);
$offset = (int) ($offsetMap[$did] ?? 0);
$effectiveSlot = $seq > 0 ? $seq + $offset : 0;
$row['global_visit_seq'] = $effectiveSlot > 0 ? $effectiveSlot : null;
$row['raw_visit_seq'] = $seq > 0 ? $seq : null;
$row['counts_for_revisit_rate'] = $effectiveSlot >= 2 ? 1 : 0;
}
unset($row);
}
/** /**
* 列表行标注:指派日志快照(related_po_creator_id + related_po_create_time)是否指向本业务单, * 列表行标注:指派日志快照(related_po_creator_id + related_po_create_time)是否指向本业务单,
* 以及该次操作的新医助(to_assistant_id)是否归属「二中心」部门子树(与 DeptLogic / 业绩看板一致)。 * 以及该次操作的新医助(to_assistant_id)是否归属「二中心」部门子树(与 DeptLogic / 业绩看板一致)。
@@ -86,7 +86,6 @@ class AdminLogic extends BaseLogic
'enable_image_consult' => $params['enable_image_consult'] ?? 1, 'enable_image_consult' => $params['enable_image_consult'] ?? 1,
'enable_video_consult' => $params['enable_video_consult'] ?? 1, 'enable_video_consult' => $params['enable_video_consult'] ?? 1,
'enable_charge' => $params['enable_charge'] ?? 0, 'enable_charge' => $params['enable_charge'] ?? 0,
'enable_beauty' => $params['enable_beauty'] ?? 1,
]); ]);
// 角色 // 角色
@@ -152,7 +151,6 @@ class AdminLogic extends BaseLogic
'enable_image_consult' => $params['enable_image_consult'] ?? 1, 'enable_image_consult' => $params['enable_image_consult'] ?? 1,
'enable_video_consult' => $params['enable_video_consult'] ?? 1, 'enable_video_consult' => $params['enable_video_consult'] ?? 1,
'enable_charge' => $params['enable_charge'] ?? 0, 'enable_charge' => $params['enable_charge'] ?? 0,
'enable_beauty' => $params['enable_beauty'] ?? 1,
]; ];
// 头像 // 头像
@@ -288,7 +286,7 @@ class AdminLogic extends BaseLogic
'gender', 'age', 'phone', 'title', 'department', 'gender', 'age', 'phone', 'title', 'department',
'specialty', 'education', 'experience', 'honors', 'specialty', 'education', 'experience', 'honors',
'license_no', 'qualification_images', 'enable_image_consult', 'enable_video_consult', 'enable_charge', 'license_no', 'qualification_images', 'enable_image_consult', 'enable_video_consult', 'enable_charge',
'enable_beauty', 'work_wechat_userid' 'work_wechat_userid'
])->findOrEmpty($params['id'])->toArray(); ])->findOrEmpty($params['id'])->toArray();
// 将资质图片JSON字符串转换为数组,供前端组件使用 // 将资质图片JSON字符串转换为数组,供前端组件使用
+32 -1
View File
@@ -151,6 +151,24 @@ class DeptLogic extends BaseLogic
* @return list<int> * @return list<int>
*/ */
private static function findErCenterRootDeptIds(): array private static function findErCenterRootDeptIds(): array
{
return self::findCenterRootDeptIdsByNameKeyword('二中心');
}
/**
* 名称含「一中心」的部门 id。
*
* @return list<int>
*/
private static function findYiCenterRootDeptIds(): array
{
return self::findCenterRootDeptIdsByNameKeyword('一中心');
}
/**
* @return list<int>
*/
private static function findCenterRootDeptIdsByNameKeyword(string $keyword): array
{ {
$rows = Dept::whereNull('delete_time') $rows = Dept::whereNull('delete_time')
->field(['id', 'name']) ->field(['id', 'name'])
@@ -159,7 +177,7 @@ class DeptLogic extends BaseLogic
$out = []; $out = [];
foreach ($rows as $r) { foreach ($rows as $r) {
$name = (string) ($r['name'] ?? ''); $name = (string) ($r['name'] ?? '');
if ($name !== '' && mb_strpos($name, '二中心') !== false) { if ($name !== '' && mb_strpos($name, $keyword) !== false) {
$out[] = (int) $r['id']; $out[] = (int) $r['id'];
} }
} }
@@ -201,6 +219,19 @@ class DeptLogic extends BaseLogic
return array_fill_keys($subtreeIds, true); return array_fill_keys($subtreeIds, true);
} }
/**
* 名称含「一中心」的部门及其全部下级 idmap)。
*
* @return array<int, true>
*/
public static function getYiCenterSubtreeDeptIdSet(): array
{
$yiRoots = self::findYiCenterRootDeptIds();
$subtreeIds = self::unionErCenterSubtreeDeptIds($yiRoots);
return array_fill_keys($subtreeIds, true);
}
/** /**
* 二中心复诊统计用的业务订单行(与 rollup 同源 SQL)。 * 二中心复诊统计用的业务订单行(与 rollup 同源 SQL)。
* *
@@ -0,0 +1,227 @@
<?php
declare(strict_types=1);
namespace app\adminapi\logic\stats;
use app\common\logic\BaseLogic;
use think\facade\Db;
use think\facade\Log;
/**
* 待分配诊单自动指派日志:回退已自动分配的医助
*/
class AutoAssignLogLogic extends BaseLogic
{
/**
* 批量回退:将诊单医助撤回到自动分配前的原医助(指派日志 from_assistant_id),并标记自动分配日志已回退。
*
* @param list<int|string> $ids 自动分配日志 id
* @return array{success:int,failed:int,messages:list<string>}|false
*/
public static function rollback(array $ids, int $adminId, array $adminInfo = []): array|false
{
$idList = array_values(array_unique(array_filter(array_map('intval', $ids), static fn (int $id) => $id > 0)));
if ($idList === []) {
self::setError('请选择要回退的记录');
return false;
}
$adminName = (string) ($adminInfo['name'] ?? '');
$adminAccount = (string) ($adminInfo['account'] ?? '');
$req = request();
$ip = (string) ($req->ip() ?? '');
$now = time();
$success = 0;
$failed = 0;
$messages = [];
foreach ($idList as $logId) {
try {
$ret = self::rollbackOne($logId, $adminId, $adminName, $adminAccount, $ip, $now);
if ($ret === true) {
$success++;
} else {
$failed++;
$messages[] = (string) $ret;
}
} catch (\Throwable $e) {
$failed++;
$messages[] = sprintf('日志#%d%s', $logId, $e->getMessage());
Log::warning('auto assign rollback failed: ' . $e->getMessage(), ['log_id' => $logId]);
}
}
if ($success === 0 && $failed > 0) {
self::setError($messages[0] ?? '回退失败');
return false;
}
return [
'success' => $success,
'failed' => $failed,
'messages' => $messages,
];
}
/**
* @return true|string true=成功,string=失败原因
*/
private static function rollbackOne(
int $logId,
int $adminId,
string $adminName,
string $adminAccount,
string $ip,
int $now
): bool|string {
Db::startTrans();
try {
$log = Db::name('tcm_diagnosis_auto_assign_log')
->where('id', $logId)
->lock(true)
->find();
if ($log === null || $log === []) {
Db::rollback();
return sprintf('日志#%d:记录不存在', $logId);
}
if ((int) ($log['action'] ?? 0) !== 1) {
Db::rollback();
return sprintf('日志#%d:仅「已分配」记录可回退', $logId);
}
if ((int) ($log['rollback_time'] ?? 0) > 0) {
Db::rollback();
return sprintf('日志#%d:已回退,勿重复操作', $logId);
}
$diagnosisId = (int) ($log['diagnosis_id'] ?? 0);
$assignedAssistantId = (int) ($log['assistant_id'] ?? 0);
if ($diagnosisId <= 0 || $assignedAssistantId <= 0) {
Db::rollback();
return sprintf('日志#%d:数据不完整,无法回退', $logId);
}
$diag = Db::name('tcm_diagnosis')
->where('id', $diagnosisId)
->whereNull('delete_time')
->lock(true)
->field(['id', 'assistant_id'])
->find();
if ($diag === null || $diag === []) {
Db::rollback();
return sprintf('日志#%d:诊单#%d 不存在', $logId, $diagnosisId);
}
$currentAssistantId = (int) ($diag['assistant_id'] ?? 0);
if ($currentAssistantId !== $assignedAssistantId) {
Db::rollback();
return sprintf(
'日志#%d:诊单#%d 当前医助已变更(非自动分配的医助),跳过回退',
$logId,
$diagnosisId
);
}
$prevAssistantId = self::resolvePreviousAssistantIdFromAutoAssign(
$diagnosisId,
$assignedAssistantId,
(int) ($log['create_time'] ?? 0)
);
$poSnap = Db::name('tcm_prescription_order')
->where('diagnosis_id', $diagnosisId)
->whereNull('delete_time')
->order(['create_time' => 'desc', 'id' => 'desc'])
->field(['creator_id', 'create_time'])
->find();
$relatedPoCreatorId = (int) ($poSnap['creator_id'] ?? 0);
$relatedPoCreateTime = (int) ($poSnap['create_time'] ?? 0);
if ($relatedPoCreateTime <= 0) {
$relatedPoCreateTime = $now;
$relatedPoCreatorId = 0;
}
Db::name('tcm_diagnosis')
->where('id', $diagnosisId)
->whereNull('delete_time')
->update([
'assistant_id' => $prevAssistantId,
'assign_read_at' => $prevAssistantId > 0 ? null : 0,
]);
Db::name('tcm_diagnosis_assign_log')->insert([
'diagnosis_id' => $diagnosisId,
'from_assistant_id' => $currentAssistantId,
'to_assistant_id' => $prevAssistantId,
'operator_admin_id' => $adminId,
'operator_name' => $adminName !== '' ? $adminName : '回退自动分配',
'operator_account' => $adminAccount,
'ip' => $ip,
'related_po_creator_id' => $relatedPoCreatorId,
'related_po_create_time' => $relatedPoCreateTime,
'is_inherit' => 0,
'create_time' => $now,
]);
Db::name('tcm_diagnosis_auto_assign_log')
->where('id', $logId)
->update([
'rollback_time' => $now,
'rollback_admin_id' => $adminId,
'rollback_admin_name' => $adminName,
]);
Db::commit();
return true;
} catch (\Throwable $e) {
Db::rollback();
throw $e;
}
}
/**
* 从「系统自动分配」指派日志取 from_assistant_id 作为回退目标。
*/
private static function resolvePreviousAssistantIdFromAutoAssign(
int $diagnosisId,
int $assignedAssistantId,
int $autoLogCreateTime
): int {
$query = Db::name('tcm_diagnosis_assign_log')
->where('diagnosis_id', $diagnosisId)
->where('to_assistant_id', $assignedAssistantId)
->where('operator_name', '系统自动分配');
if ($autoLogCreateTime > 0) {
$query->where('create_time', '>=', $autoLogCreateTime - 30)
->where('create_time', '<=', $autoLogCreateTime + 30);
}
$row = $query->order('id', 'desc')->field(['from_assistant_id'])->find();
if ($row !== null && $row !== []) {
return (int) ($row['from_assistant_id'] ?? 0);
}
// 兜底:不限时间窗再查最近一条系统自动分配
$fallback = Db::name('tcm_diagnosis_assign_log')
->where('diagnosis_id', $diagnosisId)
->where('to_assistant_id', $assignedAssistantId)
->where('operator_name', '系统自动分配')
->order('id', 'desc')
->field(['from_assistant_id'])
->find();
return (int) ($fallback['from_assistant_id'] ?? 0);
}
}
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace app\adminapi\logic\stats; namespace app\adminapi\logic\stats;
use app\adminapi\logic\dept\DeptLogic;
use think\facade\Db; use think\facade\Db;
/** /**
@@ -12,9 +13,11 @@ use think\facade\Db;
* 口径说明: * 口径说明:
* - 当月被指派总数 = 当月内 `tcm_diagnosis_assign_log`(按 **指派操作时间 lg.create_time** 落月,to_assistant_id>0 * - 当月被指派总数 = 当月内 `tcm_diagnosis_assign_log`(按 **指派操作时间 lg.create_time** 落月,to_assistant_id>0
* **剔除勾选「继承」的指派 is_inherit=1**,诊单未删除)去重后的「医助 × 诊单」组合; * **剔除勾选「继承」的指派 is_inherit=1**,诊单未删除)去重后的「医助 × 诊单」组合;
* 同一诊单当月被多次指派给同一医助只计 1 * 同一诊单当月被多次指派给同一医助只计 1
* **再剔除**名下存在履约「拒收(9) / 退款(10)」业务订单(未软删)的诊单。
* - N 次下单 = 诊单(患者)名下计入业绩的业务订单(剔除履约 4/9/10、软删)按 create_time 升序的**全局**序列中第 N 笔; * - N 次下单 = 诊单(患者)名下计入业绩的业务订单(剔除履约 4/9/10、软删)按 create_time 升序的**全局**序列中第 N 笔;
* 诊次**跨月累计不重置**:例如 5 月指派后旗下成交 4 单为二诊~五诊,6 月再成交即为六诊。 * 诊次**跨月累计不重置**:例如 5 月指派后旗下成交 4 单为二诊~五诊,6 月再成交即为六诊。
* 诊单可配置 `revisit_slot_start_offset`(默认 0:第 1 笔实单计为一诊;设为 1 则第 1 笔实单计为二诊;设为 2 则计为三诊,即在实单序号上叠加偏移,5 笔实单+偏移 2 等价于计至七诊)。
* - 当月 N 诊单数 = **当月内下单**且全局序号为 N 的订单数,归属下单时点的**持有医助**—— * - 当月 N 诊单数 = **当月内下单**且全局序号为 N 的订单数,归属下单时点的**持有医助**——
* 按指派日志时间线取「订单时间之前最近一次指派」的 to_assistant_id(释放 to=0 即不再归属; * 按指派日志时间线取「订单时间之前最近一次指派」的 to_assistant_id(释放 to=0 即不再归属;
* 「继承」指派会转移持有人用于归属,但不计被指派数)。指派可发生在往月。 * 「继承」指派会转移持有人用于归属,但不计被指派数)。指派可发生在往月。
@@ -22,7 +25,8 @@ use think\facade\Db;
* 医助当月无新指派但旗下有成交时,被指派数为 0、比率显示为空。 * 医助当月无新指派但旗下有成交时,被指派数为 0、比率显示为空。
* - 分档动态产出:N 2 起,至当月命中数据的最大序号(至少展示到四诊,上限 MAX_VISIT_SLOT 防御异常数据), * - 分档动态产出:N 2 起,至当月命中数据的最大序号(至少展示到四诊,上限 MAX_VISIT_SLOT 防御异常数据),
* 返回 `slots` 列表供前端动态渲染「五诊」「六诊」… 列。 * 返回 `slots` 列表供前端动态渲染「五诊」「六诊」… 列。
* - 部门归类:医助按其人事部门(admin_dept 最小 dept_id)归组;部门筛选(dept_ids,含组织下级)按该归属部门过滤。 * - 部门归类:医助按其人事部门(admin_dept 最小 dept_id)归组;**仅统计「二中心」及其组织下级**(与 DeptLogic::getErCenterSubtreeDeptIdSet 一致);
* 部门筛选下拉与未选部门时的默认范围均限定在该子树内,选定部门时含其组织下级。
* - 部门行 / 合计行:被指派数按诊单去重(可能小于下级行相加);N 诊单数为下级行求和(每笔订单唯一归属一名医助)。 * - 部门行 / 合计行:被指派数按诊单去重(可能小于下级行相加);N 诊单数为下级行求和(每笔订单唯一归属一名医助)。
*/ */
class RevisitRateLogic class RevisitRateLogic
@@ -294,14 +298,19 @@ class RevisitRateLogic
} }
/** /**
* 部门下拉(全量未删除部门,前端组树)。 * 部门下拉:仅「二中心」及其组织下级(与业绩看板 ErCenter 子树一致)。
* *
* @return array{rows: list<array{id:int,pid:int,name:string}>} * @return array{rows: list<array{id:int,pid:int,name:string}>}
*/ */
public static function deptOptions(): array public static function deptOptions(): array
{ {
$erSet = DeptLogic::getErCenterSubtreeDeptIdSet();
if ($erSet === []) {
return ['rows' => []];
}
$rows = Db::name('dept') $rows = Db::name('dept')
->whereNull('delete_time') ->whereNull('delete_time')
->whereIn('id', array_keys($erSet))
->field(['id', 'pid', 'name']) ->field(['id', 'pid', 'name'])
->order('sort', 'desc') ->order('sort', 'desc')
->order('id', 'asc') ->order('id', 'asc')
@@ -322,9 +331,9 @@ class RevisitRateLogic
/** /**
* 核心统计上下文: * 核心统计上下文:
* 1. 全量指派日志(≤ 月末)构建持有时间线; * 1. 全量指派日志(≤ 月末)构建持有时间线;
* 2. 分母:当月非继承指派的「医助 × 诊单」; * 2. 分母:当月非继承指派的「医助 × 诊单」,再剔除名下存在拒收(9)/退款(10) 订单的诊单
* 3. 分子:曾被指派诊单的当月订单按全局序号 ≥2 归属持有医助 * 3. 分子:曾被指派诊单的当月订单,统计诊次 = 实单序号 + 诊单偏移(默认第 1 笔实单为一诊)
* 4. 应用部门筛选(含组织下级)。 * 4. 应用部门筛选:默认限定「二中心」子树;选定部门时再收窄到该部门及其下级(且须落在二中心子树内)。
* *
* @param array{month?:string,dept_ids?:int[]|string} $params * @param array{month?:string,dept_ids?:int[]|string} $params
* *
@@ -375,9 +384,35 @@ class RevisitRateLogic
} }
} }
// 分子:曾被指派诊单的当月订单(全局序号 ≥2),归属下单时点的持有医助 // 分母:剔除名下存在拒收(9)/退款(10) 业务订单的诊单(与明细 assignLines 同口径)
$assignedDiagIds = [];
foreach ($diagsByAssistant as $diagSet) {
foreach ($diagSet as $did => $_) {
$assignedDiagIds[(int) $did] = true;
}
}
$refundRejectDiagSet = self::fetchRefundOrRejectDiagnosisSet(array_keys($assignedDiagIds));
if ($refundRejectDiagSet !== []) {
foreach ($diagsByAssistant as $aid => $diagSet) {
foreach ($diagSet as $did => $_) {
if (isset($refundRejectDiagSet[$did])) {
unset($diagsByAssistant[$aid][$did]);
}
}
if ($diagsByAssistant[$aid] === []) {
unset($diagsByAssistant[$aid]);
}
}
$pairsRaw = array_values(array_filter(
$pairsRaw,
static fn (array $p): bool => !isset($refundRejectDiagSet[(int) $p['diagnosis_id']])
));
}
// 分子:曾被指派诊单的当月订单,统计诊次 = 实单全局序号 + 诊单偏移(默认偏移 0 → 第 1 笔实单为一诊)
/** @var array<int, array<int, list<array<string, mixed>>>> $slotOrdersByAssistant */ /** @var array<int, array<int, list<array<string, mixed>>>> $slotOrdersByAssistant */
$slotOrdersByAssistant = []; $slotOrdersByAssistant = [];
$offsetMap = self::fetchRevisitSlotStartOffsetMap(array_keys($candidateDiagSet));
foreach (array_chunk(array_keys($candidateDiagSet), 2000) as $chunk) { foreach (array_chunk(array_keys($candidateDiagSet), 2000) as $chunk) {
$orderRows = self::fetchOrderSeqRows( $orderRows = self::fetchOrderSeqRows(
$chunk, $chunk,
@@ -387,6 +422,7 @@ class RevisitRateLogic
$seq = 0; $seq = 0;
$ptr = 0; $ptr = 0;
$holder = 0; $holder = 0;
$offset = 0;
foreach ($orderRows as $r) { foreach ($orderRows as $r) {
$did = (int) ($r['diagnosis_id'] ?? 0); $did = (int) ($r['diagnosis_id'] ?? 0);
if ($did <= 0) { if ($did <= 0) {
@@ -397,8 +433,10 @@ class RevisitRateLogic
$seq = 0; $seq = 0;
$ptr = 0; $ptr = 0;
$holder = 0; $holder = 0;
$offset = self::resolveRevisitSlotStartOffset($did, $offsetMap);
} }
$seq++; $seq++;
$effectiveSlot = $seq + $offset;
$ct = (int) ($r['create_time'] ?? 0); $ct = (int) ($r['create_time'] ?? 0);
// 推进时间线指针:订单时间之前(含同刻)最近一次指派的持有人 // 推进时间线指针:订单时间之前(含同刻)最近一次指派的持有人
$tl = $timeline[$did] ?? []; $tl = $timeline[$did] ?? [];
@@ -407,24 +445,27 @@ class RevisitRateLogic
$holder = (int) $tl[$ptr]['to']; $holder = (int) $tl[$ptr]['to'];
$ptr++; $ptr++;
} }
if ($seq < 2 || $seq > self::MAX_VISIT_SLOT) { if ($effectiveSlot < 2 || $effectiveSlot > self::MAX_VISIT_SLOT) {
continue; continue;
} }
if ($ct < $startTs || $ct > $endTs) { if ($ct < $startTs || $ct > $endTs) {
continue; continue;
} }
if ($holder > 0) { if ($holder > 0) {
$slotOrdersByAssistant[$holder][$seq][] = $r; $slotOrdersByAssistant[$holder][$effectiveSlot][] = $r;
} }
} }
} }
// 医助归属部门 + 部门筛选(含组织下级) // 医助归属部门 + 部门筛选(默认仅二中心子树;选定部门时再收窄,含组织下级)
$universeIds = array_keys($diagsByAssistant + $slotOrdersByAssistant); $universeIds = array_keys($diagsByAssistant + $slotOrdersByAssistant);
[$assistantDept, $deptNames] = self::buildAssistantDeptIndex($universeIds); [$assistantDept, $deptNames] = self::buildAssistantDeptIndex($universeIds);
$deptFilterIds = self::parseDeptIds($params['dept_ids'] ?? null); $subtreeSet = self::resolveDeptFilterSet($params['dept_ids'] ?? null);
if ($deptFilterIds !== []) { if ($subtreeSet === []) {
$subtreeSet = self::expandDeptSubtreeSet($deptFilterIds); // 无二中心部门时整表为空,避免误展示其它中心数据
$diagsByAssistant = [];
$slotOrdersByAssistant = [];
} else {
foreach ($universeIds as $aid) { foreach ($universeIds as $aid) {
$deptId = (int) ($assistantDept[$aid] ?? 0); $deptId = (int) ($assistantDept[$aid] ?? 0);
if ($deptId <= 0 || !isset($subtreeSet[$deptId])) { if ($deptId <= 0 || !isset($subtreeSet[$deptId])) {
@@ -543,6 +584,45 @@ class RevisitRateLogic
return [$canonical, $names]; return [$canonical, $names];
} }
/**
* 部门筛选集合:始终落在「二中心」子树内。
* - 未传 dept_ids:整棵二中心子树
* - 已传:所选部门及其下级 二中心子树(非法/非二中心 id 被忽略)
*
* @param mixed $raw
*
* @return array<int, true>
*/
private static function resolveDeptFilterSet(mixed $raw): array
{
$erSet = DeptLogic::getErCenterSubtreeDeptIdSet();
if ($erSet === []) {
return [];
}
$deptFilterIds = self::parseDeptIds($raw);
if ($deptFilterIds === []) {
return $erSet;
}
$allowedRoots = [];
foreach ($deptFilterIds as $id) {
if (isset($erSet[$id])) {
$allowedRoots[] = $id;
}
}
if ($allowedRoots === []) {
return [];
}
$expanded = self::expandDeptSubtreeSet($allowedRoots);
$out = [];
foreach ($expanded as $id => $_) {
if (isset($erSet[$id])) {
$out[$id] = true;
}
}
return $out;
}
/** /**
* @param mixed $raw int[] | 逗号分隔字符串 * @param mixed $raw int[] | 逗号分隔字符串
* *
@@ -596,6 +676,35 @@ class RevisitRateLogic
return $set; return $set;
} }
/**
* 名下存在履约「拒收(9) / 退款(10)」业务订单(未软删)的诊单集合。
* 用于「当月被指派总数」分母过滤;不限订单创建月份。
*
* @param list<int> $diagIds
*
* @return array<int, true>
*/
private static function fetchRefundOrRejectDiagnosisSet(array $diagIds): array
{
if ($diagIds === []) {
return [];
}
$out = [];
foreach (array_chunk($diagIds, 2000) as $chunk) {
$ids = Db::name('tcm_prescription_order')
->whereIn('diagnosis_id', $chunk)
->whereNull('delete_time')
->whereIn('fulfillment_status', [9, 10])
->group('diagnosis_id')
->column('diagnosis_id');
foreach ($ids as $id) {
$out[(int) $id] = true;
}
}
return $out;
}
/** /**
* 诊单订单序列源查询(与业绩口径一致),统一排序保证序号稳定。 * 诊单订单序列源查询(与业绩口径一致),统一排序保证序号稳定。
* *
@@ -619,6 +728,48 @@ class RevisitRateLogic
->toArray(); ->toArray();
} }
/**
* @param list<int> $diagIds
*
* @return array<int, int> diagnosis_id => revisit_slot_start_offset
*/
private static function fetchRevisitSlotStartOffsetMap(array $diagIds): array
{
if ($diagIds === []) {
return [];
}
$out = [];
foreach (array_chunk($diagIds, 2000) as $chunk) {
$rows = Db::name('tcm_diagnosis')
->whereIn('id', $chunk)
->whereNull('delete_time')
->column('revisit_slot_start_offset', 'id');
foreach ($rows as $id => $offset) {
$out[(int) $id] = (int) $offset;
}
}
return $out;
}
/**
* 诊单复诊统计起始偏移(默认 0:第 1 笔实单计为一诊;统计诊次 = 实单序号 + 偏移)
*
* @param array<int, int> $offsetMap
*/
private static function resolveRevisitSlotStartOffset(int $diagId, array $offsetMap): int
{
$offset = (int) ($offsetMap[$diagId] ?? 0);
if ($offset < 0) {
$offset = 0;
}
if ($offset > 20) {
$offset = 20;
}
return $offset;
}
/** /**
* @param list<int> $diagIds * @param list<int> $diagIds
* *
@@ -748,6 +748,49 @@ class DiagnosisLogic extends BaseLogic
} }
} }
/**
* @notes 诊单指派医助操作记录列表
*/
public static function setRevisitSlotStartOffset(int $diagnosisId, int $offset, array $adminInfo): bool
{
self::$error = '';
if ($diagnosisId <= 0) {
self::setError('诊单不存在');
return false;
}
if ($offset < 0 || $offset > 20) {
self::setError('起始偏移须在 020 之间');
return false;
}
$diagnosis = Diagnosis::where('id', $diagnosisId)->whereNull('delete_time')->find();
if (!$diagnosis) {
self::setError('诊单不存在');
return false;
}
$old = (int) ($diagnosis->revisit_slot_start_offset ?? 0);
if ($old < 0) {
$old = 0;
}
if ($old > 20) {
$old = 20;
}
if ($old === $offset) {
return true;
}
try {
$diagnosis->save(['revisit_slot_start_offset' => $offset]);
return true;
} catch (\Exception $e) {
self::setError($e->getMessage());
return false;
}
}
/** /**
* @notes 诊单指派医助操作记录列表 * @notes 诊单指派医助操作记录列表
*/ */
@@ -2375,6 +2375,123 @@ class PrescriptionOrderLogic
->toArray(); ->toArray();
} }
/**
* 手工新增操作日志;可选单独调整处方审核 / 支付单审核状态(不触发常规审核流程副作用)
*
* @param array<string,mixed> $params id, summary, prescription_audit_status?, payment_slip_audit_status?, prescription_audit_remark?, payment_slip_audit_remark?
* @return array<string,mixed>|false
*/
public static function addLog(array $params, int $adminId, array $adminInfo)
{
self::$error = '';
$id = (int) ($params['id'] ?? 0);
$summary = mb_substr(trim((string) ($params['summary'] ?? '')), 0, 500);
if ($summary === '') {
self::$error = '请填写日志内容';
return false;
}
$order = PrescriptionOrder::where('id', $id)->whereNull('delete_time')->find();
if (!$order) {
self::$error = '订单不存在';
return false;
}
if (!self::canAccessOrder($order, $adminId, $adminInfo)) {
self::$error = '无权限操作';
return false;
}
$changeParts = [];
$hasRxChange = array_key_exists('prescription_audit_status', $params)
&& $params['prescription_audit_status'] !== ''
&& $params['prescription_audit_status'] !== null;
$hasPayChange = array_key_exists('payment_slip_audit_status', $params)
&& $params['payment_slip_audit_status'] !== ''
&& $params['payment_slip_audit_status'] !== null;
if ($hasRxChange) {
if (!self::canAuditPrescriptionOrder($adminInfo)) {
self::$error = '无处方审核权限,不能调整处方审核状态';
return false;
}
$newRx = (int) $params['prescription_audit_status'];
if (!in_array($newRx, [0, 1, 2], true)) {
self::$error = '处方审核状态无效';
return false;
}
$oldRx = (int) $order->prescription_audit_status;
if ($newRx !== $oldRx) {
$order->prescription_audit_status = $newRx;
$changeParts[] = '处方审核:' . self::auditStatusLabelForLog($oldRx)
. ' → ' . self::auditStatusLabelForLog($newRx);
}
if (array_key_exists('prescription_audit_remark', $params)) {
$order->prescription_audit_remark = mb_substr(trim((string) $params['prescription_audit_remark']), 0, 500);
}
}
if ($hasPayChange) {
if (!self::canAuditPaymentSlipOrder($adminInfo)) {
self::$error = '无支付单审核权限,不能调整支付单审核状态';
return false;
}
$newPay = (int) $params['payment_slip_audit_status'];
if (!in_array($newPay, [0, 1, 2], true)) {
self::$error = '支付单审核状态无效';
return false;
}
$oldPay = (int) $order->payment_slip_audit_status;
if ($newPay !== $oldPay) {
$order->payment_slip_audit_status = $newPay;
$changeParts[] = '支付单审核:' . self::auditStatusLabelForLog($oldPay)
. ' → ' . self::auditStatusLabelForLog($newPay);
}
if (array_key_exists('payment_slip_audit_remark', $params)) {
$order->payment_slip_audit_remark = mb_substr(trim((string) $params['payment_slip_audit_remark']), 0, 500);
}
}
if ($hasRxChange || $hasPayChange) {
self::syncFulfillmentStatus($order);
try {
$order->save();
} catch (\Throwable $e) {
self::$error = $e->getMessage();
return false;
}
}
$logSummary = $summary;
if ($changeParts !== []) {
$logSummary .= '' . implode('', $changeParts) . '';
}
self::writeLog($id, $adminId, $adminInfo, 'manual_log', $logSummary);
$out = $order->toArray();
self::maskInternalCostIfNeeded($out, $adminInfo);
self::maskRemarkExtraIfNeeded($out, $adminInfo);
self::attachLinkedPayOrders($out);
return $out;
}
private static function auditStatusLabelForLog(int $status): string
{
return match ($status) {
1 => '已通过',
2 => '已驳回',
default => '待审核',
};
}
/** /**
* 为「已发货/已签收」(fulfillment_status=5/6) 的业务订单新增一条关联支付单(zyt_order), * 为「已发货/已签收」(fulfillment_status=5/6) 的业务订单新增一条关联支付单(zyt_order),
* 创建后将支付单链接到业务订单,并将处方/支付审核状态重置为待审核以启动再次审核流程。 * 创建后将支付单链接到业务订单,并将处方/支付审核状态重置为待审核以启动再次审核流程。
@@ -3107,6 +3224,207 @@ class PrescriptionOrderLogic
return $type; return $type;
} }
/**
* 导出用:处方药材明细(主方/辅方分行,与处方笺一致)
*
* @param array<string, mixed> $rx
*/
public static function formatPrescriptionHerbsForExport(array $rx): string
{
[$mainHerbs, $auxHerbs] = self::splitPrescriptionHerbsFromRx($rx);
$formatList = static function (array $herbs): string {
$parts = [];
foreach ($herbs as $h) {
if (!\is_array($h)) {
continue;
}
$name = trim((string) ($h['name'] ?? ''));
if ($name === '') {
continue;
}
$parts[] = $name . ' ' . self::formatExportDosageNumber((float) ($h['dosage'] ?? 0)) . 'g';
}
return implode('、', $parts);
};
$sections = [];
$mainText = $formatList($mainHerbs);
if ($mainText !== '') {
$sections[] = '主方:' . $mainText;
}
$auxText = $formatList($auxHerbs);
if ($auxText !== '') {
$sections[] = '辅方:' . $auxText;
}
return implode("\n", $sections);
}
/**
* 导出用:主方/辅方服用方式(与前端 buildUsageSegmentText / 处方笺同口径)
*
* @param array<string, mixed> $usage
*/
public static function formatUsageSegmentForExport(
array $usage,
string $prescriptionType = '浓缩水丸',
string $fallbackWay = '',
string $fallbackTime = ''
): string {
$pt = trim($prescriptionType) !== '' ? trim($prescriptionType) : '浓缩水丸';
$times = (int) ($usage['times_per_day'] ?? 0);
if ($times <= 0) {
$times = 3;
}
$amount = isset($usage['dosage_amount']) && $usage['dosage_amount'] !== '' && $usage['dosage_amount'] !== null
? (float) $usage['dosage_amount']
: 10.0;
$unit = trim((string) ($usage['usage_dosage_unit'] ?? ($usage['dosage_unit'] ?? '')));
if ($unit === '') {
$unit = $pt === '饮片' ? 'ml' : 'g';
}
$usageWay = trim((string) ($usage['usage_way'] ?? ''));
if ($usageWay === '') {
$usageWay = $fallbackWay !== '' ? $fallbackWay : '温水送服';
}
$usageTime = trim((string) ($usage['usage_time'] ?? ''));
if ($usageTime === '') {
$usageTime = $fallbackTime;
}
$seg = ['每天' . $times . '次'];
if ($pt === '浓缩水丸') {
$bags = (int) ($usage['dosage_bag_count'] ?? 0);
if ($bags <= 0) {
$bags = 1;
}
$seg[] = '一次' . $bags . '袋';
$seg[] = '每袋' . self::formatExportDosageNumber($amount) . $unit;
} else {
$seg[] = '一次' . self::formatExportDosageNumber($amount) . $unit;
}
$seg[] = $usageWay;
if ($usageTime !== '') {
$seg[] = $usageTime;
}
return implode(', ', $seg);
}
/**
* 导出用:辅方用法 JSON 规范化(与前端 normalizeSlipAuxUsageForm 默认值一致)
*
* @return array<string, mixed>
*/
private static function normalizeAuxUsageForExport($raw, string $prescriptionType): array
{
$pt = trim($prescriptionType) !== '' ? trim($prescriptionType) : '浓缩水丸';
if ($pt === '饮片') {
$base = [
'dosage_amount' => 50.0,
'dosage_bag_count' => 1,
'times_per_day' => 3,
'usage_days' => 7,
];
} elseif ($pt === '浓缩水丸') {
$base = [
'dosage_amount' => 5.0,
'dosage_bag_count' => 1,
'times_per_day' => 3,
'usage_days' => 7,
];
} else {
$base = [
'dosage_amount' => 1.0,
'dosage_bag_count' => 1,
'times_per_day' => 3,
'usage_days' => 7,
];
}
if (\is_string($raw) && $raw !== '') {
$decoded = json_decode($raw, true);
$raw = \is_array($decoded) ? $decoded : null;
}
if (!\is_array($raw)) {
return $base;
}
return [
'dosage_amount' => isset($raw['dosage_amount']) && $raw['dosage_amount'] !== '' && $raw['dosage_amount'] !== null
? (float) $raw['dosage_amount']
: $base['dosage_amount'],
'dosage_bag_count' => (int) ($raw['dosage_bag_count'] ?? 0) > 0
? (int) $raw['dosage_bag_count']
: $base['dosage_bag_count'],
'times_per_day' => (int) ($raw['times_per_day'] ?? 0) > 0
? (int) $raw['times_per_day']
: $base['times_per_day'],
'usage_days' => (int) ($raw['usage_days'] ?? 0) > 0
? (int) $raw['usage_days']
: $base['usage_days'],
];
}
/**
* @return array{0: array<int, array<string, mixed>>, 1: array<int, array<string, mixed>>}
*/
private static function splitPrescriptionHerbsFromRx(array $rx): array
{
$herbs = $rx['herbs'] ?? null;
if (\is_string($herbs) && $herbs !== '') {
$decoded = json_decode($herbs, true);
$herbs = \is_array($decoded) ? $decoded : [];
}
if (!\is_array($herbs)) {
$herbs = [];
}
$mainHerbs = [];
$auxHerbs = [];
foreach ($herbs as $h) {
if (!\is_array($h)) {
continue;
}
if (((string) ($h['formula_type'] ?? '')) === '辅方') {
$auxHerbs[] = $h;
} else {
$mainHerbs[] = $h;
}
}
return [$mainHerbs, $auxHerbs];
}
private static function formatExportDosageNumber(float $dosage): string
{
if (floor($dosage) === $dosage) {
return (string) (int) $dosage;
}
return rtrim(rtrim(number_format($dosage, 4, '.', ''), '0'), '.');
}
/**
* 导出用:主方/辅方开立天数(与详情侧栏「处方开立」同口径:主方取处方 usage_days,辅方取 aux_usage.usage_days
* 订单服用天数单独导出在 export_medication_days 列,不在此混用。
*
* @param array<string, mixed> $rx
* @param array<string, mixed>|null $auxUsage
*/
private static function resolveExportUsageDays(array $rx, ?array $auxUsage, bool $isAux): string
{
if ($isAux) {
$days = (int) ($auxUsage['usage_days'] ?? 0);
return $days > 0 ? (string) $days : '';
}
$days = (int) ($rx['usage_days'] ?? 0);
return $days > 0 ? (string) $days : '';
}
/** /**
* 导出列:挂号表渠道来源展示(与 AppointmentLists channel_source_desc 同字典口径) * 导出列:挂号表渠道来源展示(与 AppointmentLists channel_source_desc 同字典口径)
* *
@@ -3403,7 +3721,12 @@ class PrescriptionOrderLogic
$rxById = []; $rxById = [];
if ($rxIdList !== []) { if ($rxIdList !== []) {
$rxRows = Prescription::whereIn('id', $rxIdList)->whereNull('delete_time') $rxRows = Prescription::whereIn('id', $rxIdList)->whereNull('delete_time')
->field(['id', 'prescription_type', 'need_decoction', 'dose_unit', 'assistant_id', 'appointment_id', 'prescription_name', 'aux_usage', 'herbs', 'creator_id']) ->field([
'id', 'prescription_type', 'need_decoction', 'dose_unit', 'assistant_id', 'appointment_id',
'prescription_name', 'aux_usage', 'herbs', 'creator_id',
'dosage_amount', 'dosage_unit', 'dosage_bag_count', 'times_per_day', 'usage_days',
'usage_way', 'usage_time',
])
->select() ->select()
->toArray(); ->toArray();
foreach ($rxRows as $xr) { foreach ($rxRows as $xr) {
@@ -3637,6 +3960,38 @@ class PrescriptionOrderLogic
} }
$item['export_prescription_name'] = implode(' ', $rxNameParts); $item['export_prescription_name'] = implode(' ', $rxNameParts);
$rxArr = \is_array($rx) ? $rx : [];
$rxType = trim((string) ($rxArr['prescription_type'] ?? '')) ?: '浓缩水丸';
$item['export_prescription_herbs'] = self::formatPrescriptionHerbsForExport($rxArr);
$item['export_main_usage'] = $rxArr !== []
? self::formatUsageSegmentForExport($rxArr, $rxType)
: '';
[, $auxHerbs] = self::splitPrescriptionHerbsFromRx($rxArr);
$auxUsageNorm = $auxHerbs !== []
? self::normalizeAuxUsageForExport($rxArr['aux_usage'] ?? null, $rxType)
: null;
if ($auxHerbs !== [] && $auxUsageNorm !== null) {
$item['export_aux_usage'] = self::formatUsageSegmentForExport(
[
'dosage_amount' => $auxUsageNorm['dosage_amount'],
'dosage_bag_count' => $auxUsageNorm['dosage_bag_count'],
'times_per_day' => $auxUsageNorm['times_per_day'],
'usage_dosage_unit' => $rxArr['dosage_unit'] ?? '',
'usage_way' => $rxArr['usage_way'] ?? '',
'usage_time' => $rxArr['usage_time'] ?? '',
],
$rxType,
(string) ($rxArr['usage_way'] ?? ''),
(string) ($rxArr['usage_time'] ?? '')
);
} else {
$item['export_aux_usage'] = '';
}
$item['export_main_usage_days'] = self::resolveExportUsageDays($rxArr, $auxUsageNorm, false);
$item['export_aux_usage_days'] = $auxHerbs !== []
? self::resolveExportUsageDays($rxArr, $auxUsageNorm, true)
: '';
$item['export_service_package'] = self::formatServicePackageForExport( $item['export_service_package'] = self::formatServicePackageForExport(
$item['service_package'] ?? '', $item['service_package'] ?? '',
$packageNameByValue $packageNameByValue
@@ -3807,27 +4162,7 @@ class PrescriptionOrderLogic
$doctorId = (int) ($rx['creator_id'] ?? 0); $doctorId = (int) ($rx['creator_id'] ?? 0);
$herbs = $rx['herbs'] ?? null; [$mainHerbs, $auxHerbs] = self::splitPrescriptionHerbsFromRx($rx);
if (\is_string($herbs) && $herbs !== '') {
$decoded = json_decode($herbs, true);
$herbs = \is_array($decoded) ? $decoded : [];
}
if (!\is_array($herbs)) {
$herbs = [];
}
$mainHerbs = [];
$auxHerbs = [];
foreach ($herbs as $h) {
if (!\is_array($h)) {
continue;
}
if (((string) ($h['formula_type'] ?? '')) === '辅方') {
$auxHerbs[] = $h;
} else {
$mainHerbs[] = $h;
}
}
$lookup = static function (string $ft, array $hs) use ($doctorId, $libByDoctor, $libPublic): string { $lookup = static function (string $ft, array $hs) use ($doctorId, $libByDoctor, $libPublic): string {
if ($hs === []) { if ($hs === []) {
@@ -4546,4 +4881,175 @@ class PrescriptionOrderLogic
{ {
self::writeLog($prescriptionOrderId, $adminId, $adminInfo, 'patch_rx_patient', $summary); self::writeLog($prescriptionOrderId, $adminId, $adminInfo, 'patch_rx_patient', $summary);
} }
/**
* 业务订单详情场景:更新主方/辅方服用次数与开立天数,以及订单服用天数
*
* @param array<string, mixed> $params
*/
public static function patchPrescriptionUsage(array $params, int $adminId, array $adminInfo): bool
{
self::$error = '';
$prescriptionOrderId = (int) ($params['id'] ?? 0);
$order = PrescriptionOrder::where('id', $prescriptionOrderId)->whereNull('delete_time')->find();
if (!$order) {
self::setError('订单不存在');
return false;
}
if (!self::canAccessOrder($order, $adminId, $adminInfo)) {
self::setError('无权限操作');
return false;
}
if ((int) $order->fulfillment_status === 4) {
self::setError('已取消的订单不可修改');
return false;
}
$rxId = (int) ($order->prescription_id ?? 0);
if ($rxId <= 0) {
self::setError('该订单未关联处方');
return false;
}
$rx = Prescription::where('id', $rxId)->whereNull('delete_time')->find();
if (!$rx) {
self::setError('处方不存在');
return false;
}
if (!PrescriptionLogic::canViewPrescription($rx, $adminId, $adminInfo)) {
self::setError('无权限修改此处方');
return false;
}
$timesPerDay = (int) ($params['times_per_day'] ?? 0);
$usageDays = (int) ($params['usage_days'] ?? 0);
$medDays = (int) ($params['medication_days'] ?? 0);
if ($timesPerDay < 1 || $timesPerDay > 6) {
self::setError('主方每天次数须在 16 之间');
return false;
}
if ($usageDays < 1 || $usageDays > 999) {
self::setError('主方开立天数须在 1999 之间');
return false;
}
if ($medDays < 1 || $medDays > 999) {
self::setError('订单服用天数须在 1999 之间');
return false;
}
$hasAux = self::prescriptionHasAuxFormula($rx);
$auxTimesPerDay = null;
$auxUsageDays = null;
if ($hasAux) {
if (!array_key_exists('aux_times_per_day', $params) || !array_key_exists('aux_usage_days', $params)) {
self::setError('含辅方处方须填写辅方服用参数');
return false;
}
$auxTimesPerDay = (int) $params['aux_times_per_day'];
$auxUsageDays = (int) $params['aux_usage_days'];
if ($auxTimesPerDay < 1 || $auxTimesPerDay > 6) {
self::setError('辅方每天次数须在 16 之间');
return false;
}
if ($auxUsageDays < 1 || $auxUsageDays > 999) {
self::setError('辅方开立天数须在 1999 之间');
return false;
}
}
$oldTimes = (int) ($rx->times_per_day ?? 0);
$oldUsageDays = (int) ($rx->usage_days ?? 0);
$oldMedDays = (int) ($order->medication_days ?? 0);
$oldAuxUsage = $rx->aux_usage;
if (is_string($oldAuxUsage)) {
$decoded = json_decode($oldAuxUsage, true);
$oldAuxUsage = is_array($decoded) ? $decoded : [];
}
if (!is_array($oldAuxUsage)) {
$oldAuxUsage = [];
}
$oldAuxTimes = (int) ($oldAuxUsage['times_per_day'] ?? 0);
$oldAuxUsageDays = (int) ($oldAuxUsage['usage_days'] ?? 0);
try {
$rxUpdates = [
'times_per_day' => $timesPerDay,
'usage_days' => $usageDays,
];
if ($hasAux) {
$auxUsage = $oldAuxUsage;
$auxUsage['times_per_day'] = $auxTimesPerDay;
$auxUsage['usage_days'] = $auxUsageDays;
$rxUpdates['aux_usage'] = $auxUsage;
}
$rx->save($rxUpdates);
$order->medication_days = $medDays;
$order->save();
$parts = [
sprintf(
'主方 每天%d次/开立%d天 → 每天%d次/开立%d天',
$oldTimes > 0 ? $oldTimes : 0,
$oldUsageDays > 0 ? $oldUsageDays : 0,
$timesPerDay,
$usageDays
),
];
if ($hasAux) {
$parts[] = sprintf(
'辅方 每天%d次/开立%d天 → 每天%d次/开立%d天',
$oldAuxTimes > 0 ? $oldAuxTimes : 0,
$oldAuxUsageDays > 0 ? $oldAuxUsageDays : 0,
$auxTimesPerDay,
$auxUsageDays
);
}
$parts[] = sprintf(
'订单设置 %d天 → %d天',
$oldMedDays > 0 ? $oldMedDays : 0,
$medDays
);
$summary = '服用参数:' . implode('', $parts);
self::writeLog($prescriptionOrderId, $adminId, $adminInfo, 'patch_rx_usage', $summary);
return true;
} catch (\Exception $e) {
self::setError($e->getMessage());
return false;
}
}
/**
* 处方是否含辅方药材(与列表/详情 has_aux_formula 口径一致)
*/
private static function prescriptionHasAuxFormula(Prescription $rx): bool
{
$herbs = $rx->herbs;
if (is_string($herbs)) {
$decoded = json_decode($herbs, true);
$herbs = is_array($decoded) ? $decoded : [];
}
if (!is_array($herbs)) {
return false;
}
foreach ($herbs as $h) {
if (is_array($h) && (string) ($h['formula_type'] ?? '') === '辅方') {
return true;
}
}
return false;
}
} }
@@ -44,6 +44,7 @@ class DiagnosisValidate extends BaseValidate
'end_date' => 'date|checkDateRange', 'end_date' => 'date|checkDateRange',
'diagnosis_id' => 'require|integer|checkDiagnosisId', 'diagnosis_id' => 'require|integer|checkDiagnosisId',
'tracking_content' => 'require|length:1,1000', 'tracking_content' => 'require|length:1,1000',
'revisit_slot_start_offset' => 'integer|between:0,20',
]; ];
protected $message = [ protected $message = [
@@ -139,6 +140,13 @@ class DiagnosisValidate extends BaseValidate
return $this->only(['id']); return $this->only(['id']);
} }
/** 业务订单 tab:设置复诊接诊率统计起始偏移 */
public function sceneSetRevisitSlotStartOffset()
{
return $this->only(['id', 'revisit_slot_start_offset'])
->append('revisit_slot_start_offset', 'require|integer|between:0,20');
}
protected function checkDiagnosis($value) protected function checkDiagnosis($value)
{ {
$diagnosis = Diagnosis::findOrEmpty($value); $diagnosis = Diagnosis::findOrEmpty($value);
@@ -32,6 +32,11 @@ class PrescriptionOrderValidate extends BaseValidate
'remark_assistant' => 'max:500', 'remark_assistant' => 'max:500',
'action' => 'require|in:approve,reject', 'action' => 'require|in:approve,reject',
'remark' => 'max:500', 'remark' => 'max:500',
'summary' => 'require|max:500',
'prescription_audit_status' => 'in:0,1,2',
'payment_slip_audit_status' => 'in:0,1,2',
'prescription_audit_remark' => 'max:500',
'payment_slip_audit_remark' => 'max:500',
'fulfillment_status' => 'require|integer|in:3,7,8,9,11,12', 'fulfillment_status' => 'require|integer|in:3,7,8,9,11,12',
'reason' => 'require|max:500', 'reason' => 'require|max:500',
'refund_amount' => 'float|egt:0', 'refund_amount' => 'float|egt:0',
@@ -74,6 +79,7 @@ class PrescriptionOrderValidate extends BaseValidate
'withdraw' => ['id'], 'withdraw' => ['id'],
'ship' => ['id', 'tracking_number', 'express_company', 'ship_mode'], 'ship' => ['id', 'tracking_number', 'express_company', 'ship_mode'],
'logs' => ['id'], 'logs' => ['id'],
'addLog' => ['id', 'summary'],
'paidPayOrders' => ['diagnosis_id'], 'paidPayOrders' => ['diagnosis_id'],
'addPayOrder' => ['id', 'order_type', 'pay_amount', 'pay_remark'], 'addPayOrder' => ['id', 'order_type', 'pay_amount', 'pay_remark'],
'linkPayOrder' => ['id', 'pay_order_id'], 'linkPayOrder' => ['id', 'pay_order_id'],
@@ -83,6 +89,7 @@ class PrescriptionOrderValidate extends BaseValidate
'submitGancaoRecipel' => ['id'], 'submitGancaoRecipel' => ['id'],
'previewGancaoRecipel' => ['id'], 'previewGancaoRecipel' => ['id'],
'patchPrescriptionPatient' => ['id', 'patient_name', 'phone'], 'patchPrescriptionPatient' => ['id', 'patient_name', 'phone'],
'patchPrescriptionUsage' => ['id', 'times_per_day', 'usage_days', 'medication_days', 'aux_times_per_day', 'aux_usage_days'],
'updateAmount' => ['id', 'amount'], 'updateAmount' => ['id', 'amount'],
'setShipMode' => ['id', 'ship_mode'], 'setShipMode' => ['id', 'ship_mode'],
]; ];
@@ -93,4 +100,15 @@ class PrescriptionOrderValidate extends BaseValidate
->append('id', 'require|integer|gt:0') ->append('id', 'require|integer|gt:0')
->append('amount', 'require|float|egt:0'); ->append('amount', 'require|float|egt:0');
} }
public function patchPrescriptionUsage(): PrescriptionOrderValidate
{
return $this->only(['id', 'times_per_day', 'usage_days', 'medication_days', 'aux_times_per_day', 'aux_usage_days'])
->append('id', 'require|integer|gt:0')
->append('times_per_day', 'require|integer|between:1,6')
->append('usage_days', 'require|integer|between:1,999')
->append('medication_days', 'require|integer|between:1,999')
->append('aux_times_per_day', 'integer|between:1,6')
->append('aux_usage_days', 'integer|between:1,999');
}
} }
@@ -0,0 +1,658 @@
<?php
declare(strict_types=1);
namespace app\command;
use app\adminapi\logic\dept\DeptLogic;
use app\adminapi\logic\stats\RevisitRateLogic;
use think\console\Command;
use think\console\Input;
use think\console\input\Option;
use think\console\Output;
use think\facade\Db;
use think\facade\Log;
/**
* 待分配诊单自动指派(每天定时执行)
*
* 使用方法:
* php think tcm:auto-assign-pending # 正式执行
* php think tcm:auto-assign-pending --dry-run # 只演练输出分配计划,不写库
*
* 推荐 cron(每天 09:00 执行一次;或经 zyt_dev_crontab 表调度):
* 0 9 * * * cd /path/to/server && php think tcm:auto-assign-pending >> /var/log/zyt-auto-assign.log 2>&1
*
* 分配规则(依据上个自然月「二诊复诊接诊率」,与 RevisitRateLogic::overview 口径完全一致):
* - 接诊率 > 70% :第一优先档,当日每人最多 3
* - 接诊率 60% ~ 70% :第二档,当日每人最多 2
* - 接诊率 50% ~ 60% :第三档,当日每人最多 1
* - 接诊率 < 50% 或上月无被指派数据:不参与分配
* - 仅分配给「二中心」及其组织下级部门的在职医助(当前部门校验,调离二中心即不再参与)
*
* 轮询方式:按轮次分配,每轮内先 >70% 档每人 1 条,再 60%~70% 档每人 1 条,再 50%~60% 档每人 1 条;
* 一轮结束还有剩余待指派诊单则进入下一轮,直至待指派池为空或所有医助当日额度用尽。
* 例:当天 20 条,>70% 3 人、60%~70% 7 人、50%~60% 5
* 第一轮 3+7+5=15 条;剩余 5 条进第二轮:>70% 再各 1 条(3 条),余 2 条给 60%~70% 档前 2 人。
*
* 日上限跨执行累计:同一天重复执行命令时,会先从 tcm_diagnosis_auto_assign_log 扣减当日已自动分配数,不会超额。
*
* 日志:无论分配与否,每条待指派诊单都会写入 tcm_diagnosis_auto_assign_log,记录原因(为什么分配 / 为什么不分配)。
* 成功分配同时写 tcm_diagnosis_assign_logis_inherit=0,计入次月接诊率分母),与手动指派同口径。
* 写入时 from_assistant_id 尽量带上「一中心」原医助(指派日志中最近的一中心身份,否则回退业务单创建人若属一中心),避免原医助恒为空。
*/
class AutoAssignPendingDiagnosis extends Command
{
/** 档位标识 */
private const TIER_GT70 = 'gt70';
private const TIER_60_70 = '60_70';
private const TIER_50_60 = '50_60';
/** 各档位当日每人分配上限(按优先级排列,先高档后低档) */
private const TIER_DAILY_CAPS = [
self::TIER_GT70 => 3,
self::TIER_60_70 => 2,
self::TIER_50_60 => 1,
];
private const TIER_LABELS = [
self::TIER_GT70 => '>70%',
self::TIER_60_70 => '60%~70%',
self::TIER_50_60 => '50%~60%',
];
protected function configure()
{
$this->setName('tcm:auto-assign-pending')
->setDescription('待分配诊单自动指派:按上月二诊复诊接诊率分档轮询分配,并写入自动指派日志')
->addOption('dry-run', null, Option::VALUE_NONE, '演练模式:只输出分配计划,不写库');
}
protected function execute(Input $input, Output $output): int
{
$now = time();
$dryRun = (bool) $input->getOption('dry-run');
$runDate = date('Y-m-d', $now);
$statMonth = date('Y-m', strtotime(date('Y-m-01', $now) . ' -1 month'));
$batchNo = date('YmdHis', $now) . str_pad((string) random_int(0, 9999), 4, '0', STR_PAD_LEFT);
$output->writeln(sprintf(
'[%s] 开始自动指派待分配诊单,批次=%s,统计月=%s%s',
date('Y-m-d H:i:s', $now),
$batchNo,
$statMonth,
$dryRun ? '(演练模式,不写库)'
: ''
));
try {
// 1. 上月二诊复诊接诊率 → 医助分档
$tiers = $this->buildAssistantTiers($statMonth);
$tierTotal = array_sum(array_map('count', $tiers));
$output->writeln(sprintf(
'医助分档:>70%% 共 %d 人,60%%~70%% 共 %d 人,50%%~60%% 共 %d 人',
\count($tiers[self::TIER_GT70]),
\count($tiers[self::TIER_60_70]),
\count($tiers[self::TIER_50_60])
));
// 2. 当日剩余额度(扣减当日已自动分配数,防止同日重复执行超额)
$remaining = $this->buildRemainingQuota($tiers, $runDate);
// 3. 待指派池:与「待分配医助」Tab 同口径(assistant_id 空/0 + 当月有业务订单),先到先分
[$pool, $ineligible] = $this->fetchPendingPool($now);
$output->writeln(sprintf('待指派池:符合条件 %d 条,不符合条件 %d 条', \count($pool), \count($ineligible)));
$logRows = [];
// 不符合条件的待指派诊单:不分配,逐条记录原因
foreach ($ineligible as $item) {
$logRows[] = $this->buildLogRow($batchNo, $runDate, $statMonth, $item['diag'], [
'action' => 0,
'reason' => $item['reason'],
], $now);
}
if ($pool === []) {
$this->flushLogs($logRows, $dryRun, $output);
$output->writeln('待指派池为空,本次无需分配。');
return 0;
}
if ($tierTotal === 0) {
$reason = sprintf('未分配:上月(%s)无二诊复诊接诊率≥50%%的医助,本批次不执行分配', $statMonth);
foreach ($pool as $diag) {
$logRows[] = $this->buildLogRow($batchNo, $runDate, $statMonth, $diag, [
'action' => 0,
'reason' => $reason,
], $now);
}
$this->flushLogs($logRows, $dryRun, $output);
$output->writeln($reason);
return 0;
}
// 4. 轮询排分配计划
[$plan, $leftover] = $this->buildAssignPlan($pool, $tiers, $remaining);
// 5. 执行计划(逐条事务 + 行锁复核,落库诊单/指派日志/自动指派日志)
$assigned = 0;
$skipped = 0;
foreach ($plan as $item) {
if ($dryRun) {
$assigned++;
$output->writeln(sprintf(
'[演练] 诊单#%d(%s) → %s%s,第%d轮,当日第%d/%d条)',
$item['diagnosis']['id'],
(string) $item['diagnosis']['patient_name'],
$item['assistant']['name'],
self::TIER_LABELS[$item['tier']],
$item['round'],
$item['day_seq'],
self::TIER_DAILY_CAPS[$item['tier']]
));
continue;
}
$ok = $this->applyAssignment($item, $now);
if ($ok) {
$assigned++;
$logRows[] = $this->buildLogRow($batchNo, $runDate, $statMonth, $item['diagnosis'], [
'action' => 1,
'assistant_id' => $item['assistant']['id'],
'assistant_name' => $item['assistant']['name'],
'tier' => $item['tier'],
'visit2_rate' => $item['assistant']['rate'],
'round_no' => $item['round'],
'reason' => sprintf(
'已分配给医助[%s](ID:%d):上月(%s)二诊复诊接诊率 %.2f%%,档位[%s](日上限%d条),第 %d 轮轮询分得,当日该医助第 %d 条',
$item['assistant']['name'],
$item['assistant']['id'],
$statMonth,
$item['assistant']['rate'],
self::TIER_LABELS[$item['tier']],
self::TIER_DAILY_CAPS[$item['tier']],
$item['round'],
$item['day_seq']
),
], $now);
} else {
$skipped++;
$logRows[] = $this->buildLogRow($batchNo, $runDate, $statMonth, $item['diagnosis'], [
'action' => 0,
'reason' => '未分配:执行时诊单已被指派给其他医助(并发/人工抢先),本次跳过',
], $now);
}
}
// 6. 额度用尽后剩余的待指派诊单:不分配,记录原因
foreach ($leftover as $diag) {
$logRows[] = $this->buildLogRow($batchNo, $runDate, $statMonth, $diag, [
'action' => 0,
'reason' => '未分配:各档位医助当日剩余额度已用尽(>70%每人3条、60%~70%每人2条、50%~60%每人1条),顺延至下次执行',
], $now);
}
$this->flushLogs($logRows, $dryRun, $output);
$output->writeln(sprintf(
'执行完成。计划分配: %d, 实际分配: %d, 并发跳过: %d, 额度不足未分: %d, 不符合条件: %d',
\count($plan),
$assigned,
$skipped,
\count($leftover),
\count($ineligible)
));
return 0;
} catch (\Throwable $e) {
Log::error('待分配诊单自动指派异常: ' . $e->getMessage() . ' @ ' . $e->getFile() . ':' . $e->getLine());
$output->error('执行异常: ' . $e->getMessage());
return 1;
}
}
/**
* 按上月二诊复诊接诊率把医助分档(复用 RevisitRateLogic 口径),并剔除已禁用/已删除账号。
*
* @return array{gt70: list<array{id:int,name:string,rate:float}>, 60_70: list<array{id:int,name:string,rate:float}>, 50_60: list<array{id:int,name:string,rate:float}>}
*/
private function buildAssistantTiers(string $statMonth): array
{
$overview = RevisitRateLogic::overview(['month' => $statMonth]);
/** @var list<array{id:int,name:string,rate:float}> $candidates */
$candidates = [];
foreach ($overview['rows'] ?? [] as $deptRow) {
foreach ($deptRow['children'] ?? [] as $row) {
$aid = (int) ($row['assistant_id'] ?? 0);
$rate = $row['visit2_rate'] ?? null;
// 上月无被指派数据(rate=null)或接诊率低于 50% 的医助不参与分配
if ($aid <= 0 || $rate === null || (float) $rate < 50.0) {
continue;
}
$candidates[] = [
'id' => $aid,
'name' => (string) ($row['assistant_name'] ?? ('#' . $aid)),
'rate' => (float) $rate,
];
}
}
if ($candidates === []) {
return [self::TIER_GT70 => [], self::TIER_60_70 => [], self::TIER_50_60 => []];
}
// 只分配给当前在职可用的医助账号(role_id=2 且未禁用未删除),
// 且当前部门须在「二中心」子树内(统计月在二中心、后来调离的不再参与)
$erDeptSet = DeptLogic::getErCenterSubtreeDeptIdSet();
if ($erDeptSet === []) {
return [self::TIER_GT70 => [], self::TIER_60_70 => [], self::TIER_50_60 => []];
}
$activeIds = Db::name('admin')
->alias('a')
->join('admin_role ar', 'a.id = ar.admin_id')
->join('admin_dept ad', 'a.id = ad.admin_id')
->where('ar.role_id', 2)
->where('a.disable', 0)
->whereNull('a.delete_time')
->whereIn('ad.dept_id', array_keys($erDeptSet))
->whereIn('a.id', array_column($candidates, 'id'))
->group('a.id')
->column('a.id');
$activeSet = array_fill_keys(array_map('intval', $activeIds), true);
$tiers = [self::TIER_GT70 => [], self::TIER_60_70 => [], self::TIER_50_60 => []];
foreach ($candidates as $c) {
if (!isset($activeSet[$c['id']])) {
continue;
}
if ($c['rate'] > 70.0) {
$tiers[self::TIER_GT70][] = $c;
} elseif ($c['rate'] > 60.0) {
$tiers[self::TIER_60_70][] = $c;
} else { // 50 <= rate <= 60
$tiers[self::TIER_50_60][] = $c;
}
}
// 档内按接诊率降序、id 升序,保证分配顺序确定可复现
foreach ($tiers as &$list) {
usort($list, static function (array $a, array $b): int {
if ($a['rate'] !== $b['rate']) {
return $b['rate'] <=> $a['rate'];
}
return $a['id'] <=> $b['id'];
});
}
unset($list);
return $tiers;
}
/**
* 各医助当日剩余额度 = 档位日上限 - 当日已自动分配条数(同日重复执行不超额)。
*
* @param array<string, list<array{id:int,name:string,rate:float}>> $tiers
*
* @return array<int, int> assistant_id => 剩余额度
*/
private function buildRemainingQuota(array $tiers, string $runDate): array
{
$usedToday = Db::name('tcm_diagnosis_auto_assign_log')
->where('run_date', $runDate)
->where('action', 1)
->where('assistant_id', '>', 0)
->where('rollback_time', 0)
->group('assistant_id')
->column('COUNT(*)', 'assistant_id');
$remaining = [];
foreach (self::TIER_DAILY_CAPS as $tier => $cap) {
foreach ($tiers[$tier] as $assistant) {
$used = (int) ($usedToday[$assistant['id']] ?? 0);
$remaining[$assistant['id']] = max(0, $cap - $used);
}
}
return $remaining;
}
/**
* 待指派池:与后台「待分配医助」Tab 同口径 —— assistant_id 为空/0、未删除,
* 且当月内存在业务订单(order.patient_id = 诊单 idTab 默认按当月过滤)。先到先分。
* 返回 [符合条件, 不符合条件(附原因)] 两组;不符合条件的诊单不分配,只记日志。
*
* @return array{0: list<array<string,mixed>>, 1: list<array{diag:array<string,mixed>,reason:string}>}
*/
private function fetchPendingPool(int $now): array
{
$rows = Db::name('tcm_diagnosis')
->whereRaw('(assistant_id IS NULL OR assistant_id = 0)')
->whereNull('delete_time')
->field(['id', 'patient_name', 'phone', 'status', 'create_time'])
->order(['create_time' => 'asc', 'id' => 'asc'])
->select()
->toArray();
if ($rows === []) {
return [[], []];
}
// 当月存在业务订单的诊单集合(order.create_time 兼容整型时间戳与 datetime 字符串,与 DiagnosisLists 一致)
$tStart = (int) strtotime(date('Y-m-01 00:00:00', $now));
$tEnd = (int) strtotime(date('Y-m-t 23:59:59', $now));
$dsStart = date('Y-m-d H:i:s', $tStart);
$dsEnd = date('Y-m-d H:i:s', $tEnd);
$hasOrderSet = [];
foreach (array_chunk(array_column($rows, 'id'), 2000) as $chunk) {
$ids = Db::name('order')
->whereIn('patient_id', $chunk)
->whereNull('delete_time')
->where(static function ($q) use ($tStart, $tEnd, $dsStart, $dsEnd) {
$q->whereBetween('create_time', [$tStart, $tEnd])
->whereOr(static function ($q2) use ($dsStart, $dsEnd) {
$q2->where('create_time', '>=', $dsStart)->where('create_time', '<=', $dsEnd);
});
})
->group('patient_id')
->column('patient_id');
foreach ($ids as $id) {
$hasOrderSet[(int) $id] = true;
}
}
$curMonth = date('Y-m', $now);
$eligible = [];
$ineligible = [];
foreach ($rows as $r) {
$did = (int) ($r['id'] ?? 0);
if (!isset($hasOrderSet[$did])) {
$ineligible[] = [
'diag' => $r,
'reason' => sprintf('未分配:诊单当月(%s)无业务订单,不在「待分配医助」列表范围内,不满足自动分配条件', $curMonth),
];
continue;
}
if ((int) ($r['status'] ?? 0) !== 1) {
$ineligible[] = [
'diag' => $r,
'reason' => '未分配:诊单未启用(status≠1),不满足自动分配条件',
];
continue;
}
$eligible[] = $r;
}
return [$eligible, $ineligible];
}
/**
* 轮询排分配计划:每轮内先 >70% 档每人 1 条,再 60%~70%,再 50%~60%;受各自当日剩余额度约束。
*
* @param list<array<string,mixed>> $pool 待指派诊单(先到先分)
* @param array<string, list<array{id:int,name:string,rate:float}>> $tiers
* @param array<int, int> $remaining assistant_id => 剩余额度(会被消耗)
*
* @return array{
* 0: list<array{diagnosis:array<string,mixed>,assistant:array{id:int,name:string,rate:float},tier:string,round:int,day_seq:int}>,
* 1: list<array<string,mixed>>
* } [分配计划, 额度用尽后剩余诊单]
*/
private function buildAssignPlan(array $pool, array $tiers, array $remaining): array
{
$plan = [];
$poolIdx = 0;
$poolCount = \count($pool);
/** @var array<int, int> $daySeq 医助当日已排序号(含历史已用额度) */
$daySeq = [];
foreach ($remaining as $aid => $left) {
// 起始序号 = 日上限 - 剩余额度(同日多次执行时序号衔接)
$cap = 0;
foreach (self::TIER_DAILY_CAPS as $tier => $tierCap) {
foreach ($tiers[$tier] as $assistant) {
if ($assistant['id'] === $aid) {
$cap = $tierCap;
break 2;
}
}
}
$daySeq[$aid] = $cap - $left;
}
$round = 0;
while ($poolIdx < $poolCount) {
$round++;
$assignedThisRound = 0;
foreach (self::TIER_DAILY_CAPS as $tier => $_cap) {
foreach ($tiers[$tier] as $assistant) {
if ($poolIdx >= $poolCount) {
break 2;
}
$aid = $assistant['id'];
if (($remaining[$aid] ?? 0) <= 0) {
continue;
}
$remaining[$aid]--;
$daySeq[$aid]++;
$plan[] = [
'diagnosis' => $pool[$poolIdx],
'assistant' => $assistant,
'tier' => $tier,
'round' => $round,
'day_seq' => $daySeq[$aid],
];
$poolIdx++;
$assignedThisRound++;
}
}
if ($assignedThisRound === 0) {
// 所有医助额度用尽,剩余诊单不再分配
break;
}
}
return [$plan, \array_slice($pool, $poolIdx)];
}
/**
* 落库单条分配:事务 + 行锁复核诊单仍未指派,更新诊单并写指派日志(与手动指派 DiagnosisLogic::assign 同口径)。
*
* @param array{diagnosis:array<string,mixed>,assistant:array{id:int,name:string,rate:float}} $item
*/
private function applyAssignment(array $item, int $now): bool
{
$diagnosisId = (int) $item['diagnosis']['id'];
$toAssistantId = (int) $item['assistant']['id'];
Db::startTrans();
try {
$diagLock = Db::name('tcm_diagnosis')
->where('id', $diagnosisId)
->whereNull('delete_time')
->lock(true)
->field(['id', 'assistant_id'])
->find();
if ($diagLock === null || $diagLock === [] || (int) ($diagLock['assistant_id'] ?? 0) > 0) {
Db::rollback();
return false;
}
Db::name('tcm_diagnosis')
->where('id', $diagnosisId)
->whereNull('delete_time')
->update([
'assistant_id' => $toAssistantId,
'assign_read_at' => null,
]);
$poSnap = Db::name('tcm_prescription_order')
->where('diagnosis_id', $diagnosisId)
->whereNull('delete_time')
->order(['create_time' => 'desc', 'id' => 'desc'])
->field(['creator_id', 'create_time'])
->find();
$relatedPoCreatorId = (int) ($poSnap['creator_id'] ?? 0);
$relatedPoCreateTime = (int) ($poSnap['create_time'] ?? 0);
if ($relatedPoCreateTime <= 0) {
$relatedPoCreateTime = $now;
$relatedPoCreatorId = 0;
}
// 自动分给二中心时,原医助带上一中心身份(列表「原医助」不再恒为空)
$fromAssistantId = $this->resolveYiCenterPreviousAssistantId($diagnosisId, $relatedPoCreatorId);
Db::name('tcm_diagnosis_assign_log')->insert([
'diagnosis_id' => $diagnosisId,
'from_assistant_id' => $fromAssistantId,
'to_assistant_id' => $toAssistantId,
'operator_admin_id' => 0,
'operator_name' => '系统自动分配',
'operator_account' => 'system',
'ip' => '',
'related_po_creator_id' => $relatedPoCreatorId,
'related_po_create_time' => $relatedPoCreateTime,
'is_inherit' => 0,
'create_time' => $now,
]);
Db::commit();
return true;
} catch (\Throwable $e) {
Db::rollback();
Log::error(sprintf('自动指派落库失败 diagnosis_id=%d assistant_id=%d msg=%s', $diagnosisId, $toAssistantId, $e->getMessage()));
return false;
}
}
/**
* 解析诊单对应的「一中心」原医助 id,供自动指派写入 from_assistant_id。
* 优先:指派日志由新到旧,取最近出现的一中心身份(先 to from);
* 回退:最新业务单创建人若属一中心则用之。跳过二中心身份,避免原医助写成上一任二中心。
*/
private function resolveYiCenterPreviousAssistantId(int $diagnosisId, int $relatedPoCreatorId): int
{
if ($diagnosisId <= 0) {
return 0;
}
$yiDeptSet = DeptLogic::getYiCenterSubtreeDeptIdSet();
if ($yiDeptSet === []) {
return 0;
}
$logs = Db::name('tcm_diagnosis_assign_log')
->where('diagnosis_id', $diagnosisId)
->order('id', 'desc')
->field(['from_assistant_id', 'to_assistant_id'])
->select()
->toArray();
$candidateIds = [];
foreach ($logs as $log) {
$to = (int) ($log['to_assistant_id'] ?? 0);
$from = (int) ($log['from_assistant_id'] ?? 0);
if ($to > 0) {
$candidateIds[] = $to;
}
if ($from > 0) {
$candidateIds[] = $from;
}
}
if ($relatedPoCreatorId > 0) {
$candidateIds[] = $relatedPoCreatorId;
}
$candidateIds = array_values(array_unique(array_filter($candidateIds)));
if ($candidateIds === []) {
return 0;
}
$yiAdminSet = $this->filterAdminIdsInDeptSet($candidateIds, $yiDeptSet);
if ($yiAdminSet === []) {
return 0;
}
foreach ($logs as $log) {
$to = (int) ($log['to_assistant_id'] ?? 0);
if ($to > 0 && isset($yiAdminSet[$to])) {
return $to;
}
$from = (int) ($log['from_assistant_id'] ?? 0);
if ($from > 0 && isset($yiAdminSet[$from])) {
return $from;
}
}
if ($relatedPoCreatorId > 0 && isset($yiAdminSet[$relatedPoCreatorId])) {
return $relatedPoCreatorId;
}
return 0;
}
/**
* @param list<int> $adminIds
* @param array<int, true> $deptIdSet
*
* @return array<int, true> admin_id => true
*/
private function filterAdminIdsInDeptSet(array $adminIds, array $deptIdSet): array
{
if ($adminIds === [] || $deptIdSet === []) {
return [];
}
$rows = Db::name('admin_dept')
->whereIn('admin_id', $adminIds)
->whereIn('dept_id', array_keys($deptIdSet))
->column('admin_id');
return array_fill_keys(array_map('intval', $rows), true);
}
/**
* @param array<string,mixed> $diag 诊单行(含 id/patient_name/phone
* @param array<string,mixed> $extra action/assistant_id/assistant_name/tier/visit2_rate/round_no/reason
*
* @return array<string,mixed>
*/
private function buildLogRow(string $batchNo, string $runDate, string $statMonth, array $diag, array $extra, int $now): array
{
return [
'batch_no' => $batchNo,
'run_date' => $runDate,
'stat_month' => $statMonth,
'diagnosis_id' => (int) ($diag['id'] ?? 0),
'patient_name' => mb_substr(trim((string) ($diag['patient_name'] ?? '')), 0, 64),
'patient_phone' => mb_substr(trim((string) ($diag['phone'] ?? '')), 0, 32),
'action' => (int) ($extra['action'] ?? 0),
'assistant_id' => (int) ($extra['assistant_id'] ?? 0),
'assistant_name' => mb_substr((string) ($extra['assistant_name'] ?? ''), 0, 64),
'tier' => (string) ($extra['tier'] ?? ''),
'visit2_rate' => $extra['visit2_rate'] ?? null,
'round_no' => (int) ($extra['round_no'] ?? 0),
'reason' => mb_substr((string) ($extra['reason'] ?? ''), 0, 500),
'create_time' => $now,
];
}
/**
* @param list<array<string,mixed>> $logRows
*/
private function flushLogs(array $logRows, bool $dryRun, Output $output): void
{
if ($logRows === []) {
return;
}
if ($dryRun) {
$output->writeln(sprintf('[演练] 应写入自动指派日志 %d 条(未落库)', \count($logRows)));
return;
}
foreach (array_chunk($logRows, 500) as $chunk) {
Db::name('tcm_diagnosis_auto_assign_log')->insertAll($chunk);
}
$output->writeln(sprintf('已写入自动指派日志 %d 条', \count($logRows)));
}
}
+1 -1
View File
@@ -18,7 +18,7 @@ use think\console\Output;
* 配置 crontab(每 10 分钟):拉快递 100 + 按履约「已发货」核对释放诊单医助(跳过已完成/已签收业务单,与是否甘草单无关) * 配置 crontab(每 10 分钟):拉快递 100 + 按履约「已发货」核对释放诊单医助(跳过已完成/已签收业务单,与是否甘草单无关)
* 0,10,20,30,40,50 * * * * cd /path/to/server && php think express:auto-update >> /dev/null 2>&1 * 0,10,20,30,40,50 * * * * cd /path/to/server && php think express:auto-update >> /dev/null 2>&1
* *
* 已对「业务订单创建人非二中心」(或创建人为开方医生时按待释放身份判定)执行发货/签收自动释放的诊单会写入 tcm_diagnosis.shipped_non_er_assistant_cleared_at * 已对「待释放医助非二中心」执行发货/签收自动释放的诊单会写入 tcm_diagnosis.shipped_non_er_assistant_cleared_at
* 后续履约核对不再重复扫描(需先执行 sql/1.9.20260507/add_diagnosis_shipped_non_er_assistant_cleared_at.sql)。 * 后续履约核对不再重复扫描(需先执行 sql/1.9.20260507/add_diagnosis_shipped_non_er_assistant_cleared_at.sql)。
*/ */
class ExpressAutoUpdate extends Command class ExpressAutoUpdate extends Command
@@ -34,7 +34,7 @@ use think\console\Output;
* 0,30 * * * * cd /path/to/server && php think gancao:sync-logistics >> runtime/log/gancao_sync.log 2>&1 * 0,30 * * * * cd /path/to/server && php think gancao:sync-logistics >> runtime/log/gancao_sync.log 2>&1
* *
* 跳过履约状态为已完成(3)、已签收(6) 的业务订单,不再拉取甘草路由。 * 跳过履约状态为已完成(3)、已签收(6) 的业务订单,不再拉取甘草路由。
* 已对「业务订单创建人非二中心」(发货/签收自动释放规则见 ExpressTrackingService)的诊单会写入 tcm_diagnosis.shipped_non_er_assistant_cleared_at * 已对「待释放医助非二中心」(发货/签收自动释放规则见 ExpressTrackingService)的诊单会写入 tcm_diagnosis.shipped_non_er_assistant_cleared_at
* 后续履约核对不再重复扫描(需先执行 sql/1.9.20260507/add_diagnosis_shipped_non_er_assistant_cleared_at.sql)。 * 后续履约核对不再重复扫描(需先执行 sql/1.9.20260507/add_diagnosis_shipped_non_er_assistant_cleared_at.sql)。
*/ */
class GancaoSyncLogisticsRoute extends Command class GancaoSyncLogisticsRoute extends Command
@@ -588,7 +588,7 @@ class ExpressTrackingService
/** /**
* 处方业务订单履约「已发货(5) / 已签收(6)」时:清空关联诊单(患者)医助,并写入 tcm_diagnosis_assign_log(含关联业务订单 creator_id / create_time 快照,便于核对医助创建订单时间)。 * 处方业务订单履约「已发货(5) / 已签收(6)」时:清空关联诊单(患者)医助,并写入 tcm_diagnosis_assign_log(含关联业务订单 creator_id / create_time 快照,便于核对医助创建订单时间)。
* 快递 100 定时拉轨迹、甘草路由、甘草回调与履约核对共用同一规则。 * 快递 100 定时拉轨迹、甘草路由、甘草回调与履约核对共用同一规则。
* 不自动清空的情形:(1按业务订单 creator_id(代建场景)或回退身份判断是否在「二中心」部门子树;(2)诊单已有 shipped_non_er_assistant_cleared_at assistant_id 已为 0(视为已处理);(3)指派日志存在后台人工指派:`operator_admin_id>0` `to_assistant_id>0`(系统任务为 operator_admin_id=0;或 related_po_creator_id=from_assistant_id to>0 的旧数据兼容。避免「系统·已发货履约核对」覆盖人工指派链。 * 不自动清空的情形:(1待释放医助(诊单 assistant_id,或代建回退身份)归属「二中心」部门子树;(2)诊单当前已指派、或指派日志曾 to 给「二中心」医助(含系统自动分配);(3)诊单已有 shipped_non_er_assistant_cleared_at assistant_id 已为 0(视为已处理);(4)指派日志存在后台人工指派:`operator_admin_id>0` `to_assistant_id>0`;或 related_po_creator_id=from_assistant_id to>0 的旧数据兼容。避免「系统·已发货履约核对」覆盖人工指派链。
* *
* @param array{tracking_id?:int,tracking_number?:string,source?:string} $meta * @param array{tracking_id?:int,tracking_number?:string,source?:string} $meta
* @return array{ * @return array{
@@ -663,19 +663,18 @@ class ExpressTrackingService
return null; return null;
} }
/** 是否因「二中心」跳过释放:优先看业务订单创建人 creator_id(代建场景);创建人即开方医生时用 fromAssistantId */ // 二中心不释放:(a)待释放医助属二中心;(b)诊单当前医助属二中心;(c)指派日志曾指派给二中心医助
$erZhongxinCheckAdminId = ($poCreatorId > 0 && ($rxId <= 0 || $poCreatorId !== $rxCreator)) $skipErzhongxin = self::currentAssistantBelongsToErzhongxinDeptTree($fromAssistantId)
? $poCreatorId || ($diagAssistantIdNow > 0 && self::currentAssistantBelongsToErzhongxinDeptTree($diagAssistantIdNow))
: $fromAssistantId; || self::diagnosisHasBeenAssignedToErzhongxinAssistant($diagnosisId);
if ($skipErzhongxin) {
if ($erZhongxinCheckAdminId > 0 && self::currentAssistantBelongsToErzhongxinDeptTree($erZhongxinCheckAdminId)) { Log::info('Skipped auto assistant release (shipped/signed): 二中心 assistant assigned or targeted', [
Log::info('Skipped auto assistant release (shipped/signed): order creator (or fallback from-id) under 二中心 dept tree', [
'diagnosis_id' => $diagnosisId, 'diagnosis_id' => $diagnosisId,
'prescription_order_id' => $orderId, 'prescription_order_id' => $orderId,
'source' => $source, 'source' => $source,
'er_zhongxin_check_admin_id' => $erZhongxinCheckAdminId, 'from_assistant_id' => $fromAssistantId,
'prescription_order_creator_id' => $poCreatorId, 'diagnosis_assistant_id' => $diagAssistantIdNow,
'from_assistant_id_for_log' => $fromAssistantId, 'prescription_order_creator_id' => $poCreatorId,
]); ]);
return null; return null;
@@ -798,6 +797,36 @@ class ExpressTrackingService
return false; return false;
} }
/**
* 诊单指派日志是否曾指派给「二中心」医助(to_assistant_id>0,含人工指派与系统自动分配)。
*/
private static function diagnosisHasBeenAssignedToErzhongxinAssistant(int $diagnosisId): bool
{
if ($diagnosisId <= 0) {
return false;
}
$toIds = Db::name('tcm_diagnosis_assign_log')
->where('diagnosis_id', $diagnosisId)
->where('to_assistant_id', '>', 0)
->column('to_assistant_id');
if ($toIds === [] || $toIds === null) {
return false;
}
$seen = [];
foreach ($toIds as $aid) {
$aid = (int) $aid;
if ($aid <= 0 || isset($seen[$aid])) {
continue;
}
$seen[$aid] = true;
if (self::currentAssistantBelongsToErzhongxinDeptTree($aid)) {
return true;
}
}
return false;
}
/** /**
* 若诊单曾被后台人工指派到具体医助,则不再执行发运/签收自动清空(避免「系统·已发货履约核对」覆盖人工链)。 * 若诊单曾被后台人工指派到具体医助,则不再执行发运/签收自动清空(避免「系统·已发货履约核对」覆盖人工链)。
* 判定:operator_admin_id>0 to_assistant_id>0(人工);系统写入的释放/同步为 operator_admin_id=0 * 判定:operator_admin_id>0 to_assistant_id>0(人工);系统写入的释放/同步为 operator_admin_id=0
@@ -1031,7 +1060,7 @@ class ExpressTrackingService
/** /**
* 按处方订单履约核对:仅已发货(5) 时符合条件的订单调用 applyAssistantReleaseForShippedPrescriptionOrder(已完成/已签收不再扫描)。 * 按处方订单履约核对:仅已发货(5) 时符合条件的订单调用 applyAssistantReleaseForShippedPrescriptionOrder(已完成/已签收不再扫描)。
* 与快递 100 / 甘草来自哪个物流渠道无关;是否清空医助以该方法为准(**二中心豁免看业务单创建人**参见 apply;已 shipped 释放且 assistant_id=0 视为已处理)。 * 与快递 100 / 甘草来自哪个物流渠道无关;是否清空医助以该方法为准(**二中心豁免看待释放医助**参见 apply;已 shipped 释放且 assistant_id=0 视为已处理)。
* *
* @return array{scanned: int, cleared: int, lines: list<string>} * @return array{scanned: int, cleared: int, lines: list<string>}
*/ */
+2
View File
@@ -40,5 +40,7 @@ return [
'migrate:images-to-doctor-note' => 'app\\command\\MigrateImagesToDoctorNote', 'migrate:images-to-doctor-note' => 'app\\command\\MigrateImagesToDoctorNote',
// 诊单待办事项:扫描到点的待执行项并向创建人发送企业微信消息 // 诊单待办事项:扫描到点的待执行项并向创建人发送企业微信消息
'tcm:diagnosis-todo-notify' => 'app\\command\\DiagnosisTodoNotify', 'tcm:diagnosis-todo-notify' => 'app\\command\\DiagnosisTodoNotify',
// 待分配诊单自动指派:按上月二诊复诊接诊率分档轮询分配(>70% 3条/60~70% 2条/50~60% 1条),写自动指派日志
'tcm:auto-assign-pending' => 'app\\command\\AutoAssignPendingDiagnosis',
], ],
]; ];
Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

@@ -1 +1 @@
import r from"./error-BT3t0AXw.js";import{f as p,ak as i,I as m,a as e,aN as s,J as o}from"./@vue/runtime-core-C6bnekPw.js";import"./element-plus-CXrrycLn.js";import"./@vue/runtime-dom-DDAG46FW.js";import"./lodash-D3kF6u-c.js";import"./@vue/shared-mAAVTE9n.js";import"./@vue/reactivity-DiY1c2vO.js";import"./@element-plus/icons-vue-BAghejlR.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./vue-router-QlpZ4wdW.js";import"./index-CO3hhC4Q.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./@vueuse/core-1S4fJlii.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";const a="/admin/assets/no_perms-jDxcYpYC.png",n={class:"error404"},W=p({__name:"403",setup(c){return(_,t)=>(i(),m("div",n,[e(r,{code:"403",title:"您的账号权限不足,请联系管理员添加权限!","show-btn":!1},{content:s(()=>[...t[0]||(t[0]=[o("div",{class:"flex justify-center"},[o("img",{class:"w-[150px] h-[150px]",src:a,alt:""})],-1)])]),_:1})]))}});export{W as default}; import r from"./error-D7g_31ON.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-DiYdpaIG.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./@vueuse/core-1S4fJlii.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";const a="/admin/assets/no_perms-jDxcYpYC.png",n={class:"error404"},W=p({__name:"403",setup(c){return(_,t)=>(i(),m("div",n,[e(r,{code:"403",title:"您的账号权限不足,请联系管理员添加权限!","show-btn":!1},{content:s(()=>[...t[0]||(t[0]=[o("div",{class:"flex justify-center"},[o("img",{class:"w-[150px] h-[150px]",src:a,alt:""})],-1)])]),_:1})]))}});export{W as default};
@@ -1 +1 @@
import o from"./error-BT3t0AXw.js";import{f as r,ak as t,I as m,a as p}from"./@vue/runtime-core-C6bnekPw.js";import"./element-plus-CXrrycLn.js";import"./@vue/runtime-dom-DDAG46FW.js";import"./lodash-D3kF6u-c.js";import"./@vue/shared-mAAVTE9n.js";import"./@vue/reactivity-DiY1c2vO.js";import"./@element-plus/icons-vue-BAghejlR.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./vue-router-QlpZ4wdW.js";import"./index-CO3hhC4Q.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./@vueuse/core-1S4fJlii.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";const i={class:"error404"},T=r({__name:"404",setup(e){return(a,s)=>(t(),m("div",i,[p(o,{code:"404",title:"哎呀,出错了!您访问的页面不存在…"})]))}});export{T as default}; import o from"./error-D7g_31ON.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-DiYdpaIG.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./@vueuse/core-1S4fJlii.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";const i={class:"error404"},T=r({__name:"404",setup(e){return(a,s)=>(t(),m("div",i,[p(o,{code:"404",title:"哎呀,出错了!您访问的页面不存在…"})]))}});export{T as default};
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
import{M as T,N as C,T as $,r as D,d as L,L as k}from"./element-plus-CXrrycLn.js";import{Y as E}from"./@element-plus/icons-vue-BAghejlR.js";import{a6 as P}from"./tcm-ujpoSvdA.js";import{f as A,w as B,ak as p,I as b,aP as F,G as h,aN as n,a as i,O as m,J as M}from"./@vue/runtime-core-C6bnekPw.js";import{Q as c}from"./@vue/shared-mAAVTE9n.js";import{y as V,n as w}from"./@vue/reactivity-DiY1c2vO.js";import{_ as K}from"./index-CO3hhC4Q.js";import"./@vue/runtime-dom-DDAG46FW.js";import"./lodash-D3kF6u-c.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./vue-router-QlpZ4wdW.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./@vueuse/core-1S4fJlii.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";const Y={class:"assign-log-panel"},q={key:1,class:"text-gray-400"},z=A({__name:"AssignLogPanel",props:{diagnosisId:{}},setup(N,{expose:v}){const _=N,d=w(!1),u=w([]);function y(o){const e=o.related_po_creator_name;if(e!=null&&String(e).trim()!=="")return String(e);const t=Number(o.related_po_creator_id);return Number.isFinite(t)&&t>0?`ID:${t}`:"—"}function x(o){const e=o.related_po_create_time_text;if(e!=null&&String(e).trim()!=="")return String(e);const t=Number(o.related_po_create_time);if(!Number.isFinite(t)||t<=0)return"—";const a=new Date(t*1e3);if(Number.isNaN(a.getTime()))return"—";const r=l=>String(l).padStart(2,"0");return`${a.getFullYear()}-${r(a.getMonth()+1)}-${r(a.getDate())} ${r(a.getHours())}:${r(a.getMinutes())}:${r(a.getSeconds())}`}function f(o,e){const t=e==="from"?"from_assistant_name":"to_assistant_name",a=e==="from"?"from_assistant_id":"to_assistant_id",r=o[t];if(r!=null&&String(r).trim()!==""&&String(r)!=="—")return String(r);const l=Number(o[a]);return Number.isFinite(l)&&l>0?`ID:${l}`:"—"}const g=async()=>{if(_.diagnosisId){d.value=!0;try{const o=await P({id:_.diagnosisId}),e=Array.isArray(o)?o:[];u.value=e}catch(o){console.error(o),u.value=[]}finally{d.value=!1}}};return B(()=>_.diagnosisId,()=>{g()},{immediate:!0}),v({refresh:g}),(o,e)=>{const t=C,a=$,r=L,l=D,S=T,I=k;return p(),b("div",Y,[F((p(),h(S,{data:u.value,border:"",stripe:"","empty-text":"暂无指派记录"},{default:n(()=>[i(t,{label:"操作时间",width:"175",prop:"create_time_text"}),i(t,{label:"原医助","min-width":"120"},{default:n(({row:s})=>[m(c(f(s,"from")),1)]),_:1}),i(t,{label:"新医助","min-width":"120"},{default:n(({row:s})=>[m(c(f(s,"to")),1)]),_:1}),i(t,{label:"继承",width:"72",align:"center"},{default:n(({row:s})=>[Number(s.is_inherit)===1?(p(),h(a,{key:0,type:"success",size:"small"},{default:n(()=>[...e[0]||(e[0]=[m("是",-1)])]),_:1})):(p(),b("span",q,"否"))]),_:1}),i(t,{label:"快照·业务单创建人","min-width":"130","show-overflow-tooltip":""},{default:n(({row:s})=>[m(c(y(s)),1)]),_:1}),i(t,{label:"快照·业务单创建时间",width:"190"},{header:n(()=>[e[1]||(e[1]=M("span",null,"快照·业务单创建时间",-1)),i(l,{placement:"top",content:"related_po_create_time:触发本次操作的处方业务订单 create_time;与原文助一致时表示医助创建订单时间"},{default:n(()=>[i(r,{class:"assign-log-col-hint"},{default:n(()=>[i(V(E))]),_:1})]),_:1})]),default:n(({row:s})=>[m(c(x(s)),1)]),_:1}),i(t,{label:"操作人",width:"110",prop:"operator_name"}),i(t,{label:"操作账号",width:"120",prop:"operator_account","show-overflow-tooltip":""}),i(t,{label:"IP",width:"130",prop:"ip","show-overflow-tooltip":""})]),_:1},8,["data"])),[[I,d.value]])])}}}),Ct=K(z,[["__scopeId","data-v-f670e3e6"]]);export{Ct as default}; import{M as T,N as C,T as $,r as D,d as L,L as k}from"./element-plus-Bolc0EfP.js";import{Y as E}from"./@element-plus/icons-vue-B0jSCQ-G.js";import{a8 as P}from"./tcm-B_U8CrQ9.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-DiYdpaIG.js";import"./@vue/runtime-dom-DDAG46FW.js";import"./lodash-D3kF6u-c.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./vue-router-QlpZ4wdW.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./@vueuse/core-1S4fJlii.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";const Y={class:"assign-log-panel"},q={key:1,class:"text-gray-400"},z=A({__name:"AssignLogPanel",props:{diagnosisId:{}},setup(N,{expose:v}){const _=N,d=w(!1),u=w([]);function y(o){const e=o.related_po_creator_name;if(e!=null&&String(e).trim()!=="")return String(e);const t=Number(o.related_po_creator_id);return Number.isFinite(t)&&t>0?`ID:${t}`:"—"}function x(o){const e=o.related_po_create_time_text;if(e!=null&&String(e).trim()!=="")return String(e);const t=Number(o.related_po_create_time);if(!Number.isFinite(t)||t<=0)return"—";const a=new Date(t*1e3);if(Number.isNaN(a.getTime()))return"—";const r=l=>String(l).padStart(2,"0");return`${a.getFullYear()}-${r(a.getMonth()+1)}-${r(a.getDate())} ${r(a.getHours())}:${r(a.getMinutes())}:${r(a.getSeconds())}`}function f(o,e){const t=e==="from"?"from_assistant_name":"to_assistant_name",a=e==="from"?"from_assistant_id":"to_assistant_id",r=o[t];if(r!=null&&String(r).trim()!==""&&String(r)!=="—")return String(r);const l=Number(o[a]);return Number.isFinite(l)&&l>0?`ID:${l}`:"—"}const g=async()=>{if(_.diagnosisId){d.value=!0;try{const o=await P({id:_.diagnosisId}),e=Array.isArray(o)?o:[];u.value=e}catch(o){console.error(o),u.value=[]}finally{d.value=!1}}};return B(()=>_.diagnosisId,()=>{g()},{immediate:!0}),v({refresh:g}),(o,e)=>{const t=C,a=$,r=L,l=D,S=T,I=k;return p(),b("div",Y,[F((p(),h(S,{data:u.value,border:"",stripe:"","empty-text":"暂无指派记录"},{default:n(()=>[i(t,{label:"操作时间",width:"175",prop:"create_time_text"}),i(t,{label:"原医助","min-width":"120"},{default:n(({row:s})=>[m(c(f(s,"from")),1)]),_:1}),i(t,{label:"新医助","min-width":"120"},{default:n(({row:s})=>[m(c(f(s,"to")),1)]),_:1}),i(t,{label:"继承",width:"72",align:"center"},{default:n(({row:s})=>[Number(s.is_inherit)===1?(p(),h(a,{key:0,type:"success",size:"small"},{default:n(()=>[...e[0]||(e[0]=[m("是",-1)])]),_:1})):(p(),b("span",q,"否"))]),_:1}),i(t,{label:"快照·业务单创建人","min-width":"130","show-overflow-tooltip":""},{default:n(({row:s})=>[m(c(y(s)),1)]),_:1}),i(t,{label:"快照·业务单创建时间",width:"190"},{header:n(()=>[e[1]||(e[1]=M("span",null,"快照·业务单创建时间",-1)),i(l,{placement:"top",content:"related_po_create_time:触发本次操作的处方业务订单 create_time;与原文助一致时表示医助创建订单时间"},{default:n(()=>[i(r,{class:"assign-log-col-hint"},{default:n(()=>[i(V(E))]),_:1})]),_:1})]),default:n(({row:s})=>[m(c(x(s)),1)]),_:1}),i(t,{label:"操作人",width:"110",prop:"operator_name"}),i(t,{label:"操作账号",width:"120",prop:"operator_account","show-overflow-tooltip":""}),i(t,{label:"IP",width:"130",prop:"ip","show-overflow-tooltip":""})]),_:1},8,["data"])),[[I,d.value]])])}}}),Ct=K(z,[["__scopeId","data-v-f670e3e6"]]);export{Ct as default};
@@ -1 +1 @@
import{i as L,L as T,N as V,T as D,M as z,W as M}from"./element-plus-CXrrycLn.js";import{ae as O}from"./tcm-ujpoSvdA.js";import{f as P,b as F,w as j,ak as a,I as n,a as i,aN as s,O as m,H as w,aP as A,G as g,F as H,J as R}from"./@vue/runtime-core-C6bnekPw.js";import{y as d,n as k}from"./@vue/reactivity-DiY1c2vO.js";import{Q as c}from"./@vue/shared-mAAVTE9n.js";import{_ as G}from"./index-CO3hhC4Q.js";import"./@vue/runtime-dom-DDAG46FW.js";import"./lodash-D3kF6u-c.js";import"./@element-plus/icons-vue-BAghejlR.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./vue-router-QlpZ4wdW.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./@vueuse/core-1S4fJlii.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";const J={class:"case-record-list"},Q={key:0,class:"mb-3 flex justify-end"},W={key:0},Y={key:1,class:"text-gray-400"},q={class:"void-detail text-xs text-gray-500 mt-1"},K=P({__name:"CaseRecordList",props:{diagnosisId:{type:Number,default:0},readOnly:{type:Boolean,default:!1}},emits:["view","openPrescription"],setup(y,{expose:x,emit:C}){const _=y,h=C,l=k([]),p=k(!1),u=async()=>{if(_.diagnosisId){p.value=!0;try{const e=await O({diagnosis_id:_.diagnosisId});l.value=Array.isArray(e)?e:[]}catch(e){console.error("获取病历记录失败:",e),l.value=[]}finally{p.value=!1}}},S=e=>{if(!e)return"";const t=new Date(e*1e3);return`${t.getFullYear()}-${String(t.getMonth()+1).padStart(2,"0")}-${String(t.getDate()).padStart(2,"0")} ${String(t.getHours()).padStart(2,"0")}:${String(t.getMinutes()).padStart(2,"0")}`},$=e=>{h("view",e)},B=()=>{h("openPrescription")};return F(()=>{u()}),j(()=>_.diagnosisId,()=>{u()}),x({refresh:u}),(e,t)=>{const b=L,r=V,v=D,E=z,N=M,I=T;return a(),n("div",J,[y.readOnly?w("",!0):(a(),n("div",Q,[i(b,{type:"primary",size:"small",onClick:B},{default:s(()=>[...t[0]||(t[0]=[m("开方",-1)])]),_:1})])),A((a(),g(E,{data:d(l),border:""},{default:s(()=>[i(r,{prop:"prescription_date",label:"就诊日期",width:"120"}),i(r,{prop:"visit_no",label:"门诊号",width:"120"}),i(r,{prop:"clinical_diagnosis",label:"临床诊断","min-width":"160","show-overflow-tooltip":""}),i(r,{label:"处方摘要","min-width":"180"},{default:s(({row:o})=>[o.herbs&&o.herbs.length?(a(),n("span",W,c(o.herbs.slice(0,3).map(f=>`${f.name}${f.dosage}`).join("、"))+c(o.herbs.length>3?"...":""),1)):(a(),n("span",Y,"—"))]),_:1}),i(r,{prop:"doctor_name",label:"医师",width:"90","show-overflow-tooltip":""}),i(r,{label:"状态",width:"140",align:"center"},{default:s(({row:o})=>[o.void_status===1?(a(),n(H,{key:0},[i(v,{type:"danger",size:"small"},{default:s(()=>[...t[1]||(t[1]=[m("已作废",-1)])]),_:1}),R("div",q,c(o.void_by_name||"—")+" "+c(S(o.void_time)),1)],64)):(a(),g(v,{key:1,type:"success",size:"small"},{default:s(()=>[...t[2]||(t[2]=[m("正常",-1)])]),_:1}))]),_:1}),i(r,{label:"操作",width:"120",fixed:"right"},{default:s(({row:o})=>[i(b,{link:"",type:"primary",size:"small",onClick:f=>$(o)},{default:s(()=>[...t[3]||(t[3]=[m(" 查看 ",-1)])]),_:1},8,["onClick"])]),_:1})]),_:1},8,["data"])),[[I,d(p)]]),!d(p)&&d(l).length===0?(a(),g(N,{key:1,description:"暂无病历记录,开方后会自动显示",class:"mt-4"})):w("",!0)])}}}),zt=G(K,[["__scopeId","data-v-043d2738"]]);export{zt as default}; import{i as L,L as T,N as V,T as D,M as z,W as M}from"./element-plus-Bolc0EfP.js";import{ag as O}from"./tcm-B_U8CrQ9.js";import{f as P,b as F,w as j,ak as a,I as n,a as i,aN as s,O as m,H as w,aP as A,G as g,F as H,J as R}from"./@vue/runtime-core-C6bnekPw.js";import{y as d,n as k}from"./@vue/reactivity-DiY1c2vO.js";import{Q as c}from"./@vue/shared-mAAVTE9n.js";import{_ as G}from"./index-DiYdpaIG.js";import"./@vue/runtime-dom-DDAG46FW.js";import"./lodash-D3kF6u-c.js";import"./@element-plus/icons-vue-B0jSCQ-G.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./vue-router-QlpZ4wdW.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./@vueuse/core-1S4fJlii.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";const J={class:"case-record-list"},Q={key:0,class:"mb-3 flex justify-end"},W={key:0},Y={key:1,class:"text-gray-400"},q={class:"void-detail text-xs text-gray-500 mt-1"},K=P({__name:"CaseRecordList",props:{diagnosisId:{type:Number,default:0},readOnly:{type:Boolean,default:!1}},emits:["view","openPrescription"],setup(y,{expose:x,emit:C}){const _=y,h=C,l=k([]),p=k(!1),u=async()=>{if(_.diagnosisId){p.value=!0;try{const e=await O({diagnosis_id:_.diagnosisId});l.value=Array.isArray(e)?e:[]}catch(e){console.error("获取病历记录失败:",e),l.value=[]}finally{p.value=!1}}},S=e=>{if(!e)return"";const t=new Date(e*1e3);return`${t.getFullYear()}-${String(t.getMonth()+1).padStart(2,"0")}-${String(t.getDate()).padStart(2,"0")} ${String(t.getHours()).padStart(2,"0")}:${String(t.getMinutes()).padStart(2,"0")}`},$=e=>{h("view",e)},B=()=>{h("openPrescription")};return F(()=>{u()}),j(()=>_.diagnosisId,()=>{u()}),x({refresh:u}),(e,t)=>{const b=L,r=V,v=D,E=z,N=M,I=T;return a(),n("div",J,[y.readOnly?w("",!0):(a(),n("div",Q,[i(b,{type:"primary",size:"small",onClick:B},{default:s(()=>[...t[0]||(t[0]=[m("开方",-1)])]),_:1})])),A((a(),g(E,{data:d(l),border:""},{default:s(()=>[i(r,{prop:"prescription_date",label:"就诊日期",width:"120"}),i(r,{prop:"visit_no",label:"门诊号",width:"120"}),i(r,{prop:"clinical_diagnosis",label:"临床诊断","min-width":"160","show-overflow-tooltip":""}),i(r,{label:"处方摘要","min-width":"180"},{default:s(({row:o})=>[o.herbs&&o.herbs.length?(a(),n("span",W,c(o.herbs.slice(0,3).map(f=>`${f.name}${f.dosage}`).join("、"))+c(o.herbs.length>3?"...":""),1)):(a(),n("span",Y,"—"))]),_:1}),i(r,{prop:"doctor_name",label:"医师",width:"90","show-overflow-tooltip":""}),i(r,{label:"状态",width:"140",align:"center"},{default:s(({row:o})=>[o.void_status===1?(a(),n(H,{key:0},[i(v,{type:"danger",size:"small"},{default:s(()=>[...t[1]||(t[1]=[m("已作废",-1)])]),_:1}),R("div",q,c(o.void_by_name||"—")+" "+c(S(o.void_time)),1)],64)):(a(),g(v,{key:1,type:"success",size:"small"},{default:s(()=>[...t[2]||(t[2]=[m("正常",-1)])]),_:1}))]),_:1}),i(r,{label:"操作",width:"120",fixed:"right"},{default:s(({row:o})=>[i(b,{link:"",type:"primary",size:"small",onClick:f=>$(o)},{default:s(()=>[...t[3]||(t[3]=[m(" 查看 ",-1)])]),_:1},8,["onClick"])]),_:1})]),_:1},8,["data"])),[[I,d(p)]]),!d(p)&&d(l).length===0?(a(),g(N,{key:1,description:"暂无病历记录,开方后会自动显示",class:"mt-4"})):w("",!0)])}}}),zt=G(K,[["__scopeId","data-v-043d2738"]]);export{zt as default};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
import{_ as o}from"./MediaSourceSelect.vue_vue_type_script_setup_true_lang-CMN0ydu1.js";import"./element-plus-CXrrycLn.js";import"./@vue/runtime-dom-DDAG46FW.js";import"./lodash-D3kF6u-c.js";import"./@vue/runtime-core-C6bnekPw.js";import"./@vue/reactivity-DiY1c2vO.js";import"./@vue/shared-mAAVTE9n.js";import"./@element-plus/icons-vue-BAghejlR.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";export{o as default}; import{_ as o}from"./MediaSourceSelect.vue_vue_type_script_setup_true_lang-DBM4IcXw.js";import"./element-plus-Bolc0EfP.js";import"./@vue/runtime-dom-DDAG46FW.js";import"./lodash-D3kF6u-c.js";import"./@vue/runtime-core-C6bnekPw.js";import"./@vue/reactivity-DiY1c2vO.js";import"./@vue/shared-mAAVTE9n.js";import"./@element-plus/icons-vue-B0jSCQ-G.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";export{o as default};
@@ -1 +1 @@
import{Q as g,P as V}from"./element-plus-CXrrycLn.js";import{f as C,ak as o,G as s,aN as v,I as p,F as h,ap as B,H as k,A as m}from"./@vue/runtime-core-C6bnekPw.js";import{y as c}from"./@vue/reactivity-DiY1c2vO.js";import{p as w,o as S}from"./@vue/shared-mAAVTE9n.js";const N=C({__name:"MediaSourceSelect",props:{modelValue:{default:""},options:{},loading:{type:Boolean,default:!1},placeholder:{default:"请选择自媒体来源"},clearable:{type:Boolean,default:!0},filterable:{type:Boolean,default:!0},allowCreate:{type:Boolean,default:!1},disabled:{type:Boolean,default:!1},selectClass:{},selectStyle:{}},emits:["update:modelValue","change","visible-change"],setup(e,{emit:i}){const u=e,n=i,d=m(()=>(u.options||[]).map(l=>typeof l=="string"?{name:l}:{name:l.name,value:l.value})),f=m(()=>d.value.some(l=>l.name===u.modelValue)),b=l=>{n("visible-change",l)};return(l,t)=>{const r=g,y=V;return o(),s(y,{"model-value":e.modelValue,placeholder:e.placeholder,clearable:e.clearable,filterable:e.filterable,"allow-create":e.allowCreate,"default-first-option":e.allowCreate,disabled:e.disabled,loading:e.loading,class:S(e.selectClass),style:w(e.selectStyle),"onUpdate:modelValue":t[0]||(t[0]=a=>n("update:modelValue",a)),onChange:t[1]||(t[1]=a=>n("change",a)),onVisibleChange:b},{default:v(()=>[(o(!0),p(h,null,B(c(d),a=>(o(),s(r,{key:a.name,label:a.name,value:a.name},null,8,["label","value"]))),128)),e.modelValue&&!c(f)?(o(),s(r,{key:`__legacy_${e.modelValue}`,label:`${e.modelValue}(已停用)`,value:e.modelValue},null,8,["label","value"])):k("",!0)]),_:1},8,["model-value","placeholder","clearable","filterable","allow-create","default-first-option","disabled","loading","class","style"])}}});export{N as _}; import{Q as g,P as V}from"./element-plus-Bolc0EfP.js";import{f as C,ak as o,G as s,aN as v,I as p,F as h,ap as B,H as k,A as m}from"./@vue/runtime-core-C6bnekPw.js";import{y as c}from"./@vue/reactivity-DiY1c2vO.js";import{p as w,o as S}from"./@vue/shared-mAAVTE9n.js";const N=C({__name:"MediaSourceSelect",props:{modelValue:{default:""},options:{},loading:{type:Boolean,default:!1},placeholder:{default:"请选择自媒体来源"},clearable:{type:Boolean,default:!0},filterable:{type:Boolean,default:!0},allowCreate:{type:Boolean,default:!1},disabled:{type:Boolean,default:!1},selectClass:{},selectStyle:{}},emits:["update:modelValue","change","visible-change"],setup(e,{emit:i}){const u=e,n=i,d=m(()=>(u.options||[]).map(l=>typeof l=="string"?{name:l}:{name:l.name,value:l.value})),f=m(()=>d.value.some(l=>l.name===u.modelValue)),b=l=>{n("visible-change",l)};return(l,t)=>{const r=g,y=V;return o(),s(y,{"model-value":e.modelValue,placeholder:e.placeholder,clearable:e.clearable,filterable:e.filterable,"allow-create":e.allowCreate,"default-first-option":e.allowCreate,disabled:e.disabled,loading:e.loading,class:S(e.selectClass),style:w(e.selectStyle),"onUpdate:modelValue":t[0]||(t[0]=a=>n("update:modelValue",a)),onChange:t[1]||(t[1]=a=>n("change",a)),onVisibleChange:b},{default:v(()=>[(o(!0),p(h,null,B(c(d),a=>(o(),s(r,{key:a.name,label:a.name,value:a.name},null,8,["label","value"]))),128)),e.modelValue&&!c(f)?(o(),s(r,{key:`__legacy_${e.modelValue}`,label:`${e.modelValue}(已停用)`,value:e.modelValue},null,8,["label","value"])):k("",!0)]),_:1},8,["model-value","placeholder","clearable","filterable","allow-create","default-first-option","disabled","loading","class","style"])}}});export{N as _};
File diff suppressed because one or more lines are too long
@@ -1,2 +1,2 @@
import{i as se,W as ne,C as le,V as ae,v as re,d as me,a as de}from"./element-plus-CXrrycLn.js";import{_ as pe}from"./picker-DJROU0jC.js";import{e as ue,c as ce,i as g,_ as ge}from"./index-CO3hhC4Q.js";import{s as U}from"./@vue/runtime-dom-DDAG46FW.js";import{b as z,G as fe}from"./@element-plus/icons-vue-BAghejlR.js";import{a as P,d as ve}from"./patient-CXHiwyhn.js";import{h as _e}from"./perm-WF2yqSYy.js";import{f as ye,w as he,ak as o,I as n,G as k,aN as r,O as M,H as p,a as l,J as m,F as w,ap as E,A as ke}from"./@vue/runtime-core-C6bnekPw.js";import{n as f,y as N}from"./@vue/reactivity-DiY1c2vO.js";import{Q as T}from"./@vue/shared-mAAVTE9n.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./lodash-D3kF6u-c.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./index-f0T-c8zR.js";import"./index-x_mVhNP1.js";import"./index.vue_vue_type_script_setup_true_lang-iW1NfatD.js";import"./index-DjxDai3I.js";import"./index-CkJ0T1tI.js";import"./cos-js-sdk-v5-DSMN0LUT.js";import"./file-BdBRZBUu.js";import"./index.vue_vue_type_script_setup_true_lang-C6MkXIUg.js";import"./@vueuse/core-1S4fJlii.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./usePaging-VsbTxSU0.js";import"./vuedraggable-5bKFmC7X.js";import"./vue-BVs68q8v.js";import"./@vue/compiler-dom-CqayqxS7.js";import"./@vue/compiler-core-CvbQOXIO.js";import"./sortablejs-dsEAUXlE.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./vue-router-QlpZ4wdW.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";const we={class:"note-timeline-wrap"},Ie={key:0,class:"timeline-actions"},Ce={class:"upload-trigger"},xe={class:"upload-trigger"},Ve={key:1,class:"note-timeline"},Ee={class:"timeline-date"},Ne={class:"timeline-body"},be={key:0,class:"timeline-content"},Se={key:1,class:"timeline-images"},Ae={key:2,class:"timeline-images"},De={key:0,class:"thumb-wrap"},Be={key:1,class:"file-wrap"},Ue=["href","title"],ze={class:"file-name"},O=8e3,Pe=ye({__name:"NoteTimeline",props:{notes:{},diagnosisId:{},readonly:{type:Boolean}},emits:["refresh"],setup(d,{emit:j}){const u=d,I=j,X=ke(()=>_e(["doctor.appointment/addDoctorNote"])),v=f(!1),C=f(""),b=f(!1),H=ue(),_=t=>H.getImageUrl(t),S=f([]),A=f([]),y=f(0),h=f(0),J=["jpg","jpeg","png","gif","bmp","webp","svg"],D=t=>{var s;const e=((s=t.split(".").pop())==null?void 0:s.toLowerCase().split("?")[0])||"";return J.includes(e)},$=t=>{var s;const e=t.split("/");return decodeURIComponent(((s=e[e.length-1])==null?void 0:s.split("?")[0])||"文件")},Q=t=>t.filter(D).map(_),Z=(t,e)=>{const s=t.filter(D),x=t[e];return s.indexOf(x)},q=t=>t?t.split(` import{i as se,W as ne,C as le,V as ae,v as re,d as me,a as de}from"./element-plus-Bolc0EfP.js";import{_ as pe}from"./picker-D9fcgO3K.js";import{e as ue,c as ce,i as g,_ as ge}from"./index-DiYdpaIG.js";import{s as U}from"./@vue/runtime-dom-DDAG46FW.js";import{b as z,G as fe}from"./@element-plus/icons-vue-B0jSCQ-G.js";import{a as P,d as ve}from"./patient-BQT9cOvr.js";import{h as _e}from"./perm-1ezFGXyy.js";import{f as ye,w as he,ak as o,I as n,G as k,aN as r,O as M,H as p,a as l,J as m,F as w,ap as E,A as ke}from"./@vue/runtime-core-C6bnekPw.js";import{n as f,y as N}from"./@vue/reactivity-DiY1c2vO.js";import{Q as T}from"./@vue/shared-mAAVTE9n.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./lodash-D3kF6u-c.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./index-JpA6TixL.js";import"./index-HnXSQ-Mh.js";import"./index.vue_vue_type_script_setup_true_lang-EJPdzUeK.js";import"./index-DPe01mMR.js";import"./index-DITSRXGV.js";import"./cos-js-sdk-v5-DSMN0LUT.js";import"./file-BwO4oT1W.js";import"./index.vue_vue_type_script_setup_true_lang-Lcfr_gGB.js";import"./@vueuse/core-1S4fJlii.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./usePaging-VsbTxSU0.js";import"./vuedraggable-5bKFmC7X.js";import"./vue-BVs68q8v.js";import"./@vue/compiler-dom-CqayqxS7.js";import"./@vue/compiler-core-CvbQOXIO.js";import"./sortablejs-dsEAUXlE.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./vue-router-QlpZ4wdW.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";const we={class:"note-timeline-wrap"},Ie={key:0,class:"timeline-actions"},Ce={class:"upload-trigger"},xe={class:"upload-trigger"},Ve={key:1,class:"note-timeline"},Ee={class:"timeline-date"},Ne={class:"timeline-body"},be={key:0,class:"timeline-content"},Se={key:1,class:"timeline-images"},Ae={key:2,class:"timeline-images"},De={key:0,class:"thumb-wrap"},Be={key:1,class:"file-wrap"},Ue=["href","title"],ze={class:"file-name"},O=8e3,Pe=ye({__name:"NoteTimeline",props:{notes:{},diagnosisId:{},readonly:{type:Boolean}},emits:["refresh"],setup(d,{emit:j}){const u=d,I=j,X=ke(()=>_e(["doctor.appointment/addDoctorNote"])),v=f(!1),C=f(""),b=f(!1),H=ue(),_=t=>H.getImageUrl(t),S=f([]),A=f([]),y=f(0),h=f(0),J=["jpg","jpeg","png","gif","bmp","webp","svg"],D=t=>{var s;const e=((s=t.split(".").pop())==null?void 0:s.toLowerCase().split("?")[0])||"";return J.includes(e)},$=t=>{var s;const e=t.split("/");return decodeURIComponent(((s=e[e.length-1])==null?void 0:s.split("?")[0])||"文件")},Q=t=>t.filter(D).map(_),Z=(t,e)=>{const s=t.filter(D),x=t[e];return s.indexOf(x)},q=t=>t?t.split(`
`).filter(Boolean):[],K=t=>{if(!u.diagnosisId)return;const e=Array.isArray(t)?t:[t];if(e.length<=y.value){y.value=e.length;return}const s=e.slice(y.value);y.value=e.length,s.length>0&&P({diagnosis_id:u.diagnosisId,tongue_images:s}).then(()=>{g.msgSuccess("舌苔照片已添加"),I("refresh")})},Y=t=>{if(!u.diagnosisId)return;const e=Array.isArray(t)?t:[t];if(e.length<=h.value){h.value=e.length;return}const s=e.slice(h.value);h.value=e.length,s.length>0&&P({diagnosis_id:u.diagnosisId,report_files:s}).then(()=>{g.msgSuccess("检查报告已添加"),I("refresh")})};he(()=>u.notes,()=>{S.value=[],A.value=[],y.value=0,h.value=0});const ee=async()=>{if(!u.diagnosisId){g.msgWarning("当前无诊单,无法添加备注");return}const t=C.value.trim();if(!t){g.msgWarning("请输入备注内容");return}b.value=!0;try{await P({diagnosis_id:u.diagnosisId,content:t}),g.msgSuccess("备注保存成功"),C.value="",v.value=!1,I("refresh")}catch(e){g.msgError((e==null?void 0:e.message)||"保存失败")}finally{b.value=!1}},B=async(t,e,s)=>{try{await de.confirm("确认删除?","提示",{type:"warning"})}catch{return}await ve({note_id:t,image_type:e,image_path:s}),g.msgSuccess("已删除"),I("refresh")};return(t,e)=>{const s=se,x=ce,F=pe,G=re,V=me,te=ne,ie=le,oe=ae;return o(),n("div",we,[!d.readonly&&d.diagnosisId?(o(),n("div",Ie,[X.value?(o(),k(s,{key:0,type:"primary",plain:"",size:"small",onClick:e[0]||(e[0]=i=>v.value=!0)},{default:r(()=>[...e[6]||(e[6]=[M(" 添加备注 ",-1)])]),_:1})):p("",!0),l(F,{modelValue:S.value,"onUpdate:modelValue":e[1]||(e[1]=i=>S.value=i),limit:99,type:"image","exclude-domain":!0,onChange:K},{upload:r(()=>[m("div",Ce,[l(x,{size:20,name:"el-icon-Plus"}),e[7]||(e[7]=m("span",null,"舌苔照片",-1))])]),_:1},8,["modelValue"]),l(F,{modelValue:A.value,"onUpdate:modelValue":e[2]||(e[2]=i=>A.value=i),limit:99,type:"file","exclude-domain":!0,onChange:Y},{upload:r(()=>[m("div",xe,[l(x,{size:20,name:"el-icon-Plus"}),e[8]||(e[8]=m("span",null,"检查报告",-1))])]),_:1},8,["modelValue"])])):p("",!0),d.notes.length?(o(),n("div",Ve,[(o(!0),n(w,null,E(d.notes,i=>{var L,R;return o(),n("div",{key:i.id,class:"timeline-node"},[e[11]||(e[11]=m("div",{class:"timeline-dot"},null,-1)),m("div",Ee,T(i.note_date),1),m("div",Ne,[i.content?(o(),n("div",be,[(o(!0),n(w,null,E(q(i.content),(a,c)=>(o(),n("div",{key:c,class:"content-line"},T(a),1))),128))])):p("",!0),(L=i.tongue_images)!=null&&L.length?(o(),n("div",Se,[e[9]||(e[9]=m("span",{class:"images-label"},"舌苔照片",-1)),(o(!0),n(w,null,E(i.tongue_images,(a,c)=>(o(),n("div",{key:c,class:"thumb-wrap"},[l(G,{src:_(a),"preview-src-list":i.tongue_images.map(_),"initial-index":c,"z-index":O,fit:"cover",class:"timeline-thumb","preview-teleported":""},null,8,["src","preview-src-list","initial-index"]),d.readonly?p("",!0):(o(),k(V,{key:0,class:"thumb-delete",onClick:U(W=>B(i.id,"tongue_images",a),["stop"])},{default:r(()=>[l(N(z))]),_:1},8,["onClick"]))]))),128))])):p("",!0),(R=i.report_files)!=null&&R.length?(o(),n("div",Ae,[e[10]||(e[10]=m("span",{class:"images-label"},"检查报告",-1)),(o(!0),n(w,null,E(i.report_files,(a,c)=>(o(),n(w,{key:c},[D(a)?(o(),n("div",De,[l(G,{src:_(a),"preview-src-list":Q(i.report_files),"initial-index":Z(i.report_files,c),"z-index":O,fit:"cover",class:"timeline-thumb","preview-teleported":""},null,8,["src","preview-src-list","initial-index"]),d.readonly?p("",!0):(o(),k(V,{key:0,class:"thumb-delete",onClick:U(W=>B(i.id,"report_files",a),["stop"])},{default:r(()=>[l(N(z))]),_:1},8,["onClick"]))])):(o(),n("div",Be,[m("a",{href:_(a),target:"_blank",class:"file-link",title:$(a)},[l(V,{size:20},{default:r(()=>[l(N(fe))]),_:1}),m("span",ze,T($(a)),1)],8,Ue),d.readonly?p("",!0):(o(),k(V,{key:0,class:"file-delete",onClick:U(W=>B(i.id,"report_files",a),["stop"])},{default:r(()=>[l(N(z))]),_:1},8,["onClick"]))]))],64))),128))])):p("",!0)])])}),128))])):p("",!0),!d.notes.length&&d.readonly?(o(),k(te,{key:2,description:"暂无备注","image-size":48})):p("",!0),l(oe,{modelValue:v.value,"onUpdate:modelValue":e[5]||(e[5]=i=>v.value=i),title:"添加备注",width:"480px","append-to-body":""},{footer:r(()=>[l(s,{onClick:e[4]||(e[4]=i=>v.value=!1)},{default:r(()=>[...e[12]||(e[12]=[M("取消",-1)])]),_:1}),l(s,{type:"primary",loading:b.value,onClick:ee},{default:r(()=>[...e[13]||(e[13]=[M("保存",-1)])]),_:1},8,["loading"])]),default:r(()=>[l(ie,{modelValue:C.value,"onUpdate:modelValue":e[3]||(e[3]=i=>C.value=i),type:"textarea",rows:4,placeholder:"输入今日备注...",maxlength:"500","show-word-limit":""},null,8,["modelValue"])]),_:1},8,["modelValue"])])}}}),Mt=ge(Pe,[["__scopeId","data-v-530ad386"]]);export{Mt as default}; `).filter(Boolean):[],K=t=>{if(!u.diagnosisId)return;const e=Array.isArray(t)?t:[t];if(e.length<=y.value){y.value=e.length;return}const s=e.slice(y.value);y.value=e.length,s.length>0&&P({diagnosis_id:u.diagnosisId,tongue_images:s}).then(()=>{g.msgSuccess("舌苔照片已添加"),I("refresh")})},Y=t=>{if(!u.diagnosisId)return;const e=Array.isArray(t)?t:[t];if(e.length<=h.value){h.value=e.length;return}const s=e.slice(h.value);h.value=e.length,s.length>0&&P({diagnosis_id:u.diagnosisId,report_files:s}).then(()=>{g.msgSuccess("检查报告已添加"),I("refresh")})};he(()=>u.notes,()=>{S.value=[],A.value=[],y.value=0,h.value=0});const ee=async()=>{if(!u.diagnosisId){g.msgWarning("当前无诊单,无法添加备注");return}const t=C.value.trim();if(!t){g.msgWarning("请输入备注内容");return}b.value=!0;try{await P({diagnosis_id:u.diagnosisId,content:t}),g.msgSuccess("备注保存成功"),C.value="",v.value=!1,I("refresh")}catch(e){g.msgError((e==null?void 0:e.message)||"保存失败")}finally{b.value=!1}},B=async(t,e,s)=>{try{await de.confirm("确认删除?","提示",{type:"warning"})}catch{return}await ve({note_id:t,image_type:e,image_path:s}),g.msgSuccess("已删除"),I("refresh")};return(t,e)=>{const s=se,x=ce,F=pe,G=re,V=me,te=ne,ie=le,oe=ae;return o(),n("div",we,[!d.readonly&&d.diagnosisId?(o(),n("div",Ie,[X.value?(o(),k(s,{key:0,type:"primary",plain:"",size:"small",onClick:e[0]||(e[0]=i=>v.value=!0)},{default:r(()=>[...e[6]||(e[6]=[M(" 添加备注 ",-1)])]),_:1})):p("",!0),l(F,{modelValue:S.value,"onUpdate:modelValue":e[1]||(e[1]=i=>S.value=i),limit:99,type:"image","exclude-domain":!0,onChange:K},{upload:r(()=>[m("div",Ce,[l(x,{size:20,name:"el-icon-Plus"}),e[7]||(e[7]=m("span",null,"舌苔照片",-1))])]),_:1},8,["modelValue"]),l(F,{modelValue:A.value,"onUpdate:modelValue":e[2]||(e[2]=i=>A.value=i),limit:99,type:"file","exclude-domain":!0,onChange:Y},{upload:r(()=>[m("div",xe,[l(x,{size:20,name:"el-icon-Plus"}),e[8]||(e[8]=m("span",null,"检查报告",-1))])]),_:1},8,["modelValue"])])):p("",!0),d.notes.length?(o(),n("div",Ve,[(o(!0),n(w,null,E(d.notes,i=>{var L,R;return o(),n("div",{key:i.id,class:"timeline-node"},[e[11]||(e[11]=m("div",{class:"timeline-dot"},null,-1)),m("div",Ee,T(i.note_date),1),m("div",Ne,[i.content?(o(),n("div",be,[(o(!0),n(w,null,E(q(i.content),(a,c)=>(o(),n("div",{key:c,class:"content-line"},T(a),1))),128))])):p("",!0),(L=i.tongue_images)!=null&&L.length?(o(),n("div",Se,[e[9]||(e[9]=m("span",{class:"images-label"},"舌苔照片",-1)),(o(!0),n(w,null,E(i.tongue_images,(a,c)=>(o(),n("div",{key:c,class:"thumb-wrap"},[l(G,{src:_(a),"preview-src-list":i.tongue_images.map(_),"initial-index":c,"z-index":O,fit:"cover",class:"timeline-thumb","preview-teleported":""},null,8,["src","preview-src-list","initial-index"]),d.readonly?p("",!0):(o(),k(V,{key:0,class:"thumb-delete",onClick:U(W=>B(i.id,"tongue_images",a),["stop"])},{default:r(()=>[l(N(z))]),_:1},8,["onClick"]))]))),128))])):p("",!0),(R=i.report_files)!=null&&R.length?(o(),n("div",Ae,[e[10]||(e[10]=m("span",{class:"images-label"},"检查报告",-1)),(o(!0),n(w,null,E(i.report_files,(a,c)=>(o(),n(w,{key:c},[D(a)?(o(),n("div",De,[l(G,{src:_(a),"preview-src-list":Q(i.report_files),"initial-index":Z(i.report_files,c),"z-index":O,fit:"cover",class:"timeline-thumb","preview-teleported":""},null,8,["src","preview-src-list","initial-index"]),d.readonly?p("",!0):(o(),k(V,{key:0,class:"thumb-delete",onClick:U(W=>B(i.id,"report_files",a),["stop"])},{default:r(()=>[l(N(z))]),_:1},8,["onClick"]))])):(o(),n("div",Be,[m("a",{href:_(a),target:"_blank",class:"file-link",title:$(a)},[l(V,{size:20},{default:r(()=>[l(N(fe))]),_:1}),m("span",ze,T($(a)),1)],8,Ue),d.readonly?p("",!0):(o(),k(V,{key:0,class:"file-delete",onClick:U(W=>B(i.id,"report_files",a),["stop"])},{default:r(()=>[l(N(z))]),_:1},8,["onClick"]))]))],64))),128))])):p("",!0)])])}),128))])):p("",!0),!d.notes.length&&d.readonly?(o(),k(te,{key:2,description:"暂无备注","image-size":48})):p("",!0),l(oe,{modelValue:v.value,"onUpdate:modelValue":e[5]||(e[5]=i=>v.value=i),title:"添加备注",width:"480px","append-to-body":""},{footer:r(()=>[l(s,{onClick:e[4]||(e[4]=i=>v.value=!1)},{default:r(()=>[...e[12]||(e[12]=[M("取消",-1)])]),_:1}),l(s,{type:"primary",loading:b.value,onClick:ee},{default:r(()=>[...e[13]||(e[13]=[M("保存",-1)])]),_:1},8,["loading"])]),default:r(()=>[l(ie,{modelValue:C.value,"onUpdate:modelValue":e[3]||(e[3]=i=>C.value=i),type:"textarea",rows:4,placeholder:"输入今日备注...",maxlength:"500","show-word-limit":""},null,8,["modelValue"])]),_:1},8,["modelValue"])])}}}),Mt=ge(Pe,[["__scopeId","data-v-530ad386"]]);export{Mt as default};
@@ -1 +1 @@
import{m as x,f as y,a as B}from"./diag-display-DCz_VAqj.js";import{f as w,ak as I,I as P,J as a,H as N}from"./@vue/runtime-core-C6bnekPw.js";import{Q as i}from"./@vue/shared-mAAVTE9n.js";import{y as e}from"./@vue/reactivity-DiY1c2vO.js";import{_ as V}from"./index-CO3hhC4Q.js";import"./lodash-D3kF6u-c.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./vue-router-QlpZ4wdW.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./element-plus-CXrrycLn.js";import"./@vue/runtime-dom-DDAG46FW.js";import"./@element-plus/icons-vue-BAghejlR.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./@vueuse/core-1S4fJlii.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";const b={class:"card patient-card"},D={class:"patient-hero"},E={class:"patient-name"},G={class:"patient-meta"},H={key:0},J=w({__name:"PatientInfoCard",props:{apt:{default:()=>({})},diag:{default:()=>({})}},setup(t){return(Q,o)=>{var m,r,n,d,p,s,c,l,g,f,u,h,v,k,C;return I(),P("div",b,[o[0]||(o[0]=a("div",{class:"card-title"},"患者信息",-1)),a("div",D,[a("div",E,i(((m=t.apt)==null?void 0:m.patient_name)||"—"),1),a("div",G,[a("div",null,i(e(x)((r=t.apt)==null?void 0:r.patient_phone))+" · "+i(e(y)((n=t.diag)==null?void 0:n.gender))+" · "+i(((d=t.diag)==null?void 0:d.age)!=null?t.diag.age+"岁":"—"),1),a("div",null,i((p=t.diag)!=null&&p.height?t.diag.height+"cm":"—")+" / "+i((s=t.diag)!=null&&s.weight?t.diag.weight+"kg":"—")+" · "+i(((c=t.diag)==null?void 0:c.region)||"—"),1),a("div",null," 预约:"+i((l=t.apt)==null?void 0:l.appointment_date)+" "+i((g=t.apt)==null?void 0:g.appointment_time)+" · "+i(e(B)((f=t.apt)==null?void 0:f.period)),1),a("div",null,"医生:"+i(((u=t.apt)==null?void 0:u.doctor_name)||"—")+" 客服:"+i(((h=t.apt)==null?void 0:h.assistant_name)||"—"),1),a("div",null," 状态:"+i(((v=t.apt)==null?void 0:v.status_desc)||"—")+" · "+i((k=t.apt)!=null&&k.has_prescription?"已开方":"未开方"),1),(C=t.apt)!=null&&C.remark?(I(),P("div",H,"备注:"+i(t.apt.remark),1)):N("",!0)])])])}}}),Ct=V(J,[["__scopeId","data-v-1b0de09c"]]);export{Ct as default}; import{m as x,f as y,a as B}from"./diag-display-DCz_VAqj.js";import{f as w,ak as I,I as P,J as a,H as N}from"./@vue/runtime-core-C6bnekPw.js";import{Q as i}from"./@vue/shared-mAAVTE9n.js";import{y as e}from"./@vue/reactivity-DiY1c2vO.js";import{_ as V}from"./index-DiYdpaIG.js";import"./lodash-D3kF6u-c.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./vue-router-QlpZ4wdW.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./element-plus-Bolc0EfP.js";import"./@vue/runtime-dom-DDAG46FW.js";import"./@element-plus/icons-vue-B0jSCQ-G.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./@vueuse/core-1S4fJlii.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";const b={class:"card patient-card"},D={class:"patient-hero"},E={class:"patient-name"},G={class:"patient-meta"},H={key:0},J=w({__name:"PatientInfoCard",props:{apt:{default:()=>({})},diag:{default:()=>({})}},setup(t){return(Q,o)=>{var m,r,n,d,p,s,c,l,g,f,u,h,v,k,C;return I(),P("div",b,[o[0]||(o[0]=a("div",{class:"card-title"},"患者信息",-1)),a("div",D,[a("div",E,i(((m=t.apt)==null?void 0:m.patient_name)||"—"),1),a("div",G,[a("div",null,i(e(x)((r=t.apt)==null?void 0:r.patient_phone))+" · "+i(e(y)((n=t.diag)==null?void 0:n.gender))+" · "+i(((d=t.diag)==null?void 0:d.age)!=null?t.diag.age+"岁":"—"),1),a("div",null,i((p=t.diag)!=null&&p.height?t.diag.height+"cm":"—")+" / "+i((s=t.diag)!=null&&s.weight?t.diag.weight+"kg":"—")+" · "+i(((c=t.diag)==null?void 0:c.region)||"—"),1),a("div",null," 预约:"+i((l=t.apt)==null?void 0:l.appointment_date)+" "+i((g=t.apt)==null?void 0:g.appointment_time)+" · "+i(e(B)((f=t.apt)==null?void 0:f.period)),1),a("div",null,"医生:"+i(((u=t.apt)==null?void 0:u.doctor_name)||"—")+" 客服:"+i(((h=t.apt)==null?void 0:h.assistant_name)||"—"),1),a("div",null," 状态:"+i(((v=t.apt)==null?void 0:v.status_desc)||"—")+" · "+i((k=t.apt)!=null&&k.has_prescription?"已开方":"未开方"),1),(C=t.apt)!=null&&C.remark?(I(),P("div",H,"备注:"+i(t.apt.remark),1)):N("",!0)])])])}}}),Ct=V(J,[["__scopeId","data-v-1b0de09c"]]);export{Ct as default};
@@ -1 +0,0 @@
.patient-order-list[data-v-4a0ef613]{padding:4px 0}.po-empty-tip[data-v-4a0ef613]{padding:24px 0}
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
import{W as B,M as A,N as R,T as j,i as q,L as z}from"./element-plus-CXrrycLn.js";import{_ as $}from"./index.vue_vue_type_script_setup_true_lang-iW1NfatD.js";import{u as G}from"./usePaging-VsbTxSU0.js";import{q as J}from"./tcm-ujpoSvdA.js";import M from"./PrescriptionOrderDetailDrawer-CIHvcvfO.js";import{w as Q,a as S,f as U}from"./prescription-order-utils-70xOmaF9.js";import{f as W,w as H,as as K,ak as m,I as u,a as e,F as X,aP as b,G as h,aN as a,J as w,O as p,A as y}from"./@vue/runtime-core-C6bnekPw.js";import{y as n,a as Y,n as Z,r as tt}from"./@vue/reactivity-DiY1c2vO.js";import{Q as d}from"./@vue/shared-mAAVTE9n.js";import{_ as et}from"./index-CO3hhC4Q.js";import"./@vue/runtime-dom-DDAG46FW.js";import"./lodash-D3kF6u-c.js";import"./@element-plus/icons-vue-BAghejlR.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./vue-router-QlpZ4wdW.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./@vueuse/core-1S4fJlii.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";const ot={class:"patient-order-list"},it={key:0,class:"po-empty-tip"},at={class:"text-red-500 font-semibold"},rt={class:"flex justify-end mt-3"},st=W({__name:"PatientOrderList",props:{diagnosisId:{},patientId:{}},setup(x,{expose:k}){const c=x,_=y(()=>{const o=Number(c.patientId);return Number.isFinite(o)&&o>0?o:0}),f=y(()=>_.value>0),l=tt({}),{pager:r,getLists:g,resetPage:I}=G({fetchFun:J,params:l,size:10}),N=()=>{Object.keys(l).forEach(o=>delete l[o]),c.diagnosisId>0&&(l.context_diagnosis_id=c.diagnosisId),f.value&&(l.patient_id=_.value),l.scene="diagnosis_edit"},v=Z();function D(o){var i;(i=v.value)==null||i.open(o)}const T=o=>{const i=Number(o);return Number.isFinite(i)?i.toFixed(2):"0.00"};return H(()=>[c.diagnosisId,_.value],()=>{if(!f.value){r.lists=[],r.count=0;return}N(),I()},{immediate:!0}),k({refresh:()=>g()}),(o,i)=>{const E=B,s=R,P=j,C=q,O=A,F=$,L=K("perms"),V=z;return m(),u("div",ot,[f.value?(m(),u(X,{key:1},[b((m(),h(O,{data:n(r).lists,border:"",stripe:"","empty-text":"暂无业务订单"},{default:a(()=>[e(s,{label:"订单编号",prop:"order_no","min-width":"200","show-overflow-tooltip":""}),e(s,{label:"金额",width:"120",align:"right"},{default:a(({row:t})=>[w("span",at,"¥"+d(T(t.amount)),1)]),_:1}),e(s,{label:"医生",prop:"doctor_name",width:"110","show-overflow-tooltip":""},{default:a(({row:t})=>[p(d(t.doctor_name||"—"),1)]),_:1}),e(s,{label:"医助",prop:"assistant_name",width:"110","show-overflow-tooltip":""},{default:a(({row:t})=>[p(d(t.assistant_name||"—"),1)]),_:1}),e(s,{label:"履约状态",width:"110"},{default:a(({row:t})=>[e(P,{type:n(Q)(t.fulfillment_status),size:"small",effect:"light"},{default:a(()=>[p(d(n(S)(t.fulfillment_status)),1)]),_:2},1032,["type"])]),_:1}),e(s,{label:"创建时间","min-width":"170"},{default:a(({row:t})=>[p(d(n(U)(t.create_time)),1)]),_:1}),e(s,{label:"操作",width:"100",fixed:"right"},{default:a(({row:t})=>[b((m(),h(C,{type:"primary",link:"",onClick:nt=>D(t.id)},{default:a(()=>[...i[1]||(i[1]=[p("查看详情",-1)])]),_:1},8,["onClick"])),[[L,["tcm.prescriptionOrder/detail"]]])]),_:1})]),_:1},8,["data"])),[[V,n(r).loading]]),w("div",rt,[e(F,{modelValue:n(r),"onUpdate:modelValue":i[0]||(i[0]=t=>Y(r)?r.value=t:null),onChange:n(g)},null,8,["modelValue","onChange"])])],64)):(m(),u("div",it,[e(E,{description:"当前诊单未携带患者ID,无法列出业务订单"})])),e(M,{ref_key:"detailDrawerRef",ref:v,readonly:"","append-to-body":""},null,512)])}}}),Kt=et(st,[["__scopeId","data-v-4a0ef613"]]);export{Kt as default};
@@ -0,0 +1 @@
.patient-order-list[data-v-fe72f000]{padding:4px 0}.po-empty-tip[data-v-fe72f000]{padding:24px 0}.po-revisit-offset-bar[data-v-fe72f000]{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;padding:12px 14px;border:1px solid var(--el-border-color-lighter);border-radius:8px;background:var(--el-fill-color-lighter)}.po-revisit-offset-bar__main[data-v-fe72f000]{display:flex;align-items:center;flex-wrap:wrap;gap:4px}
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 @@
@charset "UTF-8";.po-detail-drawer[data-v-04e38845] .el-drawer__header{margin-bottom:0;padding:16px 24px;border-bottom:1px solid var(--el-border-color-lighter)}.stat-card[data-v-04e38845]{border-radius:8px}.stat-title[data-v-04e38845]{font-weight:500}.po-panel[data-v-04e38845]{border-radius:8px;transition:all .3s}.po-panel[data-v-04e38845] .el-card__header{padding:14px 16px;background-color:var(--el-bg-color-page);border-bottom:1px solid var(--el-border-color-lighter)}.po-panel[data-v-04e38845] .el-card__body{padding:16px}.po-desc[data-v-04e38845] .el-descriptions__label{width:120px;color:var(--el-text-color-regular)}.po-audit-remark[data-v-04e38845]{color:var(--el-color-danger);font-weight:600;white-space:pre-wrap;word-break:break-word}.audit-stamp[data-v-04e38845]{position:absolute;top:18px;right:-14px;width:72px;height:72px;border:3px solid currentColor;border-radius:50%;display:flex;align-items:center;justify-content:center;transform:rotate(20deg);opacity:.8;pointer-events:none;z-index:10;-webkit-user-select:none;-moz-user-select:none;user-select:none;font-weight:700;font-size:13px;letter-spacing:1px;box-shadow:inset 0 0 0 1px #ffffff80}.audit-stamp[data-v-04e38845]:after{content:"";position:absolute;top:4px;left:4px;right:4px;bottom:4px;border:1px double currentColor;border-radius:50%;opacity:.6}.audit-stamp .stamp-inner[data-v-04e38845]{text-align:center;line-height:1.1}.stamp-pass[data-v-04e38845]{color:var(--el-color-success)}.stamp-reject[data-v-04e38845]{color:var(--el-color-danger)}.po-diagnosis-creator-dept-breadcrumb[data-v-04e38845] .el-breadcrumb__item{display:inline-flex;float:none}.po-diagnosis-creator-dept-breadcrumb[data-v-04e38845] .el-breadcrumb__separator{margin:0 2px 0 4px}
@@ -1 +0,0 @@
@charset "UTF-8";.po-detail-drawer[data-v-0e106ce1] .el-drawer__header{margin-bottom:0;padding:16px 24px;border-bottom:1px solid var(--el-border-color-lighter)}.stat-card[data-v-0e106ce1]{border-radius:8px}.stat-title[data-v-0e106ce1]{font-weight:500}.po-panel[data-v-0e106ce1]{border-radius:8px;transition:all .3s}.po-panel[data-v-0e106ce1] .el-card__header{padding:14px 16px;background-color:var(--el-bg-color-page);border-bottom:1px solid var(--el-border-color-lighter)}.po-panel[data-v-0e106ce1] .el-card__body{padding:16px}.po-desc[data-v-0e106ce1] .el-descriptions__label{width:120px;color:var(--el-text-color-regular)}.po-audit-remark[data-v-0e106ce1]{color:var(--el-color-danger);font-weight:600;white-space:pre-wrap;word-break:break-word}.audit-stamp[data-v-0e106ce1]{position:absolute;top:18px;right:-14px;width:72px;height:72px;border:3px solid currentColor;border-radius:50%;display:flex;align-items:center;justify-content:center;transform:rotate(20deg);opacity:.8;pointer-events:none;z-index:10;-webkit-user-select:none;-moz-user-select:none;user-select:none;font-weight:700;font-size:13px;letter-spacing:1px;box-shadow:inset 0 0 0 1px #ffffff80}.audit-stamp[data-v-0e106ce1]:after{content:"";position:absolute;top:4px;left:4px;right:4px;bottom:4px;border:1px double currentColor;border-radius:50%;opacity:.6}.audit-stamp .stamp-inner[data-v-0e106ce1]{text-align:center;line-height:1.1}.stamp-pass[data-v-0e106ce1]{color:var(--el-color-success)}.stamp-reject[data-v-0e106ce1]{color:var(--el-color-danger)}.po-diagnosis-creator-dept-breadcrumb[data-v-0e106ce1] .el-breadcrumb__item{display:inline-flex;float:none}.po-diagnosis-creator-dept-breadcrumb[data-v-0e106ce1] .el-breadcrumb__separator{margin:0 2px 0 4px}
@@ -1 +1 @@
import{S as L}from"./element-plus-CXrrycLn.js";import B from"./RecordingVideoPlayer-BZKhGYiH.js";import{e as H,_ as I}from"./index-CO3hhC4Q.js";import{f as w,ak as n,I as m,J as p,F as v,G as u,aN as _,O as k,H as y,ap as R,A as d}from"./@vue/runtime-core-C6bnekPw.js";import{Q as q}from"./@vue/shared-mAAVTE9n.js";import"./@vue/runtime-dom-DDAG46FW.js";import"./lodash-D3kF6u-c.js";import"./@vue/reactivity-DiY1c2vO.js";import"./@element-plus/icons-vue-BAghejlR.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./vue-router-QlpZ4wdW.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./@vueuse/core-1S4fJlii.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";const C={key:0,class:"recording-list"},N={class:"recording-item"},P={key:1,class:"recording-alternates"},V={class:"recording-alternates__links"},A={key:1,class:"text-gray-400"},E=w({__name:"RecordingPlaybackBlock",props:{recordId:{},urls:{}},setup(c){const $=c,h=H(),l=d(()=>{const e=$.urls||[],t=new Set,r=[];for(const s of e){const o=String(s??"").trim();!o||t.has(o)||(t.add(o),r.push(o))}return r}),a=d(()=>{const e=l.value;if(!e.length)return null;const t=e.find(i=>/\.mp4(\?|#|$)/i.test(i));if(t)return t;const r=e.find(i=>/\.m3u8(\?|#|$)/i.test(i)&&/vod-qcloud\.com/i.test(i));if(r)return r;const s=e.find(i=>/\.m3u8(\?|#|$)/i.test(i)&&/\.cos\.[^/]+\.myqcloud\.com/i.test(i));if(s)return s;const o=e.find(i=>/\.m3u8(\?|#|$)/i.test(i));return o||e[0]}),f=d(()=>{const e=l.value,t=a.value;return t?e.filter(r=>r!==t):e.slice(1)});function S(e){if(!e||typeof e!="string")return!1;const t=e.trim();return/^https?:\/\//i.test(t)?/\.(mp4|webm|ogg|mov|mkv|m4v|m3u8)(\?|#|$)/i.test(t):/\.(mp4|webm|ogg|mov|mkv|m4v|m3u8)(\?|#|$)/i.test(t)||t.startsWith("/uploads/")}function g(e){return h.getImageUrl(String(e||"").trim())}function b(e,t){const r=e.trim();return/\.mp4(\?|#|$)/i.test(r)?`MP4 ${t+1}`:/vod-qcloud\.com/i.test(r)&&/\.m3u8/i.test(r)?`点播 ${t+1}`:/\.cos\.[^/]+\.myqcloud\.com/i.test(r)&&/\.m3u8/i.test(r)?`COS HLS ${t+1}`:/\.m3u8/i.test(r)?`HLS ${t+1}`:`链接 ${t+1}`}return(e,t)=>{const r=L;return l.value.length?(n(),m("div",C,[p("div",N,[a.value?(n(),m(v,{key:0},[S(a.value)?(n(),u(B,{key:`${c.recordId}-${a.value}`,src:a.value},null,8,["src"])):(n(),u(r,{key:1,href:g(a.value),target:"_blank",type:"primary"},{default:_(()=>[...t[0]||(t[0]=[k(" 打开回放 ",-1)])]),_:1},8,["href"]))],64)):y("",!0),f.value.length?(n(),m("div",P,[t[1]||(t[1]=p("span",{class:"recording-alternates__label"},"备用地址",-1)),p("div",V,[(n(!0),m(v,null,R(f.value,(s,o)=>(n(),u(r,{key:`${c.recordId}-alt-${o}-${s.slice(-32)}`,href:g(s),target:"_blank",type:"primary",class:"recording-alternates__link"},{default:_(()=>[k(q(b(s,o)),1)]),_:2},1032,["href"]))),128))])])):y("",!0)])])):(n(),m("span",A,"暂无"))}}}),ht=I(E,[["__scopeId","data-v-d67b2e91"]]);export{ht as default}; import{S as L}from"./element-plus-Bolc0EfP.js";import B from"./RecordingVideoPlayer-Dm4eRh1Y.js";import{e as H,_ as I}from"./index-DiYdpaIG.js";import{f as w,ak as n,I as m,J as p,F as v,G as u,aN as _,O as k,H as y,ap as R,A as d}from"./@vue/runtime-core-C6bnekPw.js";import{Q as q}from"./@vue/shared-mAAVTE9n.js";import"./@vue/runtime-dom-DDAG46FW.js";import"./lodash-D3kF6u-c.js";import"./@vue/reactivity-DiY1c2vO.js";import"./@element-plus/icons-vue-B0jSCQ-G.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./vue-router-QlpZ4wdW.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./@vueuse/core-1S4fJlii.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";const C={key:0,class:"recording-list"},N={class:"recording-item"},P={key:1,class:"recording-alternates"},V={class:"recording-alternates__links"},A={key:1,class:"text-gray-400"},E=w({__name:"RecordingPlaybackBlock",props:{recordId:{},urls:{}},setup(c){const $=c,h=H(),l=d(()=>{const e=$.urls||[],t=new Set,r=[];for(const s of e){const o=String(s??"").trim();!o||t.has(o)||(t.add(o),r.push(o))}return r}),a=d(()=>{const e=l.value;if(!e.length)return null;const t=e.find(i=>/\.mp4(\?|#|$)/i.test(i));if(t)return t;const r=e.find(i=>/\.m3u8(\?|#|$)/i.test(i)&&/vod-qcloud\.com/i.test(i));if(r)return r;const s=e.find(i=>/\.m3u8(\?|#|$)/i.test(i)&&/\.cos\.[^/]+\.myqcloud\.com/i.test(i));if(s)return s;const o=e.find(i=>/\.m3u8(\?|#|$)/i.test(i));return o||e[0]}),f=d(()=>{const e=l.value,t=a.value;return t?e.filter(r=>r!==t):e.slice(1)});function S(e){if(!e||typeof e!="string")return!1;const t=e.trim();return/^https?:\/\//i.test(t)?/\.(mp4|webm|ogg|mov|mkv|m4v|m3u8)(\?|#|$)/i.test(t):/\.(mp4|webm|ogg|mov|mkv|m4v|m3u8)(\?|#|$)/i.test(t)||t.startsWith("/uploads/")}function g(e){return h.getImageUrl(String(e||"").trim())}function b(e,t){const r=e.trim();return/\.mp4(\?|#|$)/i.test(r)?`MP4 ${t+1}`:/vod-qcloud\.com/i.test(r)&&/\.m3u8/i.test(r)?`点播 ${t+1}`:/\.cos\.[^/]+\.myqcloud\.com/i.test(r)&&/\.m3u8/i.test(r)?`COS HLS ${t+1}`:/\.m3u8/i.test(r)?`HLS ${t+1}`:`链接 ${t+1}`}return(e,t)=>{const r=L;return l.value.length?(n(),m("div",C,[p("div",N,[a.value?(n(),m(v,{key:0},[S(a.value)?(n(),u(B,{key:`${c.recordId}-${a.value}`,src:a.value},null,8,["src"])):(n(),u(r,{key:1,href:g(a.value),target:"_blank",type:"primary"},{default:_(()=>[...t[0]||(t[0]=[k(" 打开回放 ",-1)])]),_:1},8,["href"]))],64)):y("",!0),f.value.length?(n(),m("div",P,[t[1]||(t[1]=p("span",{class:"recording-alternates__label"},"备用地址",-1)),p("div",V,[(n(!0),m(v,null,R(f.value,(s,o)=>(n(),u(r,{key:`${c.recordId}-alt-${o}-${s.slice(-32)}`,href:g(s),target:"_blank",type:"primary",class:"recording-alternates__link"},{default:_(()=>[k(q(b(s,o)),1)]),_:2},1032,["href"]))),128))])])):y("",!0)])])):(n(),m("span",A,"暂无"))}}}),ht=I(E,[["__scopeId","data-v-d67b2e91"]]);export{ht as default};
File diff suppressed because one or more lines are too long
@@ -1,2 +1,2 @@
import{C as I,i as V,W as w}from"./element-plus-CXrrycLn.js";import{X as T}from"./tcm-ujpoSvdA.js";import{i as C,_ as E}from"./index-CO3hhC4Q.js";import{f as z,ak as t,I as e,a as p,J as l,aN as H,O as S,H as m,F as c,ap as u,G as F}from"./@vue/runtime-core-C6bnekPw.js";import{Q as f}from"./@vue/shared-mAAVTE9n.js";import{n as g}from"./@vue/reactivity-DiY1c2vO.js";/* empty css */import"./@vue/runtime-dom-DDAG46FW.js";import"./lodash-D3kF6u-c.js";import"./@element-plus/icons-vue-BAghejlR.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./vue-router-QlpZ4wdW.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./@vueuse/core-1S4fJlii.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";const L={class:"tracking-timeline-wrap"},M={key:0,class:"timeline-input"},A={class:"input-actions"},D={key:1,class:"tracking-timeline"},G={class:"timeline-date"},J={class:"timeline-body"},O={key:0,class:"timeline-content"},Q=z({__name:"TrackingNoteTimeline",props:{notes:{},diagnosisId:{},readonly:{type:Boolean}},emits:["refresh"],setup(i,{emit:v}){const d=i,y=v,r=g(""),a=g(!1),_=o=>o?o.split(` import{C as I,i as V,W as w}from"./element-plus-Bolc0EfP.js";import{Z as T}from"./tcm-B_U8CrQ9.js";import{i as C,_ as E}from"./index-DiYdpaIG.js";import{f as z,ak as t,I as e,a as p,J as l,aN as H,O as S,H as m,F as c,ap as u,G as F}from"./@vue/runtime-core-C6bnekPw.js";import{Q as f}from"./@vue/shared-mAAVTE9n.js";import{n as g}from"./@vue/reactivity-DiY1c2vO.js";/* empty css */import"./@vue/runtime-dom-DDAG46FW.js";import"./lodash-D3kF6u-c.js";import"./@element-plus/icons-vue-B0jSCQ-G.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./vue-router-QlpZ4wdW.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./@vueuse/core-1S4fJlii.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";const L={class:"tracking-timeline-wrap"},M={key:0,class:"timeline-input"},A={class:"input-actions"},D={key:1,class:"tracking-timeline"},G={class:"timeline-date"},J={class:"timeline-body"},O={key:0,class:"timeline-content"},Q=z({__name:"TrackingNoteTimeline",props:{notes:{},diagnosisId:{},readonly:{type:Boolean}},emits:["refresh"],setup(i,{emit:v}){const d=i,y=v,r=g(""),a=g(!1),_=o=>o?o.split(`
`).filter(Boolean):[],k=async()=>{if(!d.diagnosisId)return;const o=r.value.trim();if(o){a.value=!0;try{await T({diagnosis_id:d.diagnosisId,tracking_content:o}),C.msgSuccess("已添加"),r.value="",y("refresh")}finally{a.value=!1}}};return(o,n)=>{const h=I,b=V,N=w;return t(),e("div",L,[!i.readonly&&i.diagnosisId?(t(),e("div",M,[p(h,{modelValue:r.value,"onUpdate:modelValue":n[0]||(n[0]=s=>r.value=s),type:"textarea",rows:2,placeholder:"输入跟踪备注,回车换行;保存后将以「[HH:MM] 内容」追加到当天记录",maxlength:"1000","show-word-limit":"",resize:"none",disabled:a.value},null,8,["modelValue","disabled"]),l("div",A,[p(b,{type:"primary",size:"small",loading:a.value,disabled:!r.value.trim(),onClick:k},{default:H(()=>[...n[1]||(n[1]=[S(" 添加 ",-1)])]),_:1},8,["loading","disabled"])])])):m("",!0),i.notes.length?(t(),e("div",D,[(t(!0),e(c,null,u(i.notes,s=>(t(),e("div",{key:s.id,class:"timeline-node"},[n[2]||(n[2]=l("div",{class:"timeline-dot"},null,-1)),l("div",G,f(s.note_date),1),l("div",J,[s.content?(t(),e("div",O,[(t(!0),e(c,null,u(_(s.content),(x,B)=>(t(),e("div",{key:B,class:"content-line"},f(x),1))),128))])):m("",!0)])]))),128))])):m("",!0),!i.notes.length&&i.readonly?(t(),F(N,{key:2,description:"暂无跟踪备注","image-size":48})):m("",!0)])}}}),Tt=E(Q,[["__scopeId","data-v-82b635bd"]]);export{Tt as default}; `).filter(Boolean):[],k=async()=>{if(!d.diagnosisId)return;const o=r.value.trim();if(o){a.value=!0;try{await T({diagnosis_id:d.diagnosisId,tracking_content:o}),C.msgSuccess("已添加"),r.value="",y("refresh")}finally{a.value=!1}}};return(o,n)=>{const h=I,b=V,N=w;return t(),e("div",L,[!i.readonly&&i.diagnosisId?(t(),e("div",M,[p(h,{modelValue:r.value,"onUpdate:modelValue":n[0]||(n[0]=s=>r.value=s),type:"textarea",rows:2,placeholder:"输入跟踪备注,回车换行;保存后将以「[HH:MM] 内容」追加到当天记录",maxlength:"1000","show-word-limit":"",resize:"none",disabled:a.value},null,8,["modelValue","disabled"]),l("div",A,[p(b,{type:"primary",size:"small",loading:a.value,disabled:!r.value.trim(),onClick:k},{default:H(()=>[...n[1]||(n[1]=[S(" 添加 ",-1)])]),_:1},8,["loading","disabled"])])])):m("",!0),i.notes.length?(t(),e("div",D,[(t(!0),e(c,null,u(i.notes,s=>(t(),e("div",{key:s.id,class:"timeline-node"},[n[2]||(n[2]=l("div",{class:"timeline-dot"},null,-1)),l("div",G,f(s.note_date),1),l("div",J,[s.content?(t(),e("div",O,[(t(!0),e(c,null,u(_(s.content),(x,B)=>(t(),e("div",{key:B,class:"content-line"},f(x),1))),128))])):m("",!0)])]))),128))])):m("",!0),!i.notes.length&&i.readonly?(t(),F(N,{key:2,description:"暂无跟踪备注","image-size":48})):m("",!0)])}}}),Tt=E(Q,[["__scopeId","data-v-82b635bd"]]);export{Tt as default};
@@ -1 +1 @@
import{_ as o}from"./account-adjust.vue_vue_type_script_setup_true_lang-BV2QPxh2.js";import"./element-plus-CXrrycLn.js";import"./@vue/runtime-dom-DDAG46FW.js";import"./lodash-D3kF6u-c.js";import"./@vue/runtime-core-C6bnekPw.js";import"./@vue/reactivity-DiY1c2vO.js";import"./@vue/shared-mAAVTE9n.js";import"./@element-plus/icons-vue-BAghejlR.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./index-f0T-c8zR.js";import"./index-CO3hhC4Q.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";export{o as default}; import{_ as o}from"./account-adjust.vue_vue_type_script_setup_true_lang-BsnPY7DP.js";import"./element-plus-Bolc0EfP.js";import"./@vue/runtime-dom-DDAG46FW.js";import"./lodash-D3kF6u-c.js";import"./@vue/runtime-core-C6bnekPw.js";import"./@vue/reactivity-DiY1c2vO.js";import"./@vue/shared-mAAVTE9n.js";import"./@element-plus/icons-vue-B0jSCQ-G.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./index-JpA6TixL.js";import"./index-DiYdpaIG.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";export{o as default};
@@ -1 +1 @@
import{D as h,B as q,G as B,I,C as D}from"./element-plus-CXrrycLn.js";import{_ as F}from"./index-f0T-c8zR.js";import{i as b}from"./index-CO3hhC4Q.js";import{f as G,w,ak as j,G as S,aN as r,J as U,a,O as u,A}from"./@vue/runtime-core-C6bnekPw.js";import{y as n,q as y,r as J}from"./@vue/reactivity-DiY1c2vO.js";import{Q as k}from"./@vue/shared-mAAVTE9n.js";const M={class:"pr-8"},L=G({__name:"account-adjust",props:{show:{type:Boolean,required:!0},value:{type:[Number,String],required:!0}},emits:["update:show","confirm"],setup(d,{emit:V}){const s=y(),i=d,f=V,o=J({action:1,num:"",remark:""}),m=y(),c=A(()=>Number(i.value)+Number(o.num)*(o.action==1?1:-1)),R={num:[{required:!0,message:"请输入调整的金额"}]},g=e=>{if(e.includes("-"))return b.msgError("请输入正整数");o.num=e},x=async()=>{var e;await((e=s.value)==null?void 0:e.validate()),f("confirm",o)},C=()=>{var e;f("update:show",!1),(e=s.value)==null||e.resetFields()};return w(()=>i.show,e=>{var t,l;e?(t=m.value)==null||t.open():(l=m.value)==null||l.close()}),w(c,e=>{e<0&&(b.msgError("调整后余额需大于0"),o.num="")}),(e,t)=>{const l=q,_=I,E=B,v=D,N=h;return j(),S(F,{ref_key:"popupRef",ref:m,title:"余额调整",width:"500px",onConfirm:x,async:!0,onClose:C},{default:r(()=>[U("div",M,[a(N,{ref_key:"formRef",ref:s,model:n(o),"label-width":"120px",rules:R},{default:r(()=>[a(l,{label:"当前余额"},{default:r(()=>[u("¥ "+k(d.value),1)]),_:1}),a(l,{label:"余额增减",required:"",prop:"action"},{default:r(()=>[a(E,{modelValue:n(o).action,"onUpdate:modelValue":t[0]||(t[0]=p=>n(o).action=p)},{default:r(()=>[a(_,{value:1},{default:r(()=>[...t[2]||(t[2]=[u("增加余额",-1)])]),_:1}),a(_,{value:2},{default:r(()=>[...t[3]||(t[3]=[u("扣减余额",-1)])]),_:1})]),_:1},8,["modelValue"])]),_:1}),a(l,{label:"调整余额",prop:"num"},{default:r(()=>[a(v,{"model-value":n(o).num,placeholder:"请输入调整的金额",type:"number",onInput:g},null,8,["model-value"])]),_:1}),a(l,{label:"调整后余额"},{default:r(()=>[u(" ¥ "+k(n(c)),1)]),_:1}),a(l,{label:"备注",prop:"remark"},{default:r(()=>[a(v,{modelValue:n(o).remark,"onUpdate:modelValue":t[1]||(t[1]=p=>n(o).remark=p),type:"textarea",rows:4},null,8,["modelValue"])]),_:1})]),_:1},8,["model"])])]),_:1},512)}}});export{L as _}; import{D as h,B as q,G as B,I,C as D}from"./element-plus-Bolc0EfP.js";import{_ as F}from"./index-JpA6TixL.js";import{i as b}from"./index-DiYdpaIG.js";import{f as G,w,ak as j,G as S,aN as r,J as U,a,O as u,A}from"./@vue/runtime-core-C6bnekPw.js";import{y as n,q as y,r as J}from"./@vue/reactivity-DiY1c2vO.js";import{Q as k}from"./@vue/shared-mAAVTE9n.js";const M={class:"pr-8"},L=G({__name:"account-adjust",props:{show:{type:Boolean,required:!0},value:{type:[Number,String],required:!0}},emits:["update:show","confirm"],setup(d,{emit:V}){const s=y(),i=d,f=V,o=J({action:1,num:"",remark:""}),m=y(),c=A(()=>Number(i.value)+Number(o.num)*(o.action==1?1:-1)),R={num:[{required:!0,message:"请输入调整的金额"}]},g=e=>{if(e.includes("-"))return b.msgError("请输入正整数");o.num=e},x=async()=>{var e;await((e=s.value)==null?void 0:e.validate()),f("confirm",o)},C=()=>{var e;f("update:show",!1),(e=s.value)==null||e.resetFields()};return w(()=>i.show,e=>{var t,l;e?(t=m.value)==null||t.open():(l=m.value)==null||l.close()}),w(c,e=>{e<0&&(b.msgError("调整后余额需大于0"),o.num="")}),(e,t)=>{const l=q,_=I,E=B,v=D,N=h;return j(),S(F,{ref_key:"popupRef",ref:m,title:"余额调整",width:"500px",onConfirm:x,async:!0,onClose:C},{default:r(()=>[U("div",M,[a(N,{ref_key:"formRef",ref:s,model:n(o),"label-width":"120px",rules:R},{default:r(()=>[a(l,{label:"当前余额"},{default:r(()=>[u("¥ "+k(d.value),1)]),_:1}),a(l,{label:"余额增减",required:"",prop:"action"},{default:r(()=>[a(E,{modelValue:n(o).action,"onUpdate:modelValue":t[0]||(t[0]=p=>n(o).action=p)},{default:r(()=>[a(_,{value:1},{default:r(()=>[...t[2]||(t[2]=[u("增加余额",-1)])]),_:1}),a(_,{value:2},{default:r(()=>[...t[3]||(t[3]=[u("扣减余额",-1)])]),_:1})]),_:1},8,["modelValue"])]),_:1}),a(l,{label:"调整余额",prop:"num"},{default:r(()=>[a(v,{"model-value":n(o).num,placeholder:"请输入调整的金额",type:"number",onInput:g},null,8,["model-value"])]),_:1}),a(l,{label:"调整后余额"},{default:r(()=>[u(" ¥ "+k(n(c)),1)]),_:1}),a(l,{label:"备注",prop:"remark"},{default:r(()=>[a(v,{modelValue:n(o).remark,"onUpdate:modelValue":t[1]||(t[1]=p=>n(o).remark=p),type:"textarea",rows:4},null,8,["modelValue"])]),_:1})]),_:1},8,["model"])])]),_:1},512)}}});export{L as _};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
import{_ as o}from"./add-nav.vue_vue_type_script_setup_true_lang-CjODJcUC.js";import"./element-plus-CXrrycLn.js";import"./@vue/runtime-dom-DDAG46FW.js";import"./lodash-D3kF6u-c.js";import"./@vue/runtime-core-C6bnekPw.js";import"./@vue/reactivity-DiY1c2vO.js";import"./@vue/shared-mAAVTE9n.js";import"./@element-plus/icons-vue-BAghejlR.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./index-DjxDai3I.js";import"./index-CO3hhC4Q.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";import"./picker-B3tk5E5C.js";import"./index-f0T-c8zR.js";import"./index.vue_vue_type_script_setup_true_lang-iW1NfatD.js";import"./article-BFZKbqa6.js";import"./usePaging-VsbTxSU0.js";import"./picker-DJROU0jC.js";import"./index-x_mVhNP1.js";import"./index-CkJ0T1tI.js";import"./cos-js-sdk-v5-DSMN0LUT.js";import"./file-BdBRZBUu.js";import"./index.vue_vue_type_script_setup_true_lang-C6MkXIUg.js";import"./vuedraggable-5bKFmC7X.js";import"./vue-BVs68q8v.js";import"./@vue/compiler-dom-CqayqxS7.js";import"./@vue/compiler-core-CvbQOXIO.js";import"./sortablejs-dsEAUXlE.js";export{o as default}; import{_ as o}from"./add-nav.vue_vue_type_script_setup_true_lang-4JSvHHc2.js";import"./element-plus-Bolc0EfP.js";import"./@vue/runtime-dom-DDAG46FW.js";import"./lodash-D3kF6u-c.js";import"./@vue/runtime-core-C6bnekPw.js";import"./@vue/reactivity-DiY1c2vO.js";import"./@vue/shared-mAAVTE9n.js";import"./@element-plus/icons-vue-B0jSCQ-G.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./index-DPe01mMR.js";import"./index-DiYdpaIG.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";import"./picker-cmH3TFcX.js";import"./index-JpA6TixL.js";import"./index.vue_vue_type_script_setup_true_lang-EJPdzUeK.js";import"./article-DiEZ2pl3.js";import"./usePaging-VsbTxSU0.js";import"./picker-D9fcgO3K.js";import"./index-HnXSQ-Mh.js";import"./index-DITSRXGV.js";import"./cos-js-sdk-v5-DSMN0LUT.js";import"./file-BwO4oT1W.js";import"./index.vue_vue_type_script_setup_true_lang-Lcfr_gGB.js";import"./vuedraggable-5bKFmC7X.js";import"./vue-BVs68q8v.js";import"./@vue/compiler-dom-CqayqxS7.js";import"./@vue/compiler-core-CvbQOXIO.js";import"./sortablejs-dsEAUXlE.js";export{o as default};
@@ -1 +1 @@
import{C as E,B,g as C,i as N}from"./element-plus-CXrrycLn.js";import{_ as $}from"./index-DjxDai3I.js";import{_ as z}from"./picker-B3tk5E5C.js";import{_ as A}from"./picker-DJROU0jC.js";import{c as D,i as r}from"./index-CO3hhC4Q.js";import{D as I}from"./vuedraggable-5bKFmC7X.js";import{f as R,ak as p,I as F,J as l,a,aN as d,G,O as J,A as L}from"./@vue/runtime-core-C6bnekPw.js";import{y as c,a as O}from"./@vue/reactivity-DiY1c2vO.js";const P={class:"bg-fill-light flex items-center w-full p-4 mb-4"},S={class:"upload-btn w-[60px] h-[60px]"},T={class:"ml-3 flex-1"},j={class:"flex items-center"},q={class:"flex items-center mt-[18px]"},H={class:"flex-1 flex items-center"},K={class:"drag-move cursor-move ml-auto"},oe=R({__name:"add-nav",props:{modelValue:{type:Array,default:()=>[]},max:{type:Number,default:100},min:{type:Number,default:1}},emits:["update:modelValue"],setup(_,{emit:f}){const t=_,V=f,m=L({get(){return t.modelValue},set(s){V("update:modelValue",s)}}),x=()=>{var s;((s=t.modelValue)==null?void 0:s.length)<t.max?m.value.push({image:"",name:"导航名称",link:{},is_show:"1"}):r.msgError(`最多添加${t.max}`)},g=s=>{var e;if(((e=t.modelValue)==null?void 0:e.length)<=t.min)return r.msgError(`最少保留${t.min}`);m.value.splice(s,1)};return(s,e)=>{const i=D,v=A,h=E,k=z,b=C,w=B,y=$,U=N;return p(),F("div",null,[l("div",null,[a(c(I),{class:"draggable",modelValue:c(m),"onUpdate:modelValue":e[0]||(e[0]=o=>O(m)?m.value=o:null),animation:"300",handle:".drag-move","item-key":"index"},{item:d(({element:o,index:u})=>[(p(),G(y,{class:"w-[467px]",key:u,onClose:n=>g(u)},{default:d(()=>[l("div",P,[a(v,{modelValue:o.image,"onUpdate:modelValue":n=>o.image=n,"upload-class":"bg-body",size:"60px","exclude-domain":""},{upload:d(()=>[l("div",S,[a(i,{name:"el-icon-Plus",size:20})])]),_:1},8,["modelValue","onUpdate:modelValue"]),l("div",T,[l("div",j,[e[1]||(e[1]=l("span",{class:"text-tx-regular flex-none mr-3"},"名称",-1)),a(h,{modelValue:o.name,"onUpdate:modelValue":n=>o.name=n,placeholder:"请输入名称"},null,8,["modelValue","onUpdate:modelValue"])]),l("div",q,[e[2]||(e[2]=l("span",{class:"text-tx-regular flex-none mr-3"},"链接",-1)),a(k,{modelValue:o.link,"onUpdate:modelValue":n=>o.link=n},null,8,["modelValue","onUpdate:modelValue"])]),a(w,{label:"是否显示",class:"mt-[18px]"},{default:d(()=>[l("div",H,[a(b,{modelValue:o.is_show,"onUpdate:modelValue":n=>o.is_show=n,"active-value":"1","inactive-value":"0"},null,8,["modelValue","onUpdate:modelValue"]),l("div",K,[a(i,{name:"el-icon-Rank",size:"18"})])])]),_:2},1024)])])]),_:2},1032,["onClose"]))]),_:1},8,["modelValue"])]),l("div",null,[a(U,{type:"primary",onClick:x},{default:d(()=>[...e[3]||(e[3]=[J("添加",-1)])]),_:1})])])}}});export{oe as _}; import{C as E,B,g as C,i as N}from"./element-plus-Bolc0EfP.js";import{_ as $}from"./index-DPe01mMR.js";import{_ as z}from"./picker-cmH3TFcX.js";import{_ as A}from"./picker-D9fcgO3K.js";import{c as D,i as r}from"./index-DiYdpaIG.js";import{D as I}from"./vuedraggable-5bKFmC7X.js";import{f as R,ak as p,I as F,J as l,a,aN as d,G,O as J,A as L}from"./@vue/runtime-core-C6bnekPw.js";import{y as c,a as O}from"./@vue/reactivity-DiY1c2vO.js";const P={class:"bg-fill-light flex items-center w-full p-4 mb-4"},S={class:"upload-btn w-[60px] h-[60px]"},T={class:"ml-3 flex-1"},j={class:"flex items-center"},q={class:"flex items-center mt-[18px]"},H={class:"flex-1 flex items-center"},K={class:"drag-move cursor-move ml-auto"},oe=R({__name:"add-nav",props:{modelValue:{type:Array,default:()=>[]},max:{type:Number,default:100},min:{type:Number,default:1}},emits:["update:modelValue"],setup(_,{emit:f}){const t=_,V=f,m=L({get(){return t.modelValue},set(s){V("update:modelValue",s)}}),x=()=>{var s;((s=t.modelValue)==null?void 0:s.length)<t.max?m.value.push({image:"",name:"导航名称",link:{},is_show:"1"}):r.msgError(`最多添加${t.max}`)},g=s=>{var e;if(((e=t.modelValue)==null?void 0:e.length)<=t.min)return r.msgError(`最少保留${t.min}`);m.value.splice(s,1)};return(s,e)=>{const i=D,v=A,h=E,k=z,b=C,w=B,y=$,U=N;return p(),F("div",null,[l("div",null,[a(c(I),{class:"draggable",modelValue:c(m),"onUpdate:modelValue":e[0]||(e[0]=o=>O(m)?m.value=o:null),animation:"300",handle:".drag-move","item-key":"index"},{item:d(({element:o,index:u})=>[(p(),G(y,{class:"w-[467px]",key:u,onClose:n=>g(u)},{default:d(()=>[l("div",P,[a(v,{modelValue:o.image,"onUpdate:modelValue":n=>o.image=n,"upload-class":"bg-body",size:"60px","exclude-domain":""},{upload:d(()=>[l("div",S,[a(i,{name:"el-icon-Plus",size:20})])]),_:1},8,["modelValue","onUpdate:modelValue"]),l("div",T,[l("div",j,[e[1]||(e[1]=l("span",{class:"text-tx-regular flex-none mr-3"},"名称",-1)),a(h,{modelValue:o.name,"onUpdate:modelValue":n=>o.name=n,placeholder:"请输入名称"},null,8,["modelValue","onUpdate:modelValue"])]),l("div",q,[e[2]||(e[2]=l("span",{class:"text-tx-regular flex-none mr-3"},"链接",-1)),a(k,{modelValue:o.link,"onUpdate:modelValue":n=>o.link=n},null,8,["modelValue","onUpdate:modelValue"])]),a(w,{label:"是否显示",class:"mt-[18px]"},{default:d(()=>[l("div",H,[a(b,{modelValue:o.is_show,"onUpdate:modelValue":n=>o.is_show=n,"active-value":"1","inactive-value":"0"},null,8,["modelValue","onUpdate:modelValue"]),l("div",K,[a(i,{name:"el-icon-Rank",size:"18"})])])]),_:2},1024)])])]),_:2},1032,["onClose"]))]),_:1},8,["modelValue"])]),l("div",null,[a(U,{type:"primary",onClick:x},{default:d(()=>[...e[3]||(e[3]=[J("添加",-1)])]),_:1})])])}}});export{oe as _};
@@ -1 +1 @@
import{r as n}from"./index-CO3hhC4Q.js";function e(t){return n.get({url:"/auth.admin/lists",params:t},{ignoreCancelToken:!0})}function i(t){return n.post({url:"/auth.admin/add",params:t})}function r(t){return n.post({url:"/auth.admin/edit",params:t})}function u(t){return n.post({url:"/auth.admin/delete",params:t})}function d(t){return n.get({url:"/auth.admin/detail",params:t})}export{e as a,r as b,u as c,i as d,d as e}; import{r as n}from"./index-DiYdpaIG.js";function e(t){return n.get({url:"/auth.admin/lists",params:t},{ignoreCancelToken:!0})}function i(t){return n.post({url:"/auth.admin/add",params:t})}function r(t){return n.post({url:"/auth.admin/edit",params:t})}function u(t){return n.post({url:"/auth.admin/delete",params:t})}function d(t){return n.get({url:"/auth.admin/detail",params:t})}export{e as a,r as b,u as c,i as d,d as e};
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
import{r as e}from"./index-CO3hhC4Q.js";function a(t){return e.get({url:"/article.articleCate/lists",params:t})}function l(t){return e.get({url:"/article.articleCate/all",params:t})}function i(t){return e.post({url:"/article.articleCate/add",params:t})}function c(t){return e.post({url:"/article.articleCate/edit",params:t})}function u(t){return e.post({url:"/article.articleCate/delete",params:t})}function n(t){return e.get({url:"/article.articleCate/detail",params:t})}function s(t){return e.post({url:"/article.articleCate/updateStatus",params:t})}function o(t){return e.get({url:"/article.article/lists",params:t})}function d(t){return e.post({url:"/article.article/add",params:t})}function f(t){return e.post({url:"/article.article/edit",params:t})}function C(t){return e.post({url:"/article.article/delete",params:t})}function p(t){return e.get({url:"/article.article/detail",params:t})}function g(t){return e.post({url:"/article.article/updateStatus",params:t})}export{a,u as b,s as c,c as d,i as e,n as f,p as g,f as h,d as i,l as j,o as k,g as l,C as m}; import{r as e}from"./index-DiYdpaIG.js";function a(t){return e.get({url:"/article.articleCate/lists",params:t})}function l(t){return e.get({url:"/article.articleCate/all",params:t})}function i(t){return e.post({url:"/article.articleCate/add",params:t})}function c(t){return e.post({url:"/article.articleCate/edit",params:t})}function u(t){return e.post({url:"/article.articleCate/delete",params:t})}function n(t){return e.get({url:"/article.articleCate/detail",params:t})}function s(t){return e.post({url:"/article.articleCate/updateStatus",params:t})}function o(t){return e.get({url:"/article.article/lists",params:t})}function d(t){return e.post({url:"/article.article/add",params:t})}function f(t){return e.post({url:"/article.article/edit",params:t})}function C(t){return e.post({url:"/article.article/delete",params:t})}function p(t){return e.get({url:"/article.article/detail",params:t})}function g(t){return e.post({url:"/article.article/updateStatus",params:t})}export{a,u as b,s as c,c as d,i as e,n as f,p as g,f as h,d as i,l as j,o as k,g as l,C as m};

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