Compare commits

...
33 Commits
Author SHA1 Message Date
Your Name abc78f4291 更新 2026-06-10 09:49:46 +08:00
Your Name 072c0c3663 Merge branch 'master' into master-6-9 2026-06-09 11:43:25 +08:00
Your Name e9cf65a41c 更新 2026-06-09 11:42:35 +08:00
long af9bd05bb4 update资源分发小程序 2026-06-09 10:31:18 +08:00
Your Name 18f7fb3772 Merge branch 'master' into master-6-9 2026-06-09 10:17:29 +08:00
Your Name 29ab45033d 更新 2026-06-09 10:16:38 +08:00
long 8a1a9ea0ca Merge branch '20260604-long' 2026-06-09 10:13:49 +08:00
Your Name f8ccd8b0af 更新 2026-06-09 10:11:40 +08:00
long 9e019708f2 Merge branch 'master' of https://gitee.com/v_cms/zyt 2026-06-09 10:05:49 +08:00
long 2712c262bd 1 2026-06-09 10:05:44 +08:00
Your Name bd7002e176 更新 2026-06-09 09:42:29 +08:00
Your Name 0be6d06544 更新 2026-06-09 09:31:03 +08:00
long 7a4973470c Merge branch 'codex/latest-appointment-filter' 2026-06-06 15:59:19 +08:00
long e79f3190b8 feat: 支持患者列表按最近挂号筛选
- tcm.diagnosis/lists 新增最近有效挂号时间区间与渠道筛选,lists/count 共用过滤逻辑

- 诊单患者列表更多筛选增加最近挂号时间和渠道,并与顶部挂号快捷筛选互斥

