Compare commits
32
Commits
master-6-5
...
bg20260613
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8b129b3823 | ||
|
|
4ba75e5a94 | ||
|
|
8ae5b4eead | ||
|
|
e5959fd89a | ||
|
|
08249451a7 | ||
|
|
79ae055bb6 | ||
|
|
c0cc23393e | ||
|
|
470ba419e3 | ||
|
|
1c0cd84355 | ||
|
|
f2e2f84e18 | ||
|
|
f7e52bcafc | ||
|
|
2e59e883b8 | ||
|
|
9f1801771e | ||
|
|
1adf8ccf4b | ||
|
|
abc78f4291 | ||
|
|
072c0c3663 | ||
|
|
e9cf65a41c | ||
|
|
af9bd05bb4 | ||
|
|
18f7fb3772 | ||
|
|
29ab45033d | ||
|
|
8a1a9ea0ca | ||
|
|
f8ccd8b0af | ||
|
|
9e019708f2 | ||
|
|
2712c262bd | ||
|
|
bd7002e176 | ||
|
|
0be6d06544 | ||
|
|
7a4973470c | ||
|
|
e79f3190b8 | ||
|
|
1811abc794 | ||
|
|
07abf2abca | ||
|
|
c76bd5416a | ||
|
|
7bb8db4126 |
@@ -1,120 +0,0 @@
|
||||
alias ll='ls -alhG'
|
||||
|
||||
# Starship prompt
|
||||
eval "$(starship init zsh)"
|
||||
|
||||
# zsh-autosuggestions 自动补全
|
||||
source /opt/homebrew/share/zsh-autosuggestions/zsh-autosuggestions.zsh
|
||||
|
||||
# NVM 懒加载 - 只在第一次使用相关命令时再初始化
|
||||
export NVM_DIR="$HOME/.nvm"
|
||||
_nvm_default_bin() {
|
||||
local default_alias version_dir
|
||||
[ -r "$NVM_DIR/alias/default" ] || return 1
|
||||
|
||||
default_alias="$(<"$NVM_DIR/alias/default")"
|
||||
default_alias="${default_alias#"${default_alias%%[![:space:]]*}"}"
|
||||
default_alias="${default_alias%"${default_alias##*[![:space:]]}"}"
|
||||
|
||||
case "$default_alias" in
|
||||
v[0-9]*)
|
||||
version_dir="$NVM_DIR/versions/node/$default_alias"
|
||||
;;
|
||||
[0-9]*)
|
||||
version_dir="$(ls -d "$NVM_DIR"/versions/node/v"$default_alias".* 2>/dev/null | tail -n 1)"
|
||||
;;
|
||||
node|stable|lts/*)
|
||||
version_dir="$(ls -d "$NVM_DIR"/versions/node/v* 2>/dev/null | sort -V | tail -n 1)"
|
||||
;;
|
||||
*)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
[ -n "$version_dir" ] && [ -x "$version_dir/bin/node" ] || return 1
|
||||
printf '%s\n' "$version_dir/bin"
|
||||
}
|
||||
if _nvm_default_node_bin="$(_nvm_default_bin)"; then
|
||||
export PATH="$_nvm_default_node_bin:$PATH"
|
||||
fi
|
||||
unset _nvm_default_node_bin
|
||||
_lazy_load_nvm() {
|
||||
unset -f nvm node npm npx pnpm yarn 2>/dev/null
|
||||
[ -s "$NVM_DIR/nvm.sh" ] || return 1
|
||||
\. "$NVM_DIR/nvm.sh"
|
||||
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"
|
||||
hash -r 2>/dev/null
|
||||
}
|
||||
nvm() { _lazy_load_nvm && nvm "$@"; }
|
||||
node() { _lazy_load_nvm && node "$@"; }
|
||||
npm() { _lazy_load_nvm && npm "$@"; }
|
||||
npx() { _lazy_load_nvm && npx "$@"; }
|
||||
pnpm() { _lazy_load_nvm && pnpm "$@"; }
|
||||
yarn() { _lazy_load_nvm && yarn "$@"; }
|
||||
export PATH="/Applications/EServer/bin:$PATH"
|
||||
function EC_start(){
|
||||
/Applications/EasyConnect.app/Contents/Resources/bin/EasyMonitor > /dev/null 2>&1 &
|
||||
/Applications/EasyConnect.app/Contents/MacOS/EasyConnect > /dev/null 2>&1 &
|
||||
open /Applications/EasyConnect.app
|
||||
}
|
||||
|
||||
function EC_kill(){
|
||||
pkill EasyMonitor
|
||||
pkill ECAgent
|
||||
pkill ECAgentProxy
|
||||
pkill EasyConnect
|
||||
}
|
||||
# The following lines have been added by Docker Desktop to enable Docker CLI completions.
|
||||
fpath=(/Users/long/.docker/completions $fpath)
|
||||
autoload -Uz compinit
|
||||
compinit
|
||||
# End of Docker CLI completions
|
||||
|
||||
|
||||
export HOMEBREW_BOTTLE_DOMAIN=https://mirrors.ustc.edu.cn/homebrew-bottles/
|
||||
|
||||
# pyenv 懒加载 - 只在第一次使用 Python 相关命令时再初始化
|
||||
export PYENV_ROOT="$HOME/.pyenv"
|
||||
_lazy_load_pyenv() {
|
||||
unset -f pyenv python python3 pip pip3 2>/dev/null
|
||||
|
||||
local pyenv_bin
|
||||
pyenv_bin="$(command -v pyenv 2>/dev/null)"
|
||||
if [ -z "$pyenv_bin" ] && [ -x /opt/homebrew/bin/pyenv ]; then
|
||||
pyenv_bin=/opt/homebrew/bin/pyenv
|
||||
fi
|
||||
|
||||
[ -n "$pyenv_bin" ] || return 1
|
||||
|
||||
eval "$("$pyenv_bin" init --path)"
|
||||
eval "$("$pyenv_bin" init - zsh)"
|
||||
hash -r 2>/dev/null
|
||||
}
|
||||
pyenv() { _lazy_load_pyenv && pyenv "$@"; }
|
||||
python() { _lazy_load_pyenv && python "$@"; }
|
||||
python3() { _lazy_load_pyenv && python3 "$@"; }
|
||||
pip() { _lazy_load_pyenv && pip "$@"; }
|
||||
pip3() { _lazy_load_pyenv && pip3 "$@"; }
|
||||
|
||||
# 引入 bash 配置
|
||||
if [ -f ~/.bash_profile ]; then
|
||||
source ~/.bash_profile
|
||||
fi
|
||||
if [ -f ~/.bashrc ]; then
|
||||
source ~/.bashrc
|
||||
fi
|
||||
|
||||
# Added by Windsurf
|
||||
export PATH="/Users/long/.codeium/windsurf/bin:$PATH"
|
||||
|
||||
# Added by Antigravity
|
||||
export PATH="/Users/long/.antigravity/antigravity/bin:$PATH"
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
|
||||
# MindOS Desktop — CLI (mindos)
|
||||
export PATH="$HOME/.mindos/bin:$PATH"
|
||||
|
||||
source "$HOME/.cargo/env"
|
||||
|
||||
# Added by Antigravity
|
||||
export PATH="/Users/long/.antigravity/antigravity/bin:$PATH"
|
||||
@@ -1,74 +0,0 @@
|
||||
alias ll='ls -alhG'
|
||||
|
||||
# Starship prompt
|
||||
eval "$(starship init zsh)"
|
||||
|
||||
# zsh-autosuggestions 自动补全
|
||||
source /opt/homebrew/share/zsh-autosuggestions/zsh-autosuggestions.zsh
|
||||
|
||||
# NVM
|
||||
export NVM_DIR="$HOME/.nvm"
|
||||
if [ -s "$NVM_DIR/nvm.sh" ]; then
|
||||
\. "$NVM_DIR/nvm.sh"
|
||||
fi
|
||||
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"
|
||||
export PATH="/Applications/EServer/bin:$PATH"
|
||||
function EC_start(){
|
||||
/Applications/EasyConnect.app/Contents/Resources/bin/EasyMonitor > /dev/null 2>&1 &
|
||||
/Applications/EasyConnect.app/Contents/MacOS/EasyConnect > /dev/null 2>&1 &
|
||||
open /Applications/EasyConnect.app
|
||||
}
|
||||
|
||||
function EC_kill(){
|
||||
pkill EasyMonitor
|
||||
pkill ECAgent
|
||||
pkill ECAgentProxy
|
||||
pkill EasyConnect
|
||||
}
|
||||
# The following lines have been added by Docker Desktop to enable Docker CLI completions.
|
||||
fpath=(/Users/long/.docker/completions $fpath)
|
||||
autoload -Uz compinit
|
||||
compinit
|
||||
# End of Docker CLI completions
|
||||
|
||||
|
||||
export HOMEBREW_BOTTLE_DOMAIN=https://mirrors.ustc.edu.cn/homebrew-bottles/
|
||||
|
||||
# pyenv
|
||||
export PYENV_ROOT="$HOME/.pyenv"
|
||||
export PATH="/opt/homebrew/bin:$PYENV_ROOT/bin:$PATH"
|
||||
if command -v pyenv >/dev/null 2>&1; then
|
||||
eval "$(pyenv init --path)"
|
||||
eval "$(pyenv init - zsh)"
|
||||
fi
|
||||
|
||||
# 引入 bash 配置
|
||||
if [ -f ~/.bash_profile ]; then
|
||||
source ~/.bash_profile
|
||||
fi
|
||||
if [ -f ~/.bashrc ]; then
|
||||
source ~/.bashrc
|
||||
fi
|
||||
|
||||
# Added by Windsurf
|
||||
export PATH="/Users/long/.codeium/windsurf/bin:$PATH"
|
||||
|
||||
# Added by Antigravity
|
||||
export PATH="/Users/long/.antigravity/antigravity/bin:$PATH"
|
||||
|
||||
if command -v nvm >/dev/null 2>&1; then
|
||||
_nvm_default_version="$(nvm version default 2>/dev/null)"
|
||||
if [ -n "$_nvm_default_version" ] && [ -d "$NVM_DIR/versions/node/$_nvm_default_version/bin" ]; then
|
||||
export PATH="$NVM_DIR/versions/node/$_nvm_default_version/bin:$PATH"
|
||||
fi
|
||||
unset _nvm_default_version
|
||||
fi
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
|
||||
# MindOS Desktop — CLI (mindos)
|
||||
export PATH="$HOME/.mindos/bin:$PATH"
|
||||
|
||||
source "$HOME/.cargo/env"
|
||||
|
||||
# Added by Antigravity
|
||||
export PATH="/Users/long/.antigravity/antigravity/bin:$PATH"
|
||||
@@ -52,14 +52,14 @@
|
||||
"mp-weixin" : {
|
||||
"appid" : "wx79b9a0bfbfe7cbcd",
|
||||
"setting" : {
|
||||
"urlCheck" : false
|
||||
"urlCheck" : false,
|
||||
"minified" : true
|
||||
},
|
||||
"optimization" : {
|
||||
"subPackages" : true
|
||||
},
|
||||
"usingComponents" : true,
|
||||
"requiredBackgroundModes" : ["audio"],
|
||||
|
||||
"requiredBackgroundModes" : [ "audio" ],
|
||||
"plugins" : {
|
||||
"WechatSI" : {
|
||||
"version" : "0.3.5",
|
||||
|
||||
@@ -204,7 +204,15 @@
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTitleText": "日常护理",
|
||||
"backgroundColor": "#f4fbf4",
|
||||
"enablePullDownRefresh": true
|
||||
"enablePullDownRefresh": true,
|
||||
"mp-weixin": {
|
||||
"usingPlugins": {
|
||||
"WechatSI": {
|
||||
"version": "0.3.5",
|
||||
"provider": "wx069ba97219f66d99"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 45 KiB |
@@ -207,6 +207,8 @@ export function useDietAi(proxy, { diagnosisId, showUserToast, formatUserMessage
|
||||
return
|
||||
}
|
||||
if (!diagnosisId.value) return
|
||||
// 发送后立即清空输入框
|
||||
dietAiAskText.value = ''
|
||||
dietAiAsking.value = true
|
||||
dietAiAskResult.value = { advice: '', level: '', level_label: '', portion: '', food: '' }
|
||||
try {
|
||||
|
||||
@@ -2,22 +2,85 @@ import { ref, onUnmounted } from 'vue'
|
||||
|
||||
const MIN_HOLD_MS = 280
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
// WechatSI 录音管理器是全局单例:回调只在模块级绑定一次,
|
||||
// 再分发给「当前活跃实例」。否则跨页面(如 more → weekly)后,
|
||||
// 回调仍指向已销毁页面的闭包,新页面收不到 onStart/onStop,
|
||||
// 其本地状态卡在 recording,表现为一直录制/无法再次语音。
|
||||
let sharedManager = null
|
||||
let sharedManagerInited = false
|
||||
let sharedRecording = false
|
||||
let activeCtl = null
|
||||
|
||||
function getSharedRecordManager() {
|
||||
if (sharedManagerInited) return sharedManager
|
||||
sharedManagerInited = true
|
||||
try {
|
||||
// eslint-disable-next-line no-undef
|
||||
const plugin = requirePlugin('WechatSI')
|
||||
sharedManager = plugin.getRecordRecognitionManager()
|
||||
} catch (e) {
|
||||
console.warn('WechatSI record recognition not available', e)
|
||||
sharedManager = null
|
||||
return null
|
||||
}
|
||||
if (!sharedManager) return null
|
||||
|
||||
sharedManager.onStart = () => {
|
||||
sharedRecording = true
|
||||
if (activeCtl) activeCtl.handleStart()
|
||||
}
|
||||
sharedManager.onRecognize = (res) => {
|
||||
if (activeCtl) activeCtl.handleRecognize(res)
|
||||
}
|
||||
sharedManager.onStop = (res) => {
|
||||
sharedRecording = false
|
||||
if (activeCtl) activeCtl.handleStop(res)
|
||||
}
|
||||
sharedManager.onError = (res) => {
|
||||
sharedRecording = false
|
||||
if (activeCtl) activeCtl.handleError(res)
|
||||
}
|
||||
return sharedManager
|
||||
}
|
||||
// #endif
|
||||
|
||||
/**
|
||||
* 语音转文字(长按说话):微信小程序 WechatSI;H5 Web Speech API。
|
||||
*
|
||||
* @param {{ onResult?: (text: string) => void, showToast?: (msg: string) => void }} options
|
||||
* @param {{
|
||||
* onResult?: (text: string) => void,
|
||||
* onSettle?: (text: string, info: { heldMs: number }) => boolean | void,
|
||||
* onPartial?: (text: string) => void,
|
||||
* onError?: (msg: string) => boolean | void,
|
||||
* showToast?: (msg: string) => void
|
||||
* }} options
|
||||
*
|
||||
* onSettle 在每次录音结束时都会触发(含空结果),用于语音一问一答等需要
|
||||
* 完全接管识别结果的场景。若 onSettle 返回 true,则跳过默认的 onResult 回调
|
||||
* 与「没听清」提示,避免与上层流程重复处理。
|
||||
*
|
||||
* onPartial 在录音过程中实时返回中间识别结果(微信 onRecognize / H5 interim),
|
||||
* 用于「抢答打断」等场景:检测到用户已开口作答即可提前结束播报/录音。
|
||||
*
|
||||
* onError 在录音/识别出错时触发(如 "record manager recordfailed")。
|
||||
* 若返回 true,则跳过默认的错误 Toast,由上层自行处理(如自动重试)。
|
||||
*/
|
||||
export function useSpeechToText({ onResult, showToast } = {}) {
|
||||
export function useSpeechToText({ onResult, onSettle, onPartial, onError, showToast } = {}) {
|
||||
const sttListening = ref(false)
|
||||
const sttHolding = ref(false)
|
||||
const sttSupported = ref(false)
|
||||
|
||||
let wxRecordManager = null
|
||||
let h5Recognition = null
|
||||
let h5GotResult = false
|
||||
let holdStartTs = 0
|
||||
let holdSessionId = 0
|
||||
let startRequested = false
|
||||
let recordAuthorized = null
|
||||
// 快速点按可能残留会话:用排队标记把 start/stop 串行化,
|
||||
// 避免 "please stop after start"
|
||||
let pendingStartSession = 0
|
||||
|
||||
function notify(msg) {
|
||||
if (!msg) return
|
||||
@@ -31,6 +94,11 @@ export function useSpeechToText({ onResult, showToast } = {}) {
|
||||
function emitResult(text) {
|
||||
const t = String(text || '').trim()
|
||||
const heldMs = Date.now() - holdStartTs
|
||||
// onSettle 总会收到结果(含空),返回 true 表示已完全接管
|
||||
if (typeof onSettle === 'function') {
|
||||
const handled = onSettle(t, { heldMs })
|
||||
if (handled === true) return
|
||||
}
|
||||
if (!t) {
|
||||
if (heldMs < MIN_HOLD_MS) return
|
||||
notify('没听清,请再试一次')
|
||||
@@ -43,7 +111,8 @@ export function useSpeechToText({ onResult, showToast } = {}) {
|
||||
|
||||
function stopListening() {
|
||||
// #ifdef MP-WEIXIN
|
||||
if (wxRecordManager && (sttListening.value || startRequested)) {
|
||||
const ownsRecording = sharedRecording && activeCtl === controller
|
||||
if (wxRecordManager && (sttListening.value || startRequested || ownsRecording)) {
|
||||
try {
|
||||
wxRecordManager.stop()
|
||||
} catch (e) {}
|
||||
@@ -95,22 +164,42 @@ export function useSpeechToText({ onResult, showToast } = {}) {
|
||||
// #endif
|
||||
}
|
||||
|
||||
function startWechatRecord(session) {
|
||||
// #ifdef MP-WEIXIN
|
||||
// #ifdef MP-WEIXIN
|
||||
function actuallyStartWechat(session) {
|
||||
if (!wxRecordManager || session !== holdSessionId || !sttHolding.value) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
startRequested = true
|
||||
sharedRecording = true
|
||||
wxRecordManager.start({
|
||||
duration: 60000,
|
||||
lang: 'zh_CN'
|
||||
})
|
||||
} catch (e) {
|
||||
startRequested = false
|
||||
sharedRecording = false
|
||||
sttHolding.value = false
|
||||
notify('无法开始录音')
|
||||
}
|
||||
}
|
||||
// #endif
|
||||
|
||||
function startWechatRecord(session) {
|
||||
// #ifdef MP-WEIXIN
|
||||
if (!wxRecordManager || session !== holdSessionId || !sttHolding.value) {
|
||||
return
|
||||
}
|
||||
// 上一段会话还没结束(含全局单例残留):先停止并排队,
|
||||
// 待 onStop 回调后再真正开始,避免 "please stop after start"
|
||||
if (sharedRecording || startRequested) {
|
||||
pendingStartSession = session
|
||||
try {
|
||||
wxRecordManager.stop()
|
||||
} catch (e) {}
|
||||
return
|
||||
}
|
||||
actuallyStartWechat(session)
|
||||
// #endif
|
||||
}
|
||||
|
||||
@@ -123,6 +212,7 @@ export function useSpeechToText({ onResult, showToast } = {}) {
|
||||
}
|
||||
try {
|
||||
startRequested = true
|
||||
h5GotResult = false
|
||||
h5Recognition.start()
|
||||
sttListening.value = true
|
||||
} catch (e) {
|
||||
@@ -146,6 +236,8 @@ export function useSpeechToText({ onResult, showToast } = {}) {
|
||||
sttHolding.value = true
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
// 把全局录音回调切到当前实例
|
||||
activeCtl = controller
|
||||
ensureRecordAuth(() => startWechatRecord(session))
|
||||
// #endif
|
||||
// #ifdef H5
|
||||
@@ -161,11 +253,17 @@ export function useSpeechToText({ onResult, showToast } = {}) {
|
||||
|
||||
/** 松手结束:手指抬起 */
|
||||
function endHoldSpeech() {
|
||||
if (!sttHolding.value && !sttListening.value && !startRequested) {
|
||||
let recording = false
|
||||
// #ifdef MP-WEIXIN
|
||||
recording = sharedRecording && activeCtl === controller
|
||||
// #endif
|
||||
if (!sttHolding.value && !sttListening.value && !startRequested && !recording) {
|
||||
return
|
||||
}
|
||||
sttHolding.value = false
|
||||
if (sttListening.value || startRequested) {
|
||||
// 松手后不再补开排队的录音
|
||||
pendingStartSession = 0
|
||||
if (sttListening.value || startRequested || recording) {
|
||||
stopListening()
|
||||
return
|
||||
}
|
||||
@@ -173,13 +271,10 @@ export function useSpeechToText({ onResult, showToast } = {}) {
|
||||
}
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
try {
|
||||
// eslint-disable-next-line no-undef
|
||||
const plugin = requirePlugin('WechatSI')
|
||||
wxRecordManager = plugin.getRecordRecognitionManager()
|
||||
sttSupported.value = !!wxRecordManager
|
||||
|
||||
wxRecordManager.onStart = () => {
|
||||
// 当前实例的回调控制器:全局单例 manager 的事件由模块级分发器
|
||||
// 转发给 activeCtl(最后一次 beginHoldSpeech 的实例)
|
||||
const controller = {
|
||||
handleStart() {
|
||||
if (!sttHolding.value) {
|
||||
try {
|
||||
wxRecordManager.stop()
|
||||
@@ -188,21 +283,50 @@ export function useSpeechToText({ onResult, showToast } = {}) {
|
||||
}
|
||||
startRequested = false
|
||||
sttListening.value = true
|
||||
}
|
||||
wxRecordManager.onStop = (res) => {
|
||||
},
|
||||
handleRecognize(res) {
|
||||
if (typeof onPartial !== 'function') return
|
||||
const t = String(res?.result || '').trim()
|
||||
if (t) onPartial(t)
|
||||
},
|
||||
handleStop(res) {
|
||||
sttListening.value = false
|
||||
startRequested = false
|
||||
// 有排队的开始请求(残留会话已停止 / 用户仍按住):现在再真正开始
|
||||
if (pendingStartSession && pendingStartSession === holdSessionId && sttHolding.value) {
|
||||
const s = pendingStartSession
|
||||
pendingStartSession = 0
|
||||
actuallyStartWechat(s)
|
||||
return
|
||||
}
|
||||
pendingStartSession = 0
|
||||
emitResult(res?.result)
|
||||
}
|
||||
wxRecordManager.onError = (res) => {
|
||||
},
|
||||
handleError(res) {
|
||||
sttListening.value = false
|
||||
startRequested = false
|
||||
const msg = (res && res.msg) || ''
|
||||
// 上一段未停止就再次 start 触发:停止后若仍按住则自动重试,不打扰用户
|
||||
if (/please stop after start/i.test(msg)) {
|
||||
try {
|
||||
wxRecordManager.stop()
|
||||
} catch (e) {}
|
||||
pendingStartSession = sttHolding.value ? holdSessionId : 0
|
||||
return
|
||||
}
|
||||
pendingStartSession = 0
|
||||
sttHolding.value = false
|
||||
notify(res?.msg || '语音识别失败')
|
||||
// 上层(如一问一答流程)可接管错误并自行重试,返回 true 时不再弹默认提示
|
||||
if (typeof onError === 'function') {
|
||||
const handled = onError(msg || '')
|
||||
if (handled === true) return
|
||||
}
|
||||
notify(msg || '语音识别失败')
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('WechatSI record recognition not available', e)
|
||||
}
|
||||
|
||||
wxRecordManager = getSharedRecordManager()
|
||||
sttSupported.value = !!wxRecordManager
|
||||
// #endif
|
||||
|
||||
// #ifdef H5
|
||||
@@ -212,27 +336,49 @@ export function useSpeechToText({ onResult, showToast } = {}) {
|
||||
if (SR) {
|
||||
h5Recognition = new SR()
|
||||
h5Recognition.lang = 'zh-CN'
|
||||
h5Recognition.interimResults = false
|
||||
h5Recognition.interimResults = true
|
||||
h5Recognition.continuous = false
|
||||
h5Recognition.maxAlternatives = 1
|
||||
sttSupported.value = true
|
||||
|
||||
h5Recognition.onresult = (event) => {
|
||||
const item = event.results?.[0]?.[0]
|
||||
emitResult(item?.transcript || '')
|
||||
let interim = ''
|
||||
let finalText = ''
|
||||
for (let i = event.resultIndex; i < event.results.length; i++) {
|
||||
const r = event.results[i]
|
||||
const txt = r?.[0]?.transcript || ''
|
||||
if (r.isFinal) finalText += txt
|
||||
else interim += txt
|
||||
}
|
||||
if (interim && typeof onPartial === 'function') onPartial(interim)
|
||||
if (finalText) {
|
||||
h5GotResult = true
|
||||
emitResult(finalText)
|
||||
}
|
||||
}
|
||||
h5Recognition.onend = () => {
|
||||
sttListening.value = false
|
||||
startRequested = false
|
||||
sttHolding.value = false
|
||||
// 无识别结果也要兜底触发一次 settle,便于一问一答流程重试
|
||||
if (!h5GotResult) {
|
||||
emitResult('')
|
||||
}
|
||||
h5GotResult = false
|
||||
}
|
||||
h5Recognition.onerror = (event) => {
|
||||
sttListening.value = false
|
||||
startRequested = false
|
||||
sttHolding.value = false
|
||||
if (event?.error === 'not-allowed') {
|
||||
const err = event?.error || ''
|
||||
if (err === 'aborted') return
|
||||
if (typeof onError === 'function') {
|
||||
const handled = onError(err)
|
||||
if (handled === true) return
|
||||
}
|
||||
if (err === 'not-allowed') {
|
||||
notify('请允许浏览器使用麦克风')
|
||||
} else if (event?.error !== 'aborted') {
|
||||
} else {
|
||||
notify('语音识别失败')
|
||||
}
|
||||
}
|
||||
@@ -246,7 +392,14 @@ export function useSpeechToText({ onResult, showToast } = {}) {
|
||||
onUnmounted(() => {
|
||||
sttHolding.value = false
|
||||
holdSessionId += 1
|
||||
pendingStartSession = 0
|
||||
stopListening()
|
||||
// #ifdef MP-WEIXIN
|
||||
// 释放全局回调指向,避免事件继续派发给已销毁的实例
|
||||
if (activeCtl === controller) {
|
||||
activeCtl = null
|
||||
}
|
||||
// #endif
|
||||
})
|
||||
|
||||
return {
|
||||
|
||||
@@ -923,6 +923,9 @@ function initBoard() {
|
||||
commitBoard(next)
|
||||
// 注入本关唯一高糖(解锁前固定 2 个,无法三连)
|
||||
ensureHighPresence()
|
||||
// 注入高糖可能破坏“有解”性:开局静默兜底,避免出现整盘无法消除的死局
|
||||
// (仅打乱类型、保持高糖数量不变)
|
||||
if (!boardHasSolution()) shuffleTypesUntilPlayable()
|
||||
startHintTimer()
|
||||
}
|
||||
|
||||
@@ -1423,6 +1426,38 @@ function startHintTimer() {
|
||||
hintTimer = setTimeout(showOperationHint, HINT_DELAY_MS)
|
||||
}
|
||||
|
||||
/**
|
||||
* 就地打乱棋盘上所有方块的“食物类型”(位置/对象不变),直到棋盘“有解”:
|
||||
* 无现成消除组 + 至少存在一个可用移动。仅交换 type,故高糖数量等多重集不变。
|
||||
* 不带任何 UI 反馈,供初始化等静默场景使用。返回是否成功。
|
||||
*/
|
||||
function shuffleTypesUntilPlayable(maxRetry = 80) {
|
||||
const tiles = []
|
||||
for (let r = 0; r < ROWS; r++) {
|
||||
for (let c = 0; c < COLS; c++) {
|
||||
const t = board.value[r]?.[c]
|
||||
if (t && !t.isRemoved) tiles.push(t)
|
||||
}
|
||||
}
|
||||
if (tiles.length < 3) return false
|
||||
for (let attempt = 0; attempt < maxRetry; attempt++) {
|
||||
for (let i = tiles.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[tiles[i].type, tiles[j].type] = [tiles[j].type, tiles[i].type]
|
||||
}
|
||||
if (findMatches().length === 0 && findPossibleMoves().length > 0) {
|
||||
bumpLayout()
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/** 当前棋盘是否“有解”(无现成消除组且至少有一个可用移动) */
|
||||
function boardHasSolution() {
|
||||
return findMatches().length === 0 && findPossibleMoves().length > 0
|
||||
}
|
||||
|
||||
/**
|
||||
* 棋盘死锁自动被测与修复。
|
||||
* 使用 Fisher-Yates 打乱食物类型,最多重试 maxRetry 次。
|
||||
@@ -1431,7 +1466,7 @@ function startHintTimer() {
|
||||
async function reshuffleIfStuck() {
|
||||
if (gamePhase.value !== 'playing') return
|
||||
// 已有解(无现成消除组 + 存在可用移动)则无需重排
|
||||
if (findMatches().length === 0 && findPossibleMoves().length > 0) return
|
||||
if (boardHasSolution()) return
|
||||
|
||||
// 给玩家反馈
|
||||
playSfx('reshuffle', 0.75)
|
||||
@@ -1444,32 +1479,8 @@ async function reshuffleIfStuck() {
|
||||
setTimeout(() => { floatingVals.value = floatingVals.value.filter(v => v.id !== id) }, 1400)
|
||||
} catch (_) {}
|
||||
|
||||
// 收集棋盘上所有有效方块(保持 tile 对象与位置不变,仅打乱其食物类型)
|
||||
const tiles = []
|
||||
for (let r = 0; r < ROWS; r++) {
|
||||
for (let c = 0; c < COLS; c++) {
|
||||
const t = board.value[r]?.[c]
|
||||
if (t && !t.isRemoved) tiles.push(t)
|
||||
}
|
||||
}
|
||||
|
||||
// 反复打乱类型并就地试写,直到“有解且无现成三连”;
|
||||
// tiles 已是 board.value 中的同一批对象,故 findMatches/findPossibleMoves 可直接复用
|
||||
const MAX_RETRY = 60
|
||||
let ok = false
|
||||
for (let attempt = 0; attempt < MAX_RETRY; attempt++) {
|
||||
for (let i = tiles.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[tiles[i].type, tiles[j].type] = [tiles[j].type, tiles[i].type]
|
||||
}
|
||||
if (findMatches().length === 0 && findPossibleMoves().length > 0) {
|
||||
ok = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (!ok) {
|
||||
// 就地洗牌无法洗出可玩布局,回退到全棋盘重新生成
|
||||
// 就地打乱类型直到有解;失败则回退到全棋盘重新生成
|
||||
if (!shuffleTypesUntilPlayable(60)) {
|
||||
initBoard()
|
||||
return
|
||||
}
|
||||
@@ -1754,10 +1765,8 @@ async function processSwap(t1, t2, opts = {}) {
|
||||
isProcessing.value = false
|
||||
return
|
||||
}
|
||||
// 按解锁进度维持高糖数量(到第 7 步起补到可消除的 3 个)
|
||||
ensureHighPresence()
|
||||
// 所有连消结束后,检查棋盘是否死锁并自动重排
|
||||
await reshuffleIfStuck()
|
||||
// 落子算法已保证“下落即有可消除走法”;此处仅作极端兜底(静默、无横幅)
|
||||
if (!boardHasSolution()) shuffleTypesUntilPlayable()
|
||||
} else {
|
||||
await animateSwapOffset(t1, t2)
|
||||
swapTiles(t1, t2)
|
||||
@@ -1831,9 +1840,134 @@ async function clearAndRefill() {
|
||||
return false
|
||||
}
|
||||
|
||||
/* ——— 落子前在 next 网格上做的辅助判定(不触动可见的 board.value) ——— */
|
||||
function gridIsHigh(grid, r, c) {
|
||||
const t = grid[r]?.[c]
|
||||
return t && !t.isRemoved && t.type.gi === 'high'
|
||||
}
|
||||
|
||||
function gridWouldFormHighRun(grid, r, c) {
|
||||
let h = 1
|
||||
for (let cc = c - 1; cc >= 0 && gridIsHigh(grid, r, cc); cc--) h++
|
||||
for (let cc = c + 1; cc < COLS && gridIsHigh(grid, r, cc); cc++) h++
|
||||
if (h >= 3) return true
|
||||
let v = 1
|
||||
for (let rr = r - 1; rr >= 0 && gridIsHigh(grid, rr, c); rr--) v++
|
||||
for (let rr = r + 1; rr < ROWS && gridIsHigh(grid, rr, c); rr++) v++
|
||||
return v >= 3
|
||||
}
|
||||
|
||||
/** 在 grid 上是否存在“现成可消除组” */
|
||||
function gridHasCurrentMatch(grid) {
|
||||
const saved = board.value
|
||||
board.value = grid
|
||||
const has = findMatches().length > 0
|
||||
board.value = saved
|
||||
return has
|
||||
}
|
||||
|
||||
/** 在 grid 上是否存在“可交换出消除的走法” */
|
||||
function gridHasMove(grid) {
|
||||
const saved = board.value
|
||||
board.value = grid
|
||||
const ok = findPossibleMoves().length > 0
|
||||
board.value = saved
|
||||
return ok
|
||||
}
|
||||
|
||||
/**
|
||||
* 仅在 next 网格上调整本局高糖数量(只动“新生成的方块”,玩家尚未看到,故无可见突变):
|
||||
* 解锁前固定 2 个、解锁后 3~HIGH_COUNT_MAX 个;注入时避免直接连成三连。
|
||||
*/
|
||||
function balanceHighInRefill(next, newTiles) {
|
||||
const highFood = sessionHighFood.value
|
||||
if (!highFood) return
|
||||
const before = playerMoves.value < HIGH_UNLOCK_MOVES
|
||||
const minTarget = before ? HIGH_COUNT_LOCKED : HIGH_COUNT_UNLOCKED
|
||||
const maxCap = before ? HIGH_COUNT_LOCKED : HIGH_COUNT_MAX
|
||||
|
||||
let high = 0
|
||||
for (let r = 0; r < ROWS; r++) {
|
||||
for (let c = 0; c < COLS; c++) if (gridIsHigh(next, r, c)) high++
|
||||
}
|
||||
const lowMidPool = activeFoods.value.filter((f) => f.gi !== 'high')
|
||||
|
||||
if (high > maxCap) {
|
||||
for (const t of shuffleInPlace([...newTiles])) {
|
||||
if (high <= maxCap) break
|
||||
if (t.type.gi !== 'high') continue
|
||||
const repl = lowMidPool.length ? lowMidPool[Math.floor(Math.random() * lowMidPool.length)] : null
|
||||
if (repl) { t.type = cloneFoodType(repl); high-- }
|
||||
}
|
||||
}
|
||||
|
||||
if (high < minTarget) {
|
||||
const cands = shuffleInPlace([...newTiles])
|
||||
for (const t of cands) {
|
||||
if (high >= minTarget) break
|
||||
if (t.type.gi === 'high') continue
|
||||
if (gridWouldFormHighRun(next, t.r, t.c)) continue
|
||||
t.type = cloneFoodType(highFood)
|
||||
high++
|
||||
}
|
||||
for (const t of cands) {
|
||||
if (high >= minTarget) break
|
||||
if (t.type.gi === 'high') continue
|
||||
t.type = cloneFoodType(highFood)
|
||||
high++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保证“落下后必有可消除的走法”:只重 roll 新方块里的非高糖类型(落子前完成,无可见重排)。
|
||||
* 若 next 已有现成消除组(即将触发连锁),则交给连锁处理,最终落子时再保证。
|
||||
*/
|
||||
function ensureRefillPlayable(next, newTiles) {
|
||||
if (gridHasCurrentMatch(next)) return
|
||||
if (gridHasMove(next)) return
|
||||
|
||||
const nonHighPool = activeFoods.value.filter((f) => f.gi !== 'high')
|
||||
const rerollable = newTiles.filter((t) => t.type.gi !== 'high')
|
||||
if (nonHighPool.length && rerollable.length) {
|
||||
for (let attempt = 0; attempt < 100; attempt++) {
|
||||
rerollable.forEach((t) => {
|
||||
t.type = cloneFoodType(nonHighPool[Math.floor(Math.random() * nonHighPool.length)])
|
||||
})
|
||||
if (gridHasMove(next)) return
|
||||
}
|
||||
}
|
||||
|
||||
// 极端兜底:静默打乱整盘类型(保持多重集/高糖数量),无任何横幅提示
|
||||
silentShuffleGrid(next)
|
||||
}
|
||||
|
||||
/** 在 next 网格上静默打乱类型直至“无现成消除组且有可用走法”,仅作极端兜底 */
|
||||
function silentShuffleGrid(next) {
|
||||
const tiles = []
|
||||
for (let r = 0; r < ROWS; r++) {
|
||||
for (let c = 0; c < COLS; c++) {
|
||||
const t = next[r]?.[c]
|
||||
if (t && !t.isRemoved) tiles.push(t)
|
||||
}
|
||||
}
|
||||
if (tiles.length < 3) return
|
||||
const saved = board.value
|
||||
board.value = next
|
||||
for (let attempt = 0; attempt < 80; attempt++) {
|
||||
for (let i = tiles.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[tiles[i].type, tiles[j].type] = [tiles[j].type, tiles[i].type]
|
||||
}
|
||||
if (findMatches().length === 0 && findPossibleMoves().length > 0) break
|
||||
}
|
||||
board.value = saved
|
||||
}
|
||||
|
||||
async function applyGravity() {
|
||||
const next = Array.from({ length: ROWS }, () => Array(COLS).fill(null))
|
||||
const dropTiles = []
|
||||
const newTiles = []
|
||||
|
||||
for (let c = 0; c < COLS; c++) {
|
||||
const surviving = []
|
||||
@@ -1861,10 +1995,15 @@ async function applyGravity() {
|
||||
tile.isDropAnimate = false
|
||||
next[r][c] = tile
|
||||
dropTiles.push(tile)
|
||||
newTiles.push(tile)
|
||||
}
|
||||
}
|
||||
|
||||
ensureBoardFull(next)
|
||||
// 落子前只调整“新方块”:①按解锁进度维持高糖数量 ②保证落下后必有可消除走法
|
||||
// 这样棋盘随下落即更新,无需“无解时整盘自动重排”。
|
||||
balanceHighInRefill(next, newTiles)
|
||||
ensureRefillPlayable(next, newTiles)
|
||||
commitBoard(next)
|
||||
await nextTick()
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -193,19 +193,26 @@
|
||||
<view class="st-float-close" @click="floatAskClosed = true">
|
||||
<text class="st-float-close-icon">×</text>
|
||||
</view>
|
||||
<view class="st-float-inner">
|
||||
<view class="st-float-inner" :class="{ 'is-expanded': dietAiAskExpanded }">
|
||||
<view class="st-float-icon">
|
||||
<TongjiIcon name="sparkles" size="sm" color="#006c49" />
|
||||
</view>
|
||||
<input
|
||||
<textarea
|
||||
class="st-float-input"
|
||||
type="text"
|
||||
:value="dietAiAskText"
|
||||
placeholder="长按麦克风说话,或输入文字咨询 AI..."
|
||||
placeholder="输入或语音咨询 AI…"
|
||||
placeholder-class="st-float-placeholder"
|
||||
:maxlength="-1"
|
||||
auto-height
|
||||
:show-confirm-bar="false"
|
||||
:disable-default-padding="true"
|
||||
:cursor-spacing="24"
|
||||
confirm-type="send"
|
||||
:confirm-hold="false"
|
||||
@input="onDietAiAskInput"
|
||||
@confirm="askDietAiFood"
|
||||
@focus="onDietAiAskFocus"
|
||||
@blur="onDietAiAskBlur"
|
||||
/>
|
||||
<view
|
||||
class="st-float-mic"
|
||||
@@ -519,6 +526,19 @@ const {
|
||||
/** 底部 AI 浮动卡片是否被用户收起 */
|
||||
const floatAskClosed = ref(false)
|
||||
|
||||
/** AI 输入框是否聚焦:聚焦时输入框变大 */
|
||||
const dietAiAskFocused = ref(false)
|
||||
function onDietAiAskFocus() {
|
||||
dietAiAskFocused.value = true
|
||||
}
|
||||
function onDietAiAskBlur() {
|
||||
dietAiAskFocused.value = false
|
||||
}
|
||||
/** 聚焦或已有内容时展开为多行输入(内容多则自动增高) */
|
||||
const dietAiAskExpanded = computed(
|
||||
() => dietAiAskFocused.value || String(dietAiAskText.value || '').length > 0
|
||||
)
|
||||
|
||||
/** 键盘高度(px):录入弹窗据此整体上移,避免输入框被键盘遮挡 */
|
||||
const keyboardHeight = ref(0)
|
||||
function onKeyboardHeightChange(res) {
|
||||
|
||||
@@ -740,13 +740,22 @@
|
||||
.weekly-page .st-float-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
padding: 16rpx 16rpx 16rpx 40rpx;
|
||||
gap: 16rpx;
|
||||
padding: 16rpx 16rpx 16rpx 28rpx;
|
||||
border-radius: 999rpx;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
backdrop-filter: blur(20rpx);
|
||||
border: 1rpx solid rgba(0, 108, 73, 0.1);
|
||||
box-shadow: var(--st-shadow-float);
|
||||
transition: border-radius 0.22s ease, padding 0.22s ease, border-color 0.22s ease;
|
||||
}
|
||||
|
||||
/* 聚焦/有内容时展开:底部对齐让输入框向上增高,圆角收敛为圆角矩形 */
|
||||
.weekly-page .st-float-inner.is-expanded {
|
||||
align-items: flex-end;
|
||||
border-radius: 32rpx;
|
||||
padding: 18rpx 16rpx 18rpx 32rpx;
|
||||
border-color: rgba(0, 108, 73, 0.28);
|
||||
}
|
||||
|
||||
.weekly-page .st-float-icon {
|
||||
@@ -763,15 +772,30 @@
|
||||
.weekly-page .st-float-input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
height: 64rpx;
|
||||
width: 100%;
|
||||
min-height: 64rpx;
|
||||
max-height: 240rpx;
|
||||
font-size: 28rpx;
|
||||
line-height: 40rpx;
|
||||
color: var(--on-surface);
|
||||
background: transparent;
|
||||
padding: 12rpx 0;
|
||||
box-sizing: border-box;
|
||||
transition: min-height 0.2s ease;
|
||||
}
|
||||
|
||||
/* 聚焦时输入框变大;内容超长时由 auto-height 继续向上增高,超过上限可滚动 */
|
||||
.weekly-page .st-float-inner.is-expanded .st-float-input {
|
||||
min-height: 104rpx;
|
||||
}
|
||||
|
||||
.weekly-page .st-float-placeholder {
|
||||
color: rgba(60, 74, 66, 0.6);
|
||||
font-size: 28rpx;
|
||||
font-size: 26rpx;
|
||||
line-height: 40rpx;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.weekly-page .st-float-mic {
|
||||
|
||||
@@ -0,0 +1,307 @@
|
||||
/**
|
||||
* 健康数据语音文本解析。
|
||||
*
|
||||
* 把语音识别得到的中文口语文本解析为结构化字段:
|
||||
* - 血糖(空腹 / 餐后 / 其他)
|
||||
* - 血压(高压 / 低压 / 西药 / 胰岛素)
|
||||
* - 饮食(早 / 午 / 晚餐 + 备注)
|
||||
* - 运动(类型 / 时长 / 强度)
|
||||
*
|
||||
* 兼容中文数字(六点五 / 一百二十 / 一百二)与阿拉伯数字(6.5 / 120)。
|
||||
* 纯前端解析,无网络依赖,识别失败时返回空对象。
|
||||
*/
|
||||
|
||||
const CN_DIGIT = {
|
||||
零: 0, 〇: 0, 一: 1, 壹: 1, 幺: 1, 二: 2, 贰: 2, 两: 2,
|
||||
三: 3, 叁: 3, 四: 4, 肆: 4, 五: 5, 伍: 5, 六: 6, 陆: 6,
|
||||
七: 7, 柒: 7, 八: 8, 捌: 8, 九: 9, 玖: 9
|
||||
}
|
||||
const CN_UNIT = { 十: 10, 拾: 10, 百: 100, 佰: 100, 千: 1000, 仟: 1000, 万: 10000, 亿: 100000000 }
|
||||
|
||||
/** 中文整数串 → 数字,例如 "一百二十" → 120、"八十" → 80、"十" → 10、"一百二" → 120 */
|
||||
function cnIntToNumber(s) {
|
||||
let total = 0
|
||||
let section = 0
|
||||
let number = 0
|
||||
let hadUnit = false
|
||||
let lastUnit = 0
|
||||
let sawZeroAfterUnit = false
|
||||
for (const ch of s) {
|
||||
if (CN_DIGIT[ch] !== undefined) {
|
||||
number = CN_DIGIT[ch]
|
||||
if (number === 0) sawZeroAfterUnit = true
|
||||
} else if (CN_UNIT[ch] !== undefined) {
|
||||
hadUnit = true
|
||||
const unit = CN_UNIT[ch]
|
||||
lastUnit = unit
|
||||
sawZeroAfterUnit = false
|
||||
if (unit >= 10000) {
|
||||
section = (section + number) * unit
|
||||
total += section
|
||||
section = 0
|
||||
} else {
|
||||
if (number === 0) number = 1 // 十 = 10
|
||||
section += number * unit
|
||||
}
|
||||
number = 0
|
||||
}
|
||||
}
|
||||
// 没有任何单位且为纯数字串(如 "一二零")时按位拼接更符合口语
|
||||
if (!hadUnit && s.length > 1) {
|
||||
let joined = ''
|
||||
for (const ch of s) {
|
||||
if (CN_DIGIT[ch] !== undefined) joined += CN_DIGIT[ch]
|
||||
}
|
||||
if (joined) return Number(joined)
|
||||
}
|
||||
// 口语省略尾部单位:"一百二" → 120、"一千五" → 1500(避开 "一百零五" 这类带零的)
|
||||
if (number > 0 && lastUnit >= 100 && !sawZeroAfterUnit) {
|
||||
number = number * (lastUnit / 10)
|
||||
}
|
||||
return total + section + number
|
||||
}
|
||||
|
||||
/** 中文数字串(含小数点)→ 数字,例如 "六点五" → 6.5 */
|
||||
function cnSeqToNumber(seq) {
|
||||
if (seq.includes('点')) {
|
||||
const parts = seq.split('点')
|
||||
const intPart = parts[0]
|
||||
const decPart = parts.slice(1).join('')
|
||||
const intVal = intPart ? cnIntToNumber(intPart) : 0
|
||||
let decStr = ''
|
||||
for (const ch of decPart) {
|
||||
if (CN_DIGIT[ch] !== undefined) decStr += CN_DIGIT[ch]
|
||||
}
|
||||
if (!decStr) return intVal
|
||||
return Number(`${intVal}.${decStr}`)
|
||||
}
|
||||
return cnIntToNumber(seq)
|
||||
}
|
||||
|
||||
/** 是否包含中文数字字符(用于过滤“点”“重点”等误匹配) */
|
||||
function hasCnDigit(seq) {
|
||||
for (const ch of seq) {
|
||||
if (CN_DIGIT[ch] !== undefined || CN_UNIT[ch] !== undefined) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/** 把文本中的中文数字段落替换为阿拉伯数字 */
|
||||
export function normalizeNumbers(text) {
|
||||
const raw = String(text || '')
|
||||
return raw.replace(/[零〇一壹幺二贰两三叁四肆五伍六陆七柒八捌九玖十拾百佰千仟万亿点]+/g, (m) => {
|
||||
if (!hasCnDigit(m)) return m
|
||||
const n = cnSeqToNumber(m)
|
||||
return Number.isFinite(n) ? String(n) : m
|
||||
})
|
||||
}
|
||||
|
||||
/** 在文本中找到关键词后紧随的第一个数字 */
|
||||
function numAfter(text, keys) {
|
||||
for (const k of keys) {
|
||||
const i = text.indexOf(k)
|
||||
if (i >= 0) {
|
||||
const rest = text.slice(i + k.length)
|
||||
const m = rest.match(/-?\d+(?:\.\d+)?/)
|
||||
if (m) return m[0]
|
||||
}
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
/** 关键词后紧随的一段文本(截止到标点或下一个分隔关键词) */
|
||||
function textAfter(text, keys, stopKeys = []) {
|
||||
for (const k of keys) {
|
||||
const i = text.indexOf(k)
|
||||
if (i >= 0) {
|
||||
let rest = text.slice(i + k.length)
|
||||
// 去掉口语连接词
|
||||
rest = rest.replace(/^[是为吃了喝了吃的喝的有打了用了::,,、。\s]+/, '')
|
||||
let end = rest.length
|
||||
const mStop = rest.match(/[,,。.;;!!??\n]/)
|
||||
if (mStop && mStop.index < end) end = mStop.index
|
||||
for (const sk of stopKeys) {
|
||||
const si = rest.indexOf(sk)
|
||||
if (si >= 0 && si < end) end = si
|
||||
}
|
||||
const seg = rest.slice(0, end).trim()
|
||||
if (seg) return seg
|
||||
}
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析血糖:返回 { fasting_blood_sugar, postprandial_blood_sugar, other_blood_sugar }
|
||||
* 例:"空腹六点五餐后八点二" → { fasting:6.5, postprandial:8.2 }
|
||||
*/
|
||||
export function parseGlucose(raw) {
|
||||
const t = normalizeNumbers(raw)
|
||||
const result = {}
|
||||
const fasting = numAfter(t, ['空腹'])
|
||||
const post = numAfter(t, ['餐后', '饭后'])
|
||||
const other = numAfter(t, ['其他', '随机', '睡前', '凌晨', '夜间', '晚上'])
|
||||
if (fasting) result.fasting_blood_sugar = fasting
|
||||
if (post) result.postprandial_blood_sugar = post
|
||||
if (other) result.other_blood_sugar = other
|
||||
// 没有关键词但只有一个数字 → 视为“其他血糖”
|
||||
if (!fasting && !post && !other) {
|
||||
const nums = t.match(/\d+(?:\.\d+)?/g)
|
||||
if (nums && nums.length === 1) result.other_blood_sugar = nums[0]
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析血压:返回 { systolic_pressure, diastolic_pressure, western_medicine, insulin }
|
||||
* 例:"高压一百二低压八十" → { systolic:120, diastolic:80 }
|
||||
*/
|
||||
export function parseBloodPressure(raw) {
|
||||
const t = normalizeNumbers(raw)
|
||||
const result = {}
|
||||
let sys = numAfter(t, ['高压', '收缩压', '收缩'])
|
||||
let dia = numAfter(t, ['低压', '舒张压', '舒张'])
|
||||
if (!sys || !dia) {
|
||||
const nums = (t.match(/\d{2,3}/g) || [])
|
||||
.map(Number)
|
||||
.filter((n) => n >= 30 && n <= 300)
|
||||
if (!sys && !dia && nums.length >= 2) {
|
||||
sys = String(nums[0])
|
||||
dia = String(nums[1])
|
||||
} else if (!sys && nums.length === 1 && nums[0] >= 90) {
|
||||
sys = String(nums[0])
|
||||
}
|
||||
}
|
||||
// 高压应不低于低压,顺序异常时交换
|
||||
if (sys && dia && Number(sys) < Number(dia)) {
|
||||
const tmp = sys
|
||||
sys = dia
|
||||
dia = tmp
|
||||
}
|
||||
if (sys) result.systolic_pressure = sys
|
||||
if (dia) result.diastolic_pressure = dia
|
||||
|
||||
const insulin = textAfter(raw, ['胰岛素'], ['高压', '低压', '血压', '西药', '备注'])
|
||||
if (insulin) result.insulin = insulin
|
||||
const western = textAfter(raw, ['西药', '降糖药', '口服药'], ['胰岛素', '高压', '低压', '血压', '备注'])
|
||||
if (western) result.western_medicine = western
|
||||
return result
|
||||
}
|
||||
|
||||
const DIET_MARKERS = [
|
||||
{ keys: ['早餐', '早饭', '早上', '早点', '早晨'], field: 'breakfast_foods' },
|
||||
{ keys: ['午餐', '午饭', '中午'], field: 'lunch_foods' },
|
||||
{ keys: ['晚餐', '晚饭', '晚上', '夜宵', '夜里'], field: 'dinner_foods' }
|
||||
]
|
||||
|
||||
function cleanFoodSeg(seg) {
|
||||
return String(seg || '')
|
||||
.replace(/^[是为吃了吃的喝了喝的有::,,、。\s]+/, '')
|
||||
.replace(/[。.\s]+$/, '')
|
||||
.trim()
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析饮食:返回 { breakfast_foods, lunch_foods, dinner_foods, note }
|
||||
* 例:"早餐鸡蛋粥中午米饭炒青菜晚上面条" → 分别归位
|
||||
* 无餐别关键词时整句写入 note
|
||||
*/
|
||||
export function parseDiet(raw) {
|
||||
const t = String(raw || '').trim()
|
||||
const result = {}
|
||||
const hits = []
|
||||
DIET_MARKERS.forEach((m) => {
|
||||
for (const k of m.keys) {
|
||||
const idx = t.indexOf(k)
|
||||
if (idx >= 0) {
|
||||
hits.push({ idx, field: m.field, klen: k.length })
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
if (!hits.length) {
|
||||
if (t) result.note = t
|
||||
return result
|
||||
}
|
||||
hits.sort((a, b) => a.idx - b.idx)
|
||||
hits.forEach((h, i) => {
|
||||
const start = h.idx + h.klen
|
||||
const end = i + 1 < hits.length ? hits[i + 1].idx : t.length
|
||||
const seg = cleanFoodSeg(t.slice(start, end))
|
||||
if (seg && !result[h.field]) result[h.field] = seg
|
||||
})
|
||||
// 第一个餐别关键词之前的内容作为备注
|
||||
const head = cleanFoodSeg(t.slice(0, hits[0].idx))
|
||||
if (head) result.note = head
|
||||
return result
|
||||
}
|
||||
|
||||
const EXERCISE_FILLERS = [
|
||||
'今天', '我', '做了', '做', '进行了', '进行', '运动了', '运动', '锻炼了', '锻炼',
|
||||
'了', '大概', '左右', '持续', '差不多', '一共', '总共', '的', '走了', '打了', '跑了'
|
||||
]
|
||||
|
||||
/**
|
||||
* 解析运动:返回 { exercise_type, duration, intensity }
|
||||
* 例:"散步三十分钟中强度" → { type:'散步', duration:30, intensity:2 }
|
||||
*/
|
||||
export function parseExercise(raw) {
|
||||
const t = normalizeNumbers(raw)
|
||||
const result = {}
|
||||
|
||||
const durMatch = t.match(/(\d+(?:\.\d+)?)\s*(个小时|小时|钟头|时|分钟|分)/)
|
||||
if (durMatch) {
|
||||
const val = parseFloat(durMatch[1])
|
||||
const isHour = /个小时|小时|钟头|时/.test(durMatch[2])
|
||||
result.duration = String(Math.round(isHour ? val * 60 : val))
|
||||
}
|
||||
|
||||
if (/高强度|剧烈|很累|大汗|气喘/.test(t)) result.intensity = 3
|
||||
else if (/中强度|中等强度|中等|有点累|微微出汗|微汗/.test(t)) result.intensity = 2
|
||||
else if (/低强度|轻松|轻微|溜达|缓慢/.test(t)) result.intensity = 1
|
||||
|
||||
// 运动类型:移除时长、强度、填充词后剩余文本
|
||||
let type = t
|
||||
if (durMatch) type = type.replace(durMatch[0], '')
|
||||
type = type
|
||||
.replace(/高强度|中强度|中等强度|低强度|剧烈|很累|大汗|气喘|有点累|微微出汗|微汗|轻松|轻微|缓慢/g, '')
|
||||
.replace(/\d+(?:\.\d+)?/g, '')
|
||||
.replace(/[,,。.;;、\s]+/g, '')
|
||||
EXERCISE_FILLERS.forEach((f) => {
|
||||
type = type.split(f).join('')
|
||||
})
|
||||
type = type.trim()
|
||||
if (type && type.length <= 20) result.exercise_type = type
|
||||
return result
|
||||
}
|
||||
|
||||
const FIELD_LABELS = {
|
||||
fasting_blood_sugar: '空腹',
|
||||
postprandial_blood_sugar: '餐后',
|
||||
other_blood_sugar: '其他血糖',
|
||||
systolic_pressure: '高压',
|
||||
diastolic_pressure: '低压',
|
||||
western_medicine: '西药',
|
||||
insulin: '胰岛素',
|
||||
breakfast_foods: '早餐',
|
||||
lunch_foods: '午餐',
|
||||
dinner_foods: '晚餐',
|
||||
note: '备注',
|
||||
exercise_type: '运动',
|
||||
duration: '时长',
|
||||
intensity: '强度'
|
||||
}
|
||||
|
||||
const INTENSITY_LABELS = { 1: '低强度', 2: '中强度', 3: '高强度' }
|
||||
|
||||
/** 把解析结果转成可读的反馈文案,例如 "空腹6.5 · 餐后8.2" */
|
||||
export function summarizeParsed(parsed) {
|
||||
const parts = []
|
||||
Object.keys(parsed).forEach((k) => {
|
||||
const label = FIELD_LABELS[k] || k
|
||||
let val = parsed[k]
|
||||
if (k === 'intensity') val = INTENSITY_LABELS[val] || val
|
||||
parts.push(`${label}${val}`)
|
||||
})
|
||||
return parts.join(' · ')
|
||||
}
|
||||
+2
-2
@@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>后台管理系统</title>
|
||||
<title>中医问诊管理后台</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
@@ -29,7 +29,7 @@
|
||||
stroke-dasharray: 90, 150;
|
||||
stroke-dashoffset: 0;
|
||||
stroke-width: 2;
|
||||
stroke: #4073fa;
|
||||
stroke: #0EA5E9;
|
||||
stroke-linecap: round;
|
||||
}
|
||||
|
||||
|
||||
@@ -181,6 +181,37 @@ export function commissionSettlementConfirmRevoke(params: Record<string, any>) {
|
||||
return request.post({ url: '/stats.commissionSettlement/confirmRevoke', params })
|
||||
}
|
||||
|
||||
/** 复诊接诊率(按月):当月 N 诊接诊率,部门 → 医助分组 + 合计;剔除「继承」指派 */
|
||||
export function revisitRateOverview(params: { month?: string; dept_ids?: string }) {
|
||||
return request.get({ url: '/stats.revisitRate/overview', params })
|
||||
}
|
||||
|
||||
/** 复诊接诊率:部门下拉(前端组树) */
|
||||
export function revisitRateDeptOptions() {
|
||||
return request.get({ url: '/stats.revisitRate/deptOptions' })
|
||||
}
|
||||
|
||||
/** 复诊接诊率:「被指派数」点击下钻(按诊单聚合明细) */
|
||||
export function revisitRateAssignLines(params: {
|
||||
month?: string
|
||||
dept_ids?: string
|
||||
assistant_id?: number
|
||||
dept_id?: number
|
||||
}) {
|
||||
return request.get({ url: '/stats.revisitRate/assignLines', params })
|
||||
}
|
||||
|
||||
/** 复诊接诊率:「N 诊单数」点击下钻(具体订单明细) */
|
||||
export function revisitRateVisitOrderLines(params: {
|
||||
month?: string
|
||||
slot: number
|
||||
dept_ids?: string
|
||||
assistant_id?: number
|
||||
dept_id?: number
|
||||
}) {
|
||||
return request.get({ url: '/stats.revisitRate/visitOrderLines', params })
|
||||
}
|
||||
|
||||
/** 医助个人业绩概览 */
|
||||
export function assistantPerformanceOverview(params: {
|
||||
time_type?: string
|
||||
|
||||
@@ -468,6 +468,8 @@ export function prescriptionOrderAddPayOrder(params: {
|
||||
pay_amount: number
|
||||
pay_remark?: string
|
||||
completion_request?: number
|
||||
/** 创建方式:fubei 付呗(默认) / express_cod 快递代收 */
|
||||
pay_create_type?: 'fubei' | 'express_cod'
|
||||
}) {
|
||||
return request.post({ url: '/tcm.prescriptionOrder/addPayOrder', params })
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
:end-placeholder="endPlaceholder"
|
||||
:value-format="valueFormat"
|
||||
clearable
|
||||
@change="emit('change', $event)"
|
||||
></el-date-picker>
|
||||
</template>
|
||||
|
||||
@@ -29,7 +30,7 @@ const props = withDefaults(
|
||||
endPlaceholder: '结束时间'
|
||||
}
|
||||
)
|
||||
const emit = defineEmits(['update:startTime', 'update:endTime'])
|
||||
const emit = defineEmits(['update:startTime', 'update:endTime', 'change'])
|
||||
|
||||
const content = computed<any>({
|
||||
get: () => {
|
||||
|
||||
+15
-12
@@ -1,28 +1,31 @@
|
||||
const defaultSetting = {
|
||||
showCrumb: true, // 是否显示面包屑
|
||||
showLogo: false, // 是否显示logo
|
||||
showLogo: true, // 是否显示logo
|
||||
isUniqueOpened: true, //只展开一个一级菜单
|
||||
sideWidth: 183, //侧边栏宽度
|
||||
sideTheme: 'dark', //侧边栏主题
|
||||
sideDarkColor: '#1d2124', //侧边栏深色主题颜色
|
||||
sideWidth: 200, //侧边栏宽度
|
||||
sideTheme: 'light', //侧边栏主题
|
||||
sideDarkColor: '#0369A1', //侧边栏深色主题颜色(深天蓝)
|
||||
openMultipleTabs: true, // 是否开启多标签tab栏
|
||||
theme: '#4A5DFF', //主题色
|
||||
successTheme: '#67c23a', //成功主题色
|
||||
warningTheme: '#e6a23c', //警告主题色
|
||||
dangerTheme: '#f56c6c', //危险主题色
|
||||
errorTheme: '#f56c6c', //错误主题色
|
||||
infoTheme: '#909399' //信息主题色
|
||||
theme: '#0EA5E9', //主题色(天蓝)
|
||||
successTheme: '#16A34A', //成功主题色
|
||||
warningTheme: '#D97706', //警告主题色
|
||||
dangerTheme: '#DC2626', //危险主题色
|
||||
errorTheme: '#DC2626', //错误主题色
|
||||
infoTheme: '#64748B' //信息主题色
|
||||
}
|
||||
|
||||
/** 本地 setting 缓存结构版本。提升后仅对低于该版本的老缓存执行 SETTING_SCHEMA_MIGRATIONS */
|
||||
export const SETTING_SCHEMA_VERSION = 1
|
||||
export const SETTING_SCHEMA_VERSION = 4
|
||||
|
||||
/**
|
||||
* 按版本写入 defaultSetting 中的键(老用户 localStorage 会长期盖住 config 默认值)。
|
||||
* 以后若要再推一批新默认值:把 SETTING_SCHEMA_VERSION +1,并为本版本追加一条迁移键列表。
|
||||
*/
|
||||
export const SETTING_SCHEMA_MIGRATIONS: Record<number, (keyof typeof defaultSetting)[]> = {
|
||||
1: ['sideTheme', 'sideDarkColor']
|
||||
1: ['sideTheme', 'sideDarkColor'],
|
||||
2: ['theme', 'successTheme', 'warningTheme', 'dangerTheme', 'errorTheme', 'infoTheme', 'sideTheme', 'sideDarkColor', 'showLogo', 'sideWidth'],
|
||||
3: ['theme', 'sideTheme', 'sideDarkColor', 'sideWidth'],
|
||||
4: ['theme', 'sideDarkColor']
|
||||
}
|
||||
|
||||
export default defaultSetting
|
||||
|
||||
@@ -26,17 +26,19 @@ useWatchRoute((route) => {
|
||||
.app-breadcrumb {
|
||||
:deep(.el-breadcrumb__item) {
|
||||
.el-breadcrumb__inner {
|
||||
color: #303133;
|
||||
color: var(--el-text-color-regular);
|
||||
font-weight: 400;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
&:last-child .el-breadcrumb__inner {
|
||||
color: var(--health-primary-dark, var(--el-color-primary));
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&:last-child .el-breadcrumb__inner {
|
||||
color: #303133;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
:deep(.el-breadcrumb__separator) {
|
||||
color: #606266;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
<template>
|
||||
<header class="header">
|
||||
<div class="navbar">
|
||||
<div class="flex-1 flex">
|
||||
<div class="navbar-item">
|
||||
<div class="navbar-left flex-1 flex items-center min-w-0">
|
||||
<side-logo
|
||||
v-if="settingStore.showLogo"
|
||||
class="navbar-logo shrink-0"
|
||||
:show-title="!isCollapsed || isMobile"
|
||||
theme="light"
|
||||
/>
|
||||
<div class="navbar-item shrink-0">
|
||||
<el-tooltip
|
||||
class="box-item"
|
||||
effect="dark"
|
||||
@@ -12,16 +18,19 @@
|
||||
<fold />
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<div class="navbar-item">
|
||||
<div class="navbar-item shrink-0">
|
||||
<el-tooltip class="box-item" effect="dark" content="刷新" placement="bottom">
|
||||
<refresh />
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<div class="flex items-center px-2" v-if="!isMobile && settingStore.showCrumb">
|
||||
<div
|
||||
class="flex items-center px-2 min-w-0"
|
||||
v-if="!isMobile && settingStore.showCrumb"
|
||||
>
|
||||
<breadcrumb />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex">
|
||||
<div class="navbar-right flex shrink-0">
|
||||
<div class="navbar-item" v-if="!isMobile">
|
||||
<el-tooltip
|
||||
class="box-item"
|
||||
@@ -47,7 +56,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<multiple-tabs v-if="settingStore.openMultipleTabs" />
|
||||
</header>
|
||||
</template>
|
||||
|
||||
@@ -57,11 +65,11 @@ import { useFullscreen } from '@vueuse/core'
|
||||
import useAppStore from '@/stores/modules/app'
|
||||
import useSettingStore from '@/stores/modules/setting'
|
||||
|
||||
import SideLogo from '../sidebar/logo.vue'
|
||||
import Setting from '../setting/index.vue'
|
||||
import Breadcrumb from './breadcrumb.vue'
|
||||
import Fold from './fold.vue'
|
||||
import FullScreen from './full-screen.vue'
|
||||
import MultipleTabs from './multiple-tabs.vue'
|
||||
import Refresh from './refresh.vue'
|
||||
import UserDropDown from './user-drop-down.vue'
|
||||
|
||||
@@ -73,11 +81,31 @@ const { isFullscreen } = useFullscreen()
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.header {
|
||||
background: var(--el-bg-color);
|
||||
border-bottom: 1px solid var(--health-border, var(--el-border-color-lighter));
|
||||
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.03);
|
||||
}
|
||||
|
||||
.navbar {
|
||||
height: var(--navbar-height);
|
||||
@apply flex px-2 bg-body;
|
||||
@apply flex px-4;
|
||||
|
||||
.navbar-logo {
|
||||
margin-right: 4px;
|
||||
border-bottom: none !important;
|
||||
padding-right: 8px !important;
|
||||
}
|
||||
|
||||
.navbar-item {
|
||||
@apply h-full flex justify-center items-center hover:bg-page;
|
||||
@apply h-10 flex justify-center items-center cursor-pointer;
|
||||
min-width: 40px;
|
||||
border-radius: 8px;
|
||||
transition: background-color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--el-fill-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="app-tabs pl-4 flex bg-body">
|
||||
<div class="app-tabs flex bg-body">
|
||||
<div class="flex-1 min-w-0">
|
||||
<el-tabs
|
||||
:model-value="currentTab"
|
||||
@@ -13,7 +13,7 @@
|
||||
</el-tabs>
|
||||
</div>
|
||||
<el-dropdown @command="handleCommand">
|
||||
<span class="flex items-center px-3">
|
||||
<span class="flex items-center px-3 cursor-pointer tab-dropdown">
|
||||
<icon :size="16" name="el-icon-arrow-down" />
|
||||
</span>
|
||||
<template #dropdown>
|
||||
@@ -58,9 +58,22 @@ const handleCommand = (command: any) => {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.app-tabs {
|
||||
@apply border-t border-br;
|
||||
padding: 0 16px;
|
||||
background: var(--el-bg-color);
|
||||
border-bottom: 1px solid var(--health-border, var(--el-border-color-lighter));
|
||||
|
||||
.tab-dropdown {
|
||||
height: 40px;
|
||||
border-radius: 6px;
|
||||
transition: background-color 0.2s ease;
|
||||
&:hover {
|
||||
background-color: var(--el-fill-color);
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-tabs) {
|
||||
height: 40px;
|
||||
.el-tabs {
|
||||
@@ -80,35 +93,19 @@ const handleCommand = (command: any) => {
|
||||
}
|
||||
&__item {
|
||||
font-weight: normal;
|
||||
padding: 0 15px !important;
|
||||
padding: 0 16px !important;
|
||||
box-sizing: border-box;
|
||||
color: var(--el-text-color-secondary);
|
||||
transition: color 0.2s ease;
|
||||
&.is-active {
|
||||
color: var(--el-text-color-primary);
|
||||
background-color: var(--el-color-primary-light-9);
|
||||
&::before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background-color: var(--el-color-primary);
|
||||
margin-right: 6px;
|
||||
border-radius: 50%;
|
||||
vertical-align: 2px;
|
||||
}
|
||||
&::after {
|
||||
position: absolute;
|
||||
content: '';
|
||||
display: block;
|
||||
top: 0;
|
||||
height: 2px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
background-color: var(--el-color-primary);
|
||||
}
|
||||
color: var(--health-primary-dark, var(--el-color-primary));
|
||||
font-weight: 500;
|
||||
}
|
||||
.is-icon-close {
|
||||
color: var(--el-text-color-regular);
|
||||
color: var(--el-text-color-secondary);
|
||||
vertical-align: -2px;
|
||||
border-radius: 50%;
|
||||
transition: all 0.2s ease;
|
||||
&:hover {
|
||||
color: var(--color-white);
|
||||
background-color: var(--el-color-danger);
|
||||
@@ -116,7 +113,9 @@ const handleCommand = (command: any) => {
|
||||
}
|
||||
}
|
||||
&__active-bar {
|
||||
display: none;
|
||||
height: 2px;
|
||||
border-radius: 2px;
|
||||
background-color: var(--health-primary, var(--el-color-primary));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<main class="main-wrap h-full bg-page">
|
||||
<main class="main-wrap h-full">
|
||||
<el-scrollbar>
|
||||
<div class="px-2 py-4">
|
||||
<div class="main-content">
|
||||
<router-view v-if="isRouteShow" v-slot="{ Component, route }">
|
||||
<keep-alive :include="includeList" :max="20">
|
||||
<component :is="Component" :key="route.fullPath" />
|
||||
@@ -24,4 +24,14 @@ const isRouteShow = computed(() => appStore.isRouteShow)
|
||||
const includeList = computed(() => (settingStore.openMultipleTabs ? tabsStore.getCacheTabList : []))
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
<style lang="scss" scoped>
|
||||
.main-wrap {
|
||||
height: 100%;
|
||||
background: var(--el-bg-color);
|
||||
}
|
||||
|
||||
.main-content {
|
||||
padding: 16px 20px 24px;
|
||||
min-height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -100,7 +100,7 @@ import theme_light from '@/assets/images/theme_white.png'
|
||||
import useSettingStore from '@/stores/modules/setting'
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
const predefineColors = ref(['#409EFF', '#28C76F', '#EA5455', '#FF9F43', '#01CFE8', '#4A5DFF'])
|
||||
const predefineColors = ref(['#0EA5E9', '#38BDF8', '#0284C7', '#16A34A', '#DC2626', '#0369A1'])
|
||||
const sideThemeList = [
|
||||
{
|
||||
type: 'dark',
|
||||
|
||||
@@ -44,11 +44,13 @@ const handleClick = () => {
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
@apply flex items-center p-2 relative;
|
||||
@apply flex items-center px-3 relative;
|
||||
|
||||
.logo-title {
|
||||
width: 70%;
|
||||
position: absolute;
|
||||
@apply text-xl;
|
||||
@apply text-base font-medium;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.title-width-enter-active {
|
||||
|
||||
@@ -67,33 +67,53 @@ const themeClass = computed(() => `theme-${props.theme}`)
|
||||
&.theme-dark {
|
||||
.el-menu {
|
||||
:deep(.el-menu-item) {
|
||||
margin: 2px 10px;
|
||||
border-radius: 6px;
|
||||
&.is-active {
|
||||
@apply bg-primary border-primary;
|
||||
background-color: rgba(255, 255, 255, 0.12);
|
||||
color: #fff;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
:deep(.el-sub-menu__title) {
|
||||
margin: 2px 10px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
:deep(.el-menu--collapse) {
|
||||
.el-sub-menu.is-active .el-sub-menu__title {
|
||||
@apply bg-primary #{!important};
|
||||
background-color: rgba(255, 255, 255, 0.12) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
&.theme-light {
|
||||
:deep(.el-menu) {
|
||||
.el-menu-item {
|
||||
border-color: transparent;
|
||||
margin: 2px 10px;
|
||||
border-radius: 6px;
|
||||
color: var(--el-text-color-regular);
|
||||
&.is-active {
|
||||
@apply bg-primary-light-9 border-r-2 border-primary;
|
||||
background-color: var(--health-primary-light, var(--el-color-primary-light-9));
|
||||
color: var(--health-primary-dark, var(--el-color-primary));
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
.el-sub-menu__title {
|
||||
margin: 2px 10px;
|
||||
border-radius: 6px;
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
.el-menu-item:hover,
|
||||
.el-sub-menu__title:hover {
|
||||
color: var(--el-color-primary);
|
||||
background-color: var(--el-fill-color);
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
.el-menu {
|
||||
border-right: none;
|
||||
background: transparent;
|
||||
padding: 10px 0;
|
||||
&:not(.el-menu--collapse) {
|
||||
width: var(--aside-width);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
<template>
|
||||
<div class="side" :style="sideStyle">
|
||||
<side-logo v-if="settingStore.showLogo" :show-title="!isCollapsed" :theme="sideTheme" />
|
||||
<side-logo
|
||||
v-if="settingStore.showLogo && appStore.isMobile"
|
||||
:show-title="!isCollapsed"
|
||||
:theme="sideTheme"
|
||||
/>
|
||||
<side-menu
|
||||
:routes="routes"
|
||||
:is-collapsed="isCollapsed"
|
||||
@@ -45,9 +49,9 @@ const sideStyle = computed(() => {
|
||||
})
|
||||
const menuProp = computed(() => {
|
||||
return {
|
||||
backgroundColor: sideTheme.value == 'dark' ? settingStore.sideDarkColor : '',
|
||||
textColor: sideTheme.value == 'dark' ? 'var(--el-color-white)' : '',
|
||||
activeTextColor: sideTheme.value == 'dark' ? 'var(--el-color-white)' : ''
|
||||
backgroundColor: sideTheme.value == 'dark' ? settingStore.sideDarkColor : 'transparent',
|
||||
textColor: sideTheme.value == 'dark' ? 'rgba(255,255,255,0.85)' : '',
|
||||
activeTextColor: sideTheme.value == 'dark' ? '#ffffff' : ''
|
||||
}
|
||||
})
|
||||
const handleSelect = () => {
|
||||
@@ -60,8 +64,8 @@ const handleSelect = () => {
|
||||
<style lang="scss" scoped>
|
||||
.side {
|
||||
position: relative;
|
||||
z-index: 999;
|
||||
@apply border-r border-br-light h-full flex flex-col;
|
||||
background-color: var(--side-dark-color, var(--el-bg-color));
|
||||
z-index: 10;
|
||||
@apply h-full flex flex-col;
|
||||
background-color: var(--side-dark-color, var(--health-bg-sidebar, var(--el-bg-color)));
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,23 +1,42 @@
|
||||
<template>
|
||||
<div class="layout-default flex h-screen w-full">
|
||||
<!-- shrink-0:避免主内容区(宽表格/图表)把侧栏压缩到折叠宽度 -->
|
||||
<div class="app-aside shrink-0">
|
||||
<layout-sidebar />
|
||||
<div class="layout-default flex flex-col h-screen w-full bg-page">
|
||||
<div class="app-header shrink-0">
|
||||
<layout-header />
|
||||
</div>
|
||||
|
||||
<div class="flex-1 flex flex-col min-w-0">
|
||||
<div class="app-header">
|
||||
<layout-header />
|
||||
<div class="layout-body flex flex-1 min-h-0">
|
||||
<div class="app-aside shrink-0">
|
||||
<layout-sidebar />
|
||||
</div>
|
||||
<div class="app-main flex-1 min-h-0">
|
||||
<layout-main />
|
||||
|
||||
<div class="app-content flex flex-1 flex-col min-w-0 bg-body">
|
||||
<multiple-tabs v-if="settingStore.openMultipleTabs" />
|
||||
<div class="app-main flex-1 min-h-0">
|
||||
<layout-main />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import useSettingStore from '@/stores/modules/setting'
|
||||
|
||||
import LayoutHeader from './components/header/index.vue'
|
||||
import MultipleTabs from './components/header/multiple-tabs.vue'
|
||||
import LayoutMain from './components/main.vue'
|
||||
import LayoutSidebar from './components/sidebar/index.vue'
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.layout-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.app-content {
|
||||
border-left: 1px solid var(--health-border, var(--el-border-color-lighter));
|
||||
}
|
||||
</style>
|
||||
|
||||
+34
-31
@@ -1,43 +1,46 @@
|
||||
:root.dark {
|
||||
color-scheme: dark;
|
||||
--table-header-bg-color: var(--el-bg-color);
|
||||
--el-bg-color-page: #0a0a0a;
|
||||
--el-bg-color: #1d2124;
|
||||
--el-bg-color-overlay: #1d1e1f;
|
||||
--el-text-color-primary: #e5eaf3;
|
||||
--el-text-color-regular: #cfd3dc;
|
||||
--el-text-color-secondary: #a3a6ad;
|
||||
--el-text-color-placeholder: #8d9095;
|
||||
--el-text-color-disabled: #6c6e72;
|
||||
--el-border-color-darker: #636466;
|
||||
--el-border-color-dark: #58585b;
|
||||
--el-border-color: #4c4d4f;
|
||||
--el-border-color-light: #414243;
|
||||
--el-border-color-lighter: #363637;
|
||||
--el-border-color-extra-light: #2b2b2c;
|
||||
--el-fill-color-darker: #424243;
|
||||
--el-fill-color-dark: #39393a;
|
||||
--el-fill-color: #303030;
|
||||
--el-fill-color-light: #262727;
|
||||
--el-fill-color-lighter: #1d1d1d;
|
||||
--el-fill-color-extra-light: #191919;
|
||||
--table-header-bg-color: #1E293B;
|
||||
--health-bg-page: #0F1419;
|
||||
--health-bg-sidebar: #151B23;
|
||||
--health-text-primary: #F1F5F9;
|
||||
--health-text-secondary: #94A3B8;
|
||||
--health-border: #334155;
|
||||
--health-border-light: #1E293B;
|
||||
--health-primary-light: rgba(14, 165, 233, 0.15);
|
||||
--el-bg-color-page: var(--health-bg-page);
|
||||
--el-bg-color: #151B23;
|
||||
--el-bg-color-overlay: #1E293B;
|
||||
--el-text-color-primary: var(--health-text-primary);
|
||||
--el-text-color-regular: #CBD5E1;
|
||||
--el-text-color-secondary: #94A3B8;
|
||||
--el-text-color-placeholder: #64748B;
|
||||
--el-text-color-disabled: #475569;
|
||||
--el-border-color-darker: #475569;
|
||||
--el-border-color-dark: #334155;
|
||||
--el-border-color: #334155;
|
||||
--el-border-color-light: #334155;
|
||||
--el-border-color-lighter: #1E293B;
|
||||
--el-border-color-extra-light: #151B23;
|
||||
--el-fill-color-darker: #334155;
|
||||
--el-fill-color-dark: #1E293B;
|
||||
--el-fill-color: #1E293B;
|
||||
--el-fill-color-light: #151B23;
|
||||
--el-fill-color-lighter: #0F1419;
|
||||
--el-fill-color-extra-light: #1E293B;
|
||||
--el-fill-color-blank: var(--el-bg-color);
|
||||
--el-mask-color: rgba(0, 0, 0, 0.8);
|
||||
--el-mask-color-extra-light: rgba(0, 0, 0, 0.3);
|
||||
--el-box-shadow: 0px 12px 32px 4px rgba(0, 0, 0, 0.36), 0px 8px 20px rgba(0, 0, 0, 0.72);
|
||||
--el-box-shadow-light: 0px 0px 12px rgba(0, 0, 0, 0.72);
|
||||
--el-box-shadow-lighter: 0px 0px 6px rgba(0, 0, 0, 0.72);
|
||||
--el-box-shadow-dark: 0px 16px 48px 16px rgba(0, 0, 0, 0.72), 0px 12px 32px #000000,
|
||||
0px 8px 16px -8px #000000 !important;
|
||||
/* wangeditor主题 */
|
||||
--el-mask-color: rgba(0, 0, 0, 0.72);
|
||||
--el-mask-color-extra-light: rgba(0, 0, 0, 0.36);
|
||||
--el-box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||
--el-box-shadow-light: 0 1px 4px rgba(0, 0, 0, 0.25);
|
||||
--el-box-shadow-lighter: 0 1px 2px rgba(0, 0, 0, 0.2);
|
||||
--el-box-shadow-dark: 0 4px 16px rgba(0, 0, 0, 0.4);
|
||||
--w-e-textarea-bg-color: var(--el-bg-color);
|
||||
--w-e-textarea-color: var(--el-text-color-primary);
|
||||
--w-e-textarea-border-color: var(--el-border-color);
|
||||
--w-e-textarea-slight-border-color: var(--el-border-color-light);
|
||||
--w-e-textarea-slight-color: var(--el-border-color);
|
||||
--w-e-textarea-slight-bg-color: var(--el-bg-color-page);
|
||||
/* --w-e-textarea-selected-border-color: #b4d5ff;
|
||||
--w-e-textarea-handler-bg-color: #4290f7; */
|
||||
--w-e-toolbar-color: var(--el-text-color-primary);
|
||||
--w-e-toolbar-bg-color: var(--el-bg-color);
|
||||
--w-e-toolbar-active-color: var(--el-text-color-primary);
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
flex: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: 5px;
|
||||
border-radius: 8px;
|
||||
|
||||
&.body-padding .el-dialog__body {
|
||||
padding: 0;
|
||||
@@ -56,17 +56,27 @@
|
||||
}
|
||||
|
||||
.el-table {
|
||||
--el-table-header-text-color: var(--el-text-color-primary);
|
||||
--el-table-header-text-color: var(--el-text-color-regular);
|
||||
--el-table-header-bg-color: var(--table-header-bg-color);
|
||||
--el-table-border-color: var(--health-border, var(--el-border-color-lighter));
|
||||
--el-table-row-hover-bg-color: var(--el-fill-color-light);
|
||||
font-size: var(--el-font-size-base);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
|
||||
thead {
|
||||
th {
|
||||
font-weight: 400;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-card {
|
||||
border-radius: 8px;
|
||||
border-color: var(--health-border, var(--el-border-color-lighter));
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.el-input-group__prepend {
|
||||
background-color: var(--el-fill-color-blank);
|
||||
}
|
||||
@@ -78,22 +88,30 @@
|
||||
.el-menu--popup-container {
|
||||
&.theme-light {
|
||||
.el-menu {
|
||||
border-radius: 8px;
|
||||
padding: 4px;
|
||||
.el-menu-item {
|
||||
border-radius: 6px;
|
||||
&.is-active {
|
||||
@apply bg-primary-light-9 border-primary border-r-2;
|
||||
background-color: var(--health-primary-light, var(--el-color-primary-light-9));
|
||||
color: var(--health-primary-dark, var(--el-color-primary));
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
.el-menu-item:hover,
|
||||
.el-sub-menu__title:hover {
|
||||
color: var(--el-color-primary);
|
||||
background-color: var(--el-fill-color-light);
|
||||
}
|
||||
}
|
||||
}
|
||||
&.theme-dark {
|
||||
.el-menu {
|
||||
border-radius: 8px;
|
||||
padding: 4px;
|
||||
.el-menu-item {
|
||||
border-radius: 6px;
|
||||
&.is-active {
|
||||
@apply bg-primary;
|
||||
background-color: rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -110,6 +128,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
.el-button {
|
||||
border-radius: 6px;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.el-button--primary {
|
||||
--el-button-hover-link-text-color: var(--el-color-primary-light-3);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;600;700&display=swap');
|
||||
|
||||
body {
|
||||
@apply text-base text-tx-primary overflow-hidden min-w-[375px];
|
||||
font-feature-settings: 'tnum' 1;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.form-tips {
|
||||
@apply text-tx-secondary text-xs leading-6 mt-1;
|
||||
}
|
||||
@@ -15,4 +21,9 @@ body {
|
||||
/* NProgress */
|
||||
#nprogress .bar {
|
||||
@apply bg-primary #{!important};
|
||||
height: 3px !important;
|
||||
}
|
||||
|
||||
#nprogress .peg {
|
||||
box-shadow: 0 0 8px var(--el-color-primary), 0 0 4px var(--el-color-primary) !important;
|
||||
}
|
||||
|
||||
+40
-31
@@ -1,49 +1,58 @@
|
||||
:root {
|
||||
--el-font-family: theme(fontFamily.sans);
|
||||
--el-font-weight-primary: 400;
|
||||
--el-menu-item-height: 46px;
|
||||
--el-menu-item-height: 42px;
|
||||
--el-menu-sub-item-height: var(--el-menu-item-height);
|
||||
--el-menu-icon-width: 18px;
|
||||
--aside-width: 200px;
|
||||
--navbar-height: 50px;
|
||||
--navbar-height: 52px;
|
||||
--color-white: #ffffff;
|
||||
--table-header-bg-color: #f8f8f8;
|
||||
--table-header-bg-color: #F8FAFC;
|
||||
--el-font-size-extra-large: 18px;
|
||||
--el-menu-base-level-padding: 16px;
|
||||
--el-menu-level-padding: 26px;
|
||||
--el-menu-base-level-padding: 14px;
|
||||
--el-menu-level-padding: 24px;
|
||||
--el-font-size-large: 16px;
|
||||
--el-font-size-medium: 15px;
|
||||
--el-font-size-base: 14px;
|
||||
--el-font-size-small: 13px;
|
||||
--el-font-size-extra-small: 12px;
|
||||
|
||||
/* 天蓝点缀 + 中性底色(天蓝仅用于主色/激活态,不铺满全局) */
|
||||
--health-primary: #0EA5E9;
|
||||
--health-primary-light: #E0F2FE;
|
||||
--health-primary-dark: #0284C7;
|
||||
--health-bg-page: #F1F5F9;
|
||||
--health-bg-sidebar: #FFFFFF;
|
||||
--health-text-primary: #1E293B;
|
||||
--health-text-secondary: #64748B;
|
||||
--health-border: #E2E8F0;
|
||||
--health-border-light: #EEF2F6;
|
||||
|
||||
--el-bg-color: var(--color-white);
|
||||
--el-bg-color-page: #f6f6f6;
|
||||
--el-bg-color-page: var(--health-bg-page);
|
||||
--el-bg-color-overlay: #ffffff;
|
||||
--el-text-color-primary: #333333;
|
||||
--el-text-color-regular: #666666;
|
||||
--el-text-color-secondary: #999999;
|
||||
--el-text-color-placeholder: #a8abb2;
|
||||
--el-text-color-disabled: #c0c4cc;
|
||||
--el-border-color: #dcdfe6;
|
||||
--el-border-color-light: #e4e7ed;
|
||||
--el-border-color-lighter: #ebeef5;
|
||||
--el-border-color-extra-light: #f2f2f2;
|
||||
--el-border-color-dark: #d4d7de;
|
||||
--el-border-color-darker: #cdd0d6;
|
||||
--el-fill-color: #f0f2f5;
|
||||
--el-fill-color-light: #f8f8f8;
|
||||
--el-fill-color-lighter: #fafafa;
|
||||
--el-fill-color-extra-light: #fafcff;
|
||||
--el-fill-color-dark: #ebedf0;
|
||||
--el-fill-color-darker: #e6e8eb;
|
||||
--el-text-color-primary: var(--health-text-primary);
|
||||
--el-text-color-regular: var(--health-text-secondary);
|
||||
--el-text-color-secondary: #94A3B8;
|
||||
--el-text-color-placeholder: #94A3B8;
|
||||
--el-text-color-disabled: #CBD5E1;
|
||||
--el-border-color: var(--health-border);
|
||||
--el-border-color-light: var(--health-border);
|
||||
--el-border-color-lighter: var(--health-border-light);
|
||||
--el-border-color-extra-light: #F8FAFC;
|
||||
--el-border-color-dark: #CBD5E1;
|
||||
--el-border-color-darker: #94A3B8;
|
||||
--el-fill-color: #F1F5F9;
|
||||
--el-fill-color-light: #F8FAFC;
|
||||
--el-fill-color-lighter: #FAFBFC;
|
||||
--el-fill-color-extra-light: #F8FAFC;
|
||||
--el-fill-color-dark: #E2E8F0;
|
||||
--el-fill-color-darker: #CBD5E1;
|
||||
--el-fill-color-blank: #ffffff;
|
||||
/* 过亮会盖住抽屉/弹窗下的内容;Element Loading 与部分蒙层共用此变量 */
|
||||
--el-mask-color: rgba(255, 255, 255, 0.5);
|
||||
--el-mask-color-extra-light: rgba(255, 255, 255, 0.22);
|
||||
-el-box-shadow: 0px 12px 32px 4px rgba(0, 0, 0, 0.04), 0px 8px 20px rgba(0, 0, 0, 0.08);
|
||||
--el-box-shadow-light: 0px 0px 12px rgba(0, 0, 0, 0.12);
|
||||
--el-box-shadow-lighter: 0px 0px 6px rgba(0, 0, 0, 0.12);
|
||||
--el-box-shadow-dark: 0px 16px 48px 16px rgba(0, 0, 0, 0.08), 0px 12px 32px rgba(0, 0, 0, 0.12),
|
||||
0px 8px 16px -8px rgba(0, 0, 0, 0.16);
|
||||
--el-mask-color: rgba(255, 255, 255, 0.65);
|
||||
--el-mask-color-extra-light: rgba(255, 255, 255, 0.28);
|
||||
--el-box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
|
||||
--el-box-shadow-light: 0 1px 3px rgba(15, 23, 42, 0.06);
|
||||
--el-box-shadow-lighter: 0 1px 2px rgba(15, 23, 42, 0.04);
|
||||
--el-box-shadow-dark: 0 4px 12px rgba(15, 23, 42, 0.08);
|
||||
}
|
||||
|
||||
@@ -1,14 +1,27 @@
|
||||
<template>
|
||||
<div class="login flex flex-col">
|
||||
<div class="flex-1 flex items-center justify-center">
|
||||
<div class="login-card flex rounded-md overflow-hidden">
|
||||
<div class="flex-1 h-full hidden md:inline-block">
|
||||
<image-contain :src="config.login_image" :width="400" height="100%" />
|
||||
<div class="login-bg-decoration"></div>
|
||||
<div class="flex-1 flex items-center justify-center relative z-10">
|
||||
<div class="login-card flex rounded-xl overflow-hidden shadow-lg">
|
||||
<div class="login-visual hidden md:flex flex-col justify-center items-center relative">
|
||||
<div class="login-visual-content">
|
||||
<div class="login-visual-icon">
|
||||
<svg viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="20" y="8" width="8" height="32" rx="2" fill="currentColor" opacity="0.9"/>
|
||||
<rect x="8" y="20" width="32" height="8" rx="2" fill="currentColor" opacity="0.9"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 class="login-visual-title">中医在线问诊平台</h2>
|
||||
<p class="login-visual-desc">专业 · 安全 · 可信赖的医疗管理系统</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="login-form bg-body flex flex-col justify-center px-10 py-10 md:w-[420px] w-[380px] flex-none mx-auto"
|
||||
>
|
||||
<div class="text-center text-3xl font-medium mb-8">{{ config.web_name }}</div>
|
||||
<div class="text-center mb-8">
|
||||
<div class="login-form-badge">管理后台</div>
|
||||
<div class="text-2xl font-semibold text-tx-primary mt-3">{{ config.web_name }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 企业微信自动授权中 -->
|
||||
<div v-if="wxWorkAutoLogin" class="text-center py-10">
|
||||
@@ -287,11 +300,68 @@ onMounted(async () => {
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.login {
|
||||
background-image: url('./images/login_bg.png');
|
||||
@apply min-h-screen bg-no-repeat bg-center bg-cover;
|
||||
position: relative;
|
||||
@apply min-h-screen overflow-hidden;
|
||||
background: var(--health-bg-page, #F1F5F9);
|
||||
|
||||
.login-bg-decoration {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.login-card {
|
||||
height: auto;
|
||||
min-height: 400px;
|
||||
min-height: 440px;
|
||||
border: 1px solid var(--health-border, var(--el-border-color-lighter));
|
||||
box-shadow: var(--el-box-shadow-light);
|
||||
background: var(--el-bg-color);
|
||||
}
|
||||
|
||||
.login-visual {
|
||||
width: 400px;
|
||||
background: var(--el-bg-color);
|
||||
color: var(--el-text-color-primary);
|
||||
padding: 48px 40px;
|
||||
border-right: 1px solid var(--health-border, var(--el-border-color-lighter));
|
||||
|
||||
.login-visual-icon {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 14px;
|
||||
background: var(--el-color-primary-light-9);
|
||||
color: var(--el-color-primary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 24px;
|
||||
|
||||
svg {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
}
|
||||
|
||||
.login-visual-title {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 10px;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.login-visual-desc {
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-secondary);
|
||||
line-height: 1.7;
|
||||
}
|
||||
}
|
||||
|
||||
.login-form-badge {
|
||||
display: inline-block;
|
||||
padding: 4px 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--el-color-primary);
|
||||
background: var(--el-color-primary-light-9);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -92,7 +92,10 @@
|
||||
<el-input v-model="formData.title" placeholder="请输入资源标题" />
|
||||
</el-form-item>
|
||||
<el-form-item label="资源文件" prop="file_url" v-if="!isEdit">
|
||||
<material-picker v-if="formData.type === 1" v-model="formData.file_url" :limit="1" type="image" />
|
||||
<div v-if="formData.type === 1" class="w-full">
|
||||
<material-picker v-model="formData.file_url" :limit="9" type="image" />
|
||||
<div style="font-size: 12px; color: #909399; margin-top: 4px;">支持批量上传多张图片,一次最多 9 张,每张图片将生成一条独立资源(多张时标题自动追加序号)</div>
|
||||
</div>
|
||||
<upload v-else-if="formData.type === 2" type="video" direct :multiple="false" :limit="1" :show-progress="true" @success="handleUploadSuccess">
|
||||
<el-button type="primary">上传视频 (OSS直传)</el-button>
|
||||
</upload>
|
||||
@@ -156,6 +159,15 @@ const formData = reactive<any>({
|
||||
user_ids: []
|
||||
})
|
||||
|
||||
const validateFileUrl = (_rule: any, value: any, callback: any) => {
|
||||
if (Array.isArray(value)) {
|
||||
if (!value.length) return callback(new Error('请上传资源文件'))
|
||||
} else if (!value) {
|
||||
return callback(new Error('请输入或上传文件获取链接'))
|
||||
}
|
||||
callback()
|
||||
}
|
||||
|
||||
const computedRules = computed(() => {
|
||||
if (isEdit.value) {
|
||||
return {
|
||||
@@ -164,7 +176,7 @@ const computedRules = computed(() => {
|
||||
}
|
||||
return {
|
||||
title: [{ required: true, message: '请输入标题', trigger: 'blur' }],
|
||||
file_url: [{ required: true, message: '请输入或上传文件获取链接', trigger: 'blur' }]
|
||||
file_url: [{ required: true, validator: validateFileUrl, trigger: 'change' }]
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,342 @@
|
||||
/**
|
||||
* 处方业务订单——纯展示/格式化工具
|
||||
*
|
||||
* 唯一数据源:order_list.vue(处方业务订单)、PatientOrderList.vue(诊单业务订单 Tab)
|
||||
* 与 PrescriptionOrderDetailDrawer.vue(共享详情抽屉)共用,禁止在页面内重复定义同名函数,
|
||||
* 否则两个详情页面会再次出现文案/口径漂移。
|
||||
*/
|
||||
|
||||
/** 与诊间医助角色 ID 一致(server 角色表) */
|
||||
export const TCM_ASSISTANT_ROLE_ID = 2
|
||||
/** 与 server/config/project.php prescription_audit_roles 默认一致,可处方审核的角色 */
|
||||
export const PRESCRIPTION_AUDIT_ROLE_IDS = [0, 3, 6]
|
||||
|
||||
export function formatTime(v: unknown) {
|
||||
if (v === null || v === undefined || v === '') return '—'
|
||||
if (typeof v === 'number' && v > 1e9 && v < 1e11) {
|
||||
const d = new Date(v * 1000)
|
||||
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`
|
||||
}
|
||||
return String(v)
|
||||
}
|
||||
|
||||
export function formatMoney(v: unknown) {
|
||||
const n = Number(v)
|
||||
if (Number.isNaN(n)) return '—'
|
||||
return n.toFixed(2)
|
||||
}
|
||||
|
||||
export function formatOrderTime(v: unknown) {
|
||||
if (v === null || v === undefined || v === '') return '—'
|
||||
if (typeof v === 'string' && String(v).includes('-')) return String(v)
|
||||
return formatTime(v)
|
||||
}
|
||||
|
||||
export function feeTypeText(t: number | undefined) {
|
||||
const m: Record<number, string> = {
|
||||
1: '挂号费',
|
||||
2: '问诊费',
|
||||
3: '药品费用',
|
||||
4: '首付费用',
|
||||
5: '尾款费用',
|
||||
6: '其他费用',
|
||||
7: '全部费用',
|
||||
8: '驼奶费用'
|
||||
}
|
||||
return m[Number(t)] ?? '—'
|
||||
}
|
||||
|
||||
export function fulfillmentText(s: number | undefined) {
|
||||
const m: Record<number, string> = {
|
||||
1: '待双审通过',
|
||||
2: '待发货',
|
||||
3: '已完成',
|
||||
4: '已取消',
|
||||
5: '已发货',
|
||||
6: '已签收',
|
||||
7: '进行中',
|
||||
8: '暂不制药',
|
||||
9: '拒收',
|
||||
10: '退款',
|
||||
11: '保留药方',
|
||||
12: '制药缓发'
|
||||
}
|
||||
return m[Number(s)] ?? '—'
|
||||
}
|
||||
|
||||
export function fulfillmentTagType(s: number | undefined): 'success' | 'warning' | 'danger' | 'info' | 'primary' {
|
||||
const n = Number(s)
|
||||
if (n === 3) return 'success'
|
||||
if (n === 6) return 'success'
|
||||
if (n === 5) return 'primary'
|
||||
if (n === 4) return 'danger'
|
||||
if (n === 2) return 'warning'
|
||||
if (n === 7) return 'warning'
|
||||
if (n === 8 || n === 11 || n === 12) return 'info'
|
||||
if (n === 9 || n === 10) return 'danger'
|
||||
return 'info'
|
||||
}
|
||||
|
||||
export function orderStatusText(s: number | undefined) {
|
||||
const m: Record<number, string> = {
|
||||
1: '待支付',
|
||||
2: '已支付',
|
||||
3: '已取消',
|
||||
4: '已退款',
|
||||
5: '待审核'
|
||||
}
|
||||
return m[Number(s)] ?? '—'
|
||||
}
|
||||
|
||||
export function payOrderStatusTag(s: number | undefined): 'success' | 'warning' | 'danger' | 'info' {
|
||||
const n = Number(s)
|
||||
if (n === 2) return 'success'
|
||||
if (n === 1) return 'warning'
|
||||
return 'info'
|
||||
}
|
||||
|
||||
export function consumerRxAuditText(s: number | undefined) {
|
||||
const n = Number(s)
|
||||
if (n === 1) return '已通过'
|
||||
if (n === 2) return '已驳回'
|
||||
return '待审核'
|
||||
}
|
||||
|
||||
export function consumerRxAuditTag(s: number | undefined): 'success' | 'warning' | 'danger' | 'info' {
|
||||
const n = Number(s)
|
||||
if (n === 1) return 'success'
|
||||
if (n === 2) return 'danger'
|
||||
return 'warning'
|
||||
}
|
||||
|
||||
export function expressCompanyLabel(v: unknown) {
|
||||
const s = String(v || '').toLowerCase()
|
||||
if (s === 'sf') return '顺丰速运'
|
||||
if (s === 'jd') return '京东快递'
|
||||
if (s === 'jt' || s === 'jtexpress') return '极兔速递'
|
||||
return '自动识别'
|
||||
}
|
||||
|
||||
export function logActionText(act: string) {
|
||||
const m: Record<string, string> = {
|
||||
create: '创建',
|
||||
edit: '编辑',
|
||||
audit_rx_approve: '处方审核',
|
||||
audit_rx_reject: '处方审核',
|
||||
audit_pay_approve: '支付审核',
|
||||
audit_pay_reject: '支付审核',
|
||||
fill_tracking: '填快递单',
|
||||
ship: '确认发货',
|
||||
withdraw: '撤销',
|
||||
link_pay_order: '关联支付单',
|
||||
completion_request: '完单申请',
|
||||
auto_complete: '自动完成',
|
||||
revoke_rx_audit: '撤回处方审核',
|
||||
revoke_pay_audit: '撤回支付审核',
|
||||
gancao_submit: '甘草下单',
|
||||
patch_rx_patient: '处方患者信息',
|
||||
update_amount: '修改订单金额',
|
||||
complete: '完成订单',
|
||||
refund: '退款'
|
||||
}
|
||||
return m[act] || act
|
||||
}
|
||||
|
||||
/** 支付单来源/方式:企微对外收款、付呗等创建链路 + 支付方式回退 */
|
||||
export function formatPayOrderSource(row: { payment_method?: unknown; create_type?: unknown }) {
|
||||
const createType = String(row?.create_type || '')
|
||||
if (createType === 'wechat_work') return '企业微信对外收款'
|
||||
if (createType === 'fubei') return '付呗'
|
||||
const paymentMethod = String(row?.payment_method || '')
|
||||
const methodMap: Record<string, string> = {
|
||||
alipay: '支付宝',
|
||||
wechat: '微信',
|
||||
wechat_work: '企业微信',
|
||||
fubei: '付呗',
|
||||
manual: '手动确认到账'
|
||||
}
|
||||
if (paymentMethod && methodMap[paymentMethod]) {
|
||||
return methodMap[paymentMethod]
|
||||
}
|
||||
return '普通订单'
|
||||
}
|
||||
|
||||
export function normalizeBizPhone(v: unknown): string {
|
||||
if (v === null || v === undefined) return ''
|
||||
return String(v).replace(/\s/g, '').trim()
|
||||
}
|
||||
|
||||
export function recipientVsPrescriptionPhoneMismatch(recipient: unknown, rxPhone: unknown): boolean {
|
||||
const a = normalizeBizPhone(recipient)
|
||||
const b = normalizeBizPhone(rxPhone)
|
||||
if (!a || !b) return false
|
||||
return a !== b
|
||||
}
|
||||
|
||||
export function normalizeSlipHerbs(raw: unknown): Array<{ name: string; dosage: number }> {
|
||||
if (!raw) return []
|
||||
if (Array.isArray(raw)) {
|
||||
return raw.map((x: any) => ({
|
||||
name: String(x?.name ?? '').trim(),
|
||||
dosage: Number(x?.dosage) || 0
|
||||
}))
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
export type SlipFormulaType = '主方' | '辅方'
|
||||
|
||||
export interface SlipAuxUsageForm {
|
||||
dosage_amount?: number
|
||||
dosage_bag_count: number
|
||||
need_decoction: boolean
|
||||
bags_per_dose: number
|
||||
times_per_day: number
|
||||
usage_days: number
|
||||
}
|
||||
|
||||
export function normalizeSlipFormulaType(v: unknown): SlipFormulaType {
|
||||
return v === '辅方' ? '辅方' : '主方'
|
||||
}
|
||||
|
||||
export function defaultSlipAuxUsage(prescriptionType = '浓缩水丸'): SlipAuxUsageForm {
|
||||
if (prescriptionType === '饮片') {
|
||||
return {
|
||||
dosage_amount: 50,
|
||||
dosage_bag_count: 1,
|
||||
need_decoction: false,
|
||||
bags_per_dose: 1,
|
||||
times_per_day: 3,
|
||||
usage_days: 7
|
||||
}
|
||||
}
|
||||
if (prescriptionType === '浓缩水丸') {
|
||||
return {
|
||||
dosage_amount: 5,
|
||||
dosage_bag_count: 1,
|
||||
need_decoction: false,
|
||||
bags_per_dose: 1,
|
||||
times_per_day: 3,
|
||||
usage_days: 7
|
||||
}
|
||||
}
|
||||
return {
|
||||
dosage_amount: 1,
|
||||
dosage_bag_count: 1,
|
||||
need_decoction: false,
|
||||
bags_per_dose: 1,
|
||||
times_per_day: 3,
|
||||
usage_days: 7
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeSlipAuxUsageForm(raw: unknown, prescriptionType: string): SlipAuxUsageForm {
|
||||
const base = defaultSlipAuxUsage(prescriptionType)
|
||||
if (!raw || typeof raw !== 'object') return { ...base }
|
||||
const o = raw as Record<string, unknown>
|
||||
return {
|
||||
dosage_amount:
|
||||
o.dosage_amount !== null && o.dosage_amount !== undefined && o.dosage_amount !== ''
|
||||
? Number(o.dosage_amount)
|
||||
: base.dosage_amount,
|
||||
dosage_bag_count: o.dosage_bag_count != null ? Number(o.dosage_bag_count) || 1 : base.dosage_bag_count,
|
||||
need_decoction: o.need_decoction === 1 || o.need_decoction === true,
|
||||
bags_per_dose: o.bags_per_dose != null ? Number(o.bags_per_dose) || 1 : base.bags_per_dose,
|
||||
times_per_day: o.times_per_day != null ? Number(o.times_per_day) || 3 : base.times_per_day,
|
||||
usage_days: o.usage_days != null ? Number(o.usage_days) || 7 : base.usage_days
|
||||
}
|
||||
}
|
||||
|
||||
/** 是否含辅方药材(仅 formula_type=辅方;医助无药材权限时 herbs 被剥离,用后端 has_aux_formula) */
|
||||
export function prescriptionHasAuxFormula(rx: Record<string, unknown> | null | undefined): boolean {
|
||||
if (!rx) return false
|
||||
const herbs = rx.herbs
|
||||
if (Array.isArray(herbs)) {
|
||||
return herbs.some((h: any) => normalizeSlipFormulaType(h?.formula_type) === '辅方')
|
||||
}
|
||||
return Number(rx.has_aux_formula) === 1
|
||||
}
|
||||
|
||||
/** 物流轨迹中需人工立即跟进的常见异常关键词 */
|
||||
export const LOGISTICS_URGENT_KEYWORDS = [
|
||||
'拒收',
|
||||
'拒签',
|
||||
'退件',
|
||||
'客户拒收',
|
||||
'拦截退回',
|
||||
'退回发件',
|
||||
'派件退回',
|
||||
'无人签收',
|
||||
'退回快件'
|
||||
]
|
||||
|
||||
export function logisticsStringHasUrgentKeyword(s: unknown): boolean {
|
||||
const t = String(s ?? '')
|
||||
return LOGISTICS_URGENT_KEYWORDS.some((k) => t.includes(k))
|
||||
}
|
||||
|
||||
export function logisticsTraceLineUrgent(context: unknown): boolean {
|
||||
return logisticsStringHasUrgentKeyword(context)
|
||||
}
|
||||
|
||||
/** 根据单次拉取的物流 payload 判断是否含拒收/退回等(详情抽屉、完成订单弹窗共用) */
|
||||
export function analyzeLogisticsPayloadUrgent(p: Record<string, any> | null | undefined): {
|
||||
show: boolean
|
||||
keywords: string[]
|
||||
lines: string[]
|
||||
} {
|
||||
if (!p) {
|
||||
return { show: false, keywords: [], lines: [] }
|
||||
}
|
||||
const traces = Array.isArray(p.traces)
|
||||
? (p.traces as Array<{ time?: string; context?: string }>)
|
||||
: []
|
||||
const chunks: string[] = []
|
||||
if (p.state_text) chunks.push(String(p.state_text))
|
||||
if (p.hint) chunks.push(String(p.hint))
|
||||
for (const row of traces) {
|
||||
if (row.context) chunks.push(String(row.context))
|
||||
}
|
||||
const joined = chunks.join('\n')
|
||||
const keywords = LOGISTICS_URGENT_KEYWORDS.filter((k) => joined.includes(k))
|
||||
if (!keywords.length) {
|
||||
return { show: false, keywords: [], lines: [] }
|
||||
}
|
||||
const lines: string[] = []
|
||||
for (const row of traces) {
|
||||
const ctx = String(row.context || '')
|
||||
if (logisticsStringHasUrgentKeyword(ctx)) {
|
||||
lines.push(ctx)
|
||||
if (lines.length >= 5) break
|
||||
}
|
||||
}
|
||||
if (!lines.length && logisticsStringHasUrgentKeyword(p.state_text)) {
|
||||
lines.push(`最新状态:${p.state_text}`)
|
||||
}
|
||||
if (!lines.length && logisticsStringHasUrgentKeyword(p.hint)) {
|
||||
lines.push(String(p.hint))
|
||||
}
|
||||
return { show: true, keywords, lines }
|
||||
}
|
||||
|
||||
/** 校验物流轨迹响应与请求上下文匹配(防止快速切换订单时旧响应串单) */
|
||||
export function parseLogisticsTracePayload(
|
||||
res: unknown,
|
||||
expectedTrackingNumber: string,
|
||||
expectedOrderId: number
|
||||
): Record<string, any> | null {
|
||||
const raw = (res as { data?: unknown })?.data ?? res
|
||||
if (!raw || typeof raw !== 'object') return null
|
||||
const payload = raw as Record<string, any>
|
||||
const respNum = String(payload.tracking_number || '').trim()
|
||||
if (respNum && respNum !== expectedTrackingNumber) return null
|
||||
const respOrderId = Number(payload.order_id)
|
||||
if (respOrderId > 0 && respOrderId !== expectedOrderId) return null
|
||||
return payload
|
||||
}
|
||||
|
||||
export function canUpdateAmount(row: { id?: number; fulfillment_status?: number } | null | undefined) {
|
||||
if (!row?.id) return false
|
||||
const fs = Number(row.fulfillment_status)
|
||||
return fs !== 3 && fs !== 4
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -962,6 +962,11 @@
|
||||
<el-tag v-if="row.is_exempt === 1" type="warning" size="small" class="ml-1">豁免</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="方式" min-width="120" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
{{ formatPayOrderSource(row) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="creator_name" label="创建人" width="80" show-overflow-tooltip />
|
||||
<el-table-column label="创建时间" min-width="136">
|
||||
<template #default="{ row }">
|
||||
@@ -1020,6 +1025,11 @@
|
||||
<el-tag v-if="row.is_exempt === 1" type="warning" size="small" class="ml-1">豁免</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="方式" min-width="120" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
{{ formatPayOrderSource(row) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="creator_name" label="创建人" width="80" show-overflow-tooltip />
|
||||
<el-table-column label="创建时间" min-width="136">
|
||||
<template #default="{ row }">
|
||||
@@ -1555,7 +1565,7 @@
|
||||
<el-option
|
||||
v-for="o in editPaidOrders"
|
||||
:key="o.id"
|
||||
:label="`${o.order_no} · ¥${o.amount} · ${formatPoCategory(o.order_type)}`"
|
||||
:label="`${o.order_no} · ¥${o.amount} · ${formatPoCategory(o.order_type)} · ${formatPayOrderSource(o)}`"
|
||||
:value="o.id"
|
||||
class="!h-auto py-1.5"
|
||||
>
|
||||
@@ -1565,7 +1575,7 @@
|
||||
<span class="text-orange-500 font-medium ml-4">¥{{ o.amount }}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between text-xs text-gray-400 mt-1">
|
||||
<span class="truncate pr-2">{{ formatPoCategory(o.order_type) }}<template v-if="o.remark"> · {{ o.remark }}</template></span>
|
||||
<span class="truncate pr-2">{{ formatPoCategory(o.order_type) }} · {{ formatPayOrderSource(o) }}<template v-if="o.remark"> · {{ o.remark }}</template></span>
|
||||
<span class="shrink-0">{{ String(o.create_time || '').substring(0, 16) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1971,7 +1981,7 @@
|
||||
@closed="addPayOrderFormRef?.clearValidate()"
|
||||
>
|
||||
<el-alert
|
||||
title="可以手动创建新支付单,或关联已存在但未绑定的支付单。"
|
||||
:title="addPayOrderAlertTitle"
|
||||
type="info"
|
||||
:closable="false"
|
||||
show-icon
|
||||
@@ -1987,13 +1997,15 @@
|
||||
>
|
||||
<el-form-item label="添加方式" prop="add_mode">
|
||||
<el-radio-group v-model="addPayOrderForm.add_mode">
|
||||
<el-radio value="create">手动创建</el-radio>
|
||||
<el-radio value="create">付呗</el-radio>
|
||||
<el-radio value="create_express">快递代收</el-radio>
|
||||
<el-radio value="link">关联已有</el-radio>
|
||||
<el-radio value="completion_only">直接完单申请</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 手动创建模式 -->
|
||||
<template v-if="addPayOrderForm.add_mode === 'create'">
|
||||
<!-- 付呗 / 快递代收(手动创建支付单) -->
|
||||
<template v-if="addPayOrderForm.add_mode === 'create' || addPayOrderForm.add_mode === 'create_express'">
|
||||
<el-form-item label="费用类别" prop="order_type">
|
||||
<el-select v-model="addPayOrderForm.order_type" class="w-full">
|
||||
<el-option label="挂号费" :value="1" />
|
||||
@@ -2028,6 +2040,20 @@
|
||||
</el-form-item>
|
||||
</template>
|
||||
|
||||
<!-- 直接完单申请(不创建/关联支付单) -->
|
||||
<template v-else-if="addPayOrderForm.add_mode === 'completion_only'">
|
||||
<el-alert
|
||||
type="warning"
|
||||
:closable="false"
|
||||
show-icon
|
||||
class="!mb-0"
|
||||
>
|
||||
<template #title>
|
||||
不新增或关联支付单,仅向审核人员提交「完成订单」申请;提交后支付审核将变为待审核,审核通过后将自动结案。
|
||||
</template>
|
||||
</el-alert>
|
||||
</template>
|
||||
|
||||
<!-- 关联已有模式 -->
|
||||
<template v-else>
|
||||
<el-form-item label="选择支付单" prop="link_pay_order_id">
|
||||
@@ -2043,12 +2069,17 @@
|
||||
<el-option
|
||||
v-for="o in addPayOrderAvailableList"
|
||||
:key="o.id"
|
||||
:label="`${o.order_no} · ¥${o.amount} · ${formatPoCategory(o.order_type)}`"
|
||||
:label="`${o.order_no} · ¥${o.amount} · ${formatPoCategory(o.order_type)} · ${formatPayOrderSource(o)}`"
|
||||
:value="o.id"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm">{{ o.order_no }}</span>
|
||||
<span class="text-orange-500 font-medium ml-4">¥{{ o.amount }}</span>
|
||||
<div class="flex flex-col gap-0.5 py-0.5">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm">{{ o.order_no }}</span>
|
||||
<span class="text-orange-500 font-medium ml-4">¥{{ o.amount }}</span>
|
||||
</div>
|
||||
<div class="text-xs text-gray-400">
|
||||
{{ formatPoCategory(o.order_type) }} · {{ formatPayOrderSource(o) }}
|
||||
</div>
|
||||
</div>
|
||||
</el-option>
|
||||
</el-select>
|
||||
@@ -2059,7 +2090,7 @@
|
||||
</template>
|
||||
|
||||
<!-- 完单申请选项 -->
|
||||
<el-form-item label="完单申请">
|
||||
<el-form-item v-if="addPayOrderForm.add_mode !== 'completion_only'" label="完单申请">
|
||||
<el-radio-group v-model="addPayOrderForm.completion_request">
|
||||
<el-radio :value="0">不申请</el-radio>
|
||||
<el-radio :value="1">申请完成订单</el-radio>
|
||||
@@ -2531,6 +2562,7 @@ import {
|
||||
prescriptionOrderRevokePayAudit,
|
||||
prescriptionOrderPatchPrescriptionPatient,
|
||||
prescriptionOrderLinkPayOrder,
|
||||
prescriptionOrderRequestCompletion,
|
||||
prescriptionOrderSubmitGancaoRecipel,
|
||||
prescriptionOrderPreviewGancaoRecipel,
|
||||
prescriptionDetail,
|
||||
@@ -3495,10 +3527,14 @@ const detailLinkedPayOrders = computed(() => {
|
||||
return Array.isArray(arr) ? arr : []
|
||||
})
|
||||
|
||||
/** 需代收:业务订单总金额 − 关联已付总额 */
|
||||
/** 需代收:优先用关联时记录的固定快照值;历史单(无快照)回退实时计算 总金额 − 关联已付总额 */
|
||||
const detailAgencyToCollect = computed(() => {
|
||||
const d = detailData.value
|
||||
if (!d) return 0
|
||||
const snap = d.agency_collect_amount
|
||||
if (snap !== null && snap !== undefined && snap !== '') {
|
||||
return Number(snap) || 0
|
||||
}
|
||||
const total = Number(d.amount) || 0
|
||||
const paid = Number(d.linked_pay_paid_total) || 0
|
||||
return Math.round((total - paid) * 100) / 100
|
||||
@@ -4041,7 +4077,7 @@ const editDiagnosisCreatorDeptBreadcrumbs = computed(() => {
|
||||
.filter((segs) => segs.length > 0)
|
||||
})
|
||||
|
||||
const editPaidOrders = ref<Array<{ id: number; order_no?: string; amount?: number | string; order_type?: number | string; remark?: string; create_time?: string }>>([])
|
||||
const editPaidOrders = ref<Array<{ id: number; order_no?: string; amount?: number | string; order_type?: number | string; remark?: string; create_time?: string; payment_method?: string; create_type?: string }>>([])
|
||||
const editPaidOrdersLoading = ref(false)
|
||||
const editDepositMin = ref(0)
|
||||
|
||||
@@ -4057,6 +4093,24 @@ function formatPoCategory(t: unknown) {
|
||||
return m[Number(t)] || '未知'
|
||||
}
|
||||
|
||||
function formatPayOrderSource(row: { payment_method?: unknown; create_type?: unknown }) {
|
||||
const createType = String(row?.create_type || '')
|
||||
if (createType === 'wechat_work') return '企业微信对外收款'
|
||||
if (createType === 'fubei') return '付呗'
|
||||
const paymentMethod = String(row?.payment_method || '')
|
||||
const methodMap: Record<string, string> = {
|
||||
alipay: '支付宝',
|
||||
wechat: '微信',
|
||||
wechat_work: '企业微信',
|
||||
fubei: '付呗',
|
||||
manual: '手动确认到账'
|
||||
}
|
||||
if (paymentMethod && methodMap[paymentMethod]) {
|
||||
return methodMap[paymentMethod]
|
||||
}
|
||||
return '普通订单'
|
||||
}
|
||||
|
||||
const editLinkedPaidTotal = computed(() => {
|
||||
const ids = new Set(editForm.pay_order_ids.map((x) => Number(x)))
|
||||
let s = 0
|
||||
@@ -4898,8 +4952,17 @@ const addPayOrderRowId = ref(0)
|
||||
const addPayOrderFormRef = ref<FormInstance>()
|
||||
const addPayOrderAvailableLoading = ref(false)
|
||||
const addPayOrderAvailableList = ref<any[]>([])
|
||||
const addPayOrderAlertTitle = computed(() => {
|
||||
if (addPayOrderForm.add_mode === 'completion_only') {
|
||||
return '不创建或关联支付单,仅提交完单申请,由审核人员在支付审核时处理。'
|
||||
}
|
||||
if (addPayOrderForm.add_mode === 'create_express') {
|
||||
return '通过快递代收创建新支付单(流转同付呗),或关联已存在但未绑定的支付单。'
|
||||
}
|
||||
return '可以通过付呗创建新支付单,或关联已存在但未绑定的支付单。'
|
||||
})
|
||||
const addPayOrderForm = reactive({
|
||||
add_mode: 'create' as 'create' | 'link',
|
||||
add_mode: 'create' as 'create' | 'create_express' | 'link' | 'completion_only',
|
||||
order_type: 3,
|
||||
pay_amount: undefined as number | undefined,
|
||||
pay_remark: '',
|
||||
@@ -4907,38 +4970,44 @@ const addPayOrderForm = reactive({
|
||||
completion_request: 0
|
||||
})
|
||||
|
||||
const addPayOrderRules: FormRules = {
|
||||
add_mode: [{ required: true, message: '请选择添加方式', trigger: 'change' }],
|
||||
order_type: [{ required: true, message: '请选择费用类别', trigger: 'change' }],
|
||||
pay_amount: [
|
||||
{ required: true, message: '请输入支付金额', trigger: 'blur' },
|
||||
{
|
||||
validator: (_rule, v, cb) => {
|
||||
const n = Number(v)
|
||||
if (!Number.isFinite(n) || n <= 0) {
|
||||
cb(new Error('金额须大于 0'))
|
||||
} else {
|
||||
cb()
|
||||
}
|
||||
},
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
link_pay_order_ids: [
|
||||
{
|
||||
required: true,
|
||||
message: '请至少选择一个支付单',
|
||||
trigger: 'change',
|
||||
validator: (_rule, v, cb) => {
|
||||
if (!Array.isArray(v) || v.length === 0) {
|
||||
cb(new Error('请至少选择一个支付单'))
|
||||
} else {
|
||||
cb()
|
||||
const addPayOrderRules = computed<FormRules>(() => {
|
||||
const rules: FormRules = {
|
||||
add_mode: [{ required: true, message: '请选择添加方式', trigger: 'change' }]
|
||||
}
|
||||
if (addPayOrderForm.add_mode === 'create' || addPayOrderForm.add_mode === 'create_express') {
|
||||
rules.order_type = [{ required: true, message: '请选择费用类别', trigger: 'change' }]
|
||||
rules.pay_amount = [
|
||||
{ required: true, message: '请输入支付金额', trigger: 'blur' },
|
||||
{
|
||||
validator: (_rule, v, cb) => {
|
||||
const n = Number(v)
|
||||
if (!Number.isFinite(n) || n <= 0) {
|
||||
cb(new Error('金额须大于 0'))
|
||||
} else {
|
||||
cb()
|
||||
}
|
||||
},
|
||||
trigger: 'blur'
|
||||
}
|
||||
]
|
||||
} else if (addPayOrderForm.add_mode === 'link') {
|
||||
rules.link_pay_order_ids = [
|
||||
{
|
||||
required: true,
|
||||
message: '请至少选择一个支付单',
|
||||
trigger: 'change',
|
||||
validator: (_rule, v, cb) => {
|
||||
if (!Array.isArray(v) || v.length === 0) {
|
||||
cb(new Error('请至少选择一个支付单'))
|
||||
} else {
|
||||
cb()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
return rules
|
||||
})
|
||||
|
||||
async function loadAddPayOrderAvailable(diagnosisId: number, currentLinkedIds: number[] = []) {
|
||||
if (!diagnosisId) {
|
||||
@@ -4986,14 +5055,22 @@ async function submitAddPayOrder() {
|
||||
await addPayOrderFormRef.value.validate()
|
||||
addPayOrderSaving.value = true
|
||||
try {
|
||||
if (addPayOrderForm.add_mode === 'create') {
|
||||
// 手动创建新支付单
|
||||
if (addPayOrderForm.add_mode === 'completion_only') {
|
||||
await prescriptionOrderRequestCompletion({ id: addPayOrderRowId.value })
|
||||
feedback.msgSuccess('完单申请已提交,请等待支付审核')
|
||||
} else if (
|
||||
addPayOrderForm.add_mode === 'create' ||
|
||||
addPayOrderForm.add_mode === 'create_express'
|
||||
) {
|
||||
// 手动创建新支付单(付呗 / 快递代收)
|
||||
await prescriptionOrderAddPayOrder({
|
||||
id: addPayOrderRowId.value,
|
||||
order_type: addPayOrderForm.order_type,
|
||||
pay_amount: addPayOrderForm.pay_amount!,
|
||||
pay_remark: addPayOrderForm.pay_remark || '',
|
||||
completion_request: addPayOrderForm.completion_request
|
||||
completion_request: addPayOrderForm.completion_request,
|
||||
pay_create_type:
|
||||
addPayOrderForm.add_mode === 'create_express' ? 'express_cod' : 'fubei'
|
||||
})
|
||||
feedback.msgSuccess('支付单已新增,请等待审核')
|
||||
} else {
|
||||
|
||||
@@ -390,7 +390,7 @@
|
||||
<el-tooltip placement="top" effect="dark" :show-after="200">
|
||||
<template #content>
|
||||
<div style="max-width: 320px; line-height: 1.7; font-size: 12px">
|
||||
<b>tcm_diagnosis_assign_log</b> 行数(成功指派:<b>to_assistant_id > 0</b>),**仅按 <b>related_po_create_time</b>** 落在所选区间内(与日志表快照一致;诊单手动指派已写入该字段)。按<b>被指派医助</b>归属展示部门。<template v-if="tb.channel_name">
|
||||
<b>tcm_diagnosis_assign_log</b> 中成功指派(<b>to_assistant_id > 0</b>,<b>剔除勾选「继承」的指派</b>)按<b>指派操作时间</b>落在所选区间内,「医助 × 诊单」去重、剔除已删诊单,与「复诊接诊率」页面同口径。按<b>被指派医助</b>归属展示部门。<template v-if="tb.channel_name">
|
||||
选定渠道时收窄与「{{ tb.channel_name }}」业绩同口径(标签诊单或 channels EXISTS)。<br />
|
||||
<b>二中心及其组织下级</b>在选定渠道下本列计 <b>0</b>。
|
||||
</template>
|
||||
|
||||
@@ -174,9 +174,9 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="支付方式" width="100">
|
||||
<el-table-column label="支付方式" width="120">
|
||||
<template #default="{ row }">
|
||||
{{ getPaymentMethodText(row.payment_method) }}
|
||||
{{ getCreateTypeText(row) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
@@ -220,7 +220,7 @@
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="row.status === 1 || row.status === 2 || (row.status === 5 && row.payment_method === 'fubei')"
|
||||
v-if="row.status === 1 || row.status === 2 || (row.status === 5 && (row.payment_method === 'fubei' || row.create_type === 'express_cod'))"
|
||||
v-perms="['order.order/split']"
|
||||
type="primary"
|
||||
link
|
||||
@@ -237,7 +237,7 @@
|
||||
小程序码
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="row.status === 1 || (row.status === 5 && row.payment_method === 'fubei')"
|
||||
v-if="row.status === 1 || (row.status === 5 && (row.payment_method === 'fubei' || row.create_type === 'express_cod'))"
|
||||
v-perms="['order.order/pay']"
|
||||
type="success"
|
||||
link
|
||||
@@ -255,7 +255,7 @@
|
||||
退款
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="row.status === 1 || (row.status === 5 && row.payment_method === 'fubei')"
|
||||
v-if="row.status === 1 || (row.status === 5 && (row.payment_method === 'fubei' || row.create_type === 'express_cod'))"
|
||||
v-perms="['order.order/cancel']"
|
||||
type="danger"
|
||||
link
|
||||
@@ -327,7 +327,7 @@
|
||||
<span class="text-red-500 font-semibold">¥{{ detailData.amount }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="支付方式">
|
||||
{{ getPaymentMethodText(detailData.payment_method) }}
|
||||
{{ getCreateTypeText(detailData) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="支付时间">
|
||||
{{ detailData.payment_time || '-' }}
|
||||
@@ -434,7 +434,7 @@
|
||||
type="warning"
|
||||
:closable="false"
|
||||
class="mb-3"
|
||||
title="该单为付呗·待审核,请选择实际到账方式后确认「已支付」"
|
||||
title="该单为待审核(付呗/快递代收),请选择实际到账方式后确认「已支付」"
|
||||
/>
|
||||
<el-form-item v-if="!payForm.fubeiPendingAudit" label="支付类型" required>
|
||||
<el-radio-group v-model="payForm.payType">
|
||||
@@ -502,6 +502,7 @@
|
||||
<el-radio label="normal">普通订单</el-radio>
|
||||
<el-radio label="wechat_work">企业微信对外收款</el-radio>
|
||||
<el-radio label="fubei">付呗</el-radio>
|
||||
<el-radio label="express_cod">快递代收</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-alert
|
||||
@@ -524,7 +525,20 @@
|
||||
<template #title>付呗</template>
|
||||
通过付呗收款的支付单,创建后请按实际对账/审核流程在列表中处理。
|
||||
</el-alert>
|
||||
<el-form-item v-if="createForm.createType === 'fubei'" label="支付单审核">
|
||||
<el-alert
|
||||
v-if="createForm.createType === 'express_cod'"
|
||||
type="info"
|
||||
:closable="false"
|
||||
show-icon
|
||||
class="mb-4"
|
||||
>
|
||||
<template #title>快递代收</template>
|
||||
通过快递代收的支付单,流转与付呗一致;审核通过/确认到账后将更新为已支付。
|
||||
</el-alert>
|
||||
<el-form-item
|
||||
v-if="createForm.createType === 'fubei' || createForm.createType === 'express_cod'"
|
||||
label="支付单审核"
|
||||
>
|
||||
<el-switch
|
||||
v-model="createForm.requirePaymentSlipAudit"
|
||||
active-text="申请审核"
|
||||
@@ -980,8 +994,8 @@ const patientLoading = ref(false)
|
||||
const patientList = ref<any[]>([])
|
||||
|
||||
const createForm = reactive({
|
||||
createType: 'normal' as 'normal' | 'wechat_work' | 'fubei',
|
||||
/** 仅「付呗」:开启后订单为待审核(5) */
|
||||
createType: 'normal' as 'normal' | 'wechat_work' | 'fubei' | 'express_cod',
|
||||
/** 「付呗」「快递代收」:开启后订单为待审核(5) */
|
||||
requirePaymentSlipAudit: false,
|
||||
patient_id: '',
|
||||
order_type: '',
|
||||
@@ -1148,7 +1162,8 @@ const submitCreateOrder = async () => {
|
||||
patient_id: createForm.patient_id,
|
||||
order_type: createForm.order_type,
|
||||
amount: createForm.amount,
|
||||
remark: createForm.remark
|
||||
remark: createForm.remark,
|
||||
create_type: 'wechat_work'
|
||||
}
|
||||
const res: any = await orderCreateForWechatWork(params)
|
||||
const orderNo = res?.order_no ?? res?.data?.order_no
|
||||
@@ -1160,18 +1175,23 @@ const submitCreateOrder = async () => {
|
||||
patient_id: createForm.patient_id,
|
||||
order_type: createForm.order_type,
|
||||
amount: createForm.amount,
|
||||
remark: createForm.remark
|
||||
remark: createForm.remark,
|
||||
create_type: createForm.createType
|
||||
}
|
||||
if (createForm.createType === 'fubei') {
|
||||
params.payment_channel = 'fubei'
|
||||
params.require_payment_slip_audit = createForm.requirePaymentSlipAudit ? 1 : 0
|
||||
} else if (createForm.createType === 'express_cod') {
|
||||
params.payment_channel = 'express_cod'
|
||||
params.require_payment_slip_audit = createForm.requirePaymentSlipAudit ? 1 : 0
|
||||
} else {
|
||||
params.payment_channel = 'normal'
|
||||
params.require_payment_slip_audit = 0
|
||||
}
|
||||
await orderCreate(params)
|
||||
const tip =
|
||||
createForm.createType === 'fubei' && createForm.requirePaymentSlipAudit
|
||||
(createForm.createType === 'fubei' || createForm.createType === 'express_cod') &&
|
||||
createForm.requirePaymentSlipAudit
|
||||
? '订单已创建,支付状态为「待审核」'
|
||||
: '订单创建成功'
|
||||
feedback.msgSuccess(tip)
|
||||
@@ -1247,6 +1267,27 @@ const getPaymentMethodText = (method: string | null) => {
|
||||
return methodMap[method] || '未知'
|
||||
}
|
||||
|
||||
// 支付方式展示:优先按「创建方式」三值显示;历史单/无 create_type 时回退真实支付渠道文案
|
||||
const getCreateTypeText = (row: any) => {
|
||||
const createTypeMap: Record<string, string> = {
|
||||
normal: '普通订单',
|
||||
wechat_work: '企业微信对外收款',
|
||||
fubei: '付呗',
|
||||
express_cod: '快递代收'
|
||||
}
|
||||
const ct = row?.create_type
|
||||
if (ct === 'wechat_work' || ct === 'fubei' || ct === 'express_cod') {
|
||||
return createTypeMap[ct]
|
||||
}
|
||||
if (ct === 'normal' && row?.payment_method) {
|
||||
return getPaymentMethodText(row.payment_method)
|
||||
}
|
||||
if (ct && createTypeMap[ct]) {
|
||||
return createTypeMap[ct]
|
||||
}
|
||||
return getPaymentMethodText(row?.payment_method)
|
||||
}
|
||||
|
||||
// 编辑订单
|
||||
const handleEditOrder = (row: any) => {
|
||||
editOrderForm.value = {
|
||||
@@ -1373,7 +1414,8 @@ const handleDetail = async (row: any) => {
|
||||
|
||||
// 支付订单
|
||||
const handlePay = (row: any) => {
|
||||
const fubeiPending = row.status === 5 && row.payment_method === 'fubei'
|
||||
const fubeiPending =
|
||||
row.status === 5 && (row.payment_method === 'fubei' || row.create_type === 'express_cod')
|
||||
payForm.value = {
|
||||
order_id: row.id,
|
||||
order_no: row.order_no,
|
||||
|
||||
@@ -0,0 +1,644 @@
|
||||
<template>
|
||||
<div class="revisit-rate-page">
|
||||
<el-card class="!border-none" shadow="never">
|
||||
<el-form :inline="true" class="rate-filter-form">
|
||||
<el-form-item label="统计月份">
|
||||
<el-date-picker
|
||||
v-model="month"
|
||||
type="month"
|
||||
value-format="YYYY-MM"
|
||||
placeholder="选择月份"
|
||||
:clearable="false"
|
||||
:disabled-date="disableFutureMonth"
|
||||
style="width: 160px"
|
||||
@change="loadData"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="部门">
|
||||
<el-tree-select
|
||||
v-model="deptId"
|
||||
:data="deptTreeOptions"
|
||||
placeholder="全部部门"
|
||||
clearable
|
||||
filterable
|
||||
check-strictly
|
||||
node-key="id"
|
||||
:default-expand-all="true"
|
||||
:props="deptTreeProps"
|
||||
style="width: 220px"
|
||||
@change="loadData"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" :loading="loading" @click="loadData">查询</el-button>
|
||||
<el-button :disabled="loading" @click="handleReset">重置</el-button>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-popover placement="bottom-start" :width="500" trigger="hover">
|
||||
<template #reference>
|
||||
<span class="rate-caliber-trigger">
|
||||
<el-icon><InfoFilled /></el-icon>
|
||||
统计口径
|
||||
</span>
|
||||
</template>
|
||||
<div class="rate-caliber">
|
||||
<p>
|
||||
<b>当月被指派总数</b>:当月内诊单被指派给医助(按指派操作时间落月,<b>剔除勾选「继承」的指派</b>)的诊单数,按「医助 × 诊单」去重;部门行 / 合计行按诊单去重。
|
||||
</p>
|
||||
<p>
|
||||
<b>诊次(第 N 次下单)</b>:患者(诊单)名下计入业绩的业务订单(剔除已取消 / 拒收 / 退款)按下单时间升序的全局序号,<b>跨月累计不重置</b>——如 5 月指派后旗下成交 4 单为二诊~五诊,下月再成交即为六诊。
|
||||
</p>
|
||||
<p>
|
||||
<b>当月 N 诊单数</b>:当月内下单且诊次为 N 的订单数,归属下单时点<b>持有该患者的医助</b>(指派可在往月;释放后不再归属;「继承」指派会转移持有人但不计被指派数)。
|
||||
</p>
|
||||
<p>
|
||||
<b>当月 N 诊接诊率</b> = 当月 N 诊单数 ÷ 当月被指派总数。往月指派、当月成交会推高分子,比率可能超过 100%;医助当月无新指派但旗下有成交时,被指派数为 0、比率显示「—」。
|
||||
</p>
|
||||
<p>
|
||||
医助按人事部门归组;选定部门时含其组织下级。
|
||||
</p>
|
||||
</div>
|
||||
</el-popover>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<div class="rate-kpi-grid">
|
||||
<div
|
||||
v-for="card in summaryCards"
|
||||
:key="card.key"
|
||||
class="rate-kpi-card"
|
||||
:class="{ 'is-clickable': card.clickable }"
|
||||
@click="card.clickable && card.onClick && card.onClick()"
|
||||
>
|
||||
<div class="rate-kpi-label">{{ card.label }}</div>
|
||||
<div class="rate-kpi-value">{{ card.value }}</div>
|
||||
<div v-if="card.sub" class="rate-kpi-sub">{{ card.sub }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-card class="!border-none mt-4" shadow="never">
|
||||
<template #header>
|
||||
<div class="rate-card-header">
|
||||
<span>部门 · 医助明细({{ monthText }})</span>
|
||||
<span class="rate-card-hint">点击「被指派数 / N诊单数」可查看具体诊单与订单;底栏合计的被指派数按诊单去重</span>
|
||||
</div>
|
||||
</template>
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="rows"
|
||||
size="small"
|
||||
border
|
||||
row-key="row_key"
|
||||
:tree-props="{ children: 'children' }"
|
||||
:default-expand-all="true"
|
||||
show-summary
|
||||
:summary-method="getSummaries"
|
||||
>
|
||||
<el-table-column label="部门 / 医助" min-width="200" fixed>
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.is_dept" class="rate-dept-cell">
|
||||
{{ row.dept_name }}
|
||||
<span class="rate-dept-count">{{ row.assistant_count }} 人</span>
|
||||
</span>
|
||||
<span v-else>{{ row.assistant_name }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="assigned_count" label="当月被指派数" min-width="110" align="right">
|
||||
<template #default="{ row }">
|
||||
<el-link
|
||||
v-if="canViewAssignLines && row.assigned_count > 0"
|
||||
type="primary"
|
||||
:underline="false"
|
||||
@click="openAssignDetail(row)"
|
||||
>
|
||||
{{ row.assigned_count }}
|
||||
</el-link>
|
||||
<span v-else>{{ row.assigned_count }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template v-for="slot in visitSlots" :key="slot">
|
||||
<el-table-column
|
||||
:prop="`visit${slot}_count`"
|
||||
:label="`${slotLabel(slot)}单数`"
|
||||
min-width="96"
|
||||
align="right"
|
||||
>
|
||||
<template #header>
|
||||
<span>{{ slotLabel(slot) }}单数</span>
|
||||
<el-tooltip
|
||||
effect="dark"
|
||||
placement="top"
|
||||
:content="`当月内下单、诊次为第 ${slot} 次的订单数(诊次跨月累计;归属下单时点持有患者的医助)`"
|
||||
>
|
||||
<el-icon class="rate-th-hint"><InfoFilled /></el-icon>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<template #default="{ row }">
|
||||
<el-link
|
||||
v-if="canViewOrderLines && row[`visit${slot}_count`] > 0"
|
||||
type="primary"
|
||||
:underline="false"
|
||||
@click="openOrderDetail(slot, row)"
|
||||
>
|
||||
{{ row[`visit${slot}_count`] }}
|
||||
</el-link>
|
||||
<span v-else>{{ row[`visit${slot}_count`] }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
:prop="`visit${slot}_rate`"
|
||||
:label="`${slotLabel(slot)}接诊率`"
|
||||
min-width="96"
|
||||
align="right"
|
||||
>
|
||||
<template #default="{ row }">{{ formatRate(row[`visit${slot}_rate`]) }}</template>
|
||||
</el-table-column>
|
||||
</template>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<!-- 下钻明细 -->
|
||||
<el-dialog
|
||||
v-model="detailVisible"
|
||||
:title="detailTitle"
|
||||
width="980px"
|
||||
top="6vh"
|
||||
destroy-on-close
|
||||
>
|
||||
<el-table
|
||||
v-loading="detailLoading"
|
||||
:data="detailRows"
|
||||
size="small"
|
||||
border
|
||||
stripe
|
||||
max-height="60vh"
|
||||
>
|
||||
<template v-if="detailType === 'assign'">
|
||||
<el-table-column prop="diagnosis_id" label="诊单ID" width="90" />
|
||||
<el-table-column prop="patient_name" label="患者" min-width="100">
|
||||
<template #default="{ row }">
|
||||
<el-link
|
||||
v-if="canViewPatientDetail && row.diagnosis_id"
|
||||
type="primary"
|
||||
:underline="false"
|
||||
@click="openPatientDetail(row.diagnosis_id)"
|
||||
>
|
||||
{{ row.patient_name || `诊单#${row.diagnosis_id}` }}
|
||||
</el-link>
|
||||
<span v-else>{{ row.patient_name || '—' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="patient_phone" label="联系电话" min-width="120">
|
||||
<template #default="{ row }">{{ row.patient_phone || '—' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="assistant_names" label="被指派医助" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column prop="assign_count" label="当月指派次数" width="110" align="right" />
|
||||
<el-table-column prop="last_assign_time_text" label="最近指派时间" min-width="160" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-table-column prop="order_no" label="订单号" min-width="180" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<el-link
|
||||
v-if="canViewOrderDetail && row.order_id"
|
||||
type="primary"
|
||||
:underline="false"
|
||||
@click="openOrderInfo(row.order_id)"
|
||||
>
|
||||
{{ row.order_no || `#${row.order_id}` }}
|
||||
</el-link>
|
||||
<span v-else>{{ row.order_no || `#${row.order_id}` }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="diagnosis_id" label="诊单ID" width="90" />
|
||||
<el-table-column prop="patient_name" label="患者" min-width="100">
|
||||
<template #default="{ row }">
|
||||
<el-link
|
||||
v-if="canViewPatientDetail && row.diagnosis_id"
|
||||
type="primary"
|
||||
:underline="false"
|
||||
@click="openPatientDetail(row.diagnosis_id)"
|
||||
>
|
||||
{{ row.patient_name || `诊单#${row.diagnosis_id}` }}
|
||||
</el-link>
|
||||
<span v-else>{{ row.patient_name || '—' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="amount" label="金额" width="100" align="right">
|
||||
<template #default="{ row }">¥ {{ row.amount }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="create_time_text" label="下单时间" min-width="160" />
|
||||
<el-table-column prop="assistant_name" label="持有医助" min-width="100" />
|
||||
<el-table-column prop="creator_name" label="订单创建人" min-width="100" />
|
||||
</template>
|
||||
</el-table>
|
||||
<div class="rate-detail-footer">共 {{ detailRows.length }} 条</div>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 订单详情抽屉(共享组件,与处方订单列表详情一致;只读) -->
|
||||
<order-detail-drawer ref="orderDetailRef" readonly append-to-body />
|
||||
|
||||
<!-- 诊单(患者)详情:编辑页只读模式 -->
|
||||
<diagnosis-edit-popup ref="diagnosisEditRef" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="revisitRateStatsPage">
|
||||
import { computed, defineAsyncComponent, onMounted, ref } from 'vue'
|
||||
import { InfoFilled } from '@element-plus/icons-vue'
|
||||
|
||||
import {
|
||||
revisitRateAssignLines,
|
||||
revisitRateDeptOptions,
|
||||
revisitRateOverview,
|
||||
revisitRateVisitOrderLines
|
||||
} from '@/api/stats'
|
||||
import { hasPermission } from '@/utils/perm'
|
||||
|
||||
/** 业务订单详情抽屉(共享组件,readonly 模式,与 order_list.vue 详情同源) */
|
||||
const OrderDetailDrawer = defineAsyncComponent(
|
||||
() => import('@/views/consumer/prescription/components/PrescriptionOrderDetailDrawer.vue')
|
||||
)
|
||||
/** 诊单编辑弹窗(只读打开) */
|
||||
const DiagnosisEditPopup = defineAsyncComponent(() => import('@/views/tcm/diagnosis/edit.vue'))
|
||||
|
||||
interface MetricPack extends Record<string, any> {
|
||||
assigned_count: number
|
||||
}
|
||||
|
||||
interface StatsRow extends MetricPack {
|
||||
row_key: string
|
||||
is_dept: 0 | 1
|
||||
dept_id: number
|
||||
dept_name: string
|
||||
assistant_id?: number
|
||||
assistant_name?: string
|
||||
assistant_count?: number
|
||||
children?: StatsRow[]
|
||||
}
|
||||
|
||||
interface DeptTreeNode {
|
||||
id: number
|
||||
name: string
|
||||
pid: number
|
||||
children?: DeptTreeNode[]
|
||||
}
|
||||
|
||||
const canViewAssignLines = hasPermission(['stats.revisitRate/assignLines'])
|
||||
const canViewOrderLines = hasPermission(['stats.revisitRate/visitOrderLines'])
|
||||
const canViewOrderDetail = hasPermission(['tcm.prescriptionOrder/detail'])
|
||||
const canViewPatientDetail = hasPermission(['tcm.diagnosis/detail'])
|
||||
|
||||
/** 接口动态返回的诊次分档(最少 2~4,按数据扩展到五诊、六诊…) */
|
||||
const visitSlots = ref<number[]>([2, 3, 4])
|
||||
|
||||
const cnDigits = ['零', '一', '二', '三', '四', '五', '六', '七', '八', '九', '十']
|
||||
const slotLabel = (slot: number) => {
|
||||
if (slot >= 2 && slot <= 10) {
|
||||
return `${cnDigits[slot]}诊`
|
||||
}
|
||||
if (slot > 10 && slot < 20) {
|
||||
return `十${cnDigits[slot - 10]}诊`
|
||||
}
|
||||
return `${slot}诊`
|
||||
}
|
||||
|
||||
const initNow = new Date()
|
||||
const currentMonth = `${initNow.getFullYear()}-${String(initNow.getMonth() + 1).padStart(2, '0')}`
|
||||
const month = ref<string>(currentMonth)
|
||||
const deptId = ref<number | undefined>(undefined)
|
||||
const loading = ref(false)
|
||||
const rows = ref<StatsRow[]>([])
|
||||
const total = ref<MetricPack | null>(null)
|
||||
|
||||
const deptTreeOptions = ref<DeptTreeNode[]>([])
|
||||
const deptTreeProps = { value: 'id', label: 'name', children: 'children' }
|
||||
|
||||
const monthText = computed(() => {
|
||||
const [y, m] = month.value.split('-')
|
||||
return y && m ? `${y}年${Number(m)}月` : month.value
|
||||
})
|
||||
|
||||
const formatRate = (rate: number | null | undefined) =>
|
||||
rate === null || rate === undefined ? '—' : `${rate}%`
|
||||
|
||||
const baseQuery = () => ({
|
||||
month: month.value,
|
||||
dept_ids: deptId.value ? String(deptId.value) : ''
|
||||
})
|
||||
|
||||
interface SummaryCard {
|
||||
key: string
|
||||
label: string
|
||||
value: string
|
||||
sub: string
|
||||
clickable: boolean
|
||||
onClick?: () => void
|
||||
}
|
||||
|
||||
const summaryCards = computed<SummaryCard[]>(() => {
|
||||
const t = total.value
|
||||
const cards: SummaryCard[] = [
|
||||
{
|
||||
key: 'assigned',
|
||||
label: '当月被指派总数',
|
||||
value: t ? String(t.assigned_count) : '—',
|
||||
sub: '',
|
||||
clickable: !!(canViewAssignLines && t && t.assigned_count > 0),
|
||||
onClick: () => openAssignDetail(null)
|
||||
}
|
||||
]
|
||||
for (const slot of visitSlots.value) {
|
||||
const cnt = t ? t[`visit${slot}_count`] : null
|
||||
const rate = t ? t[`visit${slot}_rate`] : null
|
||||
cards.push({
|
||||
key: `visit${slot}`,
|
||||
label: `当月${slotLabel(slot)}接诊率`,
|
||||
value: t ? formatRate(rate) : '—',
|
||||
sub: t ? `${slotLabel(slot)}下单 ${cnt} 单` : '',
|
||||
clickable: !!(canViewOrderLines && cnt > 0),
|
||||
onClick: () => openOrderDetail(slot, null)
|
||||
})
|
||||
}
|
||||
return cards
|
||||
})
|
||||
|
||||
/** 底栏合计:直接展示后端按诊单去重后的合计,不做各行求和 */
|
||||
const getSummaries = ({ columns }: { columns: any[] }) => {
|
||||
const t = total.value
|
||||
return columns.map((col, index) => {
|
||||
if (index === 0) {
|
||||
return '合计'
|
||||
}
|
||||
if (!t) {
|
||||
return ''
|
||||
}
|
||||
const prop = String(col.property || '')
|
||||
if (prop === 'assigned_count') {
|
||||
return String(t.assigned_count)
|
||||
}
|
||||
if (/^visit\d+_count$/.test(prop)) {
|
||||
return String(t[prop] ?? '')
|
||||
}
|
||||
if (/^visit\d+_rate$/.test(prop)) {
|
||||
return formatRate(t[prop])
|
||||
}
|
||||
return ''
|
||||
})
|
||||
}
|
||||
|
||||
const disableFutureMonth = (date: Date) => {
|
||||
const now = new Date()
|
||||
return date.getFullYear() * 12 + date.getMonth() > now.getFullYear() * 12 + now.getMonth()
|
||||
}
|
||||
|
||||
const loadData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await revisitRateOverview(baseQuery())
|
||||
rows.value = res?.rows ?? []
|
||||
total.value = res?.total ?? null
|
||||
if (Array.isArray(res?.slots) && res.slots.length) {
|
||||
visitSlots.value = res.slots.map((s: any) => Number(s)).filter((s: number) => s >= 2)
|
||||
}
|
||||
if (res?.month) {
|
||||
month.value = res.month
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const buildDeptTree = (flat: Array<{ id: number; pid: number; name: string }>): DeptTreeNode[] => {
|
||||
const map = new Map<number, DeptTreeNode>()
|
||||
for (const d of flat) {
|
||||
map.set(d.id, { ...d, children: [] })
|
||||
}
|
||||
const roots: DeptTreeNode[] = []
|
||||
for (const d of flat) {
|
||||
const node = map.get(d.id)!
|
||||
if (d.pid > 0 && map.has(d.pid)) {
|
||||
map.get(d.pid)!.children!.push(node)
|
||||
} else {
|
||||
roots.push(node)
|
||||
}
|
||||
}
|
||||
const strip = (n: DeptTreeNode) => {
|
||||
if (n.children?.length) {
|
||||
n.children.forEach(strip)
|
||||
} else {
|
||||
delete n.children
|
||||
}
|
||||
}
|
||||
roots.forEach(strip)
|
||||
return roots
|
||||
}
|
||||
|
||||
const loadDeptOptions = async () => {
|
||||
try {
|
||||
const res = await revisitRateDeptOptions()
|
||||
deptTreeOptions.value = buildDeptTree(res?.rows ?? [])
|
||||
} catch {
|
||||
deptTreeOptions.value = []
|
||||
}
|
||||
}
|
||||
|
||||
// ── 下钻明细 ──────────────────────────────────────────
|
||||
const detailVisible = ref(false)
|
||||
const detailType = ref<'assign' | 'orders'>('assign')
|
||||
const detailTitle = ref('')
|
||||
const detailLoading = ref(false)
|
||||
const detailRows = ref<any[]>([])
|
||||
|
||||
/** row 为 null 时表示合计(当前部门筛选范围) */
|
||||
const rowScope = (row: StatsRow | null): { assistant_id?: number; dept_id?: number; label: string } => {
|
||||
if (!row) {
|
||||
return { label: '合计' }
|
||||
}
|
||||
if (row.is_dept) {
|
||||
return { dept_id: row.dept_id, label: row.dept_name }
|
||||
}
|
||||
return { assistant_id: row.assistant_id, label: row.assistant_name || '' }
|
||||
}
|
||||
|
||||
const openAssignDetail = async (row: StatsRow | null) => {
|
||||
if (!canViewAssignLines) {
|
||||
return
|
||||
}
|
||||
const scope = rowScope(row)
|
||||
detailType.value = 'assign'
|
||||
detailTitle.value = `${scope.label} · 被指派明细(${monthText.value})`
|
||||
detailVisible.value = true
|
||||
detailLoading.value = true
|
||||
detailRows.value = []
|
||||
try {
|
||||
const res = await revisitRateAssignLines({
|
||||
...baseQuery(),
|
||||
assistant_id: scope.assistant_id,
|
||||
dept_id: scope.dept_id
|
||||
})
|
||||
detailRows.value = res?.rows ?? []
|
||||
} finally {
|
||||
detailLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const openOrderDetail = async (slot: number, row: StatsRow | null) => {
|
||||
if (!canViewOrderLines) {
|
||||
return
|
||||
}
|
||||
const scope = rowScope(row)
|
||||
detailType.value = 'orders'
|
||||
detailTitle.value = `${scope.label} · ${slotLabel(slot)}订单明细(${monthText.value})`
|
||||
detailVisible.value = true
|
||||
detailLoading.value = true
|
||||
detailRows.value = []
|
||||
try {
|
||||
const res = await revisitRateVisitOrderLines({
|
||||
...baseQuery(),
|
||||
slot,
|
||||
assistant_id: scope.assistant_id,
|
||||
dept_id: scope.dept_id
|
||||
})
|
||||
detailRows.value = res?.rows ?? []
|
||||
} finally {
|
||||
detailLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ── 订单详情(复用处方订单列表的详情抽屉) ──────────────
|
||||
const orderDetailRef = ref<{ open: (id: number) => void } | null>(null)
|
||||
|
||||
const openOrderInfo = (orderId: number) => {
|
||||
if (!canViewOrderDetail || !orderId) {
|
||||
return
|
||||
}
|
||||
orderDetailRef.value?.open(orderId)
|
||||
}
|
||||
|
||||
// ── 患者(诊单)详情:诊单编辑页只读打开 ────────────────
|
||||
const diagnosisEditRef = ref<{ openViewOnly: (id: number) => void } | null>(null)
|
||||
|
||||
const openPatientDetail = (diagnosisId: number) => {
|
||||
if (!canViewPatientDetail || !diagnosisId) {
|
||||
return
|
||||
}
|
||||
diagnosisEditRef.value?.openViewOnly(diagnosisId)
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
month.value = currentMonth
|
||||
deptId.value = undefined
|
||||
loadData()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadDeptOptions()
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.revisit-rate-page {
|
||||
.rate-filter-form {
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.rate-caliber-trigger {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 13px;
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
.rate-kpi-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 12px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.rate-kpi-card {
|
||||
background: var(--el-bg-color);
|
||||
border-radius: 8px;
|
||||
padding: 16px 20px;
|
||||
|
||||
&.is-clickable {
|
||||
cursor: pointer;
|
||||
transition: box-shadow 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
box-shadow: var(--el-box-shadow-light);
|
||||
}
|
||||
}
|
||||
|
||||
.rate-kpi-label {
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.rate-kpi-value {
|
||||
margin-top: 6px;
|
||||
font-size: 26px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.rate-kpi-sub {
|
||||
margin-top: 2px;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
.rate-card-header {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 10px;
|
||||
|
||||
.rate-card-hint {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
.rate-dept-cell {
|
||||
font-weight: 600;
|
||||
|
||||
.rate-dept-count {
|
||||
margin-left: 6px;
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
.rate-th-hint {
|
||||
margin-left: 2px;
|
||||
vertical-align: -2px;
|
||||
color: var(--el-text-color-secondary);
|
||||
cursor: help;
|
||||
}
|
||||
}
|
||||
|
||||
.rate-detail-footer {
|
||||
margin-top: 10px;
|
||||
text-align: right;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.rate-caliber {
|
||||
font-size: 13px;
|
||||
line-height: 1.7;
|
||||
color: var(--el-text-color-regular);
|
||||
|
||||
p + p {
|
||||
margin-top: 6px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -56,298 +56,21 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 业务订单详情抽屉(只读,与 order_list.vue 详情保持一致) -->
|
||||
<el-drawer
|
||||
v-model="detailVisible"
|
||||
size="80%"
|
||||
destroy-on-close
|
||||
:close-on-click-modal="false"
|
||||
append-to-body
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex items-center">
|
||||
<span class="font-semibold text-lg">业务订单详情</span>
|
||||
<el-tag v-if="detailData?.order_no" type="primary" effect="plain" round class="ml-3">{{ detailData.order_no }}</el-tag>
|
||||
<el-tag v-if="detailData?.gancao_reciperl_order_no" type="success" effect="plain" round class="ml-2">甘草 {{ detailData.gancao_reciperl_order_no }}</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
<el-skeleton v-if="detailLoading" :rows="12" animated class="px-6 py-4" />
|
||||
<div v-else-if="detailData" class="px-6 pb-6 overflow-y-auto">
|
||||
|
||||
<!-- 顶部数据概览 -->
|
||||
<el-row :gutter="16" class="mb-5">
|
||||
<el-col :xs="12" :sm="6">
|
||||
<el-card shadow="never" class="border border-gray-100 bg-gray-50/50">
|
||||
<div class="text-gray-500 text-xs mb-1">总金额</div>
|
||||
<div class="text-red-500 font-bold text-xl">¥{{ detailData.amount }}</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :xs="12" :sm="6">
|
||||
<el-card shadow="never" class="border border-gray-100 bg-gray-50/50">
|
||||
<div class="text-gray-500 text-xs mb-1">已付总额</div>
|
||||
<div class="text-green-600 font-bold text-xl">¥{{ detailData.linked_pay_paid_total || 0 }}</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :xs="12" :sm="6">
|
||||
<el-card shadow="never" class="border border-gray-100 bg-gray-50/50">
|
||||
<div class="text-gray-500 text-xs mb-1">需代收</div>
|
||||
<div class="font-bold text-xl" :class="detailAgencyToCollect > 0 ? 'text-amber-600' : 'text-gray-500'">¥{{ formatMoney(detailAgencyToCollect) }}</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :xs="12" :sm="6">
|
||||
<el-card shadow="never" class="border border-gray-100 bg-gray-50/50">
|
||||
<div class="text-gray-500 text-xs mb-1">已付笔数</div>
|
||||
<div class="text-primary font-bold text-xl">{{ detailLinkedPayOrders.length }} 笔</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
||||
<!-- 左侧:处方单(不含测试价格、改患者信息、药材明细) -->
|
||||
<el-card shadow="never" class="border-gray-100 h-full relative overflow-hidden">
|
||||
<div v-if="detailData.prescription_audit_status === 1" class="audit-stamp stamp-pass"><div class="stamp-inner">审核通过</div></div>
|
||||
<div v-if="detailData.prescription_audit_status === 2" class="audit-stamp stamp-reject"><div class="stamp-inner">已驳回</div></div>
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="font-medium text-[15px]">处方详情</span>
|
||||
<span v-if="detailData.doctor_name" class="text-xs text-gray-500">
|
||||
开方人:<span class="text-primary font-medium">{{ detailData.doctor_name }}</span>
|
||||
</span>
|
||||
</div>
|
||||
<span class="text-xs text-gray-400">ID: {{ detailData.prescription_id || '—' }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="detailPrescription">
|
||||
<el-descriptions :column="2" border size="small" class="bg-white">
|
||||
<el-descriptions-item label="处方编号" :span="2">{{ detailPrescription.sn || '—' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="患者">{{ detailPrescription.patient_name || '—' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="性别 / 年龄">{{ detailPrescription.gender_desc || '—' }} · {{ detailPrescription.age ?? '—' }}岁</el-descriptions-item>
|
||||
<el-descriptions-item label="手机">
|
||||
<span :class="detailRecipientRxPhoneMismatch ? 'text-red-600 font-bold' : ''">{{ detailPrescription.phone || '—' }}</span>
|
||||
<el-tag v-if="detailRecipientRxPhoneMismatch" type="danger" size="small" effect="dark" class="ml-2">与订单手机不一致</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="处方日期">{{ detailPrescription.prescription_date || '—' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="医师">{{ detailPrescription.doctor_name || '—' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="类型">{{ detailPrescription.prescription_type || '—' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="临床诊断" :span="2">{{ detailPrescription.clinical_diagnosis || '—' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="剂数 / 用法" :span="2" v-if="showPrescriptionAuditFilter">
|
||||
{{ detailData.dose_count ?? detailPrescription.dose_count ?? '—' }} {{ detailData.dose_unit || '剂' }} ·
|
||||
{{ detailPrescription.usage_instruction || detailPrescription.usage_method || '—' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="处方金额">
|
||||
<span class="text-red-500 font-medium">¥{{ detailPrescription.amount ?? '—' }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="消费者审核">
|
||||
<el-tag :type="consumerRxAuditTag(detailPrescription.audit_status)" size="small">{{ consumerRxAuditText(detailPrescription.audit_status) }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="用量">
|
||||
<template v-if="detailPrescription.dosage_amount">
|
||||
{{ detailPrescription.dosage_amount }}{{ detailPrescription.dosage_unit || 'g' }}
|
||||
<template v-if="detailPrescription.prescription_type === '浓缩水丸'">
|
||||
· {{ Number(detailPrescription.dosage_bag_count) > 0 ? Number(detailPrescription.dosage_bag_count) : 1 }}袋
|
||||
</template>
|
||||
<span v-if="detailPrescription.prescription_type === '饮片' && detailPrescription.need_decoction !== null" class="ml-2 text-gray-500">
|
||||
({{ detailPrescription.need_decoction ? '代煎' : '不代煎' }})
|
||||
</span>
|
||||
</template>
|
||||
<template v-else>—</template>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="服用方式">
|
||||
<div class="flex flex-col gap-1.5 text-sm leading-relaxed">
|
||||
<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>
|
||||
<div>
|
||||
<span class="text-gray-500">订单设置:</span>
|
||||
{{ detailData.medication_days != null && String(detailData.medication_days).trim() !== '' ? detailData.medication_days + ' 天' : '—' }}
|
||||
</div>
|
||||
</div>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="状态" :span="2">
|
||||
<el-tag :type="Number(detailPrescription.void_status) === 1 ? 'danger' : 'success'" size="small">{{ Number(detailPrescription.void_status) === 1 ? '已作废' : '正常' }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</template>
|
||||
<el-empty v-else description="无处方数据" :image-size="72" />
|
||||
</el-card>
|
||||
|
||||
<!-- 右侧:未关联 + 已关联支付单 -->
|
||||
<div class="flex flex-col gap-4 h-full">
|
||||
<el-card shadow="never" class="border-gray-100">
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="font-medium text-[15px]">未关联的收款记录</span>
|
||||
<span class="text-xs text-gray-400">本诊单下未被占用的支付单</span>
|
||||
</div>
|
||||
</template>
|
||||
<el-table v-if="detailUnlinkedPayOrders.length" :data="detailUnlinkedPayOrders" size="small" border stripe max-height="280">
|
||||
<el-table-column prop="id" label="ID" width="70" />
|
||||
<el-table-column prop="order_no" label="单号" min-width="135" show-overflow-tooltip />
|
||||
<el-table-column label="类型" width="90">
|
||||
<template #default="{ row }">{{ row.order_type_desc || feeTypeText(row.order_type) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="金额" width="86" align="right">
|
||||
<template #default="{ row }"><span class="text-red-500">¥{{ row.amount }}</span></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="82" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="payOrderStatusTag(row.status)" size="small">{{ row.status_desc || orderStatusText(row.status) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="creator_name" label="创建人" width="80" show-overflow-tooltip />
|
||||
<el-table-column label="创建时间" min-width="136">
|
||||
<template #default="{ row }">{{ formatOrderTime(row.create_time) }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-empty v-else description="无未关联的收款单" :image-size="60" />
|
||||
</el-card>
|
||||
|
||||
<el-card shadow="never" class="border-gray-100 relative overflow-hidden">
|
||||
<div v-if="detailData.payment_slip_audit_status === 1" class="audit-stamp stamp-pass"><div class="stamp-inner">审核通过</div></div>
|
||||
<div v-if="detailData.payment_slip_audit_status === 2" class="audit-stamp stamp-reject"><div class="stamp-inner">已驳回</div></div>
|
||||
<template #header>
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="font-medium text-[15px]">关联收款记录</span>
|
||||
<span v-if="detailData.creator_name" class="text-xs text-gray-500">创建人:<span class="text-primary font-medium">{{ detailData.creator_name }}</span></span>
|
||||
</div>
|
||||
</template>
|
||||
<el-table v-if="detailLinkedPayOrders.length" :data="detailLinkedPayOrders" size="small" border stripe max-height="280">
|
||||
<el-table-column prop="id" label="ID" width="70" />
|
||||
<el-table-column prop="order_no" label="单号" min-width="135" show-overflow-tooltip />
|
||||
<el-table-column label="类型" width="90">
|
||||
<template #default="{ row }">{{ row.order_type_desc || feeTypeText(row.order_type) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="金额" width="86" align="right">
|
||||
<template #default="{ row }"><span class="text-red-500">¥{{ row.amount }}</span></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="82" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="payOrderStatusTag(row.status)" size="small">{{ row.status_desc || orderStatusText(row.status) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="creator_name" label="创建人" width="80" show-overflow-tooltip />
|
||||
<el-table-column label="创建时间" min-width="136">
|
||||
<template #default="{ row }">{{ formatOrderTime(row.create_time) }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-empty v-else description="未关联收款单" :image-size="60" />
|
||||
</el-card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 履约与收货信息 -->
|
||||
<el-card shadow="never" class="border-gray-100 mb-4">
|
||||
<template #header><span class="font-medium text-[15px]">履约与收货信息</span></template>
|
||||
<el-descriptions :column="3" border size="small">
|
||||
<el-descriptions-item label="诊单ID" :span="3"><span class="font-mono">{{ detailData.diagnosis_id }}</span></el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间">{{ formatTime(detailData.create_time) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="业务订单创建人" :span="2">
|
||||
<div v-if="detailData.creator_name" class="text-[13px] space-y-1">
|
||||
<div><span class="font-medium text-gray-900">{{ detailData.creator_name || '—' }}</span></div>
|
||||
<div v-if="detailOrderCreatorMetaLine" class="text-gray-600 text-xs">{{ detailOrderCreatorMetaLine }}</div>
|
||||
</div>
|
||||
<span v-else class="text-gray-400">—</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="创建人所属部门" :span="3">
|
||||
<div v-if="detailOrderCreatorDeptBreadcrumbs.length" class="space-y-1.5">
|
||||
<div v-for="(segments, idx) in detailOrderCreatorDeptBreadcrumbs" :key="idx">
|
||||
<el-breadcrumb v-if="segments.length" class="text-[13px]" separator="/">
|
||||
<el-breadcrumb-item v-for="(name, j) in segments" :key="j"><span class="text-gray-800">{{ name }}</span></el-breadcrumb-item>
|
||||
</el-breadcrumb>
|
||||
</div>
|
||||
</div>
|
||||
<span v-else class="text-gray-400">—</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="收货人">{{ detailData.recipient_name }}</el-descriptions-item>
|
||||
<el-descriptions-item label="收货手机">
|
||||
<span :class="detailRecipientRxPhoneMismatch ? 'text-red-600 font-bold font-mono' : 'font-mono'">{{ detailData.recipient_phone || '—' }}</span>
|
||||
<el-tag v-if="detailRecipientRxPhoneMismatch" type="danger" size="small" effect="dark" class="ml-2">与处方手机不一致</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="收货地址">{{ detailFullAddress }}</el-descriptions-item>
|
||||
<el-descriptions-item label="复诊">{{ detailData.is_follow_up ? '是' : '否' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="用药疗程">{{ detailData.medication_days ? detailData.medication_days + ' 天' : '—' }}</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="服务套餐">{{ formatServicePackage(detailData.service_package) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="费用类别">{{ feeTypeText(detailData.fee_type) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="快递单号">{{ detailData.tracking_number || '—' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="快递公司">{{ expressCompanyLabel(detailData.express_company) }}</el-descriptions-item>
|
||||
<el-descriptions-item v-perms="['tcm.prescriptionOrder/editRemarkExtra']" label="药房备注" :span="3">{{ detailData.remark_extra || '—' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="医助备注" :span="3">{{ detailData.remark_assistant || '—' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="处方审核意见" :span="3">{{ detailData.prescription_audit_remark || '—' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="支付审核意见" :span="3">{{ detailData.payment_slip_audit_remark || '—' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
|
||||
<!-- 物流轨迹(只读) -->
|
||||
<el-card v-if="String(detailData.tracking_number || '').trim()" shadow="never" class="border-gray-100 mb-4">
|
||||
<template #header><span class="font-medium text-[15px]">物流轨迹</span></template>
|
||||
<div class="flex flex-wrap items-center gap-3 mb-4">
|
||||
<el-input v-model="logisticsTracePhoneTail" clearable maxlength="20" placeholder="收件手机" class="w-[180px]" @keyup.enter="fetchLogisticsTrace" />
|
||||
<el-select v-model="detailLogisticsExpress" placeholder="承运商" style="width: 140px">
|
||||
<el-option label="自动识别" value="auto" />
|
||||
<el-option label="顺丰速运" value="sf" />
|
||||
<el-option label="京东快递" value="jd" />
|
||||
<el-option label="极兔速递" value="jt" />
|
||||
</el-select>
|
||||
<el-button v-perms="['tcm.prescriptionOrder/logisticsTrace']" type="primary" plain size="default" :loading="logisticsTraceLoading" @click="fetchLogisticsTrace">刷新轨迹</el-button>
|
||||
</div>
|
||||
<div v-if="logisticsTraceLoading && !logisticsTracePayload" class="py-6 text-center text-sm text-gray-500">正在加载物流轨迹...</div>
|
||||
<div v-else-if="logisticsTracePayload" class="bg-gray-50/50 p-4 rounded-md">
|
||||
<div v-if="logisticsTracePayload.state_text" class="text-sm mb-4 font-medium flex items-center gap-2">
|
||||
<span class="text-gray-800">状态:{{ logisticsTracePayload.state_text }}</span>
|
||||
<el-tag v-if="logisticsTracePayload.carrier_label" size="small" type="info">{{ logisticsTracePayload.carrier_label }}</el-tag>
|
||||
</div>
|
||||
<el-timeline v-if="logisticsTraceList.length" class="mt-2 pl-2">
|
||||
<el-timeline-item v-for="(t, idx) in logisticsTraceList" :key="idx" :type="idx === 0 ? 'primary' : 'info'" :hollow="idx !== 0" :timestamp="t.time" placement="top">
|
||||
<span :class="idx === 0 ? 'text-gray-800 font-medium' : 'text-gray-500'">{{ t.context }}</span>
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
<el-empty v-else description="暂无轨迹节点记录" :image-size="64" />
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 操作日志 -->
|
||||
<el-card v-perms="['tcm.prescriptionOrder/logs']" shadow="never" class="border-gray-100">
|
||||
<template #header><span class="font-medium text-[15px]">操作日志</span></template>
|
||||
<el-timeline v-if="detailLogs.length" class="mt-2 pl-2">
|
||||
<el-timeline-item v-for="(log, idx) in detailLogs" :key="log.id" :type="idx === 0 ? 'primary' : 'info'" :hollow="idx !== 0" :timestamp="formatTime(log.create_time)" placement="top">
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<span class="font-medium text-[13px]">{{ log.admin_name }}</span>
|
||||
<el-tag size="small" type="info">{{ logActionText(log.action) }}</el-tag>
|
||||
</div>
|
||||
<span :class="idx === 0 ? 'text-gray-800 font-medium' : 'text-gray-500'">{{ log.summary }}</span>
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
<el-empty v-else description="暂无操作日志" :image-size="64" />
|
||||
</el-card>
|
||||
</div>
|
||||
</el-drawer>
|
||||
<!-- 业务订单详情抽屉:共享组件的 readonly 受限版(展示范围由组件内 v-if="!readonly" 门控) -->
|
||||
<prescription-order-detail-drawer ref="detailDrawerRef" readonly append-to-body />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, watch, ref, onMounted } from 'vue'
|
||||
import { computed, reactive, ref, watch } from 'vue'
|
||||
import { usePaging } from '@/hooks/usePaging'
|
||||
import { prescriptionOrderLists } from '@/api/tcm'
|
||||
import PrescriptionOrderDetailDrawer from '@/views/consumer/prescription/components/PrescriptionOrderDetailDrawer.vue'
|
||||
import {
|
||||
prescriptionOrderLists,
|
||||
prescriptionOrderDetail,
|
||||
prescriptionOrderLogs,
|
||||
prescriptionOrderLogisticsTrace,
|
||||
prescriptionOrderPaidPayOrders
|
||||
} from '@/api/tcm'
|
||||
import { getDictData } from '@/api/app'
|
||||
import { hasPermission } from '@/utils/perm'
|
||||
import feedback from '@/utils/feedback'
|
||||
import useUserStore from '@/stores/modules/user'
|
||||
|
||||
const userStore = useUserStore()
|
||||
formatTime,
|
||||
fulfillmentText,
|
||||
fulfillmentTagType
|
||||
} from '@/views/consumer/prescription/components/prescription-order-utils'
|
||||
|
||||
const props = defineProps<{
|
||||
diagnosisId: number
|
||||
@@ -379,175 +102,11 @@ const buildParams = () => {
|
||||
queryParams.scene = 'diagnosis_edit'
|
||||
}
|
||||
|
||||
/** 诊间医助角色(与 DiagnosisLists 等一致) */
|
||||
const TCM_ASSISTANT_ROLE_ID = 2
|
||||
/** 与 server/config/project.php prescription_audit_roles 默认一致,可处方审核的角色 */
|
||||
const PRESCRIPTION_AUDIT_ROLE_IDS = [0, 3, 6]
|
||||
// ─── 详情抽屉(共享组件,数据拉取/展示全部在组件内) ───
|
||||
const detailDrawerRef = ref<InstanceType<typeof PrescriptionOrderDetailDrawer>>()
|
||||
|
||||
/**
|
||||
* 是否展示「处方审核」相关字段
|
||||
*/
|
||||
const showPrescriptionAuditFilter = 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)) : []
|
||||
if (!ids.includes(TCM_ASSISTANT_ROLE_ID)) return true
|
||||
return ids.some((id) => PRESCRIPTION_AUDIT_ROLE_IDS.includes(id))
|
||||
})
|
||||
|
||||
// ─── 详情抽屉 ───
|
||||
const detailVisible = ref(false)
|
||||
const detailLoading = ref(false)
|
||||
const detailData = ref<any>(null)
|
||||
const detailLogs = ref<any[]>([])
|
||||
const detailUnlinkedPayOrders = ref<any[]>([])
|
||||
|
||||
const detailPrescription = computed(() => {
|
||||
const p = detailData.value?.prescription
|
||||
return p && typeof p === 'object' ? p : null
|
||||
})
|
||||
|
||||
const detailLinkedPayOrders = computed(() => {
|
||||
const arr = detailData.value?.linked_pay_orders
|
||||
return Array.isArray(arr) ? arr : []
|
||||
})
|
||||
|
||||
const detailAgencyToCollect = computed(() => {
|
||||
const d = detailData.value
|
||||
if (!d) return 0
|
||||
const total = Number(d.amount) || 0
|
||||
const paid = Number(d.linked_pay_paid_total) || 0
|
||||
return Math.round((total - paid) * 100) / 100
|
||||
})
|
||||
|
||||
const detailRecipientRxPhoneMismatch = computed(() => {
|
||||
const rx = normalizeBizPhone(detailPrescription.value?.phone)
|
||||
const biz = normalizeBizPhone(detailData.value?.recipient_phone)
|
||||
if (!rx || !biz) return false
|
||||
return rx !== biz
|
||||
})
|
||||
|
||||
const detailFullAddress = computed(() => {
|
||||
const d = detailData.value
|
||||
if (!d) return '—'
|
||||
const parts = []
|
||||
if (d.shipping_province) parts.push(d.shipping_province)
|
||||
if (d.shipping_city) parts.push(d.shipping_city)
|
||||
if (d.shipping_district) parts.push(d.shipping_district)
|
||||
if (d.shipping_address) parts.push(d.shipping_address)
|
||||
return parts.length > 0 ? parts.join(' ') : '—'
|
||||
})
|
||||
|
||||
const detailOrderCreatorMetaLine = computed(() => {
|
||||
const d = detailData.value
|
||||
if (!d) return ''
|
||||
const account = String(d.creator_account ?? '').trim()
|
||||
const mobile = String(d.creator_mobile ?? '').trim()
|
||||
const parts: string[] = []
|
||||
if (account) parts.push(`登录账号:${account}`)
|
||||
if (mobile) parts.push(`手机:${mobile}`)
|
||||
return parts.join(' · ')
|
||||
})
|
||||
|
||||
const detailOrderCreatorDeptBreadcrumbs = computed(() => {
|
||||
const raw = String(detailData.value?.order_creator_dept_path ?? detailData.value?.diagnosis_creator_dept_path ?? '').trim()
|
||||
if (!raw) return [] as string[][]
|
||||
return raw.split(';').map((s) => s.trim()).filter(Boolean)
|
||||
.map((p) => p.includes(' / ') ? p.split(' / ').map((x) => x.trim()).filter(Boolean) : p.split('/').map((x) => x.trim()).filter(Boolean))
|
||||
.filter((segs) => segs.length > 0)
|
||||
})
|
||||
|
||||
// ─── 物流轨迹 ───
|
||||
const detailLogisticsExpress = ref<string>('auto')
|
||||
const logisticsTraceLoading = ref(false)
|
||||
const logisticsTracePayload = ref<Record<string, any> | null>(null)
|
||||
const logisticsTracePhoneTail = ref('')
|
||||
|
||||
const logisticsTraceList = computed(() => {
|
||||
const p = logisticsTracePayload.value
|
||||
if (!p || !Array.isArray(p.traces)) return []
|
||||
return p.traces as Array<{ time: string; context: string }>
|
||||
})
|
||||
|
||||
async function fetchLogisticsTrace() {
|
||||
const id = Number(detailData.value?.id)
|
||||
if (!id) return
|
||||
logisticsTraceLoading.value = true
|
||||
try {
|
||||
const digits = String(logisticsTracePhoneTail.value || '').replace(/\D/g, '')
|
||||
const params: { id: number; express_company?: string; phone_tail?: string } = {
|
||||
id,
|
||||
express_company: detailLogisticsExpress.value
|
||||
}
|
||||
if (digits.length >= 4) params.phone_tail = digits
|
||||
const res: any = await prescriptionOrderLogisticsTrace(params)
|
||||
logisticsTracePayload.value = (res?.data ?? res) as Record<string, any>
|
||||
} catch {
|
||||
logisticsTracePayload.value = null
|
||||
} finally {
|
||||
logisticsTraceLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 打开详情 ───
|
||||
async function openDetail(id: number) {
|
||||
detailData.value = null
|
||||
detailUnlinkedPayOrders.value = []
|
||||
logisticsTracePayload.value = null
|
||||
detailLogisticsExpress.value = 'auto'
|
||||
logisticsTracePhoneTail.value = ''
|
||||
detailLogs.value = []
|
||||
|
||||
detailVisible.value = true
|
||||
detailLoading.value = true
|
||||
try {
|
||||
const res: any = await prescriptionOrderDetail({ id })
|
||||
const d = res?.data ?? res ?? null
|
||||
detailData.value = d
|
||||
if (d) {
|
||||
detailLogisticsExpress.value = String(d.express_company || 'auto') || 'auto'
|
||||
const dig = String(d.recipient_phone || '').replace(/\D/g, '')
|
||||
logisticsTracePhoneTail.value = dig.length >= 4 ? dig : ''
|
||||
if (String(d.tracking_number || '').trim()) fetchLogisticsTrace()
|
||||
fetchLogs(id)
|
||||
if (d.diagnosis_id) {
|
||||
loadDetailUnlinkedPayOrders(d.diagnosis_id, id, d.pay_order_ids || [])
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
feedback.msgError(e?.message || '加载详情失败')
|
||||
detailVisible.value = false
|
||||
} finally {
|
||||
detailLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadDetailUnlinkedPayOrders(diagnosisId: number, prescriptionOrderId: number, linkedIds: number[]) {
|
||||
if (!diagnosisId) { detailUnlinkedPayOrders.value = []; return }
|
||||
try {
|
||||
const res: any = await prescriptionOrderPaidPayOrders({ diagnosis_id: diagnosisId, prescription_order_id: prescriptionOrderId })
|
||||
const lists = res?.lists ?? res?.data?.lists ?? []
|
||||
detailUnlinkedPayOrders.value = Array.isArray(lists) ? lists.filter((item: any) => !linkedIds.includes(item.id)) : []
|
||||
} catch { detailUnlinkedPayOrders.value = [] }
|
||||
}
|
||||
|
||||
function canViewPrescriptionOrderLogs() {
|
||||
const p = userStore.perms || []
|
||||
return p.includes('*') || p.includes('tcm.prescriptionOrder/logs')
|
||||
}
|
||||
|
||||
async function fetchLogs(id: number) {
|
||||
if (!canViewPrescriptionOrderLogs()) { detailLogs.value = []; return }
|
||||
try {
|
||||
const res: any = await prescriptionOrderLogs({ id })
|
||||
detailLogs.value = res?.data ?? res ?? []
|
||||
} catch { detailLogs.value = [] }
|
||||
}
|
||||
|
||||
// ─── 工具函数 ───
|
||||
function normalizeBizPhone(v: unknown): string {
|
||||
if (v === null || v === undefined) return ''
|
||||
return String(v).replace(/\s/g, '').trim()
|
||||
function openDetail(id: number) {
|
||||
detailDrawerRef.value?.open(id)
|
||||
}
|
||||
|
||||
const formatAmount = (value: unknown) => {
|
||||
@@ -555,126 +114,6 @@ const formatAmount = (value: unknown) => {
|
||||
return Number.isFinite(n) ? n.toFixed(2) : '0.00'
|
||||
}
|
||||
|
||||
function formatMoney(v: unknown) {
|
||||
const n = Number(v)
|
||||
return Number.isFinite(n) ? n.toFixed(2) : '0.00'
|
||||
}
|
||||
|
||||
const formatTime = (value: unknown) => {
|
||||
if (value === null || value === undefined || value === '') return '—'
|
||||
if (typeof value === 'number' && value > 1e9 && value < 1e11) {
|
||||
const d = new Date(value * 1000)
|
||||
const pad = (n: number) => String(n).padStart(2, '0')
|
||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
|
||||
}
|
||||
return String(value)
|
||||
}
|
||||
|
||||
function formatOrderTime(v: unknown) {
|
||||
if (v === null || v === undefined || v === '') return '—'
|
||||
if (typeof v === 'string' && String(v).includes('-')) return String(v)
|
||||
return formatTime(v)
|
||||
}
|
||||
|
||||
function feeTypeText(t: number | undefined) {
|
||||
const m: Record<number, string> = { 1: '药费', 2: '诊费', 3: '药费+诊费', 4: '其他' }
|
||||
return m[Number(t)] ?? '—'
|
||||
}
|
||||
|
||||
// 服务套餐字典选项(与 order_list.vue 数据源一致:dict 类型 server_order)
|
||||
const servicePackageOptions = ref<Array<{ name: string; value: string }>>([])
|
||||
|
||||
async function loadServicePackageOptions() {
|
||||
try {
|
||||
const data: any = await getDictData({ type: 'server_order' })
|
||||
const list = (data?.server_order || []) as Array<{ name: string; value: string; status?: number }>
|
||||
servicePackageOptions.value = list.filter((item) => item.status !== 0)
|
||||
} catch {
|
||||
servicePackageOptions.value = []
|
||||
}
|
||||
}
|
||||
|
||||
function formatServicePackage(value: unknown): string {
|
||||
if (value === null || value === undefined || value === '') return '—'
|
||||
let packages: string[] = []
|
||||
if (Array.isArray(value)) {
|
||||
packages = value.map((v) => String(v)).filter((v) => v !== '')
|
||||
} else if (typeof value === 'string') {
|
||||
packages = value.split(',').map((v) => v.trim()).filter((v) => v !== '')
|
||||
}
|
||||
if (packages.length === 0) return '—'
|
||||
const names = packages.map((val) => {
|
||||
const opt = servicePackageOptions.value.find((o) => o.value === val)
|
||||
return opt ? opt.name : val
|
||||
})
|
||||
return names.join('、')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadServicePackageOptions()
|
||||
})
|
||||
|
||||
function expressCompanyLabel(v: unknown) {
|
||||
const s = String(v || '').toLowerCase()
|
||||
if (s === 'sf') return '顺丰速运'
|
||||
if (s === 'jd') return '京东快递'
|
||||
if (s === 'jt' || s === 'jtexpress') return '极兔速递'
|
||||
return '自动识别'
|
||||
}
|
||||
|
||||
function consumerRxAuditText(s: number | undefined) {
|
||||
const n = Number(s)
|
||||
if (n === 1) return '已通过'
|
||||
if (n === 2) return '已驳回'
|
||||
return '待审核'
|
||||
}
|
||||
|
||||
function consumerRxAuditTag(s: number | undefined): 'success' | 'warning' | 'danger' | 'info' {
|
||||
const n = Number(s)
|
||||
if (n === 1) return 'success'
|
||||
if (n === 2) return 'danger'
|
||||
return 'warning'
|
||||
}
|
||||
|
||||
function payOrderStatusTag(s: number | undefined): 'success' | 'warning' | 'danger' | 'info' {
|
||||
const n = Number(s)
|
||||
if (n === 2) return 'success'
|
||||
if (n === 1) return 'warning'
|
||||
return 'info'
|
||||
}
|
||||
|
||||
function orderStatusText(s: number | undefined) {
|
||||
const m: Record<number, string> = { 0: '待支付', 1: '待支付', 2: '已支付', 3: '已取消' }
|
||||
return m[Number(s)] ?? '—'
|
||||
}
|
||||
|
||||
const fulfillmentText = (s: number | undefined) => {
|
||||
const m: Record<number, string> = { 1: '待双审通过', 2: '待发货', 3: '已完成', 4: '已取消', 5: '已发货', 6: '已签收', 7: '进行中', 8: '暂不制药', 9: '拒收', 10: '退款', 11: '保留药方', 12: '制药缓发' }
|
||||
return m[Number(s)] ?? '—'
|
||||
}
|
||||
|
||||
const fulfillmentTagType = (s: number | undefined): 'success' | 'warning' | 'danger' | 'info' | 'primary' => {
|
||||
const n = Number(s)
|
||||
if (n === 3 || n === 6) return 'success'
|
||||
if (n === 5) return 'primary'
|
||||
if (n === 4 || n === 9 || n === 10) return 'danger'
|
||||
if (n === 2 || n === 7) return 'warning'
|
||||
if (n === 8 || n === 11 || n === 12) return 'info'
|
||||
return 'info'
|
||||
}
|
||||
|
||||
function logActionText(act: string) {
|
||||
const m: Record<string, string> = {
|
||||
create: '创建', edit: '编辑', audit_rx_approve: '处方审核', audit_rx_reject: '处方审核',
|
||||
audit_pay_approve: '支付审核', audit_pay_reject: '支付审核', fill_tracking: '填快递单',
|
||||
ship: '确认发货', withdraw: '撤销', link_pay_order: '关联支付单',
|
||||
revoke_rx_audit: '撤回处方审核', revoke_pay_audit: '撤回支付审核',
|
||||
gancao_submit: '甘草下单', patch_rx_patient: '处方患者信息',
|
||||
update_amount: '修改订单金额', complete: '完成订单'
|
||||
}
|
||||
return m[act] || act
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [props.diagnosisId, patientIdNum.value] as const,
|
||||
() => {
|
||||
@@ -695,22 +134,4 @@ defineExpose({ refresh: () => getLists() })
|
||||
.po-empty-tip {
|
||||
padding: 24px 0;
|
||||
}
|
||||
.audit-stamp {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
right: 16px;
|
||||
transform: rotate(15deg);
|
||||
opacity: 0.15;
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
.stamp-inner {
|
||||
border: 3px solid currentColor;
|
||||
border-radius: 8px;
|
||||
padding: 4px 12px;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.stamp-pass { color: #22c55e; }
|
||||
.stamp-reject { color: #ef4444; }
|
||||
</style>
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
formData.pending_assign !== '1' &&
|
||||
formData.pending_booking !== '1' &&
|
||||
formData.completed_appointment !== '1' &&
|
||||
!hasLatestAppointmentFilter() &&
|
||||
formData.appointment_date === tab.value
|
||||
}
|
||||
]"
|
||||
@@ -110,6 +111,32 @@
|
||||
<el-select v-model="formData.assistant_id" placeholder="医助" clearable filterable size="small" @change="doSearch">
|
||||
<el-option v-for="item in assistantOptions" :key="item.id" :label="item.name" :value="Number(item.id)" />
|
||||
</el-select>
|
||||
<daterange-picker
|
||||
class="latest-appointment-range"
|
||||
v-model:startTime="formData.latest_appointment_start_date"
|
||||
v-model:endTime="formData.latest_appointment_end_date"
|
||||
picker-type="daterange"
|
||||
value-format="YYYY-MM-DD"
|
||||
start-placeholder="最近挂号开始"
|
||||
end-placeholder="最近挂号结束"
|
||||
@change="handleLatestAppointmentFilterChange"
|
||||
/>
|
||||
<el-select
|
||||
v-model="formData.latest_appointment_channel_source"
|
||||
placeholder="最近挂号渠道"
|
||||
clearable
|
||||
filterable
|
||||
size="small"
|
||||
class="latest-appointment-channel"
|
||||
@change="handleLatestAppointmentFilterChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in channelOptions"
|
||||
:key="String(item.value)"
|
||||
:label="item.name"
|
||||
:value="String(item.value)"
|
||||
/>
|
||||
</el-select>
|
||||
<el-button size="small" @click="handleReset">重置</el-button>
|
||||
</div>
|
||||
</el-card>
|
||||
@@ -213,6 +240,9 @@
|
||||
<div class="apt-doctor">{{ row.appointment_doctor_name || '-' }}</div>
|
||||
<div class="apt-time">{{ row.appointment_time_text || '-' }}</div>
|
||||
</template>
|
||||
<div v-if="latestAppointmentChannelText(row)" class="apt-latest-channel">
|
||||
最近渠道:{{ latestAppointmentChannelText(row) }}
|
||||
</div>
|
||||
</template>
|
||||
<span v-else class="apt-none">未挂号</span>
|
||||
</div>
|
||||
@@ -689,6 +719,7 @@ import useUserStore from '@/stores/modules/user'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { computed, defineAsyncComponent, onMounted, onUnmounted, watch } from 'vue'
|
||||
import dayjs from 'dayjs'
|
||||
import DaterangePicker from '@/components/daterange-picker/index.vue'
|
||||
|
||||
const EditPopup = defineAsyncComponent(() => import('./edit.vue'))
|
||||
const DetailPopup = defineAsyncComponent(() => import('./detail.vue'))
|
||||
@@ -728,6 +759,9 @@ const formData = reactive({
|
||||
assistant_id: '',
|
||||
start_time: '',
|
||||
end_time: '',
|
||||
latest_appointment_start_date: '' as string,
|
||||
latest_appointment_end_date: '' as string,
|
||||
latest_appointment_channel_source: '' as string,
|
||||
diagnosis_confirmed: '' as '' | '0' | '1',
|
||||
appointment_date: '' as string,
|
||||
has_appointment: '' as '' | '0' | '1',
|
||||
@@ -760,6 +794,9 @@ const setHasAppointment = (v: string) => {
|
||||
formData.has_appointment = v as '' | '0' | '1'
|
||||
formData.pending_booking = ''
|
||||
formData.completed_appointment = ''
|
||||
if (v === '0') {
|
||||
clearLatestAppointmentFilters()
|
||||
}
|
||||
pager.page = 1
|
||||
getLists()
|
||||
fetchDateCounts()
|
||||
@@ -823,6 +860,9 @@ function buildPendingAssignCountPayload(): Record<string, unknown> {
|
||||
has_appointment: '',
|
||||
pending_booking: '',
|
||||
completed_appointment: '',
|
||||
latest_appointment_start_date: '',
|
||||
latest_appointment_end_date: '',
|
||||
latest_appointment_channel_source: '',
|
||||
pending_assign_order_month: resolvePendingAssignOrderMonthForRequest()
|
||||
} as Record<string, unknown>) as Record<string, unknown>
|
||||
}
|
||||
@@ -850,6 +890,9 @@ function clearSecondaryFiltersWhenPendingAssignWideSearch() {
|
||||
formData.assistant_id = ''
|
||||
formData.start_time = ''
|
||||
formData.end_time = ''
|
||||
formData.latest_appointment_start_date = ''
|
||||
formData.latest_appointment_end_date = ''
|
||||
formData.latest_appointment_channel_source = ''
|
||||
formData.pending_assign_order_month = ''
|
||||
activeTab.value = 'all'
|
||||
if (kw1 !== '') {
|
||||
@@ -886,6 +929,7 @@ const handleDateTabClick = async (value: string) => {
|
||||
formData.pending_booking = ''
|
||||
formData.completed_appointment = ''
|
||||
formData.has_appointment = ''
|
||||
clearLatestAppointmentFilters()
|
||||
formData.appointment_date = value
|
||||
pager.page = 1
|
||||
await getLists()
|
||||
@@ -901,6 +945,7 @@ const handlePendingBookingTabClick = async () => {
|
||||
formData.completed_appointment = ''
|
||||
formData.appointment_date = ''
|
||||
formData.has_appointment = '0'
|
||||
clearLatestAppointmentFilters()
|
||||
pager.page = 1
|
||||
await getLists()
|
||||
fetchDateCounts()
|
||||
@@ -915,6 +960,7 @@ const handleCompletedVisitTabClick = async () => {
|
||||
formData.completed_appointment = '1'
|
||||
formData.has_appointment = ''
|
||||
formData.appointment_date = ''
|
||||
clearLatestAppointmentFilters()
|
||||
pager.page = 1
|
||||
await getLists()
|
||||
fetchDateCounts()
|
||||
@@ -926,6 +972,7 @@ const handlePendingAssignTabClick = async () => {
|
||||
formData.completed_appointment = ''
|
||||
formData.has_appointment = ''
|
||||
formData.appointment_date = ''
|
||||
clearLatestAppointmentFilters()
|
||||
if (!formData.pending_assign_order_month) {
|
||||
formData.pending_assign_order_month = dayjs().format('YYYY-MM')
|
||||
}
|
||||
@@ -1047,16 +1094,29 @@ const handleMoreCommand = (cmd: string) => {
|
||||
const diagnosisTypeOptions = ref<any[]>([])
|
||||
const syndromeTypeOptions = ref<any[]>([])
|
||||
const assistantOptions = ref<any[]>([])
|
||||
const channelOptions = ref<Array<{ name: string; value: string }>>([])
|
||||
|
||||
const getDictOptions = async () => {
|
||||
try {
|
||||
const [diagnosisType, syndromeType, assistants] = await Promise.all([
|
||||
const [diagnosisType, syndromeType, channels, assistants] = await Promise.all([
|
||||
getDictData({ type: 'diagnosis_type' }),
|
||||
getDictData({ type: 'syndrome_type' }),
|
||||
getDictData({ type: 'channels' }),
|
||||
getAssistants()
|
||||
])
|
||||
diagnosisTypeOptions.value = diagnosisType?.diagnosis_type || []
|
||||
syndromeTypeOptions.value = syndromeType?.syndrome_type || []
|
||||
channelOptions.value = (channels?.channels || [])
|
||||
.filter((row: any) => row.status !== 0)
|
||||
.sort((a: any, b: any) => {
|
||||
const ds = Number(b?.sort ?? 0) - Number(a?.sort ?? 0)
|
||||
if (ds !== 0) return ds
|
||||
return Number(b?.id ?? 0) - Number(a?.id ?? 0)
|
||||
})
|
||||
.map((row: any) => ({
|
||||
name: String(row?.name ?? row?.value ?? ''),
|
||||
value: row?.value != null && row.value !== '' ? String(row.value) : ''
|
||||
}))
|
||||
assistantOptions.value = assistants || []
|
||||
} catch (error) {
|
||||
console.error('获取字典数据失败:', error)
|
||||
@@ -1069,6 +1129,40 @@ const getDictLabel = (options: any[], value: string) => {
|
||||
return item ? item.name : value || '-'
|
||||
}
|
||||
|
||||
const clearLatestAppointmentFilters = () => {
|
||||
formData.latest_appointment_start_date = ''
|
||||
formData.latest_appointment_end_date = ''
|
||||
formData.latest_appointment_channel_source = ''
|
||||
}
|
||||
|
||||
const hasLatestAppointmentFilter = () =>
|
||||
!!(
|
||||
formData.latest_appointment_start_date ||
|
||||
formData.latest_appointment_end_date ||
|
||||
formData.latest_appointment_channel_source
|
||||
)
|
||||
|
||||
const handleLatestAppointmentFilterChange = () => {
|
||||
if (hasLatestAppointmentFilter()) {
|
||||
formData.appointment_date = ''
|
||||
formData.pending_booking = ''
|
||||
formData.completed_appointment = ''
|
||||
if (formData.has_appointment === '0') {
|
||||
formData.has_appointment = ''
|
||||
}
|
||||
}
|
||||
doSearch()
|
||||
}
|
||||
|
||||
const latestAppointmentChannelText = (row: any) => {
|
||||
const desc = String(row?.latest_appointment_channel_source_desc || '').trim()
|
||||
const raw = String(row?.latest_appointment_channel_source || '').trim()
|
||||
const detail = String(row?.latest_appointment_channel_source_detail || '').trim()
|
||||
const base = desc || raw
|
||||
if (!base) return ''
|
||||
return detail ? `${base}(${detail})` : base
|
||||
}
|
||||
|
||||
// 获取医助名称
|
||||
const getAssistantName = (assistantId: number | string) => {
|
||||
// 如果没有assistant_id,返回"-"
|
||||
@@ -1103,6 +1197,9 @@ const handleReset = () => {
|
||||
formData.diagnosis_type = ''
|
||||
formData.syndrome_type = ''
|
||||
formData.assistant_id = ''
|
||||
formData.latest_appointment_start_date = ''
|
||||
formData.latest_appointment_end_date = ''
|
||||
formData.latest_appointment_channel_source = ''
|
||||
formData.diagnosis_confirmed = ''
|
||||
formData.appointment_date = ''
|
||||
formData.has_appointment = ''
|
||||
@@ -2190,10 +2287,20 @@ onUnmounted(() => {
|
||||
.filter-more {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
margin-top: 8px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px dashed var(--el-border-color-lighter);
|
||||
|
||||
.latest-appointment-range {
|
||||
width: 260px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.latest-appointment-channel {
|
||||
width: 170px;
|
||||
}
|
||||
}
|
||||
|
||||
.list-card {
|
||||
@@ -2363,6 +2470,13 @@ onUnmounted(() => {
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-placeholder);
|
||||
}
|
||||
|
||||
.apt-latest-channel {
|
||||
margin-top: 6px;
|
||||
font-size: 12px;
|
||||
line-height: 1.35;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
.status-confirmed {
|
||||
|
||||
@@ -77,7 +77,14 @@ module.exports = {
|
||||
mask: 'var(--el-mask-color)'
|
||||
},
|
||||
fontFamily: {
|
||||
sans: ['PingFang SC', 'Arial', 'Hiragino Sans GB', 'Microsoft YaHei', 'sans-serif']
|
||||
sans: [
|
||||
'Noto Sans SC',
|
||||
'PingFang SC',
|
||||
'Microsoft YaHei',
|
||||
'Hiragino Sans GB',
|
||||
'Arial',
|
||||
'sans-serif'
|
||||
]
|
||||
},
|
||||
boxShadow: {
|
||||
DEFAULT: 'var(--el-box-shadow)',
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -55,34 +55,59 @@ class AssetResourceController extends BaseAdminController
|
||||
public function add()
|
||||
{
|
||||
$params = $this->request->post();
|
||||
if (empty($params['type']) || empty($params['title']) || empty($params['file_url'])) {
|
||||
$type = $params['type'] ?? 0;
|
||||
$title = trim((string)($params['title'] ?? ''));
|
||||
|
||||
// 兼容单图(字符串)与多图批量上传(数组)
|
||||
$fileUrl = $params['file_url'] ?? '';
|
||||
if (is_array($fileUrl)) {
|
||||
$fileUrls = array_values(array_filter($fileUrl, fn($v) => trim((string)$v) !== ''));
|
||||
} else {
|
||||
$fileUrls = trim((string)$fileUrl) !== '' ? [$fileUrl] : [];
|
||||
}
|
||||
|
||||
if (empty($type) || $title === '' || empty($fileUrls)) {
|
||||
return $this->fail('请填写完整的资源信息');
|
||||
}
|
||||
|
||||
|
||||
// 仅图片支持批量;视频/语音只取首个
|
||||
if ($type != 1) {
|
||||
$fileUrls = [$fileUrls[0]];
|
||||
}
|
||||
|
||||
$multi = count($fileUrls) > 1;
|
||||
|
||||
Db::startTrans();
|
||||
try {
|
||||
$resource = AssetResource::create([
|
||||
'type' => $params['type'],
|
||||
'title' => $params['title'],
|
||||
'file_url' => $params['file_url'],
|
||||
'cover_url' => $params['cover_url'] ?? '',
|
||||
]);
|
||||
|
||||
// 绑定用户
|
||||
if (!empty($params['user_ids']) && is_array($params['user_ids'])) {
|
||||
$userResources = [];
|
||||
foreach ($params['user_ids'] as $userId) {
|
||||
$userResources[] = [
|
||||
'user_id' => $userId,
|
||||
'resource_id' => $resource->id,
|
||||
'create_time' => time(),
|
||||
];
|
||||
$createdIds = [];
|
||||
$index = 0;
|
||||
foreach ($fileUrls as $url) {
|
||||
$index++;
|
||||
$resource = AssetResource::create([
|
||||
'type' => $type,
|
||||
'title' => $multi ? $title . '_' . $index : $title,
|
||||
'file_url' => $url,
|
||||
'cover_url' => $params['cover_url'] ?? '',
|
||||
]);
|
||||
|
||||
// 绑定用户
|
||||
if (!empty($params['user_ids']) && is_array($params['user_ids'])) {
|
||||
$userResources = [];
|
||||
foreach ($params['user_ids'] as $userId) {
|
||||
$userResources[] = [
|
||||
'user_id' => $userId,
|
||||
'resource_id' => $resource->id,
|
||||
'create_time' => time(),
|
||||
];
|
||||
}
|
||||
(new AssetUserResource())->saveAll($userResources);
|
||||
}
|
||||
(new AssetUserResource())->saveAll($userResources);
|
||||
|
||||
$createdIds[] = $resource->id;
|
||||
}
|
||||
|
||||
|
||||
Db::commit();
|
||||
return $this->success('添加并分配成功', ['id' => $resource->id]);
|
||||
return $this->success('添加并分配成功', ['ids' => $createdIds]);
|
||||
} catch (\Exception $e) {
|
||||
Db::rollback();
|
||||
return $this->fail('操作失败: ' . $e->getMessage());
|
||||
|
||||
@@ -207,6 +207,17 @@ class OrderController extends BaseAdminController
|
||||
$params['creator_id'] = $this->adminId;
|
||||
$params['payment_channel'] = (string)$this->request->post('payment_channel', 'normal');
|
||||
$params['require_payment_slip_audit'] = (int)$this->request->post('require_payment_slip_audit', 0);
|
||||
// 创建方式:优先取前端透传的 create_type,否则按 payment_channel 派生(fubei→fubei,express_cod→express_cod,其余 normal)
|
||||
$createTypeReq = (string)$this->request->post('create_type', '');
|
||||
if (in_array($createTypeReq, ['normal', 'wechat_work', 'fubei', 'express_cod'], true)) {
|
||||
$params['create_type'] = $createTypeReq;
|
||||
} elseif ($params['payment_channel'] === 'fubei') {
|
||||
$params['create_type'] = 'fubei';
|
||||
} elseif ($params['payment_channel'] === 'express_cod') {
|
||||
$params['create_type'] = 'express_cod';
|
||||
} else {
|
||||
$params['create_type'] = 'normal';
|
||||
}
|
||||
|
||||
$result = OrderLogic::create($params);
|
||||
if (!$result) {
|
||||
@@ -227,6 +238,7 @@ class OrderController extends BaseAdminController
|
||||
{
|
||||
$params = (new OrderValidate())->post()->goCheck('create');
|
||||
$params['creator_id'] = $this->adminId;
|
||||
$params['create_type'] = 'wechat_work';
|
||||
|
||||
$result = OrderLogic::createForWechatWork($params);
|
||||
if (!$result) {
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\adminapi\controller\stats;
|
||||
|
||||
use app\adminapi\controller\BaseAdminController;
|
||||
use app\adminapi\logic\stats\RevisitRateLogic;
|
||||
|
||||
/**
|
||||
* 复诊接诊率统计(按月)
|
||||
*
|
||||
* - GET stats.revisitRate/overview 当月 N 诊接诊率(部门 → 医助分组 + 合计)
|
||||
* - GET stats.revisitRate/deptOptions 部门下拉(前端组树)
|
||||
* - GET stats.revisitRate/assignLines 「被指派数」明细(按诊单聚合)
|
||||
* - GET stats.revisitRate/visitOrderLines 「N 诊单数」订单明细
|
||||
*/
|
||||
class RevisitRateController extends BaseAdminController
|
||||
{
|
||||
public function overview()
|
||||
{
|
||||
// 涉及指派日志 + 全量订单序列扫描,放宽执行时间兜底
|
||||
@set_time_limit(120);
|
||||
$params = $this->request->get();
|
||||
|
||||
return $this->data(RevisitRateLogic::overview($params));
|
||||
}
|
||||
|
||||
public function deptOptions()
|
||||
{
|
||||
return $this->data(RevisitRateLogic::deptOptions());
|
||||
}
|
||||
|
||||
/** 「被指派数」点击下钻:诊单维度明细 */
|
||||
public function assignLines()
|
||||
{
|
||||
@set_time_limit(120);
|
||||
$params = $this->request->get();
|
||||
|
||||
return $this->data(RevisitRateLogic::assignLines($params));
|
||||
}
|
||||
|
||||
/** 「N 诊单数」点击下钻:具体订单明细 */
|
||||
public function visitOrderLines()
|
||||
{
|
||||
@set_time_limit(120);
|
||||
$params = $this->request->get();
|
||||
|
||||
return $this->data(RevisitRateLogic::visitOrderLines($params));
|
||||
}
|
||||
}
|
||||
@@ -28,8 +28,10 @@ use app\common\model\tcm\CallRecord;
|
||||
use app\common\model\auth\Admin;
|
||||
use app\common\model\auth\AdminDept;
|
||||
use app\common\model\auth\AdminRole;
|
||||
use app\common\model\dict\DictData;
|
||||
use app\common\lists\ListsSearchInterface;
|
||||
use app\common\lists\Traits\HasDataScopeFilter;
|
||||
use think\facade\Db;
|
||||
|
||||
/**
|
||||
* 中医辨房病因诊单列表
|
||||
@@ -39,6 +41,10 @@ use app\common\lists\Traits\HasDataScopeFilter;
|
||||
class DiagnosisLists extends BaseAdminDataLists implements ListsSearchInterface
|
||||
{
|
||||
use HasDataScopeFilter;
|
||||
|
||||
/** 最近挂号筛选只统计这些有效状态:已预约、已完成、已过号 */
|
||||
private const EFFECTIVE_APPOINTMENT_STATUSES = [1, 3, 4];
|
||||
|
||||
/**
|
||||
* @notes 设置搜索条件
|
||||
* @return array
|
||||
@@ -133,6 +139,8 @@ class DiagnosisLists extends BaseAdminDataLists implements ListsSearchInterface
|
||||
$query->whereExists("SELECT 1 FROM {$aptTbl} apt WHERE apt.patient_id = {$diagTbl}.id AND apt.status = 3");
|
||||
}
|
||||
|
||||
$this->applyLatestAppointmentFilters($query, $pendingWideSearch);
|
||||
|
||||
$this->applyPendingAssignBusinessOrderMonthFilter($query);
|
||||
|
||||
// 仅已开方(待分配+关键词检索时不限制)
|
||||
@@ -210,6 +218,7 @@ class DiagnosisLists extends BaseAdminDataLists implements ListsSearchInterface
|
||||
->order('appointment_time', 'asc')
|
||||
->order('id', 'asc');
|
||||
$appointments = $subQuery->select()->toArray();
|
||||
$latestAppointmentMap = $this->buildLatestAppointmentMap($diagnosisIds);
|
||||
|
||||
foreach ($appointments as $apt) {
|
||||
$did = (int) ($apt['patient_id'] ?? 0);
|
||||
@@ -240,6 +249,8 @@ class DiagnosisLists extends BaseAdminDataLists implements ListsSearchInterface
|
||||
}
|
||||
// 合并到诊单列表
|
||||
foreach ($lists as &$item) {
|
||||
$latestAppointment = $latestAppointmentMap[(int) $item['id']] ?? null;
|
||||
$this->appendLatestAppointmentSummary($item, $latestAppointment);
|
||||
$aptList = $appointmentMap[(int) $item['id']] ?? [];
|
||||
if (!empty($aptList)) {
|
||||
$apt = $aptList[0];
|
||||
@@ -280,6 +291,7 @@ class DiagnosisLists extends BaseAdminDataLists implements ListsSearchInterface
|
||||
}
|
||||
} else {
|
||||
foreach ($lists as &$item) {
|
||||
$this->appendLatestAppointmentSummary($item, null);
|
||||
$item['has_appointment'] = 0;
|
||||
$item['appointment_id'] = null;
|
||||
$item['appointment_status'] = 0;
|
||||
@@ -558,6 +570,8 @@ class DiagnosisLists extends BaseAdminDataLists implements ListsSearchInterface
|
||||
$query->whereExists("SELECT 1 FROM {$aptTbl} apt WHERE apt.patient_id = {$diagTbl}.id AND apt.status = 3");
|
||||
}
|
||||
|
||||
$this->applyLatestAppointmentFilters($query, $pendingWideSearch);
|
||||
|
||||
// 仅已开方(待分配+关键词检索时不限制)
|
||||
if (!$pendingWideSearch && isset($this->params['only_has_prescription']) && (string) $this->params['only_has_prescription'] === '1') {
|
||||
$rxTbl = (new Prescription())->getTable();
|
||||
@@ -587,6 +601,204 @@ class DiagnosisLists extends BaseAdminDataLists implements ListsSearchInterface
|
||||
return $query->count();
|
||||
}
|
||||
|
||||
/**
|
||||
* 最近一次有效挂号过滤:按 appointment_date DESC, appointment_time DESC, id DESC 取一条。
|
||||
*
|
||||
* @param mixed $query
|
||||
*/
|
||||
private function applyLatestAppointmentFilters($query, bool $pendingWideSearch): void
|
||||
{
|
||||
if ($pendingWideSearch) {
|
||||
return;
|
||||
}
|
||||
|
||||
$startDate = $this->normalizeYmd($this->params['latest_appointment_start_date'] ?? '');
|
||||
$endDate = $this->normalizeYmd($this->params['latest_appointment_end_date'] ?? '');
|
||||
$channelSource = trim((string) ($this->params['latest_appointment_channel_source'] ?? ''));
|
||||
|
||||
if ($startDate === '' && $endDate === '' && $channelSource === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
$aptTbl = (new Appointment())->getTable();
|
||||
$diagTbl = (new Diagnosis())->getTable();
|
||||
$latestIdSql = $this->latestAppointmentIdSubSql($aptTbl, $diagTbl);
|
||||
$conditions = ["latest_apt.id = ({$latestIdSql})"];
|
||||
|
||||
if ($startDate !== '') {
|
||||
$conditions[] = "latest_apt.appointment_date >= '" . addslashes($startDate) . "'";
|
||||
}
|
||||
if ($endDate !== '') {
|
||||
$conditions[] = "latest_apt.appointment_date <= '" . addslashes($endDate) . "'";
|
||||
}
|
||||
if ($channelSource !== '') {
|
||||
$channelCond = $this->appointmentChannelConditionSql('latest_apt', $channelSource);
|
||||
if ($channelCond === '') {
|
||||
$query->whereRaw('0 = 1');
|
||||
|
||||
return;
|
||||
}
|
||||
$conditions[] = $channelCond;
|
||||
}
|
||||
|
||||
$query->whereExists("SELECT 1 FROM {$aptTbl} latest_apt WHERE " . implode(' AND ', $conditions));
|
||||
}
|
||||
|
||||
private function latestAppointmentIdSubSql(string $aptTbl, string $diagTbl): string
|
||||
{
|
||||
$statuses = implode(',', self::EFFECTIVE_APPOINTMENT_STATUSES);
|
||||
|
||||
return "SELECT apt_latest.id FROM {$aptTbl} apt_latest "
|
||||
. "WHERE apt_latest.patient_id = {$diagTbl}.id "
|
||||
. "AND apt_latest.status IN ({$statuses}) "
|
||||
. "ORDER BY apt_latest.appointment_date DESC, "
|
||||
. "IFNULL(NULLIF(TRIM(apt_latest.appointment_time), ''), '00:00:00') DESC, "
|
||||
. "apt_latest.id DESC LIMIT 1";
|
||||
}
|
||||
|
||||
private function appointmentChannelConditionSql(string $alias, string $channelSource): string
|
||||
{
|
||||
$cols = $this->appointmentTableFields();
|
||||
$hasChannelSource = in_array('channel_source', $cols, true);
|
||||
$hasChannels = in_array('channels', $cols, true);
|
||||
if (!$hasChannelSource && !$hasChannels) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$quoted = "'" . addslashes($channelSource) . "'";
|
||||
$parts = [];
|
||||
if ($hasChannelSource) {
|
||||
$parts[] = "{$alias}.channel_source = {$quoted}";
|
||||
}
|
||||
if ($hasChannels) {
|
||||
$parts[] = "{$alias}.channels = {$quoted}";
|
||||
if (is_numeric($channelSource)) {
|
||||
$parts[] = "{$alias}.channels = " . (int) $channelSource;
|
||||
}
|
||||
}
|
||||
|
||||
$parts = array_values(array_unique($parts));
|
||||
|
||||
return $parts === [] ? '' : '(' . implode(' OR ', $parts) . ')';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int|string, mixed> $diagnosisIds
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
private function buildLatestAppointmentMap(array $diagnosisIds): array
|
||||
{
|
||||
$diagnosisIds = array_values(array_filter(array_unique(array_map('intval', $diagnosisIds)), static function (int $id): bool {
|
||||
return $id > 0;
|
||||
}));
|
||||
if ($diagnosisIds === []) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$cols = $this->appointmentTableFields();
|
||||
$fields = ['id', 'patient_id', 'appointment_date', 'appointment_time', 'status'];
|
||||
foreach (['channel_source', 'channel_source_detail', 'channels'] as $col) {
|
||||
if (in_array($col, $cols, true)) {
|
||||
$fields[] = $col;
|
||||
}
|
||||
}
|
||||
|
||||
$appointments = Appointment::whereIn('patient_id', $diagnosisIds)
|
||||
->whereIn('status', self::EFFECTIVE_APPOINTMENT_STATUSES)
|
||||
->field($fields)
|
||||
->order('patient_id', 'asc')
|
||||
->order('appointment_date', 'desc')
|
||||
->orderRaw("IFNULL(NULLIF(TRIM(appointment_time), ''), '00:00:00') DESC")
|
||||
->order('id', 'desc')
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
$map = [];
|
||||
$channelNameByValue = DictData::where('type_value', 'channels')->column('name', 'value');
|
||||
foreach ($appointments as $appointment) {
|
||||
$diagnosisId = (int) ($appointment['patient_id'] ?? 0);
|
||||
if ($diagnosisId <= 0 || isset($map[$diagnosisId])) {
|
||||
continue;
|
||||
}
|
||||
$appointment['channel_source_desc'] = $this->appointmentChannelDesc($appointment, $channelNameByValue ?: []);
|
||||
$map[$diagnosisId] = $appointment;
|
||||
}
|
||||
|
||||
return $map;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $item
|
||||
* @param array<string, mixed>|null $appointment
|
||||
*/
|
||||
private function appendLatestAppointmentSummary(array &$item, ?array $appointment): void
|
||||
{
|
||||
$item['latest_appointment_id'] = null;
|
||||
$item['latest_appointment_time_text'] = '';
|
||||
$item['latest_appointment_channel_source'] = '';
|
||||
$item['latest_appointment_channel_source_desc'] = '';
|
||||
$item['latest_appointment_channel_source_detail'] = '';
|
||||
|
||||
if ($appointment === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$timePart = (string) ($appointment['appointment_time'] ?? '');
|
||||
if (strlen($timePart) > 5) {
|
||||
$timePart = substr($timePart, 0, 5);
|
||||
}
|
||||
$rawChannel = trim((string) ($appointment['channel_source'] ?? ''));
|
||||
if ($rawChannel === '' && isset($appointment['channels']) && $appointment['channels'] !== '' && $appointment['channels'] !== null) {
|
||||
$rawChannel = trim((string) $appointment['channels']);
|
||||
}
|
||||
|
||||
$item['latest_appointment_id'] = (int) ($appointment['id'] ?? 0);
|
||||
$item['latest_appointment_time_text'] = trim((string) ($appointment['appointment_date'] ?? '') . ' ' . $timePart);
|
||||
$item['latest_appointment_channel_source'] = $rawChannel;
|
||||
$item['latest_appointment_channel_source_desc'] = (string) ($appointment['channel_source_desc'] ?? '');
|
||||
$item['latest_appointment_channel_source_detail'] = trim((string) ($appointment['channel_source_detail'] ?? ''));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $appointment
|
||||
* @param array<string, string> $channelNameByValue
|
||||
*/
|
||||
private function appointmentChannelDesc(array $appointment, array $channelNameByValue): string
|
||||
{
|
||||
$srcKey = trim((string) ($appointment['channel_source'] ?? ''));
|
||||
if ($srcKey === '' && isset($appointment['channels']) && $appointment['channels'] !== '' && $appointment['channels'] !== null) {
|
||||
$srcKey = trim((string) $appointment['channels']);
|
||||
}
|
||||
if ($srcKey === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
return (string) ($channelNameByValue[$srcKey] ?? $channelNameByValue[(string) (int) $srcKey] ?? $srcKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
private function appointmentTableFields(): array
|
||||
{
|
||||
static $fields = null;
|
||||
if ($fields !== null) {
|
||||
return $fields;
|
||||
}
|
||||
|
||||
$tblFields = Db::name('doctor_appointment')->getTableFields();
|
||||
$fields = is_array($tblFields) ? array_values(array_map('strval', $tblFields)) : [];
|
||||
|
||||
return $fields;
|
||||
}
|
||||
|
||||
private function normalizeYmd($value): string
|
||||
{
|
||||
$date = trim((string) $value);
|
||||
|
||||
return preg_match('/^\d{4}-\d{2}-\d{2}$/', $date) ? $date : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 待分配 Tab 且有关键词时:放宽列表条件(与 `applyDiagnosisListKeywordFilter` 判定一致)。
|
||||
*/
|
||||
|
||||
@@ -22,6 +22,11 @@ use think\facade\Log;
|
||||
*/
|
||||
class OrderLogic
|
||||
{
|
||||
/**
|
||||
* 创建方式:normal 普通订单 / wechat_work 企业微信对外收款 / fubei 付呗 / express_cod 快递代收
|
||||
*/
|
||||
private const CREATE_TYPES = ['normal', 'wechat_work', 'fubei', 'express_cod'];
|
||||
|
||||
/**
|
||||
* @notes 生成订单号
|
||||
* @return string
|
||||
@@ -31,6 +36,43 @@ class OrderLogic
|
||||
return 'ORD' . date('YmdHis') . mt_rand(100000, 999999);
|
||||
}
|
||||
|
||||
private static function normalizeCreateType(
|
||||
?string $createType,
|
||||
?string $paymentMethod = null,
|
||||
?string $payeeUserid = null,
|
||||
?string $payerExternalUserid = null,
|
||||
?string $remark = null
|
||||
): string {
|
||||
$raw = trim((string) $createType);
|
||||
if (in_array($raw, self::CREATE_TYPES, true)) {
|
||||
return $raw;
|
||||
}
|
||||
|
||||
$paymentMethod = trim((string) $paymentMethod);
|
||||
if ($paymentMethod === 'fubei') {
|
||||
return 'fubei';
|
||||
}
|
||||
|
||||
if (
|
||||
trim((string) $payeeUserid) !== ''
|
||||
|| trim((string) $payerExternalUserid) !== ''
|
||||
|| str_contains((string) $remark, '企业微信')
|
||||
) {
|
||||
return 'wechat_work';
|
||||
}
|
||||
|
||||
return 'normal';
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否为「待审核(5)且需人工确认到账」的单:付呗(payment_method=fubei) 或 快递代收(create_type=express_cod)
|
||||
*/
|
||||
private static function isPendingManualAudit(Order $order): bool
|
||||
{
|
||||
return (string)($order->payment_method ?? '') === 'fubei'
|
||||
|| (string)($order->create_type ?? '') === 'express_cod';
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 创建订单
|
||||
* @param array $params
|
||||
@@ -40,9 +82,16 @@ class OrderLogic
|
||||
{
|
||||
try {
|
||||
$channel = (string)($params['payment_channel'] ?? 'normal');
|
||||
if (!in_array($channel, ['normal', 'fubei'], true)) {
|
||||
if (!in_array($channel, ['normal', 'fubei', 'express_cod'], true)) {
|
||||
$channel = 'normal';
|
||||
}
|
||||
$paymentMethod = $channel === 'fubei' ? 'fubei' : null;
|
||||
// 快递代收:以 express_cod 标记创建方式(与 fubei 流转一致,但 payment_method 待到账时再写)
|
||||
$createTypeHint = $channel === 'express_cod' ? 'express_cod' : '';
|
||||
$createType = self::normalizeCreateType(
|
||||
(string)($params['create_type'] ?? $createTypeHint),
|
||||
$paymentMethod
|
||||
);
|
||||
$requirePaymentSlipAudit = (int)($params['require_payment_slip_audit'] ?? 0) === 1;
|
||||
|
||||
$order = new Order();
|
||||
@@ -51,10 +100,14 @@ class OrderLogic
|
||||
$order->creator_id = $params['creator_id'];
|
||||
$order->order_type = $params['order_type'];
|
||||
$order->amount = $params['amount'];
|
||||
// 付呗且申请审核支付单:待审核(5);否则待支付(1)
|
||||
$order->create_type = $createType;
|
||||
// 付呗/快递代收且申请审核支付单:待审核(5);否则待支付(1)
|
||||
if ($channel === 'fubei') {
|
||||
$order->payment_method = 'fubei';
|
||||
$order->status = $requirePaymentSlipAudit ? 5 : 1;
|
||||
} elseif ($channel === 'express_cod') {
|
||||
// payment_method 留空,待人工确认到账时再写真实支付方式
|
||||
$order->status = $requirePaymentSlipAudit ? 5 : 1;
|
||||
} else {
|
||||
$order->status = 1; // 待支付
|
||||
}
|
||||
@@ -183,6 +236,10 @@ class OrderLogic
|
||||
$order->payment_method = 'wechat';
|
||||
$needSave = true;
|
||||
}
|
||||
if ((string) ($order->create_type ?? '') !== 'wechat_work') {
|
||||
$order->create_type = 'wechat_work';
|
||||
$needSave = true;
|
||||
}
|
||||
if ($paymentTimeStr !== '' && (string)$order->payment_time !== $paymentTimeStr) {
|
||||
$order->payment_time = $paymentTimeStr;
|
||||
$needSave = true;
|
||||
@@ -235,6 +292,7 @@ class OrderLogic
|
||||
$order->amount = $amount;
|
||||
$order->status = $targetStatus;
|
||||
$order->payment_method = 'wechat';
|
||||
$order->create_type = 'wechat_work';
|
||||
$order->payment_time = $paymentTimeStr;
|
||||
$order->trade_no = $tradeNo;
|
||||
$remark = '企业微信直接收款同步,待关联患者';
|
||||
@@ -283,6 +341,7 @@ class OrderLogic
|
||||
$order->amount = $amount;
|
||||
$order->status = 2; // 已支付
|
||||
$order->payment_method = 'wechat';
|
||||
$order->create_type = 'wechat_work';
|
||||
$order->payment_time = date('Y-m-d H:i:s');
|
||||
$order->trade_no = $tradeNo;
|
||||
$order->remark = '企业微信直接收款,待关联患者';
|
||||
@@ -345,11 +404,11 @@ class OrderLogic
|
||||
}
|
||||
|
||||
$st = (int)$order->status;
|
||||
// 1=待支付;5=待审核(付呗+申请支付单审核) 时允许录入手动到账
|
||||
// 1=待支付;5=待审核(付呗/快递代收+申请支付单审核) 时允许录入手动到账
|
||||
if ($st === 1) {
|
||||
// 正常待支付
|
||||
} elseif ($st === 5 && (string)($order->payment_method ?? '') === 'fubei') {
|
||||
// 待审核的付呗单,通过人工确认后标记已支付
|
||||
} elseif ($st === 5 && self::isPendingManualAudit($order)) {
|
||||
// 待审核的付呗/快递代收单,通过人工确认后标记已支付
|
||||
} else {
|
||||
self::setError('订单状态不允许支付');
|
||||
return false;
|
||||
@@ -382,8 +441,8 @@ class OrderLogic
|
||||
}
|
||||
|
||||
$st = (int)$order->status;
|
||||
if ($st !== 1 && !($st === 5 && (string)($order->payment_method ?? '') === 'fubei')) {
|
||||
self::setError('只有待支付或待审核(付呗)的订单才能取消');
|
||||
if ($st !== 1 && !($st === 5 && self::isPendingManualAudit($order))) {
|
||||
self::setError('只有待支付或待审核(付呗/快递代收)的订单才能取消');
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -467,13 +526,13 @@ class OrderLogic
|
||||
}
|
||||
|
||||
$st = (int) $order->status;
|
||||
if ($st === 5 && (string) ($order->payment_method ?? '') !== 'fubei') {
|
||||
self::setError('待审核订单仅支持付呗渠道拆分');
|
||||
if ($st === 5 && !self::isPendingManualAudit($order)) {
|
||||
self::setError('待审核订单仅支持付呗/快递代收渠道拆分');
|
||||
|
||||
return false;
|
||||
}
|
||||
if (! in_array($st, [1, 5, 2], true)) {
|
||||
self::setError('仅「待支付」「待审核(付呗)」或「已支付」的订单可拆分');
|
||||
self::setError('仅「待支付」「待审核(付呗/快递代收)」或「已支付」的订单可拆分');
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -536,8 +595,18 @@ class OrderLogic
|
||||
$n->creator_id = $order->creator_id;
|
||||
$n->order_type = (int) $typeList[$i];
|
||||
$n->amount = $amt;
|
||||
$n->create_type = self::normalizeCreateType(
|
||||
(string) ($order->create_type ?? ''),
|
||||
(string) ($order->payment_method ?? ''),
|
||||
(string) ($order->payee_userid ?? ''),
|
||||
(string) ($order->payer_external_userid ?? ''),
|
||||
(string) ($order->remark ?? '')
|
||||
);
|
||||
if ($st === 5) {
|
||||
$n->payment_method = 'fubei';
|
||||
// 付呗子单沿用 payment_method=fubei;快递代收子单 payment_method 留空,靠 create_type 标记
|
||||
if ((string) ($order->payment_method ?? '') === 'fubei') {
|
||||
$n->payment_method = 'fubei';
|
||||
}
|
||||
$n->status = 5;
|
||||
} elseif ($st === 2) {
|
||||
$n->status = 2;
|
||||
@@ -1213,7 +1282,7 @@ class OrderLogic
|
||||
}
|
||||
|
||||
$rows = $q
|
||||
->field(['id', 'order_no', 'order_type', 'amount', 'status', 'create_time', 'creator_id', 'remark', 'is_exempt'])
|
||||
->field(['id', 'order_no', 'order_type', 'amount', 'status', 'create_time', 'creator_id', 'remark', 'is_exempt', 'payment_method', 'create_type'])
|
||||
->order('id', 'desc')
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
@@ -0,0 +1,678 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\adminapi\logic\stats;
|
||||
|
||||
use think\facade\Db;
|
||||
|
||||
/**
|
||||
* 复诊接诊率统计(按月)
|
||||
*
|
||||
* 口径说明:
|
||||
* - 当月被指派总数 = 当月内 `tcm_diagnosis_assign_log`(按 **指派操作时间 lg.create_time** 落月,to_assistant_id>0,
|
||||
* **剔除勾选「继承」的指派 is_inherit=1**,诊单未删除)去重后的「医助 × 诊单」组合;
|
||||
* 同一诊单当月被多次指派给同一医助只计 1 次。
|
||||
* - 第 N 次下单 = 诊单(患者)名下计入业绩的业务订单(剔除履约 4/9/10、软删)按 create_time 升序的**全局**序列中第 N 笔;
|
||||
* 诊次**跨月累计不重置**:例如 5 月指派后旗下成交 4 单为二诊~五诊,6 月再成交即为六诊。
|
||||
* - 当月 N 诊单数 = **当月内下单**且全局序号为 N 的订单数,归属下单时点的**持有医助**——
|
||||
* 按指派日志时间线取「订单时间之前最近一次指派」的 to_assistant_id(释放 to=0 即不再归属;
|
||||
* 「继承」指派会转移持有人用于归属,但不计被指派数)。指派可发生在往月。
|
||||
* - 当月 N 诊接诊率 = 当月 N 诊单数 ÷ 当月被指派总数;往月指派当月成交会推高分子,故比率可能超过 100%;
|
||||
* 医助当月无新指派但旗下有成交时,被指派数为 0、比率显示为空。
|
||||
* - 分档动态产出:N 从 2 起,至当月命中数据的最大序号(至少展示到四诊,上限 MAX_VISIT_SLOT 防御异常数据),
|
||||
* 返回 `slots` 列表供前端动态渲染「五诊」「六诊」… 列。
|
||||
* - 部门归类:医助按其人事部门(admin_dept 最小 dept_id)归组;部门筛选(dept_ids,含组织下级)按该归属部门过滤。
|
||||
* - 部门行 / 合计行:被指派数按诊单去重(可能小于下级行相加);N 诊单数为下级行求和(每笔订单唯一归属一名医助)。
|
||||
*/
|
||||
class RevisitRateLogic
|
||||
{
|
||||
/** 至少展示到的复诊序号(二诊/三诊/四诊) */
|
||||
private const MIN_VISIT_SLOT_CEILING = 4;
|
||||
|
||||
/** 复诊序号统计上限(防御脏数据导致列爆炸;诊次跨月累计,上限放宽) */
|
||||
private const MAX_VISIT_SLOT = 50;
|
||||
|
||||
/** 未分配部门的占位分组 */
|
||||
private const UNASSIGNED_DEPT_NAME = '未分配部门';
|
||||
|
||||
/**
|
||||
* @param array{month?:string,dept_ids?:int[]|string} $params
|
||||
*
|
||||
* @return array{
|
||||
* month:string,start_date:string,end_date:string,
|
||||
* slots:list<int>,
|
||||
* total:array<string,int|float|null>,
|
||||
* rows:list<array<string,mixed>>
|
||||
* }
|
||||
*/
|
||||
public static function overview(array $params): array
|
||||
{
|
||||
$ctx = self::buildStatsCore($params);
|
||||
$month = $ctx['month'];
|
||||
|
||||
$minSlots = range(2, self::MIN_VISIT_SLOT_CEILING);
|
||||
$universe = self::assistantUniverse($ctx);
|
||||
if ($universe === []) {
|
||||
return [
|
||||
'month' => $month,
|
||||
'start_date' => $ctx['startDate'],
|
||||
'end_date' => $ctx['endDate'],
|
||||
'slots' => $minSlots,
|
||||
'total' => self::buildMetricPack(0, [], $minSlots),
|
||||
'rows' => [],
|
||||
];
|
||||
}
|
||||
|
||||
// 分档:2 ~ max(4, 当月命中的最大诊次)
|
||||
$maxHitSlot = 0;
|
||||
foreach ($ctx['slotOrdersByAssistant'] as $slotMap) {
|
||||
foreach ($slotMap as $slot => $_) {
|
||||
if ((int) $slot > $maxHitSlot) {
|
||||
$maxHitSlot = (int) $slot;
|
||||
}
|
||||
}
|
||||
}
|
||||
$slots = range(2, max(self::MIN_VISIT_SLOT_CEILING, $maxHitSlot));
|
||||
|
||||
$nameMap = Db::name('admin')
|
||||
->whereIn('id', array_keys($universe))
|
||||
->column('name', 'id');
|
||||
|
||||
// 医助行(按部门分组收集)
|
||||
/** @var array<int, list<array<string, mixed>>> $assistantRowsByDept */
|
||||
$assistantRowsByDept = [];
|
||||
/** @var array<int, array<int, true>> $deptDiagSet 部门 => 去重被指派诊单集合 */
|
||||
$deptDiagSet = [];
|
||||
/** @var array<int, array<int, int>> $deptSlotCounts 部门 => [slot => 订单数] */
|
||||
$deptSlotCounts = [];
|
||||
/** @var array<int, true> $totalDiagSet */
|
||||
$totalDiagSet = [];
|
||||
/** @var array<int, int> $totalSlotCounts */
|
||||
$totalSlotCounts = [];
|
||||
|
||||
foreach ($universe as $aid => $_) {
|
||||
$deptId = (int) ($ctx['assistantDept'][$aid] ?? 0);
|
||||
$diagSet = $ctx['diagsByAssistant'][$aid] ?? [];
|
||||
foreach ($diagSet as $did => $_d) {
|
||||
$deptDiagSet[$deptId][$did] = true;
|
||||
$totalDiagSet[$did] = true;
|
||||
}
|
||||
$slotCounts = [];
|
||||
foreach ($ctx['slotOrdersByAssistant'][$aid] ?? [] as $slot => $orders) {
|
||||
$cnt = \count($orders);
|
||||
$slotCounts[$slot] = $cnt;
|
||||
$deptSlotCounts[$deptId][$slot] = ($deptSlotCounts[$deptId][$slot] ?? 0) + $cnt;
|
||||
$totalSlotCounts[$slot] = ($totalSlotCounts[$slot] ?? 0) + $cnt;
|
||||
}
|
||||
$deptName = $deptId > 0
|
||||
? (string) ($ctx['deptNames'][$deptId] ?? ('#' . $deptId))
|
||||
: self::UNASSIGNED_DEPT_NAME;
|
||||
$assistantRowsByDept[$deptId][] = [
|
||||
'row_key' => 'a' . $aid,
|
||||
'is_dept' => 0,
|
||||
'assistant_id' => (int) $aid,
|
||||
'assistant_name' => (string) ($nameMap[$aid] ?? ('#' . $aid)),
|
||||
'dept_id' => $deptId,
|
||||
'dept_name' => $deptName,
|
||||
] + self::buildMetricPack(\count($diagSet), $slotCounts, $slots);
|
||||
}
|
||||
|
||||
// 部门行 + 子行
|
||||
$rows = [];
|
||||
foreach ($assistantRowsByDept as $deptId => $children) {
|
||||
usort($children, static function (array $a, array $b): int {
|
||||
if ($a['assigned_count'] !== $b['assigned_count']) {
|
||||
return $b['assigned_count'] <=> $a['assigned_count'];
|
||||
}
|
||||
|
||||
return strcmp((string) $a['assistant_name'], (string) $b['assistant_name']);
|
||||
});
|
||||
$deptName = $deptId > 0
|
||||
? (string) ($ctx['deptNames'][$deptId] ?? ('#' . $deptId))
|
||||
: self::UNASSIGNED_DEPT_NAME;
|
||||
$rows[] = [
|
||||
'row_key' => 'd' . $deptId,
|
||||
'is_dept' => 1,
|
||||
'dept_id' => (int) $deptId,
|
||||
'dept_name' => $deptName,
|
||||
'assistant_count' => \count($children),
|
||||
'children' => $children,
|
||||
] + self::buildMetricPack(\count($deptDiagSet[$deptId] ?? []), $deptSlotCounts[$deptId] ?? [], $slots);
|
||||
}
|
||||
usort($rows, static function (array $a, array $b): int {
|
||||
if ($a['assigned_count'] !== $b['assigned_count']) {
|
||||
return $b['assigned_count'] <=> $a['assigned_count'];
|
||||
}
|
||||
|
||||
return strcmp((string) $a['dept_name'], (string) $b['dept_name']);
|
||||
});
|
||||
|
||||
return [
|
||||
'month' => $month,
|
||||
'start_date' => $ctx['startDate'],
|
||||
'end_date' => $ctx['endDate'],
|
||||
'slots' => $slots,
|
||||
'total' => self::buildMetricPack(\count($totalDiagSet), $totalSlotCounts, $slots),
|
||||
'rows' => $rows,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 被指派明细(按诊单聚合,与「被指派数」同口径可对账)。
|
||||
* scope:assistant_id(医助行)/ dept_id(部门行,含 0=未分配部门)/ 都不传 = 当前部门筛选下合计。
|
||||
*
|
||||
* @param array{month?:string,dept_ids?:int[]|string,assistant_id?:int|string,dept_id?:int|string} $params
|
||||
*
|
||||
* @return array{month:string,count:int,rows:list<array<string,mixed>>}
|
||||
*/
|
||||
public static function assignLines(array $params): array
|
||||
{
|
||||
$ctx = self::buildStatsCore($params);
|
||||
$assistantSet = self::applyRowScope($ctx, $params);
|
||||
|
||||
/** @var array<int, array{assistants: array<int, true>, assign_count: int, last_time: int}> $byDiag */
|
||||
$byDiag = [];
|
||||
foreach ($ctx['pairsRaw'] as $p) {
|
||||
$aid = (int) $p['to_assistant_id'];
|
||||
$did = (int) $p['diagnosis_id'];
|
||||
if (!isset($assistantSet[$aid])) {
|
||||
continue;
|
||||
}
|
||||
if (!isset($byDiag[$did])) {
|
||||
$byDiag[$did] = ['assistants' => [], 'assign_count' => 0, 'last_time' => 0];
|
||||
}
|
||||
$byDiag[$did]['assistants'][$aid] = true;
|
||||
$byDiag[$did]['assign_count']++;
|
||||
$byDiag[$did]['last_time'] = max($byDiag[$did]['last_time'], (int) $p['create_time']);
|
||||
}
|
||||
|
||||
if ($byDiag === []) {
|
||||
return ['month' => $ctx['month'], 'count' => 0, 'rows' => []];
|
||||
}
|
||||
|
||||
$diagInfo = self::fetchDiagnosisInfo(array_keys($byDiag));
|
||||
$assistantIds = [];
|
||||
foreach ($byDiag as $d) {
|
||||
foreach ($d['assistants'] as $aid => $_) {
|
||||
$assistantIds[$aid] = true;
|
||||
}
|
||||
}
|
||||
$nameMap = Db::name('admin')
|
||||
->whereIn('id', array_keys($assistantIds))
|
||||
->column('name', 'id');
|
||||
|
||||
$rows = [];
|
||||
foreach ($byDiag as $did => $d) {
|
||||
$names = [];
|
||||
foreach ($d['assistants'] as $aid => $_) {
|
||||
$names[] = (string) ($nameMap[$aid] ?? ('#' . $aid));
|
||||
}
|
||||
$rows[] = [
|
||||
'diagnosis_id' => (int) $did,
|
||||
'patient_name' => (string) ($diagInfo[$did]['patient_name'] ?? ''),
|
||||
'patient_phone' => (string) ($diagInfo[$did]['phone'] ?? ''),
|
||||
'assistant_names' => implode('、', $names),
|
||||
'assign_count' => (int) $d['assign_count'],
|
||||
'last_assign_time' => (int) $d['last_time'],
|
||||
'last_assign_time_text' => $d['last_time'] > 0 ? date('Y-m-d H:i:s', $d['last_time']) : '',
|
||||
];
|
||||
}
|
||||
usort($rows, static fn (array $a, array $b): int => $b['last_assign_time'] <=> $a['last_assign_time']);
|
||||
|
||||
return ['month' => $ctx['month'], 'count' => \count($rows), 'rows' => $rows];
|
||||
}
|
||||
|
||||
/**
|
||||
* N 诊订单明细:scope 内当月下单、全局序号 = slot 的具体订单(归属持有医助),与 visit{slot}_count 同口径可对账。
|
||||
*
|
||||
* @param array{month?:string,slot?:int|string,dept_ids?:int[]|string,assistant_id?:int|string,dept_id?:int|string} $params
|
||||
*
|
||||
* @return array{month:string,slot:int,count:int,rows:list<array<string,mixed>>}
|
||||
*/
|
||||
public static function visitOrderLines(array $params): array
|
||||
{
|
||||
$slot = (int) ($params['slot'] ?? 0);
|
||||
if ($slot < 2 || $slot > self::MAX_VISIT_SLOT) {
|
||||
return ['month' => self::normalizeMonth((string) ($params['month'] ?? '')), 'slot' => $slot, 'count' => 0, 'rows' => []];
|
||||
}
|
||||
|
||||
$ctx = self::buildStatsCore($params);
|
||||
$assistantSet = self::applyRowScope($ctx, $params);
|
||||
|
||||
$orderRows = [];
|
||||
foreach ($assistantSet as $aid => $_) {
|
||||
foreach ($ctx['slotOrdersByAssistant'][$aid][$slot] ?? [] as $r) {
|
||||
$r['holder_assistant_id'] = (int) $aid;
|
||||
$orderRows[] = $r;
|
||||
}
|
||||
}
|
||||
if ($orderRows === []) {
|
||||
return ['month' => $ctx['month'], 'slot' => $slot, 'count' => 0, 'rows' => []];
|
||||
}
|
||||
|
||||
$diagIds = array_values(array_unique(array_map(
|
||||
static fn (array $r): int => (int) $r['diagnosis_id'],
|
||||
$orderRows
|
||||
)));
|
||||
$diagInfo = self::fetchDiagnosisInfo($diagIds);
|
||||
$adminIds = [];
|
||||
foreach ($orderRows as $r) {
|
||||
if ((int) $r['creator_id'] > 0) {
|
||||
$adminIds[(int) $r['creator_id']] = true;
|
||||
}
|
||||
$adminIds[(int) $r['holder_assistant_id']] = true;
|
||||
}
|
||||
$adminNames = $adminIds !== []
|
||||
? Db::name('admin')->whereIn('id', array_keys($adminIds))->column('name', 'id')
|
||||
: [];
|
||||
|
||||
$rows = [];
|
||||
foreach ($orderRows as $r) {
|
||||
$did = (int) $r['diagnosis_id'];
|
||||
$cid = (int) $r['creator_id'];
|
||||
$hid = (int) $r['holder_assistant_id'];
|
||||
$ct = (int) $r['create_time'];
|
||||
$rows[] = [
|
||||
'order_id' => (int) $r['id'],
|
||||
'order_no' => (string) ($r['order_no'] ?? ''),
|
||||
'diagnosis_id' => $did,
|
||||
'patient_name' => (string) ($diagInfo[$did]['patient_name'] ?? ''),
|
||||
'patient_phone' => (string) ($diagInfo[$did]['phone'] ?? ''),
|
||||
'amount' => round((float) ($r['amount'] ?? 0), 2),
|
||||
'create_time' => $ct,
|
||||
'create_time_text' => $ct > 0 ? date('Y-m-d H:i:s', $ct) : '',
|
||||
'creator_id' => $cid,
|
||||
'creator_name' => $cid > 0 ? (string) ($adminNames[$cid] ?? ('#' . $cid)) : '—',
|
||||
'assistant_id' => $hid,
|
||||
'assistant_name' => $hid > 0 ? (string) ($adminNames[$hid] ?? ('#' . $hid)) : '—',
|
||||
];
|
||||
}
|
||||
usort($rows, static fn (array $a, array $b): int => $b['create_time'] <=> $a['create_time']);
|
||||
|
||||
return ['month' => $ctx['month'], 'slot' => $slot, 'count' => \count($rows), 'rows' => $rows];
|
||||
}
|
||||
|
||||
/**
|
||||
* 部门下拉(全量未删除部门,前端组树)。
|
||||
*
|
||||
* @return array{rows: list<array{id:int,pid:int,name:string}>}
|
||||
*/
|
||||
public static function deptOptions(): array
|
||||
{
|
||||
$rows = Db::name('dept')
|
||||
->whereNull('delete_time')
|
||||
->field(['id', 'pid', 'name'])
|
||||
->order('sort', 'desc')
|
||||
->order('id', 'asc')
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
return [
|
||||
'rows' => array_map(static fn (array $r): array => [
|
||||
'id' => (int) $r['id'],
|
||||
'pid' => (int) $r['pid'],
|
||||
'name' => (string) $r['name'],
|
||||
], $rows),
|
||||
];
|
||||
}
|
||||
|
||||
// ─────────────────────────── 内部实现 ───────────────────────────
|
||||
|
||||
/**
|
||||
* 核心统计上下文:
|
||||
* 1. 全量指派日志(≤ 月末)构建持有时间线;
|
||||
* 2. 分母:当月非继承指派的「医助 × 诊单」;
|
||||
* 3. 分子:曾被指派诊单的当月订单按全局序号 ≥2 归属持有医助;
|
||||
* 4. 应用部门筛选(含组织下级)。
|
||||
*
|
||||
* @param array{month?:string,dept_ids?:int[]|string} $params
|
||||
*
|
||||
* @return array{
|
||||
* month:string,startDate:string,endDate:string,startTs:int,endTs:int,
|
||||
* pairsRaw:list<array{diagnosis_id:int,to_assistant_id:int,create_time:int}>,
|
||||
* diagsByAssistant:array<int,array<int,true>>,
|
||||
* slotOrdersByAssistant:array<int,array<int,list<array<string,mixed>>>>,
|
||||
* assistantDept:array<int,int>,
|
||||
* deptNames:array<int,string>
|
||||
* }
|
||||
*/
|
||||
private static function buildStatsCore(array $params): array
|
||||
{
|
||||
$month = self::normalizeMonth((string) ($params['month'] ?? ''));
|
||||
$startTs = (int) strtotime($month . '-01 00:00:00');
|
||||
$endTs = (int) strtotime(date('Y-m-t', $startTs) . ' 23:59:59');
|
||||
|
||||
// 全量指派日志(≤ 月末,诊单未删除):含释放(to=0)与继承行,用于持有时间线
|
||||
$logRows = Db::name('tcm_diagnosis_assign_log')
|
||||
->alias('lg')
|
||||
->join('tcm_diagnosis dg', 'dg.id = lg.diagnosis_id AND dg.delete_time IS NULL', 'INNER')
|
||||
->where('lg.create_time', '<=', $endTs)
|
||||
->where('lg.diagnosis_id', '>', 0)
|
||||
->field(['lg.id', 'lg.diagnosis_id', 'lg.to_assistant_id', 'lg.create_time', 'lg.is_inherit'])
|
||||
->order(['lg.diagnosis_id' => 'asc', 'lg.create_time' => 'asc', 'lg.id' => 'asc'])
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
/** @var array<int, list<array{t:int,to:int}>> $timeline 诊单 => 持有变更时间线(升序) */
|
||||
$timeline = [];
|
||||
$pairsRaw = [];
|
||||
/** @var array<int, array<int, true>> $diagsByAssistant 分母:医助 => 诊单集合 */
|
||||
$diagsByAssistant = [];
|
||||
/** @var array<int, true> $candidateDiagSet 曾被指派(to>0,含继承)的诊单 */
|
||||
$candidateDiagSet = [];
|
||||
foreach ($logRows as $r) {
|
||||
$did = (int) ($r['diagnosis_id'] ?? 0);
|
||||
$aid = (int) ($r['to_assistant_id'] ?? 0);
|
||||
$t = (int) ($r['create_time'] ?? 0);
|
||||
$timeline[$did][] = ['t' => $t, 'to' => $aid];
|
||||
if ($aid > 0) {
|
||||
$candidateDiagSet[$did] = true;
|
||||
if ((int) ($r['is_inherit'] ?? 0) === 0 && $t >= $startTs && $t <= $endTs) {
|
||||
$pairsRaw[] = ['diagnosis_id' => $did, 'to_assistant_id' => $aid, 'create_time' => $t];
|
||||
$diagsByAssistant[$aid][$did] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 分子:曾被指派诊单的当月订单(全局序号 ≥2),归属下单时点的持有医助
|
||||
/** @var array<int, array<int, list<array<string, mixed>>>> $slotOrdersByAssistant */
|
||||
$slotOrdersByAssistant = [];
|
||||
foreach (array_chunk(array_keys($candidateDiagSet), 2000) as $chunk) {
|
||||
$orderRows = self::fetchOrderSeqRows(
|
||||
$chunk,
|
||||
['o.id', 'o.order_no', 'o.diagnosis_id', 'o.create_time', 'o.amount', 'o.creator_id']
|
||||
);
|
||||
$curDid = 0;
|
||||
$seq = 0;
|
||||
$ptr = 0;
|
||||
$holder = 0;
|
||||
foreach ($orderRows as $r) {
|
||||
$did = (int) ($r['diagnosis_id'] ?? 0);
|
||||
if ($did <= 0) {
|
||||
continue;
|
||||
}
|
||||
if ($did !== $curDid) {
|
||||
$curDid = $did;
|
||||
$seq = 0;
|
||||
$ptr = 0;
|
||||
$holder = 0;
|
||||
}
|
||||
$seq++;
|
||||
$ct = (int) ($r['create_time'] ?? 0);
|
||||
// 推进时间线指针:订单时间之前(含同刻)最近一次指派的持有人
|
||||
$tl = $timeline[$did] ?? [];
|
||||
$tlCount = \count($tl);
|
||||
while ($ptr < $tlCount && $tl[$ptr]['t'] <= $ct) {
|
||||
$holder = (int) $tl[$ptr]['to'];
|
||||
$ptr++;
|
||||
}
|
||||
if ($seq < 2 || $seq > self::MAX_VISIT_SLOT) {
|
||||
continue;
|
||||
}
|
||||
if ($ct < $startTs || $ct > $endTs) {
|
||||
continue;
|
||||
}
|
||||
if ($holder > 0) {
|
||||
$slotOrdersByAssistant[$holder][$seq][] = $r;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 医助归属部门 + 部门筛选(含组织下级)
|
||||
$universeIds = array_keys($diagsByAssistant + $slotOrdersByAssistant);
|
||||
[$assistantDept, $deptNames] = self::buildAssistantDeptIndex($universeIds);
|
||||
$deptFilterIds = self::parseDeptIds($params['dept_ids'] ?? null);
|
||||
if ($deptFilterIds !== []) {
|
||||
$subtreeSet = self::expandDeptSubtreeSet($deptFilterIds);
|
||||
foreach ($universeIds as $aid) {
|
||||
$deptId = (int) ($assistantDept[$aid] ?? 0);
|
||||
if ($deptId <= 0 || !isset($subtreeSet[$deptId])) {
|
||||
unset($diagsByAssistant[$aid], $slotOrdersByAssistant[$aid]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'month' => $month,
|
||||
'startDate' => date('Y-m-d', $startTs),
|
||||
'endDate' => date('Y-m-d', $endTs),
|
||||
'startTs' => $startTs,
|
||||
'endTs' => $endTs,
|
||||
'pairsRaw' => $pairsRaw,
|
||||
'diagsByAssistant' => $diagsByAssistant,
|
||||
'slotOrdersByAssistant' => $slotOrdersByAssistant,
|
||||
'assistantDept' => $assistantDept,
|
||||
'deptNames' => $deptNames,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计涉及的医助全集:分母(被指派)∪ 分子(持有成交)。
|
||||
*
|
||||
* @param array{diagsByAssistant:array<int,array<int,true>>,slotOrdersByAssistant:array<int,array<int,list<array<string,mixed>>>>} $ctx
|
||||
*
|
||||
* @return array<int, true>
|
||||
*/
|
||||
private static function assistantUniverse(array $ctx): array
|
||||
{
|
||||
$set = [];
|
||||
foreach (array_keys($ctx['diagsByAssistant']) as $aid) {
|
||||
$set[(int) $aid] = true;
|
||||
}
|
||||
foreach (array_keys($ctx['slotOrdersByAssistant']) as $aid) {
|
||||
$set[(int) $aid] = true;
|
||||
}
|
||||
|
||||
return $set;
|
||||
}
|
||||
|
||||
/**
|
||||
* 行级 scope:assistant_id(医助行)优先;其次 dept_id(部门归组行,0=未分配部门);都不传 = 全部(已含部门筛选)。
|
||||
*
|
||||
* @return array<int, true> scope 内医助集合
|
||||
*/
|
||||
private static function applyRowScope(array $ctx, array $params): array
|
||||
{
|
||||
$assistantId = (int) ($params['assistant_id'] ?? 0);
|
||||
$hasDeptScope = isset($params['dept_id']) && $params['dept_id'] !== '' && $params['dept_id'] !== null;
|
||||
$deptScopeId = $hasDeptScope ? (int) $params['dept_id'] : -1;
|
||||
|
||||
$assistantSet = [];
|
||||
foreach (self::assistantUniverse($ctx) as $aid => $_) {
|
||||
if ($assistantId > 0) {
|
||||
if ((int) $aid === $assistantId) {
|
||||
$assistantSet[$aid] = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if ($hasDeptScope) {
|
||||
if ((int) ($ctx['assistantDept'][$aid] ?? 0) === $deptScopeId) {
|
||||
$assistantSet[$aid] = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
$assistantSet[$aid] = true;
|
||||
}
|
||||
|
||||
return $assistantSet;
|
||||
}
|
||||
|
||||
/**
|
||||
* 医助 → 归属部门 + 部门名称表。
|
||||
*
|
||||
* @param list<int> $adminIds
|
||||
*
|
||||
* @return array{0: array<int,int>, 1: array<int,string>}
|
||||
*/
|
||||
private static function buildAssistantDeptIndex(array $adminIds): array
|
||||
{
|
||||
if ($adminIds === []) {
|
||||
return [[], []];
|
||||
}
|
||||
// admin_dept 为 (admin_id, dept_id) 联合主键、无自增 id;取最小 dept_id 作为归属部门保证确定性
|
||||
$relRows = Db::name('admin_dept')
|
||||
->whereIn('admin_id', $adminIds)
|
||||
->order(['admin_id' => 'asc', 'dept_id' => 'asc'])
|
||||
->field(['admin_id', 'dept_id'])
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
$deptNames = Db::name('dept')
|
||||
->whereNull('delete_time')
|
||||
->column('name', 'id');
|
||||
|
||||
$canonical = [];
|
||||
foreach ($relRows as $r) {
|
||||
$aid = (int) ($r['admin_id'] ?? 0);
|
||||
$deptId = (int) ($r['dept_id'] ?? 0);
|
||||
if ($aid <= 0 || $deptId <= 0 || isset($canonical[$aid])) {
|
||||
continue;
|
||||
}
|
||||
if (!isset($deptNames[$deptId])) {
|
||||
continue;
|
||||
}
|
||||
$canonical[$aid] = $deptId;
|
||||
}
|
||||
|
||||
$names = [];
|
||||
foreach ($deptNames as $id => $name) {
|
||||
$names[(int) $id] = (string) $name;
|
||||
}
|
||||
|
||||
return [$canonical, $names];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $raw int[] | 逗号分隔字符串
|
||||
*
|
||||
* @return list<int>
|
||||
*/
|
||||
private static function parseDeptIds(mixed $raw): array
|
||||
{
|
||||
if ($raw === null || $raw === '' || $raw === []) {
|
||||
return [];
|
||||
}
|
||||
$list = \is_array($raw) ? $raw : explode(',', (string) $raw);
|
||||
|
||||
return array_values(array_unique(array_filter(
|
||||
array_map('intval', $list),
|
||||
static fn (int $v): bool => $v > 0
|
||||
)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 选中部门 + 全部组织下级的 id 集合。
|
||||
*
|
||||
* @param list<int> $deptIds
|
||||
*
|
||||
* @return array<int, true>
|
||||
*/
|
||||
private static function expandDeptSubtreeSet(array $deptIds): array
|
||||
{
|
||||
$rows = Db::name('dept')
|
||||
->whereNull('delete_time')
|
||||
->field(['id', 'pid'])
|
||||
->select()
|
||||
->toArray();
|
||||
$childrenByPid = [];
|
||||
foreach ($rows as $r) {
|
||||
$childrenByPid[(int) $r['pid']][] = (int) $r['id'];
|
||||
}
|
||||
|
||||
$set = [];
|
||||
$queue = $deptIds;
|
||||
while ($queue !== []) {
|
||||
$id = (int) array_shift($queue);
|
||||
if ($id <= 0 || isset($set[$id])) {
|
||||
continue;
|
||||
}
|
||||
$set[$id] = true;
|
||||
foreach ($childrenByPid[$id] ?? [] as $childId) {
|
||||
$queue[] = $childId;
|
||||
}
|
||||
}
|
||||
|
||||
return $set;
|
||||
}
|
||||
|
||||
/**
|
||||
* 诊单订单序列源查询(与业绩口径一致),统一排序保证序号稳定。
|
||||
*
|
||||
* @param list<int> $diagIds
|
||||
* @param list<string> $fields
|
||||
*
|
||||
* @return list<array<string, mixed>>
|
||||
*/
|
||||
private static function fetchOrderSeqRows(array $diagIds, array $fields): array
|
||||
{
|
||||
$q = Db::name('tcm_prescription_order')
|
||||
->alias('o')
|
||||
->whereIn('o.diagnosis_id', $diagIds)
|
||||
->whereNull('o.delete_time');
|
||||
YejiStatsLogic::applyPrescriptionOrderNotCancelledForPerformanceQuery($q, 'o');
|
||||
|
||||
return $q
|
||||
->field($fields)
|
||||
->order(['o.diagnosis_id' => 'asc', 'o.create_time' => 'asc', 'o.id' => 'asc'])
|
||||
->select()
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<int> $diagIds
|
||||
*
|
||||
* @return array<int, array{patient_name:string,phone:string}>
|
||||
*/
|
||||
private static function fetchDiagnosisInfo(array $diagIds): array
|
||||
{
|
||||
if ($diagIds === []) {
|
||||
return [];
|
||||
}
|
||||
$rows = Db::name('tcm_diagnosis')
|
||||
->whereIn('id', $diagIds)
|
||||
->field(['id', 'patient_name', 'phone'])
|
||||
->select()
|
||||
->toArray();
|
||||
$out = [];
|
||||
foreach ($rows as $r) {
|
||||
$out[(int) $r['id']] = [
|
||||
'patient_name' => trim((string) ($r['patient_name'] ?? '')),
|
||||
'phone' => trim((string) ($r['phone'] ?? '')),
|
||||
];
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, int> $slotCounts [slot => n]
|
||||
* @param list<int> $slots 需输出的分档列表(保证各行键齐全)
|
||||
*
|
||||
* @return array<string, int|float|null>
|
||||
*/
|
||||
private static function buildMetricPack(int $assigned, array $slotCounts, array $slots): array
|
||||
{
|
||||
$pack = ['assigned_count' => $assigned];
|
||||
foreach ($slots as $slot) {
|
||||
$cnt = (int) ($slotCounts[$slot] ?? 0);
|
||||
$pack['visit' . $slot . '_count'] = $cnt;
|
||||
$pack['visit' . $slot . '_rate'] = $assigned > 0
|
||||
? round($cnt / $assigned * 100, 2)
|
||||
: null;
|
||||
}
|
||||
|
||||
return $pack;
|
||||
}
|
||||
|
||||
/** 归一化月份参数为 YYYY-MM,非法时回退当前月 */
|
||||
private static function normalizeMonth(string $month): string
|
||||
{
|
||||
$month = trim($month);
|
||||
if (preg_match('/^\d{4}-(0[1-9]|1[0-2])$/', $month) === 1) {
|
||||
return $month;
|
||||
}
|
||||
|
||||
return date('Y-m');
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,8 @@ use think\facade\Db;
|
||||
*
|
||||
* - 进线数据 = 区间内 add_external_contact 事件,按接待员工(user_id) 归属部门
|
||||
* · 与 qywx.customer/todayArrival、客户列表同源事件表;选定渠道时客户须带对应企微标签(contact_tag);**二中心及其组织下级在选定渠道下进线计 0**
|
||||
* - 被指派数 = 区间内 `tcm_diagnosis_assign_log` 且 **to_assistant_id>0**,且 **related_po_create_time>0** 并落在区间(**一律按快照字段统计**,与履约释放写入一致;诊单后台指派时会回填该字段,见 DiagnosisLogic::assign)
|
||||
* - 被指派数 = 区间内 `tcm_diagnosis_assign_log` 且 **to_assistant_id>0**,**剔除勾选「继承」的指派 is_inherit=1**,按**指派操作时间 lg.create_time** 落区间,
|
||||
* 「医助 × 诊单」去重、剔除已删诊单 —— 与「复诊接诊率」页面「当月被指派总数」完全同口径(见 RevisitRateLogic)
|
||||
* · **二中心及其组织下级在选定渠道下计 0**(与进线一致)
|
||||
* - 复诊统计 = 仅**名称含「二中心」的部门及其组织架构下级**:区间内业务单按患者 create_time 排序,第 1 笔不计,第 2 笔起分列「复诊2」「复诊3」…;口径同业绩单条件;按**订单创建人**所在部门在组织树内 rollup(含下级),与排行榜复诊一致
|
||||
* - 已完成挂号 = status=3 挂号条数(appointment_date),列「已完成挂号」;部门归属:有效医助→接诊医生→展示行映射。
|
||||
@@ -857,7 +858,7 @@ class YejiStatsLogic
|
||||
? ('排行诊金 = 选定渠道「' . $channelName . '」业绩中「订单创建人 = 该医助」的部分;与部门表该渠道业绩列同口径(按订单创建人归属)。被指派数与部门表同口径。接诊率 = 诊金 ÷ 进线(元/进线)。'
|
||||
. '「二中心」及其组织下级医助在该渠道下诊金、进线与被指派计 0。'
|
||||
. ' 预约诊单:预约表按「预约日期」落在区间内、有效医助口径同接诊,状态含已预约/已完成/已过号(不含已取消);科室下医助全量展示。')
|
||||
: '排行诊金 = 业务订单中「订单创建人 = 该医助」的金额合计;与部门表「合计业绩」同口径(按订单创建人归属)。被指派数为指派至该医助的次数。接诊率 = 诊金 ÷ 进线(元/进线;进线为 0 时为 0)。'
|
||||
: '排行诊金 = 业务订单中「订单创建人 = 该医助」的金额合计;与部门表「合计业绩」同口径(按订单创建人归属)。被指派数为指派至该医助的次数(剔除勾选「继承」的指派)。接诊率 = 诊金 ÷ 进线(元/进线;进线为 0 时为 0)。'
|
||||
. ' 预约诊单:预约表按「预约日期」落在区间内、有效医助口径同接诊,状态含已预约/已完成/已过号(不含已取消);科室下医助全量展示。';
|
||||
|
||||
$leaderboards = [];
|
||||
@@ -1777,8 +1778,9 @@ class YejiStatsLogic
|
||||
}
|
||||
|
||||
/**
|
||||
* 被指派数:统计 `tcm_diagnosis_assign_log`(to_assistant_id>0),**仅**按 **related_po_create_time** 落入区间(须 >0)。
|
||||
* 选定字典渠道时 INNER JOIN 诊单以满足挂号 channels EXISTS;tag 退化口径只过滤日志字段,不强制挂诊单。
|
||||
* 被指派数:统计 `tcm_diagnosis_assign_log`(to_assistant_id>0,**剔除勾选「继承」的指派 is_inherit=1**),
|
||||
* 按**指派操作时间 lg.create_time** 落入区间,「医助 × 诊单」去重(COUNT DISTINCT diagnosis_id)、剔除已删诊单,
|
||||
* 与「复诊接诊率」页面同口径。选定字典渠道时按挂号 channels EXISTS 收窄;tag 退化口径按日志字段过滤。
|
||||
*
|
||||
* @param array<int, int> $adminToPrimary
|
||||
* @param int[]|null $tagDiagIds
|
||||
@@ -1801,11 +1803,13 @@ class YejiStatsLogic
|
||||
return [];
|
||||
}
|
||||
|
||||
$diagTable = self::tableWithPrefix('tcm_diagnosis');
|
||||
$query = Db::name('tcm_diagnosis_assign_log')
|
||||
->alias('lg')
|
||||
->where('lg.related_po_create_time', '>', 0)
|
||||
->where('lg.related_po_create_time', 'between', [$startTs, $endTs])
|
||||
->where('lg.to_assistant_id', '>', 0);
|
||||
->join("{$diagTable} dg", 'dg.id = lg.diagnosis_id AND dg.delete_time IS NULL', 'INNER')
|
||||
->where('lg.create_time', 'between', [$startTs, $endTs])
|
||||
->where('lg.to_assistant_id', '>', 0)
|
||||
->where('lg.is_inherit', 0);
|
||||
if ($channelFilterActive) {
|
||||
if ($appointmentChannelValues !== []) {
|
||||
$norm = self::normalizeAppointmentChannelValues($appointmentChannelValues);
|
||||
@@ -1816,8 +1820,6 @@ class YejiStatsLogic
|
||||
if ($pack === null) {
|
||||
return [];
|
||||
}
|
||||
$diagTable = self::tableWithPrefix('tcm_diagnosis');
|
||||
$query->join("{$diagTable} dg", 'dg.id = lg.diagnosis_id AND dg.delete_time IS NULL', 'INNER');
|
||||
$query->whereRaw($pack[0], $pack[1]);
|
||||
} elseif ($tagDiagIds !== null || $tagAssistantIds !== null) {
|
||||
if ($tagDiagIds !== null && $tagDiagIds === []) {
|
||||
@@ -1837,7 +1839,7 @@ class YejiStatsLogic
|
||||
}
|
||||
}
|
||||
|
||||
$query->field(['lg.to_assistant_id', Db::raw('COUNT(*) AS cnt')])
|
||||
$query->field(['lg.to_assistant_id', Db::raw('COUNT(DISTINCT lg.diagnosis_id) AS cnt')])
|
||||
->group('lg.to_assistant_id');
|
||||
|
||||
$rows = $query->select()->toArray();
|
||||
@@ -1869,7 +1871,7 @@ class YejiStatsLogic
|
||||
}
|
||||
|
||||
/**
|
||||
* 医助维度被指派次数:与 countDiagnosisAssignsByDept 同过滤与区间,按 to_assistant_id 汇总(供排行榜)。
|
||||
* 医助维度被指派次数:与 countDiagnosisAssignsByDept 同过滤与区间(同样剔除 is_inherit=1),按 to_assistant_id 汇总(供排行榜)。
|
||||
*
|
||||
* @return array<int, int>
|
||||
*/
|
||||
@@ -1883,11 +1885,13 @@ class YejiStatsLogic
|
||||
?array $dataScopeAdminIds = null,
|
||||
?array $channelInfo = null
|
||||
): array {
|
||||
$diagTable = self::tableWithPrefix('tcm_diagnosis');
|
||||
$query = Db::name('tcm_diagnosis_assign_log')
|
||||
->alias('lg')
|
||||
->where('lg.related_po_create_time', '>', 0)
|
||||
->where('lg.related_po_create_time', 'between', [$startTs, $endTs])
|
||||
->where('lg.to_assistant_id', '>', 0);
|
||||
->join("{$diagTable} dg", 'dg.id = lg.diagnosis_id AND dg.delete_time IS NULL', 'INNER')
|
||||
->where('lg.create_time', 'between', [$startTs, $endTs])
|
||||
->where('lg.to_assistant_id', '>', 0)
|
||||
->where('lg.is_inherit', 0);
|
||||
|
||||
if ($channelFilterActive) {
|
||||
if ($appointmentChannelValues !== []) {
|
||||
@@ -1899,8 +1903,6 @@ class YejiStatsLogic
|
||||
if ($pack === null) {
|
||||
return [];
|
||||
}
|
||||
$diagTable = self::tableWithPrefix('tcm_diagnosis');
|
||||
$query->join("{$diagTable} dg", 'dg.id = lg.diagnosis_id AND dg.delete_time IS NULL', 'INNER');
|
||||
$query->whereRaw($pack[0], $pack[1]);
|
||||
} elseif ($tagDiagIds !== null || $tagAssistantIds !== null) {
|
||||
if ($tagDiagIds !== null && $tagDiagIds === []) {
|
||||
@@ -1920,7 +1922,7 @@ class YejiStatsLogic
|
||||
}
|
||||
}
|
||||
|
||||
$query->field(['lg.to_assistant_id', Db::raw('COUNT(*) AS cnt')])
|
||||
$query->field(['lg.to_assistant_id', Db::raw('COUNT(DISTINCT lg.diagnosis_id) AS cnt')])
|
||||
->group('lg.to_assistant_id');
|
||||
|
||||
$rows = $query->select()->toArray();
|
||||
|
||||
@@ -696,6 +696,8 @@ class PrescriptionOrderLogic
|
||||
{
|
||||
PrescriptionOrderPayOrder::where('prescription_order_id', (int) $order->id)->delete();
|
||||
$order->linked_pay_order_id = null;
|
||||
// 解除关联后刷新「需代收」固定快照(已付归零 → 需代收=总金额)
|
||||
self::refreshAgencyCollectSnapshot((int) $order->id);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -768,7 +770,7 @@ class PrescriptionOrderLogic
|
||||
return [];
|
||||
}
|
||||
$rows = Order::whereIn('id', $ids)->whereNull('delete_time')
|
||||
->field(['id', 'order_no', 'order_type', 'amount', 'status', 'create_time', 'creator_id', 'remark', 'is_exempt'])
|
||||
->field(['id', 'order_no', 'order_type', 'amount', 'status', 'create_time', 'creator_id', 'remark', 'is_exempt', 'payment_method', 'create_type'])
|
||||
->order('id', 'asc')
|
||||
->whereIn('status', [2, 4, 5])
|
||||
->select()
|
||||
@@ -837,6 +839,35 @@ class PrescriptionOrderLogic
|
||||
$arr['deposit_min_amount'] = self::depositMinAmount();
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新「需代收」固定快照:按当前 总金额 − 关联已付总额(支付单 status ∈ {2 已支付, 5 待审核})写入 agency_collect_amount。
|
||||
* 在关联/新增支付单、建单、改总金额、解除关联等写操作末尾调用;之后退款/审核状态变化不再影响该值。
|
||||
*/
|
||||
private static function refreshAgencyCollectSnapshot(int $id): void
|
||||
{
|
||||
if ($id <= 0) {
|
||||
return;
|
||||
}
|
||||
$order = PrescriptionOrder::where('id', $id)->whereNull('delete_time')->find();
|
||||
if (!$order) {
|
||||
return;
|
||||
}
|
||||
$amount = round((float) $order->amount, 2);
|
||||
$paid = 0.0;
|
||||
$ids = self::linkedPayOrderIdList($id);
|
||||
if ($ids !== []) {
|
||||
$payOrders = Order::whereIn('id', $ids)->whereNull('delete_time')->field(['amount', 'status'])->select();
|
||||
foreach ($payOrders as $po) {
|
||||
$st = (int) $po->status;
|
||||
if ($st === 2 || $st === 5) {
|
||||
$paid += (float) $po->amount;
|
||||
}
|
||||
}
|
||||
}
|
||||
$order->agency_collect_amount = round($amount - $paid, 2);
|
||||
$order->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $params
|
||||
* @return array<string,mixed>|false
|
||||
@@ -969,7 +1000,9 @@ class PrescriptionOrderLogic
|
||||
self::replacePayOrderLinks((int) $order->id, $payOrderIds);
|
||||
}
|
||||
|
||||
$out = $order->toArray();
|
||||
self::refreshAgencyCollectSnapshot((int) $order->id);
|
||||
|
||||
$out = PrescriptionOrder::where('id', (int) $order->id)->find()->toArray();
|
||||
self::maskInternalCostIfNeeded($out, $adminInfo);
|
||||
self::maskRemarkExtraIfNeeded($out, $adminInfo);
|
||||
self::attachLinkedPayOrders($out);
|
||||
@@ -1737,7 +1770,9 @@ class PrescriptionOrderLogic
|
||||
|
||||
self::writeLog((int) $order->id, $adminId, $adminInfo, 'edit', '编辑了业务订单及相关信息');
|
||||
|
||||
$out = $order->toArray();
|
||||
self::refreshAgencyCollectSnapshot((int) $order->id);
|
||||
|
||||
$out = PrescriptionOrder::where('id', (int) $order->id)->find()->toArray();
|
||||
self::maskInternalCostIfNeeded($out, $adminInfo);
|
||||
self::maskRemarkExtraIfNeeded($out, $adminInfo);
|
||||
self::attachLinkedPayOrders($out);
|
||||
@@ -2281,12 +2316,17 @@ class PrescriptionOrderLogic
|
||||
$order->payment_slip_audit_remark = '';
|
||||
self::syncFulfillmentStatus($order);
|
||||
|
||||
// 将关联的已支付支付单(status=2,且payment_method='manual')恢复为待审核(status=5)
|
||||
// 将关联的已支付补齐支付单恢复为待审核:兼容旧 manual 与新 fubei/create_type=fubei 两种标记
|
||||
$payOrderIds = self::linkedPayOrderIdList($id);
|
||||
if (!empty($payOrderIds)) {
|
||||
Order::whereIn('id', $payOrderIds)
|
||||
->where('status', 2) // 只恢复已支付状态的
|
||||
->where('payment_method', 'manual') // 只恢复手动创建的
|
||||
->where(function ($query) {
|
||||
$query->where('payment_method', 'manual')
|
||||
->whereOr('payment_method', 'fubei')
|
||||
->whereOr('create_type', 'fubei')
|
||||
->whereOr('create_type', 'express_cod');
|
||||
})
|
||||
->update([
|
||||
'status' => 5, // 待审核
|
||||
'payment_time' => null
|
||||
@@ -2364,6 +2404,9 @@ class PrescriptionOrderLogic
|
||||
$amount = round((float) ($params['pay_amount'] ?? 0), 2);
|
||||
$remark = mb_substr(trim((string) ($params['pay_remark'] ?? '')), 0, 200);
|
||||
$completionRequest = (int) ($params['completion_request'] ?? 0);
|
||||
// 创建方式:付呗(默认) 或 快递代收(express_cod)
|
||||
$payCreateType = (string) ($params['pay_create_type'] ?? 'fubei');
|
||||
$isExpressCod = $payCreateType === 'express_cod';
|
||||
|
||||
if ($amount < 0) {
|
||||
self::$error = '支付单金额不能为负数';
|
||||
@@ -2378,7 +2421,13 @@ class PrescriptionOrderLogic
|
||||
$payOrder->order_type = $orderType;
|
||||
$payOrder->amount = $amount;
|
||||
$payOrder->status = 5; // 待审核
|
||||
$payOrder->payment_method = 'manual';
|
||||
if ($isExpressCod) {
|
||||
// 快递代收:payment_method 留空,审核通过/到账后再写;用 create_type 标记
|
||||
$payOrder->create_type = 'express_cod';
|
||||
} else {
|
||||
$payOrder->payment_method = 'fubei'; // 补齐支付单按「付呗」记账
|
||||
$payOrder->create_type = 'fubei';
|
||||
}
|
||||
$payOrder->payment_time = null; // 审核通过后再设置支付时间
|
||||
$payOrder->remark = $remark;
|
||||
|
||||
@@ -2420,7 +2469,9 @@ class PrescriptionOrderLogic
|
||||
}
|
||||
self::writeLog($id, $adminId, $adminInfo, 'add_pay_order', $logMsg);
|
||||
|
||||
$out = $order->toArray();
|
||||
self::refreshAgencyCollectSnapshot($id);
|
||||
|
||||
$out = PrescriptionOrder::where('id', $id)->find()->toArray();
|
||||
self::maskInternalCostIfNeeded($out, $adminInfo);
|
||||
self::maskRemarkExtraIfNeeded($out, $adminInfo);
|
||||
self::attachLinkedPayOrders($out);
|
||||
@@ -2531,7 +2582,9 @@ class PrescriptionOrderLogic
|
||||
}
|
||||
self::writeLog($id, $adminId, $adminInfo, 'link_pay_order', $logMsg);
|
||||
|
||||
$out = $order->toArray();
|
||||
self::refreshAgencyCollectSnapshot($id);
|
||||
|
||||
$out = PrescriptionOrder::where('id', $id)->find()->toArray();
|
||||
self::maskInternalCostIfNeeded($out, $adminInfo);
|
||||
self::maskRemarkExtraIfNeeded($out, $adminInfo);
|
||||
self::attachLinkedPayOrders($out);
|
||||
@@ -3415,7 +3468,11 @@ class PrescriptionOrderLogic
|
||||
$item['export_refund_amount'] = $refundStored > 0
|
||||
? number_format($refundStored, 2, '.', '')
|
||||
: '';
|
||||
$item['export_agency_collect'] = number_format(round($amt - $paid, 2), 2, '.', '');
|
||||
// 需代收:优先用固定快照值 agency_collect_amount;历史单为 NULL 时回退实时 amt − paid
|
||||
$agencySnap = $item['agency_collect_amount'] ?? null;
|
||||
$item['export_agency_collect'] = ($agencySnap !== null && $agencySnap !== '')
|
||||
? number_format(round((float) $agencySnap, 2), 2, '.', '')
|
||||
: number_format(round($amt - $paid, 2), 2, '.', '');
|
||||
|
||||
$item['export_tracking_number'] = (string) ($item['tracking_number'] ?? '');
|
||||
$signTs = $poId > 0 ? (int) ($signTsByPoId[$poId] ?? 0) : 0;
|
||||
@@ -4149,6 +4206,9 @@ class PrescriptionOrderLogic
|
||||
return false;
|
||||
}
|
||||
|
||||
// 总金额变化后刷新「需代收」固定快照,保持与新总额一致
|
||||
self::refreshAgencyCollectSnapshot($id);
|
||||
|
||||
// 记录日志
|
||||
$summary = sprintf('将订单金额从 ¥%.2f 修改为 ¥%.2f', $oldAmount, $newAmount);
|
||||
self::writeLog($id, $adminId, $adminInfo, 'update_amount', $summary);
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
import r from"./error-Baib8GsN.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-lurTijPg.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-HOEUG8sr.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-DKxtBL_b.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-DzAM_8sh.js";import{f as p,ak as i,I as m,a as e,aN as s,J as o}from"./@vue/runtime-core-C6bnekPw.js";import"./element-plus-DM-pfG5J.js";import"./@vue/runtime-dom-DDAG46FW.js";import"./lodash-D3kF6u-c.js";import"./@vue/shared-mAAVTE9n.js";import"./@vue/reactivity-DiY1c2vO.js";import"./@element-plus/icons-vue-B0jSCQ-G.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./vue-router-BQ5Mf_ti.js";import"./index-CaVLWczr.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./@vueuse/core-DfvfIixE.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";const a="/admin/assets/no_perms-jDxcYpYC.png",n={class:"error404"},W=p({__name:"403",setup(c){return(_,t)=>(i(),m("div",n,[e(r,{code:"403",title:"您的账号权限不足,请联系管理员添加权限!","show-btn":!1},{content:s(()=>[...t[0]||(t[0]=[o("div",{class:"flex justify-center"},[o("img",{class:"w-[150px] h-[150px]",src:a,alt:""})],-1)])]),_:1})]))}});export{W as default};
|
||||
@@ -1 +0,0 @@
|
||||
import o from"./error-Baib8GsN.js";import{f as r,ak as t,I as m,a as p}from"./@vue/runtime-core-C6bnekPw.js";import"./element-plus-lurTijPg.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-HOEUG8sr.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-DKxtBL_b.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};
|
||||
@@ -0,0 +1 @@
|
||||
import o from"./error-DzAM_8sh.js";import{f as r,ak as t,I as m,a as p}from"./@vue/runtime-core-C6bnekPw.js";import"./element-plus-DM-pfG5J.js";import"./@vue/runtime-dom-DDAG46FW.js";import"./lodash-D3kF6u-c.js";import"./@vue/shared-mAAVTE9n.js";import"./@vue/reactivity-DiY1c2vO.js";import"./@element-plus/icons-vue-B0jSCQ-G.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./vue-router-BQ5Mf_ti.js";import"./index-CaVLWczr.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./@vueuse/core-DfvfIixE.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";const i={class:"error404"},T=r({__name:"404",setup(e){return(a,s)=>(t(),m("div",i,[p(o,{code:"404",title:"哎呀,出错了!您访问的页面不存在…"})]))}});export{T as default};
|
||||
Executable → Regular
+1
-1
File diff suppressed because one or more lines are too long
Executable → Regular
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
@@ -1 +1 @@
|
||||
import{M as T,N as C,T as $,r as D,d as L,L as k}from"./element-plus-lurTijPg.js";import{W as E}from"./@element-plus/icons-vue-HOEUG8sr.js";import{a4 as P}from"./tcm-GRQUYn2R.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-DKxtBL_b.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 q={class:"assign-log-panel"},z={key:1,class:"text-gray-400"},G=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",q,[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",z,"否"))]),_: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(G,[["__scopeId","data-v-f670e3e6"]]);export{Ct as default};
|
||||
import{M as T,N as C,T as $,d as D,f as L,L as k}from"./element-plus-DM-pfG5J.js";import{Y as E}from"./@element-plus/icons-vue-B0jSCQ-G.js";import{a5 as P}from"./tcm-DREvQype.js";import{f as A,w as B,ak as p,I as b,aP as F,G as h,aN as n,a as i,O as m,J as M}from"./@vue/runtime-core-C6bnekPw.js";import{Q as c}from"./@vue/shared-mAAVTE9n.js";import{y as V,n as w}from"./@vue/reactivity-DiY1c2vO.js";import{_ as K}from"./index-CaVLWczr.js";import"./@vue/runtime-dom-DDAG46FW.js";import"./lodash-D3kF6u-c.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./vue-router-BQ5Mf_ti.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./@vueuse/core-DfvfIixE.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";const Y={class:"assign-log-panel"},q={key:1,class:"text-gray-400"},z=A({__name:"AssignLogPanel",props:{diagnosisId:{}},setup(N,{expose:v}){const _=N,d=w(!1),u=w([]);function y(o){const e=o.related_po_creator_name;if(e!=null&&String(e).trim()!=="")return String(e);const t=Number(o.related_po_creator_id);return Number.isFinite(t)&&t>0?`ID:${t}`:"—"}function x(o){const e=o.related_po_create_time_text;if(e!=null&&String(e).trim()!=="")return String(e);const t=Number(o.related_po_create_time);if(!Number.isFinite(t)||t<=0)return"—";const a=new Date(t*1e3);if(Number.isNaN(a.getTime()))return"—";const r=l=>String(l).padStart(2,"0");return`${a.getFullYear()}-${r(a.getMonth()+1)}-${r(a.getDate())} ${r(a.getHours())}:${r(a.getMinutes())}:${r(a.getSeconds())}`}function f(o,e){const t=e==="from"?"from_assistant_name":"to_assistant_name",a=e==="from"?"from_assistant_id":"to_assistant_id",r=o[t];if(r!=null&&String(r).trim()!==""&&String(r)!=="—")return String(r);const l=Number(o[a]);return Number.isFinite(l)&&l>0?`ID:${l}`:"—"}const g=async()=>{if(_.diagnosisId){d.value=!0;try{const o=await P({id:_.diagnosisId}),e=Array.isArray(o)?o:[];u.value=e}catch(o){console.error(o),u.value=[]}finally{d.value=!1}}};return B(()=>_.diagnosisId,()=>{g()},{immediate:!0}),v({refresh:g}),(o,e)=>{const t=C,a=$,r=L,l=D,S=T,I=k;return p(),b("div",Y,[F((p(),h(S,{data:u.value,border:"",stripe:"","empty-text":"暂无指派记录"},{default:n(()=>[i(t,{label:"操作时间",width:"175",prop:"create_time_text"}),i(t,{label:"原医助","min-width":"120"},{default:n(({row:s})=>[m(c(f(s,"from")),1)]),_:1}),i(t,{label:"新医助","min-width":"120"},{default:n(({row:s})=>[m(c(f(s,"to")),1)]),_:1}),i(t,{label:"继承",width:"72",align:"center"},{default:n(({row:s})=>[Number(s.is_inherit)===1?(p(),h(a,{key:0,type:"success",size:"small"},{default:n(()=>[...e[0]||(e[0]=[m("是",-1)])]),_:1})):(p(),b("span",q,"否"))]),_:1}),i(t,{label:"快照·业务单创建人","min-width":"130","show-overflow-tooltip":""},{default:n(({row:s})=>[m(c(y(s)),1)]),_:1}),i(t,{label:"快照·业务单创建时间",width:"190"},{header:n(()=>[e[1]||(e[1]=M("span",null,"快照·业务单创建时间",-1)),i(l,{placement:"top",content:"related_po_create_time:触发本次操作的处方业务订单 create_time;与原文助一致时表示医助创建订单时间"},{default:n(()=>[i(r,{class:"assign-log-col-hint"},{default:n(()=>[i(V(E))]),_:1})]),_:1})]),default:n(({row:s})=>[m(c(x(s)),1)]),_:1}),i(t,{label:"操作人",width:"110",prop:"operator_name"}),i(t,{label:"操作账号",width:"120",prop:"operator_account","show-overflow-tooltip":""}),i(t,{label:"IP",width:"130",prop:"ip","show-overflow-tooltip":""})]),_:1},8,["data"])),[[I,d.value]])])}}}),Ct=K(z,[["__scopeId","data-v-f670e3e6"]]);export{Ct as default};
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
@@ -1 +1 @@
|
||||
import{i as L,L as T,N as V,T as D,M as z,W as M}from"./element-plus-lurTijPg.js";import{ac as O}from"./tcm-GRQUYn2R.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-DKxtBL_b.js";import"./@vue/runtime-dom-DDAG46FW.js";import"./lodash-D3kF6u-c.js";import"./@element-plus/icons-vue-HOEUG8sr.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{l as L,L as T,N as V,T as D,M as z,W as M}from"./element-plus-DM-pfG5J.js";import{ad as O}from"./tcm-DREvQype.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-CaVLWczr.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-BQ5Mf_ti.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./@vueuse/core-DfvfIixE.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";const 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};
|
||||
+2
-2
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
@@ -1 +1 @@
|
||||
import{_ as o}from"./MediaSourceSelect.vue_vue_type_script_setup_true_lang-BOQUTiLN.js";import"./element-plus-lurTijPg.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-HOEUG8sr.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-C3cIqxhZ.js";import"./element-plus-DM-pfG5J.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
@@ -1 +1 @@
|
||||
import{Q as g,P as V}from"./element-plus-lurTijPg.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-DM-pfG5J.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 _};
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+2
-2
@@ -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-lurTijPg.js";import{_ as pe}from"./picker-CgNjB8GY.js";import{e as ue,c as ce,i as g,_ as ge}from"./index-DKxtBL_b.js";import{s as U}from"./@vue/runtime-dom-DDAG46FW.js";import{b as z,G as fe}from"./@element-plus/icons-vue-HOEUG8sr.js";import{a as P,d as ve}from"./patient-CWXcCXvQ.js";import{h as _e}from"./perm-cJuZHqXB.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-DM0Uukea.js";import"./index-CHT25lVJ.js";import"./index.vue_vue_type_script_setup_true_lang-CB1JgtXC.js";import"./index-tZCDKLYp.js";import"./index-BfzWyWIF.js";import"./cos-js-sdk-v5-DSMN0LUT.js";import"./file-DXygoiIs.js";import"./index.vue_vue_type_script_setup_true_lang-SfFZFG0h.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};
|
||||
import{l as se,W as ne,C as le,V as ae,e as re,f as me,a as de}from"./element-plus-DM-pfG5J.js";import{_ as pe}from"./picker-BVR108QV.js";import{e as ue,c as ce,h as g,_ as ge}from"./index-CaVLWczr.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-3j0u_1ZH.js";import{h as _e}from"./perm-DpuQ-hbC.js";import{f as ye,w as he,ak as i,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-Dtw-XGAA.js";import"./index-D7o6UQWd.js";import"./index.vue_vue_type_script_setup_true_lang-BxXLvzbM.js";import"./index-DXYzjdn6.js";import"./index-IKz52fU_.js";import"./cos-js-sdk-v5-DSMN0LUT.js";import"./file-fmh9V7UH.js";import"./index.vue_vue_type_script_setup_true_lang-ChsiNjV-.js";import"./@vueuse/core-DfvfIixE.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-BQ5Mf_ti.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./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,oe=le,ie=ae;return i(),n("div",we,[!d.readonly&&d.diagnosisId?(i(),n("div",Ie,[X.value?(i(),k(s,{key:0,type:"primary",plain:"",size:"small",onClick:e[0]||(e[0]=o=>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]=o=>S.value=o),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]=o=>A.value=o),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?(i(),n("div",Ve,[(i(!0),n(w,null,E(d.notes,o=>{var L,R;return i(),n("div",{key:o.id,class:"timeline-node"},[e[11]||(e[11]=m("div",{class:"timeline-dot"},null,-1)),m("div",Ee,T(o.note_date),1),m("div",Ne,[o.content?(i(),n("div",be,[(i(!0),n(w,null,E(q(o.content),(a,c)=>(i(),n("div",{key:c,class:"content-line"},T(a),1))),128))])):p("",!0),(L=o.tongue_images)!=null&&L.length?(i(),n("div",Se,[e[9]||(e[9]=m("span",{class:"images-label"},"舌苔照片",-1)),(i(!0),n(w,null,E(o.tongue_images,(a,c)=>(i(),n("div",{key:c,class:"thumb-wrap"},[l(G,{src:_(a),"preview-src-list":o.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):(i(),k(V,{key:0,class:"thumb-delete",onClick:U(W=>B(o.id,"tongue_images",a),["stop"])},{default:r(()=>[l(N(z))]),_:1},8,["onClick"]))]))),128))])):p("",!0),(R=o.report_files)!=null&&R.length?(i(),n("div",Ae,[e[10]||(e[10]=m("span",{class:"images-label"},"检查报告",-1)),(i(!0),n(w,null,E(o.report_files,(a,c)=>(i(),n(w,{key:c},[D(a)?(i(),n("div",De,[l(G,{src:_(a),"preview-src-list":Q(o.report_files),"initial-index":Z(o.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):(i(),k(V,{key:0,class:"thumb-delete",onClick:U(W=>B(o.id,"report_files",a),["stop"])},{default:r(()=>[l(N(z))]),_:1},8,["onClick"]))])):(i(),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):(i(),k(V,{key:0,class:"file-delete",onClick:U(W=>B(o.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?(i(),k(te,{key:2,description:"暂无备注","image-size":48})):p("",!0),l(ie,{modelValue:v.value,"onUpdate:modelValue":e[5]||(e[5]=o=>v.value=o),title:"添加备注",width:"480px","append-to-body":""},{footer:r(()=>[l(s,{onClick:e[4]||(e[4]=o=>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(oe,{modelValue:C.value,"onUpdate:modelValue":e[3]||(e[3]=o=>C.value=o),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
File diff suppressed because one or more lines are too long
+1
-1
@@ -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-DKxtBL_b.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-lurTijPg.js";import"./@vue/runtime-dom-DDAG46FW.js";import"./@element-plus/icons-vue-HOEUG8sr.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-CaVLWczr.js";import"./lodash-D3kF6u-c.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./vue-router-BQ5Mf_ti.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./element-plus-DM-pfG5J.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"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./@vueuse/core-DfvfIixE.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";const 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};
|
||||
@@ -0,0 +1 @@
|
||||
import{W as B,M as A,N as R,T as j,l as q,L as z}from"./element-plus-DM-pfG5J.js";import{_ as $}from"./index.vue_vue_type_script_setup_true_lang-BxXLvzbM.js";import{u as G}from"./usePaging-VsbTxSU0.js";import{q as J}from"./tcm-DREvQype.js";import{b as M,c as Q,d as S,P as U}from"./PrescriptionOrderDetailDrawer-C6mzrFGm.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-CaVLWczr.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-BQ5Mf_ti.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./@vueuse/core-DfvfIixE.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";const 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 P=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 T=B,s=R,E=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(P(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(E,{type:n(M)(t.fulfillment_status),size:"small",effect:"light"},{default:a(()=>[p(d(n(Q)(t.fulfillment_status)),1)]),_:2},1032,["type"])]),_:1}),e(s,{label:"创建时间","min-width":"170"},{default:a(({row:t})=>[p(d(n(S)(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(T,{description:"当前诊单未携带患者ID,无法列出业务订单"})])),e(U,{ref_key:"detailDrawerRef",ref:v,readonly:"","append-to-body":""},null,512)])}}}),Ht=et(st,[["__scopeId","data-v-4a0ef613"]]);export{Ht as default};
|
||||
@@ -0,0 +1 @@
|
||||
.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 @@
|
||||
.patient-order-list[data-v-2e08802c]{padding:4px 0}.po-empty-tip[data-v-2e08802c]{padding:24px 0}.audit-stamp[data-v-2e08802c]{position:absolute;top:12px;right:16px;transform:rotate(15deg);opacity:.15;pointer-events:none;z-index:1}.stamp-inner[data-v-2e08802c]{border:3px solid currentColor;border-radius:8px;padding:4px 12px;font-size:18px;font-weight:700}.stamp-pass[data-v-2e08802c]{color:#22c55e}.stamp-reject[data-v-2e08802c]{color:#ef4444}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
@charset "UTF-8";.po-detail-drawer[data-v-e14cbef6] .el-drawer__header{margin-bottom:0;padding:16px 24px;border-bottom:1px solid var(--el-border-color-lighter)}.stat-card[data-v-e14cbef6]{border-radius:8px}.stat-title[data-v-e14cbef6]{font-weight:500}.po-panel[data-v-e14cbef6]{border-radius:8px;transition:all .3s}.po-panel[data-v-e14cbef6] .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-e14cbef6] .el-card__body{padding:16px}.po-desc[data-v-e14cbef6] .el-descriptions__label{width:120px;color:var(--el-text-color-regular)}.po-audit-remark[data-v-e14cbef6]{color:var(--el-color-danger);font-weight:600;white-space:pre-wrap;word-break:break-word}.audit-stamp[data-v-e14cbef6]{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-e14cbef6]: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-e14cbef6]{text-align:center;line-height:1.1}.stamp-pass[data-v-e14cbef6]{color:var(--el-color-success)}.stamp-reject[data-v-e14cbef6]{color:var(--el-color-danger)}.po-diagnosis-creator-dept-breadcrumb[data-v-e14cbef6] .el-breadcrumb__item{display:inline-flex;float:none}.po-diagnosis-creator-dept-breadcrumb[data-v-e14cbef6] .el-breadcrumb__separator{margin:0 2px 0 4px}
|
||||
+1
-1
@@ -1 +1 @@
|
||||
import{S as L}from"./element-plus-lurTijPg.js";import B from"./RecordingVideoPlayer-C_z4yRwZ.js";import{e as H,_ as I}from"./index-DKxtBL_b.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-HOEUG8sr.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-DM-pfG5J.js";import B from"./RecordingVideoPlayer-Cy6aqokN.js";import{e as H,_ as I}from"./index-CaVLWczr.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-BQ5Mf_ti.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./@vueuse/core-DfvfIixE.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";const 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};
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+2
-2
@@ -1,2 +1,2 @@
|
||||
import{C as B,i as I,W as w}from"./element-plus-lurTijPg.js";import{V as T}from"./tcm-GRQUYn2R.js";import{i as C,_ as E}from"./index-DKxtBL_b.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-HOEUG8sr.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=B,b=I,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),(V,x)=>(t(),e("div",{key:x,class:"content-line"},f(V),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};
|
||||
import{C as I,l as V,W as w}from"./element-plus-DM-pfG5J.js";import{W as T}from"./tcm-DREvQype.js";import{h as C,_ as E}from"./index-CaVLWczr.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-BQ5Mf_ti.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./@vueuse/core-DfvfIixE.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";const L={class:"tracking-timeline-wrap"},M={key:0,class:"timeline-input"},W={class:"input-actions"},A={key:1,class:"tracking-timeline"},D={class:"timeline-date"},G={class:"timeline-body"},J={key:0,class:"timeline-content"},O=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",W,[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",A,[(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",D,f(s.note_date),1),l("div",G,[s.content?(t(),e("div",J,[(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(O,[["__scopeId","data-v-82b635bd"]]);export{Tt as default};
|
||||
+1
-1
@@ -1 +1 @@
|
||||
import{_ as o}from"./account-adjust.vue_vue_type_script_setup_true_lang-DTYNkDkC.js";import"./element-plus-lurTijPg.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-HOEUG8sr.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-DM0Uukea.js";import"./index-DKxtBL_b.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-D5yF-zri.js";import"./element-plus-DM-pfG5J.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-Dtw-XGAA.js";import"./index-CaVLWczr.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./vue-router-BQ5Mf_ti.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./@vueuse/core-DfvfIixE.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";export{o as default};
|
||||
+1
-1
@@ -1 +1 @@
|
||||
import{D as h,B as q,G as B,I,C as D}from"./element-plus-lurTijPg.js";import{_ as F}from"./index-DM0Uukea.js";import{i as b}from"./index-DKxtBL_b.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-DM-pfG5J.js";import{_ as F}from"./index-Dtw-XGAA.js";import{h as b}from"./index-CaVLWczr.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 _};
|
||||
+1
-1
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 +0,0 @@
|
||||
import{_ as o}from"./add-nav.vue_vue_type_script_setup_true_lang-Bm4K3gzC.js";import"./element-plus-lurTijPg.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-HOEUG8sr.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-tZCDKLYp.js";import"./index-DKxtBL_b.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-DGrppj2G.js";import"./index-DM0Uukea.js";import"./index.vue_vue_type_script_setup_true_lang-CB1JgtXC.js";import"./article-DgkRr8ir.js";import"./usePaging-VsbTxSU0.js";import"./picker-CgNjB8GY.js";import"./index-CHT25lVJ.js";import"./index-BfzWyWIF.js";import"./cos-js-sdk-v5-DSMN0LUT.js";import"./file-DXygoiIs.js";import"./index.vue_vue_type_script_setup_true_lang-SfFZFG0h.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};
|
||||
@@ -0,0 +1 @@
|
||||
import{_ as o}from"./add-nav.vue_vue_type_script_setup_true_lang-BtFyoC78.js";import"./element-plus-DM-pfG5J.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-DXYzjdn6.js";import"./index-CaVLWczr.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./vue-router-BQ5Mf_ti.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./@vueuse/core-DfvfIixE.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";import"./picker-GJ0IKFTv.js";import"./index-Dtw-XGAA.js";import"./index.vue_vue_type_script_setup_true_lang-BxXLvzbM.js";import"./article-C_n-ZYdZ.js";import"./usePaging-VsbTxSU0.js";import"./picker-BVR108QV.js";import"./index-D7o6UQWd.js";import"./index-IKz52fU_.js";import"./cos-js-sdk-v5-DSMN0LUT.js";import"./file-fmh9V7UH.js";import"./index.vue_vue_type_script_setup_true_lang-ChsiNjV-.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 +0,0 @@
|
||||
import{C as E,B,g as C,i as N}from"./element-plus-lurTijPg.js";import{_ as $}from"./index-tZCDKLYp.js";import{_ as z}from"./picker-DGrppj2G.js";import{_ as A}from"./picker-CgNjB8GY.js";import{c as D,i as r}from"./index-DKxtBL_b.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 _};
|
||||
@@ -0,0 +1 @@
|
||||
import{C as E,B,j as C,l as N}from"./element-plus-DM-pfG5J.js";import{_ as $}from"./index-DXYzjdn6.js";import{_ as z}from"./picker-GJ0IKFTv.js";import{_ as A}from"./picker-BVR108QV.js";import{c as D,h as r}from"./index-CaVLWczr.js";import{D as I}from"./vuedraggable-5bKFmC7X.js";import{f as R,ak as p,I as j,J as l,a,aN as d,G as F,O as G,A as J}from"./@vue/runtime-core-C6bnekPw.js";import{y as c,a as L}from"./@vue/reactivity-DiY1c2vO.js";const O={class:"bg-fill-light flex items-center w-full p-4 mb-4"},P={class:"upload-btn w-[60px] h-[60px]"},S={class:"ml-3 flex-1"},T={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=J({get(){return t.modelValue},set(s){V("update:modelValue",s)}}),x=()=>{var s;((s=t.modelValue)==null?void 0:s.length)<t.max?m.value.push({image:"",name:"导航名称",link:{},is_show:"1"}):r.msgError(`最多添加${t.max}个`)},v=s=>{var e;if(((e=t.modelValue)==null?void 0:e.length)<=t.min)return r.msgError(`最少保留${t.min}个`);m.value.splice(s,1)};return(s,e)=>{const i=D,g=A,h=E,k=z,b=C,w=B,y=$,U=N;return p(),j("div",null,[l("div",null,[a(c(I),{class:"draggable",modelValue:c(m),"onUpdate:modelValue":e[0]||(e[0]=o=>L(m)?m.value=o:null),animation:"300",handle:".drag-move","item-key":"index"},{item:d(({element:o,index:u})=>[(p(),F(y,{class:"w-[467px]",key:u,onClose:n=>v(u)},{default:d(()=>[l("div",O,[a(g,{modelValue:o.image,"onUpdate:modelValue":n=>o.image=n,"upload-class":"bg-body",size:"60px","exclude-domain":""},{upload:d(()=>[l("div",P,[a(i,{name:"el-icon-Plus",size:20})])]),_:1},8,["modelValue","onUpdate:modelValue"]),l("div",S,[l("div",T,[e[1]||(e[1]=l("span",{class:"text-tx-regular flex-none mr-3"},"名称",-1)),a(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]=[G("添加",-1)])]),_:1})])])}}});export{oe as _};
|
||||
+1
-1
@@ -1 +1 @@
|
||||
import{r as n}from"./index-DKxtBL_b.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-CaVLWczr.js";function e(t){return n.get({url:"/auth.admin/lists",params:t},{ignoreCancelToken:!0})}function i(t){return n.post({url:"/auth.admin/add",params:t})}function r(t){return n.post({url:"/auth.admin/edit",params:t})}function u(t){return n.post({url:"/auth.admin/delete",params:t})}function d(t){return n.get({url:"/auth.admin/detail",params:t})}export{e as a,r as b,u as c,i as d,d as e};
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
@@ -1 +1 @@
|
||||
import{r as e}from"./index-DKxtBL_b.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-CaVLWczr.js";function a(t){return e.get({url:"/article.articleCate/lists",params:t})}function l(t){return e.get({url:"/article.articleCate/all",params:t})}function i(t){return e.post({url:"/article.articleCate/add",params:t})}function c(t){return e.post({url:"/article.articleCate/edit",params:t})}function u(t){return e.post({url:"/article.articleCate/delete",params:t})}function n(t){return e.get({url:"/article.articleCate/detail",params:t})}function s(t){return e.post({url:"/article.articleCate/updateStatus",params:t})}function o(t){return e.get({url:"/article.article/lists",params:t})}function d(t){return e.post({url:"/article.article/add",params:t})}function f(t){return e.post({url:"/article.article/edit",params:t})}function C(t){return e.post({url:"/article.article/delete",params:t})}function p(t){return e.get({url:"/article.article/detail",params:t})}function g(t){return e.post({url:"/article.article/updateStatus",params:t})}export{a,u as b,s as c,c as d,i as e,n as f,p as g,f as h,d as i,l as j,o as k,g as l,C as m};
|
||||
+1
-1
@@ -1 +1 @@
|
||||
import{r as e}from"./index-DKxtBL_b.js";function r(s){return e.get({url:"/asset.AssetUser/lists",params:s})}function u(s){return e.post({url:"/asset.AssetUser/add",params:s})}function a(s){return e.post({url:"/asset.AssetUser/edit",params:s})}function i(s){return e.post({url:"/asset.AssetUser/delete",params:s})}function o(s){return e.get({url:"/asset.AssetResource/lists",params:s})}function n(s){return e.post({url:"/asset.AssetResource/add",params:s})}function A(s){return e.post({url:"/asset.AssetResource/edit",params:s})}function c(s){return e.post({url:"/asset.AssetResource/delete",params:s})}export{o as a,c as b,A as c,n as d,r as e,a as f,i as g,u as h};
|
||||
import{r as e}from"./index-CaVLWczr.js";function r(s){return e.get({url:"/asset.AssetUser/lists",params:s})}function u(s){return e.post({url:"/asset.AssetUser/add",params:s})}function a(s){return e.post({url:"/asset.AssetUser/edit",params:s})}function i(s){return e.post({url:"/asset.AssetUser/delete",params:s})}function o(s){return e.get({url:"/asset.AssetResource/lists",params:s})}function n(s){return e.post({url:"/asset.AssetResource/add",params:s})}function A(s){return e.post({url:"/asset.AssetResource/edit",params:s})}function c(s){return e.post({url:"/asset.AssetResource/delete",params:s})}export{o as a,c as b,A as c,n as d,r as e,a as f,i as g,u as h};
|
||||
@@ -1 +0,0 @@
|
||||
import{_ as o}from"./attr.vue_vue_type_script_setup_true_lang-DpnLsrQ5.js";import"./element-plus-lurTijPg.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-HOEUG8sr.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-tZCDKLYp.js";import"./index-DKxtBL_b.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-DGrppj2G.js";import"./index-DM0Uukea.js";import"./index.vue_vue_type_script_setup_true_lang-CB1JgtXC.js";import"./article-DgkRr8ir.js";import"./usePaging-VsbTxSU0.js";import"./picker-CgNjB8GY.js";import"./index-CHT25lVJ.js";import"./index-BfzWyWIF.js";import"./cos-js-sdk-v5-DSMN0LUT.js";import"./file-DXygoiIs.js";import"./index.vue_vue_type_script_setup_true_lang-SfFZFG0h.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};
|
||||
@@ -0,0 +1 @@
|
||||
import{_ as o}from"./attr.vue_vue_type_script_setup_true_lang-Cl3rDtpI.js";import"./element-plus-DM-pfG5J.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"./add-nav.vue_vue_type_script_setup_true_lang-BtFyoC78.js";import"./index-DXYzjdn6.js";import"./index-CaVLWczr.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./vue-router-BQ5Mf_ti.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./@vueuse/core-DfvfIixE.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";import"./picker-GJ0IKFTv.js";import"./index-Dtw-XGAA.js";import"./index.vue_vue_type_script_setup_true_lang-BxXLvzbM.js";import"./article-C_n-ZYdZ.js";import"./usePaging-VsbTxSU0.js";import"./picker-BVR108QV.js";import"./index-D7o6UQWd.js";import"./index-IKz52fU_.js";import"./cos-js-sdk-v5-DSMN0LUT.js";import"./file-fmh9V7UH.js";import"./index.vue_vue_type_script_setup_true_lang-ChsiNjV-.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};
|
||||
@@ -0,0 +1 @@
|
||||
import{_ as o}from"./attr.vue_vue_type_script_setup_true_lang-U4IHoVZJ.js";import"./element-plus-DM-pfG5J.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"./add-nav.vue_vue_type_script_setup_true_lang-BtFyoC78.js";import"./index-DXYzjdn6.js";import"./index-CaVLWczr.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./vue-router-BQ5Mf_ti.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./@vueuse/core-DfvfIixE.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";import"./picker-GJ0IKFTv.js";import"./index-Dtw-XGAA.js";import"./index.vue_vue_type_script_setup_true_lang-BxXLvzbM.js";import"./article-C_n-ZYdZ.js";import"./usePaging-VsbTxSU0.js";import"./picker-BVR108QV.js";import"./index-D7o6UQWd.js";import"./index-IKz52fU_.js";import"./cos-js-sdk-v5-DSMN0LUT.js";import"./file-fmh9V7UH.js";import"./index.vue_vue_type_script_setup_true_lang-ChsiNjV-.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};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user