Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e76f16193b |
+1
-284
File diff suppressed because one or more lines are too long
Binary file not shown.
|
Before Width: | Height: | Size: 64 KiB |
@@ -0,0 +1,158 @@
|
|||||||
|
/**
|
||||||
|
* 将标准双 el-card 列表页迁移为 admin-page 架构组件
|
||||||
|
* 用法: node scripts/migrate-admin-pages.mjs [--dry-run] [glob...]
|
||||||
|
*/
|
||||||
|
import fs from 'node:fs'
|
||||||
|
import path from 'node:path'
|
||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||||
|
const viewsRoot = path.join(__dirname, '../src/views')
|
||||||
|
|
||||||
|
const dryRun = process.argv.includes('--dry-run')
|
||||||
|
const extraPaths = process.argv.slice(2).filter((a) => !a.startsWith('--'))
|
||||||
|
|
||||||
|
const defaultTargets = [
|
||||||
|
'article/lists/index.vue',
|
||||||
|
'article/column/index.vue',
|
||||||
|
'permission/role/index.vue',
|
||||||
|
'permission/menu/index.vue',
|
||||||
|
'dev_tools/code/index.vue',
|
||||||
|
'setting/dict/type/index.vue',
|
||||||
|
'setting/dict/data/index.vue',
|
||||||
|
'message/notice/index.vue',
|
||||||
|
'message/short_letter/index.vue',
|
||||||
|
'fans/index.vue',
|
||||||
|
'order/index.vue',
|
||||||
|
'asset/user/index.vue',
|
||||||
|
'asset/resource/index.vue',
|
||||||
|
'finance/balance_details.vue',
|
||||||
|
'finance/recharge_record.vue',
|
||||||
|
'finance/refund_record.vue'
|
||||||
|
]
|
||||||
|
|
||||||
|
function migrate(content) {
|
||||||
|
if (content.includes('admin-page-filter-panel')) return null
|
||||||
|
if (!content.includes('el-card class="!border-none"')) return null
|
||||||
|
if (!content.includes('<el-table')) return null
|
||||||
|
|
||||||
|
let out = content
|
||||||
|
|
||||||
|
// 根容器 class 清理
|
||||||
|
out = out.replace(
|
||||||
|
/<div class="[^"]*">\s*\n\s*<el-card class="!border-none" shadow="never">/,
|
||||||
|
'<div>\n <admin-page-filter-panel>'
|
||||||
|
)
|
||||||
|
if (!out.includes('admin-page-filter-panel')) {
|
||||||
|
out = out.replace(
|
||||||
|
/<el-card class="!border-none" shadow="never">/,
|
||||||
|
'<admin-page-filter-panel>'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 第一个 filter card 结束
|
||||||
|
out = out.replace(
|
||||||
|
/<\/el-form>\s*\n\s*<\/el-card>/,
|
||||||
|
'</el-form>\n </admin-page-filter-panel>'
|
||||||
|
)
|
||||||
|
|
||||||
|
// 第二个 data card 开始 - 提取 v-loading
|
||||||
|
const loadingMatch = out.match(
|
||||||
|
/<el-card([^>]*?)class="[^"]*mt-4[^"]*"[^>]*shadow="never"[^>]*>/
|
||||||
|
)
|
||||||
|
const altLoadingMatch = out.match(
|
||||||
|
/<el-card v-loading="([^"]+)"([^>]*)class="[^"]*mt-4[^"]*"([^>]*)shadow="never"([^>]*)>/
|
||||||
|
)
|
||||||
|
|
||||||
|
if (altLoadingMatch) {
|
||||||
|
out = out.replace(
|
||||||
|
altLoadingMatch[0],
|
||||||
|
`<admin-page-data-panel v-loading="${altLoadingMatch[1]}">`
|
||||||
|
)
|
||||||
|
} else if (loadingMatch) {
|
||||||
|
out = out.replace(loadingMatch[0], '<admin-page-data-panel>')
|
||||||
|
} else {
|
||||||
|
out = out.replace(
|
||||||
|
/<el-card class="!border-none mt-4" shadow="never">/,
|
||||||
|
'<admin-page-data-panel>'
|
||||||
|
)
|
||||||
|
out = out.replace(
|
||||||
|
/<el-card v-loading="([^"]+)" class="mt-4 !border-none" shadow="never">/,
|
||||||
|
'<admin-page-data-panel v-loading="$1">'
|
||||||
|
)
|
||||||
|
out = out.replace(
|
||||||
|
/<el-card v-loading="([^"]+)" class="!border-none mt-4" shadow="never">/,
|
||||||
|
'<admin-page-data-panel v-loading="$1">'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// toolbar: 紧跟 data panel 后的首个 div 包裹按钮
|
||||||
|
out = out.replace(
|
||||||
|
/(<admin-page-data-panel[^>]*>\s*)<div>\s*\n(\s*<el-button[\s\S]*?<\/div>\s*\n)/,
|
||||||
|
'$1<template #toolbar>\n$2</template>\n'
|
||||||
|
)
|
||||||
|
out = out.replace(
|
||||||
|
/(<admin-page-data-panel[^>]*>\s*)<div>\s*\n(\s*<router-link[\s\S]*?<\/div>\s*\n)/,
|
||||||
|
'$1<template #toolbar>\n$2</template>\n'
|
||||||
|
)
|
||||||
|
|
||||||
|
// 移除 table 前的 mt-4 class
|
||||||
|
out = out.replace(/<el-table class="mt-4"/g, '<el-table')
|
||||||
|
out = out.replace(/<el-table\s+class="mt-4"\s+/g, '<el-table ')
|
||||||
|
|
||||||
|
// pagination footer
|
||||||
|
out = out.replace(
|
||||||
|
/<div class="flex(?: mt-4)? justify-end(?: mt-4)?">\s*\n\s*<pagination([^/]*)\/>\s*\n\s*<\/div>\s*\n\s*<\/el-card>/,
|
||||||
|
'<template #footer>\n <pagination$1/>\n </template>\n </admin-page-data-panel>'
|
||||||
|
)
|
||||||
|
out = out.replace(
|
||||||
|
/<div class="flex mt-4 justify-end">\s*\n\s*<pagination([^/]*)\/>\s*\n\s*<\/div>\s*\n\s*<\/el-card>/,
|
||||||
|
'<template #footer>\n <pagination$1/>\n </template>\n </admin-page-data-panel>'
|
||||||
|
)
|
||||||
|
|
||||||
|
// 无 pagination 的 data card 闭合
|
||||||
|
if (out.includes('<admin-page-data-panel') && out.includes('</el-card>')) {
|
||||||
|
out = out.replace(/<\/el-table>\s*\n\s*<\/el-card>/, '</el-table>\n </admin-page-data-panel>')
|
||||||
|
}
|
||||||
|
|
||||||
|
// table 上的 v-loading 移到 panel(若 panel 还没有)
|
||||||
|
out = out.replace(
|
||||||
|
/<admin-page-data-panel>\s*\n(\s*)<el-table([^>]*?)v-loading="([^"]+)"([^>]*)>/,
|
||||||
|
'<admin-page-data-panel v-loading="$3">\n$1<el-table$2$4>'
|
||||||
|
)
|
||||||
|
out = out.replace(/<el-table([^>]*?)v-loading="([^"]+)"([^>]*)>/, '<el-table$1$3>')
|
||||||
|
|
||||||
|
if (out === content || !out.includes('admin-page-data-panel')) return null
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
const targets = extraPaths.length
|
||||||
|
? extraPaths.map((p) => path.resolve(p))
|
||||||
|
: defaultTargets.map((p) => path.join(viewsRoot, p))
|
||||||
|
|
||||||
|
let migrated = 0
|
||||||
|
let skipped = 0
|
||||||
|
|
||||||
|
for (const file of targets) {
|
||||||
|
if (!fs.existsSync(file)) {
|
||||||
|
console.warn('skip (missing):', path.relative(viewsRoot, file))
|
||||||
|
skipped++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const original = fs.readFileSync(file, 'utf8')
|
||||||
|
const result = migrate(original)
|
||||||
|
if (!result) {
|
||||||
|
console.log('skip (no match):', path.relative(viewsRoot, file))
|
||||||
|
skipped++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (dryRun) {
|
||||||
|
console.log('would migrate:', path.relative(viewsRoot, file))
|
||||||
|
} else {
|
||||||
|
fs.writeFileSync(file, result, 'utf8')
|
||||||
|
console.log('migrated:', path.relative(viewsRoot, file))
|
||||||
|
}
|
||||||
|
migrated++
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\nDone. migrated=${migrated} skipped=${skipped}${dryRun ? ' (dry-run)' : ''}`)
|
||||||
@@ -227,11 +227,6 @@ export function revisitRateVisitOrderLines(params: {
|
|||||||
return request.get({ url: '/stats.revisitRate/visitOrderLines', params })
|
return request.get({ url: '/stats.revisitRate/visitOrderLines', params })
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 待分配诊单自动指派日志列表(定时命令 tcm:auto-assign-pending 写入,含分配/未分配原因) */
|
|
||||||
export function autoAssignLogLists(params: Record<string, any>) {
|
|
||||||
return request.get({ url: '/stats.autoAssignLog/lists', params })
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 医助个人业绩概览 */
|
/** 医助个人业绩概览 */
|
||||||
export function assistantPerformanceOverview(params: {
|
export function assistantPerformanceOverview(params: {
|
||||||
time_type?: string
|
time_type?: string
|
||||||
|
|||||||
@@ -61,14 +61,6 @@ export function tcmDiagnosisDetail(params: any) {
|
|||||||
return request.get({ url: '/tcm.diagnosis/detail', params })
|
return request.get({ url: '/tcm.diagnosis/detail', params })
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 设置复诊接诊率统计起始偏移(统计诊次=实单序号+偏移;1=二诊起,2=三诊起) */
|
|
||||||
export function tcmDiagnosisSetRevisitSlotStartOffset(params: {
|
|
||||||
id: number
|
|
||||||
revisit_slot_start_offset: number
|
|
||||||
}) {
|
|
||||||
return request.post({ url: '/tcm.diagnosis/setRevisitSlotStartOffset', params })
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 诊单挂号 / 取消挂号 操作日志 */
|
/** 诊单挂号 / 取消挂号 操作日志 */
|
||||||
export function tcmDiagnosisGuahaoLogList(params: { id: number }) {
|
export function tcmDiagnosisGuahaoLogList(params: { id: number }) {
|
||||||
return request.get({ url: '/tcm.diagnosis/guahaoLogList', params })
|
return request.get({ url: '/tcm.diagnosis/guahaoLogList', params })
|
||||||
@@ -433,17 +425,6 @@ export function prescriptionOrderPatchPrescriptionPatient(params: {
|
|||||||
return request.post({ url: '/tcm.prescriptionOrder/patchPrescriptionPatient', params })
|
return request.post({ url: '/tcm.prescriptionOrder/patchPrescriptionPatient', params })
|
||||||
}
|
}
|
||||||
|
|
||||||
export function prescriptionOrderPatchPrescriptionUsage(params: {
|
|
||||||
id: number
|
|
||||||
times_per_day: number
|
|
||||||
usage_days: number
|
|
||||||
medication_days: number
|
|
||||||
aux_times_per_day?: number
|
|
||||||
aux_usage_days?: number
|
|
||||||
}) {
|
|
||||||
return request.post({ url: '/tcm.prescriptionOrder/patchPrescriptionUsage', params })
|
|
||||||
}
|
|
||||||
|
|
||||||
export function prescriptionOrderAuditPrescription(params: {
|
export function prescriptionOrderAuditPrescription(params: {
|
||||||
id: number
|
id: number
|
||||||
action: 'approve' | 'reject'
|
action: 'approve' | 'reject'
|
||||||
@@ -542,18 +523,6 @@ export function prescriptionOrderLogs(params: { id: number }) {
|
|||||||
return request.get({ url: '/tcm.prescriptionOrder/logs', params })
|
return request.get({ url: '/tcm.prescriptionOrder/logs', params })
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 手工新增操作日志(可选调整处方/支付单审核状态) */
|
|
||||||
export function prescriptionOrderAddLog(params: {
|
|
||||||
id: number
|
|
||||||
summary: string
|
|
||||||
prescription_audit_status?: number | ''
|
|
||||||
payment_slip_audit_status?: number | ''
|
|
||||||
prescription_audit_remark?: string
|
|
||||||
payment_slip_audit_remark?: string
|
|
||||||
}) {
|
|
||||||
return request.post({ url: '/tcm.prescriptionOrder/addLog', params })
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 修改订单金额 */
|
/** 修改订单金额 */
|
||||||
export function prescriptionOrderUpdateAmount(params: { id: number; amount: number }) {
|
export function prescriptionOrderUpdateAmount(params: { id: number; amount: number }) {
|
||||||
return request.post({ url: '/tcm.prescriptionOrder/updateAmount', params })
|
return request.post({ url: '/tcm.prescriptionOrder/updateAmount', params })
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<template>
|
||||||
|
<section class="admin-data-panel" v-loading="loading">
|
||||||
|
<div v-if="$slots.toolbar" class="admin-data-panel__toolbar">
|
||||||
|
<slot name="toolbar" />
|
||||||
|
</div>
|
||||||
|
<div class="admin-data-panel__body">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
<div v-if="$slots.footer" class="admin-data-panel__footer">
|
||||||
|
<slot name="footer" />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
loading?: boolean
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
loading: false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
<template>
|
||||||
|
|
||||||
|
<section class="admin-filter-panel" :class="{ 'is-collapsed': collapsed }">
|
||||||
|
|
||||||
|
<div v-if="collapsible" class="admin-filter-panel__head">
|
||||||
|
|
||||||
|
<span class="admin-filter-panel__label">{{ title }}</span>
|
||||||
|
|
||||||
|
<el-button class="admin-filter-panel__toggle" link type="primary" @click="collapsed = !collapsed">
|
||||||
|
|
||||||
|
{{ collapsed ? '展开筛选' : '收起筛选' }}
|
||||||
|
|
||||||
|
</el-button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="admin-filter-panel__body">
|
||||||
|
|
||||||
|
<slot />
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
withDefaults(
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
|
||||||
|
/** 是否显示收起/展开 */
|
||||||
|
|
||||||
|
collapsible?: boolean
|
||||||
|
|
||||||
|
/** 筛选区标题 */
|
||||||
|
|
||||||
|
title?: string
|
||||||
|
|
||||||
|
}>(),
|
||||||
|
|
||||||
|
{
|
||||||
|
|
||||||
|
collapsible: true,
|
||||||
|
|
||||||
|
title: '筛选条件'
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const collapsed = ref(false)
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<template>
|
||||||
|
<section class="admin-form-panel">
|
||||||
|
<header v-if="title || $slots.header" class="admin-form-panel__header">
|
||||||
|
<slot name="header">
|
||||||
|
<h2 v-if="title" class="admin-form-panel__title">{{ title }}</h2>
|
||||||
|
</slot>
|
||||||
|
</header>
|
||||||
|
<div class="admin-form-panel__body">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
<footer v-if="$slots.footer" class="admin-form-panel__footer">
|
||||||
|
<slot name="footer" />
|
||||||
|
</footer>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
defineProps({
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<template>
|
||||||
|
<div class="admin-page-actions">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<template>
|
||||||
|
<div class="admin-stat-grid" :class="[`admin-stat-grid--cols-${columns}`]">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
columns?: 2 | 3 | 4 | 5
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
columns: 4
|
||||||
|
}
|
||||||
|
)
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<template>
|
||||||
|
<article class="admin-stat-item" :class="[`admin-stat-item--${tone}`]">
|
||||||
|
<div class="admin-stat-item__label">{{ label }}</div>
|
||||||
|
<div class="admin-stat-item__value" :class="{ 'is-money': money }">
|
||||||
|
<slot>{{ value }}</slot>
|
||||||
|
</div>
|
||||||
|
<p v-if="hint" class="admin-stat-item__hint">{{ hint }}</p>
|
||||||
|
</article>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
label: string
|
||||||
|
value?: string | number
|
||||||
|
hint?: string
|
||||||
|
money?: boolean
|
||||||
|
tone?: 'default' | 'primary' | 'success' | 'warning'
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
value: '',
|
||||||
|
hint: '',
|
||||||
|
money: false,
|
||||||
|
tone: 'default'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
</script>
|
||||||
@@ -17,14 +17,18 @@ defineProps({
|
|||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.footer-btns {
|
.footer-btns {
|
||||||
height: 60px;
|
height: 64px;
|
||||||
|
|
||||||
&__content {
|
&__content {
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
height: 60px;
|
height: 64px;
|
||||||
right: 0;
|
right: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
z-index: 99;
|
z-index: 99;
|
||||||
@apply flex justify-center items-center shadow bg-body;
|
padding: 0 20px;
|
||||||
|
border-top: 1px solid var(--el-border-color-lighter);
|
||||||
|
box-shadow: 0 -4px 16px rgba(15, 23, 42, 0.04);
|
||||||
|
@apply flex justify-center items-center gap-3 bg-body;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,228 @@
|
|||||||
|
<template>
|
||||||
|
|
||||||
|
<div class="page-stage admin-page" :class="{ 'page-stage--flat': hideChrome }">
|
||||||
|
|
||||||
|
<header v-if="showHeader" class="page-stage__hero">
|
||||||
|
|
||||||
|
<div class="page-stage__hero-glow" aria-hidden="true"></div>
|
||||||
|
|
||||||
|
<div class="page-stage__hero-inner">
|
||||||
|
|
||||||
|
<div class="page-stage__intro">
|
||||||
|
|
||||||
|
<h1 class="page-stage__title">{{ pageTitle }}</h1>
|
||||||
|
|
||||||
|
<p v-if="pageDesc" class="page-stage__desc">{{ pageDesc }}</p>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="$slots.actions" class="page-stage__actions">
|
||||||
|
|
||||||
|
<slot name="actions" />
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="page-stage__body">
|
||||||
|
|
||||||
|
<slot />
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
import { PageEnum } from '@/enums/pageEnum'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const hideChrome = computed(() => {
|
||||||
|
|
||||||
|
if (route.meta?.hidePageHeader) return true
|
||||||
|
|
||||||
|
if (route.path === PageEnum.INDEX || route.path === `${PageEnum.INDEX}/`) return true
|
||||||
|
|
||||||
|
const title = (route.meta?.title as string) || ''
|
||||||
|
|
||||||
|
return title.includes('工作台')
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const pageTitle = computed(() => {
|
||||||
|
|
||||||
|
const matched = route.matched.filter((item) => item.meta?.title)
|
||||||
|
|
||||||
|
const last = matched[matched.length - 1]
|
||||||
|
|
||||||
|
return (last?.meta?.title as string) || ''
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const pageDesc = computed(() => (route.meta?.pageDesc as string) || '')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const showHeader = computed(() => !hideChrome.value && Boolean(pageTitle.value))
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
|
||||||
|
.page-stage {
|
||||||
|
|
||||||
|
min-height: 100%;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.page-stage__hero {
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
margin-bottom: 20px;
|
||||||
|
|
||||||
|
padding: 22px 24px;
|
||||||
|
|
||||||
|
border-radius: var(--admin-radius-xl);
|
||||||
|
|
||||||
|
border: 1px solid var(--el-border-color-lighter);
|
||||||
|
|
||||||
|
background: var(--admin-surface-glass);
|
||||||
|
|
||||||
|
backdrop-filter: blur(16px) saturate(140%);
|
||||||
|
|
||||||
|
box-shadow: var(--el-box-shadow-light);
|
||||||
|
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.page-stage__hero-glow {
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
inset: 0;
|
||||||
|
|
||||||
|
background:
|
||||||
|
|
||||||
|
linear-gradient(120deg, rgba(6, 182, 212, 0.12) 0%, transparent 42%),
|
||||||
|
|
||||||
|
linear-gradient(300deg, rgba(16, 185, 129, 0.1) 0%, transparent 38%);
|
||||||
|
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.page-stage__hero-inner {
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
align-items: flex-start;
|
||||||
|
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
gap: 12px 20px;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.page-stage__title {
|
||||||
|
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
font-size: 24px;
|
||||||
|
|
||||||
|
font-weight: 800;
|
||||||
|
|
||||||
|
line-height: 1.2;
|
||||||
|
|
||||||
|
letter-spacing: -0.03em;
|
||||||
|
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
|
||||||
|
background: var(--admin-brand-gradient);
|
||||||
|
|
||||||
|
background-clip: text;
|
||||||
|
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.page-stage__desc {
|
||||||
|
|
||||||
|
margin: 8px 0 0;
|
||||||
|
|
||||||
|
font-size: 14px;
|
||||||
|
|
||||||
|
line-height: 1.5;
|
||||||
|
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
|
||||||
|
max-width: 62ch;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.page-stage__actions {
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.page-stage__body {
|
||||||
|
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.page-stage--flat .page-stage__hero {
|
||||||
|
|
||||||
|
display: none;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
||||||
+20
-20
@@ -1,28 +1,28 @@
|
|||||||
const defaultSetting = {
|
const defaultSetting = {
|
||||||
showCrumb: true, // 是否显示面包屑
|
showCrumb: false,
|
||||||
showLogo: false, // 是否显示logo
|
showLogo: true,
|
||||||
isUniqueOpened: true, //只展开一个一级菜单
|
isUniqueOpened: true,
|
||||||
sideWidth: 183, //侧边栏宽度
|
sideWidth: 248,
|
||||||
sideTheme: 'dark', //侧边栏主题
|
sideTheme: 'dark',
|
||||||
sideDarkColor: '#1d2124', //侧边栏深色主题颜色
|
sideDarkColor: '#060d18',
|
||||||
openMultipleTabs: true, // 是否开启多标签tab栏
|
openMultipleTabs: true,
|
||||||
theme: '#4A5DFF', //主题色
|
theme: '#06b6d4',
|
||||||
successTheme: '#67c23a', //成功主题色
|
successTheme: '#10b981',
|
||||||
warningTheme: '#e6a23c', //警告主题色
|
warningTheme: '#f59e0b',
|
||||||
dangerTheme: '#f56c6c', //危险主题色
|
dangerTheme: '#ef4444',
|
||||||
errorTheme: '#f56c6c', //错误主题色
|
errorTheme: '#ef4444',
|
||||||
infoTheme: '#909399' //信息主题色
|
infoTheme: '#6366f1'
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 本地 setting 缓存结构版本。提升后仅对低于该版本的老缓存执行 SETTING_SCHEMA_MIGRATIONS */
|
export const SETTING_SCHEMA_VERSION = 6
|
||||||
export const SETTING_SCHEMA_VERSION = 1
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 按版本写入 defaultSetting 中的键(老用户 localStorage 会长期盖住 config 默认值)。
|
|
||||||
* 以后若要再推一批新默认值:把 SETTING_SCHEMA_VERSION +1,并为本版本追加一条迁移键列表。
|
|
||||||
*/
|
|
||||||
export const SETTING_SCHEMA_MIGRATIONS: Record<number, (keyof typeof defaultSetting)[]> = {
|
export const SETTING_SCHEMA_MIGRATIONS: Record<number, (keyof typeof defaultSetting)[]> = {
|
||||||
1: ['sideTheme', 'sideDarkColor']
|
1: ['sideTheme', 'sideDarkColor'],
|
||||||
|
2: ['theme', 'successTheme', 'warningTheme', 'dangerTheme', 'errorTheme', 'infoTheme', 'sideDarkColor', 'sideWidth', 'showLogo'],
|
||||||
|
3: ['theme', 'sideDarkColor', 'sideWidth', 'showCrumb', 'showLogo'],
|
||||||
|
4: ['theme', 'successTheme', 'warningTheme', 'dangerTheme', 'errorTheme', 'infoTheme', 'sideDarkColor'],
|
||||||
|
5: ['theme', 'successTheme', 'warningTheme', 'dangerTheme', 'errorTheme', 'infoTheme', 'sideDarkColor'],
|
||||||
|
6: ['theme', 'successTheme', 'warningTheme', 'dangerTheme', 'errorTheme', 'infoTheme', 'sideDarkColor', 'sideWidth']
|
||||||
}
|
}
|
||||||
|
|
||||||
export default defaultSetting
|
export default defaultSetting
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-breadcrumb class="app-breadcrumb">
|
<el-breadcrumb class="app-breadcrumb" separator="/">
|
||||||
<el-breadcrumb-item v-for="item in breadcrumbs" :key="item.path">
|
<el-breadcrumb-item v-for="item in breadcrumbs" :key="item.path">
|
||||||
{{ item.meta.title }}
|
{{ item.meta.title }}
|
||||||
</el-breadcrumb-item>
|
</el-breadcrumb-item>
|
||||||
@@ -21,22 +21,24 @@ useWatchRoute((route) => {
|
|||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.app-breadcrumb {
|
.app-breadcrumb {
|
||||||
:deep(.el-breadcrumb__item) {
|
:deep(.el-breadcrumb__item) {
|
||||||
.el-breadcrumb__inner {
|
.el-breadcrumb__inner {
|
||||||
color: #303133;
|
color: var(--el-text-color-secondary);
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
font-size: var(--el-font-size-small);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:last-child .el-breadcrumb__inner {
|
&:last-child .el-breadcrumb__inner {
|
||||||
color: #303133;
|
color: var(--el-text-color-primary);
|
||||||
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.el-breadcrumb__separator) {
|
:deep(.el-breadcrumb__separator) {
|
||||||
color: #606266;
|
color: var(--el-text-color-placeholder);
|
||||||
|
font-weight: 400;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<header class="header">
|
<header class="header">
|
||||||
<div class="navbar">
|
<div class="navbar">
|
||||||
<div class="flex-1 flex">
|
<div class="flex-1 flex items-center gap-1 min-w-0">
|
||||||
<div class="navbar-item">
|
<div class="navbar-item">
|
||||||
<el-tooltip
|
<el-tooltip
|
||||||
class="box-item"
|
class="box-item"
|
||||||
@@ -17,11 +17,14 @@
|
|||||||
<refresh />
|
<refresh />
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center px-2" v-if="!isMobile && settingStore.showCrumb">
|
<div
|
||||||
|
class="hidden md:flex items-center min-w-0 px-2"
|
||||||
|
v-if="settingStore.showCrumb && breadcrumbs.length"
|
||||||
|
>
|
||||||
<breadcrumb />
|
<breadcrumb />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex">
|
<div class="flex items-center gap-1">
|
||||||
<div class="navbar-item" v-if="!isMobile">
|
<div class="navbar-item" v-if="!isMobile">
|
||||||
<el-tooltip
|
<el-tooltip
|
||||||
class="box-item"
|
class="box-item"
|
||||||
@@ -36,12 +39,7 @@
|
|||||||
<user-drop-down />
|
<user-drop-down />
|
||||||
</div>
|
</div>
|
||||||
<div class="navbar-item">
|
<div class="navbar-item">
|
||||||
<el-tooltip
|
<el-tooltip class="box-item" effect="dark" content="主题设置" placement="bottom">
|
||||||
class="box-item"
|
|
||||||
effect="dark"
|
|
||||||
content="主题设置"
|
|
||||||
placement="bottom"
|
|
||||||
>
|
|
||||||
<setting />
|
<setting />
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
</div>
|
</div>
|
||||||
@@ -52,8 +50,10 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { RouteLocationMatched } from 'vue-router'
|
||||||
import { useFullscreen } from '@vueuse/core'
|
import { useFullscreen } from '@vueuse/core'
|
||||||
|
|
||||||
|
import { useWatchRoute } from '@/hooks/useWatchRoute'
|
||||||
import useAppStore from '@/stores/modules/app'
|
import useAppStore from '@/stores/modules/app'
|
||||||
import useSettingStore from '@/stores/modules/setting'
|
import useSettingStore from '@/stores/modules/setting'
|
||||||
|
|
||||||
@@ -70,14 +70,20 @@ const isMobile = computed(() => appStore.isMobile)
|
|||||||
const isCollapsed = computed(() => appStore.isCollapsed)
|
const isCollapsed = computed(() => appStore.isCollapsed)
|
||||||
const settingStore = useSettingStore()
|
const settingStore = useSettingStore()
|
||||||
const { isFullscreen } = useFullscreen()
|
const { isFullscreen } = useFullscreen()
|
||||||
|
|
||||||
|
const breadcrumbs = ref<RouteLocationMatched[]>([])
|
||||||
|
useWatchRoute((route) => {
|
||||||
|
breadcrumbs.value = route.matched.filter((item) => item.meta && item.meta.title)
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.navbar {
|
.navbar {
|
||||||
height: var(--navbar-height);
|
height: var(--navbar-height);
|
||||||
@apply flex px-2 bg-body;
|
@apply flex px-3 bg-body;
|
||||||
|
|
||||||
.navbar-item {
|
.navbar-item {
|
||||||
@apply h-full flex justify-center items-center hover:bg-page;
|
@apply h-full flex justify-center items-center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="app-tabs pl-4 flex bg-body">
|
<div class="app-tabs flex bg-body">
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0 pl-3">
|
||||||
<el-tabs
|
<el-tabs
|
||||||
:model-value="currentTab"
|
:model-value="currentTab"
|
||||||
:closable="tabsLists.length > 1"
|
:closable="tabsLists.length > 1"
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
</el-tabs>
|
</el-tabs>
|
||||||
</div>
|
</div>
|
||||||
<el-dropdown @command="handleCommand">
|
<el-dropdown @command="handleCommand">
|
||||||
<span class="flex items-center px-3">
|
<span class="tabs-more-btn flex items-center px-3">
|
||||||
<icon :size="16" name="el-icon-arrow-down" />
|
<icon :size="16" name="el-icon-arrow-down" />
|
||||||
</span>
|
</span>
|
||||||
<template #dropdown>
|
<template #dropdown>
|
||||||
@@ -60,61 +60,84 @@ const handleCommand = (command: any) => {
|
|||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.app-tabs {
|
.app-tabs {
|
||||||
@apply border-t border-br;
|
height: var(--tabs-height);
|
||||||
|
border-top: 1px solid var(--admin-header-border);
|
||||||
|
background: var(--admin-surface-glass);
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
|
||||||
|
.tabs-more-btn {
|
||||||
|
height: var(--tabs-height);
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color 0.2s ease, background-color 0.2s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--admin-brand-primary-dark);
|
||||||
|
background: var(--admin-brand-gradient-soft);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
:deep(.el-tabs) {
|
:deep(.el-tabs) {
|
||||||
height: 40px;
|
height: var(--tabs-height);
|
||||||
|
|
||||||
.el-tabs {
|
.el-tabs {
|
||||||
&__header {
|
&__header {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__content {
|
&__content {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__nav-next,
|
&__nav-next,
|
||||||
&__nav-prev {
|
&__nav-prev {
|
||||||
@apply text-xl;
|
@apply text-lg;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__nav-wrap::after {
|
&__nav-wrap::after {
|
||||||
height: 0;
|
height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__item {
|
&__item {
|
||||||
font-weight: normal;
|
font-weight: 600;
|
||||||
padding: 0 15px !important;
|
font-size: var(--el-font-size-small);
|
||||||
|
padding: 0 16px !important;
|
||||||
|
height: calc(var(--tabs-height) - 8px);
|
||||||
|
margin-top: 4px;
|
||||||
|
border-radius: var(--admin-radius-md) var(--admin-radius-md) 0 0;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
transition: color 0.2s ease, background-color 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--admin-brand-primary-dark);
|
||||||
|
}
|
||||||
|
|
||||||
&.is-active {
|
&.is-active {
|
||||||
color: var(--el-text-color-primary);
|
color: var(--admin-brand-primary-dark);
|
||||||
background-color: var(--el-color-primary-light-9);
|
background: var(--el-bg-color);
|
||||||
&::before {
|
border: 1px solid var(--el-border-color-lighter);
|
||||||
content: '';
|
border-bottom-color: transparent;
|
||||||
display: inline-block;
|
box-shadow: var(--el-box-shadow-lighter);
|
||||||
width: 6px;
|
|
||||||
height: 6px;
|
&::before,
|
||||||
background-color: var(--el-color-primary);
|
|
||||||
margin-right: 6px;
|
|
||||||
border-radius: 50%;
|
|
||||||
vertical-align: 2px;
|
|
||||||
}
|
|
||||||
&::after {
|
&::after {
|
||||||
position: absolute;
|
display: none;
|
||||||
content: '';
|
|
||||||
display: block;
|
|
||||||
top: 0;
|
|
||||||
height: 2px;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
background-color: var(--el-color-primary);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.is-icon-close {
|
.is-icon-close {
|
||||||
color: var(--el-text-color-regular);
|
color: var(--el-text-color-placeholder);
|
||||||
vertical-align: -2px;
|
vertical-align: -2px;
|
||||||
|
border-radius: var(--admin-radius-sm);
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: var(--color-white);
|
color: var(--color-white);
|
||||||
background-color: var(--el-color-danger);
|
background-color: var(--el-color-danger);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__active-bar {
|
&__active-bar {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-dropdown class="px-2" @command="handleCommand">
|
<el-dropdown class="user-dropdown px-1" @command="handleCommand">
|
||||||
<div class="flex items-center">
|
<div class="user-trigger flex items-center gap-2.5 px-2 py-1 rounded-md cursor-pointer">
|
||||||
<el-avatar :size="34" :src="userInfo.avatar" />
|
<el-avatar :size="32" :src="userInfo.avatar" />
|
||||||
<div class="ml-3 mr-1">{{ userInfo.name }}</div>
|
<span class="user-name max-w-[120px] truncate text-sm font-medium text-tx-primary">{{
|
||||||
<icon name="el-icon-ArrowDown" />
|
userInfo.name
|
||||||
|
}}</span>
|
||||||
|
<icon name="el-icon-ArrowDown" :size="14" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template #dropdown>
|
<template #dropdown>
|
||||||
@@ -41,3 +43,13 @@ const handleCommand = async (command: string) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.user-trigger {
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--el-fill-color-light);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,27 +1,56 @@
|
|||||||
<template>
|
<template>
|
||||||
<main class="main-wrap h-full bg-page">
|
|
||||||
|
<main class="main-wrap h-full">
|
||||||
|
|
||||||
<el-scrollbar>
|
<el-scrollbar>
|
||||||
<div class="px-2 py-4">
|
|
||||||
<router-view v-if="isRouteShow" v-slot="{ Component, route }">
|
<div class="main-stage">
|
||||||
<keep-alive :include="includeList" :max="20">
|
|
||||||
<component :is="Component" :key="route.fullPath" />
|
<page-shell v-if="isRouteShow">
|
||||||
</keep-alive>
|
|
||||||
</router-view>
|
<router-view v-slot="{ Component, route }">
|
||||||
|
|
||||||
|
<keep-alive :include="includeList" :max="20">
|
||||||
|
|
||||||
|
<component :is="Component" :key="route.fullPath" />
|
||||||
|
|
||||||
|
</keep-alive>
|
||||||
|
|
||||||
|
</router-view>
|
||||||
|
|
||||||
|
</page-shell>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</el-scrollbar>
|
</el-scrollbar>
|
||||||
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
import PageShell from '@/components/page-shell/index.vue'
|
||||||
|
|
||||||
import useAppStore from '@/stores/modules/app'
|
import useAppStore from '@/stores/modules/app'
|
||||||
|
|
||||||
import useTabsStore from '@/stores/modules/multipleTabs'
|
import useTabsStore from '@/stores/modules/multipleTabs'
|
||||||
|
|
||||||
import useSettingStore from '@/stores/modules/setting'
|
import useSettingStore from '@/stores/modules/setting'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const appStore = useAppStore()
|
const appStore = useAppStore()
|
||||||
|
|
||||||
const tabsStore = useTabsStore()
|
const tabsStore = useTabsStore()
|
||||||
|
|
||||||
const settingStore = useSettingStore()
|
const settingStore = useSettingStore()
|
||||||
|
|
||||||
const isRouteShow = computed(() => appStore.isRouteShow)
|
const isRouteShow = computed(() => appStore.isRouteShow)
|
||||||
|
|
||||||
const includeList = computed(() => (settingStore.openMultipleTabs ? tabsStore.getCacheTabList : []))
|
const includeList = computed(() => (settingStore.openMultipleTabs ? tabsStore.getCacheTabList : []))
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style></style>
|
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ import theme_light from '@/assets/images/theme_white.png'
|
|||||||
import useSettingStore from '@/stores/modules/setting'
|
import useSettingStore from '@/stores/modules/setting'
|
||||||
|
|
||||||
const settingStore = useSettingStore()
|
const settingStore = useSettingStore()
|
||||||
const predefineColors = ref(['#409EFF', '#28C76F', '#EA5455', '#FF9F43', '#01CFE8', '#4A5DFF'])
|
const predefineColors = ref(['#06b6d4', '#10b981', '#0891b2', '#6366f1', '#f59e0b', '#ef4444', '#64748b'])
|
||||||
const sideThemeList = [
|
const sideThemeList = [
|
||||||
{
|
{
|
||||||
type: 'dark',
|
type: 'dark',
|
||||||
|
|||||||
@@ -68,32 +68,42 @@ const themeClass = computed(() => `theme-${props.theme}`)
|
|||||||
.el-menu {
|
.el-menu {
|
||||||
:deep(.el-menu-item) {
|
:deep(.el-menu-item) {
|
||||||
&.is-active {
|
&.is-active {
|
||||||
@apply bg-primary border-primary;
|
@apply bg-primary;
|
||||||
|
box-shadow: inset 3px 0 0 var(--el-color-primary-light-3);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.el-menu--collapse) {
|
:deep(.el-menu--collapse) {
|
||||||
.el-sub-menu.is-active .el-sub-menu__title {
|
.el-sub-menu.is-active .el-sub-menu__title {
|
||||||
@apply bg-primary #{!important};
|
@apply bg-primary #{!important};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.theme-light {
|
&.theme-light {
|
||||||
:deep(.el-menu) {
|
:deep(.el-menu) {
|
||||||
.el-menu-item {
|
.el-menu-item {
|
||||||
border-color: transparent;
|
border-color: transparent;
|
||||||
|
|
||||||
&.is-active {
|
&.is-active {
|
||||||
@apply bg-primary-light-9 border-r-2 border-primary;
|
@apply bg-primary-light-9;
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
font-weight: 600;
|
||||||
|
box-shadow: inset 3px 0 0 var(--el-color-primary);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-menu-item:hover,
|
.el-menu-item:hover,
|
||||||
.el-sub-menu__title:hover {
|
.el-sub-menu__title:hover {
|
||||||
color: var(--el-color-primary);
|
color: var(--el-color-primary);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-menu {
|
.el-menu {
|
||||||
border-right: none;
|
border-right: none;
|
||||||
|
|
||||||
&:not(.el-menu--collapse) {
|
&:not(.el-menu--collapse) {
|
||||||
width: var(--aside-width);
|
width: var(--aside-width);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="side" :style="sideStyle">
|
<div class="side" :style="sideStyle">
|
||||||
|
<div v-if="showBrandStrip" class="sidebar-brand">
|
||||||
|
<div class="sidebar-brand__mark">ZY</div>
|
||||||
|
<overflow-tooltip
|
||||||
|
class="sidebar-brand__name"
|
||||||
|
:content="config.web_name"
|
||||||
|
:teleported="true"
|
||||||
|
placement="bottom"
|
||||||
|
overflo-type="unset"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<side-logo v-if="settingStore.showLogo" :show-title="!isCollapsed" :theme="sideTheme" />
|
<side-logo v-if="settingStore.showLogo" :show-title="!isCollapsed" :theme="sideTheme" />
|
||||||
<side-menu
|
<side-menu
|
||||||
:routes="routes"
|
:routes="routes"
|
||||||
@@ -25,17 +35,19 @@ const appStore = useAppStore()
|
|||||||
const isCollapsed = computed(() => {
|
const isCollapsed = computed(() => {
|
||||||
if (appStore.isMobile) {
|
if (appStore.isMobile) {
|
||||||
return false
|
return false
|
||||||
} else {
|
|
||||||
return appStore.isCollapsed
|
|
||||||
}
|
}
|
||||||
|
return appStore.isCollapsed
|
||||||
})
|
})
|
||||||
|
|
||||||
const settingStore = useSettingStore()
|
const settingStore = useSettingStore()
|
||||||
const sideTheme = computed(() => settingStore.sideTheme)
|
const sideTheme = computed(() => settingStore.sideTheme)
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
|
const config = computed(() => appStore.config)
|
||||||
|
|
||||||
const routes = computed(() => userStore.routes)
|
const routes = computed(() => userStore.routes)
|
||||||
|
|
||||||
|
const showBrandStrip = computed(() => !settingStore.showLogo && !isCollapsed.value)
|
||||||
|
|
||||||
const sideStyle = computed(() => {
|
const sideStyle = computed(() => {
|
||||||
return sideTheme.value == 'dark'
|
return sideTheme.value == 'dark'
|
||||||
? {
|
? {
|
||||||
@@ -43,6 +55,7 @@ const sideStyle = computed(() => {
|
|||||||
}
|
}
|
||||||
: ''
|
: ''
|
||||||
})
|
})
|
||||||
|
|
||||||
const menuProp = computed(() => {
|
const menuProp = computed(() => {
|
||||||
return {
|
return {
|
||||||
backgroundColor: sideTheme.value == 'dark' ? settingStore.sideDarkColor : '',
|
backgroundColor: sideTheme.value == 'dark' ? settingStore.sideDarkColor : '',
|
||||||
@@ -50,6 +63,7 @@ const menuProp = computed(() => {
|
|||||||
activeTextColor: sideTheme.value == 'dark' ? 'var(--el-color-white)' : ''
|
activeTextColor: sideTheme.value == 'dark' ? 'var(--el-color-white)' : ''
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleSelect = () => {
|
const handleSelect = () => {
|
||||||
if (appStore.isMobile) {
|
if (appStore.isMobile) {
|
||||||
appStore.toggleCollapsed(true)
|
appStore.toggleCollapsed(true)
|
||||||
@@ -61,7 +75,8 @@ const handleSelect = () => {
|
|||||||
.side {
|
.side {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 999;
|
z-index: 999;
|
||||||
@apply border-r border-br-light h-full flex flex-col;
|
@apply h-full flex flex-col;
|
||||||
background-color: var(--side-dark-color, var(--el-bg-color));
|
border-right: 1px solid var(--admin-sidebar-border);
|
||||||
|
background-color: var(--side-dark-color, var(--sidebar-dark-bg));
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,298 @@
|
|||||||
|
/**
|
||||||
|
* Admin 页面架构 v2 - 大改版面板系统
|
||||||
|
*/
|
||||||
|
|
||||||
|
.admin-page .page-stage__body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- FilterPanel ---------- */
|
||||||
|
.admin-filter-panel {
|
||||||
|
position: relative;
|
||||||
|
border-radius: var(--admin-radius-xl);
|
||||||
|
border: 1px solid var(--el-border-color-lighter);
|
||||||
|
background: var(--admin-surface-glass);
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
box-shadow: var(--el-box-shadow-lighter);
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0 0 auto;
|
||||||
|
height: 3px;
|
||||||
|
background: var(--admin-brand-gradient);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-filter-panel__head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 14px 22px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-filter-panel__label {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-filter-panel__toggle {
|
||||||
|
padding: 0;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-filter-panel__body {
|
||||||
|
padding: 14px 22px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-filter-panel.is-collapsed .admin-filter-panel__body {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-filter-panel .el-form--inline {
|
||||||
|
margin-bottom: 0;
|
||||||
|
|
||||||
|
.el-form-item {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
margin-right: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-form-item__label {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--el-text-color-regular);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- DataPanel ---------- */
|
||||||
|
.admin-data-panel {
|
||||||
|
border-radius: var(--admin-radius-xl);
|
||||||
|
border: 1px solid var(--el-border-color-lighter);
|
||||||
|
background: var(--admin-surface-elevated);
|
||||||
|
box-shadow: var(--el-box-shadow);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-data-panel__toolbar {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 16px 22px;
|
||||||
|
border-bottom: 1px solid var(--el-border-color-extra-light);
|
||||||
|
background: var(--admin-brand-gradient-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-data-panel__body {
|
||||||
|
padding: 0 22px 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-data-panel__toolbar + .admin-data-panel__body {
|
||||||
|
padding-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-data-panel__footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding: 0 22px 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- FormPanel ---------- */
|
||||||
|
.admin-form-panel {
|
||||||
|
border-radius: var(--admin-radius-xl);
|
||||||
|
border: 1px solid var(--el-border-color-lighter);
|
||||||
|
background: var(--admin-surface-elevated);
|
||||||
|
box-shadow: var(--el-box-shadow-light);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-form-panel__header {
|
||||||
|
padding: 16px 22px;
|
||||||
|
border-bottom: 1px solid var(--el-border-color-extra-light);
|
||||||
|
background: var(--admin-brand-gradient-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-form-panel__title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 17px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-form-panel__body {
|
||||||
|
padding: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-form-panel__footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 16px 22px 22px;
|
||||||
|
border-top: 1px solid var(--el-border-color-extra-light);
|
||||||
|
background: var(--el-fill-color-lighter);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- StatGrid ---------- */
|
||||||
|
.admin-stat-grid {
|
||||||
|
display: grid;
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-stat-grid--cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||||
|
.admin-stat-grid--cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
||||||
|
.admin-stat-grid--cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
|
||||||
|
.admin-stat-grid--cols-5 { grid-template-columns: repeat(5, minmax(0, 1fr)); }
|
||||||
|
|
||||||
|
.admin-stat-item {
|
||||||
|
position: relative;
|
||||||
|
padding: 18px 20px;
|
||||||
|
border-radius: var(--admin-radius-lg);
|
||||||
|
border: 1px solid var(--el-border-color-lighter);
|
||||||
|
background: var(--admin-surface-elevated);
|
||||||
|
box-shadow: var(--el-box-shadow-lighter);
|
||||||
|
overflow: hidden;
|
||||||
|
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0 auto 0 0;
|
||||||
|
width: 4px;
|
||||||
|
background: var(--el-border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: var(--el-box-shadow-light);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-stat-item--primary {
|
||||||
|
&::before { background: var(--admin-brand-gradient); }
|
||||||
|
border-color: rgba(6, 182, 212, 0.2);
|
||||||
|
background: linear-gradient(145deg, rgba(6, 182, 212, 0.08), rgba(16, 185, 129, 0.05));
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-stat-item--success {
|
||||||
|
&::before { background: #10b981; }
|
||||||
|
border-color: rgba(16, 185, 129, 0.22);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-stat-item--warning {
|
||||||
|
&::before { background: #f59e0b; }
|
||||||
|
border-color: rgba(245, 158, 11, 0.22);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-stat-item__label {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-stat-item__value {
|
||||||
|
margin-top: 10px;
|
||||||
|
font-size: 30px;
|
||||||
|
font-weight: 800;
|
||||||
|
line-height: 1.05;
|
||||||
|
letter-spacing: -0.03em;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-stat-item__value.is-money {
|
||||||
|
font-size: 26px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-stat-item__hint {
|
||||||
|
margin: 8px 0 0;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--el-text-color-placeholder);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- Tables ---------- */
|
||||||
|
.admin-data-panel .el-table,
|
||||||
|
.admin-page .el-card .el-table {
|
||||||
|
--el-table-border-color: transparent;
|
||||||
|
border-radius: var(--admin-radius-md);
|
||||||
|
|
||||||
|
thead th.el-table__cell {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 2;
|
||||||
|
background: var(--table-header-bg-color) !important;
|
||||||
|
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: var(--el-font-size-small);
|
||||||
|
}
|
||||||
|
|
||||||
|
td.el-table__cell {
|
||||||
|
border-bottom: 1px solid var(--el-border-color-extra-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-table__row:hover > td.el-table__cell {
|
||||||
|
background-color: rgba(6, 182, 212, 0.04) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-table__row.current-row > td.el-table__cell {
|
||||||
|
background-color: rgba(6, 182, 212, 0.08) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- Legacy cards ---------- */
|
||||||
|
.admin-page .page-stage__body {
|
||||||
|
> div > .el-card:first-child:has(.el-form),
|
||||||
|
> .el-card:first-child:has(.el-form) {
|
||||||
|
border-radius: var(--admin-radius-xl);
|
||||||
|
border: 1px solid var(--el-border-color-lighter);
|
||||||
|
box-shadow: var(--el-box-shadow-lighter);
|
||||||
|
}
|
||||||
|
|
||||||
|
> div > .el-card + .el-card,
|
||||||
|
> .el-card + .el-card {
|
||||||
|
border-radius: var(--admin-radius-xl);
|
||||||
|
box-shadow: var(--el-box-shadow);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.workbench-page {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.admin-stat-grid--cols-4,
|
||||||
|
.admin-stat-grid--cols-5 {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.admin-filter-panel__body,
|
||||||
|
.admin-data-panel__body,
|
||||||
|
.admin-data-panel__toolbar,
|
||||||
|
.admin-data-panel__footer {
|
||||||
|
padding-left: 16px;
|
||||||
|
padding-right: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-stat-grid--cols-2,
|
||||||
|
.admin-stat-grid--cols-3,
|
||||||
|
.admin-stat-grid--cols-4,
|
||||||
|
.admin-stat-grid--cols-5 {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-stat-item:hover {
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.admin-stat-item {
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,190 @@
|
|||||||
|
/* Admin Shell v2 - 互联网医院工作台大改版 */
|
||||||
|
|
||||||
|
.layout-default {
|
||||||
|
background: var(--sidebar-dark-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- Sidebar ---------- */
|
||||||
|
.app-aside .side {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0 auto 0 0;
|
||||||
|
width: var(--sidebar-rail-width);
|
||||||
|
background: var(--admin-brand-gradient);
|
||||||
|
z-index: 2;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 0% 0%, var(--admin-brand-mesh-a), transparent 45%),
|
||||||
|
radial-gradient(circle at 100% 100%, var(--admin-brand-mesh-b), transparent 40%);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-brand {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
min-height: calc(var(--navbar-height) + 4px);
|
||||||
|
padding: 0 18px 0 20px;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-brand__mark {
|
||||||
|
width: 38px;
|
||||||
|
height: 38px;
|
||||||
|
border-radius: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
color: #ffffff;
|
||||||
|
background: var(--admin-brand-gradient);
|
||||||
|
box-shadow: var(--admin-brand-glow);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-brand__name {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: rgba(255, 255, 255, 0.95);
|
||||||
|
line-height: 1.25;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu.theme-dark .el-menu {
|
||||||
|
--el-menu-bg-color: transparent;
|
||||||
|
--el-menu-hover-bg-color: var(--sidebar-dark-hover);
|
||||||
|
--el-menu-active-color: #ffffff;
|
||||||
|
--el-menu-text-color: rgba(255, 255, 255, 0.62);
|
||||||
|
padding: 12px 10px 16px;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu.theme-dark .el-menu .el-menu-item,
|
||||||
|
.menu.theme-dark .el-menu .el-sub-menu__title {
|
||||||
|
margin: 3px 8px;
|
||||||
|
border-radius: var(--admin-radius-md);
|
||||||
|
transition: background-color 0.2s ease, color 0.2s ease, box-shadow 0.2s ease, transform 0.12s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu.theme-dark .el-menu .el-menu-item.is-active {
|
||||||
|
background: var(--sidebar-dark-active) !important;
|
||||||
|
color: #ffffff;
|
||||||
|
font-weight: 600;
|
||||||
|
box-shadow:
|
||||||
|
inset 0 0 0 1px rgba(6, 182, 212, 0.28),
|
||||||
|
0 8px 24px rgba(6, 182, 212, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu.theme-dark .el-menu .el-menu-item:hover,
|
||||||
|
.menu.theme-dark .el-menu .el-sub-menu__title:hover {
|
||||||
|
background: var(--sidebar-dark-hover);
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu.theme-light .el-menu .el-menu-item.is-active {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--admin-brand-primary-dark);
|
||||||
|
background: var(--admin-brand-gradient-soft);
|
||||||
|
box-shadow: inset 3px 0 0 var(--admin-brand-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- Header ---------- */
|
||||||
|
.app-header {
|
||||||
|
position: relative;
|
||||||
|
z-index: 20;
|
||||||
|
background: var(--admin-surface-glass);
|
||||||
|
backdrop-filter: blur(20px) saturate(160%);
|
||||||
|
border-bottom: 1px solid var(--admin-header-border);
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: auto 0 0;
|
||||||
|
height: 2px;
|
||||||
|
background: var(--admin-brand-gradient);
|
||||||
|
opacity: 0.75;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:root.dark .app-header {
|
||||||
|
background: rgba(15, 23, 42, 0.88);
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-tool {
|
||||||
|
width: 38px;
|
||||||
|
height: 38px;
|
||||||
|
border-radius: var(--admin-radius-md);
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: var(--el-text-color-regular);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s ease, color 0.2s ease, transform 0.12s ease, box-shadow 0.2s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--admin-brand-gradient-soft);
|
||||||
|
color: var(--admin-brand-primary-dark);
|
||||||
|
box-shadow: var(--el-box-shadow-lighter);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: scale(0.96);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- Main stage (去掉双层白盒) ---------- */
|
||||||
|
.main-wrap {
|
||||||
|
position: relative;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 0% 0%, var(--admin-brand-mesh-a), transparent 34%),
|
||||||
|
radial-gradient(circle at 100% 0%, var(--admin-brand-mesh-c), transparent 30%),
|
||||||
|
radial-gradient(circle at 50% 100%, var(--admin-brand-mesh-b), transparent 38%),
|
||||||
|
var(--el-bg-color-page);
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-stage {
|
||||||
|
max-width: var(--content-max-width);
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: var(--stage-padding-y) var(--stage-padding-x);
|
||||||
|
min-height: calc(100vh - var(--navbar-height) - var(--tabs-height, 0px));
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
:root {
|
||||||
|
--stage-padding-x: 14px;
|
||||||
|
--stage-padding-y: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.navbar-tool,
|
||||||
|
.menu.theme-dark .el-menu .el-menu-item,
|
||||||
|
.menu.theme-dark .el-menu .el-sub-menu__title {
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-transparency: reduce) {
|
||||||
|
.app-header {
|
||||||
|
backdrop-filter: none;
|
||||||
|
background: var(--el-bg-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
+46
-31
@@ -1,43 +1,58 @@
|
|||||||
:root.dark {
|
:root.dark {
|
||||||
color-scheme: dark;
|
color-scheme: dark;
|
||||||
--table-header-bg-color: var(--el-bg-color);
|
|
||||||
--el-bg-color-page: #0a0a0a;
|
--table-header-bg-color: rgba(6, 182, 212, 0.12);
|
||||||
--el-bg-color: #1d2124;
|
--sidebar-dark-bg: #030712;
|
||||||
--el-bg-color-overlay: #1d1e1f;
|
--sidebar-dark-hover: rgba(255, 255, 255, 0.05);
|
||||||
--el-text-color-primary: #e5eaf3;
|
--sidebar-dark-active: rgba(6, 182, 212, 0.2);
|
||||||
--el-text-color-regular: #cfd3dc;
|
|
||||||
--el-text-color-secondary: #a3a6ad;
|
--admin-surface-elevated: #111827;
|
||||||
--el-text-color-placeholder: #8d9095;
|
--admin-surface-muted: rgba(17, 24, 39, 0.88);
|
||||||
--el-text-color-disabled: #6c6e72;
|
--admin-surface-glass: rgba(17, 24, 39, 0.86);
|
||||||
--el-border-color-darker: #636466;
|
|
||||||
--el-border-color-dark: #58585b;
|
--el-bg-color-page: #030712;
|
||||||
--el-border-color: #4c4d4f;
|
--el-bg-color: #0f172a;
|
||||||
--el-border-color-light: #414243;
|
--el-bg-color-overlay: #1e293b;
|
||||||
--el-border-color-lighter: #363637;
|
--el-text-color-primary: #f1f5f9;
|
||||||
--el-border-color-extra-light: #2b2b2c;
|
--el-text-color-regular: #cbd5e1;
|
||||||
--el-fill-color-darker: #424243;
|
--el-text-color-secondary: #94a3b8;
|
||||||
--el-fill-color-dark: #39393a;
|
--el-text-color-placeholder: #64748b;
|
||||||
--el-fill-color: #303030;
|
--el-text-color-disabled: #475569;
|
||||||
--el-fill-color-light: #262727;
|
--el-border-color-darker: #64748b;
|
||||||
--el-fill-color-lighter: #1d1d1d;
|
--el-border-color-dark: #475569;
|
||||||
--el-fill-color-extra-light: #191919;
|
--el-border-color: rgba(6, 182, 212, 0.18);
|
||||||
|
--el-border-color-light: rgba(6, 182, 212, 0.12);
|
||||||
|
--el-border-color-lighter: rgba(255, 255, 255, 0.06);
|
||||||
|
--el-border-color-extra-light: rgba(255, 255, 255, 0.04);
|
||||||
|
--el-fill-color-darker: #334155;
|
||||||
|
--el-fill-color-dark: #1e293b;
|
||||||
|
--el-fill-color: #1e293b;
|
||||||
|
--el-fill-color-light: #172033;
|
||||||
|
--el-fill-color-lighter: #131c2e;
|
||||||
|
--el-fill-color-extra-light: #0f172a;
|
||||||
--el-fill-color-blank: var(--el-bg-color);
|
--el-fill-color-blank: var(--el-bg-color);
|
||||||
--el-mask-color: rgba(0, 0, 0, 0.8);
|
|
||||||
--el-mask-color-extra-light: rgba(0, 0, 0, 0.3);
|
--el-mask-color: rgba(2, 6, 23, 0.78);
|
||||||
--el-box-shadow: 0px 12px 32px 4px rgba(0, 0, 0, 0.36), 0px 8px 20px rgba(0, 0, 0, 0.72);
|
--el-mask-color-extra-light: rgba(2, 6, 23, 0.42);
|
||||||
--el-box-shadow-light: 0px 0px 12px rgba(0, 0, 0, 0.72);
|
|
||||||
--el-box-shadow-lighter: 0px 0px 6px rgba(0, 0, 0, 0.72);
|
--el-box-shadow: 0 4px 24px rgba(0, 0, 0, 0.28);
|
||||||
--el-box-shadow-dark: 0px 16px 48px 16px rgba(0, 0, 0, 0.72), 0px 12px 32px #000000,
|
--el-box-shadow-light: 0 2px 16px rgba(0, 0, 0, 0.22);
|
||||||
0px 8px 16px -8px #000000 !important;
|
--el-box-shadow-lighter: 0 1px 4px rgba(0, 0, 0, 0.16);
|
||||||
/* wangeditor主题 */
|
--el-box-shadow-dark: 0 16px 48px rgba(0, 0, 0, 0.36);
|
||||||
|
|
||||||
|
--admin-header-border: rgba(6, 182, 212, 0.14);
|
||||||
|
--admin-sidebar-border: rgba(255, 255, 255, 0.04);
|
||||||
|
|
||||||
|
--admin-brand-mesh-a: rgba(6, 182, 212, 0.12);
|
||||||
|
--admin-brand-mesh-b: rgba(16, 185, 129, 0.08);
|
||||||
|
--admin-brand-mesh-c: rgba(99, 102, 241, 0.06);
|
||||||
|
|
||||||
--w-e-textarea-bg-color: var(--el-bg-color);
|
--w-e-textarea-bg-color: var(--el-bg-color);
|
||||||
--w-e-textarea-color: var(--el-text-color-primary);
|
--w-e-textarea-color: var(--el-text-color-primary);
|
||||||
--w-e-textarea-border-color: var(--el-border-color);
|
--w-e-textarea-border-color: var(--el-border-color);
|
||||||
--w-e-textarea-slight-border-color: var(--el-border-color-light);
|
--w-e-textarea-slight-border-color: var(--el-border-color-light);
|
||||||
--w-e-textarea-slight-color: var(--el-border-color);
|
--w-e-textarea-slight-color: var(--el-border-color);
|
||||||
--w-e-textarea-slight-bg-color: var(--el-bg-color-page);
|
--w-e-textarea-slight-bg-color: var(--el-bg-color-page);
|
||||||
/* --w-e-textarea-selected-border-color: #b4d5ff;
|
|
||||||
--w-e-textarea-handler-bg-color: #4290f7; */
|
|
||||||
--w-e-toolbar-color: var(--el-text-color-primary);
|
--w-e-toolbar-color: var(--el-text-color-primary);
|
||||||
--w-e-toolbar-bg-color: var(--el-bg-color);
|
--w-e-toolbar-bg-color: var(--el-bg-color);
|
||||||
--w-e-toolbar-active-color: var(--el-text-color-primary);
|
--w-e-toolbar-active-color: var(--el-text-color-primary);
|
||||||
|
|||||||
+246
-26
@@ -1,14 +1,20 @@
|
|||||||
:root {
|
:root {
|
||||||
// 确保消息提示在最上层
|
/* Messages & notifications */
|
||||||
.el-message {
|
.el-message {
|
||||||
z-index: 9999 !important;
|
z-index: 9999 !important;
|
||||||
|
border-radius: var(--admin-radius-md);
|
||||||
|
border: 1px solid var(--el-border-color-lighter);
|
||||||
|
box-shadow: var(--el-box-shadow);
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-notification {
|
.el-notification {
|
||||||
z-index: 9999 !important;
|
z-index: 9999 !important;
|
||||||
|
border-radius: var(--admin-radius-lg);
|
||||||
|
border: 1px solid var(--el-border-color-lighter);
|
||||||
|
box-shadow: var(--el-box-shadow);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 弹窗居中
|
/* Dialog */
|
||||||
.el-overlay-dialog {
|
.el-overlay-dialog {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -19,11 +25,14 @@
|
|||||||
.el-dialog {
|
.el-dialog {
|
||||||
--el-dialog-content-font-size: var(--el-font-size-base);
|
--el-dialog-content-font-size: var(--el-font-size-base);
|
||||||
--el-dialog-margin-top: 50px;
|
--el-dialog-margin-top: 50px;
|
||||||
max-width: calc(100vw - 30px);
|
--el-dialog-border-radius: var(--admin-radius-lg);
|
||||||
|
max-width: calc(100vw - 32px);
|
||||||
flex: none;
|
flex: none;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
border-radius: 5px;
|
border-radius: var(--admin-radius-lg);
|
||||||
|
border: 1px solid var(--el-border-color-lighter);
|
||||||
|
box-shadow: var(--el-box-shadow-dark);
|
||||||
|
|
||||||
&.body-padding .el-dialog__body {
|
&.body-padding .el-dialog__body {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@@ -31,50 +40,158 @@
|
|||||||
|
|
||||||
.el-dialog__body {
|
.el-dialog__body {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 15px 20px;
|
padding: 16px 20px 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-dialog__header {
|
.el-dialog__header {
|
||||||
font-size: var(--el-font-size-large);
|
font-size: var(--el-font-size-large);
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 16px 20px 12px;
|
||||||
|
margin-right: 0;
|
||||||
|
border-bottom: 1px solid var(--el-border-color-extra-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-dialog__footer {
|
||||||
|
padding: 12px 20px 16px;
|
||||||
|
border-top: 1px solid var(--el-border-color-extra-light);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Cards - applies to all list pages */
|
||||||
|
.el-card {
|
||||||
|
--el-card-border-radius: var(--admin-radius-lg);
|
||||||
|
border: 1px solid var(--el-border-color-lighter);
|
||||||
|
box-shadow: none;
|
||||||
|
background: var(--admin-surface-elevated);
|
||||||
|
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
|
||||||
|
&.is-always-shadow,
|
||||||
|
&.is-hover-shadow:hover {
|
||||||
|
box-shadow: var(--el-box-shadow-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-card__header {
|
||||||
|
padding: 14px 18px;
|
||||||
|
border-bottom: 1px solid var(--el-border-color-extra-light);
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-card__body {
|
||||||
|
padding: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Menu */
|
||||||
.el-menu {
|
.el-menu {
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Drawer */
|
||||||
.el-drawer {
|
.el-drawer {
|
||||||
--el-drawer-padding-primary: 16px;
|
--el-drawer-padding-primary: 16px;
|
||||||
|
|
||||||
&__header {
|
&__header {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
padding: 13px 16px;
|
padding: 14px 18px;
|
||||||
border-bottom: 1px solid var(--el-border-color-lighter);
|
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||||
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__title {
|
&__title {
|
||||||
@apply text-tx-primary;
|
@apply text-tx-primary;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.el-table {
|
&__body {
|
||||||
--el-table-header-text-color: var(--el-text-color-primary);
|
padding: 16px 18px;
|
||||||
--el-table-header-bg-color: var(--table-header-bg-color);
|
|
||||||
font-size: var(--el-font-size-base);
|
|
||||||
|
|
||||||
thead {
|
|
||||||
th {
|
|
||||||
font-weight: 400;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Table */
|
||||||
|
.el-table {
|
||||||
|
--el-table-header-text-color: var(--el-text-color-primary);
|
||||||
|
--el-table-header-bg-color: var(--table-header-bg-color);
|
||||||
|
--el-table-border-color: var(--el-border-color-extra-light);
|
||||||
|
--el-table-row-hover-bg-color: var(--el-fill-color-lighter);
|
||||||
|
font-size: var(--el-font-size-base);
|
||||||
|
border-radius: var(--admin-radius-md);
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
thead {
|
||||||
|
th {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: var(--el-font-size-small);
|
||||||
|
letter-spacing: 0.01em;
|
||||||
|
}
|
||||||
|
|
||||||
|
th.el-table__cell {
|
||||||
|
background-color: var(--table-header-bg-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-table__cell {
|
||||||
|
padding: 12px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form controls */
|
||||||
.el-input-group__prepend {
|
.el-input-group__prepend {
|
||||||
background-color: var(--el-fill-color-blank);
|
background-color: var(--el-fill-color-light);
|
||||||
|
border-color: var(--el-border-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-checkbox {
|
.el-checkbox {
|
||||||
--el-checkbox-font-size: var(--el-font-size-base);
|
--el-checkbox-font-size: var(--el-font-size-base);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.el-button {
|
||||||
|
--el-border-radius-base: var(--admin-radius-md);
|
||||||
|
font-weight: 500;
|
||||||
|
transition: transform 0.12s ease, box-shadow 0.2s ease, background-color 0.2s ease;
|
||||||
|
|
||||||
|
&:active:not(.is-disabled) {
|
||||||
|
transform: scale(0.98);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-button--primary:not(.is-link):not(.is-text):not(.is-plain) {
|
||||||
|
background: var(--admin-brand-gradient);
|
||||||
|
border: none;
|
||||||
|
box-shadow: var(--admin-brand-glow);
|
||||||
|
--el-button-bg-color: transparent;
|
||||||
|
--el-button-border-color: transparent;
|
||||||
|
--el-button-hover-bg-color: transparent;
|
||||||
|
--el-button-hover-border-color: transparent;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
filter: brightness(1.05);
|
||||||
|
box-shadow: 0 12px 32px rgba(6, 182, 212, 0.32);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-button--primary.is-link,
|
||||||
|
.el-button--primary.is-text {
|
||||||
|
--el-button-hover-link-text-color: var(--admin-brand-primary-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-button--primary.is-plain {
|
||||||
|
--el-button-bg-color: rgba(6, 182, 212, 0.1);
|
||||||
|
--el-button-border-color: rgba(6, 182, 212, 0.35);
|
||||||
|
--el-button-text-color: var(--admin-brand-primary-dark);
|
||||||
|
--el-button-hover-bg-color: rgba(6, 182, 212, 0.16);
|
||||||
|
--el-button-hover-border-color: var(--admin-brand-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-button--large {
|
||||||
|
--el-border-radius-base: var(--admin-radius-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-button.is-round {
|
||||||
|
--el-border-radius-base: var(--admin-radius-round);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Popup menus */
|
||||||
.el-menu--popup-container {
|
.el-menu--popup-container {
|
||||||
&.theme-light {
|
&.theme-light {
|
||||||
.el-menu {
|
.el-menu {
|
||||||
@@ -83,12 +200,14 @@
|
|||||||
@apply bg-primary-light-9 border-primary border-r-2;
|
@apply bg-primary-light-9 border-primary border-r-2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-menu-item:hover,
|
.el-menu-item:hover,
|
||||||
.el-sub-menu__title:hover {
|
.el-sub-menu__title:hover {
|
||||||
color: var(--el-color-primary);
|
color: var(--el-color-primary);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.theme-dark {
|
&.theme-dark {
|
||||||
.el-menu {
|
.el-menu {
|
||||||
.el-menu-item {
|
.el-menu-item {
|
||||||
@@ -101,52 +220,120 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.el-message-box {
|
.el-message-box {
|
||||||
--el-messagebox-width: 350px;
|
--el-messagebox-width: 380px;
|
||||||
|
--el-messagebox-border-radius: var(--admin-radius-lg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-date-editor {
|
.el-date-editor {
|
||||||
--el-date-editor-datetimerange-width: 380px;
|
--el-date-editor-datetimerange-width: 380px;
|
||||||
|
|
||||||
.el-range-input {
|
.el-range-input {
|
||||||
font-size: var(--el-font-size-small);
|
font-size: var(--el-font-size-small);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-button--primary {
|
|
||||||
--el-button-hover-link-text-color: var(--el-color-primary-light-3);
|
|
||||||
}
|
|
||||||
.el-button--success {
|
.el-button--success {
|
||||||
--el-button-hover-link-text-color: var(--el-color-success-light-3);
|
--el-button-hover-link-text-color: var(--el-color-success-light-3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-button--info {
|
.el-button--info {
|
||||||
--el-button-hover-link-text-color: var(--el-color-info-light-3);
|
--el-button-hover-link-text-color: var(--el-color-info-light-3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-button--warning {
|
.el-button--warning {
|
||||||
--el-button-hover-link-text-color: var(--el-color-warning-light-3);
|
--el-button-hover-link-text-color: var(--el-color-warning-light-3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-button--danger {
|
.el-button--danger {
|
||||||
--el-button-hover-link-text-color: var(--el-color-danger-light-3);
|
--el-button-hover-link-text-color: var(--el-color-danger-light-3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-image__error {
|
.el-image__error {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-tabs__nav-wrap::after {
|
.el-tabs__nav-wrap::after {
|
||||||
height: 1px;
|
height: 1px;
|
||||||
|
background: var(--el-border-color-extra-light);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.el-tabs__item {
|
||||||
|
font-weight: 500;
|
||||||
|
transition: color 0.2s ease;
|
||||||
|
|
||||||
|
&.is-active {
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--el-color-primary-light-3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-tabs__active-bar {
|
||||||
|
height: 3px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--admin-brand-gradient);
|
||||||
|
}
|
||||||
|
|
||||||
.el-page-header {
|
.el-page-header {
|
||||||
&__breadcrumb {
|
&__breadcrumb {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Tags */
|
||||||
|
.el-tag {
|
||||||
|
--el-tag-border-radius: var(--admin-radius-sm);
|
||||||
|
border: none;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pagination */
|
||||||
|
.el-pagination {
|
||||||
|
.el-pager li {
|
||||||
|
border-radius: var(--admin-radius-sm);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-prev,
|
||||||
|
.btn-next {
|
||||||
|
border-radius: var(--admin-radius-sm);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Alert */
|
||||||
|
.el-alert {
|
||||||
|
border-radius: var(--admin-radius-md);
|
||||||
|
border: 1px solid var(--el-border-color-extra-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Empty state */
|
||||||
|
.el-empty {
|
||||||
|
padding: 32px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Descriptions */
|
||||||
|
.el-descriptions {
|
||||||
|
.el-descriptions__label {
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Focus rings */
|
||||||
.el-input,
|
.el-input,
|
||||||
.el-select,
|
.el-select,
|
||||||
.el-textarea {
|
.el-textarea {
|
||||||
@apply shadow-primary-light-8;
|
@apply shadow-primary-light-8;
|
||||||
|
|
||||||
box-shadow: 0 0 0 0 var(--tw-shadow-color);
|
box-shadow: 0 0 0 0 var(--tw-shadow-color);
|
||||||
|
|
||||||
&:focus-within {
|
&:focus-within {
|
||||||
box-shadow: 0 0 0 2px var(--tw-shadow-color);
|
box-shadow: 0 0 0 2px var(--tw-shadow-color);
|
||||||
border-radius: var(--el-input-border-radius, var(--el-border-radius-base));
|
border-radius: var(--el-input-border-radius, var(--el-border-radius-base));
|
||||||
transition: box-shadow ease 0.1s;
|
transition: box-shadow ease 0.15s;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,6 +343,7 @@
|
|||||||
border-radius: var(--el-checkbox-border-radius);
|
border-radius: var(--el-checkbox-border-radius);
|
||||||
|
|
||||||
box-shadow: 0 0 0 0 var(--tw-shadow-color);
|
box-shadow: 0 0 0 0 var(--tw-shadow-color);
|
||||||
|
|
||||||
&:active {
|
&:active {
|
||||||
box-shadow: 0 0 0 2px var(--tw-shadow-color);
|
box-shadow: 0 0 0 2px var(--tw-shadow-color);
|
||||||
transition: box-shadow ease 0s;
|
transition: box-shadow ease 0s;
|
||||||
@@ -173,29 +361,61 @@
|
|||||||
.el-form-item.is-error .el-checkbox {
|
.el-form-item.is-error .el-checkbox {
|
||||||
@apply shadow-danger-light-8;
|
@apply shadow-danger-light-8;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Dropdown */
|
||||||
|
.el-dropdown-menu {
|
||||||
|
border-radius: var(--admin-radius-md);
|
||||||
|
border: 1px solid var(--el-border-color-lighter);
|
||||||
|
box-shadow: var(--el-box-shadow);
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-dropdown-menu__item {
|
||||||
|
border-radius: var(--admin-radius-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Popover / tooltip polish */
|
||||||
|
.el-popover.el-popper {
|
||||||
|
border-radius: var(--admin-radius-md);
|
||||||
|
border: 1px solid var(--el-border-color-lighter);
|
||||||
|
box-shadow: var(--el-box-shadow);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.el-pagination > .el-pagination__jump {
|
.el-pagination > .el-pagination__jump {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-pagination > .el-pagination__sizes {
|
.el-pagination > .el-pagination__sizes {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-button {
|
.el-button {
|
||||||
// 防止被tailwindcss默认样式覆盖
|
|
||||||
background-color: var(--el-button-bg-color, var(--el-color-white));
|
background-color: var(--el-button-bg-color, var(--el-color-white));
|
||||||
|
|
||||||
//覆盖el-button的点击样式
|
|
||||||
&:focus {
|
&:focus {
|
||||||
color: var(--el-button-text-color);
|
color: var(--el-button-text-color);
|
||||||
border-color: var(--el-button-border-color);
|
border-color: var(--el-button-border-color);
|
||||||
background-color: var(--el-button-bg-color);
|
background-color: var(--el-button-bg-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: var(--el-button-hover-text-color);
|
color: var(--el-button-hover-text-color);
|
||||||
border-color: var(--el-button-hover-border-color);
|
border-color: var(--el-button-hover-border-color);
|
||||||
background-color: var(--el-button-hover-bg-color);
|
background-color: var(--el-button-hover-bg-color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Inline form filter blocks on list pages */
|
||||||
|
.el-card .el-form--inline {
|
||||||
|
.el-form-item {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Page section spacing between stacked cards */
|
||||||
|
.el-card + .el-card {
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
@import 'element.scss';
|
|
||||||
@import 'dark.css';
|
|
||||||
@import 'var.css';
|
@import 'var.css';
|
||||||
|
@import 'dark.css';
|
||||||
@import 'tailwind.css';
|
@import 'tailwind.css';
|
||||||
|
@import 'element.scss';
|
||||||
|
@import 'admin-shell.scss';
|
||||||
|
@import 'admin-pages.scss';
|
||||||
@import 'public.scss';
|
@import 'public.scss';
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
body {
|
body {
|
||||||
@apply text-base text-tx-primary overflow-hidden min-w-[375px];
|
@apply text-base text-tx-primary overflow-hidden min-w-[375px];
|
||||||
|
font-feature-settings: 'kern' 1;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-tips {
|
.form-tips {
|
||||||
@apply text-tx-secondary text-xs leading-6 mt-1;
|
@apply text-tx-secondary text-xs leading-6 mt-1;
|
||||||
}
|
}
|
||||||
@@ -12,7 +16,51 @@ body {
|
|||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Scrollbar */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--el-border-color);
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
background-clip: content-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: var(--el-border-color-dark);
|
||||||
|
background-clip: content-box;
|
||||||
|
}
|
||||||
|
|
||||||
/* NProgress */
|
/* NProgress */
|
||||||
#nprogress .bar {
|
#nprogress .bar {
|
||||||
@apply bg-primary #{!important};
|
@apply bg-primary #{!important};
|
||||||
|
height: 2px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#nprogress .peg {
|
||||||
|
box-shadow: 0 0 8px var(--el-color-primary), 0 0 4px var(--el-color-primary) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Shared page utilities */
|
||||||
|
.admin-page-title {
|
||||||
|
@apply text-xl font-semibold text-tx-primary tracking-tight;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-page-desc {
|
||||||
|
@apply text-sm text-tx-secondary mt-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-stat-value {
|
||||||
|
@apply text-3xl font-semibold text-tx-primary tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-stat-label {
|
||||||
|
@apply text-sm text-tx-secondary;
|
||||||
}
|
}
|
||||||
|
|||||||
+85
-35
@@ -1,49 +1,99 @@
|
|||||||
:root {
|
:root {
|
||||||
|
/* Typography */
|
||||||
--el-font-family: theme(fontFamily.sans);
|
--el-font-family: theme(fontFamily.sans);
|
||||||
--el-font-weight-primary: 400;
|
--el-font-weight-primary: 400;
|
||||||
--el-menu-item-height: 46px;
|
|
||||||
--el-menu-sub-item-height: var(--el-menu-item-height);
|
|
||||||
--el-menu-icon-width: 18px;
|
|
||||||
--aside-width: 200px;
|
|
||||||
--navbar-height: 50px;
|
|
||||||
--color-white: #ffffff;
|
|
||||||
--table-header-bg-color: #f8f8f8;
|
|
||||||
--el-font-size-extra-large: 18px;
|
--el-font-size-extra-large: 18px;
|
||||||
--el-menu-base-level-padding: 16px;
|
|
||||||
--el-menu-level-padding: 26px;
|
|
||||||
--el-font-size-large: 16px;
|
--el-font-size-large: 16px;
|
||||||
--el-font-size-medium: 15px;
|
--el-font-size-medium: 15px;
|
||||||
--el-font-size-base: 14px;
|
--el-font-size-base: 14px;
|
||||||
--el-font-size-small: 13px;
|
--el-font-size-small: 13px;
|
||||||
--el-font-size-extra-small: 12px;
|
--el-font-size-extra-small: 12px;
|
||||||
|
|
||||||
|
/* Brand - MedTech 绚丽临床 */
|
||||||
|
--admin-brand-primary: #06b6d4;
|
||||||
|
--admin-brand-primary-dark: #0891b2;
|
||||||
|
--admin-brand-accent: #10b981;
|
||||||
|
--admin-brand-secondary: #6366f1;
|
||||||
|
--admin-brand-success: #10b981;
|
||||||
|
--admin-brand-gradient: linear-gradient(120deg, #06b6d4 0%, #10b981 48%, #0891b2 100%);
|
||||||
|
--admin-brand-gradient-soft: linear-gradient(
|
||||||
|
120deg,
|
||||||
|
rgba(6, 182, 212, 0.16) 0%,
|
||||||
|
rgba(16, 185, 129, 0.1) 50%,
|
||||||
|
rgba(8, 145, 178, 0.08) 100%
|
||||||
|
);
|
||||||
|
--admin-brand-glow: 0 0 24px rgba(6, 182, 212, 0.35);
|
||||||
|
--admin-brand-mesh-a: rgba(6, 182, 212, 0.18);
|
||||||
|
--admin-brand-mesh-b: rgba(16, 185, 129, 0.14);
|
||||||
|
--admin-brand-mesh-c: rgba(99, 102, 241, 0.1);
|
||||||
|
|
||||||
|
/* Layout shell v2 */
|
||||||
|
--aside-width: 248px;
|
||||||
|
--navbar-height: 60px;
|
||||||
|
--tabs-height: 42px;
|
||||||
|
--stage-padding-x: 28px;
|
||||||
|
--stage-padding-y: 24px;
|
||||||
|
--content-max-width: 1680px;
|
||||||
|
--sidebar-rail-width: 3px;
|
||||||
|
|
||||||
|
/* Shape */
|
||||||
|
--admin-radius-sm: 8px;
|
||||||
|
--admin-radius-md: 12px;
|
||||||
|
--admin-radius-lg: 16px;
|
||||||
|
--admin-radius-xl: 22px;
|
||||||
|
--admin-radius-2xl: 28px;
|
||||||
|
--el-border-radius-base: var(--admin-radius-md);
|
||||||
|
--el-border-radius-small: var(--admin-radius-sm);
|
||||||
|
--el-border-radius-round: 999px;
|
||||||
|
|
||||||
|
/* Menu */
|
||||||
|
--el-menu-item-height: 46px;
|
||||||
|
--el-menu-sub-item-height: var(--el-menu-item-height);
|
||||||
|
--el-menu-icon-width: 20px;
|
||||||
|
--el-menu-base-level-padding: 14px;
|
||||||
|
--el-menu-level-padding: 22px;
|
||||||
|
|
||||||
|
/* Surfaces */
|
||||||
|
--color-white: #ffffff;
|
||||||
|
--table-header-bg-color: rgba(6, 182, 212, 0.06);
|
||||||
|
--sidebar-dark-bg: #060d18;
|
||||||
|
--sidebar-dark-hover: rgba(255, 255, 255, 0.05);
|
||||||
|
--sidebar-dark-active: rgba(6, 182, 212, 0.14);
|
||||||
|
--admin-surface-elevated: #ffffff;
|
||||||
|
--admin-surface-muted: rgba(255, 255, 255, 0.72);
|
||||||
|
--admin-surface-glass: rgba(255, 255, 255, 0.82);
|
||||||
|
|
||||||
--el-bg-color: var(--color-white);
|
--el-bg-color: var(--color-white);
|
||||||
--el-bg-color-page: #f6f6f6;
|
--el-bg-color-page: #eef6fb;
|
||||||
--el-bg-color-overlay: #ffffff;
|
--el-bg-color-overlay: #ffffff;
|
||||||
--el-text-color-primary: #333333;
|
--el-text-color-primary: #0b1220;
|
||||||
--el-text-color-regular: #666666;
|
--el-text-color-regular: #334155;
|
||||||
--el-text-color-secondary: #999999;
|
--el-text-color-secondary: #64748b;
|
||||||
--el-text-color-placeholder: #a8abb2;
|
--el-text-color-placeholder: #94a3b8;
|
||||||
--el-text-color-disabled: #c0c4cc;
|
--el-text-color-disabled: #cbd5e1;
|
||||||
--el-border-color: #dcdfe6;
|
--el-border-color: rgba(6, 182, 212, 0.12);
|
||||||
--el-border-color-light: #e4e7ed;
|
--el-border-color-light: rgba(6, 182, 212, 0.08);
|
||||||
--el-border-color-lighter: #ebeef5;
|
--el-border-color-lighter: rgba(15, 23, 42, 0.06);
|
||||||
--el-border-color-extra-light: #f2f2f2;
|
--el-border-color-extra-light: rgba(15, 23, 42, 0.04);
|
||||||
--el-border-color-dark: #d4d7de;
|
--el-border-color-dark: #cbd5e1;
|
||||||
--el-border-color-darker: #cdd0d6;
|
--el-border-color-darker: #94a3b8;
|
||||||
--el-fill-color: #f0f2f5;
|
--el-fill-color: #f1f5f9;
|
||||||
--el-fill-color-light: #f8f8f8;
|
--el-fill-color-light: #f8fafc;
|
||||||
--el-fill-color-lighter: #fafafa;
|
--el-fill-color-lighter: #fafbfc;
|
||||||
--el-fill-color-extra-light: #fafcff;
|
--el-fill-color-extra-light: #fcfdfe;
|
||||||
--el-fill-color-dark: #ebedf0;
|
--el-fill-color-dark: #e2e8f0;
|
||||||
--el-fill-color-darker: #e6e8eb;
|
--el-fill-color-darker: #cbd5e1;
|
||||||
--el-fill-color-blank: #ffffff;
|
--el-fill-color-blank: #ffffff;
|
||||||
/* 过亮会盖住抽屉/弹窗下的内容;Element Loading 与部分蒙层共用此变量 */
|
|
||||||
--el-mask-color: rgba(255, 255, 255, 0.5);
|
--el-mask-color: rgba(6, 13, 24, 0.55);
|
||||||
--el-mask-color-extra-light: rgba(255, 255, 255, 0.22);
|
--el-mask-color-extra-light: rgba(6, 13, 24, 0.1);
|
||||||
-el-box-shadow: 0px 12px 32px 4px rgba(0, 0, 0, 0.04), 0px 8px 20px rgba(0, 0, 0, 0.08);
|
|
||||||
--el-box-shadow-light: 0px 0px 12px rgba(0, 0, 0, 0.12);
|
--el-box-shadow: 0 4px 24px rgba(6, 182, 212, 0.08), 0 12px 40px rgba(15, 23, 42, 0.06);
|
||||||
--el-box-shadow-lighter: 0px 0px 6px rgba(0, 0, 0, 0.12);
|
--el-box-shadow-light: 0 2px 16px rgba(6, 182, 212, 0.1);
|
||||||
--el-box-shadow-dark: 0px 16px 48px 16px rgba(0, 0, 0, 0.08), 0px 12px 32px rgba(0, 0, 0, 0.12),
|
--el-box-shadow-lighter: 0 1px 4px rgba(15, 23, 42, 0.04);
|
||||||
0px 8px 16px -8px rgba(0, 0, 0, 0.16);
|
--el-box-shadow-dark: 0 16px 48px rgba(6, 182, 212, 0.14), 0 32px 64px rgba(15, 23, 42, 0.1);
|
||||||
|
|
||||||
|
--admin-header-border: rgba(6, 182, 212, 0.1);
|
||||||
|
--admin-sidebar-border: rgba(255, 255, 255, 0.06);
|
||||||
|
--admin-stage-bg: transparent;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="change-password flex flex-col">
|
<div class="change-password flex flex-col">
|
||||||
<div class="flex-1 flex items-center justify-center">
|
<div class="change-password-backdrop" aria-hidden="true"></div>
|
||||||
<div class="change-password-card bg-body rounded-md px-10 py-10 w-[480px]">
|
<div class="flex-1 flex items-center justify-center relative z-[1] px-4">
|
||||||
<div class="text-center text-2xl font-medium mb-2">首次登录</div>
|
<div class="change-password-card">
|
||||||
<div class="text-center text-gray-500 text-sm mb-8">为了您的账号安全,请修改初始密码</div>
|
<div class="text-center text-2xl font-semibold mb-2 text-tx-primary">首次登录</div>
|
||||||
|
<div class="text-center text-tx-secondary text-sm mb-8">为了您的账号安全,请修改初始密码</div>
|
||||||
|
|
||||||
<el-form ref="formRef" :model="formData" size="large" :rules="rules">
|
<el-form ref="formRef" :model="formData" size="large" :rules="rules">
|
||||||
<el-form-item prop="password">
|
<el-form-item prop="password">
|
||||||
@@ -117,7 +118,25 @@ const { isLock, lockFn: lockSubmit } = useLockFn(handleSubmit)
|
|||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.change-password {
|
.change-password {
|
||||||
background-image: url('./images/login_bg.png');
|
position: relative;
|
||||||
@apply min-h-screen bg-no-repeat bg-center bg-cover;
|
min-height: 100vh;
|
||||||
|
background: #0f172a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.change-password-backdrop {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 15% 20%, rgba(15, 118, 110, 0.35), transparent 42%),
|
||||||
|
linear-gradient(160deg, #0f172a 0%, #111827 48%, #0b1120 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.change-password-card {
|
||||||
|
width: min(480px, 100%);
|
||||||
|
padding: 40px 36px;
|
||||||
|
border-radius: var(--admin-radius-xl);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
box-shadow: 0 24px 64px rgba(2, 6, 23, 0.45);
|
||||||
|
background: var(--el-bg-color);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,33 +1,41 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="login flex flex-col">
|
<div class="login-v2">
|
||||||
<div class="flex-1 flex items-center justify-center">
|
<div class="login-v2__mesh" aria-hidden="true"></div>
|
||||||
<div class="login-card flex rounded-md overflow-hidden">
|
<div class="login-v2__layout">
|
||||||
<div class="flex-1 h-full hidden md:inline-block">
|
<aside class="login-v2__brand">
|
||||||
<image-contain :src="config.login_image" :width="400" height="100%" />
|
<div class="login-v2__brand-inner">
|
||||||
|
<div class="login-v2__mark">ZYT</div>
|
||||||
|
<p class="login-v2__kicker">互联网医院</p>
|
||||||
|
<h1 class="login-v2__headline">医生与医助<br />智慧工作台</h1>
|
||||||
|
<p class="login-v2__lead">问诊、处方、订单与运营数据,一屏协同。</p>
|
||||||
|
<div v-if="config.login_image" class="login-v2__visual hidden xl:block">
|
||||||
|
<image-contain :src="config.login_image" :width="420" height="280" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
</aside>
|
||||||
class="login-form bg-body flex flex-col justify-center px-10 py-10 md:w-[420px] w-[380px] flex-none mx-auto"
|
|
||||||
>
|
|
||||||
<div class="text-center text-3xl font-medium mb-8">{{ config.web_name }}</div>
|
|
||||||
|
|
||||||
<!-- 企业微信自动授权中 -->
|
<section class="login-v2__panel">
|
||||||
<div v-if="wxWorkAutoLogin" class="text-center py-10">
|
<div class="login-v2__glass">
|
||||||
|
<div class="login-v2__form-head">
|
||||||
|
<h2 class="login-v2__form-title">{{ config.web_name }}</h2>
|
||||||
|
<p class="login-v2__form-sub">登录后继续你的接诊与管理工作</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="wxWorkAutoLogin" class="login-v2__loading">
|
||||||
<el-icon class="is-loading mb-4" :size="40" color="var(--el-color-primary)">
|
<el-icon class="is-loading mb-4" :size="40" color="var(--el-color-primary)">
|
||||||
<Loading />
|
<Loading />
|
||||||
</el-icon>
|
</el-icon>
|
||||||
<div class="text-gray-500">企业微信授权登录中...</div>
|
<div class="text-tx-secondary">企业微信授权登录中...</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<!-- 登录方式切换标签 -->
|
<div v-if="wxWorkEnabled" class="login-v2__mode">
|
||||||
<div v-if="wxWorkEnabled" class="flex justify-center mb-6">
|
|
||||||
<el-radio-group v-model="loginMode" size="large">
|
<el-radio-group v-model="loginMode" size="large">
|
||||||
<el-radio-button value="account">账号登录</el-radio-button>
|
<el-radio-button value="account">账号登录</el-radio-button>
|
||||||
<el-radio-button value="wxwork">企业微信</el-radio-button>
|
<el-radio-button value="wxwork">企业微信</el-radio-button>
|
||||||
</el-radio-group>
|
</el-radio-group>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 账号密码登录 -->
|
|
||||||
<template v-if="loginMode === 'account'">
|
<template v-if="loginMode === 'account'">
|
||||||
<el-form ref="formRef" :model="formData" size="large" :rules="rules">
|
<el-form ref="formRef" :model="formData" size="large" :rules="rules">
|
||||||
<el-form-item prop="account">
|
<el-form-item prop="account">
|
||||||
@@ -58,29 +66,34 @@
|
|||||||
<div class="mb-5">
|
<div class="mb-5">
|
||||||
<el-checkbox v-model="remAccount" label="记住账号"></el-checkbox>
|
<el-checkbox v-model="remAccount" label="记住账号"></el-checkbox>
|
||||||
</div>
|
</div>
|
||||||
<el-button type="primary" size="large" :loading="isLock" @click="lockLogin">
|
<el-button
|
||||||
登录
|
class="login-v2__submit"
|
||||||
|
type="primary"
|
||||||
|
size="large"
|
||||||
|
:loading="isLock"
|
||||||
|
@click="lockLogin"
|
||||||
|
>
|
||||||
|
进入工作台
|
||||||
</el-button>
|
</el-button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- 企业微信扫码登录(非企业微信内浏览器) -->
|
|
||||||
<template v-if="loginMode === 'wxwork'">
|
<template v-if="loginMode === 'wxwork'">
|
||||||
<div class="wxwork-qrcode-wrap">
|
<div class="wxwork-qrcode-wrap">
|
||||||
<div v-if="wxWorkLoading" class="text-center py-10">
|
<div v-if="wxWorkLoading" class="text-center py-10">
|
||||||
<el-icon class="is-loading" :size="32" color="var(--el-color-primary)">
|
<el-icon class="is-loading" :size="32" color="var(--el-color-primary)">
|
||||||
<Loading />
|
<Loading />
|
||||||
</el-icon>
|
</el-icon>
|
||||||
<div class="mt-2 text-gray-400 text-sm">加载企业微信扫码...</div>
|
<div class="mt-2 text-tx-secondary text-sm">加载企业微信扫码...</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else id="wxwork_qrcode_container" class="wxwork-qrcode"></div>
|
<div v-else id="wxwork_qrcode_container" class="wxwork-qrcode"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-center text-sm text-gray-400 mt-4">
|
<div class="text-center text-sm text-tx-secondary mt-4">
|
||||||
请使用企业微信扫描二维码登录
|
请使用企业微信扫描二维码登录
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
<layout-footer />
|
<layout-footer />
|
||||||
</div>
|
</div>
|
||||||
@@ -286,31 +299,178 @@ onMounted(async () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.login {
|
.login-v2 {
|
||||||
background-image: url('./images/login_bg.png');
|
position: relative;
|
||||||
@apply min-h-screen bg-no-repeat bg-center bg-cover;
|
min-height: 100vh;
|
||||||
.login-card {
|
display: flex;
|
||||||
height: auto;
|
flex-direction: column;
|
||||||
min-height: 400px;
|
background: #030712;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-v2__mesh {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 12% 18%, rgba(6, 182, 212, 0.35), transparent 42%),
|
||||||
|
radial-gradient(circle at 88% 12%, rgba(16, 185, 129, 0.28), transparent 38%),
|
||||||
|
radial-gradient(circle at 70% 88%, rgba(99, 102, 241, 0.18), transparent 40%),
|
||||||
|
linear-gradient(155deg, #030712 0%, #0b1220 45%, #060d18 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-v2__layout {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
flex: 1;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
min-height: calc(100vh - 48px);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.login-v2__layout {
|
||||||
|
grid-template-columns: minmax(0, 1.05fr) minmax(420px, 520px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.login-v2__brand {
|
||||||
|
display: none;
|
||||||
|
padding: 48px 56px;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.login-v2__brand {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-v2__brand-inner {
|
||||||
|
max-width: 520px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-v2__mark {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border-radius: 14px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
background: var(--admin-brand-gradient);
|
||||||
|
box-shadow: var(--admin-brand-glow);
|
||||||
|
margin-bottom: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-v2__kicker {
|
||||||
|
margin: 0 0 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.14em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: rgba(255, 255, 255, 0.72);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-v2__headline {
|
||||||
|
margin: 0;
|
||||||
|
font-size: clamp(32px, 4vw, 44px);
|
||||||
|
font-weight: 800;
|
||||||
|
line-height: 1.15;
|
||||||
|
letter-spacing: -0.03em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-v2__lead {
|
||||||
|
margin: 16px 0 0;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: rgba(255, 255, 255, 0.72);
|
||||||
|
max-width: 36ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-v2__visual {
|
||||||
|
margin-top: 36px;
|
||||||
|
border-radius: var(--admin-radius-xl);
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||||
|
box-shadow: 0 24px 64px rgba(0, 0, 0, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-v2__panel {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 32px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-v2__glass {
|
||||||
|
width: min(440px, 100%);
|
||||||
|
padding: 36px 32px;
|
||||||
|
border-radius: var(--admin-radius-2xl);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.14);
|
||||||
|
background: rgba(255, 255, 255, 0.92);
|
||||||
|
backdrop-filter: blur(24px) saturate(160%);
|
||||||
|
box-shadow: var(--el-box-shadow-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-v2__form-head {
|
||||||
|
margin-bottom: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-v2__form-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 26px;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-v2__form-sub {
|
||||||
|
margin: 8px 0 0;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-v2__mode {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-v2__submit {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-v2__loading {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px 0;
|
||||||
|
}
|
||||||
|
|
||||||
.wxwork-qrcode-wrap {
|
.wxwork-qrcode-wrap {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
min-height: 400px;
|
min-height: 360px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wxwork-qrcode {
|
.wxwork-qrcode {
|
||||||
width: 340px;
|
width: 320px;
|
||||||
height: 400px;
|
height: 360px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
:deep(iframe) {
|
:deep(iframe) {
|
||||||
width: 340px !important;
|
width: 320px !important;
|
||||||
height: 400px !important;
|
height: 360px !important;
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-transparency: reduce) {
|
||||||
|
.login-v2__glass {
|
||||||
|
background: #ffffff;
|
||||||
|
backdrop-filter: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<el-card class="!border-none" shadow="never">
|
<admin-page-filter-panel>
|
||||||
<el-alert
|
<el-alert
|
||||||
type="warning"
|
type="warning"
|
||||||
title="温馨提示:用于管理网站的分类,只可添加到一级"
|
title="温馨提示:用于管理网站的分类,只可添加到一级"
|
||||||
:closable="false"
|
:closable="false"
|
||||||
show-icon
|
show-icon
|
||||||
/>
|
/>
|
||||||
</el-card>
|
</admin-page-filter-panel>
|
||||||
<el-card class="!border-none mt-4" shadow="never" v-loading="pager.loading">
|
|
||||||
<div>
|
<admin-page-data-panel v-loading="pager.loading">
|
||||||
|
<template #toolbar>
|
||||||
<el-button
|
<el-button
|
||||||
class="mb-4"
|
|
||||||
v-perms="['article.articleCate/add']"
|
v-perms="['article.articleCate/add']"
|
||||||
type="primary"
|
type="primary"
|
||||||
@click="handleAdd()"
|
@click="handleAdd()"
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
</template>
|
</template>
|
||||||
新增
|
新增
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</template>
|
||||||
<el-table size="large" :data="pager.lists">
|
<el-table size="large" :data="pager.lists">
|
||||||
<el-table-column label="栏目名称" prop="name" min-width="120" />
|
<el-table-column label="栏目名称" prop="name" min-width="120" />
|
||||||
<el-table-column label="文章数" prop="article_count" min-width="120" />
|
<el-table-column label="文章数" prop="article_count" min-width="120" />
|
||||||
@@ -58,10 +58,10 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
<div class="flex justify-end mt-4">
|
<template #footer>
|
||||||
<pagination v-model="pager" @change="getLists" />
|
<pagination v-model="pager" @change="getLists" />
|
||||||
</div>
|
</template>
|
||||||
</el-card>
|
</admin-page-data-panel>
|
||||||
<edit-popup v-if="showEdit" ref="editRef" @success="getLists" @close="showEdit = false" />
|
<edit-popup v-if="showEdit" ref="editRef" @success="getLists" @close="showEdit = false" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="article-lists">
|
<div>
|
||||||
<el-card class="!border-none" shadow="never">
|
<admin-page-filter-panel>
|
||||||
<el-form ref="formRef" class="mb-[-16px]" :model="queryParams" :inline="true">
|
<el-form ref="formRef" class="mb-[-16px]" :model="queryParams" :inline="true">
|
||||||
<el-form-item class="w-[280px]" label="文章标题">
|
<el-form-item class="w-[280px]" label="文章标题">
|
||||||
<el-input
|
<el-input
|
||||||
@@ -33,24 +33,25 @@
|
|||||||
<el-button @click="resetParams">重置</el-button>
|
<el-button @click="resetParams">重置</el-button>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
</el-card>
|
</admin-page-filter-panel>
|
||||||
<el-card class="!border-none mt-4" shadow="never">
|
|
||||||
<div>
|
<admin-page-data-panel v-loading="pager.loading">
|
||||||
|
<template #toolbar>
|
||||||
<router-link
|
<router-link
|
||||||
v-perms="['article.article/add', 'article.article/add:edit']"
|
v-perms="['article.article/add', 'article.article/add:edit']"
|
||||||
:to="{
|
:to="{
|
||||||
path: getRoutePath('article.article/add:edit')
|
path: getRoutePath('article.article/add:edit')
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<el-button type="primary" class="mb-4">
|
<el-button type="primary">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<icon name="el-icon-Plus" />
|
<icon name="el-icon-Plus" />
|
||||||
</template>
|
</template>
|
||||||
发布文章
|
发布文章
|
||||||
</el-button>
|
</el-button>
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</template>
|
||||||
<el-table size="large" v-loading="pager.loading" :data="pager.lists">
|
<el-table size="large" :data="pager.lists">
|
||||||
<el-table-column label="ID" prop="id" min-width="80" />
|
<el-table-column label="ID" prop="id" min-width="80" />
|
||||||
<el-table-column label="封面" min-width="100">
|
<el-table-column label="封面" min-width="100">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
@@ -116,10 +117,10 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
<div class="flex justify-end mt-4">
|
<template #footer>
|
||||||
<pagination v-model="pager" @change="getLists" />
|
<pagination v-model="pager" @change="getLists" />
|
||||||
</div>
|
</template>
|
||||||
</el-card>
|
</admin-page-data-panel>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts" setup name="articleLists">
|
<script lang="ts" setup name="articleLists">
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="asset-resource-container">
|
<div class="asset-resource-container">
|
||||||
<!-- 搜索区域 -->
|
<!-- 搜索区域 -->
|
||||||
<el-card class="!border-none" shadow="never">
|
<admin-page-filter-panel>
|
||||||
<el-form class="ls-form" :model="searchData" inline>
|
<el-form class="ls-form" :model="searchData" inline>
|
||||||
<el-form-item class="w-[280px]" label="标题">
|
<el-form-item class="w-[280px]" label="标题">
|
||||||
<el-input
|
<el-input
|
||||||
@@ -28,25 +28,25 @@
|
|||||||
<el-button @click="handleReset">重置</el-button>
|
<el-button @click="handleReset">重置</el-button>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
</el-card>
|
</admin-page-filter-panel>
|
||||||
|
|
||||||
<!-- 列表区域 -->
|
<!-- 列表区域 -->
|
||||||
<el-card class="!border-none mt-4" shadow="never">
|
<admin-page-data-panel v-loading="loading">
|
||||||
<div class="mb-4 flex items-center gap-2">
|
<template #toolbar>
|
||||||
<el-button type="primary" @click="handleAdd">上传资源</el-button>
|
<el-button type="primary" @click="handleAdd">上传资源</el-button>
|
||||||
<el-button type="danger" :disabled="selectedIds.length === 0" @click="handleBatchDelete">
|
<el-button type="danger" :disabled="selectedIds.length === 0" @click="handleBatchDelete">
|
||||||
批量删除
|
批量删除
|
||||||
</el-button>
|
</el-button>
|
||||||
<span v-if="selectedIds.length > 0" class="text-sm text-gray-500">已选 {{ selectedIds.length }} 条</span>
|
<span v-if="selectedIds.length > 0" class="text-sm text-gray-500">已选 {{ selectedIds.length }} 条</span>
|
||||||
</div>
|
</template>
|
||||||
|
|
||||||
<el-tabs v-model="queryParams.type" @tab-change="handleTabChange">
|
<el-tabs v-model="queryParams.type" @tab-change="handleTabChange">
|
||||||
<el-tab-pane label="图片" name="1"></el-tab-pane>
|
<el-tab-pane label="图片" name="1"></el-tab-pane>
|
||||||
<el-tab-pane label="视频" name="2"></el-tab-pane>
|
<el-tab-pane label="视频" name="2"></el-tab-pane>
|
||||||
<el-tab-pane label="语音" name="3"></el-tab-pane>
|
<el-tab-pane label="语音" name="3"></el-tab-pane>
|
||||||
</el-tabs>
|
</el-tabs>
|
||||||
|
|
||||||
<el-table :data="tableData" v-loading="loading" @selection-change="handleSelectionChange">
|
<el-table :data="tableData" @selection-change="handleSelectionChange">
|
||||||
<el-table-column type="selection" width="55" />
|
<el-table-column type="selection" width="55" />
|
||||||
<!-- <el-table-column prop="id" label="ID" width="80" /> -->
|
<!-- <el-table-column prop="id" label="ID" width="80" /> -->
|
||||||
<el-table-column prop="title" label="标题" min-width="80" />
|
<el-table-column prop="title" label="标题" min-width="80" />
|
||||||
@@ -71,7 +71,7 @@
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
|
|
||||||
<div class="mt-4 flex justify-end">
|
<template #footer>
|
||||||
<el-pagination
|
<el-pagination
|
||||||
v-model:current-page="queryParams.page_no"
|
v-model:current-page="queryParams.page_no"
|
||||||
v-model:page-size="queryParams.page_size"
|
v-model:page-size="queryParams.page_size"
|
||||||
@@ -80,8 +80,8 @@
|
|||||||
@size-change="getList"
|
@size-change="getList"
|
||||||
@current-change="getList"
|
@current-change="getList"
|
||||||
/>
|
/>
|
||||||
</div>
|
</template>
|
||||||
</el-card>
|
</admin-page-data-panel>
|
||||||
|
|
||||||
<!-- 新增/编辑资源弹窗 -->
|
<!-- 新增/编辑资源弹窗 -->
|
||||||
<el-dialog v-model="dialogVisible" :title="isEdit ? '编辑资源' : '新增分发资源'" width="600px" destroy-on-close>
|
<el-dialog v-model="dialogVisible" :title="isEdit ? '编辑资源' : '新增分发资源'" width="600px" destroy-on-close>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="asset-user-container">
|
<div class="asset-user-container">
|
||||||
<!-- 搜索区域 -->
|
<!-- 搜索区域 -->
|
||||||
<el-card class="!border-none" shadow="never">
|
<admin-page-filter-panel>
|
||||||
<el-form class="ls-form" :model="searchData" inline>
|
<el-form class="ls-form" :model="searchData" inline>
|
||||||
<el-form-item class="w-[280px]" label="手机号">
|
<el-form-item class="w-[280px]" label="手机号">
|
||||||
<el-input
|
<el-input
|
||||||
@@ -16,15 +16,15 @@
|
|||||||
<el-button @click="handleReset">重置</el-button>
|
<el-button @click="handleReset">重置</el-button>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
</el-card>
|
</admin-page-filter-panel>
|
||||||
|
|
||||||
<!-- 列表区域 -->
|
<!-- 列表区域 -->
|
||||||
<el-card class="!border-none mt-4" shadow="never">
|
<admin-page-data-panel v-loading="loading">
|
||||||
<div class="mb-4">
|
<template #toolbar>
|
||||||
<el-button type="primary" @click="handleAdd">新增账号</el-button>
|
<el-button type="primary" @click="handleAdd">新增账号</el-button>
|
||||||
</div>
|
</template>
|
||||||
|
|
||||||
<el-table :data="tableData" v-loading="loading">
|
<el-table :data="tableData">
|
||||||
<!-- <el-table-column prop="id" label="ID" width="80" /> -->
|
<!-- <el-table-column prop="id" label="ID" width="80" /> -->
|
||||||
<el-table-column prop="phone" label="手机号" />
|
<el-table-column prop="phone" label="手机号" />
|
||||||
<el-table-column label="备注" min-width="200">
|
<el-table-column label="备注" min-width="200">
|
||||||
@@ -63,7 +63,7 @@
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
|
|
||||||
<div class="mt-4 flex justify-end">
|
<template #footer>
|
||||||
<el-pagination
|
<el-pagination
|
||||||
v-model:current-page="queryParams.page_no"
|
v-model:current-page="queryParams.page_no"
|
||||||
v-model:page-size="queryParams.page_size"
|
v-model:page-size="queryParams.page_size"
|
||||||
@@ -72,8 +72,8 @@
|
|||||||
@size-change="getList"
|
@size-change="getList"
|
||||||
@current-change="getList"
|
@current-change="getList"
|
||||||
/>
|
/>
|
||||||
</div>
|
</template>
|
||||||
</el-card>
|
</admin-page-data-panel>
|
||||||
|
|
||||||
<!-- 编辑/新增弹窗 -->
|
<!-- 编辑/新增弹窗 -->
|
||||||
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="500px" destroy-on-close>
|
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="500px" destroy-on-close>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<el-card class="!border-none" shadow="never">
|
<admin-page-filter-panel>
|
||||||
<el-form ref="formRef" class="mb-[-16px]" :model="queryParams" :inline="true">
|
<el-form ref="formRef" class="mb-[-16px]" :model="queryParams" :inline="true">
|
||||||
<el-form-item class="w-[280px]" label="用户信息">
|
<el-form-item class="w-[280px]" label="用户信息">
|
||||||
<el-input
|
<el-input
|
||||||
@@ -37,8 +37,9 @@
|
|||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
</el-card>
|
</admin-page-filter-panel>
|
||||||
<el-card class="!border-none mt-4" shadow="never">
|
|
||||||
|
<admin-page-data-panel>
|
||||||
<el-table size="large" v-loading="pager.loading" :data="pager.lists">
|
<el-table size="large" v-loading="pager.loading" :data="pager.lists">
|
||||||
<el-table-column label="头像" min-width="100">
|
<el-table-column label="头像" min-width="100">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
@@ -67,10 +68,10 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
<div class="flex justify-end mt-4">
|
<template #footer>
|
||||||
<pagination v-model="pager" @change="getLists" />
|
<pagination v-model="pager" @change="getLists" />
|
||||||
</div>
|
</template>
|
||||||
</el-card>
|
</admin-page-data-panel>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts" setup name="consumerLists">
|
<script lang="ts" setup name="consumerLists">
|
||||||
|
|||||||
+28
-407
@@ -327,27 +327,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item>
|
<el-descriptions-item label="服用方式">
|
||||||
<template #label>
|
|
||||||
<div class="flex items-center gap-1">
|
|
||||||
<span>服用方式</span>
|
|
||||||
<el-button
|
|
||||||
v-if="
|
|
||||||
!readonly &&
|
|
||||||
detailData.prescription_id &&
|
|
||||||
detailPrescription &&
|
|
||||||
!String(detailData.prescription_detail_error || '').trim()
|
|
||||||
"
|
|
||||||
v-perms="['tcm.prescriptionOrder/patchPrescriptionUsage']"
|
|
||||||
type="primary"
|
|
||||||
size="small"
|
|
||||||
link
|
|
||||||
@click="openPatchUsageDialog"
|
|
||||||
>
|
|
||||||
修改
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<div class="flex flex-col gap-1.5 text-sm leading-relaxed">
|
<div class="flex flex-col gap-1.5 text-sm leading-relaxed">
|
||||||
<template v-if="detailHasAuxHerbs">
|
<template v-if="detailHasAuxHerbs">
|
||||||
<div>
|
<div>
|
||||||
@@ -651,7 +631,7 @@
|
|||||||
<el-descriptions-item label="上次医护">{{ detailData.prev_staff || '—' }}</el-descriptions-item>
|
<el-descriptions-item label="上次医护">{{ detailData.prev_staff || '—' }}</el-descriptions-item>
|
||||||
|
|
||||||
<el-descriptions-item label="服务渠道">{{ detailData.service_channel || '—' }}</el-descriptions-item>
|
<el-descriptions-item label="服务渠道">{{ detailData.service_channel || '—' }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="服务套餐">{{ detailServicePackageText }}</el-descriptions-item>
|
<el-descriptions-item label="服务套餐">{{ formatServicePackage(detailData.service_package) }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="费用类别">{{ feeTypeText(detailData.fee_type) }}</el-descriptions-item>
|
<el-descriptions-item label="费用类别">{{ feeTypeText(detailData.fee_type) }}</el-descriptions-item>
|
||||||
|
|
||||||
<el-descriptions-item v-if="showInternalCost" label="内部成本">
|
<el-descriptions-item v-if="showInternalCost" label="内部成本">
|
||||||
@@ -800,18 +780,7 @@
|
|||||||
class="po-panel border-gray-100 mt-4"
|
class="po-panel border-gray-100 mt-4"
|
||||||
>
|
>
|
||||||
<template #header>
|
<template #header>
|
||||||
<div class="flex items-center justify-between gap-2">
|
<span class="font-medium text-[15px]">操作日志</span>
|
||||||
<span class="font-medium text-[15px]">操作日志</span>
|
|
||||||
<el-button
|
|
||||||
v-if="canAddPrescriptionOrderLog()"
|
|
||||||
type="primary"
|
|
||||||
link
|
|
||||||
size="small"
|
|
||||||
@click="openAddLogDialog"
|
|
||||||
>
|
|
||||||
新增日志
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
<el-timeline v-if="detailLogs.length" class="mt-2 pl-2">
|
<el-timeline v-if="detailLogs.length" class="mt-2 pl-2">
|
||||||
<el-timeline-item
|
<el-timeline-item
|
||||||
@@ -831,182 +800,19 @@
|
|||||||
</el-timeline>
|
</el-timeline>
|
||||||
<el-empty v-else description="暂无操作日志" :image-size="64" />
|
<el-empty v-else description="暂无操作日志" :image-size="64" />
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
<!-- 新增操作日志 -->
|
|
||||||
<el-dialog
|
|
||||||
v-model="addLogVisible"
|
|
||||||
title="新增操作日志"
|
|
||||||
width="520px"
|
|
||||||
:close-on-click-modal="false"
|
|
||||||
destroy-on-close
|
|
||||||
append-to-body
|
|
||||||
@closed="resetAddLogForm"
|
|
||||||
>
|
|
||||||
<el-form ref="addLogFormRef" :model="addLogForm" :rules="addLogRules" label-width="108px">
|
|
||||||
<el-form-item label="日志内容" prop="summary">
|
|
||||||
<el-input
|
|
||||||
v-model="addLogForm.summary"
|
|
||||||
type="textarea"
|
|
||||||
:rows="4"
|
|
||||||
maxlength="500"
|
|
||||||
show-word-limit
|
|
||||||
placeholder="记录本次操作说明、沟通结果等"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item v-if="canSetRxAuditOnAddLog" label="处方审核">
|
|
||||||
<el-select v-model="addLogForm.prescription_audit_status" class="w-full" clearable placeholder="不修改">
|
|
||||||
<el-option label="待审核" :value="0" />
|
|
||||||
<el-option label="已通过" :value="1" />
|
|
||||||
<el-option label="已驳回" :value="2" />
|
|
||||||
</el-select>
|
|
||||||
<div v-if="detailData" class="text-xs text-gray-400 mt-1">
|
|
||||||
当前:{{ auditStatusText(detailData.prescription_audit_status) }}
|
|
||||||
</div>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item
|
|
||||||
v-if="canSetRxAuditOnAddLog && addLogForm.prescription_audit_status !== '' && addLogForm.prescription_audit_status !== null && addLogForm.prescription_audit_status !== undefined"
|
|
||||||
label="处方审核意见"
|
|
||||||
>
|
|
||||||
<el-input
|
|
||||||
v-model="addLogForm.prescription_audit_remark"
|
|
||||||
type="textarea"
|
|
||||||
:rows="2"
|
|
||||||
maxlength="500"
|
|
||||||
show-word-limit
|
|
||||||
placeholder="选填"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item v-if="canSetPayAuditOnAddLog" label="支付单审核">
|
|
||||||
<el-select v-model="addLogForm.payment_slip_audit_status" class="w-full" clearable placeholder="不修改">
|
|
||||||
<el-option label="待审核" :value="0" />
|
|
||||||
<el-option label="已通过" :value="1" />
|
|
||||||
<el-option label="已驳回" :value="2" />
|
|
||||||
</el-select>
|
|
||||||
<div v-if="detailData" class="text-xs text-gray-400 mt-1">
|
|
||||||
当前:{{ auditStatusText(detailData.payment_slip_audit_status) }}
|
|
||||||
</div>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item
|
|
||||||
v-if="canSetPayAuditOnAddLog && addLogForm.payment_slip_audit_status !== '' && addLogForm.payment_slip_audit_status !== null && addLogForm.payment_slip_audit_status !== undefined"
|
|
||||||
label="支付审核意见"
|
|
||||||
>
|
|
||||||
<el-input
|
|
||||||
v-model="addLogForm.payment_slip_audit_remark"
|
|
||||||
type="textarea"
|
|
||||||
:rows="2"
|
|
||||||
maxlength="500"
|
|
||||||
show-word-limit
|
|
||||||
placeholder="选填"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
<template #footer>
|
|
||||||
<el-button @click="addLogVisible = false">取消</el-button>
|
|
||||||
<el-button type="primary" :loading="addLogSaving" @click="submitAddLog">保存</el-button>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
|
||||||
|
|
||||||
<!-- 修改服用参数:主方 / 辅方 / 订单设置 -->
|
|
||||||
<el-dialog
|
|
||||||
v-model="patchUsageVisible"
|
|
||||||
title="修改服用参数"
|
|
||||||
width="480px"
|
|
||||||
:close-on-click-modal="false"
|
|
||||||
destroy-on-close
|
|
||||||
@closed="resetPatchUsageForm"
|
|
||||||
>
|
|
||||||
<el-form
|
|
||||||
ref="patchUsageFormRef"
|
|
||||||
:model="patchUsageForm"
|
|
||||||
:rules="patchUsageRules"
|
|
||||||
label-width="108px"
|
|
||||||
>
|
|
||||||
<div v-if="detailHasAuxHerbs" class="text-xs font-medium text-gray-500 mb-3">主方</div>
|
|
||||||
<el-form-item label="每天次数" prop="times_per_day">
|
|
||||||
<el-input-number
|
|
||||||
v-model="patchUsageForm.times_per_day"
|
|
||||||
:min="1"
|
|
||||||
:max="6"
|
|
||||||
:step="1"
|
|
||||||
controls-position="right"
|
|
||||||
class="w-full"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="处方开立" prop="usage_days">
|
|
||||||
<div class="flex items-center gap-1 w-full">
|
|
||||||
<el-input-number
|
|
||||||
v-model="patchUsageForm.usage_days"
|
|
||||||
:min="1"
|
|
||||||
:max="999"
|
|
||||||
:step="1"
|
|
||||||
controls-position="right"
|
|
||||||
class="flex-1 min-w-0"
|
|
||||||
/>
|
|
||||||
<span class="text-gray-500 shrink-0">天</span>
|
|
||||||
</div>
|
|
||||||
</el-form-item>
|
|
||||||
<template v-if="detailHasAuxHerbs">
|
|
||||||
<div class="text-xs font-medium text-gray-500 mb-3 mt-2 pt-2 border-t border-gray-100">辅方</div>
|
|
||||||
<el-form-item label="每天次数" prop="aux_times_per_day">
|
|
||||||
<el-input-number
|
|
||||||
v-model="patchUsageForm.aux_times_per_day"
|
|
||||||
:min="1"
|
|
||||||
:max="6"
|
|
||||||
:step="1"
|
|
||||||
controls-position="right"
|
|
||||||
class="w-full"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="处方开立" prop="aux_usage_days">
|
|
||||||
<div class="flex items-center gap-1 w-full">
|
|
||||||
<el-input-number
|
|
||||||
v-model="patchUsageForm.aux_usage_days"
|
|
||||||
:min="1"
|
|
||||||
:max="999"
|
|
||||||
:step="1"
|
|
||||||
controls-position="right"
|
|
||||||
class="flex-1 min-w-0"
|
|
||||||
/>
|
|
||||||
<span class="text-gray-500 shrink-0">天</span>
|
|
||||||
</div>
|
|
||||||
</el-form-item>
|
|
||||||
</template>
|
|
||||||
<div class="text-xs font-medium text-gray-500 mb-3 mt-2 pt-2 border-t border-gray-100">订单设置</div>
|
|
||||||
<el-form-item label="服用天数" prop="medication_days">
|
|
||||||
<div class="flex items-center gap-1 w-full">
|
|
||||||
<el-input-number
|
|
||||||
v-model="patchUsageForm.medication_days"
|
|
||||||
:min="1"
|
|
||||||
:max="999"
|
|
||||||
:step="1"
|
|
||||||
controls-position="right"
|
|
||||||
class="flex-1 min-w-0"
|
|
||||||
/>
|
|
||||||
<span class="text-gray-500 shrink-0">天</span>
|
|
||||||
</div>
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
<template #footer>
|
|
||||||
<el-button @click="patchUsageVisible = false">取消</el-button>
|
|
||||||
<el-button type="primary" :loading="patchUsageSaving" @click="submitPatchUsage">保存</el-button>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
|
||||||
</div>
|
</div>
|
||||||
</el-drawer>
|
</el-drawer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup name="PrescriptionOrderDetailDrawer">
|
<script lang="ts" setup name="PrescriptionOrderDetailDrawer">
|
||||||
import { computed, nextTick, onMounted, reactive, ref } from 'vue'
|
import { computed, onMounted, ref } from 'vue'
|
||||||
import type { FormInstance, FormRules } from 'element-plus'
|
|
||||||
import { Refresh, Loading, Search, Van } from '@element-plus/icons-vue'
|
import { Refresh, Loading, Search, Van } from '@element-plus/icons-vue'
|
||||||
import {
|
import {
|
||||||
prescriptionOrderDetail,
|
prescriptionOrderDetail,
|
||||||
prescriptionOrderLogs,
|
prescriptionOrderLogs,
|
||||||
prescriptionOrderAddLog,
|
|
||||||
prescriptionOrderLogisticsTrace,
|
prescriptionOrderLogisticsTrace,
|
||||||
prescriptionOrderLogisticsJdUpdate,
|
prescriptionOrderLogisticsJdUpdate,
|
||||||
prescriptionOrderPaidPayOrders,
|
prescriptionOrderPaidPayOrders
|
||||||
prescriptionOrderPatchPrescriptionUsage
|
|
||||||
} from '@/api/tcm'
|
} from '@/api/tcm'
|
||||||
import { getDictData } from '@/api/app'
|
import { getDictData } from '@/api/app'
|
||||||
import feedback from '@/utils/feedback'
|
import feedback from '@/utils/feedback'
|
||||||
@@ -1025,7 +831,6 @@ import {
|
|||||||
consumerRxAuditTag,
|
consumerRxAuditTag,
|
||||||
expressCompanyLabel,
|
expressCompanyLabel,
|
||||||
logActionText,
|
logActionText,
|
||||||
auditStatusText,
|
|
||||||
formatPayOrderSource,
|
formatPayOrderSource,
|
||||||
normalizeBizPhone,
|
normalizeBizPhone,
|
||||||
recipientVsPrescriptionPhoneMismatch,
|
recipientVsPrescriptionPhoneMismatch,
|
||||||
@@ -1036,10 +841,7 @@ import {
|
|||||||
analyzeLogisticsPayloadUrgent,
|
analyzeLogisticsPayloadUrgent,
|
||||||
parseLogisticsTracePayload,
|
parseLogisticsTracePayload,
|
||||||
canUpdateAmount,
|
canUpdateAmount,
|
||||||
formatDietaryTaboo,
|
formatDietaryTaboo
|
||||||
type ServicePackageOption,
|
|
||||||
normalizeServicePackageOptions,
|
|
||||||
formatServicePackageLabels
|
|
||||||
} from './prescription-order-utils'
|
} from './prescription-order-utils'
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
@@ -1071,7 +873,6 @@ const emit = defineEmits<{
|
|||||||
(e: 'view-prescription'): void
|
(e: 'view-prescription'): void
|
||||||
(e: 'test-gancao-preview'): void
|
(e: 'test-gancao-preview'): void
|
||||||
(e: 'view-patient'): void
|
(e: 'view-patient'): void
|
||||||
(e: 'detail-changed'): void
|
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
@@ -1287,24 +1088,36 @@ const detailFullAddress = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// ─── 服务套餐字典 ───
|
// ─── 服务套餐字典 ───
|
||||||
const servicePackageOptions = ref<ServicePackageOption[]>([])
|
const servicePackageOptions = ref<Array<{ name: string; value: string }>>([])
|
||||||
|
|
||||||
async function loadServicePackageOptions() {
|
async function loadServicePackageOptions() {
|
||||||
if (servicePackageOptions.value.length > 0) return
|
|
||||||
try {
|
try {
|
||||||
const data: any = await getDictData({ type: 'server_order' })
|
const data: any = await getDictData({ type: 'server_order' })
|
||||||
const opts = normalizeServicePackageOptions(data?.server_order)
|
servicePackageOptions.value = (data?.server_order || []).filter((item: any) => item.status !== 0)
|
||||||
if (opts.length > 0) {
|
|
||||||
servicePackageOptions.value = opts
|
|
||||||
}
|
|
||||||
} catch {
|
} catch {
|
||||||
/* 请求被同参数请求取消或失败时保留现值,open() 时会重试 */
|
servicePackageOptions.value = []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const detailServicePackageText = computed(() =>
|
function formatServicePackage(value: any): string {
|
||||||
formatServicePackageLabels(detailData.value?.service_package, servicePackageOptions.value)
|
if (!value) return '—'
|
||||||
)
|
|
||||||
|
let packages: string[] = []
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
packages = value
|
||||||
|
} else if (typeof value === 'string') {
|
||||||
|
packages = value.split(',').filter((v) => v.trim() !== '')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (packages.length === 0) return '—'
|
||||||
|
|
||||||
|
const names = packages.map((val) => {
|
||||||
|
const option = servicePackageOptions.value.find((opt) => opt.value === val)
|
||||||
|
return option ? option.name : val
|
||||||
|
})
|
||||||
|
|
||||||
|
return names.join('、')
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadServicePackageOptions()
|
loadServicePackageOptions()
|
||||||
@@ -1329,196 +1142,6 @@ async function fetchLogs(id: number) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasPerm(perm: string) {
|
|
||||||
const p = userStore.perms || []
|
|
||||||
return p.includes('*') || p.includes(perm)
|
|
||||||
}
|
|
||||||
|
|
||||||
function canAddPrescriptionOrderLog() {
|
|
||||||
return hasPerm('tcm.prescriptionOrder/addLog')
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 与后端 canAuditPrescriptionOrder 同档:超管或 prescription_audit_roles */
|
|
||||||
const canSetRxAuditOnAddLog = computed(() => {
|
|
||||||
const u = userStore.userInfo
|
|
||||||
if (!u || Number(u.root) === 1) return true
|
|
||||||
const ids = Array.isArray(u.role_ids) ? u.role_ids.map((n: unknown) => Number(n)) : []
|
|
||||||
return ids.some((id) => PRESCRIPTION_AUDIT_ROLE_IDS.includes(id))
|
|
||||||
})
|
|
||||||
|
|
||||||
/** 与后端 canAuditPaymentSlipOrder 同档:超管或 prescription_order_payment_audit_roles 默认 0,3 */
|
|
||||||
const canSetPayAuditOnAddLog = computed(() => {
|
|
||||||
const u = userStore.userInfo
|
|
||||||
if (!u || Number(u.root) === 1) return true
|
|
||||||
const ids = Array.isArray(u.role_ids) ? u.role_ids.map((n: unknown) => Number(n)) : []
|
|
||||||
return ids.some((id) => [0, 3].includes(id))
|
|
||||||
})
|
|
||||||
|
|
||||||
const addLogVisible = ref(false)
|
|
||||||
const addLogSaving = ref(false)
|
|
||||||
const addLogFormRef = ref<FormInstance>()
|
|
||||||
const addLogForm = reactive({
|
|
||||||
summary: '',
|
|
||||||
prescription_audit_status: '' as number | '',
|
|
||||||
payment_slip_audit_status: '' as number | '',
|
|
||||||
prescription_audit_remark: '',
|
|
||||||
payment_slip_audit_remark: ''
|
|
||||||
})
|
|
||||||
const addLogRules: FormRules = {
|
|
||||||
summary: [{ required: true, message: '请填写日志内容', trigger: 'blur' }]
|
|
||||||
}
|
|
||||||
|
|
||||||
const patchUsageVisible = ref(false)
|
|
||||||
const patchUsageSaving = ref(false)
|
|
||||||
const patchUsageFormRef = ref<FormInstance>()
|
|
||||||
const patchUsageForm = reactive({
|
|
||||||
times_per_day: 3 as number | undefined,
|
|
||||||
usage_days: 7 as number | undefined,
|
|
||||||
aux_times_per_day: 3 as number | undefined,
|
|
||||||
aux_usage_days: 7 as number | undefined,
|
|
||||||
medication_days: undefined as number | undefined
|
|
||||||
})
|
|
||||||
const patchUsageRules = computed<FormRules>(() => {
|
|
||||||
const rules: FormRules = {
|
|
||||||
times_per_day: [{ required: true, message: '请填写主方每天次数', trigger: 'change' }],
|
|
||||||
usage_days: [{ required: true, message: '请填写主方开立天数', trigger: 'change' }],
|
|
||||||
medication_days: [{ required: true, message: '请填写订单服用天数', trigger: 'change' }]
|
|
||||||
}
|
|
||||||
if (detailHasAuxHerbs.value) {
|
|
||||||
rules.aux_times_per_day = [{ required: true, message: '请填写辅方每天次数', trigger: 'change' }]
|
|
||||||
rules.aux_usage_days = [{ required: true, message: '请填写辅方开立天数', trigger: 'change' }]
|
|
||||||
}
|
|
||||||
return rules
|
|
||||||
})
|
|
||||||
|
|
||||||
function openPatchUsageDialog() {
|
|
||||||
const rx = detailPrescription.value
|
|
||||||
const ord = detailData.value
|
|
||||||
if (!rx || !ord?.id || !ord.prescription_id) {
|
|
||||||
feedback.msgWarning('无处方数据')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const aux = detailAuxUsage.value
|
|
||||||
patchUsageForm.times_per_day =
|
|
||||||
Number(rx.times_per_day) > 0 ? Number(rx.times_per_day) : 3
|
|
||||||
patchUsageForm.usage_days =
|
|
||||||
Number(rx.usage_days) > 0 ? Number(rx.usage_days) : 7
|
|
||||||
patchUsageForm.aux_times_per_day =
|
|
||||||
aux && Number(aux.times_per_day) > 0 ? Number(aux.times_per_day) : 3
|
|
||||||
patchUsageForm.aux_usage_days =
|
|
||||||
aux && Number(aux.usage_days) > 0 ? Number(aux.usage_days) : 7
|
|
||||||
const md = Number(ord.medication_days)
|
|
||||||
patchUsageForm.medication_days = md > 0 ? md : Number(rx.usage_days) > 0 ? Number(rx.usage_days) : 7
|
|
||||||
patchUsageVisible.value = true
|
|
||||||
nextTick(() => patchUsageFormRef.value?.clearValidate())
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetPatchUsageForm() {
|
|
||||||
patchUsageForm.times_per_day = 3
|
|
||||||
patchUsageForm.usage_days = 7
|
|
||||||
patchUsageForm.aux_times_per_day = 3
|
|
||||||
patchUsageForm.aux_usage_days = 7
|
|
||||||
patchUsageForm.medication_days = undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
async function submitPatchUsage() {
|
|
||||||
const form = patchUsageFormRef.value
|
|
||||||
if (!form) return
|
|
||||||
try {
|
|
||||||
await form.validate()
|
|
||||||
} catch {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const ordId = detailData.value?.id
|
|
||||||
if (!ordId) return
|
|
||||||
patchUsageSaving.value = true
|
|
||||||
try {
|
|
||||||
const payload: {
|
|
||||||
id: number
|
|
||||||
times_per_day: number
|
|
||||||
usage_days: number
|
|
||||||
medication_days: number
|
|
||||||
aux_times_per_day?: number
|
|
||||||
aux_usage_days?: number
|
|
||||||
} = {
|
|
||||||
id: ordId,
|
|
||||||
times_per_day: Number(patchUsageForm.times_per_day),
|
|
||||||
usage_days: Number(patchUsageForm.usage_days),
|
|
||||||
medication_days: Number(patchUsageForm.medication_days)
|
|
||||||
}
|
|
||||||
if (detailHasAuxHerbs.value) {
|
|
||||||
payload.aux_times_per_day = Number(patchUsageForm.aux_times_per_day)
|
|
||||||
payload.aux_usage_days = Number(patchUsageForm.aux_usage_days)
|
|
||||||
}
|
|
||||||
await prescriptionOrderPatchPrescriptionUsage(payload)
|
|
||||||
feedback.msgSuccess('保存成功')
|
|
||||||
patchUsageVisible.value = false
|
|
||||||
await refresh()
|
|
||||||
emit('detail-changed')
|
|
||||||
} catch {
|
|
||||||
/* 拦截器已提示 */
|
|
||||||
} finally {
|
|
||||||
patchUsageSaving.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetAddLogForm() {
|
|
||||||
addLogForm.summary = ''
|
|
||||||
addLogForm.prescription_audit_status = ''
|
|
||||||
addLogForm.payment_slip_audit_status = ''
|
|
||||||
addLogForm.prescription_audit_remark = ''
|
|
||||||
addLogForm.payment_slip_audit_remark = ''
|
|
||||||
addLogFormRef.value?.clearValidate()
|
|
||||||
}
|
|
||||||
|
|
||||||
function openAddLogDialog() {
|
|
||||||
if (!detailData.value?.id) return
|
|
||||||
resetAddLogForm()
|
|
||||||
const d = detailData.value
|
|
||||||
addLogForm.prescription_audit_remark = String(d.prescription_audit_remark || '')
|
|
||||||
addLogForm.payment_slip_audit_remark = String(d.payment_slip_audit_remark || '')
|
|
||||||
addLogVisible.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
async function submitAddLog() {
|
|
||||||
if (!addLogFormRef.value || !detailData.value?.id) return
|
|
||||||
await addLogFormRef.value.validate()
|
|
||||||
addLogSaving.value = true
|
|
||||||
try {
|
|
||||||
const payload: Record<string, unknown> = {
|
|
||||||
id: detailData.value.id,
|
|
||||||
summary: addLogForm.summary.trim()
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
canSetRxAuditOnAddLog.value &&
|
|
||||||
addLogForm.prescription_audit_status !== '' &&
|
|
||||||
addLogForm.prescription_audit_status !== null &&
|
|
||||||
addLogForm.prescription_audit_status !== undefined
|
|
||||||
) {
|
|
||||||
payload.prescription_audit_status = addLogForm.prescription_audit_status
|
|
||||||
payload.prescription_audit_remark = addLogForm.prescription_audit_remark
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
canSetPayAuditOnAddLog.value &&
|
|
||||||
addLogForm.payment_slip_audit_status !== '' &&
|
|
||||||
addLogForm.payment_slip_audit_status !== null &&
|
|
||||||
addLogForm.payment_slip_audit_status !== undefined
|
|
||||||
) {
|
|
||||||
payload.payment_slip_audit_status = addLogForm.payment_slip_audit_status
|
|
||||||
payload.payment_slip_audit_remark = addLogForm.payment_slip_audit_remark
|
|
||||||
}
|
|
||||||
await prescriptionOrderAddLog(payload as any)
|
|
||||||
feedback.msgSuccess('日志已添加')
|
|
||||||
addLogVisible.value = false
|
|
||||||
await refresh()
|
|
||||||
emit('detail-changed')
|
|
||||||
} catch {
|
|
||||||
/* 拦截器已提示 */
|
|
||||||
} finally {
|
|
||||||
addLogSaving.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── 未关联支付单 ───
|
// ─── 未关联支付单 ───
|
||||||
async function loadDetailUnlinkedPayOrders(diagnosisId: number, prescriptionOrderId: number, linkedIds: number[]) {
|
async function loadDetailUnlinkedPayOrders(diagnosisId: number, prescriptionOrderId: number, linkedIds: number[]) {
|
||||||
if (!diagnosisId) {
|
if (!diagnosisId) {
|
||||||
@@ -1643,8 +1266,6 @@ async function updateJdLogistics() {
|
|||||||
|
|
||||||
// ─── 打开 / 刷新 ───
|
// ─── 打开 / 刷新 ───
|
||||||
async function open(id: number) {
|
async function open(id: number) {
|
||||||
// 页面级同参数字典请求会取消抽屉挂载时的那次(axios 去重取消),打开时兜底重试
|
|
||||||
void loadServicePackageOptions()
|
|
||||||
// 显式彻底清空缓存,防止前一次弹窗的数据残留
|
// 显式彻底清空缓存,防止前一次弹窗的数据残留
|
||||||
detailData.value = null
|
detailData.value = null
|
||||||
detailUnlinkedPayOrders.value = []
|
detailUnlinkedPayOrders.value = []
|
||||||
|
|||||||
@@ -135,25 +135,13 @@ export function logActionText(act: string) {
|
|||||||
revoke_pay_audit: '撤回支付审核',
|
revoke_pay_audit: '撤回支付审核',
|
||||||
gancao_submit: '甘草下单',
|
gancao_submit: '甘草下单',
|
||||||
patch_rx_patient: '处方患者信息',
|
patch_rx_patient: '处方患者信息',
|
||||||
patch_rx_usage: '服用参数',
|
|
||||||
update_amount: '修改订单金额',
|
update_amount: '修改订单金额',
|
||||||
complete: '完成订单',
|
complete: '完成订单',
|
||||||
refund: '退款',
|
refund: '退款'
|
||||||
manual_log: '手工备注',
|
|
||||||
assign_assistant: '改派医助',
|
|
||||||
add_pay_order: '补齐支付单',
|
|
||||||
set_ship_mode: '发货类型'
|
|
||||||
}
|
}
|
||||||
return m[act] || act
|
return m[act] || act
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 处方/支付单审核状态文案(0 待审核 / 1 已通过 / 2 已驳回) */
|
|
||||||
export function auditStatusText(s: number | undefined) {
|
|
||||||
if (s === 1) return '已通过'
|
|
||||||
if (s === 2) return '已驳回'
|
|
||||||
return '待审核'
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 支付单来源/方式:企微对外收款、付呗、快递代收等创建链路 + 支付方式回退 */
|
/** 支付单来源/方式:企微对外收款、付呗、快递代收等创建链路 + 支付方式回退 */
|
||||||
export function formatPayOrderSource(row: { payment_method?: unknown; create_type?: unknown }) {
|
export function formatPayOrderSource(row: { payment_method?: unknown; create_type?: unknown }) {
|
||||||
const createType = String(row?.create_type || '')
|
const createType = String(row?.create_type || '')
|
||||||
@@ -369,85 +357,3 @@ export function formatDietaryTaboo(raw: unknown): string {
|
|||||||
}
|
}
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 服务套餐 dict:server_order */
|
|
||||||
export type ServicePackageOption = { name: string; value: string; status?: number }
|
|
||||||
|
|
||||||
export function normalizeServicePackageValue(v: unknown): string {
|
|
||||||
return String(v ?? '').trim()
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 解析订单 service_package(逗号串 / 数组 / 单值数字) */
|
|
||||||
export function parseServicePackageValues(raw: unknown): string[] {
|
|
||||||
if (raw == null || raw === '') return []
|
|
||||||
if (Array.isArray(raw)) {
|
|
||||||
return raw.map(normalizeServicePackageValue).filter(Boolean)
|
|
||||||
}
|
|
||||||
if (typeof raw === 'string') {
|
|
||||||
return raw.split(',').map((v) => v.trim()).filter(Boolean)
|
|
||||||
}
|
|
||||||
const one = normalizeServicePackageValue(raw)
|
|
||||||
return one ? [one] : []
|
|
||||||
}
|
|
||||||
|
|
||||||
export function servicePackageValueEquals(a: unknown, b: unknown): boolean {
|
|
||||||
const sa = normalizeServicePackageValue(a)
|
|
||||||
const sb = normalizeServicePackageValue(b)
|
|
||||||
if (!sa || !sb) return false
|
|
||||||
if (sa === sb) return true
|
|
||||||
const na = Number(sa)
|
|
||||||
const nb = Number(sb)
|
|
||||||
return Number.isFinite(na) && Number.isFinite(nb) && na === nb
|
|
||||||
}
|
|
||||||
|
|
||||||
export function normalizeServicePackageOptions(raw: unknown): ServicePackageOption[] {
|
|
||||||
if (!Array.isArray(raw)) return []
|
|
||||||
return raw
|
|
||||||
.map((item: any) => ({
|
|
||||||
name: String(item?.name ?? '').trim() || normalizeServicePackageValue(item?.value),
|
|
||||||
value: normalizeServicePackageValue(item?.value),
|
|
||||||
status: Number(item?.status ?? 1)
|
|
||||||
}))
|
|
||||||
.filter((item) => item.value !== '')
|
|
||||||
}
|
|
||||||
|
|
||||||
export function findServicePackageOption(
|
|
||||||
options: ServicePackageOption[],
|
|
||||||
value: unknown
|
|
||||||
): ServicePackageOption | undefined {
|
|
||||||
const key = normalizeServicePackageValue(value)
|
|
||||||
if (!key) return undefined
|
|
||||||
return options.find((opt) => servicePackageValueEquals(opt.value, key))
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 展示用:value → 字典 name,多选用「、」连接 */
|
|
||||||
export function formatServicePackageLabels(
|
|
||||||
value: unknown,
|
|
||||||
options: ServicePackageOption[],
|
|
||||||
emptyText = '—'
|
|
||||||
): string {
|
|
||||||
const packages = parseServicePackageValues(value)
|
|
||||||
if (packages.length === 0) return emptyText
|
|
||||||
const names = packages.map((val) => {
|
|
||||||
const option = findServicePackageOption(options, val)
|
|
||||||
return option?.name || val
|
|
||||||
})
|
|
||||||
return names.join('、')
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 编辑下拉:字典项 + 当前已选但字典缺失的兜底项 */
|
|
||||||
export function mergeServicePackageSelectOptions(
|
|
||||||
options: ServicePackageOption[],
|
|
||||||
selected: unknown[]
|
|
||||||
): ServicePackageOption[] {
|
|
||||||
const known = new Set(options.map((o) => o.value))
|
|
||||||
const extras: ServicePackageOption[] = []
|
|
||||||
for (const raw of selected) {
|
|
||||||
const val = normalizeServicePackageValue(raw)
|
|
||||||
if (!val || known.has(val)) continue
|
|
||||||
const matched = findServicePackageOption(options, val)
|
|
||||||
extras.push(matched ?? { name: val, value: val, status: 0 })
|
|
||||||
known.add(val)
|
|
||||||
}
|
|
||||||
return extras.length ? [...options, ...extras] : options
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -298,7 +298,7 @@
|
|||||||
:fetch-fun="prescriptionOrderExport"
|
:fetch-fun="prescriptionOrderExport"
|
||||||
:params="prescriptionOrderExportParams"
|
:params="prescriptionOrderExportParams"
|
||||||
:page-size="pager.size"
|
:page-size="pager.size"
|
||||||
export-hint="导出范围与上方筛选一致(履约状态、创建时间及其他条件均会生效)。含「自媒体渠道(挂号渠道来源)」:优先取该单关联处方登记的挂号;无则诊单下同患者挂号取 id 最大的一条(与前台挂号选择的记录一致);业绩侧栏带渠道筛选导出时与同页列表高亮挂号同源。「服务套餐」按字典 server_order 解析展示。「处方」导出主方/辅方药材明细;「主方/辅方服用方式、天数」与详情侧栏同口径(主方/辅方天数分别取处方 usage_days、辅方 aux_usage.usage_days;「天数」列为订单 medication_days)。「关联收款记录」与详情侧栏同源(已支付/已退款/待审核),每笔两行展示(摘要行+明细行),多笔空行分隔,单元格自动换行。「签收日期」与详情/业绩看板同源,仅读物流库(轨迹/签收时间):导出不再实时查快递100,速度只取决于数据库;签收时间由 `tcm:backfill-sign-time` 命令与物流自动更新定时任务落库,刚发货尚未同步的单子会暂时为空,待下次回填/定时任务刷新后显示。"
|
export-hint="导出范围与上方筛选一致(履约状态、创建时间及其他条件均会生效)。含「自媒体渠道(挂号渠道来源)」:优先取该单关联处方登记的挂号;无则诊单下同患者挂号取 id 最大的一条(与前台挂号选择的记录一致);业绩侧栏带渠道筛选导出时与同页列表高亮挂号同源。「服务套餐」按字典 server_order 解析展示。「关联收款记录」与详情侧栏同源(已支付/已退款/待审核),每笔两行展示(摘要行+明细行),多笔空行分隔,单元格自动换行。「签收日期」与详情/业绩看板同源,仅读物流库(轨迹/签收时间):导出不再实时查快递100,速度只取决于数据库;签收时间由 `tcm:backfill-sign-time` 命令与物流自动更新定时任务落库,刚发货尚未同步的单子会暂时为空,待下次回填/定时任务刷新后显示。"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
@@ -623,7 +623,6 @@
|
|||||||
@view-prescription="detailData && openPrescriptionView(detailData)"
|
@view-prescription="detailData && openPrescriptionView(detailData)"
|
||||||
@test-gancao-preview="testGancaoPreviewFromDetail"
|
@test-gancao-preview="testGancaoPreviewFromDetail"
|
||||||
@view-patient="openDiagnosisPatientDetailFromOrder"
|
@view-patient="openDiagnosisPatientDetailFromOrder"
|
||||||
@detail-changed="getLists"
|
|
||||||
>
|
>
|
||||||
<template #header-extra="{ detail }">
|
<template #header-extra="{ detail }">
|
||||||
<div class="flex items-center gap-2 ml-4 shrink-0">
|
<div class="flex items-center gap-2 ml-4 shrink-0">
|
||||||
@@ -1025,11 +1024,10 @@
|
|||||||
class="w-full"
|
class="w-full"
|
||||||
>
|
>
|
||||||
<el-option
|
<el-option
|
||||||
v-for="item in editServicePackageSelectOptions"
|
v-for="item in servicePackageOptions"
|
||||||
:key="item.value"
|
:key="item.value"
|
||||||
:label="item.name"
|
:label="item.name"
|
||||||
:value="item.value"
|
:value="item.value"
|
||||||
:disabled="item.status === 0"
|
|
||||||
/>
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@@ -2227,11 +2225,7 @@ import {
|
|||||||
canUpdateAmount,
|
canUpdateAmount,
|
||||||
formatDietaryTaboo,
|
formatDietaryTaboo,
|
||||||
type SlipFormulaType,
|
type SlipFormulaType,
|
||||||
type SlipAuxUsageForm,
|
type SlipAuxUsageForm
|
||||||
type ServicePackageOption,
|
|
||||||
normalizeServicePackageOptions,
|
|
||||||
parseServicePackageValues,
|
|
||||||
mergeServicePackageSelectOptions
|
|
||||||
} from './components/prescription-order-utils'
|
} from './components/prescription-order-utils'
|
||||||
import { useListTimeFilter } from '@/hooks/useListTimeFilter'
|
import { useListTimeFilter } from '@/hooks/useListTimeFilter'
|
||||||
import {
|
import {
|
||||||
@@ -2420,8 +2414,8 @@ async function submitReassign() {
|
|||||||
// 省市区数据
|
// 省市区数据
|
||||||
const regionOptions = ref([])
|
const regionOptions = ref([])
|
||||||
|
|
||||||
// 服务套餐选项(含已停用项,便于编辑时回显历史值)
|
// 服务套餐选项
|
||||||
const servicePackageOptions = ref<ServicePackageOption[]>([])
|
const servicePackageOptions = ref<Array<{ name: string; value: string }>>([])
|
||||||
|
|
||||||
/** 筛选:开方医生、诊单医助(与列表接口 doctor_id / assistant_id 一致) */
|
/** 筛选:开方医生、诊单医助(与列表接口 doctor_id / assistant_id 一致) */
|
||||||
const doctorOptions = ref<Array<{ id: number; name: string }>>([])
|
const doctorOptions = ref<Array<{ id: number; name: string }>>([])
|
||||||
@@ -2542,7 +2536,8 @@ const loadRegionData = async () => {
|
|||||||
const loadServicePackageOptions = async () => {
|
const loadServicePackageOptions = async () => {
|
||||||
try {
|
try {
|
||||||
const data = await getDictData({ type: 'server_order' })
|
const data = await getDictData({ type: 'server_order' })
|
||||||
servicePackageOptions.value = normalizeServicePackageOptions(data?.server_order)
|
servicePackageOptions.value = (data?.server_order || []).filter((item: any) => item.status !== 0)
|
||||||
|
console.log('服务套餐选项已加载:', servicePackageOptions.value.length)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载服务套餐选项失败:', error)
|
console.error('加载服务套餐选项失败:', error)
|
||||||
servicePackageOptions.value = []
|
servicePackageOptions.value = []
|
||||||
@@ -3200,17 +3195,10 @@ async function onDetailShipModeChange(mode: string | number | boolean | undefine
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function canAddPayOrderRow(row: {
|
function canAddPayOrderRow(row: { fulfillment_status?: number }) {
|
||||||
fulfillment_status?: number
|
// 已发货(5) / 已签收(6) 状态可补齐支付单
|
||||||
amount?: number | string
|
|
||||||
linked_pay_paid_total?: number | string
|
|
||||||
}) {
|
|
||||||
// 已发货(5) / 已签收(6) 状态可补齐支付单;总金额已付清则不允许
|
|
||||||
const fs = Number(row.fulfillment_status)
|
const fs = Number(row.fulfillment_status)
|
||||||
if (fs !== 5 && fs !== 6) return false
|
return fs === 5 || fs === 6
|
||||||
const orderAmount = Math.round((Number(row.amount) || 0) * 100) / 100
|
|
||||||
const paidTotal = Math.round((Number(row.linked_pay_paid_total) || 0) * 100) / 100
|
|
||||||
return paidTotal < orderAmount
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function canCompleteRow(row: { fulfillment_status?: number; payment_slip_audit_status?: number }) {
|
function canCompleteRow(row: { fulfillment_status?: number; payment_slip_audit_status?: number }) {
|
||||||
@@ -3461,11 +3449,6 @@ const editForm = reactive({
|
|||||||
diagnosis_creator_dept_path: ''
|
diagnosis_creator_dept_path: ''
|
||||||
})
|
})
|
||||||
|
|
||||||
/** 编辑弹窗下拉:字典项 + 当前已选但字典中缺失的兜底项 */
|
|
||||||
const editServicePackageSelectOptions = computed(() =>
|
|
||||||
mergeServicePackageSelectOptions(servicePackageOptions.value, editForm.service_package)
|
|
||||||
)
|
|
||||||
|
|
||||||
/** 诊单创建人部门:拆成面包屑。多部门为「;」分隔;路径内为「 / 」含父级(与后端 buildDeptPath 一致) */
|
/** 诊单创建人部门:拆成面包屑。多部门为「;」分隔;路径内为「 / 」含父级(与后端 buildDeptPath 一致) */
|
||||||
const editDiagnosisCreatorDeptBreadcrumbs = computed(() => {
|
const editDiagnosisCreatorDeptBreadcrumbs = computed(() => {
|
||||||
const raw = String(editForm.diagnosis_creator_dept_path || '').trim()
|
const raw = String(editForm.diagnosis_creator_dept_path || '').trim()
|
||||||
@@ -3698,7 +3681,18 @@ async function openEdit(row: {
|
|||||||
editForm.dose_unit = d.dose_unit || '剂'
|
editForm.dose_unit = d.dose_unit || '剂'
|
||||||
editForm.prev_staff = d.prev_staff || ''
|
editForm.prev_staff = d.prev_staff || ''
|
||||||
editForm.service_channel = d.service_channel || ''
|
editForm.service_channel = d.service_channel || ''
|
||||||
editForm.service_package = parseServicePackageValues(d.service_package)
|
// 处理服务套餐:如果是字符串,转换为数组
|
||||||
|
if (d.service_package) {
|
||||||
|
if (Array.isArray(d.service_package)) {
|
||||||
|
editForm.service_package = d.service_package
|
||||||
|
} else if (typeof d.service_package === 'string') {
|
||||||
|
editForm.service_package = d.service_package.split(',').filter(v => v.trim() !== '')
|
||||||
|
} else {
|
||||||
|
editForm.service_package = []
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
editForm.service_package = []
|
||||||
|
}
|
||||||
editForm.express_company = String(d.express_company || 'auto') || 'auto'
|
editForm.express_company = String(d.express_company || 'auto') || 'auto'
|
||||||
editForm.tracking_number = d.tracking_number || ''
|
editForm.tracking_number = d.tracking_number || ''
|
||||||
editForm.fee_type = Number(d.fee_type) || 3
|
editForm.fee_type = Number(d.fee_type) || 3
|
||||||
@@ -4515,18 +4509,7 @@ async function loadAddPayOrderAvailable(diagnosisId: number, currentLinkedIds: n
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function openAddPayOrder(row: {
|
function openAddPayOrder(row: { id: number; diagnosis_id?: number; pay_order_ids?: number[] }) {
|
||||||
id: number
|
|
||||||
diagnosis_id?: number
|
|
||||||
pay_order_ids?: number[]
|
|
||||||
fulfillment_status?: number
|
|
||||||
amount?: number | string
|
|
||||||
linked_pay_paid_total?: number | string
|
|
||||||
}) {
|
|
||||||
if (!canAddPayOrderRow(row)) {
|
|
||||||
feedback.msgWarning('订单总金额与已付金额一致,无需补齐支付单')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
addPayOrderRowId.value = row.id
|
addPayOrderRowId.value = row.id
|
||||||
addPayOrderForm.add_mode = 'create'
|
addPayOrderForm.add_mode = 'create'
|
||||||
addPayOrderForm.order_type = 3
|
addPayOrderForm.order_type = 3
|
||||||
|
|||||||
@@ -858,94 +858,31 @@
|
|||||||
</el-tag>
|
</el-tag>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="用量">
|
<el-descriptions-item label="用量">
|
||||||
<div class="flex flex-col gap-1 text-sm leading-relaxed">
|
<template v-if="detailPrescription.dosage_amount">
|
||||||
<div>
|
{{ detailPrescription.dosage_amount }}{{ detailPrescription.dosage_unit || 'g' }}
|
||||||
<span v-if="detailHasAuxHerbs" class="text-gray-500 mr-1">主方:</span>
|
<template v-if="detailPrescription.prescription_type === '浓缩水丸'">
|
||||||
<template v-if="detailPrescription.dosage_amount">
|
· {{ Number(detailPrescription.dosage_bag_count) > 0 ? Number(detailPrescription.dosage_bag_count) : 1 }}袋
|
||||||
{{ detailPrescription.dosage_amount }}{{ detailPrescription.dosage_unit || 'g' }}
|
</template>
|
||||||
<template v-if="detailPrescription.prescription_type === '浓缩水丸'">
|
<span v-if="detailPrescription.prescription_type === '饮片' && detailPrescription.need_decoction !== null" class="ml-2 text-gray-500">
|
||||||
· {{ Number(detailPrescription.dosage_bag_count) > 0 ? Number(detailPrescription.dosage_bag_count) : 1 }}袋
|
({{ detailPrescription.need_decoction ? '代煎' : '不代煎' }})
|
||||||
</template>
|
</span>
|
||||||
<span v-if="detailPrescription.prescription_type === '饮片' && detailPrescription.need_decoction !== null" class="ml-2 text-gray-500">
|
|
||||||
({{ detailPrescription.need_decoction ? '代煎' : '不代煎' }})
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
<template v-else>—</template>
|
|
||||||
</div>
|
|
||||||
<div v-if="detailHasAuxHerbs && detailAuxUsage">
|
|
||||||
<span class="text-gray-500 mr-1">辅方:</span>
|
|
||||||
<template v-if="detailAuxUsage.dosage_amount != null && detailAuxUsage.dosage_amount !== 0">
|
|
||||||
{{ detailAuxUsage.dosage_amount }}{{ detailPrescription.dosage_unit || 'g' }}
|
|
||||||
<template v-if="detailPrescription.prescription_type === '浓缩水丸'">
|
|
||||||
· {{ Number(detailAuxUsage.dosage_bag_count) > 0 ? Number(detailAuxUsage.dosage_bag_count) : 1 }}袋
|
|
||||||
</template>
|
|
||||||
<span v-if="detailPrescription.prescription_type === '饮片'" class="ml-2 text-gray-500">
|
|
||||||
({{ detailAuxUsage.need_decoction ? '代煎' : '不代煎' }})
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
<template v-else>—</template>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</el-descriptions-item>
|
|
||||||
<el-descriptions-item>
|
|
||||||
<template #label>
|
|
||||||
<div class="flex items-center gap-1 flex-wrap">
|
|
||||||
<span>服用方式</span>
|
|
||||||
<el-button
|
|
||||||
v-if="
|
|
||||||
detailData.prescription_id &&
|
|
||||||
detailPrescription &&
|
|
||||||
!String(detailData.prescription_detail_error || '').trim()
|
|
||||||
"
|
|
||||||
v-perms="['tcm.prescriptionOrder/patchPrescriptionUsage']"
|
|
||||||
type="primary"
|
|
||||||
size="small"
|
|
||||||
link
|
|
||||||
@click="openPatchUsageDialog"
|
|
||||||
>
|
|
||||||
修改
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
<template v-else>—</template>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="服用方式">
|
||||||
<div class="flex flex-col gap-1.5 text-sm leading-relaxed">
|
<div class="flex flex-col gap-1.5 text-sm leading-relaxed">
|
||||||
<template v-if="detailHasAuxHerbs">
|
<div>
|
||||||
<div>
|
<span class="text-gray-500">每天次数:</span>
|
||||||
<span class="text-gray-500 mr-1">主方:</span>
|
{{ detailPrescription.times_per_day ? detailPrescription.times_per_day + ' 次' : '—' }}
|
||||||
每天
|
</div>
|
||||||
{{ detailPrescription.times_per_day ? detailPrescription.times_per_day + ' 次' : '— 次' }}
|
<div>
|
||||||
· 处方开立
|
<span class="text-gray-500">处方开立:</span>
|
||||||
{{
|
{{
|
||||||
detailPrescription.usage_days != null && detailPrescription.usage_days !== ''
|
detailPrescription.usage_days != null && detailPrescription.usage_days !== ''
|
||||||
? detailPrescription.usage_days + ' 天'
|
? detailPrescription.usage_days + ' 天'
|
||||||
: '— 天'
|
: '—'
|
||||||
}}
|
}}
|
||||||
</div>
|
</div>
|
||||||
<div v-if="detailAuxUsage">
|
|
||||||
<span class="text-gray-500 mr-1">辅方:</span>
|
|
||||||
每天
|
|
||||||
{{ detailAuxUsage.times_per_day ? detailAuxUsage.times_per_day + ' 次' : '— 次' }}
|
|
||||||
· 处方开立
|
|
||||||
{{
|
|
||||||
detailAuxUsage.usage_days != null && Number(detailAuxUsage.usage_days) > 0
|
|
||||||
? detailAuxUsage.usage_days + ' 天'
|
|
||||||
: '— 天'
|
|
||||||
}}
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<div>
|
|
||||||
<span class="text-gray-500">每天次数:</span>
|
|
||||||
{{ detailPrescription.times_per_day ? detailPrescription.times_per_day + ' 次' : '—' }}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span class="text-gray-500">处方开立:</span>
|
|
||||||
{{
|
|
||||||
detailPrescription.usage_days != null && detailPrescription.usage_days !== ''
|
|
||||||
? detailPrescription.usage_days + ' 天'
|
|
||||||
: '—'
|
|
||||||
}}
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<div>
|
<div>
|
||||||
<span class="text-gray-500">订单设置:</span>
|
<span class="text-gray-500">订单设置:</span>
|
||||||
{{
|
{{
|
||||||
@@ -1202,7 +1139,7 @@
|
|||||||
<el-descriptions-item label="上次医护">{{ detailData.prev_staff || '—' }}</el-descriptions-item>
|
<el-descriptions-item label="上次医护">{{ detailData.prev_staff || '—' }}</el-descriptions-item>
|
||||||
|
|
||||||
<el-descriptions-item label="服务渠道">{{ detailData.service_channel || '—' }}</el-descriptions-item>
|
<el-descriptions-item label="服务渠道">{{ detailData.service_channel || '—' }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="服务套餐">{{ detailServicePackageText }}</el-descriptions-item>
|
<el-descriptions-item label="服务套餐">{{ formatServicePackage(detailData.service_package) }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="费用类别">{{ feeTypeText(detailData.fee_type) }}</el-descriptions-item>
|
<el-descriptions-item label="费用类别">{{ feeTypeText(detailData.fee_type) }}</el-descriptions-item>
|
||||||
|
|
||||||
<el-descriptions-item v-if="detailData.internal_cost != null && detailData.internal_cost !== ''" label="内部成本">
|
<el-descriptions-item v-if="detailData.internal_cost != null && detailData.internal_cost !== ''" label="内部成本">
|
||||||
@@ -1588,11 +1525,10 @@
|
|||||||
class="w-full"
|
class="w-full"
|
||||||
>
|
>
|
||||||
<el-option
|
<el-option
|
||||||
v-for="item in editServicePackageSelectOptions"
|
v-for="item in servicePackageOptions"
|
||||||
:key="item.value"
|
:key="item.value"
|
||||||
:label="item.name"
|
:label="item.name"
|
||||||
:value="item.value"
|
:value="item.value"
|
||||||
:disabled="item.status === 0"
|
|
||||||
/>
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@@ -2215,93 +2151,6 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
<!-- 修改服用参数:主方 / 辅方 / 订单设置 -->
|
|
||||||
<el-dialog
|
|
||||||
v-model="patchUsageVisible"
|
|
||||||
title="修改服用参数"
|
|
||||||
width="92%"
|
|
||||||
:close-on-click-modal="false"
|
|
||||||
destroy-on-close
|
|
||||||
class="po-h5-dialog"
|
|
||||||
@closed="resetPatchUsageForm"
|
|
||||||
>
|
|
||||||
<el-form
|
|
||||||
ref="patchUsageFormRef"
|
|
||||||
:model="patchUsageForm"
|
|
||||||
:rules="patchUsageRules"
|
|
||||||
label-width="96px"
|
|
||||||
>
|
|
||||||
<div v-if="detailHasAuxHerbs" class="text-xs font-medium text-gray-500 mb-3">主方</div>
|
|
||||||
<el-form-item label="每天次数" prop="times_per_day">
|
|
||||||
<el-input-number
|
|
||||||
v-model="patchUsageForm.times_per_day"
|
|
||||||
:min="1"
|
|
||||||
:max="6"
|
|
||||||
:step="1"
|
|
||||||
controls-position="right"
|
|
||||||
class="w-full"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="处方开立" prop="usage_days">
|
|
||||||
<div class="flex items-center gap-1 w-full">
|
|
||||||
<el-input-number
|
|
||||||
v-model="patchUsageForm.usage_days"
|
|
||||||
:min="1"
|
|
||||||
:max="999"
|
|
||||||
:step="1"
|
|
||||||
controls-position="right"
|
|
||||||
class="flex-1 min-w-0"
|
|
||||||
/>
|
|
||||||
<span class="text-gray-500 shrink-0">天</span>
|
|
||||||
</div>
|
|
||||||
</el-form-item>
|
|
||||||
<template v-if="detailHasAuxHerbs">
|
|
||||||
<div class="text-xs font-medium text-gray-500 mb-3 mt-2 pt-2 border-t border-gray-100">辅方</div>
|
|
||||||
<el-form-item label="每天次数" prop="aux_times_per_day">
|
|
||||||
<el-input-number
|
|
||||||
v-model="patchUsageForm.aux_times_per_day"
|
|
||||||
:min="1"
|
|
||||||
:max="6"
|
|
||||||
:step="1"
|
|
||||||
controls-position="right"
|
|
||||||
class="w-full"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="处方开立" prop="aux_usage_days">
|
|
||||||
<div class="flex items-center gap-1 w-full">
|
|
||||||
<el-input-number
|
|
||||||
v-model="patchUsageForm.aux_usage_days"
|
|
||||||
:min="1"
|
|
||||||
:max="999"
|
|
||||||
:step="1"
|
|
||||||
controls-position="right"
|
|
||||||
class="flex-1 min-w-0"
|
|
||||||
/>
|
|
||||||
<span class="text-gray-500 shrink-0">天</span>
|
|
||||||
</div>
|
|
||||||
</el-form-item>
|
|
||||||
</template>
|
|
||||||
<div class="text-xs font-medium text-gray-500 mb-3 mt-2 pt-2 border-t border-gray-100">订单设置</div>
|
|
||||||
<el-form-item label="服用天数" prop="medication_days">
|
|
||||||
<div class="flex items-center gap-1 w-full">
|
|
||||||
<el-input-number
|
|
||||||
v-model="patchUsageForm.medication_days"
|
|
||||||
:min="1"
|
|
||||||
:max="999"
|
|
||||||
:step="1"
|
|
||||||
controls-position="right"
|
|
||||||
class="flex-1 min-w-0"
|
|
||||||
/>
|
|
||||||
<span class="text-gray-500 shrink-0">天</span>
|
|
||||||
</div>
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
<template #footer>
|
|
||||||
<el-button @click="patchUsageVisible = false">取消</el-button>
|
|
||||||
<el-button type="primary" :loading="patchUsageSaving" @click="submitPatchUsage">保存</el-button>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
|
||||||
|
|
||||||
<!-- 处方详情查看(处方单样式) -->
|
<!-- 处方详情查看(处方单样式) -->
|
||||||
<el-drawer
|
<el-drawer
|
||||||
v-model="prescriptionViewVisible"
|
v-model="prescriptionViewVisible"
|
||||||
@@ -2716,7 +2565,6 @@ import {
|
|||||||
prescriptionOrderRevokeRxAudit,
|
prescriptionOrderRevokeRxAudit,
|
||||||
prescriptionOrderRevokePayAudit,
|
prescriptionOrderRevokePayAudit,
|
||||||
prescriptionOrderPatchPrescriptionPatient,
|
prescriptionOrderPatchPrescriptionPatient,
|
||||||
prescriptionOrderPatchPrescriptionUsage,
|
|
||||||
prescriptionOrderLinkPayOrder,
|
prescriptionOrderLinkPayOrder,
|
||||||
prescriptionOrderRequestCompletion,
|
prescriptionOrderRequestCompletion,
|
||||||
prescriptionOrderSubmitGancaoRecipel,
|
prescriptionOrderSubmitGancaoRecipel,
|
||||||
@@ -2725,16 +2573,7 @@ import {
|
|||||||
getDoctors,
|
getDoctors,
|
||||||
getAssistants
|
getAssistants
|
||||||
} from '@/api/tcm'
|
} from '@/api/tcm'
|
||||||
import {
|
import { formatDietaryTaboo } from './components/prescription-order-utils'
|
||||||
formatDietaryTaboo,
|
|
||||||
type ServicePackageOption,
|
|
||||||
normalizeServicePackageOptions,
|
|
||||||
parseServicePackageValues,
|
|
||||||
mergeServicePackageSelectOptions,
|
|
||||||
formatServicePackageLabels,
|
|
||||||
normalizeSlipAuxUsageForm,
|
|
||||||
prescriptionHasAuxFormula
|
|
||||||
} from './components/prescription-order-utils'
|
|
||||||
import html2canvas from 'html2canvas'
|
import html2canvas from 'html2canvas'
|
||||||
import { jsPDF } from 'jspdf'
|
import { jsPDF } from 'jspdf'
|
||||||
import { getDictData } from '@/api/app'
|
import { getDictData } from '@/api/app'
|
||||||
@@ -2830,7 +2669,7 @@ const canViewFinanceFields = () => {
|
|||||||
const regionOptions = ref([])
|
const regionOptions = ref([])
|
||||||
|
|
||||||
// 服务套餐选项
|
// 服务套餐选项
|
||||||
const servicePackageOptions = ref<ServicePackageOption[]>([])
|
const servicePackageOptions = ref<Array<{ name: string; value: string }>>([])
|
||||||
|
|
||||||
/** 筛选:开方医生、诊单医助(与列表接口 doctor_id / assistant_id 一致) */
|
/** 筛选:开方医生、诊单医助(与列表接口 doctor_id / assistant_id 一致) */
|
||||||
const doctorOptions = ref<Array<{ id: number; name: string }>>([])
|
const doctorOptions = ref<Array<{ id: number; name: string }>>([])
|
||||||
@@ -2951,7 +2790,8 @@ const loadRegionData = async () => {
|
|||||||
const loadServicePackageOptions = async () => {
|
const loadServicePackageOptions = async () => {
|
||||||
try {
|
try {
|
||||||
const data = await getDictData({ type: 'server_order' })
|
const data = await getDictData({ type: 'server_order' })
|
||||||
servicePackageOptions.value = normalizeServicePackageOptions(data?.server_order)
|
servicePackageOptions.value = (data?.server_order || []).filter((item: any) => item.status !== 0)
|
||||||
|
console.log('服务套餐选项已加载:', servicePackageOptions.value.length)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载服务套餐选项失败:', error)
|
console.error('加载服务套餐选项失败:', error)
|
||||||
servicePackageOptions.value = []
|
servicePackageOptions.value = []
|
||||||
@@ -3359,6 +3199,28 @@ function feeTypeText(t: number | undefined) {
|
|||||||
return m[Number(t)] ?? '—'
|
return m[Number(t)] ?? '—'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 格式化服务套餐显示
|
||||||
|
function formatServicePackage(value: any): string {
|
||||||
|
if (!value) return '—'
|
||||||
|
|
||||||
|
let packages: string[] = []
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
packages = value
|
||||||
|
} else if (typeof value === 'string') {
|
||||||
|
packages = value.split(',').filter(v => v.trim() !== '')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (packages.length === 0) return '—'
|
||||||
|
|
||||||
|
// 将值转换为名称
|
||||||
|
const names = packages.map(val => {
|
||||||
|
const option = servicePackageOptions.value.find(opt => opt.value === val)
|
||||||
|
return option ? option.name : val
|
||||||
|
})
|
||||||
|
|
||||||
|
return names.join('、')
|
||||||
|
}
|
||||||
|
|
||||||
function auditStatusText(s: number | undefined) {
|
function auditStatusText(s: number | undefined) {
|
||||||
if (s === 1) return '已通过'
|
if (s === 1) return '已通过'
|
||||||
if (s === 2) return '已驳回'
|
if (s === 2) return '已驳回'
|
||||||
@@ -3611,10 +3473,6 @@ const detailVisible = ref(false)
|
|||||||
const detailLoading = ref(false)
|
const detailLoading = ref(false)
|
||||||
const detailData = ref<Record<string, any> | null>(null)
|
const detailData = ref<Record<string, any> | null>(null)
|
||||||
|
|
||||||
const detailServicePackageText = computed(() =>
|
|
||||||
formatServicePackageLabels(detailData.value?.service_package, servicePackageOptions.value)
|
|
||||||
)
|
|
||||||
|
|
||||||
// --- 订单可视化审批履约流程逻辑 ---
|
// --- 订单可视化审批履约流程逻辑 ---
|
||||||
const workflowActiveStep = computed(() => {
|
const workflowActiveStep = computed(() => {
|
||||||
if (!detailData.value) return 0
|
if (!detailData.value) return 0
|
||||||
@@ -3771,16 +3629,6 @@ const detailLinkedAppointmentResolvedFromTag = computed(() => {
|
|||||||
|
|
||||||
const detailRxHerbs = computed(() => normalizeSlipHerbs(detailPrescription.value?.herbs))
|
const detailRxHerbs = computed(() => normalizeSlipHerbs(detailPrescription.value?.herbs))
|
||||||
|
|
||||||
const detailHasAuxHerbs = computed(() => prescriptionHasAuxFormula(detailPrescription.value as any))
|
|
||||||
|
|
||||||
const detailAuxUsage = computed(() => {
|
|
||||||
const rx = detailPrescription.value as any
|
|
||||||
if (!rx || !prescriptionHasAuxFormula(rx)) return null
|
|
||||||
const raw = rx.aux_usage
|
|
||||||
if (raw == null || raw === '' || (Array.isArray(raw) && raw.length === 0)) return null
|
|
||||||
return normalizeSlipAuxUsageForm(raw, rx.prescription_type || '浓缩水丸')
|
|
||||||
})
|
|
||||||
|
|
||||||
/** false=无权限;true/缺省兼容旧接口(旧版未下发该字段时仍展示药材) */
|
/** false=无权限;true/缺省兼容旧接口(旧版未下发该字段时仍展示药材) */
|
||||||
const detailHerbsVisible = computed(() => detailData.value?.prescription_detail_herbs_visible !== false)
|
const detailHerbsVisible = computed(() => detailData.value?.prescription_detail_herbs_visible !== false)
|
||||||
|
|
||||||
@@ -4007,101 +3855,6 @@ const patchRxPatientRules: FormRules = {
|
|||||||
phone: [{ required: true, message: '请输入手机号', trigger: 'blur' }]
|
phone: [{ required: true, message: '请输入手机号', trigger: 'blur' }]
|
||||||
}
|
}
|
||||||
|
|
||||||
const patchUsageVisible = ref(false)
|
|
||||||
const patchUsageSaving = ref(false)
|
|
||||||
const patchUsageFormRef = ref<FormInstance>()
|
|
||||||
const patchUsageForm = reactive({
|
|
||||||
times_per_day: 3 as number | undefined,
|
|
||||||
usage_days: 7 as number | undefined,
|
|
||||||
aux_times_per_day: 3 as number | undefined,
|
|
||||||
aux_usage_days: 7 as number | undefined,
|
|
||||||
medication_days: undefined as number | undefined
|
|
||||||
})
|
|
||||||
const patchUsageRules = computed<FormRules>(() => {
|
|
||||||
const rules: FormRules = {
|
|
||||||
times_per_day: [{ required: true, message: '请填写主方每天次数', trigger: 'change' }],
|
|
||||||
usage_days: [{ required: true, message: '请填写主方开立天数', trigger: 'change' }],
|
|
||||||
medication_days: [{ required: true, message: '请填写订单服用天数', trigger: 'change' }]
|
|
||||||
}
|
|
||||||
if (detailHasAuxHerbs.value) {
|
|
||||||
rules.aux_times_per_day = [{ required: true, message: '请填写辅方每天次数', trigger: 'change' }]
|
|
||||||
rules.aux_usage_days = [{ required: true, message: '请填写辅方开立天数', trigger: 'change' }]
|
|
||||||
}
|
|
||||||
return rules
|
|
||||||
})
|
|
||||||
|
|
||||||
function openPatchUsageDialog() {
|
|
||||||
const rx = detailPrescription.value
|
|
||||||
const ord = detailData.value
|
|
||||||
if (!rx || !ord?.id || !ord.prescription_id) {
|
|
||||||
feedback.msgWarning('无处方数据')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const aux = detailAuxUsage.value
|
|
||||||
patchUsageForm.times_per_day =
|
|
||||||
Number(rx.times_per_day) > 0 ? Number(rx.times_per_day) : 3
|
|
||||||
patchUsageForm.usage_days =
|
|
||||||
Number(rx.usage_days) > 0 ? Number(rx.usage_days) : 7
|
|
||||||
patchUsageForm.aux_times_per_day =
|
|
||||||
aux && Number(aux.times_per_day) > 0 ? Number(aux.times_per_day) : 3
|
|
||||||
patchUsageForm.aux_usage_days =
|
|
||||||
aux && Number(aux.usage_days) > 0 ? Number(aux.usage_days) : 7
|
|
||||||
const md = Number(ord.medication_days)
|
|
||||||
patchUsageForm.medication_days = md > 0 ? md : Number(rx.usage_days) > 0 ? Number(rx.usage_days) : 7
|
|
||||||
patchUsageVisible.value = true
|
|
||||||
nextTick(() => patchUsageFormRef.value?.clearValidate())
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetPatchUsageForm() {
|
|
||||||
patchUsageForm.times_per_day = 3
|
|
||||||
patchUsageForm.usage_days = 7
|
|
||||||
patchUsageForm.aux_times_per_day = 3
|
|
||||||
patchUsageForm.aux_usage_days = 7
|
|
||||||
patchUsageForm.medication_days = undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
async function submitPatchUsage() {
|
|
||||||
const form = patchUsageFormRef.value
|
|
||||||
if (!form) return
|
|
||||||
try {
|
|
||||||
await form.validate()
|
|
||||||
} catch {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const ordId = detailData.value?.id
|
|
||||||
if (!ordId) return
|
|
||||||
patchUsageSaving.value = true
|
|
||||||
try {
|
|
||||||
const payload: {
|
|
||||||
id: number
|
|
||||||
times_per_day: number
|
|
||||||
usage_days: number
|
|
||||||
medication_days: number
|
|
||||||
aux_times_per_day?: number
|
|
||||||
aux_usage_days?: number
|
|
||||||
} = {
|
|
||||||
id: ordId,
|
|
||||||
times_per_day: Number(patchUsageForm.times_per_day),
|
|
||||||
usage_days: Number(patchUsageForm.usage_days),
|
|
||||||
medication_days: Number(patchUsageForm.medication_days)
|
|
||||||
}
|
|
||||||
if (detailHasAuxHerbs.value) {
|
|
||||||
payload.aux_times_per_day = Number(patchUsageForm.aux_times_per_day)
|
|
||||||
payload.aux_usage_days = Number(patchUsageForm.aux_usage_days)
|
|
||||||
}
|
|
||||||
await prescriptionOrderPatchPrescriptionUsage(payload)
|
|
||||||
feedback.msgSuccess('保存成功')
|
|
||||||
patchUsageVisible.value = false
|
|
||||||
await refreshCurrentPrescriptionOrderDetail()
|
|
||||||
await fetchLogs(ordId)
|
|
||||||
getLists()
|
|
||||||
} catch {
|
|
||||||
/* 拦截器已提示 */
|
|
||||||
} finally {
|
|
||||||
patchUsageSaving.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function openPatchRxPatientDialog() {
|
function openPatchRxPatientDialog() {
|
||||||
const rx = detailPrescription.value
|
const rx = detailPrescription.value
|
||||||
const ord = detailData.value
|
const ord = detailData.value
|
||||||
@@ -4307,10 +4060,6 @@ const editForm = reactive({
|
|||||||
diagnosis_creator_dept_path: ''
|
diagnosis_creator_dept_path: ''
|
||||||
})
|
})
|
||||||
|
|
||||||
const editServicePackageSelectOptions = computed(() =>
|
|
||||||
mergeServicePackageSelectOptions(servicePackageOptions.value, editForm.service_package)
|
|
||||||
)
|
|
||||||
|
|
||||||
/** 诊单创建人部门:拆成面包屑。多部门为「;」分隔;路径内为「 / 」含父级(与后端 buildDeptPath 一致) */
|
/** 诊单创建人部门:拆成面包屑。多部门为「;」分隔;路径内为「 / 」含父级(与后端 buildDeptPath 一致) */
|
||||||
const editDiagnosisCreatorDeptBreadcrumbs = computed(() => {
|
const editDiagnosisCreatorDeptBreadcrumbs = computed(() => {
|
||||||
const raw = String(editForm.diagnosis_creator_dept_path || '').trim()
|
const raw = String(editForm.diagnosis_creator_dept_path || '').trim()
|
||||||
@@ -4561,7 +4310,18 @@ async function openEdit(row: {
|
|||||||
editForm.dose_unit = d.dose_unit || '剂'
|
editForm.dose_unit = d.dose_unit || '剂'
|
||||||
editForm.prev_staff = d.prev_staff || ''
|
editForm.prev_staff = d.prev_staff || ''
|
||||||
editForm.service_channel = d.service_channel || ''
|
editForm.service_channel = d.service_channel || ''
|
||||||
editForm.service_package = parseServicePackageValues(d.service_package)
|
// 处理服务套餐:如果是字符串,转换为数组
|
||||||
|
if (d.service_package) {
|
||||||
|
if (Array.isArray(d.service_package)) {
|
||||||
|
editForm.service_package = d.service_package
|
||||||
|
} else if (typeof d.service_package === 'string') {
|
||||||
|
editForm.service_package = d.service_package.split(',').filter(v => v.trim() !== '')
|
||||||
|
} else {
|
||||||
|
editForm.service_package = []
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
editForm.service_package = []
|
||||||
|
}
|
||||||
editForm.express_company = String(d.express_company || 'auto') || 'auto'
|
editForm.express_company = String(d.express_company || 'auto') || 'auto'
|
||||||
editForm.tracking_number = d.tracking_number || ''
|
editForm.tracking_number = d.tracking_number || ''
|
||||||
editForm.fee_type = Number(d.fee_type) || 3
|
editForm.fee_type = Number(d.fee_type) || 3
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="code-generation">
|
<div>
|
||||||
<el-card class="!border-none" shadow="never">
|
<admin-page-filter-panel>
|
||||||
<el-form class="mb-[-16px]" :model="formData" inline>
|
<el-form class="mb-[-16px]" :model="formData" inline>
|
||||||
<el-form-item class="w-[280px]" label="表名称">
|
<el-form-item class="w-[280px]" label="表名称">
|
||||||
<el-input v-model="formData.table_name" clearable @keyup.enter="resetPage" />
|
<el-input v-model="formData.table_name" clearable @keyup.enter="resetPage" />
|
||||||
@@ -13,8 +13,8 @@
|
|||||||
<el-button @click="resetParams">重置</el-button>
|
<el-button @click="resetParams">重置</el-button>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
</el-card>
|
</admin-page-filter-panel>
|
||||||
<el-card class="!border-none mt-4" shadow="never" v-loading="pager.loading">
|
<admin-page-data-panel>
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<data-table
|
<data-table
|
||||||
v-perms="['tools.generator/selectTable']"
|
v-perms="['tools.generator/selectTable']"
|
||||||
@@ -126,10 +126,10 @@
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-end mt-4">
|
<template #footer>
|
||||||
<pagination v-model="pager" @change="getLists" />
|
<pagination v-model="pager" @change="getLists" />
|
||||||
</div>
|
</template>
|
||||||
</el-card>
|
</admin-page-data-panel>
|
||||||
<code-preview
|
<code-preview
|
||||||
v-if="previewState.show"
|
v-if="previewState.show"
|
||||||
v-model="previewState.show"
|
v-model="previewState.show"
|
||||||
|
|||||||
@@ -866,7 +866,7 @@ onUnmounted(() => {
|
|||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background: linear-gradient(145deg, #6366f1 0%, #8b5cf6 100%);
|
background: linear-gradient(145deg, #0d9488 0%, #0891b2 100%);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="error">
|
<div class="error">
|
||||||
<div>
|
<div class="error-panel">
|
||||||
<slot name="content">
|
<slot name="content">
|
||||||
<div class="error-code">{{ code }}</div>
|
<div class="error-code">{{ code }}</div>
|
||||||
</slot>
|
</slot>
|
||||||
<div class="text-lg text-tx-secondary mt-7 mb-7">{{ title }}</div>
|
<div class="error-title">{{ title }}</div>
|
||||||
<el-button v-if="showBtn" type="primary" @click="router.go(-1)">
|
<el-button v-if="showBtn" type="primary" size="large" @click="router.go(-1)">
|
||||||
{{ second }} 秒后返回上一页
|
{{ second }} 秒后返回上一页
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
@@ -43,16 +43,38 @@ onUnmounted(() => {
|
|||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.error {
|
.error {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
height: 100vh;
|
min-height: 100vh;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
.error-code {
|
background: var(--el-bg-color-page);
|
||||||
@apply text-primary;
|
padding: 24px;
|
||||||
font-size: 150px;
|
}
|
||||||
}
|
|
||||||
.el-button {
|
.error-panel {
|
||||||
width: 176px;
|
width: min(480px, 100%);
|
||||||
}
|
padding: 48px 32px;
|
||||||
|
border-radius: var(--admin-radius-xl);
|
||||||
|
border: 1px solid var(--el-border-color-lighter);
|
||||||
|
background: var(--el-bg-color);
|
||||||
|
box-shadow: var(--el-box-shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-code {
|
||||||
|
@apply text-primary;
|
||||||
|
font-size: 96px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1;
|
||||||
|
letter-spacing: -0.04em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-title {
|
||||||
|
@apply text-tx-secondary;
|
||||||
|
font-size: 18px;
|
||||||
|
margin: 20px 0 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-button {
|
||||||
|
min-width: 176px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="fans-management">
|
<div class="fans-management">
|
||||||
<!-- 搜索区域 -->
|
<!-- 搜索区域 -->
|
||||||
<el-card class="!border-none" shadow="never">
|
<admin-page-filter-panel>
|
||||||
<el-form class="ls-form" :model="formData" inline>
|
<el-form class="ls-form" :model="formData" inline>
|
||||||
<el-form-item class="w-[280px]" label="姓名">
|
<el-form-item class="w-[280px]" label="姓名">
|
||||||
<el-input
|
<el-input
|
||||||
@@ -36,14 +36,14 @@
|
|||||||
<el-button @click="resetParams">重置</el-button>
|
<el-button @click="resetParams">重置</el-button>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
</el-card>
|
</admin-page-filter-panel>
|
||||||
|
|
||||||
<!-- 列表区域 -->
|
<!-- 列表区域 -->
|
||||||
<el-card class="!border-none mt-4" shadow="never">
|
<admin-page-data-panel v-loading="pager.loading">
|
||||||
<div class="mb-4">
|
<template #toolbar>
|
||||||
<el-button type="primary" @click="handleAdd">新增粉丝</el-button>
|
<el-button type="primary" @click="handleAdd">新增粉丝</el-button>
|
||||||
</div>
|
</template>
|
||||||
<el-table :data="pager.lists" size="large" v-loading="pager.loading">
|
<el-table :data="pager.lists" size="large">
|
||||||
<el-table-column label="ID" prop="id" width="70" />
|
<el-table-column label="ID" prop="id" width="70" />
|
||||||
<el-table-column label="姓名" prop="name" min-width="100" />
|
<el-table-column label="姓名" prop="name" min-width="100" />
|
||||||
<el-table-column label="手机号" prop="phone" min-width="130" />
|
<el-table-column label="手机号" prop="phone" min-width="130" />
|
||||||
@@ -81,10 +81,10 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
<div class="flex mt-4 justify-end">
|
<template #footer>
|
||||||
<pagination v-model="pager" @change="getLists" />
|
<pagination v-model="pager" @change="getLists" />
|
||||||
</div>
|
</template>
|
||||||
</el-card>
|
</admin-page-data-panel>
|
||||||
|
|
||||||
<!-- 新增/编辑粉丝弹窗 -->
|
<!-- 新增/编辑粉丝弹窗 -->
|
||||||
<el-dialog
|
<el-dialog
|
||||||
|
|||||||
@@ -3704,7 +3704,7 @@ function getYejiDeptComboOption(tb: YejiTable) {
|
|||||||
const cats = rows.map(r =>
|
const cats = rows.map(r =>
|
||||||
r.dept_name.length > 10 ? `${r.dept_name.slice(0, 10)}…` : r.dept_name
|
r.dept_name.length > 10 ? `${r.dept_name.slice(0, 10)}…` : r.dept_name
|
||||||
)
|
)
|
||||||
const palette = ['#64748b', '#38bdf8', '#6366f1']
|
const palette = ['#64748b', '#38bdf8', '#0d9488']
|
||||||
const series: any[] = [
|
const series: any[] = [
|
||||||
{
|
{
|
||||||
name: '进线',
|
name: '进线',
|
||||||
@@ -4029,8 +4029,8 @@ onMounted(async () => {
|
|||||||
|
|
||||||
.yeji-page {
|
.yeji-page {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
--yj-brand: #6366f1;
|
--yj-brand: #0d9488;
|
||||||
--yj-brand-soft: #eef2ff;
|
--yj-brand-soft: #f0fdfa;
|
||||||
--yj-teal: #0ea5e9;
|
--yj-teal: #0ea5e9;
|
||||||
--yj-teal-soft: #f0f9ff;
|
--yj-teal-soft: #f0f9ff;
|
||||||
--yj-accent: #f43f5e;
|
--yj-accent: #f43f5e;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<el-card class="!border-none" shadow="never">
|
<admin-page-filter-panel>
|
||||||
<el-alert
|
<el-alert
|
||||||
type="warning"
|
type="warning"
|
||||||
title="温馨提示:用户账户变动记录"
|
title="温馨提示:用户账户变动记录"
|
||||||
@@ -38,9 +38,9 @@
|
|||||||
<el-button @click="resetParams">重置</el-button>
|
<el-button @click="resetParams">重置</el-button>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
</el-card>
|
</admin-page-filter-panel>
|
||||||
<el-card class="!border-none mt-4" shadow="never">
|
<admin-page-data-panel v-loading="pager.loading">
|
||||||
<el-table size="large" v-loading="pager.loading" :data="pager.lists">
|
<el-table size="large" :data="pager.lists">
|
||||||
<el-table-column label="用户账号" prop="account" min-width="100" />
|
<el-table-column label="用户账号" prop="account" min-width="100" />
|
||||||
<el-table-column label="用户昵称" min-width="160">
|
<el-table-column label="用户昵称" min-width="160">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
@@ -71,10 +71,10 @@
|
|||||||
<el-table-column label="来源单号" prop="source_sn" min-width="100" />
|
<el-table-column label="来源单号" prop="source_sn" min-width="100" />
|
||||||
<el-table-column label="记录时间" prop="create_time" min-width="120" />
|
<el-table-column label="记录时间" prop="create_time" min-width="120" />
|
||||||
</el-table>
|
</el-table>
|
||||||
<div class="flex justify-end mt-4">
|
<template #footer>
|
||||||
<pagination v-model="pager" @change="getLists" />
|
<pagination v-model="pager" @change="getLists" />
|
||||||
</div>
|
</template>
|
||||||
</el-card>
|
</admin-page-data-panel>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts" setup name="balanceDetail">
|
<script lang="ts" setup name="balanceDetail">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<el-card class="!border-none" shadow="never">
|
<admin-page-filter-panel>
|
||||||
<el-alert
|
<el-alert
|
||||||
type="warning"
|
type="warning"
|
||||||
title="温馨提示:用户充值记录"
|
title="温馨提示:用户充值记录"
|
||||||
@@ -54,9 +54,9 @@
|
|||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
</el-card>
|
</admin-page-filter-panel>
|
||||||
<el-card class="!border-none mt-4" shadow="never">
|
<admin-page-data-panel v-loading="pager.loading">
|
||||||
<el-table size="large" v-loading="pager.loading" :data="pager.lists">
|
<el-table size="large" :data="pager.lists">
|
||||||
<el-table-column label="用户信息" min-width="160">
|
<el-table-column label="用户信息" min-width="160">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
@@ -104,10 +104,10 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
<div class="flex justify-end mt-4">
|
<template #footer>
|
||||||
<pagination v-model="pager" @change="getLists" />
|
<pagination v-model="pager" @change="getLists" />
|
||||||
</div>
|
</template>
|
||||||
</el-card>
|
</admin-page-data-panel>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts" setup name="rechargeRecord">
|
<script lang="ts" setup name="rechargeRecord">
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
<el-card class="!border-none" shadow="never">
|
<admin-page-filter-panel>
|
||||||
<el-form ref="formRef" class="mb-[-16px] mt-[16px]" :model="queryParams" :inline="true">
|
<el-form ref="formRef" class="mb-[-16px] mt-[16px]" :model="queryParams" :inline="true">
|
||||||
<el-form-item class="w-[280px]" label="退款单号">
|
<el-form-item class="w-[280px]" label="退款单号">
|
||||||
<el-input
|
<el-input
|
||||||
@@ -69,8 +69,8 @@
|
|||||||
/> -->
|
/> -->
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
</el-card>
|
</admin-page-filter-panel>
|
||||||
<el-card class="!border-none mt-4" shadow="never">
|
<admin-page-data-panel>
|
||||||
<el-tabs v-model="activeTab" @tab-change="handleTabChange">
|
<el-tabs v-model="activeTab" @tab-change="handleTabChange">
|
||||||
<el-tab-pane
|
<el-tab-pane
|
||||||
v-for="(item, index) in tabLists"
|
v-for="(item, index) in tabLists"
|
||||||
@@ -78,7 +78,7 @@
|
|||||||
:name="index"
|
:name="index"
|
||||||
:key="index"
|
:key="index"
|
||||||
>
|
>
|
||||||
<el-table size="large" v-loading="pager.loading" :data="pager.lists">
|
<el-table size="large" :data="pager.lists">
|
||||||
<el-table-column label="退款单号" prop="sn" min-width="190" />
|
<el-table-column label="退款单号" prop="sn" min-width="190" />
|
||||||
<el-table-column label="用户信息" min-width="160">
|
<el-table-column label="用户信息" min-width="160">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
@@ -140,10 +140,10 @@
|
|||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
</el-tabs>
|
</el-tabs>
|
||||||
|
|
||||||
<div class="flex justify-end mt-4">
|
<template #footer>
|
||||||
<pagination v-model="pager" @change="getLists" />
|
<pagination v-model="pager" @change="getLists" />
|
||||||
</div>
|
</template>
|
||||||
</el-card>
|
</admin-page-data-panel>
|
||||||
<refund-log v-model="showRefundLog" :refund-id="selectRefundId" />
|
<refund-log v-model="showRefundLog" :refund-id="selectRefundId" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<el-card class="!border-none" shadow="never">
|
<admin-page-filter-panel>
|
||||||
<el-alert
|
<el-alert
|
||||||
type="warning"
|
type="warning"
|
||||||
title="温馨提示:平台配置在各个场景下的通知发送方式和内容模板"
|
title="温馨提示:平台配置在各个场景下的通知发送方式和内容模板"
|
||||||
:closable="false"
|
:closable="false"
|
||||||
show-icon
|
show-icon
|
||||||
></el-alert>
|
></el-alert>
|
||||||
</el-card>
|
</admin-page-filter-panel>
|
||||||
<el-card class="!border-none mt-4" shadow="never">
|
<admin-page-data-panel v-loading="pager.loading">
|
||||||
<el-tabs v-model="tabsActive" @tab-change="getLists">
|
<el-tabs v-model="tabsActive" @tab-change="getLists">
|
||||||
<el-tab-pane
|
<el-tab-pane
|
||||||
v-for="(item, index) in tabsMap"
|
v-for="(item, index) in tabsMap"
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
lazy
|
lazy
|
||||||
></el-tab-pane>
|
></el-tab-pane>
|
||||||
</el-tabs>
|
</el-tabs>
|
||||||
<el-table size="large" :data="pager.lists" v-loading="pager.loading">
|
<el-table size="large" :data="pager.lists" >
|
||||||
<el-table-column label="通知场景" prop="scene_name" min-width="120" />
|
<el-table-column label="通知场景" prop="scene_name" min-width="120" />
|
||||||
<el-table-column label="通知类型" prop="type_desc" min-width="160" />
|
<el-table-column label="通知类型" prop="type_desc" min-width="160" />
|
||||||
<el-table-column label="短信通知" min-width="80">
|
<el-table-column label="短信通知" min-width="80">
|
||||||
@@ -44,7 +44,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
</el-card>
|
</admin-page-data-panel>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts" setup name="notice">
|
<script lang="ts" setup name="notice">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<el-card class="!border-none" shadow="never" v-loading="state.loading">
|
<admin-page-data-panel v-loading="state.loading">
|
||||||
<el-table size="large" :data="state.lists">
|
<el-table size="large" :data="state.lists">
|
||||||
<el-table-column label="短信渠道" prop="name" min-width="120" />
|
<el-table-column label="短信渠道" prop="name" min-width="120" />
|
||||||
<el-table-column label="状态" min-width="120">
|
<el-table-column label="状态" min-width="120">
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
</el-card>
|
</admin-page-data-panel>
|
||||||
<edit-popup ref="editRef" @success="getLists" />
|
<edit-popup ref="editRef" @success="getLists" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -33,13 +33,11 @@ import EditPopup from './edit.vue'
|
|||||||
|
|
||||||
const editRef = shallowRef<InstanceType<typeof EditPopup>>()
|
const editRef = shallowRef<InstanceType<typeof EditPopup>>()
|
||||||
|
|
||||||
// 列表数据
|
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
loading: false,
|
loading: false,
|
||||||
lists: []
|
lists: []
|
||||||
})
|
})
|
||||||
|
|
||||||
// 获取存储引擎列表数据
|
|
||||||
const getLists = async () => {
|
const getLists = async () => {
|
||||||
try {
|
try {
|
||||||
state.loading = true
|
state.loading = true
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<!-- 订单列表 -->
|
<!-- 订单列表 -->
|
||||||
<template>
|
<template>
|
||||||
<div class="order-list">
|
<div>
|
||||||
<el-card class="!border-none" shadow="never">
|
<admin-page-filter-panel>
|
||||||
<!-- 搜索表单 -->
|
<!-- 搜索表单 -->
|
||||||
<el-form class="ls-form" :model="queryParams" inline>
|
<el-form class="ls-form" :model="queryParams" inline>
|
||||||
<el-form-item class="w-[280px]" label="订单号">
|
<el-form-item class="w-[280px]" label="订单号">
|
||||||
@@ -106,10 +106,10 @@
|
|||||||
</el-button>
|
</el-button>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
</el-card>
|
</admin-page-filter-panel>
|
||||||
|
|
||||||
<!-- Tab 筛选 + 今日收益 -->
|
<!-- Tab 筛选 + 今日收益 + 数据表格 -->
|
||||||
<el-card class="!border-none mt-4" shadow="never">
|
<admin-page-data-panel v-loading="pager.loading">
|
||||||
<div class="flex items-center justify-between mb-4">
|
<div class="flex items-center justify-between mb-4">
|
||||||
<el-tabs v-model="patientAssociationTab" @tab-change="handleTabChange">
|
<el-tabs v-model="patientAssociationTab" @tab-change="handleTabChange">
|
||||||
<el-tab-pane label="全部" name="" />
|
<el-tab-pane label="全部" name="" />
|
||||||
@@ -122,10 +122,7 @@
|
|||||||
<span class="text-gray-400">({{ todayRevenue.count }} 笔)</span>
|
<span class="text-gray-400">({{ todayRevenue.count }} 笔)</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
|
||||||
|
|
||||||
<!-- 数据表格 -->
|
|
||||||
<el-card class="!border-none mt-4" shadow="never">
|
|
||||||
<div v-perms="['order.order/zhipai']" class="mb-3 flex items-center gap-3">
|
<div v-perms="['order.order/zhipai']" class="mb-3 flex items-center gap-3">
|
||||||
<el-button type="primary" :disabled="!selectedOrderIds.length" @click="openAssignDialog">
|
<el-button type="primary" :disabled="!selectedOrderIds.length" @click="openAssignDialog">
|
||||||
将创建人指给医助
|
将创建人指给医助
|
||||||
@@ -136,7 +133,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<el-table
|
<el-table
|
||||||
ref="orderTableRef"
|
ref="orderTableRef"
|
||||||
v-loading="pager.loading"
|
|
||||||
:data="pager.lists"
|
:data="pager.lists"
|
||||||
row-key="id"
|
row-key="id"
|
||||||
size="large"
|
size="large"
|
||||||
@@ -283,10 +279,10 @@
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
|
|
||||||
<div class="flex justify-end mt-4">
|
<template #footer>
|
||||||
<pagination v-model="pager" @change="getLists" />
|
<pagination v-model="pager" @change="getLists" />
|
||||||
</div>
|
</template>
|
||||||
</el-card>
|
</admin-page-data-panel>
|
||||||
|
|
||||||
<!-- 详情弹窗 -->
|
<!-- 详情弹窗 -->
|
||||||
<el-dialog
|
<el-dialog
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="department">
|
<div>
|
||||||
<el-card class="!border-none" shadow="never">
|
<admin-page-filter-panel>
|
||||||
<el-form ref="formRef" class="mb-[-16px]" :model="queryParams" :inline="true">
|
<el-form ref="formRef" class="mb-[-16px]" :model="queryParams" :inline="true">
|
||||||
<el-form-item class="w-[280px]" label="部门名称" prop="name">
|
<el-form-item class="w-[280px]" label="部门名称" prop="name">
|
||||||
<el-input
|
<el-input
|
||||||
@@ -22,22 +22,21 @@
|
|||||||
<el-button @click="resetParams">重置</el-button>
|
<el-button @click="resetParams">重置</el-button>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
</el-card>
|
</admin-page-filter-panel>
|
||||||
<el-card class="!border-none mt-4" shadow="never">
|
|
||||||
<div>
|
<admin-page-data-panel v-loading="loading">
|
||||||
|
<template #toolbar>
|
||||||
<el-button v-perms="['dept.dept/add']" type="primary" @click="handleAdd()">
|
<el-button v-perms="['dept.dept/add']" type="primary" @click="handleAdd()">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<icon name="el-icon-Plus" />
|
<icon name="el-icon-Plus" />
|
||||||
</template>
|
</template>
|
||||||
新增
|
新增
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button @click="handleExpand"> 展开/折叠 </el-button>
|
<el-button @click="handleExpand">展开/折叠</el-button>
|
||||||
</div>
|
</template>
|
||||||
<el-table
|
<el-table
|
||||||
ref="tableRef"
|
ref="tableRef"
|
||||||
class="mt-4"
|
|
||||||
size="large"
|
size="large"
|
||||||
v-loading="loading"
|
|
||||||
:data="lists"
|
:data="lists"
|
||||||
row-key="id"
|
row-key="id"
|
||||||
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
|
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
|
||||||
@@ -68,7 +67,6 @@
|
|||||||
}}</el-tag>
|
}}</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
|
||||||
<el-table-column label="排序" prop="sort" min-width="100" />
|
<el-table-column label="排序" prop="sort" min-width="100" />
|
||||||
<el-table-column label="更新时间" prop="update_time" min-width="180" />
|
<el-table-column label="更新时间" prop="update_time" min-width="180" />
|
||||||
<el-table-column label="操作" width="160" fixed="right">
|
<el-table-column label="操作" width="160" fixed="right">
|
||||||
@@ -101,7 +99,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
</el-card>
|
</admin-page-data-panel>
|
||||||
<edit-popup v-if="showEdit" ref="editRef" @success="getLists" @close="showEdit = false" />
|
<edit-popup v-if="showEdit" ref="editRef" @success="getLists" @close="showEdit = false" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -120,14 +118,18 @@ let isExpand = false
|
|||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const lists = ref<any[]>([])
|
const lists = ref<any[]>([])
|
||||||
const queryParams = reactive({
|
const queryParams = reactive({
|
||||||
status: '',
|
name: '',
|
||||||
name: ''
|
status: ''
|
||||||
})
|
})
|
||||||
const showEdit = ref(false)
|
const showEdit = ref(false)
|
||||||
|
|
||||||
const getLists = async () => {
|
const getLists = async () => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
lists.value = await deptLists(queryParams)
|
try {
|
||||||
loading.value = false
|
lists.value = await deptLists(queryParams)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const resetParams = () => {
|
const resetParams = () => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="post-lists">
|
<div>
|
||||||
<el-card class="!border-none" shadow="never">
|
<admin-page-filter-panel>
|
||||||
<el-form ref="formRef" class="mb-[-16px]" :model="queryParams" :inline="true">
|
<el-form ref="formRef" class="mb-[-16px]" :model="queryParams" :inline="true">
|
||||||
<el-form-item class="w-[280px]" label="岗位编码">
|
<el-form-item class="w-[280px]" label="岗位编码">
|
||||||
<el-input
|
<el-input
|
||||||
@@ -31,17 +31,18 @@
|
|||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
</el-card>
|
</admin-page-filter-panel>
|
||||||
<el-card class="!border-none mt-4" shadow="never">
|
|
||||||
<div>
|
<admin-page-data-panel v-loading="pager.loading">
|
||||||
|
<template #toolbar>
|
||||||
<el-button v-perms="['dept.jobs/add']" type="primary" @click="handleAdd()">
|
<el-button v-perms="['dept.jobs/add']" type="primary" @click="handleAdd()">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<icon name="el-icon-Plus" />
|
<icon name="el-icon-Plus" />
|
||||||
</template>
|
</template>
|
||||||
新增
|
新增
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</template>
|
||||||
<el-table class="mt-4" size="large" v-loading="pager.loading" :data="pager.lists">
|
<el-table size="large" :data="pager.lists">
|
||||||
<el-table-column label="岗位编码" prop="code" min-width="100" />
|
<el-table-column label="岗位编码" prop="code" min-width="100" />
|
||||||
<el-table-column label="岗位名称" prop="name" min-width="100" />
|
<el-table-column label="岗位名称" prop="name" min-width="100" />
|
||||||
<el-table-column label="排序" prop="sort" min-width="100" />
|
<el-table-column label="排序" prop="sort" min-width="100" />
|
||||||
@@ -75,10 +76,10 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
<div class="flex justify-end mt-4">
|
<template #footer>
|
||||||
<pagination v-model="pager" @change="getLists" />
|
<pagination v-model="pager" @change="getLists" />
|
||||||
</div>
|
</template>
|
||||||
</el-card>
|
</admin-page-data-panel>
|
||||||
<edit-popup v-if="showEdit" ref="editRef" @success="getLists" @close="showEdit = false" />
|
<edit-popup v-if="showEdit" ref="editRef" @success="getLists" @close="showEdit = false" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1275,7 +1275,7 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
.float-action.edit {
|
.float-action.edit {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%);
|
background: linear-gradient(135deg, #0d9488 0%, #0f766e 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.float-action.edit:hover,
|
.float-action.edit:hover,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="admin">
|
<div>
|
||||||
<el-card class="!border-none" shadow="never">
|
<admin-page-filter-panel>
|
||||||
<el-form class="mb-[-16px]" :model="formData" inline>
|
<el-form class="mb-[-16px]" :model="formData" inline>
|
||||||
<el-form-item class="w-[280px]" label="管理员账号">
|
<el-form-item class="w-[280px]" label="管理员账号">
|
||||||
<el-input
|
<el-input
|
||||||
@@ -40,77 +40,78 @@
|
|||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
</el-card>
|
</admin-page-filter-panel>
|
||||||
<el-card v-loading="pager.loading" class="mt-4 !border-none" shadow="never">
|
|
||||||
<el-button v-perms="['auth.admin/add']" type="primary" @click="handleAdd">
|
<admin-page-data-panel v-loading="pager.loading">
|
||||||
<template #icon>
|
<template #toolbar>
|
||||||
<icon name="el-icon-Plus" />
|
<el-button v-perms="['auth.admin/add']" type="primary" @click="handleAdd">
|
||||||
</template>
|
<template #icon>
|
||||||
新增
|
<icon name="el-icon-Plus" />
|
||||||
</el-button>
|
</template>
|
||||||
<div class="mt-4">
|
新增
|
||||||
<el-table :data="pager.lists" size="large">
|
</el-button>
|
||||||
<el-table-column label="ID" prop="id" min-width="60" />>
|
</template>
|
||||||
<el-table-column label="头像" min-width="100">
|
<el-table :data="pager.lists" size="large">
|
||||||
<template #default="{ row }">
|
<el-table-column label="ID" prop="id" min-width="60" />
|
||||||
<el-avatar :size="50" :src="row.avatar"></el-avatar>
|
<el-table-column label="头像" min-width="100">
|
||||||
</template>
|
<template #default="{ row }">
|
||||||
</el-table-column>
|
<el-avatar :size="50" :src="row.avatar"></el-avatar>
|
||||||
<el-table-column label="账号" prop="account" min-width="100" />
|
</template>
|
||||||
<el-table-column label="名称" prop="name" min-width="100" />
|
</el-table-column>
|
||||||
<el-table-column
|
<el-table-column label="账号" prop="account" min-width="100" />
|
||||||
label="角色"
|
<el-table-column label="名称" prop="name" min-width="100" />
|
||||||
prop="role_name"
|
<el-table-column
|
||||||
min-width="100"
|
label="角色"
|
||||||
show-tooltip-when-overflow
|
prop="role_name"
|
||||||
/>
|
min-width="100"
|
||||||
<el-table-column
|
show-tooltip-when-overflow
|
||||||
label="部门"
|
/>
|
||||||
prop="dept_name"
|
<el-table-column
|
||||||
min-width="100"
|
label="部门"
|
||||||
show-tooltip-when-overflow
|
prop="dept_name"
|
||||||
/>
|
min-width="100"
|
||||||
<el-table-column label="创建时间" prop="create_time" min-width="180" />
|
show-tooltip-when-overflow
|
||||||
<el-table-column label="最近登录时间" prop="login_time" min-width="180" />
|
/>
|
||||||
<el-table-column label="最近登录IP" prop="login_ip" min-width="120" />
|
<el-table-column label="创建时间" prop="create_time" min-width="180" />
|
||||||
<el-table-column label="状态" min-width="100" v-perms="['auth.admin/edit']">
|
<el-table-column label="最近登录时间" prop="login_time" min-width="180" />
|
||||||
<template #default="{ row }">
|
<el-table-column label="最近登录IP" prop="login_ip" min-width="120" />
|
||||||
<el-switch
|
<el-table-column label="状态" min-width="100" v-perms="['auth.admin/edit']">
|
||||||
v-if="row.root != 1"
|
<template #default="{ row }">
|
||||||
v-model="row.disable"
|
<el-switch
|
||||||
:active-value="0"
|
v-if="row.root != 1"
|
||||||
:inactive-value="1"
|
v-model="row.disable"
|
||||||
@change="changeStatus(row)"
|
:active-value="0"
|
||||||
/>
|
:inactive-value="1"
|
||||||
</template>
|
@change="changeStatus(row)"
|
||||||
</el-table-column>
|
/>
|
||||||
<el-table-column label="操作" width="120" fixed="right">
|
</template>
|
||||||
<template #default="{ row }">
|
</el-table-column>
|
||||||
<el-button
|
<el-table-column label="操作" width="120" fixed="right">
|
||||||
v-perms="['auth.admin/edit']"
|
<template #default="{ row }">
|
||||||
type="primary"
|
<el-button
|
||||||
link
|
v-perms="['auth.admin/edit']"
|
||||||
@click="handleEdit(row)"
|
type="primary"
|
||||||
>
|
link
|
||||||
编辑
|
@click="handleEdit(row)"
|
||||||
</el-button>
|
>
|
||||||
<el-button
|
编辑
|
||||||
v-if="row.root != 1"
|
</el-button>
|
||||||
v-perms="['auth.admin/delete']"
|
<el-button
|
||||||
type="danger"
|
v-if="row.root != 1"
|
||||||
link
|
v-perms="['auth.admin/delete']"
|
||||||
@click="handleDelete(row.id)"
|
type="danger"
|
||||||
>
|
link
|
||||||
删除
|
@click="handleDelete(row.id)"
|
||||||
</el-button>
|
>
|
||||||
</template>
|
删除
|
||||||
</el-table-column>
|
</el-button>
|
||||||
</el-table>
|
</template>
|
||||||
</div>
|
</el-table-column>
|
||||||
<div class="flex mt-4 justify-end">
|
</el-table>
|
||||||
|
<template #footer>
|
||||||
<pagination v-model="pager" @change="getLists" />
|
<pagination v-model="pager" @change="getLists" />
|
||||||
</div>
|
</template>
|
||||||
</el-card>
|
</admin-page-data-panel>
|
||||||
<edit-popup v-if="showEdit" ref="editRef" @success="getLists" @close="showEdit = false" />
|
<edit-popup v-if="showEdit" ref="editRef" @success="getLists" @close="showEdit = false" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -125,7 +126,6 @@ import feedback from '@/utils/feedback'
|
|||||||
import EditPopup from './edit.vue'
|
import EditPopup from './edit.vue'
|
||||||
|
|
||||||
const editRef = shallowRef<InstanceType<typeof EditPopup>>()
|
const editRef = shallowRef<InstanceType<typeof EditPopup>>()
|
||||||
// 表单数据
|
|
||||||
const formData = reactive({
|
const formData = reactive({
|
||||||
account: '',
|
account: '',
|
||||||
name: '',
|
name: '',
|
||||||
|
|||||||
@@ -1,20 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="menu-lists">
|
<div>
|
||||||
<el-card class="!border-none" shadow="never">
|
<admin-page-data-panel v-loading="pager.loading">
|
||||||
<div>
|
<template #toolbar>
|
||||||
<el-button v-perms="['auth.menu/add']" type="primary" @click="handleAdd()">
|
<el-button v-perms="['auth.menu/add']" type="primary" @click="handleAdd()">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<icon name="el-icon-Plus" />
|
<icon name="el-icon-Plus" />
|
||||||
</template>
|
</template>
|
||||||
新增
|
新增
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button @click="handleExpand"> 展开/折叠 </el-button>
|
<el-button @click="handleExpand">展开/折叠</el-button>
|
||||||
</div>
|
</template>
|
||||||
<el-table
|
<el-table
|
||||||
ref="tableRef"
|
ref="tableRef"
|
||||||
class="mt-4"
|
|
||||||
size="large"
|
size="large"
|
||||||
v-loading="pager.loading"
|
|
||||||
:data="pager.lists"
|
:data="pager.lists"
|
||||||
row-key="id"
|
row-key="id"
|
||||||
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
|
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
|
||||||
@@ -87,7 +85,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
</el-card>
|
</admin-page-data-panel>
|
||||||
<edit-popup v-if="showEdit" ref="editRef" @success="getLists" @close="showEdit = false" />
|
<edit-popup v-if="showEdit" ref="editRef" @success="getLists" @close="showEdit = false" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,68 +1,64 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="role-lists">
|
<div>
|
||||||
<el-card class="!border-none" shadow="never">
|
<admin-page-data-panel v-loading="pager.loading">
|
||||||
<div>
|
<template #toolbar>
|
||||||
<el-button v-perms="['auth.role/add']" type="primary" @click="handleAdd">
|
<el-button v-perms="['auth.role/add']" type="primary" @click="handleAdd">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<icon name="el-icon-Plus" />
|
<icon name="el-icon-Plus" />
|
||||||
</template>
|
</template>
|
||||||
新增
|
新增
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</template>
|
||||||
<div class="mt-4" v-loading="pager.loading">
|
<el-table :data="pager.lists" size="large">
|
||||||
<div>
|
<el-table-column prop="id" label="ID" min-width="100" />
|
||||||
<el-table :data="pager.lists" size="large">
|
<el-table-column prop="name" label="名称" min-width="150" />
|
||||||
<el-table-column prop="id" label="ID" min-width="100" />
|
<el-table-column
|
||||||
<el-table-column prop="name" label="名称" min-width="150" />
|
prop="desc"
|
||||||
<el-table-column
|
label="备注"
|
||||||
prop="desc"
|
min-width="150"
|
||||||
label="备注"
|
show-overflow-tooltip
|
||||||
min-width="150"
|
/>
|
||||||
show-overflow-tooltip
|
<el-table-column prop="sort" label="排序" min-width="100" />
|
||||||
/>
|
<el-table-column label="数据范围" min-width="140">
|
||||||
<el-table-column prop="sort" label="排序" min-width="100" />
|
<template #default="{ row }">
|
||||||
<el-table-column label="数据范围" min-width="140">
|
{{ dataScopeLabel(row.data_scope) }}
|
||||||
<template #default="{ row }">
|
</template>
|
||||||
{{ dataScopeLabel(row.data_scope) }}
|
</el-table-column>
|
||||||
</template>
|
<el-table-column prop="num" label="管理员人数" min-width="100" />
|
||||||
</el-table-column>
|
<el-table-column prop="create_time" label="创建时间" min-width="180" />
|
||||||
<el-table-column prop="num" label="管理员人数" min-width="100" />
|
<el-table-column label="操作" width="200" fixed="right">
|
||||||
<el-table-column prop="create_time" label="创建时间" min-width="180" />
|
<template #default="{ row }">
|
||||||
<el-table-column label="操作" width="200" fixed="right">
|
<el-button
|
||||||
<template #default="{ row }">
|
link
|
||||||
<el-button
|
type="primary"
|
||||||
link
|
v-perms="['auth.role/edit']"
|
||||||
type="primary"
|
@click="handleEdit(row)"
|
||||||
v-perms="['auth.role/edit']"
|
>
|
||||||
@click="handleEdit(row)"
|
编辑
|
||||||
>
|
</el-button>
|
||||||
编辑
|
<el-button
|
||||||
</el-button>
|
link
|
||||||
<el-button
|
type="primary"
|
||||||
link
|
v-perms="['auth.role/edit']"
|
||||||
type="primary"
|
@click="handleAuth(row)"
|
||||||
v-perms="['auth.role/edit']"
|
>
|
||||||
@click="handleAuth(row)"
|
分配权限
|
||||||
>
|
</el-button>
|
||||||
分配权限
|
<el-button
|
||||||
</el-button>
|
v-perms="['auth.role/delete']"
|
||||||
<el-button
|
link
|
||||||
v-perms="['auth.role/delete']"
|
type="danger"
|
||||||
link
|
@click="handleDelete(row.id)"
|
||||||
type="danger"
|
>
|
||||||
@click="handleDelete(row.id)"
|
删除
|
||||||
>
|
</el-button>
|
||||||
删除
|
</template>
|
||||||
</el-button>
|
</el-table-column>
|
||||||
</template>
|
</el-table>
|
||||||
</el-table-column>
|
<template #footer>
|
||||||
</el-table>
|
<pagination v-model="pager" @change="getLists" />
|
||||||
</div>
|
</template>
|
||||||
<div class="flex justify-end mt-4">
|
</admin-page-data-panel>
|
||||||
<pagination v-model="pager" @change="getLists" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</el-card>
|
|
||||||
<edit-popup v-if="showEdit" ref="editRef" @success="getLists" @close="showEdit = false" />
|
<edit-popup v-if="showEdit" ref="editRef" @success="getLists" @close="showEdit = false" />
|
||||||
<auth-popup v-if="showAuth" ref="authRef" @success="getLists" @close="showAuth = false" />
|
<auth-popup v-if="showAuth" ref="authRef" @success="getLists" @close="showAuth = false" />
|
||||||
</div>
|
</div>
|
||||||
@@ -115,7 +111,6 @@ const dataScopeLabel = (scope: number | string | null | undefined) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 删除角色
|
|
||||||
const handleDelete = async (id: number) => {
|
const handleDelete = async (id: number) => {
|
||||||
await feedback.confirm('确定要删除?')
|
await feedback.confirm('确定要删除?')
|
||||||
await roleDelete({ id })
|
await roleDelete({ id })
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="dict-type">
|
<div>
|
||||||
<el-card class="!border-none" shadow="never">
|
<admin-page-filter-panel>
|
||||||
<el-page-header class="mb-4" content="数据管理" @back="$router.back()" />
|
<el-page-header class="mb-4" content="数据管理" @back="$router.back()" />
|
||||||
<el-form ref="formRef" class="mb-[-16px]" :model="queryParams" inline>
|
<el-form ref="formRef" class="mb-[-16px]" :model="queryParams" inline>
|
||||||
<el-form-item class="w-[280px]" label="字典名称">
|
<el-form-item class="w-[280px]" label="字典名称">
|
||||||
@@ -28,9 +28,10 @@
|
|||||||
<el-button @click="resetParams">重置</el-button>
|
<el-button @click="resetParams">重置</el-button>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
</el-card>
|
</admin-page-filter-panel>
|
||||||
<el-card class="!border-none mt-4" shadow="never">
|
|
||||||
<div>
|
<admin-page-data-panel v-loading="pager.loading">
|
||||||
|
<template #toolbar>
|
||||||
<el-button
|
<el-button
|
||||||
v-perms="['setting.dict.dict_data/add']"
|
v-perms="['setting.dict.dict_data/add']"
|
||||||
type="primary"
|
type="primary"
|
||||||
@@ -52,58 +53,54 @@
|
|||||||
</template>
|
</template>
|
||||||
删除
|
删除
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</template>
|
||||||
<div class="mt-4" v-loading="pager.loading">
|
<el-table
|
||||||
<div>
|
:data="pager.lists"
|
||||||
<el-table
|
size="large"
|
||||||
:data="pager.lists"
|
@selection-change="handleSelectionChange"
|
||||||
size="large"
|
>
|
||||||
@selection-change="handleSelectionChange"
|
<el-table-column type="selection" width="55" />
|
||||||
>
|
<el-table-column label="ID" prop="id" />
|
||||||
<el-table-column type="selection" width="55" />
|
<el-table-column label="数据名称" prop="name" min-width="120" />
|
||||||
<el-table-column label="ID" prop="id" />
|
<el-table-column label="数据值" prop="value" min-width="120" />
|
||||||
<el-table-column label="数据名称" prop="name" min-width="120" />
|
<el-table-column label="状态">
|
||||||
<el-table-column label="数据值" prop="value" min-width="120" />
|
<template v-slot="{ row }">
|
||||||
<el-table-column label="状态">
|
<el-tag v-if="row.status == 1">正常</el-tag>
|
||||||
<template v-slot="{ row }">
|
<el-tag v-else type="danger">停用</el-tag>
|
||||||
<el-tag v-if="row.status == 1">正常</el-tag>
|
</template>
|
||||||
<el-tag v-else type="danger">停用</el-tag>
|
</el-table-column>
|
||||||
</template>
|
<el-table-column
|
||||||
</el-table-column>
|
label="备注"
|
||||||
<el-table-column
|
prop="remark"
|
||||||
label="备注"
|
min-width="120"
|
||||||
prop="remark"
|
show-tooltip-when-overflow
|
||||||
min-width="120"
|
/>
|
||||||
show-tooltip-when-overflow
|
<el-table-column label="排序" prop="sort" />
|
||||||
/>
|
<el-table-column label="操作" width="120" fixed="right">
|
||||||
<el-table-column label="排序" prop="sort" />
|
<template #default="{ row }">
|
||||||
<el-table-column label="操作" width="120" fixed="right">
|
<el-button
|
||||||
<template #default="{ row }">
|
v-perms="['setting.dict.dict_data/edit']"
|
||||||
<el-button
|
link
|
||||||
v-perms="['setting.dict.dict_data/edit']"
|
type="primary"
|
||||||
link
|
@click="handleEdit(row)"
|
||||||
type="primary"
|
>
|
||||||
@click="handleEdit(row)"
|
编辑
|
||||||
>
|
</el-button>
|
||||||
编辑
|
<el-button
|
||||||
</el-button>
|
v-perms="['setting.dict.dict_data/delete']"
|
||||||
<el-button
|
link
|
||||||
v-perms="['setting.dict.dict_data/delete']"
|
type="danger"
|
||||||
link
|
@click="handleDelete(row.id)"
|
||||||
type="danger"
|
>
|
||||||
@click="handleDelete(row.id)"
|
删除
|
||||||
>
|
</el-button>
|
||||||
删除
|
</template>
|
||||||
</el-button>
|
</el-table-column>
|
||||||
</template>
|
</el-table>
|
||||||
</el-table-column>
|
<template #footer>
|
||||||
</el-table>
|
<pagination v-model="pager" @change="getLists" />
|
||||||
</div>
|
</template>
|
||||||
<div class="flex justify-end mt-4">
|
</admin-page-data-panel>
|
||||||
<pagination v-model="pager" @change="getLists" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</el-card>
|
|
||||||
<edit-popup v-if="showEdit" ref="editRef" @success="getLists" @close="showEdit = false" />
|
<edit-popup v-if="showEdit" ref="editRef" @success="getLists" @close="showEdit = false" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="dict-type">
|
<div>
|
||||||
<el-card class="!border-none" shadow="never">
|
<admin-page-filter-panel>
|
||||||
<el-form ref="formRef" class="mb-[-16px]" :model="queryParams" inline>
|
<el-form ref="formRef" class="mb-[-16px]" :model="queryParams" inline>
|
||||||
<el-form-item class="w-[280px]" label="字典名称">
|
<el-form-item class="w-[280px]" label="字典名称">
|
||||||
<el-input v-model="queryParams.name" clearable @keyup.enter="resetPage" />
|
<el-input v-model="queryParams.name" clearable @keyup.enter="resetPage" />
|
||||||
@@ -20,9 +20,10 @@
|
|||||||
<el-button @click="resetParams">重置</el-button>
|
<el-button @click="resetParams">重置</el-button>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
</el-card>
|
</admin-page-filter-panel>
|
||||||
<el-card class="!border-none mt-4" shadow="never">
|
|
||||||
<div>
|
<admin-page-data-panel v-loading="pager.loading">
|
||||||
|
<template #toolbar>
|
||||||
<el-button
|
<el-button
|
||||||
v-perms="['setting.dict.dict_type/add']"
|
v-perms="['setting.dict.dict_type/add']"
|
||||||
type="primary"
|
type="primary"
|
||||||
@@ -44,69 +45,65 @@
|
|||||||
</template>
|
</template>
|
||||||
删除
|
删除
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</template>
|
||||||
<div class="mt-4" v-loading="pager.loading">
|
<el-table
|
||||||
<div>
|
:data="pager.lists"
|
||||||
<el-table
|
size="large"
|
||||||
:data="pager.lists"
|
@selection-change="handleSelectionChange"
|
||||||
size="large"
|
>
|
||||||
@selection-change="handleSelectionChange"
|
<el-table-column type="selection" width="55" />
|
||||||
>
|
<el-table-column label="ID" prop="id" />
|
||||||
<el-table-column type="selection" width="55" />
|
<el-table-column label="字典名称" prop="name" min-width="120" />
|
||||||
<el-table-column label="ID" prop="id" />
|
<el-table-column label="字典类型" prop="type" min-width="120" />
|
||||||
<el-table-column label="字典名称" prop="name" min-width="120" />
|
<el-table-column label="状态">
|
||||||
<el-table-column label="字典类型" prop="type" min-width="120" />
|
<template v-slot="{ row }">
|
||||||
<el-table-column label="状态">
|
<el-tag v-if="row.status == 1">正常</el-tag>
|
||||||
<template v-slot="{ row }">
|
<el-tag v-else type="danger">停用</el-tag>
|
||||||
<el-tag v-if="row.status == 1">正常</el-tag>
|
</template>
|
||||||
<el-tag v-else type="danger">停用</el-tag>
|
</el-table-column>
|
||||||
</template>
|
<el-table-column label="备注" prop="remark" show-tooltip-when-overflow />
|
||||||
</el-table-column>
|
<el-table-column label="创建时间" prop="create_time" min-width="180" />
|
||||||
<el-table-column label="备注" prop="remark" show-tooltip-when-overflow />
|
<el-table-column label="操作" width="190" fixed="right">
|
||||||
<el-table-column label="创建时间" prop="create_time" min-width="180" />
|
<template #default="{ row }">
|
||||||
<el-table-column label="操作" width="190" fixed="right">
|
<el-button
|
||||||
<template #default="{ row }">
|
v-perms="['setting.dict.dict_type/edit']"
|
||||||
<el-button
|
link
|
||||||
v-perms="['setting.dict.dict_type/edit']"
|
type="primary"
|
||||||
link
|
@click="handleEdit(row)"
|
||||||
type="primary"
|
>
|
||||||
@click="handleEdit(row)"
|
编辑
|
||||||
>
|
</el-button>
|
||||||
编辑
|
<el-button
|
||||||
</el-button>
|
v-perms="['setting.dict.dict_data/lists']"
|
||||||
<el-button
|
type="primary"
|
||||||
v-perms="['setting.dict.dict_data/lists']"
|
link
|
||||||
type="primary"
|
>
|
||||||
link
|
<router-link
|
||||||
>
|
:to="{
|
||||||
<router-link
|
path: getRoutePath('setting.dict.dict_data/lists'),
|
||||||
:to="{
|
query: {
|
||||||
path: getRoutePath('setting.dict.dict_data/lists'),
|
id: row.id
|
||||||
query: {
|
}
|
||||||
id: row.id
|
}"
|
||||||
}
|
>
|
||||||
}"
|
数据管理
|
||||||
>
|
</router-link>
|
||||||
数据管理
|
</el-button>
|
||||||
</router-link>
|
<el-button
|
||||||
</el-button>
|
v-perms="['setting.dict.dict_type/delete']"
|
||||||
<el-button
|
link
|
||||||
v-perms="['setting.dict.dict_type/delete']"
|
type="danger"
|
||||||
link
|
@click="handleDelete(row.id)"
|
||||||
type="danger"
|
>
|
||||||
@click="handleDelete(row.id)"
|
删除
|
||||||
>
|
</el-button>
|
||||||
删除
|
</template>
|
||||||
</el-button>
|
</el-table-column>
|
||||||
</template>
|
</el-table>
|
||||||
</el-table-column>
|
<template #footer>
|
||||||
</el-table>
|
<pagination v-model="pager" @change="getLists" />
|
||||||
</div>
|
</template>
|
||||||
<div class="flex justify-end mt-4">
|
</admin-page-data-panel>
|
||||||
<pagination v-model="pager" @change="getLists" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</el-card>
|
|
||||||
<edit-popup v-if="showEdit" ref="editRef" @success="getLists" @close="showEdit = false" />
|
<edit-popup v-if="showEdit" ref="editRef" @success="getLists" @close="showEdit = false" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -151,7 +148,6 @@ const handleEdit = async (data: any) => {
|
|||||||
editRef.value?.setFormData(data)
|
editRef.value?.setFormData(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 删除角色
|
|
||||||
const handleDelete = async (id: any[] | number) => {
|
const handleDelete = async (id: any[] | number) => {
|
||||||
await feedback.confirm('确定要删除?')
|
await feedback.confirm('确定要删除?')
|
||||||
await dictTypeDelete({ id })
|
await dictTypeDelete({ id })
|
||||||
|
|||||||
@@ -1,154 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="auto-assign-log-page">
|
|
||||||
<el-card class="!border-none" shadow="never">
|
|
||||||
<el-form :inline="true" class="log-filter-form">
|
|
||||||
<el-form-item label="执行日期">
|
|
||||||
<el-date-picker
|
|
||||||
v-model="dateRange"
|
|
||||||
type="daterange"
|
|
||||||
range-separator="至"
|
|
||||||
start-placeholder="开始日期"
|
|
||||||
end-placeholder="结束日期"
|
|
||||||
value-format="YYYY-MM-DD"
|
|
||||||
clearable
|
|
||||||
@change="doSearch"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="结果">
|
|
||||||
<el-select v-model="queryParams.action" clearable placeholder="全部" class="w-[120px]" @change="doSearch">
|
|
||||||
<el-option label="已分配" value="1" />
|
|
||||||
<el-option label="未分配" value="0" />
|
|
||||||
</el-select>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="关键词">
|
|
||||||
<el-input
|
|
||||||
v-model="queryParams.keyword"
|
|
||||||
placeholder="患者/手机号/医助/诊单ID"
|
|
||||||
clearable
|
|
||||||
class="w-[220px]"
|
|
||||||
@keyup.enter="doSearch"
|
|
||||||
@clear="doSearch"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item>
|
|
||||||
<el-button type="primary" @click="doSearch">查询</el-button>
|
|
||||||
<el-button @click="handleReset">重置</el-button>
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
<div class="log-hint">
|
|
||||||
由每日定时任务「待分配诊单自动指派」写入:按上月二诊复诊接诊率分档轮询分配(>70% 每人3条/日,60%~70% 每人2条/日,50%~60% 每人1条/日,<50% 不分)。每条待指派诊单一行,含分配 / 不分配原因。
|
|
||||||
</div>
|
|
||||||
</el-card>
|
|
||||||
|
|
||||||
<el-card class="!border-none mt-3" shadow="never">
|
|
||||||
<el-table :data="pager.lists" v-loading="pager.loading" size="default" stripe>
|
|
||||||
<el-table-column label="记录时间" width="160">
|
|
||||||
<template #default="{ row }">{{ row.create_time_text || '—' }}</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="诊单" width="90" prop="diagnosis_id" />
|
|
||||||
<el-table-column label="患者" min-width="110">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<div>{{ row.patient_name || '—' }}</div>
|
|
||||||
<div class="cell-sub">{{ row.patient_phone || '' }}</div>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="结果" width="90" align="center">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<el-tag :type="Number(row.action) === 1 ? 'success' : 'info'" size="small">
|
|
||||||
{{ row.action_text }}
|
|
||||||
</el-tag>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="医助" min-width="110">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<template v-if="Number(row.assistant_id) > 0">
|
|
||||||
<div>{{ row.assistant_name || '#' + row.assistant_id }}</div>
|
|
||||||
<div class="cell-sub">上月二诊接诊率 {{ formatRate(row.visit2_rate) }}</div>
|
|
||||||
</template>
|
|
||||||
<span v-else>—</span>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="档位" width="100" align="center">
|
|
||||||
<template #default="{ row }">{{ row.tier_text || '—' }}</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="轮次" width="70" align="center">
|
|
||||||
<template #default="{ row }">{{ Number(row.round_no) > 0 ? '第' + row.round_no + '轮' : '—' }}</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="原因" min-width="360" show-overflow-tooltip>
|
|
||||||
<template #default="{ row }">{{ row.reason }}</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="统计月" width="90" prop="stat_month" align="center" />
|
|
||||||
<el-table-column label="批次" width="180">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<span class="cell-sub">{{ row.batch_no }}</span>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
<div class="flex justify-end mt-4">
|
|
||||||
<pagination v-model="pager" @change="getLists" />
|
|
||||||
</div>
|
|
||||||
</el-card>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts" name="statsAutoAssignLog">
|
|
||||||
import { autoAssignLogLists } from '@/api/stats'
|
|
||||||
import { usePaging } from '@/hooks/usePaging'
|
|
||||||
import { onMounted, reactive, ref, watch } from 'vue'
|
|
||||||
|
|
||||||
const dateRange = ref<[string, string] | null>(null)
|
|
||||||
|
|
||||||
const queryParams = reactive({
|
|
||||||
start_date: '',
|
|
||||||
end_date: '',
|
|
||||||
action: '' as '' | '0' | '1',
|
|
||||||
keyword: ''
|
|
||||||
})
|
|
||||||
|
|
||||||
watch(dateRange, (val) => {
|
|
||||||
queryParams.start_date = val?.[0] ?? ''
|
|
||||||
queryParams.end_date = val?.[1] ?? ''
|
|
||||||
})
|
|
||||||
|
|
||||||
const { pager, getLists, resetPage } = usePaging({
|
|
||||||
fetchFun: autoAssignLogLists,
|
|
||||||
params: queryParams
|
|
||||||
})
|
|
||||||
|
|
||||||
const doSearch = () => resetPage()
|
|
||||||
|
|
||||||
const handleReset = () => {
|
|
||||||
dateRange.value = null
|
|
||||||
queryParams.start_date = ''
|
|
||||||
queryParams.end_date = ''
|
|
||||||
queryParams.action = ''
|
|
||||||
queryParams.keyword = ''
|
|
||||||
resetPage()
|
|
||||||
}
|
|
||||||
|
|
||||||
const formatRate = (rate: number | null | undefined) =>
|
|
||||||
rate === null || rate === undefined ? '—' : `${rate}%`
|
|
||||||
|
|
||||||
onMounted(() => getLists())
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.auto-assign-log-page {
|
|
||||||
.log-filter-form {
|
|
||||||
:deep(.el-form-item) {
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.log-hint {
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--el-text-color-secondary);
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cell-sub {
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--el-text-color-secondary);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="conversion-stats-page">
|
<div class="conversion-stats-page">
|
||||||
<el-card class="!border-none" shadow="never">
|
<admin-page-filter-panel>
|
||||||
<el-form :inline="true" :model="queryParams" class="stats-filter-form">
|
<el-form :inline="true" :model="queryParams" class="stats-filter-form">
|
||||||
<el-form-item label="统计维度">
|
<el-form-item label="统计维度">
|
||||||
<el-segmented
|
<el-segmented
|
||||||
@@ -89,7 +89,7 @@
|
|||||||
<el-button @click="handleReset">重置</el-button>
|
<el-button @click="handleReset">重置</el-button>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
</el-card>
|
</admin-page-filter-panel>
|
||||||
|
|
||||||
<div class="stats-kpi-grid">
|
<div class="stats-kpi-grid">
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
<el-tree-select
|
<el-tree-select
|
||||||
v-model="deptId"
|
v-model="deptId"
|
||||||
:data="deptTreeOptions"
|
:data="deptTreeOptions"
|
||||||
placeholder="二中心(全部)"
|
placeholder="全部部门"
|
||||||
clearable
|
clearable
|
||||||
filterable
|
filterable
|
||||||
check-strictly
|
check-strictly
|
||||||
@@ -43,10 +43,10 @@
|
|||||||
</template>
|
</template>
|
||||||
<div class="rate-caliber">
|
<div class="rate-caliber">
|
||||||
<p>
|
<p>
|
||||||
<b>当月被指派总数</b>:当月内诊单被指派给医助(按指派操作时间落月,<b>剔除勾选「继承」的指派</b>)的诊单数,按「医助 × 诊单」去重;部门行 / 合计行按诊单去重;<b>再剔除</b>名下存在履约「拒收 / 退款」业务订单的诊单。
|
<b>当月被指派总数</b>:当月内诊单被指派给医助(按指派操作时间落月,<b>剔除勾选「继承」的指派</b>)的诊单数,按「医助 × 诊单」去重;部门行 / 合计行按诊单去重。
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<b>诊次(第 N 次下单)</b>:患者(诊单)名下计入业绩的业务订单(剔除已取消 / 拒收 / 退款)按下单时间升序编号为「实单序号」,<b>统计诊次 = 实单序号 + 诊单偏移</b>(默认偏移 0 → 第 1 笔实单为一诊;偏移 1 → 第 1 笔实单为二诊;偏移 2 → 第 1 笔实单为三诊,5 笔实单等价七诊)。诊次<b>跨月累计不重置</b>。诊单可在「业务订单」tab 配置偏移量。
|
<b>诊次(第 N 次下单)</b>:患者(诊单)名下计入业绩的业务订单(剔除已取消 / 拒收 / 退款)按下单时间升序的全局序号,<b>跨月累计不重置</b>——如 5 月指派后旗下成交 4 单为二诊~五诊,下月再成交即为六诊。
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<b>当月 N 诊单数</b>:当月内下单且诊次为 N 的订单数,归属下单时点<b>持有该患者的医助</b>(指派可在往月;释放后不再归属;「继承」指派会转移持有人但不计被指派数)。
|
<b>当月 N 诊单数</b>:当月内下单且诊次为 N 的订单数,归属下单时点<b>持有该患者的医助</b>(指派可在往月;释放后不再归属;「继承」指派会转移持有人但不计被指派数)。
|
||||||
@@ -55,7 +55,7 @@
|
|||||||
<b>当月 N 诊接诊率</b> = 当月 N 诊单数 ÷ 当月被指派总数。往月指派、当月成交会推高分子,比率可能超过 100%;医助当月无新指派但旗下有成交时,被指派数为 0、比率显示「—」。
|
<b>当月 N 诊接诊率</b> = 当月 N 诊单数 ÷ 当月被指派总数。往月指派、当月成交会推高分子,比率可能超过 100%;医助当月无新指派但旗下有成交时,被指派数为 0、比率显示「—」。
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
医助按人事部门归组;<b>仅统计「二中心」及其组织下级</b>;部门下拉与未选时的默认范围均限定在该子树,选定部门时含其组织下级。
|
医助按人事部门归组;选定部门时含其组织下级。
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</el-popover>
|
</el-popover>
|
||||||
|
|||||||
@@ -83,23 +83,6 @@
|
|||||||
<el-radio-button value="0">未确认</el-radio-button>
|
<el-radio-button value="0">未确认</el-radio-button>
|
||||||
</el-radio-group>
|
</el-radio-group>
|
||||||
</div>
|
</div>
|
||||||
<div class="filter-group">
|
|
||||||
<span class="filter-label">部门</span>
|
|
||||||
<el-tree-select
|
|
||||||
v-model="formData.assistant_dept_id"
|
|
||||||
:data="departmentTreeRaw"
|
|
||||||
class="filter-dept-select"
|
|
||||||
clearable
|
|
||||||
filterable
|
|
||||||
check-strictly
|
|
||||||
:default-expand-all="true"
|
|
||||||
node-key="id"
|
|
||||||
size="small"
|
|
||||||
:props="assistantDeptTreeProps"
|
|
||||||
placeholder="选父级含子级"
|
|
||||||
@change="handleAssistantDeptChange"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-dialog
|
<el-dialog
|
||||||
@@ -559,7 +542,6 @@
|
|||||||
import { usePaging } from '@/hooks/usePaging'
|
import { usePaging } from '@/hooks/usePaging'
|
||||||
import { defineAsyncComponent, onMounted, onUnmounted, watch } from 'vue'
|
import { defineAsyncComponent, onMounted, onUnmounted, watch } from 'vue'
|
||||||
import { appointmentLists, cancelAppointment, completeAppointment, appointmentDetail } from '@/api/doctor'
|
import { appointmentLists, cancelAppointment, completeAppointment, appointmentDetail } from '@/api/doctor'
|
||||||
import { deptAll } from '@/api/org/department'
|
|
||||||
import { getCallSignature, generateMiniProgramQrcode, tcmDiagnosisDetail, prescriptionGetByAppointment } from '@/api/tcm'
|
import { getCallSignature, generateMiniProgramQrcode, tcmDiagnosisDetail, prescriptionGetByAppointment } from '@/api/tcm'
|
||||||
import { getDictData } from '@/api/app'
|
import { getDictData } from '@/api/app'
|
||||||
import { addDoctorNote } from '@/api/patient'
|
import { addDoctorNote } from '@/api/patient'
|
||||||
@@ -621,19 +603,10 @@ const formData = reactive({
|
|||||||
end_date: '',
|
end_date: '',
|
||||||
date_preset: 'today' as '' | 'yesterday' | 'day_before' | 'today' | 'tomorrow' | 'day_after',
|
date_preset: 'today' as '' | 'yesterday' | 'day_before' | 'today' | 'tomorrow' | 'day_after',
|
||||||
diagnosis_confirmed: '' as '' | '0' | '1', // ''=全部 1=已确认 0=未确认
|
diagnosis_confirmed: '' as '' | '0' | '1', // ''=全部 1=已确认 0=未确认
|
||||||
/** 接诊医生 / 诊单医助 / 挂号医助所属部门(选父级含子级) */
|
|
||||||
assistant_dept_id: '' as number | '',
|
|
||||||
/** 为 1 时后端 extend 返回各状态数量,避免额外 4 次列表请求 */
|
/** 为 1 时后端 extend 返回各状态数量,避免额外 4 次列表请求 */
|
||||||
include_status_counts: 0 as 0 | 1
|
include_status_counts: 0 as 0 | 1
|
||||||
})
|
})
|
||||||
|
|
||||||
const departmentTreeRaw = ref<unknown[]>([])
|
|
||||||
const assistantDeptTreeProps = {
|
|
||||||
value: 'id',
|
|
||||||
label: 'name',
|
|
||||||
children: 'children'
|
|
||||||
}
|
|
||||||
|
|
||||||
const activeTab = ref('1')
|
const activeTab = ref('1')
|
||||||
const dateCustomVisible = ref(false)
|
const dateCustomVisible = ref(false)
|
||||||
const statusCount = ref<Record<number, number>>({
|
const statusCount = ref<Record<number, number>>({
|
||||||
@@ -748,12 +721,6 @@ const handleDiagnosisConfirmedChange = () => {
|
|||||||
loadData()
|
loadData()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 部门筛选变更
|
|
||||||
const handleAssistantDeptChange = () => {
|
|
||||||
pager.page = 1
|
|
||||||
loadData()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 快捷日期变更
|
// 快捷日期变更
|
||||||
const handleDatePresetChange = (val: string | number | boolean | undefined) => {
|
const handleDatePresetChange = (val: string | number | boolean | undefined) => {
|
||||||
const v = String(val || '')
|
const v = String(val || '')
|
||||||
@@ -803,7 +770,6 @@ const handleReset = () => {
|
|||||||
formData.doctor_name = ''
|
formData.doctor_name = ''
|
||||||
formData.date_preset = 'today'
|
formData.date_preset = 'today'
|
||||||
formData.diagnosis_confirmed = ''
|
formData.diagnosis_confirmed = ''
|
||||||
formData.assistant_dept_id = ''
|
|
||||||
const t = new Date()
|
const t = new Date()
|
||||||
const p = (n: number) => String(n).padStart(2, '0')
|
const p = (n: number) => String(n).padStart(2, '0')
|
||||||
formData.start_date = `${t.getFullYear()}-${p(t.getMonth() + 1)}-${p(t.getDate())}`
|
formData.start_date = `${t.getFullYear()}-${p(t.getMonth() + 1)}-${p(t.getDate())}`
|
||||||
@@ -1133,13 +1099,7 @@ formData.start_date = `${_today.getFullYear()}-${_pad(_today.getMonth() + 1)}-${
|
|||||||
formData.end_date = formData.start_date
|
formData.end_date = formData.start_date
|
||||||
formData.status = 1
|
formData.status = 1
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(() => {
|
||||||
try {
|
|
||||||
const deptTree = await deptAll()
|
|
||||||
departmentTreeRaw.value = Array.isArray(deptTree) ? deptTree : []
|
|
||||||
} catch {
|
|
||||||
departmentTreeRaw.value = []
|
|
||||||
}
|
|
||||||
loadData()
|
loadData()
|
||||||
listPollTimer = setInterval(() => {
|
listPollTimer = setInterval(() => {
|
||||||
loadData({ silent: true })
|
loadData({ silent: true })
|
||||||
@@ -1268,10 +1228,6 @@ onUnmounted(() => {
|
|||||||
padding: 6px 14px;
|
padding: 6px 14px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-dept-select {
|
|
||||||
width: 200px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,43 +4,6 @@
|
|||||||
<el-empty description="当前诊单未携带患者ID,无法列出业务订单" />
|
<el-empty description="当前诊单未携带患者ID,无法列出业务订单" />
|
||||||
</div>
|
</div>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div v-if="diagnosisId > 0" class="po-revisit-offset-bar mb-3">
|
|
||||||
<div class="po-revisit-offset-bar__main">
|
|
||||||
<span class="text-sm text-gray-600">复诊统计起始偏移</span>
|
|
||||||
<el-tooltip placement="top">
|
|
||||||
<template #content>
|
|
||||||
<div class="max-w-xs leading-relaxed">
|
|
||||||
在实单诊次序号上叠加偏移量。设为 0(默认):第 1 笔实单计为一诊;设为 1:第 1 笔实单计为二诊;设为 2:第 1 笔实单计为三诊——若有 5 笔实单且偏移 2,则统计上相当于计至七诊(5+2)。
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<el-icon class="text-gray-400 align-middle ml-1"><QuestionFilled /></el-icon>
|
|
||||||
</el-tooltip>
|
|
||||||
<el-input-number
|
|
||||||
v-model="revisitSlotStartOffset"
|
|
||||||
v-perms="['tcm.diagnosis/setRevisitSlotStartOffset']"
|
|
||||||
:min="0"
|
|
||||||
:max="20"
|
|
||||||
:step="1"
|
|
||||||
controls-position="right"
|
|
||||||
class="w-[120px] ml-3"
|
|
||||||
:disabled="offsetSaving"
|
|
||||||
/>
|
|
||||||
<span class="text-xs text-gray-500 ml-2">
|
|
||||||
第 1 笔实单计为{{ visitSlotStartLabel }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<el-button
|
|
||||||
v-perms="['tcm.diagnosis/setRevisitSlotStartOffset']"
|
|
||||||
type="primary"
|
|
||||||
size="small"
|
|
||||||
:loading="offsetSaving"
|
|
||||||
:disabled="!offsetDirty"
|
|
||||||
@click="saveRevisitSlotStartOffset"
|
|
||||||
>
|
|
||||||
保存
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<el-table
|
<el-table
|
||||||
v-loading="pager.loading"
|
v-loading="pager.loading"
|
||||||
:data="pager.lists"
|
:data="pager.lists"
|
||||||
@@ -49,23 +12,6 @@
|
|||||||
empty-text="暂无业务订单"
|
empty-text="暂无业务订单"
|
||||||
>
|
>
|
||||||
<el-table-column label="订单编号" prop="order_no" min-width="200" show-overflow-tooltip />
|
<el-table-column label="订单编号" prop="order_no" min-width="200" show-overflow-tooltip />
|
||||||
<el-table-column label="全局诊次" width="96" align="center">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<span v-if="row.global_visit_seq">{{ row.global_visit_seq }}诊</span>
|
|
||||||
<span v-else class="text-gray-400">—</span>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="计入统计" width="96" align="center">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<el-tag
|
|
||||||
v-if="row.counts_for_revisit_rate"
|
|
||||||
type="success"
|
|
||||||
size="small"
|
|
||||||
effect="plain"
|
|
||||||
>是</el-tag>
|
|
||||||
<el-tag v-else type="info" size="small" effect="plain">否</el-tag>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="金额" width="120" align="right">
|
<el-table-column label="金额" width="120" align="right">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<span class="text-red-500 font-semibold">¥{{ formatAmount(row.amount) }}</span>
|
<span class="text-red-500 font-semibold">¥{{ formatAmount(row.amount) }}</span>
|
||||||
@@ -117,11 +63,9 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, reactive, ref, watch } from 'vue'
|
import { computed, reactive, ref, watch } from 'vue'
|
||||||
import { QuestionFilled } from '@element-plus/icons-vue'
|
|
||||||
import { usePaging } from '@/hooks/usePaging'
|
import { usePaging } from '@/hooks/usePaging'
|
||||||
import { prescriptionOrderLists, tcmDiagnosisDetail, tcmDiagnosisSetRevisitSlotStartOffset } from '@/api/tcm'
|
import { prescriptionOrderLists } from '@/api/tcm'
|
||||||
import PrescriptionOrderDetailDrawer from '@/views/consumer/prescription/components/PrescriptionOrderDetailDrawer.vue'
|
import PrescriptionOrderDetailDrawer from '@/views/consumer/prescription/components/PrescriptionOrderDetailDrawer.vue'
|
||||||
import feedback from '@/utils/feedback'
|
|
||||||
import {
|
import {
|
||||||
formatTime,
|
formatTime,
|
||||||
fulfillmentText,
|
fulfillmentText,
|
||||||
@@ -147,26 +91,6 @@ const { pager, getLists, resetPage } = usePaging({
|
|||||||
size: 10
|
size: 10
|
||||||
})
|
})
|
||||||
|
|
||||||
const revisitSlotStartOffset = ref(0)
|
|
||||||
const savedRevisitSlotStartOffset = ref(0)
|
|
||||||
const offsetSaving = ref(false)
|
|
||||||
const offsetLoading = ref(false)
|
|
||||||
|
|
||||||
const offsetDirty = computed(
|
|
||||||
() => Number(revisitSlotStartOffset.value) !== Number(savedRevisitSlotStartOffset.value)
|
|
||||||
)
|
|
||||||
|
|
||||||
const visitSlotStartLabel = computed(() => {
|
|
||||||
const raw = Number(revisitSlotStartOffset.value)
|
|
||||||
const offset = Number.isFinite(raw) ? raw : 0
|
|
||||||
const slot = offset + 1
|
|
||||||
const cn = ['', '一', '二', '三', '四', '五', '六', '七', '八', '九', '十']
|
|
||||||
if (slot >= 1 && slot <= 10) {
|
|
||||||
return cn[slot] + '诊'
|
|
||||||
}
|
|
||||||
return `第${slot}诊`
|
|
||||||
})
|
|
||||||
|
|
||||||
const buildParams = () => {
|
const buildParams = () => {
|
||||||
Object.keys(queryParams).forEach((k) => delete queryParams[k])
|
Object.keys(queryParams).forEach((k) => delete queryParams[k])
|
||||||
if (props.diagnosisId > 0) {
|
if (props.diagnosisId > 0) {
|
||||||
@@ -178,47 +102,6 @@ const buildParams = () => {
|
|||||||
queryParams.scene = 'diagnosis_edit'
|
queryParams.scene = 'diagnosis_edit'
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadRevisitSlotStartOffset() {
|
|
||||||
if (props.diagnosisId <= 0) return
|
|
||||||
offsetLoading.value = true
|
|
||||||
try {
|
|
||||||
const res: any = await tcmDiagnosisDetail({ id: props.diagnosisId })
|
|
||||||
const d = res?.data ?? res ?? {}
|
|
||||||
const offset = Number(d.revisit_slot_start_offset)
|
|
||||||
const val = Number.isFinite(offset) && offset >= 0 && offset <= 20 ? offset : 0
|
|
||||||
revisitSlotStartOffset.value = val
|
|
||||||
savedRevisitSlotStartOffset.value = val
|
|
||||||
} catch {
|
|
||||||
revisitSlotStartOffset.value = 0
|
|
||||||
savedRevisitSlotStartOffset.value = 0
|
|
||||||
} finally {
|
|
||||||
offsetLoading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function saveRevisitSlotStartOffset() {
|
|
||||||
if (props.diagnosisId <= 0) return
|
|
||||||
const offset = Number(revisitSlotStartOffset.value)
|
|
||||||
if (!Number.isFinite(offset) || offset < 0 || offset > 20) {
|
|
||||||
feedback.msgWarning('起始偏移须在 0~20 之间')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
offsetSaving.value = true
|
|
||||||
try {
|
|
||||||
await tcmDiagnosisSetRevisitSlotStartOffset({
|
|
||||||
id: props.diagnosisId,
|
|
||||||
revisit_slot_start_offset: offset
|
|
||||||
})
|
|
||||||
savedRevisitSlotStartOffset.value = offset
|
|
||||||
feedback.msgSuccess('保存成功')
|
|
||||||
getLists()
|
|
||||||
} catch {
|
|
||||||
/* 拦截器已提示 */
|
|
||||||
} finally {
|
|
||||||
offsetSaving.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── 详情抽屉(共享组件,数据拉取/展示全部在组件内) ───
|
// ─── 详情抽屉(共享组件,数据拉取/展示全部在组件内) ───
|
||||||
const detailDrawerRef = ref<InstanceType<typeof PrescriptionOrderDetailDrawer>>()
|
const detailDrawerRef = ref<InstanceType<typeof PrescriptionOrderDetailDrawer>>()
|
||||||
|
|
||||||
@@ -234,13 +117,8 @@ const formatAmount = (value: unknown) => {
|
|||||||
watch(
|
watch(
|
||||||
() => [props.diagnosisId, patientIdNum.value] as const,
|
() => [props.diagnosisId, patientIdNum.value] as const,
|
||||||
() => {
|
() => {
|
||||||
if (!patientIdAvailable.value) {
|
if (!patientIdAvailable.value) { pager.lists = []; pager.count = 0; return }
|
||||||
pager.lists = []
|
|
||||||
pager.count = 0
|
|
||||||
return
|
|
||||||
}
|
|
||||||
buildParams()
|
buildParams()
|
||||||
void loadRevisitSlotStartOffset()
|
|
||||||
resetPage()
|
resetPage()
|
||||||
},
|
},
|
||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
@@ -256,21 +134,4 @@ defineExpose({ refresh: () => getLists() })
|
|||||||
.po-empty-tip {
|
.po-empty-tip {
|
||||||
padding: 24px 0;
|
padding: 24px 0;
|
||||||
}
|
}
|
||||||
.po-revisit-offset-bar {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 12px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
padding: 12px 14px;
|
|
||||||
border: 1px solid var(--el-border-color-lighter);
|
|
||||||
border-radius: 8px;
|
|
||||||
background: var(--el-fill-color-lighter);
|
|
||||||
}
|
|
||||||
.po-revisit-offset-bar__main {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 4px;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -121,16 +121,6 @@
|
|||||||
end-placeholder="最近挂号结束"
|
end-placeholder="最近挂号结束"
|
||||||
@change="handleLatestAppointmentFilterChange"
|
@change="handleLatestAppointmentFilterChange"
|
||||||
/>
|
/>
|
||||||
<daterange-picker
|
|
||||||
class="latest-assign-range"
|
|
||||||
v-model:startTime="formData.latest_assign_start_date"
|
|
||||||
v-model:endTime="formData.latest_assign_end_date"
|
|
||||||
picker-type="daterange"
|
|
||||||
value-format="YYYY-MM-DD"
|
|
||||||
start-placeholder="最近指派开始"
|
|
||||||
end-placeholder="最近指派结束"
|
|
||||||
@change="handleLatestAssignFilterChange"
|
|
||||||
/>
|
|
||||||
<el-select
|
<el-select
|
||||||
v-model="formData.latest_appointment_channel_source"
|
v-model="formData.latest_appointment_channel_source"
|
||||||
placeholder="最近挂号渠道"
|
placeholder="最近挂号渠道"
|
||||||
@@ -183,7 +173,6 @@
|
|||||||
v-loading="pager.loading"
|
v-loading="pager.loading"
|
||||||
@selection-change="handleSelectionChange"
|
@selection-change="handleSelectionChange"
|
||||||
@row-dblclick="goReadonly"
|
@row-dblclick="goReadonly"
|
||||||
@sort-change="handleTableSortChange"
|
|
||||||
:row-class-name="getRowClassName"
|
:row-class-name="getRowClassName"
|
||||||
class="diagnosis-table"
|
class="diagnosis-table"
|
||||||
stripe
|
stripe
|
||||||
@@ -294,14 +283,7 @@
|
|||||||
<span v-else class="status-unprescribed">未开方</span>
|
<span v-else class="status-unprescribed">未开方</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column
|
<el-table-column label="未服务天数" width="110" align="center">
|
||||||
label="未服务天数"
|
|
||||||
prop="unserved_days"
|
|
||||||
width="110"
|
|
||||||
align="center"
|
|
||||||
sortable="custom"
|
|
||||||
:sort-orders="['descending', 'ascending']"
|
|
||||||
>
|
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-tooltip
|
<el-tooltip
|
||||||
v-if="row.last_blood_record_at"
|
v-if="row.last_blood_record_at"
|
||||||
@@ -780,10 +762,6 @@ const formData = reactive({
|
|||||||
latest_appointment_start_date: '' as string,
|
latest_appointment_start_date: '' as string,
|
||||||
latest_appointment_end_date: '' as string,
|
latest_appointment_end_date: '' as string,
|
||||||
latest_appointment_channel_source: '' as string,
|
latest_appointment_channel_source: '' as string,
|
||||||
latest_assign_start_date: '' as string,
|
|
||||||
latest_assign_end_date: '' as string,
|
|
||||||
/** 未服务天数排序:desc=天数多到少 asc=少到多 */
|
|
||||||
sort_unserved_days: '' as '' | 'asc' | 'desc',
|
|
||||||
diagnosis_confirmed: '' as '' | '0' | '1',
|
diagnosis_confirmed: '' as '' | '0' | '1',
|
||||||
appointment_date: '' as string,
|
appointment_date: '' as string,
|
||||||
has_appointment: '' as '' | '0' | '1',
|
has_appointment: '' as '' | '0' | '1',
|
||||||
@@ -871,50 +849,22 @@ function resolvePendingAssignOrderMonthForRequest(): string {
|
|||||||
return dayjs().format('YYYY-MM')
|
return dayjs().format('YYYY-MM')
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 除顶部 Tab 专属条件外,与主列表共用的「更多筛选」参数(角标 count 需同步) */
|
|
||||||
function buildSharedDiagnosisFilterPayload(): Record<string, unknown> {
|
|
||||||
return {
|
|
||||||
keyword: formData.keyword,
|
|
||||||
diagnosis_type: formData.diagnosis_type,
|
|
||||||
syndrome_type: formData.syndrome_type,
|
|
||||||
assistant_id: formData.assistant_id,
|
|
||||||
diagnosis_confirmed: formData.diagnosis_confirmed,
|
|
||||||
has_appointment: formData.has_appointment,
|
|
||||||
latest_appointment_start_date: formData.latest_appointment_start_date,
|
|
||||||
latest_appointment_end_date: formData.latest_appointment_end_date,
|
|
||||||
latest_appointment_channel_source: formData.latest_appointment_channel_source,
|
|
||||||
latest_assign_start_date: formData.latest_assign_start_date,
|
|
||||||
latest_assign_end_date: formData.latest_assign_end_date
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 顶部 Tab 角标 count 请求:带上共用筛选,再叠加各 Tab 专属条件 */
|
|
||||||
function buildDateCountRequestPayload(overrides: Record<string, unknown> = {}): Record<string, unknown> {
|
|
||||||
return {
|
|
||||||
page_no: 1,
|
|
||||||
page_size: 1,
|
|
||||||
...buildSharedDiagnosisFilterPayload(),
|
|
||||||
appointment_date: '',
|
|
||||||
pending_booking: '',
|
|
||||||
completed_appointment: '',
|
|
||||||
pending_assign: '',
|
|
||||||
pending_assign_order_month: '',
|
|
||||||
pending_assign_keyword: '',
|
|
||||||
sort_unserved_days: '',
|
|
||||||
...overrides
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 待分配角标 count 请求:与列表同条件,且去掉其它顶部 Tab 残留(如默认「当天挂号」) */
|
/** 待分配角标 count 请求:与列表同条件,且去掉其它顶部 Tab 残留(如默认「当天挂号」) */
|
||||||
function buildPendingAssignCountPayload(): Record<string, unknown> {
|
function buildPendingAssignCountPayload(): Record<string, unknown> {
|
||||||
return buildTcmDiagnosisListRequestPayload(
|
return buildTcmDiagnosisListRequestPayload({
|
||||||
buildDateCountRequestPayload({
|
...formData,
|
||||||
pending_assign: 1,
|
page_no: 1,
|
||||||
has_appointment: '',
|
page_size: 1,
|
||||||
pending_assign_order_month: resolvePendingAssignOrderMonthForRequest(),
|
pending_assign: 1,
|
||||||
pending_assign_keyword: formData.pending_assign_keyword
|
appointment_date: '',
|
||||||
}) as Record<string, unknown>
|
has_appointment: '',
|
||||||
) as Record<string, unknown>
|
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>
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchTcmDiagnosisListsForPaging = (req: Record<string, unknown>) =>
|
const fetchTcmDiagnosisListsForPaging = (req: Record<string, unknown>) =>
|
||||||
@@ -943,9 +893,6 @@ function clearSecondaryFiltersWhenPendingAssignWideSearch() {
|
|||||||
formData.latest_appointment_start_date = ''
|
formData.latest_appointment_start_date = ''
|
||||||
formData.latest_appointment_end_date = ''
|
formData.latest_appointment_end_date = ''
|
||||||
formData.latest_appointment_channel_source = ''
|
formData.latest_appointment_channel_source = ''
|
||||||
formData.latest_assign_start_date = ''
|
|
||||||
formData.latest_assign_end_date = ''
|
|
||||||
formData.sort_unserved_days = ''
|
|
||||||
formData.pending_assign_order_month = ''
|
formData.pending_assign_order_month = ''
|
||||||
activeTab.value = 'all'
|
activeTab.value = 'all'
|
||||||
if (kw1 !== '') {
|
if (kw1 !== '') {
|
||||||
@@ -1047,26 +994,14 @@ const onPendingAssignOrderMonthChange = async (val: string | null) => {
|
|||||||
const fetchDateCounts = async () => {
|
const fetchDateCounts = async () => {
|
||||||
try {
|
try {
|
||||||
const [yesterday, dayBefore, today, tomorrow, dayAfter, all, noApt, doneVisit, pending] = await Promise.all([
|
const [yesterday, dayBefore, today, tomorrow, dayAfter, all, noApt, doneVisit, pending] = await Promise.all([
|
||||||
tcmDiagnosisLists(
|
tcmDiagnosisLists({ appointment_date: yesterdayStr.value, page_no: 1, page_size: 1 }),
|
||||||
buildDateCountRequestPayload({ appointment_date: yesterdayStr.value, has_appointment: '' }) as any
|
tcmDiagnosisLists({ appointment_date: dayBeforeStr.value, page_no: 1, page_size: 1 }),
|
||||||
),
|
tcmDiagnosisLists({ appointment_date: todayStr.value, page_no: 1, page_size: 1 }),
|
||||||
tcmDiagnosisLists(
|
tcmDiagnosisLists({ appointment_date: tomorrowStr.value, page_no: 1, page_size: 1 }),
|
||||||
buildDateCountRequestPayload({ appointment_date: dayBeforeStr.value, has_appointment: '' }) as any
|
tcmDiagnosisLists({ appointment_date: dayAfterStr.value, page_no: 1, page_size: 1 }),
|
||||||
),
|
tcmDiagnosisLists({ page_no: 1, page_size: 1 }),
|
||||||
tcmDiagnosisLists(
|
tcmDiagnosisLists({ has_appointment: 0, page_no: 1, page_size: 1 }),
|
||||||
buildDateCountRequestPayload({ appointment_date: todayStr.value, has_appointment: '' }) as any
|
tcmDiagnosisLists({ completed_appointment: 1, page_no: 1, page_size: 1 }),
|
||||||
),
|
|
||||||
tcmDiagnosisLists(
|
|
||||||
buildDateCountRequestPayload({ appointment_date: tomorrowStr.value, has_appointment: '' }) as any
|
|
||||||
),
|
|
||||||
tcmDiagnosisLists(
|
|
||||||
buildDateCountRequestPayload({ appointment_date: dayAfterStr.value, has_appointment: '' }) as any
|
|
||||||
),
|
|
||||||
tcmDiagnosisLists(buildDateCountRequestPayload() as any),
|
|
||||||
tcmDiagnosisLists(buildDateCountRequestPayload({ has_appointment: 0 }) as any),
|
|
||||||
tcmDiagnosisLists(
|
|
||||||
buildDateCountRequestPayload({ completed_appointment: 1, has_appointment: '' }) as any
|
|
||||||
),
|
|
||||||
tcmDiagnosisLists(buildPendingAssignCountPayload() as any)
|
tcmDiagnosisLists(buildPendingAssignCountPayload() as any)
|
||||||
])
|
])
|
||||||
dateCounts.value = {
|
dateCounts.value = {
|
||||||
@@ -1200,11 +1135,6 @@ const clearLatestAppointmentFilters = () => {
|
|||||||
formData.latest_appointment_channel_source = ''
|
formData.latest_appointment_channel_source = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
const clearLatestAssignFilters = () => {
|
|
||||||
formData.latest_assign_start_date = ''
|
|
||||||
formData.latest_assign_end_date = ''
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasLatestAppointmentFilter = () =>
|
const hasLatestAppointmentFilter = () =>
|
||||||
!!(
|
!!(
|
||||||
formData.latest_appointment_start_date ||
|
formData.latest_appointment_start_date ||
|
||||||
@@ -1212,9 +1142,6 @@ const hasLatestAppointmentFilter = () =>
|
|||||||
formData.latest_appointment_channel_source
|
formData.latest_appointment_channel_source
|
||||||
)
|
)
|
||||||
|
|
||||||
const hasLatestAssignFilter = () =>
|
|
||||||
!!(formData.latest_assign_start_date || formData.latest_assign_end_date)
|
|
||||||
|
|
||||||
const handleLatestAppointmentFilterChange = () => {
|
const handleLatestAppointmentFilterChange = () => {
|
||||||
if (hasLatestAppointmentFilter()) {
|
if (hasLatestAppointmentFilter()) {
|
||||||
formData.appointment_date = ''
|
formData.appointment_date = ''
|
||||||
@@ -1227,27 +1154,6 @@ const handleLatestAppointmentFilterChange = () => {
|
|||||||
doSearch()
|
doSearch()
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleLatestAssignFilterChange = () => {
|
|
||||||
doSearch()
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleTableSortChange = ({
|
|
||||||
prop,
|
|
||||||
order
|
|
||||||
}: {
|
|
||||||
prop: string
|
|
||||||
order: 'ascending' | 'descending' | null
|
|
||||||
}) => {
|
|
||||||
if (prop === 'unserved_days') {
|
|
||||||
formData.sort_unserved_days =
|
|
||||||
order === 'ascending' ? 'asc' : order === 'descending' ? 'desc' : ''
|
|
||||||
} else {
|
|
||||||
formData.sort_unserved_days = ''
|
|
||||||
}
|
|
||||||
pager.page = 1
|
|
||||||
getLists()
|
|
||||||
}
|
|
||||||
|
|
||||||
const latestAppointmentChannelText = (row: any) => {
|
const latestAppointmentChannelText = (row: any) => {
|
||||||
const desc = String(row?.latest_appointment_channel_source_desc || '').trim()
|
const desc = String(row?.latest_appointment_channel_source_desc || '').trim()
|
||||||
const raw = String(row?.latest_appointment_channel_source || '').trim()
|
const raw = String(row?.latest_appointment_channel_source || '').trim()
|
||||||
@@ -1294,9 +1200,6 @@ const handleReset = () => {
|
|||||||
formData.latest_appointment_start_date = ''
|
formData.latest_appointment_start_date = ''
|
||||||
formData.latest_appointment_end_date = ''
|
formData.latest_appointment_end_date = ''
|
||||||
formData.latest_appointment_channel_source = ''
|
formData.latest_appointment_channel_source = ''
|
||||||
formData.latest_assign_start_date = ''
|
|
||||||
formData.latest_assign_end_date = ''
|
|
||||||
formData.sort_unserved_days = ''
|
|
||||||
formData.diagnosis_confirmed = ''
|
formData.diagnosis_confirmed = ''
|
||||||
formData.appointment_date = ''
|
formData.appointment_date = ''
|
||||||
formData.has_appointment = ''
|
formData.has_appointment = ''
|
||||||
@@ -2395,11 +2298,6 @@ onUnmounted(() => {
|
|||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.latest-assign-range {
|
|
||||||
width: 260px;
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.latest-appointment-channel {
|
.latest-appointment-channel {
|
||||||
width: 170px;
|
width: 170px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
</span>
|
</span>
|
||||||
<div>
|
<div>
|
||||||
<div class="wb-section-name">订单统计</div>
|
<div class="wb-section-name">订单统计</div>
|
||||||
<div class="wb-section-sub">{{ orderStatsDateRangeText }} · {{ orderStatsData.order_type_name || '—' }}</div>
|
<div class="wb-section-sub">{{ orderStatsDateRangeText }} · {{ orderStatsData.order_type_name || '-' }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="wb-toolbar">
|
<div class="wb-toolbar">
|
||||||
@@ -821,8 +821,8 @@ onMounted(() => {
|
|||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.workbench-page {
|
.workbench-page {
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
padding: 20px 20px 40px;
|
padding: 4px 0 24px;
|
||||||
background: linear-gradient(160deg, #eef2ff 0%, #f8fafc 38%, #f1f5f9 100%);
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wb-hero {
|
.wb-hero {
|
||||||
@@ -830,56 +830,86 @@ onMounted(() => {
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 16px;
|
gap: 20px;
|
||||||
margin-bottom: 22px;
|
margin-bottom: 24px;
|
||||||
padding: 22px 26px;
|
padding: 28px 28px;
|
||||||
border-radius: 20px;
|
border-radius: var(--admin-radius-xl);
|
||||||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.92) 0%, rgba(255, 255, 255, 0.65) 100%);
|
border: 1px solid var(--el-border-color-lighter);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.95);
|
background: var(--admin-surface-glass);
|
||||||
box-shadow: 0 12px 40px rgba(15, 23, 42, 0.06);
|
backdrop-filter: blur(16px);
|
||||||
|
box-shadow: var(--el-box-shadow-light);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background:
|
||||||
|
linear-gradient(120deg, rgba(6, 182, 212, 0.14) 0%, transparent 45%),
|
||||||
|
linear-gradient(300deg, rgba(16, 185, 129, 0.1) 0%, transparent 40%);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0 0 auto;
|
||||||
|
height: 3px;
|
||||||
|
background: var(--admin-brand-gradient);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.wb-hero-text {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wb-hero-title {
|
.wb-hero-title {
|
||||||
margin: 0 0 6px;
|
margin: 0 0 8px;
|
||||||
font-size: 26px;
|
font-size: 28px;
|
||||||
font-weight: 700;
|
font-weight: 800;
|
||||||
letter-spacing: 0.02em;
|
letter-spacing: -0.03em;
|
||||||
color: #0f172a;
|
background: var(--admin-brand-gradient);
|
||||||
|
background-clip: text;
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wb-hero-desc {
|
.wb-hero-desc {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 14px;
|
font-size: 15px;
|
||||||
color: #64748b;
|
color: var(--el-text-color-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.wb-refresh-btn {
|
.wb-refresh-btn {
|
||||||
box-shadow: 0 8px 20px rgba(59, 130, 246, 0.25);
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
box-shadow: var(--admin-brand-glow);
|
||||||
}
|
}
|
||||||
|
|
||||||
.wb-section {
|
.wb-section {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
border-radius: 20px;
|
border-radius: var(--admin-radius-xl);
|
||||||
background: rgba(255, 255, 255, 0.88);
|
background: var(--admin-surface-elevated);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.9);
|
border: 1px solid var(--el-border-color-lighter);
|
||||||
box-shadow: 0 8px 32px rgba(15, 23, 42, 0.05);
|
box-shadow: var(--el-box-shadow);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
backdrop-filter: blur(10px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.wb-section--diagnosis {
|
.wb-section--diagnosis {
|
||||||
border-top: 3px solid transparent;
|
border-top: 3px solid transparent;
|
||||||
border-image: linear-gradient(90deg, #3b82f6, #60a5fa) 1;
|
border-image: linear-gradient(90deg, #06b6d4, #10b981) 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wb-section--order {
|
.wb-section--order {
|
||||||
border-top: 3px solid transparent;
|
border-top: 3px solid transparent;
|
||||||
border-image: linear-gradient(90deg, #8b5cf6, #a78bfa) 1;
|
border-image: linear-gradient(90deg, #0891b2, #6366f1) 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wb-section--trend {
|
.wb-section--trend {
|
||||||
border-top: 3px solid transparent;
|
border-top: 3px solid transparent;
|
||||||
border-image: linear-gradient(90deg, #14b8a6, #2dd4bf) 1;
|
border-image: linear-gradient(90deg, #10b981, #06b6d4) 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wb-section-head {
|
.wb-section-head {
|
||||||
@@ -889,8 +919,8 @@ onMounted(() => {
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
padding: 18px 22px;
|
padding: 18px 22px;
|
||||||
border-bottom: 1px solid rgba(226, 232, 240, 0.9);
|
border-bottom: 1px solid var(--el-border-color-extra-light);
|
||||||
background: linear-gradient(180deg, rgba(248, 250, 252, 0.9) 0%, rgba(255, 255, 255, 0) 100%);
|
background: var(--admin-brand-gradient-soft);
|
||||||
}
|
}
|
||||||
|
|
||||||
.wb-section-title {
|
.wb-section-title {
|
||||||
@@ -903,38 +933,68 @@ onMounted(() => {
|
|||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 44px;
|
width: 46px;
|
||||||
height: 44px;
|
height: 46px;
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
font-size: 22px;
|
font-size: 22px;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wb-section-icon--blue {
|
.wb-section-icon--blue {
|
||||||
background: linear-gradient(135deg, #3b82f6, #2563eb);
|
background: var(--admin-brand-gradient);
|
||||||
box-shadow: 0 8px 20px rgba(37, 99, 235, 0.35);
|
box-shadow: var(--admin-brand-glow);
|
||||||
}
|
}
|
||||||
|
|
||||||
.wb-section-icon--violet {
|
.wb-section-icon--violet {
|
||||||
background: linear-gradient(135deg, #8b5cf6, #7c3aed);
|
background: linear-gradient(135deg, #0891b2, #6366f1);
|
||||||
box-shadow: 0 8px 20px rgba(124, 58, 237, 0.3);
|
box-shadow: 0 8px 24px rgba(99, 102, 241, 0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
.wb-section-icon--teal {
|
.wb-section-icon--teal {
|
||||||
background: linear-gradient(135deg, #14b8a6, #0d9488);
|
background: linear-gradient(135deg, #10b981, #06b6d4);
|
||||||
box-shadow: 0 8px 20px rgba(13, 148, 136, 0.3);
|
box-shadow: 0 8px 24px rgba(16, 185, 129, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wb-kpi--blue {
|
||||||
|
background: linear-gradient(145deg, rgba(6, 182, 212, 0.12), rgba(16, 185, 129, 0.06));
|
||||||
|
border: 1px solid rgba(6, 182, 212, 0.22);
|
||||||
|
box-shadow: var(--el-box-shadow-lighter);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wb-kpi--violet {
|
||||||
|
background: linear-gradient(145deg, rgba(99, 102, 241, 0.1), rgba(6, 182, 212, 0.06));
|
||||||
|
border: 1px solid rgba(99, 102, 241, 0.2);
|
||||||
|
box-shadow: var(--el-box-shadow-lighter);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wb-kpi--amber {
|
||||||
|
background: linear-gradient(145deg, rgba(245, 158, 11, 0.12), rgba(251, 191, 36, 0.06));
|
||||||
|
border: 1px solid rgba(245, 158, 11, 0.24);
|
||||||
|
box-shadow: var(--el-box-shadow-lighter);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wb-rank-strip {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 112px;
|
||||||
|
padding: 14px 18px;
|
||||||
|
border-radius: var(--admin-radius-lg);
|
||||||
|
background: var(--admin-brand-gradient-soft);
|
||||||
|
border: 1px solid var(--el-border-color-lighter);
|
||||||
|
box-shadow: var(--el-box-shadow-lighter);
|
||||||
}
|
}
|
||||||
|
|
||||||
.wb-section-name {
|
.wb-section-name {
|
||||||
font-size: 17px;
|
font-size: 16px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #0f172a;
|
color: var(--el-text-color-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.wb-section-sub {
|
.wb-section-sub {
|
||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #94a3b8;
|
color: var(--el-text-color-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.wb-toolbar {
|
.wb-toolbar {
|
||||||
@@ -968,21 +1028,6 @@ onMounted(() => {
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wb-kpi--blue {
|
|
||||||
background: linear-gradient(145deg, #eff6ff 0%, #dbeafe 100%);
|
|
||||||
border: 1px solid rgba(59, 130, 246, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.wb-kpi--violet {
|
|
||||||
background: linear-gradient(145deg, #f5f3ff 0%, #ede9fe 100%);
|
|
||||||
border: 1px solid rgba(139, 92, 246, 0.22);
|
|
||||||
}
|
|
||||||
|
|
||||||
.wb-kpi--amber {
|
|
||||||
background: linear-gradient(145deg, #fffbeb 0%, #fef3c7 100%);
|
|
||||||
border: 1px solid rgba(245, 158, 11, 0.25);
|
|
||||||
}
|
|
||||||
|
|
||||||
.wb-kpi-label {
|
.wb-kpi-label {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: #64748b;
|
color: #64748b;
|
||||||
@@ -1007,17 +1052,6 @@ onMounted(() => {
|
|||||||
color: #94a3b8;
|
color: #94a3b8;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wb-rank-strip {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
min-height: 112px;
|
|
||||||
padding: 14px 18px;
|
|
||||||
border-radius: 16px;
|
|
||||||
background: #f8fafc;
|
|
||||||
border: 1px solid #e2e8f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wb-rank-strip--compact {
|
.wb-rank-strip--compact {
|
||||||
min-height: 112px;
|
min-height: 112px;
|
||||||
}
|
}
|
||||||
@@ -1080,9 +1114,10 @@ onMounted(() => {
|
|||||||
.wb-chart-card {
|
.wb-chart-card {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding: 14px 16px 8px;
|
padding: 14px 16px 8px;
|
||||||
border-radius: 16px;
|
border-radius: var(--admin-radius-lg);
|
||||||
background: #fafbfc;
|
background: var(--admin-surface-elevated);
|
||||||
border: 1px solid #eef0f4;
|
border: 1px solid var(--el-border-color-lighter);
|
||||||
|
box-shadow: var(--el-box-shadow-lighter);
|
||||||
}
|
}
|
||||||
|
|
||||||
.wb-chart-card-head {
|
.wb-chart-card-head {
|
||||||
|
|||||||
@@ -77,7 +77,22 @@ module.exports = {
|
|||||||
mask: 'var(--el-mask-color)'
|
mask: 'var(--el-mask-color)'
|
||||||
},
|
},
|
||||||
fontFamily: {
|
fontFamily: {
|
||||||
sans: ['PingFang SC', 'Arial', 'Hiragino Sans GB', 'Microsoft YaHei', 'sans-serif']
|
sans: [
|
||||||
|
'PingFang SC',
|
||||||
|
'SF Pro Text',
|
||||||
|
'Segoe UI',
|
||||||
|
'Arial',
|
||||||
|
'Hiragino Sans GB',
|
||||||
|
'Microsoft YaHei',
|
||||||
|
'sans-serif'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
borderRadius: {
|
||||||
|
sm: 'var(--admin-radius-sm)',
|
||||||
|
DEFAULT: 'var(--admin-radius-md)',
|
||||||
|
md: 'var(--admin-radius-md)',
|
||||||
|
lg: 'var(--admin-radius-lg)',
|
||||||
|
xl: 'var(--admin-radius-xl)'
|
||||||
},
|
},
|
||||||
boxShadow: {
|
boxShadow: {
|
||||||
DEFAULT: 'var(--el-box-shadow)',
|
DEFAULT: 'var(--el-box-shadow)',
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1,21 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace app\adminapi\controller\stats;
|
|
||||||
|
|
||||||
use app\adminapi\controller\BaseAdminController;
|
|
||||||
use app\adminapi\lists\stats\AutoAssignLogLists;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 待分配诊单自动指派日志
|
|
||||||
*
|
|
||||||
* - GET stats.autoAssignLog/lists 日志列表(每条待指派诊单一行:分配结果 + 原因)
|
|
||||||
*/
|
|
||||||
class AutoAssignLogController extends BaseAdminController
|
|
||||||
{
|
|
||||||
public function lists()
|
|
||||||
{
|
|
||||||
return $this->dataLists(new AutoAssignLogLists());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -94,24 +94,6 @@ class DiagnosisController extends BaseAdminController
|
|||||||
return $this->fail(DiagnosisLogic::getError());
|
return $this->fail(DiagnosisLogic::getError());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @notes 设置复诊接诊率统计起始偏移(业务订单 tab)
|
|
||||||
*/
|
|
||||||
public function setRevisitSlotStartOffset()
|
|
||||||
{
|
|
||||||
$params = (new DiagnosisValidate())->post()->goCheck('setRevisitSlotStartOffset');
|
|
||||||
$ok = DiagnosisLogic::setRevisitSlotStartOffset(
|
|
||||||
(int) $params['id'],
|
|
||||||
(int) $params['revisit_slot_start_offset'],
|
|
||||||
$this->adminInfo
|
|
||||||
);
|
|
||||||
if (!$ok) {
|
|
||||||
return $this->fail(DiagnosisLogic::getError());
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->success('保存成功');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @notes 删除诊单
|
* @notes 删除诊单
|
||||||
* @return \think\response\Json
|
* @return \think\response\Json
|
||||||
|
|||||||
@@ -181,20 +181,6 @@ class PrescriptionOrderController extends BaseAdminController
|
|||||||
return $this->success('保存成功');
|
return $this->success('保存成功');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 修改关联处方服用参数(主方/辅方次数与开立天数)及订单服用天数
|
|
||||||
*/
|
|
||||||
public function patchPrescriptionUsage()
|
|
||||||
{
|
|
||||||
$params = (new PrescriptionOrderValidate())->post()->goCheck('patchPrescriptionUsage');
|
|
||||||
$ok = PrescriptionOrderLogic::patchPrescriptionUsage($params, $this->adminId, $this->adminInfo);
|
|
||||||
if (!$ok) {
|
|
||||||
return $this->fail(PrescriptionOrderLogic::getError());
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->success('保存成功');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function auditPrescription()
|
public function auditPrescription()
|
||||||
{
|
{
|
||||||
$params = (new PrescriptionOrderValidate())->post()->goCheck('auditPrescription');
|
$params = (new PrescriptionOrderValidate())->post()->goCheck('auditPrescription');
|
||||||
@@ -342,20 +328,6 @@ class PrescriptionOrderController extends BaseAdminController
|
|||||||
return $this->success('', $result);
|
return $this->success('', $result);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 手工新增操作日志(可选同步调整处方/支付单审核状态)
|
|
||||||
*/
|
|
||||||
public function addLog()
|
|
||||||
{
|
|
||||||
$params = (new PrescriptionOrderValidate())->post()->goCheck('addLog');
|
|
||||||
$result = PrescriptionOrderLogic::addLog($params, $this->adminId, $this->adminInfo);
|
|
||||||
if ($result === false) {
|
|
||||||
return $this->fail(PrescriptionOrderLogic::getError());
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->success('日志已添加', $result);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 为「已发货」订单新增一条关联支付单,并重置支付单审核状态为待审核
|
* 为「已发货」订单新增一条关联支付单,并重置支付单审核状态为待审核
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -3,8 +3,6 @@
|
|||||||
namespace app\adminapi\lists\doctor;
|
namespace app\adminapi\lists\doctor;
|
||||||
|
|
||||||
use app\adminapi\lists\BaseAdminDataLists;
|
use app\adminapi\lists\BaseAdminDataLists;
|
||||||
use app\adminapi\logic\dept\DeptLogic;
|
|
||||||
use app\common\model\auth\AdminDept;
|
|
||||||
use app\common\model\DiagnosisViewRecord;
|
use app\common\model\DiagnosisViewRecord;
|
||||||
use app\common\model\doctor\Appointment;
|
use app\common\model\doctor\Appointment;
|
||||||
use app\common\model\tcm\Prescription;
|
use app\common\model\tcm\Prescription;
|
||||||
@@ -77,35 +75,6 @@ class AppointmentLists extends BaseAdminDataLists implements ListsSearchInterfac
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 按部门筛选:接诊医生、诊单医助或挂号医助所属部门命中子树即可(选父级含子级)
|
|
||||||
*
|
|
||||||
* @param mixed $query
|
|
||||||
*/
|
|
||||||
private function applyAssistantDeptIdFilter($query): void
|
|
||||||
{
|
|
||||||
if (!isset($this->params['assistant_dept_id']) || $this->params['assistant_dept_id'] === '' || (int) $this->params['assistant_dept_id'] <= 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
$rootDeptId = (int) $this->params['assistant_dept_id'];
|
|
||||||
$deptIds = DeptLogic::getSelfAndDescendantIds($rootDeptId);
|
|
||||||
$deptIds = array_values(array_filter(array_map('intval', $deptIds), static function (int $id): bool {
|
|
||||||
return $id > 0;
|
|
||||||
}));
|
|
||||||
if ($deptIds === []) {
|
|
||||||
$query->whereRaw('0 = 1');
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
$inList = implode(',', $deptIds);
|
|
||||||
$adTbl = (new AdminDept())->getTable();
|
|
||||||
$query->whereRaw(
|
|
||||||
"(EXISTS (SELECT 1 FROM `{$adTbl}` ad WHERE ad.`admin_id` = a.`doctor_id` AND ad.`dept_id` IN ({$inList}))"
|
|
||||||
. " OR EXISTS (SELECT 1 FROM `{$adTbl}` ad WHERE ad.`admin_id` = u.`assistant_id` AND ad.`dept_id` IN ({$inList}))"
|
|
||||||
. " OR EXISTS (SELECT 1 FROM `{$adTbl}` ad WHERE ad.`admin_id` = a.`assistant_id` AND ad.`dept_id` IN ({$inList})))"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 渠道筛选:与 AppointmentLogic 一致,兼容仅有 channel_source、仅有 channels、或两者皆有的表结构
|
* 渠道筛选:与 AppointmentLogic 一致,兼容仅有 channel_source、仅有 channels、或两者皆有的表结构
|
||||||
*
|
*
|
||||||
@@ -222,8 +191,6 @@ class AppointmentLists extends BaseAdminDataLists implements ListsSearchInterfac
|
|||||||
|
|
||||||
$this->applyAssistantIdFilter($query);
|
$this->applyAssistantIdFilter($query);
|
||||||
|
|
||||||
$this->applyAssistantDeptIdFilter($query);
|
|
||||||
|
|
||||||
$this->applyChannelSourceFilter($query, $chFilter);
|
$this->applyChannelSourceFilter($query, $chFilter);
|
||||||
|
|
||||||
// 是否确认诊单:1=已确认 0=未确认
|
// 是否确认诊单:1=已确认 0=未确认
|
||||||
@@ -406,8 +373,6 @@ class AppointmentLists extends BaseAdminDataLists implements ListsSearchInterfac
|
|||||||
|
|
||||||
$this->applyAssistantIdFilter($query);
|
$this->applyAssistantIdFilter($query);
|
||||||
|
|
||||||
$this->applyAssistantDeptIdFilter($query);
|
|
||||||
|
|
||||||
$this->applyChannelSourceFilter($query, $chFilter);
|
$this->applyChannelSourceFilter($query, $chFilter);
|
||||||
|
|
||||||
if ((int) ($this->params['exclude_cancelled'] ?? 0) === 1) {
|
if ((int) ($this->params['exclude_cancelled'] ?? 0) === 1) {
|
||||||
|
|||||||
@@ -1,96 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace app\adminapi\lists\stats;
|
|
||||||
|
|
||||||
use app\adminapi\lists\BaseAdminDataLists;
|
|
||||||
use app\common\lists\ListsSearchInterface;
|
|
||||||
use think\facade\Db;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 待分配诊单自动指派日志列表
|
|
||||||
*
|
|
||||||
* 数据来源:tcm_diagnosis_auto_assign_log(定时命令 tcm:auto-assign-pending 写入)
|
|
||||||
* 筛选:run_date(执行日期)/ action(1=已分配 0=未分配)/ assistant_id / keyword(患者姓名、手机号、医助姓名,纯数字兼容诊单ID)
|
|
||||||
*/
|
|
||||||
class AutoAssignLogLists extends BaseAdminDataLists implements ListsSearchInterface
|
|
||||||
{
|
|
||||||
private const TIER_LABELS = [
|
|
||||||
'gt70' => '>70%',
|
|
||||||
'60_70' => '60%~70%',
|
|
||||||
'50_60' => '50%~60%',
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @notes 设置搜索条件
|
|
||||||
*/
|
|
||||||
public function setSearch(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'=' => ['run_date', 'action', 'assistant_id', 'batch_no', 'stat_month'],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @notes 获取列表
|
|
||||||
*/
|
|
||||||
public function lists(): array
|
|
||||||
{
|
|
||||||
$rows = $this->buildQuery()
|
|
||||||
->order(['id' => 'desc'])
|
|
||||||
->limit($this->limitOffset, $this->limitLength)
|
|
||||||
->select()
|
|
||||||
->toArray();
|
|
||||||
|
|
||||||
foreach ($rows as &$row) {
|
|
||||||
$ct = (int) ($row['create_time'] ?? 0);
|
|
||||||
$row['create_time_text'] = $ct > 0 ? date('Y-m-d H:i:s', $ct) : '';
|
|
||||||
$row['action_text'] = (int) ($row['action'] ?? 0) === 1 ? '已分配' : '未分配';
|
|
||||||
$row['tier_text'] = self::TIER_LABELS[(string) ($row['tier'] ?? '')] ?? '';
|
|
||||||
$row['visit2_rate'] = $row['visit2_rate'] !== null ? (float) $row['visit2_rate'] : null;
|
|
||||||
}
|
|
||||||
unset($row);
|
|
||||||
|
|
||||||
return $rows;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @notes 获取数量
|
|
||||||
*/
|
|
||||||
public function count(): int
|
|
||||||
{
|
|
||||||
return $this->buildQuery()->count();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return \think\db\Query
|
|
||||||
*/
|
|
||||||
private function buildQuery()
|
|
||||||
{
|
|
||||||
$query = Db::name('tcm_diagnosis_auto_assign_log')->where($this->searchWhere);
|
|
||||||
|
|
||||||
$keyword = trim((string) ($this->params['keyword'] ?? ''));
|
|
||||||
if ($keyword !== '') {
|
|
||||||
$query->where(function ($q) use ($keyword) {
|
|
||||||
$q->whereLike('patient_name', '%' . $keyword . '%')
|
|
||||||
->whereOr('patient_phone', 'like', '%' . $keyword . '%')
|
|
||||||
->whereOr('assistant_name', 'like', '%' . $keyword . '%');
|
|
||||||
if (preg_match('/^\d+$/', $keyword) === 1 && (int) $keyword > 0) {
|
|
||||||
$q->whereOr('diagnosis_id', '=', (int) $keyword);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
$startDate = trim((string) ($this->params['start_date'] ?? ''));
|
|
||||||
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $startDate) === 1) {
|
|
||||||
$query->where('run_date', '>=', $startDate);
|
|
||||||
}
|
|
||||||
$endDate = trim((string) ($this->params['end_date'] ?? ''));
|
|
||||||
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $endDate) === 1) {
|
|
||||||
$query->where('run_date', '<=', $endDate);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $query;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -140,7 +140,6 @@ class DiagnosisLists extends BaseAdminDataLists implements ListsSearchInterface
|
|||||||
}
|
}
|
||||||
|
|
||||||
$this->applyLatestAppointmentFilters($query, $pendingWideSearch);
|
$this->applyLatestAppointmentFilters($query, $pendingWideSearch);
|
||||||
$this->applyLatestAssignFilters($query, $pendingWideSearch);
|
|
||||||
|
|
||||||
$this->applyPendingAssignBusinessOrderMonthFilter($query);
|
$this->applyPendingAssignBusinessOrderMonthFilter($query);
|
||||||
|
|
||||||
@@ -168,7 +167,30 @@ class DiagnosisLists extends BaseAdminDataLists implements ListsSearchInterface
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$orderRaw = $this->resolveListOrderRaw($pendingWideSearch);
|
// 按挂号状态优先级排序:已过号(4) > 已预约(1) > 已完成(3),然后按挂号日期+时间升序
|
||||||
|
// 若传了 appointment_date(当天/明天等筛选),只按「该日」的挂号排序与展示
|
||||||
|
$diagTbl = (new Diagnosis())->getTable();
|
||||||
|
$aptTbl = (new Appointment())->getTable();
|
||||||
|
$minAptDateCond = '';
|
||||||
|
if (!$pendingWideSearch && !empty($this->params['appointment_date'])) {
|
||||||
|
$sortAptDate = addslashes((string) $this->params['appointment_date']);
|
||||||
|
$minAptDateCond = " AND apt.appointment_date = '{$sortAptDate}'";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取最早的挂号状态(用于排序优先级)
|
||||||
|
$minAptStatusExpr = '(SELECT apt.status FROM ' . $aptTbl . ' apt WHERE apt.patient_id = ' . $diagTbl . '.id AND apt.status IN (1,3,4)' . $minAptDateCond . ' ORDER BY CASE apt.status WHEN 4 THEN 1 WHEN 1 THEN 2 WHEN 3 THEN 3 ELSE 4 END, apt.appointment_date ASC, apt.appointment_time ASC LIMIT 1)';
|
||||||
|
|
||||||
|
// 获取最早的挂号时间(用于同状态内排序)
|
||||||
|
$minAptExpr = '(SELECT MIN(CONCAT(apt.appointment_date, \' \', IFNULL(NULLIF(TRIM(apt.appointment_time), \'\'), \'00:00:00\'))) FROM ' . $aptTbl . ' apt WHERE apt.patient_id = ' . $diagTbl . '.id AND apt.status IN (1,3,4)' . $minAptDateCond . ')';
|
||||||
|
|
||||||
|
// 「已完成」Tab:按最近一条「已完成」(status=3) 挂号日期+时间降序…
|
||||||
|
$isCompletedTab = !$pendingWideSearch && isset($this->params['completed_appointment']) && (string) $this->params['completed_appointment'] === '1';
|
||||||
|
if ($isCompletedTab) {
|
||||||
|
$maxCompletedAptExpr = '(SELECT MAX(CONCAT(apt.appointment_date, \' \', IFNULL(NULLIF(TRIM(apt.appointment_time), \'\'), \'00:00:00\'))) FROM ' . $aptTbl . ' apt WHERE apt.patient_id = ' . $diagTbl . '.id AND apt.status = 3' . $minAptDateCond . ')';
|
||||||
|
$orderRaw = $diagTbl . '.assign_read_at IS NULL DESC, IFNULL(' . $maxCompletedAptExpr . ", '1970-01-01 00:00:00') DESC, {$diagTbl}.id DESC";
|
||||||
|
} else {
|
||||||
|
$orderRaw = $diagTbl . '.assign_read_at IS NULL DESC, CASE IFNULL(' . $minAptStatusExpr . ', 999) WHEN 4 THEN 1 WHEN 1 THEN 2 WHEN 3 THEN 3 ELSE 4 END ASC, IFNULL(' . $minAptExpr . ", '9999-12-31 23:59:59') ASC, {$diagTbl}.id DESC";
|
||||||
|
}
|
||||||
|
|
||||||
$lists = $query
|
$lists = $query
|
||||||
->with(['DiagnosisViewRecord'])
|
->with(['DiagnosisViewRecord'])
|
||||||
@@ -549,7 +571,6 @@ class DiagnosisLists extends BaseAdminDataLists implements ListsSearchInterface
|
|||||||
}
|
}
|
||||||
|
|
||||||
$this->applyLatestAppointmentFilters($query, $pendingWideSearch);
|
$this->applyLatestAppointmentFilters($query, $pendingWideSearch);
|
||||||
$this->applyLatestAssignFilters($query, $pendingWideSearch);
|
|
||||||
|
|
||||||
// 仅已开方(待分配+关键词检索时不限制)
|
// 仅已开方(待分配+关键词检索时不限制)
|
||||||
if (!$pendingWideSearch && isset($this->params['only_has_prescription']) && (string) $this->params['only_has_prescription'] === '1') {
|
if (!$pendingWideSearch && isset($this->params['only_has_prescription']) && (string) $this->params['only_has_prescription'] === '1') {
|
||||||
@@ -623,99 +644,6 @@ class DiagnosisLists extends BaseAdminDataLists implements ListsSearchInterface
|
|||||||
$query->whereExists("SELECT 1 FROM {$aptTbl} latest_apt WHERE " . implode(' AND ', $conditions));
|
$query->whereExists("SELECT 1 FROM {$aptTbl} latest_apt WHERE " . implode(' AND ', $conditions));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 最近一次成功指派过滤:按 create_time DESC, id DESC 取 to_assistant_id>0 的一条。
|
|
||||||
*
|
|
||||||
* @param mixed $query
|
|
||||||
*/
|
|
||||||
private function applyLatestAssignFilters($query, bool $pendingWideSearch): void
|
|
||||||
{
|
|
||||||
if ($pendingWideSearch) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$startDate = $this->normalizeYmd($this->params['latest_assign_start_date'] ?? '');
|
|
||||||
$endDate = $this->normalizeYmd($this->params['latest_assign_end_date'] ?? '');
|
|
||||||
if ($startDate === '' && $endDate === '') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$logTbl = Db::name('tcm_diagnosis_assign_log')->getTable();
|
|
||||||
$diagTbl = (new Diagnosis())->getTable();
|
|
||||||
$latestIdSql = $this->latestAssignLogIdSubSql($logTbl, $diagTbl);
|
|
||||||
$conditions = ["latest_lg.id = ({$latestIdSql})"];
|
|
||||||
|
|
||||||
if ($startDate !== '') {
|
|
||||||
$startTs = (int) strtotime($startDate . ' 00:00:00');
|
|
||||||
$conditions[] = "latest_lg.create_time >= {$startTs}";
|
|
||||||
}
|
|
||||||
if ($endDate !== '') {
|
|
||||||
$endTs = (int) strtotime($endDate . ' 23:59:59');
|
|
||||||
$conditions[] = "latest_lg.create_time <= {$endTs}";
|
|
||||||
}
|
|
||||||
|
|
||||||
$query->whereExists("SELECT 1 FROM {$logTbl} latest_lg WHERE " . implode(' AND ', $conditions));
|
|
||||||
}
|
|
||||||
|
|
||||||
private function latestAssignLogIdSubSql(string $logTbl, string $diagTbl): string
|
|
||||||
{
|
|
||||||
return "SELECT lg_latest.id FROM {$logTbl} lg_latest "
|
|
||||||
. "WHERE lg_latest.diagnosis_id = {$diagTbl}.id "
|
|
||||||
. 'AND lg_latest.to_assistant_id > 0 '
|
|
||||||
. 'ORDER BY lg_latest.create_time DESC, lg_latest.id DESC LIMIT 1';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 列表默认排序;支持 sort_unserved_days=asc|desc 按未服务天数排序。
|
|
||||||
*/
|
|
||||||
private function resolveListOrderRaw(bool $pendingWideSearch): string
|
|
||||||
{
|
|
||||||
$diagTbl = (new Diagnosis())->getTable();
|
|
||||||
$sortUnserved = strtolower(trim((string) ($this->params['sort_unserved_days'] ?? '')));
|
|
||||||
if (in_array($sortUnserved, ['asc', 'desc'], true)) {
|
|
||||||
$anchorExpr = $this->unservedAnchorExpr($diagTbl);
|
|
||||||
$nullLast = "CASE WHEN IFNULL({$anchorExpr}, 0) = 0 THEN 1 ELSE 0 END ASC";
|
|
||||||
if ($sortUnserved === 'desc') {
|
|
||||||
return "{$nullLast}, IFNULL({$anchorExpr}, 0) ASC, {$diagTbl}.id DESC";
|
|
||||||
}
|
|
||||||
|
|
||||||
return "{$nullLast}, IFNULL({$anchorExpr}, 0) DESC, {$diagTbl}.id DESC";
|
|
||||||
}
|
|
||||||
|
|
||||||
$aptTbl = (new Appointment())->getTable();
|
|
||||||
$minAptDateCond = '';
|
|
||||||
if (!$pendingWideSearch && !empty($this->params['appointment_date'])) {
|
|
||||||
$sortAptDate = addslashes((string) $this->params['appointment_date']);
|
|
||||||
$minAptDateCond = " AND apt.appointment_date = '{$sortAptDate}'";
|
|
||||||
}
|
|
||||||
|
|
||||||
$minAptStatusExpr = '(SELECT apt.status FROM ' . $aptTbl . ' apt WHERE apt.patient_id = ' . $diagTbl . '.id AND apt.status IN (1,3,4)' . $minAptDateCond . ' ORDER BY CASE apt.status WHEN 4 THEN 1 WHEN 1 THEN 2 WHEN 3 THEN 3 ELSE 4 END, apt.appointment_date ASC, apt.appointment_time ASC LIMIT 1)';
|
|
||||||
$minAptExpr = '(SELECT MIN(CONCAT(apt.appointment_date, \' \', IFNULL(NULLIF(TRIM(apt.appointment_time), \'\'), \'00:00:00\'))) FROM ' . $aptTbl . ' apt WHERE apt.patient_id = ' . $diagTbl . '.id AND apt.status IN (1,3,4)' . $minAptDateCond . ')';
|
|
||||||
|
|
||||||
$isCompletedTab = !$pendingWideSearch && isset($this->params['completed_appointment']) && (string) $this->params['completed_appointment'] === '1';
|
|
||||||
if ($isCompletedTab) {
|
|
||||||
$maxCompletedAptExpr = '(SELECT MAX(CONCAT(apt.appointment_date, \' \', IFNULL(NULLIF(TRIM(apt.appointment_time), \'\'), \'00:00:00\'))) FROM ' . $aptTbl . ' apt WHERE apt.patient_id = ' . $diagTbl . '.id AND apt.status = 3' . $minAptDateCond . ')';
|
|
||||||
|
|
||||||
return $diagTbl . '.assign_read_at IS NULL DESC, IFNULL(' . $maxCompletedAptExpr . ", '1970-01-01 00:00:00') DESC, {$diagTbl}.id DESC";
|
|
||||||
}
|
|
||||||
|
|
||||||
return $diagTbl . '.assign_read_at IS NULL DESC, CASE IFNULL(' . $minAptStatusExpr . ', 999) WHEN 4 THEN 1 WHEN 1 THEN 2 WHEN 3 THEN 3 ELSE 4 END ASC, IFNULL(' . $minAptExpr . ", '9999-12-31 23:59:59') ASC, {$diagTbl}.id DESC";
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 未服务天数锚点:血糖/饮食/运动记录最近 record_date 的最大值 */
|
|
||||||
private function unservedAnchorExpr(string $diagTbl): string
|
|
||||||
{
|
|
||||||
$bloodTbl = (new BloodRecord())->getTable();
|
|
||||||
$dietTbl = (new DietRecord())->getTable();
|
|
||||||
$exerciseTbl = (new ExerciseRecord())->getTable();
|
|
||||||
|
|
||||||
return 'GREATEST('
|
|
||||||
. "COALESCE((SELECT MAX(br.record_date) FROM {$bloodTbl} br WHERE br.diagnosis_id = {$diagTbl}.id AND br.delete_time IS NULL), 0), "
|
|
||||||
. "COALESCE((SELECT MAX(dr.record_date) FROM {$dietTbl} dr WHERE dr.diagnosis_id = {$diagTbl}.id AND dr.delete_time IS NULL), 0), "
|
|
||||||
. "COALESCE((SELECT MAX(er.record_date) FROM {$exerciseTbl} er WHERE er.diagnosis_id = {$diagTbl}.id AND er.delete_time IS NULL), 0)"
|
|
||||||
. ')';
|
|
||||||
}
|
|
||||||
|
|
||||||
private function latestAppointmentIdSubSql(string $aptTbl, string $diagTbl): string
|
private function latestAppointmentIdSubSql(string $aptTbl, string $diagTbl): string
|
||||||
{
|
{
|
||||||
$statuses = implode(',', self::EFFECTIVE_APPOINTMENT_STATUSES);
|
$statuses = implode(',', self::EFFECTIVE_APPOINTMENT_STATUSES);
|
||||||
|
|||||||
@@ -741,10 +741,6 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
|
|||||||
}
|
}
|
||||||
unset($item);
|
unset($item);
|
||||||
|
|
||||||
if ($this->shouldBypassListVisibilityForDiagnosisEdit()) {
|
|
||||||
$this->appendDiagnosisEditVisitSeqFields($lists);
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->appendPrescriptionOrderAssignSnapshotErCenterFlags($lists);
|
$this->appendPrescriptionOrderAssignSnapshotErCenterFlags($lists);
|
||||||
|
|
||||||
if ((int) ($this->params['yeji_order_drawer'] ?? 0) === 1) {
|
if ((int) ($this->params['yeji_order_drawer'] ?? 0) === 1) {
|
||||||
@@ -824,11 +820,6 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
|
|||||||
'export_guahao_channel_source' => '自媒体渠道(挂号渠道来源)',
|
'export_guahao_channel_source' => '自媒体渠道(挂号渠道来源)',
|
||||||
'export_medication_form' => '药品形态',
|
'export_medication_form' => '药品形态',
|
||||||
'export_prescription_name' => '药方名称',
|
'export_prescription_name' => '药方名称',
|
||||||
'export_prescription_herbs' => '处方',
|
|
||||||
'export_main_usage' => '主方服用方式',
|
|
||||||
'export_main_usage_days' => '主方天数',
|
|
||||||
'export_aux_usage' => '辅方服用方式',
|
|
||||||
'export_aux_usage_days' => '辅方天数',
|
|
||||||
'export_service_package' => '服务套餐',
|
'export_service_package' => '服务套餐',
|
||||||
'export_medication_days' => '天数',
|
'export_medication_days' => '天数',
|
||||||
'export_amount' => '总金额',
|
'export_amount' => '总金额',
|
||||||
@@ -918,9 +909,6 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
|
|||||||
$wrapWideKeys = [
|
$wrapWideKeys = [
|
||||||
'export_linked_pay_records',
|
'export_linked_pay_records',
|
||||||
'export_prescription_name',
|
'export_prescription_name',
|
||||||
'export_prescription_herbs',
|
|
||||||
'export_main_usage',
|
|
||||||
'export_aux_usage',
|
|
||||||
'export_guahao_channel_source',
|
'export_guahao_channel_source',
|
||||||
'export_assistant_dept',
|
'export_assistant_dept',
|
||||||
'export_service_package',
|
'export_service_package',
|
||||||
@@ -931,8 +919,6 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
|
|||||||
'export_patient_gender' => 6,
|
'export_patient_gender' => 6,
|
||||||
'export_patient_age' => 6,
|
'export_patient_age' => 6,
|
||||||
'export_medication_days' => 6,
|
'export_medication_days' => 6,
|
||||||
'export_main_usage_days' => 8,
|
|
||||||
'export_aux_usage_days' => 8,
|
|
||||||
'export_amount' => 10,
|
'export_amount' => 10,
|
||||||
'export_paid_amount' => 10,
|
'export_paid_amount' => 10,
|
||||||
'export_refund_amount' => 10,
|
'export_refund_amount' => 10,
|
||||||
@@ -941,9 +927,6 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
|
|||||||
'export_supply_mode' => 10,
|
'export_supply_mode' => 10,
|
||||||
'export_linked_pay_records' => 52,
|
'export_linked_pay_records' => 52,
|
||||||
'export_prescription_name' => 34,
|
'export_prescription_name' => 34,
|
||||||
'export_prescription_herbs' => 36,
|
|
||||||
'export_main_usage' => 28,
|
|
||||||
'export_aux_usage' => 28,
|
|
||||||
'export_guahao_channel_source' => 22,
|
'export_guahao_channel_source' => 22,
|
||||||
'export_assistant_dept' => 24,
|
'export_assistant_dept' => 24,
|
||||||
'export_service_package' => 18,
|
'export_service_package' => 18,
|
||||||
@@ -1421,69 +1404,6 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 诊单编辑-业务订单 tab:标注全局诊次及是否计入复诊接诊率(与 RevisitRateLogic 同口径)
|
|
||||||
*
|
|
||||||
* @param array<int, array<string, mixed>> $lists
|
|
||||||
*/
|
|
||||||
private function appendDiagnosisEditVisitSeqFields(array &$lists): void
|
|
||||||
{
|
|
||||||
if ($lists === []) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$diagIds = [];
|
|
||||||
foreach ($lists as $row) {
|
|
||||||
$d = (int) ($row['diagnosis_id'] ?? 0);
|
|
||||||
if ($d > 0) {
|
|
||||||
$diagIds[$d] = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$contextDid = (int) ($this->params['context_diagnosis_id'] ?? 0);
|
|
||||||
if ($contextDid > 0) {
|
|
||||||
$diagIds[$contextDid] = true;
|
|
||||||
}
|
|
||||||
$diagIdList = array_keys($diagIds);
|
|
||||||
if ($diagIdList === []) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$offsetRows = Diagnosis::whereIn('id', $diagIdList)
|
|
||||||
->whereNull('delete_time')
|
|
||||||
->column('revisit_slot_start_offset', 'id');
|
|
||||||
$offsetMap = [];
|
|
||||||
foreach ($offsetRows as $id => $offset) {
|
|
||||||
$offsetMap[(int) $id] = max(0, min(20, (int) $offset));
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @var array<int, int> $seqByOrderId order_id => global seq within diagnosis */
|
|
||||||
$seqByOrderId = [];
|
|
||||||
foreach ($diagIdList as $did) {
|
|
||||||
$q = PrescriptionOrder::where('diagnosis_id', $did)->whereNull('delete_time');
|
|
||||||
YejiStatsLogic::applyPrescriptionOrderNotCancelledForPerformanceQuery($q, '');
|
|
||||||
$orderIds = $q
|
|
||||||
->order(['create_time' => 'asc', 'id' => 'asc'])
|
|
||||||
->column('id');
|
|
||||||
$seq = 0;
|
|
||||||
foreach ($orderIds as $oid) {
|
|
||||||
$seq++;
|
|
||||||
$seqByOrderId[(int) $oid] = $seq;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($lists as &$row) {
|
|
||||||
$oid = (int) ($row['id'] ?? 0);
|
|
||||||
$did = (int) ($row['diagnosis_id'] ?? 0);
|
|
||||||
$seq = (int) ($seqByOrderId[$oid] ?? 0);
|
|
||||||
$offset = (int) ($offsetMap[$did] ?? 0);
|
|
||||||
$effectiveSlot = $seq > 0 ? $seq + $offset : 0;
|
|
||||||
$row['global_visit_seq'] = $effectiveSlot > 0 ? $effectiveSlot : null;
|
|
||||||
$row['raw_visit_seq'] = $seq > 0 ? $seq : null;
|
|
||||||
$row['counts_for_revisit_rate'] = $effectiveSlot >= 2 ? 1 : 0;
|
|
||||||
}
|
|
||||||
unset($row);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 列表行标注:指派日志快照(related_po_creator_id + related_po_create_time)是否指向本业务单,
|
* 列表行标注:指派日志快照(related_po_creator_id + related_po_create_time)是否指向本业务单,
|
||||||
* 以及该次操作的新医助(to_assistant_id)是否归属「二中心」部门子树(与 DeptLogic / 业绩看板一致)。
|
* 以及该次操作的新医助(to_assistant_id)是否归属「二中心」部门子树(与 DeptLogic / 业绩看板一致)。
|
||||||
|
|||||||
@@ -151,24 +151,6 @@ class DeptLogic extends BaseLogic
|
|||||||
* @return list<int>
|
* @return list<int>
|
||||||
*/
|
*/
|
||||||
private static function findErCenterRootDeptIds(): array
|
private static function findErCenterRootDeptIds(): array
|
||||||
{
|
|
||||||
return self::findCenterRootDeptIdsByNameKeyword('二中心');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 名称含「一中心」的部门 id。
|
|
||||||
*
|
|
||||||
* @return list<int>
|
|
||||||
*/
|
|
||||||
private static function findYiCenterRootDeptIds(): array
|
|
||||||
{
|
|
||||||
return self::findCenterRootDeptIdsByNameKeyword('一中心');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return list<int>
|
|
||||||
*/
|
|
||||||
private static function findCenterRootDeptIdsByNameKeyword(string $keyword): array
|
|
||||||
{
|
{
|
||||||
$rows = Dept::whereNull('delete_time')
|
$rows = Dept::whereNull('delete_time')
|
||||||
->field(['id', 'name'])
|
->field(['id', 'name'])
|
||||||
@@ -177,7 +159,7 @@ class DeptLogic extends BaseLogic
|
|||||||
$out = [];
|
$out = [];
|
||||||
foreach ($rows as $r) {
|
foreach ($rows as $r) {
|
||||||
$name = (string) ($r['name'] ?? '');
|
$name = (string) ($r['name'] ?? '');
|
||||||
if ($name !== '' && mb_strpos($name, $keyword) !== false) {
|
if ($name !== '' && mb_strpos($name, '二中心') !== false) {
|
||||||
$out[] = (int) $r['id'];
|
$out[] = (int) $r['id'];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -219,19 +201,6 @@ class DeptLogic extends BaseLogic
|
|||||||
return array_fill_keys($subtreeIds, true);
|
return array_fill_keys($subtreeIds, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 名称含「一中心」的部门及其全部下级 id(map)。
|
|
||||||
*
|
|
||||||
* @return array<int, true>
|
|
||||||
*/
|
|
||||||
public static function getYiCenterSubtreeDeptIdSet(): array
|
|
||||||
{
|
|
||||||
$yiRoots = self::findYiCenterRootDeptIds();
|
|
||||||
$subtreeIds = self::unionErCenterSubtreeDeptIds($yiRoots);
|
|
||||||
|
|
||||||
return array_fill_keys($subtreeIds, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 二中心复诊统计用的业务订单行(与 rollup 同源 SQL)。
|
* 二中心复诊统计用的业务订单行(与 rollup 同源 SQL)。
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace app\adminapi\logic\stats;
|
namespace app\adminapi\logic\stats;
|
||||||
|
|
||||||
use app\adminapi\logic\dept\DeptLogic;
|
|
||||||
use think\facade\Db;
|
use think\facade\Db;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -13,11 +12,9 @@ use think\facade\Db;
|
|||||||
* 口径说明:
|
* 口径说明:
|
||||||
* - 当月被指派总数 = 当月内 `tcm_diagnosis_assign_log`(按 **指派操作时间 lg.create_time** 落月,to_assistant_id>0,
|
* - 当月被指派总数 = 当月内 `tcm_diagnosis_assign_log`(按 **指派操作时间 lg.create_time** 落月,to_assistant_id>0,
|
||||||
* **剔除勾选「继承」的指派 is_inherit=1**,诊单未删除)去重后的「医助 × 诊单」组合;
|
* **剔除勾选「继承」的指派 is_inherit=1**,诊单未删除)去重后的「医助 × 诊单」组合;
|
||||||
* 同一诊单当月被多次指派给同一医助只计 1 次;
|
* 同一诊单当月被多次指派给同一医助只计 1 次。
|
||||||
* **再剔除**名下存在履约「拒收(9) / 退款(10)」业务订单(未软删)的诊单。
|
|
||||||
* - 第 N 次下单 = 诊单(患者)名下计入业绩的业务订单(剔除履约 4/9/10、软删)按 create_time 升序的**全局**序列中第 N 笔;
|
* - 第 N 次下单 = 诊单(患者)名下计入业绩的业务订单(剔除履约 4/9/10、软删)按 create_time 升序的**全局**序列中第 N 笔;
|
||||||
* 诊次**跨月累计不重置**:例如 5 月指派后旗下成交 4 单为二诊~五诊,6 月再成交即为六诊。
|
* 诊次**跨月累计不重置**:例如 5 月指派后旗下成交 4 单为二诊~五诊,6 月再成交即为六诊。
|
||||||
* 诊单可配置 `revisit_slot_start_offset`(默认 0:第 1 笔实单计为一诊;设为 1 则第 1 笔实单计为二诊;设为 2 则计为三诊,即在实单序号上叠加偏移,5 笔实单+偏移 2 等价于计至七诊)。
|
|
||||||
* - 当月 N 诊单数 = **当月内下单**且全局序号为 N 的订单数,归属下单时点的**持有医助**——
|
* - 当月 N 诊单数 = **当月内下单**且全局序号为 N 的订单数,归属下单时点的**持有医助**——
|
||||||
* 按指派日志时间线取「订单时间之前最近一次指派」的 to_assistant_id(释放 to=0 即不再归属;
|
* 按指派日志时间线取「订单时间之前最近一次指派」的 to_assistant_id(释放 to=0 即不再归属;
|
||||||
* 「继承」指派会转移持有人用于归属,但不计被指派数)。指派可发生在往月。
|
* 「继承」指派会转移持有人用于归属,但不计被指派数)。指派可发生在往月。
|
||||||
@@ -25,8 +22,7 @@ use think\facade\Db;
|
|||||||
* 医助当月无新指派但旗下有成交时,被指派数为 0、比率显示为空。
|
* 医助当月无新指派但旗下有成交时,被指派数为 0、比率显示为空。
|
||||||
* - 分档动态产出:N 从 2 起,至当月命中数据的最大序号(至少展示到四诊,上限 MAX_VISIT_SLOT 防御异常数据),
|
* - 分档动态产出:N 从 2 起,至当月命中数据的最大序号(至少展示到四诊,上限 MAX_VISIT_SLOT 防御异常数据),
|
||||||
* 返回 `slots` 列表供前端动态渲染「五诊」「六诊」… 列。
|
* 返回 `slots` 列表供前端动态渲染「五诊」「六诊」… 列。
|
||||||
* - 部门归类:医助按其人事部门(admin_dept 最小 dept_id)归组;**仅统计「二中心」及其组织下级**(与 DeptLogic::getErCenterSubtreeDeptIdSet 一致);
|
* - 部门归类:医助按其人事部门(admin_dept 最小 dept_id)归组;部门筛选(dept_ids,含组织下级)按该归属部门过滤。
|
||||||
* 部门筛选下拉与未选部门时的默认范围均限定在该子树内,选定部门时含其组织下级。
|
|
||||||
* - 部门行 / 合计行:被指派数按诊单去重(可能小于下级行相加);N 诊单数为下级行求和(每笔订单唯一归属一名医助)。
|
* - 部门行 / 合计行:被指派数按诊单去重(可能小于下级行相加);N 诊单数为下级行求和(每笔订单唯一归属一名医助)。
|
||||||
*/
|
*/
|
||||||
class RevisitRateLogic
|
class RevisitRateLogic
|
||||||
@@ -298,19 +294,14 @@ class RevisitRateLogic
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 部门下拉:仅「二中心」及其组织下级(与业绩看板 ErCenter 子树一致)。
|
* 部门下拉(全量未删除部门,前端组树)。
|
||||||
*
|
*
|
||||||
* @return array{rows: list<array{id:int,pid:int,name:string}>}
|
* @return array{rows: list<array{id:int,pid:int,name:string}>}
|
||||||
*/
|
*/
|
||||||
public static function deptOptions(): array
|
public static function deptOptions(): array
|
||||||
{
|
{
|
||||||
$erSet = DeptLogic::getErCenterSubtreeDeptIdSet();
|
|
||||||
if ($erSet === []) {
|
|
||||||
return ['rows' => []];
|
|
||||||
}
|
|
||||||
$rows = Db::name('dept')
|
$rows = Db::name('dept')
|
||||||
->whereNull('delete_time')
|
->whereNull('delete_time')
|
||||||
->whereIn('id', array_keys($erSet))
|
|
||||||
->field(['id', 'pid', 'name'])
|
->field(['id', 'pid', 'name'])
|
||||||
->order('sort', 'desc')
|
->order('sort', 'desc')
|
||||||
->order('id', 'asc')
|
->order('id', 'asc')
|
||||||
@@ -331,9 +322,9 @@ class RevisitRateLogic
|
|||||||
/**
|
/**
|
||||||
* 核心统计上下文:
|
* 核心统计上下文:
|
||||||
* 1. 全量指派日志(≤ 月末)构建持有时间线;
|
* 1. 全量指派日志(≤ 月末)构建持有时间线;
|
||||||
* 2. 分母:当月非继承指派的「医助 × 诊单」,再剔除名下存在拒收(9)/退款(10) 订单的诊单;
|
* 2. 分母:当月非继承指派的「医助 × 诊单」;
|
||||||
* 3. 分子:曾被指派诊单的当月订单,统计诊次 = 实单序号 + 诊单偏移(默认第 1 笔实单为一诊);
|
* 3. 分子:曾被指派诊单的当月订单按全局序号 ≥2 归属持有医助;
|
||||||
* 4. 应用部门筛选:默认限定「二中心」子树;选定部门时再收窄到该部门及其下级(且须落在二中心子树内)。
|
* 4. 应用部门筛选(含组织下级)。
|
||||||
*
|
*
|
||||||
* @param array{month?:string,dept_ids?:int[]|string} $params
|
* @param array{month?:string,dept_ids?:int[]|string} $params
|
||||||
*
|
*
|
||||||
@@ -384,35 +375,9 @@ class RevisitRateLogic
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 分母:剔除名下存在拒收(9)/退款(10) 业务订单的诊单(与明细 assignLines 同口径)
|
// 分子:曾被指派诊单的当月订单(全局序号 ≥2),归属下单时点的持有医助
|
||||||
$assignedDiagIds = [];
|
|
||||||
foreach ($diagsByAssistant as $diagSet) {
|
|
||||||
foreach ($diagSet as $did => $_) {
|
|
||||||
$assignedDiagIds[(int) $did] = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$refundRejectDiagSet = self::fetchRefundOrRejectDiagnosisSet(array_keys($assignedDiagIds));
|
|
||||||
if ($refundRejectDiagSet !== []) {
|
|
||||||
foreach ($diagsByAssistant as $aid => $diagSet) {
|
|
||||||
foreach ($diagSet as $did => $_) {
|
|
||||||
if (isset($refundRejectDiagSet[$did])) {
|
|
||||||
unset($diagsByAssistant[$aid][$did]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ($diagsByAssistant[$aid] === []) {
|
|
||||||
unset($diagsByAssistant[$aid]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$pairsRaw = array_values(array_filter(
|
|
||||||
$pairsRaw,
|
|
||||||
static fn (array $p): bool => !isset($refundRejectDiagSet[(int) $p['diagnosis_id']])
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 分子:曾被指派诊单的当月订单,统计诊次 = 实单全局序号 + 诊单偏移(默认偏移 0 → 第 1 笔实单为一诊)
|
|
||||||
/** @var array<int, array<int, list<array<string, mixed>>>> $slotOrdersByAssistant */
|
/** @var array<int, array<int, list<array<string, mixed>>>> $slotOrdersByAssistant */
|
||||||
$slotOrdersByAssistant = [];
|
$slotOrdersByAssistant = [];
|
||||||
$offsetMap = self::fetchRevisitSlotStartOffsetMap(array_keys($candidateDiagSet));
|
|
||||||
foreach (array_chunk(array_keys($candidateDiagSet), 2000) as $chunk) {
|
foreach (array_chunk(array_keys($candidateDiagSet), 2000) as $chunk) {
|
||||||
$orderRows = self::fetchOrderSeqRows(
|
$orderRows = self::fetchOrderSeqRows(
|
||||||
$chunk,
|
$chunk,
|
||||||
@@ -422,7 +387,6 @@ class RevisitRateLogic
|
|||||||
$seq = 0;
|
$seq = 0;
|
||||||
$ptr = 0;
|
$ptr = 0;
|
||||||
$holder = 0;
|
$holder = 0;
|
||||||
$offset = 0;
|
|
||||||
foreach ($orderRows as $r) {
|
foreach ($orderRows as $r) {
|
||||||
$did = (int) ($r['diagnosis_id'] ?? 0);
|
$did = (int) ($r['diagnosis_id'] ?? 0);
|
||||||
if ($did <= 0) {
|
if ($did <= 0) {
|
||||||
@@ -433,10 +397,8 @@ class RevisitRateLogic
|
|||||||
$seq = 0;
|
$seq = 0;
|
||||||
$ptr = 0;
|
$ptr = 0;
|
||||||
$holder = 0;
|
$holder = 0;
|
||||||
$offset = self::resolveRevisitSlotStartOffset($did, $offsetMap);
|
|
||||||
}
|
}
|
||||||
$seq++;
|
$seq++;
|
||||||
$effectiveSlot = $seq + $offset;
|
|
||||||
$ct = (int) ($r['create_time'] ?? 0);
|
$ct = (int) ($r['create_time'] ?? 0);
|
||||||
// 推进时间线指针:订单时间之前(含同刻)最近一次指派的持有人
|
// 推进时间线指针:订单时间之前(含同刻)最近一次指派的持有人
|
||||||
$tl = $timeline[$did] ?? [];
|
$tl = $timeline[$did] ?? [];
|
||||||
@@ -445,27 +407,24 @@ class RevisitRateLogic
|
|||||||
$holder = (int) $tl[$ptr]['to'];
|
$holder = (int) $tl[$ptr]['to'];
|
||||||
$ptr++;
|
$ptr++;
|
||||||
}
|
}
|
||||||
if ($effectiveSlot < 2 || $effectiveSlot > self::MAX_VISIT_SLOT) {
|
if ($seq < 2 || $seq > self::MAX_VISIT_SLOT) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if ($ct < $startTs || $ct > $endTs) {
|
if ($ct < $startTs || $ct > $endTs) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if ($holder > 0) {
|
if ($holder > 0) {
|
||||||
$slotOrdersByAssistant[$holder][$effectiveSlot][] = $r;
|
$slotOrdersByAssistant[$holder][$seq][] = $r;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 医助归属部门 + 部门筛选(默认仅二中心子树;选定部门时再收窄,含组织下级)
|
// 医助归属部门 + 部门筛选(含组织下级)
|
||||||
$universeIds = array_keys($diagsByAssistant + $slotOrdersByAssistant);
|
$universeIds = array_keys($diagsByAssistant + $slotOrdersByAssistant);
|
||||||
[$assistantDept, $deptNames] = self::buildAssistantDeptIndex($universeIds);
|
[$assistantDept, $deptNames] = self::buildAssistantDeptIndex($universeIds);
|
||||||
$subtreeSet = self::resolveDeptFilterSet($params['dept_ids'] ?? null);
|
$deptFilterIds = self::parseDeptIds($params['dept_ids'] ?? null);
|
||||||
if ($subtreeSet === []) {
|
if ($deptFilterIds !== []) {
|
||||||
// 无二中心部门时整表为空,避免误展示其它中心数据
|
$subtreeSet = self::expandDeptSubtreeSet($deptFilterIds);
|
||||||
$diagsByAssistant = [];
|
|
||||||
$slotOrdersByAssistant = [];
|
|
||||||
} else {
|
|
||||||
foreach ($universeIds as $aid) {
|
foreach ($universeIds as $aid) {
|
||||||
$deptId = (int) ($assistantDept[$aid] ?? 0);
|
$deptId = (int) ($assistantDept[$aid] ?? 0);
|
||||||
if ($deptId <= 0 || !isset($subtreeSet[$deptId])) {
|
if ($deptId <= 0 || !isset($subtreeSet[$deptId])) {
|
||||||
@@ -584,45 +543,6 @@ class RevisitRateLogic
|
|||||||
return [$canonical, $names];
|
return [$canonical, $names];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 部门筛选集合:始终落在「二中心」子树内。
|
|
||||||
* - 未传 dept_ids:整棵二中心子树
|
|
||||||
* - 已传:所选部门及其下级 ∩ 二中心子树(非法/非二中心 id 被忽略)
|
|
||||||
*
|
|
||||||
* @param mixed $raw
|
|
||||||
*
|
|
||||||
* @return array<int, true>
|
|
||||||
*/
|
|
||||||
private static function resolveDeptFilterSet(mixed $raw): array
|
|
||||||
{
|
|
||||||
$erSet = DeptLogic::getErCenterSubtreeDeptIdSet();
|
|
||||||
if ($erSet === []) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
$deptFilterIds = self::parseDeptIds($raw);
|
|
||||||
if ($deptFilterIds === []) {
|
|
||||||
return $erSet;
|
|
||||||
}
|
|
||||||
$allowedRoots = [];
|
|
||||||
foreach ($deptFilterIds as $id) {
|
|
||||||
if (isset($erSet[$id])) {
|
|
||||||
$allowedRoots[] = $id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ($allowedRoots === []) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
$expanded = self::expandDeptSubtreeSet($allowedRoots);
|
|
||||||
$out = [];
|
|
||||||
foreach ($expanded as $id => $_) {
|
|
||||||
if (isset($erSet[$id])) {
|
|
||||||
$out[$id] = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $out;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param mixed $raw int[] | 逗号分隔字符串
|
* @param mixed $raw int[] | 逗号分隔字符串
|
||||||
*
|
*
|
||||||
@@ -676,35 +596,6 @@ class RevisitRateLogic
|
|||||||
return $set;
|
return $set;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 名下存在履约「拒收(9) / 退款(10)」业务订单(未软删)的诊单集合。
|
|
||||||
* 用于「当月被指派总数」分母过滤;不限订单创建月份。
|
|
||||||
*
|
|
||||||
* @param list<int> $diagIds
|
|
||||||
*
|
|
||||||
* @return array<int, true>
|
|
||||||
*/
|
|
||||||
private static function fetchRefundOrRejectDiagnosisSet(array $diagIds): array
|
|
||||||
{
|
|
||||||
if ($diagIds === []) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
$out = [];
|
|
||||||
foreach (array_chunk($diagIds, 2000) as $chunk) {
|
|
||||||
$ids = Db::name('tcm_prescription_order')
|
|
||||||
->whereIn('diagnosis_id', $chunk)
|
|
||||||
->whereNull('delete_time')
|
|
||||||
->whereIn('fulfillment_status', [9, 10])
|
|
||||||
->group('diagnosis_id')
|
|
||||||
->column('diagnosis_id');
|
|
||||||
foreach ($ids as $id) {
|
|
||||||
$out[(int) $id] = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $out;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 诊单订单序列源查询(与业绩口径一致),统一排序保证序号稳定。
|
* 诊单订单序列源查询(与业绩口径一致),统一排序保证序号稳定。
|
||||||
*
|
*
|
||||||
@@ -728,48 +619,6 @@ class RevisitRateLogic
|
|||||||
->toArray();
|
->toArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param list<int> $diagIds
|
|
||||||
*
|
|
||||||
* @return array<int, int> diagnosis_id => revisit_slot_start_offset
|
|
||||||
*/
|
|
||||||
private static function fetchRevisitSlotStartOffsetMap(array $diagIds): array
|
|
||||||
{
|
|
||||||
if ($diagIds === []) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
$out = [];
|
|
||||||
foreach (array_chunk($diagIds, 2000) as $chunk) {
|
|
||||||
$rows = Db::name('tcm_diagnosis')
|
|
||||||
->whereIn('id', $chunk)
|
|
||||||
->whereNull('delete_time')
|
|
||||||
->column('revisit_slot_start_offset', 'id');
|
|
||||||
foreach ($rows as $id => $offset) {
|
|
||||||
$out[(int) $id] = (int) $offset;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $out;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 诊单复诊统计起始偏移(默认 0:第 1 笔实单计为一诊;统计诊次 = 实单序号 + 偏移)
|
|
||||||
*
|
|
||||||
* @param array<int, int> $offsetMap
|
|
||||||
*/
|
|
||||||
private static function resolveRevisitSlotStartOffset(int $diagId, array $offsetMap): int
|
|
||||||
{
|
|
||||||
$offset = (int) ($offsetMap[$diagId] ?? 0);
|
|
||||||
if ($offset < 0) {
|
|
||||||
$offset = 0;
|
|
||||||
}
|
|
||||||
if ($offset > 20) {
|
|
||||||
$offset = 20;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $offset;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param list<int> $diagIds
|
* @param list<int> $diagIds
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -748,49 +748,6 @@ class DiagnosisLogic extends BaseLogic
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @notes 诊单指派医助操作记录列表
|
|
||||||
*/
|
|
||||||
public static function setRevisitSlotStartOffset(int $diagnosisId, int $offset, array $adminInfo): bool
|
|
||||||
{
|
|
||||||
self::$error = '';
|
|
||||||
if ($diagnosisId <= 0) {
|
|
||||||
self::setError('诊单不存在');
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if ($offset < 0 || $offset > 20) {
|
|
||||||
self::setError('起始偏移须在 0~20 之间');
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
$diagnosis = Diagnosis::where('id', $diagnosisId)->whereNull('delete_time')->find();
|
|
||||||
if (!$diagnosis) {
|
|
||||||
self::setError('诊单不存在');
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
$old = (int) ($diagnosis->revisit_slot_start_offset ?? 0);
|
|
||||||
if ($old < 0) {
|
|
||||||
$old = 0;
|
|
||||||
}
|
|
||||||
if ($old > 20) {
|
|
||||||
$old = 20;
|
|
||||||
}
|
|
||||||
if ($old === $offset) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
$diagnosis->save(['revisit_slot_start_offset' => $offset]);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
self::setError($e->getMessage());
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @notes 诊单指派医助操作记录列表
|
* @notes 诊单指派医助操作记录列表
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -2375,123 +2375,6 @@ class PrescriptionOrderLogic
|
|||||||
->toArray();
|
->toArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 手工新增操作日志;可选单独调整处方审核 / 支付单审核状态(不触发常规审核流程副作用)
|
|
||||||
*
|
|
||||||
* @param array<string,mixed> $params id, summary, prescription_audit_status?, payment_slip_audit_status?, prescription_audit_remark?, payment_slip_audit_remark?
|
|
||||||
* @return array<string,mixed>|false
|
|
||||||
*/
|
|
||||||
public static function addLog(array $params, int $adminId, array $adminInfo)
|
|
||||||
{
|
|
||||||
self::$error = '';
|
|
||||||
$id = (int) ($params['id'] ?? 0);
|
|
||||||
$summary = mb_substr(trim((string) ($params['summary'] ?? '')), 0, 500);
|
|
||||||
if ($summary === '') {
|
|
||||||
self::$error = '请填写日志内容';
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$order = PrescriptionOrder::where('id', $id)->whereNull('delete_time')->find();
|
|
||||||
if (!$order) {
|
|
||||||
self::$error = '订单不存在';
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!self::canAccessOrder($order, $adminId, $adminInfo)) {
|
|
||||||
self::$error = '无权限操作';
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$changeParts = [];
|
|
||||||
$hasRxChange = array_key_exists('prescription_audit_status', $params)
|
|
||||||
&& $params['prescription_audit_status'] !== ''
|
|
||||||
&& $params['prescription_audit_status'] !== null;
|
|
||||||
$hasPayChange = array_key_exists('payment_slip_audit_status', $params)
|
|
||||||
&& $params['payment_slip_audit_status'] !== ''
|
|
||||||
&& $params['payment_slip_audit_status'] !== null;
|
|
||||||
|
|
||||||
if ($hasRxChange) {
|
|
||||||
if (!self::canAuditPrescriptionOrder($adminInfo)) {
|
|
||||||
self::$error = '无处方审核权限,不能调整处方审核状态';
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
$newRx = (int) $params['prescription_audit_status'];
|
|
||||||
if (!in_array($newRx, [0, 1, 2], true)) {
|
|
||||||
self::$error = '处方审核状态无效';
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
$oldRx = (int) $order->prescription_audit_status;
|
|
||||||
if ($newRx !== $oldRx) {
|
|
||||||
$order->prescription_audit_status = $newRx;
|
|
||||||
$changeParts[] = '处方审核:' . self::auditStatusLabelForLog($oldRx)
|
|
||||||
. ' → ' . self::auditStatusLabelForLog($newRx);
|
|
||||||
}
|
|
||||||
if (array_key_exists('prescription_audit_remark', $params)) {
|
|
||||||
$order->prescription_audit_remark = mb_substr(trim((string) $params['prescription_audit_remark']), 0, 500);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($hasPayChange) {
|
|
||||||
if (!self::canAuditPaymentSlipOrder($adminInfo)) {
|
|
||||||
self::$error = '无支付单审核权限,不能调整支付单审核状态';
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
$newPay = (int) $params['payment_slip_audit_status'];
|
|
||||||
if (!in_array($newPay, [0, 1, 2], true)) {
|
|
||||||
self::$error = '支付单审核状态无效';
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
$oldPay = (int) $order->payment_slip_audit_status;
|
|
||||||
if ($newPay !== $oldPay) {
|
|
||||||
$order->payment_slip_audit_status = $newPay;
|
|
||||||
$changeParts[] = '支付单审核:' . self::auditStatusLabelForLog($oldPay)
|
|
||||||
. ' → ' . self::auditStatusLabelForLog($newPay);
|
|
||||||
}
|
|
||||||
if (array_key_exists('payment_slip_audit_remark', $params)) {
|
|
||||||
$order->payment_slip_audit_remark = mb_substr(trim((string) $params['payment_slip_audit_remark']), 0, 500);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($hasRxChange || $hasPayChange) {
|
|
||||||
self::syncFulfillmentStatus($order);
|
|
||||||
try {
|
|
||||||
$order->save();
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
self::$error = $e->getMessage();
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$logSummary = $summary;
|
|
||||||
if ($changeParts !== []) {
|
|
||||||
$logSummary .= '(' . implode(';', $changeParts) . ')';
|
|
||||||
}
|
|
||||||
self::writeLog($id, $adminId, $adminInfo, 'manual_log', $logSummary);
|
|
||||||
|
|
||||||
$out = $order->toArray();
|
|
||||||
self::maskInternalCostIfNeeded($out, $adminInfo);
|
|
||||||
self::maskRemarkExtraIfNeeded($out, $adminInfo);
|
|
||||||
self::attachLinkedPayOrders($out);
|
|
||||||
|
|
||||||
return $out;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static function auditStatusLabelForLog(int $status): string
|
|
||||||
{
|
|
||||||
return match ($status) {
|
|
||||||
1 => '已通过',
|
|
||||||
2 => '已驳回',
|
|
||||||
default => '待审核',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 为「已发货/已签收」(fulfillment_status=5/6) 的业务订单新增一条关联支付单(zyt_order),
|
* 为「已发货/已签收」(fulfillment_status=5/6) 的业务订单新增一条关联支付单(zyt_order),
|
||||||
* 创建后将支付单链接到业务订单,并将处方/支付审核状态重置为待审核以启动再次审核流程。
|
* 创建后将支付单链接到业务订单,并将处方/支付审核状态重置为待审核以启动再次审核流程。
|
||||||
@@ -3224,207 +3107,6 @@ class PrescriptionOrderLogic
|
|||||||
return $type;
|
return $type;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 导出用:处方药材明细(主方/辅方分行,与处方笺一致)
|
|
||||||
*
|
|
||||||
* @param array<string, mixed> $rx
|
|
||||||
*/
|
|
||||||
public static function formatPrescriptionHerbsForExport(array $rx): string
|
|
||||||
{
|
|
||||||
[$mainHerbs, $auxHerbs] = self::splitPrescriptionHerbsFromRx($rx);
|
|
||||||
$formatList = static function (array $herbs): string {
|
|
||||||
$parts = [];
|
|
||||||
foreach ($herbs as $h) {
|
|
||||||
if (!\is_array($h)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$name = trim((string) ($h['name'] ?? ''));
|
|
||||||
if ($name === '') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$parts[] = $name . ' ' . self::formatExportDosageNumber((float) ($h['dosage'] ?? 0)) . 'g';
|
|
||||||
}
|
|
||||||
|
|
||||||
return implode('、', $parts);
|
|
||||||
};
|
|
||||||
|
|
||||||
$sections = [];
|
|
||||||
$mainText = $formatList($mainHerbs);
|
|
||||||
if ($mainText !== '') {
|
|
||||||
$sections[] = '主方:' . $mainText;
|
|
||||||
}
|
|
||||||
$auxText = $formatList($auxHerbs);
|
|
||||||
if ($auxText !== '') {
|
|
||||||
$sections[] = '辅方:' . $auxText;
|
|
||||||
}
|
|
||||||
|
|
||||||
return implode("\n", $sections);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 导出用:主方/辅方服用方式(与前端 buildUsageSegmentText / 处方笺同口径)
|
|
||||||
*
|
|
||||||
* @param array<string, mixed> $usage
|
|
||||||
*/
|
|
||||||
public static function formatUsageSegmentForExport(
|
|
||||||
array $usage,
|
|
||||||
string $prescriptionType = '浓缩水丸',
|
|
||||||
string $fallbackWay = '',
|
|
||||||
string $fallbackTime = ''
|
|
||||||
): string {
|
|
||||||
$pt = trim($prescriptionType) !== '' ? trim($prescriptionType) : '浓缩水丸';
|
|
||||||
$times = (int) ($usage['times_per_day'] ?? 0);
|
|
||||||
if ($times <= 0) {
|
|
||||||
$times = 3;
|
|
||||||
}
|
|
||||||
$amount = isset($usage['dosage_amount']) && $usage['dosage_amount'] !== '' && $usage['dosage_amount'] !== null
|
|
||||||
? (float) $usage['dosage_amount']
|
|
||||||
: 10.0;
|
|
||||||
$unit = trim((string) ($usage['usage_dosage_unit'] ?? ($usage['dosage_unit'] ?? '')));
|
|
||||||
if ($unit === '') {
|
|
||||||
$unit = $pt === '饮片' ? 'ml' : 'g';
|
|
||||||
}
|
|
||||||
$usageWay = trim((string) ($usage['usage_way'] ?? ''));
|
|
||||||
if ($usageWay === '') {
|
|
||||||
$usageWay = $fallbackWay !== '' ? $fallbackWay : '温水送服';
|
|
||||||
}
|
|
||||||
$usageTime = trim((string) ($usage['usage_time'] ?? ''));
|
|
||||||
if ($usageTime === '') {
|
|
||||||
$usageTime = $fallbackTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
$seg = ['每天' . $times . '次'];
|
|
||||||
if ($pt === '浓缩水丸') {
|
|
||||||
$bags = (int) ($usage['dosage_bag_count'] ?? 0);
|
|
||||||
if ($bags <= 0) {
|
|
||||||
$bags = 1;
|
|
||||||
}
|
|
||||||
$seg[] = '一次' . $bags . '袋';
|
|
||||||
$seg[] = '每袋' . self::formatExportDosageNumber($amount) . $unit;
|
|
||||||
} else {
|
|
||||||
$seg[] = '一次' . self::formatExportDosageNumber($amount) . $unit;
|
|
||||||
}
|
|
||||||
$seg[] = $usageWay;
|
|
||||||
if ($usageTime !== '') {
|
|
||||||
$seg[] = $usageTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
return implode(', ', $seg);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 导出用:辅方用法 JSON 规范化(与前端 normalizeSlipAuxUsageForm 默认值一致)
|
|
||||||
*
|
|
||||||
* @return array<string, mixed>
|
|
||||||
*/
|
|
||||||
private static function normalizeAuxUsageForExport($raw, string $prescriptionType): array
|
|
||||||
{
|
|
||||||
$pt = trim($prescriptionType) !== '' ? trim($prescriptionType) : '浓缩水丸';
|
|
||||||
if ($pt === '饮片') {
|
|
||||||
$base = [
|
|
||||||
'dosage_amount' => 50.0,
|
|
||||||
'dosage_bag_count' => 1,
|
|
||||||
'times_per_day' => 3,
|
|
||||||
'usage_days' => 7,
|
|
||||||
];
|
|
||||||
} elseif ($pt === '浓缩水丸') {
|
|
||||||
$base = [
|
|
||||||
'dosage_amount' => 5.0,
|
|
||||||
'dosage_bag_count' => 1,
|
|
||||||
'times_per_day' => 3,
|
|
||||||
'usage_days' => 7,
|
|
||||||
];
|
|
||||||
} else {
|
|
||||||
$base = [
|
|
||||||
'dosage_amount' => 1.0,
|
|
||||||
'dosage_bag_count' => 1,
|
|
||||||
'times_per_day' => 3,
|
|
||||||
'usage_days' => 7,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (\is_string($raw) && $raw !== '') {
|
|
||||||
$decoded = json_decode($raw, true);
|
|
||||||
$raw = \is_array($decoded) ? $decoded : null;
|
|
||||||
}
|
|
||||||
if (!\is_array($raw)) {
|
|
||||||
return $base;
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
'dosage_amount' => isset($raw['dosage_amount']) && $raw['dosage_amount'] !== '' && $raw['dosage_amount'] !== null
|
|
||||||
? (float) $raw['dosage_amount']
|
|
||||||
: $base['dosage_amount'],
|
|
||||||
'dosage_bag_count' => (int) ($raw['dosage_bag_count'] ?? 0) > 0
|
|
||||||
? (int) $raw['dosage_bag_count']
|
|
||||||
: $base['dosage_bag_count'],
|
|
||||||
'times_per_day' => (int) ($raw['times_per_day'] ?? 0) > 0
|
|
||||||
? (int) $raw['times_per_day']
|
|
||||||
: $base['times_per_day'],
|
|
||||||
'usage_days' => (int) ($raw['usage_days'] ?? 0) > 0
|
|
||||||
? (int) $raw['usage_days']
|
|
||||||
: $base['usage_days'],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array{0: array<int, array<string, mixed>>, 1: array<int, array<string, mixed>>}
|
|
||||||
*/
|
|
||||||
private static function splitPrescriptionHerbsFromRx(array $rx): array
|
|
||||||
{
|
|
||||||
$herbs = $rx['herbs'] ?? null;
|
|
||||||
if (\is_string($herbs) && $herbs !== '') {
|
|
||||||
$decoded = json_decode($herbs, true);
|
|
||||||
$herbs = \is_array($decoded) ? $decoded : [];
|
|
||||||
}
|
|
||||||
if (!\is_array($herbs)) {
|
|
||||||
$herbs = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
$mainHerbs = [];
|
|
||||||
$auxHerbs = [];
|
|
||||||
foreach ($herbs as $h) {
|
|
||||||
if (!\is_array($h)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (((string) ($h['formula_type'] ?? '')) === '辅方') {
|
|
||||||
$auxHerbs[] = $h;
|
|
||||||
} else {
|
|
||||||
$mainHerbs[] = $h;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return [$mainHerbs, $auxHerbs];
|
|
||||||
}
|
|
||||||
|
|
||||||
private static function formatExportDosageNumber(float $dosage): string
|
|
||||||
{
|
|
||||||
if (floor($dosage) === $dosage) {
|
|
||||||
return (string) (int) $dosage;
|
|
||||||
}
|
|
||||||
|
|
||||||
return rtrim(rtrim(number_format($dosage, 4, '.', ''), '0'), '.');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 导出用:主方/辅方开立天数(与详情侧栏「处方开立」同口径:主方取处方 usage_days,辅方取 aux_usage.usage_days)
|
|
||||||
* 订单服用天数单独导出在 export_medication_days 列,不在此混用。
|
|
||||||
*
|
|
||||||
* @param array<string, mixed> $rx
|
|
||||||
* @param array<string, mixed>|null $auxUsage
|
|
||||||
*/
|
|
||||||
private static function resolveExportUsageDays(array $rx, ?array $auxUsage, bool $isAux): string
|
|
||||||
{
|
|
||||||
if ($isAux) {
|
|
||||||
$days = (int) ($auxUsage['usage_days'] ?? 0);
|
|
||||||
|
|
||||||
return $days > 0 ? (string) $days : '';
|
|
||||||
}
|
|
||||||
$days = (int) ($rx['usage_days'] ?? 0);
|
|
||||||
|
|
||||||
return $days > 0 ? (string) $days : '';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 导出列:挂号表渠道来源展示(与 AppointmentLists channel_source_desc 同字典口径)
|
* 导出列:挂号表渠道来源展示(与 AppointmentLists channel_source_desc 同字典口径)
|
||||||
*
|
*
|
||||||
@@ -3721,12 +3403,7 @@ class PrescriptionOrderLogic
|
|||||||
$rxById = [];
|
$rxById = [];
|
||||||
if ($rxIdList !== []) {
|
if ($rxIdList !== []) {
|
||||||
$rxRows = Prescription::whereIn('id', $rxIdList)->whereNull('delete_time')
|
$rxRows = Prescription::whereIn('id', $rxIdList)->whereNull('delete_time')
|
||||||
->field([
|
->field(['id', 'prescription_type', 'need_decoction', 'dose_unit', 'assistant_id', 'appointment_id', 'prescription_name', 'aux_usage', 'herbs', 'creator_id'])
|
||||||
'id', 'prescription_type', 'need_decoction', 'dose_unit', 'assistant_id', 'appointment_id',
|
|
||||||
'prescription_name', 'aux_usage', 'herbs', 'creator_id',
|
|
||||||
'dosage_amount', 'dosage_unit', 'dosage_bag_count', 'times_per_day', 'usage_days',
|
|
||||||
'usage_way', 'usage_time',
|
|
||||||
])
|
|
||||||
->select()
|
->select()
|
||||||
->toArray();
|
->toArray();
|
||||||
foreach ($rxRows as $xr) {
|
foreach ($rxRows as $xr) {
|
||||||
@@ -3960,38 +3637,6 @@ class PrescriptionOrderLogic
|
|||||||
}
|
}
|
||||||
$item['export_prescription_name'] = implode(' ', $rxNameParts);
|
$item['export_prescription_name'] = implode(' ', $rxNameParts);
|
||||||
|
|
||||||
$rxArr = \is_array($rx) ? $rx : [];
|
|
||||||
$rxType = trim((string) ($rxArr['prescription_type'] ?? '')) ?: '浓缩水丸';
|
|
||||||
$item['export_prescription_herbs'] = self::formatPrescriptionHerbsForExport($rxArr);
|
|
||||||
$item['export_main_usage'] = $rxArr !== []
|
|
||||||
? self::formatUsageSegmentForExport($rxArr, $rxType)
|
|
||||||
: '';
|
|
||||||
[, $auxHerbs] = self::splitPrescriptionHerbsFromRx($rxArr);
|
|
||||||
$auxUsageNorm = $auxHerbs !== []
|
|
||||||
? self::normalizeAuxUsageForExport($rxArr['aux_usage'] ?? null, $rxType)
|
|
||||||
: null;
|
|
||||||
if ($auxHerbs !== [] && $auxUsageNorm !== null) {
|
|
||||||
$item['export_aux_usage'] = self::formatUsageSegmentForExport(
|
|
||||||
[
|
|
||||||
'dosage_amount' => $auxUsageNorm['dosage_amount'],
|
|
||||||
'dosage_bag_count' => $auxUsageNorm['dosage_bag_count'],
|
|
||||||
'times_per_day' => $auxUsageNorm['times_per_day'],
|
|
||||||
'usage_dosage_unit' => $rxArr['dosage_unit'] ?? '',
|
|
||||||
'usage_way' => $rxArr['usage_way'] ?? '',
|
|
||||||
'usage_time' => $rxArr['usage_time'] ?? '',
|
|
||||||
],
|
|
||||||
$rxType,
|
|
||||||
(string) ($rxArr['usage_way'] ?? ''),
|
|
||||||
(string) ($rxArr['usage_time'] ?? '')
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
$item['export_aux_usage'] = '';
|
|
||||||
}
|
|
||||||
$item['export_main_usage_days'] = self::resolveExportUsageDays($rxArr, $auxUsageNorm, false);
|
|
||||||
$item['export_aux_usage_days'] = $auxHerbs !== []
|
|
||||||
? self::resolveExportUsageDays($rxArr, $auxUsageNorm, true)
|
|
||||||
: '';
|
|
||||||
|
|
||||||
$item['export_service_package'] = self::formatServicePackageForExport(
|
$item['export_service_package'] = self::formatServicePackageForExport(
|
||||||
$item['service_package'] ?? '',
|
$item['service_package'] ?? '',
|
||||||
$packageNameByValue
|
$packageNameByValue
|
||||||
@@ -4162,7 +3807,27 @@ class PrescriptionOrderLogic
|
|||||||
|
|
||||||
$doctorId = (int) ($rx['creator_id'] ?? 0);
|
$doctorId = (int) ($rx['creator_id'] ?? 0);
|
||||||
|
|
||||||
[$mainHerbs, $auxHerbs] = self::splitPrescriptionHerbsFromRx($rx);
|
$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 {
|
$lookup = static function (string $ft, array $hs) use ($doctorId, $libByDoctor, $libPublic): string {
|
||||||
if ($hs === []) {
|
if ($hs === []) {
|
||||||
@@ -4881,175 +4546,4 @@ class PrescriptionOrderLogic
|
|||||||
{
|
{
|
||||||
self::writeLog($prescriptionOrderId, $adminId, $adminInfo, 'patch_rx_patient', $summary);
|
self::writeLog($prescriptionOrderId, $adminId, $adminInfo, 'patch_rx_patient', $summary);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 业务订单详情场景:更新主方/辅方服用次数与开立天数,以及订单服用天数
|
|
||||||
*
|
|
||||||
* @param array<string, mixed> $params
|
|
||||||
*/
|
|
||||||
public static function patchPrescriptionUsage(array $params, int $adminId, array $adminInfo): bool
|
|
||||||
{
|
|
||||||
self::$error = '';
|
|
||||||
$prescriptionOrderId = (int) ($params['id'] ?? 0);
|
|
||||||
$order = PrescriptionOrder::where('id', $prescriptionOrderId)->whereNull('delete_time')->find();
|
|
||||||
if (!$order) {
|
|
||||||
self::setError('订单不存在');
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!self::canAccessOrder($order, $adminId, $adminInfo)) {
|
|
||||||
self::setError('无权限操作');
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if ((int) $order->fulfillment_status === 4) {
|
|
||||||
self::setError('已取消的订单不可修改');
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
$rxId = (int) ($order->prescription_id ?? 0);
|
|
||||||
if ($rxId <= 0) {
|
|
||||||
self::setError('该订单未关联处方');
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
$rx = Prescription::where('id', $rxId)->whereNull('delete_time')->find();
|
|
||||||
if (!$rx) {
|
|
||||||
self::setError('处方不存在');
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!PrescriptionLogic::canViewPrescription($rx, $adminId, $adminInfo)) {
|
|
||||||
self::setError('无权限修改此处方');
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$timesPerDay = (int) ($params['times_per_day'] ?? 0);
|
|
||||||
$usageDays = (int) ($params['usage_days'] ?? 0);
|
|
||||||
$medDays = (int) ($params['medication_days'] ?? 0);
|
|
||||||
if ($timesPerDay < 1 || $timesPerDay > 6) {
|
|
||||||
self::setError('主方每天次数须在 1~6 之间');
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if ($usageDays < 1 || $usageDays > 999) {
|
|
||||||
self::setError('主方开立天数须在 1~999 之间');
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if ($medDays < 1 || $medDays > 999) {
|
|
||||||
self::setError('订单服用天数须在 1~999 之间');
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$hasAux = self::prescriptionHasAuxFormula($rx);
|
|
||||||
$auxTimesPerDay = null;
|
|
||||||
$auxUsageDays = null;
|
|
||||||
if ($hasAux) {
|
|
||||||
if (!array_key_exists('aux_times_per_day', $params) || !array_key_exists('aux_usage_days', $params)) {
|
|
||||||
self::setError('含辅方处方须填写辅方服用参数');
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
$auxTimesPerDay = (int) $params['aux_times_per_day'];
|
|
||||||
$auxUsageDays = (int) $params['aux_usage_days'];
|
|
||||||
if ($auxTimesPerDay < 1 || $auxTimesPerDay > 6) {
|
|
||||||
self::setError('辅方每天次数须在 1~6 之间');
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if ($auxUsageDays < 1 || $auxUsageDays > 999) {
|
|
||||||
self::setError('辅方开立天数须在 1~999 之间');
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$oldTimes = (int) ($rx->times_per_day ?? 0);
|
|
||||||
$oldUsageDays = (int) ($rx->usage_days ?? 0);
|
|
||||||
$oldMedDays = (int) ($order->medication_days ?? 0);
|
|
||||||
$oldAuxUsage = $rx->aux_usage;
|
|
||||||
if (is_string($oldAuxUsage)) {
|
|
||||||
$decoded = json_decode($oldAuxUsage, true);
|
|
||||||
$oldAuxUsage = is_array($decoded) ? $decoded : [];
|
|
||||||
}
|
|
||||||
if (!is_array($oldAuxUsage)) {
|
|
||||||
$oldAuxUsage = [];
|
|
||||||
}
|
|
||||||
$oldAuxTimes = (int) ($oldAuxUsage['times_per_day'] ?? 0);
|
|
||||||
$oldAuxUsageDays = (int) ($oldAuxUsage['usage_days'] ?? 0);
|
|
||||||
|
|
||||||
try {
|
|
||||||
$rxUpdates = [
|
|
||||||
'times_per_day' => $timesPerDay,
|
|
||||||
'usage_days' => $usageDays,
|
|
||||||
];
|
|
||||||
if ($hasAux) {
|
|
||||||
$auxUsage = $oldAuxUsage;
|
|
||||||
$auxUsage['times_per_day'] = $auxTimesPerDay;
|
|
||||||
$auxUsage['usage_days'] = $auxUsageDays;
|
|
||||||
$rxUpdates['aux_usage'] = $auxUsage;
|
|
||||||
}
|
|
||||||
$rx->save($rxUpdates);
|
|
||||||
|
|
||||||
$order->medication_days = $medDays;
|
|
||||||
$order->save();
|
|
||||||
|
|
||||||
$parts = [
|
|
||||||
sprintf(
|
|
||||||
'主方 每天%d次/开立%d天 → 每天%d次/开立%d天',
|
|
||||||
$oldTimes > 0 ? $oldTimes : 0,
|
|
||||||
$oldUsageDays > 0 ? $oldUsageDays : 0,
|
|
||||||
$timesPerDay,
|
|
||||||
$usageDays
|
|
||||||
),
|
|
||||||
];
|
|
||||||
if ($hasAux) {
|
|
||||||
$parts[] = sprintf(
|
|
||||||
'辅方 每天%d次/开立%d天 → 每天%d次/开立%d天',
|
|
||||||
$oldAuxTimes > 0 ? $oldAuxTimes : 0,
|
|
||||||
$oldAuxUsageDays > 0 ? $oldAuxUsageDays : 0,
|
|
||||||
$auxTimesPerDay,
|
|
||||||
$auxUsageDays
|
|
||||||
);
|
|
||||||
}
|
|
||||||
$parts[] = sprintf(
|
|
||||||
'订单设置 %d天 → %d天',
|
|
||||||
$oldMedDays > 0 ? $oldMedDays : 0,
|
|
||||||
$medDays
|
|
||||||
);
|
|
||||||
$summary = '服用参数:' . implode(';', $parts);
|
|
||||||
self::writeLog($prescriptionOrderId, $adminId, $adminInfo, 'patch_rx_usage', $summary);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
self::setError($e->getMessage());
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处方是否含辅方药材(与列表/详情 has_aux_formula 口径一致)
|
|
||||||
*/
|
|
||||||
private static function prescriptionHasAuxFormula(Prescription $rx): bool
|
|
||||||
{
|
|
||||||
$herbs = $rx->herbs;
|
|
||||||
if (is_string($herbs)) {
|
|
||||||
$decoded = json_decode($herbs, true);
|
|
||||||
$herbs = is_array($decoded) ? $decoded : [];
|
|
||||||
}
|
|
||||||
if (!is_array($herbs)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
foreach ($herbs as $h) {
|
|
||||||
if (is_array($h) && (string) ($h['formula_type'] ?? '') === '辅方') {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,6 @@ class DiagnosisValidate extends BaseValidate
|
|||||||
'end_date' => 'date|checkDateRange',
|
'end_date' => 'date|checkDateRange',
|
||||||
'diagnosis_id' => 'require|integer|checkDiagnosisId',
|
'diagnosis_id' => 'require|integer|checkDiagnosisId',
|
||||||
'tracking_content' => 'require|length:1,1000',
|
'tracking_content' => 'require|length:1,1000',
|
||||||
'revisit_slot_start_offset' => 'integer|between:0,20',
|
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $message = [
|
protected $message = [
|
||||||
@@ -140,13 +139,6 @@ class DiagnosisValidate extends BaseValidate
|
|||||||
return $this->only(['id']);
|
return $this->only(['id']);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 业务订单 tab:设置复诊接诊率统计起始偏移 */
|
|
||||||
public function sceneSetRevisitSlotStartOffset()
|
|
||||||
{
|
|
||||||
return $this->only(['id', 'revisit_slot_start_offset'])
|
|
||||||
->append('revisit_slot_start_offset', 'require|integer|between:0,20');
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function checkDiagnosis($value)
|
protected function checkDiagnosis($value)
|
||||||
{
|
{
|
||||||
$diagnosis = Diagnosis::findOrEmpty($value);
|
$diagnosis = Diagnosis::findOrEmpty($value);
|
||||||
|
|||||||
@@ -32,11 +32,6 @@ class PrescriptionOrderValidate extends BaseValidate
|
|||||||
'remark_assistant' => 'max:500',
|
'remark_assistant' => 'max:500',
|
||||||
'action' => 'require|in:approve,reject',
|
'action' => 'require|in:approve,reject',
|
||||||
'remark' => 'max:500',
|
'remark' => 'max:500',
|
||||||
'summary' => 'require|max:500',
|
|
||||||
'prescription_audit_status' => 'in:0,1,2',
|
|
||||||
'payment_slip_audit_status' => 'in:0,1,2',
|
|
||||||
'prescription_audit_remark' => 'max:500',
|
|
||||||
'payment_slip_audit_remark' => 'max:500',
|
|
||||||
'fulfillment_status' => 'require|integer|in:3,7,8,9,11,12',
|
'fulfillment_status' => 'require|integer|in:3,7,8,9,11,12',
|
||||||
'reason' => 'require|max:500',
|
'reason' => 'require|max:500',
|
||||||
'refund_amount' => 'float|egt:0',
|
'refund_amount' => 'float|egt:0',
|
||||||
@@ -79,7 +74,6 @@ class PrescriptionOrderValidate extends BaseValidate
|
|||||||
'withdraw' => ['id'],
|
'withdraw' => ['id'],
|
||||||
'ship' => ['id', 'tracking_number', 'express_company', 'ship_mode'],
|
'ship' => ['id', 'tracking_number', 'express_company', 'ship_mode'],
|
||||||
'logs' => ['id'],
|
'logs' => ['id'],
|
||||||
'addLog' => ['id', 'summary'],
|
|
||||||
'paidPayOrders' => ['diagnosis_id'],
|
'paidPayOrders' => ['diagnosis_id'],
|
||||||
'addPayOrder' => ['id', 'order_type', 'pay_amount', 'pay_remark'],
|
'addPayOrder' => ['id', 'order_type', 'pay_amount', 'pay_remark'],
|
||||||
'linkPayOrder' => ['id', 'pay_order_id'],
|
'linkPayOrder' => ['id', 'pay_order_id'],
|
||||||
@@ -89,7 +83,6 @@ class PrescriptionOrderValidate extends BaseValidate
|
|||||||
'submitGancaoRecipel' => ['id'],
|
'submitGancaoRecipel' => ['id'],
|
||||||
'previewGancaoRecipel' => ['id'],
|
'previewGancaoRecipel' => ['id'],
|
||||||
'patchPrescriptionPatient' => ['id', 'patient_name', 'phone'],
|
'patchPrescriptionPatient' => ['id', 'patient_name', 'phone'],
|
||||||
'patchPrescriptionUsage' => ['id', 'times_per_day', 'usage_days', 'medication_days', 'aux_times_per_day', 'aux_usage_days'],
|
|
||||||
'updateAmount' => ['id', 'amount'],
|
'updateAmount' => ['id', 'amount'],
|
||||||
'setShipMode' => ['id', 'ship_mode'],
|
'setShipMode' => ['id', 'ship_mode'],
|
||||||
];
|
];
|
||||||
@@ -100,15 +93,4 @@ class PrescriptionOrderValidate extends BaseValidate
|
|||||||
->append('id', 'require|integer|gt:0')
|
->append('id', 'require|integer|gt:0')
|
||||||
->append('amount', 'require|float|egt:0');
|
->append('amount', 'require|float|egt:0');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function patchPrescriptionUsage(): PrescriptionOrderValidate
|
|
||||||
{
|
|
||||||
return $this->only(['id', 'times_per_day', 'usage_days', 'medication_days', 'aux_times_per_day', 'aux_usage_days'])
|
|
||||||
->append('id', 'require|integer|gt:0')
|
|
||||||
->append('times_per_day', 'require|integer|between:1,6')
|
|
||||||
->append('usage_days', 'require|integer|between:1,999')
|
|
||||||
->append('medication_days', 'require|integer|between:1,999')
|
|
||||||
->append('aux_times_per_day', 'integer|between:1,6')
|
|
||||||
->append('aux_usage_days', 'integer|between:1,999');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,571 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace app\command;
|
|
||||||
|
|
||||||
use app\adminapi\logic\dept\DeptLogic;
|
|
||||||
use app\adminapi\logic\stats\RevisitRateLogic;
|
|
||||||
use think\console\Command;
|
|
||||||
use think\console\Input;
|
|
||||||
use think\console\input\Option;
|
|
||||||
use think\console\Output;
|
|
||||||
use think\facade\Db;
|
|
||||||
use think\facade\Log;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 待分配诊单自动指派(每天定时执行)
|
|
||||||
*
|
|
||||||
* 使用方法:
|
|
||||||
* php think tcm:auto-assign-pending # 正式执行
|
|
||||||
* php think tcm:auto-assign-pending --dry-run # 只演练输出分配计划,不写库
|
|
||||||
*
|
|
||||||
* 推荐 cron(每天 09:00 执行一次;或经 zyt_dev_crontab 表调度):
|
|
||||||
* 0 9 * * * cd /path/to/server && php think tcm:auto-assign-pending >> /var/log/zyt-auto-assign.log 2>&1
|
|
||||||
*
|
|
||||||
* 分配规则(依据上个自然月「二诊复诊接诊率」,与 RevisitRateLogic::overview 口径完全一致):
|
|
||||||
* - 接诊率 > 70% :第一优先档,当日每人最多 3 条
|
|
||||||
* - 接诊率 60% ~ 70% :第二档,当日每人最多 2 条
|
|
||||||
* - 接诊率 50% ~ 60% :第三档,当日每人最多 1 条
|
|
||||||
* - 接诊率 < 50% 或上月无被指派数据:不参与分配
|
|
||||||
* - 仅分配给「二中心」及其组织下级部门的在职医助(当前部门校验,调离二中心即不再参与)
|
|
||||||
*
|
|
||||||
* 轮询方式:按轮次分配,每轮内先 >70% 档每人 1 条,再 60%~70% 档每人 1 条,再 50%~60% 档每人 1 条;
|
|
||||||
* 一轮结束还有剩余待指派诊单则进入下一轮,直至待指派池为空或所有医助当日额度用尽。
|
|
||||||
* 例:当天 20 条,>70% 有 3 人、60%~70% 有 7 人、50%~60% 有 5 人 →
|
|
||||||
* 第一轮 3+7+5=15 条;剩余 5 条进第二轮:>70% 再各 1 条(3 条),余 2 条给 60%~70% 档前 2 人。
|
|
||||||
*
|
|
||||||
* 日上限跨执行累计:同一天重复执行命令时,会先从 tcm_diagnosis_auto_assign_log 扣减当日已自动分配数,不会超额。
|
|
||||||
*
|
|
||||||
* 日志:无论分配与否,每条待指派诊单都会写入 tcm_diagnosis_auto_assign_log,记录原因(为什么分配 / 为什么不分配)。
|
|
||||||
* 成功分配同时写 tcm_diagnosis_assign_log(is_inherit=0,计入次月接诊率分母),与手动指派同口径。
|
|
||||||
*/
|
|
||||||
class AutoAssignPendingDiagnosis extends Command
|
|
||||||
{
|
|
||||||
/** 档位标识 */
|
|
||||||
private const TIER_GT70 = 'gt70';
|
|
||||||
private const TIER_60_70 = '60_70';
|
|
||||||
private const TIER_50_60 = '50_60';
|
|
||||||
|
|
||||||
/** 各档位当日每人分配上限(按优先级排列,先高档后低档) */
|
|
||||||
private const TIER_DAILY_CAPS = [
|
|
||||||
self::TIER_GT70 => 3,
|
|
||||||
self::TIER_60_70 => 2,
|
|
||||||
self::TIER_50_60 => 1,
|
|
||||||
];
|
|
||||||
|
|
||||||
private const TIER_LABELS = [
|
|
||||||
self::TIER_GT70 => '>70%',
|
|
||||||
self::TIER_60_70 => '60%~70%',
|
|
||||||
self::TIER_50_60 => '50%~60%',
|
|
||||||
];
|
|
||||||
|
|
||||||
protected function configure()
|
|
||||||
{
|
|
||||||
$this->setName('tcm:auto-assign-pending')
|
|
||||||
->setDescription('待分配诊单自动指派:按上月二诊复诊接诊率分档轮询分配,并写入自动指派日志')
|
|
||||||
->addOption('dry-run', null, Option::VALUE_NONE, '演练模式:只输出分配计划,不写库');
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function execute(Input $input, Output $output): int
|
|
||||||
{
|
|
||||||
$now = time();
|
|
||||||
$dryRun = (bool) $input->getOption('dry-run');
|
|
||||||
$runDate = date('Y-m-d', $now);
|
|
||||||
$statMonth = date('Y-m', strtotime(date('Y-m-01', $now) . ' -1 month'));
|
|
||||||
$batchNo = date('YmdHis', $now) . str_pad((string) random_int(0, 9999), 4, '0', STR_PAD_LEFT);
|
|
||||||
|
|
||||||
$output->writeln(sprintf(
|
|
||||||
'[%s] 开始自动指派待分配诊单,批次=%s,统计月=%s%s',
|
|
||||||
date('Y-m-d H:i:s', $now),
|
|
||||||
$batchNo,
|
|
||||||
$statMonth,
|
|
||||||
$dryRun ? '(演练模式,不写库)'
|
|
||||||
: ''
|
|
||||||
));
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 1. 上月二诊复诊接诊率 → 医助分档
|
|
||||||
$tiers = $this->buildAssistantTiers($statMonth);
|
|
||||||
$tierTotal = array_sum(array_map('count', $tiers));
|
|
||||||
$output->writeln(sprintf(
|
|
||||||
'医助分档:>70%% 共 %d 人,60%%~70%% 共 %d 人,50%%~60%% 共 %d 人',
|
|
||||||
\count($tiers[self::TIER_GT70]),
|
|
||||||
\count($tiers[self::TIER_60_70]),
|
|
||||||
\count($tiers[self::TIER_50_60])
|
|
||||||
));
|
|
||||||
|
|
||||||
// 2. 当日剩余额度(扣减当日已自动分配数,防止同日重复执行超额)
|
|
||||||
$remaining = $this->buildRemainingQuota($tiers, $runDate);
|
|
||||||
|
|
||||||
// 3. 待指派池:与「待分配医助」Tab 同口径(assistant_id 空/0 + 当月有业务订单),先到先分
|
|
||||||
[$pool, $ineligible] = $this->fetchPendingPool($now);
|
|
||||||
$output->writeln(sprintf('待指派池:符合条件 %d 条,不符合条件 %d 条', \count($pool), \count($ineligible)));
|
|
||||||
|
|
||||||
$logRows = [];
|
|
||||||
|
|
||||||
// 不符合条件的待指派诊单:不分配,逐条记录原因
|
|
||||||
foreach ($ineligible as $item) {
|
|
||||||
$logRows[] = $this->buildLogRow($batchNo, $runDate, $statMonth, $item['diag'], [
|
|
||||||
'action' => 0,
|
|
||||||
'reason' => $item['reason'],
|
|
||||||
], $now);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($pool === []) {
|
|
||||||
$this->flushLogs($logRows, $dryRun, $output);
|
|
||||||
$output->writeln('待指派池为空,本次无需分配。');
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($tierTotal === 0) {
|
|
||||||
$reason = sprintf('未分配:上月(%s)无二诊复诊接诊率≥50%%的医助,本批次不执行分配', $statMonth);
|
|
||||||
foreach ($pool as $diag) {
|
|
||||||
$logRows[] = $this->buildLogRow($batchNo, $runDate, $statMonth, $diag, [
|
|
||||||
'action' => 0,
|
|
||||||
'reason' => $reason,
|
|
||||||
], $now);
|
|
||||||
}
|
|
||||||
$this->flushLogs($logRows, $dryRun, $output);
|
|
||||||
$output->writeln($reason);
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. 轮询排分配计划
|
|
||||||
[$plan, $leftover] = $this->buildAssignPlan($pool, $tiers, $remaining);
|
|
||||||
|
|
||||||
// 5. 执行计划(逐条事务 + 行锁复核,落库诊单/指派日志/自动指派日志)
|
|
||||||
$assigned = 0;
|
|
||||||
$skipped = 0;
|
|
||||||
foreach ($plan as $item) {
|
|
||||||
if ($dryRun) {
|
|
||||||
$assigned++;
|
|
||||||
$output->writeln(sprintf(
|
|
||||||
'[演练] 诊单#%d(%s) → %s(%s,第%d轮,当日第%d/%d条)',
|
|
||||||
$item['diagnosis']['id'],
|
|
||||||
(string) $item['diagnosis']['patient_name'],
|
|
||||||
$item['assistant']['name'],
|
|
||||||
self::TIER_LABELS[$item['tier']],
|
|
||||||
$item['round'],
|
|
||||||
$item['day_seq'],
|
|
||||||
self::TIER_DAILY_CAPS[$item['tier']]
|
|
||||||
));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$ok = $this->applyAssignment($item, $now);
|
|
||||||
if ($ok) {
|
|
||||||
$assigned++;
|
|
||||||
$logRows[] = $this->buildLogRow($batchNo, $runDate, $statMonth, $item['diagnosis'], [
|
|
||||||
'action' => 1,
|
|
||||||
'assistant_id' => $item['assistant']['id'],
|
|
||||||
'assistant_name' => $item['assistant']['name'],
|
|
||||||
'tier' => $item['tier'],
|
|
||||||
'visit2_rate' => $item['assistant']['rate'],
|
|
||||||
'round_no' => $item['round'],
|
|
||||||
'reason' => sprintf(
|
|
||||||
'已分配给医助[%s](ID:%d):上月(%s)二诊复诊接诊率 %.2f%%,档位[%s](日上限%d条),第 %d 轮轮询分得,当日该医助第 %d 条',
|
|
||||||
$item['assistant']['name'],
|
|
||||||
$item['assistant']['id'],
|
|
||||||
$statMonth,
|
|
||||||
$item['assistant']['rate'],
|
|
||||||
self::TIER_LABELS[$item['tier']],
|
|
||||||
self::TIER_DAILY_CAPS[$item['tier']],
|
|
||||||
$item['round'],
|
|
||||||
$item['day_seq']
|
|
||||||
),
|
|
||||||
], $now);
|
|
||||||
} else {
|
|
||||||
$skipped++;
|
|
||||||
$logRows[] = $this->buildLogRow($batchNo, $runDate, $statMonth, $item['diagnosis'], [
|
|
||||||
'action' => 0,
|
|
||||||
'reason' => '未分配:执行时诊单已被指派给其他医助(并发/人工抢先),本次跳过',
|
|
||||||
], $now);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 6. 额度用尽后剩余的待指派诊单:不分配,记录原因
|
|
||||||
foreach ($leftover as $diag) {
|
|
||||||
$logRows[] = $this->buildLogRow($batchNo, $runDate, $statMonth, $diag, [
|
|
||||||
'action' => 0,
|
|
||||||
'reason' => '未分配:各档位医助当日剩余额度已用尽(>70%每人3条、60%~70%每人2条、50%~60%每人1条),顺延至下次执行',
|
|
||||||
], $now);
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->flushLogs($logRows, $dryRun, $output);
|
|
||||||
|
|
||||||
$output->writeln(sprintf(
|
|
||||||
'执行完成。计划分配: %d, 实际分配: %d, 并发跳过: %d, 额度不足未分: %d, 不符合条件: %d',
|
|
||||||
\count($plan),
|
|
||||||
$assigned,
|
|
||||||
$skipped,
|
|
||||||
\count($leftover),
|
|
||||||
\count($ineligible)
|
|
||||||
));
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
Log::error('待分配诊单自动指派异常: ' . $e->getMessage() . ' @ ' . $e->getFile() . ':' . $e->getLine());
|
|
||||||
$output->error('执行异常: ' . $e->getMessage());
|
|
||||||
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 按上月二诊复诊接诊率把医助分档(复用 RevisitRateLogic 口径),并剔除已禁用/已删除账号。
|
|
||||||
*
|
|
||||||
* @return array{gt70: list<array{id:int,name:string,rate:float}>, 60_70: list<array{id:int,name:string,rate:float}>, 50_60: list<array{id:int,name:string,rate:float}>}
|
|
||||||
*/
|
|
||||||
private function buildAssistantTiers(string $statMonth): array
|
|
||||||
{
|
|
||||||
$overview = RevisitRateLogic::overview(['month' => $statMonth]);
|
|
||||||
|
|
||||||
/** @var list<array{id:int,name:string,rate:float}> $candidates */
|
|
||||||
$candidates = [];
|
|
||||||
foreach ($overview['rows'] ?? [] as $deptRow) {
|
|
||||||
foreach ($deptRow['children'] ?? [] as $row) {
|
|
||||||
$aid = (int) ($row['assistant_id'] ?? 0);
|
|
||||||
$rate = $row['visit2_rate'] ?? null;
|
|
||||||
// 上月无被指派数据(rate=null)或接诊率低于 50% 的医助不参与分配
|
|
||||||
if ($aid <= 0 || $rate === null || (float) $rate < 50.0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$candidates[] = [
|
|
||||||
'id' => $aid,
|
|
||||||
'name' => (string) ($row['assistant_name'] ?? ('#' . $aid)),
|
|
||||||
'rate' => (float) $rate,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ($candidates === []) {
|
|
||||||
return [self::TIER_GT70 => [], self::TIER_60_70 => [], self::TIER_50_60 => []];
|
|
||||||
}
|
|
||||||
|
|
||||||
// 只分配给当前在职可用的医助账号(role_id=2 且未禁用未删除),
|
|
||||||
// 且当前部门须在「二中心」子树内(统计月在二中心、后来调离的不再参与)
|
|
||||||
$erDeptSet = DeptLogic::getErCenterSubtreeDeptIdSet();
|
|
||||||
if ($erDeptSet === []) {
|
|
||||||
return [self::TIER_GT70 => [], self::TIER_60_70 => [], self::TIER_50_60 => []];
|
|
||||||
}
|
|
||||||
$activeIds = Db::name('admin')
|
|
||||||
->alias('a')
|
|
||||||
->join('admin_role ar', 'a.id = ar.admin_id')
|
|
||||||
->join('admin_dept ad', 'a.id = ad.admin_id')
|
|
||||||
->where('ar.role_id', 2)
|
|
||||||
->where('a.disable', 0)
|
|
||||||
->whereNull('a.delete_time')
|
|
||||||
->whereIn('ad.dept_id', array_keys($erDeptSet))
|
|
||||||
->whereIn('a.id', array_column($candidates, 'id'))
|
|
||||||
->group('a.id')
|
|
||||||
->column('a.id');
|
|
||||||
$activeSet = array_fill_keys(array_map('intval', $activeIds), true);
|
|
||||||
|
|
||||||
$tiers = [self::TIER_GT70 => [], self::TIER_60_70 => [], self::TIER_50_60 => []];
|
|
||||||
foreach ($candidates as $c) {
|
|
||||||
if (!isset($activeSet[$c['id']])) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if ($c['rate'] > 70.0) {
|
|
||||||
$tiers[self::TIER_GT70][] = $c;
|
|
||||||
} elseif ($c['rate'] > 60.0) {
|
|
||||||
$tiers[self::TIER_60_70][] = $c;
|
|
||||||
} else { // 50 <= rate <= 60
|
|
||||||
$tiers[self::TIER_50_60][] = $c;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 档内按接诊率降序、id 升序,保证分配顺序确定可复现
|
|
||||||
foreach ($tiers as &$list) {
|
|
||||||
usort($list, static function (array $a, array $b): int {
|
|
||||||
if ($a['rate'] !== $b['rate']) {
|
|
||||||
return $b['rate'] <=> $a['rate'];
|
|
||||||
}
|
|
||||||
|
|
||||||
return $a['id'] <=> $b['id'];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
unset($list);
|
|
||||||
|
|
||||||
return $tiers;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 各医助当日剩余额度 = 档位日上限 - 当日已自动分配条数(同日重复执行不超额)。
|
|
||||||
*
|
|
||||||
* @param array<string, list<array{id:int,name:string,rate:float}>> $tiers
|
|
||||||
*
|
|
||||||
* @return array<int, int> assistant_id => 剩余额度
|
|
||||||
*/
|
|
||||||
private function buildRemainingQuota(array $tiers, string $runDate): array
|
|
||||||
{
|
|
||||||
$usedToday = Db::name('tcm_diagnosis_auto_assign_log')
|
|
||||||
->where('run_date', $runDate)
|
|
||||||
->where('action', 1)
|
|
||||||
->where('assistant_id', '>', 0)
|
|
||||||
->group('assistant_id')
|
|
||||||
->column('COUNT(*)', 'assistant_id');
|
|
||||||
|
|
||||||
$remaining = [];
|
|
||||||
foreach (self::TIER_DAILY_CAPS as $tier => $cap) {
|
|
||||||
foreach ($tiers[$tier] as $assistant) {
|
|
||||||
$used = (int) ($usedToday[$assistant['id']] ?? 0);
|
|
||||||
$remaining[$assistant['id']] = max(0, $cap - $used);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $remaining;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 待指派池:与后台「待分配医助」Tab 同口径 —— assistant_id 为空/0、未删除,
|
|
||||||
* 且当月内存在业务订单(order.patient_id = 诊单 id,Tab 默认按当月过滤)。先到先分。
|
|
||||||
* 返回 [符合条件, 不符合条件(附原因)] 两组;不符合条件的诊单不分配,只记日志。
|
|
||||||
*
|
|
||||||
* @return array{0: list<array<string,mixed>>, 1: list<array{diag:array<string,mixed>,reason:string}>}
|
|
||||||
*/
|
|
||||||
private function fetchPendingPool(int $now): array
|
|
||||||
{
|
|
||||||
$rows = Db::name('tcm_diagnosis')
|
|
||||||
->whereRaw('(assistant_id IS NULL OR assistant_id = 0)')
|
|
||||||
->whereNull('delete_time')
|
|
||||||
->field(['id', 'patient_name', 'phone', 'status', 'create_time'])
|
|
||||||
->order(['create_time' => 'asc', 'id' => 'asc'])
|
|
||||||
->select()
|
|
||||||
->toArray();
|
|
||||||
if ($rows === []) {
|
|
||||||
return [[], []];
|
|
||||||
}
|
|
||||||
|
|
||||||
// 当月存在业务订单的诊单集合(order.create_time 兼容整型时间戳与 datetime 字符串,与 DiagnosisLists 一致)
|
|
||||||
$tStart = (int) strtotime(date('Y-m-01 00:00:00', $now));
|
|
||||||
$tEnd = (int) strtotime(date('Y-m-t 23:59:59', $now));
|
|
||||||
$dsStart = date('Y-m-d H:i:s', $tStart);
|
|
||||||
$dsEnd = date('Y-m-d H:i:s', $tEnd);
|
|
||||||
$hasOrderSet = [];
|
|
||||||
foreach (array_chunk(array_column($rows, 'id'), 2000) as $chunk) {
|
|
||||||
$ids = Db::name('order')
|
|
||||||
->whereIn('patient_id', $chunk)
|
|
||||||
->whereNull('delete_time')
|
|
||||||
->where(static function ($q) use ($tStart, $tEnd, $dsStart, $dsEnd) {
|
|
||||||
$q->whereBetween('create_time', [$tStart, $tEnd])
|
|
||||||
->whereOr(static function ($q2) use ($dsStart, $dsEnd) {
|
|
||||||
$q2->where('create_time', '>=', $dsStart)->where('create_time', '<=', $dsEnd);
|
|
||||||
});
|
|
||||||
})
|
|
||||||
->group('patient_id')
|
|
||||||
->column('patient_id');
|
|
||||||
foreach ($ids as $id) {
|
|
||||||
$hasOrderSet[(int) $id] = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$curMonth = date('Y-m', $now);
|
|
||||||
$eligible = [];
|
|
||||||
$ineligible = [];
|
|
||||||
foreach ($rows as $r) {
|
|
||||||
$did = (int) ($r['id'] ?? 0);
|
|
||||||
if (!isset($hasOrderSet[$did])) {
|
|
||||||
$ineligible[] = [
|
|
||||||
'diag' => $r,
|
|
||||||
'reason' => sprintf('未分配:诊单当月(%s)无业务订单,不在「待分配医助」列表范围内,不满足自动分配条件', $curMonth),
|
|
||||||
];
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if ((int) ($r['status'] ?? 0) !== 1) {
|
|
||||||
$ineligible[] = [
|
|
||||||
'diag' => $r,
|
|
||||||
'reason' => '未分配:诊单未启用(status≠1),不满足自动分配条件',
|
|
||||||
];
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$eligible[] = $r;
|
|
||||||
}
|
|
||||||
|
|
||||||
return [$eligible, $ineligible];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 轮询排分配计划:每轮内先 >70% 档每人 1 条,再 60%~70%,再 50%~60%;受各自当日剩余额度约束。
|
|
||||||
*
|
|
||||||
* @param list<array<string,mixed>> $pool 待指派诊单(先到先分)
|
|
||||||
* @param array<string, list<array{id:int,name:string,rate:float}>> $tiers
|
|
||||||
* @param array<int, int> $remaining assistant_id => 剩余额度(会被消耗)
|
|
||||||
*
|
|
||||||
* @return array{
|
|
||||||
* 0: list<array{diagnosis:array<string,mixed>,assistant:array{id:int,name:string,rate:float},tier:string,round:int,day_seq:int}>,
|
|
||||||
* 1: list<array<string,mixed>>
|
|
||||||
* } [分配计划, 额度用尽后剩余诊单]
|
|
||||||
*/
|
|
||||||
private function buildAssignPlan(array $pool, array $tiers, array $remaining): array
|
|
||||||
{
|
|
||||||
$plan = [];
|
|
||||||
$poolIdx = 0;
|
|
||||||
$poolCount = \count($pool);
|
|
||||||
/** @var array<int, int> $daySeq 医助当日已排序号(含历史已用额度) */
|
|
||||||
$daySeq = [];
|
|
||||||
foreach ($remaining as $aid => $left) {
|
|
||||||
// 起始序号 = 日上限 - 剩余额度(同日多次执行时序号衔接)
|
|
||||||
$cap = 0;
|
|
||||||
foreach (self::TIER_DAILY_CAPS as $tier => $tierCap) {
|
|
||||||
foreach ($tiers[$tier] as $assistant) {
|
|
||||||
if ($assistant['id'] === $aid) {
|
|
||||||
$cap = $tierCap;
|
|
||||||
break 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$daySeq[$aid] = $cap - $left;
|
|
||||||
}
|
|
||||||
|
|
||||||
$round = 0;
|
|
||||||
while ($poolIdx < $poolCount) {
|
|
||||||
$round++;
|
|
||||||
$assignedThisRound = 0;
|
|
||||||
foreach (self::TIER_DAILY_CAPS as $tier => $_cap) {
|
|
||||||
foreach ($tiers[$tier] as $assistant) {
|
|
||||||
if ($poolIdx >= $poolCount) {
|
|
||||||
break 2;
|
|
||||||
}
|
|
||||||
$aid = $assistant['id'];
|
|
||||||
if (($remaining[$aid] ?? 0) <= 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$remaining[$aid]--;
|
|
||||||
$daySeq[$aid]++;
|
|
||||||
$plan[] = [
|
|
||||||
'diagnosis' => $pool[$poolIdx],
|
|
||||||
'assistant' => $assistant,
|
|
||||||
'tier' => $tier,
|
|
||||||
'round' => $round,
|
|
||||||
'day_seq' => $daySeq[$aid],
|
|
||||||
];
|
|
||||||
$poolIdx++;
|
|
||||||
$assignedThisRound++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ($assignedThisRound === 0) {
|
|
||||||
// 所有医助额度用尽,剩余诊单不再分配
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return [$plan, \array_slice($pool, $poolIdx)];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 落库单条分配:事务 + 行锁复核诊单仍未指派,更新诊单并写指派日志(与手动指派 DiagnosisLogic::assign 同口径)。
|
|
||||||
*
|
|
||||||
* @param array{diagnosis:array<string,mixed>,assistant:array{id:int,name:string,rate:float}} $item
|
|
||||||
*/
|
|
||||||
private function applyAssignment(array $item, int $now): bool
|
|
||||||
{
|
|
||||||
$diagnosisId = (int) $item['diagnosis']['id'];
|
|
||||||
$toAssistantId = (int) $item['assistant']['id'];
|
|
||||||
|
|
||||||
Db::startTrans();
|
|
||||||
try {
|
|
||||||
$diagLock = Db::name('tcm_diagnosis')
|
|
||||||
->where('id', $diagnosisId)
|
|
||||||
->whereNull('delete_time')
|
|
||||||
->lock(true)
|
|
||||||
->field(['id', 'assistant_id'])
|
|
||||||
->find();
|
|
||||||
if ($diagLock === null || $diagLock === [] || (int) ($diagLock['assistant_id'] ?? 0) > 0) {
|
|
||||||
Db::rollback();
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
Db::name('tcm_diagnosis')
|
|
||||||
->where('id', $diagnosisId)
|
|
||||||
->whereNull('delete_time')
|
|
||||||
->update([
|
|
||||||
'assistant_id' => $toAssistantId,
|
|
||||||
'assign_read_at' => null,
|
|
||||||
]);
|
|
||||||
|
|
||||||
$poSnap = Db::name('tcm_prescription_order')
|
|
||||||
->where('diagnosis_id', $diagnosisId)
|
|
||||||
->whereNull('delete_time')
|
|
||||||
->order(['create_time' => 'desc', 'id' => 'desc'])
|
|
||||||
->field(['creator_id', 'create_time'])
|
|
||||||
->find();
|
|
||||||
$relatedPoCreatorId = (int) ($poSnap['creator_id'] ?? 0);
|
|
||||||
$relatedPoCreateTime = (int) ($poSnap['create_time'] ?? 0);
|
|
||||||
if ($relatedPoCreateTime <= 0) {
|
|
||||||
$relatedPoCreateTime = $now;
|
|
||||||
$relatedPoCreatorId = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
Db::name('tcm_diagnosis_assign_log')->insert([
|
|
||||||
'diagnosis_id' => $diagnosisId,
|
|
||||||
'from_assistant_id' => 0,
|
|
||||||
'to_assistant_id' => $toAssistantId,
|
|
||||||
'operator_admin_id' => 0,
|
|
||||||
'operator_name' => '系统自动分配',
|
|
||||||
'operator_account' => 'system',
|
|
||||||
'ip' => '',
|
|
||||||
'related_po_creator_id' => $relatedPoCreatorId,
|
|
||||||
'related_po_create_time' => $relatedPoCreateTime,
|
|
||||||
'is_inherit' => 0,
|
|
||||||
'create_time' => $now,
|
|
||||||
]);
|
|
||||||
|
|
||||||
Db::commit();
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
Db::rollback();
|
|
||||||
Log::error(sprintf('自动指派落库失败 diagnosis_id=%d assistant_id=%d msg=%s', $diagnosisId, $toAssistantId, $e->getMessage()));
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array<string,mixed> $diag 诊单行(含 id/patient_name/phone)
|
|
||||||
* @param array<string,mixed> $extra action/assistant_id/assistant_name/tier/visit2_rate/round_no/reason
|
|
||||||
*
|
|
||||||
* @return array<string,mixed>
|
|
||||||
*/
|
|
||||||
private function buildLogRow(string $batchNo, string $runDate, string $statMonth, array $diag, array $extra, int $now): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'batch_no' => $batchNo,
|
|
||||||
'run_date' => $runDate,
|
|
||||||
'stat_month' => $statMonth,
|
|
||||||
'diagnosis_id' => (int) ($diag['id'] ?? 0),
|
|
||||||
'patient_name' => mb_substr(trim((string) ($diag['patient_name'] ?? '')), 0, 64),
|
|
||||||
'patient_phone' => mb_substr(trim((string) ($diag['phone'] ?? '')), 0, 32),
|
|
||||||
'action' => (int) ($extra['action'] ?? 0),
|
|
||||||
'assistant_id' => (int) ($extra['assistant_id'] ?? 0),
|
|
||||||
'assistant_name' => mb_substr((string) ($extra['assistant_name'] ?? ''), 0, 64),
|
|
||||||
'tier' => (string) ($extra['tier'] ?? ''),
|
|
||||||
'visit2_rate' => $extra['visit2_rate'] ?? null,
|
|
||||||
'round_no' => (int) ($extra['round_no'] ?? 0),
|
|
||||||
'reason' => mb_substr((string) ($extra['reason'] ?? ''), 0, 500),
|
|
||||||
'create_time' => $now,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param list<array<string,mixed>> $logRows
|
|
||||||
*/
|
|
||||||
private function flushLogs(array $logRows, bool $dryRun, Output $output): void
|
|
||||||
{
|
|
||||||
if ($logRows === []) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if ($dryRun) {
|
|
||||||
$output->writeln(sprintf('[演练] 应写入自动指派日志 %d 条(未落库)', \count($logRows)));
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
foreach (array_chunk($logRows, 500) as $chunk) {
|
|
||||||
Db::name('tcm_diagnosis_auto_assign_log')->insertAll($chunk);
|
|
||||||
}
|
|
||||||
$output->writeln(sprintf('已写入自动指派日志 %d 条', \count($logRows)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -40,7 +40,5 @@ return [
|
|||||||
'migrate:images-to-doctor-note' => 'app\\command\\MigrateImagesToDoctorNote',
|
'migrate:images-to-doctor-note' => 'app\\command\\MigrateImagesToDoctorNote',
|
||||||
// 诊单待办事项:扫描到点的待执行项并向创建人发送企业微信消息
|
// 诊单待办事项:扫描到点的待执行项并向创建人发送企业微信消息
|
||||||
'tcm:diagnosis-todo-notify' => 'app\\command\\DiagnosisTodoNotify',
|
'tcm:diagnosis-todo-notify' => 'app\\command\\DiagnosisTodoNotify',
|
||||||
// 待分配诊单自动指派:按上月二诊复诊接诊率分档轮询分配(>70% 3条/60~70% 2条/50~60% 1条),写自动指派日志
|
|
||||||
'tcm:auto-assign-pending' => 'app\\command\\AutoAssignPendingDiagnosis',
|
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 64 KiB |
+1
-1
@@ -1 +1 @@
|
|||||||
import r from"./error-B96pOmhv.js";import{f as p,ak as i,I as m,a as e,aN as s,J as o}from"./@vue/runtime-core-C6bnekPw.js";import"./element-plus-Bolc0EfP.js";import"./@vue/runtime-dom-DDAG46FW.js";import"./lodash-D3kF6u-c.js";import"./@vue/shared-mAAVTE9n.js";import"./@vue/reactivity-DiY1c2vO.js";import"./@element-plus/icons-vue-B0jSCQ-G.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./vue-router-QlpZ4wdW.js";import"./index-CzSP4TPL.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./@vueuse/core-1S4fJlii.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";const a="/admin/assets/no_perms-jDxcYpYC.png",n={class:"error404"},W=p({__name:"403",setup(c){return(_,t)=>(i(),m("div",n,[e(r,{code:"403",title:"您的账号权限不足,请联系管理员添加权限!","show-btn":!1},{content:s(()=>[...t[0]||(t[0]=[o("div",{class:"flex justify-center"},[o("img",{class:"w-[150px] h-[150px]",src:a,alt:""})],-1)])]),_:1})]))}});export{W as default};
|
import r from"./error-BUBKvVs4.js";import{f as p,ak as i,I as m,a as e,aN as s,J as o}from"./@vue/runtime-core-C6bnekPw.js";import"./element-plus-Bolc0EfP.js";import"./@vue/runtime-dom-DDAG46FW.js";import"./lodash-D3kF6u-c.js";import"./@vue/shared-mAAVTE9n.js";import"./@vue/reactivity-DiY1c2vO.js";import"./@element-plus/icons-vue-B0jSCQ-G.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./vue-router-QlpZ4wdW.js";import"./index-B6p-ZV3k.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./@vueuse/core-1S4fJlii.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";const a="/admin/assets/no_perms-jDxcYpYC.png",n={class:"error404"},W=p({__name:"403",setup(c){return(_,t)=>(i(),m("div",n,[e(r,{code:"403",title:"您的账号权限不足,请联系管理员添加权限!","show-btn":!1},{content:s(()=>[...t[0]||(t[0]=[o("div",{class:"flex justify-center"},[o("img",{class:"w-[150px] h-[150px]",src:a,alt:""})],-1)])]),_:1})]))}});export{W as default};
|
||||||
+1
-1
@@ -1 +1 @@
|
|||||||
import o from"./error-B96pOmhv.js";import{f as r,ak as t,I as m,a as p}from"./@vue/runtime-core-C6bnekPw.js";import"./element-plus-Bolc0EfP.js";import"./@vue/runtime-dom-DDAG46FW.js";import"./lodash-D3kF6u-c.js";import"./@vue/shared-mAAVTE9n.js";import"./@vue/reactivity-DiY1c2vO.js";import"./@element-plus/icons-vue-B0jSCQ-G.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./vue-router-QlpZ4wdW.js";import"./index-CzSP4TPL.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./@vueuse/core-1S4fJlii.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";const i={class:"error404"},T=r({__name:"404",setup(e){return(a,s)=>(t(),m("div",i,[p(o,{code:"404",title:"哎呀,出错了!您访问的页面不存在…"})]))}});export{T as default};
|
import o from"./error-BUBKvVs4.js";import{f as r,ak as t,I as m,a as p}from"./@vue/runtime-core-C6bnekPw.js";import"./element-plus-Bolc0EfP.js";import"./@vue/runtime-dom-DDAG46FW.js";import"./lodash-D3kF6u-c.js";import"./@vue/shared-mAAVTE9n.js";import"./@vue/reactivity-DiY1c2vO.js";import"./@element-plus/icons-vue-B0jSCQ-G.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./vue-router-QlpZ4wdW.js";import"./index-B6p-ZV3k.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./@vueuse/core-1S4fJlii.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";const i={class:"error404"},T=r({__name:"404",setup(e){return(a,s)=>(t(),m("div",i,[p(o,{code:"404",title:"哎呀,出错了!您访问的页面不存在…"})]))}});export{T as default};
|
||||||
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
@@ -1 +1 @@
|
|||||||
import{M as T,N as C,T as $,r as D,d as L,L as k}from"./element-plus-Bolc0EfP.js";import{Y as E}from"./@element-plus/icons-vue-B0jSCQ-G.js";import{a8 as P}from"./tcm-B2eDN42y.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-CzSP4TPL.js";import"./@vue/runtime-dom-DDAG46FW.js";import"./lodash-D3kF6u-c.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./vue-router-QlpZ4wdW.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./@vueuse/core-1S4fJlii.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";const Y={class:"assign-log-panel"},q={key:1,class:"text-gray-400"},z=A({__name:"AssignLogPanel",props:{diagnosisId:{}},setup(N,{expose:v}){const _=N,d=w(!1),u=w([]);function y(o){const e=o.related_po_creator_name;if(e!=null&&String(e).trim()!=="")return String(e);const t=Number(o.related_po_creator_id);return Number.isFinite(t)&&t>0?`ID:${t}`:"—"}function x(o){const e=o.related_po_create_time_text;if(e!=null&&String(e).trim()!=="")return String(e);const t=Number(o.related_po_create_time);if(!Number.isFinite(t)||t<=0)return"—";const a=new Date(t*1e3);if(Number.isNaN(a.getTime()))return"—";const r=l=>String(l).padStart(2,"0");return`${a.getFullYear()}-${r(a.getMonth()+1)}-${r(a.getDate())} ${r(a.getHours())}:${r(a.getMinutes())}:${r(a.getSeconds())}`}function f(o,e){const t=e==="from"?"from_assistant_name":"to_assistant_name",a=e==="from"?"from_assistant_id":"to_assistant_id",r=o[t];if(r!=null&&String(r).trim()!==""&&String(r)!=="—")return String(r);const l=Number(o[a]);return Number.isFinite(l)&&l>0?`ID:${l}`:"—"}const g=async()=>{if(_.diagnosisId){d.value=!0;try{const o=await P({id:_.diagnosisId}),e=Array.isArray(o)?o:[];u.value=e}catch(o){console.error(o),u.value=[]}finally{d.value=!1}}};return B(()=>_.diagnosisId,()=>{g()},{immediate:!0}),v({refresh:g}),(o,e)=>{const t=C,a=$,r=L,l=D,S=T,I=k;return p(),b("div",Y,[F((p(),h(S,{data:u.value,border:"",stripe:"","empty-text":"暂无指派记录"},{default:n(()=>[i(t,{label:"操作时间",width:"175",prop:"create_time_text"}),i(t,{label:"原医助","min-width":"120"},{default:n(({row:s})=>[m(c(f(s,"from")),1)]),_:1}),i(t,{label:"新医助","min-width":"120"},{default:n(({row:s})=>[m(c(f(s,"to")),1)]),_:1}),i(t,{label:"继承",width:"72",align:"center"},{default:n(({row:s})=>[Number(s.is_inherit)===1?(p(),h(a,{key:0,type:"success",size:"small"},{default:n(()=>[...e[0]||(e[0]=[m("是",-1)])]),_:1})):(p(),b("span",q,"否"))]),_:1}),i(t,{label:"快照·业务单创建人","min-width":"130","show-overflow-tooltip":""},{default:n(({row:s})=>[m(c(y(s)),1)]),_:1}),i(t,{label:"快照·业务单创建时间",width:"190"},{header:n(()=>[e[1]||(e[1]=M("span",null,"快照·业务单创建时间",-1)),i(l,{placement:"top",content:"related_po_create_time:触发本次操作的处方业务订单 create_time;与原文助一致时表示医助创建订单时间"},{default:n(()=>[i(r,{class:"assign-log-col-hint"},{default:n(()=>[i(V(E))]),_:1})]),_:1})]),default:n(({row:s})=>[m(c(x(s)),1)]),_:1}),i(t,{label:"操作人",width:"110",prop:"operator_name"}),i(t,{label:"操作账号",width:"120",prop:"operator_account","show-overflow-tooltip":""}),i(t,{label:"IP",width:"130",prop:"ip","show-overflow-tooltip":""})]),_:1},8,["data"])),[[I,d.value]])])}}}),Ct=K(z,[["__scopeId","data-v-f670e3e6"]]);export{Ct as default};
|
import{M as T,N as C,T as $,r as D,d as L,L as k}from"./element-plus-Bolc0EfP.js";import{Y as E}from"./@element-plus/icons-vue-B0jSCQ-G.js";import{a6 as P}from"./tcm-Ba4j6pRH.js";import{f as A,w as B,ak as p,I as b,aP as F,G as h,aN as n,a as i,O as m,J as M}from"./@vue/runtime-core-C6bnekPw.js";import{Q as c}from"./@vue/shared-mAAVTE9n.js";import{y as V,n as w}from"./@vue/reactivity-DiY1c2vO.js";import{_ as K}from"./index-B6p-ZV3k.js";import"./@vue/runtime-dom-DDAG46FW.js";import"./lodash-D3kF6u-c.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./vue-router-QlpZ4wdW.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./@vueuse/core-1S4fJlii.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";const Y={class:"assign-log-panel"},q={key:1,class:"text-gray-400"},z=A({__name:"AssignLogPanel",props:{diagnosisId:{}},setup(N,{expose:v}){const _=N,d=w(!1),u=w([]);function y(o){const e=o.related_po_creator_name;if(e!=null&&String(e).trim()!=="")return String(e);const t=Number(o.related_po_creator_id);return Number.isFinite(t)&&t>0?`ID:${t}`:"—"}function x(o){const e=o.related_po_create_time_text;if(e!=null&&String(e).trim()!=="")return String(e);const t=Number(o.related_po_create_time);if(!Number.isFinite(t)||t<=0)return"—";const a=new Date(t*1e3);if(Number.isNaN(a.getTime()))return"—";const r=l=>String(l).padStart(2,"0");return`${a.getFullYear()}-${r(a.getMonth()+1)}-${r(a.getDate())} ${r(a.getHours())}:${r(a.getMinutes())}:${r(a.getSeconds())}`}function f(o,e){const t=e==="from"?"from_assistant_name":"to_assistant_name",a=e==="from"?"from_assistant_id":"to_assistant_id",r=o[t];if(r!=null&&String(r).trim()!==""&&String(r)!=="—")return String(r);const l=Number(o[a]);return Number.isFinite(l)&&l>0?`ID:${l}`:"—"}const g=async()=>{if(_.diagnosisId){d.value=!0;try{const o=await P({id:_.diagnosisId}),e=Array.isArray(o)?o:[];u.value=e}catch(o){console.error(o),u.value=[]}finally{d.value=!1}}};return B(()=>_.diagnosisId,()=>{g()},{immediate:!0}),v({refresh:g}),(o,e)=>{const t=C,a=$,r=L,l=D,S=T,I=k;return p(),b("div",Y,[F((p(),h(S,{data:u.value,border:"",stripe:"","empty-text":"暂无指派记录"},{default:n(()=>[i(t,{label:"操作时间",width:"175",prop:"create_time_text"}),i(t,{label:"原医助","min-width":"120"},{default:n(({row:s})=>[m(c(f(s,"from")),1)]),_:1}),i(t,{label:"新医助","min-width":"120"},{default:n(({row:s})=>[m(c(f(s,"to")),1)]),_:1}),i(t,{label:"继承",width:"72",align:"center"},{default:n(({row:s})=>[Number(s.is_inherit)===1?(p(),h(a,{key:0,type:"success",size:"small"},{default:n(()=>[...e[0]||(e[0]=[m("是",-1)])]),_:1})):(p(),b("span",q,"否"))]),_:1}),i(t,{label:"快照·业务单创建人","min-width":"130","show-overflow-tooltip":""},{default:n(({row:s})=>[m(c(y(s)),1)]),_:1}),i(t,{label:"快照·业务单创建时间",width:"190"},{header:n(()=>[e[1]||(e[1]=M("span",null,"快照·业务单创建时间",-1)),i(l,{placement:"top",content:"related_po_create_time:触发本次操作的处方业务订单 create_time;与原文助一致时表示医助创建订单时间"},{default:n(()=>[i(r,{class:"assign-log-col-hint"},{default:n(()=>[i(V(E))]),_:1})]),_:1})]),default:n(({row:s})=>[m(c(x(s)),1)]),_:1}),i(t,{label:"操作人",width:"110",prop:"operator_name"}),i(t,{label:"操作账号",width:"120",prop:"operator_account","show-overflow-tooltip":""}),i(t,{label:"IP",width:"130",prop:"ip","show-overflow-tooltip":""})]),_:1},8,["data"])),[[I,d.value]])])}}}),Ct=K(z,[["__scopeId","data-v-f670e3e6"]]);export{Ct as default};
|
||||||
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
@@ -1 +1 @@
|
|||||||
import{i as L,L as T,N as V,T as D,M as z,W as M}from"./element-plus-Bolc0EfP.js";import{ag as O}from"./tcm-B2eDN42y.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-CzSP4TPL.js";import"./@vue/runtime-dom-DDAG46FW.js";import"./lodash-D3kF6u-c.js";import"./@element-plus/icons-vue-B0jSCQ-G.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./vue-router-QlpZ4wdW.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./@vueuse/core-1S4fJlii.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";const J={class:"case-record-list"},Q={key:0,class:"mb-3 flex justify-end"},W={key:0},Y={key:1,class:"text-gray-400"},q={class:"void-detail text-xs text-gray-500 mt-1"},K=P({__name:"CaseRecordList",props:{diagnosisId:{type:Number,default:0},readOnly:{type:Boolean,default:!1}},emits:["view","openPrescription"],setup(y,{expose:x,emit:C}){const _=y,h=C,l=k([]),p=k(!1),u=async()=>{if(_.diagnosisId){p.value=!0;try{const e=await O({diagnosis_id:_.diagnosisId});l.value=Array.isArray(e)?e:[]}catch(e){console.error("获取病历记录失败:",e),l.value=[]}finally{p.value=!1}}},S=e=>{if(!e)return"";const t=new Date(e*1e3);return`${t.getFullYear()}-${String(t.getMonth()+1).padStart(2,"0")}-${String(t.getDate()).padStart(2,"0")} ${String(t.getHours()).padStart(2,"0")}:${String(t.getMinutes()).padStart(2,"0")}`},$=e=>{h("view",e)},B=()=>{h("openPrescription")};return F(()=>{u()}),j(()=>_.diagnosisId,()=>{u()}),x({refresh:u}),(e,t)=>{const b=L,r=V,v=D,E=z,N=M,I=T;return a(),n("div",J,[y.readOnly?w("",!0):(a(),n("div",Q,[i(b,{type:"primary",size:"small",onClick:B},{default:s(()=>[...t[0]||(t[0]=[m("开方",-1)])]),_:1})])),A((a(),g(E,{data:d(l),border:""},{default:s(()=>[i(r,{prop:"prescription_date",label:"就诊日期",width:"120"}),i(r,{prop:"visit_no",label:"门诊号",width:"120"}),i(r,{prop:"clinical_diagnosis",label:"临床诊断","min-width":"160","show-overflow-tooltip":""}),i(r,{label:"处方摘要","min-width":"180"},{default:s(({row:o})=>[o.herbs&&o.herbs.length?(a(),n("span",W,c(o.herbs.slice(0,3).map(f=>`${f.name}${f.dosage}克`).join("、"))+c(o.herbs.length>3?"...":""),1)):(a(),n("span",Y,"—"))]),_:1}),i(r,{prop:"doctor_name",label:"医师",width:"90","show-overflow-tooltip":""}),i(r,{label:"状态",width:"140",align:"center"},{default:s(({row:o})=>[o.void_status===1?(a(),n(H,{key:0},[i(v,{type:"danger",size:"small"},{default:s(()=>[...t[1]||(t[1]=[m("已作废",-1)])]),_:1}),R("div",q,c(o.void_by_name||"—")+" "+c(S(o.void_time)),1)],64)):(a(),g(v,{key:1,type:"success",size:"small"},{default:s(()=>[...t[2]||(t[2]=[m("正常",-1)])]),_:1}))]),_:1}),i(r,{label:"操作",width:"120",fixed:"right"},{default:s(({row:o})=>[i(b,{link:"",type:"primary",size:"small",onClick:f=>$(o)},{default:s(()=>[...t[3]||(t[3]=[m(" 查看 ",-1)])]),_:1},8,["onClick"])]),_:1})]),_:1},8,["data"])),[[I,d(p)]]),!d(p)&&d(l).length===0?(a(),g(N,{key:1,description:"暂无病历记录,开方后会自动显示",class:"mt-4"})):w("",!0)])}}}),zt=G(K,[["__scopeId","data-v-043d2738"]]);export{zt as default};
|
import{i as L,L as T,N as V,T as D,M as z,W as M}from"./element-plus-Bolc0EfP.js";import{ae as O}from"./tcm-Ba4j6pRH.js";import{f as P,b as F,w as j,ak as a,I as n,a as i,aN as s,O as m,H as w,aP as A,G as g,F as H,J as R}from"./@vue/runtime-core-C6bnekPw.js";import{y as d,n as k}from"./@vue/reactivity-DiY1c2vO.js";import{Q as c}from"./@vue/shared-mAAVTE9n.js";import{_ as G}from"./index-B6p-ZV3k.js";import"./@vue/runtime-dom-DDAG46FW.js";import"./lodash-D3kF6u-c.js";import"./@element-plus/icons-vue-B0jSCQ-G.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./vue-router-QlpZ4wdW.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./@vueuse/core-1S4fJlii.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";const J={class:"case-record-list"},Q={key:0,class:"mb-3 flex justify-end"},W={key:0},Y={key:1,class:"text-gray-400"},q={class:"void-detail text-xs text-gray-500 mt-1"},K=P({__name:"CaseRecordList",props:{diagnosisId:{type:Number,default:0},readOnly:{type:Boolean,default:!1}},emits:["view","openPrescription"],setup(y,{expose:x,emit:C}){const _=y,h=C,l=k([]),p=k(!1),u=async()=>{if(_.diagnosisId){p.value=!0;try{const e=await O({diagnosis_id:_.diagnosisId});l.value=Array.isArray(e)?e:[]}catch(e){console.error("获取病历记录失败:",e),l.value=[]}finally{p.value=!1}}},S=e=>{if(!e)return"";const t=new Date(e*1e3);return`${t.getFullYear()}-${String(t.getMonth()+1).padStart(2,"0")}-${String(t.getDate()).padStart(2,"0")} ${String(t.getHours()).padStart(2,"0")}:${String(t.getMinutes()).padStart(2,"0")}`},$=e=>{h("view",e)},B=()=>{h("openPrescription")};return F(()=>{u()}),j(()=>_.diagnosisId,()=>{u()}),x({refresh:u}),(e,t)=>{const b=L,r=V,v=D,E=z,N=M,I=T;return a(),n("div",J,[y.readOnly?w("",!0):(a(),n("div",Q,[i(b,{type:"primary",size:"small",onClick:B},{default:s(()=>[...t[0]||(t[0]=[m("开方",-1)])]),_:1})])),A((a(),g(E,{data:d(l),border:""},{default:s(()=>[i(r,{prop:"prescription_date",label:"就诊日期",width:"120"}),i(r,{prop:"visit_no",label:"门诊号",width:"120"}),i(r,{prop:"clinical_diagnosis",label:"临床诊断","min-width":"160","show-overflow-tooltip":""}),i(r,{label:"处方摘要","min-width":"180"},{default:s(({row:o})=>[o.herbs&&o.herbs.length?(a(),n("span",W,c(o.herbs.slice(0,3).map(f=>`${f.name}${f.dosage}克`).join("、"))+c(o.herbs.length>3?"...":""),1)):(a(),n("span",Y,"—"))]),_:1}),i(r,{prop:"doctor_name",label:"医师",width:"90","show-overflow-tooltip":""}),i(r,{label:"状态",width:"140",align:"center"},{default:s(({row:o})=>[o.void_status===1?(a(),n(H,{key:0},[i(v,{type:"danger",size:"small"},{default:s(()=>[...t[1]||(t[1]=[m("已作废",-1)])]),_:1}),R("div",q,c(o.void_by_name||"—")+" "+c(S(o.void_time)),1)],64)):(a(),g(v,{key:1,type:"success",size:"small"},{default:s(()=>[...t[2]||(t[2]=[m("正常",-1)])]),_:1}))]),_:1}),i(r,{label:"操作",width:"120",fixed:"right"},{default:s(({row:o})=>[i(b,{link:"",type:"primary",size:"small",onClick:f=>$(o)},{default:s(()=>[...t[3]||(t[3]=[m(" 查看 ",-1)])]),_:1},8,["onClick"])]),_:1})]),_:1},8,["data"])),[[I,d(p)]]),!d(p)&&d(l).length===0?(a(),g(N,{key:1,description:"暂无病历记录,开方后会自动显示",class:"mt-4"})):w("",!0)])}}}),zt=G(K,[["__scopeId","data-v-043d2738"]]);export{zt as default};
|
||||||
+2
-2
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
|||||||
|
import{L as d}from"./element-plus-Bolc0EfP.js";import{f as i,aP as r,ak as e,I as o,aq as s,H as t,J as _}from"./@vue/runtime-core-C6bnekPw.js";const c={class:"admin-data-panel"},m={key:0,class:"admin-data-panel__toolbar"},p={class:"admin-data-panel__body"},f={key:1,class:"admin-data-panel__footer"},k=i({__name:"DataPanel",props:{loading:{type:Boolean,default:!1}},setup(n){return(a,u)=>{const l=d;return r((e(),o("section",c,[a.$slots.toolbar?(e(),o("div",m,[s(a.$slots,"toolbar")])):t("",!0),_("div",p,[s(a.$slots,"default")]),a.$slots.footer?(e(),o("div",f,[s(a.$slots,"footer")])):t("",!0)])),[[l,n.loading]])}}});export{k as _};
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
|||||||
|
import{i as d}from"./element-plus-Bolc0EfP.js";import{f as p,ak as s,I as o,J as n,a as m,aN as _,O as f,H as u,aq as y}from"./@vue/runtime-core-C6bnekPw.js";import{o as k,Q as i}from"./@vue/shared-mAAVTE9n.js";import{y as a,n as b}from"./@vue/reactivity-DiY1c2vO.js";const B={key:0,class:"admin-filter-panel__head"},C={class:"admin-filter-panel__label"},N={class:"admin-filter-panel__body"},E=p({__name:"FilterPanel",props:{collapsible:{type:Boolean,default:!0},title:{default:"筛选条件"}},setup(t){const e=b(!1);return(r,l)=>{const c=d;return s(),o("section",{class:k(["admin-filter-panel",{"is-collapsed":a(e)}])},[t.collapsible?(s(),o("div",B,[n("span",C,i(t.title),1),m(c,{class:"admin-filter-panel__toggle",link:"",type:"primary",onClick:l[0]||(l[0]=h=>e.value=!a(e))},{default:_(()=>[f(i(a(e)?"展开筛选":"收起筛选"),1)]),_:1})])):u("",!0),n("div",N,[y(r.$slots,"default")])],2)}}});export{E as _};
|
||||||
+1
-1
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user