- 明确 daterange-picker change 事件转发,列表补充最近挂号渠道摘要展示
2026-06-06 15:58:23 +08:00
long 1811abc794 完善支付单创建方式链路 2026-06-06 15:25:16 +08:00
long 07abf2abca 支付方式完善 2026-06-05 18:24:41 +08:00
long c76bd5416a 代收金额快照 2026-06-05 17:35:49 +08:00
Your Name 7bb8db4126 更新 2026-06-05 17:22:29 +08:00
Your Name 30b6037752 更新 2026-06-05 17:15:42 +08:00
Your Name 5a235280a1 更新 2026-06-05 17:15:15 +08:00
Your Name b9d36d8d04 更新 2026-06-05 15:51:48 +08:00
Your Name f37310ec50 Merge branch 'master' of https://gitee.com/v_cms/zyt 2026-06-05 15:13:10 +08:00
Your Name a440702c07 更新 2026-06-05 15:12:57 +08:00
long b9486f32fe Merge branch '20260604-long' 2026-06-05 12:02:40 +08:00
long 07018598ad add
素材分发 增加用户备注
add_asset_user_remark.sql
2026-06-05 12:02:35 +08:00
Your Name c554e08f2e 更新 2026-06-05 09:38:31 +08:00
long e6209c8912 Merge branch '20260604-long' 2026-06-04 17:37:29 +08:00
Your Name e49f806c3a 更新 2026-06-04 16:51:46 +08:00
Your Name b566e60624 更新 2026-06-04 16:51:25 +08:00
long 123e9075dd 处方增加禁用修改 2026-06-04 15:11:16 +08:00
Your Name 84eef45553 更新 2026-06-04 14:10:22 +08:00
long cdf8b1f76d udpate 2026-06-04 11:46:32 +08:00
long ff61e815d8 时间范围筛选优化 2026-06-04 10:51:35 +08:00
373 changed files with 5137 additions and 1172 deletions
-4
View File
@@ -26,10 +26,6 @@ bin-release/
/.cursor
/.codex
/.agents
/.claude
/.claude
/.claude
/.kbctx
/server/.spool
/server/.claude
/.spool
-120
View File
@@ -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"
-74
View File
@@ -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"
+1 -1
View File
@@ -1,5 +1,5 @@
import App from './App'
var baseUrl ='https://css.zhenyangtang.com.cn/';
var baseUrl ='https://admin.zhenyangtang.com.cn/';
function joinApiUrl(base, path) {
const b = String(base || '').replace(/\/+$/, '')
+3 -3
View File
@@ -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",
+9 -1
View File
@@ -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

@@ -5,7 +5,7 @@
v-if="src && !imageFailed"
class="food-tile-icon__img"
:src="src"
:mode="size === 'tile' ? 'aspectFill' : 'aspectFit'"
mode="aspectFit"
@error="onImageError"
/>
</view>
@@ -66,11 +66,16 @@ function onImageError() {
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 58%;
height: 58%;
width: 75%;
height: 75%;
z-index: 2;
}
/* 玻璃格上的食材图片:投影增强立体感(参考稿) */
.food-tile-icon--tile .food-tile-icon__img {
filter: drop-shadow(0 6rpx 10rpx rgba(0, 0, 0, 0.16));
}
.food-tile-icon--goal {
width: 40rpx;
height: 40rpx;
@@ -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
/**
* 语音转文字(长按说话):微信小程序 WechatSIH5 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 {
+2 -1
View File
@@ -32,7 +32,8 @@ export function warmUpFoodImgs(foods) {
const list = Array.isArray(foods) ? foods : []
list.forEach((f) => {
const icon = typeof f === 'string' ? f : f?.icon
const url = getFoodImgUrl(icon)
// 优先预热自定义图片素材,缺省回退 emoji PNG
const url = (typeof f === 'object' && f?.img) ? f.img : getFoodImgUrl(icon)
if (!url) return
// #ifdef MP-WEIXIN
try {
+18 -4
View File
@@ -58,7 +58,12 @@ const FOOD_TIP_OVERRIDE = {
apple: '带皮整果,升糖较慢',
milk: '低糖,含蛋白,适量喝好',
fish: '优质蛋白,升糖低',
beef: '蛋白为主,升糖低'
beef: '蛋白为主,升糖低',
milkOats: '燕麦加奶,升糖中等,选无糖燕麦更好',
grainMantou: '杂粮做的,比白馒头稳,仍要控量',
riceNoodleSoup: '米粉升糖快,汤粉要少吃',
friedNoodles: '油多又是精面,升糖快',
centuryEggCongee: '白粥熬得软烂,升糖很快,糖友要少喝'
}
/** 含糖等级通用科普文案 */
@@ -76,6 +81,9 @@ export function getFoodTip(food) {
return GI_TIP_DEFAULT[food.gi] || ''
}
/** 自定义图片素材目录(腾讯云 COS,优先使用真实图片而非 emoji) */
const GAMES_IMG_BASE = 'https://gz-1349751149.cos.ap-guangzhou.myqcloud.com/games'
const RAW_FOODS = [
// 低 GI
{ key: 'lettuce', icon: '🥬', name: '生菜', gi: 'low', val: -3, color: '#8BC34A' },
@@ -120,6 +128,8 @@ const RAW_FOODS = [
{ key: 'coffee', icon: '☕', name: '咖啡', gi: 'mid', val: 2, color: '#6F4E37' },
{ key: 'corn', icon: '🌽', name: '玉米', gi: 'mid', val: 5, color: '#F9C513' },
{ key: 'udon', icon: '🍲', name: '乌冬面', gi: 'mid', val: 5, color: '#D9B88F' },
{ key: 'milkOats', icon: '🥣', name: '奶冲麦片', gi: 'mid', val: 6, color: '#E8D9B5', img: `${GAMES_IMG_BASE}/%E5%A5%B6%E5%86%B2%E9%BA%A6%E7%89%87.png` },
{ key: 'grainMantou', icon: '🫓', name: '杂粮馒头', gi: 'mid', val: 5, color: '#C8A878', img: `${GAMES_IMG_BASE}/%E6%9D%82%E7%B2%AE%E9%A6%92%E5%A4%B4.png` },
// 高 GI
{ key: 'whiteRice', icon: '🍚', name: '白米饭', gi: 'high', val: 18, color: '#E0E0E0' },
@@ -136,7 +146,10 @@ const RAW_FOODS = [
{ key: 'maltose', icon: '🍬', name: '麦芽糖', gi: 'high', val: 24, color: '#FFA726' },
{ key: 'soda', icon: '🥤', name: '汽水', gi: 'high', val: 25, color: '#C62828' },
{ key: 'orangeJuice', icon: '🧃', name: '柳橙汁', gi: 'high', val: 22, color: '#FF9F1C' },
{ key: 'stickyRice', icon: '🍡', name: '糯米饭', gi: 'high', val: 19, color: '#F2A7B8' }
{ key: 'stickyRice', icon: '🍡', name: '糯米饭', gi: 'high', val: 19, color: '#F2A7B8' },
{ key: 'riceNoodleSoup', icon: '🍜', name: '汤粉', gi: 'high', val: 16, color: '#E8C9A0', img: `${GAMES_IMG_BASE}/%E6%B1%A4%E7%B2%89.png` },
{ key: 'friedNoodles', icon: '🍝', name: '炒面', gi: 'high', val: 18, color: '#C8843C', img: `${GAMES_IMG_BASE}/%E7%82%92%E9%9D%A2.png` },
{ key: 'centuryEggCongee', icon: '🥣', name: '皮蛋瘦肉粥', gi: 'high', val: 15, color: '#D9CBB0', img: `${GAMES_IMG_BASE}/%E7%9A%AE%E8%9B%8B%E7%98%A6%E8%82%89%E7%B2%A5.png` }
]
export const FOOD_LIBRARY = RAW_FOODS.map((f, i) => {
@@ -149,7 +162,8 @@ export const FOOD_LIBRARY = RAW_FOODS.map((f, i) => {
clearVal,
giLabel: GI_LABEL[f.gi],
get img() {
return getFoodImgUrl(f.icon)
// 优先使用自定义图片素材,缺省回退到 emoji PNG
return f.img || getFoodImgUrl(f.icon)
}
}
})
@@ -170,6 +184,6 @@ export function cloneFoodType(food) {
giLabel: food.giLabel,
val: food.val,
color: food.color,
img: getFoodImgUrl(food.icon)
img: food.img || getFoodImgUrl(food.icon)
}
}
+10
View File
@@ -2,6 +2,14 @@
* 关卡配置:面向 45+ 的认知练习,节奏放慢、无失败惩罚。
* 每组目标只比上一组略增,步数充裕(仅展示,不会因耗尽判负)。
*/
/**
* 指定关卡固定食材(按 key)。未配置的关卡走随机抽取逻辑。
* 第一关固定为这 5 种带真实图片素材的食物。
*/
const FIXED_LEVEL_FOODS = {
1: ['milkOats', 'grainMantou', 'riceNoodleSoup', 'friedNoodles', 'centuryEggCongee']
}
export function getLevelConfig(levelId) {
const lv = Math.max(1, Math.min(999, Number(levelId) || 1))
const tier = Math.floor((lv - 1) / 5)
@@ -14,6 +22,8 @@ export function getLevelConfig(levelId) {
goalTargets: [6 + tier, 8 + tier],
scoreTarget: 2000 + lv * 300,
startGlucose: 50,
// 固定食材(可选):存在时本关只用这些食物
foods: FIXED_LEVEL_FOODS[lv] || null,
boosters: {
insulin: 3,
fiber: 1,
+380 -92
View File
@@ -125,30 +125,28 @@
</view>
<!-- Header HUD -->
<view class="gg-header gg-glass" :style="headerStyle">
<view class="gg-header" :style="headerStyle">
<!-- 顶部导航与全局状态 -->
<view class="gg-hud-nav" :style="navStyle">
<!-- 侧控制组 -->
<!-- 返回 -->
<view class="gg-hud-btn" hover-class="gg-hud-btn--pressed" @tap="goBack">
<TongjiIcon name="chevron-left" size="md" color="#006c4b" />
</view>
<!-- 绝对居中的关卡 -->
<view class="gg-hud-level-center">
<text class="gg-hud-level-text"> {{ levelConfig.id }} </text>
</view>
<!-- 声音 -->
<view class="gg-hud-control-group">
<view class="gg-hud-btn" hover-class="gg-hud-btn--pressed" @tap="goBack">
<TongjiIcon name="chevron-left" size="md" color="#006c49" />
</view>
<view
class="gg-hud-btn"
:class="{ 'is-muted': !sfxEnabled }"
hover-class="gg-hud-btn--pressed"
@tap="onSoundToggle"
>
<TongjiIcon name="volume" size="md" :color="sfxEnabled ? '#006c49' : '#94a3b8'" />
</view>
</view>
<!-- 绝对居中的关卡与得分 -->
<view class="gg-hud-level-center">
<text class="gg-hud-level-text"> {{ levelConfig.id }} </text>
<view class="gg-hud-score" :class="{ 'is-pop': scoreAnimating }">
<text class="gg-hud-score-val">{{ scoreText }}</text>
<text class="gg-hud-score-unit"></text>
<TongjiIcon name="volume" size="md" :color="sfxEnabled ? '#006c4b' : '#94a3b8'" />
</view>
</view>
</view>
@@ -156,41 +154,40 @@
<!-- 主区棋盘为主辅助信息弱化 -->
<view class="gg-main" :style="mainStyle">
<!-- 仅展示上次消除的食物含糖 -->
<view v-if="gamePhase === 'playing'" class="gg-hud-last-food">
<text class="gg-hud-last-food-kicker">上次消除</text>
<view
v-if="lastClearedFood"
class="gg-hud-last-food-card"
:class="'is-' + lastClearedFood.gi"
>
<view class="gg-hud-last-food-icon">
<!-- 信息横幅默认为玩法提示消除后展示该食物含糖与建议 -->
<view
v-if="gamePhase === 'playing'"
class="gg-info-banner"
:class="lastClearedFood ? 'is-food is-' + lastClearedFood.gi : ''"
>
<!-- 已消除食物含糖 + 建议 -->
<template v-if="lastClearedFood">
<view class="gg-info-banner-icon is-food" :class="'is-' + lastClearedFood.gi">
<FoodTileIcon :food="lastClearedFood.food" size="goal" />
</view>
<view class="gg-hud-last-food-body">
<text class="gg-hud-last-food-name">{{ lastClearedFood.name }}</text>
<view class="gg-hud-last-food-sugar-row">
<text class="gg-hud-last-food-sugar-label">含糖量</text>
<text
class="gg-hud-last-food-gi"
:class="'is-' + lastClearedFood.gi"
:style="{ backgroundColor: lastClearedFood.color }"
>{{ lastClearedFood.giShort }}</text>
<text class="gg-hud-last-food-sugar-hint">{{ lastClearedFood.eatHint }}</text>
<view class="gg-info-banner-body">
<view class="gg-info-food-head">
<text class="gg-info-food-name">{{ lastClearedFood.name }}</text>
<view class="gg-info-food-gi" :class="'is-' + lastClearedFood.gi">
<text class="gg-info-food-gi-text">含糖{{ lastClearedFood.giShort }}</text>
</view>
</view>
<text class="gg-info-food-tip" :class="'is-' + lastClearedFood.gi">{{ lastClearedFood.tip }}</text>
</view>
</view>
<view v-else class="gg-hud-last-food-placeholder">
<text>三连消除后这里显示食物名称与含糖量</text>
</view>
</template>
<!-- 默认玩法提示 -->
<template v-else>
<view class="gg-info-banner-icon">
<TongjiIcon name="info" size="md" color="#ffffff" />
</view>
<view class="gg-info-banner-body">
<text class="gg-info-banner-text"><text class="gg-info-banner-kw">认糖</text>点击食物看含糖量 · 消除红色即过关</text>
</view>
</template>
</view>
<!-- 棋盘 -->
<view class="gg-board-hero">
<view v-if="gamePhase === 'playing'" class="gg-board-hint-inline">
<text class="gg-hint-tag">认糖</text>
<text class="gg-board-hint-text">点食物看含糖 · 消掉红色即过关</text>
</view>
<view class="gg-board-stack" :style="boardStackStyle">
<view class="gg-board-zone">
<view v-if="hintTipVisible" class="gg-no-move-tip">
@@ -257,8 +254,8 @@
</view>
</view>
<!-- 道具栏弱化 -->
<view class="gg-footer gg-glass gg-footer--sub" :style="footerStyle">
<!-- 底部道具栏 -->
<view class="gg-footer" :style="footerStyle">
<view class="gg-boosters">
<view
v-for="booster in boosters"
@@ -268,9 +265,11 @@
:class="{ 'is-disabled': booster.count <= 0 }"
@tap="onBooster(booster)"
>
<view class="gg-booster-count" :class="booster.countClass">{{ booster.count }}</view>
<view class="gg-booster-icon">
<TongjiIcon :name="booster.icon" size="xl" :color="booster.color" />
<view class="gg-booster-tile">
<view class="gg-booster-icon">
<TongjiIcon :name="booster.icon" size="xl" :color="booster.color" />
</view>
<view class="gg-booster-count" :class="booster.countClass">{{ booster.count }}</view>
</view>
<text class="gg-booster-label">{{ booster.label }}</text>
</view>
@@ -290,6 +289,7 @@ import {
getClearVal,
getFoodTip,
formatEatLabel,
getFoodByKey,
GI_SHORT,
GI_COLOR
} from '../data/gameFoodLibrary.js'
@@ -327,8 +327,8 @@ const COL_AXES = Array.from({ length: COLS }, (_, i) => i)
const BOARD_INSET_RPX = 4
/** 棋盘与底部道具栏之间的安全间隙,避免最后一行被遮挡 */
const BOARD_FOOTER_GAP_RPX = 28
const GRID_PAD_RPX = 8
const GRID_GAP_RPX = 8
const GRID_PAD_RPX = 18
const GRID_GAP_RPX = 14
const SWAP_MS = 280
const MATCH_MS = 400
const GRAVITY_MS = 300
@@ -351,6 +351,21 @@ const activeFoods = ref([])
const sessionHighFood = ref(null)
const goalDefs = ref([])
/** 解锁高糖三连所需的有效操作次数(约 8 步内才有机会消除红色) */
const HIGH_UNLOCK_MOVES = 7
/** 解锁前棋盘固定保留的高糖数量(<3 无法三连,故无法消除) */
const HIGH_COUNT_LOCKED = 2
/** 解锁后棋盘至少保留的高糖数量(够凑三连) */
const HIGH_COUNT_UNLOCKED = 3
/** 解锁后高糖数量上限,避免刷屏 */
const HIGH_COUNT_MAX = 6
/** 解锁后每次补格生成高糖的概率 */
const HIGH_SPAWN_P = 0.16
/** 非高糖棋子的最小种类数,保证棋盘有足够变化 */
const MIN_NON_HIGH_TYPES = 4
/** 本局玩家有效操作次数(成功消除的交换计为一次) */
const playerMoves = ref(0)
function pickRandom(pool, n) {
const arr = [...pool]
for (let i = arr.length - 1; i > 0; i--) {
@@ -365,6 +380,38 @@ function pickRandom(pool, n) {
* 低/中各若干 + 仅 1 种高糖;再从中挑 2 种作为收集目标。
*/
function selectSessionFoods() {
// 固定食材关卡(如第一关):只用配置里的这些食物
const fixedKeys = levelConfig.value.foods
if (Array.isArray(fixedKeys) && fixedKeys.length) {
const fixed = fixedKeys.map((k) => getFoodByKey(k)).filter(Boolean)
const fixedHighs = fixed.filter((f) => f.gi === 'high')
const fixedNonHighs = fixed.filter((f) => f.gi !== 'high')
// 每关只允许一种高糖:从固定高糖里随机取 1 种
const fixedHighPick = pickRandom(fixedHighs, 1)
sessionHighFood.value = fixedHighPick[0] ? cloneFoodType(fixedHighPick[0]) : null
// 非高糖种类不足时,用随机低糖补足,保证棋盘有变化
const nonHigh = [...fixedNonHighs]
if (nonHigh.length < MIN_NON_HIGH_TYPES) {
const used = new Set([...nonHigh, ...fixedHighPick].map((f) => f.key))
const extraLows = FOOD_LIBRARY.filter((f) => f.gi === 'low' && !used.has(f.key))
nonHigh.push(...pickRandom(extraLows, MIN_NON_HIGH_TYPES - nonHigh.length))
}
const active = [...nonHigh, ...fixedHighPick]
activeFoods.value = active
warmUpFoodImgs(active)
// 收集目标从非高糖里挑(高糖以“消除即过关”驱动)
const goalPool = nonHigh.length ? nonHigh : active
const goalFoods = pickRandom(goalPool, Math.min(2, goalPool.length))
const fixedTargets = levelConfig.value.goalTargets || [10, 12]
goalDefs.value = goalFoods.map((f, i) => ({ key: f.key, food: cloneFoodType(f), target: fixedTargets[i] ?? 10 }))
rebuildGoals()
return
}
const lows = FOOD_LIBRARY.filter((f) => f.gi === 'low')
const mids = FOOD_LIBRARY.filter((f) => f.gi === 'mid')
const highs = FOOD_LIBRARY.filter((f) => f.gi === 'high')
@@ -554,7 +601,7 @@ const gridStyle = computed(() => {
}
if (w > 0 && h > 0) {
style.width = `${w}px`
style.height = `${h}px`
style.height = `${h+10}px`
}
return style
})
@@ -631,10 +678,6 @@ const goalList = computed(() =>
})
)
const allGoalsDone = computed(() =>
goalList.value.length > 0 && goalList.value.every((g) => g.done)
)
const glucoseStatusText = computed(() => {
if (glucoseLevel.value < 30) return '血糖偏低'
if (glucoseLevel.value > 70) return '血糖过高'
@@ -716,7 +759,15 @@ function updateGridMetrics() {
function randomFood() {
const pool = activeFoods.value.length ? activeFoods.value : FOOD_LIBRARY
const pick = pool[Math.floor(Math.random() * pool.length)]
const highFood = sessionHighFood.value
// 解锁后高糖才会随补格生成;解锁前一律生成非高糖,由 ensureHighPresence 控制高糖数量
const afterUnlock = highFood && playerMoves.value >= HIGH_UNLOCK_MOVES
if (afterUnlock && Math.random() < HIGH_SPAWN_P) {
return cloneFoodType(highFood)
}
const nonHigh = pool.filter((f) => f.gi !== 'high')
const src = nonHigh.length ? nonHigh : pool
const pick = src[Math.floor(Math.random() * src.length)]
return cloneFoodType(pick)
}
@@ -731,6 +782,91 @@ function boardHasHighGiFood() {
return false
}
/** 该位置若变为高糖,是否会与现有高糖形成三连(注入时尽量避免,让玩家自己凑) */
function wouldFormHighRun(r, c) {
const isHigh = (rr, cc) => {
const t = board.value[rr]?.[cc]
return t && !t.isRemoved && t.type.gi === 'high'
}
let h = 1
for (let cc = c - 1; cc >= 0 && isHigh(r, cc); cc--) h++
for (let cc = c + 1; cc < COLS && isHigh(r, cc); cc++) h++
if (h >= 3) return true
let v = 1
for (let rr = r - 1; rr >= 0 && isHigh(rr, c); rr--) v++
for (let rr = r + 1; rr < ROWS && isHigh(rr, c); rr++) v++
return v >= 3
}
function shuffleInPlace(arr) {
for (let i = arr.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[arr[i], arr[j]] = [arr[j], arr[i]]
}
return arr
}
/**
* 维持棋盘上高糖(红色)食物的数量,是“约 8 步才能消除红色”的核心:
* - 解锁前(playerMoves < HIGH_UNLOCK_MOVES)固定保留 2 个,永远无法三连 → 无法消除;
* - 解锁后至少保留 3 个(散开、避免直接成行),玩家凑三连即可消除过关;上限 HIGH_COUNT_MAX。
* 高糖只在此注入/回收,randomFood 解锁前不生成高糖。
*/
function ensureHighPresence() {
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
const highTiles = []
const nonHighTiles = []
for (let r = 0; r < ROWS; r++) {
for (let c = 0; c < COLS; c++) {
const t = board.value[r]?.[c]
if (!t || t.isRemoved) continue
if (t.type.gi === 'high') highTiles.push(t)
else nonHighTiles.push(t)
}
}
if (highTiles.length < minTarget) {
let need = minTarget - highTiles.length
const candidates = shuffleInPlace([...nonHighTiles])
// 优先选不会立即成三连的位置
for (const t of candidates) {
if (need <= 0) break
if (t.type.gi === 'high') continue
if (wouldFormHighRun(t.r, t.c)) continue
t.type = cloneFoodType(highFood)
need--
}
// 仍不够再放宽限制
for (const t of candidates) {
if (need <= 0) break
if (t.type.gi === 'high') continue
t.type = cloneFoodType(highFood)
need--
}
bumpLayout()
} else if (highTiles.length > maxCap) {
const lowMidPool = activeFoods.value.filter((f) => f.gi !== 'high')
let remove = highTiles.length - maxCap
const shuffled = shuffleInPlace([...highTiles])
for (const t of shuffled) {
if (remove <= 0) break
const repl = lowMidPool.length
? lowMidPool[Math.floor(Math.random() * lowMidPool.length)]
: null
if (repl) {
t.type = cloneFoodType(repl)
remove--
}
}
bumpLayout()
}
}
function wouldCreateInitialMatch(r, c, type, currentBoard, currentRow) {
const key = type.key
if (r >= 2 && currentBoard[r - 1][c]?.type.key === key && currentBoard[r - 2][c]?.type.key === key) {
@@ -785,6 +921,11 @@ function initBoard() {
}
ensureBoardFull(next)
commitBoard(next)
// 注入本关唯一高糖(解锁前固定 2 个,无法三连)
ensureHighPresence()
// 注入高糖可能破坏“有解”性:开局静默兜底,避免出现整盘无法消除的死局
// (仅打乱类型、保持高糖数量不变)
if (!boardHasSolution()) shuffleTypesUntilPlayable()
startHintTimer()
}
@@ -846,6 +987,7 @@ function setupLevel(levelId) {
highClearAlertVisible.value = false
clearedHighFoodName.value = ''
lastClearedFood.value = null
playerMoves.value = 0
tileTouchStart = null
clearDragPreview()
selectSessionFoods()
@@ -1284,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 次。
@@ -1292,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)
@@ -1305,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
}
@@ -1439,6 +1589,7 @@ function recordLastClearedFood(groups) {
gi,
giShort: GI_SHORT[gi] || '低糖',
eatHint: formatEatLabel(t),
tip: getFoodTip(t),
color: GI_COLOR[gi] || '#64748b',
food: cloneFoodType(t)
}
@@ -1485,11 +1636,7 @@ function showLearnFx(groups) {
function checkGameEnd() {
if (gamePhase.value !== 'playing') return
// 无失败惩罚:认全目标或已消除场上全部高糖食物即通关
if (allGoalsDone.value) {
endGame('won', { reason: 'goals' })
return
}
// 唯一通关条件:消除本关唯一的高糖(红色)食物
if (sessionHighFood.value && !boardHasHighGiFood()) {
endGame('won', {
reason: 'high_cleared',
@@ -1612,13 +1759,14 @@ async function processSwap(t1, t2, opts = {}) {
const matches = findMatches()
if (matches.length > 0) {
movesLeft.value = Math.max(0, movesLeft.value - 1)
playerMoves.value += 1
const endedByHigh = await clearAndRefill()
if (endedByHigh) {
isProcessing.value = false
return
}
// 所有连消结束后,检查棋盘是否死锁并自动重排
await reshuffleIfStuck()
// 落子算法已保证“下落即有可消除走法”;此处仅作极端兜底(静默、无横幅)
if (!boardHasSolution()) shuffleTypesUntilPlayable()
} else {
await animateSwapOffset(t1, t2)
swapTiles(t1, t2)
@@ -1692,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 = []
@@ -1722,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()
@@ -1859,6 +2137,8 @@ async function onBooster(booster) {
return
}
}
// 恢复本关高糖数量,避免纤维清空高糖直接跳过“8 步”门槛
ensureHighPresence()
// fiber 改变了棋盘,保险检查是否死锁
await reshuffleIfStuck()
@@ -1975,6 +2255,8 @@ async function onBooster(booster) {
await delay(GRAVITY_MS)
clearDropFlags()
// 维持本关高糖数量
ensureHighPresence()
// meal 自带洗牌已保证有解,但再做一次轻量检查保险
await reshuffleIfStuck()
@@ -2023,7 +2305,7 @@ function initLayout() {
paddingTop: `${paddingTop}px`,
paddingLeft: '32rpx',
paddingRight: '32rpx',
paddingBottom: '10rpx'
paddingBottom: '18rpx'
}
navStyle.value = {
paddingRight: `${Math.max(0, headerPaddingRight - uni.upx2px(32))}px`
@@ -2043,19 +2325,25 @@ function recalcBoardSize() {
const query = uni.createSelectorQuery().in(instance?.proxy)
query.select('.gg-header').boundingClientRect()
query.select('.gg-footer').boundingClientRect()
query.select('.gg-board-hero').boundingClientRect()
query.exec((res) => {
try {
const sys = uni.getSystemInfoSync()
const header = res?.[0]
const footer = res?.[1]
const hero = res?.[2]
const headerBottom = Math.ceil(header?.bottom ?? header?.height ?? uni.upx2px(360))
// 棋盘真正可用区从进度条/信息横幅之下(board-hero 顶部)算起
const boardAreaTop = Math.ceil(hero?.top ?? headerBottom)
// 底部道具栏较高,测量失败时用更保守的兜底高度,避免最后一行被遮挡
const footerTop = Math.floor(footer?.top ?? (sys.windowHeight - uni.upx2px(300)))
const inset = uni.upx2px(BOARD_INSET_RPX)
const footerGap = uni.upx2px(BOARD_FOOTER_GAP_RPX)
const zoneW = sys.windowWidth
// gg-main 左右内边距各 20rpx,棋盘可用宽度需相应收窄
const sidePad = uni.upx2px(20)
const zoneW = sys.windowWidth - sidePad * 2
// 预留底部安全间隙,棋盘不贴到道具栏
const zoneH = Math.max(0, footerTop - headerBottom - footerGap)
const zoneH = Math.max(0, footerTop - boardAreaTop - footerGap)
// 5 行 × 4 列:宽高分别约束,取较小单格像素
cellSizePx.value = calcMaxCellSize(zoneW - inset * 2, zoneH - inset * 2)
File diff suppressed because it is too large Load Diff
+24 -4
View File
@@ -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) {
+427 -170
View File
@@ -5,8 +5,10 @@
.game-page {
height: 100vh;
height: 100dvh;
background: #f4fbf4; /* VitalMint background */
color: #161d19; /* VitalMint text color */
/* 参考稿:顶部高光的薄荷渐变背景 */
background: #e9f0e9;
background-image: radial-gradient(circle at 50% -20%, #ffffff 0%, #e9f0e9 60%, #d5dcd5 100%);
color: #171d19;
overflow: hidden;
position: relative;
display: flex;
@@ -24,68 +26,72 @@
}
.game-page .gg-glass {
background: rgba(255, 255, 255, 0.75);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border: 1.5rpx solid rgba(255, 255, 255, 0.6);
background: rgba(255, 255, 255, 0.6);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.8);
}
/* ===== Header HUD ===== */
/* ===== Header / TopAppBar(参考稿玻璃顶栏) ===== */
.game-page .gg-header {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 50;
border-radius: 0 0 24rpx 24rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 108, 73, 0.04);
background: rgba(255, 255, 255, 0.88);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border-bottom: 1rpx solid rgba(0, 108, 73, 0.06);
background: rgba(255, 255, 255, 0.4);
backdrop-filter: blur(24px);
-webkit-backdrop-filter: blur(24px);
border-bottom: 1px solid rgba(255, 255, 255, 0.6);
box-shadow: 0 4rpx 24rpx rgba(0, 0, 0, 0.02);
}
.game-page .gg-hud-nav {
display: flex;
align-items: center;
justify-content: space-between;
position: relative;
height: var(--menu-height, 64rpx);
min-height: var(--menu-height, 64rpx);
margin-bottom: 0;
}
.game-page .gg-hud-control-group {
display: flex;
align-items: center;
gap: 10rpx;
gap: 16rpx;
}
/* 玻璃圆形图标按钮 */
.game-page .gg-hud-btn {
width: var(--menu-height, 64rpx);
height: var(--menu-height, 64rpx);
width: 84rpx;
height: 84rpx;
border-radius: 50%;
background: rgba(255, 255, 255, 0.85);
border: 1rpx solid rgba(0, 108, 73, 0.08);
background: rgba(255, 255, 255, 0.7);
border: 1px solid rgba(255, 255, 255, 0.9);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
box-shadow: 0 4rpx 12rpx rgba(0, 108, 73, 0.03);
transition: all 0.2s ease;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.04), inset 0 1px 2px rgba(255, 255, 255, 0.8);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
transition: all 0.15s ease;
}
.game-page .gg-hud-btn--pressed {
opacity: 0.75;
transform: scale(0.94);
background: rgba(255, 255, 255, 0.95);
}
.game-page .gg-hud-btn.is-muted {
opacity: 0.45;
opacity: 0.55;
}
.game-page .gg-hud-level-center {
position: absolute;
left: 50%;
transform: translateX(-50%);
top: 50%;
transform: translate(-50%, -50%);
display: flex;
flex-direction: column;
align-items: center;
@@ -93,25 +99,29 @@
}
.game-page .gg-hud-level-text {
font-size: 30rpx;
font-size: 44rpx;
font-weight: 800;
color: #475569;
color: #006c4b;
line-height: 1.1;
letter-spacing: 1rpx;
letter-spacing: 2rpx;
text-shadow: 0 2rpx 6rpx rgba(0, 108, 75, 0.12);
}
.game-page .gg-hud-score {
display: flex;
align-items: baseline;
gap: 4rpx;
gap: 6rpx;
margin-top: 2rpx;
}
.game-page .gg-hud-score-val {
font-size: 44rpx;
font-weight: 900;
color: #006c49;
font-size: 68rpx;
font-weight: 800;
color: #006c4b;
font-variant-numeric: tabular-nums;
line-height: 1.2;
line-height: 1;
letter-spacing: -2rpx;
text-shadow: 0 2rpx 6rpx rgba(0, 108, 75, 0.12);
}
.game-page .gg-hud-score.is-pop .gg-hud-score-val {
@@ -119,44 +129,254 @@
}
.game-page .gg-hud-score-unit {
font-size: 26rpx;
font-size: 28rpx;
font-weight: 700;
color: #64748b;
color: #3d4a42;
}
/* ===== 次:上次消除(名称 + 含糖量 ===== */
/* ===== 信息横幅(参考稿玻璃卡 + 绿色 info 图标 ===== */
.game-page .gg-info-banner {
flex-shrink: 0;
width: 100%;
max-width: 750rpx;
margin: 0 auto 20rpx;
padding: 20rpx 24rpx;
box-sizing: border-box;
display: flex;
align-items: center;
gap: 20rpx;
border-radius: 28rpx;
background: rgba(255, 255, 255, 0.6);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.8);
box-shadow: 0 12rpx 32rpx rgba(0, 0, 0, 0.06), inset 0 1px 2px rgba(255, 255, 255, 0.9);
}
.game-page .gg-info-banner-icon {
flex-shrink: 0;
width: 80rpx;
height: 80rpx;
border-radius: 22rpx;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #00a676, #006c4b);
border: 1px solid rgba(255, 255, 255, 0.2);
box-shadow: 0 8rpx 18rpx rgba(0, 108, 75, 0.28);
}
.game-page .gg-info-banner-body {
flex: 1;
min-width: 0;
}
.game-page .gg-info-banner-text {
font-size: 28rpx;
font-weight: 500;
color: #171d19;
line-height: 1.5;
}
.game-page .gg-info-banner-kw {
font-size: 32rpx;
font-weight: 800;
color: #006c4b;
letter-spacing: 1rpx;
}
/* —— 消除后:该食物的含糖 + 建议 —— */
.game-page .gg-info-banner.is-food {
gap: 18rpx;
animation: gg-info-food-in 0.28s cubic-bezier(0.22, 1, 0.36, 1);
}
.game-page .gg-info-banner.is-food.is-low { border-color: rgba(22, 163, 74, 0.28); }
.game-page .gg-info-banner.is-food.is-mid { border-color: rgba(234, 88, 12, 0.28); }
.game-page .gg-info-banner.is-food.is-high { border-color: rgba(220, 38, 38, 0.3); }
/* 食物图标:白底 + GI 彩色描边(覆盖默认绿色渐变) */
.game-page .gg-info-banner-icon.is-food {
background: #ffffff;
box-shadow: 0 4rpx 12rpx rgba(15, 23, 42, 0.08), inset 0 1px 2px rgba(255, 255, 255, 0.9);
border-width: 2rpx;
border-style: solid;
}
.game-page .gg-info-banner-icon.is-food.is-low { border-color: rgba(22, 163, 74, 0.4); }
.game-page .gg-info-banner-icon.is-food.is-mid { border-color: rgba(234, 88, 12, 0.4); }
.game-page .gg-info-banner-icon.is-food.is-high { border-color: rgba(220, 38, 38, 0.45); }
.game-page .gg-info-banner-icon.is-food .food-tile-icon--goal {
width: 60rpx;
height: 60rpx;
}
.game-page .gg-info-food-head {
display: flex;
align-items: center;
gap: 14rpx;
margin-bottom: 8rpx;
}
.game-page .gg-info-food-name {
font-size: 34rpx;
font-weight: 900;
color: #171d19;
line-height: 1.2;
max-width: 320rpx;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.game-page .gg-info-food-gi {
flex-shrink: 0;
display: inline-flex;
align-items: center;
padding: 4rpx 16rpx;
border-radius: 999rpx;
}
.game-page .gg-info-food-gi-text {
font-size: 24rpx;
font-weight: 800;
color: #ffffff;
letter-spacing: 1rpx;
}
.game-page .gg-info-food-gi.is-low { background: linear-gradient(135deg, #5bdda8, #00a676); box-shadow: 0 2rpx 8rpx rgba(0, 166, 118, 0.32); }
.game-page .gg-info-food-gi.is-mid { background: linear-gradient(135deg, #fbbf24, #d97706); box-shadow: 0 2rpx 8rpx rgba(217, 119, 6, 0.32); }
.game-page .gg-info-food-gi.is-high { background: linear-gradient(135deg, #f87171, #ba1a1a); box-shadow: 0 2rpx 8rpx rgba(186, 26, 26, 0.34); }
.game-page .gg-info-food-tip {
display: block;
font-size: 26rpx;
font-weight: 600;
color: #3d4a42;
line-height: 1.4;
}
.game-page .gg-info-food-tip.is-low { color: #15803d; }
.game-page .gg-info-food-tip.is-mid { color: #c2410c; }
.game-page .gg-info-food-tip.is-high { color: #b91c1c; }
@keyframes gg-info-food-in {
from { opacity: 0.35; transform: translateY(-6rpx); }
to { opacity: 1; transform: translateY(0); }
}
/* ===== 次:上次消除(名称 + 含糖量),GI 三色编码学习卡 ===== */
.game-page .gg-hud-last-food {
flex-shrink: 0;
width: 100%;
max-width: 750rpx;
margin: 0 auto 14rpx;
padding: 16rpx 18rpx;
border-radius: 22rpx;
background: rgba(255, 255, 255, 0.88);
padding: 16rpx 18rpx 18rpx;
border-radius: 26rpx;
background: rgba(255, 255, 255, 0.92);
border: 1rpx solid rgba(0, 108, 73, 0.1);
box-sizing: border-box;
box-shadow: 0 4rpx 16rpx rgba(0, 108, 73, 0.06);
box-shadow: 0 6rpx 20rpx rgba(0, 108, 73, 0.07);
}
/* 头部:标题 + GI 状态胶囊 */
.game-page .gg-hud-last-food-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12rpx;
margin-bottom: 12rpx;
}
.game-page .gg-hud-last-food-kicker {
display: block;
font-size: 24rpx;
font-weight: 700;
color: #475569;
letter-spacing: 0.04em;
margin-bottom: 12rpx;
line-height: 1.3;
}
.game-page .gg-hud-last-food-card {
display: flex;
align-items: center;
gap: 16rpx;
padding: 14rpx 16rpx;
border-radius: 18rpx;
font-size: 24rpx;
font-weight: 800;
color: #64748b;
letter-spacing: 0.08em;
line-height: 1.3;
padding-left: 16rpx;
position: relative;
}
.game-page .gg-hud-last-food-kicker::before {
content: '';
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 6rpx;
height: 24rpx;
border-radius: 3rpx;
background: #006c49;
}
/* 头部 GI 胶囊 */
.game-page .gg-hud-last-food-chip {
display: flex;
align-items: center;
gap: 8rpx;
padding: 5rpx 16rpx 5rpx 12rpx;
border-radius: 999rpx;
font-weight: 800;
line-height: 1;
border: 1rpx solid transparent;
}
.game-page .gg-hud-last-food-chip-dot {
width: 14rpx;
height: 14rpx;
border-radius: 50%;
flex-shrink: 0;
}
.game-page .gg-hud-last-food-chip-text {
font-size: 24rpx;
font-weight: 800;
}
.game-page .gg-hud-last-food-chip.is-low {
background: rgba(22, 163, 74, 0.12);
border-color: rgba(22, 163, 74, 0.22);
}
.game-page .gg-hud-last-food-chip.is-low .gg-hud-last-food-chip-dot { background: #16a34a; }
.game-page .gg-hud-last-food-chip.is-low .gg-hud-last-food-chip-text { color: #15803d; }
.game-page .gg-hud-last-food-chip.is-mid {
background: rgba(234, 88, 12, 0.12);
border-color: rgba(234, 88, 12, 0.22);
}
.game-page .gg-hud-last-food-chip.is-mid .gg-hud-last-food-chip-dot { background: #ea580c; }
.game-page .gg-hud-last-food-chip.is-mid .gg-hud-last-food-chip-text { color: #c2410c; }
.game-page .gg-hud-last-food-chip.is-high {
background: rgba(220, 38, 38, 0.12);
border-color: rgba(220, 38, 38, 0.24);
}
.game-page .gg-hud-last-food-chip.is-high .gg-hud-last-food-chip-dot { background: #dc2626; }
.game-page .gg-hud-last-food-chip.is-high .gg-hud-last-food-chip-text { color: #b91c1c; }
/* 主卡:左侧 GI 强调条 + 图标 + 文案 */
.game-page .gg-hud-last-food-card {
position: relative;
display: flex;
align-items: center;
gap: 18rpx;
padding: 16rpx 18rpx 16rpx 24rpx;
border-radius: 20rpx;
overflow: hidden;
background: #f8fafc;
border: 1rpx solid rgba(0, 108, 73, 0.08);
animation: gg-last-food-in 0.22s ease-out;
border: 1rpx solid rgba(15, 23, 42, 0.06);
animation: gg-last-food-in 0.24s cubic-bezier(0.22, 1, 0.36, 1);
}
/* 左侧强调条(GI 三色) */
.game-page .gg-hud-last-food-card::before {
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 8rpx;
border-radius: 8rpx;
background: #cbd5e1;
}
@media (prefers-reduced-motion: reduce) {
@@ -166,35 +386,44 @@
}
.game-page .gg-hud-last-food-card.is-high {
background: rgba(254, 242, 242, 0.9);
border-color: rgba(220, 38, 38, 0.2);
background: linear-gradient(90deg, rgba(254, 226, 226, 0.85) 0%, rgba(254, 242, 242, 0.6) 60%);
border-color: rgba(220, 38, 38, 0.18);
}
.game-page .gg-hud-last-food-card.is-high::before { background: #dc2626; }
.game-page .gg-hud-last-food-card.is-mid {
background: rgba(255, 251, 235, 0.95);
border-color: rgba(234, 88, 12, 0.18);
background: linear-gradient(90deg, rgba(255, 237, 213, 0.85) 0%, rgba(255, 251, 235, 0.6) 60%);
border-color: rgba(234, 88, 12, 0.16);
}
.game-page .gg-hud-last-food-card.is-mid::before { background: #ea580c; }
.game-page .gg-hud-last-food-card.is-low {
background: rgba(240, 253, 244, 0.95);
border-color: rgba(22, 163, 74, 0.15);
background: linear-gradient(90deg, rgba(220, 252, 231, 0.85) 0%, rgba(240, 253, 244, 0.6) 60%);
border-color: rgba(22, 163, 74, 0.16);
}
.game-page .gg-hud-last-food-card.is-low::before { background: #16a34a; }
/* 图标:白底 + GI 彩色描边环 */
.game-page .gg-hud-last-food-icon {
width: 72rpx;
height: 72rpx;
width: 80rpx;
height: 80rpx;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
border-radius: 16rpx;
border-radius: 18rpx;
background: #fff;
box-shadow: 0 2rpx 8rpx rgba(15, 23, 42, 0.06);
border: 2rpx solid rgba(15, 23, 42, 0.06);
box-shadow: 0 3rpx 10rpx rgba(15, 23, 42, 0.08);
}
.game-page .gg-hud-last-food-icon.is-low { border-color: rgba(22, 163, 74, 0.35); }
.game-page .gg-hud-last-food-icon.is-mid { border-color: rgba(234, 88, 12, 0.35); }
.game-page .gg-hud-last-food-icon.is-high { border-color: rgba(220, 38, 38, 0.4); }
.game-page .gg-hud-last-food-icon .food-tile-icon--goal {
width: 56rpx;
height: 56rpx;
width: 62rpx;
height: 62rpx;
}
.game-page .gg-hud-last-food-body {
@@ -206,10 +435,10 @@
}
.game-page .gg-hud-last-food-name {
font-size: 34rpx;
font-weight: 800;
font-size: 36rpx;
font-weight: 900;
color: #0f172a;
line-height: 1.25;
line-height: 1.2;
word-break: break-all;
}
@@ -217,42 +446,61 @@
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8rpx 12rpx;
gap: 8rpx 14rpx;
}
.game-page .gg-hud-last-food-sugar-label {
font-size: 26rpx;
font-weight: 700;
color: #475569;
/* 含糖等级徽标:GI 三色实底 + 白字(保证对比度) */
.game-page .gg-hud-last-food-gi {
display: inline-flex;
align-items: center;
padding: 5rpx 16rpx;
border-radius: 999rpx;
line-height: 1.3;
}
.game-page .gg-hud-last-food-gi {
.game-page .gg-hud-last-food-gi-text {
font-size: 26rpx;
font-weight: 800;
color: #fff;
padding: 4rpx 14rpx;
border-radius: 8rpx;
line-height: 1.3;
letter-spacing: 0.02em;
}
.game-page .gg-hud-last-food-gi.is-low { background: #16a34a; box-shadow: 0 2rpx 8rpx rgba(22, 163, 74, 0.3); }
.game-page .gg-hud-last-food-gi.is-mid { background: #ea580c; box-shadow: 0 2rpx 8rpx rgba(234, 88, 12, 0.3); }
.game-page .gg-hud-last-food-gi.is-high { background: #dc2626; box-shadow: 0 2rpx 8rpx rgba(220, 38, 38, 0.32); }
.game-page .gg-hud-last-food-sugar-hint {
font-size: 26rpx;
font-weight: 700;
color: #334155;
color: #475569;
line-height: 1.35;
}
.game-page .gg-hud-last-food-card.is-high .gg-hud-last-food-sugar-hint {
color: #991b1b;
.game-page .gg-hud-last-food-card.is-high .gg-hud-last-food-sugar-hint { color: #b91c1c; }
.game-page .gg-hud-last-food-card.is-mid .gg-hud-last-food-sugar-hint { color: #c2410c; }
.game-page .gg-hud-last-food-card.is-low .gg-hud-last-food-sugar-hint { color: #15803d; }
/* 空态占位 */
.game-page .gg-hud-last-food-placeholder {
display: flex;
align-items: center;
gap: 12rpx;
padding: 18rpx 16rpx;
border-radius: 20rpx;
background: #f8fafc;
border: 1rpx dashed rgba(15, 23, 42, 0.12);
}
.game-page .gg-hud-last-food-placeholder {
padding: 12rpx 4rpx;
.game-page .gg-hud-last-food-placeholder-dot {
width: 16rpx;
height: 16rpx;
border-radius: 50%;
flex-shrink: 0;
background: #cbd5e1;
}
.game-page .gg-hud-last-food-placeholder text {
font-size: 28rpx;
font-size: 27rpx;
font-weight: 600;
color: #94a3b8;
line-height: 1.45;
@@ -434,8 +682,8 @@
align-items: center;
justify-content: flex-start;
box-sizing: border-box;
overflow: hidden;
padding: 8rpx 16rpx 0;
overflow: visible;
padding: 14rpx 20rpx 0;
}
/* gg-hint-banner 已移除,提示直接显示在棋盘食物上 */
@@ -469,17 +717,16 @@
.game-page .game-grid {
display: flex;
flex-direction: column;
border-radius: 36rpx;
background: rgba(255, 255, 255, 0.92);
backdrop-filter: blur(24px);
-webkit-backdrop-filter: blur(24px);
border-radius: 48rpx;
background: rgba(255, 255, 255, 0.6);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
box-shadow:
0 20rpx 56rpx rgba(0, 108, 73, 0.14),
0 4rpx 16rpx rgba(15, 23, 42, 0.06),
inset 0 2rpx 12rpx rgba(255, 255, 255, 0.85);
border: 2rpx solid rgba(0, 108, 73, 0.12);
0 12rpx 32rpx rgba(0, 0, 0, 0.06),
inset 0 1px 2px rgba(255, 255, 255, 0.9);
border: 1px solid rgba(255, 255, 255, 0.8);
box-sizing: border-box;
overflow: hidden;
overflow: visible;
flex-shrink: 0;
}
@@ -503,14 +750,17 @@
inset: 0;
width: 100%;
height: 100%;
border-radius: 0;
border-radius: 28rpx;
display: flex;
align-items: stretch;
justify-content: stretch;
background: transparent;
box-shadow: none;
border: none;
transition: transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1), opacity 0.2s;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.9), rgba(244, 251, 245, 0.6));
box-shadow:
0 10rpx 20rpx rgba(0, 108, 75, 0.08),
inset 0 2rpx 4rpx rgba(255, 255, 255, 0.8),
inset 0 -2rpx 6rpx rgba(0, 0, 0, 0.02);
border: 1px solid rgba(255, 255, 255, 0.9);
transition: transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1), box-shadow 0.25s ease, opacity 0.2s;
z-index: 1;
box-sizing: border-box;
overflow: visible;
@@ -543,13 +793,13 @@
}
.game-page .game-tile.is-selected {
transform: scale(1.12);
transform: scale(1.08);
z-index: 20;
border: none;
box-shadow: none;
outline: 4rpx solid #006c49;
outline-offset: -2rpx;
border-radius: 8rpx;
border: 2px solid #00a676;
border-radius: 28rpx;
box-shadow:
0 0 20rpx rgba(0, 166, 118, 0.35),
inset 0 0 10rpx rgba(0, 166, 118, 0.12);
}
.game-page .game-tile--pressed {
@@ -752,31 +1002,39 @@
/* ===== 棋子含糖等级常驻角标(认知核心:一眼看出高/中/低糖) ===== */
.game-page .gg-tile-gi {
position: absolute;
top: 2rpx;
left: 2rpx;
z-index: 3;
min-width: 30rpx;
height: 30rpx;
padding: 0 6rpx;
top: -12rpx;
left: 50%;
transform: translateX(-50%);
z-index: 5;
min-width: 40rpx;
padding: 6rpx 16rpx;
border-radius: 999rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 22rpx;
font-weight: 900;
font-weight: 800;
color: #ffffff;
line-height: 1;
letter-spacing: 1rpx;
pointer-events: none;
box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.25);
border: 2rpx solid rgba(255, 255, 255, 0.9);
border: 1px solid rgba(255, 255, 255, 0.6);
text-shadow: 0 1rpx 2rpx rgba(0, 0, 0, 0.2);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
}
.game-page .gg-tile-gi.is-low { background: #16a34a; }
.game-page .gg-tile-gi.is-mid { background: #ea580c; }
.game-page .gg-tile-gi.is-low {
background: linear-gradient(135deg, rgba(91, 221, 168, 0.95), rgba(0, 166, 118, 0.95));
box-shadow: 0 4rpx 12rpx rgba(0, 166, 118, 0.4), inset 0 2rpx 4rpx rgba(255, 255, 255, 0.4);
}
.game-page .gg-tile-gi.is-mid {
background: linear-gradient(135deg, rgba(251, 191, 36, 0.95), rgba(217, 119, 6, 0.95));
box-shadow: 0 4rpx 12rpx rgba(217, 119, 6, 0.4), inset 0 2rpx 4rpx rgba(255, 255, 255, 0.4);
}
.game-page .gg-tile-gi.is-high {
background: #dc2626;
box-shadow: 0 2rpx 10rpx rgba(220, 38, 38, 0.45);
font-size: 22rpx;
background: linear-gradient(135deg, rgba(248, 113, 113, 0.95), rgba(186, 26, 26, 0.95));
box-shadow: 0 4rpx 12rpx rgba(186, 26, 26, 0.4), inset 0 2rpx 4rpx rgba(255, 255, 255, 0.4);
}
/* ===== 点击食物的大字科普卡 ===== */
@@ -869,97 +1127,96 @@
right: 0;
bottom: 0;
z-index: 40;
border-radius: 48rpx 48rpx 0 0;
border-top: 1rpx solid rgba(0, 108, 73, 0.06);
box-shadow: 0 -4rpx 24rpx rgba(0, 108, 73, 0.04);
padding-top: 12rpx;
border-radius: 64rpx 64rpx 0 0;
border-top: 1px solid #ffffff;
box-shadow: 0 -12rpx 40rpx rgba(0, 0, 0, 0.08);
padding-top: 24rpx;
padding-left: 24rpx;
padding-right: 24rpx;
background: rgba(255, 255, 255, 0.78);
}
.game-page .gg-footer--sub .gg-booster-btn {
padding: 16rpx 0;
width: 152rpx;
border-radius: 32rpx;
background: rgba(255, 255, 255, 0.75);
border-color: rgba(0, 108, 73, 0.08);
box-shadow: 0 4rpx 12rpx rgba(0, 108, 73, 0.04);
}
.game-page .gg-footer--sub .gg-booster-label {
font-size: 24rpx;
color: #64748b;
background: rgba(255, 255, 255, 0.7);
backdrop-filter: blur(28px);
-webkit-backdrop-filter: blur(28px);
}
.game-page .gg-boosters {
display: flex;
justify-content: space-around;
align-items: center;
align-items: flex-start;
max-width: 750rpx;
margin: 0 auto;
}
/* 单个道具:图标卡 + 角标 + 文字 */
.game-page .gg-booster-btn {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
padding: 22rpx 0;
width: 176rpx;
background: rgba(255, 255, 255, 0.85);
border: 1.5rpx solid rgba(0, 108, 73, 0.12);
border-radius: 40rpx;
box-shadow: 0 8rpx 20rpx rgba(0, 108, 73, 0.03), inset 0 2rpx 4rpx rgba(255, 255, 255, 0.8);
transition: all 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
padding: 8rpx 0;
width: 184rpx;
background: transparent;
border: none;
transition: transform 0.2s ease;
}
.game-page .gg-booster-btn--pressed {
.game-page .gg-booster-tile {
position: relative;
width: 112rpx;
height: 112rpx;
border-radius: 36rpx;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.85);
border: 1px solid #ffffff;
box-shadow: 0 8rpx 18rpx rgba(0, 0, 0, 0.08), inset 0 1px 2px rgba(255, 255, 255, 0.9);
transition: transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1), box-shadow 0.2s ease;
}
.game-page .gg-booster-btn--pressed .gg-booster-tile {
transform: scale(0.92);
background: rgba(255, 255, 255, 0.65);
box-shadow: 0 4rpx 10rpx rgba(0, 0, 0, 0.06);
}
.game-page .gg-booster-btn.is-disabled {
opacity: 0.35;
opacity: 0.4;
filter: grayscale(100%);
pointer-events: none !important;
}
.game-page .gg-booster-count {
position: absolute;
top: -10rpx;
right: -10rpx;
width: 52rpx;
height: 52rpx;
border-radius: 50%;
top: -12rpx;
right: -12rpx;
min-width: 50rpx;
height: 50rpx;
padding: 0 10rpx;
border-radius: 26rpx;
color: #fff;
font-size: 30rpx;
font-weight: 900;
font-size: 28rpx;
font-weight: 800;
display: flex;
align-items: center;
justify-content: center;
border: 4rpx solid #fff;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.12);
border: 3rpx solid #ffffff;
box-shadow: 0 4rpx 10rpx rgba(0, 0, 0, 0.18);
text-shadow: 0 1rpx 1rpx rgba(0, 0, 0, 0.2);
z-index: 2;
}
.game-page .gg-booster-count.is-green { background: #10b981; }
.game-page .gg-booster-count.is-orange { background: #f97316; }
.game-page .gg-booster-count.is-rose { background: #f43f5e; }
.game-page .gg-booster-count.is-green { background: linear-gradient(135deg, #00a676, #006c4b); box-shadow: 0 4rpx 10rpx rgba(0, 166, 118, 0.3); }
.game-page .gg-booster-count.is-orange { background: linear-gradient(135deg, #f59e0b, #d97706); box-shadow: 0 4rpx 10rpx rgba(217, 119, 6, 0.3); }
.game-page .gg-booster-count.is-rose { background: linear-gradient(135deg, #ef4444, #ba1a1a); box-shadow: 0 4rpx 10rpx rgba(186, 26, 26, 0.3); }
.game-page .gg-booster-icon {
margin-bottom: 8rpx;
transition: transform 0.25s ease;
}
.game-page .gg-booster-btn--pressed .gg-booster-icon {
transform: scale(0.85);
transition: transform 0.2s ease;
}
.game-page .gg-booster-label {
margin-top: 14rpx;
font-size: 30rpx;
font-weight: 700;
color: #2b3a32;
color: #3d4a42;
letter-spacing: 1rpx;
}
/* ===== 高糖消除红色警示弹窗 ===== */
@@ -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 {
+307
View File
@@ -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(' · ')
}
+5
View File
@@ -403,6 +403,11 @@ export function prescriptionOrderLogisticsTrace(params: {
return request.get({ url: '/tcm.prescriptionOrder/logisticsTrace', params })
}
/** 直接调用京东官方物流接口刷新轨迹并落库(绕过快递100缓存) */
export function prescriptionOrderLogisticsJdUpdate(params: { id: number }) {
return request.post({ url: '/tcm.prescriptionOrder/logisticsJdUpdate', params })
}
export function prescriptionOrderDetail(params: { id: number }) {
return request.get({ url: '/tcm.prescriptionOrder/detail', 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: () => {
@@ -0,0 +1,96 @@
<template>
<template v-if="variant === 'split'">
<el-form-item :label="label" :class="{ 'is-compact': compact }">
<el-radio-group v-model="timeType" :size="compact ? 'small' : 'default'" @change="emit('time-type-change')">
<el-radio-button v-if="showAll" label="all">全部</el-radio-button>
<el-radio-button label="today">今天</el-radio-button>
<el-radio-button label="yesterday">昨天</el-radio-button>
<el-radio-button label="week">最近7天</el-radio-button>
<el-radio-button label="month">本月</el-radio-button>
<el-radio-button label="custom">自定义</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item v-if="timeType === 'custom'">
<el-date-picker
v-model="dateRangeModel"
type="daterange"
value-format="YYYY-MM-DD"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
:size="compact ? 'small' : 'default'"
:style="{ width: pickerWidth }"
@change="emit('custom-change')"
/>
</el-form-item>
</template>
<el-form-item v-else :label="label" :class="{ 'is-compact': compact }">
<div class="list-time-filter-inline">
<el-radio-group v-model="timeType" :size="compact ? 'small' : 'default'" @change="emit('time-type-change')">
<el-radio-button v-if="showAll" label="all">全部</el-radio-button>
<el-radio-button label="today">今天</el-radio-button>
<el-radio-button label="yesterday">昨天</el-radio-button>
<el-radio-button label="week">最近7天</el-radio-button>
<el-radio-button label="month">本月</el-radio-button>
<el-radio-button label="custom">自定义</el-radio-button>
</el-radio-group>
<el-date-picker
v-if="timeType === 'custom'"
v-model="dateRangeModel"
type="daterange"
value-format="YYYY-MM-DD"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
:size="compact ? 'small' : 'default'"
:style="{ width: pickerWidth }"
@change="emit('custom-change')"
/>
</div>
</el-form-item>
</template>
<script lang="ts" setup>
import type { ListTimeType } from '@/hooks/useListTimeFilter'
withDefaults(
defineProps<{
label?: string
/** split:与自录统计一致,两个 form-item;inline:处方订单筛选用单行 */
variant?: 'split' | 'inline'
pickerWidth?: string
/** 紧凑模式:small 尺寸 radio / 日期 */
compact?: boolean
/** 是否显示「全部」(不限时间)选项 */
showAll?: boolean
}>(),
{
label: '时间范围',
variant: 'split',
pickerWidth: '260px',
compact: false,
showAll: false,
}
)
const timeType = defineModel<ListTimeType>('timeType', { required: true })
const dateRangeModel = defineModel<string[]>('dateRange', { default: () => [] })
const emit = defineEmits<{
'time-type-change': []
'custom-change': []
}>()
</script>
<style scoped lang="scss">
.list-time-filter-inline {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 6px 8px;
}
:deep(.is-compact.el-form-item) {
margin-bottom: 0;
}
</style>
+84 -21
View File
@@ -88,13 +88,17 @@
<strong>{{ formData.herbs.length }}</strong> 主方 {{ mainHerbCount }} / 辅方
{{ auxHerbCount }}· 可手动添加从处方库或粘贴药方批量录入
</p>
<p v-if="herbsLocked" class="rp-toolbar__lock-tip">
<el-tag type="danger" size="small" effect="plain">禁用修改</el-tag>
禁用修改处方库模板药材已锁定不可增删改如需调整请重新从处方库导入覆盖
</p>
</div>
</div>
<div class="rp-toolbar__actions" role="toolbar" aria-label="处方药材快捷操作">
<el-button type="primary" size="small" :icon="CirclePlus" @click="handleAddHerb('主方')">
<el-button type="primary" size="small" :icon="CirclePlus" :disabled="herbsLocked" @click="handleAddHerb('主方')">
添加主方
</el-button>
<el-button type="primary" plain size="small" @click="handleAddHerb('辅方')">
<el-button type="primary" plain size="small" :disabled="herbsLocked" @click="handleAddHerb('辅方')">
添加辅方
</el-button>
<el-button
@@ -135,7 +139,7 @@
:class="{ 'herb-editor-card--dup': isDuplicateHerbName(row.index) }"
>
<div class="herb-editor-card__title">主方 {{ row.index + 1 }}</div>
<MedicineNameSelect v-model="formData.herbs[row.index].name" class="herb-editor-card__select" />
<MedicineNameSelect v-model="formData.herbs[row.index].name" :disabled="herbsLocked" class="herb-editor-card__select" />
<div
v-if="isDuplicateHerbName(row.index)"
class="herb-editor-card__dup-hint text-amber-600 text-xs mb-1"
@@ -148,11 +152,12 @@
:min="0.1"
:step="1"
:precision="1"
:disabled="herbsLocked"
placeholder="剂量(克)"
class="herb-editor-card__dose"
/>
<span class="text-gray-500 shrink-0"></span>
<el-button type="danger" size="small" link @click="handleRemoveHerb(row.index)">
<el-button v-if="!herbsLocked" type="danger" size="small" link @click="handleRemoveHerb(row.index)">
删除
</el-button>
</div>
@@ -171,7 +176,7 @@
:class="{ 'herb-editor-card--dup': isDuplicateHerbName(row.index) }"
>
<div class="herb-editor-card__title">辅方</div>
<MedicineNameSelect v-model="formData.herbs[row.index].name" class="herb-editor-card__select" />
<MedicineNameSelect v-model="formData.herbs[row.index].name" :disabled="herbsLocked" class="herb-editor-card__select" />
<div
v-if="isDuplicateHerbName(row.index)"
class="herb-editor-card__dup-hint text-amber-600 text-xs mb-1"
@@ -184,11 +189,12 @@
:min="0.1"
:step="1"
:precision="1"
:disabled="herbsLocked"
placeholder="剂量(克)"
class="herb-editor-card__dose"
/>
<span class="text-gray-500 shrink-0"></span>
<el-button type="danger" size="small" link @click="handleRemoveHerb(row.index)">
<el-button v-if="!herbsLocked" type="danger" size="small" link @click="handleRemoveHerb(row.index)">
删除
</el-button>
</div>
@@ -553,7 +559,11 @@
<div class="rx-title">{{ prescriptionTabType === 'internal' ? '药房联' : SLIP_TITLE }}</div>
<div class="rx-notice">
<span class="rx-notice-text">
服药前请核对姓名电话医生等信息以及服法医嘱等要点
{{
prescriptionTabType === 'user'
? '服药前请核对姓名、电话、医生等信息以及医嘱等要点'
: '服药前请核对姓名、电话、医生等信息以及服法、医嘱等要点'
}}
</span>
<span class="rx-notice-meta">
<span>日期{{ rxDateText }}</span>
@@ -672,22 +682,30 @@
</div>
<div class="rx-text">
<p v-if="prescriptionTabType === 'internal' || savedAuxUsageText">主方服法{{ rxUsageText }}</p>
<p v-else>服法{{ rxUsageText }}</p>
<p v-if="savedAuxUsageText">{{ prescriptionTabType === 'user' ? '辅服法' : '辅方服法' }}{{ savedAuxUsageText }}</p>
<template v-if="prescriptionTabType === 'internal'">
<p v-if="savedAuxUsageText">主方服法{{ rxUsageText }}</p>
<p v-else>服法{{ rxUsageText }}</p>
<p v-if="savedAuxUsageText">辅方服法{{ savedAuxUsageText }}</p>
</template>
<p v-if="rxAdviceText">医嘱{{ rxAdviceText }}</p>
<p v-if="rxRemarkText">备注{{ rxRemarkText }}</p>
<p v-if="prescriptionTabType === 'internal' && rxRemarkText">备注{{ rxRemarkText }}</p>
<p v-if="savedPrescription.dietary_taboo && savedPrescription.dietary_taboo.length">
忌口{{ Array.isArray(savedPrescription.dietary_taboo) ? savedPrescription.dietary_taboo.join('、') : savedPrescription.dietary_taboo }}
</p>
<p v-if="rxPharmacyRemarkText" class="rx-text-warn">药房备注{{ rxPharmacyRemarkText }}</p>
<p v-if="rxOutPelletText && savedPrescription.prescription_type !== '饮片'" class="rx-text-warn">
<p
v-if="prescriptionTabType === 'internal' && rxOutPelletText && savedPrescription.prescription_type !== '饮片'"
class="rx-text-warn"
>
出丸{{ rxOutPelletText }}
</p>
</div>
<div class="rx-bottom">
<div class="rx-bot-row rx-bot-row-1">
<div
class="rx-bot-row rx-bot-row-1"
:class="{ 'rx-bot-row-1--user': prescriptionTabType === 'user' }"
>
<div class="rx-bot-cell rx-bot-stack rx-bot-doctor">
<div class="rx-bot-label">医师</div>
<div class="rx-bot-doctor-body">
@@ -697,10 +715,12 @@
alt="医师签名"
class="rx-bot-sign-img"
/>
<div class="rx-bot-doctor-name">{{ savedPrescription.doctor_name || '—' }}</div>
<div v-if="!savedPrescription.doctor_signature" class="rx-bot-doctor-name">
{{ savedPrescription.doctor_name || '—' }}
</div>
</div>
</div>
<div class="rx-bot-cell rx-bot-meta">
<div v-if="prescriptionTabType === 'internal'" class="rx-bot-cell rx-bot-meta">
<span class="rx-bot-meta-key">类型:</span>
<span class="rx-bot-meta-val">{{ rxTypeText }}</span>
</div>
@@ -708,7 +728,7 @@
<span class="rx-bot-meta-key">天数:</span>
<span class="rx-bot-meta-val">{{ rxSlipMedicationDaysText }}</span>
</div>
<div class="rx-bot-cell rx-bot-meta">
<div v-if="prescriptionTabType === 'internal'" class="rx-bot-cell rx-bot-meta">
<span class="rx-bot-meta-key">剂量:</span>
<span class="rx-bot-meta-val">{{ rxPerDoseAmount }}</span>
</div>
@@ -1028,6 +1048,8 @@ interface Herb {
name: string
dosage: number
formula_type?: FormulaType
/** 来自「禁用修改」处方库模板:锁定后不可增删改,只能再次导入覆盖 */
locked?: boolean
}
type AuxUsageForm = {
@@ -1044,11 +1066,15 @@ function normalizeFormulaType(v: unknown): FormulaType {
}
function normalizeHerbRow(raw: any): Herb {
return {
const row: Herb = {
name: String(raw?.name ?? '').trim(),
dosage: Number(raw?.dosage) || 0,
formula_type: normalizeFormulaType(raw?.formula_type)
}
if (raw?.locked === true || raw?.locked === 1) {
row.locked = true
}
return row
}
function defaultAuxUsage(prescriptionType = '浓缩水丸'): AuxUsageForm {
@@ -1430,6 +1456,12 @@ const auxHerbRows = computed(() =>
const mainHerbCount = computed(() => mainHerbRows.value.length)
const auxHerbCount = computed(() => auxHerbRows.value.length)
/**
* 整张处方是否被锁定只要任一药材来自禁用修改模板即锁定
* 锁定后不可增删改任意药材仅可再次从处方库导入覆盖
*/
const herbsLocked = computed(() => formData.herbs.some((h) => (h as Herb).locked === true))
const libraryDoctorId = computed(() => {
const uid = Number(userStore.userInfo?.id)
return uid > 0 ? uid : 0
@@ -2029,10 +2061,18 @@ function findDuplicateHerbNames(): string[] {
}
const handleAddHerb = (formulaType: FormulaType = '主方') => {
if (herbsLocked.value) {
feedback.msgWarning('该处方含「禁用修改」模板,药材已锁定,请重新从处方库导入覆盖')
return
}
formData.herbs.push({ name: '', dosage: 6, formula_type: formulaType })
}
const handleRemoveHerb = (index: number) => {
if (herbsLocked.value) {
feedback.msgWarning('该处方含「禁用修改」模板,药材已锁定,不可删除')
return
}
formData.herbs.splice(index, 1)
}
@@ -2089,10 +2129,18 @@ const handleImportLibrary = (row: any) => {
}
const formulaType = normalizeFormulaType(row.formula_type)
const imported = (JSON.parse(JSON.stringify(row.herbs)) as any[]).map((h) => ({
...normalizeHerbRow(h),
formula_type: formulaType
}))
//
const isLockedTemplate = Number(row?.disable_edit) === 1
const imported = (JSON.parse(JSON.stringify(row.herbs)) as any[]).map((h) => {
const herb = normalizeHerbRow(h)
herb.formula_type = formulaType
if (isLockedTemplate) {
herb.locked = true
} else {
delete herb.locked
}
return herb
})
if (libraryImportMode.value === 'replace') {
const kept = formData.herbs.filter((h) => normalizeFormulaType(h.formula_type) !== formulaType)
formData.herbs = [...kept, ...imported]
@@ -2532,6 +2580,17 @@ defineExpose({
color: var(--el-text-color-regular);
}
.rp-toolbar__lock-tip {
display: flex;
align-items: center;
gap: 6px;
flex-wrap: wrap;
margin: 6px 0 0;
font-size: 12px;
line-height: 1.55;
color: var(--el-color-danger);
}
.rp-toolbar__actions {
display: flex;
flex-wrap: wrap;
@@ -2925,6 +2984,10 @@ defineExpose({
min-height: 70px;
}
.rx-bot-row-1--user {
grid-template-columns: 1.4fr 0.6fr;
}
.rx-hospital-row {
display: flex;
flex-direction: column;
+88
View File
@@ -0,0 +1,88 @@
export type ListTimeType = 'all' | 'today' | 'yesterday' | 'week' | 'month' | 'custom'
export function formatYmd(d: Date): string {
const y = d.getFullYear()
const m = String(d.getMonth() + 1).padStart(2, '0')
const day = String(d.getDate()).padStart(2, '0')
return `${y}-${m}-${day}`
}
/** 列表时间筛选:month 为本自然月 1 日~月末 */
export function resolveListTimeRange(timeType: ListTimeType, dateRange: string[]): [string, string] {
const today = formatYmd(new Date())
switch (timeType) {
case 'all':
return ['', '']
case 'yesterday': {
const d = new Date()
d.setDate(d.getDate() - 1)
const y = formatYmd(d)
return [y, y]
}
case 'week': {
const end = new Date()
const start = new Date()
start.setDate(start.getDate() - 6)
return [formatYmd(start), formatYmd(end)]
}
case 'month': {
const now = new Date()
const start = new Date(now.getFullYear(), now.getMonth(), 1)
const end = new Date(now.getFullYear(), now.getMonth() + 1, 0)
return [formatYmd(start), formatYmd(end)]
}
case 'custom': {
let start = dateRange[0] || today
let end = dateRange[1] || today
if (start > end) {
;[start, end] = [end, start]
}
return [start, end]
}
case 'today':
default:
return [today, today]
}
}
export function useListTimeFilter(defaultType: ListTimeType = 'today') {
const time_type = ref<ListTimeType>(defaultType)
const dateRange = ref<string[]>([])
const resolve = () => resolveListTimeRange(time_type.value, dateRange.value)
const handleTimeTypeChange = (onQuery: () => void) => {
if (time_type.value !== 'custom') {
dateRange.value = []
onQuery()
}
}
const handleCustomDateChange = (onQuery: () => void) => {
if (dateRange.value.length === 2) {
onQuery()
}
}
const resetTimeFilter = () => {
time_type.value = defaultType
dateRange.value = []
}
const applyCustomRange = (start: string, end: string) => {
if (!start || !end) return
time_type.value = 'custom'
dateRange.value = [start, end]
}
return {
time_type,
dateRange,
resolve,
handleTimeTypeChange,
handleCustomDateChange,
resetTimeFilter,
applyCustomRange,
}
}
+14 -2
View File
@@ -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' }]
}
})
+76 -2
View File
@@ -27,6 +27,20 @@
<el-table :data="tableData" v-loading="loading">
<!-- <el-table-column prop="id" label="ID" width="80" /> -->
<el-table-column prop="phone" label="手机号" />
<el-table-column label="备注" min-width="200">
<template #default="{ row }">
<div class="flex items-center gap-1 min-w-0">
<span class="truncate text-gray-600" :title="row.remark || ''">
{{ row.remark || '—' }}
</span>
<el-tooltip content="编辑备注" placement="top">
<el-icon class="shrink-0 cursor-pointer text-gray-400 hover:text-primary" @click="openQuickRemark(row)">
<Edit />
</el-icon>
</el-tooltip>
</div>
</template>
</el-table-column>
<el-table-column prop="create_time" label="创建时间" width="180" />
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
@@ -70,6 +84,16 @@
<el-form-item label="密码" prop="password">
<el-input v-model="formData.password" placeholder="为空则默认为 123456" show-password />
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input
v-model="formData.remark"
type="textarea"
:rows="3"
maxlength="500"
show-word-limit
placeholder="可填写用户相关信息,仅管理员可见"
/>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-switch v-model="formData.status" :active-value="1" :inactive-value="0" />
</el-form-item>
@@ -79,12 +103,30 @@
<el-button type="primary" @click="submitForm" :loading="submitLoading">确定</el-button>
</template>
</el-dialog>
<!-- 快捷备注 -->
<el-dialog v-model="remarkDialogVisible" title="快捷备注" width="420px" destroy-on-close>
<div class="text-sm text-gray-500 mb-3">账号{{ remarkTargetPhone }}</div>
<el-input
v-model="remarkDraft"
type="textarea"
:rows="4"
maxlength="500"
show-word-limit
placeholder="可填写用户相关信息,仅管理员可见"
/>
<template #footer>
<el-button @click="remarkDialogVisible = false">取消</el-button>
<el-button type="primary" :loading="remarkSaving" @click="saveQuickRemark">保存</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Edit } from '@element-plus/icons-vue'
import { apiAssetUserList, apiAssetUserAdd, apiAssetUserEdit, apiAssetUserDelete } from '@/api/asset'
const loading = ref(false)
@@ -105,10 +147,17 @@ const dialogTitle = ref('')
const submitLoading = ref(false)
const formRef = ref()
const remarkDialogVisible = ref(false)
const remarkSaving = ref(false)
const remarkDraft = ref('')
const remarkTargetId = ref<number | string>('')
const remarkTargetPhone = ref('')
const formData = reactive({
id: '',
phone: '',
password: '',
remark: '',
status: 1
})
@@ -154,16 +203,41 @@ const handleStatusChange = async (row: any) => {
const handleAdd = () => {
dialogTitle.value = '新增账号'
Object.assign(formData, { id: '', phone: '', password: '', status: 1 })
Object.assign(formData, { id: '', phone: '', password: '', remark: '', status: 1 })
dialogVisible.value = true
}
const handleEdit = (row: any) => {
dialogTitle.value = '编辑账号'
Object.assign(formData, { id: row.id, phone: row.phone, password: '', status: row.status })
Object.assign(formData, { id: row.id, phone: row.phone, password: '', remark: row.remark || '', status: row.status })
dialogVisible.value = true
}
const openQuickRemark = (row: any) => {
remarkTargetId.value = row.id
remarkTargetPhone.value = row.phone || ''
remarkDraft.value = row.remark || ''
remarkDialogVisible.value = true
}
const saveQuickRemark = async () => {
if (!remarkTargetId.value) return
remarkSaving.value = true
try {
await apiAssetUserEdit({ id: remarkTargetId.value, remark: remarkDraft.value })
const target = tableData.value.find((item: any) => item.id === remarkTargetId.value)
if (target) {
target.remark = remarkDraft.value
}
ElMessage.success('备注已保存')
remarkDialogVisible.value = false
} catch (e) {
console.error(e)
} finally {
remarkSaving.value = false
}
}
const handleDelete = async (row: any) => {
try {
await ElMessageBox.confirm('确定要删除该账号及其关联资源吗?', '提示', { type: 'warning' })
+49 -13
View File
@@ -458,7 +458,9 @@
alt="医师签名"
class="rx-bot-sign-img"
/>
<div class="rx-bot-doctor-name">{{ slipView.doctor_name || '—' }}</div>
<div v-if="!slipView.doctor_signature" class="rx-bot-doctor-name">
{{ slipView.doctor_name || '—' }}
</div>
</div>
</div>
<div class="rx-bot-cell rx-bot-meta">
@@ -679,17 +681,21 @@
粘贴文本解析药名与剂量须与药品库名称完全一致才录入导入至主方
</span>
</div>
<div v-if="herbsLocked" class="flex items-center gap-2 flex-wrap mt-2 text-xs text-[var(--el-color-danger)]">
<el-tag type="danger" size="small" effect="plain">禁用修改</el-tag>
禁用修改处方库模板药材已锁定不可增删改如需调整请重新从处方库导入覆盖
</div>
<div class="herb-formula-block mt-3">
<div class="herb-formula-block__head">
<el-tag type="primary" size="small">主方</el-tag>
<el-button type="primary" size="small" @click="addHerb('主方')">添加主方药材</el-button>
<el-button type="primary" size="small" :disabled="herbsLocked" @click="addHerb('主方')">添加主方药材</el-button>
</div>
<el-table :data="mainHerbRows" class="mt-2" border empty-text="暂无主方药材">
<el-table-column label="序号" type="index" width="60" />
<el-table-column label="药材名称" min-width="220">
<template #default="{ row }">
<MedicineNameSelect v-model="editForm.herbs[row.index].name" class="w-full" />
<MedicineNameSelect v-model="editForm.herbs[row.index].name" :disabled="herbsLocked" class="w-full" />
</template>
</el-table-column>
<el-table-column label="剂量(克)" min-width="120">
@@ -699,6 +705,7 @@
:min="0"
:precision="1"
:step="0.5"
:disabled="herbsLocked"
placeholder="剂量"
class="w-full"
/>
@@ -706,7 +713,8 @@
</el-table-column>
<el-table-column label="操作" width="80">
<template #default="{ row }">
<el-button type="danger" link @click="removeHerb(row.index)">删除</el-button>
<el-button v-if="!herbsLocked" type="danger" link @click="removeHerb(row.index)">删除</el-button>
<span v-else class="text-gray-400 text-xs">已锁定</span>
</template>
</el-table-column>
</el-table>
@@ -715,7 +723,7 @@
<div class="herb-formula-block mt-4">
<div class="herb-formula-block__head">
<el-tag type="warning" size="small">辅方</el-tag>
<el-button type="primary" size="small" plain @click="addHerb('辅方')">
<el-button type="primary" size="small" plain :disabled="herbsLocked" @click="addHerb('辅方')">
添加辅方药材
</el-button>
</div>
@@ -723,7 +731,7 @@
<el-table-column label="序号" type="index" width="60" />
<el-table-column label="药材名称" min-width="220">
<template #default="{ row }">
<MedicineNameSelect v-model="editForm.herbs[row.index].name" class="w-full" />
<MedicineNameSelect v-model="editForm.herbs[row.index].name" :disabled="herbsLocked" class="w-full" />
</template>
</el-table-column>
<el-table-column label="剂量(克)" min-width="120">
@@ -733,6 +741,7 @@
:min="0"
:precision="1"
:step="0.5"
:disabled="herbsLocked"
placeholder="剂量"
class="w-full"
/>
@@ -740,7 +749,8 @@
</el-table-column>
<el-table-column label="操作" width="80">
<template #default="{ row }">
<el-button type="danger" link @click="removeHerb(row.index)">删除</el-button>
<el-button v-if="!herbsLocked" type="danger" link @click="removeHerb(row.index)">删除</el-button>
<span v-else class="text-gray-400 text-xs">已锁定</span>
</template>
</el-table-column>
</el-table>
@@ -1698,7 +1708,7 @@ import jsPDF from 'jspdf'
const TcmDiagnosisEditView = defineAsyncComponent(() => import('@/views/tcm/diagnosis/edit.vue'))
type FormulaType = '主方' | '辅方'
type HerbRow = { name: string; dosage: number; formula_type: FormulaType }
type HerbRow = { name: string; dosage: number; formula_type: FormulaType; locked?: boolean }
type AuxUsageForm = {
dosage_amount?: number
@@ -1798,11 +1808,15 @@ function normalizeFormulaType(v: unknown): FormulaType {
}
function normalizeHerbRow(raw: any): HerbRow {
return {
const row: HerbRow = {
name: String(raw?.name ?? '').trim(),
dosage: Number(raw?.dosage) || 0,
formula_type: normalizeFormulaType(raw?.formula_type)
}
if (raw?.locked === true || raw?.locked === 1) {
row.locked = true
}
return row
}
function normalizeSlipHerbs(raw: unknown): HerbRow[] {
@@ -2481,6 +2495,12 @@ const auxHerbRows = computed(() =>
.filter(({ herb }) => normalizeFormulaType(herb.formula_type) === '辅方')
)
/**
* 整张处方是否被锁定只要任一药材来自禁用修改处方库模板即锁定
* 锁定后不可增删改任意药材只能再次从处方库导入覆盖
*/
const herbsLocked = computed(() => editForm.herbs.some((h) => (h as HerbRow).locked === true))
const slipDietaryText = computed(() => {
const d = slipView.value?.dietary_taboo
if (Array.isArray(d)) return d.filter(Boolean).join('、')
@@ -3441,10 +3461,18 @@ const handleImportLibrary = (row: any) => {
return
}
const formulaType = normalizeFormulaType(row.formula_type)
const imported = (JSON.parse(JSON.stringify(row.herbs)) as any[]).map((h) => ({
...normalizeHerbRow(h),
formula_type: formulaType
}))
//
const isLockedTemplate = Number(row?.disable_edit) === 1
const imported = (JSON.parse(JSON.stringify(row.herbs)) as any[]).map((h) => {
const herb = normalizeHerbRow(h)
herb.formula_type = formulaType
if (isLockedTemplate) {
herb.locked = true
} else {
delete herb.locked
}
return herb
})
if (libraryImportMode.value === 'replace') {
const kept = editForm.herbs.filter((h) => normalizeFormulaType(h.formula_type) !== formulaType)
editForm.herbs = [...kept, ...imported]
@@ -3650,6 +3678,10 @@ async function handlePasteRecipeImport() {
// /
const addHerb = (formulaType: FormulaType = '主方') => {
if (herbsLocked.value) {
feedback.msgWarning('该处方含「禁用修改」模板,药材已锁定,请重新从处方库导入覆盖')
return
}
editForm.herbs.push({
name: '',
dosage: 0,
@@ -3659,6 +3691,10 @@ const addHerb = (formulaType: FormulaType = '主方') => {
//
const removeHerb = (index: number) => {
if (herbsLocked.value) {
feedback.msgWarning('该处方含「禁用修改」模板,药材已锁定,不可删除')
return
}
editForm.herbs.splice(index, 1)
}
+27 -1
View File
@@ -71,6 +71,13 @@
</el-tag>
</template>
</el-table-column>
<el-table-column label="禁用修改" min-width="100">
<template #default="{ row }">
<el-tag :type="row.disable_edit ? 'danger' : 'info'">
{{ row.disable_edit ? '已禁用' : '可修改' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="创建人" prop="creator_name" min-width="100" />
<el-table-column label="创建时间" prop="create_time" min-width="160" />
<el-table-column label="操作" width="180" fixed="right">
@@ -193,6 +200,21 @@
勾选后所有医生都可以查看和使用此处方模板
</div>
</el-form-item>
<el-form-item label="禁用修改" prop="disable_edit">
<div class="flex flex-col gap-1 leading-none">
<el-switch
v-model="editForm.disable_edit"
:active-value="1"
:inactive-value="0"
active-text="开启"
inactive-text="关闭"
/>
<div class="text-xs text-gray-400 leading-relaxed">
开启后导入此模板到处方时将锁定整张处方的药材不可增删改只能再次导入覆盖
</div>
</div>
</el-form-item>
</el-form>
<template #footer v-if="editMode !== 'view'">
@@ -236,7 +258,8 @@ const editForm = reactive({
prescription_name: '',
formula_type: '主方',
herbs: [] as Array<{ name: string; dosage: number }>,
is_public: 0
is_public: 0,
disable_edit: 0
})
//
@@ -293,6 +316,7 @@ const resetForm = () => {
editForm.formula_type = '主方'
editForm.herbs = []
editForm.is_public = 0
editForm.disable_edit = 0
}
//
@@ -310,6 +334,7 @@ const handleView = (row: any) => {
editForm.formula_type = row.formula_type || '主方'
editForm.herbs = row.herbs ? JSON.parse(JSON.stringify(row.herbs)) : []
editForm.is_public = row.is_public ?? 0
editForm.disable_edit = row.disable_edit ?? 0
showEdit.value = true
}
@@ -321,6 +346,7 @@ const handleEdit = (row: any) => {
editForm.formula_type = row.formula_type || '主方'
editForm.herbs = row.herbs ? JSON.parse(JSON.stringify(row.herbs)) : []
editForm.is_public = row.is_public ?? 0
editForm.disable_edit = row.disable_edit ?? 0
showEdit.value = true
}
@@ -173,17 +173,16 @@
</div>
</div>
<el-form class="ls-form" :model="queryParams" inline>
<el-form-item class="w-[360px] min-w-[240px] max-w-full" label="创建时间">
<DaterangePicker
v-model:startTime="queryParams.start_time"
v-model:endTime="queryParams.end_time"
picker-type="daterange"
value-format="YYYY-MM-DD"
start-placeholder="开始日期"
end-placeholder="结束日期"
class="!w-full"
/>
</el-form-item>
<list-time-filter
v-model:time-type="time_type"
v-model:date-range="dateRange"
label="创建时间"
variant="inline"
show-all
picker-width="230px"
@time-type-change="onTimeTypeChange"
@custom-change="onCustomDateChange"
/>
<el-form-item class="w-[260px]" label="订单号">
<el-input
v-model="queryParams.order_no"
@@ -797,49 +796,50 @@
</div>
<!-- 顶部数据概览卡片 -->
<el-row :gutter="16" class="mb-5">
<el-col :xs="12" :sm="6">
<el-card shadow="never" class="stat-card border border-gray-100 bg-gray-50/50">
<div class="flex items-center justify-between mb-1">
<div class="stat-title text-gray-500 text-xs">总金额</div>
<el-button
v-if="canUpdateAmount(detailData)"
v-perms="['tcm.prescriptionOrder/updateAmount']"
type="primary"
size="small"
link
@click="openUpdateAmount"
>
修改
</el-button>
</div>
<div class="stat-value 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="stat-card border border-gray-100 bg-gray-50/50">
<div class="stat-title text-gray-500 text-xs mb-1">已付总额</div>
<div class="stat-value 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="stat-card border border-gray-100 bg-gray-50/50">
<div class="stat-title text-gray-500 text-xs mb-1">需代收</div>
<div
class="stat-value font-bold text-xl"
:class="detailAgencyToCollect > 0 ? 'text-amber-600' : 'text-gray-500'"
<div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-4 mb-5">
<el-card shadow="never" class="stat-card border border-gray-100 bg-gray-50/50">
<div class="flex items-center justify-between mb-1">
<div class="stat-title text-gray-500 text-xs">总金额</div>
<el-button
v-if="canUpdateAmount(detailData)"
v-perms="['tcm.prescriptionOrder/updateAmount']"
type="primary"
size="small"
link
@click="openUpdateAmount"
>
¥{{ formatMoney(detailAgencyToCollect) }}
</div>
</el-card>
</el-col>
<el-col :xs="12" :sm="6">
<el-card shadow="never" class="stat-card border border-gray-100 bg-gray-50/50">
<div class="stat-title text-gray-500 text-xs mb-1">已付笔数</div>
<div class="stat-value text-primary font-bold text-xl">{{ detailLinkedPayOrders.length }} </div>
</el-card>
</el-col>
</el-row>
修改
</el-button>
</div>
<div class="stat-value text-red-500 font-bold text-xl">¥{{ detailData.amount }}</div>
</el-card>
<el-card shadow="never" class="stat-card border border-gray-100 bg-gray-50/50">
<div class="stat-title text-gray-500 text-xs mb-1">已付总额</div>
<div class="stat-value text-green-600 font-bold text-xl">¥{{ detailData.linked_pay_paid_total || 0 }}</div>
</el-card>
<el-card shadow="never" class="stat-card border border-gray-100 bg-gray-50/50">
<div class="stat-title text-gray-500 text-xs mb-1">退款金额</div>
<div
class="stat-value font-bold text-xl"
:class="Number(detailData.refund_amount) > 0 ? 'text-rose-500' : 'text-gray-400'"
>
¥{{ formatMoney(detailData.refund_amount || 0) }}
</div>
</el-card>
<el-card shadow="never" class="stat-card border border-gray-100 bg-gray-50/50">
<div class="stat-title text-gray-500 text-xs mb-1">需代收</div>
<div
class="stat-value font-bold text-xl"
:class="detailAgencyToCollect > 0 ? 'text-amber-600' : 'text-gray-500'"
>
¥{{ formatMoney(detailAgencyToCollect) }}
</div>
</el-card>
<el-card shadow="never" class="stat-card border border-gray-100 bg-gray-50/50">
<div class="stat-title text-gray-500 text-xs mb-1">已付笔数</div>
<div class="stat-value text-primary font-bold text-xl">{{ detailLinkedPayOrders.length }} </div>
</el-card>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<!-- 左侧处方单 -->
@@ -1096,6 +1096,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 }">
@@ -1154,6 +1159,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 }">
@@ -1353,6 +1363,17 @@
<template #icon><el-icon><Refresh /></el-icon></template>
刷新轨迹
</el-button>
<el-button
v-perms="['tcm.prescriptionOrder/logisticsTrace']"
type="warning"
plain
size="default"
:loading="jdLogisticsUpdating"
@click="updateJdLogistics"
>
<template #icon><el-icon><Van /></el-icon></template>
京东接口更新
</el-button>
<div class="ml-2 flex items-center gap-4 border-l pl-4 border-gray-200">
<el-link :href="detailOfficialUrls.sf" target="_blank" type="primary" :underline="false" class="text-[13px]">顺丰官网</el-link>
<el-link :href="detailOfficialUrls.jd" target="_blank" type="primary" :underline="false" class="text-[13px]">京东物流</el-link>
@@ -1772,7 +1793,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"
>
@@ -1782,7 +1803,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>
@@ -2276,13 +2297,13 @@
>
<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="link">关联已有</el-radio>
<el-radio value="completion_only">直接完单申请</el-radio>
</el-radio-group>
</el-form-item>
<!-- 手动创建模式 -->
<!-- 付呗手动创建支付单 -->
<template v-if="addPayOrderForm.add_mode === 'create'">
<el-form-item label="费用类别" prop="order_type">
<el-select v-model="addPayOrderForm.order_type" class="w-full">
@@ -2347,12 +2368,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>
@@ -2513,7 +2539,11 @@
<div class="rx-title">{{ prescriptionTabType === 'internal' ? '药房联' : SLIP_TITLE }}</div>
<div class="rx-notice">
<span class="rx-notice-text">
服药前请核对姓名电话医生等信息以及服法医嘱等要点
{{
prescriptionTabType === 'user'
? '服药前请核对姓名、电话、医生等信息以及医嘱等要点'
: '服药前请核对姓名、电话、医生等信息以及服法、医嘱等要点'
}}
</span>
<div class="rx-notice-meta">
<div class="rx-meta-item">
@@ -2637,21 +2667,29 @@
</div>
<div class="rx-text">
<p v-if="prescriptionTabType === 'internal' || rxAuxUsageText">主服法{{ rxUsageText }}</p>
<p v-else>服法{{ rxUsageText }}</p>
<p v-if="rxAuxUsageText">服法{{ rxAuxUsageText }}</p>
<template v-if="prescriptionTabType === 'internal'">
<p v-if="rxAuxUsageText">服法{{ rxUsageText }}</p>
<p v-else>服法{{ rxUsageText }}</p>
<p v-if="rxAuxUsageText">辅服法{{ rxAuxUsageText }}</p>
</template>
<p v-if="rxAdviceText">医嘱{{ rxAdviceText }}</p>
<p v-if="rxRemarkText">备注{{ rxRemarkText }}</p>
<p v-if="prescriptionTabType === 'internal' && rxRemarkText">备注{{ rxRemarkText }}</p>
<p v-if="rxPharmacyRemarkText" class="rx-text-warn">
药房备注{{ rxPharmacyRemarkText }}
</p>
<p v-if="rxOutPelletText && prescriptionViewData?.prescription_type !== '饮片'" class="rx-text-warn">
<p
v-if="prescriptionTabType === 'internal' && rxOutPelletText && prescriptionViewData?.prescription_type !== '饮片'"
class="rx-text-warn"
>
出丸{{ rxOutPelletText }}
</p>
</div>
<div class="rx-bottom">
<div class="rx-bot-row rx-bot-row-1">
<div
class="rx-bot-row rx-bot-row-1"
:class="{ 'rx-bot-row-1--user': prescriptionTabType === 'user' }"
>
<div class="rx-bot-cell rx-bot-doctor">
<div class="rx-bot-label rx-bot-label--doctor">医师</div>
<div class="rx-bot-doctor-body">
@@ -2661,11 +2699,14 @@
alt="医师签名"
class="rx-bot-sign-img"
/>
<!-- <span class="rx-bot-doctor-name">{{ prescriptionViewData.doctor_name || '—' }}</span> -->
<span
v-if="!prescriptionViewData.doctor_signature"
class="rx-bot-doctor-name"
>{{ prescriptionViewData.doctor_name || '—' }}</span>
</div>
</div>
<div class="rx-bot-cell rx-bot-meta">
<div v-if="prescriptionTabType === 'internal'" class="rx-bot-cell rx-bot-meta">
<span class="rx-bot-meta-key">类型:</span>
<span class="rx-bot-meta-val">{{ rxTypeText }}</span>
</div>
@@ -2679,7 +2720,7 @@
: '—'
}} </span>
</div>
<div class="rx-bot-cell rx-bot-meta">
<div v-if="prescriptionTabType === 'internal'" class="rx-bot-cell rx-bot-meta">
<span class="rx-bot-meta-key">剂量:</span>
<span class="rx-bot-meta-val">{{ rxPerDoseAmount }}</span>
</div>
@@ -2855,8 +2896,9 @@
<script lang="ts" setup name="prescriptionOrderList">
import { computed, onMounted, reactive, ref, nextTick, watch, defineAsyncComponent } from 'vue'
import { useRoute } from 'vue-router'
import { Refresh, Loading, ArrowDown, InfoFilled, QuestionFilled, Search, Calendar, Document, Link as LinkIcon, Wallet } from '@element-plus/icons-vue'
import DaterangePicker from '@/components/daterange-picker/index.vue'
import { Refresh, Loading, ArrowDown, InfoFilled, QuestionFilled, Search, Calendar, Document, Link as LinkIcon, Wallet, Van } from '@element-plus/icons-vue'
import ListTimeFilter from '@/components/list-time-filter/index.vue'
import { useListTimeFilter } from '@/hooks/useListTimeFilter'
import {
prescriptionOrderAuditPayment,
prescriptionOrderAuditPrescription,
@@ -2867,6 +2909,7 @@ import {
prescriptionOrderPaidPayOrders,
prescriptionOrderWithdraw,
prescriptionOrderLogisticsTrace,
prescriptionOrderLogisticsJdUpdate,
prescriptionOrderShip,
prescriptionOrderLogs,
prescriptionOrderUpdateAmount,
@@ -3162,9 +3205,6 @@ function handleExpressCompanyTabClick(value: '' | 'sf' | 'jd') {
}
const queryParams = reactive({
/** 创建时间区间(与列表筛选一致;不影响「今日」统计) */
start_time: '' as string,
end_time: '' as string,
order_no: '',
prescription_id: '' as string | number,
/** 诊单患者姓名/手机号(后台关联 diagnosis */
@@ -3252,6 +3292,24 @@ function handleFocusBoardSearch(key: 'pendingRx' | 'pendingPay' | 'pendingShip'
resetPage()
}
const {
time_type,
dateRange,
resolve: resolveCreateTimeRange,
handleTimeTypeChange,
handleCustomDateChange,
resetTimeFilter,
applyCustomRange,
} = useListTimeFilter('all')
const handleQuery = () => {
pager.page = 1
getLists()
}
const onTimeTypeChange = () => handleTimeTypeChange(handleQuery)
const onCustomDateChange = () => handleCustomDateChange(handleQuery)
function normalizePrescriptionOrderListQuery(params: Record<string, unknown>): Record<string, unknown> {
const p: Record<string, unknown> = { ...params }
if (p.prescription_id === '' || p.prescription_id === undefined) {
@@ -3331,11 +3389,15 @@ function normalizePrescriptionOrderListQuery(params: Record<string, unknown>): R
}
async function fetchLists(params: Record<string, unknown>) {
const p = normalizePrescriptionOrderListQuery({ ...params })
const [start_time, end_time] = resolveCreateTimeRange()
const p = normalizePrescriptionOrderListQuery({ ...params, start_time, end_time })
return prescriptionOrderLists(p)
}
const prescriptionOrderExportParams = computed(() => normalizePrescriptionOrderListQuery({ ...queryParams }))
const prescriptionOrderExportParams = computed(() => {
const [start_time, end_time] = resolveCreateTimeRange()
return normalizePrescriptionOrderListQuery({ ...queryParams, start_time, end_time })
})
const { pager, getLists, resetPage, resetParams } = usePaging({
fetchFun: fetchLists,
@@ -3370,8 +3432,9 @@ function applyRouteQueryToPrescriptionOrderList() {
}
const st = normDate(q.start_time)
const et = normDate(q.end_time)
if (st) queryParams.start_time = st
if (et) queryParams.end_time = et
if (st && et) {
applyCustomRange(st, et)
}
const ad = q.assistant_dept_id
if (ad !== undefined && ad !== null && String(ad) !== '') {
const n = Number(ad)
@@ -3462,9 +3525,7 @@ const listStats = computed(() => {
scopeHint =
'统计口径:本人创建的订单 + 关联处方开方人为本人的订单(含医助代建);与下方筛选项(不含「创建时间」)一致'
}
const hasCustomTime =
String(queryParams.start_time || '').trim() !== '' && String(queryParams.end_time || '').trim() !== ''
const headPrefix = hasCustomTime ? '区间' : '今日'
const headPrefix = time_type.value === 'all' ? '全部' : time_type.value === 'today' ? '今日' : '区间'
return {
order: n(o),
pay: n(pay),
@@ -3536,8 +3597,7 @@ watch(
function handleReset() {
activeFocusKey.value = ''
queryParams.start_time = ''
queryParams.end_time = ''
resetTimeFilter()
queryParams.order_no = ''
queryParams.prescription_id = ''
queryParams.patient_keyword = ''
@@ -3935,10 +3995,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
@@ -4148,6 +4212,7 @@ function formatOrderTime(v: unknown) {
const detailLogisticsExpress = ref<string>('auto')
const logisticsTraceLoading = ref(false)
const jdLogisticsUpdating = ref(false)
const logisticsTracePayload = ref<Record<string, any> | null>(null)
/** 顺丰/快递100:与运单一致的收件手机后四位(可覆盖订单收货手机) */
const logisticsTracePhoneTail = ref('')
@@ -4381,6 +4446,26 @@ async function fetchLogisticsTrace() {
}
}
//
async function updateJdLogistics() {
const orderId = Number(detailData.value?.id)
const trackingNumber = String(detailData.value?.tracking_number || '').trim()
if (!orderId || !trackingNumber) {
feedback.msgWarning('当前订单未填写快递单号')
return
}
jdLogisticsUpdating.value = true
try {
const res: any = await prescriptionOrderLogisticsJdUpdate({ id: orderId })
feedback.msgSuccess(res?.message || '京东物流轨迹已更新')
await fetchLogisticsTrace()
} catch (e: any) {
feedback.msgError(e?.message || '京东物流更新失败')
} finally {
jdLogisticsUpdating.value = false
}
}
const patchRxPatientVisible = ref(false)
const patchRxPatientSaving = ref(false)
const patchRxPatientFormRef = ref<FormInstance>()
@@ -4626,7 +4711,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)
@@ -4642,6 +4727,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
@@ -5635,7 +5738,7 @@ const addPayOrderAlertTitle = computed(() => {
if (addPayOrderForm.add_mode === 'completion_only') {
return '不创建或关联支付单,仅提交完单申请,由审核人员在支付审核时处理。'
}
return '可以手动创建新支付单,或关联已存在但未绑定的支付单。'
return '可以通过付呗创建新支付单,或关联已存在但未绑定的支付单。'
})
const addPayOrderForm = reactive({
@@ -7091,6 +7194,10 @@ async function downloadPrescriptionSlipPdf() {
min-height: 70px;
}
.rx-bot-row-1--user {
grid-template-columns: 1.6fr 0.6fr;
}
.rx-bot-row-2 {
grid-template-columns: repeat(6, 1fr);
}
@@ -190,18 +190,17 @@
<ArrowDown />
</el-icon>
</div>
<el-form v-show="!h5FilterCollapsed" class="ls-form" :model="queryParams" inline>
<el-form-item class="w-[360px] min-w-[240px] max-w-full" label="创建时间">
<DaterangePicker
v-model:startTime="queryParams.start_time"
v-model:endTime="queryParams.end_time"
picker-type="daterange"
value-format="YYYY-MM-DD"
start-placeholder="开始日期"
end-placeholder="结束日期"
class="!w-full"
/>
</el-form-item>
<el-form v-show="!h5FilterCollapsed" class="ls-form stats-filter-form" :model="queryParams" inline>
<list-time-filter
v-model:time-type="time_type"
v-model:date-range="dateRange"
label="创建时间"
variant="inline"
show-all
picker-width="260px"
@time-type-change="onTimeTypeChange"
@custom-change="onCustomDateChange"
/>
<el-form-item class="w-[260px]" label="订单号">
<el-input
v-model="queryParams.order_no"
@@ -703,49 +702,50 @@
</div>
<!-- 顶部数据概览卡片 -->
<el-row :gutter="16" class="mb-5">
<el-col :xs="12" :sm="6">
<el-card shadow="never" class="stat-card border border-gray-100 bg-gray-50/50">
<div class="flex items-center justify-between mb-1">
<div class="stat-title text-gray-500 text-xs">总金额</div>
<el-button
v-if="canUpdateAmount(detailData)"
v-perms="['tcm.prescriptionOrder/updateAmount']"
type="primary"
size="small"
link
@click="openUpdateAmount"
>
修改
</el-button>
</div>
<div class="stat-value 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="stat-card border border-gray-100 bg-gray-50/50">
<div class="stat-title text-gray-500 text-xs mb-1">已付总额</div>
<div class="stat-value 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="stat-card border border-gray-100 bg-gray-50/50">
<div class="stat-title text-gray-500 text-xs mb-1">需代收</div>
<div
class="stat-value font-bold text-xl"
:class="detailAgencyToCollect > 0 ? 'text-amber-600' : 'text-gray-500'"
<div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-4 mb-5">
<el-card shadow="never" class="stat-card border border-gray-100 bg-gray-50/50">
<div class="flex items-center justify-between mb-1">
<div class="stat-title text-gray-500 text-xs">总金额</div>
<el-button
v-if="canUpdateAmount(detailData)"
v-perms="['tcm.prescriptionOrder/updateAmount']"
type="primary"
size="small"
link
@click="openUpdateAmount"
>
¥{{ formatMoney(detailAgencyToCollect) }}
</div>
</el-card>
</el-col>
<el-col :xs="12" :sm="6">
<el-card shadow="never" class="stat-card border border-gray-100 bg-gray-50/50">
<div class="stat-title text-gray-500 text-xs mb-1">已付笔数</div>
<div class="stat-value text-primary font-bold text-xl">{{ detailLinkedPayOrders.length }} </div>
</el-card>
</el-col>
</el-row>
修改
</el-button>
</div>
<div class="stat-value text-red-500 font-bold text-xl">¥{{ detailData.amount }}</div>
</el-card>
<el-card shadow="never" class="stat-card border border-gray-100 bg-gray-50/50">
<div class="stat-title text-gray-500 text-xs mb-1">已付总额</div>
<div class="stat-value text-green-600 font-bold text-xl">¥{{ detailData.linked_pay_paid_total || 0 }}</div>
</el-card>
<el-card shadow="never" class="stat-card border border-gray-100 bg-gray-50/50">
<div class="stat-title text-gray-500 text-xs mb-1">退款金额</div>
<div
class="stat-value font-bold text-xl"
:class="Number(detailData.refund_amount) > 0 ? 'text-rose-500' : 'text-gray-400'"
>
¥{{ formatMoney(detailData.refund_amount || 0) }}
</div>
</el-card>
<el-card shadow="never" class="stat-card border border-gray-100 bg-gray-50/50">
<div class="stat-title text-gray-500 text-xs mb-1">需代收</div>
<div
class="stat-value font-bold text-xl"
:class="detailAgencyToCollect > 0 ? 'text-amber-600' : 'text-gray-500'"
>
¥{{ formatMoney(detailAgencyToCollect) }}
</div>
</el-card>
<el-card shadow="never" class="stat-card border border-gray-100 bg-gray-50/50">
<div class="stat-title text-gray-500 text-xs mb-1">已付笔数</div>
<div class="stat-value text-primary font-bold text-xl">{{ detailLinkedPayOrders.length }} </div>
</el-card>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<!-- 左侧处方单 -->
@@ -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,12 +1997,13 @@
>
<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="link">关联已有</el-radio>
<el-radio value="completion_only">直接完单申请</el-radio>
</el-radio-group>
</el-form-item>
<!-- 手动创建模式 -->
<!-- 付呗手动创建支付单 -->
<template v-if="addPayOrderForm.add_mode === 'create'">
<el-form-item label="费用类别" prop="order_type">
<el-select v-model="addPayOrderForm.order_type" class="w-full">
@@ -2028,6 +2039,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 +2068,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 +2089,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>
@@ -2300,7 +2330,10 @@
alt="医师签名"
class="rx-bot-sign-img"
/>
<!-- <span class="rx-bot-doctor-name">{{ prescriptionViewData.doctor_name || '—' }}</span> -->
<span
v-if="!prescriptionViewData.doctor_signature"
class="rx-bot-doctor-name"
>{{ prescriptionViewData.doctor_name || '—' }}</span>
</div>
</div>
@@ -2507,7 +2540,8 @@ import {
Wallet,
User
} from '@element-plus/icons-vue'
import DaterangePicker from '@/components/daterange-picker/index.vue'
import ListTimeFilter from '@/components/list-time-filter/index.vue'
import { useListTimeFilter } from '@/hooks/useListTimeFilter'
import {
prescriptionOrderAuditPayment,
prescriptionOrderAuditPrescription,
@@ -2527,6 +2561,7 @@ import {
prescriptionOrderRevokePayAudit,
prescriptionOrderPatchPrescriptionPatient,
prescriptionOrderLinkPayOrder,
prescriptionOrderRequestCompletion,
prescriptionOrderSubmitGancaoRecipel,
prescriptionOrderPreviewGancaoRecipel,
prescriptionDetail,
@@ -2805,9 +2840,6 @@ function handleExpressCompanyTabClick(value: '' | 'sf' | 'jd') {
}
const queryParams = reactive({
/** 创建时间区间(与列表筛选一致;不影响「今日」统计) */
start_time: '' as string,
end_time: '' as string,
order_no: '',
prescription_id: '' as string | number,
/** 诊单患者姓名/手机号(后台关联 diagnosis */
@@ -2886,8 +2918,26 @@ function handleFocusBoardSearch(key: 'pendingRx' | 'pendingPay' | 'pendingShip'
resetPage()
}
const {
time_type,
dateRange,
resolve: resolveCreateTimeRange,
handleTimeTypeChange,
handleCustomDateChange,
resetTimeFilter,
} = useListTimeFilter('all')
const handleQuery = () => {
pager.page = 1
getLists()
}
const onTimeTypeChange = () => handleTimeTypeChange(handleQuery)
const onCustomDateChange = () => handleCustomDateChange(handleQuery)
async function fetchLists(params: Record<string, unknown>) {
const p: Record<string, unknown> = { ...params }
const [start_time, end_time] = resolveCreateTimeRange()
const p: Record<string, unknown> = { ...params, start_time, end_time }
if (p.prescription_id === '' || p.prescription_id === undefined) {
delete p.prescription_id
} else {
@@ -3041,9 +3091,7 @@ const listStats = computed(() => {
scopeHint =
'统计口径:本人创建的订单 + 关联处方开方人为本人的订单(含医助代建);与下方筛选项(不含「创建时间」)一致'
}
const hasCustomTime =
String(queryParams.start_time || '').trim() !== '' && String(queryParams.end_time || '').trim() !== ''
const headPrefix = hasCustomTime ? '区间' : '今日'
const headPrefix = time_type.value === 'all' ? '全部' : time_type.value === 'today' ? '今日' : '区间'
return {
order: n(o),
pay: n(pay),
@@ -3114,8 +3162,7 @@ watch(
function handleReset() {
activeFocusKey.value = ''
queryParams.start_time = ''
queryParams.end_time = ''
resetTimeFilter()
queryParams.order_no = ''
queryParams.prescription_id = ''
queryParams.patient_keyword = ''
@@ -3381,11 +3428,11 @@ const h5FilterCollapsed = ref(true)
const h5ActiveFilterCount = computed(() => {
const q = queryParams as any
const keys = [
'start_time', 'end_time', 'order_no', 'prescription_id',
'order_no', 'prescription_id',
'patient_keyword', 'express_keyword', 'doctor_id',
'assistant_dept_id', 'assistant_id'
]
let n = 0
let n = time_type.value !== 'all' ? 1 : 0
for (const k of keys) {
const v = q[k]
if (v === null || v === undefined) continue
@@ -3479,10 +3526,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
@@ -4025,7 +4076,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)
@@ -4041,6 +4092,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
@@ -4882,8 +4951,14 @@ 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 '不创建或关联支付单,仅提交完单申请,由审核人员在支付审核时处理。'
}
return '可以通过付呗创建新支付单,或关联已存在但未绑定的支付单。'
})
const addPayOrderForm = reactive({
add_mode: 'create' as 'create' | 'link',
add_mode: 'create' as 'create' | 'link' | 'completion_only',
order_type: 3,
pay_amount: undefined as number | undefined,
pay_remark: '',
@@ -4891,38 +4966,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') {
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) {
@@ -4970,7 +5051,10 @@ 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') {
//
await prescriptionOrderAddPayOrder({
id: addPayOrderRowId.value,
@@ -5520,6 +5604,12 @@ async function downloadPrescriptionSlipPdf() {
</script>
<style scoped lang="scss">
.stats-filter-form {
:deep(.el-form-item) {
margin-bottom: 0;
}
}
.prescription-order-page {
margin: 0 auto;
+50 -17
View File
@@ -14,13 +14,13 @@
</el-card>
<el-card class="!border-none" shadow="never">
<el-form class="mb-[-16px]" :model="queryParams" inline>
<el-form-item label="日期范围">
<daterange-picker
v-model:startTime="queryParams.start_date"
v-model:endTime="queryParams.end_date"
/>
</el-form-item>
<el-form :inline="true" :model="queryParams" class="stats-filter-form">
<list-time-filter
v-model:time-type="time_type"
v-model:date-range="dateRange"
@time-type-change="onTimeTypeChange"
@custom-change="onCustomDateChange"
/>
<el-form-item label="自媒体渠道">
<el-select
v-model="queryParams.media_channel_code"
@@ -67,11 +67,11 @@
v-model="queryParams.remark"
placeholder="备注 / 创建人 / 修改人"
clearable
@keyup.enter="resetPage"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="resetPage">查询</el-button>
<el-button type="primary" :loading="pager.loading" @click="handleQuery">查询</el-button>
<el-button @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
@@ -148,7 +148,9 @@
<script lang="ts" setup name="accountCostList">
import { accountCostDelete, accountCostLists } from '@/api/finance'
import { usePaging } from '@/hooks/usePaging'
import { useListTimeFilter } from '@/hooks/useListTimeFilter'
import feedback from '@/utils/feedback'
import ListTimeFilter from '@/components/list-time-filter/index.vue'
import EditPopup from './edit.vue'
@@ -172,18 +174,40 @@ const editRef = shallowRef<InstanceType<typeof EditPopup>>()
const showEdit = ref(false)
const queryParams = reactive({
start_date: '',
end_date: '',
media_channel_code: '',
dept_id: '',
remark: '',
})
const { pager, getLists, resetPage } = usePaging({
fetchFun: accountCostLists,
const {
time_type,
dateRange,
resolve,
handleTimeTypeChange,
handleCustomDateChange,
resetTimeFilter,
} = useListTimeFilter('today')
const { pager, getLists } = usePaging({
fetchFun: async (params: Record<string, any>) => {
const [start_date, end_date] = resolve()
return accountCostLists({
...params,
start_date,
end_date,
})
},
params: queryParams,
})
const handleQuery = () => {
pager.page = 1
getLists()
}
const onTimeTypeChange = () => handleTimeTypeChange(handleQuery)
const onCustomDateChange = () => handleCustomDateChange(handleQuery)
const mediaChannelOptionsFlat = computed<MediaChannelOption[]>(() => {
const options = pager.extend.media_channel_options
if (!Array.isArray(options)) return []
@@ -258,18 +282,27 @@ const handleDelete = async (id: number) => {
}
const handleReset = () => {
queryParams.start_date = ''
queryParams.end_date = ''
resetTimeFilter()
queryParams.media_channel_code = ''
queryParams.dept_id = ''
queryParams.remark = ''
resetPage()
pager.page = 1
pager.size = 15
getLists()
}
getLists()
onMounted(() => {
getLists()
})
</script>
<style scoped lang="scss">
.stats-filter-form {
:deep(.el-form-item) {
margin-bottom: 0;
}
}
.account-cost-channel-select {
:deep(.channel-opt-row) {
display: flex;
+27 -5
View File
@@ -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>
@@ -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 || '-' }}
@@ -1148,7 +1148,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,7 +1161,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: createForm.createType
}
if (createForm.createType === 'fubei') {
params.payment_channel = 'fubei'
@@ -1247,6 +1249,26 @@ const getPaymentMethodText = (method: string | null) => {
return methodMap[method] || '未知'
}
// / create_type 退
const getCreateTypeText = (row: any) => {
const createTypeMap: Record<string, string> = {
normal: '普通订单',
wechat_work: '企业微信对外收款',
fubei: '付呗'
}
const ct = row?.create_type
if (ct === 'wechat_work' || ct === 'fubei') {
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 = {
@@ -14,13 +14,14 @@
</el-card>
<el-card class="!border-none" shadow="never">
<el-form class="mb-[-16px]" :model="queryParams" inline>
<el-form-item label="日期范围">
<daterange-picker
v-model:startTime="queryParams.start_date"
v-model:endTime="queryParams.end_date"
/>
</el-form-item>
<el-form :inline="true" :model="queryParams" class="stats-filter-form">
<list-time-filter
v-model:time-type="time_type"
v-model:date-range="dateRange"
@time-type-change="onTimeTypeChange"
@custom-change="onCustomDateChange"
/>
<el-form-item label="部门">
<el-tree-select
v-model="queryParams.dept_id"
@@ -33,7 +34,7 @@
clearable
filterable
class="w-[220px]"
@change="resetPage"
@change="handleQuery"
/>
</el-form-item>
<el-form-item label="自媒体来源">
@@ -44,7 +45,7 @@
placeholder="全部来源"
:allow-create="false"
select-class="w-[220px]"
@change="resetPage"
@change="handleQuery"
@visible-change="onMediaSourceDropdownVisible"
/>
</el-form-item>
@@ -53,11 +54,11 @@
v-model="queryParams.remark"
placeholder="备注 / 创建人"
clearable
@keyup.enter="resetPage"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="resetPage">查询</el-button>
<el-button type="primary" :loading="pager.loading" @click="handleQuery">查询</el-button>
<el-button @click="handleReset">重置</el-button>
<el-button v-if="selfInputPath" class="ml-2" @click="goSelfInput">返回统计</el-button>
</el-form-item>
@@ -146,6 +147,8 @@ import feedback from '@/utils/feedback'
import CostEditPopup from './cost-edit.vue'
import MediaSourceSelect from './MediaSourceSelect.vue'
import { useMediaSourceOptions } from './useMediaSourceOptions'
import ListTimeFilter from '@/components/list-time-filter/index.vue'
import { useListTimeFilter } from '@/hooks/useListTimeFilter'
const router = useRouter()
const editRef = shallowRef<InstanceType<typeof CostEditPopup>>()
@@ -173,25 +176,48 @@ const goSelfInput = () => {
}
const queryParams = reactive({
start_date: '',
end_date: '',
media_source: '',
dept_id: undefined as number | undefined,
remark: '',
})
const {
time_type,
dateRange,
resolve,
handleTimeTypeChange,
handleCustomDateChange,
resetTimeFilter,
} = useListTimeFilter('today')
const { pager, getLists, resetPage } = usePaging({
fetchFun: personalAccountCostLists,
fetchFun: async (params: Record<string, any>) => {
const [start_date, end_date] = resolve()
return personalAccountCostLists({
...params,
start_date,
end_date,
})
},
params: queryParams,
})
const handleQuery = () => {
pager.page = 1
getLists()
}
const onTimeTypeChange = () => handleTimeTypeChange(handleQuery)
const onCustomDateChange = () => handleCustomDateChange(handleQuery)
const handleReset = () => {
queryParams.start_date = ''
queryParams.end_date = ''
resetTimeFilter()
queryParams.media_source = ''
queryParams.dept_id = undefined
queryParams.remark = ''
resetPage()
pager.page = 1
pager.size = 15
getLists()
}
const onMediaSourceDropdownVisible = (visible: boolean) => {
@@ -236,9 +262,10 @@ const loadDeptOptions = async () => {
}
}
onMounted(() => {
onMounted(async () => {
loadDeptOptions()
loadMediaSourceOptions()
await getLists()
})
</script>
@@ -247,6 +274,12 @@ onMounted(() => {
padding: 20px;
}
.stats-filter-form {
:deep(.el-form-item) {
margin-bottom: 0;
}
}
.dept-cell {
cursor: help;
border-bottom: 1px dashed #c0c4cc;
@@ -415,6 +415,10 @@ const detailLinkedPayOrders = computed(() => {
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
+1 -1
View File
@@ -1262,7 +1262,7 @@ const handleSubmit = async () => {
}
const handleViewCase = (row: any) => {
prescriptionRef.value?.openById(row.id, { slipType: 'user', hideCaseRecord: true })
prescriptionRef.value?.openById(row.id, { slipType: 'user' })
}
//
+115 -1
View File
@@ -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 {
@@ -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());
@@ -57,6 +57,7 @@ class AssetUserController extends BaseAdminController
'phone' => $params['phone'],
'password' => $passwordHash,
'status' => $params['status'] ?? 1,
'remark' => trim((string)($params['remark'] ?? '')),
]);
return $this->success('添加成功', ['id' => $user->id]);
@@ -92,6 +93,10 @@ class AssetUserController extends BaseAdminController
if (isset($params['status'])) {
$user->status = $params['status'];
}
if (array_key_exists('remark', $params)) {
$user->remark = trim((string)$params['remark']);
}
$user->save();
return $this->success('修改成功');
@@ -207,6 +207,11 @@ 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,其余 normal
$createTypeReq = (string)$this->request->post('create_type', '');
$params['create_type'] = in_array($createTypeReq, ['normal', 'wechat_work', 'fubei'], true)
? $createTypeReq
: ($params['payment_channel'] === 'fubei' ? 'fubei' : 'normal');
$result = OrderLogic::create($params);
if (!$result) {
@@ -227,6 +232,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) {
@@ -102,6 +102,24 @@ class PrescriptionOrderController extends BaseAdminController
return $this->success('', $data);
}
/**
* 直接调用京东官方物流接口刷新轨迹并落库(绕过快递100缓存)
*/
public function logisticsJdUpdate()
{
$params = (new PrescriptionOrderValidate())->post()->goCheck('logisticsJdUpdate');
$data = PrescriptionOrderLogic::logisticsJdUpdate(
(int) $params['id'],
$this->adminId,
$this->adminInfo
);
if ($data === null) {
return $this->fail(PrescriptionOrderLogic::getError());
}
return $this->success((string) ($data['message'] ?? '京东物流轨迹已更新'), $data);
}
public function edit()
{
$params = (new PrescriptionOrderValidate())->post()->goCheck('edit');
@@ -126,6 +126,12 @@ class AuthMiddleware
return true;
}
// 京东物流接口刷新:与「物流轨迹」同一数据域,复用 logisticsTrace 权限
if ($accessUri === 'tcm.prescriptionorder/logisticsjdupdate'
&& in_array('tcm.prescriptionorder/logisticstrace', $adminUris, true)) {
return true;
}
// 挂号列表批量改渠道:与单条编辑同一数据域,复用 doctor.appointment/edit
if ($accessUri === 'doctor.appointment/batcheditchannel'
&& in_array('doctor.appointment/edit', $adminUris, true)) {
@@ -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` 判定一致)。
*/
@@ -56,7 +56,7 @@ class PrescriptionLibraryLists extends BaseAdminDataLists implements ListsSearch
public function lists(): array
{
$field = [
'id', 'prescription_name', 'formula_type', 'herbs', 'is_public',
'id', 'prescription_name', 'formula_type', 'herbs', 'is_public', 'disable_edit',
'creator_id', 'creator_name', 'create_time', 'update_time'
];
@@ -778,6 +778,7 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
'export_channel' => '渠道',
'export_guahao_channel_source' => '自媒体渠道(挂号渠道来源)',
'export_medication_form' => '药品形态',
'export_prescription_name' => '药方名称',
'export_service_package' => '服务套餐',
'export_medication_days' => '天数',
'export_amount' => '总金额',
+50 -1
View File
@@ -22,6 +22,11 @@ use think\facade\Log;
*/
class OrderLogic
{
/**
* 创建方式统一三值:normal 普通订单 / wechat_work 企业微信对外收款 / fubei 付呗
*/
private const CREATE_TYPES = ['normal', 'wechat_work', 'fubei'];
/**
* @notes 生成订单号
* @return string
@@ -31,6 +36,34 @@ 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';
}
/**
* @notes 创建订单
* @param array $params
@@ -43,6 +76,8 @@ class OrderLogic
if (!in_array($channel, ['normal', 'fubei'], true)) {
$channel = 'normal';
}
$paymentMethod = $channel === 'fubei' ? 'fubei' : null;
$createType = self::normalizeCreateType((string)($params['create_type'] ?? ''), $paymentMethod);
$requirePaymentSlipAudit = (int)($params['require_payment_slip_audit'] ?? 0) === 1;
$order = new Order();
@@ -51,6 +86,7 @@ class OrderLogic
$order->creator_id = $params['creator_id'];
$order->order_type = $params['order_type'];
$order->amount = $params['amount'];
$order->create_type = $createType;
// 付呗且申请审核支付单:待审核(5);否则待支付(1)
if ($channel === 'fubei') {
$order->payment_method = 'fubei';
@@ -183,6 +219,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 +275,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 +324,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 = '企业微信直接收款,待关联患者';
@@ -536,6 +578,13 @@ 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';
$n->status = 5;
@@ -1213,7 +1262,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();
@@ -11,6 +11,7 @@ use app\common\model\Order;
use app\common\model\tcm\Diagnosis;
use app\common\model\tcm\DiagnosisAssignLog;
use app\common\model\tcm\Prescription;
use app\common\model\tcm\PrescriptionLibrary;
use app\common\model\tcm\PrescriptionOrder;
use app\common\model\tcm\PrescriptionOrderLog;
use app\common\model\tcm\PrescriptionOrderPayOrder;
@@ -695,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);
}
/**
@@ -767,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()
@@ -836,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
@@ -968,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);
@@ -1524,6 +1558,48 @@ class PrescriptionOrderLogic
return $payload;
}
/**
* 直接调用京东官方物流接口刷新轨迹并落库(后台「京东接口更新」按钮)。
*
* @return array{success:bool, message:string, traces_count:int, state:string}|null
*/
public static function logisticsJdUpdate(int $id, int $adminId, array $adminInfo): ?array
{
self::$error = '';
$row = PrescriptionOrder::where('id', $id)->whereNull('delete_time')->find();
if (!$row) {
self::$error = '订单不存在';
return null;
}
if (!self::canAccessOrder($row, $adminId, $adminInfo)) {
self::$error = '无权限操作';
return null;
}
$num = trim((string) ($row->tracking_number ?? ''));
if ($num === '') {
self::$error = '未填写快递单号';
return null;
}
$ret = \app\common\service\ExpressTrackingService::queryJdOfficialAndUpdate($num);
if (empty($ret['success'])) {
self::$error = (string) ($ret['message'] ?? '京东物流更新失败');
return null;
}
return [
'success' => true,
'message' => (string) ($ret['message'] ?? '京东物流轨迹已更新'),
'traces_count' => (int) ($ret['traces_count'] ?? 0),
'state' => (string) ($ret['state'] ?? ''),
];
}
/**
* @param array<string,mixed> $params
* @return array<string,mixed>|false
@@ -1694,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);
@@ -2238,12 +2316,16 @@ 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');
})
->update([
'status' => 5, // 待审核
'payment_time' => null
@@ -2335,7 +2417,8 @@ class PrescriptionOrderLogic
$payOrder->order_type = $orderType;
$payOrder->amount = $amount;
$payOrder->status = 5; // 待审核
$payOrder->payment_method = 'manual';
$payOrder->payment_method = 'fubei'; // 补齐支付单按「付呗」记账
$payOrder->create_type = 'fubei';
$payOrder->payment_time = null; // 审核通过后再设置支付时间
$payOrder->remark = $remark;
@@ -2377,7 +2460,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);
@@ -2488,7 +2573,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);
@@ -3116,7 +3203,7 @@ class PrescriptionOrderLogic
$rxById = [];
if ($rxIdList !== []) {
$rxRows = Prescription::whereIn('id', $rxIdList)->whereNull('delete_time')
->field(['id', 'prescription_type', 'need_decoction', 'dose_unit', 'assistant_id', 'appointment_id'])
->field(['id', 'prescription_type', 'need_decoction', 'dose_unit', 'assistant_id', 'appointment_id', 'prescription_name', 'aux_usage', 'herbs', 'creator_id'])
->select()
->toArray();
foreach ($rxRows as $xr) {
@@ -3124,6 +3211,10 @@ class PrescriptionOrderLogic
}
}
// 药方名称反查:处方库(开方医师 creator_id + 公开模板),按 formula_type + 药材集合匹配,
// 复刻前端 resolveSlipAuxLibraryName 口径(主方/辅方均如此;处方记录 prescription_name 仅做兜底)。
[$libByDoctor, $libPublic] = self::buildPrescriptionLibraryNameIndexForExport($rxById);
if ($needGuahaoChannel) {
$yejiChannelHighlightByDiag = [];
if (
@@ -3329,6 +3420,22 @@ class PrescriptionOrderLogic
}
$item['export_medication_form'] = self::formatMedicationFormForExport(\is_array($rx) ? $rx : []);
// 药方名称:主方/辅方均反查处方库取名称(与处方笺一致),带标签;甘草/无匹配留空
[$mainRxName, $auxRxName] = self::resolvePrescriptionNamesForExport(
\is_array($rx) ? $rx : [],
$libByDoctor,
$libPublic
);
$rxNameParts = [];
if ($mainRxName !== '') {
$rxNameParts[] = '主方:' . $mainRxName;
}
if ($auxRxName !== '') {
$rxNameParts[] = '辅方:' . $auxRxName;
}
$item['export_prescription_name'] = implode(' ', $rxNameParts);
$item['export_service_package'] = self::formatServicePackageForExport(
$item['service_package'] ?? '',
$packageNameByValue
@@ -3352,7 +3459,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;
@@ -3401,6 +3512,186 @@ class PrescriptionOrderLogic
unset($item);
}
/**
* 导出药方名称反查:批量加载相关开方医师处方库(creator_id 命中 + 公开模板),
* 建立 [creator_id][formula_type][herbKey] => name 公开 [formula_type][herbKey] => name 两级索引。
*
* @param array<int, array> $rxById 处方 id => 处方行(需含 herbs / creator_id
*
* @return array{0: array<int, array<string, array<string, string>>>, 1: array<string, array<string, string>>}
*/
private static function buildPrescriptionLibraryNameIndexForExport(array $rxById): array
{
$libByDoctor = [];
$libPublic = [];
if ($rxById === []) {
return [$libByDoctor, $libPublic];
}
$doctorIds = [];
foreach ($rxById as $rx) {
$cid = (int) ($rx['creator_id'] ?? 0);
if ($cid > 0) {
$doctorIds[$cid] = true;
}
}
$doctorIdList = array_keys($doctorIds);
try {
$libRows = PrescriptionLibrary::whereNull('delete_time')
->where(function ($q) use ($doctorIdList) {
if ($doctorIdList !== []) {
$q->whereIn('creator_id', $doctorIdList)->whereOr('is_public', 1);
} else {
$q->where('is_public', 1);
}
})
->field(['prescription_name', 'formula_type', 'herbs', 'creator_id', 'is_public'])
->select()
->toArray();
} catch (\Throwable $e) {
Log::warning('buildPrescriptionLibraryNameIndexForExport failed: ' . $e->getMessage());
return [$libByDoctor, $libPublic];
}
foreach ($libRows as $lib) {
$name = trim((string) ($lib['prescription_name'] ?? ''));
if ($name === '') {
continue;
}
$herbsRaw = $lib['herbs'] ?? null;
if (\is_string($herbsRaw) && $herbsRaw !== '') {
$decoded = json_decode($herbsRaw, true);
$herbsRaw = \is_array($decoded) ? $decoded : [];
}
if (!\is_array($herbsRaw) || $herbsRaw === []) {
continue;
}
$key = self::herbSetMatchKeyForExport($herbsRaw);
if ($key === '') {
continue;
}
$ft = ((string) ($lib['formula_type'] ?? '')) === '辅方' ? '辅方' : '主方';
if ((int) ($lib['is_public'] ?? 0) === 1) {
$libPublic[$ft][$key] ??= $name;
}
$cid = (int) ($lib['creator_id'] ?? 0);
if ($cid > 0) {
$libByDoctor[$cid][$ft][$key] ??= $name;
}
}
return [$libByDoctor, $libPublic];
}
/**
* 反查单张处方的主方 / 辅方名称(与处方笺展示一致):
* - 主方:主方药材集合匹配开方医师/公开处方库(formula_type=主方);兜底取处方 prescription_name。
* - 辅方:优先 aux_usage.prescription_name / library_name,否则辅方药材集合匹配(formula_type=辅方)。
*
* @param array $rx 处方行(含 herbs / creator_id / prescription_name / aux_usage
* @param array<int, array<string, array<string, string>>> $libByDoctor
* @param array<string, array<string, string>> $libPublic
*
* @return array{0: string, 1: string} [主方名称, 辅方名称]
*/
private static function resolvePrescriptionNamesForExport(array $rx, array $libByDoctor, array $libPublic): array
{
if ($rx === []) {
return ['', ''];
}
$doctorId = (int) ($rx['creator_id'] ?? 0);
$herbs = $rx['herbs'] ?? null;
if (\is_string($herbs) && $herbs !== '') {
$decoded = json_decode($herbs, true);
$herbs = \is_array($decoded) ? $decoded : [];
}
if (!\is_array($herbs)) {
$herbs = [];
}
$mainHerbs = [];
$auxHerbs = [];
foreach ($herbs as $h) {
if (!\is_array($h)) {
continue;
}
if (((string) ($h['formula_type'] ?? '')) === '辅方') {
$auxHerbs[] = $h;
} else {
$mainHerbs[] = $h;
}
}
$lookup = static function (string $ft, array $hs) use ($doctorId, $libByDoctor, $libPublic): string {
if ($hs === []) {
return '';
}
$key = self::herbSetMatchKeyForExport($hs);
if ($key === '') {
return '';
}
if ($doctorId > 0 && isset($libByDoctor[$doctorId][$ft][$key])) {
return $libByDoctor[$doctorId][$ft][$key];
}
return (string) ($libPublic[$ft][$key] ?? '');
};
// 主方
$mainName = $lookup('主方', $mainHerbs);
if ($mainName === '') {
$mainName = trim((string) ($rx['prescription_name'] ?? ''));
}
// 辅方:先读持久化字段
$auxName = '';
$auxUsage = $rx['aux_usage'] ?? null;
if (\is_string($auxUsage) && $auxUsage !== '') {
$decodedAux = json_decode($auxUsage, true);
$auxUsage = \is_array($decodedAux) ? $decodedAux : null;
}
if (\is_array($auxUsage)) {
$auxName = trim((string) ($auxUsage['prescription_name'] ?? ($auxUsage['library_name'] ?? '')));
}
if ($auxName === '') {
$auxName = $lookup('辅方', $auxHerbs);
}
return [$mainName, $auxName];
}
/**
* 药材集合匹配 key:每味 name:dosagedosage 规整去尾零)排序后拼接,与处方库反查双方同口径。
*/
private static function herbSetMatchKeyForExport(array $herbs): string
{
$parts = [];
foreach ($herbs as $h) {
if (!\is_array($h)) {
continue;
}
$name = trim((string) ($h['name'] ?? ''));
if ($name === '') {
continue;
}
$dosage = (float) ($h['dosage'] ?? 0);
if (floor($dosage) === $dosage) {
$dStr = (string) (int) $dosage;
} else {
$dStr = rtrim(rtrim(number_format($dosage, 4, '.', ''), '0'), '.');
}
$parts[] = $name . ':' . $dStr;
}
sort($parts);
return implode('|', $parts);
}
/**
* 完成订单时把关联诊单的 assistant_id 清空,让患者回到「待分配」状态
* 仅当该患者没有其它进行中的处方订单且无指派日志时才清空,避免误覆盖
@@ -3906,6 +4197,9 @@ class PrescriptionOrderLogic
return false;
}
// 总金额变化后刷新「需代收」固定快照,保持与新总额一致
self::refreshAgencyCollectSnapshot($id);
// 记录日志
$summary = sprintf('将订单金额从 ¥%.2f 修改为 ¥%.2f', $oldAmount, $newAmount);
self::writeLog($id, $adminId, $adminInfo, 'update_amount', $summary);
@@ -16,7 +16,8 @@ class PrescriptionLibraryValidate extends BaseValidate
'prescription_name' => 'require|max:100',
'formula_type' => 'in:主方,辅方',
'herbs' => 'require|array',
'is_public' => 'in:0,1'
'is_public' => 'in:0,1',
'disable_edit' => 'in:0,1'
];
protected $message = [
@@ -27,7 +28,8 @@ class PrescriptionLibraryValidate extends BaseValidate
'formula_type.in' => '处方类型只能是主方或辅方',
'herbs.require' => '药材列表不能为空',
'herbs.array' => '药材列表格式错误',
'is_public.in' => '是否公开参数错误'
'is_public.in' => '是否公开参数错误',
'disable_edit.in' => '禁用修改参数错误'
];
/**
@@ -35,7 +37,7 @@ class PrescriptionLibraryValidate extends BaseValidate
*/
public function sceneAdd()
{
return $this->only(['prescription_name', 'formula_type', 'herbs', 'is_public']);
return $this->only(['prescription_name', 'formula_type', 'herbs', 'is_public', 'disable_edit']);
}
/**
@@ -43,7 +45,7 @@ class PrescriptionLibraryValidate extends BaseValidate
*/
public function sceneEdit()
{
return $this->only(['id', 'prescription_name', 'formula_type', 'herbs', 'is_public']);
return $this->only(['id', 'prescription_name', 'formula_type', 'herbs', 'is_public', 'disable_edit']);
}
/**
@@ -68,6 +68,7 @@ class PrescriptionOrderValidate extends BaseValidate
'tracking_number', 'express_company', 'fee_type', 'amount', 'remark_extra', 'remark_assistant', 'pay_order_ids',
],
'logisticsTrace' => ['id', 'phone_tail'],
'logisticsJdUpdate' => ['id'],
'auditPrescription' => ['id', 'action', 'remark'],
'auditPayment' => ['id', 'action', 'remark'],
'withdraw' => ['id'],
+2 -1
View File
@@ -38,7 +38,8 @@ class ExpressAutoUpdate extends Command
try {
$result = ExpressTrackingService::autoUpdateBatch(1000);
$recon = ExpressTrackingService::reconcileAssistantReleaseForShippedPrescriptionOrders(1000);
$duration = round(microtime(true) - $startTime, 2);
$output->writeln("更新完成!");
@@ -63,29 +63,95 @@ class ExpressTrackService
$cfg = Config::get('logistics.kuaidi100', []);
$enable = !empty($cfg['enable']);
$result = $out;
if (! $enable) {
$out['hint'] = '未配置快递100查询密钥或已关闭(LOGISTICS_KUAIDI100_DISABLE),仅可打开官网查件。请在 .env 中配置 LOGISTICS_KUAIDI100_CUSTOMER、LOGISTICS_KUAIDI100_KEY';
return $out;
}
// 顺丰(及快递100 要求电话的承运商)无 phone 时不请求接口,避免无效调用
if ($phoneForKuaidi === '' && self::kuaidiPhoneRequired($kuaidiCom)) {
$out['hint'] = '顺丰查询需在快递100 中同时提交单号与收/寄件人电话(可与面单一致的完整手机号或后四位)。请填写收件电话后点「刷新轨迹」。';
return $out;
}
$lastFail = null;
foreach ($comCandidates as $tryCom) {
$tryOut = self::queryKuaidiOnce($cfg, $tryCom, $num, $phoneForKuaidi, $carrier, $label);
if (!empty($tryOut['traces']) || ($tryOut['state'] ?? '') !== '') {
return $tryOut;
$result['hint'] = '未配置快递100查询密钥或已关闭(LOGISTICS_KUAIDI100_DISABLE),仅可打开官网查件。请在 .env 中配置 LOGISTICS_KUAIDI100_CUSTOMER、LOGISTICS_KUAIDI100_KEY';
} elseif ($phoneForKuaidi === '' && self::kuaidiPhoneRequired($kuaidiCom)) {
// 顺丰(及快递100 要求电话的承运商)无 phone 时不请求接口,避免无效调用
$result['hint'] = '顺丰查询需在快递100 中同时提交单号与收/寄件人电话(可与面单一致的完整手机号或后四位)。请填写收件电话后点「刷新轨迹」。';
} else {
$matched = null;
$lastFail = null;
foreach ($comCandidates as $tryCom) {
$tryOut = self::queryKuaidiOnce($cfg, $tryCom, $num, $phoneForKuaidi, $carrier, $label);
if (!empty($tryOut['traces']) || ($tryOut['state'] ?? '') !== '') {
$matched = $tryOut;
break;
}
$lastFail = $tryOut;
}
$lastFail = $tryOut;
$result = $matched ?? $lastFail ?? $out;
}
return $lastFail ?? $out;
// 京东自营单(JDVE…)兜底:快递100 无轨迹/陈旧时,用京东官方接口(更新或更全才采用)。
// 未配置京东官方接口时 isConfigured()=false,本段跳过,行为与原先一致。
if ($carrier === 'jd' && JdLogisticsService::isConfigured()) {
try {
$jdPhone = $overrideDigits !== '' ? $overrideDigits : $recipientDigits;
$jd = JdLogisticsService::queryTrace($num, $jdPhone);
if ($jd !== null && !empty($jd['traces']) && self::jdResultPreferred($jd, $result)) {
$result['traces'] = $jd['traces'];
$result['state'] = (string) $jd['state'];
$result['state_text'] = (string) $jd['state_text'];
$result['source'] = 'jd_official';
$result['hint'] = '';
// carrier / carrier_label / official_url / kuaidi_com 保留原值
}
} catch (\Throwable $e) {
Log::warning('ExpressTrackService jd official fallback failed', [
'num' => $num,
'error' => $e->getMessage(),
]);
}
}
return $result;
}
/**
* 京东官方轨迹是否应优先于快递100 结果采用:
* 快递100 无轨迹 直接用;否则京东更「新」(最新轨迹时间更晚)或同样新但条目更多 用。
*
* @param array{traces?:array,newest_unix?:int} $jd
* @param array{traces?:array} $kuaidi
*/
private static function jdResultPreferred(array $jd, array $kuaidi): bool
{
$kuaidiTraces = is_array($kuaidi['traces'] ?? null) ? $kuaidi['traces'] : [];
if ($kuaidiTraces === []) {
return true;
}
$jdNewest = (int) ($jd['newest_unix'] ?? 0);
$kuaidiNewest = self::newestUnixFromTraces($kuaidiTraces);
if ($jdNewest > $kuaidiNewest) {
return true;
}
if ($jdNewest === $kuaidiNewest && $jdNewest > 0) {
return count($jd['traces'] ?? []) > count($kuaidiTraces);
}
return false;
}
/**
* @param array<int, array{time?:string}> $traces
*/
private static function newestUnixFromTraces(array $traces): int
{
$best = 0;
foreach ($traces as $t) {
if (!is_array($t)) {
continue;
}
$p = strtotime((string) ($t['time'] ?? ''));
if ($p !== false && (int) $p > $best) {
$best = (int) $p;
}
}
return $best;
}
/**
@@ -353,6 +353,93 @@ class ExpressTrackingService
$responseTime = (int) ((microtime(true) - $startTime) * 1000);
return self::persistTrackingResult($tracking, $result, $isAuto, $responseTime);
}
/**
* 直接调用京东官方物流接口(绕过快递100),将轨迹落库到 express_tracking / express_trace。
* 供后台「京东接口更新」按钮使用;未配置京东官方接口或无轨迹时返回失败。
*
* @return array{success:bool, message:string, traces_count:int, state:string, source:string}
*/
public static function queryJdOfficialAndUpdate(string $trackingNumber): array
{
$fail = static fn (string $msg): array => [
'success' => false,
'message' => $msg,
'traces_count' => 0,
'state' => '',
'source' => 'jd_official',
];
$tn = trim($trackingNumber);
if ($tn === '') {
return $fail('快递单号不能为空');
}
if (!JdLogisticsService::isConfigured()) {
return $fail('未配置京东官方物流接口(请在 .env 配置 JD_LOGISTICS_* 后重试)');
}
$tracking = self::resolveTrackingByNumber($tn);
if (!$tracking) {
return $fail('未找到该快递单号对应的业务订单或物流追踪记录');
}
$startTime = microtime(true);
try {
$jd = JdLogisticsService::queryTrace($tn, (string) ($tracking->recipient_phone ?? ''));
} catch (\Throwable $e) {
Log::error('queryJdOfficialAndUpdate failed', [
'tracking_number' => $tn,
'error' => $e->getMessage(),
]);
return $fail('京东官方接口请求异常:' . $e->getMessage());
}
$responseTime = (int) ((microtime(true) - $startTime) * 1000);
if ($jd === null || empty($jd['traces'])) {
self::logQuery($tn, (string) $tracking->express_company, 'manual', [
'source' => 'jd_official',
'hint' => '京东官方接口未返回有效轨迹',
'traces' => [],
], $responseTime);
return $fail('京东官方接口未返回有效轨迹,请确认运单号或稍后重试');
}
$result = [
'carrier' => 'jd',
'carrier_label' => '京东快递',
'kuaidi_com' => 'jd',
'traces' => $jd['traces'],
'state' => (string) ($jd['state'] ?? '0'),
'state_text' => (string) ($jd['state_text'] ?? ''),
'source' => 'jd_official',
'hint' => '',
'official_url' => '',
];
self::persistTrackingResult($tracking, $result, false, $responseTime);
return [
'success' => true,
'message' => '京东物流轨迹已更新',
'traces_count' => count($jd['traces']),
'state' => (string) ($jd['state_text'] ?? ''),
'source' => 'jd_official',
];
}
/**
* 将一次轨迹查询结果(快递100 / 京东官方等)落库并推进订单状态。
*
* @param array<string, mixed> $result query 返回的轨迹结构
* @param bool $isAuto 是否定时任务触发(影响查询日志类型与医助释放)
* @return array<string, mixed>|null 查询结果;自动任务时可能含 _assistant_sync
*/
private static function persistTrackingResult(ExpressTracking $tracking, array $result, bool $isAuto, int $responseTime): ?array
{
// 记录查询日志
self::logQuery(
$tracking->tracking_number,
@@ -1047,7 +1134,7 @@ class ExpressTrackingService
->field('et.*')
->limit($limit)
->select();
$success = 0;
$failed = 0;
$assistantCleared = 0;
@@ -0,0 +1,461 @@
<?php
declare(strict_types=1);
namespace app\common\service;
use think\facade\Config;
use think\facade\Log;
/**
* 京东官方物流轨迹查询(京东物流开放平台 LOPhttps://api.jdl.com
*
* 作用:作为快递100 对「京东自营运单(JDVE…)」轨迹陈旧/缺失时的兜底数据源,
* 同时供后台「京东接口更新」按钮直接拉取并落库。
* 仅在 config/logistics.php jd.enable=true(填好 app_key/app_secret/access_token)时生效;
* 未配置时 isConfigured()=falseExpressTrackService 完全沿用快递100 逻辑,互不影响。
*
* 接口:京东物流标准轨迹服务 /jd/tracking/query2025-04-29 改版,对接方案编码 Tracking_JD)。
* 调用走 LOP 统一网关,鉴权/签名规则与官方 SDK(IsvFilter) 完全一致:
* - 公共参数(app_key/access_token/timestamp/v/sign/algorithm/LOP-DN) query string 拼到 URL
* - 业务参数 JSON 字符串作为请求体;待签串固定顺序拼接并首尾包 app_secret。
* 加签算法由 .env JD_LOGISTICS_ALGORITHM 控制(默认 md5-salt=md5(content),另支持 HMacMD5/SHA1/SHA256/SHA512)。
* 注意:后台「报文加解密密钥」的 RSA 公私钥仅用于报文加解密,与本网关签名无关。
*
* 网关/path/对接方案编码/单号类型 .envJD_LOGISTICS_GATEWAY / METHOD / LOP_DN / REFERENCE_TYPE)。
* 响应解析采用「递归找轨迹行」的宽松策略,兼容多种返回结构。
*/
final class JdLogisticsService
{
/** 轨迹行「时间」候选字段(按优先级) */
private const TIME_FIELDS = [
'operationTime', 'operatorTime', 'opeTime', 'operateTime', 'msgTime', 'scanTime',
'time', 'createTime', 'waybillStateTime', 'orderTime',
];
/** 轨迹行「描述」候选字段(按优先级) */
private const CONTEXT_FIELDS = [
'remark', 'operateRemark', 'opeRemark', 'content', 'opeTitle',
'operationCodeName', 'operationTypeName', 'scanTypeName', 'waybillStateName', 'message', 'desc', 'msg',
];
public static function isConfigured(): bool
{
$cfg = Config::get('logistics.jd', []);
// LOP 网关签名用 app_secretmd5-salt / HMAC),不需要 RSA 私钥(私钥仅用于报文加解密)
return !empty($cfg['enable'])
&& trim((string) ($cfg['app_key'] ?? '')) !== ''
&& trim((string) ($cfg['app_secret'] ?? '')) !== ''
&& trim((string) ($cfg['access_token'] ?? '')) !== '';
}
/**
* 查询京东官方轨迹,返回与 ExpressTrackService::query 兼容的结构(失败/未配置返回 null)。
*
* @return array{
* traces: list<array{time:string,context:string,status:string}>,
* state: string,
* state_text: string,
* source: string,
* hint: string,
* newest_unix: int
* }|null
*/
public static function queryTrace(string $waybillCode, string $phoneTail = ''): ?array
{
$num = trim($waybillCode);
if ($num === '' || !self::isConfigured()) {
return null;
}
$cfg = Config::get('logistics.jd', []);
// 京东物流标准轨迹服务 /jd/tracking/querybody 为 JSON 数组 [{referenceNumber, referenceType, phone}]
$row = [
(string) ($cfg['reference_field'] ?? 'referenceNumber') => $num,
'referenceType' => (string) ($cfg['reference_type'] ?? '20000'),
];
$tail = substr(preg_replace('/\D/', '', $phoneTail) ?? '', -4);
if ($tail !== '') {
$row['phone'] = $tail;
}
$customerCode = trim((string) ($cfg['customer_code'] ?? ''));
if ($customerCode !== '') {
$row['customerCode'] = $customerCode;
}
$body = json_encode([$row], JSON_UNESCAPED_UNICODE);
$raw = self::request($cfg, (string) $body);
if ($raw === null) {
return null;
}
$json = json_decode($raw, true);
if (!is_array($json)) {
Log::warning('JdLogisticsService invalid json', ['raw' => mb_substr($raw, 0, 500), 'num' => $num]);
return null;
}
// LOP 网关/业务错误:code 非 1000(成功)时记录原始报文,便于排查鉴权/单号/权限问题
$code = (string) ($json['code'] ?? $json['resultCode'] ?? '');
if ($code !== '' && !in_array($code, ['1000', '0000', '0'], true)) {
Log::warning('JdLogisticsService lop error', [
'num' => $num,
'code' => $code,
'message' => (string) ($json['msg'] ?? $json['message'] ?? $json['resultMessage'] ?? ''),
'raw' => mb_substr($raw, 0, 500),
]);
return null;
}
// 兼容 JOS 网关层错误结构
if (isset($json['error_response'])) {
Log::warning('JdLogisticsService gateway error', [
'num' => $num,
'error' => $json['error_response'],
]);
return null;
}
$rows = self::extractTraceRows($json);
if ($rows === []) {
Log::info('JdLogisticsService no trace rows', ['num' => $num, 'json' => mb_substr($raw, 0, 800)]);
return null;
}
$traces = self::normalizeRows($rows);
if ($traces === []) {
return null;
}
// 时间倒序(最新在前),与快递100 输出一致
usort($traces, static function (array $a, array $b): int {
return ($b['_unix'] ?? 0) <=> ($a['_unix'] ?? 0);
});
$newestUnix = (int) ($traces[0]['_unix'] ?? 0);
$signed = false;
foreach ($traces as $t) {
if (self::looksSigned((string) $t['context'])) {
$signed = true;
break;
}
}
$state = $signed ? '3' : '0';
// 去掉内部辅助字段
$clean = [];
foreach ($traces as $t) {
$clean[] = [
'time' => (string) $t['time'],
'context' => (string) $t['context'],
'status' => (string) ($t['status'] ?? ''),
];
}
return [
'traces' => $clean,
'state' => $state,
'state_text' => $signed ? '已签收' : '在途',
'source' => 'jd_official',
'hint' => '',
'newest_unix' => $newestUnix,
];
}
/**
* 调用 LOP 网关(统一鉴权/签名,与官方 SDK IsvFilter 一致)。
*
* 公共参数以 query string 拼到 URL;业务参数(JSON 字符串)作为请求体;
* 网关靠 LOP-DN(对接方案编码) 路由到对应服务。
*
* @param array<string,mixed> $cfg
* @param string $body 业务参数 JSON 字符串(param_json
*/
private static function request(array $cfg, string $body): ?string
{
$appKey = (string) ($cfg['app_key'] ?? '');
$appSecret = (string) ($cfg['app_secret'] ?? '');
$accessToken = (string) ($cfg['access_token'] ?? '');
$path = (string) ($cfg['method'] ?? '/jd/tracking/query');
$version = (string) ($cfg['api_version'] ?? '2.0');
$algorithm = trim((string) ($cfg['algorithm'] ?? 'md5-salt')) ?: 'md5-salt';
$lopDn = (string) ($cfg['lop_dn'] ?? 'Tracking_JD');
// 时间戳与时区必须自洽(否则网关报 471 时间戳已失效):统一用北京时间 + lop-tz=8
$now = new \DateTime('now', new \DateTimeZone('Asia/Shanghai'));
$timestamp = $now->format('Y-m-d H:i:s');
// 待签串:固定顺序拼接,首尾包 appSecretmethod=接口pathparam_json=业务体)
$content = implode('', [
$appSecret,
'access_token', $accessToken,
'app_key', $appKey,
'method', $path,
'param_json', $body,
'timestamp', $timestamp,
'v', $version,
$appSecret,
]);
$sign = self::sign($algorithm, $content, $appSecret);
if ($sign === null) {
return null;
}
$query = [
'LOP-DN' => $lopDn,
'app_key' => $appKey,
'access_token' => $accessToken,
'timestamp' => $timestamp,
'v' => $version,
'sign' => $sign,
'algorithm' => $algorithm,
];
$base = rtrim((string) ($cfg['gateway'] ?? 'https://api.jdl.com'), '/');
$url = $base . $path . '?' . http_build_query($query);
// lop-tz:与 timestamp 同源(北京时间 = 东八区 = 8)
$offsetHours = (int) ($now->getOffset() / 3600);
return self::httpPostJson($url, $body, [
'Content-Type: application/json;charset=utf-8',
'User-Agent: lop-http/php',
'lop-tz: ' . $offsetHours,
]);
}
/**
* LOP 网关签名(与官方 SDK Utils::sign 一致):
* - md5-salt md5(content) 的小写十六进制
* - HMacMD5 / HMacSHA1 / HMacSHA256 / HMacSHA512base64(hmac(算法, content, appSecret))
* 不支持的算法返回 null
*/
private static function sign(string $algorithm, string $content, string $secret): ?string
{
switch (trim($algorithm)) {
case 'md5-salt':
return md5($content);
case 'HMacMD5':
return base64_encode(hash_hmac('md5', $content, $secret, true));
case 'HMacSHA1':
return base64_encode(hash_hmac('sha1', $content, $secret, true));
case 'HMacSHA256':
return base64_encode(hash_hmac('sha256', $content, $secret, true));
case 'HMacSHA512':
return base64_encode(hash_hmac('sha512', $content, $secret, true));
default:
Log::warning('JdLogisticsService unsupported algorithm', ['algorithm' => $algorithm]);
return null;
}
}
/**
* 递归在响应 JSON 中找出「轨迹行数组」:取出现轨迹行最多的一组。
* 兼容字段被序列化成 JSON 字符串(如 querytrace_result string)的情况。
*
* @param mixed $node
* @return list<array<string,mixed>>
*/
private static function extractTraceRows($node): array
{
$best = [];
$walk = function ($n) use (&$walk, &$best): void {
if (is_string($n)) {
$trimmed = trim($n);
if ($trimmed !== '' && ($trimmed[0] === '{' || $trimmed[0] === '[')) {
$decoded = json_decode($trimmed, true);
if (is_array($decoded)) {
$walk($decoded);
}
}
return;
}
if (!is_array($n)) {
return;
}
// 是否为「轨迹行的列表」:连续数字键、且元素是带时间/描述字段的关联数组
if (self::isList($n)) {
$rows = [];
foreach ($n as $item) {
if (is_array($item) && self::rowHasTraceFields($item)) {
$rows[] = $item;
}
}
if (count($rows) > count($best)) {
$best = $rows;
}
}
foreach ($n as $v) {
$walk($v);
}
};
$walk($node);
return $best;
}
/**
* @param array<string,mixed> $row
*/
private static function rowHasTraceFields(array $row): bool
{
$hasTime = false;
foreach (self::TIME_FIELDS as $f) {
if (isset($row[$f]) && trim((string) $row[$f]) !== '') {
$hasTime = true;
break;
}
}
if (!$hasTime) {
return false;
}
foreach (self::CONTEXT_FIELDS as $f) {
if (isset($row[$f]) && trim((string) $row[$f]) !== '') {
return true;
}
}
return false;
}
/**
* @param array<int, array<string,mixed>> $rows
* @return list<array{time:string,context:string,status:string,_unix:int}>
*/
private static function normalizeRows(array $rows): array
{
$out = [];
foreach ($rows as $row) {
$time = '';
foreach (self::TIME_FIELDS as $f) {
if (isset($row[$f]) && trim((string) $row[$f]) !== '') {
$time = trim((string) $row[$f]);
break;
}
}
$context = '';
foreach (self::CONTEXT_FIELDS as $f) {
if (isset($row[$f]) && trim((string) $row[$f]) !== '') {
$context = trim((string) $row[$f]);
break;
}
}
if ($time === '' && $context === '') {
continue;
}
$unix = self::parseTimeToUnix($time);
$out[] = [
'time' => $time !== '' ? self::formatTime($time, $unix) : '',
'context' => $context,
'status' => '',
'_unix' => $unix,
];
}
return $out;
}
private static function parseTimeToUnix(string $time): int
{
$t = trim($time);
if ($t === '') {
return 0;
}
// 毫秒时间戳
if (preg_match('/^\d{13}$/', $t)) {
return (int) ((int) $t / 1000);
}
// 秒时间戳
if (preg_match('/^\d{10}$/', $t)) {
return (int) $t;
}
$p = strtotime($t);
return $p !== false ? (int) $p : 0;
}
private static function formatTime(string $raw, int $unix): string
{
// 纯时间戳统一格式化成可读时间,便于落库/前端展示
if ($unix > 0 && preg_match('/^\d{10,13}$/', trim($raw))) {
return date('Y-m-d H:i:s', $unix);
}
return $raw;
}
/**
* @param array<mixed> $arr
*/
private static function isList(array $arr): bool
{
if ($arr === []) {
return false;
}
if (function_exists('array_is_list')) {
return array_is_list($arr);
}
return array_keys($arr) === range(0, count($arr) - 1);
}
private static function looksSigned(string $hay): bool
{
if ($hay === '') {
return false;
}
foreach (['准备签收', '待签收', '等待签收', '预计', '即将送达'] as $neg) {
if (mb_stripos($hay, $neg) !== false) {
return false;
}
}
foreach (['签收', '妥投', '送达', '已放在'] as $k) {
if (mb_stripos($hay, $k) !== false) {
return true;
}
}
return false;
}
/**
* @param list<string> $headers
*/
private static function httpPostJson(string $url, string $body, array $headers): ?string
{
if (!function_exists('curl_init')) {
return null;
}
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 15);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
$resp = curl_exec($ch);
$err = curl_error($ch);
curl_close($ch);
if ($resp === false) {
Log::warning('JdLogisticsService http error', ['url' => $url, 'error' => $err]);
return null;
}
return (string) $resp;
}
}
+47
View File
@@ -41,6 +41,34 @@ $forceDisable = filter_var($forceDisableRaw, FILTER_VALIDATE_BOOLEAN);
// 已填 CUSTOMER+KEY 即启用;需临时关闭时设 LOGISTICS_KUAIDI100_DISABLE=trueLOGISTICS_KUAIDI100_ENABLE 已废弃,可删)
$kuaidiEnabled = ! $forceDisable && $hasCreds;
// 京东物流开放平台 LOPhttps://api.jdl.com
// 仅当填好 APP_KEY / APP_SECRET / ACCESS_TOKENRSA2 模式还需 RSA_PRIVATE_KEY)时启用;
// 作为快递100 对京东自营单(JDVE…)轨迹陈旧时的兜底数据源,并供后台「京东接口更新」按钮使用。
// 未配置时本段 enable=falseExpressTrackService 完全沿用现有快递100 逻辑,互不影响。
// 加签方式由 JD_LOGISTICS_ALGORITHM 控制:rsa2(默认,需私钥) / md5(摘要,无需私钥)。
$envJd = static function (string $suffix, $default = '') use ($stripQuotes) {
$top = 'JD_LOGISTICS_' . $suffix;
$v = env($top, null);
if ($v !== null && $v !== '') {
return $stripQuotes((string) $v);
}
// 兼容误写进 [trtc] 等分区的情况
$nested = env('trtc.' . $top, $default);
return $stripQuotes((string) $nested);
};
$jdAppKey = $envJd('APP_KEY', '');
$jdAppSecret = $envJd('APP_SECRET', '');
$jdAccessToken = $envJd('ACCESS_TOKEN', '');
$jdHasCreds = $jdAppKey !== '' && $jdAppSecret !== '' && $jdAccessToken !== '';
$jdDisableRaw = env('JD_LOGISTICS_DISABLE', null);
if ($jdDisableRaw === null || $jdDisableRaw === '') {
$jdDisableRaw = env('trtc.JD_LOGISTICS_DISABLE', false);
}
$jdDisabled = filter_var($jdDisableRaw, FILTER_VALIDATE_BOOLEAN);
$jdEnabled = ! $jdDisabled && $jdHasCreds;
return [
'kuaidi100' => [
'enable' => $kuaidiEnabled,
@@ -48,4 +76,23 @@ return [
'key' => $key,
'query_url' => $envKuaidi('QUERY_URL', 'https://poll.kuaidi100.com/poll/query.do'),
],
'jd' => [
'enable' => $jdEnabled,
'app_key' => $jdAppKey,
'app_secret' => $jdAppSecret,
'access_token' => $jdAccessToken,
// LOP 网关签名算法(与官方 SDK 一致):md5-salt(默认) / HMacMD5 / HMacSHA1 / HMacSHA256 / HMacSHA512
'algorithm' => $envJd('ALGORITHM', 'md5-salt'),
// 生产网关基址(不含路径)
'gateway' => $envJd('GATEWAY', 'https://api.jdl.com'),
// 接口 path(即签名里的 method);京东物流标准轨迹服务(2025-04-29 改版)
'method' => $envJd('METHOD', '/jd/tracking/query'),
// LOP-DN:对接方案编码(订阅记录可查),标准轨迹服务为 Tracking_JD
'lop_dn' => $envJd('LOP_DN', 'Tracking_JD'),
// 业务体字段:单号字段名 / 单号类型(20000=运单号) / 商家编码(可选)
'reference_field' => $envJd('REFERENCE_FIELD', 'referenceNumber'),
'reference_type' => $envJd('REFERENCE_TYPE', '20000'),
'customer_code' => $envJd('CUSTOMER_CODE', ''),
'api_version' => $envJd('API_VERSION', '2.0'),
],
];
@@ -1 +1 @@
import r from"./error-CodtDy4p.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-Dk6XWTdf.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-3-P5389h.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-Ds3qCU_G.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-D49jsFPW.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-CYLw0J2V.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./@vueuse/core-1S4fJlii.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";const a="/admin/assets/no_perms-jDxcYpYC.png",n={class:"error404"},W=p({__name:"403",setup(c){return(_,t)=>(i(),m("div",n,[e(r,{code:"403",title:"您的账号权限不足,请联系管理员添加权限!","show-btn":!1},{content:s(()=>[...t[0]||(t[0]=[o("div",{class:"flex justify-center"},[o("img",{class:"w-[150px] h-[150px]",src:a,alt:""})],-1)])]),_:1})]))}});export{W as default};
@@ -1 +1 @@
import o from"./error-CodtDy4p.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-Dk6XWTdf.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./@vueuse/core-1S4fJlii.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";const i={class:"error404"},T=r({__name:"404",setup(e){return(a,s)=>(t(),m("div",i,[p(o,{code:"404",title:"哎呀,出错了!您访问的页面不存在…"})]))}});export{T as default};
import o from"./error-3-P5389h.js";import{f as r,ak as t,I as m,a as p}from"./@vue/runtime-core-C6bnekPw.js";import"./element-plus-Ds3qCU_G.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-D49jsFPW.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-CYLw0J2V.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./@vueuse/core-1S4fJlii.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";const i={class:"error404"},T=r({__name:"404",setup(e){return(a,s)=>(t(),m("div",i,[p(o,{code:"404",title:"哎呀,出错了!您访问的页面不存在…"})]))}});export{T as default};
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
import{M as T,N as C,T as $,r as D,d as L,L as k}from"./element-plus-lurTijPg.js";import{W as E}from"./@element-plus/icons-vue-HOEUG8sr.js";import{a4 as P}from"./tcm-DEF3AGUA.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-Dk6XWTdf.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 $,r as D,d as L,L as k}from"./element-plus-Ds3qCU_G.js";import{X as E}from"./@element-plus/icons-vue-D49jsFPW.js";import{a5 as P}from"./tcm-BqLeG7XT.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-CYLw0J2V.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};
@@ -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-DEF3AGUA.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-Dk6XWTdf.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{i as L,L as T,N as V,T as D,M as z,W as M}from"./element-plus-Ds3qCU_G.js";import{ad as O}from"./tcm-BqLeG7XT.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-CYLw0J2V.js";import"./@vue/runtime-dom-DDAG46FW.js";import"./lodash-D3kF6u-c.js";import"./@element-plus/icons-vue-D49jsFPW.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./vue-router-QlpZ4wdW.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./@vueuse/core-1S4fJlii.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";const J={class:"case-record-list"},Q={key:0,class:"mb-3 flex justify-end"},W={key:0},Y={key:1,class:"text-gray-400"},q={class:"void-detail text-xs text-gray-500 mt-1"},K=P({__name:"CaseRecordList",props:{diagnosisId:{type:Number,default:0},readOnly:{type:Boolean,default:!1}},emits:["view","openPrescription"],setup(y,{expose:x,emit:C}){const _=y,h=C,l=k([]),p=k(!1),u=async()=>{if(_.diagnosisId){p.value=!0;try{const e=await O({diagnosis_id:_.diagnosisId});l.value=Array.isArray(e)?e:[]}catch(e){console.error("获取病历记录失败:",e),l.value=[]}finally{p.value=!1}}},S=e=>{if(!e)return"";const t=new Date(e*1e3);return`${t.getFullYear()}-${String(t.getMonth()+1).padStart(2,"0")}-${String(t.getDate()).padStart(2,"0")} ${String(t.getHours()).padStart(2,"0")}:${String(t.getMinutes()).padStart(2,"0")}`},$=e=>{h("view",e)},B=()=>{h("openPrescription")};return F(()=>{u()}),j(()=>_.diagnosisId,()=>{u()}),x({refresh:u}),(e,t)=>{const b=L,r=V,v=D,E=z,N=M,I=T;return a(),n("div",J,[y.readOnly?w("",!0):(a(),n("div",Q,[i(b,{type:"primary",size:"small",onClick:B},{default:s(()=>[...t[0]||(t[0]=[m("开方",-1)])]),_:1})])),A((a(),g(E,{data:d(l),border:""},{default:s(()=>[i(r,{prop:"prescription_date",label:"就诊日期",width:"120"}),i(r,{prop:"visit_no",label:"门诊号",width:"120"}),i(r,{prop:"clinical_diagnosis",label:"临床诊断","min-width":"160","show-overflow-tooltip":""}),i(r,{label:"处方摘要","min-width":"180"},{default:s(({row:o})=>[o.herbs&&o.herbs.length?(a(),n("span",W,c(o.herbs.slice(0,3).map(f=>`${f.name}${f.dosage}`).join("、"))+c(o.herbs.length>3?"...":""),1)):(a(),n("span",Y,"—"))]),_:1}),i(r,{prop:"doctor_name",label:"医师",width:"90","show-overflow-tooltip":""}),i(r,{label:"状态",width:"140",align:"center"},{default:s(({row:o})=>[o.void_status===1?(a(),n(H,{key:0},[i(v,{type:"danger",size:"small"},{default:s(()=>[...t[1]||(t[1]=[m("已作废",-1)])]),_:1}),R("div",q,c(o.void_by_name||"—")+" "+c(S(o.void_time)),1)],64)):(a(),g(v,{key:1,type:"success",size:"small"},{default:s(()=>[...t[2]||(t[2]=[m("正常",-1)])]),_:1}))]),_:1}),i(r,{label:"操作",width:"120",fixed:"right"},{default:s(({row:o})=>[i(b,{link:"",type:"primary",size:"small",onClick:f=>$(o)},{default:s(()=>[...t[3]||(t[3]=[m(" 查看 ",-1)])]),_:1},8,["onClick"])]),_:1})]),_:1},8,["data"])),[[I,d(p)]]),!d(p)&&d(l).length===0?(a(),g(N,{key:1,description:"暂无病历记录,开方后会自动显示",class:"mt-4"})):w("",!0)])}}}),zt=G(K,[["__scopeId","data-v-043d2738"]]);export{zt as default};
File diff suppressed because one or more lines are too long
@@ -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-DIj1J3Im.js";import"./element-plus-Ds3qCU_G.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-D49jsFPW.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";export{o as default};
@@ -1 +1 @@
import{Q as g,P as V}from"./element-plus-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-Ds3qCU_G.js";import{f as C,ak as o,G as s,aN as v,I as p,F as h,ap as B,H as k,A as m}from"./@vue/runtime-core-C6bnekPw.js";import{y as c}from"./@vue/reactivity-DiY1c2vO.js";import{p as w,o as S}from"./@vue/shared-mAAVTE9n.js";const N=C({__name:"MediaSourceSelect",props:{modelValue:{default:""},options:{},loading:{type:Boolean,default:!1},placeholder:{default:"请选择自媒体来源"},clearable:{type:Boolean,default:!0},filterable:{type:Boolean,default:!0},allowCreate:{type:Boolean,default:!1},disabled:{type:Boolean,default:!1},selectClass:{},selectStyle:{}},emits:["update:modelValue","change","visible-change"],setup(e,{emit:i}){const u=e,n=i,d=m(()=>(u.options||[]).map(l=>typeof l=="string"?{name:l}:{name:l.name,value:l.value})),f=m(()=>d.value.some(l=>l.name===u.modelValue)),b=l=>{n("visible-change",l)};return(l,t)=>{const r=g,y=V;return o(),s(y,{"model-value":e.modelValue,placeholder:e.placeholder,clearable:e.clearable,filterable:e.filterable,"allow-create":e.allowCreate,"default-first-option":e.allowCreate,disabled:e.disabled,loading:e.loading,class:S(e.selectClass),style:w(e.selectStyle),"onUpdate:modelValue":t[0]||(t[0]=a=>n("update:modelValue",a)),onChange:t[1]||(t[1]=a=>n("change",a)),onVisibleChange:b},{default:v(()=>[(o(!0),p(h,null,B(c(d),a=>(o(),s(r,{key:a.name,label:a.name,value:a.name},null,8,["label","value"]))),128)),e.modelValue&&!c(f)?(o(),s(r,{key:`__legacy_${e.modelValue}`,label:`${e.modelValue}(已停用)`,value:e.modelValue},null,8,["label","value"])):k("",!0)]),_:1},8,["model-value","placeholder","clearable","filterable","allow-create","default-first-option","disabled","loading","class","style"])}}});export{N as _};
File diff suppressed because one or more lines are too long
@@ -1,2 +1,2 @@
import{i as se,W as ne,C as le,V as ae,v as re,d as me,a as de}from"./element-plus-lurTijPg.js";import{_ as pe}from"./picker-DePJ7jQN.js";import{e as ue,c as ce,i as g,_ as ge}from"./index-Dk6XWTdf.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-DjH7mxcn.js";import{h as _e}from"./perm-D9KNzoFr.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-BRWMojQO.js";import"./index-Cwo7-aW2.js";import"./index.vue_vue_type_script_setup_true_lang-CB1JgtXC.js";import"./index-D_asycd1.js";import"./index-CJyq8Xg-.js";import"./cos-js-sdk-v5-DSMN0LUT.js";import"./file-BkQxjA_Z.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(`
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-Ds3qCU_G.js";import{_ as pe}from"./picker-DcVdXs-r.js";import{e as ue,c as ce,i as g,_ as ge}from"./index-CYLw0J2V.js";import{s as U}from"./@vue/runtime-dom-DDAG46FW.js";import{b as z,G as fe}from"./@element-plus/icons-vue-D49jsFPW.js";import{a as P,d as ve}from"./patient-jF4jBNUB.js";import{h as _e}from"./perm-DTInbRd6.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-wRiKfob5.js";import"./index-DQ_c_7GJ.js";import"./index.vue_vue_type_script_setup_true_lang-Ch-2CJwb.js";import"./index-BXes3WfF.js";import"./index-BdBwexoG.js";import"./cos-js-sdk-v5-DSMN0LUT.js";import"./file-C_xnWpAD.js";import"./index.vue_vue_type_script_setup_true_lang-d5pf8jrB.js";import"./@vueuse/core-1S4fJlii.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./usePaging-VsbTxSU0.js";import"./vuedraggable-5bKFmC7X.js";import"./vue-BVs68q8v.js";import"./@vue/compiler-dom-CqayqxS7.js";import"./@vue/compiler-core-CvbQOXIO.js";import"./sortablejs-dsEAUXlE.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./vue-router-QlpZ4wdW.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";const we={class:"note-timeline-wrap"},Ie={key:0,class:"timeline-actions"},Ce={class:"upload-trigger"},xe={class:"upload-trigger"},Ve={key:1,class:"note-timeline"},Ee={class:"timeline-date"},Ne={class:"timeline-body"},be={key:0,class:"timeline-content"},Se={key:1,class:"timeline-images"},Ae={key:2,class:"timeline-images"},De={key:0,class:"thumb-wrap"},Be={key:1,class:"file-wrap"},Ue=["href","title"],ze={class:"file-name"},O=8e3,Pe=ye({__name:"NoteTimeline",props:{notes:{},diagnosisId:{},readonly:{type:Boolean}},emits:["refresh"],setup(d,{emit:j}){const u=d,I=j,X=ke(()=>_e(["doctor.appointment/addDoctorNote"])),v=f(!1),C=f(""),b=f(!1),H=ue(),_=t=>H.getImageUrl(t),S=f([]),A=f([]),y=f(0),h=f(0),J=["jpg","jpeg","png","gif","bmp","webp","svg"],D=t=>{var s;const e=((s=t.split(".").pop())==null?void 0:s.toLowerCase().split("?")[0])||"";return J.includes(e)},$=t=>{var s;const e=t.split("/");return decodeURIComponent(((s=e[e.length-1])==null?void 0:s.split("?")[0])||"文件")},Q=t=>t.filter(D).map(_),Z=(t,e)=>{const s=t.filter(D),x=t[e];return s.indexOf(x)},q=t=>t?t.split(`
`).filter(Boolean):[],K=t=>{if(!u.diagnosisId)return;const e=Array.isArray(t)?t:[t];if(e.length<=y.value){y.value=e.length;return}const s=e.slice(y.value);y.value=e.length,s.length>0&&P({diagnosis_id:u.diagnosisId,tongue_images:s}).then(()=>{g.msgSuccess("舌苔照片已添加"),I("refresh")})},Y=t=>{if(!u.diagnosisId)return;const e=Array.isArray(t)?t:[t];if(e.length<=h.value){h.value=e.length;return}const s=e.slice(h.value);h.value=e.length,s.length>0&&P({diagnosis_id:u.diagnosisId,report_files:s}).then(()=>{g.msgSuccess("检查报告已添加"),I("refresh")})};he(()=>u.notes,()=>{S.value=[],A.value=[],y.value=0,h.value=0});const ee=async()=>{if(!u.diagnosisId){g.msgWarning("当前无诊单,无法添加备注");return}const t=C.value.trim();if(!t){g.msgWarning("请输入备注内容");return}b.value=!0;try{await P({diagnosis_id:u.diagnosisId,content:t}),g.msgSuccess("备注保存成功"),C.value="",v.value=!1,I("refresh")}catch(e){g.msgError((e==null?void 0:e.message)||"保存失败")}finally{b.value=!1}},B=async(t,e,s)=>{try{await de.confirm("确认删除?","提示",{type:"warning"})}catch{return}await ve({note_id:t,image_type:e,image_path:s}),g.msgSuccess("已删除"),I("refresh")};return(t,e)=>{const s=se,x=ce,F=pe,G=re,V=me,te=ne,ie=le,oe=ae;return o(),n("div",we,[!d.readonly&&d.diagnosisId?(o(),n("div",Ie,[X.value?(o(),k(s,{key:0,type:"primary",plain:"",size:"small",onClick:e[0]||(e[0]=i=>v.value=!0)},{default:r(()=>[...e[6]||(e[6]=[M(" 添加备注 ",-1)])]),_:1})):p("",!0),l(F,{modelValue:S.value,"onUpdate:modelValue":e[1]||(e[1]=i=>S.value=i),limit:99,type:"image","exclude-domain":!0,onChange:K},{upload:r(()=>[m("div",Ce,[l(x,{size:20,name:"el-icon-Plus"}),e[7]||(e[7]=m("span",null,"舌苔照片",-1))])]),_:1},8,["modelValue"]),l(F,{modelValue:A.value,"onUpdate:modelValue":e[2]||(e[2]=i=>A.value=i),limit:99,type:"file","exclude-domain":!0,onChange:Y},{upload:r(()=>[m("div",xe,[l(x,{size:20,name:"el-icon-Plus"}),e[8]||(e[8]=m("span",null,"检查报告",-1))])]),_:1},8,["modelValue"])])):p("",!0),d.notes.length?(o(),n("div",Ve,[(o(!0),n(w,null,E(d.notes,i=>{var L,R;return o(),n("div",{key:i.id,class:"timeline-node"},[e[11]||(e[11]=m("div",{class:"timeline-dot"},null,-1)),m("div",Ee,T(i.note_date),1),m("div",Ne,[i.content?(o(),n("div",be,[(o(!0),n(w,null,E(q(i.content),(a,c)=>(o(),n("div",{key:c,class:"content-line"},T(a),1))),128))])):p("",!0),(L=i.tongue_images)!=null&&L.length?(o(),n("div",Se,[e[9]||(e[9]=m("span",{class:"images-label"},"舌苔照片",-1)),(o(!0),n(w,null,E(i.tongue_images,(a,c)=>(o(),n("div",{key:c,class:"thumb-wrap"},[l(G,{src:_(a),"preview-src-list":i.tongue_images.map(_),"initial-index":c,"z-index":O,fit:"cover",class:"timeline-thumb","preview-teleported":""},null,8,["src","preview-src-list","initial-index"]),d.readonly?p("",!0):(o(),k(V,{key:0,class:"thumb-delete",onClick:U(W=>B(i.id,"tongue_images",a),["stop"])},{default:r(()=>[l(N(z))]),_:1},8,["onClick"]))]))),128))])):p("",!0),(R=i.report_files)!=null&&R.length?(o(),n("div",Ae,[e[10]||(e[10]=m("span",{class:"images-label"},"检查报告",-1)),(o(!0),n(w,null,E(i.report_files,(a,c)=>(o(),n(w,{key:c},[D(a)?(o(),n("div",De,[l(G,{src:_(a),"preview-src-list":Q(i.report_files),"initial-index":Z(i.report_files,c),"z-index":O,fit:"cover",class:"timeline-thumb","preview-teleported":""},null,8,["src","preview-src-list","initial-index"]),d.readonly?p("",!0):(o(),k(V,{key:0,class:"thumb-delete",onClick:U(W=>B(i.id,"report_files",a),["stop"])},{default:r(()=>[l(N(z))]),_:1},8,["onClick"]))])):(o(),n("div",Be,[m("a",{href:_(a),target:"_blank",class:"file-link",title:$(a)},[l(V,{size:20},{default:r(()=>[l(N(fe))]),_:1}),m("span",ze,T($(a)),1)],8,Ue),d.readonly?p("",!0):(o(),k(V,{key:0,class:"file-delete",onClick:U(W=>B(i.id,"report_files",a),["stop"])},{default:r(()=>[l(N(z))]),_:1},8,["onClick"]))]))],64))),128))])):p("",!0)])])}),128))])):p("",!0),!d.notes.length&&d.readonly?(o(),k(te,{key:2,description:"暂无备注","image-size":48})):p("",!0),l(oe,{modelValue:v.value,"onUpdate:modelValue":e[5]||(e[5]=i=>v.value=i),title:"添加备注",width:"480px","append-to-body":""},{footer:r(()=>[l(s,{onClick:e[4]||(e[4]=i=>v.value=!1)},{default:r(()=>[...e[12]||(e[12]=[M("取消",-1)])]),_:1}),l(s,{type:"primary",loading:b.value,onClick:ee},{default:r(()=>[...e[13]||(e[13]=[M("保存",-1)])]),_:1},8,["loading"])]),default:r(()=>[l(ie,{modelValue:C.value,"onUpdate:modelValue":e[3]||(e[3]=i=>C.value=i),type:"textarea",rows:4,placeholder:"输入今日备注...",maxlength:"500","show-word-limit":""},null,8,["modelValue"])]),_:1},8,["modelValue"])])}}}),Mt=ge(Pe,[["__scopeId","data-v-530ad386"]]);export{Mt as default};
@@ -1 +1 @@
import{m as x,f as y,a as B}from"./diag-display-DCz_VAqj.js";import{f as w,ak as I,I as P,J as a,H as N}from"./@vue/runtime-core-C6bnekPw.js";import{Q as i}from"./@vue/shared-mAAVTE9n.js";import{y as e}from"./@vue/reactivity-DiY1c2vO.js";import{_ as V}from"./index-Dk6XWTdf.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-CYLw0J2V.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-Ds3qCU_G.js";import"./@vue/runtime-dom-DDAG46FW.js";import"./@element-plus/icons-vue-D49jsFPW.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./@vueuse/core-1S4fJlii.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";const b={class:"card patient-card"},D={class:"patient-hero"},E={class:"patient-name"},G={class:"patient-meta"},H={key:0},J=w({__name:"PatientInfoCard",props:{apt:{default:()=>({})},diag:{default:()=>({})}},setup(t){return(Q,o)=>{var m,r,n,d,p,s,c,l,g,f,u,h,v,k,C;return I(),P("div",b,[o[0]||(o[0]=a("div",{class:"card-title"},"患者信息",-1)),a("div",D,[a("div",E,i(((m=t.apt)==null?void 0:m.patient_name)||"—"),1),a("div",G,[a("div",null,i(e(x)((r=t.apt)==null?void 0:r.patient_phone))+" · "+i(e(y)((n=t.diag)==null?void 0:n.gender))+" · "+i(((d=t.diag)==null?void 0:d.age)!=null?t.diag.age+"岁":"—"),1),a("div",null,i((p=t.diag)!=null&&p.height?t.diag.height+"cm":"—")+" / "+i((s=t.diag)!=null&&s.weight?t.diag.weight+"kg":"—")+" · "+i(((c=t.diag)==null?void 0:c.region)||"—"),1),a("div",null," 预约:"+i((l=t.apt)==null?void 0:l.appointment_date)+" "+i((g=t.apt)==null?void 0:g.appointment_time)+" · "+i(e(B)((f=t.apt)==null?void 0:f.period)),1),a("div",null,"医生:"+i(((u=t.apt)==null?void 0:u.doctor_name)||"—")+" 客服:"+i(((h=t.apt)==null?void 0:h.assistant_name)||"—"),1),a("div",null," 状态:"+i(((v=t.apt)==null?void 0:v.status_desc)||"—")+" · "+i((k=t.apt)!=null&&k.has_prescription?"已开方":"未开方"),1),(C=t.apt)!=null&&C.remark?(I(),P("div",H,"备注:"+i(t.apt.remark),1)):N("",!0)])])])}}}),Ct=V(J,[["__scopeId","data-v-1b0de09c"]]);export{Ct as default};
@@ -1 +1 @@
import{S as L}from"./element-plus-lurTijPg.js";import B from"./RecordingVideoPlayer-Ckr2FFyz.js";import{e as H,_ as I}from"./index-Dk6XWTdf.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-Ds3qCU_G.js";import B from"./RecordingVideoPlayer-CqR6OAdN.js";import{e as H,_ as I}from"./index-CYLw0J2V.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-D49jsFPW.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./vue-router-QlpZ4wdW.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./@vueuse/core-1S4fJlii.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";const C={key:0,class:"recording-list"},N={class:"recording-item"},P={key:1,class:"recording-alternates"},V={class:"recording-alternates__links"},A={key:1,class:"text-gray-400"},E=w({__name:"RecordingPlaybackBlock",props:{recordId:{},urls:{}},setup(c){const $=c,h=H(),l=d(()=>{const e=$.urls||[],t=new Set,r=[];for(const s of e){const o=String(s??"").trim();!o||t.has(o)||(t.add(o),r.push(o))}return r}),a=d(()=>{const e=l.value;if(!e.length)return null;const t=e.find(i=>/\.mp4(\?|#|$)/i.test(i));if(t)return t;const r=e.find(i=>/\.m3u8(\?|#|$)/i.test(i)&&/vod-qcloud\.com/i.test(i));if(r)return r;const s=e.find(i=>/\.m3u8(\?|#|$)/i.test(i)&&/\.cos\.[^/]+\.myqcloud\.com/i.test(i));if(s)return s;const o=e.find(i=>/\.m3u8(\?|#|$)/i.test(i));return o||e[0]}),f=d(()=>{const e=l.value,t=a.value;return t?e.filter(r=>r!==t):e.slice(1)});function S(e){if(!e||typeof e!="string")return!1;const t=e.trim();return/^https?:\/\//i.test(t)?/\.(mp4|webm|ogg|mov|mkv|m4v|m3u8)(\?|#|$)/i.test(t):/\.(mp4|webm|ogg|mov|mkv|m4v|m3u8)(\?|#|$)/i.test(t)||t.startsWith("/uploads/")}function g(e){return h.getImageUrl(String(e||"").trim())}function b(e,t){const r=e.trim();return/\.mp4(\?|#|$)/i.test(r)?`MP4 ${t+1}`:/vod-qcloud\.com/i.test(r)&&/\.m3u8/i.test(r)?`点播 ${t+1}`:/\.cos\.[^/]+\.myqcloud\.com/i.test(r)&&/\.m3u8/i.test(r)?`COS HLS ${t+1}`:/\.m3u8/i.test(r)?`HLS ${t+1}`:`链接 ${t+1}`}return(e,t)=>{const r=L;return l.value.length?(n(),m("div",C,[p("div",N,[a.value?(n(),m(v,{key:0},[S(a.value)?(n(),u(B,{key:`${c.recordId}-${a.value}`,src:a.value},null,8,["src"])):(n(),u(r,{key:1,href:g(a.value),target:"_blank",type:"primary"},{default:_(()=>[...t[0]||(t[0]=[k(" 打开回放 ",-1)])]),_:1},8,["href"]))],64)):y("",!0),f.value.length?(n(),m("div",P,[t[1]||(t[1]=p("span",{class:"recording-alternates__label"},"备用地址",-1)),p("div",V,[(n(!0),m(v,null,R(f.value,(s,o)=>(n(),u(r,{key:`${c.recordId}-alt-${o}-${s.slice(-32)}`,href:g(s),target:"_blank",type:"primary",class:"recording-alternates__link"},{default:_(()=>[k(q(b(s,o)),1)]),_:2},1032,["href"]))),128))])])):y("",!0)])])):(n(),m("span",A,"暂无"))}}}),ht=I(E,[["__scopeId","data-v-d67b2e91"]]);export{ht as default};
File diff suppressed because one or more lines are too long
@@ -1,2 +1,2 @@
import{C as B,i as I,W as w}from"./element-plus-lurTijPg.js";import{V as T}from"./tcm-DEF3AGUA.js";import{i as C,_ as E}from"./index-Dk6XWTdf.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,i as V,W as w}from"./element-plus-Ds3qCU_G.js";import{W as T}from"./tcm-BqLeG7XT.js";import{i as C,_ as E}from"./index-CYLw0J2V.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-D49jsFPW.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"},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 @@
import{_ as o}from"./account-adjust.vue_vue_type_script_setup_true_lang-CAZcCNSN.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-BRWMojQO.js";import"./index-Dk6XWTdf.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-DCoTQTCs.js";import"./element-plus-Ds3qCU_G.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-D49jsFPW.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-wRiKfob5.js";import"./index-CYLw0J2V.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./vue-router-QlpZ4wdW.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./@vueuse/core-1S4fJlii.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";export{o as default};
@@ -1 +1 @@
import{D as h,B as q,G as B,I,C as D}from"./element-plus-lurTijPg.js";import{_ as F}from"./index-BRWMojQO.js";import{i as b}from"./index-Dk6XWTdf.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-Ds3qCU_G.js";import{_ as F}from"./index-wRiKfob5.js";import{i as b}from"./index-CYLw0J2V.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 +0,0 @@
.personal-account-cost-page[data-v-b8e68581]{padding:20px}.dept-cell[data-v-b8e68581]{cursor:help;border-bottom:1px dashed #c0c4cc}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
.personal-account-cost-page[data-v-1e1f4e9b]{padding:20px}.stats-filter-form[data-v-1e1f4e9b] .el-form-item{margin-bottom:0}.dept-cell[data-v-1e1f4e9b]{cursor:help;border-bottom:1px dashed #c0c4cc}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
import{_ as o}from"./add-nav.vue_vue_type_script_setup_true_lang-Dji_sJVN.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-D_asycd1.js";import"./index-Dk6XWTdf.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-BUnU5YBo.js";import"./index-BRWMojQO.js";import"./index.vue_vue_type_script_setup_true_lang-CB1JgtXC.js";import"./article-DKHAEkJP.js";import"./usePaging-VsbTxSU0.js";import"./picker-DePJ7jQN.js";import"./index-Cwo7-aW2.js";import"./index-CJyq8Xg-.js";import"./cos-js-sdk-v5-DSMN0LUT.js";import"./file-BkQxjA_Z.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};
import{_ as o}from"./add-nav.vue_vue_type_script_setup_true_lang-DxFacXhn.js";import"./element-plus-Ds3qCU_G.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-D49jsFPW.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-BXes3WfF.js";import"./index-CYLw0J2V.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-UB-0VH7g.js";import"./index-wRiKfob5.js";import"./index.vue_vue_type_script_setup_true_lang-Ch-2CJwb.js";import"./article-DBGc93Gj.js";import"./usePaging-VsbTxSU0.js";import"./picker-DcVdXs-r.js";import"./index-DQ_c_7GJ.js";import"./index-BdBwexoG.js";import"./cos-js-sdk-v5-DSMN0LUT.js";import"./file-C_xnWpAD.js";import"./index.vue_vue_type_script_setup_true_lang-d5pf8jrB.js";import"./vuedraggable-5bKFmC7X.js";import"./vue-BVs68q8v.js";import"./@vue/compiler-dom-CqayqxS7.js";import"./@vue/compiler-core-CvbQOXIO.js";import"./sortablejs-dsEAUXlE.js";export{o as default};
@@ -1 +1 @@
import{C as E,B,g as C,i as N}from"./element-plus-lurTijPg.js";import{_ as $}from"./index-D_asycd1.js";import{_ as z}from"./picker-BUnU5YBo.js";import{_ as A}from"./picker-DePJ7jQN.js";import{c as D,i as r}from"./index-Dk6XWTdf.js";import{D as I}from"./vuedraggable-5bKFmC7X.js";import{f as R,ak as p,I as F,J as l,a,aN as d,G,O as J,A as L}from"./@vue/runtime-core-C6bnekPw.js";import{y as c,a as O}from"./@vue/reactivity-DiY1c2vO.js";const P={class:"bg-fill-light flex items-center w-full p-4 mb-4"},S={class:"upload-btn w-[60px] h-[60px]"},T={class:"ml-3 flex-1"},j={class:"flex items-center"},q={class:"flex items-center mt-[18px]"},H={class:"flex-1 flex items-center"},K={class:"drag-move cursor-move ml-auto"},oe=R({__name:"add-nav",props:{modelValue:{type:Array,default:()=>[]},max:{type:Number,default:100},min:{type:Number,default:1}},emits:["update:modelValue"],setup(_,{emit:f}){const t=_,V=f,m=L({get(){return t.modelValue},set(s){V("update:modelValue",s)}}),x=()=>{var s;((s=t.modelValue)==null?void 0:s.length)<t.max?m.value.push({image:"",name:"导航名称",link:{},is_show:"1"}):r.msgError(`最多添加${t.max}`)},g=s=>{var e;if(((e=t.modelValue)==null?void 0:e.length)<=t.min)return r.msgError(`最少保留${t.min}`);m.value.splice(s,1)};return(s,e)=>{const i=D,v=A,h=E,k=z,b=C,w=B,y=$,U=N;return p(),F("div",null,[l("div",null,[a(c(I),{class:"draggable",modelValue:c(m),"onUpdate:modelValue":e[0]||(e[0]=o=>O(m)?m.value=o:null),animation:"300",handle:".drag-move","item-key":"index"},{item:d(({element:o,index:u})=>[(p(),G(y,{class:"w-[467px]",key:u,onClose:n=>g(u)},{default:d(()=>[l("div",P,[a(v,{modelValue:o.image,"onUpdate:modelValue":n=>o.image=n,"upload-class":"bg-body",size:"60px","exclude-domain":""},{upload:d(()=>[l("div",S,[a(i,{name:"el-icon-Plus",size:20})])]),_:1},8,["modelValue","onUpdate:modelValue"]),l("div",T,[l("div",j,[e[1]||(e[1]=l("span",{class:"text-tx-regular flex-none mr-3"},"名称",-1)),a(h,{modelValue:o.name,"onUpdate:modelValue":n=>o.name=n,placeholder:"请输入名称"},null,8,["modelValue","onUpdate:modelValue"])]),l("div",q,[e[2]||(e[2]=l("span",{class:"text-tx-regular flex-none mr-3"},"链接",-1)),a(k,{modelValue:o.link,"onUpdate:modelValue":n=>o.link=n},null,8,["modelValue","onUpdate:modelValue"])]),a(w,{label:"是否显示",class:"mt-[18px]"},{default:d(()=>[l("div",H,[a(b,{modelValue:o.is_show,"onUpdate:modelValue":n=>o.is_show=n,"active-value":"1","inactive-value":"0"},null,8,["modelValue","onUpdate:modelValue"]),l("div",K,[a(i,{name:"el-icon-Rank",size:"18"})])])]),_:2},1024)])])]),_:2},1032,["onClose"]))]),_:1},8,["modelValue"])]),l("div",null,[a(U,{type:"primary",onClick:x},{default:d(()=>[...e[3]||(e[3]=[J("添加",-1)])]),_:1})])])}}});export{oe as _};
import{C as E,B,g as C,i as N}from"./element-plus-Ds3qCU_G.js";import{_ as $}from"./index-BXes3WfF.js";import{_ as z}from"./picker-UB-0VH7g.js";import{_ as A}from"./picker-DcVdXs-r.js";import{c as D,i as r}from"./index-CYLw0J2V.js";import{D as I}from"./vuedraggable-5bKFmC7X.js";import{f as R,ak as p,I as F,J as l,a,aN as d,G,O as J,A as L}from"./@vue/runtime-core-C6bnekPw.js";import{y as c,a as O}from"./@vue/reactivity-DiY1c2vO.js";const P={class:"bg-fill-light flex items-center w-full p-4 mb-4"},S={class:"upload-btn w-[60px] h-[60px]"},T={class:"ml-3 flex-1"},j={class:"flex items-center"},q={class:"flex items-center mt-[18px]"},H={class:"flex-1 flex items-center"},K={class:"drag-move cursor-move ml-auto"},oe=R({__name:"add-nav",props:{modelValue:{type:Array,default:()=>[]},max:{type:Number,default:100},min:{type:Number,default:1}},emits:["update:modelValue"],setup(_,{emit:f}){const t=_,V=f,m=L({get(){return t.modelValue},set(s){V("update:modelValue",s)}}),x=()=>{var s;((s=t.modelValue)==null?void 0:s.length)<t.max?m.value.push({image:"",name:"导航名称",link:{},is_show:"1"}):r.msgError(`最多添加${t.max}`)},g=s=>{var e;if(((e=t.modelValue)==null?void 0:e.length)<=t.min)return r.msgError(`最少保留${t.min}`);m.value.splice(s,1)};return(s,e)=>{const i=D,v=A,h=E,k=z,b=C,w=B,y=$,U=N;return p(),F("div",null,[l("div",null,[a(c(I),{class:"draggable",modelValue:c(m),"onUpdate:modelValue":e[0]||(e[0]=o=>O(m)?m.value=o:null),animation:"300",handle:".drag-move","item-key":"index"},{item:d(({element:o,index:u})=>[(p(),G(y,{class:"w-[467px]",key:u,onClose:n=>g(u)},{default:d(()=>[l("div",P,[a(v,{modelValue:o.image,"onUpdate:modelValue":n=>o.image=n,"upload-class":"bg-body",size:"60px","exclude-domain":""},{upload:d(()=>[l("div",S,[a(i,{name:"el-icon-Plus",size:20})])]),_:1},8,["modelValue","onUpdate:modelValue"]),l("div",T,[l("div",j,[e[1]||(e[1]=l("span",{class:"text-tx-regular flex-none mr-3"},"名称",-1)),a(h,{modelValue:o.name,"onUpdate:modelValue":n=>o.name=n,placeholder:"请输入名称"},null,8,["modelValue","onUpdate:modelValue"])]),l("div",q,[e[2]||(e[2]=l("span",{class:"text-tx-regular flex-none mr-3"},"链接",-1)),a(k,{modelValue:o.link,"onUpdate:modelValue":n=>o.link=n},null,8,["modelValue","onUpdate:modelValue"])]),a(w,{label:"是否显示",class:"mt-[18px]"},{default:d(()=>[l("div",H,[a(b,{modelValue:o.is_show,"onUpdate:modelValue":n=>o.is_show=n,"active-value":"1","inactive-value":"0"},null,8,["modelValue","onUpdate:modelValue"]),l("div",K,[a(i,{name:"el-icon-Rank",size:"18"})])])]),_:2},1024)])])]),_:2},1032,["onClose"]))]),_:1},8,["modelValue"])]),l("div",null,[a(U,{type:"primary",onClick:x},{default:d(()=>[...e[3]||(e[3]=[J("添加",-1)])]),_:1})])])}}});export{oe as _};
@@ -1 +1 @@
import{r as n}from"./index-Dk6XWTdf.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-CYLw0J2V.js";function e(t){return n.get({url:"/auth.admin/lists",params:t},{ignoreCancelToken:!0})}function i(t){return n.post({url:"/auth.admin/add",params:t})}function r(t){return n.post({url:"/auth.admin/edit",params:t})}function u(t){return n.post({url:"/auth.admin/delete",params:t})}function d(t){return n.get({url:"/auth.admin/detail",params:t})}export{e as a,r as b,u as c,i as d,d as e};
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
import{r as e}from"./index-Dk6XWTdf.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-CYLw0J2V.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 @@
import{r as e}from"./index-Dk6XWTdf.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-CYLw0J2V.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 +1 @@
import{_ as o}from"./attr.vue_vue_type_script_setup_true_lang-B9mftSGQ.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"./add-nav.vue_vue_type_script_setup_true_lang-Dji_sJVN.js";import"./index-D_asycd1.js";import"./index-Dk6XWTdf.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-BUnU5YBo.js";import"./index-BRWMojQO.js";import"./index.vue_vue_type_script_setup_true_lang-CB1JgtXC.js";import"./article-DKHAEkJP.js";import"./usePaging-VsbTxSU0.js";import"./picker-DePJ7jQN.js";import"./index-Cwo7-aW2.js";import"./index-CJyq8Xg-.js";import"./cos-js-sdk-v5-DSMN0LUT.js";import"./file-BkQxjA_Z.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};
import{_ as o}from"./attr.vue_vue_type_script_setup_true_lang-DcMb4onv.js";import"./element-plus-Ds3qCU_G.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-D49jsFPW.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-DxFacXhn.js";import"./index-BXes3WfF.js";import"./index-CYLw0J2V.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-UB-0VH7g.js";import"./index-wRiKfob5.js";import"./index.vue_vue_type_script_setup_true_lang-Ch-2CJwb.js";import"./article-DBGc93Gj.js";import"./usePaging-VsbTxSU0.js";import"./picker-DcVdXs-r.js";import"./index-DQ_c_7GJ.js";import"./index-BdBwexoG.js";import"./cos-js-sdk-v5-DSMN0LUT.js";import"./file-C_xnWpAD.js";import"./index.vue_vue_type_script_setup_true_lang-d5pf8jrB.js";import"./vuedraggable-5bKFmC7X.js";import"./vue-BVs68q8v.js";import"./@vue/compiler-dom-CqayqxS7.js";import"./@vue/compiler-core-CvbQOXIO.js";import"./sortablejs-dsEAUXlE.js";export{o as default};
@@ -1 +1 @@
import{m as b,l as c,D as V}from"./element-plus-lurTijPg.js";import{_ as l}from"./menu-set.vue_vue_type_script_setup_true_lang-CKdTihMo.js";import{f as v,ak as x,I as k,J as E,a as o,aN as e,F as g,A as w}from"./@vue/runtime-core-C6bnekPw.js";import{y as r}from"./@vue/reactivity-DiY1c2vO.js";import"./@vue/runtime-dom-DDAG46FW.js";import"./lodash-D3kF6u-c.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-D_asycd1.js";import"./index-Dk6XWTdf.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-BUnU5YBo.js";import"./index-BRWMojQO.js";import"./index.vue_vue_type_script_setup_true_lang-CB1JgtXC.js";import"./article-DKHAEkJP.js";import"./usePaging-VsbTxSU0.js";import"./picker-DePJ7jQN.js";import"./index-Cwo7-aW2.js";import"./index-CJyq8Xg-.js";import"./cos-js-sdk-v5-DSMN0LUT.js";import"./file-BkQxjA_Z.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";const Bt=v({__name:"attr",props:{modelValue:{type:Object,default:()=>({nav:[],menu:{}})}},emits:["update:modelValue"],setup(n,{emit:s}){const u=n,d=s,m=w({get(){return u.modelValue},set(i){d("update:modelValue",i)}});return(i,t)=>{const a=c,f=b,_=V;return x(),k(g,null,[t[2]||(t[2]=E("div",{class:"title flex items-center before:w-[3px] before:h-[14px] before:block before:bg-primary before:mr-2"}," pc导航设置 ",-1)),o(_,{class:"mt-4","label-width":"70px"},{default:e(()=>[o(f,{"model-value":"nav"},{default:e(()=>[o(a,{label:"主导航设置",name:"nav"},{default:e(()=>[o(l,{modelValue:r(m).nav,"onUpdate:modelValue":t[0]||(t[0]=p=>r(m).nav=p)},null,8,["modelValue"])]),_:1}),o(a,{label:"菜单设置",name:"menu"},{default:e(()=>[o(l,{modelValue:r(m).menu,"onUpdate:modelValue":t[1]||(t[1]=p=>r(m).menu=p)},null,8,["modelValue"])]),_:1})]),_:1})]),_:1})],64)}}});export{Bt as default};
import{m as b,l as c,D as V}from"./element-plus-Ds3qCU_G.js";import{_ as l}from"./menu-set.vue_vue_type_script_setup_true_lang-REtTeywT.js";import{f as v,ak as x,I as k,J as E,a as o,aN as e,F as g,A as w}from"./@vue/runtime-core-C6bnekPw.js";import{y as r}from"./@vue/reactivity-DiY1c2vO.js";import"./@vue/runtime-dom-DDAG46FW.js";import"./lodash-D3kF6u-c.js";import"./@vue/shared-mAAVTE9n.js";import"./@element-plus/icons-vue-D49jsFPW.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-BXes3WfF.js";import"./index-CYLw0J2V.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-UB-0VH7g.js";import"./index-wRiKfob5.js";import"./index.vue_vue_type_script_setup_true_lang-Ch-2CJwb.js";import"./article-DBGc93Gj.js";import"./usePaging-VsbTxSU0.js";import"./picker-DcVdXs-r.js";import"./index-DQ_c_7GJ.js";import"./index-BdBwexoG.js";import"./cos-js-sdk-v5-DSMN0LUT.js";import"./file-C_xnWpAD.js";import"./index.vue_vue_type_script_setup_true_lang-d5pf8jrB.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";const Bt=v({__name:"attr",props:{modelValue:{type:Object,default:()=>({nav:[],menu:{}})}},emits:["update:modelValue"],setup(n,{emit:s}){const u=n,d=s,m=w({get(){return u.modelValue},set(i){d("update:modelValue",i)}});return(i,t)=>{const a=c,f=b,_=V;return x(),k(g,null,[t[2]||(t[2]=E("div",{class:"title flex items-center before:w-[3px] before:h-[14px] before:block before:bg-primary before:mr-2"}," pc导航设置 ",-1)),o(_,{class:"mt-4","label-width":"70px"},{default:e(()=>[o(f,{"model-value":"nav"},{default:e(()=>[o(a,{label:"主导航设置",name:"nav"},{default:e(()=>[o(l,{modelValue:r(m).nav,"onUpdate:modelValue":t[0]||(t[0]=p=>r(m).nav=p)},null,8,["modelValue"])]),_:1}),o(a,{label:"菜单设置",name:"menu"},{default:e(()=>[o(l,{modelValue:r(m).menu,"onUpdate:modelValue":t[1]||(t[1]=p=>r(m).menu=p)},null,8,["modelValue"])]),_:1})]),_:1})]),_:1})],64)}}});export{Bt as default};
@@ -1 +1 @@
import{_ as o}from"./attr.vue_vue_type_script_setup_true_lang-JOjIeYU_.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"./picker-DePJ7jQN.js";import"./index-BRWMojQO.js";import"./index-Dk6XWTdf.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"./index-Cwo7-aW2.js";import"./index.vue_vue_type_script_setup_true_lang-CB1JgtXC.js";import"./index-D_asycd1.js";import"./index-CJyq8Xg-.js";import"./cos-js-sdk-v5-DSMN0LUT.js";import"./file-BkQxjA_Z.js";import"./index.vue_vue_type_script_setup_true_lang-SfFZFG0h.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";export{o as default};
import{_ as o}from"./attr.vue_vue_type_script_setup_true_lang-CQRXtwjJ.js";import"./element-plus-Ds3qCU_G.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-D49jsFPW.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"./picker-DcVdXs-r.js";import"./index-wRiKfob5.js";import"./index-CYLw0J2V.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"./index-DQ_c_7GJ.js";import"./index.vue_vue_type_script_setup_true_lang-Ch-2CJwb.js";import"./index-BXes3WfF.js";import"./index-BdBwexoG.js";import"./cos-js-sdk-v5-DSMN0LUT.js";import"./file-C_xnWpAD.js";import"./index.vue_vue_type_script_setup_true_lang-d5pf8jrB.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";export{o as default};
@@ -1 +1 @@
import{_ as o}from"./attr.vue_vue_type_script_setup_true_lang-BV5QqN8L.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.vue_vue_type_script_setup_true_lang-8-xP7boW.js";import"./@vueuse/core-1S4fJlii.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./picker-DePJ7jQN.js";import"./index-BRWMojQO.js";import"./index-Dk6XWTdf.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";import"./index-Cwo7-aW2.js";import"./index.vue_vue_type_script_setup_true_lang-CB1JgtXC.js";import"./index-D_asycd1.js";import"./index-CJyq8Xg-.js";import"./cos-js-sdk-v5-DSMN0LUT.js";import"./file-BkQxjA_Z.js";import"./index.vue_vue_type_script_setup_true_lang-SfFZFG0h.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";export{o as default};
import{_ as o}from"./attr.vue_vue_type_script_setup_true_lang-CimCXKMG.js";import"./element-plus-Ds3qCU_G.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-D49jsFPW.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.vue_vue_type_script_setup_true_lang-DZ8cC-yU.js";import"./@vueuse/core-1S4fJlii.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./picker-DcVdXs-r.js";import"./index-wRiKfob5.js";import"./index-CYLw0J2V.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";import"./index-DQ_c_7GJ.js";import"./index.vue_vue_type_script_setup_true_lang-Ch-2CJwb.js";import"./index-BXes3WfF.js";import"./index-BdBwexoG.js";import"./cos-js-sdk-v5-DSMN0LUT.js";import"./file-C_xnWpAD.js";import"./index.vue_vue_type_script_setup_true_lang-d5pf8jrB.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";export{o as default};
@@ -1 +1 @@
import{_ as o}from"./attr.vue_vue_type_script_setup_true_lang-sAhVy4zf.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-D_asycd1.js";import"./index-Dk6XWTdf.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-BUnU5YBo.js";import"./index-BRWMojQO.js";import"./index.vue_vue_type_script_setup_true_lang-CB1JgtXC.js";import"./article-DKHAEkJP.js";import"./usePaging-VsbTxSU0.js";import"./picker-DePJ7jQN.js";import"./index-Cwo7-aW2.js";import"./index-CJyq8Xg-.js";import"./cos-js-sdk-v5-DSMN0LUT.js";import"./file-BkQxjA_Z.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};
import{_ as o}from"./attr.vue_vue_type_script_setup_true_lang-3eudbug2.js";import"./element-plus-Ds3qCU_G.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-D49jsFPW.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-BXes3WfF.js";import"./index-CYLw0J2V.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-UB-0VH7g.js";import"./index-wRiKfob5.js";import"./index.vue_vue_type_script_setup_true_lang-Ch-2CJwb.js";import"./article-DBGc93Gj.js";import"./usePaging-VsbTxSU0.js";import"./picker-DcVdXs-r.js";import"./index-DQ_c_7GJ.js";import"./index-BdBwexoG.js";import"./cos-js-sdk-v5-DSMN0LUT.js";import"./file-C_xnWpAD.js";import"./index.vue_vue_type_script_setup_true_lang-d5pf8jrB.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};
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
import{_ as o}from"./attr.vue_vue_type_script_setup_true_lang-wHfB6SAf.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-D_asycd1.js";import"./index-Dk6XWTdf.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-BUnU5YBo.js";import"./index-BRWMojQO.js";import"./index.vue_vue_type_script_setup_true_lang-CB1JgtXC.js";import"./article-DKHAEkJP.js";import"./usePaging-VsbTxSU0.js";import"./picker-DePJ7jQN.js";import"./index-Cwo7-aW2.js";import"./index-CJyq8Xg-.js";import"./cos-js-sdk-v5-DSMN0LUT.js";import"./file-BkQxjA_Z.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";import"./index.vue_vue_type_script_setup_true_lang-8-xP7boW.js";export{o as default};
import{_ as o}from"./attr.vue_vue_type_script_setup_true_lang-DdedGrqO.js";import"./element-plus-Ds3qCU_G.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-D49jsFPW.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-BXes3WfF.js";import"./index-CYLw0J2V.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-UB-0VH7g.js";import"./index-wRiKfob5.js";import"./index.vue_vue_type_script_setup_true_lang-Ch-2CJwb.js";import"./article-DBGc93Gj.js";import"./usePaging-VsbTxSU0.js";import"./picker-DcVdXs-r.js";import"./index-DQ_c_7GJ.js";import"./index-BdBwexoG.js";import"./cos-js-sdk-v5-DSMN0LUT.js";import"./file-C_xnWpAD.js";import"./index.vue_vue_type_script_setup_true_lang-d5pf8jrB.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"./index.vue_vue_type_script_setup_true_lang-DZ8cC-yU.js";export{o as default};

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